@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.
Files changed (149) hide show
  1. package/dist/chain.d.ts +446 -0
  2. package/dist/chain.js +98 -0
  3. package/dist/chain.test.js +112 -0
  4. package/dist/contracts.d.ts +206 -0
  5. package/dist/contracts.js +292 -0
  6. package/dist/contracts.test.js +243 -0
  7. package/dist/conversionRates.d.ts +215 -0
  8. package/dist/{src/conversionRates.js → conversionRates.js} +74 -4
  9. package/dist/conversionRates.test.js +473 -0
  10. package/dist/currencies.d.ts +248 -0
  11. package/dist/{src/currencies.js → currencies.js} +119 -38
  12. package/dist/currencies.test.js +343 -0
  13. package/dist/data/countries.json +1099 -0
  14. package/dist/deposit.d.ts +69 -0
  15. package/dist/deposit.js +52 -0
  16. package/dist/depositKeys.d.ts +154 -0
  17. package/dist/depositKeys.js +104 -0
  18. package/dist/depositKeys.test.d.ts +1 -0
  19. package/dist/depositKeys.test.js +231 -0
  20. package/dist/events.d.ts +41 -0
  21. package/dist/events.js +92 -0
  22. package/dist/events.test.d.ts +1 -0
  23. package/dist/events.test.js +111 -0
  24. package/dist/fees.d.ts +136 -0
  25. package/dist/fees.js +178 -0
  26. package/dist/fees.test.d.ts +1 -0
  27. package/dist/fees.test.js +165 -0
  28. package/dist/ids.d.ts +175 -0
  29. package/dist/ids.js +231 -0
  30. package/dist/ids.test.d.ts +1 -0
  31. package/dist/ids.test.js +607 -0
  32. package/dist/index.d.ts +45 -0
  33. package/dist/index.js +45 -0
  34. package/dist/input.d.ts +26 -0
  35. package/dist/{src/input.js → input.js} +23 -4
  36. package/dist/intent.d.ts +50 -0
  37. package/dist/intent.js +28 -0
  38. package/dist/intent.test.d.ts +1 -0
  39. package/dist/intent.test.js +119 -0
  40. package/dist/json.d.ts +55 -0
  41. package/dist/json.js +80 -0
  42. package/dist/participants.d.ts +22 -0
  43. package/dist/participants.js +28 -0
  44. package/dist/payment.d.ts +481 -0
  45. package/dist/payment.js +1149 -0
  46. package/dist/payment.test.d.ts +1 -0
  47. package/dist/payment.test.js +272 -0
  48. package/dist/peer-types.d.ts +327 -0
  49. package/dist/peer-types.js +169 -0
  50. package/dist/profit.d.ts +49 -0
  51. package/dist/profit.js +57 -0
  52. package/dist/profit.test.d.ts +1 -0
  53. package/dist/profit.test.js +315 -0
  54. package/dist/reputation.d.ts +125 -0
  55. package/dist/reputation.js +188 -0
  56. package/dist/reputation.test.d.ts +1 -0
  57. package/dist/reputation.test.js +156 -0
  58. package/dist/sort.d.ts +75 -0
  59. package/dist/sort.js +70 -0
  60. package/dist/sort.test.d.ts +1 -0
  61. package/dist/sort.test.js +216 -0
  62. package/dist/stateKey.d.ts +35 -0
  63. package/dist/stateKey.js +36 -0
  64. package/dist/stats.d.ts +61 -0
  65. package/dist/stats.js +53 -0
  66. package/dist/stats.test.d.ts +1 -0
  67. package/dist/stats.test.js +104 -0
  68. package/dist/time.d.ts +104 -0
  69. package/dist/time.js +162 -0
  70. package/dist/time.test.d.ts +1 -0
  71. package/dist/time.test.js +231 -0
  72. package/dist/tokens.d.ts +15502 -0
  73. package/dist/{src/tokens.js → tokens.js} +98 -17
  74. package/dist/tokens.test.d.ts +1 -0
  75. package/dist/tokens.test.js +193 -0
  76. package/dist/units.d.ts +200 -0
  77. package/dist/units.js +279 -0
  78. package/dist/units.test.d.ts +1 -0
  79. package/dist/units.test.js +327 -0
  80. package/package.json +119 -29
  81. package/dist/scripts/convert-svg.js +0 -17
  82. package/dist/src/chain.d.ts +0 -14
  83. package/dist/src/chain.js +0 -18
  84. package/dist/src/contracts.d.ts +0 -30
  85. package/dist/src/contracts.js +0 -64
  86. package/dist/src/conversionRates.d.ts +0 -124
  87. package/dist/src/currencies.d.ts +0 -144
  88. package/dist/src/data/countries.json +0 -1099
  89. package/dist/src/deposit.d.ts +0 -1
  90. package/dist/src/events.d.ts +0 -3
  91. package/dist/src/events.js +0 -16
  92. package/dist/src/ids.d.ts +0 -72
  93. package/dist/src/ids.js +0 -81
  94. package/dist/src/index.d.ts +0 -16
  95. package/dist/src/index.js +0 -16
  96. package/dist/src/input.d.ts +0 -7
  97. package/dist/src/intent.d.ts +0 -14
  98. package/dist/src/json.d.ts +0 -18
  99. package/dist/src/json.js +0 -44
  100. package/dist/src/participants.d.ts +0 -7
  101. package/dist/src/participants.js +0 -13
  102. package/dist/src/payment.d.ts +0 -164
  103. package/dist/src/payment.js +0 -439
  104. package/dist/src/profit.d.ts +0 -28
  105. package/dist/src/profit.js +0 -34
  106. package/dist/src/sort.d.ts +0 -36
  107. package/dist/src/sort.js +0 -39
  108. package/dist/src/stats.d.ts +0 -7
  109. package/dist/src/stats.js +0 -8
  110. package/dist/src/time.d.ts +0 -54
  111. package/dist/src/time.js +0 -106
  112. package/dist/src/tokens.d.ts +0 -3675
  113. package/dist/src/units.d.ts +0 -17
  114. package/dist/src/units.js +0 -79
  115. package/dist/src/zkp2p-types.d.ts +0 -48
  116. package/dist/tsconfig.tsbuildinfo +0 -1
  117. package/scripts/convert-svg.ts +0 -22
  118. package/src/chain.ts +0 -27
  119. package/src/contracts.ts +0 -89
  120. package/src/conversionRates.ts +0 -356
  121. package/src/currencies.ts +0 -286
  122. package/src/data/countries.json +0 -1099
  123. package/src/deposit.ts +0 -2
  124. package/src/events.ts +0 -21
  125. package/src/flags/eur.svg +0 -4
  126. package/src/flags/usd.svg +0 -25
  127. package/src/flags.ts +0 -4
  128. package/src/global.d.ts +0 -9
  129. package/src/ids.ts +0 -137
  130. package/src/index.ts +0 -16
  131. package/src/input.ts +0 -35
  132. package/src/intent.ts +0 -15
  133. package/src/json.ts +0 -42
  134. package/src/participants.ts +0 -14
  135. package/src/payment.ts +0 -573
  136. package/src/profit.ts +0 -55
  137. package/src/sort.ts +0 -49
  138. package/src/stats.ts +0 -10
  139. package/src/time.ts +0 -123
  140. package/src/tokens.ts +0 -159
  141. package/src/units.ts +0 -120
  142. package/src/zkp2p-types.ts +0 -55
  143. package/tsconfig.json +0 -18
  144. /package/dist/{scripts/convert-svg.d.ts → chain.test.d.ts} +0 -0
  145. /package/dist/{src/deposit.js → contracts.test.d.ts} +0 -0
  146. /package/dist/{src/intent.js → conversionRates.test.d.ts} +0 -0
  147. /package/dist/{src/zkp2p-types.js → currencies.test.d.ts} +0 -0
  148. /package/dist/{src/flags.d.ts → flags.d.ts} +0 -0
  149. /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
+ });