@anywalletkit/protocol-swap2-omniston 0.2.7 → 0.3.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
@@ -34,6 +34,12 @@ Every route is fail-closed until its allowlists and validators are configured:
34
34
  wallets, pTON wallet ownership and DeDust pool pairs on chain.
35
35
  `tonSwapPayloadCheck: "report"` traces `ton-swap-payload <reason>` instead
36
36
  of refusing (default `"enforce"`).
37
+ Behind `remoteProvider`, the app checks the built swap itself:
38
+ `remoteProvider({name: "omniston", baseUrl, verifyBuild: omnistonVerifyBuild({validateTonSwapTarget,
39
+ validateTonSwapLeg})})` opens the body with `checkTonSwapPayload` against the app's own request
40
+ (receiver, refund, on-chain minimum, input; the quote carries `routeSteps`) and asks the app's
41
+ validators about the DEX it pays. Without `verifyBuild` the engine refuses remote cell bodies; a
42
+ TON → EVM escrow body cannot be checked on the client and is refused by this helper.
37
43
  - **TON → EVM** (HTLC order, the wallet pays a jetton escrow): factory
38
44
  allowlisted on both chains, `validateTonJettonWallet`,
39
45
  `validateTonEscrowPayload`, `validateEvmOutputPosition`.
@@ -48,6 +54,13 @@ Every route is fail-closed until its allowlists and validators are configured:
48
54
  `validateEvmInputPosition` (finalized `SrcEscrowCreated` + escrow state)
49
55
  AND `validateTonOutputPosition` (factory-deployed TON position with bound
50
56
  state, inside its completion window) both pass.
57
+ Behind `remoteProvider` the order is built on your server, so the app pins
58
+ the order protocols itself: `remoteProvider({name: "omniston", baseUrl,
59
+ ...omnistonPins(trustedEvmOrderProtocols)})` — the same audited table. The
60
+ engine then signs only an `Order` whose verifying contract is a pinned
61
+ spender, whose maker is this account and that sells exactly the swap's input
62
+ of the source token; a `Permit` or any other typed message is refused.
63
+ `validateEvmTypedOrder` is exported for apps that hold the settlement data.
51
64
  - **EVM → EVM** (gasless order, same order protocol as EVM → TON): the signed
52
65
  extension's ExtraDataArgs name the destination chain, token and deposit and
53
66
  are checked against the quote. The EVM recipient is NOT in anything the
@@ -74,7 +87,11 @@ escrow, not that transaction), `AUDITED_OMNISTON_SETTLEMENT_CONTRACTS`,
74
87
  own endpoints as `rpcUrls` (CAIP-2 → URLs, tried before the public fallbacks)
75
88
  and `tonApiUrl` / `tonApiKey`. On serverless also inject durable
76
89
  `secretStore` / `quoteStore` — the HTLC secret must survive to the poll that
77
- discloses it.
90
+ discloses it. Give the quote store a `claim` (an atomic create-if-absent: Redis
91
+ `SET NX`, a conditional KV put): the platform picks quote ids, and a context is
92
+ never rebound to another user's quote under the same id. A secret is disclosed
93
+ only against the exact order it was locked to (its stored binding), so even a
94
+ rebound context gets nothing.
78
95
 
79
96
  The destination-escrow proof requires the escrow immutables to be found in the
80
97
  resolver's calldata AND to derive, through the trusted factory's
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  omnistonProvider
3
- } from "./chunk-PKDRSR7N.js";
3
+ } from "./chunk-JVFDD7PV.js";
4
4
  import {
5
5
  omniston
6
6
  } from "./chunk-QDXFC6BC.js";
