@gibs/bridge-sdk 1.12.0 → 1.13.1

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.
Files changed (118) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +234 -432
  3. package/dist/abis.d.ts +1 -0
  4. package/dist/bridge-limits.d.ts +103 -0
  5. package/dist/bridge-limits.js +84 -0
  6. package/dist/chain-info.d.ts +169 -0
  7. package/dist/chain-info.js +410 -0
  8. package/dist/chain-names.d.ts +160 -0
  9. package/dist/chain-names.js +200 -0
  10. package/dist/chains.d.ts +65 -0
  11. package/dist/chains.js +284 -0
  12. package/dist/config.d.ts +216 -0
  13. package/dist/config.js +493 -0
  14. package/dist/consolidated-quote.d.ts +554 -0
  15. package/dist/consolidated-quote.js +773 -0
  16. package/dist/destination-calldata.d.ts +118 -0
  17. package/dist/destination-calldata.js +152 -0
  18. package/dist/ecosystems.d.ts +217 -0
  19. package/dist/ecosystems.js +288 -0
  20. package/dist/fee-type.d.ts +10 -0
  21. package/dist/fee-type.js +9 -0
  22. package/dist/hyperliquid-execution.d.ts +152 -0
  23. package/dist/hyperliquid-execution.js +168 -0
  24. package/dist/image-links.d.ts +117 -0
  25. package/dist/image-links.js +142 -0
  26. package/dist/index.d.ts +20 -0
  27. package/dist/index.js +20 -0
  28. package/dist/lifi-execution-guard.d.ts +199 -0
  29. package/dist/lifi-execution-guard.js +400 -0
  30. package/dist/near-intents-conformance.d.ts +85 -0
  31. package/dist/near-intents-conformance.js +329 -0
  32. package/dist/near-intents-route.d.ts +47 -0
  33. package/dist/near-intents-route.js +265 -0
  34. package/dist/near-intents.d.ts +524 -0
  35. package/dist/near-intents.js +1055 -0
  36. package/dist/origin-deposit.d.ts +326 -0
  37. package/dist/origin-deposit.js +68 -0
  38. package/dist/pre-quote.d.ts +198 -0
  39. package/dist/pre-quote.js +175 -0
  40. package/dist/prices.d.ts +107 -0
  41. package/dist/prices.js +162 -0
  42. package/dist/providers.d.ts +221 -0
  43. package/dist/providers.js +157 -0
  44. package/dist/quote-fees.d.ts +102 -0
  45. package/dist/quote-fees.js +195 -0
  46. package/dist/referral.d.ts +77 -0
  47. package/dist/referral.js +72 -0
  48. package/dist/relay-currencies.d.ts +47 -0
  49. package/dist/relay-currencies.js +89 -0
  50. package/dist/relay-quote.d.ts +301 -0
  51. package/dist/relay-quote.js +983 -0
  52. package/dist/route-steps.d.ts +138 -0
  53. package/dist/route-steps.js +305 -0
  54. package/dist/routing/amounts.d.ts +80 -0
  55. package/dist/routing/amounts.js +120 -0
  56. package/dist/routing/candidate-route.d.ts +43 -0
  57. package/dist/routing/candidate-route.js +45 -0
  58. package/dist/routing/candidate.d.ts +237 -0
  59. package/dist/routing/candidate.js +96 -0
  60. package/dist/routing/carriers/ask-answer.d.ts +60 -0
  61. package/dist/routing/carriers/ask-answer.js +1 -0
  62. package/dist/routing/carriers/omnibridge.d.ts +137 -0
  63. package/dist/routing/carriers/omnibridge.js +419 -0
  64. package/dist/routing/carriers/pulsex.d.ts +149 -0
  65. package/dist/routing/carriers/pulsex.js +453 -0
  66. package/dist/routing/carriers/wrap.d.ts +25 -0
  67. package/dist/routing/carriers/wrap.js +115 -0
  68. package/dist/routing/composed-route-quality.d.ts +103 -0
  69. package/dist/routing/composed-route-quality.js +108 -0
  70. package/dist/routing/entry-chain-anchor.d.ts +88 -0
  71. package/dist/routing/entry-chain-anchor.js +55 -0
  72. package/dist/routing/format.d.ts +31 -0
  73. package/dist/routing/format.js +58 -0
  74. package/dist/routing/index.d.ts +34 -0
  75. package/dist/routing/index.js +42 -0
  76. package/dist/routing/producers.d.ts +770 -0
  77. package/dist/routing/producers.js +1564 -0
  78. package/dist/routing/providers.d.ts +26 -0
  79. package/dist/routing/providers.js +38 -0
  80. package/dist/routing/route-data.d.ts +355 -0
  81. package/dist/routing/route-data.js +1 -0
  82. package/dist/routing/search/compose.d.ts +168 -0
  83. package/dist/routing/search/compose.js +98 -0
  84. package/dist/routing/search/graph.d.ts +653 -0
  85. package/dist/routing/search/graph.js +445 -0
  86. package/dist/routing/search/plan-route-data.d.ts +244 -0
  87. package/dist/routing/search/plan-route-data.js +767 -0
  88. package/dist/routing/search/score.d.ts +353 -0
  89. package/dist/routing/search/score.js +427 -0
  90. package/dist/routing/usd-value.d.ts +17 -0
  91. package/dist/routing/usd-value.js +25 -0
  92. package/dist/settings.d.ts +692 -0
  93. package/dist/settings.js +909 -0
  94. package/dist/ton-execution.d.ts +117 -0
  95. package/dist/ton-execution.js +247 -0
  96. package/dist/tron-execution.d.ts +146 -0
  97. package/dist/tron-execution.js +239 -0
  98. package/dist/types.d.ts +51 -0
  99. package/dist/types.js +1 -0
  100. package/llms.txt +169 -0
  101. package/package.json +204 -23
  102. package/src/chain-info.test.ts +0 -968
  103. package/src/chain-info.ts +0 -498
  104. package/src/chains.test.ts +0 -123
  105. package/src/chains.ts +0 -88
  106. package/src/config.test.ts +0 -679
  107. package/src/config.ts +0 -500
  108. package/src/fee-type.test.ts +0 -45
  109. package/src/fee-type.ts +0 -14
  110. package/src/image-links.test.ts +0 -160
  111. package/src/image-links.ts +0 -65
  112. package/src/index.ts +0 -8
  113. package/src/settings.test.ts +0 -2754
  114. package/src/settings.ts +0 -875
  115. package/src/types.ts +0 -48
  116. package/tsconfig.json +0 -17
  117. package/vitest.config.ts +0 -13
  118. /package/{src/abis.ts → dist/abis.js} +0 -0
