@gibs/bridge-sdk 1.17.0 → 1.18.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 (44) hide show
  1. package/README.md +52 -1
  2. package/dist/cjs/config.d.ts +18 -3
  3. package/dist/cjs/config.js +49 -17
  4. package/dist/cjs/execution.d.ts +17 -4
  5. package/dist/cjs/execution.js +40 -13
  6. package/dist/cjs/index.d.ts +1 -0
  7. package/dist/cjs/index.js +1 -0
  8. package/dist/cjs/prices.d.ts +10 -0
  9. package/dist/cjs/prices.js +54 -2
  10. package/dist/cjs/routing/carrier-provider-key.d.ts +31 -0
  11. package/dist/cjs/routing/carrier-provider-key.js +42 -0
  12. package/dist/cjs/routing/carriers/pulsex.d.ts +50 -25
  13. package/dist/cjs/routing/carriers/pulsex.js +135 -85
  14. package/dist/cjs/routing/index.d.ts +6 -2
  15. package/dist/cjs/routing/index.js +9 -1
  16. package/dist/cjs/routing/route-data.d.ts +47 -5
  17. package/dist/cjs/routing/search/plan-route-data.d.ts +17 -1
  18. package/dist/cjs/routing/search/plan-route-data.js +30 -7
  19. package/dist/cjs/routing/swap-transactions.d.ts +7 -3
  20. package/dist/cjs/routing/swap-transactions.js +6 -4
  21. package/dist/cjs/swap-venues.d.ts +43 -0
  22. package/dist/cjs/swap-venues.js +29 -0
  23. package/dist/config.d.ts +18 -3
  24. package/dist/config.js +48 -16
  25. package/dist/execution.d.ts +17 -4
  26. package/dist/execution.js +41 -14
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.js +1 -0
  29. package/dist/prices.d.ts +10 -0
  30. package/dist/prices.js +52 -1
  31. package/dist/routing/carrier-provider-key.d.ts +31 -0
  32. package/dist/routing/carrier-provider-key.js +37 -0
  33. package/dist/routing/carriers/pulsex.d.ts +50 -25
  34. package/dist/routing/carriers/pulsex.js +133 -84
  35. package/dist/routing/index.d.ts +6 -2
  36. package/dist/routing/index.js +3 -1
  37. package/dist/routing/route-data.d.ts +47 -5
  38. package/dist/routing/search/plan-route-data.d.ts +17 -1
  39. package/dist/routing/search/plan-route-data.js +30 -7
  40. package/dist/routing/swap-transactions.d.ts +7 -3
  41. package/dist/routing/swap-transactions.js +7 -5
  42. package/dist/swap-venues.d.ts +43 -0
  43. package/dist/swap-venues.js +24 -0
  44. package/package.json +3 -3
package/README.md CHANGED
@@ -432,7 +432,8 @@ if (plan.kind === 'refused') throw new Error(plan.refusal.reason);
432
432
  non-zero allowance is reset to zero first, because some tokens refuse to change
433
433
  one non-zero allowance into another.
434
434
  - **Spenders come from fixed tables.** The plan approves only the omnibridge
435
- mediator and the PulseX router from this package's configuration, LI.FI's
435
+ mediator and the swap routers in the swap venues (PulseX by default, see
436
+ "Overriding swap routers"), LI.FI's
436
437
  Diamond (`lifiDiamondAddressByChainId`, plus the LI.FI execution guard), and
437
438
  Relay's depository (`relayDepositoryByChainId`). A quote that names any other
438
439
  spender or deposit target is refused. NEAR Intents may name no spender at all.
@@ -440,6 +441,56 @@ if (plan.kind === 'refused') throw new Error(plan.refusal.reason);
440
441
  asked for your wallet, in `sections[i].aggregator`. The candidate's own quote
441
442
  priced the route on screen and is never signed.
442
443
 
