Back to docs

# Developer Guide

Integrate your Canton dApp with Zoff Wallet. Users connect, you submit Daml commands, Zoff handles signing.

## Overview

Zoff injects a provider at `window.cantonWallet` on every page where the extension is active. The provider exposes direct methods for connecting, querying, and submitting transactions. If you've built for MetaMask on Ethereum, the mental model is similar: detect the provider, request a connection, submit transactions.

## Detecting Zoff Wallet

    // Check if Zoff is installed
    if (typeof window.cantonWallet !== 'undefined') {
      console.log('Zoff Wallet detected');
      console.log('Version:', window.cantonWallet.version);
    } else {
      console.log('No Canton wallet found');
    }

The provider object exposes two boolean flags for identification:

- `window.cantonWallet.isCantonWallet` — always `true`
- `window.cantonWallet.isExtension` — always `true`

## Connecting to the Wallet

    const result = await window.cantonWallet.connect({
      dappName: 'My Canton dApp',
    });

    // result contains the user's active party ID
    console.log(result.partyId); // "party::1a2b3c..."

When your dApp calls `connect()`, Zoff shows the user a connection approval popup displaying your origin. The user can approve or reject. If approved, your origin is stored in the user's connected dApps list and persists across sessions until revoked.

## Provider Methods

### `connect(options?)`

Request wallet connection. Returns the user's active party ID.

    const { partyId } = await window.cantonWallet.connect({
      dappName: 'My dApp',               // optional
      requestedPermissions: ['sign'],     // optional
    });

Parameters:

Field | Type | Description
--- | --- | ---
`dappName` | `string` (optional) | Your dApp's display name shown in the approval popup
`requestedPermissions` | `string[]` (optional) | Permissions your dApp is requesting

Returns: `{ partyId: string }`

### `disconnect()`

Disconnect your dApp from the wallet.

    await window.cantonWallet.disconnect();

### `isConnected()`

Check if your dApp is currently connected to the wallet.

    const connected = await window.cantonWallet.isConnected();

### `getPartyId()`

Get the currently active party ID (requires existing connection).

    const { partyId } = await window.cantonWallet.getPartyId();

### `getBalances()`

Get the balances for the currently active account.

    const balances = await window.cantonWallet.getBalances();

### `signAndSubmit(transaction)`

Submit a Daml command for the user to sign and execute on Canton.

    const result = await window.cantonWallet.signAndSubmit({
      templateId: {
        moduleName: 'Splice.Amulet',
        entityName: 'TransferPreapproval',
      },
      choiceName: 'TransferPreapproval_Accept',
      arguments: {
        receiver: recipientPartyId,
        amount: '100.0',
      },
    });

When `signAndSubmit` is called, Zoff shows the user a signing popup with the full transaction details — template ID, choice, and arguments. The user must explicitly approve. Signing happens locally in the browser; only the signature is transmitted to the Canton participant node.

Parameters:

Field | Type | Description
--- | --- | ---
`templateId` | `{ moduleName, entityName }` | The Daml template to exercise
`choiceName` | `string` | The choice to exercise on the template
`arguments` | `object` | Choice arguments as key-value pairs
`contractId` | `string` (optional) | Contract ID to exercise the choice on

Returns: `{ transactionId: string }` on success.

Errors: If the user rejects the signing request, the promise rejects with error code `4001` (User Rejected).

### Event Listeners

    // Listen for account changes (user switches active account)
    window.cantonWallet.on('accountChanged', (partyId) => {
      console.log('Active account changed to:', partyId);
    });

    // Listen for disconnection
    window.cantonWallet.on('disconnect', () => {
      console.log('Wallet disconnected');
    });

    // Remove a listener
    window.cantonWallet.off('accountChanged', myHandler);

Supported events: `accountChanged`, `disconnect`.

## Error Codes

Code | Meaning
--- | ---
`4001` | User rejected the request

Additional error codes may be added in future releases.

## Integration Example

A complete example connecting to Zoff and sending CC:

    async function sendCC(recipientPartyId, amount) {
      // 1. Ensure wallet is connected
      const wallet = window.cantonWallet;
      if (!wallet) throw new Error('Install Zoff Wallet');

      const { partyId } = await wallet.connect();
      console.log('Connected as:', partyId);

      // 2. Submit a transfer
      const result = await wallet.signAndSubmit({
        templateId: {
          moduleName: 'Splice.Amulet',
          entityName: 'TransferPreapproval',
        },
        choiceName: 'TransferPreapproval_Accept',
        arguments: {
          receiver: recipientPartyId,
          amount: amount.toString(),
        },
      });

      console.log('Transfer submitted:', result.transactionId);
      return result;
    }

## React Hook (Example)

    import { useState, useEffect, useCallback } from 'react';

    interface CantonWallet {
      isCantonWallet: boolean;
      version: string;
      connect: (options?: { dappName?: string }) => Promise<{ partyId: string }>;
      disconnect: () => Promise<void>;
      isConnected: () => Promise<boolean>;
      getPartyId: () => Promise<{ partyId: string }>;
      getBalances: () => Promise<unknown>;
      signAndSubmit: (tx: Record<string, unknown>) => Promise<{ transactionId: string }>;
      on: (event: string, handler: (data?: unknown) => void) => void;
      off: (event: string, handler: (data?: unknown) => void) => void;
    }

    export function useZoff() {
      const [isConnected, setIsConnected] = useState(false);
      const [partyId, setPartyId] = useState<string | null>(null);

      useEffect(() => {
        const wallet = (window as any).cantonWallet as CantonWallet | undefined;
        if (!wallet) return;

        const onAccountChanged = (newPartyId: unknown) => {
          setPartyId(newPartyId as string);
        };

        const onDisconnect = () => {
          setIsConnected(false);
          setPartyId(null);
        };

        wallet.on('accountChanged', onAccountChanged);
        wallet.on('disconnect', onDisconnect);

        return () => {
          wallet.off('accountChanged', onAccountChanged);
          wallet.off('disconnect', onDisconnect);
        };
      }, []);

      const connect = useCallback(async () => {
        const wallet = (window as any).cantonWallet as CantonWallet | undefined;
        if (!wallet) throw new Error('Zoff Wallet not installed');

        const result = await wallet.connect();
        setIsConnected(true);
        setPartyId(result.partyId);
      }, []);

      const disconnect = useCallback(async () => {
        const wallet = (window as any).cantonWallet as CantonWallet | undefined;
        await wallet?.disconnect();
        setIsConnected(false);
        setPartyId(null);
      }, []);

      const signAndSubmit = useCallback(async (transaction: Record<string, unknown>) => {
        const wallet = (window as any).cantonWallet as CantonWallet | undefined;
        if (!wallet) throw new Error('Zoff Wallet not installed');
        return wallet.signAndSubmit(transaction);
      }, []);

      return { isConnected, partyId, connect, disconnect, signAndSubmit };
    }

## Testing on DevNet

Zoff is currently deployed on Canton DevNet. To test your integration:

1. Install the Zoff extension.
2. Create a wallet (this allocates a DevNet party).
3. Your dApp should point to DevNet endpoints.

Mainnet integration will be available once Zoff receives mainnet credentials. The provider API is identical — no code changes needed on your end.

Next: Architecture Overview →