@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.
Files changed (219) hide show
  1. package/dist/cjs/helpers/evm-dln-hook-pack.js +8 -6
  2. package/dist/cjs/helpers/evm-dln-hook-pack.js.map +1 -1
  3. package/dist/cjs/helpers/evm-gas-compensation-tx.js +2 -1
  4. package/dist/cjs/helpers/evm-gas-compensation-tx.js.map +1 -1
  5. package/dist/cjs/helpers/evm-intent-id.js +6 -5
  6. package/dist/cjs/helpers/evm-intent-id.js.map +1 -1
  7. package/dist/cjs/helpers/evm-trade-amounts.js +25 -43
  8. package/dist/cjs/helpers/evm-trade-amounts.js.map +1 -1
  9. package/dist/cjs/helpers/expected-delegate.js +10 -11
  10. package/dist/cjs/helpers/expected-delegate.js.map +1 -1
  11. package/dist/cjs/helpers/gas-compensation.js +3 -0
  12. package/dist/cjs/helpers/gas-compensation.js.map +1 -1
  13. package/dist/cjs/helpers/hex.js +35 -0
  14. package/dist/cjs/helpers/hex.js.map +1 -1
  15. package/dist/cjs/helpers/integer.js +10 -0
  16. package/dist/cjs/helpers/integer.js.map +1 -0
  17. package/dist/cjs/helpers/placeholder.js +3 -2
  18. package/dist/cjs/helpers/placeholder.js.map +1 -1
  19. package/dist/cjs/helpers/solana-intent-id.js +6 -5
  20. package/dist/cjs/helpers/solana-intent-id.js.map +1 -1
  21. package/dist/cjs/helpers/synthetic-native-wrap.js +56 -0
  22. package/dist/cjs/helpers/synthetic-native-wrap.js.map +1 -0
  23. package/dist/cjs/types/enums.js +2 -0
  24. package/dist/cjs/types/enums.js.map +1 -1
  25. package/dist/cjs/types/request.js +27 -5
  26. package/dist/cjs/types/request.js.map +1 -1
  27. package/dist/cjs/types/response.js +11 -3
  28. package/dist/cjs/types/response.js.map +1 -1
  29. package/dist/cjs/types/validation.js +40 -1
  30. package/dist/cjs/types/validation.js.map +1 -1
  31. package/dist/cjs/validations/accumulative-token.js +272 -0
  32. package/dist/cjs/validations/accumulative-token.js.map +1 -0
  33. package/dist/cjs/validations/compensation-hook.js +9 -0
  34. package/dist/cjs/validations/compensation-hook.js.map +1 -1
  35. package/dist/cjs/validations/cost-token.js +56 -0
  36. package/dist/cjs/validations/cost-token.js.map +1 -0
  37. package/dist/cjs/validations/dln-metadata.js +63 -0
  38. package/dist/cjs/validations/dln-metadata.js.map +1 -0
  39. package/dist/cjs/validations/evm-caveats.js +5 -3
  40. package/dist/cjs/validations/evm-caveats.js.map +1 -1
  41. package/dist/cjs/validations/evm-op-budget.js +4 -4
  42. package/dist/cjs/validations/evm-op-budget.js.map +1 -1
  43. package/dist/cjs/validations/evm-op-hook.js +7 -1
  44. package/dist/cjs/validations/evm-op-hook.js.map +1 -1
  45. package/dist/cjs/validations/evm-op-wrap.js +6 -5
  46. package/dist/cjs/validations/evm-op-wrap.js.map +1 -1
  47. package/dist/cjs/validations/evm-permit2.js +7 -4
  48. package/dist/cjs/validations/evm-permit2.js.map +1 -1
  49. package/dist/cjs/validations/evm-required-actions.js +7 -4
  50. package/dist/cjs/validations/evm-required-actions.js.map +1 -1
  51. package/dist/cjs/validations/external-call.js +330 -0
  52. package/dist/cjs/validations/external-call.js.map +1 -0
  53. package/dist/cjs/validations/hooks-common.js +38 -2
  54. package/dist/cjs/validations/hooks-common.js.map +1 -1
  55. package/dist/cjs/validations/intent-tokens.js +54 -40
  56. package/dist/cjs/validations/intent-tokens.js.map +1 -1
  57. package/dist/cjs/validations/intent.js +192 -71
  58. package/dist/cjs/validations/intent.js.map +1 -1
  59. package/dist/cjs/validations/ptp.js +157 -0
  60. package/dist/cjs/validations/ptp.js.map +1 -0
  61. package/dist/cjs/validations/required-actions.js +2 -2
  62. package/dist/cjs/validations/required-actions.js.map +1 -1
  63. package/dist/cjs/validations/solana-hook.js +12 -0
  64. package/dist/cjs/validations/solana-hook.js.map +1 -1
  65. package/dist/cjs/validations/solana-op-budget.js +20 -40
  66. package/dist/cjs/validations/solana-op-budget.js.map +1 -1
  67. package/dist/cjs/validations/solana-op-wrap.js +27 -32
  68. package/dist/cjs/validations/solana-op-wrap.js.map +1 -1
  69. package/dist/cjs/validations/total-dst-amount.js +432 -0
  70. package/dist/cjs/validations/total-dst-amount.js.map +1 -0
  71. package/dist/cjs/validations/trade-grouping.js +67 -25
  72. package/dist/cjs/validations/trade-grouping.js.map +1 -1
  73. package/dist/cjs/validations/trade-matching.js +112 -0
  74. package/dist/cjs/validations/trade-matching.js.map +1 -0
  75. package/dist/cjs/validations/trades.js +365 -71
  76. package/dist/cjs/validations/trades.js.map +1 -1
  77. package/dist/cjs/validator.js +22 -4
  78. package/dist/cjs/validator.js.map +1 -1
  79. package/dist/cjs/whitelists/environment-config.js +5 -5
  80. package/dist/cjs/whitelists/environment-config.js.map +1 -1
  81. package/dist/esm/helpers/evm-dln-hook-pack.js +8 -6
  82. package/dist/esm/helpers/evm-dln-hook-pack.js.map +1 -1
  83. package/dist/esm/helpers/evm-gas-compensation-tx.js +2 -1
  84. package/dist/esm/helpers/evm-gas-compensation-tx.js.map +1 -1
  85. package/dist/esm/helpers/evm-intent-id.js +6 -5
  86. package/dist/esm/helpers/evm-intent-id.js.map +1 -1
  87. package/dist/esm/helpers/evm-trade-amounts.js +26 -44
  88. package/dist/esm/helpers/evm-trade-amounts.js.map +1 -1
  89. package/dist/esm/helpers/expected-delegate.js +10 -11
  90. package/dist/esm/helpers/expected-delegate.js.map +1 -1
  91. package/dist/esm/helpers/gas-compensation.js +3 -0
  92. package/dist/esm/helpers/gas-compensation.js.map +1 -1
  93. package/dist/esm/helpers/hex.js +29 -0
  94. package/dist/esm/helpers/hex.js.map +1 -1
  95. package/dist/esm/helpers/integer.js +7 -0
  96. package/dist/esm/helpers/integer.js.map +1 -0
  97. package/dist/esm/helpers/placeholder.js +3 -2
  98. package/dist/esm/helpers/placeholder.js.map +1 -1
  99. package/dist/esm/helpers/solana-intent-id.js +6 -5
  100. package/dist/esm/helpers/solana-intent-id.js.map +1 -1
  101. package/dist/esm/helpers/synthetic-native-wrap.js +52 -0
  102. package/dist/esm/helpers/synthetic-native-wrap.js.map +1 -0
  103. package/dist/esm/types/enums.js +2 -0
  104. package/dist/esm/types/enums.js.map +1 -1
  105. package/dist/esm/types/request.js +25 -4
  106. package/dist/esm/types/request.js.map +1 -1
  107. package/dist/esm/types/response.js +11 -3
  108. package/dist/esm/types/response.js.map +1 -1
  109. package/dist/esm/types/validation.js +40 -1
  110. package/dist/esm/types/validation.js.map +1 -1
  111. package/dist/esm/validations/accumulative-token.js +269 -0
  112. package/dist/esm/validations/accumulative-token.js.map +1 -0
  113. package/dist/esm/validations/compensation-hook.js +9 -0
  114. package/dist/esm/validations/compensation-hook.js.map +1 -1
  115. package/dist/esm/validations/cost-token.js +53 -0
  116. package/dist/esm/validations/cost-token.js.map +1 -0
  117. package/dist/esm/validations/dln-metadata.js +56 -0
  118. package/dist/esm/validations/dln-metadata.js.map +1 -0
  119. package/dist/esm/validations/evm-caveats.js +5 -3
  120. package/dist/esm/validations/evm-caveats.js.map +1 -1
  121. package/dist/esm/validations/evm-op-budget.js +4 -4
  122. package/dist/esm/validations/evm-op-budget.js.map +1 -1
  123. package/dist/esm/validations/evm-op-hook.js +7 -1
  124. package/dist/esm/validations/evm-op-hook.js.map +1 -1
  125. package/dist/esm/validations/evm-op-wrap.js +6 -5
  126. package/dist/esm/validations/evm-op-wrap.js.map +1 -1
  127. package/dist/esm/validations/evm-permit2.js +7 -4
  128. package/dist/esm/validations/evm-permit2.js.map +1 -1
  129. package/dist/esm/validations/evm-required-actions.js +7 -4
  130. package/dist/esm/validations/evm-required-actions.js.map +1 -1
  131. package/dist/esm/validations/external-call.js +325 -0
  132. package/dist/esm/validations/external-call.js.map +1 -0
  133. package/dist/esm/validations/hooks-common.js +38 -2
  134. package/dist/esm/validations/hooks-common.js.map +1 -1
  135. package/dist/esm/validations/intent-tokens.js +55 -41
  136. package/dist/esm/validations/intent-tokens.js.map +1 -1
  137. package/dist/esm/validations/intent.js +194 -73
  138. package/dist/esm/validations/intent.js.map +1 -1
  139. package/dist/esm/validations/ptp.js +154 -0
  140. package/dist/esm/validations/ptp.js.map +1 -0
  141. package/dist/esm/validations/required-actions.js +2 -2
  142. package/dist/esm/validations/required-actions.js.map +1 -1
  143. package/dist/esm/validations/solana-hook.js +13 -1
  144. package/dist/esm/validations/solana-hook.js.map +1 -1
  145. package/dist/esm/validations/solana-op-budget.js +21 -41
  146. package/dist/esm/validations/solana-op-budget.js.map +1 -1
  147. package/dist/esm/validations/solana-op-wrap.js +28 -33
  148. package/dist/esm/validations/solana-op-wrap.js.map +1 -1
  149. package/dist/esm/validations/total-dst-amount.js +429 -0
  150. package/dist/esm/validations/total-dst-amount.js.map +1 -0
  151. package/dist/esm/validations/trade-grouping.js +67 -25
  152. package/dist/esm/validations/trade-grouping.js.map +1 -1
  153. package/dist/esm/validations/trade-matching.js +109 -0
  154. package/dist/esm/validations/trade-matching.js.map +1 -0
  155. package/dist/esm/validations/trades.js +365 -71
  156. package/dist/esm/validations/trades.js.map +1 -1
  157. package/dist/esm/validator.js +22 -4
  158. package/dist/esm/validator.js.map +1 -1
  159. package/dist/esm/whitelists/environment-config.js +5 -5
  160. package/dist/esm/whitelists/environment-config.js.map +1 -1
  161. package/dist/types/helpers/evm-dln-hook-pack.d.ts.map +1 -1
  162. package/dist/types/helpers/evm-gas-compensation-tx.d.ts.map +1 -1
  163. package/dist/types/helpers/evm-intent-id.d.ts.map +1 -1
  164. package/dist/types/helpers/evm-trade-amounts.d.ts +1 -1
  165. package/dist/types/helpers/evm-trade-amounts.d.ts.map +1 -1
  166. package/dist/types/helpers/expected-delegate.d.ts.map +1 -1
  167. package/dist/types/helpers/gas-compensation.d.ts.map +1 -1
  168. package/dist/types/helpers/hex.d.ts +6 -0
  169. package/dist/types/helpers/hex.d.ts.map +1 -1
  170. package/dist/types/helpers/integer.d.ts +2 -0
  171. package/dist/types/helpers/integer.d.ts.map +1 -0
  172. package/dist/types/helpers/placeholder.d.ts.map +1 -1
  173. package/dist/types/helpers/solana-intent-id.d.ts.map +1 -1
  174. package/dist/types/helpers/synthetic-native-wrap.d.ts +4 -0
  175. package/dist/types/helpers/synthetic-native-wrap.d.ts.map +1 -0
  176. package/dist/types/types/enums.d.ts +4 -2
  177. package/dist/types/types/enums.d.ts.map +1 -1
  178. package/dist/types/types/request.d.ts +85 -6
  179. package/dist/types/types/request.d.ts.map +1 -1
  180. package/dist/types/types/response.d.ts +96 -12
  181. package/dist/types/types/response.d.ts.map +1 -1
  182. package/dist/types/types/validation.d.ts +36 -1
  183. package/dist/types/types/validation.d.ts.map +1 -1
  184. package/dist/types/validations/accumulative-token.d.ts +3 -0
  185. package/dist/types/validations/accumulative-token.d.ts.map +1 -0
  186. package/dist/types/validations/compensation-hook.d.ts.map +1 -1
  187. package/dist/types/validations/cost-token.d.ts +3 -0
  188. package/dist/types/validations/cost-token.d.ts.map +1 -0
  189. package/dist/types/validations/dln-metadata.d.ts +7 -0
  190. package/dist/types/validations/dln-metadata.d.ts.map +1 -0
  191. package/dist/types/validations/evm-caveats.d.ts.map +1 -1
  192. package/dist/types/validations/evm-op-hook.d.ts.map +1 -1
  193. package/dist/types/validations/evm-op-wrap.d.ts.map +1 -1
  194. package/dist/types/validations/evm-permit2.d.ts.map +1 -1
  195. package/dist/types/validations/evm-required-actions.d.ts +1 -1
  196. package/dist/types/validations/evm-required-actions.d.ts.map +1 -1
  197. package/dist/types/validations/external-call.d.ts +15 -0
  198. package/dist/types/validations/external-call.d.ts.map +1 -0
  199. package/dist/types/validations/intent-tokens.d.ts.map +1 -1
  200. package/dist/types/validations/intent.d.ts +1 -1
  201. package/dist/types/validations/intent.d.ts.map +1 -1
  202. package/dist/types/validations/ptp.d.ts +3 -0
  203. package/dist/types/validations/ptp.d.ts.map +1 -0
  204. package/dist/types/validations/required-actions.d.ts +1 -1
  205. package/dist/types/validations/required-actions.d.ts.map +1 -1
  206. package/dist/types/validations/solana-hook.d.ts.map +1 -1
  207. package/dist/types/validations/solana-op-budget.d.ts.map +1 -1
  208. package/dist/types/validations/solana-op-wrap.d.ts.map +1 -1
  209. package/dist/types/validations/total-dst-amount.d.ts +3 -0
  210. package/dist/types/validations/total-dst-amount.d.ts.map +1 -0
  211. package/dist/types/validations/trade-grouping.d.ts +2 -1
  212. package/dist/types/validations/trade-grouping.d.ts.map +1 -1
  213. package/dist/types/validations/trade-matching.d.ts +13 -0
  214. package/dist/types/validations/trade-matching.d.ts.map +1 -0
  215. package/dist/types/validations/trades.d.ts +2 -1
  216. package/dist/types/validations/trades.d.ts.map +1 -1
  217. package/dist/types/validator.d.ts.map +1 -1
  218. package/dist/types/whitelists/environment-config.d.ts.map +1 -1
  219. 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
