@sigilry/dapp
@sigilry/dapp is the core package for CIP-103-compliant dApp ↔ wallet extension communication on Canton Network. It provides the CIP-103 provider interface (SpliceProvider), JSON-RPC client and server, transport abstractions, and Zod schemas generated from the CIP-103 OpenRPC spec.
Install
Section titled “Install”yarn add @sigilry/dapp@^3Use the stable 3.x line on latest. Partner guides cover Send Connect testnet, verifying signMessage, and reading holdings.
Provider usage (dApp)
Section titled “Provider usage (dApp)”The wallet extension injects a CIP-103 provider at window.canton. The object shape (request, on, removeListener) is the EIP-1193 pattern that CIP-103 adopts; method names, params, results, and error codes are fixed by the CIP-103 spec.
import "@sigilry/dapp/browser-globals";
export async function connectAndListAccounts() { if (!window.canton) { throw new Error("Canton provider not available"); }
const status = await window.canton.request({ method: "status" }); if (!status.connection.isConnected) { await window.canton.request({ method: "connect" }); }
const accounts = await window.canton.request({ method: "listAccounts" }); console.log("Accounts:", accounts); return accounts;}Typed client usage (custom transport)
Section titled “Typed client usage (custom transport)”import { createCantonClient, WindowTransport } from "@sigilry/dapp";
export async function getStatus() { const transport = new WindowTransport(window, { timeout: 30000 }); const client = createCantonClient(transport); return await client.status();}Extension server usage
Section titled “Extension server usage”import { createCantonServer, createStubHandlers, isSpliceMessageEvent, jsonRpcResponse, WalletEvent,} from "@sigilry/dapp";
const server = createCantonServer({ ...createStubHandlers(), status: async () => ({ provider: { id: "send-extension", providerType: "browser" }, connection: { isConnected: true, isNetworkConnected: true }, }), connect: async () => ({ isConnected: true, isNetworkConnected: true, }), disconnect: async () => null, getActiveNetwork: async () => ({ networkId: "canton:localnet" }), listAccounts: async () => [], getPrimaryAccount: async () => ({ primary: true, partyId: "alice::1220abc123", status: "allocated", hint: "alice", publicKey: "ed25519:abc123", namespace: "1220abc123", networkId: "canton:localnet", signingProviderId: "passkey", }), prepareExecute: async () => null, prepareExecuteAndWait: async () => ({ tx: { status: "executed", commandId: "cmd-123", payload: { updateId: "update-1", completionOffset: 1 }, }, }), // Stub only: DER ECDSA integers r=1, s=1, encoded as hex; not a valid message signature. signMessage: async () => ({ signature: "3006020101020101" }), ledgerApi: async () => ({ events: [] }),});
window.addEventListener("message", async (event) => { if (!isSpliceMessageEvent(event)) return; if (event.data.type !== WalletEvent.SPLICE_WALLET_REQUEST) return;
const { id, method, params } = event.data.request; const response = await server.handleRequest(method, params); // JSON-RPC notifications carry no id and receive no response. if (id === undefined) return;
window.postMessage( { type: WalletEvent.SPLICE_WALLET_RESPONSE, response: jsonRpcResponse(id, response), }, "*", );});The server above uses stub handlers. A real Send Connect signMessage response contains a lowercase hex DER ECDSA P-256/SHA-256 signature. CIP-0103 leaves the encoding unspecified; other wallets can use different algorithms and encodings. See Verify signMessage for a working verifier.
Provider discovery
Section titled “Provider discovery”When more than one Canton wallet may be present, dApps should discover providers instead of reaching for window.canton directly. The discovery primitives live on the @sigilry/dapp/discovery subpath — they are intentionally not re-exported from the package root. See Provider Discovery for the model and fallback semantics.
import { createDiscoveryStore } from "@sigilry/dapp/discovery";
const store = createDiscoveryStore();
const unsubscribe = store.subscribe( (wallets) => { // wallets: readonly DiscoveredWallet[] for (const w of wallets) console.log(w.info.rdns, w.info.name); }, { emitImmediately: true },);
// Build a live provider bound to a chosen wallet's transport target.const provider = store.findProvider({ rdns: "it.send.connect" })?.getProvider();console.log(provider);
// Teardown removes the window listeners.unsubscribe();store.destroy();A wallet extension announces itself instead:
import { announceProvider } from "@sigilry/dapp/discovery";
announceProvider({ id: "send-extension", name: "Send Connect", icon: "data:image/svg+xml;base64,...", target: "send-connect", rdns: "it.send.connect", uuid: crypto.randomUUID(),});Discovery subpath exports: requestProviders, announceProvider, createDiscoveryStore, createProvider, and the DiscoveredWallet, SpliceProviderInfo, SpliceAnnounceDetail, DiscoveryStore, and TransportOptions types. Pass a TransportOptions to wallet.getProvider(opts?) to tune the announced wallet’s transport (e.g. a longer request timeout).
Push events
Section titled “Push events”A SpliceProvider delivers the CIP-103 push events through the EIP-1193-style .on() API — the §4.2.2 sync events (accountsChanged, statusChanged, txChanged) plus the connected login event. WindowTransport carries notifications inbound on the same channel as requests (a notification is an id-less frame; the JSON-RPC id is the direction discriminant), and SpliceProviderBase auto-wires them to the provider’s emitter — so a provider from createProvider (or the injected window.canton) emits with no extra setup.
const onStatus = (status: StatusEvent) => { /* ongoing status changes: network, session, disconnect */ console.log(status);};provider.on("statusChanged", onStatus);provider.on("connected", (status) => { /* login-flow completion (login event, not §4.2.2) */ console.log(status);});provider.on("accountsChanged", (accounts) => { /* active account / session changes */ console.log(accounts);});provider.on("txChanged", (tx) => { /* transaction updates */ console.log(tx);});
// Unsubscribe:provider.removeListener("statusChanged", onStatus);On the wallet (server) side, emit notifications with notify(event, payload, target?) on the transport. Frames whose target does not match a provider’s configured target are filtered out, so multi-wallet pages never cross-deliver events. See CIP-103 Conformance for the normative event semantics.
Key exports
Section titled “Key exports”CANTON_DAPP_API_VERSIONSpliceProviderBase,SpliceProviderWindowTransportcreateCantonClient,createCantonServer,createStubHandlersWalletEvent,isSpliceMessage,isSpliceMessageEventjsonRpcRequest,jsonRpcResponseRpcErrorCode,rpcError,RpcClientError- Discovery (subpath
@sigilry/dapp/discovery):requestProviders,announceProvider,createDiscoveryStore,createProvider, and theTransportOptionstype
Generated schemas
Section titled “Generated schemas”Zod schemas are generated from OpenRPC specifications and exposed under @sigilry/dapp/schemas:
import { StatusEventSchema, type StatusEvent } from "@sigilry/dapp/schemas";
const status: StatusEvent = StatusEventSchema.parse(data);SignMessageResultSchema requires signature and accepts optional signedBy,
publicKey (base64 DER SPKI), signingAlgorithmSpec ("ed25519" | "ecdsa-sha256"),
format ("concat" | "der"), and encoding ("base64" | "hex"). Some wallets, including a pending
Send release, may include these fields; they are not yet part of CIP-0103. Signature-only results remain valid; unknown keys are
rejected. The inferred SignMessageResult type carries the same optional fields. See
Verify signMessage for the Send Connect extension’s format, metadata availability, and key
authorization considerations.
Regenerate schemas after spec updates:
yarn workspace @sigilry/dapp codegen