@zkp2p/pay-shared 5.0.0 → 7.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/README.md CHANGED
@@ -9,7 +9,7 @@ Shared TypeScript types, enums, and chain utilities used by ZKP2P Pay API, SDK,
9
9
  ## Install
10
10
 
11
11
  ```bash
12
- npm install @zkp2p/pay-shared@5.0.0
12
+ npm install @zkp2p/pay-shared@7.0.0
13
13
  ```
14
14
 
15
15
  ## Published artifacts
@@ -85,6 +85,10 @@ Core type exports:
85
85
  - `CheckoutAggregate`
86
86
  - `CheckoutQuotes`
87
87
  - `MerchantProfile`
88
+ - `PayoutView`
89
+ - `PayoutRailType`
90
+ - `WebhookPayload`
91
+ - `PayoutWebhookPayload`
88
92
 
89
93
  Core constants:
90
94
 
@@ -96,6 +100,15 @@ Core constants:
96
100
  - `ProofAttemptStatus`
97
101
  - `OrderErrorCode`
98
102
  - `MerchantEnvironment`
103
+ - `PayoutStatus`
104
+ - `PayoutRail`
105
+ - `PayoutCryptoRail`
106
+ - `PayoutProvider`
107
+ - `PAY_CRYPTO_TOKEN_SYMBOLS`
108
+
109
+ Webhook helpers:
110
+
111
+ - `isPayoutWebhook(...)` — narrows `WebhookPayload` to `PayoutWebhookPayload`
99
112
 
100
113
  Chain constants and helpers:
101
114
 
@@ -1,4 +1,5 @@
1
1
  export declare enum IntentAttributionReferrer {
2
2
  PAY = "zkp2p-pay",
3
- SUPPORT = "zkp2p-pay-cs"
3
+ SUPPORT = "zkp2p-pay-cs",
4
+ CASHOUT = "zkp2p-pay-cashout"
4
5
  }
@@ -2,4 +2,5 @@ export var IntentAttributionReferrer;
2
2
  (function (IntentAttributionReferrer) {
3
3
  IntentAttributionReferrer["PAY"] = "zkp2p-pay";
4
4
  IntentAttributionReferrer["SUPPORT"] = "zkp2p-pay-cs";
5
+ IntentAttributionReferrer["CASHOUT"] = "zkp2p-pay-cashout";
5
6
  })(IntentAttributionReferrer || (IntentAttributionReferrer = {}));
package/dist/bitcoin.d.ts CHANGED
@@ -1 +1,5 @@
1
1
  export declare function isValidBitcoinAddress(value: string): boolean;
2
+ /** A Bitcoin txid as Relay writes it: 64 lower-case hex digits, no 0x prefix. */
3
+ export declare function isBitcoinTxid(value: string): boolean;
4
+ /** A mainnet P2WPKH address (bech32, witness version 0, lower-case) for a 20-byte witness program. */
5
+ export declare function encodeBitcoinP2wpkhAddress(program: Uint8Array): string;
package/dist/bitcoin.js CHANGED
@@ -2,6 +2,13 @@ import { hasBase58CheckChecksum } from './addressEncoding.js';
2
2
  const BECH32_ALPHABET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
3
3
  const BECH32_CHECKSUM = 1;
4
4
  const BECH32M_CHECKSUM = 0x2bc830a3;
5
+ // The bech32 HRP "bc" expanded for the checksum: high bits, a zero, low bits.
6
+ const BITCOIN_HRP_VALUES = [
7
+ ...[...'bc'].map((character) => character.charCodeAt(0) >>> 5),
8
+ 0,
9
+ ...[...'bc'].map((character) => character.charCodeAt(0) & 31),
10
+ ];
11
+ const BITCOIN_TXID_PATTERN = /^[0-9a-f]{64}$/;
5
12
  function calculatePolymod(values) {
6
13
  const generators = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
7
14
  let checksum = 1;
@@ -33,6 +40,23 @@ function convertWitnessProgram(values) {
33
40
  }
34
41
  return Uint8Array.from(decoded);
35
42
  }
