nansen-cli 1.41.0 → 1.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +33 -7
- package/package.json +1 -1
- package/scripts/postinstall.js +3 -3
- package/skills/nansen-wallet-manager/SKILL.md +5 -5
- package/src/api.js +11 -5
- package/src/bridge.js +791 -55
- package/src/cli.js +68 -41
- package/src/commands/agent.js +4 -0
- package/src/commands/mcp.js +373 -0
- package/src/doctor.js +16 -7
- package/src/limit-order.js +16 -2
- package/src/mcp-verify.js +292 -0
- package/src/privy.js +12 -2
- package/src/schema.json +55 -0
- package/src/swap-simulation.js +6 -0
- package/src/trade-validation.js +185 -37
- package/src/trading.js +208 -31
- package/src/wallet.js +20 -14
- package/src/x402-svm.js +9 -5
package/src/trade-validation.js
CHANGED
|
@@ -8,6 +8,7 @@ import { validateAddress } from './api.js';
|
|
|
8
8
|
import { CHAIN_RPCS } from './rpc-urls.js';
|
|
9
9
|
import { parseTransactionMessage, resolveStaticAccount } from './solana-tx.js';
|
|
10
10
|
import { SOL_SENTINEL } from './solana-simulation.js';
|
|
11
|
+
import { EVM_NATIVE_SENTINEL } from './swap-simulation.js';
|
|
11
12
|
|
|
12
13
|
const SUPPORTED_CHAINS = ['solana', 'base'];
|
|
13
14
|
|
|
@@ -146,6 +147,21 @@ const NATIVE_TOKEN_ADDRESSES = {
|
|
|
146
147
|
base: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
|
|
147
148
|
};
|
|
148
149
|
|
|
150
|
+
// Upper bound on native (ETH) that may leave the wallet as a *sibling* of a
|
|
151
|
+
// cross-chain bridge — some bridges pay a network fee via msg.value on a
|
|
152
|
+
// token-input route, which surfaces as a native outflow distinct from the input
|
|
153
|
+
// token. assertSwapOutcome's no-sibling-drain check (assertion 3) is otherwise
|
|
154
|
+
// strict-zero, so without a bound such a fee would false-block a legitimate
|
|
155
|
+
// bridge. The tolerance is the smaller of the transaction's declared native
|
|
156
|
+
// value and THIS cap (see verifySwapOutcome); capping it — rather than trusting
|
|
157
|
+
// the quote's value outright — is what keeps the check from being turned into a
|
|
158
|
+
// native-ETH drain (a hostile quote could otherwise set value to the whole
|
|
159
|
+
// balance). Conservative and gas-independent (gas is already excluded from the
|
|
160
|
+
// simulated native delta); a real bridge fee is far below it. This is a
|
|
161
|
+
// defence-in-depth loss ceiling, not a fee estimate — revisit if a route ever
|
|
162
|
+
// legitimately needs more.
|
|
163
|
+
export const EVM_BRIDGE_NATIVE_FEE_SLACK = 2_000_000_000_000_000n; // 0.002 ETH
|
|
164
|
+
|
|
149
165
|
// Native SOL has two on-chain spellings that denote the same asset: the
|
|
150
166
|
// canonical wrapped-SOL mint (what the CLI resolves `SOL` to and persists as
|
|
151
167
|
// the request intent) and the System Program address that aggregators and
|
|
@@ -912,6 +928,18 @@ export function assertSwapCalldataNotBareTransfer(data) {
|
|
|
912
928
|
|
|
913
929
|
// ============= Swap-outcome verification (balance-delta simulation) =============
|
|
914
930
|
|
|
931
|
+
/**
|
|
932
|
+
* A cross-chain bridge's output settles on the destination chain, invisible to
|
|
933
|
+
* a source-chain simulation, so the output-arrival assertion is meaningless
|
|
934
|
+
* for one and both assert...SwapOutcome functions skip it via this check.
|
|
935
|
+
* Derived from the immutable persisted request intent (not the loose
|
|
936
|
+
* quote/quoteData) so it can't drift between calls or across chains.
|
|
937
|
+
*/
|
|
938
|
+
export function isBridgeRequest(request) {
|
|
939
|
+
return request.toChain != null
|
|
940
|
+
&& String(request.toChain).toLowerCase() !== String(request.chain).toLowerCase();
|
|
941
|
+
}
|
|
942
|
+
|
|
915
943
|
/**
|
|
916
944
|
* Assert that a SIMULATED swap's asset changes match the user's intent, failing
|
|
917
945
|
* closed on any mismatch. This is a defence-in-depth outcome check that
|
|
@@ -929,6 +957,8 @@ export function assertSwapCalldataNotBareTransfer(data) {
|
|
|
929
957
|
* log) is never counted.
|
|
930
958
|
* 2. the output token arrives by AT LEAST minOut — exactOut: >= the requested
|
|
931
959
|
* output; exactIn: the quoted output reduced by the slippage in effect.
|
|
960
|
+
* SKIPPED for a cross-chain bridge: the output settles on the destination
|
|
961
|
+
* chain and can never appear in a source-chain simulation.
|
|
932
962
|
* 3. NO token other than the input leaves the wallet.
|
|
933
963
|
* 4. the wallet grants no Approval to a spender outside `expectedSpenders`.
|
|
934
964
|
*
|
|
@@ -942,8 +972,14 @@ export function assertSwapCalldataNotBareTransfer(data) {
|
|
|
942
972
|
* @param {Set<string>|string[]} [ctx.expectedSpenders] - spenders the wallet may
|
|
943
973
|
* legitimately (re)approve during the swap (e.g. the approval target and the
|
|
944
974
|
* router); anything else fails assertion 4. Compared case-insensitively.
|
|
945
|
-
* @param {bigint} [ctx.siblingDustThreshold=0n] -
|
|
946
|
-
* before assertion 3 fires
|
|
975
|
+
* @param {bigint} [ctx.siblingDustThreshold=0n] - native-token (ETH) sibling
|
|
976
|
+
* outflow tolerated before assertion 3 fires, and only for a cross-chain
|
|
977
|
+
* bridge (some bridges pay a fee via msg.value on a token-input route). ERC-20
|
|
978
|
+
* siblings and every same-chain swap stay strict 0. The caller must pass a
|
|
979
|
+
* bounded value (see EVM_BRIDGE_NATIVE_FEE_SLACK). Strict 0 default.
|
|
980
|
+
* @returns {{verified: true, outputAssertionSkipped: boolean}} outputAssertionSkipped
|
|
981
|
+
* is true for a cross-chain bridge, meaning assertion 2 did not run — the
|
|
982
|
+
* caller should surface this.
|
|
947
983
|
* @throws {Error} with `code = 'SWAP_OUTCOME_MISMATCH'` on any failed assertion.
|
|
948
984
|
*/
|
|
949
985
|
export function assertSwapOutcome(request, quote, sim, { slippage, expectedSpenders, siblingDustThreshold = 0n } = {}) {
|
|
@@ -983,6 +1019,9 @@ export function assertSwapOutcome(request, quote, sim, { slippage, expectedSpend
|
|
|
983
1019
|
throw fail(`quote input and output tokens are the same (${inputToken}); refusing to verify.`);
|
|
984
1020
|
}
|
|
985
1021
|
|
|
1022
|
+
// Bridges skip only assertion 2 (output arrival) below — see isBridgeRequest.
|
|
1023
|
+
const isBridge = isBridgeRequest(request);
|
|
1024
|
+
|
|
986
1025
|
// --- Assertion 1: input outflow within the spend ceiling ---
|
|
987
1026
|
// This bounds the outflow by maxInputAmount (the slippage-buffered ceiling),
|
|
988
1027
|
// NOT the exact expected input: for exactOut the aggregator may legitimately
|
|
@@ -1005,9 +1044,50 @@ export function assertSwapOutcome(request, quote, sim, { slippage, expectedSpend
|
|
|
1005
1044
|
throw fail(`the input token (${inputToken}) left the wallet by ${outflow}, exceeding your maximum input (${cap}).`);
|
|
1006
1045
|
}
|
|
1007
1046
|
|
|
1008
|
-
//
|
|
1047
|
+
// Fail closed on an unrecognized swap mode. `?? 'exactIn'` only defaults a
|
|
1048
|
+
// missing mode; a persisted request with a garbage value (an edited/older
|
|
1049
|
+
// quote record) must not silently fall into the exactOut branch below, which
|
|
1050
|
+
// would drop the intent-relative input floor. The CLI validates --swap-mode
|
|
1051
|
+
// against this same enum, so a well-formed quote never reaches here invalid.
|
|
1009
1052
|
const swapMode = request.swapMode ?? 'exactIn';
|
|
1010
|
-
|
|
1053
|
+
if (swapMode !== 'exactIn' && swapMode !== 'exactOut') {
|
|
1054
|
+
throw fail(`unrecognized swap mode "${swapMode}"; expected exactIn or exactOut.`);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// A bridge drops assertion 2 (its output settles on the destination chain),
|
|
1058
|
+
// and for a normal swap that positive-output check is what implicitly proves
|
|
1059
|
+
// the input was actually consumed. Restore that with an intent-relative LOWER
|
|
1060
|
+
// bound on the source-chain outflow: a bare `outflow > 0` is too weak — a
|
|
1061
|
+
// fee-only or 1-unit no-op would still verify a bridge that never funded its
|
|
1062
|
+
// input. For exactIn the outflow must be ~the requested input (assertion 1
|
|
1063
|
+
// already caps it above); for exactOut the input is variable up to the cap, so
|
|
1064
|
+
// only a positive-outflow floor is meaningful. EVM native input delta is the
|
|
1065
|
+
// transferred value with gas excluded, so the outflow is exact — no fee slack.
|
|
1066
|
+
if (isBridge) {
|
|
1067
|
+
if (swapMode === 'exactIn') {
|
|
1068
|
+
if (request.amount == null) throw fail('bridge exactIn request is missing the requested input amount.');
|
|
1069
|
+
let requested;
|
|
1070
|
+
try {
|
|
1071
|
+
requested = BigInt(request.amount);
|
|
1072
|
+
} catch {
|
|
1073
|
+
throw fail(`requested input amount (${request.amount}) is not an integer.`);
|
|
1074
|
+
}
|
|
1075
|
+
if (requested <= 0n) throw fail(`bridge exactIn request has a non-positive input amount (${requested}).`);
|
|
1076
|
+
if (outflow < requested) {
|
|
1077
|
+
throw fail(`the bridge moved only ${outflow} of the input token (${inputToken}) out of the wallet, below the requested input (${requested}); a bridge must spend its full input on the source chain.`);
|
|
1078
|
+
}
|
|
1079
|
+
} else if (outflow <= 0n) {
|
|
1080
|
+
throw fail(`the bridge moved no input token (${inputToken}) out of the wallet; a bridge must spend its input on the source chain.`);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// --- Assertion 2: output arrives at or above the minimum acceptable ---
|
|
1085
|
+
// The minimum-output computation below — and its quote-integrity checks (the
|
|
1086
|
+
// output amount must be present, an integer, and positive) — runs for EVERY
|
|
1087
|
+
// swap, bridges included: a bridge quote with a missing or zero output is
|
|
1088
|
+
// still malformed. Only the final delta comparison is bridge-skipped, because
|
|
1089
|
+
// the output settles on the destination chain and can never appear in this
|
|
1090
|
+
// source-chain simulation.
|
|
1011
1091
|
let minOut;
|
|
1012
1092
|
if (swapMode === 'exactOut') {
|
|
1013
1093
|
if (request.amount == null) throw fail('exactOut request is missing the requested output amount.');
|
|
@@ -1058,15 +1138,26 @@ export function assertSwapOutcome(request, quote, sim, { slippage, expectedSpend
|
|
|
1058
1138
|
const bps = BigInt(Math.min(10000, Math.round(slip * 10000)));
|
|
1059
1139
|
minOut = (quoted * (10000n - bps)) / 10000n;
|
|
1060
1140
|
}
|
|
1061
|
-
if (
|
|
1062
|
-
|
|
1141
|
+
if (!isBridge) {
|
|
1142
|
+
const outputDelta = deltas[outputToken] || 0n;
|
|
1143
|
+
if (outputDelta < minOut) {
|
|
1144
|
+
throw fail(`the output token (${outputToken}) increased by only ${outputDelta}, below the minimum acceptable output (${minOut}).`);
|
|
1145
|
+
}
|
|
1063
1146
|
}
|
|
1064
1147
|
|
|
1065
1148
|
// --- Assertion 3: no token other than the input leaves the wallet ---
|
|
1066
|
-
|
|
1149
|
+
// A bridge may pay a network fee in native ETH via msg.value on a
|
|
1150
|
+
// token-input route, which shows up here as a native sibling outflow. Tolerate
|
|
1151
|
+
// that — but only native, only for a bridge, and only up to the bounded
|
|
1152
|
+
// threshold the caller passes (gas is already excluded from the native delta).
|
|
1153
|
+
// ERC-20 siblings and every same-chain swap keep strict-zero. This mirrors the
|
|
1154
|
+
// native-only carve-out assertSolanaSwapOutcome already applies for SOL.
|
|
1155
|
+
const nativeDust = isBridge && siblingDustThreshold > 0n ? siblingDustThreshold : 0n;
|
|
1067
1156
|
for (const [token, delta] of Object.entries(deltas)) {
|
|
1068
1157
|
if (token === inputToken) continue; // its outflow is bounded by assertion 1
|
|
1069
|
-
if (delta
|
|
1158
|
+
if (delta >= 0n) continue;
|
|
1159
|
+
const dust = token === EVM_NATIVE_SENTINEL ? nativeDust : 0n;
|
|
1160
|
+
if (-delta > dust) {
|
|
1070
1161
|
throw fail(`a token other than the one you are selling (${token}) left the wallet (delta ${delta}); a swap must not move any token except the input.`);
|
|
1071
1162
|
}
|
|
1072
1163
|
}
|
|
@@ -1117,7 +1208,7 @@ export function assertSwapOutcome(request, quote, sim, { slippage, expectedSpend
|
|
|
1117
1208
|
}
|
|
1118
1209
|
}
|
|
1119
1210
|
|
|
1120
|
-
return { verified: true };
|
|
1211
|
+
return { verified: true, outputAssertionSkipped: isBridge };
|
|
1121
1212
|
}
|
|
1122
1213
|
|
|
1123
1214
|
// Native-SOL dust tolerated on a non-input sibling in assertSolanaSwapOutcome —
|
|
@@ -1170,6 +1261,8 @@ const NATIVE_FEE_RENT_SLACK_LAMPORTS = MAX_PRIORITY_FEE_LAMPORTS + NATIVE_SIBLIN
|
|
|
1170
1261
|
* Native-SOL output relaxes this floor by NATIVE_FEE_RENT_SLACK_LAMPORTS
|
|
1171
1262
|
* because its lamport delta also nets out the base fee, priority fee, and
|
|
1172
1263
|
* ATA rent (same noise as native input); SPL output keeps the exact floor.
|
|
1264
|
+
* SKIPPED for a cross-chain bridge: the output settles on the destination
|
|
1265
|
+
* chain and can never appear in a source-chain simulation.
|
|
1173
1266
|
* 3. no OTHER tracked asset leaves the wallet. SPL-token siblings get zero
|
|
1174
1267
|
* tolerance; native SOL, when it's a sibling (not the input), tolerates
|
|
1175
1268
|
* NATIVE_FEE_RENT_SLACK_LAMPORTS of fee/rent dust. All three assertions
|
|
@@ -1186,10 +1279,12 @@ const NATIVE_FEE_RENT_SLACK_LAMPORTS = MAX_PRIORITY_FEE_LAMPORTS + NATIVE_SIBLIN
|
|
|
1186
1279
|
* @param {object} [ctx]
|
|
1187
1280
|
* @param {number} [ctx.slippage] - slippage fraction in effect; defaults to 3%
|
|
1188
1281
|
* @param {bigint} [ctx.siblingDustThreshold] - overrides NATIVE_FEE_RENT_SLACK_LAMPORTS
|
|
1189
|
-
* @returns {{verified: true, inputAssertionSkipped: boolean}}
|
|
1190
|
-
* is true when the input was native SOL, meaning
|
|
1191
|
-
* fee/rent slack applied instead of an exact bound
|
|
1192
|
-
* rationale above)
|
|
1282
|
+
* @returns {{verified: true, inputAssertionSkipped: boolean, outputAssertionSkipped: boolean}}
|
|
1283
|
+
* inputAssertionSkipped is true when the input was native SOL, meaning
|
|
1284
|
+
* assertion 1 ran with the fee/rent slack applied instead of an exact bound
|
|
1285
|
+
* (see assertion 1's rationale above). outputAssertionSkipped is true for a
|
|
1286
|
+
* cross-chain bridge, meaning assertion 2 did not run. The caller should
|
|
1287
|
+
* surface both.
|
|
1193
1288
|
* @throws {Error} with `code = 'SWAP_OUTCOME_MISMATCH'` on any failed assertion.
|
|
1194
1289
|
*/
|
|
1195
1290
|
export function assertSolanaSwapOutcome(request, quote, sim, { slippage, siblingDustThreshold } = {}) {
|
|
@@ -1227,6 +1322,9 @@ export function assertSolanaSwapOutcome(request, quote, sim, { slippage, sibling
|
|
|
1227
1322
|
|
|
1228
1323
|
const inputIsNative = inputAsset === SOL_SENTINEL;
|
|
1229
1324
|
|
|
1325
|
+
// Bridges skip only assertion 2 (output arrival) below — see isBridgeRequest.
|
|
1326
|
+
const isBridge = isBridgeRequest(request);
|
|
1327
|
+
|
|
1230
1328
|
// --- Assertion 1: input outflow within the spend ceiling ---
|
|
1231
1329
|
if (request.maxInputAmount == null) {
|
|
1232
1330
|
throw fail('request has no maximum input to bound the outflow against.');
|
|
@@ -1252,11 +1350,55 @@ export function assertSolanaSwapOutcome(request, quote, sim, { slippage, sibling
|
|
|
1252
1350
|
if (outflow > effectiveCap) {
|
|
1253
1351
|
throw fail(`the input token (${inputAsset}) left the wallet by ${outflow}, exceeding your maximum input (${cap}${inputIsNative ? ` plus fee/rent slack` : ''}).`);
|
|
1254
1352
|
}
|
|
1353
|
+
// Fail closed on an unrecognized swap mode (mirrors assertSwapOutcome). A
|
|
1354
|
+
// persisted request with a garbage value must not fall into the exactOut
|
|
1355
|
+
// branch below and drop the intent-relative input floor.
|
|
1356
|
+
const swapMode = request.swapMode ?? 'exactIn';
|
|
1357
|
+
if (swapMode !== 'exactIn' && swapMode !== 'exactOut') {
|
|
1358
|
+
throw fail(`unrecognized swap mode "${swapMode}"; expected exactIn or exactOut.`);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// A bridge drops assertion 2 (its output settles on the destination chain),
|
|
1362
|
+
// and for a normal swap that positive-output check is what implicitly proves
|
|
1363
|
+
// the input was actually consumed. Restore that with an intent-relative LOWER
|
|
1364
|
+
// bound on the source-chain outflow: a bare `outflow > 0` is too weak — a
|
|
1365
|
+
// native-SOL leg always burns a fee, so a fee-only no-op (and any partial SPL
|
|
1366
|
+
// outflow) would otherwise verify a bridge that never funded its input. For
|
|
1367
|
+
// exactIn the outflow must be ~the requested input (assertion 1 caps it
|
|
1368
|
+
// above); for exactOut the input is variable up to the cap, so only a
|
|
1369
|
+
// positive-outflow floor is meaningful. Native-SOL input carries fee/rent
|
|
1370
|
+
// noise (bridged amount + base/priority fee − reclaimed ATA rent), so relax
|
|
1371
|
+
// the floor by the same slack assertion 1 adds to the ceiling; SPL is exact.
|
|
1372
|
+
if (isBridge) {
|
|
1373
|
+
if (swapMode === 'exactIn') {
|
|
1374
|
+
if (request.amount == null) throw fail('bridge exactIn request is missing the requested input amount.');
|
|
1375
|
+
let requested;
|
|
1376
|
+
try {
|
|
1377
|
+
requested = BigInt(request.amount);
|
|
1378
|
+
} catch {
|
|
1379
|
+
throw fail(`requested input amount (${request.amount}) is not an integer.`);
|
|
1380
|
+
}
|
|
1381
|
+
if (requested <= 0n) throw fail(`bridge exactIn request has a non-positive input amount (${requested}).`);
|
|
1382
|
+
const floorSlack = inputIsNative ? NATIVE_FEE_RENT_SLACK_LAMPORTS : 0n;
|
|
1383
|
+
// Clamp the floor to a positive minimum: for a native bridge smaller than
|
|
1384
|
+
// the fee/rent slack a real leg is indistinguishable from a fee-only no-op,
|
|
1385
|
+
// so the tightest we can still require is a non-zero outflow.
|
|
1386
|
+
const minOutflow = requested > floorSlack ? requested - floorSlack : 1n;
|
|
1387
|
+
if (outflow < minOutflow) {
|
|
1388
|
+
throw fail(`the bridge moved only ${outflow} of the input token (${inputAsset}) out of the wallet, below the requested input (${requested}${inputIsNative ? ` minus fee/rent slack` : ''}); a bridge must spend its full input on the source chain.`);
|
|
1389
|
+
}
|
|
1390
|
+
} else if (outflow <= 0n) {
|
|
1391
|
+
throw fail(`the bridge moved no input token (${inputAsset}) out of the wallet; a bridge must spend its input on the source chain.`);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1255
1394
|
|
|
1256
1395
|
// --- Assertion 2: output arrives at or above the minimum acceptable ---
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1396
|
+
// The minimum-output computation below — and its quote-integrity checks (the
|
|
1397
|
+
// output amount must be present, an integer, and positive) — runs for EVERY
|
|
1398
|
+
// swap, bridges included: a bridge quote with a missing or zero output is
|
|
1399
|
+
// still malformed. Only the final delta comparison is bridge-skipped, because
|
|
1400
|
+
// the output settles on the destination chain and can never appear in this
|
|
1401
|
+
// source-chain simulation.
|
|
1260
1402
|
let minOut;
|
|
1261
1403
|
if (swapMode === 'exactOut') {
|
|
1262
1404
|
if (request.amount == null) throw fail('exactOut request is missing the requested output amount.');
|
|
@@ -1290,26 +1432,30 @@ export function assertSolanaSwapOutcome(request, quote, sim, { slippage, sibling
|
|
|
1290
1432
|
const bps = BigInt(Math.min(10000, Math.round(slip * 10000)));
|
|
1291
1433
|
minOut = (quoted * (10000n - bps)) / 10000n;
|
|
1292
1434
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1435
|
+
if (!isBridge) {
|
|
1436
|
+
const outputIsNative = outputAsset === SOL_SENTINEL;
|
|
1437
|
+
const outputDelta = deltas[outputAsset] || 0n;
|
|
1438
|
+
// Native-SOL output carries the same fee/rent noise as native input: the
|
|
1439
|
+
// lamport delta is (SOL received − base/priority fee − net ATA rent), so a
|
|
1440
|
+
// legitimate trade can land a few million lamports under the quoted amount at
|
|
1441
|
+
// tight slippage or on a congested-network priority fee. Relax the floor by
|
|
1442
|
+
// the same combined fee/rent slack used for native siblings (assertion 3) and
|
|
1443
|
+
// native input (assertion 1) so fee noise never false-blocks; the slippage
|
|
1444
|
+
// floor still bounds any real shortfall. SPL output has no such noise and
|
|
1445
|
+
// keeps the exact floor.
|
|
1446
|
+
const outputFloorSlack = outputIsNative
|
|
1447
|
+
? (siblingDustThreshold != null ? siblingDustThreshold : NATIVE_FEE_RENT_SLACK_LAMPORTS)
|
|
1448
|
+
: 0n;
|
|
1449
|
+
// minOut can be smaller than the dust tolerance for a dust-quoted swap; clamp
|
|
1450
|
+
// the floor at 0 so the subtraction never goes negative and silently admits
|
|
1451
|
+
// any non-negative outputDelta (including zero). The explicit outputDelta <= 0n
|
|
1452
|
+
// check below then restores the invariant assertSwapOutcome (the EVM sibling)
|
|
1453
|
+
// gets for free because its minOut can never collapse to <= 0: a swap must
|
|
1454
|
+
// deliver SOME positive output, even when the dust-adjusted floor is 0.
|
|
1455
|
+
const adjustedFloor = minOut > outputFloorSlack ? minOut - outputFloorSlack : 0n;
|
|
1456
|
+
if (outputDelta <= 0n || outputDelta < adjustedFloor) {
|
|
1457
|
+
throw fail(`the output token (${outputAsset}) increased by only ${outputDelta}, below the minimum acceptable output (${minOut}).`);
|
|
1458
|
+
}
|
|
1313
1459
|
}
|
|
1314
1460
|
|
|
1315
1461
|
// --- Assertion 3: no other tracked asset leaves the wallet ---
|
|
@@ -1326,7 +1472,9 @@ export function assertSolanaSwapOutcome(request, quote, sim, { slippage, sibling
|
|
|
1326
1472
|
// inputAssertionSkipped tells the caller assertion 1 ran with the fee/rent
|
|
1327
1473
|
// slack applied (native-SOL input, per the JSDoc above), so it can surface
|
|
1328
1474
|
// that instead of implying the input spend was tightly delta-verified.
|
|
1329
|
-
|
|
1475
|
+
// outputAssertionSkipped tells the caller assertion 2 did not run at all
|
|
1476
|
+
// (cross-chain bridge, per the JSDoc above).
|
|
1477
|
+
return { verified: true, inputAssertionSkipped: inputIsNative, outputAssertionSkipped: isBridge };
|
|
1330
1478
|
}
|
|
1331
1479
|
|
|
1332
1480
|
/**
|