@@ -0,0 +1,427 @@
1
+ import { isNativeAsset } from '../../ecosystems.js';
2
+ import { compareRoutePlansByDeliveredAmount, planIsRankable, isRankableBasis, } from './graph.js';
3
+ /**
4
+ * PHASE 0 — FOUNDATION ONLY. Today this module holds exactly one function,
5
+ * {@link waitPenaltyPpm}, moved here unchanged from
6
+ * `packages/ui/src/lib/stores/consolidated-quote.ts` (that file now imports
7
+ * and re-exports it, so every existing caller keeps working). Phase 1's
8
+ * `score.ts` work (design section 4) generalizes `candidateNetValueUsd` and
9
+ * `netValueAfterWaitPenalty` from `../producers.js` and
10
+ * `packages/ui/src/lib/state/useRouteOptions.ts` into one `scoreRoutePlan`
11
+ * function that lives here beside it.
12
+ *
13
+ * WHY THIS MOVES TO `bridge-sdk` AT ALL. `waitPenaltyPpm` is a pure function
14
+ * of a number — it has no dependency on React, a store, or any
15
+ * user-interface concern — and `scoreRoutePlan` will need it to score a
16
+ * `RoutePlan` (`../graph.js`), which is a `bridge-sdk` type. Leaving the
17
+ * function in the user-interface package would make `bridge-sdk`'s own
18
+ * scoring logic import FROM the application that is supposed to consume it.
19
+ */
20
+ /**
21
+ * The share of the transfer a full minute of extra waiting is charged, in
22
+ * parts per million — 5,000 ppm is half of one percent.
23
+ *
24
+ * WHERE THE NUMBER COMES FROM. The owner's own crossing: LI.FI (through
25
+ * squid) claimed twenty seconds and Relay claimed two, and the strip
26
+ * preferred LI.FI because it delivered 0.075% more — nine cents on a
27
+ * hundred and twenty-six dollars. The transfer was still unsettled
28
+ * twenty-five minutes later. A second live measurement on Base to Polygon:
29
+ * twenty-nine seconds against three, for 0.0545%. Both are the same shape —
30
+ * a few cents of advantage bought with a ten-fold increase in the wait —
31
+ * and under this rate both go the other way: at eighteen seconds of extra
32
+ * claimed wait a route must deliver 0.15% more to win, and at twenty-six
33
+ * seconds it must deliver 0.217% more. Neither does.
34
+ *
35
+ * The rate is also calibrated NOT to override a real price advantage. A
36
+ * route that is a full minute slower still wins on 0.6% more delivered,
37
+ * which on a hundred-and-twenty-six-dollar transfer is seventy-six cents —
38
+ * a figure worth waiting a minute for, where nine cents is not.
39
+ */
40
+ const WAIT_PENALTY_PPM_PER_MINUTE = 5_000;
41
+ /** The wait beyond which extra claimed seconds stop adding to the penalty. */
42
+ const WAIT_PENALTY_CEILING_SECONDS = 60;
43
+ /**
44
+ * What a quote's CLAIMED duration costs it in the ranking, as a share of the
45
+ * transfer in parts per million. It ramps linearly to
46
+ * {@link WAIT_PENALTY_PPM_PER_MINUTE} at {@link WAIT_PENALTY_CEILING_SECONDS}
47
+ * and stops there.
48
+ *
49
+ * THE DURATION IS A CLAIM AND IS TREATED AS ONE. The route that prompted
50
+ * this ran about seventy-five times its provider's own estimate, so the
51
+ * figure cannot be trusted as a measurement of anything. It is still the
52
+ * only signal there is, and it is informative in one direction: a provider
53
+ * that says it will take longer is not going to turn out faster, and a
54
+ * claimed wait understates the real one far more often than it overstates
55
+ * it. So the penalty is used to demand a real price advantage before ANY
56
+ * extra claimed wait is accepted — never to predict a settlement time, and
57
+ * never to produce a number shown to the reader. Nothing on screen is
58
+ * adjusted by this; only the order is.
59
+ *
60
+ * The ceiling is what keeps the rule from becoming a speed preference. Past
61
+ * a minute the penalty stops growing, so a route that is genuinely worth
62
+ * waiting for still wins on the strength of its price.
63
+ *
64
+ * @param durationSeconds - the provider's own claimed execution duration
65
+ * @returns the penalty as parts per million of the transfer, 0 for a missing claim
66
+ */
67
+ export const waitPenaltyPpm = (durationSeconds) => {
68
+ if (typeof durationSeconds !== 'number' || !Number.isFinite(durationSeconds))
69
+ return 0;
70
+ if (durationSeconds <= 0)
71
+ return 0;
72
+ const rampedSeconds = Math.min(durationSeconds, WAIT_PENALTY_CEILING_SECONDS);
73
+ return Math.round((WAIT_PENALTY_PPM_PER_MINUTE * rampedSeconds) / WAIT_PENALTY_CEILING_SECONDS);
74
+ };
75
+ // ---------------------------------------------------------------------------
76
+ // scoreRoutePlan — the one maximization function
77
+ // ---------------------------------------------------------------------------
78
+ /**
79
+ * The share of the transfer charged for EACH reader action beyond the first,
80
+ * in parts per million — see {@link signaturePenaltyUsd}.
81
+ *
82
+ * THIS IS A PROPOSAL, NOT A MEASUREMENT. Unlike {@link WAIT_PENALTY_PPM_PER_MINUTE},
83
+ * which is calibrated against two logged, live crossings, no comparable
84
+ * signature-count measurement exists yet. 2,500 ppm (0.25% of the delivered
85
+ * amount) is a starting point sized to sit BELOW the wait penalty's own
86
+ * ceiling (5,000 ppm) — a second signature should cost less than the worst
87
+ * wait penalty already accepted, since a signature is a moment of the
88
+ * reader's attention, not twenty-five minutes of theirs. The owner is meant
89
+ * to calibrate this against real reader behavior once it ships.
90
+ */
91
+ export const SIGNATURE_PENALTY_PPM = 2_500;
92
+ /**
93
+ * SUPERSEDED FOR THE SEARCH ENGINE'S OWN RANKING — see
94
+ * {@link compareRoutePlansByGasAdjustedDeliveredAmount}, below, which
95
+ * `waves.ts` now uses in place of {@link compareRoutePlansByScoreAdjustedDeliveredAmount}
96
+ * once real gas data exists. A flat parts-per-million guess cannot tell a
97
+ * cheap chain from an expensive one, and it charges nothing for gas at all —
98
+ * the owner's own instruction: "it's custom for each provider ... gather the
99
+ * logic around how much a user is about to be charged." This constant, and
100
+ * {@link GAS_TOPUP_PENALTY_PPM} below, are KEPT UNCHANGED because
101
+ * {@link scoreRoutePlan} and every existing United States dollar-based
102
+ * caller of it (`plan-route-data.ts`, `producers.ts`, the user interface's
103
+ * own single-candidate scoring) still read them — migrating those callers to
104
+ * a real gas-cost model is FLAGGED HERE as follow-up work outside this
105
+ * module's own owned scope, not done silently in this change.
106
+ */
107
+ /**
108
+ * The minimum a signature is ever charged, in United States dollars — see
109
+ * {@link signaturePenaltyUsd}.
110
+ *
111
+ * THIS IS A PROPOSAL, NOT A MEASUREMENT. Without a floor, {@link SIGNATURE_PENALTY_PPM}
112
+ * charges almost nothing on a small transfer (0.25% of ten dollars is two and
113
+ * a half cents), which would let a route add a whole extra signature for a
114
+ * fraction of a cent. One dollar is a guess at what a reader's own time and
115
+ * attention for one extra wallet prompt is worth, floored so it cannot be
116
+ * bought away by a large delivered amount either — the floor and the rate are
117
+ * a `max`, so whichever is larger applies. The owner is meant to calibrate
118
+ * this once real reader behavior is observed.
119
+ */
120
+ export const SIGNATURE_PENALTY_FLOOR_USD = 1;
121
+ /**
122
+ * The share of the transfer charged for EACH intermediate chain where the
123
+ * reader holds no gas, in parts per million — see {@link gasTopUpPenaltyUsd}.
124
+ *
125
+ * THIS IS A PROPOSAL, NOT A MEASUREMENT. Sized larger than
126
+ * {@link SIGNATURE_PENALTY_PPM} because a gas top-up is not a moment of
127
+ * attention like a signature — it is a SEPARATE errand: acquire the
128
+ * intermediate chain's native coin, by another bridge or a purchase, before
129
+ * the planned hop can even be signed. That errand risks stranding the
130
+ * transfer mid-route if the reader does not complete it, which the
131
+ * "stranded funds" risk in the route-search design's section 10 names
132
+ * directly. The owner is meant to calibrate this once real reader behavior
133
+ * is observed.
134
+ */
135
+ export const GAS_TOPUP_PENALTY_PPM = 10_000;
136
+ /**
137
+ * The minimum a gas top-up is ever charged, in United States dollars — see
138
+ * {@link gasTopUpPenaltyUsd}.
139
+ *
140
+ * THIS IS A PROPOSAL, NOT A MEASUREMENT. Set higher than
141
+ * {@link SIGNATURE_PENALTY_FLOOR_USD} for the same reason
142
+ * {@link GAS_TOPUP_PENALTY_PPM} is: this penalty stands in for a separate
143
+ * errand with its own real cost (buying gas on a chain the reader was not
144
+ * already using), not a moment of attention. The owner is meant to calibrate
145
+ * this once real reader behavior is observed.
146
+ */
147
+ export const GAS_TOPUP_PENALTY_FLOOR_USD = 3;
148
+ /**
149
+ * What one extra reader action (a signature or a pay-by-hand deposit) costs
150
+ * in the ranking, in United States dollars: the larger of
151
+ * {@link SIGNATURE_PENALTY_FLOOR_USD} and {@link SIGNATURE_PENALTY_PPM} of
152
+ * the delivered amount. See both constants' docs for where the numbers come
153
+ * from and that they are proposals, not measurements.
154
+ *
155
+ * @param deliveredUsd - the route's delivered value in United States dollars
156
+ * @returns the penalty for one extra action, always positive
157
+ */
158
+ const signaturePenaltyUsd = (deliveredUsd) => Math.max(SIGNATURE_PENALTY_FLOOR_USD, (deliveredUsd * SIGNATURE_PENALTY_PPM) / 1_000_000);
159
+ /**
160
+ * What one intermediate chain with no reader gas costs in the ranking, in
161
+ * United States dollars: the larger of {@link GAS_TOPUP_PENALTY_FLOOR_USD}
162
+ * and {@link GAS_TOPUP_PENALTY_PPM} of the delivered amount. See both
163
+ * constants' docs for where the numbers come from and that they are
164
+ * proposals, not measurements.
165
+ *
166
+ * @param deliveredUsd - the route's delivered value in United States dollars
167
+ * @returns the penalty for one gasless intermediate chain, always positive
168
+ */
169
+ const gasTopUpPenaltyUsd = (deliveredUsd) => Math.max(GAS_TOPUP_PENALTY_FLOOR_USD, (deliveredUsd * GAS_TOPUP_PENALTY_PPM) / 1_000_000);
170
+ /**
171
+ * The one maximization function a route search ranks every plan by —
172
+ * generalizing today's `candidateNetValueUsd` and `netValueAfterWaitPenalty`
173
+ * (`../producers.js` and `packages/ui/src/lib/state/useRouteOptions.ts`)
174
+ * from a single quoted candidate to a whole {@link RouteScoreInputs} plan, per
175
+ * the route-search design's section 4:
176
+ *
177
+ * ```
178
+ * score = D − P − F
179
+ * − Σ_legs D·waitPenaltyPpm(claimedSeconds_leg)/1e6
180
+ * − (A − 1)·max($1, D·SIGNATURE_PENALTY_PPM/1e6)
181
+ * − G·max($3, D·GAS_TOPUP_PENALTY_PPM/1e6)
182
+ * ```
183
+ *
184
+ * A HIGHER SCORE IS BETTER. A route search picks the plan with the largest
185
+ * `scoreRoutePlan` result, exactly as today's ranking picks the candidate
186
+ * with the largest `netValueAfterWaitPenalty`.
187
+ *
188
+ * THE WAIT PENALTY IS UNCHANGED, JUST SUMMED ACROSS LEGS. Today's single-quote
189
+ * penalty is `deliveredUsd * waitPenaltyPpm(oneClaimedDuration) / 1e6`, one
190
+ * term. A plan can carry several hops, each with its own claimed duration, so
191
+ * this sums that same per-unit charge over every hop in
192
+ * {@link RouteScoreInputs.legClaimedDurationsSeconds} — still against the
193
+ * PLAN'S total delivered value, not each leg's own, because the reader is
194
+ * charged for the whole route's wait, not a fraction of it per leg.
195
+ *
196
+ * WHAT IS NEW: THE ACTION AND GAS-TOP-UP TERMS. Today's ranking only ever
197
+ * compared plans that cost one signature, so it had nothing to charge for a
198
+ * second one. A general search can find a plan needing two or three actions,
199
+ * or one that strands the reader without gas on an intermediate chain — see
200
+ * `signaturePenaltyUsd` and `gasTopUpPenaltyUsd` for what each extra action
201
+ * or gasless chain costs, and {@link SIGNATURE_PENALTY_PPM} /
202
+ * {@link GAS_TOPUP_PENALTY_PPM} for where the rates come from.
203
+ *
204
+ * WHY `(A − 1)` AND NOT `A`. Every plan needs at least one action to execute
205
+ * at all — signing or depositing is not itself a mark against a route, it is
206
+ * what EXECUTES it. Only actions beyond the first are extra friction the
207
+ * reader would not have paid on a one-action alternative, so only those are
208
+ * charged.
209
+ *
210
+ * REFUSES AN UNPRICED PLAN BY SCORING IT NEGATIVE INFINITY — PHASE 3. When
211
+ * {@link RouteScoreInputs.basis} is `'unpriced'`, `deliveredUsd` is a bare
212
+ * decimal rescale with no value behind it (see `EstimateBasis`'s own doc,
213
+ * `graph.ts`), and no arithmetic performed on it below would make it
214
+ * trustworthy. Returning `Number.NEGATIVE_INFINITY` rather than, say, `-1` or
215
+ * `0` is what makes this impossible to sidestep BY ACCIDENT: an ordinary `>`
216
+ * comparison, a plain numeric `.sort()`, or a `Math.max` over several plans'
217
+ * scores all already put an unpriced plan last WITHOUT the caller having to
218
+ * remember to check {@link RouteScoreInputs.basis} first — the same defence
219
+ * `compareRoutePlansByDeliveredAmount` (`graph.ts`) applies explicitly to a
220
+ * plan-to-plan comparison, applied here so a caller who reaches for
221
+ * `scoreRoutePlan` directly, for a single plan's score, gets the same
222
+ * protection for free.
223
+ *
224
+ * @param inputs - the plan's already-priced figures; see {@link RouteScoreInputs}
225
+ * @returns the plan's score — higher is better, and the units are United
226
+ * States dollars throughout; `Number.NEGATIVE_INFINITY` for an unpriced plan
227
+ */
228
+ // ---------------------------------------------------------------------------
229
+ // compareRoutePlansByScoreAdjustedDeliveredAmount — ranking that charges for
230
+ // an extra signature, without needing a United States dollar price
231
+ // ---------------------------------------------------------------------------
232
+ /** `SIGNATURE_PENALTY_PPM`'s denominator: parts per million, as a bigint. */
233
+ const PPM_DENOMINATOR = 1000000n;
234
+ /**
235
+ * `plan`'s last-hop `deliveredAmount`, discounted by {@link SIGNATURE_PENALTY_PPM}
236
+ * for every signature beyond the first — proportionally, in the delivered
237
+ * token's own base units, entirely in bigint arithmetic.
238
+ *
239
+ * WHY ONLY THE PROPORTIONAL SHARE, NEVER {@link SIGNATURE_PENALTY_FLOOR_USD}.
240
+ * The floor is denominated in United States dollars; converting a base-unit
241
+ * `deliveredAmount` into dollars needs a live price, which a `RoutePlan`
242
+ * (`graph.ts`) does not carry — it is a bigint amount in one token's own
243
+ * units, nothing more. `scoreRoutePlan` still applies the full formula,
244
+ * floor included, once a caller (`plan-route-data.ts`) has priced a plan into
245
+ * `RouteScoreInputs`. This function is the version usable at the STRUCTURAL
246
+ * ranking sites — `enumerate.ts`'s cut and `waves.ts`'s between-wave
247
+ * re-ranking — which only ever see the plan's own hops, not a price. See
248
+ * {@link compareRoutePlansByScoreAdjustedDeliveredAmount}'s own doc for why a
249
+ * proportional-only penalty is still enough to settle the case it exists for.
250
+ *
251
+ * @param plan - the plan to discount
252
+ * @returns the discounted delivered amount, floored at zero
253
+ */
254
+ const signaturePenaltyAdjustedDeliveredAmount = (plan) => {
255
+ const deliveredAmount = plan.hops[plan.hops.length - 1]?.outcome.deliveredAmount ?? 0n;
256
+ const extraActionCount = Math.max(0, plan.signatures.length - 1);
257
+ const penaltyPpm = BigInt(extraActionCount) * BigInt(SIGNATURE_PENALTY_PPM);
258
+ const retainedPpm = penaltyPpm >= PPM_DENOMINATOR ? 0n : PPM_DENOMINATOR - penaltyPpm;
259
+ return (deliveredAmount * retainedPpm) / PPM_DENOMINATOR;
260
+ };
261
+ /**
262
+ * Orders two RANKABLE plans (both pass `graph.ts`'s `planIsRankable`) by
263
+ * their {@link signaturePenaltyAdjustedDeliveredAmount} rather than their raw
264
+ * `deliveredAmount` — the STRUCTURAL counterpart to `graph.ts`'s
265
+ * {@link @gibs/bridge-sdk/routing/search/graph!compareRoutePlansByDeliveredAmount},
266
+ * reusing every one of that function's rules (rankable beats unrankable,
267
+ * `compareUnrankablePlans`'s tiebreak among two unrankable plans) and adding
268
+ * exactly one more: within two RANKABLE plans, a plan needing more
269
+ * signatures than another must clear {@link SIGNATURE_PENALTY_PPM} of
270
+ * "extra" delivered value per extra signature before it is allowed to win,
271
+ * not merely `> 0`.
272
+ *
273
+ * THE DEFECT THIS CLOSES. `SIGNATURE_PENALTY_PPM` and `GAS_TOPUP_PENALTY_PPM`
274
+ * existed in this module as constants `scoreRoutePlan` charges — but nothing
275
+ * in the actual ranking path called `scoreRoutePlan` at all:
276
+ * `enumerate.ts`'s cut, `waves.ts`'s between-wave re-ranking, and
277
+ * `useRouteSearch.ts`'s final sort all imported `graph.ts`'s plain
278
+ * delivered-amount comparator directly, so a route needing a second
279
+ * signature for a fractional gain (measured: 0.006% more, needing one more
280
+ * wallet prompt) always won outright. Sorting on
281
+ * {@link signaturePenaltyAdjustedDeliveredAmount} instead is what makes that
282
+ * lose to the one-signature alternative: the extra signature's proportional
283
+ * cost (0.25% per {@link SIGNATURE_PENALTY_PPM}) outweighs a 0.006% edge by
284
+ * roughly forty times.
285
+ *
286
+ * WHY NOT `GAS_TOPUP_PENALTY_PPM` HERE TOO. A gasless-intermediate-chain
287
+ * charge needs to know whether the connected account holds gas on a chain in
288
+ * the middle of the plan — a live wallet balance read `RoutePlan` has no way
289
+ * to name. Applying it here would require guessing, which this comparator
290
+ * refuses to do; `GAS_TOPUP_PENALTY_PPM` is still applied wherever a caller
291
+ * has that balance and reaches for `scoreRoutePlan` directly (`plan-route-data.ts`
292
+ * onward). Left as an OWNER DECISION whether a structural ranking site should
293
+ * ever get gas-balance context of its own.
294
+ *
295
+ * @param a - one plan being compared
296
+ * @param b - the other
297
+ * @returns negative when `a` ranks ahead of `b`, positive when `b` ranks
298
+ * ahead of `a`, zero when they tie
299
+ */
300
+ export const compareRoutePlansByScoreAdjustedDeliveredAmount = (a, b) => {
301
+ const rankableA = planIsRankable(a.hops);
302
+ const rankableB = planIsRankable(b.hops);
303
+ if (rankableA !== rankableB)
304
+ return rankableA ? -1 : 1;
305
+ if (!rankableA)
306
+ return compareRoutePlansByDeliveredAmount(a, b);
307
+ const adjustedA = signaturePenaltyAdjustedDeliveredAmount(a);
308
+ const adjustedB = signaturePenaltyAdjustedDeliveredAmount(b);
309
+ if (adjustedA === adjustedB)
310
+ return 0;
311
+ return adjustedA > adjustedB ? -1 : 1;
312
+ };
313
+ export const scoreRoutePlan = (inputs) => {
314
+ if (!isRankableBasis(inputs.basis)) {
315
+ return Number.NEGATIVE_INFINITY;
316
+ }
317
+ const totalWaitPenaltyUsd = inputs.legClaimedDurationsSeconds.reduce((total, claimedSeconds) => total + (inputs.deliveredUsd * waitPenaltyPpm(claimedSeconds)) / 1_000_000, 0);
318
+ const extraActionCount = Math.max(0, inputs.readerActionCount - 1);
319
+ const totalSignaturePenaltyUsd = extraActionCount * signaturePenaltyUsd(inputs.deliveredUsd);
320
+ const totalGasTopUpPenaltyUsd = inputs.gaslessIntermediateChainCount * gasTopUpPenaltyUsd(inputs.deliveredUsd);
321
+ return (inputs.deliveredUsd -
322
+ inputs.paidUsd -
323
+ inputs.outsideFeesUsd -
324
+ totalWaitPenaltyUsd -
325
+ totalSignaturePenaltyUsd -
326
+ totalGasTopUpPenaltyUsd);
327
+ };
328
+ /**
329
+ * A PLACEHOLDER {@link GasCostFor} that charges nothing — preserves the
330
+ * behaviour of ranking on raw delivered amount for a caller that has not yet
331
+ * wired a real gas-price and native-price source. NOT A PRODUCTION DEFAULT:
332
+ * unlike {@link WAIT_PENALTY_PPM_PER_MINUTE}'s two logged, live crossings, no
333
+ * per-chain gas figure here would be a measurement — it would be a guess
334
+ * dressed up as one, exactly what Rule 13 of this codebase's own working
335
+ * principles warns against. A host should always supply its own
336
+ * {@link GasCostFor} once it has gas-price and native-price feeds to build
337
+ * one from; this export exists only so `waves.ts`'s own default comparator
338
+ * has something total to call when nobody has.
339
+ */
340
+ export const zeroGasCostFor = () => ({ gasAmount: 0n, refuelAmount: 0n });
341
+ /**
342
+ * Whether the section at `sectionIndex` starts with native coin already in
343
+ * the reader's hand: the hop that ends the PREVIOUS section delivers the
344
+ * chain's native coin, so the reader arrives holding gas for this section's
345
+ * own signature and needs no top-up there, whatever their balance was.
346
+ *
347
+ * Always false for the first section — nothing lands before it, so its gas
348
+ * can only come from the reader's own balance.
349
+ *
350
+ * @param plan - the plan the section belongs to
351
+ * @param sectionIndex - 0-based index into `plan.signatures`
352
+ * @returns true when the previous section lands native coin on this
353
+ * section's chain
354
+ */
355
+ export const sectionIsFundedByLanding = (plan, sectionIndex) => {
356
+ if (sectionIndex < 1)
357
+ return false;
358
+ const previousGroup = plan.signatures[sectionIndex - 1];
359
+ const landingHop = plan.hops[previousGroup?.[previousGroup.length - 1] ?? -1];
360
+ if (landingHop === undefined)
361
+ return false;
362
+ return isNativeAsset(landingHop.edge.to.address, landingHop.edge.to.ecosystem);
363
+ };
364
+ /**
365
+ * `plan`'s last-hop `deliveredAmount`, minus the REAL cost of every
366
+ * signature it needs — `gasCostFor` charged ONCE per signature group
367
+ * (`plan.signatures`, built by `groupHopsBySignature`), on the chain and
368
+ * carrier of that group's FIRST hop, which is the hop the reader actually
369
+ * signs (or deposits by hand) for that group. A group's refuel cost is
370
+ * dropped when the previous group lands native coin
371
+ * ({@link sectionIsFundedByLanding}). Floored at zero: a plan whose
372
+ * gas costs exceed its own delivered amount is worth nothing, never negative.
373
+ *
374
+ * @param plan - the plan to charge
375
+ * @param gasCostFor - supplies each signature's real cost; see {@link GasCostFor}
376
+ * @returns the gas-adjusted delivered amount, in the plan's destination token's base units
377
+ */
378
+ export const gasAdjustedDeliveredAmount = (plan, gasCostFor) => {
379
+ const deliveredAmount = plan.hops[plan.hops.length - 1]?.outcome.deliveredAmount ?? 0n;
380
+ const totalGasCost = plan.signatures.reduce((total, group, sectionIndex) => {
381
+ const firstHopIndex = group[0];
382
+ const firstHop = firstHopIndex === undefined ? undefined : plan.hops[firstHopIndex];
383
+ if (firstHop === undefined)
384
+ return total;
385
+ const quote = gasCostFor(firstHop.edge.from.chainId, firstHop.edge.carrier);
386
+ const refuelAmount = sectionIsFundedByLanding(plan, sectionIndex) ? 0n : quote.refuelAmount;
387
+ return total + quote.gasAmount + refuelAmount;
388
+ }, 0n);
389
+ const netAmount = deliveredAmount - totalGasCost;
390
+ return netAmount < 0n ? 0n : netAmount;
391
+ };
392
+ /**
393
+ * Orders two plans by {@link gasAdjustedDeliveredAmount} rather than raw
394
+ * `deliveredAmount` or {@link signaturePenaltyAdjustedDeliveredAmount}'s flat
395
+ * ppm guess — the structural ranking `waves.ts` uses once Round A/B have real
396
+ * gas data to charge, replacing {@link SIGNATURE_PENALTY_PPM}/
397
+ * {@link GAS_TOPUP_PENALTY_PPM} at that one site per the owner's own
398
+ * instruction (see both constants' updated docs). A CURRIED FACTORY, not a
399
+ * plain comparator, because the real cost this needs — {@link GasCostFor} —
400
+ * is caller-supplied and cannot be a module-level default the way
401
+ * {@link compareRoutePlansByScoreAdjustedDeliveredAmount} is; call it once
402
+ * per search with the caller's own `gasCostFor`, then pass the returned
403
+ * comparator wherever a plan-to-plan comparison is needed.
404
+ *
405
+ * KEEPS THE SAME RANKABLE-BEFORE-UNRANKABLE RULE `compareRoutePlansByScoreAdjustedDeliveredAmount`
406
+ * ALREADY APPLIES, unchanged, including its `compareRoutePlansByDeliveredAmount`
407
+ * tiebreak among two unrankable plans — only the tiebreak among two RANKABLE
408
+ * plans changes, from a proportional signature-count discount to the route's
409
+ * own real charges.
410
+ *
411
+ * @param gasCostFor - supplies each signature's real cost; see {@link GasCostFor}
412
+ * @returns a comparator: negative when `a` ranks ahead of `b`, positive when
413
+ * `b` ranks ahead of `a`, zero when they tie
414
+ */
415
+ export const compareRoutePlansByGasAdjustedDeliveredAmount = (gasCostFor) => (a, b) => {
416
+ const rankableA = planIsRankable(a.hops);
417
+ const rankableB = planIsRankable(b.hops);
418
+ if (rankableA !== rankableB)
419
+ return rankableA ? -1 : 1;
420
+ if (!rankableA)
421
+ return compareRoutePlansByDeliveredAmount(a, b);
422
+ const adjustedA = gasAdjustedDeliveredAmount(a, gasCostFor);
423
+ const adjustedB = gasAdjustedDeliveredAmount(b, gasCostFor);
424
+ if (adjustedA === adjustedB)
425
+ return 0;
426
+ return adjustedA > adjustedB ? -1 : 1;
427
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The United States dollar value of a base-unit token amount at a given price, as
3
+ * a NUMBER — the numeric core of the user interface's `formatFiatValue` without the
4
+ * currency string. Returns null when the token is unpriced (`price` is null) or the
5
+ * computed value is not finite, mirroring the "return null for invalid state"
6
+ * convention so callers can fall back.
7
+ *
8
+ * @param params.balance - the amount in base units
9
+ * @param params.decimals - the token's decimals
10
+ * @param params.price - the token's United States dollar price, or null when unpriced
11
+ * @returns the dollar value as a number, or null
12
+ */
13
+ export declare const usdValueOfAmount: (params: {
14
+ balance: bigint;
15
+ decimals: number;
16
+ price: number | null;
17
+ }) => number | null;
@@ -0,0 +1,25 @@
1
+ import { formatUnits } from 'viem';
2
+ /**
3
+ * The United States dollar value of a base-unit token amount at a given price, as
4
+ * a NUMBER — the numeric core of the user interface's `formatFiatValue` without the
5
+ * currency string. Returns null when the token is unpriced (`price` is null) or the
6
+ * computed value is not finite, mirroring the "return null for invalid state"
7
+ * convention so callers can fall back.
8
+ *
9
+ * @param params.balance - the amount in base units
10
+ * @param params.decimals - the token's decimals
11
+ * @param params.price - the token's United States dollar price, or null when unpriced
12
+ * @returns the dollar value as a number, or null
13
+ */
14
+ export const usdValueOfAmount = (params) => {
15
+ const { balance, decimals, price } = params;
16
+ if (price === null)
17
+ return null;
18
+ const whole = Number(formatUnits(balance, decimals));
19
+ if (!Number.isFinite(whole))
20
+ return null;
21
+ const value = whole * price;
22
+ if (!Number.isFinite(value))
23
+ return null;
24
+ return value;
25
+ };