- function validateTrades(request, response, quoteMode) {
9
- // CRITICAL: trade counts must match 1:1
10
- if (request.trades.length !== response.trades.length) {
11
- return [{
12
- code: types_1.ViolationCode.TradeCountMismatch,
13
- message: "response.trades.length does not match request.trades.length",
14
- path: "response.trades",
15
- expected: request.trades.length,
16
- actual: response.trades.length,
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
- for (let i = 0; i < request.trades.length; i++) {
21
- violations.push(...validateSingleTrade(request.trades[i], response.trades[i], i, quoteMode));
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
- violations.push(...validatePartnerCost(request, response));
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
- function validateSingleTrade(reqTrade, resTrade, tradeIndex, quoteMode) {
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[${tradeIndex}]`;
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[${tradeIndex}].dstChainId`,
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 amount is numeric
80
- violations.push(...validateSrcTokenInAmount(reqTrade, src, path));
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
- violations.push(...validateMutatedWithOperatingExpense(reqTrade, src, path));
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 = BigInt(amount);
115
- const actual = BigInt(src.amount);
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 = BigInt(src.approximateOperatingExpense);
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); `BigInt(undefined)` would
191
- // throw and produce a misleading `TradePrependedCostInvalid`.
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 (BigInt(resTrade.prependedOperatingExpenseCost) !== 0n) {
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
- const prependExpenses = reqTrade.prependOperatingExpenses ?? false;
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 (BigInt(resTrade.srcChainTokenOut.amount) < 0n) {
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, dst, path));
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 = BigInt(reqTrade.dstChainTokenOutAmount);
340
- const actual = BigInt(dst.amount);
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, dst, path) {
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 = BigInt(dst.withoutPartnerFeeBpsAmount);
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 = BigInt(dst.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.srcChainTokenInAmount === types_1.SpecialAmount.Auto
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
- // Only check: when prependOperatingExpenses is false AND not auto/max → must be zero
492
- if (prependExpenses || isAuto || isMax)
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 = BigInt(resTrade.prependedOperatingExpenseCost);
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, `feeBps` is taken from the first per-trade
532
- // entry — which is always trade index 0 since the API preserves request order.
533
- function validatePartnerCost(request, response) {
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 = BigInt(partnerCost.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
- // CRITICAL: when at least one trade has a partner fee, the aggregated amount must be > 0.
588
- if (anyNonZero && amount === 0n) {
589
- violations.push({
590
- code: types_1.ViolationCode.PartnerCostAmountUnexpectedlyZero,
591
- message: "PARTNER_COST.amount must be > 0 when at least one trade has partnerFeeBps > 0",
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 request.trades[0].partnerFeeBps",
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,