@provex/utils 1.6.2 → 1.7.0-rc.20260522201545.020a3eb
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/LICENSE +21 -0
- package/README.md +3 -1
- package/dist/consent.d.ts +111 -0
- package/dist/consent.js +194 -0
- package/dist/consent.test.d.ts +1 -0
- package/dist/consent.test.js +247 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/payment.d.ts +64 -2
- package/dist/payment.js +80 -5
- package/dist/payment.test.js +212 -9
- package/dist/reputation.d.ts +59 -20
- package/dist/reputation.js +75 -26
- package/dist/reputation.test.js +73 -25
- package/dist/transports.js +38 -16
- package/package.json +8 -3
- package/skills/utils-integration/SKILL.md +161 -0
package/dist/payment.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* - Low risk (Revolut, Wise, Monzo, MercadoPago): 5x cap multiplier, no cooldown
|
|
18
18
|
* - Medium risk (Zelle): 1.5x cap multiplier, cooldown applies
|
|
19
19
|
* - High risk (Venmo, CashApp): 1x cap multiplier, cooldown applies
|
|
20
|
-
* - Highest risk (PayPal): 0.75x cap multiplier, requires
|
|
20
|
+
* - Highest risk (PayPal): 0.75x cap multiplier, requires Plus tier or higher
|
|
21
21
|
*
|
|
22
22
|
* @see {@link reputation} for tier system
|
|
23
23
|
* @see {@link currencies} for supported currencies per provider
|
|
@@ -155,7 +155,15 @@ export type Limits = {
|
|
|
155
155
|
/** Send link options */
|
|
156
156
|
export type SendLinkOptions = {
|
|
157
157
|
username: string;
|
|
158
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Fiat amount, formatted as a decimal string (e.g. `"1.00"`, `"100.50"`),
|
|
160
|
+
* suitable for inlining directly into a payment-provider URL's `amount`
|
|
161
|
+
* query parameter. Providers that pre-fill amount fields (Venmo today)
|
|
162
|
+
* expect a dollar-units number, NOT token base units. The caller is
|
|
163
|
+
* responsible for converting from on-chain token amounts via
|
|
164
|
+
* `convertTokenInputToCurrency` + `formatUnits` before passing this in.
|
|
165
|
+
*/
|
|
166
|
+
amount: string;
|
|
159
167
|
subProvider?: SubProviderKey;
|
|
160
168
|
qrCode?: boolean;
|
|
161
169
|
};
|
|
@@ -177,6 +185,8 @@ export type PaymentSubProviderConfig = {
|
|
|
177
185
|
* e.g., 'zelle-chase' -> 'chase', 'zelle-bofa' -> 'bofa'
|
|
178
186
|
*/
|
|
179
187
|
extensionPlatform: ExtensionPlatformKey;
|
|
188
|
+
/** Whether the extension sends amounts as integer cents. Defaults to false (dollar decimal). */
|
|
189
|
+
amountInCents?: boolean;
|
|
180
190
|
};
|
|
181
191
|
/** Payment config */
|
|
182
192
|
export type PaymentConfig = {
|
|
@@ -431,6 +441,58 @@ export declare const getAllPaymentMethods: (chainId: ChainId, includeSubProvider
|
|
|
431
441
|
* @returns All verifiable payment methods
|
|
432
442
|
*/
|
|
433
443
|
export declare const verifiablePaymentMethods: (chainId: ChainId, includeSubProviders?: boolean) => ProviderKey[];
|
|
444
|
+
/**
|
|
445
|
+
* Compute every payment-method id (lowercased, 32-byte keccak hex) that
|
|
446
|
+
* attaching the given parent provider would attempt to register on a deposit.
|
|
447
|
+
*
|
|
448
|
+
* For a provider with no sub-providers this is just `[providerKeyToContractId(key)]`.
|
|
449
|
+
* For a provider with sub-providers (currently only Zelle), the contract does
|
|
450
|
+
* NOT accept the parent key — the UI fans it out into one row per sub-provider.
|
|
451
|
+
* The returned list mirrors what {@link providerConfigs}-driven submission
|
|
452
|
+
* code (e.g. AddPlatformModal) would put on the wire.
|
|
453
|
+
*
|
|
454
|
+
* Keys are lowercased so callers can compare directly against
|
|
455
|
+
* `Deposit.attachedPaymentMethodIds` without per-call normalization.
|
|
456
|
+
*/
|
|
457
|
+
export declare function getProviderPaymentMethodIds(config: PaymentConfig): string[];
|
|
458
|
+
/**
|
|
459
|
+
* Args for {@link computeProviderAvailability} and {@link availableProviderConfigs}.
|
|
460
|
+
*/
|
|
461
|
+
export interface ProviderAvailabilityArgs {
|
|
462
|
+
chainId: ChainId;
|
|
463
|
+
/**
|
|
464
|
+
* Lowercased hex of every `paymentMethodId` already attached to the deposit
|
|
465
|
+
* (active OR inactive). Source from {@link Deposit.attachedPaymentMethodIds}.
|
|
466
|
+
* The contract reverts on re-add of any hash in this set, so even inactive
|
|
467
|
+
* rows count as unavailable.
|
|
468
|
+
*/
|
|
469
|
+
attachedPaymentMethodIds: Set<string>;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Decide, for every non-manual non-disabled provider on `chainId`, whether
|
|
473
|
+
* it can still be attached to a deposit given the deposit's already-attached
|
|
474
|
+
* payment method id hashes.
|
|
475
|
+
*
|
|
476
|
+
* Important — a parent provider is unavailable if ANY of its candidate hashes
|
|
477
|
+
* (own contractId plus every sub-provider's contractId) is already attached.
|
|
478
|
+
* This matches the contract's atomicity: `addPaymentMethods` submits the full
|
|
479
|
+
* fan-out in one transaction and reverts with `PaymentMethodAlreadyExists`
|
|
480
|
+
* the moment one row collides. A maker who already has Zelle-Chase attached
|
|
481
|
+
* cannot re-add the Zelle parent without colliding on the Chase sub-row.
|
|
482
|
+
*
|
|
483
|
+
* `manual` is always excluded from both lists.
|
|
484
|
+
*/
|
|
485
|
+
export declare function computeProviderAvailability(args: ProviderAvailabilityArgs): {
|
|
486
|
+
available: PaymentConfig[];
|
|
487
|
+
unavailable: PaymentConfig[];
|
|
488
|
+
};
|
|
489
|
+
/**
|
|
490
|
+
* Convenience wrapper around {@link computeProviderAvailability} that returns
|
|
491
|
+
* only the available list. Use this on call sites that don't need the
|
|
492
|
+
* unavailable set (e.g. counting providers remaining to gate an Add Platform
|
|
493
|
+
* button).
|
|
494
|
+
*/
|
|
495
|
+
export declare function availableProviderConfigs(args: ProviderAvailabilityArgs): PaymentConfig[];
|
|
434
496
|
/**
|
|
435
497
|
* Get supported currencies for a payment provider.
|
|
436
498
|
* Returns only the currencies that are in the provider's supported list.
|
package/dist/payment.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* - Low risk (Revolut, Wise, Monzo, MercadoPago): 5x cap multiplier, no cooldown
|
|
18
18
|
* - Medium risk (Zelle): 1.5x cap multiplier, cooldown applies
|
|
19
19
|
* - High risk (Venmo, CashApp): 1x cap multiplier, cooldown applies
|
|
20
|
-
* - Highest risk (PayPal): 0.75x cap multiplier, requires
|
|
20
|
+
* - Highest risk (PayPal): 0.75x cap multiplier, requires Plus tier or higher
|
|
21
21
|
*
|
|
22
22
|
* @see {@link reputation} for tier system
|
|
23
23
|
* @see {@link currencies} for supported currencies per provider
|
|
@@ -587,8 +587,11 @@ export const providerConfigs = (chainId) => {
|
|
|
587
587
|
SUPPORTED_CURRENCIES.EUR.contractId,
|
|
588
588
|
],
|
|
589
589
|
color: '#088177',
|
|
590
|
+
// Aligned to peer's medium-risk pattern (1.5x cap multiplier, has
|
|
591
|
+
// cooldown, no min tier) — matches the `medium` riskLevel label.
|
|
592
|
+
// Previously sat at 1x, which is peer's high-risk multiplier.
|
|
590
593
|
riskLevel: 'medium',
|
|
591
|
-
capMultiplier: 1,
|
|
594
|
+
capMultiplier: 1.5,
|
|
592
595
|
hasCooldown: true,
|
|
593
596
|
minimumTier: null,
|
|
594
597
|
disabled: true,
|
|
@@ -645,8 +648,11 @@ export const providerConfigs = (chainId) => {
|
|
|
645
648
|
SUPPORTED_CURRENCIES.USD.contractId,
|
|
646
649
|
],
|
|
647
650
|
color: '#1EC677',
|
|
651
|
+
// Aligned to peer's medium-risk pattern (1.5x cap multiplier, has
|
|
652
|
+
// cooldown, no min tier) — matches the `medium` riskLevel label.
|
|
653
|
+
// Previously sat at 1x, which is peer's high-risk multiplier.
|
|
648
654
|
riskLevel: 'medium',
|
|
649
|
-
capMultiplier: 1,
|
|
655
|
+
capMultiplier: 1.5,
|
|
650
656
|
hasCooldown: true,
|
|
651
657
|
minimumTier: null,
|
|
652
658
|
disabled: true,
|
|
@@ -668,8 +674,11 @@ export const providerConfigs = (chainId) => {
|
|
|
668
674
|
SUPPORTED_CURRENCIES.CAD.contractId,
|
|
669
675
|
],
|
|
670
676
|
color: '#4A5568',
|
|
677
|
+
// Aligned to peer's medium-risk pattern (1.5x cap multiplier, has
|
|
678
|
+
// cooldown, no min tier) — matches the `medium` riskLevel label.
|
|
679
|
+
// Previously sat at 1x, which is peer's high-risk multiplier.
|
|
671
680
|
riskLevel: 'medium',
|
|
672
|
-
capMultiplier: 1,
|
|
681
|
+
capMultiplier: 1.5,
|
|
673
682
|
hasCooldown: true,
|
|
674
683
|
minimumTier: null,
|
|
675
684
|
disabled: true,
|
|
@@ -691,8 +700,11 @@ export const providerConfigs = (chainId) => {
|
|
|
691
700
|
SUPPORTED_CURRENCIES.INR.contractId,
|
|
692
701
|
],
|
|
693
702
|
color: '#2D3748',
|
|
703
|
+
// Aligned to peer's medium-risk pattern (1.5x cap multiplier, has
|
|
704
|
+
// cooldown, no min tier) — matches the `medium` riskLevel label.
|
|
705
|
+
// Previously sat at 1x, which is peer's high-risk multiplier.
|
|
694
706
|
riskLevel: 'medium',
|
|
695
|
-
capMultiplier: 1,
|
|
707
|
+
capMultiplier: 1.5,
|
|
696
708
|
hasCooldown: true,
|
|
697
709
|
minimumTier: null,
|
|
698
710
|
disabled: true,
|
|
@@ -1048,6 +1060,69 @@ export const getAllPaymentMethods = (chainId, includeSubProviders = false) => {
|
|
|
1048
1060
|
export const verifiablePaymentMethods = (chainId, includeSubProviders = false) => {
|
|
1049
1061
|
return getAllPaymentMethods(chainId, includeSubProviders).filter(key => key !== providers.MANUAL);
|
|
1050
1062
|
};
|
|
1063
|
+
/**
|
|
1064
|
+
* Compute every payment-method id (lowercased, 32-byte keccak hex) that
|
|
1065
|
+
* attaching the given parent provider would attempt to register on a deposit.
|
|
1066
|
+
*
|
|
1067
|
+
* For a provider with no sub-providers this is just `[providerKeyToContractId(key)]`.
|
|
1068
|
+
* For a provider with sub-providers (currently only Zelle), the contract does
|
|
1069
|
+
* NOT accept the parent key — the UI fans it out into one row per sub-provider.
|
|
1070
|
+
* The returned list mirrors what {@link providerConfigs}-driven submission
|
|
1071
|
+
* code (e.g. AddPlatformModal) would put on the wire.
|
|
1072
|
+
*
|
|
1073
|
+
* Keys are lowercased so callers can compare directly against
|
|
1074
|
+
* `Deposit.attachedPaymentMethodIds` without per-call normalization.
|
|
1075
|
+
*/
|
|
1076
|
+
export function getProviderPaymentMethodIds(config) {
|
|
1077
|
+
const subProviders = config.subProviders;
|
|
1078
|
+
if (subProviders && subProviders.size > 0) {
|
|
1079
|
+
return Array.from(subProviders).map(sub => providerKeyToContractId(sub.key).toLowerCase());
|
|
1080
|
+
}
|
|
1081
|
+
return [providerKeyToContractId(config.key).toLowerCase()];
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Decide, for every non-manual non-disabled provider on `chainId`, whether
|
|
1085
|
+
* it can still be attached to a deposit given the deposit's already-attached
|
|
1086
|
+
* payment method id hashes.
|
|
1087
|
+
*
|
|
1088
|
+
* Important — a parent provider is unavailable if ANY of its candidate hashes
|
|
1089
|
+
* (own contractId plus every sub-provider's contractId) is already attached.
|
|
1090
|
+
* This matches the contract's atomicity: `addPaymentMethods` submits the full
|
|
1091
|
+
* fan-out in one transaction and reverts with `PaymentMethodAlreadyExists`
|
|
1092
|
+
* the moment one row collides. A maker who already has Zelle-Chase attached
|
|
1093
|
+
* cannot re-add the Zelle parent without colliding on the Chase sub-row.
|
|
1094
|
+
*
|
|
1095
|
+
* `manual` is always excluded from both lists.
|
|
1096
|
+
*/
|
|
1097
|
+
export function computeProviderAvailability(args) {
|
|
1098
|
+
const configs = providerConfigs(args.chainId);
|
|
1099
|
+
const available = [];
|
|
1100
|
+
const unavailable = [];
|
|
1101
|
+
for (const config of Object.values(configs)) {
|
|
1102
|
+
if (config.key === providers.MANUAL)
|
|
1103
|
+
continue;
|
|
1104
|
+
if (isProviderDisabled(config.key, config.disabled))
|
|
1105
|
+
continue;
|
|
1106
|
+
const candidateIds = getProviderPaymentMethodIds(config);
|
|
1107
|
+
const collides = candidateIds.some(id => args.attachedPaymentMethodIds.has(id));
|
|
1108
|
+
if (collides) {
|
|
1109
|
+
unavailable.push(config);
|
|
1110
|
+
}
|
|
1111
|
+
else {
|
|
1112
|
+
available.push(config);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
return { available, unavailable };
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Convenience wrapper around {@link computeProviderAvailability} that returns
|
|
1119
|
+
* only the available list. Use this on call sites that don't need the
|
|
1120
|
+
* unavailable set (e.g. counting providers remaining to gate an Add Platform
|
|
1121
|
+
* button).
|
|
1122
|
+
*/
|
|
1123
|
+
export function availableProviderConfigs(args) {
|
|
1124
|
+
return computeProviderAvailability(args).available;
|
|
1125
|
+
}
|
|
1051
1126
|
/**
|
|
1052
1127
|
* Get supported currencies for a payment provider.
|
|
1053
1128
|
* Returns only the currencies that are in the provider's supported list.
|
package/dist/payment.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 } from 'viem/chains';
|
|
4
|
-
import { providerConfigs, getCurrenciesForProvider, verifierGetsKey, providerKeyToContractId, resolveApiProviderKey } from './payment.js';
|
|
4
|
+
import { providerConfigs, getCurrenciesForProvider, providers, verifierGetsKey, providerKeyToContractId, resolveApiProviderKey, getProviderPaymentMethodIds, computeProviderAvailability, availableProviderConfigs, subProviders } from './payment.js';
|
|
5
5
|
import { SUPPORTED_CURRENCIES } from './currencies.js';
|
|
6
6
|
describe('getCurrenciesForProvider', () => {
|
|
7
7
|
const configs = providerConfigs(base.id);
|
|
@@ -210,33 +210,44 @@ describe('providerConfigs', () => {
|
|
|
210
210
|
const configs = providerConfigs(base.id);
|
|
211
211
|
describe('sendLink functions', () => {
|
|
212
212
|
it('venmo sendLink generates correct URL without QR code', () => {
|
|
213
|
-
const result = configs.venmo.sendLink?.({ username: 'testuser', amount:
|
|
213
|
+
const result = configs.venmo.sendLink?.({ username: 'testuser', amount: '100.00' });
|
|
214
214
|
assert.ok(result?.includes('account.venmo.com/pay'), 'Should use web URL');
|
|
215
215
|
assert.ok(result?.includes('testuser'), 'Should include username');
|
|
216
|
-
assert.ok(result?.includes('100'), 'Should
|
|
216
|
+
assert.ok(result?.includes('amount=100.00'), 'Should pass through the formatted fiat amount verbatim');
|
|
217
217
|
});
|
|
218
218
|
it('venmo sendLink generates correct URL with QR code', () => {
|
|
219
|
-
const result = configs.venmo.sendLink?.({ username: 'testuser', amount:
|
|
219
|
+
const result = configs.venmo.sendLink?.({ username: 'testuser', amount: '100.00', qrCode: true });
|
|
220
220
|
assert.ok(result?.startsWith('venmo://'), 'Should use venmo:// protocol');
|
|
221
|
+
assert.ok(result?.includes('amount=100.00'), 'Should pass through the formatted fiat amount verbatim');
|
|
222
|
+
});
|
|
223
|
+
it('venmo sendLink rejects a numeric (bigint base-unit) amount via type system', () => {
|
|
224
|
+
// Regression: previous signature was `amount: bigint`, and callers
|
|
225
|
+
// passed `intent.amount` (USDC base units, e.g. 1_000000n for $1).
|
|
226
|
+
// The URL templated literally as `amount=1000000`, which Venmo
|
|
227
|
+
// interpreted as $1,000,000. This assertion exists as a sentinel
|
|
228
|
+
// that the formatted-string path is the supported contract.
|
|
229
|
+
const result = configs.venmo.sendLink?.({ username: 'testuser', amount: '1.00' });
|
|
230
|
+
assert.ok(result?.includes('amount=1.00'), 'Fiat string should pass through unchanged');
|
|
231
|
+
assert.ok(!result?.includes('amount=1000000'), 'Token base units must never appear in the URL');
|
|
221
232
|
});
|
|
222
233
|
it('cashapp sendLink generates correct URL', () => {
|
|
223
|
-
const result = configs.cashapp.sendLink?.({ username: '$testuser', amount:
|
|
234
|
+
const result = configs.cashapp.sendLink?.({ username: '$testuser', amount: '100.00' });
|
|
224
235
|
assert.ok(result?.includes('cash.app'), 'Should use cash.app URL');
|
|
225
236
|
});
|
|
226
237
|
it('cashapp sendLink handles username without $ prefix', () => {
|
|
227
|
-
const result = configs.cashapp.sendLink?.({ username: 'testuser', amount:
|
|
238
|
+
const result = configs.cashapp.sendLink?.({ username: 'testuser', amount: '100.00' });
|
|
228
239
|
assert.ok(result?.includes('$testuser'), 'Should add $ prefix to username');
|
|
229
240
|
});
|
|
230
241
|
it('revolut sendLink generates correct URL', () => {
|
|
231
|
-
const result = configs.revolut.sendLink?.({ username: 'testuser', amount:
|
|
242
|
+
const result = configs.revolut.sendLink?.({ username: 'testuser', amount: '100.00' });
|
|
232
243
|
assert.ok(result?.includes('revolut.me'), 'Should use revolut.me URL');
|
|
233
244
|
});
|
|
234
245
|
it('wise sendLink generates correct URL', () => {
|
|
235
|
-
const result = configs.wise.sendLink?.({ username: 'testuser', amount:
|
|
246
|
+
const result = configs.wise.sendLink?.({ username: 'testuser', amount: '100.00' });
|
|
236
247
|
assert.ok(result?.includes('wise.com/pay/me'), 'Should use wise.com URL');
|
|
237
248
|
});
|
|
238
249
|
it('mercadopago sendLink generates correct URL', () => {
|
|
239
|
-
const result = configs.mercadopago.sendLink?.({ username: 'testuser', amount:
|
|
250
|
+
const result = configs.mercadopago.sendLink?.({ username: 'testuser', amount: '100.00' });
|
|
240
251
|
assert.ok(result?.includes('mercadopago.com'), 'Should use mercadopago URL');
|
|
241
252
|
});
|
|
242
253
|
// PayPal sendLink tests removed — provider disabled
|
|
@@ -300,3 +311,195 @@ describe('providerConfigs', () => {
|
|
|
300
311
|
});
|
|
301
312
|
});
|
|
302
313
|
});
|
|
314
|
+
/**
|
|
315
|
+
* Tests for {@link getProviderPaymentMethodIds}.
|
|
316
|
+
*
|
|
317
|
+
* The contract uniqueness predicate is `paymentMethodId = keccak(providerKey)`,
|
|
318
|
+
* and providers with sub-providers (currently only Zelle) fan out into one
|
|
319
|
+
* row per sub-provider — the parent key itself is NOT submitted on-chain.
|
|
320
|
+
* The helper has to mirror that fan-out exactly or the availability check
|
|
321
|
+
* downstream will be wrong.
|
|
322
|
+
*/
|
|
323
|
+
describe('getProviderPaymentMethodIds', () => {
|
|
324
|
+
const configs = providerConfigs(base.id);
|
|
325
|
+
it('returns the parent contractId (lowercased) for a provider with no sub-providers', () => {
|
|
326
|
+
const ids = getProviderPaymentMethodIds(configs.venmo);
|
|
327
|
+
const expected = providerKeyToContractId('venmo').toLowerCase();
|
|
328
|
+
assert.equal(ids.length, 1);
|
|
329
|
+
assert.equal(ids[0], expected);
|
|
330
|
+
});
|
|
331
|
+
it('returns ONLY sub-provider contractIds for a parent with sub-providers (not the parent itself)', () => {
|
|
332
|
+
// Zelle's parent key is NOT whitelisted on-chain — only sub-providers
|
|
333
|
+
// are. Submitting the parent would revert with `PaymentMethodNotWhitelisted`,
|
|
334
|
+
// so it must not be in the returned list.
|
|
335
|
+
const ids = getProviderPaymentMethodIds(configs.zelle);
|
|
336
|
+
const parentId = providerKeyToContractId('zelle').toLowerCase();
|
|
337
|
+
assert.ok(!ids.includes(parentId), 'zelle parent contractId must NOT be in the fan-out list');
|
|
338
|
+
const expectedSubIds = Object.values(subProviders).map(k => providerKeyToContractId(k).toLowerCase());
|
|
339
|
+
assert.equal(ids.length, expectedSubIds.length);
|
|
340
|
+
for (const subId of expectedSubIds) {
|
|
341
|
+
assert.ok(ids.includes(subId), `expected sub-provider contractId ${subId} in fan-out list`);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
it('returns lowercased hex (downstream comparison is case-sensitive)', () => {
|
|
345
|
+
// `Deposit.attachedPaymentMethodIds` is lowercased; the comparator at the
|
|
346
|
+
// call site does no extra normalization. If this helper ever returns
|
|
347
|
+
// mixed case we'd silently start treating "already attached" rows as
|
|
348
|
+
// available — the worst kind of regression.
|
|
349
|
+
const ids = getProviderPaymentMethodIds(configs.revolut);
|
|
350
|
+
for (const id of ids) {
|
|
351
|
+
assert.equal(id, id.toLowerCase(), `id ${id} must be lowercased`);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
/**
|
|
356
|
+
* Tests for {@link computeProviderAvailability} / {@link availableProviderConfigs}.
|
|
357
|
+
*
|
|
358
|
+
* These map directly to the bugs the user reported:
|
|
359
|
+
*
|
|
360
|
+
* Bug 1 — "active-only" wrongly counted deactivated providers as
|
|
361
|
+
* available. Fix verified by `marks a deactivated provider as
|
|
362
|
+
* unavailable when its hash is in the attached set`.
|
|
363
|
+
*
|
|
364
|
+
* Bug 2 — parent provider was wrongly available when only sub-providers
|
|
365
|
+
* collided. Fix verified by `parent is unavailable when ANY
|
|
366
|
+
* sub-provider hash is in the attached set` + the variants
|
|
367
|
+
* below.
|
|
368
|
+
*
|
|
369
|
+
* Bug 3 — UI gate. Verified by the empty-list case
|
|
370
|
+
* (`returns empty available list when every supported provider is
|
|
371
|
+
* attached`).
|
|
372
|
+
*/
|
|
373
|
+
describe('computeProviderAvailability', () => {
|
|
374
|
+
const chainId = base.id;
|
|
375
|
+
const configs = providerConfigs(chainId);
|
|
376
|
+
const venmoId = providerKeyToContractId('venmo').toLowerCase();
|
|
377
|
+
const zelleParentId = providerKeyToContractId('zelle').toLowerCase();
|
|
378
|
+
const zelleChaseId = providerKeyToContractId('zelle-chase').toLowerCase();
|
|
379
|
+
const zelleBofaId = providerKeyToContractId('zelle-bofa').toLowerCase();
|
|
380
|
+
const zelleCitiId = providerKeyToContractId('zelle-citi').toLowerCase();
|
|
381
|
+
const revolutId = providerKeyToContractId('revolut').toLowerCase();
|
|
382
|
+
it('deposit with no providers attached: every non-disabled provider is available', () => {
|
|
383
|
+
const { available, unavailable } = computeProviderAvailability({
|
|
384
|
+
chainId,
|
|
385
|
+
attachedPaymentMethodIds: new Set(),
|
|
386
|
+
});
|
|
387
|
+
assert.equal(unavailable.length, 0);
|
|
388
|
+
// Every non-manual, non-disabled provider should be available. The
|
|
389
|
+
// exact count depends on `disabled` flags and env-var overrides; just
|
|
390
|
+
// check that the parent providers we DO ship come through.
|
|
391
|
+
const keys = available.map(c => c.key);
|
|
392
|
+
assert.ok(keys.includes('venmo'), 'venmo should be available');
|
|
393
|
+
assert.ok(keys.includes('zelle'), 'zelle should be available');
|
|
394
|
+
assert.ok(keys.includes('revolut'), 'revolut should be available');
|
|
395
|
+
// manual must NEVER be in the available list.
|
|
396
|
+
assert.ok(!keys.includes('manual'), 'manual must never be offered');
|
|
397
|
+
});
|
|
398
|
+
it('deposit with one parent attached: that parent is unavailable, others remain available', () => {
|
|
399
|
+
const { available, unavailable } = computeProviderAvailability({
|
|
400
|
+
chainId,
|
|
401
|
+
attachedPaymentMethodIds: new Set([venmoId]),
|
|
402
|
+
});
|
|
403
|
+
assert.ok(!available.some(c => c.key === 'venmo'), 'venmo must NOT be in available');
|
|
404
|
+
assert.ok(unavailable.some(c => c.key === 'venmo'), 'venmo must be in unavailable');
|
|
405
|
+
assert.ok(available.some(c => c.key === 'revolut'), 'revolut still available');
|
|
406
|
+
assert.ok(available.some(c => c.key === 'zelle'), 'zelle still available (no sub collisions)');
|
|
407
|
+
});
|
|
408
|
+
it('parent is unavailable when ONE sub-provider hash is in the attached set (Bug 2)', () => {
|
|
409
|
+
// Only zelle-chase is attached. Per the contract semantics — the modal
|
|
410
|
+
// submits ALL Zelle sub-providers in one batch and the first existing
|
|
411
|
+
// row makes the whole tx revert — the Zelle parent must be flagged
|
|
412
|
+
// unavailable.
|
|
413
|
+
const { available, unavailable } = computeProviderAvailability({
|
|
414
|
+
chainId,
|
|
415
|
+
attachedPaymentMethodIds: new Set([zelleChaseId]),
|
|
416
|
+
});
|
|
417
|
+
assert.ok(!available.some(c => c.key === 'zelle'), 'zelle parent must NOT be available');
|
|
418
|
+
assert.ok(unavailable.some(c => c.key === 'zelle'), 'zelle parent must be in unavailable');
|
|
419
|
+
});
|
|
420
|
+
it('parent is unavailable when ALL sub-providers are attached', () => {
|
|
421
|
+
// Every Zelle sub is attached. Parent must be unavailable.
|
|
422
|
+
const { available, unavailable } = computeProviderAvailability({
|
|
423
|
+
chainId,
|
|
424
|
+
attachedPaymentMethodIds: new Set([zelleChaseId, zelleBofaId, zelleCitiId]),
|
|
425
|
+
});
|
|
426
|
+
assert.ok(!available.some(c => c.key === 'zelle'));
|
|
427
|
+
assert.ok(unavailable.some(c => c.key === 'zelle'));
|
|
428
|
+
});
|
|
429
|
+
it('parent is available when only the parent-key hash is in the set but no sub-providers are', () => {
|
|
430
|
+
// Defensive edge case — a malformed indexer row that stored the parent
|
|
431
|
+
// keccak hash rather than a sub-provider hash. The contract would also
|
|
432
|
+
// reject re-adding the parent hash, so we must treat the parent as
|
|
433
|
+
// unavailable here. This guards against the inverse of Bug 2 too: a
|
|
434
|
+
// future change that compares the parent's own contractId.
|
|
435
|
+
const { unavailable } = computeProviderAvailability({
|
|
436
|
+
chainId,
|
|
437
|
+
attachedPaymentMethodIds: new Set([zelleParentId]),
|
|
438
|
+
});
|
|
439
|
+
// The parent's own contractId is in `getProviderPaymentMethodIds`
|
|
440
|
+
// ONLY for providers without sub-providers, so for Zelle (which has
|
|
441
|
+
// sub-providers) this attached id has no effect — Zelle stays
|
|
442
|
+
// available because the modal would only ever submit sub-provider
|
|
443
|
+
// hashes. This is the correct behavior: the contract row's
|
|
444
|
+
// paymentMethodId would never realistically be the parent for a Zelle
|
|
445
|
+
// deposit since the parent isn't whitelisted.
|
|
446
|
+
assert.ok(!unavailable.some(c => c.key === 'zelle'), 'zelle remains available — parent hash unused at submit time');
|
|
447
|
+
});
|
|
448
|
+
it('marks a deactivated provider as unavailable when its hash is in the attached set (Bug 1)', () => {
|
|
449
|
+
// The contract's `setPaymentMethodActive(false)` does NOT remove the
|
|
450
|
+
// depositVerifierAdded row; the paymentMethodId stays attached and
|
|
451
|
+
// re-adding it reverts. So even an attached set sourced from
|
|
452
|
+
// inactive-included rows must drive this provider out of `available`.
|
|
453
|
+
const { available, unavailable } = computeProviderAvailability({
|
|
454
|
+
chainId,
|
|
455
|
+
attachedPaymentMethodIds: new Set([revolutId]),
|
|
456
|
+
});
|
|
457
|
+
assert.ok(!available.some(c => c.key === 'revolut'));
|
|
458
|
+
assert.ok(unavailable.some(c => c.key === 'revolut'));
|
|
459
|
+
});
|
|
460
|
+
it('returns empty available list when every supported provider is attached (Bug 3 gate)', () => {
|
|
461
|
+
// Build attached set from EVERY non-disabled provider's payment method
|
|
462
|
+
// ids (using getProviderPaymentMethodIds so the fan-out is correct for
|
|
463
|
+
// sub-providered parents).
|
|
464
|
+
const allAttached = new Set();
|
|
465
|
+
for (const config of Object.values(configs)) {
|
|
466
|
+
if (config.key === providers.MANUAL)
|
|
467
|
+
continue;
|
|
468
|
+
for (const id of getProviderPaymentMethodIds(config)) {
|
|
469
|
+
allAttached.add(id);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const result = computeProviderAvailability({ chainId, attachedPaymentMethodIds: allAttached });
|
|
473
|
+
assert.equal(result.available.length, 0, `available should be empty, got ${result.available.map(c => c.key).join(',')}`);
|
|
474
|
+
// unavailable should contain at least the visible non-disabled set
|
|
475
|
+
assert.ok(result.unavailable.length > 0);
|
|
476
|
+
});
|
|
477
|
+
it('availableProviderConfigs is a thin wrapper that returns only the available list', () => {
|
|
478
|
+
const both = computeProviderAvailability({
|
|
479
|
+
chainId,
|
|
480
|
+
attachedPaymentMethodIds: new Set([venmoId]),
|
|
481
|
+
});
|
|
482
|
+
const wrapped = availableProviderConfigs({
|
|
483
|
+
chainId,
|
|
484
|
+
attachedPaymentMethodIds: new Set([venmoId]),
|
|
485
|
+
});
|
|
486
|
+
assert.deepEqual(wrapped.map(c => c.key), both.available.map(c => c.key));
|
|
487
|
+
});
|
|
488
|
+
it('excludes the manual provider regardless of attached set', () => {
|
|
489
|
+
// manual is always excluded — adding it has no meaning in the UX. This
|
|
490
|
+
// is true for every input.
|
|
491
|
+
const cases = [
|
|
492
|
+
new Set(),
|
|
493
|
+
new Set([venmoId]),
|
|
494
|
+
new Set([zelleChaseId, revolutId]),
|
|
495
|
+
];
|
|
496
|
+
for (const attached of cases) {
|
|
497
|
+
const { available, unavailable } = computeProviderAvailability({
|
|
498
|
+
chainId,
|
|
499
|
+
attachedPaymentMethodIds: attached,
|
|
500
|
+
});
|
|
501
|
+
assert.ok(!available.some(c => c.key === 'manual'), 'manual must not be in available');
|
|
502
|
+
assert.ok(!unavailable.some(c => c.key === 'manual'), 'manual must not be in unavailable');
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
});
|
package/dist/reputation.d.ts
CHANGED
|
@@ -9,22 +9,27 @@
|
|
|
9
9
|
* - **Platform risk configs**: Multipliers and cooldown flags from payment.ts
|
|
10
10
|
*
|
|
11
11
|
* @remarks
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Reputation is now scored from PulseChain volume only — Base data is no
|
|
13
|
+
* longer aggregated. The taker cap still varies by tier; PulseChain is the
|
|
14
|
+
* only chain that produces taker-tier-influencing volume.
|
|
14
15
|
*
|
|
15
|
-
* Tier progression (by fulfilled volume in USD)
|
|
16
|
-
*
|
|
17
|
-
* -
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* -
|
|
21
|
-
* -
|
|
16
|
+
* Tier progression (by CUMULATIVE / lifetime fulfilled volume in USD).
|
|
17
|
+
* Thresholds mirror peer.xyz's published ladder
|
|
18
|
+
* (https://docs.peer.xyz/guides/for-buyers/reputation#the-five-tiers).
|
|
19
|
+
* Volume is NOT windowed — it accumulates over the user's lifetime, so
|
|
20
|
+
* a user only ever moves UP the ladder, never back down for inactivity:
|
|
21
|
+
* - Entry: $0+ → $100 cap, 12h cooldown
|
|
22
|
+
* - Neutral: $500+ → $250 cap, 6h cooldown
|
|
23
|
+
* - Plus: $2,000+ → $1,000 cap, no cooldown
|
|
24
|
+
* - Pro: $10,000+ → $2,500 cap, no cooldown
|
|
25
|
+
* - Platinum: $25,000+ → $5,000 cap, no cooldown
|
|
26
|
+
* - VIP: Manually assigned → unlimited cap, no cooldown, bypasses all locks
|
|
22
27
|
*
|
|
23
28
|
* Platform multipliers modify the base cap:
|
|
24
29
|
* - Low risk (Revolut, Wise, Monzo, MercadoPago): 5x multiplier, no cooldown
|
|
25
30
|
* - Medium risk (Zelle): 1.5x multiplier
|
|
26
31
|
* - High risk (Venmo, CashApp): 1x multiplier
|
|
27
|
-
* - Highest risk (PayPal): 0.75x multiplier, requires
|
|
32
|
+
* - Highest risk (PayPal): 0.75x multiplier, requires Plus tier or higher
|
|
28
33
|
*
|
|
29
34
|
* @see {@link payment} for platform risk configurations
|
|
30
35
|
*/
|
|
@@ -42,7 +47,7 @@ export interface TierConfig {
|
|
|
42
47
|
baseCap: number;
|
|
43
48
|
/** Cooldown duration in hours (0 = no cooldown) */
|
|
44
49
|
cooldownHours: number;
|
|
45
|
-
/** Volume range display text (e.g. "$
|
|
50
|
+
/** Volume range display text (e.g. "$200 - $1k") */
|
|
46
51
|
volumeRangeDisplay: string;
|
|
47
52
|
}
|
|
48
53
|
/**
|
|
@@ -50,8 +55,13 @@ export interface TierConfig {
|
|
|
50
55
|
* Maps universal tier names to their configuration.
|
|
51
56
|
*
|
|
52
57
|
* @remarks
|
|
53
|
-
*
|
|
54
|
-
*
|
|
58
|
+
* Internal tier keys (peasant / neutral / plus / pro / platinum / president)
|
|
59
|
+
* are stable data identifiers and DO NOT change — the indexer, the contract
|
|
60
|
+
* gating service, and persisted user state all key on them. The `name` field
|
|
61
|
+
* below is the user-facing display name; we use a neutral progression ladder
|
|
62
|
+
* (Entry / Neutral / Plus / Pro / Platinum / VIP) rather than peer.xyz's
|
|
63
|
+
* brand voice ("Peer Peasant" etc.), which is theirs and reads as gentle
|
|
64
|
+
* hazing of low-volume users — not the tone Provex wants.
|
|
55
65
|
*/
|
|
56
66
|
export declare const TAKER_TIERS: Record<TakerTier, TierConfig>;
|
|
57
67
|
/**
|
|
@@ -64,23 +74,52 @@ export declare const TIER_ORDER: TakerTier[];
|
|
|
64
74
|
* Matches peer.xyz's TR() function behaviour.
|
|
65
75
|
*
|
|
66
76
|
* @example
|
|
67
|
-
* getVolumeRangeDisplay('pro') // "$
|
|
77
|
+
* getVolumeRangeDisplay('pro') // "$5k - $10k"
|
|
68
78
|
* getVolumeRangeDisplay('president') // "VIP"
|
|
69
79
|
*/
|
|
70
80
|
export declare function getVolumeRangeDisplay(tier: TakerTier): string;
|
|
71
81
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
82
|
+
* Sentinel cap value used in place of `Infinity` (which has no bigint
|
|
83
|
+
* representation) to signal "no upper limit" — president tier on every
|
|
84
|
+
* platform. Equal to `2n ** 256n - 1n` (max uint256); no realistic
|
|
85
|
+
* token amount can exceed it, so the standard `amount > cap` comparison
|
|
86
|
+
* naturally short-circuits to false for unlimited users.
|
|
87
|
+
*
|
|
88
|
+
* Callers that render the cap as a string MUST check for this sentinel
|
|
89
|
+
* before formatting — `Number(UNLIMITED_CAP_BASE_UNITS)` overflows to
|
|
90
|
+
* `Infinity` but `.toLocaleString()` on the bigint prints a 78-digit
|
|
91
|
+
* number that is not useful in a UI.
|
|
92
|
+
*/
|
|
93
|
+
export declare const UNLIMITED_CAP_BASE_UNITS: bigint;
|
|
94
|
+
/**
|
|
95
|
+
* Calculate the effective cap for a tier and platform combination, in
|
|
96
|
+
* token base units (scaled by `decimals`). Designed so the caller can
|
|
97
|
+
* compare directly against a token amount it already holds as a bigint
|
|
98
|
+
* — no rebasing from USD floats on the consumer side.
|
|
99
|
+
*
|
|
100
|
+
* Assumes the token trades 1:1 with USD (USDC, USDT, DAI). When we add
|
|
101
|
+
* a non-stablecoin token this function needs a price feed or the caller
|
|
102
|
+
* needs to convert before comparing.
|
|
74
103
|
*
|
|
75
104
|
* @param tier - The user's current tier
|
|
76
105
|
* @param providerKey - The payment platform
|
|
77
|
-
* @
|
|
78
|
-
*
|
|
106
|
+
* @param decimals - Token decimals (USDC = 6, an 18-decimal stable = 18)
|
|
107
|
+
* @returns Effective cap in token base units, or:
|
|
108
|
+
* - `null` — platform is locked at this tier
|
|
109
|
+
* - `UNLIMITED_CAP_BASE_UNITS` — president tier (no cap enforcement)
|
|
79
110
|
*/
|
|
80
|
-
export declare function getEffectiveCap({ tier, providerKey, }: {
|
|
111
|
+
export declare function getEffectiveCap({ tier, providerKey, decimals, }: {
|
|
81
112
|
tier: TakerTier;
|
|
82
113
|
providerKey: ProviderKey;
|
|
83
|
-
|
|
114
|
+
decimals: number;
|
|
115
|
+
}): bigint | null;
|
|
116
|
+
/**
|
|
117
|
+
* Companion display helper — converts the base-units cap back into a
|
|
118
|
+
* USD number for `toLocaleString()`-style formatting. Returns `null`
|
|
119
|
+
* for locked and `Infinity` for unlimited, mirroring the legacy
|
|
120
|
+
* `getEffectiveCap` return shape so display code stays simple.
|
|
121
|
+
*/
|
|
122
|
+
export declare function getEffectiveCapDisplayUsd(cap: bigint | null, decimals: number): number | null;
|
|
84
123
|
/**
|
|
85
124
|
* Get the cooldown duration for a tier and platform.
|
|
86
125
|
* Derives from tier cooldown hours and platform hasCooldown flag.
|