@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,1065 @@
|
|
|
1
|
+
// src/validators/evm-output.ts
|
|
2
|
+
import {
|
|
3
|
+
decodeAbiParameters,
|
|
4
|
+
decodeFunctionResult,
|
|
5
|
+
encodeFunctionData,
|
|
6
|
+
getAddress,
|
|
7
|
+
isAddress,
|
|
8
|
+
keccak256,
|
|
9
|
+
stringToHex
|
|
10
|
+
} from "viem";
|
|
11
|
+
var ESCROW_FACTORY_ABI = [
|
|
12
|
+
{
|
|
13
|
+
type: "function",
|
|
14
|
+
name: "createDstEscrow",
|
|
15
|
+
stateMutability: "payable",
|
|
16
|
+
inputs: [
|
|
17
|
+
{
|
|
18
|
+
name: "dstImmutables",
|
|
19
|
+
type: "tuple",
|
|
20
|
+
components: [
|
|
21
|
+
{ name: "orderHash", type: "bytes32" },
|
|
22
|
+
{ name: "hashlock", type: "bytes32" },
|
|
23
|
+
{ name: "maker", type: "uint256" },
|
|
24
|
+
{ name: "taker", type: "uint256" },
|
|
25
|
+
{ name: "token", type: "uint256" },
|
|
26
|
+
{ name: "amount", type: "uint256" },
|
|
27
|
+
{ name: "safetyDeposit", type: "uint256" },
|
|
28
|
+
{ name: "timelocks", type: "uint256" },
|
|
29
|
+
{ name: "parameters", type: "bytes" }
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
{ name: "srcCancellationTimestamp", type: "uint256" }
|
|
33
|
+
],
|
|
34
|
+
outputs: []
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
type: "function",
|
|
38
|
+
name: "addressOfEscrowDst",
|
|
39
|
+
stateMutability: "view",
|
|
40
|
+
inputs: [
|
|
41
|
+
{
|
|
42
|
+
name: "immutables",
|
|
43
|
+
type: "tuple",
|
|
44
|
+
components: [
|
|
45
|
+
{ name: "orderHash", type: "bytes32" },
|
|
46
|
+
{ name: "hashlock", type: "bytes32" },
|
|
47
|
+
{ name: "maker", type: "uint256" },
|
|
48
|
+
{ name: "taker", type: "uint256" },
|
|
49
|
+
{ name: "token", type: "uint256" },
|
|
50
|
+
{ name: "amount", type: "uint256" },
|
|
51
|
+
{ name: "safetyDeposit", type: "uint256" },
|
|
52
|
+
{ name: "timelocks", type: "uint256" },
|
|
53
|
+
{ name: "parameters", type: "bytes" }
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
outputs: [{ name: "escrow", type: "address" }]
|
|
58
|
+
}
|
|
59
|
+
];
|
|
60
|
+
var FACTORY_ABI = [{
|
|
61
|
+
type: "function",
|
|
62
|
+
name: "FACTORY",
|
|
63
|
+
stateMutability: "view",
|
|
64
|
+
inputs: [],
|
|
65
|
+
outputs: [{ type: "address" }]
|
|
66
|
+
}];
|
|
67
|
+
var BALANCE_OF_ABI = [{
|
|
68
|
+
type: "function",
|
|
69
|
+
name: "balanceOf",
|
|
70
|
+
stateMutability: "view",
|
|
71
|
+
inputs: [{ name: "account", type: "address" }],
|
|
72
|
+
outputs: [{ type: "uint256" }]
|
|
73
|
+
}];
|
|
74
|
+
var DST_ESCROW_CREATED_TOPIC = keccak256(stringToHex("DstEscrowCreated(address,bytes32,uint256)"));
|
|
75
|
+
var UINT32_MASK = (1n << 32n) - 1n;
|
|
76
|
+
var DEPLOYED_AT_OFFSET = 224n;
|
|
77
|
+
var DEPLOYED_AT_MASK = UINT32_MASK << DEPLOYED_AT_OFFSET;
|
|
78
|
+
var ADDRESS_MASK = (1n << 160n) - 1n;
|
|
79
|
+
var DST_STAGES = [1n, 120n, 180n];
|
|
80
|
+
var DST_STAGE_WORD = 4;
|
|
81
|
+
var DST_CANCELLATION_STAGE = 2;
|
|
82
|
+
var DISCLOSURE_MARGIN_SECONDS = 45n;
|
|
83
|
+
var FEE_PARAMETERS = [{ type: "uint256" }, { type: "uint256" }, { type: "uint256" }, { type: "uint256" }];
|
|
84
|
+
var FEE_PARAMETERS_HEX_LENGTH = 2 + 128 * 2;
|
|
85
|
+
var DEFAULT_RPC_URLS = {
|
|
86
|
+
"eip155:1": ["https://ethereum-rpc.publicnode.com"],
|
|
87
|
+
"eip155:56": ["https://bsc-rpc.publicnode.com"],
|
|
88
|
+
"eip155:137": ["https://polygon-bor-rpc.publicnode.com"],
|
|
89
|
+
"eip155:4663": ["https://rpc.mainnet.chain.robinhood.com"],
|
|
90
|
+
"eip155:8453": ["https://base-rpc.publicnode.com"],
|
|
91
|
+
"eip155:42161": ["https://arbitrum-one-rpc.publicnode.com"],
|
|
92
|
+
"eip155:43114": ["https://avalanche-c-chain-rpc.publicnode.com"],
|
|
93
|
+
"eip155:196": ["https://rpc.xlayer.tech"],
|
|
94
|
+
"eip155:999": ["https://rpc.hyperliquid.xyz/evm"],
|
|
95
|
+
"eip155:9745": ["https://rpc.plasma.to"]
|
|
96
|
+
};
|
|
97
|
+
var OMNISTON_EVM_LOOKBACK_BLOCKS = {
|
|
98
|
+
"eip155:1": 3000n,
|
|
99
|
+
"eip155:56": 40000n,
|
|
100
|
+
"eip155:137": 20000n,
|
|
101
|
+
"eip155:4663": 100000n,
|
|
102
|
+
"eip155:8453": 20000n,
|
|
103
|
+
"eip155:42161": 100000n,
|
|
104
|
+
"eip155:43114": 20000n,
|
|
105
|
+
// ~1 s blocks (measured 2026-10-20): ~11 h, past the 6 h quote horizon.
|
|
106
|
+
"eip155:196": 40000n,
|
|
107
|
+
"eip155:999": 40000n,
|
|
108
|
+
"eip155:9745": 40000n
|
|
109
|
+
};
|
|
110
|
+
var OMNISTON_EVM_MIN_CONFIRMATIONS = {
|
|
111
|
+
"eip155:1": 2n,
|
|
112
|
+
"eip155:56": 5n,
|
|
113
|
+
"eip155:137": 20n,
|
|
114
|
+
"eip155:4663": 20n,
|
|
115
|
+
"eip155:8453": 20n,
|
|
116
|
+
"eip155:42161": 20n,
|
|
117
|
+
"eip155:43114": 5n,
|
|
118
|
+
"eip155:196": 20n,
|
|
119
|
+
// sequencer rollup
|
|
120
|
+
"eip155:999": 5n,
|
|
121
|
+
// HyperBFT, fast finality
|
|
122
|
+
"eip155:9745": 5n
|
|
123
|
+
// PlasmaBFT, fast finality
|
|
124
|
+
};
|
|
125
|
+
function quantity(value) {
|
|
126
|
+
return `0x${value.toString(16)}`;
|
|
127
|
+
}
|
|
128
|
+
function uintAddress(value) {
|
|
129
|
+
return getAddress(`0x${(value & ADDRESS_MASK).toString(16).padStart(40, "0")}`);
|
|
130
|
+
}
|
|
131
|
+
function bytesHex(bytes) {
|
|
132
|
+
return `0x${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
133
|
+
}
|
|
134
|
+
function stage(timelocks, index) {
|
|
135
|
+
return timelocks >> BigInt(index * 32) & UINT32_MASK;
|
|
136
|
+
}
|
|
137
|
+
function decodeFeeParameters(parameters) {
|
|
138
|
+
if (parameters.length !== FEE_PARAMETERS_HEX_LENGTH) return null;
|
|
139
|
+
try {
|
|
140
|
+
const [protocolFee, integratorFee, protocolFeeRecipient, integratorFeeRecipient] = decodeAbiParameters(FEE_PARAMETERS, parameters);
|
|
141
|
+
return { protocolFee, integratorFee, protocolFeeRecipient, integratorFeeRecipient };
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function omnistonEvmRpcRequest(chain, method, params, rpcUrls) {
|
|
147
|
+
const urls = [...rpcUrls?.[chain] ?? [], ...DEFAULT_RPC_URLS[chain] ?? []].filter((url) => !!url);
|
|
148
|
+
if (!urls.length) throw new Error(`omniston: no RPC configured for ${chain}`);
|
|
149
|
+
let lastError;
|
|
150
|
+
for (const url of urls) {
|
|
151
|
+
try {
|
|
152
|
+
const response = await fetch(url, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers: { "content-type": "application/json" },
|
|
155
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
|
|
156
|
+
signal: AbortSignal.timeout(1e4)
|
|
157
|
+
});
|
|
158
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
159
|
+
const body = await response.json();
|
|
160
|
+
if (body.error !== void 0 || body.result === void 0) throw new Error("invalid JSON-RPC response");
|
|
161
|
+
return body.result;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
lastError = error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
throw lastError instanceof Error ? lastError : new Error(`omniston: RPC failed for ${chain}`);
|
|
167
|
+
}
|
|
168
|
+
async function ethCall(request, chain, to, data) {
|
|
169
|
+
return await request(chain, "eth_call", [{ to, data }, "latest"]);
|
|
170
|
+
}
|
|
171
|
+
function locateImmutables(input, secretHash) {
|
|
172
|
+
const data = input.slice(2).toLowerCase();
|
|
173
|
+
const needle = secretHash.slice(2).toLowerCase();
|
|
174
|
+
const at = data.indexOf(needle);
|
|
175
|
+
if (at < 72 || (at - 8) % 64 !== 0 || data.length < at + 64 * 8) return null;
|
|
176
|
+
const start = at - 64;
|
|
177
|
+
const word = (i) => BigInt(`0x${data.slice(at + 64 * i, at + 64 * (i + 1))}`);
|
|
178
|
+
try {
|
|
179
|
+
const offset = word(7);
|
|
180
|
+
if (offset % 32n !== 0n || offset < 288n || offset > 4096n) return null;
|
|
181
|
+
const lengthAt = start + Number(offset) * 2;
|
|
182
|
+
if (data.length < lengthAt + 64) return null;
|
|
183
|
+
const length = BigInt(`0x${data.slice(lengthAt, lengthAt + 64)}`);
|
|
184
|
+
if (length > 1024n || data.length < lengthAt + 64 + Number(length) * 2) return null;
|
|
185
|
+
return {
|
|
186
|
+
orderHash: `0x${data.slice(start, at)}`,
|
|
187
|
+
hashlock: `0x${needle}`,
|
|
188
|
+
maker: word(1),
|
|
189
|
+
taker: word(2),
|
|
190
|
+
token: word(3),
|
|
191
|
+
amount: word(4),
|
|
192
|
+
safetyDeposit: word(5),
|
|
193
|
+
timelocks: word(6),
|
|
194
|
+
parameters: `0x${data.slice(lengthAt + 64, lengthAt + 64 + Number(length) * 2)}`
|
|
195
|
+
};
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function createOmnistonEvmOutputValidator(options = {}) {
|
|
201
|
+
const refuse = (reason, details) => {
|
|
202
|
+
try {
|
|
203
|
+
options.onRefusal?.(reason, details);
|
|
204
|
+
} catch {
|
|
205
|
+
}
|
|
206
|
+
return false;
|
|
207
|
+
};
|
|
208
|
+
const request = options.request ?? ((chain, method, params) => omnistonEvmRpcRequest(chain, method, params, options.rpcUrls));
|
|
209
|
+
const now = options.now ?? Date.now;
|
|
210
|
+
return async (input) => {
|
|
211
|
+
try {
|
|
212
|
+
if (!OMNISTON_EVM_LOOKBACK_BLOCKS[input.chain] || input.executionIndex !== 0 || !isAddress(input.positionAddress) || !isAddress(input.factoryAddress) || !isAddress(input.recipientAddress) || !isAddress(input.outputTokenAddress) || !Number.isInteger(input.quoteTimestamp) || input.quoteTimestamp * 1e3 > now() + 12e4 || now() - input.quoteTimestamp * 1e3 > 6 * 60 * 60 * 1e3) return refuse("input-shape");
|
|
213
|
+
const position = getAddress(input.positionAddress);
|
|
214
|
+
const factory = getAddress(input.factoryAddress);
|
|
215
|
+
const recipient = getAddress(input.recipientAddress);
|
|
216
|
+
const token = getAddress(input.outputTokenAddress);
|
|
217
|
+
const secretHash = bytesHex(input.secretHash);
|
|
218
|
+
const outputUnits = BigInt(input.outputUnits);
|
|
219
|
+
const resolverSendsUnits = BigInt(input.resolverSendsUnits);
|
|
220
|
+
const protocolFeeUnits = BigInt(input.protocolFeeUnits);
|
|
221
|
+
const integratorFeeUnits = BigInt(input.integratorFeeUnits);
|
|
222
|
+
const dstDeposit = BigInt(input.dstSecurityDepositUnits);
|
|
223
|
+
if (BigInt(input.inputUnits) <= 0n || outputUnits <= 0n || dstDeposit <= 0n || resolverSendsUnits !== outputUnits + protocolFeeUnits + integratorFeeUnits || input.secretHash.length !== 32) return refuse("quote-amounts", { outputUnits: String(outputUnits), resolverSendsUnits: String(resolverSendsUnits), protocolFeeUnits: String(protocolFeeUnits), integratorFeeUnits: String(integratorFeeUnits) });
|
|
224
|
+
if (protocolFeeUnits < 0n || integratorFeeUnits < 0n) return refuse("quote-amounts", { protocolFeeUnits: String(protocolFeeUnits), integratorFeeUnits: String(integratorFeeUnits) });
|
|
225
|
+
if (protocolFeeUnits > 0n && (!input.protocolFeeRecipientAddress || !isAddress(input.protocolFeeRecipientAddress))) {
|
|
226
|
+
return refuse("quote-missing-protocol-fee-recipient", { protocolFeeUnits: String(protocolFeeUnits) });
|
|
227
|
+
}
|
|
228
|
+
if (integratorFeeUnits > 0n && (!input.integratorAddress || !isAddress(input.integratorAddress))) {
|
|
229
|
+
return refuse("quote-missing-integrator-address", { integratorFeeUnits: String(integratorFeeUnits) });
|
|
230
|
+
}
|
|
231
|
+
const latest = BigInt(await request(input.chain, "eth_blockNumber", []));
|
|
232
|
+
const minConfirmations = OMNISTON_EVM_MIN_CONFIRMATIONS[input.chain] ?? 20n;
|
|
233
|
+
const newestFinal = latest >= minConfirmations ? latest - minConfirmations : 0n;
|
|
234
|
+
const oldest = newestFinal > OMNISTON_EVM_LOOKBACK_BLOCKS[input.chain] ? newestFinal - OMNISTON_EVM_LOOKBACK_BLOCKS[input.chain] : 0n;
|
|
235
|
+
let matchingLog;
|
|
236
|
+
for (let to = newestFinal; to >= oldest && !matchingLog; ) {
|
|
237
|
+
const from = to - oldest > 4999n ? to - 4999n : oldest;
|
|
238
|
+
const logs = await request(input.chain, "eth_getLogs", [{
|
|
239
|
+
address: factory,
|
|
240
|
+
topics: [DST_ESCROW_CREATED_TOPIC],
|
|
241
|
+
fromBlock: quantity(from),
|
|
242
|
+
toBlock: quantity(to)
|
|
243
|
+
}]);
|
|
244
|
+
matchingLog = logs.find((log) => {
|
|
245
|
+
try {
|
|
246
|
+
const [escrow, hashlock] = decodeAbiParameters(
|
|
247
|
+
[{ type: "address" }, { type: "bytes32" }, { type: "uint256" }],
|
|
248
|
+
log.data
|
|
249
|
+
);
|
|
250
|
+
return getAddress(escrow) === position && hashlock.toLowerCase() === secretHash.toLowerCase();
|
|
251
|
+
} catch {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
if (from === oldest) break;
|
|
256
|
+
to = from - 1n;
|
|
257
|
+
}
|
|
258
|
+
if (!matchingLog) return refuse("no-escrow-event", { factory, lookback: String(OMNISTON_EVM_LOOKBACK_BLOCKS[input.chain]) });
|
|
259
|
+
const [transaction, receipt, block, code] = await Promise.all([
|
|
260
|
+
request(input.chain, "eth_getTransactionByHash", [matchingLog.transactionHash]),
|
|
261
|
+
request(input.chain, "eth_getTransactionReceipt", [matchingLog.transactionHash]),
|
|
262
|
+
request(input.chain, "eth_getBlockByNumber", [matchingLog.blockNumber, false]),
|
|
263
|
+
request(input.chain, "eth_getCode", [position, "latest"])
|
|
264
|
+
]);
|
|
265
|
+
if (!transaction || !receipt || !block || !code || code === "0x" || receipt.status !== "0x1" || BigInt(receipt.blockNumber) !== BigInt(matchingLog.blockNumber) || latest - BigInt(receipt.blockNumber) < minConfirmations || BigInt(block.timestamp) + 120n < BigInt(input.quoteTimestamp)) return refuse("receipt-or-confirmations", { receiptStatus: receipt?.status, receiptBlock: receipt?.blockNumber, logBlock: matchingLog.blockNumber, latest: String(latest), minConfirmations: String(minConfirmations), codeLength: code ? code.length : 0, blockTimestamp: block?.timestamp });
|
|
266
|
+
const immutables = locateImmutables(transaction.input, secretHash);
|
|
267
|
+
if (!immutables) return refuse("immutables-not-found", { transaction: matchingLog.transactionHash });
|
|
268
|
+
const stamped = { ...immutables, timelocks: immutables.timelocks & ~DEPLOYED_AT_MASK | BigInt(block.timestamp) << DEPLOYED_AT_OFFSET };
|
|
269
|
+
const derived = decodeFunctionResult({
|
|
270
|
+
abi: ESCROW_FACTORY_ABI,
|
|
271
|
+
functionName: "addressOfEscrowDst",
|
|
272
|
+
data: await ethCall(request, input.chain, factory, encodeFunctionData({ abi: ESCROW_FACTORY_ABI, functionName: "addressOfEscrowDst", args: [stamped] }))
|
|
273
|
+
});
|
|
274
|
+
if (getAddress(derived) !== position) return refuse("derived-address", { derived, position });
|
|
275
|
+
if (!(uintAddress(immutables.maker) === recipient && uintAddress(immutables.token) === token && immutables.amount === resolverSendsUnits && immutables.safetyDeposit === dstDeposit)) return refuse("escrow-terms", { maker: uintAddress(immutables.maker), recipient, token: uintAddress(immutables.token), expectedToken: token, amount: String(immutables.amount), resolverSendsUnits: String(resolverSendsUnits), safetyDeposit: String(immutables.safetyDeposit), dstDeposit: String(dstDeposit) });
|
|
276
|
+
const fees = decodeFeeParameters(immutables.parameters);
|
|
277
|
+
if (!fees || fees.protocolFeeRecipient > ADDRESS_MASK || fees.integratorFeeRecipient > ADDRESS_MASK) {
|
|
278
|
+
return refuse("fee-parameters-shape", { parametersLength: (immutables.parameters.length - 2) / 2 });
|
|
279
|
+
}
|
|
280
|
+
if (fees.protocolFee !== protocolFeeUnits || fees.integratorFee !== integratorFeeUnits || immutables.amount - fees.protocolFee - fees.integratorFee !== outputUnits) {
|
|
281
|
+
return refuse("fee-amounts", { protocolFee: String(fees.protocolFee), protocolFeeUnits: String(protocolFeeUnits), integratorFee: String(fees.integratorFee), integratorFeeUnits: String(integratorFeeUnits), amount: String(immutables.amount), outputUnits: String(outputUnits) });
|
|
282
|
+
}
|
|
283
|
+
if (fees.protocolFee > 0n && uintAddress(fees.protocolFeeRecipient) !== getAddress(input.protocolFeeRecipientAddress)) {
|
|
284
|
+
return refuse("protocol-fee-recipient", { escrow: uintAddress(fees.protocolFeeRecipient), quote: getAddress(input.protocolFeeRecipientAddress) });
|
|
285
|
+
}
|
|
286
|
+
if (fees.integratorFee > 0n && uintAddress(fees.integratorFeeRecipient) !== getAddress(input.integratorAddress)) {
|
|
287
|
+
return refuse("integrator-fee-recipient", { escrow: uintAddress(fees.integratorFeeRecipient), quote: getAddress(input.integratorAddress) });
|
|
288
|
+
}
|
|
289
|
+
if (DST_STAGES.some((expected, index) => stage(immutables.timelocks, DST_STAGE_WORD + index) !== expected)) {
|
|
290
|
+
return refuse("dst-timelocks", { stages: DST_STAGES.map((_, index) => String(stage(immutables.timelocks, DST_STAGE_WORD + index))) });
|
|
291
|
+
}
|
|
292
|
+
const deployedAt = BigInt(block.timestamp);
|
|
293
|
+
const nowSeconds = BigInt(Math.floor(now() / 1e3));
|
|
294
|
+
if (nowSeconds + DISCLOSURE_MARGIN_SECONDS >= deployedAt + DST_STAGES[DST_CANCELLATION_STAGE]) {
|
|
295
|
+
return refuse("dst-cancellation-window", { now: String(nowSeconds), deployedAt: String(deployedAt), cancellationAt: String(deployedAt + DST_STAGES[DST_CANCELLATION_STAGE]), marginSeconds: String(DISCLOSURE_MARGIN_SECONDS) });
|
|
296
|
+
}
|
|
297
|
+
const [reportedFactory, tokenBalance, nativeBalance] = await Promise.all([
|
|
298
|
+
ethCall(request, input.chain, position, encodeFunctionData({ abi: FACTORY_ABI, functionName: "FACTORY" })).then((data) => decodeFunctionResult({ abi: FACTORY_ABI, functionName: "FACTORY", data })),
|
|
299
|
+
ethCall(request, input.chain, token, encodeFunctionData({ abi: BALANCE_OF_ABI, functionName: "balanceOf", args: [position] })).then((data) => decodeFunctionResult({ abi: BALANCE_OF_ABI, functionName: "balanceOf", data })),
|
|
300
|
+
request(input.chain, "eth_getBalance", [position, "latest"])
|
|
301
|
+
]);
|
|
302
|
+
const ok = getAddress(reportedFactory) === factory && tokenBalance >= resolverSendsUnits && BigInt(nativeBalance) >= dstDeposit;
|
|
303
|
+
return ok || refuse("escrow-state", { reportedFactory, factory, tokenBalance: String(tokenBalance), resolverSendsUnits: String(resolverSendsUnits), nativeBalance: String(BigInt(nativeBalance)), dstDeposit: String(dstDeposit) });
|
|
304
|
+
} catch (error) {
|
|
305
|
+
return refuse("error", { message: error instanceof Error ? error.message : String(error) });
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
var TRANSFER_TOPIC = keccak256(stringToHex("Transfer(address,address,uint256)"));
|
|
310
|
+
var topicAddress = (address) => `0x${getAddress(address).slice(2).toLowerCase().padStart(64, "0")}`;
|
|
311
|
+
function createOmnistonEvmDeliveryLocator(options = {}) {
|
|
312
|
+
const request = options.request ?? ((chain, method, params) => omnistonEvmRpcRequest(chain, method, params, options.rpcUrls));
|
|
313
|
+
return async (input) => {
|
|
314
|
+
try {
|
|
315
|
+
const lookback = OMNISTON_EVM_LOOKBACK_BLOCKS[input.chain];
|
|
316
|
+
if (!lookback || !isAddress(input.positionAddress) || !isAddress(input.recipientAddress) || !isAddress(input.tokenAddress)) return null;
|
|
317
|
+
const min = BigInt(input.minUnits);
|
|
318
|
+
const latest = BigInt(await request(input.chain, "eth_blockNumber", []));
|
|
319
|
+
const oldest = latest > lookback ? latest - lookback : 0n;
|
|
320
|
+
for (let to = latest; to >= oldest; ) {
|
|
321
|
+
const from = to - oldest > 4999n ? to - 4999n : oldest;
|
|
322
|
+
const logs = await request(input.chain, "eth_getLogs", [{
|
|
323
|
+
address: getAddress(input.tokenAddress),
|
|
324
|
+
topics: [TRANSFER_TOPIC, topicAddress(input.positionAddress), topicAddress(input.recipientAddress)],
|
|
325
|
+
fromBlock: quantity(from),
|
|
326
|
+
toBlock: quantity(to)
|
|
327
|
+
}]);
|
|
328
|
+
const hit = [...logs].reverse().find((log) => {
|
|
329
|
+
try {
|
|
330
|
+
return getAddress(log.address) === getAddress(input.tokenAddress) && BigInt(log.data) >= min;
|
|
331
|
+
} catch {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
if (hit) return hit.transactionHash;
|
|
336
|
+
if (from === oldest) break;
|
|
337
|
+
to = from - 1n;
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
} catch {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/validators/evm-input.ts
|
|
347
|
+
import {
|
|
348
|
+
decodeEventLog,
|
|
349
|
+
decodeFunctionResult as decodeFunctionResult2,
|
|
350
|
+
encodeFunctionData as encodeFunctionData2,
|
|
351
|
+
getAddress as getAddress2,
|
|
352
|
+
isAddress as isAddress2,
|
|
353
|
+
keccak256 as keccak2562,
|
|
354
|
+
stringToHex as stringToHex2
|
|
355
|
+
} from "viem";
|
|
356
|
+
var IMMUTABLES = [
|
|
357
|
+
{ name: "orderHash", type: "bytes32" },
|
|
358
|
+
{ name: "hashlock", type: "bytes32" },
|
|
359
|
+
{ name: "maker", type: "uint256" },
|
|
360
|
+
{ name: "taker", type: "uint256" },
|
|
361
|
+
{ name: "token", type: "uint256" },
|
|
362
|
+
{ name: "amount", type: "uint256" },
|
|
363
|
+
{ name: "safetyDeposit", type: "uint256" },
|
|
364
|
+
{ name: "timelocks", type: "uint256" },
|
|
365
|
+
{ name: "parameters", type: "bytes" }
|
|
366
|
+
];
|
|
367
|
+
var DST_COMPLEMENT = [
|
|
368
|
+
{ name: "maker", type: "uint256" },
|
|
369
|
+
{ name: "amount", type: "uint256" },
|
|
370
|
+
{ name: "token", type: "uint256" },
|
|
371
|
+
{ name: "safetyDeposit", type: "uint256" },
|
|
372
|
+
{ name: "chainId", type: "uint256" },
|
|
373
|
+
{ name: "parameters", type: "bytes" }
|
|
374
|
+
];
|
|
375
|
+
var FACTORY_ABI2 = [
|
|
376
|
+
{
|
|
377
|
+
type: "event",
|
|
378
|
+
name: "SrcEscrowCreated",
|
|
379
|
+
inputs: [
|
|
380
|
+
{ name: "srcImmutables", type: "tuple", components: IMMUTABLES, indexed: false },
|
|
381
|
+
{ name: "dstImmutablesComplement", type: "tuple", components: DST_COMPLEMENT, indexed: false }
|
|
382
|
+
]
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
type: "function",
|
|
386
|
+
name: "addressOfEscrowSrc",
|
|
387
|
+
stateMutability: "view",
|
|
388
|
+
inputs: [{ name: "immutables", type: "tuple", components: IMMUTABLES }],
|
|
389
|
+
outputs: [{ name: "escrow", type: "address" }]
|
|
390
|
+
},
|
|
391
|
+
{ type: "function", name: "FACTORY", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] },
|
|
392
|
+
{ type: "function", name: "balanceOf", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ type: "uint256" }] }
|
|
393
|
+
];
|
|
394
|
+
var SRC_ESCROW_CREATED_TOPIC = keccak2562(stringToHex2(
|
|
395
|
+
"SrcEscrowCreated((bytes32,bytes32,uint256,uint256,uint256,uint256,uint256,uint256,bytes),(uint256,uint256,uint256,uint256,uint256,bytes))"
|
|
396
|
+
));
|
|
397
|
+
var SRC_STAGES = [1n, 120n, 240n, 300n, 1n, 120n, 180n];
|
|
398
|
+
var SRC_CANCELLATION_STAGE = 2;
|
|
399
|
+
var DISCLOSURE_MARGIN_SECONDS2 = 45n;
|
|
400
|
+
var UINT32_MASK2 = (1n << 32n) - 1n;
|
|
401
|
+
var ADDRESS_MASK2 = (1n << 160n) - 1n;
|
|
402
|
+
function quantity2(value) {
|
|
403
|
+
return `0x${value.toString(16)}`;
|
|
404
|
+
}
|
|
405
|
+
function uintAddress2(value) {
|
|
406
|
+
return getAddress2(`0x${(value & ADDRESS_MASK2).toString(16).padStart(40, "0")}`);
|
|
407
|
+
}
|
|
408
|
+
function bytesHex2(bytes) {
|
|
409
|
+
return `0x${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
410
|
+
}
|
|
411
|
+
function stage2(timelocks, index) {
|
|
412
|
+
return timelocks >> BigInt(index * 32) & UINT32_MASK2;
|
|
413
|
+
}
|
|
414
|
+
async function ethCall2(request, chain, to, data) {
|
|
415
|
+
return await request(chain, "eth_call", [{ to, data }, "latest"]);
|
|
416
|
+
}
|
|
417
|
+
function createOmnistonEvmInputValidator(options = {}) {
|
|
418
|
+
const refuse = (reason, details) => {
|
|
419
|
+
try {
|
|
420
|
+
options.onRefusal?.(reason, details);
|
|
421
|
+
} catch {
|
|
422
|
+
}
|
|
423
|
+
return false;
|
|
424
|
+
};
|
|
425
|
+
const request = options.request ?? ((chain, method, params) => omnistonEvmRpcRequest(chain, method, params, options.rpcUrls));
|
|
426
|
+
const now = options.now ?? Date.now;
|
|
427
|
+
return async (input) => {
|
|
428
|
+
try {
|
|
429
|
+
if (!OMNISTON_EVM_LOOKBACK_BLOCKS[input.chain] || !isAddress2(input.positionAddress) || !isAddress2(input.factoryAddress) || !isAddress2(input.orderProtocolAddress) || !isAddress2(input.ownerAddress) || !isAddress2(input.resolverAddress) || !isAddress2(input.inputTokenAddress) || !Number.isInteger(input.quoteTimestamp) || input.quoteTimestamp * 1e3 > now() + 12e4 || now() - input.quoteTimestamp * 1e3 > 6 * 60 * 60 * 1e3 || input.secretHash.length !== 32) return refuse("input-shape");
|
|
430
|
+
const position = getAddress2(input.positionAddress);
|
|
431
|
+
const factory = getAddress2(input.factoryAddress);
|
|
432
|
+
const owner = getAddress2(input.ownerAddress);
|
|
433
|
+
const resolver = getAddress2(input.resolverAddress);
|
|
434
|
+
const token = getAddress2(input.inputTokenAddress);
|
|
435
|
+
const secretHash = bytesHex2(input.secretHash).toLowerCase();
|
|
436
|
+
const inputUnits = BigInt(input.inputUnits);
|
|
437
|
+
const resolverSendsUnits = BigInt(input.resolverSendsUnits);
|
|
438
|
+
const safetyDeposit = BigInt(input.srcHtlcSecurityDepositUnits);
|
|
439
|
+
if (inputUnits <= 0n || resolverSendsUnits <= 0n || safetyDeposit <= 0n) return refuse("non-positive-amounts");
|
|
440
|
+
const latest = BigInt(await request(input.chain, "eth_blockNumber", []));
|
|
441
|
+
const minConfirmations = OMNISTON_EVM_MIN_CONFIRMATIONS[input.chain] ?? 20n;
|
|
442
|
+
const newestFinal = latest >= minConfirmations ? latest - minConfirmations : 0n;
|
|
443
|
+
const lookback = OMNISTON_EVM_LOOKBACK_BLOCKS[input.chain];
|
|
444
|
+
const oldest = newestFinal > lookback ? newestFinal - lookback : 0n;
|
|
445
|
+
let match;
|
|
446
|
+
for (let to = newestFinal; to >= oldest && !match; ) {
|
|
447
|
+
const from = to - oldest > 4999n ? to - 4999n : oldest;
|
|
448
|
+
const logs = await request(input.chain, "eth_getLogs", [{
|
|
449
|
+
address: factory,
|
|
450
|
+
topics: [SRC_ESCROW_CREATED_TOPIC],
|
|
451
|
+
fromBlock: quantity2(from),
|
|
452
|
+
toBlock: quantity2(to)
|
|
453
|
+
}]);
|
|
454
|
+
for (const log2 of logs) {
|
|
455
|
+
if (getAddress2(log2.address) !== factory) continue;
|
|
456
|
+
try {
|
|
457
|
+
const decoded = decodeEventLog({ abi: FACTORY_ABI2, eventName: "SrcEscrowCreated", data: log2.data, topics: log2.topics });
|
|
458
|
+
if (decoded.args.srcImmutables.hashlock.toLowerCase() === secretHash) {
|
|
459
|
+
match = { log: log2, immutables: decoded.args.srcImmutables, complement: decoded.args.dstImmutablesComplement };
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
} catch {
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (from === oldest) break;
|
|
466
|
+
to = from - 1n;
|
|
467
|
+
}
|
|
468
|
+
if (!match) return refuse("no-fill-event", { factory, lookback: String(OMNISTON_EVM_LOOKBACK_BLOCKS[input.chain]) });
|
|
469
|
+
const { immutables, complement, log } = match;
|
|
470
|
+
const deployedAt = stage2(immutables.timelocks, 7);
|
|
471
|
+
if (uintAddress2(immutables.maker) !== owner || uintAddress2(immutables.taker) !== resolver || uintAddress2(immutables.token) !== token || immutables.amount !== inputUnits || immutables.safetyDeposit !== safetyDeposit || immutables.parameters !== "0x" || SRC_STAGES.some((expected, index) => stage2(immutables.timelocks, index) !== expected) || deployedAt < BigInt(input.quoteTimestamp) - 120n || deployedAt > BigInt(input.quoteTimestamp) + 900n || // EVM destination: the complement is the destination escrow's amount, enforced by its CREATE2
|
|
472
|
+
// derivation — exact. TON destination: Omniston records a larger figure here (live 2026-10-20,
|
|
473
|
+
// both orders: 1.089 × resolverSendsUnits — 4 485 039 for 4 118 665, 791 340 for 726 690); the
|
|
474
|
+
// delivered amount is proven on the TON position itself (validateTonOutputPosition), so this
|
|
475
|
+
// only has to be no smaller than the promise. Strict equality blocked every EVM → TON disclosure.
|
|
476
|
+
(input.destinationChainId !== void 0 ? complement.amount !== resolverSendsUnits : complement.amount < resolverSendsUnits)) return refuse("refused");
|
|
477
|
+
if (input.destinationChainId !== void 0 && (complement.chainId !== BigInt(input.destinationChainId) || !input.destinationTokenAddress || !isAddress2(input.destinationTokenAddress) || uintAddress2(complement.token) !== getAddress2(input.destinationTokenAddress) || complement.safetyDeposit !== BigInt(input.dstSecurityDepositUnits ?? "-1"))) return refuse("complement-destination");
|
|
478
|
+
const derived = decodeFunctionResult2({
|
|
479
|
+
abi: FACTORY_ABI2,
|
|
480
|
+
functionName: "addressOfEscrowSrc",
|
|
481
|
+
data: await ethCall2(request, input.chain, factory, encodeFunctionData2({ abi: FACTORY_ABI2, functionName: "addressOfEscrowSrc", args: [immutables] }))
|
|
482
|
+
});
|
|
483
|
+
if (getAddress2(derived) !== position) return refuse("derived-address", { derived, position });
|
|
484
|
+
const [receipt, block, code] = await Promise.all([
|
|
485
|
+
request(input.chain, "eth_getTransactionReceipt", [log.transactionHash]),
|
|
486
|
+
request(input.chain, "eth_getBlockByNumber", [log.blockNumber, false]),
|
|
487
|
+
request(input.chain, "eth_getCode", [position, "latest"])
|
|
488
|
+
]);
|
|
489
|
+
if (!receipt || !block || !code || code === "0x" || receipt.status !== "0x1" || BigInt(receipt.blockNumber) !== BigInt(log.blockNumber) || latest - BigInt(receipt.blockNumber) < minConfirmations || BigInt(block.timestamp) !== deployedAt) return refuse("receipt-or-confirmations", { receiptStatus: receipt?.status, receiptBlock: receipt?.blockNumber, logBlock: log.blockNumber, latest: String(latest), minConfirmations: String(minConfirmations), codeLength: code ? code.length : 0, blockTimestamp: block?.timestamp, deployedAt: String(deployedAt) });
|
|
490
|
+
const nowSeconds = BigInt(Math.floor(now() / 1e3));
|
|
491
|
+
if (nowSeconds + DISCLOSURE_MARGIN_SECONDS2 >= deployedAt + SRC_STAGES[SRC_CANCELLATION_STAGE]) return refuse("receipt-or-confirmations", { receiptStatus: receipt?.status, receiptBlock: receipt?.blockNumber, logBlock: log.blockNumber, latest: String(latest), minConfirmations: String(minConfirmations), codeLength: code ? code.length : 0, blockTimestamp: block?.timestamp, deployedAt: String(deployedAt) });
|
|
492
|
+
const [reportedFactory, tokenBalance, nativeBalance] = await Promise.all([
|
|
493
|
+
ethCall2(request, input.chain, position, encodeFunctionData2({ abi: FACTORY_ABI2, functionName: "FACTORY" })).then((data) => decodeFunctionResult2({ abi: FACTORY_ABI2, functionName: "FACTORY", data })),
|
|
494
|
+
ethCall2(request, input.chain, token, encodeFunctionData2({ abi: FACTORY_ABI2, functionName: "balanceOf", args: [position] })).then((data) => decodeFunctionResult2({ abi: FACTORY_ABI2, functionName: "balanceOf", data })),
|
|
495
|
+
request(input.chain, "eth_getBalance", [position, "latest"])
|
|
496
|
+
]);
|
|
497
|
+
const ok = getAddress2(reportedFactory) === factory && tokenBalance >= inputUnits && BigInt(nativeBalance) >= safetyDeposit;
|
|
498
|
+
return ok || refuse("escrow-state", { reportedFactory, factory, tokenBalance: String(tokenBalance), inputUnits: String(inputUnits), nativeBalance: String(BigInt(nativeBalance)), safetyDeposit: String(safetyDeposit) });
|
|
499
|
+
} catch (error) {
|
|
500
|
+
return refuse("error", { message: error instanceof Error ? error.message : String(error) });
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/validators/ton.ts
|
|
506
|
+
import { Buffer } from "buffer";
|
|
507
|
+
import { Address, Cell } from "@ton/core";
|
|
508
|
+
var JETTON_TRANSFER_OP = 260734629;
|
|
509
|
+
var ESCROW_CREATE_OP = 3232100569;
|
|
510
|
+
var ESCROW_CONFIG_OP = 4073126499;
|
|
511
|
+
var ESCROW_EXECUTION_KECCAK_OP = 3369382952;
|
|
512
|
+
var MAX_FORWARD_NANO = 2000000000n;
|
|
513
|
+
var DEFAULT_OMNISTON_TON_CODE_HASHES = {
|
|
514
|
+
escrow: ["9e77cb9e8fd7406d4e85e5f57378eb51e51b862424cf988306c783d50ade697a"],
|
|
515
|
+
execution: ["f6edbc76e6fbcd6bac573cb942401a3d9198ee9605f9403b0f5c8081259210e5"],
|
|
516
|
+
authorityKeys: ["ed210da12aa96ea42de4f2f61dc172bbaa3da86051a1b2a6f342f69a777d2770"],
|
|
517
|
+
/** EVM→TON output position: the contract the factory deploys for the
|
|
518
|
+
* resolver's jetton lock (observed on every resolver-created position,
|
|
519
|
+
* 2026-10-15/16), and the escrow code cell it carries in its state. */
|
|
520
|
+
position: ["e4d5115a321ade7b827e38fb522c20d2f2cd46e4c6b0b5f7dd8b6b096792d77b"],
|
|
521
|
+
outputEscrow: ["d30423575384773abaad8d3c6f763898afa3d57a5693005904b7c61e95cee630"]
|
|
522
|
+
};
|
|
523
|
+
var POSITION_DEPLOY_OP = "0xf240cd30";
|
|
524
|
+
var POSITION_STAGES = { publicWithdrawalAfterWithdrawal: 119n, cancellationAfterWithdrawal: 179n };
|
|
525
|
+
var OUTPUT_DISCLOSURE_MARGIN_SECONDS = 45;
|
|
526
|
+
var MAX_OMNISTON_EXCLUSIVITY_SECONDS = 3600;
|
|
527
|
+
var DEFAULT_OMNISTON_TON_SWAP_INTERFACES = [
|
|
528
|
+
"stonfi_router",
|
|
529
|
+
"stonfi_router_v2",
|
|
530
|
+
"stonfi_pool",
|
|
531
|
+
"stonfi_pool_v2",
|
|
532
|
+
"dedust_vault",
|
|
533
|
+
"dedust_v2_cpmm",
|
|
534
|
+
"dedust_pool"
|
|
535
|
+
];
|
|
536
|
+
var DEFAULT_OMNISTON_TON_PROXY_TON_MASTERS = [
|
|
537
|
+
"EQCM3B12QK1e4yZSf8GtBRT0aLMNyEsBc_DhVfRRtOEffLez",
|
|
538
|
+
// pTON v1
|
|
539
|
+
"EQBnGWMCf3-FZZq1W4IWcWiGAc3PHuZ0_H-7sad2oY00o83S"
|
|
540
|
+
// pTON v2
|
|
541
|
+
];
|
|
542
|
+
function cellFromApi(value) {
|
|
543
|
+
const roots = Cell.fromBoc(Buffer.from(value, /^b5ee9c/i.test(value) ? "hex" : "base64"));
|
|
544
|
+
if (roots.length !== 1) throw new Error("omniston TON validator: account cell must have one root");
|
|
545
|
+
return roots[0];
|
|
546
|
+
}
|
|
547
|
+
var lastStateRefusal = null;
|
|
548
|
+
function validateOutputPositionState(data, input, expected) {
|
|
549
|
+
if (!/^[0-9a-fA-F]{48,64}$/.test(input.quoteId) || input.secretHash.length !== 32) {
|
|
550
|
+
lastStateRefusal = "state#1: if (!/^[0-9a-fA-F]{48,64}$/.test(input.quoteId) || input.secretHash.length !== 32) return null;";
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
if (!/^\d+$/.test(input.resolverSendsUnits) || !/^\d+$/.test(input.outputUnits) || !/^\d+$/.test(input.dstSecurityDepositUnits)) {
|
|
554
|
+
lastStateRefusal = "state#2: if (!/^\\d+$/.test(input.resolverSendsUnits) || !/^\\d+$/.test(input.outputUnits) || !/^\\d+$/.test(inp";
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
if (data.refs.length !== 3) {
|
|
558
|
+
lastStateRefusal = "state#3: if (data.refs.length !== 3) return null;";
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
const root = data.beginParse();
|
|
562
|
+
root.loadUint(8);
|
|
563
|
+
if (!sameHex(root.loadBuffer(32), input.quoteId.padStart(64, "0"))) {
|
|
564
|
+
lastStateRefusal = 'state#4: if (!sameHex(root.loadBuffer(32), input.quoteId.padStart(64, "0"))) return null;';
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
if (!sameAddress(root.loadAddressAny(), input.protocolAddress)) {
|
|
568
|
+
lastStateRefusal = "state#5: if (!sameAddress(root.loadAddressAny(), input.protocolAddress)) return null;";
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
if (!sameAddress(root.loadAddressAny(), expected.jettonWallet)) {
|
|
572
|
+
lastStateRefusal = "state#6: if (!sameAddress(root.loadAddressAny(), expected.jettonWallet)) return null;";
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
if (root.remainingBits !== 5 && root.remainingBits !== 29 || root.remainingRefs !== 3) {
|
|
576
|
+
lastStateRefusal = `state#7: root tail ${root.remainingBits} bits / ${root.remainingRefs} refs`;
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
const parties = data.refs[0];
|
|
580
|
+
if (parties.refs.length !== 2) {
|
|
581
|
+
lastStateRefusal = "state#8: if (parties.refs.length !== 2) return null;";
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
const partySlice = parties.beginParse();
|
|
585
|
+
if (!(partySlice.loadAddressAny() instanceof Address)) {
|
|
586
|
+
lastStateRefusal = "state#9: if (!(partySlice.loadAddressAny() instanceof Address)) return null; // owner (resolver) \u2014 any";
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
if (!sameAddress(partySlice.loadAddressAny(), input.outputJettonMasterAddress)) {
|
|
590
|
+
lastStateRefusal = "state#10: if (!sameAddress(partySlice.loadAddressAny(), input.outputJettonMasterAddress)) return null;";
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
if (partySlice.remainingBits !== 6) {
|
|
594
|
+
lastStateRefusal = "state#11: if (partySlice.remainingBits !== 6) return null;";
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
const lock = data.refs[1];
|
|
598
|
+
if (lock.refs.length !== 2) {
|
|
599
|
+
lastStateRefusal = "state#12: if (lock.refs.length !== 2) return null;";
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
const lockSlice = lock.beginParse();
|
|
603
|
+
if (!Buffer.from(lockSlice.loadBuffer(32)).equals(Buffer.from(input.secretHash))) {
|
|
604
|
+
lastStateRefusal = "state#13: if (!Buffer.from(lockSlice.loadBuffer(32)).equals(Buffer.from(input.secretHash))) return null;";
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
if (lockSlice.loadCoins() < BigInt(input.resolverSendsUnits)) {
|
|
608
|
+
lastStateRefusal = "state#14: if (lockSlice.loadCoins() < BigInt(input.resolverSendsUnits)) return null;";
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
const withdrawalAt = lockSlice.loadUintBig(64);
|
|
612
|
+
const publicWithdrawalAt = lockSlice.loadUintBig(64);
|
|
613
|
+
const cancellationAt = lockSlice.loadUintBig(64);
|
|
614
|
+
if (lockSlice.remainingBits !== 0) {
|
|
615
|
+
lastStateRefusal = "state#15: if (lockSlice.remainingBits !== 0) return null; // its two refs are the terms and authority cells be";
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
if (publicWithdrawalAt - withdrawalAt !== POSITION_STAGES.publicWithdrawalAfterWithdrawal) {
|
|
619
|
+
lastStateRefusal = "state#16: if (publicWithdrawalAt - withdrawalAt !== POSITION_STAGES.publicWithdrawalAfterWithdrawal) return nu";
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
if (cancellationAt - withdrawalAt !== POSITION_STAGES.cancellationAfterWithdrawal) {
|
|
623
|
+
lastStateRefusal = "state#17: if (cancellationAt - withdrawalAt !== POSITION_STAGES.cancellationAfterWithdrawal) return null;";
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
if (withdrawalAt < BigInt(input.quoteTimestamp) - 120n || withdrawalAt > BigInt(input.quoteTimestamp) + 900n) {
|
|
627
|
+
lastStateRefusal = "state#18: if (withdrawalAt < BigInt(input.quoteTimestamp) - 120n || withdrawalAt > BigInt(input.quoteTimestamp";
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
if (BigInt(expected.nowSeconds + OUTPUT_DISCLOSURE_MARGIN_SECONDS) >= cancellationAt) {
|
|
631
|
+
lastStateRefusal = "state#19: if (BigInt(expected.nowSeconds + OUTPUT_DISCLOSURE_MARGIN_SECONDS) >= cancellationAt) return null;";
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
const terms = lock.refs[0].beginParse();
|
|
635
|
+
if (!sameAddress(terms.loadAddressAny(), input.recipientAddress)) {
|
|
636
|
+
lastStateRefusal = "state#20: if (!sameAddress(terms.loadAddressAny(), input.recipientAddress)) return null;";
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
if (!(terms.loadAddressAny() instanceof Address)) {
|
|
640
|
+
lastStateRefusal = "state#21: if (!(terms.loadAddressAny() instanceof Address)) return null; // resolver \u2014 any";
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
if (terms.loadCoins() !== BigInt(input.dstSecurityDepositUnits) || !exactCellEnd(terms)) {
|
|
644
|
+
lastStateRefusal = "state#22: if (terms.loadCoins() !== BigInt(input.dstSecurityDepositUnits) || !exactCellEnd(terms)) return null";
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
const authority = lock.refs[1].beginParse();
|
|
648
|
+
if (!authority.loadBit() || !expected.trusted.authorityKeys.has(Buffer.from(authority.loadBuffer(32)).toString("hex"))) {
|
|
649
|
+
lastStateRefusal = "state#23: if (!authority.loadBit() || !expected.trusted.authorityKeys.has(Buffer.from(authority.loadBuffer(32)";
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
const authorityAddress = authority.loadAddressAny();
|
|
653
|
+
if (authorityAddress !== null && !sameAddress(authorityAddress, input.recipientAddress) || !exactCellEnd(authority)) {
|
|
654
|
+
lastStateRefusal = "state#24: if ((authorityAddress !== null && !sameAddress(authorityAddress, input.recipientAddress)) || !exactC";
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
if (!expected.trusted.outputEscrow.has(data.refs[2].hash().toString("hex"))) {
|
|
658
|
+
lastStateRefusal = 'state#25: if (!expected.trusted.outputEscrow.has(data.refs[2]!.hash().toString("hex"))) return null;';
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
return { withdrawalAt };
|
|
662
|
+
}
|
|
663
|
+
function oneRoot(hex, label) {
|
|
664
|
+
if (!/^(?:[0-9a-fA-F]{2})+$/.test(hex)) throw new Error(`omniston TON validator: malformed ${label} hex`);
|
|
665
|
+
const roots = Cell.fromBoc(Buffer.from(hex, "hex"));
|
|
666
|
+
if (roots.length !== 1) throw new Error(`omniston TON validator: ${label} must have one root`);
|
|
667
|
+
return roots[0];
|
|
668
|
+
}
|
|
669
|
+
function sameAddress(actual, expected) {
|
|
670
|
+
try {
|
|
671
|
+
return actual instanceof Address && actual.equals(Address.parse(expected));
|
|
672
|
+
} catch {
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function sameHex(actual, expected) {
|
|
677
|
+
const clean = expected.replace(/^0x/, "").toLowerCase();
|
|
678
|
+
return clean.length === actual.length * 2 && Buffer.from(actual).toString("hex") === clean;
|
|
679
|
+
}
|
|
680
|
+
function exactCellEnd(slice) {
|
|
681
|
+
return slice.remainingBits === 0 && slice.remainingRefs === 0;
|
|
682
|
+
}
|
|
683
|
+
function destinationChainId(chain) {
|
|
684
|
+
const ids = {
|
|
685
|
+
ethereum: 1n,
|
|
686
|
+
bnb: 56n,
|
|
687
|
+
polygon: 137n,
|
|
688
|
+
robinhood: 4663n,
|
|
689
|
+
base: 8453n,
|
|
690
|
+
arbitrum: 42161n,
|
|
691
|
+
avalanche: 43114n,
|
|
692
|
+
xlayer: 196n,
|
|
693
|
+
hyperevm: 999n,
|
|
694
|
+
plasma: 9745n
|
|
695
|
+
};
|
|
696
|
+
return ids[chain] ?? null;
|
|
697
|
+
}
|
|
698
|
+
function validatePayload(input, trusted) {
|
|
699
|
+
const exclusivityTimeout = input.exclusivityTimeout;
|
|
700
|
+
if (typeof exclusivityTimeout !== "number" || !Number.isSafeInteger(exclusivityTimeout) || exclusivityTimeout < 1 || exclusivityTimeout > MAX_OMNISTON_EXCLUSIVITY_SECONDS) return false;
|
|
701
|
+
if (!/^\d+$/.test(input.inputUnits) || !/^\d+$/.test(input.resolverSendsUnits) || !/^\d+$/.test(input.srcHtlcSecurityDepositUnits) || !/^[0-9a-fA-F]{48,64}$/.test(input.quoteId) || input.secretHashes.length !== 1 || input.secretHashes[0]?.length !== 32 || input.hashingFunction !== "HASHING_FUNCTION_KECCAK256") return false;
|
|
702
|
+
const payload = oneRoot(input.payloadBocHex, "payload");
|
|
703
|
+
const outer = payload.beginParse();
|
|
704
|
+
if (outer.loadUint(32) !== JETTON_TRANSFER_OP) return false;
|
|
705
|
+
outer.loadUintBig(64);
|
|
706
|
+
if (outer.loadCoins() !== BigInt(input.inputUnits)) return false;
|
|
707
|
+
if (!sameAddress(outer.loadAddressAny(), input.protocolAddress)) return false;
|
|
708
|
+
if (!sameAddress(outer.loadAddressAny(), input.ownerAddress)) return false;
|
|
709
|
+
if (outer.loadMaybeRef() !== null) return false;
|
|
710
|
+
const forwardNano = outer.loadCoins();
|
|
711
|
+
if (forwardNano <= 0n || forwardNano > MAX_FORWARD_NANO || !outer.loadBit()) return false;
|
|
712
|
+
if (outer.remainingBits !== 0 || outer.remainingRefs !== 1) return false;
|
|
713
|
+
const embeddedForward = outer.loadRef();
|
|
714
|
+
const forward = oneRoot(input.forwardPayloadBocHex, "forward payload");
|
|
715
|
+
if (!embeddedForward.hash().equals(forward.hash())) return false;
|
|
716
|
+
const root = forward.beginParse();
|
|
717
|
+
if (root.loadUint(32) !== ESCROW_CREATE_OP || !sameHex(root.loadBuffer(32), input.quoteId.padStart(64, "0"))) return false;
|
|
718
|
+
if (root.loadBit() || !sameAddress(root.loadAddressAny(), input.ownerAddress)) return false;
|
|
719
|
+
const escrowForwardNano = root.loadCoins();
|
|
720
|
+
if (escrowForwardNano <= 0n || escrowForwardNano > MAX_FORWARD_NANO || root.loadBit()) return false;
|
|
721
|
+
if (root.remainingBits !== 0 || root.remainingRefs !== 4) return false;
|
|
722
|
+
const [config, escrowCode, auxiliary, flags] = [root.loadRef(), root.loadRef(), root.loadRef(), root.loadRef()];
|
|
723
|
+
if (!trusted.escrow.has(escrowCode.hash().toString("hex"))) return false;
|
|
724
|
+
const configSlice = config.beginParse();
|
|
725
|
+
if (configSlice.loadUint(32) !== ESCROW_CONFIG_OP || !sameAddress(configSlice.loadAddressAny(), input.initialResolverAddress)) return false;
|
|
726
|
+
if (!configSlice.loadBit() || !trusted.authorityKeys.has(Buffer.from(configSlice.loadBuffer(32)).toString("hex"))) return false;
|
|
727
|
+
if (configSlice.loadUintBig(64) !== BigInt(exclusivityTimeout)) return false;
|
|
728
|
+
if (configSlice.loadUintBig(64) !== 0n) return false;
|
|
729
|
+
if (configSlice.remainingRefs !== 2) return false;
|
|
730
|
+
const state = configSlice.loadRef();
|
|
731
|
+
if (!configSlice.loadBit()) return false;
|
|
732
|
+
const destination = configSlice.loadRef();
|
|
733
|
+
if (configSlice.loadUint(16) !== 0 || !exactCellEnd(configSlice)) return false;
|
|
734
|
+
if (state.bits.length !== 0 || state.refs.length !== 2) return false;
|
|
735
|
+
if (!trusted.execution.has(state.refs[0].hash().toString("hex"))) return false;
|
|
736
|
+
const execution = state.refs[1].beginParse();
|
|
737
|
+
if (execution.loadUint(32) !== ESCROW_EXECUTION_KECCAK_OP || !Buffer.from(execution.loadBuffer(32)).equals(Buffer.from(input.secretHashes[0])) || !sameAddress(execution.loadAddressAny(), input.initialResolverAddress)) return false;
|
|
738
|
+
if ([1n, 120n, 240n, 300n].some((expected) => execution.loadUintBig(64) !== expected)) return false;
|
|
739
|
+
if (execution.remainingBits !== 0 || execution.remainingRefs !== 1) return false;
|
|
740
|
+
const ownerData = execution.loadRef().beginParse();
|
|
741
|
+
if (!sameAddress(ownerData.loadAddressAny(), input.ownerAddress) || ownerData.loadBit()) return false;
|
|
742
|
+
if (ownerData.loadCoins() !== BigInt(input.srcHtlcSecurityDepositUnits) || !exactCellEnd(ownerData)) return false;
|
|
743
|
+
const destinationSlice = destination.beginParse();
|
|
744
|
+
const chainId = destinationChainId(input.destinationChain);
|
|
745
|
+
if (chainId === null || destinationSlice.loadUintBig(64) !== chainId) return false;
|
|
746
|
+
if (!sameHex(destinationSlice.loadBuffer(20), input.outputTokenAddress)) return false;
|
|
747
|
+
if (!sameHex(destinationSlice.loadBuffer(20), input.destinationAddress)) return false;
|
|
748
|
+
if (destinationSlice.loadCoins() !== BigInt(input.resolverSendsUnits) || !exactCellEnd(destinationSlice)) return false;
|
|
749
|
+
if (auxiliary.bits.length !== 537 || auxiliary.refs.length !== 1) return false;
|
|
750
|
+
if (flags.bits.length !== 98 || flags.refs.length !== 0) return false;
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
function createOmnistonTonValidators(options = {}) {
|
|
754
|
+
const now = options.now ?? Date.now;
|
|
755
|
+
const swapInterfaces = new Set(options.swapTargetInterfaces ?? DEFAULT_OMNISTON_TON_SWAP_INTERFACES);
|
|
756
|
+
const proxyTonMasters = new Set(
|
|
757
|
+
(options.proxyTonMasters ?? DEFAULT_OMNISTON_TON_PROXY_TON_MASTERS).map((value) => Address.parse(value).toRawString())
|
|
758
|
+
);
|
|
759
|
+
const request = options.fetch ?? fetch;
|
|
760
|
+
const base = (options.tonApiUrl ?? "https://tonapi.io").replace(/\/$/, "");
|
|
761
|
+
const headers = options.tonApiKey ? { Authorization: `Bearer ${options.tonApiKey}` } : void 0;
|
|
762
|
+
const get = (path) => request(`${base}${path}`, { ...headers ? { headers } : {}, signal: AbortSignal.timeout(5e3) });
|
|
763
|
+
const getRetry = async (path) => {
|
|
764
|
+
for (let attempt = 0; ; attempt++) {
|
|
765
|
+
const response = await get(path);
|
|
766
|
+
if (response.status !== 429 || attempt >= 3) return response;
|
|
767
|
+
await new Promise((resolve) => setTimeout(resolve, 1100 * (attempt + 1)));
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
const runGet = async (address, method, args = []) => {
|
|
771
|
+
const query = args.length ? `?${args.map((arg) => `args=${encodeURIComponent(arg)}`).join("&")}` : "";
|
|
772
|
+
const response = await getRetry(`/v2/blockchain/accounts/${encodeURIComponent(address)}/methods/${method}${query}`);
|
|
773
|
+
if (!response.ok) return null;
|
|
774
|
+
const body = await response.json();
|
|
775
|
+
return body.success === true && (body.exit_code ?? 0) === 0 && Array.isArray(body.stack) ? body.stack : null;
|
|
776
|
+
};
|
|
777
|
+
const stackCell = (entry) => entry && (entry.type === "cell" || entry.type === "slice") && entry.cell ? cellFromApi(entry.cell) : null;
|
|
778
|
+
const derivedWallet = async (master, owner) => {
|
|
779
|
+
const cell = stackCell((await runGet(master, "get_wallet_address", [Address.parse(owner).toRawString()]))?.[0]);
|
|
780
|
+
return cell ? cell.beginParse().loadAddress() : null;
|
|
781
|
+
};
|
|
782
|
+
const walletData = async (wallet) => {
|
|
783
|
+
const stack = await runGet(wallet, "get_wallet_data");
|
|
784
|
+
const owner = stackCell(stack?.[1]);
|
|
785
|
+
const master = stackCell(stack?.[2]);
|
|
786
|
+
return owner && master ? { owner: owner.beginParse().loadAddress(), master: master.beginParse().loadAddress() } : null;
|
|
787
|
+
};
|
|
788
|
+
const accountHas = async (address, iface) => {
|
|
789
|
+
const response = await getRetry(`/v2/accounts/${encodeURIComponent(address)}`);
|
|
790
|
+
if (!response.ok) return false;
|
|
791
|
+
const body = await response.json();
|
|
792
|
+
return !!body.address && sameAddress(Address.parse(body.address), address) && body.is_wallet !== true && (body.interfaces ?? []).includes(iface);
|
|
793
|
+
};
|
|
794
|
+
const samePair = (pool, leg) => {
|
|
795
|
+
const key = (value) => value === null ? "native" : Address.parse(value).toRawString();
|
|
796
|
+
const a = pool.map(key).sort();
|
|
797
|
+
const b = leg.map(key).sort();
|
|
798
|
+
return a.length === 2 && a[0] === b[0] && a[1] === b[1] && a[0] !== a[1];
|
|
799
|
+
};
|
|
800
|
+
const readAccount = async (address) => {
|
|
801
|
+
const response = await get(`/v2/accounts/${encodeURIComponent(address)}`);
|
|
802
|
+
if (!response.ok) return null;
|
|
803
|
+
const body = await response.json();
|
|
804
|
+
if (!body.address || !sameAddress(Address.parse(body.address), address)) return null;
|
|
805
|
+
return { interfaces: body.interfaces ?? [], isWallet: body.is_wallet === true };
|
|
806
|
+
};
|
|
807
|
+
const isDexContract = (account) => !account.isWallet && account.interfaces.some((name) => swapInterfaces.has(name));
|
|
808
|
+
const trusted = {
|
|
809
|
+
escrow: new Set((options.escrowCodeHashes ?? DEFAULT_OMNISTON_TON_CODE_HASHES.escrow).map((value) => value.toLowerCase())),
|
|
810
|
+
execution: new Set((options.executionCodeHashes ?? DEFAULT_OMNISTON_TON_CODE_HASHES.execution).map((value) => value.toLowerCase())),
|
|
811
|
+
authorityKeys: new Set((options.authorityKeys ?? DEFAULT_OMNISTON_TON_CODE_HASHES.authorityKeys).map((value) => value.toLowerCase())),
|
|
812
|
+
position: new Set((options.positionCodeHashes ?? DEFAULT_OMNISTON_TON_CODE_HASHES.position).map((value) => value.toLowerCase())),
|
|
813
|
+
outputEscrow: new Set((options.outputEscrowCodeHashes ?? DEFAULT_OMNISTON_TON_CODE_HASHES.outputEscrow).map((value) => value.toLowerCase()))
|
|
814
|
+
};
|
|
815
|
+
const jettonWalletOf = async (ownerAddress, jettonMasterAddress) => {
|
|
816
|
+
const response = await get(`/v2/accounts/${encodeURIComponent(ownerAddress)}/jettons/${encodeURIComponent(jettonMasterAddress)}`);
|
|
817
|
+
if (!response.ok) return null;
|
|
818
|
+
const body = await response.json();
|
|
819
|
+
const wallet = body.wallet_address?.address;
|
|
820
|
+
if (!wallet || body.jetton?.address !== void 0 && !sameAddress(Address.parse(body.jetton.address), jettonMasterAddress)) return null;
|
|
821
|
+
return Address.parse(wallet).toString();
|
|
822
|
+
};
|
|
823
|
+
return {
|
|
824
|
+
async validateTonJettonWallet({ ownerAddress, jettonMasterAddress, walletAddress }) {
|
|
825
|
+
try {
|
|
826
|
+
const url = `${base}/v2/accounts/${encodeURIComponent(ownerAddress)}/jettons/${encodeURIComponent(jettonMasterAddress)}`;
|
|
827
|
+
const response = await request(url, {
|
|
828
|
+
...headers ? { headers } : {},
|
|
829
|
+
signal: AbortSignal.timeout(5e3)
|
|
830
|
+
});
|
|
831
|
+
if (!response.ok) return false;
|
|
832
|
+
const body = await response.json();
|
|
833
|
+
if (!sameAddress(body.wallet_address?.address ? Address.parse(body.wallet_address.address) : null, walletAddress)) return false;
|
|
834
|
+
return body.jetton?.address === void 0 || sameAddress(Address.parse(body.jetton.address), jettonMasterAddress);
|
|
835
|
+
} catch {
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
},
|
|
839
|
+
validateTonEscrowPayload(input) {
|
|
840
|
+
try {
|
|
841
|
+
return validatePayload(input, trusted);
|
|
842
|
+
} catch {
|
|
843
|
+
return false;
|
|
844
|
+
}
|
|
845
|
+
},
|
|
846
|
+
/**
|
|
847
|
+
* EVM→TON: the position the tracking feed named must be (1) an active
|
|
848
|
+
* contract with the pinned position code, (2) carrying state that binds
|
|
849
|
+
* this quote, hashlock, recipient, output jetton, amount, deposit and the
|
|
850
|
+
* pinned timelocks — still inside the completion window — with jetton
|
|
851
|
+
* custody at the factory's own wallet, and (3) deployed by the trusted
|
|
852
|
+
* factory itself (its deploy message is only sent after the factory's
|
|
853
|
+
* jetton wallet notified it of the resolver's transfer). Nothing here
|
|
854
|
+
* trusts the feed beyond the address it suggested.
|
|
855
|
+
*/
|
|
856
|
+
async validateTonOutputPosition(input) {
|
|
857
|
+
const refuse = (reason, details) => {
|
|
858
|
+
try {
|
|
859
|
+
options.onRefusal?.(reason, details);
|
|
860
|
+
} catch {
|
|
861
|
+
}
|
|
862
|
+
return false;
|
|
863
|
+
};
|
|
864
|
+
lastStateRefusal = null;
|
|
865
|
+
try {
|
|
866
|
+
Address.parse(input.positionAddress);
|
|
867
|
+
Address.parse(input.recipientAddress);
|
|
868
|
+
const nowSeconds = Math.floor(now() / 1e3);
|
|
869
|
+
if (!Number.isInteger(input.quoteTimestamp) || input.quoteTimestamp > nowSeconds + 120 || nowSeconds - input.quoteTimestamp > 6 * 60 * 60) return refuse("quote-age");
|
|
870
|
+
const accountResponse = await get(`/v2/blockchain/accounts/${encodeURIComponent(input.positionAddress)}`);
|
|
871
|
+
if (!accountResponse.ok) return refuse("account-fetch", { status: accountResponse.status });
|
|
872
|
+
const account = await accountResponse.json();
|
|
873
|
+
if (!account.address || !sameAddress(Address.parse(account.address), input.positionAddress) || account.status !== "active" || !account.code || !account.data) return refuse("account-state", { status: account.status, hasCode: !!account.code, hasData: !!account.data });
|
|
874
|
+
if (!trusted.position.has(cellFromApi(account.code).hash().toString("hex"))) return refuse("position-code-hash", { hash: cellFromApi(account.code).hash().toString("hex") });
|
|
875
|
+
const jettonWallet = await jettonWalletOf(input.protocolAddress, input.outputJettonMasterAddress);
|
|
876
|
+
if (!jettonWallet) return refuse("jetton-wallet");
|
|
877
|
+
const state = validateOutputPositionState(cellFromApi(account.data), input, { jettonWallet, trusted, nowSeconds });
|
|
878
|
+
if (!state) return refuse("state", { check: lastStateRefusal });
|
|
879
|
+
const txResponse = await get(`/v2/blockchain/accounts/${encodeURIComponent(input.positionAddress)}/transactions?limit=25`);
|
|
880
|
+
if (!txResponse.ok) return refuse("transactions-fetch", { status: txResponse.status });
|
|
881
|
+
const body = await txResponse.json();
|
|
882
|
+
const deployedByFactory = (body.transactions ?? []).some((tx) => tx.success === true && tx.aborted !== true && tx.in_msg?.op_code?.toLowerCase() === POSITION_DEPLOY_OP && !!tx.in_msg.source?.address && sameAddress(Address.parse(tx.in_msg.source.address), input.protocolAddress) && typeof tx.utime === "number" && Math.abs(Number(state.withdrawalAt) - 1 - tx.utime) <= 5);
|
|
883
|
+
return deployedByFactory || refuse("deploy-transaction", { transactions: (body.transactions ?? []).length });
|
|
884
|
+
} catch (error) {
|
|
885
|
+
return refuse("error", { message: error instanceof Error ? error.message : String(error) });
|
|
886
|
+
}
|
|
887
|
+
},
|
|
888
|
+
/**
|
|
889
|
+
* Scan F11: one chain fact a decoded TON swap body depends on.
|
|
890
|
+
* - STON.fi hop: the router carries the STON.fi router interface of its
|
|
891
|
+
* version; for native input the pTON wallet the message pays is a
|
|
892
|
+
* proxy-TON wallet OWNED BY that router; the hop's ask wallet is the
|
|
893
|
+
* router's own wallet of the hop's output (of a proxy-TON master for
|
|
894
|
+
* native output), as the output's master derives it.
|
|
895
|
+
* - DeDust pool: the pool carries the DeDust pool interface of its version
|
|
896
|
+
* and trades exactly the hop's pair.
|
|
897
|
+
*/
|
|
898
|
+
async validateTonSwapLeg(leg) {
|
|
899
|
+
try {
|
|
900
|
+
if (leg.kind === "stonfi-router") {
|
|
901
|
+
let router = leg.router ? Address.parse(leg.router) : null;
|
|
902
|
+
if (leg.proxyTonWallet) {
|
|
903
|
+
const data = await walletData(leg.proxyTonWallet);
|
|
904
|
+
if (!data || !proxyTonMasters.has(data.master.toRawString())) return false;
|
|
905
|
+
if (router && !router.equals(data.owner)) return false;
|
|
906
|
+
router = data.owner;
|
|
907
|
+
}
|
|
908
|
+
if (!router) return false;
|
|
909
|
+
if (!await accountHas(router.toString(), leg.version === 1 ? "stonfi_router" : "stonfi_router_v2")) return false;
|
|
910
|
+
const masters = leg.outputJetton ? [leg.outputJetton] : [...proxyTonMasters];
|
|
911
|
+
for (const master of masters) {
|
|
912
|
+
const wallet = await derivedWallet(master, router.toString());
|
|
913
|
+
if (wallet && wallet.equals(Address.parse(leg.askWallet))) return true;
|
|
914
|
+
}
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
if (leg.version === 1) {
|
|
918
|
+
if (!await accountHas(leg.pool, "dedust_pool")) return false;
|
|
919
|
+
const stack2 = await runGet(leg.pool, "get_assets");
|
|
920
|
+
const assets2 = [stackCell(stack2?.[0]), stackCell(stack2?.[1])].map((cell) => {
|
|
921
|
+
if (!cell) return void 0;
|
|
922
|
+
const slice = cell.beginParse();
|
|
923
|
+
const tag = slice.loadUint(4);
|
|
924
|
+
if (tag === 0) return null;
|
|
925
|
+
if (tag !== 1) return void 0;
|
|
926
|
+
return new Address(slice.loadInt(8), slice.loadBuffer(32)).toString();
|
|
927
|
+
});
|
|
928
|
+
if (assets2.some((asset) => asset === void 0)) return false;
|
|
929
|
+
return samePair(assets2, [leg.inputJetton, leg.outputJetton]);
|
|
930
|
+
}
|
|
931
|
+
if (!await accountHas(leg.pool, "dedust_v2_cpmm")) return false;
|
|
932
|
+
const stack = await runGet(leg.pool, "get_pool_data");
|
|
933
|
+
const assets = [stackCell(stack?.[3]), stackCell(stack?.[4])].map((cell) => {
|
|
934
|
+
if (!cell) return void 0;
|
|
935
|
+
const address = cell.beginParse().loadAddressAny();
|
|
936
|
+
if (address === null) return null;
|
|
937
|
+
return address instanceof Address ? address.toString() : void 0;
|
|
938
|
+
});
|
|
939
|
+
if (assets.some((asset) => asset === void 0)) return false;
|
|
940
|
+
return samePair(assets, [leg.inputJetton, leg.outputJetton]);
|
|
941
|
+
} catch {
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
944
|
+
},
|
|
945
|
+
/** The contract a swap pays into must carry a recognised DEX code
|
|
946
|
+
* interface on the indexer — an EOA, an unknown contract or a fresh
|
|
947
|
+
* resolver-deployed proxy is refused. */
|
|
948
|
+
async validateTonSwapTarget({ address }) {
|
|
949
|
+
try {
|
|
950
|
+
Address.parse(address);
|
|
951
|
+
const account = await readAccount(address);
|
|
952
|
+
if (!account || account.isWallet) return false;
|
|
953
|
+
if (isDexContract(account)) return true;
|
|
954
|
+
if (!account.interfaces.includes("jetton_wallet")) return false;
|
|
955
|
+
const data = await get(`/v2/blockchain/accounts/${encodeURIComponent(address)}/methods/get_wallet_data`);
|
|
956
|
+
if (!data.ok) return false;
|
|
957
|
+
const body = await data.json();
|
|
958
|
+
const owner = body.decoded?.owner;
|
|
959
|
+
const master = body.decoded?.jetton;
|
|
960
|
+
if (!body.success || !owner || !master || !proxyTonMasters.has(Address.parse(master).toRawString())) return false;
|
|
961
|
+
const ownerAccount = await readAccount(owner);
|
|
962
|
+
return ownerAccount !== null && isDexContract(ownerAccount);
|
|
963
|
+
} catch {
|
|
964
|
+
return false;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
function createOmnistonTonDeliveryLocator(options = {}) {
|
|
970
|
+
const request = options.fetch ?? fetch;
|
|
971
|
+
const base = (options.tonApiUrl ?? "https://tonapi.io").replace(/\/$/, "");
|
|
972
|
+
const headers = options.tonApiKey ? { Authorization: `Bearer ${options.tonApiKey}` } : void 0;
|
|
973
|
+
const limit = Math.min(100, Math.max(1, Math.floor(options.limit ?? 20)));
|
|
974
|
+
return async (input) => {
|
|
975
|
+
try {
|
|
976
|
+
const recipient = Address.parse(input.recipientAddress);
|
|
977
|
+
const master = Address.parse(input.jettonMasterAddress);
|
|
978
|
+
const position = input.positionAddress ? Address.parse(input.positionAddress) : null;
|
|
979
|
+
const min = BigInt(input.minUnits);
|
|
980
|
+
if (min <= 0n) return null;
|
|
981
|
+
const since = Number.isFinite(input.afterTimestamp) ? `&start_date=${Math.max(0, Math.floor(input.afterTimestamp))}` : "";
|
|
982
|
+
const until = Number.isFinite(input.beforeTimestamp) ? `&end_date=${Math.max(0, Math.floor(input.beforeTimestamp))}` : "";
|
|
983
|
+
const url = `${base}/v2/accounts/${encodeURIComponent(recipient.toString())}/events?limit=${limit}${since}${until}`;
|
|
984
|
+
const response = await request(url, { ...headers ? { headers } : {}, signal: AbortSignal.timeout(8e3) });
|
|
985
|
+
if (!response.ok) return null;
|
|
986
|
+
const body = await response.json();
|
|
987
|
+
const events = [...body.events ?? []];
|
|
988
|
+
if (input.afterTimestamp !== void 0) events.sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0));
|
|
989
|
+
for (const event of events) {
|
|
990
|
+
if (!event.event_id || event.in_progress === true) continue;
|
|
991
|
+
if (input.afterTimestamp !== void 0 && typeof event.timestamp === "number" && event.timestamp < input.afterTimestamp) continue;
|
|
992
|
+
if (input.beforeTimestamp !== void 0 && typeof event.timestamp === "number" && event.timestamp > input.beforeTimestamp) continue;
|
|
993
|
+
for (const action of event.actions ?? []) {
|
|
994
|
+
if (action.type !== "JettonTransfer" || action.status !== "ok") continue;
|
|
995
|
+
const t = action.JettonTransfer;
|
|
996
|
+
if (!t?.jetton?.address || !t.recipient?.address || t.amount === void 0) continue;
|
|
997
|
+
if (!sameAddress(Address.parse(t.jetton.address), master.toString())) continue;
|
|
998
|
+
if (!sameAddress(Address.parse(t.recipient.address), recipient.toString())) continue;
|
|
999
|
+
if (position && (!t.sender?.address || !sameAddress(Address.parse(t.sender.address), position.toString()))) continue;
|
|
1000
|
+
let amount;
|
|
1001
|
+
try {
|
|
1002
|
+
amount = BigInt(t.amount);
|
|
1003
|
+
} catch {
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
if (amount < min) continue;
|
|
1007
|
+
return event.event_id.toLowerCase();
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return null;
|
|
1011
|
+
} catch {
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// src/validators/settlement.ts
|
|
1018
|
+
var AUDITED_OMNISTON_SETTLEMENT_CONTRACTS = {
|
|
1019
|
+
"ton:-239": ["EQAbWJ3Y1HgIIvcMq1prG1anlDC0T3cZlAU7luPT6LmTpmrZ"],
|
|
1020
|
+
"eip155:1": ["0xafCF681272AF10959736f45616eeCBC22D7095E2"],
|
|
1021
|
+
"eip155:56": ["0xafCF681272AF10959736f45616eeCBC22D7095E2"],
|
|
1022
|
+
"eip155:137": ["0x41A392062A4307a6961d48e30FF20bF6823715B8"],
|
|
1023
|
+
"eip155:4663": ["0xafCF681272AF10959736f45616eeCBC22D7095E2"],
|
|
1024
|
+
"eip155:8453": ["0xACa3E2F9b61a44892926bC6c3e104192E7D1c3cd"],
|
|
1025
|
+
"eip155:42161": ["0xafCF681272AF10959736f45616eeCBC22D7095E2"],
|
|
1026
|
+
"eip155:43114": ["0xafCF681272AF10959736f45616eeCBC22D7095E2"],
|
|
1027
|
+
// 2026-10-20: one factory address on X Layer, HyperEVM and Plasma. Runtime
|
|
1028
|
+
// bytecode equals Ethereum's 0xafCF… byte for byte except the immutable slots
|
|
1029
|
+
// (escrow implementations, its order protocol, domain hash); it references
|
|
1030
|
+
// the order protocol trusted below on the same chain.
|
|
1031
|
+
"eip155:196": ["0xd602079AFa5c38F550510a2a988CbFa698Fa0189"],
|
|
1032
|
+
"eip155:999": ["0xd602079AFa5c38F550510a2a988CbFa698Fa0189"],
|
|
1033
|
+
"eip155:9745": ["0xd602079AFa5c38F550510a2a988CbFa698Fa0189"]
|
|
1034
|
+
};
|
|
1035
|
+
var AUDITED_OMNISTON_EVM_ORDER_PROTOCOLS = {
|
|
1036
|
+
"eip155:1": ["0xb5810aCfD45D85Eaa938bF90a5857295513DA7E9"],
|
|
1037
|
+
"eip155:56": ["0xb5810aCfD45D85Eaa938bF90a5857295513DA7E9"],
|
|
1038
|
+
"eip155:137": ["0xafCF681272AF10959736f45616eeCBC22D7095E2"],
|
|
1039
|
+
"eip155:8453": ["0xe7405fAcDEDB2512e91F2886BDff13231343aF5E"],
|
|
1040
|
+
"eip155:42161": ["0xb5810aCfD45D85Eaa938bF90a5857295513DA7E9"],
|
|
1041
|
+
"eip155:43114": ["0xb5810aCfD45D85Eaa938bF90a5857295513DA7E9"],
|
|
1042
|
+
// 2026-10-20: bytecode equals Ethereum's 0xb581… except immutables (wrapped
|
|
1043
|
+
// native, own address, chain id 196 / 9745 / 999 at the chain-id slot).
|
|
1044
|
+
"eip155:196": ["0xb5810aCfD45D85Eaa938bF90a5857295513DA7E9"],
|
|
1045
|
+
"eip155:9745": ["0xb5810aCfD45D85Eaa938bF90a5857295513DA7E9"],
|
|
1046
|
+
"eip155:999": ["0xD84778bbeEC5D2a8d17c6424249C438EC0b1ABE3"]
|
|
1047
|
+
};
|
|
1048
|
+
export {
|
|
1049
|
+
AUDITED_OMNISTON_EVM_ORDER_PROTOCOLS,
|
|
1050
|
+
AUDITED_OMNISTON_SETTLEMENT_CONTRACTS,
|
|
1051
|
+
DEFAULT_OMNISTON_TON_CODE_HASHES,
|
|
1052
|
+
DEFAULT_OMNISTON_TON_PROXY_TON_MASTERS,
|
|
1053
|
+
DEFAULT_OMNISTON_TON_SWAP_INTERFACES,
|
|
1054
|
+
MAX_OMNISTON_EXCLUSIVITY_SECONDS,
|
|
1055
|
+
OMNISTON_EVM_LOOKBACK_BLOCKS,
|
|
1056
|
+
OMNISTON_EVM_MIN_CONFIRMATIONS,
|
|
1057
|
+
SRC_ESCROW_CREATED_TOPIC,
|
|
1058
|
+
createOmnistonEvmDeliveryLocator,
|
|
1059
|
+
createOmnistonEvmInputValidator,
|
|
1060
|
+
createOmnistonEvmOutputValidator,
|
|
1061
|
+
createOmnistonTonDeliveryLocator,
|
|
1062
|
+
createOmnistonTonValidators,
|
|
1063
|
+
omnistonEvmRpcRequest
|
|
1064
|
+
};
|
|
1065
|
+
//# sourceMappingURL=validators.js.map
|