@anywalletkit/protocol-swap2-omniston 0.2.8 → 0.3.1

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,14 @@ 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 body itself:
38
+ `remoteProvider({name: "omniston", baseUrl, verifyBuild: omnistonVerifyBuild({...createOmnistonTonValidators()})})`.
39
+ A TON → TON swap body is opened with `checkTonSwapPayload` against the app's own request (receiver,
40
+ refund, on-chain minimum, input; the quote carries `routeSteps`) and the DEX it pays is confirmed with
41
+ the app's validators. A TON → EVM escrow body goes to `verifyTonEscrowBuild`: the jetton amount, the
42
+ trader as owner, the destination chain, address and token and the quote's minimum delivery come from
43
+ the app, the protocol contract from `trustedProtocolAddresses`, and the escrow code, authority key and
44
+ timelocks are the audited ones. Without `verifyBuild` the engine refuses remote cell bodies.
37
45
  - **TON → EVM** (HTLC order, the wallet pays a jetton escrow): factory
38
46
  allowlisted on both chains, `validateTonJettonWallet`,
39
47
  `validateTonEscrowPayload`, `validateEvmOutputPosition`.
@@ -48,6 +56,13 @@ Every route is fail-closed until its allowlists and validators are configured:
48
56
  `validateEvmInputPosition` (finalized `SrcEscrowCreated` + escrow state)
49
57
  AND `validateTonOutputPosition` (factory-deployed TON position with bound
50
58
  state, inside its completion window) both pass.
59
+ Behind `remoteProvider` the order is built on your server, so the app pins
60
+ the order protocols itself: `remoteProvider({name: "omniston", baseUrl,
61
+ ...omnistonPins(trustedEvmOrderProtocols)})` — the same audited table. The
62
+ engine then signs only an `Order` whose verifying contract is a pinned
63
+ spender, whose maker is this account and that sells exactly the swap's input
64
+ of the source token; a `Permit` or any other typed message is refused.
65
+ `validateEvmTypedOrder` is exported for apps that hold the settlement data.
51
66
  - **EVM → EVM** (gasless order, same order protocol as EVM → TON): the signed
52
67
  extension's ExtraDataArgs name the destination chain, token and deposit and
53
68
  are checked against the quote. The EVM recipient is NOT in anything the
@@ -74,7 +89,11 @@ escrow, not that transaction), `AUDITED_OMNISTON_SETTLEMENT_CONTRACTS`,
74
89
  own endpoints as `rpcUrls` (CAIP-2 → URLs, tried before the public fallbacks)
75
90
  and `tonApiUrl` / `tonApiKey`. On serverless also inject durable
76
91
  `secretStore` / `quoteStore` — the HTLC secret must survive to the poll that
77
- discloses it.
92
+ discloses it. Give the quote store a `claim` (an atomic create-if-absent: Redis
93
+ `SET NX`, a conditional KV put): the platform picks quote ids, and a context is
94
+ never rebound to another user's quote under the same id. A secret is disclosed
95
+ only against the exact order it was locked to (its stored binding), so even a
96
+ rebound context gets nothing.
78
97
 
79
98
  The destination-escrow proof requires the escrow immutables to be found in the
80
99
  resolver's calldata AND to derive, through the trusted factory's
