@provex/utils 1.9.2-rc.20260722211737.69a2b06 → 1.9.2-rc.20260723142825.d415ded

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/payment.d.ts CHANGED
@@ -542,19 +542,37 @@ export declare function isSubProviderKey(key: string): key is SubProviderKey;
542
542
  */
543
543
  export declare function getParentProviderKey(key: string): ProviderKey | null;
544
544
  /**
545
- * Canonicalize a provider key for off-chain maker-record lookups.
545
+ * Collapse a sub-provider key to the rail it belongs to.
546
+ *
547
+ * A sub-provider names a bank the buyer captures from — it is a routing detail
548
+ * of the capture step, not an identity of its own. Everything keyed by rail
549
+ * rather than by bank MUST collapse through here:
550
+ *
551
+ * - **The on-chain payment method.** A Zelle attestation is issued for the
552
+ * rail: every bank template in `@zkp2p/providers` declares
553
+ * `metadata.platform: "zelle"`, and the enclave verify route is
554
+ * `zelle/transfer_zelle_{bank}` — the bank rides in the ACTION type, never
555
+ * the platform. So `keccak256("zelle")` is the only payment method a Zelle
556
+ * payment can ever satisfy. Deposits that attached `zelle-chase` and friends
557
+ * accept intents no attestation can fulfil.
558
+ * - **Off-chain maker records.** Makers register once at rail level, so a
559
+ * lookup keyed by `(provider, payeeDetailsHash)` misses the row unless the
560
+ * bank is collapsed away. Skipping this in verify-intent once produced a 400
561
+ * "maker contact could not be verified" for every buy on such a deposit.
562
+ *
563
+ * Non-sub-provider keys pass through unchanged, so this is safe to apply
564
+ * unconditionally.
546
565
  *
547
- * Makers register once at the parent provider level (e.g. `zelle`) and the
548
- * deposit's on-chain `paymentMethodId` is fanned out to sub-providers
549
- * (`zelle-chase`, `zelle-bofa`, …). Every off-chain lookup keyed by
550
- * `(provider, payeeDetailsHash)` MUST collapse the sub-provider back to
551
- * its parent or it will miss the row.
566
+ * @param key - Provider or sub-provider key, e.g. `'zelle-chase'`.
567
+ * @returns The rail key, e.g. `'zelle'`.
568
+ */
569
+ export declare function resolveRailProviderKey(key: string): string;
570
+ /**
571
+ * Canonicalize a provider key for off-chain maker-record lookups.
552
572
  *
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.
573
+ * @deprecated Prefer {@link resolveRailProviderKey}, which names the concept
574
+ * both call-site families share. Retained because the off-chain lookup sites
575
+ * read better with an API-flavoured name.
558
576
  */
559
577
  export declare function resolveApiProviderKey(key: string): string;
