@anywalletkit/protocol-swap2-omniston 0.3.1 → 0.5.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
@@ -25,20 +25,23 @@ TRON needs its own order path and is not wired yet.
25
25
  Every route is fail-closed until its allowlists and validators are configured:
26
26
 
27
27
  - **TON → TON** (DEX swap): `trustedSettlementContracts["ton:-239"]`,
28
- `validateTonJettonWallet`, `validateTonSwapTarget`. The built message's DEX
28
+ `validateTonJettonWallet`, `validateTonSwapTarget`, `validateTonSwapLeg`. The built message's DEX
29
29
  call is decoded in the package (STON.fi v1/v2 incl. pTON and cross-swap,
30
30
  DeDust v1, DeDust CPMM v2): receiver = destination, refunds/excess = trader,
31
31
  on-chain minimum ≥ the quote's, no foreign referral; unknown layouts and
32
32
  split/multi-route quotes are refused by name. `validateTonSwapLeg`
33
33
  (supplied by `createOmnistonTonValidators`) confirms the routers' ask
34
34
  wallets, pTON wallet ownership and DeDust pool pairs on chain.
35
- `tonSwapPayloadCheck: "report"` traces `ton-swap-payload <reason>` instead
36
- of refusing (default `"enforce"`).
35
+ Without it the decoder cannot tell which token the DEX pays out, so the
36
+ route is not advertised. `tonSwapPayloadCheck: "report"` traces
37
+ `ton-swap-payload <reason>` (incl. `legs-unverified`) instead of refusing
38
+ (default `"enforce"`) — an explicit rollout opt-in.
37
39
  Behind `remoteProvider`, the app checks the built body itself:
38
40
  `remoteProvider({name: "omniston", baseUrl, verifyBuild: omnistonVerifyBuild({...createOmnistonTonValidators()})})`.
39
41
  A TON → TON swap body is opened with `checkTonSwapPayload` against the app's own request (receiver,
40
42
  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
43
+ the app's validators — `validateTonSwapTarget` and `validateTonSwapLeg`; without the leg check a TON swap body
44
+ is refused. A TON → EVM escrow body goes to `verifyTonEscrowBuild`: the jetton amount, the
42
45
  trader as owner, the destination chain, address and token and the quote's minimum delivery come from
43
46
  the app, the protocol contract from `trustedProtocolAddresses`, and the escrow code, authority key and
44
47
  timelocks are the audited ones. Without `verifyBuild` the engine refuses remote cell bodies.
@@ -62,6 +65,19 @@ Every route is fail-closed until its allowlists and validators are configured:
62
65
  engine then signs only an `Order` whose verifying contract is a pinned
63
66
  spender, whose maker is this account and that sells exactly the swap's input
64
67
  of the source token; a `Permit` or any other typed message is refused.
68
+ The rest of the order is the app's to check, and the engine signs a remote
69
+ typed order only with `verifyBuild`: `remoteProvider({name: "omniston",
70
+ baseUrl, ...omnistonPins(AUDITED_OMNISTON_EVM_ORDER_PROTOCOLS), verifyBuild:
71
+ omnistonVerifyBuild({...createOmnistonTonValidators()})})`. The build carries
72
+ the `orderExtension` the order's salt commits to, and `omnistonVerifyBuild`
73
+ opens both: the v4 domain on a trusted order protocol, the exact Order schema,
74
+ `takingAmount` at least the quote's minimum, a single-fill order with no
75
+ allowed sender and an expiry minutes away, the pinned v1beta8 auction and fee
76
+ terms, one trusted escrow factory (`trustedSettlementContracts`, default the
77
+ audited table) as the extension target, the whitelisted resolver as the
78
+ order's receiver, the destination chain and token, the audited timelocks, the
79
+ TON token and recipient and the quote id. The hashlock is the server's: the
80
+ server holds the HTLC secret. The same applies to EVM → EVM.
65
81
  `validateEvmTypedOrder` is exported for apps that hold the settlement data.
66
82
  - **EVM → EVM** (gasless order, same order protocol as EVM → TON): the signed
67
83
  extension's ExtraDataArgs name the destination chain, token and deposit and
@@ -4,6 +4,10 @@ import {
4
4
  import {
5
5
  describeOmnistonActivity
6
6
  } from "./chunk-AIWAM7BS.js";
7
+ import {
8
+ AUDITED_OMNISTON_EVM_ORDER_PROTOCOLS,
9
+ AUDITED_OMNISTON_SETTLEMENT_CONTRACTS
10
+ } from "./chunk-JWYDIW4V.js";
7
11
 
8
12
  // src/provider.ts
9
13
  import { Address as TonAddress, Cell as TonCell } from "@ton/core";
@@ -390,18 +394,36 @@ function toChainAddress(caip2, address) {
390
394
  if (!$case) return null;
391
395
  return { chain: { $case, value: getAddress(address) } };
392
396
  }
393
- function encodeIntent(quoteId, trader, dest, srcChain, kind = "order") {
394
- return [INTENT_PREFIX, quoteId, trader, dest, srcChain, ...kind === "swap" ? ["swap"] : []].map(encodeURIComponent).join("|");
397
+ var QUOTE_DIGEST_SKIP = /* @__PURE__ */ new Set(["rfqId", "resolverName", "estimatedSettlementDuration", "estimatedGasConsumption"]);
398
+ function canonicalJson(value) {
399
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
400
+ if (value && typeof value === "object") {
401
+ return `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(value[k])}`).join(",")}}`;
402
+ }
403
+ return JSON.stringify(value) ?? "null";
404
+ }
405
+ function quoteDigest(q) {
406
+ const terms = JSON.parse(JSON.stringify(q));
407
+ for (const key of QUOTE_DIGEST_SKIP) delete terms[key];
408
+ return sha256(new TextEncoder().encode(canonicalJson(terms))).slice(2, 34);
409
+ }
410
+ var QUOTE_DIGEST = /^[0-9a-f]{32}$/;
411
+ function encodeIntent(quoteId, trader, dest, srcChain, kind, digest) {
412
+ return [INTENT_PREFIX, quoteId, trader, `${dest}#${digest}`, srcChain, ...kind === "swap" ? ["swap"] : []].map(encodeURIComponent).join("|");
395
413
  }
