@gibs/bridge-sdk 1.12.0 → 1.13.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 (117) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +234 -432
  3. package/dist/abis.d.ts +1 -0
  4. package/dist/bridge-limits.d.ts +103 -0
  5. package/dist/bridge-limits.js +84 -0
  6. package/dist/chain-info.d.ts +169 -0
  7. package/dist/chain-info.js +410 -0
  8. package/dist/chain-names.d.ts +160 -0
  9. package/dist/chain-names.js +200 -0
  10. package/dist/chains.d.ts +65 -0
  11. package/dist/chains.js +284 -0
  12. package/dist/config.d.ts +216 -0
  13. package/dist/config.js +493 -0
  14. package/dist/consolidated-quote.d.ts +554 -0
  15. package/dist/consolidated-quote.js +773 -0
  16. package/dist/destination-calldata.d.ts +118 -0
  17. package/dist/destination-calldata.js +152 -0
  18. package/dist/ecosystems.d.ts +217 -0
  19. package/dist/ecosystems.js +288 -0
  20. package/dist/fee-type.d.ts +10 -0
  21. package/dist/fee-type.js +9 -0
  22. package/dist/hyperliquid-execution.d.ts +152 -0
  23. package/dist/hyperliquid-execution.js +168 -0
  24. package/dist/image-links.d.ts +117 -0
  25. package/dist/image-links.js +142 -0
  26. package/dist/index.d.ts +20 -0
  27. package/dist/index.js +20 -0
  28. package/dist/lifi-execution-guard.d.ts +199 -0
  29. package/dist/lifi-execution-guard.js +400 -0
  30. package/dist/near-intents-conformance.d.ts +85 -0
  31. package/dist/near-intents-conformance.js +329 -0
  32. package/dist/near-intents-route.d.ts +47 -0
  33. package/dist/near-intents-route.js +265 -0
  34. package/dist/near-intents.d.ts +524 -0
  35. package/dist/near-intents.js +1055 -0
  36. package/dist/origin-deposit.d.ts +326 -0
  37. package/dist/origin-deposit.js +68 -0
  38. package/dist/pre-quote.d.ts +198 -0
  39. package/dist/pre-quote.js +175 -0
  40. package/dist/prices.d.ts +107 -0
  41. package/dist/prices.js +162 -0
  42. package/dist/providers.d.ts +221 -0
  43. package/dist/providers.js +157 -0
  44. package/dist/quote-fees.d.ts +102 -0
  45. package/dist/quote-fees.js +195 -0
  46. package/dist/referral.d.ts +77 -0
  47. package/dist/referral.js +72 -0
  48. package/dist/relay-currencies.d.ts +47 -0
  49. package/dist/relay-currencies.js +89 -0
  50. package/dist/relay-quote.d.ts +301 -0
  51. package/dist/relay-quote.js +983 -0
  52. package/dist/route-steps.d.ts +138 -0
  53. package/dist/route-steps.js +305 -0
  54. package/dist/routing/amounts.d.ts +80 -0
  55. package/dist/routing/amounts.js +120 -0
  56. package/dist/routing/candidate-route.d.ts +43 -0
  57. package/dist/routing/candidate-route.js +45 -0
  58. package/dist/routing/candidate.d.ts +237 -0
  59. package/dist/routing/candidate.js +96 -0
  60. package/dist/routing/carriers/ask-answer.d.ts +58 -0
  61. package/dist/routing/carriers/ask-answer.js +1 -0
  62. package/dist/routing/carriers/omnibridge.d.ts +137 -0
  63. package/dist/routing/carriers/omnibridge.js +419 -0
  64. package/dist/routing/carriers/pulsex.d.ts +111 -0
  65. package/dist/routing/carriers/pulsex.js +404 -0
  66. package/dist/routing/carriers/wrap.d.ts +25 -0
  67. package/dist/routing/carriers/wrap.js +115 -0
  68. package/dist/routing/composed-route-quality.d.ts +103 -0
  69. package/dist/routing/composed-route-quality.js +108 -0
  70. package/dist/routing/entry-chain-anchor.d.ts +88 -0
  71. package/dist/routing/entry-chain-anchor.js +55 -0
  72. package/dist/routing/format.d.ts +31 -0
  73. package/dist/routing/format.js +58 -0
  74. package/dist/routing/index.d.ts +34 -0
  75. package/dist/routing/index.js +42 -0
  76. package/dist/routing/producers.d.ts +770 -0
  77. package/dist/routing/producers.js +1564 -0
  78. package/dist/routing/providers.d.ts +26 -0
  79. package/dist/routing/providers.js +38 -0
  80. package/dist/routing/route-data.d.ts +355 -0
  81. package/dist/routing/route-data.js +1 -0
  82. package/dist/routing/search/compose.d.ts +168 -0
  83. package/dist/routing/search/compose.js +98 -0
  84. package/dist/routing/search/graph.d.ts +639 -0
  85. package/dist/routing/search/graph.js +445 -0
  86. package/dist/routing/search/plan-route-data.d.ts +244 -0
  87. package/dist/routing/search/plan-route-data.js +767 -0
  88. package/dist/routing/search/score.d.ts +324 -0
  89. package/dist/routing/search/score.js +400 -0
  90. package/dist/routing/usd-value.d.ts +17 -0
  91. package/dist/routing/usd-value.js +25 -0
  92. package/dist/settings.d.ts +692 -0
  93. package/dist/settings.js +909 -0
  94. package/dist/ton-execution.d.ts +117 -0
  95. package/dist/ton-execution.js +247 -0
  96. package/dist/tron-execution.d.ts +146 -0
  97. package/dist/tron-execution.js +239 -0
  98. package/dist/types.d.ts +51 -0
  99. package/dist/types.js +1 -0
  100. package/package.json +203 -23
  101. package/src/chain-info.test.ts +0 -968
  102. package/src/chain-info.ts +0 -498
  103. package/src/chains.test.ts +0 -123
  104. package/src/chains.ts +0 -88
  105. package/src/config.test.ts +0 -679
  106. package/src/config.ts +0 -500
  107. package/src/fee-type.test.ts +0 -45
  108. package/src/fee-type.ts +0 -14
  109. package/src/image-links.test.ts +0 -160
  110. package/src/image-links.ts +0 -65
  111. package/src/index.ts +0 -8
  112. package/src/settings.test.ts +0 -2754
  113. package/src/settings.ts +0 -875
  114. package/src/types.ts +0 -48
  115. package/tsconfig.json +0 -17
  116. package/vitest.config.ts +0 -13
  117. /package/{src/abis.ts → dist/abis.js} +0 -0
