Token Transfers
Transferring Amulet (or any CIP-56 token) on Canton goes through the Token Standard: you exercise a factory choice by interface, not a choice on the holding contract itself. The wallet prompts the user, the factory produces a TransferInstruction contract, and the recipient accepts it.
Amulet_Transfer directly on #splice-amulet:Splice.Amulet:Amulet is the legacy (pre-Token Standard) path. On today's Canton the ledger rejects it as an unknown choice and Loop's UI shows Execute Unknown on Unknown. Use the Token Standard flow described below.The Token Standard transfer flow
Canton Improvement Proposal CIP-56 defines a two-step transfer:
- Sender exercises
TransferFactory_Transferby interface on aTransferFactorycontract. The factory validates the request and creates aTransferInstruction. - Recipient exercises
TransferInstruction_Accepton the instruction contract. The ledger settles the transfer — input holdings are burned, output holdings are minted.
The sender does not exercise a choice on their own Splice.Amulet:Amulet contract — under CIP-56 the Amulet is a Holding (data only), and transfer logic lives on the factory.
Prerequisites
- Wallet connected — see Quick Start
- A TransferFactory contractId — fetched from the app-provider's Scan / off-ledger API. For Amulet on mainnet/devnet, that's the
/registry/transfer-instruction/v1/transfer-factoryendpoint of the DSO's Scan (it returns both the factoryId and aChoiceContextblob to pass asextraArgs.context). - One or more input holding contractIds — fetched from your own ACS (see Wallet Balances) using the
#splice-api-token-holding-v1:Splice.Api.Token.HoldingV1:HoldinginterfaceId. - Recipient partyId and the instrument details (
admin= DSO party,id="Amulet").
React (interface-based TransferFactory_Transfer)
factoryCid and choiceContext come from? From the Splice Scan HTTP API that the registry issuer runs. For the Amulet DSO on devnet/mainnet, you call GET /api/scan/v0/registry/transfer-instruction/v1/transfer-factory (body includes the sender and receiver) and it returns { factoryId, choiceContext, disclosedContracts }. This is dApp-side off-ledger coordination — PartyLayer does not abstract it because the endpoint varies by app provider.Loop wallet — convenience helper
Loop's own SDK ships a loop.wallet.transfer() helper that handles the Scan lookup, factory resolution, and submission for you. If your dApp targets Loop specifically, this is the simplest path. Access the underlying Loop SDK via the @fivenorth/loop-sdk package alongside PartyLayer:
Recipient accepts the instruction
After the sender's transaction commits, a TransferInstruction contract appears in the recipient's ACS. The recipient's dApp (or wallet UI) then exercises TransferInstruction_Accept on it:
Other instruction choices: TransferInstruction_Reject, TransferInstruction_Withdraw (sender cancels before accept), TransferInstruction_Update.
Troubleshooting
Loop UI shows "Execute Unknown on Unknown"
You're sending the legacy Amulet_Transfer command exercised directly on Splice.Amulet:Amulet. Loop's UI (and Canton since CIP-56) does not recognize this command. Switch to TransferFactory_Transfer on the TransferFactory interface as shown above.
"Loop Wallet submitAndWaitForTransaction resolved with an empty response"
The popup closed before you confirmed, or the wallet server returned an empty frame. Confirm in the wallet UI and retry. If it persists, double-check the interfaceId has the fully-qualified #package:Module:Interface form and that actAs matches session.partyId.
Template / interface ID format
Loop requires the fully-qualified Daml form with the package-name prefix — #splice-amulet:... or #splice-api-token-transfer-instruction-v1:..., not the short Canton form. Our adapter surfaces the short-form mistake as a clear error pointing at this fix.
commandId uniqueness
Always generate a fresh commandId per submission (for example crypto.randomUUID()). The ledger deduplicates on commandId, so reusing one silently drops the second submission.
Error handling
References
- hyperledger-labs/splice → token-standard — Daml sources for
TransferFactory,TransferInstruction, andHolding. - CIP-56 / CIP-78 — Canton Token Standard
- Splice docs → Token Standard APIs
- Loop SDK →
wallet.transfer()helper