@zkp2p/pay-shared 2.0.0 → 3.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@2.0.0
12
+ npm install @zkp2p/pay-shared@3.0.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,24 @@ 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
+ };
211
232
  export type CheckoutQuotes = {
212
233
  orderId: string;
213
234
  fiatCurrency: string;
214
235
  platforms: CheckoutQuoteEntry[];
236
+ nearbySuggestions?: CheckoutNearbySuggestions;
215
237
  };
216
238
  export type CheckoutPayment = {
217
239
  id: string;
@@ -256,8 +278,6 @@ export type CreateOrderResponse = {
256
278
  order: CheckoutOrder;
257
279
  orderToken: string;
258
280
  };
259
- export type CreateCheckoutResponse = CreateOrderResponse;
260
- export type CheckoutCreateResponse = CreateOrderResponse;
261
281
  export type CreatePaymentRequest = {
262
282
  rail: string;
263
283
  paymentMethodId?: string;
@@ -266,6 +286,7 @@ export type CreatePaymentRequest = {
266
286
  replacementPaymentId?: string;
267
287
  quoteLimit?: number;
268
288
  excludedPayToValues?: string[];
289
+ refundTo?: string;
269
290
  };
270
291
  export type CheckoutPaymentCreationResponse = CheckoutPayment & {
271
292
  relayerTransactionId?: string;
@@ -291,24 +312,14 @@ export type MerchantConfig = {
291
312
  sweepEnabled: boolean;
292
313
  recipientOverrideEnabled: boolean;
293
314
  disableBranding: boolean;
315
+ dynamicOrdersEnabled: boolean;
294
316
  merchantCheckoutTheme: MerchantCheckoutTheme;
295
317
  platformReferralFeeConfig: ReferralFeeConfig | null;
296
318
  referralSplitConfig: ReferralSplitConfig | null;
319
+ flatFeePricing: boolean;
297
320
  createdAt: string;
298
321
  updatedAt: string | null;
299
322
  };
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
323
  export type MerchantUser = {
313
324
  id: string;
314
325
  email: string | null;
@@ -325,11 +336,13 @@ export type MerchantProfile = {
325
336
  logoUrl: string | null;
326
337
  industryType: string | null;
327
338
  industryOtherText: string | null;
339
+ integrationPath: MerchantIntegrationPathValue | null;
340
+ onboardingCompletedAt: string | null;
341
+ onboardingSkippedAt: string | null;
328
342
  apiKey: string;
329
343
  environment: MerchantEnvironmentType;
330
344
  sandboxMerchantId: string | null;
331
345
  inPersonCheckoutEnabled: boolean;
332
- migrationStatus: MerchantMigrationStatusType;
333
346
  evmWalletAddress: string | null;
334
347
  solanaWalletAddress: string | null;
335
348
  v1EvmWalletAddress: string | null;
@@ -547,6 +560,7 @@ type CreateSessionBaseRequest = {
547
560
  cancelUrl?: string;
548
561
  metadata?: Record<string, string>;
549
562
  enabledPaymentPlatforms?: PaymentPlatformType[];
563
+ dynamicOrdersEnabled?: boolean;
550
564
  };
551
565
  type CreateSessionExactTokenRequest = CreateSessionBaseRequest & {
552
566
  checkoutMode?: 'exact-token';
@@ -668,10 +682,41 @@ export interface OrderBridgeInfo {
668
682
  fallbackToOrigin?: boolean;
669
683
  failureMessage?: string | null;
670
684
  }
685
+ export declare const MerchantIntegrationPath: {
686
+ readonly CUSTOM_API: "CUSTOM_API";
687
+ readonly WOOCOMMERCE: "WOOCOMMERCE";
688
+ readonly IN_PERSON: "IN_PERSON";
689
+ readonly TELEGRAM: "TELEGRAM";
690
+ readonly SHOPIFY: "SHOPIFY";
691
+ };
692
+ export type MerchantIntegrationPathValue = typeof MerchantIntegrationPath[keyof typeof MerchantIntegrationPath];
693
+ export declare const OnboardingStepStatus: {
694
+ readonly DONE: "done";
695
+ readonly TODO: "todo";
696
+ };
697
+ export type OnboardingStepStatusValue = typeof OnboardingStepStatus[keyof typeof OnboardingStepStatus];
698
+ export declare const OnboardingStepVerifiedBy: {
699
+ readonly AUTO: "auto";
700
+ readonly MANUAL: "manual";
701
+ };
702
+ export type OnboardingStepVerifiedByValue = typeof OnboardingStepVerifiedBy[keyof typeof OnboardingStepVerifiedBy];
703
+ export type OnboardingStepState = {
704
+ id: string;
705
+ status: OnboardingStepStatusValue;
706
+ verifiedBy: OnboardingStepVerifiedByValue;
707
+ };
708
+ export type MerchantOnboardingResponse = {
709
+ path: MerchantIntegrationPathValue | null;
710
+ sandboxApiKey: string | null;
711
+ completedAt: string | null;
712
+ skippedAt: string | null;
713
+ steps: OnboardingStepState[];
714
+ };
671
715
  export declare const WebhookEventType: {
672
716
  readonly ORDER_CREATED: "ORDER_CREATED";
673
717
  readonly ORDER_FULFILLED: "ORDER_FULFILLED";
674
718
  readonly ORDER_CANCELLED: "ORDER_CANCELLED";
719
+ readonly ORDER_RESIZED: "ORDER_RESIZED";
675
720
  readonly PAYMENT_CREATED: "PAYMENT_CREATED";
676
721
  readonly PAYMENT_SETTLED: "PAYMENT_SETTLED";
677
722
  readonly PAYMENT_CANCELLED: "PAYMENT_CANCELLED";
@@ -705,9 +750,6 @@ export type Webhook = {
705
750
  createdAt: string;
706
751
  updatedAt: string;
707
752
  };
708
- export type WebhookWithSecret = Webhook & {
709
- secret: string;
710
- };
711
753
  export type WebhookDelivery = {
712
754
  id: string;
713
755
  webhookId: string;
@@ -729,6 +771,10 @@ export type WebhookPayload = {
729
771
  payment: CheckoutPayment | null;
730
772
  refund: Record<string, unknown> | null;
731
773
  paymentBridge: Record<string, unknown> | null;
774
+ resize?: {
775
+ previousAmountUsdc: string;
776
+ newAmountUsdc: string;
777
+ };
732
778
  };
733
779
  };
734
780
  export type CreateWebhookRequest = {
@@ -736,7 +782,10 @@ export type CreateWebhookRequest = {
736
782
  events?: WebhookEventTypeValue[];
737
783
  customHeaders?: WebhookHeader[];
738
784
  };
739
- export type CreateWebhookResponse = WebhookWithSecret;
785
+ export type CreateWebhookResponse = {
786
+ webhook: Webhook;
787
+ secret: string;
788
+ };
740
789
  export type UpdateWebhookRequest = {
741
790
  url?: string;
742
791
  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.0.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",