From 0148c419f3369bee7f5c74db41e524712fe4ebec Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 29 Apr 2026 22:54:56 +0200 Subject: [PATCH] docs(ledgers): add Transaction deduplication section Extracted the single-sentence deduplication note from ### Fee handling into a dedicated ### Transaction deduplication section. Covers what the ledger tracks, how to enable it (Motoko and Rust), the Duplicate response and its duplicate_of field, and the TooOld / CreatedInFuture boundary errors. References the existing code examples above rather than adding redundant tabs. Updated the bitcoin.mdx CLI note to link to #transaction-deduplication instead of the broader #transferring-assets-icrc-1 anchor. --- docs/guides/chain-fusion/bitcoin.mdx | 2 +- docs/guides/digital-assets/ledgers.mdx | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/guides/chain-fusion/bitcoin.mdx b/docs/guides/chain-fusion/bitcoin.mdx index fa9eeb6c..ec52779b 100644 --- a/docs/guides/chain-fusion/bitcoin.mdx +++ b/docs/guides/chain-fusion/bitcoin.mdx @@ -454,7 +454,7 @@ icp canister call mxzaz-hqaaa-aaaar-qaada-cai icrc1_transfer \ })" -n ic ``` -`created_at_time = null` skips deduplication: if you run this command twice, both transfers execute. In production canister code, set this field to the current nanosecond timestamp so that retried calls are rejected as duplicates rather than sending twice. See [Transferring assets (ICRC-1)](../digital-assets/ledgers.md#transferring-assets-icrc-1) for details. +`created_at_time = null` skips deduplication: if you run this command twice, both transfers execute. In production canister code, set this field to the current nanosecond timestamp so that retried calls are rejected as duplicates rather than sending twice. See [Transaction deduplication](../digital-assets/ledgers.md#transaction-deduplication) for details. ### Common mistakes diff --git a/docs/guides/digital-assets/ledgers.mdx b/docs/guides/digital-assets/ledgers.mdx index 5358bf41..f6c1439c 100644 --- a/docs/guides/digital-assets/ledgers.mdx +++ b/docs/guides/digital-assets/ledgers.mdx @@ -172,7 +172,23 @@ Always set the `fee` field explicitly. If you pass a fee that does not match the icp canister call ryjl3-tyaaa-aaaaa-aaaba-cai icrc1_fee '()' -n ic ``` -Always set `created_at_time` to enable deduplication. Without it, two identical transfers submitted within 24 hours both execute. +### Transaction deduplication + +When `created_at_time` is set to the current nanosecond timestamp, the ledger tracks submitted transactions and rejects exact duplicates within a 24-hour window. A duplicate submission returns `Duplicate { duplicate_of: block_index }` instead of executing again. The `duplicate_of` value is the block index of the original accepted transaction, so you can confirm it succeeded without re-submitting. + +Without `created_at_time` (set to `null`), every submission is treated as a new transaction: submitting the same call twice sends the amount twice. + +Set `created_at_time` to the current nanosecond timestamp to enable deduplication: + +- **Motoko**: `Nat64.fromNat(Int.abs(Time.now()))` (as shown in `sendTokens` above) +- **Rust**: `ic_cdk::api::time()` (as shown in `send_tokens` above) + +Two boundary errors to handle alongside the normal transfer errors: + +- `TooOld`: the timestamp is more than 24 hours in the past. The ledger no longer tracks that window and rejects the transaction. +- `CreatedInFuture { ledger_time }`: the timestamp is ahead of the ledger's current time, typically due to system clock drift. The `ledger_time` field shows the ledger's view of the current time so you can diagnose the skew. + +Always set `created_at_time` in production canister code. `null` is only appropriate for one-off manual CLI calls where double-submission is not a concern. ## Checking balances