396
414
  var QUOTE_ID = /^[0-9a-fA-F]{48,64}$/;
397
415
  function decodeIntent(intentId) {
398
416
  try {
399
417
  const parts = intentId.split("|");
400
418
  if (parts.length !== 5 && parts.length !== 6 || parts[0] !== INTENT_PREFIX) return null;
401
- const [, quoteId = "", trader = "", dest = "", srcChain = "", kindPart = ""] = parts.map(decodeURIComponent);
419
+ const [, quoteId = "", trader = "", destPart = "", srcChain = "", kindPart = ""] = parts.map(decodeURIComponent);
420
+ const mark = destPart.lastIndexOf("#");
421
+ const dest = mark < 0 ? destPart : destPart.slice(0, mark);
422
+ const digest = mark < 0 ? void 0 : destPart.slice(mark + 1);
423
+ if (digest !== void 0 && !QUOTE_DIGEST.test(digest)) return null;
402
424
  if (!QUOTE_ID.test(quoteId) || !trader || !dest || !srcChain) return null;
403
425
  if (parts.length === 6 && kindPart !== "swap") return null;
404
- return { quoteId, trader, dest, srcChain, kind: parts.length === 6 ? "swap" : "order" };
426
+ return { quoteId, trader, dest, srcChain, kind: parts.length === 6 ? "swap" : "order", ...digest ? { digest } : {} };
405
427
  } catch {
406
428
  return null;
407
429
  }
@@ -522,9 +544,9 @@ function validEvmAddress(value) {
522
544
  }
523
545
  }
524
546
  var EVM_DEST_PROTOCOL_FEE_RECIPIENTS = /* @__PURE__ */ new Set(["0x076268146b4ec6d21fc49c27586cfa91b9079a00"]);
525
- async function validateEvmToTonOrderExtension(extension, expected) {
526
- const evmDest = expected.dstChainId !== void 0;
527
- if (evmDest && (!Number.isSafeInteger(expected.dstChainId) || expected.dstChainId <= 0)) throw new Error("omniston: invalid EVM destination chain id");
547
+ var EVM_AUCTION_POINTS = [[800000n, 100n], [700000n, 100n], [600000n, 100n], [500000n, 100n], [400000n, 100n]];
548
+ var EVM_TIMELOCKS = [1n, 120n, 240n, 300n, 1n, 120n, 180n, 0n].reduce((packed, value, index) => packed | value << BigInt(index * 32), 0n);
549
+ function readEvmOrderExtension(extension) {
528
550
  if (extension.length < 32) throw new Error("omniston: malformed EVM order extension");
529
551
  const view = new DataView(extension.buffer, extension.byteOffset, extension.byteLength);
530
552
  const fields = [];
@@ -544,54 +566,48 @@ async function validateEvmToTonOrderExtension(extension, expected) {
544
566
  }
545
567
  let target;
546
568
  for (const index of [2, 3, 7]) {
547
- const field = fields[index];
548
- if (field.length <= 20) throw new Error("omniston: incomplete EVM order extension program");
549
- const address = getAddress(`0x${bytesToHex(field.slice(0, 20))}`);
550
- if (!expected.trustedTarget(address) || target !== void 0 && getAddress(target) !== address) {
569
+ const field2 = fields[index];
570
+ if (field2.length <= 20) throw new Error("omniston: incomplete EVM order extension program");
571
+ const address = getAddress(`0x${bytesToHex(field2.slice(0, 20))}`);
572
+ if (target !== void 0 && getAddress(target) !== address) {
551
573
  throw new Error("omniston: EVM order extension target is not independently trusted");
552
574
  }
553
575
  target = address;
554
576
  }
555
- const getter = fields[2];
577
+ const field = fields[2];
556
578
  let cursor = 20;
557
- const gasBump = readBigEndian(getter, cursor, 3);
579
+ const gasBump = readBigEndian(field, cursor, 3);
558
580
  cursor += 3;
559
- const gasPrice = readBigEndian(getter, cursor, 4);
581
+ const gasPrice = readBigEndian(field, cursor, 4);
560
582
  cursor += 4;
561
- const auctionStart = readBigEndian(getter, cursor, 4);
583
+ const auctionStart = readBigEndian(field, cursor, 4);
562
584
  cursor += 4;
563
- const duration = readBigEndian(getter, cursor, 3);
585
+ const duration = readBigEndian(field, cursor, 3);
564
586
  cursor += 3;
565
- const initialRateBump = readBigEndian(getter, cursor, 3);
587
+ const initialRateBump = readBigEndian(field, cursor, 3);
566
588
  cursor += 3;
567
- const pointCount = bigintToNumber(readBigEndian(getter, cursor, 1));
589
+ const pointCount = bigintToNumber(readBigEndian(field, cursor, 1));
568
590
  cursor += 1;
569
591
  if (pointCount !== 5) throw new Error("omniston: unsupported EVM auction profile");
570
592
  const points = [];
571
593
  for (let index = 0; index < pointCount; index++) {
572
- points.push([readBigEndian(getter, cursor, 3), readBigEndian(getter, cursor + 3, 2)]);
594
+ points.push([readBigEndian(field, cursor, 3), readBigEndian(field, cursor + 3, 2)]);
573
595
  cursor += 5;
574
596
  }
575
- const integratorFee = readBigEndian(getter, cursor, 2);
597
+ const integratorFee = readBigEndian(field, cursor, 2);
576
598
  cursor += 2;
577
- const integratorShare = readBigEndian(getter, cursor, 1);
599
+ const integratorShare = readBigEndian(field, cursor, 1);
578
600
  cursor += 1;
579
- const resolverFee = readBigEndian(getter, cursor, 2);
601
+ const resolverFee = readBigEndian(field, cursor, 2);
580
602
  cursor += 2;
581
- const whitelistDiscount = readBigEndian(getter, cursor, 1);
603
+ const whitelistDiscount = readBigEndian(field, cursor, 1);
582
604
  cursor += 1;
583
- const getterWhitelistCount = bigintToNumber(readBigEndian(getter, cursor, 1));
605
+ const getterWhitelistCount = bigintToNumber(readBigEndian(field, cursor, 1));
584
606
  cursor += 1;
585
- if (getterWhitelistCount !== 1 || cursor + getterWhitelistCount * 10 !== getter.length) {
607
+ if (getterWhitelistCount !== 1 || cursor + getterWhitelistCount * 10 !== field.length) {
586
608
  throw new Error("omniston: malformed EVM amount-getter whitelist");
587
609
  }
588
- const getterResolver = readBigEndian(getter, cursor, 10);
589
- const expectedPoints = [[800000n, 100n], [700000n, 100n], [600000n, 100n], [500000n, 100n], [400000n, 100n]];
590
- const quoteResolverFee = expected.protocolFeePips % 10 === 0 ? BigInt(expected.protocolFeePips / 10) : -1n;
591
- const resolverLow80 = BigInt(getAddress(expected.initialResolverAddress)) & (1n << 80n) - 1n;
592
- if (gasBump !== 0n || gasPrice !== 0n || !within(auctionStart, expected.quoteTimestamp, 5, 5) || duration !== 1800n || initialRateBump !== 900000n || points.some((point, index) => point[0] !== expectedPoints[index][0] || point[1] !== expectedPoints[index][1]) || expected.integratorFeePips !== 0 || integratorFee !== 0n || integratorShare !== 100n || quoteResolverFee < 0n || resolverFee !== quoteResolverFee || whitelistDiscount !== 50n || getterResolver !== resolverLow80) {
593
- throw new Error("omniston: EVM amount-getter terms do not match the pinned v1beta8 profile");
594
- }
610
+ const getter = { gasBump, gasPrice, auctionStart, duration, initialRateBump, points, integratorFee, integratorShare, resolverFee, whitelistDiscount, resolver: readBigEndian(field, cursor, 10) };
595
611
  const post = fields[7];
596
612
  cursor = 20;
597
613
  const integratorRecipient = post.slice(cursor, cursor + 20);
@@ -617,28 +633,51 @@ async function validateEvmToTonOrderExtension(extension, expected) {
617
633
  cursor += 10;
618
634
  const resolverTimeDelta = readBigEndian(post, cursor, 2);
619
635
  cursor += 2;
620
- if (!zeroBytes(integratorRecipient) || !(zeroBytes(protocolRecipient) || evmDest && EVM_DEST_PROTOCOL_FEE_RECIPIENTS.has(`0x${bytesToHex(protocolRecipient)}`)) || postIntegratorFee !== 0n || postIntegratorShare !== 100n || postResolverFee !== quoteResolverFee || postDiscount !== 50n || !within(allowedTime, expected.quoteTimestamp, 60, 5) || postResolver !== resolverLow80 || resolverTimeDelta !== 0n) {
621
- throw new Error("omniston: EVM post-interaction terms do not match the pinned v1beta8 profile");
622
- }
623
636
  const extra = Array.from({ length: 5 }, () => {
624
637
  const value = readBigEndian(post, cursor, 32);
625
638
  cursor += 32;
626
639
  return value;
627
640
  });
641
+ const customData = extension.slice(32 + begin);
642
+ if (customData.length !== 131) throw new Error("omniston: malformed EVM order extension custom data");
643
+ return {
644
+ target,
645
+ getter,
646
+ post: { integratorRecipient, protocolRecipient, integratorFee: postIntegratorFee, integratorShare: postIntegratorShare, resolverFee: postResolverFee, discount: postDiscount, allowedTime, resolver: postResolver, resolverTimeDelta },
647
+ extra,
648
+ customData
649
+ };
650
+ }
651
+ async function validateEvmToTonOrderExtension(extension, expected) {
652
+ const evmDest = expected.dstChainId !== void 0;
653
+ if (evmDest && (!Number.isSafeInteger(expected.dstChainId) || expected.dstChainId <= 0)) throw new Error("omniston: invalid EVM destination chain id");
654
+ const { target, getter, post, extra, customData } = readEvmOrderExtension(extension);
655
+ if (!expected.trustedTarget(target)) throw new Error("omniston: EVM order extension target is not independently trusted");
656
+ const quoteResolverFee = expected.protocolFeePips % 10 === 0 ? BigInt(expected.protocolFeePips / 10) : -1n;
657
+ const resolverLow80 = BigInt(getAddress(expected.initialResolverAddress)) & (1n << 80n) - 1n;
658
+ if (getter.gasBump !== 0n || getter.gasPrice !== 0n || !within(getter.auctionStart, expected.quoteTimestamp, 5, 5) || getter.duration !== 1800n || getter.initialRateBump !== 900000n || getter.points.some((point, index) => point[0] !== EVM_AUCTION_POINTS[index][0] || point[1] !== EVM_AUCTION_POINTS[index][1]) || expected.integratorFeePips !== 0 || getter.integratorFee !== 0n || getter.integratorShare !== 100n || quoteResolverFee < 0n || getter.resolverFee !== quoteResolverFee || getter.whitelistDiscount !== 50n || getter.resolver !== resolverLow80) {
659
+ throw new Error("omniston: EVM amount-getter terms do not match the pinned v1beta8 profile");
660
+ }
661
+ if (!zeroBytes(post.integratorRecipient) || !(zeroBytes(post.protocolRecipient) || evmDest && EVM_DEST_PROTOCOL_FEE_RECIPIENTS.has(`0x${bytesToHex(post.protocolRecipient)}`)) || post.integratorFee !== 0n || post.integratorShare !== 100n || post.resolverFee !== quoteResolverFee || post.discount !== 50n || !within(post.allowedTime, expected.quoteTimestamp, 60, 5) || post.resolver !== resolverLow80 || post.resolverTimeDelta !== 0n) {
662
+ throw new Error("omniston: EVM post-interaction terms do not match the pinned v1beta8 profile");
663
+ }
628
664
  const hashlock2 = readBigEndian(expected.secretHash, 0, expected.secretHash.length);
629
665
  const dstToken = evmDest ? BigInt(getAddress(expected.outputTokenAddress)) : BigInt(getAddress(expected.takerAsset));
630
666
  const deposits = BigInt(expected.srcSecurityDepositUnits) << 128n | BigInt(expected.dstSecurityDepositUnits);
631
- const timelocks = [1n, 120n, 240n, 300n, 1n, 120n, 180n, 0n].reduce((packed, value, index) => packed | value << BigInt(index * 32), 0n);
632
- if (expected.secretHash.length !== 32 || extra[0] !== hashlock2 || extra[1] !== (evmDest ? BigInt(expected.dstChainId) : 0n) || extra[2] !== dstToken || extra[3] !== deposits || extra[4] !== timelocks) {
667
+ if (expected.secretHash.length !== 32 || extra[0] !== hashlock2 || extra[1] !== (evmDest ? BigInt(expected.dstChainId) : 0n) || extra[2] !== dstToken || extra[3] !== deposits || extra[4] !== EVM_TIMELOCKS) {
633
668
  throw new Error("omniston: signed EVM HTLC terms do not match the validated order details");
634
669
  }
635
- const customData = extension.slice(32 + begin);
636
- if (customData.length !== 131) throw new Error("omniston: malformed EVM order extension custom data");
637
- if (evmDest) {
670
+ await checkOrderCustomData(customData, { evmDest, outputToken: expected.outputTokenAddress, traderDst: expected.traderDstAddress, quoteId: expected.quoteId });
671
+ if (expected.tradeStartDeadline < expected.quoteTimestamp) {
672
+ throw new Error("omniston: invalid EVM order quote deadline");
673
+ }
674
+ }
675
+ async function checkOrderCustomData(customData, expected) {
676
+ if (expected.evmDest) {
638
677
  if (!zeroBytes(customData.slice(0, 66))) throw new Error("omniston: unexpected TON addresses in an EVM-destination order");
639
678
  } else {
640
679
  const { Address: Address2 } = await import("@ton/core");
641
- const expectedTon = [expected.outputTokenAddress, expected.traderDstAddress].map((value) => Address2.parse(value));
680
+ const expectedTon = [expected.outputToken, expected.traderDst].map((value) => Address2.parse(value));
642
681
  for (let index = 0; index < expectedTon.length; index++) {
643
682
  const offset = index * 33;
644
683
  const workchain = customData[offset] === 255 ? -1 : customData[offset];
@@ -654,9 +693,6 @@ async function validateEvmToTonOrderExtension(extension, expected) {
654
693
  if (!QUOTE_ID.test(quoteId) || !sliceEqual(customData.slice(99), hexToBytes(quoteId.padStart(64, "0")))) {
655
694
  throw new Error("omniston: EVM order extension custom data is not bound to the quote id");
656
695
  }
657
- if (expected.tradeStartDeadline < expected.quoteTimestamp) {
658
- throw new Error("omniston: invalid EVM order quote deadline");
659
- }
660
696
  }
661
697
  function validateEvmTypedOrder(td, expected) {
662
698
  const value = td;
@@ -1108,8 +1144,101 @@ function omnistonPins(trustedEvmOrderProtocols) {
1108
1144
  const byChain = new Map(Object.entries(trustedEvmOrderProtocols).map(([chain, list]) => [chain, list.map((a) => getAddress(a))]));
1109
1145
  return Object.freeze({ callTargets: () => [], spenders: (chain) => byChain.get(chain) ?? [] });
1110
1146
  }
1147
+ var MAX_EVM_ORDER_LIFETIME_S = 900;
1148
+ async function checkRemoteEvmOrder(quote, tx, params, trusted) {
1149
+ const chainId = evmRef(params.sourceChain);
1150
+ if (chainId === null || !params.sourceChain.startsWith("eip155:")) return "a typed order needs an EVM source";
1151
+ const intent = decodeIntent(quote.intentId.replace(/~[0-9a-f]{32}$/, ""));
1152
+ if (!intent || intent.kind !== "order" || intent.srcChain !== params.sourceChain) return "the quote's intent is not an order from this chain";
1153
+ if (!tx.orderExtension || !/^0x(?:[0-9a-fA-F]{2})+$/.test(tx.orderExtension)) return "the build carries no order extension to check";
1154
+ const source = ccAsset(params.sourceChain, params.sourceToken);
1155
+ if (!source?.contract) return "unknown source token";
1156
+ const tonDest = isTon(params.destChain);
1157
+ const destChainId = tonDest ? null : evmRef(params.destChain);
1158
+ if (!tonDest && (destChainId === null || !params.destChain.startsWith("eip155:"))) return "the destination is neither TON nor an EVM chain";
1159
+ const outputToken = ccAssetByToken(params.destChain, params.destToken)?.contract ?? params.destToken;
1160
+ const listed = (table, chain, address) => typeof address === "string" && validEvmAddress(address) && (table[chain] ?? []).some((a) => validEvmAddress(a) && getAddress(a) === getAddress(address));
1161
+ let td;
1162
+ try {
1163
+ td = JSON.parse(tx.typedData ?? "");
1164
+ } catch {
1165
+ return "the typed data is not JSON";
1166
+ }
1167
+ const domain = td.domain ?? {};
1168
+ const contract = domain.verifyingContract;
1169
+ if (domain.name !== "1inch Limit Order Protocol" || domain.version !== "4") return "the domain is not the 1inch Limit Order Protocol v4";
1170
+ if (!listed(trusted.orderProtocols, params.sourceChain, contract) || !tx.approvalAddress || getAddress(tx.approvalAddress) !== getAddress(String(contract))) {
1171
+ return `${String(contract)} is not a trusted order protocol on ${params.sourceChain}`;
1172
+ }
1173
+ if (Number(domain.chainId) !== chainId || tx.chainId !== void 0 && tx.chainId !== chainId) return "the domain chainId is not the source chain";
1174
+ const order = td.types?.Order;
1175
+ const message = td.message;
1176
+ if (td.primaryType !== "Order" || !Array.isArray(order) || order.length !== ORDER_FIELDS.length || !order.every((field, i) => field.name === ORDER_FIELDS[i][0] && field.type === ORDER_FIELDS[i][1]) || !message || Object.keys(message).length !== ORDER_FIELDS.length || !ORDER_FIELDS.every(([name]) => Object.hasOwn(message, name)) || !ORDER_FIELDS.every(([name, type]) => type === "address" ? validEvmAddress(message[name]) : validUint256(message[name]))) {
1177
+ return "the typed data is not exactly a v4 Order";
1178
+ }
1179
+ if (!validEvmAddress(intent.trader) || getAddress(String(message.maker)) !== getAddress(intent.trader)) return "the maker is not the trader";
1180
+ if (getAddress(String(message.makerAsset)) !== getAddress(source.contract)) return "the makerAsset is not the source token";
1181
+ if (BigInt(message.makingAmount) !== BigInt(quote.sourceAmount)) return "the makingAmount is not the input";
1182
+ if (getAddress(String(message.takerAsset)) === getAddress(String(message.makerAsset))) return "the takerAsset is the makerAsset";
1183
+ 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);
1184
+ if (floor <= 0n || BigInt(message.takingAmount) < floor) return `takingAmount ${String(message.takingAmount)} is below the quote's minimum ${floor}`;
1185
+ const traits = BigInt(message.makerTraits);
1186
+ const flags = traits & (1n << 56n) - 1n << 200n;
1187
+ const baseFlags = 1n << 251n | 1n << 249n;
1188
+ if (flags !== baseFlags && flags !== (baseFlags | 1n << 255n)) return "the makerTraits flags are not a single-fill order with its extension";
1189
+ if ((traits & (1n << 80n) - 1n) !== 0n || (traits >> 160n & (1n << 40n) - 1n) !== 0n) return "the makerTraits restrict the taker or the series";
1190
+ const now = BigInt(Math.floor(Date.now() / 1e3));
1191
+ const expiration = traits >> 80n & (1n << 40n) - 1n;
1192
+ if (expiration <= now || expiration > now + BigInt(MAX_EVM_ORDER_LIFETIME_S)) return "the order's expiry is not minutes away";
1193
+ const extension = viemHexToBytes(tx.orderExtension);
1194
+ const mask = (1n << 160n) - 1n;
1195
+ if ((BigInt(message.salt) & mask) !== (BigInt(keccak256(extension)) & mask)) return "the salt does not commit the order extension";
1196
+ let parsed;
1197
+ try {
1198
+ parsed = readEvmOrderExtension(extension);
1199
+ } catch (e) {
1200
+ return e instanceof Error ? e.message.replace(/^omniston: /, "") : String(e);
1201
+ }
1202
+ const { target, getter, post, extra, customData } = parsed;
1203
+ if (!listed(trusted.factories, params.sourceChain, target)) return `the extension targets ${target}, not a trusted escrow factory on ${params.sourceChain}`;
1204
+ const receiverLow80 = BigInt(getAddress(String(message.receiver))) & (1n << 80n) - 1n;
1205
+ if (getter.resolver !== receiverLow80 || post.resolver !== receiverLow80) return "the order's receiver is not the resolver its extension whitelists";
1206
+ if (getter.gasBump !== 0n || getter.gasPrice !== 0n || getter.duration !== 1800n || getter.initialRateBump !== 900000n || getter.points.some((point, index) => point[0] !== EVM_AUCTION_POINTS[index][0] || point[1] !== EVM_AUCTION_POINTS[index][1]) || getter.integratorFee !== 0n || getter.integratorShare !== 100n || getter.whitelistDiscount !== 50n || !within(getter.auctionStart, bigintToNumber(now), MAX_EVM_ORDER_LIFETIME_S, 60)) {
1207
+ return "the amount-getter terms do not match the pinned v1beta8 profile";
1208
+ }
1209
+ if (!zeroBytes(post.integratorRecipient) || !(zeroBytes(post.protocolRecipient) || !tonDest && EVM_DEST_PROTOCOL_FEE_RECIPIENTS.has(`0x${bytesToHex(post.protocolRecipient)}`)) || post.integratorFee !== 0n || post.integratorShare !== 100n || post.resolverFee !== getter.resolverFee || post.discount !== 50n || post.resolverTimeDelta !== 0n || !within(post.allowedTime, bigintToNumber(getter.auctionStart), 60, 5)) {
1210
+ return "the post-interaction terms do not match the pinned v1beta8 profile";
1211
+ }
1212
+ let dstToken;
1213
+ try {
1214
+ dstToken = tonDest ? BigInt(getAddress(String(message.takerAsset))) : BigInt(getAddress(outputToken));
1215
+ } catch {
1216
+ return "the destination token is not an EVM address";
1217
+ }
1218
+ if (extra[1] !== (tonDest ? 0n : BigInt(destChainId)) || extra[2] !== dstToken || extra[4] !== EVM_TIMELOCKS) {
1219
+ return "the signed HTLC terms name another destination chain or token, or other timelocks";
1220
+ }
1221
+ try {
1222
+ await checkOrderCustomData(customData, { evmDest: !tonDest, outputToken, traderDst: params.destAddress, quoteId: intent.quoteId });
1223
+ } catch (e) {
1224
+ return e instanceof Error ? e.message.replace(/^omniston: /, "") : String(e);
1225
+ }
1226
+ return null;
1227
+ }
1111
1228
  function omnistonVerifyBuild(hooks) {
1229
+ const trusted = {
1230
+ orderProtocols: hooks.trustedEvmOrderProtocols ?? AUDITED_OMNISTON_EVM_ORDER_PROTOCOLS,
1231
+ factories: hooks.trustedSettlementContracts ?? AUDITED_OMNISTON_SETTLEMENT_CONTRACTS
1232
+ };
1112
1233
  return async (quote, tx, params) => {
1234
+ if (tx.typedData !== void 0) {
1235
+ if (!params) return "the request this quote answered is unknown";
1236
+ try {
1237
+ return await checkRemoteEvmOrder(quote, tx, params, trusted);
1238
+ } catch {
1239
+ return "the typed order could not be checked";
1240
+ }
1241
+ }
1113
1242
  if (tx.bodyBoc === void 0) return null;
1114
1243
  if (!params) return "the request this quote answered is unknown";
1115
1244
  if (quote.providerData?.settlement !== "swap") {
@@ -1170,16 +1299,15 @@ function omnistonVerifyBuild(hooks) {
1170
1299
  } catch {
1171
1300
  return "the swap target could not be checked";
1172
1301
  }
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`;
1302
+ if (!hooks.validateTonSwapLeg) return "a TON swap body needs validateTonSwapLeg (createOmnistonTonValidators)";
1303
+ for (const leg of checked.legs) {
1304
+ let ok = false;
1305
+ try {
1306
+ ok = await hooks.validateTonSwapLeg(leg);
1307
+ } catch {
1308
+ ok = false;
1182
1309
  }
1310
+ if (!ok) return `${leg.kind} v${leg.version} leg not verified`;
1183
1311
  }
1184
1312
  return null;
1185
1313
  };
@@ -1316,7 +1444,7 @@ function omnistonProvider(opts = {}) {
1316
1444
  return true;
1317
1445
  }
1318
1446
  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);
1447
+ const same = !!a && !!b && a.kind === b.kind && a.srcChain === b.srcChain && sameIntentAddress(a.trader, b.trader) && sameIntentAddress(a.dest, b.dest) && quoteDigest(existing.quote) === quoteDigest(entry.quote);
1320
1448
  if (!same) return false;
1321
1449
  await quotes.save(quoteId, json);
1322
1450
  return true;
@@ -1369,10 +1497,19 @@ function omnistonProvider(opts = {}) {
1369
1497
  throw new Error(`omniston: intent id does not match the quote it was issued for (${bound.field})`);
1370
1498
  }
1371
1499
  if (bound.legacy) trace("legacy-intent", { where });
1500
+ if (!decoded.digest) {
1501
+ trace("intent-mismatch", { where, field: "digest-missing" });
1502
+ throw new Error("omniston: this quote was issued before an update \u2014 request a fresh quote");
1503
+ }
1504
+ if (decoded.digest !== quoteDigest(entry.quote)) {
1505
+ trace("intent-mismatch", { where, field: "digest" });
1506
+ throw new Error("omniston: quote was replaced \u2014 request a fresh quote");
1507
+ }
1372
1508
  return bound.ref;
1373
1509
  }
1374
1510
  const bindKey = (quoteId) => `${quoteId}#bind`;
1375
1511
  const disclosedKey = (quoteId) => `${quoteId}#disclosed`;
1512
+ const disclosedIndicesKey = (quoteId) => `${quoteId}#disclosed-indices`;
1376
1513
  function orderBinding(ref, q) {
1377
1514
  return sha256(new TextEncoder().encode(JSON.stringify([
1378
1515
  ref.quoteId,
@@ -1389,6 +1526,7 @@ function omnistonProvider(opts = {}) {
1389
1526
  await store.clear(quoteId);
1390
1527
  await store.clear(bindKey(quoteId));
1391
1528
  await store.clear(disclosedKey(quoteId));
1529
+ await store.clear(disclosedIndicesKey(quoteId));
1392
1530
  }
1393
1531
  async function secretsFor(ref, q, count) {
1394
1532
  const quoteId = ref.quoteId;
@@ -1417,7 +1555,7 @@ function omnistonProvider(opts = {}) {
1417
1555
  return !!existing && existing.length > 0;
1418
1556
  }
1419
1557
  const SWAP_SLIPPAGE_PIPS_DEFAULT = 5e3;
1420
- const sameChainTonReady = () => typeof opts.validateTonJettonWallet === "function" && typeof opts.validateTonSwapTarget === "function";
1558
+ const sameChainTonReady = () => typeof opts.validateTonJettonWallet === "function" && typeof opts.validateTonSwapTarget === "function" && (typeof opts.validateTonSwapLeg === "function" || opts.tonSwapPayloadCheck === "report");
1421
1559
  const tonSourceReady = (from, to) => omniston.supports(from, to) && trustedContracts.has(from) && trustedContracts.has(to) && typeof opts.validateTonJettonWallet === "function" && typeof opts.validateTonEscrowPayload === "function" && typeof opts.validateEvmOutputPosition === "function";
1422
1560
  const evmSourceReady = (from, to) => omniston.supports(from, to) && trustedOrderProtocols.has(from) && trustedContracts.has(from) && trustedContracts.has(to) && typeof opts.validateEvmInputPosition === "function" && typeof opts.validateTonOutputPosition === "function";
1423
1561
  const evmToEvmReady = (from, to) => omniston.supports(from, to) && trustedOrderProtocols.has(from) && trustedContracts.has(from) && trustedContracts.has(to) && typeof opts.validateEvmInputPosition === "function" && typeof opts.validateEvmOutputPosition === "function";
@@ -1496,7 +1634,7 @@ function omnistonProvider(opts = {}) {
1496
1634
  const feeUsd = stableOut ? feeUnits / 10 ** dest.decimals : 0;
1497
1635
  return {
1498
1636
  provider: "omniston",
1499
- intentId: encodeIntent(q.quoteId, trader, params.destAddress, params.sourceChain, "swap"),
1637
+ intentId: encodeIntent(q.quoteId, trader, params.destAddress, params.sourceChain, "swap", quoteDigest(q)),
1500
1638
  depositAddress: "",
1501
1639
  // swap settlement — the built message pays the DEX directly
1502
1640
  depositMemo: null,
@@ -1529,7 +1667,7 @@ function omnistonProvider(opts = {}) {
1529
1667
  if (!isTon(ref.srcChain) || q.inputAsset.chain.$case !== "ton" || q.outputAsset.chain.$case !== "ton") {
1530
1668
  throw new Error("omniston: swap quote is not a TON same-chain swap");
1531
1669
  }
1532
- if (!sameChainTonReady()) throw new Error("omniston: TON same-chain swaps need independent jetton-wallet and swap-target validators");
1670
+ if (!sameChainTonReady()) throw new Error("omniston: TON same-chain swaps need independent jetton-wallet, swap-target and swap-leg validators");
1533
1671
  const owner = { chain: { $case: "ton", value: ref.trader } };
1534
1672
  const receiver = { chain: { $case: "ton", value: ref.dest } };
1535
1673
  const omni = await getClient();
@@ -1628,7 +1766,7 @@ function omnistonProvider(opts = {}) {
1628
1766
  }
1629
1767
  if (checked) {
1630
1768
  if (typeof opts.validateTonSwapLeg !== "function") {
1631
- trace("ton-swap-payload", { reason: "legs-unverified", detail: "no validateTonSwapLeg hook", mode, legs: checked.legs.length });
1769
+ payloadRefusal("legs-unverified", `no validateTonSwapLeg hook for ${checked.legs.length} leg(s)`);
1632
1770
  } else {
1633
1771
  for (const leg of checked.legs) {
1634
1772
  let ok = false;
@@ -1799,7 +1937,7 @@ function omnistonProvider(opts = {}) {
1799
1937
  }
1800
1938
  });
1801
1939
  const orderProtocol = getAddress(protocol.chain.value);
1802
- return { chain: srcChain, to: orderProtocol, value: "0", chainId, approvalAddress: orderProtocol, typedData };
1940
+ return { chain: srcChain, to: orderProtocol, value: "0", chainId, approvalAddress: orderProtocol, typedData, orderExtension: `0x${bytesToHex(payload.orderExtension)}` };
1803
1941
  }
1804
1942
  return {
1805
1943
  name: "omniston",
@@ -1908,13 +2046,13 @@ function omnistonProvider(opts = {}) {
1908
2046
  if (outputScaled * 2n < inputScaled || outputScaled > inputScaled * 2n) return refuse2("quoted output is outside 0.5x\u20132x of the input");
1909
2047
  if (await quoteIdInUse(q.quoteId)) return refuse2("the platform reused a quote id that already belongs to an order");
1910
2048
  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");
2049
+ return refuse2("the platform reused a quote id that already holds another quote");
1912
2050
  }
1913
2051
  const feeUnits = Number(q.protocolFeeUnits || "0") + Number(q.integratorFeeUnits || "0");
1914
2052
  const feeUsd = feeUnits / 10 ** dest.decimals;
1915
2053
  return {
1916
2054
  provider: "omniston",
1917
- intentId: encodeIntent(q.quoteId, trader, params.destAddress, params.sourceChain),
2055
+ intentId: encodeIntent(q.quoteId, trader, params.destAddress, params.sourceChain, "order", quoteDigest(q)),
1918
2056
  depositAddress: "",
1919
2057
  // order settlement — no static deposit address
1920
2058
  depositMemo: null,
@@ -2086,6 +2224,7 @@ function omnistonProvider(opts = {}) {
2086
2224
  );
2087
2225
  if (!order) return { state: "pending" };
2088
2226
  let secretsHex = await store.load(decoded.quoteId);
2227
+ const storedSecrets = secretsHex?.length ?? 0;
2089
2228
  if (!secretsHex || cached?.quote.settlementData.$case !== "order") trace("disclosure-skipped", { gate: "context", secrets: !!secretsHex, cached: !!cached, feedStatus: order.status });
2090
2229
  if (secretsHex && cached?.quote.settlementData.$case === "order") {
2091
2230
  const [lockedTo] = await store.load(bindKey(decoded.quoteId)) ?? [];
@@ -2094,6 +2233,7 @@ function omnistonProvider(opts = {}) {
2094
2233
  secretsHex = void 0;
2095
2234
  }
2096
2235
  }
2236
+ const disclosedNow = /* @__PURE__ */ new Set();
2097
2237
  if (secretsHex && cached?.quote.settlementData.$case === "order") {
2098
2238
  const q = cached.quote;
2099
2239
  const settlement = q.settlementData.value;
@@ -2192,11 +2332,15 @@ function omnistonProvider(opts = {}) {
2192
2332
  }
2193
2333
  await omni.orderDiscloseHtlcSecret({ quoteId: decoded.quoteId, executionIndex: i, secret: secretBytes }).then(async () => {
2194
2334
  trace("secret-disclosed", { index: i });
2335
+ disclosedNow.add(i);
2195
2336
  await store.save(disclosedKey(decoded.quoteId), ["1"]);
2196
2337
  }, (error) => trace("disclose-failed", { index: i, error: error instanceof Error ? error.message : String(error) }));
2197
2338
  })
2198
2339
  );
2199
2340
  }
2341
+ const disclosedBefore = (await store.load(disclosedIndicesKey(decoded.quoteId)) ?? []).map(Number).filter(Number.isSafeInteger);
2342
+ const disclosedAll = /* @__PURE__ */ new Set([...disclosedBefore, ...disclosedNow]);
2343
+ if (disclosedNow.size > 0) await store.save(disclosedIndicesKey(decoded.quoteId), [...disclosedAll].sort((a, b) => a - b).map(String));
2200
2344
  const status = mapOrderStatus(order);
2201
2345
  const delivered = order.executions[0]?.outputPositionAddress;
2202
2346
  const outCase = cached?.quote.outputAsset.chain.$case;
@@ -2221,6 +2365,14 @@ function omnistonProvider(opts = {}) {
2221
2365
  }).catch(() => null);
2222
2366
  if (hash) status.destTxHash = hash;
2223
2367
  }
2368
+ if (status.state === "completed" && storedSecrets > 0 && !status.destTxHash) {
2369
+ const legacyDisclosed = disclosedBefore.length === 0 && storedSecrets === 1 && (await store.load(disclosedKey(decoded.quoteId)) ?? []).length > 0;
2370
+ const allDisclosed = legacyDisclosed || (order.executions.length > 0 ? order.executions.every((execution) => disclosedAll.has(execution.index)) : disclosedAll.size > 0);
2371
+ if (!allDisclosed) {
2372
+ trace("completion-unproven", { feedStatus: order.status, executions: order.executions.length, disclosed: disclosedAll.size });
2373
+ return { state: "pending", depositReceived: order.executions.length > 0 || disclosedAll.size > 0 };
2374
+ }
2375
+ }
2224
2376
  if (status.state === "completed") {
2225
2377
  await clearSecrets(decoded.quoteId);
2226
2378
  await quotes.clear(decoded.quoteId);
@@ -2273,4 +2425,4 @@ export {
2273
2425
  omnistonVerifyBuild,
2274
2426
  omnistonProvider
2275
2427
  };
2276
- //# sourceMappingURL=chunk-HOTFZYSW.js.map
2428
+ //# sourceMappingURL=chunk-2DGCZD5K.js.map