@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
@@ -0,0 +1,40 @@
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
+ import { describe, it } from 'node:test';
13
+ import assert from 'node:assert/strict';
14
+ import { pulsechain } from 'viem/chains';
15
+ import { findLegacySubProviderAttachments, isProviderDisabled, providerKeyToContractId, subProviders, } from './payment.js';
16
+ process.env.DISABLED_PROVIDERS = 'PayPal, zelle';
17
+ process.env.ENABLED_PROVIDERS = 'venmo,paypal';
18
+ describe('isProviderDisabled', () => {
19
+ it('hides a rail named in the disabled list, whatever the registry says', () => {
20
+ assert.equal(isProviderDisabled('zelle', false), true);
21
+ assert.equal(isProviderDisabled('ZELLE'), true);
22
+ });
23
+ it('lets the disabled list win when a rail is in both lists', () => {
24
+ assert.equal(isProviderDisabled('paypal'), true);
25
+ });
26
+ it('shows a rail named in the enabled list even when the registry disables it', () => {
27
+ assert.equal(isProviderDisabled('venmo', true), false);
28
+ });
29
+ it('follows the registry field for a rail in neither list', () => {
30
+ assert.equal(isProviderDisabled('wise'), false);
31
+ assert.equal(isProviderDisabled('wise', true), true);
32
+ });
33
+ });
34
+ describe('findLegacySubProviderAttachments with a disabled rail', () => {
35
+ it('does not warn about a rail the deployment has switched off', () => {
36
+ // A maker cannot act on a warning about a rail they cannot attach here.
37
+ const bankIds = new Set(Object.values(subProviders).map((key) => providerKeyToContractId(key).toLowerCase()));
38
+ assert.deepEqual(findLegacySubProviderAttachments({ chainId: pulsechain.id, attachedPaymentMethodIds: bankIds }), []);
39
+ });
40
+ });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @fileoverview Tests for where each payment rail sends the buyer.
3
+ *
4
+ * A send link is the buyer's only route from "I must pay" to the rail's own
5
+ * payment screen. A wrong host sends money intent to the wrong place, so each
6
+ * rail is pinned to its exact destination, in both the app-link (QR) and the
7
+ * web form.
8
+ */
9
+ export {};
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @fileoverview Tests for where each payment rail sends the buyer.
3
+ *
4
+ * A send link is the buyer's only route from "I must pay" to the rail's own
5
+ * payment screen. A wrong host sends money intent to the wrong place, so each
6
+ * rail is pinned to its exact destination, in both the app-link (QR) and the
7
+ * web form.
8
+ */
9
+ import { describe, it } from 'node:test';
10
+ import assert from 'node:assert/strict';
11
+ import { pulsechain } from 'viem/chains';
12
+ import { computePayeeDetailsHash, dataEncoding, defaultZelleSendLink, providerConfigs, providers, subProviderConfigs, subProviders, } from './payment.js';
13
+ import { keccak256, stringToHex } from 'viem';
14
+ const webOptions = { username: 'alice', amount: '12.50' };
15
+ const qrOptions = { ...webOptions, qrCode: true };
16
+ const sendLinkOf = (key) => providerConfigs(pulsechain.id)[key].sendLink;
17
+ describe('computePayeeDetailsHash', () => {
18
+ it('is the keccak256 of the identifier text, so maker and taker derive the same hash', () => {
19
+ assert.equal(computePayeeDetailsHash('@myvenmo'), keccak256(stringToHex('@myvenmo')));
20
+ });
21
+ });
22
+ describe('Zelle send links', () => {
23
+ it('encodes the username into the enroll URL as a base64 payment payload', () => {
24
+ const decoded = JSON.parse(atob(dataEncoding('alice@example.com')));
25
+ assert.deepEqual(decoded, { name: '', action: 'payment', token: 'alice@example.com' });
26
+ });
27
+ it('returns no default link without a username, because there is no one to pay', () => {
28
+ assert.equal(defaultZelleSendLink({ username: '', amount: '1.00' }), null);
29
+ });
30
+ it('returns the enroll link for a username', () => {
31
+ assert.equal(defaultZelleSendLink(webOptions), `https://enroll.zellepay.com/qr-codes?data=${dataEncoding('alice')}`);
32
+ });
33
+ it('falls back to the generic enroll link when no bank is chosen', () => {
34
+ assert.equal(sendLinkOf(providers.ZELLE)(webOptions), defaultZelleSendLink(webOptions));
35
+ });
36
+ it('delegates to the chosen bank when a sub-provider is named', () => {
37
+ const link = sendLinkOf(providers.ZELLE)({ ...webOptions, subProvider: subProviders.CITI });
38
+ assert.equal(link, 'https://online.citi.com/US/nga/zelle/transfer');
39
+ });
40
+ const bankCases = [
41
+ [subProviders.CHASE, defaultZelleSendLink(qrOptions), 'https://secure.chase.com/web/auth/dashboard#/dashboard/singleDoor/singleDoorController/index'],
42
+ [subProviders.BANK_OF_AMERICA, 'https://www.bankofamerica.com/movemoney', 'https://www.bankofamerica.com/'],
43
+ [subProviders.CITI, defaultZelleSendLink(qrOptions), 'https://online.citi.com/US/nga/zelle/transfer'],
44
+ ];
45
+ for (const [bank, qrLink, webLink] of bankCases) {
46
+ it(`${bank}: sends a QR scan to the bank's QR-capable link and a web buyer to its site`, () => {
47
+ const sendLink = subProviderConfigs.zelle[bank].sendLink;
48
+ assert.equal(sendLink(qrOptions), qrLink);
49
+ assert.equal(sendLink(webOptions), webLink);
50
+ });
51
+ }
52
+ });
53
+ describe('rail send links', () => {
54
+ const cases = [
55
+ [
56
+ providers.VENMO,
57
+ 'venmo://paycharge?txn=pay&recipients=alice&note=cash&amount=12.50',
58
+ 'https://account.venmo.com/pay?recipients=alice&note=cash&amount=12.50',
59
+ ],
60
+ [providers.REVOLUT, 'https://revolut.me/alice', 'https://revolut.me/alice'],
61
+ [providers.WISE, 'https://wise.com/pay/me/alice', 'https://wise.com/pay/me/alice'],
62
+ [
63
+ providers.MERCADOPAGO,
64
+ 'https://www.mercadopago.com.ar/money-out/transfer/',
65
+ 'https://www.mercadopago.com.ar/money-out/transfer/',
66
+ ],
67
+ [providers.PAYPAL, 'paypal://', 'https://www.paypal.com/myaccount/transfer/homepage'],
68
+ [providers.MONZO, 'https://monzo.me/alice', 'https://monzo.me/alice'],
69
+ [providers.N26, 'number26://', 'https://app.n26.com/'],
70
+ [providers.ALIPAY, 'alipays://', 'https://www.alipay.com/'],
71
+ [providers.CHIME, 'https://app.chime.com/link/qr?u=alice', 'https://app.chime.com/login'],
72
+ [
73
+ providers.ROYALBANKCANADA,
74
+ 'https://www.rbcroyalbank.com/ways-to-bank/interac-e-transfer.html',
75
+ 'https://www.rbcroyalbank.com/ways-to-bank/interac-e-transfer.html',
76
+ ],
77
+ [providers.IDFC, 'https://www.idfcfirstbank.com/', 'https://www.idfcfirstbank.com/'],
78
+ ];
79
+ for (const [key, qrLink, webLink] of cases) {
80
+ it(`${key}: pins the QR and web destinations`, () => {
81
+ const sendLink = sendLinkOf(key);
82
+ assert.equal(sendLink(qrOptions), qrLink);
83
+ assert.equal(sendLink(webOptions), webLink);
84
+ });
85
+ }
86
+ it('Cash App prefixes a bare username with $, and leaves a cashtag alone', () => {
87
+ const sendLink = sendLinkOf(providers.CASHAPP);
88
+ assert.equal(sendLink({ username: 'alice', amount: '1.00' }), 'https://cash.app/$alice');
89
+ assert.equal(sendLink({ username: '$alice', amount: '1.00' }), 'https://cash.app/$alice');
90
+ });
91
+ });
@@ -2,22 +2,19 @@
2
2
  * @fileoverview Peer protocol types for attestation and proof verification.
