Skip to content

DLC-link/cbtc-transfer-public-docs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 

Repository files navigation

cBTC Transfer documentation

The cBTC token supports the Canton Token Standard (CIP-0056) across all environments. More information about the token standard can be found here.

Assumptions

  • You have a participant host that allows you to communicate with the participant node. We will refer to it later as LEDGER_HOST.
  • You already have an OIDC provider set up for your Canton ecosystem — this can be Keycloak, Auth0, or any other provider.

Detailed example

Get token standard contracts

If you want to make transfers with a token that is compliant with the token standard, it is mandatory to use the factory contracts defined by that standard. You can fetch them using the following cURL command. You will need two contracts: Utility.Registry.App.V0.Service.AllocationFactory:AllocationFactory and Utility.Registry.V0.Configuration.Instrument:InstrumentConfiguration.

curl -X POST Https://devnet.dlc.link/attestor-1/app/get-token-standard-contracts -d '{"chain": "canton-devnet"}' | jq

Act as sender

You need the OIDC provider so that you can act on behalf of parties on the Canton Network. This means you can log in to an account that represents the sender party who already owns cBTC. I’ve added an example request to illustrate how it works. This request will return an access token, which you should use in subsequent requests.

curl -X POST "$KEYCLOAK_HOST/auth/realms/$KEYCLOAK_REALM/protocol/openid-connect/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=password" \
  -d "client_id=$CLIENT_ID" \
  -d "username=$USERNAME" \
  -d "password=$PASSWORD" | jq

# OR

ACCESS_TOKEN=$(curl -X POST "$KEYCLOAK_HOST/auth/realms/$KEYCLOAK_REALM/protocol/openid-connect/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=password" \
  -d "client_id=$CLIENT_ID" \
  -d "username=$USERNAME" \
  -d "password=$PASSWORD" | jq -r '.access_token')

Get the active input contracts

The next step is to retrieve the active contracts that you can use for the transfer. First, you need to get the latest ledger end (or offset — the naming may vary) by sending the following request to the participant.

curl -X GET $LEDGER_HOST/v2/state/ledger-end \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ACCESS_TOKEN"  | jq

# OR

LEDGER_OFFSET=$(curl -X GET "$LEDGER_HOST/v2/state/ledger-end" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r '.offset')

After this, you can retrieve the contract IDs. Make sure to filter for the Holding interface, as it returns the token-standard-compliant active contracts. You can view the amounts in either the createArgument or the interfaceViews. Also, check the instrument.id to ensure you’re only using CBTC contracts. Select enough contracts to cover the amount you want to send to the receiving party, or simply pass all the CBTC-related contracts, and the system will automatically choose the required amount.

curl -X POST $LEDGER_HOST/v2/state/active-contracts \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d "$(jq -n --arg offset "$LEDGER_OFFSET" '{
    filter: {
      filtersByParty: {
        "$SENDER_PARTY_ID": {
          cumulative: [
            {
              identifierFilter: {
                InterfaceFilter: {
                  value: {
                    interfaceId: "#splice-api-token-holding-v1:Splice.Api.Token.HoldingV1:Holding",
                    includeInterfaceView: true,
                    includeCreatedEventBlob: true
                  }
                }
              }
            }
          ]
        }
      }
    },
    verbose: false,
    activeAtOffset: ($offset | tonumber)
  }')" | jq

Decentralized party

We have different decentralized parties for each environment, so make sure to use the correct one for the appropriate environment.

Devnet

DECENTRALIZED_PARTY_ID=cbtc-network::12202a83c6f4082217c175e29bc53da5f2703ba2675778ab99217a5a881a949203ff

Testnet

DECENTRALIZED_PARTY_ID=cbtc-network::12201b1741b63e2494e4214cf0bedc3d5a224da53b3bf4d76dba468f8e97eb15508f

Mainnet

DECENTRALIZED_PARTY_ID=cbtc-network::12205af3b949a04776fc48cdcc05a060f6bda2e470632935f375d1049a8546a3b262

Registry URL

There is a different registry URL for each environment.

Devnet

REGISTRY_URL=https://api.utilities.digitalasset-dev.com

Testnet

REGISTRY_URL=https://api.utilities.digitalasset-staging.com

Mainnet

REGISTRY_URL=https://api.utilities.digitalasset.com

Send the transfer