444
+ ## Overriding swap routers
445
+
446
+ The package swaps on PulseX V1 and V2 on PulseChain by default. You can swap on
447
+ other routers, on any chain. Describe each router as a `SwapVenue` and pass the
448
+ venues to the route search and to execution. Today the only `kind` is
449
+ `'uniswap-v2'`: a router with `getAmountsOut` and the three
450
+ `swapExact...` functions, a factory with `getPair`, and pairs with
451
+ `getReserves`.
452
+
453
+ ```ts
454
+ import {
455
+ type SwapVenues,
456
+ defaultSwapVenues,
457
+ planCandidateExecution,
458
+ resolveSwapVenues,
459
+ } from '@gibs/bridge-sdk';
460
+
461
+ const swapVenues: SwapVenues = {
462
+ // Swap on Ethereum, on Uniswap V2.
463
+ 1: [{
464
+ kind: 'uniswap-v2',
465
+ chainId: 1,
466
+ router: '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D',
467
+ factory: '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f',
468
+ feeBasisPoints: 30, // 29 for PulseX, 30 for Uniswap V2 forks
469
+ label: 'Uniswap V2',
470
+ }],
471
+ // An empty list switches swaps off on PulseChain.
472
+ 369: [],
473
+ };
474
+
475
+ // What the engine will use: your chains replace the defaults, other chains keep them.
476
+ const venues = resolveSwapVenues(swapVenues);
477
+ console.log(Object.keys(defaultSwapVenues)); // ['369']
478
+ ```
479
+
480
+ - **A chain you name replaces that chain's default list.** A chain you leave out
481
+ keeps its defaults. An empty list disables swaps on that chain.
482
+ - **A venue on any chain builds swap steps there.** The rule that aggregators
483
+ never receive PulseChain is separate and does not change.
484
+ - **Each swap step records the venue it was priced on.** Signing uses that
485
+ venue's router, also as the approval spender. Pass the same `swapVenues` to
486
+ `planExecution`, `planCandidateExecution` and `readPulseXSectionQuote` that
487
+ you gave the search. A router that is not in the venues throws an error. The
488
+ package never falls back to PulseX.
489
+ - **The LI.FI execution guard does not change.** It checks the LI.FI Diamond
490
+ address and has no override.
491
+ - `uniV2Routers` and `uniV2Factories` stay exported. They are frozen and derive
492
+ from `defaultSwapVenues`.
493
+
443
494
  ## Manual claims
444
495
 
445
496
  A crossing from PulseChain to Ethereum or BNB Smart Chain ends with a release on
@@ -1,4 +1,5 @@
1
1
  import { type Hex } from 'viem';
2
+ import type { SwapVenues } from './swap-venues.js';
2
3
  import type { BridgeKey, Token, TokenOut } from './types.js';
3
4
  /**
4
5
  * Add here your chain id as hex, be sure to add your chain in the chainsMetadata object too
@@ -59,8 +60,22 @@ export declare const pulsechainTokenAddresses: Readonly<{
59
60
  }>;
60
61
  /** A map of chains to deprecated native asset out addresses */
61
62
  export declare const deprecatedNativeAssetOut: Partial<Record<Chains, Set<Hex>>>;
62
- /** A map of chains to uni v2 router addresses */
63
- export declare const uniV2Routers: Record<Chains, Hex[]>;
63
+ /**
64
+ * The swap venues the package ships with: PulseX V1 then V2 on PulseChain.
65
+ * Frozen. A caller who wants other routers passes its own
66
+ * {@link SwapVenues} (see `resolveSwapVenues`); this table is never mutated.
67
+ *
68
+ * Each factory is the router's own `router.factory()` read on chain — see
69
+ * {@link uniV2Factories}'s doc. The fee is 29 basis points for both
70
+ * deployments (measured, same doc).
71
+ */
72
+ export declare const defaultSwapVenues: SwapVenues;
73
+ /**
74
+ * A map of chains to uni v2 router addresses. Frozen. PulseChain's entries
75
+ * derive from {@link defaultSwapVenues}, so the two cannot drift. Kept for
76
+ * compatibility; to swap on other routers pass `swapVenues` instead.
77
+ */
78
+ export declare const uniV2Routers: Readonly<Record<Chains, readonly Hex[]>>;
64
79
  /**
65
80
  * Each {@link uniV2Routers} entry's OWN factory address, same index, same
66
81
  * chain — `router.factory()` read live from `one.valve.city` on 2026-09-24.
@@ -85,7 +100,7 @@ export declare const uniV2Routers: Record<Chains, Hex[]>;
85
100
  * salt collision) and worth this note so a future reader does not assume a
86
101
  * transcription error and "fix" it back to something wrong.
87
102
  */
88
- export declare const uniV2Factories: Record<Chains, Hex[]>;
103
+ export declare const uniV2Factories: Readonly<Record<Chains, readonly Hex[]>>;
89
104
  /** A map of chains to native token symbols */
90
105
  export declare const nativeTokenSymbol: Record<Chains, string>;
91
106
  /** A map of chains to native token names */
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FOREIGN_TO_HOME_FEE = exports.HOME_TO_FOREIGN_FEE = exports.bridgeConfigs = exports.startBlocks = exports.chainIds = exports.chains = exports.canChangeUnwrap = exports.isUnwrappable = exports.isNative = exports.defaultAssetIn = exports.pathway = exports.pathwayGroup = exports.inferBridgeKey = exports.isTestnetChain = exports.validBridgeKeys = exports.testnetPathways = exports.pathways = exports.nativeTokenName = exports.nativeTokenSymbol = exports.uniV2Factories = exports.uniV2Routers = exports.deprecatedNativeAssetOut = exports.pulsechainTokenAddresses = exports.nativeAssetOut = exports.Providers = exports.toChainKey = exports.toChain = exports.chainIdToKey = exports.Chains = void 0;
3
+ exports.FOREIGN_TO_HOME_FEE = exports.HOME_TO_FOREIGN_FEE = exports.bridgeConfigs = exports.startBlocks = exports.chainIds = exports.chains = exports.canChangeUnwrap = exports.isUnwrappable = exports.isNative = exports.defaultAssetIn = exports.pathway = exports.pathwayGroup = exports.inferBridgeKey = exports.isTestnetChain = exports.validBridgeKeys = exports.testnetPathways = exports.pathways = exports.nativeTokenName = exports.nativeTokenSymbol = exports.uniV2Factories = exports.uniV2Routers = exports.defaultSwapVenues = exports.deprecatedNativeAssetOut = exports.pulsechainTokenAddresses = exports.nativeAssetOut = exports.Providers = exports.toChainKey = exports.toChain = exports.chainIdToKey = exports.Chains = void 0;
4
4
  const viem_1 = require("viem");