3
3
  *
4
4
  * This module defines the type system for the V3 attestation flow, including:
5
- * - Notarized proofs from the PeerAuth browser extension
6
- * - Attestation requests/responses from the peer attestation service
5
+ * - Attestation responses from the peer attestation service
7
6
  * - EIP-712 typed data structures for on-chain verification
8
7
  * - Payment proof encoding for the UnifiedPaymentVerifier contract
9
8
  *
10
9
  * @remarks
11
10
  * **V3 Attestation Flow:**
12
- * 1. Buyer generates zkTLS proof via PeerAuth extension (NotarizedProofData)
13
- * 2. Proof + intent data sent to attestation service (V3AttestationRequest)
14
- * 3. Service validates and returns EIP-712 signed attestation (V3AttestationResponse)
15
- * 4. Attestation encoded as paymentProof bytes (encodeV3PaymentProof)
16
- * 5. Buyer calls fulfillIntent() with encoded proof
11
+ * 1. Buyer captures the payment through the PeerAuth extension (buyer trusted-execution capture)
12
+ * 2. Service validates and returns EIP-712 signed attestation (V3AttestationResponse)
13
+ * 3. Attestation encoded as paymentProof bytes (encodeV3PaymentProof)
14
+ * 4. Buyer calls fulfillIntent() with encoded proof
17
15
  *
