PartyLayerDocs
Try Demo

Token Transfers

PartyLayer does not have a dedicated transfer() method. Transfers on the Canton Network are Daml commands — you construct the command payload and submit it through the wallet. The SDK handles signing, submission, and status tracking.

ℹ️ Note
Before transferring, you need the contractId of the holding you want to spend. Fetch it from the ACS first — see Wallet Balances.

Prerequisites

  • Wallet connected — see Quick Start
  • The Daml template ID and contract ID of the holding to spend
  • signTransaction and submitTransaction capabilities (supported by all built-in wallets)

React

tsx
import { useSession, useSignTransaction, useSubmitTransaction } from '@partylayer/react';

function TransferButton({ receiverPartyId }: { receiverPartyId: string }) {
  const session = useSession();
  const { signTransaction, isSigning } = useSignTransaction();
  const { submitTransaction, isSubmitting } = useSubmitTransaction();

  const handleTransfer = async () => {
    if (!session) return;

    // Build the Daml command payload.
    // Replace templateId, contractId, and choiceArgument with your own Daml values.
    const payload = {
      commands: [
        {
          exerciseCommand: {
            templateId: 'Splice.Amulet:Amulet',
            contractId: '<holding-contract-id>',
            choice: 'Amulet_Transfer',
            choiceArgument: {
              transfer: {
                sender: session.partyId,
                provider: '<provider-party-id>',
                inputs: [{ inputAmulet: { contractId: '<holding-contract-id>' } }],
                outputs: [
                  {
                    receiver: receiverPartyId,
                    amount: '10.0',
                    receiverFeeRatio: '0.0',
                  },
                ],
              },
            },
          },
        },
      ],
      commandId: crypto.randomUUID(),
      applicationId: 'my-app',
      actAs: [session.partyId],
      readAs: [],
    };

    const signed = await signTransaction({ tx: payload });
    if (!signed) return;

    const receipt = await submitTransaction({ signedTx: signed.signedTx });

    console.log('Transfer submitted:', receipt?.transactionHash);
  };

  const isLoading = isSigning || isSubmitting;

  return (
    <button onClick={handleTransfer} disabled={isLoading}>
      {isSigning ? 'Waiting for wallet…' : isSubmitting ? 'Submitting…' : 'Transfer'}
    </button>
  );
}

Listen for status updates

tsx
import { useEffect } from 'react';
import { usePartyLayer } from '@partylayer/react';

function useTxStatus(onUpdate: (status: string, hash: string) => void) {
  const client = usePartyLayer();

  useEffect(() => {
    return client.on('tx:status', (event) => {
      onUpdate(event.status, event.transactionHash);
    });
  }, [client, onUpdate]);
}

// Status transitions: pending → submitted → committed (or rejected / failed)

Vanilla JS

Sign and submit

typescript
import { createPartyLayer } from '@partylayer/sdk';

const client = createPartyLayer({
  network: 'mainnet',
  app: { name: 'My App' },
});

const session = await client.connect();

const payload = {
  commands: [
    {
      exerciseCommand: {
        templateId: 'Splice.Amulet:Amulet',
        contractId: '<holding-contract-id>',
        choice: 'Amulet_Transfer',
        choiceArgument: {
          transfer: {
            sender: session.partyId,
            provider: '<provider-party-id>',
            inputs: [{ inputAmulet: { contractId: '<holding-contract-id>' } }],
            outputs: [
              {
                receiver: '<receiver-party-id>',
                amount: '10.0',
                receiverFeeRatio: '0.0',
              },
            ],
          },
        },
      },
    },
  ],
  commandId: crypto.randomUUID(),
  applicationId: 'my-app',
  actAs: [session.partyId],
  readAs: [],
};

// Step 1 — Sign (wallet prompts user for approval)
const signed = await client.signTransaction({ tx: payload });

// Step 2 — Submit
const receipt = await client.submitTransaction({ signedTx: signed.signedTx });

// receipt: { transactionHash, submittedAt, commandId, updateId }
console.log('Transaction hash:', receipt.transactionHash);

// Step 3 — Listen for status
client.on('tx:status', (event) => {
  console.log(event.status, event.transactionHash);
  // pending → submitted → committed (or rejected / failed)
});

Submit directly via ledgerApi

Skip the separate sign step and submit in one call. Use this when the wallet handles signing internally (Console, Loop, Nightly, or Bron).

typescript
const result = await client.ledgerApi({
  requestMethod: 'POST',
  resource: '/v2/commands/submit-and-wait',
  body: JSON.stringify(payload),
});

Error Handling

typescript
import {
  UserRejectedError,
  SessionExpiredError,
  CapabilityNotSupportedError,
} from '@partylayer/sdk';

try {
  const signed = await client.signTransaction({ tx: payload });
  const receipt = await client.submitTransaction({ signedTx: signed.signedTx });
} catch (err) {
  if (err instanceof UserRejectedError) {
    // User declined in the wallet — safe to retry
  } else if (err instanceof SessionExpiredError) {
    await client.connect();
    // Retry transfer
  } else if (err instanceof CapabilityNotSupportedError) {
    // Wallet does not support signTransaction (e.g. Loop, Nightly)
    // Use submitTransaction directly instead
  }
}

See Error Handling for the full list of error types.

Notes

Payload structure

The examples above use Splice.Amulet:Amulet as the template. Replace templateId, contractId, choice, and choiceArgument with the values from your own Daml templates.

commandId uniqueness

Always generate a fresh commandId per submission — for example, crypto.randomUUID(). The ledger deduplicates on commandId, so reusing one will cause the second submission to be silently ignored.

actAs field

The actAs array must include the sender's partyId. This is available on session.partyId after connecting.

PreviousWallet BalancesNextAdvanced