@zkp2p/pay-shared 2.0.0 → 3.1.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@2.0.0
12
+ npm install @zkp2p/pay-shared@3.1.0
13
13
  ```
14
14
 
15
15
  ## Published artifacts
@@ -0,0 +1,5 @@
1
+ export declare function sha256(value: Uint8Array): Uint8Array;
2
+ export declare function doubleSha256(value: Uint8Array): Uint8Array;
3
+ export declare function decodeBase58(value: string): Uint8Array | null;
4
+ export declare function encodeBase58(value: Uint8Array): string;
5
+ export declare function hasBase58CheckChecksum(value: string, version: number): boolean;
@@ -0,0 +1,126 @@
1
+ const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
2
+ const SHA256_CONSTANTS = new Uint32Array([
3
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
4
+ 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
5
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
6
+ 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
7
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
8
+ 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
9
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
10
+ 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
11
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
12
+ 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
13
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
14
+ 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
15
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
16
+ 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
17
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
18
+ 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
19
+ ]);
20
+ function rotateRight(value, shift) {
21
+ return (value >>> shift) | (value << (32 - shift));
22
+ }
23
+ export function sha256(value) {
24
+ const paddedLength = Math.ceil((value.length + 9) / 64) * 64;
25
+ const message = new Uint8Array(paddedLength);
26
+ message.set(value);
27
+ message[value.length] = 0x80;
28
+ const bitLength = BigInt(value.length) * 8n;
29
+ const view = new DataView(message.buffer);
30
+ view.setUint32(paddedLength - 8, Number(bitLength >> 32n), false);
31
+ view.setUint32(paddedLength - 4, Number(bitLength & 0xffffffffn), false);
32
+ const hash = new Uint32Array([
33
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
34
+ 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
35
+ ]);
36
+ const words = new Uint32Array(64);
37
+ for (let offset = 0; offset < paddedLength; offset += 64) {
38
+ for (let index = 0; index < 16; index += 1) {
39
+ words[index] = view.getUint32(offset + (index * 4), false);
40
+ }
41
+ for (let index = 16; index < 64; index += 1) {
42
+ const previous15 = words[index - 15];
43
+ const previous2 = words[index - 2];
44
+ const sigma0 = rotateRight(previous15, 7) ^ rotateRight(previous15, 18) ^ (previous15 >>> 3);
45
+ const sigma1 = rotateRight(previous2, 17) ^ rotateRight(previous2, 19) ^ (previous2 >>> 10);
46
+ words[index] = (words[index - 16] + sigma0 + words[index - 7] + sigma1) >>> 0;
47
+ }
48
+ let [a, b, c, d, e, f, g, h] = hash;
49
+ for (let index = 0; index < 64; index += 1) {
50
+ const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
51
+ const choice = (e & f) ^ (~e & g);
52
+ const temporary1 = (h + sum1 + choice + SHA256_CONSTANTS[index] + words[index]) >>> 0;
53
+ const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
54
+ const majority = (a & b) ^ (a & c) ^ (b & c);
55
+ const temporary2 = (sum0 + majority) >>> 0;
56
+ h = g;
57
+ g = f;
58
+ f = e;
59
+ e = (d + temporary1) >>> 0;
60
+ d = c;
61
+ c = b;
62
+ b = a;
63
+ a = (temporary1 + temporary2) >>> 0;
64
+ }
65
+ hash[0] = (hash[0] + a) >>> 0;
66
+ hash[1] = (hash[1] + b) >>> 0;
67
+ hash[2] = (hash[2] + c) >>> 0;
68
+ hash[3] = (hash[3] + d) >>> 0;
69
+ hash[4] = (hash[4] + e) >>> 0;
70
+ hash[5] = (hash[5] + f) >>> 0;
71
+ hash[6] = (hash[6] + g) >>> 0;
72
+ hash[7] = (hash[7] + h) >>> 0;
73
+ }
74
+ const digest = new Uint8Array(32);
75
+ const digestView = new DataView(digest.buffer);
76
+ hash.forEach((word, index) => digestView.setUint32(index * 4, word, false));
77
+ return digest;
78
+ }
79
+ export function doubleSha256(value) {
80
+ return sha256(sha256(value));
81
+ }
82
+ export function decodeBase58(value) {
83
+ let decodedValue = 0n;
84
+ for (const character of value) {
85
+ const digit = BASE58_ALPHABET.indexOf(character);
86
+ if (digit === -1) {
87
+ return null;
88
+ }
89
+ decodedValue = (decodedValue * 58n) + BigInt(digit);
90
+ }
91
+ const bytes = [];
92
+ while (decodedValue > 0n) {
93
+ bytes.push(Number(decodedValue & 0xffn));
94
+ decodedValue >>= 8n;
95
+ }
96
+ bytes.reverse();
97
+ const leadingZeroes = value.match(/^1*/)?.[0].length ?? 0;
98
+ const decoded = new Uint8Array(leadingZeroes + bytes.length);
99
+ decoded.set(bytes, leadingZeroes);
100
+ return decoded;
101
+ }
102
+ export function encodeBase58(value) {
103
+ let encodedValue = 0n;
104
+ for (const byte of value) {
105
+ encodedValue = (encodedValue << 8n) + BigInt(byte);
106
+ }
107
+ let encoded = '';
108
+ while (encodedValue > 0n) {
109
+ encoded = BASE58_ALPHABET[Number(encodedValue % 58n)] + encoded;
110
+ encodedValue /= 58n;
111
+ }
112
+ let leadingZeroes = 0;
113
+ while (leadingZeroes < value.length && value[leadingZeroes] === 0) {
114
+ leadingZeroes += 1;
115
+ }
116
+ return '1'.repeat(leadingZeroes) + encoded;
117
+ }
118
+ export function hasBase58CheckChecksum(value, version) {
119
+ const decoded = decodeBase58(value);
120
+ if (decoded === null || decoded.length !== 25 || decoded[0] !== version) {
121
+ return false;
122
+ }
123
+ const payload = decoded.subarray(0, 21);
124
+ const expectedChecksum = doubleSha256(payload).subarray(0, 4);
125
+ return decoded.subarray(21).every((byte, index) => byte === expectedChecksum[index]);
126
+ }
@@ -0,0 +1,2 @@
1
+ export declare function isValidSolanaAddress(value: string): boolean;
2
+ export declare function isValidTronAddress(value: string): boolean;
@@ -0,0 +1,14 @@
1
+ import { decodeBase58, encodeBase58, hasBase58CheckChecksum, } from './addressEncoding.js';
2
+ export function isValidSolanaAddress(value) {
3
+ if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(value)) {
4
+ return false;
5
+ }
6
+ const decoded = decodeBase58(value);
7
+ return decoded !== null
8
+ && decoded.length === 32
9
+ && encodeBase58(decoded) === value;
10
+ }
11
+ export function isValidTronAddress(value) {
12
+ return /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(value)
13
+ && hasBase58CheckChecksum(value, 0x41);
14
+ }
package/dist/bitcoin.js CHANGED
@@ -1,13 +1,79 @@
1
- const BITCOIN_BASE58_ADDRESS_REGEX = /^[13][1-9A-HJ-NP-Za-km-z]{25,34}$/;
2
- const BITCOIN_BECH32_ADDRESS_REGEX = /^(bc1)[ac-hj-np-z02-9]{11,71}$/i;
1
+ import { hasBase58CheckChecksum } from './addressEncoding.js';
2
+ const BECH32_ALPHABET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
3
+ const BECH32_CHECKSUM = 1;
4
+ const BECH32M_CHECKSUM = 0x2bc830a3;
5
+ function calculatePolymod(values) {
6
+ const generators = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
7
+ let checksum = 1;
8
+ for (const value of values) {
9
+ const highBits = checksum >>> 25;
10
+ checksum = (((checksum & 0x1ffffff) << 5) ^ value) >>> 0;
11
+ for (let index = 0; index < generators.length; index += 1) {
12
+ if (((highBits >>> index) & 1) !== 0) {
13
+ checksum = (checksum ^ generators[index]) >>> 0;
14
+ }
15
+ }
16
+ }
17
+ return checksum;
18
+ }
19
+ function convertWitnessProgram(values) {
20
+ const decoded = [];
21
+ let accumulator = 0;
22
+ let bits = 0;
23
+ for (const value of values) {
24
+ accumulator = (accumulator << 5) | value;
25
+ bits += 5;
26
+ while (bits >= 8) {
27
+ bits -= 8;
28
+ decoded.push((accumulator >>> bits) & 0xff);
29
+ }
30
+ }
31
+ if (bits >= 5 || ((accumulator << (8 - bits)) & 0xff) !== 0) {
32
+ return null;
33
+ }
34
+ return Uint8Array.from(decoded);
35
+ }
36
+ function isValidBech32Address(value) {
37
+ if (value.length > 90 || (value !== value.toLowerCase() && value !== value.toUpperCase())) {
38
+ return false;
39
+ }
40
+ const normalized = value.toLowerCase();
41
+ const separatorIndex = normalized.lastIndexOf('1');
42
+ if (separatorIndex !== 2 || normalized.slice(0, separatorIndex) !== 'bc') {
43
+ return false;
44
+ }
45
+ const data = [...normalized.slice(separatorIndex + 1)]
46
+ .map((character) => BECH32_ALPHABET.indexOf(character));
47
+ if (data.length < 7 || data.some((item) => item === -1)) {
48
+ return false;
49
+ }
50
+ const hrpValues = [
51
+ ...[...'bc'].map((character) => character.charCodeAt(0) >>> 5),
52
+ 0,
53
+ ...[...'bc'].map((character) => character.charCodeAt(0) & 31),
54
+ ];
55
+ const witnessVersion = data[0];
56
+ const polymod = calculatePolymod([...hrpValues, ...data]);
57
+ if (witnessVersion > 16
58
+ || (witnessVersion === 0 && polymod !== BECH32_CHECKSUM)
59
+ || (witnessVersion > 0 && polymod !== BECH32M_CHECKSUM)) {
60
+ return false;
61
+ }
62
+ const witnessProgram = convertWitnessProgram(data.slice(1, -6));
63
+ if (witnessProgram === null || witnessProgram.length < 2 || witnessProgram.length > 40) {
64
+ return false;
65
+ }
66
+ return witnessVersion !== 0 || witnessProgram.length === 20 || witnessProgram.length === 32;
67
+ }
3
68
  export function isValidBitcoinAddress(value) {
4
69
  const trimmed = value.trim();
5
70
  if (!trimmed) {
6
71
  return false;
7
72
  }
8
73
  if (trimmed.toLowerCase().startsWith('bc1')) {
9
- const isUniformCase = trimmed === trimmed.toLowerCase() || trimmed === trimmed.toUpperCase();
10
- return isUniformCase && BITCOIN_BECH32_ADDRESS_REGEX.test(trimmed);
74
+ return isValidBech32Address(trimmed);
11
75
  }
12
- return BITCOIN_BASE58_ADDRESS_REGEX.test(trimmed);
76
+ return /^[13][1-9A-HJ-NP-Za-km-z]{25,34}$/.test(trimmed)
77
+ && (hasBase58CheckChecksum(trimmed, 0x00)
78
+ || hasBase58CheckChecksum(trimmed, 0x05));
13
79
  }
package/dist/index.d.ts CHANGED
@@ -5,6 +5,9 @@ export * from './crypto.js';
5
5
  export * from './rails.js';
6
6
  export * from './buyerTee.js';
7
7
  export * from './bitcoin.js';
8
+ export * from './addressValidators.js';
8
9
  export * from './fiatAmount.js';
9
10
  export * from './paypalSarPhase.js';
10
11
  export * from './paypalSarVerify.js';
12
+ export * from './payeeFeeCap.js';
13
+ export * from './onboarding.js';
package/dist/index.js CHANGED
@@ -5,6 +5,9 @@ export * from './crypto.js';
5
5
  export * from './rails.js';
6
6
  export * from './buyerTee.js';
7
7
  export * from './bitcoin.js';
8
+ export * from './addressValidators.js';
8
9
  export * from './fiatAmount.js';
9
10
  export * from './paypalSarPhase.js';
10
11
  export * from './paypalSarVerify.js';
12
+ export * from './payeeFeeCap.js';
13
+ export * from './onboarding.js';
@@ -0,0 +1,39 @@
1
+ import type { MerchantIntegrationPathValue, OnboardingStepState, OnboardingStepVerifiedByValue } from './types.js';
2
+ export type OnboardingFacts = {
3
+ hasName: boolean;
4
+ hasLogo: boolean;
5
+ hasIndustry: boolean;
6
+ hasIntegrationPath: boolean;
7
+ hasWebhook: boolean;
8
+ hasSandboxOrder: boolean;
9
+ hasSandboxFulfilledOrder: boolean;
10
+ hasLiveOrder: boolean;
11
+ hasLiveFulfilledOrder: boolean;
12
+ manualSteps: readonly string[];
13
+ };
14
+ export declare const OnboardingStepId: {
15
+ readonly PROFILE: "profile";
16
+ readonly INDUSTRY: "industry";
17
+ readonly PATH: "path";
18
+ readonly RAILS: "rails";
19
+ readonly INTEGRATION: "integration";
20
+ readonly WEBHOOK: "webhook";
21
+ readonly GO_LIVE: "goLive";
22
+ readonly DOWNLOAD_PLUGIN: "downloadPlugin";
23
+ readonly PLUGIN_API_KEY: "pluginApiKey";
24
+ readonly TEST_ORDER: "testOrder";
25
+ readonly FIRST_QR: "firstQr";
26
+ readonly LINK_BOT: "linkBot";
27
+ readonly REQUEST_SETUP: "requestSetup";
28
+ };
29
+ export type OnboardingStepIdValue = typeof OnboardingStepId[keyof typeof OnboardingStepId];
30
+ export type OnboardingStepRegistryEntry = {
31
+ readonly id: OnboardingStepIdValue;
32
+ readonly paths: readonly MerchantIntegrationPathValue[] | 'ALL';
33
+ readonly verifiedBy: OnboardingStepVerifiedByValue;
34
+ readonly inChecklist: boolean;
35
+ readonly impliesPrevious: boolean;
36
+ };
37
+ export declare const ONBOARDING_STEP_REGISTRY: readonly OnboardingStepRegistryEntry[];
38
+ export declare function resolveOnboardingSteps(facts: OnboardingFacts, path: MerchantIntegrationPathValue | null): OnboardingStepState[];
39
+ export declare function isOnboardingComplete(steps: readonly OnboardingStepState[]): boolean;
@@ -0,0 +1,150 @@
1
+ import { MerchantIntegrationPath, OnboardingStepStatus, OnboardingStepVerifiedBy, } from './types.js';
2
+ export const OnboardingStepId = {
3
+ PROFILE: 'profile',
4
+ INDUSTRY: 'industry',
5
+ PATH: 'path',
6
+ RAILS: 'rails',
7
+ INTEGRATION: 'integration',
8
+ WEBHOOK: 'webhook',
9
+ GO_LIVE: 'goLive',
10
+ DOWNLOAD_PLUGIN: 'downloadPlugin',
11
+ PLUGIN_API_KEY: 'pluginApiKey',
12
+ TEST_ORDER: 'testOrder',
13
+ FIRST_QR: 'firstQr',
14
+ LINK_BOT: 'linkBot',
15
+ REQUEST_SETUP: 'requestSetup',
16
+ };
17
+ export const ONBOARDING_STEP_REGISTRY = [
18
+ { id: OnboardingStepId.PROFILE, paths: 'ALL', verifiedBy: OnboardingStepVerifiedBy.AUTO, inChecklist: true, impliesPrevious: true },
19
+ { id: OnboardingStepId.INDUSTRY, paths: 'ALL', verifiedBy: OnboardingStepVerifiedBy.AUTO, inChecklist: true, impliesPrevious: true },
20
+ {
21
+ id: OnboardingStepId.RAILS,
22
+ paths: 'ALL',
23
+ verifiedBy: OnboardingStepVerifiedBy.AUTO,
24
+ inChecklist: false,
25
+ // This step is unconditionally satisfied and carries no information, so allowing
26
+ // it to imply earlier steps would mark every merchant's onboarding complete.
27
+ impliesPrevious: false,
28
+ },
29
+ { id: OnboardingStepId.PATH, paths: 'ALL', verifiedBy: OnboardingStepVerifiedBy.AUTO, inChecklist: true, impliesPrevious: true },
30
+ {
31
+ id: OnboardingStepId.INTEGRATION,
32
+ paths: [MerchantIntegrationPath.CUSTOM_API],
33
+ verifiedBy: OnboardingStepVerifiedBy.AUTO,
34
+ inChecklist: true,
35
+ impliesPrevious: true,
36
+ },
37
+ {
38
+ id: OnboardingStepId.WEBHOOK,
39
+ paths: [MerchantIntegrationPath.CUSTOM_API],
40
+ verifiedBy: OnboardingStepVerifiedBy.AUTO,
41
+ inChecklist: true,
42
+ impliesPrevious: false,
43
+ },
44
+ {
45
+ id: OnboardingStepId.LINK_BOT,
46
+ paths: [MerchantIntegrationPath.TELEGRAM],
47
+ verifiedBy: OnboardingStepVerifiedBy.AUTO,
48
+ inChecklist: true,
49
+ impliesPrevious: true,
50
+ },
51
+ {
52
+ id: OnboardingStepId.GO_LIVE,
53
+ paths: [MerchantIntegrationPath.CUSTOM_API, MerchantIntegrationPath.TELEGRAM],
54
+ verifiedBy: OnboardingStepVerifiedBy.AUTO,
55
+ inChecklist: true,
56
+ impliesPrevious: true,
57
+ },
58
+ {
59
+ id: OnboardingStepId.DOWNLOAD_PLUGIN,
60
+ paths: [MerchantIntegrationPath.WOOCOMMERCE],
61
+ verifiedBy: OnboardingStepVerifiedBy.MANUAL,
62
+ inChecklist: true,
63
+ impliesPrevious: true,
64
+ },
65
+ {
66
+ id: OnboardingStepId.PLUGIN_API_KEY,
67
+ paths: [MerchantIntegrationPath.WOOCOMMERCE],
68
+ verifiedBy: OnboardingStepVerifiedBy.MANUAL,
69
+ inChecklist: true,
70
+ impliesPrevious: true,
71
+ },
72
+ {
73
+ id: OnboardingStepId.TEST_ORDER,
74
+ paths: [MerchantIntegrationPath.WOOCOMMERCE],
75
+ verifiedBy: OnboardingStepVerifiedBy.AUTO,
76
+ inChecklist: true,
77
+ impliesPrevious: true,
78
+ },
79
+ {
80
+ id: OnboardingStepId.GO_LIVE,
81
+ paths: [MerchantIntegrationPath.WOOCOMMERCE],
82
+ verifiedBy: OnboardingStepVerifiedBy.AUTO,
83
+ inChecklist: true,
84
+ impliesPrevious: true,
85
+ },
86
+ {
87
+ id: OnboardingStepId.FIRST_QR,
88
+ paths: [MerchantIntegrationPath.IN_PERSON],
89
+ verifiedBy: OnboardingStepVerifiedBy.AUTO,
90
+ inChecklist: true,
91
+ // In-person merchants only need to show a Scan to Pay QR. Waiting for a real
92
+ // customer payment would leave unopened shops stuck in onboarding.
93
+ impliesPrevious: true,
94
+ },
95
+ {
96
+ id: OnboardingStepId.REQUEST_SETUP,
97
+ paths: [MerchantIntegrationPath.SHOPIFY],
98
+ verifiedBy: OnboardingStepVerifiedBy.MANUAL,
99
+ inChecklist: true,
100
+ impliesPrevious: true,
101
+ },
102
+ ];
103
+ const autoStepPredicates = {
104
+ [OnboardingStepId.PROFILE]: (facts) => facts.hasName && facts.hasLogo,
105
+ [OnboardingStepId.INDUSTRY]: (facts) => facts.hasIndustry,
106
+ [OnboardingStepId.PATH]: (facts) => facts.hasIntegrationPath,
107
+ [OnboardingStepId.RAILS]: () => true,
108
+ [OnboardingStepId.INTEGRATION]: (facts) => facts.hasSandboxFulfilledOrder,
109
+ [OnboardingStepId.WEBHOOK]: (facts) => facts.hasWebhook,
110
+ [OnboardingStepId.TEST_ORDER]: (facts) => facts.hasSandboxFulfilledOrder,
111
+ [OnboardingStepId.LINK_BOT]: (facts) => facts.hasSandboxFulfilledOrder,
112
+ [OnboardingStepId.GO_LIVE]: (facts) => facts.hasLiveFulfilledOrder,
113
+ [OnboardingStepId.FIRST_QR]: (facts) => facts.hasSandboxOrder,
114
+ };
115
+ export function resolveOnboardingSteps(facts, path) {
116
+ const entries = ONBOARDING_STEP_REGISTRY.filter((entry) => entry.paths === 'ALL' || (path !== null && entry.paths.includes(path)));
117
+ // Load-bearing override: merchants already taking payments must remain free of
118
+ // the onboarding checklist, even when they have no selected integration path.
119
+ if (facts.hasLiveFulfilledOrder) {
120
+ return entries.map((entry) => ({
121
+ id: entry.id,
122
+ status: OnboardingStepStatus.DONE,
123
+ verifiedBy: entry.verifiedBy,
124
+ }));
125
+ }
126
+ const steps = entries.map((entry) => {
127
+ const isDone = entry.verifiedBy === OnboardingStepVerifiedBy.MANUAL
128
+ ? facts.manualSteps.includes(entry.id)
129
+ : autoStepPredicates[entry.id]?.(facts) ?? false;
130
+ return {
131
+ id: entry.id,
132
+ status: isDone ? OnboardingStepStatus.DONE : OnboardingStepStatus.TODO,
133
+ verifiedBy: entry.verifiedBy,
134
+ };
135
+ });
136
+ let laterStepIsDone = false;
137
+ for (let index = steps.length - 1; index >= 0; index -= 1) {
138
+ if (laterStepIsDone) {
139
+ steps[index] = { ...steps[index], status: OnboardingStepStatus.DONE };
140
+ }
141
+ else if (steps[index].status === OnboardingStepStatus.DONE
142
+ && entries[index].impliesPrevious) {
143
+ laterStepIsDone = true;
144
+ }
145
+ }
146
+ return steps;
147
+ }
148
+ export function isOnboardingComplete(steps) {
149
+ return steps.every((step) => step.status === OnboardingStepStatus.DONE);
150
+ }
@@ -0,0 +1,21 @@
1
+ export type PayeeFeeEntry = {
2
+ readonly recipient: string;
3
+ readonly feeBps: number;
4
+ };
5
+ export type PayeeReferralFeeCapacityResult = {
6
+ readonly referralFees: PayeeFeeEntry[];
7
+ readonly configuredPlatformReferralFeeBps: number;
8
+ readonly adjustedPlatformReferralFeeBps: number;
9
+ readonly otherReferralFeeAmount: string;
10
+ readonly platformFeeCapacityAmount: string;
11
+ readonly insufficientCapacity: boolean;
12
+ readonly requiredNetCovered: boolean;
13
+ };
14
+ export declare function resolvePayeeReferralFeeCapacity(input: {
15
+ readonly grossSignalAmount: string;
16
+ readonly quotedTokenOutput: string;
17
+ readonly requiredNetAmount: string;
18
+ readonly platformRecipient: string;
19
+ readonly referralFees: readonly PayeeFeeEntry[];
20
+ }): PayeeReferralFeeCapacityResult;
21
+ export declare function computeNetAfterPreciseFees(grossAmount: string, feeRates18dec: readonly string[]): string;
@@ -0,0 +1,105 @@
1
+ const BPS_DENOMINATOR = 10000n;
2
+ const PRECISE_FEE_DENOMINATOR = 10n ** 18n;
3
+ const UNSIGNED_INTEGER_PATTERN = /^\d+$/;
4
+ export function resolvePayeeReferralFeeCapacity(input) {
5
+ const grossSignalAmount = parsePositiveUnits(input.grossSignalAmount, 'gross signal amount');
6
+ const quotedTokenOutput = parsePositiveUnits(input.quotedTokenOutput, 'quoted token output');
7
+ const requiredNetAmount = parsePositiveUnits(input.requiredNetAmount, 'required net amount');
8
+ if (quotedTokenOutput > grossSignalAmount) {
9
+ throw new RangeError('Quoted token output cannot exceed gross signal amount');
10
+ }
11
+ const platformRecipient = input.platformRecipient.trim().toLowerCase();
12
+ if (platformRecipient === '') {
13
+ throw new RangeError('Platform recipient is required');
14
+ }
15
+ const platformEntries = input.referralFees.filter((entry) => entry.recipient.trim().toLowerCase() === platformRecipient);
16
+ if (platformEntries.length > 1) {
17
+ throw new RangeError('Duplicate platform referral fee entries');
18
+ }
19
+ input.referralFees.forEach(validateFeeEntry);
20
+ const configuredPlatformReferralFeeBps = platformEntries[0]?.feeBps ?? 0;
21
+ const otherReferralFeeAmount = input.referralFees.reduce((total, entry) => {
22
+ if (entry.recipient.trim().toLowerCase() === platformRecipient) {
23
+ return total;
24
+ }
25
+ return total + computeBpsFee(grossSignalAmount, entry.feeBps);
26
+ }, 0n);
27
+ const platformFeeCapacityAmount = quotedTokenOutput - requiredNetAmount - otherReferralFeeAmount;
28
+ const insufficientCapacity = platformFeeCapacityAmount < 0n;
29
+ const adjustedPlatformReferralFeeBps = insufficientCapacity
30
+ ? 0
31
+ : findLargestFittingBps(grossSignalAmount, platformFeeCapacityAmount, configuredPlatformReferralFeeBps);
32
+ const adjustedPlatformFeeAmount = computeBpsFee(grossSignalAmount, adjustedPlatformReferralFeeBps);
33
+ const requiredNetCovered = quotedTokenOutput
34
+ - otherReferralFeeAmount
35
+ - adjustedPlatformFeeAmount >= requiredNetAmount;
36
+ return {
37
+ referralFees: input.referralFees.flatMap((entry) => {
38
+ if (entry.recipient.trim().toLowerCase() !== platformRecipient) {
39
+ return [{ ...entry }];
40
+ }
41
+ return adjustedPlatformReferralFeeBps === 0
42
+ ? []
43
+ : [{ ...entry, feeBps: adjustedPlatformReferralFeeBps }];
44
+ }),
45
+ configuredPlatformReferralFeeBps,
46
+ adjustedPlatformReferralFeeBps,
47
+ otherReferralFeeAmount: otherReferralFeeAmount.toString(),
48
+ platformFeeCapacityAmount: platformFeeCapacityAmount.toString(),
49
+ insufficientCapacity,
50
+ requiredNetCovered,
51
+ };
52
+ }
53
+ export function computeNetAfterPreciseFees(grossAmount, feeRates18dec) {
54
+ const gross = parsePositiveUnits(grossAmount, 'gross amount');
55
+ const totalFees = feeRates18dec.reduce((total, feeRate) => {
56
+ const rate = parseUnsignedUnits(feeRate, 'precise fee rate');
57
+ if (rate > PRECISE_FEE_DENOMINATOR) {
58
+ throw new RangeError('Precise fee rate cannot exceed 100%');
59
+ }
60
+ return total + (gross * rate / PRECISE_FEE_DENOMINATOR);
61
+ }, 0n);
62
+ if (totalFees > gross) {
63
+ throw new RangeError('Precise fees exceed gross amount');
64
+ }
65
+ return (gross - totalFees).toString();
66
+ }
67
+ function findLargestFittingBps(grossAmount, capacity, configuredBps) {
68
+ let low = 0;
69
+ let high = configuredBps;
70
+ while (low < high) {
71
+ const candidate = Math.ceil((low + high) / 2);
72
+ if (computeBpsFee(grossAmount, candidate) <= capacity) {
73
+ low = candidate;
74
+ }
75
+ else {
76
+ high = candidate - 1;
77
+ }
78
+ }
79
+ return low;
80
+ }
81
+ function computeBpsFee(grossAmount, feeBps) {
82
+ return grossAmount * BigInt(feeBps) / BPS_DENOMINATOR;
83
+ }
84
+ function validateFeeEntry(entry) {
85
+ if (entry.recipient.trim() === '') {
86
+ throw new RangeError('Referral fee recipient is required');
87
+ }
88
+ if (!Number.isInteger(entry.feeBps) || entry.feeBps < 0 || entry.feeBps > 10000) {
89
+ throw new RangeError(`Invalid referral fee bps: ${entry.feeBps}`);
90
+ }
91
+ }
92
+ function parsePositiveUnits(value, label) {
93
+ const parsed = parseUnsignedUnits(value, label);
94
+ if (parsed <= 0n) {
95
+ throw new RangeError(`${label} must be positive`);
96
+ }
97
+ return parsed;
98
+ }
99
+ function parseUnsignedUnits(value, label) {
100
+ const normalized = value.trim();
101
+ if (!UNSIGNED_INTEGER_PATTERN.test(normalized)) {
102
+ throw new RangeError(`${label} must be an unsigned integer`);
103
+ }
104
+ return BigInt(normalized);
105
+ }
package/dist/types.d.ts CHANGED
@@ -112,6 +112,7 @@ type CreateOrderRequestBase = {
112
112
  destinationChainId?: number;
113
113
  feePayer?: FeePayerType;
114
114
  enabledRails?: string[];
115
+ dynamicOrdersEnabled?: boolean;
115
116
  successUrl?: string | null;
116
117
  cancelUrl?: string | null;
117
118
  notes?: Record<string, unknown> | null;
@@ -140,6 +141,7 @@ export type MerchantCheckoutTheme = {
140
141
  buttonTextColor?: string | null;
141
142
  defaultMaxFeePercentage?: number;
142
143
  payWithCryptoEnabled?: boolean;
144
+ currencySelectorEnabled?: boolean;
143
145
  [key: string]: unknown;
144
146
  };
145
147
  export type CheckoutMerchant = {
@@ -174,6 +176,11 @@ export type CheckoutOrder = {
174
176
  cancelUrl: string | null;
175
177
  notes: unknown | null;
176
178
  metadata: unknown | null;
179
+ /** Merchant-requested fiat price; non-null only for pristine, un-resized fiat-input orders. */
180
+ requestedFiat?: {
181
+ amount: string;
182
+ currency: string;
183
+ } | null;
177
184
  bridgeInfo?: OrderBridgeInfo | null;
178
185
  referralSplitConfig: ReferralSplitConfig | null;
179
186
  cancelledAt: string | null;
@@ -189,6 +196,7 @@ export type CheckoutPaymentQuote = {
189
196
  amount: string;
190
197
  toAddress: string;
191
198
  escrowAddress?: string;
199
+ orchestratorAddress?: string;
192
200
  fiatCurrencyCode?: string;
193
201
  payeeDetails: string;
194
202
  };
@@ -208,10 +216,53 @@ export type CheckoutQuoteEntry = {
208
216
  spreadPercentage: number;
209
217
  sellerAutomatedReleaseAvailable?: boolean;
210
218
  };
219
+ export type CheckoutNearbySuggestion = {
220
+ resizeAmountUsdc: string;
221
+ paymentAmount: string;
222
+ paymentCurrency: string;
223
+ tokenAmount: string;
224
+ percentDifference: string;
225
+ rail: string;
226
+ };
227
+ export type CheckoutNearbySuggestions = {
228
+ status: 'suggestions' | 'none' | 'ineligible' | 'unavailable';
229
+ below: CheckoutNearbySuggestion[];
230
+ above: CheckoutNearbySuggestion[];
231
+ };
232
+ export type QuoteAvailabilityRequest = {
233
+ amount: string;
234
+ quoteMode: CheckoutModeType;
235
+ fiatCurrency?: string;
236
+ destinationChainId: string | number;
237
+ destinationToken: string;
238
+ destinationAddress: string;
239
+ /** Max suggestions PER DIRECTION (1-10, API default 3). A response can carry up to 2x this. */
240
+ nearbyQuotesCount?: number;
241
+ };
242
+ export type QuoteAvailabilitySuggestion = {
243
+ suggestedAmount: string;
244
+ percentDifference: string;
245
+ rail: string;
246
+ paymentAmount: string;
247
+ paymentCurrency: string;
248
+ tokenAmount: string;
249
+ conversionRate: string;
250
+ };
251
+ export type QuoteAvailabilitySuggestions = {
252
+ below: QuoteAvailabilitySuggestion[];
253
+ above: QuoteAvailabilitySuggestion[];
254
+ };
255
+ /** `nearbySuggestions` is always null when `available` is true. */
256
+ export type QuoteAvailability = {
257
+ available: boolean;
258
+ quoteCount: number;
259
+ nearbySuggestions: QuoteAvailabilitySuggestions | null;
260
+ };
211
261
  export type CheckoutQuotes = {
212
262
  orderId: string;
213
263
  fiatCurrency: string;
214
264
  platforms: CheckoutQuoteEntry[];
265
+ nearbySuggestions?: CheckoutNearbySuggestions;
215
266
  };
216
267
  export type CheckoutPayment = {
217
268
  id: string;
@@ -256,8 +307,6 @@ export type CreateOrderResponse = {
256
307
  order: CheckoutOrder;
257
308
  orderToken: string;
258
309
  };
259
- export type CreateCheckoutResponse = CreateOrderResponse;
260
- export type CheckoutCreateResponse = CreateOrderResponse;
261
310
  export type CreatePaymentRequest = {
262
311
  rail: string;
263
312
  paymentMethodId?: string;
@@ -266,6 +315,7 @@ export type CreatePaymentRequest = {
266
315
  replacementPaymentId?: string;
267
316
  quoteLimit?: number;
268
317
  excludedPayToValues?: string[];
318
+ refundTo?: string;
269
319
  };
270
320
  export type CheckoutPaymentCreationResponse = CheckoutPayment & {
271
321
  relayerTransactionId?: string;
@@ -291,24 +341,14 @@ export type MerchantConfig = {
291
341
  sweepEnabled: boolean;
292
342
  recipientOverrideEnabled: boolean;
293
343
  disableBranding: boolean;
344
+ dynamicOrdersEnabled: boolean;
294
345
  merchantCheckoutTheme: MerchantCheckoutTheme;
295
346
  platformReferralFeeConfig: ReferralFeeConfig | null;
296
347
  referralSplitConfig: ReferralSplitConfig | null;
348
+ flatFeePricing: boolean;
297
349
  createdAt: string;
298
350
  updatedAt: string | null;
299
351
  };
300
- export declare const DashboardPrivyApp: {
301
- readonly LEGACY: "LEGACY";
302
- readonly V1: "V1";
303
- };
304
- export type DashboardPrivyAppType = typeof DashboardPrivyApp[keyof typeof DashboardPrivyApp];
305
- export declare const MerchantMigrationStatus: {
306
- readonly PENDING: "PENDING";
307
- readonly MIGRATING: "MIGRATING";
308
- readonly COMPLETE: "COMPLETE";
309
- readonly FAILED: "FAILED";
310
- };
311
- export type MerchantMigrationStatusType = typeof MerchantMigrationStatus[keyof typeof MerchantMigrationStatus];
312
352
  export type MerchantUser = {
313
353
  id: string;
314
354
  email: string | null;
@@ -325,11 +365,13 @@ export type MerchantProfile = {
325
365
  logoUrl: string | null;
326
366
  industryType: string | null;
327
367
  industryOtherText: string | null;
368
+ integrationPath: MerchantIntegrationPathValue | null;
369
+ onboardingCompletedAt: string | null;
370
+ onboardingSkippedAt: string | null;
328
371
  apiKey: string;
329
372
  environment: MerchantEnvironmentType;
330
373
  sandboxMerchantId: string | null;
331
374
  inPersonCheckoutEnabled: boolean;
332
- migrationStatus: MerchantMigrationStatusType;
333
375
  evmWalletAddress: string | null;
334
376
  solanaWalletAddress: string | null;
335
377
  v1EvmWalletAddress: string | null;
@@ -547,6 +589,7 @@ type CreateSessionBaseRequest = {
547
589
  cancelUrl?: string;
548
590
  metadata?: Record<string, string>;
549
591
  enabledPaymentPlatforms?: PaymentPlatformType[];
592
+ dynamicOrdersEnabled?: boolean;
550
593
  };
551
594
  type CreateSessionExactTokenRequest = CreateSessionBaseRequest & {
552
595
  checkoutMode?: 'exact-token';
@@ -668,10 +711,41 @@ export interface OrderBridgeInfo {
668
711
  fallbackToOrigin?: boolean;
669
712
  failureMessage?: string | null;
670
713
  }
714
+ export declare const MerchantIntegrationPath: {
715
+ readonly CUSTOM_API: "CUSTOM_API";
716
+ readonly WOOCOMMERCE: "WOOCOMMERCE";
717
+ readonly IN_PERSON: "IN_PERSON";
718
+ readonly TELEGRAM: "TELEGRAM";
719
+ readonly SHOPIFY: "SHOPIFY";
720
+ };
721
+ export type MerchantIntegrationPathValue = typeof MerchantIntegrationPath[keyof typeof MerchantIntegrationPath];
722
+ export declare const OnboardingStepStatus: {
723
+ readonly DONE: "done";
724
+ readonly TODO: "todo";
725
+ };
726
+ export type OnboardingStepStatusValue = typeof OnboardingStepStatus[keyof typeof OnboardingStepStatus];
727
+ export declare const OnboardingStepVerifiedBy: {
728
+ readonly AUTO: "auto";
729
+ readonly MANUAL: "manual";
730
+ };
731
+ export type OnboardingStepVerifiedByValue = typeof OnboardingStepVerifiedBy[keyof typeof OnboardingStepVerifiedBy];
732
+ export type OnboardingStepState = {
733
+ id: string;
734
+ status: OnboardingStepStatusValue;
735
+ verifiedBy: OnboardingStepVerifiedByValue;
736
+ };
737
+ export type MerchantOnboardingResponse = {
738
+ path: MerchantIntegrationPathValue | null;
739
+ sandboxApiKey: string | null;
740
+ completedAt: string | null;
741
+ skippedAt: string | null;
742
+ steps: OnboardingStepState[];
743
+ };
671
744
  export declare const WebhookEventType: {
672
745
  readonly ORDER_CREATED: "ORDER_CREATED";
673
746
  readonly ORDER_FULFILLED: "ORDER_FULFILLED";
674
747
  readonly ORDER_CANCELLED: "ORDER_CANCELLED";
748
+ readonly ORDER_RESIZED: "ORDER_RESIZED";
675
749
  readonly PAYMENT_CREATED: "PAYMENT_CREATED";
676
750
  readonly PAYMENT_SETTLED: "PAYMENT_SETTLED";
677
751
  readonly PAYMENT_CANCELLED: "PAYMENT_CANCELLED";
@@ -705,9 +779,6 @@ export type Webhook = {
705
779
  createdAt: string;
706
780
  updatedAt: string;
707
781
  };
708
- export type WebhookWithSecret = Webhook & {
709
- secret: string;
710
- };
711
782
  export type WebhookDelivery = {
712
783
  id: string;
713
784
  webhookId: string;
@@ -729,6 +800,10 @@ export type WebhookPayload = {
729
800
  payment: CheckoutPayment | null;
730
801
  refund: Record<string, unknown> | null;
731
802
  paymentBridge: Record<string, unknown> | null;
803
+ resize?: {
804
+ previousAmountUsdc: string;
805
+ newAmountUsdc: string;
806
+ };
732
807
  };
733
808
  };
734
809
  export type CreateWebhookRequest = {
@@ -736,7 +811,10 @@ export type CreateWebhookRequest = {
736
811
  events?: WebhookEventTypeValue[];
737
812
  customHeaders?: WebhookHeader[];
738
813
  };
739
- export type CreateWebhookResponse = WebhookWithSecret;
814
+ export type CreateWebhookResponse = {
815
+ webhook: Webhook;
816
+ secret: string;
817
+ };
740
818
  export type UpdateWebhookRequest = {
741
819
  url?: string;
742
820
  events?: WebhookEventTypeValue[];
package/dist/types.js CHANGED
@@ -90,16 +90,6 @@ export const CheckoutPaymentMemoPolicy = {
90
90
  REQUIRED: 'REQUIRED',
91
91
  EMPTY: 'EMPTY',
92
92
  };
93
- export const DashboardPrivyApp = {
94
- LEGACY: 'LEGACY',
95
- V1: 'V1',
96
- };
97
- export const MerchantMigrationStatus = {
98
- PENDING: 'PENDING',
99
- MIGRATING: 'MIGRATING',
100
- COMPLETE: 'COMPLETE',
101
- FAILED: 'FAILED',
102
- };
103
93
  // Structured error codes for Order failures
104
94
  export const OrderErrorCode = {
105
95
  // Proof verification errors
@@ -131,11 +121,28 @@ export const TrancheCurrency = {
131
121
  USD: 'USD',
132
122
  USDC: 'USDC',
133
123
  };
124
+ // ============ Merchant Onboarding Types ============
125
+ export const MerchantIntegrationPath = {
126
+ CUSTOM_API: 'CUSTOM_API',
127
+ WOOCOMMERCE: 'WOOCOMMERCE',
128
+ IN_PERSON: 'IN_PERSON',
129
+ TELEGRAM: 'TELEGRAM',
130
+ SHOPIFY: 'SHOPIFY',
131
+ };
132
+ export const OnboardingStepStatus = {
133
+ DONE: 'done',
134
+ TODO: 'todo',
135
+ };
136
+ export const OnboardingStepVerifiedBy = {
137
+ AUTO: 'auto',
138
+ MANUAL: 'manual',
139
+ };
134
140
  // ============ Webhook Types ============
135
141
  export const WebhookEventType = {
136
142
  ORDER_CREATED: 'ORDER_CREATED',
137
143
  ORDER_FULFILLED: 'ORDER_FULFILLED',
138
144
  ORDER_CANCELLED: 'ORDER_CANCELLED',
145
+ ORDER_RESIZED: 'ORDER_RESIZED',
139
146
  PAYMENT_CREATED: 'PAYMENT_CREATED',
140
147
  PAYMENT_SETTLED: 'PAYMENT_SETTLED',
141
148
  PAYMENT_CANCELLED: 'PAYMENT_CANCELLED',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zkp2p/pay-shared",
3
- "version": "2.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Shared TypeScript types, enums, and chain utilities used by ZKP2P Pay API, SDK, and frontend apps.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",