1+ /**
2+ * Integration Tests for Payouts Module
3+ *
4+ * Tests the complete payout flow: buildManifest → toCSV → preparePushTxs → executeTxPlan → reconcilePush
5+ * Verifies that all functions can be imported from the package root.
6+ */
7+
8+ import {
9+ buildManifest ,
10+ toCSV ,
11+ preparePushTxs ,
12+ executeTxPlan ,
13+ reconcilePush ,
14+ TokenInfo ,
15+ WinnerRow ,
16+ ExecuteOptions ,
17+ } from '../index' ;
18+
19+ interface MockTransaction {
20+ to : string ;
21+ value : string ;
22+ data : string ;
23+ gasLimit : string ;
24+ gasPrice ?: string ;
25+ maxFeePerGas ?: string ;
26+ maxPriorityFeePerGas ?: string ;
27+ nonce : number ;
28+ chainId : number ;
29+ type ?: number ;
30+ }
31+
32+ // Mock viem types for testing
33+ interface MockWalletClient {
34+ sendTransaction : ( tx : MockTransaction ) => Promise < `0x${string } `> ;
35+ }
36+
37+ interface MockPublicClient {
38+ waitForTransactionReceipt : ( options : { hash : `0x${string } `; confirmations : number } ) => Promise < { status : 'success' | 'reverted' } > ;
39+ getTransactionReceipt : ( options : { hash : `0x${string } ` } ) => Promise < {
40+ status : 'success' | 'reverted' ;
41+ logs : Array < {
42+ topics : string [ ] ;
43+ data : string ;
44+ } > ;
45+ } > ;
46+ }
47+
48+ describe ( 'Payouts Integration' , ( ) => {
49+ const mockToken : TokenInfo = {
50+ address : '0xa0b86A33e6441e7344C2C3Dd84A1ba8F3894e5D8' , // USDC on Ethereum (properly checksummed)
51+ symbol : 'USDC' ,
52+ name : 'USD Coin' ,
53+ decimals : 6 ,
54+ chainId : 1 ,
55+ isNative : false ,
56+ } ;
57+
58+ const mockWinners : WinnerRow [ ] = [
59+ {
60+ address : '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' , // vitalik.eth (properly checksummed)
61+ amount : '100' ,
62+ rank : 1 ,
63+ id : 'winner-1' ,
64+ } ,
65+ {
66+ address : 'invalid-address' , // Invalid address for testing
67+ amount : '50' ,
68+ rank : 2 ,
69+ id : 'winner-2' ,
70+ } ,
71+ {
72+ address : '0x742D35Cc6584c0532e47a89c9Fdd3d3F8c6c1B66' , // Another valid address (properly checksummed)
73+ amount : '25' ,
74+ rank : 3 ,
75+ id : 'winner-3' ,
76+ } ,
77+ ] ;
78+
79+ const createMockWalletClient = ( ) : MockWalletClient => ( {
80+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
81+ async sendTransaction ( _tx : MockTransaction ) : Promise < `0x${string } `> {
82+ // Simulate successful transaction with proper 64-character hash
83+ const hash = '0x' + '1234567890abcdef' . repeat ( 4 ) ; // Creates exactly 64 hex chars
84+ return hash as `0x${string } `;
85+ } ,
86+ } ) ;
87+
88+ const createMockPublicClient = ( ) : MockPublicClient => ( {
89+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
90+ async waitForTransactionReceipt ( _options : { hash : `0x${string } `; confirmations : number } ) {
91+ return { status : 'success' as const } ;
92+ } ,
93+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
94+ async getTransactionReceipt ( _options : { hash : `0x${string } ` } ) {
95+ return {
96+ status : 'success' as const ,
97+ logs : [
98+ {
99+ topics : [
100+ '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' , // Transfer event
101+ '0x000000000000000000000000742d35cc6584c0532e47a89c9fdd3d3f8c6c1b66' , // from
102+ '0x000000000000000000000000742d35cc6584c0532e47a89c9fdd3d3f8c6c1b66' , // to (winner address)
103+ ] ,
104+ data : '0x0000000000000000000000000000000000000000000000000000000000000064' , // amount (100 in hex)
105+ } ,
106+ ] ,
107+ } ;
108+ } ,
109+ } ) ;
110+
111+ test ( 'should complete full payout flow with happy path' , async ( ) => {
112+ // Step 1: Build manifest
113+ const buildResult = buildManifest ( mockWinners , {
114+ token : mockToken ,
115+ roundId : 'round-123' ,
116+ groupId : 'group-456' ,
117+ } ) ;
118+
119+ expect ( buildResult . manifest ) . toBeDefined ( ) ;
120+ expect ( buildResult . manifest . winners . length ) . toBeGreaterThan ( 0 ) ;
121+ expect ( buildResult . rejectedAddresses . length ) . toBeGreaterThan ( 0 ) ; // One invalid address
122+
123+ // Step 2: Export to CSV
124+ const csvData = toCSV ( buildResult . manifest ) ;
125+
126+ expect ( csvData ) . toContain ( 'address,amountWei,symbol' ) ;
127+ expect ( csvData . toLowerCase ( ) ) . toContain ( '0x742d35cc6584c0532e47a89c9fdd3d3f8c6c1b66' ) ;
128+ expect ( csvData ) . toContain ( 'USDC' ) ;
129+
130+ // Step 3: Prepare push transactions
131+ const preparedPayout = preparePushTxs ( buildResult . manifest , {
132+ token : mockToken ,
133+ maxPerBatch : 2 ,
134+ singleApproval : true ,
135+ airdrop : '0x2aACce8B9522F81F14834883198645BB6894Bfc0' , // Provide a valid airdrop address
136+ } ) ;
137+
138+ expect ( preparedPayout . manifestId ) . toBe ( buildResult . manifest . id ) ;
139+ expect ( preparedPayout . validation . isValid ) . toBe ( true ) ;
140+ expect ( preparedPayout . transactions . length ) . toBeGreaterThan ( 0 ) ;
141+
142+ // Step 4: Execute transaction plan (mocked)
143+ const mockWallet = createMockWalletClient ( ) ;
144+ const mockPublic = createMockPublicClient ( ) ;
145+ const executeOptions : ExecuteOptions = {
146+ wallet : mockWallet as unknown as import ( 'viem' ) . WalletClient ,
147+ publicClient : mockPublic as unknown as import ( 'viem' ) . PublicClient ,
148+ stopOnFail : false ,
149+ } ;
150+
151+ const hashes = await executeTxPlan ( preparedPayout , executeOptions ) ;
152+
153+ expect ( hashes . length ) . toBeGreaterThan ( 0 ) ;
154+ expect ( hashes [ 0 ] ) . toMatch ( / ^ 0 x [ a - f A - F 0 - 9 ] { 64 } $ / ) ;
155+
156+ // Step 5: Reconcile push (mocked)
157+ const reconcileResult = await reconcilePush (
158+ mockPublic as unknown as import ( 'viem' ) . PublicClient ,
159+ mockToken . address as `0x${string } `,
160+ buildResult . manifest ,
161+ hashes
162+ ) ;
163+
164+ expect ( reconcileResult . success ) . toBeDefined ( ) ;
165+ expect ( reconcileResult . totalAmountFound ) . toBeDefined ( ) ;
166+ expect ( reconcileResult . expectedTotalAmount ) . toBe ( buildResult . manifest . totalAmount ) ;
167+ expect ( reconcileResult . details . successfulTransfers ) . toBeDefined ( ) ;
168+ } ) ;
169+
170+ test ( 'should handle failed transactions in execution' , async ( ) => {
171+ // Create a mock wallet that throws errors
172+ const failingWallet : MockWalletClient = {
173+ async sendTransaction ( ) {
174+ throw new Error ( 'Network error' ) ;
175+ } ,
176+ } ;
177+
178+ const buildResult = buildManifest ( [ mockWinners [ 0 ] ! ] , {
179+ token : mockToken ,
180+ roundId : 'round-123' ,
181+ groupId : 'group-456' ,
182+ } ) ;
183+
184+ const preparedPayout = preparePushTxs ( buildResult . manifest , {
185+ token : mockToken ,
186+ airdrop : '0x2aACce8B9522F81F14834883198645BB6894Bfc0' , // Provide a valid airdrop address
187+ } ) ;
188+
189+ const mockPublic = createMockPublicClient ( ) ;
190+ const executeOptions : ExecuteOptions = {
191+ wallet : failingWallet as unknown as import ( 'viem' ) . WalletClient ,
192+ publicClient : mockPublic as unknown as import ( 'viem' ) . PublicClient ,
193+ stopOnFail : false ,
194+ } ;
195+
196+ const hashes = await executeTxPlan ( preparedPayout , executeOptions ) ;
197+
198+ // Should return empty array when all transactions fail
199+ expect ( hashes . length ) . toBe ( 0 ) ;
200+ } ) ;
201+
202+ test ( 'should import all required functions from package root' , ( ) => {
203+ // This test verifies the main requirement from the issue
204+ // that all functions can be imported from the package root
205+ expect ( typeof buildManifest ) . toBe ( 'function' ) ;
206+ expect ( typeof toCSV ) . toBe ( 'function' ) ;
207+ expect ( typeof preparePushTxs ) . toBe ( 'function' ) ;
208+ expect ( typeof executeTxPlan ) . toBe ( 'function' ) ;
209+ expect ( typeof reconcilePush ) . toBe ( 'function' ) ;
210+ } ) ;
211+
212+ test ( 'should export CSV with correct format' , ( ) => {
213+ const buildResult = buildManifest ( [ mockWinners [ 0 ] ! ] , {
214+ token : mockToken ,
215+ roundId : 'round-123' ,
216+ groupId : 'group-456' ,
217+ } ) ;
218+
219+ const csvData = toCSV ( buildResult . manifest ) ;
220+
221+ // Test canonical format: address,amountWei,symbol,roundId,groupId
222+ expect ( csvData ) . toContain ( 'address,amountWei,symbol,roundId,groupId' ) ;
223+ expect ( csvData . toLowerCase ( ) ) . toContain ( '0xd8da6bf26964af9d7eed9e03e53415d37aa96045' ) ;
224+ expect ( csvData ) . toContain ( 'USDC' ) ;
225+ expect ( csvData ) . toContain ( 'round-123' ) ;
226+ expect ( csvData ) . toContain ( 'group-456' ) ;
227+ } ) ;
228+ } ) ;
0 commit comments