@fluxpointstudios/cip30-deeplink-client
v0.1.0
Published
Reference TypeScript SDK for CIP-30-DeepLink — mobile deep-link signing for Cardano dApps. Companion to cardano-foundation/CIPs#1189.
Downloads
107
Maintainers
Readme
cip30-deeplink-client
Reference TypeScript SDK for CIP-30-DeepLink — a deep-link wire protocol that lets a native mobile dApp ask an installed Cardano wallet to perform CIP-30 operations (signTx, signData, etc.) over the OS's URL-handling mechanism. No relay server, no embedded WebView, no QR, no WebSocket.
This repository is the dApp-side reference implementation that accompanies the CIP draft. The wallet-side reference implementation is Flux-Point-Studios/yuti; the wire format here is byte-for-byte interoperable with it.
Status
Pre-release (0.1.x). The CIP is open for community review at:
- CIP PR: https://github.com/cardano-foundation/CIPs/pull/1189
- Forum thread: https://forum.cardano.org/t/cip-proposal-mobile-deep-link-signing-for-native-dapps-cip-30-extension/154561
Install
npm install @fluxpointstudios/cip30-deeplink-clientRuntime dependency: tweetnacl (NaCl-box + Ed25519). Runs in the browser and Node.
How it works
A deep-link round-trip is not a single call: the dApp navigates to the wallet, the wallet processes the request and redirects back to the dApp's redirect URL with the response in the query string. In a browser that is a full page navigation, so the SDK persists the in-flight request and you recover the result on the next page load with resume().
import { DeepLinkClient } from "@fluxpointstudios/cip30-deeplink-client";
const client = new DeepLinkClient({
wallet: "yuti", // deep-link scheme: cip30dl-yuti
chain: "cardano:preprod",
// resolveTxHash: resolveTxHash, // wire your tx builder's hash for signTx
});
// 1. On every page load, first recover any pending wallet response.
const resumed = await client.resume();
if (resumed?.kind === "connect") {
// resumed.session — { addresses, signingPublicKey, chain, ... } (now stored)
}
if (resumed?.kind === "signTx") {
const { witnessSet, txHash, signatureValid } = resumed.result;
}
// 2. Start a flow (navigates to the wallet; returns on the redirect → resume()).
await client.connect({ name: "Aegis", url: "https://aegis.fluxpointstudios.com" });
// 3. Once connected, request a signature.
await client.signTx({ tx: cborHex }); // needs resolveTxHash, or pass commitComputing the commit (tx hash)
signTx binds the request to commit = BLAKE2b-256(tx_body) — the Cardano transaction hash. The SDK stays dependency-light and does not bundle a serialization library, so either:
- wire your tx builder's hasher once:
new DeepLinkClient({ ..., resolveTxHash: (cbor) => resolveTxHash(cbor) })(MeshJSresolveTxHash, Lucid, or CSLhash_transaction), or - pass it per call:
client.signTx({ tx, commit })wherecommitis base64url of the 32-byte hash.
Assembling & submitting
The wallet returns a witness set, not a signed tx. Splice it into the body with your serialization library, or send { txCbor, witnessSet } to your backend to assemble + submit (what the Aegis demo does — keeps CSL/Mesh off the client).
Verification
Both legs of the handshake are authenticated, and resume() rejects anything that doesn't check out:
- connect — the response must carry the wallet's Ed25519 signature over the canonical subject (which covers
walletKey,method, the encrypted payload, and anechoof the request nonce), amethod=connectdomain tag, and that nonce echo. The SDK decrypts the session, verifies the signature against thesigningPublicKeyit advertises, and confirms the echo — so an unsigned, tampered, or replayed connect can never seat a session, and the verified key is pinned for the rest of the session. - signTx — every response is Ed25519-signed over the canonical subject, keyed by the
signingPublicKeypinned at connect. A returned witness is always signature-verified.
Connect is first contact, so the signature proves the response is consistent, untampered, and bound to this request — not, on its own, that the responder is the user's genuine wallet (that rests on the OS routing the scheme plus the in-wallet consent screen). What the pin buys is that every later signTx reply is provably from the same key-holder this connect established.
API surface
new DeepLinkClient(options)—{ wallet, chain, redirectUrl?, storage?, resolveTxHash? }connect(dappInfo)/signTx(opts)— start a flow (navigate to the wallet)resume(url?)— recover the pending response; returns the connect session, the signTx result, ornullgetSession()/disconnect()- Low-level pure helpers (custom transports, server-side, testing):
buildConnectUrl,buildSignTxUrl,decodeConnectResponse,decodeSignTxResponse,canonicalSubject,b64uEncode/Decode
Outside a browser, pass redirectUrl, a storage (a localStorage-shaped object), and a navigate adapter.
Example
examples/aegis-web/ is the Aegis demo dApp driven entirely by this SDK (connect → signTx → verify). Build the SDK, then serve the folder over http:
npm install && npm run build
npx serve examples/aegis-web # ESM modules need http, not file://Develop
npm install
npm test # vitest — incl. a connect+signTx round-trip vs a Yuti-compatible wallet
npm run typecheck
npm run build # dist/ (ESM + CJS + d.ts) via tsupScope of this repository
Per CIP-0001, reference implementations live in repositories belonging to the author. This SDK is intended as a reference, not a production dependency. Production dApps are encouraged to fork, adapt, or re-implement against the CIP itself.
The companion dApp that this SDK was extracted from is Aegis (parametric DeFi insurance), at https://github.com/Flux-Point-Studios/aegis-contracts for the on-chain validator suite.
License
Apache-2.0. See LICENSE.
