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