5
5
  (function (Chains) {
6
6
  Chains["PLS"] = "0x171";
@@ -72,12 +72,13 @@ exports.pulsechainTokenAddresses = Object.freeze({
72
72
  exports.deprecatedNativeAssetOut = {
73
73
  [exports.Chains.PLS]: new Set(['0x97Ac4a2439A47c07ad535bb1188c989dae755341']),
74
74
  };
75
- /** A map of chains to uni v2 router addresses */
76
- exports.uniV2Routers = {
77
- [exports.Chains.PLS]: [
78
- '0x98bf93ebf5c380C0e6Ae8e192A7e2AE08edAcc02',
79
- '0x165C3410fC91EF562C50559f7d2289fEbed552d9',
80
- ],
75
+ /**
76
+ * Routers on chains where this package knows the router but has not verified
77
+ * a factory, so no {@link SwapVenue} can be built for them. They stay in
78
+ * {@link uniV2Routers} for the native-price reads (`chain-info.ts`,
79
+ * `readAmountOut`) that need only a router.
80
+ */
81
+ const routersWithoutVenue = {
81
82
  [exports.Chains.ETH]: ['0x7a250d5630b4cf539739df2c5dacb4c659f2488d'],
82
83
  [exports.Chains.BNB]: ['0x05fF2B0DB69458A0750badebc4f9e13aDd608C7F'],
83
84
  [exports.Chains.SEP]: ['0xeE567Fe1712Faf6149d80dA1E6934E354124CfE3'],
@@ -86,6 +87,43 @@ exports.uniV2Routers = {
86
87
  '0x636f6407B90661b73b1C0F7e24F4C79f624d0738',
87
88
  ],
88
89
  };
90
+ /**
91
+ * The swap venues the package ships with: PulseX V1 then V2 on PulseChain.
92
+ * Frozen. A caller who wants other routers passes its own
93
+ * {@link SwapVenues} (see `resolveSwapVenues`); this table is never mutated.
94
+ *
95
+ * Each factory is the router's own `router.factory()` read on chain — see
96
+ * {@link uniV2Factories}'s doc. The fee is 29 basis points for both
97
+ * deployments (measured, same doc).
98
+ */
99
+ exports.defaultSwapVenues = Object.freeze({
100
+ [Number(exports.Chains.PLS)]: Object.freeze([
101
+ Object.freeze({
102
+ kind: 'uniswap-v2',
103
+ chainId: Number(exports.Chains.PLS),
104
+ router: '0x98bf93ebf5c380C0e6Ae8e192A7e2AE08edAcc02',
105
+ factory: '0x1715a3E4A142d8b698131108995174F37aEBA10D',
106
+ feeBasisPoints: 29,
107
+ label: 'PulseX V1',
108
+ }),
109
+ Object.freeze({
110
+ kind: 'uniswap-v2',
111
+ chainId: Number(exports.Chains.PLS),
112
+ router: '0x165C3410fC91EF562C50559f7d2289fEbed552d9',
113
+ factory: '0x29eA7545DEf87022BAdc76323F373EA1e707C523',
114
+ feeBasisPoints: 29,
115
+ label: 'PulseX V2',
116
+ }),
117
+ ]),
118
+ });
119
+ /** One chain's routers: its default venues' routers, else the router-only entries. */
120
+ const defaultRoutersFor = (chain) => exports.defaultSwapVenues[Number(chain)]?.map((venue) => venue.router) ?? routersWithoutVenue[chain] ?? [];
121
+ /**
122
+ * A map of chains to uni v2 router addresses. Frozen. PulseChain's entries
123
+ * derive from {@link defaultSwapVenues}, so the two cannot drift. Kept for
124
+ * compatibility; to swap on other routers pass `swapVenues` instead.
125
+ */
126
+ exports.uniV2Routers = Object.freeze(Object.fromEntries(Object.values(exports.Chains).map((chain) => [chain, Object.freeze([...defaultRoutersFor(chain)])])));
89
127
  /**
90
128
  * Each {@link uniV2Routers} entry's OWN factory address, same index, same
91
129
  * chain — `router.factory()` read live from `one.valve.city` on 2026-09-24.
@@ -110,16 +148,10 @@ exports.uniV2Routers = {
110
148
  * salt collision) and worth this note so a future reader does not assume a
111
149
  * transcription error and "fix" it back to something wrong.
112
150
  */
113
- exports.uniV2Factories = {
114
- [exports.Chains.PLS]: [
115
- '0x1715a3E4A142d8b698131108995174F37aEBA10D',
116
- '0x29eA7545DEf87022BAdc76323F373EA1e707C523',
117
- ],
118
- [exports.Chains.ETH]: [],
119
- [exports.Chains.BNB]: [],
120
- [exports.Chains.SEP]: [],
121
- [exports.Chains.V4PLS]: [],
122
- };
151
+ exports.uniV2Factories = Object.freeze(Object.fromEntries(Object.values(exports.Chains).map((chain) => [
152
+ chain,
153
+ Object.freeze((exports.defaultSwapVenues[Number(chain)] ?? []).map((venue) => venue.factory)),
154
+ ])));
123
155
  /** A map of chains to native token symbols */
124
156
  exports.nativeTokenSymbol = {
125
157
  [exports.Chains.PLS]: 'PLS',
@@ -1,5 +1,6 @@
1
1
  import { type Hex, type PublicClient } from 'viem';
2
2
  import type { TokenBridgeInfo } from './chain-info.js';
3
+ import { type SwapVenues } from './swap-venues.js';
3
4
  import { type AggregatorOriginRefusal, type ConsolidatedQuote, type ConsolidatedQuoteRequest } from './consolidated-quote.js';
4
5
  import type { FeeType } from './fee-type.js';
5
6
  import type { RouteCandidate } from './routing/candidate.js';
@@ -20,7 +21,7 @@ import type { BridgeKey, Token } from './types.js';
20
21
  * EVERY SPENDER IS ONE THIS CODE ALREADY KNOWS. Approvals are for the exact
21
22
  * amount spent, never unlimited, and go only to:
22
23
  * - the omnibridge pathway's `from` mediator, read from `config.ts`;
23
- * - the PulseX router the edge was quoted on, read from `config.ts`;
24
+ * - the swap router the edge was quoted on, found in the swap venues (the defaults in `config.ts` unless the caller passes its own);
24
25
  * - an aggregator spender that `aggregatorOriginCalls` checked against a
25
26
  * fixed table for its provider and chain (LI.FI's Diamond, Relay's
26
27
  * depository). A quote cannot name its own spender.
@@ -212,12 +213,15 @@ export type ExecutionPlan = {
212
213
  * @param options.account - the account that signs every transaction
213
214
  * @param options.legs - the legs, in the order the wallet signs them
214
215
  * @param options.readAllowance - reads an allowance on chain; see {@link AllowanceReader}
216
+ * @param options.swapVenues - the routers swaps may sign on, merged over the defaults; a swap whose router is in none of them throws
215
217
  * @returns the ordered transactions, or the first leg that cannot be planned
218
+ * @throws when a swap leg's router is not in the swap venues
216
219
  */
217
- export declare const planExecution: ({ account, legs, readAllowance, }: {
220
+ export declare const planExecution: ({ account, legs, readAllowance, swapVenues, }: {
218
221
  readonly account: Hex;
219
222
  readonly legs: readonly ExecutionLeg[];
220
223
  readonly readAllowance: AllowanceReader;
224
+ readonly swapVenues?: SwapVenues;
221
225
  }) => Promise<ExecutionPlan>;
222
226
  /**
223
227
  * What the caller supplies for one signature section of a candidate. Each
@@ -270,6 +274,12 @@ export type CandidateExecutionInputs = {
270
274
  readonly readAllowance: AllowanceReader;
271
275
  /** The resolver the candidate was built with; turns a plan's graph node into its token. */
272
276
  readonly resolveToken: ResolveNodeToken;
277
+ /**
278
+ * The routers swaps may sign on, merged over the defaults: a chain named
279
+ * here replaces that chain's default list. Pass the SAME venues the route
280
+ * search used. A swap whose router is in none of them throws.
281
+ */
282
+ readonly swapVenues?: SwapVenues;
273
283
  /**
274
284
  * The index of the section that `sections[0]` plans. It defaults to 0.
275
285
  * Pass it to plan a later section after the earlier ones landed, with the
@@ -337,10 +347,13 @@ export declare const planCandidateExecution: (inputs: CandidateExecutionInputs)
337
347
  * @param options.hop - the swap hop; its recorded path and edge name what to read
338
348
  * @param options.amountIn - the amount the swap spends, in base units
339
349
  * @param options.client - a client that can call `readContract`
340
- * @returns the amount the last token of the path delivers, or null when the hop records no path, names no known router, the amount is not positive, or the read fails or returns nothing
350
+ * @param options.swapVenues - the routers swaps may sign on, merged over the defaults
351
+ * @returns the amount the last token of the path delivers, or null when the hop records no path, the amount is not positive, or the read fails or returns nothing
352
+ * @throws when the hop records a path but its router is not in the swap venues
341
353
  */
342
- export declare const readPulseXSectionQuote: ({ hop, amountIn, client, }: {
354
+ export declare const readPulseXSectionQuote: ({ hop, amountIn, client, swapVenues, }: {
343
355
  readonly hop: Pick<PlannedHop, "edge" | "outcome">;
344
356
  readonly amountIn: bigint;
345
357
  readonly client: Pick<PublicClient, "readContract">;
358
+ readonly swapVenues?: SwapVenues;
346
359
  }) => Promise<bigint | null>;
@@ -37,6 +37,7 @@ exports.readPulseXSectionQuote = exports.planCandidateExecution = exports.planEx
37
37
  const viem_1 = require("viem");
38
38
  const abis = __importStar(require("./abis.js"));
39
39
  const config_js_1 = require("./config.js");
40
+ const swap_venues_js_1 = require("./swap-venues.js");
40
41
  const consolidated_quote_js_1 = require("./consolidated-quote.js");
41
42
  const pulsex_js_1 = require("./routing/carriers/pulsex.js");
42
43
  const swap_transactions_js_1 = require("./routing/swap-transactions.js");
@@ -131,10 +132,30 @@ const omnibridgeCalls = (leg, account) => {
131
132
  delivery: deliveryTermsOf(leg.delivery),
132
133
  };
133
134
  };
134
- const pulseXSwapCalls = (leg) => {
135
+ /**
136
+ * The venue a swap edge was priced on, from the venues execution was given.
137
+ * A router that is not in those venues THROWS: signing on another router, on
138
+ * PulseX most of all, would trade against pools the reader was never shown.
139
+ *
140
+ * @param venues - the venues passed to execution
141
+ * @param edge - the swap edge
142
+ * @returns the venue
143
+ * @throws when the edge's router is not in `venues`, or its chain has no venue
144
+ */
145
+ const requireSwapVenue = (venues, edge) => {
146
+ const venue = (0, pulsex_js_1.venueForEdge)(venues, edge);
147
+ if (venue !== null)
148
+ return venue;
149
+ const router = edge.venue ?? 'the default router';
150
+ throw new Error(`The swap was priced on ${router} on chain ${edge.from.chainId}, but that router is not in the swap venues passed to execution. ` +
151
+ 'Pass the same swapVenues the route search used.');
152
+ };
153
+ const pulseXSwapCalls = (leg, venues) => {
135
154
  const { edge } = leg.hop;
136
155
  const chainId = edge.from.chainId;
137
- const routerAddress = (0, pulsex_js_1.pulseXRouterForEdge)(edge);
156
+ if (edge.carrier !== 'PulseX')
157
+ return refusal({ reason: 'swap-refused' });
158
+ const routerAddress = requireSwapVenue(venues, edge).router;
138
159
  // The builder refuses a hop that records no priced path, a router other
139
160
  // than the edge's own, and a leg on screen that disagrees.
140
161
  const call = (0, swap_transactions_js_1.buildPulseXSwapTransaction)({
@@ -146,8 +167,9 @@ const pulseXSwapCalls = (leg) => {
146
167
  signedLeg: { kind: 'swap', chainId },
147
168
  chainId,
148
169
  routerAddress,
170
+ venues,
149
171
  });
150
- if (call === null || routerAddress === null)
172
+ if (call === null)
151
173
  return refusal({ reason: 'swap-refused' });
152
174
  const spendsNative = edge.from.address.toLowerCase() === viem_1.zeroAddress;
153
175
  return {
@@ -191,11 +213,11 @@ const aggregatorCalls = (leg) => {
191
213
  delivery: null,
192
214
  };
193
215
  };
194
- const legCalls = (leg, account) => {
216
+ const legCalls = (leg, account, venues) => {
195
217
  if (leg.kind === 'omnibridge')
196
218
  return omnibridgeCalls(leg, account);
197
219
  if (leg.kind === 'pulsex-swap')
198
- return pulseXSwapCalls(leg);
220
+ return pulseXSwapCalls(leg, venues);
199
221
  if (leg.kind === 'wrap')
200
222
  return wrapCalls(leg);
201
223
  return aggregatorCalls(leg);
@@ -220,10 +242,13 @@ const approvalTransactions = async ({ approval, account, readAllowance, }) => {
220
242
  * @param options.account - the account that signs every transaction
221
243
  * @param options.legs - the legs, in the order the wallet signs them
222
244
  * @param options.readAllowance - reads an allowance on chain; see {@link AllowanceReader}
245
+ * @param options.swapVenues - the routers swaps may sign on, merged over the defaults; a swap whose router is in none of them throws
223
246
  * @returns the ordered transactions, or the first leg that cannot be planned
247
+ * @throws when a swap leg's router is not in the swap venues
224
248
  */
225
- const planExecution = async ({ account, legs, readAllowance, }) => {
226
- const calls = legs.map((leg) => legCalls(leg, account));
249
+ const planExecution = async ({ account, legs, readAllowance, swapVenues, }) => {
250
+ const venues = (0, swap_venues_js_1.resolveSwapVenues)(swapVenues);
251
+ const calls = legs.map((leg) => legCalls(leg, account, venues));
227
252
  const refusedIndex = calls.findIndex((entry) => entry.kind === 'refused');
228
253
  const refused = calls[refusedIndex];
229
254
  if (refused !== undefined && refused.kind === 'refused') {
@@ -401,7 +426,7 @@ const sectionWindowProblem = ({ firstSectionIndex, plannedCount, sectionCount, }
401
426
  * @returns the ordered transactions with the deliveries they buy, or the first section that cannot be planned
402
427
  */
403
428
  const planCandidateExecution = async (inputs) => {
404
- const { candidate, account, readAllowance, resolveToken, sections } = inputs;
429
+ const { candidate, account, readAllowance, resolveToken, sections, swapVenues } = inputs;
405
430
  const firstSectionIndex = inputs.firstSectionIndex ?? 0;
406
431
  const plan = candidate.plan;
407
432
  const allSections = plan === undefined ? null : planSections(plan);
@@ -424,7 +449,7 @@ const planCandidateExecution = async (inputs) => {
424
449
  return { kind: 'refused', sectionIndex: firstSectionIndex + refusedIndex, refusal: refused.refusal };
425
450
  }
426
451
  const legs = sectionLegs.flatMap((entry) => (entry.kind === 'leg' ? [entry.leg] : []));
427
- const planned = await (0, exports.planExecution)({ account, legs, readAllowance });
452
+ const planned = await (0, exports.planExecution)({ account, legs, readAllowance, ...(swapVenues === undefined ? {} : { swapVenues }) });
428
453
  if (planned.kind === 'refused') {
429
454
  return { kind: 'refused', sectionIndex: firstSectionIndex + planned.legIndex, refusal: planned.refusal };
430
455
  }
@@ -445,13 +470,15 @@ exports.planCandidateExecution = planCandidateExecution;
445
470
  * @param options.hop - the swap hop; its recorded path and edge name what to read
446
471
  * @param options.amountIn - the amount the swap spends, in base units
447
472
  * @param options.client - a client that can call `readContract`
448
- * @returns the amount the last token of the path delivers, or null when the hop records no path, names no known router, the amount is not positive, or the read fails or returns nothing
473
+ * @param options.swapVenues - the routers swaps may sign on, merged over the defaults
474
+ * @returns the amount the last token of the path delivers, or null when the hop records no path, the amount is not positive, or the read fails or returns nothing
475
+ * @throws when the hop records a path but its router is not in the swap venues
449
476
  */
450
- const readPulseXSectionQuote = async ({ hop, amountIn, client, }) => {
477
+ const readPulseXSectionQuote = async ({ hop, amountIn, client, swapVenues, }) => {
451
478
  const path = (0, pulsex_js_1.pulseXSwapPathForHop)(hop);
452
- const router = (0, pulsex_js_1.pulseXRouterForEdge)(hop.edge);
453
- if (path === null || router === null || amountIn <= 0n)
479
+ if (path === null || amountIn <= 0n)
454
480
  return null;
481
+ const router = requireSwapVenue((0, swap_venues_js_1.resolveSwapVenues)(swapVenues), hop.edge).router;
455
482
  try {
456
483
  const amounts = await client.readContract({
457
484
  address: router,
@@ -17,6 +17,7 @@ export * from './referral.js';
17
17
  export * from './route-steps.js';
18
18
  export * from './routing/index.js';
19
19
  export * from './settings.js';
20
+ export * from './swap-venues.js';
20
21
  export * from './types.js';
21
22
  export * from './claims.js';
22
23
  export * from './execution.js';
package/dist/cjs/index.js CHANGED
@@ -33,6 +33,7 @@ __exportStar(require("./referral.js"), exports);
33
33
  __exportStar(require("./route-steps.js"), exports);
34
34
  __exportStar(require("./routing/index.js"), exports);
35
35
  __exportStar(require("./settings.js"), exports);
36
+ __exportStar(require("./swap-venues.js"), exports);
36
37
  __exportStar(require("./types.js"), exports);
37
38
  __exportStar(require("./claims.js"), exports);
38
39
  __exportStar(require("./execution.js"), exports);
@@ -105,3 +105,13 @@ export declare const resolveTokenPriceUsd: (token: Token, { pulsexPrices, lifiPr
105
105
  pulsexPrices: Map<string, number>;
106
106
  lifiPrices: Map<string, number>;
107
107
  }) => number | null;
108
+ /**
109
+ * The chain's canonical wrapped native address when `token` is the native
110
+ * coin (zero address) or that wrapper itself, and null for every other token.
111
+ * Only the SAME chain's pair is returned: a bridged copy of a wrapper on
112
+ * another chain is a different token and keeps its own price.
113
+ *
114
+ * @param token - the token to test
115
+ * @returns the wrapped native address on the token's own chain, or null
116
+ */
117
+ export declare const sameChainWrappedNative: (token: Token) => Hex | null;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolveTokenPriceUsd = exports.computeUsdPriceFromAmountsOut = exports.pulsexPricePaths = exports.pulsechainOriginAddress = exports.isPulsechain = exports.lifiPriceKey = exports.lowercaseAddress = exports.WPLS_ADDRESS = exports.PULSECHAIN_USD_ANCHOR_DECIMALS = exports.PULSECHAIN_USD_ANCHOR_ADDRESS = exports.PULSECHAIN_TESTNET_CHAIN_ID = exports.PULSECHAIN_CHAIN_ID = void 0;
3
+ exports.sameChainWrappedNative = exports.resolveTokenPriceUsd = exports.computeUsdPriceFromAmountsOut = exports.pulsexPricePaths = exports.pulsechainOriginAddress = exports.isPulsechain = exports.lifiPriceKey = exports.lowercaseAddress = exports.WPLS_ADDRESS = exports.PULSECHAIN_USD_ANCHOR_DECIMALS = exports.PULSECHAIN_USD_ANCHOR_ADDRESS = exports.PULSECHAIN_TESTNET_CHAIN_ID = exports.PULSECHAIN_CHAIN_ID = void 0;
4
4
  const viem_1 = require("viem");
5
5
  const config_js_1 = require("./config.js");
6
6
  /**
@@ -147,6 +147,59 @@ exports.computeUsdPriceFromAmountsOut = computeUsdPriceFromAmountsOut;
147
147
  * @returns the USD price as a number, or null when no source can price it
148
148
  */
149
149
  const resolveTokenPriceUsd = (token, { pulsexPrices, lifiPrices, }) => {
150
+ const wrappedNative = (0, exports.sameChainWrappedNative)(token);
151
+ if (wrappedNative !== null) {
152
+ return resolveNativeFamilyPriceUsd(token, wrappedNative, { pulsexPrices, lifiPrices });
153
+ }
154
+ return resolveOwnPriceUsd(token, { pulsexPrices, lifiPrices });
155
+ };
156
+ exports.resolveTokenPriceUsd = resolveTokenPriceUsd;
157
+ /**
158
+ * The chain's canonical wrapped native address when `token` is the native
159
+ * coin (zero address) or that wrapper itself, and null for every other token.
160
+ * Only the SAME chain's pair is returned: a bridged copy of a wrapper on
161
+ * another chain is a different token and keeps its own price.
162
+ *
163
+ * @param token - the token to test
164
+ * @returns the wrapped native address on the token's own chain, or null
165
+ */
166
+ const sameChainWrappedNative = (token) => {
167
+ const wrapped = config_js_1.nativeAssetOut[(0, config_js_1.toChain)(token.chainId)];
168
+ if (wrapped === undefined) {
169
+ return null;
170
+ }
171
+ const isNativeCoin = sameAddress(token.address, NATIVE_TOKEN_ADDRESS);
172
+ return isNativeCoin || sameAddress(token.address, wrapped) ? wrapped : null;
173
+ };
174
+ exports.sameChainWrappedNative = sameChainWrappedNative;
175
+ /**
176
+ * One price for a native coin and its canonical wrapper. The contract wraps
177
+ * and unwraps 1:1, so two prices would show a value loss on a wrap step that
178
+ * loses nothing.
179
+ *
180
+ * Off PulseChain the native coin's market feed (LI.FI) wins. The wrapper's
181
+ * own price there is the PulseX price of its bridged copy (it carries a
182
+ * PulseChain origin link), which is a derivative market with its own
183
+ * premium or discount. The coin's feed prices the underlying asset. When the
184
+ * feed has no native price, the wrapper's feed price, then its own resolved
185
+ * price, stand in.
186
+ *
187
+ * On PulseChain both are priced by PulseX through the WPLS path, so the
188
+ * wrapper's entry is used and the coin's entry stands in when the wrapper was
189
+ * not in the priced set.
190
+ */
191
+ const resolveNativeFamilyPriceUsd = (token, wrappedNative, maps) => {
192
+ const wrapped = { ...token, address: wrappedNative };
193
+ if ((0, exports.isPulsechain)(token.chainId)) {
194
+ return (resolveOwnPriceUsd(wrapped, maps) ??
195
+ resolveOwnPriceUsd({ ...token, address: NATIVE_TOKEN_ADDRESS }, maps));
196
+ }
197
+ return (maps.lifiPrices.get((0, exports.lifiPriceKey)(token.chainId, NATIVE_TOKEN_ADDRESS)) ??
198
+ maps.lifiPrices.get((0, exports.lifiPriceKey)(token.chainId, wrappedNative)) ??
199
+ resolveOwnPriceUsd(wrapped, maps));
200
+ };
201
+ /** The token's price by its own address, with no native-wrapper pairing. */
202
+ const resolveOwnPriceUsd = (token, { pulsexPrices, lifiPrices, }) => {
150
203
  if ((0, exports.isPulsechain)(token.chainId)) {
151
204
  if (sameAddress(token.address, exports.PULSECHAIN_USD_ANCHOR_ADDRESS)) {
152
205
  return 1;
@@ -169,4 +222,3 @@ const resolveTokenPriceUsd = (token, { pulsexPrices, lifiPrices, }) => {
169
222
  }
170
223
  return lifiPrices.get((0, exports.lifiPriceKey)(token.chainId, token.address)) ?? null;
171
224
  };
172
- exports.resolveTokenPriceUsd = resolveTokenPriceUsd;
@@ -0,0 +1,31 @@
1
+ import { type ProviderKey } from '../providers.js';
2
+ /**
3
+ * The carrier id of the native-coin wrap and unwrap step
4
+ * (`routing/carriers/wrap.ts`).
5
+ */
6
+ export declare const WRAP_CARRIER_ID = "wrap";
7
+ /**
8
+ * The steps a route can take that no third-party provider performs. A step of
9
+ * this kind is drawn from its own mark, never from a provider's.
10
+ */
11
+ export type CarrierStepKind = 'wrap';
12
+ /**
13
+ * The one place an engine carrier id becomes a {@link ProviderKey}.
14
+ *
15
+ * AN ENGINE CARRIER ID IS NOT A PROVIDER KEY. The engine is a plug-in host: a
16
+ * carrier id is any string, and one of them, `'wrap'`, names a step that no
17
+ * provider performs. Handing a raw id to the logo, glyph, or name tables drew a
18
+ * blank mark and logged an error. Route data resolves the key here, once, and
19
+ * every display site reads the resolved key.
20
+ *
21
+ * @param carrierId - the `carrier` of a planned hop's edge
22
+ * @returns the provider key, or null when no registered provider carries that id
23
+ */
24
+ export declare const carrierProviderKey: (carrierId: string) => ProviderKey | null;
25
+ /**
26
+ * The kind of provider-less step a carrier id performs.
27
+ *
28
+ * @param carrierId - the `carrier` of a planned hop's edge
29
+ * @returns the step kind, or null when the carrier is a provider or unknown
30
+ */
31
+ export declare const carrierStepKind: (carrierId: string) => CarrierStepKind | null;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.carrierStepKind = exports.carrierProviderKey = exports.WRAP_CARRIER_ID = void 0;
4
+ const config_js_1 = require("../config.js");
5
+ const providers_js_1 = require("../providers.js");
6
+ /**
7
+ * The carrier id of the native-coin wrap and unwrap step
8
+ * (`routing/carriers/wrap.ts`).
9
+ */
10
+ exports.WRAP_CARRIER_ID = 'wrap';
11
+ /**
12
+ * Every engine carrier id that IS a provider, mapped to its {@link ProviderKey}.
13
+ * The omnibridge crossings carry the provider value itself; the aggregators
14
+ * carry their registry id; the PulseChain exchange carries its own literal.
15
+ */
16
+ const providerKeyByCarrierId = new Map([
17
+ ...Object.values(config_js_1.Providers).map((provider) => [provider, provider]),
18
+ ...providers_js_1.providerRegistry.map((descriptor) => [descriptor.id, descriptor.id]),
19
+ ['PulseX', 'PulseX'],
20
+ ]);
21
+ /**
22
+ * The one place an engine carrier id becomes a {@link ProviderKey}.
23
+ *
24
+ * AN ENGINE CARRIER ID IS NOT A PROVIDER KEY. The engine is a plug-in host: a
25
+ * carrier id is any string, and one of them, `'wrap'`, names a step that no
26
+ * provider performs. Handing a raw id to the logo, glyph, or name tables drew a
27
+ * blank mark and logged an error. Route data resolves the key here, once, and
28
+ * every display site reads the resolved key.
29
+ *
30
+ * @param carrierId - the `carrier` of a planned hop's edge
31
+ * @returns the provider key, or null when no registered provider carries that id
32
+ */
33
+ const carrierProviderKey = (carrierId) => providerKeyByCarrierId.get(carrierId) ?? null;
34
+ exports.carrierProviderKey = carrierProviderKey;
35
+ /**
36
+ * The kind of provider-less step a carrier id performs.
37
+ *
38
+ * @param carrierId - the `carrier` of a planned hop's edge
39
+ * @returns the step kind, or null when the carrier is a provider or unknown
40
+ */
41
+ const carrierStepKind = (carrierId) => carrierId === exports.WRAP_CARRIER_ID ? 'wrap' : null;
42
+ exports.carrierStepKind = carrierStepKind;