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