@provex/utils 1.0.16 → 1.2.3

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 (122) hide show
  1. package/dist/src/chain.d.ts +436 -4
  2. package/dist/src/chain.js +80 -0
  3. package/dist/src/chain.test.d.ts +1 -0
  4. package/dist/src/chain.test.js +112 -0
  5. package/dist/src/contracts.d.ts +196 -20
  6. package/dist/src/contracts.js +245 -17
  7. package/dist/src/contracts.test.d.ts +1 -0
  8. package/dist/src/contracts.test.js +243 -0
  9. package/dist/src/conversionRates.d.ts +99 -8
  10. package/dist/src/conversionRates.js +74 -4
  11. package/dist/src/conversionRates.test.d.ts +1 -0
  12. package/dist/src/conversionRates.test.js +473 -0
  13. package/dist/src/currencies.d.ts +113 -9
  14. package/dist/src/currencies.js +119 -38
  15. package/dist/src/currencies.test.d.ts +1 -0
  16. package/dist/src/currencies.test.js +343 -0
  17. package/dist/src/data/countries.json +166 -166
  18. package/dist/src/deposit.d.ts +69 -1
  19. package/dist/src/deposit.js +52 -1
  20. package/dist/src/depositKeys.d.ts +154 -0
  21. package/dist/src/depositKeys.js +104 -0
  22. package/dist/src/depositKeys.test.d.ts +1 -0
  23. package/dist/src/depositKeys.test.js +231 -0
  24. package/dist/src/endpoints.d.ts +51 -0
  25. package/dist/src/endpoints.js +70 -0
  26. package/dist/src/endpoints.test.d.ts +1 -0
  27. package/dist/src/endpoints.test.js +54 -0
  28. package/dist/src/events.d.ts +40 -2
  29. package/dist/src/events.js +86 -10
  30. package/dist/src/events.test.d.ts +1 -0
  31. package/dist/src/events.test.js +111 -0
  32. package/dist/src/fees.d.ts +136 -0
  33. package/dist/src/fees.js +178 -0
  34. package/dist/src/fees.test.d.ts +1 -0
  35. package/dist/src/fees.test.js +165 -0
  36. package/dist/src/ids.d.ts +125 -22
  37. package/dist/src/ids.js +211 -61
  38. package/dist/src/ids.test.d.ts +1 -0
  39. package/dist/src/ids.test.js +607 -0
  40. package/dist/src/index.d.ts +30 -1
  41. package/dist/src/index.js +30 -1
  42. package/dist/src/input.d.ts +23 -4
  43. package/dist/src/input.js +23 -4
  44. package/dist/src/intent.d.ts +38 -2
  45. package/dist/src/intent.js +28 -1
  46. package/dist/src/intent.test.d.ts +1 -0
  47. package/dist/src/intent.test.js +119 -0
  48. package/dist/src/json.d.ts +38 -1
  49. package/dist/src/json.js +38 -2
  50. package/dist/src/normalization.d.ts +224 -0
  51. package/dist/src/normalization.js +119 -0
  52. package/dist/src/normalization.test.d.ts +1 -0
  53. package/dist/src/normalization.test.js +284 -0
  54. package/dist/src/participants.d.ts +19 -4
  55. package/dist/src/participants.js +19 -4
  56. package/dist/src/payment.d.ts +333 -16
  57. package/dist/src/payment.js +768 -58
  58. package/dist/src/payment.test.d.ts +1 -0
  59. package/dist/src/payment.test.js +272 -0
  60. package/dist/src/peer-types.d.ts +327 -0
  61. package/dist/src/peer-types.js +169 -0
  62. package/dist/src/profit.d.ts +26 -5
  63. package/dist/src/profit.js +31 -8
  64. package/dist/src/profit.test.d.ts +1 -0
  65. package/dist/src/profit.test.js +315 -0
  66. package/dist/src/reputation.d.ts +125 -0
  67. package/dist/src/reputation.js +188 -0
  68. package/dist/src/reputation.test.d.ts +1 -0
  69. package/dist/src/reputation.test.js +156 -0
  70. package/dist/src/sort.d.ts +49 -10
  71. package/dist/src/sort.js +40 -9
  72. package/dist/src/sort.test.d.ts +1 -0
  73. package/dist/src/sort.test.js +216 -0
  74. package/dist/src/stateKey.d.ts +35 -0
  75. package/dist/src/stateKey.js +36 -0
  76. package/dist/src/stats.d.ts +55 -1
  77. package/dist/src/stats.js +46 -1
  78. package/dist/src/stats.test.d.ts +1 -0
  79. package/dist/src/stats.test.js +104 -0
  80. package/dist/src/time.d.ts +59 -9
  81. package/dist/src/time.js +71 -15
  82. package/dist/src/time.test.d.ts +1 -0
  83. package/dist/src/time.test.js +231 -0
  84. package/dist/src/tokens.d.ts +11972 -145
  85. package/dist/src/tokens.js +98 -17
  86. package/dist/src/tokens.test.d.ts +1 -0
  87. package/dist/src/tokens.test.js +193 -0
  88. package/dist/src/units.d.ts +183 -0
  89. package/dist/src/units.js +206 -6
  90. package/dist/src/units.test.d.ts +1 -0
  91. package/dist/src/units.test.js +327 -0
  92. package/dist/src/zkp2p-types.d.ts +273 -4
  93. package/dist/src/zkp2p-types.js +169 -1
  94. package/dist/tsconfig.tsbuildinfo +1 -1
  95. package/package.json +116 -26
  96. package/scripts/convert-svg.ts +0 -22
  97. package/src/chain.ts +0 -27
  98. package/src/contracts.ts +0 -89
  99. package/src/conversionRates.ts +0 -356
  100. package/src/currencies.ts +0 -286
  101. package/src/data/countries.json +0 -1099
  102. package/src/deposit.ts +0 -2
  103. package/src/events.ts +0 -21
  104. package/src/flags/eur.svg +0 -4
  105. package/src/flags/usd.svg +0 -25
  106. package/src/flags.ts +0 -4
  107. package/src/global.d.ts +0 -9
  108. package/src/ids.ts +0 -137
  109. package/src/index.ts +0 -16
  110. package/src/input.ts +0 -35
  111. package/src/intent.ts +0 -15
  112. package/src/json.ts +0 -42
  113. package/src/participants.ts +0 -14
  114. package/src/payment.ts +0 -573
  115. package/src/profit.ts +0 -55
  116. package/src/sort.ts +0 -49
  117. package/src/stats.ts +0 -10
  118. package/src/time.ts +0 -123
  119. package/src/tokens.ts +0 -159
  120. package/src/units.ts +0 -120
  121. package/src/zkp2p-types.ts +0 -55
  122. package/tsconfig.json +0 -18
