@gibs/bridge-sdk 1.12.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +234 -432
- package/dist/abis.d.ts +1 -0
- package/dist/bridge-limits.d.ts +103 -0
- package/dist/bridge-limits.js +84 -0
- package/dist/chain-info.d.ts +169 -0
- package/dist/chain-info.js +410 -0
- package/dist/chain-names.d.ts +160 -0
- package/dist/chain-names.js +200 -0
- package/dist/chains.d.ts +65 -0
- package/dist/chains.js +284 -0
- package/dist/config.d.ts +216 -0
- package/dist/config.js +493 -0
- package/dist/consolidated-quote.d.ts +554 -0
- package/dist/consolidated-quote.js +773 -0
- package/dist/destination-calldata.d.ts +118 -0
- package/dist/destination-calldata.js +152 -0
- package/dist/ecosystems.d.ts +217 -0
- package/dist/ecosystems.js +288 -0
- package/dist/fee-type.d.ts +10 -0
- package/dist/fee-type.js +9 -0
- package/dist/hyperliquid-execution.d.ts +152 -0
- package/dist/hyperliquid-execution.js +168 -0
- package/dist/image-links.d.ts +117 -0
- package/dist/image-links.js +142 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +20 -0
- package/dist/lifi-execution-guard.d.ts +199 -0
- package/dist/lifi-execution-guard.js +400 -0
- package/dist/near-intents-conformance.d.ts +85 -0
- package/dist/near-intents-conformance.js +329 -0
- package/dist/near-intents-route.d.ts +47 -0
- package/dist/near-intents-route.js +265 -0
- package/dist/near-intents.d.ts +524 -0
- package/dist/near-intents.js +1055 -0
- package/dist/origin-deposit.d.ts +326 -0
- package/dist/origin-deposit.js +68 -0
- package/dist/pre-quote.d.ts +198 -0
- package/dist/pre-quote.js +175 -0
- package/dist/prices.d.ts +107 -0
- package/dist/prices.js +162 -0
- package/dist/providers.d.ts +221 -0
- package/dist/providers.js +157 -0
- package/dist/quote-fees.d.ts +102 -0
- package/dist/quote-fees.js +195 -0
- package/dist/referral.d.ts +77 -0
- package/dist/referral.js +72 -0
- package/dist/relay-currencies.d.ts +47 -0
- package/dist/relay-currencies.js +89 -0
- package/dist/relay-quote.d.ts +301 -0
- package/dist/relay-quote.js +983 -0
- package/dist/route-steps.d.ts +138 -0
- package/dist/route-steps.js +305 -0
- package/dist/routing/amounts.d.ts +80 -0
- package/dist/routing/amounts.js +120 -0
- package/dist/routing/candidate-route.d.ts +43 -0
- package/dist/routing/candidate-route.js +45 -0
- package/dist/routing/candidate.d.ts +237 -0
- package/dist/routing/candidate.js +96 -0
- package/dist/routing/carriers/ask-answer.d.ts +58 -0
- package/dist/routing/carriers/ask-answer.js +1 -0
- package/dist/routing/carriers/omnibridge.d.ts +137 -0
- package/dist/routing/carriers/omnibridge.js +419 -0
- package/dist/routing/carriers/pulsex.d.ts +111 -0
- package/dist/routing/carriers/pulsex.js +404 -0
- package/dist/routing/carriers/wrap.d.ts +25 -0
- package/dist/routing/carriers/wrap.js +115 -0
- package/dist/routing/composed-route-quality.d.ts +103 -0
- package/dist/routing/composed-route-quality.js +108 -0
- package/dist/routing/entry-chain-anchor.d.ts +88 -0
- package/dist/routing/entry-chain-anchor.js +55 -0
- package/dist/routing/format.d.ts +31 -0
- package/dist/routing/format.js +58 -0
- package/dist/routing/index.d.ts +34 -0
- package/dist/routing/index.js +42 -0
- package/dist/routing/producers.d.ts +770 -0
- package/dist/routing/producers.js +1564 -0
- package/dist/routing/providers.d.ts +26 -0
- package/dist/routing/providers.js +38 -0
- package/dist/routing/route-data.d.ts +355 -0
- package/dist/routing/route-data.js +1 -0
- package/dist/routing/search/compose.d.ts +168 -0
- package/dist/routing/search/compose.js +98 -0
- package/dist/routing/search/graph.d.ts +639 -0
- package/dist/routing/search/graph.js +445 -0
- package/dist/routing/search/plan-route-data.d.ts +244 -0
- package/dist/routing/search/plan-route-data.js +767 -0
- package/dist/routing/search/score.d.ts +324 -0
- package/dist/routing/search/score.js +400 -0
- package/dist/routing/usd-value.d.ts +17 -0
- package/dist/routing/usd-value.js +25 -0
- package/dist/settings.d.ts +692 -0
- package/dist/settings.js +909 -0
- package/dist/ton-execution.d.ts +117 -0
- package/dist/ton-execution.js +247 -0
- package/dist/tron-execution.d.ts +146 -0
- package/dist/tron-execution.js +239 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.js +1 -0
- package/package.json +203 -23
- package/src/chain-info.test.ts +0 -968
- package/src/chain-info.ts +0 -498
- package/src/chains.test.ts +0 -123
- package/src/chains.ts +0 -88
- package/src/config.test.ts +0 -679
- package/src/config.ts +0 -500
- package/src/fee-type.test.ts +0 -45
- package/src/fee-type.ts +0 -14
- package/src/image-links.test.ts +0 -160
- package/src/image-links.ts +0 -65
- package/src/index.ts +0 -8
- package/src/settings.test.ts +0 -2754
- package/src/settings.ts +0 -875
- package/src/types.ts +0 -48
- package/tsconfig.json +0 -17
- package/vitest.config.ts +0 -13
- /package/{src/abis.ts → dist/abis.js} +0 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { multicallRead } from '@gibs/common/multicall';
|
|
2
|
+
import { getAddress, parseAbi, zeroAddress } from 'viem';
|
|
3
|
+
import * as abis from '../../abis.js';
|
|
4
|
+
import { chainsMetadata } from '../../chains.js';
|
|
5
|
+
import { Chains, nativeAssetOut, toChain, uniV2Factories, uniV2Routers } from '../../config.js';
|
|
6
|
+
import { sameAddress } from '../../ecosystems.js';
|
|
7
|
+
import { pulseXConversionSwapPath } from '../providers.js';
|
|
8
|
+
import { pulsechainChainId } from '../producers.js';
|
|
9
|
+
import { assetNodeKey } from '../search/graph.js';
|
|
10
|
+
/**
|
|
11
|
+
* PulseX (the PulseChain-native decentralized exchange, used here for a
|
|
12
|
+
* conversion swap leg) as a plug-in `Carrier` for `@gibs/quotes`'s
|
|
13
|
+
* route-search engine. GIBS-SPECIFIC — see `docs/quote-service.md`'s
|
|
14
|
+
* "Carriers" section: "never built in ... PulseX." Registered from
|
|
15
|
+
* `packages/ui/src/lib/state/useRouteSearch.ts` / `route-search-quotes.ts`.
|
|
16
|
+
*
|
|
17
|
+
* See `omnibridge.ts`'s head doc for why this returns a plain object shaped
|
|
18
|
+
* like `Carrier` rather than importing that type.
|
|
19
|
+
*/
|
|
20
|
+
/** WPLS, the wrapped-native intermediary {@link pulseXConversionSwapPath} routes conversions through. */
|
|
21
|
+
const wplsAddress = nativeAssetOut[Chains.PLS];
|
|
22
|
+
/**
|
|
23
|
+
* Every PulseX deployment on PulseChain, V1 first: `uniV2Routers` paired with
|
|
24
|
+
* `uniV2Factories` by index. Each factory is the router's own
|
|
25
|
+
* `router.factory()` read on chain — see `uniV2Factories`'s own doc in
|
|
26
|
+
* `config.ts`. A router with no recorded factory is left out, because its
|
|
27
|
+
* pools cannot be read.
|
|
28
|
+
*/
|
|
29
|
+
export const pulseXVenues = (uniV2Routers[Chains.PLS] ?? []).flatMap((router, index) => {
|
|
30
|
+
const factory = uniV2Factories[Chains.PLS]?.[index];
|
|
31
|
+
return factory === undefined ? [] : [{ router: getAddress(router), factory: getAddress(factory) }];
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* The venue an edge with no {@link HopEdge.venue} runs on: V1, index 0. Every
|
|
35
|
+
* PulseX edge built before venues existed was quoted and signed on V1 — the
|
|
36
|
+
* same-chain swap sender's own hand-built edge still is — so this keeps that
|
|
37
|
+
* meaning rather than guessing a new one.
|
|
38
|
+
*/
|
|
39
|
+
const defaultPulseXVenue = pulseXVenues[0];
|
|
40
|
+
/**
|
|
41
|
+
* The router a PulseX edge must be signed on — the one its figure was quoted
|
|
42
|
+
* against. Reads the edge's own {@link HopEdge.venue}; an edge with none is a
|
|
43
|
+
* V1 edge (see {@link defaultPulseXVenue}).
|
|
44
|
+
*
|
|
45
|
+
* @param edge - the edge to sign
|
|
46
|
+
* @returns the router, or null when the edge is not a PulseX edge or names a venue that is not a known PulseX router
|
|
47
|
+
*/
|
|
48
|
+
export const pulseXRouterForEdge = (edge) => pulseXVenueForEdge(edge)?.router ?? null;
|
|
49
|
+
/**
|
|
50
|
+
* The {@link PulseXVenue} a PulseX edge runs on — see {@link pulseXRouterForEdge}.
|
|
51
|
+
*
|
|
52
|
+
* @param edge - the edge
|
|
53
|
+
* @returns the venue, or null when there is none
|
|
54
|
+
*/
|
|
55
|
+
const pulseXVenueForEdge = (edge) => {
|
|
56
|
+
if (edge.carrier !== 'PulseX')
|
|
57
|
+
return null;
|
|
58
|
+
if (edge.venue === undefined)
|
|
59
|
+
return defaultPulseXVenue ?? null;
|
|
60
|
+
const venue = edge.venue;
|
|
61
|
+
return pulseXVenues.find((candidate) => sameAddress(candidate.router, venue, 'evm')) ?? null;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* A node's own address for a PulseX pool lookup: native PLS (the zero
|
|
65
|
+
* address) is not a token the router or its factory ever holds a pool for —
|
|
66
|
+
* only Wrapped PLS is — so a native node resolves to {@link wplsAddress}
|
|
67
|
+
* before it ever reaches {@link pulseXConversionSwapPath}. Every other node
|
|
68
|
+
* is unchanged.
|
|
69
|
+
*
|
|
70
|
+
* MIRRORS `useRouteSearch.ts`'s `quotePathAddress` (the UI's async `ask`
|
|
71
|
+
* already resolves native this way through that function) — this is the
|
|
72
|
+
* SAME rule, restated here because `prepare`/`exact` never go through the
|
|
73
|
+
* UI's `readAmountOut` at all; see {@link pulseXPoolPath}'s own doc.
|
|
74
|
+
*
|
|
75
|
+
* @param node - the node to resolve
|
|
76
|
+
* @returns the node's own address, or {@link wplsAddress} for native PLS
|
|
77
|
+
*/
|
|
78
|
+
const poolTokenAddress = (node) => (sameAddress(node.address, zeroAddress, 'evm') ? wplsAddress : node.address);
|
|
79
|
+
/**
|
|
80
|
+
* The router swap path a PulseX hop from `from` to `to` prices and signs
|
|
81
|
+
* along, or null when there is no swap for PulseX to do — including when
|
|
82
|
+
* both ends resolve to the SAME pool token, which is true of PLS<->WPLS
|
|
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.
|
|
87
|
+
*
|
|
88
|
+
* EVERY PULSEX QUOTE AND SIGNATURE IN THIS FILE MUST GO THROUGH THIS ONE
|
|
89
|
+
* FUNCTION — `pulseXCarries`, `prepare`'s pool discovery, and `exact`'s hop
|
|
90
|
+
* pricing all call it, so native PLS resolves the same way in every one of
|
|
91
|
+
* them. Before this existed, `pulseXCarries` and `prepare`/`exact` called
|
|
92
|
+
* `pulseXConversionSwapPath` directly on the raw node address: a native PLS
|
|
93
|
+
* node produced a path that LITERALLY STARTED WITH THE ZERO ADDRESS
|
|
94
|
+
* (`[zeroAddress, wplsAddress, ...]`), which `pulseXCarries` accepted (its
|
|
95
|
+
* length is still >= 2) but for which `prepare` could never find a pool —
|
|
96
|
+
* the factory has no pair for the zero address — so `exact` always answered
|
|
97
|
+
* `null` for a direct native-PLS PulseX hop, and `enumerate.ts` pruned it,
|
|
98
|
+
* leaving only the two-signature wrap-then-swap path.
|
|
99
|
+
*
|
|
100
|
+
* @param from - the hop's origin node
|
|
101
|
+
* @param to - the hop's destination node
|
|
102
|
+
* @returns the swap path, or null when there is nothing for PulseX to swap
|
|
103
|
+
*/
|
|
104
|
+
export const pulseXPoolPath = (from, to) => {
|
|
105
|
+
const fromAddress = poolTokenAddress(from);
|
|
106
|
+
const toAddress = poolTokenAddress(to);
|
|
107
|
+
if (sameAddress(fromAddress, toAddress, 'evm'))
|
|
108
|
+
return null;
|
|
109
|
+
return pulseXConversionSwapPath(fromAddress, toAddress, wplsAddress);
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Whether PulseX can carry funds directly from `from` to `to`: both must be
|
|
113
|
+
* distinct Ethereum-Virtual-Machine tokens on PulseChain itself — PulseX runs
|
|
114
|
+
* only on PulseChain, never on an entry chain.
|
|
115
|
+
*
|
|
116
|
+
* NO TOKEN ALLOWLIST IS CONSULTED, on purpose: "any PulseChain token with a
|
|
117
|
+
* {@link pulseXPoolPath}" is every PulseChain token, because that function
|
|
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.
|
|
124
|
+
*/
|
|
125
|
+
const pulseXCarries = (from, to) => {
|
|
126
|
+
if (from.ecosystem !== 'evm' || to.ecosystem !== 'evm')
|
|
127
|
+
return false;
|
|
128
|
+
if (from.chainId !== pulsechainChainId || to.chainId !== pulsechainChainId)
|
|
129
|
+
return false;
|
|
130
|
+
const swapPath = pulseXPoolPath(from, to);
|
|
131
|
+
return swapPath !== null && swapPath.length >= 2;
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* PulseX's own constant-product swap fee, as a numerator over
|
|
135
|
+
* {@link PULSEX_FEE_DENOMINATOR} — the SAME `getAmountOut` formula every
|
|
136
|
+
* Uniswap-V2-style router uses: `amountOut = floor(amountIn * feeNumerator *
|
|
137
|
+
* reserveOut / (reserveIn * feeDenominator + amountIn * feeNumerator))`.
|
|
138
|
+
*
|
|
139
|
+
* MEASURED, NOT ASSUMED — see `uniV2Factories`'s own doc in `config.ts` for
|
|
140
|
+
* the full account. Reading live reserves from both PulseX router
|
|
141
|
+
* deployments (V1 `0x98bf93…`, V2 `0x165C34…`) and back-solving this ratio
|
|
142
|
+
* against each router's own `getAmountsOut`, on a one-hop and a two-hop
|
|
143
|
+
* path, gave the IDENTICAL ratio — `9971/10000` — for both, to the base
|
|
144
|
+
* unit. One constant serves both, and `pulsex-carrier.fork.test.ts` pins
|
|
145
|
+
* this claim against live chain data rather than trusting the arithmetic
|
|
146
|
+
* alone.
|
|
147
|
+
*/
|
|
148
|
+
const PULSEX_FEE_NUMERATOR = 9971n;
|
|
149
|
+
/** The denominator {@link PULSEX_FEE_NUMERATOR} is a fraction of. */
|
|
150
|
+
const PULSEX_FEE_DENOMINATOR = 10000n;
|
|
151
|
+
/** The minimal read-only factory ABI `prepare` needs — just enough to resolve a pool's address from its two tokens. */
|
|
152
|
+
const univ2FactoryAbi = parseAbi([
|
|
153
|
+
'function getPair(address tokenA, address tokenB) external view returns (address pair)',
|
|
154
|
+
]);
|
|
155
|
+
/**
|
|
156
|
+
* The Uniswap-V2-style constant-product `getAmountOut`, applied with
|
|
157
|
+
* {@link PULSEX_FEE_NUMERATOR}. Reproduces the router's own on-chain formula
|
|
158
|
+
* exactly — see `pulsex-carrier.fork.test.ts` — so PRICE IMPACT IS ALREADY
|
|
159
|
+
* IN HERE: this is the same nonlinear function the router evaluates, not a
|
|
160
|
+
* flat-fee approximation with an impact correction bolted on afterward.
|
|
161
|
+
*
|
|
162
|
+
* @param amountIn - the input amount, in `reserveIn`'s token's base units
|
|
163
|
+
* @param reserveIn - the pool's reserve of the input token
|
|
164
|
+
* @param reserveOut - the pool's reserve of the output token
|
|
165
|
+
* @returns the output amount, in `reserveOut`'s token's base units; 0 when either reserve is 0
|
|
166
|
+
*/
|
|
167
|
+
const constantProductAmountOut = (amountIn, reserveIn, reserveOut) => {
|
|
168
|
+
if (reserveIn <= 0n || reserveOut <= 0n || amountIn <= 0n)
|
|
169
|
+
return 0n;
|
|
170
|
+
const amountInWithFee = amountIn * PULSEX_FEE_NUMERATOR;
|
|
171
|
+
const numerator = amountInWithFee * reserveOut;
|
|
172
|
+
const denominator = reserveIn * PULSEX_FEE_DENOMINATOR + amountInWithFee;
|
|
173
|
+
return numerator / denominator;
|
|
174
|
+
};
|
|
175
|
+
/** 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
|
+
const poolKey = (tokenA, tokenB) => {
|
|
177
|
+
const [lower, higher] = [tokenA.toLowerCase(), tokenB.toLowerCase()].sort();
|
|
178
|
+
return `${lower}|${higher}`;
|
|
179
|
+
};
|
|
180
|
+
/** 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. */
|
|
181
|
+
const venuePoolKey = (venue, tokenA, tokenB) => `${venue.factory.toLowerCase()}|${poolKey(tokenA, tokenB)}`;
|
|
182
|
+
/** Every consecutive (tokenIn, tokenOut) hop a PulseX swap path implies. */
|
|
183
|
+
const hopsAlongPath = (path) => path.slice(0, -1).map((tokenIn, index) => [tokenIn, path[index + 1]]);
|
|
184
|
+
/**
|
|
185
|
+
* Batch-reads reserves for every pool `nodes`' candidate PulseX paths touch,
|
|
186
|
+
* on EVERY venue in `venues`, once, so {@link createPulseXCarrier}'s `exact`
|
|
187
|
+
* can answer every hop SYNCHRONOUSLY afterward.
|
|
188
|
+
*
|
|
189
|
+
* EACH VENUE IS READ THROUGH ITS OWN FACTORY. The same two tokens usually
|
|
190
|
+
* have a pool on V1 and another on V2, with different reserves; the two are
|
|
191
|
+
* stored apart ({@link venuePoolKey}) and never blended. `PULSEX_FEE_NUMERATOR`
|
|
192
|
+
* is proven identical for both deployments (see its own doc), so the formula
|
|
193
|
+
* is shared and only the reserves differ.
|
|
194
|
+
*
|
|
195
|
+
* TWO MULTICALLS ON PULSECHAIN, WHATEVER THE VENUE COUNT: `getPair` must
|
|
196
|
+
* resolve each pool's address before `getReserves` can be read from it, so
|
|
197
|
+
* the second read cannot join the first — but every venue's `getPair` rides
|
|
198
|
+
* the first multicall and every pool's `getReserves` rides the second.
|
|
199
|
+
*
|
|
200
|
+
* @param nodes - every node the search might branch through
|
|
201
|
+
* @param venues - the venues this carrier offers
|
|
202
|
+
* @param clientForChain - see {@link CreatePulseXCarrierOptions}'s own field
|
|
203
|
+
* @returns every pool this universe touches, keyed by {@link venuePoolKey} — pools with no `getPair` result are simply absent, not an error
|
|
204
|
+
*/
|
|
205
|
+
const preparePulseXReserves = async ({ nodes, venues, clientForChain, }) => {
|
|
206
|
+
const reservesByPoolKey = new Map();
|
|
207
|
+
const client = clientForChain(pulsechainChainId);
|
|
208
|
+
if (client === null || venues.length === 0)
|
|
209
|
+
return reservesByPoolKey;
|
|
210
|
+
const pulsechainNodes = nodes.filter((node) => node.ecosystem === 'evm' && node.chainId === pulsechainChainId);
|
|
211
|
+
const poolTokenPairs = new Map();
|
|
212
|
+
for (const from of pulsechainNodes) {
|
|
213
|
+
for (const to of pulsechainNodes) {
|
|
214
|
+
if (assetNodeKey(from) === assetNodeKey(to))
|
|
215
|
+
continue;
|
|
216
|
+
if (!pulseXCarries(from, to))
|
|
217
|
+
continue;
|
|
218
|
+
const path = pulseXPoolPath(from, to);
|
|
219
|
+
if (path === null)
|
|
220
|
+
continue;
|
|
221
|
+
for (const [tokenIn, tokenOut] of hopsAlongPath(path)) {
|
|
222
|
+
poolTokenPairs.set(poolKey(tokenIn, tokenOut), [tokenIn, tokenOut]);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (poolTokenPairs.size === 0)
|
|
227
|
+
return reservesByPoolKey;
|
|
228
|
+
const poolQueries = venues.flatMap((venue) => Array.from(poolTokenPairs.values()).map(([tokenIn, tokenOut]) => ({
|
|
229
|
+
key: venuePoolKey(venue, tokenIn, tokenOut),
|
|
230
|
+
factory: venue.factory,
|
|
231
|
+
tokenIn,
|
|
232
|
+
tokenOut,
|
|
233
|
+
})));
|
|
234
|
+
const factoryResults = await multicallRead({
|
|
235
|
+
chain: chainsMetadata[toChain(pulsechainChainId)],
|
|
236
|
+
client,
|
|
237
|
+
abi: univ2FactoryAbi,
|
|
238
|
+
calls: poolQueries.map((query) => ({
|
|
239
|
+
functionName: 'getPair',
|
|
240
|
+
target: query.factory,
|
|
241
|
+
args: [query.tokenIn, query.tokenOut],
|
|
242
|
+
allowFailure: true,
|
|
243
|
+
})),
|
|
244
|
+
});
|
|
245
|
+
const pairAddressByPoolKey = new Map();
|
|
246
|
+
poolQueries.forEach((query, index) => {
|
|
247
|
+
const pairAddress = factoryResults[index];
|
|
248
|
+
// A decoded address is exactly 42 characters; a failed `allowFailure`
|
|
249
|
+
// call's undecoded revert bytes are almost never that length — see
|
|
250
|
+
// `omnibridge.ts`'s identical guard for why `typeof` alone cannot tell
|
|
251
|
+
// success from failure for an address-returning call.
|
|
252
|
+
if (typeof pairAddress === 'string' && pairAddress.length === 42 && pairAddress !== zeroAddress) {
|
|
253
|
+
pairAddressByPoolKey.set(query.key, getAddress(pairAddress));
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
if (pairAddressByPoolKey.size === 0)
|
|
257
|
+
return reservesByPoolKey;
|
|
258
|
+
const reservePoolKeys = Array.from(pairAddressByPoolKey.keys());
|
|
259
|
+
const reserveResults = await multicallRead({
|
|
260
|
+
chain: chainsMetadata[toChain(pulsechainChainId)],
|
|
261
|
+
client,
|
|
262
|
+
abi: abis.pair,
|
|
263
|
+
calls: reservePoolKeys.map((key) => ({
|
|
264
|
+
functionName: 'getReserves',
|
|
265
|
+
target: pairAddressByPoolKey.get(key),
|
|
266
|
+
allowFailure: true,
|
|
267
|
+
})),
|
|
268
|
+
});
|
|
269
|
+
reservePoolKeys.forEach((key, index) => {
|
|
270
|
+
const result = reserveResults[index];
|
|
271
|
+
if (!Array.isArray(result))
|
|
272
|
+
return;
|
|
273
|
+
const [reserve0, reserve1] = result;
|
|
274
|
+
if (typeof reserve0 !== 'bigint' || typeof reserve1 !== 'bigint')
|
|
275
|
+
return;
|
|
276
|
+
// Standard Uniswap-V2-style factories sort a pair's tokens before
|
|
277
|
+
// creating it, so `token0` is always the LEXICOGRAPHICALLY LOWER
|
|
278
|
+
// address — confirmed against live PulseX pools 2026-09-24 (both V1 and
|
|
279
|
+
// V2), never re-derived with an extra `token0()` read.
|
|
280
|
+
reservesByPoolKey.set(key, { reserveOfLowerAddress: reserve0, reserveOfHigherAddress: reserve1 });
|
|
281
|
+
});
|
|
282
|
+
return reservesByPoolKey;
|
|
283
|
+
};
|
|
284
|
+
/**
|
|
285
|
+
* The venues a carrier offers: every known venue, or only the routers the
|
|
286
|
+
* host names. A named router that is not a known PulseX router is a
|
|
287
|
+
* configuration error, thrown at construction rather than offered silently.
|
|
288
|
+
*
|
|
289
|
+
* @param routers - the routers the host can sign on, or undefined for all
|
|
290
|
+
* @returns the venues, in {@link pulseXVenues} order
|
|
291
|
+
*/
|
|
292
|
+
const selectPulseXVenues = (routers) => {
|
|
293
|
+
if (routers === undefined)
|
|
294
|
+
return pulseXVenues;
|
|
295
|
+
for (const router of routers) {
|
|
296
|
+
if (!pulseXVenues.some((venue) => sameAddress(venue.router, router, 'evm'))) {
|
|
297
|
+
throw new Error(`createPulseXCarrier: ${router} is not a known PulseX router on PulseChain`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return pulseXVenues.filter((venue) => routers.some((router) => sameAddress(venue.router, router, 'evm')));
|
|
301
|
+
};
|
|
302
|
+
/**
|
|
303
|
+
* Builds the PulseX conversion swap as a plug-in carrier.
|
|
304
|
+
*
|
|
305
|
+
* @param options - see {@link CreatePulseXCarrierOptions}
|
|
306
|
+
* @returns the carrier — `id: 'PulseX'`
|
|
307
|
+
*/
|
|
308
|
+
export const createPulseXCarrier = (options) => {
|
|
309
|
+
const { readAmountOut, clientForChain } = options;
|
|
310
|
+
const venues = selectPulseXVenues(options.routers);
|
|
311
|
+
// ONE EDGE PER VENUE. Each names its router as its `venue`, so the search
|
|
312
|
+
// quotes and ranks V1 and V2 as two separate hops and the better one wins
|
|
313
|
+
// on its own figure — and the winner is signed on the router it names.
|
|
314
|
+
const edgesFrom = (from, to) => {
|
|
315
|
+
if (assetNodeKey(from) === assetNodeKey(to))
|
|
316
|
+
return [];
|
|
317
|
+
if (!pulseXCarries(from, to))
|
|
318
|
+
return [];
|
|
319
|
+
return venues.map((venue) => ({
|
|
320
|
+
carrier: 'PulseX',
|
|
321
|
+
from,
|
|
322
|
+
to,
|
|
323
|
+
fusesNext: false,
|
|
324
|
+
origin: 'wallet',
|
|
325
|
+
floors: [],
|
|
326
|
+
venue: venue.router,
|
|
327
|
+
}));
|
|
328
|
+
};
|
|
329
|
+
const ask = async ({ edge, inputAmount }) => {
|
|
330
|
+
const amountOut = await readAmountOut(edge, inputAmount);
|
|
331
|
+
if (amountOut === null || amountOut <= 0n) {
|
|
332
|
+
return { ok: false, refusal: { kind: 'no-quote', reason: 'no liquidity on this PulseX path', minimumAmount: null } };
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
ok: true,
|
|
336
|
+
deliveredAmount: amountOut,
|
|
337
|
+
// getAmountsOut is exact for the CURRENT block; no slippage tolerance is
|
|
338
|
+
// modelled here, so the guaranteed figure is left equal to the expected
|
|
339
|
+
// one, matching the omnibridge carrier's own convention.
|
|
340
|
+
minimumDeliveredAmount: amountOut,
|
|
341
|
+
claimedDurationSeconds: 0,
|
|
342
|
+
};
|
|
343
|
+
};
|
|
344
|
+
// Populated by `prepare`, read by `exact` — see `omnibridge.ts`'s identical
|
|
345
|
+
// field for why a mutable closure here is safe (client-side,
|
|
346
|
+
// one-search-at-a-time; PulseX is likewise "never built into" the shared
|
|
347
|
+
// display-quote service).
|
|
348
|
+
let reservesByPoolKey = new Map();
|
|
349
|
+
const prepare = async (nodes) => {
|
|
350
|
+
if (clientForChain === undefined)
|
|
351
|
+
return;
|
|
352
|
+
reservesByPoolKey = await preparePulseXReserves({ nodes, venues, clientForChain });
|
|
353
|
+
};
|
|
354
|
+
const exact = (edge, amountIn) => {
|
|
355
|
+
if (!pulseXCarries(edge.from, edge.to))
|
|
356
|
+
return null;
|
|
357
|
+
// Quoted on the edge's OWN venue — the router it will be signed on — and
|
|
358
|
+
// only on a venue this carrier offers.
|
|
359
|
+
const venue = pulseXVenueForEdge(edge);
|
|
360
|
+
if (venue === null || !venues.includes(venue))
|
|
361
|
+
return null;
|
|
362
|
+
const path = pulseXPoolPath(edge.from, edge.to);
|
|
363
|
+
if (path === null)
|
|
364
|
+
return null;
|
|
365
|
+
const hops = hopsAlongPath(path);
|
|
366
|
+
if (hops.length === 0)
|
|
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;
|
|
387
|
+
}
|
|
388
|
+
return { outcome: 'delivered', deliveredAmount: runningAmount };
|
|
389
|
+
};
|
|
390
|
+
return {
|
|
391
|
+
id: 'PulseX',
|
|
392
|
+
edgesFrom,
|
|
393
|
+
ask,
|
|
394
|
+
prepare,
|
|
395
|
+
exact,
|
|
396
|
+
isRateLimited: false,
|
|
397
|
+
// A same-chain swap requires the reader's own wallet signature — never
|
|
398
|
+
// a by-hand deposit.
|
|
399
|
+
depositMode: 'wallet-signed',
|
|
400
|
+
sender: 'swap',
|
|
401
|
+
// A same-chain swap settles with the transaction itself.
|
|
402
|
+
settlementSeconds: 0,
|
|
403
|
+
};
|
|
404
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type AssetNode, type HopEdge } from '../search/graph.js';
|
|
2
|
+
import type { CarrierAskAnswer, CarrierExactResult } from './ask-answer.js';
|
|
3
|
+
/**
|
|
4
|
+
* Builds the native wrap/unwrap carrier as a plug-in.
|
|
5
|
+
*
|
|
6
|
+
* NO OPTIONS, NO `prepare`. There is no local read to batch — a wrap has no
|
|
7
|
+
* minimum, no maximum, no fee rate and no reserve to be wrong about; it is a
|
|
8
|
+
* pure 1:1 identity over every chain `config.ts` names a wrapped-native
|
|
9
|
+
* token for. `exact` alone is total over every edge this carrier offers.
|
|
10
|
+
*
|
|
11
|
+
* @returns the carrier — `id: 'wrap'`
|
|
12
|
+
*/
|
|
13
|
+
export declare const createWrapCarrier: () => {
|
|
14
|
+
id: string;
|
|
15
|
+
edgesFrom: (from: AssetNode, to: AssetNode) => readonly HopEdge[];
|
|
16
|
+
ask: ({ edge, inputAmount }: {
|
|
17
|
+
edge: HopEdge;
|
|
18
|
+
inputAmount: bigint;
|
|
19
|
+
}) => Promise<CarrierAskAnswer>;
|
|
20
|
+
exact: (edge: HopEdge, amountIn: bigint) => CarrierExactResult | null;
|
|
21
|
+
isRateLimited: boolean;
|
|
22
|
+
depositMode: "wallet-signed";
|
|
23
|
+
sender: "swap";
|
|
24
|
+
settlementSeconds: number;
|
|
25
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { getAddress, zeroAddress } from 'viem';
|
|
2
|
+
import { nativeAssetOut, toChain } from '../../config.js';
|
|
3
|
+
import { assetNodeKey } from '../search/graph.js';
|
|
4
|
+
/**
|
|
5
|
+
* Native-coin wrap/unwrap (PLS <-> WPLS on PulseChain, and the same pair on
|
|
6
|
+
* every other Ethereum-Virtual-Machine chain the search graph touches — ETH
|
|
7
|
+
* <-> WETH, BNB <-> WBNB) as a plug-in `Carrier` for `@gibs/quotes`'s
|
|
8
|
+
* route-search engine. GIBS-SPECIFIC in the sense `omnibridge.ts`/`pulsex.ts`
|
|
9
|
+
* are (see `omnibridge.ts`'s head doc for why this returns a plain object
|
|
10
|
+
* shaped like `Carrier` rather than importing that type) — though the
|
|
11
|
+
* carrier itself names no chain: it derives every wrap pair from
|
|
12
|
+
* `config.ts`'s own `nativeAssetOut` map, so a chain added there gains a
|
|
13
|
+
* wrap edge with no change here.
|
|
14
|
+
*
|
|
15
|
+
* WHY THIS IS A SEPARATE CARRIER FROM PulseX. `pulsex.ts`'s own
|
|
16
|
+
* `readPulseXAmountOut` (via `pulseXSwapPathForEdge` in
|
|
17
|
+
* `packages/ui/src/lib/state/useRouteSearch.ts`) quotes native Pulse AS
|
|
18
|
+
* Wrapped Pulse — both map to the same `quotePathAddress` — so a PLS<->WPLS
|
|
19
|
+
* pair collapses to "no swap to quote" and PulseX has never carried it.
|
|
20
|
+
* There is no PulseX POOL for native-versus-wrapped; it is a 1:1 contract
|
|
21
|
+
* call (`deposit()`/`withdraw(uint256)`), not a trade, so it needed its own
|
|
22
|
+
* carrier rather than a PulseX special case.
|
|
23
|
+
*
|
|
24
|
+
* SENDER: `'swap'`, THE SAME NAME `pulsex.ts` USES — the reader's own
|
|
25
|
+
* connected wallet signs a `deposit()`/`withdraw(uint256)` call on the
|
|
26
|
+
* wrapped token's own contract (WETH9-style; confirmed present on WPLS by
|
|
27
|
+
* bytecode selector 2026-09-24), exactly the wallet a same-chain PulseX swap
|
|
28
|
+
* already uses. WHETHER `settings.ts`'s EXISTING swap-transaction builder
|
|
29
|
+
* (`pulseXSwapTransactionInputs`) CAN BUILD THIS CALL: NO, not without a
|
|
30
|
+
* change there — that builder only ever targets a PulseX ROUTER address and
|
|
31
|
+
* encodes `swapExact*` calldata; a wrap needs `to` = the wrapped token
|
|
32
|
+
* itself and `data` = plain `deposit()`/`withdraw(amount)` calldata, which
|
|
33
|
+
* is a different (and simpler) shape. Sharing the `'swap'` sender name says
|
|
34
|
+
* "the same wallet, the same signing step in the flow" — it does not claim
|
|
35
|
+
* the existing builder already emits this calldata. Building that call is
|
|
36
|
+
* `settings.ts`'s job, out of this carrier's scope; this module answers only
|
|
37
|
+
* the route-search question ("what does this edge deliver, exactly").
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* Whether this carrier can wrap/unwrap directly between `from` and `to`:
|
|
41
|
+
* same Ethereum-Virtual-Machine chain, and the two addresses are EXACTLY the
|
|
42
|
+
* zero-address/wrapped-native pair `config.ts`'s `nativeAssetOut` names for
|
|
43
|
+
* that chain — nothing else, so a plain token pair that merely happens to
|
|
44
|
+
* share a chain is never mistaken for a wrap.
|
|
45
|
+
*/
|
|
46
|
+
const wrapCarries = (from, to) => {
|
|
47
|
+
if (from.ecosystem !== 'evm' || to.ecosystem !== 'evm')
|
|
48
|
+
return false;
|
|
49
|
+
if (from.chainId !== to.chainId)
|
|
50
|
+
return false;
|
|
51
|
+
const wrappedNativeAddress = nativeAssetOut[toChain(from.chainId)];
|
|
52
|
+
if (wrappedNativeAddress === undefined)
|
|
53
|
+
return false;
|
|
54
|
+
const wrapped = getAddress(wrappedNativeAddress).toLowerCase();
|
|
55
|
+
const fromAddress = from.address.toLowerCase();
|
|
56
|
+
const toAddress = to.address.toLowerCase();
|
|
57
|
+
const fromIsNative = fromAddress === zeroAddress;
|
|
58
|
+
const toIsNative = toAddress === zeroAddress;
|
|
59
|
+
if (fromIsNative)
|
|
60
|
+
return toAddress === wrapped;
|
|
61
|
+
if (toIsNative)
|
|
62
|
+
return fromAddress === wrapped;
|
|
63
|
+
return false;
|
|
64
|
+
};
|
|
65
|
+
/** 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). */
|
|
66
|
+
const wrapExact = (edge, amountIn) => {
|
|
67
|
+
if (!wrapCarries(edge.from, edge.to))
|
|
68
|
+
return null;
|
|
69
|
+
return { outcome: 'delivered', deliveredAmount: amountIn };
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Builds the native wrap/unwrap carrier as a plug-in.
|
|
73
|
+
*
|
|
74
|
+
* NO OPTIONS, NO `prepare`. There is no local read to batch — a wrap has no
|
|
75
|
+
* minimum, no maximum, no fee rate and no reserve to be wrong about; it is a
|
|
76
|
+
* pure 1:1 identity over every chain `config.ts` names a wrapped-native
|
|
77
|
+
* token for. `exact` alone is total over every edge this carrier offers.
|
|
78
|
+
*
|
|
79
|
+
* @returns the carrier — `id: 'wrap'`
|
|
80
|
+
*/
|
|
81
|
+
export const createWrapCarrier = () => {
|
|
82
|
+
const edgesFrom = (from, to) => {
|
|
83
|
+
if (assetNodeKey(from) === assetNodeKey(to))
|
|
84
|
+
return [];
|
|
85
|
+
if (!wrapCarries(from, to))
|
|
86
|
+
return [];
|
|
87
|
+
return [{ carrier: 'wrap', from, to, fusesNext: false, origin: 'wallet', floors: [] }];
|
|
88
|
+
};
|
|
89
|
+
const ask = async ({ edge, inputAmount }) => {
|
|
90
|
+
const result = wrapExact(edge, inputAmount);
|
|
91
|
+
if (result === null || result.outcome === 'refused') {
|
|
92
|
+
return { ok: false, refusal: { kind: 'cannot-carry', reason: 'not a native/wrapped pair', minimumAmount: null } };
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
ok: true,
|
|
96
|
+
deliveredAmount: result.deliveredAmount,
|
|
97
|
+
// 1:1, always — there is no slippage to guarantee against.
|
|
98
|
+
minimumDeliveredAmount: result.deliveredAmount,
|
|
99
|
+
claimedDurationSeconds: 0,
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
id: 'wrap',
|
|
104
|
+
edgesFrom,
|
|
105
|
+
ask,
|
|
106
|
+
exact: wrapExact,
|
|
107
|
+
isRateLimited: false,
|
|
108
|
+
// A `deposit()`/`withdraw()` call requires the reader's own wallet
|
|
109
|
+
// signature — never a by-hand deposit.
|
|
110
|
+
depositMode: 'wallet-signed',
|
|
111
|
+
sender: 'swap',
|
|
112
|
+
// A same-chain call settles with the transaction itself.
|
|
113
|
+
settlementSeconds: 0,
|
|
114
|
+
};
|
|
115
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a composed multi-signature route is good enough to offer at all.
|
|
3
|
+
*
|
|
4
|
+
* WHY A ROUTE NEEDS A GATE THAT A SINGLE CROSSING DOES NOT. A one-signature
|
|
5
|
+
* route that turns out badly costs the reader one transaction and they still
|
|
6
|
+
* hold something they chose. A composed route — an aggregator leg, then an
|
|
7
|
+
* omnibridge crossing, then a swap — strands them between signatures: after
|
|
8
|
+
* the first they hold an anchor token on a chain they did not ask for, and the
|
|
9
|
+
* only way out is to pay again. So a cheap first leg followed by a bad swap is
|
|
10
|
+
* worse than the route it replaced, and the difference has to be found BEFORE
|
|
11
|
+
* the route is listed rather than after the reader has signed.
|
|
12
|
+
*
|
|
13
|
+
* NOTHING HERE COSTS A REQUEST. Both figures the verdict reads are already in
|
|
14
|
+
* hand when a composer runs: the swap's price impact comes from the on-chain
|
|
15
|
+
* `getAmountsOut` pair the estimate already fetched, and the two dollar values
|
|
16
|
+
* come from the route's own basis. The gate is free, which is why it can run
|
|
17
|
+
* on every route rather than only on suspicious ones.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* The price impact above which a composed route is withdrawn, in percent.
|
|
21
|
+
*
|
|
22
|
+
* MEASURED RATHER THAN CHOSEN. Across the anchor and size combinations swept
|
|
23
|
+
* on 2026-09-21 — bridged wrapped ether, USD Coin, DAI and USDT into wrapped
|
|
24
|
+
* PulseChain coin, at roughly forty and one hundred and forty United States
|
|
25
|
+
* dollars — the impact ran from 0.008 percent to 0.037 percent, with bridged
|
|
26
|
+
* USDT the one outlier at 0.34 to 0.37 percent. Three percent is two orders of
|
|
27
|
+
* magnitude above the normal case and an order above the worst observed, so it
|
|
28
|
+
* cannot fire on an ordinary route; and it is still far below the point where
|
|
29
|
+
* a reader is quietly losing real money on a leg they cannot undo.
|
|
30
|
+
*
|
|
31
|
+
* See `docs/near-intents-conversion-composition.md` for the full sweep.
|
|
32
|
+
*/
|
|
33
|
+
export declare const COMPOSED_ROUTE_MAX_PRICE_IMPACT_PERCENT = 3;
|
|
34
|
+
/**
|
|
35
|
+
* The end-to-end value loss above which a composed route is withdrawn, in
|
|
36
|
+
* percent of what the reader pays.
|
|
37
|
+
*
|
|
38
|
+
* A SECOND TEST BECAUSE THE FIRST CAN READ FINE ON A BROKEN POOL. Price impact
|
|
39
|
+
* is measured against a marginal reference quote taken from the same pool, so
|
|
40
|
+
* a pool that is uniformly mispriced — a stale pair, a pair holding almost
|
|
41
|
+
* nothing — reports a small impact while paying out far less than the tokens
|
|
42
|
+
* going in are worth. Only comparing the two ends of the journey catches that.
|
|
43
|
+
*
|
|
44
|
+
* Measured whole-journey loss on the same sweep ran from 0.81 to 1.36 percent,
|
|
45
|
+
* of which the swap fee alone is 0.29. Five percent leaves every observed
|
|
46
|
+
* route untouched and still stops a route that is quietly eating a twentieth
|
|
47
|
+
* of the reader's money.
|
|
48
|
+
*/
|
|
49
|
+
export declare const COMPOSED_ROUTE_MAX_VALUE_LOSS_PERCENT = 5;
|
|
50
|
+
/**
|
|
51
|
+
* The verdict on one composed route.
|
|
52
|
+
*
|
|
53
|
+
* THREE STATES RATHER THAN A BOOLEAN, because "we have not priced this yet"
|
|
54
|
+
* and "we priced it and it is bad" are different facts and a caller must be
|
|
55
|
+
* able to act differently on them. Collapsing them would either hide a route
|
|
56
|
+
* that is merely still loading or offer one nobody has checked.
|
|
57
|
+
*/
|
|
58
|
+
export type ComposedRouteVerdict = {
|
|
59
|
+
readonly state: 'acceptable';
|
|
60
|
+
} | {
|
|
61
|
+
readonly state: 'unknown';
|
|
62
|
+
readonly missing: 'price-impact' | 'route-value';
|
|
63
|
+
} | {
|
|
64
|
+
readonly state: 'rejected';
|
|
65
|
+
readonly reason: 'price-impact' | 'value-loss';
|
|
66
|
+
/** The measured figure, in percent, that failed its ceiling. */
|
|
67
|
+
readonly measuredPercent: number;
|
|
68
|
+
/** The ceiling it failed. */
|
|
69
|
+
readonly ceilingPercent: number;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Judges a composed route against both ceilings.
|
|
73
|
+
*
|
|
74
|
+
* Checks price impact first and value loss second, because impact names the
|
|
75
|
+
* specific leg at fault while value loss only says the journey as a whole went
|
|
76
|
+
* wrong — so reporting impact when both fail is the more useful answer.
|
|
77
|
+
*
|
|
78
|
+
* @param options.priceImpactPercent - the swap leg's price impact, from the
|
|
79
|
+
* estimate's marginal reference quote; null when not yet quoted
|
|
80
|
+
* @param options.paidUsdValue - what the reader spends at the origin, in
|
|
81
|
+
* United States dollars; null when unpriced
|
|
82
|
+
* @param options.deliveredUsdValue - what the route delivers, valued on the
|
|
83
|
+
* SAME basis as `paidUsdValue`; null when unpriced
|
|
84
|
+
* @returns the verdict
|
|
85
|
+
*/
|
|
86
|
+
export declare const assessComposedRoute: ({ priceImpactPercent, paidUsdValue, deliveredUsdValue, }: {
|
|
87
|
+
priceImpactPercent: number | null | undefined;
|
|
88
|
+
paidUsdValue: number | null | undefined;
|
|
89
|
+
deliveredUsdValue: number | null | undefined;
|
|
90
|
+
}) => ComposedRouteVerdict;
|
|
91
|
+
/**
|
|
92
|
+
* Whether a composed route may be listed on the strip.
|
|
93
|
+
*
|
|
94
|
+
* ONLY AN EXPLICIT PASS LISTS THE ROUTE. An unknown verdict withholds it, the
|
|
95
|
+
* same as a rejection, because a three-signature route nobody has checked is
|
|
96
|
+
* the case this gate exists for. The two remain distinct in
|
|
97
|
+
* {@link ComposedRouteVerdict} so a surface can tell the reader "still
|
|
98
|
+
* pricing" rather than "no route", which a boolean alone could not.
|
|
99
|
+
*
|
|
100
|
+
* @param verdict - the verdict from {@link assessComposedRoute}
|
|
101
|
+
* @returns true only when the route passed both ceilings
|
|
102
|
+
*/
|
|
103
|
+
export declare const composedRouteMayBeOffered: (verdict: ComposedRouteVerdict) => boolean;
|