560
578
  /**
package/dist/payment.js CHANGED
@@ -1200,25 +1200,45 @@ export function getParentProviderKey(key) {
1200
1200
  return null;
1201
1201
  }
1202
1202
  /**
1203
- * Canonicalize a provider key for off-chain maker-record lookups.
1203
+ * Collapse a sub-provider key to the rail it belongs to.
1204
+ *
1205
+ * A sub-provider names a bank the buyer captures from — it is a routing detail
1206
+ * of the capture step, not an identity of its own. Everything keyed by rail
1207
+ * rather than by bank MUST collapse through here:
1208
+ *
1209
+ * - **The on-chain payment method.** A Zelle attestation is issued for the
1210
+ * rail: every bank template in `@zkp2p/providers` declares
1211
+ * `metadata.platform: "zelle"`, and the enclave verify route is
1212
+ * `zelle/transfer_zelle_{bank}` — the bank rides in the ACTION type, never
1213
+ * the platform. So `keccak256("zelle")` is the only payment method a Zelle
1214
+ * payment can ever satisfy. Deposits that attached `zelle-chase` and friends
1215
+ * accept intents no attestation can fulfil.
1216
+ * - **Off-chain maker records.** Makers register once at rail level, so a
1217
+ * lookup keyed by `(provider, payeeDetailsHash)` misses the row unless the
1218
+ * bank is collapsed away. Skipping this in verify-intent once produced a 400
1219
+ * "maker contact could not be verified" for every buy on such a deposit.
1204
1220
  *
1205
- * Makers register once at the parent provider level (e.g. `zelle`) and the
1206
- * deposit's on-chain `paymentMethodId` is fanned out to sub-providers
1207
- * (`zelle-chase`, `zelle-bofa`, …). Every off-chain lookup keyed by
1208
- * `(provider, payeeDetailsHash)` MUST collapse the sub-provider back to
1209
- * its parent or it will miss the row.
1221
+ * Non-sub-provider keys pass through unchanged, so this is safe to apply
1222
+ * unconditionally.
1210
1223
  *
1211
- * Use this anywhere you'd otherwise inline the
1212
- * `isSubProviderKey(k) ? getParentProviderKey(k) : k` ternary so the
1213
- * normalization can't drift between call sites — verify-intent regressed
1214
- * by skipping it and produced 400 "maker contact could not be verified"
1215
- * for every buy on a sub-provider deposit.
1224
+ * @param key - Provider or sub-provider key, e.g. `'zelle-chase'`.
1225
+ * @returns The rail key, e.g. `'zelle'`.
1216
1226
  */
