@provex/utils 1.6.2 → 1.7.0-rc.20260522201545.020a3eb
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +3 -1
- package/dist/consent.d.ts +111 -0
- package/dist/consent.js +194 -0
- package/dist/consent.test.d.ts +1 -0
- package/dist/consent.test.js +247 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/payment.d.ts +64 -2
- package/dist/payment.js +80 -5
- package/dist/payment.test.js +212 -9
- package/dist/reputation.d.ts +59 -20
- package/dist/reputation.js +75 -26
- package/dist/reputation.test.js +73 -25
- package/dist/transports.js +38 -16
- package/package.json +8 -3
- package/skills/utils-integration/SKILL.md +161 -0
package/dist/reputation.js
CHANGED
|
@@ -9,22 +9,27 @@
|
|
|
9
9
|
* - **Platform risk configs**: Multipliers and cooldown flags from payment.ts
|
|
10
10
|
*
|
|
11
11
|
* @remarks
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Reputation is now scored from PulseChain volume only — Base data is no
|
|
13
|
+
* longer aggregated. The taker cap still varies by tier; PulseChain is the
|
|
14
|
+
* only chain that produces taker-tier-influencing volume.
|
|
14
15
|
*
|
|
15
|
-
* Tier progression (by fulfilled volume in USD)
|
|
16
|
-
*
|
|
17
|
-
* -
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* -
|
|
21
|
-
* -
|
|
16
|
+
* Tier progression (by CUMULATIVE / lifetime fulfilled volume in USD).
|
|
17
|
+
* Thresholds mirror peer.xyz's published ladder
|
|
18
|
+
* (https://docs.peer.xyz/guides/for-buyers/reputation#the-five-tiers).
|
|
19
|
+
* Volume is NOT windowed — it accumulates over the user's lifetime, so
|
|
20
|
+
* a user only ever moves UP the ladder, never back down for inactivity:
|
|
21
|
+
* - Entry: $0+ → $100 cap, 12h cooldown
|
|
22
|
+
* - Neutral: $500+ → $250 cap, 6h cooldown
|
|
23
|
+
* - Plus: $2,000+ → $1,000 cap, no cooldown
|
|
24
|
+
* - Pro: $10,000+ → $2,500 cap, no cooldown
|
|
25
|
+
* - Platinum: $25,000+ → $5,000 cap, no cooldown
|
|
26
|
+
* - VIP: Manually assigned → unlimited cap, no cooldown, bypasses all locks
|
|
22
27
|
*
|
|
23
28
|
* Platform multipliers modify the base cap:
|
|
24
29
|
* - Low risk (Revolut, Wise, Monzo, MercadoPago): 5x multiplier, no cooldown
|
|
25
30
|
* - Medium risk (Zelle): 1.5x multiplier
|
|
26
31
|
* - High risk (Venmo, CashApp): 1x multiplier
|
|
27
|
-
* - Highest risk (PayPal): 0.75x multiplier, requires
|
|
32
|
+
* - Highest risk (PayPal): 0.75x multiplier, requires Plus tier or higher
|
|
28
33
|
*
|
|
29
34
|
* @see {@link payment} for platform risk configurations
|
|
30
35
|
*/
|
|
@@ -35,47 +40,52 @@ import { tiers, getPlatformRisk, } from './payment.js';
|
|
|
35
40
|
* Maps universal tier names to their configuration.
|
|
36
41
|
*
|
|
37
42
|
* @remarks
|
|
38
|
-
*
|
|
39
|
-
*
|
|
43
|
+
* Internal tier keys (peasant / neutral / plus / pro / platinum / president)
|
|
44
|
+
* are stable data identifiers and DO NOT change — the indexer, the contract
|
|
45
|
+
* gating service, and persisted user state all key on them. The `name` field
|
|
46
|
+
* below is the user-facing display name; we use a neutral progression ladder
|
|
47
|
+
* (Entry / Neutral / Plus / Pro / Platinum / VIP) rather than peer.xyz's
|
|
48
|
+
* brand voice ("Peer Peasant" etc.), which is theirs and reads as gentle
|
|
49
|
+
* hazing of low-volume users — not the tone Provex wants.
|
|
40
50
|
*/
|
|
41
51
|
export const TAKER_TIERS = {
|
|
42
52
|
[tiers.PEASANT]: {
|
|
43
|
-
name: '
|
|
53
|
+
name: 'Entry',
|
|
44
54
|
volumeThreshold: 0,
|
|
45
55
|
baseCap: 100,
|
|
46
56
|
cooldownHours: 12,
|
|
47
57
|
volumeRangeDisplay: '$0 - $500',
|
|
48
58
|
},
|
|
49
59
|
[tiers.NEUTRAL]: {
|
|
50
|
-
name: '
|
|
60
|
+
name: 'Neutral',
|
|
51
61
|
volumeThreshold: 500,
|
|
52
62
|
baseCap: 250,
|
|
53
63
|
cooldownHours: 6,
|
|
54
64
|
volumeRangeDisplay: '$500 - $2k',
|
|
55
65
|
},
|
|
56
66
|
[tiers.PLUS]: {
|
|
57
|
-
name: '
|
|
67
|
+
name: 'Plus',
|
|
58
68
|
volumeThreshold: 2000,
|
|
59
69
|
baseCap: 1000,
|
|
60
70
|
cooldownHours: 0,
|
|
61
71
|
volumeRangeDisplay: '$2k - $10k',
|
|
62
72
|
},
|
|
63
73
|
[tiers.PRO]: {
|
|
64
|
-
name: '
|
|
74
|
+
name: 'Pro',
|
|
65
75
|
volumeThreshold: 10000,
|
|
66
76
|
baseCap: 2500,
|
|
67
77
|
cooldownHours: 0,
|
|
68
78
|
volumeRangeDisplay: '$10k - $25k',
|
|
69
79
|
},
|
|
70
80
|
[tiers.PLATINUM]: {
|
|
71
|
-
name: '
|
|
81
|
+
name: 'Platinum',
|
|
72
82
|
volumeThreshold: 25000,
|
|
73
83
|
baseCap: 5000,
|
|
74
84
|
cooldownHours: 0,
|
|
75
85
|
volumeRangeDisplay: '$25k+',
|
|
76
86
|
},
|
|
77
87
|
[tiers.PRESIDENT]: {
|
|
78
|
-
name: '
|
|
88
|
+
name: 'VIP',
|
|
79
89
|
volumeThreshold: Infinity, // Manually assigned — cannot be reached via volume
|
|
80
90
|
baseCap: 0, // 0 = no cap (VIP limits)
|
|
81
91
|
cooldownHours: 0,
|
|
@@ -94,25 +104,46 @@ export const TIER_ORDER = [
|
|
|
94
104
|
* Matches peer.xyz's TR() function behaviour.
|
|
95
105
|
*
|
|
96
106
|
* @example
|
|
97
|
-
* getVolumeRangeDisplay('pro') // "$
|
|
107
|
+
* getVolumeRangeDisplay('pro') // "$5k - $10k"
|
|
98
108
|
* getVolumeRangeDisplay('president') // "VIP"
|
|
99
109
|
*/
|
|
100
110
|
export function getVolumeRangeDisplay(tier) {
|
|
101
111
|
return TAKER_TIERS[tier].volumeRangeDisplay;
|
|
102
112
|
}
|
|
103
113
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
114
|
+
* Sentinel cap value used in place of `Infinity` (which has no bigint
|
|
115
|
+
* representation) to signal "no upper limit" — president tier on every
|
|
116
|
+
* platform. Equal to `2n ** 256n - 1n` (max uint256); no realistic
|
|
117
|
+
* token amount can exceed it, so the standard `amount > cap` comparison
|
|
118
|
+
* naturally short-circuits to false for unlimited users.
|
|
119
|
+
*
|
|
120
|
+
* Callers that render the cap as a string MUST check for this sentinel
|
|
121
|
+
* before formatting — `Number(UNLIMITED_CAP_BASE_UNITS)` overflows to
|
|
122
|
+
* `Infinity` but `.toLocaleString()` on the bigint prints a 78-digit
|
|
123
|
+
* number that is not useful in a UI.
|
|
124
|
+
*/
|
|
125
|
+
export const UNLIMITED_CAP_BASE_UNITS = 2n ** 256n - 1n;
|
|
126
|
+
/**
|
|
127
|
+
* Calculate the effective cap for a tier and platform combination, in
|
|
128
|
+
* token base units (scaled by `decimals`). Designed so the caller can
|
|
129
|
+
* compare directly against a token amount it already holds as a bigint
|
|
130
|
+
* — no rebasing from USD floats on the consumer side.
|
|
131
|
+
*
|
|
132
|
+
* Assumes the token trades 1:1 with USD (USDC, USDT, DAI). When we add
|
|
133
|
+
* a non-stablecoin token this function needs a price feed or the caller
|
|
134
|
+
* needs to convert before comparing.
|
|
106
135
|
*
|
|
107
136
|
* @param tier - The user's current tier
|
|
108
137
|
* @param providerKey - The payment platform
|
|
109
|
-
* @
|
|
110
|
-
*
|
|
138
|
+
* @param decimals - Token decimals (USDC = 6, an 18-decimal stable = 18)
|
|
139
|
+
* @returns Effective cap in token base units, or:
|
|
140
|
+
* - `null` — platform is locked at this tier
|
|
141
|
+
* - `UNLIMITED_CAP_BASE_UNITS` — president tier (no cap enforcement)
|
|
111
142
|
*/
|
|
112
|
-
export function getEffectiveCap({ tier, providerKey, }) {
|
|
143
|
+
export function getEffectiveCap({ tier, providerKey, decimals, }) {
|
|
113
144
|
// President bypasses all caps and locks
|
|
114
145
|
if (tier === tiers.PRESIDENT)
|
|
115
|
-
return
|
|
146
|
+
return UNLIMITED_CAP_BASE_UNITS;
|
|
116
147
|
const tierConfig = TAKER_TIERS[tier];
|
|
117
148
|
const platformRisk = getPlatformRisk(base.id, providerKey);
|
|
118
149
|
// Check if platform is locked for this tier
|
|
@@ -123,7 +154,25 @@ export function getEffectiveCap({ tier, providerKey, }) {
|
|
|
123
154
|
return null; // Platform locked
|
|
124
155
|
}
|
|
125
156
|
}
|
|
126
|
-
|
|
157
|
+
// Compute the USD cap first (still uses the integer-USD ladder), then
|
|
158
|
+
// scale up by 10^decimals into token base units. Math.floor on the
|
|
159
|
+
// USD product matches the legacy display where a $375.50 cap shows as
|
|
160
|
+
// $375 — keep the rounding identical to avoid silent enforcement drift.
|
|
161
|
+
const capUsd = Math.floor(tierConfig.baseCap * platformRisk.capMultiplier);
|
|
162
|
+
return BigInt(capUsd) * (10n ** BigInt(decimals));
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Companion display helper — converts the base-units cap back into a
|
|
166
|
+
* USD number for `toLocaleString()`-style formatting. Returns `null`
|
|
167
|
+
* for locked and `Infinity` for unlimited, mirroring the legacy
|
|
168
|
+
* `getEffectiveCap` return shape so display code stays simple.
|
|
169
|
+
*/
|
|
170
|
+
export function getEffectiveCapDisplayUsd(cap, decimals) {
|
|
171
|
+
if (cap === null)
|
|
172
|
+
return null;
|
|
173
|
+
if (cap === UNLIMITED_CAP_BASE_UNITS)
|
|
174
|
+
return Infinity;
|
|
175
|
+
return Number(cap) / 10 ** decimals;
|
|
127
176
|
}
|
|
128
177
|
/**
|
|
129
178
|
* Get the cooldown duration for a tier and platform.
|
package/dist/reputation.test.js
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { TAKER_TIERS, TIER_ORDER, getTierFromVolume, getEffectiveCap, getCooldownHours, getVolumeRangeDisplay, formatCooldown, formatCap, } from './reputation.js';
|
|
3
|
+
import { TAKER_TIERS, TIER_ORDER, getTierFromVolume, getEffectiveCap, getEffectiveCapDisplayUsd, UNLIMITED_CAP_BASE_UNITS, getCooldownHours, getVolumeRangeDisplay, formatCooldown, formatCap, } from './reputation.js';
|
|
4
4
|
import { tiers } from './payment.js';
|
|
5
|
+
// USDC convention — 6 decimals, the existing on-chain token. Tests below
|
|
6
|
+
// also cover an 18-decimal stable to lock in the scaling behavior the
|
|
7
|
+
// future non-USDC rollout will rely on.
|
|
8
|
+
const USDC = 6;
|
|
9
|
+
const STABLE18 = 18;
|
|
5
10
|
describe('TAKER_TIERS', () => {
|
|
6
11
|
it('should have correct tier names', () => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
assert.equal(TAKER_TIERS.
|
|
12
|
+
// Display names use a neutral progression ladder, NOT peer.xyz brand
|
|
13
|
+
// voice. Internal tier keys (peasant/neutral/plus/pro/platinum) are
|
|
14
|
+
// unchanged — those are stable data identifiers consumed by the
|
|
15
|
+
// indexer and the contract gating service.
|
|
16
|
+
assert.equal(TAKER_TIERS.peasant.name, 'Entry');
|
|
17
|
+
assert.equal(TAKER_TIERS.neutral.name, 'Neutral');
|
|
18
|
+
assert.equal(TAKER_TIERS.plus.name, 'Plus');
|
|
19
|
+
assert.equal(TAKER_TIERS.pro.name, 'Pro');
|
|
20
|
+
assert.equal(TAKER_TIERS.platinum.name, 'Platinum');
|
|
12
21
|
});
|
|
13
22
|
it('should have correct volume thresholds', () => {
|
|
23
|
+
// Cumulative / lifetime fulfilled volume — mirrors peer.xyz's
|
|
24
|
+
// published ladder ($0 / $500 / $2k / $10k / $25k). Locked here so
|
|
25
|
+
// any drift surfaces immediately; the JSDoc on TAKER_TIERS cites
|
|
26
|
+
// the upstream source.
|
|
14
27
|
assert.equal(TAKER_TIERS.peasant.volumeThreshold, 0);
|
|
15
28
|
assert.equal(TAKER_TIERS.neutral.volumeThreshold, 500);
|
|
16
29
|
assert.equal(TAKER_TIERS.plus.volumeThreshold, 2000);
|
|
@@ -88,25 +101,60 @@ describe('getTierFromVolume', () => {
|
|
|
88
101
|
assert.notEqual(getTierFromVolume(1_000_000_000), tiers.PRESIDENT);
|
|
89
102
|
});
|
|
90
103
|
});
|
|
91
|
-
describe('getEffectiveCap', () => {
|
|
92
|
-
it('
|
|
93
|
-
// Peasant ($100 base) with Revolut (5x) = $500
|
|
94
|
-
assert.equal(getEffectiveCap({ tier: 'peasant', providerKey: 'revolut' }),
|
|
95
|
-
// Neutral ($250 base) with Zelle (1.5x) = $375
|
|
96
|
-
assert.equal(getEffectiveCap({ tier: 'neutral', providerKey: 'zelle' }),
|
|
97
|
-
// Plus ($1000 base) with Venmo (1x) = $1000
|
|
98
|
-
assert.equal(getEffectiveCap({ tier: 'plus', providerKey: 'venmo' }),
|
|
99
|
-
});
|
|
100
|
-
it('
|
|
101
|
-
// Peasant ($100
|
|
102
|
-
assert.equal(getEffectiveCap({ tier: 'peasant', providerKey: 'monzo' }),
|
|
103
|
-
// Neutral ($250
|
|
104
|
-
assert.equal(getEffectiveCap({ tier: 'neutral', providerKey: 'mercadopago' }),
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
assert.equal(getEffectiveCap({ tier:
|
|
109
|
-
|
|
104
|
+
describe('getEffectiveCap (USDC base units)', () => {
|
|
105
|
+
it('returns the cap in USDC base units (6 decimals)', () => {
|
|
106
|
+
// Peasant ($100 base) with Revolut (5x) = $500 = 500_000_000n base units
|
|
107
|
+
assert.equal(getEffectiveCap({ tier: 'peasant', providerKey: 'revolut', decimals: USDC }), 500000000n);
|
|
108
|
+
// Neutral ($250 base) with Zelle (1.5x) = $375 = 375_000_000n
|
|
109
|
+
assert.equal(getEffectiveCap({ tier: 'neutral', providerKey: 'zelle', decimals: USDC }), 375000000n);
|
|
110
|
+
// Plus ($1000 base) with Venmo (1x) = $1000 = 1_000_000_000n
|
|
111
|
+
assert.equal(getEffectiveCap({ tier: 'plus', providerKey: 'venmo', decimals: USDC }), 1000000000n);
|
|
112
|
+
});
|
|
113
|
+
it('returns the cap for low-risk platforms (5x multiplier)', () => {
|
|
114
|
+
// Peasant ($100) × Monzo (5x) = $500
|
|
115
|
+
assert.equal(getEffectiveCap({ tier: 'peasant', providerKey: 'monzo', decimals: USDC }), 500000000n);
|
|
116
|
+
// Neutral ($250) × MercadoPago (5x) = $1250
|
|
117
|
+
assert.equal(getEffectiveCap({ tier: 'neutral', providerKey: 'mercadopago', decimals: USDC }), 1250000000n);
|
|
118
|
+
});
|
|
119
|
+
it('scales correctly for an 18-decimal token (future non-USDC rollout)', () => {
|
|
120
|
+
// Same $500 cap, but in 18-decimal base units: 500 * 10^18
|
|
121
|
+
assert.equal(getEffectiveCap({ tier: 'peasant', providerKey: 'revolut', decimals: STABLE18 }), 500n * 10n ** 18n);
|
|
122
|
+
// Plus $1000 cap in 18-decimal base units
|
|
123
|
+
assert.equal(getEffectiveCap({ tier: 'plus', providerKey: 'venmo', decimals: STABLE18 }), 1000n * 10n ** 18n);
|
|
124
|
+
});
|
|
125
|
+
it('returns UNLIMITED_CAP_BASE_UNITS for president on every platform', () => {
|
|
126
|
+
// President bypasses the cap on every platform, regardless of decimals
|
|
127
|
+
assert.equal(getEffectiveCap({ tier: tiers.PRESIDENT, providerKey: 'venmo', decimals: USDC }), UNLIMITED_CAP_BASE_UNITS);
|
|
128
|
+
assert.equal(getEffectiveCap({ tier: tiers.PRESIDENT, providerKey: 'revolut', decimals: STABLE18 }), UNLIMITED_CAP_BASE_UNITS);
|
|
129
|
+
});
|
|
130
|
+
it('returns null when the platform is locked at this tier', () => {
|
|
131
|
+
// PayPal carries `minimumTier: PLUS` in the production config, so
|
|
132
|
+
// peasant and neutral tiers should see it as locked. PayPal is
|
|
133
|
+
// currently disabled at the deposit-liquidity layer (no makers),
|
|
134
|
+
// but the gating logic must still treat it as locked for low tiers
|
|
135
|
+
// so the rule stays enforced if it comes back online.
|
|
136
|
+
assert.equal(getEffectiveCap({ tier: 'peasant', providerKey: 'paypal', decimals: USDC }), null);
|
|
137
|
+
assert.equal(getEffectiveCap({ tier: 'neutral', providerKey: 'paypal', decimals: USDC }), null);
|
|
138
|
+
});
|
|
139
|
+
it('unlocks the cap once the user reaches the platform minimum tier', () => {
|
|
140
|
+
// Plus tier matches PayPal's minimumTier — cap should compute as a
|
|
141
|
+
// bigint, not null. $1000 base * 0.75 multiplier = $750
|
|
142
|
+
assert.equal(getEffectiveCap({ tier: 'plus', providerKey: 'paypal', decimals: USDC }), 750000000n);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
describe('getEffectiveCapDisplayUsd', () => {
|
|
146
|
+
it('returns null for a locked platform', () => {
|
|
147
|
+
assert.equal(getEffectiveCapDisplayUsd(null, USDC), null);
|
|
148
|
+
});
|
|
149
|
+
it('returns Infinity for the unlimited sentinel', () => {
|
|
150
|
+
assert.equal(getEffectiveCapDisplayUsd(UNLIMITED_CAP_BASE_UNITS, USDC), Infinity);
|
|
151
|
+
});
|
|
152
|
+
it('round-trips a USDC base-units cap back to USD dollars', () => {
|
|
153
|
+
// 500_000_000n base units at 6 decimals = $500
|
|
154
|
+
assert.equal(getEffectiveCapDisplayUsd(500000000n, USDC), 500);
|
|
155
|
+
});
|
|
156
|
+
it('round-trips an 18-decimal base-units cap back to USD dollars', () => {
|
|
157
|
+
assert.equal(getEffectiveCapDisplayUsd(500n * 10n ** 18n, STABLE18), 500);
|
|
110
158
|
});
|
|
111
159
|
});
|
|
112
160
|
describe('getCooldownHours', () => {
|
package/dist/transports.js
CHANGED
|
@@ -16,6 +16,21 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { fallback, http } from 'viem';
|
|
18
18
|
import { chainIdToChain } from './chain.js';
|
|
19
|
+
// Per-method call counter. Enabled at runtime by setting window.__RPC_DEBUG = true
|
|
20
|
+
// in the browser console (works in pre-built dist — no Vite env injection needed).
|
|
21
|
+
const _rpcCounts = {};
|
|
22
|
+
let _rpcLastWindow = Date.now();
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
const _isDebug = () => typeof window !== 'undefined' && window.__RPC_DEBUG === true;
|
|
25
|
+
const _rpcLog = (method) => {
|
|
26
|
+
if (!_isDebug())
|
|
27
|
+
return;
|
|
28
|
+
_rpcCounts[method] = (_rpcCounts[method] ?? 0) + 1;
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
const total = Object.values(_rpcCounts).reduce((a, b) => a + b, 0);
|
|
31
|
+
const elapsed = ((now - _rpcLastWindow) / 1000).toFixed(1);
|
|
32
|
+
console.debug(`[RPC] ${method} | ${total} calls in ${elapsed}s | window: ${JSON.stringify(_rpcCounts)}`);
|
|
33
|
+
};
|
|
19
34
|
/**
|
|
20
35
|
* Provex-curated fallback RPC URLs per chain, applied after caller overrides
|
|
21
36
|
* but before the viem chain default.
|
|
@@ -75,24 +90,31 @@ export const resolveChainRpcUrls = (chainId, options = {}) => {
|
|
|
75
90
|
*/
|
|
76
91
|
export const createChainTransport = (chainId, options = {}) => {
|
|
77
92
|
const urls = resolveChainRpcUrls(chainId, options);
|
|
78
|
-
const transports = urls.map((url) => http(url
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// 2. Multi-transport chains get a 60s interval + 3-sample window. Slower
|
|
88
|
-
// than viem's default but still tight enough that a hung primary
|
|
89
|
-
// shifts traffic within a couple of minutes, which is the right grain
|
|
90
|
-
// for the browser-side latency model.
|
|
93
|
+
const transports = urls.map((url) => http(url, {
|
|
94
|
+
retryCount: 2,
|
|
95
|
+
onFetchRequest: (req) => {
|
|
96
|
+
void req.clone().json().then((body) => _rpcLog(body.method ?? '?')).catch(() => { });
|
|
97
|
+
},
|
|
98
|
+
}));
|
|
99
|
+
// Single-URL chains: return the transport directly. fallback([single]) adds
|
|
100
|
+
// overhead with no benefit — there's nothing to fall back to, and retryCount
|
|
101
|
+
// is already set on the http() call above.
|
|
91
102
|
if (transports.length <= 1) {
|
|
92
|
-
return
|
|
103
|
+
return transports[0];
|
|
93
104
|
}
|
|
105
|
+
// Multi-URL chains: rank-enabled fallback so the fastest healthy endpoint
|
|
106
|
+
// serves traffic. Custom ping uses eth_blockNumber instead of viem's default
|
|
107
|
+
// net_listening — measures actual query latency (not just TCP round-trip) and
|
|
108
|
+
// avoids noisy net_listening entries in server access logs.
|
|
109
|
+
//
|
|
110
|
+
// interval=60s: one probe cycle per minute is plenty for browser clients; the
|
|
111
|
+
// primary RPC only shifts after it's been slow for a full minute, which is the
|
|
112
|
+
// right grain for a latency model (not a health check).
|
|
94
113
|
return fallback(transports, {
|
|
95
|
-
rank: {
|
|
96
|
-
|
|
114
|
+
rank: {
|
|
115
|
+
interval: 60_000,
|
|
116
|
+
sampleCount: 3,
|
|
117
|
+
ping: (t) => t.transport.request({ method: 'eth_blockNumber', params: [] }),
|
|
118
|
+
},
|
|
97
119
|
});
|
|
98
120
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@provex/utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0-rc.20260522201545.020a3eb",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -108,6 +108,10 @@
|
|
|
108
108
|
"./transports": {
|
|
109
109
|
"import": "./dist/transports.js",
|
|
110
110
|
"types": "./dist/transports.d.ts"
|
|
111
|
+
},
|
|
112
|
+
"./consent": {
|
|
113
|
+
"import": "./dist/consent.js",
|
|
114
|
+
"types": "./dist/consent.d.ts"
|
|
111
115
|
}
|
|
112
116
|
},
|
|
113
117
|
"scripts": {
|
|
@@ -118,10 +122,11 @@
|
|
|
118
122
|
},
|
|
119
123
|
"keywords": [],
|
|
120
124
|
"author": "",
|
|
121
|
-
"license": "
|
|
122
|
-
"description": "",
|
|
125
|
+
"license": "MIT",
|
|
126
|
+
"description": "Shared types, chain configuration, and utilities for the Provex protocol",
|
|
123
127
|
"files": [
|
|
124
128
|
"dist",
|
|
129
|
+
"skills",
|
|
125
130
|
"README.md"
|
|
126
131
|
],
|
|
127
132
|
"dependencies": {
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: utils-integration
|
|
3
|
+
description: Use when integrating `@provex/utils` — when looking up Provex contract addresses by chain, formatting fiat currencies, converting between token amounts and currency amounts, sorting deposits/intents, computing maker/taker profits, hashing payee identifiers, serializing bigints across JSON boundaries, or resolving payment provider keys to validation keys. Covers the subpath-import pattern, the bigint-only-no-intermediate-Number arithmetic rule, the JSON bigint round-trip convention, and where chain/currency/provider/token registries live.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# `@provex/utils` Integration
|
|
7
|
+
|
|
8
|
+
Shared types, registries, and pure utilities for the Provex protocol. No React, no viem clients, no async dependencies — everything here is sync and tree-shakeable via subpath imports.
|
|
9
|
+
|
|
10
|
+
## Subpath imports — pay only for what you use
|
|
11
|
+
|
|
12
|
+
The package exposes ~20 subpaths. Always import from the specific subpath, not the root, so bundlers can tree-shake aggressively.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { contracts } from '@provex/utils/contracts'
|
|
16
|
+
import { Currency, getCurrencyInfo, formatCurrencyAmount } from '@provex/utils/currencies'
|
|
17
|
+
import { providers, providerKeyToValidationKey } from '@provex/utils/payment'
|
|
18
|
+
import { tokenByAddress, allTokensInfo } from '@provex/utils/tokens'
|
|
19
|
+
import { formatUnits, parseUnits, addThousandsSeparator } from '@provex/utils/units'
|
|
20
|
+
import { formatTimestamp, formatDuration, interval, sleep } from '@provex/utils/time'
|
|
21
|
+
import { processConversionRates, convertTokenInputToCurrency } from '@provex/utils/conversionRates'
|
|
22
|
+
import { jsonSerializer, mapBigIntFields } from '@provex/utils/json'
|
|
23
|
+
import { getExplorerUrl } from '@provex/utils/chain'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Root `@provex/utils` re-exports everything but defeats tree-shaking. Prefer subpath imports.
|
|
27
|
+
|
|
28
|
+
## BigInt-only arithmetic (non-negotiable)
|
|
29
|
+
|
|
30
|
+
All fee/wei/rate/percentile math must stay `bigint` end-to-end. Use `Number()` exactly once — at the leaf where you hand a value to CSS/render. **Never** introduce intermediate `Number()` coercions.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// ✅ correct
|
|
34
|
+
const total = amountInt + (amountInt * feeBps) / 10000n
|
|
35
|
+
const displayed = Number(formatUnits(total, { decimals: 6, truncateDecimals: 2 }))
|
|
36
|
+
|
|
37
|
+
// ❌ wrong — intermediate Number() loses precision on large values
|
|
38
|
+
const total = Number(amountInt) + Number(amountInt * feeBps / 10000n)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`formatUnits` and `parseUnits` are the canonical bigint ↔ string converters. They never go through `Number`.
|
|
42
|
+
|
|
43
|
+
## JSON round-trip for bigints
|
|
44
|
+
|
|
45
|
+
Any time bigints cross a JSON boundary (localStorage, fetch body, postMessage), use the package's serializer pair:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { jsonSerializer } from '@provex/utils/json'
|
|
49
|
+
|
|
50
|
+
// Serialize
|
|
51
|
+
const blob = JSON.stringify({ amount: 1_000_000n }, jsonSerializer.replacer)
|
|
52
|
+
// → '{"amount":"bigint:1000000"}'
|
|
53
|
+
|
|
54
|
+
// Deserialize
|
|
55
|
+
const parsed = JSON.parse(blob, jsonSerializer.reviver)
|
|
56
|
+
// → { amount: 1000000n }
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The tagged-string convention (`bigint:<dec>`) survives `JSON.stringify` / `JSON.parse` round-trips without losing precision. Anything custom should follow this exact shape so other Provex packages can interop.
|
|
60
|
+
|
|
61
|
+
## Chain and contract lookups
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { contracts } from '@provex/utils/contracts'
|
|
65
|
+
|
|
66
|
+
contracts.escrow[369] // V3 Escrow on PulseChain
|
|
67
|
+
contracts.orchestrator[369] // V3 Orchestrator on PulseChain
|
|
68
|
+
contracts.usdc[369] // USDC token on PulseChain
|
|
69
|
+
contracts.venmoReclaimVerifier[369]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The contracts map is keyed by chain ID. PulseChain (`369`) is the primary deployment; Base (`8453`) is peer-trade only.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { chainIds } from '@provex/utils/chain'
|
|
76
|
+
|
|
77
|
+
chainIds // [369, 8453, ...] — protocol-supported set, for STATIC registration
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
> **Two-layer chain model.** `chainIds` is the **protocol-supported** set (every chain the SDK can address). The **deployment-enabled** set (the chains an app actually trades on) is separate and app-defined. Background loops (oracles, trackers, polling) MUST consume the deployment layer — iterating `chainIds` will boot lazy ChainSources for chains your app isn't trading on and burn RPC credits.
|
|
81
|
+
|
|
82
|
+
## Currency formatting
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { getCurrencyInfo, formatCurrencyAmount, Currency } from '@provex/utils/currencies'
|
|
86
|
+
|
|
87
|
+
const usd = getCurrencyInfo('USD') // { symbol: '$', decimals: 2, ... }
|
|
88
|
+
formatCurrencyAmount(1234.56, 'USD') // '$1,234.56'
|
|
89
|
+
formatCurrencyAmount(1234.56, 'EUR') // '€1,234.56'
|
|
90
|
+
|
|
91
|
+
// Custom currency (rare — most callers use the SUPPORTED_CURRENCIES registry)
|
|
92
|
+
const gbp = new Currency('GBP', '£', 'British Pound', 2)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Supported codes: USD, EUR, GBP, CAD, AUD, JPY, CHF, CNY, INR, BRL, MXN, ARS (plus a few more — see `SUPPORTED_CURRENCIES`).
|
|
96
|
+
|
|
97
|
+
## Token amount conversions
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { convertTokenInputToCurrency, convertCurrencyToTokenOutput } from '@provex/utils/conversionRates'
|
|
101
|
+
import { parseUnits } from '@provex/utils/units'
|
|
102
|
+
|
|
103
|
+
// Token → fiat (e.g. "100 USDC at 1.05 USD/USDC")
|
|
104
|
+
const fiatAmount = convertTokenInputToCurrency({
|
|
105
|
+
token: { decimals: 6 },
|
|
106
|
+
amountOutInt: parseUnits('100', 6),
|
|
107
|
+
rate: parseUnits('1.05', 18),
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// Fiat → token
|
|
111
|
+
const tokenAmount = convertCurrencyToTokenOutput({
|
|
112
|
+
token: { decimals: 6 },
|
|
113
|
+
currencyAmountInt: parseUnits('100', 2), // $100
|
|
114
|
+
rate: parseUnits('1.05', 18),
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Rate decimals are **18** by convention across the protocol — don't change them.
|
|
119
|
+
|
|
120
|
+
## Payment providers
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { providers, subProviders, providerKeyToValidationKey } from '@provex/utils/payment'
|
|
124
|
+
|
|
125
|
+
providers.VENMO // 'venmo'
|
|
126
|
+
providers.ZELLE // 'zelle'
|
|
127
|
+
subProviders.CHASE // 'chase' (Zelle banking partner)
|
|
128
|
+
providerKeyToValidationKey.venmo // 'venmoUsername'
|
|
129
|
+
providerKeyToValidationKey.zelle // 'zelleUsername'
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Always source provider keys and validation keys from this registry — don't hardcode strings. The keys are used in deposit creation, intent signaling, and indexer queries; drift between sites creates silent matching failures.
|
|
133
|
+
|
|
134
|
+
## Time and duration helpers
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { interval, formatTimestamp, formatDuration, sleep } from '@provex/utils/time'
|
|
138
|
+
|
|
139
|
+
interval.second // 1000
|
|
140
|
+
interval.minute // 60 * 1000
|
|
141
|
+
interval.hour // 60 * 60 * 1000
|
|
142
|
+
interval.day // 24 * 60 * 60 * 1000
|
|
143
|
+
|
|
144
|
+
formatTimestamp(Date.now() - interval.hour) // '1h ago'
|
|
145
|
+
formatDuration({ from: t0, to: t1 }) // '1 day'
|
|
146
|
+
|
|
147
|
+
await sleep(1000)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Common pitfalls
|
|
151
|
+
|
|
152
|
+
- **Don't hardcode contract addresses.** Always go through `@provex/utils/contracts` so the v2/v3 distinction stays correct.
|
|
153
|
+
- **Don't conflate `chainIds` with deployment-enabled chains.** See the two-layer note above.
|
|
154
|
+
- **Don't introduce `Number()` mid-pipeline.** Single coercion at the leaf only.
|
|
155
|
+
- **Don't `JSON.stringify` raw bigints.** Use `jsonSerializer.replacer`.
|
|
156
|
+
|
|
157
|
+
## When NOT to import from this package
|
|
158
|
+
|
|
159
|
+
- **You need React hooks** → use `@provex/react`.
|
|
160
|
+
- **You need a viem client wrapper** → build one with viem directly; this package is sync-only.
|
|
161
|
+
- **You need indexer queries** → use `@provex/indexer-client`.
|