18
16
  * **Type Aliases:**
19
17
  * For cleaner imports, V3-prefixed types have unprefixed aliases:
20
- * - AttestationOptions = V3AttestationOptions
21
18
  * - AttestationResponse = V3AttestationResponse
22
19
  * - IntentData = V3IntentData
23
20
  *
@@ -110,27 +107,6 @@ export type IntentValidation = {
110
107
  };
111
108
  depositData: ValidationDepositData;
112
109
  };
113
- /**
114
- * Notarized proof from PeerAuth extension.
115
- * This is the zkTLS proof that proves a payment transaction occurred.
116
- * Mirrors the NotarizedProof type from @provex/extension-react.
117
- */
118
- export type NotarizedProofData = {
119
- claim: {
120
- context: string;
121
- epoch: number;
122
- identifier: Hex;
123
- owner: Hex;
124
- parameters: string;
125
- provider: string;
126
- timestampS: number;
127
- };
128
- signatures: {
129
- attestorAddress: Hex;
130
- claimSignature: Record<number, number>;
131
- resultSignature: Record<number, number>;
132
- };
133
- };
134
110
  /**
135
111
  * Intent data required by the peer attestation service.
136
112
  * This contains all the on-chain intent parameters needed to verify
@@ -154,43 +130,6 @@ export type V3IntentData = {
154
130
  /** Optional timestamp buffer in milliseconds (default: 3000) */
155
131
  timestampBufferMs?: string;
156
132
  };
157
- /**
158
- * V3 attestation request sent to the peer attestation service.
159
- * The service validates Reclaim proofs and returns an EIP-712 signed attestation.
160
- *
161
- * Endpoint: POST /verify/{platform}/{action_type}
162
- * Example: POST /verify/chase/transfer_zelle
163
- */
164
- export type V3AttestationRequest = {
165
- /** Type of proof being submitted */
166
- proofType: 'reclaim';
167
- /** JSON stringified array of Reclaim proofs */
168
- proof: string;
169
- /** Chain ID (e.g., 8453 for Base) */
170
- chainId: number;
171
- /** UnifiedPaymentVerifier contract address */
172
- verifyingContract: Hex;
173
- /** Intent data from on-chain signalIntent */
174
- intent: V3IntentData;
175
- };
176
- /**
177
- * Options passed to the v3 getAttestation API.
178
- * These are transformed into V3AttestationRequest format.
179
- */
180
- export type V3AttestationOptions = {
181
- /** Platform for the attestation service path (e.g., 'chase') */
182
- platform: string;
183
- /** Action type for the attestation service path (e.g., 'transfer_zelle') */
184
- actionType: string;
185
- /** Chain ID */
186
- chainId: number;
187
- /** UnifiedPaymentVerifier contract address */
188
- verifyingContract: Hex;
189
- /** Array of Reclaim proofs from PeerAuth */
190
- proofs: NotarizedProofData[];
191
- /** Intent data from on-chain intent */
192
- intent: V3IntentData;
193
- };
194
133
  /**
195
134
  * EIP-712 typed data specification from attestation service.
196
135
  */
