Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.canton.network/llms.txt

Use this file to discover all available pages before exploring further.

This section was copied from existing reviewed documentation. Source: docs/wallet-integration-guide/src/exchange-integration/index.rst Reviewers: Skip this section. Remove markers after final approval.

Exchange Integration Guide

The pages below guide you how to integrate an exchange with Canton for the purpose of trading Canton Network Token Standard compliant tokens like Canton Coin. The core of such an integration is automating the deposits and withdrawals of tokens to and from the exchange. The guide can thus also be used for services other than exchanges that want to support deposits and withdrawals of Canton Network tokens.
overview architecture workflows txingestion non-functionals node-operations disaster-recovery testing extensions
This section was copied from existing reviewed documentation. Source: docs/wallet-integration-guide/src/exchange-integration/overview.rst Reviewers: Skip this section. Remove markers after final approval.

Overview

This guide aims to help you integrate your exchange with Canton for the purpose of trading Canton Coin (CC) and Canton Network (CN) tokens.

What to develop in what order?

This material is comprehensive guidance for integrating with the Canton Network. You may need to review it several times to become familiar with Canton’s UTXO-based chain, smart contract language, and its privacy model. The guide is intentionally structured such that you can use a learning-by-doing approach that delivers your integration in a series of incremental milestones:
  • Canton Coin (CC) with 1-step withdrawal only.
  • Support for all CN Tokens, not just CC.
  • Earning additional application rewards for all CN tokens.
The following dependency diagrams shows the work items for each milestone. milestone and delivery dependency diagram CC with 1-step withdrawal only: this milestone allows you to support deposits and withdrawals of CC. It includes earning app rewards for all CC deposits. The workflows build on the Canton Network Token Standard which is the foundation for supporting all CN tokens in the next milestone. We consider it an intermediate milestone, as it does not support:
  • all CN tokens
  • CC users that prefer to control the receipt of transfers, and thus do not want to setup preapprovals
  • earning app rewards for all deposits and withdrawals
See the following sections for details on the work items it depends on.
  • Setup DevNet node and/or use LocalNet
  • exchange-parties-setup
  • one-step-deposit-workflow
  • one-step-withdrawal-workflow
  • Support restore from validator node backup
  • Support hard synchronizer migration
MVP for all CN Tokens: this milestone allows you to support deposits and withdrawals of all CN tokens. It comes with the limitation that application rewards are only earned on deposits of CC, but not on deposits of other CN tokens. It depends on the MVP for CC and the following additional work items:
  • multi-step-deposit-workflow
  • multi-step-withdrawal-workflow, which resolves the limitation that users must setup a CC transfer preapproval to receive withdrawals.
  • token-onboarding
Earn app rewards for all CN tokens: is a milestone that improves the profitability of the integration by implementing changes so the exchange earns application rewards on both withdrawals and deposits of all CN tokens. Sharing application rewards is an optional steps.
  • withdrawal-app-rewards
  • deposit-app-rewards
  • share-rewards-with-customers

Integration support code

Use the following support code to simplify your integration development for:
  • JavaScript/TypeScript: use the functions from the Wallet SDK to simplify building your integration.
  • Java/JVM: use the sample code from the https://github.com/digital-asset/ex-java-json-api-bindings repository as a starting point.
  • Other languages: use the code from the Wallet SDK or the Java sample code as a blueprint.
This section was copied from existing reviewed documentation. Source: docs/wallet-integration-guide/src/exchange-integration/architecture.rst Reviewers: Skip this section. Remove markers after final approval.

Integration Architecture

High-Level Overview

merge, link, align this brief summary with the overview in the wallet integration guide
If you have integrated your exchange with other BTC and other UTXO-based chains, the architecture presented here will be familiar and you will be able to reuse existing components and patterns. Before jumping into the discussion, it is important to map your preexisting concepts using the following mapping:
  • Transactions are identified in Canton using their globally unique update-id.
  • Each transaction is committed at specific record time that is assigned by the synchronizer used to commit the transaction.
  • Blockheight in BTC can be mapped to the record time of the Global Synchronizer in Canton Network.
  • BTC UTXOs map to what are usually called active contracts in Canton. Every Canton contract carries data of a specific Daml template type. For ease of understanding, we often refer to active contracts as “UTXOs” in this guide.
  • BTC addresses map to parties in Canton.
  • Validator nodes host parties and store their private data. Validator nodes also expose the Ledger API (LAPI), which can be used by an owner of a party to read their party’s state and transactions.
  • Canton is designed as a network-of-networks where each network is a separate synchronizer that is distinct and separate from other synchronizers. For example, the Global Synchronizer is a synchronizer that connects validators in its network.
  • Validator nodes can be connected to multiple synchronizers. Validator nodes merge the data streams from all connected synchronizers into a single logical stream, which is why they assign a local Ledger API offset to every transaction. These offsets are not comparable across validator nodes, but update-ids and record times are.
  • Transactions in Canton have a hierarchical structure that reflects the nested execution and visibility of Daml choices. This hierarchical structure guarantees privacy between parties in the same transaction. Different validator nodes may see different sub-trees of the same transaction depending on which parties they host.
  • Memos are stored in the transfer metadata using the splice.lfdecentralizedtrust.org/reason key. The Canton Network Token Standard defines this key and a way to parse these memo tags and other transfer information from transactions.
This guide provides a sample architecture and workflows for integrating an exchange with Canton. The expectation is that the integration components are reasonably thin wrappers over the functionality provided by the wallet SDK. The guide expects you to provide these components since they are mostly concerned integrating with your exchange’s internal systems and its requirements.

Component Overview

The following diagram shows the components to integrate an exchange’s internal systems with Canton Network. We explain the components in the subsections below. Integration architecture component overview

Exchange Components

This guide assumes that there are Exchange Internal Systems that manage, among other things, the exchange’s internal ledger of Customer balances. These systems serve data to the Exchange UI, which is used by exchange customers to trade, observe their deposits, and request withdrawals of funds to their wallets. The guide’s assumptions might not perfectly match your exchange’s actual architecture. We encourage you to consider them in spirit and map the diagram as best as possible.

Canton Integration Components

There are five Canton integration components:
  • The Exchange Validator Node is a Splice validator node that hosts your treasuryParty, which is the party you setup to control funds, receive deposits, and execute transfers for withdrawals. See exchange-parties-setup for details on how to setup the treasuryParty. You can deploy and operate a validator yourself or use a node-as-a-service provider to operate it for you.
  • The Canton Integration DB is used to keep track of the state of withdrawals and the customer-attribution of the funds held by the treasuryParty. It is shown as a separate component in the diagram, but it could be part of an existing databases.
  • The Tx History Ingestion service uses the JSON Ledger API exposed by the Exchange Validator Node to read Daml transactions affecting the treasuryParty. It parses these transactions and updates the Canton Integration DB with the effect of these transactions (e.g. a successful deposit to a customer account).
  • The Withdrawal Automation service is responsible for executing withdrawals requested by the Exchange Internal Systems via the Canton Integration DB.
  • The Multi-Step Deposit Automation service is responsible for accepting or rejecting transfers from customers to their exchange accounts for CN tokens that do not support direct transfers. It is not necessary for an integration with Canton Coin, which does support direct, 1-step transfers.
You are expected to provide the three services and DBs listed above in a way that is accessible for querying by the Exchange Internal Systems. As explained in the architecture-high-level-overview, you should be able to build these services as thin wrappers over the functionality provided by the wallet SDK and reuse the DB schemas from your existing UTXO-based integrations. We explain the expected functionality of the services and the state they store in the Canton Integration DB in the integration-workflows section.

Third-Party Components

The purpose of the third-party components in the diagram above (in gray) is:
  • The Global Synchronizer serves the validator nodes to commit Daml transactions in a decentralized and fault-tolerant manner.
  • The Customer Validator Node is the validator node that hosts the customerParty which is used by the Customer to hold and transfer their funds.
  • The Customer Wallet is the wallet used by the customer to manage their funds and make transactions.
  • The Admin Validator Node is the validator node used by the token administrator to track the ownership records of the token and validate changes to them. We use the adminParty to refer to the party that represents them on ledger. Note that the adminParty for a decentralized token is hosted on multiple validator nodes. For example the adminParty for Canton Coin is hosted on every SV node.
  • The Registry API Server provides access to extra context to execute token transfers. This context is often only known to the token administrator, which is why access is provided to it off-ledger. The OpenAPI specification of the Registry API is maintained as part the Canton Network Token Standard definitions in the Splice repository.

Information Flows

The following diagram shows the information flows between the components. The main information flows of the Canton integration are highlighted using bold arrows. We explain them below. Information flow diagram There are three main information flows:
  1. Tx History Ingestion: ingests the transactions (tx) affecting the treasuryParty from the Exchange Validator Node into the Canton Integration DB. Arrow 1.a represents the transaction data being read using the /v2/updates/flats Ledger API endpoint using either plain HTTP or websockets. It is parsed by the Tx History Ingestion service to update the status of funds, deposits, and withdrawals in the Canton Integration DB (Arrow 1.b). This data is queried by Exchange Internal Systems (Arrow 1.c), for example to serve the Exchange UI. For brevity, the diagram shows direct access to the Canton Integration DB by the Exchange Internal Systems. However using a micro-services architecture, the Exchange Internal Systems would typically access the Canton Integration DB through a dedicated API layer. Choose whatever architecture best fits your exchange’s needs.
  2. Withdrawal Automation: starts with the Exchange Internal Systems writing a withdrawal request to the Canton Integration DB (Arrow 2.a). The Withdrawal Automation service reads the request from the DB (Arrow 2.b), and prepares, signs, and executes a Canton Network Token standard transfer corresponding to the withdrawal request using the Ledger API (Arrow 2.c). Note that the status of transfers becomes visible in the transaction history ingested by the Tx History Ingestion service; and is communicated to both the Exchange Internal Systems and the Withdrawal Automation service via the Canton Integration DB. This routing of information through the Canton Integration DB is intentional to simplify disaster recovery. Note also that the Withdrawal Automation may write back to the Canton Integration DB to mark a withdrawal as failed.
  3. Multi-Step Deposit Automation: is required to support offer-and-accept style transfers for tokens that do not support direct transfers. It relies on the Tx Ingestion Service to ingest transfer offers as part of Arrow 1.c. The workflow starts with the Multi-Step Deposit Automation service querying the Canton Integration DB to see whether there are pending transfers for deposits from customers (Arrow 3.a). The service then checks whether the deposit address specified in the transfer is known. If yes, it prepares, signs, and executes an accept transaction using the Ledger API (Arrow 3.b). If no, then it takes no action, and lets the transfer offer expire or be withdrawn by the sender. Note that there is an arrow from Multi-Step Deposit Automation back to the Canton Integration DB, as the Multi-Step Deposit Automation may write back to the Canton Integration DB to store that the transaction to accept the deposit could not be committed even after retrying multiple times.
The other information flows interact with the main flows as part of a deposit or withdrawal. We explain them in the integration-workflows section.
This section was copied from existing reviewed documentation. Source: docs/wallet-integration-guide/src/exchange-integration/workflows.rst Reviewers: Skip this section. Remove markers after final approval.

Integration Workflows

Overview

The workflows below are grouped into two milestones.
  • mvp-for-cc contains the minimum viable product (MVP) workflows for integrating Canton Coin (CC) into the exchange. It comes with the limitation that both the exchange and the customers need to set up a TransferPreapproval contract to enable 1-step transfers of CC.
  • mvp-for-cn-tokens contains the additional workflows required to support all CN tokens. They are the workflows to onboard a new token and to support multi-step transfers for both deposits and withdrawals. Multi-step transfers gives the receiver a choice to: reject an incoming transfer as well as enable additional asynchronous checks on transfers by the token admin (e.g. KYC/AML checks).
Further extensions of these two MVPs to address Day-2 requirements are explored in integration-extensions.
add these functions. potentially using sphinx-tabs to allow switching between SDK function view and higher-level description

MVP for Canton Coin

The diagrams in the sections below adapt the diagram from the information-flows section to the Canton Coin workflow. The adaptations are:
  • The role of the adminParty is assumed by the dsoParty, which is the token admin party for CC. The dsoParty is a decentralized party that is hosted on the validator nodes run by Super Validator (SV) operators. A confirmation threshold of 2/3 is used to achieve Byzantine fault-tolerance for its transaction validation.
  • The role of the Registry API Server is taken over by the Canton Coin Scan services that every SV operator runs. They serve the Registry API for CC. See reading-from-canton-coin-scan for more information about how to reliably read from multiple Canton Coin Scan instances.

1-Step Deposit Workflow

1-Step Deposit Workflow Diagram Assumptions:
  • The Exchange has set up a CC TransferPreapproval for their treasuryParty as explained in treasury-party-setup.
  • The Exchange has associated deposit account “abc123” with the Customer in the Canton Integration DB.
Example flow:
  1. Customer uses Exchange UI to retrieve treasuryParty and the deposit account-id (“abc123”) to use for the deposit
  2. Customer uses Customer Wallet to initiate a token standard transfer of 100 CC to treasuryParty with metadata key splice.lfdecentralizedtrust.org/reason set to “abc123”.
    1. Customer Wallet selects CC Holding UTXOs to fund the transfer and queries Canton Coin Scan to retrieve registry-specific TransferFactory and extra transfer context. The returned data includes the TransferPreapproval for the treasuryParty.
    2. Customer wallet submits the command to exercise the TransferFactory_Transfer choice together with the extra transfer context. The resulting transaction:
      • archives the funding CC Holding UTXOs
      • creates a CC Holding UTXO with contract-id coid234 owned by the treasuryParty
      • creates another CC Holding UTXO for the change owned by the Customer.
    3. The resulting transaction gets committed across the Customer, Exchange, and SV validator nodes. It is assigned an update-id upd567 and a record time t1 by the Global Synchronizer. It is assigned offset off1 by the Exchange Validator Node. (The other validator nodes will have a different offset value.)
  3. Tx History Ingestion observes upd567 at record time t1 with offset off1 and updates the Canton Integration DB as follows.
    1. Tx History Ingestion parses upd567 using the token standard tx history parser from the Wallet SDK to determine:
      • The deposit amount of 100 CC.
      • The deposit account “abc123” from the splice.lfdecentralizedtrust.org/reason metadata value.
      • The new Holding UTXO coid234 owned by the treasuryParty
    2. Tx History ingestion writes the following in a single, atomic transaction to the Canton Integration DB
      • The latest ingested update-id upd567, its record time t1, its offset off1, and the synchronizerId of the Global Synchronizer.
      • The new CC Holding UTXO coid234 for the 100 CC that was received.
      • The credit of 100 CC on the Customer’s account at the exchange.
  4. Customer observes the successful deposit in their Exchange UI, whose data is retrieved from the Canton Integration DB via the Exchange Internal Systems.

1-Step Withdrawal Workflow

1-Step Withdrawal Workflow Diagram Assumptions:
  1. Customer set up a CC TransferPreapproval for their customerParty.
