Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add docs best effort responses #3865

Open
wants to merge 54 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
f61fe5a
WIP: adjust the docs to best-effort responses
oggy-dfin Nov 29, 2024
5f89f7e
Adjust the safe retries doc
oggy-dfin Dec 3, 2024
44806de
Adjust security best practices
oggy-dfin Dec 5, 2024
410e042
Improve the terminology in the message execution properties doc
oggy-dfin Dec 6, 2024
2916a32
Revamp the overview doc for inter-canister calls
oggy-dfin Dec 6, 2024
0613d32
Fix some typos
oggy-dfin Dec 6, 2024
d344cd9
Some more improvements
oggy-dfin Dec 10, 2024
ddf5818
Apply suggestions from code review
oggy-dfin Dec 11, 2024
e213314
Address David's and Jessie's comments
oggy-dfin Dec 12, 2024
7d82156
Update docs/references/message-execution-properties.mdx
oggy-dfin Dec 12, 2024
d545ab8
Update docs/references/message-execution-properties.mdx
oggy-dfin Dec 12, 2024
0132c93
Update docs/references/message-execution-properties.mdx
oggy-dfin Dec 12, 2024
9eb0ce2
Update docs/references/message-execution-properties.mdx
oggy-dfin Dec 12, 2024
7f4f09b
Update docs/references/message-execution-properties.mdx
oggy-dfin Dec 12, 2024
d626bb4
Address Alin's comments (WIP)
oggy-dfin Dec 12, 2024
7da5a43
Address Alin's comments
oggy-dfin Dec 12, 2024
dacf8bc
Minor improvements
oggy-dfin Dec 12, 2024
7a4f424
Update docs/developer-docs/smart-contracts/advanced-features/async-co…
oggy-dfin Dec 13, 2024
2cd1f77
Andy's comments
oggy-dfin Dec 23, 2024
bf6cb13
Merge branch 'master' into oggy/best-effort-responses
oggy-dfin Dec 23, 2024
2ad10b6
Improve the messaging properties doc a bit
oggy-dfin Dec 23, 2024
f7f7e1a
Alin's comment on users vs applications
oggy-dfin Dec 23, 2024
6b0944d
Improve description of asynchronous calls
oggy-dfin Jan 28, 2025
922e0c2
Merge branch 'master' into oggy/best-effort-responses
oggy-dfin Jan 29, 2025
9f30199
Merge branch 'master' into oggy/best-effort-responses
oggy-dfin Jan 31, 2025
d6ef0c0
Revamp the docs on inter-canister calls in Rust
oggy-dfin Jan 31, 2025
2f1fc25
Polish the Rust inter-canister calls page a bit
oggy-dfin Jan 31, 2025
4425de4
Use more positive language on the call properties
oggy-dfin Feb 3, 2025
03b0f8f
fix CSP
jessiemongeon1 Feb 3, 2025
56f84fe
Fix typo
oggy-dfin Feb 4, 2025
856cf74
Panic instead of unreachable on ICP ledger errors
oggy-dfin Feb 4, 2025
349a221
Update docs/developer-docs/backend/rust/intercanister.mdx
oggy-dfin Feb 4, 2025
6971edd
Update docs/developer-docs/backend/rust/intercanister.mdx
oggy-dfin Feb 4, 2025
17c59d3
Update intercanister.mdx
oggy-dfin Feb 4, 2025
f18c6fb
Revamp the inter-canister messaging tutorial completely
oggy-dfin Feb 7, 2025
022c1e2
Add the cycles example
oggy-dfin Feb 10, 2025
0d7cd05
Touch up some text
oggy-dfin Feb 10, 2025
8a0f049
Start the tutorial with a very basic example.
oggy-dfin Feb 10, 2025
5e40227
Move to the new proposed CDK call error API and restructure again
oggy-dfin Feb 12, 2025
419649a
Fix the page (stop from crashing)
oggy-dfin Feb 12, 2025
d18f034
Add summaries in info boxes
oggy-dfin Feb 12, 2025
f59bb96
Update ICRC-1 example and the inter-cansiter calls explanation
oggy-dfin Feb 13, 2025
6ee732b
Fix the ICRC1 example
oggy-dfin Feb 13, 2025
069994f
Consistent naming (use (un)bounded-wait calls)
oggy-dfin Feb 13, 2025
9adbd3b
Update docs/developer-docs/backend/rust/intercanister.mdx
oggy-dfin Feb 17, 2025
dd3c9b8
Update docs/developer-docs/smart-contracts/advanced-features/async-co…
oggy-dfin Feb 17, 2025
f337117
Improve wording
oggy-dfin Feb 17, 2025
1165a6f
Touch up the documentation
oggy-dfin Feb 17, 2025
b437d6d
Update docs/developer-docs/smart-contracts/advanced-features/async-co…
oggy-dfin Feb 17, 2025
8cc7ca0
Apply suggestions from code review
oggy-dfin Feb 19, 2025
a063097
Apply suggestions from code review
oggy-dfin Feb 19, 2025
3998d1a
Some minor language/keyword changes
oggy-dfin Feb 19, 2025
66f2bf6
Apply suggestions from code review
oggy-dfin Feb 20, 2025
22db96a
Update code examples to the new CDK
oggy-dfin Feb 20, 2025
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
18 changes: 18 additions & 0 deletions docs/developer-docs/backend/rust/counter.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ fn increment() {
COUNTER.with(|counter| *counter.borrow_mut() += 1 as u32);
}

