@provex/utils 1.9.2 → 1.10.0-rc.20260912192040.fdbb8fc
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/chain.d.ts +13 -0
- package/dist/chain.js +13 -0
- package/dist/contracts.config.test.d.ts +18 -0
- package/dist/contracts.config.test.js +220 -0
- package/dist/contracts.d.ts +169 -29
- package/dist/contracts.js +188 -31
- package/dist/contracts.test.js +134 -7
- package/dist/deposit.d.ts +50 -0
- package/dist/deposit.js +45 -0
- package/dist/events.d.ts +2 -2
- package/dist/events.js +20 -0
- package/dist/payment.currency.test.d.ts +11 -0
- package/dist/payment.currency.test.js +134 -0
- package/dist/payment.d.ts +149 -23
- package/dist/payment.js +170 -29
- package/dist/payment.rails.test.d.ts +25 -0
- package/dist/payment.rails.test.js +213 -0
- package/dist/payment.test.js +73 -51
- package/dist/rateInput.d.ts +163 -0
- package/dist/rateInput.js +173 -0
- package/dist/rateInput.test.d.ts +1 -0
- package/dist/rateInput.test.js +318 -0
- package/dist/tokens.d.ts +15068 -15037
- package/dist/tokens.js +37 -8
- package/dist/tokens.test.js +11 -0
- package/dist/transports.d.ts +86 -8
- package/dist/transports.js +126 -10
- package/dist/transports.test.js +131 -5
- package/dist/units.d.ts +9 -0
- package/dist/units.js +9 -0
- package/package.json +5 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for currency-aware payment-provider selection.
|
|
3
|
+
*
|
|
4
|
+
* These exist because the Buy page auto-picked a payment method without ever
|
|
5
|
+
* consulting the buyer's currency. The volume leaders on every chain are
|
|
6
|
+
* United States dollar rails (Venmo, Zelle, Cash App), so a buyer who chose
|
|
7
|
+
* pounds or euros was seeded onto a rail that structurally cannot serve them
|
|
8
|
+
* and shown an empty order book. Each test below names the behaviour that
|
|
9
|
+
* failure would break, not just the value it returns.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it } from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { pulsechain } from 'viem/chains';
|
|
14
|
+
import { providers, providerSupportsCurrency, selectVolumeDefaultProvider } from './payment.js';
|
|
15
|
+
import { SUPPORTED_CURRENCIES } from './currencies.js';
|
|
16
|
+
// Which providers are verifiable is DEPLOYMENT CONFIGURATION, not a fact about
|
|
17
|
+
// the chain: `DISABLED_PROVIDERS` / `ENABLED_PROVIDERS` override the `disabled`
|
|
18
|
+
// field in the registry, and staging already ships
|
|
19
|
+
// `vite_enabled_providers: "paypal"`. The PayPal cases below passed only
|
|
20
|
+
// because this process happens to inherit neither variable — the same
|
|
21
|
+
// assertions failed the moment they ran under the user-interface test runner,
|
|
22
|
+
// which loads an environment file that enables PayPal.
|
|
23
|
+
//
|
|
24
|
+
// Pin it rather than depend on the ambient value. The disabled list wins over
|
|
25
|
+
// the enabled list, so this holds whatever the environment carries.
|
|
26
|
+
//
|
|
27
|
+
// Must be set before the first call into `payment.ts`, which memoizes both
|
|
28
|
+
// lists on first use. Module scope is early enough; a `before()` hook is not,
|
|
29
|
+
// once another suite in the same process has already called in.
|
|
30
|
+
process.env.DISABLED_PROVIDERS = 'paypal';
|
|
31
|
+
const chainId = pulsechain.id;
|
|
32
|
+
describe('providerSupportsCurrency', () => {
|
|
33
|
+
it('refuses a United States dollar rail for a pounds buyer', () => {
|
|
34
|
+
// Venmo lists only the United States dollar. If this ever passes, the
|
|
35
|
+
// predicate has stopped reading the provider's currency list and the
|
|
36
|
+
// empty-order-book bug is back.
|
|
37
|
+
assert.equal(providerSupportsCurrency({
|
|
38
|
+
providerKey: providers.VENMO,
|
|
39
|
+
chainId,
|
|
40
|
+
currency: SUPPORTED_CURRENCIES.GBP,
|
|
41
|
+
}), false);
|
|
42
|
+
});
|
|
43
|
+
it('accepts a United States dollar rail for a dollar buyer', () => {
|
|
44
|
+
assert.equal(providerSupportsCurrency({
|
|
45
|
+
providerKey: providers.VENMO,
|
|
46
|
+
chainId,
|
|
47
|
+
currency: SUPPORTED_CURRENCIES.USD,
|
|
48
|
+
}), true);
|
|
49
|
+
});
|
|
50
|
+
it('accepts a pounds-only rail for a pounds buyer', () => {
|
|
51
|
+
// Monzo is pounds-only — the mirror image of the Venmo case, so a
|
|
52
|
+
// predicate hardcoded to either currency fails one of the two.
|
|
53
|
+
assert.equal(providerSupportsCurrency({
|
|
54
|
+
providerKey: providers.MONZO,
|
|
55
|
+
chainId,
|
|
56
|
+
currency: SUPPORTED_CURRENCIES.GBP,
|
|
57
|
+
}), true);
|
|
58
|
+
});
|
|
59
|
+
it('refuses a provider this deployment does not offer', () => {
|
|
60
|
+
// PayPal supports pounds in its own config but is switched off here.
|
|
61
|
+
// Offering it would seed a provider the buy flow cannot use.
|
|
62
|
+
assert.equal(providerSupportsCurrency({
|
|
63
|
+
providerKey: providers.PAYPAL,
|
|
64
|
+
chainId,
|
|
65
|
+
currency: SUPPORTED_CURRENCIES.GBP,
|
|
66
|
+
}), false);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe('selectVolumeDefaultProvider', () => {
|
|
70
|
+
it('skips higher-volume dollar rails to reach one that serves pounds', () => {
|
|
71
|
+
// The regression test for the reported bug. Venmo leads on volume, so a
|
|
72
|
+
// currency-blind ranker returns it and the pounds buyer sees no liquidity.
|
|
73
|
+
const picked = selectVolumeDefaultProvider({
|
|
74
|
+
verifiersByVolume: [providers.VENMO, providers.ZELLE, providers.REVOLUT],
|
|
75
|
+
chainId,
|
|
76
|
+
currency: SUPPORTED_CURRENCIES.GBP,
|
|
77
|
+
});
|
|
78
|
+
assert.equal(picked, providers.REVOLUT);
|
|
79
|
+
});
|
|
80
|
+
it('still returns the volume leader for a dollar buyer', () => {
|
|
81
|
+
// Guards the other direction: the currency filter must not disturb the
|
|
82
|
+
// existing dollar behaviour, which is the overwhelming majority of traffic.
|
|
83
|
+
const picked = selectVolumeDefaultProvider({
|
|
84
|
+
verifiersByVolume: [providers.VENMO, providers.ZELLE, providers.REVOLUT],
|
|
85
|
+
chainId,
|
|
86
|
+
currency: SUPPORTED_CURRENCIES.USD,
|
|
87
|
+
});
|
|
88
|
+
assert.equal(picked, providers.VENMO);
|
|
89
|
+
});
|
|
90
|
+
it('falls back to a capable provider when the volume list has none', () => {
|
|
91
|
+
// Volume data only ever covers rails that have traded. A pounds buyer
|
|
92
|
+
// arriving before any pounds volume exists must still land somewhere
|
|
93
|
+
// usable rather than on the dollar leader.
|
|
94
|
+
const picked = selectVolumeDefaultProvider({
|
|
95
|
+
verifiersByVolume: [providers.VENMO, providers.ZELLE, providers.CASHAPP],
|
|
96
|
+
chainId,
|
|
97
|
+
currency: SUPPORTED_CURRENCIES.GBP,
|
|
98
|
+
});
|
|
99
|
+
// Asserted against a literal set, not against providerSupportsCurrency —
|
|
100
|
+
// routing the assertion through the same predicate under test lets a
|
|
101
|
+
// broken predicate satisfy it. Mutation testing caught exactly that.
|
|
102
|
+
assert.ok(picked !== null && [providers.REVOLUT, providers.WISE, providers.MONZO].includes(picked), `expected a pounds-capable provider, got ${String(picked)}`);
|
|
103
|
+
});
|
|
104
|
+
it('finds the sole provider for a single-rail currency', () => {
|
|
105
|
+
// MercadoPago is the only rail for Argentine pesos, and it never leads on
|
|
106
|
+
// volume, so a ranker that only reorders the volume list never reaches it.
|
|
107
|
+
const picked = selectVolumeDefaultProvider({
|
|
108
|
+
verifiersByVolume: [providers.VENMO, providers.ZELLE],
|
|
109
|
+
chainId,
|
|
110
|
+
currency: SUPPORTED_CURRENCIES.ARS,
|
|
111
|
+
});
|
|
112
|
+
assert.equal(picked, providers.MERCADOPAGO);
|
|
113
|
+
});
|
|
114
|
+
it('returns null when no verifiable provider serves the currency', () => {
|
|
115
|
+
// Roubles have no verifiable rail on PulseChain. Returning a provider here
|
|
116
|
+
// would be a lie the caller cannot detect; null lets it decide what to show.
|
|
117
|
+
const picked = selectVolumeDefaultProvider({
|
|
118
|
+
verifiersByVolume: [providers.VENMO, providers.ZELLE],
|
|
119
|
+
chainId,
|
|
120
|
+
currency: SUPPORTED_CURRENCIES.RUB,
|
|
121
|
+
});
|
|
122
|
+
assert.equal(picked, null);
|
|
123
|
+
});
|
|
124
|
+
it('ignores a volume entry this deployment does not offer', () => {
|
|
125
|
+
// Stats come from the indexer and can name a rail that is switched off
|
|
126
|
+
// here. Such an entry must never win, even when it lists the currency.
|
|
127
|
+
const picked = selectVolumeDefaultProvider({
|
|
128
|
+
verifiersByVolume: [providers.PAYPAL, providers.REVOLUT],
|
|
129
|
+
chainId,
|
|
130
|
+
currency: SUPPORTED_CURRENCIES.GBP,
|
|
131
|
+
});
|
|
132
|
+
assert.equal(picked, providers.REVOLUT);
|
|
133
|
+
});
|
|
134
|
+
});
|
package/dist/payment.d.ts
CHANGED
|
@@ -459,13 +459,25 @@ export declare const getAllPaymentMethods: (chainId: ChainId, includeSubProvider
|
|
|
459
459
|
export declare const verifiablePaymentMethods: (chainId: ChainId, includeSubProviders?: boolean) => ProviderKey[];
|
|
460
460
|
/**
|
|
461
461
|
* Compute every payment-method id (lowercased, 32-byte keccak hex) that
|
|
462
|
-
* attaching the given
|
|
462
|
+
* attaching the given provider would register on a deposit.
|
|
463
463
|
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
464
|
+
* **One id per provider — the rail's own, never a fan-out.** This used to
|
|
465
|
+
* expand a provider with sub-providers (only Zelle has any) into one id per
|
|
466
|
+
* bank, mirroring an `addPaymentMethods` call that submitted the same fan-out.
|
|
467
|
+
* That submission is gone: a Zelle attestation is issued for the rail, so
|
|
468
|
+
* `AddPlatformModal` now writes `keccak256("zelle")` alone (see
|
|
469
|
+
* {@link resolveRailProviderKey}). Leaving the expansion here desynchronized
|
|
470
|
+
* the availability gate from the write in both directions:
|
|
471
|
+
*
|
|
472
|
+
* - A deposit already holding `zelle` collided with none of the bank ids, so
|
|
473
|
+
* Zelle kept showing as addable and the re-add reverted with
|
|
474
|
+
* `PaymentMethodAlreadyExists`.
|
|
475
|
+
* - A deposit holding only the legacy bank ids collided on all of them, so the
|
|
476
|
+
* rail — the one method its intents can actually be fulfilled on — looked
|
|
477
|
+
* unavailable, blocking the very migration that repairs the deposit.
|
|
478
|
+
*
|
|
479
|
+
* Keeping this to the submitted id fixes both: the gate now answers exactly the
|
|
480
|
+
* question the contract will ask.
|
|
469
481
|
*
|
|
470
482
|
* Keys are lowercased so callers can compare directly against
|
|
471
483
|
* `Deposit.attachedPaymentMethodIds` without per-call normalization.
|
|
@@ -489,12 +501,11 @@ export interface ProviderAvailabilityArgs {
|
|
|
489
501
|
* it can still be attached to a deposit given the deposit's already-attached
|
|
490
502
|
* payment method id hashes.
|
|
491
503
|
*
|
|
492
|
-
* Important —
|
|
493
|
-
* (
|
|
494
|
-
*
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
* cannot re-add the Zelle parent without colliding on the Chase sub-row.
|
|
504
|
+
* Important — availability is decided against exactly the id the write would
|
|
505
|
+
* submit (see {@link getProviderPaymentMethodIds}), which is one id per rail.
|
|
506
|
+
* A maker holding the legacy `zelle-chase` row therefore still sees Zelle as
|
|
507
|
+
* addable, and should add it: the rail is the only method its intents can be
|
|
508
|
+
* fulfilled on, and attaching it is how such a deposit gets repaired.
|
|
498
509
|
*
|
|
499
510
|
* `manual` is always excluded from both lists.
|
|
500
511
|
*/
|
|
@@ -509,6 +520,41 @@ export declare function computeProviderAvailability(args: ProviderAvailabilityAr
|
|
|
509
520
|
* button).
|
|
510
521
|
*/
|
|
511
522
|
export declare function availableProviderConfigs(args: ProviderAvailabilityArgs): PaymentConfig[];
|
|
523
|
+
/**
|
|
524
|
+
* A deposit that attached bank-level payment methods instead of the rail, and
|
|
525
|
+
* so holds no method any attestation can satisfy.
|
|
526
|
+
*/
|
|
527
|
+
export interface LegacySubProviderAttachment {
|
|
528
|
+
/** The rail to attach to make the deposit fulfillable, e.g. `'zelle'`. */
|
|
529
|
+
rail: ProviderKey;
|
|
530
|
+
/** The bank-level keys it holds instead, e.g. `['zelle-bofa', 'zelle-chase']`. */
|
|
531
|
+
attachedSubProviderKeys: SubProviderKey[];
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Find rails this deposit attached at bank level but never at rail level.
|
|
535
|
+
*
|
|
536
|
+
* Such a deposit is not degraded — it is **inert**. `fetchMatchableDeposits`
|
|
537
|
+
* matches the rail id alone, so the deposit appears in no buyer's orderbook,
|
|
538
|
+
* and `resolveTargetedVerifier` returns null for it. Both were deliberate:
|
|
539
|
+
* bank-level rows once put these deposits in front of buyers who reserved
|
|
540
|
+
* funds, paid a real person, and then could not submit.
|
|
541
|
+
*
|
|
542
|
+
* The consequence is that the maker gets **no signal at all** — no failed
|
|
543
|
+
* orders, no complaints, just silence indistinguishable from no demand. Nothing
|
|
544
|
+
* in the product tells them, which is why this predicate exists: it is the only
|
|
545
|
+
* way the interface can raise the problem.
|
|
546
|
+
*
|
|
547
|
+
* The repair is one transaction. Attaching the rail makes the deposit
|
|
548
|
+
* matchable, and the bank-level rows are never consulted again on any buyer
|
|
549
|
+
* path — there is nothing to deactivate. They remain visible only on the
|
|
550
|
+
* owner's own platform list, which is cosmetic.
|
|
551
|
+
*
|
|
552
|
+
* Rails already holding their own id are skipped, as are disabled rails: it
|
|
553
|
+
* would be wrong to tell a maker to attach a method the chain won't accept.
|
|
554
|
+
*
|
|
555
|
+
* @returns One entry per rail needing repair; empty when the deposit is healthy.
|
|
556
|
+
*/
|
|
557
|
+
export declare function findLegacySubProviderAttachments(args: ProviderAvailabilityArgs): LegacySubProviderAttachment[];
|
|
512
558
|
/**
|
|
513
559
|
* Get supported currencies for a payment provider.
|
|
514
560
|
* Returns only the currencies that are in the provider's supported list.
|
|
@@ -528,6 +574,68 @@ export declare function availableProviderConfigs(args: ProviderAvailabilityArgs)
|
|
|
528
574
|
* ```
|
|
529
575
|
*/
|
|
530
576
|
export declare function getCurrenciesForProvider(config: PaymentConfig): Currency[];
|
|
577
|
+
/**
|
|
578
|
+
* Verifiable providers on `chainId` whose currency list contains `currency`,
|
|
579
|
+
* in registry order.
|
|
580
|
+
*
|
|
581
|
+
* "Verifiable" and "supports the currency" are two separate gates and both
|
|
582
|
+
* matter: a rail can list pounds and still be switched off here, and offering
|
|
583
|
+
* it would seed a rail the buy flow cannot settle on.
|
|
584
|
+
*
|
|
585
|
+
* Note that verifiability is DEPLOYMENT CONFIGURATION, not a property of the
|
|
586
|
+
* chain — `DISABLED_PROVIDERS` / `ENABLED_PROVIDERS` override the registry's
|
|
587
|
+
* `disabled` field, and staging ships `vite_enabled_providers: "paypal"` while
|
|
588
|
+
* production does not. Two environments on the same chain legitimately return
|
|
589
|
+
* different lists. Never hardcode the answer for a named provider.
|
|
590
|
+
*
|
|
591
|
+
* An empty `list` means "serves no currency", not "serves every currency".
|
|
592
|
+
* `manual` is the only provider with an empty list and `verifiablePaymentMethods`
|
|
593
|
+
* already drops it, so the rule is unobservable today — but it is the rule to
|
|
594
|
+
* keep, because seeding a buyer onto a rail that declares no currency support
|
|
595
|
+
* is the bug this function exists to prevent.
|
|
596
|
+
*
|
|
597
|
+
* @example
|
|
598
|
+
* ```ts
|
|
599
|
+
* currencyCapableProviders(pulsechain.id, SUPPORTED_CURRENCIES.GBP)
|
|
600
|
+
* // ['revolut', 'wise', 'monzo'] — no Venmo, Zelle or Cash App
|
|
601
|
+
* ```
|
|
602
|
+
*/
|
|
603
|
+
export declare function currencyCapableProviders(chainId: ChainId, currency: Currency): ProviderKey[];
|
|
604
|
+
/**
|
|
605
|
+
* Whether a provider can actually serve a buyer paying in `currency` on this
|
|
606
|
+
* chain — it must be verifiable here AND list the currency.
|
|
607
|
+
*
|
|
608
|
+
* @example
|
|
609
|
+
* ```ts
|
|
610
|
+
* providerSupportsCurrency({ providerKey: providers.VENMO, chainId: pulsechain.id, currency: SUPPORTED_CURRENCIES.GBP })
|
|
611
|
+
* // false — Venmo is a United States dollar rail
|
|
612
|
+
* ```
|
|
613
|
+
*/
|
|
614
|
+
export declare function providerSupportsCurrency(args: {
|
|
615
|
+
readonly providerKey: ProviderKey;
|
|
616
|
+
readonly chainId: ChainId;
|
|
617
|
+
readonly currency: Currency;
|
|
618
|
+
}): boolean;
|
|
619
|
+
/**
|
|
620
|
+
* Pick a default payment method for a buyer paying in `currency`, preferring
|
|
621
|
+
* the highest-volume rail that can actually serve them.
|
|
622
|
+
*
|
|
623
|
+
* `verifiersByVolume` is the monthly-volume ranking, highest first. The volume
|
|
624
|
+
* leaders are United States dollar rails on every chain, so ranking without
|
|
625
|
+
* the currency gate seeds a pounds or euro buyer onto a rail that lists only
|
|
626
|
+
* dollars and shows them an empty order book. Falls back to the first capable
|
|
627
|
+
* provider in registry order when no ranked rail qualifies — volume data only
|
|
628
|
+
* covers rails that have already traded, which a new currency has not.
|
|
629
|
+
*
|
|
630
|
+
* @returns The chosen provider, or `null` when no verifiable provider on this
|
|
631
|
+
* chain serves the currency at all. Null rather than an arbitrary provider,
|
|
632
|
+
* so the caller can tell "nothing can serve this" from a real choice.
|
|
633
|
+
*/
|
|
634
|
+
export declare function selectVolumeDefaultProvider(args: {
|
|
635
|
+
readonly verifiersByVolume: readonly ProviderKey[];
|
|
636
|
+
readonly chainId: ChainId;
|
|
637
|
+
readonly currency: Currency;
|
|
638
|
+
}): ProviderKey | null;
|
|
531
639
|
/**
|
|
532
640
|
* Check if a provider key is a sub-provider (e.g., 'zelle-chase', 'zelle-bofa')
|
|
533
641
|
* @param key - The provider key to check
|
|
@@ -541,20 +649,38 @@ export declare function isSubProviderKey(key: string): key is SubProviderKey;
|
|
|
541
649
|
* @returns The parent provider key (e.g., 'zelle') or null
|
|
542
650
|
*/
|
|
543
651
|
export declare function getParentProviderKey(key: string): ProviderKey | null;
|
|
652
|
+
/**
|
|
653
|
+
* Collapse a sub-provider key to the rail it belongs to.
|
|
654
|
+
*
|
|
655
|
+
* A sub-provider names a bank the buyer captures from — it is a routing detail
|
|
656
|
+
* of the capture step, not an identity of its own. Everything keyed by rail
|
|
657
|
+
* rather than by bank MUST collapse through here:
|
|
658
|
+
*
|
|
659
|
+
* - **The on-chain payment method.** A Zelle attestation is issued for the
|
|
660
|
+
* rail: every bank template in `@zkp2p/providers` declares
|
|
661
|
+
* `metadata.platform: "zelle"`, and the enclave verify route is
|
|
662
|
+
* `zelle/transfer_zelle_{bank}` — the bank rides in the ACTION type, never
|
|
663
|
+
* the platform. So `keccak256("zelle")` is the only payment method a Zelle
|
|
664
|
+
* payment can ever satisfy. Deposits that attached `zelle-chase` and friends
|
|
665
|
+
* accept intents no attestation can fulfil.
|
|
666
|
+
* - **Off-chain maker records.** Makers register once at rail level, so a
|
|
667
|
+
* lookup keyed by `(provider, payeeDetailsHash)` misses the row unless the
|
|
668
|
+
* bank is collapsed away. Skipping this in verify-intent once produced a 400
|
|
669
|
+
* "maker contact could not be verified" for every buy on such a deposit.
|
|
670
|
+
*
|
|
671
|
+
* Non-sub-provider keys pass through unchanged, so this is safe to apply
|
|
672
|
+
* unconditionally.
|
|
673
|
+
*
|
|
674
|
+
* @param key - Provider or sub-provider key, e.g. `'zelle-chase'`.
|
|
675
|
+
* @returns The rail key, e.g. `'zelle'`.
|
|
676
|
+
*/
|
|
677
|
+
export declare function resolveRailProviderKey(key: string): string;
|
|
544
678
|
/**
|
|
545
679
|
* Canonicalize a provider key for off-chain maker-record lookups.
|
|
546
680
|
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
* `(provider, payeeDetailsHash)` MUST collapse the sub-provider back to
|
|
551
|
-
* its parent or it will miss the row.
|
|
552
|
-
*
|
|
553
|
-
* Use this anywhere you'd otherwise inline the
|
|
554
|
-
* `isSubProviderKey(k) ? getParentProviderKey(k) : k` ternary so the
|
|
555
|
-
* normalization can't drift between call sites — verify-intent regressed
|
|
556
|
-
* by skipping it and produced 400 "maker contact could not be verified"
|
|
557
|
-
* for every buy on a sub-provider deposit.
|
|
681
|
+
* @deprecated Prefer {@link resolveRailProviderKey}, which names the concept
|
|
682
|
+
* both call-site families share. Retained because the off-chain lookup sites
|
|
683
|
+
* read better with an API-flavoured name.
|
|
558
684
|
*/
|
|
559
685
|
export declare function resolveApiProviderKey(key: string): string;
|
|
560
686
|
/**
|
package/dist/payment.js
CHANGED
|
@@ -1087,35 +1087,42 @@ export const verifiablePaymentMethods = (chainId, includeSubProviders = false) =
|
|
|
1087
1087
|
};
|
|
1088
1088
|
/**
|
|
1089
1089
|
* Compute every payment-method id (lowercased, 32-byte keccak hex) that
|
|
1090
|
-
* attaching the given
|
|
1090
|
+
* attaching the given provider would register on a deposit.
|
|
1091
1091
|
*
|
|
1092
|
-
*
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1095
|
-
*
|
|
1096
|
-
*
|
|
1092
|
+
* **One id per provider — the rail's own, never a fan-out.** This used to
|
|
1093
|
+
* expand a provider with sub-providers (only Zelle has any) into one id per
|
|
1094
|
+
* bank, mirroring an `addPaymentMethods` call that submitted the same fan-out.
|
|
1095
|
+
* That submission is gone: a Zelle attestation is issued for the rail, so
|
|
1096
|
+
* `AddPlatformModal` now writes `keccak256("zelle")` alone (see
|
|
1097
|
+
* {@link resolveRailProviderKey}). Leaving the expansion here desynchronized
|
|
1098
|
+
* the availability gate from the write in both directions:
|
|
1099
|
+
*
|
|
1100
|
+
* - A deposit already holding `zelle` collided with none of the bank ids, so
|
|
1101
|
+
* Zelle kept showing as addable and the re-add reverted with
|
|
1102
|
+
* `PaymentMethodAlreadyExists`.
|
|
1103
|
+
* - A deposit holding only the legacy bank ids collided on all of them, so the
|
|
1104
|
+
* rail — the one method its intents can actually be fulfilled on — looked
|
|
1105
|
+
* unavailable, blocking the very migration that repairs the deposit.
|
|
1106
|
+
*
|
|
1107
|
+
* Keeping this to the submitted id fixes both: the gate now answers exactly the
|
|
1108
|
+
* question the contract will ask.
|
|
1097
1109
|
*
|
|
1098
1110
|
* Keys are lowercased so callers can compare directly against
|
|
1099
1111
|
* `Deposit.attachedPaymentMethodIds` without per-call normalization.
|
|
1100
1112
|
*/
|
|
1101
1113
|
export function getProviderPaymentMethodIds(config) {
|
|
1102
|
-
|
|
1103
|
-
if (subProviders && subProviders.size > 0) {
|
|
1104
|
-
return Array.from(subProviders).map(sub => providerKeyToContractId(sub.key).toLowerCase());
|
|
1105
|
-
}
|
|
1106
|
-
return [providerKeyToContractId(config.key).toLowerCase()];
|
|
1114
|
+
return [providerKeyToContractId(resolveRailProviderKey(config.key)).toLowerCase()];
|
|
1107
1115
|
}
|
|
1108
1116
|
/**
|
|
1109
1117
|
* Decide, for every non-manual non-disabled provider on `chainId`, whether
|
|
1110
1118
|
* it can still be attached to a deposit given the deposit's already-attached
|
|
1111
1119
|
* payment method id hashes.
|
|
1112
1120
|
*
|
|
1113
|
-
* Important —
|
|
1114
|
-
* (
|
|
1115
|
-
*
|
|
1116
|
-
*
|
|
1117
|
-
*
|
|
1118
|
-
* cannot re-add the Zelle parent without colliding on the Chase sub-row.
|
|
1121
|
+
* Important — availability is decided against exactly the id the write would
|
|
1122
|
+
* submit (see {@link getProviderPaymentMethodIds}), which is one id per rail.
|
|
1123
|
+
* A maker holding the legacy `zelle-chase` row therefore still sees Zelle as
|
|
1124
|
+
* addable, and should add it: the rail is the only method its intents can be
|
|
1125
|
+
* fulfilled on, and attaching it is how such a deposit gets repaired.
|
|
1119
1126
|
*
|
|
1120
1127
|
* `manual` is always excluded from both lists.
|
|
1121
1128
|
*/
|
|
@@ -1148,6 +1155,51 @@ export function computeProviderAvailability(args) {
|
|
|
1148
1155
|
export function availableProviderConfigs(args) {
|
|
1149
1156
|
return computeProviderAvailability(args).available;
|
|
1150
1157
|
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Find rails this deposit attached at bank level but never at rail level.
|
|
1160
|
+
*
|
|
1161
|
+
* Such a deposit is not degraded — it is **inert**. `fetchMatchableDeposits`
|
|
1162
|
+
* matches the rail id alone, so the deposit appears in no buyer's orderbook,
|
|
1163
|
+
* and `resolveTargetedVerifier` returns null for it. Both were deliberate:
|
|
1164
|
+
* bank-level rows once put these deposits in front of buyers who reserved
|
|
1165
|
+
* funds, paid a real person, and then could not submit.
|
|
1166
|
+
*
|
|
1167
|
+
* The consequence is that the maker gets **no signal at all** — no failed
|
|
1168
|
+
* orders, no complaints, just silence indistinguishable from no demand. Nothing
|
|
1169
|
+
* in the product tells them, which is why this predicate exists: it is the only
|
|
1170
|
+
* way the interface can raise the problem.
|
|
1171
|
+
*
|
|
1172
|
+
* The repair is one transaction. Attaching the rail makes the deposit
|
|
1173
|
+
* matchable, and the bank-level rows are never consulted again on any buyer
|
|
1174
|
+
* path — there is nothing to deactivate. They remain visible only on the
|
|
1175
|
+
* owner's own platform list, which is cosmetic.
|
|
1176
|
+
*
|
|
1177
|
+
* Rails already holding their own id are skipped, as are disabled rails: it
|
|
1178
|
+
* would be wrong to tell a maker to attach a method the chain won't accept.
|
|
1179
|
+
*
|
|
1180
|
+
* @returns One entry per rail needing repair; empty when the deposit is healthy.
|
|
1181
|
+
*/
|
|
1182
|
+
export function findLegacySubProviderAttachments(args) {
|
|
1183
|
+
const configs = providerConfigs(args.chainId);
|
|
1184
|
+
const out = [];
|
|
1185
|
+
for (const config of Object.values(configs)) {
|
|
1186
|
+
const subConfigs = subProviderConfigs[config.key];
|
|
1187
|
+
if (!subConfigs)
|
|
1188
|
+
continue;
|
|
1189
|
+
if (isProviderDisabled(config.key, config.disabled))
|
|
1190
|
+
continue;
|
|
1191
|
+
// Already holds the rail — whatever else it carries, it is fulfillable.
|
|
1192
|
+
if (args.attachedPaymentMethodIds.has(providerKeyToContractId(config.key).toLowerCase()))
|
|
1193
|
+
continue;
|
|
1194
|
+
const attachedSubProviderKeys = Object.keys(subConfigs)
|
|
1195
|
+
.filter(key => args.attachedPaymentMethodIds.has(providerKeyToContractId(key).toLowerCase()))
|
|
1196
|
+
.sort();
|
|
1197
|
+
if (attachedSubProviderKeys.length === 0)
|
|
1198
|
+
continue;
|
|
1199
|
+
out.push({ rail: config.key, attachedSubProviderKeys });
|
|
1200
|
+
}
|
|
1201
|
+
return out;
|
|
1202
|
+
}
|
|
1151
1203
|
/**
|
|
1152
1204
|
* Get supported currencies for a payment provider.
|
|
1153
1205
|
* Returns only the currencies that are in the provider's supported list.
|
|
@@ -1170,6 +1222,75 @@ export function getCurrenciesForProvider(config) {
|
|
|
1170
1222
|
const supportedIds = new Set(config.list);
|
|
1171
1223
|
return Object.values(SUPPORTED_CURRENCIES).filter((currency) => supportedIds.has(currency.contractId));
|
|
1172
1224
|
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Verifiable providers on `chainId` whose currency list contains `currency`,
|
|
1227
|
+
* in registry order.
|
|
1228
|
+
*
|
|
1229
|
+
* "Verifiable" and "supports the currency" are two separate gates and both
|
|
1230
|
+
* matter: a rail can list pounds and still be switched off here, and offering
|
|
1231
|
+
* it would seed a rail the buy flow cannot settle on.
|
|
1232
|
+
*
|
|
1233
|
+
* Note that verifiability is DEPLOYMENT CONFIGURATION, not a property of the
|
|
1234
|
+
* chain — `DISABLED_PROVIDERS` / `ENABLED_PROVIDERS` override the registry's
|
|
1235
|
+
* `disabled` field, and staging ships `vite_enabled_providers: "paypal"` while
|
|
1236
|
+
* production does not. Two environments on the same chain legitimately return
|
|
1237
|
+
* different lists. Never hardcode the answer for a named provider.
|
|
1238
|
+
*
|
|
1239
|
+
* An empty `list` means "serves no currency", not "serves every currency".
|
|
1240
|
+
* `manual` is the only provider with an empty list and `verifiablePaymentMethods`
|
|
1241
|
+
* already drops it, so the rule is unobservable today — but it is the rule to
|
|
1242
|
+
* keep, because seeding a buyer onto a rail that declares no currency support
|
|
1243
|
+
* is the bug this function exists to prevent.
|
|
1244
|
+
*
|
|
1245
|
+
* @example
|
|
1246
|
+
* ```ts
|
|
1247
|
+
* currencyCapableProviders(pulsechain.id, SUPPORTED_CURRENCIES.GBP)
|
|
1248
|
+
* // ['revolut', 'wise', 'monzo'] — no Venmo, Zelle or Cash App
|
|
1249
|
+
* ```
|
|
1250
|
+
*/
|
|
1251
|
+
export function currencyCapableProviders(chainId, currency) {
|
|
1252
|
+
const configs = providerConfigs(chainId);
|
|
1253
|
+
return verifiablePaymentMethods(chainId).filter((key) => configs[key]?.list.includes(currency.contractId) ?? false);
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Whether a provider can actually serve a buyer paying in `currency` on this
|
|
1257
|
+
* chain — it must be verifiable here AND list the currency.
|
|
1258
|
+
*
|
|
1259
|
+
* @example
|
|
1260
|
+
* ```ts
|
|
1261
|
+
* providerSupportsCurrency({ providerKey: providers.VENMO, chainId: pulsechain.id, currency: SUPPORTED_CURRENCIES.GBP })
|
|
1262
|
+
* // false — Venmo is a United States dollar rail
|
|
1263
|
+
* ```
|
|
1264
|
+
*/
|
|
1265
|
+
export function providerSupportsCurrency(args) {
|
|
1266
|
+
return currencyCapableProviders(args.chainId, args.currency).includes(args.providerKey);
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Pick a default payment method for a buyer paying in `currency`, preferring
|
|
1270
|
+
* the highest-volume rail that can actually serve them.
|
|
1271
|
+
*
|
|
1272
|
+
* `verifiersByVolume` is the monthly-volume ranking, highest first. The volume
|
|
1273
|
+
* leaders are United States dollar rails on every chain, so ranking without
|
|
1274
|
+
* the currency gate seeds a pounds or euro buyer onto a rail that lists only
|
|
1275
|
+
* dollars and shows them an empty order book. Falls back to the first capable
|
|
1276
|
+
* provider in registry order when no ranked rail qualifies — volume data only
|
|
1277
|
+
* covers rails that have already traded, which a new currency has not.
|
|
1278
|
+
*
|
|
1279
|
+
* @returns The chosen provider, or `null` when no verifiable provider on this
|
|
1280
|
+
* chain serves the currency at all. Null rather than an arbitrary provider,
|
|
1281
|
+
* so the caller can tell "nothing can serve this" from a real choice.
|
|
1282
|
+
*/
|
|
1283
|
+
export function selectVolumeDefaultProvider(args) {
|
|
1284
|
+
const capable = currencyCapableProviders(args.chainId, args.currency);
|
|
1285
|
+
if (capable.length === 0)
|
|
1286
|
+
return null;
|
|
1287
|
+
const capableKeys = new Set(capable);
|
|
1288
|
+
for (const key of args.verifiersByVolume) {
|
|
1289
|
+
if (capableKeys.has(key))
|
|
1290
|
+
return key;
|
|
1291
|
+
}
|
|
1292
|
+
return capable[0] ?? null;
|
|
1293
|
+
}
|
|
1173
1294
|
/** Set of all sub-provider keys for fast lookup */
|
|
1174
1295
|
const subProviderKeySet = new Set(Object.values(subProviders));
|
|
1175
1296
|
/** Set of all main provider keys for fast lookup */
|
|
@@ -1200,25 +1321,45 @@ export function getParentProviderKey(key) {
|
|
|
1200
1321
|
return null;
|
|
1201
1322
|
}
|
|
1202
1323
|
/**
|
|
1203
|
-
*
|
|
1324
|
+
* Collapse a sub-provider key to the rail it belongs to.
|
|
1204
1325
|
*
|
|
1205
|
-
*
|
|
1206
|
-
*
|
|
1207
|
-
*
|
|
1208
|
-
* `(provider, payeeDetailsHash)` MUST collapse the sub-provider back to
|
|
1209
|
-
* its parent or it will miss the row.
|
|
1326
|
+
* A sub-provider names a bank the buyer captures from — it is a routing detail
|
|
1327
|
+
* of the capture step, not an identity of its own. Everything keyed by rail
|
|
1328
|
+
* rather than by bank MUST collapse through here:
|
|
1210
1329
|
*
|
|
1211
|
-
*
|
|
1212
|
-
*
|
|
1213
|
-
*
|
|
1214
|
-
*
|
|
1215
|
-
*
|
|
1330
|
+
* - **The on-chain payment method.** A Zelle attestation is issued for the
|
|
1331
|
+
* rail: every bank template in `@zkp2p/providers` declares
|
|
1332
|
+
* `metadata.platform: "zelle"`, and the enclave verify route is
|
|
1333
|
+
* `zelle/transfer_zelle_{bank}` — the bank rides in the ACTION type, never
|
|
1334
|
+
* the platform. So `keccak256("zelle")` is the only payment method a Zelle
|
|
1335
|
+
* payment can ever satisfy. Deposits that attached `zelle-chase` and friends
|
|
1336
|
+
* accept intents no attestation can fulfil.
|
|
1337
|
+
* - **Off-chain maker records.** Makers register once at rail level, so a
|
|
1338
|
+
* lookup keyed by `(provider, payeeDetailsHash)` misses the row unless the
|
|
1339
|
+
* bank is collapsed away. Skipping this in verify-intent once produced a 400
|
|
1340
|
+
* "maker contact could not be verified" for every buy on such a deposit.
|
|
1341
|
+
*
|
|
1342
|
+
* Non-sub-provider keys pass through unchanged, so this is safe to apply
|
|
1343
|
+
* unconditionally.
|
|
1344
|
+
*
|
|
1345
|
+
* @param key - Provider or sub-provider key, e.g. `'zelle-chase'`.
|
|
1346
|
+
* @returns The rail key, e.g. `'zelle'`.
|
|
1216
1347
|
*/
|
|
1217
|
-
export function
|
|
1348
|
+
export function resolveRailProviderKey(key) {
|
|
1218
1349
|
if (!isSubProviderKey(key))
|
|
1219
1350
|
return key;
|
|
1220
1351
|
return getParentProviderKey(key) ?? key;
|
|
1221
1352
|
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Canonicalize a provider key for off-chain maker-record lookups.
|
|
1355
|
+
*
|
|
1356
|
+
* @deprecated Prefer {@link resolveRailProviderKey}, which names the concept
|
|
1357
|
+
* both call-site families share. Retained because the off-chain lookup sites
|
|
1358
|
+
* read better with an API-flavoured name.
|
|
1359
|
+
*/
|
|
1360
|
+
export function resolveApiProviderKey(key) {
|
|
1361
|
+
return resolveRailProviderKey(key);
|
|
1362
|
+
}
|
|
1222
1363
|
/**
|
|
1223
1364
|
* Get the extension platform key for a provider or sub-provider.
|
|
1224
1365
|
* For sub-providers like 'zelle-chase', returns the extensionPlatform from config (e.g., 'chase').
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rail invariants for payment method configuration.
|
|
3
|
+
*
|
|
4
|
+
* `payment.test.ts` covers the behaviour of the individual helpers with named
|
|
5
|
+
* cases. This file states the rule those helpers exist to enforce, as a sweep
|
|
6
|
+
* over every provider on every chain, so a newly added provider is covered
|
|
7
|
+
* without anyone remembering to add a case.
|
|
8
|
+
*
|
|
9
|
+
* **The rule.** A payment method on-chain is `keccak256(platform)`, where
|
|
10
|
+
* `platform` is what the zkTLS attestation names — the `metadata.platform` of
|
|
11
|
+
* the `@zkp2p/providers` template. `UnifiedPaymentVerifier` requires
|
|
12
|
+
* `snapshot.paymentMethod == intent.paymentMethod`, so a deposit that attaches
|
|
13
|
+
* anything else accepts intents no attestation can ever satisfy.
|
|
14
|
+
*
|
|
15
|
+
* Zelle is the case that taught this: every bank template (Chase, Bank of
|
|
16
|
+
* America, Citi, and now Wells Fargo, U.S. Bank, and TD) declares
|
|
17
|
+
* `platform: "zelle"`. The bank rides in the action type, never the platform.
|
|
18
|
+
* So bank-level keys are a capture-time detail — which website the extension
|
|
19
|
+
* opens — and must collapse to the rail everywhere the on-chain method is
|
|
20
|
+
* computed, while staying distinct everywhere the buyer picks a bank.
|
|
21
|
+
*
|
|
22
|
+
* The tests below assert both directions, because collapsing too much is its
|
|
23
|
+
* own bug: it would leave the buyer unable to say which bank they paid from.
|
|
24
|
+
*/
|
|
25
|
+
export {};
|