Example flow:
  1. Customer requests withdrawal of 100 CC to customerParty using the Exchange UI.
  2. Exchange Internal Systems process that request and update the Canton Integration DB to store:
    • The deduction of 100 CC from the Customer’s trading account.
    • The pending withdrawal with id wid123 of 100 CC to customerParty.
    • The CC Holding UTXOs coids to use to fund the transfer to customerParty for wid123. See utxo-management for more information.
    • The target record time trecTgt on the Global Synchronizer until which the transaction for the CC transfer must be committed. The coids are considered to be reserved for funding the transfer for withdrawal wid123 until trecTgt has passed.
  3. Withdrawal Automation queries the Canton Integration DB in a polling fashion, observes the pending withdrawal wid123, and commits the corresponding CC transfer as follows.
    1. Withdrawal Automation queries Canton Coin Scan to retrieve the TransferFactory for CC and extra transfer context.
    2. Withdrawal automation checks that transfer is indeed a 1-step transfer by checking that transfer_kind = "direct" in the response from Canton Coin Scan. If that is not the case, then it marks the withdrawal as failed in the Canton Integration DB with reason “lack of CC transfer-preapproval for customerParty” and stops processing.
    3. Withdrawal Automation prepares, signs, and submits the command to exercise the TransferFactory_Transfer choice with the exclusive upper-bound for the record time of the commit set to trecTgt. It also sets the value for key splice.lfdecentralizedtrust.org/reason in the Transfer metadata to wid123.
    4. The resulting transaction:
      • archives the CC Holding UTXOs coids used to fund the transfer
      • creates a CC Holding UTXO with contract-id coid345 owned by the customerParty
      • creates a CC Holding UTXO with contract-id coid789 owned by treasuryParty representing the change returned to the Exchange.
      The transaction is committed across the Customer, Exchange, and SV validator nodes. It is assigned an update-id upd567 and a record time t1 < trecTgt by the Global Synchronizer. It is assigned off1 by the Exchange Validator Node. It is assigned off2 by the Customer Validator Node.
  4. Tx History Ingestion observes upd567 at t1 with offset off1 and updates the Canton Integration DB as follows.
    1. Tx History Ingestion parses upd567 using the token standard tx history parser from the Wallet SDK to determine:
      • The withdrawal-id wid123 from the splice.lfdecentralizedtrust.org/reason metadata value.
      • The new Holding UTXO coid789 owned by the treasuryParty
    2. Tx History ingestion writes the following in a single, atomic transaction to the Canton Integration DB
      • The latest ingested update-id upd567, its record time t1 and offset off1.
      • The successful completion of withdrawal wid123 by the transaction with update-id upd567 at record time t1.
      • The deduction of 100 CC from the Customer’s trading account.
      • The archival of the CC Holding UTXOs coids.
      • The new CC Holding UTXO coid789 for the change returned after funding the CC transfer.
  5. Customer Wallet observes upd567 at t1 with offset off2 on the Customer Validator Node, parses it using the token standard tx history parser and updates its UI as follows:
    • Its tx history shows the receipt of 100 CC from exchangeParty with “Reason” wid123 that was committed as update upd567 at t1.
    • Its holding listing shows the new CC Holding with contract id coid345.
  6. Customer observes the completion of the withdrawal at t1 in the Exchange UI and the receipt of the expected funds in their Customer Wallet.

UTXO Selection and Management

Executing a withdrawal requires selecting Holding UTXOs to fund the withdrawal, as described for example in one-step-withdrawal-workflow. You likely already have a UTXO management strategy in place for your existing UTXO-chain integrations. Here some considerations to take into account when adapting your strategy to work with Canton:
  • Canton Coin charges a small holding fee of about $1 per year for each Holding UTXO to allow archiving dust coins once their holding fee surpasses their value.
  • Canton Coin limits the number of UTXOs for a single transfer to 100 Holding UTXOs to avoid large transactions that are expensive to process.
  • Canton Coin transactions also merge all input Holding UTXOs and return the change to the sender as a single Holding UTXO to allow batching the merging of Holding UTXOs with transfers.
  • Other tokens are likely to follow similar strategies for the same rationale.
  • At the time of writing (2025-08-29), the Canton Network Token Standard recommends to use self-transfers (i.e., sender = receiver) to be used to merge Holding UTXOs into two Holding UTXOs: one for the transferred amount and another one for the change. It does not (yet) support requesting multiple Holding UTXOs to be created for the change.
We therefore recommend the following approach:
  • Limit the number of input UTXOs to less than 100 UTXOs per transfer. Thus staying with the Canton Coin limits and keeping transaction size small, which also helps you to reduce your traffic spend when having to retry transaction execution.
  • Consider using a UTXO selection strategy for withdrawals that favors smaller UTXOs so that they get merged automatically as part of executing transfers.
  • Consider keeping a pool of k large amount UTXOs to be able to execute up to k withdrawals at the same time. Run a periodic background job to manage this pool using self-transfers.
    • From an implementation perspective, these self-transfers are a special kind of withdrawal. We thus recommend to implement them using the same code path as withdrawals: start with writing the self-transfer request into the Canton Integration DB and have the Withdrawal Automation execute it.

MVP for all Canton Network Tokens

The MVP for supporting all Canton Network tokens builds on the MVP for Canton Coin. The key changes required are:
  • Change Tx History Ingestion to also ingest the TransferInstruction UTXOs, which are used by the Canton Network Token Standard to represent in-progress transfers (see docs, code).
  • Adjust the Exchange UI to show the status of in-progress transfers.
  • Adjust the user funds tracking done as part of Tx History Ingestion to credit funds back to the user if they reject a withdrawal transfer. Consider deducting a fee for the failed withdrawal.
  • Implement the Multi-Step Deposit Automation service to auto-accept incoming transfers that are pending receiver acceptance. Ensure that the deposit address is known before accepting the transfer.
  • Add support for configuring the URL of a token admin’s Registry API Server and to deploy their .dar files as described in token-onboarding.
The sections below provide worked examples for the resulting multi-step deposit and withdrawal workflows. All examples assume that:
  1. There is a token admin called Acme who issues a token called AcmeToken on the Canton Network and operates their own Admin Validator Node and their own Registry API Server.
  2. The Exchange and Customer have onboarded AcmeToken as per token-onboarding.

Multi-Step Deposit Workflow

Multi-Step Deposit Workflow Diagram

Example flow: deposit offer and acceptance

The flow uses essentially the same initial four steps as the one-step-deposit-workflow above. We list them in full for completeness.
  1. Customer uses Exchange UI to retrieve treasuryParty and deposit account-id “abc123” to use for the deposit.
  2. Customer uses Customer Wallet to initiate a token standard transfer of 100 AcmeToken to treasuryParty with metadata key splice.lfdecentralizedtrust.org/reason set to “abc123”.
    1. Customer Wallet selects AcmeToken Holding UTXOs to fund the transfer and queries Acme’s Registry API Server to retrieve registry-specific TransferFactory and extra transfer context. The URL for this server was configured in the Customer Wallet as part of token-onboarding.
    2. Customer wallet submits the command to exercise the TransferFactory_Transfer choice together with the extra transfer context. The resulting transaction:
      • archives the funding AcmeToken Holding UTXOs
      • creates a locked 100 AcmeToken Holding UTXO with contract-id coid234 owned by the customerParty
      • creates another AcmeToken Holding UTXO for the change owned by the Customer.
      The transaction also creates a TransferInstruction UTXO with contract-id coid567, which represents the transfer offer to the Exchange.
    3. The resulting transaction gets committed across the Customer, Exchange, and Acme validator nodes. It is assigned an update-id upd567 and a record time t1 by the Global Synchronizer. It is assigned offset off1 by the Exchange Validator Node.
  3. Tx History Ingestion observes upd567 at t1 with offset off1 and updates the Canton Integration DB as follows.
    1. Tx History Ingestion parses upd567 using the token standard tx history parser from the Wallet SDK to determine:
      • The deposit amount of 100 AcmeToken.
      • The deposit account “abc123” from the splice.lfdecentralizedtrust.org/reason metadata value.
      • The TransferInstruction UTXO coid567 representing the transfer offer for the deposit.
    2. Tx History ingestion writes the following in a single, atomic transaction to the Canton Integration DB
      • The latest ingested update-id upd567 its record time t1 and offset off1.
      • The TransferInstruction UTXO coid567 representing the transfer offer from customerParty for a deposit of 100 AcmeToken in account “abc123”.
  4. Customer Wallet ingests update upd567 and Customer observes the pending transfer offer for the deposit in the Customer Wallet. Customer also sees the 100 AcmeToken Holding UTXO coid234 locked to the deposit.
This is where the main difference to the one-step-deposit-workflow starts. The Multi-Step Deposit Automation service will now auto-accept the transfer offer.
  1. The Multi-Step Deposit Automation regularly queries the Canton Integration DB for pending transfer offers for known deposit accounts. It thus observes the pending transfer offer coid567 and accepts it as follows.
    1. Multi-Step Deposit Automation retrieves the URL for Acme’s Registry API Server from the Canton Integration DB.
    2. Multi-Step Deposit Automation queries Acme’s Registry API Server to retrieve the extra context to exercise the TransferInstruction_Accept choice on coid567.
    3. Multi-Step Deposit Automation prepares, signs, and submits the command to exercise the TransferInstruction_Accept choice on coid567.
    4. The resulting transaction gets committed across the Customer, Exchange, and Acme validator nodes. It is assigned an update-id upd789 and a record time t2 the Global Synchronizer. It is assigned off3 by the Exchange Validator Node. The resulting transaction has the following effects:
      • It archives the TransferInstruction UTXO coid567.
      • It archives the locked 100 AcmeToken Holding UTXO coid234 owned by the customerParty.
      • It creates a 100 AcmeToken Holding UTXO coid999 owned by the treasuryParty.
At this point the workflow again proceeds the same way as the one-step-deposit-workflow.
  1. Tx History Ingestion observes upd789 at t2 with offset off3 and updates the Canton Integration DB as follows.
    1. Tx History Ingestion parses upd789 using the token standard tx history parser from the Wallet SDK to determine:
      • The deposit amount of 100 AcmeToken.
      • The deposit account “abc123” from the splice.lfdecentralizedtrust.org/reason metadata value.
    2. Tx History ingestion writes the following in a single, atomic transaction to the Canton Integration DB
      • The latest ingested update-id upd789, its record time t2 and offset off3.
      • The new AcmeToken Holding UTXO coid999 for the 100 AcmeToken that was received.
      • The credit of 100 AcmeToken on the Customer’s account at the exchange.
  2. Customer Wallet observes upd789 at t2 on the Customer Validator Node, parses it using the token standard tx history parser and updates its UI as follows:
    • Its tx history shows the successful transfer of 100 AcmeToken to exchangeParty with “Reason” wid123 that was committed as update upd789 at t2.
  3. Customer observes the successful deposit in their Exchange UI, whose data is retrieved from the Canton Integration DB via the Exchange Internal Systems.

Example: handling deposits with unknown deposit accounts

To minimize traffic cost, we recommend not acting on deposits with unknown deposit accounts. The sender can use their wallet to withdraw the offer. Ingesting deposit offers with unknown deposit accounts is still valuable to allow the exchange’s support team to handle customer inquiries about these transfers.

Multi-Step Withdrawal Workflow

Multi-Step Withdrawal Workflow

Example flow: withdrawal offer and acceptance

The flow uses essentially the same initial six steps as the one-step-withdrawal-workflow above. We list them in full for completeness.
  1. Customer requests withdrawal of 100 AcmeToken to customerParty using the Exchange UI.
  2. Exchange Internal Systems process that request and update the Canton Integration DB to store:
    • The deduction of 100 AcmeToken from the Customer’s trading account.
    • The pending withdrawal with id wid123 of 100 AcmeToken to customerParty.
    • The AcmeToken Holding UTXOs coids to use to fund the transfer to customerParty for wid123. See utxo-management for more information.
    • The target record time trecTgt on the Global Synchronizer until which the transaction for the AcmeToken transfer must be committed using the coids UTXOs for funding wid123. The coids are considered to be reserved to funding this transfer until trecTgt has passed.
  3. Withdrawal Automation queries the Canton Integration DB in a polling fashion, observes the pending withdrawal wid123, and commits the corresponding AcmeToken transfer as follows.
    1. Withdrawal Automation retrieves the URL for Acme’s Registry API Server from the Canton Integration DB.
    2. Withdrawal Automation queries Acme’s Registry API Server to retrieve the TransferFactory for AcmeToken and extra transfer context.
    3. Withdrawal Automation prepares, signs, and submits the command to exercise the TransferFactory_Transfer choice with the exclusive upper-bound for the record time of the commit set to trecTgt. It also sets the value for key splice.lfdecentralizedtrust.org/reason in the Transfer metadata to wid123; and it sets the upper bound for the customer to accept the transfer far enough in the future, so that the customer has sufficient time to act (e.g. 1 year).
    4. The resulting transaction gets committed across the Customer, Exchange, and Acme validator nodes. It is assigned an update-id upd567 and a record time t1 < trecTgt by the Global Synchronizer. It is assigned off1 by the Exchange Validator Node. It is assigned off2 by the Customer Validator Node. The resulting transaction has the following effects:
      • It archives the AcmeToken Holding UTXOs coids used to fund the transfer.
      • It creates an AcmeToken Holding UTXO with contract-id coid789 owned by treasuryParty representing the change returned to the Exchange.
      • It creates one locked AcmeToken Holding UTXO with amount 100 and contract-id coid345 owned by the treasuryParty.
      • It creates a TransferInstruction UTXO with contract-id coid567 representing the transfer offer. This TransferInstruction includes a copy of the Transfer specification and its metadata.
  4. Tx History Ingestion observes upd567 at t1 with offset off1 and updates the Canton Integration DB as follows.
    1. Tx History Ingestion parses upd567 using the token standard tx history parser from the Wallet SDK to determine:
      • The withdrawal-id wid123 from the splice.lfdecentralizedtrust.org/reason metadata value.
      • The new locked AcmeToken Holding UTXO coid345 owned by the treasuryParty and locked to the withdrawal wid123 of 100 AcmeToken to customerParty.
      • The new AcmeToken Holding UTXO coid789 owned by the treasuryParty
      • The TransferInstruction UTXO coid567 representing the transfer offer for the withdrawal.
    2. Tx History ingestion writes the following in a single, atomic transaction to the Canton Integration DB:
      • The latest ingested update-id upd567, its record time t1 and offset off1.
      • The successful transfer offer for withdrawal wid123 by the transaction with update-id upd567 at record time t1.
      • The Holding UTXO coid345 locked to the withdrawal.
      • The TransferInstruction UTXO coid567 representing the transfer offer.
      • The archival of the AcmeToken Holding UTXOs coids.
      • The new AcmeToken Holding UTXO coid789 for the change returned after funding the AcmeToken transfer.
  5. Exchange UI displays that withdrawal wid123 is pending transfer offer acceptance by the Customer.
  6. Customer Wallet observes update with update-id upd567 at t1 with offset off2 on the Customer Validator Node.
    1. It parses the transaction using the token standard transaction history parser and updates its UI so that its transaction history shows the offer for a transfer of 100 AcmeToken from exchangeParty with “Reason” wid123 that was committed as update upd567 at t1.
This is where the main difference to the one-step-withdrawal-workflow starts. The customer has a choice whether to accept or reject the transfer offer. Here they choose to accept it.
  1. Customer uses their Customer Wallet to accept the offer using the TransferInstruction_Accept choice.
    1. The resulting transaction is committed across Exchange, Acme, and Customer validator nodes and assigned update-id upd789 and record time t2. The transaction has the following effects:
      • It archives the locked Holding UTXO coid345.
      • It archives the TransferInstruction UTXO coid567.
      • It creates a 100 AcmeToken Holding UTXO coid999 owned by the customerParty.
  2. Tx History Ingestion observes update upd789 at t2 and offset off3 assigned by the Exchange Validator Node.
    1. It parses the update using the token standard parser to extract the withdrawal-id wid123 from the splice.lfdecentralizedtrust.org/reason metadata value.
    2. Tx History Ingestion writes the following in a single, atomic transaction to the Canton Integration DB
      • The latest ingested update-id upd789, its record time t2 and offset off3.
      • The successful completion of the withdrawal wid123 by the transaction with update-id upd789 at record time t2.
      • The archival of the locked AcmeToken Holding UTXO coid345.
  3. Customer Wallet observes upd789 at t2 and updates its display to reflect its effects.
  4. Customer observes the completion of the withdrawal at t2 in Exchange UI and confirms the receipt of funds in their Customer Wallet.

