From afb1aa9334c801fb81f3c5b9c2c46c0a314174a7 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Mon, 16 Mar 2026 17:49:33 +0100 Subject: [PATCH 1/7] docs: onchain calls guide --- docs/guides/canister-calls/onchain-calls.md | 212 ++++++++++++++++++-- 1 file changed, 195 insertions(+), 17 deletions(-) diff --git a/docs/guides/canister-calls/onchain-calls.md b/docs/guides/canister-calls/onchain-calls.md index 0716c6ab..9b79e3ac 100644 --- a/docs/guides/canister-calls/onchain-calls.md +++ b/docs/guides/canister-calls/onchain-calls.md @@ -6,20 +6,198 @@ sidebar: icskills: [multi-canister] --- -TODO: Write content for this page. - - -Call functions on other canisters from your canister code. Start with the query vs update decision: queries are fast (~200ms), free, and run on a single node but aren't replicated (caller must trust the node); updates go through consensus (~2s), cost cycles, and modify state reliably. Explain when to use each and the trust tradeoffs. Cover canister discovery (env vars, ic_env cookie), error handling (reject codes, trap handling), calling third-party canisters, and the pub/sub pattern. Show Rust (ic_cdk::call) and Motoko (async message sends) patterns for both query and update calls. Explain the 2MB response limit. Link to parallel-calls for composite queries (the advanced query pattern) and to certified-variables for making query responses verifiable. - - -- Portal: building-apps/integrations/advanced-calls.mdx, advanced/using-third-party-canisters.mdx, developer-tools/cdks/rust/intercanister.mdx -- icp-cli: concepts/canister-discovery.md -- icskills: multi-canister -- Examples: inter-canister-calls (Rust), pub-sub (Motoko), composite_query (both) - - -- concepts/canisters -- messaging model -- guides/canister-calls/candid -- interface definitions -- guides/canister-calls/parallel-calls -- concurrent calls and composite queries -- guides/backends/certified-variables -- making query responses verifiable -- guides/security/inter-canister-calls -- async safety +Canisters on the Internet Computer communicate by calling each other's functions. A caller canister sends a request message containing the method name, arguments, and optionally attached cycles. The callee executes the method and returns a response. If the callee cannot be reached or a resource limit is hit, the system produces a reject response instead. + +This guide covers making inter-canister calls in both Motoko and Rust, choosing between query and update calls, handling errors, and avoiding common pitfalls. For the messaging model behind these calls, see [Canisters](../../concepts/canisters.md). + +## Query vs update calls + +There are two types of canister methods, and the choice affects latency, cost, and trust: + +| | Query | Update | +|---|---|---| +| **Latency** | ~200ms | ~2s | +| **Cycle cost** | Free | Costs cycles | +| **Execution** | Single replica | Full consensus | +| **State changes** | Not persisted | Persisted | +| **Trust model** | Caller trusts one replica | Replicated and verifiable | + +**Use query calls** when reading data where the caller trusts the subnet (or will verify the response independently). Queries are fast and free, but the response comes from a single node and is not replicated. + +**Use update calls** when modifying state, transferring cycles, or when the caller needs a consensus-backed guarantee that the call was executed correctly. + +To make query responses verifiable without the cost of update calls, see [Certified Variables](../backends/certified-variables.md). For running multiple query calls in parallel, see [Parallel Calls and Composite Queries](parallel-calls.md). + +## Making calls in Motoko + +Import another canister by name (the name must match your `icp.yaml` configuration) and call its methods with `await`: + +```motoko +import Counter "canister:counter"; + +persistent actor { + public func getCount() : async Nat { + await Counter.get() + }; + + public func incrementAndGet() : async Nat { + await Counter.increment(); + await Counter.get() + }; +}; +``` + +### Error handling in Motoko + +Wrap inter-canister calls in `try`/`catch` to handle rejects: + +```motoko +import Counter "canister:counter"; +import Error "mo:base/Error"; +import Runtime "mo:base/Runtime"; + +persistent actor { + public shared ({ caller }) func safeIncrement() : async Result.Result { + let originalCaller = caller; + + try { + await Counter.increment(); + let count = await Counter.get(); + #ok(count) + } catch (e : Error.Error) { + #err("Counter call failed: " # Error.message(e)) + }; + }; +}; +``` + +Note that `caller` is captured before the first `await`. In Motoko, `public shared ({ caller })` binds the caller at method entry, so it is safe to use after `await`. However, binding it to a local variable makes the intent explicit. + +## Making calls in Rust + +The Rust CDK provides `Call::unbounded_wait` and `Call::bounded_wait` to call other canisters. Both return a builder that lets you attach arguments, cycles, and timeout settings. + +### Basic call + +```rust +use candid::{Nat, Principal}; +use ic_cdk::call::{Call, CallErrorExt}; +use ic_cdk_macros::update; + +#[update] +pub async fn call_get_and_set(counter: Principal, new_value: Nat) -> Nat { + Call::unbounded_wait(counter, "get_and_set") + .with_arg(&new_value) + .await + .expect("Failed to get the old value") + .candid::() + .expect("Candid decoding failed") +} +``` + +### Error handling in Rust + +The call returns a `Result` where the error type distinguishes **clean rejects** (the call definitively did not execute) from **non-clean rejects** (the outcome is unknown): + +```rust +use candid::Principal; +use ic_cdk::call::{Call, CallErrorExt}; +use ic_cdk_macros::update; + +#[update] +pub async fn call_increment(counter: Principal) -> Result<(), String> { + match Call::unbounded_wait(counter, "increment").await { + Ok(_) => Ok(()), + Err(e) if !e.is_clean_reject() => { + Err(format!("Non-clean reject: {:?}. Outcome unknown.", e)) + } + Err(e) => { + Err(format!("Clean reject: {:?}. Counter was not incremented.", e)) + } + } +} +``` + +The distinction matters for correctness: + +- **Clean reject** -- the callee never executed the method. Safe to retry. +- **Non-clean reject** -- the callee may or may not have executed. Use idempotent APIs or provide a separate endpoint to query the outcome. + +## Bounded vs unbounded wait + +Every inter-canister call must choose a wait strategy: + +### Unbounded wait + +```rust +Call::unbounded_wait(callee, "method") +``` + +The caller waits until the callee produces a response. Response delivery (including rejects) is guaranteed. Use this for calls to canisters you control, where you trust the callee to respond in a timely manner. + +### Bounded wait + +```rust +use ic_cdk::call::Call; + +Call::bounded_wait(callee, "method") + .change_timeout(1) + .await +``` + +The caller may receive a `SYS_UNKNOWN` response after the timeout expires or if the subnet runs low on resources. Response delivery is best-effort. Use this for calls to third-party or untrusted canisters. + +**Upgrade safety:** unbounded wait calls may prevent your canister from upgrading until the callee responds. If the callee is unresponsive or malicious, your canister could be stuck indefinitely. Prefer bounded wait when calling canisters you do not control. + +## Important caveats + +### 2 MB payload limit + +Request and response payloads are each limited to 2 MB. For larger data transfers, chunk the payload across multiple calls. + +### Non-atomic execution across await + +Update methods that make inter-canister calls are **not** executed atomically. Code before `await` runs as one atomic message; code after `await` runs as a separate message. If the callback traps after `await`: + +- State changes made **before** `await` are persisted +- State changes made **after** `await` are rolled back + +This means a trap in your callback does not undo work done before the call. Design accordingly -- use idempotent operations and check postconditions. + +### Caller identity across await (Rust) + +In Rust, `ic_cdk::api::msg_caller()` returns the caller of the **current message**, not the original ingress caller. After an `await`, the "caller" is the callee returning a response. Always bind the caller to a local variable before the first `await`: + +```rust +#[update] +pub async fn transfer(to: Principal, amount: Nat) -> Result<(), String> { + let caller = ic_cdk::api::msg_caller(); // Bind BEFORE await + // ... use `caller` after await points + Ok(()) +} +``` + +In Motoko, `public shared ({ caller })` captures the original caller at method entry, so this issue does not apply. + +### Reentrancy + +Inter-canister calls are not atomic, which creates reentrancy risks. Between your outgoing call and the callback, other messages (including calls from the same canister) can execute and modify state. This can lead to double-spending or other inconsistencies. + +Mitigate with locking patterns: set a flag before the call, clear it in the callback. For detailed guidance, see [Inter-Canister Call Security](../security/inter-canister-calls.md). + +### canister_inspect_message does not apply + +The `canister_inspect_message` hook is only called for ingress messages (calls from external users). It is **not** called for inter-canister calls. Do not rely on it for access control between canisters -- perform authorization checks inside the method body instead. + +### Cross-subnet latency + +Calls between canisters on the same subnet complete within a single round. Cross-subnet calls require 2-3 consensus rounds and are noticeably slower. Keep this in mind when designing multi-canister architectures. + +## What's next + +- [Parallel Calls and Composite Queries](parallel-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 +- [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 + + From 591598e2d3b7c5bdee59a6f7ef108837366de4d5 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Mon, 16 Mar 2026 17:52:33 +0100 Subject: [PATCH 2/7] fix: use mo:core imports instead of mo:base in Motoko examples --- docs/guides/canister-calls/onchain-calls.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/canister-calls/onchain-calls.md b/docs/guides/canister-calls/onchain-calls.md index 9b79e3ac..dde440dc 100644 --- a/docs/guides/canister-calls/onchain-calls.md +++ b/docs/guides/canister-calls/onchain-calls.md @@ -53,8 +53,8 @@ Wrap inter-canister calls in `try`/`catch` to handle rejects: ```motoko import Counter "canister:counter"; -import Error "mo:base/Error"; -import Runtime "mo:base/Runtime"; +import Error "mo:core/Error"; +import Result "mo:core/Result"; persistent actor { public shared ({ caller }) func safeIncrement() : async Result.Result { From 46ae834b4610c446668cebe951397066ca6a2244 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Mon, 16 Mar 2026 18:00:28 +0100 Subject: [PATCH 3/7] fix: correct Motoko catch syntax and remove unused variable --- docs/guides/canister-calls/onchain-calls.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/guides/canister-calls/onchain-calls.md b/docs/guides/canister-calls/onchain-calls.md index dde440dc..fbfd900a 100644 --- a/docs/guides/canister-calls/onchain-calls.md +++ b/docs/guides/canister-calls/onchain-calls.md @@ -58,20 +58,18 @@ import Result "mo:core/Result"; persistent actor { public shared ({ caller }) func safeIncrement() : async Result.Result { - let originalCaller = caller; - try { await Counter.increment(); let count = await Counter.get(); #ok(count) - } catch (e : Error.Error) { + } catch (e) { #err("Counter call failed: " # Error.message(e)) }; }; }; ``` -Note that `caller` is captured before the first `await`. In Motoko, `public shared ({ caller })` binds the caller at method entry, so it is safe to use after `await`. However, binding it to a local variable makes the intent explicit. +In Motoko, `public shared ({ caller })` binds the original caller at method entry, so `caller` remains valid after `await` points. ## Making calls in Rust From 44438595e30397db628963a4391a29640620ec7a Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 17 Mar 2026 10:15:38 +0100 Subject: [PATCH 4/7] docs: address PR feedback for onchain calls guide - Remove unused CallErrorExt import from basic call example - Use ic_cdk::update re-export instead of ic_cdk_macros::update - Add canister discovery section (Motoko import, Rust init arg, hardcoded) - Add pub/sub pattern section with Motoko publisher/subscriber examples - Add third-party canister calling guidance in bounded wait section - Use realistic 5s timeout in bounded wait example - Make caller identity example instructive (use caller after await) - Add TODO for Rust pub/sub example in dfinity/examples --- docs/guides/canister-calls/onchain-calls.md | 103 ++++++++++++++++++-- 1 file changed, 97 insertions(+), 6 deletions(-) diff --git a/docs/guides/canister-calls/onchain-calls.md b/docs/guides/canister-calls/onchain-calls.md index fbfd900a..43ce0ada 100644 --- a/docs/guides/canister-calls/onchain-calls.md +++ b/docs/guides/canister-calls/onchain-calls.md @@ -79,8 +79,8 @@ The Rust CDK provides `Call::unbounded_wait` and `Call::bounded_wait` to call ot ```rust use candid::{Nat, Principal}; -use ic_cdk::call::{Call, CallErrorExt}; -use ic_cdk_macros::update; +use ic_cdk::call::Call; +use ic_cdk::update; #[update] pub async fn call_get_and_set(counter: Principal, new_value: Nat) -> Nat { @@ -100,7 +100,7 @@ The call returns a `Result` where the error type distinguishes **clean rejects** ```rust use candid::Principal; use ic_cdk::call::{Call, CallErrorExt}; -use ic_cdk_macros::update; +use ic_cdk::update; #[update] pub async fn call_increment(counter: Principal) -> Result<(), String> { @@ -121,6 +121,21 @@ The distinction matters for correctness: - **Clean reject** -- the callee never executed the method. Safe to retry. - **Non-clean reject** -- the callee may or may not have executed. Use idempotent APIs or provide a separate endpoint to query the outcome. +## Canister discovery + +Before making an inter-canister call, your canister needs the `Principal` of the target canister. There are three common approaches: + +**Motoko — import by name.** Use `import Counter "canister:counter"` where the name matches your `icp.yaml` configuration. The build toolchain resolves the canister ID at compile time. + +**Rust — init argument.** Accept the target canister's `Principal` as an `#[init]` argument and store it for later use. Pass the canister ID at deploy time: + +```bash +TARGET_ID=$(icp canister id counter) +icp deploy my_canister --argument "(principal \"$TARGET_ID\")" +``` + +**Hardcoded principal.** For well-known system canisters (like the management canister `aaaaa-aa` or the NNS ledger), you can hardcode the `Principal` directly. Avoid this for application canisters since their IDs may differ between local and mainnet deployments. + ## Bounded vs unbounded wait Every inter-canister call must choose a wait strategy: @@ -139,7 +154,7 @@ The caller waits until the callee produces a response. Response delivery (includ use ic_cdk::call::Call; Call::bounded_wait(callee, "method") - .change_timeout(1) + .change_timeout(5) // timeout in seconds .await ``` @@ -147,6 +162,76 @@ The caller may receive a `SYS_UNKNOWN` response after the timeout expires or if **Upgrade safety:** unbounded wait calls may prevent your canister from upgrading until the callee responds. If the callee is unresponsive or malicious, your canister could be stuck indefinitely. Prefer bounded wait when calling canisters you do not control. +**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. + +## 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). + +### Publisher + +The publisher stores subscriber callbacks and invokes them when publishing: + +```motoko +import List "mo:base/List"; + +persistent actor Publisher { + + type Event = { topic : Text; value : Nat }; + + type Subscriber = { + topic : Text; + callback : shared Event -> (); + }; + + var subscribers = List.nil(); + + public func subscribe(subscriber : Subscriber) { + subscribers := List.push(subscriber, subscribers); + }; + + public func publish(event : Event) { + for (sub in List.toArray(subscribers).vals()) { + if (sub.topic == event.topic) { + sub.callback(event); + }; + }; + }; +}; +``` + +### Subscriber + +The subscriber registers a callback with the publisher using an inter-canister call: + +```motoko +import Publisher "canister:pub"; + +persistent actor Subscriber { + + type Event = { topic : Text; value : Nat }; + + var count : Nat = 0; + + public func init(topic : Text) { + Publisher.subscribe({ + topic; + callback = onEvent; + }); + }; + + public func onEvent(event : Event) { + count += event.value; + }; + + public query func getCount() : async Nat { count }; +}; +``` + +The key mechanism is passing a **shared function reference** (`callback`) across canisters. When the publisher calls `sub.callback(event)`, it makes an inter-canister call back to the subscriber. + + + ## Important caveats ### 2 MB payload limit @@ -170,7 +255,13 @@ In Rust, `ic_cdk::api::msg_caller()` returns the caller of the **current message #[update] pub async fn transfer(to: Principal, amount: Nat) -> Result<(), String> { let caller = ic_cdk::api::msg_caller(); // Bind BEFORE await - // ... use `caller` after await points + + Call::unbounded_wait(LEDGER, "transfer") + .with_arg(&(caller, to, amount)) + .await + .map_err(|e| format!("Transfer failed: {:?}", e))?; + + ic_cdk::println!("Transfer initiated by {}", caller); // Safe: captured before await Ok(()) } ``` @@ -198,4 +289,4 @@ Calls between canisters on the same subnet complete within a single round. Cross - [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 - + From c57c99bfe24d4ec913a360ecadd2da2a32ec3ae2 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 17 Mar 2026 10:25:03 +0100 Subject: [PATCH 5/7] docs: add environment variable canister discovery, note Motoko import redesign - Rewrite canister discovery section to recommend icp-cli's automatic PUBLIC_CANISTER_ID: environment variables as the primary approach - Show Rust (ic_cdk::api::env_var_value) and Motoko (Prim.envVar) patterns - Move init arguments and hardcoded principals to "Alternative approaches" - Add notes throughout about canister:name syntax being redesigned for icp-cli compatibility - Add note on pub/sub subscriber's canister:pub import --- docs/guides/canister-calls/onchain-calls.md | 47 ++++++++++++++++++--- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/docs/guides/canister-calls/onchain-calls.md b/docs/guides/canister-calls/onchain-calls.md index 43ce0ada..c3c79342 100644 --- a/docs/guides/canister-calls/onchain-calls.md +++ b/docs/guides/canister-calls/onchain-calls.md @@ -30,7 +30,9 @@ To make query responses verifiable without the cost of update calls, see [Certif ## Making calls in Motoko -Import another canister by name (the name must match your `icp.yaml` configuration) and call its methods with `await`: +Import another canister by name and call its methods with `await`: + +> **Note:** The `canister:name` import syntax is being redesigned for icp-cli compatibility. See [Canister discovery](#canister-discovery) for the recommended environment variable approach. ```motoko import Counter "canister:counter"; @@ -123,18 +125,51 @@ The distinction matters for correctness: ## Canister discovery -Before making an inter-canister call, your canister needs the `Principal` of the target canister. There are three common approaches: +Before making an inter-canister call, your canister needs the `Principal` of the target canister. Canister IDs are assigned at deployment time and differ between environments (local, staging, mainnet), so hardcoding them creates portability problems. + +### Environment variables (recommended) + +`icp deploy` automatically injects `PUBLIC_CANISTER_ID:` environment variables into every canister in the environment. This means each canister can discover any other canister's ID at runtime without hardcoding: + +**Rust:** + +```rust +use candid::Principal; -**Motoko — import by name.** Use `import Counter "canister:counter"` where the name matches your `icp.yaml` configuration. The build toolchain resolves the canister ID at compile time. +let counter_id = Principal::from_text( + &ic_cdk::api::env_var_value("PUBLIC_CANISTER_ID:counter") +).unwrap(); +``` -**Rust — init argument.** Accept the target canister's `Principal` as an `#[init]` argument and store it for later use. Pass the canister ID at deploy time: +**Motoko:** + +```motoko +import Prim "mo:⛔"; +import Principal "mo:core/Principal"; + +let ?counterIdText = Prim.envVar("PUBLIC_CANISTER_ID:counter") else { + return #err("counter canister ID not set"); +}; +let counterId = Principal.fromText(counterIdText); +``` + +> **Note:** `Prim.envVar` uses an internal module (`mo:⛔`). This functionality will move to the Motoko core library in a future release. + +Deployment order does not matter — `icp deploy` creates all canisters first, then injects variables, then installs code. Variables are only updated for the canisters being deployed, so run `icp deploy` (without arguments) when adding new canisters to update all of them. + +### Alternative approaches + +**Init arguments.** Accept the target `Principal` as an `#[init]` argument and store it. This avoids the environment variable lookup at call time but requires passing the ID at every deploy and upgrade: ```bash TARGET_ID=$(icp canister id counter) icp deploy my_canister --argument "(principal \"$TARGET_ID\")" ``` -**Hardcoded principal.** For well-known system canisters (like the management canister `aaaaa-aa` or the NNS ledger), you can hardcode the `Principal` directly. Avoid this for application canisters since their IDs may differ between local and mainnet deployments. +**Hardcoded principal.** Acceptable for well-known system canisters (like the management canister `aaaaa-aa` or the NNS ledger). Avoid for application canisters. + +> **Motoko named imports:** Motoko's `import Counter "canister:counter"` syntax resolves canister IDs at compile time. This syntax is currently being redesigned to work with icp-cli's environment-based discovery model. Use environment variables for now if you are building with icp-cli. + ## Bounded vs unbounded wait @@ -230,6 +265,8 @@ persistent actor Subscriber { The key mechanism is passing a **shared function reference** (`callback`) across canisters. When the publisher calls `sub.callback(event)`, it makes an inter-canister call back to the subscriber. +> **Note:** The subscriber uses `canister:pub` to import the publisher. See [Canister discovery](#canister-discovery) for the note on this syntax and the recommended environment variable alternative. + ## Important caveats From c9da682ced0276dfdba43fa99d7b95833bd9e778 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 17 Mar 2026 10:28:21 +0100 Subject: [PATCH 6/7] docs: use Runtime.envVar instead of Prim.envVar, add icp-cli to upstream notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace Prim.envVar (internal mo:⛔ module) with Runtime.envVar from mo:core/Runtime — the public API available since motoko-core v2.1.0 - Add icp-cli docs and icp-cli icskill to upstream attribution comment --- docs/guides/canister-calls/onchain-calls.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/guides/canister-calls/onchain-calls.md b/docs/guides/canister-calls/onchain-calls.md index c3c79342..dd5f0685 100644 --- a/docs/guides/canister-calls/onchain-calls.md +++ b/docs/guides/canister-calls/onchain-calls.md @@ -144,17 +144,15 @@ let counter_id = Principal::from_text( **Motoko:** ```motoko -import Prim "mo:⛔"; +import Runtime "mo:core/Runtime"; import Principal "mo:core/Principal"; -let ?counterIdText = Prim.envVar("PUBLIC_CANISTER_ID:counter") else { +let ?counterIdText = Runtime.envVar("PUBLIC_CANISTER_ID:counter") else { return #err("counter canister ID not set"); }; let counterId = Principal.fromText(counterIdText); ``` -> **Note:** `Prim.envVar` uses an internal module (`mo:⛔`). This functionality will move to the Motoko core library in a future release. - Deployment order does not matter — `icp deploy` creates all canisters first, then injects variables, then installs code. Variables are only updated for the canisters being deployed, so run `icp deploy` (without arguments) when adding new canisters to update all of them. ### Alternative approaches @@ -326,4 +324,4 @@ Calls between canisters on the same subnet complete within a single round. Cross - [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 - + From 87023a65ce52fecf9e50faf2c5d18b107b5d2c50 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 17 Mar 2026 10:33:21 +0100 Subject: [PATCH 7/7] docs: modernize pub/sub to mo:core/List, fix caller identity example - Update pub/sub publisher example from mo:base/List (linked list) to mo:core/List (mutable growable array) for consistency with other examples - Fix caller identity example: pass ledger as parameter instead of using undefined LEDGER constant --- docs/guides/canister-calls/onchain-calls.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/guides/canister-calls/onchain-calls.md b/docs/guides/canister-calls/onchain-calls.md index dd5f0685..dd9d3084 100644 --- a/docs/guides/canister-calls/onchain-calls.md +++ b/docs/guides/canister-calls/onchain-calls.md @@ -206,7 +206,7 @@ The publisher/subscriber pattern is a natural fit for inter-canister communicati The publisher stores subscriber callbacks and invokes them when publishing: ```motoko -import List "mo:base/List"; +import List "mo:core/List"; persistent actor Publisher { @@ -217,14 +217,14 @@ persistent actor Publisher { callback : shared Event -> (); }; - var subscribers = List.nil(); + var subscribers = List.empty(); public func subscribe(subscriber : Subscriber) { - subscribers := List.push(subscriber, subscribers); + List.add(subscribers, subscriber); }; public func publish(event : Event) { - for (sub in List.toArray(subscribers).vals()) { + for (sub in List.values(subscribers)) { if (sub.topic == event.topic) { sub.callback(event); }; @@ -288,10 +288,10 @@ In Rust, `ic_cdk::api::msg_caller()` returns the caller of the **current message ```rust #[update] -pub async fn transfer(to: Principal, amount: Nat) -> Result<(), String> { +pub async fn transfer(ledger: Principal, to: Principal, amount: Nat) -> Result<(), String> { let caller = ic_cdk::api::msg_caller(); // Bind BEFORE await - Call::unbounded_wait(LEDGER, "transfer") + Call::unbounded_wait(ledger, "transfer") .with_arg(&(caller, to, amount)) .await .map_err(|e| format!("Transfer failed: {:?}", e))?;