For the complete documentation index, see llms.txt. This page is also available as Markdown.

Batch transfers and UTXO management

Outcome

Distribute CBTC to multiple Canton parties, record one result for each sender-side transfer, and manage CBTC holdings without creating an unsafe retry or consolidation workflow.

Terminology

This guide uses UTXO as shorthand used by the current library for an active CBTC holding contract. A CBTC holding on Canton Network is not a Bitcoin UTXO.

Before you start

  • Complete one sender-to-receiver transfer and receiver acceptance in the intended environment.

  • Use an Engineering-approved cbtc-lib, canton-lib, DAR, and Canton compatibility row.

  • Confirm authentication, the sender party, the CBTC instrument identifier, the registry service, and the intended environment.

  • Query the sender’s current holdings and outgoing offers.

  • Confirm the sender has enough available CBTC for the full distribution.

  • Define a durable application-side row identifier and reconciliation record for every recipient.

  • Stop other processes from spending or restructuring the same sender’s holdings during the run.

A batch is not one atomic transfer. In v0.6.4, the library submits sender-side transfers sequentially. Each successful submission creates a transfer instruction or offer that the receiver must still accept unless a separately supported preapproval flow applies.

Choose the batch interface

Interface
Use
Result handling

cbtc::batch::submit_from_csv()

Read receiver,amount rows from a CSV file and submit a sequential distribution.

The helper returns an overall Result<(), String>. It does not return the per-item result collection to the caller. Use it only when its logging behavior meets the approved operational policy.

cbtc::distribute::submit()

Submit an in-memory recipient list.

Returns successful and failed counts plus one result per row. It can call an asynchronous callback after each row completes.

For production-style reconciliation, prefer the interface that exposes per-item results and a durable callback.

How the current sequential flow works

At v0.6.4, the distribution flow:

  1. Authenticates and refreshes the access token when required.

  2. Queries all active sender holdings once at the start.

  3. Uses those holding contract identifiers as the first transfer’s inputs.

  4. Fetches transfer-factory context once and reuses it for the run.

  5. Submits one transfer at a time.

  6. Uses the sender change holdings returned by a successful row as the next row’s inputs.

  7. Records success or failure for each row and then attempts the next row.

This is a sequential chained flow. It is not concurrent, and it does not pre-split holdings.

1. Prepare and validate recipient input

The repository example accepts this CSV shape:

Before passing data to the library:

  • Reject empty receiver identifiers.

  • Validate every receiver against the intended environment.

  • Parse every amount as the supported decimal type.

  • Reject zero, negative, or amounts outside the documented policy limits.

  • Reject duplicate application-side row identifiers.

  • Review repeated receiver identifiers intentionally. Do not assume one receiver appears only once.

  • Sum all amounts and compare the total with the sender’s available balance.

The CSV helper rejects malformed amounts and an empty recipient list. The reviewed source does not implement business-reference deduplication or complete recipient-policy validation. Perform those checks in the calling application.

2. Create a run record

Create a durable run record before submission. Store:

  • Application run identifier.

  • Application row identifier.

  • Row index.

  • Receiver.

  • Amount.

  • Intended environment.

  • Submission state.

  • Transfer-offer contract identifier when available.

  • Canton update identifier when available.

  • Library reference when available.

  • Error and raw-response location, with secrets removed.

Reference limitation in v0.6.4

The optional reference_base generates a reference from the base value, sender, and receiver. It does not include the row index or amount. Repeated rows for the same receiver can therefore produce the same generated reference.

Do not treat this generated value as a guaranteed per-row idempotency key. Keep a separate application-side row identifier and reconcile state before replay.

3. Check holdings and pending work

Immediately before submission:

  1. Query active CBTC holdings for the sender.

  2. Record the holding contract identifiers and total available amount.

  3. Query pending outgoing offers and unresolved withdrawals.

  4. Confirm no other batch, split, consolidation, redemption, or transfer process can consume the same holdings.

  5. Apply the holding-count and transaction-size limits documented for the supported release.

The batch implementation fetches holdings once. Concurrent mutations can make its chained input set stale.

4. Run a batch

The simple repository example uses:

The callback example uses:

Use placeholders for credentials and environment values. Never commit .env files, access tokens, passwords, or production party identifiers.

5. Record each callback result

The callback receives one result after each sender-side submission attempt.

Field
Use

success

Whether the library classified the sender-side submission and response parsing as successful.

transfer_index

Zero-based position in the submitted recipient list.

receiver, amount

Match the result to the intended row. Also verify the application-side row identifier.

transfer_offer_cid

Track the created transfer instruction or offer.

update_id

Correlate the result with the Canton update.

reference

Store as supporting metadata. Do not assume uniqueness under the limitation above.

raw_response

Retain securely for diagnostics when policy permits. Remove secrets before support escalation.

error

Classify the row as failed or ambiguous and apply the approved recovery procedure.

Make the callback durable and safe to call once per completed attempt. If writing to a database, protect the application row from duplicate callback processing.

6. Reconcile the run