@@ -303,14 +242,6 @@ export type SignedAttestation = {
303
242
  * @returns ABI-encoded bytes to pass as paymentProof in fulfillIntent
304
243
  */
305
244
  export declare function encodeV3PaymentProof(attestation: V3AttestationResponseObject): Hex;
306
- /**
307
- * Extracts the Reclaim attestor address from the attestation proofInput.
308
- * This is needed for the verificationData field in fulfillIntent.
309
- *
310
- * @param attestation - The V3AttestationResponseObject from the attestation service
311
- * @returns The Reclaim attestor address as Hex, or null if not found
312
- */
313
- export declare function extractReclaimAttestorAddress(attestation: V3AttestationResponseObject): Hex | null;
314
245
  /**
315
246
  * Extracts the nullifier from a V3 attestation response.
316
247
  *
@@ -326,10 +257,6 @@ export declare function extractReclaimAttestorAddress(attestation: V3Attestation
326
257
  * @returns The derived nullifier as Hex, or null if extraction fails
327
258
  */
328
259
  export declare function extractNullifier(attestation: V3AttestationResponseObject): Hex | null;
329
- /** @alias V3AttestationOptions */
330
- export type AttestationOptions = V3AttestationOptions;
331
- /** @alias V3AttestationRequest */
332
- export type AttestationRequest = V3AttestationRequest;
333
260
  /** @alias V3AttestationResponse */
334
261
  export type AttestationResponse = V3AttestationResponse;
335
262
  /** @alias V3AttestationResponseObject */
@@ -2,22 +2,19 @@
2
2
  * @fileoverview Peer protocol types for attestation and proof verification.
3
3
  *
4
4
  * This module defines the type system for the V3 attestation flow, including:
5
- * - Notarized proofs from the PeerAuth browser extension
6
- * - Attestation requests/responses from the peer attestation service
5
+ * - Attestation responses from the peer attestation service
7
6
  * - EIP-712 typed data structures for on-chain verification
8
7
  * - Payment proof encoding for the UnifiedPaymentVerifier contract
9
8
  *
10
9
  * @remarks
11
10
  * **V3 Attestation Flow:**
12
- * 1. Buyer generates zkTLS proof via PeerAuth extension (NotarizedProofData)
13
- * 2. Proof + intent data sent to attestation service (V3AttestationRequest)
14
- * 3. Service validates and returns EIP-712 signed attestation (V3AttestationResponse)
15
- * 4. Attestation encoded as paymentProof bytes (encodeV3PaymentProof)
16
- * 5. Buyer calls fulfillIntent() with encoded proof
11
+ * 1. Buyer captures the payment through the PeerAuth extension (buyer trusted-execution capture)
12
+ * 2. Service validates and returns EIP-712 signed attestation (V3AttestationResponse)
13
+ * 3. Attestation encoded as paymentProof bytes (encodeV3PaymentProof)
14
+ * 4. Buyer calls fulfillIntent() with encoded proof
17
15
  *
18
16
  * **Type Aliases:**
19
17
  * For cleaner imports, V3-prefixed types have unprefixed aliases:
20
- * - AttestationOptions = V3AttestationOptions
21
18
  * - AttestationResponse = V3AttestationResponse
22
19
  * - IntentData = V3IntentData
23
20
  *
@@ -91,29 +88,6 @@ export function encodeV3PaymentProof(attestation) {
91
88
  }
92
89
  ]);
93
90
  }