@@ -0,0 +1,419 @@
1
+ import { multicallRead } from '@gibs/common/multicall';
2
+ import { getAddress, zeroAddress } from 'viem';
3
+ import * as abis from '../../abis.js';
4
+ import { checkEntryAmount, entryLimitsAbi } from '../../bridge-limits.js';
5
+ import { chainsMetadata } from '../../chains.js';
6
+ import { FOREIGN_TO_HOME_FEE, HOME_TO_FOREIGN_FEE, nativeAssetOut, pathway, Providers, pathwayGroup, toChain, } from '../../config.js';
7
+ import { isNativeAsset } from '../../ecosystems.js';
8
+ import { findProviderById } from '../../providers.js';
9
+ import { netDeliveredAmount } from '../amounts.js';
10
+ import { entryChainAnchorsForPair } from '../entry-chain-anchor.js';
11
+ import { BRIDGE_SETTLEMENT_ETA_MINUTES, pulsechainChainId } from '../producers.js';
12
+ import { assetNodeKey } from '../search/graph.js';
13
+ /** Builds an {@link AssetNode} for an Ethereum-Virtual-Machine token. */
14
+ const evmNode = (token) => ({
15
+ chainId: token.chainId,
16
+ address: token.address,
17
+ decimals: token.decimals,
18
+ ecosystem: 'evm',
19
+ });
20
+ /**
21
+ * A directed pair of node keys, joined for use as a `Map` key. Never leaves
22
+ * this module — see `capabilities.ts`'s former `directedPairKey` doc for why
23
+ * a plain `|` join is enough here.
24
+ */
25
+ const directedPairKey = (from, to) => `${assetNodeKey(from)}|${assetNodeKey(to)}`;
26
+ /**
27
+ * Finds which omnibridge provider, if any, has a live pathway carrying funds
28
+ * from `fromChainId` to `toChainId`. Checks BOTH omnibridge providers rather
29
+ * than assuming one: `Providers.PULSECHAIN` carries the Ethereum pathway,
30
+ * `Providers.TOKENSEX` the Binance Smart Chain one.
31
+ */
32
+ const omnibridgeCarrierBetween = (fromChainId, toChainId, isProductionNetwork) => {
33
+ const fromChain = toChain(fromChainId);
34
+ const toChainKey = toChain(toChainId);
35
+ const omnibridgeProviders = [Providers.PULSECHAIN, Providers.TOKENSEX];
36
+ for (const provider of omnibridgeProviders) {
37
+ const group = pathwayGroup(provider, fromChain, isProductionNetwork);
38
+ if (group?.[toChainKey]) {
39
+ return provider;
40
+ }
41
+ }
42
+ return null;
43
+ };
44
+ /**
45
+ * Builds the omnibridge's directed edge map from the bridgeable pairs and the
46
+ * live pathway config, in both directions. INBOUND goes through
47
+ * {@link entryChainAnchorsForPair} so a native coin and its wrapped form both
48
+ * anchor the same PulseChain mint; OUTBOUND needs no aliasing.
49
+ */
50
+ const buildOmnibridgeEdgeMap = (bridgeablePairs, isProductionNetwork) => {
51
+ const edges = new Map();
52
+ for (const pair of bridgeablePairs) {
53
+ const inboundCarrier = omnibridgeCarrierBetween(pair.entryChainId, pair.pulsechainToken.chainId, isProductionNetwork);
54
+ if (inboundCarrier !== null) {
55
+ const anchors = entryChainAnchorsForPair({
56
+ entryChainToken: pair.entryChainToken,
57
+ pulsechainToken: pair.pulsechainToken,
58
+ entryChainId: pair.entryChainId,
59
+ });
60
+ for (const anchor of anchors) {
61
+ edges.set(directedPairKey(evmNode(anchor.token), evmNode(anchor.landsAs)), inboundCarrier);
62
+ }
63
+ }
64
+ const outboundCarrier = omnibridgeCarrierBetween(pair.pulsechainToken.chainId, pair.entryChainId, isProductionNetwork);
65
+ if (outboundCarrier !== null) {
66
+ edges.set(directedPairKey(evmNode(pair.pulsechainToken), evmNode(pair.entryChainToken)), outboundCarrier);
67
+ }
68
+ }
69
+ return edges;
70
+ };
71
+ /**
72
+ * Resolves one edge's crossing details from the live pathway config, or null
73
+ * when no pathway matches — the same "no live pathway" case `edgesFrom`
74
+ * already prunes, kept here as a guard rather than assumed away, because
75
+ * `prepare` walks every node pair itself rather than trusting the caller to
76
+ * hand it only edges this carrier already offered.
77
+ */
78
+ const resolveCrossing = (edge, isProductionNetwork) => {
79
+ const bridgeKey = [edge.carrier, toChain(edge.from.chainId), toChain(edge.to.chainId)];
80
+ const wrappedNativeAddress = nativeAssetOut[toChain(edge.from.chainId)];
81
+ const isNativeEntry = edge.from.address.toLowerCase() === zeroAddress;
82
+ const entryTokenAddress = isNativeEntry && wrappedNativeAddress !== undefined ? getAddress(wrappedNativeAddress) : getAddress(edge.from.address);
83
+ const path = pathway(bridgeKey, isProductionNetwork, entryTokenAddress);
84
+ if (path === undefined)
85
+ return null;
86
+ const feeManagerHostChainId = path.feeManager === 'from' ? edge.from.chainId : edge.to.chainId;
87
+ const feeManagerHostAddress = path.feeManager === 'from' ? path.from : path.to;
88
+ return {
89
+ edge,
90
+ entryTokenAddress,
91
+ entryMediator: path.from,
92
+ limitsChainId: edge.from.chainId,
93
+ feeManagerHostAddress,
94
+ feeManagerHostChainId,
95
+ feeTypeKey: path.toHome ? FOREIGN_TO_HOME_FEE : HOME_TO_FOREIGN_FEE,
96
+ };
97
+ };
98
+ /** Groups values by a computed chain id, preserving the deduped-by-`keyOf` invariant callers rely on. */
99
+ const groupByChainId = (values, chainIdOf) => {
100
+ const groups = new Map();
101
+ for (const value of values) {
102
+ const chainId = chainIdOf(value);
103
+ const group = groups.get(chainId);
104
+ if (group === undefined) {
105
+ groups.set(chainId, [value]);
106
+ }
107
+ else {
108
+ group.push(value);
109
+ }
110
+ }
111
+ return groups;
112
+ };
113
+ /** Stage-one read: `minPerTx`, `maxAvailablePerTx` (an entry mediator's own bounds) and `feeManager()` (the fee contract's address) — every one a fixed-arity view call, so one multicall per chain covers all three kinds at once. */
114
+ const readStageOneBatch = async ({ resolvedCrossings, clientForChain, }) => {
115
+ const limitsByKey = new Map();
116
+ const feeManagerAddressByKey = new Map();
117
+ const limitsRequests = new Map();
118
+ for (const crossing of resolvedCrossings) {
119
+ limitsRequests.set(limitsKey(crossing), {
120
+ chainId: crossing.limitsChainId,
121
+ mediator: crossing.entryMediator,
122
+ token: crossing.entryTokenAddress,
123
+ });
124
+ }
125
+ const feeManagerAddressRequests = new Map();
126
+ for (const crossing of resolvedCrossings) {
127
+ feeManagerAddressRequests.set(feeManagerHostKey(crossing), {
128
+ chainId: crossing.feeManagerHostChainId,
129
+ mediatorHost: crossing.feeManagerHostAddress,
130
+ });
131
+ }
132
+ const requestsByChain = groupByChainId([
133
+ ...Array.from(limitsRequests.entries()).map(([key, request]) => ({ kind: 'limits', key, ...request })),
134
+ ...Array.from(feeManagerAddressRequests.entries()).map(([key, request]) => ({
135
+ kind: 'fee-manager-address',
136
+ key,
137
+ chainId: request.chainId,
138
+ mediatorHost: request.mediatorHost,
139
+ })),
140
+ ], (request) => request.chainId);
141
+ await Promise.all(Array.from(requestsByChain.entries()).map(async ([chainId, requests]) => {
142
+ const client = clientForChain(chainId);
143
+ if (client === null)
144
+ return;
145
+ const results = await multicallRead({
146
+ chain: chainsMetadata[toChain(chainId)],
147
+ client,
148
+ abi: entryLimitsAbi,
149
+ calls: requests.flatMap((request) => request.kind === 'limits'
150
+ ? [
151
+ { functionName: 'minPerTx', args: [request.token], target: request.mediator, allowFailure: true },
152
+ { functionName: 'maxAvailablePerTx', args: [request.token], target: request.mediator, allowFailure: true },
153
+ ]
154
+ : [{ functionName: 'feeManager', abi: abis.inputBridge, args: [], target: request.mediatorHost, allowFailure: true }]),
155
+ });
156
+ let cursor = 0;
157
+ for (const request of requests) {
158
+ if (request.kind === 'limits') {
159
+ const minimumPerTransaction = results[cursor];
160
+ const availablePerTransaction = results[cursor + 1];
161
+ cursor += 2;
162
+ if (typeof minimumPerTransaction === 'bigint' && typeof availablePerTransaction === 'bigint') {
163
+ limitsByKey.set(request.key, { minimumPerTransaction, availablePerTransaction });
164
+ }
165
+ continue;
166
+ }
167
+ const feeManagerAddress = results[cursor];
168
+ cursor += 1;
169
+ // A DECODED address is exactly 42 characters ("0x" + 40 hex digits).
170
+ // `multicallRead` returns the UNDECODED revert bytes on a failed
171
+ // `allowFailure` call — also a `string`, usually a different length
172
+ // (`'0x'` for a bare revert) — so `typeof` alone cannot tell success
173
+ // from failure here the way it does for the `bigint` reads above.
174
+ if (typeof feeManagerAddress === 'string' && feeManagerAddress.length === 42) {
175
+ feeManagerAddressByKey.set(request.key, getAddress(feeManagerAddress));
176
+ }
177
+ }
178
+ }));
179
+ return { limitsByKey, feeManagerAddressByKey };
180
+ };
181
+ /** Stage-two read: `getFee(feeTypeKey, address(0))` on every fee manager contract stage one resolved — needs the address stage one just learned, so it cannot join that multicall. */
182
+ const readStageTwoBatch = async ({ resolvedCrossings, feeManagerAddressByKey, clientForChain, }) => {
183
+ const feeRateByKey = new Map();
184
+ const feeRateRequests = new Map();
185
+ for (const crossing of resolvedCrossings) {
186
+ const feeManagerAddress = feeManagerAddressByKey.get(feeManagerHostKey(crossing));
187
+ if (feeManagerAddress === undefined)
188
+ continue;
189
+ feeRateRequests.set(feeRateKey(crossing, feeManagerAddress), {
190
+ chainId: crossing.feeManagerHostChainId,
191
+ feeManagerAddress,
192
+ feeTypeKey: crossing.feeTypeKey,
193
+ });
194
+ }
195
+ const requestsByChain = groupByChainId(Array.from(feeRateRequests.entries()), ([, request]) => request.chainId);
196
+ await Promise.all(Array.from(requestsByChain.entries()).map(async ([chainId, entries]) => {
197
+ const client = clientForChain(chainId);
198
+ if (client === null)
199
+ return;
200
+ const results = await multicallRead({
201
+ chain: chainsMetadata[toChain(chainId)],
202
+ client,
203
+ abi: abis.feeManager,
204
+ calls: entries.map(([, request]) => ({
205
+ functionName: 'getFee',
206
+ args: [request.feeTypeKey, zeroAddress],
207
+ target: request.feeManagerAddress,
208
+ allowFailure: true,
209
+ })),
210
+ });
211
+ entries.forEach(([key], index) => {
212
+ const rate = results[index];
213
+ if (typeof rate === 'bigint') {
214
+ feeRateByKey.set(key, rate);
215
+ }
216
+ });
217
+ }));
218
+ return feeRateByKey;
219
+ };
220
+ const limitsKey = (crossing) => `${crossing.limitsChainId}|${crossing.entryMediator}|${crossing.entryTokenAddress}`;
221
+ const feeManagerHostKey = (crossing) => `${crossing.feeManagerHostChainId}|${crossing.feeManagerHostAddress}`;
222
+ const feeRateKey = (crossing, feeManagerAddress) => `${crossing.feeManagerHostChainId}|${feeManagerAddress}|${crossing.feeTypeKey}`;
223
+ const preparedEdgeKey = (edge) => `${edge.carrier}|${assetNodeKey(edge.from)}|${assetNodeKey(edge.to)}`;
224
+ /**
225
+ * Batch-reads every entry-mediator bound and fee rate `nodes` implies, once,
226
+ * so {@link createOmnibridgeCarrier}'s `exact` can answer every crossing
227
+ * SYNCHRONOUSLY afterward — the owner's own model, "each individual step and
228
+ * its associated fees can be known ahead of time."
229
+ *
230
+ * TWO MULTICALLS PER CHAIN, NOT ONE: `getFee` needs the fee manager's
231
+ * address, which stage one is what LEARNS — the same two-stage shape
232
+ * `chain-info.ts`'s `loadBridgeFees` already uses for one pathway at a time,
233
+ * generalized here to batch across every crossing the search's node universe
234
+ * touches. `HOME_TO_FOREIGN_FEE`/`FOREIGN_TO_HOME_FEE` are not re-read from
235
+ * the chain — they are fixed `keccak256` constants (`config.ts`), so reading
236
+ * them per pathway (as `loadBridgeFees` still does, defensively) would only
237
+ * spend a third multicall confirming a value that cannot change.
238
+ *
239
+ * WATCH PAYLOAD SIZE (request-payload-size-traps.md). `nodes` is the search
240
+ * graph's own hub-token-restricted universe, not a full token list — this
241
+ * function reads one `minPerTx`/`maxAvailablePerTx` pair per DISTINCT
242
+ * (mediator, token) crossing in that universe, never per token a chain
243
+ * lists. `multicallRead` (`@gibs/common`) chunks by measured encoded size on
244
+ * top of that, so a universe large enough to matter still cannot repeat the
245
+ * 8.5 MB trap that memory records.
246
+ *
247
+ * @param nodes - every node the search might branch through
248
+ * @param edgesFrom - this carrier's own edge enumerator, reused to find every candidate crossing among `nodes`
249
+ * @param isProductionNetwork - see `CreateOmnibridgeCarrierOptions`'s own field
250
+ * @param clientForChain - see `CreateOmnibridgeCarrierOptions`'s own field
251
+ * @returns every resolved crossing's edge key, mapped to what `exact` needs — never thrown; a chain this host has no client for is simply left unanswered
252
+ */
253
+ const prepareOmnibridgeReads = async ({ nodes, edgesFrom, isProductionNetwork, clientForChain, }) => {
254
+ const edges = [];
255
+ for (const from of nodes) {
256
+ for (const to of nodes) {
257
+ if (assetNodeKey(from) === assetNodeKey(to))
258
+ continue;
259
+ edges.push(...edgesFrom(from, to));
260
+ }
261
+ }
262
+ const resolvedCrossings = edges
263
+ .map((edge) => resolveCrossing(edge, isProductionNetwork))
264
+ .filter((resolved) => resolved !== null);
265
+ const preparedByEdgeKey = new Map();
266
+ if (resolvedCrossings.length === 0)
267
+ return preparedByEdgeKey;
268
+ const { limitsByKey, feeManagerAddressByKey } = await readStageOneBatch({ resolvedCrossings, clientForChain });
269
+ const feeRateByKey = await readStageTwoBatch({ resolvedCrossings, feeManagerAddressByKey, clientForChain });
270
+ for (const crossing of resolvedCrossings) {
271
+ const feeManagerAddress = feeManagerAddressByKey.get(feeManagerHostKey(crossing));
272
+ const feePerEther = feeManagerAddress === undefined ? null : (feeRateByKey.get(feeRateKey(crossing, feeManagerAddress)) ?? null);
273
+ preparedByEdgeKey.set(preparedEdgeKey(crossing.edge), {
274
+ limits: limitsByKey.get(limitsKey(crossing)) ?? null,
275
+ feePerEther,
276
+ });
277
+ }
278
+ return preparedByEdgeKey;
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)`;
283
+ /**
284
+ * Builds the omnibridge crossing as a plug-in carrier.
285
+ *
286
+ * @param options - see {@link CreateOmnibridgeCarrierOptions}
287
+ * @returns the carrier — `id: 'omnibridge'`; every EDGE it offers carries the
288
+ * real `'pulsechain'`/`'tokensex'` provider id, since one registration can
289
+ * speak for either entry chain depending on the pair
290
+ */
291
+ export const createOmnibridgeCarrier = (options) => {
292
+ const { bridgeablePairs, isProductionNetwork, readEntryLimits, readFeePerEther, clientForChain } = options;
293
+ const edgeMap = buildOmnibridgeEdgeMap(bridgeablePairs, isProductionNetwork);
294
+ const edgesFrom = (from, to) => {
295
+ if (assetNodeKey(from) === assetNodeKey(to))
296
+ return [];
297
+ const carrier = edgeMap.get(directedPairKey(from, to));
298
+ if (carrier === undefined)
299
+ return [];
300
+ return [{ carrier, from, to, fusesNext: false, origin: 'wallet', floors: [] }];
301
+ };
302
+ // Populated by `prepare`, read by `exact` — see that method's own doc for
303
+ // why a mutable closure here is safe: this carrier is a client-side,
304
+ // one-search-at-a-time object (quote-service.md: the omnibridge crossing
305
+ // is "never built into" the shared, multi-tenant display-quote service),
306
+ // so nothing else can be mutating this map while a search reads it.
307
+ let preparedByEdgeKey = new Map();
308
+ const prepare = async (nodes) => {
309
+ if (clientForChain === undefined)
310
+ return;
311
+ preparedByEdgeKey = await prepareOmnibridgeReads({ nodes, edgesFrom, isProductionNetwork, clientForChain });
312
+ };
313
+ const exact = (edge, amountIn) => {
314
+ const prepared = preparedByEdgeKey.get(preparedEdgeKey(edge));
315
+ if (prepared === undefined)
316
+ return null;
317
+ const verdict = checkEntryAmount({ amount: amountIn, limits: prepared.limits });
318
+ 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
+ };
328
+ }
329
+ return { outcome: 'delivered', deliveredAmount: netDeliveredAmount(amountIn, prepared.feePerEther) };
330
+ };
331
+ const ask = async ({ edge, inputAmount }) => {
332
+ const limits = await readEntryLimits(edge);
333
+ const verdict = checkEntryAmount({ amount: inputAmount, limits });
334
+ 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
+ };
344
+ }
345
+ const feePerEther = await readFeePerEther(edge);
346
+ const deliveredAmount = netDeliveredAmount(inputAmount, feePerEther);
347
+ return {
348
+ ok: true,
349
+ deliveredAmount,
350
+ // An exact local computation: the guaranteed figure IS the expected one.
351
+ minimumDeliveredAmount: deliveredAmount,
352
+ claimedDurationSeconds: BRIDGE_SETTLEMENT_ETA_MINUTES * 60,
353
+ };
354
+ };
355
+ return {
356
+ id: 'omnibridge',
357
+ edgesFrom,
358
+ ask,
359
+ prepare,
360
+ exact,
361
+ isRateLimited: false,
362
+ // Crossing requires the reader's own wallet signature on the entry
363
+ // chain — never a by-hand deposit.
364
+ depositMode: 'wallet-signed',
365
+ sender: 'omnibridge',
366
+ settlementSeconds: BRIDGE_SETTLEMENT_ETA_MINUTES * 60,
367
+ };
368
+ };
369
+ /**
370
+ * Wraps ANY carrier-shaped object (built-in or a host's own) with the
371
+ * omnibridge's OWN fuse rule: whether THIS carrier's execution, landing on
372
+ * arrival, also performs the omnibridge's entry call — so the two hops
373
+ * together cost the reader one signature instead of two.
374
+ *
375
+ * EVERY CONDITION IS A FACT THE PROVIDER'S OWN DESCRIPTOR STATES
376
+ * (`../../providers.ts`), never a name compared here:
377
+ *
378
+ * - it executes destination calls at all (`executesDestinationCalls` — true
379
+ * for LI.FI and Relay, false for NEAR Intents, which delivers to the
380
+ * reader's own wallet and stops);
381
+ * - it can do so FROM this hop's origin chain (`destinationCallOriginChainIds`
382
+ * — LI.FI's contract-calls endpoint is Ethereum only, so from Base or
383
+ * Arbitrum a LI.FI hop is an unfused delivery followed by a crossing the
384
+ * reader signs);
385
+ * - when the entry is the omnibridge's NATIVE one (the provider lands a
386
+ * native coin, and `wrapAndRelayTokens` is payable), it can attach value to
387
+ * the call (`destinationCallCarriesNativeValue`);
388
+ * - and the next hop really IS the omnibridge entry, landing on PulseChain
389
+ * (`relayTokens`/`wrapAndRelayTokens` both MINT there; any other
390
+ * destination is the omnibridge's OUTBOUND leg, which no arrival can make).
391
+ *
392
+ * THIS IS THE ONE PLACE `'pulsechain'`/`'tokensex'`/PulseChain'S CHAIN ID
393
+ * APPEAR IN A `fusesWithNext` CALLBACK — gibs's own registration
394
+ * (`packages/ui/src/lib/state/useRouteSearch.ts`) wraps `createLifiCarrier`/
395
+ * `createRelayCarrier` with this before registering them, so the generic
396
+ * search engine (`@gibs/quotes`'s `capabilities.ts`) never has to know what
397
+ * "the bridge" means.
398
+ *
399
+ * @param carrier - the carrier to wrap; its own shape is preserved, only `fusesWithNext` is added/replaced
400
+ * @returns the same carrier, with a `fusesWithNext` that recognizes the omnibridge entry call
401
+ */
402
+ export const withOmnibridgeEntryFusing = (carrier) => {
403
+ const descriptor = findProviderById(carrier.id);
404
+ return {
405
+ ...carrier,
406
+ fusesWithNext: (nextHop) => {
407
+ if (descriptor === null || !descriptor.executesDestinationCalls)
408
+ return false;
409
+ const nextHopIsOmnibridgeCrossing = nextHop.nextHopCarrier === Providers.PULSECHAIN || nextHop.nextHopCarrier === Providers.TOKENSEX;
410
+ if (!nextHopIsOmnibridgeCrossing || nextHop.nextHopDestinationChainId !== pulsechainChainId)
411
+ return false;
412
+ const origins = descriptor.destinationCallOriginChainIds;
413
+ if (origins !== 'any' && !origins.includes(nextHop.hopFrom.chainId))
414
+ return false;
415
+ const entersNative = isNativeAsset(nextHop.nextHopFrom.address, nextHop.nextHopFrom.ecosystem);
416
+ return !entersNative || descriptor.destinationCallCarriesNativeValue;
417
+ },
418
+ };
419
+ };
@@ -0,0 +1,111 @@
1
+ import { type Hex } from 'viem';
2
+ import type { PublicClient } from 'viem';
3
+ import { type AssetNode, type HopEdge } from '../search/graph.js';
4
+ import type { CarrierAskAnswer, CarrierExactResult } from './ask-answer.js';
5
+ /**
6
+ * One PulseX deployment a swap can execute on: the router the reader signs
7
+ * and the factory whose pools that router trades against. A Uniswap-V2-style
8
+ * router resolves every pool through its OWN factory, so one router call can
9
+ * only ever touch that one deployment's pools — a path can never mix them.
10
+ */
11
+ export type PulseXVenue = {
12
+ readonly router: Hex;
13
+ readonly factory: Hex;
14
+ };
15
+ /**
16
+ * Every PulseX deployment on PulseChain, V1 first: `uniV2Routers` paired with
17
+ * `uniV2Factories` by index. Each factory is the router's own
18
+ * `router.factory()` read on chain — see `uniV2Factories`'s own doc in
19
+ * `config.ts`. A router with no recorded factory is left out, because its
20
+ * pools cannot be read.
21
+ */
22
+ export declare const pulseXVenues: readonly PulseXVenue[];
23
+ /**
24
+ * The router a PulseX edge must be signed on — the one its figure was quoted
25
+ * against. Reads the edge's own {@link HopEdge.venue}; an edge with none is a
26
+ * V1 edge (see {@link defaultPulseXVenue}).
27
+ *
28
+ * @param edge - the edge to sign
29
+ * @returns the router, or null when the edge is not a PulseX edge or names a venue that is not a known PulseX router
30
+ */
31
+ export declare const pulseXRouterForEdge: (edge: Pick<HopEdge, "carrier" | "venue">) => Hex | null;
32
+ /**
33
+ * The router swap path a PulseX hop from `from` to `to` prices and signs
34
+ * along, or null when there is no swap for PulseX to do — including when
35
+ * both ends resolve to the SAME pool token, which is true of PLS<->WPLS
36
+ * (native PLS resolves to {@link wplsAddress} via {@link poolTokenAddress},
37
+ * same as Wrapped PLS itself): that pair is the wrap carrier's job, never a
38
+ * PulseX hop, so this returns null rather than a zero-length or self-paired
39
+ * path.
40
+ *
41
+ * EVERY PULSEX QUOTE AND SIGNATURE IN THIS FILE MUST GO THROUGH THIS ONE
42
+ * FUNCTION — `pulseXCarries`, `prepare`'s pool discovery, and `exact`'s hop
43
+ * pricing all call it, so native PLS resolves the same way in every one of
44
+ * them. Before this existed, `pulseXCarries` and `prepare`/`exact` called
45
+ * `pulseXConversionSwapPath` directly on the raw node address: a native PLS
46
+ * node produced a path that LITERALLY STARTED WITH THE ZERO ADDRESS
47
+ * (`[zeroAddress, wplsAddress, ...]`), which `pulseXCarries` accepted (its
48
+ * length is still >= 2) but for which `prepare` could never find a pool —
49
+ * the factory has no pair for the zero address — so `exact` always answered
50
+ * `null` for a direct native-PLS PulseX hop, and `enumerate.ts` pruned it,
51
+ * leaving only the two-signature wrap-then-swap path.
52
+ *
53
+ * @param from - the hop's origin node
54
+ * @param to - the hop's destination node
55
+ * @returns the swap path, or null when there is nothing for PulseX to swap
56
+ */
57
+ export declare const pulseXPoolPath: (from: AssetNode, to: AssetNode) => readonly Hex[] | null;
58
+ /** What {@link createPulseXCarrier} needs. */
59
+ export type CreatePulseXCarrierOptions = {
60
+ /**
61
+ * Reads PulseX's `getAmountsOut` for one PulseX edge and an input amount,
62
+ * in the edge's `to` token's base units. Returns null when the path has no
63
+ * liquidity.
64
+ *
65
+ * MUST READ THE EDGE'S OWN ROUTER ({@link pulseXRouterForEdge}). A V2 edge
66
+ * answered from V1 would carry V1's figure into a V2 signature.
67
+ */
68
+ readonly readAmountOut: (edge: HopEdge, inputAmount: bigint) => Promise<bigint | null>;
69
+ /**
70
+ * Returns a PulseChain client, for {@link Carrier.prepare}'s own batched
71
+ * reserve reads — never called by `ask`, which keeps using
72
+ * {@link readAmountOut} unchanged.
73
+ *
74
+ * OPTIONAL, ON PURPOSE — see `omnibridge.ts`'s identical `clientForChain`
75
+ * field for why: a host that has not wired this yet gets the carrier's
76
+ * original behavior, `prepare` a no-op and `exact` always `null`.
77
+ *
78
+ * @param chainId - always {@link pulsechainChainId} — PulseX runs nowhere else
79
+ * @returns a client, or null when this host has none
80
+ */
81
+ readonly clientForChain?: (chainId: number) => PublicClient | null;
82
+ /**
83
+ * The PulseX routers this host can SIGN on. The carrier offers an edge on
84
+ * each, and only on these: an edge is always signed on the router it was
85
+ * quoted against ({@link pulseXRouterForEdge}), so a router the host cannot
86
+ * sign on must never be quoted either.
87
+ *
88
+ * Defaults to every known venue ({@link pulseXVenues}: V1 and V2).
89
+ */
90
+ readonly routers?: readonly Hex[];
91
+ };
92
+ /**
93
+ * Builds the PulseX conversion swap as a plug-in carrier.
94
+ *
95
+ * @param options - see {@link CreatePulseXCarrierOptions}
96
+ * @returns the carrier — `id: 'PulseX'`
97
+ */
98
+ export declare const createPulseXCarrier: (options: CreatePulseXCarrierOptions) => {
99
+ id: string;
100
+ edgesFrom: (from: AssetNode, to: AssetNode) => readonly HopEdge[];
101
+ ask: ({ edge, inputAmount }: {
102
+ edge: HopEdge;
103
+ inputAmount: bigint;
104
+ }) => Promise<CarrierAskAnswer>;
105
+ prepare: (nodes: readonly AssetNode[]) => Promise<void>;
106
+ exact: (edge: HopEdge, amountIn: bigint) => CarrierExactResult | null;
107
+ isRateLimited: boolean;
108
+ depositMode: "wallet-signed";
109
+ sender: "swap";
110
+ settlementSeconds: number;
111
+ };