@provex/utils 1.11.0 → 2.0.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/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,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview What the buy panel tells the buyer about provider fillability.
|
|
3
|
+
*
|
|
4
|
+
* One pure decision turns the fillability summaries (from
|
|
5
|
+
* `useProviderFillability`) and the buyer's state into at most one notice.
|
|
6
|
+
* BrowsePanel renders what this returns and decides nothing itself.
|
|
7
|
+
*
|
|
8
|
+
* The rules, from the product owner:
|
|
9
|
+
* 1. A zero or empty amount never shows an error. There is nothing to fill yet.
|
|
10
|
+
* 2. Auto mode with no amount cycles through the providers that have sellers
|
|
11
|
+
* (`carousel`). Display only. It ends when the buyer types an amount. With
|
|
12
|
+
* reduced motion, or with fewer than two providers, nothing cycles.
|
|
13
|
+
* 3. Auto mode with an amount follows the auto pick. The panel does not fall
|
|
14
|
+
* back to the volume leader when nothing fills (see `nearestAmount`).
|
|
15
|
+
* 4. A manual pick that cannot fill the amount is kept. If another provider
|
|
16
|
+
* can, offer a one-tap switch to the best-rate one (`switchProvider`).
|
|
17
|
+
* Never override the pick.
|
|
18
|
+
* 5. If no provider can fill the amount, suggest the nearest amount that works
|
|
19
|
+
* (`nearestAmount`). The suggestion comes from the provider the buyer is on:
|
|
20
|
+
* the selected one in manual mode, the one auto names otherwise. A manual
|
|
21
|
+
* pick never gets another provider's amount; that is the switch segment's job.
|
|
22
|
+
* 6. "Can fill" includes the buyer's per-order cap. A provider whose cap is
|
|
23
|
+
* below the amount does not count, and a suggestion never exceeds the cap.
|
|
24
|
+
*/
|
|
25
|
+
import type { ProviderKey } from '../payment.js';
|
|
26
|
+
import type { ProviderSelectionMode } from './providerSelectionMode.js';
|
|
27
|
+
/**
|
|
28
|
+
* What the decisions below read from one provider's fillability summary.
|
|
29
|
+
*
|
|
30
|
+
* The indexer client's `ProviderFillability` satisfies this shape. It is
|
|
31
|
+
* declared here because the indexer client depends on this package, so this
|
|
32
|
+
* package cannot import its type. Deposits are opaque: the decisions only
|
|
33
|
+
* count them and compare them by identity.
|
|
34
|
+
*/
|
|
35
|
+
export interface FillabilitySummary {
|
|
36
|
+
providerKey: ProviderKey;
|
|
37
|
+
/** Deposits that fill the buyer amount, best rate first. */
|
|
38
|
+
fillingDeposits: readonly unknown[];
|
|
39
|
+
/** One range per viable deposit, carrying the rate that deposit offers. */
|
|
40
|
+
fillableRanges: readonly {
|
|
41
|
+
deposit: unknown;
|
|
42
|
+
rate: bigint;
|
|
43
|
+
}[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The closest amount a buyer could enter instead, when their amount fills
|
|
47
|
+
* nowhere. `raise` means the buyer must enter more, `lower` means less.
|
|
48
|
+
* `reason` says which limit the amount sits on.
|
|
49
|
+
*
|
|
50
|
+
* Mirrors `NearestWorkableAmount` in the indexer client, which produces it.
|
|
51
|
+
* A type test in the ui package fails when the two drift apart.
|
|
52
|
+
*/
|
|
53
|
+
export type NearestWorkableAmount = {
|
|
54
|
+
direction: 'raise';
|
|
55
|
+
reason: 'sellerMinimum';
|
|
56
|
+
amountInCurrency: bigint;
|
|
57
|
+
providerKey: ProviderKey;
|
|
58
|
+
} | {
|
|
59
|
+
direction: 'lower';
|
|
60
|
+
reason: 'buyerCap' | 'sellerFunds' | 'sellerOrderMaximum';
|
|
61
|
+
amountInCurrency: bigint;
|
|
62
|
+
providerKey: ProviderKey;
|
|
63
|
+
};
|
|
64
|
+
/** At most one notice the panel shows about fillability. */
|
|
65
|
+
export type FillabilityNotice = {
|
|
66
|
+
kind: 'none';
|
|
67
|
+
} | {
|
|
68
|
+
kind: 'carousel';
|
|
69
|
+
providerKeys: readonly ProviderKey[];
|
|
70
|
+
} | {
|
|
71
|
+
kind: 'switchProvider';
|
|
72
|
+
providerKey: ProviderKey;
|
|
73
|
+
amountInCurrency: bigint;
|
|
74
|
+
} | ({
|
|
75
|
+
kind: 'nearestAmount';
|
|
76
|
+
} & NearestWorkableAmount);
|
|
77
|
+
export declare const NO_NOTICE: FillabilityNotice;
|
|
78
|
+
export interface DecideFillabilityNoticeInput {
|
|
79
|
+
mode: ProviderSelectionMode;
|
|
80
|
+
/** Buyer amount in currency base units. Null or 0n means "no amount yet". */
|
|
81
|
+
amountInCurrency: bigint | null;
|
|
82
|
+
selectedProvider: ProviderKey;
|
|
83
|
+
summaries: readonly FillabilitySummary[];
|
|
84
|
+
/**
|
|
85
|
+
* The closest amount the given provider can fill, or the closest across all
|
|
86
|
+
* providers when given null. Null result: nothing to suggest.
|
|
87
|
+
*/
|
|
88
|
+
nearestWorkableAmountFor: (providerKey: ProviderKey | null) => NearestWorkableAmount | null;
|
|
89
|
+
/** Providers with sellers, cheapest first. */
|
|
90
|
+
providersWithLiquidity: readonly ProviderKey[];
|
|
91
|
+
prefersReducedMotion: boolean;
|
|
92
|
+
/** True while the first deposit fetch has not finished. */
|
|
93
|
+
isLoading: boolean;
|
|
94
|
+
/** Providers the buyer cannot use right now (cooldown). Excluded from the switch offer. */
|
|
95
|
+
isProviderExcluded?: (providerKey: ProviderKey) => boolean;
|
|
96
|
+
/** True when the amount is above the buyer's per-order cap for this provider. */
|
|
97
|
+
exceedsCapFor?: (providerKey: ProviderKey) => boolean;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The provider with the lowest rate among those that can fill the amount.
|
|
101
|
+
* Same order as the auto pick: rate ascending, then provider key for a stable
|
|
102
|
+
* result. Skips excluded providers and the provider named in `except`.
|
|
103
|
+
*
|
|
104
|
+
* @returns The provider key, or null when none can fill
|
|
105
|
+
*/
|
|
106
|
+
export declare function bestRateFillingProvider({ summaries, isProviderExcluded, except, }: {
|
|
107
|
+
summaries: readonly FillabilitySummary[];
|
|
108
|
+
isProviderExcluded?: (providerKey: ProviderKey) => boolean;
|
|
109
|
+
except?: ProviderKey;
|
|
110
|
+
}): ProviderKey | null;
|
|
111
|
+
/** Decide the one fillability notice for the buy panel. */
|
|
112
|
+
export declare function decideFillabilityNotice({ mode, amountInCurrency, selectedProvider, summaries, nearestWorkableAmountFor, providersWithLiquidity, prefersReducedMotion, isLoading, isProviderExcluded, exceedsCapFor, }: DecideFillabilityNoticeInput): FillabilityNotice;
|
|
113
|
+
/**
|
|
114
|
+
* Whether the "No deposits" badge shows on the send card.
|
|
115
|
+
* It is an error badge, so it stays hidden when there is no amount, while
|
|
116
|
+
* data loads, when deposits are listed, or when a notice already explains it.
|
|
117
|
+
*/
|
|
118
|
+
export declare function shouldShowNoDepositsBadge({ amountInCurrency, hasListedDeposits, isLoading, notice, }: {
|
|
119
|
+
amountInCurrency: bigint | null;
|
|
120
|
+
hasListedDeposits: boolean;
|
|
121
|
+
isLoading: boolean;
|
|
122
|
+
notice: FillabilityNotice;
|
|
123
|
+
}): boolean;
|
|
124
|
+
/** Why the pill names the provider it names. */
|
|
125
|
+
export type AutoProviderBasis =
|
|
126
|
+
/** Nothing has loaded yet: the only state without a provider. */
|
|
127
|
+
'pending'
|
|
128
|
+
/** Deposits are in but the maker check is not: a best guess from unchecked sellers. */
|
|
129
|
+
| 'provisional'
|
|
130
|
+
/** The best-rate provider that can fill the amount, or the best-rate provider with sellers when there is no amount. */
|
|
131
|
+
| 'fills'
|
|
132
|
+
/** An amount no provider can fill: the provider the Min or Max button uses. */
|
|
133
|
+
| 'nearest'
|
|
134
|
+
/** No seller anywhere for this currency: the volume default. */
|
|
135
|
+
| 'volume-default';
|
|
136
|
+
export interface AutoProvider {
|
|
137
|
+
/** The provider auto shows and works with. Null only for `pending`. */
|
|
138
|
+
provider: ProviderKey | null;
|
|
139
|
+
basis: AutoProviderBasis;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Decide which provider auto mode shows. Auto always names a concrete provider,
|
|
143
|
+
* the one the buyer would actually use; the only state without one is the wait
|
|
144
|
+
* for the very first deposits.
|
|
145
|
+
*
|
|
146
|
+
* 1. No deposits fetched yet: `pending` (a brief skeleton).
|
|
147
|
+
* 2. Deposits fetched, maker check still running: `provisional`, the best-rate
|
|
148
|
+
* provider among UNCHECKED sellers. It names a provider early. The receive
|
|
149
|
+
* card stays a skeleton and no notice shows until the check ends, so nothing
|
|
150
|
+
* claims "no sellers" about a provider that may change; if the check removes
|
|
151
|
+
* the sellers, the pill moves once.
|
|
152
|
+
* 3. Checked, no amount: the best-rate provider with sellers (the carousel, when
|
|
153
|
+
* it runs, only cycles the display).
|
|
154
|
+
* 4. Checked, an amount some provider fills within the buyer's cap: the best-rate
|
|
155
|
+
* one that fills it.
|
|
156
|
+
* 5. Checked, an amount nothing fills: the provider of the nearest-amount
|
|
157
|
+
* suggestion, so the pill and the Min or Max button name the same provider.
|
|
158
|
+
* 6. No seller anywhere: the volume default. The receive card and the notice
|
|
159
|
+
* explain there are no sellers.
|
|
160
|
+
*/
|
|
161
|
+
export declare function decideAutoProvider({ amountInCurrency, hasFetchedDeposits, isVerifying, isRankingPending, provisionalProvider, bestRateProvider, cheapestFillingProvider, someProviderFills, nearestWorkableAmount, providersWithLiquidity, volumeDefault, }: {
|
|
162
|
+
amountInCurrency: bigint | null;
|
|
163
|
+
/** The first cross-provider deposit fetch has finished. */
|
|
164
|
+
hasFetchedDeposits: boolean;
|
|
165
|
+
/** The maker check is still running. */
|
|
166
|
+
isVerifying: boolean;
|
|
167
|
+
/** The completion-rate ranking is still loading. */
|
|
168
|
+
isRankingPending: boolean;
|
|
169
|
+
/** Best-rate provider among unchecked sellers, or null. */
|
|
170
|
+
provisionalProvider: ProviderKey | null;
|
|
171
|
+
/** The ranked pick (best rate that clears the completion gate), or null. */
|
|
172
|
+
bestRateProvider: ProviderKey | null;
|
|
173
|
+
/** Cheapest provider that can serve the amount, ignoring the completion gate. */
|
|
174
|
+
cheapestFillingProvider: ProviderKey | null;
|
|
175
|
+
/** Some provider fills the amount within the buyer's cap. */
|
|
176
|
+
someProviderFills: boolean;
|
|
177
|
+
nearestWorkableAmount: NearestWorkableAmount | null;
|
|
178
|
+
/** Providers with checked sellers, cheapest first. */
|
|
179
|
+
providersWithLiquidity: readonly ProviderKey[];
|
|
180
|
+
/** The highest-volume provider that serves the currency. A concrete last resort. */
|
|
181
|
+
volumeDefault: ProviderKey;
|
|
182
|
+
}): AutoProvider;
|
|
183
|
+
/** What the provider pill and the "You Receive" card show while data loads. */
|
|
184
|
+
export interface LoadingPresentation {
|
|
185
|
+
/**
|
|
186
|
+
* `loading`: a skeleton, only before auto has any provider to name.
|
|
187
|
+
* `selected`: show the provider. There is no neutral "Auto" state: auto always
|
|
188
|
+
* names a concrete provider.
|
|
189
|
+
*/
|
|
190
|
+
pill: 'selected' | 'loading';
|
|
191
|
+
/** `skeleton` replaces any "no sellers" text until the data is settled. */
|
|
192
|
+
receiveCard: 'normal' | 'skeleton';
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Decide how the pill and the receive card look while data is still loading.
|
|
196
|
+
*
|
|
197
|
+
* In auto mode the pill is a skeleton only until auto has a provider to name
|
|
198
|
+
* (see {@link decideAutoProvider}). In manual mode the pick is the buyer's own
|
|
199
|
+
* and always shows. In both modes the receive card shows a skeleton, never "no
|
|
200
|
+
* sellers", until loading ends, and in auto mode also while the pick can still
|
|
201
|
+
* move (provisional).
|
|
202
|
+
*/
|
|
203
|
+
export declare function decideLoadingPresentation({ mode, amountInCurrency, isFillabilityPending, isDepositsPending, isAutoPickPending, autoProvider, }: {
|
|
204
|
+
mode: ProviderSelectionMode;
|
|
205
|
+
amountInCurrency: bigint | null;
|
|
206
|
+
/** Cross-provider deposits or maker verification are still loading. */
|
|
207
|
+
isFillabilityPending: boolean;
|
|
208
|
+
/** The selected provider's own deposit list or maker check is still loading. */
|
|
209
|
+
isDepositsPending: boolean;
|
|
210
|
+
/** Auto mode only: the ranking is still being computed. */
|
|
211
|
+
isAutoPickPending: boolean;
|
|
212
|
+
/** What auto names; null only before the first data. */
|
|
213
|
+
autoProvider: AutoProvider;
|
|
214
|
+
}): LoadingPresentation;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview What the buy panel tells the buyer about provider fillability.
|
|
3
|
+
*
|
|
4
|
+
* One pure decision turns the fillability summaries (from
|
|
5
|
+
* `useProviderFillability`) and the buyer's state into at most one notice.
|
|
6
|
+
* BrowsePanel renders what this returns and decides nothing itself.
|
|
7
|
+
*
|
|
8
|
+
* The rules, from the product owner:
|
|
9
|
+
* 1. A zero or empty amount never shows an error. There is nothing to fill yet.
|
|
10
|
+
* 2. Auto mode with no amount cycles through the providers that have sellers
|
|
11
|
+
* (`carousel`). Display only. It ends when the buyer types an amount. With
|
|
12
|
+
* reduced motion, or with fewer than two providers, nothing cycles.
|
|
13
|
+
* 3. Auto mode with an amount follows the auto pick. The panel does not fall
|
|
14
|
+
* back to the volume leader when nothing fills (see `nearestAmount`).
|
|
15
|
+
* 4. A manual pick that cannot fill the amount is kept. If another provider
|
|
16
|
+
* can, offer a one-tap switch to the best-rate one (`switchProvider`).
|
|
17
|
+
* Never override the pick.
|
|
18
|
+
* 5. If no provider can fill the amount, suggest the nearest amount that works
|
|
19
|
+
* (`nearestAmount`). The suggestion comes from the provider the buyer is on:
|
|
20
|
+
* the selected one in manual mode, the one auto names otherwise. A manual
|
|
21
|
+
* pick never gets another provider's amount; that is the switch segment's job.
|
|
22
|
+
* 6. "Can fill" includes the buyer's per-order cap. A provider whose cap is
|
|
23
|
+
* below the amount does not count, and a suggestion never exceeds the cap.
|
|
24
|
+
*/
|
|
25
|
+
export const NO_NOTICE = { kind: 'none' };
|
|
26
|
+
/**
|
|
27
|
+
* Rate a provider offers on its best filling deposit, or null when none fills.
|
|
28
|
+
* `fillingDeposits` is sorted best rate first, and each filling deposit has a
|
|
29
|
+
* range carrying its rate.
|
|
30
|
+
*/
|
|
31
|
+
function bestFillingRate(summary) {
|
|
32
|
+
const bestDeposit = summary.fillingDeposits[0];
|
|
33
|
+
if (!bestDeposit)
|
|
34
|
+
return null;
|
|
35
|
+
const range = summary.fillableRanges.find((candidate) => candidate.deposit === bestDeposit);
|
|
36
|
+
return range ? range.rate : null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The provider with the lowest rate among those that can fill the amount.
|
|
40
|
+
* Same order as the auto pick: rate ascending, then provider key for a stable
|
|
41
|
+
* result. Skips excluded providers and the provider named in `except`.
|
|
42
|
+
*
|
|
43
|
+
* @returns The provider key, or null when none can fill
|
|
44
|
+
*/
|
|
45
|
+
export function bestRateFillingProvider({ summaries, isProviderExcluded, except, }) {
|
|
46
|
+
let best = null;
|
|
47
|
+
for (const summary of summaries) {
|
|
48
|
+
if (summary.providerKey === except)
|
|
49
|
+
continue;
|
|
50
|
+
if (isProviderExcluded?.(summary.providerKey))
|
|
51
|
+
continue;
|
|
52
|
+
const rate = bestFillingRate(summary);
|
|
53
|
+
if (rate === null)
|
|
54
|
+
continue;
|
|
55
|
+
const beatsBest = best === null ||
|
|
56
|
+
rate < best.rate ||
|
|
57
|
+
(rate === best.rate && summary.providerKey < best.providerKey);
|
|
58
|
+
if (beatsBest)
|
|
59
|
+
best = { providerKey: summary.providerKey, rate };
|
|
60
|
+
}
|
|
61
|
+
return best ? best.providerKey : null;
|
|
62
|
+
}
|
|
63
|
+
/** Decide the one fillability notice for the buy panel. */
|
|
64
|
+
export function decideFillabilityNotice({ mode, amountInCurrency, selectedProvider, summaries, nearestWorkableAmountFor, providersWithLiquidity, prefersReducedMotion, isLoading, isProviderExcluded, exceedsCapFor, }) {
|
|
65
|
+
if (isLoading)
|
|
66
|
+
return NO_NOTICE;
|
|
67
|
+
if (!amountInCurrency) {
|
|
68
|
+
const canCycle = mode === 'auto' && !prefersReducedMotion && providersWithLiquidity.length >= 2;
|
|
69
|
+
return canCycle ? { kind: 'carousel', providerKeys: providersWithLiquidity } : NO_NOTICE;
|
|
70
|
+
}
|
|
71
|
+
const fillsWithinCap = (summary) => summary.fillingDeposits.length > 0 && !exceedsCapFor?.(summary.providerKey);
|
|
72
|
+
const selectedSummary = summaries.find((summary) => summary.providerKey === selectedProvider);
|
|
73
|
+
if (selectedSummary && fillsWithinCap(selectedSummary))
|
|
74
|
+
return NO_NOTICE;
|
|
75
|
+
const someProviderFills = summaries.some(fillsWithinCap);
|
|
76
|
+
if (!someProviderFills) {
|
|
77
|
+
const nearestWorkableAmount = nearestWorkableAmountFor(mode === 'manual' ? selectedProvider : null);
|
|
78
|
+
return nearestWorkableAmount ? { kind: 'nearestAmount', ...nearestWorkableAmount } : NO_NOTICE;
|
|
79
|
+
}
|
|
80
|
+
// Auto mode: the auto pick lands on a provider that fills. Nothing to say.
|
|
81
|
+
if (mode === 'auto')
|
|
82
|
+
return NO_NOTICE;
|
|
83
|
+
const alternative = bestRateFillingProvider({
|
|
84
|
+
summaries,
|
|
85
|
+
isProviderExcluded: (providerKey) => !!isProviderExcluded?.(providerKey) || !!exceedsCapFor?.(providerKey),
|
|
86
|
+
except: selectedProvider,
|
|
87
|
+
});
|
|
88
|
+
return alternative
|
|
89
|
+
? { kind: 'switchProvider', providerKey: alternative, amountInCurrency }
|
|
90
|
+
: NO_NOTICE;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Whether the "No deposits" badge shows on the send card.
|
|
94
|
+
* It is an error badge, so it stays hidden when there is no amount, while
|
|
95
|
+
* data loads, when deposits are listed, or when a notice already explains it.
|
|
96
|
+
*/
|
|
97
|
+
export function shouldShowNoDepositsBadge({ amountInCurrency, hasListedDeposits, isLoading, notice, }) {
|
|
98
|
+
if (!amountInCurrency)
|
|
99
|
+
return false;
|
|
100
|
+
if (hasListedDeposits)
|
|
101
|
+
return false;
|
|
102
|
+
if (isLoading)
|
|
103
|
+
return false;
|
|
104
|
+
return notice.kind === 'none';
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Decide which provider auto mode shows. Auto always names a concrete provider,
|
|
108
|
+
* the one the buyer would actually use; the only state without one is the wait
|
|
109
|
+
* for the very first deposits.
|
|
110
|
+
*
|
|
111
|
+
* 1. No deposits fetched yet: `pending` (a brief skeleton).
|
|
112
|
+
* 2. Deposits fetched, maker check still running: `provisional`, the best-rate
|
|
113
|
+
* provider among UNCHECKED sellers. It names a provider early. The receive
|
|
114
|
+
* card stays a skeleton and no notice shows until the check ends, so nothing
|
|
115
|
+
* claims "no sellers" about a provider that may change; if the check removes
|
|
116
|
+
* the sellers, the pill moves once.
|
|
117
|
+
* 3. Checked, no amount: the best-rate provider with sellers (the carousel, when
|
|
118
|
+
* it runs, only cycles the display).
|
|
119
|
+
* 4. Checked, an amount some provider fills within the buyer's cap: the best-rate
|
|
120
|
+
* one that fills it.
|
|
121
|
+
* 5. Checked, an amount nothing fills: the provider of the nearest-amount
|
|
122
|
+
* suggestion, so the pill and the Min or Max button name the same provider.
|
|
123
|
+
* 6. No seller anywhere: the volume default. The receive card and the notice
|
|
124
|
+
* explain there are no sellers.
|
|
125
|
+
*/
|
|
126
|
+
export function decideAutoProvider({ amountInCurrency, hasFetchedDeposits, isVerifying, isRankingPending, provisionalProvider, bestRateProvider, cheapestFillingProvider, someProviderFills, nearestWorkableAmount, providersWithLiquidity, volumeDefault, }) {
|
|
127
|
+
if (!hasFetchedDeposits)
|
|
128
|
+
return { provider: null, basis: 'pending' };
|
|
129
|
+
if (isVerifying) {
|
|
130
|
+
return provisionalProvider
|
|
131
|
+
? { provider: provisionalProvider, basis: 'provisional' }
|
|
132
|
+
: { provider: null, basis: 'pending' };
|
|
133
|
+
}
|
|
134
|
+
const ranked = isRankingPending ? null : bestRateProvider;
|
|
135
|
+
if (!amountInCurrency) {
|
|
136
|
+
const provider = ranked ?? cheapestFillingProvider ?? providersWithLiquidity[0] ?? null;
|
|
137
|
+
return provider ? { provider, basis: 'fills' } : { provider: volumeDefault, basis: 'volume-default' };
|
|
138
|
+
}
|
|
139
|
+
if (someProviderFills) {
|
|
140
|
+
const provider = ranked ?? cheapestFillingProvider;
|
|
141
|
+
return provider ? { provider, basis: 'fills' } : { provider: volumeDefault, basis: 'volume-default' };
|
|
142
|
+
}
|
|
143
|
+
if (nearestWorkableAmount)
|
|
144
|
+
return { provider: nearestWorkableAmount.providerKey, basis: 'nearest' };
|
|
145
|
+
return { provider: volumeDefault, basis: 'volume-default' };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Decide how the pill and the receive card look while data is still loading.
|
|
149
|
+
*
|
|
150
|
+
* In auto mode the pill is a skeleton only until auto has a provider to name
|
|
151
|
+
* (see {@link decideAutoProvider}). In manual mode the pick is the buyer's own
|
|
152
|
+
* and always shows. In both modes the receive card shows a skeleton, never "no
|
|
153
|
+
* sellers", until loading ends, and in auto mode also while the pick can still
|
|
154
|
+
* move (provisional).
|
|
155
|
+
*/
|
|
156
|
+
export function decideLoadingPresentation({ mode, amountInCurrency, isFillabilityPending, isDepositsPending, isAutoPickPending, autoProvider, }) {
|
|
157
|
+
const autoPickUnsettled = mode === 'auto' && (isFillabilityPending || isAutoPickPending);
|
|
158
|
+
const receivePending = isFillabilityPending || isDepositsPending || autoPickUnsettled;
|
|
159
|
+
const receiveCard = !!amountInCurrency && receivePending ? 'skeleton' : 'normal';
|
|
160
|
+
const pill = mode === 'auto' && autoProvider.provider === null ? 'loading' : 'selected';
|
|
161
|
+
return { pill, receiveCard };
|
|
162
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for the fillability decisions, with plain-object summaries.
|
|
3
|
+
*
|
|
4
|
+
* The user-interface package keeps the end-to-end suite, which builds real
|
|
5
|
+
* deposits through the indexer client. These tests pin the decisions alone, so
|
|
6
|
+
* this package can change them without that client.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for the fillability decisions, with plain-object summaries.
|
|
3
|
+
*
|
|
4
|
+
* The user-interface package keeps the end-to-end suite, which builds real
|
|
5
|
+
* deposits through the indexer client. These tests pin the decisions alone, so
|
|
6
|
+
* this package can change them without that client.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it } from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { providers } from '../payment.js';
|
|
11
|
+
import { bestRateFillingProvider, decideAutoProvider, decideFillabilityNotice, decideLoadingPresentation, shouldShowNoDepositsBadge, } from './fillabilityNotice.js';
|
|
12
|
+
const { VENMO, CASHAPP, ZELLE } = providers;
|
|
13
|
+
/** A summary whose deposits fill the amount at the given rate, or fill nothing when rate is null. */
|
|
14
|
+
const summary = (providerKey, rate) => {
|
|
15
|
+
if (rate === null)
|
|
16
|
+
return { providerKey, fillingDeposits: [], fillableRanges: [] };
|
|
17
|
+
const deposit = { providerKey };
|
|
18
|
+
return { providerKey, fillingDeposits: [deposit], fillableRanges: [{ deposit, rate }] };
|
|
19
|
+
};
|
|
20
|
+
const nearest = {
|
|
21
|
+
direction: 'raise',
|
|
22
|
+
reason: 'sellerMinimum',
|
|
23
|
+
amountInCurrency: 500n,
|
|
24
|
+
providerKey: CASHAPP,
|
|
25
|
+
};
|
|
26
|
+
const baseInput = (overrides) => ({
|
|
27
|
+
mode: 'auto',
|
|
28
|
+
amountInCurrency: 1000n,
|
|
29
|
+
selectedProvider: VENMO,
|
|
30
|
+
summaries: [summary(VENMO, 102n), summary(CASHAPP, 101n)],
|
|
31
|
+
nearestWorkableAmountFor: () => null,
|
|
32
|
+
providersWithLiquidity: [CASHAPP, VENMO],
|
|
33
|
+
prefersReducedMotion: false,
|
|
34
|
+
isLoading: false,
|
|
35
|
+
...overrides,
|
|
36
|
+
});
|
|
37
|
+
describe('decideFillabilityNotice', () => {
|
|
38
|
+
it('says nothing while data loads', () => {
|
|
39
|
+
assert.deepEqual(decideFillabilityNotice(baseInput({ isLoading: true })), { kind: 'none' });
|
|
40
|
+
});
|
|
41
|
+
it('cycles providers in auto mode with no amount', () => {
|
|
42
|
+
assert.deepEqual(decideFillabilityNotice(baseInput({ amountInCurrency: null })), {
|
|
43
|
+
kind: 'carousel',
|
|
44
|
+
providerKeys: [CASHAPP, VENMO],
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
it('does not cycle with reduced motion or a single provider', () => {
|
|
48
|
+
assert.deepEqual(decideFillabilityNotice(baseInput({ amountInCurrency: null, prefersReducedMotion: true })), {
|
|
49
|
+
kind: 'none',
|
|
50
|
+
});
|
|
51
|
+
assert.deepEqual(decideFillabilityNotice(baseInput({ amountInCurrency: null, providersWithLiquidity: [VENMO] })), {
|
|
52
|
+
kind: 'none',
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
it('offers a switch to the cheapest filling provider when a manual pick cannot fill', () => {
|
|
56
|
+
const notice = decideFillabilityNotice(baseInput({
|
|
57
|
+
mode: 'manual',
|
|
58
|
+
selectedProvider: ZELLE,
|
|
59
|
+
summaries: [summary(ZELLE, null), summary(VENMO, 102n), summary(CASHAPP, 101n)],
|
|
60
|
+
}));
|
|
61
|
+
assert.deepEqual(notice, { kind: 'switchProvider', providerKey: CASHAPP, amountInCurrency: 1000n });
|
|
62
|
+
});
|
|
63
|
+
it('keeps a manual pick that fills', () => {
|
|
64
|
+
const notice = decideFillabilityNotice(baseInput({ mode: 'manual', selectedProvider: VENMO }));
|
|
65
|
+
assert.deepEqual(notice, { kind: 'none' });
|
|
66
|
+
});
|
|
67
|
+
it('treats a provider over the buyer cap as unable to fill', () => {
|
|
68
|
+
const notice = decideFillabilityNotice(baseInput({ mode: 'manual', selectedProvider: VENMO, exceedsCapFor: (providerKey) => providerKey === VENMO }));
|
|
69
|
+
assert.deepEqual(notice, { kind: 'switchProvider', providerKey: CASHAPP, amountInCurrency: 1000n });
|
|
70
|
+
});
|
|
71
|
+
it('suggests the nearest amount when no provider fills', () => {
|
|
72
|
+
const asked = [];
|
|
73
|
+
const notice = decideFillabilityNotice(baseInput({
|
|
74
|
+
mode: 'manual',
|
|
75
|
+
selectedProvider: VENMO,
|
|
76
|
+
summaries: [summary(VENMO, null), summary(CASHAPP, null)],
|
|
77
|
+
nearestWorkableAmountFor: (providerKey) => {
|
|
78
|
+
asked.push(providerKey);
|
|
79
|
+
return nearest;
|
|
80
|
+
},
|
|
81
|
+
}));
|
|
82
|
+
assert.deepEqual(notice, { kind: 'nearestAmount', ...nearest });
|
|
83
|
+
// A manual pick never gets another provider's amount.
|
|
84
|
+
assert.deepEqual(asked, [VENMO]);
|
|
85
|
+
});
|
|
86
|
+
it('asks across all providers in auto mode', () => {
|
|
87
|
+
const asked = [];
|
|
88
|
+
decideFillabilityNotice(baseInput({
|
|
89
|
+
summaries: [summary(VENMO, null)],
|
|
90
|
+
nearestWorkableAmountFor: (providerKey) => {
|
|
91
|
+
asked.push(providerKey);
|
|
92
|
+
return null;
|
|
93
|
+
},
|
|
94
|
+
}));
|
|
95
|
+
assert.deepEqual(asked, [null]);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('decideFillabilityNotice: other providers fill but the buyer is not on one', () => {
|
|
99
|
+
it('says nothing in auto mode, because auto already lands on a provider that fills', () => {
|
|
100
|
+
const notice = decideFillabilityNotice(baseInput({ mode: 'auto', selectedProvider: ZELLE, summaries: [summary(ZELLE, null), summary(CASHAPP, 101n)] }));
|
|
101
|
+
assert.deepEqual(notice, { kind: 'none' });
|
|
102
|
+
});
|
|
103
|
+
it('offers no switch when every other filling provider is in cooldown', () => {
|
|
104
|
+
const notice = decideFillabilityNotice(baseInput({
|
|
105
|
+
mode: 'manual',
|
|
106
|
+
selectedProvider: ZELLE,
|
|
107
|
+
summaries: [summary(ZELLE, null), summary(CASHAPP, 101n)],
|
|
108
|
+
isProviderExcluded: (providerKey) => providerKey === CASHAPP,
|
|
109
|
+
}));
|
|
110
|
+
assert.deepEqual(notice, { kind: 'none' });
|
|
111
|
+
});
|
|
112
|
+
it('offers no switch when every other filling provider is over the buyer cap', () => {
|
|
113
|
+
const notice = decideFillabilityNotice(baseInput({
|
|
114
|
+
mode: 'manual',
|
|
115
|
+
selectedProvider: ZELLE,
|
|
116
|
+
summaries: [summary(ZELLE, null), summary(CASHAPP, 101n)],
|
|
117
|
+
exceedsCapFor: (providerKey) => providerKey === CASHAPP,
|
|
118
|
+
}));
|
|
119
|
+
assert.deepEqual(notice, { kind: 'none' });
|
|
120
|
+
});
|
|
121
|
+
it('says nothing when no provider fills and there is no nearer amount to suggest', () => {
|
|
122
|
+
const notice = decideFillabilityNotice(baseInput({ mode: 'manual', summaries: [summary(VENMO, null)], nearestWorkableAmountFor: () => null }));
|
|
123
|
+
assert.deepEqual(notice, { kind: 'none' });
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe('bestRateFillingProvider: inconsistent summaries', () => {
|
|
127
|
+
it('ignores a filling deposit that has no rate range, so a rate is never invented', () => {
|
|
128
|
+
const orphan = { providerKey: VENMO, fillingDeposits: [{}], fillableRanges: [] };
|
|
129
|
+
assert.equal(bestRateFillingProvider({ summaries: [orphan] }), null);
|
|
130
|
+
});
|
|
131
|
+
it('keeps the earlier provider when a later one offers a worse rate', () => {
|
|
132
|
+
assert.equal(bestRateFillingProvider({ summaries: [summary(VENMO, 101n), summary(CASHAPP, 102n)] }), VENMO);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe('bestRateFillingProvider', () => {
|
|
136
|
+
const summaries = [summary(VENMO, 102n), summary(CASHAPP, 101n), summary(ZELLE, 101n)];
|
|
137
|
+
it('picks the lowest rate and breaks a tie by provider key', () => {
|
|
138
|
+
assert.equal(bestRateFillingProvider({ summaries }), CASHAPP);
|
|
139
|
+
});
|
|
140
|
+
it('skips the named provider and excluded providers', () => {
|
|
141
|
+
assert.equal(bestRateFillingProvider({ summaries, except: CASHAPP }), ZELLE);
|
|
142
|
+
assert.equal(bestRateFillingProvider({ summaries, isProviderExcluded: (providerKey) => providerKey !== VENMO }), VENMO);
|
|
143
|
+
});
|
|
144
|
+
it('returns null when none fill', () => {
|
|
145
|
+
assert.equal(bestRateFillingProvider({ summaries: [summary(VENMO, null)] }), null);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
describe('shouldShowNoDepositsBadge', () => {
|
|
149
|
+
it('shows only for a real amount, nothing listed, nothing loading and no notice', () => {
|
|
150
|
+
const base = { amountInCurrency: 500n, hasListedDeposits: false, isLoading: false, notice: { kind: 'none' } };
|
|
151
|
+
assert.equal(shouldShowNoDepositsBadge(base), true);
|
|
152
|
+
assert.equal(shouldShowNoDepositsBadge({ ...base, amountInCurrency: 0n }), false);
|
|
153
|
+
assert.equal(shouldShowNoDepositsBadge({ ...base, hasListedDeposits: true }), false);
|
|
154
|
+
assert.equal(shouldShowNoDepositsBadge({ ...base, isLoading: true }), false);
|
|
155
|
+
assert.equal(shouldShowNoDepositsBadge({ ...base, notice: { kind: 'nearestAmount', ...nearest } }), false);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
describe('decideAutoProvider', () => {
|
|
159
|
+
const settled = {
|
|
160
|
+
amountInCurrency: 1000n,
|
|
161
|
+
hasFetchedDeposits: true,
|
|
162
|
+
isVerifying: false,
|
|
163
|
+
isRankingPending: false,
|
|
164
|
+
provisionalProvider: null,
|
|
165
|
+
bestRateProvider: CASHAPP,
|
|
166
|
+
cheapestFillingProvider: VENMO,
|
|
167
|
+
someProviderFills: true,
|
|
168
|
+
nearestWorkableAmount: null,
|
|
169
|
+
providersWithLiquidity: [CASHAPP, VENMO],
|
|
170
|
+
volumeDefault: ZELLE,
|
|
171
|
+
};
|
|
172
|
+
it('names no provider before the first deposits arrive', () => {
|
|
173
|
+
assert.deepEqual(decideAutoProvider({ ...settled, hasFetchedDeposits: false }), { provider: null, basis: 'pending' });
|
|
174
|
+
});
|
|
175
|
+
it('names a provisional provider while the maker check runs', () => {
|
|
176
|
+
assert.deepEqual(decideAutoProvider({ ...settled, isVerifying: true, provisionalProvider: VENMO }), {
|
|
177
|
+
provider: VENMO,
|
|
178
|
+
basis: 'provisional',
|
|
179
|
+
});
|
|
180
|
+
assert.deepEqual(decideAutoProvider({ ...settled, isVerifying: true }), { provider: null, basis: 'pending' });
|
|
181
|
+
});
|
|
182
|
+
it('names the ranked provider that fills the amount', () => {
|
|
183
|
+
assert.deepEqual(decideAutoProvider(settled), { provider: CASHAPP, basis: 'fills' });
|
|
184
|
+
});
|
|
185
|
+
it('falls back to the cheapest filling provider while ranking loads', () => {
|
|
186
|
+
assert.deepEqual(decideAutoProvider({ ...settled, isRankingPending: true }), { provider: VENMO, basis: 'fills' });
|
|
187
|
+
});
|
|
188
|
+
it('names the provider of the nearest amount when nothing fills', () => {
|
|
189
|
+
assert.deepEqual(decideAutoProvider({ ...settled, someProviderFills: false, nearestWorkableAmount: nearest }), {
|
|
190
|
+
provider: CASHAPP,
|
|
191
|
+
basis: 'nearest',
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
it('names the first provider with liquidity when no amount is typed and nothing ranks', () => {
|
|
195
|
+
assert.deepEqual(decideAutoProvider({
|
|
196
|
+
...settled,
|
|
197
|
+
amountInCurrency: null,
|
|
198
|
+
bestRateProvider: null,
|
|
199
|
+
cheapestFillingProvider: null,
|
|
200
|
+
providersWithLiquidity: [VENMO],
|
|
201
|
+
}), { provider: VENMO, basis: 'fills' });
|
|
202
|
+
});
|
|
203
|
+
it('names the volume default when the buyer cap leaves no provider to name for an amount', () => {
|
|
204
|
+
assert.deepEqual(decideAutoProvider({ ...settled, bestRateProvider: null, cheapestFillingProvider: null }), { provider: ZELLE, basis: 'volume-default' });
|
|
205
|
+
});
|
|
206
|
+
it('names the volume default when nothing fills and there is no nearer amount', () => {
|
|
207
|
+
assert.deepEqual(decideAutoProvider({ ...settled, someProviderFills: false, nearestWorkableAmount: null }), {
|
|
208
|
+
provider: ZELLE,
|
|
209
|
+
basis: 'volume-default',
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
it('falls back to the volume default when no seller exists', () => {
|
|
213
|
+
assert.deepEqual(decideAutoProvider({
|
|
214
|
+
...settled,
|
|
215
|
+
amountInCurrency: null,
|
|
216
|
+
bestRateProvider: null,
|
|
217
|
+
cheapestFillingProvider: null,
|
|
218
|
+
providersWithLiquidity: [],
|
|
219
|
+
}), { provider: ZELLE, basis: 'volume-default' });
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
describe('decideLoadingPresentation', () => {
|
|
223
|
+
const idle = {
|
|
224
|
+
mode: 'auto',
|
|
225
|
+
amountInCurrency: 1000n,
|
|
226
|
+
isFillabilityPending: false,
|
|
227
|
+
isDepositsPending: false,
|
|
228
|
+
isAutoPickPending: false,
|
|
229
|
+
autoProvider: { provider: VENMO, basis: 'fills' },
|
|
230
|
+
};
|
|
231
|
+
it('shows a skeleton pill only in auto mode before a provider exists', () => {
|
|
232
|
+
assert.equal(decideLoadingPresentation({ ...idle, autoProvider: { provider: null, basis: 'pending' } }).pill, 'loading');
|
|
233
|
+
assert.equal(decideLoadingPresentation({ ...idle, mode: 'manual', autoProvider: { provider: null, basis: 'pending' } }).pill, 'selected');
|
|
234
|
+
});
|
|
235
|
+
it('shows a skeleton receive card instead of "no sellers" while data loads', () => {
|
|
236
|
+
assert.equal(decideLoadingPresentation({ ...idle, isDepositsPending: true }).receiveCard, 'skeleton');
|
|
237
|
+
assert.equal(decideLoadingPresentation({ ...idle, isAutoPickPending: true }).receiveCard, 'skeleton');
|
|
238
|
+
assert.equal(decideLoadingPresentation(idle).receiveCard, 'normal');
|
|
239
|
+
});
|
|
240
|
+
it('keeps the receive card normal with no amount typed', () => {
|
|
241
|
+
assert.equal(decideLoadingPresentation({ ...idle, amountInCurrency: null, isDepositsPending: true }).receiveCard, 'normal');
|
|
242
|
+
});
|
|
243
|
+
it('does not wait on the auto pick in manual mode', () => {
|
|
244
|
+
assert.equal(decideLoadingPresentation({ ...idle, mode: 'manual', isAutoPickPending: true }).receiveCard, 'normal');
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Format a currency amount held in base units.
|
|
3
|
+
*
|
|
4
|
+
* Two outputs, for two audiences:
|
|
5
|
+
* - `formatCurrencyBaseUnits` is for reading: symbol, thousands separators,
|
|
6
|
+
* and the currency's own decimals (cents for USD, none for KRW).
|
|
7
|
+
* - `toAmountInputString` is for the amount field: plain digits with a dot,
|
|
8
|
+
* no symbol and no separators, so the field's own sanitizer accepts it.
|
|
9
|
+
*/
|
|
10
|
+
import { type Currency } from '../currencies.js';
|
|
11
|
+
/**
|
|
12
|
+
* Display string such as `$5.00`, `€9.20` or `₩14,000`.
|
|
13
|
+
*
|
|
14
|
+
* @param params.amount - Amount in the currency's base units
|
|
15
|
+
* @param params.currency - Currency whose symbol and decimals apply
|
|
16
|
+
*/
|
|
17
|
+
export declare function formatCurrencyBaseUnits({ amount, currency }: {
|
|
18
|
+
amount: bigint;
|
|
19
|
+
currency: Currency;
|
|
20
|
+
}): string;
|
|
21
|
+
/**
|
|
22
|
+
* Plain decimal string for the amount input, such as `5.00` or `14000`.
|
|
23
|
+
*
|
|
24
|
+
* @param params.amount - Amount in the currency's base units
|
|
25
|
+
* @param params.currency - Currency whose decimals apply
|
|
26
|
+
*/
|
|
27
|
+
export declare function toAmountInputString({ amount, currency }: {
|
|
28
|
+
amount: bigint;
|
|
29
|
+
currency: Currency;
|
|
30
|
+
}): string;
|