@gibs/bridge-sdk 1.13.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +244 -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 +240 -0
- package/dist/cjs/config.js +537 -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 +346 -0
- package/dist/cjs/execution.js +468 -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 +23 -0
- package/dist/cjs/index.js +39 -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 +107 -0
- package/dist/cjs/prices.js +172 -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/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 +159 -0
- package/dist/cjs/routing/carriers/pulsex.js +565 -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 +38 -0
- package/dist/cjs/routing/index.js +134 -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 +355 -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 +269 -0
- package/dist/cjs/routing/search/plan-route-data.js +848 -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 +91 -0
- package/dist/cjs/routing/swap-transactions.js +131 -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/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 +24 -0
- package/dist/config.js +39 -5
- package/dist/consolidated-quote.d.ts +109 -15
- package/dist/consolidated-quote.js +310 -52
- package/dist/execution.d.ts +346 -0
- package/dist/execution.js +428 -0
- package/dist/hyperliquid-execution.js +2 -3
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -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/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/carriers/ask-answer.d.ts +19 -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 +69 -21
- package/dist/routing/carriers/pulsex.js +184 -65
- 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 +7 -3
- package/dist/routing/index.js +7 -3
- 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/search/graph.d.ts +29 -0
- package/dist/routing/search/plan-route-data.d.ts +25 -0
- package/dist/routing/search/plan-route-data.js +43 -7
- package/dist/routing/search/score.d.ts +33 -1
- package/dist/routing/search/score.js +41 -6
- package/dist/routing/swap-transactions.d.ts +91 -0
- package/dist/routing/swap-transactions.js +125 -0
- package/dist/settings.d.ts +94 -1
- package/dist/settings.js +95 -3
- 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 +234 -0
- 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 +818 -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
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { PULSEX_MARGINAL_REFERENCE_DIVISOR } from '../producers.js';
|
|
2
|
+
import { amountOutAlongPath, priceImpactAgainstMarginal } from './pulsex.js';
|
|
3
|
+
/**
|
|
4
|
+
* How many equal chunks the search divides the trade into: 20, so legs move
|
|
5
|
+
* in 5% steps. Finer steps buy a little output for proportionally more work.
|
|
6
|
+
*/
|
|
7
|
+
export const PULSEX_SPLIT_CHUNKS = 20;
|
|
8
|
+
/** The most legs one split may have. Each leg is one more router call to sign or batch, and one more swap's gas. */
|
|
9
|
+
export const PULSEX_SPLIT_MAX_LEGS = 3;
|
|
10
|
+
/** How many candidate paths, best single-path output first, the search may spread the trade over. */
|
|
11
|
+
export const PULSEX_SPLIT_CANDIDATE_LIMIT = 4;
|
|
12
|
+
/**
|
|
13
|
+
* The gain a split must show over the best single path before it is chosen,
|
|
14
|
+
* in basis points of the single path's output: 25, or 0.25%.
|
|
15
|
+
*
|
|
16
|
+
* GAS IS REAL. Each extra leg costs roughly one more swap's gas, and a
|
|
17
|
+
* batching contract adds its own overhead. A split that wins by a rounding
|
|
18
|
+
* margin buys the reader nothing but a larger fee. The figure is a judgement,
|
|
19
|
+
* not a measurement; revisit it once an execution path has a real gas cost.
|
|
20
|
+
*/
|
|
21
|
+
export const PULSEX_SPLIT_MINIMUM_GAIN_BASIS_POINTS = 25n;
|
|
22
|
+
const BASIS_POINTS = 10000n;
|
|
23
|
+
/** The key one token's reserve in one pool is held under while a split executes. */
|
|
24
|
+
const reserveKey = (pool, token) => `${pool}|${token.toLowerCase()}`;
|
|
25
|
+
/**
|
|
26
|
+
* Executes legs one after another against shared pool state, exactly as
|
|
27
|
+
* sequential router calls would. A leg that crosses a pool an earlier leg
|
|
28
|
+
* already traded sees that pool's moved reserves, in whichever direction the
|
|
29
|
+
* earlier leg traded it.
|
|
30
|
+
*
|
|
31
|
+
* Each trade adds its full input to the pool and removes its output, the
|
|
32
|
+
* Uniswap-V2 accounting. See the open risk in the report: if PulseX moves
|
|
33
|
+
* part of its fee out of the pool during a swap, a shared pool is a few
|
|
34
|
+
* hundredths of a percent shallower here than on chain.
|
|
35
|
+
*
|
|
36
|
+
* @param legs - each candidate with the amount it spends, in execution order
|
|
37
|
+
* @returns what each leg delivers, in the same order
|
|
38
|
+
*/
|
|
39
|
+
export const executePulseXSplitInOrder = (legs) => {
|
|
40
|
+
const reserves = new Map();
|
|
41
|
+
const reserveOf = (pool, token, initial) => reserves.get(reserveKey(pool, token)) ?? initial;
|
|
42
|
+
return legs.map(({ candidate, amountIn }) => candidate.steps.reduce((runningAmount, step) => {
|
|
43
|
+
const reserveIn = reserveOf(step.pool, step.tokenIn, step.reserveIn);
|
|
44
|
+
const reserveOut = reserveOf(step.pool, step.tokenOut, step.reserveOut);
|
|
45
|
+
const amountOut = amountOutAlongPath(runningAmount, [{ reserveIn, reserveOut }]);
|
|
46
|
+
reserves.set(reserveKey(step.pool, step.tokenIn), reserveIn + runningAmount);
|
|
47
|
+
reserves.set(reserveKey(step.pool, step.tokenOut), reserveOut - amountOut);
|
|
48
|
+
return amountOut;
|
|
49
|
+
}, amountIn));
|
|
50
|
+
};
|
|
51
|
+
/** What one candidate alone delivers for the whole trade. */
|
|
52
|
+
const singlePathOutput = (candidate, amountIn) => executePulseXSplitInOrder([{ candidate, amountIn }])[0] ?? 0n;
|
|
53
|
+
/**
|
|
54
|
+
* The candidates a split may use: the {@link PULSEX_SPLIT_CANDIDATE_LIMIT}
|
|
55
|
+
* best by single-path output for the whole trade. A candidate that delivers
|
|
56
|
+
* nothing is dropped. Ties go to the shorter path, then the earlier
|
|
57
|
+
* candidate, so the ranking is deterministic.
|
|
58
|
+
*
|
|
59
|
+
* @param candidates - every priced path, on every venue
|
|
60
|
+
* @param amountIn - the whole trade
|
|
61
|
+
* @returns the ranked candidates, best first, each with its single-path output
|
|
62
|
+
*/
|
|
63
|
+
export const rankPulseXSplitCandidates = (candidates, amountIn) => candidates
|
|
64
|
+
.map((candidate, index) => ({ candidate, index, deliveredAmount: singlePathOutput(candidate, amountIn) }))
|
|
65
|
+
.filter((ranked) => ranked.deliveredAmount > 0n)
|
|
66
|
+
.sort((left, right) => {
|
|
67
|
+
if (left.deliveredAmount !== right.deliveredAmount)
|
|
68
|
+
return left.deliveredAmount > right.deliveredAmount ? -1 : 1;
|
|
69
|
+
if (left.candidate.path.length !== right.candidate.path.length)
|
|
70
|
+
return left.candidate.path.length - right.candidate.path.length;
|
|
71
|
+
return left.index - right.index;
|
|
72
|
+
})
|
|
73
|
+
.slice(0, PULSEX_SPLIT_CANDIDATE_LIMIT)
|
|
74
|
+
.map(({ candidate, deliveredAmount }) => ({ candidate, deliveredAmount }));
|
|
75
|
+
/** The trade divided into {@link PULSEX_SPLIT_CHUNKS} chunks; the last one takes the remainder, so the chunks sum to the trade exactly. */
|
|
76
|
+
const chunksOf = (amountIn) => {
|
|
77
|
+
const count = BigInt(PULSEX_SPLIT_CHUNKS);
|
|
78
|
+
const chunk = amountIn / count;
|
|
79
|
+
return Array.from({ length: PULSEX_SPLIT_CHUNKS }, (_, index) => index === PULSEX_SPLIT_CHUNKS - 1 ? amountIn - chunk * (count - 1n) : chunk);
|
|
80
|
+
};
|
|
81
|
+
/** Each candidate with a positive allocation, in candidate order — the order the legs execute in. */
|
|
82
|
+
const legsOf = (candidates, allocation) => candidates.flatMap((candidate, index) => {
|
|
83
|
+
const amountIn = allocation[index] ?? 0n;
|
|
84
|
+
return amountIn > 0n ? [{ candidate, amountIn }] : [];
|
|
85
|
+
});
|
|
86
|
+
/** What an allocation delivers in total, executed in candidate order. */
|
|
87
|
+
const allocationOutput = (candidates, allocation) => executePulseXSplitInOrder(legsOf(candidates, allocation)).reduce((total, amount) => total + amount, 0n);
|
|
88
|
+
/**
|
|
89
|
+
* Spreads a trade over ranked candidates, one chunk at a time. Each chunk goes
|
|
90
|
+
* to the candidate whose share, once raised by that chunk, makes the WHOLE
|
|
91
|
+
* split deliver the most — the split is re-executed in full each time
|
|
92
|
+
* ({@link executePulseXSplitInOrder}), so a shared pool is always priced with
|
|
93
|
+
* the reserves every leg leaves behind. A candidate not yet used is skipped
|
|
94
|
+
* once {@link PULSEX_SPLIT_MAX_LEGS} legs exist. Ties go to the earlier
|
|
95
|
+
* candidate.
|
|
96
|
+
*
|
|
97
|
+
* BOUNDED AND DETERMINISTIC. Work per quote is at most
|
|
98
|
+
* `PULSEX_SPLIT_CHUNKS × PULSEX_SPLIT_CANDIDATE_LIMIT` full executions (80),
|
|
99
|
+
* each of at most `PULSEX_SPLIT_MAX_LEGS` legs of at most three pools: under
|
|
100
|
+
* 750 constant-product evaluations, no network call.
|
|
101
|
+
*
|
|
102
|
+
* On pools that share nothing, each candidate's output is concave in its
|
|
103
|
+
* share, so this greedy fill reaches the best allocation on the chunk grid.
|
|
104
|
+
* Shared pools break that guarantee; {@link bestPulseXSplitQuote} still never
|
|
105
|
+
* returns a split worse than the best single path.
|
|
106
|
+
*
|
|
107
|
+
* @param candidates - ranked candidates, best first
|
|
108
|
+
* @param amountIn - the whole trade
|
|
109
|
+
* @returns the share of the trade each candidate spends, in candidate order
|
|
110
|
+
*/
|
|
111
|
+
export const allocatePulseXSplit = (candidates, amountIn) => chunksOf(amountIn).reduce((allocation, chunk) => {
|
|
112
|
+
const usedLegs = allocation.filter((amount) => amount > 0n).length;
|
|
113
|
+
const trials = candidates.flatMap((_, index) => {
|
|
114
|
+
const unused = (allocation[index] ?? 0n) === 0n;
|
|
115
|
+
if (unused && usedLegs >= PULSEX_SPLIT_MAX_LEGS)
|
|
116
|
+
return [];
|
|
117
|
+
const trial = allocation.map((amount, trialIndex) => (trialIndex === index ? amount + chunk : amount));
|
|
118
|
+
return [{ trial, output: allocationOutput(candidates, trial) }];
|
|
119
|
+
});
|
|
120
|
+
const best = trials.reduce((leader, entry) => (leader === null || entry.output > leader.output ? entry : leader), null);
|
|
121
|
+
return best === null ? allocation : best.trial;
|
|
122
|
+
}, candidates.map(() => 0n));
|
|
123
|
+
/**
|
|
124
|
+
* Price impact of a combined trade, in percent, measured with the same
|
|
125
|
+
* reference size the single-path figure uses
|
|
126
|
+
* (`amountIn / PULSEX_MARGINAL_REFERENCE_DIVISOR`).
|
|
127
|
+
*
|
|
128
|
+
* THE REFERENCE IS THE BEST MARGINAL RATE OF ANY CANDIDATE, not a blend of
|
|
129
|
+
* the legs' own rates. A split that leans on a path with a worse spot price
|
|
130
|
+
* pays for that, and the figure shows it. A blend would hide it.
|
|
131
|
+
*
|
|
132
|
+
* @param options.candidates - every ranked candidate
|
|
133
|
+
* @param options.amountIn - the whole trade
|
|
134
|
+
* @param options.deliveredAmount - what the combined trade delivers
|
|
135
|
+
* @returns the impact in percent, or null when the reference trade rounds away
|
|
136
|
+
*/
|
|
137
|
+
export const splitPriceImpact = (options) => {
|
|
138
|
+
const { candidates, amountIn, deliveredAmount } = options;
|
|
139
|
+
const referenceAmountIn = amountIn / PULSEX_MARGINAL_REFERENCE_DIVISOR;
|
|
140
|
+
const referenceAmountOut = candidates.reduce((best, candidate) => {
|
|
141
|
+
const output = singlePathOutput(candidate, referenceAmountIn);
|
|
142
|
+
return output > best ? output : best;
|
|
143
|
+
}, 0n);
|
|
144
|
+
return priceImpactAgainstMarginal({ amountIn, amountOut: deliveredAmount, referenceAmountIn, referenceAmountOut });
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* The best split of a PulseX trade, or null when no split beats the best
|
|
148
|
+
* single path by {@link PULSEX_SPLIT_MINIMUM_GAIN_BASIS_POINTS}.
|
|
149
|
+
*
|
|
150
|
+
* NEVER WORSE THAN ONE PATH. A null answer means "take the best single path";
|
|
151
|
+
* this never returns a split that delivers less than that path.
|
|
152
|
+
*
|
|
153
|
+
* @param candidates - every priced path, on every venue
|
|
154
|
+
* @param amountIn - the whole trade
|
|
155
|
+
* @returns the split with its legs in execution order, or null
|
|
156
|
+
*/
|
|
157
|
+
export const bestPulseXSplitQuote = (candidates, amountIn) => {
|
|
158
|
+
if (amountIn <= 0n)
|
|
159
|
+
return null;
|
|
160
|
+
const ranked = rankPulseXSplitCandidates(candidates, amountIn);
|
|
161
|
+
const singlePathDeliveredAmount = ranked[0]?.deliveredAmount;
|
|
162
|
+
if (singlePathDeliveredAmount === undefined)
|
|
163
|
+
return null;
|
|
164
|
+
const rankedCandidates = ranked.map(({ candidate }) => candidate);
|
|
165
|
+
const allocation = allocatePulseXSplit(rankedCandidates, amountIn);
|
|
166
|
+
const legs = legsOf(rankedCandidates, allocation);
|
|
167
|
+
if (legs.length < 2)
|
|
168
|
+
return null;
|
|
169
|
+
const legOutputs = executePulseXSplitInOrder(legs);
|
|
170
|
+
const deliveredAmount = legOutputs.reduce((total, amount) => total + amount, 0n);
|
|
171
|
+
const requiredAmount = singlePathDeliveredAmount + (singlePathDeliveredAmount * PULSEX_SPLIT_MINIMUM_GAIN_BASIS_POINTS) / BASIS_POINTS;
|
|
172
|
+
if (deliveredAmount < requiredAmount)
|
|
173
|
+
return null;
|
|
174
|
+
return {
|
|
175
|
+
legs: legs.map(({ candidate, amountIn: legAmountIn }, index) => ({
|
|
176
|
+
venue: candidate.venue,
|
|
177
|
+
path: candidate.path,
|
|
178
|
+
amountIn: legAmountIn,
|
|
179
|
+
deliveredAmount: legOutputs[index] ?? 0n,
|
|
180
|
+
})),
|
|
181
|
+
deliveredAmount,
|
|
182
|
+
singlePathDeliveredAmount,
|
|
183
|
+
priceImpactPercent: splitPriceImpact({ candidates: rankedCandidates, amountIn, deliveredAmount }),
|
|
184
|
+
};
|
|
185
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Hex } from 'viem';
|
|
2
2
|
import type { PublicClient } from 'viem';
|
|
3
|
-
import { type AssetNode, type HopEdge } from '../search/graph.js';
|
|
3
|
+
import { type AssetNode, type HopEdge, type PlannedHop } from '../search/graph.js';
|
|
4
4
|
import type { CarrierAskAnswer, CarrierExactResult } from './ask-answer.js';
|
|
5
5
|
/**
|
|
6
6
|
* One PulseX deployment a swap can execute on: the router the reader signs
|
|
@@ -30,31 +30,79 @@ export declare const pulseXVenues: readonly PulseXVenue[];
|
|
|
30
30
|
*/
|
|
31
31
|
export declare const pulseXRouterForEdge: (edge: Pick<HopEdge, "carrier" | "venue">) => Hex | null;
|
|
32
32
|
/**
|
|
33
|
-
* The
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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.
|
|
33
|
+
* The fixed Wrapped-PLS path from `from` to `to` — `[from, WPLS, to]`, or
|
|
34
|
+
* `[from, to]` when one side is WPLS — or null when both ends resolve to the
|
|
35
|
+
* same pool token (PLS<->WPLS is the wrap carrier's job).
|
|
40
36
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* `
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* `null` for a direct native-PLS PulseX hop, and `enumerate.ts` pruned it,
|
|
51
|
-
* leaving only the two-signature wrap-then-swap path.
|
|
37
|
+
* NOT THE PATH A HOP IS SIGNED ALONG. The carrier's own `exact` picks the
|
|
38
|
+
* best of many paths ({@link candidatePulseXPaths}) and records the winner on
|
|
39
|
+
* the hop's outcome; the signer reads it back with {@link pulseXSwapPathForHop}.
|
|
40
|
+
* This fixed path serves only the carrier's asynchronous `ask` — the
|
|
41
|
+
* interface's `getAmountsOut` read, which the route search never calls for a
|
|
42
|
+
* PulseX hop, because the search only asks aggregators live.
|
|
43
|
+
*
|
|
44
|
+
* Native PLS resolves to WPLS first ({@link poolTokenAddress}), so a path
|
|
45
|
+
* never starts or ends on the zero address, which has no pool.
|
|
52
46
|
*
|
|
53
47
|
* @param from - the hop's origin node
|
|
54
48
|
* @param to - the hop's destination node
|
|
55
|
-
* @returns the
|
|
49
|
+
* @returns the fixed path, or null when there is nothing for PulseX to swap
|
|
50
|
+
*/
|
|
51
|
+
export declare const pulseXWrappedNativePath: (from: AssetNode, to: AssetNode) => readonly Hex[] | null;
|
|
52
|
+
/**
|
|
53
|
+
* The swap path a planned PulseX hop must be signed along: the path its
|
|
54
|
+
* outcome records ({@link HopOutcome.swapPath}), which is the path the
|
|
55
|
+
* carrier's `exact` priced the figure on. NEVER REBUILT HERE — rebuilding a
|
|
56
|
+
* path at signing time is how a quote on one path becomes a signature on
|
|
57
|
+
* another.
|
|
58
|
+
*
|
|
59
|
+
* The recorded path is checked against the hop's own edge before it is
|
|
60
|
+
* returned: it must start at the edge's input pool token, end at its output
|
|
61
|
+
* pool token, hold only addresses, and name no token twice. A path that fails
|
|
62
|
+
* any check is refused (null) rather than signed.
|
|
63
|
+
*
|
|
64
|
+
* @param hop - the planned hop to sign
|
|
65
|
+
* @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
|
|
66
|
+
*/
|
|
67
|
+
export declare const pulseXSwapPathForHop: (hop: Pick<PlannedHop, "edge" | "outcome">) => readonly Hex[] | null;
|
|
68
|
+
/** One pool a swap path crosses, read in the direction of travel. */
|
|
69
|
+
export type PulseXPathStep = {
|
|
70
|
+
/** The pool's reserve of the token flowing in. */
|
|
71
|
+
readonly reserveIn: bigint;
|
|
72
|
+
/** The pool's reserve of the token flowing out. */
|
|
73
|
+
readonly reserveOut: bigint;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* What `amountIn` delivers after crossing every pool of a path in order,
|
|
77
|
+
* chaining {@link constantProductAmountOut}.
|
|
78
|
+
*
|
|
79
|
+
* @param amountIn - the amount entering the first pool
|
|
80
|
+
* @param steps - the pools in order of travel
|
|
81
|
+
* @returns the amount leaving the last pool; 0 when any pool cannot pay out
|
|
82
|
+
*/
|
|
83
|
+
export declare const amountOutAlongPath: (amountIn: bigint, steps: readonly PulseXPathStep[]) => bigint;
|
|
84
|
+
/**
|
|
85
|
+
* How far a trade's executed rate falls below the marginal (near-spot) rate,
|
|
86
|
+
* in percent. The marginal rate is a tiny reference trade through the same
|
|
87
|
+
* pools (`amountIn / PULSEX_MARGINAL_REFERENCE_DIVISOR`). The swap fee rides
|
|
88
|
+
* in both rates, so it cancels and the gap is price impact alone. A path
|
|
89
|
+
* through several pools composes by itself: the reference trade crosses the
|
|
90
|
+
* same pools in the same order.
|
|
91
|
+
*
|
|
92
|
+
* impact = (1 - (amountOut / amountIn) / (referenceAmountOut / referenceAmountIn)) * 100
|
|
93
|
+
*
|
|
94
|
+
* @param options.amountIn - the full trade's input
|
|
95
|
+
* @param options.amountOut - what the full trade delivers
|
|
96
|
+
* @param options.referenceAmountIn - the reference trade's input
|
|
97
|
+
* @param options.referenceAmountOut - what the reference trade delivers
|
|
98
|
+
* @returns the impact in percent, never below 0; null when a reference figure is zero (impact cannot be measured)
|
|
56
99
|
*/
|
|
57
|
-
export declare const
|
|
100
|
+
export declare const priceImpactAgainstMarginal: (options: {
|
|
101
|
+
readonly amountIn: bigint;
|
|
102
|
+
readonly amountOut: bigint;
|
|
103
|
+
readonly referenceAmountIn: bigint;
|
|
104
|
+
readonly referenceAmountOut: bigint;
|
|
105
|
+
}) => number | null;
|
|
58
106
|
/** What {@link createPulseXCarrier} needs. */
|
|
59
107
|
export type CreatePulseXCarrierOptions = {
|
|
60
108
|
/**
|
|
@@ -1,12 +1,13 @@
|
|
|
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
5
|
import { Chains, nativeAssetOut, toChain, uniV2Factories, uniV2Routers } from '../../config.js';
|
|
6
6
|
import { sameAddress } from '../../ecosystems.js';
|
|
7
7
|
import { pulseXConversionSwapPath } from '../providers.js';
|
|
8
|
-
import { pulsechainChainId } from '../producers.js';
|
|
8
|
+
import { PULSEX_MARGINAL_REFERENCE_DIVISOR, pulsechainChainId } from '../producers.js';
|
|
9
9
|
import { assetNodeKey } from '../search/graph.js';
|
|
10
|
+
import { bestPulseXPathQuote, candidatePulseXPaths, pulseXHubTokens } from './pulsex-paths.js';
|
|
10
11
|
/**
|
|
11
12
|
* PulseX (the PulseChain-native decentralized exchange, used here for a
|
|
12
13
|
* conversion swap leg) as a plug-in `Carrier` for `@gibs/quotes`'s
|
|
@@ -19,6 +20,8 @@ import { assetNodeKey } from '../search/graph.js';
|
|
|
19
20
|
*/
|
|
20
21
|
/** WPLS, the wrapped-native intermediary {@link pulseXConversionSwapPath} routes conversions through. */
|
|
21
22
|
const wplsAddress = nativeAssetOut[Chains.PLS];
|
|
23
|
+
/** The hub addresses every candidate path is built from — see {@link pulseXHubTokens}. */
|
|
24
|
+
const hubAddresses = pulseXHubTokens.map((hub) => hub.address);
|
|
22
25
|
/**
|
|
23
26
|
* Every PulseX deployment on PulseChain, V1 first: `uniV2Routers` paired with
|
|
24
27
|
* `uniV2Factories` by index. Each factory is the router's own
|
|
@@ -70,65 +73,104 @@ const pulseXVenueForEdge = (edge) => {
|
|
|
70
73
|
* MIRRORS `useRouteSearch.ts`'s `quotePathAddress` (the UI's async `ask`
|
|
71
74
|
* already resolves native this way through that function) — this is the
|
|
72
75
|
* SAME rule, restated here because `prepare`/`exact` never go through the
|
|
73
|
-
* UI's `readAmountOut` at all; see {@link
|
|
76
|
+
* UI's `readAmountOut` at all; see {@link pulseXCandidatePaths}'s own doc.
|
|
74
77
|
*
|
|
75
78
|
* @param node - the node to resolve
|
|
76
79
|
* @returns the node's own address, or {@link wplsAddress} for native PLS
|
|
77
80
|
*/
|
|
78
81
|
const poolTokenAddress = (node) => (sameAddress(node.address, zeroAddress, 'evm') ? wplsAddress : node.address);
|
|
79
82
|
/**
|
|
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.
|
|
83
|
+
* The fixed Wrapped-PLS path from `from` to `to` — `[from, WPLS, to]`, or
|
|
84
|
+
* `[from, to]` when one side is WPLS — or null when both ends resolve to the
|
|
85
|
+
* same pool token (PLS<->WPLS is the wrap carrier's job).
|
|
87
86
|
*
|
|
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.
|
|
87
|
+
* NOT THE PATH A HOP IS SIGNED ALONG. The carrier's own `exact` picks the
|
|
88
|
+
* best of many paths ({@link candidatePulseXPaths}) and records the winner on
|
|
89
|
+
* the hop's outcome; the signer reads it back with {@link pulseXSwapPathForHop}.
|
|
90
|
+
* This fixed path serves only the carrier's asynchronous `ask` — the
|
|
91
|
+
* interface's `getAmountsOut` read, which the route search never calls for a
|
|
92
|
+
* PulseX hop, because the search only asks aggregators live.
|
|
93
|
+
*
|
|
94
|
+
* Native PLS resolves to WPLS first ({@link poolTokenAddress}), so a path
|
|
95
|
+
* never starts or ends on the zero address, which has no pool.
|
|
99
96
|
*
|
|
100
97
|
* @param from - the hop's origin node
|
|
101
98
|
* @param to - the hop's destination node
|
|
102
|
-
* @returns the
|
|
99
|
+
* @returns the fixed path, or null when there is nothing for PulseX to swap
|
|
103
100
|
*/
|
|
104
|
-
export const
|
|
101
|
+
export const pulseXWrappedNativePath = (from, to) => {
|
|
105
102
|
const fromAddress = poolTokenAddress(from);
|
|
106
103
|
const toAddress = poolTokenAddress(to);
|
|
107
104
|
if (sameAddress(fromAddress, toAddress, 'evm'))
|
|
108
105
|
return null;
|
|
109
106
|
return pulseXConversionSwapPath(fromAddress, toAddress, wplsAddress);
|
|
110
107
|
};
|
|
108
|
+
/**
|
|
109
|
+
* Every path a PulseX hop from `from` to `to` may be priced along, or none
|
|
110
|
+
* when both ends resolve to the same pool token. EVERY PULSEX PRICE AND
|
|
111
|
+
* POOL READ IN THIS FILE GOES THROUGH THIS ONE FUNCTION, so `prepare` reads
|
|
112
|
+
* exactly the pools `exact` will look up, and native PLS resolves to WPLS
|
|
113
|
+
* the same way in both ({@link poolTokenAddress} — a path that started on
|
|
114
|
+
* the zero address once left every native-PLS hop unpriced).
|
|
115
|
+
*
|
|
116
|
+
* @param from - the hop's origin node
|
|
117
|
+
* @param to - the hop's destination node
|
|
118
|
+
* @returns the candidate paths, direct first; empty when there is nothing to swap
|
|
119
|
+
*/
|
|
120
|
+
const pulseXCandidatePaths = (from, to) => {
|
|
121
|
+
const fromAddress = poolTokenAddress(from);
|
|
122
|
+
const toAddress = poolTokenAddress(to);
|
|
123
|
+
if (sameAddress(fromAddress, toAddress, 'evm'))
|
|
124
|
+
return [];
|
|
125
|
+
return candidatePulseXPaths({ from: fromAddress, to: toAddress, wrappedNative: wplsAddress, hubs: hubAddresses });
|
|
126
|
+
};
|
|
111
127
|
/**
|
|
112
128
|
* Whether PulseX can carry funds directly from `from` to `to`: both must be
|
|
113
|
-
*
|
|
114
|
-
*
|
|
129
|
+
* Ethereum-Virtual-Machine tokens on PulseChain itself — PulseX runs only on
|
|
130
|
+
* PulseChain, never on an entry chain — that resolve to two different pool
|
|
131
|
+
* tokens, so there is at least one path to price.
|
|
115
132
|
*
|
|
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.
|
|
133
|
+
* NO TOKEN ALLOWLIST IS CONSULTED, on purpose: whether a pool exists is the
|
|
134
|
+
* reserve read's question, answered in `prepare`, not this one's.
|
|
124
135
|
*/
|
|
125
136
|
const pulseXCarries = (from, to) => {
|
|
126
137
|
if (from.ecosystem !== 'evm' || to.ecosystem !== 'evm')
|
|
127
138
|
return false;
|
|
128
139
|
if (from.chainId !== pulsechainChainId || to.chainId !== pulsechainChainId)
|
|
129
140
|
return false;
|
|
130
|
-
|
|
131
|
-
|
|
141
|
+
return pulseXCandidatePaths(from, to).length > 0;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* The swap path a planned PulseX hop must be signed along: the path its
|
|
145
|
+
* outcome records ({@link HopOutcome.swapPath}), which is the path the
|
|
146
|
+
* carrier's `exact` priced the figure on. NEVER REBUILT HERE — rebuilding a
|
|
147
|
+
* path at signing time is how a quote on one path becomes a signature on
|
|
148
|
+
* another.
|
|
149
|
+
*
|
|
150
|
+
* The recorded path is checked against the hop's own edge before it is
|
|
151
|
+
* returned: it must start at the edge's input pool token, end at its output
|
|
152
|
+
* pool token, hold only addresses, and name no token twice. A path that fails
|
|
153
|
+
* any check is refused (null) rather than signed.
|
|
154
|
+
*
|
|
155
|
+
* @param hop - the planned hop to sign
|
|
156
|
+
* @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
|
|
157
|
+
*/
|
|
158
|
+
export const pulseXSwapPathForHop = (hop) => {
|
|
159
|
+
if (hop.edge.carrier !== 'PulseX')
|
|
160
|
+
return null;
|
|
161
|
+
const swapPath = hop.outcome.swapPath;
|
|
162
|
+
if (swapPath === undefined || swapPath.length < 2)
|
|
163
|
+
return null;
|
|
164
|
+
if (!swapPath.every((address) => isAddress(address, { strict: false })))
|
|
165
|
+
return null;
|
|
166
|
+
const path = swapPath.map((address) => getAddress(address));
|
|
167
|
+
if (new Set(path).size !== path.length)
|
|
168
|
+
return null;
|
|
169
|
+
if (!sameAddress(path[0], poolTokenAddress(hop.edge.from), 'evm'))
|
|
170
|
+
return null;
|
|
171
|
+
if (!sameAddress(path[path.length - 1], poolTokenAddress(hop.edge.to), 'evm'))
|
|
172
|
+
return null;
|
|
173
|
+
return path;
|
|
132
174
|
};
|
|
133
175
|
/**
|
|
134
176
|
* PulseX's own constant-product swap fee, as a numerator over
|
|
@@ -172,6 +214,44 @@ const constantProductAmountOut = (amountIn, reserveIn, reserveOut) => {
|
|
|
172
214
|
const denominator = reserveIn * PULSEX_FEE_DENOMINATOR + amountInWithFee;
|
|
173
215
|
return numerator / denominator;
|
|
174
216
|
};
|
|
217
|
+
/**
|
|
218
|
+
* What `amountIn` delivers after crossing every pool of a path in order,
|
|
219
|
+
* chaining {@link constantProductAmountOut}.
|
|
220
|
+
*
|
|
221
|
+
* @param amountIn - the amount entering the first pool
|
|
222
|
+
* @param steps - the pools in order of travel
|
|
223
|
+
* @returns the amount leaving the last pool; 0 when any pool cannot pay out
|
|
224
|
+
*/
|
|
225
|
+
export const amountOutAlongPath = (amountIn, steps) => steps.reduce((runningAmount, step) => constantProductAmountOut(runningAmount, step.reserveIn, step.reserveOut), amountIn);
|
|
226
|
+
/**
|
|
227
|
+
* How far a trade's executed rate falls below the marginal (near-spot) rate,
|
|
228
|
+
* in percent. The marginal rate is a tiny reference trade through the same
|
|
229
|
+
* pools (`amountIn / PULSEX_MARGINAL_REFERENCE_DIVISOR`). The swap fee rides
|
|
230
|
+
* in both rates, so it cancels and the gap is price impact alone. A path
|
|
231
|
+
* through several pools composes by itself: the reference trade crosses the
|
|
232
|
+
* same pools in the same order.
|
|
233
|
+
*
|
|
234
|
+
* impact = (1 - (amountOut / amountIn) / (referenceAmountOut / referenceAmountIn)) * 100
|
|
235
|
+
*
|
|
236
|
+
* @param options.amountIn - the full trade's input
|
|
237
|
+
* @param options.amountOut - what the full trade delivers
|
|
238
|
+
* @param options.referenceAmountIn - the reference trade's input
|
|
239
|
+
* @param options.referenceAmountOut - what the reference trade delivers
|
|
240
|
+
* @returns the impact in percent, never below 0; null when a reference figure is zero (impact cannot be measured)
|
|
241
|
+
*/
|
|
242
|
+
export const priceImpactAgainstMarginal = (options) => {
|
|
243
|
+
const { amountIn, amountOut, referenceAmountIn, referenceAmountOut } = options;
|
|
244
|
+
if (amountIn <= 0n || referenceAmountIn <= 0n || referenceAmountOut <= 0n)
|
|
245
|
+
return null;
|
|
246
|
+
// The output the trade would deliver at the marginal rate with no impact.
|
|
247
|
+
const idealAmountOut = (referenceAmountOut * amountIn) / referenceAmountIn;
|
|
248
|
+
if (idealAmountOut <= 0n)
|
|
249
|
+
return null;
|
|
250
|
+
if (amountOut >= idealAmountOut)
|
|
251
|
+
return 0;
|
|
252
|
+
const MILLIONTHS = 1000000n;
|
|
253
|
+
return Number(((idealAmountOut - amountOut) * MILLIONTHS * 100n) / idealAmountOut) / Number(MILLIONTHS);
|
|
254
|
+
};
|
|
175
255
|
/** The stable key one pool is stored and looked up under — the two token addresses, lower-cased and sorted, so either query order finds the same entry. */
|
|
176
256
|
const poolKey = (tokenA, tokenB) => {
|
|
177
257
|
const [lower, higher] = [tokenA.toLowerCase(), tokenB.toLowerCase()].sort();
|
|
@@ -181,6 +261,32 @@ const poolKey = (tokenA, tokenB) => {
|
|
|
181
261
|
const venuePoolKey = (venue, tokenA, tokenB) => `${venue.factory.toLowerCase()}|${poolKey(tokenA, tokenB)}`;
|
|
182
262
|
/** Every consecutive (tokenIn, tokenOut) hop a PulseX swap path implies. */
|
|
183
263
|
const hopsAlongPath = (path) => path.slice(0, -1).map((tokenIn, index) => [tokenIn, path[index + 1]]);
|
|
264
|
+
/**
|
|
265
|
+
* The pools a path crosses on one venue, each read in the direction of
|
|
266
|
+
* travel, or null when any of them was never read — `getPair` found no pool,
|
|
267
|
+
* or the read failed. A path with a missing pool cannot be priced, exactly as
|
|
268
|
+
* the router's own `getAmountsOut` would revert on it.
|
|
269
|
+
*
|
|
270
|
+
* @param options.venue - the deployment whose pools the path trades against
|
|
271
|
+
* @param options.path - the tokens, in order
|
|
272
|
+
* @param options.reservesByPoolKey - every pool `prepare` read, keyed by {@link venuePoolKey}
|
|
273
|
+
* @returns the steps in order, or null when a pool is missing
|
|
274
|
+
*/
|
|
275
|
+
const stepsAlongPath = (options) => {
|
|
276
|
+
const { venue, path, reservesByPoolKey } = options;
|
|
277
|
+
const steps = [];
|
|
278
|
+
for (const [tokenIn, tokenOut] of hopsAlongPath(path)) {
|
|
279
|
+
const reserves = reservesByPoolKey.get(venuePoolKey(venue, tokenIn, tokenOut));
|
|
280
|
+
if (reserves === undefined)
|
|
281
|
+
return null;
|
|
282
|
+
const tokenInIsLower = tokenIn.toLowerCase() < tokenOut.toLowerCase();
|
|
283
|
+
steps.push({
|
|
284
|
+
reserveIn: tokenInIsLower ? reserves.reserveOfLowerAddress : reserves.reserveOfHigherAddress,
|
|
285
|
+
reserveOut: tokenInIsLower ? reserves.reserveOfHigherAddress : reserves.reserveOfLowerAddress,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return steps;
|
|
289
|
+
};
|
|
184
290
|
/**
|
|
185
291
|
* Batch-reads reserves for every pool `nodes`' candidate PulseX paths touch,
|
|
186
292
|
* on EVERY venue in `venues`, once, so {@link createPulseXCarrier}'s `exact`
|
|
@@ -197,6 +303,14 @@ const hopsAlongPath = (path) => path.slice(0, -1).map((tokenIn, index) => [token
|
|
|
197
303
|
* the second read cannot join the first — but every venue's `getPair` rides
|
|
198
304
|
* the first multicall and every pool's `getReserves` rides the second.
|
|
199
305
|
*
|
|
306
|
+
* THE HUB SEARCH WIDENS EACH READ, NOT THEIR NUMBER. Every pool any candidate
|
|
307
|
+
* path crosses is read here ({@link pulseXCandidatePaths}), so `exact` can
|
|
308
|
+
* price every path with no further call. Measured 2026-10-06: an 8-token
|
|
309
|
+
* PulseChain universe asked 84 entries across the two reads (the fixed
|
|
310
|
+
* Wrapped-PLS path asked about 16). The shared multicall helper splits a read
|
|
311
|
+
* past its request-size budget into sequential posts; `getPair` crosses that
|
|
312
|
+
* line at about 11 pool tokens (198 entries over two venues).
|
|
313
|
+
*
|
|
200
314
|
* @param nodes - every node the search might branch through
|
|
201
315
|
* @param venues - the venues this carrier offers
|
|
202
316
|
* @param clientForChain - see {@link CreatePulseXCarrierOptions}'s own field
|
|
@@ -215,11 +329,10 @@ const preparePulseXReserves = async ({ nodes, venues, clientForChain, }) => {
|
|
|
215
329
|
continue;
|
|
216
330
|
if (!pulseXCarries(from, to))
|
|
217
331
|
continue;
|
|
218
|
-
const path
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
poolTokenPairs.set(poolKey(tokenIn, tokenOut), [tokenIn, tokenOut]);
|
|
332
|
+
for (const path of pulseXCandidatePaths(from, to)) {
|
|
333
|
+
for (const [tokenIn, tokenOut] of hopsAlongPath(path)) {
|
|
334
|
+
poolTokenPairs.set(poolKey(tokenIn, tokenOut), [tokenIn, tokenOut]);
|
|
335
|
+
}
|
|
223
336
|
}
|
|
224
337
|
}
|
|
225
338
|
}
|
|
@@ -359,33 +472,39 @@ export const createPulseXCarrier = (options) => {
|
|
|
359
472
|
const venue = pulseXVenueForEdge(edge);
|
|
360
473
|
if (venue === null || !venues.includes(venue))
|
|
361
474
|
return null;
|
|
362
|
-
|
|
363
|
-
|
|
475
|
+
// EVERY CANDIDATE PATH IS PRICED, AND THE BEST ONE WINS. A path with a
|
|
476
|
+
// pool `prepare` never found is left out — the same as a router call
|
|
477
|
+
// that would revert — and a path that pays out nothing is never picked.
|
|
478
|
+
const quotes = pulseXCandidatePaths(edge.from, edge.to).flatMap((path) => {
|
|
479
|
+
const steps = stepsAlongPath({ venue, path, reservesByPoolKey });
|
|
480
|
+
return steps === null ? [] : [{ path, steps, deliveredAmount: amountOutAlongPath(amountIn, steps) }];
|
|
481
|
+
});
|
|
482
|
+
// No path had every pool read — a structural non-answer, not a refusal
|
|
483
|
+
// (`exact`'s own doc: `null` means "nothing to say"). NOTE THIS DOES NOT
|
|
484
|
+
// FALL BACK TO AN ESTIMATE: a carrier with `exact` defined never yields
|
|
485
|
+
// an `'estimate'` outcome (see `enumerate.ts`'s `resolveHopOutcome`,
|
|
486
|
+
// branch 1) — `null` here PRUNES the hop entirely, the same as a refusal.
|
|
487
|
+
if (quotes.length === 0)
|
|
364
488
|
return null;
|
|
365
|
-
const
|
|
366
|
-
if (
|
|
367
|
-
return null;
|
|
368
|
-
let runningAmount = amountIn;
|
|
369
|
-
for (const [tokenIn, tokenOut] of hops) {
|
|
370
|
-
const reserves = reservesByPoolKey.get(venuePoolKey(venue, tokenIn, tokenOut));
|
|
371
|
-
// `prepare` never read this pool — a structural non-answer, not a
|
|
372
|
-
// refusal (`exact`'s own doc: `null` means "nothing to say"). NOTE
|
|
373
|
-
// THIS DOES NOT FALL BACK TO AN ESTIMATE: a carrier with `exact`
|
|
374
|
-
// defined never yields an `'estimate'` outcome (see
|
|
375
|
-
// `enumerate.ts`'s `resolveHopOutcome`, branch 1) — `null` here
|
|
376
|
-
// PRUNES the hop entirely, the same as a refusal.
|
|
377
|
-
if (reserves === undefined)
|
|
378
|
-
return null;
|
|
379
|
-
const tokenInIsLower = tokenIn.toLowerCase() < tokenOut.toLowerCase();
|
|
380
|
-
const reserveIn = tokenInIsLower ? reserves.reserveOfLowerAddress : reserves.reserveOfHigherAddress;
|
|
381
|
-
const reserveOut = tokenInIsLower ? reserves.reserveOfHigherAddress : reserves.reserveOfLowerAddress;
|
|
382
|
-
const hopAmountOut = constantProductAmountOut(runningAmount, reserveIn, reserveOut);
|
|
383
|
-
if (hopAmountOut <= 0n) {
|
|
384
|
-
return { outcome: 'refused', refusal: { kind: 'no-quote', reason: 'no liquidity on this PulseX path', minimumAmount: null } };
|
|
385
|
-
}
|
|
386
|
-
runningAmount = hopAmountOut;
|
|
489
|
+
const best = bestPulseXPathQuote(quotes);
|
|
490
|
+
if (best === null) {
|
|
491
|
+
return { outcome: 'refused', refusal: { kind: 'no-quote', reason: 'no liquidity on this PulseX path', minimumAmount: null } };
|
|
387
492
|
}
|
|
388
|
-
|
|
493
|
+
// Price impact is measured on the CHOSEN path's own pools, with the same
|
|
494
|
+
// reference trade as before.
|
|
495
|
+
const referenceAmountIn = amountIn / PULSEX_MARGINAL_REFERENCE_DIVISOR;
|
|
496
|
+
const priceImpactPercent = priceImpactAgainstMarginal({
|
|
497
|
+
amountIn,
|
|
498
|
+
amountOut: best.deliveredAmount,
|
|
499
|
+
referenceAmountIn,
|
|
500
|
+
referenceAmountOut: amountOutAlongPath(referenceAmountIn, best.steps),
|
|
501
|
+
});
|
|
502
|
+
// The path rides on the answer, so the signer signs exactly what was
|
|
503
|
+
// priced (`pulseXSwapPathForHop`). Impact is left off (not zero) when the
|
|
504
|
+
// reference trade rounds away: an unknown impact must stay unknown so the
|
|
505
|
+
// quality gate withholds the plan.
|
|
506
|
+
const delivered = { outcome: 'delivered', deliveredAmount: best.deliveredAmount, swapPath: best.path };
|
|
507
|
+
return priceImpactPercent === null ? delivered : { ...delivered, priceImpactPercent };
|
|
389
508
|
};
|
|
390
509
|
return {
|
|
391
510
|
id: 'PulseX',
|