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
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
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
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).
const result = await client.ledgerApi({
requestMethod: 'POST',
resource: '/v2/commands/submit-and-wait',
body: JSON.stringify(payload),
});
Error Handling
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.