@zkp2p/pay-shared 4.0.1 → 6.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@4.0.1
12
+ npm install @zkp2p/pay-shared@6.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
+ - `CashoutView`
89
+ - `CashoutRailType`
90
+ - `WebhookPayload`
91
+ - `CashoutWebhookPayload`
88
92
 
89
93
  Core constants:
90
94
 
@@ -96,6 +100,15 @@ Core constants:
96
100
  - `ProofAttemptStatus`
97
101
  - `OrderErrorCode`
98
102
  - `MerchantEnvironment`
103
+ - `CashoutStatus`
104
+ - `CashoutRail`
105
+ - `CashoutCryptoRail`
106
+ - `CashoutPayoutProvider`
107
+ - `PAY_CRYPTO_TOKEN_SYMBOLS`
108
+
109
+ Webhook helpers:
110
+
111
+ - `isCashoutWebhook(...)` — narrows `WebhookPayload` to `CashoutWebhookPayload`
99
112
 
100
113
  Chain constants and helpers:
101
114
 
@@ -115,5 +128,5 @@ Chain constants and helpers:
115
128
  ## Notes
116
129
 
117
130
  - Amount fields are strings to preserve precision across APIs and clients.
118
- - Canonical order/payment amounts are available on `CheckoutOrder` and `CheckoutPayment` (for example `requestedUsdcAmount`, `remainingUsdcAmount`, `netSettledUsdcAmount`).
131
+ - Canonical order/payment amounts are available on `CheckoutOrder` and `CheckoutPayment` (for example `requestedUsdcAmount`, `remainingUsdcAmount`, `netSettledUsdcAmount`). `CheckoutPayment.penalties` lists attestation-applied reductions (`PaymentPenalty[]`, empty when none).
119
132
  - This package is ESM-only.
@@ -1,2 +1,4 @@
1
+ /** NEAR Intents accepts mainnet transparent Zcash refund addresses only. */
2
+ export declare function isValidZcashAddress(value: string): boolean;
1
3
  export declare function isValidSolanaAddress(value: string): boolean;
2
4
  export declare function isValidTronAddress(value: string): boolean;