Example flow: customer rejects transfer offer

The Customer might decide to reject the offer in Step 7 in the example above. The corresponding transaction will
  • archive the locked Holding UTXO coid345,
  • archive the TransferInstruction UTXO coid567, and
  • create a new 100 AcmeToken Holding UTXO coid999 owned by the treasuryParty.
Steps 8 - 10 are largely the same as for the successful acceptance with the difference that Tx History Ingestion will see this transaction and update the Canton Integration DB to such that
  • withdrawal wid123 is marked as failed because the customer rejected the offer, and
  • the customer account is credited back the 100 AcmeToken, potentially minus a fee for the failed withdrawal.
And the user will ultimately see in both the Exchange UI and the Customer Wallet that the transfer was offered, but rejected by them.
In most cases a TransferInstruction will be completed in a single extra step: the receiver either accepts or rejects the transfer, or the sender withdraws it. Each of these steps will manifest as one of the choices on the TransferInstruction interface (code) and its TransferInstructionResult.output value clearly tells whether the instruction completed with a successful transfer, failed, or is still pending an action by one of the stakeholders.

Canton Network Token Onboarding

You likely have requirements and considerations for onboarding a token. In the following, we document the additional considerations that are specific to Canton. At a high-level, the Canton-specific steps to onboarding a token are:
  1. Upload the token admin’s .dar files to your validator node.
  2. Store the mapping from the token admin’s adminParty id to the admin’s Registry API Server URL in your Canton Integration DB (or another suitable place).
  3. In case the token is permissioned, follow the token admin’s instructions to have your exchange’s treasuryParty added to the token’s allowlist.
Make sure that you only upload .dar files from trusted token admins to avoid unwanted changes to the behavior of your existing contracts on-ledger. Many token admin’s run a test instance of their token on TestNet. Consider using these test instances as part of your testing strategy. For example, Canton Coin also exist on TestNet and DevNet with different dsoParty ids. You can retrieve the dsoParty id for each network using the CC Scan API served from the SV nodes of that network:
  • Use /v0/dso to query the dsoParty for the network you are connected to.
  • Use /v0/splice-instance-names to query the network name (DevNet, TestNet, or MainNet).
This section was copied from existing reviewed documentation. Source: docs/wallet-integration-guide/src/exchange-integration/txingestion.rst Reviewers: Skip this section. Remove markers after final approval.

Transaction History Ingestion Details

Offset Checkpoints

When consuming transactions through the update service at /v2/updates you will not just receive transactions but you will also receive offset checkpoints. Each offset checkpoint contains an offset and the most recent observed record time for each synchronizer. Your Tx History Ingestion should use that to update the last processed offset and record time (in addition to updating those after each transaction) so that it will resume processing transactions from that point on after a crash or restart. Offset checkpoints are in particular required around Major Splice Upgrades where there is no Daml transaction for an extended period of time, but you want to ensure that your Tx History Ingestion advances beyond a particular record time.

Transaction Parsing

As part of the integration-workflows, Tx History Ingestion is expected to extract a number of fields for both deposits and withdrawals. Below we provide details on the transaction structure of the integration-workflows and how to parse it.
The following code is available to help you implement your own parsing logic:

1-Step Transfers

