@provex/utils 1.11.0 → 2.0.0-rc.20261103015524.5c1d822
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/buy/browsePanelDerivations.d.ts +212 -0
- package/dist/buy/browsePanelDerivations.js +269 -0
- package/dist/buy/browsePanelDerivations.test.d.ts +9 -0
- package/dist/buy/browsePanelDerivations.test.js +552 -0
- package/dist/buy/fillabilityNotice.d.ts +214 -0
- package/dist/buy/fillabilityNotice.js +162 -0
- package/dist/buy/fillabilityNotice.test.d.ts +8 -0
- package/dist/buy/fillabilityNotice.test.js +246 -0
- package/dist/buy/formatCurrencyBaseUnits.d.ts +30 -0
- package/dist/buy/formatCurrencyBaseUnits.js +34 -0
- package/dist/buy/formatCurrencyBaseUnits.test.d.ts +7 -0
- package/dist/buy/formatCurrencyBaseUnits.test.js +34 -0
- package/dist/buy/fulfillTransactionHash.d.ts +29 -0
- package/dist/buy/fulfillTransactionHash.js +27 -0
- package/dist/buy/fulfillTransactionHash.test.d.ts +7 -0
- package/dist/buy/fulfillTransactionHash.test.js +46 -0
- package/dist/buy/index.d.ts +17 -0
- package/dist/buy/index.js +17 -0
- package/dist/buy/orderCap.d.ts +29 -0
- package/dist/buy/orderCap.js +35 -0
- package/dist/buy/orderCap.test.d.ts +8 -0
- package/dist/buy/orderCap.test.js +35 -0
- package/dist/buy/postFeeAmount.d.ts +39 -0
- package/dist/buy/postFeeAmount.js +36 -0
- package/dist/buy/postFeeAmount.test.d.ts +9 -0
- package/dist/buy/postFeeAmount.test.js +82 -0
- package/dist/buy/providerSelectionMode.d.ts +5 -0
- package/dist/buy/providerSelectionMode.js +4 -0
- package/dist/buy/settlementReconciliation.d.ts +48 -0
- package/dist/buy/settlementReconciliation.js +65 -0
- package/dist/buy/settlementReconciliation.test.d.ts +9 -0
- package/dist/buy/settlementReconciliation.test.js +164 -0
- package/dist/buy/submitStepDerivations.d.ts +122 -0
- package/dist/buy/submitStepDerivations.js +140 -0
- package/dist/buy/submitStepDerivations.test.d.ts +1 -0
- package/dist/buy/submitStepDerivations.test.js +305 -0
- package/dist/buy/verifiableBanks.d.ts +29 -0
- package/dist/buy/verifiableBanks.js +28 -0
- package/dist/buy/verifiableBanks.test.d.ts +4 -0
- package/dist/buy/verifiableBanks.test.js +31 -0
- package/dist/consent.js +0 -2
- package/dist/consent.storage.test.d.ts +8 -0
- package/dist/consent.storage.test.js +111 -0
- package/dist/consent.test.js +60 -1
- package/dist/contracts.js +3 -1
- package/dist/contracts.test.js +17 -1
- package/dist/conversionRates.test.js +18 -0
- package/dist/currencies.d.ts +6 -0
- package/dist/currencies.js +1 -1
- package/dist/currencies.test.js +29 -2
- package/dist/fees.js +0 -3
- package/dist/gating.test.js +16 -1
- package/dist/ids.test.js +48 -0
- package/dist/payment.d.ts +50 -0
- package/dist/payment.js +72 -51
- package/dist/payment.lookup.test.d.ts +5 -0
- package/dist/payment.lookup.test.js +169 -0
- package/dist/payment.providerLists.test.d.ts +12 -0
- package/dist/payment.providerLists.test.js +40 -0
- package/dist/payment.sendLinks.test.d.ts +9 -0
- package/dist/payment.sendLinks.test.js +91 -0
- package/dist/peer-types.d.ts +5 -78
- package/dist/peer-types.js +5 -31
- package/dist/peer-types.test.js +1 -31
- package/dist/profit.d.ts +8 -0
- package/dist/profit.js +16 -3
- package/dist/profit.test.js +12 -1
- package/dist/reputation.js +4 -6
- package/dist/reputation.test.js +8 -0
- package/dist/time.test.js +26 -5
- package/dist/tokens.d.ts +2 -2
- package/dist/transports.d.ts +53 -2
- package/dist/transports.debug.test.d.ts +9 -0
- package/dist/transports.debug.test.js +95 -0
- package/dist/transports.js +26 -10
- package/dist/transports.test.js +5 -1
- package/dist/units.d.ts +19 -0
- package/dist/units.js +21 -0
- package/dist/units.test.js +31 -1
- package/package.json +6 -2
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pure derivations extracted from BrowsePanel.tsx.
|
|
3
|
+
*
|
|
4
|
+
* Each helper takes ordinary inputs (no React hooks, no wagmi state)
|
|
5
|
+
* and returns a value, so they're testable in isolation without
|
|
6
|
+
* mounting the panel or stubbing wagmi/router/appkit.
|
|
7
|
+
*
|
|
8
|
+
* Rationale for the split: BrowsePanel sits at the very top of the
|
|
9
|
+
* buy-flow surface area — every input flows through it. Inlining
|
|
10
|
+
* these derivations next to the JSX makes the file (1) hard to read
|
|
11
|
+
* and (2) impossible to test without a full router+wallet harness.
|
|
12
|
+
* Pulling them out is both a navigation and a coverage win.
|
|
13
|
+
*
|
|
14
|
+
* Keep helpers PURE: no side effects, no module-level state, no
|
|
15
|
+
* useState/useMemo. The panel still calls each one inside its own
|
|
16
|
+
* useMemo so React-level memoization stays at the call site.
|
|
17
|
+
*/
|
|
18
|
+
import { type ProviderKey, type SubProviderKey } from '../payment.js';
|
|
19
|
+
import type { ChainId } from '../chain.js';
|
|
20
|
+
import type { Currency } from '../currencies.js';
|
|
21
|
+
/**
|
|
22
|
+
* Sanitize a raw amount-input string so it only contains digits and a
|
|
23
|
+
* single `.`, and clamp the fractional precision to `maxDecimals`.
|
|
24
|
+
*
|
|
25
|
+
* Mirrors the live filter on the BrowsePanel `<Input>` element. Pure
|
|
26
|
+
* string transform — the call site wraps it in `setState` per-keystroke.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* sanitizeAmountInput('12.34.56', 2) // '12.3456' → '12.34' (clamp)
|
|
30
|
+
* sanitizeAmountInput('1a2b.3', 2) // '12.3' (filter)
|
|
31
|
+
* sanitizeAmountInput('100.500', 2) // '100.50' (clamp)
|
|
32
|
+
*/
|
|
33
|
+
export declare function sanitizeAmountInput(raw: string, maxDecimals: number): string;
|
|
34
|
+
/**
|
|
35
|
+
* Parse the sanitized amount string into a USDC-6-decimal-base-units
|
|
36
|
+
* bigint (the unit every downstream calc uses).
|
|
37
|
+
*
|
|
38
|
+
* Returns `null` if the amount can't be parsed (empty, NaN, missing
|
|
39
|
+
* currency). Negative values clamp to `0n` rather than propagating
|
|
40
|
+
* the negative — the input filter already strips `-`, so this is
|
|
41
|
+
* defensive against a hypothetical refactor that accepts it.
|
|
42
|
+
*
|
|
43
|
+
* @param amount - The sanitized amount string ('100.50').
|
|
44
|
+
* @param currencyDecimals - Decimals of the fiat currency (USD = 2, JPY = 0).
|
|
45
|
+
* Falls back to 2 when undefined to match the
|
|
46
|
+
* panel's `currency?.decimals ?? 2` shape.
|
|
47
|
+
* @returns Amount as a bigint in USDC base units, or null.
|
|
48
|
+
*/
|
|
49
|
+
export declare function parseAmountToBaseUnits(amount: string, currencyDecimals: number | undefined): bigint | null;
|
|
50
|
+
/** The least a deposit has to expose for the buy panel to rank it. */
|
|
51
|
+
export interface RankableDeposit {
|
|
52
|
+
depositId: bigint;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Sort matched deposits by rate ascending (lowest fiat-per-USDC =
|
|
56
|
+
* best deal for the buyer), breaking ties by deposit id ascending
|
|
57
|
+
* so the order is deterministic across renders.
|
|
58
|
+
*
|
|
59
|
+
* The rate is injected so this module does not depend on the indexer
|
|
60
|
+
* client, which depends on this package.
|
|
61
|
+
*
|
|
62
|
+
* Returns a fresh array — does not mutate the input.
|
|
63
|
+
*
|
|
64
|
+
* @param params.deposits - The match set
|
|
65
|
+
* @param params.rateOf - Rate of one deposit for the selected payment method and currency
|
|
66
|
+
*/
|
|
67
|
+
export declare function sortDepositsByRate<D extends RankableDeposit>({ deposits, rateOf, }: {
|
|
68
|
+
deposits: readonly D[];
|
|
69
|
+
rateOf: (deposit: D) => bigint;
|
|
70
|
+
}): D[];
|
|
71
|
+
/**
|
|
72
|
+
* Is the user's currently-selected deposit the best-rate option
|
|
73
|
+
* across the full match set?
|
|
74
|
+
*
|
|
75
|
+
* Returns `false` when there's no selection or only one match —
|
|
76
|
+
* the "Best of N" badge gates on this so it doesn't claim "best" in
|
|
77
|
+
* a one-deposit set (trivially true and not useful), nor when the
|
|
78
|
+
* user manually picked a non-best alternative.
|
|
79
|
+
*
|
|
80
|
+
* @param sortedDeposits - The match set sorted by `sortDepositsByRate`.
|
|
81
|
+
* The first entry is the best.
|
|
82
|
+
*/
|
|
83
|
+
export declare function isSelectedDepositBest({ selectedDeposit, sortedDeposits, }: {
|
|
84
|
+
selectedDeposit: RankableDeposit | null;
|
|
85
|
+
sortedDeposits: readonly RankableDeposit[];
|
|
86
|
+
}): boolean;
|
|
87
|
+
export interface TargetedVerifier<V> {
|
|
88
|
+
/** Payment-method key that resolved (parent OR sub-provider). */
|
|
89
|
+
providerKey: ProviderKey | SubProviderKey;
|
|
90
|
+
/** The verifier struct attached to the deposit for that method. */
|
|
91
|
+
verifier: V;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Pick the deposit's verifier that should drive the maker-username
|
|
95
|
+
* lookup, the intent gating, and the payment method the intent is
|
|
96
|
+
* signalled on. Resolves the rail's own payment method (`zelle`) when the
|
|
97
|
+
* deposit carries it with a rate for this currency.
|
|
98
|
+
*
|
|
99
|
+
* Returns `null` when it does not — the deposit can't accept this currency
|
|
100
|
+
* through this payment method at all.
|
|
101
|
+
*
|
|
102
|
+
* ### Why only the rail
|
|
103
|
+
*
|
|
104
|
+
* The bank picker exists so the buyer's capture opens the right bank; it
|
|
105
|
+
* does not name a payment method. A Zelle attestation is issued for the
|
|
106
|
+
* rail — every bank template declares `metadata.platform: "zelle"` and
|
|
107
|
+
* the enclave route is `zelle/transfer_zelle_{bank}` — so an intent
|
|
108
|
+
* signalled on a bank-level method can never be fulfilled.
|
|
109
|
+
*
|
|
110
|
+
* This used to fall back to the picked bank, then to any other bank with a
|
|
111
|
+
* rate, so that pre-migration deposits stayed addressable. Those fallbacks
|
|
112
|
+
* were reachable only for deposits whose orders were guaranteed to strand,
|
|
113
|
+
* and `fetchMatchableDeposits` no longer surfaces such deposits at all.
|
|
114
|
+
* Returning null instead fails the order at the Reserve button — before the
|
|
115
|
+
* buyer sends money — rather than at submission, after they have.
|
|
116
|
+
*
|
|
117
|
+
* Generic over the verifier shape so tests can pass simplified
|
|
118
|
+
* structs without importing the full Ponder verifier type.
|
|
119
|
+
*
|
|
120
|
+
* The original implementation inlined a 3-loop chain in BrowsePanel's
|
|
121
|
+
* 30-line `useMemo`. Extracting to a pure helper makes the fallback
|
|
122
|
+
* order reviewable on its own AND lets us pin each branch without
|
|
123
|
+
* mounting the panel.
|
|
124
|
+
*/
|
|
125
|
+
export declare function resolveTargetedVerifier<V>({ deposit, paymentMethod, currency, }: {
|
|
126
|
+
deposit: DepositLikeForResolver<V> | null;
|
|
127
|
+
/**
|
|
128
|
+
* The rail. Pass the parent key even when the buyer has picked a bank — the
|
|
129
|
+
* bank is a capture-time routing detail, not a payment method.
|
|
130
|
+
*/
|
|
131
|
+
paymentMethod: ProviderKey;
|
|
132
|
+
currency: Currency;
|
|
133
|
+
}): TargetedVerifier<V> | null;
|
|
134
|
+
/**
|
|
135
|
+
* The minimal Deposit shape `resolveTargetedVerifier` reads from.
|
|
136
|
+
* Lets tests pass in a fixture object instead of constructing a full
|
|
137
|
+
* `Deposit` class instance (which has dozens of unrelated getters).
|
|
138
|
+
*
|
|
139
|
+
* `chainId` is typed as `ChainId` (literal union) because
|
|
140
|
+
* `providerConfigs()` is keyed on that union; tests pass real chain
|
|
141
|
+
* literals (369, 8453) so the type narrowing holds. The verifier
|
|
142
|
+
* shape is generic so call sites can pass whatever the indexer
|
|
143
|
+
* client returns without us re-exporting that type here.
|
|
144
|
+
*/
|
|
145
|
+
export interface DepositLikeForResolver<V> {
|
|
146
|
+
chainId: ChainId;
|
|
147
|
+
getVerifier(key: ProviderKey | SubProviderKey | string): V | null | undefined;
|
|
148
|
+
currentConversionRates: ReadonlyMap<ProviderKey, ReadonlyMap<string, {
|
|
149
|
+
value: string;
|
|
150
|
+
} | undefined>>;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Build a `ProviderKey → totalVolume` map from the indexer's ordered
|
|
154
|
+
* verifier list. Used as the tiebreaker when multiple deposits share
|
|
155
|
+
* the same lowest rate in `useDefaultProviderByRate`.
|
|
156
|
+
*
|
|
157
|
+
* Returns an empty map when the input is missing or malformed —
|
|
158
|
+
* downstream callers treat "no volume data" as "no tiebreaker
|
|
159
|
+
* available", same as a zero entry.
|
|
160
|
+
*/
|
|
161
|
+
export declare function buildVolumeByProviderKey(verifiersByVolume: readonly {
|
|
162
|
+
verifier: string | null | undefined;
|
|
163
|
+
totalVolume: bigint;
|
|
164
|
+
}[] | null | undefined): Map<ProviderKey, bigint>;
|
|
165
|
+
/**
|
|
166
|
+
* Whole-form validity gate for the BrowsePanel Reserve CTA. Returns
|
|
167
|
+
* `true` only when EVERY precondition is met — any single false
|
|
168
|
+
* input disables the CTA.
|
|
169
|
+
*
|
|
170
|
+
* Order of fields here doesn't drive the disabled-label priority
|
|
171
|
+
* chain (that's in `reserveDisabledLabel`). This is the boolean
|
|
172
|
+
* AND of "can we submit at all?"
|
|
173
|
+
*/
|
|
174
|
+
export declare function isBrowseFormValid({ amountInInt, amountOutInt, hasSelectedDeposit, depositRate, amountExceedsCap, makerUnavailable, isOnCooldown, }: {
|
|
175
|
+
amountInInt: bigint | null;
|
|
176
|
+
amountOutInt: bigint | null;
|
|
177
|
+
hasSelectedDeposit: boolean;
|
|
178
|
+
depositRate: bigint | null | undefined;
|
|
179
|
+
amountExceedsCap: boolean;
|
|
180
|
+
makerUnavailable: boolean;
|
|
181
|
+
isOnCooldown: boolean;
|
|
182
|
+
}): boolean;
|
|
183
|
+
/**
|
|
184
|
+
* Decide whether the selected provider has to move because it cannot serve
|
|
185
|
+
* the selected currency, and to what.
|
|
186
|
+
*
|
|
187
|
+
* The buy panel's auto-pick only runs in auto mode, so a manual pick or a
|
|
188
|
+
* shared link can leave a buyer on a rail with no matching liquidity — Venmo
|
|
189
|
+
* is a United States dollar rail, so `?provider=venmo¤cy=GBP` shows an
|
|
190
|
+
* empty order book and says nothing about why.
|
|
191
|
+
*
|
|
192
|
+
* @param selectedProvider - What the URL currently holds.
|
|
193
|
+
* @param fallbackProvider - Where to move the buyer. The caller passes its
|
|
194
|
+
* volume-ranked, currency-filtered default so the replacement is the same
|
|
195
|
+
* rail auto mode would have chosen.
|
|
196
|
+
* @returns The provider to switch to, or `null` to leave the selection alone.
|
|
197
|
+
*
|
|
198
|
+
* Returns `null` in two distinct situations, deliberately collapsed because
|
|
199
|
+
* the caller's action is identical — do nothing:
|
|
200
|
+
*
|
|
201
|
+
* 1. The selection is already fine.
|
|
202
|
+
* 2. The fallback cannot serve the currency either, so no rail this deployment
|
|
203
|
+
* offers can. Moving the buyer would swap one dead end for another,
|
|
204
|
+
* and returning the fallback here would make the caller's write re-trigger
|
|
205
|
+
* this check forever.
|
|
206
|
+
*/
|
|
207
|
+
export declare function reconcileProviderForCurrency({ selectedProvider, fallbackProvider, chainId, currency, }: {
|
|
208
|
+
selectedProvider: ProviderKey;
|
|
209
|
+
fallbackProvider: ProviderKey;
|
|
210
|
+
chainId: ChainId;
|
|
211
|
+
currency: Currency;
|
|
212
|
+
}): ProviderKey | null;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pure derivations extracted from BrowsePanel.tsx.
|
|
3
|
+
*
|
|
4
|
+
* Each helper takes ordinary inputs (no React hooks, no wagmi state)
|
|
5
|
+
* and returns a value, so they're testable in isolation without
|
|
6
|
+
* mounting the panel or stubbing wagmi/router/appkit.
|
|
7
|
+
*
|
|
8
|
+
* Rationale for the split: BrowsePanel sits at the very top of the
|
|
9
|
+
* buy-flow surface area — every input flows through it. Inlining
|
|
10
|
+
* these derivations next to the JSX makes the file (1) hard to read
|
|
11
|
+
* and (2) impossible to test without a full router+wallet harness.
|
|
12
|
+
* Pulling them out is both a navigation and a coverage win.
|
|
13
|
+
*
|
|
14
|
+
* Keep helpers PURE: no side effects, no module-level state, no
|
|
15
|
+
* useState/useMemo. The panel still calls each one inside its own
|
|
16
|
+
* useMemo so React-level memoization stays at the call site.
|
|
17
|
+
*/
|
|
18
|
+
import { providerSupportsCurrency } from '../payment.js';
|
|
19
|
+
import { parseUnits } from '../units.js';
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Amount input sanitization
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
/**
|
|
24
|
+
* Sanitize a raw amount-input string so it only contains digits and a
|
|
25
|
+
* single `.`, and clamp the fractional precision to `maxDecimals`.
|
|
26
|
+
*
|
|
27
|
+
* Mirrors the live filter on the BrowsePanel `<Input>` element. Pure
|
|
28
|
+
* string transform — the call site wraps it in `setState` per-keystroke.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* sanitizeAmountInput('12.34.56', 2) // '12.3456' → '12.34' (clamp)
|
|
32
|
+
* sanitizeAmountInput('1a2b.3', 2) // '12.3' (filter)
|
|
33
|
+
* sanitizeAmountInput('100.500', 2) // '100.50' (clamp)
|
|
34
|
+
*/
|
|
35
|
+
export function sanitizeAmountInput(raw, maxDecimals) {
|
|
36
|
+
// Step 1: keep only digits + period. Replace every other character
|
|
37
|
+
// with '' rather than rejecting — pastes with thousands separators
|
|
38
|
+
// ('1,000.00') still yield '1000.00' instead of dropping the input.
|
|
39
|
+
const onlyDigitsAndDot = raw.replace(/[^\d.]/g, '');
|
|
40
|
+
// Step 2: collapse any second/third '.' into the first one. The
|
|
41
|
+
// regex captures everything from the first '.' onward and strips
|
|
42
|
+
// additional '.' inside that captured group.
|
|
43
|
+
const singleDot = onlyDigitsAndDot.replace(/(\..*)\./g, '$1');
|
|
44
|
+
// Step 3: clamp fractional length. Skip if the input has no '.'.
|
|
45
|
+
const dotIndex = singleDot.indexOf('.');
|
|
46
|
+
if (dotIndex === -1)
|
|
47
|
+
return singleDot;
|
|
48
|
+
// Special case: zero-decimal currency (JPY) — the dot itself is
|
|
49
|
+
// meaningless. Strip it entirely rather than leaving a trailing
|
|
50
|
+
// "100." that the downstream parser would render oddly.
|
|
51
|
+
if (maxDecimals === 0)
|
|
52
|
+
return singleDot.slice(0, dotIndex);
|
|
53
|
+
const fractional = singleDot.length - dotIndex - 1;
|
|
54
|
+
if (fractional <= maxDecimals)
|
|
55
|
+
return singleDot;
|
|
56
|
+
return singleDot.slice(0, dotIndex + maxDecimals + 1);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Parse the sanitized amount string into a USDC-6-decimal-base-units
|
|
60
|
+
* bigint (the unit every downstream calc uses).
|
|
61
|
+
*
|
|
62
|
+
* Returns `null` if the amount can't be parsed (empty, NaN, missing
|
|
63
|
+
* currency). Negative values clamp to `0n` rather than propagating
|
|
64
|
+
* the negative — the input filter already strips `-`, so this is
|
|
65
|
+
* defensive against a hypothetical refactor that accepts it.
|
|
66
|
+
*
|
|
67
|
+
* @param amount - The sanitized amount string ('100.50').
|
|
68
|
+
* @param currencyDecimals - Decimals of the fiat currency (USD = 2, JPY = 0).
|
|
69
|
+
* Falls back to 2 when undefined to match the
|
|
70
|
+
* panel's `currency?.decimals ?? 2` shape.
|
|
71
|
+
* @returns Amount as a bigint in USDC base units, or null.
|
|
72
|
+
*/
|
|
73
|
+
export function parseAmountToBaseUnits(amount, currencyDecimals) {
|
|
74
|
+
// 10^(6 - currencyDecimals) rebases from fiat decimals to USDC
|
|
75
|
+
// 6-decimal base units. For USD (decimals=2), converter = 10^4
|
|
76
|
+
// so $1.00 (= parseUnits('1.00') = 100n) becomes 1_000_000n.
|
|
77
|
+
// For JPY (decimals=0), converter = 10^6 so ¥1 (= 1n) becomes 1_000_000n.
|
|
78
|
+
const decimals = currencyDecimals ?? 2;
|
|
79
|
+
if (!amount)
|
|
80
|
+
return null;
|
|
81
|
+
try {
|
|
82
|
+
const parsed = parseUnits(amount, { decimals });
|
|
83
|
+
if (parsed < 0n)
|
|
84
|
+
return 0n;
|
|
85
|
+
const converter = 10n ** BigInt(6 - decimals);
|
|
86
|
+
return parsed * converter;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Sort matched deposits by rate ascending (lowest fiat-per-USDC =
|
|
94
|
+
* best deal for the buyer), breaking ties by deposit id ascending
|
|
95
|
+
* so the order is deterministic across renders.
|
|
96
|
+
*
|
|
97
|
+
* The rate is injected so this module does not depend on the indexer
|
|
98
|
+
* client, which depends on this package.
|
|
99
|
+
*
|
|
100
|
+
* Returns a fresh array — does not mutate the input.
|
|
101
|
+
*
|
|
102
|
+
* @param params.deposits - The match set
|
|
103
|
+
* @param params.rateOf - Rate of one deposit for the selected payment method and currency
|
|
104
|
+
*/
|
|
105
|
+
export function sortDepositsByRate({ deposits, rateOf, }) {
|
|
106
|
+
return deposits.slice().sort((a, b) => {
|
|
107
|
+
const rateA = rateOf(a);
|
|
108
|
+
const rateB = rateOf(b);
|
|
109
|
+
if (rateA < rateB)
|
|
110
|
+
return -1;
|
|
111
|
+
if (rateA > rateB)
|
|
112
|
+
return 1;
|
|
113
|
+
if (a.depositId < b.depositId)
|
|
114
|
+
return -1;
|
|
115
|
+
if (a.depositId > b.depositId)
|
|
116
|
+
return 1;
|
|
117
|
+
return 0;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Is the user's currently-selected deposit the best-rate option
|
|
122
|
+
* across the full match set?
|
|
123
|
+
*
|
|
124
|
+
* Returns `false` when there's no selection or only one match —
|
|
125
|
+
* the "Best of N" badge gates on this so it doesn't claim "best" in
|
|
126
|
+
* a one-deposit set (trivially true and not useful), nor when the
|
|
127
|
+
* user manually picked a non-best alternative.
|
|
128
|
+
*
|
|
129
|
+
* @param sortedDeposits - The match set sorted by `sortDepositsByRate`.
|
|
130
|
+
* The first entry is the best.
|
|
131
|
+
*/
|
|
132
|
+
export function isSelectedDepositBest({ selectedDeposit, sortedDeposits, }) {
|
|
133
|
+
if (!selectedDeposit || sortedDeposits.length < 2)
|
|
134
|
+
return false;
|
|
135
|
+
return sortedDeposits[0]?.depositId === selectedDeposit.depositId;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Pick the deposit's verifier that should drive the maker-username
|
|
139
|
+
* lookup, the intent gating, and the payment method the intent is
|
|
140
|
+
* signalled on. Resolves the rail's own payment method (`zelle`) when the
|
|
141
|
+
* deposit carries it with a rate for this currency.
|
|
142
|
+
*
|
|
143
|
+
* Returns `null` when it does not — the deposit can't accept this currency
|
|
144
|
+
* through this payment method at all.
|
|
145
|
+
*
|
|
146
|
+
* ### Why only the rail
|
|
147
|
+
*
|
|
148
|
+
* The bank picker exists so the buyer's capture opens the right bank; it
|
|
149
|
+
* does not name a payment method. A Zelle attestation is issued for the
|
|
150
|
+
* rail — every bank template declares `metadata.platform: "zelle"` and
|
|
151
|
+
* the enclave route is `zelle/transfer_zelle_{bank}` — so an intent
|
|
152
|
+
* signalled on a bank-level method can never be fulfilled.
|
|
153
|
+
*
|
|
154
|
+
* This used to fall back to the picked bank, then to any other bank with a
|
|
155
|
+
* rate, so that pre-migration deposits stayed addressable. Those fallbacks
|
|
156
|
+
* were reachable only for deposits whose orders were guaranteed to strand,
|
|
157
|
+
* and `fetchMatchableDeposits` no longer surfaces such deposits at all.
|
|
158
|
+
* Returning null instead fails the order at the Reserve button — before the
|
|
159
|
+
* buyer sends money — rather than at submission, after they have.
|
|
160
|
+
*
|
|
161
|
+
* Generic over the verifier shape so tests can pass simplified
|
|
162
|
+
* structs without importing the full Ponder verifier type.
|
|
163
|
+
*
|
|
164
|
+
* The original implementation inlined a 3-loop chain in BrowsePanel's
|
|
165
|
+
* 30-line `useMemo`. Extracting to a pure helper makes the fallback
|
|
166
|
+
* order reviewable on its own AND lets us pin each branch without
|
|
167
|
+
* mounting the panel.
|
|
168
|
+
*/
|
|
169
|
+
export function resolveTargetedVerifier({ deposit, paymentMethod, currency, }) {
|
|
170
|
+
if (!deposit)
|
|
171
|
+
return null;
|
|
172
|
+
// The rail's own payment method, and only if it has a rate for this
|
|
173
|
+
// currency. This is the method an attestation can actually satisfy.
|
|
174
|
+
const mainRate = deposit.currentConversionRates
|
|
175
|
+
.get(paymentMethod)
|
|
176
|
+
?.get(currency.contractId)?.value;
|
|
177
|
+
if (!mainRate)
|
|
178
|
+
return null;
|
|
179
|
+
const verifier = deposit.getVerifier(paymentMethod);
|
|
180
|
+
if (!verifier)
|
|
181
|
+
return null;
|
|
182
|
+
return { providerKey: paymentMethod, verifier };
|
|
183
|
+
}
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Volume map + monthly default
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
/**
|
|
188
|
+
* Build a `ProviderKey → totalVolume` map from the indexer's ordered
|
|
189
|
+
* verifier list. Used as the tiebreaker when multiple deposits share
|
|
190
|
+
* the same lowest rate in `useDefaultProviderByRate`.
|
|
191
|
+
*
|
|
192
|
+
* Returns an empty map when the input is missing or malformed —
|
|
193
|
+
* downstream callers treat "no volume data" as "no tiebreaker
|
|
194
|
+
* available", same as a zero entry.
|
|
195
|
+
*/
|
|
196
|
+
export function buildVolumeByProviderKey(verifiersByVolume) {
|
|
197
|
+
const map = new Map();
|
|
198
|
+
if (!verifiersByVolume)
|
|
199
|
+
return map;
|
|
200
|
+
for (const entry of verifiersByVolume) {
|
|
201
|
+
if (entry.verifier) {
|
|
202
|
+
map.set(entry.verifier, entry.totalVolume);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return map;
|
|
206
|
+
}
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// Form validity gate
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
/**
|
|
211
|
+
* Whole-form validity gate for the BrowsePanel Reserve CTA. Returns
|
|
212
|
+
* `true` only when EVERY precondition is met — any single false
|
|
213
|
+
* input disables the CTA.
|
|
214
|
+
*
|
|
215
|
+
* Order of fields here doesn't drive the disabled-label priority
|
|
216
|
+
* chain (that's in `reserveDisabledLabel`). This is the boolean
|
|
217
|
+
* AND of "can we submit at all?"
|
|
218
|
+
*/
|
|
219
|
+
export function isBrowseFormValid({ amountInInt, amountOutInt, hasSelectedDeposit, depositRate, amountExceedsCap, makerUnavailable, isOnCooldown, }) {
|
|
220
|
+
if (!amountInInt)
|
|
221
|
+
return false;
|
|
222
|
+
if (!amountOutInt)
|
|
223
|
+
return false;
|
|
224
|
+
if (!hasSelectedDeposit)
|
|
225
|
+
return false;
|
|
226
|
+
if (!depositRate)
|
|
227
|
+
return false;
|
|
228
|
+
if (amountExceedsCap)
|
|
229
|
+
return false;
|
|
230
|
+
if (makerUnavailable)
|
|
231
|
+
return false;
|
|
232
|
+
if (isOnCooldown)
|
|
233
|
+
return false;
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Provider / currency reconciliation
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
/**
|
|
240
|
+
* Decide whether the selected provider has to move because it cannot serve
|
|
241
|
+
* the selected currency, and to what.
|
|
242
|
+
*
|
|
243
|
+
* The buy panel's auto-pick only runs in auto mode, so a manual pick or a
|
|
244
|
+
* shared link can leave a buyer on a rail with no matching liquidity — Venmo
|
|
245
|
+
* is a United States dollar rail, so `?provider=venmo¤cy=GBP` shows an
|
|
246
|
+
* empty order book and says nothing about why.
|
|
247
|
+
*
|
|
248
|
+
* @param selectedProvider - What the URL currently holds.
|
|
249
|
+
* @param fallbackProvider - Where to move the buyer. The caller passes its
|
|
250
|
+
* volume-ranked, currency-filtered default so the replacement is the same
|
|
251
|
+
* rail auto mode would have chosen.
|
|
252
|
+
* @returns The provider to switch to, or `null` to leave the selection alone.
|
|
253
|
+
*
|
|
254
|
+
* Returns `null` in two distinct situations, deliberately collapsed because
|
|
255
|
+
* the caller's action is identical — do nothing:
|
|
256
|
+
*
|
|
257
|
+
* 1. The selection is already fine.
|
|
258
|
+
* 2. The fallback cannot serve the currency either, so no rail this deployment
|
|
259
|
+
* offers can. Moving the buyer would swap one dead end for another,
|
|
260
|
+
* and returning the fallback here would make the caller's write re-trigger
|
|
261
|
+
* this check forever.
|
|
262
|
+
*/
|
|
263
|
+
export function reconcileProviderForCurrency({ selectedProvider, fallbackProvider, chainId, currency, }) {
|
|
264
|
+
if (providerSupportsCurrency({ providerKey: selectedProvider, chainId, currency }))
|
|
265
|
+
return null;
|
|
266
|
+
if (!providerSupportsCurrency({ providerKey: fallbackProvider, chainId, currency }))
|
|
267
|
+
return null;
|
|
268
|
+
return fallbackProvider;
|
|
269
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for the pure derivations behind the buy panel.
|
|
3
|
+
*
|
|
4
|
+
* `browsePanelDerivations.ts` exists so these can be checked without mounting
|
|
5
|
+
* BrowsePanel. The reconciliation suite guards a bug that put buyers on a rail
|
|
6
|
+
* their currency cannot settle on. The rest cover helpers whose failure modes
|
|
7
|
+
* are silent (a swallowed keystroke, a wrong-by-10000 amount) rather than loud.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|