@gibs/bridge-sdk 1.13.1 → 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.
- package/README.md +295 -18
- package/abis/package.json +6 -0
- package/bridge-limits/package.json +6 -0
- package/chain-info/package.json +6 -0
- package/chain-names/package.json +6 -0
- package/chains/package.json +6 -0
- package/claims/package.json +6 -0
- package/config/package.json +6 -0
- package/consolidated-quote/package.json +6 -0
- package/destination-calldata/package.json +6 -0
- package/dist/chain-info.d.ts +1 -1
- package/dist/chain-info.js +11 -12
- package/dist/chains.d.ts +25 -0
- package/dist/chains.js +55 -20
- package/dist/cjs/abis.d.ts +1 -0
- package/dist/cjs/abis.js +19 -0
- package/dist/cjs/bridge-limits.d.ts +103 -0
- package/dist/cjs/bridge-limits.js +89 -0
- package/dist/cjs/chain-info.d.ts +169 -0
- package/dist/cjs/chain-info.js +452 -0
- package/dist/cjs/chain-names.d.ts +160 -0
- package/dist/cjs/chain-names.js +207 -0
- package/dist/cjs/chains.d.ts +90 -0
- package/dist/cjs/chains.js +357 -0
- package/dist/cjs/claims.d.ts +198 -0
- package/dist/cjs/claims.js +182 -0
- package/dist/cjs/config.d.ts +255 -0
- package/dist/cjs/config.js +569 -0
- package/dist/cjs/consolidated-quote.d.ts +648 -0
- package/dist/cjs/consolidated-quote.js +1048 -0
- package/dist/cjs/destination-calldata.d.ts +118 -0
- package/dist/cjs/destination-calldata.js +156 -0
- package/dist/cjs/ecosystems.d.ts +217 -0
- package/dist/cjs/ecosystems.js +298 -0
- package/dist/cjs/execution.d.ts +359 -0
- package/dist/cjs/execution.js +495 -0
- package/dist/cjs/fee-type.d.ts +10 -0
- package/dist/cjs/fee-type.js +10 -0
- package/dist/cjs/hyperliquid-execution.d.ts +152 -0
- package/dist/cjs/hyperliquid-execution.js +175 -0
- package/dist/cjs/image-links.d.ts +117 -0
- package/dist/cjs/image-links.js +153 -0
- package/dist/cjs/index.d.ts +24 -0
- package/dist/cjs/index.js +40 -0
- package/dist/cjs/lifi-execution-guard.d.ts +210 -0
- package/dist/cjs/lifi-execution-guard.js +409 -0
- package/dist/cjs/near-intents-conformance.d.ts +85 -0
- package/dist/cjs/near-intents-conformance.js +337 -0
- package/dist/cjs/near-intents-route.d.ts +47 -0
- package/dist/cjs/near-intents-route.js +303 -0
- package/dist/cjs/near-intents.d.ts +524 -0
- package/dist/cjs/near-intents.js +1071 -0
- package/dist/cjs/origin-deposit.d.ts +326 -0
- package/dist/cjs/origin-deposit.js +82 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/pre-quote.d.ts +198 -0
- package/dist/cjs/pre-quote.js +180 -0
- package/dist/cjs/prices.d.ts +117 -0
- package/dist/cjs/prices.js +224 -0
- package/dist/cjs/providers.d.ts +238 -0
- package/dist/cjs/providers.js +171 -0
- package/dist/cjs/quote-fees.d.ts +102 -0
- package/dist/cjs/quote-fees.js +201 -0
- package/dist/cjs/referral.d.ts +85 -0
- package/dist/cjs/referral.js +85 -0
- package/dist/cjs/relay-currencies.d.ts +47 -0
- package/dist/cjs/relay-currencies.js +95 -0
- package/dist/cjs/relay-depository.d.ts +2 -0
- package/dist/cjs/relay-depository.js +85 -0
- package/dist/cjs/relay-quote.d.ts +301 -0
- package/dist/cjs/relay-quote.js +996 -0
- package/dist/cjs/route-steps.d.ts +138 -0
- package/dist/cjs/route-steps.js +310 -0
- package/dist/cjs/routing/amounts.d.ts +80 -0
- package/dist/cjs/routing/amounts.js +126 -0
- package/dist/cjs/routing/candidate-route.d.ts +43 -0
- package/dist/cjs/routing/candidate-route.js +50 -0
- package/dist/cjs/routing/candidate.d.ts +237 -0
- package/dist/cjs/routing/candidate.js +100 -0
- package/dist/cjs/routing/carrier-provider-key.d.ts +31 -0
- package/dist/cjs/routing/carrier-provider-key.js +42 -0
- package/dist/cjs/routing/carriers/ask-answer.d.ts +77 -0
- package/dist/cjs/routing/carriers/ask-answer.js +2 -0
- package/dist/cjs/routing/carriers/omnibridge.d.ts +137 -0
- package/dist/cjs/routing/carriers/omnibridge.js +459 -0
- package/dist/cjs/routing/carriers/pulsex-paths.d.ts +80 -0
- package/dist/cjs/routing/carriers/pulsex-paths.js +97 -0
- package/dist/cjs/routing/carriers/pulsex-split.d.ts +175 -0
- package/dist/cjs/routing/carriers/pulsex-split.js +193 -0
- package/dist/cjs/routing/carriers/pulsex.d.ts +184 -0
- package/dist/cjs/routing/carriers/pulsex.js +615 -0
- package/dist/cjs/routing/carriers/wrap.d.ts +25 -0
- package/dist/cjs/routing/carriers/wrap.js +119 -0
- package/dist/cjs/routing/composed-route-quality.d.ts +125 -0
- package/dist/cjs/routing/composed-route-quality.js +144 -0
- package/dist/cjs/routing/entry-chain-anchor.d.ts +88 -0
- package/dist/cjs/routing/entry-chain-anchor.js +59 -0
- package/dist/cjs/routing/format.d.ts +31 -0
- package/dist/cjs/routing/format.js +64 -0
- package/dist/cjs/routing/index.d.ts +42 -0
- package/dist/cjs/routing/index.js +142 -0
- package/dist/cjs/routing/producers.d.ts +770 -0
- package/dist/cjs/routing/producers.js +1617 -0
- package/dist/cjs/routing/providers.d.ts +31 -0
- package/dist/cjs/routing/providers.js +48 -0
- package/dist/cjs/routing/route-data.d.ts +397 -0
- package/dist/cjs/routing/route-data.js +2 -0
- package/dist/cjs/routing/search/compose.d.ts +168 -0
- package/dist/cjs/routing/search/compose.js +104 -0
- package/dist/cjs/routing/search/graph.d.ts +668 -0
- package/dist/cjs/routing/search/graph.js +461 -0
- package/dist/cjs/routing/search/plan-route-data.d.ts +285 -0
- package/dist/cjs/routing/search/plan-route-data.js +871 -0
- package/dist/cjs/routing/search/score.d.ts +356 -0
- package/dist/cjs/routing/search/score.js +445 -0
- package/dist/cjs/routing/swap-transactions.d.ts +95 -0
- package/dist/cjs/routing/swap-transactions.js +133 -0
- package/dist/cjs/routing/usd-value.d.ts +17 -0
- package/dist/cjs/routing/usd-value.js +29 -0
- package/dist/cjs/settings.d.ts +785 -0
- package/dist/cjs/settings.js +1074 -0
- package/dist/cjs/swap-venues.d.ts +43 -0
- package/dist/cjs/swap-venues.js +29 -0
- package/dist/cjs/ton-connect.d.ts +98 -0
- package/dist/cjs/ton-connect.js +206 -0
- package/dist/cjs/ton-execution.d.ts +7 -0
- package/dist/cjs/ton-execution.js +23 -0
- package/dist/cjs/ton.d.ts +37 -0
- package/dist/cjs/ton.js +83 -0
- package/dist/cjs/tron-execution.d.ts +146 -0
- package/dist/cjs/tron-execution.js +248 -0
- package/dist/cjs/types.d.ts +51 -0
- package/dist/cjs/types.js +2 -0
- package/dist/claims.d.ts +198 -0
- package/dist/claims.js +140 -0
- package/dist/config.d.ts +42 -3
- package/dist/config.js +87 -21
- package/dist/consolidated-quote.d.ts +109 -15
- package/dist/consolidated-quote.js +310 -52
- package/dist/execution.d.ts +359 -0
- package/dist/execution.js +455 -0
- package/dist/hyperliquid-execution.js +2 -3
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/lifi-execution-guard.d.ts +13 -2
- package/dist/lifi-execution-guard.js +4 -0
- package/dist/near-intents.js +2 -3
- package/dist/pre-quote.d.ts +6 -6
- package/dist/pre-quote.js +10 -14
- package/dist/prices.d.ts +10 -0
- package/dist/prices.js +52 -1
- package/dist/providers.d.ts +17 -0
- package/dist/providers.js +6 -0
- package/dist/referral.d.ts +12 -4
- package/dist/referral.js +12 -4
- package/dist/relay-depository.d.ts +2 -0
- package/dist/relay-depository.js +82 -0
- package/dist/relay-quote.js +2 -3
- package/dist/routing/candidate.d.ts +3 -3
- package/dist/routing/candidate.js +3 -3
- package/dist/routing/carrier-provider-key.d.ts +31 -0
- package/dist/routing/carrier-provider-key.js +37 -0
- package/dist/routing/carriers/ask-answer.d.ts +17 -0
- package/dist/routing/carriers/omnibridge.js +23 -21
- package/dist/routing/carriers/pulsex-paths.d.ts +80 -0
- package/dist/routing/carriers/pulsex-paths.js +92 -0
- package/dist/routing/carriers/pulsex-split.d.ts +175 -0
- package/dist/routing/carriers/pulsex-split.js +185 -0
- package/dist/routing/carriers/pulsex.d.ts +81 -46
- package/dist/routing/carriers/pulsex.js +267 -148
- package/dist/routing/composed-route-quality.d.ts +28 -6
- package/dist/routing/composed-route-quality.js +36 -5
- package/dist/routing/entry-chain-anchor.d.ts +1 -1
- package/dist/routing/entry-chain-anchor.js +1 -1
- package/dist/routing/index.d.ts +11 -3
- package/dist/routing/index.js +8 -2
- package/dist/routing/producers.js +1 -1
- package/dist/routing/providers.d.ts +5 -0
- package/dist/routing/providers.js +5 -0
- package/dist/routing/route-data.d.ts +47 -5
- package/dist/routing/search/graph.d.ts +15 -0
- package/dist/routing/search/plan-route-data.d.ts +42 -1
- package/dist/routing/search/plan-route-data.js +68 -9
- package/dist/routing/search/score.d.ts +3 -0
- package/dist/routing/search/score.js +11 -3
- package/dist/routing/swap-transactions.d.ts +95 -0
- package/dist/routing/swap-transactions.js +127 -0
- package/dist/settings.d.ts +94 -1
- package/dist/settings.js +95 -3
- package/dist/swap-venues.d.ts +43 -0
- package/dist/swap-venues.js +24 -0
- package/dist/ton-connect.d.ts +98 -0
- package/dist/ton-connect.js +200 -0
- package/dist/ton-execution.d.ts +4 -114
- package/dist/ton-execution.js +4 -244
- package/dist/ton.d.ts +37 -0
- package/dist/ton.js +65 -0
- package/docs/examples/example-a-ether-to-pulse.ts +300 -0
- package/docs/examples/example-b-token-pulsechain-to-ethereum.ts +270 -0
- package/docs/examples/shared.ts +261 -0
- package/docs/examples/tsconfig.json +18 -0
- package/docs/integration-guide.md +617 -0
- package/ecosystems/package.json +6 -0
- package/execution/package.json +6 -0
- package/fee-type/package.json +6 -0
- package/hyperliquid-execution/package.json +6 -0
- package/image-links/package.json +6 -0
- package/lifi-execution-guard/package.json +6 -0
- package/llms.txt +75 -10
- package/near-intents/package.json +6 -0
- package/near-intents-route/package.json +6 -0
- package/origin-deposit/package.json +6 -0
- package/package.json +817 -10
- package/pre-quote/package.json +6 -0
- package/prices/package.json +6 -0
- package/providers/package.json +6 -0
- package/quote-fees/package.json +6 -0
- package/referral/package.json +6 -0
- package/relay-currencies/package.json +6 -0
- package/relay-quote/package.json +6 -0
- package/route-steps/package.json +6 -0
- package/routing/package.json +6 -0
- package/settings/package.json +6 -0
- package/ton/package.json +6 -0
- package/ton-connect/package.json +6 -0
- package/ton-execution/package.json +6 -0
- package/tron-execution/package.json +6 -0
- package/types/package.json +6 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { multicallRead } from '@gibs/common/multicall';
|
|
2
|
-
import { getAddress, parseAbi, zeroAddress } from 'viem';
|
|
2
|
+
import { getAddress, isAddress, parseAbi, zeroAddress } from 'viem';
|
|
3
3
|
import * as abis from '../../abis.js';
|
|
4
4
|
import { chainsMetadata } from '../../chains.js';
|
|
5
|
-
import {
|
|
5
|
+
import { defaultSwapVenues, nativeAssetOut, toChain } from '../../config.js';
|
|
6
6
|
import { sameAddress } from '../../ecosystems.js';
|
|
7
|
+
import { findSwapVenue, resolveSwapVenues } from '../../swap-venues.js';
|
|
7
8
|
import { pulseXConversionSwapPath } from '../providers.js';
|
|
8
9
|
import { PULSEX_MARGINAL_REFERENCE_DIVISOR, pulsechainChainId } from '../producers.js';
|
|
9
10
|
import { assetNodeKey } from '../search/graph.js';
|
|
11
|
+
import { bestPulseXPathQuote, candidatePulseXPaths, pulseXHubTokens } from './pulsex-paths.js';
|
|
10
12
|
/**
|
|
11
13
|
* PulseX (the PulseChain-native decentralized exchange, used here for a
|
|
12
14
|
* conversion swap leg) as a plug-in `Carrier` for `@gibs/quotes`'s
|
|
@@ -17,122 +19,187 @@ import { assetNodeKey } from '../search/graph.js';
|
|
|
17
19
|
* See `omnibridge.ts`'s head doc for why this returns a plain object shaped
|
|
18
20
|
* like `Carrier` rather than importing that type.
|
|
19
21
|
*/
|
|
20
|
-
/**
|
|
21
|
-
const
|
|
22
|
+
/** The hub addresses every candidate path on PulseChain is built from — see {@link pulseXHubTokens}. */
|
|
23
|
+
const hubAddresses = pulseXHubTokens.map((hub) => hub.address);
|
|
22
24
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* The wrapped-native token a chain's swaps route through, or undefined when
|
|
26
|
+
* this package does not know one for the chain. Without it a path on that
|
|
27
|
+
* chain can only be direct.
|
|
28
|
+
*
|
|
29
|
+
* @param chainId - the numeric chain id
|
|
28
30
|
*/
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
return
|
|
32
|
-
}
|
|
31
|
+
const wrappedNativeFor = (chainId) => {
|
|
32
|
+
const wrapped = nativeAssetOut[toChain(chainId)];
|
|
33
|
+
return wrapped === undefined ? undefined : getAddress(wrapped);
|
|
34
|
+
};
|
|
33
35
|
/**
|
|
34
|
-
* The
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* meaning rather than guessing a new one.
|
|
36
|
+
* The hub tokens a path on `chainId` may pass through. PulseChain has the
|
|
37
|
+
* full list; any other chain has only its wrapped-native token, because this
|
|
38
|
+
* package knows no other hubs.
|
|
38
39
|
*/
|
|
39
|
-
const
|
|
40
|
+
const hubsFor = (chainId) => {
|
|
41
|
+
if (chainId === pulsechainChainId)
|
|
42
|
+
return hubAddresses;
|
|
43
|
+
const wrapped = wrappedNativeFor(chainId);
|
|
44
|
+
return wrapped === undefined ? [] : [wrapped];
|
|
45
|
+
};
|
|
40
46
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* @param edge - the edge to sign
|
|
46
|
-
* @returns the router, or null when the edge is not a PulseX edge or names a venue that is not a known PulseX router
|
|
47
|
+
* Every PulseX deployment on PulseChain, V1 first: the PulseChain entry of
|
|
48
|
+
* {@link defaultSwapVenues}. Frozen. It lists the DEFAULTS only — a carrier
|
|
49
|
+
* built with its own `venues` offers those instead.
|
|
47
50
|
*/
|
|
48
|
-
export const
|
|
51
|
+
export const pulseXVenues = defaultSwapVenues[pulsechainChainId] ?? [];
|
|
49
52
|
/**
|
|
50
|
-
* The {@link
|
|
53
|
+
* The {@link SwapVenue} a swap edge runs on: the one its figure was quoted
|
|
54
|
+
* against. Reads the edge's own {@link HopEdge.venue}; an edge with none runs
|
|
55
|
+
* on the FIRST venue of its chain (V1 for PulseX) — every swap edge built
|
|
56
|
+
* before venues existed was quoted and signed there, and the same-chain swap
|
|
57
|
+
* sender's own hand-built edge still is.
|
|
58
|
+
*
|
|
59
|
+
* NEVER FALLS BACK TO ANOTHER CHAIN OR ANOTHER ROUTER. An edge that names a
|
|
60
|
+
* router `venues` does not list gets null, so the caller can refuse it.
|
|
51
61
|
*
|
|
52
|
-
* @param
|
|
53
|
-
* @
|
|
62
|
+
* @param venues - the venues the caller allows
|
|
63
|
+
* @param edge - the edge to sign; `from` names the chain, and defaults to PulseChain when absent
|
|
64
|
+
* @returns the venue, or null when the edge is not a swap edge or names a router that is not in `venues`
|
|
54
65
|
*/
|
|
55
|
-
const
|
|
66
|
+
export const venueForEdge = (venues, edge) => {
|
|
56
67
|
if (edge.carrier !== 'PulseX')
|
|
57
68
|
return null;
|
|
69
|
+
const chainId = edge.from?.chainId ?? pulsechainChainId;
|
|
58
70
|
if (edge.venue === undefined)
|
|
59
|
-
return
|
|
60
|
-
|
|
61
|
-
return pulseXVenues.find((candidate) => sameAddress(candidate.router, venue, 'evm')) ?? null;
|
|
71
|
+
return venues[chainId]?.[0] ?? null;
|
|
72
|
+
return findSwapVenue(venues, chainId, edge.venue);
|
|
62
73
|
};
|
|
63
74
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
75
|
+
* The router a PulseX edge must be signed on, judged against the DEFAULT
|
|
76
|
+
* venues only. Prefer {@link venueForEdge} with the venues your host passes.
|
|
77
|
+
*
|
|
78
|
+
* @param edge - the edge to sign
|
|
79
|
+
* @returns the router, or null when the edge is not a swap edge or names a router that is not a default
|
|
80
|
+
*/
|
|
81
|
+
export const pulseXRouterForEdge = (edge) => venueForEdge(defaultSwapVenues, edge)?.router ?? null;
|
|
82
|
+
/**
|
|
83
|
+
* A node's own address for a pool lookup: native (the zero address) is not a
|
|
84
|
+
* token the router or its factory ever holds a pool for — only the chain's
|
|
85
|
+
* wrapped-native token is — so a native node resolves to that wrapped token
|
|
67
86
|
* before it ever reaches {@link pulseXConversionSwapPath}. Every other node
|
|
68
|
-
* is unchanged.
|
|
87
|
+
* is unchanged. A chain with no known wrapped-native token leaves a native
|
|
88
|
+
* node as it is; it has no pool, so no path prices.
|
|
69
89
|
*
|
|
70
90
|
* MIRRORS `useRouteSearch.ts`'s `quotePathAddress` (the UI's async `ask`
|
|
71
91
|
* already resolves native this way through that function) — this is the
|
|
72
92
|
* SAME rule, restated here because `prepare`/`exact` never go through the
|
|
73
|
-
* UI's `readAmountOut` at all; see {@link
|
|
93
|
+
* UI's `readAmountOut` at all; see {@link pulseXCandidatePaths}'s own doc.
|
|
74
94
|
*
|
|
75
95
|
* @param node - the node to resolve
|
|
76
|
-
* @returns the node's own address, or
|
|
96
|
+
* @returns the node's own address, or its chain's wrapped-native address for a native node
|
|
77
97
|
*/
|
|
78
|
-
const poolTokenAddress = (node) =>
|
|
98
|
+
const poolTokenAddress = (node) => sameAddress(node.address, zeroAddress, 'evm') ? (wrappedNativeFor(node.chainId) ?? zeroAddress) : node.address;
|
|
79
99
|
/**
|
|
80
|
-
* The
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* (native PLS resolves to {@link wplsAddress} via {@link poolTokenAddress},
|
|
84
|
-
* same as Wrapped PLS itself): that pair is the wrap carrier's job, never a
|
|
85
|
-
* PulseX hop, so this returns null rather than a zero-length or self-paired
|
|
86
|
-
* path.
|
|
100
|
+
* The fixed Wrapped-PLS path from `from` to `to` — `[from, WPLS, to]`, or
|
|
101
|
+
* `[from, to]` when one side is WPLS — or null when both ends resolve to the
|
|
102
|
+
* same pool token (PLS<->WPLS is the wrap carrier's job).
|
|
87
103
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* `
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* `null` for a direct native-PLS PulseX hop, and `enumerate.ts` pruned it,
|
|
98
|
-
* leaving only the two-signature wrap-then-swap path.
|
|
104
|
+
* NOT THE PATH A HOP IS SIGNED ALONG. The carrier's own `exact` picks the
|
|
105
|
+
* best of many paths ({@link candidatePulseXPaths}) and records the winner on
|
|
106
|
+
* the hop's outcome; the signer reads it back with {@link pulseXSwapPathForHop}.
|
|
107
|
+
* This fixed path serves only the carrier's asynchronous `ask` — the
|
|
108
|
+
* interface's `getAmountsOut` read, which the route search never calls for a
|
|
109
|
+
* PulseX hop, because the search only asks aggregators live.
|
|
110
|
+
*
|
|
111
|
+
* Native PLS resolves to WPLS first ({@link poolTokenAddress}), so a path
|
|
112
|
+
* never starts or ends on the zero address, which has no pool.
|
|
99
113
|
*
|
|
100
114
|
* @param from - the hop's origin node
|
|
101
115
|
* @param to - the hop's destination node
|
|
102
|
-
* @returns the
|
|
116
|
+
* @returns the fixed path, or null when there is nothing for PulseX to swap
|
|
103
117
|
*/
|
|
104
|
-
export const
|
|
118
|
+
export const pulseXWrappedNativePath = (from, to) => {
|
|
105
119
|
const fromAddress = poolTokenAddress(from);
|
|
106
120
|
const toAddress = poolTokenAddress(to);
|
|
107
121
|
if (sameAddress(fromAddress, toAddress, 'evm'))
|
|
108
122
|
return null;
|
|
109
|
-
|
|
123
|
+
const wrappedNative = wrappedNativeFor(from.chainId);
|
|
124
|
+
return wrappedNative === undefined ? [fromAddress, toAddress] : pulseXConversionSwapPath(fromAddress, toAddress, wrappedNative);
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Every path a PulseX hop from `from` to `to` may be priced along, or none
|
|
128
|
+
* when both ends resolve to the same pool token. EVERY PULSEX PRICE AND
|
|
129
|
+
* POOL READ IN THIS FILE GOES THROUGH THIS ONE FUNCTION, so `prepare` reads
|
|
130
|
+
* exactly the pools `exact` will look up, and native PLS resolves to WPLS
|
|
131
|
+
* the same way in both ({@link poolTokenAddress} — a path that started on
|
|
132
|
+
* the zero address once left every native-PLS hop unpriced).
|
|
133
|
+
*
|
|
134
|
+
* @param from - the hop's origin node
|
|
135
|
+
* @param to - the hop's destination node
|
|
136
|
+
* @returns the candidate paths, direct first; empty when there is nothing to swap
|
|
137
|
+
*/
|
|
138
|
+
const pulseXCandidatePaths = (from, to) => {
|
|
139
|
+
const fromAddress = poolTokenAddress(from);
|
|
140
|
+
const toAddress = poolTokenAddress(to);
|
|
141
|
+
if (sameAddress(fromAddress, toAddress, 'evm'))
|
|
142
|
+
return [];
|
|
143
|
+
const wrappedNative = wrappedNativeFor(from.chainId);
|
|
144
|
+
// No known wrapped-native token: the direct pool is the only path.
|
|
145
|
+
if (wrappedNative === undefined)
|
|
146
|
+
return [[fromAddress, toAddress]];
|
|
147
|
+
return candidatePulseXPaths({ from: fromAddress, to: toAddress, wrappedNative, hubs: hubsFor(from.chainId) });
|
|
110
148
|
};
|
|
111
149
|
/**
|
|
112
|
-
* Whether
|
|
113
|
-
*
|
|
114
|
-
*
|
|
150
|
+
* Whether a swap venue in `venues` can carry funds directly from `from` to
|
|
151
|
+
* `to`: both must be Ethereum-Virtual-Machine tokens on the SAME chain, that
|
|
152
|
+
* chain must have at least one venue, and the two must resolve to different
|
|
153
|
+
* pool tokens, so there is at least one path to price.
|
|
115
154
|
*
|
|
116
|
-
* NO TOKEN ALLOWLIST IS CONSULTED, on purpose:
|
|
117
|
-
*
|
|
118
|
-
* is total over any two addresses that do not resolve to the same pool
|
|
119
|
-
* token — it collapses to a direct hop only when one side is already WPLS,
|
|
120
|
-
* and otherwise always routes through WPLS. This calls it anyway and checks
|
|
121
|
-
* the result is at least two addresses long, so a future change that
|
|
122
|
-
* narrowed it to return an empty path for some input would fail this
|
|
123
|
-
* predicate rather than silently offer a hop with no path.
|
|
155
|
+
* NO TOKEN ALLOWLIST IS CONSULTED, on purpose: whether a pool exists is the
|
|
156
|
+
* reserve read's question, answered in `prepare`, not this one's.
|
|
124
157
|
*/
|
|
125
|
-
const pulseXCarries = (from, to) => {
|
|
158
|
+
const pulseXCarries = (venues, from, to) => {
|
|
126
159
|
if (from.ecosystem !== 'evm' || to.ecosystem !== 'evm')
|
|
127
160
|
return false;
|
|
128
|
-
if (from.chainId !==
|
|
161
|
+
if (from.chainId !== to.chainId)
|
|
129
162
|
return false;
|
|
130
|
-
|
|
131
|
-
|
|
163
|
+
if ((venues[from.chainId] ?? []).length === 0)
|
|
164
|
+
return false;
|
|
165
|
+
return pulseXCandidatePaths(from, to).length > 0;
|
|
132
166
|
};
|
|
133
167
|
/**
|
|
134
|
-
* PulseX
|
|
135
|
-
* {@link
|
|
168
|
+
* The swap path a planned PulseX hop must be signed along: the path its
|
|
169
|
+
* outcome records ({@link HopOutcome.swapPath}), which is the path the
|
|
170
|
+
* carrier's `exact` priced the figure on. NEVER REBUILT HERE — rebuilding a
|
|
171
|
+
* path at signing time is how a quote on one path becomes a signature on
|
|
172
|
+
* another.
|
|
173
|
+
*
|
|
174
|
+
* The recorded path is checked against the hop's own edge before it is
|
|
175
|
+
* returned: it must start at the edge's input pool token, end at its output
|
|
176
|
+
* pool token, hold only addresses, and name no token twice. A path that fails
|
|
177
|
+
* any check is refused (null) rather than signed.
|
|
178
|
+
*
|
|
179
|
+
* @param hop - the planned hop to sign
|
|
180
|
+
* @returns the path to sign, or null when the hop is not a PulseX hop, records no path, or records one that does not fit its edge
|
|
181
|
+
*/
|
|
182
|
+
export const pulseXSwapPathForHop = (hop) => {
|
|
183
|
+
if (hop.edge.carrier !== 'PulseX')
|
|
184
|
+
return null;
|
|
185
|
+
const swapPath = hop.outcome.swapPath;
|
|
186
|
+
if (swapPath === undefined || swapPath.length < 2)
|
|
187
|
+
return null;
|
|
188
|
+
if (!swapPath.every((address) => isAddress(address, { strict: false })))
|
|
189
|
+
return null;
|
|
190
|
+
const path = swapPath.map((address) => getAddress(address));
|
|
191
|
+
if (new Set(path).size !== path.length)
|
|
192
|
+
return null;
|
|
193
|
+
if (!sameAddress(path[0], poolTokenAddress(hop.edge.from), 'evm'))
|
|
194
|
+
return null;
|
|
195
|
+
if (!sameAddress(path[path.length - 1], poolTokenAddress(hop.edge.to), 'evm'))
|
|
196
|
+
return null;
|
|
197
|
+
return path;
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* PulseX's own constant-product swap fee in basis points (29 of 10000, a
|
|
201
|
+
* numerator of 9971) — the default for {@link amountOutAlongPath}; a
|
|
202
|
+
* venue carries its own `feeBasisPoints`. The SAME `getAmountOut` formula every
|
|
136
203
|
* Uniswap-V2-style router uses: `amountOut = floor(amountIn * feeNumerator *
|
|
137
204
|
* reserveOut / (reserveIn * feeDenominator + amountIn * feeNumerator))`.
|
|
138
205
|
*
|
|
@@ -145,16 +212,16 @@ const pulseXCarries = (from, to) => {
|
|
|
145
212
|
* this claim against live chain data rather than trusting the arithmetic
|
|
146
213
|
* alone.
|
|
147
214
|
*/
|
|
148
|
-
const
|
|
149
|
-
/**
|
|
150
|
-
const
|
|
215
|
+
const PULSEX_FEE_BASIS_POINTS = 29;
|
|
216
|
+
/** Basis points in a whole: the denominator of every venue's fee. */
|
|
217
|
+
const BASIS_POINTS_DENOMINATOR = 10000n;
|
|
151
218
|
/** The minimal read-only factory ABI `prepare` needs — just enough to resolve a pool's address from its two tokens. */
|
|
152
219
|
const univ2FactoryAbi = parseAbi([
|
|
153
220
|
'function getPair(address tokenA, address tokenB) external view returns (address pair)',
|
|
154
221
|
]);
|
|
155
222
|
/**
|
|
156
223
|
* The Uniswap-V2-style constant-product `getAmountOut`, applied with
|
|
157
|
-
*
|
|
224
|
+
* the venue's fee. Reproduces the router's own on-chain formula
|
|
158
225
|
* exactly — see `pulsex-carrier.fork.test.ts` — so PRICE IMPACT IS ALREADY
|
|
159
226
|
* IN HERE: this is the same nonlinear function the router evaluates, not a
|
|
160
227
|
* flat-fee approximation with an impact correction bolted on afterward.
|
|
@@ -162,14 +229,15 @@ const univ2FactoryAbi = parseAbi([
|
|
|
162
229
|
* @param amountIn - the input amount, in `reserveIn`'s token's base units
|
|
163
230
|
* @param reserveIn - the pool's reserve of the input token
|
|
164
231
|
* @param reserveOut - the pool's reserve of the output token
|
|
232
|
+
* @param feeBasisPoints - the venue's swap fee, in basis points
|
|
165
233
|
* @returns the output amount, in `reserveOut`'s token's base units; 0 when either reserve is 0
|
|
166
234
|
*/
|
|
167
|
-
const constantProductAmountOut = (amountIn, reserveIn, reserveOut) => {
|
|
235
|
+
const constantProductAmountOut = (amountIn, reserveIn, reserveOut, feeBasisPoints) => {
|
|
168
236
|
if (reserveIn <= 0n || reserveOut <= 0n || amountIn <= 0n)
|
|
169
237
|
return 0n;
|
|
170
|
-
const amountInWithFee = amountIn *
|
|
238
|
+
const amountInWithFee = amountIn * (BASIS_POINTS_DENOMINATOR - BigInt(feeBasisPoints));
|
|
171
239
|
const numerator = amountInWithFee * reserveOut;
|
|
172
|
-
const denominator = reserveIn *
|
|
240
|
+
const denominator = reserveIn * BASIS_POINTS_DENOMINATOR + amountInWithFee;
|
|
173
241
|
return numerator / denominator;
|
|
174
242
|
};
|
|
175
243
|
/**
|
|
@@ -178,9 +246,10 @@ const constantProductAmountOut = (amountIn, reserveIn, reserveOut) => {
|
|
|
178
246
|
*
|
|
179
247
|
* @param amountIn - the amount entering the first pool
|
|
180
248
|
* @param steps - the pools in order of travel
|
|
249
|
+
* @param feeBasisPoints - the venue's swap fee in basis points; PulseX's 29 when omitted
|
|
181
250
|
* @returns the amount leaving the last pool; 0 when any pool cannot pay out
|
|
182
251
|
*/
|
|
183
|
-
export const amountOutAlongPath = (amountIn, steps) => steps.reduce((runningAmount, step) => constantProductAmountOut(runningAmount, step.reserveIn, step.reserveOut), amountIn);
|
|
252
|
+
export const amountOutAlongPath = (amountIn, steps, feeBasisPoints = PULSEX_FEE_BASIS_POINTS) => steps.reduce((runningAmount, step) => constantProductAmountOut(runningAmount, step.reserveIn, step.reserveOut, feeBasisPoints), amountIn);
|
|
184
253
|
/**
|
|
185
254
|
* How far a trade's executed rate falls below the marginal (near-spot) rate,
|
|
186
255
|
* in percent. The marginal rate is a tiny reference trade through the same
|
|
@@ -216,9 +285,35 @@ const poolKey = (tokenA, tokenB) => {
|
|
|
216
285
|
return `${lower}|${higher}`;
|
|
217
286
|
};
|
|
218
287
|
/** The key one venue's pool is stored under: the same two tokens on V1 and on V2 are two different pools with two different reserves. */
|
|
219
|
-
const venuePoolKey = (venue, tokenA, tokenB) => `${venue.factory.toLowerCase()}|${poolKey(tokenA, tokenB)}`;
|
|
288
|
+
const venuePoolKey = (venue, tokenA, tokenB) => `${venue.chainId}|${venue.factory.toLowerCase()}|${poolKey(tokenA, tokenB)}`;
|
|
220
289
|
/** Every consecutive (tokenIn, tokenOut) hop a PulseX swap path implies. */
|
|
221
290
|
const hopsAlongPath = (path) => path.slice(0, -1).map((tokenIn, index) => [tokenIn, path[index + 1]]);
|
|
291
|
+
/**
|
|
292
|
+
* The pools a path crosses on one venue, each read in the direction of
|
|
293
|
+
* travel, or null when any of them was never read — `getPair` found no pool,
|
|
294
|
+
* or the read failed. A path with a missing pool cannot be priced, exactly as
|
|
295
|
+
* the router's own `getAmountsOut` would revert on it.
|
|
296
|
+
*
|
|
297
|
+
* @param options.venue - the deployment whose pools the path trades against
|
|
298
|
+
* @param options.path - the tokens, in order
|
|
299
|
+
* @param options.reservesByPoolKey - every pool `prepare` read, keyed by {@link venuePoolKey}
|
|
300
|
+
* @returns the steps in order, or null when a pool is missing
|
|
301
|
+
*/
|
|
302
|
+
const stepsAlongPath = (options) => {
|
|
303
|
+
const { venue, path, reservesByPoolKey } = options;
|
|
304
|
+
const steps = [];
|
|
305
|
+
for (const [tokenIn, tokenOut] of hopsAlongPath(path)) {
|
|
306
|
+
const reserves = reservesByPoolKey.get(venuePoolKey(venue, tokenIn, tokenOut));
|
|
307
|
+
if (reserves === undefined)
|
|
308
|
+
return null;
|
|
309
|
+
const tokenInIsLower = tokenIn.toLowerCase() < tokenOut.toLowerCase();
|
|
310
|
+
steps.push({
|
|
311
|
+
reserveIn: tokenInIsLower ? reserves.reserveOfLowerAddress : reserves.reserveOfHigherAddress,
|
|
312
|
+
reserveOut: tokenInIsLower ? reserves.reserveOfHigherAddress : reserves.reserveOfLowerAddress,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return steps;
|
|
316
|
+
};
|
|
222
317
|
/**
|
|
223
318
|
* Batch-reads reserves for every pool `nodes`' candidate PulseX paths touch,
|
|
224
319
|
* on EVERY venue in `venues`, once, so {@link createPulseXCarrier}'s `exact`
|
|
@@ -235,6 +330,14 @@ const hopsAlongPath = (path) => path.slice(0, -1).map((tokenIn, index) => [token
|
|
|
235
330
|
* the second read cannot join the first — but every venue's `getPair` rides
|
|
236
331
|
* the first multicall and every pool's `getReserves` rides the second.
|
|
237
332
|
*
|
|
333
|
+
* THE HUB SEARCH WIDENS EACH READ, NOT THEIR NUMBER. Every pool any candidate
|
|
334
|
+
* path crosses is read here ({@link pulseXCandidatePaths}), so `exact` can
|
|
335
|
+
* price every path with no further call. Measured 2026-10-06: an 8-token
|
|
336
|
+
* PulseChain universe asked 84 entries across the two reads (the fixed
|
|
337
|
+
* Wrapped-PLS path asked about 16). The shared multicall helper splits a read
|
|
338
|
+
* past its request-size budget into sequential posts; `getPair` crosses that
|
|
339
|
+
* line at about 11 pool tokens (198 entries over two venues).
|
|
340
|
+
*
|
|
238
341
|
* @param nodes - every node the search might branch through
|
|
239
342
|
* @param venues - the venues this carrier offers
|
|
240
343
|
* @param clientForChain - see {@link CreatePulseXCarrierOptions}'s own field
|
|
@@ -242,35 +345,54 @@ const hopsAlongPath = (path) => path.slice(0, -1).map((tokenIn, index) => [token
|
|
|
242
345
|
*/
|
|
243
346
|
const preparePulseXReserves = async ({ nodes, venues, clientForChain, }) => {
|
|
244
347
|
const reservesByPoolKey = new Map();
|
|
245
|
-
const
|
|
246
|
-
|
|
348
|
+
const chainIds = Object.keys(venues)
|
|
349
|
+
.map(Number)
|
|
350
|
+
.filter((chainId) => (venues[chainId] ?? []).length > 0);
|
|
351
|
+
for (const chainId of chainIds) {
|
|
352
|
+
const chainReserves = await preparePulseXReservesOnChain({ nodes, chainId, venues, clientForChain });
|
|
353
|
+
for (const [key, reserves] of chainReserves)
|
|
354
|
+
reservesByPoolKey.set(key, reserves);
|
|
355
|
+
}
|
|
356
|
+
return reservesByPoolKey;
|
|
357
|
+
};
|
|
358
|
+
/**
|
|
359
|
+
* One chain's share of {@link preparePulseXReserves}: two multicalls, the
|
|
360
|
+
* `getPair` reads and then the `getReserves` reads, over every venue of the
|
|
361
|
+
* chain. A chain with no client, or with no chain description to multicall
|
|
362
|
+
* against, reads nothing.
|
|
363
|
+
*/
|
|
364
|
+
const preparePulseXReservesOnChain = async ({ nodes, chainId, venues, clientForChain, }) => {
|
|
365
|
+
const reservesByPoolKey = new Map();
|
|
366
|
+
const client = clientForChain(chainId);
|
|
367
|
+
const knownChain = chainsMetadata[toChain(chainId)];
|
|
368
|
+
const chain = knownChain ?? client?.chain;
|
|
369
|
+
if (client === null || chain === undefined)
|
|
247
370
|
return reservesByPoolKey;
|
|
248
|
-
const
|
|
371
|
+
const chainNodes = nodes.filter((node) => node.ecosystem === 'evm' && node.chainId === chainId);
|
|
249
372
|
const poolTokenPairs = new Map();
|
|
250
|
-
for (const from of
|
|
251
|
-
for (const to of
|
|
373
|
+
for (const from of chainNodes) {
|
|
374
|
+
for (const to of chainNodes) {
|
|
252
375
|
if (assetNodeKey(from) === assetNodeKey(to))
|
|
253
376
|
continue;
|
|
254
|
-
if (!pulseXCarries(from, to))
|
|
255
|
-
continue;
|
|
256
|
-
const path = pulseXPoolPath(from, to);
|
|
257
|
-
if (path === null)
|
|
377
|
+
if (!pulseXCarries(venues, from, to))
|
|
258
378
|
continue;
|
|
259
|
-
for (const
|
|
260
|
-
|
|
379
|
+
for (const path of pulseXCandidatePaths(from, to)) {
|
|
380
|
+
for (const [tokenIn, tokenOut] of hopsAlongPath(path)) {
|
|
381
|
+
poolTokenPairs.set(poolKey(tokenIn, tokenOut), [tokenIn, tokenOut]);
|
|
382
|
+
}
|
|
261
383
|
}
|
|
262
384
|
}
|
|
263
385
|
}
|
|
264
386
|
if (poolTokenPairs.size === 0)
|
|
265
387
|
return reservesByPoolKey;
|
|
266
|
-
const poolQueries = venues.flatMap((venue) => Array.from(poolTokenPairs.values()).map(([tokenIn, tokenOut]) => ({
|
|
388
|
+
const poolQueries = (venues[chainId] ?? []).flatMap((venue) => Array.from(poolTokenPairs.values()).map(([tokenIn, tokenOut]) => ({
|
|
267
389
|
key: venuePoolKey(venue, tokenIn, tokenOut),
|
|
268
390
|
factory: venue.factory,
|
|
269
391
|
tokenIn,
|
|
270
392
|
tokenOut,
|
|
271
393
|
})));
|
|
272
394
|
const factoryResults = await multicallRead({
|
|
273
|
-
chain
|
|
395
|
+
chain,
|
|
274
396
|
client,
|
|
275
397
|
abi: univ2FactoryAbi,
|
|
276
398
|
calls: poolQueries.map((query) => ({
|
|
@@ -295,7 +417,7 @@ const preparePulseXReserves = async ({ nodes, venues, clientForChain, }) => {
|
|
|
295
417
|
return reservesByPoolKey;
|
|
296
418
|
const reservePoolKeys = Array.from(pairAddressByPoolKey.keys());
|
|
297
419
|
const reserveResults = await multicallRead({
|
|
298
|
-
chain
|
|
420
|
+
chain,
|
|
299
421
|
client,
|
|
300
422
|
abi: abis.pair,
|
|
301
423
|
calls: reservePoolKeys.map((key) => ({
|
|
@@ -320,22 +442,24 @@ const preparePulseXReserves = async ({ nodes, venues, clientForChain, }) => {
|
|
|
320
442
|
return reservesByPoolKey;
|
|
321
443
|
};
|
|
322
444
|
/**
|
|
323
|
-
* The venues a carrier offers: every
|
|
324
|
-
* host names. A named router that is
|
|
325
|
-
*
|
|
445
|
+
* The venues a carrier offers: every venue in `venues`, or only the routers
|
|
446
|
+
* the host names. A named router that is in no venue is a configuration
|
|
447
|
+
* error, thrown at construction rather than offered silently.
|
|
326
448
|
*
|
|
449
|
+
* @param venues - the resolved venues
|
|
327
450
|
* @param routers - the routers the host can sign on, or undefined for all
|
|
328
|
-
* @returns the venues, in
|
|
451
|
+
* @returns the venues, in their original order
|
|
329
452
|
*/
|
|
330
|
-
const selectPulseXVenues = (routers) => {
|
|
453
|
+
const selectPulseXVenues = (venues, routers) => {
|
|
331
454
|
if (routers === undefined)
|
|
332
|
-
return
|
|
455
|
+
return venues;
|
|
456
|
+
const named = (venue) => routers.some((router) => sameAddress(venue.router, router, 'evm'));
|
|
333
457
|
for (const router of routers) {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
458
|
+
const offered = Object.values(venues).some((list) => list.some((venue) => sameAddress(venue.router, router, 'evm')));
|
|
459
|
+
if (!offered)
|
|
460
|
+
throw new Error(`createPulseXCarrier: ${router} is not a known PulseX router in the offered swap venues`);
|
|
337
461
|
}
|
|
338
|
-
return
|
|
462
|
+
return Object.freeze(Object.fromEntries(Object.entries(venues).map(([chainId, list]) => [chainId, list.filter(named)])));
|
|
339
463
|
};
|
|
340
464
|
/**
|
|
341
465
|
* Builds the PulseX conversion swap as a plug-in carrier.
|
|
@@ -345,16 +469,16 @@ const selectPulseXVenues = (routers) => {
|
|
|
345
469
|
*/
|
|
346
470
|
export const createPulseXCarrier = (options) => {
|
|
347
471
|
const { readAmountOut, clientForChain } = options;
|
|
348
|
-
const venues = selectPulseXVenues(options.routers);
|
|
472
|
+
const venues = selectPulseXVenues(resolveSwapVenues(options.venues), options.routers);
|
|
349
473
|
// ONE EDGE PER VENUE. Each names its router as its `venue`, so the search
|
|
350
474
|
// quotes and ranks V1 and V2 as two separate hops and the better one wins
|
|
351
475
|
// on its own figure — and the winner is signed on the router it names.
|
|
352
476
|
const edgesFrom = (from, to) => {
|
|
353
477
|
if (assetNodeKey(from) === assetNodeKey(to))
|
|
354
478
|
return [];
|
|
355
|
-
if (!pulseXCarries(from, to))
|
|
479
|
+
if (!pulseXCarries(venues, from, to))
|
|
356
480
|
return [];
|
|
357
|
-
return venues.map((venue) => ({
|
|
481
|
+
return (venues[from.chainId] ?? []).map((venue) => ({
|
|
358
482
|
carrier: 'PulseX',
|
|
359
483
|
from,
|
|
360
484
|
to,
|
|
@@ -390,51 +514,46 @@ export const createPulseXCarrier = (options) => {
|
|
|
390
514
|
reservesByPoolKey = await preparePulseXReserves({ nodes, venues, clientForChain });
|
|
391
515
|
};
|
|
392
516
|
const exact = (edge, amountIn) => {
|
|
393
|
-
if (!pulseXCarries(edge.from, edge.to))
|
|
517
|
+
if (!pulseXCarries(venues, edge.from, edge.to))
|
|
394
518
|
return null;
|
|
395
519
|
// Quoted on the edge's OWN venue — the router it will be signed on — and
|
|
396
520
|
// only on a venue this carrier offers.
|
|
397
|
-
const venue =
|
|
398
|
-
if (venue === null
|
|
521
|
+
const venue = venueForEdge(venues, edge);
|
|
522
|
+
if (venue === null)
|
|
399
523
|
return null;
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
|
|
524
|
+
// EVERY CANDIDATE PATH IS PRICED, AND THE BEST ONE WINS. A path with a
|
|
525
|
+
// pool `prepare` never found is left out — the same as a router call
|
|
526
|
+
// that would revert — and a path that pays out nothing is never picked.
|
|
527
|
+
const quotes = pulseXCandidatePaths(edge.from, edge.to).flatMap((path) => {
|
|
528
|
+
const steps = stepsAlongPath({ venue, path, reservesByPoolKey });
|
|
529
|
+
return steps === null ? [] : [{ path, steps, deliveredAmount: amountOutAlongPath(amountIn, steps, venue.feeBasisPoints) }];
|
|
530
|
+
});
|
|
531
|
+
// No path had every pool read — a structural non-answer, not a refusal
|
|
532
|
+
// (`exact`'s own doc: `null` means "nothing to say"). NOTE THIS DOES NOT
|
|
533
|
+
// FALL BACK TO AN ESTIMATE: a carrier with `exact` defined never yields
|
|
534
|
+
// an `'estimate'` outcome (see `enumerate.ts`'s `resolveHopOutcome`,
|
|
535
|
+
// branch 1) — `null` here PRUNES the hop entirely, the same as a refusal.
|
|
536
|
+
if (quotes.length === 0)
|
|
405
537
|
return null;
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
const reserves = reservesByPoolKey.get(venuePoolKey(venue, tokenIn, tokenOut));
|
|
409
|
-
// `prepare` never read this pool — a structural non-answer, not a
|
|
410
|
-
// refusal (`exact`'s own doc: `null` means "nothing to say"). NOTE
|
|
411
|
-
// THIS DOES NOT FALL BACK TO AN ESTIMATE: a carrier with `exact`
|
|
412
|
-
// defined never yields an `'estimate'` outcome (see
|
|
413
|
-
// `enumerate.ts`'s `resolveHopOutcome`, branch 1) — `null` here
|
|
414
|
-
// PRUNES the hop entirely, the same as a refusal.
|
|
415
|
-
if (reserves === undefined)
|
|
416
|
-
return null;
|
|
417
|
-
const tokenInIsLower = tokenIn.toLowerCase() < tokenOut.toLowerCase();
|
|
418
|
-
steps.push({
|
|
419
|
-
reserveIn: tokenInIsLower ? reserves.reserveOfLowerAddress : reserves.reserveOfHigherAddress,
|
|
420
|
-
reserveOut: tokenInIsLower ? reserves.reserveOfHigherAddress : reserves.reserveOfLowerAddress,
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
const deliveredAmount = amountOutAlongPath(amountIn, steps);
|
|
424
|
-
if (deliveredAmount <= 0n) {
|
|
538
|
+
const best = bestPulseXPathQuote(quotes);
|
|
539
|
+
if (best === null) {
|
|
425
540
|
return { outcome: 'refused', refusal: { kind: 'no-quote', reason: 'no liquidity on this PulseX path', minimumAmount: null } };
|
|
426
541
|
}
|
|
542
|
+
// Price impact is measured on the CHOSEN path's own pools, with the same
|
|
543
|
+
// reference trade as before.
|
|
544
|
+
const referenceAmountIn = amountIn / PULSEX_MARGINAL_REFERENCE_DIVISOR;
|
|
427
545
|
const priceImpactPercent = priceImpactAgainstMarginal({
|
|
428
546
|
amountIn,
|
|
429
|
-
amountOut: deliveredAmount,
|
|
430
|
-
referenceAmountIn
|
|
431
|
-
referenceAmountOut: amountOutAlongPath(
|
|
547
|
+
amountOut: best.deliveredAmount,
|
|
548
|
+
referenceAmountIn,
|
|
549
|
+
referenceAmountOut: amountOutAlongPath(referenceAmountIn, best.steps, venue.feeBasisPoints),
|
|
432
550
|
});
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
551
|
+
// The path rides on the answer, so the signer signs exactly what was
|
|
552
|
+
// priced (`pulseXSwapPathForHop`). Impact is left off (not zero) when the
|
|
553
|
+
// reference trade rounds away: an unknown impact must stay unknown so the
|
|
554
|
+
// quality gate withholds the plan.
|
|
555
|
+
const delivered = { outcome: 'delivered', deliveredAmount: best.deliveredAmount, swapPath: best.path };
|
|
556
|
+
return priceImpactPercent === null ? delivered : { ...delivered, priceImpactPercent };
|
|
438
557
|
};
|
|
439
558
|
return {
|
|
440
559
|
id: 'PulseX',
|