@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
package/dist/referral.js CHANGED
@@ -33,7 +33,11 @@ export const referralIntegrator = 'gibs.finance';
33
33
  */
34
34
  export const resolveAttribution = (override, packageDefault) => (override === null ? undefined : (override ?? packageDefault));
35
35
  /**
36
- * Payout address for Relay application fees.
36
+ * Gibs Finance's payout address for Relay application fees.
37
+ *
38
+ * NO BUILDER APPLIES IT. A Relay request carries an `appFees` entry only when
39
+ * the caller passes `relayAppFee`. The Gibs interface passes this address
40
+ * explicitly; any other caller passes its own, or nothing.
37
41
  *
38
42
  * Set to the owner's receiving wallet. Setting it back to `null` is a working
39
43
  * state rather than a broken one, and is the way to stop collecting: Relay
@@ -52,9 +56,13 @@ export const resolveAttribution = (override, packageDefault) => (override === nu
52
56
  */
53
57
  export const relayReferralRecipient = '0xAF2ce0189f46f5663715b0b9ED2a10eA924AB9B0';
54
58
  /**
55
- * Default application fee in basis points. Ten basis points is one tenth of one
56
- * percent. The interface can override it per provider in stored settings; this
57
- * is the value a reader who has never touched the setting gets.
59
+ * The application fee the Gibs interface charges, in basis points. Ten basis
60
+ * points is one tenth of one percent. The interface can override it per
61
+ * provider in stored settings; this is the value a reader who has never touched
62
+ * the setting gets.
63
+ *
64
+ * NO BUILDER APPLIES IT. It reaches a request only when a caller passes it as
65
+ * `relayAppFee.basisPoints` (Relay) or converts it to `lifiFee` (LI.FI).
58
66
  */
59
67
  export const defaultReferralFeeBasisPoints = 10n;
60
68
  /**
@@ -0,0 +1,2 @@
1
+ import { type Hex } from 'viem';
2
+ export declare const relayDepositoryByChainId: ReadonlyMap<number, Hex>;
@@ -0,0 +1,82 @@
1
+ import { getAddress } from 'viem';
2
+ /**
3
+ * Relay's deposit contract (its "depository") on each Ethereum-style chain:
4
+ * the ONLY address a Relay origin deposit may be sent to, and the only
5
+ * address it may ask the reader to approve.
6
+ *
7
+ * WHY A FIXED TABLE. A quote that names its own spender cannot be trusted to
8
+ * name an honest one: a compromised quote host could name any contract. This
9
+ * table ships with the code and changes only through a release.
10
+ *
11
+ * SOURCE. Relay's Chains API, `GET https://api.relay.link/chains`, field
12
+ * `chains[].protocol.v2.depository`, read 2026-10-07 for every chain whose
13
+ * `vmType` is `evm`. Relay's documentation says its depository addresses are
14
+ * published only through that API
15
+ * (https://docs.relay.link/references/protocol/addresses). A live quote the
16
+ * same day (Ethereum USDC to Base, `POST /quote/v2`) approved and deposited to
17
+ * exactly the Ethereum entry below.
18
+ *
19
+ * NOT THE `contracts` BLOCK. The same API also lists `relayReceiver`,
20
+ * `approvalProxy` and `erc20Router` under `chains[].contracts`. The live
21
+ * quote used none of them, so they are left out: an address the quote does
22
+ * not use is not one this table should accept.
23
+ *
24
+ * A CHAIN NOT LISTED IS REFUSED. If Relay adds a chain or moves a depository,
25
+ * deposits there are refused until this table is updated.
26
+ */
27
+ const common = getAddress('0x4cd00e387622c35bddb9b4c962c136462338bc31');
28
+ const alternate = getAddress('0x59916da825d2d2ec1bf878d71c88826f6633ecca');
29
+ export const relayDepositoryByChainId = new Map([
30
+ [1, common], // ethereum
31
+ [10, common], // optimism
32
+ [25, alternate], // cronos
33
+ [56, common], // bsc
34
+ [100, common], // gnosis
35
+ [130, common], // unichain
36
+ [137, common], // polygon
37
+ [143, common], // monad
38
+ [146, common], // sonic
39
+ [169, common], // manta-pacific
40
+ [196, common], // xlayer
41
+ [288, common], // boba
42
+ [324, common], // zksync
43
+ [360, common], // shape
44
+ [480, common], // world-chain
45
+ [747, common], // flow-evm
46
+ [988, common], // stable
47
+ [999, common], // hyperevm
48
+ [1088, alternate], // metis
49
+ [1135, common], // lisk
50
+ [1868, common], // soneium
51
+ [2020, common], // ronin
52
+ [2741, common], // abstract
53
+ [2818, common], // morph
54
+ [4217, common], // tempo
55
+ [4326, common], // megaeth
56
+ [4663, common], // robinhood
57
+ [5000, alternate], // mantle
58
+ [5031, common], // somnia
59
+ [5042, common], // arc
60
+ [5330, common], // superseed
61
+ [8453, common], // base
62
+ [9745, common], // plasma
63
+ [33139, common], // apechain
64
+ [34443, common], // mode
65
+ [42018, common], // mythos
66
+ [42161, common], // arbitrum
67
+ [42220, common], // celo
68
+ [43114, common], // avalanche
69
+ [48900, common], // zircuit
70
+ [57073, common], // ink
71
+ [59144, alternate], // linea
72
+ [60808, common], // bob
73
+ [80094, common], // berachain
74
+ [81457, common], // blast
75
+ [97477, common], // doma
76
+ [98866, common], // plume
77
+ [534352, common], // scroll
78
+ [685689, common], // gensyn
79
+ [747474, common], // katana
80
+ [5064014, common], // ethereal
81
+ [7777777, common], // zora
82
+ ]);
@@ -1,3 +1,4 @@
1
+ import { buildUrl } from '@gibs/common/url';
1
2
  import { decodeFunctionData, encodeFunctionData, erc20Abi, maxUint256, zeroAddress, } from 'viem';
