@zkp2p/pay-shared 0.0.2 → 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/fees.js ADDED
@@ -0,0 +1,195 @@
1
+ const TRANCHE_SCALE_BY_CURRENCY = {
2
+ USD: 2,
3
+ USDC: 2,
4
+ };
5
+ const USDC_DECIMALS = 6;
6
+ const USDC_SCALE = 10n ** BigInt(USDC_DECIMALS);
7
+ const BASIS_POINTS_SCALE = 10000n;
8
+ function normalizeBoundary(raw) {
9
+ if (!Number.isFinite(raw) || raw < 0) {
10
+ throw new Error(`Invalid tranche boundary: ${raw}`);
11
+ }
12
+ const normalized = String(raw);
13
+ if (normalized.includes('e') || normalized.includes('E')) {
14
+ throw new Error(`Invalid tranche boundary: ${raw}`);
15
+ }
16
+ return normalized;
17
+ }
18
+ export function parseBoundaryUnits(raw, currency) {
19
+ const normalized = normalizeBoundary(raw);
20
+ if (!/^\d+(\.\d+)?$/.test(normalized)) {
21
+ throw new Error(`Invalid tranche boundary: ${raw}`);
22
+ }
23
+ const decimals = TRANCHE_SCALE_BY_CURRENCY[currency];
24
+ const [whole, fraction = ''] = normalized.split('.');
25
+ if (fraction.length > decimals) {
26
+ throw new Error(`Too many decimal places for ${currency}: ${raw}`);
27
+ }
28
+ return BigInt(whole) * 10n ** BigInt(decimals) + BigInt(fraction.padEnd(decimals, '0'));
29
+ }
30
+ export function parseLookupUnits(raw, currency) {
31
+ const normalized = raw.trim();
32
+ if (!/^\d+(\.\d+)?$/.test(normalized)) {
33
+ throw new Error(`Invalid lookup amount: ${raw}`);
34
+ }
35
+ const decimals = TRANCHE_SCALE_BY_CURRENCY[currency];
36
+ const [whole, fraction = ''] = normalized.split('.');
37
+ const keptFraction = fraction.slice(0, decimals).padEnd(decimals, '0');
38
+ const firstDiscardedDigit = fraction[decimals];
39
+ const shouldRoundUp = firstDiscardedDigit !== undefined && firstDiscardedDigit >= '5';
40
+ return BigInt(whole) * 10n ** BigInt(decimals)
41
+ + BigInt(keptFraction)
42
+ + (shouldRoundUp ? 1n : 0n);
43
+ }
44
+ export function parseUsdcUnits(raw) {
45
+ const normalized = raw.trim();
46
+ if (!/^\d+(\.\d+)?$/.test(normalized)) {
47
+ throw new Error(`Invalid USDC amount: ${raw}`);
48
+ }
49
+ const [whole, fraction = ''] = normalized.split('.');
50
+ if (fraction.length > USDC_DECIMALS) {
51
+ throw new Error(`USDC amount supports at most ${USDC_DECIMALS} decimals`);
52
+ }
53
+ return BigInt(whole) * USDC_SCALE + BigInt(fraction.padEnd(USDC_DECIMALS, '0'));
54
+ }
55
+ export function formatUsdcUnits(units) {
56
+ const whole = units / USDC_SCALE;
57
+ const fraction = units % USDC_SCALE;
58
+ return `${whole.toString()}.${fraction.toString().padStart(USDC_DECIMALS, '0')}`;
59
+ }
60
+ export function calculateGrossFromNetUnits(netAmountUnits, feeBps) {
61
+ if (!Number.isInteger(feeBps) || feeBps < 0 || feeBps >= 10000) {
62
+ throw new Error(`Invalid referral fee bps: ${feeBps}`);
63
+ }
64
+ if (feeBps === 0) {
65
+ return netAmountUnits;
66
+ }
67
+ const denominator = BASIS_POINTS_SCALE - BigInt(feeBps);
68
+ const numerator = netAmountUnits * BASIS_POINTS_SCALE;
69
+ return (numerator + (denominator / 2n)) / denominator;
70
+ }
71
+ export function validateTranches(tranches, currency) {
72
+ if (tranches.length === 0) {
73
+ throw new Error('At least one tranche is required');
74
+ }
75
+ let expectedMinUnits = 0n;
76
+ tranches.forEach((tranche, index) => {
77
+ const minUnits = parseBoundaryUnits(tranche.min, currency);
78
+ if (minUnits !== expectedMinUnits) {
79
+ throw new Error('Tranche ranges must be contiguous and start at zero');
80
+ }
81
+ if (tranche.max === null) {
82
+ if (index !== tranches.length - 1) {
83
+ throw new Error('Only the final tranche may be open-ended');
84
+ }
85
+ return;
86
+ }
87
+ const maxUnits = parseBoundaryUnits(tranche.max, currency);
88
+ if (maxUnits < minUnits) {
89
+ throw new Error('Tranche max must be greater than or equal to min');
90
+ }
91
+ expectedMinUnits = maxUnits + 1n;
92
+ });
93
+ if (tranches[tranches.length - 1]?.max !== null) {
94
+ throw new Error('The final tranche must be open-ended');
95
+ }
96
+ }
97
+ export function getMatchingTrancheIndex(tranches, amount, currency) {
98
+ validateTranches(tranches, currency);
99
+ const amountUnits = parseLookupUnits(amount, currency);
100
+ const index = tranches.findIndex((tranche) => {
101
+ const minUnits = parseBoundaryUnits(tranche.min, currency);
102
+ const maxUnits = tranche.max === null ? null : parseBoundaryUnits(tranche.max, currency);
103
+ return amountUnits >= minUnits && (maxUnits === null || amountUnits <= maxUnits);
104
+ });
105
+ if (index === -1) {
106
+ throw new Error(`No tranche found for ${amount} ${currency}`);
107
+ }
108
+ return index;
109
+ }
110
+ export function resolveReferralFeeConfig(config, amountUsdc, options = {}) {
111
+ if (config.mode === 'default') {
112
+ return {
113
+ feeSource: 'default',
114
+ referralFeeBps: options.defaultFeeBps ?? 0,
115
+ };
116
+ }
117
+ if (config.mode === 'exempt') {
118
+ return {
119
+ feeSource: 'exempt',
120
+ referralFeeBps: 0,
121
+ };
122
+ }
123
+ if (config.mode === 'flat') {
124
+ return {
125
+ feeSource: 'merchant',
126
+ referralFeeBps: config.valueBps,
127
+ };
128
+ }
129
+ const matchedTrancheIndex = getMatchingTrancheIndex(config.tranches, amountUsdc, 'USDC');
130
+ return {
131
+ feeSource: 'merchant',
132
+ referralFeeBps: config.tranches[matchedTrancheIndex].value,
133
+ };
134
+ }
135
+ export function resolveExactTokenGrossAmountUsdc(config, requestedNetAmountUsdc, options = {}) {
136
+ const netAmountUnits = parseUsdcUnits(requestedNetAmountUsdc);
137
+ if (config.mode === 'default') {
138
+ return formatUsdcUnits(calculateGrossFromNetUnits(netAmountUnits, options.defaultFeeBps ?? 0));
139
+ }
140
+ if (config.mode === 'exempt') {
141
+ return formatUsdcUnits(netAmountUnits);
142
+ }
143
+ if (config.mode === 'flat') {
144
+ if (config.valueBps >= 10000) {
145
+ throw new Error('Referral fee must be less than 100% for exact-token requests');
146
+ }
147
+ return formatUsdcUnits(calculateGrossFromNetUnits(netAmountUnits, config.valueBps));
148
+ }
149
+ validateTranches(config.tranches, 'USDC');
150
+ let resolvedGrossAmountUnits = null;
151
+ let snappedGrossAmountUnits = null;
152
+ let smallestSnapFloorUnits = null;
153
+ for (const tranche of config.tranches) {
154
+ if (tranche.value >= 10000) {
155
+ continue;
156
+ }
157
+ const impliedGrossAmountUnits = calculateGrossFromNetUnits(netAmountUnits, tranche.value);
158
+ const impliedGrossLookupUnits = parseLookupUnits(formatUsdcUnits(impliedGrossAmountUnits), 'USDC');
159
+ const trancheMinUnits = parseBoundaryUnits(tranche.min, 'USDC');
160
+ if (impliedGrossLookupUnits < trancheMinUnits) {
161
+ if (smallestSnapFloorUnits === null || trancheMinUnits < smallestSnapFloorUnits) {
162
+ smallestSnapFloorUnits = trancheMinUnits;
163
+ snappedGrossAmountUnits = parseUsdcUnits(normalizeBoundary(tranche.min));
164
+ }
165
+ continue;
166
+ }
167
+ const trancheMaxUnits = tranche.max === null ? null : parseBoundaryUnits(tranche.max, 'USDC');
168
+ const isSelfConsistent = impliedGrossLookupUnits >= trancheMinUnits
169
+ && (trancheMaxUnits === null || impliedGrossLookupUnits <= trancheMaxUnits);
170
+ if (!isSelfConsistent) {
171
+ continue;
172
+ }
173
+ if (resolvedGrossAmountUnits === null || impliedGrossAmountUnits < resolvedGrossAmountUnits) {
174
+ resolvedGrossAmountUnits = impliedGrossAmountUnits;
175
+ }
176
+ }
177
+ if (resolvedGrossAmountUnits !== null) {
178
+ return formatUsdcUnits(resolvedGrossAmountUnits);
179
+ }
180
+ if (snappedGrossAmountUnits !== null) {
181
+ return formatUsdcUnits(snappedGrossAmountUnits);
182
+ }
183
+ throw new Error('Unable to resolve referral fee tranche for exact-token request amount');
184
+ }
185
+ export function resolveMaxFeeConfig(config, amountUsd) {
186
+ if (config.mode === 'flat') {
187
+ return {
188
+ maxFeePercentage: config.valuePercentage,
189
+ };
190
+ }
191
+ const matchedTrancheIndex = getMatchingTrancheIndex(config.tranches, amountUsd, 'USD');
192
+ return {
193
+ maxFeePercentage: config.tranches[matchedTrancheIndex].value,
194
+ };
195
+ }
@@ -0,0 +1,19 @@
1
+ export interface DisplayedFiatAmountInput {
2
+ readonly feePayer: 'MERCHANT' | 'PAYEE';
3
+ readonly signalIntentAmountBaseUnits: string | null;
4
+ readonly conversionRateDecimal: string | null;
5
+ readonly remainingUsdcAmount: string;
6
+ readonly currencyPerUsdRate: string;
7
+ readonly paymentAmountFallback: string;
8
+ }
9
+ /**
10
+ * The exact fiat amount the buyer is instructed to pay, as a decimal string.
11
+ * BYTE-IDENTICAL to checkoutData.ts mapCheckoutPaymentToQuote (lines 336-351):
12
+ * quotedFiatAmount (PAYEE ceil-cents, else null)
13
+ * ?? (validRemaining&Rate ? (remaining*rate).toFixed(2) : paymentAmount)
14
+ * Always returns a string; the API caller passes it to fiatMinorUnitsFromDisplayedAmount,
15
+ * which returns null for an unusable "0"/malformed value so the API fails SAR closed.
16
+ */
17
+ export declare function computeDisplayedFiatAmount(input: DisplayedFiatAmountInput): string;
18
+ /** Parse a 2-decimal (or fewer) positive decimal string to integer minor units. Null if non-positive/malformed/>2dp. */
19
+ export declare function fiatMinorUnitsFromDisplayedAmount(displayed: string): bigint | null;
@@ -0,0 +1,58 @@
1
+ const USDC_SCALE = 1000000n;
2
+ const RATE_SCALE = 10n ** 18n;
3
+ const CENT_SCALE = 100n;
4
+ const QUOTE_DENOMINATOR = USDC_SCALE * RATE_SCALE; // 1e6 * 1e18
5
+ function parseDecimalToScaledUnits(value, decimals) {
6
+ const normalized = value.trim();
7
+ if (!/^\d+(\.\d+)?$/.test(normalized))
8
+ return null;
9
+ const [whole, fraction = ''] = normalized.split('.');
10
+ const base = 10n ** BigInt(decimals);
11
+ const scaledFraction = fraction.slice(0, decimals).padEnd(decimals, '0');
12
+ return BigInt(whole) * base + BigInt(scaledFraction);
13
+ }
14
+ function ceilCentsFromSignalIntent(amountBaseUnits, conversionRateDecimal) {
15
+ if (!/^\d+$/.test(amountBaseUnits))
16
+ return null;
17
+ const units = BigInt(amountBaseUnits);
18
+ const rate = parseDecimalToScaledUnits(conversionRateDecimal, 18);
19
+ if (rate === null || rate <= 0n || units <= 0n)
20
+ return null;
21
+ const numerator = units * rate * CENT_SCALE;
22
+ return (numerator + QUOTE_DENOMINATOR - 1n) / QUOTE_DENOMINATOR; // ceil
23
+ }
24
+ function centsToDecimalString(cents) {
25
+ const whole = cents / CENT_SCALE;
26
+ const fraction = (cents % CENT_SCALE).toString().padStart(2, '0');
27
+ return `${whole.toString()}.${fraction}`;
28
+ }
29
+ /**
30
+ * The exact fiat amount the buyer is instructed to pay, as a decimal string.
31
+ * BYTE-IDENTICAL to checkoutData.ts mapCheckoutPaymentToQuote (lines 336-351):
32
+ * quotedFiatAmount (PAYEE ceil-cents, else null)
33
+ * ?? (validRemaining&Rate ? (remaining*rate).toFixed(2) : paymentAmount)
34
+ * Always returns a string; the API caller passes it to fiatMinorUnitsFromDisplayedAmount,
35
+ * which returns null for an unusable "0"/malformed value so the API fails SAR closed.
36
+ */
37
+ export function computeDisplayedFiatAmount(input) {
38
+ const quotedCents = input.feePayer === 'PAYEE' && input.signalIntentAmountBaseUnits !== null && input.conversionRateDecimal !== null
39
+ ? ceilCentsFromSignalIntent(input.signalIntentAmountBaseUnits, input.conversionRateDecimal)
40
+ : null;
41
+ if (quotedCents !== null)
42
+ return centsToDecimalString(quotedCents);
43
+ const remainingUsdc = Number(input.remainingUsdcAmount);
44
+ const rate = Number(input.currencyPerUsdRate);
45
+ if (Number.isFinite(remainingUsdc) && Number.isFinite(rate) && remainingUsdc > 0 && rate > 0) {
46
+ return (remainingUsdc * rate).toFixed(2);
47
+ }
48
+ return input.paymentAmountFallback;
49
+ }
50
+ /** Parse a 2-decimal (or fewer) positive decimal string to integer minor units. Null if non-positive/malformed/>2dp. */
51
+ export function fiatMinorUnitsFromDisplayedAmount(displayed) {
52
+ const normalized = displayed.trim();
53
+ if (!/^\d+(\.\d{1,2})?$/.test(normalized))
54
+ return null;
55
+ const [whole, fraction = ''] = normalized.split('.');
56
+ const cents = BigInt(whole) * CENT_SCALE + BigInt(fraction.padEnd(2, '0'));
57
+ return cents > 0n ? cents : null;
58
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,10 @@
1
1
  export * from './types.js';
2
2
  export * from './chains.js';
3
- //# sourceMappingURL=index.d.ts.map
3
+ export * from './fees.js';
4
+ export * from './crypto.js';
5
+ export * from './rails.js';
6
+ export * from './buyerTee.js';
7
+ export * from './bitcoin.js';
8
+ export * from './fiatAmount.js';
9
+ export * from './paypalSarPhase.js';
10
+ export * from './paypalSarVerify.js';
package/dist/index.js CHANGED
@@ -1,2 +1,10 @@
1
1
  export * from './types.js';
2
2
  export * from './chains.js';
3
+ export * from './fees.js';
4
+ export * from './crypto.js';
5
+ export * from './rails.js';
6
+ export * from './buyerTee.js';
7
+ export * from './bitcoin.js';
8
+ export * from './fiatAmount.js';
9
+ export * from './paypalSarPhase.js';
10
+ export * from './paypalSarVerify.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Whether a name-mode SAR verify poll outcome is worth retrying. Only a "no receipt found yet" (404)
3
+ * may resolve on a later poll; every other terminal status is deterministic and must fail fast.
4
+ */
5
+ export declare function isRetryableSarPollStatus(statusCode: number): boolean;
@@ -0,0 +1,8 @@
1
+ const NO_RECEIPT_STATUS = 404; // SELLER_TXID_NOT_FOUND — the only status worth re-polling
2
+ /**
3
+ * Whether a name-mode SAR verify poll outcome is worth retrying. Only a "no receipt found yet" (404)
4
+ * may resolve on a later poll; every other terminal status is deterministic and must fail fast.
5
+ */
6
+ export function isRetryableSarPollStatus(statusCode) {
7
+ return statusCode === NO_RECEIPT_STATUS;
8
+ }
@@ -0,0 +1,41 @@
1
+ export type SarAttemptOutcome = {
2
+ kind: 'success';
3
+ proofAttemptId: string;
4
+ } | {
5
+ kind: 'unavailable';
6
+ } | {
7
+ kind: 'error';
8
+ errorCode: string;
9
+ errorMessage: string;
10
+ statusCode: number;
11
+ };
12
+ export type SarVerifyResult = {
13
+ kind: 'success';
14
+ proofAttemptId: string;
15
+ } | {
16
+ kind: 'unavailable';
17
+ } | {
18
+ kind: 'error';
19
+ errorCode: string;
20
+ errorMessage: string;
21
+ } | {
22
+ kind: 'aborted';
23
+ };
24
+ export interface RunPaypalSarNameVerifyDeps {
25
+ /** POST a name-mode attempt. attemptNumber is 1-based and increments per name poll. */
26
+ submitNameAttempt: (attemptNumber: number) => Promise<SarAttemptOutcome>;
27
+ sleep: (ms: number) => Promise<void>;
28
+ /** Re-checked after every await; returning false short-circuits to { kind: 'aborted' }. */
29
+ isMounted: () => boolean;
30
+ now: () => number;
31
+ retryIntervalMs: number;
32
+ retryDurationMs: number;
33
+ }
34
+ /**
35
+ * Name-only PayPal SAR verification.
36
+ * Polls name-mode verify (retrying only on retryable poll statuses until the deadline). Terminal
37
+ * name failures surface directly. Success / unavailable short-circuit. The caller maps the returned
38
+ * result to its own UI side effects; 'aborted' means the component unmounted and the caller should
39
+ * do nothing.
40
+ */
41
+ export declare function runPaypalSarNameVerify(deps: RunPaypalSarNameVerifyDeps): Promise<SarVerifyResult>;
@@ -0,0 +1,31 @@
1
+ import { isRetryableSarPollStatus } from './paypalSarPhase.js';
2
+ /**
3
+ * Name-only PayPal SAR verification.
4
+ * Polls name-mode verify (retrying only on retryable poll statuses until the deadline). Terminal
5
+ * name failures surface directly. Success / unavailable short-circuit. The caller maps the returned
6
+ * result to its own UI side effects; 'aborted' means the component unmounted and the caller should
7
+ * do nothing.
8
+ */
9
+ export async function runPaypalSarNameVerify(deps) {
10
+ const { submitNameAttempt, sleep, isMounted, now, retryIntervalMs, retryDurationMs } = deps;
11
+ const deadline = now() + retryDurationMs;
12
+ let attemptNumber = 1;
13
+ while (true) {
14
+ const outcome = await submitNameAttempt(attemptNumber);
15
+ if (!isMounted())
16
+ return { kind: 'aborted' };
17
+ if (outcome.kind === 'success')
18
+ return { kind: 'success', proofAttemptId: outcome.proofAttemptId };
19
+ if (outcome.kind === 'unavailable')
20
+ return { kind: 'unavailable' };
21
+ const deadlineReached = now() + retryIntervalMs > deadline;
22
+ if (isRetryableSarPollStatus(outcome.statusCode) && !deadlineReached) {
23
+ await sleep(retryIntervalMs);
24
+ if (!isMounted())
25
+ return { kind: 'aborted' };
26
+ attemptNumber += 1;
27
+ continue;
28
+ }
29
+ return { kind: 'error', errorCode: outcome.errorCode, errorMessage: outcome.errorMessage };
30
+ }
31
+ }
@@ -0,0 +1,137 @@
1
+ export declare enum SupportedRail {
2
+ VENMO = "venmo",
3
+ CASHAPP = "cashapp",
4
+ REVOLUT = "revolut",
5
+ ZELLE = "zelle",
6
+ PAYPAL = "paypal",
7
+ WISE = "wise",
8
+ MONZO = "monzo",
9
+ N26 = "n26",
10
+ CHIME = "chime",
11
+ RELAY_1 = "relay_1",
12
+ RELAY_10 = "relay_10",
13
+ RELAY_56 = "relay_56",
14
+ RELAY_137 = "relay_137",
15
+ RELAY_480 = "relay_480",
16
+ RELAY_999 = "relay_999",
17
+ RELAY_8453 = "relay_8453",
18
+ RELAY_42161 = "relay_42161",
19
+ RELAY_8253038 = "relay_8253038",
20
+ RELAY_728126428 = "relay_728126428",
21
+ RELAY_792703809 = "relay_792703809"
22
+ }
23
+ /**
24
+ * Canonical relay rails currently supported by zkp2p/pay.
25
+ *
26
+ * Chain id source reference:
27
+ * https://docs.relay.link/references/api/api_resources/supported-chains
28
+ */
29
+ export declare enum SupportedRelayRail {
30
+ RELAY_1 = "relay_1",
31
+ RELAY_10 = "relay_10",
32
+ RELAY_56 = "relay_56",
33
+ RELAY_137 = "relay_137",
34
+ RELAY_480 = "relay_480",
35
+ RELAY_999 = "relay_999",
36
+ RELAY_8453 = "relay_8453",
37
+ RELAY_42161 = "relay_42161",
38
+ RELAY_8253038 = "relay_8253038",
39
+ RELAY_728126428 = "relay_728126428",
40
+ RELAY_792703809 = "relay_792703809"
41
+ }
42
+ export declare const DEFAULT_FIAT_RAILS: readonly SupportedRail[];
43
+ export declare const SUPPORTED_RAILS: readonly SupportedRail[];
44
+ export declare const SUPPORTED_RELAY_RAILS: readonly SupportedRelayRail[];
45
+ export declare const FIAT_SUPPORTED_RAILS: readonly ["venmo", "cashapp", "revolut", "wise", "zelle", "paypal", "monzo", "n26", "chime"];
46
+ export type FiatSupportedRail = typeof FIAT_SUPPORTED_RAILS[number];
47
+ export declare const PAYMENT_PLATFORM_LABELS: Record<string, string>;
48
+ export declare const FIAT_RAIL_DISPLAY_NAMES: Record<FiatSupportedRail, string>;
49
+ export declare const ORDER_SEARCH_MIN_QUERY_LENGTH = 3;
50
+ export declare function railKeysForQuery(query: string): string[];
51
+ export declare const RELAY_CHAIN_DISPLAY_NAMES: Record<number, string>;
52
+ /**
53
+ * Surfaces crypto-settled orders for token, chain, and "crypto" prefix queries.
54
+ */
55
+ export declare function isCryptoMethodQuery(query: string): boolean;
56
+ /**
57
+ * Normalizes a rail string into a crypto chain id.
58
+ *
59
+ * Supports both:
60
+ * - legacy numeric format: "8453"
61
+ * - canonical relay format: "relay_8453"
62
+ */
63
+ export declare function parseCryptoRailChainId(rail: string | null | undefined): number | null;
64
+ export declare function isSupportedRail(rail: string): rail is SupportedRail;
65
+ export declare function isFiatSupportedRail(rail: string): rail is FiatSupportedRail;
66
+ /** Returns false for rails that remain recognized but are disabled at runtime. */
67
+ export declare function isRailRuntimeEnabled(rail: string | null | undefined): boolean;
68
+ /** Filters out globally disabled runtime rails while preserving caller ordering. */
69
+ export declare function filterRuntimeEnabledRails<T extends string>(rails: readonly T[]): T[];
70
+ /**
71
+ * Replaces the set of globally runtime-disabled fiat rails. Only EXACT base
72
+ * fiat rails are honored (e.g. `venmo`, `paypal`, `zelle`); rail variants
73
+ * (e.g. `zelle-chase`, or an env typo like `paypal-test`), crypto/relay rails,
74
+ * and unknown strings are ignored so a malformed env value cannot silently
75
+ * disable a live rail. Disable is base-granular: disabling `zelle` also
76
+ * disables its variants at check time (isRailRuntimeEnabled normalizes the
77
+ * checked rail to its base). Backend-only: the API applies the `DISABLED_RAILS`
78
+ * env at boot; the frontend never calls this, so its disabled set stays empty.
79
+ */
80
+ export declare function setRuntimeDisabledFiatRails(rails: readonly string[]): void;
81
+ /** Returns the currently runtime-disabled fiat rails (canonical base rails). */
82
+ export declare function getRuntimeDisabledFiatRails(): string[];
83
+ /** Returns true when the rail targets a crypto chain (legacy numeric or relay-prefixed format). */
84
+ export declare function isCryptoRail(rail: string | null | undefined): boolean;
85
+ /** Formats a chain id into the canonical relay rail string: `relay_<chainId>`. */
86
+ export declare function formatRelayRail(chainId: number): string;
87
+ /** Normalizes any crypto rail into canonical relay format (or null for non-crypto rails). */
88
+ export declare function normalizeCryptoRail(rail: string | null | undefined): string | null;
89
+ /** Returns the canonical base fiat rail for base rails and supported variants such as `zelle-chase`. */
90
+ export declare function normalizeFiatRail(rail: string | null | undefined): string | null;
91
+ /**
92
+ * Fiat rails that support Seller Automated Release (SAR). Merchants in
93
+ * EXCLUSIVE_SAR mode may only offer these rails; every other fiat rail is
94
+ * force-disabled in the merchant and admin dashboards.
95
+ */
96
+ export declare const SAR_SUPPORTED_FIAT_RAILS: readonly ["venmo", "cashapp", "wise", "paypal"];
97
+ export type SarSupportedFiatRail = typeof SAR_SUPPORTED_FIAT_RAILS[number];
98
+ /** Returns true when the fiat rail (incl. variants like `zelle-chase`) supports SAR. */
99
+ export declare function isSarSupportedFiatRail(rail: string | null | undefined): boolean;
100
+ /**
101
+ * Returns `rails` with non-SAR fiat rails removed, preserving crypto/relay rails,
102
+ * SAR-supported fiat rails, and original ordering. Used to keep an EXCLUSIVE_SAR
103
+ * merchant's `enabledRails` in sync with their quote preference: non-SAR fiat
104
+ * rails (e.g. `zelle`, `revolut`, incl. variants like `zelle-chase`) are dropped
105
+ * while crypto rails and SAR rails (venmo/cashapp/wise/paypal) are kept.
106
+ */
107
+ export declare function filterRailsForExclusiveSar(rails: readonly string[]): string[];
108
+ /** Canonical Zelle paymentMethodId -> attestation-service actionType suffix. */
109
+ export declare const ZELLE_METHOD_ACTION_SUFFIX: Record<string, string>;
110
+ /** Strict create-boundary set for Zelle method ids and transitional variant rails. */
111
+ export declare const ZELLE_PAYMENT_METHOD_IDS: ReadonlySet<string>;
112
+ /** Bank-qualified buyer TEE action types accepted for generic Zelle proofs. */
113
+ export declare const ZELLE_BUYER_TEE_ACTION_TYPES: ReadonlySet<string>;
114
+ export type AttestationRoute = {
115
+ readonly platform: string;
116
+ readonly actionType: string;
117
+ };
118
+ /**
119
+ * Resolves the attestation-service verifier route (`platform` + `actionType`)
120
+ * for a persisted `payment.rail` value.
121
+ *
122
+ * Zelle variants map to the new generic `zelle` platform with a bank-qualified
123
+ * actionType. Legacy bank-specific verifier routes are drain-only and should
124
+ * stay outside shared core. Non-Zelle fiat rails use the base rail as platform
125
+ * and `transfer_{rail}` as actionType. Unknown / crypto rails fall back to the
126
+ * raw trimmed rail so callers still surface a verifier mismatch via the
127
+ * attestation response rather than silently mis-routing.
128
+ */
129
+ export declare function resolveAttestationRoute(rail: string | null | undefined): AttestationRoute;
130
+ /** Canonical generic Zelle on-chain method hash: keccak256("zelle"), lowercase. */
131
+ export declare const GENERIC_ZELLE_PAYMENT_METHOD_HASH = "0xf752c7d19698ecb0bb8988abf9b9a53a4c3657f3bc8850a6fb59fdf3e3ce8cd3";
132
+ /** Case-insensitive check that a hash is the canonical generic Zelle on-chain method hash. */
133
+ export declare function isGenericZellePaymentMethodHash(hash: string | null | undefined): boolean;
134
+ /** True when the rail is Zelle (generic or a bank variant). */
135
+ export declare function isZelleRail(rail: string | null | undefined): boolean;
136
+ /** Formats a rail value for human-readable UI labels. */
137
+ export declare function getRailDisplayName(rail: string | null | undefined): string;