@gibs/bridge-sdk 1.18.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/README.md +33 -0
- 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 +2 -464
- 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.d.ts +13 -0
- package/dist/cjs/near-intents-route.js +7 -140
- 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.d.ts +46 -1
- package/dist/cjs/routing/candidate-route.js +12 -40
- 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.d.ts +1 -1
- package/dist/cjs/routing/index.js +3 -30
- 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 +5 -510
- 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/cjs/types.d.ts +8 -0
- package/dist/claims.js +0 -60
- package/dist/config.js +1 -211
- package/dist/consolidated-quote.js +2 -464
- 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.d.ts +13 -0
- package/dist/near-intents-route.js +5 -139
- 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.d.ts +46 -1
- package/dist/routing/candidate-route.js +10 -39
- 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.d.ts +1 -1
- package/dist/routing/index.js +1 -29
- 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 +6 -511
- 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/dist/types.d.ts +8 -0
- package/package.json +3 -3
|
@@ -1,301 +1,62 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure route-producer helpers and constants shared by every route builder.
|
|
3
|
-
* Lives in the software development kit so the builders can relocate here in
|
|
4
|
-
* subsequent tasks without pulling user-interface dependencies into the kit.
|
|
5
|
-
*
|
|
6
|
-
* All helpers are framework-agnostic: no React, no Svelte, no display formatters.
|
|
7
|
-
*/
|
|
8
1
|
import { Chains, Providers, pathway } from '../config.js';
|
|
9
2
|
import { chainDisplayName } from '../chain-names.js';
|
|
10
3
|
import { usdValueOfAmount } from './usd-value.js';
|
|
11
4
|
import * as imageLinks from '../image-links.js';
|
|
12
5
|
import { roundUsdToCents } from './format.js';
|
|
13
6
|
import { formatUnits } from 'viem';
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// Constants
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
/**
|
|
18
|
-
* The PulseX swap fee in basis points (hundredths of a percent). The deferred
|
|
19
|
-
* outbound leg swaps the bridged intermediate token into the wanted token on the
|
|
20
|
-
* entry chain at this rate, so the deferred section's fee is APPROXIMATE — the
|
|
21
|
-
* binding quote is only known when the user returns. 29 basis points is roughly
|
|
22
|
-
* 0.29%.
|
|
23
|
-
*/
|
|
24
7
|
const PULSEX_SWAP_FEE_BASIS_POINTS = 29n;
|
|
25
|
-
/** The denominator that turns {@link PULSEX_SWAP_FEE_BASIS_POINTS} into a fraction. */
|
|
26
8
|
const BASIS_POINTS_DENOMINATOR = 10000n;
|
|
27
|
-
/**
|
|
28
|
-
* Divisor for the MARGINAL reference swap quote. A swap's price impact cannot be
|
|
29
|
-
* read from the trade quote alone — `getAmountsOut(fullInput)` already folds the
|
|
30
|
-
* impact into its output, and the PulseChain USD prices are themselves PulseX
|
|
31
|
-
* pool quotes, so differencing them cancels the impact out (it vanishes to
|
|
32
|
-
* $0.00). Instead the estimate quotes a TINY reference input — `1/this` of the
|
|
33
|
-
* trade — whose own impact is negligible, giving the near-spot marginal rate. The
|
|
34
|
-
* gap between that marginal rate and the full trade's realized rate IS the price
|
|
35
|
-
* impact (the swap fee rides in BOTH quotes, so it cancels and the gap is impact
|
|
36
|
-
* alone). 1/10000 of the trade is small enough to be effectively marginal yet
|
|
37
|
-
* large enough that its output does not round away.
|
|
38
|
-
*/
|
|
39
9
|
export const PULSEX_MARGINAL_REFERENCE_DIVISOR = 10000n;
|
|
40
|
-
/**
|
|
41
|
-
* The approximate wall-clock wait for an omnibridge crossing to settle before a
|
|
42
|
-
* DEFERRED swap leg can be signed — the same ~20 minutes whether the swap happens
|
|
43
|
-
* on the entry chain (outbound PulseChain → Ethereum/Binance Smart Chain) or on
|
|
44
|
-
* PulseChain (conversion route: bridge an anchor in, then swap on PulseX).
|
|
45
|
-
* Surfaced compactly as `~20 min` in the deferred section head, mirroring how
|
|
46
|
-
* every other section shows its estimate, rather than spelled out inside the
|
|
47
|
-
* deferred note.
|
|
48
|
-
*/
|
|
49
10
|
export const BRIDGE_SETTLEMENT_ETA_MINUTES = 20;
|
|
50
|
-
/**
|
|
51
|
-
* The approximate wall-clock duration of the SWAP itself, in seconds — a PulseX
|
|
52
|
-
* swap (or an entry-chain swap) settles in roughly ten seconds once signed. This
|
|
53
|
-
* is the swap leg's OWN time, distinct from the ~20-minute crossing it waits
|
|
54
|
-
* behind ({@link BRIDGE_SETTLEMENT_ETA_MINUTES}): the wait belongs on the BRIDGE
|
|
55
|
-
* leg, the near-instant swap on the SWAP leg, so each section reads honestly
|
|
56
|
-
* ("~20 min" to cross, then "~10 sec" to swap) rather than stamping the crossing's
|
|
57
|
-
* twenty minutes onto the swap.
|
|
58
|
-
*/
|
|
59
11
|
const PULSEX_SWAP_ETA_SECONDS = 10;
|
|
60
|
-
/** Decimal chain id for PulseChain — the shared destination of every inbound route. */
|
|
61
12
|
export const pulsechainChainId = Number(Chains.PLS);
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
// Private backing maps
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
/**
|
|
66
|
-
* The fully spelled-out display name of each omnibridge provider, keyed by the
|
|
67
|
-
* raw provider identifier the builders carry. These are the headline names the
|
|
68
|
-
* route strip shows as the route's bridge.
|
|
69
|
-
*
|
|
70
|
-
* Typed `Record<Provider, string>`, exhaustive over the two real omnibridge
|
|
71
|
-
* crossings, so a third one added to `Providers` without a name here is a
|
|
72
|
-
* compile error rather than a silent gap in {@link omnibridgeProviderDisplayName}.
|
|
73
|
-
*/
|
|
74
13
|
const omnibridgeProviderDisplayNames = {
|
|
75
14
|
[Providers.PULSECHAIN]: 'PulseChain Bridge',
|
|
76
15
|
[Providers.TOKENSEX]: 'Tokens Express Bridge',
|
|
77
16
|
};
|
|
78
|
-
/**
|
|
79
|
-
* Whether `value` genuinely names an omnibridge crossing, narrowing it to
|
|
80
|
-
* {@link Provider}.
|
|
81
|
-
*
|
|
82
|
-
* @param value - a string that may or may not be a real omnibridge provider
|
|
83
|
-
* @returns whether `value` is one of the two registered crossings
|
|
84
|
-
*/
|
|
85
17
|
const isOmnibridgeProvider = (value) => value === Providers.PULSECHAIN || value === Providers.TOKENSEX;
|
|
86
|
-
/**
|
|
87
|
-
* The display name for an omnibridge crossing, fully spelled out. Narrowed to
|
|
88
|
-
* exactly {@link Provider} — the two real crossings — rather than any string,
|
|
89
|
-
* so passing an aggregator's `ProviderId` (or anything else) is a COMPILE
|
|
90
|
-
* ERROR here, not a silently wrong name.
|
|
91
|
-
*
|
|
92
|
-
* PREFER THIS OVER {@link providerDisplayName} wherever the caller already
|
|
93
|
-
* holds a genuine `Provider`. `providerDisplayName` exists only because some
|
|
94
|
-
* callers (`RouteCandidate.omnibridgeProvider`, `RouteData.headlineProviderKey`)
|
|
95
|
-
* are typed `string` at their own source and cannot be narrowed from here.
|
|
96
|
-
*
|
|
97
|
-
* @param provider - a real omnibridge provider
|
|
98
|
-
* @returns the spelled-out provider name
|
|
99
|
-
*/
|
|
100
18
|
export const omnibridgeProviderDisplayName = (provider) => omnibridgeProviderDisplayNames[provider];
|
|
101
|
-
/**
|
|
102
|
-
* The fully spelled-out display name of an omnibridge ENTRY chain, keyed by its
|
|
103
|
-
* decimal chain id. Acronyms are spelled out rather than read from viem's terse
|
|
104
|
-
* `name`. Ethereum and Binance Smart Chain are the only omnibridge entry points;
|
|
105
|
-
* any other chain id has no entry and {@link resolveEntryChainName} returns null.
|
|
106
|
-
*/
|
|
107
19
|
const entryChainDisplayName = {
|
|
108
20
|
[Number(Chains.ETH)]: 'Ethereum',
|
|
109
21
|
[Number(Chains.BNB)]: 'Binance Smart Chain',
|
|
110
22
|
};
|
|
111
|
-
// ---------------------------------------------------------------------------
|
|
112
|
-
// Helpers
|
|
113
|
-
// ---------------------------------------------------------------------------
|
|
114
|
-
/**
|
|
115
|
-
* Adds two nullable United States dollar NUMBERS. A null operand contributes
|
|
116
|
-
* nothing; two nulls yield null so an all-unpriced route keeps a null cost rather
|
|
117
|
-
* than a misleading 0.
|
|
118
|
-
*
|
|
119
|
-
* This sums the RAW values and leaves the single cent-rounding to the display
|
|
120
|
-
* adapter (round(a + b)). It deliberately differs from the pre-split
|
|
121
|
-
* `sumFormattedUsd`, which summed two already-cent-rounded strings
|
|
122
|
-
* (round(a) + round(b)); the two can diverge by a cent when both operands carry
|
|
123
|
-
* sub-cent fractions. Rounding the total once is the more accurate result and
|
|
124
|
-
* keeps this helper free of any display formatter, so the raw producers stay
|
|
125
|
-
* relocatable. See the "sums the raw total once" test for the locked behavior.
|
|
126
|
-
*
|
|
127
|
-
* @param first - the first dollar value, or null/undefined when absent
|
|
128
|
-
* @param second - the second dollar value, or null/undefined when absent
|
|
129
|
-
* @returns the sum, or null when both operands are absent
|
|
130
|
-
*/
|
|
131
23
|
export const addUsdValues = (first, second) => {
|
|
132
24
|
if (first == null && second == null)
|
|
133
25
|
return null;
|
|
134
26
|
return (first ?? 0) + (second ?? 0);
|
|
135
27
|
};
|
|
136
|
-
/**
|
|
137
|
-
* Subtracts a cost from a dollar value on the SAME basis — the operation every
|
|
138
|
-
* producer uses to derive what a leg delivers from what entered it.
|
|
139
|
-
*
|
|
140
|
-
* Unlike {@link addUsdValues} this is deliberately NOT symmetric about null: an
|
|
141
|
-
* unknown starting value cannot be reduced, so it stays unknown, while an unknown
|
|
142
|
-
* cost is treated as no cost. That asymmetry is what keeps a route with one
|
|
143
|
-
* unpriced leg from reporting a delivered value it never established.
|
|
144
|
-
*
|
|
145
|
-
* @param value - the dollar value entering the leg, or null/undefined when unknown
|
|
146
|
-
* @param cost - the leg's dollar cost, or null/undefined when unpriced
|
|
147
|
-
* @returns the value less the cost, or null when the starting value is unknown
|
|
148
|
-
*/
|
|
149
28
|
export const subtractUsdValues = (value, cost) => {
|
|
150
29
|
if (value == null)
|
|
151
30
|
return null;
|
|
152
31
|
return value - (cost ?? 0);
|
|
153
32
|
};
|
|
154
|
-
/**
|
|
155
|
-
* The display name for an omnibridge crossing, accepting a raw `string` rather
|
|
156
|
-
* than {@link Provider}.
|
|
157
|
-
*
|
|
158
|
-
* PREFER {@link omnibridgeProviderDisplayName} INSTEAD wherever the caller
|
|
159
|
-
* already holds a genuine `Provider` — that one is a compile error for
|
|
160
|
-
* anything else. This wide version exists only for callers whose OWN field is
|
|
161
|
-
* typed `string` at its source and so cannot be narrowed here:
|
|
162
|
-
* `RouteCandidate.omnibridgeProvider` and `RouteData.headlineProviderKey`
|
|
163
|
-
* (`routing/candidate.ts`, `routing/route-data.ts`).
|
|
164
|
-
*
|
|
165
|
-
* KEPT DELIBERATELY NON-THROWING, UNLIKE {@link providerById}. The obvious
|
|
166
|
-
* next step — throw on an unrecognized key, the way `providerById` does for
|
|
167
|
-
* an aggregator — was tried and reverted: `toRouteView.ts`'s headline
|
|
168
|
-
* resolution calls this with `RouteData.headlineProviderKey`, a `string` this
|
|
169
|
-
* repository does not otherwise guarantee is a real provider of ANY kind, and
|
|
170
|
-
* an existing, deliberate test (`toRouteView.test.ts`, "headline falls back
|
|
171
|
-
* to 'Omnibridge' for an unknown headline provider key") requires that a
|
|
172
|
-
* genuinely-unrecognized key render a generic label rather than crash the
|
|
173
|
-
* whole route card. A NEAR Intents leg being mislabeled "Omnibridge" was a
|
|
174
|
-
* real defect (see `near-intents-route.ts`), fixed there by calling the
|
|
175
|
-
* RIGHT function (`providerById(id).label`) instead of this one — not by
|
|
176
|
-
* making this one less forgiving of input nobody validates before it arrives.
|
|
177
|
-
*
|
|
178
|
-
* `'PulseX'` IS RECOGNIZED HERE TOO, unlike an aggregator `ProviderId`, and for
|
|
179
|
-
* a different reason than the omnibridge case: it needs no registry lookup —
|
|
180
|
-
* there is exactly one PulseX, the label IS the key — so there is nothing an
|
|
181
|
-
* aggregator-style `providerById(id).label` call would add. A same-chain
|
|
182
|
-
* PulseX swap route (`RouteData.kind === 'swap'`) has NO omnibridge leg at
|
|
183
|
-
* all, so falling back to "Omnibridge" for it would name a bridge that never
|
|
184
|
-
* ran.
|
|
185
|
-
*
|
|
186
|
-
* @param provider - the raw provider key (e.g. `'pulsechain'`, `'PulseX'`)
|
|
187
|
-
* @returns the spelled-out provider name, or "Omnibridge" when unregistered
|
|
188
|
-
*/
|
|
189
33
|
export const providerDisplayName = (provider) => {
|
|
190
34
|
if (provider === 'PulseX')
|
|
191
35
|
return 'PulseX';
|
|
192
36
|
return isOmnibridgeProvider(provider) ? omnibridgeProviderDisplayName(provider) : 'Omnibridge';
|
|
193
37
|
};
|
|
194
|
-
/**
|
|
195
|
-
* The fully spelled-out display name of an omnibridge entry chain, or null when
|
|
196
|
-
* the chain id is null or is not one of the two entry points (Ethereum, Binance
|
|
197
|
-
* Smart Chain). A null result makes route builders return null, so an
|
|
198
|
-
* unrecognised destination leaves the strip empty rather than naming a chain it
|
|
199
|
-
* cannot bridge to.
|
|
200
|
-
*
|
|
201
|
-
* @param entryChainId - the decimal entry chain id, or null for direct routes
|
|
202
|
-
* @returns the spelled-out chain name, or null
|
|
203
|
-
*/
|
|
204
38
|
export const resolveEntryChainName = (entryChainId) => {
|
|
205
39
|
if (entryChainId === null)
|
|
206
40
|
return null;
|
|
207
41
|
return entryChainDisplayName[entryChainId] ?? null;
|
|
208
42
|
};
|
|
209
|
-
/**
|
|
210
|
-
* Parses one of the aggregator's own United States dollar figures —
|
|
211
|
-
* `fromAmountUsd` or `toAmountUsd` on a candidate's quote.
|
|
212
|
-
*
|
|
213
|
-
* `Number('')` IS `0`, NOT "UNKNOWN", and both the LI.FI and Relay quote
|
|
214
|
-
* parsers can hand this module a blank amount string. Read with a bare
|
|
215
|
-
* `Number()`, a blank `fromAmountUsd` priced a route's origin leg at zero
|
|
216
|
-
* cost — one measured case showed a route reporting a net gain of +$273 that
|
|
217
|
-
* had in fact cost real money, and led the strip on the strength of it — and
|
|
218
|
-
* a blank `toAmountUsd` would do the same to what a direct route (no
|
|
219
|
-
* omnibridge leg) reports as delivered. A dollar figure only means something
|
|
220
|
-
* when it is a positive, finite number: blank text, non-numeric text, zero,
|
|
221
|
-
* and a negative number are all treated as UNPRICED rather than free.
|
|
222
|
-
*
|
|
223
|
-
* @param raw - the provider's amount string, or null/undefined when absent
|
|
224
|
-
* @returns the parsed value, or null when the field cannot be trusted
|
|
225
|
-
*/
|
|
226
43
|
export const usdNumber = (raw) => {
|
|
227
44
|
if (raw == null || raw.trim() === '')
|
|
228
45
|
return null;
|
|
229
46
|
const value = Number(raw);
|
|
230
47
|
return Number.isFinite(value) && value > 0 ? value : null;
|
|
231
48
|
};
|
|
232
|
-
/**
|
|
233
|
-
* What the user pays for a candidate's origin leg, in United States dollars, as
|
|
234
|
-
* the provider itself valued it (`fromAmountUsd`). This is the candidate's SOURCE
|
|
235
|
-
* BASIS: every dollar figure the route reports is derived from it, so the costs
|
|
236
|
-
* shown and the value delivered can never drift onto different price sources.
|
|
237
|
-
*
|
|
238
|
-
* @param candidate - the priced route candidate
|
|
239
|
-
* @returns the amount paid in United States dollars, or null when the provider gave none
|
|
240
|
-
*/
|
|
241
|
-
/**
|
|
242
|
-
* Exported so `search/plan-route-data.ts` can generalize this SAME valuation
|
|
243
|
-
* to a general-search plan's first hop, per the route-search design's
|
|
244
|
-
* `RouteScoreInputs` doc ("import and reuse them; do not write a second
|
|
245
|
-
* valuation") — a candidate-shaped module boundary, not a second formula.
|
|
246
|
-
*/
|
|
247
49
|
export const candidatePaidUsd = (candidate) => usdNumber(candidate.quote.fromAmountUsd);
|
|
248
|
-
/**
|
|
249
|
-
* The cost of a candidate, in United States dollars, as the difference between
|
|
250
|
-
* what the user pays for the aggregator leg ({@link candidatePaidUsd}) and what the
|
|
251
|
-
* delivered tokens are worth. Returns null when the provider gave no
|
|
252
|
-
* `fromAmountUsd` or the delivered tokens are unpriced — the fee then shows only
|
|
253
|
-
* what it can. The figure is clamped at zero because a fleeting price-source
|
|
254
|
-
* skew should never render a negative fee.
|
|
255
|
-
*
|
|
256
|
-
* @param candidate - the priced route candidate
|
|
257
|
-
* @returns the fee in United States dollars, or null when either side is unpriced
|
|
258
|
-
*/
|
|
259
50
|
const candidateFeeUsd = (candidate) => {
|
|
260
51
|
const paid = candidatePaidUsd(candidate);
|
|
261
52
|
if (paid === null)
|
|
262
53
|
return null;
|
|
263
|
-
// Charges levied outside `fromAmount` are part of the cost even though the
|
|
264
|
-
// difference between paid and delivered cannot see them.
|
|
265
54
|
const outside = candidateOutsideFeesUsd(candidate);
|
|
266
55
|
const delivered = candidateDeliveredUsd(candidate);
|
|
267
56
|
if (delivered === null)
|
|
268
57
|
return null;
|
|
269
58
|
return Math.max(0, paid + outside - delivered);
|
|
270
59
|
};
|
|
271
|
-
/**
|
|
272
|
-
* What a candidate's delivered tokens are worth, in United States dollars.
|
|
273
|
-
*
|
|
274
|
-
* TWO SOURCES, AND WHICH ONE APPLIES IS DECIDED BY WHETHER A SECOND LEG
|
|
275
|
-
* FOLLOWS — never by which one happens to be present.
|
|
276
|
-
*
|
|
277
|
-
* A route with no omnibridge leg ends where the provider drops the funds, so
|
|
278
|
-
* the provider valued the very tokens the reader receives, in the same answer
|
|
279
|
-
* and on the same feed as `fromAmountUsd`. The difference between the two is
|
|
280
|
-
* then the route's cost and nothing else. This is the case that had no price at
|
|
281
|
-
* all: the destination chain is not one the bridge's own price sources cover,
|
|
282
|
-
* so `destinationUsdPrice` arrived null and every direct route reported no fee.
|
|
283
|
-
*
|
|
284
|
-
* A route that continues through the omnibridge must NOT use it. The provider's
|
|
285
|
-
* delivered side there is the entry chain's tokens, handed to the mediator —
|
|
286
|
-
* not what finally lands on PulseChain, which is what `deliveredAmount` counts.
|
|
287
|
-
* Those two differ by the omnibridge's own fee, so reading the provider's
|
|
288
|
-
* figure would report a crossing as free.
|
|
289
|
-
*
|
|
290
|
-
* Exported because the ranking must value a route on the SAME basis the route
|
|
291
|
-
* states. The ranker used to price the delivered tokens against
|
|
292
|
-
* `destinationUsdPrice` alone, which is null on every direct route, so every
|
|
293
|
-
* direct route was unpriced and the ranker fell back to an order that could not
|
|
294
|
-
* see what each route delivers.
|
|
295
|
-
*
|
|
296
|
-
* @param candidate - the priced route candidate
|
|
297
|
-
* @returns the delivered value in United States dollars, or null when unpriced
|
|
298
|
-
*/
|
|
299
60
|
export const candidateDeliveredUsd = (candidate) => {
|
|
300
61
|
if (candidate.omnibridgeProvider === null) {
|
|
301
62
|
const quoted = usdNumber(candidate.quote.toAmountUsd);
|
|
@@ -310,34 +71,7 @@ export const candidateDeliveredUsd = (candidate) => {
|
|
|
310
71
|
price: candidate.destinationUsdPrice,
|
|
311
72
|
});
|
|
312
73
|
};
|
|
313
|
-
/**
|
|
314
|
-
* The share of the requested amount below which an unspent remainder is treated
|
|
315
|
-
* as dust and not reported, in basis points (hundredths of a percent). Ten basis
|
|
316
|
-
* points is 0.1%.
|
|
317
|
-
*
|
|
318
|
-
* A backward-solved quote lands a handful of base units away from the amount it
|
|
319
|
-
* was asked for even when it consumes essentially all of it — the control case
|
|
320
|
-
* measured eleven wei left over on a five-hundredths-of-an-ether transfer. A
|
|
321
|
-
* remainder that small is rounding, not money the user gets to keep, and naming
|
|
322
|
-
* it would add a line to every route that says nothing.
|
|
323
|
-
*/
|
|
324
74
|
const UNSPENT_MATERIAL_BASIS_POINTS = 10n;
|
|
325
|
-
/**
|
|
326
|
-
* The origin tokens a candidate's quote does NOT consume — the difference between
|
|
327
|
-
* what the user typed and what the backward-solved quote turned out to need.
|
|
328
|
-
*
|
|
329
|
-
* Valued from the candidate's OWN source basis (`fromAmountUsd` divided across
|
|
330
|
-
* `fromAmount`) rather than from a separate price feed, for the same reason every
|
|
331
|
-
* other figure on the route is: two feeds disagree, and a remainder priced against
|
|
332
|
-
* a different one would reopen the gap this whole model exists to close.
|
|
333
|
-
*
|
|
334
|
-
* Returns null when the requested amount is unknown, when the quote consumed all
|
|
335
|
-
* of it, or when the remainder is below {@link UNSPENT_MATERIAL_BASIS_POINTS} —
|
|
336
|
-
* a route that spends what it was given says nothing about unspent tokens.
|
|
337
|
-
*
|
|
338
|
-
* @param candidate - the priced route candidate
|
|
339
|
-
* @returns the unspent remainder, or null when there is none worth naming
|
|
340
|
-
*/
|
|
341
75
|
const candidateUnspent = (candidate) => {
|
|
342
76
|
const requested = candidate.requestedAmount;
|
|
343
77
|
if (requested == null || requested <= 0n)
|
|
@@ -350,9 +84,6 @@ const candidateUnspent = (candidate) => {
|
|
|
350
84
|
return null;
|
|
351
85
|
}
|
|
352
86
|
const paidUsd = candidatePaidUsd(candidate);
|
|
353
|
-
// The remainder's value is the paid basis scaled by the ratio of the two
|
|
354
|
-
// amounts, which is exact regardless of the origin token's decimals and needs
|
|
355
|
-
// no per-token price at all.
|
|
356
87
|
const usdValue = paidUsd === null
|
|
357
88
|
? null
|
|
358
89
|
: (paidUsd * Number(formatUnits(remainder, candidate.originToken.decimals))) /
|
|
@@ -364,86 +95,22 @@ const candidateUnspent = (candidate) => {
|
|
|
364
95
|
usdValue: usdValue !== null && Number.isFinite(usdValue) ? usdValue : null,
|
|
365
96
|
};
|
|
366
97
|
};
|
|
367
|
-
/**
|
|
368
|
-
* A token address shortened for display, for example `0x1234…5678`. Preserves
|
|
369
|
-
* short addresses that are already readable without truncation.
|
|
370
|
-
*
|
|
371
|
-
* @param address - the full token address
|
|
372
|
-
* @returns the shortened address string
|
|
373
|
-
*/
|
|
374
98
|
const shortenAddress = (address) => address.length > 11 ? `${address.slice(0, 6)}…${address.slice(-4)}` : address;
|
|
375
|
-
/**
|
|
376
|
-
* The token label for a step endpoint: the provider-supplied SYMBOL when present
|
|
377
|
-
* (for example "USDC"), otherwise a SHORTENED address. Never the raw full address,
|
|
378
|
-
* which is unreadable in the hop conversion line.
|
|
379
|
-
*
|
|
380
|
-
* @param symbol - the token symbol, or null when not supplied by the provider
|
|
381
|
-
* @param address - the token's full on-chain address, used as fallback
|
|
382
|
-
* @returns the label to display for this endpoint
|
|
383
|
-
*/
|
|
384
99
|
const stepTokenLabel = (symbol, address) => symbol ?? shortenAddress(address);
|
|
385
|
-
/**
|
|
386
|
-
* The swap conversion label for a normalized aggregator route step — the
|
|
387
|
-
* "from token → to token" string shown in the hop's conversion line. Returns null
|
|
388
|
-
* when the step carries no token addresses (Relay steps are empty; the hop then
|
|
389
|
-
* shows no conversion clause).
|
|
390
|
-
*
|
|
391
|
-
* @param step - one normalized aggregator route step
|
|
392
|
-
* @returns the conversion string, or null when address data is absent
|
|
393
|
-
*/
|
|
394
100
|
const stepConversion = (step) => {
|
|
395
101
|
if (!step.fromToken || !step.toToken)
|
|
396
102
|
return null;
|
|
397
103
|
return `${stepTokenLabel(step.fromTokenSymbol, step.fromToken)} → ${stepTokenLabel(step.toTokenSymbol, step.toToken)}`;
|
|
398
104
|
};
|
|
399
|
-
/**
|
|
400
|
-
* A fee rate as percent text: 3e15 reads "0.3%". Exact, because it scales the
|
|
401
|
-
* integer and lets `formatUnits` place the point, rather than going through a
|
|
402
|
-
* floating point number that prints 2.9699999999999998.
|
|
403
|
-
*/
|
|
404
105
|
const rateAsPercentText = (rate) => `${formatUnits(rate * 100n, 18)}%`;
|
|
405
|
-
/** A label, with the rate beside it when the rate is known. */
|
|
406
106
|
const labelWithRate = (label, rate) => rate == null ? label : `${label} (${rateAsPercentText(rate)})`;
|
|
407
|
-
/** A dollar value as the cent string a {@link QuoteFee} carries, or null when unpriced. */
|
|
408
107
|
const feeAmountUsdText = (value) => value === null ? null : roundUsdToCents(value).toFixed(2);
|
|
409
|
-
/**
|
|
410
|
-
* The token the bridge takes its own fee in.
|
|
411
|
-
*
|
|
412
|
-
* The fee manager lives on the home mediator, on PulseChain, and the pathway
|
|
413
|
-
* says which side of the crossing that is. Leaving PulseChain it is `'from'`:
|
|
414
|
-
* the home mediator takes the fee from the tokens it receives, before it sends
|
|
415
|
-
* the message. Arriving it is `'to'`: the home mediator takes the fee from the
|
|
416
|
-
* tokens it releases. So the fee is always in the PulseChain side's token.
|
|
417
|
-
*
|
|
418
|
-
* @returns the fee's token, or null when the bridge key names no pathway
|
|
419
|
-
*/
|
|
420
108
|
const bridgeFeeToken = ({ bridgeKey, sourceToken, arrivingToken, }) => {
|
|
421
|
-
// Not production-only: the testnet pathways live in a second map, and a
|
|
422
|
-
// mainnet key finds its own pathway in the first map either way.
|
|
423
109
|
const crossing = pathway(bridgeKey, false);
|
|
424
110
|
if (!crossing)
|
|
425
111
|
return null;
|
|
426
112
|
return crossing.feeManager === 'from' ? sourceToken : arrivingToken;
|
|
427
113
|
};
|
|
428
|
-
/**
|
|
429
|
-
* The cost of one omnibridge crossing: the bridge's own fee plus the Gibs
|
|
430
|
-
* delivery fee, both valued on the route's source basis.
|
|
431
|
-
*
|
|
432
|
-
* A PARTIAL SUM IS NEVER A COST. When either fee is unknown the figure is null,
|
|
433
|
-
* because the known half alone would read as the whole cost and understate it.
|
|
434
|
-
* A delivery the reader did not ask for is not unknown: it is a real zero.
|
|
435
|
-
*
|
|
436
|
-
* The delivery fee is in the ARRIVING token: the destination router takes it
|
|
437
|
-
* from the value that reaches it.
|
|
438
|
-
*
|
|
439
|
-
* EVERY AMOUNT IS COUNTED IN SOURCE BASE UNITS, because the settings cascade
|
|
440
|
-
* derives both fees from `amountToBridge`, which is. So both are valued with the
|
|
441
|
-
* source token's decimals and price, the same basis the route's delivered value
|
|
442
|
-
* uses. The bridge keeps a token's decimals when it creates the counterpart, so
|
|
443
|
-
* that count is also the count in the arriving token each charge names.
|
|
444
|
-
*
|
|
445
|
-
* @returns the crossing's cost and the charges behind it
|
|
446
|
-
*/
|
|
447
114
|
const crossingCost = ({ bridgeKey, feeRoots, sourceToken, arrivingToken, sourceUsdPrice, }) => {
|
|
448
115
|
const [provider] = bridgeKey;
|
|
449
116
|
const { bridgeFeeAmount, bridgeFeeRate, deliveryFee } = feeRoots;
|
|
@@ -487,25 +154,11 @@ const crossingCost = ({ bridgeKey, feeRoots, sourceToken, arrivingToken, sourceU
|
|
|
487
154
|
: bridgeFeeUsdValue + deliveryFeeUsdValue;
|
|
488
155
|
return { feeUsdValue, fees: [...bridgeFees, ...deliveryFees] };
|
|
489
156
|
};
|
|
490
|
-
/** A token as the fee token a {@link QuoteFee} names, on the chain it is presented on. */
|
|
491
157
|
const toFeeToken = (token, chainId) => ({
|
|
492
158
|
symbol: token.symbol,
|
|
493
159
|
decimals: token.decimals,
|
|
494
160
|
chainId,
|
|
495
161
|
});
|
|
496
|
-
/**
|
|
497
|
-
* The raw token reference for one end of a normalized aggregator step — the shape
|
|
498
|
-
* an intermediate stop is rendered from.
|
|
499
|
-
*
|
|
500
|
-
* The provider's own token image wins when present. When it supplies none, the
|
|
501
|
-
* logo is left UNSET for a `0x` address so the adapter derives it from the image
|
|
502
|
-
* service, and set to null for a non-Ethereum address (Bitcoin, Solana, Tron, The
|
|
503
|
-
* Open Network), which the image service cannot key by a checksummed address.
|
|
504
|
-
* Decimals are omitted: an intermediate stop formats no amount.
|
|
505
|
-
*
|
|
506
|
-
* @param endpoint - one end of a normalized step
|
|
507
|
-
* @returns the raw token reference for that end
|
|
508
|
-
*/
|
|
509
162
|
const stepTokenRef = (endpoint) => ({
|
|
510
163
|
symbol: endpoint.symbol ?? shortenAddress(endpoint.address),
|
|
511
164
|
address: endpoint.address,
|
|
@@ -513,25 +166,18 @@ const stepTokenRef = (endpoint) => ({
|
|
|
513
166
|
logoUri: endpoint.logoUri ?? (endpoint.address.startsWith('0x') ? undefined : null),
|
|
514
167
|
chainLogoUri: imageLinks.networkOrPseudo(endpoint.chainId),
|
|
515
168
|
});
|
|
516
|
-
/** The token a step consumes, as a raw reference. */
|
|
517
169
|
const stepFromTokenRef = (step) => stepTokenRef({
|
|
518
170
|
address: step.fromToken,
|
|
519
171
|
symbol: step.fromTokenSymbol,
|
|
520
172
|
chainId: step.fromChainId,
|
|
521
173
|
logoUri: step.fromTokenLogoUri,
|
|
522
174
|
});
|
|
523
|
-
/** The token a step produces, as a raw reference — the stop it lands the funds on. */
|
|
524
175
|
const stepToTokenRef = (step) => stepTokenRef({
|
|
525
176
|
address: step.toToken,
|
|
526
177
|
symbol: step.toTokenSymbol,
|
|
527
178
|
chainId: step.toChainId,
|
|
528
179
|
logoUri: step.toTokenLogoUri,
|
|
529
180
|
});
|
|
530
|
-
/**
|
|
531
|
-
* True when two references name the same token on the same chain. Hex addresses
|
|
532
|
-
* compare case-insensitively (providers disagree on checksum casing); anything
|
|
533
|
-
* else compares exactly, because a base58 address is genuinely case-sensitive.
|
|
534
|
-
*/
|
|
535
181
|
const isSameTokenRef = (left, right) => {
|
|
536
182
|
if (left.chainId !== right.chainId)
|
|
537
183
|
return false;
|
|
@@ -541,20 +187,6 @@ const isSameTokenRef = (left, right) => {
|
|
|
541
187
|
return false;
|
|
542
188
|
return left.address.toLowerCase() === right.address.toLowerCase();
|
|
543
189
|
};
|
|
544
|
-
/**
|
|
545
|
-
* The token flow a set of hops walks: the origin, then the token each hop
|
|
546
|
-
* PRODUCES. Every entry except the first is a stop the funds actually reach, and
|
|
547
|
-
* the last hop's output is the destination — so the intermediate stops are exactly
|
|
548
|
-
* the hops minus the last one.
|
|
549
|
-
*
|
|
550
|
-
* A hop that names no output token contributes nothing (the flow still reaches the
|
|
551
|
-
* destination through the hops that do), and a hop that produces the token the
|
|
552
|
-
* flow is already holding is collapsed rather than drawn twice.
|
|
553
|
-
*
|
|
554
|
-
* @param origin - the token the route starts from
|
|
555
|
-
* @param hops - the hops the signature performs, first to last
|
|
556
|
-
* @returns the ordered token flow, origin first and destination last
|
|
557
|
-
*/
|
|
558
190
|
const deriveTokenFlow = (origin, hops) => hops.reduce((flow, hop) => {
|
|
559
191
|
const produced = hop.toToken;
|
|
560
192
|
if (!produced)
|
|
@@ -564,24 +196,6 @@ const deriveTokenFlow = (origin, hops) => hops.reduce((flow, hop) => {
|
|
|
564
196
|
return flow;
|
|
565
197
|
return [...flow, produced];
|
|
566
198
|
}, [origin]);
|
|
567
|
-
/**
|
|
568
|
-
* Everything a candidate is charged that sits OUTSIDE `fromAmount`, in United
|
|
569
|
-
* States dollars.
|
|
570
|
-
*
|
|
571
|
-
* THIS REPLACES A PER-STEP GAS SUM that could only ever see part of the picture.
|
|
572
|
-
* That sum read `step.gasUsd`, so it was ZERO for every Relay quote — Relay
|
|
573
|
-
* publishes no per-leg breakdown — and for LI.FI it counted the network gas
|
|
574
|
-
* while missing the larger charge: a LayerZero fee of about 1.37 dollars marked
|
|
575
|
-
* `included: false`, paid on top in native currency. Reading the quote's own
|
|
576
|
-
* normalized fee list catches both, and cannot double-count the per-step gas
|
|
577
|
-
* against the aggregate the provider reports.
|
|
578
|
-
*
|
|
579
|
-
* A charge whose dollar value the provider did not give is skipped rather than
|
|
580
|
-
* guessed. It is still named in the breakdown the reader can open.
|
|
581
|
-
*
|
|
582
|
-
* Exported for the same reason {@link candidatePaidUsd} is — `search/plan-route-data.ts`
|
|
583
|
-
* reuses this exact valuation rather than restating it for a plan's last hop.
|
|
584
|
-
*/
|
|
585
199
|
export const candidateOutsideFeesUsd = (candidate) => {
|
|
586
200
|
const fromQuote = candidate.quote.fees.reduce((total, fee) => {
|
|
587
201
|
if (fee.includedInFromAmount) {
|
|
@@ -590,36 +204,12 @@ export const candidateOutsideFeesUsd = (candidate) => {
|
|
|
590
204
|
const value = Number(fee.amountUsd);
|
|
591
205
|
return Number.isFinite(value) ? total + value : total;
|
|
592
206
|
}, 0);
|
|
593
|
-
// A provider that reports its network cost once, for the whole route, gives a
|
|
594
|
-
// gas row here and that row is the whole of it. One that reports gas only per
|
|
595
|
-
// leg gives no such row, and the per-step figures are then the only record of
|
|
596
|
-
// it — counting them as well when the row IS present would bill the same gas
|
|
597
|
-
// twice, so this is a fallback and never an addition.
|
|
598
207
|
const reportsGasOnce = candidate.quote.fees.some((fee) => !fee.includedInFromAmount && fee.kind === 'gas');
|
|
599
208
|
if (reportsGasOnce) {
|
|
600
209
|
return fromQuote;
|
|
601
210
|
}
|
|
602
211
|
return fromQuote + candidate.quote.steps.reduce((total, step) => total + (step.gasUsd ?? 0), 0);
|
|
603
212
|
};
|
|
604
|
-
/**
|
|
605
|
-
* The user's NET dollar outcome for a candidate: the value delivered minus
|
|
606
|
-
* everything paid to reach it. Three cost components:
|
|
607
|
-
*
|
|
608
|
-
* - the origin tokens spent (`quote.fromAmountUsd`);
|
|
609
|
-
* - every charge paid on top ({@link candidateOutsideFeesUsd}), NOT part of
|
|
610
|
-
* `fromAmount` — the sender's own network gas, and any provider fee the
|
|
611
|
-
* quote marked as charged outside the amount;
|
|
612
|
-
* - the omnibridge CROSSING fee, already netted into `deliveredAmount`.
|
|
613
|
-
*
|
|
614
|
-
* The delivered value comes from {@link candidateDeliveredUsd}, the same figure
|
|
615
|
-
* the route's own dollar value and cost are built from. This is the one ranking
|
|
616
|
-
* rule: the route strip orders by it, so the route the reader sees costing least
|
|
617
|
-
* is the route that leads.
|
|
618
|
-
*
|
|
619
|
-
* @param candidate - the priced route candidate
|
|
620
|
-
* @returns the net outcome in United States dollars, or null when the leg or
|
|
621
|
-
* the delivered tokens are unpriced
|
|
622
|
-
*/
|
|
623
213
|
export const candidateNetValueUsd = (candidate) => {
|
|
624
214
|
const paid = candidatePaidUsd(candidate);
|
|
625
215
|
if (paid === null)
|
|
@@ -629,41 +219,8 @@ export const candidateNetValueUsd = (candidate) => {
|
|
|
629
219
|
return null;
|
|
630
220
|
return delivered - paid - candidateOutsideFeesUsd(candidate);
|
|
631
221
|
};
|
|
632
|
-
/**
|
|
633
|
-
* Builds the RAW, data-only {@link RouteData} for one priced aggregator candidate
|
|
634
|
-
* — the framework-agnostic half of the candidate path, carrying NUMBERS, raw
|
|
635
|
-
* token references, and raw provider keys ONLY. The display wrapper runs this
|
|
636
|
-
* through `toRouteView` to resolve every logo, name, formatted string, and the
|
|
637
|
-
* structural id.
|
|
638
|
-
*
|
|
639
|
-
* The candidate is a single signature (the aggregator swap auto-calls the
|
|
640
|
-
* omnibridge), so it produces exactly one {@link RouteSectionData} whose hops are
|
|
641
|
-
* the aggregator legs the provider reported (Relay reports none) followed by the
|
|
642
|
-
* omnibridge crossing leg. Each hop carries its OWN numeric fee so the user sees
|
|
643
|
-
* where cost accrues.
|
|
644
|
-
*
|
|
645
|
-
* @param candidate - one priced route candidate
|
|
646
|
-
* @returns the raw, data-only route for the candidate
|
|
647
|
-
*/
|
|
648
222
|
export const buildCandidateRouteData = (candidate) => {
|
|
649
223
|
const feeUsdValue = candidateFeeUsd(candidate);
|
|
650
|
-
// THE PROVIDER'S OWN FIGURE, IN ITS OWN UNIT, CARRIED THROUGH UNROUNDED.
|
|
651
|
-
//
|
|
652
|
-
// This used to keep only `Math.ceil(seconds / 60)`, and that one expression
|
|
653
|
-
// erased the difference the reader most needed to see. Measured live on Base
|
|
654
|
-
// to Polygon: LI.FI claimed 29 seconds and Relay claimed 3, and both rendered
|
|
655
|
-
// "~1 min". A reader comparing them had no way to tell a route that settles
|
|
656
|
-
// before they look away from one that claims ten times the wait — so the
|
|
657
|
-
// strip handed the choice to a 0.05% difference in delivered tokens, which is
|
|
658
|
-
// nine cents on a hundred-and-twenty-six-dollar transfer.
|
|
659
|
-
//
|
|
660
|
-
// The section therefore carries the seconds and the view picks the unit (see
|
|
661
|
-
// `sectionWaitLabel` in the route strip). `etaMinutes` stays beside it for the
|
|
662
|
-
// route-level field and for every consumer that still reads minutes.
|
|
663
|
-
//
|
|
664
|
-
// It is the provider's CLAIM and nothing more. One live route ran about
|
|
665
|
-
// seventy-five times its own estimate, which is why the strip labels this the
|
|
666
|
-
// provider's estimate rather than presenting it as a fact.
|
|
667
224
|
const etaSeconds = candidate.quote.executionDurationSeconds > 0 ? candidate.quote.executionDurationSeconds : null;
|
|
668
225
|
const etaMinutes = candidate.quote.executionDurationSeconds > 0
|
|
669
226
|
? Math.ceil(candidate.quote.executionDurationSeconds / 60)
|
|
@@ -674,79 +231,38 @@ export const buildCandidateRouteData = (candidate) => {
|
|
|
674
231
|
` entryChainId=${candidate.entryChainId}` +
|
|
675
232
|
` omnibridgeProvider=${candidate.omnibridgeProvider}` +
|
|
676
233
|
` quote.fromAmountUsd=${candidate.quote.fromAmountUsd}` +
|
|
677
|
-
// BOTH delivered-value sources, because which one applies depends on
|
|
678
|
-
// whether a second leg follows — naming only one sends whoever reads
|
|
679
|
-
// this to the wrong place. See `candidateDeliveredUsd`.
|
|
680
234
|
` quote.toAmountUsd=${candidate.quote.toAmountUsd ?? null}` +
|
|
681
235
|
` destinationUsdPrice=${candidate.destinationUsdPrice}`);
|
|
682
236
|
}
|
|
683
237
|
const { originToken, destinationToken } = candidate;
|
|
684
|
-
// Token logos are pre-resolved here via the image service (which lives in the
|
|
685
|
-
// software development kit, so the producer stays framework-agnostic), passing
|
|
686
|
-
// the FULL token so its own `logoURI` is preferred over the address-derived
|
|
687
|
-
// fallback — matching the pre-split `buildCandidateTokenFlow` exactly.
|
|
688
238
|
const originNode = {
|
|
689
239
|
symbol: originToken.symbol,
|
|
690
240
|
address: originToken.address,
|
|
691
241
|
chainId: originToken.chainId,
|
|
692
242
|
decimals: originToken.decimals,
|
|
693
|
-
// `tokenImageOrChainFallback` — see the identical note on `destinationNode`
|
|
694
|
-
// below; the same "address not resolvable yet" case applies to either end.
|
|
695
243
|
logoUri: imageLinks.tokenImageOrChainFallback(originToken),
|
|
696
|
-
// A pay-by-hand origin (Zcash) carries a local, invented chain id
|
|
697
|
-
// `imageLinks.network` cannot look up by number — see `networkOrPseudo`.
|
|
698
244
|
chainLogoUri: imageLinks.networkOrPseudo(originToken.chainId),
|
|
699
245
|
};
|
|
700
|
-
// Read up front, before the node below, so the node itself never claims
|
|
701
|
-
// PulseChain for a candidate that does not enter the omnibridge. A candidate
|
|
702
|
-
// whose omnibridge leg is absent (NEAR Intents, or any aggregator delivering
|
|
703
|
-
// straight to its own destination chain) lands on `destinationToken.chainId`
|
|
704
|
-
// — Base, in the case that pinned this — and a node hardcoded to PulseChain
|
|
705
|
-
// named the wrong chain for every stop that reused it, including the
|
|
706
|
-
// token-flow fallback below.
|
|
707
246
|
const omnibridgeProviderKey = candidate.omnibridgeProvider;
|
|
708
|
-
// THE CHAIN THIS SIGNATURE ACTUALLY ENDS ON. The label was the constant
|
|
709
|
-
// 'Bridge to PulseChain', true of every route that existed when it was
|
|
710
|
-
// written. A route the omnibridge does not carry ends somewhere else
|
|
711
|
-
// entirely, and titled that way the tracker named a chain the funds never
|
|
712
|
-
// touch — over a transfer to Base that had already landed.
|
|
713
247
|
const landingChainId = omnibridgeProviderKey === null ? destinationToken.chainId : pulsechainChainId;
|
|
714
248
|
const destinationNode = {
|
|
715
249
|
symbol: destinationToken.symbol,
|
|
716
250
|
address: destinationToken.address,
|
|
717
251
|
chainId: landingChainId,
|
|
718
252
|
decimals: destinationToken.decimals,
|
|
719
|
-
// `tokenImageOrChainFallback`, NOT `image` — a not-yet-bridged token's
|
|
720
|
-
// address can still be unresolved when this producer draws the DELIVERED
|
|
721
|
-
// node (see the repository's own "Not-Yet-Bridged Tokens" gotcha), and
|
|
722
|
-
// `image` either throws on it or silently draws nothing. See that
|
|
723
|
-
// helper's own note.
|
|
724
253
|
logoUri: imageLinks.tokenImageOrChainFallback(destinationToken),
|
|
725
|
-
// `networkOrPseudo`, matching `originNode` — a destination is not known to
|
|
726
|
-
// ever be a deposit-address pseudo chain today, but the badge helper is the
|
|
727
|
-
// one every other node in this builder goes through, and there is no reason
|
|
728
|
-
// this one should be the exception that 404s if that ever changes.
|
|
729
254
|
chainLogoUri: imageLinks.networkOrPseudo(landingChainId),
|
|
730
255
|
};
|
|
731
256
|
const aggregatorHops = candidate.quote.steps.map((step, index) => ({
|
|
732
257
|
providerName: step.label,
|
|
733
|
-
// The aggregator step carries its tool's real logo url straight from the quote
|
|
734
|
-
// — raw data, not a derived view concern, so it rides on the raw hop.
|
|
735
258
|
providerLogoUri: step.providerLogoUri,
|
|
736
259
|
title: step.label,
|
|
737
260
|
conversion: stepConversion(step),
|
|
738
|
-
// The tokens this leg moves between, structurally — the section's token flow is
|
|
739
|
-
// derived from these, so every stop the funds make is one the provider named.
|
|
740
261
|
fromToken: stepFromTokenRef(step),
|
|
741
262
|
toToken: stepToTokenRef(step),
|
|
742
263
|
feeUsdValue: step.feeUsd,
|
|
743
|
-
// The first hop is the user's single signature; later hops run inside it.
|
|
744
264
|
automatic: index > 0,
|
|
745
265
|
}));
|
|
746
|
-
// NO BRIDGE LEG MEANS NO BRIDGE HOP. A candidate the omnibridge does not
|
|
747
|
-
// carry ends where the aggregator leaves it, so the crossing hop below simply
|
|
748
|
-
// does not exist — appending one would draw a step nobody performs and name a
|
|
749
|
-
// bridge that is not involved.
|
|
750
266
|
const omnibridgeHop = omnibridgeProviderKey === null
|
|
751
267
|
? null
|
|
752
268
|
: {
|
|
@@ -754,8 +270,6 @@ export const buildCandidateRouteData = (candidate) => {
|
|
|
754
270
|
providerKey: omnibridgeProviderKey,
|
|
755
271
|
title: 'Bridge to PulseChain',
|
|
756
272
|
conversion: `→ ${candidate.destinationToken.symbol}`,
|
|
757
|
-
// The crossing consumes whatever the last aggregator leg produced — the anchor
|
|
758
|
-
// token on the entry chain — or the origin token when there were no legs.
|
|
759
273
|
fromToken: aggregatorHops[aggregatorHops.length - 1]?.toToken ?? originNode,
|
|
760
274
|
toToken: destinationNode,
|
|
761
275
|
feeUsdValue: usdValueOfAmount({
|
|
@@ -763,38 +277,17 @@ export const buildCandidateRouteData = (candidate) => {
|
|
|
763
277
|
decimals: candidate.destinationToken.decimals,
|
|
764
278
|
price: candidate.destinationUsdPrice,
|
|
765
279
|
}),
|
|
766
|
-
// The omnibridge call is always auto-called by the aggregator leg before it.
|
|
767
280
|
automatic: aggregatorHops.length > 0,
|
|
768
281
|
};
|
|
769
282
|
const hops = omnibridgeHop === null ? aggregatorHops : [...aggregatorHops, omnibridgeHop];
|
|
770
|
-
// Origin, then every stop the funds reach on the way — NOT a collapsed pair. The
|
|
771
|
-
// last hop is the crossing, so the flow still ends on the destination, EXCEPT
|
|
772
|
-
// when the provider itemised no legs at all (Relay and NEAR Intents both report
|
|
773
|
-
// zero `quote.steps`) and there is no omnibridge hop to supply one either — a
|
|
774
|
-
// candidate delivering straight to its own destination chain. `deriveTokenFlow`
|
|
775
|
-
// then has nothing to reduce over and stops at the origin, which is the exact
|
|
776
|
-
// defect that showed a NEAR Intents route to Base USD Coin as "At ETH on
|
|
777
|
-
// Ethereum": the pill drew the one-and-only flow node it was given, and that
|
|
778
|
-
// node had never left the origin. The route's own `destinationToken` is known
|
|
779
|
-
// regardless of what got itemised, so it is appended here whenever the flow
|
|
780
|
-
// does not already end on it — deduplicated, so an itemised leg that DID reach
|
|
781
|
-
// the destination is never drawn twice.
|
|
782
283
|
const rawTokenFlow = deriveTokenFlow(originNode, hops);
|
|
783
284
|
const lastStop = rawTokenFlow[rawTokenFlow.length - 1];
|
|
784
285
|
const tokenFlow = lastStop && isSameTokenRef(lastStop, destinationNode) ? rawTokenFlow : [...rawTokenFlow, destinationNode];
|
|
785
286
|
const section = {
|
|
786
|
-
// The section id stays a stable string key derived from the candidate id —
|
|
787
|
-
// section ids are within-route keys and do not need to be structural hashes.
|
|
788
287
|
id: `${candidate.id}-signature-1`,
|
|
789
288
|
label: `Bridge to ${chainDisplayName(landingChainId)}`,
|
|
790
|
-
// The signature is executed by the cross-chain AGGREGATOR (LI.FI / Relay) that
|
|
791
|
-
// carries the bridge leg — that is the provider to surface on this section, not
|
|
792
|
-
// the omnibridge headline.
|
|
793
289
|
providerKey: candidate.quote.provider,
|
|
794
290
|
tokenFlow,
|
|
795
|
-
// What this signature leaves the user holding on PulseChain. Valued on the
|
|
796
|
-
// same source basis as the route's output so the step's landing figure and
|
|
797
|
-
// the route's delivered figure are the same number, not two derivations of it.
|
|
798
291
|
landing: {
|
|
799
292
|
amount: candidate.deliveredAmount,
|
|
800
293
|
decimals: candidate.destinationToken.decimals,
|
|
@@ -803,8 +296,6 @@ export const buildCandidateRouteData = (candidate) => {
|
|
|
803
296
|
},
|
|
804
297
|
unspent: candidateUnspent(candidate),
|
|
805
298
|
feeUsdValue,
|
|
806
|
-
// The itemization behind `feeUsdValue`, plus the charges that difference
|
|
807
|
-
// cannot see: anything the provider levies outside `fromAmount`.
|
|
808
299
|
fees: candidate.quote.fees,
|
|
809
300
|
etaMinutes,
|
|
810
301
|
etaSeconds,
|
|
@@ -812,26 +303,13 @@ export const buildCandidateRouteData = (candidate) => {
|
|
|
812
303
|
};
|
|
813
304
|
return {
|
|
814
305
|
kind: 'aggregated',
|
|
815
|
-
// The entry chain this aggregator route crosses through (Ethereum or Binance
|
|
816
|
-
// Smart Chain), carried explicitly so Bridge.tsx can link the selected route
|
|
817
|
-
// back to its quoted entry candidate without string-parsing the id.
|
|
818
306
|
entryChainId: candidate.entryChainId,
|
|
819
|
-
// The route's headline provider is the omnibridge it crosses on — or, when
|
|
820
|
-
// it crosses none, the aggregator that carries the whole journey. A route
|
|
821
|
-
// has to be named after something that actually performs it.
|
|
822
307
|
headlineProviderKey: omnibridgeProviderKey ?? candidate.quote.provider,
|
|
823
308
|
sections: [section],
|
|
824
309
|
output: {
|
|
825
310
|
amount: candidate.deliveredAmount,
|
|
826
311
|
decimals: candidate.destinationToken.decimals,
|
|
827
312
|
symbol: candidate.destinationToken.symbol,
|
|
828
|
-
// Valued on the SOURCE basis — what was paid, less this route's cost — NOT by
|
|
829
|
-
// re-pricing the delivered token against its own feed. The two differ whenever
|
|
830
|
-
// the destination asset trades away from its origin-chain counterpart, and that
|
|
831
|
-
// difference used to land silently in this figure: the page then stated the
|
|
832
|
-
// route's cost twice and the two never agreed. Deriving it keeps the fee clamp
|
|
833
|
-
// above from opening a gap too. Falls back to the destination feed only when
|
|
834
|
-
// the provider gave no `fromAmountUsd`, so an unpriced leg still shows what it can.
|
|
835
313
|
usdValue: subtractUsdValues(candidatePaidUsd(candidate), feeUsdValue) ??
|
|
836
314
|
usdValueOfAmount({
|
|
837
315
|
balance: candidate.deliveredAmount,
|
|
@@ -839,36 +317,16 @@ export const buildCandidateRouteData = (candidate) => {
|
|
|
839
317
|
price: candidate.destinationUsdPrice,
|
|
840
318
|
}),
|
|
841
319
|
},
|
|
842
|
-
// The aggregate cost is the single section's spread (this route is one
|
|
843
|
-
// signature), so it equals the section fee; null when the leg is unpriced.
|
|
844
320
|
totalCostUsdValue: feeUsdValue,
|
|
845
|
-
// The net dollar outcome this route is ranked on (delivered value minus origin
|
|
846
|
-
// tokens paid, network gas, and the omnibridge crossing fee).
|
|
847
321
|
netValueUsd: candidateNetValueUsd(candidate),
|
|
848
322
|
etaMinutes,
|
|
849
323
|
};
|
|
850
324
|
};
|
|
851
|
-
// ---------------------------------------------------------------------------
|
|
852
|
-
// Producer: direct route
|
|
853
|
-
// ---------------------------------------------------------------------------
|
|
854
|
-
/**
|
|
855
|
-
* Builds the ALWAYS-PRESENT direct omnibridge route from the current outbound
|
|
856
|
-
* settings, so the strip is never empty for a resolvable pathway.
|
|
857
|
-
*
|
|
858
|
-
* The direct route is a SINGLE SIGNATURE — the user signs the omnibridge call
|
|
859
|
-
* once — so it is exactly ONE {@link RouteSectionData}. Returns null only when
|
|
860
|
-
* the pathway is NOT resolvable (a missing input or output asset).
|
|
861
|
-
*
|
|
862
|
-
* @param roots - the resolved settings + price + logo roots (see {@link DirectRouteRoots})
|
|
863
|
-
* @returns the single direct {@link RouteData}, or null when unresolvable
|
|
864
|
-
*/
|
|
865
325
|
export const buildDirectRouteData = (roots) => {
|
|
866
326
|
const { bridgeKey, assetIn, assetOut, estimatedAmountOut, assetInUsdPrice, assetOutUsdPrice, assetInLogoUri, assetOutLogoUri, fromChainLogoUri, toChainLogoUri, advanced, } = roots;
|
|
867
327
|
if (!assetIn || !assetOut)
|
|
868
328
|
return null;
|
|
869
329
|
const [provider, fromChain, toChain] = bridgeKey;
|
|
870
|
-
// The bridge's own fee plus the delivery fee, valued on the source basis; null
|
|
871
|
-
// when either is unknown or the input asset is unpriced.
|
|
872
330
|
const { feeUsdValue, fees } = crossingCost({
|
|
873
331
|
bridgeKey,
|
|
874
332
|
feeRoots: roots,
|
|
@@ -878,30 +336,15 @@ export const buildDirectRouteData = (roots) => {
|
|
|
878
336
|
});
|
|
879
337
|
const hop = {
|
|
880
338
|
providerName: omnibridgeProviderDisplayName(provider),
|
|
881
|
-
// The direct route's provider is the omnibridge it crosses on; the adapter
|
|
882
|
-
// resolves its logo from this key.
|
|
883
339
|
providerKey: provider,
|
|
884
340
|
title: omnibridgeProviderDisplayName(provider),
|
|
885
341
|
conversion: `${assetIn.symbol} → ${assetOut.symbol}`,
|
|
886
342
|
feeUsdValue,
|
|
887
|
-
// Delivery auto-calls the completion on the PulseChain side, so the user's
|
|
888
|
-
// single signature covers the whole leg.
|
|
889
343
|
automatic: true,
|
|
890
344
|
};
|
|
891
345
|
const section = {
|
|
892
346
|
id: `direct-${provider}-${fromChain}-signature-1`,
|
|
893
|
-
// NAMED FROM `toChain`, NOT HARD-CODED. A direct crossing runs both ways —
|
|
894
|
-
// a foreign chain into PulseChain, or PulseChain out to a foreign chain —
|
|
895
|
-
// and a literal "Bridge to PulseChain" on every direct route named the
|
|
896
|
-
// wrong destination on every outbound crossing: the label read "Bridge to
|
|
897
|
-
// PulseChain" while the arrival stage it produces (derived correctly, from
|
|
898
|
-
// the section's own last token-flow node — see `tracker/trackerView.ts`'s
|
|
899
|
-
// `sectionArrivalStage`) correctly read "Arrive on Ethereum" for the exact
|
|
900
|
-
// same signature. One route, two destinations stated for it.
|
|
901
347
|
label: `Bridge to ${chainDisplayName(Number(toChain))}`,
|
|
902
|
-
// The direct section carries NO section provider (matching the pre-split
|
|
903
|
-
// builder): the route strip falls back to the hop's provider for the badge,
|
|
904
|
-
// and the structural id stays identical.
|
|
905
348
|
providerKey: null,
|
|
906
349
|
tokenFlow: [
|
|
907
350
|
{
|
|
@@ -921,9 +364,6 @@ export const buildDirectRouteData = (roots) => {
|
|
|
921
364
|
chainLogoUri: toChainLogoUri,
|
|
922
365
|
},
|
|
923
366
|
],
|
|
924
|
-
// The direct crossing IS the whole route, so what this step lands in is what
|
|
925
|
-
// the route delivers — priced on the source basis for the same reason the
|
|
926
|
-
// output is (see the output note below).
|
|
927
367
|
landing: {
|
|
928
368
|
amount: estimatedAmountOut,
|
|
929
369
|
decimals: assetOut.decimals,
|
|
@@ -935,22 +375,13 @@ export const buildDirectRouteData = (roots) => {
|
|
|
935
375
|
}),
|
|
936
376
|
},
|
|
937
377
|
feeUsdValue,
|
|
938
|
-
// Who takes what: the bridge, then the delivery service.
|
|
939
378
|
fees,
|
|
940
|
-
// The direct crossing waits on the SAME omnibridge settlement as the
|
|
941
|
-
// outbound route's own "Bridge to {entry chain}" section
|
|
942
|
-
// ({@link buildOutboundRouteData}), which quotes this identical constant for
|
|
943
|
-
// the identical wait. Leaving it null here was the one crossing section that
|
|
944
|
-
// never got it, so the strip and the tracker both had no duration to show for
|
|
945
|
-
// the most common route in the product.
|
|
946
379
|
etaMinutes: BRIDGE_SETTLEMENT_ETA_MINUTES,
|
|
947
380
|
hops: [hop],
|
|
948
381
|
advanced,
|
|
949
382
|
};
|
|
950
383
|
return {
|
|
951
384
|
kind: 'direct',
|
|
952
|
-
// The direct omnibridge route IS the crossing — it does not pivot through a
|
|
953
|
-
// distinct aggregator entry chain — so `entryChainId` is null.
|
|
954
385
|
entryChainId: null,
|
|
955
386
|
headlineProviderKey: provider,
|
|
956
387
|
sections: [section],
|
|
@@ -958,65 +389,18 @@ export const buildDirectRouteData = (roots) => {
|
|
|
958
389
|
amount: estimatedAmountOut,
|
|
959
390
|
decimals: assetOut.decimals,
|
|
960
391
|
symbol: assetOut.symbol,
|
|
961
|
-
// Priced with the SOURCE asset's price, deliberately, even though the amount is
|
|
962
|
-
// counted in the destination token's units. A direct crossing hands back the
|
|
963
|
-
// same asset on another chain, and the bridged counterpart routinely trades at
|
|
964
|
-
// a discount to it — pricing the two sides from their own feeds made a
|
|
965
|
-
// zero-fee crossing report a loss (100 USDC in, 100 USDC out, "$100.03" paid
|
|
966
|
-
// and "$99.42" received). The delivered COUNT still comes from the destination
|
|
967
|
-
// token's decimals; only the basis is shared. The destination's own price
|
|
968
|
-
// remains the fallback so a route whose SOURCE side is unpriced still shows a
|
|
969
|
-
// value rather than nothing.
|
|
970
392
|
usdValue: usdValueOfAmount({
|
|
971
393
|
balance: estimatedAmountOut,
|
|
972
394
|
decimals: assetOut.decimals,
|
|
973
395
|
price: assetInUsdPrice ?? assetOutUsdPrice ?? null,
|
|
974
396
|
}),
|
|
975
397
|
},
|
|
976
|
-
// The direct route is a single signature, so its aggregate cost is that one
|
|
977
|
-
// section's fee: the bridge's own fee plus delivery. Null when either is unknown.
|
|
978
398
|
totalCostUsdValue: feeUsdValue,
|
|
979
|
-
// THE SAME BASIS AGGREGATED ROUTES ARE RANKED ON: delivered value less what
|
|
980
|
-
// was paid. The delivered amount is what is sent less both fees, and both
|
|
981
|
-
// sides are priced on the source token, so the difference is exactly the
|
|
982
|
-
// negated cost. It leaves out the sender's own network gas, which this
|
|
983
|
-
// route does not know; an aggregated route counts it when its quote does.
|
|
984
399
|
netValueUsd: feeUsdValue === null ? null : -feeUsdValue,
|
|
985
|
-
// The direct route is the only option for now, so it is always the best.
|
|
986
400
|
isBest: true,
|
|
987
401
|
deltaPercent: null,
|
|
988
402
|
};
|
|
989
403
|
};
|
|
990
|
-
/**
|
|
991
|
-
* Builds the TWO-SIGNATURE OUTBOUND raw {@link RouteData} from PulseChain, across
|
|
992
|
-
* an omnibridge entry chain (Ethereum or Binance Smart Chain), to wherever the
|
|
993
|
-
* second signature lands.
|
|
994
|
-
*
|
|
995
|
-
* The route is exactly TWO {@link RouteSectionData}s:
|
|
996
|
-
* - SECTION 1 ("Bridge to {entry chain}") — the omnibridge out.
|
|
997
|
-
* - SECTION 2 ("Complete at {final chain}") — DEFERRED. Signed only after section
|
|
998
|
-
* 1's crossing settles, and carrying no binding quote until then.
|
|
999
|
-
*
|
|
1000
|
-
* SECTION 2 HAS TWO SHAPES, and `roots.onward` is what picks between them.
|
|
1001
|
-
* Without it the deferred leg is a swap that stays on the entry chain: the
|
|
1002
|
-
* existing behaviour, down to its approximate ~0.29% PulseX fee. With it the
|
|
1003
|
-
* deferred leg is an onward BRIDGE to `onward.chainId`, carried by the named
|
|
1004
|
-
* provider, and it states NO fee — see the fee note in the body for why quoting
|
|
1005
|
-
* the swap constant there would be a fabricated number.
|
|
1006
|
-
*
|
|
1007
|
-
* WHY THE FIRST LEG CANNOT WIDEN THE SAME WAY. The bridged message carries
|
|
1008
|
-
* `(token, receiver, value, data)` and no chain id, and a mediator's counterpart
|
|
1009
|
-
* is wired in at deploy time, so the omnibridge's destination is structural
|
|
1010
|
-
* rather than a parameter. The reach comes from composing a second signature on
|
|
1011
|
-
* the far side, which is what this builder draws.
|
|
1012
|
-
*
|
|
1013
|
-
* Returns null when `assetIn`, `intermediateToken`, or `finalToken` is missing,
|
|
1014
|
-
* when the crossing's destination is not one of the two omnibridge entry chains,
|
|
1015
|
-
* or when `onward.chainId` disagrees with the final token's own `chainId`.
|
|
1016
|
-
*
|
|
1017
|
-
* @param roots - the resolved settings + price + logo roots (see {@link OutboundRouteRoots})
|
|
1018
|
-
* @returns the two-section raw outbound {@link RouteData}, or null when unresolvable
|
|
1019
|
-
*/
|
|
1020
404
|
export const buildOutboundRouteData = (roots) => {
|
|
1021
405
|
const { bridgeKey, assetIn, intermediateToken, finalToken, estimatedAmountOut, assetInUsdPrice, intermediateUsdPrice, assetInLogoUri, assetOutLogoUri, finalTokenLogoUri, fromChainLogoUri, intermediateChainLogoUri, onward = null, } = roots;
|
|
1022
406
|
if (!assetIn || !intermediateToken || !finalToken)
|
|
@@ -1026,21 +410,10 @@ export const buildOutboundRouteData = (roots) => {
|
|
|
1026
410
|
const entryChainName = resolveEntryChainName(entryChainId);
|
|
1027
411
|
if (entryChainName === null)
|
|
1028
412
|
return null;
|
|
1029
|
-
// A TOKEN DOES NOT CARRY A CHAIN, so the caller states the onward chain and the
|
|
1030
|
-
// token states its own, and the two must agree. Reading one from the other is
|
|
1031
|
-
// the cross-chain address mistake this codebase has already paid for: an origin
|
|
1032
|
-
// address asked for on the destination chain fails silently, and succeeds by
|
|
1033
|
-
// coincidence when some unrelated contract answers at the same address.
|
|
1034
|
-
// Disagreement here means one of the two is wrong and there is no way to tell
|
|
1035
|
-
// which, so the route is not drawn at all.
|
|
1036
413
|
if (onward !== null && onward.chainId !== finalToken.chainId)
|
|
1037
414
|
return null;
|
|
1038
|
-
// Where the SECOND signature leaves the user. Same as the crossing's landing
|
|
1039
|
-
// when the deferred leg is a swap; the onward chain when it is a bridge.
|
|
1040
415
|
const finalChainId = onward === null ? entryChainId : onward.chainId;
|
|
1041
416
|
const finalChainName = onward === null ? entryChainName : onward.chainName;
|
|
1042
|
-
// SECTION 1 fee: the crossing's bridge fee plus its delivery fee, priced in the
|
|
1043
|
-
// source token. Null when either is unknown.
|
|
1044
417
|
const { feeUsdValue: sectionOneFeeValue, fees: sectionOneFees } = crossingCost({
|
|
1045
418
|
bridgeKey,
|
|
1046
419
|
feeRoots: roots,
|
|
@@ -1048,16 +421,6 @@ export const buildOutboundRouteData = (roots) => {
|
|
|
1048
421
|
arrivingToken: toFeeToken(intermediateToken, entryChainId),
|
|
1049
422
|
sourceUsdPrice: assetInUsdPrice,
|
|
1050
423
|
});
|
|
1051
|
-
// SECTION 2's swap fee is APPROXIMATE: PulseX takes ~0.29% of the swapped value,
|
|
1052
|
-
// applied here to the bridged intermediate amount.
|
|
1053
|
-
//
|
|
1054
|
-
// AN ONWARD HOP GETS NO FIGURE AT ALL, and that is the honest answer rather
|
|
1055
|
-
// than a gap. `PULSEX_SWAP_FEE_BASIS_POINTS` prices a PulseX swap; an
|
|
1056
|
-
// aggregator bridge to another chain charges its own spread plus that chain's
|
|
1057
|
-
// gas, and neither is 0.29%. Reusing the constant would print a number that
|
|
1058
|
-
// looks measured and is not, beside section 1's fee, which really is measured.
|
|
1059
|
-
// The aggregator quotes the leg when the user returns to sign it, and until
|
|
1060
|
-
// then null is what we know.
|
|
1061
424
|
const sectionTwoFeeValue = onward !== null
|
|
1062
425
|
? null
|
|
1063
426
|
: usdValueOfAmount({
|
|
@@ -1071,8 +434,6 @@ export const buildOutboundRouteData = (roots) => {
|
|
|
1071
434
|
title: omnibridgeProviderDisplayName(provider),
|
|
1072
435
|
conversion: `${assetIn.symbol} → ${intermediateToken.symbol}`,
|
|
1073
436
|
feeUsdValue: sectionOneFeeValue,
|
|
1074
|
-
// Delivery auto-completes on the entry chain, so the single signature covers
|
|
1075
|
-
// the whole omnibridge leg.
|
|
1076
437
|
automatic: true,
|
|
1077
438
|
};
|
|
1078
439
|
const sectionOne = {
|
|
@@ -1096,9 +457,6 @@ export const buildOutboundRouteData = (roots) => {
|
|
|
1096
457
|
chainLogoUri: intermediateChainLogoUri,
|
|
1097
458
|
},
|
|
1098
459
|
],
|
|
1099
|
-
// The crossing lands the user in the INTERMEDIATE token on the entry chain —
|
|
1100
|
-
// the whole point of naming a step's landing, since this is exactly the stop
|
|
1101
|
-
// a two-signature route leaves you sitting at while you wait to sign again.
|
|
1102
460
|
landing: {
|
|
1103
461
|
amount: estimatedAmountOut,
|
|
1104
462
|
decimals: intermediateToken.decimals,
|
|
@@ -1110,16 +468,10 @@ export const buildOutboundRouteData = (roots) => {
|
|
|
1110
468
|
}),
|
|
1111
469
|
},
|
|
1112
470
|
feeUsdValue: sectionOneFeeValue,
|
|
1113
|
-
// Who takes what on the crossing: the bridge, then the delivery service.
|
|
1114
471
|
fees: sectionOneFees,
|
|
1115
|
-
// The omnibridge crossing out is the ~20-minute wait the deferred swap sits
|
|
1116
|
-
// behind, so the wait is shown HERE, on the crossing leg.
|
|
1117
472
|
etaMinutes: BRIDGE_SETTLEMENT_ETA_MINUTES,
|
|
1118
473
|
hops: [bridgeHop],
|
|
1119
474
|
};
|
|
1120
|
-
// SECTION 2 — DEFERRED. Signed only after section 1 settles. A swap on the entry
|
|
1121
|
-
// chain (approximate ~0.29% fee), or an onward bridge carried by a named
|
|
1122
|
-
// aggregator (no fee stated until it is quoted).
|
|
1123
475
|
const deferredHop = onward === null
|
|
1124
476
|
? {
|
|
1125
477
|
providerName: 'Swap',
|
|
@@ -1129,9 +481,6 @@ export const buildOutboundRouteData = (roots) => {
|
|
|
1129
481
|
automatic: false,
|
|
1130
482
|
}
|
|
1131
483
|
: {
|
|
1132
|
-
// THE SERVICE THAT WILL ACTUALLY CARRY IT. "Swap" names PulseX, and a
|
|
1133
|
-
// reader who saw it beside a Base destination would be told the wrong
|
|
1134
|
-
// thing about who holds their funds for the second leg.
|
|
1135
484
|
providerName: onward.providerName,
|
|
1136
485
|
title: `Bridge to ${onward.chainName}`,
|
|
1137
486
|
conversion: `${intermediateToken.symbol} → ${finalToken.symbol}`,
|
|
@@ -1154,17 +503,11 @@ export const buildOutboundRouteData = (roots) => {
|
|
|
1154
503
|
symbol: finalToken.symbol,
|
|
1155
504
|
address: finalToken.address,
|
|
1156
505
|
decimals: finalToken.decimals,
|
|
1157
|
-
// THE LINE THIS WHOLE CHANGE IS ABOUT. Pinned to `entryChainId`, the
|
|
1158
|
-
// deferred leg could only ever be a swap where the crossing landed, and
|
|
1159
|
-
// the route strip drew a Base token wearing Ethereum's badge.
|
|
1160
506
|
chainId: finalChainId,
|
|
1161
507
|
logoUri: finalTokenLogoUri,
|
|
1162
508
|
chainLogoUri: onward === null ? intermediateChainLogoUri : onward.chainLogoUri,
|
|
1163
509
|
},
|
|
1164
510
|
],
|
|
1165
|
-
// The deferred swap names the token it will land in but NOT an amount: it is
|
|
1166
|
-
// quoted only when the user returns, and stating a figure now would present an
|
|
1167
|
-
// estimate with the same weight as the crossing's real one.
|
|
1168
511
|
landing: {
|
|
1169
512
|
amount: null,
|
|
1170
513
|
decimals: finalToken.decimals,
|
|
@@ -1172,13 +515,6 @@ export const buildOutboundRouteData = (roots) => {
|
|
|
1172
515
|
usdValue: null,
|
|
1173
516
|
},
|
|
1174
517
|
feeUsdValue: sectionTwoFeeValue,
|
|
1175
|
-
// The swap itself is near-instant (~10 seconds); the ~20-minute wait it sits
|
|
1176
|
-
// behind is shown on section 1 (the crossing).
|
|
1177
|
-
//
|
|
1178
|
-
// AN ONWARD HOP STATES NO ESTIMATE, for the same reason it states no fee. Ten
|
|
1179
|
-
// seconds is how long a PulseX swap takes. How long an aggregator takes to
|
|
1180
|
-
// carry value to another chain depends on the pair and on the route it picks,
|
|
1181
|
-
// so the figure would be borrowed from a different operation.
|
|
1182
518
|
...(onward === null ? { etaSeconds: PULSEX_SWAP_ETA_SECONDS } : {}),
|
|
1183
519
|
deferred: true,
|
|
1184
520
|
deferredNote: onward === null
|
|
@@ -1187,71 +523,36 @@ export const buildOutboundRouteData = (roots) => {
|
|
|
1187
523
|
hops: [deferredHop],
|
|
1188
524
|
};
|
|
1189
525
|
return {
|
|
1190
|
-
// 'direct' EVEN WHEN THE SECOND LEG IS AN AGGREGATOR, and this is a decision
|
|
1191
|
-
// rather than a leftover. `kind` is what the interface dispatches EXECUTION
|
|
1192
|
-
// on: `executesViaConsolidated` sends a 'direct' route through the omnibridge
|
|
1193
|
-
// sender and everything else through the aggregator sender. The only
|
|
1194
|
-
// signature this route asks for now is section 1 — a plain omnibridge
|
|
1195
|
-
// crossing out of PulseChain — so the omnibridge sender is the correct one.
|
|
1196
|
-
// Calling it 'aggregated' would hand the button to a sender holding no quote
|
|
1197
|
-
// for it, because the onward leg is not quoted until the user returns and
|
|
1198
|
-
// starts a fresh route from the entry chain.
|
|
1199
526
|
kind: 'direct',
|
|
1200
527
|
entryChainId: null,
|
|
1201
|
-
// The route is shown crossing VIA the chain it bridges to. Already correct
|
|
1202
|
-
// for an onward hop: PulseChain to Base really does travel via Ethereum.
|
|
1203
528
|
viaChainId: entryChainId,
|
|
1204
529
|
headlineProviderKey: provider,
|
|
1205
530
|
sections: [sectionOne, sectionTwo],
|
|
1206
531
|
output: {
|
|
1207
|
-
// The final amount is quoted on return (deferred leg) — unknown at display time.
|
|
1208
532
|
amount: null,
|
|
1209
533
|
decimals: finalToken.decimals,
|
|
1210
534
|
symbol: finalToken.symbol,
|
|
1211
535
|
usdValue: null,
|
|
1212
536
|
},
|
|
1213
|
-
// Aggregate cost is section 1's crossing fees plus section 2's APPROXIMATE swap
|
|
1214
|
-
// fee (~0.29%). Section 2 drops out when it states no figure (an onward hop
|
|
1215
|
-
// is quoted only on return). Section 1 cannot: it is the signature the reader
|
|
1216
|
-
// makes now, so without its cost there is no total, only the swap's guess.
|
|
1217
537
|
totalCostUsdValue: sectionOneFeeValue === null ? null : addUsdValues(sectionOneFeeValue, sectionTwoFeeValue),
|
|
1218
538
|
isBest: true,
|
|
1219
539
|
deltaPercent: null,
|
|
1220
540
|
};
|
|
1221
541
|
};
|
|
1222
|
-
/**
|
|
1223
|
-
* Builds a single composable raw {@link RouteSectionData} for a swap on PulseX,
|
|
1224
|
-
* the decentralized exchange on PulseChain. A composing route appends this to its
|
|
1225
|
-
* `sections` array. Both tokens live on PulseChain (chain id 369), so both
|
|
1226
|
-
* token-flow nodes carry the PulseChain chain badge.
|
|
1227
|
-
*
|
|
1228
|
-
* The section carries raw numeric fields (`feePercent`, `priceImpactPercent`,
|
|
1229
|
-
* `feeUsdValue`); the adapter (`toRouteView`) formats them into the display
|
|
1230
|
-
* breakdown string.
|
|
1231
|
-
*
|
|
1232
|
-
* @param roots - the section's identity, tokens, pricing, and logos
|
|
1233
|
-
* @returns one raw PulseX swap {@link RouteSectionData}
|
|
1234
|
-
*/
|
|
1235
542
|
export const buildPulseXSwapSectionData = (roots) => {
|
|
1236
543
|
const { id, fromToken, toToken, feeUsdValue = null, priceImpactPercent = null, landedAmount = null, landedUsdValue = null, etaMinutes = null, etaSeconds = null, fromTokenLogoUri, toTokenLogoUri, pulsechainChainLogoUri, deferred = false, advanced = null, } = roots;
|
|
1237
544
|
const swapHop = {
|
|
1238
545
|
providerName: 'PulseX',
|
|
1239
|
-
// Resolve the PulseX logo via the provider key in the adapter; the raw hop
|
|
1240
|
-
// carries only the key, not the url.
|
|
1241
546
|
providerKey: 'PulseX',
|
|
1242
547
|
title: `Swap to ${toToken.symbol}`,
|
|
1243
548
|
conversion: `${fromToken.symbol} → ${toToken.symbol}`,
|
|
1244
549
|
feeUsdValue,
|
|
1245
|
-
// A standalone PulseX swap is the user's own signature, not an automatic hop.
|
|
1246
550
|
automatic: false,
|
|
1247
551
|
};
|
|
1248
552
|
const tokenFlow = [
|
|
1249
553
|
{ symbol: fromToken.symbol, address: fromToken.address, chainId: pulsechainChainId, decimals: fromToken.decimals, logoUri: fromTokenLogoUri, chainLogoUri: pulsechainChainLogoUri },
|
|
1250
554
|
{ symbol: toToken.symbol, address: toToken.address, chainId: pulsechainChainId, decimals: toToken.decimals, logoUri: toTokenLogoUri, chainLogoUri: pulsechainChainLogoUri },
|
|
1251
555
|
];
|
|
1252
|
-
// The swap section always shows the flat PulseX fee in its breakdown; the price
|
|
1253
|
-
// impact clause is added by the adapter only when the impact is present and
|
|
1254
|
-
// non-trivial. Round the impact to two decimals and drop it when it is <= 0.
|
|
1255
556
|
const roundedImpact = priceImpactPercent === null || Number(priceImpactPercent.toFixed(2)) <= 0
|
|
1256
557
|
? null
|
|
1257
558
|
: Number(priceImpactPercent.toFixed(2));
|
|
@@ -1260,8 +561,6 @@ export const buildPulseXSwapSectionData = (roots) => {
|
|
|
1260
561
|
label: 'Swap on PulseX',
|
|
1261
562
|
providerKey: 'PulseX',
|
|
1262
563
|
tokenFlow,
|
|
1263
|
-
// The swap is the last step of whatever composes it, so what it lands in is
|
|
1264
|
-
// what the user ends up holding.
|
|
1265
564
|
landing: {
|
|
1266
565
|
amount: landedAmount,
|
|
1267
566
|
decimals: toToken.decimals,
|
|
@@ -1269,175 +568,62 @@ export const buildPulseXSwapSectionData = (roots) => {
|
|
|
1269
568
|
usdValue: landedUsdValue,
|
|
1270
569
|
},
|
|
1271
570
|
feeUsdValue,
|
|
1272
|
-
// 0.29 — the flat PulseX fee percentage, kept in lockstep with the basis-point
|
|
1273
|
-
// constant so the breakdown line tracks the rate the estimate applies.
|
|
1274
571
|
feePercent: Number(PULSEX_SWAP_FEE_BASIS_POINTS) / 100,
|
|
1275
572
|
priceImpactPercent: roundedImpact,
|
|
1276
573
|
etaMinutes,
|
|
1277
574
|
etaSeconds,
|
|
1278
575
|
hops: [swapHop],
|
|
1279
|
-
// A PulseX swap is normally an immediate signature; deferred only when it sits
|
|
1280
|
-
// behind a prior section's crossing.
|
|
1281
576
|
deferred: deferred ? true : undefined,
|
|
1282
|
-
// NO `bridgeFrom`/`bridgeTo` — a same-chain swap crosses no bridge, and
|
|
1283
|
-
// leaving them unset (rather than null) keeps the disclosure from naming a
|
|
1284
|
-
// mediator that never runs. `destinationRouter` is the PulseX router
|
|
1285
|
-
// itself here, not a delivery leg inside a crossing — see the field's own
|
|
1286
|
-
// note on `RouteSectionData`.
|
|
1287
577
|
advanced: advanced === null ? undefined : advanced,
|
|
1288
578
|
};
|
|
1289
579
|
};
|
|
1290
|
-
/**
|
|
1291
|
-
* Composes a raw {@link RouteData} for the conversion route: bridge a liquid anchor
|
|
1292
|
-
* token onto PulseChain, then a DEFERRED PulseX swap to the wanted token.
|
|
1293
|
-
*
|
|
1294
|
-
* Operates entirely at the raw data level — no display formatting. The caller runs
|
|
1295
|
-
* the result through `toRouteView` to get the display-ready `RouteOption`.
|
|
1296
|
-
*
|
|
1297
|
-
* Per the owner's two locked decisions:
|
|
1298
|
-
*
|
|
1299
|
-
* - **LOCKED DECISION 1 — normalise via the liquid anchor.** When the omnibridge
|
|
1300
|
-
* cannot deliver the wanted token directly (the wanted token has not yet been
|
|
1301
|
-
* bridged, or its canonical foreign-chain pair is illiquid), the rails bridge a
|
|
1302
|
-
* well-known liquid anchor token instead and a PulseX swap leg converts it on
|
|
1303
|
-
* PulseChain. The {@link ConversionRouteComposition.baseData} is the raw route
|
|
1304
|
-
* that delivers that anchor; this function appends the conversion leg. The two
|
|
1305
|
-
* combined sections represent one complete user journey: bridge to the anchor,
|
|
1306
|
-
* then swap the anchor to the wanted token.
|
|
1307
|
-
*
|
|
1308
|
-
* - **LOCKED DECISION 2 — the swap is a deferred second signature.** The PulseX
|
|
1309
|
-
* swap can only be signed AFTER the PulseChain Bridge lands the anchor on
|
|
1310
|
-
* PulseChain — the funds must arrive before the user can authorise the swap. The
|
|
1311
|
-
* appended section therefore carries `deferred: true` and an ESTIMATED fee that
|
|
1312
|
-
* firms up on arrival (price impact and pool depth may shift in the ~20 minutes
|
|
1313
|
-
* the crossing takes). Do not collapse these into one signature.
|
|
1314
|
-
*
|
|
1315
|
-
* The composed route:
|
|
1316
|
-
* - keeps the base route's provider and entry chain (`entryChainId` / `via`) — it
|
|
1317
|
-
* is the same PulseChain Bridge crossing with a swap bolted on at the
|
|
1318
|
-
* destination;
|
|
1319
|
-
* - takes a DISTINCT identifier (`<baseRouteId>-pulsex-swap`) so React keys and
|
|
1320
|
-
* selection never collide with the bare anchor-delivery route;
|
|
1321
|
-
* - re-drives the output to the wanted token: the estimate's delivered amount, its
|
|
1322
|
-
* United States dollar value, and the wanted token's symbol (the bare anchor
|
|
1323
|
-
* route would otherwise claim the anchor as the delivered token);
|
|
1324
|
-
* - sums the swap's total cost (fee + price impact) into `totalCostUsdValue` so
|
|
1325
|
-
* the aggregate cost is honest — aggregator spread + omnibridge crossing +
|
|
1326
|
-
* PulseX swap — and subtracts it from `netValueUsd`.
|
|
1327
|
-
*
|
|
1328
|
-
* @param composition - the base route data, swap tokens, estimate, logos, and the
|
|
1329
|
-
* injected base route id (computed by the caller via `toRouteView`)
|
|
1330
|
-
* @returns the composed raw two-section conversion route (bridge, then deferred PulseX swap)
|
|
1331
|
-
*/
|
|
1332
580
|
export const buildConversionRouteData = (composition) => {
|
|
1333
581
|
const { baseData, baseRouteId, anchorToken, wantedToken, estimate, anchorTokenLogoUri, wantedTokenLogoUri, pulsechainChainLogoUri, } = composition;
|
|
1334
|
-
// The anchor bridges the two legs: it is BOTH the base route's delivered node and
|
|
1335
|
-
// the swap's input node. Reuse the delivered node's pre-resolved logo so the same
|
|
1336
|
-
// token shows ONE icon across the boundary; fall back to the threaded anchor logo.
|
|
1337
582
|
const deliveredAnchorNode = baseData.sections.at(-1)?.tokenFlow.at(-1);
|
|
1338
583
|
const boundaryAnchorLogoUri = deliveredAnchorNode?.logoUri ?? anchorTokenLogoUri;
|
|
1339
584
|
const swapSection = buildPulseXSwapSectionData({
|
|
1340
585
|
id: `${baseRouteId}-pulsex-swap`,
|
|
1341
586
|
fromToken: anchorToken,
|
|
1342
587
|
toToken: wantedToken,
|
|
1343
|
-
// The section cost is the swap's TOTAL value loss (fee + price impact) so the
|
|
1344
|
-
// route reconciles: input value − each step's cost = delivered output.
|
|
1345
588
|
feeUsdValue: estimate.costUsdValue,
|
|
1346
589
|
priceImpactPercent: estimate.priceImpactPercent,
|
|
1347
|
-
// The swap lands the user in the wanted token. Its value is the bridge leg's
|
|
1348
|
-
// landing less what the swap costs — the same subtraction the route's output
|
|
1349
|
-
// uses, so the last step's landing figure and the route's delivered figure
|
|
1350
|
-
// cannot drift apart.
|
|
1351
590
|
landedAmount: estimate.outputAmount,
|
|
1352
591
|
landedUsdValue: subtractUsdValues(baseData.output.usdValue, estimate.costUsdValue),
|
|
1353
|
-
// The swap is near-instant (~10s); the ~20-minute wait sits on the crossing below.
|
|
1354
592
|
etaSeconds: PULSEX_SWAP_ETA_SECONDS,
|
|
1355
593
|
fromTokenLogoUri: boundaryAnchorLogoUri,
|
|
1356
594
|
toTokenLogoUri: wantedTokenLogoUri,
|
|
1357
595
|
pulsechainChainLogoUri,
|
|
1358
|
-
// The bridge must land on PulseChain before the swap can be signed.
|
|
1359
596
|
deferred: true,
|
|
1360
597
|
});
|
|
1361
|
-
// The ~20-minute crossing wait belongs on the section that LANDS THE FUNDS ON
|
|
1362
|
-
// PULSECHAIN, replacing whatever entry-chain-only estimate that section carried.
|
|
1363
|
-
//
|
|
1364
|
-
// THE INDEX IS NOT A PROXY HERE, and the difference is worth stating because
|
|
1365
|
-
// this project has been bitten by one. A positional "last section" stood in for
|
|
1366
|
-
// "the section Gibs delivers" elsewhere and broke the moment a deferred leg
|
|
1367
|
-
// appeared. This is the other case: a base route is BUILT to end on PulseChain —
|
|
1368
|
-
// that is what makes it a base a swap can be appended to — so its final section
|
|
1369
|
-
// IS the landing section by construction, for a one-signature base and for the
|
|
1370
|
-
// two-signature NEAR Intents base alike. The name says which fact is meant, so a
|
|
1371
|
-
// future base that ends somewhere else fails a test rather than a reader.
|
|
1372
598
|
const pulsechainLandingSectionIndex = baseData.sections.length - 1;
|
|
1373
599
|
const bridgeSections = baseData.sections.map((section, index) => index === pulsechainLandingSectionIndex
|
|
1374
600
|
? { ...section, etaMinutes: BRIDGE_SETTLEMENT_ETA_MINUTES, etaSeconds: null }
|
|
1375
601
|
: section);
|
|
1376
602
|
return {
|
|
1377
603
|
...baseData,
|
|
1378
|
-
// The conversion route crosses the aggregator entry chain then swaps on PulseX.
|
|
1379
604
|
kind: 'conversion',
|
|
1380
|
-
// Carry the aggregator entry chain so Bridge.tsx can link back to the candidate.
|
|
1381
605
|
entryChainId: baseData.entryChainId ?? null,
|
|
1382
606
|
sections: [...bridgeSections, swapSection],
|
|
1383
607
|
output: {
|
|
1384
608
|
amount: estimate.outputAmount,
|
|
1385
609
|
decimals: wantedToken.decimals,
|
|
1386
610
|
symbol: wantedToken.symbol,
|
|
1387
|
-
// The wanted token's COUNT comes from the swap quote, but its value continues
|
|
1388
|
-
// the base route's basis: what the anchor was worth on arrival, less the swap's
|
|
1389
|
-
// cost. Re-pricing the wanted token against its own feed — which is what this
|
|
1390
|
-
// used to do — sets the figure by the router's realized rate instead, and that
|
|
1391
|
-
// rate routinely disagrees with the ratio of the two feeds by about a dollar on
|
|
1392
|
-
// a hundred. The route then appeared to deliver more than its own cost rows allowed.
|
|
1393
611
|
usdValue: subtractUsdValues(baseData.output.usdValue, estimate.costUsdValue),
|
|
1394
612
|
},
|
|
1395
|
-
// Aggregate cost = base cost + the swap's total loss; null only when both unknown.
|
|
1396
613
|
totalCostUsdValue: addUsdValues(baseData.totalCostUsdValue, estimate.costUsdValue),
|
|
1397
|
-
// Net = base net − the swap's total loss; null when either is unknown.
|
|
1398
614
|
netValueUsd: baseData.netValueUsd == null || estimate.costUsdValue == null
|
|
1399
615
|
? null
|
|
1400
616
|
: baseData.netValueUsd - estimate.costUsdValue,
|
|
1401
617
|
};
|
|
1402
618
|
};
|
|
1403
|
-
/**
|
|
1404
|
-
* Derives a {@link PulseXSwapEstimateData} — the raw NUMERIC form of the PulseX
|
|
1405
|
-
* swap estimate — from a PulseX `getAmountsOut` quote.
|
|
1406
|
-
*
|
|
1407
|
-
* Returns raw numbers rather than formatted currency strings, so the conversion
|
|
1408
|
-
* composition can flow them straight into a {@link RouteData} without re-parsing
|
|
1409
|
-
* currency strings.
|
|
1410
|
-
*
|
|
1411
|
-
* Returns null when the quote is absent or empty (the router reverted or has not
|
|
1412
|
-
* loaded) — the composing route then has no swap leg to append.
|
|
1413
|
-
*
|
|
1414
|
-
* @param params.amountsOut - the router's amounts-out array, or null when unquoted
|
|
1415
|
-
* @param params.inputAmount - the anchor amount entering the swap, in its base units
|
|
1416
|
-
* @param params.inputToken - the anchor token (the swap input)
|
|
1417
|
-
* @param params.inputUsdPrice - the anchor token's per-token dollar price, or null
|
|
1418
|
-
* @param params.outputToken - the wanted token (the swap output)
|
|
1419
|
-
* @param params.outputUsdPrice - the wanted token's per-token dollar price, or null
|
|
1420
|
-
* @param params.referenceAmountsOut - marginal reference quote amounts-out, or null
|
|
1421
|
-
* @param params.referenceInputAmount - the reference quote's input amount
|
|
1422
|
-
* @returns the derived estimate with numeric fields, or null when the quote yields no output amount
|
|
1423
|
-
*/
|
|
1424
619
|
export const buildPulseXSwapEstimateData = ({ amountsOut, inputAmount, inputToken, inputUsdPrice, outputToken, outputUsdPrice, referenceAmountsOut = null, referenceInputAmount = 0n, }) => {
|
|
1425
620
|
if (!amountsOut || amountsOut.length === 0)
|
|
1426
621
|
return null;
|
|
1427
622
|
const outputAmount = amountsOut[amountsOut.length - 1];
|
|
1428
623
|
if (typeof outputAmount !== 'bigint')
|
|
1429
624
|
return null;
|
|
1430
|
-
// The swap fee is APPROXIMATE: PulseX takes a flat ~0.29% of the trade. Apply that
|
|
1431
|
-
// rate to the swapped value, preferring the input (anchor) side and falling back
|
|
1432
|
-
// to the output (wanted) side, so the fee still shows when only ONE side is priced.
|
|
1433
625
|
const feeUsdValue = usdValueOfAmount({ balance: (inputAmount * PULSEX_SWAP_FEE_BASIS_POINTS) / BASIS_POINTS_DENOMINATOR, decimals: inputToken.decimals, price: inputUsdPrice }) ??
|
|
1434
626
|
usdValueOfAmount({ balance: (outputAmount * PULSEX_SWAP_FEE_BASIS_POINTS) / BASIS_POINTS_DENOMINATOR, decimals: outputToken.decimals, price: outputUsdPrice });
|
|
1435
|
-
// PRICE IMPACT comes from the MARGINAL reference quote, NOT from differencing the
|
|
1436
|
-
// two PulseChain USD prices: those prices are themselves PulseX pool quotes, so
|
|
1437
|
-
// value-in minus value-out cancels the fee and impact to ~$0 (the bug this
|
|
1438
|
-
// replaces). Instead compare the full trade's realized rate against the tiny
|
|
1439
|
-
// reference quote's near-spot rate. The swap fee rides in BOTH `getAmountsOut`
|
|
1440
|
-
// results, so it cancels in the ratio and the gap is price impact ALONE.
|
|
1441
627
|
const marginalRate = (() => {
|
|
1442
628
|
if (!referenceAmountsOut || referenceAmountsOut.length === 0)
|
|
1443
629
|
return null;
|
|
@@ -1454,18 +640,11 @@ export const buildPulseXSwapEstimateData = ({ amountsOut, inputAmount, inputToke
|
|
|
1454
640
|
})();
|
|
1455
641
|
const inputWhole = Number(formatUnits(inputAmount, inputToken.decimals));
|
|
1456
642
|
const outputWhole = Number(formatUnits(outputAmount, outputToken.decimals));
|
|
1457
|
-
// The output the trade WOULD deliver at the marginal (near-spot) rate with no
|
|
1458
|
-
// price impact — fee still applied, since the marginal rate carries it too.
|
|
1459
643
|
const idealOutputWhole = marginalRate !== null ? inputWhole * marginalRate : null;
|
|
1460
|
-
// The impact loss in OUTPUT-token units: what the marginal rate promised minus
|
|
1461
|
-
// what the curve actually delivered. The fee is in both, so this is impact only.
|
|
1462
644
|
const impactLossOutput = idealOutputWhole !== null ? Math.max(0, idealOutputWhole - outputWhole) : null;
|
|
1463
645
|
const priceImpactPercent = impactLossOutput !== null && idealOutputWhole !== null && idealOutputWhole > 0
|
|
1464
646
|
? Math.max(0, (impactLossOutput / idealOutputWhole) * 100)
|
|
1465
647
|
: null;
|
|
1466
|
-
// Value the impact loss in dollars — preferring the output token's price, falling
|
|
1467
|
-
// back to the input side via the marginal rate so the impact still shows when
|
|
1468
|
-
// only the anchor is priced.
|
|
1469
648
|
const impactUsd = (() => {
|
|
1470
649
|
if (impactLossOutput === null)
|
|
1471
650
|
return null;
|
|
@@ -1476,9 +655,6 @@ export const buildPulseXSwapEstimateData = ({ amountsOut, inputAmount, inputToke
|
|
|
1476
655
|
}
|
|
1477
656
|
return null;
|
|
1478
657
|
})();
|
|
1479
|
-
// Total cost is the flat fee PLUS the price impact — both real, neither cancelled.
|
|
1480
|
-
// Round the fee to cents the SAME way the pre-split builder did (it summed the
|
|
1481
|
-
// already-formatted fee string), so the total cost is identical to the cent.
|
|
1482
658
|
const feeRounded = feeUsdValue === null ? null : roundUsdToCents(feeUsdValue);
|
|
1483
659
|
const costUsdValue = feeRounded !== null && Number.isFinite(feeRounded)
|
|
1484
660
|
? feeRounded + (impactUsd ?? 0)
|
|
@@ -1497,33 +673,6 @@ export const buildPulseXSwapEstimateData = ({ amountsOut, inputAmount, inputToke
|
|
|
1497
673
|
etaMinutes: null,
|
|
1498
674
|
};
|
|
1499
675
|
};
|
|
1500
|
-
/**
|
|
1501
|
-
* Composes a raw {@link RouteData} for a SAME-CHAIN PulseX swap: PulseChain on
|
|
1502
|
-
* both sides of the selection, one signature, no crossing at all.
|
|
1503
|
-
*
|
|
1504
|
-
* WHY THIS IS ITS OWN PRODUCER, RATHER THAN `buildConversionRouteData` WITH NO
|
|
1505
|
-
* BASE. The conversion route APPENDS a swap section to a bridge route that
|
|
1506
|
-
* already delivered the anchor onto PulseChain; its cost, its `entryChainId`,
|
|
1507
|
-
* and its "deferred" swap all inherit from that base. A same-chain swap has no
|
|
1508
|
-
* base — the reader already holds the input token on PulseChain before
|
|
1509
|
-
* pressing anything — so the section is signed IMMEDIATELY (`deferred` stays
|
|
1510
|
-
* false, the {@link buildPulseXSwapSectionData} default) and the route has no
|
|
1511
|
-
* `entryChainId` at all, exactly like the direct omnibridge route it sits
|
|
1512
|
-
* beside in the strip.
|
|
1513
|
-
*
|
|
1514
|
-
* NOT DEFERRED, AND THAT IS LOAD-BEARING FOR THE TRACKER. `deferred` means "the
|
|
1515
|
-
* reader has not yet signed this" (see `sectionProgress.ts`); a same-chain swap
|
|
1516
|
-
* is signed in the very press that builds it, so marking it deferred would tell
|
|
1517
|
-
* the tracker a signature is still owed for a transaction already broadcast.
|
|
1518
|
-
* What DOES need care downstream is the opposite failure the tracker's own
|
|
1519
|
-
* `routeChainSequence` can produce for a route whose every stop shares one
|
|
1520
|
-
* chain id — see `routeProgress.ts`'s `reachedChainIndex` for the fix that
|
|
1521
|
-
* keeps a same-chain route from reading "delivered" the instant it is merely
|
|
1522
|
-
* broadcast.
|
|
1523
|
-
*
|
|
1524
|
-
* @param roots - the route's identity, tokens, pricing, and logos
|
|
1525
|
-
* @returns the one-section same-chain swap {@link RouteData}
|
|
1526
|
-
*/
|
|
1527
676
|
export const buildSameChainSwapRouteData = (roots) => {
|
|
1528
677
|
const { id, fromToken, toToken, estimate, inputUsdValue, fromTokenLogoUri, toTokenLogoUri, pulsechainChainLogoUri, advanced, } = roots;
|
|
1529
678
|
const swapSection = buildPulseXSwapSectionData({
|
|
@@ -1538,13 +687,11 @@ export const buildSameChainSwapRouteData = (roots) => {
|
|
|
1538
687
|
fromTokenLogoUri,
|
|
1539
688
|
toTokenLogoUri,
|
|
1540
689
|
pulsechainChainLogoUri,
|
|
1541
|
-
// Signed immediately — see the function doc above for why this must stay false.
|
|
1542
690
|
deferred: false,
|
|
1543
691
|
advanced,
|
|
1544
692
|
});
|
|
1545
693
|
return {
|
|
1546
694
|
kind: 'swap',
|
|
1547
|
-
// No aggregator entry chain: the whole journey is one signature on PulseChain.
|
|
1548
695
|
entryChainId: null,
|
|
1549
696
|
headlineProviderKey: 'PulseX',
|
|
1550
697
|
sections: [swapSection],
|
|
@@ -1558,7 +705,6 @@ export const buildSameChainSwapRouteData = (roots) => {
|
|
|
1558
705
|
netValueUsd: inputUsdValue == null || estimate.costUsdValue == null
|
|
1559
706
|
? null
|
|
1560
707
|
: inputUsdValue - estimate.costUsdValue,
|
|
1561
|
-
// The swap's own near-instant duration; there is no crossing wait to add.
|
|
1562
708
|
etaMinutes: null,
|
|
1563
709
|
};
|
|
1564
710
|
};
|