@provex/utils 1.11.0 → 2.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.
Files changed (80) hide show
  1. package/dist/buy/browsePanelDerivations.d.ts +212 -0
  2. package/dist/buy/browsePanelDerivations.js +269 -0
  3. package/dist/buy/browsePanelDerivations.test.d.ts +9 -0
  4. package/dist/buy/browsePanelDerivations.test.js +552 -0
  5. package/dist/buy/fillabilityNotice.d.ts +214 -0
  6. package/dist/buy/fillabilityNotice.js +162 -0
  7. package/dist/buy/fillabilityNotice.test.d.ts +8 -0
  8. package/dist/buy/fillabilityNotice.test.js +246 -0
  9. package/dist/buy/formatCurrencyBaseUnits.d.ts +30 -0
  10. package/dist/buy/formatCurrencyBaseUnits.js +34 -0
  11. package/dist/buy/formatCurrencyBaseUnits.test.d.ts +7 -0
  12. package/dist/buy/formatCurrencyBaseUnits.test.js +34 -0
  13. package/dist/buy/fulfillTransactionHash.d.ts +29 -0
  14. package/dist/buy/fulfillTransactionHash.js +27 -0
  15. package/dist/buy/fulfillTransactionHash.test.d.ts +7 -0
  16. package/dist/buy/fulfillTransactionHash.test.js +46 -0
  17. package/dist/buy/index.d.ts +17 -0
  18. package/dist/buy/index.js +17 -0
  19. package/dist/buy/orderCap.d.ts +29 -0
  20. package/dist/buy/orderCap.js +35 -0
  21. package/dist/buy/orderCap.test.d.ts +8 -0
  22. package/dist/buy/orderCap.test.js +35 -0
  23. package/dist/buy/postFeeAmount.d.ts +39 -0
  24. package/dist/buy/postFeeAmount.js +36 -0
  25. package/dist/buy/postFeeAmount.test.d.ts +9 -0
  26. package/dist/buy/postFeeAmount.test.js +82 -0
  27. package/dist/buy/providerSelectionMode.d.ts +5 -0
  28. package/dist/buy/providerSelectionMode.js +4 -0
  29. package/dist/buy/settlementReconciliation.d.ts +48 -0
  30. package/dist/buy/settlementReconciliation.js +65 -0
  31. package/dist/buy/settlementReconciliation.test.d.ts +9 -0
  32. package/dist/buy/settlementReconciliation.test.js +164 -0
  33. package/dist/buy/submitStepDerivations.d.ts +122 -0
  34. package/dist/buy/submitStepDerivations.js +140 -0
  35. package/dist/buy/submitStepDerivations.test.d.ts +1 -0
  36. package/dist/buy/submitStepDerivations.test.js +305 -0
  37. package/dist/buy/verifiableBanks.d.ts +29 -0
  38. package/dist/buy/verifiableBanks.js +28 -0
  39. package/dist/buy/verifiableBanks.test.d.ts +4 -0
  40. package/dist/buy/verifiableBanks.test.js +31 -0
  41. package/dist/consent.js +0 -2
  42. package/dist/consent.storage.test.d.ts +8 -0
  43. package/dist/consent.storage.test.js +111 -0
  44. package/dist/consent.test.js +60 -1
  45. package/dist/contracts.js +3 -1
  46. package/dist/contracts.test.js +17 -1
  47. package/dist/conversionRates.test.js +18 -0
  48. package/dist/currencies.d.ts +6 -0
  49. package/dist/currencies.js +1 -1
  50. package/dist/currencies.test.js +29 -2
  51. package/dist/fees.js +0 -3
  52. package/dist/gating.test.js +16 -1
  53. package/dist/ids.test.js +48 -0
  54. package/dist/payment.d.ts +50 -0
  55. package/dist/payment.js +72 -51
  56. package/dist/payment.lookup.test.d.ts +5 -0
  57. package/dist/payment.lookup.test.js +169 -0
  58. package/dist/payment.providerLists.test.d.ts +12 -0
  59. package/dist/payment.providerLists.test.js +40 -0
  60. package/dist/payment.sendLinks.test.d.ts +9 -0
  61. package/dist/payment.sendLinks.test.js +91 -0
  62. package/dist/peer-types.d.ts +5 -78
  63. package/dist/peer-types.js +5 -31
  64. package/dist/peer-types.test.js +1 -31
  65. package/dist/profit.d.ts +8 -0
  66. package/dist/profit.js +16 -3
  67. package/dist/profit.test.js +12 -1
  68. package/dist/reputation.js +4 -6
  69. package/dist/reputation.test.js +8 -0
  70. package/dist/time.test.js +26 -5
  71. package/dist/tokens.d.ts +2 -2
  72. package/dist/transports.d.ts +53 -2
  73. package/dist/transports.debug.test.d.ts +9 -0
  74. package/dist/transports.debug.test.js +95 -0
  75. package/dist/transports.js +26 -10
  76. package/dist/transports.test.js +5 -1
  77. package/dist/units.d.ts +19 -0
  78. package/dist/units.js +21 -0
  79. package/dist/units.test.js +31 -1
  80. package/package.json +6 -2
@@ -56,6 +56,13 @@ describe('conversionRates', () => {
56
56
  assert.ok(usdRates, 'Should have USD rates');
57
57
  assert.equal(usdRates.length, 1);
58
58
  });
