@anywalletkit/protocol-swap2-core 0.6.1 → 0.8.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.
Files changed (53) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-2KC734UX.js +117 -0
  3. package/dist/chunk-2KC734UX.js.map +1 -0
  4. package/dist/{chunk-KIAQXIBG.js → chunk-5FPDKWBG.js} +21 -2
  5. package/dist/chunk-5FPDKWBG.js.map +1 -0
  6. package/dist/{chunk-ZMWK3RQ4.js → chunk-6MTZNYKM.js} +40 -5
  7. package/dist/chunk-6MTZNYKM.js.map +1 -0
  8. package/dist/{chunk-IGTMUTNJ.js → chunk-CRCDWV3L.js} +130 -7
  9. package/dist/chunk-CRCDWV3L.js.map +1 -0
  10. package/dist/{chunk-JFGMF6FO.js → chunk-FHEU7E5A.js} +9 -3
  11. package/dist/chunk-FHEU7E5A.js.map +1 -0
  12. package/dist/index.cjs +256 -10
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +4 -4
  15. package/dist/index.d.ts +4 -4
  16. package/dist/index.js +10 -6
  17. package/dist/remote.cjs +38 -3
  18. package/dist/remote.cjs.map +1 -1
  19. package/dist/remote.d.cts +33 -1
  20. package/dist/remote.d.ts +33 -1
  21. package/dist/remote.js +2 -2
  22. package/dist/server.cjs +23 -4
  23. package/dist/server.cjs.map +1 -1
  24. package/dist/server.js +6 -5
  25. package/dist/server.js.map +1 -1
  26. package/dist/swap2/base.cjs +187 -16
  27. package/dist/swap2/base.cjs.map +1 -1
  28. package/dist/swap2/base.d.cts +19 -1
  29. package/dist/swap2/base.d.ts +19 -1
  30. package/dist/swap2/base.js +2 -1
  31. package/dist/swap2/single.cjs +210 -16
  32. package/dist/swap2/single.cjs.map +1 -1
  33. package/dist/swap2/single.js +4 -3
  34. package/dist/types.cjs.map +1 -1
  35. package/dist/types.d.cts +49 -1
  36. package/dist/types.d.ts +49 -1
  37. package/dist/util.cjs +70 -2
  38. package/dist/util.cjs.map +1 -1
  39. package/dist/util.d.cts +13 -2
  40. package/dist/util.d.ts +13 -2
  41. package/dist/util.js +7 -3
  42. package/dist/validate.cjs +19 -0
  43. package/dist/validate.cjs.map +1 -1
  44. package/dist/validate.d.cts +12 -2
  45. package/dist/validate.d.ts +12 -2
  46. package/dist/validate.js +3 -1
  47. package/package.json +5 -5
  48. package/dist/chunk-IGTMUTNJ.js.map +0 -1
  49. package/dist/chunk-JFGMF6FO.js.map +0 -1
  50. package/dist/chunk-KIAQXIBG.js.map +0 -1
  51. package/dist/chunk-MNJTMSFN.js +0 -49
  52. package/dist/chunk-MNJTMSFN.js.map +0 -1
  53. package/dist/chunk-ZMWK3RQ4.js.map +0 -1
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. A provider with a claim step (CCTP) needs `claimTargets` from the app (e.g. `cctpClaimTargets`): `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). 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…).
@@ -0,0 +1,117 @@
1
+ // src/util.ts
2
+ import { resolveAsset } from "@anywalletkit/swap2-assets";
3
+ function namespace(chain) {
4
+ return chain.split(":")[0] ?? "";
5
+ }
6
+ function isEvm(chain) {
7
+ return namespace(chain) === "eip155";
8
+ }
9
+ function evmRef(chain) {
10
+ return Number(chain.split(":")[1] ?? "0");
11
+ }
12
+ function timeoutSignal(ms) {
13
+ const native = AbortSignal.timeout;
14
+ if (typeof native === "function") return native.call(AbortSignal, ms);
15
+ const ctrl = new AbortController();
16
+ setTimeout(() => ctrl.abort(), ms);
17
+ return ctrl.signal;
18
+ }
19
+ function anySignal(signals) {
20
+ const native = AbortSignal.any;
21
+ if (typeof native === "function") return native.call(AbortSignal, signals);
22
+ const ctrl = new AbortController();
23
+ for (const s of signals) {
24
+ if (s.aborted) {
25
+ ctrl.abort();
26
+ break;
27
+ }
28
+ s.addEventListener("abort", () => ctrl.abort(), { once: true });
29
+ }
30
+ return ctrl.signal;
31
+ }
32
+ var SCHEME_MAX = 32;
33
+ var isLetter = (c) => c >= 97 && c <= 122 || c >= 65 && c <= 90;
34
+ var isSchemeChar = (c) => isLetter(c) || c >= 48 && c <= 57 || c === 43 || c === 45 || c === 46;
35
+ var endsUrl = (c) => c === 34 || c === 39 || c === 60 || c === 62 || c >= 9 && c <= 13 || c === 32 || c === 160 || c === 5760 || c >= 8192 && c <= 8202 || c === 8232 || c === 8233 || c === 8239 || c === 8287 || c === 12288 || c === 65279;
36
+ function redactUrls(text) {
37
+ let out = "";
38
+ let done = 0;
39
+ for (let at = text.indexOf("://"); at !== -1; at = text.indexOf("://", at + 3)) {
40
+ let start = at;
41
+ while (start > done && at - start < SCHEME_MAX && isSchemeChar(text.charCodeAt(start - 1))) start--;
42
+ while (start < at && !isLetter(text.charCodeAt(start))) start++;
43
+ if (start === at) continue;
44
+ let end = at + 3;
45
+ while (end < text.length && !endsUrl(text.charCodeAt(end))) end++;
46
+ if (end === at + 3) continue;
47
+ out += `${text.slice(done, start)}<url>`;
48
+ done = end;
49
+ at = end - 3;
50
+ }
51
+ return done === 0 ? text : out + text.slice(done);
52
+ }
53
+ function sourceAssetOf(params) {
54
+ const decimals = params.sourceDecimals;
55
+ return resolveAsset(params.sourceChain, params.sourceToken, decimals === void 0 ? {} : { allowUnlisted: true, decimals });
56
+ }
57
+ var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
58
+ function base64Bytes(text) {
59
+ const s = text.replace(/-/g, "+").replace(/_/g, "/").replace(/=+$/, "");
60
+ if (!/^[A-Za-z0-9+/]*$/.test(s) || s.length % 4 === 1) return null;
61
+ const out = new Uint8Array(Math.floor(s.length * 3 / 4));
62
+ let bits = 0, acc = 0, n = 0;
63
+ for (const ch of s) {
64
+ acc = acc << 6 | B64.indexOf(ch);
65
+ bits += 6;
66
+ if (bits >= 8) {
67
+ bits -= 8;
68
+ out[n++] = acc >> bits & 255;
69
+ }
70
+ }
71
+ return out.subarray(0, n);
72
+ }
73
+ function crc16(bytes) {
74
+ let crc = 0;
75
+ for (const b of bytes) {
76
+ crc ^= b << 8;
77
+ for (let i = 0; i < 8; i++) crc = crc & 32768 ? (crc << 1 ^ 4129) & 65535 : crc << 1 & 65535;
78
+ }
79
+ return crc;
80
+ }
81
+ function tonRawAddress(address) {
82
+ if (address && typeof address === "object") {
83
+ const o = address;
84
+ if (typeof o.toRawString === "function") {
85
+ try {
86
+ return tonRawAddress(o.toRawString());
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+ if (typeof o.workChain === "number" && o.hash instanceof Uint8Array && o.hash.length === 32) {
92
+ return `${o.workChain}:${Array.from(o.hash, (b) => b.toString(16).padStart(2, "0")).join("")}`;
93
+ }
94
+ return null;
95
+ }
96
+ if (typeof address !== "string") return null;
97
+ const raw = /^(-?[0-9]{1,3}):([0-9a-fA-F]{64})$/.exec(address.trim());
98
+ if (raw) return `${Number(raw[1])}:${raw[2].toLowerCase()}`;
99
+ if (!/^[A-Za-z0-9+/_-]{48}$/.test(address.trim())) return null;
100
+ const bytes = base64Bytes(address.trim());
101
+ if (!bytes || bytes.length !== 36 || crc16(bytes.subarray(0, 34)) !== (bytes[34] << 8 | bytes[35])) return null;
102
+ const wc = bytes[1] === 255 ? -1 : bytes[1];
103
+ return `${wc}:${Array.from(bytes.subarray(2, 34), (b) => b.toString(16).padStart(2, "0")).join("")}`;
104
+ }
105
+
106
+ export {
107
+ namespace,
108
+ isEvm,
109
+ evmRef,
110
+ timeoutSignal,
111
+ anySignal,
112
+ redactUrls,
113
+ sourceAssetOf,
114
+ base64Bytes,
115
+ tonRawAddress
116
+ };
117
+ //# sourceMappingURL=chunk-2KC734UX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/util.ts"],"sourcesContent":["import {resolveAsset, type CcAsset} from \"@anywalletkit/swap2-assets\";\nimport type {QuoteParams} from \"./types.js\";\n/** Shared CAIP-2 helpers. Contribution packages never read deployment env. */\nexport function namespace(chain: string): string {\n return chain.split(\":\")[0] ?? \"\";\n}\n/** True for an `eip155:` chain id. */\nexport function isEvm(chain: string): boolean {\n return namespace(chain) === \"eip155\";\n}\n/** Numeric chain id of an `eip155:<id>` CAIP-2 id. */\nexport function evmRef(chain: string): number {\n return Number(chain.split(\":\")[1] ?? \"0\");\n}\n\n/**\n * `AbortSignal.timeout`, portably: Hermes (React Native) ships AbortController\n * but not the static helpers, and the unguarded call threw\n * \"AbortSignal.timeout is not a function\" on every remote execution — which\n * the auction then read as \"no route\". Falls back to a manual controller.\n */\nexport function timeoutSignal(ms: number): AbortSignal {\n const native = (AbortSignal as unknown as {timeout?: (ms: number) => AbortSignal}).timeout;\n if (typeof native === \"function\") return native.call(AbortSignal, ms);\n const ctrl = new AbortController();\n setTimeout(() => ctrl.abort(), ms);\n return ctrl.signal;\n}\n\n/** `AbortSignal.any`, portably (see timeoutSignal). */\nexport function anySignal(signals: readonly AbortSignal[]): AbortSignal {\n const native = (AbortSignal as unknown as {any?: (s: readonly AbortSignal[]) => AbortSignal}).any;\n if (typeof native === \"function\") return native.call(AbortSignal, signals);\n const ctrl = new AbortController();\n for (const s of signals) {\n if (s.aborted) {\n ctrl.abort();\n break;\n }\n s.addEventListener(\"abort\", () => ctrl.abort(), {once: true});\n }\n return ctrl.signal;\n}\n\n/** Longest URL scheme taken out (`https`, `wss`, `git+ssh`…): a letter and up to 31 of `[A-Za-z0-9+.-]`. */\nconst SCHEME_MAX = 32;\nconst isLetter = (c: number): boolean => (c >= 97 && c <= 122) || (c >= 65 && c <= 90);\nconst isSchemeChar = (c: number): boolean => isLetter(c) || (c >= 48 && c <= 57) || c === 43 || c === 45 || c === 46;\n/** What ends a URL in text: `\\s` as regexes define it, and `\"` `'` `<` `>`. */\nconst endsUrl = (c: number): boolean =>\n c === 34 || c === 39 || c === 60 || c === 62 || (c >= 9 && c <= 13) || c === 32 || c === 0xa0 || c === 0x1680 ||\n (c >= 0x2000 && c <= 0x200a) || c === 0x2028 || c === 0x2029 || c === 0x202f || c === 0x205f || c === 0x3000 || c === 0xfeff;\n\n/**\n * Remove URLs from text that may travel to a client (a refusal reason, a build error):\n * transport errors carry the endpoint — and with a keyed RPC (QuickNode, Alchemy,\n * Infura…) the key sits in its path (scan 2026-10-23 F8). What `/[a-z][a-z0-9+.-]{0,31}:\\/\\/[^\\s\"'<>]+/gi`\n * would replace, found from each `://` instead of tried at every position: linear in the length of\n * `text` whatever an API or a client puts in it (\"a.a.a…\" made the regex this replaced quadratic;\n * scan 2026-10-27 modules, F34). A URL glued to a word loses its last 32 scheme characters and the rest.\n */\nexport function redactUrls(text: string): string {\n let out = \"\";\n let done = 0; // text[0, done) is in `out`\n for (let at = text.indexOf(\"://\"); at !== -1; at = text.indexOf(\"://\", at + 3)) {\n let start = at;\n while (start > done && at - start < SCHEME_MAX && isSchemeChar(text.charCodeAt(start - 1))) start--;\n while (start < at && !isLetter(text.charCodeAt(start))) start++;\n if (start === at) continue; // no scheme: not a URL (and its tail is not scanned, so \"://://…\" stays linear)\n let end = at + 3;\n while (end < text.length && !endsUrl(text.charCodeAt(end))) end++;\n if (end === at + 3) continue; // nothing after \"://\"\n out += `${text.slice(done, start)}<url>`;\n done = end;\n at = end - 3;\n }\n return done === 0 ? text : out + text.slice(done);\n}\n\n/**\n * The source asset a provider is asked to sell: the catalog's entry for `params.sourceToken`, or —\n * only when the engine sent `sourceDecimals` (an unlisted token the user added) — an unlisted asset\n * named by its contract / mint. Providers that can trade any token use this instead of the catalog\n * lookup; the others keep refusing what the catalog does not list.\n */\nexport function sourceAssetOf(params: Pick<QuoteParams, \"sourceChain\" | \"sourceToken\" | \"sourceDecimals\">): CcAsset | undefined {\n const decimals = params.sourceDecimals;\n return resolveAsset(params.sourceChain, params.sourceToken, decimals === undefined ? {} : {allowUnlisted: true, decimals});\n}\n\nconst B64 = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\n/** Standard or URL-safe base64 → bytes, without Buffer or atob (Hermes); null for anything else. */\nexport function base64Bytes(text: string): Uint8Array | null {\n const s = text.replace(/-/g, \"+\").replace(/_/g, \"/\").replace(/=+$/, \"\");\n if (!/^[A-Za-z0-9+/]*$/.test(s) || s.length % 4 === 1) return null;\n const out = new Uint8Array(Math.floor((s.length * 3) / 4));\n let bits = 0, acc = 0, n = 0;\n for (const ch of s) {\n acc = (acc << 6) | B64.indexOf(ch);\n bits += 6;\n if (bits >= 8) { bits -= 8; out[n++] = (acc >> bits) & 0xff; }\n }\n return out.subarray(0, n);\n}\n\n/** CRC16-XMODEM, the checksum of a user-friendly TON address. */\nfunction crc16(bytes: Uint8Array): number {\n let crc = 0;\n for (const b of bytes) {\n crc ^= b << 8;\n for (let i = 0; i < 8; i++) crc = crc & 0x8000 ? ((crc << 1) ^ 0x1021) & 0xffff : (crc << 1) & 0xffff;\n }\n return crc;\n}\n\n/**\n * A TON address in raw form (`<workchain>:<64 hex>`, lowercase) from a raw string, a user-friendly one (bounceable or\n * not, checksum verified), or an address object (`toRawString()` / `workChain` + `hash`); null for anything else. Two\n * spellings of one address compare equal in this form.\n */\nexport function tonRawAddress(address: unknown): string | null {\n if (address && typeof address === \"object\") {\n const o = address as {toRawString?: () => string; workChain?: unknown; hash?: unknown};\n if (typeof o.toRawString === \"function\") { try { return tonRawAddress(o.toRawString()); } catch { return null; } }\n if (typeof o.workChain === \"number\" && o.hash instanceof Uint8Array && o.hash.length === 32) {\n return `${o.workChain}:${Array.from(o.hash, (b) => b.toString(16).padStart(2, \"0\")).join(\"\")}`;\n }\n return null;\n }\n if (typeof address !== \"string\") return null;\n const raw = /^(-?[0-9]{1,3}):([0-9a-fA-F]{64})$/.exec(address.trim());\n if (raw) return `${Number(raw[1])}:${raw[2]!.toLowerCase()}`;\n if (!/^[A-Za-z0-9+/_-]{48}$/.test(address.trim())) return null;\n const bytes = base64Bytes(address.trim());\n if (!bytes || bytes.length !== 36 || crc16(bytes.subarray(0, 34)) !== ((bytes[34]! << 8) | bytes[35]!)) return null;\n const wc = bytes[1] === 0xff ? -1 : bytes[1]!;\n return `${wc}:${Array.from(bytes.subarray(2, 34), (b) => b.toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n"],"mappings":"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","names":[]}
@@ -36,8 +36,27 @@ function validQuote(q, params, provider) {
36
36
  return false;
37
37
  }
