@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,1921 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `client.canister()` and its direct calls, on a fake replica: the round trip
|
|
3
|
+
* through the generated schemas, the reply collapse and the unwrap rule, the
|
|
4
|
+
* refusals made before anything is sent, and what is re-sent after which
|
|
5
|
+
* failure.
|
|
6
|
+
*/
|
|
7
|
+
import { afterEach, describe, expect, it, vi } from "vitest"
|
|
8
|
+
import { c, principal, serviceMethods, type Principal } from "@meantjs/schema"
|
|
9
|
+
import { encodeArgs } from "@meantjs/schema/codec"
|
|
10
|
+
import { AnonymousIdentity, Cbor, type Identity } from "@icp-sdk/core/agent"
|
|
11
|
+
import { Ed25519KeyIdentity } from "@icp-sdk/core/identity"
|
|
12
|
+
import type { QueryFunctionContext } from "@tanstack/query-core"
|
|
13
|
+
import { isRetryable } from "../src/errors.js"
|
|
14
|
+
import { isReactorError } from "../src/index.js"
|
|
15
|
+
import { toHex } from "../src/keys.js"
|
|
16
|
+
import { type FakeReplica } from "../src/testing/fake-replica.js"
|
|
17
|
+
import { createTestAuth } from "../src/testing/test-auth.js"
|
|
18
|
+
import {
|
|
19
|
+
ANONYMOUS,
|
|
20
|
+
FEE,
|
|
21
|
+
LEDGER,
|
|
22
|
+
MANAGEMENT,
|
|
23
|
+
SHAPES,
|
|
24
|
+
canisterRequests,
|
|
25
|
+
clientAs,
|
|
26
|
+
clientWithAuth,
|
|
27
|
+
deferred,
|
|
28
|
+
ledgerCanister,
|
|
29
|
+
replicaWith,
|
|
30
|
+
requestsFor,
|
|
31
|
+
serve,
|
|
32
|
+
sleep,
|
|
33
|
+
} from "./canister-helpers.js"
|
|
34
|
+
import { withIdentity } from "./client-helpers.js"
|
|
35
|
+
import { icEnvCookie, stubNoWindow, stubPage } from "./network-helpers.js"
|
|
36
|
+
import * as icrc1 from "./fixtures/icrc1.js"
|
|
37
|
+
import * as management from "./fixtures/management.js"
|
|
38
|
+
import * as shapes from "./fixtures/shapes.js"
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
vi.unstubAllGlobals()
|
|
42
|
+
vi.restoreAllMocks()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const alice = Ed25519KeyIdentity.generate()
|
|
46
|
+
const ALICE = principal(alice.getPrincipal().toText())
|
|
47
|
+
const BOB = principal(Ed25519KeyIdentity.generate().getPrincipal().toText())
|
|
48
|
+
|
|
49
|
+
/** A transfer of `amount` to Bob, with every optional field filled in. */
|
|
50
|
+
const transferTo = (owner: Principal, amount: bigint): icrc1.TransferArg => ({
|
|
51
|
+
to: { owner, subaccount: new Uint8Array(32).fill(1) },
|
|
52
|
+
amount,
|
|
53
|
+
fee: FEE,
|
|
54
|
+
memo: new Uint8Array([0xde, 0xad]),
|
|
55
|
+
from_subaccount: null,
|
|
56
|
+
created_at_time: 1_700_000_000_000_000_000n,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
/** The shapes service, answering with fixed values. */
|
|
60
|
+
const shapesCanister = (
|
|
61
|
+
overrides: Parameters<typeof serve<shapes.Actor>>[1] = {}
|
|
62
|
+
) =>
|
|
63
|
+
serve<shapes.Actor>(shapes.actor, {
|
|
64
|
+
nothing: () => undefined,
|
|
65
|
+
one: ([n]) => n + 1n,
|
|
66
|
+
many: () => [1n, "two", true],
|
|
67
|
+
small: ([n]) => n + 1,
|
|
68
|
+
pair: ([left, right]) => ({ left, right }),
|
|
69
|
+
maybe: ([value]) => value,
|
|
70
|
+
bytes: ([bytes]) => bytes.slice().reverse(),
|
|
71
|
+
who: (_args, { caller }) => principal(caller),
|
|
72
|
+
lookup: () => null,
|
|
73
|
+
outcome: ([n]) =>
|
|
74
|
+
n === 0n ? { tag: "err", value: "zero" } : { tag: "ok", value: n },
|
|
75
|
+
composite: ([n]) => n * 2n,
|
|
76
|
+
bump: ([n]) =>
|
|
77
|
+
n === 0n
|
|
78
|
+
? { tag: "err", value: "nothing to bump" }
|
|
79
|
+
: { tag: "ok", value: n + 1n },
|
|
80
|
+
flag: ([on]) => (on ? { tag: "ok" } : { tag: "err", value: "off" }),
|
|
81
|
+
three: () => ({ tag: "Pending" }),
|
|
82
|
+
note: () => undefined,
|
|
83
|
+
fire: () => undefined,
|
|
84
|
+
address: ([seed], { caller }) => `${seed}:${caller}`,
|
|
85
|
+
...overrides,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
/** A replica running the shapes canister, and a client calling it as Alice. */
|
|
89
|
+
function shapesSetup(overrides?: Parameters<typeof shapesCanister>[0]) {
|
|
90
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister(overrides) })
|
|
91
|
+
const client = clientAs(replica, alice)
|
|
92
|
+
return {
|
|
93
|
+
replica,
|
|
94
|
+
client,
|
|
95
|
+
canister: client.canister<shapes.Actor>(shapes.actor, { id: SHAPES }),
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
describe("a direct call", () => {
|
|
100
|
+
it("sends the ledger's argument shapes and resolves with the decoded reply", async () => {
|
|
101
|
+
const balances = new Map([[ALICE as string, 1_000_000n]])
|
|
102
|
+
const seen: unknown[] = []
|
|
103
|
+
const ledgerFake = ledgerCanister(balances)
|
|
104
|
+
const replica = replicaWith({
|
|
105
|
+
[LEDGER]: {
|
|
106
|
+
query: ledgerFake.query,
|
|
107
|
+
update: async (method, arg, context) => {
|
|
108
|
+
seen.push(method)
|
|
109
|
+
return ledgerFake.update!(method, arg, context)
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
const ledger = clientAs(replica, alice).canister<icrc1.Actor>(icrc1.actor, {
|
|
114
|
+
id: LEDGER,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
await expect(
|
|
118
|
+
ledger.icrc1_balance_of({ owner: ALICE, subaccount: null })
|
|
119
|
+
).resolves.toBe(1_000_000n)
|
|
120
|
+
await expect(ledger.icrc1_transfer(transferTo(BOB, 100n))).resolves.toBe(7n)
|
|
121
|
+
expect(balances.get(ALICE)).toBe(1_000_000n - 100n - FEE)
|
|
122
|
+
expect(balances.get(BOB)).toBe(100n)
|
|
123
|
+
// nat8 decodes to a number, every other integer to a bigint.
|
|
124
|
+
await expect(ledger.icrc1_decimals()).resolves.toBe(8)
|
|
125
|
+
await expect(ledger.icrc1_fee()).resolves.toBe(FEE)
|
|
126
|
+
await expect(ledger.icrc1_minting_account()).resolves.toBeNull()
|
|
127
|
+
expect(seen).toEqual(["icrc1_transfer"])
|
|
128
|
+
expect(
|
|
129
|
+
canisterRequests(replica).map(({ endpoint, methodName }) => [
|
|
130
|
+
endpoint,
|
|
131
|
+
methodName,
|
|
132
|
+
])
|
|
133
|
+
).toEqual([
|
|
134
|
+
["query", "icrc1_balance_of"],
|
|
135
|
+
["call", "icrc1_transfer"],
|
|
136
|
+
["query", "icrc1_decimals"],
|
|
137
|
+
["query", "icrc1_fee"],
|
|
138
|
+
["query", "icrc1_minting_account"],
|
|
139
|
+
])
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it("hands a handler exactly the values it was called with", async () => {
|
|
143
|
+
let received: unknown
|
|
144
|
+
const replica = replicaWith({
|
|
145
|
+
[LEDGER]: serve<icrc1.Actor>(icrc1.actor, {
|
|
146
|
+
icrc1_transfer: ([arg]) => {
|
|
147
|
+
received = arg
|
|
148
|
+
return { tag: "Ok", value: 1n }
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
})
|
|
152
|
+
const ledger = clientAs(replica, alice).canister<icrc1.Actor>(icrc1.actor, {
|
|
153
|
+
id: LEDGER,
|
|
154
|
+
})
|
|
155
|
+
await ledger.icrc1_transfer(transferTo(BOB, 5n))
|
|
156
|
+
expect(received).toEqual(transferTo(BOB, 5n))
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it("collapses the reply as the generated Actor types it: nothing, the value, or the tuple", async () => {
|
|
160
|
+
const { canister } = shapesSetup()
|
|
161
|
+
await expect(canister.nothing()).resolves.toBeUndefined()
|
|
162
|
+
await expect(canister.one(41n)).resolves.toBe(42n)
|
|
163
|
+
await expect(canister.many()).resolves.toEqual([1n, "two", true])
|
|
164
|
+
await expect(canister.pair(1n, "b")).resolves.toEqual({
|
|
165
|
+
left: 1n,
|
|
166
|
+
right: "b",
|
|
167
|
+
})
|
|
168
|
+
await expect(canister.small(7)).resolves.toBe(8)
|
|
169
|
+
await expect(canister.bytes(new Uint8Array([1, 2, 3]))).resolves.toEqual(
|
|
170
|
+
new Uint8Array([3, 2, 1])
|
|
171
|
+
)
|
|
172
|
+
await expect(canister.who()).resolves.toBe(ALICE)
|
|
173
|
+
await expect(canister.lookup(BOB)).resolves.toBeNull()
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it("keeps a collapsing opt boxed both ways", async () => {
|
|
177
|
+
const { canister } = shapesSetup()
|
|
178
|
+
await expect(canister.maybe({ some: 3n })).resolves.toEqual({ some: 3n })
|
|
179
|
+
await expect(canister.maybe({ some: null })).resolves.toEqual({
|
|
180
|
+
some: null,
|
|
181
|
+
})
|
|
182
|
+
await expect(canister.maybe(null)).resolves.toBeNull()
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it("unwraps a lower-case result and rejects its err arm as canister_err", async () => {
|
|
186
|
+
const { canister, replica } = shapesSetup()
|
|
187
|
+
await expect(canister.bump(1n)).resolves.toBe(2n)
|
|
188
|
+
await expect(canister.outcome(5n)).resolves.toBe(5n)
|
|
189
|
+
const error: unknown = await canister.bump(0n).catch((e: unknown) => e)
|
|
190
|
+
expect(isReactorError(error)).toBe(true)
|
|
191
|
+
expect(error).toMatchObject({
|
|
192
|
+
kind: "canister_err",
|
|
193
|
+
err: "nothing to bump",
|
|
194
|
+
mayHaveExecuted: false,
|
|
195
|
+
method: "bump",
|
|
196
|
+
canisterId: SHAPES,
|
|
197
|
+
})
|
|
198
|
+
await expect(canister.outcome(0n)).rejects.toMatchObject({
|
|
199
|
+
kind: "canister_err",
|
|
200
|
+
err: "zero",
|
|
201
|
+
})
|
|
202
|
+
// A canister_err is the canister's answer, never sent again.
|
|
203
|
+
expect(requestsFor(replica, "bump")).toHaveLength(2)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it("resolves an ok arm that carries nothing with null", async () => {
|
|
207
|
+
const { canister } = shapesSetup()
|
|
208
|
+
await expect(canister.flag(true)).resolves.toBeNull()
|
|
209
|
+
await expect(canister.flag(false)).rejects.toMatchObject({
|
|
210
|
+
kind: "canister_err",
|
|
211
|
+
err: "off",
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it("does not unwrap a variant with arms besides Ok and Err", async () => {
|
|
216
|
+
const { canister } = shapesSetup()
|
|
217
|
+
await expect(canister.three()).resolves.toEqual({ tag: "Pending" })
|
|
218
|
+
const { canister: other } = shapesSetup({
|
|
219
|
+
three: () => ({ tag: "Err", value: "kept" }),
|
|
220
|
+
})
|
|
221
|
+
await expect(other.three()).resolves.toEqual({ tag: "Err", value: "kept" })
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it("rejects an ICRC-1 Err as canister_err with the typed payload, sent once", async () => {
|
|
225
|
+
const replica = replicaWith({ [LEDGER]: ledgerCanister(new Map()) })
|
|
226
|
+
const ledger = clientAs(replica, alice).canister<icrc1.Actor>(icrc1.actor, {
|
|
227
|
+
id: LEDGER,
|
|
228
|
+
})
|
|
229
|
+
const error = await ledger
|
|
230
|
+
.icrc1_transfer(transferTo(BOB, 1n))
|
|
231
|
+
.catch((e: unknown) => e)
|
|
232
|
+
expect(error).toMatchObject({
|
|
233
|
+
kind: "canister_err",
|
|
234
|
+
mayHaveExecuted: false,
|
|
235
|
+
err: { tag: "InsufficientFunds", value: { balance: 0n } },
|
|
236
|
+
})
|
|
237
|
+
expect((error as Error).message).toContain("Err InsufficientFunds")
|
|
238
|
+
expect(requestsFor(replica, "icrc1_transfer")).toHaveLength(1)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it("sends a oneway and settles once it is accepted", async () => {
|
|
242
|
+
const { canister, replica } = shapesSetup()
|
|
243
|
+
await expect(canister.fire("go")).resolves.toBeUndefined()
|
|
244
|
+
expect(requestsFor(replica, "fire")).toMatchObject([{ endpoint: "call" }])
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it("refuses arguments that do not encode, naming the path, and sends nothing", async () => {
|
|
248
|
+
const { canister, replica } = shapesSetup()
|
|
249
|
+
const error = await canister
|
|
250
|
+
// @ts-expect-error a nat is a bigint: the codec refuses a number
|
|
251
|
+
.one(1)
|
|
252
|
+
.catch((e: unknown) => e)
|
|
253
|
+
expect(error).toMatchObject({
|
|
254
|
+
kind: "invalid_args",
|
|
255
|
+
mayHaveExecuted: false,
|
|
256
|
+
issues: [{ code: "invalid_type", path: "$args[0]" }],
|
|
257
|
+
})
|
|
258
|
+
await expect(
|
|
259
|
+
canister.lookup("AAAAA-AA" as Principal)
|
|
260
|
+
).rejects.toMatchObject({
|
|
261
|
+
kind: "invalid_args",
|
|
262
|
+
issues: [{ path: "$args[0]" }],
|
|
263
|
+
})
|
|
264
|
+
expect(canisterRequests(replica)).toEqual([])
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it("rejects a reply that does not decode as invalid_reply: may have executed for an update only", async () => {
|
|
268
|
+
const wrong = new Uint8Array(
|
|
269
|
+
(
|
|
270
|
+
encodeArgs([shapes.Pair], [{ left: 1n, right: "x" }]) as {
|
|
271
|
+
bytes: Uint8Array
|
|
272
|
+
}
|
|
273
|
+
).bytes
|
|
274
|
+
)
|
|
275
|
+
const replica = replicaWith({
|
|
276
|
+
[SHAPES]: { query: () => wrong, update: () => wrong },
|
|
277
|
+
})
|
|
278
|
+
const canister = clientAs(replica, alice).canister<shapes.Actor>(
|
|
279
|
+
shapes.actor,
|
|
280
|
+
{ id: SHAPES }
|
|
281
|
+
)
|
|
282
|
+
await expect(canister.one(1n)).rejects.toMatchObject({
|
|
283
|
+
kind: "invalid_reply",
|
|
284
|
+
mayHaveExecuted: false,
|
|
285
|
+
})
|
|
286
|
+
await expect(canister.bump(1n)).rejects.toMatchObject({
|
|
287
|
+
kind: "invalid_reply",
|
|
288
|
+
mayHaveExecuted: true,
|
|
289
|
+
})
|
|
290
|
+
expect(requestsFor(replica, "bump")).toHaveLength(1)
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it("decodes replies no deeper than the client's maxDepth", async () => {
|
|
294
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
295
|
+
const shallow = clientAs(replica, alice, {
|
|
296
|
+
maxDepth: 1,
|
|
297
|
+
}).canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
298
|
+
const deep = clientAs(replica, alice, {
|
|
299
|
+
maxDepth: 2,
|
|
300
|
+
}).canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
301
|
+
await expect(shallow.maybe({ some: 1n })).rejects.toMatchObject({
|
|
302
|
+
kind: "invalid_reply",
|
|
303
|
+
issues: [{ code: "resource_limit_exceeded" }],
|
|
304
|
+
})
|
|
305
|
+
await expect(deep.maybe({ some: 1n })).resolves.toEqual({ some: 1n })
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it("decodes a blob reply over a million bytes with the default options", async () => {
|
|
309
|
+
// An SNS wasm or an asset chunk: `@meantjs/schema` charges a blob one element
|
|
310
|
+
// per byte, so its default budget alone would refuse this reply.
|
|
311
|
+
const wasm = new Uint8Array(2_000_000).fill(7)
|
|
312
|
+
const { canister } = shapesSetup({ bytes: () => wasm })
|
|
313
|
+
const reply = await canister.bytes(new Uint8Array([1]))
|
|
314
|
+
// Byte by byte, toEqual takes seconds on two million entries.
|
|
315
|
+
expect(Buffer.from(reply).equals(Buffer.from(wasm))).toBe(true)
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it("gives values that take no bytes the whole budget, beside a large blob", async () => {
|
|
319
|
+
// The budget is beyond one value per byte: a blob over a million bytes
|
|
320
|
+
// does not use it up for the values beside it.
|
|
321
|
+
const reply = encodeArgs(
|
|
322
|
+
[c.blob(), c.vec(c.null)],
|
|
323
|
+
[
|
|
324
|
+
new Uint8Array(1_100_000).fill(3),
|
|
325
|
+
Array.from({ length: 900_000 }, () => null),
|
|
326
|
+
],
|
|
327
|
+
{ maxElements: 3_000_000 }
|
|
328
|
+
)
|
|
329
|
+
if (!reply.ok) throw new Error("the test reply did not encode")
|
|
330
|
+
const replica = replicaWith({ [SHAPES]: { query: () => reply.bytes } })
|
|
331
|
+
const canister = clientAs(replica, alice).canister<shapes.Actor>(
|
|
332
|
+
shapes.actor,
|
|
333
|
+
{ id: SHAPES }
|
|
334
|
+
)
|
|
335
|
+
const bytes = await canister.bytes(new Uint8Array([1]))
|
|
336
|
+
expect(bytes.length).toBe(1_100_000)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it("still refuses a reply whose values take no bytes past the element budget", async () => {
|
|
340
|
+
// A trailing `vec null` the method does not declare is skipped, and each
|
|
341
|
+
// skipped null is charged, though the whole reply is a few bytes.
|
|
342
|
+
const bomb = encodeArgs(
|
|
343
|
+
[c.blob(), c.vec(c.null)],
|
|
344
|
+
[new Uint8Array([1]), Array.from({ length: 1_100_000 }, () => null)],
|
|
345
|
+
{ maxElements: 2_000_000 }
|
|
346
|
+
)
|
|
347
|
+
if (!bomb.ok) throw new Error("the test reply did not encode")
|
|
348
|
+
expect(bomb.bytes.length).toBeLessThan(100)
|
|
349
|
+
const replica = replicaWith({ [SHAPES]: { query: () => bomb.bytes } })
|
|
350
|
+
const canister = clientAs(replica, alice).canister<shapes.Actor>(
|
|
351
|
+
shapes.actor,
|
|
352
|
+
{ id: SHAPES }
|
|
353
|
+
)
|
|
354
|
+
await expect(canister.bytes(new Uint8Array([1]))).rejects.toMatchObject({
|
|
355
|
+
kind: "invalid_reply",
|
|
356
|
+
issues: [{ code: "resource_limit_exceeded" }],
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
describe("who an update is sent as", () => {
|
|
362
|
+
it("refuses an update while nobody is signed in, and sends nothing", async () => {
|
|
363
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
364
|
+
const auth = createTestAuth({ seed: 1, signedIn: false })
|
|
365
|
+
const canister = clientWithAuth(replica, auth).canister<shapes.Actor>(
|
|
366
|
+
shapes.actor,
|
|
367
|
+
{ id: SHAPES }
|
|
368
|
+
)
|
|
369
|
+
for (const write of [() => canister.bump(1n), () => canister.fire("x")]) {
|
|
370
|
+
await expect(write()).rejects.toMatchObject({
|
|
371
|
+
kind: "unauthenticated",
|
|
372
|
+
code: "anonymous_write",
|
|
373
|
+
mayHaveExecuted: false,
|
|
374
|
+
})
|
|
375
|
+
}
|
|
376
|
+
expect(canisterRequests(replica)).toEqual([])
|
|
377
|
+
// A read needs nobody.
|
|
378
|
+
await expect(canister.who()).resolves.toBe(ANONYMOUS)
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
it('refuses an update of a client built with identity: "anonymous", and sends nothing', async () => {
|
|
382
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
383
|
+
const canister = clientAs(replica, "anonymous").canister<shapes.Actor>(
|
|
384
|
+
shapes.actor,
|
|
385
|
+
{ id: SHAPES }
|
|
386
|
+
)
|
|
387
|
+
await expect(canister.note("hi")).rejects.toMatchObject({
|
|
388
|
+
kind: "unauthenticated",
|
|
389
|
+
})
|
|
390
|
+
expect(canisterRequests(replica)).toEqual([])
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
it("sends an update as an explicit AnonymousIdentity, the written-out way to write anonymously", async () => {
|
|
394
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
395
|
+
const canister = clientAs(
|
|
396
|
+
replica,
|
|
397
|
+
new AnonymousIdentity()
|
|
398
|
+
).canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
399
|
+
await expect(canister.address("x")).resolves.toBe(`x:${ANONYMOUS}`)
|
|
400
|
+
expect(requestsFor(replica, "address")).toMatchObject([
|
|
401
|
+
{ endpoint: "call", caller: ANONYMOUS },
|
|
402
|
+
])
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
it("sends a call as whoever is signed in when it is made", async () => {
|
|
406
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
407
|
+
const auth = createTestAuth({ seed: 1 })
|
|
408
|
+
const client = clientWithAuth(replica, auth)
|
|
409
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, {
|
|
410
|
+
id: SHAPES,
|
|
411
|
+
})
|
|
412
|
+
const first = client.caller()
|
|
413
|
+
await expect(canister.who()).resolves.toBe(first)
|
|
414
|
+
auth.switchTo(2)
|
|
415
|
+
await expect(canister.who()).resolves.toBe(client.caller())
|
|
416
|
+
expect(client.caller()).not.toBe(first)
|
|
417
|
+
})
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
describe("what is sent again", () => {
|
|
421
|
+
/** Waits past both re-send delays, so a re-send would be in the log. */
|
|
422
|
+
const pastResendDelays = () => sleep(300 + 600 + 200)
|
|
423
|
+
|
|
424
|
+
it.each([
|
|
425
|
+
[4, "rejected"],
|
|
426
|
+
[5, "rejected"],
|
|
427
|
+
] as const)(
|
|
428
|
+
"never re-sends an update rejected with code %i",
|
|
429
|
+
async (code, kind) => {
|
|
430
|
+
const { canister, replica } = shapesSetup({
|
|
431
|
+
bump: () => replica.reject(code),
|
|
432
|
+
})
|
|
433
|
+
await expect(canister.bump(1n)).rejects.toMatchObject({
|
|
434
|
+
kind,
|
|
435
|
+
rejectCode: code,
|
|
436
|
+
mayHaveExecuted: true,
|
|
437
|
+
})
|
|
438
|
+
await pastResendDelays()
|
|
439
|
+
expect(requestsFor(replica, "bump")).toHaveLength(1)
|
|
440
|
+
}
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
it("never re-sends an update whose reply was lost", async () => {
|
|
444
|
+
const { canister, replica } = shapesSetup()
|
|
445
|
+
replica.dropNextReply()
|
|
446
|
+
await expect(canister.bump(1n)).rejects.toMatchObject({
|
|
447
|
+
kind: "outcome_unknown",
|
|
448
|
+
mayHaveExecuted: true,
|
|
449
|
+
})
|
|
450
|
+
await pastResendDelays()
|
|
451
|
+
expect(requestsFor(replica, "bump")).toMatchObject([{ dropped: true }])
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
it.each([408, 500, 503])(
|
|
455
|
+
"never re-sends an update answered with HTTP %i",
|
|
456
|
+
async (status) => {
|
|
457
|
+
const { canister, replica } = shapesSetup()
|
|
458
|
+
replica.refuseNext(status)
|
|
459
|
+
await expect(canister.bump(1n)).rejects.toMatchObject({
|
|
460
|
+
kind: "outcome_unknown",
|
|
461
|
+
httpStatus: status,
|
|
462
|
+
mayHaveExecuted: true,
|
|
463
|
+
})
|
|
464
|
+
await pastResendDelays()
|
|
465
|
+
expect(requestsFor(replica, "bump")).toHaveLength(1)
|
|
466
|
+
}
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
it("re-sends an update once after a SysTransient reject, and resolves", async () => {
|
|
470
|
+
let attempts = 0
|
|
471
|
+
const { canister, replica } = shapesSetup({
|
|
472
|
+
bump: ([n]) => {
|
|
473
|
+
attempts += 1
|
|
474
|
+
if (attempts === 1) replica.reject(2)
|
|
475
|
+
return { tag: "ok", value: n }
|
|
476
|
+
},
|
|
477
|
+
})
|
|
478
|
+
await expect(canister.bump(9n)).resolves.toBe(9n)
|
|
479
|
+
expect(requestsFor(replica, "bump")).toHaveLength(2)
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
it("re-sends an update once after HTTP 429, and resolves", async () => {
|
|
483
|
+
const { canister, replica } = shapesSetup()
|
|
484
|
+
replica.refuseNext(429)
|
|
485
|
+
await expect(canister.bump(1n)).resolves.toBe(2n)
|
|
486
|
+
expect(requestsFor(replica, "bump")).toMatchObject([
|
|
487
|
+
{ refused: expect.stringContaining("429") },
|
|
488
|
+
{ methodName: "bump" },
|
|
489
|
+
])
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
it("re-sends an update at most twice", async () => {
|
|
493
|
+
const { canister, replica } = shapesSetup()
|
|
494
|
+
replica.refuseNext(429, 3)
|
|
495
|
+
await expect(canister.bump(1n)).rejects.toMatchObject({
|
|
496
|
+
kind: "not_delivered",
|
|
497
|
+
httpStatus: 429,
|
|
498
|
+
mayHaveExecuted: false,
|
|
499
|
+
})
|
|
500
|
+
await pastResendDelays()
|
|
501
|
+
expect(requestsFor(replica, "bump")).toHaveLength(3)
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
it.each([
|
|
505
|
+
["a SysTransient reject", "reject"],
|
|
506
|
+
["HTTP 429", "429"],
|
|
507
|
+
] as const)(
|
|
508
|
+
"never re-sends an update to aaaaa-aa after %s",
|
|
509
|
+
async (_label, failure) => {
|
|
510
|
+
const replica = replicaWith({})
|
|
511
|
+
replica.addCanister(
|
|
512
|
+
MANAGEMENT,
|
|
513
|
+
serve<management.Actor>(management.actor, {
|
|
514
|
+
start_canister: () => replica.reject(2),
|
|
515
|
+
})
|
|
516
|
+
)
|
|
517
|
+
if (failure === "429") replica.refuseNext(429)
|
|
518
|
+
const ic = clientAs(replica, alice).canister<management.Actor>(
|
|
519
|
+
management.actor,
|
|
520
|
+
{ id: MANAGEMENT }
|
|
521
|
+
)
|
|
522
|
+
await expect(
|
|
523
|
+
ic.start_canister({ canister_id: principal(SHAPES) })
|
|
524
|
+
).rejects.toSatisfy(isReactorError)
|
|
525
|
+
await pastResendDelays()
|
|
526
|
+
expect(requestsFor(replica, "start_canister")).toHaveLength(1)
|
|
527
|
+
}
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
it("re-sends a direct read after a failure that proves it was not delivered", async () => {
|
|
531
|
+
const { canister, replica } = shapesSetup()
|
|
532
|
+
replica.refuseNext(503)
|
|
533
|
+
await expect(canister.one(1n)).resolves.toBe(2n)
|
|
534
|
+
expect(requestsFor(replica, "one")).toHaveLength(2)
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
it("does not re-send a direct read the canister rejected", async () => {
|
|
538
|
+
const { canister, replica } = shapesSetup({
|
|
539
|
+
one: () => replica.reject(4),
|
|
540
|
+
})
|
|
541
|
+
await expect(canister.one(1n)).rejects.toMatchObject({
|
|
542
|
+
kind: "rejected",
|
|
543
|
+
rejectCode: 4,
|
|
544
|
+
mayHaveExecuted: false,
|
|
545
|
+
})
|
|
546
|
+
await pastResendDelays()
|
|
547
|
+
expect(requestsFor(replica, "one")).toHaveLength(1)
|
|
548
|
+
})
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
// ---------------------------------------------------------------------------
|
|
552
|
+
// Answers a replica gives that the fake does not: a 202, a certificate without
|
|
553
|
+
// the request's status, an uncertified reject of the call itself, a failed
|
|
554
|
+
// poll. Each wraps the fake's `fetch`; the fake still runs (or never sees) the
|
|
555
|
+
// call, so its log says what executed.
|
|
556
|
+
// ---------------------------------------------------------------------------
|
|
557
|
+
|
|
558
|
+
const STATUS = new TextEncoder().encode("request_status")
|
|
559
|
+
const same = (a: Uint8Array, b: Uint8Array) =>
|
|
560
|
+
a.length === b.length && a.every((byte, i) => byte === b[i])
|
|
561
|
+
const urlOf = (input: RequestInfo | URL) =>
|
|
562
|
+
String(input instanceof Request ? input.url : input)
|
|
563
|
+
const cborOf = async (body: BodyInit | null | undefined): Promise<unknown> =>
|
|
564
|
+
Cbor.decode(new Uint8Array(await new Response(body).arrayBuffer()))
|
|
565
|
+
const cborResponse = (body: unknown) =>
|
|
566
|
+
new Response(Cbor.encode(body) as BodyInit, {
|
|
567
|
+
status: 200,
|
|
568
|
+
headers: { "content-type": "application/cbor" },
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
/** The request ids, as hex, whose status a `read_state` request asks for. */
|
|
572
|
+
async function statusesAskedFor(init?: RequestInit): Promise<string[]> {
|
|
573
|
+
const envelope = (await cborOf(init?.body)) as {
|
|
574
|
+
content: { paths: Uint8Array[][] }
|
|
575
|
+
}
|
|
576
|
+
return envelope.content.paths
|
|
577
|
+
.filter(([label]) => same(label, STATUS))
|
|
578
|
+
.map(([, id]) => toHex(id))
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* A certificate the replica signed for another call: it verifies, and it holds
|
|
583
|
+
* no status for any request made after it.
|
|
584
|
+
*/
|
|
585
|
+
async function certificateOfAnotherCall(
|
|
586
|
+
replica: FakeReplica
|
|
587
|
+
): Promise<Uint8Array> {
|
|
588
|
+
let certificate: Uint8Array | undefined
|
|
589
|
+
const spy = async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
590
|
+
const response = await replica.fetch(input, init)
|
|
591
|
+
if (urlOf(input).endsWith("/call")) {
|
|
592
|
+
const body = await cborOf(await response.clone().arrayBuffer())
|
|
593
|
+
certificate = (body as { certificate: Uint8Array }).certificate
|
|
594
|
+
}
|
|
595
|
+
return response
|
|
596
|
+
}
|
|
597
|
+
await clientAs({ ...replica, fetch: spy }, alice)
|
|
598
|
+
.canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
599
|
+
.note("earlier")
|
|
600
|
+
if (certificate === undefined) throw new Error("the replica answered no call")
|
|
601
|
+
return certificate
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Wraps the replica's `fetch` so that a call is run but answered the way a
|
|
606
|
+
* busy replica can answer it: by default 202 (accepted, the answer to be
|
|
607
|
+
* polled for), or with `answer`, a certificate in the synchronous answer's
|
|
608
|
+
* place. The certificate the replica made is handed out only to a poll of
|
|
609
|
+
* the request's status, once the first `failures` polls have failed with
|
|
610
|
+
* `failWith` (a lost connection, or an HTTP status). With `pending`, every
|
|
611
|
+
* poll is answered with that certificate instead, so the call never settles.
|
|
612
|
+
*/
|
|
613
|
+
function answerLater(
|
|
614
|
+
fetch: typeof globalThis.fetch,
|
|
615
|
+
{
|
|
616
|
+
answer,
|
|
617
|
+
failures = 0,
|
|
618
|
+
failWith = "connection",
|
|
619
|
+
pending,
|
|
620
|
+
}: {
|
|
621
|
+
answer?: Uint8Array
|
|
622
|
+
failures?: number
|
|
623
|
+
failWith?: "connection" | number
|
|
624
|
+
pending?: Uint8Array
|
|
625
|
+
} = {}
|
|
626
|
+
): {
|
|
627
|
+
fetch: typeof globalThis.fetch
|
|
628
|
+
/** How many times the request's status was asked for. */
|
|
629
|
+
polls: () => number
|
|
630
|
+
/** The request id each poll asked about, as hex. */
|
|
631
|
+
polled: () => string[]
|
|
632
|
+
} {
|
|
633
|
+
let certificate: Uint8Array | undefined
|
|
634
|
+
const polled: string[] = []
|
|
635
|
+
const wrapped = async (
|
|
636
|
+
input: RequestInfo | URL,
|
|
637
|
+
init?: RequestInit
|
|
638
|
+
): Promise<Response> => {
|
|
639
|
+
const url = urlOf(input)
|
|
640
|
+
if (url.endsWith("/call")) {
|
|
641
|
+
const response = await fetch(input, init)
|
|
642
|
+
const body = await cborOf(await response.arrayBuffer())
|
|
643
|
+
certificate = (body as { certificate: Uint8Array }).certificate
|
|
644
|
+
return answer === undefined
|
|
645
|
+
? new Response(null, { status: 202 })
|
|
646
|
+
: cborResponse({ status: "replied", certificate: answer })
|
|
647
|
+
}
|
|
648
|
+
if (url.endsWith("/read_state") && certificate !== undefined) {
|
|
649
|
+
const asked = await statusesAskedFor(init)
|
|
650
|
+
if (asked.length > 0) {
|
|
651
|
+
polled.push(...asked)
|
|
652
|
+
if (pending !== undefined) return cborResponse({ certificate: pending })
|
|
653
|
+
if (polled.length <= failures) {
|
|
654
|
+
if (failWith === "connection") {
|
|
655
|
+
throw new TypeError("the connection was lost")
|
|
656
|
+
}
|
|
657
|
+
return new Response("busy", { status: failWith })
|
|
658
|
+
}
|
|
659
|
+
return cborResponse({ certificate })
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return fetch(input, init)
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
fetch: wrapped,
|
|
666
|
+
polls: () => polled.length,
|
|
667
|
+
polled: () => [...polled],
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Wraps the replica's `fetch` so that its first calls are refused the way a
|
|
673
|
+
* replica refuses a call before it runs (a canister's `inspect_message`
|
|
674
|
+
* saying no, or a full queue): HTTP 200 and an uncertified reject body, one
|
|
675
|
+
* call per code in `codes`. The replica never sees those; later calls reach it.
|
|
676
|
+
*/
|
|
677
|
+
function refuseCalls(
|
|
678
|
+
fetch: typeof globalThis.fetch,
|
|
679
|
+
codes: readonly number[]
|
|
680
|
+
): { fetch: typeof globalThis.fetch; calls: () => number } {
|
|
681
|
+
let calls = 0
|
|
682
|
+
const wrapped = async (
|
|
683
|
+
input: RequestInfo | URL,
|
|
684
|
+
init?: RequestInit
|
|
685
|
+
): Promise<Response> => {
|
|
686
|
+
if (!urlOf(input).endsWith("/call")) return fetch(input, init)
|
|
687
|
+
calls += 1
|
|
688
|
+
const code = codes[calls - 1]
|
|
689
|
+
if (code === undefined) return fetch(input, init)
|
|
690
|
+
return cborResponse({
|
|
691
|
+
status: "non_replicated_rejection",
|
|
692
|
+
reject_code: code,
|
|
693
|
+
reject_message: `refused with code ${code} before it ran`,
|
|
694
|
+
})
|
|
695
|
+
}
|
|
696
|
+
return { fetch: wrapped, calls: () => calls }
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
describe("an update the replica answers later", () => {
|
|
700
|
+
/** A client on `replica` whose requests go through `fetch`, calling the shapes canister. */
|
|
701
|
+
const shapesThrough = (
|
|
702
|
+
replica: FakeReplica,
|
|
703
|
+
fetch: typeof globalThis.fetch
|
|
704
|
+
) =>
|
|
705
|
+
clientAs({ ...replica, fetch }, alice).canister<shapes.Actor>(
|
|
706
|
+
shapes.actor,
|
|
707
|
+
{ id: SHAPES }
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
it("polls the same request again after a lost poll, and resolves with the reply", async () => {
|
|
711
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
712
|
+
const later = answerLater(replica.fetch, { failures: 1 })
|
|
713
|
+
await expect(shapesThrough(replica, later.fetch).bump(4n)).resolves.toBe(5n)
|
|
714
|
+
expect(later.polls()).toBe(2)
|
|
715
|
+
expect(requestsFor(replica, "bump")).toHaveLength(1)
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
it("polls the same request again after a poll answered HTTP 503, and resolves with the reply", async () => {
|
|
719
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
720
|
+
const later = answerLater(replica.fetch, { failures: 1, failWith: 503 })
|
|
721
|
+
await expect(shapesThrough(replica, later.fetch).bump(4n)).resolves.toBe(5n)
|
|
722
|
+
const polled = later.polled()
|
|
723
|
+
expect(polled).toHaveLength(2)
|
|
724
|
+
expect(polled[1]).toBe(polled[0])
|
|
725
|
+
expect(requestsFor(replica, "bump")).toHaveLength(1)
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
it("gives up as outcome_unknown when polls keep failing, without sending the update again", async () => {
|
|
729
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
730
|
+
const later = answerLater(replica.fetch, { failures: Infinity })
|
|
731
|
+
await expect(
|
|
732
|
+
shapesThrough(replica, later.fetch).bump(4n)
|
|
733
|
+
).rejects.toMatchObject({
|
|
734
|
+
kind: "outcome_unknown",
|
|
735
|
+
mayHaveExecuted: true,
|
|
736
|
+
})
|
|
737
|
+
expect(later.polls()).toBe(4)
|
|
738
|
+
expect(requestsFor(replica, "bump")).toHaveLength(1)
|
|
739
|
+
}, 10_000)
|
|
740
|
+
|
|
741
|
+
// `dispatch` records that the call was answered (`accepted`), so a poll
|
|
742
|
+
// answered 429 is not read as the IC turning the call away (#964). Without
|
|
743
|
+
// that record the update rejects not_delivered and is sent three times, in
|
|
744
|
+
// about 12 s: the 30 s limit lets the test fail on its assertions, not on
|
|
745
|
+
// the clock.
|
|
746
|
+
it("gives up as outcome_unknown when polls are answered HTTP 429, without sending the update again", async () => {
|
|
747
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
748
|
+
const later = answerLater(replica.fetch, {
|
|
749
|
+
failures: Infinity,
|
|
750
|
+
failWith: 429,
|
|
751
|
+
})
|
|
752
|
+
const error = await shapesThrough(replica, later.fetch)
|
|
753
|
+
.bump(4n)
|
|
754
|
+
.catch((e: unknown) => e)
|
|
755
|
+
expect(error).toMatchObject({
|
|
756
|
+
kind: "outcome_unknown",
|
|
757
|
+
mayHaveExecuted: true,
|
|
758
|
+
httpStatus: 429,
|
|
759
|
+
})
|
|
760
|
+
expect(later.polls()).toBe(4)
|
|
761
|
+
expect(requestsFor(replica, "bump")).toHaveLength(1)
|
|
762
|
+
}, 30_000)
|
|
763
|
+
|
|
764
|
+
// The regression test DECISIONS Q8 keeps from v3 (icp-js-core#1330): a
|
|
765
|
+
// synchronous answer whose certificate has no status for the request yet is
|
|
766
|
+
// polled for, not failed.
|
|
767
|
+
it("polls for an update whose synchronous answer holds no status for it, and resolves with the reply", async () => {
|
|
768
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
769
|
+
const earlier = await certificateOfAnotherCall(replica)
|
|
770
|
+
const later = answerLater(replica.fetch, { answer: earlier })
|
|
771
|
+
await expect(shapesThrough(replica, later.fetch).bump(4n)).resolves.toBe(5n)
|
|
772
|
+
expect(later.polls()).toBe(1)
|
|
773
|
+
expect(requestsFor(replica, "bump")).toHaveLength(1)
|
|
774
|
+
})
|
|
775
|
+
|
|
776
|
+
it("rejects an update the replica refused, uncertified, before it ran, and does not send it again", async () => {
|
|
777
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
778
|
+
const refusing = refuseCalls(replica.fetch, [4])
|
|
779
|
+
const error = await shapesThrough(replica, refusing.fetch)
|
|
780
|
+
.bump(1n)
|
|
781
|
+
.catch((e: unknown) => e)
|
|
782
|
+
expect(error).toMatchObject({ kind: "rejected", rejectCode: 4 })
|
|
783
|
+
expect((error as Error).message).toContain("before it ran")
|
|
784
|
+
await sleep(300 + 600 + 200)
|
|
785
|
+
expect(refusing.calls()).toBe(1)
|
|
786
|
+
expect(requestsFor(replica, "bump")).toEqual([])
|
|
787
|
+
})
|
|
788
|
+
|
|
789
|
+
it("re-sends an update the replica refused, uncertified, with a SysTransient reject, and resolves", async () => {
|
|
790
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
791
|
+
const refusing = refuseCalls(replica.fetch, [2])
|
|
792
|
+
await expect(shapesThrough(replica, refusing.fetch).bump(1n)).resolves.toBe(
|
|
793
|
+
2n
|
|
794
|
+
)
|
|
795
|
+
expect(refusing.calls()).toBe(2)
|
|
796
|
+
expect(requestsFor(replica, "bump")).toHaveLength(1)
|
|
797
|
+
})
|
|
798
|
+
})
|
|
799
|
+
|
|
800
|
+
describe("the target", () => {
|
|
801
|
+
it("is the same canister object for the same service and target, so it can be made in render", () => {
|
|
802
|
+
const replica = replicaWith({})
|
|
803
|
+
const client = clientAs(replica, alice)
|
|
804
|
+
const first = client.canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
805
|
+
expect(client.canister<shapes.Actor>(shapes.actor, { id: SHAPES })).toBe(
|
|
806
|
+
first
|
|
807
|
+
)
|
|
808
|
+
expect(
|
|
809
|
+
client.canister<shapes.Actor>(shapes.actor, {
|
|
810
|
+
id: SHAPES,
|
|
811
|
+
certified: false,
|
|
812
|
+
})
|
|
813
|
+
).toBe(first)
|
|
814
|
+
expect(
|
|
815
|
+
client.canister<shapes.Actor>(shapes.actor, {
|
|
816
|
+
id: SHAPES,
|
|
817
|
+
certified: true,
|
|
818
|
+
})
|
|
819
|
+
).not.toBe(first)
|
|
820
|
+
expect(
|
|
821
|
+
client.canister<shapes.Actor>(shapes.actor, { id: LEDGER })
|
|
822
|
+
).not.toBe(first)
|
|
823
|
+
expect(Object.isFrozen(first)).toBe(true)
|
|
824
|
+
// Its methods and nothing else: no property a Candid name could collide with.
|
|
825
|
+
expect(Object.keys(first).sort()).toEqual(
|
|
826
|
+
[...serviceMethods(shapes.actor).keys()].sort()
|
|
827
|
+
)
|
|
828
|
+
})
|
|
829
|
+
|
|
830
|
+
it("throws a TypeError naming an id that is not principal text", () => {
|
|
831
|
+
const client = clientAs(replicaWith({}), alice)
|
|
832
|
+
expect(() =>
|
|
833
|
+
client.canister<shapes.Actor>(shapes.actor, { id: "not-a-canister" })
|
|
834
|
+
).toThrow(/"not-a-canister" is not a valid canister id/)
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
it.each([
|
|
838
|
+
["both id and name", { id: SHAPES, name: "backend" }],
|
|
839
|
+
["neither", {}],
|
|
840
|
+
["an unknown option", { canisterId: SHAPES }],
|
|
841
|
+
["a certified flag that is not a boolean", { id: SHAPES, certified: 1 }],
|
|
842
|
+
])("throws a TypeError for a target with %s", (_label, target) => {
|
|
843
|
+
const client = clientAs(replicaWith({}), alice)
|
|
844
|
+
expect(() =>
|
|
845
|
+
client.canister<shapes.Actor>(
|
|
846
|
+
shapes.actor,
|
|
847
|
+
target as unknown as { id: string }
|
|
848
|
+
)
|
|
849
|
+
).toThrow(TypeError)
|
|
850
|
+
})
|
|
851
|
+
|
|
852
|
+
it("throws a TypeError for a schema that is not a service", () => {
|
|
853
|
+
const client = clientAs(replicaWith({}), alice)
|
|
854
|
+
expect(() => client.canister(shapes.Pair as never, { id: SHAPES })).toThrow(
|
|
855
|
+
/service schema/
|
|
856
|
+
)
|
|
857
|
+
})
|
|
858
|
+
|
|
859
|
+
it.each([
|
|
860
|
+
["on a server, where there is no cookie to read", false],
|
|
861
|
+
["on a page whose host the cookie is not trusted for", true],
|
|
862
|
+
])(
|
|
863
|
+
"rejects every call to an unresolved { name } %s, and sends nothing",
|
|
864
|
+
async (_label, page) => {
|
|
865
|
+
// The cookie names the canister, but on this page anyone on a sibling
|
|
866
|
+
// subdomain could have written it, so it must not be believed.
|
|
867
|
+
const cookie = icEnvCookie({ "PUBLIC_CANISTER_ID:backend": SHAPES })
|
|
868
|
+
if (page) stubPage("https://app.example.com", { cookie })
|
|
869
|
+
else stubNoWindow({ cookie })
|
|
870
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
871
|
+
const canister = clientAs(replica, alice).canister<shapes.Actor>(
|
|
872
|
+
shapes.actor,
|
|
873
|
+
{ name: "backend" }
|
|
874
|
+
)
|
|
875
|
+
for (const call of [() => canister.who(), () => canister.bump(1n)]) {
|
|
876
|
+
const error = await call().catch((e: unknown) => e)
|
|
877
|
+
expect(error).toMatchObject({
|
|
878
|
+
kind: "invalid_args",
|
|
879
|
+
code: "canister_id_unresolved",
|
|
880
|
+
canisterId: "$unresolved:backend",
|
|
881
|
+
mayHaveExecuted: false,
|
|
882
|
+
})
|
|
883
|
+
expect((error as Error).message).toContain("backend")
|
|
884
|
+
}
|
|
885
|
+
expect(replica.requests).toEqual([])
|
|
886
|
+
}
|
|
887
|
+
)
|
|
888
|
+
})
|
|
889
|
+
|
|
890
|
+
describe("a certified canister", () => {
|
|
891
|
+
it("sends its query methods as replicated calls", async () => {
|
|
892
|
+
const { client, replica } = shapesSetup()
|
|
893
|
+
const certified = client.canister<shapes.Actor>(shapes.actor, {
|
|
894
|
+
id: SHAPES,
|
|
895
|
+
certified: true,
|
|
896
|
+
})
|
|
897
|
+
await expect(certified.one(1n)).resolves.toBe(2n)
|
|
898
|
+
await expect(certified.who()).resolves.toBe(ALICE)
|
|
899
|
+
expect(canisterRequests(replica).map((r) => r.endpoint)).toEqual([
|
|
900
|
+
"call",
|
|
901
|
+
"call",
|
|
902
|
+
])
|
|
903
|
+
})
|
|
904
|
+
|
|
905
|
+
it("lets anybody read certified: a certified read is not a write", async () => {
|
|
906
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
907
|
+
const certified = clientAs(replica, "anonymous").canister<shapes.Actor>(
|
|
908
|
+
shapes.actor,
|
|
909
|
+
{ id: SHAPES, certified: true }
|
|
910
|
+
)
|
|
911
|
+
await expect(certified.who()).resolves.toBe(ANONYMOUS)
|
|
912
|
+
})
|
|
913
|
+
|
|
914
|
+
it("refuses a composite query, which has no certified path, and sends nothing", async () => {
|
|
915
|
+
const { client, replica, canister } = shapesSetup()
|
|
916
|
+
const certified = client.canister<shapes.Actor>(shapes.actor, {
|
|
917
|
+
id: SHAPES,
|
|
918
|
+
certified: true,
|
|
919
|
+
})
|
|
920
|
+
await expect(certified.composite(2n)).rejects.toMatchObject({
|
|
921
|
+
kind: "invalid_args",
|
|
922
|
+
code: "no_certified_path",
|
|
923
|
+
mayHaveExecuted: false,
|
|
924
|
+
})
|
|
925
|
+
expect(canisterRequests(replica)).toEqual([])
|
|
926
|
+
await expect(canister.composite(2n)).resolves.toBe(4n)
|
|
927
|
+
expect(canisterRequests(replica)).toMatchObject([{ endpoint: "query" }])
|
|
928
|
+
})
|
|
929
|
+
|
|
930
|
+
it("stops polling for a read once its signal aborts, and rejects cancelled", async () => {
|
|
931
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
932
|
+
// Every poll is answered without the read's status: it is never ready.
|
|
933
|
+
const later = answerLater(replica.fetch, {
|
|
934
|
+
pending: await certificateOfAnotherCall(replica),
|
|
935
|
+
})
|
|
936
|
+
const client = clientAs({ ...replica, fetch: later.fetch }, alice)
|
|
937
|
+
const certified = client.canister<shapes.Actor>(shapes.actor, {
|
|
938
|
+
id: SHAPES,
|
|
939
|
+
certified: true,
|
|
940
|
+
})
|
|
941
|
+
const { queryFn } = client.queryOptions(certified, "one", 1n)
|
|
942
|
+
// What TanStack does when the last observer of a read goes away: it
|
|
943
|
+
// aborts the signal it gave the query function.
|
|
944
|
+
const controller = new AbortController()
|
|
945
|
+
const read = queryFn({
|
|
946
|
+
signal: controller.signal,
|
|
947
|
+
} as QueryFunctionContext)
|
|
948
|
+
await vi.waitFor(() => expect(later.polls()).toBe(1))
|
|
949
|
+
controller.abort()
|
|
950
|
+
await expect(
|
|
951
|
+
Promise.race([read, sleep(1_000).then(() => "still polling")])
|
|
952
|
+
).rejects.toMatchObject({ kind: "cancelled", mayHaveExecuted: false })
|
|
953
|
+
// The agent's strategy polls again two seconds after the first poll.
|
|
954
|
+
await sleep(2_500)
|
|
955
|
+
expect(later.polls()).toBe(1)
|
|
956
|
+
}, 10_000)
|
|
957
|
+
|
|
958
|
+
it("ends the wait to poll again after a failed poll as soon as the read's signal aborts, and rejects cancelled", async () => {
|
|
959
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
960
|
+
// The first poll is answered HTTP 503, a failure worth asking again
|
|
961
|
+
// after; the second would get the reply.
|
|
962
|
+
const later = answerLater(replica.fetch, { failures: 1, failWith: 503 })
|
|
963
|
+
const client = clientAs({ ...replica, fetch: later.fetch }, alice)
|
|
964
|
+
const certified = client.canister<shapes.Actor>(shapes.actor, {
|
|
965
|
+
id: SHAPES,
|
|
966
|
+
certified: true,
|
|
967
|
+
})
|
|
968
|
+
const { queryFn } = client.queryOptions(certified, "one", 1n)
|
|
969
|
+
const controller = new AbortController()
|
|
970
|
+
const read = queryFn({
|
|
971
|
+
signal: controller.signal,
|
|
972
|
+
} as QueryFunctionContext)
|
|
973
|
+
await vi.waitFor(() => expect(later.polls()).toBe(1))
|
|
974
|
+
controller.abort()
|
|
975
|
+
// The call path waits REPOLL_DELAYS_MS[0], 500 ms, before polling again:
|
|
976
|
+
// the abort must end that wait, not the next loop turn after it.
|
|
977
|
+
await expect(
|
|
978
|
+
Promise.race([read, sleep(250).then(() => "still waiting")])
|
|
979
|
+
).rejects.toMatchObject({ kind: "cancelled", mayHaveExecuted: false })
|
|
980
|
+
await sleep(750)
|
|
981
|
+
expect(later.polls()).toBe(1)
|
|
982
|
+
}, 10_000)
|
|
983
|
+
|
|
984
|
+
it("sends no poll for a read whose signal aborted while its call was on the way, and rejects cancelled", async () => {
|
|
985
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
986
|
+
// The replica answers the call 202, so its reply must be polled for.
|
|
987
|
+
const later = answerLater(replica.fetch)
|
|
988
|
+
const controller = new AbortController()
|
|
989
|
+
// The read's last observer goes away while the replica takes the call.
|
|
990
|
+
const fetch = (
|
|
991
|
+
input: RequestInfo | URL,
|
|
992
|
+
init?: RequestInit
|
|
993
|
+
): Promise<Response> => {
|
|
994
|
+
if (urlOf(input).endsWith("/call")) controller.abort()
|
|
995
|
+
return later.fetch(input, init)
|
|
996
|
+
}
|
|
997
|
+
const client = clientAs({ ...replica, fetch }, alice)
|
|
998
|
+
const certified = client.canister<shapes.Actor>(shapes.actor, {
|
|
999
|
+
id: SHAPES,
|
|
1000
|
+
certified: true,
|
|
1001
|
+
})
|
|
1002
|
+
const { queryFn } = client.queryOptions(certified, "one", 1n)
|
|
1003
|
+
await expect(
|
|
1004
|
+
queryFn({ signal: controller.signal } as QueryFunctionContext)
|
|
1005
|
+
).rejects.toMatchObject({ kind: "cancelled", mayHaveExecuted: false })
|
|
1006
|
+
// A poll started and then abandoned would still go out: give it the time.
|
|
1007
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
1008
|
+
expect(requestsFor(replica, "one")).toHaveLength(1)
|
|
1009
|
+
expect(later.polls()).toBe(0)
|
|
1010
|
+
})
|
|
1011
|
+
})
|
|
1012
|
+
|
|
1013
|
+
describe("a read whose request gets no HTTP answer", () => {
|
|
1014
|
+
// Neither the agent nor the fetch it is given bounds the wait for an HTTP
|
|
1015
|
+
// answer (#875): a proxy that holds the request, or a fake replica whose
|
|
1016
|
+
// handler never returns, leaves the read waiting until its signal aborts.
|
|
1017
|
+
const held = (
|
|
1018
|
+
replica: ReturnType<typeof replicaWith>,
|
|
1019
|
+
endpoint: "call" | "query"
|
|
1020
|
+
) => {
|
|
1021
|
+
let sent = 0
|
|
1022
|
+
const fetch = (
|
|
1023
|
+
input: RequestInfo | URL,
|
|
1024
|
+
init?: RequestInit
|
|
1025
|
+
): Promise<Response> => {
|
|
1026
|
+
if (urlOf(input).endsWith(`/${endpoint}`)) {
|
|
1027
|
+
sent += 1
|
|
1028
|
+
return new Promise<Response>(() => {})
|
|
1029
|
+
}
|
|
1030
|
+
return replica.fetch(input, init)
|
|
1031
|
+
}
|
|
1032
|
+
return { fetch, sent: () => sent }
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
it('ends the wait for an update read with { update: "idempotent" }, which may have executed', async () => {
|
|
1036
|
+
const replica = replicaWith({
|
|
1037
|
+
[SHAPES]: serve<shapes.Actor>(shapes.actor, { address: ([s]) => s }),
|
|
1038
|
+
})
|
|
1039
|
+
const request = held(replica, "call")
|
|
1040
|
+
const client = clientAs({ ...replica, fetch: request.fetch }, alice)
|
|
1041
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
1042
|
+
const { queryFn } = client.queryOptions(canister, "address", "a", {
|
|
1043
|
+
update: "idempotent",
|
|
1044
|
+
})
|
|
1045
|
+
const controller = new AbortController()
|
|
1046
|
+
const read = queryFn({
|
|
1047
|
+
signal: controller.signal,
|
|
1048
|
+
} as QueryFunctionContext)
|
|
1049
|
+
await vi.waitFor(() => expect(request.sent()).toBe(1))
|
|
1050
|
+
controller.abort()
|
|
1051
|
+
// The call request went out: it may have reached the canister.
|
|
1052
|
+
await expect(
|
|
1053
|
+
Promise.race([read, sleep(1_000).then(() => "still waiting")])
|
|
1054
|
+
).rejects.toMatchObject({
|
|
1055
|
+
kind: "cancelled",
|
|
1056
|
+
mayHaveExecuted: true,
|
|
1057
|
+
method: "address",
|
|
1058
|
+
})
|
|
1059
|
+
})
|
|
1060
|
+
|
|
1061
|
+
it("ends the wait for a certified read's poll once its signal aborts", async () => {
|
|
1062
|
+
// The call request is answered 202, accepted, without reaching the
|
|
1063
|
+
// canister, and every read_state that polls for it is held.
|
|
1064
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
1065
|
+
let polls = 0
|
|
1066
|
+
const fetch = (
|
|
1067
|
+
input: RequestInfo | URL,
|
|
1068
|
+
init?: RequestInit
|
|
1069
|
+
): Promise<Response> => {
|
|
1070
|
+
const url = urlOf(input)
|
|
1071
|
+
if (url.endsWith("/call")) {
|
|
1072
|
+
return Promise.resolve(new Response(null, { status: 202 }))
|
|
1073
|
+
}
|
|
1074
|
+
if (url.endsWith("/read_state") && url.includes(SHAPES)) {
|
|
1075
|
+
polls += 1
|
|
1076
|
+
return new Promise<Response>(() => {})
|
|
1077
|
+
}
|
|
1078
|
+
return replica.fetch(input, init)
|
|
1079
|
+
}
|
|
1080
|
+
const client = clientAs({ ...replica, fetch }, alice)
|
|
1081
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, {
|
|
1082
|
+
id: SHAPES,
|
|
1083
|
+
certified: true,
|
|
1084
|
+
})
|
|
1085
|
+
const { queryFn } = client.queryOptions(canister, "one", 1n)
|
|
1086
|
+
const controller = new AbortController()
|
|
1087
|
+
const read = queryFn({
|
|
1088
|
+
signal: controller.signal,
|
|
1089
|
+
} as QueryFunctionContext)
|
|
1090
|
+
await vi.waitFor(() => expect(polls).toBeGreaterThan(0))
|
|
1091
|
+
controller.abort()
|
|
1092
|
+
await expect(
|
|
1093
|
+
Promise.race([read, sleep(1_000).then(() => "still waiting")])
|
|
1094
|
+
).rejects.toMatchObject({
|
|
1095
|
+
kind: "cancelled",
|
|
1096
|
+
mayHaveExecuted: false,
|
|
1097
|
+
method: "one",
|
|
1098
|
+
})
|
|
1099
|
+
})
|
|
1100
|
+
|
|
1101
|
+
it.each([
|
|
1102
|
+
["a query", false, "query"],
|
|
1103
|
+
["a certified read", true, "call"],
|
|
1104
|
+
] as const)(
|
|
1105
|
+
"ends the wait for %s once its signal aborts, and rejects cancelled with nothing executed",
|
|
1106
|
+
async (_, certified, endpoint) => {
|
|
1107
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
1108
|
+
const request = held(replica, endpoint)
|
|
1109
|
+
const client = clientAs({ ...replica, fetch: request.fetch }, alice)
|
|
1110
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, {
|
|
1111
|
+
id: SHAPES,
|
|
1112
|
+
certified,
|
|
1113
|
+
})
|
|
1114
|
+
const { queryFn } = client.queryOptions(canister, "one", 1n)
|
|
1115
|
+
const controller = new AbortController()
|
|
1116
|
+
const read = queryFn({
|
|
1117
|
+
signal: controller.signal,
|
|
1118
|
+
} as QueryFunctionContext)
|
|
1119
|
+
await vi.waitFor(() => expect(request.sent()).toBe(1))
|
|
1120
|
+
controller.abort()
|
|
1121
|
+
await expect(
|
|
1122
|
+
Promise.race([read, sleep(1_000).then(() => "still waiting")])
|
|
1123
|
+
).rejects.toMatchObject({
|
|
1124
|
+
kind: "cancelled",
|
|
1125
|
+
mayHaveExecuted: false,
|
|
1126
|
+
method: "one",
|
|
1127
|
+
})
|
|
1128
|
+
expect(request.sent()).toBe(1)
|
|
1129
|
+
}
|
|
1130
|
+
)
|
|
1131
|
+
})
|
|
1132
|
+
|
|
1133
|
+
describe("a read aborted while it waits for its caller's identity", () => {
|
|
1134
|
+
it("sends nothing, and rejects cancelled", async () => {
|
|
1135
|
+
const gate = deferred()
|
|
1136
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
1137
|
+
const auth = createTestAuth({ seed: 1 })
|
|
1138
|
+
const slowAuth = withIdentity(auth, async () => {
|
|
1139
|
+
const identity = await auth.getIdentity()
|
|
1140
|
+
await gate.promise
|
|
1141
|
+
return identity
|
|
1142
|
+
})
|
|
1143
|
+
const client = clientWithAuth(replica, slowAuth)
|
|
1144
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
1145
|
+
const { queryFn } = client.queryOptions(canister, "one", 1n)
|
|
1146
|
+
const controller = new AbortController()
|
|
1147
|
+
const read = queryFn({ signal: controller.signal } as QueryFunctionContext)
|
|
1148
|
+
await sleep(20)
|
|
1149
|
+
controller.abort()
|
|
1150
|
+
await expect(
|
|
1151
|
+
Promise.race([read, sleep(1_000).then(() => "still waiting")])
|
|
1152
|
+
).rejects.toMatchObject({ kind: "cancelled", mayHaveExecuted: false })
|
|
1153
|
+
gate.resolve()
|
|
1154
|
+
await sleep(50)
|
|
1155
|
+
expect(canisterRequests(replica)).toEqual([])
|
|
1156
|
+
})
|
|
1157
|
+
})
|
|
1158
|
+
|
|
1159
|
+
describe("a call with timeoutMs", () => {
|
|
1160
|
+
// A call bounded by `timeoutMs` stops waiting when it passes (#875): a
|
|
1161
|
+
// write whose request went out may have executed, anything else did not.
|
|
1162
|
+
const within = <T>(call: Promise<T>) =>
|
|
1163
|
+
Promise.race([call, sleep(2_000).then(() => "still waiting")])
|
|
1164
|
+
|
|
1165
|
+
/** A ledger whose transfers never answer, and a client calling it as Alice. */
|
|
1166
|
+
const heldLedger = (target: { timeoutMs?: number } = {}) => {
|
|
1167
|
+
const fake = ledgerCanister(new Map([[ALICE as string, 1_000_000n]]))
|
|
1168
|
+
const replica = replicaWith({
|
|
1169
|
+
[LEDGER]: { query: fake.query, update: () => new Promise(() => {}) },
|
|
1170
|
+
})
|
|
1171
|
+
const client = clientAs(replica, alice)
|
|
1172
|
+
const ledger = client.canister<icrc1.Actor>(icrc1.actor, {
|
|
1173
|
+
id: LEDGER,
|
|
1174
|
+
...target,
|
|
1175
|
+
})
|
|
1176
|
+
return { replica, client, ledger }
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
it("rejects a write whose request went out outcome_unknown, which may have executed, and offers it to resendOf", async () => {
|
|
1180
|
+
const { replica, client, ledger } = heldLedger({ timeoutMs: 50 })
|
|
1181
|
+
const error = await within(
|
|
1182
|
+
ledger.icrc1_transfer(transferTo(BOB, 1n))
|
|
1183
|
+
).catch((failure: unknown) => failure)
|
|
1184
|
+
expect(error).toMatchObject({
|
|
1185
|
+
kind: "outcome_unknown",
|
|
1186
|
+
mayHaveExecuted: true,
|
|
1187
|
+
code: "timeout",
|
|
1188
|
+
method: "icrc1_transfer",
|
|
1189
|
+
})
|
|
1190
|
+
expect(isReactorError(error, ledger, "icrc1_transfer")).toBe(true)
|
|
1191
|
+
expect(
|
|
1192
|
+
client.resendOf(error, ledger, "icrc1_transfer", {
|
|
1193
|
+
dedupedBy: (arg) => arg.created_at_time,
|
|
1194
|
+
})
|
|
1195
|
+
).toBeDefined()
|
|
1196
|
+
// Never sent again for the timeout.
|
|
1197
|
+
await sleep(1_000)
|
|
1198
|
+
expect(requestsFor(replica, "icrc1_transfer")).toHaveLength(1)
|
|
1199
|
+
})
|
|
1200
|
+
|
|
1201
|
+
it.each([
|
|
1202
|
+
["a query", false],
|
|
1203
|
+
["a certified read", true],
|
|
1204
|
+
] as const)(
|
|
1205
|
+
"rejects %s whose canister never answers cancelled, with nothing executed",
|
|
1206
|
+
async (_, certified) => {
|
|
1207
|
+
const replica = replicaWith({
|
|
1208
|
+
[SHAPES]: shapesCanister({ one: () => new Promise(() => {}) }),
|
|
1209
|
+
})
|
|
1210
|
+
const canister = clientAs(replica, alice).canister<shapes.Actor>(
|
|
1211
|
+
shapes.actor,
|
|
1212
|
+
{ id: SHAPES, certified, timeoutMs: 50 }
|
|
1213
|
+
)
|
|
1214
|
+
await expect(within(canister.one(1n))).rejects.toMatchObject({
|
|
1215
|
+
kind: "cancelled",
|
|
1216
|
+
mayHaveExecuted: false,
|
|
1217
|
+
code: "timeout",
|
|
1218
|
+
method: "one",
|
|
1219
|
+
})
|
|
1220
|
+
expect(requestsFor(replica, "one")).toHaveLength(1)
|
|
1221
|
+
}
|
|
1222
|
+
)
|
|
1223
|
+
|
|
1224
|
+
it("rejects a write whose poll gets no answer outcome_unknown, which may have executed", async () => {
|
|
1225
|
+
// The call request is answered 202, accepted, without reaching the
|
|
1226
|
+
// canister, and every read_state that polls for it is held.
|
|
1227
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
1228
|
+
let polls = 0
|
|
1229
|
+
const fetch = (
|
|
1230
|
+
input: RequestInfo | URL,
|
|
1231
|
+
init?: RequestInit
|
|
1232
|
+
): Promise<Response> => {
|
|
1233
|
+
const url = urlOf(input)
|
|
1234
|
+
if (url.endsWith("/call")) {
|
|
1235
|
+
return Promise.resolve(new Response(null, { status: 202 }))
|
|
1236
|
+
}
|
|
1237
|
+
if (url.endsWith("/read_state") && url.includes(SHAPES)) {
|
|
1238
|
+
polls += 1
|
|
1239
|
+
return new Promise<Response>(() => {})
|
|
1240
|
+
}
|
|
1241
|
+
return replica.fetch(input, init)
|
|
1242
|
+
}
|
|
1243
|
+
const canister = clientAs(
|
|
1244
|
+
{ ...replica, fetch },
|
|
1245
|
+
alice
|
|
1246
|
+
).canister<shapes.Actor>(shapes.actor, { id: SHAPES, timeoutMs: 200 })
|
|
1247
|
+
await expect(within(canister.bump(1n))).rejects.toMatchObject({
|
|
1248
|
+
kind: "outcome_unknown",
|
|
1249
|
+
mayHaveExecuted: true,
|
|
1250
|
+
code: "timeout",
|
|
1251
|
+
})
|
|
1252
|
+
expect(polls).toBeGreaterThan(0)
|
|
1253
|
+
})
|
|
1254
|
+
|
|
1255
|
+
it("rejects a write that times out while it waits to be re-sent cancelled, and never sends it again", async () => {
|
|
1256
|
+
// A 429 proves the first attempt was not delivered; the re-send would
|
|
1257
|
+
// go out 300 ms later, after the timeout.
|
|
1258
|
+
const { client, replica } = shapesSetup()
|
|
1259
|
+
replica.refuseNext(429)
|
|
1260
|
+
const bounded = client.canister<shapes.Actor>(shapes.actor, {
|
|
1261
|
+
id: SHAPES,
|
|
1262
|
+
timeoutMs: 100,
|
|
1263
|
+
})
|
|
1264
|
+
const started = Date.now()
|
|
1265
|
+
await expect(within(bounded.bump(1n))).rejects.toMatchObject({
|
|
1266
|
+
kind: "cancelled",
|
|
1267
|
+
mayHaveExecuted: false,
|
|
1268
|
+
code: "timeout",
|
|
1269
|
+
})
|
|
1270
|
+
// At the timeout, not when the wait before the re-send would have ended.
|
|
1271
|
+
expect(Date.now() - started).toBeLessThan(280)
|
|
1272
|
+
await sleep(300 + 600 + 200)
|
|
1273
|
+
expect(requestsFor(replica, "bump")).toMatchObject([
|
|
1274
|
+
{ refused: expect.stringContaining("429") },
|
|
1275
|
+
])
|
|
1276
|
+
})
|
|
1277
|
+
|
|
1278
|
+
it("rejects a write still waiting for its caller's identity cancelled, and sends nothing", async () => {
|
|
1279
|
+
const gate = deferred()
|
|
1280
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
1281
|
+
const auth = createTestAuth({ seed: 1 })
|
|
1282
|
+
const slowAuth = withIdentity(auth, async () => {
|
|
1283
|
+
const identity = await auth.getIdentity()
|
|
1284
|
+
await gate.promise
|
|
1285
|
+
return identity
|
|
1286
|
+
})
|
|
1287
|
+
const canister = clientWithAuth(replica, slowAuth).canister<shapes.Actor>(
|
|
1288
|
+
shapes.actor,
|
|
1289
|
+
{ id: SHAPES, timeoutMs: 50 }
|
|
1290
|
+
)
|
|
1291
|
+
await expect(within(canister.bump(1n))).rejects.toMatchObject({
|
|
1292
|
+
kind: "cancelled",
|
|
1293
|
+
mayHaveExecuted: false,
|
|
1294
|
+
code: "timeout",
|
|
1295
|
+
})
|
|
1296
|
+
gate.resolve()
|
|
1297
|
+
await sleep(50)
|
|
1298
|
+
expect(canisterRequests(replica)).toEqual([])
|
|
1299
|
+
})
|
|
1300
|
+
|
|
1301
|
+
it("keeps a re-send resendOf offered outcome_unknown when it times out before it goes out, and offers it again", async () => {
|
|
1302
|
+
const { replica, client, ledger } = heldLedger({ timeoutMs: 50 })
|
|
1303
|
+
const first = await within(
|
|
1304
|
+
ledger.icrc1_transfer(transferTo(BOB, 1n))
|
|
1305
|
+
).catch((failure: unknown) => failure)
|
|
1306
|
+
const dedupedBy = (arg: icrc1.TransferArg) => arg.created_at_time
|
|
1307
|
+
const offer = client.resendOf(first, ledger, "icrc1_transfer", {
|
|
1308
|
+
dedupedBy,
|
|
1309
|
+
})
|
|
1310
|
+
expect(offer).toBeDefined()
|
|
1311
|
+
// A 429 proves the re-send was not delivered, and the timeout passes in
|
|
1312
|
+
// the wait before it would go out again: the first attempt may still
|
|
1313
|
+
// have executed.
|
|
1314
|
+
replica.refuseNext(429)
|
|
1315
|
+
const again = await within(offer!.send()).catch(
|
|
1316
|
+
(failure: unknown) => failure
|
|
1317
|
+
)
|
|
1318
|
+
expect(again).toMatchObject({
|
|
1319
|
+
kind: "outcome_unknown",
|
|
1320
|
+
mayHaveExecuted: true,
|
|
1321
|
+
code: "timeout",
|
|
1322
|
+
})
|
|
1323
|
+
expect(
|
|
1324
|
+
client.resendOf(again, ledger, "icrc1_transfer", { dedupedBy })
|
|
1325
|
+
).toBeDefined()
|
|
1326
|
+
await sleep(1_100)
|
|
1327
|
+
expect(requestsFor(replica, "icrc1_transfer")).toMatchObject([
|
|
1328
|
+
{ methodName: "icrc1_transfer" },
|
|
1329
|
+
{ refused: expect.stringContaining("429") },
|
|
1330
|
+
])
|
|
1331
|
+
})
|
|
1332
|
+
|
|
1333
|
+
it("rejects a oneway whose request went out outcome_unknown, which may have executed", async () => {
|
|
1334
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
1335
|
+
let sent = 0
|
|
1336
|
+
const fetch = (
|
|
1337
|
+
input: RequestInfo | URL,
|
|
1338
|
+
init?: RequestInit
|
|
1339
|
+
): Promise<Response> => {
|
|
1340
|
+
if (urlOf(input).endsWith("/call")) {
|
|
1341
|
+
sent += 1
|
|
1342
|
+
return new Promise<Response>(() => {})
|
|
1343
|
+
}
|
|
1344
|
+
return replica.fetch(input, init)
|
|
1345
|
+
}
|
|
1346
|
+
const canister = clientAs(
|
|
1347
|
+
{ ...replica, fetch },
|
|
1348
|
+
alice
|
|
1349
|
+
).canister<shapes.Actor>(shapes.actor, { id: SHAPES, timeoutMs: 50 })
|
|
1350
|
+
await expect(within(canister.fire("x"))).rejects.toMatchObject({
|
|
1351
|
+
kind: "outcome_unknown",
|
|
1352
|
+
mayHaveExecuted: true,
|
|
1353
|
+
code: "timeout",
|
|
1354
|
+
method: "fire",
|
|
1355
|
+
})
|
|
1356
|
+
expect(sent).toBe(1)
|
|
1357
|
+
})
|
|
1358
|
+
|
|
1359
|
+
it('rejects an update read with { update: "idempotent" } whose request went out outcome_unknown, which may have executed', async () => {
|
|
1360
|
+
const replica = replicaWith({
|
|
1361
|
+
[SHAPES]: shapesCanister({ address: () => new Promise(() => {}) }),
|
|
1362
|
+
})
|
|
1363
|
+
const client = clientAs(replica, alice)
|
|
1364
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, {
|
|
1365
|
+
id: SHAPES,
|
|
1366
|
+
timeoutMs: 50,
|
|
1367
|
+
})
|
|
1368
|
+
const { queryFn } = client.queryOptions(canister, "address", "a", {
|
|
1369
|
+
update: "idempotent",
|
|
1370
|
+
})
|
|
1371
|
+
const read = queryFn({
|
|
1372
|
+
signal: new AbortController().signal,
|
|
1373
|
+
} as QueryFunctionContext)
|
|
1374
|
+
await expect(within(read)).rejects.toMatchObject({
|
|
1375
|
+
kind: "outcome_unknown",
|
|
1376
|
+
mayHaveExecuted: true,
|
|
1377
|
+
code: "timeout",
|
|
1378
|
+
method: "address",
|
|
1379
|
+
})
|
|
1380
|
+
})
|
|
1381
|
+
|
|
1382
|
+
it("lets a read's own abort win over the timeout: cancelled with no code, at once", async () => {
|
|
1383
|
+
const replica = replicaWith({
|
|
1384
|
+
[SHAPES]: shapesCanister({ one: () => new Promise(() => {}) }),
|
|
1385
|
+
})
|
|
1386
|
+
const client = clientAs(replica, alice)
|
|
1387
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, {
|
|
1388
|
+
id: SHAPES,
|
|
1389
|
+
timeoutMs: 1_000,
|
|
1390
|
+
})
|
|
1391
|
+
const { queryFn } = client.queryOptions(canister, "one", 1n)
|
|
1392
|
+
const controller = new AbortController()
|
|
1393
|
+
const started = Date.now()
|
|
1394
|
+
const read = queryFn({
|
|
1395
|
+
signal: controller.signal,
|
|
1396
|
+
} as QueryFunctionContext).catch((failure: unknown) => failure)
|
|
1397
|
+
await vi.waitFor(() => expect(requestsFor(replica, "one")).toHaveLength(1))
|
|
1398
|
+
controller.abort()
|
|
1399
|
+
const error = await within(read)
|
|
1400
|
+
expect(error).toMatchObject({ kind: "cancelled", mayHaveExecuted: false })
|
|
1401
|
+
expect((error as { code?: string }).code).toBeUndefined()
|
|
1402
|
+
expect(Date.now() - started).toBeLessThan(800)
|
|
1403
|
+
})
|
|
1404
|
+
|
|
1405
|
+
it("rejects a read whose signal had already aborted cancelled with no code, and sends nothing", async () => {
|
|
1406
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
1407
|
+
const client = clientAs(replica, alice)
|
|
1408
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, {
|
|
1409
|
+
id: SHAPES,
|
|
1410
|
+
timeoutMs: 1_000,
|
|
1411
|
+
})
|
|
1412
|
+
const { queryFn } = client.queryOptions(canister, "one", 1n)
|
|
1413
|
+
const controller = new AbortController()
|
|
1414
|
+
controller.abort()
|
|
1415
|
+
const error = await within(
|
|
1416
|
+
queryFn({ signal: controller.signal } as QueryFunctionContext).catch(
|
|
1417
|
+
(failure: unknown) => failure
|
|
1418
|
+
)
|
|
1419
|
+
)
|
|
1420
|
+
expect(error).toMatchObject({ kind: "cancelled", mayHaveExecuted: false })
|
|
1421
|
+
expect((error as { code?: string }).code).toBeUndefined()
|
|
1422
|
+
await sleep(50)
|
|
1423
|
+
expect(requestsFor(replica, "one")).toEqual([])
|
|
1424
|
+
})
|
|
1425
|
+
|
|
1426
|
+
it("leaves a call that answers in time as it is", async () => {
|
|
1427
|
+
const { client, replica } = shapesSetup()
|
|
1428
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, {
|
|
1429
|
+
id: SHAPES,
|
|
1430
|
+
timeoutMs: 5_000,
|
|
1431
|
+
})
|
|
1432
|
+
await expect(canister.bump(1n)).resolves.toBe(2n)
|
|
1433
|
+
await expect(canister.one(1n)).resolves.toBe(2n)
|
|
1434
|
+
expect(requestsFor(replica, "bump")).toHaveLength(1)
|
|
1435
|
+
})
|
|
1436
|
+
|
|
1437
|
+
it("is part of the target: another timeoutMs is another canister object", () => {
|
|
1438
|
+
const { client } = shapesSetup()
|
|
1439
|
+
const bounded = client.canister<shapes.Actor>(shapes.actor, {
|
|
1440
|
+
id: SHAPES,
|
|
1441
|
+
timeoutMs: 50,
|
|
1442
|
+
})
|
|
1443
|
+
expect(
|
|
1444
|
+
client.canister<shapes.Actor>(shapes.actor, { id: SHAPES, timeoutMs: 50 })
|
|
1445
|
+
).toBe(bounded)
|
|
1446
|
+
expect(
|
|
1447
|
+
client.canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
1448
|
+
).not.toBe(bounded)
|
|
1449
|
+
expect(client.queryKey(bounded, "one", 1n)).toEqual(
|
|
1450
|
+
client.queryKey(
|
|
1451
|
+
client.canister<shapes.Actor>(shapes.actor, { id: SHAPES }),
|
|
1452
|
+
"one",
|
|
1453
|
+
1n
|
|
1454
|
+
)
|
|
1455
|
+
)
|
|
1456
|
+
})
|
|
1457
|
+
|
|
1458
|
+
it.each([0, -1, 0.5, Number.NaN, Infinity, 2 ** 31, "50", null])(
|
|
1459
|
+
"throws a TypeError for timeoutMs %s",
|
|
1460
|
+
(timeoutMs) => {
|
|
1461
|
+
const { client } = shapesSetup()
|
|
1462
|
+
expect(() =>
|
|
1463
|
+
client.canister(shapes.actor, {
|
|
1464
|
+
id: SHAPES,
|
|
1465
|
+
timeoutMs: timeoutMs as number,
|
|
1466
|
+
})
|
|
1467
|
+
).toThrow(/2147483647/)
|
|
1468
|
+
}
|
|
1469
|
+
)
|
|
1470
|
+
})
|
|
1471
|
+
|
|
1472
|
+
describe("a call made as a caller who signs out before it is sent", () => {
|
|
1473
|
+
it("is cancelled rather than sent as somebody else", async () => {
|
|
1474
|
+
const gate = deferred()
|
|
1475
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
1476
|
+
const auth = createTestAuth({ seed: 1 })
|
|
1477
|
+
const slowAuth = withIdentity(auth, async () => {
|
|
1478
|
+
const identity = await auth.getIdentity()
|
|
1479
|
+
await gate.promise
|
|
1480
|
+
return identity
|
|
1481
|
+
})
|
|
1482
|
+
const canister = clientWithAuth(replica, slowAuth).canister<shapes.Actor>(
|
|
1483
|
+
shapes.actor,
|
|
1484
|
+
{ id: SHAPES }
|
|
1485
|
+
)
|
|
1486
|
+
const call = canister.address("x")
|
|
1487
|
+
await auth.signOut()
|
|
1488
|
+
gate.resolve()
|
|
1489
|
+
await expect(call).rejects.toMatchObject({
|
|
1490
|
+
kind: "cancelled",
|
|
1491
|
+
code: "caller_changed",
|
|
1492
|
+
mayHaveExecuted: false,
|
|
1493
|
+
})
|
|
1494
|
+
expect(canisterRequests(replica)).toEqual([])
|
|
1495
|
+
})
|
|
1496
|
+
})
|
|
1497
|
+
|
|
1498
|
+
describe("a signer that fails to sign", () => {
|
|
1499
|
+
// The agent signs outside its own error handling, so what a signer throws
|
|
1500
|
+
// (a user who declines a passkey or hardware-wallet prompt) used to reach
|
|
1501
|
+
// the classifier as an unknown failure: outcome_unknown, offered to
|
|
1502
|
+
// resendOf, though nothing was sent (#963).
|
|
1503
|
+
const declined = new Error("the user declined the signature on the device")
|
|
1504
|
+
|
|
1505
|
+
/** Alice's identity, throwing `thrown` for every request `refuses` picks. */
|
|
1506
|
+
const signer = (
|
|
1507
|
+
refuses: (endpoint: unknown) => boolean,
|
|
1508
|
+
thrown: unknown = declined
|
|
1509
|
+
) => {
|
|
1510
|
+
const asked: unknown[] = []
|
|
1511
|
+
const identity: Identity = {
|
|
1512
|
+
getPrincipal: () => alice.getPrincipal(),
|
|
1513
|
+
transformRequest: (request) => {
|
|
1514
|
+
const endpoint = (request as { endpoint?: unknown }).endpoint
|
|
1515
|
+
asked.push(endpoint)
|
|
1516
|
+
return refuses(endpoint)
|
|
1517
|
+
? Promise.reject(thrown)
|
|
1518
|
+
: alice.transformRequest(request)
|
|
1519
|
+
},
|
|
1520
|
+
}
|
|
1521
|
+
return { identity, asked }
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/** A transfer by Alice, whose `identity` signs for her. */
|
|
1525
|
+
const transfer = (identity: Identity) => {
|
|
1526
|
+
const replica = replicaWith({
|
|
1527
|
+
[LEDGER]: ledgerCanister(new Map([[ALICE as string, 1_000_000n]])),
|
|
1528
|
+
})
|
|
1529
|
+
const client = clientAs(replica, identity)
|
|
1530
|
+
const ledger = client.canister<icrc1.Actor>(icrc1.actor, { id: LEDGER })
|
|
1531
|
+
const sent = ledger
|
|
1532
|
+
.icrc1_transfer(transferTo(BOB, 1n))
|
|
1533
|
+
.catch((failure: unknown) => failure)
|
|
1534
|
+
return { replica, client, ledger, sent }
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
it("rejects a write not_delivered, sends nothing and offers no re-send when the signer declines", async () => {
|
|
1538
|
+
const { identity, asked } = signer(() => true)
|
|
1539
|
+
const { replica, client, ledger, sent } = transfer(identity)
|
|
1540
|
+
const error = await sent
|
|
1541
|
+
|
|
1542
|
+
expect(error).toMatchObject({
|
|
1543
|
+
kind: "not_delivered",
|
|
1544
|
+
mayHaveExecuted: false,
|
|
1545
|
+
method: "icrc1_transfer",
|
|
1546
|
+
})
|
|
1547
|
+
expect((error as Error).message).toContain(
|
|
1548
|
+
`the signer did not sign the request, so it was not sent: ${declined.message}`
|
|
1549
|
+
)
|
|
1550
|
+
expect((error as { cause?: unknown }).cause).toBe(declined)
|
|
1551
|
+
expect(isRetryable(error)).toBe(false)
|
|
1552
|
+
expect(isReactorError(error, ledger, "icrc1_transfer")).toBe(true)
|
|
1553
|
+
expect(
|
|
1554
|
+
client.resendOf(error, ledger, "icrc1_transfer", {
|
|
1555
|
+
dedupedBy: (arg) => arg.created_at_time,
|
|
1556
|
+
})
|
|
1557
|
+
).toBeUndefined()
|
|
1558
|
+
// Asked once, and not again: nothing was sent, and nothing is re-sent.
|
|
1559
|
+
expect(asked).toEqual(["call"])
|
|
1560
|
+
expect(canisterRequests(replica)).toEqual([])
|
|
1561
|
+
})
|
|
1562
|
+
|
|
1563
|
+
it("rejects a write not_delivered when the signer throws something that is not an Error", async () => {
|
|
1564
|
+
const thrown = Object.create(null) as object
|
|
1565
|
+
const { identity } = signer(() => true, thrown)
|
|
1566
|
+
const { replica, sent } = transfer(identity)
|
|
1567
|
+
const error = await sent
|
|
1568
|
+
|
|
1569
|
+
expect(error).toMatchObject({
|
|
1570
|
+
kind: "not_delivered",
|
|
1571
|
+
mayHaveExecuted: false,
|
|
1572
|
+
})
|
|
1573
|
+
expect((error as { cause?: unknown }).cause).toBe(thrown)
|
|
1574
|
+
expect(canisterRequests(replica)).toEqual([])
|
|
1575
|
+
})
|
|
1576
|
+
|
|
1577
|
+
it("keeps a signer's AbortError cancelled, as any abort", async () => {
|
|
1578
|
+
const aborted = new DOMException("the prompt was aborted", "AbortError")
|
|
1579
|
+
const { identity } = signer(() => true, aborted)
|
|
1580
|
+
const { replica, sent } = transfer(identity)
|
|
1581
|
+
const error = await sent
|
|
1582
|
+
|
|
1583
|
+
expect(error).toMatchObject({ kind: "cancelled" })
|
|
1584
|
+
expect((error as { cause?: unknown }).cause).toBe(aborted)
|
|
1585
|
+
expect(canisterRequests(replica)).toEqual([])
|
|
1586
|
+
})
|
|
1587
|
+
|
|
1588
|
+
it("keeps a write outcome_unknown when the signer declines a read_state poll after the IC took the call", async () => {
|
|
1589
|
+
// The call is accepted (202), so the agent polls read_state for its
|
|
1590
|
+
// answer, and the signer declines to sign that poll.
|
|
1591
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
1592
|
+
const accepted = (
|
|
1593
|
+
input: RequestInfo | URL,
|
|
1594
|
+
init?: RequestInit
|
|
1595
|
+
): Promise<Response> =>
|
|
1596
|
+
urlOf(input).endsWith("/call")
|
|
1597
|
+
? Promise.resolve(new Response(null, { status: 202 }))
|
|
1598
|
+
: replica.fetch(input, init)
|
|
1599
|
+
const { identity, asked } = signer((endpoint) => endpoint === "read_state")
|
|
1600
|
+
const canister = clientAs(
|
|
1601
|
+
{ ...replica, fetch: accepted },
|
|
1602
|
+
identity
|
|
1603
|
+
).canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
1604
|
+
|
|
1605
|
+
const error = await canister.bump(1n).catch((failure: unknown) => failure)
|
|
1606
|
+
|
|
1607
|
+
expect(error).toMatchObject({
|
|
1608
|
+
kind: "outcome_unknown",
|
|
1609
|
+
mayHaveExecuted: true,
|
|
1610
|
+
method: "bump",
|
|
1611
|
+
})
|
|
1612
|
+
expect((error as Error).message).toContain(
|
|
1613
|
+
`the call was sent, but the signer did not sign a request for its answer: ${declined.message}`
|
|
1614
|
+
)
|
|
1615
|
+
expect((error as { cause?: unknown }).cause).toBe(declined)
|
|
1616
|
+
expect(asked).toEqual(["call", "read_state"])
|
|
1617
|
+
})
|
|
1618
|
+
|
|
1619
|
+
it("rejects a read not_delivered, and does not ask again, when the signer declines the read_state for the subnet's keys", async () => {
|
|
1620
|
+
// A query's answer is checked against the subnet's node keys, which the
|
|
1621
|
+
// agent reads with a signed read_state inside its own error handling.
|
|
1622
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
1623
|
+
const { identity, asked } = signer((endpoint) => endpoint === "read_state")
|
|
1624
|
+
const canister = clientAs(replica, identity).canister<shapes.Actor>(
|
|
1625
|
+
shapes.actor,
|
|
1626
|
+
{ id: SHAPES }
|
|
1627
|
+
)
|
|
1628
|
+
|
|
1629
|
+
const error = await canister.one(1n).catch((failure: unknown) => failure)
|
|
1630
|
+
|
|
1631
|
+
expect(error).toMatchObject({
|
|
1632
|
+
kind: "not_delivered",
|
|
1633
|
+
mayHaveExecuted: false,
|
|
1634
|
+
method: "one",
|
|
1635
|
+
})
|
|
1636
|
+
expect(isRetryable(error)).toBe(false)
|
|
1637
|
+
expect((error as Error).message).toContain(
|
|
1638
|
+
`the signer did not sign the request: ${declined.message}`
|
|
1639
|
+
)
|
|
1640
|
+
expect((error as { cause?: unknown }).cause).toBe(declined)
|
|
1641
|
+
expect(asked).toEqual(["read", "read_state"])
|
|
1642
|
+
})
|
|
1643
|
+
})
|
|
1644
|
+
|
|
1645
|
+
// ---------------------------------------------------------------------------
|
|
1646
|
+
// The ingress expiry of a write that got no answer (#878): after it, no send
|
|
1647
|
+
// of the write can still run, so a read-back that finds nothing is final.
|
|
1648
|
+
// ---------------------------------------------------------------------------
|
|
1649
|
+
|
|
1650
|
+
describe("expiresAt on a write that got no answer", () => {
|
|
1651
|
+
const shapesThrough = (
|
|
1652
|
+
replica: FakeReplica,
|
|
1653
|
+
fetch: typeof globalThis.fetch
|
|
1654
|
+
) =>
|
|
1655
|
+
clientAs({ ...replica, fetch }, alice).canister<shapes.Actor>(
|
|
1656
|
+
shapes.actor,
|
|
1657
|
+
{ id: SHAPES }
|
|
1658
|
+
)
|
|
1659
|
+
|
|
1660
|
+
/** Wraps `fetch`, keeping the ingress expiry of every call request it sees. */
|
|
1661
|
+
const expiries = (fetch: typeof globalThis.fetch) => {
|
|
1662
|
+
const seen: bigint[] = []
|
|
1663
|
+
const wrapped = async (
|
|
1664
|
+
input: RequestInfo | URL,
|
|
1665
|
+
init?: RequestInit
|
|
1666
|
+
): Promise<Response> => {
|
|
1667
|
+
if (urlOf(input).endsWith("/call")) {
|
|
1668
|
+
const envelope = (await cborOf(init?.body)) as {
|
|
1669
|
+
content: { ingress_expiry: bigint | number }
|
|
1670
|
+
}
|
|
1671
|
+
seen.push(BigInt(envelope.content.ingress_expiry))
|
|
1672
|
+
}
|
|
1673
|
+
return fetch(input, init)
|
|
1674
|
+
}
|
|
1675
|
+
return { fetch: wrapped, seen }
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
it("is the ingress expiry the replica received, for a call request lost in transport", async () => {
|
|
1679
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
1680
|
+
const sends = expiries(replica.fetch)
|
|
1681
|
+
const canister = shapesThrough(replica, sends.fetch)
|
|
1682
|
+
replica.dropNextReply()
|
|
1683
|
+
const error = await canister.bump(1n).catch((caught: unknown) => caught)
|
|
1684
|
+
expect(error).toMatchObject({ kind: "outcome_unknown" })
|
|
1685
|
+
expect(sends.seen).toHaveLength(1)
|
|
1686
|
+
expect((error as { expiresAt?: bigint }).expiresAt).toBe(sends.seen[0])
|
|
1687
|
+
// A nanosecond time minutes ahead of now, not a duration.
|
|
1688
|
+
const now = BigInt(Date.now()) * 1_000_000n
|
|
1689
|
+
expect(sends.seen[0]).toBeGreaterThan(now)
|
|
1690
|
+
expect(sends.seen[0]).toBeLessThan(now + 6n * 60n * 1_000_000_000n)
|
|
1691
|
+
// Not enumerable: logging the error as JSON still works.
|
|
1692
|
+
expect(() => JSON.stringify(error)).not.toThrow()
|
|
1693
|
+
})
|
|
1694
|
+
|
|
1695
|
+
it("is the call request's own expiry when the call was accepted but its polls never answered", async () => {
|
|
1696
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
1697
|
+
const sends = expiries(replica.fetch)
|
|
1698
|
+
const later = answerLater(sends.fetch, { failures: Infinity })
|
|
1699
|
+
const error = await shapesThrough(replica, later.fetch)
|
|
1700
|
+
.bump(4n)
|
|
1701
|
+
.catch((caught: unknown) => caught)
|
|
1702
|
+
expect(error).toMatchObject({
|
|
1703
|
+
kind: "outcome_unknown",
|
|
1704
|
+
mayHaveExecuted: true,
|
|
1705
|
+
})
|
|
1706
|
+
expect(sends.seen).toHaveLength(1)
|
|
1707
|
+
expect((error as { expiresAt?: bigint }).expiresAt).toBe(sends.seen[0])
|
|
1708
|
+
}, 10_000)
|
|
1709
|
+
|
|
1710
|
+
it("is the latest expiry of the sends of a write the client sent again", async () => {
|
|
1711
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
1712
|
+
const sends = expiries(replica.fetch)
|
|
1713
|
+
const canister = shapesThrough(replica, sends.fetch)
|
|
1714
|
+
replica.refuseNext(429)
|
|
1715
|
+
replica.dropNextReply()
|
|
1716
|
+
const error = await canister.bump(1n).catch((caught: unknown) => caught)
|
|
1717
|
+
expect(error).toMatchObject({ kind: "outcome_unknown" })
|
|
1718
|
+
expect(sends.seen).toHaveLength(2)
|
|
1719
|
+
const latest = sends.seen.reduce((a, b) => (a > b ? a : b))
|
|
1720
|
+
expect((error as { expiresAt?: bigint }).expiresAt).toBe(latest)
|
|
1721
|
+
})
|
|
1722
|
+
|
|
1723
|
+
it("is a resendOf re-send's own expiry when that send is lost too", async () => {
|
|
1724
|
+
const replica = replicaWith({ [LEDGER]: ledgerCanister(new Map()) })
|
|
1725
|
+
const sends = expiries(replica.fetch)
|
|
1726
|
+
const client = clientAs({ ...replica, fetch: sends.fetch }, alice)
|
|
1727
|
+
const ledger = client.canister<icrc1.Actor>(icrc1.actor, { id: LEDGER })
|
|
1728
|
+
const arg = { ...transferTo(BOB, 1n), created_at_time: 1_000n }
|
|
1729
|
+
replica.dropNextReply()
|
|
1730
|
+
const first = await ledger
|
|
1731
|
+
.icrc1_transfer(arg)
|
|
1732
|
+
.catch((caught: unknown) => caught)
|
|
1733
|
+
const again = client.resendOf(first, ledger, "icrc1_transfer", {
|
|
1734
|
+
dedupedBy: (sent) => sent.created_at_time,
|
|
1735
|
+
})
|
|
1736
|
+
expect(again).toBeDefined()
|
|
1737
|
+
replica.dropNextReply()
|
|
1738
|
+
const second = await again?.send().catch((caught: unknown) => caught)
|
|
1739
|
+
expect(second).toMatchObject({ kind: "outcome_unknown" })
|
|
1740
|
+
expect(sends.seen).toHaveLength(2)
|
|
1741
|
+
expect((first as { expiresAt?: bigint }).expiresAt).toBe(sends.seen[0])
|
|
1742
|
+
expect((second as { expiresAt?: bigint }).expiresAt).toBe(sends.seen[1])
|
|
1743
|
+
})
|
|
1744
|
+
|
|
1745
|
+
it("is each send's own when two sends of one resendOf offer are lost at once", async () => {
|
|
1746
|
+
const replica = replicaWith({ [LEDGER]: ledgerCanister(new Map()) })
|
|
1747
|
+
const sends = expiries(replica.fetch)
|
|
1748
|
+
const client = clientAs({ ...replica, fetch: sends.fetch }, alice)
|
|
1749
|
+
const ledger = client.canister<icrc1.Actor>(icrc1.actor, { id: LEDGER })
|
|
1750
|
+
const arg = { ...transferTo(BOB, 1n), created_at_time: 1_000n }
|
|
1751
|
+
replica.dropNextReply()
|
|
1752
|
+
const first = await ledger
|
|
1753
|
+
.icrc1_transfer(arg)
|
|
1754
|
+
.catch((caught: unknown) => caught)
|
|
1755
|
+
const again = client.resendOf(first, ledger, "icrc1_transfer", {
|
|
1756
|
+
dedupedBy: (sent) => sent.created_at_time,
|
|
1757
|
+
})
|
|
1758
|
+
replica.dropNextReply({ method: "icrc1_transfer", times: 2 })
|
|
1759
|
+
// The same encoded argument goes out twice, concurrently.
|
|
1760
|
+
const both = await Promise.all([
|
|
1761
|
+
again?.send().catch((caught: unknown) => caught),
|
|
1762
|
+
again?.send().catch((caught: unknown) => caught),
|
|
1763
|
+
])
|
|
1764
|
+
expect(sends.seen).toHaveLength(3)
|
|
1765
|
+
for (const error of both) {
|
|
1766
|
+
expect(error).toMatchObject({ kind: "outcome_unknown" })
|
|
1767
|
+
expect((error as { expiresAt?: bigint }).expiresAt).toBeDefined()
|
|
1768
|
+
}
|
|
1769
|
+
})
|
|
1770
|
+
|
|
1771
|
+
it("is the expiry of a call request whose wait was cancelled before its answer", async () => {
|
|
1772
|
+
const replica = replicaWith({
|
|
1773
|
+
[SHAPES]: serve<shapes.Actor>(shapes.actor, { address: ([s]) => s }),
|
|
1774
|
+
})
|
|
1775
|
+
const sends = expiries(replica.fetch)
|
|
1776
|
+
// The call request reaches the replica, and its answer never comes.
|
|
1777
|
+
const fetch = async (
|
|
1778
|
+
input: RequestInfo | URL,
|
|
1779
|
+
init?: RequestInit
|
|
1780
|
+
): Promise<Response> => {
|
|
1781
|
+
const response = sends.fetch(input, init)
|
|
1782
|
+
if (!urlOf(input).endsWith("/call")) return response
|
|
1783
|
+
return new Promise<Response>(() => {})
|
|
1784
|
+
}
|
|
1785
|
+
const client = clientAs({ ...replica, fetch }, alice)
|
|
1786
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
1787
|
+
const { queryFn } = client.queryOptions(canister, "address", "a", {
|
|
1788
|
+
update: "idempotent",
|
|
1789
|
+
})
|
|
1790
|
+
const controller = new AbortController()
|
|
1791
|
+
const read = queryFn({
|
|
1792
|
+
signal: controller.signal,
|
|
1793
|
+
} as QueryFunctionContext).catch((caught: unknown) => caught)
|
|
1794
|
+
await vi.waitFor(() => expect(sends.seen).toHaveLength(1))
|
|
1795
|
+
controller.abort()
|
|
1796
|
+
const error = await read
|
|
1797
|
+
expect(error).toMatchObject({ kind: "cancelled", mayHaveExecuted: true })
|
|
1798
|
+
expect((error as { expiresAt?: bigint }).expiresAt).toBe(sends.seen[0])
|
|
1799
|
+
})
|
|
1800
|
+
|
|
1801
|
+
it("sends no call request once the signal aborted before it was signed", async () => {
|
|
1802
|
+
const replica = replicaWith({
|
|
1803
|
+
[SHAPES]: serve<shapes.Actor>(shapes.actor, { address: ([s]) => s }),
|
|
1804
|
+
})
|
|
1805
|
+
const sends = expiries(replica.fetch)
|
|
1806
|
+
const client = clientAs({ ...replica, fetch: sends.fetch }, alice)
|
|
1807
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
1808
|
+
const { queryFn } = client.queryOptions(canister, "address", "a", {
|
|
1809
|
+
update: "idempotent",
|
|
1810
|
+
})
|
|
1811
|
+
const controller = new AbortController()
|
|
1812
|
+
// Aborted while the caller's agent is fetched, before the request is built.
|
|
1813
|
+
const read = queryFn({
|
|
1814
|
+
signal: controller.signal,
|
|
1815
|
+
} as QueryFunctionContext).catch((caught: unknown) => caught)
|
|
1816
|
+
controller.abort()
|
|
1817
|
+
expect(await read).toMatchObject({ kind: "cancelled" })
|
|
1818
|
+
await sleep(50)
|
|
1819
|
+
expect(sends.seen).toHaveLength(0)
|
|
1820
|
+
})
|
|
1821
|
+
|
|
1822
|
+
it("sends no call request when the signal aborts inside the agent, before it is signed", async () => {
|
|
1823
|
+
// The agent asks the identity for its principal while it builds the call
|
|
1824
|
+
// request, after the call checked its signal: the abort lands there, so
|
|
1825
|
+
// only the agent's own check before signing stops the send.
|
|
1826
|
+
const replica = replicaWith({
|
|
1827
|
+
[SHAPES]: serve<shapes.Actor>(shapes.actor, { address: ([s]) => s }),
|
|
1828
|
+
})
|
|
1829
|
+
const sends = expiries(replica.fetch)
|
|
1830
|
+
const controller = new AbortController()
|
|
1831
|
+
let armed = false
|
|
1832
|
+
let asked = 0
|
|
1833
|
+
const identity: Identity = {
|
|
1834
|
+
getPrincipal: () => {
|
|
1835
|
+
// The call asks first, as it fetches the caller's agent; the agent
|
|
1836
|
+
// asks next, as it builds the request.
|
|
1837
|
+
if (armed && ++asked === 2) controller.abort()
|
|
1838
|
+
return alice.getPrincipal()
|
|
1839
|
+
},
|
|
1840
|
+
transformRequest: (request) => alice.transformRequest(request),
|
|
1841
|
+
}
|
|
1842
|
+
const client = clientAs({ ...replica, fetch: sends.fetch }, identity)
|
|
1843
|
+
const canister = client.canister<shapes.Actor>(shapes.actor, { id: SHAPES })
|
|
1844
|
+
const { queryFn } = client.queryOptions(canister, "address", "a", {
|
|
1845
|
+
update: "idempotent",
|
|
1846
|
+
})
|
|
1847
|
+
armed = true
|
|
1848
|
+
const read = await queryFn({
|
|
1849
|
+
signal: controller.signal,
|
|
1850
|
+
} as QueryFunctionContext).catch((caught: unknown) => caught)
|
|
1851
|
+
expect(controller.signal.aborted).toBe(true)
|
|
1852
|
+
expect(read).toMatchObject({ kind: "cancelled" })
|
|
1853
|
+
await sleep(50)
|
|
1854
|
+
expect(sends.seen).toHaveLength(0)
|
|
1855
|
+
})
|
|
1856
|
+
|
|
1857
|
+
it("is the ingress expiry sent, for a write that timed out after it went out", async () => {
|
|
1858
|
+
// The call is accepted (202), and every read_state that polls for it is
|
|
1859
|
+
// held, so timeoutMs passes with the write out.
|
|
1860
|
+
const replica = replicaWith({ [SHAPES]: shapesCanister() })
|
|
1861
|
+
const held = (
|
|
1862
|
+
input: RequestInfo | URL,
|
|
1863
|
+
init?: RequestInit
|
|
1864
|
+
): Promise<Response> => {
|
|
1865
|
+
const url = urlOf(input)
|
|
1866
|
+
if (url.endsWith("/call")) {
|
|
1867
|
+
return Promise.resolve(new Response(null, { status: 202 }))
|
|
1868
|
+
}
|
|
1869
|
+
if (url.endsWith("/read_state") && url.includes(SHAPES)) {
|
|
1870
|
+
return new Promise<Response>(() => {})
|
|
1871
|
+
}
|
|
1872
|
+
return replica.fetch(input, init)
|
|
1873
|
+
}
|
|
1874
|
+
const sends = expiries(held)
|
|
1875
|
+
const canister = clientAs(
|
|
1876
|
+
{ ...replica, fetch: sends.fetch },
|
|
1877
|
+
alice
|
|
1878
|
+
).canister<shapes.Actor>(shapes.actor, { id: SHAPES, timeoutMs: 200 })
|
|
1879
|
+
const error = await canister.bump(1n).catch((caught: unknown) => caught)
|
|
1880
|
+
expect(error).toMatchObject({ kind: "outcome_unknown", code: "timeout" })
|
|
1881
|
+
expect(sends.seen).toHaveLength(1)
|
|
1882
|
+
expect((error as { expiresAt?: bigint }).expiresAt).toBe(sends.seen[0])
|
|
1883
|
+
})
|
|
1884
|
+
|
|
1885
|
+
it("is absent on a write that timed out before it went out", async () => {
|
|
1886
|
+
const { client, replica } = shapesSetup()
|
|
1887
|
+
replica.refuseNext(429)
|
|
1888
|
+
const bounded = client.canister<shapes.Actor>(shapes.actor, {
|
|
1889
|
+
id: SHAPES,
|
|
1890
|
+
timeoutMs: 100,
|
|
1891
|
+
})
|
|
1892
|
+
const error = await bounded.bump(1n).catch((caught: unknown) => caught)
|
|
1893
|
+
expect(error).toMatchObject({ kind: "cancelled", code: "timeout" })
|
|
1894
|
+
expect(error).not.toHaveProperty("expiresAt")
|
|
1895
|
+
})
|
|
1896
|
+
|
|
1897
|
+
it("is absent on an answered write, a refused one, and a read", async () => {
|
|
1898
|
+
const { canister, replica } = shapesSetup({
|
|
1899
|
+
bump: () => ({ tag: "err", value: "no" }),
|
|
1900
|
+
})
|
|
1901
|
+
const answered = await canister.bump(1n).catch((caught: unknown) => caught)
|
|
1902
|
+
expect(answered).toMatchObject({ kind: "canister_err" })
|
|
1903
|
+
expect(answered).not.toHaveProperty("expiresAt")
|
|
1904
|
+
|
|
1905
|
+
const refused = await canister
|
|
1906
|
+
.bump("not a nat" as never)
|
|
1907
|
+
.catch((caught: unknown) => caught)
|
|
1908
|
+
expect(refused).toMatchObject({ kind: "invalid_args" })
|
|
1909
|
+
expect(refused).not.toHaveProperty("expiresAt")
|
|
1910
|
+
|
|
1911
|
+
replica.refuseNext(429, 3)
|
|
1912
|
+
const neverIn = await canister.bump(1n).catch((caught: unknown) => caught)
|
|
1913
|
+
expect(neverIn).toMatchObject({ kind: "not_delivered" })
|
|
1914
|
+
expect(neverIn).not.toHaveProperty("expiresAt")
|
|
1915
|
+
|
|
1916
|
+
replica.refuseNext(503, 3)
|
|
1917
|
+
const read = await canister.one(1n).catch((caught: unknown) => caught)
|
|
1918
|
+
expect(isReactorError(read)).toBe(true)
|
|
1919
|
+
expect(read).not.toHaveProperty("expiresAt")
|
|
1920
|
+
})
|
|
1921
|
+
})
|