---
title: "@sigilry/dapp"
description: "Core provider, RPC client and server, message schemas, and transports."
url: "https://sigilry.org/packages/dapp"
---

`@sigilry/dapp` is the core package for [CIP-103](https://github.com/canton-foundation/cips/blob/main/cip-0103/cip-0103.md)-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

```bash
yarn add @sigilry/dapp@^3
```

Use the stable 3.x line on `latest`. Partner guides cover [Send Connect testnet](/guides/send-connect-testnet/), [verifying signMessage](/guides/verify-sign-message/), and [reading holdings](/guides/read-holdings/).

## 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.

```ts
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)

```ts
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

```ts
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](/guides/verify-sign-message/) for a working verifier.

## 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](/concepts/discovery/) for the model and fallback semantics.

```ts
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:

```ts
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

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.

```ts
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](/concepts/cip-103-conformance/) for the normative event semantics.

## Key exports

* `CANTON_DAPP_API_VERSION`
* `SpliceProviderBase`, `SpliceProvider`
* `WindowTransport`
* `createCantonClient`, `createCantonServer`, `createStubHandlers`
* `WalletEvent`, `isSpliceMessage`, `isSpliceMessageEvent`
* `jsonRpcRequest`, `jsonRpcResponse`
* `RpcErrorCode`, `rpcError`, `RpcClientError`
* Discovery (subpath `@sigilry/dapp/discovery`): `requestProviders`, `announceProvider`, `createDiscoveryStore`, `createProvider`, and the `TransportOptions` type

## Generated schemas

Zod schemas are generated from OpenRPC specifications and exposed under `@sigilry/dapp/schemas`:

```ts
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](/guides/verify-sign-message/) for the Send Connect extension’s format, metadata availability, and key authorization considerations.

Regenerate schemas after spec updates:

```bash
yarn workspace @sigilry/dapp codegen
```