94
- /**
95
- * Extracts the Reclaim attestor address from the attestation proofInput.
96
- * This is needed for the verificationData field in fulfillIntent.
97
- *
98
- * @param attestation - The V3AttestationResponseObject from the attestation service
99
- * @returns The Reclaim attestor address as Hex, or null if not found
100
- */
101
- export function extractReclaimAttestorAddress(attestation) {
102
- try {
103
- const proofInput = JSON.parse(attestation.proofInput);
104
- const proofs = JSON.parse(proofInput.proof);
105
- if (Array.isArray(proofs) && proofs.length > 0) {
106
- const attestorAddress = proofs[0]?.signatures?.attestorAddress;
107
- if (attestorAddress && typeof attestorAddress === 'string') {
108
- return attestorAddress;
109
- }
110
- }
111
- return null;
112
- }
113
- catch {
114
- return null;
115
- }
116
- }
117
91
  /**
118
92
  * Extracts the nullifier from a V3 attestation response.
119
93
  *
@@ -1,7 +1,7 @@
1
1
  import { describe, it } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { decodeAbiParameters, encodeAbiParameters, keccak256, parseAbiParameters } from 'viem';
4
- import { encodeV3PaymentProof, extractNullifier, extractReclaimAttestorAddress, } from './peer-types.js';
4
+ import { encodeV3PaymentProof, extractNullifier, } from './peer-types.js';
5
5
  /**
6
6
  * Where the expected values come from, so these tests can fail:
7
7
  * - REAL_ATTESTATION is a live Chase/Zelle response captured from the zkp2p
@@ -146,33 +146,3 @@ describe('extractNullifier', () => {
146
146
  assert.equal(extractNullifier({ ...REAL_ATTESTATION, encodedPaymentDetails: '0x' }), null);
147
147
  });
148
148
  });
149
- describe('extractReclaimAttestorAddress', () => {
150
- const withProofs = (proofs) => ({
151
- ...REAL_ATTESTATION,
152
- proofInput: JSON.stringify({ proofType: 'reclaim', proof: JSON.stringify(proofs) }),
153
- });
154
- it('reads the attestor address of the live proof', () => {
155
- assert.equal(extractReclaimAttestorAddress(REAL_ATTESTATION), '0x0636c417755e3ae25c6c166d181c0607f4c572a3');
156
- });
157
- it('reads the first proof only', () => {
158
- const attestation = withProofs([
159
- { signatures: { attestorAddress: '0x00000000000000000000000000000000000000f1' } },
160
- { signatures: { attestorAddress: '0x00000000000000000000000000000000000000f2' } },
161
- ]);
162
- assert.equal(extractReclaimAttestorAddress(attestation), '0x00000000000000000000000000000000000000f1');
163
- });
164
- it('returns null when there are no proofs', () => {
165
- assert.equal(extractReclaimAttestorAddress(withProofs([])), null);
166
- assert.equal(extractReclaimAttestorAddress(withProofs({ not: 'an array' })), null);
167
- });
168
- it('returns null when the signatures or the address are missing or not a string', () => {
169
- assert.equal(extractReclaimAttestorAddress(withProofs([{}])), null);
170
- assert.equal(extractReclaimAttestorAddress(withProofs([{ signatures: {} }])), null);
171
- assert.equal(extractReclaimAttestorAddress(withProofs([{ signatures: { attestorAddress: 5 } }])), null);
172
- assert.equal(extractReclaimAttestorAddress(withProofs([{ signatures: { attestorAddress: '' } }])), null);
173
- });
174
- it('returns null for malformed JSON at either level', () => {
175
- assert.equal(extractReclaimAttestorAddress({ ...REAL_ATTESTATION, proofInput: 'not json' }), null);
176
- assert.equal(extractReclaimAttestorAddress({ ...REAL_ATTESTATION, proofInput: JSON.stringify({ proof: 'not json' }) }), null);
177
- });
178
- });
package/dist/profit.d.ts CHANGED
@@ -30,6 +30,14 @@ export interface ProfitCalculationInput {
30
30
  currency: string;
31
31
  };
32
32
  }
33
+ /**
34
+ * Scale a base-unit amount by a difference in decimals.
35
+ *
36
+ * @param amount - Amount in base units.
37
+ * @param decimalDiff - Target decimals minus source decimals.
38
+ * @returns The amount rescaled to the target decimals.
39
+ */
40
+ export declare const scaleByDecimalDifference: (amount: bigint, decimalDiff: number) => bigint;
33
41
  /**
34
42
  * Calculate the profit of an intent fulfilled event
35
43
  * @param param0.amount - The fiat amount in currency base units (e.g., cents for USD)
package/dist/profit.js CHANGED
@@ -11,6 +11,21 @@
11
11
  */
12
12
  import { getCurrencyByContractId } from "./currencies.js";
13
13
  import { getDefaultToken } from "./tokens.js";
