Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/guides/chain-fusion/bitcoin.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
18 changes: 17 additions & 1 deletion docs/guides/digital-assets/ledgers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading