@anywalletkit/protocol-swap2-omniston 0.0.0-stage → 0.2.2
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/LICENSE +202 -0
- package/README.md +94 -2
- package/dist/activity.cjs +79 -0
- package/dist/activity.cjs.map +1 -0
- package/dist/activity.d.cts +10 -0
- package/dist/activity.d.ts +10 -0
- package/dist/activity.js +13 -0
- package/dist/activity.js.map +1 -0
- package/dist/chunk-AIWAM7BS.js +59 -0
- package/dist/chunk-AIWAM7BS.js.map +1 -0
- package/dist/chunk-K2KBWGNL.js +27 -0
- package/dist/chunk-K2KBWGNL.js.map +1 -0
- package/dist/chunk-QDXFC6BC.js +30 -0
- package/dist/chunk-QDXFC6BC.js.map +1 -0
- package/dist/chunk-W3AXGSX3.js +2148 -0
- package/dist/chunk-W3AXGSX3.js.map +1 -0
- package/dist/config.cjs +50 -0
- package/dist/config.cjs.map +1 -0
- package/dist/config.d.cts +12 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -0
- package/dist/index.cjs +2276 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.cjs +2262 -0
- package/dist/protocol.cjs.map +1 -0
- package/dist/protocol.d.cts +15 -0
- package/dist/protocol.d.ts +15 -0
- package/dist/protocol.js +10 -0
- package/dist/protocol.js.map +1 -0
- package/dist/provider.cjs +2251 -0
- package/dist/provider.cjs.map +1 -0
- package/dist/provider.d.cts +590 -0
- package/dist/provider.d.ts +590 -0
- package/dist/provider.js +23 -0
- package/dist/provider.js.map +1 -0
- package/dist/validators.cjs +1072 -0
- package/dist/validators.cjs.map +1 -0
- package/dist/validators.d.cts +199 -0
- package/dist/validators.d.ts +199 -0
- package/dist/validators.js +1065 -0
- package/dist/validators.js.map +1 -0
- package/module.json +17 -0
- package/package.json +88 -3
|
@@ -0,0 +1,2262 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/protocol.ts
|
|
31
|
+
var protocol_exports = {};
|
|
32
|
+
__export(protocol_exports, {
|
|
33
|
+
OmnistonSwap2Protocol: () => OmnistonSwap2Protocol
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(protocol_exports);
|
|
36
|
+
var import_single = require("@anywalletkit/protocol-swap2-core/swap2/single");
|
|
37
|
+
|
|
38
|
+
// src/config.ts
|
|
39
|
+
var import_util = require("@anywalletkit/protocol-swap2-core/util");
|
|
40
|
+
var OMNISTON_EVM = /* @__PURE__ */ new Set([1, 56, 137, 196, 999, 4663, 8453, 9745, 42161, 43114]);
|
|
41
|
+
var omniston = {
|
|
42
|
+
enabled: () => true,
|
|
43
|
+
integratorAddress: () => "",
|
|
44
|
+
integratorFeePips: () => "",
|
|
45
|
+
wsUrl: () => "wss://omni-ws.ston.fi",
|
|
46
|
+
/** Per-swap USD cap — the resolver quotes the corridor up to ~$1000. */
|
|
47
|
+
maxUsd: () => {
|
|
48
|
+
return 1e3;
|
|
49
|
+
},
|
|
50
|
+
supportsChain(chain) {
|
|
51
|
+
if ((0, import_util.namespace)(chain) === "ton") return true;
|
|
52
|
+
return (0, import_util.isEvm)(chain) && OMNISTON_EVM.has((0, import_util.evmRef)(chain));
|
|
53
|
+
},
|
|
54
|
+
// TON↔EVM and EVM↔EVM cross-chain (order settlement), TON↔TON same-chain
|
|
55
|
+
// (swap settlement — immediate DEX routes). Same-chain EVM is not an order
|
|
56
|
+
// corridor.
|
|
57
|
+
supports(from, to) {
|
|
58
|
+
if (!this.supportsChain(from) || !this.supportsChain(to)) return false;
|
|
59
|
+
const tonSide = (0, import_util.namespace)(from) === "ton" || (0, import_util.namespace)(to) === "ton";
|
|
60
|
+
return tonSide || from !== to;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// src/provider.ts
|
|
65
|
+
var import_core3 = require("@ton/core");
|
|
66
|
+
|
|
67
|
+
// src/activity.ts
|
|
68
|
+
var import_core = require("@ton/core");
|
|
69
|
+
var import_activity = require("@anywalletkit/protocol-swap2-core/activity");
|
|
70
|
+
var OMNISTON_MODULE = "@anywalletkit/protocol-swap2-omniston";
|
|
71
|
+
var STONFI_ROUTERS = [
|
|
72
|
+
"EQCS4UEa5UaJLzOyyKieqQOQ2P9M-7kXpkO5HnP3Bv250cN3",
|
|
73
|
+
"EQB3ncyBUTjZUA5EnFKR5_EnOMI9V1tTEAAPaiU71gc4TiUt"
|
|
74
|
+
];
|
|
75
|
+
var OMNISTON_ESCROWS = ["EQAbWJ3Y1HgIIvcMq1prG1anlDC0T3cZlAU7luPT6LmTpmrZ"];
|
|
76
|
+
function raw(address) {
|
|
77
|
+
if (!address) return null;
|
|
78
|
+
try {
|
|
79
|
+
return import_core.Address.parse(address).toRawString();
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
var ROUTER_RAW = new Set(STONFI_ROUTERS.map((a) => raw(a)));
|
|
85
|
+
var ESCROW_RAW = new Set(OMNISTON_ESCROWS.map((a) => raw(a)));
|
|
86
|
+
function counterparties(tx, own) {
|
|
87
|
+
const ownRaw = /* @__PURE__ */ new Set();
|
|
88
|
+
for (const a of own) {
|
|
89
|
+
const r = raw(a);
|
|
90
|
+
if (r) ownRaw.add(r);
|
|
91
|
+
}
|
|
92
|
+
const out = /* @__PURE__ */ new Set();
|
|
93
|
+
const add = (a) => {
|
|
94
|
+
const r = raw(a);
|
|
95
|
+
if (r && !ownRaw.has(r)) out.add(r);
|
|
96
|
+
};
|
|
97
|
+
add(tx.call?.to);
|
|
98
|
+
add(tx.to);
|
|
99
|
+
for (const t of tx.transfers) {
|
|
100
|
+
add(t.from);
|
|
101
|
+
add(t.to);
|
|
102
|
+
}
|
|
103
|
+
return [...out];
|
|
104
|
+
}
|
|
105
|
+
function describeOmnistonActivity(tx, ctx) {
|
|
106
|
+
if (!tx.chain.startsWith("ton:")) return null;
|
|
107
|
+
const parties = counterparties(tx, ctx.own);
|
|
108
|
+
const escrow = parties.find((p) => ESCROW_RAW.has(p));
|
|
109
|
+
if (escrow) {
|
|
110
|
+
return (0, import_activity.describeSwapLike)(tx, ctx, { protocol: OMNISTON_MODULE, subtitle: "Omniston \xB7 STON.fi", kind: "bridge", counterparty: tx.call?.to ?? tx.to ?? escrow });
|
|
111
|
+
}
|
|
112
|
+
const router = parties.find((p) => ROUTER_RAW.has(p));
|
|
113
|
+
if (router) {
|
|
114
|
+
return (0, import_activity.describeSwapLike)(tx, ctx, { protocol: OMNISTON_MODULE, subtitle: "STON.fi \xB7 TON", kind: "swap", counterparty: tx.call?.to ?? tx.to ?? router });
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/provider.ts
|
|
120
|
+
var import_buffer2 = require("buffer");
|
|
121
|
+
var import_viem = require("viem");
|
|
122
|
+
var import_swap2_assets = require("@anywalletkit/swap2-assets");
|
|
123
|
+
|
|
124
|
+
// src/ton-swap.ts
|
|
125
|
+
var import_buffer = require("buffer");
|
|
126
|
+
var import_core2 = require("@ton/core");
|
|
127
|
+
var TON_JETTON_TRANSFER_OP = 260734629;
|
|
128
|
+
var PTON_V2_TON_TRANSFER_OP = 32736093;
|
|
129
|
+
var STONFI_V2_SWAP_OP = 1717886506;
|
|
130
|
+
var STONFI_V1_SWAP_OP = 630424929;
|
|
131
|
+
var DEDUST_V1_NATIVE_SWAP_OP = 3926267997;
|
|
132
|
+
var DEDUST_V1_JETTON_SWAP_OP = 3818968194;
|
|
133
|
+
var DEDUST_V2_PAY_NATIVE_OP = 2779237368;
|
|
134
|
+
var DEDUST_V2_PAY_JETTON_OP = 3418569033;
|
|
135
|
+
var DEDUST_V2_SWAP_OP = 3292680207;
|
|
136
|
+
var TON_SWAP_PROTOCOL_LAYOUTS = {
|
|
137
|
+
StonFiV1: "stonfi-v1",
|
|
138
|
+
StonFiV2: "stonfi-v2",
|
|
139
|
+
DeDust: "dedust-v1",
|
|
140
|
+
DeDustCPMMV2: "dedust-v2"
|
|
141
|
+
};
|
|
142
|
+
var TonSwapPayloadError = class extends Error {
|
|
143
|
+
constructor(reason, detail) {
|
|
144
|
+
super(`omniston: ton-swap-payload ${reason}${detail ? ` (${detail})` : ""}`);
|
|
145
|
+
this.reason = reason;
|
|
146
|
+
this.name = "TonSwapPayloadError";
|
|
147
|
+
}
|
|
148
|
+
reason;
|
|
149
|
+
};
|
|
150
|
+
var refuse = (reason, detail) => {
|
|
151
|
+
throw new TonSwapPayloadError(reason, detail);
|
|
152
|
+
};
|
|
153
|
+
function sameAddr(a, b) {
|
|
154
|
+
if (!a || !b) return false;
|
|
155
|
+
try {
|
|
156
|
+
return a.equals(import_core2.Address.parse(b));
|
|
157
|
+
} catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function end(s, where) {
|
|
162
|
+
if (s.remainingBits !== 0 || s.remainingRefs !== 0) refuse("unknown-layout", `${where}: ${s.remainingBits} bits / ${s.remainingRefs} refs left over`);
|
|
163
|
+
}
|
|
164
|
+
function eitherRef(s) {
|
|
165
|
+
return s.loadBit() ? s.loadRef().beginParse() : s;
|
|
166
|
+
}
|
|
167
|
+
function readAddress(s) {
|
|
168
|
+
const a = s.loadAddressAny();
|
|
169
|
+
if (a === null) return null;
|
|
170
|
+
if (!(a instanceof import_core2.Address)) refuse("unknown-layout", "external address");
|
|
171
|
+
return a;
|
|
172
|
+
}
|
|
173
|
+
function isTraderOrNone(a, trader) {
|
|
174
|
+
return a === null || sameAddr(a, trader);
|
|
175
|
+
}
|
|
176
|
+
function receiverOk(a, dest, trader) {
|
|
177
|
+
if (a === null) {
|
|
178
|
+
try {
|
|
179
|
+
return import_core2.Address.parse(dest).equals(import_core2.Address.parse(trader));
|
|
180
|
+
} catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return sameAddr(a, dest);
|
|
185
|
+
}
|
|
186
|
+
function referralOk(address, fee, integrator) {
|
|
187
|
+
if (address === null) return true;
|
|
188
|
+
if (fee === 0) return true;
|
|
189
|
+
return !!integrator && sameAddr(address, integrator);
|
|
190
|
+
}
|
|
191
|
+
function decodeStonFiV2(s, ctx) {
|
|
192
|
+
const hops = [];
|
|
193
|
+
let cur = s;
|
|
194
|
+
for (let depth = 0; cur; depth++) {
|
|
195
|
+
if (depth >= 4) refuse("unsupported-ton-route", "STON.fi v2 cross-swap deeper than 4 hops");
|
|
196
|
+
if (cur.loadUint(32) !== STONFI_V2_SWAP_OP) refuse("unknown-layout", `STON.fi v2 hop ${depth} is not swap#6664de2a`);
|
|
197
|
+
const askWallet = readAddress(cur);
|
|
198
|
+
const refund = readAddress(cur);
|
|
199
|
+
const excess = readAddress(cur);
|
|
200
|
+
cur.loadUintBig(64);
|
|
201
|
+
const body = cur.loadRef().beginParse();
|
|
202
|
+
end(cur, "STON.fi v2 swap");
|
|
203
|
+
const minOut = body.loadCoins();
|
|
204
|
+
const receiver = readAddress(body);
|
|
205
|
+
const fwdGas = body.loadCoins();
|
|
206
|
+
const custom = body.loadMaybeRef();
|
|
207
|
+
body.loadCoins();
|
|
208
|
+
const refundPayload = body.loadMaybeRef();
|
|
209
|
+
const refFee = body.loadUint(16);
|
|
210
|
+
const refAddress = readAddress(body);
|
|
211
|
+
end(body, "STON.fi v2 cross-swap body");
|
|
212
|
+
if (!askWallet) refuse("unknown-layout", "STON.fi v2 swap without an ask wallet");
|
|
213
|
+
if (!sameAddr(refund, ctx.trader)) refuse("refund-not-trader", `STON.fi v2 hop ${depth}`);
|
|
214
|
+
if (!sameAddr(excess, ctx.trader)) refuse("excess-not-trader", `STON.fi v2 hop ${depth}`);
|
|
215
|
+
if (refundPayload) refuse("unknown-layout", "STON.fi v2 refund payload");
|
|
216
|
+
if (!referralOk(refAddress, refFee, ctx.integratorAddress)) refuse("referral", `STON.fi v2 hop ${depth}`);
|
|
217
|
+
if (custom && fwdGas === 0n) refuse("unknown-layout", "STON.fi v2 custom payload without forward gas");
|
|
218
|
+
hops.push({ askWallet, minOut, receiver, next: custom ? custom.beginParse() : null });
|
|
219
|
+
cur = custom ? custom.beginParse() : null;
|
|
220
|
+
}
|
|
221
|
+
return hops;
|
|
222
|
+
}
|
|
223
|
+
function decodeDeDustV1(s) {
|
|
224
|
+
const pool = readAddress(s);
|
|
225
|
+
if (!pool) refuse("unknown-layout", "DeDust v1 swap without a pool");
|
|
226
|
+
if (s.loadUint(1) !== 0) refuse("unknown-layout", "DeDust v1 swap kind is not given_in");
|
|
227
|
+
const limit = s.loadCoins();
|
|
228
|
+
if (s.loadMaybeRef()) refuse("unsupported-ton-route", "DeDust v1 multi-hop (next step)");
|
|
229
|
+
const params = s.loadRef().beginParse();
|
|
230
|
+
end(s, "DeDust v1 swap step");
|
|
231
|
+
params.loadUint(32);
|
|
232
|
+
const recipient = readAddress(params);
|
|
233
|
+
const referral = readAddress(params);
|
|
234
|
+
params.loadMaybeRef();
|
|
235
|
+
params.loadMaybeRef();
|
|
236
|
+
end(params, "DeDust v1 swap params");
|
|
237
|
+
return { pool, limit, recipient, referral };
|
|
238
|
+
}
|
|
239
|
+
function decodeDeDustV2(payment, payout) {
|
|
240
|
+
if (payment.loadUint(32) !== DEDUST_V2_SWAP_OP) refuse("unknown-layout", "DeDust CPMM v2 payment payload is not swap#c442500f");
|
|
241
|
+
const minOut = payment.loadCoins();
|
|
242
|
+
payment.loadUint(40);
|
|
243
|
+
if (payment.loadMaybeRef()) refuse("unsupported-ton-route", "DeDust CPMM v2 multi-hop (next step)");
|
|
244
|
+
if (payment.loadBit()) refuse("referral", "DeDust CPMM v2 partner config");
|
|
245
|
+
if (payment.loadBit()) refuse("referral", "DeDust CPMM v2 referrer config");
|
|
246
|
+
end(payment, "DeDust CPMM v2 swap payload");
|
|
247
|
+
const options = () => {
|
|
248
|
+
const destination = readAddress(payout);
|
|
249
|
+
payout.loadCoins();
|
|
250
|
+
payout.loadMaybeRef();
|
|
251
|
+
payout.loadBit();
|
|
252
|
+
return destination;
|
|
253
|
+
};
|
|
254
|
+
const fulfill = options();
|
|
255
|
+
const reject = options();
|
|
256
|
+
const excessesTo = readAddress(payout);
|
|
257
|
+
end(payout, "DeDust CPMM v2 payout config");
|
|
258
|
+
return { minOut, fulfill, reject, excessesTo };
|
|
259
|
+
}
|
|
260
|
+
function checkTonSwapPayload(ctx) {
|
|
261
|
+
if (!/^(?:[0-9a-fA-F]{2})+$/.test(ctx.payloadHex)) refuse("malformed", "payload hex");
|
|
262
|
+
const roots = import_core2.Cell.fromBoc(import_buffer.Buffer.from(ctx.payloadHex, "hex"));
|
|
263
|
+
if (roots.length !== 1) refuse("malformed", "payload must have one root");
|
|
264
|
+
const steps = ctx.steps;
|
|
265
|
+
if (steps.length === 0) refuse("unsupported-ton-route", "no route steps");
|
|
266
|
+
const layouts = steps.map((step) => TON_SWAP_PROTOCOL_LAYOUTS[step.protocol] ?? refuse("unsupported-ton-route", `protocol ${step.protocol}`));
|
|
267
|
+
const layout = layouts[0];
|
|
268
|
+
if (layouts.some((l) => l !== layout)) refuse("unsupported-ton-route", `mixed protocols ${steps.map((s2) => s2.protocol).join("+")}`);
|
|
269
|
+
if (steps.length > 1 && layout !== "stonfi-v2") refuse("unsupported-ton-route", `${steps.length}-step ${steps[0].protocol} route`);
|
|
270
|
+
for (let i = 0; i < steps.length; i++) {
|
|
271
|
+
const step = steps[i];
|
|
272
|
+
const wantIn = i === 0 ? ctx.inputJetton : steps[i - 1].outputJetton;
|
|
273
|
+
const wantOut = i === steps.length - 1 ? ctx.outputJetton : step.outputJetton;
|
|
274
|
+
if (step.inputJetton === void 0 || step.outputJetton === void 0) refuse("unsupported-ton-route", `step ${i} without assets`);
|
|
275
|
+
if (!sameJetton(step.inputJetton, wantIn ?? null) || !sameJetton(step.outputJetton, wantOut ?? null)) refuse("unsupported-ton-route", `step ${i} assets do not chain`);
|
|
276
|
+
}
|
|
277
|
+
const input = BigInt(ctx.inputUnits);
|
|
278
|
+
const quotedMin = BigInt(ctx.minOutputAmount);
|
|
279
|
+
const nativeInput = ctx.inputJetton === null;
|
|
280
|
+
const s = roots[0].beginParse();
|
|
281
|
+
const op = s.loadUint(32);
|
|
282
|
+
const legs = [];
|
|
283
|
+
const minOk = (minOut2) => {
|
|
284
|
+
if (minOut2 < quotedMin) refuse(minOut2 === 0n ? "min-out-zero" : "min-out-below-quote", `${minOut2} < ${quotedMin}`);
|
|
285
|
+
};
|
|
286
|
+
let transfer = null;
|
|
287
|
+
if (op === TON_JETTON_TRANSFER_OP) {
|
|
288
|
+
s.loadUintBig(64);
|
|
289
|
+
const amount = s.loadCoins();
|
|
290
|
+
const destination = readAddress(s);
|
|
291
|
+
const response = readAddress(s);
|
|
292
|
+
if (s.loadMaybeRef()) refuse("unknown-layout", "jetton transfer custom payload");
|
|
293
|
+
s.loadCoins();
|
|
294
|
+
const inline = !s.loadBit();
|
|
295
|
+
const forward = inline ? s : s.loadRef().beginParse();
|
|
296
|
+
if (amount !== input) refuse("amount-mismatch", "jetton transfer amount");
|
|
297
|
+
if (!destination) refuse("unknown-layout", "jetton transfer without a destination");
|
|
298
|
+
if (!sameAddr(response, ctx.trader)) refuse("excess-not-trader", "jetton transfer response destination");
|
|
299
|
+
transfer = { destination, forward, inline };
|
|
300
|
+
}
|
|
301
|
+
let minOut;
|
|
302
|
+
if (layout === "stonfi-v2") {
|
|
303
|
+
let hopsSlice;
|
|
304
|
+
let router;
|
|
305
|
+
let proxyTonWallet;
|
|
306
|
+
if (nativeInput) {
|
|
307
|
+
if (op !== PTON_V2_TON_TRANSFER_OP) refuse("unknown-layout", `native STON.fi v2 input op 0x${op.toString(16)}`);
|
|
308
|
+
s.loadUintBig(64);
|
|
309
|
+
const tonAmount = s.loadCoins();
|
|
310
|
+
const refund = readAddress(s);
|
|
311
|
+
hopsSlice = eitherRef(s);
|
|
312
|
+
end(s, "pTON v2 ton_transfer");
|
|
313
|
+
if (tonAmount !== input) refuse("amount-mismatch", "pTON v2 ton_amount");
|
|
314
|
+
if (!sameAddr(refund, ctx.trader)) refuse("refund-not-trader", "pTON v2 refund address");
|
|
315
|
+
router = null;
|
|
316
|
+
proxyTonWallet = ctx.targetAddress;
|
|
317
|
+
} else {
|
|
318
|
+
if (!transfer) refuse("unknown-layout", `jetton-input STON.fi v2 op 0x${op.toString(16)}`);
|
|
319
|
+
if (!transfer.inline) end(s, "jetton transfer");
|
|
320
|
+
hopsSlice = transfer.forward;
|
|
321
|
+
router = transfer.destination.toString();
|
|
322
|
+
}
|
|
323
|
+
const hops = decodeStonFiV2(hopsSlice, ctx);
|
|
324
|
+
if (hops.length !== steps.length) refuse("unsupported-ton-route", `${hops.length} STON.fi v2 hops for ${steps.length} quoted steps`);
|
|
325
|
+
hops.forEach((hop, i) => {
|
|
326
|
+
const last = i === hops.length - 1;
|
|
327
|
+
if (last) {
|
|
328
|
+
if (!receiverOk(hop.receiver, ctx.dest, ctx.trader) || hop.receiver === null) refuse("receiver-not-destination", `STON.fi v2 hop ${i}`);
|
|
329
|
+
if (hop.next) refuse("unknown-layout", "STON.fi v2 final hop carries a custom payload");
|
|
330
|
+
} else if (!hop.receiver) refuse("unknown-layout", `STON.fi v2 hop ${i} without a receiver`);
|
|
331
|
+
legs.push({
|
|
332
|
+
kind: "stonfi-router",
|
|
333
|
+
version: 2,
|
|
334
|
+
router: i === 0 ? router : hops[i - 1].receiver.toString(),
|
|
335
|
+
...i === 0 && proxyTonWallet ? { proxyTonWallet } : {},
|
|
336
|
+
askWallet: hop.askWallet.toString(),
|
|
337
|
+
inputJetton: steps[i].inputJetton ?? null,
|
|
338
|
+
outputJetton: steps[i].outputJetton ?? null
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
minOut = hops[hops.length - 1].minOut;
|
|
342
|
+
} else if (layout === "stonfi-v1") {
|
|
343
|
+
if (!transfer) refuse("unknown-layout", `STON.fi v1 op 0x${op.toString(16)}`);
|
|
344
|
+
if (!transfer.inline) end(s, "jetton transfer");
|
|
345
|
+
const f = transfer.forward;
|
|
346
|
+
if (f.loadUint(32) !== STONFI_V1_SWAP_OP) refuse("unknown-layout", "STON.fi v1 forward payload is not swap#25938561");
|
|
347
|
+
const askWallet = readAddress(f);
|
|
348
|
+
minOut = f.loadCoins();
|
|
349
|
+
const to = readAddress(f);
|
|
350
|
+
const referral = f.loadBit() ? readAddress(f) : null;
|
|
351
|
+
end(f, "STON.fi v1 swap");
|
|
352
|
+
if (!askWallet) refuse("unknown-layout", "STON.fi v1 swap without an ask wallet");
|
|
353
|
+
if (!to || !sameAddr(to, ctx.dest)) refuse("receiver-not-destination", "STON.fi v1 to_address");
|
|
354
|
+
if (!referralOk(referral, null, ctx.integratorAddress)) refuse("referral", "STON.fi v1");
|
|
355
|
+
legs.push({
|
|
356
|
+
kind: "stonfi-router",
|
|
357
|
+
version: 1,
|
|
358
|
+
router: transfer.destination.toString(),
|
|
359
|
+
...nativeInput ? { proxyTonWallet: ctx.targetAddress } : {},
|
|
360
|
+
askWallet: askWallet.toString(),
|
|
361
|
+
inputJetton: ctx.inputJetton,
|
|
362
|
+
outputJetton: ctx.outputJetton
|
|
363
|
+
});
|
|
364
|
+
} else if (layout === "dedust-v1") {
|
|
365
|
+
let swap;
|
|
366
|
+
if (nativeInput) {
|
|
367
|
+
if (op !== DEDUST_V1_NATIVE_SWAP_OP) refuse("unknown-layout", `native DeDust v1 input op 0x${op.toString(16)}`);
|
|
368
|
+
s.loadUintBig(64);
|
|
369
|
+
const amount = s.loadCoins();
|
|
370
|
+
if (amount !== input) refuse("amount-mismatch", "DeDust v1 native vault amount");
|
|
371
|
+
swap = decodeDeDustV1(s);
|
|
372
|
+
} else {
|
|
373
|
+
if (!transfer) refuse("unknown-layout", `jetton-input DeDust v1 op 0x${op.toString(16)}`);
|
|
374
|
+
if (!transfer.inline) end(s, "jetton transfer");
|
|
375
|
+
const f = transfer.forward;
|
|
376
|
+
if (f.loadUint(32) !== DEDUST_V1_JETTON_SWAP_OP) refuse("unknown-layout", "DeDust v1 forward payload is not swap#e3a0d482");
|
|
377
|
+
swap = decodeDeDustV1(f);
|
|
378
|
+
}
|
|
379
|
+
if (!receiverOk(swap.recipient, ctx.dest, ctx.trader)) refuse("receiver-not-destination", "DeDust v1 recipient");
|
|
380
|
+
if (!referralOk(swap.referral, null, ctx.integratorAddress)) refuse("referral", "DeDust v1");
|
|
381
|
+
minOut = swap.limit;
|
|
382
|
+
legs.push({ kind: "dedust-pool", version: 1, pool: swap.pool.toString(), inputJetton: ctx.inputJetton, outputJetton: ctx.outputJetton });
|
|
383
|
+
} else {
|
|
384
|
+
let swap;
|
|
385
|
+
let pool;
|
|
386
|
+
if (nativeInput) {
|
|
387
|
+
if (op !== DEDUST_V2_PAY_NATIVE_OP) refuse("unknown-layout", `native DeDust CPMM v2 input op 0x${op.toString(16)}`);
|
|
388
|
+
s.loadUintBig(64);
|
|
389
|
+
const amount = s.loadCoins();
|
|
390
|
+
const payment = s.loadRef().beginParse();
|
|
391
|
+
const payout = s.loadRef().beginParse();
|
|
392
|
+
end(s, "DeDust CPMM v2 pay_native");
|
|
393
|
+
if (amount !== input) refuse("amount-mismatch", "DeDust CPMM v2 pay_native amount");
|
|
394
|
+
swap = decodeDeDustV2(payment, payout);
|
|
395
|
+
pool = ctx.targetAddress;
|
|
396
|
+
} else {
|
|
397
|
+
if (!transfer) refuse("unknown-layout", `jetton-input DeDust CPMM v2 op 0x${op.toString(16)}`);
|
|
398
|
+
if (!transfer.inline) end(s, "jetton transfer");
|
|
399
|
+
const f = transfer.forward;
|
|
400
|
+
if (f.loadUint(32) !== DEDUST_V2_PAY_JETTON_OP) refuse("unknown-layout", "DeDust CPMM v2 forward payload is not pay_jetton#cbc33949");
|
|
401
|
+
const payment = f.loadRef().beginParse();
|
|
402
|
+
const payout = f.loadRef().beginParse();
|
|
403
|
+
end(f, "DeDust CPMM v2 pay_jetton");
|
|
404
|
+
swap = decodeDeDustV2(payment, payout);
|
|
405
|
+
pool = transfer.destination.toString();
|
|
406
|
+
}
|
|
407
|
+
if (!receiverOk(swap.fulfill, ctx.dest, ctx.trader)) refuse("receiver-not-destination", "DeDust CPMM v2 fulfill destination");
|
|
408
|
+
if (!isTraderOrNone(swap.reject, ctx.trader)) refuse("refund-not-trader", "DeDust CPMM v2 reject destination");
|
|
409
|
+
if (!isTraderOrNone(swap.excessesTo, ctx.trader)) refuse("excess-not-trader", "DeDust CPMM v2 excesses");
|
|
410
|
+
minOut = swap.minOut;
|
|
411
|
+
legs.push({ kind: "dedust-pool", version: 2, pool, inputJetton: ctx.inputJetton, outputJetton: ctx.outputJetton });
|
|
412
|
+
}
|
|
413
|
+
minOk(minOut);
|
|
414
|
+
return { layout, minOut, legs };
|
|
415
|
+
}
|
|
416
|
+
function sameJetton(a, b) {
|
|
417
|
+
if (a === null || b === null) return a === b;
|
|
418
|
+
try {
|
|
419
|
+
return import_core2.Address.parse(a).equals(import_core2.Address.parse(b));
|
|
420
|
+
} catch {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// src/provider.ts
|
|
426
|
+
var EVM_CASE = {
|
|
427
|
+
1: "ethereum",
|
|
428
|
+
56: "bnb",
|
|
429
|
+
137: "polygon",
|
|
430
|
+
196: "xlayer",
|
|
431
|
+
999: "hyperevm",
|
|
432
|
+
4663: "robinhood",
|
|
433
|
+
8453: "base",
|
|
434
|
+
9745: "plasma",
|
|
435
|
+
42161: "arbitrum",
|
|
436
|
+
43114: "avalanche"
|
|
437
|
+
};
|
|
438
|
+
var TON_NS = "ton";
|
|
439
|
+
var INTENT_PREFIX = "omn1";
|
|
440
|
+
var QUOTE_SETTLE_MS = 3e3;
|
|
441
|
+
var EXECUTION_SETTLE_MS = 600;
|
|
442
|
+
var MIN_START_HEADROOM_MS = 12e3;
|
|
443
|
+
var QUOTE_MAX_MS = 6500;
|
|
444
|
+
var TRACK_SNAPSHOT_MS = 6e3;
|
|
445
|
+
var DEFAULT_MAX_HTLC_EXECUTIONS = 1;
|
|
446
|
+
var QUOTE_TTL_MS = 45e3;
|
|
447
|
+
var CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
448
|
+
var LEGACY_INTENT_WINDOW_MS = 6 * 60 * 60 * 1e3;
|
|
449
|
+
function inMemoryQuoteStore() {
|
|
450
|
+
const map = /* @__PURE__ */ new Map();
|
|
451
|
+
return {
|
|
452
|
+
save: (id, v) => void map.set(id, v),
|
|
453
|
+
load: (id) => map.get(id),
|
|
454
|
+
clear: (id) => void map.delete(id)
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function inMemorySecretStore() {
|
|
458
|
+
const map = /* @__PURE__ */ new Map();
|
|
459
|
+
return {
|
|
460
|
+
save: (id, secrets) => void map.set(id, secrets),
|
|
461
|
+
load: (id) => map.get(id),
|
|
462
|
+
clear: (id) => void map.delete(id)
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function evmRef2(caip2) {
|
|
466
|
+
if (!caip2.startsWith("eip155:")) return null;
|
|
467
|
+
const n = Number(caip2.slice("eip155:".length));
|
|
468
|
+
return Number.isInteger(n) ? n : null;
|
|
469
|
+
}
|
|
470
|
+
function isTon(caip2) {
|
|
471
|
+
return caip2.split(":")[0] === TON_NS;
|
|
472
|
+
}
|
|
473
|
+
function caipForChainCase($case) {
|
|
474
|
+
if ($case === "ton") return "ton:-239";
|
|
475
|
+
const entry = Object.entries(EVM_CASE).find(([, value]) => value === $case);
|
|
476
|
+
return entry ? `eip155:${entry[0]}` : null;
|
|
477
|
+
}
|
|
478
|
+
function toAssetId(caip2, asset, allowNative = false) {
|
|
479
|
+
if (!asset.contract) {
|
|
480
|
+
return allowNative && isTon(caip2) ? { chain: { $case: "ton", value: { kind: { $case: "native", value: {} } } } } : null;
|
|
481
|
+
}
|
|
482
|
+
if (isTon(caip2)) {
|
|
483
|
+
return { chain: { $case: "ton", value: { kind: { $case: "jetton", value: asset.contract } } } };
|
|
484
|
+
}
|
|
485
|
+
const ref = evmRef2(caip2);
|
|
486
|
+
const $case = ref === null ? void 0 : EVM_CASE[ref];
|
|
487
|
+
if (!$case) return null;
|
|
488
|
+
return { chain: { $case, value: { kind: { $case: "erc20", value: (0, import_viem.getAddress)(asset.contract) } } } };
|
|
489
|
+
}
|
|
490
|
+
function toChainAddress(caip2, address) {
|
|
491
|
+
if (isTon(caip2)) return { chain: { $case: "ton", value: address } };
|
|
492
|
+
const ref = evmRef2(caip2);
|
|
493
|
+
const $case = ref === null ? void 0 : EVM_CASE[ref];
|
|
494
|
+
if (!$case) return null;
|
|
495
|
+
return { chain: { $case, value: (0, import_viem.getAddress)(address) } };
|
|
496
|
+
}
|
|
497
|
+
function encodeIntent(quoteId, trader, dest, srcChain, kind = "order") {
|
|
498
|
+
return [INTENT_PREFIX, quoteId, trader, dest, srcChain, ...kind === "swap" ? ["swap"] : []].map(encodeURIComponent).join("|");
|
|
499
|
+
}
|
|
500
|
+
var QUOTE_ID = /^[0-9a-fA-F]{48,64}$/;
|
|
501
|
+
function decodeIntent(intentId) {
|
|
502
|
+
try {
|
|
503
|
+
const parts = intentId.split("|");
|
|
504
|
+
if (parts.length !== 5 && parts.length !== 6 || parts[0] !== INTENT_PREFIX) return null;
|
|
505
|
+
const [, quoteId = "", trader = "", dest = "", srcChain = "", kindPart = ""] = parts.map(decodeURIComponent);
|
|
506
|
+
if (!QUOTE_ID.test(quoteId) || !trader || !dest || !srcChain) return null;
|
|
507
|
+
if (parts.length === 6 && kindPart !== "swap") return null;
|
|
508
|
+
return { quoteId, trader, dest, srcChain, kind: parts.length === 6 ? "swap" : "order" };
|
|
509
|
+
} catch {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
function sameIntentAddress(a, b) {
|
|
514
|
+
if (a === b) return true;
|
|
515
|
+
const evm = /^0x[0-9a-fA-F]{40}$/;
|
|
516
|
+
if (evm.test(a) || evm.test(b)) return evm.test(a) && evm.test(b) && a.toLowerCase() === b.toLowerCase();
|
|
517
|
+
try {
|
|
518
|
+
return import_core3.Address.parse(a).equals(import_core3.Address.parse(b));
|
|
519
|
+
} catch {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function encodeTypedMessage(td) {
|
|
524
|
+
const paramFor = (typeName, name) => {
|
|
525
|
+
const isArray = typeName.endsWith("[]");
|
|
526
|
+
const base = isArray ? typeName.slice(0, -2) : typeName;
|
|
527
|
+
const fields = td.types[base];
|
|
528
|
+
if (!fields) return { type: typeName, ...name ? { name } : {} };
|
|
529
|
+
return {
|
|
530
|
+
type: isArray ? "tuple[]" : "tuple",
|
|
531
|
+
...name ? { name } : {},
|
|
532
|
+
components: fields.map((f) => paramFor(f.type, f.name))
|
|
533
|
+
};
|
|
534
|
+
};
|
|
535
|
+
return (0, import_viem.hexToBytes)(
|
|
536
|
+
(0, import_viem.encodeAbiParameters)([paramFor(td.primaryType)], [td.message])
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
function generateSecret() {
|
|
540
|
+
return crypto.getRandomValues(new Uint8Array(32));
|
|
541
|
+
}
|
|
542
|
+
function hashlock(secret, hashingFunction) {
|
|
543
|
+
switch (hashingFunction) {
|
|
544
|
+
case "HASHING_FUNCTION_KECCAK256":
|
|
545
|
+
return (0, import_viem.keccak256)(secret, "bytes");
|
|
546
|
+
case "HASHING_FUNCTION_SHA256":
|
|
547
|
+
return (0, import_viem.sha256)(secret, "bytes");
|
|
548
|
+
default:
|
|
549
|
+
throw new Error(`omniston: unsupported HTLC hashing function ${String(hashingFunction)}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function sameAsset(a, b) {
|
|
553
|
+
const key = (asset) => {
|
|
554
|
+
const chain = asset.chain;
|
|
555
|
+
const value = chain.value;
|
|
556
|
+
const normalize = (text) => chain.$case === "ton" ? text : text.toLowerCase();
|
|
557
|
+
if (typeof value === "string") return `${chain.$case}:${normalize(value)}`;
|
|
558
|
+
const kind = value.kind;
|
|
559
|
+
return `${chain.$case}:${kind?.$case ?? ""}:${typeof kind?.value === "string" ? normalize(kind.value) : JSON.stringify(kind?.value ?? null)}`;
|
|
560
|
+
};
|
|
561
|
+
return key(a) === key(b);
|
|
562
|
+
}
|
|
563
|
+
function sameChainAddress(a, b) {
|
|
564
|
+
if (a === void 0 || b === void 0) return a === b;
|
|
565
|
+
if (a.chain.$case !== b.chain.$case) return false;
|
|
566
|
+
if (a.chain.$case !== "ton") return a.chain.value.toLowerCase() === b.chain.value.toLowerCase();
|
|
567
|
+
try {
|
|
568
|
+
return import_core3.Address.parse(a.chain.value).equals(import_core3.Address.parse(b.chain.value));
|
|
569
|
+
} catch {
|
|
570
|
+
return a.chain.value === b.chain.value;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function assetContract(asset) {
|
|
574
|
+
const value = asset.chain.value;
|
|
575
|
+
if (typeof value === "string") return null;
|
|
576
|
+
const kind = value.kind;
|
|
577
|
+
return kind && typeof kind.value === "string" ? kind.value : null;
|
|
578
|
+
}
|
|
579
|
+
function stepJetton(asset) {
|
|
580
|
+
if (!asset || asset.chain.$case !== "ton" || typeof asset.chain.value === "string") return void 0;
|
|
581
|
+
const kind = asset.chain.value.kind;
|
|
582
|
+
if (kind?.$case === "native") return null;
|
|
583
|
+
return kind?.$case === "jetton" && typeof kind.value === "string" ? kind.value : void 0;
|
|
584
|
+
}
|
|
585
|
+
function readBigEndian(bytes, offset, length) {
|
|
586
|
+
if (!Number.isInteger(offset) || !Number.isInteger(length) || length < 1 || offset < 0 || offset + length > bytes.length) {
|
|
587
|
+
throw new Error("omniston: truncated EVM order extension");
|
|
588
|
+
}
|
|
589
|
+
let value = 0n;
|
|
590
|
+
for (let index = offset; index < offset + length; index++) value = value << 8n | BigInt(bytes[index]);
|
|
591
|
+
return value;
|
|
592
|
+
}
|
|
593
|
+
function sliceEqual(a, b) {
|
|
594
|
+
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
595
|
+
}
|
|
596
|
+
function zeroBytes(bytes) {
|
|
597
|
+
return bytes.every((value) => value === 0);
|
|
598
|
+
}
|
|
599
|
+
function within(value, center, before, after) {
|
|
600
|
+
return value >= BigInt(center - before) && value <= BigInt(center + after);
|
|
601
|
+
}
|
|
602
|
+
var ORDER_FIELDS = [
|
|
603
|
+
["salt", "uint256"],
|
|
604
|
+
["maker", "address"],
|
|
605
|
+
["receiver", "address"],
|
|
606
|
+
["makerAsset", "address"],
|
|
607
|
+
["takerAsset", "address"],
|
|
608
|
+
["makingAmount", "uint256"],
|
|
609
|
+
["takingAmount", "uint256"],
|
|
610
|
+
["makerTraits", "uint256"]
|
|
611
|
+
];
|
|
612
|
+
function validUint256(value) {
|
|
613
|
+
try {
|
|
614
|
+
const n = BigInt(value);
|
|
615
|
+
return n >= 0n && n < 1n << 256n;
|
|
616
|
+
} catch {
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function validEvmAddress(value) {
|
|
621
|
+
try {
|
|
622
|
+
(0, import_viem.getAddress)(String(value));
|
|
623
|
+
return true;
|
|
624
|
+
} catch {
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
var EVM_DEST_PROTOCOL_FEE_RECIPIENTS = /* @__PURE__ */ new Set(["0x076268146b4ec6d21fc49c27586cfa91b9079a00"]);
|
|
629
|
+
async function validateEvmToTonOrderExtension(extension, expected) {
|
|
630
|
+
const evmDest = expected.dstChainId !== void 0;
|
|
631
|
+
if (evmDest && (!Number.isSafeInteger(expected.dstChainId) || expected.dstChainId <= 0)) throw new Error("omniston: invalid EVM destination chain id");
|
|
632
|
+
if (extension.length < 32) throw new Error("omniston: malformed EVM order extension");
|
|
633
|
+
const view = new DataView(extension.buffer, extension.byteOffset, extension.byteLength);
|
|
634
|
+
const fields = [];
|
|
635
|
+
let begin = 0;
|
|
636
|
+
const concatLength = extension.length - 32;
|
|
637
|
+
for (let index = 0; index < 8; index++) {
|
|
638
|
+
const end2 = view.getUint32(28 - index * 4, false);
|
|
639
|
+
if (end2 < begin || end2 > concatLength) throw new Error("omniston: malformed EVM order extension offsets");
|
|
640
|
+
fields.push(extension.slice(32 + begin, 32 + end2));
|
|
641
|
+
begin = end2;
|
|
642
|
+
}
|
|
643
|
+
for (const index of [0, 1, 4, 5, 6]) {
|
|
644
|
+
if (fields[index].length !== 0) throw new Error("omniston: unsupported EVM order extension field");
|
|
645
|
+
}
|
|
646
|
+
if (!sliceEqual(fields[2], fields[3])) {
|
|
647
|
+
throw new Error("omniston: making/taking amount getters use different programs");
|
|
648
|
+
}
|
|
649
|
+
let target;
|
|
650
|
+
for (const index of [2, 3, 7]) {
|
|
651
|
+
const field = fields[index];
|
|
652
|
+
if (field.length <= 20) throw new Error("omniston: incomplete EVM order extension program");
|
|
653
|
+
const address = (0, import_viem.getAddress)(`0x${bytesToHex(field.slice(0, 20))}`);
|
|
654
|
+
if (!expected.trustedTarget(address) || target !== void 0 && (0, import_viem.getAddress)(target) !== address) {
|
|
655
|
+
throw new Error("omniston: EVM order extension target is not independently trusted");
|
|
656
|
+
}
|
|
657
|
+
target = address;
|
|
658
|
+
}
|
|
659
|
+
const getter = fields[2];
|
|
660
|
+
let cursor = 20;
|
|
661
|
+
const gasBump = readBigEndian(getter, cursor, 3);
|
|
662
|
+
cursor += 3;
|
|
663
|
+
const gasPrice = readBigEndian(getter, cursor, 4);
|
|
664
|
+
cursor += 4;
|
|
665
|
+
const auctionStart = readBigEndian(getter, cursor, 4);
|
|
666
|
+
cursor += 4;
|
|
667
|
+
const duration = readBigEndian(getter, cursor, 3);
|
|
668
|
+
cursor += 3;
|
|
669
|
+
const initialRateBump = readBigEndian(getter, cursor, 3);
|
|
670
|
+
cursor += 3;
|
|
671
|
+
const pointCount = Number(readBigEndian(getter, cursor, 1));
|
|
672
|
+
cursor += 1;
|
|
673
|
+
if (pointCount !== 5) throw new Error("omniston: unsupported EVM auction profile");
|
|
674
|
+
const points = [];
|
|
675
|
+
for (let index = 0; index < pointCount; index++) {
|
|
676
|
+
points.push([readBigEndian(getter, cursor, 3), readBigEndian(getter, cursor + 3, 2)]);
|
|
677
|
+
cursor += 5;
|
|
678
|
+
}
|
|
679
|
+
const integratorFee = readBigEndian(getter, cursor, 2);
|
|
680
|
+
cursor += 2;
|
|
681
|
+
const integratorShare = readBigEndian(getter, cursor, 1);
|
|
682
|
+
cursor += 1;
|
|
683
|
+
const resolverFee = readBigEndian(getter, cursor, 2);
|
|
684
|
+
cursor += 2;
|
|
685
|
+
const whitelistDiscount = readBigEndian(getter, cursor, 1);
|
|
686
|
+
cursor += 1;
|
|
687
|
+
const getterWhitelistCount = Number(readBigEndian(getter, cursor, 1));
|
|
688
|
+
cursor += 1;
|
|
689
|
+
if (getterWhitelistCount !== 1 || cursor + getterWhitelistCount * 10 !== getter.length) {
|
|
690
|
+
throw new Error("omniston: malformed EVM amount-getter whitelist");
|
|
691
|
+
}
|
|
692
|
+
const getterResolver = readBigEndian(getter, cursor, 10);
|
|
693
|
+
const expectedPoints = [[800000n, 100n], [700000n, 100n], [600000n, 100n], [500000n, 100n], [400000n, 100n]];
|
|
694
|
+
const quoteResolverFee = expected.protocolFeePips % 10 === 0 ? BigInt(expected.protocolFeePips / 10) : -1n;
|
|
695
|
+
const resolverLow80 = BigInt((0, import_viem.getAddress)(expected.initialResolverAddress)) & (1n << 80n) - 1n;
|
|
696
|
+
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) {
|
|
697
|
+
throw new Error("omniston: EVM amount-getter terms do not match the pinned v1beta8 profile");
|
|
698
|
+
}
|
|
699
|
+
const post = fields[7];
|
|
700
|
+
cursor = 20;
|
|
701
|
+
const integratorRecipient = post.slice(cursor, cursor + 20);
|
|
702
|
+
cursor += 20;
|
|
703
|
+
const protocolRecipient = post.slice(cursor, cursor + 20);
|
|
704
|
+
cursor += 20;
|
|
705
|
+
const postIntegratorFee = readBigEndian(post, cursor, 2);
|
|
706
|
+
cursor += 2;
|
|
707
|
+
const postIntegratorShare = readBigEndian(post, cursor, 1);
|
|
708
|
+
cursor += 1;
|
|
709
|
+
const postResolverFee = readBigEndian(post, cursor, 2);
|
|
710
|
+
cursor += 2;
|
|
711
|
+
const postDiscount = readBigEndian(post, cursor, 1);
|
|
712
|
+
cursor += 1;
|
|
713
|
+
const allowedTime = readBigEndian(post, cursor, 4);
|
|
714
|
+
cursor += 4;
|
|
715
|
+
const whitelistCount = Number(readBigEndian(post, cursor, 1));
|
|
716
|
+
cursor += 1;
|
|
717
|
+
if (whitelistCount !== 1 || cursor + whitelistCount * 12 + 160 !== post.length) {
|
|
718
|
+
throw new Error("omniston: malformed EVM post-interaction whitelist");
|
|
719
|
+
}
|
|
720
|
+
const postResolver = readBigEndian(post, cursor, 10);
|
|
721
|
+
cursor += 10;
|
|
722
|
+
const resolverTimeDelta = readBigEndian(post, cursor, 2);
|
|
723
|
+
cursor += 2;
|
|
724
|
+
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) {
|
|
725
|
+
throw new Error("omniston: EVM post-interaction terms do not match the pinned v1beta8 profile");
|
|
726
|
+
}
|
|
727
|
+
const extra = Array.from({ length: 5 }, () => {
|
|
728
|
+
const value = readBigEndian(post, cursor, 32);
|
|
729
|
+
cursor += 32;
|
|
730
|
+
return value;
|
|
731
|
+
});
|
|
732
|
+
const hashlock2 = readBigEndian(expected.secretHash, 0, expected.secretHash.length);
|
|
733
|
+
const dstToken = evmDest ? BigInt((0, import_viem.getAddress)(expected.outputTokenAddress)) : BigInt((0, import_viem.getAddress)(expected.takerAsset));
|
|
734
|
+
const deposits = BigInt(expected.srcSecurityDepositUnits) << 128n | BigInt(expected.dstSecurityDepositUnits);
|
|
735
|
+
const timelocks = [1n, 120n, 240n, 300n, 1n, 120n, 180n, 0n].reduce((packed, value, index) => packed | value << BigInt(index * 32), 0n);
|
|
736
|
+
if (expected.secretHash.length !== 32 || extra[0] !== hashlock2 || extra[1] !== (evmDest ? BigInt(expected.dstChainId) : 0n) || extra[2] !== dstToken || extra[3] !== deposits || extra[4] !== timelocks) {
|
|
737
|
+
throw new Error("omniston: signed EVM HTLC terms do not match the validated order details");
|
|
738
|
+
}
|
|
739
|
+
const customData = extension.slice(32 + begin);
|
|
740
|
+
if (customData.length !== 131) throw new Error("omniston: malformed EVM order extension custom data");
|
|
741
|
+
if (evmDest) {
|
|
742
|
+
if (!zeroBytes(customData.slice(0, 66))) throw new Error("omniston: unexpected TON addresses in an EVM-destination order");
|
|
743
|
+
} else {
|
|
744
|
+
const { Address: Address3 } = await import("@ton/core");
|
|
745
|
+
const expectedTon = [expected.outputTokenAddress, expected.traderDstAddress].map((value) => Address3.parse(value));
|
|
746
|
+
for (let index = 0; index < expectedTon.length; index++) {
|
|
747
|
+
const offset = index * 33;
|
|
748
|
+
const workchain = customData[offset] === 255 ? -1 : customData[offset];
|
|
749
|
+
if (workchain !== expectedTon[index].workChain || !sliceEqual(customData.slice(offset + 1, offset + 33), expectedTon[index].hash)) {
|
|
750
|
+
throw new Error("omniston: signed EVM\u2192TON address does not match the validated request");
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
if (!zeroBytes(customData.slice(66, 99))) {
|
|
755
|
+
throw new Error("omniston: unexpected private TON disclosure address");
|
|
756
|
+
}
|
|
757
|
+
const quoteId = expected.quoteId.replace(/^0x/, "");
|
|
758
|
+
if (!QUOTE_ID.test(quoteId) || !sliceEqual(customData.slice(99), hexToBytes(quoteId.padStart(64, "0")))) {
|
|
759
|
+
throw new Error("omniston: EVM order extension custom data is not bound to the quote id");
|
|
760
|
+
}
|
|
761
|
+
if (expected.tradeStartDeadline < expected.quoteTimestamp) {
|
|
762
|
+
throw new Error("omniston: invalid EVM order quote deadline");
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
function validateEvmTypedOrder(td, expected) {
|
|
766
|
+
const value = td;
|
|
767
|
+
const order = value.types?.Order;
|
|
768
|
+
const exactSchema = Array.isArray(order) && order.length === ORDER_FIELDS.length && order.every((field, i) => field.name === ORDER_FIELDS[i][0] && field.type === ORDER_FIELDS[i][1]);
|
|
769
|
+
const message = value.message;
|
|
770
|
+
const salt = message?.salt;
|
|
771
|
+
const traits = validUint256(message?.makerTraits) ? BigInt(message.makerTraits) : -1n;
|
|
772
|
+
const flagMask = (1n << 56n) - 1n << 200n;
|
|
773
|
+
const baseFlags = 1n << 251n | 1n << 249n;
|
|
774
|
+
const flags = traits < 0n ? -1n : traits & flagMask;
|
|
775
|
+
const flagsOk = flags === baseFlags || flags === (baseFlags | 1n << 255n);
|
|
776
|
+
const allowedSender = traits & (1n << 80n) - 1n;
|
|
777
|
+
const expiration = traits >> 80n & (1n << 40n) - 1n;
|
|
778
|
+
const series = traits >> 160n & (1n << 40n) - 1n;
|
|
779
|
+
const extensionHash = BigInt((0, import_viem.keccak256)(expected.orderExtension));
|
|
780
|
+
const exactMessage = !!message && Object.keys(message).length === ORDER_FIELDS.length && ORDER_FIELDS.every(([name]) => Object.hasOwn(message, name));
|
|
781
|
+
if (value.domain?.name !== "1inch Limit Order Protocol" || value.domain.version !== "4" || typeof value.domain.verifyingContract !== "string" || (0, import_viem.getAddress)(value.domain.verifyingContract) !== (0, import_viem.getAddress)(expected.settlementContract) || Number(value.domain.chainId) !== expected.chainId || value.primaryType !== "Order" || !exactSchema || !message || !exactMessage || (0, import_viem.getAddress)(String(message.maker)) !== (0, import_viem.getAddress)(expected.owner) || (0, import_viem.getAddress)(String(message.receiver)) !== (0, import_viem.getAddress)(expected.receiver) || (0, import_viem.getAddress)(String(message.makerAsset)) !== (0, import_viem.getAddress)(expected.makerAsset) || !validUint256(salt) || (BigInt(salt) & (1n << 160n) - 1n) !== (extensionHash & (1n << 160n) - 1n) || // The order expires at the quote's trade-start deadline (±15 s of clock
|
|
782
|
+
// skew between the quote and the order builder), never later.
|
|
783
|
+
traits < 0n || !flagsOk || allowedSender !== 0n || series !== 0n || !within(expiration, expected.tradeStartDeadline, 15, 15) || expiration <= BigInt(expected.quoteTimestamp) || !validEvmAddress(message.takerAsset) || (0, import_viem.getAddress)(String(message.takerAsset)) === (0, import_viem.getAddress)(expected.makerAsset) || !validUint256(message.makingAmount) || BigInt(message.makingAmount) !== BigInt(expected.makingAmount) || !validUint256(message.takingAmount) || BigInt(message.takingAmount) !== BigInt(expected.takingAmount)) {
|
|
784
|
+
throw new Error("omniston: EVM order fields do not match the validated quote/request");
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
async function validateTonEscrowPayload(message, expected, validateForwardPayload) {
|
|
788
|
+
const { Address: Address3, Cell: Cell2 } = await import("@ton/core");
|
|
789
|
+
const boc = hexToBytes(message.payload);
|
|
790
|
+
let roots;
|
|
791
|
+
try {
|
|
792
|
+
roots = Cell2.fromBoc(import_buffer2.Buffer.from(boc));
|
|
793
|
+
} catch {
|
|
794
|
+
throw new Error("omniston: malformed TON escrow BoC");
|
|
795
|
+
}
|
|
796
|
+
if (roots.length !== 1) throw new Error("omniston: TON escrow must contain exactly one root cell");
|
|
797
|
+
const slice = roots[0].beginParse();
|
|
798
|
+
const op = slice.loadUint(32);
|
|
799
|
+
slice.loadUintBig(64);
|
|
800
|
+
const amount = slice.loadCoins();
|
|
801
|
+
const destination = slice.loadAddressAny();
|
|
802
|
+
const response = slice.loadAddressAny();
|
|
803
|
+
const customPayload = slice.loadMaybeRef();
|
|
804
|
+
const forwardAmount = slice.loadCoins();
|
|
805
|
+
const payloadByRef = slice.loadBit();
|
|
806
|
+
if (op !== 260734629 || amount !== BigInt(expected.inputUnits) || !(destination instanceof Address3) || !destination.equals(Address3.parse(expected.protocolAddress)) || response !== null && (!(response instanceof Address3) || !response.equals(Address3.parse(expected.ownerAddress))) || customPayload !== null || forwardAmount <= 0n || forwardAmount > BigInt(message.sendAmount) || !payloadByRef || slice.remainingBits !== 0 || slice.remainingRefs !== 1) {
|
|
807
|
+
throw new Error("omniston: TON jetton transfer does not match the validated quote/request");
|
|
808
|
+
}
|
|
809
|
+
const forwardPayload = slice.loadRef();
|
|
810
|
+
const verified = await validateForwardPayload({
|
|
811
|
+
payloadBocHex: message.payload,
|
|
812
|
+
forwardPayloadBocHex: import_buffer2.Buffer.from(forwardPayload.toBoc()).toString("hex"),
|
|
813
|
+
quoteId: expected.quoteId,
|
|
814
|
+
inputUnits: expected.inputUnits,
|
|
815
|
+
resolverSendsUnits: expected.resolverSendsUnits,
|
|
816
|
+
srcHtlcSecurityDepositUnits: expected.srcHtlcSecurityDepositUnits,
|
|
817
|
+
protocolAddress: expected.protocolAddress,
|
|
818
|
+
ownerAddress: expected.ownerAddress,
|
|
819
|
+
destinationChain: expected.destinationChain,
|
|
820
|
+
destinationAddress: expected.destinationAddress,
|
|
821
|
+
outputTokenAddress: expected.outputTokenAddress,
|
|
822
|
+
initialResolverAddress: expected.initialResolverAddress,
|
|
823
|
+
secretHashes: expected.hashes.map((hash) => hash.slice()),
|
|
824
|
+
...expected.hashingFunction ? { hashingFunction: expected.hashingFunction } : {},
|
|
825
|
+
exclusivityTimeout: expected.exclusivityTimeout
|
|
826
|
+
});
|
|
827
|
+
if (!verified) throw new Error("omniston: TON escrow forward payload failed independent semantic validation");
|
|
828
|
+
}
|
|
829
|
+
function feeToken(asset) {
|
|
830
|
+
return asset.assetId || asset.contract || asset.symbol;
|
|
831
|
+
}
|
|
832
|
+
function chainCase(asset) {
|
|
833
|
+
return asset.chain.$case;
|
|
834
|
+
}
|
|
835
|
+
function bytesEqual(a, b) {
|
|
836
|
+
if (a.length !== b.length) return false;
|
|
837
|
+
let diff = 0;
|
|
838
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
839
|
+
return diff === 0;
|
|
840
|
+
}
|
|
841
|
+
var ProtoReader = class {
|
|
842
|
+
constructor(bytes) {
|
|
843
|
+
this.bytes = bytes;
|
|
844
|
+
}
|
|
845
|
+
bytes;
|
|
846
|
+
pos = 0;
|
|
847
|
+
get done() {
|
|
848
|
+
return this.pos === this.bytes.length;
|
|
849
|
+
}
|
|
850
|
+
varint() {
|
|
851
|
+
let out = 0;
|
|
852
|
+
let shift = 0;
|
|
853
|
+
while (this.pos < this.bytes.length && shift <= 49) {
|
|
854
|
+
const byte = this.bytes[this.pos++];
|
|
855
|
+
out += (byte & 127) * 2 ** shift;
|
|
856
|
+
if ((byte & 128) === 0) {
|
|
857
|
+
if (!Number.isSafeInteger(out)) throw new Error("omniston: unsafe protobuf varint");
|
|
858
|
+
return out;
|
|
859
|
+
}
|
|
860
|
+
shift += 7;
|
|
861
|
+
}
|
|
862
|
+
throw new Error("omniston: malformed protobuf varint");
|
|
863
|
+
}
|
|
864
|
+
field() {
|
|
865
|
+
const tag = this.varint();
|
|
866
|
+
const number = Math.floor(tag / 8);
|
|
867
|
+
const wire = tag & 7;
|
|
868
|
+
if (number < 1) throw new Error("omniston: malformed protobuf field");
|
|
869
|
+
return { number, wire };
|
|
870
|
+
}
|
|
871
|
+
lengthDelimited() {
|
|
872
|
+
const length = this.varint();
|
|
873
|
+
const end2 = this.pos + length;
|
|
874
|
+
if (length < 0 || end2 > this.bytes.length) throw new Error("omniston: truncated protobuf payload");
|
|
875
|
+
const value = this.bytes.slice(this.pos, end2);
|
|
876
|
+
this.pos = end2;
|
|
877
|
+
return value;
|
|
878
|
+
}
|
|
879
|
+
skip(wire) {
|
|
880
|
+
if (wire === 0) {
|
|
881
|
+
this.varint();
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
if (wire === 1) {
|
|
885
|
+
this.advance(8);
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
if (wire === 2) {
|
|
889
|
+
this.lengthDelimited();
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
if (wire === 5) {
|
|
893
|
+
this.advance(4);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
throw new Error("omniston: unsupported protobuf wire type");
|
|
897
|
+
}
|
|
898
|
+
advance(length) {
|
|
899
|
+
if (this.pos + length > this.bytes.length) throw new Error("omniston: truncated protobuf payload");
|
|
900
|
+
this.pos += length;
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
var utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
904
|
+
var PROTO_CASE_BY_FIELD = {
|
|
905
|
+
1: "arbitrum",
|
|
906
|
+
2: "avalanche",
|
|
907
|
+
3: "base",
|
|
908
|
+
4: "bnb",
|
|
909
|
+
5: "ethereum",
|
|
910
|
+
6: "polygon",
|
|
911
|
+
7: "robinhood",
|
|
912
|
+
8: "xlayer",
|
|
913
|
+
9: "arc",
|
|
914
|
+
10: "monad",
|
|
915
|
+
11: "plasma",
|
|
916
|
+
12: "hyperevm",
|
|
917
|
+
50: "ton",
|
|
918
|
+
60: "tron"
|
|
919
|
+
};
|
|
920
|
+
function decodeChainAddress(bytes) {
|
|
921
|
+
const reader = new ProtoReader(bytes);
|
|
922
|
+
let address;
|
|
923
|
+
while (!reader.done) {
|
|
924
|
+
const { number, wire } = reader.field();
|
|
925
|
+
const $case = PROTO_CASE_BY_FIELD[number];
|
|
926
|
+
if (!$case || wire !== 2 || address) throw new Error("omniston: malformed chain address in order details");
|
|
927
|
+
address = { chain: { $case, value: utf8.decode(reader.lengthDelimited()) } };
|
|
928
|
+
}
|
|
929
|
+
if (!address?.chain.value) throw new Error("omniston: empty chain address in order details");
|
|
930
|
+
return address;
|
|
931
|
+
}
|
|
932
|
+
function decodeAssetId(bytes) {
|
|
933
|
+
const reader = new ProtoReader(bytes);
|
|
934
|
+
let asset;
|
|
935
|
+
while (!reader.done) {
|
|
936
|
+
const { number, wire } = reader.field();
|
|
937
|
+
const $case = PROTO_CASE_BY_FIELD[number];
|
|
938
|
+
if (!$case || wire !== 2 || asset) throw new Error("omniston: malformed asset id in order details");
|
|
939
|
+
const kindReader = new ProtoReader(reader.lengthDelimited());
|
|
940
|
+
const kindField = kindReader.field();
|
|
941
|
+
if (kindField.wire !== 2 || kindField.number !== 1 && kindField.number !== 2) {
|
|
942
|
+
throw new Error("omniston: unsupported asset kind in order details");
|
|
943
|
+
}
|
|
944
|
+
const kindCase = kindField.number === 1 ? "native" : $case === "ton" ? "jetton" : $case === "tron" ? "trc20" : "erc20";
|
|
945
|
+
const value = kindField.number === 1 ? {} : utf8.decode(kindReader.lengthDelimited());
|
|
946
|
+
if (!kindReader.done || kindField.number === 2 && !value) throw new Error("omniston: malformed asset kind in order details");
|
|
947
|
+
asset = { chain: { $case, value: { kind: { $case: kindCase, value } } } };
|
|
948
|
+
}
|
|
949
|
+
if (!asset) throw new Error("omniston: empty asset id in order details");
|
|
950
|
+
return asset;
|
|
951
|
+
}
|
|
952
|
+
function validateSerializedOrderDetails(bytes, expected) {
|
|
953
|
+
if (!bytes?.length) throw new Error("omniston: EVM HTLC order payload has no serialized details");
|
|
954
|
+
const wrapper = new ProtoReader(bytes);
|
|
955
|
+
const outer = wrapper.field();
|
|
956
|
+
if (outer.number !== 1 || outer.wire !== 2) throw new Error("omniston: unsupported HTLC order-details version");
|
|
957
|
+
const detailsBytes = wrapper.lengthDelimited();
|
|
958
|
+
if (!wrapper.done) throw new Error("omniston: ambiguous HTLC order-details wrapper");
|
|
959
|
+
const reader = new ProtoReader(detailsBytes);
|
|
960
|
+
let quoteId;
|
|
961
|
+
let outputAsset;
|
|
962
|
+
let traderDstAddress;
|
|
963
|
+
const hashes = [];
|
|
964
|
+
while (!reader.done) {
|
|
965
|
+
const { number, wire } = reader.field();
|
|
966
|
+
if (wire !== 2) throw new Error("omniston: malformed HTLC order details");
|
|
967
|
+
const value = reader.lengthDelimited();
|
|
968
|
+
if (number === 1 && quoteId === void 0) quoteId = utf8.decode(value);
|
|
969
|
+
else if (number === 2 && outputAsset === void 0) outputAsset = decodeAssetId(value);
|
|
970
|
+
else if (number === 3 && traderDstAddress === void 0) traderDstAddress = decodeChainAddress(value);
|
|
971
|
+
else if (number === 5) hashes.push(value);
|
|
972
|
+
else throw new Error("omniston: duplicate or unexpected HTLC order-details field");
|
|
973
|
+
}
|
|
974
|
+
if (quoteId !== expected.quoteId || !outputAsset || !sameAsset(outputAsset, expected.outputAsset) || !traderDstAddress || !sameChainAddress(traderDstAddress, expected.traderDstAddress) || hashes.length !== expected.secretHashes.length || hashes.some((hash, i) => !bytesEqual(hash, expected.secretHashes[i]))) {
|
|
975
|
+
throw new Error("omniston: resolver order payload does not match the validated quote/request");
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
function isPositiveInteger(value) {
|
|
979
|
+
try {
|
|
980
|
+
return BigInt(value) > 0n;
|
|
981
|
+
} catch {
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
function isNonNegativeInteger(value) {
|
|
986
|
+
if (value === void 0) return true;
|
|
987
|
+
return /^[0-9]+$/.test(value);
|
|
988
|
+
}
|
|
989
|
+
function decimalToUnits(value, decimals) {
|
|
990
|
+
const match = value.match(/^(\d+)(?:\.(\d+))?$/);
|
|
991
|
+
if (!match || !Number.isInteger(decimals) || decimals < 0) return null;
|
|
992
|
+
const fraction = match[2] ?? "";
|
|
993
|
+
if (fraction.length > decimals) return null;
|
|
994
|
+
const units = BigInt(match[1]) * 10n ** BigInt(decimals) + BigInt((fraction + "0".repeat(decimals)).slice(0, decimals) || "0");
|
|
995
|
+
return units > 0n ? units : null;
|
|
996
|
+
}
|
|
997
|
+
function validDisclosurePhase(phase) {
|
|
998
|
+
return phase === "EXECUTION_PHASE_READY_FOR_PRIVATE_COMPLETION" || phase === "EXECUTION_PHASE_READY_FOR_PUBLIC_COMPLETION" || phase === "OUTPUT_LOCKED";
|
|
999
|
+
}
|
|
1000
|
+
function bytesToHex(bytes) {
|
|
1001
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
1002
|
+
}
|
|
1003
|
+
function hexToBytes(hex) {
|
|
1004
|
+
const clean = hex.replace(/^0x/, "");
|
|
1005
|
+
if (!clean || !/^(?:[0-9a-fA-F]{2})+$/.test(clean)) {
|
|
1006
|
+
throw new Error("omniston: malformed hex payload");
|
|
1007
|
+
}
|
|
1008
|
+
const out = new Uint8Array(clean.length / 2);
|
|
1009
|
+
for (let i = 0; i < out.length; i++) out[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
1010
|
+
return out;
|
|
1011
|
+
}
|
|
1012
|
+
function hexToBase64(hex) {
|
|
1013
|
+
const bytes = hexToBytes(hex);
|
|
1014
|
+
let bin = "";
|
|
1015
|
+
bytes.forEach((b) => bin += String.fromCharCode(b));
|
|
1016
|
+
return btoa(bin);
|
|
1017
|
+
}
|
|
1018
|
+
function validOrderSettlement(q) {
|
|
1019
|
+
if (q.settlementData?.$case !== "order") return false;
|
|
1020
|
+
const v = q.settlementData.value;
|
|
1021
|
+
return !!v.srcProtocolContractAddress && v.srcProtocolContractAddress.chain.$case === chainCase(q.inputAsset) && !!v.srcInitialResolverAddress && v.srcInitialResolverAddress.chain.$case === chainCase(q.inputAsset) && !!v.dstProtocolContractAddress && v.dstProtocolContractAddress.chain.$case === chainCase(q.outputAsset) && isPositiveInteger(v.srcHtlcSecurityDepositUnits ?? "") && isPositiveInteger(v.dstSecurityDepositUnits ?? "") && (v.htlcHashingFunction === "HASHING_FUNCTION_KECCAK256" || v.htlcHashingFunction === "HASHING_FUNCTION_SHA256") && Number.isSafeInteger(v.exclusivityTimeout) && v.exclusivityTimeout > 0 && isPositiveInteger(v.resolverSendsUnits) && BigInt(v.resolverSendsUnits) === BigInt(q.outputUnits) + BigInt(q.protocolFeeUnits || "0") + BigInt(q.integratorFeeUnits || "0");
|
|
1022
|
+
}
|
|
1023
|
+
function validSwapSettlement(q) {
|
|
1024
|
+
if (q.settlementData?.$case !== "swap") return false;
|
|
1025
|
+
const v = q.settlementData.value;
|
|
1026
|
+
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));
|
|
1027
|
+
}
|
|
1028
|
+
function swapProtocols(v) {
|
|
1029
|
+
const names = /* @__PURE__ */ new Set();
|
|
1030
|
+
for (const route of v.routes) for (const step of route.steps) for (const chunk of step.chunks) names.add(chunk.protocol);
|
|
1031
|
+
return [...names];
|
|
1032
|
+
}
|
|
1033
|
+
function quoteRejection(q, update, acknowledgedRfqId, request, settlement) {
|
|
1034
|
+
if (!q) return "empty quote";
|
|
1035
|
+
if (!acknowledgedRfqId || update.rfqId !== acknowledgedRfqId || q.rfqId !== acknowledgedRfqId) return "quote for another RFQ";
|
|
1036
|
+
if (!sameAsset(q.inputAsset, request.inputAsset) || !sameAsset(q.outputAsset, request.outputAsset)) return "quote assets differ from the request";
|
|
1037
|
+
const requestedAmount = request.amount.$case === "inputUnits" ? request.amount.value : "";
|
|
1038
|
+
if (q.inputUnits !== requestedAmount) return "quote input differs from the request";
|
|
1039
|
+
if (!QUOTE_ID.test(q.quoteId)) return "malformed quote id";
|
|
1040
|
+
if (!isPositiveInteger(q.inputUnits) || !isPositiveInteger(q.outputUnits)) return "non-positive quote amounts";
|
|
1041
|
+
if (!isNonNegativeInteger(q.protocolFeeUnits) || !isNonNegativeInteger(q.integratorFeeUnits) || BigInt(q.protocolFeeUnits || "0") + BigInt(q.integratorFeeUnits || "0") > BigInt(q.outputUnits)) return "malformed quote fees";
|
|
1042
|
+
if (!Number.isInteger(q.quoteTimestamp) || Math.abs(Math.floor(Date.now() / 1e3) - q.quoteTimestamp) > 120) return "quote timestamp out of range";
|
|
1043
|
+
if (settlement === "order" ? !validOrderSettlement(q) : !validSwapSettlement(q)) return `incomplete ${settlement} settlement data`;
|
|
1044
|
+
if (!sameChainAddress(q.integratorAddress, request.integratorAddress)) return "quote integrator differs from the request";
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
function awaitQuote(stream, request, signal, settleMs = QUOTE_SETTLE_MS, settlement = "order", outcome = { updates: 0 }) {
|
|
1048
|
+
return new Promise((resolve) => {
|
|
1049
|
+
let latest = null;
|
|
1050
|
+
let done = false;
|
|
1051
|
+
let sub = null;
|
|
1052
|
+
let acknowledgedRfqId = null;
|
|
1053
|
+
const onAbort = () => finish(null);
|
|
1054
|
+
const finish = (value) => {
|
|
1055
|
+
if (done) return;
|
|
1056
|
+
done = true;
|
|
1057
|
+
clearTimeout(settle);
|
|
1058
|
+
clearTimeout(max);
|
|
1059
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1060
|
+
try {
|
|
1061
|
+
sub?.unsubscribe();
|
|
1062
|
+
} catch {
|
|
1063
|
+
}
|
|
1064
|
+
resolve(value);
|
|
1065
|
+
};
|
|
1066
|
+
const settle = setTimeout(() => {
|
|
1067
|
+
if (latest) finish(latest);
|
|
1068
|
+
}, settleMs);
|
|
1069
|
+
const max = setTimeout(() => finish(latest), QUOTE_MAX_MS);
|
|
1070
|
+
sub = stream.subscribe({
|
|
1071
|
+
next: (event) => {
|
|
1072
|
+
if (event.$case === "ack") {
|
|
1073
|
+
acknowledgedRfqId = event.value.rfqId || null;
|
|
1074
|
+
} else if (event.$case === "quoteUpdated") {
|
|
1075
|
+
const update = event;
|
|
1076
|
+
outcome.updates += 1;
|
|
1077
|
+
const rejection = quoteRejection(update.value, update, acknowledgedRfqId, request, settlement);
|
|
1078
|
+
if (rejection) {
|
|
1079
|
+
outcome.rejected = rejection;
|
|
1080
|
+
latest = null;
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
latest = update.value;
|
|
1084
|
+
} else if (event.$case === "noQuote") {
|
|
1085
|
+
const noQuote = event;
|
|
1086
|
+
if (acknowledgedRfqId && noQuote.rfqId === acknowledgedRfqId) latest = null;
|
|
1087
|
+
}
|
|
1088
|
+
},
|
|
1089
|
+
error: (err) => {
|
|
1090
|
+
outcome.error = String(err?.message ?? err).slice(0, 200);
|
|
1091
|
+
finish(null);
|
|
1092
|
+
},
|
|
1093
|
+
complete: () => finish(latest)
|
|
1094
|
+
});
|
|
1095
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1096
|
+
if (done) sub.unsubscribe();
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
function awaitSwapProgress(stream) {
|
|
1100
|
+
return new Promise((resolve) => {
|
|
1101
|
+
let done = false;
|
|
1102
|
+
let latest = null;
|
|
1103
|
+
let sub = null;
|
|
1104
|
+
const finish = (value) => {
|
|
1105
|
+
if (done) return;
|
|
1106
|
+
done = true;
|
|
1107
|
+
clearTimeout(timer);
|
|
1108
|
+
try {
|
|
1109
|
+
sub?.unsubscribe();
|
|
1110
|
+
} catch {
|
|
1111
|
+
}
|
|
1112
|
+
resolve(value);
|
|
1113
|
+
};
|
|
1114
|
+
const timer = setTimeout(() => finish(latest), TRACK_SNAPSHOT_MS);
|
|
1115
|
+
sub = stream.subscribe({
|
|
1116
|
+
next: (event) => {
|
|
1117
|
+
if (event.$case === "progress") {
|
|
1118
|
+
latest = event.value;
|
|
1119
|
+
if (["TRADE_STATUS_FULLY_FILLED", "TRADE_STATUS_PARTIALLY_FILLED", "TRADE_STATUS_CANCELLED", "TRADE_STATUS_FAILED"].includes(latest.status)) finish(latest);
|
|
1120
|
+
} else if (event.$case === "awaitingTransfer" && latest === null) {
|
|
1121
|
+
latest = "awaiting";
|
|
1122
|
+
}
|
|
1123
|
+
},
|
|
1124
|
+
error: () => finish(null),
|
|
1125
|
+
complete: () => finish(latest)
|
|
1126
|
+
});
|
|
1127
|
+
if (done) sub.unsubscribe();
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
var JETTON_TRANSFER_OP = 260734629;
|
|
1131
|
+
function parseSwapPayload(payloadHex) {
|
|
1132
|
+
if (!/^(?:[0-9a-fA-F]{2})+$/.test(payloadHex)) throw new Error("omniston: malformed swap payload hex");
|
|
1133
|
+
const roots = import_core3.Cell.fromBoc(import_buffer2.Buffer.from(payloadHex, "hex"));
|
|
1134
|
+
if (roots.length !== 1) throw new Error("omniston: swap payload must have one root");
|
|
1135
|
+
const s = roots[0].beginParse();
|
|
1136
|
+
const op = s.loadUint(32);
|
|
1137
|
+
if (op !== JETTON_TRANSFER_OP) return { op, jettonTransfer: null };
|
|
1138
|
+
s.loadUintBig(64);
|
|
1139
|
+
const amount = s.loadCoins();
|
|
1140
|
+
const destination = s.loadAddress();
|
|
1141
|
+
const responseDestination = s.loadMaybeAddress();
|
|
1142
|
+
s.loadMaybeRef();
|
|
1143
|
+
const forwardTon = s.loadCoins();
|
|
1144
|
+
return {
|
|
1145
|
+
op,
|
|
1146
|
+
jettonTransfer: {
|
|
1147
|
+
amount,
|
|
1148
|
+
destination: destination.toString(),
|
|
1149
|
+
responseDestination: responseDestination ? responseDestination.toString() : null,
|
|
1150
|
+
forwardTon
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
function sameTonAddress(a, b) {
|
|
1155
|
+
try {
|
|
1156
|
+
return import_core3.Address.parse(a).equals(import_core3.Address.parse(b));
|
|
1157
|
+
} catch {
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
function awaitOrder(stream) {
|
|
1162
|
+
return new Promise((resolve) => {
|
|
1163
|
+
let done = false;
|
|
1164
|
+
let latest = null;
|
|
1165
|
+
let sub = null;
|
|
1166
|
+
const finish = (value) => {
|
|
1167
|
+
if (done) return;
|
|
1168
|
+
done = true;
|
|
1169
|
+
clearTimeout(timer);
|
|
1170
|
+
try {
|
|
1171
|
+
sub?.unsubscribe();
|
|
1172
|
+
} catch {
|
|
1173
|
+
}
|
|
1174
|
+
resolve(value);
|
|
1175
|
+
};
|
|
1176
|
+
const timer = setTimeout(() => finish(latest), TRACK_SNAPSHOT_MS);
|
|
1177
|
+
sub = stream.subscribe({
|
|
1178
|
+
next: (event) => {
|
|
1179
|
+
if (event.$case === "order") {
|
|
1180
|
+
latest = event.value;
|
|
1181
|
+
if (["TRADE_STATUS_FULLY_FILLED", "TRADE_STATUS_CANCELLED", "TRADE_STATUS_FAILED"].includes(latest.status)) finish(latest);
|
|
1182
|
+
}
|
|
1183
|
+
},
|
|
1184
|
+
error: () => finish(null),
|
|
1185
|
+
complete: () => finish(latest)
|
|
1186
|
+
});
|
|
1187
|
+
if (done) sub.unsubscribe();
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
function mapOrderStatus(order) {
|
|
1191
|
+
const depositReceived = order.executions.length > 0;
|
|
1192
|
+
switch (order.status) {
|
|
1193
|
+
case "TRADE_STATUS_FULLY_FILLED":
|
|
1194
|
+
return { state: "completed", depositReceived: true };
|
|
1195
|
+
case "TRADE_STATUS_PARTIALLY_FILLED":
|
|
1196
|
+
return { state: "pending", depositReceived: true };
|
|
1197
|
+
case "TRADE_STATUS_CANCELLED":
|
|
1198
|
+
case "TRADE_STATUS_FAILED":
|
|
1199
|
+
return { state: "pending", depositReceived };
|
|
1200
|
+
default:
|
|
1201
|
+
return { state: "pending", depositReceived };
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
function omnistonProvider(opts = {}) {
|
|
1205
|
+
opts = Object.freeze({ ...opts });
|
|
1206
|
+
const apiUrl = opts.apiUrl ?? "wss://omni-ws.ston.fi";
|
|
1207
|
+
const parsedApiUrl = new URL(apiUrl);
|
|
1208
|
+
if (parsedApiUrl.protocol !== "wss:" && parsedApiUrl.protocol !== "ws:") {
|
|
1209
|
+
throw new Error("omniston: apiUrl must use ws or wss");
|
|
1210
|
+
}
|
|
1211
|
+
const maxUsdText = String(opts.maxUsd ?? 1e3);
|
|
1212
|
+
if (!/^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(maxUsdText) || Number(maxUsdText) <= 0 || !Number.isFinite(Number(maxUsdText))) {
|
|
1213
|
+
throw new Error("omniston: maxUsd must be a positive plain decimal");
|
|
1214
|
+
}
|
|
1215
|
+
const hasIntegrator = opts.integratorAddress !== void 0;
|
|
1216
|
+
const hasIntegratorPips = opts.integratorFeePips !== void 0;
|
|
1217
|
+
if (hasIntegrator !== hasIntegratorPips) throw new Error("omniston: integratorAddress and integratorFeePips must be configured together");
|
|
1218
|
+
if (hasIntegrator && (!opts.integratorAddress.trim() || !Number.isInteger(opts.integratorFeePips) || opts.integratorFeePips < 0 || opts.integratorFeePips > 1e6)) {
|
|
1219
|
+
throw new Error("omniston: integrator fee config is invalid");
|
|
1220
|
+
}
|
|
1221
|
+
const trustedContracts = /* @__PURE__ */ new Map();
|
|
1222
|
+
for (const [chain, addresses] of Object.entries(opts.trustedSettlementContracts ?? {})) {
|
|
1223
|
+
const normalized = /* @__PURE__ */ new Set();
|
|
1224
|
+
for (const address of addresses) {
|
|
1225
|
+
try {
|
|
1226
|
+
normalized.add(isTon(chain) ? address : (0, import_viem.getAddress)(address).toLowerCase());
|
|
1227
|
+
} catch {
|
|
1228
|
+
throw new Error(`omniston: invalid trusted settlement contract for ${chain}`);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
if (normalized.size) trustedContracts.set(chain, normalized);
|
|
1232
|
+
}
|
|
1233
|
+
const isTrusted = (chain, address) => {
|
|
1234
|
+
try {
|
|
1235
|
+
return trustedContracts.get(chain)?.has(isTon(chain) ? address : (0, import_viem.getAddress)(address).toLowerCase()) ?? false;
|
|
1236
|
+
} catch {
|
|
1237
|
+
return false;
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
const trustedOrderProtocols = /* @__PURE__ */ new Map();
|
|
1241
|
+
for (const [chain, addresses] of Object.entries(opts.trustedEvmOrderProtocols ?? {})) {
|
|
1242
|
+
const normalized = /* @__PURE__ */ new Set();
|
|
1243
|
+
for (const address of addresses) {
|
|
1244
|
+
try {
|
|
1245
|
+
if (evmRef2(chain) === null) throw new Error("not an EVM chain");
|
|
1246
|
+
normalized.add((0, import_viem.getAddress)(address).toLowerCase());
|
|
1247
|
+
} catch {
|
|
1248
|
+
throw new Error(`omniston: invalid trusted EVM order protocol for ${chain}`);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (normalized.size) trustedOrderProtocols.set(chain, normalized);
|
|
1252
|
+
}
|
|
1253
|
+
const isTrustedOrderProtocol = (chain, address) => {
|
|
1254
|
+
try {
|
|
1255
|
+
return trustedOrderProtocols.get(chain)?.has((0, import_viem.getAddress)(address).toLowerCase()) ?? false;
|
|
1256
|
+
} catch {
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
const store = opts.secretStore ?? inMemorySecretStore();
|
|
1261
|
+
const requestedMaxExecutions = opts.maxHtlcExecutions ?? DEFAULT_MAX_HTLC_EXECUTIONS;
|
|
1262
|
+
if (!Number.isFinite(requestedMaxExecutions) || !Number.isInteger(requestedMaxExecutions) || requestedMaxExecutions < 1) {
|
|
1263
|
+
throw new Error("omniston: maxHtlcExecutions must be a positive integer");
|
|
1264
|
+
}
|
|
1265
|
+
const maxExecutions = Math.min(16, requestedMaxExecutions);
|
|
1266
|
+
const maxTonExecutionNano = opts.maxTonExecutionNano ?? "2000000000";
|
|
1267
|
+
if (!isPositiveInteger(maxTonExecutionNano)) {
|
|
1268
|
+
throw new Error("omniston: maxTonExecutionNano must be a positive integer");
|
|
1269
|
+
}
|
|
1270
|
+
const quoteSettleMs = opts.quoteSettleMs ?? QUOTE_SETTLE_MS;
|
|
1271
|
+
if (!Number.isInteger(quoteSettleMs) || quoteSettleMs < 0 || quoteSettleMs >= QUOTE_MAX_MS) {
|
|
1272
|
+
throw new Error(`omniston: quoteSettleMs must be an integer in [0, ${QUOTE_MAX_MS})`);
|
|
1273
|
+
}
|
|
1274
|
+
const injectedClient = opts.client ?? null;
|
|
1275
|
+
let client = injectedClient;
|
|
1276
|
+
let clientPromise = null;
|
|
1277
|
+
let clientGeneration = 0;
|
|
1278
|
+
async function getClient() {
|
|
1279
|
+
if (client) return client;
|
|
1280
|
+
if (clientPromise) return clientPromise;
|
|
1281
|
+
const generation = clientGeneration;
|
|
1282
|
+
clientPromise = (async () => {
|
|
1283
|
+
const sdk = await import("@ston-fi/omniston-sdk");
|
|
1284
|
+
const webSocketTransport = new sdk.WebSocketTransport(apiUrl);
|
|
1285
|
+
const connect = webSocketTransport.connect.bind(webSocketTransport);
|
|
1286
|
+
webSocketTransport.connect = () => {
|
|
1287
|
+
const pending = connect();
|
|
1288
|
+
const socket = webSocketTransport.webSocket;
|
|
1289
|
+
socket?.addEventListener("error", () => {
|
|
1290
|
+
});
|
|
1291
|
+
return pending;
|
|
1292
|
+
};
|
|
1293
|
+
webSocketTransport.reconnect = () => webSocketTransport.connect();
|
|
1294
|
+
const messages = webSocketTransport.messagesSubject;
|
|
1295
|
+
if (messages) {
|
|
1296
|
+
const deliver = messages.next.bind(messages);
|
|
1297
|
+
messages.next = (m) => {
|
|
1298
|
+
setTimeout(() => deliver(m), 0);
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
const transport = new sdk.AutoReconnectTransport({ transport: webSocketTransport });
|
|
1302
|
+
const created = new sdk.Omniston({ apiUrl, transport });
|
|
1303
|
+
if (generation !== clientGeneration) {
|
|
1304
|
+
if (created.close) created.close();
|
|
1305
|
+
else created.transport?.close();
|
|
1306
|
+
throw new Error("omniston: provider closed while client was being created");
|
|
1307
|
+
}
|
|
1308
|
+
client = created;
|
|
1309
|
+
return created;
|
|
1310
|
+
})();
|
|
1311
|
+
try {
|
|
1312
|
+
return await clientPromise;
|
|
1313
|
+
} finally {
|
|
1314
|
+
clientPromise = null;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
const quotes = opts.quoteStore ?? inMemoryQuoteStore();
|
|
1318
|
+
async function saveBuilt(quoteId, entry) {
|
|
1319
|
+
await quotes.save(quoteId, JSON.stringify(entry));
|
|
1320
|
+
}
|
|
1321
|
+
async function loadBuilt(quoteId) {
|
|
1322
|
+
const raw2 = await quotes.load(quoteId);
|
|
1323
|
+
if (!raw2) return void 0;
|
|
1324
|
+
try {
|
|
1325
|
+
const e = JSON.parse(raw2);
|
|
1326
|
+
if (!Number.isFinite(e.at) || Date.now() - e.at > CACHE_TTL_MS) {
|
|
1327
|
+
await quotes.clear(quoteId);
|
|
1328
|
+
return void 0;
|
|
1329
|
+
}
|
|
1330
|
+
return e;
|
|
1331
|
+
} catch {
|
|
1332
|
+
await quotes.clear(quoteId);
|
|
1333
|
+
return void 0;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
function bindIntent(decoded, entry) {
|
|
1337
|
+
const owner = entry.evmOrder?.owner;
|
|
1338
|
+
const bound = entry.bound;
|
|
1339
|
+
if (bound) {
|
|
1340
|
+
if (bound.kind !== decoded.kind) return { ok: false, reason: "mismatch", field: "kind" };
|
|
1341
|
+
if (bound.srcChain !== decoded.srcChain) return { ok: false, reason: "mismatch", field: "srcChain" };
|
|
1342
|
+
if (!sameIntentAddress(bound.trader, decoded.trader)) return { ok: false, reason: "mismatch", field: "trader" };
|
|
1343
|
+
if (!sameIntentAddress(bound.dest, decoded.dest)) return { ok: false, reason: "mismatch", field: "dest" };
|
|
1344
|
+
if (owner !== void 0 && !sameIntentAddress(owner, bound.trader)) return { ok: false, reason: "mismatch", field: "owner" };
|
|
1345
|
+
return { ok: true, legacy: false, ref: { quoteId: decoded.quoteId, trader: owner ?? bound.trader, dest: bound.dest, srcChain: bound.srcChain, kind: bound.kind } };
|
|
1346
|
+
}
|
|
1347
|
+
if (!Number.isFinite(entry.at) || Date.now() - entry.at > LEGACY_INTENT_WINDOW_MS) return { ok: false, reason: "legacy-expired" };
|
|
1348
|
+
if (owner !== void 0 && !sameIntentAddress(owner, decoded.trader)) return { ok: false, reason: "mismatch", field: "owner" };
|
|
1349
|
+
return { ok: true, legacy: true, ref: { ...decoded, trader: owner ?? decoded.trader } };
|
|
1350
|
+
}
|
|
1351
|
+
const traceFor = (quoteId) => (event, details) => {
|
|
1352
|
+
try {
|
|
1353
|
+
opts.trace?.(event, { quoteId, ...details });
|
|
1354
|
+
} catch {
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
function requireBinding(decoded, entry, where) {
|
|
1358
|
+
const trace = traceFor(decoded.quoteId);
|
|
1359
|
+
const bound = bindIntent(decoded, entry);
|
|
1360
|
+
if (!bound.ok) {
|
|
1361
|
+
if (bound.reason === "legacy-expired") {
|
|
1362
|
+
trace("legacy-intent-expired", { where });
|
|
1363
|
+
throw new Error("omniston: quote expired \u2014 request a fresh quote");
|
|
1364
|
+
}
|
|
1365
|
+
trace("intent-mismatch", { where, field: bound.field });
|
|
1366
|
+
throw new Error(`omniston: intent id does not match the quote it was issued for (${bound.field})`);
|
|
1367
|
+
}
|
|
1368
|
+
if (bound.legacy) trace("legacy-intent", { where });
|
|
1369
|
+
return bound.ref;
|
|
1370
|
+
}
|
|
1371
|
+
const bindKey = (quoteId) => `${quoteId}#bind`;
|
|
1372
|
+
const disclosedKey = (quoteId) => `${quoteId}#disclosed`;
|
|
1373
|
+
function orderBinding(ref, q) {
|
|
1374
|
+
return (0, import_viem.sha256)(new TextEncoder().encode(JSON.stringify([
|
|
1375
|
+
ref.quoteId,
|
|
1376
|
+
ref.trader ?? null,
|
|
1377
|
+
ref.dest ?? null,
|
|
1378
|
+
ref.srcChain ?? null,
|
|
1379
|
+
q.inputUnits,
|
|
1380
|
+
q.outputUnits,
|
|
1381
|
+
JSON.stringify(q.inputAsset),
|
|
1382
|
+
JSON.stringify(q.outputAsset)
|
|
1383
|
+
]))).slice(2);
|
|
1384
|
+
}
|
|
1385
|
+
async function clearSecrets(quoteId) {
|
|
1386
|
+
await store.clear(quoteId);
|
|
1387
|
+
await store.clear(bindKey(quoteId));
|
|
1388
|
+
await store.clear(disclosedKey(quoteId));
|
|
1389
|
+
}
|
|
1390
|
+
async function secretsFor(ref, q, count) {
|
|
1391
|
+
const quoteId = ref.quoteId;
|
|
1392
|
+
const binding = orderBinding(ref, q);
|
|
1393
|
+
const existing = await store.load(quoteId);
|
|
1394
|
+
if (existing && existing.length > 0) {
|
|
1395
|
+
const [boundTo] = await store.load(bindKey(quoteId)) ?? [];
|
|
1396
|
+
const disclosed = (await store.load(disclosedKey(quoteId)) ?? []).length > 0;
|
|
1397
|
+
if (boundTo !== binding || disclosed) {
|
|
1398
|
+
traceFor(quoteId)("secret-reuse-refused", { disclosed, bound: boundTo !== void 0 });
|
|
1399
|
+
throw new Error("omniston: this quote id already belongs to another order \u2014 request a fresh quote");
|
|
1400
|
+
}
|
|
1401
|
+
if (existing.length !== count || !existing.every((s) => /^[0-9a-fA-F]{64}$/.test(s))) {
|
|
1402
|
+
throw new Error("omniston: stored HTLC secrets for this quote do not fit the order \u2014 request a fresh quote");
|
|
1403
|
+
}
|
|
1404
|
+
traceFor(quoteId)("secret-reused", { count });
|
|
1405
|
+
return { secrets: existing.map(hexToBytes), created: false };
|
|
1406
|
+
}
|
|
1407
|
+
const secrets = Array.from({ length: count }, generateSecret);
|
|
1408
|
+
await store.save(bindKey(quoteId), [binding]);
|
|
1409
|
+
await store.save(quoteId, secrets.map(bytesToHex));
|
|
1410
|
+
return { secrets, created: true };
|
|
1411
|
+
}
|
|
1412
|
+
async function quoteIdInUse(quoteId) {
|
|
1413
|
+
const existing = await store.load(quoteId);
|
|
1414
|
+
return !!existing && existing.length > 0;
|
|
1415
|
+
}
|
|
1416
|
+
const SWAP_SLIPPAGE_PIPS_DEFAULT = 5e3;
|
|
1417
|
+
const sameChainTonReady = () => typeof opts.validateTonJettonWallet === "function" && typeof opts.validateTonSwapTarget === "function";
|
|
1418
|
+
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";
|
|
1419
|
+
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";
|
|
1420
|
+
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";
|
|
1421
|
+
async function getSwapQuote(params, signal) {
|
|
1422
|
+
if (!omniston.supports(params.sourceChain, params.destChain) || !trustedContracts.has(params.sourceChain) || !sameChainTonReady()) return null;
|
|
1423
|
+
const source = (0, import_swap2_assets.ccAsset)(params.sourceChain, params.sourceToken);
|
|
1424
|
+
const dest = (0, import_swap2_assets.ccAssetByToken)(params.destChain, params.destToken);
|
|
1425
|
+
if (!source || !dest || source.symbol === dest.symbol) return null;
|
|
1426
|
+
const inputAsset = toAssetId(params.sourceChain, source, true);
|
|
1427
|
+
const outputAsset = toAssetId(params.destChain, dest, true);
|
|
1428
|
+
if (!inputAsset || !outputAsset || sameAsset(inputAsset, outputAsset) || !isPositiveInteger(params.amountIn)) return null;
|
|
1429
|
+
const trader = params.refundAddress ?? "";
|
|
1430
|
+
if (!trader && !params.dry) return null;
|
|
1431
|
+
const slippagePips = params.slippageBps !== void 0 ? Math.round(params.slippageBps * 100) : SWAP_SLIPPAGE_PIPS_DEFAULT;
|
|
1432
|
+
if (!Number.isInteger(slippagePips) || slippagePips < 0 || slippagePips > 5e5) return null;
|
|
1433
|
+
const omni = await getClient();
|
|
1434
|
+
const settle = params.dry === false ? Math.min(quoteSettleMs, EXECUTION_SETTLE_MS) : quoteSettleMs;
|
|
1435
|
+
const outcome = { updates: 0 };
|
|
1436
|
+
const rfq = async (pips) => {
|
|
1437
|
+
const request = {
|
|
1438
|
+
inputAsset,
|
|
1439
|
+
outputAsset,
|
|
1440
|
+
amount: { $case: "inputUnits", value: params.amountIn },
|
|
1441
|
+
// One route → one outgoing message: a TON account sends exactly one.
|
|
1442
|
+
settlementParams: [{ params: { $case: "swap", value: { maxPriceSlippagePips: pips, maxRoutes: 1 } } }]
|
|
1443
|
+
};
|
|
1444
|
+
if (hasIntegrator) {
|
|
1445
|
+
const address = opts.integratorAddress ? toChainAddress(params.destChain, opts.integratorAddress) : null;
|
|
1446
|
+
if (!address) return null;
|
|
1447
|
+
request.integratorAddress = address;
|
|
1448
|
+
request.integratorFeePips = opts.integratorFeePips;
|
|
1449
|
+
}
|
|
1450
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1451
|
+
if (attempt > 0) await new Promise((resolve) => setTimeout(resolve, 300));
|
|
1452
|
+
const quote = await awaitQuote(omni.requestForQuote(request), request, signal, settle, "swap", outcome);
|
|
1453
|
+
if (quote && quote.settlementData.$case === "swap") return quote;
|
|
1454
|
+
if (signal?.aborted) return null;
|
|
1455
|
+
}
|
|
1456
|
+
return null;
|
|
1457
|
+
};
|
|
1458
|
+
let q = await rfq(slippagePips);
|
|
1459
|
+
if (!q) {
|
|
1460
|
+
if (!signal?.aborted) {
|
|
1461
|
+
params.onRefusal?.(`omniston: ${outcome.error ? `RFQ failed: ${outcome.error}` : outcome.rejected ? `swap quote rejected: ${outcome.rejected}` : "no TON DEX route quoted this pair/amount"}`);
|
|
1462
|
+
}
|
|
1463
|
+
return null;
|
|
1464
|
+
}
|
|
1465
|
+
let swap = q.settlementData.$case === "swap" ? q.settlementData.value : null;
|
|
1466
|
+
if (!swap) return null;
|
|
1467
|
+
const absoluteFloor = params.minAmountOut !== void 0 && isPositiveInteger(params.minAmountOut) ? BigInt(params.minAmountOut) : 0n;
|
|
1468
|
+
const requiredMin = (outputUnits) => {
|
|
1469
|
+
const out = BigInt(outputUnits);
|
|
1470
|
+
const slippageFloor = params.slippageBps !== void 0 ? (out * BigInt(1e4 - params.slippageBps) + 9999n) / 10000n : 0n;
|
|
1471
|
+
return absoluteFloor > slippageFloor ? absoluteFloor : slippageFloor;
|
|
1472
|
+
};
|
|
1473
|
+
for (let attempt = 0; attempt < 2 && BigInt(swap.minOutputAmount) < requiredMin(q.outputUnits); attempt++) {
|
|
1474
|
+
const out = BigInt(q.outputUnits);
|
|
1475
|
+
const floor = requiredMin(q.outputUnits);
|
|
1476
|
+
if (out < floor) return null;
|
|
1477
|
+
const tightened = Number((out - floor) * 1000000n / out);
|
|
1478
|
+
const again = await rfq(Math.max(0, Math.min(tightened, slippagePips)));
|
|
1479
|
+
const againSwap = again?.settlementData.$case === "swap" ? again.settlementData.value : null;
|
|
1480
|
+
if (!again || !againSwap) return null;
|
|
1481
|
+
q = again;
|
|
1482
|
+
swap = againSwap;
|
|
1483
|
+
}
|
|
1484
|
+
if (BigInt(swap.minOutputAmount) < requiredMin(q.outputUnits)) return null;
|
|
1485
|
+
if (q.gasBudget !== void 0 && BigInt(q.gasBudget) > BigInt(maxTonExecutionNano)) return null;
|
|
1486
|
+
const minOut = swap.minOutputAmount;
|
|
1487
|
+
await saveBuilt(q.quoteId, { quote: q, at: Date.now(), bound: { trader, dest: params.destAddress, srcChain: params.sourceChain, kind: "swap" } });
|
|
1488
|
+
const feeUnits = Number(q.protocolFeeUnits || "0") + Number(q.integratorFeeUnits || "0");
|
|
1489
|
+
const stableOut = /^(USDT|USDC|USD₮|jUSDT)$/.test(dest.symbol);
|
|
1490
|
+
const feeUsd = stableOut ? feeUnits / 10 ** dest.decimals : 0;
|
|
1491
|
+
return {
|
|
1492
|
+
provider: "omniston",
|
|
1493
|
+
intentId: encodeIntent(q.quoteId, trader, params.destAddress, params.sourceChain, "swap"),
|
|
1494
|
+
depositAddress: "",
|
|
1495
|
+
// swap settlement — the built message pays the DEX directly
|
|
1496
|
+
depositMemo: null,
|
|
1497
|
+
sourceAmount: q.inputUnits,
|
|
1498
|
+
destAmount: q.outputUnits,
|
|
1499
|
+
minDestAmount: minOut,
|
|
1500
|
+
feeUsd: Number.isFinite(feeUsd) ? feeUsd : 0,
|
|
1501
|
+
fees: [
|
|
1502
|
+
...q.protocolFeeUnits && q.protocolFeeUnits !== "0" ? [{ type: "protocol", amount: q.protocolFeeUnits, token: feeToken(dest), included: true }] : [],
|
|
1503
|
+
...q.integratorFeeUnits && q.integratorFeeUnits !== "0" ? [{ type: "affiliate", amount: q.integratorFeeUnits, token: feeToken(dest), included: true }] : []
|
|
1504
|
+
],
|
|
1505
|
+
etaSeconds: 30,
|
|
1506
|
+
reliability: 0.9,
|
|
1507
|
+
validUntilMs: Date.now() + QUOTE_TTL_MS,
|
|
1508
|
+
// gasBudget = TON the message carries on top of the input; gasEstimate =
|
|
1509
|
+
// what the route is expected to burn. Consumers show both so the user
|
|
1510
|
+
// sees the 1.1 GRAM leaving for a 1 GRAM swap — and the refund.
|
|
1511
|
+
providerData: {
|
|
1512
|
+
settlement: "swap",
|
|
1513
|
+
protocols: swapProtocols(swap).join(","),
|
|
1514
|
+
...q.gasBudget ? { gasBudget: q.gasBudget } : {},
|
|
1515
|
+
...q.estimatedGasConsumption ? { gasEstimate: q.estimatedGasConsumption } : {}
|
|
1516
|
+
}
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
async function buildSwapDeposit(ref, q, swap) {
|
|
1520
|
+
if (!ref.trader) throw new Error("omniston: quote was requested without a refund/source address");
|
|
1521
|
+
if (!isTon(ref.srcChain) || q.inputAsset.chain.$case !== "ton" || q.outputAsset.chain.$case !== "ton") {
|
|
1522
|
+
throw new Error("omniston: swap quote is not a TON same-chain swap");
|
|
1523
|
+
}
|
|
1524
|
+
if (!sameChainTonReady()) throw new Error("omniston: TON same-chain swaps need independent jetton-wallet and swap-target validators");
|
|
1525
|
+
const owner = { chain: { $case: "ton", value: ref.trader } };
|
|
1526
|
+
const receiver = { chain: { $case: "ton", value: ref.dest } };
|
|
1527
|
+
const omni = await getClient();
|
|
1528
|
+
if (typeof omni.tonBuildSwap !== "function") throw new Error("omniston: client does not support swap settlement (tonBuildSwap)");
|
|
1529
|
+
const tx = await omni.tonBuildSwap({
|
|
1530
|
+
quoteId: ref.quoteId,
|
|
1531
|
+
transferSrcAddress: owner,
|
|
1532
|
+
refundSrcAddress: owner,
|
|
1533
|
+
gasExcessAddress: owner,
|
|
1534
|
+
traderDstAddress: receiver,
|
|
1535
|
+
// Build at the slippage the quote was requested with: its
|
|
1536
|
+
// minOutputAmount is the minimum this module reported to the router.
|
|
1537
|
+
useRecommendedSlippage: false
|
|
1538
|
+
});
|
|
1539
|
+
const [message, ...rest] = tx.messages;
|
|
1540
|
+
if (!message || rest.length > 0) throw new Error(`omniston: expected exactly one swap message, got ${tx.messages.length}`);
|
|
1541
|
+
if (message.jettonWalletStateInit) throw new Error("omniston: deploying a resolver-provided jetton wallet is not supported");
|
|
1542
|
+
if (!isPositiveInteger(message.sendAmount)) throw new Error("omniston: malformed swap message value");
|
|
1543
|
+
const sendAmount = BigInt(message.sendAmount);
|
|
1544
|
+
const gasCap = BigInt(maxTonExecutionNano);
|
|
1545
|
+
const gasBudget = q.gasBudget && isPositiveInteger(q.gasBudget) ? BigInt(q.gasBudget) : gasCap;
|
|
1546
|
+
const protocols = swapProtocols(swap);
|
|
1547
|
+
const inputJetton = assetContract(q.inputAsset);
|
|
1548
|
+
const payload = parseSwapPayload(message.payload);
|
|
1549
|
+
const routeSteps = swap.routes.length === 1 ? swap.routes[0].steps : [];
|
|
1550
|
+
const singleV1 = routeSteps.length === 1 && routeSteps[0].chunks.length === 1 && routeSteps[0].chunks[0].protocol === "StonFiV1";
|
|
1551
|
+
const ptonV1Native = !inputJetton && !!payload.jettonTransfer && singleV1;
|
|
1552
|
+
if (inputJetton) {
|
|
1553
|
+
const transfer = payload.jettonTransfer;
|
|
1554
|
+
if (!transfer) throw new Error("omniston: jetton-input swap message is not a jetton transfer");
|
|
1555
|
+
if (sendAmount > gasCap || sendAmount > gasBudget) throw new Error(`omniston: swap message attaches more TON than the quoted gas budget (${sendAmount} > ${gasBudget} nanoTON, cap ${gasCap}, ${protocols.join("+")})`);
|
|
1556
|
+
if (transfer.amount !== BigInt(q.inputUnits)) throw new Error("omniston: swap message moves a different amount than quoted");
|
|
1557
|
+
if (!transfer.responseDestination || !sameTonAddress(transfer.responseDestination, ref.trader)) {
|
|
1558
|
+
throw new Error("omniston: swap message returns excess to a wallet other than the trader");
|
|
1559
|
+
}
|
|
1560
|
+
if (transfer.forwardTon >= sendAmount) throw new Error("omniston: swap message forwards more TON than it carries");
|
|
1561
|
+
const walletValid = await opts.validateTonJettonWallet({
|
|
1562
|
+
ownerAddress: ref.trader,
|
|
1563
|
+
jettonMasterAddress: inputJetton,
|
|
1564
|
+
walletAddress: message.targetAddress
|
|
1565
|
+
});
|
|
1566
|
+
if (!walletValid) throw new Error("omniston: TON jetton wallet was not independently validated");
|
|
1567
|
+
const targetValid = await opts.validateTonSwapTarget({ address: transfer.destination, protocols });
|
|
1568
|
+
if (!targetValid) throw new Error("omniston: swap destination is not a recognized DEX contract for the quoted protocol");
|
|
1569
|
+
} else if (ptonV1Native) {
|
|
1570
|
+
const transfer = payload.jettonTransfer;
|
|
1571
|
+
const input = BigInt(q.inputUnits);
|
|
1572
|
+
if (transfer.amount !== input) throw new Error("omniston: swap message moves a different amount than quoted");
|
|
1573
|
+
if (sendAmount < input) throw new Error("omniston: swap message carries less than the quoted input");
|
|
1574
|
+
const gas = sendAmount - input;
|
|
1575
|
+
if (gas > gasCap || gas > gasBudget) throw new Error(`omniston: swap message attaches more TON than the quoted gas budget (${gas} > ${gasBudget} nanoTON, cap ${gasCap}, ${protocols.join("+")})`);
|
|
1576
|
+
if (transfer.forwardTon > gas) throw new Error("omniston: swap message forwards more TON than its gas");
|
|
1577
|
+
if (!transfer.responseDestination || !sameTonAddress(transfer.responseDestination, ref.trader)) {
|
|
1578
|
+
throw new Error("omniston: swap message returns excess to a wallet other than the trader");
|
|
1579
|
+
}
|
|
1580
|
+
if (typeof opts.validateTonSwapLeg !== "function") throw new Error("omniston: STON.fi v1 native input needs the validateTonSwapLeg validator (pTON v1 wallet ownership)");
|
|
1581
|
+
const targetValid = await opts.validateTonSwapTarget({ address: message.targetAddress, protocols });
|
|
1582
|
+
if (!targetValid) throw new Error("omniston: swap target is not a recognized DEX contract for the quoted protocol");
|
|
1583
|
+
const routerValid = await opts.validateTonSwapTarget({ address: transfer.destination, protocols });
|
|
1584
|
+
if (!routerValid) throw new Error("omniston: swap destination is not a recognized DEX contract for the quoted protocol");
|
|
1585
|
+
} else {
|
|
1586
|
+
if (payload.jettonTransfer) {
|
|
1587
|
+
throw new Error(`omniston: native-input swap message must not be a jetton transfer (route ${protocols.join("+") || "?"}, target ${message.targetAddress}, transfer ${payload.jettonTransfer.amount} to ${payload.jettonTransfer.destination}, forwardTon ${payload.jettonTransfer.forwardTon}, value ${sendAmount})`);
|
|
1588
|
+
}
|
|
1589
|
+
const input = BigInt(q.inputUnits);
|
|
1590
|
+
if (sendAmount < input) throw new Error("omniston: swap message carries less than the quoted input");
|
|
1591
|
+
const gas = sendAmount - input;
|
|
1592
|
+
if (gas > gasCap || gas > gasBudget) throw new Error(`omniston: swap message attaches more TON than the quoted gas budget (${gas} > ${gasBudget} nanoTON, cap ${gasCap}, ${protocols.join("+")})`);
|
|
1593
|
+
const targetValid = await opts.validateTonSwapTarget({ address: message.targetAddress, protocols });
|
|
1594
|
+
if (!targetValid) throw new Error("omniston: swap target is not a recognized DEX contract for the quoted protocol");
|
|
1595
|
+
}
|
|
1596
|
+
const mode = ptonV1Native ? "enforce" : opts.tonSwapPayloadCheck ?? "enforce";
|
|
1597
|
+
const trace = traceFor(ref.quoteId);
|
|
1598
|
+
const payloadRefusal = (reason, detail) => {
|
|
1599
|
+
trace("ton-swap-payload", { reason, detail, mode, protocols: protocols.join("+") });
|
|
1600
|
+
if (mode === "enforce") throw new Error(`omniston: ton-swap-payload ${reason} \u2014 ${detail}`);
|
|
1601
|
+
};
|
|
1602
|
+
let checked = null;
|
|
1603
|
+
try {
|
|
1604
|
+
checked = checkTonSwapPayload({
|
|
1605
|
+
payloadHex: message.payload,
|
|
1606
|
+
targetAddress: message.targetAddress,
|
|
1607
|
+
sendAmount,
|
|
1608
|
+
trader: ref.trader,
|
|
1609
|
+
dest: ref.dest,
|
|
1610
|
+
inputUnits: q.inputUnits,
|
|
1611
|
+
minOutputAmount: swap.minOutputAmount,
|
|
1612
|
+
inputJetton,
|
|
1613
|
+
outputJetton: assetContract(q.outputAsset),
|
|
1614
|
+
steps: swap.routes.length === 1 ? routeSteps.map((step) => {
|
|
1615
|
+
if (step.chunks.length !== 1) return { protocol: `split(${step.chunks.map((c) => c.protocol).join("|")})`, inputJetton: void 0, outputJetton: void 0 };
|
|
1616
|
+
return { protocol: step.chunks[0].protocol, inputJetton: stepJetton(step.inputAsset), outputJetton: stepJetton(step.outputAsset) };
|
|
1617
|
+
}) : [{ protocol: `${swap.routes.length}-routes`, inputJetton: void 0, outputJetton: void 0 }],
|
|
1618
|
+
integratorAddress: q.integratorAddress?.chain.$case === "ton" ? q.integratorAddress.chain.value : null
|
|
1619
|
+
});
|
|
1620
|
+
} catch (error) {
|
|
1621
|
+
if (error instanceof TonSwapPayloadError) payloadRefusal(error.reason, error.message);
|
|
1622
|
+
else payloadRefusal("malformed", error instanceof Error ? error.message : String(error));
|
|
1623
|
+
}
|
|
1624
|
+
if (checked) {
|
|
1625
|
+
if (typeof opts.validateTonSwapLeg !== "function") {
|
|
1626
|
+
trace("ton-swap-payload", { reason: "legs-unverified", detail: "no validateTonSwapLeg hook", mode, legs: checked.legs.length });
|
|
1627
|
+
} else {
|
|
1628
|
+
for (const leg of checked.legs) {
|
|
1629
|
+
let ok = false;
|
|
1630
|
+
try {
|
|
1631
|
+
ok = await opts.validateTonSwapLeg(leg);
|
|
1632
|
+
} catch {
|
|
1633
|
+
ok = false;
|
|
1634
|
+
}
|
|
1635
|
+
if (!ok) {
|
|
1636
|
+
payloadRefusal("leg-unverified", `${leg.kind} v${leg.version} ${leg.kind === "dedust-pool" ? `pool ${leg.pool}` : `ask wallet ${leg.askWallet}`}`);
|
|
1637
|
+
break;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
return {
|
|
1643
|
+
chain: ref.srcChain,
|
|
1644
|
+
to: message.targetAddress,
|
|
1645
|
+
value: message.sendAmount,
|
|
1646
|
+
// nanotons: gas only (jetton input) or input + gas (native)
|
|
1647
|
+
bodyBoc: hexToBase64(message.payload)
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
async function trackSwap(decoded, sourceTxHash) {
|
|
1651
|
+
if (!sourceTxHash) return { state: "pending" };
|
|
1652
|
+
const omni = await getClient();
|
|
1653
|
+
if (typeof omni.swapTrack !== "function") return { state: "pending" };
|
|
1654
|
+
const progress = await awaitSwapProgress(
|
|
1655
|
+
omni.swapTrack({ quoteId: decoded.quoteId, traderAddress: { chain: { $case: "ton", value: decoded.trader } }, outgoingTxQuery: sourceTxHash })
|
|
1656
|
+
);
|
|
1657
|
+
if (!progress || progress === "awaiting") return { state: "pending" };
|
|
1658
|
+
switch (progress.status) {
|
|
1659
|
+
case "TRADE_STATUS_FULLY_FILLED":
|
|
1660
|
+
await quotes.clear(decoded.quoteId);
|
|
1661
|
+
return { state: "completed", depositReceived: true, ...progress.outgoingTxHash ? { destTxHash: progress.outgoingTxHash } : {} };
|
|
1662
|
+
case "TRADE_STATUS_PARTIALLY_FILLED":
|
|
1663
|
+
await quotes.clear(decoded.quoteId);
|
|
1664
|
+
return { state: "completed", depositReceived: true, ...progress.outgoingTxHash ? { destTxHash: progress.outgoingTxHash } : {} };
|
|
1665
|
+
case "TRADE_STATUS_CANCELLED":
|
|
1666
|
+
case "TRADE_STATUS_FAILED":
|
|
1667
|
+
await quotes.clear(decoded.quoteId);
|
|
1668
|
+
return { state: "failed", depositReceived: true };
|
|
1669
|
+
default:
|
|
1670
|
+
return { state: "pending", depositReceived: true };
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
async function buildEvmOrder(ref, q, settlement, bound) {
|
|
1674
|
+
const srcChain = ref.srcChain;
|
|
1675
|
+
const chainId = evmRef2(srcChain);
|
|
1676
|
+
const srcCase = q.inputAsset.chain.$case;
|
|
1677
|
+
if (chainId === null || EVM_CASE[chainId] !== srcCase) throw new Error("omniston: source chain does not match the EVM quote");
|
|
1678
|
+
const destCase = q.outputAsset.chain.$case;
|
|
1679
|
+
const destChain = caipForChainCase(destCase);
|
|
1680
|
+
const tonDest = destCase === "ton";
|
|
1681
|
+
if (!destChain || destChain === srcChain) throw new Error("omniston: EVM-source order has no supported destination chain");
|
|
1682
|
+
if (!trustedOrderProtocols.has(srcChain) || typeof opts.validateEvmInputPosition !== "function" || typeof (tonDest ? opts.validateTonOutputPosition : opts.validateEvmOutputPosition) !== "function") {
|
|
1683
|
+
throw new Error(`omniston: EVM\u2192${tonDest ? "TON" : "EVM"} needs a trusted order protocol and both position validators`);
|
|
1684
|
+
}
|
|
1685
|
+
let owner;
|
|
1686
|
+
try {
|
|
1687
|
+
owner = (0, import_viem.getAddress)(ref.trader);
|
|
1688
|
+
} catch {
|
|
1689
|
+
throw new Error("omniston: trader is not an EVM address");
|
|
1690
|
+
}
|
|
1691
|
+
let dest;
|
|
1692
|
+
if (tonDest) {
|
|
1693
|
+
try {
|
|
1694
|
+
import_core3.Address.parse(ref.dest);
|
|
1695
|
+
} catch {
|
|
1696
|
+
throw new Error("omniston: destination is not a TON address");
|
|
1697
|
+
}
|
|
1698
|
+
dest = ref.dest;
|
|
1699
|
+
} else {
|
|
1700
|
+
try {
|
|
1701
|
+
dest = (0, import_viem.getAddress)(ref.dest);
|
|
1702
|
+
} catch {
|
|
1703
|
+
throw new Error("omniston: destination is not an EVM address");
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
const traderDst = { chain: { $case: destCase, value: dest } };
|
|
1707
|
+
const protocol = settlement.srcProtocolContractAddress;
|
|
1708
|
+
const resolver = settlement.srcInitialResolverAddress;
|
|
1709
|
+
const factory = settlement.dstProtocolContractAddress;
|
|
1710
|
+
const inputToken = assetContract(q.inputAsset);
|
|
1711
|
+
const outputToken = assetContract(q.outputAsset);
|
|
1712
|
+
const destChainId = tonDest ? null : evmRef2(destChain);
|
|
1713
|
+
if (protocol.chain.$case !== srcCase || !isTrustedOrderProtocol(srcChain, protocol.chain.value) || resolver?.chain.$case !== srcCase || !validEvmAddress(resolver.chain.value) || factory?.chain.$case !== destCase || !isTrusted(destChain, factory.chain.value) || !inputToken || !outputToken || !Number.isSafeInteger(settlement.protocolFeePips) || !tonDest && (destChainId === null || !validEvmAddress(outputToken) || !isPositiveInteger(settlement.dstSecurityDepositUnits ?? ""))) throw new Error("omniston: incomplete EVM order settlement binding in quote");
|
|
1714
|
+
const { secrets, created } = await secretsFor(ref, q, 1);
|
|
1715
|
+
const hashes = secrets.map((secret) => hashlock(secret, settlement.htlcHashingFunction));
|
|
1716
|
+
const forget = async () => {
|
|
1717
|
+
if (created) await clearSecrets(ref.quoteId);
|
|
1718
|
+
};
|
|
1719
|
+
const omni = await getClient();
|
|
1720
|
+
let payload;
|
|
1721
|
+
try {
|
|
1722
|
+
payload = await omni.evmBuildOrderPayload({
|
|
1723
|
+
quoteId: ref.quoteId,
|
|
1724
|
+
ownerSrcAddress: { chain: { $case: srcCase, value: owner } },
|
|
1725
|
+
traderDstAddress: traderDst,
|
|
1726
|
+
htlcSecrets: { secretMode: { $case: "provided", value: { hashes } } }
|
|
1727
|
+
});
|
|
1728
|
+
} catch (error) {
|
|
1729
|
+
await forget();
|
|
1730
|
+
throw error;
|
|
1731
|
+
}
|
|
1732
|
+
let td;
|
|
1733
|
+
try {
|
|
1734
|
+
try {
|
|
1735
|
+
td = JSON.parse(payload.typedData);
|
|
1736
|
+
} catch {
|
|
1737
|
+
throw new Error("omniston: malformed EVM order typed data");
|
|
1738
|
+
}
|
|
1739
|
+
validateSerializedOrderDetails(payload.serializedOrderDetails, {
|
|
1740
|
+
quoteId: ref.quoteId,
|
|
1741
|
+
outputAsset: q.outputAsset,
|
|
1742
|
+
traderDstAddress: traderDst,
|
|
1743
|
+
secretHashes: hashes
|
|
1744
|
+
});
|
|
1745
|
+
validateEvmTypedOrder(td, {
|
|
1746
|
+
chainId,
|
|
1747
|
+
settlementContract: protocol.chain.value,
|
|
1748
|
+
owner,
|
|
1749
|
+
receiver: resolver.chain.value,
|
|
1750
|
+
makerAsset: inputToken,
|
|
1751
|
+
makingAmount: q.inputUnits,
|
|
1752
|
+
takingAmount: settlement.resolverSendsUnits,
|
|
1753
|
+
orderExtension: payload.orderExtension,
|
|
1754
|
+
quoteTimestamp: q.quoteTimestamp,
|
|
1755
|
+
tradeStartDeadline: settlement.tradeStartDeadline
|
|
1756
|
+
});
|
|
1757
|
+
await validateEvmToTonOrderExtension(payload.orderExtension, {
|
|
1758
|
+
quoteId: ref.quoteId,
|
|
1759
|
+
quoteTimestamp: q.quoteTimestamp,
|
|
1760
|
+
tradeStartDeadline: settlement.tradeStartDeadline,
|
|
1761
|
+
protocolFeePips: settlement.protocolFeePips,
|
|
1762
|
+
integratorFeePips: settlement.integratorFeePips ?? 0,
|
|
1763
|
+
initialResolverAddress: resolver.chain.value,
|
|
1764
|
+
outputTokenAddress: outputToken,
|
|
1765
|
+
traderDstAddress: dest,
|
|
1766
|
+
takerAsset: String(td.message.takerAsset),
|
|
1767
|
+
srcSecurityDepositUnits: settlement.srcHtlcSecurityDepositUnits ?? "",
|
|
1768
|
+
// TON destination: the TON deposit is bound in the TON position, not in
|
|
1769
|
+
// the EVM deposits word (live: 0 for every TON-destination order). EVM
|
|
1770
|
+
// destination: the word carries the quote's destination deposit and
|
|
1771
|
+
// ExtraDataArgs name the destination chain and token (live 2026-10-20).
|
|
1772
|
+
dstSecurityDepositUnits: tonDest ? "0" : settlement.dstSecurityDepositUnits,
|
|
1773
|
+
...tonDest ? {} : { dstChainId: destChainId },
|
|
1774
|
+
secretHash: hashes[0],
|
|
1775
|
+
trustedTarget: (address) => isTrusted(srcChain, address)
|
|
1776
|
+
});
|
|
1777
|
+
} catch (error) {
|
|
1778
|
+
await forget();
|
|
1779
|
+
throw error;
|
|
1780
|
+
}
|
|
1781
|
+
const extensionTarget = (0, import_viem.getAddress)(`0x${bytesToHex(payload.orderExtension.slice(32 + new DataView(payload.orderExtension.buffer, payload.orderExtension.byteOffset).getUint32(4, false), 32 + new DataView(payload.orderExtension.buffer, payload.orderExtension.byteOffset).getUint32(4, false) + 20))}`);
|
|
1782
|
+
const typedData = JSON.stringify({ ...td, domain: { ...td.domain, chainId } });
|
|
1783
|
+
await saveBuilt(ref.quoteId, {
|
|
1784
|
+
quote: q,
|
|
1785
|
+
at: Date.now(),
|
|
1786
|
+
...bound ? { bound } : {},
|
|
1787
|
+
evmOrder: {
|
|
1788
|
+
typedData,
|
|
1789
|
+
orderExtension: bytesToHex(payload.orderExtension),
|
|
1790
|
+
serializedOrderDetails: bytesToHex(payload.serializedOrderDetails ?? new Uint8Array()),
|
|
1791
|
+
owner,
|
|
1792
|
+
chainCase: srcCase,
|
|
1793
|
+
factory: extensionTarget
|
|
1794
|
+
}
|
|
1795
|
+
});
|
|
1796
|
+
const orderProtocol = (0, import_viem.getAddress)(protocol.chain.value);
|
|
1797
|
+
return { chain: srcChain, to: orderProtocol, value: "0", chainId, approvalAddress: orderProtocol, typedData };
|
|
1798
|
+
}
|
|
1799
|
+
return {
|
|
1800
|
+
name: "omniston",
|
|
1801
|
+
describeActivity: describeOmnistonActivity,
|
|
1802
|
+
enabled: () => (opts.enabled ?? true) && trustedContracts.size > 0,
|
|
1803
|
+
// Capability discovery comes from the provider itself, so it can run alone
|
|
1804
|
+
// or inside the optional aggregator without a central capability table.
|
|
1805
|
+
supportsChain: (chain) => omniston.supportsChain(chain),
|
|
1806
|
+
supportsChainPair: (fromChain, toChain) => omniston.supports(fromChain, toChain),
|
|
1807
|
+
// TON↔TON swap settlement is same-chain by definition.
|
|
1808
|
+
supportsSameChain: true,
|
|
1809
|
+
supportsRoute(from, to) {
|
|
1810
|
+
if (isTon(from.chain) && isTon(to.chain)) {
|
|
1811
|
+
return omniston.supports(from.chain, to.chain) && trustedContracts.has(from.chain) && sameChainTonReady();
|
|
1812
|
+
}
|
|
1813
|
+
if (isTon(from.chain) && !isTon(to.chain)) return tonSourceReady(from.chain, to.chain);
|
|
1814
|
+
if (!isTon(from.chain) && isTon(to.chain)) return evmSourceReady(from.chain, to.chain);
|
|
1815
|
+
return evmToEvmReady(from.chain, to.chain);
|
|
1816
|
+
},
|
|
1817
|
+
async getQuote(params, signal) {
|
|
1818
|
+
if (params.flex) return null;
|
|
1819
|
+
if (isTon(params.sourceChain) && isTon(params.destChain)) return await getSwapQuote(params, signal);
|
|
1820
|
+
const refuse2 = (reason) => {
|
|
1821
|
+
params.onRefusal?.(`omniston: ${reason}`);
|
|
1822
|
+
return null;
|
|
1823
|
+
};
|
|
1824
|
+
const evmSource = !isTon(params.sourceChain);
|
|
1825
|
+
const tonDest = isTon(params.destChain);
|
|
1826
|
+
if (!omniston.supports(params.sourceChain, params.destChain)) return refuse2("route is not an Omniston order corridor");
|
|
1827
|
+
if (!trustedContracts.has(params.sourceChain) || !trustedContracts.has(params.destChain)) {
|
|
1828
|
+
return refuse2(`no trusted settlement contract configured for ${trustedContracts.has(params.sourceChain) ? params.destChain : params.sourceChain}`);
|
|
1829
|
+
}
|
|
1830
|
+
if (evmSource) {
|
|
1831
|
+
if (!trustedOrderProtocols.has(params.sourceChain)) return refuse2(`no trusted EVM order protocol configured for ${params.sourceChain}`);
|
|
1832
|
+
if (typeof opts.validateEvmInputPosition !== "function" || typeof (tonDest ? opts.validateTonOutputPosition : opts.validateEvmOutputPosition) !== "function") {
|
|
1833
|
+
return refuse2(tonDest ? "EVM\u2192TON needs the EVM input-position and TON output-position validators" : "EVM\u2192EVM needs the EVM input-position and EVM output-position validators");
|
|
1834
|
+
}
|
|
1835
|
+
if (params.dry === false) {
|
|
1836
|
+
if (tonDest) {
|
|
1837
|
+
try {
|
|
1838
|
+
import_core3.Address.parse(params.destAddress);
|
|
1839
|
+
} catch {
|
|
1840
|
+
return refuse2("destination is not a TON address");
|
|
1841
|
+
}
|
|
1842
|
+
} else if (!validEvmAddress(params.destAddress)) return refuse2("destination is not an EVM address");
|
|
1843
|
+
}
|
|
1844
|
+
} else if (typeof opts.validateTonJettonWallet !== "function" || typeof opts.validateTonEscrowPayload !== "function" || typeof opts.validateEvmOutputPosition !== "function") return refuse2("TON\u2192EVM needs the jetton-wallet, escrow-payload and EVM output-position validators");
|
|
1845
|
+
const source = (0, import_swap2_assets.ccAsset)(params.sourceChain, params.sourceToken);
|
|
1846
|
+
const dest = (0, import_swap2_assets.ccAssetByToken)(params.destChain, params.destToken);
|
|
1847
|
+
if (!source || !dest) return refuse2("asset is not in the catalog");
|
|
1848
|
+
const inputAsset = toAssetId(params.sourceChain, source);
|
|
1849
|
+
const outputAsset = toAssetId(params.destChain, dest);
|
|
1850
|
+
if (!inputAsset || !outputAsset) {
|
|
1851
|
+
return refuse2(`cross-chain orders are token corridors only (no ${!inputAsset ? source.symbol : dest.symbol} native leg)`);
|
|
1852
|
+
}
|
|
1853
|
+
const maxUnits = decimalToUnits(maxUsdText, source.decimals);
|
|
1854
|
+
if (!maxUnits || !isPositiveInteger(params.amountIn)) return refuse2("invalid amount");
|
|
1855
|
+
if (BigInt(params.amountIn) > maxUnits) return refuse2(`amount above the ${maxUsdText} USD per-swap cap`);
|
|
1856
|
+
const trader = params.refundAddress ?? "";
|
|
1857
|
+
if (!trader && !params.dry) return refuse2(`execution quote needs the trader's ${evmSource ? "EVM" : "TON"} address (refundAddress)`);
|
|
1858
|
+
if (evmSource && trader && !validEvmAddress(trader)) return refuse2("refundAddress is not an EVM address");
|
|
1859
|
+
const request = {
|
|
1860
|
+
inputAsset,
|
|
1861
|
+
outputAsset,
|
|
1862
|
+
amount: { $case: "inputUnits", value: params.amountIn },
|
|
1863
|
+
settlementParams: [{ params: { $case: "order", value: {} } }]
|
|
1864
|
+
};
|
|
1865
|
+
const integrator = opts.integratorAddress ?? "";
|
|
1866
|
+
if (hasIntegrator) {
|
|
1867
|
+
const pips = opts.integratorFeePips;
|
|
1868
|
+
const address = integrator ? toChainAddress(params.destChain, integrator) : null;
|
|
1869
|
+
if (!address) return refuse2("integrator address is not valid on the destination chain");
|
|
1870
|
+
request.integratorAddress = address;
|
|
1871
|
+
request.integratorFeePips = pips;
|
|
1872
|
+
}
|
|
1873
|
+
const omni = await getClient();
|
|
1874
|
+
const rfq = { updates: 0 };
|
|
1875
|
+
const q = await awaitQuote(
|
|
1876
|
+
omni.requestForQuote(request),
|
|
1877
|
+
request,
|
|
1878
|
+
signal,
|
|
1879
|
+
params.dry === false ? Math.min(quoteSettleMs, EXECUTION_SETTLE_MS) : quoteSettleMs,
|
|
1880
|
+
"order",
|
|
1881
|
+
rfq
|
|
1882
|
+
);
|
|
1883
|
+
if (!q) {
|
|
1884
|
+
if (signal?.aborted) return null;
|
|
1885
|
+
return refuse2(
|
|
1886
|
+
rfq.error ? `RFQ failed: ${rfq.error}` : rfq.rejected ? `resolver quote rejected: ${rfq.rejected}` : "no resolver quoted this route/amount"
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
const orderData = q.settlementData.$case === "order" ? q.settlementData.value : null;
|
|
1890
|
+
if (!orderData) return refuse2("resolver answered without order settlement");
|
|
1891
|
+
const srcContract = orderData.srcProtocolContractAddress.chain.value;
|
|
1892
|
+
if (evmSource ? !isTrustedOrderProtocol(params.sourceChain, srcContract) : !isTrusted(params.sourceChain, srcContract)) {
|
|
1893
|
+
return refuse2(evmSource ? `untrusted ${params.sourceChain} order protocol ${srcContract}` : `untrusted TON settlement contract ${srcContract}`);
|
|
1894
|
+
}
|
|
1895
|
+
if (!orderData.dstProtocolContractAddress || !isTrusted(params.destChain, orderData.dstProtocolContractAddress.chain.value)) {
|
|
1896
|
+
return refuse2(`untrusted ${params.destChain} settlement contract ${orderData.dstProtocolContractAddress?.chain.value ?? "(none)"}`);
|
|
1897
|
+
}
|
|
1898
|
+
const deadlineMs = orderData.tradeStartDeadline * 1e3;
|
|
1899
|
+
if (!Number.isFinite(deadlineMs) || deadlineMs <= Date.now()) return refuse2("quote trade-start deadline already passed");
|
|
1900
|
+
if (params.dry === false && deadlineMs - Date.now() < MIN_START_HEADROOM_MS) return refuse2("quote trade-start deadline too close to execute");
|
|
1901
|
+
const inputScaled = BigInt(q.inputUnits) * 10n ** BigInt(dest.decimals);
|
|
1902
|
+
const outputScaled = BigInt(q.outputUnits) * 10n ** BigInt(source.decimals);
|
|
1903
|
+
if (outputScaled * 2n < inputScaled || outputScaled > inputScaled * 2n) return refuse2("quoted output is outside 0.5x\u20132x of the input");
|
|
1904
|
+
if (await quoteIdInUse(q.quoteId)) return refuse2("the platform reused a quote id that already belongs to an order");
|
|
1905
|
+
await saveBuilt(q.quoteId, { quote: q, at: Date.now(), bound: { trader, dest: params.destAddress, srcChain: params.sourceChain, kind: "order" } });
|
|
1906
|
+
const feeUnits = Number(q.protocolFeeUnits || "0") + Number(q.integratorFeeUnits || "0");
|
|
1907
|
+
const feeUsd = feeUnits / 10 ** dest.decimals;
|
|
1908
|
+
return {
|
|
1909
|
+
provider: "omniston",
|
|
1910
|
+
intentId: encodeIntent(q.quoteId, trader, params.destAddress, params.sourceChain),
|
|
1911
|
+
depositAddress: "",
|
|
1912
|
+
// order settlement — no static deposit address
|
|
1913
|
+
depositMemo: null,
|
|
1914
|
+
sourceAmount: q.inputUnits,
|
|
1915
|
+
destAmount: q.outputUnits,
|
|
1916
|
+
minDestAmount: q.outputUnits,
|
|
1917
|
+
// HTLC fills at the quoted amount or refunds
|
|
1918
|
+
feeUsd: Number.isFinite(feeUsd) ? feeUsd : 0,
|
|
1919
|
+
fees: [
|
|
1920
|
+
...q.protocolFeeUnits && q.protocolFeeUnits !== "0" ? [{ type: "protocol", amount: q.protocolFeeUnits, token: feeToken(dest), included: true }] : [],
|
|
1921
|
+
...q.integratorFeeUnits && q.integratorFeeUnits !== "0" ? [{ type: "affiliate", amount: q.integratorFeeUnits, token: feeToken(dest), included: true }] : []
|
|
1922
|
+
],
|
|
1923
|
+
etaSeconds: 240,
|
|
1924
|
+
reliability: 0.9,
|
|
1925
|
+
validUntilMs: Math.min(Date.now() + QUOTE_TTL_MS, deadlineMs)
|
|
1926
|
+
};
|
|
1927
|
+
},
|
|
1928
|
+
/**
|
|
1929
|
+
* TON-source only: generate the HTLC secret, persist it, build the escrow
|
|
1930
|
+
* transfer. The returned tx carries the payload as a serialized CELL
|
|
1931
|
+
* (`bodyBoc`, base64) — sending it as a text comment would strand the swap.
|
|
1932
|
+
*/
|
|
1933
|
+
async buildDeposit(quote) {
|
|
1934
|
+
if (!Number.isFinite(quote.validUntilMs) || quote.validUntilMs <= Date.now()) {
|
|
1935
|
+
throw new Error("omniston: quote expired \u2014 request a fresh quote");
|
|
1936
|
+
}
|
|
1937
|
+
const decoded = decodeIntent(quote.intentId);
|
|
1938
|
+
if (!decoded) throw new Error("omniston: malformed intent id");
|
|
1939
|
+
const cached = await loadBuilt(decoded.quoteId);
|
|
1940
|
+
if (!cached) throw new Error("omniston: quote expired \u2014 request a fresh quote");
|
|
1941
|
+
const ref = requireBinding(decoded, cached, "buildDeposit");
|
|
1942
|
+
const q = cached.quote;
|
|
1943
|
+
if (q.settlementData.$case === "swap") return await buildSwapDeposit(ref, q, q.settlementData.value);
|
|
1944
|
+
if (q.settlementData.$case !== "order" || q.settlementData.value.tradeStartDeadline * 1e3 <= Date.now()) {
|
|
1945
|
+
throw new Error("omniston: quote trade deadline expired \u2014 request a fresh quote");
|
|
1946
|
+
}
|
|
1947
|
+
if (!ref.trader) throw new Error("omniston: quote was requested without a refund/source address");
|
|
1948
|
+
const srcCase = q.inputAsset.chain.$case;
|
|
1949
|
+
if (srcCase !== "ton") return await buildEvmOrder(ref, q, q.settlementData.value, cached.bound);
|
|
1950
|
+
const srcChain = ref.srcChain;
|
|
1951
|
+
if (!isTon(srcChain)) throw new Error("omniston: source chain does not match TON quote");
|
|
1952
|
+
const destCase = q.outputAsset.chain.$case;
|
|
1953
|
+
const traderDst = destCase === "ton" ? null : { chain: { $case: destCase, value: (0, import_viem.getAddress)(ref.dest) } };
|
|
1954
|
+
if (!traderDst) throw new Error("omniston: TON-destination orders are not supported");
|
|
1955
|
+
const settlement = q.settlementData;
|
|
1956
|
+
const hashingFunction = settlement.$case === "order" ? settlement.value.htlcHashingFunction : void 0;
|
|
1957
|
+
const { secrets, created } = await secretsFor(ref, q, maxExecutions);
|
|
1958
|
+
const hashes = secrets.map((s) => hashlock(s, hashingFunction));
|
|
1959
|
+
const forget = async () => {
|
|
1960
|
+
if (created) await clearSecrets(ref.quoteId);
|
|
1961
|
+
};
|
|
1962
|
+
const owner = { chain: { $case: "ton", value: ref.trader } };
|
|
1963
|
+
const omni = await getClient();
|
|
1964
|
+
let tx;
|
|
1965
|
+
try {
|
|
1966
|
+
tx = await omni.tonBuildEscrowTransfer({
|
|
1967
|
+
quoteId: ref.quoteId,
|
|
1968
|
+
ownerSrcAddress: owner,
|
|
1969
|
+
transferSrcAddress: owner,
|
|
1970
|
+
refundSrcAddress: owner,
|
|
1971
|
+
gasExcessAddress: owner,
|
|
1972
|
+
traderDstAddress: traderDst,
|
|
1973
|
+
htlcSecrets: { secretMode: { $case: "provided", value: { hashes } } }
|
|
1974
|
+
});
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
await forget();
|
|
1977
|
+
throw error;
|
|
1978
|
+
}
|
|
1979
|
+
const [message, ...rest] = tx.messages;
|
|
1980
|
+
if (!message || rest.length > 0) {
|
|
1981
|
+
await forget();
|
|
1982
|
+
throw new Error(`omniston: expected exactly one escrow message, got ${tx.messages.length}`);
|
|
1983
|
+
}
|
|
1984
|
+
if (!isPositiveInteger(message.sendAmount) || BigInt(message.sendAmount) > BigInt(maxTonExecutionNano)) {
|
|
1985
|
+
await forget();
|
|
1986
|
+
throw new Error("omniston: resolver TON execution value exceeds the configured safety cap");
|
|
1987
|
+
}
|
|
1988
|
+
const expectedProtocol = q.settlementData.value.srcProtocolContractAddress;
|
|
1989
|
+
const initialResolver = q.settlementData.value.srcInitialResolverAddress;
|
|
1990
|
+
const jettonMaster = assetContract(q.inputAsset);
|
|
1991
|
+
if (expectedProtocol.chain.$case !== "ton" || initialResolver?.chain.$case !== "ton" || !jettonMaster) {
|
|
1992
|
+
await forget();
|
|
1993
|
+
throw new Error("omniston: incomplete TON settlement binding in quote");
|
|
1994
|
+
}
|
|
1995
|
+
if (message.jettonWalletStateInit) {
|
|
1996
|
+
await forget();
|
|
1997
|
+
throw new Error("omniston: deploying a resolver-provided jetton wallet is not supported");
|
|
1998
|
+
}
|
|
1999
|
+
let walletValid = false;
|
|
2000
|
+
try {
|
|
2001
|
+
walletValid = opts.validateTonJettonWallet ? await opts.validateTonJettonWallet({
|
|
2002
|
+
ownerAddress: ref.trader,
|
|
2003
|
+
jettonMasterAddress: jettonMaster,
|
|
2004
|
+
walletAddress: message.targetAddress
|
|
2005
|
+
}) : false;
|
|
2006
|
+
} catch (error) {
|
|
2007
|
+
await forget();
|
|
2008
|
+
throw error;
|
|
2009
|
+
}
|
|
2010
|
+
if (!walletValid) {
|
|
2011
|
+
await forget();
|
|
2012
|
+
throw new Error("omniston: TON jetton wallet was not independently validated");
|
|
2013
|
+
}
|
|
2014
|
+
try {
|
|
2015
|
+
if (!opts.validateTonEscrowPayload) {
|
|
2016
|
+
throw new Error("omniston: TON source requires an independent escrow-payload validator");
|
|
2017
|
+
}
|
|
2018
|
+
await validateTonEscrowPayload(message, {
|
|
2019
|
+
quoteId: ref.quoteId,
|
|
2020
|
+
inputUnits: q.inputUnits,
|
|
2021
|
+
resolverSendsUnits: q.settlementData.value.resolverSendsUnits,
|
|
2022
|
+
srcHtlcSecurityDepositUnits: q.settlementData.value.srcHtlcSecurityDepositUnits ?? "",
|
|
2023
|
+
protocolAddress: expectedProtocol.chain.value,
|
|
2024
|
+
ownerAddress: ref.trader,
|
|
2025
|
+
destinationChain: q.outputAsset.chain.$case,
|
|
2026
|
+
destinationAddress: ref.dest,
|
|
2027
|
+
outputTokenAddress: assetContract(q.outputAsset) ?? "",
|
|
2028
|
+
initialResolverAddress: initialResolver.chain.value,
|
|
2029
|
+
hashes,
|
|
2030
|
+
...hashingFunction ? { hashingFunction } : {},
|
|
2031
|
+
exclusivityTimeout: q.settlementData.value.exclusivityTimeout ?? 0
|
|
2032
|
+
}, opts.validateTonEscrowPayload);
|
|
2033
|
+
} catch (error) {
|
|
2034
|
+
await forget();
|
|
2035
|
+
throw error;
|
|
2036
|
+
}
|
|
2037
|
+
return {
|
|
2038
|
+
chain: srcChain,
|
|
2039
|
+
to: message.targetAddress,
|
|
2040
|
+
value: message.sendAmount,
|
|
2041
|
+
// nanotons (gas + forward), NOT the jetton amount
|
|
2042
|
+
bodyBoc: hexToBase64(message.payload)
|
|
2043
|
+
};
|
|
2044
|
+
},
|
|
2045
|
+
/**
|
|
2046
|
+
* Snapshot the order and use ready phases only to discover candidate EVM
|
|
2047
|
+
* escrows. Disclosure follows only after independent finalized on-chain
|
|
2048
|
+
* validation; transient failures are retried on the next poll.
|
|
2049
|
+
*/
|
|
2050
|
+
async trackIntent(ref) {
|
|
2051
|
+
const decoded = decodeIntent(ref.intentId);
|
|
2052
|
+
if (!decoded) throw new Error("omniston: malformed intent id");
|
|
2053
|
+
if (decoded.kind === "swap") return await trackSwap(decoded, ref.sourceTxHash ?? null);
|
|
2054
|
+
const trace = traceFor(decoded.quoteId);
|
|
2055
|
+
const stored = await loadBuilt(decoded.quoteId);
|
|
2056
|
+
let cached;
|
|
2057
|
+
let bound = decoded;
|
|
2058
|
+
if (stored) {
|
|
2059
|
+
const result = bindIntent(decoded, stored);
|
|
2060
|
+
if (!result.ok && result.reason === "mismatch") {
|
|
2061
|
+
trace("intent-mismatch", { where: "trackIntent", field: result.field });
|
|
2062
|
+
throw new Error(`omniston: intent id does not match the quote it was issued for (${result.field})`);
|
|
2063
|
+
}
|
|
2064
|
+
if (result.ok) {
|
|
2065
|
+
if (result.legacy) trace("legacy-intent", { where: "trackIntent" });
|
|
2066
|
+
cached = stored;
|
|
2067
|
+
bound = result.ref;
|
|
2068
|
+
} else {
|
|
2069
|
+
trace("legacy-intent-expired", { where: "trackIntent" });
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
const omni = await getClient();
|
|
2073
|
+
const order = await awaitOrder(
|
|
2074
|
+
omni.orderTrack({
|
|
2075
|
+
quoteId: decoded.quoteId,
|
|
2076
|
+
traderAddress: toChainAddress(bound.srcChain, bound.trader) ?? { chain: { $case: "ton", value: bound.trader } }
|
|
2077
|
+
})
|
|
2078
|
+
);
|
|
2079
|
+
if (!order) return { state: "pending" };
|
|
2080
|
+
const secretsHex = await store.load(decoded.quoteId);
|
|
2081
|
+
if (!secretsHex || cached?.quote.settlementData.$case !== "order") trace("disclosure-skipped", { gate: "context", secrets: !!secretsHex, cached: !!cached, feedStatus: order.status });
|
|
2082
|
+
if (secretsHex && cached?.quote.settlementData.$case === "order") {
|
|
2083
|
+
const q = cached.quote;
|
|
2084
|
+
const settlement = q.settlementData.value;
|
|
2085
|
+
const indices = order.executions.map((execution) => execution.index);
|
|
2086
|
+
const validIndices = indices.every((index) => Number.isSafeInteger(index) && index >= 0 && index < secretsHex.length) && new Set(indices).size === indices.length;
|
|
2087
|
+
const outputTokenAddress = assetContract(q.outputAsset);
|
|
2088
|
+
const outputChain = q.outputAsset.chain.$case;
|
|
2089
|
+
const outputCaip = caipForChainCase(outputChain);
|
|
2090
|
+
const factory = settlement.dstProtocolContractAddress;
|
|
2091
|
+
const evmSource = q.inputAsset.chain.$case !== "ton";
|
|
2092
|
+
const tonDest = outputChain === "ton";
|
|
2093
|
+
const inputChain = q.inputAsset.chain.$case;
|
|
2094
|
+
const inputCaip = caipForChainCase(inputChain);
|
|
2095
|
+
const inputTokenAddress = assetContract(q.inputAsset);
|
|
2096
|
+
const evmOrder = cached.evmOrder;
|
|
2097
|
+
await Promise.all(
|
|
2098
|
+
order.executions.map(async (execution) => {
|
|
2099
|
+
const i = execution.index;
|
|
2100
|
+
const secret = validIndices ? secretsHex[i] : void 0;
|
|
2101
|
+
const phase = execution.outputPositionPhase;
|
|
2102
|
+
const position = execution.outputPositionAddress;
|
|
2103
|
+
const inputPosition = execution.inputPositionAddress;
|
|
2104
|
+
if (!secret || !validDisclosurePhase(phase) || !position || position.chain.$case !== outputChain || !position.chain.value || !factory || factory.chain.$case !== outputChain || !outputTokenAddress || !outputCaip) {
|
|
2105
|
+
trace("disclosure-skipped", { gate: "position", index: i, phase, positionChain: position?.chain.$case, outputChain });
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
if (evmSource ? !evmOrder || !inputCaip || !inputTokenAddress || !inputPosition || inputPosition.chain.$case !== inputChain || !inputPosition.chain.value || !settlement.srcInitialResolverAddress || typeof opts.validateEvmInputPosition !== "function" || typeof (tonDest ? opts.validateTonOutputPosition : opts.validateEvmOutputPosition) !== "function" : typeof opts.validateEvmOutputPosition !== "function") {
|
|
2109
|
+
trace("disclosure-skipped", { gate: "context-fields", index: i, evmSource, evmOrder: !!evmOrder, inputPositionChain: inputPosition?.chain.$case });
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2112
|
+
const secretBytes = hexToBytes(secret);
|
|
2113
|
+
const expectedHash = hashlock(secretBytes, settlement.htlcHashingFunction);
|
|
2114
|
+
if (!execution.secretHash || !bytesEqual(execution.secretHash, expectedHash) || execution.inputUnits !== q.inputUnits || execution.outputUnits !== q.outputUnits || execution.resolverSendsUnits !== settlement.resolverSendsUnits) {
|
|
2115
|
+
trace("disclosure-skipped", { gate: "terms", index: i, hashMatches: !!execution.secretHash && bytesEqual(execution.secretHash, expectedHash), inputUnits: [execution.inputUnits, q.inputUnits], outputUnits: [execution.outputUnits, q.outputUnits], resolverSendsUnits: [execution.resolverSendsUnits, settlement.resolverSendsUnits] });
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
let verified = false;
|
|
2119
|
+
try {
|
|
2120
|
+
const evmOutput = () => opts.validateEvmOutputPosition({
|
|
2121
|
+
chain: outputCaip,
|
|
2122
|
+
positionAddress: position.chain.value,
|
|
2123
|
+
factoryAddress: factory.chain.value,
|
|
2124
|
+
quoteTimestamp: q.quoteTimestamp,
|
|
2125
|
+
executionIndex: i,
|
|
2126
|
+
recipientAddress: bound.dest,
|
|
2127
|
+
outputTokenAddress,
|
|
2128
|
+
inputUnits: execution.inputUnits,
|
|
2129
|
+
outputUnits: execution.outputUnits,
|
|
2130
|
+
resolverSendsUnits: execution.resolverSendsUnits,
|
|
2131
|
+
srcHtlcSecurityDepositUnits: settlement.srcHtlcSecurityDepositUnits ?? "",
|
|
2132
|
+
dstSecurityDepositUnits: settlement.dstSecurityDepositUnits ?? "",
|
|
2133
|
+
protocolFeeUnits: q.protocolFeeUnits ?? "0",
|
|
2134
|
+
integratorFeeUnits: q.integratorFeeUnits ?? "0",
|
|
2135
|
+
...q.integratorAddress?.chain.value ? { integratorAddress: q.integratorAddress.chain.value } : {},
|
|
2136
|
+
...settlement.protocolFeeRecipientAddress?.chain.value ? { protocolFeeRecipientAddress: settlement.protocolFeeRecipientAddress.chain.value } : {},
|
|
2137
|
+
secretHash: expectedHash
|
|
2138
|
+
});
|
|
2139
|
+
verified = evmSource ? await opts.validateEvmInputPosition({
|
|
2140
|
+
chain: inputCaip,
|
|
2141
|
+
positionAddress: inputPosition.chain.value,
|
|
2142
|
+
factoryAddress: evmOrder.factory,
|
|
2143
|
+
orderProtocolAddress: settlement.srcProtocolContractAddress.chain.value,
|
|
2144
|
+
ownerAddress: evmOrder.owner,
|
|
2145
|
+
resolverAddress: settlement.srcInitialResolverAddress.chain.value,
|
|
2146
|
+
inputTokenAddress,
|
|
2147
|
+
inputUnits: execution.inputUnits,
|
|
2148
|
+
resolverSendsUnits: execution.resolverSendsUnits,
|
|
2149
|
+
srcHtlcSecurityDepositUnits: settlement.srcHtlcSecurityDepositUnits ?? "",
|
|
2150
|
+
quoteTimestamp: q.quoteTimestamp,
|
|
2151
|
+
secretHash: expectedHash,
|
|
2152
|
+
...tonDest ? {} : {
|
|
2153
|
+
destinationChainId: evmRef2(outputCaip),
|
|
2154
|
+
destinationTokenAddress: outputTokenAddress,
|
|
2155
|
+
dstSecurityDepositUnits: settlement.dstSecurityDepositUnits ?? ""
|
|
2156
|
+
}
|
|
2157
|
+
}) && (tonDest ? await opts.validateTonOutputPosition({
|
|
2158
|
+
positionAddress: position.chain.value,
|
|
2159
|
+
protocolAddress: factory.chain.value,
|
|
2160
|
+
outputJettonMasterAddress: outputTokenAddress,
|
|
2161
|
+
recipientAddress: bound.dest,
|
|
2162
|
+
quoteId: decoded.quoteId,
|
|
2163
|
+
quoteTimestamp: q.quoteTimestamp,
|
|
2164
|
+
inputUnits: execution.inputUnits,
|
|
2165
|
+
outputUnits: execution.outputUnits,
|
|
2166
|
+
resolverSendsUnits: execution.resolverSendsUnits,
|
|
2167
|
+
dstSecurityDepositUnits: settlement.dstSecurityDepositUnits ?? "",
|
|
2168
|
+
secretHash: expectedHash
|
|
2169
|
+
}) : await evmOutput()) : await evmOutput();
|
|
2170
|
+
} catch (error) {
|
|
2171
|
+
verified = false;
|
|
2172
|
+
trace("validator-threw", { index: i, error: error instanceof Error ? error.message : String(error) });
|
|
2173
|
+
}
|
|
2174
|
+
if (!verified) {
|
|
2175
|
+
trace("validator-refused", { index: i, evmSource, tonDest });
|
|
2176
|
+
return;
|
|
2177
|
+
}
|
|
2178
|
+
await omni.orderDiscloseHtlcSecret({ quoteId: decoded.quoteId, executionIndex: i, secret: secretBytes }).then(async () => {
|
|
2179
|
+
trace("secret-disclosed", { index: i });
|
|
2180
|
+
await store.save(disclosedKey(decoded.quoteId), ["1"]);
|
|
2181
|
+
}, (error) => trace("disclose-failed", { index: i, error: error instanceof Error ? error.message : String(error) }));
|
|
2182
|
+
})
|
|
2183
|
+
);
|
|
2184
|
+
}
|
|
2185
|
+
const status = mapOrderStatus(order);
|
|
2186
|
+
const delivered = order.executions[0]?.outputPositionAddress;
|
|
2187
|
+
const outCase = cached?.quote.outputAsset.chain.$case;
|
|
2188
|
+
const outCaip = outCase ? caipForChainCase(outCase) : null;
|
|
2189
|
+
const outToken = cached ? assetContract(cached.quote.outputAsset) : null;
|
|
2190
|
+
if (status.state === "completed" && opts.locateEvmDelivery && outCase && outCase !== "ton" && outCaip && outToken && order.executions.length === 1 && delivered?.chain.$case === outCase && delivered.chain.value) {
|
|
2191
|
+
const hash = await opts.locateEvmDelivery({
|
|
2192
|
+
chain: outCaip,
|
|
2193
|
+
positionAddress: delivered.chain.value,
|
|
2194
|
+
recipientAddress: bound.dest,
|
|
2195
|
+
tokenAddress: outToken,
|
|
2196
|
+
minUnits: order.executions[0].outputUnits
|
|
2197
|
+
}).catch(() => null);
|
|
2198
|
+
if (hash) status.destTxHash = hash;
|
|
2199
|
+
}
|
|
2200
|
+
if (status.state === "completed" && !status.destTxHash && opts.locateTonDelivery && outCase === "ton" && outToken && order.executions.length === 1 && delivered?.chain.$case === "ton" && delivered.chain.value) {
|
|
2201
|
+
const hash = await opts.locateTonDelivery({
|
|
2202
|
+
positionAddress: delivered.chain.value,
|
|
2203
|
+
recipientAddress: bound.dest,
|
|
2204
|
+
jettonMasterAddress: outToken,
|
|
2205
|
+
minUnits: order.executions[0].outputUnits
|
|
2206
|
+
}).catch(() => null);
|
|
2207
|
+
if (hash) status.destTxHash = hash;
|
|
2208
|
+
}
|
|
2209
|
+
if (status.state === "completed") {
|
|
2210
|
+
await clearSecrets(decoded.quoteId);
|
|
2211
|
+
await quotes.clear(decoded.quoteId);
|
|
2212
|
+
}
|
|
2213
|
+
return status;
|
|
2214
|
+
},
|
|
2215
|
+
/** EVM→TON: hand the wallet's EIP-712 signature over the validated order to Omniston. */
|
|
2216
|
+
async registerSignedOrder(intentId, signature) {
|
|
2217
|
+
const ref = decodeIntent(intentId);
|
|
2218
|
+
if (!ref) throw new Error("omniston: malformed intent id");
|
|
2219
|
+
const cached = await loadBuilt(ref.quoteId);
|
|
2220
|
+
const order = cached?.evmOrder;
|
|
2221
|
+
if (!order) throw new Error("omniston: no signed-order context for this quote \u2014 build the order again");
|
|
2222
|
+
requireBinding(ref, cached, "registerSignedOrder");
|
|
2223
|
+
if (!/^0x[0-9a-fA-F]{130}$/.test(signature)) throw new Error("omniston: malformed order signature");
|
|
2224
|
+
const td = JSON.parse(order.typedData);
|
|
2225
|
+
const omni = await getClient();
|
|
2226
|
+
await omni.orderRegisterSignedOrder({
|
|
2227
|
+
quoteId: ref.quoteId,
|
|
2228
|
+
ownerSrcAddress: { chain: { $case: order.chainCase, value: order.owner } },
|
|
2229
|
+
signedOrder: { order: { $case: "evmV1", value: {
|
|
2230
|
+
encodedOrder: encodeTypedMessage(td),
|
|
2231
|
+
signature: hexToBytes(signature),
|
|
2232
|
+
orderExtension: hexToBytes(order.orderExtension)
|
|
2233
|
+
} } },
|
|
2234
|
+
...order.serializedOrderDetails ? { serializedOrderDetails: hexToBytes(order.serializedOrderDetails) } : {}
|
|
2235
|
+
});
|
|
2236
|
+
},
|
|
2237
|
+
close() {
|
|
2238
|
+
clientGeneration += 1;
|
|
2239
|
+
if (client && (client !== injectedClient || opts.closeInjectedClient)) {
|
|
2240
|
+
if (client.close) client.close();
|
|
2241
|
+
else client.transport?.close();
|
|
2242
|
+
}
|
|
2243
|
+
client = null;
|
|
2244
|
+
}
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
// src/protocol.ts
|
|
2249
|
+
var OmnistonSwap2Protocol = class extends import_single.SingleProviderSwap2 {
|
|
2250
|
+
provider;
|
|
2251
|
+
constructor(account, config) {
|
|
2252
|
+
super(account, config);
|
|
2253
|
+
this.provider = omnistonProvider(config);
|
|
2254
|
+
}
|
|
2255
|
+
chainSupported(caip2) {
|
|
2256
|
+
return omniston.supportsChain(caip2);
|
|
2257
|
+
}
|
|
2258
|
+
pairSupported(fromChain, toChain) {
|
|
2259
|
+
return this.provider.supportsRoute({ chain: fromChain, token: "" }, { chain: toChain, token: "" });
|
|
2260
|
+
}
|
|
2261
|
+
};
|
|
2262
|
+
//# sourceMappingURL=protocol.cjs.map
|