@anywalletkit/protocol-swap2-core 0.8.0 → 0.9.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/README.md CHANGED
@@ -15,7 +15,7 @@ On npm: [@anywalletkit/protocol-swap2-core](https://www.npmjs.com/package/@anywa
15
15
 
16
16
  - **`BaseSwap2Protocol` / `SingleProviderSwap2`** — a `Swap2Protocol` (from `@anywalletkit/core`) over a provider. It quotes, binds the execution to the quote, runs approvals, sends, and resumes status by id (`getSwap2Status`, `claimSwap2` for two-phase routes).
17
17
  - **Safety checks.** Quote validation, a minimum-output floor, fee caps, a review gate before signing, and refusals that name their reason. Status ids look like `awk1.<provider>.<intent>[.<hash>]` (`SWAP2_ID_PREFIX`).
18
- - **`remoteProvider`** — runs a provider on your server (API keys, shared state) while the app keeps the keys and signs. The backend is not the authority over what the wallet signs: spread the provider package's pins into the stub (`remoteProvider({name, baseUrl, ...lifiPins})`, `fyndPins`, `uniswapPins`, `cctpPins`…). The engine then lets an EVM route call only the pinned `callTargets`, approve only the pinned `spenders`, and runs the provider's own `verifyQuote` (Fynd: `checkFyndTransaction`) on the client; a stub without pins gets no EVM calldata route at all (the router drops it with the reason). A route that pays an address the backend names — a deposit address, or a plain value send — runs only on a stub the app opened with `depositAddressRoutes: true` (`nearIntentsPins`; `thorchainPins()` and `chainflipPins` for their Tron vaults), on every chain. Every token call in a swap's calldata, from any provider, must move the source token, at most the swap amount; operator and Permit2 approvals are never a swap's main call. A typed (gasless) order is signed only when it is the swap's own Limit Order Protocol `Order` — verifying contract = the approved settlement contract, maker = this account, the source token, exactly the input — never a `Permit*` or another typed message; a provider's own check of the build can run on the client too (`verifyBuild`), and a remote stub's typed order is signed only when it has one (Omniston: `omnistonVerifyBuild` opens the order with the `orderExtension` the build carries). A TON cell-body deposit (`bodyBoc`) is bound too: it attaches the input plus at most `tonGasCeiling` of gas (default 1 TON; a jetton source: the ceiling alone), a jetton source's body must be a TEP-74 transfer of exactly the input from this account's own jetton wallet (`getJettonWalletAddress`) with the excess returning here, the decoded body reaches the review (`cellBodyDecoded`), and a remote stub's cell body needs the app's own validator (`verifyBuild`). An absolute `baseUrl` must be `https:` (a same-origin path is fine; `allowInsecureBaseUrl` for local development). A provider with a claim step (CCTP) also takes `claimTargets` from the app: `claimSwap2` calls only those contracts.
18
+ - **`remoteProvider`** — runs a provider on your server (API keys, shared state) while the app keeps the keys and signs. The backend is not the authority over what the wallet signs: spread the provider package's pins into the stub (`remoteProvider({name, baseUrl, ...lifiPins})`, `fyndPins`, `uniswapPins`, `cctpPins`…). The engine then lets an EVM route call only the pinned `callTargets`, approve only the pinned `spenders`, and runs the provider's own `verifyQuote` (Fynd: `checkFyndTransaction`) on the client; a stub without pins gets no EVM calldata route at all (the router drops it with the reason), and neither does a stub whose pins name call targets but carry no check of the call itself — `verifyQuote`, or a `verifyBuild` the app supplies — since a pinned contract executes whatever its arguments say. Every shipped EVM pins export carries one. A route that pays an address the backend names — a deposit address, or a plain value send — runs only on a stub the app opened with `depositAddressRoutes: true` (`nearIntentsPins`; `thorchainPins()` and `chainflipPins` for their Tron vaults), on every chain. Every token call in a swap's calldata, from any provider, must move the source token, at most the swap amount; operator and Permit2 approvals are never a swap's main call. A typed (gasless) order is signed only when it is the swap's own Limit Order Protocol `Order` — verifying contract = the approved settlement contract, maker = this account, the source token, exactly the input — never a `Permit*` or another typed message; a provider's own check of the build can run on the client too (`verifyBuild`), and a remote stub's typed order is signed only when it has one (Omniston: `omnistonVerifyBuild` opens the order with the `orderExtension` the build carries). A TON cell-body deposit (`bodyBoc`) is bound too: it attaches the input plus at most `tonGasCeiling` of gas (default 1 TON; a jetton source: the ceiling alone), a jetton source's body must be a TEP-74 transfer of exactly the input from this account's own jetton wallet (`getJettonWalletAddress`) with the excess returning here, the decoded body reaches the review (`cellBodyDecoded`), and a remote stub's cell body needs the app's own validator (`verifyBuild`). An absolute `baseUrl` must be `https:` (a same-origin path is fine; `allowInsecureBaseUrl` for local development). A provider with a claim step (CCTP) also takes `claimTargets` from the app: `claimSwap2` calls only those contracts.
19
19
  - **`createSwap2Handler`** (`@anywalletkit/protocol-swap2-core/server`, server-only) — the other end of `remoteProvider`: validates every request, HMAC-seals only quotes that pass the engine's own checks (`validQuote`: provider, input, floor, slippage, expiry; else 502) and builds only sealed, unexpired ones, puts an intent token on every swap id (tracking, claims and notices without it are refused), passes refusals through with URLs redacted; you supply `guard` (auth + rate limits, it sees the validated request) and, with per-user sign-in, `subject` (quotes and swap ids bound to the user). A reference server: [`examples/server`](https://github.com/ultratestbro/anywalletkit/tree/main/examples/server).
20
20
  - **Tokens outside the catalog** — `allowUnlistedTokens: true` on the engine (and on `createSwap2Handler` for a remote engine) lets a swap name a token the catalog does not list by its EVM contract or Solana mint: the user's own tokens. Off by default. The engine reads the token's decimals with the account's `getTokenInfo` and sends them as `sourceDecimals`; providers that trade any token resolve the source with `sourceAssetOf(params)`, the rest refuse it. Approvals and value binding are the same as for catalog tokens.
21
21
  - **Errors** — `Swap2Error` and its subclasses (quote, validation, execution, fee limit, status…).
@@ -49,6 +49,7 @@ function remoteProvider(opts) {
49
49
  name,
50
50
  remote: true,
51
51
  ...opts.verifyBuild ? { verifiesBuilds: true } : {},
52
+ ...opts.verifyQuote ? { verifiesQuotes: true } : {},
52
53
  ...opts.depositAddressRoutes === true ? { depositAddressRoutes: true } : {},
53
54
  // The browser side has no dark-flag env; the backend enforces its own
54
55
  // flags — a disabled provider simply quotes null / rejects build.
@@ -115,4 +116,4 @@ function remoteProvider(opts) {
115
116
  export {
116
117
  remoteProvider
117
118
  };
118
- //# sourceMappingURL=chunk-6MTZNYKM.js.map
119
+ //# sourceMappingURL=chunk-KB2GX6R2.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/remote.ts"],"sourcesContent":["import type {Asset, CrossChainProvider, IntentRef, IntentStatus, Quote, QuoteParams, UnsignedTx} from \"./types.js\";\nimport {anySignal, timeoutSignal} from \"./util.js\";\n\n/**\n * HTTP-proxied `CrossChainProvider`: forwards every provider call to a backend\n * that runs the real provider. This is how a BROWSER context (e.g. a wallet's\n * signing worker) executes the full swap2 layer — account binding,\n * approvals, cell-body guards, id/status lifecycle — while provider secrets\n * (API JWTs, affiliate config) and non-CORS REST endpoints stay server-side.\n *\n * Wire protocol: POST {baseUrl}/{action} with a JSON body, one action per\n * provider method:\n * quote {provider, params: QuoteParams} → Quote | null\n * build {provider, quote: Quote} → UnsignedTx\n * preflight {provider, quote: Quote, tx: UnsignedTx} → {}\n * track {provider, ref: IntentRef} → IntentStatus\n * notify {provider, intentId, txHash} → {}\n * register {provider, intentId, signature} → {}\n * claim {provider, ref: IntentRef} → UnsignedTx | null\n * supports {provider, from, to} → {supported: boolean}\n *\n * `supportsRoute`/`enabled` need a SYNC answer, so they are config: pass the\n * routes the backend advertises via `supports`, or leave the default\n * allow-and-let-quote-decide behavior (a null quote is a clean miss).\n */\nexport interface RemoteProviderOptions {\n /** Provider name the backend multiplexes on (e.g. \"near-intents\"). */\n name: string;\n /** Endpoint prefix, e.g. \"/api/v2/swap2\" (same-origin) or absolute URL. */\n baseUrl: string;\n /** Sync capability gate; defaults to true (the backend quote decides). */\n supportsRoute?: (from: Asset, to: Asset) => boolean;\n /** Optional synchronous dark flag for accurate client-side discovery. */\n enabled?: () => boolean;\n /** Optional chain-level discovery hooks mirrored from the backend. */\n supportsChain?: (caip2: string) => boolean;\n supportsChainPair?: (fromChain: string, toChain: string) => boolean;\n /** Explicit opt-in when the backend can execute same-chain swaps. */\n supportsSameChain?: boolean;\n /**\n * The contracts this provider's claims may call, per chain (e.g. `cctpClaimTargets` from\n * `@anywalletkit/protocol-swap2-cctp`). The app's list, not the server's: without it the engine\n * refuses claims from this stub (round 3, M24).\n */\n claimTargets?: (chain: string) => readonly string[];\n /**\n * The contracts this provider's EVM swaps may call, and the spenders they may be approved for,\n * per chain — the provider package's pins (`lifiPins`, `fyndPins`, `uniswapPins`…, spread into\n * these options). The app's lists, not the server's: without them the engine refuses this stub's\n * EVM calldata routes and approvals, so a compromised backend cannot name its own contract or\n * spender (scan 2026-10-27 modules, F8/F9/F16). Deposit-address routes need `depositAddressRoutes`.\n */\n callTargets?: (chain: string) => readonly string[];\n spenders?: (chain: string) => readonly string[];\n /**\n * The provider's own check of a quote's transaction, run here on the client against the request\n * the app sent (shipped with the pins, e.g. `fyndPins`): a quote it refuses never reaches the\n * auction. The backend runs the same check, but the backend is what this guards against.\n */\n verifyQuote?: (params: QuoteParams, quote: Quote) => string | null;\n /**\n * The provider's own check of a build on the client — a typed order, a cell body — against the quote it was built\n * for: the reason it fails, or null. A build it refuses is never approved or signed.\n */\n verifyBuild?: (quote: Quote, tx: UnsignedTx, params?: QuoteParams) => string | null | Promise<string | null>;\n /**\n * Let this stub's routes send the input to an address the backend names: a deposit address (NEAR Intents, a\n * THORChain or Chainflip Tron vault) or a plain value send. Off by default: such a route bypasses `callTargets`, so\n * a compromised backend could name its own address (scan 2026-10-28, F9). Opt in only for a provider whose deposit\n * addresses cannot be pinned, or whose `verifyQuote` / `verifyBuild` checks them (`thorchainPins`, `chainflipPins`).\n */\n depositAddressRoutes?: boolean;\n /**\n * Allow a non-https absolute `baseUrl` (local development only). Quotes, builds and typed orders travel this\n * channel; over plain http anyone on the path can rewrite them (scan 2026-10-27 modules, F4).\n */\n allowInsecureBaseUrl?: boolean;\n /** Defaults to globalThis.fetch — injectable for tests. */\n fetchImpl?: typeof fetch;\n /** Extra headers (e.g. auth) attached to every call. */\n headers?: Record<string, string>;\n /** Per-request fail-closed timeout. Defaults to 12 seconds. */\n timeoutMs?: number;\n}\n\n/**\n * A provider that runs on your server: quotes, deposits and tracking go to `baseUrl`, so API keys\n * and affiliate settings never ship in the app. The engine still validates every quote and\n * transaction locally before signing.\n */\nexport function remoteProvider(opts: RemoteProviderOptions): CrossChainProvider {\n const name = opts.name;\n if (!/^[A-Za-z0-9_-]+$/.test(name)) throw new Error(\"swap2-remote: invalid provider name\");\n // Trimmed first: a URL parser drops leading spaces and tabs, so \" http://host\" would reach http (scan 2026-10-28 review).\n const baseUrl = opts.baseUrl.trim().replace(/\\/+$/, \"\");\n if (!baseUrl) throw new Error(\"swap2-remote: baseUrl is required\");\n // A same-origin path (\"/api/swap2\") inherits the page's scheme; an absolute URL must be https, and a\n // scheme-relative one (\"//host/…\") names another origin without saying how to reach it.\n const insecure = /^[a-z][a-z0-9+.-]*:/i.test(baseUrl) ? !/^https:\\/\\//i.test(baseUrl) : /^[\\\\/]{2}/.test(baseUrl);\n if (insecure && opts.allowInsecureBaseUrl !== true) {\n throw new Error(\"swap2-remote: baseUrl must be https:// or a same-origin path (allowInsecureBaseUrl for local development)\");\n }\n const supportsRoute = opts.supportsRoute;\n const enabled = opts.enabled;\n const headers = Object.freeze({...opts.headers});\n const doFetch = opts.fetchImpl ?? fetch;\n const timeoutMs = opts.timeoutMs ?? 12_000;\n if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 60_000) {\n throw new Error(\"swap2-remote: timeoutMs must be an integer between 1 and 60000\");\n }\n\n // The request each quote answered, for verifyBuild (the build only carries the quote). Bounded: a stub quotes often.\n const requests = new Map<string, QuoteParams>();\n const remember = (intentId: string, params: QuoteParams) => {\n requests.delete(intentId);\n requests.set(intentId, params);\n if (requests.size > 64) requests.delete(requests.keys().next().value!);\n };\n\n async function call<T>(action: string, body: Record<string, unknown>, signal?: AbortSignal): Promise<T> {\n const timeout = timeoutSignal(timeoutMs);\n const requestSignal = signal ? anySignal([signal, timeout]) : timeout;\n const res = await doFetch(`${baseUrl}/${action}`, {\n method: \"POST\",\n headers: {...headers, \"content-type\": \"application/json\"},\n // UnsignedTx carries bigint fields (value, gasLimit) — JSON.stringify\n // throws on them (\"Do not know how to serialize a BigInt\", seen\n // 2026-10-10 on a Solana → Base preflight). Wire them as decimal strings.\n body: JSON.stringify({provider: name, ...body}, (_k, v) => (typeof v === \"bigint\" ? v.toString() : v)),\n signal: requestSignal,\n });\n if (!res.ok) {\n // Surface the backend's message — provider errors are actionable\n // (\"quote expired\", \"route unsupported\"), not just status codes.\n const text = await res.text().catch(() => \"\");\n throw new Error(`swap2-remote ${name}/${action}: ${res.status} ${text.slice(0, 300)}`);\n }\n return (await res.json()) as T;\n }\n\n return {\n name,\n remote: true,\n ...(opts.verifyBuild ? {verifiesBuilds: true} : {}),\n ...(opts.depositAddressRoutes === true ? {depositAddressRoutes: true} : {}),\n // The browser side has no dark-flag env; the backend enforces its own\n // flags — a disabled provider simply quotes null / rejects build.\n enabled: () => enabled ? enabled() : true,\n supportsRoute(from: Asset, to: Asset): boolean {\n return supportsRoute ? supportsRoute(from, to) : true;\n },\n async getQuote(params: QuoteParams, signal?: AbortSignal): Promise<Quote | null> {\n const out = await call<Quote | {noQuote: string} | null>(\"quote\", {params}, signal);\n if (out && typeof out === \"object\" && \"noQuote\" in out) {\n // The backend forwarded the venue's own refusal — throw it so the\n // router records a REASON instead of an anonymous null.\n throw new Error(String((out as {noQuote: unknown}).noQuote).slice(0, 300));\n }\n if (out && typeof out === \"object\" && typeof out.intentId === \"string\") remember(out.intentId, params);\n if (out && opts.verifyQuote) {\n let refusal: string | null;\n try { refusal = opts.verifyQuote(params, out); } catch (e) { refusal = e instanceof Error ? e.message : String(e); }\n if (refusal) throw new Error(`${name}: the quote fails this app's own check — ${refusal.slice(0, 240)}`);\n }\n return out;\n },\n async buildDeposit(quote: Quote): Promise<UnsignedTx> {\n const tx = await call<UnsignedTx>(\"build\", {quote});\n if (opts.verifyBuild) {\n let refusal: string | null;\n try { refusal = await opts.verifyBuild(quote, tx, requests.get(quote.intentId)); } catch (e) { refusal = e instanceof Error ? e.message : String(e); }\n if (refusal) throw new Error(`${name}: the build fails this app's own check — ${refusal.slice(0, 240)}`);\n }\n return tx;\n },\n async preflightDeposit(quote: Quote, tx: UnsignedTx): Promise<void> {\n await call<Record<string, never>>(\"preflight\", {quote, tx});\n },\n async trackIntent(ref: IntentRef): Promise<IntentStatus> {\n return call<IntentStatus>(\"track\", {ref});\n },\n async notifyDeposit(intentId: string, txHash: string): Promise<void> {\n await call<Record<string, never>>(\"notify\", {intentId, txHash});\n },\n async registerSignedOrder(intentId: string, signature: string): Promise<void> {\n await call<Record<string, never>>(\"register\", {intentId, signature});\n },\n async buildClaim(ref: IntentRef): Promise<UnsignedTx | null> {\n const out = await call<UnsignedTx | null | Record<string, never>>(\"claim\", {ref});\n // A backend without the action (older deploy) answers {} — treat as \"not ready\".\n if (!out || typeof out !== \"object\" || !(\"to\" in out)) return null;\n return out as UnsignedTx;\n },\n ...(opts.supportsChain ? {supportsChain: opts.supportsChain.bind(opts)} : {}),\n ...(opts.supportsChainPair ? {supportsChainPair: opts.supportsChainPair.bind(opts)} : {}),\n ...(opts.supportsSameChain !== undefined ? {supportsSameChain: opts.supportsSameChain} : {}),\n ...(opts.claimTargets ? {claimTargets: opts.claimTargets} : {}),\n ...(opts.callTargets ? {callTargets: opts.callTargets} : {}),\n ...(opts.spenders ? {spenders: opts.spenders} : {}),\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/remote.ts"],"sourcesContent":["import type {Asset, CrossChainProvider, IntentRef, IntentStatus, Quote, QuoteParams, UnsignedTx} from \"./types.js\";\nimport {anySignal, timeoutSignal} from \"./util.js\";\n\n/**\n * HTTP-proxied `CrossChainProvider`: forwards every provider call to a backend\n * that runs the real provider. This is how a BROWSER context (e.g. a wallet's\n * signing worker) executes the full swap2 layer — account binding,\n * approvals, cell-body guards, id/status lifecycle — while provider secrets\n * (API JWTs, affiliate config) and non-CORS REST endpoints stay server-side.\n *\n * Wire protocol: POST {baseUrl}/{action} with a JSON body, one action per\n * provider method:\n * quote {provider, params: QuoteParams} → Quote | null\n * build {provider, quote: Quote} → UnsignedTx\n * preflight {provider, quote: Quote, tx: UnsignedTx} → {}\n * track {provider, ref: IntentRef} → IntentStatus\n * notify {provider, intentId, txHash} → {}\n * register {provider, intentId, signature} → {}\n * claim {provider, ref: IntentRef} → UnsignedTx | null\n * supports {provider, from, to} → {supported: boolean}\n *\n * `supportsRoute`/`enabled` need a SYNC answer, so they are config: pass the\n * routes the backend advertises via `supports`, or leave the default\n * allow-and-let-quote-decide behavior (a null quote is a clean miss).\n */\nexport interface RemoteProviderOptions {\n /** Provider name the backend multiplexes on (e.g. \"near-intents\"). */\n name: string;\n /** Endpoint prefix, e.g. \"/api/v2/swap2\" (same-origin) or absolute URL. */\n baseUrl: string;\n /** Sync capability gate; defaults to true (the backend quote decides). */\n supportsRoute?: (from: Asset, to: Asset) => boolean;\n /** Optional synchronous dark flag for accurate client-side discovery. */\n enabled?: () => boolean;\n /** Optional chain-level discovery hooks mirrored from the backend. */\n supportsChain?: (caip2: string) => boolean;\n supportsChainPair?: (fromChain: string, toChain: string) => boolean;\n /** Explicit opt-in when the backend can execute same-chain swaps. */\n supportsSameChain?: boolean;\n /**\n * The contracts this provider's claims may call, per chain (e.g. `cctpClaimTargets` from\n * `@anywalletkit/protocol-swap2-cctp`). The app's list, not the server's: without it the engine\n * refuses claims from this stub (round 3, M24).\n */\n claimTargets?: (chain: string) => readonly string[];\n /**\n * The contracts this provider's EVM swaps may call, and the spenders they may be approved for,\n * per chain — the provider package's pins (`lifiPins`, `fyndPins`, `uniswapPins`…, spread into\n * these options). The app's lists, not the server's: without them the engine refuses this stub's\n * EVM calldata routes and approvals, so a compromised backend cannot name its own contract or\n * spender (scan 2026-10-27 modules, F8/F9/F16). Deposit-address routes need `depositAddressRoutes`.\n */\n callTargets?: (chain: string) => readonly string[];\n spenders?: (chain: string) => readonly string[];\n /**\n * The provider's own check of a quote's transaction, run here on the client against the request\n * the app sent (shipped with the pins, e.g. `fyndPins`): a quote it refuses never reaches the\n * auction. The backend runs the same check, but the backend is what this guards against.\n */\n verifyQuote?: (params: QuoteParams, quote: Quote) => string | null;\n /**\n * The provider's own check of a build on the client — a typed order, a cell body — against the quote it was built\n * for: the reason it fails, or null. A build it refuses is never approved or signed.\n */\n verifyBuild?: (quote: Quote, tx: UnsignedTx, params?: QuoteParams) => string | null | Promise<string | null>;\n /**\n * Let this stub's routes send the input to an address the backend names: a deposit address (NEAR Intents, a\n * THORChain or Chainflip Tron vault) or a plain value send. Off by default: such a route bypasses `callTargets`, so\n * a compromised backend could name its own address (scan 2026-10-28, F9). Opt in only for a provider whose deposit\n * addresses cannot be pinned, or whose `verifyQuote` / `verifyBuild` checks them (`thorchainPins`, `chainflipPins`).\n */\n depositAddressRoutes?: boolean;\n /**\n * Allow a non-https absolute `baseUrl` (local development only). Quotes, builds and typed orders travel this\n * channel; over plain http anyone on the path can rewrite them (scan 2026-10-27 modules, F4).\n */\n allowInsecureBaseUrl?: boolean;\n /** Defaults to globalThis.fetch — injectable for tests. */\n fetchImpl?: typeof fetch;\n /** Extra headers (e.g. auth) attached to every call. */\n headers?: Record<string, string>;\n /** Per-request fail-closed timeout. Defaults to 12 seconds. */\n timeoutMs?: number;\n}\n\n/**\n * A provider that runs on your server: quotes, deposits and tracking go to `baseUrl`, so API keys\n * and affiliate settings never ship in the app. The engine still validates every quote and\n * transaction locally before signing.\n */\nexport function remoteProvider(opts: RemoteProviderOptions): CrossChainProvider {\n const name = opts.name;\n if (!/^[A-Za-z0-9_-]+$/.test(name)) throw new Error(\"swap2-remote: invalid provider name\");\n // Trimmed first: a URL parser drops leading spaces and tabs, so \" http://host\" would reach http (scan 2026-10-28 review).\n const baseUrl = opts.baseUrl.trim().replace(/\\/+$/, \"\");\n if (!baseUrl) throw new Error(\"swap2-remote: baseUrl is required\");\n // A same-origin path (\"/api/swap2\") inherits the page's scheme; an absolute URL must be https, and a\n // scheme-relative one (\"//host/…\") names another origin without saying how to reach it.\n const insecure = /^[a-z][a-z0-9+.-]*:/i.test(baseUrl) ? !/^https:\\/\\//i.test(baseUrl) : /^[\\\\/]{2}/.test(baseUrl);\n if (insecure && opts.allowInsecureBaseUrl !== true) {\n throw new Error(\"swap2-remote: baseUrl must be https:// or a same-origin path (allowInsecureBaseUrl for local development)\");\n }\n const supportsRoute = opts.supportsRoute;\n const enabled = opts.enabled;\n const headers = Object.freeze({...opts.headers});\n const doFetch = opts.fetchImpl ?? fetch;\n const timeoutMs = opts.timeoutMs ?? 12_000;\n if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 60_000) {\n throw new Error(\"swap2-remote: timeoutMs must be an integer between 1 and 60000\");\n }\n\n // The request each quote answered, for verifyBuild (the build only carries the quote). Bounded: a stub quotes often.\n const requests = new Map<string, QuoteParams>();\n const remember = (intentId: string, params: QuoteParams) => {\n requests.delete(intentId);\n requests.set(intentId, params);\n if (requests.size > 64) requests.delete(requests.keys().next().value!);\n };\n\n async function call<T>(action: string, body: Record<string, unknown>, signal?: AbortSignal): Promise<T> {\n const timeout = timeoutSignal(timeoutMs);\n const requestSignal = signal ? anySignal([signal, timeout]) : timeout;\n const res = await doFetch(`${baseUrl}/${action}`, {\n method: \"POST\",\n headers: {...headers, \"content-type\": \"application/json\"},\n // UnsignedTx carries bigint fields (value, gasLimit) — JSON.stringify\n // throws on them (\"Do not know how to serialize a BigInt\", seen\n // 2026-10-10 on a Solana → Base preflight). Wire them as decimal strings.\n body: JSON.stringify({provider: name, ...body}, (_k, v) => (typeof v === \"bigint\" ? v.toString() : v)),\n signal: requestSignal,\n });\n if (!res.ok) {\n // Surface the backend's message — provider errors are actionable\n // (\"quote expired\", \"route unsupported\"), not just status codes.\n const text = await res.text().catch(() => \"\");\n throw new Error(`swap2-remote ${name}/${action}: ${res.status} ${text.slice(0, 300)}`);\n }\n return (await res.json()) as T;\n }\n\n return {\n name,\n remote: true,\n ...(opts.verifyBuild ? {verifiesBuilds: true} : {}),\n ...(opts.verifyQuote ? {verifiesQuotes: true} : {}),\n ...(opts.depositAddressRoutes === true ? {depositAddressRoutes: true} : {}),\n // The browser side has no dark-flag env; the backend enforces its own\n // flags — a disabled provider simply quotes null / rejects build.\n enabled: () => enabled ? enabled() : true,\n supportsRoute(from: Asset, to: Asset): boolean {\n return supportsRoute ? supportsRoute(from, to) : true;\n },\n async getQuote(params: QuoteParams, signal?: AbortSignal): Promise<Quote | null> {\n const out = await call<Quote | {noQuote: string} | null>(\"quote\", {params}, signal);\n if (out && typeof out === \"object\" && \"noQuote\" in out) {\n // The backend forwarded the venue's own refusal — throw it so the\n // router records a REASON instead of an anonymous null.\n throw new Error(String((out as {noQuote: unknown}).noQuote).slice(0, 300));\n }\n if (out && typeof out === \"object\" && typeof out.intentId === \"string\") remember(out.intentId, params);\n if (out && opts.verifyQuote) {\n let refusal: string | null;\n try { refusal = opts.verifyQuote(params, out); } catch (e) { refusal = e instanceof Error ? e.message : String(e); }\n if (refusal) throw new Error(`${name}: the quote fails this app's own check — ${refusal.slice(0, 240)}`);\n }\n return out;\n },\n async buildDeposit(quote: Quote): Promise<UnsignedTx> {\n const tx = await call<UnsignedTx>(\"build\", {quote});\n if (opts.verifyBuild) {\n let refusal: string | null;\n try { refusal = await opts.verifyBuild(quote, tx, requests.get(quote.intentId)); } catch (e) { refusal = e instanceof Error ? e.message : String(e); }\n if (refusal) throw new Error(`${name}: the build fails this app's own check — ${refusal.slice(0, 240)}`);\n }\n return tx;\n },\n async preflightDeposit(quote: Quote, tx: UnsignedTx): Promise<void> {\n await call<Record<string, never>>(\"preflight\", {quote, tx});\n },\n async trackIntent(ref: IntentRef): Promise<IntentStatus> {\n return call<IntentStatus>(\"track\", {ref});\n },\n async notifyDeposit(intentId: string, txHash: string): Promise<void> {\n await call<Record<string, never>>(\"notify\", {intentId, txHash});\n },\n async registerSignedOrder(intentId: string, signature: string): Promise<void> {\n await call<Record<string, never>>(\"register\", {intentId, signature});\n },\n async buildClaim(ref: IntentRef): Promise<UnsignedTx | null> {\n const out = await call<UnsignedTx | null | Record<string, never>>(\"claim\", {ref});\n // A backend without the action (older deploy) answers {} — treat as \"not ready\".\n if (!out || typeof out !== \"object\" || !(\"to\" in out)) return null;\n return out as UnsignedTx;\n },\n ...(opts.supportsChain ? {supportsChain: opts.supportsChain.bind(opts)} : {}),\n ...(opts.supportsChainPair ? {supportsChainPair: opts.supportsChainPair.bind(opts)} : {}),\n ...(opts.supportsSameChain !== undefined ? {supportsSameChain: opts.supportsSameChain} : {}),\n ...(opts.claimTargets ? {claimTargets: opts.claimTargets} : {}),\n ...(opts.callTargets ? {callTargets: opts.callTargets} : {}),\n ...(opts.spenders ? {spenders: opts.spenders} : {}),\n };\n}\n"],"mappings":";;;;;;AA0FO,SAAS,eAAe,MAAiD;AAC9E,QAAM,OAAO,KAAK;AAClB,MAAI,CAAC,mBAAmB,KAAK,IAAI,EAAG,OAAM,IAAI,MAAM,qCAAqC;AAEzF,QAAM,UAAU,KAAK,QAAQ,KAAK,EAAE,QAAQ,QAAQ,EAAE;AACtD,MAAI,CAAC,QAAS,OAAM,IAAI,MAAM,mCAAmC;AAGjE,QAAM,WAAW,uBAAuB,KAAK,OAAO,IAAI,CAAC,eAAe,KAAK,OAAO,IAAI,YAAY,KAAK,OAAO;AAChH,MAAI,YAAY,KAAK,yBAAyB,MAAM;AAClD,UAAM,IAAI,MAAM,2GAA2G;AAAA,EAC7H;AACA,QAAM,gBAAgB,KAAK;AAC3B,QAAM,UAAU,KAAK;AACrB,QAAM,UAAU,OAAO,OAAO,EAAC,GAAG,KAAK,QAAO,CAAC;AAC/C,QAAM,UAAU,KAAK,aAAa;AAClC,QAAM,YAAY,KAAK,aAAa;AACpC,MAAI,CAAC,OAAO,UAAU,SAAS,KAAK,aAAa,KAAK,YAAY,KAAQ;AACxE,UAAM,IAAI,MAAM,gEAAgE;AAAA,EAClF;AAGA,QAAM,WAAW,oBAAI,IAAyB;AAC9C,QAAM,WAAW,CAAC,UAAkB,WAAwB;AAC1D,aAAS,OAAO,QAAQ;AACxB,aAAS,IAAI,UAAU,MAAM;AAC7B,QAAI,SAAS,OAAO,GAAI,UAAS,OAAO,SAAS,KAAK,EAAE,KAAK,EAAE,KAAM;AAAA,EACvE;AAEA,iBAAe,KAAQ,QAAgB,MAA+B,QAAkC;AACtG,UAAM,UAAU,cAAc,SAAS;AACvC,UAAM,gBAAgB,SAAS,UAAU,CAAC,QAAQ,OAAO,CAAC,IAAI;AAC9D,UAAM,MAAM,MAAM,QAAQ,GAAG,OAAO,IAAI,MAAM,IAAI;AAAA,MAChD,QAAQ;AAAA,MACR,SAAS,EAAC,GAAG,SAAS,gBAAgB,mBAAkB;AAAA;AAAA;AAAA;AAAA,MAIxD,MAAM,KAAK,UAAU,EAAC,UAAU,MAAM,GAAG,KAAI,GAAG,CAAC,IAAI,MAAO,OAAO,MAAM,WAAW,EAAE,SAAS,IAAI,CAAE;AAAA,MACrG,QAAQ;AAAA,IACV,CAAC;AACD,QAAI,CAAC,IAAI,IAAI;AAGX,YAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,EAAE;AAC5C,YAAM,IAAI,MAAM,gBAAgB,IAAI,IAAI,MAAM,KAAK,IAAI,MAAM,IAAI,KAAK,MAAM,GAAG,GAAG,CAAC,EAAE;AAAA,IACvF;AACA,WAAQ,MAAM,IAAI,KAAK;AAAA,EACzB;AAEA,SAAO;AAAA,IACL;AAAA,IACA,QAAQ;AAAA,IACR,GAAI,KAAK,cAAc,EAAC,gBAAgB,KAAI,IAAI,CAAC;AAAA,IACjD,GAAI,KAAK,cAAc,EAAC,gBAAgB,KAAI,IAAI,CAAC;AAAA,IACjD,GAAI,KAAK,yBAAyB,OAAO,EAAC,sBAAsB,KAAI,IAAI,CAAC;AAAA;AAAA;AAAA,IAGzE,SAAS,MAAM,UAAU,QAAQ,IAAI;AAAA,IACrC,cAAc,MAAa,IAAoB;AAC7C,aAAO,gBAAgB,cAAc,MAAM,EAAE,IAAI;AAAA,IACnD;AAAA,IACA,MAAM,SAAS,QAAqB,QAA6C;AAC/E,YAAM,MAAM,MAAM,KAAuC,SAAS,EAAC,OAAM,GAAG,MAAM;AAClF,UAAI,OAAO,OAAO,QAAQ,YAAY,aAAa,KAAK;AAGtD,cAAM,IAAI,MAAM,OAAQ,IAA2B,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC;AAAA,MAC3E;AACA,UAAI,OAAO,OAAO,QAAQ,YAAY,OAAO,IAAI,aAAa,SAAU,UAAS,IAAI,UAAU,MAAM;AACrG,UAAI,OAAO,KAAK,aAAa;AAC3B,YAAI;AACJ,YAAI;AAAE,oBAAU,KAAK,YAAY,QAAQ,GAAG;AAAA,QAAG,SAAS,GAAG;AAAE,oBAAU,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;AAAA,QAAG;AACnH,YAAI,QAAS,OAAM,IAAI,MAAM,GAAG,IAAI,iDAA4C,QAAQ,MAAM,GAAG,GAAG,CAAC,EAAE;AAAA,MACzG;AACA,aAAO;AAAA,IACT;AAAA,IACA,MAAM,aAAa,OAAmC;AACpD,YAAM,KAAK,MAAM,KAAiB,SAAS,EAAC,MAAK,CAAC;AAClD,UAAI,KAAK,aAAa;AACpB,YAAI;AACJ,YAAI;AAAE,oBAAU,MAAM,KAAK,YAAY,OAAO,IAAI,SAAS,IAAI,MAAM,QAAQ,CAAC;AAAA,QAAG,SAAS,GAAG;AAAE,oBAAU,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;AAAA,QAAG;AACrJ,YAAI,QAAS,OAAM,IAAI,MAAM,GAAG,IAAI,iDAA4C,QAAQ,MAAM,GAAG,GAAG,CAAC,EAAE;AAAA,MACzG;AACA,aAAO;AAAA,IACT;AAAA,IACA,MAAM,iBAAiB,OAAc,IAA+B;AAClE,YAAM,KAA4B,aAAa,EAAC,OAAO,GAAE,CAAC;AAAA,IAC5D;AAAA,IACA,MAAM,YAAY,KAAuC;AACvD,aAAO,KAAmB,SAAS,EAAC,IAAG,CAAC;AAAA,IAC1C;AAAA,IACA,MAAM,cAAc,UAAkB,QAA+B;AACnE,YAAM,KAA4B,UAAU,EAAC,UAAU,OAAM,CAAC;AAAA,IAChE;AAAA,IACA,MAAM,oBAAoB,UAAkB,WAAkC;AAC5E,YAAM,KAA4B,YAAY,EAAC,UAAU,UAAS,CAAC;AAAA,IACrE;AAAA,IACA,MAAM,WAAW,KAA4C;AAC3D,YAAM,MAAM,MAAM,KAAgD,SAAS,EAAC,IAAG,CAAC;AAEhF,UAAI,CAAC,OAAO,OAAO,QAAQ,YAAY,EAAE,QAAQ,KAAM,QAAO;AAC9D,aAAO;AAAA,IACT;AAAA,IACA,GAAI,KAAK,gBAAgB,EAAC,eAAe,KAAK,cAAc,KAAK,IAAI,EAAC,IAAI,CAAC;AAAA,IAC3E,GAAI,KAAK,oBAAoB,EAAC,mBAAmB,KAAK,kBAAkB,KAAK,IAAI,EAAC,IAAI,CAAC;AAAA,IACvF,GAAI,KAAK,sBAAsB,SAAY,EAAC,mBAAmB,KAAK,kBAAiB,IAAI,CAAC;AAAA,IAC1F,GAAI,KAAK,eAAe,EAAC,cAAc,KAAK,aAAY,IAAI,CAAC;AAAA,IAC7D,GAAI,KAAK,cAAc,EAAC,aAAa,KAAK,YAAW,IAAI,CAAC;AAAA,IAC1D,GAAI,KAAK,WAAW,EAAC,UAAU,KAAK,SAAQ,IAAI,CAAC;AAAA,EACnD;AACF;","names":[]}
@@ -793,6 +793,8 @@ var BaseSwap2Protocol = class extends Swap2Protocol {
793
793
  }
794
794
  } else if (remote && !pinnedTargets.includes(unsigned.to.toLowerCase())) {
795
795
  throw new Swap2ValidationError(`swap2: remote provider '${q.provider}' calls ${getAddress(unsigned.to)} on ${this.fromChain}, which the app did not pin as a call target (remoteProvider callTargets) \u2014 refused`);
796
+ } else if (remote && provider.verifiesQuotes !== true && provider.verifiesBuilds !== true) {
797
+ throw new Swap2ValidationError(`swap2: remote provider '${q.provider}' calls ${getAddress(unsigned.to)} with calldata the app does not check (remoteProvider verifyQuote / verifyBuild) \u2014 refused`);
796
798
  }
797
799
  }
798
800
  const needsApproval = !!unsigned.approvalAddress && !!source.contract;
@@ -1236,4 +1238,4 @@ export {
1236
1238
  swap2ChainId,
1237
1239
  BaseSwap2Protocol
1238
1240
  };
1239
- //# sourceMappingURL=chunk-CRCDWV3L.js.map
1241
+ //# sourceMappingURL=chunk-PU5NM3Y6.js.map