@ic-reactor/mcp 0.0.0-stage → 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.yml +108 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +77 -0
- package/.github/actions/setup-rust-wasm/action.yml +25 -0
- package/.github/dependabot.yml +107 -0
- package/.github/workflows/auth-latest.yml +67 -0
- package/.github/workflows/b3pay-site.yml +49 -0
- package/.github/workflows/ci.yml +430 -0
- package/.github/workflows/codeql.yml +41 -0
- package/.github/workflows/dependency-review.yml +68 -0
- package/.github/workflows/docs-check.yml +98 -0
- package/.github/workflows/docs.yml +265 -0
- package/.github/workflows/e2e.yml +90 -0
- package/.github/workflows/examples-published.yml +97 -0
- package/.github/workflows/release.yml +326 -0
- package/.husky/pre-commit +12 -0
- package/.prettierignore +109 -0
- package/.prettierrc +6 -0
- package/AGENTS.md +103 -0
- package/CHANGELOG.md +3268 -0
- package/CLAUDE.md +93 -0
- package/CNAME +1 -0
- package/CODE_OF_CONDUCT.md +7 -0
- package/CONTRIBUTING.md +433 -0
- package/LICENSE +21 -0
- package/README.md +2 -2
- package/SECURITY.md +74 -0
- package/b3pay-module.json +194 -0
- package/docs/.remarkrc.json +10 -0
- package/docs/README.md +64 -0
- package/docs/astro.config.mjs +205 -0
- package/docs/favicon.svg +15 -0
- package/docs/package.json +39 -0
- package/docs/public/favicon.svg +15 -0
- package/docs/public/ic-reactor-social-card.png +0 -0
- package/docs/scripts/b3pay-static.mjs +22 -0
- package/docs/scripts/check-example-docs.mjs +214 -0
- package/docs/scripts/check-links.mjs +212 -0
- package/docs/scripts/fix-dist-links.js +125 -0
- package/docs/scripts/fix-frontmatter.js +71 -0
- package/docs/scripts/remove-redundant-h1.cjs +48 -0
- package/docs/scripts/rewrite-typedoc-links.js +110 -0
- package/docs/scripts/split-index.js +69 -0
- package/docs/src/assets/ic-reactor-hero-dark.svg +23 -0
- package/docs/src/assets/ic-reactor-hero-light.svg +23 -0
- package/docs/src/assets/ic-reactor-lockup-dark.svg +15 -0
- package/docs/src/assets/ic-reactor-lockup-light.svg +15 -0
- package/docs/src/assets/ic-reactor-mark-dark.svg +14 -0
- package/docs/src/assets/ic-reactor-mark-light.svg +14 -0
- package/docs/src/assets/layers/candid-core-layer-icon-dark.svg +4 -0
- package/docs/src/assets/layers/candid-core-layer-icon-light.svg +4 -0
- package/docs/src/assets/layers/candid-profile-layer-icon-dark.svg +4 -0
- package/docs/src/assets/layers/candid-profile-layer-icon-light.svg +4 -0
- package/docs/src/assets/layers/ic-reactor-layer-icon-dark.svg +4 -0
- package/docs/src/assets/layers/ic-reactor-layer-icon-light.svg +4 -0
- package/docs/src/components/LibraryCards.astro +172 -0
- package/docs/src/components/LibraryIcon.astro +28 -0
- package/docs/src/components/SiteTitle.astro +241 -0
- package/docs/src/content/docs/404.mdx +19 -0
- package/docs/src/content/docs/build/agent-tools.mdx +593 -0
- package/docs/src/content/docs/build/icrc1-wallet.mdx +619 -0
- package/docs/src/content/docs/build/ssr-per-user.mdx +552 -0
- package/docs/src/content/docs/examples/canister-studio.mdx +95 -0
- package/docs/src/content/docs/examples/icrc-ledger.mdx +45 -0
- package/docs/src/content/docs/examples/index.mdx +38 -0
- package/docs/src/content/docs/examples/next-ssr.mdx +73 -0
- package/docs/src/content/docs/examples/node-agent-tool.mdx +80 -0
- package/docs/src/content/docs/examples/vite-wallet.mdx +71 -0
- package/docs/src/content/docs/guides/auth.mdx +1181 -0
- package/docs/src/content/docs/guides/client.mdx +551 -0
- package/docs/src/content/docs/guides/deploy.mdx +180 -0
- package/docs/src/content/docs/guides/errors.mdx +475 -0
- package/docs/src/content/docs/guides/getting-started.mdx +620 -0
- package/docs/src/content/docs/guides/reads.mdx +586 -0
- package/docs/src/content/docs/guides/ssr.mdx +529 -0
- package/docs/src/content/docs/guides/testing.mdx +401 -0
- package/docs/src/content/docs/guides/values.mdx +261 -0
- package/docs/src/content/docs/guides/vite-plugin.mdx +690 -0
- package/docs/src/content/docs/guides/writes.mdx +572 -0
- package/docs/src/content/docs/ic-reactor/index.mdx +167 -0
- package/docs/src/content/docs/index.mdx +65 -0
- package/docs/src/content/docs/migrating-from-3.mdx +535 -0
- package/docs/src/content/docs/packages/core.mdx +186 -0
- package/docs/src/content/docs/packages/mcp.mdx +163 -0
- package/docs/src/content/docs/packages/react.mdx +207 -0
- package/docs/src/content/docs/packages/vite-plugin.mdx +73 -0
- package/docs/src/content/docs/versioning.mdx +104 -0
- package/docs/src/content.config.ts +7 -0
- package/docs/src/styles/custom.css +189 -0
- package/docs/tsconfig.typedoc.json +26 -0
- package/docs/typedoc-entry.ts +17 -0
- package/docs/typedoc.json +37 -0
- package/e2e/Cargo.lock +646 -0
- package/e2e/Cargo.toml +3 -0
- package/e2e/README.md +101 -0
- package/e2e/frontends/build.mjs +94 -0
- package/e2e/frontends/index.html +10 -0
- package/e2e/global-setup.ts +65 -0
- package/e2e/icp.yaml +38 -0
- package/e2e/package.json +40 -0
- package/e2e/setup.ts +6 -0
- package/e2e/src/actor/Cargo.toml +14 -0
- package/e2e/src/actor/hello_actor.did +10 -0
- package/e2e/src/actor/src/lib.rs +73 -0
- package/e2e/src/calls.test.ts +132 -0
- package/e2e/src/declarations/hello_actor.envelope.json +175 -0
- package/e2e/src/declarations/hello_actor.ts +15 -0
- package/e2e/src/errors.test.ts +107 -0
- package/e2e/src/frontend-headers.test.ts +253 -0
- package/e2e/src/identity.test.ts +61 -0
- package/e2e/src/react.test.tsx +59 -0
- package/e2e/src/replica.ts +70 -0
- package/e2e/src/vite-env.test.ts +108 -0
- package/e2e/test.sh +47 -0
- package/e2e/tsconfig.json +20 -0
- package/e2e/tsconfig.typecheck.json +9 -0
- package/e2e/vitest.config.ts +22 -0
- package/eslint.config.mjs +100 -0
- package/evals/PREREGISTRATION.md +1346 -0
- package/evals/README.md +756 -0
- package/evals/conditions/thin/condition.json +8 -0
- package/evals/conditions/thin/docs/candid-core-cli.README.md +90 -0
- package/evals/conditions/thin/docs/candid-core-schema.README.md +339 -0
- package/evals/conditions/thin/package.json +25 -0
- package/evals/conditions/thin/starter/src/generated/icrc1.ts +57 -0
- package/evals/conditions/thin-guide/condition.json +10 -0
- package/evals/conditions/thin-guide/docs/llms.txt +292 -0
- package/evals/conditions/v3/condition.json +9 -0
- package/evals/conditions/v3/docs/ic-reactor-core.llms.txt +232 -0
- package/evals/conditions/v3/docs/ic-reactor-react.llms.txt +280 -0
- package/evals/conditions/v3/docs/llms-full.txt +1053 -0
- package/evals/conditions/v3/package.json +26 -0
- package/evals/conditions/v3/starter/src/declarations/icrc1.did.d.ts +95 -0
- package/evals/conditions/v3/starter/src/declarations/icrc1.did.js +140 -0
- package/evals/conditions/v4/PORTING.md +384 -0
- package/evals/conditions/v4/README.md +106 -0
- package/evals/conditions/v4/condition.json +5 -0
- package/evals/conditions/v4/docs/llms.txt +323 -0
- package/evals/conditions/v4/package.json +27 -0
- package/evals/conditions/v4/starter/src/generated/icrc1.ts +78 -0
- package/evals/conditions/v4-proto/condition.json +6 -0
- package/evals/conditions/v4-proto/docs/llms.txt +291 -0
- package/evals/conditions/v4-proto/lib/SHIM-NOTES.md +20 -0
- package/evals/conditions/v4-proto/lib/package.json +27 -0
- package/evals/conditions/v4-proto/lib/src/client.ts +471 -0
- package/evals/conditions/v4-proto/lib/src/errors.ts +214 -0
- package/evals/conditions/v4-proto/lib/src/index.ts +36 -0
- package/evals/conditions/v4-proto/lib/src/principal.ts +175 -0
- package/evals/conditions/v4-proto/lib/src/service.ts +69 -0
- package/evals/conditions/v4-proto/lib/src/types.ts +174 -0
- package/evals/conditions/v4-proto/lib/test/runtime.test.ts +170 -0
- package/evals/conditions/v4-proto/lib/test/tsconfig.json +10 -0
- package/evals/conditions/v4-proto/lib/test/types.test-d.ts +145 -0
- package/evals/conditions/v4-proto/lib/test/vitest.config.mjs +8 -0
- package/evals/conditions/v4-proto/lib/tsconfig.json +17 -0
- package/evals/conditions/v4-proto/package.json +26 -0
- package/evals/conditions/v4-proto/starter/src/generated/icrc1.service.ts +19 -0
- package/evals/conditions/v4-proto/starter/src/generated/icrc1.ts +57 -0
- package/evals/drive.mjs +1221 -0
- package/evals/gate.mjs +161 -0
- package/evals/harness/aggregate.mjs +534 -0
- package/evals/harness/aggregate.test.mjs +344 -0
- package/evals/harness/assemble.mjs +216 -0
- package/evals/harness/assemble.test.mjs +143 -0
- package/evals/harness/auth.mjs +91 -0
- package/evals/harness/check-docs.mjs +157 -0
- package/evals/harness/declarations/icrc1.did.d.ts +95 -0
- package/evals/harness/declarations/icrc1.did.js +140 -0
- package/evals/harness/drive-auth.test.mjs +584 -0
- package/evals/harness/drive.test.mjs +509 -0
- package/evals/harness/fake-auth.ts +67 -0
- package/evals/harness/fake-ledger.ts +178 -0
- package/evals/harness/gate-plan.mjs +104 -0
- package/evals/harness/gate-plan.test.mjs +243 -0
- package/evals/harness/icrc1.did +88 -0
- package/evals/harness/judge.mjs +63 -0
- package/evals/harness/judge.test.mjs +81 -0
- package/evals/harness/leak-scan.mjs +1373 -0
- package/evals/harness/leak-scan.test.mjs +1300 -0
- package/evals/harness/probes/amounts.ts +14 -0
- package/evals/harness/probes/node-tool.probe.ts +48 -0
- package/evals/harness/probes/probe.mjs +149 -0
- package/evals/harness/probes/react-wallet.probe.ts +96 -0
- package/evals/harness/public/support/auth.ts +38 -0
- package/evals/harness/public/support/fake-replica.d.ts +28 -0
- package/evals/harness/public/support/fake-replica.js +434 -0
- package/evals/harness/public/support/ledger.idl.d.ts +95 -0
- package/evals/harness/public/support/ledger.idl.js +140 -0
- package/evals/harness/public/support/world.ts +179 -0
- package/evals/harness/runs.mjs +139 -0
- package/evals/harness/sandbox.mjs +88 -0
- package/evals/harness/sandbox.test.mjs +134 -0
- package/evals/harness/ship.mjs +264 -0
- package/evals/harness/ship.test.mjs +552 -0
- package/evals/harness/world.ts +291 -0
- package/evals/package.json +26 -0
- package/evals/pnpm-lock.yaml +1537 -0
- package/evals/pnpm-workspace.yaml +12 -0
- package/evals/results/2026-09-30T14-22-05-924Z/pilot.json +384 -0
- package/evals/results/2026-09-30T14-22-05-924Z/pilot.rescanned.json +404 -0
- package/evals/results/2026-09-30T14-22-05-924Z/pilot.rescanned.rescored.json +404 -0
- package/evals/results/2026-09-30T14-22-05-924Z/plan.json +240 -0
- package/evals/results/2026-09-30T14-22-05-924Z/summary.json +1800 -0
- package/evals/results/2026-09-30T14-22-05-924Z/summary.rescanned.json +1843 -0
- package/evals/results/2026-09-30T14-22-05-924Z/summary.rescanned.rescored.json +2100 -0
- package/evals/results/2026-09-30T15-48-23-343Z/pilot.json +404 -0
- package/evals/results/2026-09-30T15-48-23-343Z/pilot.rescored.json +404 -0
- package/evals/results/2026-09-30T15-48-23-343Z/plan.json +242 -0
- package/evals/results/2026-09-30T15-48-23-343Z/summary.json +1843 -0
- package/evals/results/2026-09-30T15-48-23-343Z/summary.rescored.json +2195 -0
- package/evals/results/2026-10-03-beta1-gate/pilot.json +162 -0
- package/evals/results/2026-10-03-beta1-gate/plan.json +141 -0
- package/evals/results/2026-10-03-beta1-gate/summary.json +983 -0
- package/evals/results/2026-10-05-ga-gate/contaminated-run.json +55 -0
- package/evals/results/2026-10-05-ga-gate/pilot.json +162 -0
- package/evals/results/2026-10-05-ga-gate/plan.json +457 -0
- package/evals/results/2026-10-05-ga-gate/summary.json +1037 -0
- package/evals/results/2026-10-05-ga-gate/summary.rescanned.json +1037 -0
- package/evals/results/README.md +31 -0
- package/evals/runs-pilot.log +150 -0
- package/evals/runs-pilot2.log +150 -0
- package/evals/score.mjs +256 -0
- package/evals/setup.mjs +547 -0
- package/evals/tasks/node-tool/faulty/thin-accepts-exponent/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-accepts-exponent/src/index.ts +121 -0
- package/evals/tasks/node-tool/faulty/thin-anonymous-transfer/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-anonymous-transfer/src/index.ts +116 -0
- package/evals/tasks/node-tool/faulty/thin-every-reject-may-have-executed/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/thin-every-reject-may-have-executed/src/index.ts +118 -0
- package/evals/tasks/node-tool/faulty/thin-fetches-root-key-off-local/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-fetches-root-key-off-local/src/index.ts +118 -0
- package/evals/tasks/node-tool/faulty/thin-ignores-root-key/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-ignores-root-key/src/index.ts +117 -0
- package/evals/tasks/node-tool/faulty/thin-misclassifies-reject-codes/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/thin-misclassifies-reject-codes/src/index.ts +119 -0
- package/evals/tasks/node-tool/faulty/thin-no-nat64-cap/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/thin-no-nat64-cap/src/index.ts +118 -0
- package/evals/tasks/node-tool/faulty/thin-number-amount/meta.json +10 -0
- package/evals/tasks/node-tool/faulty/thin-number-amount/src/index.ts +117 -0
- package/evals/tasks/node-tool/faulty/thin-truncates-fraction-digits/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-truncates-fraction-digits/src/index.ts +120 -0
- package/evals/tasks/node-tool/faulty/thin-unvalidated-recipient/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-unvalidated-recipient/src/index.ts +118 -0
- package/evals/tasks/node-tool/faulty/v3-retry-call-errors/meta.json +11 -0
- package/evals/tasks/node-tool/faulty/v3-retry-call-errors/src/index.ts +149 -0
- package/evals/tasks/node-tool/faulty/v4-anonymous-identity-sent/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/v4-anonymous-identity-sent/src/index.ts +71 -0
- package/evals/tasks/node-tool/faulty/v4-never-may-have-executed/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/v4-never-may-have-executed/src/index.ts +84 -0
- package/evals/tasks/node-tool/faulty/v4-proto-never-may-have-executed/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/v4-proto-never-may-have-executed/src/index.ts +72 -0
- package/evals/tasks/node-tool/faulty/v4-proto-refuses-nat64-max/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/v4-proto-refuses-nat64-max/src/index.ts +72 -0
- package/evals/tasks/node-tool/faulty/v4-refuses-nat64-max/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/v4-refuses-nat64-max/src/index.ts +84 -0
- package/evals/tasks/node-tool/hidden/node-tool.test.ts +312 -0
- package/evals/tasks/node-tool/prompt.md +55 -0
- package/evals/tasks/node-tool/prompt.minimal.md +30 -0
- package/evals/tasks/node-tool/solutions/thin/reference/src/index.ts +118 -0
- package/evals/tasks/node-tool/solutions/thin/reference-module-scope/src/index.ts +145 -0
- package/evals/tasks/node-tool/solutions/thin-guide/reference/src/index.ts +118 -0
- package/evals/tasks/node-tool/solutions/thin-guide/reference-module-scope/src/index.ts +145 -0
- package/evals/tasks/node-tool/solutions/v3/reference/src/index.ts +140 -0
- package/evals/tasks/node-tool/solutions/v3/reference-module-scope/src/index.ts +153 -0
- package/evals/tasks/node-tool/solutions/v4/reference/src/index.ts +84 -0
- package/evals/tasks/node-tool/solutions/v4/reference-module-scope/src/index.ts +105 -0
- package/evals/tasks/node-tool/solutions/v4-proto/reference/src/index.ts +72 -0
- package/evals/tasks/node-tool/solutions/v4-proto/reference-module-scope/src/index.ts +87 -0
- package/evals/tasks/node-tool/starter/src/contract.ts +31 -0
- package/evals/tasks/node-tool/starter/src/index.ts +7 -0
- package/evals/tasks/node-tool/starter/test/smoke.test.ts +22 -0
- package/evals/tasks/node-tool/starter/tsconfig.json +17 -0
- package/evals/tasks/node-tool/task.json +63 -0
- package/evals/tasks/react-wallet/faulty/thin-accepts-exponent/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-accepts-exponent/src/Wallet.tsx +216 -0
- package/evals/tasks/react-wallet/faulty/thin-anonymous-transfer/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-anonymous-transfer/src/Wallet.tsx +212 -0
- package/evals/tasks/react-wallet/faulty/thin-both-auth-buttons/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-both-auth-buttons/src/Wallet.tsx +210 -0
- package/evals/tasks/react-wallet/faulty/thin-constant-balance-key/meta.json +8 -0
- package/evals/tasks/react-wallet/faulty/thin-constant-balance-key/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/faulty/thin-no-nat64-cap/meta.json +9 -0
- package/evals/tasks/react-wallet/faulty/thin-no-nat64-cap/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/faulty/thin-number-balance/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-number-balance/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/faulty/thin-transfer-as-query/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-transfer-as-query/src/Wallet.tsx +254 -0
- package/evals/tasks/react-wallet/faulty/thin-truncates-fraction-digits/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-truncates-fraction-digits/src/Wallet.tsx +215 -0
- package/evals/tasks/react-wallet/faulty/thin-unguarded-balance-effect/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-unguarded-balance-effect/src/Wallet.tsx +214 -0
- package/evals/tasks/react-wallet/faulty/v3-http-4xx-unknown/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v3-http-4xx-unknown/src/Wallet.tsx +224 -0
- package/evals/tasks/react-wallet/faulty/v3-mutation-retry-3/meta.json +12 -0
- package/evals/tasks/react-wallet/faulty/v3-mutation-retry-3/src/Wallet.tsx +225 -0
- package/evals/tasks/react-wallet/faulty/v3-unknown-as-error/meta.json +11 -0
- package/evals/tasks/react-wallet/faulty/v3-unknown-as-error/src/Wallet.tsx +182 -0
- package/evals/tasks/react-wallet/faulty/v4-every-reject-unknown/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-every-reject-unknown/src/Wallet.tsx +160 -0
- package/evals/tasks/react-wallet/faulty/v4-keep-previous-data/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-keep-previous-data/src/Wallet.tsx +166 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-every-reject-unknown/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-every-reject-unknown/src/Wallet.tsx +161 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-keep-previous-data/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-keep-previous-data/src/Wallet.tsx +163 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-retry-spread/meta.json +12 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-retry-spread/src/Wallet.tsx +162 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-status-not-idle/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-status-not-idle/src/Wallet.tsx +163 -0
- package/evals/tasks/react-wallet/faulty/v4-retry-spread/meta.json +12 -0
- package/evals/tasks/react-wallet/faulty/v4-retry-spread/src/Wallet.tsx +163 -0
- package/evals/tasks/react-wallet/faulty/v4-status-not-idle/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-status-not-idle/src/Wallet.tsx +162 -0
- package/evals/tasks/react-wallet/hidden/react-wallet.test.ts +536 -0
- package/evals/tasks/react-wallet/prompt.md +88 -0
- package/evals/tasks/react-wallet/prompt.minimal.md +51 -0
- package/evals/tasks/react-wallet/solutions/thin/reference/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/solutions/thin/reference-module-scope/src/Wallet.tsx +227 -0
- package/evals/tasks/react-wallet/solutions/thin-guide/reference/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/solutions/thin-guide/reference-module-scope/src/Wallet.tsx +227 -0
- package/evals/tasks/react-wallet/solutions/v3/reference/src/Wallet.tsx +224 -0
- package/evals/tasks/react-wallet/solutions/v3/reference-module-scope/src/Wallet.tsx +230 -0
- package/evals/tasks/react-wallet/solutions/v4/reference/src/Wallet.tsx +160 -0
- package/evals/tasks/react-wallet/solutions/v4/reference-module-scope/src/Wallet.tsx +178 -0
- package/evals/tasks/react-wallet/solutions/v4-proto/reference/src/Wallet.tsx +161 -0
- package/evals/tasks/react-wallet/solutions/v4-proto/reference-module-scope/src/Wallet.tsx +161 -0
- package/evals/tasks/react-wallet/starter/src/Wallet.tsx +12 -0
- package/evals/tasks/react-wallet/starter/src/auth.ts +16 -0
- package/evals/tasks/react-wallet/starter/src/config.ts +14 -0
- package/evals/tasks/react-wallet/starter/src/ii-auth.ts +43 -0
- package/evals/tasks/react-wallet/starter/src/main.tsx +15 -0
- package/evals/tasks/react-wallet/starter/test/smoke.test.ts +34 -0
- package/evals/tasks/react-wallet/starter/tsconfig.json +18 -0
- package/evals/tasks/react-wallet/task.json +84 -0
- package/examples/canister-studio/.stackblitzrc +4 -0
- package/examples/canister-studio/README.md +183 -0
- package/examples/canister-studio/did/ckbtc_minter.did +806 -0
- package/examples/canister-studio/did/icp_ledger.did +586 -0
- package/examples/canister-studio/did/icrc1.did +88 -0
- package/examples/canister-studio/package.json +45 -0
- package/examples/canister-studio/scripts/smoke.ts +188 -0
- package/examples/canister-studio/src/calls/CacheKeyPanel.tsx +104 -0
- package/examples/canister-studio/src/calls/ConfirmWrite.tsx +265 -0
- package/examples/canister-studio/src/calls/ErrorPanel.test.tsx +113 -0
- package/examples/canister-studio/src/calls/ErrorPanel.tsx +214 -0
- package/examples/canister-studio/src/calls/MethodRunner.test.tsx +354 -0
- package/examples/canister-studio/src/calls/MethodRunner.tsx +57 -0
- package/examples/canister-studio/src/calls/ReadRunner.tsx +244 -0
- package/examples/canister-studio/src/calls/ResendPanel.tsx +165 -0
- package/examples/canister-studio/src/calls/ShareLink.tsx +59 -0
- package/examples/canister-studio/src/calls/WriteRunner.test.tsx +678 -0
- package/examples/canister-studio/src/calls/WriteRunner.tsx +670 -0
- package/examples/canister-studio/src/calls/dedupe.ts +189 -0
- package/examples/canister-studio/src/calls/error-info.ts +168 -0
- package/examples/canister-studio/src/calls/history.test.ts +284 -0
- package/examples/canister-studio/src/calls/history.ts +287 -0
- package/examples/canister-studio/src/calls/kinds.ts +68 -0
- package/examples/canister-studio/src/calls/prepare.ts +32 -0
- package/examples/canister-studio/src/calls/query-options.ts +30 -0
- package/examples/canister-studio/src/calls/read.ts +47 -0
- package/examples/canister-studio/src/calls/readback.ts +100 -0
- package/examples/canister-studio/src/calls/record.ts +123 -0
- package/examples/canister-studio/src/calls/safe-calls.test.ts +299 -0
- package/examples/canister-studio/src/calls/share.ts +46 -0
- package/examples/canister-studio/src/calls/testing/doubles.tsx +108 -0
- package/examples/canister-studio/src/calls/testing/fixtures.ts +185 -0
- package/examples/canister-studio/src/calls/useCanisterHandle.ts +35 -0
- package/examples/canister-studio/src/canisters/ckbtc_minter.envelope.json +4228 -0
- package/examples/canister-studio/src/canisters/ckbtc_minter.ts +868 -0
- package/examples/canister-studio/src/canisters/icp_ledger.envelope.json +3454 -0
- package/examples/canister-studio/src/canisters/icp_ledger.ts +584 -0
- package/examples/canister-studio/src/canisters/icrc1.envelope.json +840 -0
- package/examples/canister-studio/src/canisters/icrc1.ts +78 -0
- package/examples/canister-studio/src/components/ActionButton.tsx +32 -0
- package/examples/canister-studio/src/components/AppHeader.tsx +83 -0
- package/examples/canister-studio/src/components/AppShell.tsx +29 -0
- package/examples/canister-studio/src/components/AuthButton.tsx +79 -0
- package/examples/canister-studio/src/components/Badge.tsx +79 -0
- package/examples/canister-studio/src/components/CanisterHeader.tsx +200 -0
- package/examples/canister-studio/src/components/CanisterJump.tsx +130 -0
- package/examples/canister-studio/src/components/Card.tsx +64 -0
- package/examples/canister-studio/src/components/Code.tsx +101 -0
- package/examples/canister-studio/src/components/CopyButton.tsx +126 -0
- package/examples/canister-studio/src/components/Disclosure.tsx +34 -0
- package/examples/canister-studio/src/components/EmptyState.tsx +34 -0
- package/examples/canister-studio/src/components/MethodList.test.tsx +137 -0
- package/examples/canister-studio/src/components/MethodList.tsx +244 -0
- package/examples/canister-studio/src/components/Notice.tsx +36 -0
- package/examples/canister-studio/src/components/ProvenanceBadge.tsx +44 -0
- package/examples/canister-studio/src/components/RouteError.tsx +42 -0
- package/examples/canister-studio/src/components/SectionBoundary.tsx +76 -0
- package/examples/canister-studio/src/components/Spinner.tsx +33 -0
- package/examples/canister-studio/src/components/Tabs.tsx +96 -0
- package/examples/canister-studio/src/components/canister/CanisterLoadFailure.test.tsx +56 -0
- package/examples/canister-studio/src/components/canister/CanisterLoadFailure.tsx +165 -0
- package/examples/canister-studio/src/components/canister/CanisterNotFound.tsx +59 -0
- package/examples/canister-studio/src/components/canister/CanisterPending.tsx +33 -0
- package/examples/canister-studio/src/components/canister/MethodOverview.tsx +93 -0
- package/examples/canister-studio/src/components/canister/MethodWorkspace.test.tsx +201 -0
- package/examples/canister-studio/src/components/canister/MethodWorkspace.tsx +187 -0
- package/examples/canister-studio/src/components/canister/shared-call.test.ts +186 -0
- package/examples/canister-studio/src/components/canister/shared-call.ts +133 -0
- package/examples/canister-studio/src/components/canister/shared-link-loader.test.ts +116 -0
- package/examples/canister-studio/src/components/canister/use-canister-page.ts +44 -0
- package/examples/canister-studio/src/components/canister/workspace-key.test.ts +27 -0
- package/examples/canister-studio/src/components/canister/workspace-key.ts +36 -0
- package/examples/canister-studio/src/components/canister-id-input.ts +69 -0
- package/examples/canister-studio/src/components/canister-names.ts +11 -0
- package/examples/canister-studio/src/components/download.ts +34 -0
- package/examples/canister-studio/src/components/fallbacks.tsx +33 -0
- package/examples/canister-studio/src/components/helpers.test.ts +107 -0
- package/examples/canister-studio/src/components/history/HistoryList.tsx +284 -0
- package/examples/canister-studio/src/components/home/FeaturedLedger.tsx +149 -0
- package/examples/canister-studio/src/components/home/OpenCanister.tsx +99 -0
- package/examples/canister-studio/src/components/home/featured.test.tsx +92 -0
- package/examples/canister-studio/src/components/home/featured.ts +64 -0
- package/examples/canister-studio/src/components/interface/CandidTab.tsx +38 -0
- package/examples/canister-studio/src/components/interface/CompatReport.tsx +92 -0
- package/examples/canister-studio/src/components/interface/CompatibilityTab.tsx +190 -0
- package/examples/canister-studio/src/components/interface/ProjectTab.tsx +196 -0
- package/examples/canister-studio/src/components/interface/TypeScriptTab.tsx +114 -0
- package/examples/canister-studio/src/components/interface/tools.test.tsx +169 -0
- package/examples/canister-studio/src/components/modes.ts +31 -0
- package/examples/canister-studio/src/components/playground/MethodPicker.tsx +67 -0
- package/examples/canister-studio/src/components/playground/PlaygroundForms.tsx +112 -0
- package/examples/canister-studio/src/components/playground/PlaygroundTools.tsx +219 -0
- package/examples/canister-studio/src/components/playground/sample.ts +43 -0
- package/examples/canister-studio/src/components/playground/target.test.tsx +86 -0
- package/examples/canister-studio/src/components/primitives.test.tsx +121 -0
- package/examples/canister-studio/src/components/provenance.ts +38 -0
- package/examples/canister-studio/src/components/route-render.test.tsx +71 -0
- package/examples/canister-studio/src/components/use-task.ts +56 -0
- package/examples/canister-studio/src/components/wallet/BalancePanel.tsx +110 -0
- package/examples/canister-studio/src/components/wallet/LedgerSummary.tsx +63 -0
- package/examples/canister-studio/src/components/wallet/MinterPanel.tsx +255 -0
- package/examples/canister-studio/src/components/wallet/SendPanel.test.tsx +297 -0
- package/examples/canister-studio/src/components/wallet/SendPanel.tsx +446 -0
- package/examples/canister-studio/src/components/wallet/ledgers.test.ts +36 -0
- package/examples/canister-studio/src/components/wallet/ledgers.ts +111 -0
- package/examples/canister-studio/src/forms/JsonPanel.tsx +157 -0
- package/examples/canister-studio/src/forms/MethodForm.test.tsx +369 -0
- package/examples/canister-studio/src/forms/MethodForm.tsx +368 -0
- package/examples/canister-studio/src/forms/TypedForm.tsx +55 -0
- package/examples/canister-studio/src/forms/context.ts +62 -0
- package/examples/canister-studio/src/forms/convert.ts +131 -0
- package/examples/canister-studio/src/forms/edge.test.ts +252 -0
- package/examples/canister-studio/src/forms/engine.test.ts +358 -0
- package/examples/canister-studio/src/forms/engine.ts +1492 -0
- package/examples/canister-studio/src/forms/fields/BytesField.tsx +210 -0
- package/examples/canister-studio/src/forms/fields/Field.tsx +106 -0
- package/examples/canister-studio/src/forms/fields/FieldShell.tsx +101 -0
- package/examples/canister-studio/src/forms/fields/Group.tsx +74 -0
- package/examples/canister-studio/src/forms/fields/ListField.tsx +256 -0
- package/examples/canister-studio/src/forms/fields/Segmented.tsx +78 -0
- package/examples/canister-studio/src/forms/fields/TimestampField.tsx +108 -0
- package/examples/canister-studio/src/forms/fields/UnitsField.tsx +119 -0
- package/examples/canister-studio/src/forms/fields/composites.tsx +389 -0
- package/examples/canister-studio/src/forms/fields/leaves.tsx +350 -0
- package/examples/canister-studio/src/forms/fields.test.tsx +396 -0
- package/examples/canister-studio/src/forms/helpers.test.ts +225 -0
- package/examples/canister-studio/src/forms/hints.ts +128 -0
- package/examples/canister-studio/src/forms/json.ts +119 -0
- package/examples/canister-studio/src/forms/parsing.test.ts +217 -0
- package/examples/canister-studio/src/forms/useMethodForm.ts +251 -0
- package/examples/canister-studio/src/interface/browser.test.ts +23 -0
- package/examples/canister-studio/src/interface/browser.ts +153 -0
- package/examples/canister-studio/src/interface/cache.ts +68 -0
- package/examples/canister-studio/src/interface/cli-cache.test.ts +155 -0
- package/examples/canister-studio/src/interface/cli-cache.ts +125 -0
- package/examples/canister-studio/src/interface/compile.server.test.ts +198 -0
- package/examples/canister-studio/src/interface/compile.server.ts +262 -0
- package/examples/canister-studio/src/interface/compile.test.ts +108 -0
- package/examples/canister-studio/src/interface/compile.ts +305 -0
- package/examples/canister-studio/src/interface/docs.test.ts +75 -0
- package/examples/canister-studio/src/interface/docs.ts +100 -0
- package/examples/canister-studio/src/interface/functions.test.ts +42 -0
- package/examples/canister-studio/src/interface/functions.ts +200 -0
- package/examples/canister-studio/src/interface/live-candid.server.ts +91 -0
- package/examples/canister-studio/src/interface/live-loader.test.ts +515 -0
- package/examples/canister-studio/src/interface/live-loader.ts +364 -0
- package/examples/canister-studio/src/interface/load.test.ts +328 -0
- package/examples/canister-studio/src/interface/load.ts +249 -0
- package/examples/canister-studio/src/interface/read-candid.test.ts +185 -0
- package/examples/canister-studio/src/interface/read-candid.ts +233 -0
- package/examples/canister-studio/src/interface/server-result.ts +42 -0
- package/examples/canister-studio/src/interface/typed.ts +68 -0
- package/examples/canister-studio/src/interface/types.ts +121 -0
- package/examples/canister-studio/src/lib/format.test.ts +107 -0
- package/examples/canister-studio/src/lib/format.ts +139 -0
- package/examples/canister-studio/src/lib/hex.test.ts +43 -0
- package/examples/canister-studio/src/lib/hex.ts +33 -0
- package/examples/canister-studio/src/lib/ids.test.ts +70 -0
- package/examples/canister-studio/src/lib/ids.ts +93 -0
- package/examples/canister-studio/src/lib/reactor.test.ts +55 -0
- package/examples/canister-studio/src/lib/reactor.ts +55 -0
- package/examples/canister-studio/src/lib/registry.test.ts +74 -0
- package/examples/canister-studio/src/lib/registry.ts +238 -0
- package/examples/canister-studio/src/lib/schema.ts +148 -0
- package/examples/canister-studio/src/routeTree.gen.ts +227 -0
- package/examples/canister-studio/src/router.test.ts +290 -0
- package/examples/canister-studio/src/router.tsx +127 -0
- package/examples/canister-studio/src/routes/__root.tsx +58 -0
- package/examples/canister-studio/src/routes/canister.$canisterId.index.tsx +172 -0
- package/examples/canister-studio/src/routes/canister.$canisterId.interface.tsx +98 -0
- package/examples/canister-studio/src/routes/canister.$canisterId.tsx +91 -0
- package/examples/canister-studio/src/routes/history.tsx +55 -0
- package/examples/canister-studio/src/routes/index.tsx +169 -0
- package/examples/canister-studio/src/routes/playground.tsx +286 -0
- package/examples/canister-studio/src/routes/safety.tsx +57 -0
- package/examples/canister-studio/src/routes/wallet.tsx +213 -0
- package/examples/canister-studio/src/safety/LabTransferForm.tsx +84 -0
- package/examples/canister-studio/src/safety/RequestLog.tsx +79 -0
- package/examples/canister-studio/src/safety/SafetyLab.test.tsx +89 -0
- package/examples/canister-studio/src/safety/SafetyLab.tsx +140 -0
- package/examples/canister-studio/src/safety/ScenarioCard.tsx +262 -0
- package/examples/canister-studio/src/safety/lab-run.ts +283 -0
- package/examples/canister-studio/src/safety/mock-ledger.test.ts +99 -0
- package/examples/canister-studio/src/safety/mock-ledger.ts +263 -0
- package/examples/canister-studio/src/safety/scenarios.test.ts +157 -0
- package/examples/canister-studio/src/safety/scenarios.ts +924 -0
- package/examples/canister-studio/src/styles.css +3453 -0
- package/examples/canister-studio/src/styles.test.ts +46 -0
- package/examples/canister-studio/src/values/CallPreview.test.tsx +86 -0
- package/examples/canister-studio/src/values/CallPreview.tsx +148 -0
- package/examples/canister-studio/src/values/ErrValueView.tsx +66 -0
- package/examples/canister-studio/src/values/Inspector.tsx +133 -0
- package/examples/canister-studio/src/values/ReplyView.test.tsx +341 -0
- package/examples/canister-studio/src/values/ReplyView.tsx +164 -0
- package/examples/canister-studio/src/values/candid-text.test.ts +516 -0
- package/examples/canister-studio/src/values/candid-text.ts +479 -0
- package/examples/canister-studio/src/values/hints.ts +58 -0
- package/examples/canister-studio/src/values/json.test.ts +66 -0
- package/examples/canister-studio/src/values/json.ts +31 -0
- package/examples/canister-studio/src/values/leaves.tsx +309 -0
- package/examples/canister-studio/src/values/schema.test.ts +123 -0
- package/examples/canister-studio/src/values/schema.ts +78 -0
- package/examples/canister-studio/src/values/test-router.tsx +21 -0
- package/examples/canister-studio/src/values/tree.tsx +670 -0
- package/examples/canister-studio/src/values/use-sha256.ts +29 -0
- package/examples/canister-studio/tsconfig.json +20 -0
- package/examples/canister-studio/vite.config.ts +50 -0
- package/examples/canister-studio/vitest.config.ts +20 -0
- package/examples/icrc-ledger/.stackblitzrc +4 -0
- package/examples/icrc-ledger/README.md +100 -0
- package/examples/icrc-ledger/ckbtc_minter.did +12 -0
- package/examples/icrc-ledger/icp_ledger.did +147 -0
- package/examples/icrc-ledger/icrc1.did +88 -0
- package/examples/icrc-ledger/index.html +13 -0
- package/examples/icrc-ledger/package.json +40 -0
- package/examples/icrc-ledger/scripts/smoke.ts +151 -0
- package/examples/icrc-ledger/src/App.tsx +71 -0
- package/examples/icrc-ledger/src/ErrorPanel.tsx +105 -0
- package/examples/icrc-ledger/src/MainnetTab.tsx +450 -0
- package/examples/icrc-ledger/src/SandboxTab.test.tsx +379 -0
- package/examples/icrc-ledger/src/SandboxTab.tsx +761 -0
- package/examples/icrc-ledger/src/blocks.test.ts +247 -0
- package/examples/icrc-ledger/src/blocks.ts +140 -0
- package/examples/icrc-ledger/src/canisters/ckbtc_minter.envelope.json +96 -0
- package/examples/icrc-ledger/src/canisters/ckbtc_minter.ts +16 -0
- package/examples/icrc-ledger/src/canisters/icp_ledger.envelope.json +690 -0
- package/examples/icrc-ledger/src/canisters/icp_ledger.ts +223 -0
- package/examples/icrc-ledger/src/canisters/icrc1.envelope.json +840 -0
- package/examples/icrc-ledger/src/canisters/icrc1.ts +78 -0
- package/examples/icrc-ledger/src/format.test.ts +41 -0
- package/examples/icrc-ledger/src/format.ts +68 -0
- package/examples/icrc-ledger/src/ledger.ts +15 -0
- package/examples/icrc-ledger/src/main.tsx +10 -0
- package/examples/icrc-ledger/src/minter.ts +16 -0
- package/examples/icrc-ledger/src/sandbox.test.ts +464 -0
- package/examples/icrc-ledger/src/sandbox.ts +281 -0
- package/examples/icrc-ledger/src/styles.css +391 -0
- package/examples/icrc-ledger/src/transfer-form.test.ts +62 -0
- package/examples/icrc-ledger/src/transfer-form.ts +97 -0
- package/examples/icrc-ledger/tsconfig.json +19 -0
- package/examples/icrc-ledger/vite.config.ts +44 -0
- package/examples/icrc-ledger/vitest.config.ts +12 -0
- package/examples/next-ssr/.stackblitzrc +4 -0
- package/examples/next-ssr/README.md +112 -0
- package/examples/next-ssr/icrc1.did +88 -0
- package/examples/next-ssr/next.config.ts +10 -0
- package/examples/next-ssr/package.json +41 -0
- package/examples/next-ssr/scripts/smoke.ts +169 -0
- package/examples/next-ssr/src/app/account/CertifiedBalances.tsx +58 -0
- package/examples/next-ssr/src/app/account/account.test.tsx +165 -0
- package/examples/next-ssr/src/app/account/page.tsx +82 -0
- package/examples/next-ssr/src/app/api/balance/[ledger]/[principal]/route.ts +10 -0
- package/examples/next-ssr/src/app/globals.css +278 -0
- package/examples/next-ssr/src/app/layout.tsx +34 -0
- package/examples/next-ssr/src/app/page.tsx +46 -0
- package/examples/next-ssr/src/app/providers.browser.test.tsx +127 -0
- package/examples/next-ssr/src/app/providers.test.tsx +73 -0
- package/examples/next-ssr/src/app/providers.tsx +60 -0
- package/examples/next-ssr/src/canisters/icrc1.envelope.json +840 -0
- package/examples/next-ssr/src/canisters/icrc1.ts +78 -0
- package/examples/next-ssr/src/components/BalanceTable.tsx +43 -0
- package/examples/next-ssr/src/components/LedgerCard.test.tsx +96 -0
- package/examples/next-ssr/src/components/LedgerCard.tsx +230 -0
- package/examples/next-ssr/src/components/LedgerError.tsx +32 -0
- package/examples/next-ssr/src/components/LedgerSections.tsx +22 -0
- package/examples/next-ssr/src/components/MyBalances.test.tsx +253 -0
- package/examples/next-ssr/src/components/MyBalances.tsx +129 -0
- package/examples/next-ssr/src/components/SessionBadge.tsx +18 -0
- package/examples/next-ssr/src/components/hydration.test.tsx +259 -0
- package/examples/next-ssr/src/components/server-data.ts +10 -0
- package/examples/next-ssr/src/format.ts +46 -0
- package/examples/next-ssr/src/ledgers.ts +38 -0
- package/examples/next-ssr/src/server/balance-route.test.ts +151 -0
- package/examples/next-ssr/src/server/balance-route.ts +157 -0
- package/examples/next-ssr/src/server/error-summary.test.ts +34 -0
- package/examples/next-ssr/src/server/error-summary.ts +37 -0
- package/examples/next-ssr/src/server/parse-owner.test.ts +39 -0
- package/examples/next-ssr/src/server/parse-owner.ts +34 -0
- package/examples/next-ssr/src/server/prefetch-ledgers.test.ts +137 -0
- package/examples/next-ssr/src/server/prefetch-ledgers.ts +70 -0
- package/examples/next-ssr/src/server/read-balances.test.ts +103 -0
- package/examples/next-ssr/src/server/read-balances.ts +78 -0
- package/examples/next-ssr/src/server/request-client.rsc.test.tsx +110 -0
- package/examples/next-ssr/src/server/request-client.ts +24 -0
- package/examples/next-ssr/src/testing/mock-ledgers.ts +115 -0
- package/examples/next-ssr/src/testing/react-server-dom-turbopack.d.ts +15 -0
- package/examples/next-ssr/src/testing/setup.ts +10 -0
- package/examples/next-ssr/tsconfig.json +31 -0
- package/examples/next-ssr/vitest.config.ts +59 -0
- package/examples/node-agent-tool/.stackblitzrc +4 -0
- package/examples/node-agent-tool/README.md +165 -0
- package/examples/node-agent-tool/icp.yaml +11 -0
- package/examples/node-agent-tool/icrc1.did +88 -0
- package/examples/node-agent-tool/package.json +32 -0
- package/examples/node-agent-tool/src/canisters/icrc1.envelope.json +840 -0
- package/examples/node-agent-tool/src/canisters/icrc1.ts +78 -0
- package/examples/node-agent-tool/src/cli.test.ts +64 -0
- package/examples/node-agent-tool/src/cli.ts +23 -0
- package/examples/node-agent-tool/src/command-line.ts +159 -0
- package/examples/node-agent-tool/src/commands/balance.ts +73 -0
- package/examples/node-agent-tool/src/commands/demo.test.ts +51 -0
- package/examples/node-agent-tool/src/commands/demo.ts +359 -0
- package/examples/node-agent-tool/src/commands/info.ts +69 -0
- package/examples/node-agent-tool/src/commands/reads.test.ts +133 -0
- package/examples/node-agent-tool/src/commands/resend.test.ts +363 -0
- package/examples/node-agent-tool/src/commands/transfer.test.ts +313 -0
- package/examples/node-agent-tool/src/commands/transfer.ts +415 -0
- package/examples/node-agent-tool/src/commands/watch.test.ts +159 -0
- package/examples/node-agent-tool/src/commands/watch.ts +135 -0
- package/examples/node-agent-tool/src/commands/whoami.ts +36 -0
- package/examples/node-agent-tool/src/context.ts +20 -0
- package/examples/node-agent-tool/src/dispatch.ts +70 -0
- package/examples/node-agent-tool/src/failure.test.ts +59 -0
- package/examples/node-agent-tool/src/failure.ts +176 -0
- package/examples/node-agent-tool/src/identity.test.ts +242 -0
- package/examples/node-agent-tool/src/identity.ts +228 -0
- package/examples/node-agent-tool/src/input.ts +110 -0
- package/examples/node-agent-tool/src/ledgers.test.ts +74 -0
- package/examples/node-agent-tool/src/ledgers.ts +62 -0
- package/examples/node-agent-tool/src/mock-ledger.ts +211 -0
- package/examples/node-agent-tool/src/network.test.ts +128 -0
- package/examples/node-agent-tool/src/network.ts +101 -0
- package/examples/node-agent-tool/src/output.test.ts +208 -0
- package/examples/node-agent-tool/src/output.ts +163 -0
- package/examples/node-agent-tool/src/run.ts +73 -0
- package/examples/node-agent-tool/src/test-kit.ts +167 -0
- package/examples/node-agent-tool/tsconfig.json +18 -0
- package/examples/node-agent-tool/vitest.config.ts +8 -0
- package/examples/vite-wallet/.stackblitzrc +4 -0
- package/examples/vite-wallet/Cargo.lock +646 -0
- package/examples/vite-wallet/Cargo.toml +5 -0
- package/examples/vite-wallet/README.md +107 -0
- package/examples/vite-wallet/backend/Cargo.toml +12 -0
- package/examples/vite-wallet/backend/backend.did +37 -0
- package/examples/vite-wallet/backend/src/lib.rs +133 -0
- package/examples/vite-wallet/icp.yaml +44 -0
- package/examples/vite-wallet/index.html +13 -0
- package/examples/vite-wallet/ledger.did +54 -0
- package/examples/vite-wallet/package.json +46 -0
- package/examples/vite-wallet/public/.ic-assets.json5 +26 -0
- package/examples/vite-wallet/scripts/faucet-args.test.ts +30 -0
- package/examples/vite-wallet/scripts/faucet-args.ts +41 -0
- package/examples/vite-wallet/scripts/faucet.ts +41 -0
- package/examples/vite-wallet/src/AddressBook.test.tsx +191 -0
- package/examples/vite-wallet/src/AddressBook.tsx +147 -0
- package/examples/vite-wallet/src/App.tsx +53 -0
- package/examples/vite-wallet/src/Balance.test.tsx +57 -0
- package/examples/vite-wallet/src/Balance.tsx +58 -0
- package/examples/vite-wallet/src/EnvironmentPanel.test.tsx +83 -0
- package/examples/vite-wallet/src/EnvironmentPanel.tsx +103 -0
- package/examples/vite-wallet/src/ErrorNote.tsx +70 -0
- package/examples/vite-wallet/src/Profile.test.tsx +145 -0
- package/examples/vite-wallet/src/Profile.tsx +92 -0
- package/examples/vite-wallet/src/SendIcp.test.tsx +231 -0
- package/examples/vite-wallet/src/SendIcp.tsx +244 -0
- package/examples/vite-wallet/src/SendToContact.test.tsx +113 -0
- package/examples/vite-wallet/src/SendToContact.tsx +102 -0
- package/examples/vite-wallet/src/SignIn.test.tsx +106 -0
- package/examples/vite-wallet/src/SignIn.tsx +120 -0
- package/examples/vite-wallet/src/Wallet.test.tsx +89 -0
- package/examples/vite-wallet/src/account-switch.test.tsx +108 -0
- package/examples/vite-wallet/src/auth/dev-accounts.test.ts +113 -0
- package/examples/vite-wallet/src/auth/dev-accounts.ts +216 -0
- package/examples/vite-wallet/src/auth/local-page.test.ts +28 -0
- package/examples/vite-wallet/src/auth/local-page.ts +20 -0
- package/examples/vite-wallet/src/auth/wallet-auth.test.ts +159 -0
- package/examples/vite-wallet/src/auth/wallet-auth.ts +131 -0
- package/examples/vite-wallet/src/canisters/backend.envelope.json +275 -0
- package/examples/vite-wallet/src/canisters/backend.ts +58 -0
- package/examples/vite-wallet/src/canisters/ledger.envelope.json +451 -0
- package/examples/vite-wallet/src/canisters/ledger.ts +56 -0
- package/examples/vite-wallet/src/client.ts +45 -0
- package/examples/vite-wallet/src/main.tsx +18 -0
- package/examples/vite-wallet/src/optimistic-contacts.ts +113 -0
- package/examples/vite-wallet/src/refusal.ts +17 -0
- package/examples/vite-wallet/src/styles.css +282 -0
- package/examples/vite-wallet/src/test/test-wallet.tsx +322 -0
- package/examples/vite-wallet/src/token.ts +50 -0
- package/examples/vite-wallet/src/transfer.test.ts +178 -0
- package/examples/vite-wallet/src/transfer.ts +110 -0
- package/examples/vite-wallet/src/use-canisters.ts +49 -0
- package/examples/vite-wallet/tsconfig.json +19 -0
- package/examples/vite-wallet/vite.config.ts +47 -0
- package/examples/vite-wallet/vitest.config.ts +17 -0
- package/package.json +9 -4
- package/packages/core/.size-limit.js +47 -0
- package/packages/core/LICENSE +21 -0
- package/packages/core/README.md +84 -0
- package/packages/core/llms.txt +610 -0
- package/packages/core/package.json +89 -0
- package/packages/core/src/call.ts +961 -0
- package/packages/core/src/canister.ts +259 -0
- package/packages/core/src/client.ts +2061 -0
- package/packages/core/src/errors.ts +1079 -0
- package/packages/core/src/hydration.ts +239 -0
- package/packages/core/src/index.ts +23 -0
- package/packages/core/src/keys.ts +108 -0
- package/packages/core/src/management.ts +138 -0
- package/packages/core/src/network.ts +1020 -0
- package/packages/core/src/options.ts +1000 -0
- package/packages/core/src/runtime.ts +50 -0
- package/packages/core/src/testing/fake-replica.ts +1166 -0
- package/packages/core/src/testing/index.ts +16 -0
- package/packages/core/src/testing/test-auth.ts +340 -0
- package/packages/core/src/testing/test-client.ts +559 -0
- package/packages/core/src/types.ts +541 -0
- package/packages/core/src/units.ts +299 -0
- package/packages/core/src/version.ts +4 -0
- package/packages/core/tests/canister-helpers.ts +187 -0
- package/packages/core/tests/canister-id.test.ts +158 -0
- package/packages/core/tests/canister-unwrap.test.ts +94 -0
- package/packages/core/tests/canister.test-d.ts +406 -0
- package/packages/core/tests/canister.test.ts +1921 -0
- package/packages/core/tests/client-account-change.test.ts +131 -0
- package/packages/core/tests/client-agents.test.ts +402 -0
- package/packages/core/tests/client-auth-network.test.ts +719 -0
- package/packages/core/tests/client-disposed.test.ts +403 -0
- package/packages/core/tests/client-helpers.ts +136 -0
- package/packages/core/tests/client-hydration.test.ts +166 -0
- package/packages/core/tests/client-pinned.test.ts +626 -0
- package/packages/core/tests/client-session.test.ts +348 -0
- package/packages/core/tests/client.test-d.ts +181 -0
- package/packages/core/tests/client.test.ts +1162 -0
- package/packages/core/tests/encode.test.ts +171 -0
- package/packages/core/tests/env-canister-id.test.ts +666 -0
- package/packages/core/tests/env-cookie-copies.test.ts +182 -0
- package/packages/core/tests/error-guard.test.ts +222 -0
- package/packages/core/tests/errors.test-d.ts +280 -0
- package/packages/core/tests/errors.test.ts +1359 -0
- package/packages/core/tests/fixtures/README.md +28 -0
- package/packages/core/tests/fixtures/accounts.did +21 -0
- package/packages/core/tests/fixtures/accounts.envelope.json +367 -0
- package/packages/core/tests/fixtures/accounts.ts +26 -0
- package/packages/core/tests/fixtures/archive.did +31 -0
- package/packages/core/tests/fixtures/archive.envelope.json +259 -0
- package/packages/core/tests/fixtures/archive.ts +55 -0
- package/packages/core/tests/fixtures/icrc1.did +88 -0
- package/packages/core/tests/fixtures/icrc1.envelope.json +840 -0
- package/packages/core/tests/fixtures/icrc1.ts +78 -0
- package/packages/core/tests/fixtures/management.did +724 -0
- package/packages/core/tests/fixtures/management.envelope.json +4455 -0
- package/packages/core/tests/fixtures/management.ts +486 -0
- package/packages/core/tests/fixtures/shapes.did +33 -0
- package/packages/core/tests/fixtures/shapes.envelope.json +440 -0
- package/packages/core/tests/fixtures/shapes.ts +56 -0
- package/packages/core/tests/fixtures/skippable.did +15 -0
- package/packages/core/tests/fixtures/skippable.envelope.json +182 -0
- package/packages/core/tests/fixtures/skippable.ts +33 -0
- package/packages/core/tests/func.test.ts +154 -0
- package/packages/core/tests/identity-attributes.test.ts +214 -0
- package/packages/core/tests/keys.test.ts +289 -0
- package/packages/core/tests/management.test.ts +206 -0
- package/packages/core/tests/network-helpers.ts +121 -0
- package/packages/core/tests/network.test-d.ts +78 -0
- package/packages/core/tests/network.test.ts +1051 -0
- package/packages/core/tests/options.test.ts +1594 -0
- package/packages/core/tests/redirect-sign-in.test.ts +393 -0
- package/packages/core/tests/resend.test-d.ts +81 -0
- package/packages/core/tests/resend.test.ts +723 -0
- package/packages/core/tests/skip.test-d.ts +125 -0
- package/packages/core/tests/testing-entry.test.ts +73 -0
- package/packages/core/tests/testing-fake-replica.test.ts +1242 -0
- package/packages/core/tests/testing-test-auth.test-d.ts +84 -0
- package/packages/core/tests/testing-test-auth.test.ts +483 -0
- package/packages/core/tests/testing-test-client.test-d.ts +184 -0
- package/packages/core/tests/testing-test-client.test.ts +1272 -0
- package/packages/core/tests/time-realm.test.ts +63 -0
- package/packages/core/tests/time.test.ts +238 -0
- package/packages/core/tests/traps/README.md +39 -0
- package/packages/core/tests/traps/anonymous-write.test.ts +145 -0
- package/packages/core/tests/traps/classification-table.test.ts +256 -0
- package/packages/core/tests/traps/err-arm.test.ts +144 -0
- package/packages/core/tests/traps/fetch-spy.ts +39 -0
- package/packages/core/tests/traps/invalidation-after-write.test.ts +178 -0
- package/packages/core/tests/traps/ledger.ts +248 -0
- package/packages/core/tests/traps/root-key-off-local.test.ts +130 -0
- package/packages/core/tests/traps/stale-data-across-users.test.ts +173 -0
- package/packages/core/tests/traps/unresolved-name.test.ts +155 -0
- package/packages/core/tests/traps/update-as-query.test.ts +244 -0
- package/packages/core/tests/traps/update-not-resent.test.ts +126 -0
- package/packages/core/tests/traps/update-resent-when-never-delivered.test.ts +159 -0
- package/packages/core/tests/traps.test-d.ts +476 -0
- package/packages/core/tests/units.test-d.ts +118 -0
- package/packages/core/tests/units.test.ts +634 -0
- package/packages/core/tsconfig.json +19 -0
- package/packages/core/tsconfig.typecheck.json +16 -0
- package/packages/core/vitest.config.ts +13 -0
- package/packages/mcp/LICENSE +21 -0
- package/packages/mcp/README.md +189 -0
- package/packages/mcp/package.json +62 -0
- package/packages/mcp/src/candid/candid-text.test.ts +455 -0
- package/packages/mcp/src/candid/candid-text.ts +501 -0
- package/packages/mcp/src/candid/contract.test.ts +398 -0
- package/packages/mcp/src/candid/formats.ts +155 -0
- package/packages/mcp/src/candid/hex.ts +29 -0
- package/packages/mcp/src/candid/json-schema.test.ts +758 -0
- package/packages/mcp/src/candid/json-schema.ts +454 -0
- package/packages/mcp/src/candid/json.test.ts +1207 -0
- package/packages/mcp/src/candid/json.ts +1053 -0
- package/packages/mcp/src/candid/schema-walk.ts +189 -0
- package/packages/mcp/src/candid/type-text.test.ts +179 -0
- package/packages/mcp/src/candid/type-text.ts +238 -0
- package/packages/mcp/src/cli.ts +134 -0
- package/packages/mcp/src/identity.ts +188 -0
- package/packages/mcp/src/interface/cache.test.ts +48 -0
- package/packages/mcp/src/interface/cache.ts +68 -0
- package/packages/mcp/src/interface/cli-cache.test.ts +150 -0
- package/packages/mcp/src/interface/cli-cache.ts +122 -0
- package/packages/mcp/src/interface/compile-worker.test.ts +246 -0
- package/packages/mcp/src/interface/compile-worker.ts +290 -0
- package/packages/mcp/src/interface/compile.test.ts +155 -0
- package/packages/mcp/src/interface/compile.ts +281 -0
- package/packages/mcp/src/interface/docs.test.ts +105 -0
- package/packages/mcp/src/interface/docs.ts +86 -0
- package/packages/mcp/src/interface/fixtures/icrc1.did +88 -0
- package/packages/mcp/src/interface/hex.ts +8 -0
- package/packages/mcp/src/interface/ids.test.ts +71 -0
- package/packages/mcp/src/interface/ids.ts +87 -0
- package/packages/mcp/src/interface/loader.test.ts +804 -0
- package/packages/mcp/src/interface/loader.ts +519 -0
- package/packages/mcp/src/interface/read-candid.test.ts +300 -0
- package/packages/mcp/src/interface/read-candid.ts +259 -0
- package/packages/mcp/src/interface/types.ts +113 -0
- package/packages/mcp/src/network.ts +122 -0
- package/packages/mcp/src/options.ts +149 -0
- package/packages/mcp/src/server/approval.ts +159 -0
- package/packages/mcp/src/server/naming.ts +43 -0
- package/packages/mcp/src/server/results.ts +102 -0
- package/packages/mcp/src/server/server.ts +797 -0
- package/packages/mcp/src/server/tools.ts +116 -0
- package/packages/mcp/src/usage.ts +40 -0
- package/packages/mcp/tests/approval.test.ts +60 -0
- package/packages/mcp/tests/identity.test.ts +234 -0
- package/packages/mcp/tests/naming.test.ts +62 -0
- package/packages/mcp/tests/options.test.ts +140 -0
- package/packages/mcp/tests/server.test.ts +997 -0
- package/packages/mcp/tsconfig.json +19 -0
- package/packages/mcp/tsconfig.typecheck.json +10 -0
- package/packages/mcp/vitest.config.ts +10 -0
- package/packages/react/LICENSE +21 -0
- package/packages/react/README.md +328 -0
- package/packages/react/llms.txt +95 -0
- package/packages/react/package.json +95 -0
- package/packages/react/src/index.tsx +678 -0
- package/packages/react/tests/abandoned-render.test.tsx +816 -0
- package/packages/react/tests/auth.test.tsx +315 -0
- package/packages/react/tests/caller-keys.test.tsx +2334 -0
- package/packages/react/tests/client-time.test.ts +40 -0
- package/packages/react/tests/entry.test.ts +52 -0
- package/packages/react/tests/helpers.tsx +291 -0
- package/packages/react/tests/hydration.test.tsx +262 -0
- package/packages/react/tests/ownership.test.tsx +260 -0
- package/packages/react/tests/provider.server.test.tsx +169 -0
- package/packages/react/tests/query-client.test.tsx +107 -0
- package/packages/react/tests/strict-mode.test.tsx +288 -0
- package/packages/react/tests/suspense.test-d.tsx +357 -0
- package/packages/react/tests/two-query-cores.test.tsx +135 -0
- package/packages/react/tests/types.test-d.tsx +63 -0
- package/packages/react/tsconfig.json +23 -0
- package/packages/react/tsconfig.test.json +8 -0
- package/packages/react/tsconfig.typecheck.json +16 -0
- package/packages/react/vitest.config.ts +39 -0
- package/packages/vite-plugin/LICENSE +21 -0
- package/packages/vite-plugin/README.md +532 -0
- package/packages/vite-plugin/package.json +76 -0
- package/packages/vite-plugin/src/describe-value.test.ts +43 -0
- package/packages/vite-plugin/src/describe-value.ts +167 -0
- package/packages/vite-plugin/src/dev-environment.test.ts +747 -0
- package/packages/vite-plugin/src/dev-environment.ts +510 -0
- package/packages/vite-plugin/src/env.ts +431 -0
- package/packages/vite-plugin/src/fetch.ts +348 -0
- package/packages/vite-plugin/src/file-identity.ts +106 -0
- package/packages/vite-plugin/src/generate.ts +691 -0
- package/packages/vite-plugin/src/index.test.ts +1243 -0
- package/packages/vite-plugin/src/index.ts +2565 -0
- package/packages/vite-plugin/src/interface.ts +339 -0
- package/packages/vite-plugin/src/local-host.ts +34 -0
- package/packages/vite-plugin/src/options.ts +641 -0
- package/packages/vite-plugin/src/principal-text.test.ts +87 -0
- package/packages/vite-plugin/src/principal-text.ts +98 -0
- package/packages/vite-plugin/tests/close.test.ts +141 -0
- package/packages/vite-plugin/tests/environment-mode.test.ts +136 -0
- package/packages/vite-plugin/tests/fake-ic.ts +214 -0
- package/packages/vite-plugin/tests/fetch-sdk.test.ts +27 -0
- package/packages/vite-plugin/tests/fetch.test.ts +1196 -0
- package/packages/vite-plugin/tests/fixtures/fake-cli/bin.js +90 -0
- package/packages/vite-plugin/tests/fixtures/fake-cli/package.json +8 -0
- package/packages/vite-plugin/tests/fixtures/icrc1.did +88 -0
- package/packages/vite-plugin/tests/generation.test.ts +1036 -0
- package/packages/vite-plugin/tests/icp-command.test.ts +194 -0
- package/packages/vite-plugin/tests/icp-timeout.test.ts +83 -0
- package/packages/vite-plugin/tests/no-process-exit.test.ts +83 -0
- package/packages/vite-plugin/tests/on-generated.test.ts +323 -0
- package/packages/vite-plugin/tests/output-collision.test.ts +329 -0
- package/packages/vite-plugin/tests/package.test.ts +62 -0
- package/packages/vite-plugin/tests/project.test.ts +2993 -0
- package/packages/vite-plugin/tests/support.ts +339 -0
- package/packages/vite-plugin/tests/timeout.test.ts +107 -0
- package/packages/vite-plugin/tsconfig.json +23 -0
- package/packages/vite-plugin/tsconfig.typecheck.json +16 -0
- package/packages/vite-plugin/vitest.config.ts +9 -0
- package/pnpm-workspace.yaml +23 -0
- package/scripts/ai-context-files.js +72 -0
- package/scripts/assemble-docs-site.mjs +485 -0
- package/scripts/assemble-docs-site.test.mjs +568 -0
- package/scripts/build-examples.js +58 -0
- package/scripts/check-ai-context.js +963 -0
- package/scripts/check-ai-context.test.mjs +811 -0
- package/scripts/check-exports.mjs +767 -0
- package/scripts/check-exports.test.mjs +1136 -0
- package/scripts/check-snippets/ambient.d.ts +428 -0
- package/scripts/check-snippets/app/generated/backend.did +7 -0
- package/scripts/check-snippets/app/generated/backend.ts +15 -0
- package/scripts/check-snippets/app/generated/icrc1.did +88 -0
- package/scripts/check-snippets/app/generated/icrc1.ts +78 -0
- package/scripts/check-snippets/app/globals.ts +26 -0
- package/scripts/check-snippets/docs/guides/App.tsx +21 -0
- package/scripts/check-snippets/docs/guides/app/providers.tsx +23 -0
- package/scripts/check-snippets/docs/guides/auth-client.ts +47 -0
- package/scripts/check-snippets/docs/guides/canisters/backend.ts +3 -0
- package/scripts/check-snippets/docs/guides/canisters/icrc1.ts +3 -0
- package/scripts/check-snippets/docs/guides/components/LedgerCard.tsx +26 -0
- package/scripts/check-snippets/docs/guides/components/shell.tsx +11 -0
- package/scripts/check-snippets/docs/guides/components/sign-in-page.tsx +29 -0
- package/scripts/check-snippets/docs/guides/components/slow-section.tsx +5 -0
- package/scripts/check-snippets/docs/guides/generated/accounts.ts +95 -0
- package/scripts/check-snippets/docs/guides/generated/ckbtc_minter.ts +40 -0
- package/scripts/check-snippets/docs/guides/generated/icp_ledger.ts +267 -0
- package/scripts/check-snippets/docs/guides/ledger.ts +10 -0
- package/scripts/check-snippets/docs/guides/server/request-client.ts +8 -0
- package/scripts/check-snippets.mjs +993 -0
- package/scripts/check-snippets.test.mjs +338 -0
- package/scripts/check-stack.mjs +501 -0
- package/scripts/check-stack.test.mjs +800 -0
- package/scripts/clean-node-modules.js +67 -0
- package/scripts/example-projects.js +58 -0
- package/scripts/export-budget.mjs +145 -0
- package/scripts/faults.json +6749 -0
- package/scripts/lib/fault-workspace.mjs +210 -0
- package/scripts/lib/io.mjs +29 -0
- package/scripts/lib/io.test.mjs +38 -0
- package/scripts/lib/markdown-fences.mjs +56 -0
- package/scripts/lib/markdown-fences.test.mjs +84 -0
- package/scripts/lib/package-exports.mjs +54 -0
- package/scripts/lib/package-exports.test.mjs +66 -0
- package/scripts/lib/release-pr-steps.mjs +47 -0
- package/scripts/release-tag.mjs +95 -0
- package/scripts/release-tag.test.mjs +278 -0
- package/scripts/release-workflow.test.mjs +98 -0
- package/scripts/release.js +167 -0
- package/scripts/release.test.mjs +347 -0
- package/scripts/removed-v3-names.js +125 -0
- package/scripts/size-app/.size-limit.js +90 -0
- package/scripts/size-app/core.js +3 -0
- package/scripts/size-app/package.json +24 -0
- package/scripts/size-app/react.js +4 -0
- package/scripts/size-app.test.mjs +73 -0
- package/scripts/sync-ai-context-versions.js +102 -0
- package/scripts/sync-example-versions.js +151 -0
- package/scripts/test-examples-published.mjs +1585 -0
- package/scripts/test-examples-published.test.mjs +1586 -0
- package/scripts/traps/any-method-vars-not-a-list.json +5 -0
- package/scripts/traps/canister-method-unknown.json +5 -0
- package/scripts/traps/client-says-who-calls-once.json +5 -0
- package/scripts/traps/client-says-who-calls.json +5 -0
- package/scripts/traps/err-only-on-canister-err.json +14 -0
- package/scripts/traps/format-units-refuses-missing-value.json +5 -0
- package/scripts/traps/format-units-refuses-number.json +5 -0
- package/scripts/traps/format-units-unknown-option.json +5 -0
- package/scripts/traps/guard-err-is-the-methods.json +5 -0
- package/scripts/traps/guard-false-keeps-every-kind.json +5 -0
- package/scripts/traps/internals-not-exported.json +9 -0
- package/scripts/traps/network-refuses-unknown-name.json +5 -0
- package/scripts/traps/never-err-has-no-type.json +5 -0
- package/scripts/traps/options-method-unknown.json +5 -0
- package/scripts/traps/options-vars-wrong-type.json +5 -0
- package/scripts/traps/parse-units-refuses-number.json +5 -0
- package/scripts/traps/parse-units-unknown-option.json +5 -0
- package/scripts/traps/principal-is-not-plain-text.json +14 -0
- package/scripts/traps/query-options-refuses-oneway.json +5 -0
- package/scripts/traps/query-options-refuses-update.json +5 -0
- package/scripts/traps/refuse-next-unknown-option.json +5 -0
- package/scripts/traps/resend-key-reads-the-methods-arg.json +5 -0
- package/scripts/traps/resend-states-its-dedupe-key.json +5 -0
- package/scripts/traps/suspense-refuses-argument-skip.json +5 -0
- package/scripts/traps/suspense-refuses-skippable-read.json +5 -0
- package/scripts/traps/suspense-refuses-vars-skip-fits.json +5 -0
- package/scripts/traps/test-handler-method-unknown.json +5 -0
- package/scripts/traps/test-handler-nat-is-bigint.json +5 -0
- package/scripts/traps/test-handler-takes-the-domain-value.json +5 -0
- package/scripts/traps/vars-of-two-arguments-are-the-tuple.json +5 -0
- package/scripts/typecheck-examples.js +122 -0
- package/scripts/verify-faults.mjs +375 -0
- package/scripts/verify-faults.test.mjs +402 -0
- package/scripts/verify-package-artifacts.mjs +590 -0
- package/scripts/verify-peer-floors.js +399 -0
- package/scripts/verify-peer-floors.test.mjs +422 -0
- package/scripts/verify-test-fails-rules.mjs +104 -0
- package/scripts/verify-test-fails-rules.test.mjs +215 -0
- package/scripts/verify-test-fails.js +371 -0
- package/scripts/verify-traps.mjs +505 -0
- package/scripts/verify-traps.test.mjs +521 -0
- package/scripts/verify-typescript-floor.js +239 -0
- package/skill-packages/ic-reactor/SKILL.md +103 -0
- package/stack.json +8 -0
- package/tsconfig.base.json +23 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,1166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fake replica, reached through the `fetch` an `HttpAgent` is given.
|
|
3
|
+
*
|
|
4
|
+
* {@link createFakeReplica} returns a `fetch` to pass as
|
|
5
|
+
* `HttpAgentOptions.fetch`, so the agents that use it are the only code that
|
|
6
|
+
* reaches the fake and nothing global is touched.
|
|
7
|
+
*
|
|
8
|
+
* It holds its own root key and node key, and signs what a replica signs: a
|
|
9
|
+
* BLS signature over each certificate and a node signature over each query
|
|
10
|
+
* response. The agent verifies both exactly as it would against a replica, so
|
|
11
|
+
* nothing in the agent is mocked, and an agent that checks a certificate
|
|
12
|
+
* against the wrong root key fails here as it would there.
|
|
13
|
+
*
|
|
14
|
+
* Like a replica, it checks who sent each request before any canister sees it,
|
|
15
|
+
* so the caller a canister is told about is the one a replica would report.
|
|
16
|
+
*
|
|
17
|
+
* It implements the endpoints an agent uses for canister calls and nothing
|
|
18
|
+
* else: the status endpoint (the root key), and a canister's `query`, `call`
|
|
19
|
+
* (which always answers synchronously) and `read_state` (the subnet's node
|
|
20
|
+
* keys and canister ranges, which an agent reads before it trusts a query).
|
|
21
|
+
*
|
|
22
|
+
* Three fault hooks reproduce what a real network does to a call: a canister
|
|
23
|
+
* that rejects it with a chosen reject code, a reply that is lost after the
|
|
24
|
+
* canister ran, and an HTTP refusal before any canister sees the call.
|
|
25
|
+
*/
|
|
26
|
+
import { bls12_381 } from "@noble/curves/bls12-381.js"
|
|
27
|
+
import { ed25519 } from "@noble/curves/ed25519.js"
|
|
28
|
+
import { p256 } from "@noble/curves/nist.js"
|
|
29
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js"
|
|
30
|
+
import {
|
|
31
|
+
BLS12_381_G2_OID,
|
|
32
|
+
Cbor,
|
|
33
|
+
IC_REQUEST_AUTH_DELEGATION_DOMAIN_SEPARATOR,
|
|
34
|
+
IC_REQUEST_DOMAIN_SEPARATOR,
|
|
35
|
+
IC_RESPONSE_DOMAIN_SEPARATOR,
|
|
36
|
+
IC_STATE_ROOT_DOMAIN_SEPARATOR,
|
|
37
|
+
NodeType,
|
|
38
|
+
ReplicaRejectCode,
|
|
39
|
+
hashOfMap,
|
|
40
|
+
reconstruct,
|
|
41
|
+
requestIdOf,
|
|
42
|
+
wrapDER,
|
|
43
|
+
type HashTree,
|
|
44
|
+
} from "@icp-sdk/core/agent"
|
|
45
|
+
import { Ed25519KeyIdentity } from "@icp-sdk/core/identity"
|
|
46
|
+
import { Principal } from "@icp-sdk/core/principal"
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether a page on this hostname talks to a replica it serves itself: a
|
|
50
|
+
* local host (`localhost`, its subdomains, all of 127.0.0.0/8, `::1`) or a
|
|
51
|
+
* Codespaces/Gitpod forward. Kept from v3's `getNetworkByHostname` so the
|
|
52
|
+
* fake routes a page's same-origin requests as it always did.
|
|
53
|
+
*/
|
|
54
|
+
const isNonMainnetPageHost = (hostname: string): boolean =>
|
|
55
|
+
hostname === "localhost" ||
|
|
56
|
+
hostname.endsWith(".localhost") ||
|
|
57
|
+
hostname.endsWith("127.0.0.1") ||
|
|
58
|
+
/^127\.(?:\d{1,3}\.){2}\d{1,3}$/.test(hostname) ||
|
|
59
|
+
hostname === "::1" ||
|
|
60
|
+
hostname === "[::1]" ||
|
|
61
|
+
hostname.endsWith(".github.dev") ||
|
|
62
|
+
hostname.endsWith(".gitpod.io")
|
|
63
|
+
|
|
64
|
+
/** Who sent a call, as the fake replica reports it to a canister. */
|
|
65
|
+
export interface FakeCallContext {
|
|
66
|
+
/**
|
|
67
|
+
* The principal that sent the call: the anonymous principal, or the one
|
|
68
|
+
* the signing identity names (the root of its delegation chain, if it has
|
|
69
|
+
* one), after the fake has checked the request's signatures.
|
|
70
|
+
*/
|
|
71
|
+
readonly caller: Principal
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A canister the fake replica routes calls to, by method name and raw Candid.
|
|
76
|
+
*
|
|
77
|
+
* Each handler receives the method name, the Candid-encoded argument and the
|
|
78
|
+
* call context, and returns the Candid-encoded reply. A handler that throws
|
|
79
|
+
* rejects the call as a canister trap does, and one that calls
|
|
80
|
+
* {@link FakeReplica.reject} rejects it with the reject code it names. A
|
|
81
|
+
* canister without `query` rejects every query, and one without `update`
|
|
82
|
+
* every update call.
|
|
83
|
+
*/
|
|
84
|
+
export interface FakeCanister {
|
|
85
|
+
query?(
|
|
86
|
+
method: string,
|
|
87
|
+
arg: Uint8Array,
|
|
88
|
+
context: FakeCallContext
|
|
89
|
+
): Uint8Array | Promise<Uint8Array>
|
|
90
|
+
update?(
|
|
91
|
+
method: string,
|
|
92
|
+
arg: Uint8Array,
|
|
93
|
+
context: FakeCallContext
|
|
94
|
+
): Uint8Array | Promise<Uint8Array>
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A request an agent sent to the fake replica. */
|
|
98
|
+
export interface FakeReplicaRequest {
|
|
99
|
+
readonly endpoint: "status" | "query" | "call" | "read_state"
|
|
100
|
+
/** The canister the request was addressed to, as text. */
|
|
101
|
+
readonly canisterId?: string
|
|
102
|
+
/**
|
|
103
|
+
* The effective canister id the agent put in the request path
|
|
104
|
+
* (`/api/v3/canister/<id>/call`), as text. It is the canister the request is
|
|
105
|
+
* routed by, and it differs from {@link FakeReplicaRequest.canisterId} when
|
|
106
|
+
* a call goes to the management canister (`aaaaa-aa`), where the client
|
|
107
|
+
* chooses it from the call's arguments. Set on `query`, `call` and
|
|
108
|
+
* `read_state` requests.
|
|
109
|
+
*/
|
|
110
|
+
readonly effectiveCanisterId?: string
|
|
111
|
+
/** The canister method a query or call named. */
|
|
112
|
+
readonly methodName?: string
|
|
113
|
+
/** The principal the request came from, as text, once it was checked. */
|
|
114
|
+
readonly caller?: string
|
|
115
|
+
/**
|
|
116
|
+
* Why the fake refused the request before any canister saw it: either the
|
|
117
|
+
* signature, a delegation, or its targets did not check out, as a replica
|
|
118
|
+
* refuses it (HTTP 400), or {@link FakeReplica.refuseNext} told the fake to
|
|
119
|
+
* answer with an HTTP status.
|
|
120
|
+
*/
|
|
121
|
+
readonly refused?: string
|
|
122
|
+
/**
|
|
123
|
+
* Whether the reply to this request was lost by
|
|
124
|
+
* {@link FakeReplica.dropNextReply}: the agent saw a network failure. The
|
|
125
|
+
* first send of a dropped call ran the canister; a re-send of it did not.
|
|
126
|
+
*/
|
|
127
|
+
readonly dropped?: true
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Options for {@link createFakeReplica}. */
|
|
131
|
+
export interface FakeReplicaOptions {
|
|
132
|
+
/**
|
|
133
|
+
* The origin the fake answers for. Point the agents under test at it, for
|
|
134
|
+
* example with `HttpAgent.createSync({ host: replica.host, ... })`.
|
|
135
|
+
*
|
|
136
|
+
* @defaultValue The page's origin when the test environment has a local one
|
|
137
|
+
* (`http://localhost:3000` in Vitest's jsdom and happy-dom), which is where
|
|
138
|
+
* an agent built with no `host` calls, and `"http://127.0.0.1:8000"`
|
|
139
|
+
* (the client's `"local"` network) otherwise.
|
|
140
|
+
*/
|
|
141
|
+
host?: string
|
|
142
|
+
/**
|
|
143
|
+
* The canisters the fake runs, by canister ID. A call to any other
|
|
144
|
+
* canister is rejected, as a replica rejects a call to a canister that
|
|
145
|
+
* does not exist.
|
|
146
|
+
*/
|
|
147
|
+
canisters?: Record<string, FakeCanister>
|
|
148
|
+
/**
|
|
149
|
+
* The time its certificates carry, in nanoseconds since the epoch: the
|
|
150
|
+
* IC's clock as an agent reads it from the `time` path. Internal, for the
|
|
151
|
+
* tests of `client.time()`; every other check the fake makes reads the
|
|
152
|
+
* host's clock.
|
|
153
|
+
*
|
|
154
|
+
* @defaultValue The host's clock.
|
|
155
|
+
*/
|
|
156
|
+
now?: () => bigint
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The reject codes a canister call can end with, as the Internet Computer
|
|
161
|
+
* interface specification numbers them: `1` SYS_FATAL, `2` SYS_TRANSIENT,
|
|
162
|
+
* `3` DESTINATION_INVALID, `4` CANISTER_REJECT, `5` CANISTER_ERROR and `6`
|
|
163
|
+
* SYS_UNKNOWN.
|
|
164
|
+
*/
|
|
165
|
+
export type FakeRejectCode = 1 | 2 | 3 | 4 | 5 | 6
|
|
166
|
+
|
|
167
|
+
/** A fake replica made by {@link createFakeReplica}. */
|
|
168
|
+
export interface FakeReplica {
|
|
169
|
+
/** The origin the fake answers for. */
|
|
170
|
+
readonly host: string
|
|
171
|
+
/**
|
|
172
|
+
* The DER-encoded root key the fake signs certificates with. Each fake has
|
|
173
|
+
* its own. Pass it as `rootKey` to an agent that does not fetch its root
|
|
174
|
+
* key, or let the agent fetch it from the fake with `shouldFetchRootKey`.
|
|
175
|
+
*/
|
|
176
|
+
readonly rootKey: Uint8Array
|
|
177
|
+
/**
|
|
178
|
+
* The `fetch` to give an agent, as `HttpAgent.createSync({ host, fetch,
|
|
179
|
+
* rootKey })`. It answers the IC API on {@link FakeReplica.host} and
|
|
180
|
+
* refuses the IC API on any other origin with a network error, which it
|
|
181
|
+
* also logs once, so an agent built for the wrong host fails at once. It
|
|
182
|
+
* does not touch `globalThis.fetch`.
|
|
183
|
+
*/
|
|
184
|
+
readonly fetch: typeof globalThis.fetch
|
|
185
|
+
/** Every request an agent sent to the fake, in order. */
|
|
186
|
+
readonly requests: readonly FakeReplicaRequest[]
|
|
187
|
+
/**
|
|
188
|
+
* Runs a canister at `canisterId`, replacing the one already there. For a
|
|
189
|
+
* canister that is not known when the fake is created.
|
|
190
|
+
*/
|
|
191
|
+
addCanister(canisterId: string, canister: FakeCanister): void
|
|
192
|
+
/**
|
|
193
|
+
* Rejects the call being handled with `code`, as a canister does: call it
|
|
194
|
+
* from inside a canister handler. An update is rejected in a certificate,
|
|
195
|
+
* as a replica certifies a reject, and a query is rejected in the signed
|
|
196
|
+
* query response.
|
|
197
|
+
*
|
|
198
|
+
* @param code - The reject code the agent receives.
|
|
199
|
+
* @param message - The reject message. A default naming the code is used
|
|
200
|
+
* when it is left out.
|
|
201
|
+
*/
|
|
202
|
+
reject(code: FakeRejectCode, message?: string): never
|
|
203
|
+
/**
|
|
204
|
+
* Loses the reply to the next update call. The canister runs and its state
|
|
205
|
+
* changes, but the agent sees a network failure instead of the answer, so
|
|
206
|
+
* it cannot know the call happened.
|
|
207
|
+
*
|
|
208
|
+
* The request is remembered, and every send of it, and every `read_state`
|
|
209
|
+
* of its status, fails as a lost connection without running the canister
|
|
210
|
+
* again. The agent's own retries, which re-send the same request, therefore
|
|
211
|
+
* cannot recover the reply and cannot run the call twice, and the agent
|
|
212
|
+
* cannot learn the outcome by polling. A real replica would still answer a
|
|
213
|
+
* `read_state` for a request id it holds: the fake models a network
|
|
214
|
+
* partition for that one request id, so the call stays outcome-unknown.
|
|
215
|
+
*
|
|
216
|
+
* With no argument it loses the reply of whatever call request comes next,
|
|
217
|
+
* a certified read or an update read included. Given {@link FakeRefusal}
|
|
218
|
+
* options, it loses only the replies of the next `times` call requests that
|
|
219
|
+
* name `method` and are addressed to `canister` (each one that is given),
|
|
220
|
+
* and lets every other request through, with the matching rules of
|
|
221
|
+
* {@link FakeReplica.refuseNext}. A request that several armed drops match
|
|
222
|
+
* uses up the one armed first.
|
|
223
|
+
*
|
|
224
|
+
* @param which - Which call requests to lose the reply of, and how many.
|
|
225
|
+
*/
|
|
226
|
+
dropNextReply(which?: FakeRefusal): void
|
|
227
|
+
/**
|
|
228
|
+
* Answers the next `times` canister requests (a `query` or a `call`) with an
|
|
229
|
+
* HTTP error `status` before any canister sees them, as a gateway or a
|
|
230
|
+
* replica does when it throttles or refuses a request. The status endpoint
|
|
231
|
+
* and `read_state`, which an agent makes on its own, are not counted.
|
|
232
|
+
*
|
|
233
|
+
* An agent retries a refused request on its own unless it is built with
|
|
234
|
+
* `retryTimes: 0`; `times` is how many sends to refuse, so a test can refuse
|
|
235
|
+
* the first and let a retry through.
|
|
236
|
+
*
|
|
237
|
+
* Given {@link FakeRefusal} options instead of a count, it refuses only the
|
|
238
|
+
* requests that name `method` and are addressed to `canister` (each one
|
|
239
|
+
* that is given), and lets every other request through. A request that
|
|
240
|
+
* several armed refusals match is refused by the one armed first.
|
|
241
|
+
*
|
|
242
|
+
* @param status - An HTTP error status, from 400 to 599.
|
|
243
|
+
* @param times - How many requests to refuse, or which ones and how many.
|
|
244
|
+
* @defaultValue 1
|
|
245
|
+
*/
|
|
246
|
+
refuseNext(status: number, times?: number | FakeRefusal): void
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Which canister requests {@link FakeReplica.refuseNext} refuses, and how
|
|
251
|
+
* many. A request matches when it names `method` and is addressed to
|
|
252
|
+
* `canister`, each when it is given: options with neither match any query or
|
|
253
|
+
* call, as a count does.
|
|
254
|
+
*/
|
|
255
|
+
export interface FakeRefusal {
|
|
256
|
+
/**
|
|
257
|
+
* The method a matching `query` or `call` names. A query and a replicated
|
|
258
|
+
* call of the same method both match.
|
|
259
|
+
*/
|
|
260
|
+
readonly method?: string
|
|
261
|
+
/**
|
|
262
|
+
* The canister a matching request is addressed to, as text: the canister
|
|
263
|
+
* the request names, not the effective canister it is routed by, so
|
|
264
|
+
* `aaaaa-aa` for a call to the management canister. It need not run on
|
|
265
|
+
* the fake: a gateway refuses a request for a canister that does not exist.
|
|
266
|
+
*/
|
|
267
|
+
readonly canister?: string
|
|
268
|
+
/**
|
|
269
|
+
* How many matching requests to refuse: each send counts, an agent's own
|
|
270
|
+
* re-send of a refused request included.
|
|
271
|
+
*
|
|
272
|
+
* @defaultValue 1
|
|
273
|
+
*/
|
|
274
|
+
readonly times?: number
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** The options of {@link FakeRefusal}, for the message of one that is not. */
|
|
278
|
+
const REFUSAL_OPTIONS = ["method", "canister", "times"] as const
|
|
279
|
+
|
|
280
|
+
/** An armed fault: the requests it matches, and how many it has left. */
|
|
281
|
+
interface Armed {
|
|
282
|
+
readonly method: string | undefined
|
|
283
|
+
readonly canister: string | undefined
|
|
284
|
+
left: number
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Checks the options of `refuseNext` or `dropNextReply`, and returns what
|
|
289
|
+
* they match, with the canister id in its canonical text.
|
|
290
|
+
*/
|
|
291
|
+
function readMatch(call: string, options: FakeRefusal): Armed {
|
|
292
|
+
if (
|
|
293
|
+
typeof options !== "object" ||
|
|
294
|
+
options === null ||
|
|
295
|
+
Array.isArray(options)
|
|
296
|
+
) {
|
|
297
|
+
throw new TypeError(
|
|
298
|
+
`${call}() takes { ${REFUSAL_OPTIONS.join(", ")} }, got ${typeof options}.`
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
const unknown = Object.keys(options).filter(
|
|
302
|
+
(key) => !(REFUSAL_OPTIONS as readonly string[]).includes(key)
|
|
303
|
+
)
|
|
304
|
+
if (unknown.length > 0) {
|
|
305
|
+
throw new TypeError(
|
|
306
|
+
`${call}() has no option ${unknown.join(", ")}. Its options: ${REFUSAL_OPTIONS.join(", ")}.`
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
const { method, canister, times = 1 } = options
|
|
310
|
+
if (!Number.isInteger(times) || times < 1) {
|
|
311
|
+
throw new RangeError(
|
|
312
|
+
`${call}: \`times\` is ${times}, not a count of at least 1`
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
if (method !== undefined && (typeof method !== "string" || !method)) {
|
|
316
|
+
throw new TypeError(
|
|
317
|
+
`${call}: \`method\` is the name of a canister method, got ${
|
|
318
|
+
typeof method === "string" ? '""' : typeof method
|
|
319
|
+
}`
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
let canisterId: string | undefined
|
|
323
|
+
if (canister !== undefined) {
|
|
324
|
+
try {
|
|
325
|
+
canisterId = Principal.fromText(canister).toText()
|
|
326
|
+
} catch {
|
|
327
|
+
throw new TypeError(
|
|
328
|
+
`${call}: \`canister\` is a canister id, such as "ryjl3-tyaaa-aaaaa-aaaba-cai" or "aaaaa-aa", got ${
|
|
329
|
+
typeof canister === "string"
|
|
330
|
+
? JSON.stringify(canister)
|
|
331
|
+
: typeof canister
|
|
332
|
+
}`
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return { method, canister: canisterId, left: times }
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* The first fault in `armed` that a request for `method` on `canisterId`
|
|
341
|
+
* matches, with one use taken from it (and removed once it has none left);
|
|
342
|
+
* `undefined` when none matches.
|
|
343
|
+
*/
|
|
344
|
+
function useUp<T extends Armed>(
|
|
345
|
+
armed: T[],
|
|
346
|
+
canisterId: string,
|
|
347
|
+
method: string | undefined
|
|
348
|
+
): T | undefined {
|
|
349
|
+
const index = armed.findIndex(
|
|
350
|
+
(fault) =>
|
|
351
|
+
(fault.method === undefined || fault.method === method) &&
|
|
352
|
+
(fault.canister === undefined || fault.canister === canisterId)
|
|
353
|
+
)
|
|
354
|
+
if (index === -1) return undefined
|
|
355
|
+
const fault = armed[index]
|
|
356
|
+
fault.left -= 1
|
|
357
|
+
if (fault.left === 0) armed.splice(index, 1)
|
|
358
|
+
return fault
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Rejects a call with a reject code. Thrown by {@link FakeReplica.reject}.
|
|
363
|
+
*
|
|
364
|
+
* @internal
|
|
365
|
+
*/
|
|
366
|
+
export class FakeReplicaReject extends Error {
|
|
367
|
+
constructor(
|
|
368
|
+
readonly rejectCode: number,
|
|
369
|
+
readonly errorCode: string | undefined,
|
|
370
|
+
message: string
|
|
371
|
+
) {
|
|
372
|
+
super(message)
|
|
373
|
+
this.name = "FakeReplicaReject"
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Thrown out of the fake's `fetch` when a reply is lost, in the form of a
|
|
379
|
+
* network failure.
|
|
380
|
+
*/
|
|
381
|
+
class LostReply extends TypeError {}
|
|
382
|
+
|
|
383
|
+
/** The client's `"local"` network: icp-cli's gateway on its default port. */
|
|
384
|
+
const DEFAULT_HOST = "http://127.0.0.1:8000"
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* The page's origin when it is one the fake can answer for, which is where an
|
|
388
|
+
* agent built with no `host` calls: a local or remote development origin,
|
|
389
|
+
* such as jsdom's `http://localhost:3000`. A mainnet origin is left out, as
|
|
390
|
+
* its agents check certificates against mainnet's root key.
|
|
391
|
+
*/
|
|
392
|
+
function localPageOrigin(): string | undefined {
|
|
393
|
+
try {
|
|
394
|
+
const origin = (globalThis as { location?: { origin?: string } }).location
|
|
395
|
+
?.origin
|
|
396
|
+
if (!origin) return undefined
|
|
397
|
+
const { protocol, hostname } = new URL(origin)
|
|
398
|
+
return (protocol === "http:" || protocol === "https:") &&
|
|
399
|
+
isNonMainnetPageHost(hostname)
|
|
400
|
+
? origin
|
|
401
|
+
: undefined
|
|
402
|
+
} catch {
|
|
403
|
+
// No page (Node), an opaque origin ("null"), or a `location` that throws.
|
|
404
|
+
return undefined
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* The IC HTTP API paths an agent requests: the status endpoint, and a
|
|
410
|
+
* canister's or subnet's `query`, `call` and `read_state`. Anything else, such
|
|
411
|
+
* as an app's own `/api/v1/...` REST calls, is not the fake's to answer.
|
|
412
|
+
*/
|
|
413
|
+
const IC_API_PATH =
|
|
414
|
+
/^\/api\/v\d+\/(?:status|(?:canister|subnet)\/[^/]+\/(?:query|call|read_state))$/
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* The URL a request is for. An agent always asks for an absolute one; an app
|
|
418
|
+
* may ask for a path, which a browser, happy-dom or a request mock such as
|
|
419
|
+
* MSW reads against the page. `undefined` when it cannot be read at all.
|
|
420
|
+
*/
|
|
421
|
+
function urlOf(input: RequestInfo | URL): URL | undefined {
|
|
422
|
+
const raw = input instanceof Request ? input.url : String(input)
|
|
423
|
+
let base: string | undefined
|
|
424
|
+
try {
|
|
425
|
+
base = (globalThis as { location?: { href?: string } }).location?.href
|
|
426
|
+
} catch {
|
|
427
|
+
base = undefined
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
return new URL(raw, base)
|
|
431
|
+
} catch {
|
|
432
|
+
return undefined
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const encoder = new TextEncoder()
|
|
437
|
+
// Copied: a jsdom test setup often installs Node's `TextEncoder`, whose bytes
|
|
438
|
+
// come from another realm than the environment's `Uint8Array`. The CBOR
|
|
439
|
+
// encoder does not recognise them as bytes and writes them out as a map.
|
|
440
|
+
const utf8 = (text: string) => new Uint8Array(encoder.encode(text))
|
|
441
|
+
|
|
442
|
+
/** A subtree to certify: a leaf's bytes, or labelled children. */
|
|
443
|
+
type TreeNode = Uint8Array | Array<[string | Uint8Array, TreeNode]>
|
|
444
|
+
|
|
445
|
+
function compareBytes(a: Uint8Array, b: Uint8Array): number {
|
|
446
|
+
const length = Math.min(a.length, b.length)
|
|
447
|
+
for (let i = 0; i < length; i += 1) {
|
|
448
|
+
if (a[i] !== b[i]) return a[i] - b[i]
|
|
449
|
+
}
|
|
450
|
+
return a.length - b.length
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function toHashTree(node: TreeNode): HashTree {
|
|
454
|
+
if (!Array.isArray(node)) {
|
|
455
|
+
return [NodeType.Leaf, node] as HashTree
|
|
456
|
+
}
|
|
457
|
+
// Ascending, because the agent's label search stops at the first label
|
|
458
|
+
// greater than the one it wants.
|
|
459
|
+
const labelled = node
|
|
460
|
+
.map(([label, child]) => ({
|
|
461
|
+
label: typeof label === "string" ? utf8(label) : label,
|
|
462
|
+
child,
|
|
463
|
+
}))
|
|
464
|
+
.sort((a, b) => compareBytes(a.label, b.label))
|
|
465
|
+
.map(
|
|
466
|
+
({ label, child }) =>
|
|
467
|
+
[NodeType.Labeled, label, toHashTree(child)] as HashTree
|
|
468
|
+
)
|
|
469
|
+
return fork(labelled)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function fork(nodes: HashTree[]): HashTree {
|
|
473
|
+
if (nodes.length === 0) return [NodeType.Empty]
|
|
474
|
+
if (nodes.length === 1) return nodes[0]
|
|
475
|
+
const middle = Math.ceil(nodes.length / 2)
|
|
476
|
+
return [
|
|
477
|
+
NodeType.Fork,
|
|
478
|
+
fork(nodes.slice(0, middle)),
|
|
479
|
+
fork(nodes.slice(middle)),
|
|
480
|
+
]
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function leb128(value: bigint): Uint8Array {
|
|
484
|
+
const out: number[] = []
|
|
485
|
+
let rest = value
|
|
486
|
+
do {
|
|
487
|
+
let byte = Number(rest & 0x7fn)
|
|
488
|
+
rest >>= 7n
|
|
489
|
+
if (rest !== 0n) byte |= 0x80
|
|
490
|
+
out.push(byte)
|
|
491
|
+
} while (rest !== 0n)
|
|
492
|
+
return new Uint8Array(out)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function concat(...parts: Uint8Array[]): Uint8Array {
|
|
496
|
+
const out = new Uint8Array(parts.reduce((n, part) => n + part.length, 0))
|
|
497
|
+
let offset = 0
|
|
498
|
+
for (const part of parts) {
|
|
499
|
+
out.set(part, offset)
|
|
500
|
+
offset += part.length
|
|
501
|
+
}
|
|
502
|
+
return out
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const nowNanos = () => BigInt(Date.now()) * 1_000_000n
|
|
506
|
+
|
|
507
|
+
function cborResponse(body: unknown, status = 200): Response {
|
|
508
|
+
return new Response(Cbor.encode(body) as BodyInit, {
|
|
509
|
+
status,
|
|
510
|
+
headers: { "content-type": "application/cbor" },
|
|
511
|
+
})
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
interface Envelope {
|
|
515
|
+
content: {
|
|
516
|
+
canister_id?: Uint8Array
|
|
517
|
+
method_name?: string
|
|
518
|
+
arg?: Uint8Array
|
|
519
|
+
paths?: Uint8Array[][]
|
|
520
|
+
sender: Uint8Array
|
|
521
|
+
}
|
|
522
|
+
sender_pubkey?: Uint8Array
|
|
523
|
+
sender_sig?: Uint8Array
|
|
524
|
+
sender_delegation?: Array<{
|
|
525
|
+
delegation: {
|
|
526
|
+
pubkey: Uint8Array
|
|
527
|
+
expiration: bigint | number
|
|
528
|
+
targets?: Uint8Array[]
|
|
529
|
+
}
|
|
530
|
+
signature: Uint8Array
|
|
531
|
+
}>
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// The DER prefix of each kind of key an identity in `@icp-sdk/core` signs
|
|
535
|
+
// with, before the raw key: Ed25519, ECDSA on P-256 (`ECDSAKeyIdentity`, and
|
|
536
|
+
// the session keys `@icp-sdk/auth` makes) and ECDSA on secp256k1.
|
|
537
|
+
const hex = (text: string) =>
|
|
538
|
+
Uint8Array.from(text.match(/../g) ?? [], (byte) => parseInt(byte, 16))
|
|
539
|
+
const ED25519_SPKI_PREFIX = hex("302a300506032b6570032100")
|
|
540
|
+
const P256_SPKI_PREFIX = hex(
|
|
541
|
+
"3059301306072a8648ce3d020106082a8648ce3d030107034200"
|
|
542
|
+
)
|
|
543
|
+
const SECP256K1_SPKI_PREFIX = hex(
|
|
544
|
+
"3056301006072a8648ce3d020106052b8104000a034200"
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
const startsWith = (bytes: Uint8Array, prefix: Uint8Array) =>
|
|
548
|
+
bytes.length > prefix.length && prefix.every((byte, i) => bytes[i] === byte)
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Whether `signature` is the signature of the key `publicKey` (DER) over
|
|
552
|
+
* `message`, or `undefined` for a kind of key the fake cannot check.
|
|
553
|
+
*/
|
|
554
|
+
function verifySignature(
|
|
555
|
+
publicKey: Uint8Array,
|
|
556
|
+
signature: Uint8Array,
|
|
557
|
+
message: Uint8Array
|
|
558
|
+
): boolean | undefined {
|
|
559
|
+
try {
|
|
560
|
+
if (startsWith(publicKey, ED25519_SPKI_PREFIX)) {
|
|
561
|
+
const raw = publicKey.subarray(ED25519_SPKI_PREFIX.length)
|
|
562
|
+
return ed25519.verify(signature, message, raw)
|
|
563
|
+
}
|
|
564
|
+
if (startsWith(publicKey, P256_SPKI_PREFIX)) {
|
|
565
|
+
// WebCrypto, which signs for `ECDSAKeyIdentity`, does not normalise S.
|
|
566
|
+
const raw = publicKey.subarray(P256_SPKI_PREFIX.length)
|
|
567
|
+
return p256.verify(signature, message, raw, { lowS: false })
|
|
568
|
+
}
|
|
569
|
+
if (startsWith(publicKey, SECP256K1_SPKI_PREFIX)) {
|
|
570
|
+
const raw = publicKey.subarray(SECP256K1_SPKI_PREFIX.length)
|
|
571
|
+
return secp256k1.verify(signature, message, raw)
|
|
572
|
+
}
|
|
573
|
+
} catch {
|
|
574
|
+
// A malformed key or signature does not verify.
|
|
575
|
+
return false
|
|
576
|
+
}
|
|
577
|
+
return undefined
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** A canister's reply, or the reject a replica sends in its place. */
|
|
581
|
+
type Outcome =
|
|
582
|
+
| { reply: Uint8Array }
|
|
583
|
+
| { reject: { code: number; message: string; errorCode?: string } }
|
|
584
|
+
|
|
585
|
+
const messageOf = (error: unknown) =>
|
|
586
|
+
error instanceof Error ? error.message : String(error)
|
|
587
|
+
|
|
588
|
+
/** Lower-case hex of `bytes`: a map key for a request id. */
|
|
589
|
+
const toHex = (bytes: Uint8Array) =>
|
|
590
|
+
Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")
|
|
591
|
+
|
|
592
|
+
/** The names the interface specification gives each reject code. */
|
|
593
|
+
const REJECT_NAMES: Record<FakeRejectCode, string> = {
|
|
594
|
+
1: "SYS_FATAL",
|
|
595
|
+
2: "SYS_TRANSIENT",
|
|
596
|
+
3: "DESTINATION_INVALID",
|
|
597
|
+
4: "CANISTER_REJECT",
|
|
598
|
+
5: "CANISTER_ERROR",
|
|
599
|
+
6: "SYS_UNKNOWN",
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Creates a fake replica that runs `options.canisters`, to give to the agents
|
|
604
|
+
* under test as their `fetch`. Nothing global is touched, so two fakes run
|
|
605
|
+
* side by side in one process, each with its own root key, node key and
|
|
606
|
+
* request log, and the agents that use one never reach the other.
|
|
607
|
+
*
|
|
608
|
+
* Point an agent at it with its `host`, its `fetch` and its root key:
|
|
609
|
+
* `HttpAgent.createSync({ host: replica.host, fetch: replica.fetch, rootKey:
|
|
610
|
+
* replica.rootKey })`, or pass `shouldFetchRootKey: true` instead of the
|
|
611
|
+
* `rootKey` to let the agent fetch it from the fake as it does from a local
|
|
612
|
+
* replica. The fake answers the IC API on `replica.host` and refuses the IC
|
|
613
|
+
* API on any other origin as a network error, which it also logs once, so a
|
|
614
|
+
* test never reaches a real network by mistake.
|
|
615
|
+
*
|
|
616
|
+
* It checks each request's signatures as a replica does, and refuses one that
|
|
617
|
+
* does not verify with HTTP 400. It can check Ed25519, ECDSA P-256 and
|
|
618
|
+
* secp256k1 keys, including delegation chains between them, which covers
|
|
619
|
+
* `Ed25519KeyIdentity`, `ECDSAKeyIdentity`, `Secp256k1KeyIdentity` and a
|
|
620
|
+
* `DelegationIdentity` built from them. It refuses any other kind of key.
|
|
621
|
+
*
|
|
622
|
+
* An agent keeps the node keys it reads in IndexedDB when the environment has
|
|
623
|
+
* one, under a name that holds only the host. Agents of two fakes that share a
|
|
624
|
+
* host therefore share cached keys. The agent notices a signature that does
|
|
625
|
+
* not verify and reads fresh keys, but pass each agent its own
|
|
626
|
+
* `subnetNodeKeyExpirableStore` to keep them apart.
|
|
627
|
+
*
|
|
628
|
+
* The signing uses `@noble/curves`, an optional peer dependency of
|
|
629
|
+
* `@ic-reactor/core` that `@icp-sdk/core` already installs. Add it to your
|
|
630
|
+
* devDependencies if your package manager does not let this package resolve
|
|
631
|
+
* it.
|
|
632
|
+
*
|
|
633
|
+
* @param options - The canisters to run and the origin to answer for.
|
|
634
|
+
* @returns The fake: its `fetch`, `host` and `rootKey`, the requests it
|
|
635
|
+
* received, and the hooks that fail a call on purpose.
|
|
636
|
+
*
|
|
637
|
+
* @example
|
|
638
|
+
* ```typescript
|
|
639
|
+
* import { HttpAgent } from "@icp-sdk/core/agent"
|
|
640
|
+
* import { IDL } from "@icp-sdk/core/candid"
|
|
641
|
+
* // Internal: core's own tests import it from its module.
|
|
642
|
+
* import { createFakeReplica } from "../src/testing/fake-replica.js"
|
|
643
|
+
*
|
|
644
|
+
* const BACKEND = "bkyz2-fmaaa-aaaaa-qaaaq-cai"
|
|
645
|
+
*
|
|
646
|
+
* const replica = createFakeReplica({
|
|
647
|
+
* canisters: {
|
|
648
|
+
* [BACKEND]: {
|
|
649
|
+
* query: () => new Uint8Array(IDL.encode([IDL.Text], ["Hello, Ada!"])),
|
|
650
|
+
* update: () => replica.reject(4, "not today"),
|
|
651
|
+
* },
|
|
652
|
+
* },
|
|
653
|
+
* })
|
|
654
|
+
* const agent = HttpAgent.createSync({
|
|
655
|
+
* host: replica.host,
|
|
656
|
+
* fetch: replica.fetch,
|
|
657
|
+
* rootKey: replica.rootKey,
|
|
658
|
+
* })
|
|
659
|
+
*
|
|
660
|
+
* const answer = await agent.query(BACKEND, {
|
|
661
|
+
* methodName: "greet",
|
|
662
|
+
* arg: new Uint8Array(IDL.encode([IDL.Text], ["Ada"])),
|
|
663
|
+
* })
|
|
664
|
+
* ```
|
|
665
|
+
*/
|
|
666
|
+
export function createFakeReplica(
|
|
667
|
+
options: FakeReplicaOptions = {}
|
|
668
|
+
): FakeReplica {
|
|
669
|
+
const host = new URL(options.host ?? localPageOrigin() ?? DEFAULT_HOST).origin
|
|
670
|
+
const canisters: Record<string, FakeCanister> = {}
|
|
671
|
+
const requests: FakeReplicaRequest[] = []
|
|
672
|
+
|
|
673
|
+
// Throws for an ID that is not a canister ID.
|
|
674
|
+
function canisterIdOf(id: string): Principal {
|
|
675
|
+
try {
|
|
676
|
+
return Principal.fromText(id)
|
|
677
|
+
} catch {
|
|
678
|
+
throw new Error(
|
|
679
|
+
`fake replica: "${id}" in \`canisters\` is not a canister ID`
|
|
680
|
+
)
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
function addCanister(id: string, canister: FakeCanister) {
|
|
684
|
+
canisters[canisterIdOf(id).toText()] = canister
|
|
685
|
+
}
|
|
686
|
+
for (const [id, canister] of Object.entries(options.canisters ?? {})) {
|
|
687
|
+
addCanister(id, canister)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const secretKey = bls12_381.utils.randomSecretKey()
|
|
691
|
+
const rootKey = wrapDER(
|
|
692
|
+
bls12_381.shortSignatures.getPublicKey(secretKey).toBytes(),
|
|
693
|
+
BLS12_381_G2_OID
|
|
694
|
+
)
|
|
695
|
+
// With no delegation in its certificates, the subnet an agent checks node
|
|
696
|
+
// keys and canister ranges under is the one the root key names.
|
|
697
|
+
const subnetId = Principal.selfAuthenticating(rootKey)
|
|
698
|
+
const node = Ed25519KeyIdentity.generate()
|
|
699
|
+
const nodeKey = new Uint8Array(node.getPublicKey().toDer())
|
|
700
|
+
const nodeId = Principal.selfAuthenticating(nodeKey)
|
|
701
|
+
|
|
702
|
+
// The fault hooks' state: the drops and the refusals still armed, each in
|
|
703
|
+
// the order they were armed, and the calls whose reply was lost.
|
|
704
|
+
const drops: Armed[] = []
|
|
705
|
+
const refusals: Array<Armed & { readonly status: number }> = []
|
|
706
|
+
const lostRequests = new Set<string>()
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* The status to refuse a `query` or `call` with, from the first armed
|
|
710
|
+
* refusal it matches, which it uses up; `undefined` to let it through.
|
|
711
|
+
*/
|
|
712
|
+
function refusalFor(canisterId: string, method: string | undefined) {
|
|
713
|
+
return useUp(refusals, canisterId, method)?.status
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async function certify(entries: Array<[string, TreeNode]>) {
|
|
717
|
+
const tree = toHashTree([
|
|
718
|
+
...entries,
|
|
719
|
+
["time", leb128((options.now ?? nowNanos)())],
|
|
720
|
+
])
|
|
721
|
+
const rootHash = await reconstruct(tree)
|
|
722
|
+
const message = bls12_381.shortSignatures.hash(
|
|
723
|
+
concat(IC_STATE_ROOT_DOMAIN_SEPARATOR, rootHash)
|
|
724
|
+
)
|
|
725
|
+
const signature = bls12_381.shortSignatures.sign(message, secretKey)
|
|
726
|
+
return Cbor.encode({ tree, signature: signature.toBytes() })
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Who sent `envelope`, checked as a replica checks it, or why it is refused.
|
|
731
|
+
*/
|
|
732
|
+
function authenticate(
|
|
733
|
+
envelope: Envelope,
|
|
734
|
+
canisterId: string
|
|
735
|
+
): Principal | string {
|
|
736
|
+
const sender = Principal.fromUint8Array(envelope.content.sender)
|
|
737
|
+
const { sender_pubkey, sender_sig, sender_delegation = [] } = envelope
|
|
738
|
+
|
|
739
|
+
if (sender.isAnonymous()) {
|
|
740
|
+
return sender_pubkey || sender_sig
|
|
741
|
+
? "an anonymous request carries a signature"
|
|
742
|
+
: sender
|
|
743
|
+
}
|
|
744
|
+
if (!sender_pubkey || !sender_sig) {
|
|
745
|
+
return "the request is not signed"
|
|
746
|
+
}
|
|
747
|
+
if (
|
|
748
|
+
Principal.selfAuthenticating(sender_pubkey).toText() !== sender.toText()
|
|
749
|
+
) {
|
|
750
|
+
return "the sender is not the principal of sender_pubkey"
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const now = nowNanos()
|
|
754
|
+
let signer = sender_pubkey
|
|
755
|
+
for (const { delegation, signature } of sender_delegation) {
|
|
756
|
+
const challenge = concat(
|
|
757
|
+
IC_REQUEST_AUTH_DELEGATION_DOMAIN_SEPARATOR,
|
|
758
|
+
requestIdOf(delegation)
|
|
759
|
+
)
|
|
760
|
+
const signed = verifySignature(signer, signature, challenge)
|
|
761
|
+
if (signed === undefined) {
|
|
762
|
+
return "a delegation is signed by a kind of key the fake replica cannot check"
|
|
763
|
+
}
|
|
764
|
+
if (!signed) {
|
|
765
|
+
return "a delegation is not signed by the key before it"
|
|
766
|
+
}
|
|
767
|
+
if (BigInt(delegation.expiration) <= now) {
|
|
768
|
+
return "a delegation has expired"
|
|
769
|
+
}
|
|
770
|
+
if (
|
|
771
|
+
delegation.targets &&
|
|
772
|
+
!delegation.targets.some(
|
|
773
|
+
(target) => Principal.fromUint8Array(target).toText() === canisterId
|
|
774
|
+
)
|
|
775
|
+
) {
|
|
776
|
+
return `a delegation does not allow calls to ${canisterId}`
|
|
777
|
+
}
|
|
778
|
+
signer = delegation.pubkey
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const message = concat(
|
|
782
|
+
IC_REQUEST_DOMAIN_SEPARATOR,
|
|
783
|
+
requestIdOf(envelope.content)
|
|
784
|
+
)
|
|
785
|
+
const signed = verifySignature(signer, sender_sig, message)
|
|
786
|
+
if (signed === undefined) {
|
|
787
|
+
return "the request is signed by a kind of key the fake replica cannot check"
|
|
788
|
+
}
|
|
789
|
+
if (!signed) {
|
|
790
|
+
return "sender_sig is not the signing key's signature over the request"
|
|
791
|
+
}
|
|
792
|
+
return sender
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/** Runs a canister's handler, and turns what it throws into a reject. */
|
|
796
|
+
async function execute(
|
|
797
|
+
kind: "query" | "update",
|
|
798
|
+
canisterId: string,
|
|
799
|
+
method: string,
|
|
800
|
+
arg: Uint8Array,
|
|
801
|
+
caller: Principal
|
|
802
|
+
): Promise<Outcome> {
|
|
803
|
+
const reject = (
|
|
804
|
+
code: number,
|
|
805
|
+
errorCode: string | undefined,
|
|
806
|
+
message: string
|
|
807
|
+
): Outcome => ({ reject: { code, errorCode, message } })
|
|
808
|
+
|
|
809
|
+
const canister = Object.prototype.hasOwnProperty.call(canisters, canisterId)
|
|
810
|
+
? canisters[canisterId]
|
|
811
|
+
: undefined
|
|
812
|
+
if (!canister) {
|
|
813
|
+
return reject(
|
|
814
|
+
ReplicaRejectCode.DestinationInvalid,
|
|
815
|
+
"IC0301",
|
|
816
|
+
`fake replica: no canister is installed at ${canisterId}`
|
|
817
|
+
)
|
|
818
|
+
}
|
|
819
|
+
const handler = kind === "query" ? canister.query : canister.update
|
|
820
|
+
if (!handler) {
|
|
821
|
+
return reject(
|
|
822
|
+
ReplicaRejectCode.DestinationInvalid,
|
|
823
|
+
"IC0536",
|
|
824
|
+
`fake replica: canister ${canisterId} answers no ${kind === "query" ? "queries" : "update calls"}`
|
|
825
|
+
)
|
|
826
|
+
}
|
|
827
|
+
try {
|
|
828
|
+
return {
|
|
829
|
+
reply: new Uint8Array(
|
|
830
|
+
await handler.call(canister, method, arg, { caller })
|
|
831
|
+
),
|
|
832
|
+
}
|
|
833
|
+
} catch (error) {
|
|
834
|
+
if (error instanceof FakeReplicaReject) {
|
|
835
|
+
return reject(error.rejectCode, error.errorCode, error.message)
|
|
836
|
+
}
|
|
837
|
+
return reject(
|
|
838
|
+
ReplicaRejectCode.CanisterError,
|
|
839
|
+
"IC0503",
|
|
840
|
+
`Canister ${canisterId} trapped: ${messageOf(error)}`
|
|
841
|
+
)
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async function handleQuery(
|
|
846
|
+
canisterId: string,
|
|
847
|
+
effectiveCanisterId: string,
|
|
848
|
+
envelope: Envelope,
|
|
849
|
+
caller: Principal
|
|
850
|
+
) {
|
|
851
|
+
const { method_name = "", arg = new Uint8Array() } = envelope.content
|
|
852
|
+
requests.push({
|
|
853
|
+
endpoint: "query",
|
|
854
|
+
canisterId,
|
|
855
|
+
effectiveCanisterId,
|
|
856
|
+
methodName: method_name,
|
|
857
|
+
caller: caller.toText(),
|
|
858
|
+
})
|
|
859
|
+
const outcome = await execute("query", canisterId, method_name, arg, caller)
|
|
860
|
+
const timestamp = nowNanos()
|
|
861
|
+
const request_id = requestIdOf(envelope.content)
|
|
862
|
+
const body =
|
|
863
|
+
"reply" in outcome
|
|
864
|
+
? { status: "replied", reply: { arg: outcome.reply } }
|
|
865
|
+
: {
|
|
866
|
+
status: "rejected",
|
|
867
|
+
reject_code: outcome.reject.code,
|
|
868
|
+
reject_message: outcome.reject.message,
|
|
869
|
+
...(outcome.reject.errorCode === undefined
|
|
870
|
+
? {}
|
|
871
|
+
: { error_code: outcome.reject.errorCode }),
|
|
872
|
+
}
|
|
873
|
+
const hash = hashOfMap({ ...body, timestamp, request_id })
|
|
874
|
+
const signature = await node.sign(
|
|
875
|
+
concat(IC_RESPONSE_DOMAIN_SEPARATOR, hash)
|
|
876
|
+
)
|
|
877
|
+
return cborResponse({
|
|
878
|
+
...body,
|
|
879
|
+
signatures: [
|
|
880
|
+
{
|
|
881
|
+
timestamp,
|
|
882
|
+
signature: new Uint8Array(signature),
|
|
883
|
+
identity: nodeId.toUint8Array(),
|
|
884
|
+
},
|
|
885
|
+
],
|
|
886
|
+
})
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
async function handleCall(
|
|
890
|
+
canisterId: string,
|
|
891
|
+
effectiveCanisterId: string,
|
|
892
|
+
envelope: Envelope,
|
|
893
|
+
caller: Principal
|
|
894
|
+
) {
|
|
895
|
+
const { method_name = "", arg = new Uint8Array() } = envelope.content
|
|
896
|
+
const requestId = toHex(requestIdOf(envelope.content))
|
|
897
|
+
// A request id whose reply was lost stays lost: a re-send is neither run
|
|
898
|
+
// nor answered again, and its status cannot be read (see
|
|
899
|
+
// `readsLostRequest`). That models a partition for that one request id,
|
|
900
|
+
// not what a replica does, which would still answer a `read_state`.
|
|
901
|
+
const resent = lostRequests.has(requestId)
|
|
902
|
+
const lose = resent || useUp(drops, canisterId, method_name) !== undefined
|
|
903
|
+
if (lose && !resent) lostRequests.add(requestId)
|
|
904
|
+
requests.push({
|
|
905
|
+
endpoint: "call",
|
|
906
|
+
canisterId,
|
|
907
|
+
effectiveCanisterId,
|
|
908
|
+
methodName: method_name,
|
|
909
|
+
caller: caller.toText(),
|
|
910
|
+
...(lose ? { dropped: true as const } : {}),
|
|
911
|
+
})
|
|
912
|
+
if (resent) throw lostReply(method_name)
|
|
913
|
+
|
|
914
|
+
const outcome = await execute(
|
|
915
|
+
"update",
|
|
916
|
+
canisterId,
|
|
917
|
+
method_name,
|
|
918
|
+
arg,
|
|
919
|
+
caller
|
|
920
|
+
)
|
|
921
|
+
// The canister has run and its state has changed; only the answer is lost.
|
|
922
|
+
if (lose) throw lostReply(method_name)
|
|
923
|
+
|
|
924
|
+
const status: Array<[string, TreeNode]> =
|
|
925
|
+
"reply" in outcome
|
|
926
|
+
? [
|
|
927
|
+
["status", utf8("replied")],
|
|
928
|
+
["reply", outcome.reply],
|
|
929
|
+
]
|
|
930
|
+
: [
|
|
931
|
+
["status", utf8("rejected")],
|
|
932
|
+
["reject_code", leb128(BigInt(outcome.reject.code))],
|
|
933
|
+
["reject_message", utf8(outcome.reject.message)],
|
|
934
|
+
...(outcome.reject.errorCode === undefined
|
|
935
|
+
? []
|
|
936
|
+
: ([["error_code", utf8(outcome.reject.errorCode)]] as Array<
|
|
937
|
+
[string, TreeNode]
|
|
938
|
+
>)),
|
|
939
|
+
]
|
|
940
|
+
const certificate = await certify([
|
|
941
|
+
["request_status", [[requestIdOf(envelope.content), status]]],
|
|
942
|
+
])
|
|
943
|
+
return cborResponse({ status: "replied", certificate })
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/** Whether `envelope` reads the status of a call whose reply was lost. */
|
|
947
|
+
const readsLostRequest = (envelope: Envelope) =>
|
|
948
|
+
(envelope.content.paths ?? []).some(
|
|
949
|
+
([label, id]) =>
|
|
950
|
+
label !== undefined &&
|
|
951
|
+
id !== undefined &&
|
|
952
|
+
new TextDecoder().decode(label) === "request_status" &&
|
|
953
|
+
lostRequests.has(toHex(id))
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
async function handleReadState(canisterId: string) {
|
|
957
|
+
requests.push({
|
|
958
|
+
endpoint: "read_state",
|
|
959
|
+
canisterId,
|
|
960
|
+
effectiveCanisterId: canisterId,
|
|
961
|
+
})
|
|
962
|
+
// The subnet holds the installed canisters and the one asked about, so a
|
|
963
|
+
// query to a canister that is not installed reaches the canister lookup
|
|
964
|
+
// and is rejected there, rather than failing the agent's range check.
|
|
965
|
+
const inSubnet = [
|
|
966
|
+
...Object.keys(canisters).map((id) => Principal.fromText(id)),
|
|
967
|
+
Principal.fromText(canisterId),
|
|
968
|
+
]
|
|
969
|
+
.map((id) => id.toUint8Array())
|
|
970
|
+
.sort(compareBytes)
|
|
971
|
+
.filter((id, i, ids) => i === 0 || compareBytes(ids[i - 1], id) !== 0)
|
|
972
|
+
const certificate = await certify([
|
|
973
|
+
[
|
|
974
|
+
"subnet",
|
|
975
|
+
[
|
|
976
|
+
[
|
|
977
|
+
subnetId.toUint8Array(),
|
|
978
|
+
[
|
|
979
|
+
["canister_ranges", Cbor.encode(inSubnet.map((id) => [id, id]))],
|
|
980
|
+
["node", [[nodeId.toUint8Array(), [["public_key", nodeKey]]]]],
|
|
981
|
+
],
|
|
982
|
+
],
|
|
983
|
+
],
|
|
984
|
+
],
|
|
985
|
+
])
|
|
986
|
+
return cborResponse({ certificate })
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Origins a misrouted IC API request came from, each logged once.
|
|
990
|
+
const misrouted = new Set<string>()
|
|
991
|
+
|
|
992
|
+
const fakeFetch = async (
|
|
993
|
+
input: RequestInfo | URL,
|
|
994
|
+
init?: RequestInit
|
|
995
|
+
): Promise<Response> => {
|
|
996
|
+
const url = urlOf(input)
|
|
997
|
+
if (!url || !IC_API_PATH.test(url.pathname)) {
|
|
998
|
+
throw new TypeError(
|
|
999
|
+
`fake replica: ${url?.href ?? String(input)} is not an IC API request, ` +
|
|
1000
|
+
`which is all the fake answers`
|
|
1001
|
+
)
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
if (url.origin !== host) {
|
|
1005
|
+
const message =
|
|
1006
|
+
`fake replica: no route to ${url.origin}. The fake answers ${host}; ` +
|
|
1007
|
+
`build the agent with \`host: replica.host\` and \`fetch: replica.fetch\`, ` +
|
|
1008
|
+
`or create the fake with \`host: "${url.origin}"\``
|
|
1009
|
+
// The agent retries a failed request, and a query hook retries a
|
|
1010
|
+
// failed query, so the error below can surface long after the test
|
|
1011
|
+
// timed out. Logged here, the cause shows up at once.
|
|
1012
|
+
if (!misrouted.has(url.origin)) {
|
|
1013
|
+
misrouted.add(url.origin)
|
|
1014
|
+
console.error(message)
|
|
1015
|
+
}
|
|
1016
|
+
throw new TypeError(message)
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (url.pathname === "/api/v2/status") {
|
|
1020
|
+
requests.push({ endpoint: "status" })
|
|
1021
|
+
return cborResponse({
|
|
1022
|
+
ic_api_version: "0.18.0",
|
|
1023
|
+
impl_version: "fake",
|
|
1024
|
+
replica_health_status: "healthy",
|
|
1025
|
+
root_key: rootKey,
|
|
1026
|
+
})
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const match =
|
|
1030
|
+
/^\/api\/v[234]\/canister\/([^/]+)\/(query|call|read_state)$/.exec(
|
|
1031
|
+
url.pathname
|
|
1032
|
+
)
|
|
1033
|
+
if (!match) {
|
|
1034
|
+
return new Response(`fake replica: no endpoint ${url.pathname}`, {
|
|
1035
|
+
status: 404,
|
|
1036
|
+
})
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const [, effectiveCanisterId, path] = match
|
|
1040
|
+
const endpoint = path as "query" | "call" | "read_state"
|
|
1041
|
+
|
|
1042
|
+
try {
|
|
1043
|
+
const body = new Uint8Array(
|
|
1044
|
+
await new Response(
|
|
1045
|
+
input instanceof Request ? input.body : (init?.body as BodyInit)
|
|
1046
|
+
).arrayBuffer()
|
|
1047
|
+
)
|
|
1048
|
+
const envelope = Cbor.decode(body) as Envelope
|
|
1049
|
+
// A call goes to the canister it names, as on a replica. The path
|
|
1050
|
+
// names the effective canister, which only routes the request to a
|
|
1051
|
+
// subnet and differs when the caller passes `effectiveCanisterId`.
|
|
1052
|
+
const canisterId = envelope.content.canister_id
|
|
1053
|
+
? Principal.fromUint8Array(envelope.content.canister_id).toText()
|
|
1054
|
+
: effectiveCanisterId
|
|
1055
|
+
|
|
1056
|
+
// Before the signatures are read, as a gateway refuses a request.
|
|
1057
|
+
const status =
|
|
1058
|
+
endpoint === "read_state"
|
|
1059
|
+
? undefined
|
|
1060
|
+
: refusalFor(canisterId, envelope.content.method_name)
|
|
1061
|
+
if (status !== undefined) {
|
|
1062
|
+
const refused = `refuseNext(${status}) answered the request with HTTP ${status}`
|
|
1063
|
+
requests.push({
|
|
1064
|
+
endpoint,
|
|
1065
|
+
canisterId,
|
|
1066
|
+
effectiveCanisterId,
|
|
1067
|
+
methodName: envelope.content.method_name,
|
|
1068
|
+
refused,
|
|
1069
|
+
})
|
|
1070
|
+
return new Response(`fake replica: ${refused}`, { status })
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const caller = authenticate(envelope, canisterId)
|
|
1074
|
+
if (typeof caller === "string") {
|
|
1075
|
+
requests.push({
|
|
1076
|
+
endpoint,
|
|
1077
|
+
canisterId,
|
|
1078
|
+
effectiveCanisterId,
|
|
1079
|
+
methodName: envelope.content.method_name,
|
|
1080
|
+
refused: caller,
|
|
1081
|
+
})
|
|
1082
|
+
return new Response(`fake replica: ${caller}`, { status: 400 })
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
if (endpoint === "query") {
|
|
1086
|
+
return await handleQuery(
|
|
1087
|
+
canisterId,
|
|
1088
|
+
effectiveCanisterId,
|
|
1089
|
+
envelope,
|
|
1090
|
+
caller
|
|
1091
|
+
)
|
|
1092
|
+
}
|
|
1093
|
+
if (endpoint === "call") {
|
|
1094
|
+
return await handleCall(
|
|
1095
|
+
canisterId,
|
|
1096
|
+
effectiveCanisterId,
|
|
1097
|
+
envelope,
|
|
1098
|
+
caller
|
|
1099
|
+
)
|
|
1100
|
+
}
|
|
1101
|
+
if (readsLostRequest(envelope)) {
|
|
1102
|
+
requests.push({
|
|
1103
|
+
endpoint,
|
|
1104
|
+
canisterId: effectiveCanisterId,
|
|
1105
|
+
effectiveCanisterId,
|
|
1106
|
+
dropped: true,
|
|
1107
|
+
})
|
|
1108
|
+
throw lostReply()
|
|
1109
|
+
}
|
|
1110
|
+
return await handleReadState(effectiveCanisterId)
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
// A lost reply is the network failing, which the agent sees as the
|
|
1113
|
+
// fetch rejecting, not as an HTTP answer.
|
|
1114
|
+
if (error instanceof LostReply) throw error
|
|
1115
|
+
// A request the fake cannot read, or a bug in the fake. The agent sees
|
|
1116
|
+
// an HTTP error, which fails the test; the details go to its output.
|
|
1117
|
+
console.error("fake replica:", error)
|
|
1118
|
+
return new Response(
|
|
1119
|
+
`fake replica: the request failed: ${messageOf(error)}`,
|
|
1120
|
+
{ status: 500 }
|
|
1121
|
+
)
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
return {
|
|
1126
|
+
host,
|
|
1127
|
+
rootKey,
|
|
1128
|
+
fetch: fakeFetch,
|
|
1129
|
+
requests,
|
|
1130
|
+
addCanister,
|
|
1131
|
+
reject(code, message) {
|
|
1132
|
+
throw new FakeReplicaReject(
|
|
1133
|
+
code,
|
|
1134
|
+
undefined,
|
|
1135
|
+
message ??
|
|
1136
|
+
`fake replica: the canister rejected the call with reject code ${code} (${REJECT_NAMES[code]})`
|
|
1137
|
+
)
|
|
1138
|
+
},
|
|
1139
|
+
dropNextReply(which) {
|
|
1140
|
+
drops.push(readMatch("dropNextReply", which === undefined ? {} : which))
|
|
1141
|
+
},
|
|
1142
|
+
refuseNext(status, which = 1) {
|
|
1143
|
+
if (!Number.isInteger(status) || status < 400 || status > 599) {
|
|
1144
|
+
throw new RangeError(
|
|
1145
|
+
`refuseNext: ${status} is not an HTTP error status (400 to 599)`
|
|
1146
|
+
)
|
|
1147
|
+
}
|
|
1148
|
+
// A count, or anything that is not an object, is the published
|
|
1149
|
+
// `(status, times?)` form, checked as it always was.
|
|
1150
|
+
const options: FakeRefusal =
|
|
1151
|
+
typeof which === "object" && which !== null && !Array.isArray(which)
|
|
1152
|
+
? which
|
|
1153
|
+
: { times: which as number }
|
|
1154
|
+
refusals.push({ status, ...readMatch("refuseNext", options) })
|
|
1155
|
+
},
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* The error a request fails with when its reply is lost: a `fetch` rejection,
|
|
1161
|
+
* as a dropped connection is.
|
|
1162
|
+
*/
|
|
1163
|
+
const lostReply = (method?: string) =>
|
|
1164
|
+
new LostReply(
|
|
1165
|
+
`fake replica: the connection was lost before the reply${method ? ` to ${method}` : ""} arrived`
|
|
1166
|
+
)
|