43
+ /** 8-bit bytes to 5-bit bech32 words, padding the last word with zero bits. */
44
+ function toBech32Words(bytes) {
45
+ const words = [];
46
+ let accumulator = 0;
47
+ let bits = 0;
48
+ for (const byte of bytes) {
49
+ accumulator = ((accumulator << 8) | byte) & 0xffff;
50
+ bits += 8;
51
+ while (bits >= 5) {
52
+ bits -= 5;
53
+ words.push((accumulator >>> bits) & 31);
54
+ }
55
+ }
56
+ if (bits > 0)
57
+ words.push((accumulator << (5 - bits)) & 31);
58
+ return words;
59
+ }
36
60
  function isValidBech32Address(value) {
37
61
  if (value.length > 90 || (value !== value.toLowerCase() && value !== value.toUpperCase())) {
38
62
  return false;
@@ -47,13 +71,8 @@ function isValidBech32Address(value) {
47
71
  if (data.length < 7 || data.some((item) => item === -1)) {
48
72
  return false;
49
73
  }
50
- const hrpValues = [
51
- ...[...'bc'].map((character) => character.charCodeAt(0) >>> 5),
52
- 0,
53
- ...[...'bc'].map((character) => character.charCodeAt(0) & 31),
54
- ];
55
74
  const witnessVersion = data[0];
56
- const polymod = calculatePolymod([...hrpValues, ...data]);
75
+ const polymod = calculatePolymod([...BITCOIN_HRP_VALUES, ...data]);
57
76
  if (witnessVersion > 16
58
77
  || (witnessVersion === 0 && polymod !== BECH32_CHECKSUM)
59
78
  || (witnessVersion > 0 && polymod !== BECH32M_CHECKSUM)) {
@@ -77,3 +96,16 @@ export function isValidBitcoinAddress(value) {
77
96
  && (hasBase58CheckChecksum(trimmed, 0x00)
78
97
  || hasBase58CheckChecksum(trimmed, 0x05));
79
98
  }
99
+ /** A Bitcoin txid as Relay writes it: 64 lower-case hex digits, no 0x prefix. */
100
+ export function isBitcoinTxid(value) {
101
+ return BITCOIN_TXID_PATTERN.test(value);
102
+ }
103
+ /** A mainnet P2WPKH address (bech32, witness version 0, lower-case) for a 20-byte witness program. */
104
+ export function encodeBitcoinP2wpkhAddress(program) {
105
+ if (program.length !== 20)
106
+ throw new Error('A P2WPKH witness program is 20 bytes');
107
+ const data = [0, ...toBech32Words(program)];
108
+ const polymod = calculatePolymod([...BITCOIN_HRP_VALUES, ...data, 0, 0, 0, 0, 0, 0]) ^ BECH32_CHECKSUM;
109
+ const checksum = [0, 1, 2, 3, 4, 5].map((index) => (polymod >>> (5 * (5 - index))) & 31);
110
+ return `bc1${[...data, ...checksum].map((value) => BECH32_ALPHABET[value]).join('')}`;
111
+ }
@@ -0,0 +1,109 @@
1
+ import { type PayoutFiatRailType } from './types.js';
2
+ /** Currency metadata in selector order. Names follow the SDK's currencyInfo. */
3
+ export declare const PAYOUT_CURRENCY_INFO: {
4
+ readonly USD: {
5
+ readonly name: "United States Dollar";
6
+ readonly prefix: "$";
7
+ readonly minorUnits: 2;
8
+ };
9
+ readonly EUR: {
10
+ readonly name: "Euro";
11
+ readonly prefix: "€";
12
+ readonly minorUnits: 2;
13
+ };
14
+ readonly GBP: {
15
+ readonly name: "British Pound";
16
+ readonly prefix: "£";
17
+ readonly minorUnits: 2;
18
+ };
19
+ readonly AUD: {
20
+ readonly name: "Australian Dollar";
21
+ readonly prefix: "A$";
22
+ readonly minorUnits: 2;
23
+ };
24
+ readonly CAD: {
25
+ readonly name: "Canadian Dollar";
26
+ readonly prefix: "C$";
27
+ readonly minorUnits: 2;
28
+ };
29
+ readonly CHF: {
30
+ readonly name: "Swiss Franc";
31
+ readonly prefix: "CHF ";
32
+ readonly minorUnits: 2;
33
+ };
34
+ readonly CNY: {
35
+ readonly name: "Chinese Yuan";
36
+ readonly prefix: "CN¥";
37
+ readonly minorUnits: 2;
38
+ };
39
+ readonly MXN: {
40
+ readonly name: "Mexican Peso";
41
+ readonly prefix: "MX$";
42
+ readonly minorUnits: 2;
43
+ };
44
+ readonly NZD: {
45
+ readonly name: "New Zealand Dollar";
46
+ readonly prefix: "NZ$";
47
+ readonly minorUnits: 2;
48
+ };
49
+ readonly SGD: {
50
+ readonly name: "Singapore Dollar";
51
+ readonly prefix: "S$";
52
+ readonly minorUnits: 2;
53
+ };
54
+ readonly TRY: {
55
+ readonly name: "Turkish Lira";
56
+ readonly prefix: "₺";
57
+ readonly minorUnits: 2;
58
+ };
59
+ readonly ZAR: {
60
+ readonly name: "South African Rand";
61
+ readonly prefix: "R";
62
+ readonly minorUnits: 2;
63
+ };
64
+ };
65
+ export type PayoutCurrencyType = keyof typeof PAYOUT_CURRENCY_INFO;
66
+ /** Every currency but USD: priced by the protocol oracle at zero spread, bound when a buyer signals. */
67
+ export type PayoutOracleCurrencyType = Exclude<PayoutCurrencyType, 'USD'>;
68
+ export declare const PayoutCurrency: { readonly [K in PayoutCurrencyType]: K; };
69
+ /** Oracle currencies in catalog order; payoutCurrencies follows it. */
70
+ export declare const PAYOUT_ORACLE_CURRENCIES: PayoutOracleCurrencyType[];
71
+ /** Each rail's supported currencies, USD first then selector order. The API validates every pair against the SDK at boot. */
72
+ export declare const PAYOUT_RAIL_CURRENCIES: {
73
+ readonly venmo: readonly ["USD"];
74
+ readonly cashapp: readonly ["USD"];
75
+ readonly paypal: readonly ["USD", "EUR", "GBP", "AUD", "CAD", "NZD", "SGD"];
76
+ readonly zelle: readonly ["USD"];
77
+ readonly revolut: readonly ["USD", "EUR", "GBP", "AUD", "CAD", "CHF", "CNY", "MXN", "NZD", "SGD", "TRY", "ZAR"];
78
+ readonly chime: readonly ["USD"];
79
+ };
80
+ export declare const PAYOUT_CURRENCY_NAMES: Readonly<Record<"USD" | "EUR" | "GBP" | "AUD" | "CAD" | "CHF" | "CNY" | "MXN" | "NZD" | "SGD" | "TRY" | "ZAR", string>>;
81
+ export declare const PAYOUT_CURRENCY_SYMBOLS: Readonly<Record<"USD" | "EUR" | "GBP" | "AUD" | "CAD" | "CHF" | "CNY" | "MXN" | "NZD" | "SGD" | "TRY" | "ZAR", string>>;
82
+ /** A USD listing's rate: exactly 1.00 USD per USDC in 18-decimal units; USD never uses an oracle. */
83
+ export declare const CASHOUT_FIXED_USD_RATE: bigint;
84
+ /** An oracle listing's fixed floor; below any market rate, so the escrow's effective rate is the oracle's (cash's ORACLE_MIN_CONVERSION_RATE_SENTINEL). */
85
+ export declare const CASHOUT_ORACLE_MIN_RATE_SENTINEL = 1n;
86
+ /** A fiat amount: `amount` is a decimal string with exactly 2 decimals. */
87
+ export interface PayoutFiatValue {
88
+ currency: PayoutCurrencyType;
89
+ amount: string;
90
+ }
91
+ export declare function isPayoutCurrency(value: string): value is PayoutCurrencyType;
92
+ export declare function isCashoutOracleCurrency(value: string): value is PayoutOracleCurrencyType;
93
+ export declare function payoutRailPaysCurrency(rail: PayoutFiatRailType, currency: PayoutCurrencyType): boolean;
94
+ /** A rail whose payout-method request carries `currency`: its catalog has more than one. */
95
+ export declare function isPayoutMultiCurrencyRail(rail: PayoutFiatRailType): boolean;
96
+ /**
97
+ * Fiat cents for `units` USDC base units at `rate` (fiat per USDC, 18 decimals), rounded up: Curator's buyer quote rule.
98
+ * While a USDC unit is worth less than a cent (rate below 10,000), it recovers exactly the cents a buyer paid from the
99
+ * attestation's floored release amount.
100
+ */
101
+ export declare function payoutFiatCents(units: bigint, rate: bigint): bigint;
102
+ export declare function formatPayoutFiatCents(cents: bigint): string;
103
+ /** Fiat per USDC with 6 decimals, rounded half up: "0.892698". Display only. */
104
+ export declare function formatPayoutRate(rate: bigint): string;
105
+ /** A stored 18-decimal rate; null unless it is a positive integer string without leading zeros. */
106
+ export declare function parseCashoutRate(value: string | null): bigint | null;
107
+ export declare function cashoutFiatValue(currency: PayoutCurrencyType, units: bigint, rate: bigint): PayoutFiatValue;
108
+ /** Group a non-negative fiat amount with exactly two decimals; reject malformed amounts and preserve cents beyond Number's safe range. */
109
+ export declare function formatPayoutFiatValue(value: PayoutFiatValue): string;
@@ -0,0 +1,94 @@
1
+ import { PayoutFiatRail } from './types.js';
2
+ /** Currency metadata in selector order. Names follow the SDK's currencyInfo. */
3
+ export const PAYOUT_CURRENCY_INFO = {
4
+ USD: { name: 'United States Dollar', prefix: '$', minorUnits: 2 },
5
+ EUR: { name: 'Euro', prefix: '€', minorUnits: 2 },
6
+ GBP: { name: 'British Pound', prefix: '£', minorUnits: 2 },
7
+ AUD: { name: 'Australian Dollar', prefix: 'A$', minorUnits: 2 },
8
+ CAD: { name: 'Canadian Dollar', prefix: 'C$', minorUnits: 2 },
9
+ CHF: { name: 'Swiss Franc', prefix: 'CHF ', minorUnits: 2 },
10
+ CNY: { name: 'Chinese Yuan', prefix: 'CN¥', minorUnits: 2 },
11
+ MXN: { name: 'Mexican Peso', prefix: 'MX$', minorUnits: 2 },
12
+ NZD: { name: 'New Zealand Dollar', prefix: 'NZ$', minorUnits: 2 },
13
+ SGD: { name: 'Singapore Dollar', prefix: 'S$', minorUnits: 2 },
14
+ TRY: { name: 'Turkish Lira', prefix: '₺', minorUnits: 2 },
15
+ ZAR: { name: 'South African Rand', prefix: 'R', minorUnits: 2 },
16
+ };
17
+ // The keys of this local, closed catalog are exactly PayoutCurrencyType.
18
+ const CURRENCY_CODES = Object.keys(PAYOUT_CURRENCY_INFO);
19
+ // Every catalog key maps to itself; Object.fromEntries otherwise loses that key/value relationship.
20
+ export const PayoutCurrency = Object.fromEntries(CURRENCY_CODES.map(code => [code, code]));
21
+ /** Oracle currencies in catalog order; payoutCurrencies follows it. */
22
+ export const PAYOUT_ORACLE_CURRENCIES = CURRENCY_CODES.filter((code) => code !== 'USD');
23
+ /** Each rail's supported currencies, USD first then selector order. The API validates every pair against the SDK at boot. */
24
+ export const PAYOUT_RAIL_CURRENCIES = {
25
+ [PayoutFiatRail.VENMO]: ['USD'],
26
+ [PayoutFiatRail.CASHAPP]: ['USD'],
27
+ [PayoutFiatRail.PAYPAL]: ['USD', 'EUR', 'GBP', 'AUD', 'CAD', 'NZD', 'SGD'],
28
+ [PayoutFiatRail.ZELLE]: ['USD'],
29
+ [PayoutFiatRail.REVOLUT]: ['USD', 'EUR', 'GBP', 'AUD', 'CAD', 'CHF', 'CNY', 'MXN', 'NZD', 'SGD', 'TRY', 'ZAR'],
30
+ [PayoutFiatRail.CHIME]: ['USD'],
31
+ };
32
+ function currencyLabels(field) {
33
+ // The mapping visits every catalog key once, preserving a complete record.
34
+ return Object.fromEntries(CURRENCY_CODES.map(code => [code, PAYOUT_CURRENCY_INFO[code][field]]));
35
+ }
36
+ export const PAYOUT_CURRENCY_NAMES = currencyLabels('name');
37
+ export const PAYOUT_CURRENCY_SYMBOLS = currencyLabels('prefix');
38
+ /** A USD listing's rate: exactly 1.00 USD per USDC in 18-decimal units; USD never uses an oracle. */
39
+ export const CASHOUT_FIXED_USD_RATE = 10n ** 18n;
40
+ /** An oracle listing's fixed floor; below any market rate, so the escrow's effective rate is the oracle's (cash's ORACLE_MIN_CONVERSION_RATE_SENTINEL). */
41
+ export const CASHOUT_ORACLE_MIN_RATE_SENTINEL = 1n;
42
+ const CURRENCY_SET = new Set(Object.values(PayoutCurrency));
43
+ export function isPayoutCurrency(value) {
44
+ return CURRENCY_SET.has(value);
45
+ }
46
+ export function isCashoutOracleCurrency(value) {
47
+ return isPayoutCurrency(value) && value !== PayoutCurrency.USD;
48
+ }
49
+ export function payoutRailPaysCurrency(rail, currency) {
50
+ const currencies = PAYOUT_RAIL_CURRENCIES[rail];
51
+ return currencies.includes(currency);
52
+ }
53
+ /** A rail whose payout-method request carries `currency`: its catalog has more than one. */
54
+ export function isPayoutMultiCurrencyRail(rail) {
55
+ return PAYOUT_RAIL_CURRENCIES[rail].length > 1;
56
+ }
57
+ /** 1e6 USDC units × 1e18 rate units ÷ 100 cents. */
58
+ const CENTS_DENOMINATOR = 10n ** 22n;
59
+ /**
60
+ * Fiat cents for `units` USDC base units at `rate` (fiat per USDC, 18 decimals), rounded up: Curator's buyer quote rule.
61
+ * While a USDC unit is worth less than a cent (rate below 10,000), it recovers exactly the cents a buyer paid from the
62
+ * attestation's floored release amount.
63
+ */
64
+ export function payoutFiatCents(units, rate) {
65
+ if (units < 0n)
66
+ throw new Error('USDC units must not be negative');
67
+ if (rate <= 0n)
68
+ throw new Error('Rate must be positive');
69
+ return (units * rate + CENTS_DENOMINATOR - 1n) / CENTS_DENOMINATOR;
70
+ }
71
+ export function formatPayoutFiatCents(cents) {
72
+ return `${cents / 100n}.${(cents % 100n).toString().padStart(2, '0')}`;
73
+ }
74
+ /** Fiat per USDC with 6 decimals, rounded half up: "0.892698". Display only. */
75
+ export function formatPayoutRate(rate) {
76
+ const micro = (rate + 500000000000n) / 1000000000000n;
77
+ return `${micro / 1000000n}.${(micro % 1000000n).toString().padStart(6, '0')}`;
78
+ }
79
+ /** A stored 18-decimal rate; null unless it is a positive integer string without leading zeros. */
80
+ export function parseCashoutRate(value) {
81
+ if (value === null || !/^[1-9][0-9]{0,77}$/u.test(value))
82
+ return null;
83
+ return BigInt(value);
84
+ }
85
+ export function cashoutFiatValue(currency, units, rate) {
86
+ return { currency, amount: formatPayoutFiatCents(payoutFiatCents(units, rate)) };
87
+ }
88
+ /** Group a non-negative fiat amount with exactly two decimals; reject malformed amounts and preserve cents beyond Number's safe range. */
89
+ export function formatPayoutFiatValue(value) {
90
+ if (!/^[0-9]+\.[0-9]{2}$/u.test(value.amount))
91
+ throw new Error('Fiat amount must have exactly two decimals');
92
+ const [whole, fraction] = value.amount.split('.');
93
+ return `${PAYOUT_CURRENCY_SYMBOLS[value.currency]}${whole.replace(/\B(?=(\d{3})+(?!\d))/gu, ',')}.${fraction}`;
94
+ }
@@ -0,0 +1,34 @@
1
+ import { type CashoutBuyerProofRailType, type PayoutCryptoRailType, type PayoutFiatRailType, type PayoutSarRailType } from './types.js';
2
+ /**
3
+ * The cashout rails SAR_SUPPORTED_FIAT_RAILS lists, in rail order: Venmo, Cash App and PayPal. Wise is SAR but not a
4
+ * cashout rail yet.
5
+ */
6
+ export declare const CASHOUT_SAR_RAILS: ReadonlyArray<PayoutSarRailType>;
7
+ /** A rail in CASHOUT_SAR_RAILS. Exact rails only; a variant such as "venmo-x" is not a payout rail. */
8
+ export declare function isPayoutSarRail(rail: string): rail is PayoutSarRailType;
9
+ /** A cashout fiat rail with no connect step: Zelle, Revolut and Chime. */
10
+ export declare function isCashoutBuyerProofRail(rail: string): rail is CashoutBuyerProofRailType;
11
+ /** Every crypto payout network's rail, in rail order (checkout's relay rail order). */
12
+ export declare const PAYOUT_CRYPTO_RAILS: ReadonlyArray<PayoutCryptoRailType>;
13
+ /** An exact crypto payout rail; replaces every comparison with the retired `crypto` rail. */
14
+ export declare function isPayoutCryptoRail(rail: string): rail is PayoutCryptoRailType;
15
+ export declare function payoutCryptoRailChainId(rail: PayoutCryptoRailType): number;
16
+ export declare function cashoutCryptoRailForChain(chainId: number): PayoutCryptoRailType | null;
17
+ /** The payout chains a rail list offers, in rail order; the checkout's pickers and the server's checks read it. */
18
+ export declare function cashoutRailsChainIds(rails: ReadonlyArray<string>): number[];
19
+ export declare function hasPayoutCryptoRail(rails: ReadonlyArray<string>): boolean;
20
+ /**
21
+ * Venmo and PayPal listings get the Peer Pay merchant group. The set is @zkp2p/core's
22
+ * DISPUTE_PROTECTION_STAKE_PLATFORMS, which @zkp2p/sdk keeps private, and
23
+ * zkp2p-mobile's CHARGEBACK_PRONE_SELL_PLATFORMS. Cash App left that set on 2026-08-28.
24
+ */
25
+ export declare const CASHOUT_PEER_PAY_GROUP_RAILS: readonly ["venmo", "paypal"];
26
+ /** Rails whose listings get the Peer Pay merchant group (CASHOUT_PEER_PAY_GROUP_RAILS). */
27
+ export type CashoutPeerPayGroupRailType = typeof CASHOUT_PEER_PAY_GROUP_RAILS[number];
28
+ /** A rail in CASHOUT_PEER_PAY_GROUP_RAILS. */
29
+ export declare function isCashoutPeerPayGroupRail(rail: string): rail is CashoutPeerPayGroupRailType;
30
+ /**
31
+ * A payout account as its rail writes it: "@alice" (Venmo, Revolut), "$alice1" (Cash App), "paypal.me/bob" (PayPal),
32
+ * the email (Zelle) and "$chimesign" (Chime, whose stored handle already carries its "$").
33
+ */
34
+ export declare function payoutAccountLabel(rail: PayoutFiatRailType, handle: string): string;
@@ -0,0 +1,71 @@
1
+ import { SAR_SUPPORTED_FIAT_RAILS, parseCryptoRailChainId } from './rails.js';
2
+ import { PayoutCryptoRail, PayoutFiatRail, PayoutRail, } from './types.js';
3
+ const CASHOUT_FIAT_RAIL_SET = new Set(Object.values(PayoutFiatRail));
4
+ const SAR_SUPPORTED_FIAT_RAIL_SET = new Set(SAR_SUPPORTED_FIAT_RAILS);
5
+ /**
6
+ * The cashout rails SAR_SUPPORTED_FIAT_RAILS lists, in rail order: Venmo, Cash App and PayPal. Wise is SAR but not a
7
+ * cashout rail yet.
8
+ */
9
+ export const CASHOUT_SAR_RAILS = Object.values(PayoutFiatRail).filter((rail) => SAR_SUPPORTED_FIAT_RAIL_SET.has(rail));
10
+ const CASHOUT_SAR_RAIL_SET = new Set(CASHOUT_SAR_RAILS);
11
+ /** A rail in CASHOUT_SAR_RAILS. Exact rails only; a variant such as "venmo-x" is not a payout rail. */
12
+ export function isPayoutSarRail(rail) {
13
+ return CASHOUT_SAR_RAIL_SET.has(rail);
14
+ }
15
+ /** A cashout fiat rail with no connect step: Zelle, Revolut and Chime. */
16
+ export function isCashoutBuyerProofRail(rail) {
17
+ return CASHOUT_FIAT_RAIL_SET.has(rail) && !CASHOUT_SAR_RAIL_SET.has(rail);
18
+ }
19
+ /** Every crypto payout network's rail, in rail order (checkout's relay rail order). */
20
+ export const PAYOUT_CRYPTO_RAILS = Object.values(PayoutCryptoRail);
21
+ const CASHOUT_CRYPTO_RAIL_SET = new Set(PAYOUT_CRYPTO_RAILS);
22
+ /** An exact crypto payout rail; replaces every comparison with the retired `crypto` rail. */
23
+ export function isPayoutCryptoRail(rail) {
24
+ return CASHOUT_CRYPTO_RAIL_SET.has(rail);
25
+ }
26
+ export function payoutCryptoRailChainId(rail) {
27
+ const chainId = parseCryptoRailChainId(rail);
28
+ if (chainId === null)
29
+ throw new Error(`Cashout crypto rail ${rail} names no chain`);
30
+ return chainId;
31
+ }
32
+ const CASHOUT_CRYPTO_RAIL_BY_CHAIN = new Map(PAYOUT_CRYPTO_RAILS.map((rail) => [payoutCryptoRailChainId(rail), rail]));
33
+ export function cashoutCryptoRailForChain(chainId) {
34
+ return CASHOUT_CRYPTO_RAIL_BY_CHAIN.get(chainId) ?? null;
35
+ }
36
+ /** The payout chains a rail list offers, in rail order; the checkout's pickers and the server's checks read it. */
37
+ export function cashoutRailsChainIds(rails) {
38
+ return PAYOUT_CRYPTO_RAILS.filter((rail) => rails.includes(rail)).map(payoutCryptoRailChainId);
39
+ }
40
+ export function hasPayoutCryptoRail(rails) {
41
+ return rails.some(isPayoutCryptoRail);
42
+ }
43
+ /**
44
+ * Venmo and PayPal listings get the Peer Pay merchant group. The set is @zkp2p/core's
45
+ * DISPUTE_PROTECTION_STAKE_PLATFORMS, which @zkp2p/sdk keeps private, and
46
+ * zkp2p-mobile's CHARGEBACK_PRONE_SELL_PLATFORMS. Cash App left that set on 2026-08-28.
47
+ */
48
+ export const CASHOUT_PEER_PAY_GROUP_RAILS = [PayoutRail.VENMO, PayoutRail.PAYPAL];
49
+ const CASHOUT_PEER_PAY_GROUP_RAIL_SET = new Set(CASHOUT_PEER_PAY_GROUP_RAILS);
50
+ /** A rail in CASHOUT_PEER_PAY_GROUP_RAILS. */
51
+ export function isCashoutPeerPayGroupRail(rail) {
52
+ return CASHOUT_PEER_PAY_GROUP_RAIL_SET.has(rail);
53
+ }
54
+ /**
55
+ * A payout account as its rail writes it: "@alice" (Venmo, Revolut), "$alice1" (Cash App), "paypal.me/bob" (PayPal),
56
+ * the email (Zelle) and "$chimesign" (Chime, whose stored handle already carries its "$").
57
+ */
58
+ export function payoutAccountLabel(rail, handle) {
59
+ switch (rail) {
60
+ case PayoutRail.VENMO:
61
+ case PayoutRail.REVOLUT:
62
+ return `@${handle.replace(/^@/u, '')}`;
63
+ case PayoutRail.CASHAPP:
64
+ return `$${handle.replace(/^\$/u, '')}`;
65
+ case PayoutRail.PAYPAL:
66
+ return `paypal.me/${handle}`;
67
+ case PayoutRail.ZELLE:
68
+ case PayoutRail.CHIME:
69
+ return handle;
70
+ }
71
+ }
package/dist/crypto.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export declare const BITCOIN_CHAIN_ID = 8253038;
2
2
  export declare const ZCASH_CHAIN_ID = 133701;
3
3
  export declare const ZCASH_ASSET = "nep141:zec.omft.near";
4
+ /** NEAR Intents' asset id for USDC on Base (origin of cashout payouts, destination of Zcash checkout payments). */
5
+ export declare const NEAR_BASE_USDC_ASSET = "nep141:base-0x833589fcd6edb6e08f4c7c32d4f71b54bda02913.omft.near";
4
6
  export declare const PAY_CRYPTO_TOKEN_SYMBOLS: readonly ["BTC", "USDC", "USDT", "ETH", "PYUSD", "SOL", "BNB", "HYPE", "WBTC", "USDH", "ZEC"];
5
7
  export type PayCryptoTokenSymbol = (typeof PAY_CRYPTO_TOKEN_SYMBOLS)[number];
6
8
  export declare const DEFAULT_ENABLED_PAY_CRYPTO_TOKENS: PayCryptoTokenSymbol[];
@@ -21,6 +23,8 @@ export interface SupportedCryptoTokenConfig {
21
23
  decimals: number;
22
24
  isNative: boolean;
23
25
  }
26
+ /** Relay's sentinel for native BTC. Its bech32 checksum verifies, but it decodes to a 19-byte witness-v0 program, so isValidBitcoinAddress rejects it and it never works as a refund address. */
27
+ export declare const BITCOIN_NATIVE_TOKEN_ADDRESS = "bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmql8k8";
24
28
  export declare function normalizePayCryptoTokenSymbol(value: string | null | undefined): PayCryptoTokenSymbol | null;
25
29
  export declare function isStablecoinSymbol(symbol: string | null | undefined): boolean;
26
30
  export declare function getSupportedPayCryptoTokenConfigsForChain(chainId: number, enabledSymbols?: readonly string[] | null): SupportedCryptoTokenConfig[];
@@ -64,3 +68,38 @@ export declare function resolvePayCryptoTokenConfig(chainId: number, symbol: str
64
68
  export declare function resolvePayCryptoTokenConfigByAddress(chainId: number, address: string | null | undefined): SupportedCryptoTokenConfig | null;
65
69
  export declare function getSupportedDestinationBalanceTokenConfigsForChain(chainId: number, enabledSymbols?: readonly string[] | null): SupportedCryptoTokenConfig[];
66
70
  export declare function resolveDestinationBalanceTokenConfig(chainId: number, symbol: string | null | undefined): SupportedCryptoTokenConfig | null;
71
+ /** Chains a payout can be funded from through Relay. Zcash is out: ZEC funding is out of scope (2026-10-07). */
72
+ export declare const PAYOUT_FUNDING_CHAIN_IDS: readonly number[];
73
+ /**
74
+ * The tokens a payout can be funded with: every supported pay-crypto token on a Relay-fundable chain. An explicit list,
75
+ * because isEvmChain answers true for chains the table doesn't know, which let Bitcoin and Zcash in by accident.
76
+ */
77
+ export declare function getPayoutFundingTokenConfigs(): SupportedCryptoTokenConfig[];
78
+ /** The address formats a cashout address can take. */
79
+ export declare const CashoutAddressFamily: {
80
+ readonly EVM: "EVM";
81
+ readonly SOLANA: "SOLANA";
82
+ readonly TRON: "TRON";
83
+ readonly BITCOIN: "BITCOIN";
84
+ readonly ZCASH: "ZCASH";
85
+ };
86
+ export type CashoutAddressFamilyType = (typeof CashoutAddressFamily)[keyof typeof CashoutAddressFamily];
87
+ /**
88
+ * The address format a cashout address on this chain must have. Bitcoin, Solana and Tron map by id;
89
+ * Zcash is ZCASH (transparent addresses only, Zcash spec Z3). Every remaining chain in the pay-crypto token table
90
+ * is EVM; any other chain has none (null).
91
+ * Deliberately not isEvmChain, which answers true for unknown ids, Bitcoin's included.
92
+ */
93
+ export declare function cashoutAddressFamily(chainId: number): CashoutAddressFamilyType | null;
94
+ /** Every chain a cashout can pay out on, ascending: each pay-crypto chain, Zcash included (through NEAR Intents). */
95
+ export declare const CASHOUT_PAYOUT_CHAIN_IDS: readonly number[];
96
+ /** The catalog token at tokenAddress on a payout chain, spelled as the catalog spells it; null off the catalog. */
97
+ export declare function resolveCashoutPayoutToken(chainId: number, tokenAddress: string): SupportedCryptoTokenConfig | null;
98
+ export declare function getPayCryptoTxUrl(chainId: number, txHash: string): string | null;
99
+ export declare function getPayCryptoAddressUrl(chainId: number, address: string): string | null;
100
+ /**
101
+ * Display form of an estimate or a delivered amount (any-coin spec "Units of amounts"): stablecoins round down to two
102
+ * decimals, other tokens to six significant digits; trailing zeros go and the integer part is grouped with commas.
103
+ * The input is already exact in the token's decimals, so the result never shows more decimals than the token has.
104
+ */
105
+ export declare function formatPayCryptoDisplayAmount(amount: string, symbol: PayCryptoTokenSymbol): string;
package/dist/crypto.js CHANGED
@@ -1,8 +1,10 @@
1
- import { MAINNET_CHAINS, BASE_CHAIN_ID, ETHEREUM_CHAIN_ID, SOLANA_CHAIN_ID, TRON_CHAIN_ID, getChainName, } from './chains.js';
1
+ import { MAINNET_CHAINS, BASE_CHAIN_ID, ETHEREUM_CHAIN_ID, HYPEREVM_CHAIN_ID, SOLANA_CHAIN_ID, TRON_CHAIN_ID, getChainName, getExplorerUrl, } from './chains.js';
2
2
  export const BITCOIN_CHAIN_ID = 8253038;
3
3
  // Synthetic network ID shared with the mobile and web NEAR Intents clients.
4
4
  export const ZCASH_CHAIN_ID = 133701;
5
5
  export const ZCASH_ASSET = 'nep141:zec.omft.near';
6
+ /** NEAR Intents' asset id for USDC on Base (origin of cashout payouts, destination of Zcash checkout payments). */
7
+ export const NEAR_BASE_USDC_ASSET = 'nep141:base-0x833589fcd6edb6e08f4c7c32d4f71b54bda02913.omft.near';
6
8
  export const PAY_CRYPTO_TOKEN_SYMBOLS = [
7
9
  'BTC', // Kept in symbols for DB compatibility with existing sessions
8
10
  'USDC',
@@ -52,7 +54,8 @@ export const STABLECOIN_SYMBOLS = new Set([
52
54
  ]);
53
55
  const NATIVE_ETH_ADDRESS = '0x0000000000000000000000000000000000000000';
54
56
  const NATIVE_SOL_ADDRESS = '11111111111111111111111111111111';
55
- const NATIVE_BTC_ADDRESS = 'bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmql8k8';
57
+ /** Relay's sentinel for native BTC. Its bech32 checksum verifies, but it decodes to a 19-byte witness-v0 program, so isValidBitcoinAddress rejects it and it never works as a refund address. */
58
+ export const BITCOIN_NATIVE_TOKEN_ADDRESS = 'bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmql8k8';
56
59
  const PAY_CRYPTO_CHAIN_CONFIGS = {
57
60
  ...MAINNET_CHAINS,
58
61
  [ZCASH_CHAIN_ID]: { chainId: ZCASH_CHAIN_ID, name: 'Zcash', shortName: 'ZEC' },
@@ -67,7 +70,7 @@ const CHAIN_TOKEN_CONFIGS = {
67
70
  ZEC: { address: ZCASH_ASSET, decimals: 8, isNative: true },
68
71
  },
69
72
  [BITCOIN_CHAIN_ID]: {
70
- BTC: { address: NATIVE_BTC_ADDRESS, decimals: 8, isNative: true },
73
+ BTC: { address: BITCOIN_NATIVE_TOKEN_ADDRESS, decimals: 8, isNative: true },
71
74
  },
72
75
  1: {
73
76
  ETH: { address: NATIVE_ETH_ADDRESS, decimals: 18, isNative: true },
@@ -296,3 +299,107 @@ export function getSupportedDestinationBalanceTokenConfigsForChain(chainId, enab
296
299
  export function resolveDestinationBalanceTokenConfig(chainId, symbol) {
297
300
  return resolvePayCryptoTokenConfig(chainId, symbol);
298
301
  }
302
+ /** Chains a payout can be funded from through Relay. Zcash is out: ZEC funding is out of scope (2026-10-07). */
303
+ export const PAYOUT_FUNDING_CHAIN_IDS = [
304
+ ETHEREUM_CHAIN_ID, 10, 56, 137, 480, HYPEREVM_CHAIN_ID, 5042, BASE_CHAIN_ID, 42161, BITCOIN_CHAIN_ID,
305
+ ];
306
+ /**
307
+ * The tokens a payout can be funded with: every supported pay-crypto token on a Relay-fundable chain. An explicit list,
308
+ * because isEvmChain answers true for chains the table doesn't know, which let Bitcoin and Zcash in by accident.
309
+ */
310
+ export function getPayoutFundingTokenConfigs() {
311
+ return getSupportedPayCryptoChainIds(PAY_CRYPTO_TOKEN_SYMBOLS)
312
+ .filter((chainId) => PAYOUT_FUNDING_CHAIN_IDS.includes(chainId))
313
+ .flatMap((chainId) => getSupportedPayCryptoTokenConfigsForChain(chainId, PAY_CRYPTO_TOKEN_SYMBOLS))
314
+ .filter((token) => resolvePayCryptoTokenConfigByAddress(token.chainId, token.address) !== null);
315
+ }
316
+ /** The address formats a cashout address can take. */
317
+ export const CashoutAddressFamily = {
318
+ EVM: 'EVM',
319
+ SOLANA: 'SOLANA',
320
+ TRON: 'TRON',
321
+ BITCOIN: 'BITCOIN',
322
+ ZCASH: 'ZCASH',
323
+ };
324
+ /**
325
+ * The address format a cashout address on this chain must have. Bitcoin, Solana and Tron map by id;
326
+ * Zcash is ZCASH (transparent addresses only, Zcash spec Z3). Every remaining chain in the pay-crypto token table
327
+ * is EVM; any other chain has none (null).
328
+ * Deliberately not isEvmChain, which answers true for unknown ids, Bitcoin's included.
329
+ */
330
+ export function cashoutAddressFamily(chainId) {
331
+ if (chainId === BITCOIN_CHAIN_ID)
332
+ return CashoutAddressFamily.BITCOIN;
333
+ if (chainId === SOLANA_CHAIN_ID)
334
+ return CashoutAddressFamily.SOLANA;
335
+ if (chainId === TRON_CHAIN_ID)
336
+ return CashoutAddressFamily.TRON;
337
+ if (chainId === ZCASH_CHAIN_ID)
338
+ return CashoutAddressFamily.ZCASH;
339
+ if (CHAIN_TOKEN_CONFIGS[chainId] === undefined)
340
+ return null;
341
+ return CashoutAddressFamily.EVM;
342
+ }
343
+ /** Every chain a cashout can pay out on, ascending: each pay-crypto chain, Zcash included (through NEAR Intents). */
344
+ export const CASHOUT_PAYOUT_CHAIN_IDS = Object.keys(CHAIN_TOKEN_CONFIGS).map(Number);
345
+ /** The catalog token at tokenAddress on a payout chain, spelled as the catalog spells it; null off the catalog. */
346
+ export function resolveCashoutPayoutToken(chainId, tokenAddress) {
347
+ if (!CASHOUT_PAYOUT_CHAIN_IDS.includes(chainId))
348
+ return null;
349
+ return resolvePayCryptoTokenConfigByAddress(chainId, tokenAddress);
350
+ }
351
+ /** Chains whose explorer paths differ from the EVM `{explorer}/tx/` and `{explorer}/address/` forms. */
352
+ const PAY_CRYPTO_EXPLORER_PATHS = {
353
+ [SOLANA_CHAIN_ID]: { tx: 'https://solscan.io/tx/', address: 'https://solscan.io/account/' },
354
+ [TRON_CHAIN_ID]: { tx: 'https://tronscan.org/#/transaction/', address: 'https://tronscan.org/#/address/' },
355
+ [BITCOIN_CHAIN_ID]: { tx: 'https://mempool.space/tx/', address: 'https://mempool.space/address/' },
356
+ [ZCASH_CHAIN_ID]: { tx: 'https://blockchair.com/zcash/transaction/', address: 'https://blockchair.com/zcash/address/' },
357
+ };
358
+ export function getPayCryptoTxUrl(chainId, txHash) {
359
+ const paths = PAY_CRYPTO_EXPLORER_PATHS[chainId];
360
+ if (paths !== undefined)
361
+ return `${paths.tx}${txHash}`;
362
+ const explorer = getExplorerUrl(chainId);
363
+ return explorer === undefined ? null : `${explorer}/tx/${txHash}`;
364
+ }
365
+ export function getPayCryptoAddressUrl(chainId, address) {
366
+ const paths = PAY_CRYPTO_EXPLORER_PATHS[chainId];
367
+ if (paths !== undefined)
368
+ return `${paths.address}${address}`;
369
+ const explorer = getExplorerUrl(chainId);
370
+ return explorer === undefined ? null : `${explorer}/address/${address}`;
371
+ }
372
+ const DISPLAY_SIGNIFICANT_DIGITS = 6;
373
+ /**
374
+ * Display form of an estimate or a delivered amount (any-coin spec "Units of amounts"): stablecoins round down to two
375
+ * decimals, other tokens to six significant digits; trailing zeros go and the integer part is grouped with commas.
376
+ * The input is already exact in the token's decimals, so the result never shows more decimals than the token has.
377
+ */
378
+ export function formatPayCryptoDisplayAmount(amount, symbol) {
379
+ const match = /^(\d+)(?:\.(\d+))?$/u.exec(amount);
380
+ if (match === null)
381
+ throw new Error(`Not a decimal amount: ${amount}`);
382
+ const integer = match[1].replace(/^0+(?=\d)/u, '');
383
+ const fraction = match[2] ?? '';
384
+ let keptInteger = integer;
385
+ let keptFraction;
386
+ if (isStablecoinSymbol(symbol)) {
387
+ keptFraction = fraction.slice(0, 2);
388
+ }
389
+ else {
390
+ const first = `${integer}${fraction}`.search(/[1-9]/u);
391
+ if (first === -1)
392
+ return '0';
393
+ const cut = first + DISPLAY_SIGNIFICANT_DIGITS;
394
+ if (cut <= integer.length) {
395
+ keptInteger = `${integer.slice(0, cut)}${'0'.repeat(integer.length - cut)}`;
396
+ keptFraction = '';
397
+ }
398
+ else {
399
+ keptFraction = fraction.slice(0, cut - integer.length);
400
+ }
401
+ }
402
+ const trimmed = keptFraction.replace(/0+$/u, '');
403
+ const grouped = keptInteger.replace(/\B(?=(\d{3})+(?!\d))/gu, ',');
404
+ return trimmed === '' ? grouped : `${grouped}.${trimmed}`;
405
+ }
package/dist/index.d.ts CHANGED
@@ -4,6 +4,8 @@ export * from './chains.js';
4
4
  export * from './fees.js';
5
5
  export * from './crypto.js';
6
6
  export * from './rails.js';
7
+ export * from './cashoutRails.js';
8
+ export * from './cashoutCurrencies.js';
7
9
  export * from './buyerTee.js';
8
10
  export * from './bitcoin.js';
9
11
  export * from './addressValidators.js';
package/dist/index.js CHANGED
@@ -4,6 +4,8 @@ export * from './chains.js';
4
4
  export * from './fees.js';
5
5
  export * from './crypto.js';
6
6
  export * from './rails.js';
7
+ export * from './cashoutRails.js';
8
+ export * from './cashoutCurrencies.js';
7
9
  export * from './buyerTee.js';
8
10
  export * from './bitcoin.js';
9
11
  export * from './addressValidators.js';