Open the transfer.sh file to view the cURL command used for a token-standard-compliant transfer request. The response from this request will be used later. In the choiceArgument section, you’ll find an explanation of the schema:

  • expectedAdmin: Always set to $DECENTRALIZED_PARTY_ID for CBTC transfers.
  • transfer.sender: The partyID of the sender.
  • transfer.receiver: The partyID of the receiver.
  • transfer.amount: The amount to be transferred.
  • transfer.instrumentId.admin: Always set to $DECENTRALIZED_PARTY_ID for CBTC transfers.
  • transfer.instrumentId.id: Always set to CBTC for CBTC transfers.
  • transfer.requestedAt: The time when the transfer request is made - typically NOW().
  • transfer.executeBefore: The time (exclusive) until which the transfer may be executed. This must be a future time when issuing the transfer instruction. Registries should not execute the transfer instruction after this time, allowing senders to retry by creating a new transfer instruction once it expires.
  • transfer.inputHoldingCids: A list of contract IDs from the active contracts request.
  • transfer.meta: Leave as is.
  • extraArgs: Leave as is.

Get disclosed contracts

You can send a request to the POST $REGISTRY_URL/api/token-standard/v0/registrars/$DECENTRALIZED_PARTY_ID/registry/transfer-instruction/v1/transfer-factory endpoint to retrieve the missing information required for the actual transfer submission. You can use the disclosed contracts from the response to populate the request’s disclosedContracts section. Additionally, you can use the factoryId as the contractId for the transfer submission. See the example request to this endpoint below.

curl -X POST $REGISTRY_URL/api/token-standard/v0/registrars/$DECENTRALIZED_PARTY_ID/registry/transfer-instruction/v1/transfer-factory \
  -H "Content-Type: application/json" \
  -d '{
  "choiceArguments": {
    "expectedAdmin": "$DECENTRALIZED_PARTY_ID",
    "transfer": {
      "sender": "",
      "receiver": "",
      "amount": 0.002,
      "instrumentId": {
        "admin": "$DECENTRALIZED_PARTY_ID",
        "id": "CBTC"
      },
      "requestedAt": "2025-11-05T18:33:30Z",
      "executeBefore": "2025-11-10T18:33:47Z",
      "inputHoldingCids": [
        "00729ad73e6af0c69af833fc7b59ca3d949f06472664be95df7ab4ec1a8565f0fcca111220919a93e464eae32d4b159085039de601185aee0cb5a324632cf430da77c3e51a"
      ]
    },
    "extraArgs": {
      "context": {
        "values": {
          "utility.digitalasset.com/instrument-configuration": {
            "tag": "AV_ContractId",
            "value": "00da61fcfa2d9b358c606f040a1d635fbabe4265d33908744a148a80be0dcdc383ca111220e86787467ef7be665f68b813a30de6b0480955dcb514ef29832720618375dee5"
          },
          "utility.digitalasset.com/sender-credentials": {
            "tag": "AV_List",
            "value": []
          },
          "instrument-configuration": {
            "tag": "AV_ContractId",
            "value": "00da61fcfa2d9b358c606f040a1d635fbabe4265d33908744a148a80be0dcdc383ca111220e86787467ef7be665f68b813a30de6b0480955dcb514ef29832720618375dee5"
          },
          "sender-credentials": {
            "tag": "AV_List",
            "value": []
          }
        }
      },
      "meta": {
        "values": {}
      }
    }
  },
  "excludeDebugFields": true
}'

Get choice context for accept

The Send Transfer section returns the contract changes that occurred in the transaction. You need to locate the contract ID of the Utility.Registry.App.V0.Model.Transfer:TransferOffer template. This contract ID is required to first retrieve the context and then accept the transfer.

Before you can accept the transfer, you must obtain the context by calling the following API endpoint:

curl -X POST $REGISTRY_URL/api/token-standard/v0/registrars/$DECENTRALIZED_PARTY_ID/registry/transfer-instruction/v1/$TRANSFER_OFFER_CONTRACT_ID/choice-contexts/accept \
  -H "Content-Type: application/json" \
  -d '{"meta":{}}'

You will receive a response consisting of two main sections: choiceContextData.value and disclosedContracts. You will need both of these to accept the TransferOffer.

Accept transfer

As the final step, you can accept the transfer. It’s important to note that this action must be submitted on behalf of the receiver party (The ACCESS_TOKEN must be able to actAs the receiver party.). This means you can only accept a transfer if you are authorized to actAs that party.

In the future, this requirement will change when Canton introduces pre-approval for the token standard.

curl -X POST $LEDGER_HOST/v2/commands/submit-and-wait-for-transaction-tree \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d '{
  "commands": [
    {
      "ExerciseCommand": {
        "templateId": "#splice-api-token-transfer-instruction-v1:Splice.Api.Token.TransferInstructionV1:TransferInstruction",
        "contractId": "$TRANSFER_OFFER_CONTRACT_ID",
        "choice": "TransferInstruction_Accept",
        "choiceArgument": {
          "extraArgs": {
            "context": {
              "values": $choiceContextData.value
            },
            "meta": {
              "values": {}
            }
          }
        }
      }
    }
  ],
  "commandId": "$UUID",
  "actAs": [
    "$RECEIVER_PARTY"
  ],
  "disclosedContracts": $disclosedContracts
}'

About

cBTC transfer documentation by using Token Standard

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages