@zkp2p/pay-shared 0.0.1 → 1.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
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,6 @@
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 './bitcoin.js';
package/dist/index.js CHANGED
@@ -1,2 +1,6 @@
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 './bitcoin.js';
@@ -0,0 +1,61 @@
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
+ /**
46
+ * Normalizes a rail string into a crypto chain id.
47
+ *
48
+ * Supports both:
49
+ * - legacy numeric format: "8453"
50
+ * - canonical relay format: "relay_8453"
51
+ */
52
+ export declare function parseCryptoRailChainId(rail: string | null | undefined): number | null;
53
+ export declare function isSupportedRail(rail: string): rail is SupportedRail;
54
+ /** Returns true when the rail targets a crypto chain (legacy numeric or relay-prefixed format). */
55
+ export declare function isCryptoRail(rail: string | null | undefined): boolean;
56
+ /** Formats a chain id into the canonical relay rail string: `relay_<chainId>`. */
57
+ export declare function formatRelayRail(chainId: number): string;
58
+ /** Normalizes any crypto rail into canonical relay format (or null for non-crypto rails). */
59
+ export declare function normalizeCryptoRail(rail: string | null | undefined): string | null;
60
+ /** Formats a rail value for human-readable UI labels. */
61
+ export declare function getRailDisplayName(rail: string | null | undefined): string;
package/dist/rails.js ADDED
@@ -0,0 +1,144 @@
1
+ const RELAY_RAIL_PREFIX = 'relay_';
2
+ export var SupportedRail;
3
+ (function (SupportedRail) {
4
+ SupportedRail["VENMO"] = "venmo";
5
+ SupportedRail["CASHAPP"] = "cashapp";
6
+ SupportedRail["REVOLUT"] = "revolut";
7
+ SupportedRail["ZELLE"] = "zelle";
8
+ SupportedRail["PAYPAL"] = "paypal";
9
+ SupportedRail["WISE"] = "wise";
10
+ SupportedRail["MONZO"] = "monzo";
11
+ SupportedRail["N26"] = "n26";
12
+ SupportedRail["CHIME"] = "chime";
13
+ SupportedRail["RELAY_1"] = "relay_1";
14
+ SupportedRail["RELAY_10"] = "relay_10";
15
+ SupportedRail["RELAY_56"] = "relay_56";
16
+ SupportedRail["RELAY_137"] = "relay_137";
17
+ SupportedRail["RELAY_480"] = "relay_480";
18
+ SupportedRail["RELAY_999"] = "relay_999";
19
+ SupportedRail["RELAY_8453"] = "relay_8453";
20
+ SupportedRail["RELAY_42161"] = "relay_42161";
21
+ SupportedRail["RELAY_8253038"] = "relay_8253038";
22
+ SupportedRail["RELAY_728126428"] = "relay_728126428";
23
+ SupportedRail["RELAY_792703809"] = "relay_792703809";
24
+ })(SupportedRail || (SupportedRail = {}));
25
+ /**
26
+ * Canonical relay rails currently supported by zkp2p/pay.
27
+ *
28
+ * Chain id source reference:
29
+ * https://docs.relay.link/references/api/api_resources/supported-chains
30
+ */
31
+ export var SupportedRelayRail;
32
+ (function (SupportedRelayRail) {
33
+ SupportedRelayRail["RELAY_1"] = "relay_1";
34
+ SupportedRelayRail["RELAY_10"] = "relay_10";
35
+ SupportedRelayRail["RELAY_56"] = "relay_56";
36
+ SupportedRelayRail["RELAY_137"] = "relay_137";
37
+ SupportedRelayRail["RELAY_480"] = "relay_480";
38
+ SupportedRelayRail["RELAY_999"] = "relay_999";
39
+ SupportedRelayRail["RELAY_8453"] = "relay_8453";
40
+ SupportedRelayRail["RELAY_42161"] = "relay_42161";
41
+ SupportedRelayRail["RELAY_8253038"] = "relay_8253038";
42
+ SupportedRelayRail["RELAY_728126428"] = "relay_728126428";
43
+ SupportedRelayRail["RELAY_792703809"] = "relay_792703809";
44
+ })(SupportedRelayRail || (SupportedRelayRail = {}));
45
+ export const DEFAULT_FIAT_RAILS = [
46
+ SupportedRail.VENMO,
47
+ SupportedRail.CASHAPP,
48
+ SupportedRail.PAYPAL,
49
+ SupportedRail.WISE,
50
+ ];
51
+ export const SUPPORTED_RAILS = Object.values(SupportedRail);
52
+ export const SUPPORTED_RELAY_RAILS = Object.values(SupportedRelayRail);
53
+ const SUPPORTED_RAIL_SET = new Set(SUPPORTED_RAILS);
54
+ const FIAT_RAIL_DISPLAY_NAMES = {
55
+ [SupportedRail.VENMO]: 'Venmo',
56
+ [SupportedRail.CASHAPP]: 'Cash App',
57
+ [SupportedRail.REVOLUT]: 'Revolut',
58
+ [SupportedRail.ZELLE]: 'Zelle',
59
+ [SupportedRail.PAYPAL]: 'PayPal',
60
+ [SupportedRail.WISE]: 'Wise',
61
+ [SupportedRail.MONZO]: 'Monzo',
62
+ [SupportedRail.N26]: 'N26',
63
+ [SupportedRail.CHIME]: 'Chime',
64
+ };
65
+ const RELAY_CHAIN_DISPLAY_NAMES = {
66
+ 1: 'Ethereum',
67
+ 10: 'Optimism',
68
+ 56: 'BNB Smart Chain',
69
+ 137: 'Polygon',
70
+ 480: 'World Chain',
71
+ 999: 'Hyperliquid',
72
+ 8453: 'Base',
73
+ 42161: 'Arbitrum',
74
+ 8253038: 'Bitcoin',
75
+ 728126428: 'Tron',
76
+ 792703809: 'Solana',
77
+ };
78
+ /**
79
+ * Normalizes a rail string into a crypto chain id.
80
+ *
81
+ * Supports both:
82
+ * - legacy numeric format: "8453"
83
+ * - canonical relay format: "relay_8453"
84
+ */
85
+ export function parseCryptoRailChainId(rail) {
86
+ if (typeof rail !== 'string') {
87
+ return null;
88
+ }
89
+ const trimmed = rail.trim();
90
+ if (trimmed === '') {
91
+ return null;
92
+ }
93
+ const numericMatch = /^\d+$/.exec(trimmed);
94
+ if (numericMatch !== null) {
95
+ const parsed = Number(trimmed);
96
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
97
+ }
98
+ const prefixedMatch = /^relay_(\d+)$/i.exec(trimmed);
99
+ if (prefixedMatch === null) {
100
+ return null;
101
+ }
102
+ const parsed = Number(prefixedMatch[1]);
103
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
104
+ }
105
+ export function isSupportedRail(rail) {
106
+ return SUPPORTED_RAIL_SET.has(rail);
107
+ }
108
+ /** Returns true when the rail targets a crypto chain (legacy numeric or relay-prefixed format). */
109
+ export function isCryptoRail(rail) {
110
+ return parseCryptoRailChainId(rail) !== null;
111
+ }
112
+ /** Formats a chain id into the canonical relay rail string: `relay_<chainId>`. */
113
+ export function formatRelayRail(chainId) {
114
+ if (!Number.isSafeInteger(chainId) || chainId <= 0) {
115
+ throw new Error(`Invalid chain id for relay rail: ${chainId}`);
116
+ }
117
+ return `${RELAY_RAIL_PREFIX}${chainId}`;
118
+ }
119
+ /** Normalizes any crypto rail into canonical relay format (or null for non-crypto rails). */
120
+ export function normalizeCryptoRail(rail) {
121
+ const chainId = parseCryptoRailChainId(rail);
122
+ return chainId === null ? null : formatRelayRail(chainId);
123
+ }
124
+ /** Formats a rail value for human-readable UI labels. */
125
+ export function getRailDisplayName(rail) {
126
+ if (typeof rail !== 'string') {
127
+ return 'Unknown';
128
+ }
129
+ const trimmed = rail.trim();
130
+ if (trimmed === '') {
131
+ return 'Unknown';
132
+ }
133
+ const normalized = trimmed.toLowerCase();
134
+ const fiatDisplayName = FIAT_RAIL_DISPLAY_NAMES[normalized];
135
+ if (fiatDisplayName) {
136
+ return fiatDisplayName;
137
+ }
138
+ const chainId = parseCryptoRailChainId(trimmed);
139
+ if (chainId !== null) {
140
+ const chainName = RELAY_CHAIN_DISPLAY_NAMES[chainId] ?? `Chain ${chainId}`;
141
+ return `Relay (${chainName})`;
142
+ }
143
+ return trimmed.toUpperCase();
144
+ }