feat: Eco API gateway as the default quote source (+ status via /v1/intents/status) - #31
Conversation
…ly chains Arc is Circle's L1 (native gas token USDC, 18 dp at the native layer; the 6 dp ERC-20 view is the 0x3600…0000 precompile). Register it as a production EVM chain with the dedicated CreateX Portal 0xEC002CA1… (version 2.10.0) and the fleet HyperProver, and map USDC on 5042 to the precompile. The pinned viem (2.40) has no Arc definition, so EvmPublisher.getChain now falls back to defineChain() built from RAW_CHAIN_CONFIGS when viem/chains lacks the id, instead of throwing. The chains.config regression test that required every production EVM id to exist in viem/chains is relaxed to "exists in viem or is fully described for the fallback", with an explicit guard that Arc is currently the only such chain. rpc.mainnet.arc.io is IP-allowlisted while the mainnet is private; set EVM_RPC_URL_5042 (e.g. an Alchemy URL) to reach it. Claude-Session: https://claude.ai/code/session_01ENQw9Ze2ZdghQGCNpKjo9L
…urce Adds the approved design (docs/plans/2026-09-14-api-gateway-quotes-design.md) and the @eco-foundation/api-schemas devDependency that supplies the public /v1 types it references. Claude-Session: https://claude.ai/code/session_01ENQw9Ze2ZdghQGCNpKjo9L
… endpoint getQuoteEndpoint now returns a discriminated union; the Eco API gateway is the default source, with SOLVER_URL, QUOTES_API_URL and QUOTES_PREPROD kept as explicit escape hatches. The design/plan docs are aligned with main's variable names. Claude-Session: https://claude.ai/code/session_01ENQw9Ze2ZdghQGCNpKjo9L
…t status) Auth problems (401/403, invalid-api-key) become RoutesCliError naming ECO_API_KEY; other problems and network failures become EcoApiRequestError so the publish flow's manual-route fallback still applies. Debug output logs header names only. Claude-Session: https://claude.ai/code/session_01ENQw9Ze2ZdghQGCNpKjo9L
…1/quotes contract Claude-Session: https://claude.ai/code/session_01ENQw9Ze2ZdghQGCNpKjo9L
…lish --env QuoteService dispatches on the configured endpoint type: gateway (default, via EcoApiClient + adapter) or the legacy solver-v2 / quote-service v3 paths, which are unchanged. The publish flow forwards --env and the source chain's configured portal (needed for non-EVM funding transactions). Claude-Session: https://claude.ai/code/session_01ENQw9Ze2ZdghQGCNpKjo9L
…hain is given --chain still forces the on-chain Portal lookup. Gateway results carry the raw status word (pending, filled, refundable, refunded, ...) on IntentStatus.state; filled/settled count as fulfilled. Claude-Session: https://claude.ai/code/session_01ENQw9Ze2ZdghQGCNpKjo9L
…on test, docs Documents ECO_ENV / ECO_API_URL / ECO_API_KEY, the new quote-source precedence, --env, and gateway-backed status in README, .env.example and the routes-cli skill. Claude-Session: https://claude.ai/code/session_01ENQw9Ze2ZdghQGCNpKjo9L
OverviewThis PR integrates the public Eco API gateway as the default source for quotes and intent status in Highlights (4)
QA & Test Coverage · 1 warning · 3 suggestion
Web SecurityNo issues found in this domain. Blockchain SecuritySkipped: not applicable to this PR — PR changes quote sourcing and status lookup but does not modify intent signing, on-chain publishing, portal interactions, or blockchain transaction validation. PerformanceNo issues found in this domain. Stats
Automated review by Eco's specialized review team. Architecture, business logic, and correctness remain with the human reviewer. |
| const status = fromV1StatusEntries([entry({ status: word })], HASH); | ||
| expect(status.fulfilled).toBe(false); | ||
| expect(status.state).toBe(word); | ||
| } |
There was a problem hiding this comment.
🟡 Warning · missing test coverage · agent-qa
The watch method in StatusService was updated to accept an optional chain and to propagate env, and now also triggers onUpdate when status.state changes (not just fulfilled). There are no tests for watch at all — neither for the gateway path, the on-chain path, nor the new state-change trigger logic. A regression in the polling/notification logic would go undetected.
🤖 Prompt to fix with an AI coding agent
File: tests/status/status.service.test.ts, line 43 and beyond. Problem: The `watch` method in `StatusService` was updated to accept an optional `chain` parameter, to propagate `env`, and to trigger `onUpdate` when `status.state` changes (not just when `fulfilled` becomes true). The test file currently has no test coverage for the `watch` method at all — neither for the gateway code path, the on-chain code path, nor the new state-change trigger logic. This means regressions in the polling/notification logic would go undetected. Task: Add test cases for the `watch` method to cover: 1. The gateway path (when no chain is given, forwarding env) 2. The on-chain path (when a chain is given) 3. The new behavior where `onUpdate` is triggered when `status.state` changes Make the smallest, most focused addition to the test file. Do not refactor existing tests. If you believe this finding is invalid or the watch method does not need testing in this context, add a one-line justification to .agent-coding-decisions.md in the worktree root instead.
…N / ECO_API_KEY_STAGING) Production answers keyless but rejects unknown keys (401 invalid-api-key), so a staging key in a single ECO_API_KEY broke production quotes. Keys are now looked up per gateway environment, with ECO_API_KEY as the fallback. Claude-Session: https://claude.ai/code/session_01ENQw9Ze2ZdghQGCNpKjo9L
…rom the router's publishAndFund calldata eco-router adopted @eco-foundation/api-schemas 0.9.0 (router #51): the request discriminator is `type`, `funder` lives under `source`, `dappId` and `destination.recipient` are required, `destination.amount` became `amountOut`, and `execution.encodedRoute` is gone in favour of the exact Portal.publishAndFund transaction the router built. The adapter now decodes that calldata and publishes the same route bytes the router hashed and signed. Non-EVM funding transactions are refused (SVM sources keep using SOLVER_URL), so the sourcePortalFallback hook is dropped. Verified on staging (router at 9e254c5): Base→Arc and Arc→Base both quote 200 through api.stag.eco.com. Production's router is still pinned to a 0.8.0-wire image and answers 400 to this shape until app-deploy bumps its digest. Claude-Session: https://claude.ai/code/session_01ENQw9Ze2ZdghQGCNpKjo9L
Why
routes-cli quoted against internal hosts:
quotes.eco.comby default, a solver-v2 host viaSOLVER_URL, or the preprod quote service. Partners and Eco tooling are moving to the public front door,api.eco.com/v1/*(AWS API Gateway,x-api-key, typed by the published@eco-foundation/api-schemaspackage, staging twin atapi.stag.eco.com). The CLI is the reference client for self-published intents, so it should speak that contract natively. Design:docs/plans/2026-09-14-api-gateway-quotes-design.md; plan:docs/plans/2026-09-14-api-gateway-quotes-plan.md.Behaviour change
The default quote source is now the Eco API gateway (
POST /v1/quotes).SOLVER_URL,QUOTES_API_URLandQUOTES_PREPRODkeep working as explicit bypasses, in that order of precedence, so anyone who relies on them sees no change.status <hash>no longer requires--chain: without it the intent is looked up throughGET /v1/intents/status; with it the on-chain Portal lookup runs exactly as before.What changed
src/config/—ECO_ENV=production|staging(default production →api.eco.com, staging →api.stag.eco.com),ECO_API_URL(host override), and per-environment keysECO_API_KEY_PRODUCTION/ECO_API_KEY_STAGINGsent asx-api-keyto their own host only, withECO_API_KEYas the fallback. Keys are per environment on purpose: production answers keyless today but rejects unknown keys (401invalid-api-key, observed with a staging key), so one shared variable would break production for anyone holding only a staging key.getQuoteEndpoint()returns a discriminated union (solver-v2|custom|gateway) and accepts a per-command env override.src/eco-api/— newEcoApiClient(quote,intentStatus). Auth failures (401/403 or Problemcode: invalid-api-key, including API Gateway's bare resource-policy 403) throwRoutesCliError.apiErrornamingECO_API_KEY; every other non-2xx and network failure throwsEcoApiRequestError, a plainErrorsubclass, so the publish flow's existing manual-route fallback still applies. Debug output logs header names only, never values.src/quote/gateway-quote.adapter.ts— pure mapping between the CLI'sQuoteRequest/QuoteResultandV1QuoteRequest/V1QuoteResponseon the api-schemas 0.9.0 wire (the one eco-router #51 now serves): requesttype: exact-in,source.funder, requireddappIdanddestination.recipient; responsedestination.amountOut, portal/prover/deadline fromexecution.intent. 0.9.0 returns noencodedRoute, so the adapter decodes the exactPortal.publishAndFundcalldata the router built and publishes thatrouteargument — the bytes the router hashed and signed.execution: null, a non-EVM funding transaction (SVM sources keep usingSOLVER_URL), or a funding call that is notpublishAndFundare hard errors, not silent fallbacks.src/quote/quote.service.ts— dispatches on endpoint type; the solver-v2 and v3 branches are untouched.publish --env <production|staging>overridesECO_ENVper run.src/status/+status.command.ts— gateway-backed lookup when no--chain;IntentStatus.statecarries the raw gateway word (pending,filled,refundable,refunded, …),filled/settledcount as fulfilled;status --env.config listalso redacts keys containingapi_key/apikey.@eco-foundation/api-schemas@0.9.0added as a devDependency forimport typeonly;pnpm buildconfirms nothing from it reaches the ncc bundle..env.example, and theroutes-cliskill.How verified
pnpm test:unit— 180/180 (15 suites; new: config endpoint precedence incl. per-env keys,apiError,EcoApiClientwith mockedfetch, adapter incl. route-bytes decoding and the three hard errors,QuoteServicedispatch, status mapping).pnpm test:integration— 9/9, including a spawned-CLI case: with only an unreachableECO_API_URLset, the gateway quote fails and the manual route still dry-runs.pnpm typecheck,pnpm lint(0 errors; pre-existingno-unsafe-assignmentwarnings on test mocks),pnpm format:check,pnpm build— clean.ECO_API_KEY_STAGINGset:publish -s base -d arc … --dry-run --env staging→api.stag.eco.com/v1/quotes200, 0.5 → 0.5 USDC, source portal0xEC000064…(Base), prover0xec004Ab4…;publish -s arc -d base …→ 200, source portal0xEC002CA1…(Arc's dedicated Portal).status <hash> --env staging→filled. Without a key the same staging calls are a 403 from the gateway's resource policy, surfaced asEco API error 403 … ECO_API_KEY_STAGING.api.eco.comstill runs the digest-pinned pre-#51 image and answers 400invalid-request(swapType … expected,source: Unrecognized key "funder") to the 0.9.0 shape. Production quotes through this CLI work once app-deploy bumpsenvs/prod-486940690197/core/routerto an image carrying router #51 (and #52 for Arc).Deploy order
Merge this after the production router digest bump, or production users of the default quote source get 400s until then.
QUOTES_API_URL=https://quotes.eco.com/api/v3/quotes/singleremains the bypass in the meantime.Follow-ups (out of scope)
Send the gateway's prebuilt
execution.transactioninstead of encodingpublishAndFundlocally; sourcechains/tokensfrom/v1/chainsand/v1/tokens; verify the quotesignature.Stacked on #30 (Arc mainnet). Merge that first; this PR's base then moves to
main.https://claude.ai/code/session_01ENQw9Ze2ZdghQGCNpKjo9L