@@ -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,86 @@ 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") {
1116
+ if (params.sourceChain.startsWith("ton:") && params.destChain.startsWith("eip155:")) {
1117
+ if (!hooks.verifyTonEscrowBuild) return "a TON \u2192 EVM escrow body needs verifyTonEscrowBuild (createOmnistonTonValidators)";
1118
+ if (!params.refundAddress) return "the trader's TON address (refundAddress) is unknown";
1119
+ const floor = [quote.minDestAmount, params.minAmountOut ?? "0"].filter((v) => /^\d{1,80}$/.test(v)).map((v) => BigInt(v)).reduce((a, b) => a > b ? a : b, 0n);
1120
+ try {
1121
+ return await hooks.verifyTonEscrowBuild({
1122
+ payloadBocHex: Buffer2.from(tx.bodyBoc, "base64").toString("hex"),
1123
+ ownerAddress: params.refundAddress,
1124
+ inputUnits: quote.sourceAmount,
1125
+ destinationChain: params.destChain,
1126
+ destinationAddress: params.destAddress,
1127
+ outputTokenAddress: params.destToken,
1128
+ minResolverSendsUnits: floor.toString()
1129
+ });
1130
+ } catch {
1131
+ return "the escrow body could not be checked";
1132
+ }
1133
+ }
1134
+ return "only TON swap and TON \u2192 EVM escrow bodies can be checked on the client";
1135
+ }
1136
+ let steps;
1137
+ try {
1138
+ steps = JSON.parse(quote.providerData.routeSteps ?? "");
1139
+ } catch {
1140
+ return "the quote carries no route steps";
1141
+ }
1142
+ if (!Array.isArray(steps) || steps.length === 0) return "the quote carries no route steps";
1143
+ const source = ccAsset(params.sourceChain, params.sourceToken);
1144
+ const dest = ccAssetByToken(params.destChain, params.destToken);
1145
+ if (!source || !dest) return "unknown source or destination token";
1146
+ const protocols = (quote.providerData.protocols ?? "").split(",").filter(Boolean);
1147
+ let checked;
1148
+ try {
1149
+ checked = checkTonSwapPayload({
1150
+ payloadHex: Buffer2.from(tx.bodyBoc, "base64").toString("hex"),
1151
+ targetAddress: tx.to,
1152
+ sendAmount: BigInt(tx.value),
1153
+ trader: params.refundAddress ?? "",
1154
+ dest: params.destAddress,
1155
+ inputUnits: quote.sourceAmount,
1156
+ minOutputAmount: quote.minDestAmount,
1157
+ inputJetton: source.contract ?? null,
1158
+ outputJetton: dest.contract ?? null,
1159
+ steps: steps.map((st) => ({ protocol: String(st.protocol), inputJetton: st.inputJetton, outputJetton: st.outputJetton })),
1160
+ integratorAddress: hooks.integratorAddress ?? null
1161
+ });
1162
+ } catch (e) {
1163
+ return e instanceof Error ? e.message.replace(/^omniston: /, "") : String(e);
1164
+ }
1165
+ const payload = parseSwapPayload(Buffer2.from(tx.bodyBoc, "base64").toString("hex"));
1166
+ const target = source.contract ? payload.jettonTransfer?.destination : tx.to;
1167
+ if (!target) return "the body names no swap destination";
1168
+ try {
1169
+ if (!await hooks.validateTonSwapTarget({ address: target, protocols })) return `${target} is not a recognized DEX contract for ${protocols.join("+")}`;
1170
+ } catch {
1171
+ return "the swap target could not be checked";
1172
+ }
1173
+ if (hooks.validateTonSwapLeg) {
1174
+ for (const leg of checked.legs) {
1175
+ let ok = false;
1176
+ try {
1177
+ ok = await hooks.validateTonSwapLeg(leg);
1178
+ } catch {
1179
+ ok = false;
1180
+ }
1181
+ if (!ok) return `${leg.kind} v${leg.version} leg not verified`;
1182
+ }
1183
+ }
1184
+ return null;
1185
+ };
1186
+ }
1095
1187
  function omnistonProvider(opts = {}) {
1096
1188
  opts = Object.freeze({ ...opts });
1097
1189
  const apiUrl = opts.apiUrl ?? "wss://omni-ws.ston.fi";
@@ -1209,6 +1301,26 @@ function omnistonProvider(opts = {}) {
1209
1301
  async function saveBuilt(quoteId, entry) {
1210
1302
  await quotes.save(quoteId, JSON.stringify(entry));
1211
1303
  }
1304
+ async function claimBuilt(quoteId, entry) {
1305
+ const json = JSON.stringify(entry);
1306
+ if (typeof quotes.claim === "function") {
1307
+ if (await quotes.claim(quoteId, json)) return true;
1308
+ } else if (!await quotes.load(quoteId)) {
1309
+ await quotes.save(quoteId, json);
1310
+ return true;
1311
+ }
1312
+ const existing = await loadBuilt(quoteId);
1313
+ if (!existing) {
1314
+ if (typeof quotes.claim === "function") return await quotes.claim(quoteId, json);
1315
+ await quotes.save(quoteId, json);
1316
+ return true;
1317
+ }
1318
+ const a = existing.bound, b = entry.bound;
1319
+ const same = !!a && !!b && a.kind === b.kind && a.srcChain === b.srcChain && sameIntentAddress(a.trader, b.trader) && sameIntentAddress(a.dest, b.dest);
1320
+ if (!same) return false;
1321
+ await quotes.save(quoteId, json);
1322
+ return true;
1323
+ }
1212
1324
  async function loadBuilt(quoteId) {
1213
1325
  const raw = await quotes.load(quoteId);
1214
1326
  if (!raw) return void 0;
@@ -1375,7 +1487,10 @@ function omnistonProvider(opts = {}) {
1375
1487
  if (BigInt(swap.minOutputAmount) < requiredMin(q.outputUnits)) return null;
1376
1488
  if (q.gasBudget !== void 0 && BigInt(q.gasBudget) > BigInt(maxTonExecutionNano)) return null;
1377
1489
  const minOut = swap.minOutputAmount;
1378
- await saveBuilt(q.quoteId, { quote: q, at: Date.now(), bound: { trader, dest: params.destAddress, srcChain: params.sourceChain, kind: "swap" } });
1490
+ if (!await claimBuilt(q.quoteId, { quote: q, at: Date.now(), bound: { trader, dest: params.destAddress, srcChain: params.sourceChain, kind: "swap" } })) {
1491
+ traceFor(q.quoteId)("quote-id-reused", { kind: "swap" });
1492
+ return null;
1493
+ }
1379
1494
  const feeUnits = Number(q.protocolFeeUnits || "0") + Number(q.integratorFeeUnits || "0");
1380
1495
  const stableOut = /^(USDT|USDC|USD₮|jUSDT)$/.test(dest.symbol);
1381
1496
  const feeUsd = stableOut ? feeUnits / 10 ** dest.decimals : 0;
@@ -1402,6 +1517,8 @@ function omnistonProvider(opts = {}) {
1402
1517
  providerData: {
1403
1518
  settlement: "swap",
1404
1519
  protocols: swapProtocols(swap).join(","),
1520
+ // For the app's own check of a remote build (omnistonVerifyBuild): the route the body must follow.
1521
+ routeSteps: JSON.stringify(swapSteps(swap)),
1405
1522
  ...q.gasBudget ? { gasBudget: q.gasBudget } : {},
1406
1523
  ...q.estimatedGasConsumption ? { gasEstimate: q.estimatedGasConsumption } : {}
1407
1524
  }
@@ -1502,10 +1619,7 @@ function omnistonProvider(opts = {}) {
1502
1619
  minOutputAmount: swap.minOutputAmount,
1503
1620
  inputJetton,
1504
1621
  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 }],
1622
+ steps: swapSteps(swap),
1509
1623
  integratorAddress: q.integratorAddress?.chain.$case === "ton" ? q.integratorAddress.chain.value : null
1510
1624
  });
1511
1625
  } catch (error) {
@@ -1793,7 +1907,9 @@ function omnistonProvider(opts = {}) {
1793
1907
  const outputScaled = BigInt(q.outputUnits) * 10n ** BigInt(source.decimals);
1794
1908
  if (outputScaled * 2n < inputScaled || outputScaled > inputScaled * 2n) return refuse2("quoted output is outside 0.5x\u20132x of the input");
1795
1909
  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" } });
1910
+ if (!await claimBuilt(q.quoteId, { quote: q, at: Date.now(), bound: { trader, dest: params.destAddress, srcChain: params.sourceChain, kind: "order" } })) {
1911
+ return refuse2("the platform reused a quote id that already holds another user's quote");
1912
+ }
1797
1913
  const feeUnits = Number(q.protocolFeeUnits || "0") + Number(q.integratorFeeUnits || "0");
1798
1914
  const feeUsd = feeUnits / 10 ** dest.decimals;
1799
1915
  return {
@@ -1925,6 +2041,7 @@ function omnistonProvider(opts = {}) {
1925
2041
  await forget();
1926
2042
  throw error;
1927
2043
  }
2044
+ await saveBuilt(ref.quoteId, { quote: q, at: Date.now(), ...cached.bound ? { bound: cached.bound } : {} });
1928
2045
  return {
1929
2046
  chain: srcChain,
1930
2047
  to: message.targetAddress,
@@ -1968,8 +2085,15 @@ function omnistonProvider(opts = {}) {
1968
2085
  })
1969
2086
  );
1970
2087
  if (!order) return { state: "pending" };
1971
- const secretsHex = await store.load(decoded.quoteId);
2088
+ let secretsHex = await store.load(decoded.quoteId);
1972
2089
  if (!secretsHex || cached?.quote.settlementData.$case !== "order") trace("disclosure-skipped", { gate: "context", secrets: !!secretsHex, cached: !!cached, feedStatus: order.status });
2090
+ if (secretsHex && cached?.quote.settlementData.$case === "order") {
2091
+ const [lockedTo] = await store.load(bindKey(decoded.quoteId)) ?? [];
2092
+ if (lockedTo !== orderBinding(bound, cached.quote)) {
2093
+ trace("disclosure-skipped", { gate: "binding", bound: lockedTo !== void 0, feedStatus: order.status });
2094
+ secretsHex = void 0;
2095
+ }
2096
+ }
1973
2097
  if (secretsHex && cached?.quote.settlementData.$case === "order") {
1974
2098
  const q = cached.quote;
1975
2099
  const settlement = q.settlementData.value;
@@ -2144,6 +2268,9 @@ export {
2144
2268
  inMemorySecretStore,
2145
2269
  encodeTypedMessage,
2146
2270
  validateEvmToTonOrderExtension,
2271
+ validateEvmTypedOrder,
2272
+ omnistonPins,
2273
+ omnistonVerifyBuild,
2147
2274
  omnistonProvider
2148
2275
  };
2149
- //# sourceMappingURL=chunk-KNT2XSQU.js.map
2276
+ //# sourceMappingURL=chunk-HOTFZYSW.js.map