2
3
  import { Chains } from './config.js';
3
4
  import { isNonEthereumVirtualMachineOrigin, } from './consolidated-quote.js';
@@ -861,9 +862,7 @@ export const parseRelayQuote = (payload) => {
861
862
  * than trust it — see `checkConsolidatedRecord`.
862
863
  */
863
864
  export const relayStatusUrl = ({ quoteId }) => {
864
- const url = new URL(relayRequestsUrl);
865
- url.searchParams.set('id', quoteId);
866
- return url.toString();
865
+ return buildUrl(relayRequestsUrl, { id: quoteId });
867
866
  };
868
867
  /**
869
868
  * Relay's request lifecycle states. Terminal states are `success` (the
@@ -208,9 +208,9 @@ export type DirectCrossingCandidateInputs = {
208
208
  * to quote — the user bridges directly from that chain to PulseChain. The
209
209
  * same-chain skip in {@link useAggregatorRouteCandidate} is correct (nothing to
210
210
  * quote), but it left the conversion path empty. This helper fills that gap by
211
- * synthesizing a zero-step candidate that `usePulseXConversionRoutes` (the user
212
- * interface's plural composer) can consume unchanged, producing the correct
213
- * same-chain conversion route so the ranker can pick the cheapest option.
211
+ * synthesizing a zero-step candidate. The user interface no longer calls this:
212
+ * the route engine serves a conversion now. The export stays as published
213
+ * interface until a separate decision removes it.
214
214
  *
215
215
  * The synthetic quote carries zero steps (no aggregator hops), a zero execution
216
216
  * duration (the direct crossing needs no quote wait), and a null transaction
@@ -8,9 +8,9 @@ import { omnibridgeProviderForEntryChain } from './providers.js';
8
8
  * to quote — the user bridges directly from that chain to PulseChain. The
9
9
  * same-chain skip in {@link useAggregatorRouteCandidate} is correct (nothing to
10
10
  * quote), but it left the conversion path empty. This helper fills that gap by
11
- * synthesizing a zero-step candidate that `usePulseXConversionRoutes` (the user
12
- * interface's plural composer) can consume unchanged, producing the correct
13
- * same-chain conversion route so the ranker can pick the cheapest option.
11
+ * synthesizing a zero-step candidate. The user interface no longer calls this:
12
+ * the route engine serves a conversion now. The export stays as published
13
+ * interface until a separate decision removes it.
14
14
  *
15
15
  * The synthetic quote carries zero steps (no aggregator hops), a zero execution
16
16
  * duration (the direct crossing needs no quote wait), and a null transaction
@@ -29,6 +29,11 @@ export type CarrierAskAnswer = {
29
29
  readonly reason: string | null;
30
30
  /** A minimum amount this refusal names, in `edge.from`'s base units, or null. */
31
31
  readonly minimumAmount: bigint | null;
32
+ /**
33
+ * What the carrier can still take today, in `edge.from`'s base units,
34
+ * when the refusal is `'above-available'`. Absent otherwise.
35
+ */
36
+ readonly availableAmount?: bigint;
32
37
  };