@@ -24,4 +24,4 @@ var OmnistonSwap2Protocol = class extends SingleProviderSwap2 {
24
24
  export {
25
25
  OmnistonSwap2Protocol
26
26
  };
27
- //# sourceMappingURL=chunk-IIFNHGC5.js.map
27
+ //# sourceMappingURL=chunk-CRYYP6X3.js.map
@@ -342,7 +342,12 @@ function inMemoryQuoteStore() {
342
342
  return {
343
343
  save: (id, v) => void map.set(id, v),
344
344
  load: (id) => map.get(id),
345
- clear: (id) => void map.delete(id)
345
+ clear: (id) => void map.delete(id),
346
+ claim: (id, v) => {
347
+ if (map.has(id)) return false;
348
+ map.set(id, v);
349
+ return true;
350
+ }
346
351
  };
347
352
  }
348
353
  function inMemorySecretStore() {
@@ -916,6 +921,13 @@ function validSwapSettlement(q) {
916
921
  const v = q.settlementData.value;
917
922
  return Array.isArray(v.routes) && v.routes.length > 0 && v.routes.every((route) => Array.isArray(route?.steps) && route.steps.length > 0 && route.steps.every((step) => Array.isArray(step?.chunks) && step.chunks.length > 0 && step.chunks.every((chunk) => typeof chunk?.protocol === "string" && chunk.protocol.length > 0 && isPositiveInteger(chunk.inputUnits) && isPositiveInteger(chunk.outputUnits)))) && isPositiveInteger(v.minOutputAmount) && BigInt(v.minOutputAmount) <= BigInt(q.outputUnits) && (v.recommendedMinOutputAmount === void 0 || isPositiveInteger(v.recommendedMinOutputAmount) && BigInt(v.recommendedMinOutputAmount) <= BigInt(q.outputUnits)) && (q.gasBudget === void 0 || isPositiveInteger(q.gasBudget));
918
923
  }
924
+ function swapSteps(v) {
925
+ if (v.routes.length !== 1) return [{ protocol: `${v.routes.length}-routes`, inputJetton: void 0, outputJetton: void 0 }];
926
+ return v.routes[0].steps.map((step) => {
927
+ if (step.chunks.length !== 1) return { protocol: `split(${step.chunks.map((c) => c.protocol).join("|")})`, inputJetton: void 0, outputJetton: void 0 };
928
+ return { protocol: step.chunks[0].protocol, inputJetton: stepJetton(step.inputAsset), outputJetton: stepJetton(step.outputAsset) };
929
+ });
930
+ }
919
931
  function swapProtocols(v) {
920
932
  const names = /* @__PURE__ */ new Set();
921
933
  for (const route of v.routes) for (const step of route.steps) for (const chunk of step.chunks) names.add(chunk.protocol);
@@ -1092,6 +1104,66 @@ function mapOrderStatus(order) {
1092
1104
  return { state: "pending", depositReceived };
1093
1105
  }
1094
1106
  }
1107
+ function omnistonPins(trustedEvmOrderProtocols) {
1108
+ const byChain = new Map(Object.entries(trustedEvmOrderProtocols).map(([chain, list]) => [chain, list.map((a) => getAddress(a))]));
1109
+ return Object.freeze({ callTargets: () => [], spenders: (chain) => byChain.get(chain) ?? [] });
1110
+ }
1111
+ function omnistonVerifyBuild(hooks) {
1112
+ return async (quote, tx, params) => {
1113
+ if (tx.bodyBoc === void 0) return null;
1114
+ if (!params) return "the request this quote answered is unknown";
1115
+ if (quote.providerData?.settlement !== "swap") return "only TON swap bodies can be checked on the client";
1116
+ let steps;
1117
+ try {
1118
+ steps = JSON.parse(quote.providerData.routeSteps ?? "");
1119
+ } catch {
1120
+ return "the quote carries no route steps";
1121
+ }
1122
+ if (!Array.isArray(steps) || steps.length === 0) return "the quote carries no route steps";
1123
+ const source = ccAsset(params.sourceChain, params.sourceToken);
1124
+ const dest = ccAssetByToken(params.destChain, params.destToken);
1125
+ if (!source || !dest) return "unknown source or destination token";
1126
+ const protocols = (quote.providerData.protocols ?? "").split(",").filter(Boolean);
1127
+ let checked;
1128
+ try {
1129
+ checked = checkTonSwapPayload({
1130
+ payloadHex: Buffer2.from(tx.bodyBoc, "base64").toString("hex"),
1131
+ targetAddress: tx.to,
1132
+ sendAmount: BigInt(tx.value),
1133
+ trader: params.refundAddress ?? "",
1134
+ dest: params.destAddress,
1135
+ inputUnits: quote.sourceAmount,
1136
+ minOutputAmount: quote.minDestAmount,
1137
+ inputJetton: source.contract ?? null,
1138
+ outputJetton: dest.contract ?? null,
1139
+ steps: steps.map((st) => ({ protocol: String(st.protocol), inputJetton: st.inputJetton, outputJetton: st.outputJetton })),
1140
+ integratorAddress: hooks.integratorAddress ?? null
1141
+ });
1142
+ } catch (e) {
1143
+ return e instanceof Error ? e.message.replace(/^omniston: /, "") : String(e);
1144
+ }
1145
+ const payload = parseSwapPayload(Buffer2.from(tx.bodyBoc, "base64").toString("hex"));
1146
+ const target = source.contract ? payload.jettonTransfer?.destination : tx.to;
1147
+ if (!target) return "the body names no swap destination";
1148
+ try {
1149
+ if (!await hooks.validateTonSwapTarget({ address: target, protocols })) return `${target} is not a recognized DEX contract for ${protocols.join("+")}`;
1150
+ } catch {
1151
+ return "the swap target could not be checked";
1152
+ }
1153
+ if (hooks.validateTonSwapLeg) {
1154
+ for (const leg of checked.legs) {
1155
+ let ok = false;
1156
+ try {
1157
+ ok = await hooks.validateTonSwapLeg(leg);
1158
+ } catch {
1159
+ ok = false;
1160
+ }
1161
+ if (!ok) return `${leg.kind} v${leg.version} leg not verified`;
1162
+ }
1163
+ }
1164
+ return null;
1165
+ };
1166
+ }
1095
1167
  function omnistonProvider(opts = {}) {
1096
1168
  opts = Object.freeze({ ...opts });
1097
1169
  const apiUrl = opts.apiUrl ?? "wss://omni-ws.ston.fi";
@@ -1209,6 +1281,26 @@ function omnistonProvider(opts = {}) {
1209
1281
  async function saveBuilt(quoteId, entry) {
1210
1282
  await quotes.save(quoteId, JSON.stringify(entry));
1211
1283
  }
1284
+ async function claimBuilt(quoteId, entry) {
1285
+ const json = JSON.stringify(entry);
1286
+ if (typeof quotes.claim === "function") {
1287
+ if (await quotes.claim(quoteId, json)) return true;
1288
+ } else if (!await quotes.load(quoteId)) {
1289
+ await quotes.save(quoteId, json);
1290
+ return true;
1291
+ }
1292
+ const existing = await loadBuilt(quoteId);
1293
+ if (!existing) {
1294
+ if (typeof quotes.claim === "function") return await quotes.claim(quoteId, json);
1295
+ await quotes.save(quoteId, json);
1296
+ return true;
1297
+ }
1298
+ const a = existing.bound, b = entry.bound;
1299
+ const same = !!a && !!b && a.kind === b.kind && a.srcChain === b.srcChain && sameIntentAddress(a.trader, b.trader) && sameIntentAddress(a.dest, b.dest);
1300
+ if (!same) return false;
1301
+ await quotes.save(quoteId, json);
1302
+ return true;
1303
+ }
1212
1304
  async function loadBuilt(quoteId) {
1213
1305
  const raw = await quotes.load(quoteId);
1214
1306
  if (!raw) return void 0;
@@ -1375,7 +1467,10 @@ function omnistonProvider(opts = {}) {
1375
1467
  if (BigInt(swap.minOutputAmount) < requiredMin(q.outputUnits)) return null;
1376
1468
  if (q.gasBudget !== void 0 && BigInt(q.gasBudget) > BigInt(maxTonExecutionNano)) return null;
1377
1469
  const minOut = swap.minOutputAmount;
1378
- await saveBuilt(q.quoteId, { quote: q, at: Date.now(), bound: { trader, dest: params.destAddress, srcChain: params.sourceChain, kind: "swap" } });
1470
+ if (!await claimBuilt(q.quoteId, { quote: q, at: Date.now(), bound: { trader, dest: params.destAddress, srcChain: params.sourceChain, kind: "swap" } })) {
1471
+ traceFor(q.quoteId)("quote-id-reused", { kind: "swap" });
1472
+ return null;
1473
+ }
1379
1474
  const feeUnits = Number(q.protocolFeeUnits || "0") + Number(q.integratorFeeUnits || "0");
1380
1475
  const stableOut = /^(USDT|USDC|USD₮|jUSDT)$/.test(dest.symbol);
1381
1476
  const feeUsd = stableOut ? feeUnits / 10 ** dest.decimals : 0;
@@ -1402,6 +1497,8 @@ function omnistonProvider(opts = {}) {
1402
1497
  providerData: {
1403
1498
  settlement: "swap",
1404
1499
  protocols: swapProtocols(swap).join(","),
1500
+ // For the app's own check of a remote build (omnistonVerifyBuild): the route the body must follow.
1501
+ routeSteps: JSON.stringify(swapSteps(swap)),
1405
1502
  ...q.gasBudget ? { gasBudget: q.gasBudget } : {},
1406
1503
  ...q.estimatedGasConsumption ? { gasEstimate: q.estimatedGasConsumption } : {}
1407
1504
  }
@@ -1502,10 +1599,7 @@ function omnistonProvider(opts = {}) {
1502
1599
  minOutputAmount: swap.minOutputAmount,
1503
1600
  inputJetton,
1504
1601
  outputJetton: assetContract(q.outputAsset),
1505
- steps: swap.routes.length === 1 ? routeSteps.map((step) => {
1506
- if (step.chunks.length !== 1) return { protocol: `split(${step.chunks.map((c) => c.protocol).join("|")})`, inputJetton: void 0, outputJetton: void 0 };
1507
- return { protocol: step.chunks[0].protocol, inputJetton: stepJetton(step.inputAsset), outputJetton: stepJetton(step.outputAsset) };
1508
- }) : [{ protocol: `${swap.routes.length}-routes`, inputJetton: void 0, outputJetton: void 0 }],
1602
+ steps: swapSteps(swap),
1509
1603
  integratorAddress: q.integratorAddress?.chain.$case === "ton" ? q.integratorAddress.chain.value : null
1510
1604
  });
1511
1605
  } catch (error) {
@@ -1793,7 +1887,9 @@ function omnistonProvider(opts = {}) {
1793
1887
  const outputScaled = BigInt(q.outputUnits) * 10n ** BigInt(source.decimals);
1794
1888
  if (outputScaled * 2n < inputScaled || outputScaled > inputScaled * 2n) return refuse2("quoted output is outside 0.5x\u20132x of the input");
1795
1889
  if (await quoteIdInUse(q.quoteId)) return refuse2("the platform reused a quote id that already belongs to an order");
1796
- await saveBuilt(q.quoteId, { quote: q, at: Date.now(), bound: { trader, dest: params.destAddress, srcChain: params.sourceChain, kind: "order" } });
1890
+ if (!await claimBuilt(q.quoteId, { quote: q, at: Date.now(), bound: { trader, dest: params.destAddress, srcChain: params.sourceChain, kind: "order" } })) {
1891
+ return refuse2("the platform reused a quote id that already holds another user's quote");
1892
+ }
1797
1893
  const feeUnits = Number(q.protocolFeeUnits || "0") + Number(q.integratorFeeUnits || "0");
1798
1894
  const feeUsd = feeUnits / 10 ** dest.decimals;
1799
1895
  return {
@@ -1925,6 +2021,7 @@ function omnistonProvider(opts = {}) {
1925
2021
  await forget();
1926
2022
  throw error;
1927
2023
  }
2024
+ await saveBuilt(ref.quoteId, { quote: q, at: Date.now(), ...cached.bound ? { bound: cached.bound } : {} });
1928
2025
  return {
1929
2026
  chain: srcChain,
1930
2027
  to: message.targetAddress,
@@ -1968,8 +2065,15 @@ function omnistonProvider(opts = {}) {
1968
2065
  })
1969
2066
  );
1970
2067
  if (!order) return { state: "pending" };
1971
- const secretsHex = await store.load(decoded.quoteId);
2068
+ let secretsHex = await store.load(decoded.quoteId);
1972
2069
  if (!secretsHex || cached?.quote.settlementData.$case !== "order") trace("disclosure-skipped", { gate: "context", secrets: !!secretsHex, cached: !!cached, feedStatus: order.status });
2070
+ if (secretsHex && cached?.quote.settlementData.$case === "order") {
2071
+ const [lockedTo] = await store.load(bindKey(decoded.quoteId)) ?? [];
2072
+ if (lockedTo !== orderBinding(bound, cached.quote)) {
2073
+ trace("disclosure-skipped", { gate: "binding", bound: lockedTo !== void 0, feedStatus: order.status });
2074
+ secretsHex = void 0;
2075
+ }
2076
+ }
1973
2077
  if (secretsHex && cached?.quote.settlementData.$case === "order") {
1974
2078
  const q = cached.quote;
1975
2079
  const settlement = q.settlementData.value;
@@ -2144,6 +2248,9 @@ export {
2144
2248
  inMemorySecretStore,
2145
2249
  encodeTypedMessage,
2146
2250
  validateEvmToTonOrderExtension,
2251
+ validateEvmTypedOrder,
2252
+ omnistonPins,
2253
+ omnistonVerifyBuild,
2147
2254
  omnistonProvider
2148
2255
  };
2149
- //# sourceMappingURL=chunk-PKDRSR7N.js.map
2256
+ //# sourceMappingURL=chunk-JVFDD7PV.js.map