Token Standard APIs
Overview
See the text of the CIP-0056 for an overview of the APIs that are part of the Canton Network Token Standard.
See the README in its source-code for background on how to use the APIs.
Holding UTXO Management
Analogous to Bitcoin, Canton uses a UTXO model, where the UTXOs are all the active
contracts that implement the Holding interface.
Active Holding contracts incur both storage and compute cost on the
validator nodes hosting users that own Holding contracts and the validator nodes hosting the
token administrator. To make efficient use of the network’s resources,
we thus recommend that wallet providers aim to keep the number of
UTXOs per user low, i.e, below ~10 UTXOs per user on average.
This also optimizes traffic costs, as each UTXO input to a transfer costs extra traffic. Furthermore, this recommendation aligns with the incentives put in place by tokens like Canton Coin, which:
allows at most 100 input contracts for a single transfer, and thus discourages excessive splitting of holdings
expires coin UTXOs whose initial amount is lower than the accrued holding fee, and thus discourages the creation of dust UTXOs
We recommend wallet providers to implement a UTXO management strategy that:
prefers the selection of
HoldingUTXOs with small amounts to provide the input holdings to fund a transferasks the user to setup a
MergeDelegationcontract (see docs) as part of wallet onboarding, which enables the wallet provider to automatically merge smallHoldingUTXOs on behalf of the user
Note
MergeDelegation contracts also allow wallet providers to
run airdrop campaigns jointly with the auto merging of holdings
using a single, batched call to airdrop and merge holdings for
multiple users and multiple instruments.
Furthermore, featured wallet providers
earn featured app rewards for performing merges for their users.
Setting up MergeDelegations
Assuming you are a wallet provider that runs a validator node for your users,
you can set up MergeDelegation contracts for your users as follows.
Extract the latest version of the
splice-util-token-standard-wallet.darfile from the release bundle (Download Bundle).Upload the extracted
.darfile to your validator node.Adjust your user onboarding procedure such that the users signs the creation of a
MergeDelegationProposalcontract (see docs).Accept the
MergeDelegationProposalcontracts by exercising theirAcceptchoice using your wallet provider’s party.
Using MergeDelegations
We recommend to use the MergeDelegation contracts in a batched fashion
as follows.
Create a single
BatchMergeUtilitycontract (see docs) for your wallet provider’s party as part of your validator node’s setup.Grant your wallet provider’s user the
CanReadAsAnyPartyright on your validator node to allow it to read all users’HoldingUTXOs.Run a background process that regularly performs the following steps:
Determines all users that have more than 10
HoldingUTXOs. For example, using the DB provided by the Participant Query Store; or by reading the Holding contracts directly from the Ledger API of your validator node. The former being the more scalable option.Constructs the transfer choices to merge the extra
Holdingcontracts by querying the registry API as explained in Executing a factory choice.Lookup the
MergeDelegationcontract for each user and construct the corresponding call to exercise theMergeDelegation_Mergechoice.Assemble batches of ~100 merge delegation choices into a single call to the
BatchMergeUtility_MergeHoldingschoice.Lookup the contract-id of the
BatchMergeUtilitycontract that you setup above.Execute the batched merge by exercising the
BatchMergeUtility_MergeHoldingschoice using your wallet provider user on your validator node. Use the Ledger API to exercise the choice and make sure that you add all disclosed contracts obtained from the previous steps to the single call.Note that for you can execute multiple batches in parallel for higher throughput.
Optionally, you can add transfers from your operator party to the merge calls to implement airdrop campaigns in a batched fashion.
Upgrading from custom MergeDelegation implementations
Some wallet providers already implement their own custom merge delegation contracts.
They can continue to use them alongside the MergeDelegation contracts provided
by Splice.
There is no requirement to upgrade to the Splice-provided contracts.
However, if you would like to upgrade to the Splice-provided contracts (e.g., to benefit from the additional features), then you can do so as follows.
Add a
CustomMergeDelegation_Upgradechoice to yourCustomMergeDelegationtemplate that creates aMergeDelegationcontract for the user. Make the choiceconsuming, so that the oldCustomMergeDelegationcontract is archived as part of exercising the upgrade choice.Bump the version of your custom merge delegation
.darfile and build a new release.Upload the new
custom-merge-delegation.darfile to your validator node.Call the
CustomMergeDelegation_Upgradechoice on all existingCustomMergeDelegationcontracts to upgrade them to the Splice-providedMergeDelegationcontracts
Wallet integration with Token Standard Assets
This section provides wallet developers with guidance on how to integrate with token standard assets. Such an integration works by sending the right read and write requests to the Ledger API of the validator node hosting the wallet user’s party. There are five kinds of integration patterns:
These integrations patterns have recently been nicely packaged in the wallet SDK maintained as part of hyperledger-labs/splice-wallet-kernel and documented here.
All of these integration patterns are also demonstrated in the form of executable code as part of the experimental command-line interface for token standard assets. The sections below explaining the patterns below thus all start with a link to the code. They then provide additional context for an implementor.
All interaction works via the JSON Ledger API (see its OpenAPI definition here).
This OpenAPI definition is also accessible at http(s)://${YOUR_PARTICIPANT}/docs/openapi.
We encourage developers to use OpenAPI code generation tools as opposed to manually writing HTTP requests.
Check out the Authentication docs for more information on how to authenticate the requests.
Reading contracts implementing a Token Standard interface for a party
Reference code from the Token Standard CLI to list contracts by interface
The Token Standard includes several interfaces that are implemented by Daml templates. To list all contracts implementing a particular interface, you have to query the participant’s active-contracts endpoint.
The activeAtOffset parameter can be set to the result of
the ledger-end endpoint on the participant
to get the latest ACS, or an older (non-pruned) one to get the ACS at that point in time.
To filter for a particular party and interface, it should include a filtersByParty with an InterfaceFilter:
{
"filtersByParty": {
"$A_PARTY": {
"cumulative": [
{
"identifierFilter": {
"InterfaceFilter": {
"value": {
"interfaceId": "$AN_INTERFACE_ID",
"includeInterfaceView": true,
"includeCreatedEventBlob": true
}
}
}
}
]
}
}
}
For example:
"$A_PARTY"could look liketest::1220a0db3761b3fc919b55e7ff80ad740824336010bfde8829611c0e64477ab7bee5."$AN_INTERFACE_ID"could be#splice-api-token-holding-v1:Splice.Api.Token.HoldingV1:Holding.
Additionally, there’s three flags that can be set:
includeInterfaceView: to include the interface view of the contract in the response.includeCreatedEventBlob: to include a binary blob that is required for explicit disclosure.verbose: to include additional information in the response.
The response for such a query will contain the createdEvent of the contract, including the interface views requested (if any).
The viewValue within it will be the JSON-serialized Daml interface view.
If more than one interface is requested, you can distinguish them by checking the interfaceId field.
You can find an example response for Holdings here.
Reading and parsing transaction history involving Token Standard contracts
Example code: Token Standard CLI’s code to list transactions
The participant has an endpoint to list all transactions involving the provided parties and interfaces.
To filter for a particular party and interface, it should include a filtersByParty with an InterfaceFilter:
{
"filtersByParty": {
"$A_PARTY": {
"cumulative": [
{
"identifierFilter": {
"InterfaceFilter": {
"value": {
"interfaceId": "$AN_INTERFACE_ID",
"includeInterfaceView": true,
"includeCreatedEventBlob": true
}
}
}
}
]
}
}
}
For example:
"$A_PARTY"could look liketest::1220a0db3761b3fc919b55e7ff80ad740824336010bfde8829611c0e64477ab7bee5."$AN_INTERFACE_ID"could be#splice-api-token-holding-v1:Splice.Api.Token.HoldingV1:Holdingto read allHoldingcontracts of the specified party.
To include other transaction nodes that don’t directly involve the interfaces (e.g., non-interface-specific children nodes),
a WildcardFilter can be included in the cumulative filter array:
{
"identifierFilter": {
"WildcardFilter": {
"value": {
"includeCreatedEventBlob": true
}
}
}
}
The beginExclusive field is the offset from which to start reading transactions.
To paginate, you can start with the participantPrunedUpToInclusive from GET ${PARTICIPANT_URL}/v2/state/latest-pruned-offsets
and continue by passing the offset of the last transaction from the previous response.
Parsing the history
Example code: the parser here.
It extracts a user-readable wallet history by parsing transactions involving the Holding and TransferInstruction interfaces.
The endpoint returns transaction trees as an array.
The transactions are ordered as they occur in the ledger.
Given an ExercisedEvent with nodeId=X and lastDescendantNodeId=Y,
the children of that node are those with nodeId in the range [X+1, Y].
CreatedEvent and ArchivedEvent (or equivalently, ExercisedEvent where consuming=true) do not have children.
Given the above, a tree-like traversal can be performed on the transaction nodes. Generally, a Token Standard parser will focus on the exercise of Token Standard choices and creation of contracts implementing Token Standard interfaces. Where further customization is required, a parser can decide to also focus on internal/specific choices that are not available in the standard, but in some specific implementation.
In each Token Standard exercise node, one can find:
The choice being executed, useful to distinguish what operation was performed.
As part of the archival/creation of children, one can find out other relevant operations that happened. For example, creation or archival of
Holdings.Meta key/values, of which part of the standard:
splice.lfdecentralizedtrust.org/tx-kind: the kind of operation happening in the node. This can give more information than the exercised choice does. It can be one of:transfermerge-splitburnmintunlockexpire-dust
splice.lfdecentralizedtrust.org/sender: which party is the sender in the node.splice.lfdecentralizedtrust.org/reason: a text specifying the reason for the operation in the node.splice.lfdecentralizedtrust.org/burned: how much of a holding was burned in the node.
Warning
Meta key/values can be specified in several optional fields. For transfers, the values from fields that are present should be merged in last-write-wins order of:
event.choiceArgument.transfer.meta,
event.choiceArgument.extraArgs.meta,
event.choiceArgument.meta,
event.exerciseResult.meta,
Executing a factory choice
Example code: Token Standard CLI’s code to create a transfer via TransferFactory
To execute a choice via a Token Standard factory, first you need need to fetch the factory from the corresponding registry.
Note
The mapping from an instrument’s admin party-id to the corresponding registry URL needs to be maintained currently by wallets themselves, until a generic solution (likely based on CNS) is implemented.
The registry will return the relevant factory in the corresponding endpoint:
The response’s payload will include three relevant fields:
factoryId: the contract id of the factorydisclosedContracts: must be provided to the exercise of the factory’s choice for it to workchoiceContextData: to be passed ascontextin thechoiceArgument.
With this data, you can execute a choice on the factory. For external parties you must call the prepare and execute endpoints of the participant. For non-external parties, you can just use the submit-and-wait endpoint.
In both cases, you must include an ExerciseCommand in your payload with the following fields:
templateId: the interface id of the factory you want to exercise the choice on. For example,#splice-api-token-transfer-instruction-v1:Splice.Api.Token.TransferInstructionV1:TransferFactory.contractId: thefactoryIdobtained from the registry.choice: the name of the choice you want to execute. For example,TransferFactory_Transfer.choiceArgument: the arguments that will be passed to the Daml choice. These will be decoded from JSON. For aTransferFactory_Transfer, this will include for example the sender, receiver and amount, among other fields.
Executing a non-factory choice
Example code: Token Standard CLI’s code to accept a transfer instruction
To execute a choice on a contract implementing a Token Standard interface for external parties, you must call the prepare and execute endpoints of the participant. For non-external parties, you can just use the submit-and-wait endpoint.
In both cases, you must include an ExerciseCommand in your payload with the following fields:
templateId: the interface id of the contract you want to exercise the choice on. For example,#splice-api-token-transfer-instruction-v1:Splice.Api.Token.TransferInstructionV1:TransferInstruction.contractId: the contract id of the contract you want to exercise the choice on. Typically, you’ll get this from the current ACS of a party.choice: the name of the choice you want to execute. For example,TransferInstruction_Accept.choiceArgument: the arguments that will be passed to the Daml choice. These will be decoded from JSON.
Where a context is required as part of the choiceArgument, it can be fetched from the corresponding registry:
The response of these endpoints include two fields:
choiceContextData: to be passed ascontextin thechoiceArgument.disclosedContracts: to be passed in the submit or prepare request.
Warning
Note that AllocationRequest_Reject and AllocationRequest_Withdraw should be called with an empty choice context.
This ChoiceContext is present to allow for potential future extensions of the behavior of implementations of these choices.
Using token standard choices from custom Daml code
Calling the token standard choices from custom Daml code is useful when integrating one’s own app workflows with the token standard. Example workflows relevant to a wallet provider are merging of holdings for a user to keep their ACS small, doing bulk transfers, or marking a user’s action as activity of the wallet app.
Splice releases the optional splice-util-token-standard-wallet.dar file, which packages common workflows that improve the
operations of a wallet app. See its documentation below for the reference of the templates provided.
API References
Refer to CIP-0056 for more context on the APIs.
Token Metadata
Holding
This allows implementation of a Portfolio View.
Transfer Instruction
This allows implementation of Direct Peer-to-Peer / Free of Payment (FOP) Transfers.
Allocation
This allows implementation of Delivery versus Payment (DVP) Transfer Workflows, jointly with the Allocation Instruction and Allocation Request APIs below.
Comments