38
38
  }
39
+ function remotePinRefusal(provider, q, chain) {
40
+ if (provider.remote !== true) return null;
41
+ const tx = q.txRequest;
42
+ const plain = tx ? tx.data === void 0 && tx.serializedTx === void 0 : typeof q.depositAddress === "string" && q.depositAddress !== "";
43
+ if (plain && provider.depositAddressRoutes !== true) {
44
+ return `${provider.name}: the route pays ${tx?.to || q.depositAddress}, an address the backend named, and the app did not allow deposit-address routes for this provider (remoteProvider depositAddressRoutes)`;
45
+ }
46
+ if (!chain.startsWith("eip155:")) return null;
47
+ if (!tx || tx.serializedTx !== void 0) return null;
48
+ const pinned = (list, address) => (list ?? []).some((a) => typeof a === "string" && a.toLowerCase() === address.toLowerCase());
49
+ if (tx.approvalAddress && !pinned(provider.spenders?.(chain), tx.approvalAddress)) {
50
+ return `${provider.name}: the route approves ${tx.approvalAddress}, which the app did not pin as a spender on ${chain} (remoteProvider spenders)`;
51
+ }
52
+ if (tx.data !== void 0 && tx.to && !pinned(provider.callTargets?.(chain), tx.to)) {
53
+ return `${provider.name}: the route calls ${tx.to}, which the app did not pin as a call target on ${chain} (remoteProvider callTargets)`;
54
+ }
55
+ return null;
56
+ }
39
57
 
