From da095c58f22609ad8ccb7cf45528ecaade1f4717 Mon Sep 17 00:00:00 2001 From: GiMa-SwapKit Date: Thu, 11 Jun 2026 14:38:09 +0200 Subject: [PATCH 1/4] feat: add Cardano wallet extension export --- packages/wallet-extensions/package.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/wallet-extensions/package.json b/packages/wallet-extensions/package.json index c55d0c3..6303328 100644 --- a/packages/wallet-extensions/package.json +++ b/packages/wallet-extensions/package.json @@ -50,6 +50,12 @@ "require": "./dist/src/bitget/index.cjs", "types": "./dist/types/bitget/index.d.ts" }, + "./cardano": { + "bun": "./src/cardano/index.ts", + "default": "./dist/src/cardano/index.js", + "require": "./dist/src/cardano/index.cjs", + "types": "./dist/types/cardano/index.d.ts" + }, "./cosmostation": { "bun": "./src/cosmostation/index.ts", "default": "./dist/src/cosmostation/index.js", From 1ca971c2eb54f0b97f8e04081d431494c5ccb267 Mon Sep 17 00:00:00 2001 From: GiMa-SwapKit Date: Thu, 11 Jun 2026 14:39:01 +0200 Subject: [PATCH 2/4] feat: add Cardano wallet extension helpers --- .../wallet-extensions/src/cardano/index.ts | 318 ++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 packages/wallet-extensions/src/cardano/index.ts diff --git a/packages/wallet-extensions/src/cardano/index.ts b/packages/wallet-extensions/src/cardano/index.ts new file mode 100644 index 0000000..77ad279 --- /dev/null +++ b/packages/wallet-extensions/src/cardano/index.ts @@ -0,0 +1,318 @@ +/** + * Single-file Cardano wallet helpers for dApps. + * + * Covers the dApp-facing parts of: + * - CIP-30: browser wallet bridge: discover, connect, sign, submit + * - CIP-8: message signing via CIP-30 api.signData(address, payloadHex) + * - CIP-45: mobile ↔ desktop QR/deep-link connect URI + injectable P2P RPC adapter + * - CIP-158: open a dApp inside a mobile wallet's in-app browser + */ + +export const CARDANO_CIPS = { + messageSigning: { cip: 8, name: "Message signing" }, + p2pWalletConnection: { cip: 45, name: "Decentralized WebRTC dApp-Wallet Communication" }, + webWalletBridge: { cip: 30, name: "Cardano dApp-Wallet Web Bridge" }, + walletBrowseDeepLink: { cip: 158, name: "Cardano URIs - Browse Application" }, +} as const; + +export type Address = string; +export type CborHex = string; +export type Hash32 = string; +export type HexString = string; + +export type Cip30Extension = { + cip: number; +}; + +export type Cip30Paginate = { + limit: number; + page: number; +}; + +export type Cip30DataSignature = { + key: CborHex; + signature: CborHex; +}; + +export type Cip30WalletApi = { + getBalance(): Promise; + getChangeAddress(): Promise
; + getExtensions?(): Promise; + getNetworkId(): Promise; + getRewardAddresses(): Promise; + getUnusedAddresses(): Promise; + getUsedAddresses(paginate?: Cip30Paginate): Promise; + getUtxos(amount?: CborHex, paginate?: Cip30Paginate): Promise; + signData(address: Address, payload: HexString): Promise; + signTx(tx: CborHex, partialSign?: boolean): Promise; + submitTx(tx: CborHex): Promise; + getCollateral?(params?: { amount?: CborHex; paginate?: Cip30Paginate }): Promise; +}; + +export type Cip30Wallet = { + apiVersion: string; + enable(options?: { extensions?: Cip30Extension[] }): Promise; + icon: string; + isEnabled(): Promise; + name: string; + supportedExtensions?: Cip30Extension[]; +}; + +export type CardanoNamespace = Record; + +export type CardanoWalletInfo = { + id: string; + wallet: Cip30Wallet; +}; + +export type ConnectedCardanoWallet = { + api: Cip30WalletApi; + wallet: Cip30Wallet; + walletId: string; +}; + +export type ConnectCardanoWalletParams = { + /** Pass explicitly in tests/SSR. Defaults to window.cardano in browsers. */ + cardano?: CardanoNamespace; + /** Request CIP-8 support by default because dApps commonly need signData. */ + extensions?: Cip30Extension[]; + walletId: string; +}; + +export class CardanoWalletError extends Error { + constructor( + readonly code: string, + message: string, + ) { + super(message); + this.name = "CardanoWalletError"; + } +} + +declare global { + interface Window { + cardano?: CardanoNamespace; + } +} + +const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null; + +const getInjectedCardano = (): CardanoNamespace | undefined => { + if (typeof window === "undefined") return undefined; + + return window.cardano; +}; + +export const isHex = (value: string) => value.length % 2 === 0 && /^[0-9a-fA-F]*$/.test(value); + +export const assertHex = (value: string, fieldName: string): void => { + if (!isHex(value)) throw new CardanoWalletError("cardano_invalid_hex", `${fieldName} must be an even-length hex string`); +}; + +export const utf8ToHex = (value: string): HexString => { + const bytes = new TextEncoder().encode(value); + + return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join(""); +}; + +export const isCip30Wallet = (wallet: unknown): wallet is Cip30Wallet => { + if (!isRecord(wallet)) return false; + + return ( + typeof wallet.apiVersion === "string" && + typeof wallet.enable === "function" && + typeof wallet.icon === "string" && + typeof wallet.isEnabled === "function" && + typeof wallet.name === "string" + ); +}; + +export const getCardanoWallets = (cardano: CardanoNamespace | undefined = getInjectedCardano()): CardanoWalletInfo[] => + Object.entries(cardano ?? {}).flatMap(([id, wallet]) => (isCip30Wallet(wallet) ? [{ id, wallet }] : [])); + +export const getCardanoWallet = (walletId: string, cardano?: CardanoNamespace): Cip30Wallet => { + const walletInfo = getCardanoWallets(cardano).find(({ id, wallet }) => id === walletId || wallet.name === walletId); + + if (!walletInfo) throw new CardanoWalletError("cardano_wallet_not_found", `Cardano wallet "${walletId}" was not found`); + + return walletInfo.wallet; +}; + +export const connectCardanoWallet = async ({ + cardano, + extensions = [{ cip: CARDANO_CIPS.messageSigning.cip }], + walletId, +}: ConnectCardanoWalletParams): Promise => { + const wallet = getCardanoWallet(walletId, cardano); + const api = await wallet.enable(extensions.length > 0 ? { extensions } : undefined); + + return { api, wallet, walletId }; +}; + +export const connectFirstCardanoWallet = async ({ + cardano = getInjectedCardano(), + extensions = [{ cip: CARDANO_CIPS.messageSigning.cip }], +}: Omit = {}): Promise => { + const [firstWallet] = getCardanoWallets(cardano); + if (!firstWallet) throw new CardanoWalletError("cardano_wallet_not_found", "No CIP-30 Cardano wallet was found"); + + return connectCardanoWallet({ cardano, extensions, walletId: firstWallet.id }); +}; + +export const getPrimaryCardanoAddress = async (api: Cip30WalletApi): Promise
=> { + const usedAddresses = await api.getUsedAddresses(); + const [usedAddress] = usedAddresses; + if (usedAddress) return usedAddress; + + return api.getChangeAddress(); +}; + +export type CardanoMessage = string | { hex: HexString } | { text: string }; + +export const cardanoMessageToHex = (message: CardanoMessage): HexString => { + if (typeof message === "string") return utf8ToHex(message); + if ("text" in message) return utf8ToHex(message.text); + + assertHex(message.hex, "message.hex"); + return message.hex; +}; + +export const signCardanoMessage = async ({ + address, + api, + message, +}: { + address?: Address; + api: Cip30WalletApi; + message: CardanoMessage; +}): Promise => { + const signingAddress = address ?? (await getPrimaryCardanoAddress(api)); + + return api.signData(signingAddress, cardanoMessageToHex(message)); +}; + +export type CardanoDappClient = ConnectedCardanoWallet & { + getAddress(): Promise
; + signMessage(message: CardanoMessage, address?: Address): Promise; + signMessageHex(payloadHex: HexString, address?: Address): Promise; + signTransaction(txCborHex: CborHex, partialSign?: boolean): Promise; + submitTransaction(txCborHex: CborHex): Promise; +}; + +export const createCardanoDappClient = async (params: ConnectCardanoWalletParams): Promise => { + const connected = await connectCardanoWallet(params); + const { api } = connected; + + return { + ...connected, + getAddress: () => getPrimaryCardanoAddress(api), + signMessage: (message, address) => signCardanoMessage({ address, api, message }), + signMessageHex: (payloadHex, address) => signCardanoMessage({ address, api, message: { hex: payloadHex } }), + signTransaction: (txCborHex, partialSign = false) => { + assertHex(txCborHex, "txCborHex"); + + return api.signTx(txCborHex, partialSign); + }, + submitTransaction: (txCborHex) => { + assertHex(txCborHex, "txCborHex"); + + return api.submitTx(txCborHex); + }, + }; +}; + +const ensureHttpUri = (uri: string): string => { + const uriWithScheme = /^[a-z][a-z0-9+.-]*:/i.test(uri) ? uri : `https://${uri}`; + const parsed = new URL(uriWithScheme); + + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + throw new CardanoWalletError("cardano_invalid_uri", "Only http and https dApp URIs are supported"); + } + + return parsed.toString(); +}; + +/** CIP-158: deep-link a user into a wallet's in-app browser with your dApp URL. */ +export const createCardanoBrowseUri = (dappUri: string): string => + `web+cardano://browse/v1?uri=${encodeURIComponent(ensureHttpUri(dappUri))}`; + +export const parseCardanoBrowseUri = (uri: string): { dappUri: string; version: "v1" } => { + const parsed = new URL(uri); + const dappUri = parsed.searchParams.get("uri"); + + if (parsed.protocol !== "web+cardano:" || parsed.hostname !== "browse" || parsed.pathname !== "/v1" || !dappUri) { + throw new CardanoWalletError("cardano_invalid_browse_uri", "Invalid CIP-158 browse URI"); + } + + return { dappUri: ensureHttpUri(dappUri), version: "v1" }; +}; + +/** CIP-45: QR/deep-link payload for mobile ↔ desktop P2P wallet connection. */ +export const createCardanoP2PConnectUri = ({ + identifier, + params = {}, +}: { + identifier: string; + params?: Record; +}): string => { + if (!identifier) throw new CardanoWalletError("cardano_missing_identifier", "A CIP-45 identifier is required"); + + const search = new URLSearchParams({ identifier }); + for (const [key, value] of Object.entries(params)) search.set(key, String(value)); + + return `web+cardano://connect/v1?${search.toString()}`; +}; + +export const parseCardanoP2PConnectUri = (uri: string): { identifier: string; params: Record; version: "v1" } => { + const parsed = new URL(uri); + const identifier = parsed.searchParams.get("identifier"); + + if (parsed.protocol !== "web+cardano:" || parsed.hostname !== "connect" || parsed.pathname !== "/v1" || !identifier) { + throw new CardanoWalletError("cardano_invalid_connect_uri", "Invalid CIP-45 connect URI"); + } + + const params = Object.fromEntries(parsed.searchParams.entries()); + delete params.identifier; + + return { identifier, params, version: "v1" }; +}; + +/** + * Transport-agnostic adapter for CIP-45-style P2P/RPC connections. + * Plug in Bugout, Meerkat, WebRTC, postMessage, WalletConnect-like transport, etc. + */ +export type CardanoP2PTransport = { + disconnect?(): Promise | void; + request(method: keyof Cip30WalletApi | string, params?: unknown): Promise; +}; + +export const createRemoteCardanoApi = (transport: CardanoP2PTransport): Cip30WalletApi => ({ + getBalance: () => transport.request("getBalance"), + getChangeAddress: () => transport.request
("getChangeAddress"), + getCollateral: (params) => transport.request("getCollateral", params), + getExtensions: () => transport.request("getExtensions"), + getNetworkId: () => transport.request("getNetworkId"), + getRewardAddresses: () => transport.request("getRewardAddresses"), + getUnusedAddresses: () => transport.request("getUnusedAddresses"), + getUsedAddresses: (paginate) => transport.request("getUsedAddresses", paginate), + getUtxos: (amount, paginate) => transport.request("getUtxos", { amount, paginate }), + signData: (address, payload) => { + assertHex(payload, "payload"); + + return transport.request("signData", { address, payload }); + }, + signTx: (tx, partialSign = false) => { + assertHex(tx, "tx"); + + return transport.request("signTx", { partialSign, tx }); + }, + submitTx: (tx) => { + assertHex(tx, "tx"); + + return transport.request("submitTx", { tx }); + }, +}); + +export const openCardanoBrowseUri = (dappUri: string, target: Pick = window.location): void => { + target.assign(createCardanoBrowseUri(dappUri)); +}; From 276ec858f242e728eccec2ea464c19e06773fc6c Mon Sep 17 00:00:00 2001 From: GiMa-SwapKit Date: Thu, 11 Jun 2026 14:40:18 +0200 Subject: [PATCH 3/4] test: add Cardano wallet extension coverage --- .../test/cardano-extensions.test.ts | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 packages/wallet-extensions/test/cardano-extensions.test.ts diff --git a/packages/wallet-extensions/test/cardano-extensions.test.ts b/packages/wallet-extensions/test/cardano-extensions.test.ts new file mode 100644 index 0000000..5998f2b --- /dev/null +++ b/packages/wallet-extensions/test/cardano-extensions.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, test } from "bun:test"; +import { + type Cip30WalletApi, + createCardanoBrowseUri, + createCardanoDappClient, + createCardanoP2PConnectUri, + createRemoteCardanoApi, + parseCardanoBrowseUri, + parseCardanoP2PConnectUri, +} from "../src/cardano"; + +const mockApi: Cip30WalletApi = { + getBalance: async () => "1a", + getChangeAddress: async () => "addr_change", + getNetworkId: async () => 1, + getRewardAddresses: async () => [], + getUnusedAddresses: async () => [], + getUsedAddresses: async () => ["addr_used"], + getUtxos: async () => [], + signData: async (address, payload) => ({ key: `key:${address}`, signature: `sig:${payload}` }), + signTx: async (tx, partialSign) => `${tx}:${partialSign ? "partial" : "full"}`, + submitTx: async (tx) => `hash:${tx}`, +}; + +describe("cardano extensions", () => { + test("connects to an injected CIP-30 wallet and signs/submits through the dApp client", async () => { + const client = await createCardanoDappClient({ + cardano: { + nami: { + apiVersion: "1.0.0", + enable: async () => mockApi, + icon: "data:image/svg+xml;base64,", + isEnabled: async () => true, + name: "Nami", + }, + }, + walletId: "nami", + }); + + expect(await client.getAddress()).toBe("addr_used"); + expect(await client.signTransaction("deadbeef", true)).toBe("deadbeef:partial"); + expect(await client.submitTransaction("cafe")).toBe("hash:cafe"); + }); + + test("wraps CIP-8 message signing through CIP-30 signData", async () => { + const client = await createCardanoDappClient({ + cardano: { + eternl: { + apiVersion: "1.0.0", + enable: async () => mockApi, + icon: "data:image/svg+xml;base64,", + isEnabled: async () => true, + name: "Eternl", + }, + }, + walletId: "Eternl", + }); + + const signature = await client.signMessage("Login"); + + expect(signature.key).toBe("key:addr_used"); + expect(signature.signature).toBe("sig:4c6f67696e"); + }); + + test("creates and parses CIP-158 wallet browse deep links", () => { + const uri = createCardanoBrowseUri("https://swapkit.dev/swap?asset=ada"); + const parsed = parseCardanoBrowseUri(uri); + + expect(uri).toBe("web+cardano://browse/v1?uri=https%3A%2F%2Fswapkit.dev%2Fswap%3Fasset%3Dada"); + expect(parsed).toEqual({ dappUri: "https://swapkit.dev/swap?asset=ada", version: "v1" }); + }); + + test("creates and parses CIP-45 P2P connect URIs", () => { + const uri = createCardanoP2PConnectUri({ identifier: "peer_public_key", params: { name: "SwapKit" } }); + const parsed = parseCardanoP2PConnectUri(uri); + + expect(uri).toBe("web+cardano://connect/v1?identifier=peer_public_key&name=SwapKit"); + expect(parsed).toEqual({ identifier: "peer_public_key", params: { name: "SwapKit" }, version: "v1" }); + }); + + test("adapts any P2P transport into a CIP-30-shaped remote API", async () => { + const calls: { method: string; params?: unknown }[] = []; + const remote = createRemoteCardanoApi({ + request: async (method: keyof Cip30WalletApi | string, params?: unknown): Promise => { + calls.push({ method, params }); + + return (method === "getNetworkId" ? 1 : "ok") as TResponse; + }, + }); + + expect(await remote.getNetworkId()).toBe(1); + expect(await remote.signTx("beef", true)).toBe("ok"); + expect(calls).toEqual([ + { method: "getNetworkId", params: undefined }, + { method: "signTx", params: { partialSign: true, tx: "beef" } }, + ]); + }); +}); From 79dbd7af036a70b4e93d5eba86796c71debda726 Mon Sep 17 00:00:00 2001 From: GiMa-SwapKit Date: Thu, 11 Jun 2026 14:40:31 +0200 Subject: [PATCH 4/4] chore: add Cardano wallet extensions changeset --- .changeset/cardano-wallet-extensions.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cardano-wallet-extensions.md diff --git a/.changeset/cardano-wallet-extensions.md b/.changeset/cardano-wallet-extensions.md new file mode 100644 index 0000000..ee56909 --- /dev/null +++ b/.changeset/cardano-wallet-extensions.md @@ -0,0 +1,5 @@ +--- +"@swapkit/wallet-extensions": minor +--- + +Add Cardano wallet extension helpers for CIP-30 wallet connection, CIP-8 message signing, CIP-45 P2P connect URIs, and CIP-158 mobile browse deep links.