1217
- export function resolveApiProviderKey(key) {
1227
+ export function resolveRailProviderKey(key) {
1218
1228
  if (!isSubProviderKey(key))
1219
1229
  return key;
1220
1230
  return getParentProviderKey(key) ?? key;
1221
1231
  }
1232
+ /**
1233
+ * Canonicalize a provider key for off-chain maker-record lookups.
1234
+ *
1235
+ * @deprecated Prefer {@link resolveRailProviderKey}, which names the concept
1236
+ * both call-site families share. Retained because the off-chain lookup sites
1237
+ * read better with an API-flavoured name.
1238
+ */
1239
+ export function resolveApiProviderKey(key) {
1240
+ return resolveRailProviderKey(key);
1241
+ }
1222
1242
  /**
1223
1243
  * Get the extension platform key for a provider or sub-provider.
1224
1244
  * For sub-providers like 'zelle-chase', returns the extensionPlatform from config (e.g., 'chase').
@@ -2,7 +2,7 @@ import { describe, it, test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { base } from 'viem/chains';
4
4
  import { getAddress } from 'viem';
5
- import { providerConfigs, getCurrenciesForProvider, providers, verifierGetsKey, providerKeyToContractId, resolveApiProviderKey, getProviderPaymentMethodIds, computeProviderAvailability, availableProviderConfigs, subProviders, isDayResolutionRail } from './payment.js';
5
+ import { providerConfigs, getCurrenciesForProvider, providers, verifierGetsKey, providerKeyToContractId, resolveApiProviderKey, resolveRailProviderKey, getProviderPaymentMethodIds, computeProviderAvailability, availableProviderConfigs, subProviders, isDayResolutionRail } from './payment.js';
6
6
  import { paymentVerifierToKey, providerByContractId, getProviderByContractId, getProviderNameByContractId, providerKeyToVerifiers, providerKeyToVerifiersMap, getPaymentMethodConfig, getPaymentMethodName, getPaymentMethodMinimum, getPaymentProviderMinimum, calculateEffectiveMinimum, isPaymentMethodKnown, isPaymentMethodIdKnown, isExternalPaymentMethod, getExternalPaymentMethod, externalPaymentMethods, getExtensionPlatform, getParentProviderKey, isSubProviderKey, providerRequiresMetadataIndex, getProviderIconStack, getPaymentIcon, getPlatformRisk, fromPeerTier, tierToPeerTier, tiers, getAllPaymentMethods, verifiablePaymentMethods, } from './payment.js';
7
7
  import { getDefaultToken } from './tokens.js';
8
8
  import { SUPPORTED_CURRENCIES } from './currencies.js';
@@ -206,6 +206,38 @@ describe('resolveApiProviderKey', () => {
206
206
  assert.equal(resolveApiProviderKey('unknown-sub'), 'unknown-sub');
207
207
  });
208
208
  });
209
+ /**
210
+ * Tests for resolveRailProviderKey — the same collapse, applied to the
211
+ * ON-CHAIN payment method. A Zelle attestation is issued for the rail: every
212
+ * bank template declares `metadata.platform: "zelle"` and the enclave verify
213
+ * route is `zelle/transfer_zelle_{bank}`, so the bank never reaches the
214
+ * payment method. Deposits that attached bank-level methods accepted intents
215
+ * that no attestation could fulfil — they sat until they expired.
216
+ */
217
+ describe('resolveRailProviderKey', () => {
218
+ it('collapses every Zelle bank to the rail', () => {
219
+ // The full set, not a sample: one bank left un-collapsed is one bank whose
220
+ // deposits silently strand every intent signalled against them.
221
+ for (const bank of Object.values(subProviders)) {
222
+ assert.equal(resolveRailProviderKey(bank), providers.ZELLE);
223
+ }
224
+ });
225
+ it('leaves rails that have no banks untouched', () => {
226
+ assert.equal(resolveRailProviderKey(providers.VENMO), 'venmo');
227
+ assert.equal(resolveRailProviderKey(providers.CASHAPP), 'cashapp');
228
+ assert.equal(resolveRailProviderKey(providers.ZELLE), 'zelle');
229
+ });
230
+ it('is idempotent, so applying it twice cannot drift', () => {
231
+ // Call sites apply it at the write boundary; a second application (say a
232
+ // caller that already normalized) must be a no-op.
233
+ assert.equal(resolveRailProviderKey(resolveRailProviderKey('zelle-chase')), resolveRailProviderKey('zelle-chase'));
234
+ });
235
+ it('maps the rail key to the payment method id registered on-chain', () => {
236
+ // Pins the migration target: keccak256("zelle"). If this changes, deposits
237
+ // would be attaching a method the registry does not route.
238
+ assert.equal(providerKeyToContractId(resolveRailProviderKey('zelle-bofa')), '0xf752c7d19698ecb0bb8988abf9b9a53a4c3657f3bc8850a6fb59fdf3e3ce8cd3');
239
+ });
240
+ });
209
241
  /**
210
242
  * Tests for providerConfigs - payment provider configuration
211
243
  */
@@ -16,15 +16,36 @@
16
16
  */
17
17
  import { type Transport } from 'viem';
18
18
  import { type ChainId } from './chain.js';
19
+ /**
20
+ * valve.city PulseChain endpoint — one URL fronting a load-balanced node pool.
21
+ *
22
+ * `vk_demo` is the PUBLIC, RATE-LIMITED demo key. It is the right default for
23
+ * local development and for anyone building against this package without
24
+ * credentials, but a keyed endpoint (`https://one.valve.city/rpc/<key>/evm/369`)
25
+ * belongs in front of production traffic — a shared demo key is a shared rate
26
+ * limit. Override per-deployment via the `VITE_RPC_URLS_369` env (caller
27
+ * overrides are tried ahead of this list) rather than editing this constant.
28
+ *
29
+ * Kept in sync with `DEFAULT_MSGBOARD_ENDPOINT` in @provex/onramp, which points
30
+ * at the same host: one keyed valve.city endpoint serves both `msgboard_*` and
31
+ * `eth_*` methods.
32
+ */
33
+ export declare const VALVE_CITY_PULSECHAIN_RPC = "https://one.valve.city/rpc/vk_demo/evm/369";
19
34
  /**
20
35
  * Provex-curated fallback RPC URLs per chain, applied after caller overrides
21
36
  * but before the viem chain default.
22
37
  *
23
38
  * @remarks
24
- * - Pulsechain (369): g4mm4 operates a public PulseChain RPC that is more
25
- * reliable from browsers than the official `rpc.pulsechain.com`.
39
+ * - Pulsechain (369): valve.city is listed first — it load balances across a
40
+ * pool of PulseChain nodes behind one endpoint, so it absorbs a single node
41
+ * going slow or stale without the client noticing. g4mm4 is retained behind
42
+ * it as an independent operator: valve.city's internal balancing cannot help
43
+ * if the endpoint ITSELF is unreachable, so a second operator is what makes
44
+ * this list a real fallback rather than one host with extra steps.
45
+ * Order is a hint, not a guarantee — {@link createChainTransport} ranks by
46
+ * measured latency, so the fastest healthy endpoint serves traffic either way.
26
47
  * - Base (8453): no built-in fallback — Base's default endpoint is reliable
27
- * and g4mm4 does not operate a Base RPC.
48
+ * and neither operator runs a Base RPC.
28
49
  */
29
50
  export declare const FALLBACK_RPC_URLS: Partial<Record<ChainId, readonly string[]>>;
30
51
  /**
@@ -16,6 +16,21 @@
16
16
  */
17
17
  import { fallback, http } from 'viem';
18
18
  import { chainIdToChain } from './chain.js';
19
+ /**
20
+ * valve.city PulseChain endpoint — one URL fronting a load-balanced node pool.
21
+ *
22
+ * `vk_demo` is the PUBLIC, RATE-LIMITED demo key. It is the right default for
23
+ * local development and for anyone building against this package without
24
+ * credentials, but a keyed endpoint (`https://one.valve.city/rpc/<key>/evm/369`)
25
+ * belongs in front of production traffic — a shared demo key is a shared rate
26
+ * limit. Override per-deployment via the `VITE_RPC_URLS_369` env (caller
27
+ * overrides are tried ahead of this list) rather than editing this constant.
28
+ *
29
+ * Kept in sync with `DEFAULT_MSGBOARD_ENDPOINT` in @provex/onramp, which points
30
+ * at the same host: one keyed valve.city endpoint serves both `msgboard_*` and
31
+ * `eth_*` methods.
32
+ */
33
+ export const VALVE_CITY_PULSECHAIN_RPC = 'https://one.valve.city/rpc/vk_demo/evm/369';
19
34
  // Per-method call counter. Enabled at runtime by setting window.__RPC_DEBUG = true
20
35
  // in the browser console (works in pre-built dist — no Vite env injection needed).
21
36
  const _rpcCounts = {};
@@ -36,13 +51,19 @@ const _rpcLog = (method) => {
36
51
  * but before the viem chain default.
37
52
  *
38
53
  * @remarks
39
- * - Pulsechain (369): g4mm4 operates a public PulseChain RPC that is more
40
- * reliable from browsers than the official `rpc.pulsechain.com`.
54
+ * - Pulsechain (369): valve.city is listed first — it load balances across a
55
+ * pool of PulseChain nodes behind one endpoint, so it absorbs a single node
56
+ * going slow or stale without the client noticing. g4mm4 is retained behind
57
+ * it as an independent operator: valve.city's internal balancing cannot help
58
+ * if the endpoint ITSELF is unreachable, so a second operator is what makes
59
+ * this list a real fallback rather than one host with extra steps.
60
+ * Order is a hint, not a guarantee — {@link createChainTransport} ranks by
61
+ * measured latency, so the fastest healthy endpoint serves traffic either way.
41
62
  * - Base (8453): no built-in fallback — Base's default endpoint is reliable
42
- * and g4mm4 does not operate a Base RPC.
63
+ * and neither operator runs a Base RPC.
43
64
  */
44
65
  export const FALLBACK_RPC_URLS = {
45
- 369: ['https://rpc-pulsechain.g4mm4.io'],
66
+ 369: [VALVE_CITY_PULSECHAIN_RPC, 'https://rpc-pulsechain.g4mm4.io'],
46
67
  8453: [],
47
68
  };
48
69
  /**
@@ -1,7 +1,7 @@
1
1
  import { describe, it } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { base, pulsechain } from 'viem/chains';
4
- import { createChainTransport, parseRpcUrlList, resolveChainRpcUrls, FALLBACK_RPC_URLS, } from './transports.js';
4
+ import { createChainTransport, parseRpcUrlList, resolveChainRpcUrls, FALLBACK_RPC_URLS, VALVE_CITY_PULSECHAIN_RPC, } from './transports.js';
5
5
  /**
6
6
  * Read the ordered list of RPC URLs a built transport will actually dial.
7
7
  *
@@ -19,10 +19,26 @@ const dialledUrls = (transport) => {
19
19
  };
20
20
  describe('transports', () => {
21
21
  describe('FALLBACK_RPC_URLS', () => {
22
- it('lists g4mm4 for PulseChain (369)', () => {
23
- assert.deepEqual(FALLBACK_RPC_URLS[369], ['https://rpc-pulsechain.g4mm4.io']);
22
+ it('lists valve.city ahead of g4mm4 for PulseChain (369)', () => {
23
+ // valve.city load balances across a node pool, so it leads. g4mm4 stays
24
+ // behind it as an INDEPENDENT operator: valve.city's internal balancing
25
+ // cannot save us if that endpoint itself is down, which is the only
26
+ // failure a fallback list exists to survive.
27
+ assert.deepEqual(FALLBACK_RPC_URLS[369], [
28
+ VALVE_CITY_PULSECHAIN_RPC,
29
+ 'https://rpc-pulsechain.g4mm4.io',
30
+ ]);
31
+ });
32
+ it('keeps two independent operators for PulseChain', () => {
33
+ // Guard against someone "simplifying" the list down to a single host.
34
+ // One entry means fallback() short-circuits to a bare transport and a
35
+ // single operator outage takes the app down.
36
+ const urls = FALLBACK_RPC_URLS[369] ?? [];
37
+ assert.ok(urls.length >= 2, 'PulseChain must keep a second, independent RPC operator');
38
+ const hosts = new Set(urls.map((u) => new URL(u).host));
39
+ assert.equal(hosts.size, urls.length, 'fallback URLs must not share a host');
24
40
  });
25
- it('has an empty list for Base (8453) — g4mm4 does not run a Base RPC', () => {
41
+ it('has an empty list for Base (8453) — neither operator runs a Base RPC', () => {
26
42
  assert.deepEqual(FALLBACK_RPC_URLS[8453], []);
27
43
  });
28
44
  });
@@ -47,9 +63,10 @@ describe('transports', () => {
47
63
  });
48
64
  });
49
65
  describe('resolveChainRpcUrls', () => {
50
- it('returns g4mm4 then chain default for PulseChain', () => {
66
+ it('returns valve.city, then g4mm4, then chain default for PulseChain', () => {
51
67
  const urls = resolveChainRpcUrls(369);
52
68
  assert.deepEqual(urls, [
69
+ VALVE_CITY_PULSECHAIN_RPC,
53
70
  'https://rpc-pulsechain.g4mm4.io',
54
71
  pulsechain.rpcUrls.default.http[0],
55
72
  ]);
@@ -61,9 +78,12 @@ describe('transports', () => {
61
78
  it('places caller-supplied userUrls ahead of curated fallbacks and the default', () => {
62
79
  const userUrls = ['https://my-private-rpc.example', 'https://another.example'];
63
80
  const urls = resolveChainRpcUrls(369, { userUrls });
81
+ // A deployment-supplied keyed endpoint MUST outrank the shared vk_demo
82
+ // default — this is how production escapes the demo key's rate limit.
64
83
  assert.deepEqual(urls, [
65
84
  'https://my-private-rpc.example',
66
85
  'https://another.example',
86
+ VALVE_CITY_PULSECHAIN_RPC,
67
87
  'https://rpc-pulsechain.g4mm4.io',
68
88
  pulsechain.rpcUrls.default.http[0],
69
89
  ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@provex/utils",
3
- "version": "1.9.2-rc.20260722211737.69a2b06",
3
+ "version": "1.9.2-rc.20260723142825.d415ded",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",