40
58
  export {
41
- validQuote
59
+ validQuote,
60
+ remotePinRefusal
42
61
  };
43
- //# sourceMappingURL=chunk-KIAQXIBG.js.map
62
+ //# sourceMappingURL=chunk-5FPDKWBG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/validate.ts"],"sourcesContent":["import type {CrossChainProvider, Quote, QuoteParams} from \"./types.js\";\n\n/** Shared quote validator used by both the aggregate and single-provider paths. */\nexport function validQuote(q: Quote, params: QuoteParams, provider: string): boolean {\n try {\n if (q.provider !== provider || q.sourceAmount !== params.amountIn) return false;\n if (typeof q.intentId !== \"string\" || !q.intentId || typeof q.depositAddress !== \"string\" || (q.depositMemo !== null && typeof q.depositMemo !== \"string\")) return false;\n if (![q.sourceAmount, q.destAmount, q.minDestAmount].every((value) => /^[0-9]+$/.test(value))) return false;\n const source = BigInt(q.sourceAmount);\n const dest = BigInt(q.destAmount);\n const min = BigInt(q.minDestAmount);\n if (source <= 0n || dest <= 0n || min <= 0n || min > dest) return false;\n if (params.minAmountOut !== undefined) {\n if (!/^[0-9]+$/.test(params.minAmountOut) || BigInt(params.minAmountOut) <= 0n || min < BigInt(params.minAmountOut)) return false;\n }\n if (params.slippageBps !== undefined) {\n if (!Number.isInteger(params.slippageBps) || params.slippageBps < 0 || params.slippageBps >= 10_000) return false;\n // Providers round the floor DOWN (1Click, LI.FI: floor(dest × (1 − s))).\n // A ceil here refused nearly every slippage-bounded quote by one base unit.\n const required = (dest * BigInt(10_000 - params.slippageBps)) / 10_000n;\n if (min < required) return false;\n }\n if (!Number.isFinite(q.etaSeconds) || q.etaSeconds < 0) return false;\n if (!Number.isFinite(q.reliability) || q.reliability < 0 || q.reliability > 1) return false;\n if (!Number.isFinite(q.feeUsd) || q.feeUsd < 0) return false;\n if (q.fees && (!Array.isArray(q.fees) || !q.fees.every((fee) =>\n [\"network\", \"protocol\", \"affiliate\", \"other\"].includes(fee.type) &&\n /^[0-9]+$/.test(fee.amount) &&\n typeof fee.token === \"string\" && fee.token.length > 0 &&\n typeof fee.included === \"boolean\" &&\n (fee.description === undefined || typeof fee.description === \"string\")\n ))) return false;\n if (q.providerData && (Array.isArray(q.providerData) || typeof q.providerData !== \"object\" || !Object.values(q.providerData).every((value) => typeof value === \"string\"))) return false;\n const tx = q.txRequest;\n if (tx) {\n if (tx.value !== undefined && !/^[0-9]+$/.test(tx.value)) return false;\n if (tx.gasLimit !== undefined && !/^[0-9]+$/.test(tx.gasLimit)) return false;\n if (tx.chainId !== undefined && (!Number.isSafeInteger(tx.chainId) || tx.chainId <= 0)) return false;\n if ([tx.to, tx.data, tx.approvalAddress, tx.serializedTx].some((value) => value !== undefined && typeof value !== \"string\")) return false;\n }\n // Execution is always re-quoted. An unknown/zero expiry cannot prove that\n // the transaction is still safe to sign, so contribution packages reject it.\n if (!Number.isFinite(q.validUntilMs) || q.validUntilMs <= Date.now()) return false;\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Why a remote provider's quote cannot execute in this app, or null: its EVM transaction calls or\n * approves an address the app did not pin (`remoteProvider` `callTargets` / `spenders`), or it pays\n * an address the backend named — a deposit address or a plain value send — on a stub the app did not\n * open to such routes (`depositAddressRoutes`, scan 2026-10-28 F9). The router and the\n * single-provider path drop such a quote with this reason, so the auction never picks a route\n * execution would refuse (scan 2026-10-27 modules, F8/F9/F16); execution checks the build again, in\n * full. Local providers are not judged here.\n */\nexport function remotePinRefusal(provider: Pick<CrossChainProvider, \"name\" | \"remote\" | \"callTargets\" | \"spenders\" | \"depositAddressRoutes\">, q: Quote, chain: string): string | null {\n if (provider.remote !== true) return null;\n const tx = q.txRequest;\n const plain = tx ? tx.data === undefined && tx.serializedTx === undefined : typeof q.depositAddress === \"string\" && q.depositAddress !== \"\";\n if (plain && provider.depositAddressRoutes !== true) {\n return `${provider.name}: the route pays ${tx?.to || q.depositAddress}, an address the backend named, and the app did not allow deposit-address routes for this provider (remoteProvider depositAddressRoutes)`;\n }\n if (!chain.startsWith(\"eip155:\")) return null;\n if (!tx || tx.serializedTx !== undefined) return null;\n const pinned = (list: readonly string[] | undefined, address: string) =>\n (list ?? []).some((a) => typeof a === \"string\" && a.toLowerCase() === address.toLowerCase());\n if (tx.approvalAddress && !pinned(provider.spenders?.(chain), tx.approvalAddress)) {\n return `${provider.name}: the route approves ${tx.approvalAddress}, which the app did not pin as a spender on ${chain} (remoteProvider spenders)`;\n }\n if (tx.data !== undefined && tx.to && !pinned(provider.callTargets?.(chain), tx.to)) {\n return `${provider.name}: the route calls ${tx.to}, which the app did not pin as a call target on ${chain} (remoteProvider callTargets)`;\n }\n return null;\n}\n"],"mappings":"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","names":[]}
@@ -1,14 +1,18 @@
1
1
  import {
2
2
  anySignal,
3
3
  timeoutSignal
4
- } from "./chunk-MNJTMSFN.js";
4
+ } from "./chunk-2KC734UX.js";
5
5
 