Classify each row separately:

  • Submitted: The callback includes a transfer-offer contract identifier and Canton update identifier. This confirms sender-side submission, not receiver acceptance.

  • Accepted: The receiver accepted the intended offer, and both parties’ holdings reflect the completed transfer.

  • Failed before submission: Evidence proves that no ledger update committed.

  • Ambiguous: The request may have committed, but the client lacks a parseable or authoritative result.

  • Rejected, cancelled, or expired: The offer reached the corresponding terminal state. Use the versioned reference for exact state names and expiry policy.

After the sender-side run, verify incoming offers with each receiver or the approved receiving service. Do not mark a distribution complete from successful_count alone.

Partial failure and safe resume

The reviewed implementation continues to later rows after a failed submission or response-parsing error. It preserves the last known input-holding list in memory.

For production recovery:

  1. Stop the run when an outcome is ambiguous.

  2. Requery active holdings and outgoing offers from authoritative sources.

  3. Match receiver, amount, application row identifier, offer identifier, update identifier, and available reference metadata.

  4. Mark already-created offers as submitted. Do not create replacements for them.

  5. Resume only rows proven not to have committed.

  6. Build a new run from fresh holdings. Do not reuse a stale in-memory holding list.

  7. Keep accepted, pending, failed, and ambiguous rows in separate states.

Never rerun the complete CSV after partial success.

Concurrency policy

cbtc-lib v0.6.4 processes one distribution sequentially. Do not run parallel distributions, transfers, redemptions, splits, or consolidations from the same sender without an application-level lock and fresh-state strategy.

Use the supported release limits for recipient count, total amount, execution duration, timeouts, and rate limits.

Manage CBTC holdings

Inspect holdings

Use the active-contract query or the consolidation count helper to record:

  • Holding count.

  • Holding contract identifiers.

  • Total amount.

  • Pending operations that reference those holdings.

  • Last successful reconciliation time.

A high holding count can increase transaction size and operational complexity. Use the current operational limits when deciding whether to consolidate.

Consolidate holdings

The v0.6.4 consolidation function performs a self-transfer with merge-split metadata. It can consolidate all active holdings or a selected set. If the party has fewer holdings than the supplied threshold, check_and_consolidate() does nothing. If the threshold is met or exceeded, it consolidates all queried holdings and returns the resulting holding contract identifiers.

The repository example uses a configurable threshold and defaults that example to 10. Treat this as example behavior, not a production limit. Use the threshold documented for the target environment.

Run the example only after review:

Before consolidation:

  • Reconcile pending transfers and withdrawals.

  • Confirm the selected holdings are still active.

  • Stop concurrent operations for the party.

  • Estimate the expected resulting holding count and transaction impact.

  • Record the input holding identifiers.

After consolidation, query the party again and verify the total amount and returned holding identifiers.

Split holdings

The v0.6.4 split function uses sequential self-transfers. It separates each requested amount in order, then feeds the returned change holdings into the next split. It returns output holding identifiers and remaining change identifiers.

Use split only when an approved downstream workflow requires specific holding sizes. Confirm:

  • The requested amounts and order.

  • The selected input holdings.

  • The expected remaining change.

  • Behavior when requested amounts consume the exact available total.

  • The release-specific recovery procedure if a later split step fails.

Do not split solely to prepare the sequential batch flow. The current batch implementation is designed to chain change holdings without pre-splitting.

Expected result

A completed operation has:

  • One durable record per recipient.

  • A reconciled sender-side result for every row.

  • Receiver acceptance or another approved terminal state for every created offer.

  • No ambiguous row left queued for automatic retry.

  • A final sender balance and holding set that match the reconciled run.

  • A recorded before-and-after holding set for every split or consolidation.

Troubleshooting

Symptom
Check
Safe next action

No recipients found

CSV path, header, and parsed row count

Correct the file. Do not submit an unreviewed replacement.

Invalid amount

Decimal syntax and business amount rules

Reject the row before any submission.

No holdings available

Sender party, environment, active holdings, and pending operations

Stop and reconcile the sender. Do not retry with another party implicitly.

Some rows succeed and later rows fail

Per-item results, current holdings, token refresh, and errors

Do not rerun the complete CSV. Requery authoritative state and resume only proven-uncommitted rows.

Ledger response exists but parsing fails

Raw response, update visibility, outgoing offers, and client compatibility

Treat the row as ambiguous. Reconcile before any retry.

Sender-side success but receiver balance is unchanged

Incoming offer, receiver acceptance, preapproval policy, rejection, cancellation, and expiry

Complete or resolve the receiver-side flow. Do not duplicate the sender submission.

Generated references repeat

Repeated receiver identifiers and shared reference_base

Use application-side row identifiers. Escalate before relying on the library reference for replay protection.

Consolidation returns an unexpected holding set

Input holdings, concurrent operations, transaction response, and final active holdings

Stop new operations and reconcile total amount and contract identifiers.

Split stops after creating earlier outputs

Completed split steps, current change holdings, and remaining requested amounts

Requery holdings. Do not restart from the original input identifiers.

Technical sources

Next step

👉 Plan safe recovery: CBTC errors, retries, and troubleshooting.

Last updated