14
+ /**
15
+ * Scale a base-unit amount by a difference in decimals.
16
+ *
17
+ * @param amount - Amount in base units.
18
+ * @param decimalDiff - Target decimals minus source decimals.
19
+ * @returns The amount rescaled to the target decimals.
20
+ */
21
+ export const scaleByDecimalDifference = (amount, decimalDiff) => {
22
+ // A negative difference does not occur today (fiat has at most 2 decimals,
23
+ // the settlement token 6), but a future low-decimal token must still scale.
24
+ if (decimalDiff < 0) {
25
+ return amount / 10n ** BigInt(-decimalDiff);
26
+ }
27
+ return amount * 10n ** BigInt(decimalDiff);
28
+ };
14
29
  /**
15
30
  * Calculate the profit of an intent fulfilled event
16
31
  * @param param0.amount - The fiat amount in currency base units (e.g., cents for USD)
@@ -33,9 +48,7 @@ export const calculateProfit = ({ amount: amountInput, conversionRate, chainId,
33
48
  // e.g., $100 USD (10000 in 2 decimals) -> 100000000 in 6 decimals for USDC
34
49
  // e.g., €100 EUR (10000 in 2 decimals) -> 100000000 in 6 decimals for USDC
35
50
  const decimalDiff = inputTokenInfo.decimals - currency.decimals;
36
- const amountScaled = decimalDiff >= 0
37
- ? amount * 10n ** BigInt(decimalDiff)
38
- : amount / 10n ** BigInt(-decimalDiff);
51
+ const amountScaled = scaleByDecimalDifference(amount, decimalDiff);
39
52
  // Profit = what we received - what we expected for 1:1 fiat:token exchange
40
53
  // Note: This is most meaningful for USD where $1 ≈ 1 USDC
41
54
  // For other currencies, this shows the mathematical difference assuming 1:1
@@ -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 { calculateProfit, calculateProfits } from './profit.js';
4
+ import { calculateProfit, calculateProfits, scaleByDecimalDifference } from './profit.js';
5
5
  import { SUPPORTED_CURRENCIES } from './currencies.js';
6
6
  describe('profit', () => {
7
7
  // Constants for testing
@@ -312,4 +312,15 @@ describe('profit', () => {
312
312
  assert.equal(result.profit, 10000000n);
313
313
  });
314
314
  });
315
+ describe('scaleByDecimalDifference', () => {
316
+ it('multiplies when the target has more decimals, so cents become USDC base units', () => {
317
+ assert.equal(scaleByDecimalDifference(10000n, 4), 100000000n);
318
+ });
319
+ it('divides when the target has fewer decimals, so a low-decimal token is not inflated', () => {
320
+ assert.equal(scaleByDecimalDifference(100000000n, -4), 10000n);
321
+ });
322
+ it('leaves the amount unchanged when decimals match', () => {
323
+ assert.equal(scaleByDecimalDifference(12345n, 0), 12345n);
324
+ });
325
+ });
315
326
  });
@@ -241,13 +241,11 @@ export function formatCap(cap) {
241
241
  * (its threshold is `null`).
242
242
  */