6
6
  // src/remote.ts
7
7
  function remoteProvider(opts) {
8
8
  const name = opts.name;
9
9
  if (!/^[A-Za-z0-9_-]+$/.test(name)) throw new Error("swap2-remote: invalid provider name");
10
- const baseUrl = opts.baseUrl.replace(/\/+$/, "");
10
+ const baseUrl = opts.baseUrl.trim().replace(/\/+$/, "");
11
11
  if (!baseUrl) throw new Error("swap2-remote: baseUrl is required");
12
+ const insecure = /^[a-z][a-z0-9+.-]*:/i.test(baseUrl) ? !/^https:\/\//i.test(baseUrl) : /^[\\/]{2}/.test(baseUrl);
13
+ if (insecure && opts.allowInsecureBaseUrl !== true) {
14
+ throw new Error("swap2-remote: baseUrl must be https:// or a same-origin path (allowInsecureBaseUrl for local development)");
15
+ }
12
16
  const supportsRoute = opts.supportsRoute;
13
17
  const enabled = opts.enabled;
14
18
  const headers = Object.freeze({ ...opts.headers });
@@ -17,6 +21,12 @@ function remoteProvider(opts) {
17
21
  if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 6e4) {
18
22
  throw new Error("swap2-remote: timeoutMs must be an integer between 1 and 60000");
19
23
  }
24
+ const requests = /* @__PURE__ */ new Map();
25
+ const remember = (intentId, params) => {
26
+ requests.delete(intentId);
27
+ requests.set(intentId, params);
28
+ if (requests.size > 64) requests.delete(requests.keys().next().value);
29
+ };
20
30
  async function call(action, body, signal) {
21
31
  const timeout = timeoutSignal(timeoutMs);
22
32
  const requestSignal = signal ? anySignal([signal, timeout]) : timeout;
@@ -37,6 +47,9 @@ function remoteProvider(opts) {
37
47
  }
38
48
  return {
39
49
  name,
50
+ remote: true,
51
+ ...opts.verifyBuild ? { verifiesBuilds: true } : {},
52
+ ...opts.depositAddressRoutes === true ? { depositAddressRoutes: true } : {},
40
53
  // The browser side has no dark-flag env; the backend enforces its own
41
54
  // flags — a disabled provider simply quotes null / rejects build.
42
55
  enabled: () => enabled ? enabled() : true,
@@ -48,10 +61,30 @@ function remoteProvider(opts) {
48
61
  if (out && typeof out === "object" && "noQuote" in out) {
49
62
  throw new Error(String(out.noQuote).slice(0, 300));
50
63
  }
64
+ if (out && typeof out === "object" && typeof out.intentId === "string") remember(out.intentId, params);
65
+ if (out && opts.verifyQuote) {
66
+ let refusal;
67
+ try {
68
+ refusal = opts.verifyQuote(params, out);
69
+ } catch (e) {
70
+ refusal = e instanceof Error ? e.message : String(e);
71
+ }
72
+ if (refusal) throw new Error(`${name}: the quote fails this app's own check \u2014 ${refusal.slice(0, 240)}`);
73
+ }
51
74
  return out;
52
75
  },
53
76
  async buildDeposit(quote) {
54
- return call("build", { quote });
77
+ const tx = await call("build", { quote });
78
+ if (opts.verifyBuild) {
79
+ let refusal;
80
+ try {
81
+ refusal = await opts.verifyBuild(quote, tx, requests.get(quote.intentId));
82
+ } catch (e) {
83
+ refusal = e instanceof Error ? e.message : String(e);
84
+ }
85
+ if (refusal) throw new Error(`${name}: the build fails this app's own check \u2014 ${refusal.slice(0, 240)}`);
86
+ }
87
+ return tx;
55
88
  },
56
89
  async preflightDeposit(quote, tx) {
57
90
  await call("preflight", { quote, tx });
@@ -73,11 +106,13 @@ function remoteProvider(opts) {
73
106
  ...opts.supportsChain ? { supportsChain: opts.supportsChain.bind(opts) } : {},
74
107
  ...opts.supportsChainPair ? { supportsChainPair: opts.supportsChainPair.bind(opts) } : {},
75
108
  ...opts.supportsSameChain !== void 0 ? { supportsSameChain: opts.supportsSameChain } : {},
76
- ...opts.claimTargets ? { claimTargets: opts.claimTargets } : {}
109
+ ...opts.claimTargets ? { claimTargets: opts.claimTargets } : {},
110
+ ...opts.callTargets ? { callTargets: opts.callTargets } : {},
111
+ ...opts.spenders ? { spenders: opts.spenders } : {}
77
112
  };
78
113
  }
79
114
 
80
115
  export {
81
116
  remoteProvider
82
117
  };
83
- //# sourceMappingURL=chunk-ZMWK3RQ4.js.map
118
+ //# sourceMappingURL=chunk-6MTZNYKM.js.map
@@ -0,0 +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":[]}
@@ -10,6 +10,9 @@ import {
10
10
  Swap2UnsupportedOperationError,
11
11
  Swap2ValidationError
12
12
  } from "./chunk-IVR3SREQ.js";
13
+ import {
14
+ tonRawAddress
15
+ } from "./chunk-2KC734UX.js";
13
16
 
14
17
  // src/swap2/base.ts
15
18
  import { Swap2Protocol } from "@anywalletkit/core";
@@ -130,7 +133,64 @@ function normalizeTypedDomain(domain) {
130
133
  else if (typeof raw === "string" && /^0x[0-9a-fA-F]+$/.test(raw)) chainId = safeChainId(BigInt(raw));
131
134
  return chainId === void 0 || !Number.isSafeInteger(chainId) ? domain : { ...domain, chainId };
132
135
  }
136
+ function assertTypedOrder(typedData, approvalAddress, e) {
137
+ const fail = (why) => new Swap2ValidationError(`swap2: typed order refused \u2014 ${why}`);
138
+ let parsed;
139
+ try {
140
+ parsed = JSON.parse(typedData);
141
+ } catch {
142
+ throw fail("the typed data is not JSON");
143
+ }
144
+ if (!parsed || typeof parsed !== "object") throw fail("the typed data is not an object");
145
+ const td = parsed;
146
+ const primary = td.primaryType;
147
+ if (typeof primary !== "string" || !primary) throw fail("it names no primaryType");
148
+ if (/^permit/i.test(primary)) throw fail(`${primary} is a token approval, not an order`);
149
+ if (primary !== "Order") throw fail(`primaryType ${primary} is not an order this engine signs`);
150
+ const fields = td.types?.Order;
151
+ const has = (name, type) => Array.isArray(fields) && fields.some((f) => f?.name === name && f?.type === type);
152
+ if (!has("maker", "address") || !has("makerAsset", "address") || !has("makingAmount", "uint256")) throw fail("the Order type has no maker / makerAsset / makingAmount");
153
+ const verifying = td.domain?.verifyingContract;
154
+ if (!approvalAddress || typeof verifying !== "string" || verifying.toLowerCase() !== approvalAddress.toLowerCase()) {
155
+ throw fail(`verifyingContract ${String(verifying)} is not the settlement contract the swap approves`);
156
+ }
157
+ if (e.chainId !== null && normalizeTypedDomain(td.domain)?.chainId !== e.chainId) throw fail("the domain chainId is not the source chain");
158
+ const m = td.message ?? {};
159
+ if (String(m.maker ?? "").toLowerCase() !== e.self.toLowerCase()) throw fail("the maker is not this account");
160
+ if (!e.token || String(m.makerAsset ?? "").toLowerCase() !== e.token.toLowerCase()) throw fail("the makerAsset is not the source token");
161
+ const raw = m.makingAmount;
162
+ const making = typeof raw === "string" && /^(?:0x[0-9a-fA-F]{1,64}|[0-9]{1,78})$/.test(raw) ? BigInt(raw) : typeof raw === "number" && Number.isSafeInteger(raw) ? BigInt(raw) : null;
163
+ if (making === null || making !== BigInt(e.amount)) throw fail(`makingAmount ${String(raw)} is not the swap's ${e.amount}`);
164
+ }
133
165
  var DEFAULT_SERIALIZED_NATIVE_ALLOWANCE = 20000000n;
166
+ var DEFAULT_TON_GAS_CEILING = 1000000000n;
167
+ var JETTON_TRANSFER_OP = 260734629;
168
+ function readTonBody(cell) {
169
+ let s;
170
+ try {
171
+ s = cell.beginParse();
172
+ } catch {
173
+ return { op: null };
174
+ }
175
+ let op;
176
+ try {
177
+ op = s.loadUint(32);
178
+ } catch {
179
+ return { op: null };
180
+ }
181
+ if (op !== JETTON_TRANSFER_OP) return { op };
182
+ try {
183
+ s.loadUintBig(64);
184
+ const amount = s.loadCoins();
185
+ const destination = tonRawAddress(s.loadAddress());
186
+ const response = s.loadMaybeAddress();
187
+ s.loadMaybeRef();
188
+ const forwardTon = s.loadCoins();
189
+ return { op, jettonTransfer: { amount, destination, responseDestination: response == null ? null : tonRawAddress(response), forwardTon } };
190
+ } catch {
191
+ return { op };
192
+ }
193
+ }
134
194
  function reviewedIdentity(plan) {
135
195
  const { feeUsd: _fee, etaSeconds: _eta, validUntilMs: _valid, approvals: _approvals, destAmount: _dest, requoted: _requoted, ...pinned } = plan;
136
196
  return JSON.stringify(pinned, (_k, v) => typeof v === "bigint" ? v.toString() : v);
@@ -590,12 +650,15 @@ var BaseSwap2Protocol = class extends Swap2Protocol {
590
650
  }
591
651
  }
592
652
  const executionModels = [unsigned.data !== void 0, unsigned.bodyBoc !== void 0, unsigned.typedData !== void 0].filter(Boolean).length;
593
- if (executionModels > 1 || unsigned.memo !== void 0 && (unsigned.bodyBoc !== void 0 || unsigned.typedData !== void 0)) {
653
+ if (executionModels > 1 || unsigned.memo !== void 0 && (unsigned.bodyBoc !== void 0 || unsigned.typedData !== void 0) || unsigned.orderExtension !== void 0 && unsigned.typedData === void 0) {
594
654
  throw new Error("swap2: provider returned ambiguous execution payloads");
595
655
  }
596
656
  if (unsigned.approvalAddress && !unsigned.data && !unsigned.typedData) {
597
657
  throw new Error("swap2: approval is only valid for calldata or typed-order execution");
598
658
  }
659
+ if (provider.remote === true && provider.depositAddressRoutes !== true && unsigned.data === void 0 && unsigned.bodyBoc === void 0 && unsigned.typedData === void 0 && !serialized) {
660
+ throw new Swap2ValidationError(`swap2: remote provider '${q.provider}' sends the input to ${unsigned.to}, an address its backend named, and the app did not allow deposit-address routes for it (remoteProvider depositAddressRoutes) \u2014 refused`);
661
+ }
599
662
  const tronSource = this.fromChain.startsWith("tron:");
600
663
  const nearSource = this.fromChain.startsWith("near:");
601
664
  if (unsigned.memo && !this.fromChain.startsWith("ton:") && !tronSource && !nearSource) {
@@ -623,10 +686,12 @@ var BaseSwap2Protocol = class extends Swap2Protocol {
623
686
  throw new Error(`swap2: provider transaction chain ${unsigned.chainId} does not match ${this.fromChain}`);
624
687
  }
625
688
  let tokenId;
689
+ let engineTransfer = false;
626
690
  if (!unsigned.data && !unsigned.bodyBoc && !unsigned.typedData && !serialized) {
627
691
  const src = this.sourceAsset(executionOptions);
628
692
  if (src?.contract) {
629
693
  if (this.fromChain.startsWith("eip155:")) {
694
+ engineTransfer = true;
630
695
  unsigned = {
631
696
  ...unsigned,
632
697
  to: src.contract,
@@ -665,12 +730,69 @@ var BaseSwap2Protocol = class extends Swap2Protocol {
665
730
  } else if (!unsigned.typedData && !unsigned.bodyBoc && executionValue !== BigInt(q.sourceAmount) + declaredNativeFee(q, this.fromChain)) {
666
731
  throw new Error("swap2: provider execution amount does not match the validated quote");
667
732
  }
733
+ let tonBody;
734
+ if (cellBody !== void 0) {
735
+ const refuse = (why) => {
736
+ throw new Swap2ValidationError(`swap2: TON cell-body build refused \u2014 ${why}`);
737
+ };
738
+ const raw = this._config.tonGasCeiling;
739
+ const ceiling = raw === void 0 ? DEFAULT_TON_GAS_CEILING : BigInt(raw);
740
+ if (ceiling < 0n) throw new Swap2ConfigurationError("swap2: tonGasCeiling must be non-negative");
741
+ if (provider.remote === true && provider.verifiesBuilds !== true) refuse("a remote provider's cell body needs the app's own validator (remoteProvider verifyBuild)");
742
+ tonBody = readTonBody(cellBody);
743
+ const input = BigInt(q.sourceAmount);
744
+ if (source.contract) {
745
+ if (executionValue > ceiling) refuse(`it attaches ${executionValue} nanoTON to a jetton transfer, more than the ${ceiling} gas ceiling`);
746
+ const t = tonBody.jettonTransfer;
747
+ if (!t) refuse("a jetton-source body must be a jetton transfer");
748
+ if (t.amount !== input) refuse(`the jetton transfer moves ${t.amount}, not the swap's ${input}`);
749
+ if (!t.responseDestination || t.responseDestination !== tonRawAddress(selfAddress)) refuse("the jetton transfer returns its excess to another wallet");
750
+ if (t.forwardTon >= executionValue) refuse("the jetton transfer forwards more TON than the message carries");
751
+ const own = typeof signer.getJettonWalletAddress === "function" ? tonRawAddress(await signer.getJettonWalletAddress(source.contract)) : null;
752
+ if (!own) refuse("the account cannot name its own jetton wallet (getJettonWalletAddress), so the target cannot be checked");
753
+ if (tonRawAddress(unsigned.to) !== own) refuse(`it is sent to ${unsigned.to}, not this account's ${source.symbol ?? "source"} jetton wallet`);
754
+ } else {
755
+ if (executionValue < input || executionValue - input > ceiling) refuse(`it attaches ${executionValue} nanoTON for a ${input} input, more than the ${ceiling} gas ceiling on top`);
756
+ const pinned = provider.remote === true ? [...provider.callTargets?.(this.fromChain) ?? [], ...q.depositAddress ? [q.depositAddress] : []].map(tonRawAddress).filter((a) => !!a) : [];
757
+ if (pinned.length && !pinned.includes(tonRawAddress(unsigned.to) ?? "")) refuse(`remote provider '${q.provider}' sends it to ${unsigned.to}, which the app did not pin (remoteProvider callTargets)`);
758
+ }
759
+ }
760
+ const remote = provider.remote === true;
761
+ const pins = (list) => (list ?? []).filter((a) => typeof a === "string" && /^0x[0-9a-fA-F]{40}$/.test(a)).map((a) => a.toLowerCase());
762
+ const pinnedTargets = remote ? pins(provider.callTargets?.(this.fromChain)) : [];
763
+ const pinnedSpenders = remote ? pins(provider.spenders?.(this.fromChain)) : [];
764
+ if (remote && unsigned.approvalAddress && !pinnedSpenders.includes(unsigned.approvalAddress.toLowerCase())) {
765
+ throw new Swap2ValidationError(`swap2: remote provider '${q.provider}' asks to approve ${unsigned.approvalAddress} on ${this.fromChain}, which the app did not pin as a spender (remoteProvider spenders) \u2014 refused`);
766
+ }
767
+ if (unsigned.typedData !== void 0) {
768
+ if (remote && provider.verifiesBuilds !== true) {
769
+ throw new Swap2ValidationError(`swap2: remote provider '${q.provider}' asks to sign a typed order, which needs the app's own validator (remoteProvider verifyBuild) \u2014 refused`);
770
+ }
771
+ const chainId = this.fromChain.startsWith("eip155:") ? Number(this.fromChain.slice("eip155:".length)) : null;
772
+ assertTypedOrder(unsigned.typedData, unsigned.approvalAddress, { self: selfAddress, token: source.contract ?? void 0, amount: q.sourceAmount, chainId });
773
+ }
668
774
  if (unsigned.data !== void 0 && this.fromChain.startsWith("eip155:")) {
669
775
  const call = decodeCall({ to: unsigned.to, data: unsigned.data, value: executionValue });
670
- const named = call.transfer?.to ?? call.approval?.spender;
671
- const allowed = [selfAddress, q.depositAddress, unsigned.approvalAddress, unsigned.to].filter((a) => !!a).map((a) => a.toLowerCase());
672
- if (named && !allowed.includes(named.toLowerCase())) {
673
- throw new Swap2ValidationError(`swap2: quote calldata (${call.method}) names ${getAddress(named)}, which is not this account, the deposit address or the declared spender \u2014 refused`);
776
+ if (call.undecodable) throw new Swap2ValidationError("swap2: quote calldata is not hex bytes \u2014 refused");
777
+ if (call.approval && call.approval.kind !== "erc20") {
778
+ throw new Swap2ValidationError(`swap2: quote calldata is ${call.approval.kind === "operator" ? "an operator approval (setApprovalForAll)" : "a Permit2 approval"} \u2014 never a swap's main call; refused`);
779
+ }
780
+ if (call.transfer || call.approval) {
781
+ if (!source.contract || unsigned.to.toLowerCase() !== source.contract.toLowerCase()) {
782
+ throw new Swap2ValidationError(`swap2: quote calldata (${call.method}) moves ${getAddress(unsigned.to)}, not the source token \u2014 refused`);
783
+ }
784
+ if (call.method === "transferFrom") throw new Swap2ValidationError("swap2: quote calldata pulls the source token with transferFrom \u2014 refused");
785
+ const amount = call.transfer?.amount ?? call.approval?.amount ?? null;
786
+ if (amount === null || amount > BigInt(q.sourceAmount)) {
787
+ throw new Swap2ValidationError(`swap2: quote calldata (${call.method}) covers ${amount === null ? "an unlimited amount" : amount} of the source token, more than the swap's ${q.sourceAmount} \u2014 refused`);
788
+ }
789
+ const named = (call.transfer?.to ?? call.approval.spender).toLowerCase();
790
+ const allowed = engineTransfer ? [q.depositAddress] : remote ? [selfAddress, ...pinnedTargets, ...pinnedSpenders] : [selfAddress, q.depositAddress, unsigned.approvalAddress];
791
+ if (!allowed.filter((a) => !!a).map((a) => a.toLowerCase()).includes(named)) {
792
+ throw new Swap2ValidationError(`swap2: quote calldata (${call.method}) names ${getAddress(named)}, which is not this account, the deposit address or ${remote ? "a spender the app pinned" : "the declared spender"} \u2014 refused`);
793
+ }
794
+ } else if (remote && !pinnedTargets.includes(unsigned.to.toLowerCase())) {
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`);
674
796
  }
675
797
  }
676
798
  const needsApproval = !!unsigned.approvalAddress && !!source.contract;
@@ -732,7 +854,7 @@ var BaseSwap2Protocol = class extends Swap2Protocol {
732
854
  ...unsigned.chainId !== void 0 ? { chainId: unsigned.chainId } : {},
733
855
  ...unsigned.gasLimit ? { gasLimit: BigInt(unsigned.gasLimit) } : {},
734
856
  ...unsigned.memo ? { memo: unsigned.memo, ...unsigned.memoEncoding ? { memoEncoding: unsigned.memoEncoding } : {} } : {},
735
- ...cellBody !== void 0 ? { cellBody: true } : {}
857
+ ...cellBody !== void 0 ? { cellBody: true, ...tonBody ? { cellBodyDecoded: tonBody } : {} } : {}
736
858
  }
737
859
  };
738
860
  const previous = carried?.reviewed;
@@ -997,6 +1119,7 @@ var BaseSwap2Protocol = class extends Swap2Protocol {
997
1119
  throw new Swap2ValidationError(`swap2: claim target ${unsigned.to} is not a contract '${name}' declares for ${unsigned.chain} \u2014 refused`);
998
1120
  }
999
1121
  const claimCall = decodeCall({ to: unsigned.to, data: unsigned.data, value: 0n });
1122
+ if (claimCall.undecodable) throw new Swap2ValidationError("swap2: claim calldata is not hex bytes \u2014 refused");
1000
1123
  if (claimCall.transfer || claimCall.approval) {
1001
1124
  throw new Swap2ValidationError(`swap2: claim calldata (${claimCall.method}) moves tokens or grants an allowance \u2014 refused`);
1002
1125
  }
@@ -1113,4 +1236,4 @@ export {
1113
1236
  swap2ChainId,
1114
1237
  BaseSwap2Protocol
1115
1238
  };
1116
- //# sourceMappingURL=chunk-IGTMUTNJ.js.map
1239
+ //# sourceMappingURL=chunk-CRCDWV3L.js.map