#[update]
fn get_and_set(n: Nat) -> Nat {
COUNTER.with(|counter| {
let old = counter.borrow().clone();
*counter.borrow_mut() = n;
old
})
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -97,6 +106,14 @@ mod tests {
assert_eq!(get(), Nat::from(i as u32));
}
}

#[test]
fn test_get_and_set() {
let old = get_and_set(Nat::from(1 as u32));
let new = get();
assert_eq!(old, Nat::from(0 as u32));
assert_eq!(new, Nat::from(1 as u32));
}
}
```

Expand All @@ -113,6 +130,7 @@ service : {
    "increment": () -> ();
    "get": () -> (nat) query;
    "set": (nat) -> ();
    "get_and_set": (nat) -> (nat);
}
```

Expand Down
410 changes: 302 additions & 108 deletions docs/developer-docs/backend/rust/intercanister.mdx

Large diffs are not rendered by default.

101 changes: 93 additions & 8 deletions docs/developer-docs/defi/tokens/ledger/usage/icp_ledger_usage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,23 @@ To interact with the ICRC-1 endpoints of the ICP ledger, learn more about [inter

## Interacting with ICP from a canister (inter-canister calls via `ic-cdk`)

View the [inter-canister call documentation] (/docs/developer-docs/backend/rust/intercanister) to see how you can call one canister from within another.
View the [inter-canister call documentation](/docs/current/developer-docs/backend/rust/intercanister) to see how you can call one canister from within another.

Here is an example of how to fetch the token name from the ICP ledger using Rust and the `ic-cdk` [library](https://github.com/dfinity/cdk-rs) from within a canister:

```
```rust
// You will need the canister ID of the ICP ledger: `ryjl3-tyaaa-aaaaa-aaaba-cai`.

let ledger_id = Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap();

// The request object of the `icrc1_name` endpoint is empty.

    let req = ();
    let (res,): (String,) =
        ic_cdk::call(ledger_id, "icrc1_name", (req,))
            .await.unwrap();
let req = ();
// Since this is a query, use a bounded wait call
let res: String = ic_cdk::call::Call::bounded_wait(ledger_id, "icrc1_name")
.with_args(req)
.await
// You should add proper error handling here to avoid panicking.
.unwrap();
```

You can find all available methods listed within the ICP ledger canister's Candid file or view the [mainnet ICP ledger canister on the dashboard](https://dashboard.internetcomputer.org/canister/ryjl3-tyaaa-aaaaa-aaaba-cai).
Expand All @@ -95,6 +97,89 @@ icrc-ledger-types = "0.1.1"

[View the documentation for this crate](https://docs.rs/icrc-ledger-types/0.1.1/icrc_ledger_types/).

### Sending ICP

The recommended way to send ICP is using the ledger's ICRC-1 interface.

```rust
use candid::Principal;
use ic_cdk::call::{Call, CallErrorExt};
use icrc_ledger_types::icrc1::account::{Account, Subaccount};
use icrc_ledger_types::icrc1::transfer::{BlockIndex, Memo, NumTokens, TransferArg, TransferError};

/// Transfers some ICP to the specified account.
pub async fn icp_transfer(
from_subaccount: Option<Subaccount>,
to: Account,
memo: Option<Vec<u8>>,
amount: NumTokens,
) -> Result<(), String> {
// The ID of the ledger canister on the IC mainnet.
const ICP_LEDGER_CANISTER_ID: &str = "ryjl3-tyaaa-aaaaa-aaaba-cai";
let icp_ledger = Principal::from_text(ICP_LEDGER_CANISTER_ID).unwrap();
let args = TransferArg {
// A "memo" is an arbitrary blob that has no meaning to the ledger, but can be used by
// the sender or receiver to attach additional information to the transaction.
memo: memo.map(|m| Memo::from(m)),
to,
amount,
// The ledger supports subaccounts. You can pick the subaccount of the caller canister's
// account to use for transferring the ICP. If you don't specify a subaccount, the default
// subaccount of the caller's account is used.
from_subaccount,
// The ICP ledger canister charges a fee for transfers, which is deducted from the
// sender's account. The fee is fixed to 10_000 e8s (0.0001 ICP). You can specify it here,
// to ensure that it hasn't changed, or leave it as None to use the current fee.
fee: Some(NumTokens::from(10_000_u32)),
// The created_at_time is used for deduplication. Not set in this example since it uses
// unbounded-wait calls. You should, however, set it if you opt to use bounded-wait
// calls, or if you use ingress messages, or if you are worried about bugs in the ICP
// ledger.
created_at_time: None,
};

// The unbounded-wait call here assumes that you trust the ICP ledger, in particular that it
// won't spin forever before producing a response.
match Call::unbounded_wait(icp_ledger, "icrc1_transfer")
.with_arg(&args)
.await
{
// The transfer call succeeded
Ok(res) => match res.candid::<Result<BlockIndex, TransferError>>() {
Ok(Ok(_i)) => Ok(()),
// The ledger canister returned an error, for example because the caller's balance was
// too low.
// The transfer didn't happen. Report an error back to the user.
// Look up the TransferError type in icrc_ledger_types for more details.
Ok(Err(e)) => Err(format!("Ledger returned an error: {:?}", e)),
Err(e) => Err(format!(
"Should not happen. Error decoding ledger response: {:?}",
e
)),
},
// An unclean reject signals that something went wrong with the call, but the system isn't
// sure whether the call was executed. Since this was an unbounded-wait call, this
// happens either because the ledger explicitly rejected the call, or because it panicked
// while processing our request.
// The ICP ledger doesn't explicitly reject calls. When using the icrc1 interface, it's also
// not intended to panic, but it reports errors at the "user-level", encoded in Candid.
// Here, the assumption is that it doesn't panic. However, if you don't want to make that
// assumption, you can add your own error handling of that case here.
// If you choose to use bounded-wait calls instead of unbounded-wait ones like this example,
// an unclean reject can also happen in case of a timeout. You can follow the ICRC-1 example
// to see how to handle this case.
Err(e) if !e.is_clean_reject() => Err(format!(
"Should not happen; error calling ledger canister: {:?}",
e
)),
// A clean reject means that the system can guarantee that the call wasn't executed at all
// (not even partially). It's always safe to assume that the transfer didn't happen
Err(e) => Err(format!("Error calling ledger canister: {:?}", e)),
}
}
```


### Receiving ICP

If you want a canister to receive payment in ICP, you need to make sure that the canister knows about the payment because a transfer only involves the sender and the ledger canister.
Expand Down Expand Up @@ -133,4 +218,4 @@ In this pattern, the ledger itself notifies the receiver. Thereby, the receiver
    "ICP Ledger" --> Sender: blockNumber
    Sender -> "ICP Ledger": notify(blockNumber, receiver)
    "ICP Ledger" -> "Receiver": transaction_notification(details)
```
```
141 changes: 128 additions & 13 deletions docs/developer-docs/defi/tokens/ledger/usage/icrc1_ledger_usage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,136 @@ View the [`dfx canister call` documentation](/docs/current/developer-docs/develo

## Interacting with an ICRC-1 ledger from another canister (inter-canister calls via `ic-cdk`)

View the [inter-canister call documentation] (/docs/developer-docs/backend/rust/intercanister) to see how you can call one canister from within another.

Here is an example of how to fetch the token name from the ICP ledger using Rust and the `ic-cdk` [library](https://github.com/dfinity/cdk-rs) from within a canister:

When calling into arbitrary ICRC-1 ledgers, we recommend you use [bounded wait (aka best-effort response) calls](/docs/current/developer-docs/smart-contracts/advanced-features/async-code). These calls ensure that your canister does not get stuck waiting for a response from the ledger.

Here sample code to fetch the transfer fee from an ICRC-1 ledger using Rust and the `ic-cdk` [library](https://github.com/dfinity/cdk-rs) from within a canister. The example includes retry logic to handle errors when possible.

```rust
pub enum GetFeeError {
/// The ledger didn't implement the ICRC-1 API correctly, e.g., returning an invalid response.
Icrc1ApiViolation(String),
/// A CDK CallError that we cannot recover from synchronously.
FatalCallError(CallFailed),
}

/// Obtain the fee that the ledger canister charges for a transfer.
/// This functiuon will keep retrying to fetch the fees for as long possible, and for as long as the
/// `should_retry` predicate returns true. Note that using a predicate that just always returns
/// `true` can keep your canister in a retry loop, and potentially unable to upgrade. The
/// recommended way is to set a limit on the number of retries, use a timeout, or abort when the
/// caller canister enters the stopping state.
pub async fn icrc1_get_fee<P>(ledger: Principal, should_retry: &P) -> Result<NumTokens, GetFeeError>
where
P: Fn() -> bool,
{
loop {
match Call::bounded_wait(ledger, "icrc1_fee").await {
Ok(res) => match res.candid() {
Ok(fee) => return Ok(fee),
Err(msg) => {
return Err(GetFeeError::Icrc1ApiViolation(format!(
"Unable to decode the fee: {:?}; failed with error {:?}",
res, msg
)))
}
},
// The system rejected our call, but it is possible to retry immediately.
// Since obtaining the fees is idempotent, it's always safe to retry.
Err(err) if err.is_immediately_retryable() && should_retry() => continue,
// The system rejected our call, but it is not possible to retry immediately.
Err(err) => return Err(GetFeeError::FatalCallError(err)),
}
}
}
```
// You will need the canister ID of the ICRC ledger.

let ledger_id = Principal::from_text("ss2fx-dyaaa-aaaar-qacoq-cai").unwrap();

// The request object of the `icrc1_name` endpoint is empty.

    let req = ();
    let (res,): (String,) =
        ic_cdk::call(ledger_id, "icrc1_name", (req,))
            .await.unwrap();
Here's sample code for transferring ICRC-1 ledger tokens using bounded wait response calls. It handles the unknown state case by using the [transaction deduplication feature](https://internetcomputer.org/docs/current/references/icrc1-standard#transaction-deduplication-) of ICRC-1 ledgers. However, if the transaction keeps failing beyond the deduplication window, the transaction state will be unknown and you will need to perform manual recovery.

```rust
pub enum TransferErrorCause {
Icrc1ApiViolation(String),
FatalCallError(CallFailed),
LedgerError(TransferError),
}

pub enum Icrc1TransferError {
// The transfer didn't happen.
TransferFailed(TransferErrorCause),
// The transfer may have happened.
UnknownState(TransferErrorCause),
}

/// Transfer the tokens on the specified ledger. The caller must ensure that:
/// 1. The `created_at` time of the `TransferArg` is set.
/// 2. The transaction described by the `TransferArg` has not yet been executed by the ledger.
/// Otherwise, the function may return `Ok` even if the transfer didn't happen.
pub async fn icrc1_transfer<P>(
ledger: Principal,
arg: TransferArg,
should_retry: &P,
) -> Result<BlockIndex, Icrc1TransferError>
where
P: Fn() -> bool,
{
assert!(
arg.created_at_time.is_some(),
"The created_at_time must be set in the TransferArg"
);

let mut no_unknowns = true;
loop {
match Call::bounded_wait(ledger, "icrc1_transfer")
.with_arg(&arg)
// Use the longest timeout supported by the system, as we'll retry later anyways.
.change_timeout(u32::MAX)
.await
{
Ok(res) => match res.candid() {
Ok(Ok(i)) => return Ok(i),
Ok(Err(e)) => match e {
// Since the assumption is that the transaction didn't happen before the call,
// treat a duplicate error as a success.
TransferError::Duplicate { duplicate_of } => return Ok(duplicate_of),
e if no_unknowns => {
return Err(Icrc1TransferError::TransferFailed(
TransferErrorCause::LedgerError(e),
))
}
// Unknown state. To recover, you can query the ledger blocks to see if the
// transaction was executed.
e => {
return Err(Icrc1TransferError::UnknownState(
TransferErrorCause::LedgerError(e),
))
}
},
Err(e) => {
return Err(Icrc1TransferError::TransferFailed(
TransferErrorCause::Icrc1ApiViolation(e.to_string()),
))
}
},
// Since the ICRC1 transfer is idempotent, it's always safe to retry.
Err(e) if e.is_immediately_retryable() && should_retry() => {
// If the reject wasn't clean, mark the state as unknown.
if !e.is_clean_reject() {
no_unknowns = false;
}
continue;
}
Err(e) if e.is_clean_reject() && no_unknowns => {
return Err(Icrc1TransferError::TransferFailed(
TransferErrorCause::FatalCallError(e),
))
}
Err(e) => {
return Err(Icrc1TransferError::UnknownState(
TransferErrorCause::FatalCallError(e),
))
}
}
}
}
```

You can find all available methods for your ICRC-1 ledger within the ICRC-1 ledger canister's Candid file or, if your ICRC-1 ledger has been deployed to the mainnet, view your ICRC-1 ledger canister [on the dashboard](https://dashboard.internetcomputer.org/canisters). An example of an ICRC-1 ledger deployed on the mainnet that you can reference is the [ckETH ledger canister](https://dashboard.internetcomputer.org/canister/ss2fx-dyaaa-aaaar-qacoq-cai).
Expand Down
Loading