243
243
  export function getTierFromVolume(volumeBaseUnits) {
244
- // Iterate from platinum down (exclude president — manual assignment only)
245
- const volumeTiers = TIER_ORDER.filter(t => t !== tiers.PRESIDENT);
246
- for (let i = volumeTiers.length - 1; i >= 0; i--) {
247
- const tier = volumeTiers[i];
244
+ // Iterate from the top tier down. `null` is the manual-assignment marker
245
+ // (PRESIDENT) — skipped, since volume alone can never reach it.
246
+ for (let i = TIER_ORDER.length - 1; i >= 0; i--) {
247
+ const tier = TIER_ORDER[i];
248
248
  const threshold = TAKER_TIERS[tier].volumeThreshold;
249
- // `null` is the manual-assignment marker (PRESIDENT) — already
250
- // filtered out above, defensive check kept for type narrowing.
251
249
  if (threshold === null)
252
250
  continue;
253
251
  if (volumeBaseUnits >= threshold) {
@@ -289,3 +289,11 @@ describe('isOverOrderCap: base unit boundaries', () => {
289
289
  assert.equal(isOverOrderCap({ orderTokenAmount: UNLIMITED_CAP_BASE_UNITS + 1n, cap: UNLIMITED_CAP_BASE_UNITS }), true);
290
290
  });
291
291
  });
292
+ describe('getTierFromVolume: volume below every threshold', () => {
293
+ it('falls back to peasant for a negative volume instead of returning undefined', () => {
294
+ assert.equal(getTierFromVolume(-1n), 'peasant');
295
+ });
296
+ it('never returns president, whose threshold is null (manual assignment only)', () => {
297
+ assert.notEqual(getTierFromVolume(10n ** 30n), 'president');
298
+ });
299
+ });
package/dist/time.test.js CHANGED
@@ -112,11 +112,32 @@ describe('time', () => {
112
112
  const result = sleep(1);
113
113
  assert.ok(result instanceof Promise);
114
114
  });
115
- it('should resolve immediately for 0ms', async () => {
116
- const start = Date.now();
117
- await sleep(0);
118
- const elapsed = Date.now() - start;
119
- assert.ok(elapsed < 10, `Expected less than 10ms, got ${elapsed}ms`);
115
+ /**
116
+ * Lets resolved promises run their callbacks. setImmediate is not mocked,
117
+ * so this returns even when sleep is wrong, and the assertion then fails
118
+ * instead of the test hanging.
119
+ */
120
+ const flushPendingCallbacks = () => new Promise((resolve) => { setImmediate(resolve); });
121
+ // Mocked timers, not the wall clock: a loaded machine made the old
122
+ // "under 10ms" check fail even though sleep itself was correct.
123
+ it('resolves on the zero-delay tick for 0ms', async (context) => {
124
+ context.mock.timers.enable({ apis: ['setTimeout'] });
125
+ let resolved = false;
126
+ void sleep(0).then(() => { resolved = true; });
127
+ context.mock.timers.tick(0);
128
+ await flushPendingCallbacks();
129
+ assert.equal(resolved, true);
130
+ });
131
+ it('does not resolve before its delay has passed', async (context) => {
132
+ context.mock.timers.enable({ apis: ['setTimeout'] });
133
+ let resolved = false;
134
+ void sleep(50).then(() => { resolved = true; });
135
+ context.mock.timers.tick(49);
136
+ await flushPendingCallbacks();
137
+ assert.equal(resolved, false);
138
+ context.mock.timers.tick(1);
139
+ await flushPendingCallbacks();
140
+ assert.equal(resolved, true);
120
141
  });
121
142
  });
122
143
  describe('formatAbsoluteTimestamp', () => {
package/dist/tokens.d.ts CHANGED
@@ -8370,9 +8370,9 @@ declare const createClient: {
8370
8370
  blockTag?: import("viem").BlockTag | undefined;
8371
8371
  nonce?: string | undefined | undefined;
8372
8372
  address?: `0x${string}` | undefined;
8373
+ time?: Date | undefined;
8373
8374
  domain?: string | undefined | undefined;
8374
8375
  scheme?: string | undefined | undefined;
8375
- time?: Date | undefined;
8376
8376
  message: string;
8377
8377
  signature: import("viem").Hex;
8378
8378
  }) => Promise<boolean>;
@@ -15069,9 +15069,9 @@ declare const createClient: {
15069
15069
  blockTag?: import("viem").BlockTag | undefined;
15070
15070
  nonce?: string | undefined | undefined;
15071
15071
  address?: `0x${string}` | undefined;
15072
+ time?: Date | undefined;
15072
15073
  domain?: string | undefined | undefined;
15073
15074
  scheme?: string | undefined | undefined;
15074
- time?: Date | undefined;
15075
15075
  message: string;
15076
15076
  signature: import("viem").Hex;
15077
15077
  }) => Promise<boolean>;
@@ -31,6 +31,32 @@ import { type ChainId } from './chain.js';
31
31
  * `eth_*` methods.
32
32
  */
33
33
  export declare const VALVE_CITY_PULSECHAIN_RPC = "https://one.valve.city/rpc/vk_demo/evm/369";
34
+ /**
35
+ * Count one outgoing JSON-RPC request for the debug log.
36
+ *
37
+ * Reads the method from a clone, so the real request body stays unread. A body
38
+ * that is not JSON is ignored: a debug counter must never fail a request.
39
+ *
40
+ * @param req - The request viem is about to send.
41
+ */
42
+ export declare const logRpcRequest: (req: Request) => void;
43
+ /**
44
+ * Latency probe for ranked transports: one `eth_blockNumber` call.
45
+ *
46
+ * It measures real query latency, not just the TCP round trip, and avoids the
47
+ * noisy `net_listening` entries viem's default probe leaves in server logs.
48
+ *
49
+ * @param transport - The transport instance viem is ranking.
50
+ * @returns The block number response, which viem times and discards.
51
+ */
52
+ export declare const pingBlockNumber: (transport: {
53
+ transport: {
54
+ request: (args: {
55
+ method: string;
56
+ params: unknown[];
57
+ }) => Promise<unknown>;
58
+ };
59
+ }) => Promise<unknown>;
34
60
  /**
35
61
  * Provex-curated fallback RPC URLs per chain, applied after caller overrides
36
62
  * but before the viem chain default.
@@ -47,7 +73,7 @@ export declare const VALVE_CITY_PULSECHAIN_RPC = "https://one.valve.city/rpc/vk_
47
73
  * - Base (8453): no built-in fallback — Base's default endpoint is reliable
48
74
  * and neither operator runs a Base RPC.
49
75
  */
50
- export declare const FALLBACK_RPC_URLS: Partial<Record<ChainId, readonly string[]>>;
76
+ export declare const FALLBACK_RPC_URLS: Record<ChainId, readonly string[]>;
51
77
  /**
52
78
  * Parse a comma-separated RPC URL list into an ordered array of valid URLs.
53
79
  *
@@ -82,6 +108,31 @@ export declare const parseRpcUrlList: (raw: string | undefined | null) => string
82
108
  * defeat the reason it was placed second.
83
109
  */
84
110
  export type ChainTransportStrategy = 'ranked' | 'ordered';
111
+ /**
112
+ * The strategy to use when the caller does not name one.
113
+ *
114
+ * `ranked` in a browser, `ordered` everywhere else.
115
+ *
116
+ * **Why the environment decides.** viem's ranking is a long-lived-client
117
+ * optimisation: it starts a self-recursive `setTimeout` loop that pings every
118
+ * endpoint forever, and it exposes no way to stop it — `rankTransports` takes
119
+ * no abort signal and the client has no teardown. In a browser that is exactly
120
+ * right; the page owns the client for its lifetime and gets the fastest healthy
121
+ * endpoint for free.
122
+ *
123
+ * In Node the same loop means the process can never exit on its own. A one-shot
124
+ * script that reads one value hangs at the end of its work, and a test file that
125
+ * so much as constructs a client hangs for its full timeout with every
126
+ * assertion passing. `packages/scripts/src/lookup-hash.ts` already carries a
127
+ * hand-added `process.exit(0)` for precisely this.
128
+ *
129
+ * Ranking buys a short-lived process nothing anyway — there is no traffic to
130
+ * optimise over — while `ordered` gives it the same fallback coverage with
131
+ * strict priority and no timer. Callers that genuinely want ranking outside a
132
+ * browser (a long-running Node service) can still pass `strategy: 'ranked'`
133
+ * explicitly.
134
+ */
135
+ export declare const defaultChainTransportStrategy: () => ChainTransportStrategy;
85
136
  /**
86
137
  * Options accepted by {@link createChainTransport}.
87
138
  */
@@ -93,7 +144,7 @@ interface CreateChainTransportOptions {
93
144
  userUrls?: readonly string[];
94
145
  /**
95
146
  * Traffic-distribution strategy. Defaults per environment — see
96
- * {@link defaultStrategy}: `ranked` in a browser, preserving the
147
+ * {@link defaultChainTransportStrategy}: `ranked` in a browser, preserving the
97
148
  * latency-optimised behaviour every browser call-site relies on, and
98
149
  * `ordered` in Node, where ranking's unstoppable poller would keep the
99
150
  * process alive forever and buy a short-lived process nothing.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @fileoverview Tests for the RPC call counter, the ranking probe and the
3
+ * environment-dependent default strategy.
4
+ *
5
+ * The counter exists so a developer can open a browser console, set
6
+ * `window.__RPC_DEBUG = true`, and see which JSON-RPC methods a page is
7
+ * hammering. It must stay silent otherwise and must never fail a request.
8
+ */
9
+ export {};