refactor(motoko): canister: import + --actor-env-alias for external-canister calls (ic-pos, evm_block_explorer, llm_chatbot) - #1453
Conversation
…al-canister calls
Apply the Tier 1 pattern (canister:<name> import backed by moc's
--actor-env-alias) to the three Motoko examples that call an external
canister already named + env-injected in icp.yaml:
- ic-pos → icrc1_ledger (drops the hand-written Ledger actor
type and the Account/Transfer/Transaction subtypes)
- evm_block_explorer → evm_rpc (drops the ~140-line EvmRpcApi type
surface: RpcServices/RpcConfig/BlockTag/errors/actor)
- llm_chatbot → llm (drops the LlmActor/Request/Response
boilerplate; keeps the public message types, which
are the backend's own API and are structurally
identical to the LLM canister's)
For each backend:
- import the target by name and drop the actor type + Runtime.envVar +
actor(id) plumbing (and the now-unneeded <system> params);
- commit the target's Candid interface, extracted from the exact pre-built
Wasm pinned in icp.yaml via `ic-wasm <wasm> metadata candid:service`
(icrc1_ledger.did, evm_rpc.did, llm.did);
- wire the import in mops.toml with
`--actor-env-alias <alias> PUBLIC_CANISTER_ID:<name> <did-path>`
(per-canister args replace the global [moc].args, so the global flags
are repeated).
The principal is still resolved from PUBLIC_CANISTER_ID:<name>, now at
canister install/upgrade rather than per call. Each backend's public
backend.did is byte-identical after the change (verified with
`mops generate candid backend`), so the frontends are unaffected.
READMEs document the typed import and, for these external targets, that
the committed .did is refreshed by re-extracting it from the pinned Wasm
(not `mops generate candid`).
Verified: all three build with `mops build`; evm_block_explorer deployed
locally and `get_evm_block(1)` returns Ethereum mainnet block 1 with the
correct hash and miner.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…vel candid/ dir Address review feedback on the Tier 2 examples: keep a project's own interface with its code (backend/backend.did, generated by `mops generate candid`), and move the interfaces of *external* canisters the project calls into a shared, project-root `candid/` directory: - ic-pos: backend/icrc1_ledger.did -> candid/icrc1_ledger.did - evm_block_explorer: backend/evm_rpc.did -> candid/evm_rpc.did - llm_chatbot: backend/llm.did -> candid/llm.did This separates first-party from vendored interfaces (a 586-line ledger .did no longer sits next to the 20-line backend.did) and scales to projects with several canisters that share an external interface — one copy in candid/, referenced by each canister's mops.toml, rather than a per-backend folder that would duplicate it. Updates the `--actor-env-alias` did-paths in mops.toml, the in-code comments, and the READMEs (which now explain the candid/ convention and drop the vague "different kind of file" wording). Rebuilt all three; each backend.did is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Updated per review feedback: the vendored external-canister interfaces now live in a shared project-root
This keeps first-party and third-party interfaces separate (a 586-line ledger |
The refresh instructions previously said "re-extract it from that Wasm" but never said where the Wasm comes from — it is only pinned by URL in icp.yaml, not present in the project. Rewrite the note in all three Tier 2 examples to give two clear, self-contained options: - from mainnet, by the canister's principal: `icp canister metadata <principal> candid:service -e ic` - from the pinned Wasm: download the exact pre-built artifact pinned in icp.yaml (build.steps[].url), unpack it, then `ic-wasm ... candid:service` Both were verified to reproduce the committed candid/*.did byte-for-byte. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR continues the Motoko canister:<name> + --actor-env-alias migration for examples that call external canisters, replacing hand-written actor/type surfaces with typed canister imports backed by committed external .did files.
Changes:
- Switched
llm_chatbot,ic-pos, andevm_block_explorerbackends to typed imports (import ... "canister:...") for external canister calls. - Added vendored external Candid interfaces under each example’s
candid/directory and wired them intomops.tomlvia--actor-env-alias. - Updated READMEs to document how to refresh external
.didfiles (from mainnet metadata or pinned Wasm artifacts).
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| motoko/llm_chatbot/backend/app.mo | Replaces env-var + actor(id) plumbing with import LLM "canister:llm" and calls LLM.v1_chat directly. |
| motoko/llm_chatbot/mops.toml | Adds per-canister args including --actor-env-alias llm ... candid/llm.did. |
| motoko/llm_chatbot/README.md | Documents the typed import approach and how to refresh candid/llm.did. |
| motoko/llm_chatbot/candid/llm.did | Adds the full vendored LLM canister interface for typing canister:llm. |
| motoko/ic-pos/backend/app.mo | Replaces the minimal inline ledger actor/type subset with import Ledger "canister:icrc1_ledger" calls. |
| motoko/ic-pos/mops.toml | Adds --actor-env-alias icrc1_ledger ... candid/icrc1_ledger.did. |
| motoko/ic-pos/README.md | Documents the typed import approach and how to refresh candid/icrc1_ledger.did. |
| motoko/ic-pos/candid/icrc1_ledger.did | Adds the full vendored ICRC-1 ledger interface for typing canister:icrc1_ledger. |
| motoko/evm_block_explorer/backend/EvmRpcApi.mo | Removes the hand-written EVM RPC type surface in favor of import EvmRpc "canister:evm_rpc" and EvmRpc.* types. |
| motoko/evm_block_explorer/mops.toml | Adds --actor-env-alias evm_rpc ... candid/evm_rpc.did. |
| motoko/evm_block_explorer/README.md | Documents the typed import approach and how to refresh candid/evm_rpc.did. |
| motoko/evm_block_explorer/candid/evm_rpc.did | Adds the full vendored EVM RPC canister interface for typing canister:evm_rpc. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
What
Tier 2 of the
canister:<name>import +--actor-env-aliasmigration (follows #1452). Applies the same pattern to the three Motoko examples whose backend calls an external canister that is already named and env-injected inicp.yaml:ic-posicrc1_ledgerLedgeractor type +Account/Transfer/Transaction/GetTransactions*subtypesevm_block_explorerevm_rpcEvmRpcApi.mo(RpcServices/RpcConfig/BlockTag/error types/EvmRpcActor)llm_chatbotllmLlmActor/Request/Response+ theRuntime.envVarresolverApproach
For each backend:
import Ledger "canister:icrc1_ledger", etc.) and drop the actor type +Runtime.envVar+actor(id)plumbing (and the now-unneeded<system>params).icp.yamlviaic-wasm <wasm> metadata candid:service— so it is authoritative and matches the deployed canister.mops.toml:--actor-env-alias <alias> PUBLIC_CANISTER_ID:<name> <did-path>. Per-canisterargsreplace the global[moc].args, so the global flags are repeated.The principal is still resolved from
PUBLIC_CANISTER_ID:<name>, now at canister install/upgrade rather than per call.File-placement convention
External-canister interfaces live in a shared project-root
candid/directory, distinct from the project's own generated interface:backend/backend.did←mops generate candid backendcandid/<name>.did, extracted from the pinned WasmThis separates first-party from third-party interfaces and scales to projects where several canisters share one external interface (a single copy in
candid/, referenced by each canister'smops.toml).Committed
.didfiles are full, not trimmedThe vendored interfaces are the full authoritative services (icrc1_ledger 586 lines, evm_rpc 348, llm 56) — the decided convention is to avoid hand-trimmed subsets. They match the "provide the target canister's
.did" principle and stay regenerable by re-extraction (documented in each README).Frontend safety
llm_chatbot's message types andevm_block_explorer'sBlockare part of the backend's own public API (the frontends generate bindings frombackend.did). Forllm_chatbotthe public message types are kept (they are structurally identical to the LLM canister's, so values flow straight intoLLM.v1_chat); forevm_block_explorerthe identicalEvmRpc.Blockis reused. All threebackend.didfiles are byte-identical after the change (verified withmops generate candid backend), so no frontend is affected.Testing
All three build with
mops build, and everybackend.didis unchanged. The backend→external call was verified end-to-end for each (note: the example CI jobs deploy all three but onlyevm_block_explorer'stest.shasserts the external call —ic-pos's runs in an unasserted timer andllm_chatbot'stest.shis empty — so the other two were verified locally):get_evm_block(1)returns Ethereum mainnet block 1 with the correct hash and miner (CItest.sh+ local), a real call throughcanister:evm_rpc+ HTTPS outcall.Payment of 0.005 LICRC1 received by merchant 'Test Shop', which requiresicrc1_symbol+icrc1_decimals+get_transactionsto all succeed throughcanister:icrc1_ledger(incl. decoding the full ICRC transaction response).prompt(...)returns a model reply, exercising the full backend→llmv1_chatround trip (withtools = nulland the structural response decode) against a local Ollama-backed LLM canister.🤖 Generated with Claude Code