diff --git a/docs/concepts/cycles.md b/docs/concepts/cycles.md index 885fbf1..77a1e73 100644 --- a/docs/concepts/cycles.md +++ b/docs/concepts/cycles.md @@ -94,7 +94,7 @@ Every state-changing operation (each block created) costs 100M cycles as a fee. The cycles ledger does not support calling arbitrary canisters with cycles attached, because open call contexts can cause the ledger to become stuck. Two patterns address this: - **Top up the target canister first**: if you control the canister, transfer cycles to it using `withdraw` or `icp canister top-up`, then let the canister attach cycles internally from its own balance. This is the preferred pattern for canisters you deploy and control. -- **Proxy canister**: if you need to call a canister method with cycles attached from the CLI or an external agent, deploy a proxy canister using the [`proxy` template](https://github.com/dfinity/icp-cli-templates/tree/main/proxy) and route the call through it. See [Calling canisters that require cycles](../guides/canister-management/cycles-management.md#calling-canisters-that-require-cycles) for the how-to. +- **Proxy canister**: if you need to call a canister method with cycles attached from the CLI or an external agent, deploy a proxy canister using the [`proxy` template](https://github.com/dfinity/icp-cli-templates/tree/main/proxy) and route the call through it. See [Calls with attached cycles](../guides/canister-calls/inter-canister-calls.md#calls-with-attached-cycles) for the how-to. ## Developer responsibility @@ -118,7 +118,7 @@ The tradeoff is that developers must forecast and fund usage upfront rather than ## Related - [Cycles Management](../guides/canister-management/cycles-management.md): how to check balances, top up canisters, and set freezing thresholds -- [Calling canisters that require cycles](../guides/canister-management/cycles-management.md#calling-canisters-that-require-cycles): proxy canister pattern for attaching cycles from the CLI +- [Calls with attached cycles](../guides/canister-calls/inter-canister-calls.md#calls-with-attached-cycles): attach cycles to an inter-canister call and use the proxy canister pattern for the CLI - [Cycles ledger reference](../references/system-canisters.md#cycles-ledger): canister IDs, interface specification, and CMC integration - [Cycles Costs Reference](../references/cycles-costs.md): exact cost tables for all operations - [Canisters](./canisters.md): canisters as the paying entity for compute and storage diff --git a/docs/guides/canister-calls/inter-canister-calls.mdx b/docs/guides/canister-calls/inter-canister-calls.mdx index 575d4a7..d13ea2a 100644 --- a/docs/guides/canister-calls/inter-canister-calls.mdx +++ b/docs/guides/canister-calls/inter-canister-calls.mdx @@ -264,6 +264,138 @@ Call::bounded_wait(callee, "method") **Calling third-party canisters:** When calling canisters outside your control, always use bounded wait and design for uncertainty. The callee may be upgraded, become unresponsive, or behave unexpectedly. Use idempotent operations where possible and provide a way to query the outcome of a call separately, so your canister can recover from ambiguous responses. +## Calls with attached cycles + +Some canister methods require cycles to be attached to the incoming call as a per-request fee. The [exchange rate canister](../chain-fusion/exchange-rates.mdx) is a common example: each request costs one XDR's worth of cycles, which must arrive with the call. + +This is distinct from a canister's ongoing operational balance. The [cycles ledger](../../concepts/cycles.md#cycles-ledger) cannot forward calls with cycles attached, so you must attach them explicitly at the call site. + +### Sending cycles + + + + +Use the `(with cycles = amount)` parenthetical on any `await` expression: + +```motoko +import Cycles "mo:core/Cycles"; + +persistent actor { + let target = actor ("rrkah-fqaaa-aaaaa-aaaaq-cai") : actor { + someMethod : () -> async (); + }; + + public func callWithCycles() : async () { + await (with cycles = 500_000_000) target.someMethod(); + }; +} +``` + + + + +Chain `.with_cycles()` on the `Call` builder before awaiting: + +```rust +use candid::Principal; +use ic_cdk::call::Call; +use ic_cdk::update; + +#[update] +async fn call_with_cycles() { + Call::unbounded_wait( + Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap(), + "someMethod", + ) + .with_cycles(500_000_000u128) + .await + .expect("call failed"); +} +``` + + + + +The cycles come from the calling canister's own balance, not the cycles ledger. Top up the calling canister with `icp canister top-up` before using this pattern. + +### Charging a cycle fee + +A canister that charges per-call defines a required fee, rejects calls that don't meet it, and accepts exactly that amount before doing its work. Any cycles above the fee are returned to the caller automatically: + + + + +```motoko +import Cycles "mo:core/Cycles"; +import Runtime "mo:core/Runtime"; + +persistent actor { + let fee : Nat = 100_000_000; + + public func compute() : async () { + let available = Cycles.available(); + if (available < fee) { + Runtime.trap("Insufficient cycles: requires " # debug_show fee) + }; + ignore Cycles.accept(fee); // accept exactly the fee; excess is returned automatically + // ... do work here + }; +} +``` + + + + +```rust +use ic_cdk::update; + +const FEE: u128 = 100_000_000; + +#[update] +fn compute() { + let available = ic_cdk::api::msg_cycles_available(); + if available < FEE { + ic_cdk::trap("Insufficient cycles"); + } + ic_cdk::api::msg_cycles_accept(FEE); // accept exactly the fee; excess is returned automatically + // ... do work here +} +``` + + + + +### Attaching cycles from the CLI + +The CLI cannot attach cycles directly to a canister call. Two approaches address this: + +**Top up the target canister first** (preferred when you control it): transfer cycles to the target using `icp canister top-up`, then call the method normally. The canister uses its own balance when the method runs. + +```bash +# Transfer 1T cycles to the target canister +icp canister top-up rrkah-fqaaa-aaaaa-aaaaq-cai --amount 1T -n ic + +# Then call the method as normal +icp canister call rrkah-fqaaa-aaaaa-aaaaq-cai someMethod '()' -n ic +``` + +**Proxy canister** (required when you cannot top up the target, or need cycles attached to each individual call): deploy a proxy canister that forwards calls with cycles attached. + +```bash +# Deploy the proxy canister using the provided template +icp new proxy --subfolder proxy +cd proxy +icp deploy -e ic + +# Get the proxy canister ID +export PROXY_ID=$(icp canister status -e ic --id-only proxy) + +# Call any canister through the proxy with cycles attached +icp canister call --proxy "$PROXY_ID" rrkah-fqaaa-aaaaa-aaaaq-cai someMethod '()' -n ic +``` + +The proxy canister template is available at [icp-cli-templates/proxy](https://github.com/dfinity/icp-cli-templates/tree/main/proxy). It deploys the [proxy-canister](https://github.com/dfinity/proxy-canister), which is automatically provisioned on local networks but must be deployed manually on mainnet. + ## Pub/sub pattern The publisher/subscriber pattern is a natural fit for inter-canister communication on ICP. A publisher canister maintains a list of subscribers and notifies them when events occur. Unlike traditional pub/sub systems, ICP's reliable message delivery means subscribers are guaranteed to receive notifications (as long as both canisters have sufficient cycles). @@ -400,7 +532,8 @@ Calls between canisters on the same subnet complete within a single round. Cross - [Parallel inter-canister calls](parallel-inter-canister-calls.md): make multiple calls concurrently and use composite queries for efficient read patterns - [Candid](candid.md): define the interface your canister exposes for inter-canister calls +- [Cycles Management](../canister-management/cycles-management.md): acquire cycles, monitor balances, and set freezing thresholds - [Certified Variables](../backends/certified-variables.md): make query responses verifiable without update call overhead - [Inter-Canister Call Security](../security/inter-canister-calls.md): reentrancy guards, async safety patterns, and trust considerations -{/* Upstream: informed by dfinity/portal docs/building-apps/interact-with-canisters/advanced-calls.mdx, docs/building-apps/developer-tools/cdks/rust/intercanister.mdx, multi-canister icskill, icp-cli icskill, dfinity/icp-cli docs/concepts/canister-discovery.md, dfinity/examples motoko/pub-sub, and caffeinelabs/motoko doc/md/fundamentals/2-actors/1-actors-async.md (timeout parenthetical syntax, try/finally cleanup pattern) */} +{/* Upstream: informed by dfinity/portal docs/building-apps/interact-with-canisters/advanced-calls.mdx, docs/building-apps/developer-tools/cdks/rust/intercanister.mdx, multi-canister icskill, icp-cli icskill, dfinity/icp-cli docs/concepts/canister-discovery.md, dfinity/examples motoko/pub-sub, caffeinelabs/motoko doc/md/fundamentals/2-actors/1-actors-async.md (timeout parenthetical syntax, try/finally cleanup pattern); cycles-attachment section moved from docs/guides/canister-management/cycles-management.mdx, proxy template from dfinity/icp-cli-templates */} diff --git a/docs/guides/canister-management/cycles-management.mdx b/docs/guides/canister-management/cycles-management.mdx index 129b20c..758cd85 100644 --- a/docs/guides/canister-management/cycles-management.mdx +++ b/docs/guides/canister-management/cycles-management.mdx @@ -140,48 +140,7 @@ icp cycles mint --icp 1.0 -n ic icp canister top-up backend --amount 1T -e ic ``` -### Accepting cycles in your canister - -Canisters can also accept cycles sent with an inter-canister call. This pattern is used for "tip jar" flows and payment routing: - - - - -```motoko -import Cycles "mo:core/Cycles"; -import Runtime "mo:core/Runtime"; - -persistent actor { - public func deposit() : async Nat { - let available = Cycles.available(); - if (available == 0) { - Runtime.trap("No cycles sent with this call") - }; - Cycles.accept(available) - }; -} -``` - - - - -```rust -use ic_cdk::update; -use candid::Nat; - -#[update] -fn deposit() -> Nat { - let available = ic_cdk::api::msg_cycles_available(); - if available == 0 { - ic_cdk::trap("No cycles sent with this call"); - } - let accepted = ic_cdk::api::msg_cycles_accept(available); - Nat::from(accepted) -} -``` - - - +For accepting cycles sent with an inter-canister call (the per-request payment pattern used by canisters like the exchange rate canister), see [Calls with attached cycles](../canister-calls/inter-canister-calls.md#calls-with-attached-cycles). ## Freezing threshold @@ -313,87 +272,6 @@ async fn top_up_canister(canister_id: Principal, amount: u128) { -## Calling canisters that require cycles - -Some canister methods expect cycles to be attached to the call itself. The [cycles ledger](../../concepts/cycles.md#cycles-ledger) cannot forward calls with cycles attached, so you need a different approach depending on whether you are calling from canister code or from the CLI. - -### From canister code - -Attach cycles to an inter-canister call using `Cycles.add` (Motoko) or `msg_cycles_add` (Rust). The called canister receives the cycles as part of the message context and accepts them with `Cycles.accept`: - - - - -```motoko -import Cycles "mo:core/Cycles"; - -persistent actor { - let target = actor ("rrkah-fqaaa-aaaaa-aaaaq-cai") : actor { - someMethod : () -> async (); - }; - - public func callWithCycles() : async () { - Cycles.add(500_000_000); - await target.someMethod(); - }; -} -``` - - - - -```rust -use ic_cdk::update; - -#[update] -async fn call_with_cycles() { - ic_cdk::api::call::msg_cycles_add(500_000_000u64); - let _: () = ic_cdk::call( - candid::Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap(), - "someMethod", - (), - ) - .await - .expect("call failed"); -} -``` - - - - -The calling canister uses cycles from its own balance, not from the cycles ledger. Top up the calling canister first using `icp canister top-up` or `icp cycles mint`. - -### From the CLI or an agent - -The CLI cannot attach cycles directly to a canister call. Two approaches address this: - -**Top up the target canister first** (preferred when you control it): transfer cycles to the target canister using `icp canister top-up`, then call the method normally. The canister uses its own balance when the method runs. - -```bash -# Transfer 1T cycles to the target canister -icp canister top-up rrkah-fqaaa-aaaaa-aaaaq-cai --amount 1T -n ic - -# Then call the method as normal -icp canister call rrkah-fqaaa-aaaaa-aaaaq-cai someMethod '()' -n ic -``` - -**Proxy canister** (required when you need to attach cycles to a call and don't control the target): deploy a proxy canister that can forward calls with cycles attached. - -```bash -# Deploy the proxy canister using the provided template -icp new proxy --template proxy -cd proxy -icp deploy -e ic - -# Get the proxy canister ID -export PROXY_ID=$(icp canister status -e ic --id-only proxy) - -# Call any canister through the proxy with cycles attached -icp canister call --proxy "$PROXY_ID" rrkah-fqaaa-aaaaa-aaaaq-cai someMethod '()' -n ic -``` - -The proxy canister template is available at [icp-cli-templates/proxy](https://github.com/dfinity/icp-cli-templates/tree/main/proxy). It deploys the [proxy-canister](https://github.com/dfinity/proxy-canister), which is automatically provisioned on local networks but must be deployed manually on mainnet. - ## Multi-environment deployment For production, use separate environments for staging and production to avoid accidentally affecting live canisters. Configure environments in `icp.yaml`: @@ -495,6 +373,7 @@ icp canister top-up backend --amount 1T -n ic - [Cycles costs reference](../../references/cycles-costs.md#cost-table): Exact cost tables per operation - [Cycles](../../concepts/cycles.md): Why canisters pay for execution and how the cycles ledger works - [Cycles ledger reference](../../references/system-canisters.md#cycles-ledger): Canister IDs and interface specification +- [Calls with attached cycles](../canister-calls/inter-canister-calls.md#calls-with-attached-cycles): attach cycles to an inter-canister call and accept them in the callee - [Reproducible builds](reproducible-builds.md): Verify your WASM is trustworthy before deploying - [icp-cli docs](https://cli.internetcomputer.org/0.2/reference/cli#icp-cycles): Full command reference for `icp cycles` and `icp canister top-up` diff --git a/docs/guides/chain-fusion/exchange-rates.mdx b/docs/guides/chain-fusion/exchange-rates.mdx index a12fe55..4a86040 100644 --- a/docs/guides/chain-fusion/exchange-rates.mdx +++ b/docs/guides/chain-fusion/exchange-rates.mdx @@ -240,7 +240,7 @@ The most important errors to handle explicitly: ## Testing from the CLI -The XRC requires cycles attached to the call, so you cannot call it directly from the CLI on mainnet. To test the integration from the terminal, use the [proxy canister pattern](../canister-management/cycles-management.md#calling-canisters-that-require-cycles): deploy a proxy canister that forwards the call with cycles attached. +The XRC requires cycles attached to the call, so you cannot call it directly from the CLI on mainnet. To test the integration from the terminal, use the [proxy canister pattern](../canister-calls/inter-canister-calls.md#attaching-cycles-from-the-cli): deploy a proxy canister that forwards the call with cycles attached. On a local replica, note that the XRC fetches from live external exchanges via HTTPS outcalls, so local testing requires a connection to the internet and a subnet configured as type `system`. @@ -248,7 +248,7 @@ On a local replica, note that the XRC fetches from live external exchanges via H - [Exchange rate canister concept](../../concepts/chain-fusion/exchange-rate-canister.md): how median aggregation and rate derivation work - [Exchange rate canister reference](../../references/protocol-canisters.md#exchange-rate-canister-xrc): full Candid interface, all error types, and data sources -- [Calling canisters that require cycles](../canister-management/cycles-management.md#calling-canisters-that-require-cycles): proxy canister pattern for CLI testing +- [Calls with attached cycles](../canister-calls/inter-canister-calls.md#calls-with-attached-cycles): attach cycles to an outgoing call and use the proxy canister pattern for CLI testing - [HTTPS outcalls](../../concepts/https-outcalls.md): how the XRC fetches external price data - [Full Rust example](https://github.com/dfinity/examples/tree/master/rust/exchange-rates): complete Rust project with build configuration