Skip to content

Commit 43c027e

Browse files
authored
refactor(guides): move cycle-attachment content to inter-canister-calls (#243)
## Summary - Moves the `## Calling canisters that require cycles` section out of the cycles-management guide and into inter-canister-calls as a new `## Calls with attached cycles` section. Attaching cycles to a call is a call mechanic, not an operational management concern. - Also moves the `### Accepting cycles in your canister` subsection (which was under "Topping up canisters") to the same new section, since it is the callee-side of the same mechanic. - Modernizes the moved Motoko examples from the deprecated `Cycles.add<system>()` imperative style to the current `(with cycles = amount)` parenthetical syntax, and updates the Rust examples from the low-level `msg_cycles_add` call to `.with_cycles()` on the `Call` builder — consistent with how the rest of `inter-canister-calls.mdx` is written. - Extends the callee example to require a specific fee, trap if the attached amount falls short, and accept exactly that fee — excess is returned to the caller automatically. The original "accept all" pattern was a tip-jar, not representative of how real fee-charging canisters (like the XRC) work. - Renames the callee subsection to `### Charging a cycle fee` and the example function to `compute()` returning `()`, making clear that cycles are payment for work rather than the subject of the function. - Fixes `icp new proxy --template proxy` → `icp new proxy --subfolder proxy` (no `--template` flag exists; verified against `.sources/icp-cli/docs/reference/cli.md`). The bug was present in the original section and carried over during the move. - Leaves a precise cross-reference in `cycles-management.md` pointing to the new location. - Updates four inbound links: `concepts/cycles.md` (×2), `guides/chain-fusion/exchange-rates.mdx` (×2). ## Sync recommendation `hand-written` Closes #231
1 parent 620d9f7 commit 43c027e

4 files changed

Lines changed: 140 additions & 128 deletions

File tree

‎docs/concepts/cycles.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ Every state-changing operation (each block created) costs 100M cycles as a fee.
9494
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:
9595

9696
- **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.
97-
- **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.
97+
- **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.
9898

9999
## Developer responsibility
100100

@@ -118,7 +118,7 @@ The tradeoff is that developers must forecast and fund usage upfront rather than
118118
## Related
119119

120120
- [Cycles Management](../guides/canister-management/cycles-management.md): how to check balances, top up canisters, and set freezing thresholds
121-
- [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
121+
- [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
122122
- [Cycles ledger reference](../references/system-canisters.md#cycles-ledger): canister IDs, interface specification, and CMC integration
123123
- [Cycles Costs Reference](../references/cycles-costs.md): exact cost tables for all operations
124124
- [Canisters](./canisters.md): canisters as the paying entity for compute and storage

‎docs/guides/canister-calls/inter-canister-calls.mdx‎

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,138 @@ Call::bounded_wait(callee, "method")
264264

265265
**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.
266266

267+
## Calls with attached cycles
268+
269+
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.
270+
271+
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.
272+
273+
### Sending cycles
274+
275+
<Tabs syncKey="lang">
276+
<TabItem label="Motoko">
277+
278+
Use the `(with cycles = amount)` parenthetical on any `await` expression:
279+
280+
```motoko
281+
import Cycles "mo:core/Cycles";
282+
283+
persistent actor {
284+
let target = actor ("rrkah-fqaaa-aaaaa-aaaaq-cai") : actor {
285+
someMethod : () -> async ();
286+
};
287+
288+
public func callWithCycles() : async () {
289+
await (with cycles = 500_000_000) target.someMethod();
290+
};
291+
}
292+
```
293+
294+
</TabItem>
295+
<TabItem label="Rust">
296+
297+
Chain `.with_cycles()` on the `Call` builder before awaiting:
298+
299+
```rust
300+
use candid::Principal;
301+
use ic_cdk::call::Call;
302+
use ic_cdk::update;
303+
304+
#[update]
305+
async fn call_with_cycles() {
306+
Call::unbounded_wait(
307+
Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap(),
308+
"someMethod",
309+
)
310+
.with_cycles(500_000_000u128)
311+
.await
312+
.expect("call failed");
313+
}
314+
```
315+
316+
</TabItem>
317+
</Tabs>
318+
319+
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.
320+
321+
### Charging a cycle fee
322+
323+
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:
324+
325+
<Tabs syncKey="lang">
326+
<TabItem label="Motoko">
327+
328+
```motoko
329+
import Cycles "mo:core/Cycles";
330+
import Runtime "mo:core/Runtime";
331+
332+
persistent actor {
333+
let fee : Nat = 100_000_000;
334+
335+
public func compute() : async () {
336+
let available = Cycles.available();
337+
if (available < fee) {
338+
Runtime.trap("Insufficient cycles: requires " # debug_show fee)
339+
};
340+
ignore Cycles.accept<system>(fee); // accept exactly the fee; excess is returned automatically
341+
// ... do work here
342+
};
343+
}
344+
```
345+
346+
</TabItem>
347+
<TabItem label="Rust">
348+
349+
```rust
350+
use ic_cdk::update;
351+
352+
const FEE: u128 = 100_000_000;
353+
354+
#[update]
355+
fn compute() {
356+
let available = ic_cdk::api::msg_cycles_available();
357+
if available < FEE {
358+
ic_cdk::trap("Insufficient cycles");
359+
}
360+
ic_cdk::api::msg_cycles_accept(FEE); // accept exactly the fee; excess is returned automatically
361+
// ... do work here
362+
}
363+
```
364+
365+
</TabItem>
366+
</Tabs>
367+
368+
### Attaching cycles from the CLI
369+
370+
The CLI cannot attach cycles directly to a canister call. Two approaches address this:
371+
372+
**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.
373+
374+
```bash
375+
# Transfer 1T cycles to the target canister
376+
icp canister top-up rrkah-fqaaa-aaaaa-aaaaq-cai --amount 1T -n ic
377+
378+
# Then call the method as normal
379+
icp canister call rrkah-fqaaa-aaaaa-aaaaq-cai someMethod '()' -n ic
380+
```
381+
382+
**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.
383+
384+
```bash
385+
# Deploy the proxy canister using the provided template
386+
icp new proxy --subfolder proxy
387+
cd proxy
388+
icp deploy -e ic
389+
390+
# Get the proxy canister ID
391+
export PROXY_ID=$(icp canister status -e ic --id-only proxy)
392+
393+
# Call any canister through the proxy with cycles attached
394+
icp canister call --proxy "$PROXY_ID" rrkah-fqaaa-aaaaa-aaaaq-cai someMethod '()' -n ic
395+
```
396+
397+
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.
398+
267399
## Pub/sub pattern
268400

269401
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
400532

401533
- [Parallel inter-canister calls](parallel-inter-canister-calls.md): make multiple calls concurrently and use composite queries for efficient read patterns
402534
- [Candid](candid.md): define the interface your canister exposes for inter-canister calls
535+
- [Cycles Management](../canister-management/cycles-management.md): acquire cycles, monitor balances, and set freezing thresholds
403536
- [Certified Variables](../backends/certified-variables.md): make query responses verifiable without update call overhead
404537
- [Inter-Canister Call Security](../security/inter-canister-calls.md): reentrancy guards, async safety patterns, and trust considerations
405538

406-
{/* 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) */}
539+
{/* 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 */}

‎docs/guides/canister-management/cycles-management.mdx‎

Lines changed: 2 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -140,48 +140,7 @@ icp cycles mint --icp 1.0 -n ic
140140
icp canister top-up backend --amount 1T -e ic
141141
```
142142

143-
### Accepting cycles in your canister
144-
145-
Canisters can also accept cycles sent with an inter-canister call. This pattern is used for "tip jar" flows and payment routing:
146-
147-
<Tabs syncKey="lang">
148-
<TabItem label="Motoko">
149-
150-
```motoko
151-
import Cycles "mo:core/Cycles";
152-
import Runtime "mo:core/Runtime";
153-
154-
persistent actor {
155-
public func deposit() : async Nat {
156-
let available = Cycles.available();
157-
if (available == 0) {
158-
Runtime.trap("No cycles sent with this call")
159-
};
160-
Cycles.accept<system>(available)
161-
};
162-
}
163-
```
164-
165-
</TabItem>
166-
<TabItem label="Rust">
167-
168-
```rust
169-
use ic_cdk::update;
170-
use candid::Nat;
171-
172-
#[update]
173-
fn deposit() -> Nat {
174-
let available = ic_cdk::api::msg_cycles_available();
175-
if available == 0 {
176-
ic_cdk::trap("No cycles sent with this call");
177-
}
178-
let accepted = ic_cdk::api::msg_cycles_accept(available);
179-
Nat::from(accepted)
180-
}
181-
```
182-
183-
</TabItem>
184-
</Tabs>
143+
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).
185144

186145
## Freezing threshold
187146

@@ -313,87 +272,6 @@ async fn top_up_canister(canister_id: Principal, amount: u128) {
313272
</TabItem>
314273
</Tabs>
315274

316-
## Calling canisters that require cycles
317-
318-
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.
319-
320-
### From canister code
321-
322-
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`:
323-
324-
<Tabs syncKey="lang">
325-
<TabItem label="Motoko">
326-
327-
```motoko
328-
import Cycles "mo:core/Cycles";
329-
330-
persistent actor {
331-
let target = actor ("rrkah-fqaaa-aaaaa-aaaaq-cai") : actor {
332-
someMethod : () -> async ();
333-
};
334-
335-
public func callWithCycles() : async () {
336-
Cycles.add<system>(500_000_000);
337-
await target.someMethod();
338-
};
339-
}
340-
```
341-
342-
</TabItem>
343-
<TabItem label="Rust">
344-
345-
```rust
346-
use ic_cdk::update;
347-
348-
#[update]
349-
async fn call_with_cycles() {
350-
ic_cdk::api::call::msg_cycles_add(500_000_000u64);
351-
let _: () = ic_cdk::call(
352-
candid::Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap(),
353-
"someMethod",
354-
(),
355-
)
356-
.await
357-
.expect("call failed");
358-
}
359-
```
360-
361-
</TabItem>
362-
</Tabs>
363-
364-
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`.
365-
366-
### From the CLI or an agent
367-
368-
The CLI cannot attach cycles directly to a canister call. Two approaches address this:
369-
370-
**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.
371-
372-
```bash
373-
# Transfer 1T cycles to the target canister
374-
icp canister top-up rrkah-fqaaa-aaaaa-aaaaq-cai --amount 1T -n ic
375-
376-
# Then call the method as normal
377-
icp canister call rrkah-fqaaa-aaaaa-aaaaq-cai someMethod '()' -n ic
378-
```
379-
380-
**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.
381-
382-
```bash
383-
# Deploy the proxy canister using the provided template
384-
icp new proxy --template proxy
385-
cd proxy
386-
icp deploy -e ic
387-
388-
# Get the proxy canister ID
389-
export PROXY_ID=$(icp canister status -e ic --id-only proxy)
390-
391-
# Call any canister through the proxy with cycles attached
392-
icp canister call --proxy "$PROXY_ID" rrkah-fqaaa-aaaaa-aaaaq-cai someMethod '()' -n ic
393-
```
394-
395-
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.
396-
397275
## Multi-environment deployment
398276

399277
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
495373
- [Cycles costs reference](../../references/cycles-costs.md#cost-table): Exact cost tables per operation
496374
- [Cycles](../../concepts/cycles.md): Why canisters pay for execution and how the cycles ledger works
497375
- [Cycles ledger reference](../../references/system-canisters.md#cycles-ledger): Canister IDs and interface specification
376+
- [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
498377
- [Reproducible builds](reproducible-builds.md): Verify your WASM is trustworthy before deploying
499378
- [icp-cli docs](https://cli.internetcomputer.org/0.2/reference/cli#icp-cycles): Full command reference for `icp cycles` and `icp canister top-up`
500379

‎docs/guides/chain-fusion/exchange-rates.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,15 +240,15 @@ The most important errors to handle explicitly:
240240

241241
## Testing from the CLI
242242

243-
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.
243+
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.
244244

245245
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`.
246246

247247
## Next steps
248248

249249
- [Exchange rate canister concept](../../concepts/chain-fusion/exchange-rate-canister.md): how median aggregation and rate derivation work
250250
- [Exchange rate canister reference](../../references/protocol-canisters.md#exchange-rate-canister-xrc): full Candid interface, all error types, and data sources
251-
- [Calling canisters that require cycles](../canister-management/cycles-management.md#calling-canisters-that-require-cycles): proxy canister pattern for CLI testing
251+
- [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
252252
- [HTTPS outcalls](../../concepts/https-outcalls.md): how the XRC fetches external price data
253253
- [Full Rust example](https://github.com/dfinity/examples/tree/master/rust/exchange-rates): complete Rust project with build configuration
254254

0 commit comments

Comments
 (0)