@provex/utils 1.9.1 → 1.9.2-rc.20260702180200.fd5d003
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/consent.test.js +24 -0
- package/dist/contracts.d.ts +44 -5
- package/dist/contracts.js +49 -6
- package/dist/contracts.test.js +54 -1
- package/dist/fees.test.js +25 -0
- package/dist/input.test.js +13 -0
- package/dist/payment.test.js +504 -0
- package/dist/reputation.test.js +18 -0
- package/dist/units.test.js +12 -0
- package/package.json +1 -4
package/dist/consent.test.js
CHANGED
|
@@ -65,6 +65,21 @@ describe('formatRelative', () => {
|
|
|
65
65
|
it('returns empty string for empty input', () => {
|
|
66
66
|
assert.equal(formatRelative('', now), '');
|
|
67
67
|
});
|
|
68
|
+
it('returns empty string for an unparseable timestamp', () => {
|
|
69
|
+
// A stored `acceptedAt` could be corrupted by a manual localStorage
|
|
70
|
+
// edit or a botched migration. `new Date('garbage').getTime()` is
|
|
71
|
+
// NaN — without the guard this would render "NaNs ago" in the audit
|
|
72
|
+
// row. Degrade to empty instead, matching the empty-input handling.
|
|
73
|
+
assert.equal(formatRelative('not-a-date', now), '');
|
|
74
|
+
});
|
|
75
|
+
it('clamps future timestamps to "just now"', () => {
|
|
76
|
+
// A clock skew between the user's device and the acceptance source
|
|
77
|
+
// can produce an acceptedAt in the future. The JSDoc commits to
|
|
78
|
+
// clamping these to "just now" rather than emitting a negative
|
|
79
|
+
// duration; the Math.max(0, …) floor is what enforces it.
|
|
80
|
+
const future = new Date(T0 + 60_000).toISOString();
|
|
81
|
+
assert.equal(formatRelative(future, now), 'just now');
|
|
82
|
+
});
|
|
68
83
|
});
|
|
69
84
|
describe('readConsent', () => {
|
|
70
85
|
it('returns {} when the storage key is unset', () => {
|
|
@@ -94,6 +109,15 @@ describe('readConsent', () => {
|
|
|
94
109
|
// Server-side render path: no localStorage available.
|
|
95
110
|
assert.deepEqual(readConsent('k', null), {});
|
|
96
111
|
});
|
|
112
|
+
it('falls back to ambient storage and degrades to {} off-browser', () => {
|
|
113
|
+
// Omitting the storage argument exercises the default-parameter
|
|
114
|
+
// path, which calls ambientStorage(). Under the Node test runner
|
|
115
|
+
// there is no globalThis.localStorage, so ambientStorage() returns
|
|
116
|
+
// null and readConsent degrades to {} — the same safe result the
|
|
117
|
+
// SSR / private-mode paths produce. This pins that the module never
|
|
118
|
+
// throws when used without an explicit storage handle.
|
|
119
|
+
assert.deepEqual(readConsent('unset-key'), {});
|
|
120
|
+
});
|
|
97
121
|
});
|
|
98
122
|
describe('sectionStatus', () => {
|
|
99
123
|
it('returns "unread" when no stored entry exists', () => {
|
package/dist/contracts.d.ts
CHANGED
|
@@ -62,7 +62,7 @@ export declare const v3BaseContracts: V3Contracts;
|
|
|
62
62
|
* V3 contracts deployed to Pulsechain (Chain ID 369)
|
|
63
63
|
* Deployed: 2026-01-12
|
|
64
64
|
* Deployer: 0xC3c7e05D1BA19563693D891e5c38f0FC988A5d11
|
|
65
|
-
*
|
|
65
|
+
* Protocol fee recipient (EOA, not a multisig): 0x948eB6D3A08BEb29Dc1D08d09753862573A94122
|
|
66
66
|
*/
|
|
67
67
|
export declare const v3PulsechainContracts: V3Contracts;
|
|
68
68
|
/**
|
|
@@ -192,15 +192,54 @@ export declare function shortenEscrowAddress(escrow: Hex): string;
|
|
|
192
192
|
export declare function getEscrowVersion(escrowAddress: Hex, chainId?: ChainId): Version | null;
|
|
193
193
|
/**
|
|
194
194
|
* Check if an escrow address is a V3 contract.
|
|
195
|
-
*
|
|
195
|
+
*
|
|
196
|
+
* @deprecated Returns true for **exactly** v3, so it silently excludes every
|
|
197
|
+
* newer orchestrator-based generation (v4+). For feature gates that mean
|
|
198
|
+
* "modern orchestrator escrow vs the legacy v2 monolith", use
|
|
199
|
+
* {@link supportsOrchestratorEscrow} instead. Only keep using this where a
|
|
200
|
+
* call site genuinely needs to single out v3 and nothing newer.
|
|
196
201
|
*
|
|
197
202
|
* @param escrowAddress - The escrow contract address
|
|
198
203
|
* @param chainId - The chain ID
|
|
199
204
|
* @returns true if the escrow is V3, false otherwise
|
|
205
|
+
*/
|
|
206
|
+
export declare function isV3Escrow(escrowAddress: Hex, chainId: ChainId): boolean;
|
|
207
|
+
/**
|
|
208
|
+
* Whether a generation is an orchestrator-based escrow (v3 or newer) rather
|
|
209
|
+
* than the legacy v2 monolith.
|
|
210
|
+
*
|
|
211
|
+
* Use this to gate features that exist on every modern generation — add /
|
|
212
|
+
* remove funds, retain-on-empty, delegate, accepting-intents toggles, intent
|
|
213
|
+
* pruning, owner management panels. Because it is defined as "not v2" rather
|
|
214
|
+
* than "is v3", new generations (v4+) light up automatically without editing
|
|
215
|
+
* each call site.
|
|
216
|
+
*
|
|
217
|
+
* @param version - The escrow version, or null when the address is unknown
|
|
218
|
+
* @returns true for v3 and every newer generation; false for v2 and null
|
|
200
219
|
*
|
|
201
220
|
* @example
|
|
202
|
-
*
|
|
203
|
-
* const showRetainToggle =
|
|
221
|
+
* const supportsOrchestrator = supportsOrchestrator(deposit.version)
|
|
222
|
+
* const showRetainToggle = supportsOrchestrator && isOwner
|
|
204
223
|
*/
|
|
205
|
-
export declare function
|
|
224
|
+
export declare function supportsOrchestrator(version: Version | null): boolean;
|
|
225
|
+
/**
|
|
226
|
+
* Address-based convenience over {@link supportsOrchestrator}: resolve the
|
|
227
|
+
* escrow's generation from its address, then test the capability. This is the
|
|
228
|
+
* v4-safe replacement for {@link isV3Escrow} at feature-gate call sites.
|
|
229
|
+
*
|
|
230
|
+
* @param escrowAddress - The escrow contract address
|
|
231
|
+
* @param chainId - The chain ID
|
|
232
|
+
* @returns true if the escrow is v3 or a newer orchestrator generation
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* // Gate orchestrator-only features in UI (v3 and v4 both pass)
|
|
236
|
+
* const showRetainToggle = supportsOrchestratorEscrow(deposit.escrow, deposit.chainId) && isOwner
|
|
237
|
+
*/
|
|
238
|
+
export declare function supportsOrchestratorEscrow(escrowAddress: Hex, chainId: ChainId): boolean;
|
|
239
|
+
/** MessageBus contract addresses — placeholder until deployed */
|
|
240
|
+
export declare const messageBus: {
|
|
241
|
+
readonly 943: "0x0000000000000000000000000000000000000000";
|
|
242
|
+
readonly 369: "0x0000000000000000000000000000000000000000";
|
|
243
|
+
readonly 8453: "0x0000000000000000000000000000000000000000";
|
|
244
|
+
};
|
|
206
245
|
export {};
|
package/dist/contracts.js
CHANGED
|
@@ -83,7 +83,7 @@ export const v3BaseContracts = {
|
|
|
83
83
|
* V3 contracts deployed to Pulsechain (Chain ID 369)
|
|
84
84
|
* Deployed: 2026-01-12
|
|
85
85
|
* Deployer: 0xC3c7e05D1BA19563693D891e5c38f0FC988A5d11
|
|
86
|
-
*
|
|
86
|
+
* Protocol fee recipient (EOA, not a multisig): 0x948eB6D3A08BEb29Dc1D08d09753862573A94122
|
|
87
87
|
*/
|
|
88
88
|
export const v3PulsechainContracts = {
|
|
89
89
|
usdc: getAddress('0x15D38573d2feeb82e7ad5187aB8c1D52810B1f07'),
|
|
@@ -277,16 +277,59 @@ export function getEscrowVersion(escrowAddress, chainId) {
|
|
|
277
277
|
}
|
|
278
278
|
/**
|
|
279
279
|
* Check if an escrow address is a V3 contract.
|
|
280
|
-
*
|
|
280
|
+
*
|
|
281
|
+
* @deprecated Returns true for **exactly** v3, so it silently excludes every
|
|
282
|
+
* newer orchestrator-based generation (v4+). For feature gates that mean
|
|
283
|
+
* "modern orchestrator escrow vs the legacy v2 monolith", use
|
|
284
|
+
* {@link supportsOrchestratorEscrow} instead. Only keep using this where a
|
|
285
|
+
* call site genuinely needs to single out v3 and nothing newer.
|
|
281
286
|
*
|
|
282
287
|
* @param escrowAddress - The escrow contract address
|
|
283
288
|
* @param chainId - The chain ID
|
|
284
289
|
* @returns true if the escrow is V3, false otherwise
|
|
285
|
-
*
|
|
286
|
-
* @example
|
|
287
|
-
* // Gate V3-only features in UI
|
|
288
|
-
* const showRetainToggle = isV3Escrow(deposit.escrow, deposit.chainId) && isOwner
|
|
289
290
|
*/
|
|
290
291
|
export function isV3Escrow(escrowAddress, chainId) {
|
|
291
292
|
return getEscrowVersion(escrowAddress, chainId) === 'v3';
|
|
292
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Whether a generation is an orchestrator-based escrow (v3 or newer) rather
|
|
296
|
+
* than the legacy v2 monolith.
|
|
297
|
+
*
|
|
298
|
+
* Use this to gate features that exist on every modern generation — add /
|
|
299
|
+
* remove funds, retain-on-empty, delegate, accepting-intents toggles, intent
|
|
300
|
+
* pruning, owner management panels. Because it is defined as "not v2" rather
|
|
301
|
+
* than "is v3", new generations (v4+) light up automatically without editing
|
|
302
|
+
* each call site.
|
|
303
|
+
*
|
|
304
|
+
* @param version - The escrow version, or null when the address is unknown
|
|
305
|
+
* @returns true for v3 and every newer generation; false for v2 and null
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* const supportsOrchestrator = supportsOrchestrator(deposit.version)
|
|
309
|
+
* const showRetainToggle = supportsOrchestrator && isOwner
|
|
310
|
+
*/
|
|
311
|
+
export function supportsOrchestrator(version) {
|
|
312
|
+
return version !== null && version !== 'v2';
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Address-based convenience over {@link supportsOrchestrator}: resolve the
|
|
316
|
+
* escrow's generation from its address, then test the capability. This is the
|
|
317
|
+
* v4-safe replacement for {@link isV3Escrow} at feature-gate call sites.
|
|
318
|
+
*
|
|
319
|
+
* @param escrowAddress - The escrow contract address
|
|
320
|
+
* @param chainId - The chain ID
|
|
321
|
+
* @returns true if the escrow is v3 or a newer orchestrator generation
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* // Gate orchestrator-only features in UI (v3 and v4 both pass)
|
|
325
|
+
* const showRetainToggle = supportsOrchestratorEscrow(deposit.escrow, deposit.chainId) && isOwner
|
|
326
|
+
*/
|
|
327
|
+
export function supportsOrchestratorEscrow(escrowAddress, chainId) {
|
|
328
|
+
return supportsOrchestrator(getEscrowVersion(escrowAddress, chainId));
|
|
329
|
+
}
|
|
330
|
+
/** MessageBus contract addresses — placeholder until deployed */
|
|
331
|
+
export const messageBus = {
|
|
332
|
+
943: "0x0000000000000000000000000000000000000000",
|
|
333
|
+
369: "0x0000000000000000000000000000000000000000",
|
|
334
|
+
8453: "0x0000000000000000000000000000000000000000",
|
|
335
|
+
};
|
package/dist/contracts.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { base, pulsechain } from 'viem/chains';
|
|
4
|
-
import { getContracts, getLatestContract, getAllEscrowContracts, shortenEscrowAddress, getEscrowVersion, isV3Escrow, v2BaseContracts, v3BaseContracts, v2PulsechainContracts, v3PulsechainContracts, V3_PULSECHAIN_DEPLOYMENT_REFERENCE, contracts, } from './contracts.js';
|
|
4
|
+
import { getContracts, getLatestContract, getAllEscrowContracts, getAllV3EscrowContracts, shortenEscrowAddress, getEscrowVersion, isV3Escrow, supportsOrchestrator, supportsOrchestratorEscrow, v2BaseContracts, v3BaseContracts, v2PulsechainContracts, v3PulsechainContracts, V3_PULSECHAIN_DEPLOYMENT_REFERENCE, contracts, } from './contracts.js';
|
|
5
5
|
describe('contracts', () => {
|
|
6
6
|
describe('getDefaultVersion', () => {
|
|
7
7
|
it('returns v3 for Base', async () => {
|
|
@@ -132,6 +132,30 @@ describe('contracts', () => {
|
|
|
132
132
|
}
|
|
133
133
|
});
|
|
134
134
|
});
|
|
135
|
+
describe('getAllV3EscrowContracts', () => {
|
|
136
|
+
// This list drives cross-chain nullifier checking — a V2 escrow
|
|
137
|
+
// leaking into it would have the dedup logic look for double-spends
|
|
138
|
+
// in the wrong contract set, so the V3-only filter is correctness-
|
|
139
|
+
// critical, not cosmetic.
|
|
140
|
+
it('returns only v3 escrow locations', () => {
|
|
141
|
+
const v3Escrows = getAllV3EscrowContracts();
|
|
142
|
+
assert.ok(v3Escrows.length > 0);
|
|
143
|
+
for (const location of v3Escrows) {
|
|
144
|
+
assert.strictEqual(location.version, 'v3');
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
it('excludes every v2 escrow that getAllEscrowContracts reports', () => {
|
|
148
|
+
// The filtered set must equal exactly the v3 subset of the full
|
|
149
|
+
// set — no v2 leakage, no v3 omissions.
|
|
150
|
+
const expectedV3Count = getAllEscrowContracts().filter((location) => location.version === 'v3').length;
|
|
151
|
+
assert.strictEqual(getAllV3EscrowContracts().length, expectedV3Count);
|
|
152
|
+
});
|
|
153
|
+
it('includes both the Base and Pulsechain v3 escrows', () => {
|
|
154
|
+
const v3Escrows = getAllV3EscrowContracts();
|
|
155
|
+
assert.ok(v3Escrows.some((location) => location.chainId === base.id));
|
|
156
|
+
assert.ok(v3Escrows.some((location) => location.chainId === pulsechain.id));
|
|
157
|
+
});
|
|
158
|
+
});
|
|
135
159
|
describe('contract structure', () => {
|
|
136
160
|
it('v2 contracts have required verifiers', () => {
|
|
137
161
|
assert.ok(v2BaseContracts.venmoReclaimVerifier);
|
|
@@ -240,4 +264,33 @@ describe('contracts', () => {
|
|
|
240
264
|
assert.strictEqual(result, false);
|
|
241
265
|
});
|
|
242
266
|
});
|
|
267
|
+
describe('supportsOrchestrator', () => {
|
|
268
|
+
it('returns true for v3', () => {
|
|
269
|
+
assert.strictEqual(supportsOrchestrator('v3'), true);
|
|
270
|
+
});
|
|
271
|
+
it('returns false for the legacy v2 monolith', () => {
|
|
272
|
+
assert.strictEqual(supportsOrchestrator('v2'), false);
|
|
273
|
+
});
|
|
274
|
+
it('returns false for an unknown (null) version', () => {
|
|
275
|
+
assert.strictEqual(supportsOrchestrator(null), false);
|
|
276
|
+
});
|
|
277
|
+
it('admits future orchestrator generations, unlike an exact-v3 check', () => {
|
|
278
|
+
// The predicate is defined as "not v2", so a newer generation (v4+)
|
|
279
|
+
// must pass — that is the entire reason it exists instead of a
|
|
280
|
+
// `version === 'v3'` comparison. If anyone reimplements it as an
|
|
281
|
+
// exact-v3 check, this assertion fails and flags the regression.
|
|
282
|
+
assert.strictEqual(supportsOrchestrator('v4'), true);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
describe('supportsOrchestratorEscrow', () => {
|
|
286
|
+
it('returns true for a v3 escrow address', () => {
|
|
287
|
+
assert.strictEqual(supportsOrchestratorEscrow(v3BaseContracts.escrow[0], base.id), true);
|
|
288
|
+
});
|
|
289
|
+
it('returns false for a v2 escrow address', () => {
|
|
290
|
+
assert.strictEqual(supportsOrchestratorEscrow(v2BaseContracts.escrow[0], base.id), false);
|
|
291
|
+
});
|
|
292
|
+
it('returns false for an unknown escrow address', () => {
|
|
293
|
+
assert.strictEqual(supportsOrchestratorEscrow('0x0000000000000000000000000000000000000000', base.id), false);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
243
296
|
});
|
package/dist/fees.test.js
CHANGED
|
@@ -138,6 +138,31 @@ describe('normalizeFeePrecision', () => {
|
|
|
138
138
|
assert.equal(result.percentage, '100%');
|
|
139
139
|
});
|
|
140
140
|
});
|
|
141
|
+
describe('precision-detection boundary', () => {
|
|
142
|
+
// The heuristic switches at PRECISION_DETECTION_THRESHOLD (1,000,000).
|
|
143
|
+
// This is the money-critical decision: misclassifying the precision
|
|
144
|
+
// scales the fee by 1e14. Pin both sides of the exact boundary so a
|
|
145
|
+
// change to the threshold or comparison operator surfaces.
|
|
146
|
+
it('treats a value exactly at the threshold as basis points', () => {
|
|
147
|
+
// 1,000,000 is NOT greater than the threshold (strict `>`), so it
|
|
148
|
+
// stays in the basis-points branch and is returned unscaled.
|
|
149
|
+
const result = normalizeFeePrecision(1000000n);
|
|
150
|
+
assert.equal(result.precision, 'bps');
|
|
151
|
+
assert.equal(result.normalizedBps, 1000000n);
|
|
152
|
+
});
|
|
153
|
+
it('treats one unit above the threshold as 18-decimal precision', () => {
|
|
154
|
+
// 1,000,001 crosses into the 18-decimal branch. Because the value
|
|
155
|
+
// is a negligible fraction of 1e18, the basis-points normalization
|
|
156
|
+
// floors to 0 — a real consequence of the heuristic at its edge.
|
|
157
|
+
// note: a genuine basis-points fee just above 1,000,000 (≈10000%)
|
|
158
|
+
// would be silently normalized to 0 bps here. Not a concern for
|
|
159
|
+
// real fees (always well under 100%), but pinned so the boundary
|
|
160
|
+
// behavior is explicit rather than accidental.
|
|
161
|
+
const result = normalizeFeePrecision(1000001n);
|
|
162
|
+
assert.equal(result.precision, '18decimals');
|
|
163
|
+
assert.equal(result.normalizedBps, 0n);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
141
166
|
});
|
|
142
167
|
describe('calculateTotalFees', () => {
|
|
143
168
|
it('should sum multiple fees', () => {
|
package/dist/input.test.js
CHANGED
|
@@ -83,6 +83,19 @@ describe('adjustLowestDecimal', () => {
|
|
|
83
83
|
assert.equal(result, '0.123456789012345679');
|
|
84
84
|
});
|
|
85
85
|
});
|
|
86
|
+
describe('leading-dot input (no whole part)', () => {
|
|
87
|
+
// A paste or autofill can yield ".5" with an empty integer part.
|
|
88
|
+
// The split produces whole === '', so after the bigint math the
|
|
89
|
+
// reconstructed whole part slices to an empty string and the
|
|
90
|
+
// `|| '0'` fallback supplies the missing zero. Without it the result
|
|
91
|
+
// would be ".6" (or throw on BigInt('')). Pin the normalization.
|
|
92
|
+
it('normalizes a missing integer part to a leading zero on increment', () => {
|
|
93
|
+
assert.equal(adjustLowestDecimal('.5', 1), '0.6');
|
|
94
|
+
});
|
|
95
|
+
it('normalizes a missing integer part to a leading zero on decrement', () => {
|
|
96
|
+
assert.equal(adjustLowestDecimal('.5', -1), '0.4');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
86
99
|
describe('large whole-number values', () => {
|
|
87
100
|
it('handles values larger than Number.MAX_SAFE_INTEGER via bigint', () => {
|
|
88
101
|
// 2^53 = 9007199254740992 — anything above this loses precision
|
package/dist/payment.test.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { describe, it, test } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { base } from 'viem/chains';
|
|
4
|
+
import { getAddress } from 'viem';
|
|
4
5
|
import { providerConfigs, getCurrenciesForProvider, providers, verifierGetsKey, providerKeyToContractId, resolveApiProviderKey, getProviderPaymentMethodIds, computeProviderAvailability, availableProviderConfigs, subProviders, isDayResolutionRail } from './payment.js';
|
|
6
|
+
import { paymentVerifierToKey, providerByContractId, getProviderByContractId, getProviderNameByContractId, providerKeyToVerifiers, providerKeyToVerifiersMap, getPaymentMethodConfig, getPaymentMethodName, getPaymentMethodMinimum, getPaymentProviderMinimum, calculateEffectiveMinimum, isPaymentMethodKnown, isPaymentMethodIdKnown, isExternalPaymentMethod, getExternalPaymentMethod, externalPaymentMethods, getExtensionPlatform, getParentProviderKey, isSubProviderKey, providerRequiresMetadataIndex, getProviderIconStack, getPaymentIcon, getPlatformRisk, fromPeerTier, tierToPeerTier, tiers, getAllPaymentMethods, verifiablePaymentMethods, } from './payment.js';
|
|
7
|
+
import { getDefaultToken } from './tokens.js';
|
|
5
8
|
import { SUPPORTED_CURRENCIES } from './currencies.js';
|
|
6
9
|
describe('getCurrenciesForProvider', () => {
|
|
7
10
|
const configs = providerConfigs(base.id);
|
|
@@ -515,3 +518,504 @@ describe('computeProviderAvailability', () => {
|
|
|
515
518
|
}
|
|
516
519
|
});
|
|
517
520
|
});
|
|
521
|
+
/**
|
|
522
|
+
* Tests for {@link paymentVerifierToKey} — the reverse index that maps every
|
|
523
|
+
* on-chain identifier (V2 verifier address OR V3 keccak contract id, plus the
|
|
524
|
+
* same for sub-providers) back to the provider key. This is the lookup that
|
|
525
|
+
* decides which verifier a settled payment was proven against. A wrong entry
|
|
526
|
+
* here routes a buyer's proof to the wrong rail.
|
|
527
|
+
*/
|
|
528
|
+
describe('paymentVerifierToKey', () => {
|
|
529
|
+
// Base V2 venmo verifier address (from contracts.ts v2BaseContracts).
|
|
530
|
+
const venmoBaseVerifier = getAddress('0x9a733B55a875D0DB4915c6B36350b24F8AB99dF5');
|
|
531
|
+
// Base V2 zelle-chase sub-provider verifier address.
|
|
532
|
+
const zelleChaseBaseVerifier = getAddress('0xBcD7C6BBcA5869fBefe3E322263EE1090221D7A9');
|
|
533
|
+
it('maps a V2 verifier address (checksummed) to its provider key', () => {
|
|
534
|
+
// Address keys are stored checksummed (getAddress) in the map; a settled
|
|
535
|
+
// V2 deposit carries the verifier address, and this is how the UI decides
|
|
536
|
+
// it was a Venmo payment.
|
|
537
|
+
const map = paymentVerifierToKey(base.id);
|
|
538
|
+
assert.equal(map.get(venmoBaseVerifier), 'venmo');
|
|
539
|
+
});
|
|
540
|
+
it('maps a V3 keccak contract id (lowercased) to its provider key', () => {
|
|
541
|
+
// V3 deposits carry the bytes32 keccak(providerKey); contract-id keys are
|
|
542
|
+
// stored lowercased so callers compare without re-normalizing.
|
|
543
|
+
const map = paymentVerifierToKey(base.id);
|
|
544
|
+
const venmoContractId = providerKeyToContractId('venmo').toLowerCase();
|
|
545
|
+
assert.equal(map.get(venmoContractId), 'venmo');
|
|
546
|
+
});
|
|
547
|
+
it('maps a sub-provider verifier address to the SUB key, not the parent', () => {
|
|
548
|
+
// Sub-providers are flattened into the same map. A Zelle-Chase proof must
|
|
549
|
+
// resolve to 'zelle-chase' (so the bank-specific timing window applies),
|
|
550
|
+
// never collapse to the 'zelle' parent at this layer.
|
|
551
|
+
const map = paymentVerifierToKey(base.id);
|
|
552
|
+
assert.equal(map.get(zelleChaseBaseVerifier), 'zelle-chase');
|
|
553
|
+
});
|
|
554
|
+
it('maps a sub-provider contract id to the sub key', () => {
|
|
555
|
+
const map = paymentVerifierToKey(base.id);
|
|
556
|
+
const chaseContractId = providerKeyToContractId('zelle-chase').toLowerCase();
|
|
557
|
+
assert.equal(map.get(chaseContractId), 'zelle-chase');
|
|
558
|
+
});
|
|
559
|
+
it('with null chainId aggregates every supported chain into one map', () => {
|
|
560
|
+
// Passing null walks all v2 chains. Both the Base AND the Pulsechain venmo
|
|
561
|
+
// verifier addresses must resolve — the cross-chain transaction list relies
|
|
562
|
+
// on a single chain-agnostic lookup.
|
|
563
|
+
const map = paymentVerifierToKey(null);
|
|
564
|
+
const venmoPulseVerifier = getAddress('0x0Dd524dA03f610480aF3F433B411b6016cFBDeab');
|
|
565
|
+
assert.equal(map.get(venmoBaseVerifier), 'venmo');
|
|
566
|
+
assert.equal(map.get(venmoPulseVerifier), 'venmo');
|
|
567
|
+
});
|
|
568
|
+
it('returns undefined for an address that is not a registered verifier', () => {
|
|
569
|
+
const map = paymentVerifierToKey(base.id);
|
|
570
|
+
assert.equal(map.get(getAddress('0x0000000000000000000000000000000000000001')), undefined);
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
/**
|
|
574
|
+
* Tests for {@link providerByContractId} / {@link getProviderByContractId} /
|
|
575
|
+
* {@link getProviderNameByContractId} — keccak-hash → config reverse lookup,
|
|
576
|
+
* including the synthetic configs minted for sub-providers.
|
|
577
|
+
*/
|
|
578
|
+
describe('getProviderByContractId', () => {
|
|
579
|
+
const venmoContractId = providerKeyToContractId('venmo');
|
|
580
|
+
const chaseContractId = providerKeyToContractId('zelle-chase');
|
|
581
|
+
it('resolves a main-provider contract id to its config', () => {
|
|
582
|
+
const config = getProviderByContractId(base.id, venmoContractId);
|
|
583
|
+
assert.equal(config?.key, 'venmo');
|
|
584
|
+
assert.equal(config?.name, 'Venmo');
|
|
585
|
+
});
|
|
586
|
+
it('resolves a sub-provider contract id to a synthetic config with the sub name', () => {
|
|
587
|
+
// Sub-providers get a synthetic PaymentConfig: parent fields cloned, but
|
|
588
|
+
// key/name/contractId/icon overridden to the sub. Confirms the override,
|
|
589
|
+
// not just a parent passthrough.
|
|
590
|
+
const config = getProviderByContractId(base.id, chaseContractId);
|
|
591
|
+
assert.equal(config?.key, 'zelle-chase');
|
|
592
|
+
assert.equal(config?.name, 'Chase');
|
|
593
|
+
assert.equal(config?.subProviders.size, 0, 'synthetic sub config must not re-nest sub-providers');
|
|
594
|
+
});
|
|
595
|
+
it('is case-insensitive on the contract id', () => {
|
|
596
|
+
// Indexer rows may arrive checksum-cased or upper-cased; the lookup
|
|
597
|
+
// lowercases before indexing, so an upper-cased id must still resolve.
|
|
598
|
+
const upper = venmoContractId.toUpperCase().replace('0X', '0x');
|
|
599
|
+
assert.equal(getProviderByContractId(base.id, upper)?.key, 'venmo');
|
|
600
|
+
});
|
|
601
|
+
it('returns null for an unknown contract id', () => {
|
|
602
|
+
const unknown = '0x0000000000000000000000000000000000000000000000000000000000000001';
|
|
603
|
+
assert.equal(getProviderByContractId(base.id, unknown), null);
|
|
604
|
+
});
|
|
605
|
+
it('getProviderNameByContractId returns the name for a known id', () => {
|
|
606
|
+
assert.equal(getProviderNameByContractId(base.id, venmoContractId), 'Venmo');
|
|
607
|
+
});
|
|
608
|
+
it('getProviderNameByContractId echoes the raw contract id back when unknown', () => {
|
|
609
|
+
// Documented fallback: no config → return the id itself (so the UI shows
|
|
610
|
+
// *something* rather than a crash or empty string).
|
|
611
|
+
const unknown = '0x0000000000000000000000000000000000000000000000000000000000000002';
|
|
612
|
+
assert.equal(getProviderNameByContractId(base.id, unknown), unknown);
|
|
613
|
+
});
|
|
614
|
+
it('providerByContractId map contains both main and sub entries keyed lowercased', () => {
|
|
615
|
+
const map = providerByContractId(base.id);
|
|
616
|
+
assert.equal(map.get(venmoContractId.toLowerCase())?.key, 'venmo');
|
|
617
|
+
assert.equal(map.get(chaseContractId.toLowerCase())?.key, 'zelle-chase');
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
/**
|
|
621
|
+
* Tests for {@link providerKeyToVerifiers} / {@link providerKeyToVerifiersMap} —
|
|
622
|
+
* the forward map from a provider key to its V2 verifier contract addresses.
|
|
623
|
+
*/
|
|
624
|
+
describe('providerKeyToVerifiers', () => {
|
|
625
|
+
it('returns the V2 verifier addresses for a configured provider', () => {
|
|
626
|
+
const verifiers = providerKeyToVerifiers(base.id, 'venmo');
|
|
627
|
+
assert.ok(verifiers, 'venmo must have verifiers on Base');
|
|
628
|
+
assert.ok(verifiers.some(v => getAddress(v) === getAddress('0x9a733B55a875D0DB4915c6B36350b24F8AB99dF5')), 'venmo verifier list must contain the Base venmo verifier address');
|
|
629
|
+
});
|
|
630
|
+
it('round-trips: every verifier it returns maps back to the same key', () => {
|
|
631
|
+
// Forward (key→addresses) and reverse (address→key) indices must agree, or
|
|
632
|
+
// a proof verified against an address would be attributed to a different
|
|
633
|
+
// provider than the deposit advertised.
|
|
634
|
+
const verifiers = providerKeyToVerifiers(base.id, 'venmo') ?? [];
|
|
635
|
+
for (const verifier of verifiers) {
|
|
636
|
+
assert.equal(verifierGetsKey(base.id, verifier), 'venmo', `verifier ${verifier} should map back to venmo`);
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
it('returns null for a key that is not a top-level provider config (e.g. a sub-provider)', () => {
|
|
640
|
+
// The map is built from providerConfigs keys only; 'zelle-chase' is a
|
|
641
|
+
// sub-provider and is NOT a top-level key, so the lookup misses → null.
|
|
642
|
+
assert.equal(providerKeyToVerifiers(base.id, 'zelle-chase'), null);
|
|
643
|
+
});
|
|
644
|
+
it('providerKeyToVerifiersMap returns the exact v2Verifiers array of each config', () => {
|
|
645
|
+
const map = providerKeyToVerifiersMap(base.id);
|
|
646
|
+
const config = providerConfigs(base.id).venmo;
|
|
647
|
+
assert.deepEqual(map.get('venmo'), config.v2Verifiers);
|
|
648
|
+
});
|
|
649
|
+
it('manual maps to the zero address (its only "verifier")', () => {
|
|
650
|
+
const verifiers = providerKeyToVerifiers(base.id, 'manual');
|
|
651
|
+
assert.deepEqual(verifiers, ['0x0000000000000000000000000000000000000000']);
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
/**
|
|
655
|
+
* Tests for {@link getPaymentMethodConfig} — resolve a config from EITHER a
|
|
656
|
+
* 20-byte verifier address (V2) OR a 32-byte payment-method hash (V3), with a
|
|
657
|
+
* null-chain "search every chain" path.
|
|
658
|
+
*/
|
|
659
|
+
describe('getPaymentMethodConfig', () => {
|
|
660
|
+
const venmoBaseVerifier = '0x9a733B55a875D0DB4915c6B36350b24F8AB99dF5';
|
|
661
|
+
it('resolves a 20-byte V2 verifier address to its config', () => {
|
|
662
|
+
assert.equal(getPaymentMethodConfig(base.id, venmoBaseVerifier)?.key, 'venmo');
|
|
663
|
+
});
|
|
664
|
+
it('resolves a 32-byte V3 payment-method hash to its config', () => {
|
|
665
|
+
const venmoHash = providerKeyToContractId('venmo');
|
|
666
|
+
assert.equal(getPaymentMethodConfig(base.id, venmoHash)?.key, 'venmo');
|
|
667
|
+
});
|
|
668
|
+
it('with null chainId searches every chain for a V2 verifier address', () => {
|
|
669
|
+
// The Pulsechain venmo verifier is unknown to Base, so this only resolves
|
|
670
|
+
// because null fans the search out across all chains.
|
|
671
|
+
const venmoPulseVerifier = '0x0Dd524dA03f610480aF3F433B411b6016cFBDeab';
|
|
672
|
+
assert.equal(getPaymentMethodConfig(null, venmoPulseVerifier)?.key, 'venmo');
|
|
673
|
+
});
|
|
674
|
+
it('returns null for an unknown verifier address', () => {
|
|
675
|
+
assert.equal(getPaymentMethodConfig(base.id, '0x0000000000000000000000000000000000000001'), null);
|
|
676
|
+
});
|
|
677
|
+
it('returns null for a string that is neither a 20-byte address nor a 32-byte hash', () => {
|
|
678
|
+
// Guard: a 10-char "0xdeadbeef" is not an address and not length 66, so
|
|
679
|
+
// there is nothing to look up.
|
|
680
|
+
assert.equal(getPaymentMethodConfig(base.id, '0xdeadbeef'), null);
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
/**
|
|
684
|
+
* Tests for {@link getPaymentMethodName} — name resolution from a provider key,
|
|
685
|
+
* a sub-provider key, a V3 hash, or a V2 address.
|
|
686
|
+
*/
|
|
687
|
+
describe('getPaymentMethodName', () => {
|
|
688
|
+
it('returns the display name for a main provider key', () => {
|
|
689
|
+
assert.equal(getPaymentMethodName(base.id, 'cashapp'), 'Cash App');
|
|
690
|
+
});
|
|
691
|
+
it('returns the bank name for a sub-provider key', () => {
|
|
692
|
+
assert.equal(getPaymentMethodName(base.id, 'zelle-bofa'), 'Bank of America');
|
|
693
|
+
});
|
|
694
|
+
it('resolves a V3 payment-method hash to the provider name', () => {
|
|
695
|
+
const revolutHash = providerKeyToContractId('revolut');
|
|
696
|
+
assert.equal(getPaymentMethodName(base.id, revolutHash), 'Revolut');
|
|
697
|
+
});
|
|
698
|
+
it('resolves a V2 verifier address to the provider name', () => {
|
|
699
|
+
assert.equal(getPaymentMethodName(base.id, '0x9a733B55a875D0DB4915c6B36350b24F8AB99dF5'), 'Venmo');
|
|
700
|
+
});
|
|
701
|
+
it('returns "Unknown" for an unrecognised 32-byte hash', () => {
|
|
702
|
+
const unknown = '0x0000000000000000000000000000000000000000000000000000000000000003';
|
|
703
|
+
assert.equal(getPaymentMethodName(base.id, unknown), 'Unknown');
|
|
704
|
+
});
|
|
705
|
+
it('returns "Unknown" for an unrecognised verifier address', () => {
|
|
706
|
+
assert.equal(getPaymentMethodName(base.id, '0x0000000000000000000000000000000000000001'), 'Unknown');
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
/**
|
|
710
|
+
* Tests for the minimum-amount helpers: {@link getPaymentMethodMinimum},
|
|
711
|
+
* {@link getPaymentProviderMinimum}, {@link calculateEffectiveMinimum}.
|
|
712
|
+
* A wrong minimum lets a buyer attempt an order the rail will reject.
|
|
713
|
+
*/
|
|
714
|
+
describe('payment minimums', () => {
|
|
715
|
+
it('getPaymentMethodMinimum returns the configured per-provider minimum', () => {
|
|
716
|
+
assert.equal(getPaymentMethodMinimum(base.id, 'venmo'), 1.0);
|
|
717
|
+
});
|
|
718
|
+
it('getPaymentMethodMinimum reflects Chime\'s lower 0.1 minimum (per-provider override)', () => {
|
|
719
|
+
// Chime is the one provider configured below 1.0; this guards the value
|
|
720
|
+
// actually flowing through rather than a hard-coded default.
|
|
721
|
+
assert.equal(getPaymentMethodMinimum(base.id, 'chime'), 0.1);
|
|
722
|
+
});
|
|
723
|
+
it('getPaymentMethodMinimum returns 0 for the manual sentinel', () => {
|
|
724
|
+
assert.equal(getPaymentMethodMinimum(base.id, 'manual'), 0.0);
|
|
725
|
+
});
|
|
726
|
+
it('getPaymentMethodMinimum returns 0 for an unknown provider key', () => {
|
|
727
|
+
assert.equal(getPaymentMethodMinimum(base.id, 'nonexistent'), 0);
|
|
728
|
+
});
|
|
729
|
+
it('getPaymentMethodMinimum defaults to Base config when chainId is null', () => {
|
|
730
|
+
assert.equal(getPaymentMethodMinimum(null, 'venmo'), 1.0);
|
|
731
|
+
});
|
|
732
|
+
it('getPaymentProviderMinimum is a thin wrapper over getPaymentMethodMinimum', () => {
|
|
733
|
+
assert.equal(getPaymentProviderMinimum(base.id, 'venmo'), getPaymentMethodMinimum(base.id, 'venmo'));
|
|
734
|
+
assert.equal(getPaymentProviderMinimum(base.id, 'chime'), 0.1);
|
|
735
|
+
});
|
|
736
|
+
describe('calculateEffectiveMinimum', () => {
|
|
737
|
+
const token = getDefaultToken(base.id); // USDC, 6 decimals
|
|
738
|
+
const ratesFor = (keys) => new Map(keys.map(key => [key, new Map()]));
|
|
739
|
+
it('uses the user minimum when it exceeds every provider minimum', () => {
|
|
740
|
+
// User asks for a $5 floor; venmo's rail floor is $1. The effective floor
|
|
741
|
+
// is the higher of the two = $5 (5_000000 base units at 6 decimals).
|
|
742
|
+
const result = calculateEffectiveMinimum(base.id, 5000000n, token, ratesFor(['venmo']));
|
|
743
|
+
assert.equal(result.userMinAmountWei, 5000000n);
|
|
744
|
+
assert.equal(result.providerMinAmountWei, 1000000n);
|
|
745
|
+
assert.equal(result.effectiveMinAmountWei, 5000000n);
|
|
746
|
+
assert.equal(result.formattedEffectiveMin, '5');
|
|
747
|
+
});
|
|
748
|
+
it('raises the floor to the provider minimum when the user minimum is lower', () => {
|
|
749
|
+
// User floor $0; venmo rail floor $1 wins.
|
|
750
|
+
const result = calculateEffectiveMinimum(base.id, 0n, token, ratesFor(['venmo']));
|
|
751
|
+
assert.equal(result.providerMinAmountWei, 1000000n);
|
|
752
|
+
assert.equal(result.effectiveMinAmountWei, 1000000n);
|
|
753
|
+
assert.equal(result.formattedEffectiveMin, '1');
|
|
754
|
+
});
|
|
755
|
+
it('takes the HIGHEST provider minimum across multiple providers', () => {
|
|
756
|
+
// Chime ($0.1) + Venmo ($1) → the provider floor is the max ($1), not the
|
|
757
|
+
// first, the min, or a sum. This is the branch that protects against a
|
|
758
|
+
// mixed-rail deposit being floored too low for one of its rails.
|
|
759
|
+
const result = calculateEffectiveMinimum(base.id, 0n, token, ratesFor(['chime', 'venmo']));
|
|
760
|
+
assert.equal(result.providerMinAmountWei, 1000000n);
|
|
761
|
+
});
|
|
762
|
+
it('treats the provider minimum as zero when no provider rates are supplied', () => {
|
|
763
|
+
// Without rates there is no rail floor to enforce; only the user floor
|
|
764
|
+
// applies.
|
|
765
|
+
const result = calculateEffectiveMinimum(base.id, 2000000n, token, undefined);
|
|
766
|
+
assert.equal(result.providerMinAmountWei, 0n);
|
|
767
|
+
assert.equal(result.effectiveMinAmountWei, 2000000n);
|
|
768
|
+
});
|
|
769
|
+
it('accepts the user minimum as a decimal string and parses it to bigint', () => {
|
|
770
|
+
// Callers pass either bigint or string; the string path must BigInt()
|
|
771
|
+
// cleanly and not drift.
|
|
772
|
+
const result = calculateEffectiveMinimum(base.id, '3000000', token, ratesFor(['venmo']));
|
|
773
|
+
assert.equal(result.userMinAmountWei, 3000000n);
|
|
774
|
+
assert.equal(result.effectiveMinAmountWei, 3000000n);
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
});
|
|
778
|
+
/**
|
|
779
|
+
* Tests for the "is this payment method known?" predicates. These gate whether
|
|
780
|
+
* the UI logs an error for an unrecognised method or silently treats it as a
|
|
781
|
+
* known external integration.
|
|
782
|
+
*/
|
|
783
|
+
describe('payment method known predicates', () => {
|
|
784
|
+
const pendleId = '0x5908bb0c9b87763ac6171d4104847667e7f02b4c47b574fe890c1f439ed128bb';
|
|
785
|
+
const venmoBaseVerifier = '0x9a733B55a875D0DB4915c6B36350b24F8AB99dF5';
|
|
786
|
+
it('isExternalPaymentMethod is true for a registered external id (Pendle)', () => {
|
|
787
|
+
assert.equal(isExternalPaymentMethod(pendleId), true);
|
|
788
|
+
});
|
|
789
|
+
it('isExternalPaymentMethod is false for an id not in the external registry', () => {
|
|
790
|
+
assert.equal(isExternalPaymentMethod('0x0000000000000000000000000000000000000000000000000000000000000009'), false);
|
|
791
|
+
});
|
|
792
|
+
it('getExternalPaymentMethod returns the metadata for a known external id', () => {
|
|
793
|
+
const meta = getExternalPaymentMethod(pendleId);
|
|
794
|
+
assert.equal(meta?.name, 'pendle');
|
|
795
|
+
assert.equal(meta?.chainId, 8453);
|
|
796
|
+
});
|
|
797
|
+
it('getExternalPaymentMethod returns undefined for an unknown id', () => {
|
|
798
|
+
assert.equal(getExternalPaymentMethod('0x0000000000000000000000000000000000000000000000000000000000000009'), undefined);
|
|
799
|
+
});
|
|
800
|
+
it('externalPaymentMethods registry keys round-trip through isExternalPaymentMethod', () => {
|
|
801
|
+
// Every key in the registry must be reported external — the predicate and
|
|
802
|
+
// the registry are the same source of truth.
|
|
803
|
+
for (const id of Object.keys(externalPaymentMethods)) {
|
|
804
|
+
assert.equal(isExternalPaymentMethod(id), true);
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
it('isPaymentMethodKnown is true for a registered V2 verifier address', () => {
|
|
808
|
+
assert.equal(isPaymentMethodKnown(base.id, venmoBaseVerifier), true);
|
|
809
|
+
});
|
|
810
|
+
it('isPaymentMethodKnown is false for an unregistered verifier address', () => {
|
|
811
|
+
assert.equal(isPaymentMethodKnown(base.id, '0x0000000000000000000000000000000000000001'), false);
|
|
812
|
+
});
|
|
813
|
+
it('isPaymentMethodIdKnown short-circuits true for an external id without a verifier address', () => {
|
|
814
|
+
assert.equal(isPaymentMethodIdKnown(base.id, pendleId), true);
|
|
815
|
+
});
|
|
816
|
+
it('isPaymentMethodIdKnown is false for an unknown id with no verifier address to fall back on', () => {
|
|
817
|
+
assert.equal(isPaymentMethodIdKnown(base.id, '0x0000000000000000000000000000000000000000000000000000000000000009'), false);
|
|
818
|
+
});
|
|
819
|
+
it('isPaymentMethodIdKnown falls back to the verifier address when the id is not external', () => {
|
|
820
|
+
// Unknown id, but the accompanying verifier address is a real Venmo
|
|
821
|
+
// verifier → known via providerConfigs.
|
|
822
|
+
const unknownId = '0x0000000000000000000000000000000000000000000000000000000000000009';
|
|
823
|
+
assert.equal(isPaymentMethodIdKnown(base.id, unknownId, venmoBaseVerifier), true);
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
/**
|
|
827
|
+
* Tests for sub-provider identity helpers: {@link isSubProviderKey},
|
|
828
|
+
* {@link getParentProviderKey}, {@link getExtensionPlatform}. Sub-providers
|
|
829
|
+
* (the Zelle banks) must collapse correctly to parents for maker lookups but
|
|
830
|
+
* fan out to bank-specific extension platforms for proof capture.
|
|
831
|
+
*/
|
|
832
|
+
describe('sub-provider identity helpers', () => {
|
|
833
|
+
it('isSubProviderKey is true for a Zelle sub key', () => {
|
|
834
|
+
assert.equal(isSubProviderKey('zelle-chase'), true);
|
|
835
|
+
});
|
|
836
|
+
it('isSubProviderKey is false for the Zelle parent key', () => {
|
|
837
|
+
// The parent is NOT a sub-provider — collapsing it would be wrong.
|
|
838
|
+
assert.equal(isSubProviderKey('zelle'), false);
|
|
839
|
+
});
|
|
840
|
+
it('isSubProviderKey is false for an unrelated main provider', () => {
|
|
841
|
+
assert.equal(isSubProviderKey('venmo'), false);
|
|
842
|
+
});
|
|
843
|
+
it('getParentProviderKey returns the parent for a sub-provider', () => {
|
|
844
|
+
assert.equal(getParentProviderKey('zelle-chase'), 'zelle');
|
|
845
|
+
assert.equal(getParentProviderKey('zelle-bofa'), 'zelle');
|
|
846
|
+
});
|
|
847
|
+
it('getParentProviderKey returns null for a non-sub-provider key', () => {
|
|
848
|
+
assert.equal(getParentProviderKey('venmo'), null);
|
|
849
|
+
});
|
|
850
|
+
it('getParentProviderKey returns null for an unknown dashed key', () => {
|
|
851
|
+
// Defensive: a 'foo-bar' shape that is not a registered sub-provider must
|
|
852
|
+
// not be treated as having a parent.
|
|
853
|
+
assert.equal(getParentProviderKey('foo-bar'), null);
|
|
854
|
+
});
|
|
855
|
+
it('getExtensionPlatform maps each Zelle sub to its bank-specific extension folder', () => {
|
|
856
|
+
// The extension folder name differs from the sub key ('zelle-bofa' →
|
|
857
|
+
// 'bankofamerica'); sending the wrong folder makes the extension fail to
|
|
858
|
+
// capture the proof.
|
|
859
|
+
assert.equal(getExtensionPlatform('zelle-chase'), 'chase');
|
|
860
|
+
assert.equal(getExtensionPlatform('zelle-bofa'), 'bankofamerica');
|
|
861
|
+
assert.equal(getExtensionPlatform('zelle-citi'), 'citi');
|
|
862
|
+
});
|
|
863
|
+
it('getExtensionPlatform returns the key itself for a main provider', () => {
|
|
864
|
+
assert.equal(getExtensionPlatform('venmo'), 'venmo');
|
|
865
|
+
assert.equal(getExtensionPlatform('revolut'), 'revolut');
|
|
866
|
+
});
|
|
867
|
+
});
|
|
868
|
+
/**
|
|
869
|
+
* Tests for {@link providerRequiresMetadataIndex} — the per-provider gate for
|
|
870
|
+
* whether a Buyer TEE proof must carry the metadata `index`. Sending it to a
|
|
871
|
+
* provider that does not expect it (or omitting it for one that does) makes the
|
|
872
|
+
* enclave reject the capture, so this must be exact per the zkp2p 0.6.0 spec.
|
|
873
|
+
*/
|
|
874
|
+
describe('providerRequiresMetadataIndex', () => {
|
|
875
|
+
it('requires the index for Venmo, Cash App, Revolut, and Zelle', () => {
|
|
876
|
+
assert.equal(providerRequiresMetadataIndex('venmo'), true);
|
|
877
|
+
assert.equal(providerRequiresMetadataIndex('cashapp'), true);
|
|
878
|
+
assert.equal(providerRequiresMetadataIndex('revolut'), true);
|
|
879
|
+
assert.equal(providerRequiresMetadataIndex('zelle'), true);
|
|
880
|
+
});
|
|
881
|
+
it('requires the index for a Zelle sub-bank via its parent prefix', () => {
|
|
882
|
+
// Keyed on the parent: every 'zelle-*' sub qualifies because its parent is
|
|
883
|
+
// Zelle.
|
|
884
|
+
assert.equal(providerRequiresMetadataIndex('zelle-chase'), true);
|
|
885
|
+
assert.equal(providerRequiresMetadataIndex('zelle-citi'), true);
|
|
886
|
+
});
|
|
887
|
+
it('OMITS the index for Wise, PayPal, and Monzo', () => {
|
|
888
|
+
assert.equal(providerRequiresMetadataIndex('wise'), false);
|
|
889
|
+
assert.equal(providerRequiresMetadataIndex('paypal'), false);
|
|
890
|
+
assert.equal(providerRequiresMetadataIndex('monzo'), false);
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
/**
|
|
894
|
+
* Tests for the icon helpers: {@link getProviderIconStack} (parent+sub stack)
|
|
895
|
+
* and {@link getPaymentIcon} (single iconify string).
|
|
896
|
+
*/
|
|
897
|
+
describe('provider icon helpers', () => {
|
|
898
|
+
it('getProviderIconStack returns a single entry for a main provider', () => {
|
|
899
|
+
const stack = getProviderIconStack(base.id, 'venmo');
|
|
900
|
+
assert.equal(stack.length, 1);
|
|
901
|
+
assert.equal(stack[0].icon, 'ion:logo-venmo');
|
|
902
|
+
assert.equal(stack[0].name, 'Venmo');
|
|
903
|
+
});
|
|
904
|
+
it('getProviderIconStack returns [parent, sub] for a sub-provider', () => {
|
|
905
|
+
// The UI badges the parent brand behind the bank glyph, so both must be
|
|
906
|
+
// present and ordered parent-first.
|
|
907
|
+
const stack = getProviderIconStack(base.id, 'zelle-chase');
|
|
908
|
+
assert.equal(stack.length, 2);
|
|
909
|
+
assert.equal(stack[0].name, 'Zelle');
|
|
910
|
+
assert.equal(stack[0].icon, 'simple-icons:zelle');
|
|
911
|
+
assert.equal(stack[1].name, 'Chase');
|
|
912
|
+
assert.equal(stack[1].icon, 'simple-icons:chase');
|
|
913
|
+
});
|
|
914
|
+
it('getProviderIconStack falls back to the manual icon for an unknown key', () => {
|
|
915
|
+
const stack = getProviderIconStack(base.id, 'nonexistent');
|
|
916
|
+
assert.equal(stack.length, 1);
|
|
917
|
+
assert.equal(stack[0].icon, 'mdi:help-circle');
|
|
918
|
+
assert.equal(stack[0].name, 'Unknown');
|
|
919
|
+
});
|
|
920
|
+
it('getPaymentIcon returns the iconify string for a main provider', () => {
|
|
921
|
+
assert.equal(getPaymentIcon(base.id, 'venmo'), 'ion:logo-venmo');
|
|
922
|
+
});
|
|
923
|
+
it('getPaymentIcon resolves a sub-provider icon directly', () => {
|
|
924
|
+
assert.equal(getPaymentIcon(base.id, 'zelle-chase'), 'simple-icons:chase');
|
|
925
|
+
});
|
|
926
|
+
it('getPaymentIcon falls back to the manual icon for an unknown key', () => {
|
|
927
|
+
assert.equal(getPaymentIcon(base.id, 'nonexistent'), 'mdi:help-circle');
|
|
928
|
+
});
|
|
929
|
+
it('getPaymentIcon tolerates a null chainId (defaults to Pulsechain config)', () => {
|
|
930
|
+
assert.equal(getPaymentIcon(null, 'venmo'), 'ion:logo-venmo');
|
|
931
|
+
});
|
|
932
|
+
});
|
|
933
|
+
/**
|
|
934
|
+
* Tests for {@link getPlatformRisk} — the reputation cap/cooldown config per
|
|
935
|
+
* provider, with a documented medium-risk default for unknown providers.
|
|
936
|
+
*/
|
|
937
|
+
describe('getPlatformRisk', () => {
|
|
938
|
+
it('returns the high-risk profile for Venmo (1x cap, cooldown)', () => {
|
|
939
|
+
const risk = getPlatformRisk(base.id, 'venmo');
|
|
940
|
+
assert.equal(risk.riskLevel, 'high');
|
|
941
|
+
assert.equal(risk.capMultiplier, 1);
|
|
942
|
+
assert.equal(risk.hasCooldown, true);
|
|
943
|
+
assert.equal(risk.minimumTier, null);
|
|
944
|
+
});
|
|
945
|
+
it('returns the low-risk profile for Revolut (5x cap, no cooldown)', () => {
|
|
946
|
+
const risk = getPlatformRisk(base.id, 'revolut');
|
|
947
|
+
assert.equal(risk.riskLevel, 'low');
|
|
948
|
+
assert.equal(risk.capMultiplier, 5);
|
|
949
|
+
assert.equal(risk.hasCooldown, false);
|
|
950
|
+
});
|
|
951
|
+
it('falls back to the medium-risk default for an unknown provider', () => {
|
|
952
|
+
// Unknown provider → DEFAULT_PLATFORM_RISK (medium, 1x, cooldown). This is
|
|
953
|
+
// the safe-by-default branch: an unrecognised rail must not get a generous
|
|
954
|
+
// cap.
|
|
955
|
+
const risk = getPlatformRisk(base.id, 'nonexistent');
|
|
956
|
+
assert.equal(risk.riskLevel, 'medium');
|
|
957
|
+
assert.equal(risk.capMultiplier, 1);
|
|
958
|
+
assert.equal(risk.hasCooldown, true);
|
|
959
|
+
assert.equal(risk.minimumTier, null);
|
|
960
|
+
});
|
|
961
|
+
});
|
|
962
|
+
/**
|
|
963
|
+
* Tests for {@link fromPeerTier} — the peer-tier-name → universal-tier-name
|
|
964
|
+
* map. A broken entry mis-tiers a taker, changing their caps and cooldowns.
|
|
965
|
+
*/
|
|
966
|
+
describe('fromPeerTier', () => {
|
|
967
|
+
it('maps the peer peasant name to the neutral-zero tier', () => {
|
|
968
|
+
assert.equal(fromPeerTier('peer_peasant'), 'peasant');
|
|
969
|
+
});
|
|
970
|
+
it('maps the bare "peer" name to the neutral tier', () => {
|
|
971
|
+
// 'peer' (no suffix) is the neutral tier, NOT peasant — an easy off-by-one
|
|
972
|
+
// in the naming scheme.
|
|
973
|
+
assert.equal(fromPeerTier('peer'), 'neutral');
|
|
974
|
+
});
|
|
975
|
+
it('throws on an invalid peer tier name', () => {
|
|
976
|
+
assert.throws(() => fromPeerTier('not_a_tier'), /Invalid peer tier/);
|
|
977
|
+
});
|
|
978
|
+
it('round-trips with tierToPeerTier for every universal tier', () => {
|
|
979
|
+
// The two maps must be exact inverses, or a tier that survives one
|
|
980
|
+
// conversion would be corrupted by the other.
|
|
981
|
+
for (const tier of Object.values(tiers)) {
|
|
982
|
+
assert.equal(fromPeerTier(tierToPeerTier[tier]), tier);
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
});
|
|
986
|
+
/**
|
|
987
|
+
* Tests for {@link getAllPaymentMethods} / {@link verifiablePaymentMethods} —
|
|
988
|
+
* the lists that populate provider pickers. Disabled providers must be hidden;
|
|
989
|
+
* manual must be present in the full list but absent from the verifiable one.
|
|
990
|
+
*/
|
|
991
|
+
describe('getAllPaymentMethods and verifiablePaymentMethods', () => {
|
|
992
|
+
it('includes enabled providers and excludes config-disabled ones', () => {
|
|
993
|
+
const methods = getAllPaymentMethods(base.id);
|
|
994
|
+
assert.ok(methods.includes('venmo'), 'venmo (enabled) should be listed');
|
|
995
|
+
assert.ok(methods.includes('revolut'), 'revolut (enabled) should be listed');
|
|
996
|
+
assert.ok(!methods.includes('paypal'), 'paypal (disabled:true) must be hidden');
|
|
997
|
+
assert.ok(!methods.includes('n26'), 'n26 (disabled:true) must be hidden');
|
|
998
|
+
});
|
|
999
|
+
it('includes the manual sentinel in the full list', () => {
|
|
1000
|
+
assert.ok(getAllPaymentMethods(base.id).includes('manual'));
|
|
1001
|
+
});
|
|
1002
|
+
it('does NOT include sub-providers unless explicitly requested', () => {
|
|
1003
|
+
assert.ok(!getAllPaymentMethods(base.id).includes('zelle-chase'));
|
|
1004
|
+
});
|
|
1005
|
+
it('includes Zelle sub-providers when includeSubProviders is true', () => {
|
|
1006
|
+
const methods = getAllPaymentMethods(base.id, true);
|
|
1007
|
+
assert.ok(methods.includes('zelle-chase'));
|
|
1008
|
+
assert.ok(methods.includes('zelle-bofa'));
|
|
1009
|
+
assert.ok(methods.includes('zelle-citi'));
|
|
1010
|
+
});
|
|
1011
|
+
it('verifiablePaymentMethods drops manual but keeps real providers', () => {
|
|
1012
|
+
const methods = verifiablePaymentMethods(base.id);
|
|
1013
|
+
assert.ok(!methods.includes('manual'), 'manual is not verifiable');
|
|
1014
|
+
assert.ok(methods.includes('venmo'));
|
|
1015
|
+
});
|
|
1016
|
+
it('verifiablePaymentMethods still fans out sub-providers when requested, minus manual', () => {
|
|
1017
|
+
const methods = verifiablePaymentMethods(base.id, true);
|
|
1018
|
+
assert.ok(methods.includes('zelle-chase'));
|
|
1019
|
+
assert.ok(!methods.includes('manual'));
|
|
1020
|
+
});
|
|
1021
|
+
});
|
package/dist/reputation.test.js
CHANGED
|
@@ -197,11 +197,29 @@ describe('formatCooldown', () => {
|
|
|
197
197
|
assert.equal(formatCooldown(6), '6h cooldown');
|
|
198
198
|
assert.equal(formatCooldown(12), '12h cooldown');
|
|
199
199
|
});
|
|
200
|
+
it('renders sub-hour durations in minutes', () => {
|
|
201
|
+
// The `hours < 1` branch converts the fractional hour to whole
|
|
202
|
+
// minutes. No tier currently configures a sub-hour cooldown, but
|
|
203
|
+
// formatCooldown is a standalone formatter the JSDoc documents as
|
|
204
|
+
// producing "30m cooldown" — pin it so a future fractional tier
|
|
205
|
+
// (or a per-platform override) doesn't silently render "0.5h".
|
|
206
|
+
assert.equal(formatCooldown(0.5), '30m cooldown');
|
|
207
|
+
assert.equal(formatCooldown(0.25), '15m cooldown');
|
|
208
|
+
});
|
|
200
209
|
});
|
|
201
210
|
describe('formatCap', () => {
|
|
202
211
|
it('should format null as "Locked"', () => {
|
|
203
212
|
assert.equal(formatCap(null), 'Locked');
|
|
204
213
|
});
|
|
214
|
+
it('formats the unlimited sentinel as "No limit"', () => {
|
|
215
|
+
// getEffectiveCapDisplayUsd returns Infinity for president, which
|
|
216
|
+
// flows straight into formatCap. Without this branch a VIP would
|
|
217
|
+
// see "$Infinity max". This is the display end of the
|
|
218
|
+
// UNLIMITED_CAP_BASE_UNITS path, so it is a reachable production
|
|
219
|
+
// state, not a defensive guard.
|
|
220
|
+
assert.equal(formatCap(Infinity), 'No limit');
|
|
221
|
+
assert.equal(formatCap(getEffectiveCapDisplayUsd(UNLIMITED_CAP_BASE_UNITS, USDC)), 'No limit');
|
|
222
|
+
});
|
|
205
223
|
it('should format cap with dollar sign', () => {
|
|
206
224
|
assert.equal(formatCap(100), '$100 max');
|
|
207
225
|
assert.equal(formatCap(1000), '$1,000 max');
|
package/dist/units.test.js
CHANGED
|
@@ -320,6 +320,18 @@ describe('formatAmount', () => {
|
|
|
320
320
|
it('should return null for null amount', () => {
|
|
321
321
|
assert.equal(formatAmount(mockToken, null), null);
|
|
322
322
|
});
|
|
323
|
+
it('uses fewer decimals once the integer part reaches double digits', () => {
|
|
324
|
+
// formatAmount widens precision only for single-digit amounts
|
|
325
|
+
// (integerDigits < 10) so small differences stay visible. At ten or
|
|
326
|
+
// more whole units it falls back to baseDecimals — for a 6-decimal
|
|
327
|
+
// token that is min(6 - 4, 8) = 2 places. 12.000000 → "12.00",
|
|
328
|
+
// not "12.0000". This is the false side of the integerDigits ternary.
|
|
329
|
+
assert.equal(formatAmount(mockToken, 12000000n), '12.00');
|
|
330
|
+
// 1234.56789 truncates to the same 2 base decimals (and gains a
|
|
331
|
+
// thousands separator), confirming the wide-precision branch is not
|
|
332
|
+
// taken for large amounts.
|
|
333
|
+
assert.equal(formatAmount(mockToken, 1234567890n), '1,234.56');
|
|
334
|
+
});
|
|
323
335
|
it('should allow overriding options', () => {
|
|
324
336
|
const result = formatAmount(mockToken, 1234567n, { truncateDecimals: 4 });
|
|
325
337
|
assert.ok(result);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@provex/utils",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.2-rc.20260702180200.fd5d003",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -129,9 +129,6 @@
|
|
|
129
129
|
"skills",
|
|
130
130
|
"README.md"
|
|
131
131
|
],
|
|
132
|
-
"dependencies": {
|
|
133
|
-
"@zkp2p/providers": "github:zkp2p/providers#7751475b73b533b342d8518628578b33ea191a19"
|
|
134
|
-
},
|
|
135
132
|
"peerDependencies": {
|
|
136
133
|
"viem": "^2.0.0"
|
|
137
134
|
},
|