@gibs/bridge-sdk 1.13.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/README.md +244 -18
  2. package/abis/package.json +6 -0
  3. package/bridge-limits/package.json +6 -0
  4. package/chain-info/package.json +6 -0
  5. package/chain-names/package.json +6 -0
  6. package/chains/package.json +6 -0
  7. package/claims/package.json +6 -0
  8. package/config/package.json +6 -0
  9. package/consolidated-quote/package.json +6 -0
  10. package/destination-calldata/package.json +6 -0
  11. package/dist/chain-info.d.ts +1 -1
  12. package/dist/chain-info.js +11 -12
  13. package/dist/chains.d.ts +25 -0
  14. package/dist/chains.js +55 -20
  15. package/dist/cjs/abis.d.ts +1 -0
  16. package/dist/cjs/abis.js +19 -0
  17. package/dist/cjs/bridge-limits.d.ts +103 -0
  18. package/dist/cjs/bridge-limits.js +89 -0
  19. package/dist/cjs/chain-info.d.ts +169 -0
  20. package/dist/cjs/chain-info.js +452 -0
  21. package/dist/cjs/chain-names.d.ts +160 -0
  22. package/dist/cjs/chain-names.js +207 -0
  23. package/dist/cjs/chains.d.ts +90 -0
  24. package/dist/cjs/chains.js +357 -0
  25. package/dist/cjs/claims.d.ts +198 -0
  26. package/dist/cjs/claims.js +182 -0
  27. package/dist/cjs/config.d.ts +240 -0
  28. package/dist/cjs/config.js +537 -0
  29. package/dist/cjs/consolidated-quote.d.ts +648 -0
  30. package/dist/cjs/consolidated-quote.js +1048 -0
  31. package/dist/cjs/destination-calldata.d.ts +118 -0
  32. package/dist/cjs/destination-calldata.js +156 -0
  33. package/dist/cjs/ecosystems.d.ts +217 -0
  34. package/dist/cjs/ecosystems.js +298 -0
  35. package/dist/cjs/execution.d.ts +346 -0
  36. package/dist/cjs/execution.js +468 -0
  37. package/dist/cjs/fee-type.d.ts +10 -0
  38. package/dist/cjs/fee-type.js +10 -0
  39. package/dist/cjs/hyperliquid-execution.d.ts +152 -0
  40. package/dist/cjs/hyperliquid-execution.js +175 -0
  41. package/dist/cjs/image-links.d.ts +117 -0
  42. package/dist/cjs/image-links.js +153 -0
  43. package/dist/cjs/index.d.ts +23 -0
  44. package/dist/cjs/index.js +39 -0
  45. package/dist/cjs/lifi-execution-guard.d.ts +210 -0
  46. package/dist/cjs/lifi-execution-guard.js +409 -0
  47. package/dist/cjs/near-intents-conformance.d.ts +85 -0
  48. package/dist/cjs/near-intents-conformance.js +337 -0
  49. package/dist/cjs/near-intents-route.d.ts +47 -0
  50. package/dist/cjs/near-intents-route.js +303 -0
  51. package/dist/cjs/near-intents.d.ts +524 -0
  52. package/dist/cjs/near-intents.js +1071 -0
  53. package/dist/cjs/origin-deposit.d.ts +326 -0
  54. package/dist/cjs/origin-deposit.js +82 -0
  55. package/dist/cjs/package.json +3 -0
  56. package/dist/cjs/pre-quote.d.ts +198 -0
  57. package/dist/cjs/pre-quote.js +180 -0
  58. package/dist/cjs/prices.d.ts +107 -0
  59. package/dist/cjs/prices.js +172 -0
  60. package/dist/cjs/providers.d.ts +238 -0
  61. package/dist/cjs/providers.js +171 -0
  62. package/dist/cjs/quote-fees.d.ts +102 -0
  63. package/dist/cjs/quote-fees.js +201 -0
  64. package/dist/cjs/referral.d.ts +85 -0
  65. package/dist/cjs/referral.js +85 -0
  66. package/dist/cjs/relay-currencies.d.ts +47 -0
  67. package/dist/cjs/relay-currencies.js +95 -0
  68. package/dist/cjs/relay-depository.d.ts +2 -0
  69. package/dist/cjs/relay-depository.js +85 -0
  70. package/dist/cjs/relay-quote.d.ts +301 -0
  71. package/dist/cjs/relay-quote.js +996 -0
  72. package/dist/cjs/route-steps.d.ts +138 -0
  73. package/dist/cjs/route-steps.js +310 -0
  74. package/dist/cjs/routing/amounts.d.ts +80 -0
  75. package/dist/cjs/routing/amounts.js +126 -0
  76. package/dist/cjs/routing/candidate-route.d.ts +43 -0
  77. package/dist/cjs/routing/candidate-route.js +50 -0
  78. package/dist/cjs/routing/candidate.d.ts +237 -0
  79. package/dist/cjs/routing/candidate.js +100 -0
  80. package/dist/cjs/routing/carriers/ask-answer.d.ts +77 -0
  81. package/dist/cjs/routing/carriers/ask-answer.js +2 -0
  82. package/dist/cjs/routing/carriers/omnibridge.d.ts +137 -0
  83. package/dist/cjs/routing/carriers/omnibridge.js +459 -0
  84. package/dist/cjs/routing/carriers/pulsex-paths.d.ts +80 -0
  85. package/dist/cjs/routing/carriers/pulsex-paths.js +97 -0
  86. package/dist/cjs/routing/carriers/pulsex-split.d.ts +175 -0
  87. package/dist/cjs/routing/carriers/pulsex-split.js +193 -0
  88. package/dist/cjs/routing/carriers/pulsex.d.ts +159 -0
  89. package/dist/cjs/routing/carriers/pulsex.js +565 -0
  90. package/dist/cjs/routing/carriers/wrap.d.ts +25 -0
  91. package/dist/cjs/routing/carriers/wrap.js +119 -0
  92. package/dist/cjs/routing/composed-route-quality.d.ts +125 -0
  93. package/dist/cjs/routing/composed-route-quality.js +144 -0
  94. package/dist/cjs/routing/entry-chain-anchor.d.ts +88 -0
  95. package/dist/cjs/routing/entry-chain-anchor.js +59 -0
  96. package/dist/cjs/routing/format.d.ts +31 -0
  97. package/dist/cjs/routing/format.js +64 -0
  98. package/dist/cjs/routing/index.d.ts +38 -0
  99. package/dist/cjs/routing/index.js +134 -0
  100. package/dist/cjs/routing/producers.d.ts +770 -0
  101. package/dist/cjs/routing/producers.js +1617 -0
  102. package/dist/cjs/routing/providers.d.ts +31 -0
  103. package/dist/cjs/routing/providers.js +48 -0
  104. package/dist/cjs/routing/route-data.d.ts +355 -0
  105. package/dist/cjs/routing/route-data.js +2 -0
  106. package/dist/cjs/routing/search/compose.d.ts +168 -0
  107. package/dist/cjs/routing/search/compose.js +104 -0
  108. package/dist/cjs/routing/search/graph.d.ts +668 -0
  109. package/dist/cjs/routing/search/graph.js +461 -0
  110. package/dist/cjs/routing/search/plan-route-data.d.ts +269 -0
  111. package/dist/cjs/routing/search/plan-route-data.js +848 -0
  112. package/dist/cjs/routing/search/score.d.ts +356 -0
  113. package/dist/cjs/routing/search/score.js +445 -0
  114. package/dist/cjs/routing/swap-transactions.d.ts +91 -0
  115. package/dist/cjs/routing/swap-transactions.js +131 -0
  116. package/dist/cjs/routing/usd-value.d.ts +17 -0
  117. package/dist/cjs/routing/usd-value.js +29 -0
  118. package/dist/cjs/settings.d.ts +785 -0
  119. package/dist/cjs/settings.js +1074 -0
  120. package/dist/cjs/ton-connect.d.ts +98 -0
  121. package/dist/cjs/ton-connect.js +206 -0
  122. package/dist/cjs/ton-execution.d.ts +7 -0
  123. package/dist/cjs/ton-execution.js +23 -0
  124. package/dist/cjs/ton.d.ts +37 -0
  125. package/dist/cjs/ton.js +83 -0
  126. package/dist/cjs/tron-execution.d.ts +146 -0
  127. package/dist/cjs/tron-execution.js +248 -0
  128. package/dist/cjs/types.d.ts +51 -0
  129. package/dist/cjs/types.js +2 -0
  130. package/dist/claims.d.ts +198 -0
  131. package/dist/claims.js +140 -0
  132. package/dist/config.d.ts +24 -0
  133. package/dist/config.js +39 -5
  134. package/dist/consolidated-quote.d.ts +109 -15
  135. package/dist/consolidated-quote.js +310 -52
  136. package/dist/execution.d.ts +346 -0
  137. package/dist/execution.js +428 -0
  138. package/dist/hyperliquid-execution.js +2 -3
  139. package/dist/index.d.ts +3 -0
  140. package/dist/index.js +3 -0
  141. package/dist/lifi-execution-guard.d.ts +13 -2
  142. package/dist/lifi-execution-guard.js +4 -0
  143. package/dist/near-intents.js +2 -3
  144. package/dist/pre-quote.d.ts +6 -6
  145. package/dist/pre-quote.js +10 -14
  146. package/dist/providers.d.ts +17 -0
  147. package/dist/providers.js +6 -0
  148. package/dist/referral.d.ts +12 -4
  149. package/dist/referral.js +12 -4
  150. package/dist/relay-depository.d.ts +2 -0
  151. package/dist/relay-depository.js +82 -0
  152. package/dist/relay-quote.js +2 -3
  153. package/dist/routing/candidate.d.ts +3 -3
  154. package/dist/routing/candidate.js +3 -3
  155. package/dist/routing/carriers/ask-answer.d.ts +19 -0
  156. package/dist/routing/carriers/omnibridge.js +23 -21
  157. package/dist/routing/carriers/pulsex-paths.d.ts +80 -0
  158. package/dist/routing/carriers/pulsex-paths.js +92 -0
  159. package/dist/routing/carriers/pulsex-split.d.ts +175 -0
  160. package/dist/routing/carriers/pulsex-split.js +185 -0
  161. package/dist/routing/carriers/pulsex.d.ts +69 -21
  162. package/dist/routing/carriers/pulsex.js +184 -65
  163. package/dist/routing/composed-route-quality.d.ts +28 -6
  164. package/dist/routing/composed-route-quality.js +36 -5
  165. package/dist/routing/entry-chain-anchor.d.ts +1 -1
  166. package/dist/routing/entry-chain-anchor.js +1 -1
  167. package/dist/routing/index.d.ts +7 -3
  168. package/dist/routing/index.js +7 -3
  169. package/dist/routing/producers.js +1 -1
  170. package/dist/routing/providers.d.ts +5 -0
  171. package/dist/routing/providers.js +5 -0
  172. package/dist/routing/search/graph.d.ts +29 -0
  173. package/dist/routing/search/plan-route-data.d.ts +25 -0
  174. package/dist/routing/search/plan-route-data.js +43 -7
  175. package/dist/routing/search/score.d.ts +33 -1
  176. package/dist/routing/search/score.js +41 -6
  177. package/dist/routing/swap-transactions.d.ts +91 -0
  178. package/dist/routing/swap-transactions.js +125 -0
  179. package/dist/settings.d.ts +94 -1
  180. package/dist/settings.js +95 -3
  181. package/dist/ton-connect.d.ts +98 -0
  182. package/dist/ton-connect.js +200 -0
  183. package/dist/ton-execution.d.ts +4 -114
  184. package/dist/ton-execution.js +4 -244
  185. package/dist/ton.d.ts +37 -0
  186. package/dist/ton.js +65 -0
  187. package/docs/examples/example-a-ether-to-pulse.ts +300 -0
  188. package/docs/examples/example-b-token-pulsechain-to-ethereum.ts +270 -0
  189. package/docs/examples/shared.ts +261 -0
  190. package/docs/examples/tsconfig.json +18 -0
  191. package/docs/integration-guide.md +617 -0
  192. package/ecosystems/package.json +6 -0
  193. package/execution/package.json +6 -0
  194. package/fee-type/package.json +6 -0
  195. package/hyperliquid-execution/package.json +6 -0
  196. package/image-links/package.json +6 -0
  197. package/lifi-execution-guard/package.json +6 -0
  198. package/llms.txt +234 -0
  199. package/near-intents/package.json +6 -0
  200. package/near-intents-route/package.json +6 -0
  201. package/origin-deposit/package.json +6 -0
  202. package/package.json +818 -10
  203. package/pre-quote/package.json +6 -0
  204. package/prices/package.json +6 -0
  205. package/providers/package.json +6 -0
  206. package/quote-fees/package.json +6 -0
  207. package/referral/package.json +6 -0
  208. package/relay-currencies/package.json +6 -0
  209. package/relay-quote/package.json +6 -0
  210. package/route-steps/package.json +6 -0
  211. package/routing/package.json +6 -0
  212. package/settings/package.json +6 -0
  213. package/ton/package.json +6 -0
  214. package/ton-connect/package.json +6 -0
  215. package/ton-execution/package.json +6 -0
  216. package/tron-execution/package.json +6 -0
  217. package/types/package.json +6 -0
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createWrapCarrier = void 0;
4
+ const viem_1 = require("viem");
5
+ const config_js_1 = require("../../config.js");
6
+ const graph_js_1 = require("../search/graph.js");
7
+ /**
8
+ * Native-coin wrap/unwrap (PLS <-> WPLS on PulseChain, and the same pair on
9
+ * every other Ethereum-Virtual-Machine chain the search graph touches — ETH
10
+ * <-> WETH, BNB <-> WBNB) as a plug-in `Carrier` for `@gibs/quotes`'s
11
+ * route-search engine. GIBS-SPECIFIC in the sense `omnibridge.ts`/`pulsex.ts`
12
+ * are (see `omnibridge.ts`'s head doc for why this returns a plain object
13
+ * shaped like `Carrier` rather than importing that type) — though the
14
+ * carrier itself names no chain: it derives every wrap pair from
15
+ * `config.ts`'s own `nativeAssetOut` map, so a chain added there gains a
16
+ * wrap edge with no change here.
17
+ *
18
+ * WHY THIS IS A SEPARATE CARRIER FROM PulseX. `pulsex.ts`'s own
19
+ * `readPulseXAmountOut` (via `pulseXSwapPathForEdge` in
20
+ * `packages/ui/src/lib/state/useRouteSearch.ts`) quotes native Pulse AS
21
+ * Wrapped Pulse — both map to the same `quotePathAddress` — so a PLS<->WPLS
22
+ * pair collapses to "no swap to quote" and PulseX has never carried it.
23
+ * There is no PulseX POOL for native-versus-wrapped; it is a 1:1 contract
24
+ * call (`deposit()`/`withdraw(uint256)`), not a trade, so it needed its own
25
+ * carrier rather than a PulseX special case.
26
+ *
27
+ * SENDER: `'swap'`, THE SAME NAME `pulsex.ts` USES — the reader's own
28
+ * connected wallet signs a `deposit()`/`withdraw(uint256)` call on the
29
+ * wrapped token's own contract (WETH9-style; confirmed present on WPLS by
30
+ * bytecode selector 2026-09-24), exactly the wallet a same-chain PulseX swap
31
+ * already uses. WHETHER `settings.ts`'s EXISTING swap-transaction builder
32
+ * (`pulseXSwapTransactionInputs`) CAN BUILD THIS CALL: NO, not without a
33
+ * change there — that builder only ever targets a PulseX ROUTER address and
34
+ * encodes `swapExact*` calldata; a wrap needs `to` = the wrapped token
35
+ * itself and `data` = plain `deposit()`/`withdraw(amount)` calldata, which
36
+ * is a different (and simpler) shape. Sharing the `'swap'` sender name says
37
+ * "the same wallet, the same signing step in the flow" — it does not claim
38
+ * the existing builder already emits this calldata. Building that call is
39
+ * `settings.ts`'s job, out of this carrier's scope; this module answers only
40
+ * the route-search question ("what does this edge deliver, exactly").
41
+ */
42
+ /**
43
+ * Whether this carrier can wrap/unwrap directly between `from` and `to`:
44
+ * same Ethereum-Virtual-Machine chain, and the two addresses are EXACTLY the
45
+ * zero-address/wrapped-native pair `config.ts`'s `nativeAssetOut` names for
46
+ * that chain — nothing else, so a plain token pair that merely happens to
47
+ * share a chain is never mistaken for a wrap.
48
+ */
49
+ const wrapCarries = (from, to) => {
50
+ if (from.ecosystem !== 'evm' || to.ecosystem !== 'evm')
51
+ return false;
52
+ if (from.chainId !== to.chainId)
53
+ return false;
54
+ const wrappedNativeAddress = config_js_1.nativeAssetOut[(0, config_js_1.toChain)(from.chainId)];
55
+ if (wrappedNativeAddress === undefined)
56
+ return false;
57
+ const wrapped = (0, viem_1.getAddress)(wrappedNativeAddress).toLowerCase();
58
+ const fromAddress = from.address.toLowerCase();
59
+ const toAddress = to.address.toLowerCase();
60
+ const fromIsNative = fromAddress === viem_1.zeroAddress;
61
+ const toIsNative = toAddress === viem_1.zeroAddress;
62
+ if (fromIsNative)
63
+ return toAddress === wrapped;
64
+ if (toIsNative)
65
+ return fromAddress === wrapped;
66
+ return false;
67
+ };
68
+ /** Every wrap/unwrap edge is worth exactly `amountIn`, in `edge.to`'s base units — native and wrapped share the same decimals on every chain this carrier has ever seen (18, both sides, everywhere `nativeAssetOut` names). */
69
+ const wrapExact = (edge, amountIn) => {
70
+ if (!wrapCarries(edge.from, edge.to))
71
+ return null;
72
+ return { outcome: 'delivered', deliveredAmount: amountIn };
73
+ };
74
+ /**
75
+ * Builds the native wrap/unwrap carrier as a plug-in.
76
+ *
77
+ * NO OPTIONS, NO `prepare`. There is no local read to batch — a wrap has no
78
+ * minimum, no maximum, no fee rate and no reserve to be wrong about; it is a
79
+ * pure 1:1 identity over every chain `config.ts` names a wrapped-native
80
+ * token for. `exact` alone is total over every edge this carrier offers.
81
+ *
82
+ * @returns the carrier — `id: 'wrap'`
83
+ */
84
+ const createWrapCarrier = () => {
85
+ const edgesFrom = (from, to) => {
86
+ if ((0, graph_js_1.assetNodeKey)(from) === (0, graph_js_1.assetNodeKey)(to))
87
+ return [];
88
+ if (!wrapCarries(from, to))
89
+ return [];
90
+ return [{ carrier: 'wrap', from, to, fusesNext: false, origin: 'wallet', floors: [] }];
91
+ };
92
+ const ask = async ({ edge, inputAmount }) => {
93
+ const result = wrapExact(edge, inputAmount);
94
+ if (result === null || result.outcome === 'refused') {
95
+ return { ok: false, refusal: { kind: 'cannot-carry', reason: 'not a native/wrapped pair', minimumAmount: null } };
96
+ }
97
+ return {
98
+ ok: true,
99
+ deliveredAmount: result.deliveredAmount,
100
+ // 1:1, always — there is no slippage to guarantee against.
101
+ minimumDeliveredAmount: result.deliveredAmount,
102
+ claimedDurationSeconds: 0,
103
+ };
104
+ };
105
+ return {
106
+ id: 'wrap',
107
+ edgesFrom,
108
+ ask,
109
+ exact: wrapExact,
110
+ isRateLimited: false,
111
+ // A `deposit()`/`withdraw()` call requires the reader's own wallet
112
+ // signature — never a by-hand deposit.
113
+ depositMode: 'wallet-signed',
114
+ sender: 'swap',
115
+ // A same-chain call settles with the transaction itself.
116
+ settlementSeconds: 0,
117
+ };
118
+ };
119
+ exports.createWrapCarrier = createWrapCarrier;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Whether a composed multi-signature route is good enough to offer at all.
3
+ *
4
+ * WHY A ROUTE NEEDS A GATE THAT A SINGLE CROSSING DOES NOT. A one-signature
5
+ * route that turns out badly costs the reader one transaction and they still
6
+ * hold something they chose. A composed route — an aggregator leg, then an
7
+ * omnibridge crossing, then a swap — strands them between signatures: after
8
+ * the first they hold an anchor token on a chain they did not ask for, and the
9
+ * only way out is to pay again. So a cheap first leg followed by a bad swap is
10
+ * worse than the route it replaced, and the difference has to be found BEFORE
11
+ * the route is listed rather than after the reader has signed.
12
+ *
13
+ * NOTHING HERE COSTS A REQUEST. Both figures the verdict reads are already in
14
+ * hand when a composer runs: the swap's price impact comes from the on-chain
15
+ * `getAmountsOut` pair the estimate already fetched, and the two dollar values
16
+ * come from the route's own basis. The gate is free, which is why it can run
17
+ * on every route rather than only on suspicious ones.
18
+ */
19
+ /**
20
+ * The price impact above which a composed route is withdrawn, in percent.
21
+ *
22
+ * MEASURED RATHER THAN CHOSEN. Across the anchor and size combinations swept
23
+ * on 2026-09-21 — bridged wrapped ether, USD Coin, DAI and USDT into wrapped
24
+ * PulseChain coin, at roughly forty and one hundred and forty United States
25
+ * dollars — the impact ran from 0.008 percent to 0.037 percent, with bridged
26
+ * USDT the one outlier at 0.34 to 0.37 percent. Three percent is two orders of
27
+ * magnitude above the normal case and an order above the worst observed, so it
28
+ * cannot fire on an ordinary route; and it is still far below the point where
29
+ * a reader is quietly losing real money on a leg they cannot undo.
30
+ *
31
+ * See `docs/near-intents-conversion-composition.md` for the full sweep.
32
+ */
33
+ export declare const COMPOSED_ROUTE_MAX_PRICE_IMPACT_PERCENT = 3;
34
+ /**
35
+ * The end-to-end value loss above which a composed route is withdrawn, in
36
+ * percent of what the reader pays.
37
+ *
38
+ * A SECOND TEST BECAUSE THE FIRST CAN READ FINE ON A BROKEN POOL. Price impact
39
+ * is measured against a marginal reference quote taken from the same pool, so
40
+ * a pool that is uniformly mispriced — a stale pair, a pair holding almost
41
+ * nothing — reports a small impact while paying out far less than the tokens
42
+ * going in are worth. Only comparing the two ends of the journey catches that.
43
+ *
44
+ * Measured whole-journey loss on the same sweep ran from 0.81 to 1.36 percent,
45
+ * of which the swap fee alone is 0.29. Five percent leaves every observed
46
+ * route untouched and still stops a route that is quietly eating a twentieth
47
+ * of the reader's money.
48
+ */
49
+ export declare const COMPOSED_ROUTE_MAX_VALUE_LOSS_PERCENT = 5;
50
+ /**
51
+ * The end-to-end value GAIN above which a composed route is withdrawn, in
52
+ * percent of what the reader pays.
53
+ *
54
+ * A ROUTE CANNOT MAKE MONEY. Every leg charges a fee, so a route that
55
+ * delivers more value than it takes says one of its figures is wrong. Only a
56
+ * disagreement between the two price feeds can make a gain look real, and the
57
+ * value-loss ceiling already trusts those feeds to within the same five
58
+ * percent. So the bound is symmetric with
59
+ * {@link COMPOSED_ROUTE_MAX_VALUE_LOSS_PERCENT}.
60
+ *
61
+ * THE DEFECT THIS STOPS. On 2026-10-03 the engine served 0.0002 BNB to HEX at
62
+ * 495,256 HEX, about seven thousand times the rate of the next tier. The
63
+ * value-loss test read that as a loss of minus seven hundred thousand percent
64
+ * and passed it. A PulseX hop rescaled from an upstream live answer keeps the
65
+ * price impact measured at its original, smaller input
66
+ * (`recomposePath` in `@gibs/quotes`'s `search/waves.ts`), so the impact test
67
+ * cannot see an inflated amount either. Only the two ends of the journey can.
68
+ */
69
+ export declare const COMPOSED_ROUTE_MAX_VALUE_GAIN_PERCENT = 5;
70
+ /**
71
+ * The verdict on one composed route.
72
+ *
73
+ * THREE STATES RATHER THAN A BOOLEAN, because "we have not priced this yet"
74
+ * and "we priced it and it is bad" are different facts and a caller must be
75
+ * able to act differently on them. Collapsing them would either hide a route
76
+ * that is merely still loading or offer one nobody has checked.
77
+ */
78
+ export type ComposedRouteVerdict = {
79
+ readonly state: 'acceptable';
80
+ } | {
81
+ readonly state: 'unknown';
82
+ readonly missing: 'price-impact' | 'route-value';
83
+ } | {
84
+ readonly state: 'rejected';
85
+ readonly reason: 'price-impact' | 'value-loss' | 'value-gain';
86
+ /** The measured figure, in percent, that failed its ceiling. */
87
+ readonly measuredPercent: number;
88
+ /** The ceiling it failed. */
89
+ readonly ceilingPercent: number;
90
+ };
91
+ /**
92
+ * Judges a composed route against its three ceilings.
93
+ *
94
+ * Checks price impact first, then value loss, then value gain. Impact names
95
+ * the specific leg at fault, while the two value tests only say the journey as
96
+ * a whole went wrong, so impact is the more useful answer when several fail.
97
+ * In the measured percent of a `'value-gain'` rejection, the gain is a
98
+ * positive number.
99
+ *
100
+ * @param options.priceImpactPercent - the swap leg's price impact, from the
101
+ * estimate's marginal reference quote; null when not yet quoted
102
+ * @param options.paidUsdValue - what the reader spends at the origin, in
103
+ * United States dollars; null when unpriced
104
+ * @param options.deliveredUsdValue - what the route delivers, valued on the
105
+ * SAME basis as `paidUsdValue`; null when unpriced
106
+ * @returns the verdict
107
+ */
108
+ export declare const assessComposedRoute: ({ priceImpactPercent, paidUsdValue, deliveredUsdValue, }: {
109
+ priceImpactPercent: number | null | undefined;
110
+ paidUsdValue: number | null | undefined;
111
+ deliveredUsdValue: number | null | undefined;
112
+ }) => ComposedRouteVerdict;
113
+ /**
114
+ * Whether a composed route may be listed on the strip.
115
+ *
116
+ * ONLY AN EXPLICIT PASS LISTS THE ROUTE. An unknown verdict withholds it, the
117
+ * same as a rejection, because a three-signature route nobody has checked is
118
+ * the case this gate exists for. The two remain distinct in
119
+ * {@link ComposedRouteVerdict} so a surface can tell the reader "still
120
+ * pricing" rather than "no route", which a boolean alone could not.
121
+ *
122
+ * @param verdict - the verdict from {@link assessComposedRoute}
123
+ * @returns true only when the route passed every ceiling
124
+ */
125
+ export declare const composedRouteMayBeOffered: (verdict: ComposedRouteVerdict) => boolean;
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.composedRouteMayBeOffered = exports.assessComposedRoute = exports.COMPOSED_ROUTE_MAX_VALUE_GAIN_PERCENT = exports.COMPOSED_ROUTE_MAX_VALUE_LOSS_PERCENT = exports.COMPOSED_ROUTE_MAX_PRICE_IMPACT_PERCENT = void 0;
4
+ /**
5
+ * Whether a composed multi-signature route is good enough to offer at all.
6
+ *
7
+ * WHY A ROUTE NEEDS A GATE THAT A SINGLE CROSSING DOES NOT. A one-signature
8
+ * route that turns out badly costs the reader one transaction and they still
9
+ * hold something they chose. A composed route — an aggregator leg, then an
10
+ * omnibridge crossing, then a swap — strands them between signatures: after
11
+ * the first they hold an anchor token on a chain they did not ask for, and the
12
+ * only way out is to pay again. So a cheap first leg followed by a bad swap is
13
+ * worse than the route it replaced, and the difference has to be found BEFORE
14
+ * the route is listed rather than after the reader has signed.
15
+ *
16
+ * NOTHING HERE COSTS A REQUEST. Both figures the verdict reads are already in
17
+ * hand when a composer runs: the swap's price impact comes from the on-chain
18
+ * `getAmountsOut` pair the estimate already fetched, and the two dollar values
19
+ * come from the route's own basis. The gate is free, which is why it can run
20
+ * on every route rather than only on suspicious ones.
21
+ */
22
+ /**
23
+ * The price impact above which a composed route is withdrawn, in percent.
24
+ *
25
+ * MEASURED RATHER THAN CHOSEN. Across the anchor and size combinations swept
26
+ * on 2026-09-21 — bridged wrapped ether, USD Coin, DAI and USDT into wrapped
27
+ * PulseChain coin, at roughly forty and one hundred and forty United States
28
+ * dollars — the impact ran from 0.008 percent to 0.037 percent, with bridged
29
+ * USDT the one outlier at 0.34 to 0.37 percent. Three percent is two orders of
30
+ * magnitude above the normal case and an order above the worst observed, so it
31
+ * cannot fire on an ordinary route; and it is still far below the point where
32
+ * a reader is quietly losing real money on a leg they cannot undo.
33
+ *
34
+ * See `docs/near-intents-conversion-composition.md` for the full sweep.
35
+ */
36
+ exports.COMPOSED_ROUTE_MAX_PRICE_IMPACT_PERCENT = 3;
37
+ /**
38
+ * The end-to-end value loss above which a composed route is withdrawn, in
39
+ * percent of what the reader pays.
40
+ *
41
+ * A SECOND TEST BECAUSE THE FIRST CAN READ FINE ON A BROKEN POOL. Price impact
42
+ * is measured against a marginal reference quote taken from the same pool, so
43
+ * a pool that is uniformly mispriced — a stale pair, a pair holding almost
44
+ * nothing — reports a small impact while paying out far less than the tokens
45
+ * going in are worth. Only comparing the two ends of the journey catches that.
46
+ *
47
+ * Measured whole-journey loss on the same sweep ran from 0.81 to 1.36 percent,
48
+ * of which the swap fee alone is 0.29. Five percent leaves every observed
49
+ * route untouched and still stops a route that is quietly eating a twentieth
50
+ * of the reader's money.
51
+ */
52
+ exports.COMPOSED_ROUTE_MAX_VALUE_LOSS_PERCENT = 5;
53
+ /**
54
+ * The end-to-end value GAIN above which a composed route is withdrawn, in
55
+ * percent of what the reader pays.
56
+ *
57
+ * A ROUTE CANNOT MAKE MONEY. Every leg charges a fee, so a route that
58
+ * delivers more value than it takes says one of its figures is wrong. Only a
59
+ * disagreement between the two price feeds can make a gain look real, and the
60
+ * value-loss ceiling already trusts those feeds to within the same five
61
+ * percent. So the bound is symmetric with
62
+ * {@link COMPOSED_ROUTE_MAX_VALUE_LOSS_PERCENT}.
63
+ *
64
+ * THE DEFECT THIS STOPS. On 2026-10-03 the engine served 0.0002 BNB to HEX at
65
+ * 495,256 HEX, about seven thousand times the rate of the next tier. The
66
+ * value-loss test read that as a loss of minus seven hundred thousand percent
67
+ * and passed it. A PulseX hop rescaled from an upstream live answer keeps the
68
+ * price impact measured at its original, smaller input
69
+ * (`recomposePath` in `@gibs/quotes`'s `search/waves.ts`), so the impact test
70
+ * cannot see an inflated amount either. Only the two ends of the journey can.
71
+ */
72
+ exports.COMPOSED_ROUTE_MAX_VALUE_GAIN_PERCENT = 5;
73
+ /**
74
+ * Judges a composed route against its three ceilings.
75
+ *
76
+ * Checks price impact first, then value loss, then value gain. Impact names
77
+ * the specific leg at fault, while the two value tests only say the journey as
78
+ * a whole went wrong, so impact is the more useful answer when several fail.
79
+ * In the measured percent of a `'value-gain'` rejection, the gain is a
80
+ * positive number.
81
+ *
82
+ * @param options.priceImpactPercent - the swap leg's price impact, from the
83
+ * estimate's marginal reference quote; null when not yet quoted
84
+ * @param options.paidUsdValue - what the reader spends at the origin, in
85
+ * United States dollars; null when unpriced
86
+ * @param options.deliveredUsdValue - what the route delivers, valued on the
87
+ * SAME basis as `paidUsdValue`; null when unpriced
88
+ * @returns the verdict
89
+ */
90
+ const assessComposedRoute = ({ priceImpactPercent, paidUsdValue, deliveredUsdValue, }) => {
91
+ if (typeof priceImpactPercent !== 'number' || !Number.isFinite(priceImpactPercent)) {
92
+ return { state: 'unknown', missing: 'price-impact' };
93
+ }
94
+ if (priceImpactPercent > exports.COMPOSED_ROUTE_MAX_PRICE_IMPACT_PERCENT) {
95
+ return {
96
+ state: 'rejected',
97
+ reason: 'price-impact',
98
+ measuredPercent: priceImpactPercent,
99
+ ceilingPercent: exports.COMPOSED_ROUTE_MAX_PRICE_IMPACT_PERCENT,
100
+ };
101
+ }
102
+ const bothValuesKnown = typeof paidUsdValue === 'number' &&
103
+ Number.isFinite(paidUsdValue) &&
104
+ paidUsdValue > 0 &&
105
+ typeof deliveredUsdValue === 'number' &&
106
+ Number.isFinite(deliveredUsdValue);
107
+ if (!bothValuesKnown) {
108
+ return { state: 'unknown', missing: 'route-value' };
109
+ }
110
+ const lossPercent = ((paidUsdValue - deliveredUsdValue) / paidUsdValue) * 100;
111
+ if (lossPercent > exports.COMPOSED_ROUTE_MAX_VALUE_LOSS_PERCENT) {
112
+ return {
113
+ state: 'rejected',
114
+ reason: 'value-loss',
115
+ measuredPercent: lossPercent,
116
+ ceilingPercent: exports.COMPOSED_ROUTE_MAX_VALUE_LOSS_PERCENT,
117
+ };
118
+ }
119
+ const gainPercent = -lossPercent;
120
+ if (gainPercent > exports.COMPOSED_ROUTE_MAX_VALUE_GAIN_PERCENT) {
121
+ return {
122
+ state: 'rejected',
123
+ reason: 'value-gain',
124
+ measuredPercent: gainPercent,
125
+ ceilingPercent: exports.COMPOSED_ROUTE_MAX_VALUE_GAIN_PERCENT,
126
+ };
127
+ }
128
+ return { state: 'acceptable' };
129
+ };
130
+ exports.assessComposedRoute = assessComposedRoute;
131
+ /**
132
+ * Whether a composed route may be listed on the strip.
133
+ *
134
+ * ONLY AN EXPLICIT PASS LISTS THE ROUTE. An unknown verdict withholds it, the
135
+ * same as a rejection, because a three-signature route nobody has checked is
136
+ * the case this gate exists for. The two remain distinct in
137
+ * {@link ComposedRouteVerdict} so a surface can tell the reader "still
138
+ * pricing" rather than "no route", which a boolean alone could not.
139
+ *
140
+ * @param verdict - the verdict from {@link assessComposedRoute}
141
+ * @returns true only when the route passed every ceiling
142
+ */
143
+ const composedRouteMayBeOffered = (verdict) => verdict.state === 'acceptable';
144
+ exports.composedRouteMayBeOffered = composedRouteMayBeOffered;
@@ -0,0 +1,88 @@
1
+ import type { Token } from '../types.js';
2
+ /**
3
+ * Which anchor tokens a non-executing aggregator may deliver on an omnibridge
4
+ * entry chain, and what the reader's own crossing signature then does with
5
+ * each one.
6
+ *
7
+ * WHY THE ANCHOR HAS TO BE A CHOICE RATHER THAN A DERIVATION. A two-signature
8
+ * route quotes towards one entry-chain token, taken straight from the
9
+ * destination pair. For a native-PulseChain destination that token is
10
+ * WRAPPED ether, because the omnibridge home list holds no entry for the
11
+ * native coin at all — measured on 2026-09-21, 657 Ethereum-side entries and
12
+ * not one at the zero address — so the pair a native destination resolves to
13
+ * is `WETH ↔ WETH from Ethereum`.
14
+ *
15
+ * NEAR Intents has NO LIQUIDITY IN WRAPPED ETHER ON ETHEREUM. Measured at
16
+ * 0.015, 0.05, 0.2 and 0.5 ether of input, every request answered
17
+ * `No liquidity available`. It carries the NATIVE coin on the same chain with
18
+ * plenty. So the one derived anchor is the one anchor the service cannot
19
+ * price, the quote is refused, and the route withdraws in silence — which is
20
+ * the correct response to a refusal and is also what hid this from us.
21
+ *
22
+ * WRAPPED NATIVE AND THE NATIVE COIN LAND IN THE SAME PLACE, which is what
23
+ * makes the substitution safe rather than merely convenient. The mediator's
24
+ * native router wraps on the way in: `wrapAndRelayTokens` takes the coin as
25
+ * transaction value and the crossing mints the SAME PulseChain token the
26
+ * wrapped entry would have minted. See `settings.ts`, the
27
+ * `assetIn.address === zeroAddress` branch. So swapping one anchor for the
28
+ * other changes what the aggregator delivers and changes nothing downstream.
29
+ *
30
+ * IT IS ALSO CHEAPER AND SHORTER, on both legs. Measured from Base native
31
+ * ether on 2026-09-21, one request batch per size:
32
+ *
33
+ * | anchor on Ethereum | 0.015 ether | 0.05 ether | 0.2 ether |
34
+ * | ------------------ | ----------- | ---------- | --------- |
35
+ * | native ether | 0.494% | 0.330% | 0.278% |
36
+ * | USD Coin | 1.037% | 0.651% | 0.509% |
37
+ * | Tether | 1.052% | 0.665% | 0.506% |
38
+ * | wrapped ether | refused | refused | refused |
39
+ *
40
+ * And the native coin needs no allowance, so its crossing is ONE signature
41
+ * where an entry-chain token's is an approval followed by a relay.
42
+ */
43
+ /** What the reader's crossing signature calls with a given anchor. */
44
+ export type EntryChainCrossingCall =
45
+ /** Native coin sent as transaction value; the router wraps and relays it. */
46
+ 'wrapAndRelayTokens'
47
+ /** An entry-chain token handed to the mediator, which needs an allowance first. */
48
+ | 'relayTokens';
49
+ /** One anchor the aggregator may be asked to deliver, and its consequences. */
50
+ export type EntryChainAnchor = {
51
+ /** The token the aggregator delivers into the reader's own wallet. */
52
+ readonly token: Token;
53
+ /** What the reader's crossing signature calls with it. */
54
+ readonly crossingCall: EntryChainCrossingCall;
55
+ /**
56
+ * Whether the crossing needs a separate approval transaction first, which
57
+ * is an EXTRA signature on a route that already asks for several.
58
+ */
59
+ readonly requiresApproval: boolean;
60
+ /** The PulseChain token the crossing mints. Identical across every anchor here. */
61
+ readonly landsAs: Token;
62
+ };
63
+ /**
64
+ * Builds the ordered anchor list for one destination pair, cheapest and
65
+ * shortest first.
66
+ *
67
+ * The list is ordered, not ranked: a caller asks the aggregator for the first
68
+ * entry and moves to the next only on a refusal. That keeps the cost at ONE
69
+ * vendor request in the ordinary case, which matters against a limit of
70
+ * twelve hundred requests per sixty seconds.
71
+ *
72
+ * The substitution is narrow ON PURPOSE. Only the chain's wrapped native
73
+ * asset gains a second anchor, because only the native coin mints the same
74
+ * PulseChain token. A USD Coin pair does not gain an ether anchor here —
75
+ * ether would land as bridged ether, a different token, needing a different
76
+ * swap path. That is a different DESTINATION PAIR, and choosing between pairs
77
+ * is made by the route engine, which holds every destination pair.
78
+ *
79
+ * @param options.entryChainToken - the destination pair's entry-chain side
80
+ * @param options.pulsechainToken - the destination pair's PulseChain side
81
+ * @param options.entryChainId - the omnibridge entry chain's decimal id
82
+ * @returns the anchors to try, in order; never empty
83
+ */
84
+ export declare const entryChainAnchorsForPair: ({ entryChainToken, pulsechainToken, entryChainId, }: {
85
+ entryChainToken: Token;
86
+ pulsechainToken: Token;
87
+ entryChainId: number;
88
+ }) => readonly EntryChainAnchor[];
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.entryChainAnchorsForPair = void 0;
4
+ const viem_1 = require("viem");
5
+ const config_js_1 = require("../config.js");
6
+ const ecosystems_js_1 = require("../ecosystems.js");
7
+ /**
8
+ * Builds the ordered anchor list for one destination pair, cheapest and
9
+ * shortest first.
10
+ *
11
+ * The list is ordered, not ranked: a caller asks the aggregator for the first
12
+ * entry and moves to the next only on a refusal. That keeps the cost at ONE
13
+ * vendor request in the ordinary case, which matters against a limit of
14
+ * twelve hundred requests per sixty seconds.
15
+ *
16
+ * The substitution is narrow ON PURPOSE. Only the chain's wrapped native
17
+ * asset gains a second anchor, because only the native coin mints the same
18
+ * PulseChain token. A USD Coin pair does not gain an ether anchor here —
19
+ * ether would land as bridged ether, a different token, needing a different
20
+ * swap path. That is a different DESTINATION PAIR, and choosing between pairs
21
+ * is made by the route engine, which holds every destination pair.
22
+ *
23
+ * @param options.entryChainToken - the destination pair's entry-chain side
24
+ * @param options.pulsechainToken - the destination pair's PulseChain side
25
+ * @param options.entryChainId - the omnibridge entry chain's decimal id
26
+ * @returns the anchors to try, in order; never empty
27
+ */
28
+ const entryChainAnchorsForPair = ({ entryChainToken, pulsechainToken, entryChainId, }) => {
29
+ const pairAnchor = {
30
+ token: entryChainToken,
31
+ crossingCall: 'relayTokens',
32
+ requiresApproval: true,
33
+ landsAs: pulsechainToken,
34
+ };
35
+ const entryChain = (0, config_js_1.toChain)(entryChainId);
36
+ // `nativeAssetOut` is typed as a total record and is not one — `toChain` is a
37
+ // cast, so an unmapped chain id yields a key that indexes to undefined. The
38
+ // runtime check outlives the static type on purpose.
39
+ const wrappedNativeAddress = config_js_1.nativeAssetOut[entryChain];
40
+ if (wrappedNativeAddress === undefined ||
41
+ !(0, ecosystems_js_1.sameAddress)(entryChainToken.address, wrappedNativeAddress, 'evm')) {
42
+ return [pairAnchor];
43
+ }
44
+ const nativeAnchor = {
45
+ token: {
46
+ // Decimals and name come from the WRAPPED token rather than from a
47
+ // constant, because the wrapped asset mirrors its coin exactly and a
48
+ // hard-coded eighteen would be a second source of the same fact.
49
+ ...entryChainToken,
50
+ address: viem_1.zeroAddress,
51
+ symbol: config_js_1.nativeTokenSymbol[entryChain] ?? entryChainToken.symbol,
52
+ },
53
+ crossingCall: 'wrapAndRelayTokens',
54
+ requiresApproval: false,
55
+ landsAs: pulsechainToken,
56
+ };
57
+ return [nativeAnchor, pairAnchor];
58
+ };
59
+ exports.entryChainAnchorsForPair = entryChainAnchorsForPair;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Formats a United States dollar number to a cent-precise string ("1,234.50",
3
+ * or "1.234,50" in German). No leading currency symbol — callers prepend it
4
+ * where the design needs one. The single source of truth shared by the raw
5
+ * producers (cent-rounding) and the display adapter (final formatting).
6
+ *
7
+ * `locale` defaults to `'en-US'` so every caller that does not read the
8
+ * reader's own language — the raw producers, and every existing test —
9
+ * keeps today's exact string. THE DISPLAY ADAPTER (`toRouteView.ts`) is the
10
+ * one caller that should pass the reader's own Lingui locale (`i18n.locale`):
11
+ * without it, the route strip's dollar figures stayed English-formatted
12
+ * inside every other translated screen, the one number on the page that
13
+ * never adopted the reader's language. The digit count never moves — only
14
+ * `toLocaleString`'s grouping and decimal separators do, per
15
+ * `minimumFractionDigits`/`maximumFractionDigits` staying pinned at 2.
16
+ *
17
+ * @param value - the United States dollar amount as a number
18
+ * @param locale - a BCP 47 locale tag to format with; defaults to `'en-US'`
19
+ * @returns the formatted two-decimal string, without a leading `$`
20
+ */
21
+ export declare const formatUsd: (value: number, locale?: string) => string;
22
+ /**
23
+ * Nullable {@link formatUsd}: null/undefined pass through as null so an unpriced
24
+ * value renders as absent rather than "0.00".
25
+ *
26
+ * @param value - the United States dollar amount, or null/undefined when unknown
27
+ * @param locale - a BCP 47 locale tag to format with; defaults to `'en-US'`
28
+ * @returns the formatted string, or null when the value is absent
29
+ */
30
+ export declare const formatUsdNullable: (value: number | null | undefined, locale?: string) => string | null;
31
+ export declare const roundUsdToCents: (value: number) => number;
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.roundUsdToCents = exports.formatUsdNullable = exports.formatUsd = void 0;
4
+ /**
5
+ * Formats a United States dollar number to a cent-precise string ("1,234.50",
6
+ * or "1.234,50" in German). No leading currency symbol — callers prepend it
7
+ * where the design needs one. The single source of truth shared by the raw
8
+ * producers (cent-rounding) and the display adapter (final formatting).
9
+ *
10
+ * `locale` defaults to `'en-US'` so every caller that does not read the
11
+ * reader's own language — the raw producers, and every existing test —
12
+ * keeps today's exact string. THE DISPLAY ADAPTER (`toRouteView.ts`) is the
13
+ * one caller that should pass the reader's own Lingui locale (`i18n.locale`):
14
+ * without it, the route strip's dollar figures stayed English-formatted
15
+ * inside every other translated screen, the one number on the page that
16
+ * never adopted the reader's language. The digit count never moves — only
17
+ * `toLocaleString`'s grouping and decimal separators do, per
18
+ * `minimumFractionDigits`/`maximumFractionDigits` staying pinned at 2.
19
+ *
20
+ * @param value - the United States dollar amount as a number
21
+ * @param locale - a BCP 47 locale tag to format with; defaults to `'en-US'`
22
+ * @returns the formatted two-decimal string, without a leading `$`
23
+ */
24
+ const formatUsd = (value, locale = 'en-US') => value.toLocaleString(locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
25
+ exports.formatUsd = formatUsd;
26
+ /**
27
+ * Nullable {@link formatUsd}: null/undefined pass through as null so an unpriced
28
+ * value renders as absent rather than "0.00".
29
+ *
30
+ * @param value - the United States dollar amount, or null/undefined when unknown
31
+ * @param locale - a BCP 47 locale tag to format with; defaults to `'en-US'`
32
+ * @returns the formatted string, or null when the value is absent
33
+ */
34
+ const formatUsdNullable = (value, locale) => value === null || value === undefined ? null : (0, exports.formatUsd)(value, locale);
35
+ exports.formatUsdNullable = formatUsdNullable;
36
+ /**
37
+ * Rounds a United States dollar amount to whole cents.
38
+ *
39
+ * This replaces cent-rounding that went through the DISPLAY formatter and parsed
40
+ * the result back — `Number(formatUsd(value).replace(/,/g, ''))`. That works
41
+ * only while the display is pinned to a locale which groups with commas and
42
+ * decimalises with a dot. The moment money follows the reader's language, a
43
+ * German build formats 1234.56 as "1.234,56", and stripping the commas from THAT
44
+ * yields 1.23456: a fee understated by a factor of a thousand, silently, with no
45
+ * NaN anywhere to make it visible.
46
+ *
47
+ * It keeps its OWN formatter rather than borrowing the display one, pinned to a
48
+ * fixed locale and with grouping switched off, so the string it parses has no
49
+ * separators to strip and no locale to disagree with. That also makes it exact
50
+ * rather than merely close: `Math.round(value * 100) / 100` looks equivalent and
51
+ * is not, because it rounds the binary double while the formatter rounds the
52
+ * decimal the reader typed — they disagree on 1.005, giving 1.00 against 1.01.
53
+ * Matching the old behaviour to the cent is the point, so the old engine stays.
54
+ *
55
+ * @param value - the United States dollar amount
56
+ * @returns the amount rounded to two decimal places
57
+ */
58
+ const centRounder = new Intl.NumberFormat('en-US', {
59
+ minimumFractionDigits: 2,
60
+ maximumFractionDigits: 2,
61
+ useGrouping: false,
62
+ });
63
+ const roundUsdToCents = (value) => Number(centRounder.format(value));
64
+ exports.roundUsdToCents = roundUsdToCents;