To understand the structure of a 1-step transfer, let’s look at an example deposit as seen through the JSON Ledger API. In this case, we query a single transaction. The format is identical to the transaction you will get when streaming transactions through /v2/updates/flats and you can also use the same filter. Note that you need to adjust the auth-token, update-id and treasury-party placeholders to match your setup.
curl -sSL --fail-with-body http://json-api-url/v2/updates/update-by-id \
    -H 'Authorization: Bearer <authtoken>' \
      -d '{
            "updateId": "&lt;update-id&gt;",
            "updateFormat": {
              "includeTransactions": {
                "transactionShape": "TRANSACTION_SHAPE_LEDGER_EFFECTS",
                "eventFormat": {
                  "filtersByParty": {
                    "&lt;treasury-party&gt;": {
                      "cumulative": [
                        {"identifierFilter": {"WildcardFilter": {"value": {"includeCreatedEventBlob": false}}}},
                        {"identifierFilter": {"InterfaceFilter": {"value": {"interfaceId": "#splice-api-token-transfer-instruction-v1:Splice.Api.Token.TransferInstructionV1:TransferFactory", "includeInterfaceView": true, "includeCreatedEventBlob": false}}}},
                        {"identifierFilter": {"InterfaceFilter": {"value": {"interfaceId": "#splice-api-token-holding-v1:Splice.Api.Token.HoldingV1:Holding", "includeInterfaceView": true, "includeCreatedEventBlob": false}}}},
                        {"identifierFilter": {"InterfaceFilter": {"value": {"interfaceId": "#splice-api-token-transfer-instruction-v1:Splice.Api.Token.TransferInstructionV1:TransferInstruction", "includeInterfaceView": true, "includeCreatedEventBlob": false}}}}
                      ]
                    }
                  },
                  "verbose": true
                }
              }
            }
          }'
{
    "update": {
        "Transaction": {
            "value": {
                "updateId": "122008a4699e61ce682917c9515ecb3b4426adf276441e41edede8b3862efa2de80e",
                "commandId": "582f81e4-86e4-48fa-ad95-607e9ebe8c9b",
                "workflowId": "",
                "effectiveAt": "1970-01-01T00:01:00Z",
                "events": [
                    {
                        "ExercisedEvent": {
                            "offset": 107,
                            "nodeId": 4,
                            "contractId": "0003113864953b90e689737a131569e8758df3cf82e3db12c89f010ac330276f3cca1112204bc3e9f7d695217e06aec436ac87a441c8e1a0f0d3ae2668c38e408fa2d5ebda",
                            "templateId": "6e9fc50fb94e56751b49f09ba2dc84da53a9d7cff08115ebb4f6b7a12d0c990c:Splice.AmuletRules:TransferPreapproval",
                            "interfaceId": null,
                            "choice": "TransferPreapproval_Send",
                            "choiceArgument": {
                                "context": {
                                    "amuletRules": "002402fc37d1f6fcb5c9247342c66659f95eb03efebba3da8c244ae7c10925aae2ca1112201ac4dd28e75b1ba2be4df65e674b0c66fa2ec934abc15824584d8566af4916e9",
                                    "context": {
                                        "openMiningRound": "009d18bf51238bb679b45ac760d418d31d95fead0538971a26eff6d2b2d582005dca1112204d969f7a6e0d271b3a85b27297879812e8c0fdaaaf8d72d64441a06556bb5955",
                                        "issuingMiningRounds": [],
                                        "validatorRights": [],
                                        "featuredAppRight": null
                                    }
                                },
                                "inputs": [
                                    {
                                        "tag": "InputAmulet",
                                        "value": "009b939ae451ef1a0cb81d1606391406690e055b5be301fd2f51efb6be5675577eca1112200f58604ac538224f73bdc57117d73830ed1e3167f956d66f9e3ecdacbf2359a7"
                                    }
                                ],
                                "amount": "200.0000000000",
                                "sender": "sender::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd",
                                "description": "token-standard-transfer-description"
                            },
                            "actingParties": [
                                "sender::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd"
                            ],
                            "consuming": false,
                            "witnessParties": [
                                "treasury-party::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd"
                            ],
                            "lastDescendantNodeId": 12,
                            "exerciseResult": {
                                "result": {
                                    "round": {
                                        "number": "1"
                                    },
                                    "summary": {
                                        "inputAppRewardAmount": "0.0000000000",
                                        "inputValidatorRewardAmount": "0.0000000000",
                                        "inputSvRewardAmount": "0.0000000000",
                                        "inputAmuletAmount": "199877.3600000000",
                                        "balanceChanges": [
                                            [
                                                "sender::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd",
                                                {
                                                    "changeToInitialAmountAsOfRoundZero": "-214.0000000000",
                                                    "changeToHoldingFeesRate": "0.0000000000"
                                                }
                                            ],
                                            [
                                                "treasury-party::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd",
                                                {
                                                    "changeToInitialAmountAsOfRoundZero": "200.0038051800",
                                                    "changeToHoldingFeesRate": "0.0038051800"
                                                }
                                            ]
                                        ],
                                        "holdingFees": "0.0000000000",
                                        "outputFees": ["8.0000000000"],
                                        "senderChangeFee": "6.0000000000",
                                        "senderChangeAmount": "199663.3600000000",
                                        "amuletPrice": "0.0050000000",
                                        "inputValidatorFaucetAmount": "0.0000000000",
                                        "inputUnclaimedActivityRecordAmount": "0.0000000000"
                                    },
                                    "createdAmulets": [
                                        {
                                            "tag": "TransferResultAmulet",
                                            "value": "008f783cd288ce926f8bc973df7ddb719e0b8c941dd84cd9a6ca0240fb7ecff390ca111220f5d7073ad68f0746851954e570f348d099a323b796f71807caf71871fce6d956"
                                        }
                                    ],
                                    "senderChangeAmulet": "00431eabcbb8f4293ccc0e17764bca260ff64aede386245c033b2d0ebddc2cbb21ca111220e755e3b05c909e84ffbc47fa4be1e1b786af37e76479efd348a0ae2722149904",
                                    "meta": null
                                },
                                "meta": {
                                    "values": {
                                        "splice.lfdecentralizedtrust.org/burned": "14.0",
                                        "splice.lfdecentralizedtrust.org/reason": "deposit-account-id",
                                        "splice.lfdecentralizedtrust.org/sender": "sender::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd",
                                        "splice.lfdecentralizedtrust.org/tx-kind": "transfer"
                                    }
                                }
                            },
                            "packageName": "splice-amulet",
                            "implementedInterfaces": []
                        }
                    },
                    {
                        "ExercisedEvent": {
                            "offset": 107,
                            "nodeId": 5,
                            "contractId": "002402fc37d1f6fcb5c9247342c66659f95eb03efebba3da8c244ae7c10925aae2ca1112201ac4dd28e75b1ba2be4df65e674b0c66fa2ec934abc15824584d8566af4916e9",
                            "templateId": "6e9fc50fb94e56751b49f09ba2dc84da53a9d7cff08115ebb4f6b7a12d0c990c:Splice.AmuletRules:AmuletRules",
                            "interfaceId": null,
                            "choice": "AmuletRules_Transfer",
                            "choiceArgument": {
                                "transfer": {
                                    "sender": "sender::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd",
                                    "provider": "sender::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd",
                                    "inputs": [
                                        {
                                            "tag": "InputAmulet",
                                            "value": "009b939ae451ef1a0cb81d1606391406690e055b5be301fd2f51efb6be5675577eca1112200f58604ac538224f73bdc57117d73830ed1e3167f956d66f9e3ecdacbf2359a7"
                                        }
                                    ],
                                    "outputs": [
                                        {
                                            "receiver": "treasury-party::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd",
                                            "receiverFeeRatio": "0.0000000000",
                                            "amount": "200.0000000000",
                                            "lock": null
                                        }
                                    ],
                                    "beneficiaries": null
                                },
                                "context": {
                                    "openMiningRound": "009d18bf51238bb679b45ac760d418d31d95fead0538971a26eff6d2b2d582005dca1112204d969f7a6e0d271b3a85b27297879812e8c0fdaaaf8d72d64441a06556bb5955",
                                    "issuingMiningRounds": [],
                                    "validatorRights": [],
                                    "featuredAppRight": null
                                },
                                "expectedDso": "DSO::12204b8b621ec1dedd51ee2510085f8164cad194953496494d32f541f3f2c170e962"
                            },
                            "actingParties": [
                                "sender::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd",
                                "treasury-party::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd"
                            ],
                            "consuming": false,
                            "witnessParties": [
                                "treasury-party::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd"
                            ],
                            "lastDescendantNodeId": 12,
                            "exerciseResult": {
                                "round": {
                                    "number": "1"
                                },
                                "summary": {
                                    "inputAppRewardAmount": "0.0000000000",
                                    "inputValidatorRewardAmount": "0.0000000000",
                                    "inputSvRewardAmount": "0.0000000000",
                                    "inputAmuletAmount": "199877.3600000000",
                                    "balanceChanges": [
                                        [
                                            "sender::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd",
                                            {
                                                "changeToInitialAmountAsOfRoundZero": "-214.0000000000",
                                                "changeToHoldingFeesRate": "0.0000000000"
                                            }
                                        ],
                                        [
                                            "treasury-party::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd",
                                            {
                                                "changeToInitialAmountAsOfRoundZero": "200.0038051800",
                                                "changeToHoldingFeesRate": "0.0038051800"
                                            }
                                        ]
                                    ],
                                    "holdingFees": "0.0000000000",
                                    "outputFees": ["8.0000000000"],
                                    "senderChangeFee": "6.0000000000",
                                    "senderChangeAmount": "199663.3600000000",
                                    "amuletPrice": "0.0050000000",
                                    "inputValidatorFaucetAmount": "0.0000000000",
                                    "inputUnclaimedActivityRecordAmount": "0.0000000000"
                                },
                                "createdAmulets": [
                                    {
                                        "tag": "TransferResultAmulet",
                                        "value": "008f783cd288ce926f8bc973df7ddb719e0b8c941dd84cd9a6ca0240fb7ecff390ca111220f5d7073ad68f0746851954e570f348d099a323b796f71807caf71871fce6d956"
                                    }
                                ],
                                "senderChangeAmulet": "00431eabcbb8f4293ccc0e17764bca260ff64aede386245c033b2d0ebddc2cbb21ca111220e755e3b05c909e84ffbc47fa4be1e1b786af37e76479efd348a0ae2722149904",
                                "meta": {
                                    "values": {
                                        "splice.lfdecentralizedtrust.org/burned": "14.0",
                                        "splice.lfdecentralizedtrust.org/sender": "sender::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd",
                                        "splice.lfdecentralizedtrust.org/tx-kind": "transfer"
                                    }
                                }
                            },
                            "packageName": "splice-amulet",
                            "implementedInterfaces": []
                        }
                    },
                    {
                        "ExercisedEvent": {
                            "offset": 107,
                            "nodeId": 8,
                            "contractId": "009b939ae451ef1a0cb81d1606391406690e055b5be301fd2f51efb6be5675577eca1112200f58604ac538224f73bdc57117d73830ed1e3167f956d66f9e3ecdacbf2359a7",
                            "templateId": "6e9fc50fb94e56751b49f09ba2dc84da53a9d7cff08115ebb4f6b7a12d0c990c:Splice.Amulet:Amulet",
                            "interfaceId": null,
                            "choice": "Archive",
                            "choiceArgument": {},
                            "actingParties": [
                                "DSO::12204b8b621ec1dedd51ee2510085f8164cad194953496494d32f541f3f2c170e962",
                                "sender::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd"
                            ],
                            "consuming": true,
                            "witnessParties": [
                                "treasury-party::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd"
                            ],
                            "lastDescendantNodeId": 8,
                            "exerciseResult": {},
                            "packageName": "splice-amulet",
                            "implementedInterfaces": [
                                "718a0f77e505a8de22f188bd4c87fe74101274e9d4cb1bfac7d09aec7158d35b:Splice.Api.Token.HoldingV1:Holding"
                            ]
                        }
                    },
                    {
                        "CreatedEvent": {
                            "offset": 107,
                            "nodeId": 9,
                            "contractId": "004d3b89582b1d286a067ea783675350f61fe1d700319deeaa5fc35a81f9357172ca111220c529ebbcad9fcce4d6a6fbc3eda16ce0155f93896df5ba212d1e959b289814b3",
                            "templateId": "6e9fc50fb94e56751b49f09ba2dc84da53a9d7cff08115ebb4f6b7a12d0c990c:Splice.Amulet:ValidatorRewardCoupon",
                            "contractKey": null,
                            "createArgument": {
                                "dso": "DSO::12204b8b621ec1dedd51ee2510085f8164cad194953496494d32f541f3f2c170e962",
                                "user": "sender::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd",
                                "amount": "8.0000000000",
                                "round": {
                                    "number": "1"
                                }
                            },
                            "createdEventBlob": "",
                            "interfaceViews": [],
                            "witnessParties": [
                                "treasury-party::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd"
                            ],
                            "signatories": [
                                "DSO::12204b8b621ec1dedd51ee2510085f8164cad194953496494d32f541f3f2c170e962"
                            ],
                            "observers": [
                                "sender::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd"
                            ],
                            "createdAt": "1970-01-01T00:01:00Z",
                            "packageName": "splice-amulet"
                        }
                    },
                    {
                        "CreatedEvent": {
                            "offset": 107,
                            "nodeId": 10,
                            "contractId": "002a7815f107134bfd776bbc50bb7ead071a050cd651e3d7d15f6ac1f970403558ca111220554d2712f9bf057953688d5fc2aaab8d890e37fe9023f4522a4a53a40d4cf538",
                            "templateId": "6e9fc50fb94e56751b49f09ba2dc84da53a9d7cff08115ebb4f6b7a12d0c990c:Splice.Amulet:AppRewardCoupon",
                            "contractKey": null,
                            "createArgument": {
                                "dso": "DSO::12204b8b621ec1dedd51ee2510085f8164cad194953496494d32f541f3f2c170e962",
                                "provider": "sender::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd",
                                "featured": false,
                                "amount": "8.0000000000",
                                "round": {
                                    "number": "1"
                                },
                                "beneficiary": "sender::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd"
                            },
                            "createdEventBlob": "",
                            "interfaceViews": [],
                            "witnessParties": [
                                "treasury-party::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd"
                            ],
                            "signatories": [
                                "DSO::12204b8b621ec1dedd51ee2510085f8164cad194953496494d32f541f3f2c170e962"
                            ],
                            "observers": [
                                "sender::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd"
                            ],
                            "createdAt": "1970-01-01T00:01:00Z",
                            "packageName": "splice-amulet"
                        }
                    },
                    {
                        "CreatedEvent": {
                            "offset": 107,
                            "nodeId": 11,
                            "contractId": "008f783cd288ce926f8bc973df7ddb719e0b8c941dd84cd9a6ca0240fb7ecff390ca111220f5d7073ad68f0746851954e570f348d099a323b796f71807caf71871fce6d956",
                            "templateId": "6e9fc50fb94e56751b49f09ba2dc84da53a9d7cff08115ebb4f6b7a12d0c990c:Splice.Amulet:Amulet",
                            "contractKey": null,
                            "createArgument": {
                                "dso": "DSO::12204b8b621ec1dedd51ee2510085f8164cad194953496494d32f541f3f2c170e962",
                                "owner": "treasury-party::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd",
                                "amount": {
                                    "initialAmount": "200.0000000000",
                                    "createdAt": {
                                        "number": "1"
                                    },
                                    "ratePerRound": {
                                        "rate": "0.0038051800"
                                    }
                                }
                            },
                            "createdEventBlob": "",
                            "interfaceViews": [
                                {
                                    "interfaceId": "718a0f77e505a8de22f188bd4c87fe74101274e9d4cb1bfac7d09aec7158d35b:Splice.Api.Token.HoldingV1:Holding",
                                    "viewStatus": {
                                        "code": 0,
                                        "message": "",
                                        "details": []
                                    },
                                    "viewValue": {
                                        "owner": "treasury-party::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd",
                                        "instrumentId": {
                                            "admin": "DSO::12204b8b621ec1dedd51ee2510085f8164cad194953496494d32f541f3f2c170e962",
                                            "id": "Amulet"
                                        },
                                        "amount": "200.0000000000",
                                        "lock": null,
                                        "meta": {
                                            "values": {
                                                "amulet.splice.lfdecentralizedtrust.org/created-in-round": "1",
                                                "amulet.splice.lfdecentralizedtrust.org/rate-per-round": "0.00380518"
                                            }
                                        }
                                    }
                                }
                            ],
                            "witnessParties": [
                                "treasury-party::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd"
                            ],
                            "signatories": [
                                "DSO::12204b8b621ec1dedd51ee2510085f8164cad194953496494d32f541f3f2c170e962",
                                "treasury-party::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd"
                            ],
                            "observers": [],
                            "createdAt": "1970-01-01T00:01:00Z",
                            "packageName": "splice-amulet"
                        }
                    },
                    {
                        "CreatedEvent": {
                            "offset": 107,
                            "nodeId": 12,
                            "contractId": "00431eabcbb8f4293ccc0e17764bca260ff64aede386245c033b2d0ebddc2cbb21ca111220e755e3b05c909e84ffbc47fa4be1e1b786af37e76479efd348a0ae2722149904",
                            "templateId": "6e9fc50fb94e56751b49f09ba2dc84da53a9d7cff08115ebb4f6b7a12d0c990c:Splice.Amulet:Amulet",
                            "contractKey": null,
                            "createArgument": {
                                "dso": "DSO::12204b8b621ec1dedd51ee2510085f8164cad194953496494d32f541f3f2c170e962",
                                "owner": "sender::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd",
                                "amount": {
                                    "initialAmount": "199663.3600000000",
                                    "createdAt": {
                                        "number": "1"
                                    },
                                    "ratePerRound": {
                                        "rate": "0.0038051800"
                                    }
                                }
                            },
                            "createdEventBlob": "",
                            "interfaceViews": [
                                {
                                    "interfaceId": "718a0f77e505a8de22f188bd4c87fe74101274e9d4cb1bfac7d09aec7158d35b:Splice.Api.Token.HoldingV1:Holding",
                                    "viewStatus": {
                                        "code": 0,
                                        "message": "",
                                        "details": []
                                    },
                                    "viewValue": {
                                        "owner": "sender::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd",
                                        "instrumentId": {
                                            "admin": "DSO::12204b8b621ec1dedd51ee2510085f8164cad194953496494d32f541f3f2c170e962",
                                            "id": "Amulet"
                                        },
                                        "amount": "199663.3600000000",
                                        "lock": null,
                                        "meta": {
                                            "values": {
                                                "amulet.splice.lfdecentralizedtrust.org/created-in-round": "1",
                                                "amulet.splice.lfdecentralizedtrust.org/rate-per-round": "0.00380518"
                                            }
                                        }
                                    }
                                }
                            ],
                            "witnessParties": [
                                "treasury-party::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd"
                            ],
                            "signatories": [
                                "DSO::12204b8b621ec1dedd51ee2510085f8164cad194953496494d32f541f3f2c170e962",
                                "sender::1220278a4a0eb2c244b425dff62853ef1cd04ca1095bffcea465c0de766faf9ab8cd"
                            ],
                            "observers": [],
                            "createdAt": "1970-01-01T00:01:00Z",
                            "packageName": "splice-amulet"
                        }
                    }
                ],
                "offset": 107,
                "synchronizerId": "global-domain::12204b8b621ec1dedd51ee2510085f8164cad194953496494d32f541f3f2c170e962",
                "traceContext": {
                    "traceparent": "00-a02f704bef12ca819b369d2c2c037c55-4bc4bdde1e4377b5-01",
                    "tracestate": null
                },
                "recordTime": "1970-01-01T00:01:00.000114Z"
            }
        }
    }
}
You can parse such transactions using the token standard history parser provided in the wallet SDK to extract the deposit amount, account and holding contract ids. Note that one-step deposits are more complex to parse than two-step transfers as the token standard does not provide an interface choice visible to the receiver. If you prefer implementing your own implementation, you can parse this as follows:
  1. Go over the list of events ordered by nodeId that you see in the transaction.
  2. For each exercised event, check the exercise result. If it has a field called `meta with a "splice.lfdecentralizedtrust.org/tx-kind": "transfer" field you found a transfer. In the example here, this is the event with nodeId 4 which exercises the TransferPreapproval_Send choice. Note that this choice is specific to Canton Coin so rely on the existence of the meta field which is standardized instead of the specific choice name.
  3. Extract the "splice.lfdecentralizedtrust.org/reason" to get the deposit account. In this example it is deposit-account-id.
  4. Go over all events whose nodeId is larger than the nodeId of the transfer (4 in the example here) and smaller than the lastDescendantNodeId of the transfer (12 in the example here).
  5. Find all CreatedEvents in that range that create a Holding with "owner": "&lt;treasury-party&gt;" and sum up the amounts for each instrumentId. In this example, we have two events that create holdings, nodeId 11 and 12. However, only 12 has "owner": "&lt;treasury-party&gt;". Therefore, we extract that the transfer created 200.0000000000 for the token with instrument id {"admin": "DSO::12204b8b621ec1dedd51ee2510085f8164cad194953496494d32f541f3f2c170e962", "id": "Amulet"}.
  6. Find all ExercisedEvents with implementedInterfaces containing the Holding interface and consuming: true. In the example here, this is the event with nodeId:: 8. For each of them get the contractId and lookup the contract payload through the event query service as shown below. If you get a 404, it’s a holding for a different party so you can ignore it. If you get back an event, check if "owner": "&lt;treasury-party&gt;". If so, sum up all events for which this is the case. In the example here, we get a 404 as it is a holding of the sender not treasury-party.
    curl -sSL --fail-with-body http://json-api-url/v2/events/events-by-contract-id \
      -H 'Authorization: Bearer 721580fa5edea5c12b887af1dba4ed2381c507d1a94c96aa63685198c958bf3ddd951d3cb004ead720c61734d4035c442afc102896cdb75e1c0883f61828eaed' \
        -d '{
          "contractId": "009b939ae451ef1a0cb81d1606391406690e055b5be301fd2f51efb6be5675577eca1112200f58604ac538224f73bdc57117d73830ed1e3167f956d66f9e3ecdacbf2359a7",
          "eventFormat": {
            "filtersByParty": {
              "&lt;treasury-party&gt;": {
                "cumulative": [
                  {"identifierFilter": {"InterfaceFilter": {"value": {"interfaceId": "#splice-api-token-holding-v1:Splice.Api.Token.HoldingV1:Holding", "includeInterfaceView": true, "includeCreatedEventBlob": false}}}}
                ]
              }
            },
            "verbose": true
          }
        }'
    
  7. Subtract the sum of archived holdings for the treasury-party from the sum of created holdings. This gives you the deposit amount for each instrument id. You now extracted the deposit amount from the created and exercised events, the UTXOs from the created events and the deposit acount from the splice.lfdecentralizedtrust.org/reason field.
  8. Continue with the events starting at node id lastDescendantNodeId + 1. Note that in this example this skips over the event with nodeId: 5 which exercises AmuletRules_Transfer. This is important as you already accounted for this event through the parent event at node id 4. Note that one transaction can contain multiple deposits including mixing 1 and 2-step deposits in the same transaction.

Differences between 1-Step Deposits and Withdrawals

The example we discussed above, shows a deposit. A withdrawal is essentially the same transaction but sender and receiver are swapped. For a withdrawal, the sender, i.e. the treasury party for an exchange, will also see the TransferFactory_Transfer choice as a parent and you can extract the amount and reason from that instead of looking for the meta field in exercise results. Note however, that for Canton Coin the amount in the TransferFactory_Transfer argument will be higher than the difference of holdings archived and created for the treasury party due to Canton Coin usage fees. Once the CIP for CC fee removal is implemented, this distinction goes away. Currently Canton Coin is the only token on Canton Network charging such fees.

Multi-Step Transfers

To understand the transaction structure of a multi-step transfer, let’s look at an example transaction of a Multi-Step Deposit as seen through the JSON Ledger API. In this case, we query a single transaction. The format is identical to the transaction you will get when streaming transactions through /v2/updates/flats and you can also use the same filter. Note that you need to adjust the auth-token, update-id and treasury-party placeholders to match your setup.
curl -sSL --fail-with-body http://json-api-url/v2/updates/update-by-id \
    -H 'Authorization: Bearer <authtoken>' \
      -d '{
            "updateId": "&lt;update-id&gt;",
            "updateFormat": {
              "includeTransactions": {
                "transactionShape": "TRANSACTION_SHAPE_LEDGER_EFFECTS",
                "eventFormat": {
                  "filtersByParty": {
                    "&lt;treasury-party&gt;": {
                      "cumulative": [
                        {"identifierFilter": {"WildcardFilter": {"value": {"includeCreatedEventBlob": false}}}},
                        {"identifierFilter": {"InterfaceFilter": {"value": {"interfaceId": "#splice-api-token-transfer-instruction-v1:Splice.Api.Token.TransferInstructionV1:TransferFactory", "includeInterfaceView": true, "includeCreatedEventBlob": false}}}},
                        {"identifierFilter": {"InterfaceFilter": {"value": {"interfaceId": "#splice-api-token-holding-v1:Splice.Api.Token.HoldingV1:Holding", "includeInterfaceView": true, "includeCreatedEventBlob": false}}}},
                        {"identifierFilter": {"InterfaceFilter": {"value": {"interfaceId": "#splice-api-token-transfer-instruction-v1:Splice.Api.Token.TransferInstructionV1:TransferInstruction", "includeInterfaceView": true, "includeCreatedEventBlob": false}}}}
                      ]
                    }
                  },
                  "verbose": true
                }
              }
            }
          }'
{
    "update": {
        "Transaction": {
            "value": {
                "updateId": "12208359521a283dbd0749c2a38c858ad71612fd5177aa95fb736e77fd181b8060c7",
                "commandId": "",
                "workflowId": "",
                "effectiveAt": "1970-01-01T00:01:00Z",
                "events": [
                    {
                        "CreatedEvent": {
                            "offset": 96,
                            "nodeId": 0,
                            "contractId": "00fc774936c91f423c117744102a5996e4dc117f2b6496ef337967a7d2c5d02e4aca1112203c35266c980ae19508cc690cb501f8c767c02bfdbe838f1f89105de6fe59439f",
                            "templateId": "6e9fc50fb94e56751b49f09ba2dc84da53a9d7cff08115ebb4f6b7a12d0c990c:Splice.AmuletTransferInstruction:AmuletTransferInstruction",
                            "contractKey": null,
                            "createArgument": {
                                "lockedAmulet": "004ef3ae401af384aa37391f3a975647b1ca3d9ca3dc97f7b1e19c47d013ed4956ca11122015cac2e81f6d2e2735ed64c16326230234cc374c85ed42657b7801bf62233ddc",
                                "transfer": {
                                    "sender": "sender::1220f37942af4f4d062006155d504f1adfa1565260cd499e62325b011c06db635d8c",
                                    "receiver": "treasury-party::12207bd11907b9b3c11ade702d30b556bfe635314d3d0f708f9677e09a4ff096ef87",
                                    "amount": "1000.0000000000",
                                    "instrumentId": {
                                        "admin": "DSO::1220d26d73867a428814de451fdd8e716acf45fe59c6569d76ad77d42270629f3ce9",
                                        "id": "Amulet"
                                    },
                                    "requestedAt": "1970-01-01T00:01:00Z",
                                    "executeBefore": "1970-01-01T00:02:00Z",
                                    "inputHoldingCids": [
                                        "004ef3ae401af384aa37391f3a975647b1ca3d9ca3dc97f7b1e19c47d013ed4956ca11122015cac2e81f6d2e2735ed64c16326230234cc374c85ed42657b7801bf62233ddc"
                                    ],
                                    "meta": {
                                        "values": {
                                            "splice.lfdecentralizedtrust.org/reason": "deposit-account-id"
                                        }
                                    }
                                }
                            },
                            "createdEventBlob": "",
                            "interfaceViews": [
                                {
                                    "interfaceId": "55ba4deb0ad4662c4168b39859738a0e91388d252286480c7331b3f71a517281:Splice.Api.Token.TransferInstructionV1:TransferInstruction",
                                    "viewStatus": {
                                        "code": 0,
                                        "message": "",
                                        "details": []
                                    },
                                    "viewValue": {
                                        "originalInstructionCid": null,
                                        "transfer": {
                                            "sender": "sender::1220f37942af4f4d062006155d504f1adfa1565260cd499e62325b011c06db635d8c",
                                            "receiver": "treasury-party::12207bd11907b9b3c11ade702d30b556bfe635314d3d0f708f9677e09a4ff096ef87",
                                            "amount": "1000.0000000000",
                                            "instrumentId": {
                                                "admin": "DSO::1220d26d73867a428814de451fdd8e716acf45fe59c6569d76ad77d42270629f3ce9",
                                                "id": "Amulet"
                                            },
                                            "requestedAt": "1970-01-01T00:01:00Z",
                                            "executeBefore": "1970-01-01T00:02:00Z",
                                            "inputHoldingCids": [
                                                "004ef3ae401af384aa37391f3a975647b1ca3d9ca3dc97f7b1e19c47d013ed4956ca11122015cac2e81f6d2e2735ed64c16326230234cc374c85ed42657b7801bf62233ddc"
                                            ],
                                            "meta": {
                                                "values": {
                                                    "splice.lfdecentralizedtrust.org/reason": "deposit-account-id"
                                                }
                                            }
                                        },
                                        "status": {
                                            "tag": "TransferPendingReceiverAcceptance",
                                            "value": {}
                                        },
                                        "meta": {
                                            "values": {}
                                        }
                                    }
                                }
                            ],
                            "witnessParties": [
                                "treasury-party::12207bd11907b9b3c11ade702d30b556bfe635314d3d0f708f9677e09a4ff096ef87"
                            ],
                            "signatories": [
                                "DSO::1220d26d73867a428814de451fdd8e716acf45fe59c6569d76ad77d42270629f3ce9",
                                "sender::1220f37942af4f4d062006155d504f1adfa1565260cd499e62325b011c06db635d8c"
                            ],
                            "observers": [
                                "treasury-party::12207bd11907b9b3c11ade702d30b556bfe635314d3d0f708f9677e09a4ff096ef87"
                            ],
                            "createdAt": "1970-01-01T00:01:00Z",
                            "packageName": "splice-amulet"
                        }
                    }
                ],
                "offset": 96,
                "synchronizerId": "global-domain::1220d26d73867a428814de451fdd8e716acf45fe59c6569d76ad77d42270629f3ce9",
                "traceContext": {
                    "traceparent": "00-5272bc75d4e43cc836ad878ee28fd812-63cd1b00c1d96ad3-01",
                    "tracestate": null
                },
                "recordTime": "1970-01-01T00:01:00.000145Z"
            }
        }
    }
}
You can parse such transactions using the token standard history parser provided in the wallet SDK to extract the deposit amount, account and holding contract ids. If you prefer implementing your own implementation, you can parse this as follows:
  1. Go over the list of events ordered by nodeId that you see in the transaction.
  2. Look for all CreatedEvents of the TransferInstruction interface with "receiver": "&lt;treasury-party&gt;". Each of these represents a deposit offer that can be accepted or rejected. In the example this is only one event with node id 0. Extract the instrument, the amount and the splice.lfdecentralizedtrust.org/reason field from the interfaceView and the contract id of the TransferInstruction. Note that one transaction can contain multiple deposits including mixing 1 and 2-step deposits in the same transaction.
After accepting the deposit offer through your automation, Tx History Ingestion can then observe and process acceptance. An example of such a transaction can be seen below.
{
    "update": {
        "Transaction": {
            "value": {
                "updateId": "122027b71f7eae8f7c42e39fba745da860fed5254c32d4afbd1699deff19e5fc4206",
                "commandId": "d5e461d9-405d-4042-bea2-6eca4b82548c",
                "workflowId": "",
                "effectiveAt": "1970-01-01T00:01:00Z",
                "events": [
                    {
                        "ExercisedEvent": {
                            "offset": 106,
                            "nodeId": 0,
                            "contractId": "00fc774936c91f423c117744102a5996e4dc117f2b6496ef337967a7d2c5d02e4aca1112203c35266c980ae19508cc690cb501f8c767c02bfdbe838f1f89105de6fe59439f",
                            "templateId": "6e9fc50fb94e56751b49f09ba2dc84da53a9d7cff08115ebb4f6b7a12d0c990c:Splice.AmuletTransferInstruction:AmuletTransferInstruction",
                            "interfaceId": "55ba4deb0ad4662c4168b39859738a0e91388d252286480c7331b3f71a517281:Splice.Api.Token.TransferInstructionV1:TransferInstruction",
                            "choice": "TransferInstruction_Accept",
                            "choiceArgument": {
                                "extraArgs": {
                                    "context": {
                                        "values": {
                                            "amulet-rules": {
                                                "tag": "AV_ContractId",
                                                "value": "001b1c0752079634f968fb59cdf0ec5b4aa9a085d939f1d443ca1b2a6d050e3927ca1112204b53a7228d1305d18dc568701cfdab4f60fc193d6c2e8e09c69582b2790e3550"
                                            },
                                            "expire-lock": {
                                                "tag": "AV_Bool",
                                                "value": true
                                            },
                                            "open-round": {
                                                "tag": "AV_ContractId",
                                                "value": "00c298815a41f51f7b6a164f7a2618e03b3caa2022a1919da05b2a4aa6400f40b4ca111220f7b646f00988b0a32aa21a5ab16f5962978b108c4ff37fcc944cdb7c40e56669"
                                            }
                                        }
                                    },
                                    "meta": {
                                        "values": {}
                                    }
                                }
                            },
                            "actingParties": [
                                "treasury-party::12207bd11907b9b3c11ade702d30b556bfe635314d3d0f708f9677e09a4ff096ef87"
                            ],
                            "consuming": true,
                            "witnessParties": [
                                "treasury-party::12207bd11907b9b3c11ade702d30b556bfe635314d3d0f708f9677e09a4ff096ef87"
                            ],
                            "lastDescendantNodeId": 12,
                            "exerciseResult": {
                                "output": {
                                    "tag": "TransferInstructionResult_Completed",
                                    "value": {
                                        "receiverHoldingCids": [
                                            "0026638a9b9db54fab1cc3f260b4db189a8e65e8bdaf646a66fdff3976a48e88a6ca1112209d4295be34eb089d5b94ed0c681723a2591681f09de55a95de8040c822726306"
                                        ]
                                    }
                                },
                                "senderChangeCids": [
                                    "009d1ed65f5ab6fb57fddf2de3671bc734807ec4aaba3f37b539388787e1adb250ca111220bed445fb61640859f9ed394ae51d029e2b5cd113c6df9bdd8633333ba1dfc8e8"
                                ],
                                "meta": {
                                    "values": {
                                        "splice.lfdecentralizedtrust.org/burned": "22.0"
                                    }
                                }
                            },
                            "packageName": "splice-amulet",
                            "implementedInterfaces": [
                                "55ba4deb0ad4662c4168b39859738a0e91388d252286480c7331b3f71a517281:Splice.Api.Token.TransferInstructionV1:TransferInstruction"
                            ]
                        }
                    },
                    {
                        "ExercisedEvent": {
                            "offset": 106,
                            "nodeId": 2,
                            "contractId": "004ef3ae401af384aa37391f3a975647b1ca3d9ca3dc97f7b1e19c47d013ed4956ca11122015cac2e81f6d2e2735ed64c16326230234cc374c85ed42657b7801bf62233ddc",
                            "templateId": "6e9fc50fb94e56751b49f09ba2dc84da53a9d7cff08115ebb4f6b7a12d0c990c:Splice.Amulet:LockedAmulet",
                            "interfaceId": null,
                            "choice": "LockedAmulet_Unlock",
                            "choiceArgument": {
                                "openRoundCid": "00c298815a41f51f7b6a164f7a2618e03b3caa2022a1919da05b2a4aa6400f40b4ca111220f7b646f00988b0a32aa21a5ab16f5962978b108c4ff37fcc944cdb7c40e56669"
                            },
                            "actingParties": [
                                "DSO::1220d26d73867a428814de451fdd8e716acf45fe59c6569d76ad77d42270629f3ce9",
                                "sender::1220f37942af4f4d062006155d504f1adfa1565260cd499e62325b011c06db635d8c"
                            ],
                            "consuming": true,
                            "witnessParties": [
                                "treasury-party::12207bd11907b9b3c11ade702d30b556bfe635314d3d0f708f9677e09a4ff096ef87"
                            ],
                            "lastDescendantNodeId": 4,
                            "exerciseResult": {
                                "amuletSum": {
                                    "amulet": "004aaf5722cb10c5f59654017bcf346ba0c25838d020a79afa31b0235a58278cf0ca11122057178fb2d28c6b2563eef4035baf9e279b1f583ecc97a896fa7bd60b05ac324c",
                                    "amuletPrice": "0.0050000000",
                                    "round": {
                                        "number": "1"
                                    }
                                },
                                "meta": {
                                    "values": {
                                        "splice.lfdecentralizedtrust.org/reason": "holders released lock",
                                        "splice.lfdecentralizedtrust.org/tx-kind": "unlock"
                                    }
                                }
                            },
                            "packageName": "splice-amulet",
                            "implementedInterfaces": [
                                "718a0f77e505a8de22f188bd4c87fe74101274e9d4cb1bfac7d09aec7158d35b:Splice.Api.Token.HoldingV1:Holding"
                            ]
                        }
                    },
                    {
                        "CreatedEvent": {
                            "offset": 106,
                            "nodeId": 4,
                            "contractId": "004aaf5722cb10c5f59654017bcf346ba0c25838d020a79afa31b0235a58278cf0ca11122057178fb2d28c6b2563eef4035baf9e279b1f583ecc97a896fa7bd60b05ac324c",
                            "templateId": "6e9fc50fb94e56751b49f09ba2dc84da53a9d7cff08115ebb4f6b7a12d0c990c:Splice.Amulet:Amulet",
                            "contractKey": null,
                            "createArgument": {
                                "dso": "DSO::1220d26d73867a428814de451fdd8e716acf45fe59c6569d76ad77d42270629f3ce9",
                                "owner": "sender::1220f37942af4f4d062006155d504f1adfa1565260cd499e62325b011c06db635d8c",
                                "amount": {
                                    "initialAmount": "1064.0015220800",
                                    "createdAt": {
                                        "number": "1"
                                    },
                                    "ratePerRound": {
                                        "rate": "0.0038051800"
                                    }
                                }
                            },
                            "createdEventBlob": "",
                            "interfaceViews": [
                                {
                                    "interfaceId": "718a0f77e505a8de22f188bd4c87fe74101274e9d4cb1bfac7d09aec7158d35b:Splice.Api.Token.HoldingV1:Holding",
                                    "viewStatus": {
                                        "code": 0,
                                        "message": "",
                                        "details": []
                                    },
                                    "viewValue": {
                                        "owner": "sender::1220f37942af4f4d062006155d504f1adfa1565260cd499e62325b011c06db635d8c",
                                        "instrumentId": {
                                            "admin": "DSO::1220d26d73867a428814de451fdd8e716acf45fe59c6569d76ad77d42270629f3ce9",
                                            "id": "Amulet"
                                        },
                                        "amount": "1064.0015220800",
                                        "lock": null,
                                        "meta": {
                                            "values": {
                                                "amulet.splice.lfdecentralizedtrust.org/created-in-round": "1",
                                                "amulet.splice.lfdecentralizedtrust.org/rate-per-round": "0.00380518"
                                            }
                                        }
                                    }
                                }
                            ],
                            "witnessParties": [
                                "treasury-party::12207bd11907b9b3c11ade702d30b556bfe635314d3d0f708f9677e09a4ff096ef87"
                            ],
                            "signatories": [
                                "DSO::1220d26d73867a428814de451fdd8e716acf45fe59c6569d76ad77d42270629f3ce9",
                                "sender::1220f37942af4f4d062006155d504f1adfa1565260cd499e62325b011c06db635d8c"
                            ],
                            "observers": [],
                            "createdAt": "1970-01-01T00:01:00Z",
                            "packageName": "splice-amulet"
                        }
                    },
                    {
                        "ExercisedEvent": {
                            "offset": 106,
                            "nodeId": 5,
                            "contractId": "001b1c0752079634f968fb59cdf0ec5b4aa9a085d939f1d443ca1b2a6d050e3927ca1112204b53a7228d1305d18dc568701cfdab4f60fc193d6c2e8e09c69582b2790e3550",
                            "templateId": "6e9fc50fb94e56751b49f09ba2dc84da53a9d7cff08115ebb4f6b7a12d0c990c:Splice.AmuletRules:AmuletRules",
                            "interfaceId": null,
                            "choice": "AmuletRules_Transfer",
                            "choiceArgument": {
                                "transfer": {
                                    "sender": "sender::1220f37942af4f4d062006155d504f1adfa1565260cd499e62325b011c06db635d8c",
                                    "provider": "sender::1220f37942af4f4d062006155d504f1adfa1565260cd499e62325b011c06db635d8c",
                                    "inputs": [
                                        {
                                            "tag": "InputAmulet",
                                            "value": "004aaf5722cb10c5f59654017bcf346ba0c25838d020a79afa31b0235a58278cf0ca11122057178fb2d28c6b2563eef4035baf9e279b1f583ecc97a896fa7bd60b05ac324c"
                                        }
                                    ],
                                    "outputs": [
                                        {
                                            "receiver": "treasury-party::12207bd11907b9b3c11ade702d30b556bfe635314d3d0f708f9677e09a4ff096ef87",
                                            "receiverFeeRatio": "0.0000000000",
                                            "amount": "1000.0000000000",
                                            "lock": null
                                        }
                                    ],
                                    "beneficiaries": null
                                },
                                "context": {
                                    "openMiningRound": "00c298815a41f51f7b6a164f7a2618e03b3caa2022a1919da05b2a4aa6400f40b4ca111220f7b646f00988b0a32aa21a5ab16f5962978b108c4ff37fcc944cdb7c40e56669",
                                    "issuingMiningRounds": [],
                                    "validatorRights": [],
                                    "featuredAppRight": null
                                },
                                "expectedDso": "DSO::1220d26d73867a428814de451fdd8e716acf45fe59c6569d76ad77d42270629f3ce9"
                            },
                            "actingParties": [
                                "sender::1220f37942af4f4d062006155d504f1adfa1565260cd499e62325b011c06db635d8c",
                                "treasury-party::12207bd11907b9b3c11ade702d30b556bfe635314d3d0f708f9677e09a4ff096ef87"
                            ],
                            "consuming": false,
                            "witnessParties": [
                                "treasury-party::12207bd11907b9b3c11ade702d30b556bfe635314d3d0f708f9677e09a4ff096ef87"
                            ],
                            "lastDescendantNodeId": 12,
                            "exerciseResult": {
                                "round": {
                                    "number": "1"
                                },
                                "summary": {
                                    "inputAppRewardAmount": "0.0000000000",
                                    "inputValidatorRewardAmount": "0.0000000000",
                                    "inputSvRewardAmount": "0.0000000000",
                                    "inputAmuletAmount": "1064.0015220800",
                                    "balanceChanges": [
                                        [
                                            "sender::1220f37942af4f4d062006155d504f1adfa1565260cd499e62325b011c06db635d8c",
                                            {
                                                "changeToInitialAmountAsOfRoundZero": "-1022.0000000000",
                                                "changeToHoldingFeesRate": "0.0000000000"
                                            }
                                        ],
                                        [
                                            "treasury-party::12207bd11907b9b3c11ade702d30b556bfe635314d3d0f708f9677e09a4ff096ef87",
                                            {
                                                "changeToInitialAmountAsOfRoundZero": "1000.0038051800",
                                                "changeToHoldingFeesRate": "0.0038051800"
                                            }
                                        ]
                                    ],
                                    "holdingFees": "0.0000000000",
                                    "outputFees": ["16.0000000000"],
                                    "senderChangeFee": "6.0000000000",
                                    "senderChangeAmount": "42.0015220800",
                                    "amuletPrice": "0.0050000000",
                                    "inputValidatorFaucetAmount": "0.0000000000",
                                    "inputUnclaimedActivityRecordAmount": "0.0000000000"
                                },
                                "createdAmulets": [
                                    {
                                        "tag": "TransferResultAmulet",
                                        "value": "0026638a9b9db54fab1cc3f260b4db189a8e65e8bdaf646a66fdff3976a48e88a6ca1112209d4295be34eb089d5b94ed0c681723a2591681f09de55a95de8040c822726306"
                                    }
                                ],
                                "senderChangeAmulet": "009d1ed65f5ab6fb57fddf2de3671bc734807ec4aaba3f37b539388787e1adb250ca111220bed445fb61640859f9ed394ae51d029e2b5cd113c6df9bdd8633333ba1dfc8e8",
                                "meta": {
                                    "values": {
                                        "splice.lfdecentralizedtrust.org/burned": "22.0",
                                        "splice.lfdecentralizedtrust.org/sender": "sender::1220f37942af4f4d062006155d504f1adfa1565260cd499e62325b011c06db635d8c",
                                        "splice.lfdecentralizedtrust.org/tx-kind": "transfer"
                                    }
                                }
                            },
                            "packageName": "splice-amulet",
                            "implementedInterfaces": []
                        }
                    },
                    {
                        "ExercisedEvent": {
                            "offset": 106,
                            "nodeId": 8,
                            "contractId": "004aaf5722cb10c5f59654017bcf346ba0c25838d020a79afa31b0235a58278cf0ca11122057178fb2d28c6b2563eef4035baf9e279b1f583ecc97a896fa7bd60b05ac324c",
                            "templateId": "6e9fc50fb94e56751b49f09ba2dc84da53a9d7cff08115ebb4f6b7a12d0c990c:Splice.Amulet:Amulet",
                            "interfaceId": null,
                            "choice": "Archive",
                            "choiceArgument": {},
                            "actingParties": [
                                "DSO::1220d26d73867a428814de451fdd8e716acf45fe59c6569d76ad77d42270629f3ce9",
                                "sender::1220f37942af4f4d062006155d504f1adfa1565260cd499e62325b011c06db635d8c"
                            ],
                            "consuming": true,
                            "witnessParties": [
                                "treasury-party::12207bd11907b9b3c11ade702d30b556bfe635314d3d0f708f9677e09a4ff096ef87"
                            ],
                            "lastDescendantNodeId": 8,
                            "exerciseResult": {},
                            "packageName": "splice-amulet",
                            "implementedInterfaces": [
                                "718a0f77e505a8de22f188bd4c87fe74101274e9d4cb1bfac7d09aec7158d35b:Splice.Api.Token.HoldingV1:Holding"
                            ]
                        }
                    },
                    {
                        "CreatedEvent": {
                            "offset": 106,
                            "nodeId": 9,
                            "contractId": "0044b4793808d8844f63aae78e72c5e788eb07bf16a07fb56c02f32abde3b14f08ca111220467204e95f62b34f25d162934f48e36394b3a830b5602447d83cc5e22e0a5799",
                            "templateId": "6e9fc50fb94e56751b49f09ba2dc84da53a9d7cff08115ebb4f6b7a12d0c990c:Splice.Amulet:ValidatorRewardCoupon",
                            "contractKey": null,
                            "createArgument": {
                                "dso": "DSO::1220d26d73867a428814de451fdd8e716acf45fe59c6569d76ad77d42270629f3ce9",
                                "user": "sender::1220f37942af4f4d062006155d504f1adfa1565260cd499e62325b011c06db635d8c",
                                "amount": "16.0000000000",
                                "round": {
                                    "number": "1"
                                }
                            },
                            "createdEventBlob": "",
                            "interfaceViews": [],
                            "witnessParties": [
                                "treasury-party::12207bd11907b9b3c11ade702d30b556bfe635314d3d0f708f9677e09a4ff096ef87"
                            ],
                            "signatories": [
                                "DSO::1220d26d73867a428814de451fdd8e716acf45fe59c6569d76ad77d42270629f3ce9"
                            ],
                            "observers": [
                                "sender::1220f37942af4f4d062006155d504f1adfa1565260cd499e62325b011c06db635d8c"
                            ],
                            "createdAt": "1970-01-01T00:01:00Z",
                            "packageName": "splice-amulet"
                        }
                    },
                    {
                        "CreatedEvent": {
                            "offset": 106,
                            "nodeId": 10,
                            "contractId": "00c0543921ee917ccd7d6453e0db4ff2b8264a703fc6df0bcd69100ac589ac05d8ca111220f71cb88db3445657a64288862aaba6c7b4f463366889ee036be182c632ede1ed",
                            "templateId": "6e9fc50fb94e56751b49f09ba2dc84da53a9d7cff08115ebb4f6b7a12d0c990c:Splice.Amulet:AppRewardCoupon",
                            "contractKey": null,
                            "createArgument": {
                                "dso": "DSO::1220d26d73867a428814de451fdd8e716acf45fe59c6569d76ad77d42270629f3ce9",
                                "provider": "sender::1220f37942af4f4d062006155d504f1adfa1565260cd499e62325b011c06db635d8c",
                                "featured": false,
                                "amount": "16.0000000000",
                                "round": {
                                    "number": "1"
                                },
                                "beneficiary": "sender::1220f37942af4f4d062006155d504f1adfa1565260cd499e62325b011c06db635d8c"
                            },
                            "createdEventBlob": "",
                            "interfaceViews": [],
                            "witnessParties": [
                                "treasury-party::12207bd11907b9b3c11ade702d30b556bfe635314d3d0f708f9677e09a4ff096ef87"
                            ],
                            "signatories": [
                                "DSO::1220d26d73867a428814de451fdd8e716acf45fe59c6569d76ad77d42270629f3ce9"
                            ],
                            "observers": [
                                "sender::1220f37942af4f4d062006155d504f1adfa1565260cd499e62325b011c06db635d8c"
                            ],
                            "createdAt": "1970-01-01T00:01:00Z",
                            "packageName": "splice-amulet"
                        }
                    },
                    {
                        "CreatedEvent": {
                            "offset": 106,
                            "nodeId": 11,
                            "contractId": "0026638a9b9db54fab1cc3f260b4db189a8e65e8bdaf646a66fdff3976a48e88a6ca1112209d4295be34eb089d5b94ed0c681723a2591681f09de55a95de8040c822726306",
                            "templateId": "6e9fc50fb94e56751b49f09ba2dc84da53a9d7cff08115ebb4f6b7a12d0c990c:Splice.Amulet:Amulet",
                            "contractKey": null,
                            "createArgument": {
                                "dso": "DSO::1220d26d73867a428814de451fdd8e716acf45fe59c6569d76ad77d42270629f3ce9",
                                "owner": "treasury-party::12207bd11907b9b3c11ade702d30b556bfe635314d3d0f708f9677e09a4ff096ef87",
                                "amount": {
                                    "initialAmount": "1000.0000000000",
                                    "createdAt": {
                                        "number": "1"
                                    },
                                    "ratePerRound": {
                                        "rate": "0.0038051800"
                                    }
                                }
                            },
                            "createdEventBlob": "",
                            "interfaceViews": [
                                {
                                    "interfaceId": "718a0f77e505a8de22f188bd4c87fe74101274e9d4cb1bfac7d09aec7158d35b:Splice.Api.Token.HoldingV1:Holding",
                                    "viewStatus": {
                                        "code": 0,
                                        "message": "",
                                        "details": []
                                    },
                                    "viewValue": {
                                        "owner": "treasury-party::12207bd11907b9b3c11ade702d30b556bfe635314d3d0f708f9677e09a4ff096ef87",
                                        "instrumentId": {
                                            "admin": "DSO::1220d26d73867a428814de451fdd8e716acf45fe59c6569d76ad77d42270629f3ce9",
                                            "id": "Amulet"
                                        },
                                        "amount": "1000.0000000000",
                                        "lock": null,
                                        "meta": {
                                            "values": {
                                                "amulet.splice.lfdecentralizedtrust.org/created-in-round": "1",
                                                "amulet.splice.lfdecentralizedtrust.org/rate-per-round": "0.00380518"
                                            }
                                        }
                                    }
                                }
                            ],
                            "witnessParties": [
                                "treasury-party::12207bd11907b9b3c11ade702d30b556bfe635314d3d0f708f9677e09a4ff096ef87"
                            ],
                            "signatories": [
                                "DSO::1220d26d73867a428814de451fdd8e716acf45fe59c6569d76ad77d42270629f3ce9",
                                "treasury-party::12207bd11907b9b3c11ade702d30b556bfe635314d3d0f708f9677e09a4ff096ef87"
                            ],
                            "observers": [],
                            "createdAt": "1970-01-01T00:01:00Z",
                            "packageName": "splice-amulet"
                        }
                    },
                    {
                        "CreatedEvent": {
                            "offset": 106,
                            "nodeId": 12,
                            "contractId": "009d1ed65f5ab6fb57fddf2de3671bc734807ec4aaba3f37b539388787e1adb250ca111220bed445fb61640859f9ed394ae51d029e2b5cd113c6df9bdd8633333ba1dfc8e8",
                            "templateId": "6e9fc50fb94e56751b49f09ba2dc84da53a9d7cff08115ebb4f6b7a12d0c990c:Splice.Amulet:Amulet",
                            "contractKey": null,
                            "createArgument": {
                                "dso": "DSO::1220d26d73867a428814de451fdd8e716acf45fe59c6569d76ad77d42270629f3ce9",
                                "owner": "sender::1220f37942af4f4d062006155d504f1adfa1565260cd499e62325b011c06db635d8c",
                                "amount": {
                                    "initialAmount": "42.0015220800",
                                    "createdAt": {
                                        "number": "1"
                                    },
                                    "ratePerRound": {
                                        "rate": "0.0038051800"
                                    }
                                }
                            },
                            "createdEventBlob": "",
                            "interfaceViews": [
                                {
                                    "interfaceId": "718a0f77e505a8de22f188bd4c87fe74101274e9d4cb1bfac7d09aec7158d35b:Splice.Api.Token.HoldingV1:Holding",
                                    "viewStatus": {
                                        "code": 0,
                                        "message": "",
                                        "details": []
                                    },
                                    "viewValue": {
                                        "owner": "sender::1220f37942af4f4d062006155d504f1adfa1565260cd499e62325b011c06db635d8c",
                                        "instrumentId": {
                                            "admin": "DSO::1220d26d73867a428814de451fdd8e716acf45fe59c6569d76ad77d42270629f3ce9",
                                            "id": "Amulet"
                                        },
                                        "amount": "42.0015220800",
                                        "lock": null,
                                        "meta": {
                                            "values": {
                                                "amulet.splice.lfdecentralizedtrust.org/created-in-round": "1",
                                                "amulet.splice.lfdecentralizedtrust.org/rate-per-round": "0.00380518"
                                            }
                                        }
                                    }
                                }
                            ],
                            "witnessParties": [
                                "treasury-party::12207bd11907b9b3c11ade702d30b556bfe635314d3d0f708f9677e09a4ff096ef87"
                            ],
                            "signatories": [
                                "DSO::1220d26d73867a428814de451fdd8e716acf45fe59c6569d76ad77d42270629f3ce9",
                                "sender::1220f37942af4f4d062006155d504f1adfa1565260cd499e62325b011c06db635d8c"
                            ],
                            "observers": [],
                            "createdAt": "1970-01-01T00:01:00Z",
                            "packageName": "splice-amulet"
                        }
                    }
                ],
                "offset": 106,
                "synchronizerId": "global-domain::1220d26d73867a428814de451fdd8e716acf45fe59c6569d76ad77d42270629f3ce9",
                "traceContext": {
                    "traceparent": "00-0e49990b26e75cc4ae47300de4607087-793c606077bad4e7-01",
                    "tracestate": null
                },
                "recordTime": "1970-01-01T00:01:00.000182Z"
            }
        }
    }
}
To parse this proceed as follows:
  1. Go over the list of events ordered by nodeId that you see in the transaction.
  2. Look for exercises of the TransferInstruction_Accept choice on the TransferInstruction interface. In the example, this is the event with node id 0. For each of those, extract the contract id. You can then query the event query service using:
    curl -sSL --fail-with-body http://json-api-url/v2/events/events-by-contract-id \
      -H 'Authorization: Bearer 00fc774936c91f423c117744102a5996e4dc117f2b6496ef337967a7d2c5d02e4aca1112203c35266c980ae19508cc690cb501f8c767c02bfdbe838f1f89105de6fe59439f' \
        -d '{
          "contractId": "009b939ae451ef1a0cb81d1606391406690e055b5be301fd2f51efb6be5675577eca1112200f58604ac538224f73bdc57117d73830ed1e3167f956d66f9e3ecdacbf2359a7",
          "eventFormat": {
            "filtersByParty": {
              "&lt;treasury-party&gt;": {
                "cumulative": [
                  {"identifierFilter": {"InterfaceFilter": {"value": {"interfaceId": "#splice-api-token-transfer-instruction-v1:Splice.Api.Token.TransferInstructionV1:TransferInstruction", "includeInterfaceView": true, "includeCreatedEventBlob": false}}}}
                ]
              }
            },
            "verbose": true
          }
        }'
    
    If you get a 404, the instruction is not for your treasury party so you can ignore it. If you get back an event, it has the same structure that we’ve seen above when a transfer offer is created and you can again extract the amount, instrument id and deposit account from it.

Differences between Multi-Step Deposits and Withdrawals

Analogously to 1-step transfers, the sender that creates the withdrawal offer, i.e., the treasury party sees a TransferFactory_Transfer exercise node and can extract amount and reason from that. For Canton Coin, both the creation of the TransferInstruction as well as the acceptance currently charge fees so the amount specified in the transfer is smaller than the holdings change of the treasury party. Once the CIP for CC fee removal is implemented, this distinction goes away. Currently Canton Coin is the only token on Canton Network charging such fees.
This section was copied from existing reviewed documentation. Source: docs/wallet-integration-guide/src/exchange-integration/non-functionals.rst Reviewers: Skip this section. Remove markers after final approval.

Fault Tolerance

Recall the architecture diagram from the integration-architecture section: Integration architecture component overview Below you learn how to handle crashes of the integration components, how to handle RPC errors, and how to perform disaster recovery for the Exchange Validator Node.

Handling Crashes

Validator nodes are crash-fault tolerant and do not lose data shared on the Ledger API in case of a crash. Thus a restart is sufficient to recover from crashes. Likewise, we assume that the Canton Integration DB is backed by a crash-fault tolerant database (e.g., PostgreSQL or MySQL). For the integration components that you build, we recommend the following strategy to handle crashes and restarts:
  • Tx History Ingestion: keep track of the last ingested offset in the Canton Integration DB. On restart, continue from that offset. If none is set, then that means it never ingested any transaction. In that case, start from the beginning of the transaction history, i.e., start from offset 0. For this to work, it is important that you store the ingested offset in the same transaction as you store the ingested data. See the individual integration-workflows descriptions for details.
  • Withdrawal Automation: make it stateless, so that it can just restart. This is in line with how we recommend to implement both the one-step-withdrawal-workflow and the multi-step-withdrawal-workflow.
  • Multi-Step Deposit Automation: make it stateless, so that it can just restart. This is in line with how we recommend to implement the multi-step-deposit-workflow.

Handling RPC Errors

Below we explain our recommendation for handling RPC errors in the integration components you are building. We focus on handling errors from interacting with the Ledger API and the Registry API Servers of the token admins. We do not cover handling errors from accessing DBs or other internal systems, as we assume you have strategies in place for those.
  • Tx History Ingestion: only reads from the Ledger API. We recommend to retry these reads a bounded number of times on retryable-errors. Wait at least a few seconds between retries and consider using exponential backoff to avoid overloading the Validator Node. Consider crashing the ingestion component if the bounded number of retries is exceeded to recover from bugs in the in-memory state of the ingestion component.
  • Withdrawal Automation: recall from one-step-withdrawal-workflow that the Withdrawal Automation first retrieves extra context from the Registry API Server of the token admin and then prepares, signs, and executes the transaction to submit the transfer for the withdrawal using the /v2/interactive-submission/execute endpoint of the Ledger API. The endpoint is asynchronous and observing its response only means that the transaction has been accepted for processing. You can retrieve the status of the execution via the /v2/commands/completions endpoint of the Ledger API; or alternatively, by observing the effect of the execution via the Tx History Ingestion component. The latter option is more robust, as it ensures that you observe the effect of the execution in a persistent manner. We recommend that you retry the steps from the start when not observing the successful completion of the withdrawal within the expected time or when encountering a retryable error on the execution itself. You thereby ensure that you prepare the withdrawal transaction using the latest state of the Validator Node and the latest extra context from the Registry API Server. Use a bounded number of retries with at least a few seconds between retries and consider using exponential backoff. Retrying all steps is safe from a consistency perspective because the withdrawal transaction is idempotent, as it archives the UTXOs used to fund the transfer once the transfer is submitted. You nevertheless want to avoid retrying too often, as executing a transaction costs traffic. Stop retrying once the withdrawal has been marked as definitely failed in the Canton Integration DB by the Tx History Ingestion component. A withdrawal is considered definitely failed once its target record time trecTgt is below the last ingested record time.
  • Multi-Step Deposit Automation: the approach is analogous to the one for Withdrawal Automation. Recall from multi-step-deposit-workflow that the Multi-Step Deposit Automation discovers a pending deposit by reading from the Canton Integration DB, then retrieves extra context from the Registry API Server of the token admin and finally prepares, signs, and executes the transaction to accept the transfer offer using the /v2/interactive-submission/execute endpoint of the Ledger API. The endpoint is asynchronous and observing its response only means that the transaction has been accepted for processing. You can retrieve the status of the execution via the /v2/commands/completions endpoint of the Ledger API; or alternatively, by observing the effect of the execution via the Tx History Ingestion component. The latter option is more robust, as it ensures that you observe the effect of the execution in a persistent manner. We recommend that you retry the steps from the start when not observing the successful completion of the transfer offer acceptance within the expected time or when encountering a retryable error on the execution itself. You thereby ensure that you prepare the transaction to accept the transfer offer using the latest state of the Validator Node and the latest extra context from the Registry API Server. Use a bounded number of retries with at least a few seconds between retries and consider using exponential backoff. Retrying all steps is safe from a consistency perspective because the accept transaction is idempotent, as it archives the transfer offer once it is accepted. You nevertheless want to avoid retrying too often, as executing a transaction costs traffic. You can stop retrying after a bounded number of retries. The sender can reclaim their funds at any point by withdrawing the offer. The Multi-Step Deposit Automation will learn about the withdrawal of the offer via the Tx History Ingestion component, which will mark the transfer offer as withdrawn in the Canton Integration DB.

Retryable errors

For increased robustness and fault tolerance, we recommend to retry by default on all errors and manage an exclude list of non-retryable errors. As a starting opint, we suggest to exclude the following HTTP error codes from retries:
  • 401 Unauthorized
  • 403 Forbidden
  • 500 Internal Server Error
  • 501 Not Implemented

Reading from Canton Coin Scan

As explained in mvp-for-cc, the Registry API Server of the token admin for Canton Coin is provided by the Canton Coin Scan services. They are run as part of every SV node. For convenience, every Validator Node provides a Scan proxy service to read from the Scan instances run by SVs with Byzantine fault tolerance. The Scan proxy service also implements the Token Standard Registry API for Canton Coin. We recommend to use Scan proxy service of the Exchange Validator Node to retrieve the extra context for Canton Coin transfers. If that is not possible, then you can read from a random Canton Coin Scan instance for the purpose of retrieving extra context for Canton Coin transfers. The on-ledger validation of the transfers ensures that you do not need to trust the Scan instance for correctness. Ensure that you read from a different Scan instance on every retry to avoid being affected by a faulty Scan instance for too long.
This section was copied from existing reviewed documentation. Source: docs/wallet-integration-guide/src/exchange-integration/node-operations.rst Reviewers: Skip this section. Remove markers after final approval.

Validator Node Operations

Reward Minting and Traffic Funding

As explained in tokenomics-and-rewards, your validator node will need traffic to submit the transactions to execute withdrawals or accept multi-step deposits. As also explained in that section, the network provides rewards that can be used to fund traffic. Note also that every validator node has an associated validator operator party that represents that validator node’s administrator (docs). The validator node automatically mints rewards for that party. It can further be configured to automatically purchase traffic using that party’s CC balance, which includes the minted rewards. We thus recommend the following setup as a starting point to mint rewards and automatically fund traffic:
  1. Use the validator operator party as your featured exchangeParty. Follow exchange-party-setup to get it featured.
  2. treasury-party-setup to create a treasuryParty with a transfer preapproval managed by your exchangeParty.
  3. Setup automatic traffic purchases in the validator app.
  4. Optional: setup auto-sweep from the exchangParty to your treasuryParty to limit the funds managed directly by the validator node.
As a starting point for the automatic traffic purchase configuration, set targetThroughput to 2kB/s and minTopupInterval to 1 minute, which should be sufficient to execute about one withdrawal or deposit acceptance every 10 seconds. Please test this with your expected traffic pattern and adjust as needed. See this FAQ to measure the traffic spent on an individual transaction.

Setup Exchange Parties

As explained above in reward-minting-and-traffic-funding, we recommend to use the validator operator party as your featured exchangeParty. This party is automatically created when you deploy your validator node. Thus the only setup step is to get it featured by the SVs: On DevNet, you can self-feature your validator operator party as follows:
  1. Log into the wallet UI for the validator user, which presents itself as in this screenshot: image
  2. Tap 20 $ of CC to ensure that your validator operator party has enough funds to purchase traffic.
  3. Click on the “Self-grant featured app rights” button.
  4. The button is replaced with a star ⭐ icon once the FeaturedAppRight contract has been created for your validator operator party. This may take about 10 seconds.
That’s all. Continue with setting up your treasury party. On MainNet, apply for featured status for your validator operator party as follows:
  1. Log into the wallet UI for the validator user on your MainNet validator node.
  2. Copy the party-id of your validator operator party using the copy button right of the abbreviated "google-oaut.." party name in the screenshot above.
  3. Apply for featured application status using this link: https://sync.global/featured-app-request/
Wait until your application is approved. The validator node will automatically pick up the featured status via the corresponding FeaturedAppRight contract issued by the DSO party for its validator operator party. On TestNet there is currently no official process, but you should be able to use the same procedure as the one for MainNet.

Setup the treasury party

Setup the treasuryParty as follows with a transfer preapproval managed by your exchangeParty:
  1. Create the treasuryParty using the wallet SDK to create-an-external-party with a key managed in a system of your choice
  2. Copy the party id of your exchangeParty from the Splice Wallet UI as explained above, or retrieve it by calling /v0/validator-user on the Validator API.
  3. Call /v2/commands/submit-and-wait on the Ledger API to create a #splice-wallet:Splice.Wallet.TransferPreapproval:TransferPreapprovalProposal (code) directly with the provider set to your exchangeParty. Note that setting up this transfer preapproval requires the exchangeParty to pay a small fee of about 0.25 $ worth of CC. The funds for this fee usually come from the validator liveness rewards that a validator node starts minting about 30 minutes after it is created. On DevNet or LocalNet, you don’t have to wait that long: just “Tap” the required funds from the built-in faucet.

Testing the party setup

You can test the party setup on LocalNet or DevNet as follows:
  1. Setup your exchangeParty and treasuryParty as explained above.
  2. Setup an additional testParty representing a customer.
  3. Transfer some CC from the testParty to the treasuryParty to simulate a deposit.
  4. Observe the successful deposit by listing holdings of the treasuryParty.
  5. Observe about 30’ later in the Splice Wallet UI of your validator operator user that the exchangeParty minted app rewards for this deposit. It takes 30’, as activity recording and rewards minting happen in different phases of a minting round.

Setup Ledger API Users

Clients need to authenticate as a Ledger API user to access the Ledger API of your Exchange Validator Node. You can manage Ledger API users and their rights using the /v2/users/... endpoints of the Ledger API. You will need to authenticate as an existing user that has participant_admin rights to create additional users and grant rights. One option is to authenticate as the ledger-api-user that you configured when setting up authentication for your validator node. Another option is to log-in to your Splice Wallet UI for the validator operatory party and use the JWT token used by the UI. We recommend that you setup one user per service that needs to access the Ledger API. This way you can easily manage permissions and access rights for each service independently. The rights required by the integration components are as follows:
ComponentRequired RightsPurpose
Tx History IngestioncanReadAs(treasuryParty)Read transactions and contracts for the treasuryParty.
Withdrawal AutomationcanActAs(treasuryParty)Prepare and execute transactions on behalf of the treasuryParty.
Multi-Step Deposit AutomationcanActAs(treasuryParty)Prepare and execute transactions on behalf of the treasuryParty.
Automated exchange parties setup for exchange-integration-testingparticipant_admin and canActAs(treasuryParty)Create parties and use the treasuryParty to create its TransferPreapprovalProposal. Hint: grant canActAs(treasuryParty) to the user doing the setup after allocating the treasuryParty.
Required Ledger API User Rights

.dar File Management

.dar files define the Daml workflows used by the token admins for their tokens. They must be uploaded to your Exchange Validator Node to be able to process withdrawals and deposits for those tokens. The .dar files for Canton Coin are managed by the Validator Node itself. The .dar files for other tokens need to be uploaded by you using the /v2/packages endpoint of the Ledger API. See this how-to guide for more information.
Only upload .dar files from token admins that you trust. The uploaded .dar files define the choices available on active contracts. Uploading a malicious .dar file could result in granting an attacker an unintended delegation on your contracts, which could lead to loss of funds.

Monitoring

See the Splice documentation for guidance on how to monitor your validator node. Note in particular that it includes Grafana dashboards for monitoring the traffic usage, balances of local parties (e.g., the exchangeParty), and many other metrics.

Rolling out Major Splice Upgrades

For major protocol changes, the global sychronizer undergoes a Major Upgrade Procedure. The schedule for these upgrades is published by the Super Validators and also announced in the #validator-operations slack channel. As part of this procedure, the old synchronizer is paused, all validator operators create an export of the state of their validator, and deploy a new validator connected to the new synchronizer and import their state again. For a more detailed overview, refer to the Splice docs. The procedure requires some experience to get it right, so it is highly recommended to run nodes on DevNet and TestNet so you can practice the procedure before you encounter it on MainNet. From an integration perspective, there are a few things to keep in mind:
  1. A major upgrade only preserves the active contracts but not the update history. In particular, you will not be able to get transactions from before the major upgrade on the update service on the Ledger API of the newly deployed validator node.
  2. Offsets on the upgraded validator node start from 0 again.
  3. The update history will include special import transactions for the contracts imported from the old synchronizer. They all have record time 0001-01-01T00:00:00.000000Z, and represent the creation of the imported contracts.

Runbook

We recommend to roll-out the upgrade as follows:
  1. Wait for the synchronizer to be paused and your node to have written the migration dump as described in the Splice docs.
  2. Open the migration dump and extract the acs_timestamp from it, e.g., using jq .acs_timestamp < /domain-upgrade-dump/domain_migration_dump.json. This is the timestamp at which the synchronizer was paused.
  3. Wait for your Tx History Ingestion to have caught up to record time acs_timestamp or higher. Note that you must consume offset checkpoints to guarantee that your Tx History Ingestion advances past acs_timestamp.
  4. Stop your Tx History Ingestion component.
  5. Upgrade your validator and connect it to the new synchronizer following the Splice docs.
  6. Follow the shortened version below of the procedure for restoring a validator node from a backup to determine the offset from which to restart your Tx History Ingestion:
    1. Retrieve the synchronizerId of the last ingested transaction from the Canton Integration DB.
    2. Log into the Canton Console of your validator node and query the offset offRecovery assigned to the ACS import transactions at time 0001-01-01T00:00:00.000000Z using
      def parseTimestamp(t: String) = {
         val isoFormat = java.time.format.DateTimeFormatter.ISO_INSTANT.withZone(java.time.ZoneId.of("Z"))
         isoFormat.parse(t, java.time.Instant.from(_))
      }
      val synchronizerId = SynchronizerId.tryFromString("example::1220b1431ef217342db44d516bb9befde802be7d8899637d290895fa58880f19accc") // example
      val tRecovery = parseTimestamp("0001-01-01T00:00:00.000000Z")
      val offRecovery = participant.parties.find_highest_offset_by_timestamp(synchronizerId, tRecovery)
      
      Alternatively, you can use grpcurl to query the offset offRecovery from the command line as shown in the example below:
      grpcurl -plaintext -d \
        '{"synchronizerId" : "example::1220be58c29e65de40bf273be1dc2b266d43a9a002ea5b18955aeef7aac881bb471a",
           "timestamp": "0001-01-01T00:00:00.000000Z"}' \
        localhost:5002 \
        com.digitalasset.canton.admin.participant.v30.PartyManagementService.GetHighestOffsetByTimestamp
      
      If you use authentication for the Canton Admin gRPC API, then you need to add the appropriate authentication flags to the grpcurl command above.
    3. Configure the Tx History Ingestion component to start ingesting from offset offRecovery.
    4. Restart the Tx History Ingestion component.
Once you have completed these steps, the integration workflows will continue.
This section was copied from existing reviewed documentation. Source: docs/wallet-integration-guide/src/exchange-integration/disaster-recovery.rst Reviewers: Skip this section. Remove markers after final approval.

Backup and Restore

Recall that the integration-architecture, shown in the diagram below, relies on two stateful components: the Exchange Validator Node and the Canton Integration DB. We recommend backing them up regularly, so that you can restore them in case of a disaster. Restoring these components from a backup can lead to data loss, which needs to be handled carefully in the integration components you are building. See the sections below for guidance on how to do so. Integration architecture component overview

Backing up the Exchange Validator Node

Follow the Splice documentation on how to backup a validator node.

Backing up the Canton Integration DB

As explained in the canton-integration-components section of the integration-architecture, the Canton Integration DB is more of a logical component. Whether you implement it as a separate DB or as part of the DBs backing your Exchange Internal Systems is up to you. Follow your internal guidance and best practices on what DB system to use and how to back it up.

Restoring the Exchange Validator Node from a Backup

Follow the Splice documentation on how to restore a validator node from a backup to restore the Exchange Validator Node from a backup that is less than 30 days old. The node will resubscribe to transaction data from the synchronizer and recover all committed transactions and the corresponding changes to the set of active contracts (i.e. UTXOs). However validator-node local data written after the backup will be lost, as described on the Canton documentation page. In the context of the recommended integration-workflows, this data loss affects:
  • .dar file uploads: handle this by repeating the upload of all .dar files that were uploaded after the backup. This should be a rare event, as token onboarding is infrequent.
  • Ledger API offsets: offsets assigned to transactions received from the Ledger API may change. This only affects the Tx History Ingestion component of the integration.

Runbook

Follow these steps to restore the Exchange Validator Node from a backup:
  1. Stop Tx History Ingestion before restoring the Exchange Validator Node from a backup.
  2. Retrieve the record time tRecovery and synchronizerId of the last ingested transaction from the Canton Integration DB.
  3. Restore the Exchange Validator Node from the backup.
  4. Reupload all .dar files that were uploaded after the backup.
  5. Log into the Canton Console of your validator node and query the offset offRecovery assigned to tRecovery using
    def parseTimestamp(t: String) = {
      val isoFormat = java.time.format.DateTimeFormatter.ISO_INSTANT.withZone(java.time.ZoneId.of("Z"))
      isoFormat.parse(t, java.time.Instant.from(_))
    }
    val synchronizerId = SynchronizerId.tryFromString("example::1220b1431ef217342db44d516bb9befde802be7d8899637d290895fa58880f19accc") // example
    val tRecovery = parseTimestamp("2024-05-01T12:34:56.789Z") // example
    val offRecovery = participant.parties.find_highest_offset_by_timestamp(synchronizerId, tRecovery)
    
    Alternatively, you can use grpcurl to query the offset offRecovery from the command line as shown in the example below:
    grpcurl -plaintext -d \
      '{"synchronizerId" : "example::1220be58c29e65de40bf273be1dc2b266d43a9a002ea5b18955aeef7aac881bb471a",
        "timestamp": "2025-11-27T06:50:00.000Z"}' \
      localhost:5002 \
      com.digitalasset.canton.admin.participant.v30.PartyManagementService.GetHighestOffsetByTimestamp
    
    If you use authentication for the Canton Admin gRPC API, then you need to add the appropriate authentication flags to the grpcurl command above.
  6. Configure the Tx History Ingestion component to start ingesting from offset offRecovery.
  7. Restart the Tx History Ingestion component.
Once Tx History Ingestion has caught up, the integration workflows will continue as before the disaster.
These steps assume that record times assigned to transactions are unique, which is the case unless you are using participant-local operations that modify the transaction history. These are ACS imports, party migrations, party replication, or repair commands. Multi-hosting a party from the start does not lead to non-unique record times.If your are using participant-local operations that modify the transaction history, then you we recommend adjusting Step 5 as follows to deal with the rare case of a partial ingestion of transactions with the same record time:
  1. Lookup the recovery offset offRecovery as of tRecovery - 1 microsecond.
  2. Start ingesting from offset offRecovery, but filter out all transactions whose update-id is already known in the Canton Integration DB because they have been ingested before Tx History Ingestion was stopped in Step 1.

Restoring the Canton Integration DB from a Backup

Follow your internal guidance and best practices on how to restore the Canton Integration DB from a backup. From a data consistency perspective, all writes to the Canton Integration DB by Tx History Ingestion will be recovered without data loss from the transactions stored on the Exchange Validator Node. Likewise, the write in Step 3 of the one-step-withdrawal-workflow to mark a withdrawal as failed due to the lack of a CC transfer-preapproval is safe to redo, as it is idempotent. Thus the only data loss that you need to handle is the loss of data written by your Exchange Internal Systems to the Canton Integration DB to request the execution of a withdrawal. This data consists in particular of the withdrawal-id, the UTXO reservation state, and the reservation of user funds for the withdrawal. See Step 2 in the one-step-withdrawal-workflow and Step 2 in the multi-step-withdrawal-workflow for details. The problem to avoid is for the user to initiate another withdrawal of the funds whose withdrawal might be in-flight on Canton. You can do so as follows:
  1. Disable initiating withdrawals of CN tokens in your Exchange Internal Systems and stop the Withdrawal Automation component.
  2. Restore the Canton Integration DB from the backup.
  3. Wait until Tx History Ingestion has ingested a record time tSafe that is larger than the largest target record time trecTgt of all in-flight withdrawals. Assuming you use a constant ttl to compute the trecTgt of a withdrawal, you can estimate tSafe as now + ttl.
  4. Enable withdrawal creation in your Exchange Internal Systems and start the Withdrawal Automation component. The integration is operational again.
Step 3 takes care to resynchronize the state of the Canton Integration DB with the state of in-flight withdrawals on Canton. For this to work it is important that you implement Tx History Ingestion such that it can handle ingesting withdrawal transfers whose withdrawal-id cannot be resolved because the corresponding withdrawal request was lost in the restore. We recommend doing so by having the Tx History Ingestion re-create the withdrawal request record from the on-chain data. Likely not all fields can be recovered, so consider either marking the withdrawal as “recovered” and leaving them blank. Alternatively, you can store these fields in additional metadata on the transfer record when creating the withdrawal transfer on-chain. This will though cost additional traffic and may leak information to your customer and the token admin.
This section was copied from existing reviewed documentation. Source: docs/wallet-integration-guide/src/exchange-integration/testing.rst Reviewers: Skip this section. Remove markers after final approval.

Integration Testing

Test Node Setup

When testing on your laptop or in CI, we recommend using Splice’s LocalNet, which is a Docker-Compose based local deployment of a Global Synchronizer and Canton Coin. Automate the exchange parties setup as part of your test setup, so that you can start from a clean state for each test run while reusing the same LocalNet. Thereby achieving test isolation without the overhead of starting and stopping LocalNet for each test run. Alternatively you can consider setting up a DevNet validator node using either Docker-Compose or k8s as documented in Splice and using that for testing.

Test Scenarios

Apart from testing functional correctness, we recommend testing for robustness and fault tolerance of your integration code. In particular, we recommend testing for the following scenarios:
  • Crash-fault tolerance, in particular:
    • successfully continuing Tx History Ingestion after a crash
    • successfully continuing Withdrawal Automation after a crash
    • handling the case that two withdrawal transfers are initiated for the same withdrawal request: test that only one of them succeeds because they spend the same UTXOs, which is detected by Canton
    • successfully continuing of the Multi-Step Deposit Automation after a crash, and handling the case that the deposit offer was accepted while the Multi-Step Deposit Automation was down
  • Retrying on RPC errors, in particular:
    • retries that succeed after a few attempts
    • retries that do not succeed within the bounded number of retries, and how the integration code marks the withdrawal or deposit offer as failed.
  • Does your integration code deal well with high rates of deposits and withdrawals. We recommend to determine target throughput rates for deposits and withdrawals and test that your integration code can handle those rates without falling behind. In particular, test:
    • Can Tx History Ingestion keep up with the rate of deposits and withdrawals.
    • Can utxo-management deal with the case that there are no UTXOs available to fund a withdrawal.
    • Does your integration code rate limit executing transactions on the Validator Node to avoid running out of traffic with your automatic traffic configuration. See the validator-node-monitoring section for more information.
    • Does your utxo-management code handle the case where there are only small UTXOs available, and they first have to be merged before they can be used to fund a withdrawal.
    • Does your integration code properly rate limit bursts of deposits and withdrawals above the target throughput rate.
    • Does your integration code gracefully handle a crash when under full load.
  • Does your integration code recover from data loss due to
    • validator_backup_restore
    • restore-canton-integration-db
  • Does your integration code handle hard-synchronizer-migration. Note that simulating a major Splice upgrade is not easily possible with LocalNet. We thus recommend to the check the schedule for major Splice upgrades and ensure that you are ready to handle the first one on DevNet.
Where possible, we recommend to automate these tests as part of your CI pipeline so that you can run them frequently and with little overhead.
This section was copied from existing reviewed documentation. Source: docs/wallet-integration-guide/src/exchange-integration/extensions.rst Reviewers: Skip this section. Remove markers after final approval.

Integration Extensions

This page describes the following additional features that you can consider adding to your integration, beyond the MVP described in the exchange-integration-overview section:

Optimizing App Rewards

The MVP for all CN tokens described in the exchange-integration-overview section comes with the limitation that application rewards are only earned on deposits of CC, but not on deposits of other CN tokens. We recommend to lift this limitation and to improve the profitability of the integration using Canton Coin’s featured application activity marker mechanism. It allows tagging transactions with a featured application activity marker and earn application rewards for them. The idea is to tag both the initatiation of withdrawals and the acceptance of deposit offers with a featured application activity marker to attribute the transaction to the exchangeParty. Tagging these transactions is compliant with the guidance given in the Splice documentation, as they correspond to transfers and create value for the network. In order for the treasuryParty to create featured application activity markers in the name of the exchangeParty, a delegation contract is required. A suitable delegation template called DelegateProxy is part of the splice-util-featured-app-proxies package. We recommend to use this package and template as explained in the sections below.

Earning App Rewards for Withdrawals

The following steps describe how to adjust the Withdrawal Automation to tag withdrawal transfers with a featured application activity marker.
  1. Download the most recent version of the splice-util-featured-app-proxies.dar file from the Splice repository’s checked-in .dars. Note that at the time of writing, there was no official release of the Splice .dars that included this package, which is why we recommend downloading the .dar directly from the repository.
  2. Upload that splice-util-featured-app-proxies.dar file to your Exchange Validator Node.
  3. Change the Ledger API user setup such that the
    1. the user used by Withdrawal Automation also has the readAs(exchangeParty) right
    2. the user that performs the exchange parties setup also has the canActAs(exchangeParty) right.
  4. Add a step to the treasury party setup to also create a DelegateProxy contract with provider = exchangeParty and delegate = treasuryParty. Use the /v2/commands/submit-and-wait endpoint submit the create command for the DelegateProxy template.
  5. Change the initialization code of the Withdrawal Automation to:
    1. query the active contracts of the exchangeParty for the DelegateProxy contract created in the previous step and store its contract ID in proxyCid.
    2. query the active contracts of the exchangeParty for the FeaturedAppRight contract and store its contract ID in featuredAppRightCid and its create-event-blob in featuredAppRightEventBlob.
  6. Change the Withdrawal Automation code that initiates a withdrawal transfer to call the DelegateProxy_TransferFactory_Transfer choice instead of the TransferFactory_Transfer choice, as shown in this test case. The call to the choice takes the proxyCid and the featuredAppRightCid as parameters alongside the actual transfer parameters. Pass in the featuredAppRightEventBlob as an additional disclosed contract.
The Tx History Ingestion as described here does not need changing, as it descends into the TransferFactory_Transfer choice that is called by the DelegateProxy_TransferFactory_Transfer choice.

Earning App Rewards for Deposits

Steps 1 to 5 are analogous to the steps described in the withdrawal-app-rewards section above. In Step 6, change the Deposit Automation code that accepts a deposit offer to call the DelegateProxy_TransferInstruction_Accept choice instead of the TransferInstruction_Accept choice, as shown in this test case.

Sharing App Rewards with your Customers

The featured app marker API allows splitting the activity record across multiple beneficiaries. Each of them then gets credited for a fraction of the activity. You can use this feature to share some of the application rewards with your customers to incentivize them to use your exchange. To do so, you need to adjust the code changes described in the sections above to pass in multiple beneficiaries to the respective choices, as called out in this test case.

Sharding the Treasury

Sharding your treasury over multiple treasury parties may be interesting to reduce the risk of compromise of a single treasuryParty’s private key. Using multiple treasury parties also provides operational flexibility with respect to which validator nodes host what party. This can be useful for load balancing or to incrementally change your party hosting setup. You can shard your treasury over multiple parties as follows:
  1. Setup multiple treasury parties instead of using a single treasuryParty. Use the setup described in the treasury-party-setup section for each of them.
  2. Run one instance of Tx History Ingestion, Withdrawal Automation, and Multi-Step Deposit Automation for each treasury party.
  3. Share the Canton Integration DB across all instances, but adjust the schema such that UTXOs and pending multi-step transfers are tracked per treasury party.
  4. Change your Exchange Internal Systems such that they select the treasury party as well as the Holding UTXOs to use for funding a withdrawal. For large withdrawals that surpass the funds available to a single treasury party, you can either rebalance the funds across multiple treasury parties or split the withdrawal into multiple smaller ones.

Multi-Hosting the Treasury Party

The documentation on setting up the exchange party describes how to setup a party with a single confirming node. This can be sufficient but the confirming nodes for the party are essential to keep your party secure and compromise of them could lead to loss of funds. Refer to the trust model trust model for more details. To guard against compromise of the confirming nodes, you can setup your treasuryParty with multiple confirming nodes and a threshold N > 1. As long as less than N nodes are compromised, your party is still secured. Common setups are:
  1. Two confirming nodes with a threshold of 2. This provides security against a single node being compromised. However, if one of the two nodes is down, transactions for the party will fail.
  2. Three confirming nodes with a threshold of 2. This extends the previous setup to also provide availability in case one of the nodes goes down or gets compromised as the other two nodes are still functional.

Party Setup

https://github.com/canton-network/wallet-gateway/issues/272 Update this when wallet SDK support is available
As part of the initial treasury party setup, you generate the PartyToParticipant topology transaction which lists both the confirming nodes and the confirmation threshold. To host a party on multiple nodes, you need to include all confirming nodes in the PartyToParticipant mapping when you setup the party initially. Note that at this point, the wallet SDK library does not yet support this so you must go directly through the Canton APIs. This is expected to change soon. Until then, the easiest way to do so at the moment is through the Canton console. You can find a full reference for all required steps in the integration test. Note in particular that you must sign the PartyToParticipant mapping not just by your party’s key but also by all confirming participants. This is accomplished through the participant2.topology.transactions.authorize step in the test.

.dar File Management

Any .dar file that you upload, both as part of the initial setup but also whenever you upload newer versions to upgrade an existing package, must be uploaded to all validator nodes hosting your party.

Reading Data and Submitting Transactions

Both nodes serve all transactions for the treasuryParty and can thus be used in principle to read them. However, offsets are not comparable across nodes so it is recommended that to run Tx History Ingestion against the same node under normal operations. If you do need to switch nodes, you can do so following the same procedure used for restoring a validator from a backup to resynchronize Tx History Ingestion against the offsets of the new node. Preparation and execution of transactions can also be done against any of the confirming nodes of the party. However, Command Deduplication is only performed by the executing node so if you submit across nodes you cannot rely on it. It is therefore recommend _not to rely on command deduplication at all in favor of UTXO and max record time based deuplication.
Link to recommended deduplication strategy https://github.com/canton-network/wallet-gateway/issues/423

Changing the set of Confirming Nodes

There are some limitations on changing the set of confirming nodes: Removing confirming nodes is possible by submitting a new PartyToParticipant topology transaction. However, this can leave the nodes that you remove in a broken state so this should be limited to cases where that node got compromised or is no longer needed for other purposes. Adding new confirming nodes is not currently possible. If this is required, you need to instead:
  1. Setup a new treasury party with the desired set of confirming nodes.
  2. Either transfer all funds from the existing treasury party to the new one and switch only to the new treasury party or rely on treasury-sharding to use both treasury parties until you are ready to phase out the old party.
Changing the confirmation threshold is possible at any point by submitting a new PartyToParticipant topology transaction with the updated threshold. Future versions of Canton will allow changing the confirming nodes without the need for setting up a new party.

Using a KMS for Validator Node Keys

See the Splice docs for how to setup you validator node with keys stored in a KMS. Consider doing so as an additional security hardening measure to protect the keys of the confirming node(s)_ of your treasuryParty.

Using the gRPC Ledger API

Feel free to do so if you prefer using gRPC. It is functionally equivalent to the JSON Ledger API. See this Ledger API overview for more information.