@gibs/bridge-sdk 1.19.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abis.js +0 -2
- package/dist/bridge-limits.js +0 -38
- package/dist/chain-info.js +0 -116
- package/dist/chain-names.js +0 -151
- package/dist/chains.js +0 -198
- package/dist/cjs/abis.js +0 -2
- package/dist/cjs/bridge-limits.js +0 -38
- package/dist/cjs/chain-info.js +0 -116
- package/dist/cjs/chain-names.js +0 -151
- package/dist/cjs/chains.js +0 -198
- package/dist/cjs/claims.js +0 -60
- package/dist/cjs/config.js +1 -208
- package/dist/cjs/consolidated-quote.js +0 -465
- package/dist/cjs/destination-calldata.js +0 -71
- package/dist/cjs/ecosystems.js +0 -183
- package/dist/cjs/execution.js +0 -92
- package/dist/cjs/fee-type.js +0 -1
- package/dist/cjs/hyperliquid-execution.js +0 -98
- package/dist/cjs/image-links.js +0 -108
- package/dist/cjs/lifi-execution-guard.js +65 -172
- package/dist/cjs/near-intents-conformance.js +0 -110
- package/dist/cjs/near-intents-route.js +0 -149
- package/dist/cjs/near-intents.js +0 -562
- package/dist/cjs/origin-deposit.js +0 -56
- package/dist/cjs/pre-quote.js +0 -80
- package/dist/cjs/prices.js +0 -113
- package/dist/cjs/providers.js +0 -83
- package/dist/cjs/quote-fees.js +0 -80
- package/dist/cjs/referral.js +0 -75
- package/dist/cjs/relay-currencies.js +0 -31
- package/dist/cjs/relay-depository.js +52 -77
- package/dist/cjs/relay-quote.js +0 -394
- package/dist/cjs/route-steps.js +0 -100
- package/dist/cjs/routing/amounts.js +0 -83
- package/dist/cjs/routing/candidate-route.js +0 -73
- package/dist/cjs/routing/candidate.js +0 -46
- package/dist/cjs/routing/carrier-provider-key.js +0 -27
- package/dist/cjs/routing/carriers/omnibridge.js +0 -120
- package/dist/cjs/routing/carriers/pulsex-paths.js +0 -57
- package/dist/cjs/routing/carriers/pulsex-split.js +0 -92
- package/dist/cjs/routing/carriers/pulsex.js +0 -279
- package/dist/cjs/routing/carriers/wrap.js +0 -57
- package/dist/cjs/routing/composed-route-quality.js +0 -95
- package/dist/cjs/routing/entry-chain-anchor.js +0 -27
- package/dist/cjs/routing/format.js +0 -50
- package/dist/cjs/routing/index.js +0 -28
- package/dist/cjs/routing/producers.js +0 -854
- package/dist/cjs/routing/providers.js +0 -27
- package/dist/cjs/routing/search/compose.js +0 -56
- package/dist/cjs/routing/search/graph.js +0 -364
- package/dist/cjs/routing/search/plan-route-data.js +0 -508
- package/dist/cjs/routing/search/score.js +0 -344
- package/dist/cjs/routing/swap-transactions.js +0 -74
- package/dist/cjs/routing/usd-value.js +0 -12
- package/dist/cjs/settings.js +1 -456
- package/dist/cjs/swap-venues.js +0 -17
- package/dist/cjs/ton-connect.js +0 -82
- package/dist/cjs/ton-execution.js +0 -6
- package/dist/cjs/ton.js +0 -42
- package/dist/cjs/tron-execution.js +0 -145
- package/dist/claims.js +0 -60
- package/dist/config.js +1 -211
- package/dist/consolidated-quote.js +0 -465
- package/dist/destination-calldata.js +0 -71
- package/dist/ecosystems.js +0 -183
- package/dist/execution.js +0 -92
- package/dist/fee-type.js +0 -2
- package/dist/hyperliquid-execution.js +0 -98
- package/dist/image-links.js +0 -108
- package/dist/lifi-execution-guard.js +65 -172
- package/dist/near-intents-conformance.js +0 -110
- package/dist/near-intents-route.js +0 -149
- package/dist/near-intents.js +0 -562
- package/dist/origin-deposit.js +0 -56
- package/dist/pre-quote.js +0 -80
- package/dist/prices.js +0 -113
- package/dist/providers.js +0 -83
- package/dist/quote-fees.js +0 -80
- package/dist/referral.js +0 -75
- package/dist/relay-currencies.js +0 -31
- package/dist/relay-depository.js +52 -77
- package/dist/relay-quote.js +0 -394
- package/dist/route-steps.js +0 -100
- package/dist/routing/amounts.js +0 -83
- package/dist/routing/candidate-route.js +0 -73
- package/dist/routing/candidate.js +0 -46
- package/dist/routing/carrier-provider-key.js +0 -27
- package/dist/routing/carriers/omnibridge.js +0 -120
- package/dist/routing/carriers/pulsex-paths.js +0 -57
- package/dist/routing/carriers/pulsex-split.js +0 -92
- package/dist/routing/carriers/pulsex.js +0 -279
- package/dist/routing/carriers/wrap.js +0 -57
- package/dist/routing/composed-route-quality.js +0 -95
- package/dist/routing/entry-chain-anchor.js +0 -27
- package/dist/routing/format.js +0 -50
- package/dist/routing/index.js +0 -28
- package/dist/routing/producers.js +0 -854
- package/dist/routing/providers.js +0 -27
- package/dist/routing/search/compose.js +0 -56
- package/dist/routing/search/graph.js +0 -364
- package/dist/routing/search/plan-route-data.js +0 -508
- package/dist/routing/search/score.js +0 -344
- package/dist/routing/swap-transactions.js +0 -74
- package/dist/routing/usd-value.js +0 -12
- package/dist/settings.js +1 -456
- package/dist/swap-venues.js +0 -17
- package/dist/ton-connect.js +0 -82
- package/dist/ton-execution.js +0 -6
- package/dist/ton.js +0 -42
- package/dist/tron-execution.js +0 -145
- package/package.json +3 -3
|
@@ -3,24 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.omnibridgeProviderForEntryChain = exports.pulseXConversionSwapPath = void 0;
|
|
4
4
|
const config_js_1 = require("../config.js");
|
|
5
5
|
const ecosystems_js_1 = require("../ecosystems.js");
|
|
6
|
-
/**
|
|
7
|
-
* Builds the PulseX swap path for a conversion leg, from the bridged anchor to the
|
|
8
|
-
* wanted token. Routes through WPLS (`[anchor, WPLS, wanted]`) by default because
|
|
9
|
-
* most PulseChain tokens have a deep WPLS pool but rarely a direct pool with the USD
|
|
10
|
-
* anchor; it collapses to a direct `[anchor, wanted]` hop only when one side already
|
|
11
|
-
* IS WPLS (no point hopping through WPLS to reach WPLS). The router prices the path
|
|
12
|
-
* with `getAmountsOut`, so a path with no liquidity simply yields no quote.
|
|
13
|
-
*
|
|
14
|
-
* A FIXED PATH, NOT THE BEST ONE. The route search's PulseX carrier prices
|
|
15
|
-
* this path alongside others through its hub tokens and signs whichever
|
|
16
|
-
* delivers most (`carriers/pulsex-paths.ts`). This one remains the
|
|
17
|
-
* carrier's asynchronous `ask` path (`pulseXWrappedNativePath`).
|
|
18
|
-
*
|
|
19
|
-
* @param anchorAddress - the bridged anchor token (the swap input)
|
|
20
|
-
* @param wantedAddress - the token the user wants (the swap output)
|
|
21
|
-
* @param wplsAddress - the wrapped-native intermediary (WPLS)
|
|
22
|
-
* @returns the ordered swap path for `getAmountsOut`
|
|
23
|
-
*/
|
|
24
6
|
const pulseXConversionSwapPath = (anchorAddress, wantedAddress, wplsAddress) => {
|
|
25
7
|
const isWpls = (address) => (0, ecosystems_js_1.sameAddress)(address, wplsAddress, 'evm');
|
|
26
8
|
if (isWpls(anchorAddress) || isWpls(wantedAddress))
|
|
@@ -28,15 +10,6 @@ const pulseXConversionSwapPath = (anchorAddress, wantedAddress, wplsAddress) =>
|
|
|
28
10
|
return [anchorAddress, wplsAddress, wantedAddress];
|
|
29
11
|
};
|
|
30
12
|
exports.pulseXConversionSwapPath = pulseXConversionSwapPath;
|
|
31
|
-
/**
|
|
32
|
-
* The bridge provider that crosses each omnibridge entry point to PulseChain.
|
|
33
|
-
*
|
|
34
|
-
* Typed `Provider | null` rather than `string | null` — this is the root
|
|
35
|
-
* source `RouteCandidate.omnibridgeProvider` and `RouteData.headlineProviderKey`
|
|
36
|
-
* both derive from, so a widened return type here forced every caller downstream
|
|
37
|
-
* to re-widen too, which is what let a wrong-kind key reach `providerDisplayName`
|
|
38
|
-
* unnoticed. See `routing/candidate.ts` and `routing/route-data.ts`.
|
|
39
|
-
*/
|
|
40
13
|
const omnibridgeProviderForEntryChain = (entryChainId) => {
|
|
41
14
|
const entryChain = (0, config_js_1.toChain)(entryChainId);
|
|
42
15
|
if (entryChain === config_js_1.Chains.ETH)
|
|
@@ -2,38 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.hopOutputAmount = exports.composeRouteDelivery = exports.composableHopBasis = void 0;
|
|
4
4
|
const graph_js_1 = require("./graph.js");
|
|
5
|
-
/** The {@link ComposedPathBasis} one hop's figure contributes: `'quoted'` for a bound quote, its own basis for a `'probeRate'`. */
|
|
6
5
|
const composableHopBasis = (figure) => figure.kind === 'quote' ? 'quoted' : figure.basis;
|
|
7
6
|
exports.composableHopBasis = composableHopBasis;
|
|
8
|
-
/**
|
|
9
|
-
* Applies one {@link ExchangeRate} to an actual input amount, as an exact
|
|
10
|
-
* bigint multiply-then-divide (floor division — the same rounding direction
|
|
11
|
-
* a real on-chain quote would apply against the reader). Never routes the
|
|
12
|
-
* amount through a float.
|
|
13
|
-
*
|
|
14
|
-
* @param inputAmount - the actual base-unit amount arriving at the hop
|
|
15
|
-
* @param rate - the hop's observed {@link ExchangeRate}
|
|
16
|
-
* @returns the estimated base-unit output
|
|
17
|
-
*/
|
|
18
7
|
const applyExchangeRate = (inputAmount, rate) => (inputAmount * rate.numerator) / rate.denominator;
|
|
19
|
-
/**
|
|
20
|
-
* Composes a path's per-hop figures into one end-to-end estimated delivered
|
|
21
|
-
* amount, applying design section 3's rule: a later hop's output is its
|
|
22
|
-
* probe rate applied to the ACTUAL amount arriving at that hop — never the
|
|
23
|
-
* reference amount the probe itself was taken at.
|
|
24
|
-
*
|
|
25
|
-
* REFUSES RATHER THAN GUESSES in three cases, checked hop by hop so the
|
|
26
|
-
* refusal names the exact hop responsible: the path is empty
|
|
27
|
-
* ({@link ComposeRefusalReason} `'empty-path'`); a hop does not connect to the
|
|
28
|
-
* one before it (`'discontinuous-path'`); two hops disagree about a shared
|
|
29
|
-
* asset's decimals (`'decimals-mismatch'`); or a `'probeRate'` figure names
|
|
30
|
-
* no real ratio (`'missing-rate'`). See {@link ComposeRefusalReason} for why
|
|
31
|
-
* each of these is a refusal and not a best-effort answer.
|
|
32
|
-
*
|
|
33
|
-
* @param inputAmount - the base-unit amount entering the path's first hop
|
|
34
|
-
* @param hops - the path's hops, first to last
|
|
35
|
-
* @returns the composed estimate, or the refusal that stopped composition
|
|
36
|
-
*/
|
|
37
8
|
const composeRouteDelivery = (inputAmount, hops) => {
|
|
38
9
|
if (hops.length === 0) {
|
|
39
10
|
return { kind: 'refused', reason: 'empty-path', hopIndex: 0 };
|
|
@@ -56,15 +27,6 @@ const composeRouteDelivery = (inputAmount, hops) => {
|
|
|
56
27
|
return { kind: 'estimate', deliveredAmount: actualInput, basis };
|
|
57
28
|
};
|
|
58
29
|
exports.composeRouteDelivery = composeRouteDelivery;
|
|
59
|
-
/**
|
|
60
|
-
* Checks that hop `hopIndex` connects to the hop before it: the same asset,
|
|
61
|
-
* by {@link assetNodeKey}, with the same declared decimals. The first hop has
|
|
62
|
-
* nothing before it and always passes.
|
|
63
|
-
*
|
|
64
|
-
* @param hops - the full path being composed
|
|
65
|
-
* @param hopIndex - the hop to check against its predecessor
|
|
66
|
-
* @returns the refusal reason, or null when the hop connects cleanly
|
|
67
|
-
*/
|
|
68
30
|
const checkContinuity = (hops, hopIndex) => {
|
|
69
31
|
if (hopIndex === 0)
|
|
70
32
|
return null;
|
|
@@ -76,24 +38,6 @@ const checkContinuity = (hops, hopIndex) => {
|
|
|
76
38
|
return 'decimals-mismatch';
|
|
77
39
|
return null;
|
|
78
40
|
};
|
|
79
|
-
/**
|
|
80
|
-
* One hop's output amount for the actual amount arriving at it: the quote's
|
|
81
|
-
* own `outputAmount` when the figure is a real quote, or the probe rate
|
|
82
|
-
* applied to `actualInput` when it is a `'probeRate'`. Returns null — never a
|
|
83
|
-
* guessed number — when a `'probeRate'` figure's rate names no positive
|
|
84
|
-
* denominator.
|
|
85
|
-
*
|
|
86
|
-
* EXPORTED SO `waves.ts` REUSES THIS MATH RATHER THAN RESTATING IT.
|
|
87
|
-
* `recomposePath` (`waves.ts`) needs each hop's OWN output amount, not just
|
|
88
|
-
* the path's final total {@link composeRouteDelivery} returns, to refresh
|
|
89
|
-
* every hop's individual `outcome` — this is the one place that division
|
|
90
|
-
* happens, so both call sites agree by construction instead of by two
|
|
91
|
-
* separately-maintained copies of `numerator * input / denominator`.
|
|
92
|
-
*
|
|
93
|
-
* @param hop - the hop to price
|
|
94
|
-
* @param actualInput - the actual base-unit amount arriving at this hop
|
|
95
|
-
* @returns the hop's output amount, or null when its rate is missing
|
|
96
|
-
*/
|
|
97
41
|
const hopOutputAmount = (hop, actualInput) => {
|
|
98
42
|
if (hop.figure.kind === 'quote')
|
|
99
43
|
return hop.figure.outputAmount;
|
|
@@ -3,244 +3,38 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.compareRoutePlansByDeliveredAmount = exports.groupHopsBySignature = exports.routePlanPathKey = exports.hopEdgeKey = exports.planIsConfirmed = exports.hopComposedBasis = exports.planComposedBasis = exports.isConfirmedBasis = exports.isRankableBasis = exports.combineComposedPathBasis = exports.planIsRankable = exports.RANKABLE_ESTIMATE_BASES = exports.isRateLimitedCarrier = exports.assetNodeKey = void 0;
|
|
4
4
|
const ecosystems_js_1 = require("../../ecosystems.js");
|
|
5
5
|
const providers_js_1 = require("../../providers.js");
|
|
6
|
-
/**
|
|
7
|
-
* The stable, collision-free key for one {@link AssetNode}.
|
|
8
|
-
*
|
|
9
|
-
* Normalizes address casing through the ecosystem registry
|
|
10
|
-
* (`ecosystemById(...).addressCasing`) before keying, so a checksummed and a
|
|
11
|
-
* lower-cased reference to the SAME Ethereum address produce the SAME key —
|
|
12
|
-
* two representations of one asset must key identically, or the search would
|
|
13
|
-
* treat one node as two and silently duplicate work. An ecosystem whose
|
|
14
|
-
* addresses are case-SIGNIFICANT (Solana, Tron, The Open Network, Bitcoin,
|
|
15
|
-
* Zcash — see `ecosystems.ts`) is left exactly as given, because lower-casing
|
|
16
|
-
* a case-significant address can turn it into a different, unrelated one.
|
|
17
|
-
*
|
|
18
|
-
* Collision-free for any two distinct `(chainId, normalizedAddress)` pairs:
|
|
19
|
-
* `chainId` renders as a plain decimal numeral (never containing `:`), so the
|
|
20
|
-
* FIRST `:` in the key always marks the same boundary a caller would expect,
|
|
21
|
-
* and no two distinct pairs can render the same string.
|
|
22
|
-
*
|
|
23
|
-
* @param node - the asset to key
|
|
24
|
-
* @returns the node's graph key, e.g. `"1:0xa0b8...eb48"`
|
|
25
|
-
*/
|
|
26
6
|
const assetNodeKey = (node) => {
|
|
27
7
|
const casing = (0, ecosystems_js_1.ecosystemById)(node.ecosystem).addressCasing;
|
|
28
8
|
const normalizedAddress = casing === 'insensitive' ? node.address.toLowerCase() : node.address;
|
|
29
9
|
return `${node.chainId}:${normalizedAddress}`;
|
|
30
10
|
};
|
|
31
11
|
exports.assetNodeKey = assetNodeKey;
|
|
32
|
-
/** Every registered rate-limited provider id (LI.FI, Relay, NEAR Intents), as a lookup set. */
|
|
33
12
|
const registeredProviderIds = new Set(providers_js_1.providerRegistry.map((descriptor) => descriptor.id));
|
|
34
|
-
/**
|
|
35
|
-
* Whether `carrier` is one of the three rate-limited aggregators
|
|
36
|
-
* (`ProviderId`) rather than a FREE local read (the omnibridge crossing or a
|
|
37
|
-
* PulseX swap). Free carriers are asked on EVERY wave without counting
|
|
38
|
-
* against `waves.ts`'s own request budget or per-provider concurrency cap —
|
|
39
|
-
* design section 3: "Bridge and PulseX hops are free local reads ... done
|
|
40
|
-
* exactly at any amount."
|
|
41
|
-
*
|
|
42
|
-
* CANONICAL HOME FOR THIS CHECK — PHASE 3. Moved here from `waves.ts` (which
|
|
43
|
-
* re-exports it) because {@link compareRoutePlansByDeliveredAmount}'s own
|
|
44
|
-
* tiebreak among unpriced plans needs it too (see that function's doc): a
|
|
45
|
-
* free-carrier first hop is guaranteed a real answer, a rate-limited one is
|
|
46
|
-
* not, and that distinction belongs on `HopCarrier` itself, not inside the
|
|
47
|
-
* one module that happened to need it first.
|
|
48
|
-
*
|
|
49
|
-
* @param carrier - the carrier to classify
|
|
50
|
-
* @returns true for `'lifi'`, `'relay'` or `'near-intents'`
|
|
51
|
-
*/
|
|
52
13
|
const isRateLimitedCarrier = (carrier) => registeredProviderIds.has(carrier);
|
|
53
14
|
exports.isRateLimitedCarrier = isRateLimitedCarrier;
|
|
54
|
-
/**
|
|
55
|
-
* The bases a ranking may compare a hop's `deliveredAmount` on — every
|
|
56
|
-
* {@link EstimateBasis} except `'unpriced'`. `'unpriced'` is EXCLUDED ON
|
|
57
|
-
* PURPOSE: it names a figure with no value behind it (see that member's own
|
|
58
|
-
* doc), and a ranking that compared on it would be comparing one path's real
|
|
59
|
-
* number against another path's decimal-shifted guess — exactly the defect
|
|
60
|
-
* shadow mode found. A caller checking whether a hop or a whole plan may
|
|
61
|
-
* enter a ranking reads this set (or calls {@link planIsRankable}), never a
|
|
62
|
-
* hand-rolled `!== 'unpriced'` check, so the one rule lives in one place.
|
|
63
|
-
*
|
|
64
|
-
* THIS IS A SEARCH-ORDER RULE, NOT A REPORTING RULE — PHASE 3, SECOND ROUND.
|
|
65
|
-
* `'priced-conversion'` stays in this set on purpose: `enumerate.ts`'s own
|
|
66
|
-
* cut and `waves.ts`'s between-wave top-candidate selection both need SOME
|
|
67
|
-
* way to compare two not-yet-quoted plans, and a price-feed conversion is a
|
|
68
|
-
* far better signal for THAT purpose than no signal at all. It is a
|
|
69
|
-
* DIFFERENT question whether a plan may be handed back to a reader as the
|
|
70
|
-
* winner while still carrying `'priced-conversion'` on any hop — it may not;
|
|
71
|
-
* see {@link isConfirmedBasis} and {@link planIsConfirmed}, the stricter pair
|
|
72
|
-
* `waves.ts`'s own final return step applies, for that rule and the live
|
|
73
|
-
* shadow-mode finding it closes (1 Zcash reported as 131.6 million Wrapped
|
|
74
|
-
* Pulse via `'priced-conversion'`, against a real 33.3 million from the
|
|
75
|
-
* pool's own `getAmountsOut` — a thin-pool price-impact gap `'priced-conversion'`
|
|
76
|
-
* cannot see, because it never asks the pool at all).
|
|
77
|
-
*/
|
|
78
15
|
exports.RANKABLE_ESTIMATE_BASES = new Set([
|
|
79
16
|
'probe-rate',
|
|
80
17
|
'priced-conversion',
|
|
81
18
|
'provider-model',
|
|
82
19
|
]);
|
|
83
|
-
/**
|
|
84
|
-
* Whether every hop of `hops` carries a figure a ranking may trust: a live
|
|
85
|
-
* quote, or an estimate whose {@link EstimateBasis} is in
|
|
86
|
-
* {@link RANKABLE_ESTIMATE_BASES} AND was never refused
|
|
87
|
-
* ({@link HopOutcome.refusalReason} is `null`/`undefined`). False the moment
|
|
88
|
-
* ANY hop is `'unpriced'` — one ungrounded hop taints the whole path's
|
|
89
|
-
* composed figure, because composition multiplies every hop's figure into
|
|
90
|
-
* the next (`compose.ts`), so a single meaningless number anywhere in the
|
|
91
|
-
* chain makes the FINAL delivered amount meaningless too, however real the
|
|
92
|
-
* other hops were.
|
|
93
|
-
*
|
|
94
|
-
* A REFUSED HOP IS UNRANKABLE EVEN WITH A GOOD BASIS. A hop that carries
|
|
95
|
-
* `'priced-conversion'` or `'probe-rate'` still fails this check once a real
|
|
96
|
-
* ask for it came back declined — see {@link HopOutcome.refusalReason}'s own
|
|
97
|
-
* doc for the shadow-mode finding this closes: a refused hop's leftover
|
|
98
|
-
* price-feed estimate has no fees behind it and kept beating real quotes,
|
|
99
|
-
* because a provider's own refusal was never checked before the hop's
|
|
100
|
-
* ORIGINAL basis was trusted again.
|
|
101
|
-
*
|
|
102
|
-
* A plan that fails this check is never dropped by this function — it is the
|
|
103
|
-
* CALLER's job to decide what "not yet known" means for it (excluded from
|
|
104
|
-
* being the ranked winner, but still held back in the list; see
|
|
105
|
-
* `waves.ts`'s and `useRouteSearch.ts`'s own comparators, both of which sort
|
|
106
|
-
* every unrankable plan below every rankable one rather than comparing raw
|
|
107
|
-
* amounts across the two groups).
|
|
108
|
-
*
|
|
109
|
-
* @param hops - the plan's hops
|
|
110
|
-
* @returns true when every hop's figure is trustworthy enough to rank on
|
|
111
|
-
*/
|
|
112
20
|
const planIsRankable = (hops) => hops.every((hop) => hop.outcome.kind === 'quote' ||
|
|
113
21
|
(exports.RANKABLE_ESTIMATE_BASES.has(hop.outcome.basis) && (hop.outcome.refusalReason ?? null) === null));
|
|
114
22
|
exports.planIsRankable = planIsRankable;
|
|
115
|
-
/** How much a ranking trusts each {@link ComposedPathBasis}, highest first — private to this module's own combination and rankability logic. */
|
|
116
23
|
const COMPOSED_PATH_BASIS_RANK = {
|
|
117
24
|
quoted: 3,
|
|
118
25
|
'probe-rate': 2,
|
|
119
26
|
'priced-conversion': 1,
|
|
120
|
-
// SAME TIER AS `'priced-conversion'`, ON PURPOSE — both are rankable (see
|
|
121
|
-
// `RANKABLE_ESTIMATE_BASES`) but neither ever asked a real provider for
|
|
122
|
-
// THIS edge, so neither may report as a confirmed winner
|
|
123
|
-
// (`isConfirmedBasis` requires rank 2 or higher, `probe-rate`'s own tier).
|
|
124
27
|
'provider-model': 1,
|
|
125
28
|
unpriced: 0,
|
|
126
29
|
};
|
|
127
|
-
/**
|
|
128
|
-
* Combines two hops' (or two paths') bases into the basis their COMPOSITION
|
|
129
|
-
* deserves: the WORSE of the two, never the better one and never an average.
|
|
130
|
-
* A composed figure is a chain — `hop2`'s output is computed FROM `hop1`'s
|
|
131
|
-
* output — so if either link is `'unpriced'`, the number at the far end
|
|
132
|
-
* carries no value regardless of how solid the other link was. "Worst wins"
|
|
133
|
-
* is the only rule that keeps that true; picking the better basis (or the
|
|
134
|
-
* LAST hop's own basis, which an earlier draft of this reasoning tried) would
|
|
135
|
-
* let one grounded hop launder an ungrounded one earlier in the same chain.
|
|
136
|
-
*
|
|
137
|
-
* @param a - one basis being combined
|
|
138
|
-
* @param b - the other
|
|
139
|
-
* @returns whichever of `a`/`b` a ranking trusts less
|
|
140
|
-
*/
|
|
141
30
|
const combineComposedPathBasis = (a, b) => (COMPOSED_PATH_BASIS_RANK[a] <= COMPOSED_PATH_BASIS_RANK[b] ? a : b);
|
|
142
31
|
exports.combineComposedPathBasis = combineComposedPathBasis;
|
|
143
|
-
/**
|
|
144
|
-
* Whether a ranking may compare on `basis` — true for every
|
|
145
|
-
* {@link ComposedPathBasis} except `'unpriced'`. The whole-path counterpart to
|
|
146
|
-
* {@link RANKABLE_ESTIMATE_BASES}; see that constant's doc for the rule both
|
|
147
|
-
* enforce.
|
|
148
|
-
*
|
|
149
|
-
* @param basis - the composed path basis to check
|
|
150
|
-
* @returns false only for `'unpriced'`
|
|
151
|
-
*/
|
|
152
32
|
const isRankableBasis = (basis) => basis !== 'unpriced';
|
|
153
33
|
exports.isRankableBasis = isRankableBasis;
|
|
154
|
-
/**
|
|
155
|
-
* Whether a ranking may report `basis` as a WINNER — true only for
|
|
156
|
-
* `'quoted'` and `'probe-rate'`, i.e. {@link COMPOSED_PATH_BASIS_RANK} of 2 or
|
|
157
|
-
* higher. FALSE for `'priced-conversion'`, even though {@link isRankableBasis}
|
|
158
|
-
* accepts it — see that function's own "SEARCH-ORDER RULE, NOT A REPORTING
|
|
159
|
-
* RULE" note for why the two disagree on that one basis, on purpose.
|
|
160
|
-
*
|
|
161
|
-
* THE DEFECT THIS CLOSES. A live shadow comparison found 1 Zcash routed to
|
|
162
|
-
* Pulse (Wrapped) through the Binance Smart Chain omnibridge reported 131.6
|
|
163
|
-
* million Wrapped Pulse on `'priced-conversion'` — a United States dollar
|
|
164
|
-
* price-feed conversion that never asks the destination PulseX pool at all
|
|
165
|
-
* — while the pool's OWN `getAmountsOut` answers 33.3 million for the same
|
|
166
|
-
* size (86,540 per unit at 1 unit, 13,121 per unit at 1,500 units: an 84.8%
|
|
167
|
-
* worse rate at real size). `isRankableBasis` treated the two bases as
|
|
168
|
-
* equally trustworthy, so the price-feed number stood as the reported
|
|
169
|
-
* winner. `'priced-conversion'` IGNORES PRICE IMPACT BY CONSTRUCTION
|
|
170
|
-
* (`buildEstimateHopOutput`, `packages/ui/src/lib/state/useRouteSearch.ts`,
|
|
171
|
-
* converts through a flat United States dollar rate on each side, with no
|
|
172
|
-
* pool, no depth and no size in the computation) — it is not merely
|
|
173
|
-
* unconfirmed, it is blind to the one thing that broke here, so no amount of
|
|
174
|
-
* "it is probably close enough" reasoning can rescue it.
|
|
175
|
-
*
|
|
176
|
-
* WHY `'probe-rate'` IS STILL ACCEPTED HERE. A hop's basis is `'probe-rate'`
|
|
177
|
-
* ONLY WHEN A REAL PROVIDER (or a free local read — `getAmountsOut` included)
|
|
178
|
-
* ANSWERED THAT EXACT EDGE — see {@link HopOutcome}'s own doc and
|
|
179
|
-
* `waves.ts`'s `recomposePath`, whose fallback branch always carries forward
|
|
180
|
-
* a hop's ORIGINAL, pre-network basis (`'priced-conversion'`/`'unpriced'`,
|
|
181
|
-
* NEVER `'probe-rate'`) rather than inventing one. So a `'probe-rate'` hop is
|
|
182
|
-
* always grounded in a real, size-aware answer for SOME amount — the one
|
|
183
|
-
* residual imprecision is that `waves.ts`'s early waves may have asked it at
|
|
184
|
-
* an amount close to, but not exactly, the plan's final fully-propagated
|
|
185
|
-
* figure (see `waves.ts`'s own "confirmation pass" doc for how the final
|
|
186
|
-
* return step narrows that gap for a plan's TOP candidates before this
|
|
187
|
-
* function is ever consulted). A real answer at a slightly-off size still
|
|
188
|
-
* beats a number no provider was ever asked for at any size.
|
|
189
|
-
*
|
|
190
|
-
* @param basis - the composed path basis to check
|
|
191
|
-
* @returns true only for `'quoted'` and `'probe-rate'`
|
|
192
|
-
*/
|
|
193
34
|
const isConfirmedBasis = (basis) => COMPOSED_PATH_BASIS_RANK[basis] >= COMPOSED_PATH_BASIS_RANK['probe-rate'];
|
|
194
35
|
exports.isConfirmedBasis = isConfirmedBasis;
|
|
195
|
-
/**
|
|
196
|
-
* What grounds a WHOLE PLAN's delivered figure, read directly off its hops'
|
|
197
|
-
* own `outcome`s — the WORST (least-grounded) {@link ComposedPathBasis} across
|
|
198
|
-
* every hop, folded with {@link combineComposedPathBasis} exactly as
|
|
199
|
-
* `compose.ts`'s `composeRouteDelivery` folds one while a path is being
|
|
200
|
-
* actively composed.
|
|
201
|
-
*
|
|
202
|
-
* USE THIS ONCE A PLAN'S HOPS ALREADY CARRY THEIR FINAL `outcome`S — a
|
|
203
|
-
* finished `enumerate.ts`/`waves.ts` plan — rather than re-deriving a fresh
|
|
204
|
-
* `ComposedRouteDelivery` from scratch. `buildRouteScoreInputsFromPlan`
|
|
205
|
-
* (`plan-route-data.ts`) is the first caller, feeding `scoreRoutePlan`
|
|
206
|
-
* (`score.ts`) the one figure it needs to refuse ever scoring an unpriced
|
|
207
|
-
* plan as if its `deliveredUsd` meant anything.
|
|
208
|
-
*
|
|
209
|
-
* AGREES WITH {@link planIsRankable} BY CONSTRUCTION, NEVER BY COINCIDENCE:
|
|
210
|
-
* `isRankableBasis(planComposedBasis(hops)) === planIsRankable(hops)` for
|
|
211
|
-
* every plan, because "worst wins" and "true unless any hop is unpriced" are
|
|
212
|
-
* the same rule read two ways — see `graph.test.ts`'s own test pinning that
|
|
213
|
-
* equivalence so the two can never quietly drift apart.
|
|
214
|
-
*
|
|
215
|
-
* A REFUSED HOP FOLDS AS `'unpriced'`, WHATEVER ITS OWN `basis` SAYS — the
|
|
216
|
-
* same rule {@link planIsRankable} applies, read through
|
|
217
|
-
* {@link combineComposedPathBasis}'s "worst wins": a hop a real provider has
|
|
218
|
-
* declined is worse-grounded than any {@link EstimateBasis}, refused or not.
|
|
219
|
-
* This is WHY {@link isRankableBasis}`(planComposedBasis(hops)) === planIsRankable(hops)`
|
|
220
|
-
* still holds for every plan — see this function's own "AGREES WITH..." note
|
|
221
|
-
* below.
|
|
222
|
-
*
|
|
223
|
-
* @param hops - the plan's hops
|
|
224
|
-
* @returns the plan's composed basis; `'quoted'` (the fold's identity value)
|
|
225
|
-
* for an empty plan
|
|
226
|
-
*/
|
|
227
36
|
const planComposedBasis = (hops) => hops.reduce((basis, hop) => (0, exports.combineComposedPathBasis)(basis, (0, exports.hopComposedBasis)(hop)), 'quoted');
|
|
228
37
|
exports.planComposedBasis = planComposedBasis;
|
|
229
|
-
/**
|
|
230
|
-
* The {@link ComposedPathBasis} one hop contributes to
|
|
231
|
-
* {@link planComposedBasis}'s fold: `'quoted'` for a live quote, `'unpriced'`
|
|
232
|
-
* for a REFUSED estimate regardless of its own `basis` (see
|
|
233
|
-
* {@link planComposedBasis}'s own "REFUSED HOP" note), else the estimate's
|
|
234
|
-
* own `basis`.
|
|
235
|
-
*
|
|
236
|
-
* EXPORTED FOR `waves.ts`'s OWN "confirmation pass" — the final step,
|
|
237
|
-
* described on {@link isConfirmedBasis}'s own doc, that upgrades a
|
|
238
|
-
* TOP-ranked plan's still-estimated free-carrier hops to a real quote before
|
|
239
|
-
* the search returns. That pass needs the SAME per-hop fold this function
|
|
240
|
-
* already applies (a hop a real provider has since refused is no better
|
|
241
|
-
* grounded than one that was never asked) to decide WHICH hops still need
|
|
242
|
-
* upgrading, so it reuses this rather than re-deriving the fold by hand.
|
|
243
|
-
*/
|
|
244
38
|
const hopComposedBasis = (hop) => {
|
|
245
39
|
if (hop.outcome.kind === 'quote')
|
|
246
40
|
return 'quoted';
|
|
@@ -249,93 +43,15 @@ const hopComposedBasis = (hop) => {
|
|
|
249
43
|
return hop.outcome.basis;
|
|
250
44
|
};
|
|
251
45
|
exports.hopComposedBasis = hopComposedBasis;
|
|
252
|
-
/**
|
|
253
|
-
* Whether every hop of `hops` carries a CONFIRMED figure — read straight off
|
|
254
|
-
* {@link planComposedBasis} through {@link isConfirmedBasis}, so this agrees
|
|
255
|
-
* with that function BY CONSTRUCTION, never by coincidence, exactly as
|
|
256
|
-
* {@link planIsRankable} agrees with {@link isRankableBasis} (see that pair's
|
|
257
|
-
* own "AGREES WITH" note above). STRICTER than {@link planIsRankable}: every
|
|
258
|
-
* confirmed plan is rankable, but a rankable plan carrying `'priced-conversion'`
|
|
259
|
-
* on any hop is not confirmed — see {@link isConfirmedBasis}'s own doc for why.
|
|
260
|
-
*
|
|
261
|
-
* USE THIS TO DECIDE WHAT A SEARCH MAY REPORT AS A WINNER. `waves.ts`'s own
|
|
262
|
-
* final return step is the one caller that PARTITIONS on this (confirmed
|
|
263
|
-
* plans ahead of unconfirmed ones, each group's own search-order preserved)
|
|
264
|
-
* — see that module's "confirmation pass" doc. NEVER use this to prune or
|
|
265
|
-
* rank DURING search: `planIsRankable` is still the correct predicate for
|
|
266
|
-
* `enumerate.ts`'s cut and `waves.ts`'s between-wave top-candidate
|
|
267
|
-
* selection, so `'priced-conversion'` keeps doing its job there exactly as
|
|
268
|
-
* before — see {@link RANKABLE_ESTIMATE_BASES}'s own "SEARCH-ORDER RULE, NOT
|
|
269
|
-
* A REPORTING RULE" note.
|
|
270
|
-
*
|
|
271
|
-
* @param hops - the plan's hops
|
|
272
|
-
* @returns true when every hop's figure is confirmed by a real ask
|
|
273
|
-
*/
|
|
274
46
|
const planIsConfirmed = (hops) => (0, exports.isConfirmedBasis)((0, exports.planComposedBasis)(hops));
|
|
275
47
|
exports.planIsConfirmed = planIsConfirmed;
|
|
276
|
-
/**
|
|
277
|
-
* Encodes one string as a length-prefixed ("netstring") segment: `"3:abc"`.
|
|
278
|
-
*
|
|
279
|
-
* WHY THIS AND NOT A DELIMITER. Joining segments with a fixed delimiter
|
|
280
|
-
* (`"|"`, `":"`, ...) collides whenever a segment can itself contain that
|
|
281
|
-
* delimiter — and a `HopEdge` key is built FROM asset addresses, which this
|
|
282
|
-
* module does not control the alphabet of. Prefixing each segment with its
|
|
283
|
-
* own length makes the boundary self-describing: reconstructing the segment
|
|
284
|
-
* count from the encoded string requires no assumption about what characters
|
|
285
|
-
* appear inside a segment, so two different segment sequences can never
|
|
286
|
-
* encode to the same string. See `graph.test.ts` for a worked collision that
|
|
287
|
-
* a plain delimiter join would produce and this does not.
|
|
288
|
-
*
|
|
289
|
-
* @param value - the segment to encode
|
|
290
|
-
* @returns the length-prefixed segment
|
|
291
|
-
*/
|
|
292
48
|
const netstringEncode = (value) => `${value.length}:${value}`;
|
|
293
|
-
/**
|
|
294
|
-
* The stable, collision-free key for one {@link HopEdge}.
|
|
295
|
-
*
|
|
296
|
-
* The {@link HopEdge.venue}, when present, is a fourth segment: two venues
|
|
297
|
-
* between the same assets are two edges, so they must never share a key (a
|
|
298
|
-
* path deduplication or an answer cache would otherwise merge them). An edge
|
|
299
|
-
* with no venue keeps its three-segment key unchanged.
|
|
300
|
-
*
|
|
301
|
-
* @param edge - the edge to key
|
|
302
|
-
* @returns the edge's key, built from its carrier, its two node keys, and its venue when it has one
|
|
303
|
-
*/
|
|
304
49
|
const hopEdgeKey = (edge) => [edge.carrier, (0, exports.assetNodeKey)(edge.from), (0, exports.assetNodeKey)(edge.to), ...(edge.venue === undefined ? [] : [edge.venue])]
|
|
305
50
|
.map(netstringEncode)
|
|
306
51
|
.join('');
|
|
307
52
|
exports.hopEdgeKey = hopEdgeKey;
|
|
308
|
-
/**
|
|
309
|
-
* The stable, collision-free key for a whole path — the value
|
|
310
|
-
* {@link RoutePlan.pathKey} must hold.
|
|
311
|
-
*
|
|
312
|
-
* Two DIFFERENT hop sequences never produce the same key, and the SAME
|
|
313
|
-
* sequence always produces the same key, because each hop's key is
|
|
314
|
-
* length-prefixed before joining (see {@link netstringEncode}). This is what
|
|
315
|
-
* makes the key usable for deduplicating plans found by different search
|
|
316
|
-
* paths that happen to walk the same edges.
|
|
317
|
-
*
|
|
318
|
-
* @param hops - the plan's hops, first to last
|
|
319
|
-
* @returns the path's key
|
|
320
|
-
*/
|
|
321
53
|
const routePlanPathKey = (hops) => hops.map((hop) => netstringEncode((0, exports.hopEdgeKey)(hop.edge))).join('');
|
|
322
54
|
exports.routePlanPathKey = routePlanPathKey;
|
|
323
|
-
/**
|
|
324
|
-
* Groups a plan's hops into signature groups, as hop indices.
|
|
325
|
-
*
|
|
326
|
-
* THE ONE RULE: a hop whose PREDECESSOR fused it (`fusesNext` was true on the
|
|
327
|
-
* hop before it) joins that predecessor's group; every other hop — including
|
|
328
|
-
* the first — starts a new one. A pay-by-hand hop needs no special case
|
|
329
|
-
* either: {@link HopEdge.origin} only says how a hop is FUNDED, not whether it
|
|
330
|
-
* groups with what follows, and design section 1 is explicit that a
|
|
331
|
-
* pay-by-hand deposit "counts as one reader action but needs no wallet
|
|
332
|
-
* signature" — a fact for the ACTION count Phase 1's `score.ts` will compute
|
|
333
|
-
* (design section 4, the `A` term), not a fact about signature grouping.
|
|
334
|
-
*
|
|
335
|
-
* @param hops - the plan's hops, first to last
|
|
336
|
-
* @returns the hop index groups, in signature order — the outer array's
|
|
337
|
-
* length is the plan's signature count
|
|
338
|
-
*/
|
|
339
55
|
const groupHopsBySignature = (hops) => {
|
|
340
56
|
const groups = [];
|
|
341
57
|
hops.forEach((_currentHop, index) => {
|
|
@@ -350,57 +66,10 @@ const groupHopsBySignature = (hops) => {
|
|
|
350
66
|
return groups;
|
|
351
67
|
};
|
|
352
68
|
exports.groupHopsBySignature = groupHopsBySignature;
|
|
353
|
-
/**
|
|
354
|
-
* Whether `plan`'s FIRST hop is a FREE carrier (the omnibridge crossing or a
|
|
355
|
-
* PulseX swap) rather than a rate-limited aggregator ({@link isRateLimitedCarrier}).
|
|
356
|
-
* A free first hop is asked on every wave of `waves.ts`'s own search
|
|
357
|
-
* unconditionally — never skipped for a spent request budget — so a plan
|
|
358
|
-
* with one is guaranteed a real answer; a rate-limited first hop competes
|
|
359
|
-
* with every other rate-limited ask for `WAVE_0_MAX_REQUESTS` and can be
|
|
360
|
-
* silently skipped when the budget runs out before it. {@link compareUnrankablePlans}
|
|
361
|
-
* is the one place this distinguishes two plans neither of which has a
|
|
362
|
-
* trustworthy figure yet.
|
|
363
|
-
*
|
|
364
|
-
* @param plan - the plan to classify
|
|
365
|
-
* @returns true when the plan's first hop is free, false for a rate-limited
|
|
366
|
-
* first hop or an empty plan
|
|
367
|
-
*/
|
|
368
69
|
const firstHopIsFreeCarrier = (plan) => {
|
|
369
70
|
const firstHop = plan.hops[0];
|
|
370
71
|
return firstHop !== undefined && !(0, exports.isRateLimitedCarrier)(firstHop.edge.carrier);
|
|
371
72
|
};
|
|
372
|
-
/**
|
|
373
|
-
* Orders two UNRANKABLE plans (both fail {@link planIsRankable}) — PHASE 3,
|
|
374
|
-
* closing the gap `compareRoutePlansByDeliveredAmount` left: within this
|
|
375
|
-
* group `deliveredAmount` is not a real signal at all, it is a bare decimal
|
|
376
|
-
* rescale with no value behind it (`EstimateBasis`'s own doc), so an earlier
|
|
377
|
-
* version of that comparator fell back to comparing it anyway — an EMERGENT
|
|
378
|
-
* tiebreak that happened to work only because nothing else was tried. That
|
|
379
|
-
* let an entirely unrelated, equally-ungrounded plan win the enumeration
|
|
380
|
-
* cut's remaining slots (and `waves.ts`'s wave-0 request-budget priority,
|
|
381
|
-
* which reads plans in this same order) purely because its decimal point
|
|
382
|
-
* happened to land somewhere bigger — discarding, in the worst case, the
|
|
383
|
-
* price-feed-blind but perfectly real DIRECT omnibridge crossing in favour
|
|
384
|
-
* of aggregator noise.
|
|
385
|
-
*
|
|
386
|
-
* THE EXPLICIT RESERVATION, IN RANK ORDER:
|
|
387
|
-
*
|
|
388
|
-
* 1. A plan whose first hop is a FREE carrier ({@link firstHopIsFreeCarrier})
|
|
389
|
-
* sorts ahead of one whose first hop is rate-limited — a free hop is
|
|
390
|
-
* GUARANTEED a real wave-0 answer; a rate-limited one is only a maybe,
|
|
391
|
-
* gated by a shared, exhaustible budget.
|
|
392
|
-
* 2. Within the same free-or-not group, fewer hops sorts first — a shorter
|
|
393
|
-
* plan costs less of that shared budget to try and is simpler to execute,
|
|
394
|
-
* matching this search's own hop/signature caps (design section 2).
|
|
395
|
-
* 3. A final, purely COSMETIC tiebreak — the plan's own stable `pathKey`,
|
|
396
|
-
* lexicographically — keeps the comparator total and deterministic.
|
|
397
|
-
* Never `deliveredAmount`: see this function's own head note for why.
|
|
398
|
-
*
|
|
399
|
-
* @param a - one unrankable plan being compared
|
|
400
|
-
* @param b - the other
|
|
401
|
-
* @returns negative when `a` ranks ahead of `b`, positive when `b` ranks
|
|
402
|
-
* ahead of `a`, zero when they tie
|
|
403
|
-
*/
|
|
404
73
|
const compareUnrankablePlans = (a, b) => {
|
|
405
74
|
const freeFirstHopA = firstHopIsFreeCarrier(a);
|
|
406
75
|
const freeFirstHopB = firstHopIsFreeCarrier(b);
|
|
@@ -412,39 +81,6 @@ const compareUnrankablePlans = (a, b) => {
|
|
|
412
81
|
return 0;
|
|
413
82
|
return a.pathKey < b.pathKey ? -1 : 1;
|
|
414
83
|
};
|
|
415
|
-
/**
|
|
416
|
-
* THE ONE PLACE THIS COMPARATOR IS WRITTEN. Every rankable plan
|
|
417
|
-
* ({@link planIsRankable}) sorts ahead of every unrankable one, whatever its
|
|
418
|
-
* raw `deliveredAmount` claims to be — a QUOTABLE plan never loses its slot
|
|
419
|
-
* to one that is not, at the enumeration cut or anywhere else this
|
|
420
|
-
* comparator runs. Within the same rankability group: two RANKABLE plans
|
|
421
|
-
* compare on the larger last-hop delivered amount, a real signal; two
|
|
422
|
-
* UNRANKABLE plans compare with {@link compareUnrankablePlans} instead, per
|
|
423
|
-
* that function's own doc, because `deliveredAmount` carries no signal for
|
|
424
|
-
* that group at all. Bigint-safe throughout for the rankable branch — never
|
|
425
|
-
* widens to `Number`, which would lose precision on a large delivered amount
|
|
426
|
-
* long before the comparison needed it to.
|
|
427
|
-
*
|
|
428
|
-
* IMPORTED, NEVER RESTATED — PHASE 3's OWN CLOSE-THE-GAP RULE. Three
|
|
429
|
-
* independent call sites need exactly this rule: `enumerate.ts`'s own
|
|
430
|
-
* default (its top-N keep, applied to freshly enumerated plans — the SAME
|
|
431
|
-
* cut that decides which plans even reach `waves.ts`'s request budget),
|
|
432
|
-
* `waves.ts`'s default (its between-wave re-ranking), and
|
|
433
|
-
* `packages/ui/src/lib/state/useRouteSearch.ts`'s final sort. Before this
|
|
434
|
-
* function existed, two of those three each carried their own hand-written
|
|
435
|
-
* copy of the identical rule — a DRY violation that is also a correctness
|
|
436
|
-
* risk: a future edit to one copy (say, widening `RANKABLE_ESTIMATE_BASES`)
|
|
437
|
-
* could silently fail to reach the other, reopening the exact defect shadow
|
|
438
|
-
* mode measured (an unpriced 276 PLS outranking a priced 23 million PLS).
|
|
439
|
-
* Importing this one function everywhere a plan-to-plan comparison happens
|
|
440
|
-
* is what makes that drift structurally impossible rather than merely
|
|
441
|
-
* against convention.
|
|
442
|
-
*
|
|
443
|
-
* @param a - one plan being compared
|
|
444
|
-
* @param b - the other
|
|
445
|
-
* @returns negative when `a` ranks ahead of `b`, positive when `b` ranks
|
|
446
|
-
* ahead of `a`, zero when they tie
|
|
447
|
-
*/
|
|
448
84
|
const compareRoutePlansByDeliveredAmount = (a, b) => {
|
|
449
85
|
const rankableA = (0, exports.planIsRankable)(a.hops);
|
|
450
86
|
const rankableB = (0, exports.planIsRankable)(b.hops);
|