59
+ it('keeps an event whose rate value is missing, with an empty value, instead of dropping the deposit', () => {
60
+ const events = [
61
+ createMockEvent({ currentDepositConversionRateUpdated: { value: null } }),
62
+ ];
63
+ const result = processConversionRates({ chainId: base.id, events });
64
+ assert.equal(result.get('venmo')?.get(USD_CONTRACT_ID)?.[0]?.value, '');
65
+ });
59
66
  it('should skip events without currentDepositConversionRateUpdated', () => {
60
67
  const events = [
61
68
  createMockEvent({ currentDepositConversionRateUpdated: undefined }),
@@ -327,6 +334,17 @@ describe('conversionRates', () => {
327
334
  const result = getPrimaryConversionRate(new Map());
328
335
  assert.equal(result, null);
329
336
  });
337
+ it('skips a provider that holds no rates and finds one under a later provider', () => {
338
+ const eurRate = createMockEvent({ currency: EUR_CONTRACT_ID });
339
+ const providerRates = new Map([
340
+ ['venmo', new Map()],
341
+ ['zelle', new Map([[EUR_CONTRACT_ID, eurRate]])],
342
+ ]);
343
+ assert.equal(getPrimaryConversionRate(providerRates)?.currency, EUR_CONTRACT_ID);
344
+ });
345
+ it('returns null when every provider holds no rates', () => {
346
+ assert.equal(getPrimaryConversionRate(new Map([['venmo', new Map()]])), null);
347
+ });
330
348
  it('should prioritize USD over other currencies', () => {
331
349
  const usdRate = createMockEvent({ currency: USD_CONTRACT_ID });
332
350
  const eurRate = createMockEvent({ currency: EUR_CONTRACT_ID });
@@ -113,6 +113,12 @@ export declare class Currency implements CurrencyInfo {
113
113
  * @param currencyCode - The currency code
114
114
  */
115
115
  export declare function generateCurrencyId(currencyCode: string): string;
116
+ /**
117
+ * Get the flag for a country
118
+ * @param countryCode - The country code
119
+ * @returns The flag image path
120
+ */
121
+ export declare const flagFromCountries: (countryCode: string) => string;
116
122
  /**
117
123
  * Registry of all supported fiat currencies.
118
124
  *
@@ -132,7 +132,7 @@ export function generateCurrencyId(currencyCode) {
132
132
  * @param countryCode - The country code
133
133
  * @returns The flag image path
134
134
  */
135
- const flagFromCountries = (countryCode) => {
135
+ export const flagFromCountries = (countryCode) => {
136
136
  return countries.find((country) => country.code === countryCode)?.flag ?? '';
137
137
  };
138
138
  /**
@@ -1,6 +1,6 @@
1
- import { describe, it } from 'node:test';
1
+ import { describe, it, mock } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { SUPPORTED_CURRENCIES, getCurrencyInfo, getCurrencyByContractId, getCurrencyNameByContractId, getAllSupportedCurrencies, isSupportedCurrency, formatCurrencyAmount, tickerToContractId, generateCurrencyId, formatDefaultRate, formatCurrencyTotal, formatTokenTotal, Currency, } from './currencies.js';
3
+ import { SUPPORTED_CURRENCIES, getCurrencyInfo, getCurrencyByContractId, getCurrencyNameByContractId, getAllSupportedCurrencies, isSupportedCurrency, formatCurrencyAmount, tickerToContractId, generateCurrencyId, formatDefaultRate, formatCurrencyTotal, formatTokenTotal, flagFromCountries, Currency, } from './currencies.js';
4
4
  describe('currencies', () => {
5
5
  describe('SUPPORTED_CURRENCIES', () => {
6
6
  it('should have exactly 36 supported currencies', () => {
@@ -459,3 +459,30 @@ describe('currencies', () => {
459
459
  });
460
460
  });
461
461
  });
462
+ describe('flagFromCountries', () => {
463
+ it('returns the flag of a known country code', () => {
464
+ assert.equal(flagFromCountries('GBP'), SUPPORTED_CURRENCIES.GBP.flag);
465
+ });
466
+ it('returns an empty string for an unknown code, so a currency without a flag still renders', () => {
467
+ assert.equal(flagFromCountries('NOT-A-CODE'), '');
468
+ });
469
+ it('leaves no supported currency without a flag', () => {
470
+ for (const currency of Object.values(SUPPORTED_CURRENCIES)) {
471
+ assert.ok(currency.flag, `${currency.ticker} has no flag`);
472
+ }
473
+ });
474
+ });
475
+ describe('formatCurrencyTotal: an unexpected Intl failure', () => {
476
+ it('rethrows an error that is not the unknown-ticker RangeError, so real faults are not hidden', () => {
477
+ const unseenCurrency = { ...SUPPORTED_CURRENCIES.USD, ticker: 'ZZZ', symbol: 'Z' };
478
+ mock.method(Intl, 'NumberFormat', () => {
479
+ throw new TypeError('boom');
480
+ });
481
+ try {
482
+ assert.throws(() => formatCurrencyTotal({ amount: 100n, currency: unseenCurrency, locale: 'fr-FR' }), TypeError);
483
+ }
484
+ finally {
485
+ mock.restoreAll();
486
+ }
487
+ });
488
+ });
package/dist/fees.js CHANGED
@@ -144,9 +144,6 @@ export function normalizeFeePrecision(rawFee) {
144
144
  * @internal
145
145
  */
146
146
  function formatFromNumber(percentage) {
147
- if (percentage === 0) {
148
- return '0%';
149
- }
150
147
  if (Number.isInteger(percentage)) {
151
148
  return `${percentage}%`;
152
149
  }
@@ -14,7 +14,7 @@
14
14
  import assert from 'node:assert/strict';
15
15
  import { test } from 'node:test';
16
16
  import { keccak256, toHex } from 'viem';
17
- import { buildGatingDigest, hashReferralFees, } from './gating.js';
17
+ import { buildGatingDigest, hashReferralFees, signGatingApproval, } from './gating.js';
18
18
  // Shared inputs used to generate every golden vector below.
19
19
  const ORCHESTRATOR = '0x888888359E981B5225CA48fbCdCeff702FC3b888';
20
20
  const ESCROW = '0x777777779d229cdF3110e9de47943791c26300Ef';
@@ -104,3 +104,18 @@ test('v3 digest omits caller + referral hash and differs from v4', () => {
104
104
  const v4 = buildGatingDigest({ ...commonV4, referralFees: [] });
105
105
  assert.notEqual(v3, v4);
106
106
  });
107
+ test('signGatingApproval hands the signer the raw digest, so the contract recovers the same hash', async () => {
108
+ const input = { ...commonV4, referralFees: [] };
109
+ const received = [];
110
+ const signature = await signGatingApproval({
111
+ signer: {
112
+ signMessage: async ({ message }) => {
113
+ received.push(message.raw);
114
+ return '0xabcd';
115
+ },
116
+ },
117
+ digest: input,
118
+ });
119
+ assert.deepEqual(received, [buildGatingDigest(input)]);
120
+ assert.equal(signature, '0xabcd');
121
+ });
package/dist/ids.test.js CHANGED
@@ -604,4 +604,52 @@ describe('ids', () => {
604
604
  }, /Unknown chainId|Hex length/);
605
605
  });
606
606
  });
607
+ describe('orderId.order.parse: unknown chain', () => {
608
+ it('rejects an id whose chain is not supported rather than returning a chain the app cannot serve', () => {
609
+ const id = orderId.order.serialize({
610
+ timestamp: 1000n,
611
+ chainId: 8453n,
612
+ transactionIndex: 5n,
613
+ logIndex: 3n,
614
+ name: 'deposit_received',
615
+ });
616
+ // Overwrite the final 4 bytes (chain id) with an unsupported chain.
617
+ const unsupported = `${id.slice(0, -8)}0000270f`;
618
+ assert.throws(() => orderId.order.parse(unsupported), /Unknown chainId parsed from order id: 9999/);
619
+ });
620
+ it('leaves the name undefined for an event type id that has no registered name', () => {
621
+ const id = orderId.order.serialize({
622
+ timestamp: 1000n,
623
+ chainId: 8453n,
624
+ transactionIndex: 5n,
625
+ logIndex: 3n,
626
+ name: 'deposit_received',
627
+ });
628
+ // The event type is the single byte after timestamp (8) and tx index (4).
629
+ const offset = 2 + (8 + 4) * 2;
630
+ const unnamed = `${id.slice(0, offset)}ff${id.slice(offset + 2)}`;
631
+ const parsed = orderId.order.parse(unnamed);
632
+ assert.equal(parsed.eventTypeId, 255);
633
+ assert.equal(parsed.name, undefined);
634
+ });
635
+ });
636
+ describe('orderId.stat.parse', () => {
637
+ const stat = {
638
+ type: 'hour',
639
+ event: 'deposit',
640
+ currency: '0x1234567890123456789012345678901234567890123456789012345678901234',
641
+ token: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
642
+ verifier: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
643
+ version: 'v2',
644
+ };
645
+ it('recovers the hour-bucketed timestamp and the chain from a serialized stat id', () => {
646
+ const id = orderId.stat.serialize({ ...stat, timestamp: 3700n, chainId: 8453n });
647
+ assert.deepEqual(orderId.stat.parse(id), { timestamp: 3600n, chainId: 8453 });
648
+ });
649
+ it('rejects an id whose chain is not supported', () => {
650
+ const id = orderId.stat.serialize({ ...stat, timestamp: 3700n, chainId: 8453n });
651
+ const unsupported = `${id.slice(0, -8)}0000270f`;
652
+ assert.throws(() => orderId.stat.parse(unsupported), /Unknown chainId parsed from stat id: 9999/);
653
+ });
654
+ });
607
655
  });
package/dist/payment.d.ts CHANGED
@@ -23,6 +23,21 @@
23
23
  * @see {@link currencies} for supported currencies per provider
24
24
  */
25
25
  import { type Hex } from "viem";
26
+ /**
27
+ * Pick a value from the Vite (browser) and Node environments.
28
+ *
29
+ * Vite wins over Node. Within Node, the bare name wins over its `VITE_` twin.
30
+ *
31
+ * @param params.name - Variable name without the `VITE_` prefix.
32
+ * @param params.viteEnv - `import.meta.env`, or undefined outside Vite.
33
+ * @param params.nodeEnv - `process.env`, or undefined outside Node.
34
+ * @returns The value, or an empty string when none is set.
35
+ */
36
+ export declare function readEnvFrom({ name, viteEnv, nodeEnv, }: {
37
+ name: string;
38
+ viteEnv?: Record<string, string | undefined>;
39
+ nodeEnv?: Record<string, string | undefined>;
40
+ }): string;
26
41
  /** Returns true if a provider should be hidden from user-facing flows. */
27
42
  export declare function isProviderDisabled(key: string, configDisabled?: boolean): boolean;
28
43
  import type { TokenInfo } from "./tokens.js";
@@ -299,6 +314,41 @@ export declare const dataEncoding: (data: string) => string;
299
314
  export declare const defaultZelleSendLink: (options: SendLinkOptions) => string | null;
300
315
  /** Global configuration for all supported sub-providers. Other modules can import this directly. */
301
316
  export declare const subProviderConfigs: Partial<Record<ProviderKey, Record<SubProviderKey, PaymentSubProviderConfig>>>;
317
+ /**
318
+ * Verifier addresses for one payment method on one chain.
319
+ *
320
+ * Prefers the v2-specific verifier, so v2 history stays correctly categorized
321
+ * on chains that also run v3. Falls back to the v3 unified verifier for v3-only
322
+ * chains and v3-only payment methods.
323
+ *
324
+ * @param params.verifierKey - Key of the v2 verifier, such as `venmoReclaimVerifier`.
325
+ * @param params.v2Contracts - The chain's v2 contract set, if any.
326
+ * @param params.v3Contracts - The chain's v3 contract set, if any.
327
+ * @returns Verifier addresses; empty when neither generation names one.
328
+ */
329
+ export declare function resolveVerifierAddresses({ verifierKey, v2Contracts, v3Contracts, }: {
330
+ verifierKey: string;
331
+ v2Contracts?: Partial<Record<string, unknown>>;
332
+ v3Contracts?: {
333
+ unifiedPaymentVerifier?: readonly string[];
334
+ };
335
+ }): string[];
336
+ /**
337
+ * The icon, color and username label a sub-provider shows. Each falls back to
338
+ * the parent rail's value when the sub-provider does not set its own.
339
+ *
340
+ * @param params.subProvider - The sub-provider config.
341
+ * @param params.parent - The parent rail's config.
342
+ * @returns The resolved icon, color and username label.
343
+ */
344
+ export declare function inheritSubProviderLook({ subProvider, parent, }: {
345
+ subProvider: Pick<PaymentSubProviderConfig, 'icon' | 'color' | 'usernameType'>;
346
+ parent: Pick<PaymentConfig, 'icon' | 'color' | 'usernameType'>;
347
+ }): {
348
+ icon: string;
349
+ color: string;
350
+ usernameType: string;
351
+ };
302
352
  /** Payment method icon mappings */
303
353
  export declare const providerConfigs: (chainId: ChainId) => Record<ProviderKey, PaymentConfig>;
304
354
  /**
package/dist/payment.js CHANGED
@@ -27,13 +27,26 @@ import { getAddress, parseUnits, zeroAddress, keccak256, stringToHex, isAddress
27
27
  * Read an env var from Vite (browser) or Node, returning empty string if not set.
28
28
  */
29
29
  function readEnv(name) {
30
- const viteVal = typeof import.meta !== 'undefined'
31
- ? import.meta.env?.[`VITE_${name}`]
32
- : undefined;
33
- const nodeVal = typeof process !== 'undefined'
34
- ? process.env?.[name] ?? process.env?.[`VITE_${name}`]
35
- : undefined;
36
- return viteVal ?? nodeVal ?? '';
30
+ return readEnvFrom({
31
+ name,
32
+ viteEnv: import.meta.env,
33
+ nodeEnv: globalThis.process?.env,
34
+ });
35
+ }
36
+ /**
37
+ * Pick a value from the Vite (browser) and Node environments.
38
+ *
39
+ * Vite wins over Node. Within Node, the bare name wins over its `VITE_` twin.
40
+ *
41
+ * @param params.name - Variable name without the `VITE_` prefix.
42
+ * @param params.viteEnv - `import.meta.env`, or undefined outside Vite.
43
+ * @param params.nodeEnv - `process.env`, or undefined outside Node.
44
+ * @returns The value, or an empty string when none is set.
45
+ */
46
+ export function readEnvFrom({ name, viteEnv, nodeEnv, }) {
47
+ const viteValue = viteEnv?.[`VITE_${name}`];
48
+ const nodeValue = nodeEnv?.[name] ?? nodeEnv?.[`VITE_${name}`];
49
+ return viteValue ?? nodeValue ?? '';
37
50
  }
38
51
  /**
39
52
  * Parse a comma-separated list of provider keys from an env var.
@@ -287,6 +300,40 @@ export const subProviderConfigs = {
287
300
  },
288
301
  },
289
302
  };
303
+ /**
304
+ * Verifier addresses for one payment method on one chain.
305
+ *
306
+ * Prefers the v2-specific verifier, so v2 history stays correctly categorized
307
+ * on chains that also run v3. Falls back to the v3 unified verifier for v3-only
308
+ * chains and v3-only payment methods.
309
+ *
310
+ * @param params.verifierKey - Key of the v2 verifier, such as `venmoReclaimVerifier`.
311
+ * @param params.v2Contracts - The chain's v2 contract set, if any.
312
+ * @param params.v3Contracts - The chain's v3 contract set, if any.
313
+ * @returns Verifier addresses; empty when neither generation names one.
314
+ */
315
+ export function resolveVerifierAddresses({ verifierKey, v2Contracts, v3Contracts, }) {
316
+ if (v2Contracts && verifierKey in v2Contracts) {
317
+ const value = v2Contracts[verifierKey];
318
+ return Array.isArray(value) ? value : [];
319
+ }
320
+ return [...(v3Contracts?.unifiedPaymentVerifier ?? [])];
321
+ }
322
+ /**
323
+ * The icon, color and username label a sub-provider shows. Each falls back to
324
+ * the parent rail's value when the sub-provider does not set its own.
325
+ *
326
+ * @param params.subProvider - The sub-provider config.
327
+ * @param params.parent - The parent rail's config.
328
+ * @returns The resolved icon, color and username label.
329
+ */
330
+ export function inheritSubProviderLook({ subProvider, parent, }) {
331
+ return {
332
+ icon: subProvider.icon || parent.icon,
333
+ color: subProvider.color || parent.color,
334
+ usernameType: subProvider.usernameType || parent.usernameType,
335
+ };
336
+ }
290
337
  /** Payment method icon mappings */
291
338
  export const providerConfigs = (chainId) => {
292
339
  const v2Contracts = contracts.v2[chainId];
@@ -296,18 +343,7 @@ export const providerConfigs = (chainId) => {
296
343
  * Prioritizes v2-specific verifiers over v3 unified verifier.
297
344
  * This ensures v2 historical data is correctly categorized even on chains with both v2 and v3 contracts.
298
345
  */
299
- const getV2Verifiers = (verifierKey) => {
300
- // First check if v2 contracts have specific verifiers for this method
301
- if (v2Contracts && verifierKey in v2Contracts) {
302
- const value = v2Contracts[verifierKey];
303
- return (Array.isArray(value) ? value : []);
304
- }
305
- // Fall back to v3 unified verifier (for v3-only chains or v3-only payment methods)
306
- if (v3Contracts && 'unifiedPaymentVerifier' in v3Contracts) {
307
- return v3Contracts.unifiedPaymentVerifier || [];
308
- }
309
- return [];
310
- };
346
+ const getV2Verifiers = (verifierKey) => resolveVerifierAddresses({ verifierKey, v2Contracts, v3Contracts });
311
347
  return {
312
348
  venmo: {
313
349
  key: providers.VENMO,
@@ -821,9 +857,7 @@ export const providerByContractId = (chainId) => {
821
857
  key: subProvider.key,
822
858
  contractId: providerKeyToContractId(subProvider.key),
823
859
  name: subProvider.name,
824
- icon: subProvider.icon || config.icon,
825
- color: subProvider.color || config.color,
826
- usernameType: subProvider.usernameType || config.usernameType,
860
+ ...inheritSubProviderLook({ subProvider, parent: config }),
827
861
  v2Verifiers: subProvider.v2Verifiers(chainId),
828
862
  subProviders: new Set(),
829
863
  };
@@ -864,14 +898,8 @@ export const verifierGetsKey = (chainId, verifierAddress) => {
864
898
  // If this is a 32-byte hash (66 chars including 0x), look it up directly in the map
865
899
  if (verifierAddress?.length === 66) {
866
900
  const key = paymentVerifierToKey(chainId).get(verifierAddress.toLowerCase());
867
- if (key)
868
- return key;
869
- // Fallback: try looking it up via contractId
870
- const contractProvider = getProviderByContractId(chainId ?? base.id, verifierAddress);
871
- if (contractProvider) {
872
- return contractProvider.key;
873
- }
874
- return providers.MANUAL;
901
+ // The map holds every provider and sub-provider contract id, so a miss is unknown.
902
+ return key ?? providers.MANUAL;
875
903
  }
876
904
  // Otherwise treat it as an address
877
905
  try {
@@ -941,7 +969,7 @@ export const getPaymentMethodName = (chainId, verifierAddress) => {
941
969
  if (zelleSubConfig?.name) {
942
970
  return zelleSubConfig.name;
943
971
  }
944
- if (typeof verifierAddress === 'string' && verifierAddress.length === 66) {
972
+ if (verifierAddress.length === 66) {
945
973
  const contractProvider = getProviderByContractId(chainId ?? base.id, verifierAddress);
946
974
  if (contractProvider) {
947
975
  return contractProvider.name;
@@ -949,7 +977,7 @@ export const getPaymentMethodName = (chainId, verifierAddress) => {
949
977
  return 'Unknown';
950
978
  }
951
979
  const config = providerConfigs(chainId ?? base.id)[verifierAddress]
952
- ?? (typeof verifierAddress === 'string' && verifierAddress.length === 42
980
+ ?? (verifierAddress.length === 42
953
981
  ? getPaymentMethodConfig(chainId ?? base.id, verifierAddress)
954
982
  : null);
955
983
  return config?.name || 'Unknown';
@@ -998,7 +1026,7 @@ export function calculateEffectiveMinimum(chainId, userMinAmountWei, tokenInfo,
998
1026
  * @returns The minimum limit
999
1027
  */
1000
1028
  export function getPaymentProviderMinimum(chainId, key) {
1001
- return getPaymentMethodMinimum(chainId, key) ?? 0;
1029
+ return getPaymentMethodMinimum(chainId, key);
1002
1030
  }
1003
1031
  /**
1004
1032
  * External/unregistered payment methods that are known but not in providerConfigs.
@@ -1253,7 +1281,7 @@ export function getCurrenciesForProvider(config) {
1253
1281
  */
1254
1282
  export function currencyCapableProviders(chainId, currency) {
1255
1283
  const configs = providerConfigs(chainId);
1256
- return verifiablePaymentMethods(chainId).filter((key) => configs[key]?.list.includes(currency.contractId) ?? false);
1284
+ return verifiablePaymentMethods(chainId).filter((key) => configs[key].list.includes(currency.contractId));
1257
1285
  }
1258
1286
  /**
1259
1287
  * Whether a provider can actually serve a buyer paying in `currency` on this
@@ -1292,12 +1320,10 @@ export function selectVolumeDefaultProvider(args) {
1292
1320
  if (capableKeys.has(key))
1293
1321
  return key;
1294
1322
  }
1295
- return capable[0] ?? null;
1323
+ return capable[0];
1296
1324
  }
1297
1325
  /** Set of all sub-provider keys for fast lookup */
1298
1326
  const subProviderKeySet = new Set(Object.values(subProviders));
1299
- /** Set of all main provider keys for fast lookup */
1300
- const providerKeySet = new Set(Object.values(providers));
1301
1327
  /**
1302
1328
  * Check if a provider key is a sub-provider (e.g., 'zelle-chase', 'zelle-bofa')
1303
1329
  * @param key - The provider key to check
@@ -1317,11 +1343,8 @@ export function getParentProviderKey(key) {
1317
1343
  return null;
1318
1344
  }
1319
1345
  // Sub-provider keys follow the pattern '{parent}-{subName}'
1320
- const parentKey = key.split('-')[0];
1321
- if (parentKey && providerKeySet.has(parentKey)) {
1322
- return parentKey;
1323
- }
1324
- return null;
1346
+ // Every sub-provider's parent is a provider key (pinned by a test).
1347
+ return key.split('-')[0];
1325
1348
  }
1326
1349
  /**
1327
1350
  * Collapse a sub-provider key to the rail it belongs to.
@@ -1349,8 +1372,6 @@ export function getParentProviderKey(key) {
1349
1372
  * @returns The rail key, e.g. `'zelle'`.
1350
1373
  */
1351
1374
  export function resolveRailProviderKey(key) {
1352
- if (!isSubProviderKey(key))
1353
- return key;
1354
1375
  return getParentProviderKey(key) ?? key;
1355
1376
  }
1356
1377
  /**
@@ -1420,14 +1441,14 @@ export function getProviderIconStack(chainId, key) {
1420
1441
  // Check if this is a sub-provider
1421
1442
  const parentKey = getParentProviderKey(key);
1422
1443
  if (parentKey) {
1444
+ // A sub-provider key always has a parent config and its own entry.
1423
1445
  const parentConfig = configs[parentKey];
1424
- const subConfig = subProviderConfigs[parentKey]?.[key];
1425
- if (parentConfig && subConfig) {
1426
- return [
1427
- { icon: parentConfig.icon, color: parentConfig.color, name: parentConfig.name },
1428
- { icon: subConfig.icon ?? parentConfig.icon, color: subConfig.color ?? parentConfig.color, name: subConfig.name },
1429
- ];
1430
- }
1446
+ const subConfig = subProviderConfigs[parentKey][key];
1447
+ const { icon, color } = inheritSubProviderLook({ subProvider: subConfig, parent: parentConfig });
1448
+ return [
1449
+ { icon: parentConfig.icon, color: parentConfig.color, name: parentConfig.name },
1450
+ { icon, color, name: subConfig.name },
1451
+ ];
1431
1452
  }
1432
1453
  // Main provider - return single icon
1433
1454
  const config = configs[key];
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @fileoverview Tests for payment lookups by key, hash and address, and for
3
+ * the pure helpers that resolve verifiers, environment values and sub-provider looks.
4
+ */
5
+ export {};
@@ -0,0 +1,169 @@
1
+ /**
2
+ * @fileoverview Tests for payment lookups by key, hash and address, and for
3
+ * the pure helpers that resolve verifiers, environment values and sub-provider looks.
4
+ */
5
+ import { describe, it } from 'node:test';
6
+ import assert from 'node:assert/strict';
7
+ import { base, pulsechain } from 'viem/chains';
8
+ import { getParentProviderKey, getPaymentMethodConfig, getPaymentMethodMinimum, getPaymentMethodName, getPaymentProviderMinimum, getProviderIconStack, inheritSubProviderLook, providerConfigs, providerKeyToContractId, providers, readEnvFrom, resolveVerifierAddresses, subProviders, verifierGetsKey, } from './payment.js';
9
+ const A = '0x1111111111111111111111111111111111111111';
10
+ const B = '0x2222222222222222222222222222222222222222';
11
+ describe('readEnvFrom', () => {
12
+ it('prefers the Vite value over Node, because the browser build is the deployed one', () => {
13
+ const value = readEnvFrom({
14
+ name: 'X',
15
+ viteEnv: { VITE_X: 'vite' },
16
+ nodeEnv: { X: 'node' },
17
+ });
18
+ assert.equal(value, 'vite');
19
+ });
20
+ it('prefers the bare Node name over its VITE_ twin', () => {
21
+ assert.equal(readEnvFrom({ name: 'X', nodeEnv: { X: 'bare', VITE_X: 'twin' } }), 'bare');
22
+ });
23
+ it('falls back to the VITE_ twin in Node', () => {
24
+ assert.equal(readEnvFrom({ name: 'X', nodeEnv: { VITE_X: 'twin' } }), 'twin');
25
+ });
26
+ it('returns an empty string when nothing is set or no environment exists', () => {
27
+ assert.equal(readEnvFrom({ name: 'X', viteEnv: {}, nodeEnv: {} }), '');
28
+ assert.equal(readEnvFrom({ name: 'X' }), '');
29
+ });
30
+ });
31
+ describe('resolveVerifierAddresses', () => {
32
+ it('returns the v2-specific verifiers when the v2 set names the method', () => {
33
+ const result = resolveVerifierAddresses({
34
+ verifierKey: 'venmoReclaimVerifier',
35
+ v2Contracts: { venmoReclaimVerifier: [A] },
36
+ v3Contracts: { unifiedPaymentVerifier: [B] },
37
+ });
38
+ assert.deepEqual(result, [A]);
39
+ });
40
+ it('returns nothing, not the v3 verifier, when the v2 entry is not a list', () => {
41
+ // A v2 key that holds a single address (like `usdc`) is not a verifier list.
42
+ // Falling to v3 there would mislabel v2 history as v3.
43
+ const result = resolveVerifierAddresses({
44
+ verifierKey: 'usdc',
45
+ v2Contracts: { usdc: A },
46
+ v3Contracts: { unifiedPaymentVerifier: [B] },
47
+ });
48
+ assert.deepEqual(result, []);
49
+ });
50
+ it('falls back to the v3 unified verifier when v2 does not name the method', () => {
51
+ const result = resolveVerifierAddresses({
52
+ verifierKey: 'paypalReclaimVerifier',
53
+ v2Contracts: { venmoReclaimVerifier: [A] },
54
+ v3Contracts: { unifiedPaymentVerifier: [B] },
55
+ });
56
+ assert.deepEqual(result, [B]);
57
+ });
58
+ it('falls back to the v3 unified verifier on a chain with no v2 set', () => {
59
+ assert.deepEqual(resolveVerifierAddresses({ verifierKey: 'k', v3Contracts: { unifiedPaymentVerifier: [B] } }), [B]);
60
+ });
61
+ it('returns an empty list when neither generation names a verifier', () => {
62
+ assert.deepEqual(resolveVerifierAddresses({ verifierKey: 'k' }), []);
63
+ assert.deepEqual(resolveVerifierAddresses({ verifierKey: 'k', v3Contracts: {} }), []);
64
+ });
65
+ });
66
+ describe('inheritSubProviderLook', () => {
67
+ const parent = { icon: 'parent-icon', color: 'parent-color', usernameType: 'Parent Label' };
68
+ it('uses the sub-provider values when it sets them', () => {
69
+ const look = inheritSubProviderLook({
70
+ subProvider: { icon: 'sub-icon', color: 'sub-color', usernameType: 'Sub Label' },
71
+ parent,
72
+ });
73
+ assert.deepEqual(look, { icon: 'sub-icon', color: 'sub-color', usernameType: 'Sub Label' });
74
+ });
75
+ it('inherits each value from the parent rail when the sub-provider omits it', () => {
76
+ assert.deepEqual(inheritSubProviderLook({ subProvider: {}, parent }), parent);
77
+ });
78
+ });
79
+ describe('getParentProviderKey', () => {
80
+ it('names a real rail for every sub-provider, so the rail can always be looked up', () => {
81
+ const railKeys = new Set(Object.values(providers));
82
+ for (const subProviderKey of Object.values(subProviders)) {
83
+ const parent = getParentProviderKey(subProviderKey);
84
+ assert.ok(parent !== null && railKeys.has(parent), `${subProviderKey} has no parent rail`);
85
+ }
86
+ });
87
+ it('returns null for a key that is not a sub-provider', () => {
88
+ assert.equal(getParentProviderKey('venmo'), null);
89
+ assert.equal(getParentProviderKey('zelle-unknown'), null);
90
+ });
91
+ });
92
+ describe('getProviderIconStack', () => {
93
+ it('stacks the parent rail icon under the bank icon for a sub-provider', () => {
94
+ const configs = providerConfigs(base.id);
95
+ const stack = getProviderIconStack(base.id, subProviders.CHASE);
96
+ assert.equal(stack.length, 2);
97
+ assert.equal(stack[0].icon, configs.zelle.icon);
98
+ assert.equal(stack[1].name, 'Chase');
99
+ assert.equal(stack[1].icon, 'simple-icons:chase');
100
+ // Chase sets no color of its own, so it takes the rail's.
101
+ assert.equal(stack[1].color, configs.zelle.color);
102
+ });
103
+ it('returns a single entry for a main rail', () => {
104
+ const stack = getProviderIconStack(base.id, providers.VENMO);
105
+ assert.equal(stack.length, 1);
106
+ assert.equal(stack[0].name, 'Venmo');
107
+ });
108
+ it('names an unrecognised key Unknown, with the manual icon, instead of throwing', () => {
109
+ const stack = getProviderIconStack(base.id, 'nonsense');
110
+ assert.deepEqual(stack, [
111
+ { icon: providerConfigs(base.id).manual.icon, color: providerConfigs(base.id).manual.color, name: 'Unknown' },
112
+ ]);
113
+ });
114
+ });
115
+ describe('minimum limits', () => {
116
+ it('reads the minimum for a provider on the default chain when no chain is given', () => {
117
+ assert.equal(getPaymentMethodMinimum(null, providers.VENMO), providerConfigs(base.id).venmo.limits.min);
118
+ });
119
+ it('returns the same minimum through the provider-level helper', () => {
120
+ assert.equal(getPaymentProviderMinimum(pulsechain.id, providers.VENMO), getPaymentMethodMinimum(pulsechain.id, providers.VENMO));
121
+ });
122
+ it('returns zero for an unknown provider rather than NaN', () => {
123
+ assert.equal(getPaymentMethodMinimum(base.id, 'nonsense'), 0);
124
+ });
125
+ });
126
+ describe('lookup by payment-method hash', () => {
127
+ const zelleChaseHash = providerKeyToContractId(subProviders.CHASE);
128
+ const unknownHash = `0x${'ab'.repeat(32)}`;
129
+ it('verifierGetsKey resolves a sub-provider hash through the contract id map', () => {
130
+ assert.equal(verifierGetsKey(base.id, zelleChaseHash), subProviders.CHASE);
131
+ });
132
+ it('verifierGetsKey defaults to the base chain when none is given', () => {
133
+ assert.equal(verifierGetsKey(null, providerKeyToContractId(providers.VENMO)), providers.VENMO);
134
+ });
135
+ it('getPaymentMethodConfig returns the config for a known hash and null for an unknown one', () => {
136
+ assert.equal(getPaymentMethodConfig(base.id, providerKeyToContractId(providers.WISE))?.key, providers.WISE);
137
+ assert.equal(getPaymentMethodConfig(null, providerKeyToContractId(providers.WISE))?.key, providers.WISE);
138
+ assert.equal(getPaymentMethodConfig(base.id, unknownHash), null);
139
+ });
140
+ it('getPaymentMethodConfig returns null for text that is neither an address nor a hash', () => {
141
+ assert.equal(getPaymentMethodConfig(base.id, 'venmo'), null);
142
+ });
143
+ it('getPaymentMethodConfig finds a v2 verifier address on every chain when none is given', () => {
144
+ const venmoVerifier = providerConfigs(base.id).venmo.v2Verifiers[0];
145
+ assert.equal(getPaymentMethodConfig(null, venmoVerifier)?.key, providers.VENMO);
146
+ });
147
+ it('getPaymentMethodName names a hash, falls back to Unknown for a strange one', () => {
148
+ assert.equal(getPaymentMethodName(base.id, zelleChaseHash), 'Chase');
149
+ assert.equal(getPaymentMethodName(base.id, unknownHash), 'Unknown');
150
+ });
151
+ it('getPaymentMethodName searches the default chain for every kind of input when none is given', () => {
152
+ const venmoVerifier = providerConfigs(base.id).venmo.v2Verifiers[0];
153
+ assert.equal(getPaymentMethodName(null, zelleChaseHash), 'Chase');
154
+ assert.equal(getPaymentMethodName(null, venmoVerifier), 'Venmo');
155
+ assert.equal(getPaymentMethodName(null, 'nonsense'), 'Unknown');
156
+ });
157
+ it('getPaymentMethodName names a provider key, on the default chain when none is given', () => {
158
+ assert.equal(getPaymentMethodName(null, providers.VENMO), 'Venmo');
159
+ });
160
+ it('getPaymentMethodName names a sub-provider key', () => {
161
+ assert.equal(getPaymentMethodName(base.id, subProviders.CITI), 'Citi');
162
+ });
163
+ it('getPaymentMethodName names a verifier address, and says Unknown for a stranger', () => {
164
+ const venmoVerifier = providerConfigs(base.id).venmo.v2Verifiers[0];
165
+ assert.equal(getPaymentMethodName(base.id, venmoVerifier), 'Venmo');
166
+ assert.equal(getPaymentMethodName(base.id, '0x0000000000000000000000000000000000000dEaD'.slice(0, 42)), 'Unknown');
167
+ assert.equal(getPaymentMethodName(base.id, 'nonsense'), 'Unknown');
168
+ });
169
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @fileoverview Tests for the deployment switches that hide or force-show a
3
+ * payment rail.
4
+ *
5
+ * `DISABLED_PROVIDERS` and `ENABLED_PROVIDERS` are read once, on the first call,
6
+ * and cached in module state. Each test file runs in its own process, so this
7
+ * file sets both variables before any call and observes one fixed deployment:
8
+ *
9
+ * - disabled: `PayPal` and `zelle` (mixed case and padded, as a human writes it)
10
+ * - enabled: `venmo` and `paypal`
11
+ */
12
+ export {};