@gibs/bridge-sdk 1.19.0 → 1.20.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/dist/abis.js +0 -2
- package/dist/bridge-limits.js +0 -38
- package/dist/chain-info.js +0 -116
- package/dist/chain-names.js +0 -151
- package/dist/chains.js +0 -198
- package/dist/cjs/abis.js +0 -2
- package/dist/cjs/bridge-limits.js +0 -38
- package/dist/cjs/chain-info.js +0 -116
- package/dist/cjs/chain-names.js +0 -151
- package/dist/cjs/chains.js +0 -198
- package/dist/cjs/claims.js +0 -60
- package/dist/cjs/config.js +1 -208
- package/dist/cjs/consolidated-quote.js +0 -465
- package/dist/cjs/destination-calldata.js +0 -71
- package/dist/cjs/ecosystems.js +0 -183
- package/dist/cjs/execution.js +0 -92
- package/dist/cjs/fee-type.js +0 -1
- package/dist/cjs/hyperliquid-execution.js +0 -98
- package/dist/cjs/image-links.js +0 -108
- package/dist/cjs/lifi-execution-guard.js +65 -172
- package/dist/cjs/near-intents-conformance.js +0 -110
- package/dist/cjs/near-intents-route.js +0 -149
- package/dist/cjs/near-intents.js +0 -562
- package/dist/cjs/origin-deposit.js +0 -56
- package/dist/cjs/pre-quote.js +0 -80
- package/dist/cjs/prices.js +0 -113
- package/dist/cjs/providers.js +0 -83
- package/dist/cjs/quote-fees.js +0 -80
- package/dist/cjs/referral.js +0 -75
- package/dist/cjs/relay-currencies.js +0 -31
- package/dist/cjs/relay-depository.js +52 -77
- package/dist/cjs/relay-quote.js +0 -394
- package/dist/cjs/route-steps.js +0 -100
- package/dist/cjs/routing/amounts.js +0 -83
- package/dist/cjs/routing/candidate-route.js +0 -73
- package/dist/cjs/routing/candidate.js +0 -46
- package/dist/cjs/routing/carrier-provider-key.js +0 -27
- package/dist/cjs/routing/carriers/omnibridge.js +0 -120
- package/dist/cjs/routing/carriers/pulsex-paths.js +0 -57
- package/dist/cjs/routing/carriers/pulsex-split.js +0 -92
- package/dist/cjs/routing/carriers/pulsex.js +0 -279
- package/dist/cjs/routing/carriers/wrap.js +0 -57
- package/dist/cjs/routing/composed-route-quality.js +0 -95
- package/dist/cjs/routing/entry-chain-anchor.js +0 -27
- package/dist/cjs/routing/format.js +0 -50
- package/dist/cjs/routing/index.js +0 -28
- package/dist/cjs/routing/producers.js +0 -854
- package/dist/cjs/routing/providers.js +0 -27
- package/dist/cjs/routing/search/compose.js +0 -56
- package/dist/cjs/routing/search/graph.js +0 -364
- package/dist/cjs/routing/search/plan-route-data.js +0 -508
- package/dist/cjs/routing/search/score.js +0 -344
- package/dist/cjs/routing/swap-transactions.js +0 -74
- package/dist/cjs/routing/usd-value.js +0 -12
- package/dist/cjs/settings.js +1 -456
- package/dist/cjs/swap-venues.js +0 -17
- package/dist/cjs/ton-connect.js +0 -82
- package/dist/cjs/ton-execution.js +0 -6
- package/dist/cjs/ton.js +0 -42
- package/dist/cjs/tron-execution.js +0 -145
- package/dist/claims.js +0 -60
- package/dist/config.js +1 -211
- package/dist/consolidated-quote.js +0 -465
- package/dist/destination-calldata.js +0 -71
- package/dist/ecosystems.js +0 -183
- package/dist/execution.js +0 -92
- package/dist/fee-type.js +0 -2
- package/dist/hyperliquid-execution.js +0 -98
- package/dist/image-links.js +0 -108
- package/dist/lifi-execution-guard.js +65 -172
- package/dist/near-intents-conformance.js +0 -110
- package/dist/near-intents-route.js +0 -149
- package/dist/near-intents.js +0 -562
- package/dist/origin-deposit.js +0 -56
- package/dist/pre-quote.js +0 -80
- package/dist/prices.js +0 -113
- package/dist/providers.js +0 -83
- package/dist/quote-fees.js +0 -80
- package/dist/referral.js +0 -75
- package/dist/relay-currencies.js +0 -31
- package/dist/relay-depository.js +52 -77
- package/dist/relay-quote.js +0 -394
- package/dist/route-steps.js +0 -100
- package/dist/routing/amounts.js +0 -83
- package/dist/routing/candidate-route.js +0 -73
- package/dist/routing/candidate.js +0 -46
- package/dist/routing/carrier-provider-key.js +0 -27
- package/dist/routing/carriers/omnibridge.js +0 -120
- package/dist/routing/carriers/pulsex-paths.js +0 -57
- package/dist/routing/carriers/pulsex-split.js +0 -92
- package/dist/routing/carriers/pulsex.js +0 -279
- package/dist/routing/carriers/wrap.js +0 -57
- package/dist/routing/composed-route-quality.js +0 -95
- package/dist/routing/entry-chain-anchor.js +0 -27
- package/dist/routing/format.js +0 -50
- package/dist/routing/index.js +0 -28
- package/dist/routing/producers.js +0 -854
- package/dist/routing/providers.js +0 -27
- package/dist/routing/search/compose.js +0 -56
- package/dist/routing/search/graph.js +0 -364
- package/dist/routing/search/plan-route-data.js +0 -508
- package/dist/routing/search/score.js +0 -344
- package/dist/routing/swap-transactions.js +0 -74
- package/dist/routing/usd-value.js +0 -12
- package/dist/settings.js +1 -456
- package/dist/swap-venues.js +0 -17
- package/dist/ton-connect.js +0 -82
- package/dist/ton-execution.js +0 -6
- package/dist/ton.js +0 -42
- package/dist/tron-execution.js +0 -145
- package/package.json +3 -3
|
@@ -45,60 +45,18 @@ const providers_js_1 = require("../providers.js");
|
|
|
45
45
|
const producers_js_1 = require("../producers.js");
|
|
46
46
|
const graph_js_1 = require("../search/graph.js");
|
|
47
47
|
const pulsex_paths_js_1 = require("./pulsex-paths.js");
|
|
48
|
-
/**
|
|
49
|
-
* PulseX (the PulseChain-native decentralized exchange, used here for a
|
|
50
|
-
* conversion swap leg) as a plug-in `Carrier` for `@gibs/quotes`'s
|
|
51
|
-
* route-search engine. GIBS-SPECIFIC — see `docs/quote-service.md`'s
|
|
52
|
-
* "Carriers" section: "never built in ... PulseX." Registered from
|
|
53
|
-
* `packages/ui/src/lib/state/useRouteSearch.ts` / `route-search-quotes.ts`.
|
|
54
|
-
*
|
|
55
|
-
* See `omnibridge.ts`'s head doc for why this returns a plain object shaped
|
|
56
|
-
* like `Carrier` rather than importing that type.
|
|
57
|
-
*/
|
|
58
|
-
/** The hub addresses every candidate path on PulseChain is built from — see {@link pulseXHubTokens}. */
|
|
59
48
|
const hubAddresses = pulsex_paths_js_1.pulseXHubTokens.map((hub) => hub.address);
|
|
60
|
-
/**
|
|
61
|
-
* The wrapped-native token a chain's swaps route through, or undefined when
|
|
62
|
-
* this package does not know one for the chain. Without it a path on that
|
|
63
|
-
* chain can only be direct.
|
|
64
|
-
*
|
|
65
|
-
* @param chainId - the numeric chain id
|
|
66
|
-
*/
|
|
67
49
|
const wrappedNativeFor = (chainId) => {
|
|
68
50
|
const wrapped = config_js_1.nativeAssetOut[(0, config_js_1.toChain)(chainId)];
|
|
69
51
|
return wrapped === undefined ? undefined : (0, viem_1.getAddress)(wrapped);
|
|
70
52
|
};
|
|
71
|
-
/**
|
|
72
|
-
* The hub tokens a path on `chainId` may pass through. PulseChain has the
|
|
73
|
-
* full list; any other chain has only its wrapped-native token, because this
|
|
74
|
-
* package knows no other hubs.
|
|
75
|
-
*/
|
|
76
53
|
const hubsFor = (chainId) => {
|
|
77
54
|
if (chainId === producers_js_1.pulsechainChainId)
|
|
78
55
|
return hubAddresses;
|
|
79
56
|
const wrapped = wrappedNativeFor(chainId);
|
|
80
57
|
return wrapped === undefined ? [] : [wrapped];
|
|
81
58
|
};
|
|
82
|
-
/**
|
|
83
|
-
* Every PulseX deployment on PulseChain, V1 first: the PulseChain entry of
|
|
84
|
-
* {@link defaultSwapVenues}. Frozen. It lists the DEFAULTS only — a carrier
|
|
85
|
-
* built with its own `venues` offers those instead.
|
|
86
|
-
*/
|
|
87
59
|
exports.pulseXVenues = config_js_1.defaultSwapVenues[producers_js_1.pulsechainChainId] ?? [];
|
|
88
|
-
/**
|
|
89
|
-
* The {@link SwapVenue} a swap edge runs on: the one its figure was quoted
|
|
90
|
-
* against. Reads the edge's own {@link HopEdge.venue}; an edge with none runs
|
|
91
|
-
* on the FIRST venue of its chain (V1 for PulseX) — every swap edge built
|
|
92
|
-
* before venues existed was quoted and signed there, and the same-chain swap
|
|
93
|
-
* sender's own hand-built edge still is.
|
|
94
|
-
*
|
|
95
|
-
* NEVER FALLS BACK TO ANOTHER CHAIN OR ANOTHER ROUTER. An edge that names a
|
|
96
|
-
* router `venues` does not list gets null, so the caller can refuse it.
|
|
97
|
-
*
|
|
98
|
-
* @param venues - the venues the caller allows
|
|
99
|
-
* @param edge - the edge to sign; `from` names the chain, and defaults to PulseChain when absent
|
|
100
|
-
* @returns the venue, or null when the edge is not a swap edge or names a router that is not in `venues`
|
|
101
|
-
*/
|
|
102
60
|
const venueForEdge = (venues, edge) => {
|
|
103
61
|
if (edge.carrier !== 'PulseX')
|
|
104
62
|
return null;
|
|
@@ -108,51 +66,9 @@ const venueForEdge = (venues, edge) => {
|
|
|
108
66
|
return (0, swap_venues_js_1.findSwapVenue)(venues, chainId, edge.venue);
|
|
109
67
|
};
|
|
110
68
|
exports.venueForEdge = venueForEdge;
|
|
111
|
-
/**
|
|
112
|
-
* The router a PulseX edge must be signed on, judged against the DEFAULT
|
|
113
|
-
* venues only. Prefer {@link venueForEdge} with the venues your host passes.
|
|
114
|
-
*
|
|
115
|
-
* @param edge - the edge to sign
|
|
116
|
-
* @returns the router, or null when the edge is not a swap edge or names a router that is not a default
|
|
117
|
-
*/
|
|
118
69
|
const pulseXRouterForEdge = (edge) => (0, exports.venueForEdge)(config_js_1.defaultSwapVenues, edge)?.router ?? null;
|
|
119
70
|
exports.pulseXRouterForEdge = pulseXRouterForEdge;
|
|
120
|
-
/**
|
|
121
|
-
* A node's own address for a pool lookup: native (the zero address) is not a
|
|
122
|
-
* token the router or its factory ever holds a pool for — only the chain's
|
|
123
|
-
* wrapped-native token is — so a native node resolves to that wrapped token
|
|
124
|
-
* before it ever reaches {@link pulseXConversionSwapPath}. Every other node
|
|
125
|
-
* is unchanged. A chain with no known wrapped-native token leaves a native
|
|
126
|
-
* node as it is; it has no pool, so no path prices.
|
|
127
|
-
*
|
|
128
|
-
* MIRRORS `useRouteSearch.ts`'s `quotePathAddress` (the UI's async `ask`
|
|
129
|
-
* already resolves native this way through that function) — this is the
|
|
130
|
-
* SAME rule, restated here because `prepare`/`exact` never go through the
|
|
131
|
-
* UI's `readAmountOut` at all; see {@link pulseXCandidatePaths}'s own doc.
|
|
132
|
-
*
|
|
133
|
-
* @param node - the node to resolve
|
|
134
|
-
* @returns the node's own address, or its chain's wrapped-native address for a native node
|
|
135
|
-
*/
|
|
136
71
|
const poolTokenAddress = (node) => (0, ecosystems_js_1.sameAddress)(node.address, viem_1.zeroAddress, 'evm') ? (wrappedNativeFor(node.chainId) ?? viem_1.zeroAddress) : node.address;
|
|
137
|
-
/**
|
|
138
|
-
* The fixed Wrapped-PLS path from `from` to `to` — `[from, WPLS, to]`, or
|
|
139
|
-
* `[from, to]` when one side is WPLS — or null when both ends resolve to the
|
|
140
|
-
* same pool token (PLS<->WPLS is the wrap carrier's job).
|
|
141
|
-
*
|
|
142
|
-
* NOT THE PATH A HOP IS SIGNED ALONG. The carrier's own `exact` picks the
|
|
143
|
-
* best of many paths ({@link candidatePulseXPaths}) and records the winner on
|
|
144
|
-
* the hop's outcome; the signer reads it back with {@link pulseXSwapPathForHop}.
|
|
145
|
-
* This fixed path serves only the carrier's asynchronous `ask` — the
|
|
146
|
-
* interface's `getAmountsOut` read, which the route search never calls for a
|
|
147
|
-
* PulseX hop, because the search only asks aggregators live.
|
|
148
|
-
*
|
|
149
|
-
* Native PLS resolves to WPLS first ({@link poolTokenAddress}), so a path
|
|
150
|
-
* never starts or ends on the zero address, which has no pool.
|
|
151
|
-
*
|
|
152
|
-
* @param from - the hop's origin node
|
|
153
|
-
* @param to - the hop's destination node
|
|
154
|
-
* @returns the fixed path, or null when there is nothing for PulseX to swap
|
|
155
|
-
*/
|
|
156
72
|
const pulseXWrappedNativePath = (from, to) => {
|
|
157
73
|
const fromAddress = poolTokenAddress(from);
|
|
158
74
|
const toAddress = poolTokenAddress(to);
|
|
@@ -162,38 +78,16 @@ const pulseXWrappedNativePath = (from, to) => {
|
|
|
162
78
|
return wrappedNative === undefined ? [fromAddress, toAddress] : (0, providers_js_1.pulseXConversionSwapPath)(fromAddress, toAddress, wrappedNative);
|
|
163
79
|
};
|
|
164
80
|
exports.pulseXWrappedNativePath = pulseXWrappedNativePath;
|
|
165
|
-
/**
|
|
166
|
-
* Every path a PulseX hop from `from` to `to` may be priced along, or none
|
|
167
|
-
* when both ends resolve to the same pool token. EVERY PULSEX PRICE AND
|
|
168
|
-
* POOL READ IN THIS FILE GOES THROUGH THIS ONE FUNCTION, so `prepare` reads
|
|
169
|
-
* exactly the pools `exact` will look up, and native PLS resolves to WPLS
|
|
170
|
-
* the same way in both ({@link poolTokenAddress} — a path that started on
|
|
171
|
-
* the zero address once left every native-PLS hop unpriced).
|
|
172
|
-
*
|
|
173
|
-
* @param from - the hop's origin node
|
|
174
|
-
* @param to - the hop's destination node
|
|
175
|
-
* @returns the candidate paths, direct first; empty when there is nothing to swap
|
|
176
|
-
*/
|
|
177
81
|
const pulseXCandidatePaths = (from, to) => {
|
|
178
82
|
const fromAddress = poolTokenAddress(from);
|
|
179
83
|
const toAddress = poolTokenAddress(to);
|
|
180
84
|
if ((0, ecosystems_js_1.sameAddress)(fromAddress, toAddress, 'evm'))
|
|
181
85
|
return [];
|
|
182
86
|
const wrappedNative = wrappedNativeFor(from.chainId);
|
|
183
|
-
// No known wrapped-native token: the direct pool is the only path.
|
|
184
87
|
if (wrappedNative === undefined)
|
|
185
88
|
return [[fromAddress, toAddress]];
|
|
186
89
|
return (0, pulsex_paths_js_1.candidatePulseXPaths)({ from: fromAddress, to: toAddress, wrappedNative, hubs: hubsFor(from.chainId) });
|
|
187
90
|
};
|
|
188
|
-
/**
|
|
189
|
-
* Whether a swap venue in `venues` can carry funds directly from `from` to
|
|
190
|
-
* `to`: both must be Ethereum-Virtual-Machine tokens on the SAME chain, that
|
|
191
|
-
* chain must have at least one venue, and the two must resolve to different
|
|
192
|
-
* pool tokens, so there is at least one path to price.
|
|
193
|
-
*
|
|
194
|
-
* NO TOKEN ALLOWLIST IS CONSULTED, on purpose: whether a pool exists is the
|
|
195
|
-
* reserve read's question, answered in `prepare`, not this one's.
|
|
196
|
-
*/
|
|
197
91
|
const pulseXCarries = (venues, from, to) => {
|
|
198
92
|
if (from.ecosystem !== 'evm' || to.ecosystem !== 'evm')
|
|
199
93
|
return false;
|
|
@@ -203,21 +97,6 @@ const pulseXCarries = (venues, from, to) => {
|
|
|
203
97
|
return false;
|
|
204
98
|
return pulseXCandidatePaths(from, to).length > 0;
|
|
205
99
|
};
|
|
206
|
-
/**
|
|
207
|
-
* The swap path a planned PulseX hop must be signed along: the path its
|
|
208
|
-
* outcome records ({@link HopOutcome.swapPath}), which is the path the
|
|
209
|
-
* carrier's `exact` priced the figure on. NEVER REBUILT HERE — rebuilding a
|
|
210
|
-
* path at signing time is how a quote on one path becomes a signature on
|
|
211
|
-
* another.
|
|
212
|
-
*
|
|
213
|
-
* The recorded path is checked against the hop's own edge before it is
|
|
214
|
-
* returned: it must start at the edge's input pool token, end at its output
|
|
215
|
-
* pool token, hold only addresses, and name no token twice. A path that fails
|
|
216
|
-
* any check is refused (null) rather than signed.
|
|
217
|
-
*
|
|
218
|
-
* @param hop - the planned hop to sign
|
|
219
|
-
* @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
|
|
220
|
-
*/
|
|
221
100
|
const pulseXSwapPathForHop = (hop) => {
|
|
222
101
|
if (hop.edge.carrier !== 'PulseX')
|
|
223
102
|
return null;
|
|
@@ -236,42 +115,11 @@ const pulseXSwapPathForHop = (hop) => {
|
|
|
236
115
|
return path;
|
|
237
116
|
};
|
|
238
117
|
exports.pulseXSwapPathForHop = pulseXSwapPathForHop;
|
|
239
|
-
/**
|
|
240
|
-
* PulseX's own constant-product swap fee in basis points (29 of 10000, a
|
|
241
|
-
* numerator of 9971) — the default for {@link amountOutAlongPath}; a
|
|
242
|
-
* venue carries its own `feeBasisPoints`. The SAME `getAmountOut` formula every
|
|
243
|
-
* Uniswap-V2-style router uses: `amountOut = floor(amountIn * feeNumerator *
|
|
244
|
-
* reserveOut / (reserveIn * feeDenominator + amountIn * feeNumerator))`.
|
|
245
|
-
*
|
|
246
|
-
* MEASURED, NOT ASSUMED — see `uniV2Factories`'s own doc in `config.ts` for
|
|
247
|
-
* the full account. Reading live reserves from both PulseX router
|
|
248
|
-
* deployments (V1 `0x98bf93…`, V2 `0x165C34…`) and back-solving this ratio
|
|
249
|
-
* against each router's own `getAmountsOut`, on a one-hop and a two-hop
|
|
250
|
-
* path, gave the IDENTICAL ratio — `9971/10000` — for both, to the base
|
|
251
|
-
* unit. One constant serves both, and `pulsex-carrier.fork.test.ts` pins
|
|
252
|
-
* this claim against live chain data rather than trusting the arithmetic
|
|
253
|
-
* alone.
|
|
254
|
-
*/
|
|
255
118
|
const PULSEX_FEE_BASIS_POINTS = 29;
|
|
256
|
-
/** Basis points in a whole: the denominator of every venue's fee. */
|
|
257
119
|
const BASIS_POINTS_DENOMINATOR = 10000n;
|
|
258
|
-
/** The minimal read-only factory ABI `prepare` needs — just enough to resolve a pool's address from its two tokens. */
|
|
259
120
|
const univ2FactoryAbi = (0, viem_1.parseAbi)([
|
|
260
121
|
'function getPair(address tokenA, address tokenB) external view returns (address pair)',
|
|
261
122
|
]);
|
|
262
|
-
/**
|
|
263
|
-
* The Uniswap-V2-style constant-product `getAmountOut`, applied with
|
|
264
|
-
* the venue's fee. Reproduces the router's own on-chain formula
|
|
265
|
-
* exactly — see `pulsex-carrier.fork.test.ts` — so PRICE IMPACT IS ALREADY
|
|
266
|
-
* IN HERE: this is the same nonlinear function the router evaluates, not a
|
|
267
|
-
* flat-fee approximation with an impact correction bolted on afterward.
|
|
268
|
-
*
|
|
269
|
-
* @param amountIn - the input amount, in `reserveIn`'s token's base units
|
|
270
|
-
* @param reserveIn - the pool's reserve of the input token
|
|
271
|
-
* @param reserveOut - the pool's reserve of the output token
|
|
272
|
-
* @param feeBasisPoints - the venue's swap fee, in basis points
|
|
273
|
-
* @returns the output amount, in `reserveOut`'s token's base units; 0 when either reserve is 0
|
|
274
|
-
*/
|
|
275
123
|
const constantProductAmountOut = (amountIn, reserveIn, reserveOut, feeBasisPoints) => {
|
|
276
124
|
if (reserveIn <= 0n || reserveOut <= 0n || amountIn <= 0n)
|
|
277
125
|
return 0n;
|
|
@@ -280,38 +128,12 @@ const constantProductAmountOut = (amountIn, reserveIn, reserveOut, feeBasisPoint
|
|
|
280
128
|
const denominator = reserveIn * BASIS_POINTS_DENOMINATOR + amountInWithFee;
|
|
281
129
|
return numerator / denominator;
|
|
282
130
|
};
|
|
283
|
-
/**
|
|
284
|
-
* What `amountIn` delivers after crossing every pool of a path in order,
|
|
285
|
-
* chaining {@link constantProductAmountOut}.
|
|
286
|
-
*
|
|
287
|
-
* @param amountIn - the amount entering the first pool
|
|
288
|
-
* @param steps - the pools in order of travel
|
|
289
|
-
* @param feeBasisPoints - the venue's swap fee in basis points; PulseX's 29 when omitted
|
|
290
|
-
* @returns the amount leaving the last pool; 0 when any pool cannot pay out
|
|
291
|
-
*/
|
|
292
131
|
const amountOutAlongPath = (amountIn, steps, feeBasisPoints = PULSEX_FEE_BASIS_POINTS) => steps.reduce((runningAmount, step) => constantProductAmountOut(runningAmount, step.reserveIn, step.reserveOut, feeBasisPoints), amountIn);
|
|
293
132
|
exports.amountOutAlongPath = amountOutAlongPath;
|
|
294
|
-
/**
|
|
295
|
-
* How far a trade's executed rate falls below the marginal (near-spot) rate,
|
|
296
|
-
* in percent. The marginal rate is a tiny reference trade through the same
|
|
297
|
-
* pools (`amountIn / PULSEX_MARGINAL_REFERENCE_DIVISOR`). The swap fee rides
|
|
298
|
-
* in both rates, so it cancels and the gap is price impact alone. A path
|
|
299
|
-
* through several pools composes by itself: the reference trade crosses the
|
|
300
|
-
* same pools in the same order.
|
|
301
|
-
*
|
|
302
|
-
* impact = (1 - (amountOut / amountIn) / (referenceAmountOut / referenceAmountIn)) * 100
|
|
303
|
-
*
|
|
304
|
-
* @param options.amountIn - the full trade's input
|
|
305
|
-
* @param options.amountOut - what the full trade delivers
|
|
306
|
-
* @param options.referenceAmountIn - the reference trade's input
|
|
307
|
-
* @param options.referenceAmountOut - what the reference trade delivers
|
|
308
|
-
* @returns the impact in percent, never below 0; null when a reference figure is zero (impact cannot be measured)
|
|
309
|
-
*/
|
|
310
133
|
const priceImpactAgainstMarginal = (options) => {
|
|
311
134
|
const { amountIn, amountOut, referenceAmountIn, referenceAmountOut } = options;
|
|
312
135
|
if (amountIn <= 0n || referenceAmountIn <= 0n || referenceAmountOut <= 0n)
|
|
313
136
|
return null;
|
|
314
|
-
// The output the trade would deliver at the marginal rate with no impact.
|
|
315
137
|
const idealAmountOut = (referenceAmountOut * amountIn) / referenceAmountIn;
|
|
316
138
|
if (idealAmountOut <= 0n)
|
|
317
139
|
return null;
|
|
@@ -321,26 +143,12 @@ const priceImpactAgainstMarginal = (options) => {
|
|
|
321
143
|
return Number(((idealAmountOut - amountOut) * MILLIONTHS * 100n) / idealAmountOut) / Number(MILLIONTHS);
|
|
322
144
|
};
|
|
323
145
|
exports.priceImpactAgainstMarginal = priceImpactAgainstMarginal;
|
|
324
|
-
/** 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. */
|
|
325
146
|
const poolKey = (tokenA, tokenB) => {
|
|
326
147
|
const [lower, higher] = [tokenA.toLowerCase(), tokenB.toLowerCase()].sort();
|
|
327
148
|
return `${lower}|${higher}`;
|
|
328
149
|
};
|
|
329
|
-
/** 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. */
|
|
330
150
|
const venuePoolKey = (venue, tokenA, tokenB) => `${venue.chainId}|${venue.factory.toLowerCase()}|${poolKey(tokenA, tokenB)}`;
|
|
331
|
-
/** Every consecutive (tokenIn, tokenOut) hop a PulseX swap path implies. */
|
|
332
151
|
const hopsAlongPath = (path) => path.slice(0, -1).map((tokenIn, index) => [tokenIn, path[index + 1]]);
|
|
333
|
-
/**
|
|
334
|
-
* The pools a path crosses on one venue, each read in the direction of
|
|
335
|
-
* travel, or null when any of them was never read — `getPair` found no pool,
|
|
336
|
-
* or the read failed. A path with a missing pool cannot be priced, exactly as
|
|
337
|
-
* the router's own `getAmountsOut` would revert on it.
|
|
338
|
-
*
|
|
339
|
-
* @param options.venue - the deployment whose pools the path trades against
|
|
340
|
-
* @param options.path - the tokens, in order
|
|
341
|
-
* @param options.reservesByPoolKey - every pool `prepare` read, keyed by {@link venuePoolKey}
|
|
342
|
-
* @returns the steps in order, or null when a pool is missing
|
|
343
|
-
*/
|
|
344
152
|
const stepsAlongPath = (options) => {
|
|
345
153
|
const { venue, path, reservesByPoolKey } = options;
|
|
346
154
|
const steps = [];
|
|
@@ -356,35 +164,6 @@ const stepsAlongPath = (options) => {
|
|
|
356
164
|
}
|
|
357
165
|
return steps;
|
|
358
166
|
};
|
|
359
|
-
/**
|
|
360
|
-
* Batch-reads reserves for every pool `nodes`' candidate PulseX paths touch,
|
|
361
|
-
* on EVERY venue in `venues`, once, so {@link createPulseXCarrier}'s `exact`
|
|
362
|
-
* can answer every hop SYNCHRONOUSLY afterward.
|
|
363
|
-
*
|
|
364
|
-
* EACH VENUE IS READ THROUGH ITS OWN FACTORY. The same two tokens usually
|
|
365
|
-
* have a pool on V1 and another on V2, with different reserves; the two are
|
|
366
|
-
* stored apart ({@link venuePoolKey}) and never blended. `PULSEX_FEE_NUMERATOR`
|
|
367
|
-
* is proven identical for both deployments (see its own doc), so the formula
|
|
368
|
-
* is shared and only the reserves differ.
|
|
369
|
-
*
|
|
370
|
-
* TWO MULTICALLS ON PULSECHAIN, WHATEVER THE VENUE COUNT: `getPair` must
|
|
371
|
-
* resolve each pool's address before `getReserves` can be read from it, so
|
|
372
|
-
* the second read cannot join the first — but every venue's `getPair` rides
|
|
373
|
-
* the first multicall and every pool's `getReserves` rides the second.
|
|
374
|
-
*
|
|
375
|
-
* THE HUB SEARCH WIDENS EACH READ, NOT THEIR NUMBER. Every pool any candidate
|
|
376
|
-
* path crosses is read here ({@link pulseXCandidatePaths}), so `exact` can
|
|
377
|
-
* price every path with no further call. Measured 2026-10-06: an 8-token
|
|
378
|
-
* PulseChain universe asked 84 entries across the two reads (the fixed
|
|
379
|
-
* Wrapped-PLS path asked about 16). The shared multicall helper splits a read
|
|
380
|
-
* past its request-size budget into sequential posts; `getPair` crosses that
|
|
381
|
-
* line at about 11 pool tokens (198 entries over two venues).
|
|
382
|
-
*
|
|
383
|
-
* @param nodes - every node the search might branch through
|
|
384
|
-
* @param venues - the venues this carrier offers
|
|
385
|
-
* @param clientForChain - see {@link CreatePulseXCarrierOptions}'s own field
|
|
386
|
-
* @returns every pool this universe touches, keyed by {@link venuePoolKey} — pools with no `getPair` result are simply absent, not an error
|
|
387
|
-
*/
|
|
388
167
|
const preparePulseXReserves = async ({ nodes, venues, clientForChain, }) => {
|
|
389
168
|
const reservesByPoolKey = new Map();
|
|
390
169
|
const chainIds = Object.keys(venues)
|
|
@@ -397,12 +176,6 @@ const preparePulseXReserves = async ({ nodes, venues, clientForChain, }) => {
|
|
|
397
176
|
}
|
|
398
177
|
return reservesByPoolKey;
|
|
399
178
|
};
|
|
400
|
-
/**
|
|
401
|
-
* One chain's share of {@link preparePulseXReserves}: two multicalls, the
|
|
402
|
-
* `getPair` reads and then the `getReserves` reads, over every venue of the
|
|
403
|
-
* chain. A chain with no client, or with no chain description to multicall
|
|
404
|
-
* against, reads nothing.
|
|
405
|
-
*/
|
|
406
179
|
const preparePulseXReservesOnChain = async ({ nodes, chainId, venues, clientForChain, }) => {
|
|
407
180
|
const reservesByPoolKey = new Map();
|
|
408
181
|
const client = clientForChain(chainId);
|
|
@@ -447,10 +220,6 @@ const preparePulseXReservesOnChain = async ({ nodes, chainId, venues, clientForC
|
|
|
447
220
|
const pairAddressByPoolKey = new Map();
|
|
448
221
|
poolQueries.forEach((query, index) => {
|
|
449
222
|
const pairAddress = factoryResults[index];
|
|
450
|
-
// A decoded address is exactly 42 characters; a failed `allowFailure`
|
|
451
|
-
// call's undecoded revert bytes are almost never that length — see
|
|
452
|
-
// `omnibridge.ts`'s identical guard for why `typeof` alone cannot tell
|
|
453
|
-
// success from failure for an address-returning call.
|
|
454
223
|
if (typeof pairAddress === 'string' && pairAddress.length === 42 && pairAddress !== viem_1.zeroAddress) {
|
|
455
224
|
pairAddressByPoolKey.set(query.key, (0, viem_1.getAddress)(pairAddress));
|
|
456
225
|
}
|
|
@@ -475,23 +244,10 @@ const preparePulseXReservesOnChain = async ({ nodes, chainId, venues, clientForC
|
|
|
475
244
|
const [reserve0, reserve1] = result;
|
|
476
245
|
if (typeof reserve0 !== 'bigint' || typeof reserve1 !== 'bigint')
|
|
477
246
|
return;
|
|
478
|
-
// Standard Uniswap-V2-style factories sort a pair's tokens before
|
|
479
|
-
// creating it, so `token0` is always the LEXICOGRAPHICALLY LOWER
|
|
480
|
-
// address — confirmed against live PulseX pools 2026-09-24 (both V1 and
|
|
481
|
-
// V2), never re-derived with an extra `token0()` read.
|
|
482
247
|
reservesByPoolKey.set(key, { reserveOfLowerAddress: reserve0, reserveOfHigherAddress: reserve1 });
|
|
483
248
|
});
|
|
484
249
|
return reservesByPoolKey;
|
|
485
250
|
};
|
|
486
|
-
/**
|
|
487
|
-
* The venues a carrier offers: every venue in `venues`, or only the routers
|
|
488
|
-
* the host names. A named router that is in no venue is a configuration
|
|
489
|
-
* error, thrown at construction rather than offered silently.
|
|
490
|
-
*
|
|
491
|
-
* @param venues - the resolved venues
|
|
492
|
-
* @param routers - the routers the host can sign on, or undefined for all
|
|
493
|
-
* @returns the venues, in their original order
|
|
494
|
-
*/
|
|
495
251
|
const selectPulseXVenues = (venues, routers) => {
|
|
496
252
|
if (routers === undefined)
|
|
497
253
|
return venues;
|
|
@@ -503,18 +259,9 @@ const selectPulseXVenues = (venues, routers) => {
|
|
|
503
259
|
}
|
|
504
260
|
return Object.freeze(Object.fromEntries(Object.entries(venues).map(([chainId, list]) => [chainId, list.filter(named)])));
|
|
505
261
|
};
|
|
506
|
-
/**
|
|
507
|
-
* Builds the PulseX conversion swap as a plug-in carrier.
|
|
508
|
-
*
|
|
509
|
-
* @param options - see {@link CreatePulseXCarrierOptions}
|
|
510
|
-
* @returns the carrier — `id: 'PulseX'`
|
|
511
|
-
*/
|
|
512
262
|
const createPulseXCarrier = (options) => {
|
|
513
263
|
const { readAmountOut, clientForChain } = options;
|
|
514
264
|
const venues = selectPulseXVenues((0, swap_venues_js_1.resolveSwapVenues)(options.venues), options.routers);
|
|
515
|
-
// ONE EDGE PER VENUE. Each names its router as its `venue`, so the search
|
|
516
|
-
// quotes and ranks V1 and V2 as two separate hops and the better one wins
|
|
517
|
-
// on its own figure — and the winner is signed on the router it names.
|
|
518
265
|
const edgesFrom = (from, to) => {
|
|
519
266
|
if ((0, graph_js_1.assetNodeKey)(from) === (0, graph_js_1.assetNodeKey)(to))
|
|
520
267
|
return [];
|
|
@@ -538,17 +285,10 @@ const createPulseXCarrier = (options) => {
|
|
|
538
285
|
return {
|
|
539
286
|
ok: true,
|
|
540
287
|
deliveredAmount: amountOut,
|
|
541
|
-
// getAmountsOut is exact for the CURRENT block; no slippage tolerance is
|
|
542
|
-
// modelled here, so the guaranteed figure is left equal to the expected
|
|
543
|
-
// one, matching the omnibridge carrier's own convention.
|
|
544
288
|
minimumDeliveredAmount: amountOut,
|
|
545
289
|
claimedDurationSeconds: 0,
|
|
546
290
|
};
|
|
547
291
|
};
|
|
548
|
-
// Populated by `prepare`, read by `exact` — see `omnibridge.ts`'s identical
|
|
549
|
-
// field for why a mutable closure here is safe (client-side,
|
|
550
|
-
// one-search-at-a-time; PulseX is likewise "never built into" the shared
|
|
551
|
-
// display-quote service).
|
|
552
292
|
let reservesByPoolKey = new Map();
|
|
553
293
|
const prepare = async (nodes) => {
|
|
554
294
|
if (clientForChain === undefined)
|
|
@@ -558,31 +298,19 @@ const createPulseXCarrier = (options) => {
|
|
|
558
298
|
const exact = (edge, amountIn) => {
|
|
559
299
|
if (!pulseXCarries(venues, edge.from, edge.to))
|
|
560
300
|
return null;
|
|
561
|
-
// Quoted on the edge's OWN venue — the router it will be signed on — and
|
|
562
|
-
// only on a venue this carrier offers.
|
|
563
301
|
const venue = (0, exports.venueForEdge)(venues, edge);
|
|
564
302
|
if (venue === null)
|
|
565
303
|
return null;
|
|
566
|
-
// EVERY CANDIDATE PATH IS PRICED, AND THE BEST ONE WINS. A path with a
|
|
567
|
-
// pool `prepare` never found is left out — the same as a router call
|
|
568
|
-
// that would revert — and a path that pays out nothing is never picked.
|
|
569
304
|
const quotes = pulseXCandidatePaths(edge.from, edge.to).flatMap((path) => {
|
|
570
305
|
const steps = stepsAlongPath({ venue, path, reservesByPoolKey });
|
|
571
306
|
return steps === null ? [] : [{ path, steps, deliveredAmount: (0, exports.amountOutAlongPath)(amountIn, steps, venue.feeBasisPoints) }];
|
|
572
307
|
});
|
|
573
|
-
// No path had every pool read — a structural non-answer, not a refusal
|
|
574
|
-
// (`exact`'s own doc: `null` means "nothing to say"). NOTE THIS DOES NOT
|
|
575
|
-
// FALL BACK TO AN ESTIMATE: a carrier with `exact` defined never yields
|
|
576
|
-
// an `'estimate'` outcome (see `enumerate.ts`'s `resolveHopOutcome`,
|
|
577
|
-
// branch 1) — `null` here PRUNES the hop entirely, the same as a refusal.
|
|
578
308
|
if (quotes.length === 0)
|
|
579
309
|
return null;
|
|
580
310
|
const best = (0, pulsex_paths_js_1.bestPulseXPathQuote)(quotes);
|
|
581
311
|
if (best === null) {
|
|
582
312
|
return { outcome: 'refused', refusal: { kind: 'no-quote', reason: 'no liquidity on this PulseX path', minimumAmount: null } };
|
|
583
313
|
}
|
|
584
|
-
// Price impact is measured on the CHOSEN path's own pools, with the same
|
|
585
|
-
// reference trade as before.
|
|
586
314
|
const referenceAmountIn = amountIn / producers_js_1.PULSEX_MARGINAL_REFERENCE_DIVISOR;
|
|
587
315
|
const priceImpactPercent = (0, exports.priceImpactAgainstMarginal)({
|
|
588
316
|
amountIn,
|
|
@@ -590,10 +318,6 @@ const createPulseXCarrier = (options) => {
|
|
|
590
318
|
referenceAmountIn,
|
|
591
319
|
referenceAmountOut: (0, exports.amountOutAlongPath)(referenceAmountIn, best.steps, venue.feeBasisPoints),
|
|
592
320
|
});
|
|
593
|
-
// The path rides on the answer, so the signer signs exactly what was
|
|
594
|
-
// priced (`pulseXSwapPathForHop`). Impact is left off (not zero) when the
|
|
595
|
-
// reference trade rounds away: an unknown impact must stay unknown so the
|
|
596
|
-
// quality gate withholds the plan.
|
|
597
321
|
const delivered = { outcome: 'delivered', deliveredAmount: best.deliveredAmount, swapPath: best.path };
|
|
598
322
|
return priceImpactPercent === null ? delivered : { ...delivered, priceImpactPercent };
|
|
599
323
|
};
|
|
@@ -604,11 +328,8 @@ const createPulseXCarrier = (options) => {
|
|
|
604
328
|
prepare,
|
|
605
329
|
exact,
|
|
606
330
|
isRateLimited: false,
|
|
607
|
-
// A same-chain swap requires the reader's own wallet signature — never
|
|
608
|
-
// a by-hand deposit.
|
|
609
331
|
depositMode: 'wallet-signed',
|
|
610
332
|
sender: 'swap',
|
|
611
|
-
// A same-chain swap settles with the transaction itself.
|
|
612
333
|
settlementSeconds: 0,
|
|
613
334
|
};
|
|
614
335
|
};
|
|
@@ -4,48 +4,6 @@ exports.createWrapCarrier = void 0;
|
|
|
4
4
|
const viem_1 = require("viem");
|
|
5
5
|
const config_js_1 = require("../../config.js");
|
|
6
6
|
const graph_js_1 = require("../search/graph.js");
|
|
7
|
-
/**
|
|
8
|
-
* Native-coin wrap/unwrap (PLS <-> WPLS on PulseChain, and the same pair on
|
|
9
|
-
* every other Ethereum-Virtual-Machine chain the search graph touches — ETH
|
|
10
|
-
* <-> WETH, BNB <-> WBNB) as a plug-in `Carrier` for `@gibs/quotes`'s
|
|
11
|
-
* route-search engine. GIBS-SPECIFIC in the sense `omnibridge.ts`/`pulsex.ts`
|
|
12
|
-
* are (see `omnibridge.ts`'s head doc for why this returns a plain object
|
|
13
|
-
* shaped like `Carrier` rather than importing that type) — though the
|
|
14
|
-
* carrier itself names no chain: it derives every wrap pair from
|
|
15
|
-
* `config.ts`'s own `nativeAssetOut` map, so a chain added there gains a
|
|
16
|
-
* wrap edge with no change here.
|
|
17
|
-
*
|
|
18
|
-
* WHY THIS IS A SEPARATE CARRIER FROM PulseX. `pulsex.ts`'s own
|
|
19
|
-
* `readPulseXAmountOut` (via `pulseXSwapPathForEdge` in
|
|
20
|
-
* `packages/ui/src/lib/state/useRouteSearch.ts`) quotes native Pulse AS
|
|
21
|
-
* Wrapped Pulse — both map to the same `quotePathAddress` — so a PLS<->WPLS
|
|
22
|
-
* pair collapses to "no swap to quote" and PulseX has never carried it.
|
|
23
|
-
* There is no PulseX POOL for native-versus-wrapped; it is a 1:1 contract
|
|
24
|
-
* call (`deposit()`/`withdraw(uint256)`), not a trade, so it needed its own
|
|
25
|
-
* carrier rather than a PulseX special case.
|
|
26
|
-
*
|
|
27
|
-
* SENDER: `'swap'`, THE SAME NAME `pulsex.ts` USES — the reader's own
|
|
28
|
-
* connected wallet signs a `deposit()`/`withdraw(uint256)` call on the
|
|
29
|
-
* wrapped token's own contract (WETH9-style; confirmed present on WPLS by
|
|
30
|
-
* bytecode selector 2026-09-24), exactly the wallet a same-chain PulseX swap
|
|
31
|
-
* already uses. WHETHER `settings.ts`'s EXISTING swap-transaction builder
|
|
32
|
-
* (`pulseXSwapTransactionInputs`) CAN BUILD THIS CALL: NO, not without a
|
|
33
|
-
* change there — that builder only ever targets a PulseX ROUTER address and
|
|
34
|
-
* encodes `swapExact*` calldata; a wrap needs `to` = the wrapped token
|
|
35
|
-
* itself and `data` = plain `deposit()`/`withdraw(amount)` calldata, which
|
|
36
|
-
* is a different (and simpler) shape. Sharing the `'swap'` sender name says
|
|
37
|
-
* "the same wallet, the same signing step in the flow" — it does not claim
|
|
38
|
-
* the existing builder already emits this calldata. Building that call is
|
|
39
|
-
* `settings.ts`'s job, out of this carrier's scope; this module answers only
|
|
40
|
-
* the route-search question ("what does this edge deliver, exactly").
|
|
41
|
-
*/
|
|
42
|
-
/**
|
|
43
|
-
* Whether this carrier can wrap/unwrap directly between `from` and `to`:
|
|
44
|
-
* same Ethereum-Virtual-Machine chain, and the two addresses are EXACTLY the
|
|
45
|
-
* zero-address/wrapped-native pair `config.ts`'s `nativeAssetOut` names for
|
|
46
|
-
* that chain — nothing else, so a plain token pair that merely happens to
|
|
47
|
-
* share a chain is never mistaken for a wrap.
|
|
48
|
-
*/
|
|
49
7
|
const wrapCarries = (from, to) => {
|
|
50
8
|
if (from.ecosystem !== 'evm' || to.ecosystem !== 'evm')
|
|
51
9
|
return false;
|
|
@@ -65,22 +23,11 @@ const wrapCarries = (from, to) => {
|
|
|
65
23
|
return fromAddress === wrapped;
|
|
66
24
|
return false;
|
|
67
25
|
};
|
|
68
|
-
/** Every wrap/unwrap edge is worth exactly `amountIn`, in `edge.to`'s base units — native and wrapped share the same decimals on every chain this carrier has ever seen (18, both sides, everywhere `nativeAssetOut` names). */
|
|
69
26
|
const wrapExact = (edge, amountIn) => {
|
|
70
27
|
if (!wrapCarries(edge.from, edge.to))
|
|
71
28
|
return null;
|
|
72
29
|
return { outcome: 'delivered', deliveredAmount: amountIn };
|
|
73
30
|
};
|
|
74
|
-
/**
|
|
75
|
-
* Builds the native wrap/unwrap carrier as a plug-in.
|
|
76
|
-
*
|
|
77
|
-
* NO OPTIONS, NO `prepare`. There is no local read to batch — a wrap has no
|
|
78
|
-
* minimum, no maximum, no fee rate and no reserve to be wrong about; it is a
|
|
79
|
-
* pure 1:1 identity over every chain `config.ts` names a wrapped-native
|
|
80
|
-
* token for. `exact` alone is total over every edge this carrier offers.
|
|
81
|
-
*
|
|
82
|
-
* @returns the carrier — `id: 'wrap'`
|
|
83
|
-
*/
|
|
84
31
|
const createWrapCarrier = () => {
|
|
85
32
|
const edgesFrom = (from, to) => {
|
|
86
33
|
if ((0, graph_js_1.assetNodeKey)(from) === (0, graph_js_1.assetNodeKey)(to))
|
|
@@ -97,7 +44,6 @@ const createWrapCarrier = () => {
|
|
|
97
44
|
return {
|
|
98
45
|
ok: true,
|
|
99
46
|
deliveredAmount: result.deliveredAmount,
|
|
100
|
-
// 1:1, always — there is no slippage to guarantee against.
|
|
101
47
|
minimumDeliveredAmount: result.deliveredAmount,
|
|
102
48
|
claimedDurationSeconds: 0,
|
|
103
49
|
};
|
|
@@ -108,11 +54,8 @@ const createWrapCarrier = () => {
|
|
|
108
54
|
ask,
|
|
109
55
|
exact: wrapExact,
|
|
110
56
|
isRateLimited: false,
|
|
111
|
-
// A `deposit()`/`withdraw()` call requires the reader's own wallet
|
|
112
|
-
// signature — never a by-hand deposit.
|
|
113
57
|
depositMode: 'wallet-signed',
|
|
114
58
|
sender: 'swap',
|
|
115
|
-
// A same-chain call settles with the transaction itself.
|
|
116
59
|
settlementSeconds: 0,
|
|
117
60
|
};
|
|
118
61
|
};
|
|
@@ -1,92 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.composedRouteMayBeOffered = exports.assessComposedRoute = exports.COMPOSED_ROUTE_MAX_VALUE_GAIN_PERCENT = exports.COMPOSED_ROUTE_MAX_VALUE_LOSS_PERCENT = exports.COMPOSED_ROUTE_MAX_PRICE_IMPACT_PERCENT = void 0;
|
|
4
|
-
/**
|
|
5
|
-
* Whether a composed multi-signature route is good enough to offer at all.
|
|
6
|
-
*
|
|
7
|
-
* WHY A ROUTE NEEDS A GATE THAT A SINGLE CROSSING DOES NOT. A one-signature
|
|
8
|
-
* route that turns out badly costs the reader one transaction and they still
|
|
9
|
-
* hold something they chose. A composed route — an aggregator leg, then an
|
|
10
|
-
* omnibridge crossing, then a swap — strands them between signatures: after
|
|
11
|
-
* the first they hold an anchor token on a chain they did not ask for, and the
|
|
12
|
-
* only way out is to pay again. So a cheap first leg followed by a bad swap is
|
|
13
|
-
* worse than the route it replaced, and the difference has to be found BEFORE
|
|
14
|
-
* the route is listed rather than after the reader has signed.
|
|
15
|
-
*
|
|
16
|
-
* NOTHING HERE COSTS A REQUEST. Both figures the verdict reads are already in
|
|
17
|
-
* hand when a composer runs: the swap's price impact comes from the on-chain
|
|
18
|
-
* `getAmountsOut` pair the estimate already fetched, and the two dollar values
|
|
19
|
-
* come from the route's own basis. The gate is free, which is why it can run
|
|
20
|
-
* on every route rather than only on suspicious ones.
|
|
21
|
-
*/
|
|
22
|
-
/**
|
|
23
|
-
* The price impact above which a composed route is withdrawn, in percent.
|
|
24
|
-
*
|
|
25
|
-
* MEASURED RATHER THAN CHOSEN. Across the anchor and size combinations swept
|
|
26
|
-
* on 2026-09-21 — bridged wrapped ether, USD Coin, DAI and USDT into wrapped
|
|
27
|
-
* PulseChain coin, at roughly forty and one hundred and forty United States
|
|
28
|
-
* dollars — the impact ran from 0.008 percent to 0.037 percent, with bridged
|
|
29
|
-
* USDT the one outlier at 0.34 to 0.37 percent. Three percent is two orders of
|
|
30
|
-
* magnitude above the normal case and an order above the worst observed, so it
|
|
31
|
-
* cannot fire on an ordinary route; and it is still far below the point where
|
|
32
|
-
* a reader is quietly losing real money on a leg they cannot undo.
|
|
33
|
-
*
|
|
34
|
-
* See `docs/near-intents-conversion-composition.md` for the full sweep.
|
|
35
|
-
*/
|
|
36
4
|
exports.COMPOSED_ROUTE_MAX_PRICE_IMPACT_PERCENT = 3;
|
|
37
|
-
/**
|
|
38
|
-
* The end-to-end value loss above which a composed route is withdrawn, in
|
|
39
|
-
* percent of what the reader pays.
|
|
40
|
-
*
|
|
41
|
-
* A SECOND TEST BECAUSE THE FIRST CAN READ FINE ON A BROKEN POOL. Price impact
|
|
42
|
-
* is measured against a marginal reference quote taken from the same pool, so
|
|
43
|
-
* a pool that is uniformly mispriced — a stale pair, a pair holding almost
|
|
44
|
-
* nothing — reports a small impact while paying out far less than the tokens
|
|
45
|
-
* going in are worth. Only comparing the two ends of the journey catches that.
|
|
46
|
-
*
|
|
47
|
-
* Measured whole-journey loss on the same sweep ran from 0.81 to 1.36 percent,
|
|
48
|
-
* of which the swap fee alone is 0.29. Five percent leaves every observed
|
|
49
|
-
* route untouched and still stops a route that is quietly eating a twentieth
|
|
50
|
-
* of the reader's money.
|
|
51
|
-
*/
|
|
52
5
|
exports.COMPOSED_ROUTE_MAX_VALUE_LOSS_PERCENT = 5;
|
|
53
|
-
/**
|
|
54
|
-
* The end-to-end value GAIN above which a composed route is withdrawn, in
|
|
55
|
-
* percent of what the reader pays.
|
|
56
|
-
*
|
|
57
|
-
* A ROUTE CANNOT MAKE MONEY. Every leg charges a fee, so a route that
|
|
58
|
-
* delivers more value than it takes says one of its figures is wrong. Only a
|
|
59
|
-
* disagreement between the two price feeds can make a gain look real, and the
|
|
60
|
-
* value-loss ceiling already trusts those feeds to within the same five
|
|
61
|
-
* percent. So the bound is symmetric with
|
|
62
|
-
* {@link COMPOSED_ROUTE_MAX_VALUE_LOSS_PERCENT}.
|
|
63
|
-
*
|
|
64
|
-
* THE DEFECT THIS STOPS. On 2026-10-03 the engine served 0.0002 BNB to HEX at
|
|
65
|
-
* 495,256 HEX, about seven thousand times the rate of the next tier. The
|
|
66
|
-
* value-loss test read that as a loss of minus seven hundred thousand percent
|
|
67
|
-
* and passed it. A PulseX hop rescaled from an upstream live answer keeps the
|
|
68
|
-
* price impact measured at its original, smaller input
|
|
69
|
-
* (`recomposePath` in `@gibs/quotes`'s `search/waves.ts`), so the impact test
|
|
70
|
-
* cannot see an inflated amount either. Only the two ends of the journey can.
|
|
71
|
-
*/
|
|
72
6
|
exports.COMPOSED_ROUTE_MAX_VALUE_GAIN_PERCENT = 5;
|
|
73
|
-
/**
|
|
74
|
-
* Judges a composed route against its three ceilings.
|
|
75
|
-
*
|
|
76
|
-
* Checks price impact first, then value loss, then value gain. Impact names
|
|
77
|
-
* the specific leg at fault, while the two value tests only say the journey as
|
|
78
|
-
* a whole went wrong, so impact is the more useful answer when several fail.
|
|
79
|
-
* In the measured percent of a `'value-gain'` rejection, the gain is a
|
|
80
|
-
* positive number.
|
|
81
|
-
*
|
|
82
|
-
* @param options.priceImpactPercent - the swap leg's price impact, from the
|
|
83
|
-
* estimate's marginal reference quote; null when not yet quoted
|
|
84
|
-
* @param options.paidUsdValue - what the reader spends at the origin, in
|
|
85
|
-
* United States dollars; null when unpriced
|
|
86
|
-
* @param options.deliveredUsdValue - what the route delivers, valued on the
|
|
87
|
-
* SAME basis as `paidUsdValue`; null when unpriced
|
|
88
|
-
* @returns the verdict
|
|
89
|
-
*/
|
|
90
7
|
const assessComposedRoute = ({ priceImpactPercent, paidUsdValue, deliveredUsdValue, }) => {
|
|
91
8
|
if (typeof priceImpactPercent !== 'number' || !Number.isFinite(priceImpactPercent)) {
|
|
92
9
|
return { state: 'unknown', missing: 'price-impact' };
|
|
@@ -128,17 +45,5 @@ const assessComposedRoute = ({ priceImpactPercent, paidUsdValue, deliveredUsdVal
|
|
|
128
45
|
return { state: 'acceptable' };
|
|
129
46
|
};
|
|
130
47
|
exports.assessComposedRoute = assessComposedRoute;
|
|
131
|
-
/**
|
|
132
|
-
* Whether a composed route may be listed on the strip.
|
|
133
|
-
*
|
|
134
|
-
* ONLY AN EXPLICIT PASS LISTS THE ROUTE. An unknown verdict withholds it, the
|
|
135
|
-
* same as a rejection, because a three-signature route nobody has checked is
|
|
136
|
-
* the case this gate exists for. The two remain distinct in
|
|
137
|
-
* {@link ComposedRouteVerdict} so a surface can tell the reader "still
|
|
138
|
-
* pricing" rather than "no route", which a boolean alone could not.
|
|
139
|
-
*
|
|
140
|
-
* @param verdict - the verdict from {@link assessComposedRoute}
|
|
141
|
-
* @returns true only when the route passed every ceiling
|
|
142
|
-
*/
|
|
143
48
|
const composedRouteMayBeOffered = (verdict) => verdict.state === 'acceptable';
|
|
144
49
|
exports.composedRouteMayBeOffered = composedRouteMayBeOffered;
|