1+ #!/usr/bin/env node
2+
3+ /**
4+ * Balance Snapshots Script (JavaScript version)
5+ *
6+ * This script fetches wallet balances and stores them as snapshots in the database.
7+ * It can be run locally for testing or by GitHub Actions for automated snapshots.
8+ *
9+ * Usage:
10+ * node scripts/balance-snapshots.js
11+ * SNAPSHOT_AUTH_TOKEN=your_token node scripts/balance-snapshots.js
12+ *
13+ * Environment Variables:
14+ * - API_BASE_URL: Base URL for the API (default: http://localhost:3000)
15+ * - SNAPSHOT_AUTH_TOKEN: Authentication token for API requests
16+ * - BATCH_SIZE: Number of wallets to process per batch (default: 3)
17+ * - DELAY_BETWEEN_REQUESTS: Delay between requests in seconds (default: 3)
18+ * - DELAY_BETWEEN_BATCHES: Delay between batches in seconds (default: 15)
19+ * - MAX_RETRIES: Maximum retries for failed requests (default: 3)
20+ * - REQUEST_TIMEOUT: Request timeout in seconds (default: 30)
21+ */
22+
23+ class BalanceSnapshotService {
24+ constructor ( ) {
25+ this . config = this . loadConfig ( ) ;
26+ this . results = {
27+ walletsFound : 0 ,
28+ processedWallets : 0 ,
29+ failedWallets : 0 ,
30+ totalAdaBalance : 0 ,
31+ snapshotsStored : 0 ,
32+ executionTime : 0 ,
33+ } ;
34+ }
35+
36+ loadConfig ( ) {
37+ const apiBaseUrl = process . env . API_BASE_URL || 'http://localhost:3000' ;
38+ const authToken = process . env . SNAPSHOT_AUTH_TOKEN ;
39+
40+ if ( ! authToken ) {
41+ throw new Error ( 'SNAPSHOT_AUTH_TOKEN environment variable is required' ) ;
42+ }
43+
44+ return {
45+ apiBaseUrl,
46+ authToken,
47+ batchSize : parseInt ( process . env . BATCH_SIZE || '3' ) ,
48+ delayBetweenRequests : parseInt ( process . env . DELAY_BETWEEN_REQUESTS || '3' ) ,
49+ delayBetweenBatches : parseInt ( process . env . DELAY_BETWEEN_BATCHES || '15' ) ,
50+ maxRetries : parseInt ( process . env . MAX_RETRIES || '3' ) ,
51+ requestTimeout : parseInt ( process . env . REQUEST_TIMEOUT || '30' ) ,
52+ } ;
53+ }
54+
55+ async makeRequest ( /** @type {string } */ url , /** @type {RequestInit } */ options = { } ) {
56+ const controller = new AbortController ( ) ;
57+ const timeoutId = setTimeout ( ( ) => controller . abort ( ) , this . config . requestTimeout * 1000 ) ;
58+
59+ try {
60+ const response = await fetch ( url , {
61+ ...options ,
62+ headers : {
63+ 'Authorization' : `Bearer ${ this . config . authToken } ` ,
64+ 'Content-Type' : 'application/json' ,
65+ ...( options . headers || { } ) ,
66+ } ,
67+ signal : controller . signal ,
68+ } ) ;
69+
70+ clearTimeout ( timeoutId ) ;
71+
72+ if ( ! response . ok ) {
73+ throw new Error ( `HTTP ${ response . status } : ${ response . statusText } ` ) ;
74+ }
75+
76+ const data = await response . json ( ) ;
77+ return { data, status : response . status } ;
78+ } catch ( error ) {
79+ clearTimeout ( timeoutId ) ;
80+ throw error ;
81+ }
82+ }
83+
84+ async delay ( /** @type {number } */ seconds ) {
85+ return new Promise ( resolve => setTimeout ( resolve , seconds * 1000 ) ) ;
86+ }
87+
88+ async fetchWallets ( ) {
89+ console . log ( '📋 Fetching all wallets...' ) ;
90+
91+ const { data } = await this . makeRequest (
92+ `${ this . config . apiBaseUrl } /api/v1/aggregatedBalances/wallets`
93+ ) ;
94+
95+ console . log ( `✅ Found ${ data . walletCount } wallets` ) ;
96+ this . results . walletsFound = data . walletCount ;
97+
98+ if ( data . walletCount === 0 ) {
99+ console . log ( 'ℹ️ No wallets found, skipping snapshot process' ) ;
100+ return [ ] ;
101+ }
102+
103+ return data . wallets ;
104+ }
105+
106+ async fetchWalletBalance ( /** @type {any } */ wallet ) {
107+ const params = new URLSearchParams ( {
108+ walletId : wallet . walletId ,
109+ walletName : wallet . walletName ,
110+ signersAddresses : JSON . stringify ( wallet . signersAddresses ) ,
111+ numRequiredSigners : wallet . numRequiredSigners . toString ( ) ,
112+ type : wallet . type ,
113+ stakeCredentialHash : wallet . stakeCredentialHash || '' ,
114+ isArchived : wallet . isArchived . toString ( ) ,
115+ network : wallet . network . toString ( ) ,
116+ } ) ;
117+
118+ const url = `${ this . config . apiBaseUrl } /api/v1/aggregatedBalances/balance?${ params } ` ;
119+
120+ for ( let attempt = 1 ; attempt <= this . config . maxRetries ; attempt ++ ) {
121+ try {
122+ const { data } = await this . makeRequest ( url ) ;
123+ console . log ( ` ✅ Balance: ${ data . walletBalance . adaBalance } ADA` ) ;
124+ return data . walletBalance ;
125+ } catch ( error ) {
126+ const isLastAttempt = attempt === this . config . maxRetries ;
127+ const errorMessage = error instanceof Error ? error . message : 'Unknown error' ;
128+
129+ if ( errorMessage . includes ( '429' ) ) {
130+ // Rate limited - wait longer before retry
131+ const retryDelay = this . config . delayBetweenRequests * attempt * 2 ;
132+ console . log ( ` ⚠️ Rate limited (429). Waiting ${ retryDelay } s before retry ${ attempt } /${ this . config . maxRetries } ` ) ;
133+ await this . delay ( retryDelay ) ;
134+ } else {
135+ console . log ( ` ❌ Failed to fetch balance for wallet ${ wallet . walletId } : ${ errorMessage } ` ) ;
136+ if ( isLastAttempt ) {
137+ return null ;
138+ }
139+ }
140+ }
141+ }
142+
143+ return null ;
144+ }
145+
146+ async processWalletsInBatches ( /** @type {any[] } */ wallets ) {
147+ console . log ( `💰 Fetching balances for ${ wallets . length } wallets with rate limiting...` ) ;
148+ console . log ( `📊 Configuration: batch_size=${ this . config . batchSize } , request_delay=${ this . config . delayBetweenRequests } s, batch_delay=${ this . config . delayBetweenBatches } s` ) ;
149+
150+ const walletBalances = [ ] ;
151+ const totalBatches = Math . ceil ( wallets . length / this . config . batchSize ) ;
152+
153+ for ( let batchIndex = 0 ; batchIndex < totalBatches ; batchIndex ++ ) {
154+ const batchStart = batchIndex * this . config . batchSize ;
155+ const batchEnd = Math . min ( batchStart + this . config . batchSize , wallets . length ) ;
156+ const batchWallets = wallets . slice ( batchStart , batchEnd ) ;
157+
158+ console . log ( `📦 Processing batch ${ batchIndex + 1 } /${ totalBatches } : wallets ${ batchStart + 1 } -${ batchEnd } ` ) ;
159+
160+ for ( let i = 0 ; i < batchWallets . length ; i ++ ) {
161+ const wallet = batchWallets [ i ] ;
162+ if ( ! wallet ) continue ;
163+
164+ console . log ( ` Processing wallet: ${ wallet . walletName } (${ wallet . walletId } )` ) ;
165+
166+ const walletBalance = await this . fetchWalletBalance ( wallet ) ;
167+
168+ if ( walletBalance ) {
169+ walletBalances . push ( walletBalance ) ;
170+ this . results . totalAdaBalance += walletBalance . adaBalance ;
171+ this . results . processedWallets ++ ;
172+ } else {
173+ this . results . failedWallets ++ ;
174+ }
175+
176+ // Delay between requests within a batch (except for the last request)
177+ if ( i < batchWallets . length - 1 ) {
178+ await this . delay ( this . config . delayBetweenRequests ) ;
179+ }
180+ }
181+
182+ // Delay between batches (except for the last batch)
183+ if ( batchIndex < totalBatches - 1 ) {
184+ console . log ( ` ⏳ Waiting ${ this . config . delayBetweenBatches } s before next batch...` ) ;
185+ await this . delay ( this . config . delayBetweenBatches ) ;
186+ }
187+ }
188+
189+ console . log ( `📊 Balance fetching completed. Failed wallets: ${ this . results . failedWallets } ` ) ;
190+ console . log ( `✅ Successfully processed: ${ walletBalances . length } wallets` ) ;
191+
192+ return walletBalances ;
193+ }
194+
195+ async storeSnapshots ( /** @type {any[] } */ walletBalances ) {
196+ console . log ( '💾 Storing balance snapshots...' ) ;
197+
198+ const { data } = await this . makeRequest (
199+ `${ this . config . apiBaseUrl } /api/v1/aggregatedBalances/snapshots` ,
200+ {
201+ method : 'POST' ,
202+ body : JSON . stringify ( { walletBalances } ) ,
203+ }
204+ ) ;
205+
206+ this . results . snapshotsStored = data . snapshotsStored ;
207+ console . log ( `✅ Successfully stored ${ data . snapshotsStored } balance snapshots` ) ;
208+ }
209+
210+ async run ( ) {
211+ const startTime = Date . now ( ) ;
212+
213+ try {
214+ console . log ( '🔄 Starting daily balance snapshot process...' ) ;
215+
216+ // Step 1: Fetch all wallets
217+ const wallets = await this . fetchWallets ( ) ;
218+
219+ if ( wallets . length === 0 ) {
220+ console . log ( 'ℹ️ No wallets to process' ) ;
221+ return this . results ;
222+ }
223+
224+ // Step 2: Process wallets in batches
225+ const walletBalances = await this . processWalletsInBatches ( wallets ) ;
226+
227+ // Step 3: Store snapshots
228+ if ( walletBalances . length > 0 ) {
229+ await this . storeSnapshots ( walletBalances ) ;
230+ }
231+
232+ // Calculate execution time
233+ this . results . executionTime = Math . round ( ( Date . now ( ) - startTime ) / 1000 ) ;
234+
235+ // Final summary
236+ console . log ( '\n🎉 Balance snapshot process completed successfully!' ) ;
237+ console . log ( `📊 Summary:` ) ;
238+ console . log ( ` • Wallets found: ${ this . results . walletsFound } ` ) ;
239+ console . log ( ` • Processed: ${ this . results . processedWallets } ` ) ;
240+ console . log ( ` • Failed: ${ this . results . failedWallets } ` ) ;
241+ console . log ( ` • Snapshots stored: ${ this . results . snapshotsStored } ` ) ;
242+ console . log ( ` • Total TVL: ${ Math . round ( this . results . totalAdaBalance * 100 ) / 100 } ADA` ) ;
243+ console . log ( ` • Execution time: ${ this . results . executionTime } s` ) ;
244+
245+ return this . results ;
246+ } catch ( error ) {
247+ const errorMessage = error instanceof Error ? error . message : 'Unknown error' ;
248+ console . error ( '❌ Balance snapshot process failed:' , errorMessage ) ;
249+ throw error ;
250+ }
251+ }
252+ }
253+
254+ // Main execution
255+ async function main ( ) {
256+ try {
257+ const service = new BalanceSnapshotService ( ) ;
258+ await service . run ( ) ;
259+ process . exit ( 0 ) ;
260+ } catch ( error ) {
261+ console . error ( '❌ Script execution failed:' , error ) ;
262+ process . exit ( 1 ) ;
263+ }
264+ }
265+
266+ // Export for use in other modules
267+ export { BalanceSnapshotService } ;
268+
269+ // Run if this file is executed directly
270+ if ( import . meta. url === `file://${ process . argv [ 1 ] } ` ) {
271+ main ( ) ;
272+ }
0 commit comments