@zkp2p/pay-shared 1.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 +2 -2
- package/dist/addressEncoding.d.ts +5 -0
- package/dist/addressEncoding.js +126 -0
- package/dist/addressValidators.d.ts +2 -0
- package/dist/addressValidators.js +14 -0
- package/dist/bitcoin.js +71 -5
- package/dist/buyerTee.d.ts +4 -0
- package/dist/buyerTee.js +3 -0
- package/dist/chains.d.ts +1 -0
- package/dist/chains.js +5 -0
- package/dist/crypto.d.ts +31 -0
- package/dist/crypto.js +68 -3
- package/dist/fiatAmount.d.ts +19 -0
- package/dist/fiatAmount.js +58 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/onboarding.d.ts +39 -0
- package/dist/onboarding.js +150 -0
- package/dist/payeeFeeCap.d.ts +21 -0
- package/dist/payeeFeeCap.js +105 -0
- package/dist/paypalSarPhase.d.ts +5 -0
- package/dist/paypalSarPhase.js +8 -0
- package/dist/paypalSarVerify.d.ts +41 -0
- package/dist/paypalSarVerify.js +31 -0
- package/dist/rails.d.ts +76 -0
- package/dist/rails.js +182 -6
- package/dist/types.d.ts +133 -9
- package/dist/types.js +43 -0
- package/package.json +5 -1
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/rails.d.ts
CHANGED
|
@@ -42,6 +42,17 @@ export declare enum SupportedRelayRail {
|
|
|
42
42
|
export declare const DEFAULT_FIAT_RAILS: readonly SupportedRail[];
|
|
43
43
|
export declare const SUPPORTED_RAILS: readonly SupportedRail[];
|
|
44
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;
|
|
45
56
|
/**
|
|
46
57
|
* Normalizes a rail string into a crypto chain id.
|
|
47
58
|
*
|
|
@@ -51,11 +62,76 @@ export declare const SUPPORTED_RELAY_RAILS: readonly SupportedRelayRail[];
|
|
|
51
62
|
*/
|
|
52
63
|
export declare function parseCryptoRailChainId(rail: string | null | undefined): number | null;
|
|
53
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[];
|
|
54
83
|
/** Returns true when the rail targets a crypto chain (legacy numeric or relay-prefixed format). */
|
|
55
84
|
export declare function isCryptoRail(rail: string | null | undefined): boolean;
|
|
56
85
|
/** Formats a chain id into the canonical relay rail string: `relay_<chainId>`. */
|
|
57
86
|
export declare function formatRelayRail(chainId: number): string;
|
|
58
87
|
/** Normalizes any crypto rail into canonical relay format (or null for non-crypto rails). */
|
|
59
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;
|
|
60
136
|
/** Formats a rail value for human-readable UI labels. */
|
|
61
137
|
export declare function getRailDisplayName(rail: string | null | undefined): string;
|