@provex/utils 1.0.16 → 1.2.4
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 +446 -0
- package/dist/chain.js +98 -0
- package/dist/chain.test.js +112 -0
- package/dist/contracts.d.ts +206 -0
- package/dist/contracts.js +292 -0
- package/dist/contracts.test.js +243 -0
- package/dist/conversionRates.d.ts +215 -0
- package/dist/{src/conversionRates.js → conversionRates.js} +74 -4
- package/dist/conversionRates.test.js +473 -0
- package/dist/currencies.d.ts +248 -0
- package/dist/{src/currencies.js → currencies.js} +119 -38
- package/dist/currencies.test.js +343 -0
- package/dist/data/countries.json +1099 -0
- package/dist/deposit.d.ts +69 -0
- package/dist/deposit.js +52 -0
- package/dist/depositKeys.d.ts +154 -0
- package/dist/depositKeys.js +104 -0
- package/dist/depositKeys.test.d.ts +1 -0
- package/dist/depositKeys.test.js +231 -0
- package/dist/events.d.ts +41 -0
- package/dist/events.js +92 -0
- package/dist/events.test.d.ts +1 -0
- package/dist/events.test.js +111 -0
- package/dist/fees.d.ts +136 -0
- package/dist/fees.js +178 -0
- package/dist/fees.test.d.ts +1 -0
- package/dist/fees.test.js +165 -0
- package/dist/ids.d.ts +175 -0
- package/dist/ids.js +231 -0
- package/dist/ids.test.d.ts +1 -0
- package/dist/ids.test.js +607 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +45 -0
- package/dist/input.d.ts +26 -0
- package/dist/{src/input.js → input.js} +23 -4
- package/dist/intent.d.ts +50 -0
- package/dist/intent.js +28 -0
- package/dist/intent.test.d.ts +1 -0
- package/dist/intent.test.js +119 -0
- package/dist/json.d.ts +55 -0
- package/dist/json.js +80 -0
- package/dist/participants.d.ts +22 -0
- package/dist/participants.js +28 -0
- package/dist/payment.d.ts +481 -0
- package/dist/payment.js +1149 -0
- package/dist/payment.test.d.ts +1 -0
- package/dist/payment.test.js +272 -0
- package/dist/peer-types.d.ts +327 -0
- package/dist/peer-types.js +169 -0
- package/dist/profit.d.ts +49 -0
- package/dist/profit.js +57 -0
- package/dist/profit.test.d.ts +1 -0
- package/dist/profit.test.js +315 -0
- package/dist/reputation.d.ts +125 -0
- package/dist/reputation.js +188 -0
- package/dist/reputation.test.d.ts +1 -0
- package/dist/reputation.test.js +156 -0
- package/dist/sort.d.ts +75 -0
- package/dist/sort.js +70 -0
- package/dist/sort.test.d.ts +1 -0
- package/dist/sort.test.js +216 -0
- package/dist/stateKey.d.ts +35 -0
- package/dist/stateKey.js +36 -0
- package/dist/stats.d.ts +61 -0
- package/dist/stats.js +53 -0
- package/dist/stats.test.d.ts +1 -0
- package/dist/stats.test.js +104 -0
- package/dist/time.d.ts +104 -0
- package/dist/time.js +162 -0
- package/dist/time.test.d.ts +1 -0
- package/dist/time.test.js +231 -0
- package/dist/tokens.d.ts +15502 -0
- package/dist/{src/tokens.js → tokens.js} +98 -17
- package/dist/tokens.test.d.ts +1 -0
- package/dist/tokens.test.js +193 -0
- package/dist/units.d.ts +200 -0
- package/dist/units.js +279 -0
- package/dist/units.test.d.ts +1 -0
- package/dist/units.test.js +327 -0
- package/package.json +119 -29
- package/dist/scripts/convert-svg.js +0 -17
- package/dist/src/chain.d.ts +0 -14
- package/dist/src/chain.js +0 -18
- package/dist/src/contracts.d.ts +0 -30
- package/dist/src/contracts.js +0 -64
- package/dist/src/conversionRates.d.ts +0 -124
- package/dist/src/currencies.d.ts +0 -144
- package/dist/src/data/countries.json +0 -1099
- package/dist/src/deposit.d.ts +0 -1
- package/dist/src/events.d.ts +0 -3
- package/dist/src/events.js +0 -16
- package/dist/src/ids.d.ts +0 -72
- package/dist/src/ids.js +0 -81
- package/dist/src/index.d.ts +0 -16
- package/dist/src/index.js +0 -16
- package/dist/src/input.d.ts +0 -7
- package/dist/src/intent.d.ts +0 -14
- package/dist/src/json.d.ts +0 -18
- package/dist/src/json.js +0 -44
- package/dist/src/participants.d.ts +0 -7
- package/dist/src/participants.js +0 -13
- package/dist/src/payment.d.ts +0 -164
- package/dist/src/payment.js +0 -439
- package/dist/src/profit.d.ts +0 -28
- package/dist/src/profit.js +0 -34
- package/dist/src/sort.d.ts +0 -36
- package/dist/src/sort.js +0 -39
- package/dist/src/stats.d.ts +0 -7
- package/dist/src/stats.js +0 -8
- package/dist/src/time.d.ts +0 -54
- package/dist/src/time.js +0 -106
- package/dist/src/tokens.d.ts +0 -3675
- package/dist/src/units.d.ts +0 -17
- package/dist/src/units.js +0 -79
- package/dist/src/zkp2p-types.d.ts +0 -48
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/scripts/convert-svg.ts +0 -22
- package/src/chain.ts +0 -27
- package/src/contracts.ts +0 -89
- package/src/conversionRates.ts +0 -356
- package/src/currencies.ts +0 -286
- package/src/data/countries.json +0 -1099
- package/src/deposit.ts +0 -2
- package/src/events.ts +0 -21
- package/src/flags/eur.svg +0 -4
- package/src/flags/usd.svg +0 -25
- package/src/flags.ts +0 -4
- package/src/global.d.ts +0 -9
- package/src/ids.ts +0 -137
- package/src/index.ts +0 -16
- package/src/input.ts +0 -35
- package/src/intent.ts +0 -15
- package/src/json.ts +0 -42
- package/src/participants.ts +0 -14
- package/src/payment.ts +0 -573
- package/src/profit.ts +0 -55
- package/src/sort.ts +0 -49
- package/src/stats.ts +0 -10
- package/src/time.ts +0 -123
- package/src/tokens.ts +0 -159
- package/src/units.ts +0 -120
- package/src/zkp2p-types.ts +0 -55
- package/tsconfig.json +0 -18
- /package/dist/{scripts/convert-svg.d.ts → chain.test.d.ts} +0 -0
- /package/dist/{src/deposit.js → contracts.test.d.ts} +0 -0
- /package/dist/{src/intent.js → conversionRates.test.d.ts} +0 -0
- /package/dist/{src/zkp2p-types.js → currencies.test.d.ts} +0 -0
- /package/dist/{src/flags.d.ts → flags.d.ts} +0 -0
- /package/dist/{src/flags.js → flags.js} +0 -0
package/dist/events.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Event type definitions for the Provex indexer.
|
|
3
|
+
*
|
|
4
|
+
* This module defines all blockchain event types in their display order.
|
|
5
|
+
* The order determines how events are sorted in the UI when viewing
|
|
6
|
+
* deposit history - deposit creation events come first, followed by
|
|
7
|
+
* intent events, then close/withdrawal events.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* The eventOrderId map assigns each event a numeric ID used in orderId
|
|
11
|
+
* serialization for consistent cross-chain ordering.
|
|
12
|
+
*
|
|
13
|
+
* @see {@link ids} for orderId serialization
|
|
14
|
+
* @see {@link indexer} for event handlers
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Ordered list of all event types supported by the indexer.
|
|
18
|
+
* The order determines the display sort priority in the UI.
|
|
19
|
+
*
|
|
20
|
+
* Event categories:
|
|
21
|
+
* 1. Deposit open events (creation, configuration)
|
|
22
|
+
* 2. Intent lifecycle events (signal, lock, verify, fulfill)
|
|
23
|
+
* 3. Deposit close events (withdrawal, close)
|
|
24
|
+
* 4. Protocol configuration events (dust, fees, registries)
|
|
25
|
+
*/
|
|
26
|
+
export const eventOrder = [
|
|
27
|
+
// deposit open events
|
|
28
|
+
'deposit_received',
|
|
29
|
+
'deposit_payment_method_added',
|
|
30
|
+
'deposit_payment_method_active_updated',
|
|
31
|
+
'deposit_currency_added',
|
|
32
|
+
'deposit_delegate_set',
|
|
33
|
+
'deposit_delegate_removed',
|
|
34
|
+
// configuration events (these happen before funds_added in deposit creation)
|
|
35
|
+
'deposit_conversion_rate_updated',
|
|
36
|
+
'deposit_accepting_intents_updated',
|
|
37
|
+
'deposit_intent_amount_range_updated',
|
|
38
|
+
// funds added comes after configuration so it appears first in UI (newest-first)
|
|
39
|
+
'deposit_funds_added',
|
|
40
|
+
// intent events
|
|
41
|
+
'intent_signaled',
|
|
42
|
+
'funds_locked',
|
|
43
|
+
'payment_verified',
|
|
44
|
+
'funds_unlocked_and_transferred',
|
|
45
|
+
'intent_fulfilled',
|
|
46
|
+
'intent_pruned',
|
|
47
|
+
'funds_unlocked',
|
|
48
|
+
'intent_expiry_extended',
|
|
49
|
+
// deposit close events
|
|
50
|
+
'funds_removed', // synthetic UI event for partial withdrawal (remaining > 0)
|
|
51
|
+
'deposit_withdrawn', // full withdrawal that closes the deposit (remaining = 0)
|
|
52
|
+
'deposit_closed',
|
|
53
|
+
// dust events
|
|
54
|
+
'dust_collected',
|
|
55
|
+
'dust_recipient_updated',
|
|
56
|
+
'dust_threshold_updated',
|
|
57
|
+
// deposit config events
|
|
58
|
+
'deposit_retain_on_empty_updated',
|
|
59
|
+
'payment_method_currency_added',
|
|
60
|
+
'payment_method_currency_removed',
|
|
61
|
+
// registry events
|
|
62
|
+
'payment_method_added',
|
|
63
|
+
'payment_method_removed',
|
|
64
|
+
'allow_multiple_intents_updated',
|
|
65
|
+
'escrow_registry_updated',
|
|
66
|
+
'post_intent_hook_registry_updated',
|
|
67
|
+
'protocol_fee_recipient_updated',
|
|
68
|
+
'protocol_fee_updated',
|
|
69
|
+
'relayer_registry_updated',
|
|
70
|
+
'unpaused',
|
|
71
|
+
'paused',
|
|
72
|
+
// V3 global config events
|
|
73
|
+
'intent_expiration_period_updated',
|
|
74
|
+
'max_intents_per_deposit_updated',
|
|
75
|
+
'min_deposit_amount_set',
|
|
76
|
+
'orchestrator_updated',
|
|
77
|
+
'ownership_transferred',
|
|
78
|
+
'payment_verifier_registry_updated',
|
|
79
|
+
// V2 verifier events
|
|
80
|
+
'provider_hash_added',
|
|
81
|
+
'provider_hash_removed',
|
|
82
|
+
];
|
|
83
|
+
/**
|
|
84
|
+
* Map from event type name to its numeric order ID.
|
|
85
|
+
* Used in orderId serialization to maintain consistent ordering.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* const id = eventOrderId.get('intent_signaled') // 9
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export const eventOrderId = new Map(eventOrder.map((event, index) => [event, index]));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { eventOrder, eventOrderId } from './events.js';
|
|
4
|
+
describe('events', () => {
|
|
5
|
+
describe('eventOrder', () => {
|
|
6
|
+
it('is a non-empty array', () => {
|
|
7
|
+
assert.ok(Array.isArray(eventOrder));
|
|
8
|
+
assert.ok(eventOrder.length > 0);
|
|
9
|
+
});
|
|
10
|
+
it('contains deposit lifecycle events', () => {
|
|
11
|
+
assert.ok(eventOrder.includes('deposit_received'));
|
|
12
|
+
assert.ok(eventOrder.includes('deposit_withdrawn'));
|
|
13
|
+
assert.ok(eventOrder.includes('deposit_closed'));
|
|
14
|
+
});
|
|
15
|
+
it('contains intent lifecycle events', () => {
|
|
16
|
+
assert.ok(eventOrder.includes('intent_signaled'));
|
|
17
|
+
assert.ok(eventOrder.includes('intent_fulfilled'));
|
|
18
|
+
assert.ok(eventOrder.includes('intent_pruned'));
|
|
19
|
+
});
|
|
20
|
+
it('contains V3-specific events', () => {
|
|
21
|
+
assert.ok(eventOrder.includes('funds_locked'));
|
|
22
|
+
assert.ok(eventOrder.includes('funds_unlocked'));
|
|
23
|
+
assert.ok(eventOrder.includes('funds_unlocked_and_transferred'));
|
|
24
|
+
assert.ok(eventOrder.includes('payment_verified'));
|
|
25
|
+
assert.ok(eventOrder.includes('intent_expiry_extended'));
|
|
26
|
+
assert.ok(eventOrder.includes('deposit_payment_method_active_updated'));
|
|
27
|
+
});
|
|
28
|
+
it('contains V3 global config events', () => {
|
|
29
|
+
assert.ok(eventOrder.includes('intent_expiration_period_updated'));
|
|
30
|
+
assert.ok(eventOrder.includes('max_intents_per_deposit_updated'));
|
|
31
|
+
assert.ok(eventOrder.includes('min_deposit_amount_set'));
|
|
32
|
+
assert.ok(eventOrder.includes('orchestrator_updated'));
|
|
33
|
+
assert.ok(eventOrder.includes('payment_verifier_registry_updated'));
|
|
34
|
+
});
|
|
35
|
+
it('has no duplicate events', () => {
|
|
36
|
+
const uniqueEvents = new Set(eventOrder);
|
|
37
|
+
assert.strictEqual(uniqueEvents.size, eventOrder.length);
|
|
38
|
+
});
|
|
39
|
+
it('orders deposit events before intent events', () => {
|
|
40
|
+
const depositReceivedIdx = eventOrder.indexOf('deposit_received');
|
|
41
|
+
const intentSignaledIdx = eventOrder.indexOf('intent_signaled');
|
|
42
|
+
assert.ok(depositReceivedIdx < intentSignaledIdx);
|
|
43
|
+
});
|
|
44
|
+
it('orders intent_signaled before intent_fulfilled', () => {
|
|
45
|
+
const signaledIdx = eventOrder.indexOf('intent_signaled');
|
|
46
|
+
const fulfilledIdx = eventOrder.indexOf('intent_fulfilled');
|
|
47
|
+
assert.ok(signaledIdx < fulfilledIdx);
|
|
48
|
+
});
|
|
49
|
+
it('orders funds_locked before funds_unlocked', () => {
|
|
50
|
+
const lockedIdx = eventOrder.indexOf('funds_locked');
|
|
51
|
+
const unlockedIdx = eventOrder.indexOf('funds_unlocked');
|
|
52
|
+
assert.ok(lockedIdx < unlockedIdx);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('eventOrderId', () => {
|
|
56
|
+
it('is a Map', () => {
|
|
57
|
+
assert.ok(eventOrderId instanceof Map);
|
|
58
|
+
});
|
|
59
|
+
it('has same size as eventOrder', () => {
|
|
60
|
+
assert.strictEqual(eventOrderId.size, eventOrder.length);
|
|
61
|
+
});
|
|
62
|
+
it('maps each event to its index', () => {
|
|
63
|
+
for (let i = 0; i < eventOrder.length; i++) {
|
|
64
|
+
const event = eventOrder[i];
|
|
65
|
+
assert.strictEqual(eventOrderId.get(event), i);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
it('returns correct index for deposit_received', () => {
|
|
69
|
+
const idx = eventOrderId.get('deposit_received');
|
|
70
|
+
assert.strictEqual(idx, 0);
|
|
71
|
+
});
|
|
72
|
+
it('returns correct index for intent_signaled', () => {
|
|
73
|
+
const idx = eventOrderId.get('intent_signaled');
|
|
74
|
+
assert.strictEqual(idx, eventOrder.indexOf('intent_signaled'));
|
|
75
|
+
});
|
|
76
|
+
it('returns undefined for invalid event', () => {
|
|
77
|
+
// @ts-expect-error - Testing runtime behavior with invalid event
|
|
78
|
+
const idx = eventOrderId.get('invalid_event');
|
|
79
|
+
assert.strictEqual(idx, undefined);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('EventType', () => {
|
|
83
|
+
it('type-checks valid events', () => {
|
|
84
|
+
// These should compile without error
|
|
85
|
+
const event1 = 'deposit_received';
|
|
86
|
+
const event2 = 'intent_signaled';
|
|
87
|
+
const event3 = 'funds_locked';
|
|
88
|
+
assert.ok(event1);
|
|
89
|
+
assert.ok(event2);
|
|
90
|
+
assert.ok(event3);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
describe('event ordering consistency', () => {
|
|
94
|
+
it('maintains consistent ordering for UI display', () => {
|
|
95
|
+
// Events in same transaction should sort consistently
|
|
96
|
+
// Lower index = appears first in time-ordered list (oldest first)
|
|
97
|
+
// Higher index = appears later
|
|
98
|
+
// Deposit setup should come before funds added
|
|
99
|
+
const depositReceivedIdx = eventOrderId.get('deposit_received');
|
|
100
|
+
const fundsAddedIdx = eventOrderId.get('deposit_funds_added');
|
|
101
|
+
assert.ok(depositReceivedIdx < fundsAddedIdx);
|
|
102
|
+
// Intent signaled should come before payment verified
|
|
103
|
+
const intentSignaledIdx = eventOrderId.get('intent_signaled');
|
|
104
|
+
const paymentVerifiedIdx = eventOrderId.get('payment_verified');
|
|
105
|
+
assert.ok(intentSignaledIdx < paymentVerifiedIdx);
|
|
106
|
+
// Payment verified should come before intent fulfilled
|
|
107
|
+
const intentFulfilledIdx = eventOrderId.get('intent_fulfilled');
|
|
108
|
+
assert.ok(paymentVerifiedIdx < intentFulfilledIdx);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
package/dist/fees.d.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fee calculation utilities for Provex protocol.
|
|
3
|
+
*
|
|
4
|
+
* Fee Precision Formats:
|
|
5
|
+
* - Basis points (bps): 10000 = 100%, 100 = 1%, 1 = 0.01%
|
|
6
|
+
* - 18 decimals: 1e18 = 100% (like Solidity's ether units)
|
|
7
|
+
*
|
|
8
|
+
* The protocol uses basis points for most fee calculations.
|
|
9
|
+
* Some contracts (Pulsechain V3) use 18-decimal precision which must
|
|
10
|
+
* be normalized to basis points before use.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // Calculate 2% fee on 100 USDC
|
|
14
|
+
* const fee = calculateFeeAmount({ amount: 100_000000n, feeBps: 200n })
|
|
15
|
+
* // fee = 2_000000n (2 USDC)
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Maximum basis points value representing 100%.
|
|
19
|
+
* Used as the denominator in fee calculations.
|
|
20
|
+
*/
|
|
21
|
+
export declare const BPS_DENOMINATOR = 10000n;
|
|
22
|
+
/**
|
|
23
|
+
* 18-decimal representation of 100%.
|
|
24
|
+
* Used to detect and normalize 18-decimal fee values.
|
|
25
|
+
*/
|
|
26
|
+
export declare const EIGHTEEN_DECIMAL_100_PERCENT: bigint;
|
|
27
|
+
/**
|
|
28
|
+
* Precision format for fee values.
|
|
29
|
+
* - 'bps': Basis points (10000 = 100%)
|
|
30
|
+
* - '18decimals': 18-decimal precision (1e18 = 100%)
|
|
31
|
+
*/
|
|
32
|
+
export type FeePrecision = 'bps' | '18decimals';
|
|
33
|
+
/**
|
|
34
|
+
* Result of fee precision detection.
|
|
35
|
+
*/
|
|
36
|
+
export interface FeeInfo {
|
|
37
|
+
/** Human-readable percentage string (e.g., "2%", "0.50%") */
|
|
38
|
+
percentage: string;
|
|
39
|
+
/** Detected precision format */
|
|
40
|
+
precision: FeePrecision;
|
|
41
|
+
/** Fee value normalized to basis points */
|
|
42
|
+
normalizedBps: bigint;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Calculate the fee amount for a given transaction.
|
|
46
|
+
*
|
|
47
|
+
* @param params.amount - Transaction amount in smallest unit (e.g., 6 decimals for USDC)
|
|
48
|
+
* @param params.feeBps - Fee in basis points (200 = 2%)
|
|
49
|
+
* @returns Fee amount in the same units as the input amount
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* // 2% fee on 100 USDC (6 decimals)
|
|
53
|
+
* calculateFeeAmount({ amount: 100_000000n, feeBps: 200n })
|
|
54
|
+
* // Returns: 2_000000n (2 USDC)
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // 0.5% fee on 1000 USDC
|
|
58
|
+
* calculateFeeAmount({ amount: 1000_000000n, feeBps: 50n })
|
|
59
|
+
* // Returns: 5_000000n (5 USDC)
|
|
60
|
+
*/
|
|
61
|
+
export declare function calculateFeeAmount(params: {
|
|
62
|
+
amount: bigint;
|
|
63
|
+
feeBps: bigint;
|
|
64
|
+
}): bigint;
|
|
65
|
+
/**
|
|
66
|
+
* Calculate the net amount after deducting fees.
|
|
67
|
+
*
|
|
68
|
+
* @param params.grossAmount - Original amount before fees
|
|
69
|
+
* @param params.feeBps - Fee in basis points (200 = 2%)
|
|
70
|
+
* @returns Amount remaining after fee deduction
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* // Net amount after 2% fee on 100 USDC
|
|
74
|
+
* calculateNetAmount({ grossAmount: 100_000000n, feeBps: 200n })
|
|
75
|
+
* // Returns: 98_000000n (98 USDC)
|
|
76
|
+
*/
|
|
77
|
+
export declare function calculateNetAmount(params: {
|
|
78
|
+
grossAmount: bigint;
|
|
79
|
+
feeBps: bigint;
|
|
80
|
+
}): bigint;
|
|
81
|
+
/**
|
|
82
|
+
* Format a basis points value as a human-readable percentage string.
|
|
83
|
+
*
|
|
84
|
+
* @param feeBps - Fee in basis points
|
|
85
|
+
* @returns Formatted percentage string (e.g., "2%", "0.50%")
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* formatFeeBps(200n) // "2%"
|
|
89
|
+
* formatFeeBps(50n) // "0.50%"
|
|
90
|
+
* formatFeeBps(0n) // "0%"
|
|
91
|
+
* formatFeeBps(10000n) // "100%"
|
|
92
|
+
* formatFeeBps(1n) // "0.01%"
|
|
93
|
+
*/
|
|
94
|
+
export declare function formatFeeBps(feeBps: bigint): string;
|
|
95
|
+
/**
|
|
96
|
+
* Detect the precision of a raw fee value and normalize to basis points.
|
|
97
|
+
*
|
|
98
|
+
* Contracts may use different precisions:
|
|
99
|
+
* - Basis points: 10000 = 100% (most common)
|
|
100
|
+
* - 18 decimals: 1e18 = 100% (like ether, used by some V3 contracts)
|
|
101
|
+
*
|
|
102
|
+
* Heuristic: If value > 1,000,000, assume 18-decimal precision.
|
|
103
|
+
*
|
|
104
|
+
* @param rawFee - Raw fee value from contract
|
|
105
|
+
* @returns Normalized fee information with bps value and detected precision
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* // Basis points input
|
|
109
|
+
* normalizeFeePrecision(200n)
|
|
110
|
+
* // Returns: { percentage: "2%", precision: "bps", normalizedBps: 200n }
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* // 18-decimal input (2%)
|
|
114
|
+
* normalizeFeePrecision(20000000000000000n)
|
|
115
|
+
* // Returns: { percentage: "2%", precision: "18decimals", normalizedBps: 200n }
|
|
116
|
+
*/
|
|
117
|
+
export declare function normalizeFeePrecision(rawFee: bigint): FeeInfo;
|
|
118
|
+
/**
|
|
119
|
+
* Calculate total fees from multiple fee sources.
|
|
120
|
+
*
|
|
121
|
+
* @param fees - Array of fee amounts (all in same precision)
|
|
122
|
+
* @returns Total fee amount
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* // Protocol fee + referrer fee
|
|
126
|
+
* calculateTotalFees([2_000000n, 500000n])
|
|
127
|
+
* // Returns: 2_500000n
|
|
128
|
+
*/
|
|
129
|
+
export declare function calculateTotalFees(fees: bigint[]): bigint;
|
|
130
|
+
/**
|
|
131
|
+
* Check if a fee value represents a zero/no fee.
|
|
132
|
+
*
|
|
133
|
+
* @param feeBps - Fee in basis points
|
|
134
|
+
* @returns true if fee is zero
|
|
135
|
+
*/
|
|
136
|
+
export declare function isZeroFee(feeBps: bigint): boolean;
|
package/dist/fees.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fee calculation utilities for Provex protocol.
|
|
3
|
+
*
|
|
4
|
+
* Fee Precision Formats:
|
|
5
|
+
* - Basis points (bps): 10000 = 100%, 100 = 1%, 1 = 0.01%
|
|
6
|
+
* - 18 decimals: 1e18 = 100% (like Solidity's ether units)
|
|
7
|
+
*
|
|
8
|
+
* The protocol uses basis points for most fee calculations.
|
|
9
|
+
* Some contracts (Pulsechain V3) use 18-decimal precision which must
|
|
10
|
+
* be normalized to basis points before use.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // Calculate 2% fee on 100 USDC
|
|
14
|
+
* const fee = calculateFeeAmount({ amount: 100_000000n, feeBps: 200n })
|
|
15
|
+
* // fee = 2_000000n (2 USDC)
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Maximum basis points value representing 100%.
|
|
19
|
+
* Used as the denominator in fee calculations.
|
|
20
|
+
*/
|
|
21
|
+
export const BPS_DENOMINATOR = 10000n;
|
|
22
|
+
/**
|
|
23
|
+
* 18-decimal representation of 100%.
|
|
24
|
+
* Used to detect and normalize 18-decimal fee values.
|
|
25
|
+
*/
|
|
26
|
+
export const EIGHTEEN_DECIMAL_100_PERCENT = 10n ** 18n;
|
|
27
|
+
/**
|
|
28
|
+
* Threshold to distinguish basis points from 18-decimal values.
|
|
29
|
+
* If a raw fee value exceeds this, it's likely using 18-decimal precision.
|
|
30
|
+
*/
|
|
31
|
+
const PRECISION_DETECTION_THRESHOLD = 1000000n;
|
|
32
|
+
/**
|
|
33
|
+
* Calculate the fee amount for a given transaction.
|
|
34
|
+
*
|
|
35
|
+
* @param params.amount - Transaction amount in smallest unit (e.g., 6 decimals for USDC)
|
|
36
|
+
* @param params.feeBps - Fee in basis points (200 = 2%)
|
|
37
|
+
* @returns Fee amount in the same units as the input amount
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // 2% fee on 100 USDC (6 decimals)
|
|
41
|
+
* calculateFeeAmount({ amount: 100_000000n, feeBps: 200n })
|
|
42
|
+
* // Returns: 2_000000n (2 USDC)
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // 0.5% fee on 1000 USDC
|
|
46
|
+
* calculateFeeAmount({ amount: 1000_000000n, feeBps: 50n })
|
|
47
|
+
* // Returns: 5_000000n (5 USDC)
|
|
48
|
+
*/
|
|
49
|
+
export function calculateFeeAmount(params) {
|
|
50
|
+
const { amount, feeBps } = params;
|
|
51
|
+
return (amount * feeBps) / BPS_DENOMINATOR;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Calculate the net amount after deducting fees.
|
|
55
|
+
*
|
|
56
|
+
* @param params.grossAmount - Original amount before fees
|
|
57
|
+
* @param params.feeBps - Fee in basis points (200 = 2%)
|
|
58
|
+
* @returns Amount remaining after fee deduction
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* // Net amount after 2% fee on 100 USDC
|
|
62
|
+
* calculateNetAmount({ grossAmount: 100_000000n, feeBps: 200n })
|
|
63
|
+
* // Returns: 98_000000n (98 USDC)
|
|
64
|
+
*/
|
|
65
|
+
export function calculateNetAmount(params) {
|
|
66
|
+
const { grossAmount, feeBps } = params;
|
|
67
|
+
const feeAmount = calculateFeeAmount({ amount: grossAmount, feeBps });
|
|
68
|
+
return grossAmount - feeAmount;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Format a basis points value as a human-readable percentage string.
|
|
72
|
+
*
|
|
73
|
+
* @param feeBps - Fee in basis points
|
|
74
|
+
* @returns Formatted percentage string (e.g., "2%", "0.50%")
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* formatFeeBps(200n) // "2%"
|
|
78
|
+
* formatFeeBps(50n) // "0.50%"
|
|
79
|
+
* formatFeeBps(0n) // "0%"
|
|
80
|
+
* formatFeeBps(10000n) // "100%"
|
|
81
|
+
* formatFeeBps(1n) // "0.01%"
|
|
82
|
+
*/
|
|
83
|
+
export function formatFeeBps(feeBps) {
|
|
84
|
+
// Convert bps to percentage: bps / 100
|
|
85
|
+
// We use number for display since percentages won't exceed Number.MAX_SAFE_INTEGER
|
|
86
|
+
const percentage = Number(feeBps) / 100;
|
|
87
|
+
// Format with appropriate decimal places
|
|
88
|
+
if (percentage === 0) {
|
|
89
|
+
return '0%';
|
|
90
|
+
}
|
|
91
|
+
// If it's a whole number, show without decimals
|
|
92
|
+
if (Number.isInteger(percentage)) {
|
|
93
|
+
return `${percentage}%`;
|
|
94
|
+
}
|
|
95
|
+
// Otherwise show 2 decimal places, removing trailing zeros
|
|
96
|
+
const formatted = percentage.toFixed(2).replace(/\.?0+$/, '');
|
|
97
|
+
return `${formatted}%`;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Detect the precision of a raw fee value and normalize to basis points.
|
|
101
|
+
*
|
|
102
|
+
* Contracts may use different precisions:
|
|
103
|
+
* - Basis points: 10000 = 100% (most common)
|
|
104
|
+
* - 18 decimals: 1e18 = 100% (like ether, used by some V3 contracts)
|
|
105
|
+
*
|
|
106
|
+
* Heuristic: If value > 1,000,000, assume 18-decimal precision.
|
|
107
|
+
*
|
|
108
|
+
* @param rawFee - Raw fee value from contract
|
|
109
|
+
* @returns Normalized fee information with bps value and detected precision
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* // Basis points input
|
|
113
|
+
* normalizeFeePrecision(200n)
|
|
114
|
+
* // Returns: { percentage: "2%", precision: "bps", normalizedBps: 200n }
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* // 18-decimal input (2%)
|
|
118
|
+
* normalizeFeePrecision(20000000000000000n)
|
|
119
|
+
* // Returns: { percentage: "2%", precision: "18decimals", normalizedBps: 200n }
|
|
120
|
+
*/
|
|
121
|
+
export function normalizeFeePrecision(rawFee) {
|
|
122
|
+
// Detect 18-decimal precision by checking if value is very large
|
|
123
|
+
if (rawFee > PRECISION_DETECTION_THRESHOLD) {
|
|
124
|
+
// 18-decimal precision: 1e18 = 100%
|
|
125
|
+
// Convert to percentage for display
|
|
126
|
+
const percentage = Number(rawFee) / Number(EIGHTEEN_DECIMAL_100_PERCENT) * 100;
|
|
127
|
+
// Normalize to basis points: (rawFee * 10000) / 1e18
|
|
128
|
+
const normalizedBps = (rawFee * BPS_DENOMINATOR) / EIGHTEEN_DECIMAL_100_PERCENT;
|
|
129
|
+
return {
|
|
130
|
+
percentage: formatFromNumber(percentage),
|
|
131
|
+
precision: '18decimals',
|
|
132
|
+
normalizedBps,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// Already in basis points
|
|
136
|
+
return {
|
|
137
|
+
percentage: formatFeeBps(rawFee),
|
|
138
|
+
precision: 'bps',
|
|
139
|
+
normalizedBps: rawFee,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Helper to format a number as a percentage string.
|
|
144
|
+
* @internal
|
|
145
|
+
*/
|
|
146
|
+
function formatFromNumber(percentage) {
|
|
147
|
+
if (percentage === 0) {
|
|
148
|
+
return '0%';
|
|
149
|
+
}
|
|
150
|
+
if (Number.isInteger(percentage)) {
|
|
151
|
+
return `${percentage}%`;
|
|
152
|
+
}
|
|
153
|
+
const formatted = percentage.toFixed(2).replace(/\.?0+$/, '');
|
|
154
|
+
return `${formatted}%`;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Calculate total fees from multiple fee sources.
|
|
158
|
+
*
|
|
159
|
+
* @param fees - Array of fee amounts (all in same precision)
|
|
160
|
+
* @returns Total fee amount
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* // Protocol fee + referrer fee
|
|
164
|
+
* calculateTotalFees([2_000000n, 500000n])
|
|
165
|
+
* // Returns: 2_500000n
|
|
166
|
+
*/
|
|
167
|
+
export function calculateTotalFees(fees) {
|
|
168
|
+
return fees.reduce((total, fee) => total + fee, 0n);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Check if a fee value represents a zero/no fee.
|
|
172
|
+
*
|
|
173
|
+
* @param feeBps - Fee in basis points
|
|
174
|
+
* @returns true if fee is zero
|
|
175
|
+
*/
|
|
176
|
+
export function isZeroFee(feeBps) {
|
|
177
|
+
return feeBps === 0n;
|
|
178
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { BPS_DENOMINATOR, EIGHTEEN_DECIMAL_100_PERCENT, calculateFeeAmount, calculateNetAmount, formatFeeBps, normalizeFeePrecision, calculateTotalFees, isZeroFee, } from './fees.js';
|
|
4
|
+
describe('BPS_DENOMINATOR', () => {
|
|
5
|
+
it('should equal 10000', () => {
|
|
6
|
+
assert.equal(BPS_DENOMINATOR, 10000n);
|
|
7
|
+
});
|
|
8
|
+
});
|
|
9
|
+
describe('EIGHTEEN_DECIMAL_100_PERCENT', () => {
|
|
10
|
+
it('should equal 10^18', () => {
|
|
11
|
+
assert.equal(EIGHTEEN_DECIMAL_100_PERCENT, 10n ** 18n);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
describe('calculateFeeAmount', () => {
|
|
15
|
+
it('should calculate 2% fee correctly', () => {
|
|
16
|
+
// 2% = 200 bps, 100 USDC (6 decimals) = 100_000000
|
|
17
|
+
const result = calculateFeeAmount({ amount: 100000000n, feeBps: 200n });
|
|
18
|
+
assert.equal(result, 2000000n); // 2 USDC
|
|
19
|
+
});
|
|
20
|
+
it('should calculate 0.5% fee correctly', () => {
|
|
21
|
+
// 0.5% = 50 bps
|
|
22
|
+
const result = calculateFeeAmount({ amount: 1000000000n, feeBps: 50n });
|
|
23
|
+
assert.equal(result, 5000000n); // 5 USDC
|
|
24
|
+
});
|
|
25
|
+
it('should return 0 for 0% fee', () => {
|
|
26
|
+
const result = calculateFeeAmount({ amount: 100000000n, feeBps: 0n });
|
|
27
|
+
assert.equal(result, 0n);
|
|
28
|
+
});
|
|
29
|
+
it('should handle 100% fee', () => {
|
|
30
|
+
const result = calculateFeeAmount({ amount: 100000000n, feeBps: 10000n });
|
|
31
|
+
assert.equal(result, 100000000n);
|
|
32
|
+
});
|
|
33
|
+
it('should handle 0 amount', () => {
|
|
34
|
+
const result = calculateFeeAmount({ amount: 0n, feeBps: 200n });
|
|
35
|
+
assert.equal(result, 0n);
|
|
36
|
+
});
|
|
37
|
+
it('should handle small amounts with integer division', () => {
|
|
38
|
+
// 1 USDC with 2% fee = 0.02 USDC, but integer division truncates
|
|
39
|
+
const result = calculateFeeAmount({ amount: 1000000n, feeBps: 200n });
|
|
40
|
+
assert.equal(result, 20000n); // 0.02 USDC
|
|
41
|
+
});
|
|
42
|
+
it('should handle very small amounts that round to zero', () => {
|
|
43
|
+
// 0.0001 USDC with 0.01% fee
|
|
44
|
+
const result = calculateFeeAmount({ amount: 100n, feeBps: 1n });
|
|
45
|
+
assert.equal(result, 0n); // Rounds down to 0
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('calculateNetAmount', () => {
|
|
49
|
+
it('should calculate net amount after 2% fee', () => {
|
|
50
|
+
const result = calculateNetAmount({ grossAmount: 100000000n, feeBps: 200n });
|
|
51
|
+
assert.equal(result, 98000000n); // 98 USDC
|
|
52
|
+
});
|
|
53
|
+
it('should return full amount for 0% fee', () => {
|
|
54
|
+
const result = calculateNetAmount({ grossAmount: 100000000n, feeBps: 0n });
|
|
55
|
+
assert.equal(result, 100000000n);
|
|
56
|
+
});
|
|
57
|
+
it('should return 0 for 100% fee', () => {
|
|
58
|
+
const result = calculateNetAmount({ grossAmount: 100000000n, feeBps: 10000n });
|
|
59
|
+
assert.equal(result, 0n);
|
|
60
|
+
});
|
|
61
|
+
it('should handle 0.5% fee', () => {
|
|
62
|
+
const result = calculateNetAmount({ grossAmount: 1000000000n, feeBps: 50n });
|
|
63
|
+
assert.equal(result, 995000000n); // 995 USDC
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe('formatFeeBps', () => {
|
|
67
|
+
it('should format 0% correctly', () => {
|
|
68
|
+
assert.equal(formatFeeBps(0n), '0%');
|
|
69
|
+
});
|
|
70
|
+
it('should format whole percentages without decimals', () => {
|
|
71
|
+
assert.equal(formatFeeBps(100n), '1%');
|
|
72
|
+
assert.equal(formatFeeBps(200n), '2%');
|
|
73
|
+
assert.equal(formatFeeBps(500n), '5%');
|
|
74
|
+
assert.equal(formatFeeBps(1000n), '10%');
|
|
75
|
+
assert.equal(formatFeeBps(10000n), '100%');
|
|
76
|
+
});
|
|
77
|
+
it('should format fractional percentages with decimals', () => {
|
|
78
|
+
assert.equal(formatFeeBps(50n), '0.5%');
|
|
79
|
+
assert.equal(formatFeeBps(1n), '0.01%');
|
|
80
|
+
assert.equal(formatFeeBps(25n), '0.25%');
|
|
81
|
+
assert.equal(formatFeeBps(150n), '1.5%');
|
|
82
|
+
});
|
|
83
|
+
it('should remove trailing zeros', () => {
|
|
84
|
+
assert.equal(formatFeeBps(250n), '2.5%');
|
|
85
|
+
assert.equal(formatFeeBps(300n), '3%');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
describe('normalizeFeePrecision', () => {
|
|
89
|
+
describe('basis points input', () => {
|
|
90
|
+
it('should detect and normalize 0 bps', () => {
|
|
91
|
+
const result = normalizeFeePrecision(0n);
|
|
92
|
+
assert.equal(result.precision, 'bps');
|
|
93
|
+
assert.equal(result.normalizedBps, 0n);
|
|
94
|
+
assert.equal(result.percentage, '0%');
|
|
95
|
+
});
|
|
96
|
+
it('should detect and normalize 200 bps (2%)', () => {
|
|
97
|
+
const result = normalizeFeePrecision(200n);
|
|
98
|
+
assert.equal(result.precision, 'bps');
|
|
99
|
+
assert.equal(result.normalizedBps, 200n);
|
|
100
|
+
assert.equal(result.percentage, '2%');
|
|
101
|
+
});
|
|
102
|
+
it('should detect and normalize small bps values', () => {
|
|
103
|
+
const result = normalizeFeePrecision(50n);
|
|
104
|
+
assert.equal(result.precision, 'bps');
|
|
105
|
+
assert.equal(result.normalizedBps, 50n);
|
|
106
|
+
assert.equal(result.percentage, '0.5%');
|
|
107
|
+
});
|
|
108
|
+
it('should detect and normalize 10000 bps (100%)', () => {
|
|
109
|
+
const result = normalizeFeePrecision(10000n);
|
|
110
|
+
assert.equal(result.precision, 'bps');
|
|
111
|
+
assert.equal(result.normalizedBps, 10000n);
|
|
112
|
+
assert.equal(result.percentage, '100%');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
describe('18-decimal input', () => {
|
|
116
|
+
it('should detect and normalize 18-decimal 2%', () => {
|
|
117
|
+
// 2% in 18 decimals = 0.02 * 1e18 = 2e16
|
|
118
|
+
const rawFee = 20000000000000000n;
|
|
119
|
+
const result = normalizeFeePrecision(rawFee);
|
|
120
|
+
assert.equal(result.precision, '18decimals');
|
|
121
|
+
assert.equal(result.normalizedBps, 200n);
|
|
122
|
+
assert.equal(result.percentage, '2%');
|
|
123
|
+
});
|
|
124
|
+
it('should detect and normalize 18-decimal 0.5%', () => {
|
|
125
|
+
// 0.5% in 18 decimals = 0.005 * 1e18 = 5e15
|
|
126
|
+
const rawFee = 5000000000000000n;
|
|
127
|
+
const result = normalizeFeePrecision(rawFee);
|
|
128
|
+
assert.equal(result.precision, '18decimals');
|
|
129
|
+
assert.equal(result.normalizedBps, 50n);
|
|
130
|
+
assert.equal(result.percentage, '0.5%');
|
|
131
|
+
});
|
|
132
|
+
it('should detect and normalize 18-decimal 100%', () => {
|
|
133
|
+
// 100% in 18 decimals = 1e18
|
|
134
|
+
const rawFee = 1000000000000000000n;
|
|
135
|
+
const result = normalizeFeePrecision(rawFee);
|
|
136
|
+
assert.equal(result.precision, '18decimals');
|
|
137
|
+
assert.equal(result.normalizedBps, 10000n);
|
|
138
|
+
assert.equal(result.percentage, '100%');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
describe('calculateTotalFees', () => {
|
|
143
|
+
it('should sum multiple fees', () => {
|
|
144
|
+
const result = calculateTotalFees([2000000n, 500000n, 100000n]);
|
|
145
|
+
assert.equal(result, 2600000n);
|
|
146
|
+
});
|
|
147
|
+
it('should return 0 for empty array', () => {
|
|
148
|
+
const result = calculateTotalFees([]);
|
|
149
|
+
assert.equal(result, 0n);
|
|
150
|
+
});
|
|
151
|
+
it('should return single fee for array with one element', () => {
|
|
152
|
+
const result = calculateTotalFees([5000000n]);
|
|
153
|
+
assert.equal(result, 5000000n);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
describe('isZeroFee', () => {
|
|
157
|
+
it('should return true for 0 bps', () => {
|
|
158
|
+
assert.equal(isZeroFee(0n), true);
|
|
159
|
+
});
|
|
160
|
+
it('should return false for non-zero bps', () => {
|
|
161
|
+
assert.equal(isZeroFee(1n), false);
|
|
162
|
+
assert.equal(isZeroFee(100n), false);
|
|
163
|
+
assert.equal(isZeroFee(200n), false);
|
|
164
|
+
});
|
|
165
|
+
});
|