33
38
  };
34
39
  /**
@@ -46,6 +51,15 @@ export type CarrierExactResult = {
46
51
  readonly outcome: 'delivered';
47
52
  /** What this edge delivers for exactly the requested input amount, in `edge.to`'s base units. */
48
53
  readonly deliveredAmount: bigint;
54
+ /** How far this edge's executed rate falls below its marginal rate, in percent; absent when the carrier cannot measure it. */
55
+ readonly priceImpactPercent?: number;
56
+ /**
57
+ * The token addresses the carrier's own execution passes through, in
58
+ * order, when it chose among several (a PulseX swap path). The figure
59
+ * above was priced along exactly this path, so it is the path that must
60
+ * be signed. Absent when the carrier makes no such choice.
61
+ */
62
+ readonly swapPath?: readonly string[];
49
63
  } | {
50
64
  readonly outcome: 'refused';
51
65
  readonly refusal: {
@@ -54,5 +68,10 @@ export type CarrierExactResult = {
54
68
  readonly reason: string | null;
55
69
  /** A minimum amount this refusal names, in `edge.from`'s base units, or null. */
56
70
  readonly minimumAmount: bigint | null;
71
+ /**
72
+ * What the carrier can still take today, in `edge.from`'s base units,
73
+ * when the refusal is `'above-available'`. Absent otherwise.
74
+ */
75
+ readonly availableAmount?: bigint;
57
76
  };
58
77
  };
@@ -277,9 +277,27 @@ const prepareOmnibridgeReads = async ({ nodes, edgesFrom, isProductionNetwork, c
277
277
  }
278
278
  return preparedByEdgeKey;
279
279
  };
280
- const entryAmountVerdictReason = (verdict) => verdict.reason === 'below-minimum'
281
- ? `below the entry mediator's minimum of ${verdict.minimumPerTransaction} base units`
282
- : `above what the entry mediator has left today (${verdict.availablePerTransaction} base units)`;
280
+ /**
281
+ * The refusal an entry mediator's own bound produces, carrying the bound
282
+ * itself as a figure: `minimumAmount` below the floor, `availableAmount`
283
+ * above what is left today. A reader is told the number to type, so the
284
+ * number must survive here rather than live only inside the reason string.
285
+ *
286
+ * @param verdict - the crossed bound, in the entry token's base units
287
+ * @returns the refusal, in `edge.from`'s base units
288
+ */
289
+ const entryAmountRefusal = (verdict) => verdict.reason === 'below-minimum'
290
+ ? {
291
+ kind: 'below-minimum',
292
+ reason: `below the entry mediator's minimum of ${verdict.minimumPerTransaction} base units`,
293
+ minimumAmount: verdict.minimumPerTransaction,
294
+ }
295
+ : {
296
+ kind: 'above-available',
297
+ reason: `above what the entry mediator has left today (${verdict.availablePerTransaction} base units)`,
298
+ minimumAmount: null,
299
+ availableAmount: verdict.availablePerTransaction,
300
+ };
283
301
  /**
284
302
  * Builds the omnibridge crossing as a plug-in carrier.
285
303
  *
@@ -316,15 +334,7 @@ export const createOmnibridgeCarrier = (options) => {
316
334
  return null;
317
335
  const verdict = checkEntryAmount({ amount: amountIn, limits: prepared.limits });
318
336
  if (!verdict.fits) {
319
- const minimumAmount = verdict.reason === 'below-minimum' ? verdict.minimumPerTransaction : null;
320
- return {
321
- outcome: 'refused',
322
- refusal: {
323
- kind: verdict.reason === 'below-minimum' ? 'below-minimum' : 'above-available',
324
- reason: entryAmountVerdictReason(verdict),
325
- minimumAmount,
326
- },
327
- };
337
+ return { outcome: 'refused', refusal: entryAmountRefusal(verdict) };
328
338
  }
329
339
  return { outcome: 'delivered', deliveredAmount: netDeliveredAmount(amountIn, prepared.feePerEther) };
330
340
  };
@@ -332,15 +342,7 @@ export const createOmnibridgeCarrier = (options) => {
332
342
  const limits = await readEntryLimits(edge);
333
343
  const verdict = checkEntryAmount({ amount: inputAmount, limits });
334
344
  if (!verdict.fits) {
335
- const minimumAmount = verdict.reason === 'below-minimum' ? verdict.minimumPerTransaction : null;
336
- return {
337
- ok: false,
338
- refusal: {
339
- kind: verdict.reason === 'below-minimum' ? 'below-minimum' : 'above-available',
340
- reason: entryAmountVerdictReason(verdict),
341
- minimumAmount,
342
- },
343
- };
345
+ return { ok: false, refusal: entryAmountRefusal(verdict) };
344
346
  }
345
347
  const feePerEther = await readFeePerEther(edge);
346
348
  const deliveredAmount = netDeliveredAmount(inputAmount, feePerEther);
@@ -0,0 +1,80 @@
1
+ import { type Hex } from 'viem';
2
+ /**
3
+ * Which swap path a PulseX hop takes. The hop no longer always goes through
4
+ * Wrapped PLS: the carrier prices every candidate path from the pool reserves
5
+ * it already read, and keeps the one that delivers the most. These are the
6
+ * pure parts of that choice — the hub list, the candidate paths, and the
7
+ * pick — kept apart from the carrier so each can be tested on its own.
8
+ */
9
+ /** One token a PulseX swap may pass through on its way from the input token to the output token. */
10
+ export type PulseXHubToken = {
11
+ /** The token's on-chain `symbol()`, for logs and tests only. Never used to match a token. */
12
+ readonly symbol: string;
13
+ /** The token's checksummed PulseChain address. Tokens are always matched by this. */
14
+ readonly address: Hex;
15
+ };
16
+ /**
17
+ * The tokens a PulseX swap may route through, Wrapped PLS first. Each address
18
+ * comes from a constant this package already holds; none is written here:
19
+ *
20
+ * - WPLS — `nativeAssetOut[Chains.PLS]` (`config.ts`).
21
+ * - PLSX, HEX, USDC, USDT — `pulsechainTokenAddresses` (`config.ts`).
22
+ * - DAI (from Ethereum) — `PULSECHAIN_USD_ANCHOR_ADDRESS` (`prices.ts`).
23
+ * - WETH (from Ethereum) — the Ethereum pathway's `bridgedNativeAssetOut` (`config.ts`).
24
+ *
25
+ * KEPT SMALL ON PURPOSE. Each hub adds pools to the one batched reserve read
26
+ * the carrier makes per search, and adds paths to every price it computes.
27
+ * Seven hubs give at most 20 candidate paths per hop (see
28
+ * {@link candidatePulseXPaths}).
29
+ */
30
+ export declare const pulseXHubTokens: readonly PulseXHubToken[];
31
+ /**
32
+ * Every swap path a PulseX hop from `from` to `to` may take:
33
+ *
34
+ * - direct: `[from, to]`;
35
+ * - one hub: `[from, hub, to]` for each hub, Wrapped PLS among them;
36
+ * - two hubs: `[from, wrappedNative, hub, to]` and `[from, hub, wrappedNative, to]`
37
+ * for each other hub — Wrapped PLS holds the deepest pools on PulseX, so it
38
+ * is the one middle token worth pairing with every other hub.
39
+ *
40
+ * A path that names a token twice is dropped. That removes every path whose
41
+ * hub IS `from` or `to`, and the same-token hop when `from` equals `to`.
42
+ *
43
+ * The fixed path this carrier used before — `[from, WPLS, to]`, or
44
+ * `[from, to]` when one side is WPLS — is always in the result, so the pick
45
+ * can only match or beat it.
46
+ *
47
+ * @param options.from - the pool token the swap spends
48
+ * @param options.to - the pool token the swap buys
49
+ * @param options.wrappedNative - Wrapped PLS
50
+ * @param options.hubs - the hub token addresses, Wrapped PLS included
51
+ * @returns the candidate paths, direct first, each at least two tokens long
52
+ */
53
+ export declare const candidatePulseXPaths: (options: {
54
+ readonly from: Hex;
55
+ readonly to: Hex;
56
+ readonly wrappedNative: Hex;
57
+ readonly hubs: readonly Hex[];
58
+ }) => readonly (readonly Hex[])[];
59
+ /** One candidate path, priced. */
60
+ export type PulseXPathQuote<Step> = {
61
+ /** The tokens the swap passes through, in order. */
62
+ readonly path: readonly Hex[];
63
+ /** What the path delivers for the trade, in the last token's base units. */
64
+ readonly deliveredAmount: bigint;
65
+ /** The pools the path crosses, in order — kept so the caller can measure price impact on the same pools. */
66
+ readonly steps: readonly Step[];
67
+ };
68
+ /**
69
+ * The priced path that delivers the most. A path that delivers nothing is
70
+ * never chosen. Among equal outputs the path with fewer pools wins, and among
71
+ * equal lengths the earlier candidate wins, so the pick is deterministic.
72
+ *
73
+ * Gas is not weighed beyond that tie-break: the search charges one flat gas
74
+ * figure per swap signature, whatever its path length (`route-search-gas.ts`
75
+ * in the interface), so there is no per-pool gas figure to subtract here.
76
+ *
77
+ * @param quotes - the priced candidates; paths with a missing pool are already left out
78
+ * @returns the best candidate, or null when none delivers anything
79
+ */
80
+ export declare const bestPulseXPathQuote: <Step>(quotes: readonly PulseXPathQuote<Step>[]) => PulseXPathQuote<Step> | null;
@@ -0,0 +1,92 @@
1
+ import { getAddress } from 'viem';
2
+ import { Chains, Providers, nativeAssetOut, pathways, pulsechainTokenAddresses } from '../../config.js';
3
+ import { PULSECHAIN_USD_ANCHOR_ADDRESS } from '../../prices.js';
4
+ /** Wrapped PLS: the first hub, and the middle token of every two-hub path. */
5
+ const wrappedPulseAddress = getAddress(nativeAssetOut[Chains.PLS]);
6
+ /** The Ethereum-bridged WETH on PulseChain, read off the Ethereum pathway's own config. Undefined only if that pathway is removed. */
7
+ const bridgedWrappedEtherAddress = pathways[Providers.PULSECHAIN][Chains.PLS]?.[Chains.ETH]?.bridgedNativeAssetOut;
8
+ /**
9
+ * The tokens a PulseX swap may route through, Wrapped PLS first. Each address
10
+ * comes from a constant this package already holds; none is written here:
11
+ *
12
+ * - WPLS — `nativeAssetOut[Chains.PLS]` (`config.ts`).
13
+ * - PLSX, HEX, USDC, USDT — `pulsechainTokenAddresses` (`config.ts`).
14
+ * - DAI (from Ethereum) — `PULSECHAIN_USD_ANCHOR_ADDRESS` (`prices.ts`).
15
+ * - WETH (from Ethereum) — the Ethereum pathway's `bridgedNativeAssetOut` (`config.ts`).
16
+ *
17
+ * KEPT SMALL ON PURPOSE. Each hub adds pools to the one batched reserve read
18
+ * the carrier makes per search, and adds paths to every price it computes.
19
+ * Seven hubs give at most 20 candidate paths per hop (see
20
+ * {@link candidatePulseXPaths}).
21
+ */
22
+ export const pulseXHubTokens = Object.freeze([
23
+ { symbol: 'WPLS', address: wrappedPulseAddress },
24
+ { symbol: 'PLSX', address: pulsechainTokenAddresses.PLSX },
25
+ { symbol: 'HEX', address: pulsechainTokenAddresses.HEX },
26
+ { symbol: 'DAI', address: getAddress(PULSECHAIN_USD_ANCHOR_ADDRESS) },
27
+ { symbol: 'USDC', address: pulsechainTokenAddresses.USDC },
28
+ { symbol: 'USDT', address: pulsechainTokenAddresses.USDT },
29
+ ...(bridgedWrappedEtherAddress === undefined ? [] : [{ symbol: 'WETH', address: getAddress(bridgedWrappedEtherAddress) }]),
30
+ ].map((hub) => Object.freeze(hub)));
31
+ /** Whether a path names any token twice, compared without regard to letter case. */
32
+ const repeatsAToken = (path) => new Set(path.map((address) => address.toLowerCase())).size !== path.length;
33
+ /** Whether two addresses are the same token, compared without regard to letter case. */
34
+ const sameToken = (left, right) => left.toLowerCase() === right.toLowerCase();
35
+ /**
36
+ * Every swap path a PulseX hop from `from` to `to` may take:
37
+ *
38
+ * - direct: `[from, to]`;
39
+ * - one hub: `[from, hub, to]` for each hub, Wrapped PLS among them;
40
+ * - two hubs: `[from, wrappedNative, hub, to]` and `[from, hub, wrappedNative, to]`
41
+ * for each other hub — Wrapped PLS holds the deepest pools on PulseX, so it
42
+ * is the one middle token worth pairing with every other hub.
43
+ *
44
+ * A path that names a token twice is dropped. That removes every path whose
45
+ * hub IS `from` or `to`, and the same-token hop when `from` equals `to`.
46
+ *
47
+ * The fixed path this carrier used before — `[from, WPLS, to]`, or
48
+ * `[from, to]` when one side is WPLS — is always in the result, so the pick
49
+ * can only match or beat it.
50
+ *
51
+ * @param options.from - the pool token the swap spends
52
+ * @param options.to - the pool token the swap buys
53
+ * @param options.wrappedNative - Wrapped PLS
54
+ * @param options.hubs - the hub token addresses, Wrapped PLS included
55
+ * @returns the candidate paths, direct first, each at least two tokens long
56
+ */
57
+ export const candidatePulseXPaths = (options) => {
58
+ const { from, to, wrappedNative, hubs } = options;
59
+ const oneHubPaths = hubs.map((hub) => [from, hub, to]);
60
+ const twoHubPaths = hubs
61
+ .filter((hub) => !sameToken(hub, wrappedNative))
62
+ .flatMap((hub) => [
63
+ [from, wrappedNative, hub, to],
64
+ [from, hub, wrappedNative, to],
65
+ ]);
66
+ return [[from, to], ...oneHubPaths, ...twoHubPaths].filter((path) => !repeatsAToken(path));
67
+ };
68
+ /**
69
+ * Whether `candidate` should replace `best`: it delivers more, or it delivers
70
+ * the same and crosses fewer pools. A tie goes to the shorter path because
71
+ * each extra pool costs gas, and nothing else separates the two.
72
+ */
73
+ const beats = (candidate, best) => {
74
+ if (candidate.deliveredAmount !== best.deliveredAmount)
75
+ return candidate.deliveredAmount > best.deliveredAmount;
76
+ return candidate.path.length < best.path.length;
77
+ };
78
+ /**
79
+ * The priced path that delivers the most. A path that delivers nothing is
80
+ * never chosen. Among equal outputs the path with fewer pools wins, and among
81
+ * equal lengths the earlier candidate wins, so the pick is deterministic.
82
+ *
83
+ * Gas is not weighed beyond that tie-break: the search charges one flat gas
84
+ * figure per swap signature, whatever its path length (`route-search-gas.ts`
85
+ * in the interface), so there is no per-pool gas figure to subtract here.
86
+ *
87
+ * @param quotes - the priced candidates; paths with a missing pool are already left out
88
+ * @returns the best candidate, or null when none delivers anything
89
+ */
90
+ export const bestPulseXPathQuote = (quotes) => quotes
91
+ .filter((quote) => quote.deliveredAmount > 0n)
92
+ .reduce((best, quote) => (best === null || beats(quote, best) ? quote : best), null);
@@ -0,0 +1,175 @@
1
+ import type { Hex } from 'viem';
2
+ /**
3
+ * PRICING ONLY. NOTHING SIGNS A SPLIT, SO NOTHING SERVES ONE.
4
+ *
5
+ * A split swap sends part of one trade along one PulseX path and the rest
6
+ * along others, often on both venues (V1 and V2). That lowers price impact on
7
+ * a large trade. But a split is several router calls, and on PulseChain no
8
+ * contract this application deploys or calls can run several router calls
9
+ * from the reader's tokens in one transaction:
10
+ *
11
+ * - The PulseX routers have no split call. Each call trades one path on one
12
+ * factory.
13
+ * - The Gibs destination routers (`TokenOmnibridgeRouter`) only unwrap and pay
14
+ * delivery fees in `onTokenBridged`. They make no arbitrary call.
15
+ * - Multicall3 forwards native value, but it cannot pull the reader's tokens
16
+ * safely: an allowance to Multicall3 is open to anyone who calls it.
17
+ * - PulseChain has no EIP-7702, so an ordinary wallet cannot batch calls
18
+ * atomically through `wallet_sendCalls`.
19
+ *
20
+ * So this module stays out of the package index and out of the carrier's
21
+ * `exact`. It exists so the figures can be measured and compared. Wiring it
22
+ * into the engine needs an execution path first: see the design note in the
23
+ * report that introduced this file.
24
+ */
25
+ /** One pool a split leg crosses, read in the direction of travel, with the identity shared pools are matched by. */
26
+ export type PulseXSplitStep = {
27
+ /** The pool's identity: its venue and token pair. Two legs that name the same `pool` trade against the same reserves. */
28
+ readonly pool: string;
29
+ /** The token flowing into the pool. */
30
+ readonly tokenIn: Hex;
31
+ /** The token flowing out of the pool. */
32
+ readonly tokenOut: Hex;
33
+ /** The pool's reserve of `tokenIn` before any leg of the split trades. */
34
+ readonly reserveIn: bigint;
35
+ /** The pool's reserve of `tokenOut` before any leg of the split trades. */
36
+ readonly reserveOut: bigint;
37
+ };
38
+ /** One path a split may send part of the trade along. */
39
+ export type PulseXSplitCandidate = {
40
+ /** The router the leg would be signed on. */
41
+ readonly venue: Hex;
42
+ /** The tokens the leg passes through, in order. */
43
+ readonly path: readonly Hex[];
44
+ /** The pools the leg crosses, in order. */
45
+ readonly steps: readonly PulseXSplitStep[];
46
+ };
47
+ /** One leg of a priced split, in execution order. */
48
+ export type PulseXSplitLeg = {
49
+ readonly venue: Hex;
50
+ readonly path: readonly Hex[];
51
+ /** The part of the trade this leg spends. The legs' inputs sum to the whole trade. */
52
+ readonly amountIn: bigint;
53
+ /** What this leg delivers, after every earlier leg has moved the pools it shares. */
54
+ readonly deliveredAmount: bigint;
55
+ };
56
+ /** A priced split: the legs, in the order they must execute, and the combined figures. */
57
+ export type PulseXSplitQuote = {
58
+ readonly legs: readonly PulseXSplitLeg[];
59
+ /** What all legs deliver together. */
60
+ readonly deliveredAmount: bigint;
61
+ /** What the best single path delivers for the same trade, for comparison. */
62
+ readonly singlePathDeliveredAmount: bigint;
63
+ /**
64
+ * Price impact of the COMBINED trade, in percent, against the best marginal
65
+ * rate any candidate offers (see {@link splitPriceImpact}). Null when the
66
+ * reference trade rounds away.
67
+ */
68
+ readonly priceImpactPercent: number | null;
69
+ };
70
+ /**
71
+ * How many equal chunks the search divides the trade into: 20, so legs move
72
+ * in 5% steps. Finer steps buy a little output for proportionally more work.
73
+ */
74
+ export declare const PULSEX_SPLIT_CHUNKS = 20;
75
+ /** The most legs one split may have. Each leg is one more router call to sign or batch, and one more swap's gas. */
76
+ export declare const PULSEX_SPLIT_MAX_LEGS = 3;
77
+ /** How many candidate paths, best single-path output first, the search may spread the trade over. */
78
+ export declare const PULSEX_SPLIT_CANDIDATE_LIMIT = 4;
79
+ /**
80
+ * The gain a split must show over the best single path before it is chosen,
81
+ * in basis points of the single path's output: 25, or 0.25%.
82
+ *
83
+ * GAS IS REAL. Each extra leg costs roughly one more swap's gas, and a
84
+ * batching contract adds its own overhead. A split that wins by a rounding
85
+ * margin buys the reader nothing but a larger fee. The figure is a judgement,
86
+ * not a measurement; revisit it once an execution path has a real gas cost.
87
+ */
88
+ export declare const PULSEX_SPLIT_MINIMUM_GAIN_BASIS_POINTS = 25n;
89
+ /**
90
+ * Executes legs one after another against shared pool state, exactly as
91
+ * sequential router calls would. A leg that crosses a pool an earlier leg
92
+ * already traded sees that pool's moved reserves, in whichever direction the
93
+ * earlier leg traded it.
94
+ *
95
+ * Each trade adds its full input to the pool and removes its output, the
96
+ * Uniswap-V2 accounting. See the open risk in the report: if PulseX moves
97
+ * part of its fee out of the pool during a swap, a shared pool is a few
98
+ * hundredths of a percent shallower here than on chain.
99
+ *
100
+ * @param legs - each candidate with the amount it spends, in execution order
101
+ * @returns what each leg delivers, in the same order
102
+ */
103
+ export declare const executePulseXSplitInOrder: (legs: readonly {
104
+ readonly candidate: PulseXSplitCandidate;
105
+ readonly amountIn: bigint;
106
+ }[]) => readonly bigint[];
107
+ /**
108
+ * The candidates a split may use: the {@link PULSEX_SPLIT_CANDIDATE_LIMIT}
109
+ * best by single-path output for the whole trade. A candidate that delivers
110
+ * nothing is dropped. Ties go to the shorter path, then the earlier
111
+ * candidate, so the ranking is deterministic.
112
+ *
113
+ * @param candidates - every priced path, on every venue
114
+ * @param amountIn - the whole trade
115
+ * @returns the ranked candidates, best first, each with its single-path output
116
+ */
117
+ export declare const rankPulseXSplitCandidates: (candidates: readonly PulseXSplitCandidate[], amountIn: bigint) => readonly {
118
+ readonly candidate: PulseXSplitCandidate;
119
+ readonly deliveredAmount: bigint;
120
+ }[];
121
+ /**
122
+ * Spreads a trade over ranked candidates, one chunk at a time. Each chunk goes
123
+ * to the candidate whose share, once raised by that chunk, makes the WHOLE
124
+ * split deliver the most — the split is re-executed in full each time
125
+ * ({@link executePulseXSplitInOrder}), so a shared pool is always priced with
126
+ * the reserves every leg leaves behind. A candidate not yet used is skipped
127
+ * once {@link PULSEX_SPLIT_MAX_LEGS} legs exist. Ties go to the earlier
128
+ * candidate.
129
+ *
130
+ * BOUNDED AND DETERMINISTIC. Work per quote is at most
131
+ * `PULSEX_SPLIT_CHUNKS × PULSEX_SPLIT_CANDIDATE_LIMIT` full executions (80),
132
+ * each of at most `PULSEX_SPLIT_MAX_LEGS` legs of at most three pools: under
133
+ * 750 constant-product evaluations, no network call.
134
+ *
135
+ * On pools that share nothing, each candidate's output is concave in its
136
+ * share, so this greedy fill reaches the best allocation on the chunk grid.
137
+ * Shared pools break that guarantee; {@link bestPulseXSplitQuote} still never
138
+ * returns a split worse than the best single path.
139
+ *
140
+ * @param candidates - ranked candidates, best first
141
+ * @param amountIn - the whole trade
142
+ * @returns the share of the trade each candidate spends, in candidate order
143
+ */
144
+ export declare const allocatePulseXSplit: (candidates: readonly PulseXSplitCandidate[], amountIn: bigint) => readonly bigint[];
145
+ /**
146
+ * Price impact of a combined trade, in percent, measured with the same
147
+ * reference size the single-path figure uses
148
+ * (`amountIn / PULSEX_MARGINAL_REFERENCE_DIVISOR`).
149
+ *
150
+ * THE REFERENCE IS THE BEST MARGINAL RATE OF ANY CANDIDATE, not a blend of
151
+ * the legs' own rates. A split that leans on a path with a worse spot price
152
+ * pays for that, and the figure shows it. A blend would hide it.
153
+ *
154
+ * @param options.candidates - every ranked candidate
155
+ * @param options.amountIn - the whole trade
156
+ * @param options.deliveredAmount - what the combined trade delivers
157
+ * @returns the impact in percent, or null when the reference trade rounds away
158
+ */
159
+ export declare const splitPriceImpact: (options: {
160
+ readonly candidates: readonly PulseXSplitCandidate[];
161
+ readonly amountIn: bigint;
162
+ readonly deliveredAmount: bigint;
163
+ }) => number | null;
164
+ /**
165
+ * The best split of a PulseX trade, or null when no split beats the best
166
+ * single path by {@link PULSEX_SPLIT_MINIMUM_GAIN_BASIS_POINTS}.
167
+ *
168
+ * NEVER WORSE THAN ONE PATH. A null answer means "take the best single path";
169
+ * this never returns a split that delivers less than that path.
170
+ *
171
+ * @param candidates - every priced path, on every venue
172
+ * @param amountIn - the whole trade
173
+ * @returns the split with its legs in execution order, or null
174
+ */
175
+ export declare const bestPulseXSplitQuote: (candidates: readonly PulseXSplitCandidate[], amountIn: bigint) => PulseXSplitQuote | null;