@debridge-finance/borderless-validation-sdk 0.0.5 → 0.0.7
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/dist/cjs/helpers/evm-dln-hook-pack.js +8 -6
- package/dist/cjs/helpers/evm-dln-hook-pack.js.map +1 -1
- package/dist/cjs/helpers/evm-gas-compensation-tx.js +2 -1
- package/dist/cjs/helpers/evm-gas-compensation-tx.js.map +1 -1
- package/dist/cjs/helpers/evm-intent-id.js +6 -5
- package/dist/cjs/helpers/evm-intent-id.js.map +1 -1
- package/dist/cjs/helpers/evm-trade-amounts.js +25 -43
- package/dist/cjs/helpers/evm-trade-amounts.js.map +1 -1
- package/dist/cjs/helpers/expected-delegate.js +10 -11
- package/dist/cjs/helpers/expected-delegate.js.map +1 -1
- package/dist/cjs/helpers/gas-compensation.js +3 -0
- package/dist/cjs/helpers/gas-compensation.js.map +1 -1
- package/dist/cjs/helpers/hex.js +35 -0
- package/dist/cjs/helpers/hex.js.map +1 -1
- package/dist/cjs/helpers/integer.js +10 -0
- package/dist/cjs/helpers/integer.js.map +1 -0
- package/dist/cjs/helpers/placeholder.js +3 -2
- package/dist/cjs/helpers/placeholder.js.map +1 -1
- package/dist/cjs/helpers/solana-intent-id.js +6 -5
- package/dist/cjs/helpers/solana-intent-id.js.map +1 -1
- package/dist/cjs/helpers/synthetic-native-wrap.js +56 -0
- package/dist/cjs/helpers/synthetic-native-wrap.js.map +1 -0
- package/dist/cjs/types/enums.js +2 -0
- package/dist/cjs/types/enums.js.map +1 -1
- package/dist/cjs/types/request.js +27 -5
- package/dist/cjs/types/request.js.map +1 -1
- package/dist/cjs/types/response.js +11 -3
- package/dist/cjs/types/response.js.map +1 -1
- package/dist/cjs/types/validation.js +40 -1
- package/dist/cjs/types/validation.js.map +1 -1
- package/dist/cjs/validations/accumulative-token.js +272 -0
- package/dist/cjs/validations/accumulative-token.js.map +1 -0
- package/dist/cjs/validations/compensation-hook.js +9 -0
- package/dist/cjs/validations/compensation-hook.js.map +1 -1
- package/dist/cjs/validations/cost-token.js +56 -0
- package/dist/cjs/validations/cost-token.js.map +1 -0
- package/dist/cjs/validations/dln-metadata.js +63 -0
- package/dist/cjs/validations/dln-metadata.js.map +1 -0
- package/dist/cjs/validations/evm-caveats.js +5 -3
- package/dist/cjs/validations/evm-caveats.js.map +1 -1
- package/dist/cjs/validations/evm-op-budget.js +4 -4
- package/dist/cjs/validations/evm-op-budget.js.map +1 -1
- package/dist/cjs/validations/evm-op-hook.js +7 -1
- package/dist/cjs/validations/evm-op-hook.js.map +1 -1
- package/dist/cjs/validations/evm-op-wrap.js +6 -5
- package/dist/cjs/validations/evm-op-wrap.js.map +1 -1
- package/dist/cjs/validations/evm-permit2.js +7 -4
- package/dist/cjs/validations/evm-permit2.js.map +1 -1
- package/dist/cjs/validations/evm-required-actions.js +7 -4
- package/dist/cjs/validations/evm-required-actions.js.map +1 -1
- package/dist/cjs/validations/external-call.js +330 -0
- package/dist/cjs/validations/external-call.js.map +1 -0
- package/dist/cjs/validations/hooks-common.js +38 -2
- package/dist/cjs/validations/hooks-common.js.map +1 -1
- package/dist/cjs/validations/intent-tokens.js +54 -40
- package/dist/cjs/validations/intent-tokens.js.map +1 -1
- package/dist/cjs/validations/intent.js +192 -71
- package/dist/cjs/validations/intent.js.map +1 -1
- package/dist/cjs/validations/ptp.js +157 -0
- package/dist/cjs/validations/ptp.js.map +1 -0
- package/dist/cjs/validations/required-actions.js +2 -2
- package/dist/cjs/validations/required-actions.js.map +1 -1
- package/dist/cjs/validations/solana-hook.js +12 -0
- package/dist/cjs/validations/solana-hook.js.map +1 -1
- package/dist/cjs/validations/solana-op-budget.js +20 -40
- package/dist/cjs/validations/solana-op-budget.js.map +1 -1
- package/dist/cjs/validations/solana-op-wrap.js +27 -32
- package/dist/cjs/validations/solana-op-wrap.js.map +1 -1
- package/dist/cjs/validations/total-dst-amount.js +432 -0
- package/dist/cjs/validations/total-dst-amount.js.map +1 -0
- package/dist/cjs/validations/trade-grouping.js +67 -25
- package/dist/cjs/validations/trade-grouping.js.map +1 -1
- package/dist/cjs/validations/trade-matching.js +112 -0
- package/dist/cjs/validations/trade-matching.js.map +1 -0
- package/dist/cjs/validations/trades.js +365 -71
- package/dist/cjs/validations/trades.js.map +1 -1
- package/dist/cjs/validator.js +22 -4
- package/dist/cjs/validator.js.map +1 -1
- package/dist/cjs/whitelists/environment-config.js +5 -5
- package/dist/cjs/whitelists/environment-config.js.map +1 -1
- package/dist/esm/helpers/evm-dln-hook-pack.js +8 -6
- package/dist/esm/helpers/evm-dln-hook-pack.js.map +1 -1
- package/dist/esm/helpers/evm-gas-compensation-tx.js +2 -1
- package/dist/esm/helpers/evm-gas-compensation-tx.js.map +1 -1
- package/dist/esm/helpers/evm-intent-id.js +6 -5
- package/dist/esm/helpers/evm-intent-id.js.map +1 -1
- package/dist/esm/helpers/evm-trade-amounts.js +26 -44
- package/dist/esm/helpers/evm-trade-amounts.js.map +1 -1
- package/dist/esm/helpers/expected-delegate.js +10 -11
- package/dist/esm/helpers/expected-delegate.js.map +1 -1
- package/dist/esm/helpers/gas-compensation.js +3 -0
- package/dist/esm/helpers/gas-compensation.js.map +1 -1
- package/dist/esm/helpers/hex.js +29 -0
- package/dist/esm/helpers/hex.js.map +1 -1
- package/dist/esm/helpers/integer.js +7 -0
- package/dist/esm/helpers/integer.js.map +1 -0
- package/dist/esm/helpers/placeholder.js +3 -2
- package/dist/esm/helpers/placeholder.js.map +1 -1
- package/dist/esm/helpers/solana-intent-id.js +6 -5
- package/dist/esm/helpers/solana-intent-id.js.map +1 -1
- package/dist/esm/helpers/synthetic-native-wrap.js +52 -0
- package/dist/esm/helpers/synthetic-native-wrap.js.map +1 -0
- package/dist/esm/types/enums.js +2 -0
- package/dist/esm/types/enums.js.map +1 -1
- package/dist/esm/types/request.js +25 -4
- package/dist/esm/types/request.js.map +1 -1
- package/dist/esm/types/response.js +11 -3
- package/dist/esm/types/response.js.map +1 -1
- package/dist/esm/types/validation.js +40 -1
- package/dist/esm/types/validation.js.map +1 -1
- package/dist/esm/validations/accumulative-token.js +269 -0
- package/dist/esm/validations/accumulative-token.js.map +1 -0
- package/dist/esm/validations/compensation-hook.js +9 -0
- package/dist/esm/validations/compensation-hook.js.map +1 -1
- package/dist/esm/validations/cost-token.js +53 -0
- package/dist/esm/validations/cost-token.js.map +1 -0
- package/dist/esm/validations/dln-metadata.js +56 -0
- package/dist/esm/validations/dln-metadata.js.map +1 -0
- package/dist/esm/validations/evm-caveats.js +5 -3
- package/dist/esm/validations/evm-caveats.js.map +1 -1
- package/dist/esm/validations/evm-op-budget.js +4 -4
- package/dist/esm/validations/evm-op-budget.js.map +1 -1
- package/dist/esm/validations/evm-op-hook.js +7 -1
- package/dist/esm/validations/evm-op-hook.js.map +1 -1
- package/dist/esm/validations/evm-op-wrap.js +6 -5
- package/dist/esm/validations/evm-op-wrap.js.map +1 -1
- package/dist/esm/validations/evm-permit2.js +7 -4
- package/dist/esm/validations/evm-permit2.js.map +1 -1
- package/dist/esm/validations/evm-required-actions.js +7 -4
- package/dist/esm/validations/evm-required-actions.js.map +1 -1
- package/dist/esm/validations/external-call.js +325 -0
- package/dist/esm/validations/external-call.js.map +1 -0
- package/dist/esm/validations/hooks-common.js +38 -2
- package/dist/esm/validations/hooks-common.js.map +1 -1
- package/dist/esm/validations/intent-tokens.js +55 -41
- package/dist/esm/validations/intent-tokens.js.map +1 -1
- package/dist/esm/validations/intent.js +194 -73
- package/dist/esm/validations/intent.js.map +1 -1
- package/dist/esm/validations/ptp.js +154 -0
- package/dist/esm/validations/ptp.js.map +1 -0
- package/dist/esm/validations/required-actions.js +2 -2
- package/dist/esm/validations/required-actions.js.map +1 -1
- package/dist/esm/validations/solana-hook.js +13 -1
- package/dist/esm/validations/solana-hook.js.map +1 -1
- package/dist/esm/validations/solana-op-budget.js +21 -41
- package/dist/esm/validations/solana-op-budget.js.map +1 -1
- package/dist/esm/validations/solana-op-wrap.js +28 -33
- package/dist/esm/validations/solana-op-wrap.js.map +1 -1
- package/dist/esm/validations/total-dst-amount.js +429 -0
- package/dist/esm/validations/total-dst-amount.js.map +1 -0
- package/dist/esm/validations/trade-grouping.js +67 -25
- package/dist/esm/validations/trade-grouping.js.map +1 -1
- package/dist/esm/validations/trade-matching.js +109 -0
- package/dist/esm/validations/trade-matching.js.map +1 -0
- package/dist/esm/validations/trades.js +365 -71
- package/dist/esm/validations/trades.js.map +1 -1
- package/dist/esm/validator.js +22 -4
- package/dist/esm/validator.js.map +1 -1
- package/dist/esm/whitelists/environment-config.js +5 -5
- package/dist/esm/whitelists/environment-config.js.map +1 -1
- package/dist/types/helpers/evm-dln-hook-pack.d.ts.map +1 -1
- package/dist/types/helpers/evm-gas-compensation-tx.d.ts.map +1 -1
- package/dist/types/helpers/evm-intent-id.d.ts.map +1 -1
- package/dist/types/helpers/evm-trade-amounts.d.ts +1 -1
- package/dist/types/helpers/evm-trade-amounts.d.ts.map +1 -1
- package/dist/types/helpers/expected-delegate.d.ts.map +1 -1
- package/dist/types/helpers/gas-compensation.d.ts.map +1 -1
- package/dist/types/helpers/hex.d.ts +6 -0
- package/dist/types/helpers/hex.d.ts.map +1 -1
- package/dist/types/helpers/integer.d.ts +2 -0
- package/dist/types/helpers/integer.d.ts.map +1 -0
- package/dist/types/helpers/placeholder.d.ts.map +1 -1
- package/dist/types/helpers/solana-intent-id.d.ts.map +1 -1
- package/dist/types/helpers/synthetic-native-wrap.d.ts +4 -0
- package/dist/types/helpers/synthetic-native-wrap.d.ts.map +1 -0
- package/dist/types/types/enums.d.ts +4 -2
- package/dist/types/types/enums.d.ts.map +1 -1
- package/dist/types/types/request.d.ts +85 -6
- package/dist/types/types/request.d.ts.map +1 -1
- package/dist/types/types/response.d.ts +96 -12
- package/dist/types/types/response.d.ts.map +1 -1
- package/dist/types/types/validation.d.ts +36 -1
- package/dist/types/types/validation.d.ts.map +1 -1
- package/dist/types/validations/accumulative-token.d.ts +3 -0
- package/dist/types/validations/accumulative-token.d.ts.map +1 -0
- package/dist/types/validations/compensation-hook.d.ts.map +1 -1
- package/dist/types/validations/cost-token.d.ts +3 -0
- package/dist/types/validations/cost-token.d.ts.map +1 -0
- package/dist/types/validations/dln-metadata.d.ts +7 -0
- package/dist/types/validations/dln-metadata.d.ts.map +1 -0
- package/dist/types/validations/evm-caveats.d.ts.map +1 -1
- package/dist/types/validations/evm-op-hook.d.ts.map +1 -1
- package/dist/types/validations/evm-op-wrap.d.ts.map +1 -1
- package/dist/types/validations/evm-permit2.d.ts.map +1 -1
- package/dist/types/validations/evm-required-actions.d.ts +1 -1
- package/dist/types/validations/evm-required-actions.d.ts.map +1 -1
- package/dist/types/validations/external-call.d.ts +15 -0
- package/dist/types/validations/external-call.d.ts.map +1 -0
- package/dist/types/validations/intent-tokens.d.ts.map +1 -1
- package/dist/types/validations/intent.d.ts +1 -1
- package/dist/types/validations/intent.d.ts.map +1 -1
- package/dist/types/validations/ptp.d.ts +3 -0
- package/dist/types/validations/ptp.d.ts.map +1 -0
- package/dist/types/validations/required-actions.d.ts +1 -1
- package/dist/types/validations/required-actions.d.ts.map +1 -1
- package/dist/types/validations/solana-hook.d.ts.map +1 -1
- package/dist/types/validations/solana-op-budget.d.ts.map +1 -1
- package/dist/types/validations/solana-op-wrap.d.ts.map +1 -1
- package/dist/types/validations/total-dst-amount.d.ts +3 -0
- package/dist/types/validations/total-dst-amount.d.ts.map +1 -0
- package/dist/types/validations/trade-grouping.d.ts +2 -1
- package/dist/types/validations/trade-grouping.d.ts.map +1 -1
- package/dist/types/validations/trade-matching.d.ts +13 -0
- package/dist/types/validations/trade-matching.d.ts.map +1 -0
- package/dist/types/validations/trades.d.ts +2 -1
- package/dist/types/validations/trades.d.ts.map +1 -1
- package/dist/types/validator.d.ts.map +1 -1
- package/dist/types/whitelists/environment-config.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -5,31 +5,111 @@ const types_1 = require("../types");
|
|
|
5
5
|
const subscription_to_chain_id_1 = require("../whitelists/subscription-to-chain-id");
|
|
6
6
|
const address_1 = require("../helpers/address");
|
|
7
7
|
const trade_intent_consistency_1 = require("./trade-intent-consistency");
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
const integer_1 = require("../helpers/integer");
|
|
9
|
+
const trade_matching_1 = require("./trade-matching");
|
|
10
|
+
/**
|
|
11
|
+
* Per-trade role in asset-consolidation mode. Computed once for all trades at
|
|
12
|
+
* the start of validateTrades and threaded through to per-field validators.
|
|
13
|
+
*
|
|
14
|
+
* **Invariant** (enforced by `computeTradeRole`): `Regular` is emitted if and
|
|
15
|
+
* only if the request is NOT in consolidation mode (no `totalDstAmount`);
|
|
16
|
+
* `Balancing` and `NonBalancing` are emitted if and only if the request IS in
|
|
17
|
+
* consolidation mode. This means downstream validators can read consolidation-
|
|
18
|
+
* vs-regular directly from the role without taking `consolidationMode` as a
|
|
19
|
+
* separate parameter. This is made to simplify the logic in the per-field
|
|
20
|
+
* validators.
|
|
21
|
+
*
|
|
22
|
+
* - `Regular`: non-consolidation mode (no `totalDstAmount` set).
|
|
23
|
+
* - `Balancing`: in consolidation mode, the trade that closes the dst total.
|
|
24
|
+
* Covers two API pipeline paths collapsed into one validator behavior:
|
|
25
|
+
* 1. The last response trade when the pipeline found a balancing point
|
|
26
|
+
* (`estimatedTotalDstAmount === totalDstAmount`).
|
|
27
|
+
* 2. A non-last response trade with `mutatedWithOperatingExpense=true` —
|
|
28
|
+
* the "previously balancing" trade from the API's opEx-overflow
|
|
29
|
+
* recovery path. May appear at any non-last position; if overflow
|
|
30
|
+
* recovery recursed, several such trades can coexist in the middle.
|
|
31
|
+
* - `NonBalancing`: in consolidation mode, everything else — kept trades
|
|
32
|
+
* taken at the full upper bound, OR (when `allowPartialFill` exhausted
|
|
33
|
+
* without reaching `totalDstAmount`) every kept trade including the last.
|
|
34
|
+
*/
|
|
35
|
+
var TradeRole;
|
|
36
|
+
(function (TradeRole) {
|
|
37
|
+
TradeRole[TradeRole["Regular"] = 0] = "Regular";
|
|
38
|
+
TradeRole[TradeRole["Balancing"] = 1] = "Balancing";
|
|
39
|
+
TradeRole[TradeRole["NonBalancing"] = 2] = "NonBalancing";
|
|
40
|
+
})(TradeRole || (TradeRole = {}));
|
|
41
|
+
function validateTrades(request, response, quoteMode, tradePairs) {
|
|
19
42
|
const violations = [];
|
|
20
|
-
|
|
21
|
-
|
|
43
|
+
// CRITICAL: response cannot carry more trades than the request asked for.
|
|
44
|
+
// CRITICAL: response cannot carry fewer trades than the request, except when
|
|
45
|
+
// the request opts into asset-consolidation mode (totalDstAmount set), where
|
|
46
|
+
// the API may legitimately drop tail trades after the balancing trade.
|
|
47
|
+
const consolidationMode = request.totalDstAmount !== undefined;
|
|
48
|
+
const tooMany = response.trades.length > request.trades.length;
|
|
49
|
+
const tooFew = response.trades.length < request.trades.length && !consolidationMode;
|
|
50
|
+
if (tooMany || tooFew) {
|
|
51
|
+
violations.push({
|
|
52
|
+
code: types_1.ViolationCode.TradeCountMismatch,
|
|
53
|
+
message: "response.trades.length does not match request.trades.length",
|
|
54
|
+
path: "response.trades",
|
|
55
|
+
expected: request.trades.length,
|
|
56
|
+
actual: response.trades.length,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const isBalancingMode = computeIsBalancingMode(request, response);
|
|
60
|
+
const pairs = tradePairs ?? (0, trade_matching_1.matchTrades)(request.trades, response.trades, quoteMode, consolidationMode).pairs;
|
|
61
|
+
for (const pair of pairs) {
|
|
62
|
+
const isLastInResponse = pair.responseIndex === response.trades.length - 1;
|
|
63
|
+
const role = computeTradeRole(pair.responseTrade, isLastInResponse, consolidationMode, isBalancingMode);
|
|
64
|
+
violations.push(...validateSingleTrade(pair.requestTrade, pair.responseTrade, pair.requestIndex, pair.responseIndex, quoteMode, role));
|
|
22
65
|
}
|
|
23
66
|
// Cross-trade relatedIntentId consistency (execution mode only)
|
|
24
67
|
if (!quoteMode) {
|
|
25
68
|
violations.push(...(0, trade_intent_consistency_1.validateRelatedIntentIds)(response));
|
|
26
69
|
}
|
|
27
|
-
|
|
70
|
+
const partnerCostTrades = consolidationMode
|
|
71
|
+
? pairs.map(pair => pair.requestTrade)
|
|
72
|
+
: request.trades;
|
|
73
|
+
violations.push(...validatePartnerCost(response, partnerCostTrades));
|
|
28
74
|
return violations;
|
|
29
75
|
}
|
|
30
|
-
|
|
76
|
+
// True if and only if the pipeline ran the balancing-trade construction path
|
|
77
|
+
// (i.e. the dst total was reached). In `allowPartialFill=true` responses where
|
|
78
|
+
// the pipeline ran out of trades without reaching `totalDstAmount`, the last
|
|
79
|
+
// kept trade is NOT a balancing trade — every trade is taken in full.
|
|
80
|
+
function computeIsBalancingMode(request, response) {
|
|
81
|
+
if (request.totalDstAmount === undefined)
|
|
82
|
+
return false;
|
|
83
|
+
if (response.estimatedTotalDstAmount === undefined)
|
|
84
|
+
return false;
|
|
85
|
+
try {
|
|
86
|
+
return (0, integer_1.parseIntegerString)(response.estimatedTotalDstAmount) === (0, integer_1.parseIntegerString)(request.totalDstAmount);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Safe skip: parse error is reported separately by `validateTotalDstAmount`;
|
|
90
|
+
// fall back to the safer assumption (non-balancing mode, strict checks).
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function computeTradeRole(resTrade, isLastInResponse, consolidationMode, isBalancingMode) {
|
|
95
|
+
if (!consolidationMode)
|
|
96
|
+
return TradeRole.Regular;
|
|
97
|
+
if (isLastInResponse && isBalancingMode)
|
|
98
|
+
return TradeRole.Balancing;
|
|
99
|
+
// Overflow-recovery path: a non-last trade flagged with
|
|
100
|
+
// `mutatedWithOperatingExpense=true` is the previously-balancing trade
|
|
101
|
+
// that was demoted from the tail when its `srcAmount` overflowed the upper
|
|
102
|
+
// bound. Treat it like a balancing trade for validation purposes.
|
|
103
|
+
if (isBalancingMode
|
|
104
|
+
&& !isLastInResponse
|
|
105
|
+
&& resTrade.srcChainTokenIn.mutatedWithOperatingExpense) {
|
|
106
|
+
return TradeRole.Balancing;
|
|
107
|
+
}
|
|
108
|
+
return TradeRole.NonBalancing;
|
|
109
|
+
}
|
|
110
|
+
function validateSingleTrade(reqTrade, resTrade, requestIndex, responseIndex, quoteMode, role) {
|
|
31
111
|
const violations = [];
|
|
32
|
-
const basePath = `response.trades[${
|
|
112
|
+
const basePath = `response.trades[${responseIndex}]`;
|
|
33
113
|
const srcRealChainId = (0, subscription_to_chain_id_1.resolveChainId)(reqTrade.srcChainId);
|
|
34
114
|
const dstRealChainId = (0, subscription_to_chain_id_1.resolveChainId)(reqTrade.dstChainId);
|
|
35
115
|
// CRITICAL: dstChainId must be a known subscriptionId
|
|
@@ -37,12 +117,12 @@ function validateSingleTrade(reqTrade, resTrade, tradeIndex, quoteMode) {
|
|
|
37
117
|
violations.push({
|
|
38
118
|
code: types_1.ViolationCode.UnknownSubscriptionId,
|
|
39
119
|
message: `dstChainId=${reqTrade.dstChainId} is not a known subscriptionId`,
|
|
40
|
-
path: `request.trades[${
|
|
120
|
+
path: `request.trades[${requestIndex}].dstChainId`,
|
|
41
121
|
actual: reqTrade.dstChainId,
|
|
42
122
|
});
|
|
43
123
|
}
|
|
44
124
|
// Chain-agnostic validations (don't need resolved realChainId)
|
|
45
|
-
violations.push(...validateSrcChainTokenIn(reqTrade, resTrade, basePath), ...validateSrcChainTokenOut(reqTrade, resTrade, basePath), ...validateDstChainTokenOut(reqTrade, resTrade, basePath), ...validatePrependedOperatingExpenseCost(reqTrade, resTrade, basePath), ...validatePartnerFeeBps(reqTrade, resTrade, basePath));
|
|
125
|
+
violations.push(...validateSrcChainTokenIn(reqTrade, resTrade, basePath, role), ...validateSrcChainTokenOut(reqTrade, resTrade, basePath), ...validateDstChainTokenOut(reqTrade, resTrade, basePath, role), ...validatePrependedOperatingExpenseCost(reqTrade, resTrade, basePath, role), ...validatePartnerFeeBps(reqTrade, resTrade, basePath));
|
|
46
126
|
// Safe skip: undefined srcRealChainId is already reported as UnknownSubscriptionId by groupTrades
|
|
47
127
|
if (srcRealChainId !== undefined) {
|
|
48
128
|
violations.push(...validateSrcTokenInAddress(reqTrade, resTrade, basePath, srcRealChainId));
|
|
@@ -62,7 +142,7 @@ function validateSingleTrade(reqTrade, resTrade, tradeIndex, quoteMode) {
|
|
|
62
142
|
// =============================================
|
|
63
143
|
// srcChainTokenIn
|
|
64
144
|
// =============================================
|
|
65
|
-
function validateSrcChainTokenIn(reqTrade, resTrade, basePath) {
|
|
145
|
+
function validateSrcChainTokenIn(reqTrade, resTrade, basePath, role) {
|
|
66
146
|
const violations = [];
|
|
67
147
|
const src = resTrade.srcChainTokenIn;
|
|
68
148
|
const path = `${basePath}.srcChainTokenIn`;
|
|
@@ -76,12 +156,17 @@ function validateSrcChainTokenIn(reqTrade, resTrade, basePath) {
|
|
|
76
156
|
actual: src.chainId,
|
|
77
157
|
});
|
|
78
158
|
}
|
|
79
|
-
// CRITICAL: amount must match when prependOperatingExpenses is false and
|
|
80
|
-
|
|
159
|
+
// CRITICAL: amount must match when `prependOperatingExpenses` is false and
|
|
160
|
+
// amount is numeric. A balancing trade is allowed amount <= upperBound;
|
|
161
|
+
// a non-balancing trade in consolidation mode must equal upperBound
|
|
162
|
+
// regardless of `request.prependOperatingExpenses`.
|
|
163
|
+
violations.push(...validateSrcTokenInAmount(reqTrade, src, path, role));
|
|
81
164
|
// CRITICAL + MEDIUM: operating expense checks
|
|
82
165
|
violations.push(...validateOperatingExpense(resTrade, src, basePath));
|
|
83
|
-
// CRITICAL: mutatedWithOperatingExpense must reflect request conditions
|
|
84
|
-
|
|
166
|
+
// CRITICAL: `mutatedWithOperatingExpense` must reflect request conditions.
|
|
167
|
+
// Skipped for the balancing trade (API freely sets it on the rebalanced
|
|
168
|
+
// trade); for non-balancing in consolidation mode forced to false.
|
|
169
|
+
violations.push(...validateMutatedWithOperatingExpense(reqTrade, src, path, role));
|
|
85
170
|
// CRITICAL: USD values must be non-negative
|
|
86
171
|
violations.push(...checkNonNegativeUsd(src.approximateUsdValue, `${path}.approximateUsdValue`), ...checkNonNegativeUsd(src.originApproximateUsdValue, `${path}.originApproximateUsdValue`));
|
|
87
172
|
return violations;
|
|
@@ -101,18 +186,62 @@ function validateSrcTokenInAddress(reqTrade, resTrade, basePath, srcRealChainId)
|
|
|
101
186
|
}
|
|
102
187
|
return [];
|
|
103
188
|
}
|
|
104
|
-
function validateSrcTokenInAmount(reqTrade, src, path) {
|
|
105
|
-
// Skip when prependOperatingExpenses is true — amount includes expenses
|
|
106
|
-
if (reqTrade.prependOperatingExpenses ?? false)
|
|
107
|
-
return [];
|
|
189
|
+
function validateSrcTokenInAmount(reqTrade, src, path, role) {
|
|
108
190
|
// Skip for non-numeric amounts (auto, max)
|
|
109
191
|
const amount = reqTrade.srcChainTokenInAmount;
|
|
110
192
|
if (amount === types_1.SpecialAmount.Auto || amount === types_1.SpecialAmount.Max)
|
|
111
193
|
return [];
|
|
194
|
+
// Balancing trade: API rebalances `srcAmount` down so the dst total matches
|
|
195
|
+
// exactly. It may be strictly less than the requested upper bound but
|
|
196
|
+
// never more. Covers both the last-trade case and the "previously
|
|
197
|
+
// balancing" trade from the opEx-overflow recovery path.
|
|
198
|
+
if (role === TradeRole.Balancing) {
|
|
199
|
+
return validateSrcTokenInAmountAgainstUpperBound(amount, src, path);
|
|
200
|
+
}
|
|
201
|
+
// Two roles remain (per TradeRole invariant): NonBalancing => consolidation
|
|
202
|
+
// mode, Regular => non-consolidation. In consolidation mode the API forces
|
|
203
|
+
// `prependOperatingExpenses=false` internally regardless of the request flag,
|
|
204
|
+
// so we require exact-equality even when the request had `prepend=true`. In
|
|
205
|
+
// regular mode we honor the request flag as-is.
|
|
206
|
+
const prependOperatingExpenses = role === TradeRole.NonBalancing
|
|
207
|
+
? false
|
|
208
|
+
: (reqTrade.prependOperatingExpenses ?? false);
|
|
209
|
+
// CRITICAL: when `prependOperatingExpenses` is true the response amount
|
|
210
|
+
// must be >= request amount (operating expenses are added on top of the
|
|
211
|
+
// requested input). Exact equality is checked in the `false` branch
|
|
212
|
+
// below.
|
|
213
|
+
if (prependOperatingExpenses) {
|
|
214
|
+
try {
|
|
215
|
+
const requested = (0, integer_1.parseIntegerString)(amount);
|
|
216
|
+
const actual = (0, integer_1.parseIntegerString)(src.amount);
|
|
217
|
+
if (actual < requested) {
|
|
218
|
+
return [{
|
|
219
|
+
code: types_1.ViolationCode.TradeSrcTokenInAmountBelowRequested,
|
|
220
|
+
message: "srcChainTokenIn.amount is below requested srcChainTokenInAmount"
|
|
221
|
+
+ " (with prependOperatingExpenses=true the API must add expenses, "
|
|
222
|
+
+ "not deduct from the requested amount)",
|
|
223
|
+
path: `${path}.amount`,
|
|
224
|
+
expected: { min: amount },
|
|
225
|
+
actual: src.amount,
|
|
226
|
+
}];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
return [{
|
|
231
|
+
code: types_1.ViolationCode.TradeSrcTokenInAmountBelowRequested,
|
|
232
|
+
message: "failed to parse amounts for comparison",
|
|
233
|
+
path: `${path}.amount`,
|
|
234
|
+
expected: { min: amount },
|
|
235
|
+
actual: src.amount,
|
|
236
|
+
details: e,
|
|
237
|
+
}];
|
|
238
|
+
}
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
112
241
|
// CRITICAL: srcChainTokenIn.amount must equal request srcChainTokenInAmount
|
|
113
242
|
try {
|
|
114
|
-
const expected =
|
|
115
|
-
const actual =
|
|
243
|
+
const expected = (0, integer_1.parseIntegerString)(amount);
|
|
244
|
+
const actual = (0, integer_1.parseIntegerString)(src.amount);
|
|
116
245
|
if (actual !== expected) {
|
|
117
246
|
return [{
|
|
118
247
|
code: types_1.ViolationCode.TradeSrcTokenInAmountMismatch,
|
|
@@ -135,11 +264,39 @@ function validateSrcTokenInAmount(reqTrade, src, path) {
|
|
|
135
264
|
}
|
|
136
265
|
return [];
|
|
137
266
|
}
|
|
267
|
+
function validateSrcTokenInAmountAgainstUpperBound(upperBound, src, path) {
|
|
268
|
+
// CRITICAL: in asset-consolidation mode the balancing trade may be
|
|
269
|
+
// rebalanced down by the API to make the dst total match. It may be
|
|
270
|
+
// strictly less than the requested upper bound, but never more.
|
|
271
|
+
try {
|
|
272
|
+
const bound = (0, integer_1.parseIntegerString)(upperBound);
|
|
273
|
+
const actual = (0, integer_1.parseIntegerString)(src.amount);
|
|
274
|
+
if (actual > bound) {
|
|
275
|
+
return [{
|
|
276
|
+
code: types_1.ViolationCode.TradeSrcTokenInAmountExceedsUpperBound,
|
|
277
|
+
message: "srcChainTokenIn.amount exceeds requested upper bound on the balancing trade",
|
|
278
|
+
path: `${path}.amount`,
|
|
279
|
+
expected: { max: upperBound },
|
|
280
|
+
actual: src.amount,
|
|
281
|
+
}];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch (e) {
|
|
285
|
+
return [{
|
|
286
|
+
code: types_1.ViolationCode.TradeSrcTokenInAmountExceedsUpperBound,
|
|
287
|
+
message: "failed to parse amounts for comparison",
|
|
288
|
+
path: `${path}.amount`,
|
|
289
|
+
actual: src.amount,
|
|
290
|
+
details: e,
|
|
291
|
+
}];
|
|
292
|
+
}
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
138
295
|
function validateOperatingExpense(resTrade, src, basePath) {
|
|
139
296
|
const expensePath = `${basePath}.srcChainTokenIn.approximateOperatingExpense`;
|
|
140
297
|
let parsedExpense;
|
|
141
298
|
try {
|
|
142
|
-
parsedExpense =
|
|
299
|
+
parsedExpense = (0, integer_1.parseIntegerString)(src.approximateOperatingExpense);
|
|
143
300
|
}
|
|
144
301
|
catch (e) {
|
|
145
302
|
return [{
|
|
@@ -187,11 +344,11 @@ function checkOperatingExpensePositive(resTrade, src, parsedExpense, basePath) {
|
|
|
187
344
|
}
|
|
188
345
|
// MEDIUM: if `prependedOperatingExpenseCost` is non-zero, expense must be > 0.
|
|
189
346
|
// Skip when the field is absent — secondary trades in a group don't carry
|
|
190
|
-
// it (expenses accumulate on the primary trade);
|
|
191
|
-
//
|
|
347
|
+
// it (expenses accumulate on the primary trade); parsing an absent value
|
|
348
|
+
// would produce a misleading `TradePrependedCostInvalid`.
|
|
192
349
|
if (resTrade.prependedOperatingExpenseCost !== undefined) {
|
|
193
350
|
try {
|
|
194
|
-
if (
|
|
351
|
+
if ((0, integer_1.parseIntegerString)(resTrade.prependedOperatingExpenseCost) !== 0n) {
|
|
195
352
|
violations.push({
|
|
196
353
|
code: types_1.ViolationCode.TradeOperatingExpenseExpectedPositive,
|
|
197
354
|
message: "approximateOperatingExpense should be > 0"
|
|
@@ -213,14 +370,40 @@ function checkOperatingExpensePositive(resTrade, src, parsedExpense, basePath) {
|
|
|
213
370
|
}
|
|
214
371
|
return violations;
|
|
215
372
|
}
|
|
216
|
-
function validateMutatedWithOperatingExpense(reqTrade, src, path) {
|
|
373
|
+
function validateMutatedWithOperatingExpense(reqTrade, src, path, role) {
|
|
217
374
|
// Safe skip: in `max` mode the API forces `prependOperatingExpenses=false`
|
|
218
375
|
// internally (operating expenses are already deducted from the max balance),
|
|
219
376
|
// so `mutatedWithOperatingExpense` is unpredictable from the request alone.
|
|
220
377
|
// Validating mutated amount is also impossible (we don't know the user's balance).
|
|
221
378
|
if (reqTrade.srcChainTokenInAmount === types_1.SpecialAmount.Max)
|
|
222
379
|
return [];
|
|
223
|
-
|
|
380
|
+
// Balancing trade: API pipeline pins the flag based on its own internal
|
|
381
|
+
// path, not the request. Two cases (both checkable):
|
|
382
|
+
// - `request.srcChainTokenInAmount === 'auto'` → unlimited balancing →
|
|
383
|
+
// `prependOperatingExpenses=false` → `mutatedWithOperatingExpense=false`.
|
|
384
|
+
// - `request.srcChainTokenInAmount` is a number → scaled balancing →
|
|
385
|
+
// `prependOperatingExpenses=true` → `mutatedWithOperatingExpense=true`.
|
|
386
|
+
if (role === TradeRole.Balancing) {
|
|
387
|
+
const expectedMutated = reqTrade.srcChainTokenInAmount !== types_1.SpecialAmount.Auto;
|
|
388
|
+
if (src.mutatedWithOperatingExpense !== expectedMutated) {
|
|
389
|
+
return [{
|
|
390
|
+
code: types_1.ViolationCode.TradeMutatedWithOperatingExpenseMismatch,
|
|
391
|
+
message: "srcChainTokenIn.mutatedWithOperatingExpense does not match"
|
|
392
|
+
+ " expected value for the balancing trade",
|
|
393
|
+
path: `${path}.mutatedWithOperatingExpense`,
|
|
394
|
+
expected: expectedMutated,
|
|
395
|
+
actual: src.mutatedWithOperatingExpense,
|
|
396
|
+
}];
|
|
397
|
+
}
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
// Two roles remain (per TradeRole invariant): NonBalancing => consolidation
|
|
401
|
+
// mode, Regular => non-consolidation. In consolidation mode the API forces
|
|
402
|
+
// `prependOperatingExpenses=false` internally regardless of the request flag.
|
|
403
|
+
// In regular mode we honor the request flag as-is.
|
|
404
|
+
const prependExpenses = role === TradeRole.NonBalancing
|
|
405
|
+
? false
|
|
406
|
+
: (reqTrade.prependOperatingExpenses ?? false);
|
|
224
407
|
const isAuto = reqTrade.srcChainTokenInAmount === types_1.SpecialAmount.Auto;
|
|
225
408
|
const expectedMutated = prependExpenses || isAuto;
|
|
226
409
|
// CRITICAL: must reflect request conditions
|
|
@@ -254,7 +437,7 @@ function validateSrcChainTokenOut(reqTrade, resTrade, basePath) {
|
|
|
254
437
|
}
|
|
255
438
|
// CRITICAL: amount must be non-negative
|
|
256
439
|
try {
|
|
257
|
-
if (
|
|
440
|
+
if ((0, integer_1.parseIntegerString)(resTrade.srcChainTokenOut.amount) < 0n) {
|
|
258
441
|
return [{
|
|
259
442
|
code: types_1.ViolationCode.TradeSrcTokenOutAmountNegative,
|
|
260
443
|
message: "srcChainTokenOut.amount must be non-negative",
|
|
@@ -282,7 +465,7 @@ function validateSrcChainTokenOut(reqTrade, resTrade, basePath) {
|
|
|
282
465
|
// =============================================
|
|
283
466
|
// dstChainTokenOut
|
|
284
467
|
// =============================================
|
|
285
|
-
function validateDstChainTokenOut(reqTrade, resTrade, basePath) {
|
|
468
|
+
function validateDstChainTokenOut(reqTrade, resTrade, basePath, role) {
|
|
286
469
|
const violations = [];
|
|
287
470
|
const dst = resTrade.dstChainTokenOut;
|
|
288
471
|
const path = `${basePath}.dstChainTokenOut`;
|
|
@@ -297,7 +480,7 @@ function validateDstChainTokenOut(reqTrade, resTrade, basePath) {
|
|
|
297
480
|
});
|
|
298
481
|
}
|
|
299
482
|
// CRITICAL: amount and withoutPartnerFee checks
|
|
300
|
-
violations.push(...validateDstTokenOutAmount(reqTrade, dst, path), ...validateWithoutPartnerFeeAmount(reqTrade,
|
|
483
|
+
violations.push(...validateDstTokenOutAmount(reqTrade, dst, path), ...validateWithoutPartnerFeeAmount(reqTrade, resTrade, path, role));
|
|
301
484
|
// CRITICAL: USD values must be non-negative
|
|
302
485
|
violations.push(...checkNonNegativeUsd(dst.approximateUsdValue, `${path}.approximateUsdValue`), ...checkNonNegativeUsd(dst.recommendedApproximateUsdValue, `${path}.recommendedApproximateUsdValue`));
|
|
303
486
|
// CRITICAL: maxTheoreticalApproximateUsdValue must be present — API always sets it
|
|
@@ -336,8 +519,8 @@ function validateDstTokenOutAmount(reqTrade, dst, path) {
|
|
|
336
519
|
return [];
|
|
337
520
|
// CRITICAL: amount must be >= requested dstChainTokenOutAmount
|
|
338
521
|
try {
|
|
339
|
-
const requested =
|
|
340
|
-
const actual =
|
|
522
|
+
const requested = (0, integer_1.parseIntegerString)(reqTrade.dstChainTokenOutAmount);
|
|
523
|
+
const actual = (0, integer_1.parseIntegerString)(dst.amount);
|
|
341
524
|
if (actual < requested) {
|
|
342
525
|
return [{
|
|
343
526
|
code: types_1.ViolationCode.TradeDstTokenOutAmountBelowRequested,
|
|
@@ -360,7 +543,8 @@ function validateDstTokenOutAmount(reqTrade, dst, path) {
|
|
|
360
543
|
}
|
|
361
544
|
return [];
|
|
362
545
|
}
|
|
363
|
-
function validateWithoutPartnerFeeAmount(reqTrade,
|
|
546
|
+
function validateWithoutPartnerFeeAmount(reqTrade, resTrade, path, role) {
|
|
547
|
+
const dst = resTrade.dstChainTokenOut;
|
|
364
548
|
const feePath = `${path}.withoutPartnerFeeBpsAmount`;
|
|
365
549
|
// CRITICAL: field must be present — API always sets it
|
|
366
550
|
if (dst.withoutPartnerFeeBpsAmount === undefined) {
|
|
@@ -373,7 +557,7 @@ function validateWithoutPartnerFeeAmount(reqTrade, dst, path) {
|
|
|
373
557
|
}
|
|
374
558
|
let withoutFee;
|
|
375
559
|
try {
|
|
376
|
-
withoutFee =
|
|
560
|
+
withoutFee = (0, integer_1.parseIntegerString)(dst.withoutPartnerFeeBpsAmount);
|
|
377
561
|
}
|
|
378
562
|
catch (e) {
|
|
379
563
|
return [{
|
|
@@ -386,7 +570,7 @@ function validateWithoutPartnerFeeAmount(reqTrade, dst, path) {
|
|
|
386
570
|
}
|
|
387
571
|
let amount;
|
|
388
572
|
try {
|
|
389
|
-
amount =
|
|
573
|
+
amount = (0, integer_1.parseIntegerString)(dst.amount);
|
|
390
574
|
}
|
|
391
575
|
catch (e) {
|
|
392
576
|
return [{
|
|
@@ -397,9 +581,7 @@ function validateWithoutPartnerFeeAmount(reqTrade, dst, path) {
|
|
|
397
581
|
details: e,
|
|
398
582
|
}];
|
|
399
583
|
}
|
|
400
|
-
const isExactOutputMode = reqTrade
|
|
401
|
-
|| (reqTrade.dstChainTokenOutAmount !== undefined
|
|
402
|
-
&& reqTrade.dstChainTokenOutAmount !== types_1.SpecialAmount.Auto);
|
|
584
|
+
const isExactOutputMode = isExactOutputTrade(reqTrade, resTrade, role);
|
|
403
585
|
if (isExactOutputMode) {
|
|
404
586
|
if (withoutFee < 0n) {
|
|
405
587
|
return [{
|
|
@@ -435,6 +617,33 @@ function validateWithoutPartnerFeeAmount(reqTrade, dst, path) {
|
|
|
435
617
|
}
|
|
436
618
|
return [];
|
|
437
619
|
}
|
|
620
|
+
function isExactOutputTrade(reqTrade, resTrade, role) {
|
|
621
|
+
// Exact-output response shape means `dstChainTokenOut.amount` is pinned to a
|
|
622
|
+
// user/API target while `withoutPartnerFeeBpsAmount` can reflect the lower
|
|
623
|
+
// post-cost solver output. The API does not expose `Trade.isExactOutput`
|
|
624
|
+
// directly, so derive the shape from the request plus consolidation role.
|
|
625
|
+
return role === TradeRole.Balancing
|
|
626
|
+
|| reqTrade.srcChainTokenInAmount === types_1.SpecialAmount.Auto
|
|
627
|
+
|| (reqTrade.dstChainTokenOutAmount !== undefined
|
|
628
|
+
&& reqTrade.dstChainTokenOutAmount !== types_1.SpecialAmount.Auto)
|
|
629
|
+
|| (resTrade.srcChainTokenIn.mutatedWithOperatingExpense
|
|
630
|
+
&& isMutatedWithOperatingExpenseRoleValid(reqTrade, role));
|
|
631
|
+
}
|
|
632
|
+
function isMutatedWithOperatingExpenseRoleValid(reqTrade, role) {
|
|
633
|
+
// Max mode is intentionally not validated because the API handles opEx
|
|
634
|
+
// against the unknown wallet balance. Do not let this unverifiable flag
|
|
635
|
+
// grant exact-output `withoutFee` semantics on its own.
|
|
636
|
+
if (reqTrade.srcChainTokenInAmount === types_1.SpecialAmount.Max)
|
|
637
|
+
return false;
|
|
638
|
+
if (role === TradeRole.Balancing) {
|
|
639
|
+
return reqTrade.srcChainTokenInAmount !== types_1.SpecialAmount.Auto;
|
|
640
|
+
}
|
|
641
|
+
const prependExpenses = role === TradeRole.NonBalancing
|
|
642
|
+
? false
|
|
643
|
+
: (reqTrade.prependOperatingExpenses ?? false);
|
|
644
|
+
const isAuto = reqTrade.srcChainTokenInAmount === types_1.SpecialAmount.Auto;
|
|
645
|
+
return prependExpenses || isAuto;
|
|
646
|
+
}
|
|
438
647
|
// =============================================
|
|
439
648
|
// Execution-mode address echo fields
|
|
440
649
|
// =============================================
|
|
@@ -484,12 +693,35 @@ function validateAddressEcho(expected, actual, code, path, fieldName, realChainI
|
|
|
484
693
|
// =============================================
|
|
485
694
|
// prependedOperatingExpenseCost
|
|
486
695
|
// =============================================
|
|
487
|
-
function validatePrependedOperatingExpenseCost(reqTrade, resTrade, basePath) {
|
|
488
|
-
const prependExpenses = reqTrade.prependOperatingExpenses ?? false;
|
|
489
|
-
const isAuto = reqTrade.srcChainTokenInAmount === types_1.SpecialAmount.Auto;
|
|
696
|
+
function validatePrependedOperatingExpenseCost(reqTrade, resTrade, basePath, role) {
|
|
490
697
|
const isMax = reqTrade.srcChainTokenInAmount === types_1.SpecialAmount.Max;
|
|
491
|
-
//
|
|
492
|
-
|
|
698
|
+
// Skip: max mode handles operating expenses against the wallet balance,
|
|
699
|
+
// which the SDK cannot know.
|
|
700
|
+
if (isMax)
|
|
701
|
+
return [];
|
|
702
|
+
// Balancing trade: API pipeline pins the cost based on its own internal
|
|
703
|
+
// path, not the request:
|
|
704
|
+
// - `request.srcChainTokenInAmount === 'auto'` → unlimited balancing →
|
|
705
|
+
// `prependOperatingExpenses=false` → cost === '0' (or absent).
|
|
706
|
+
// - `request.srcChainTokenInAmount` is a number → scaled balancing →
|
|
707
|
+
// `prependOperatingExpenses=true` → cost > 0 (when present on primary).
|
|
708
|
+
if (role === TradeRole.Balancing) {
|
|
709
|
+
return validateBalancingTradePrependCost(reqTrade, resTrade, basePath);
|
|
710
|
+
}
|
|
711
|
+
// Two roles remain (per TradeRole invariant): NonBalancing => consolidation
|
|
712
|
+
// mode, Regular => non-consolidation. In consolidation mode the API forces
|
|
713
|
+
// `prependOperatingExpenses=false` internally regardless of the request flag.
|
|
714
|
+
// In regular mode we honor the request flag as-is.
|
|
715
|
+
const prependExpenses = role === TradeRole.NonBalancing
|
|
716
|
+
? false
|
|
717
|
+
: (reqTrade.prependOperatingExpenses ?? false);
|
|
718
|
+
const isAuto = reqTrade.srcChainTokenInAmount === types_1.SpecialAmount.Auto;
|
|
719
|
+
// Skip when the API is allowed to mutate `prependedOperatingExpenseCost`
|
|
720
|
+
// freely:
|
|
721
|
+
// - `prependOperatingExpenses=true` (regular mode): cost reflects the prepended amount.
|
|
722
|
+
// - 'auto' srcChainTokenInAmount (regular mode): API forces the effective flag.
|
|
723
|
+
// In all other cases the cost must be zero — that's the only check below.
|
|
724
|
+
if (prependExpenses || isAuto)
|
|
493
725
|
return [];
|
|
494
726
|
// Treat absent field as "0" — the API omits it on secondary trades in a
|
|
495
727
|
// group (expenses accumulate on the primary trade). For `prepend=false`
|
|
@@ -500,7 +732,7 @@ function validatePrependedOperatingExpenseCost(reqTrade, resTrade, basePath) {
|
|
|
500
732
|
// CRITICAL: `prependedOperatingExpenseCost` must be zero
|
|
501
733
|
let parsed;
|
|
502
734
|
try {
|
|
503
|
-
parsed =
|
|
735
|
+
parsed = (0, integer_1.parseIntegerString)(resTrade.prependedOperatingExpenseCost);
|
|
504
736
|
}
|
|
505
737
|
catch (error) {
|
|
506
738
|
return [{
|
|
@@ -524,15 +756,85 @@ function validatePrependedOperatingExpenseCost(reqTrade, resTrade, basePath) {
|
|
|
524
756
|
}
|
|
525
757
|
return [];
|
|
526
758
|
}
|
|
759
|
+
function validateBalancingTradePrependCost(reqTrade, resTrade, basePath) {
|
|
760
|
+
const costPath = `${basePath}.prependedOperatingExpenseCost`;
|
|
761
|
+
const isAuto = reqTrade.srcChainTokenInAmount === types_1.SpecialAmount.Auto;
|
|
762
|
+
// Unlimited balancing (`request.srcChainTokenInAmount === 'auto'`): the API
|
|
763
|
+
// sets the Trade-level `prependOperatingExpenses=false` on the unlimited
|
|
764
|
+
// balancing trade, so the response emits `cost === '0'` (or omits the field).
|
|
765
|
+
if (isAuto) {
|
|
766
|
+
if (resTrade.prependedOperatingExpenseCost === undefined)
|
|
767
|
+
return [];
|
|
768
|
+
let parsed;
|
|
769
|
+
try {
|
|
770
|
+
parsed = (0, integer_1.parseIntegerString)(resTrade.prependedOperatingExpenseCost);
|
|
771
|
+
}
|
|
772
|
+
catch (error) {
|
|
773
|
+
return [{
|
|
774
|
+
code: types_1.ViolationCode.TradePrependedCostInvalid,
|
|
775
|
+
message: "prependedOperatingExpenseCost is not a valid number",
|
|
776
|
+
path: costPath,
|
|
777
|
+
actual: resTrade.prependedOperatingExpenseCost,
|
|
778
|
+
details: error,
|
|
779
|
+
}];
|
|
780
|
+
}
|
|
781
|
+
if (parsed !== 0n) {
|
|
782
|
+
return [{
|
|
783
|
+
code: types_1.ViolationCode.TradePrependedCostNotZero,
|
|
784
|
+
message: "prependedOperatingExpenseCost must be zero on the unlimited"
|
|
785
|
+
+ " balancing trade (srcChainTokenInAmount='auto')",
|
|
786
|
+
path: costPath,
|
|
787
|
+
expected: "0",
|
|
788
|
+
actual: resTrade.prependedOperatingExpenseCost,
|
|
789
|
+
}];
|
|
790
|
+
}
|
|
791
|
+
return [];
|
|
792
|
+
}
|
|
793
|
+
// Scaled balancing (`request.srcChainTokenInAmount` is a number): the API
|
|
794
|
+
// sets the Trade-level `prependOperatingExpenses=true`, so the response
|
|
795
|
+
// emits `cost > 0` on the primary trade (or omits the field for secondaries
|
|
796
|
+
// in a group — we cannot distinguish here without group context, so omit
|
|
797
|
+
// means "skip"; the corresponding primary in the same group will report).
|
|
798
|
+
if (resTrade.prependedOperatingExpenseCost === undefined)
|
|
799
|
+
return [];
|
|
800
|
+
let parsed;
|
|
801
|
+
try {
|
|
802
|
+
parsed = (0, integer_1.parseIntegerString)(resTrade.prependedOperatingExpenseCost);
|
|
803
|
+
}
|
|
804
|
+
catch (error) {
|
|
805
|
+
return [{
|
|
806
|
+
code: types_1.ViolationCode.TradePrependedCostInvalid,
|
|
807
|
+
message: "prependedOperatingExpenseCost is not a valid number",
|
|
808
|
+
path: costPath,
|
|
809
|
+
actual: resTrade.prependedOperatingExpenseCost,
|
|
810
|
+
details: error,
|
|
811
|
+
}];
|
|
812
|
+
}
|
|
813
|
+
if (parsed <= 0n) {
|
|
814
|
+
return [{
|
|
815
|
+
code: types_1.ViolationCode.TradePrependedCostNotZero,
|
|
816
|
+
message: "prependedOperatingExpenseCost must be > 0 on the scaled"
|
|
817
|
+
+ " balancing trade (request.srcChainTokenInAmount is a number)",
|
|
818
|
+
path: costPath,
|
|
819
|
+
actual: resTrade.prependedOperatingExpenseCost,
|
|
820
|
+
}];
|
|
821
|
+
}
|
|
822
|
+
return [];
|
|
823
|
+
}
|
|
527
824
|
// =============================================
|
|
528
825
|
// partnerFeeBps
|
|
529
826
|
// =============================================
|
|
530
|
-
// API aggregates per-trade partner fees into a single bundle-wide cost item
|
|
531
|
-
// `amount` is the sum across trades
|
|
532
|
-
//
|
|
533
|
-
|
|
827
|
+
// API aggregates per-trade partner fees into a single bundle-wide cost item.
|
|
828
|
+
// `amount` is the sum across final response trades. `feeBps` is taken from the
|
|
829
|
+
// first final per-trade entry, so consolidation mode must ignore skipped
|
|
830
|
+
// request trades when choosing the expected value.
|
|
831
|
+
function validatePartnerCost(response, requestTradesForCost) {
|
|
534
832
|
const partnerCosts = response.bundleCosts.filter(c => c.type === types_1.CostItemType.PARTNER_COST);
|
|
833
|
+
const requestedFees = requestTradesForCost.map(t => t.partnerFeeBps ?? 0);
|
|
834
|
+
const anyNonZero = requestedFees.some(b => b > 0);
|
|
535
835
|
if (partnerCosts.length === 0) {
|
|
836
|
+
if (!anyNonZero)
|
|
837
|
+
return [];
|
|
536
838
|
return [{
|
|
537
839
|
code: types_1.ViolationCode.PartnerCostMissing,
|
|
538
840
|
message: "bundleCosts[PARTNER_COST] entry is missing",
|
|
@@ -544,7 +846,7 @@ function validatePartnerCost(request, response) {
|
|
|
544
846
|
const path = `response.bundleCosts[PARTNER_COST]`;
|
|
545
847
|
let amount;
|
|
546
848
|
try {
|
|
547
|
-
amount =
|
|
849
|
+
amount = (0, integer_1.parseIntegerString)(partnerCost.amount);
|
|
548
850
|
}
|
|
549
851
|
catch (error) {
|
|
550
852
|
return [{
|
|
@@ -572,8 +874,6 @@ function validatePartnerCost(request, response) {
|
|
|
572
874
|
actual: feeBps,
|
|
573
875
|
});
|
|
574
876
|
}
|
|
575
|
-
const requestedFees = request.trades.map(t => t.partnerFeeBps ?? 0);
|
|
576
|
-
const anyNonZero = requestedFees.some(b => b > 0);
|
|
577
877
|
// CRITICAL: when no trade has a partner fee, the aggregated amount must be zero.
|
|
578
878
|
if (!anyNonZero && amount !== 0n) {
|
|
579
879
|
violations.push({
|
|
@@ -584,22 +884,16 @@ function validatePartnerCost(request, response) {
|
|
|
584
884
|
actual: partnerCost.amount,
|
|
585
885
|
});
|
|
586
886
|
}
|
|
587
|
-
//
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
path: `${path}.amount`,
|
|
593
|
-
actual: partnerCost.amount,
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
// CRITICAL: feeBps must echo request.trades[0].partnerFeeBps (or 0 when omitted).
|
|
597
|
-
if (feeBps !== undefined && request.trades.length > 0) {
|
|
887
|
+
// NO NEED: amount may round to zero for very small selected trades even when
|
|
888
|
+
// partnerFeeBps is non-zero, so a strict positive-amount check would create
|
|
889
|
+
// false positives. We still validate amount format and the no-fee non-zero case.
|
|
890
|
+
// CRITICAL: feeBps must echo the first selected request trade's partnerFeeBps (or 0 when omitted).
|
|
891
|
+
if (feeBps !== undefined && requestTradesForCost.length > 0) {
|
|
598
892
|
const expected = requestedFees[0];
|
|
599
893
|
if (feeBps !== expected) {
|
|
600
894
|
violations.push({
|
|
601
895
|
code: types_1.ViolationCode.PartnerCostFeeBpsMismatch,
|
|
602
|
-
message: "PARTNER_COST.feeBps does not match
|
|
896
|
+
message: "PARTNER_COST.feeBps does not match the first selected trade partnerFeeBps",
|
|
603
897
|
path: `${path}.feeBps`,
|
|
604
898
|
expected,
|
|
605
899
|
actual: feeBps,
|