@@ -0,0 +1,169 @@
1
+ /**
2
+ * @fileoverview Peer protocol types for attestation and proof verification.
3
+ *
4
+ * This module defines the type system for the V3 attestation flow, including:
5
+ * - Notarized proofs from the PeerAuth browser extension
6
+ * - Attestation requests/responses from the peer attestation service
7
+ * - EIP-712 typed data structures for on-chain verification
8
+ * - Payment proof encoding for the UnifiedPaymentVerifier contract
9
+ *
10
+ * @remarks
11
+ * **V3 Attestation Flow:**
12
+ * 1. Buyer generates zkTLS proof via PeerAuth extension (NotarizedProofData)
13
+ * 2. Proof + intent data sent to attestation service (V3AttestationRequest)
14
+ * 3. Service validates and returns EIP-712 signed attestation (V3AttestationResponse)
15
+ * 4. Attestation encoded as paymentProof bytes (encodeV3PaymentProof)
16
+ * 5. Buyer calls fulfillIntent() with encoded proof
17
+ *
18
+ * **Type Aliases:**
19
+ * For cleaner imports, V3-prefixed types have unprefixed aliases:
20
+ * - AttestationOptions = V3AttestationOptions
21
+ * - AttestationResponse = V3AttestationResponse
22
+ * - IntentData = V3IntentData
23
+ *
24
+ * @see {@link intent} for intent data structures
25
+ * @see {@link payment} for provider definitions
26
+ */
27
+ import { encodeAbiParameters, decodeAbiParameters, keccak256, encodePacked } from "viem";
28
+ // ============================================================================
29
+ // V3 Payment Proof Encoding
30
+ // ============================================================================
31
+ /**
32
+ * ABI parameter definition for the V3 payment proof struct.
33
+ *
34
+ * IMPORTANT: The struct must be wrapped in an outer tuple to match the peer encoding.
35
+ * This causes ABI encoding to add an offset prefix (0x20) at the start.
36
+ *
37
+ * Structure:
38
+ * - Outer tuple wrapper (adds 0x20 offset prefix)
39
+ * - PaymentAttestation tuple (intentHash, releaseAmount, dataHash)
40
+ * - signatures[] array of EIP-712 signatures
41
+ * - encodedPaymentDetails bytes
42
+ * - metadata bytes
43
+ * - attestorAddresses[] array of signer addresses
44
+ */
45
+ const V3_PAYMENT_PROOF_ABI = [
46
+ {
47
+ type: 'tuple',
48
+ components: [
49
+ {
50
+ type: 'tuple',
51
+ name: 'attestation',
52
+ components: [
53
+ { name: 'intentHash', type: 'bytes32' },
54
+ { name: 'releaseAmount', type: 'uint256' },
55
+ { name: 'dataHash', type: 'bytes32' }
56
+ ]
57
+ },
58
+ { type: 'bytes[]', name: 'signatures' },
59
+ { type: 'bytes', name: 'encodedPaymentDetails' },
60
+ { type: 'bytes', name: 'metadata' },
61
+ { type: 'address[]', name: 'attestorAddresses' }
62
+ ]
63
+ }
64
+ ];
65
+ /**
66
+ * Encodes a V3 attestation response into the paymentProof bytes format
67
+ * expected by the UnifiedPaymentVerifier contract's fulfillIntent function.
68
+ *
69
+ * The encoded struct contains (wrapped in outer tuple for offset prefix):
70
+ * - PaymentAttestation: { intentHash, releaseAmount, dataHash }
71
+ * - signatures: [signature] (array with the EIP-712 signature)
72
+ * - encodedPaymentDetails: payment details for on-chain verification
73
+ * - metadata: additional metadata bytes
74
+ * - attestorAddresses: [signer] (array with the attestor address)
75
+ *
76
+ * @param attestation - The V3AttestationResponseObject from the attestation service
77
+ * @returns ABI-encoded bytes to pass as paymentProof in fulfillIntent
78
+ */
79
+ export function encodeV3PaymentProof(attestation) {
80
+ return encodeAbiParameters(V3_PAYMENT_PROOF_ABI, [
81
+ {
82
+ attestation: {
83
+ intentHash: attestation.typedDataValue.intentHash,
84
+ releaseAmount: BigInt(attestation.typedDataValue.releaseAmount),
85
+ dataHash: attestation.typedDataValue.dataHash
86
+ },
87
+ signatures: [attestation.signature],
88
+ encodedPaymentDetails: attestation.encodedPaymentDetails,
89
+ metadata: attestation.metadata,
90
+ attestorAddresses: [attestation.signer]
91
+ }
92
+ ]);
93
+ }
94
+ /**
95
+ * Extracts the Reclaim attestor address from the attestation proofInput.
96
+ * This is needed for the verificationData field in fulfillIntent.
97
+ *
98
+ * @param attestation - The V3AttestationResponseObject from the attestation service
99
+ * @returns The Reclaim attestor address as Hex, or null if not found
100
+ */
101
+ export function extractReclaimAttestorAddress(attestation) {
102
+ try {
103
+ const proofInput = JSON.parse(attestation.proofInput);
104
+ const proofs = JSON.parse(proofInput.proof);
105
+ if (Array.isArray(proofs) && proofs.length > 0) {
106
+ const attestorAddress = proofs[0]?.signatures?.attestorAddress;
107
+ if (attestorAddress && typeof attestorAddress === 'string') {
108
+ return attestorAddress;
109
+ }
110
+ }
111
+ return null;
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ }
117
+ /**
118
+ * Extracts the nullifier from a V3 attestation response.
119
+ *
120
+ * The nullifier is NOT directly stored in `encodedPaymentDetails`. Instead, it's derived
121
+ * from `paymentMethod` and `paymentId` using:
122
+ * `keccak256(abi.encodePacked(paymentMethod, paymentId))`
123
+ *
124
+ * The `encodedPaymentDetails` contains a tuple of `(PaymentDetails, IntentSnapshot)`.
125
+ * This function decodes the PaymentDetails struct to extract paymentMethod and paymentId,
126
+ * then derives the nullifier.
127
+ *
128
+ * @param attestation - The V3AttestationResponseObject from the attestation service
129
+ * @returns The derived nullifier as Hex, or null if extraction fails
130
+ */
131
+ export function extractNullifier(attestation) {
132
+ try {
133
+ // Decode encodedPaymentDetails as (PaymentDetails, IntentSnapshot) tuple
134
+ // PaymentDetails struct: { paymentMethod, payeeId, amount, currency, timestamp, paymentId }
135
+ // IntentSnapshot struct: { paymentMethod, currency, payeeId, conversionRate, timestamp, unknown }
136
+ const decoded = decodeAbiParameters([
137
+ {
138
+ type: 'tuple',
139
+ components: [
140
+ { type: 'bytes32', name: 'paymentMethod' },
141
+ { type: 'bytes32', name: 'payeeId' },
142
+ { type: 'uint256', name: 'amount' },
143
+ { type: 'bytes32', name: 'currency' },
144
+ { type: 'uint256', name: 'timestamp' },
145
+ { type: 'bytes32', name: 'paymentId' },
146
+ ],
147
+ },
148
+ {
149
+ type: 'tuple',
150
+ components: [
151
+ { type: 'bytes32', name: 'paymentMethod' },
152
+ { type: 'bytes32', name: 'currency' },
153
+ { type: 'bytes32', name: 'payeeId' },
154
+ { type: 'uint256', name: 'conversionRate' },
155
+ { type: 'uint256', name: 'timestamp' },
156
+ { type: 'uint256', name: 'unknown' },
157
+ ],
158
+ },
159
+ ], attestation.encodedPaymentDetails);
160
+ const paymentDetails = decoded[0];
161
+ // Derive nullifier: keccak256(abi.encodePacked(paymentMethod, paymentId))
162
+ const nullifier = keccak256(encodePacked(['bytes32', 'bytes32'], [paymentDetails.paymentMethod, paymentDetails.paymentId]));
163
+ return nullifier;
164
+ }
165
+ catch (error) {
166
+ console.error('Failed to extract nullifier:', error);
167
+ return null;
168
+ }
169
+ }
@@ -1,20 +1,41 @@
1
+ /**
2
+ * @fileoverview Profit calculation utilities for market maker analytics.
3
+ *
4
+ * Calculates the profit/loss on fulfilled intents by comparing the token
5
+ * amount received against the expected amount at a 1:1 fiat:token rate.
6
+ *
7
+ * @remarks
8
+ * This is most meaningful for USD where $1 ≈ 1 USDC. For other currencies,
9
+ * the calculation shows the mathematical difference assuming 1:1 parity,
10
+ * which may not reflect actual profit in local currency terms.
11
+ */
1
12
  import type { ChainId } from "./chain.js";
2
- /** BigIntish type */
13
+ /**
14
+ * Flexible bigint input type that accepts string, bigint, or number.
15
+ */
3
16
  export type BigIntish = string | bigint | number;
4
- /** Profit calculation input */
17
+ /**
18
+ * Input parameters for profit calculation on a single intent.
19
+ */
5
20
  export interface ProfitCalculationInput {
21
+ /** Token amount received in base units (e.g., 6 decimals for USDC) */
6
22
  amount: BigIntish;
23
+ /** Chain ID for token decimal lookup */
7
24
  chainId: ChainId;
25
+ /** Conversion rate at which the exchange occurred */
8
26
  conversionRate: {
27
+ /** Rate value with 18 decimals (1e18 = 1.0) */
9
28
  value: BigIntish;
29
+ /** Currency contract ID (keccak256 hash) */
10
30
  currency: string;
11
31
  };
12
32
  }
13
33
  /**
14
34
  * Calculate the profit of an intent fulfilled event
15
- * @param param0.amount - The amount
16
- * @param param0.conversionRate - The conversion rate
17
- * @returns The profit
35
+ * @param param0.amount - The fiat amount in currency base units (e.g., cents for USD)
36
+ * @param param0.conversionRate - The conversion rate (tokens per fiat unit, scaled by 10^18)
37
+ * @param param0.chainId - The chain ID to get the default token
38
+ * @returns The profit in token base units, or null if not calculable
18
39
  */
19
40
  export declare const calculateProfit: ({ amount: amountInput, conversionRate, chainId, }: ProfitCalculationInput) => bigint | null;
20
41
  /**
@@ -1,22 +1,45 @@
1
+ /**
2
+ * @fileoverview Profit calculation utilities for market maker analytics.
3
+ *
4
+ * Calculates the profit/loss on fulfilled intents by comparing the token
5
+ * amount received against the expected amount at a 1:1 fiat:token rate.
6
+ *
7
+ * @remarks
8
+ * This is most meaningful for USD where $1 ≈ 1 USDC. For other currencies,
9
+ * the calculation shows the mathematical difference assuming 1:1 parity,
10
+ * which may not reflect actual profit in local currency terms.
11
+ */
1
12
  import { getCurrencyByContractId } from "./currencies.js";
2
13
  import { getDefaultToken } from "./tokens.js";
3
14
  /**
4
15
  * Calculate the profit of an intent fulfilled event
5
- * @param param0.amount - The amount
6
- * @param param0.conversionRate - The conversion rate
7
- * @returns The profit
16
+ * @param param0.amount - The fiat amount in currency base units (e.g., cents for USD)
17
+ * @param param0.conversionRate - The conversion rate (tokens per fiat unit, scaled by 10^18)
18
+ * @param param0.chainId - The chain ID to get the default token
19
+ * @returns The profit in token base units, or null if not calculable
8
20
  */
9
21
  export const calculateProfit = ({ amount: amountInput, conversionRate, chainId, }) => {
10
22
  const rate = BigInt(conversionRate.value);
11
23
  const amount = BigInt(amountInput);
12
- const valueReceived = amount * rate / 10n ** 18n;
13
24
  const currency = getCurrencyByContractId(conversionRate.currency);
14
25
  const inputTokenInfo = getDefaultToken(chainId);
15
- const isOneForOne = currency.ticker === 'USD';
16
- if (inputTokenInfo && isOneForOne) {
17
- return valueReceived - amount;
26
+ if (!inputTokenInfo || !currency) {
27
+ return null;
18
28
  }
19
- return null;
29
+ // Calculate expected token amount based on fiat amount and conversion rate
30
+ // valueReceived = (fiat_amount * rate) / 10^18, gives us token amount in token base units
31
+ const valueReceived = amount * rate / 10n ** 18n;
32
+ // Scale the fiat amount to match token decimals for comparison
33
+ // e.g., $100 USD (10000 in 2 decimals) -> 100000000 in 6 decimals for USDC
34
+ // e.g., €100 EUR (10000 in 2 decimals) -> 100000000 in 6 decimals for USDC
35
+ const decimalDiff = inputTokenInfo.decimals - currency.decimals;
36
+ const amountScaled = decimalDiff >= 0
37
+ ? amount * 10n ** BigInt(decimalDiff)
38
+ : amount / 10n ** BigInt(-decimalDiff);
39
+ // Profit = what we received - what we expected for 1:1 fiat:token exchange
40
+ // Note: This is most meaningful for USD where $1 ≈ 1 USDC
41
+ // For other currencies, this shows the mathematical difference assuming 1:1
42
+ return valueReceived - amountScaled;
20
43
  };
21
44
  /**
22
45
  * Calculate the profits for a list of inputs
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,315 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { base, pulsechain } from 'viem/chains';
4
+ import { calculateProfit, calculateProfits } from './profit.js';
5
+ import { SUPPORTED_CURRENCIES } from './currencies.js';
6
+ describe('profit', () => {
7
+ // Constants for testing
8
+ const USD_CONTRACT_ID = SUPPORTED_CURRENCIES.USD.contractId;
9
+ const EUR_CONTRACT_ID = SUPPORTED_CURRENCIES.EUR.contractId;
10
+ const JPY_CONTRACT_ID = SUPPORTED_CURRENCIES.JPY.contractId;
11
+ /**
12
+ * Rate calculation for USD/USDC:
13
+ * - USD has 2 decimals (amount in cents)
14
+ * - USDC has 6 decimals
15
+ * - Rate is scaled by 10^18
16
+ *
17
+ * For a 1:1 USD/USDC rate:
18
+ * - $100 USD = 10000 cents
19
+ * - Expected: 100 USDC = 100000000 (in 6 decimals)
20
+ * - valueReceived = amount * rate / 10^18
21
+ * - 100000000 = 10000 * rate / 10^18
22
+ * - rate = 100000000 * 10^18 / 10000 = 10^22
23
+ *
24
+ * This accounts for decimal difference (6-2=4 extra zeros)
25
+ */
26
+ // 1.0 rate that produces 1:1 fiat:token (accounts for 4 decimal difference USD->USDC)
27
+ const RATE_1_0 = 10n ** 22n;
28
+ // 1.05 rate (5% premium)
29
+ const RATE_1_05 = (10n ** 22n * 105n) / 100n;
30
+ // 0.95 rate (5% discount)
31
+ const RATE_0_95 = (10n ** 22n * 95n) / 100n;
32
+ describe('calculateProfit', () => {
33
+ it('should return 0 profit for 1:1 rate with USD', () => {
34
+ // $100 USD (10000 in 2 decimals) at 1.0 rate
35
+ const input = {
36
+ amount: 10000n, // $100 in cents
37
+ chainId: base.id,
38
+ conversionRate: {
39
+ value: RATE_1_0,
40
+ currency: USD_CONTRACT_ID,
41
+ },
42
+ };
43
+ const profit = calculateProfit(input);
44
+ assert.ok(profit !== null, 'Profit should not be null');
45
+ // At 1:1 rate, profit should be 0
46
+ assert.equal(profit, 0n);
47
+ });
48
+ it('should return positive profit for rate above 1.0', () => {
49
+ // $100 USD at 1.05 rate means receiving 105 USDC
50
+ const input = {
51
+ amount: 10000n, // $100 in cents
52
+ chainId: base.id,
53
+ conversionRate: {
54
+ value: RATE_1_05,
55
+ currency: USD_CONTRACT_ID,
56
+ },
57
+ };
58
+ const profit = calculateProfit(input);
59
+ assert.ok(profit !== null, 'Profit should not be null');
60
+ assert.ok(profit > 0n, 'Profit should be positive for rate > 1.0');
61
+ // Expected: received (100 * 1.05 = 105 USDC) - expected (100 USDC) = 5 USDC = 5000000 in 6 decimals
62
+ assert.equal(profit, 5000000n);
63
+ });
64
+ it('should return negative profit (loss) for rate below 1.0', () => {
65
+ // $100 USD at 0.95 rate means receiving 95 USDC
66
+ const input = {
67
+ amount: 10000n, // $100 in cents
68
+ chainId: base.id,
69
+ conversionRate: {
70
+ value: RATE_0_95,
71
+ currency: USD_CONTRACT_ID,
72
+ },
73
+ };
74
+ const profit = calculateProfit(input);
75
+ assert.ok(profit !== null, 'Profit should not be null');
76
+ assert.ok(profit < 0n, 'Profit should be negative for rate < 1.0');
77
+ // Expected: received (100 * 0.95 = 95 USDC) - expected (100 USDC) = -5 USDC = -5000000 in 6 decimals
78
+ assert.equal(profit, -5000000n);
79
+ });
80
+ it('should work with Pulsechain', () => {
81
+ const input = {
82
+ amount: 10000n,
83
+ chainId: pulsechain.id,
84
+ conversionRate: {
85
+ value: RATE_1_0,
86
+ currency: USD_CONTRACT_ID,
87
+ },
88
+ };
89
+ const profit = calculateProfit(input);
90
+ assert.ok(profit !== null, 'Profit should not be null');
91
+ assert.equal(profit, 0n);
92
+ });
93
+ it('should handle EUR currency', () => {
94
+ // EUR also has 2 decimals like USD
95
+ const input = {
96
+ amount: 10000n, // €100 in cents
97
+ chainId: base.id,
98
+ conversionRate: {
99
+ value: RATE_1_0, // Same rate logic for EUR
100
+ currency: EUR_CONTRACT_ID,
101
+ },
102
+ };
103
+ const profit = calculateProfit(input);
104
+ assert.ok(profit !== null, 'Profit should not be null');
105
+ // At 1:1 rate, profit should be 0
106
+ assert.equal(profit, 0n);
107
+ });
108
+ it('should handle JPY (0 decimal currency)', () => {
109
+ // JPY has 0 decimals, USDC has 6
110
+ // For 1:1 rate: ¥15000 -> 15000 USDC
111
+ // Rate = 15000 * 10^6 * 10^18 / 15000 = 10^24 (accounts for 6 decimal difference)
112
+ const JPY_RATE_1_0 = 10n ** 24n;
113
+ const input = {
114
+ amount: 15000n, // ¥15000
115
+ chainId: base.id,
116
+ conversionRate: {
117
+ value: JPY_RATE_1_0,
118
+ currency: JPY_CONTRACT_ID,
119
+ },
120
+ };
121
+ const profit = calculateProfit(input);
122
+ assert.ok(profit !== null, 'Profit should not be null');
123
+ assert.equal(profit, 0n);
124
+ });
125
+ it('should accept string amount', () => {
126
+ const input = {
127
+ amount: '10000',
128
+ chainId: base.id,
129
+ conversionRate: {
130
+ value: RATE_1_0,
131
+ currency: USD_CONTRACT_ID,
132
+ },
133
+ };
134
+ const profit = calculateProfit(input);
135
+ assert.ok(profit !== null, 'Profit should not be null');
136
+ });
137
+ it('should accept number amount', () => {
138
+ const input = {
139
+ amount: 10000,
140
+ chainId: base.id,
141
+ conversionRate: {
142
+ value: RATE_1_0,
143
+ currency: USD_CONTRACT_ID,
144
+ },
145
+ };
146
+ const profit = calculateProfit(input);
147
+ assert.ok(profit !== null, 'Profit should not be null');
148
+ });
149
+ it('should accept string rate', () => {
150
+ const input = {
151
+ amount: 10000n,
152
+ chainId: base.id,
153
+ conversionRate: {
154
+ value: RATE_1_0.toString(),
155
+ currency: USD_CONTRACT_ID,
156
+ },
157
+ };
158
+ const profit = calculateProfit(input);
159
+ assert.ok(profit !== null, 'Profit should not be null');
160
+ });
161
+ it('should return null for invalid currency', () => {
162
+ const input = {
163
+ amount: 10000n,
164
+ chainId: base.id,
165
+ conversionRate: {
166
+ value: RATE_1_0,
167
+ currency: '0x' + '0'.repeat(64), // Invalid currency contract ID
168
+ },
169
+ };
170
+ const profit = calculateProfit(input);
171
+ assert.equal(profit, null);
172
+ });
173
+ it('should return null for invalid chainId', () => {
174
+ const input = {
175
+ amount: 10000n,
176
+ chainId: 99999, // Invalid chain
177
+ conversionRate: {
178
+ value: RATE_1_0,
179
+ currency: USD_CONTRACT_ID,
180
+ },
181
+ };
182
+ const profit = calculateProfit(input);
183
+ assert.equal(profit, null);
184
+ });
185
+ it('should handle large amounts', () => {
186
+ // $1,000,000 USD
187
+ const input = {
188
+ amount: 100000000n, // $1M in cents
189
+ chainId: base.id,
190
+ conversionRate: {
191
+ value: RATE_1_05,
192
+ currency: USD_CONTRACT_ID,
193
+ },
194
+ };
195
+ const profit = calculateProfit(input);
196
+ assert.ok(profit !== null, 'Profit should not be null');
197
+ // 5% profit on $1M = $50,000 = 50000000000 in 6 decimals
198
+ assert.equal(profit, 50000000000n);
199
+ });
200
+ it('should handle small amounts', () => {
201
+ // $0.01 USD (1 cent)
202
+ const input = {
203
+ amount: 1n,
204
+ chainId: base.id,
205
+ conversionRate: {
206
+ value: RATE_1_05,
207
+ currency: USD_CONTRACT_ID,
208
+ },
209
+ };
210
+ const profit = calculateProfit(input);
211
+ assert.ok(profit !== null, 'Profit should not be null');
212
+ // Small amount should still calculate correctly
213
+ assert.ok(typeof profit === 'bigint');
214
+ });
215
+ });
216
+ describe('calculateProfits', () => {
217
+ it('should calculate total profit for multiple inputs', () => {
218
+ const inputs = [
219
+ {
220
+ amount: 10000n, // $100
221
+ chainId: base.id,
222
+ conversionRate: { value: RATE_1_05, currency: USD_CONTRACT_ID },
223
+ },
224
+ {
225
+ amount: 20000n, // $200
226
+ chainId: base.id,
227
+ conversionRate: { value: RATE_1_05, currency: USD_CONTRACT_ID },
228
+ },
229
+ ];
230
+ const result = calculateProfits(inputs);
231
+ assert.equal(result.nullAmounts, 0);
232
+ // $100 * 5% + $200 * 5% = $5 + $10 = $15 = 15000000 in 6 decimals
233
+ assert.equal(result.profit, 15000000n);
234
+ });
235
+ it('should handle mix of positive and negative profits', () => {
236
+ const inputs = [
237
+ {
238
+ amount: 10000n,
239
+ chainId: base.id,
240
+ conversionRate: { value: RATE_1_05, currency: USD_CONTRACT_ID }, // +$5
241
+ },
242
+ {
243
+ amount: 10000n,
244
+ chainId: base.id,
245
+ conversionRate: { value: RATE_0_95, currency: USD_CONTRACT_ID }, // -$5
246
+ },
247
+ ];
248
+ const result = calculateProfits(inputs);
249
+ assert.equal(result.nullAmounts, 0);
250
+ // +5 + -5 = 0
251
+ assert.equal(result.profit, 0n);
252
+ });
253
+ it('should count null amounts', () => {
254
+ const inputs = [
255
+ {
256
+ amount: 10000n,
257
+ chainId: base.id,
258
+ conversionRate: { value: RATE_1_0, currency: USD_CONTRACT_ID },
259
+ },
260
+ {
261
+ amount: 10000n,
262
+ chainId: 99999, // Invalid - will return null
263
+ conversionRate: { value: RATE_1_0, currency: USD_CONTRACT_ID },
264
+ },
265
+ {
266
+ amount: 10000n,
267
+ chainId: base.id,
268
+ conversionRate: { value: RATE_1_0, currency: '0x' + '0'.repeat(64) }, // Invalid currency
269
+ },
270
+ ];
271
+ const result = calculateProfits(inputs);
272
+ assert.equal(result.nullAmounts, 2);
273
+ });
274
+ it('should return 0 profit and 0 nullAmounts for empty array', () => {
275
+ const result = calculateProfits([]);
276
+ assert.equal(result.profit, 0n);
277
+ assert.equal(result.nullAmounts, 0);
278
+ });
279
+ it('should handle all null results', () => {
280
+ const inputs = [
281
+ {
282
+ amount: 10000n,
283
+ chainId: 99999,
284
+ conversionRate: { value: RATE_1_0, currency: USD_CONTRACT_ID },
285
+ },
286
+ {
287
+ amount: 10000n,
288
+ chainId: 99998,
289
+ conversionRate: { value: RATE_1_0, currency: USD_CONTRACT_ID },
290
+ },
291
+ ];
292
+ const result = calculateProfits(inputs);
293
+ assert.equal(result.nullAmounts, 2);
294
+ assert.equal(result.profit, 0n);
295
+ });
296
+ it('should work with mixed chains', () => {
297
+ const inputs = [
298
+ {
299
+ amount: 10000n,
300
+ chainId: base.id,
301
+ conversionRate: { value: RATE_1_05, currency: USD_CONTRACT_ID },
302
+ },
303
+ {
304
+ amount: 10000n,
305
+ chainId: pulsechain.id,
306
+ conversionRate: { value: RATE_1_05, currency: USD_CONTRACT_ID },
307
+ },
308
+ ];
309
+ const result = calculateProfits(inputs);
310
+ assert.equal(result.nullAmounts, 0);
311
+ // Both should have 5% profit = $5 each = $10 total
312
+ assert.equal(result.profit, 10000000n);
313
+ });
314
+ });
315
+ });