@@ -1,4 +1,15 @@
1
- import { decodeBase58, encodeBase58, hasBase58CheckChecksum, } from './addressEncoding.js';
1
+ import { decodeBase58, doubleSha256, encodeBase58, hasBase58CheckChecksum, } from './addressEncoding.js';
2
+ /** NEAR Intents accepts mainnet transparent Zcash refund addresses only. */
3
+ export function isValidZcashAddress(value) {
4
+ if (!/^t[13][1-9A-HJ-NP-Za-km-z]{33}$/.test(value))
5
+ return false;
6
+ const decoded = decodeBase58(value);
7
+ if (decoded === null || decoded.length !== 26 || decoded[0] !== 0x1c
8
+ || (decoded[1] !== 0xb8 && decoded[1] !== 0xbd))
9
+ return false;
10
+ const checksum = doubleSha256(decoded.subarray(0, 22));
11
+ return decoded.subarray(22).every((byte, index) => byte === checksum[index]);
12
+ }
2
13
  export function isValidSolanaAddress(value) {
3
14
  if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(value)) {
4
15
  return false;
@@ -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,34 @@
1
+ import { type CashoutBuyerProofRailType, type CashoutCryptoRailType, type CashoutFiatRailType, type CashoutSarRailType } 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<CashoutSarRailType>;
7
+ /** A rail in CASHOUT_SAR_RAILS. Exact rails only; a variant such as "venmo-x" is not a cashout rail. */
8
+ export declare function isCashoutSarRail(rail: string): rail is CashoutSarRailType;
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 CASHOUT_CRYPTO_RAILS: ReadonlyArray<CashoutCryptoRailType>;
13
+ /** An exact crypto payout rail; replaces every comparison with the retired `crypto` rail. */
14
+ export declare function isCashoutCryptoRail(rail: string): rail is CashoutCryptoRailType;
15
+ export declare function cashoutCryptoRailChainId(rail: CashoutCryptoRailType): number;
16
+ export declare function cashoutCryptoRailForChain(chainId: number): CashoutCryptoRailType | 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 hasCashoutCryptoRail(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 cashoutAccountLabel(rail: CashoutFiatRailType, handle: string): string;
@@ -0,0 +1,71 @@
1
+ import { SAR_SUPPORTED_FIAT_RAILS, parseCryptoRailChainId } from './rails.js';
2
+ import { CashoutCryptoRail, CashoutFiatRail, CashoutRail, } from './types.js';
3
+ const CASHOUT_FIAT_RAIL_SET = new Set(Object.values(CashoutFiatRail));
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(CashoutFiatRail).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 cashout rail. */
12
+ export function isCashoutSarRail(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 CASHOUT_CRYPTO_RAILS = Object.values(CashoutCryptoRail);
21
+ const CASHOUT_CRYPTO_RAIL_SET = new Set(CASHOUT_CRYPTO_RAILS);
22
+ /** An exact crypto payout rail; replaces every comparison with the retired `crypto` rail. */
23
+ export function isCashoutCryptoRail(rail) {
24
+ return CASHOUT_CRYPTO_RAIL_SET.has(rail);
25
+ }
26
+ export function cashoutCryptoRailChainId(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(CASHOUT_CRYPTO_RAILS.map((rail) => [cashoutCryptoRailChainId(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 CASHOUT_CRYPTO_RAILS.filter((rail) => rails.includes(rail)).map(cashoutCryptoRailChainId);
39
+ }
40
+ export function hasCashoutCryptoRail(rails) {
41
+ return rails.some(isCashoutCryptoRail);
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 = [CashoutRail.VENMO, CashoutRail.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 cashoutAccountLabel(rail, handle) {
59
+ switch (rail) {
60
+ case CashoutRail.VENMO:
61
+ case CashoutRail.REVOLUT:
62
+ return `@${handle.replace(/^@/u, '')}`;
63
+ case CashoutRail.CASHAPP:
64
+ return `$${handle.replace(/^\$/u, '')}`;
65
+ case CashoutRail.PAYPAL:
66
+ return `paypal.me/${handle}`;
67
+ case CashoutRail.ZELLE:
68
+ case CashoutRail.CHIME:
69
+ return handle;
70
+ }
71
+ }
package/dist/chains.js CHANGED
@@ -88,6 +88,15 @@ export const MAINNET_CHAINS = {
88
88
  isEvm: true,
89
89
  explorerUrl: 'https://hyperscan.xyz',
90
90
  },
91
+ 5042: {
92
+ chainId: 5042,
93
+ name: 'Arc',
94
+ shortName: 'ARC',
95
+ usdcAddress: '0x3600000000000000000000000000000000000000',
96
+ isOriginChain: false,
97
+ isEvm: true,
98
+ explorerUrl: 'https://explorer.arc.io',
99
+ },
91
100
  // Non-EVM Destination chains
92
101
  [SOLANA_CHAIN_ID]: {
93
102
  chainId: SOLANA_CHAIN_ID,
@@ -0,0 +1,25 @@
1
+ /** Shared, non-secret context for the Pay browser surfaces. */
2
+ export type PayClientSurface = 'checkout' | 'merchant-dashboard' | 'support-dashboard';
3
+ export declare function payClientContext(input: {
4
+ surface: PayClientSurface;
5
+ hostname: string;
6
+ productionBuild: boolean;
7
+ appVersion: string;
8
+ merchantId?: string;
9
+ environment?: 'LIVE' | 'SANDBOX';
10
+ qaRun: boolean;
11
+ }): {
12
+ merchant_id?: string | undefined;
13
+ product: string;
14
+ surface: PayClientSurface;
15
+ hostname: string;
16
+ deployment_environment: string;
17
+ app_version: string;
18
+ environment: string;
19
+ is_qa: boolean;
20
+ };
21
+ /**
22
+ * Route templates only: never emit customer IDs, search parameters or fragments. The support
23
+ * dashboard templates its routes with `supportTelemetryRoute` from the support contracts.
24
+ */
25
+ export declare function payClientRoute(surface: Exclude<PayClientSurface, 'support-dashboard'>, pathname: string): string;
@@ -0,0 +1,30 @@
1
+ const PRODUCTION_HOST = {
2
+ checkout: 'pay.peer.xyz',
3
+ 'merchant-dashboard': 'merchant.pay.peer.xyz',
4
+ 'support-dashboard': 'support.pay.peer.xyz',
5
+ };
6
+ export function payClientContext(input) {
7
+ const productionHost = PRODUCTION_HOST[input.surface];
8
+ return {
9
+ product: 'pay',
10
+ surface: input.surface,
11
+ hostname: input.hostname,
12
+ deployment_environment: input.productionBuild && input.hostname === productionHost
13
+ ? 'PRODUCTION' : input.productionBuild ? 'PREPROD' : 'DEVELOPMENT',
14
+ app_version: input.appVersion,
15
+ environment: input.environment ?? 'UNKNOWN',
16
+ is_qa: input.qaRun || input.merchantId === 'cmrunizzo02wf3ua8t6yx1mns',
17
+ ...(input.merchantId ? { merchant_id: input.merchantId } : {}),
18
+ };
19
+ }
20
+ /**
21
+ * Route templates only: never emit customer IDs, search parameters or fragments. The support
22
+ * dashboard templates its routes with `supportTelemetryRoute` from the support contracts.
23
+ */
24
+ export function payClientRoute(surface, pathname) {
25
+ const path = pathname.split(/[?#]/, 1)[0];
26
+ if (surface === 'checkout')
27
+ return path === '/' ? '/' : '/:checkout';
28
+ return /^\/(?:login|onboarding|orders|staking|referrals|scan-to-pay|webhooks|faq|resources|settings(?:\/(?:account|payments|checkout|developer|billing|interface|tap-to-pay))?)?$/.test(path)
29
+ ? path : '/:unknown';
30
+ }
@@ -0,0 +1,56 @@
1
+ export declare const CONCIERGE_QUESTIONS: readonly [{
2
+ readonly id: "business";
3
+ readonly label: "What do you sell, and what industry are you in?";
4
+ readonly hint: "Your products or services and industry.";
5
+ }, {
6
+ readonly id: "storefront";
7
+ readonly label: "Where do you sell?";
8
+ readonly hint: "Your website, plus what it runs on: Shopify, WooCommerce, custom site, Telegram, or in person.";
9
+ }, {
10
+ readonly id: "volume";
11
+ readonly label: "How much volume do you process today?";
12
+ readonly hint: "Daily, weekly, or monthly volume, plus your average order size. Include the currency.";
13
+ }, {
14
+ readonly id: "customers";
15
+ readonly label: "Where are your customers?";
16
+ readonly hint: "US, UK, EU, or elsewhere. This decides which payment apps we enable.";
17
+ }, {
18
+ readonly id: "apps";
19
+ readonly label: "Which payment apps do you want your customers paying with?";
20
+ readonly hint: "Venmo, Cash App, Zelle, PayPal, Revolut, Wise, or others.";
21
+ }, {
22
+ readonly id: "payments";
23
+ readonly label: "What do you use to accept payments today?";
24
+ readonly hint: "Your current payment providers or methods.";
25
+ }, {
26
+ readonly id: "crypto";
27
+ readonly label: "Do you also want to accept crypto directly?";
28
+ readonly hint: "Customers can pay with any coin on any network and you still settle in USDC.";
29
+ readonly options: readonly ["Yes", "No", "Not sure yet"];
30
+ }, {
31
+ readonly id: "feePayer";
32
+ readonly label: "Who pays the Peer Pay fee?";
33
+ readonly hint: "Your customer, you, or a split between both.";
34
+ readonly options: readonly ["Customer", "Merchant", "Split"];
35
+ }, {
36
+ readonly id: "integrator";
37
+ readonly label: "Who is doing the integration on your side, you or a developer?";
38
+ readonly hint: "We will send the right guide and set up an API key if needed.";
39
+ }];
40
+ export type ConciergeQuestionId = typeof CONCIERGE_QUESTIONS[number]['id'];
41
+ export type ConciergeQuestion = {
42
+ readonly id: ConciergeQuestionId;
43
+ readonly label: string;
44
+ readonly hint: string;
45
+ readonly options?: readonly string[];
46
+ };
47
+ export declare const CONCIERGE_QUESTION_IDS: readonly ConciergeQuestionId[];
48
+ export type ConciergeAnswers = Partial<Record<ConciergeQuestionId, string>>;
49
+ export type ConciergeInquiryInput = {
50
+ readonly contactEmail: string;
51
+ readonly businessName: string;
52
+ readonly telegramUsername: string;
53
+ readonly answers: ConciergeAnswers;
54
+ readonly message: string;
55
+ };
56
+ export declare function isCompleteConciergeAnswers(value: ConciergeAnswers): boolean;
@@ -0,0 +1,15 @@
1
+ export const CONCIERGE_QUESTIONS = [
2
+ { id: 'business', label: 'What do you sell, and what industry are you in?', hint: 'Your products or services and industry.' },
3
+ { id: 'storefront', label: 'Where do you sell?', hint: 'Your website, plus what it runs on: Shopify, WooCommerce, custom site, Telegram, or in person.' },
4
+ { id: 'volume', label: 'How much volume do you process today?', hint: 'Daily, weekly, or monthly volume, plus your average order size. Include the currency.' },
5
+ { id: 'customers', label: 'Where are your customers?', hint: 'US, UK, EU, or elsewhere. This decides which payment apps we enable.' },
6
+ { id: 'apps', label: 'Which payment apps do you want your customers paying with?', hint: 'Venmo, Cash App, Zelle, PayPal, Revolut, Wise, or others.' },
7
+ { id: 'payments', label: 'What do you use to accept payments today?', hint: 'Your current payment providers or methods.' },
8
+ { id: 'crypto', label: 'Do you also want to accept crypto directly?', hint: 'Customers can pay with any coin on any network and you still settle in USDC.', options: ['Yes', 'No', 'Not sure yet'] },
9
+ { id: 'feePayer', label: 'Who pays the Peer Pay fee?', hint: 'Your customer, you, or a split between both.', options: ['Customer', 'Merchant', 'Split'] },
10
+ { id: 'integrator', label: 'Who is doing the integration on your side, you or a developer?', hint: 'We will send the right guide and set up an API key if needed.' },
11
+ ];
12
+ export const CONCIERGE_QUESTION_IDS = CONCIERGE_QUESTIONS.map(({ id }) => id);
13
+ export function isCompleteConciergeAnswers(value) {
14
+ return CONCIERGE_QUESTION_IDS.every((id) => Boolean(value[id]?.trim()));
15
+ }
package/dist/crypto.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  export declare const BITCOIN_CHAIN_ID = 8253038;
2
- export declare const PAY_CRYPTO_TOKEN_SYMBOLS: readonly ["BTC", "USDC", "USDT", "ETH", "PYUSD", "SOL", "BNB", "HYPE", "WBTC", "USDH"];
2
+ export declare const ZCASH_CHAIN_ID = 133701;
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";
6
+ export declare const PAY_CRYPTO_TOKEN_SYMBOLS: readonly ["BTC", "USDC", "USDT", "ETH", "PYUSD", "SOL", "BNB", "HYPE", "WBTC", "USDH", "ZEC"];
3
7
  export type PayCryptoTokenSymbol = (typeof PAY_CRYPTO_TOKEN_SYMBOLS)[number];
4
8
  export declare const DEFAULT_ENABLED_PAY_CRYPTO_TOKENS: PayCryptoTokenSymbol[];
5
9
  export declare const DEFAULT_ENABLED_DESTINATION_TOKENS: PayCryptoTokenSymbol[];
@@ -9,7 +13,7 @@ export declare const DEFAULT_ENABLED_DESTINATION_TOKENS: PayCryptoTokenSymbol[];
9
13
  * iteration order of PAY_CRYPTO_CHAIN_CONFIGS as a stable tiebreaker.
10
14
  */
11
15
  export declare const PAY_CRYPTO_CHAIN_DISPLAY_PRIORITY: readonly number[];
12
- export declare const STABLECOIN_SYMBOLS: Set<"USDC" | "USDT" | "ETH" | "BNB" | "SOL" | "BTC" | "PYUSD" | "HYPE" | "WBTC" | "USDH">;
16
+ export declare const STABLECOIN_SYMBOLS: Set<"USDC" | "USDT" | "ETH" | "BNB" | "SOL" | "BTC" | "PYUSD" | "HYPE" | "WBTC" | "USDH" | "ZEC">;
13
17
  export interface SupportedCryptoTokenConfig {
14
18
  chainId: number;
15
19
  chainName: string;
@@ -19,6 +23,8 @@ export interface SupportedCryptoTokenConfig {
19
23
  decimals: number;
20
24
  isNative: boolean;
21
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";
22
28
  export declare function normalizePayCryptoTokenSymbol(value: string | null | undefined): PayCryptoTokenSymbol | null;
23
29
  export declare function isStablecoinSymbol(symbol: string | null | undefined): boolean;
24
30
  export declare function getSupportedPayCryptoTokenConfigsForChain(chainId: number, enabledSymbols?: readonly string[] | null): SupportedCryptoTokenConfig[];
@@ -62,3 +68,38 @@ export declare function resolvePayCryptoTokenConfig(chainId: number, symbol: str
62
68
  export declare function resolvePayCryptoTokenConfigByAddress(chainId: number, address: string | null | undefined): SupportedCryptoTokenConfig | null;
63
69
  export declare function getSupportedDestinationBalanceTokenConfigsForChain(chainId: number, enabledSymbols?: readonly string[] | null): SupportedCryptoTokenConfig[];
64
70
  export declare function resolveDestinationBalanceTokenConfig(chainId: number, symbol: string | null | undefined): SupportedCryptoTokenConfig | null;
71
+ /** Chains a cashout can be funded from through Relay. Zcash is out: ZEC funding is out of scope (2026-10-07). */
72
+ export declare const CASHOUT_FUNDING_CHAIN_IDS: readonly number[];
73
+ /**
74
+ * The tokens a cashout 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 getCashoutFundingTokenConfigs(): 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,5 +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
+ // Synthetic network ID shared with the mobile and web NEAR Intents clients.
4
+ export const ZCASH_CHAIN_ID = 133701;
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';
3
8
  export const PAY_CRYPTO_TOKEN_SYMBOLS = [
4
9
  'BTC', // Kept in symbols for DB compatibility with existing sessions
5
10
  'USDC',
@@ -11,6 +16,7 @@ export const PAY_CRYPTO_TOKEN_SYMBOLS = [
11
16
  'HYPE',
12
17
  'WBTC',
13
18
  'USDH',
19
+ 'ZEC',
14
20
  ];
15
21
  export const DEFAULT_ENABLED_PAY_CRYPTO_TOKENS = [
16
22
  'BTC',
@@ -23,6 +29,7 @@ export const DEFAULT_ENABLED_PAY_CRYPTO_TOKENS = [
23
29
  'HYPE',
24
30
  'WBTC',
25
31
  'USDH',
32
+ 'ZEC',
26
33
  ];
27
34
  export const DEFAULT_ENABLED_DESTINATION_TOKENS = [
28
35
  'USDC',
@@ -47,9 +54,11 @@ export const STABLECOIN_SYMBOLS = new Set([
47
54
  ]);
48
55
  const NATIVE_ETH_ADDRESS = '0x0000000000000000000000000000000000000000';
49
56
  const NATIVE_SOL_ADDRESS = '11111111111111111111111111111111';
50
- 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';
51
59
  const PAY_CRYPTO_CHAIN_CONFIGS = {
52
60
  ...MAINNET_CHAINS,
61
+ [ZCASH_CHAIN_ID]: { chainId: ZCASH_CHAIN_ID, name: 'Zcash', shortName: 'ZEC' },
53
62
  [BITCOIN_CHAIN_ID]: {
54
63
  chainId: BITCOIN_CHAIN_ID,
55
64
  name: 'Bitcoin',
@@ -57,8 +66,11 @@ const PAY_CRYPTO_CHAIN_CONFIGS = {
57
66
  },
58
67
  };
59
68
  const CHAIN_TOKEN_CONFIGS = {
69
+ [ZCASH_CHAIN_ID]: {
70
+ ZEC: { address: ZCASH_ASSET, decimals: 8, isNative: true },
71
+ },
60
72
  [BITCOIN_CHAIN_ID]: {
61
- BTC: { address: NATIVE_BTC_ADDRESS, decimals: 8, isNative: true },
73
+ BTC: { address: BITCOIN_NATIVE_TOKEN_ADDRESS, decimals: 8, isNative: true },
62
74
  },
63
75
  1: {
64
76
  ETH: { address: NATIVE_ETH_ADDRESS, decimals: 18, isNative: true },
@@ -92,6 +104,10 @@ const CHAIN_TOKEN_CONFIGS = {
92
104
  USDC: { address: '0xb88339CB7199b77E23DB6E890353E22632Ba630f', decimals: 6, isNative: false },
93
105
  USDH: { address: '0x111111a1a0667d36bd57c0a9f569b98057111111', decimals: 6, isNative: false },
94
106
  },
107
+ 5042: {
108
+ // Arc's ERC-20 USDC interface has 6 decimals; native gas uses 18.
109
+ USDC: { address: '0x3600000000000000000000000000000000000000', decimals: 6, isNative: false },
110
+ },
95
111
  8453: {
96
112
  ETH: { address: NATIVE_ETH_ADDRESS, decimals: 18, isNative: true },
97
113
  USDC: { address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', decimals: 6, isNative: false },
@@ -283,3 +299,107 @@ export function getSupportedDestinationBalanceTokenConfigsForChain(chainId, enab
283
299
  export function resolveDestinationBalanceTokenConfig(chainId, symbol) {
284
300
  return resolvePayCryptoTokenConfig(chainId, symbol);
285
301
  }
302
+ /** Chains a cashout can be funded from through Relay. Zcash is out: ZEC funding is out of scope (2026-10-07). */
303
+ export const CASHOUT_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 cashout 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 getCashoutFundingTokenConfigs() {
311
+ return getSupportedPayCryptoChainIds(PAY_CRYPTO_TOKEN_SYMBOLS)
312
+ .filter((chainId) => CASHOUT_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
+ }