@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,34 @@
|
|
|
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 { formatCurrencyAmount } 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 function formatCurrencyBaseUnits({ amount, currency }) {
|
|
18
|
+
// `formatAmount` returns null only for a null amount, which the type excludes.
|
|
19
|
+
return formatCurrencyAmount(currency.formatAmount(amount), currency.ticker);
|
|
20
|
+
}
|
|
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 function toAmountInputString({ amount, currency }) {
|
|
28
|
+
const scale = 10n ** BigInt(currency.decimals);
|
|
29
|
+
const whole = (amount / scale).toString();
|
|
30
|
+
if (currency.decimals === 0)
|
|
31
|
+
return whole;
|
|
32
|
+
const fraction = (amount % scale).toString().padStart(currency.decimals, '0');
|
|
33
|
+
return `${whole}.${fraction}`;
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for currency amounts held in base units.
|
|
3
|
+
*
|
|
4
|
+
* Two outputs serve two readers. One is for a person (symbol and separators).
|
|
5
|
+
* The other is for the amount field, whose sanitizer accepts digits and a dot.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it } from 'node:test';
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import { SUPPORTED_CURRENCIES } from '../currencies.js';
|
|
10
|
+
import { formatCurrencyBaseUnits, toAmountInputString } from './formatCurrencyBaseUnits.js';
|
|
11
|
+
const { USD, EUR, KRW } = SUPPORTED_CURRENCIES;
|
|
12
|
+
describe('formatCurrencyBaseUnits', () => {
|
|
13
|
+
it('shows a symbol and the currency cents', () => {
|
|
14
|
+
assert.equal(formatCurrencyBaseUnits({ amount: 500n, currency: USD }), '$5.00');
|
|
15
|
+
assert.equal(formatCurrencyBaseUnits({ amount: 920n, currency: EUR }), '€9.20');
|
|
16
|
+
});
|
|
17
|
+
it('shows no cents for a currency without a minor unit', () => {
|
|
18
|
+
assert.equal(formatCurrencyBaseUnits({ amount: 14000n, currency: KRW }), '₩14,000');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
describe('toAmountInputString', () => {
|
|
22
|
+
it('writes cents with a dot and no symbol', () => {
|
|
23
|
+
assert.equal(toAmountInputString({ amount: 500n, currency: USD }), '5.00');
|
|
24
|
+
});
|
|
25
|
+
it('pads a fraction below ten units to full width', () => {
|
|
26
|
+
assert.equal(toAmountInputString({ amount: 5n, currency: USD }), '0.05');
|
|
27
|
+
});
|
|
28
|
+
it('writes no separators, so the field sanitizer accepts it', () => {
|
|
29
|
+
assert.equal(toAmountInputString({ amount: 123456789n, currency: USD }), '1234567.89');
|
|
30
|
+
});
|
|
31
|
+
it('writes whole units, with no dot, for a zero-decimal currency', () => {
|
|
32
|
+
assert.equal(toAmountInputString({ amount: 14000n, currency: KRW }), '14000');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Which `fulfillIntent` transaction hash the success screen links.
|
|
3
|
+
*
|
|
4
|
+
* The buyer's own browser sends `fulfillIntent`, so it knows the hash the
|
|
5
|
+
* moment the wallet broadcasts. The indexer learns the same hash only after
|
|
6
|
+
* it has processed the mined block, which can take several seconds more.
|
|
7
|
+
* The "View on explorer" link should not wait for that.
|
|
8
|
+
*/
|
|
9
|
+
import type { Hex } from 'viem';
|
|
10
|
+
/** A hash the wallet broadcast, tagged with the intent it settles. */
|
|
11
|
+
export interface SubmittedFulfillTransaction {
|
|
12
|
+
intentHash: Hex;
|
|
13
|
+
transactionHash: Hex;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Picks the hash for the explorer link.
|
|
17
|
+
*
|
|
18
|
+
* The indexer's hash wins when present: it is the transaction that actually
|
|
19
|
+
* fulfilled the intent, which differs from the broadcast hash after a
|
|
20
|
+
* speed-up replaced it. Until the indexer has it, the broadcast hash stands
|
|
21
|
+
* in, but only for the intent it was sent for.
|
|
22
|
+
*
|
|
23
|
+
* @returns The hash to link, or null when neither source has one.
|
|
24
|
+
*/
|
|
25
|
+
export declare function pickFulfillTransactionHash(options: {
|
|
26
|
+
intentHash: Hex | null;
|
|
27
|
+
indexedTransactionHash: Hex | null;
|
|
28
|
+
submitted: SubmittedFulfillTransaction | null;
|
|
29
|
+
}): Hex | null;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Which `fulfillIntent` transaction hash the success screen links.
|
|
3
|
+
*
|
|
4
|
+
* The buyer's own browser sends `fulfillIntent`, so it knows the hash the
|
|
5
|
+
* moment the wallet broadcasts. The indexer learns the same hash only after
|
|
6
|
+
* it has processed the mined block, which can take several seconds more.
|
|
7
|
+
* The "View on explorer" link should not wait for that.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Picks the hash for the explorer link.
|
|
11
|
+
*
|
|
12
|
+
* The indexer's hash wins when present: it is the transaction that actually
|
|
13
|
+
* fulfilled the intent, which differs from the broadcast hash after a
|
|
14
|
+
* speed-up replaced it. Until the indexer has it, the broadcast hash stands
|
|
15
|
+
* in, but only for the intent it was sent for.
|
|
16
|
+
*
|
|
17
|
+
* @returns The hash to link, or null when neither source has one.
|
|
18
|
+
*/
|
|
19
|
+
export function pickFulfillTransactionHash(options) {
|
|
20
|
+
if (options.indexedTransactionHash)
|
|
21
|
+
return options.indexedTransactionHash;
|
|
22
|
+
if (!options.intentHash || !options.submitted)
|
|
23
|
+
return null;
|
|
24
|
+
if (options.submitted.intentHash.toLowerCase() !== options.intentHash.toLowerCase())
|
|
25
|
+
return null;
|
|
26
|
+
return options.submitted.transactionHash;
|
|
27
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for the hash the success screen links.
|
|
3
|
+
*
|
|
4
|
+
* The explorer link should not wait for the indexer, but it must never link a
|
|
5
|
+
* transaction that belongs to another order.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it } from 'node:test';
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import { pickFulfillTransactionHash } from './fulfillTransactionHash.js';
|
|
10
|
+
const intentHash = `0x${'a'.repeat(64)}`;
|
|
11
|
+
const otherIntentHash = `0x${'b'.repeat(64)}`;
|
|
12
|
+
const broadcastHash = `0x${'1'.repeat(64)}`;
|
|
13
|
+
const indexedHash = `0x${'2'.repeat(64)}`;
|
|
14
|
+
describe('pickFulfillTransactionHash', () => {
|
|
15
|
+
it('links the broadcast hash before the indexer has the transaction', () => {
|
|
16
|
+
assert.equal(pickFulfillTransactionHash({
|
|
17
|
+
intentHash,
|
|
18
|
+
indexedTransactionHash: null,
|
|
19
|
+
submitted: { intentHash, transactionHash: broadcastHash },
|
|
20
|
+
}), broadcastHash);
|
|
21
|
+
});
|
|
22
|
+
it('prefers the indexed hash, which is the one that settled after a speed-up', () => {
|
|
23
|
+
assert.equal(pickFulfillTransactionHash({
|
|
24
|
+
intentHash,
|
|
25
|
+
indexedTransactionHash: indexedHash,
|
|
26
|
+
submitted: { intentHash, transactionHash: broadcastHash },
|
|
27
|
+
}), indexedHash);
|
|
28
|
+
});
|
|
29
|
+
it("never links another order's transaction", () => {
|
|
30
|
+
assert.equal(pickFulfillTransactionHash({
|
|
31
|
+
intentHash,
|
|
32
|
+
indexedTransactionHash: null,
|
|
33
|
+
submitted: { intentHash: otherIntentHash, transactionHash: broadcastHash },
|
|
34
|
+
}), null);
|
|
35
|
+
});
|
|
36
|
+
it('matches the intent regardless of hex letter case', () => {
|
|
37
|
+
assert.equal(pickFulfillTransactionHash({
|
|
38
|
+
intentHash: intentHash.toUpperCase().replace('0X', '0x'),
|
|
39
|
+
indexedTransactionHash: null,
|
|
40
|
+
submitted: { intentHash, transactionHash: broadcastHash },
|
|
41
|
+
}), broadcastHash);
|
|
42
|
+
});
|
|
43
|
+
it('returns null with neither source, so no dead link renders', () => {
|
|
44
|
+
assert.equal(pickFulfillTransactionHash({ intentHash, indexedTransactionHash: null, submitted: null }), null);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pure helpers for the buy flow.
|
|
3
|
+
*
|
|
4
|
+
* Import from `@provex/utils/buy`. Nothing here touches React, a wallet, the
|
|
5
|
+
* translation layer or the indexer client, so any buy-flow surface can share
|
|
6
|
+
* these rules. Amounts stay bigint in base units until display.
|
|
7
|
+
*/
|
|
8
|
+
export * from './browsePanelDerivations.js';
|
|
9
|
+
export * from './fillabilityNotice.js';
|
|
10
|
+
export * from './formatCurrencyBaseUnits.js';
|
|
11
|
+
export * from './fulfillTransactionHash.js';
|
|
12
|
+
export * from './orderCap.js';
|
|
13
|
+
export * from './postFeeAmount.js';
|
|
14
|
+
export * from './providerSelectionMode.js';
|
|
15
|
+
export * from './settlementReconciliation.js';
|
|
16
|
+
export * from './submitStepDerivations.js';
|
|
17
|
+
export * from './verifiableBanks.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pure helpers for the buy flow.
|
|
3
|
+
*
|
|
4
|
+
* Import from `@provex/utils/buy`. Nothing here touches React, a wallet, the
|
|
5
|
+
* translation layer or the indexer client, so any buy-flow surface can share
|
|
6
|
+
* these rules. Amounts stay bigint in base units until display.
|
|
7
|
+
*/
|
|
8
|
+
export * from './browsePanelDerivations.js';
|
|
9
|
+
export * from './fillabilityNotice.js';
|
|
10
|
+
export * from './formatCurrencyBaseUnits.js';
|
|
11
|
+
export * from './fulfillTransactionHash.js';
|
|
12
|
+
export * from './orderCap.js';
|
|
13
|
+
export * from './postFeeAmount.js';
|
|
14
|
+
export * from './providerSelectionMode.js';
|
|
15
|
+
export * from './settlementReconciliation.js';
|
|
16
|
+
export * from './submitStepDerivations.js';
|
|
17
|
+
export * from './verifiableBanks.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The buyer's per-order cap, in settlement-token units.
|
|
3
|
+
*
|
|
4
|
+
* The server enforces the cap on the TOKEN amount of the order
|
|
5
|
+
* (`evaluateReputation` compares the intent amount, in USDC base units, with
|
|
6
|
+
* `getEffectiveCap`). So the cap is a USDC amount, and the UI compares the
|
|
7
|
+
* USDC the buyer would receive, never the typed fiat figure. What a cap lets a
|
|
8
|
+
* buyer type depends on the seller's rate: 100 USDC is 140,000 won at 1,400 won
|
|
9
|
+
* per USDC. See `maximumCurrencyAmountForOrderTokenAmount` in
|
|
10
|
+
* `@provex/utils/conversionRates` for that conversion.
|
|
11
|
+
*
|
|
12
|
+
* Null (platform locked for the tier, limits off) and the unlimited sentinel
|
|
13
|
+
* both mean "no cap", matching the server's `effectiveCap !== null` guard and
|
|
14
|
+
* the unreachable unlimited value.
|
|
15
|
+
*/
|
|
16
|
+
import type { ProviderKey, TakerTier } from '../payment.js';
|
|
17
|
+
/** Settlement token decimals the cap is scaled by (USDC). */
|
|
18
|
+
export declare const CAP_TOKEN_DECIMALS = 6;
|
|
19
|
+
/**
|
|
20
|
+
* The per-order cap for one provider, in settlement-token base units.
|
|
21
|
+
*
|
|
22
|
+
* @param params.tier - The buyer's reputation tier; null means limits are off
|
|
23
|
+
* @param params.providerKey - Provider the buyer pays with (caps differ per provider)
|
|
24
|
+
* @returns The cap in token base units, or null when there is no cap
|
|
25
|
+
*/
|
|
26
|
+
export declare function orderCapInTokenUnits({ tier, providerKey, }: {
|
|
27
|
+
tier: TakerTier | null;
|
|
28
|
+
providerKey: ProviderKey;
|
|
29
|
+
}): bigint | null;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The buyer's per-order cap, in settlement-token units.
|
|
3
|
+
*
|
|
4
|
+
* The server enforces the cap on the TOKEN amount of the order
|
|
5
|
+
* (`evaluateReputation` compares the intent amount, in USDC base units, with
|
|
6
|
+
* `getEffectiveCap`). So the cap is a USDC amount, and the UI compares the
|
|
7
|
+
* USDC the buyer would receive, never the typed fiat figure. What a cap lets a
|
|
8
|
+
* buyer type depends on the seller's rate: 100 USDC is 140,000 won at 1,400 won
|
|
9
|
+
* per USDC. See `maximumCurrencyAmountForOrderTokenAmount` in
|
|
10
|
+
* `@provex/utils/conversionRates` for that conversion.
|
|
11
|
+
*
|
|
12
|
+
* Null (platform locked for the tier, limits off) and the unlimited sentinel
|
|
13
|
+
* both mean "no cap", matching the server's `effectiveCap !== null` guard and
|
|
14
|
+
* the unreachable unlimited value.
|
|
15
|
+
*/
|
|
16
|
+
import { getEffectiveCap, UNLIMITED_CAP_BASE_UNITS } from '../reputation.js';
|
|
17
|
+
/** Settlement token decimals the cap is scaled by (USDC). */
|
|
18
|
+
export const CAP_TOKEN_DECIMALS = 6;
|
|
19
|
+
/**
|
|
20
|
+
* The per-order cap for one provider, in settlement-token base units.
|
|
21
|
+
*
|
|
22
|
+
* @param params.tier - The buyer's reputation tier; null means limits are off
|
|
23
|
+
* @param params.providerKey - Provider the buyer pays with (caps differ per provider)
|
|
24
|
+
* @returns The cap in token base units, or null when there is no cap
|
|
25
|
+
*/
|
|
26
|
+
export function orderCapInTokenUnits({ tier, providerKey, }) {
|
|
27
|
+
if (tier === null)
|
|
28
|
+
return null;
|
|
29
|
+
const cap = getEffectiveCap({ tier, providerKey, decimals: CAP_TOKEN_DECIMALS });
|
|
30
|
+
if (cap === null)
|
|
31
|
+
return null;
|
|
32
|
+
if (cap === UNLIMITED_CAP_BASE_UNITS)
|
|
33
|
+
return null;
|
|
34
|
+
return cap;
|
|
35
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for the buyer's per-order cap.
|
|
3
|
+
*
|
|
4
|
+
* The server checks the cap against the token amount of the order. The panel
|
|
5
|
+
* must read "no cap" in exactly the cases the server does, or it blocks a
|
|
6
|
+
* buyer the server would accept, or lets one through that it would refuse.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for the buyer's per-order cap.
|
|
3
|
+
*
|
|
4
|
+
* The server checks the cap against the token amount of the order. The panel
|
|
5
|
+
* must read "no cap" in exactly the cases the server does, or it blocks a
|
|
6
|
+
* buyer the server would accept, or lets one through that it would refuse.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it } from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { providers } from '../payment.js';
|
|
11
|
+
import { CAP_TOKEN_DECIMALS, orderCapInTokenUnits } from './orderCap.js';
|
|
12
|
+
describe('orderCapInTokenUnits', () => {
|
|
13
|
+
it('scales the cap by the settlement token decimals', () => {
|
|
14
|
+
// Neutral tier ($250 base) on Zelle (1.5x) is $375, in six-decimal base units.
|
|
15
|
+
assert.equal(CAP_TOKEN_DECIMALS, 6);
|
|
16
|
+
assert.equal(orderCapInTokenUnits({ tier: 'neutral', providerKey: providers.ZELLE }), 375000000n);
|
|
17
|
+
});
|
|
18
|
+
it('differs by provider for the same tier', () => {
|
|
19
|
+
assert.equal(orderCapInTokenUnits({ tier: 'peasant', providerKey: providers.REVOLUT }), 500000000n);
|
|
20
|
+
assert.equal(orderCapInTokenUnits({ tier: 'plus', providerKey: providers.VENMO }), 1000000000n);
|
|
21
|
+
});
|
|
22
|
+
it('means no cap when limits are off (tier is null)', () => {
|
|
23
|
+
assert.equal(orderCapInTokenUnits({ tier: null, providerKey: providers.VENMO }), null);
|
|
24
|
+
});
|
|
25
|
+
it('means no cap for the top tier, whose cap is the unlimited sentinel', () => {
|
|
26
|
+
// The sentinel is unreachable as an amount. Returning it would make every
|
|
27
|
+
// comparison pass by accident; null states the intent.
|
|
28
|
+
assert.equal(orderCapInTokenUnits({ tier: 'president', providerKey: providers.VENMO }), null);
|
|
29
|
+
});
|
|
30
|
+
it('means no cap when the platform is locked for the tier', () => {
|
|
31
|
+
// Matches the server guard `effectiveCap !== null`: a locked platform is
|
|
32
|
+
// refused elsewhere, not by an amount comparison.
|
|
33
|
+
assert.equal(orderCapInTokenUnits({ tier: 'peasant', providerKey: providers.PAYPAL }), null);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Token amounts after the protocol fee.
|
|
3
|
+
*
|
|
4
|
+
* The buy flow shows the buyer what they receive after the fee. Several
|
|
5
|
+
* panels computed that with the same few lines. This module is the one place
|
|
6
|
+
* for the rule. Amounts stay bigint in base units.
|
|
7
|
+
*/
|
|
8
|
+
import type { Currency } from '../currencies.js';
|
|
9
|
+
import type { TokenInfo } from '../tokens.js';
|
|
10
|
+
/**
|
|
11
|
+
* Tokens left after the protocol fee comes off a gross amount.
|
|
12
|
+
*
|
|
13
|
+
* A missing, zero or negative fee leaves the gross amount unchanged.
|
|
14
|
+
*
|
|
15
|
+
* @param params.grossAmount - Amount before the fee, in token base units
|
|
16
|
+
* @param params.feeBps - Protocol fee in basis points
|
|
17
|
+
*/
|
|
18
|
+
export declare function netAmountAfterFee({ grossAmount, feeBps, }: {
|
|
19
|
+
grossAmount: bigint;
|
|
20
|
+
feeBps: bigint | null | undefined;
|
|
21
|
+
}): bigint;
|
|
22
|
+
/** What the amount a buyer receives depends on. */
|
|
23
|
+
export interface ReceivedAmountContext {
|
|
24
|
+
token: TokenInfo;
|
|
25
|
+
currency: Currency;
|
|
26
|
+
/** Buyer amount in the currency's base units; null or 0n means none typed. */
|
|
27
|
+
amountInCurrency: bigint | null;
|
|
28
|
+
/** Protocol fee in basis points; null while unknown. */
|
|
29
|
+
feeBps: bigint | null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The tokens a buyer receives for one rate, after the protocol fee.
|
|
33
|
+
* Uses the same rule as the "You Receive" card, so a row and the card agree.
|
|
34
|
+
*
|
|
35
|
+
* @returns Net token amount, `0n` with no amount typed, or null while the fee is unknown
|
|
36
|
+
*/
|
|
37
|
+
export declare function computeReceivedAmount({ rate, token, currency, amountInCurrency, feeBps, }: ReceivedAmountContext & {
|
|
38
|
+
rate: bigint;
|
|
39
|
+
}): bigint | null;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Token amounts after the protocol fee.
|
|
3
|
+
*
|
|
4
|
+
* The buy flow shows the buyer what they receive after the fee. Several
|
|
5
|
+
* panels computed that with the same few lines. This module is the one place
|
|
6
|
+
* for the rule. Amounts stay bigint in base units.
|
|
7
|
+
*/
|
|
8
|
+
import { convertCurrencyToOrderTokenAmount } from '../conversionRates.js';
|
|
9
|
+
import { calculateNetAmount } from '../fees.js';
|
|
10
|
+
/**
|
|
11
|
+
* Tokens left after the protocol fee comes off a gross amount.
|
|
12
|
+
*
|
|
13
|
+
* A missing, zero or negative fee leaves the gross amount unchanged.
|
|
14
|
+
*
|
|
15
|
+
* @param params.grossAmount - Amount before the fee, in token base units
|
|
16
|
+
* @param params.feeBps - Protocol fee in basis points
|
|
17
|
+
*/
|
|
18
|
+
export function netAmountAfterFee({ grossAmount, feeBps, }) {
|
|
19
|
+
if (!feeBps || feeBps <= 0n)
|
|
20
|
+
return grossAmount;
|
|
21
|
+
return calculateNetAmount({ grossAmount, feeBps });
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The tokens a buyer receives for one rate, after the protocol fee.
|
|
25
|
+
* Uses the same rule as the "You Receive" card, so a row and the card agree.
|
|
26
|
+
*
|
|
27
|
+
* @returns Net token amount, `0n` with no amount typed, or null while the fee is unknown
|
|
28
|
+
*/
|
|
29
|
+
export function computeReceivedAmount({ rate, token, currency, amountInCurrency, feeBps, }) {
|
|
30
|
+
if (feeBps === null)
|
|
31
|
+
return null;
|
|
32
|
+
if (!amountInCurrency)
|
|
33
|
+
return 0n;
|
|
34
|
+
const grossAmount = convertCurrencyToOrderTokenAmount({ token, currency, currencyAmountInt: amountInCurrency, rate });
|
|
35
|
+
return netAmountAfterFee({ grossAmount, feeBps });
|
|
36
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for token amounts after the protocol fee.
|
|
3
|
+
*
|
|
4
|
+
* The expected values were captured from the inline code in `index.tsx`,
|
|
5
|
+
* `BrowsePanel.tsx` and `providerQuotes.ts` before it moved here. Panels show
|
|
6
|
+
* these amounts as "You receive", so they must agree with each other and with
|
|
7
|
+
* the fee the contract takes.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for token amounts after the protocol fee.
|
|
3
|
+
*
|
|
4
|
+
* The expected values were captured from the inline code in `index.tsx`,
|
|
5
|
+
* `BrowsePanel.tsx` and `providerQuotes.ts` before it moved here. Panels show
|
|
6
|
+
* these amounts as "You receive", so they must agree with each other and with
|
|
7
|
+
* the fee the contract takes.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it } from 'node:test';
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { SUPPORTED_CURRENCIES } from '../currencies.js';
|
|
12
|
+
import { getDefaultToken } from '../tokens.js';
|
|
13
|
+
import { computeReceivedAmount, netAmountAfterFee } from './postFeeAmount.js';
|
|
14
|
+
const token = getDefaultToken(369);
|
|
15
|
+
describe('netAmountAfterFee', () => {
|
|
16
|
+
const rows = [
|
|
17
|
+
[0n, null, 0n],
|
|
18
|
+
[0n, 200n, 0n],
|
|
19
|
+
[1n, null, 1n],
|
|
20
|
+
[1n, 0n, 1n],
|
|
21
|
+
// A fee that rounds down to nothing on one base unit leaves the unit whole.
|
|
22
|
+
[1n, 1n, 1n],
|
|
23
|
+
[1n, 200n, 1n],
|
|
24
|
+
[1n, 10000n, 0n],
|
|
25
|
+
[99999999n, null, 99999999n],
|
|
26
|
+
[99999999n, 0n, 99999999n],
|
|
27
|
+
[99999999n, 1n, 99990000n],
|
|
28
|
+
[99999999n, 30n, 99700000n],
|
|
29
|
+
[99999999n, 200n, 98000000n],
|
|
30
|
+
[100000000n, undefined, 100000000n],
|
|
31
|
+
[100000000n, 1n, 99990000n],
|
|
32
|
+
[100000000n, 30n, 99700000n],
|
|
33
|
+
[100000000n, 200n, 98000000n],
|
|
34
|
+
[100000000n, 10000n, 0n],
|
|
35
|
+
[123456789n, 1n, 123444444n],
|
|
36
|
+
[123456789n, 30n, 123086419n],
|
|
37
|
+
[123456789n, 200n, 120987654n],
|
|
38
|
+
];
|
|
39
|
+
for (const [grossAmount, feeBps, expected] of rows) {
|
|
40
|
+
it(`takes ${String(feeBps)} basis points off ${grossAmount}`, () => {
|
|
41
|
+
assert.equal(netAmountAfterFee({ grossAmount, feeBps }), expected);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
it('treats a negative fee as no fee, so a bad read never pays the buyer more than the lock', () => {
|
|
45
|
+
assert.equal(netAmountAfterFee({ grossAmount: 100000000n, feeBps: -5n }), 100000000n);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('computeReceivedAmount', () => {
|
|
49
|
+
const usd = SUPPORTED_CURRENCIES.USD;
|
|
50
|
+
const eur = SUPPORTED_CURRENCIES.EUR;
|
|
51
|
+
const krw = SUPPORTED_CURRENCIES.KRW;
|
|
52
|
+
const rows = [
|
|
53
|
+
{ name: 'ten dollars at 1.02, fee unknown', currency: usd, amountInCurrency: 1000n, rate: 1020000000000000000n, feeBps: null, expected: null },
|
|
54
|
+
{ name: 'ten dollars at 1.02, no fee', currency: usd, amountInCurrency: 1000n, rate: 1020000000000000000n, feeBps: 0n, expected: 9803921n },
|
|
55
|
+
{ name: 'ten dollars at 1.02, two percent fee', currency: usd, amountInCurrency: 1000n, rate: 1020000000000000000n, feeBps: 200n, expected: 9607843n },
|
|
56
|
+
{ name: 'nine euros twenty at 0.92, fee unknown', currency: eur, amountInCurrency: 920n, rate: 920000000000000000n, feeBps: null, expected: null },
|
|
57
|
+
{ name: 'nine euros twenty at 0.92, no fee', currency: eur, amountInCurrency: 920n, rate: 920000000000000000n, feeBps: 0n, expected: 10000000n },
|
|
58
|
+
{ name: 'nine euros twenty at 0.92, two percent fee', currency: eur, amountInCurrency: 920n, rate: 920000000000000000n, feeBps: 200n, expected: 9800000n },
|
|
59
|
+
{ name: '14,000 won at 1,400 per token, no fee (no cent grid)', currency: krw, amountInCurrency: 14000n, rate: 1400n * 10n ** 18n, feeBps: 0n, expected: 10000000n },
|
|
60
|
+
{ name: '14,000 won at 1,400 per token, two percent fee', currency: krw, amountInCurrency: 14000n, rate: 1400n * 10n ** 18n, feeBps: 200n, expected: 9800000n },
|
|
61
|
+
{ name: 'a zero rate yields nothing, not a crash', currency: usd, amountInCurrency: 1000n, rate: 0n, feeBps: 200n, expected: 0n },
|
|
62
|
+
];
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
it(row.name, () => {
|
|
65
|
+
const received = computeReceivedAmount({
|
|
66
|
+
token,
|
|
67
|
+
currency: row.currency,
|
|
68
|
+
amountInCurrency: row.amountInCurrency,
|
|
69
|
+
rate: row.rate,
|
|
70
|
+
feeBps: row.feeBps,
|
|
71
|
+
});
|
|
72
|
+
assert.equal(received, row.expected);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
it('returns zero when no amount is typed', () => {
|
|
76
|
+
assert.equal(computeReceivedAmount({ token, currency: usd, amountInCurrency: null, rate: 10n ** 18n, feeBps: 200n }), 0n);
|
|
77
|
+
assert.equal(computeReceivedAmount({ token, currency: usd, amountInCurrency: 0n, rate: 10n ** 18n, feeBps: 200n }), 0n);
|
|
78
|
+
});
|
|
79
|
+
it('returns null while the fee is unknown, even with no amount, so nothing ranks on a guess', () => {
|
|
80
|
+
assert.equal(computeReceivedAmount({ token, currency: usd, amountInCurrency: null, rate: 10n ** 18n, feeBps: null }), null);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview What the buyer got when an order settled for less than its ceiling.
|
|
3
|
+
*
|
|
4
|
+
* The gate signs an attestation that carries the gross token amount the buyer
|
|
5
|
+
* is owed. When that amount is below the amount locked for the order, the
|
|
6
|
+
* buyer paid less than the contracted maximum. The contract then releases the
|
|
7
|
+
* proportional amount and returns the unused lock to the maker. This module
|
|
8
|
+
* turns that fact into the figures the success panel shows.
|
|
9
|
+
*
|
|
10
|
+
* Amounts stay bigint in base units until the last step. Only the returned
|
|
11
|
+
* display strings are formatted.
|
|
12
|
+
*/
|
|
13
|
+
import type { ChainId } from '../chain.js';
|
|
14
|
+
import type { IntentStruct } from '../intent.js';
|
|
15
|
+
import type { V3AttestationResponseObject } from '../peer-types.js';
|
|
16
|
+
/** The figures of a partial settlement, ready to show. */
|
|
17
|
+
export interface SettlementReconciliation {
|
|
18
|
+
/** Symbol of the fiat currency the order was priced in. */
|
|
19
|
+
currencySymbol: string;
|
|
20
|
+
/** Fiat value of the full locked amount: the most the buyer could have claimed. */
|
|
21
|
+
contractedFiat: string;
|
|
22
|
+
/** Fiat value of the amount the attestation released. */
|
|
23
|
+
attestedFiat: string;
|
|
24
|
+
/** Tokens the buyer receives after the protocol fee. */
|
|
25
|
+
netToBuyer: string;
|
|
26
|
+
/** Protocol fee in tokens, or null when no fee applies so the caller can skip the row. */
|
|
27
|
+
protocolFee: string | null;
|
|
28
|
+
/** Locked tokens returned to the maker. */
|
|
29
|
+
returnedToMaker: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Work out the partial-settlement figures, or null when there is nothing to reconcile.
|
|
33
|
+
*
|
|
34
|
+
* Returns null when the attestation or the intent is missing, when the chain
|
|
35
|
+
* has no default token, when the currency is unknown, and when the released
|
|
36
|
+
* amount reaches the locked amount (a full settlement).
|
|
37
|
+
*
|
|
38
|
+
* @param params.attestation - The verified gate attestation; only `releaseAmount` is read
|
|
39
|
+
* @param params.intent - The intent that was fulfilled
|
|
40
|
+
* @param params.chainId - Chain the order settled on; picks the settlement token
|
|
41
|
+
* @param params.feeBps - Protocol fee in basis points; null or zero means no fee
|
|
42
|
+
*/
|
|
43
|
+
export declare function reconcileSettlement({ attestation, intent, chainId, feeBps, }: {
|
|
44
|
+
attestation: V3AttestationResponseObject | null | undefined;
|
|
45
|
+
intent: Pick<IntentStruct, 'amount' | 'conversionRate' | 'fiatCurrency'> | null | undefined;
|
|
46
|
+
chainId: ChainId;
|
|
47
|
+
feeBps: bigint | null | undefined;
|
|
48
|
+
}): SettlementReconciliation | null;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview What the buyer got when an order settled for less than its ceiling.
|
|
3
|
+
*
|
|
4
|
+
* The gate signs an attestation that carries the gross token amount the buyer
|
|
5
|
+
* is owed. When that amount is below the amount locked for the order, the
|
|
6
|
+
* buyer paid less than the contracted maximum. The contract then releases the
|
|
7
|
+
* proportional amount and returns the unused lock to the maker. This module
|
|
8
|
+
* turns that fact into the figures the success panel shows.
|
|
9
|
+
*
|
|
10
|
+
* Amounts stay bigint in base units until the last step. Only the returned
|
|
11
|
+
* display strings are formatted.
|
|
12
|
+
*/
|
|
13
|
+
import { convertTokenInputToCurrency } from '../conversionRates.js';
|
|
14
|
+
import { getCurrencyByContractId } from '../currencies.js';
|
|
15
|
+
import { calculateFeeAmount } from '../fees.js';
|
|
16
|
+
import { getDefaultToken } from '../tokens.js';
|
|
17
|
+
import { formatUnits } from '../units.js';
|
|
18
|
+
import { netAmountAfterFee } from './postFeeAmount.js';
|
|
19
|
+
/**
|
|
20
|
+
* Work out the partial-settlement figures, or null when there is nothing to reconcile.
|
|
21
|
+
*
|
|
22
|
+
* Returns null when the attestation or the intent is missing, when the chain
|
|
23
|
+
* has no default token, when the currency is unknown, and when the released
|
|
24
|
+
* amount reaches the locked amount (a full settlement).
|
|
25
|
+
*
|
|
26
|
+
* @param params.attestation - The verified gate attestation; only `releaseAmount` is read
|
|
27
|
+
* @param params.intent - The intent that was fulfilled
|
|
28
|
+
* @param params.chainId - Chain the order settled on; picks the settlement token
|
|
29
|
+
* @param params.feeBps - Protocol fee in basis points; null or zero means no fee
|
|
30
|
+
*/
|
|
31
|
+
export function reconcileSettlement({ attestation, intent, chainId, feeBps, }) {
|
|
32
|
+
if (!attestation || !intent?.amount || !intent?.conversionRate)
|
|
33
|
+
return null;
|
|
34
|
+
const token = getDefaultToken(chainId);
|
|
35
|
+
if (!token)
|
|
36
|
+
return null;
|
|
37
|
+
const currency = getCurrencyByContractId(intent.fiatCurrency);
|
|
38
|
+
if (!currency)
|
|
39
|
+
return null;
|
|
40
|
+
const grossReleased = BigInt(attestation.typedDataValue.releaseAmount);
|
|
41
|
+
if (grossReleased >= intent.amount)
|
|
42
|
+
return null;
|
|
43
|
+
const returnedToMaker = intent.amount - grossReleased;
|
|
44
|
+
const fee = feeBps && feeBps > 0n ? calculateFeeAmount({ amount: grossReleased, feeBps }) : 0n;
|
|
45
|
+
const netToBuyer = netAmountAfterFee({ grossAmount: grossReleased, feeBps });
|
|
46
|
+
const contractedFiatRaw = convertTokenInputToCurrency({
|
|
47
|
+
token,
|
|
48
|
+
amountOutInt: intent.amount,
|
|
49
|
+
rate: intent.conversionRate,
|
|
50
|
+
});
|
|
51
|
+
const attestedFiatRaw = convertTokenInputToCurrency({
|
|
52
|
+
token,
|
|
53
|
+
amountOutInt: grossReleased,
|
|
54
|
+
rate: intent.conversionRate,
|
|
55
|
+
});
|
|
56
|
+
const format = (raw) => formatUnits(raw, { decimals: token.decimals, truncateDecimals: 2, padDecimals: 2 });
|
|
57
|
+
return {
|
|
58
|
+
currencySymbol: currency.symbol,
|
|
59
|
+
contractedFiat: format(contractedFiatRaw),
|
|
60
|
+
attestedFiat: format(attestedFiatRaw),
|
|
61
|
+
netToBuyer: format(netToBuyer),
|
|
62
|
+
protocolFee: fee > 0n ? format(fee) : null,
|
|
63
|
+
returnedToMaker: format(returnedToMaker),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for the partial-settlement figures on the success panel.
|
|
3
|
+
*
|
|
4
|
+
* The expected strings were captured from the inline code in `CompletePanel`
|
|
5
|
+
* before it moved here, so each row pins what buyers already saw. A buyer reads
|
|
6
|
+
* these numbers to learn what they received and what went back to the maker,
|
|
7
|
+
* so a rounding change here changes a figure that touches money.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|