Skip to content

refactor(motoko): canister: import + --actor-env-alias for external-canister calls (ic-pos, evm_block_explorer, llm_chatbot) - #1453

Merged
marc0olo merged 3 commits into
masterfrom
motoko-actor-env-alias-tier2
Jul 28, 2026
Merged

refactor(motoko): canister: import + --actor-env-alias for external-canister calls (ic-pos, evm_block_explorer, llm_chatbot)#1453
marc0olo merged 3 commits into
masterfrom
motoko-actor-env-alias-tier2

Conversation

@marc0olo

@marc0olo marc0olo commented Jul 28, 2026

Copy link
Copy Markdown
Member

What

Tier 2 of the canister:<name> import + --actor-env-alias migration (follows #1452). Applies the same pattern to the three Motoko examples whose backend calls an external canister that is already named and env-injected in icp.yaml:

Example Target Hand-written code removed
ic-pos icrc1_ledger Ledger actor type + Account/Transfer/Transaction/GetTransactions* subtypes
evm_block_explorer evm_rpc ~140-line type surface in EvmRpcApi.mo (RpcServices/RpcConfig/BlockTag/error types/EvmRpcActor)
llm_chatbot llm LlmActor/Request/Response + the Runtime.envVar resolver

Approach

For each backend:

  • Import the target by name (import Ledger "canister:icrc1_ledger", etc.) 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 — so it is authoritative and matches the deployed canister.
  • Wire the import in mops.toml: --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.

File-placement convention

External-canister interfaces live in a shared project-root candid/ directory, distinct from the project's own generated interface:

  • own interface → stays with its code, generated: backend/backend.didmops generate candid backend
  • external (called) interfaces → vendored into candid/<name>.did, extracted from the pinned Wasm

This 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's mops.toml).

Committed .did files are full, not trimmed

The 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 and evm_block_explorer's Block are part of the backend's own public API (the frontends generate bindings from backend.did). For llm_chatbot the public message types are kept (they are structurally identical to the LLM canister's, so values flow straight into LLM.v1_chat); for evm_block_explorer the identical EvmRpc.Block is reused. All three backend.did files are byte-identical after the change (verified with mops generate candid backend), so no frontend is affected.

Testing

All three build with mops build, and every backend.did is unchanged. The backend→external call was verified end-to-end for each (note: the example CI jobs deploy all three but only evm_block_explorer's test.sh asserts the external call — ic-pos's runs in an unasserted timer and llm_chatbot's test.sh is empty — so the other two were verified locally):

  • evm_block_explorerget_evm_block(1) returns Ethereum mainnet block 1 with the correct hash and miner (CI test.sh + local), a real call through canister:evm_rpc + HTTPS outcall.
  • ic-pos — after a real transfer, the backend logs Payment of 0.005 LICRC1 received by merchant 'Test Shop', which requires icrc1_symbol + icrc1_decimals + get_transactions to all succeed through canister:icrc1_ledger (incl. decoding the full ICRC transaction response).
  • llm_chatbotprompt(...) returns a model reply, exercising the full backend→llm v1_chat round trip (with tools = null and the structural response decode) against a local Ollama-backed LLM canister.

🤖 Generated with Claude Code

…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>
@marc0olo
marc0olo requested review from a team as code owners July 28, 2026 11:02
@marc0olo
marc0olo marked this pull request as draft July 28, 2026 12:15
…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>
@marc0olo

Copy link
Copy Markdown
Member Author

Updated per review feedback: the vendored external-canister interfaces now live in a shared project-root candid/ directory instead of backend/, establishing a clear convention:

  • a canister's own interface stays with its code and is generated — backend/backend.didmops generate candid backend;
  • interfaces of external canisters the project calls are vendored into candid/<name>.did, extracted from the pinned Wasm.

This keeps first-party and third-party interfaces separate (a 586-line ledger .did no longer sits next to the 20-line backend.did) and scales to projects where several canisters share one external interface — a single copy in candid/, referenced by each canister's mops.toml. The READMEs now explain this and drop the earlier vague "different kind of file" wording. All three backend.did files remain byte-identical.

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>
@marc0olo
marc0olo requested a review from Copilot July 28, 2026 16:18
@marc0olo
marc0olo marked this pull request as ready for review July 28, 2026 16:18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, and evm_block_explorer backends to typed imports (import ... "canister:...") for external canister calls.
  • Added vendored external Candid interfaces under each example’s candid/ directory and wired them into mops.toml via --actor-env-alias.
  • Updated READMEs to document how to refresh external .did files (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.

@marc0olo
marc0olo merged commit b562697 into master Jul 28, 2026
11 checks passed
@marc0olo
marc0olo deleted the motoko-actor-env-alias-tier2 branch July 28, 2026 19:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants