Skip to content

Commit 58ba1a8

Browse files
authored
Merge pull request #150 from MeshJS/feature/get-TVL
Feature/get-TVL
2 parents 73a2728 + 7f222bc commit 58ba1a8

File tree

4 files changed

+146
-83
lines changed

4 files changed

+146
-83
lines changed

.github/workflows/daily-balance-snapshots.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ jobs:
2525
node-version: '18'
2626
cache: 'npm'
2727

28-
- name: Install dependencies
29-
run: npm ci
3028

3129
- name: Install script dependencies
3230
run: |
@@ -44,6 +42,7 @@ jobs:
4442
DELAY_BETWEEN_BATCHES: 10
4543
MAX_RETRIES: 3
4644
REQUEST_TIMEOUT: 45
45+
ENABLE_WARM_UP: true
4746

4847
- name: Notify on failure
4948
if: failure()
@@ -52,7 +51,7 @@ jobs:
5251
echo "❌ Daily balance snapshot job failed"
5352
if [ -n "${{ secrets.SNAPSHOT_ERROR_DISCORD_WEBHOOK_URL }}" ]; then
5453
curl -X POST -H 'Content-type: application/json' \
55-
--data "{\"text\":\"❌ Daily balance snapshots failed. Check the GitHub Actions logs.\"}" \
54+
--data "{\"content\":\"❌ Daily balance snapshots failed. Check the GitHub Actions logs.\"}" \
5655
${{ secrets.SNAPSHOT_ERROR_DISCORD_WEBHOOK_URL }} || echo "Failed to send Discord notification"
5756
else
5857
echo "SNAPSHOT_ERROR_DISCORD_WEBHOOK_URL not configured, skipping notification"

scripts/batch-snapshot-orchestrator.ts

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
* - DELAY_BETWEEN_BATCHES: Delay between batches in seconds (default: 10)
1919
* - MAX_RETRIES: Maximum retries for failed batches (default: 3)
2020
* - REQUEST_TIMEOUT: Request timeout in seconds (default: 45)
21+
* - ENABLE_WARM_UP: Enable API route warm-up to prevent cold start issues (default: true)
2122
*/
2223

2324
interface BatchProgress {
@@ -111,6 +112,7 @@ interface BatchConfig {
111112
delayBetweenBatches: number;
112113
maxRetries: number;
113114
requestTimeout: number; // in seconds
115+
enableWarmUp: boolean; // whether to warm up API route before processing
114116
}
115117

116118
interface ApiResponse<T> {
@@ -167,6 +169,9 @@ class BatchSnapshotOrchestrator {
167169
const delayBetweenBatches = this.parseAndValidateNumber(process.env.DELAY_BETWEEN_BATCHES || '10', 'DELAY_BETWEEN_BATCHES', 1, 300);
168170
const maxRetries = this.parseAndValidateNumber(process.env.MAX_RETRIES || '3', 'MAX_RETRIES', 1, 10);
169171
const requestTimeout = this.parseAndValidateNumber(process.env.REQUEST_TIMEOUT || '45', 'REQUEST_TIMEOUT', 10, 300);
172+
173+
// Parse boolean environment variable for warm-up feature
174+
const enableWarmUp = process.env.ENABLE_WARM_UP !== 'false'; // Default to true unless explicitly disabled
170175

171176
return {
172177
apiBaseUrl,
@@ -175,6 +180,7 @@ class BatchSnapshotOrchestrator {
175180
delayBetweenBatches,
176181
maxRetries,
177182
requestTimeout,
183+
enableWarmUp,
178184
};
179185
}
180186

@@ -211,7 +217,16 @@ class BatchSnapshotOrchestrator {
211217
clearTimeout(timeoutId);
212218

213219
if (!response.ok) {
214-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
220+
// Provide more specific error messages for common cold start issues
221+
if (response.status === 405) {
222+
throw new Error(`HTTP 405: Method Not Allowed - Possible cold start issue`);
223+
} else if (response.status === 503) {
224+
throw new Error(`HTTP 503: Service Unavailable - Server may be starting up`);
225+
} else if (response.status === 502) {
226+
throw new Error(`HTTP 502: Bad Gateway - Upstream server may be cold`);
227+
} else {
228+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
229+
}
215230
}
216231

217232
const data = await response.json() as T;
@@ -228,6 +243,35 @@ class BatchSnapshotOrchestrator {
228243
return new Promise(resolve => setTimeout(resolve, seconds * 1000));
229244
}
230245

246+
private async warmUpApiRoute(): Promise<boolean> {
247+
console.log('🔥 Warming up API route to prevent cold start issues...');
248+
249+
try {
250+
// Make a simple OPTIONS request to warm up the route
251+
const url = new URL(`${this.config.apiBaseUrl}/api/v1/stats/run-snapshots-batch`);
252+
253+
const response = await fetch(url.toString(), {
254+
method: 'OPTIONS',
255+
headers: {
256+
'Authorization': `Bearer ${this.config.authToken}`,
257+
'Content-Type': 'application/json',
258+
},
259+
});
260+
261+
if (response.ok || response.status === 200) {
262+
console.log('✅ API route warmed up successfully');
263+
return true;
264+
} else {
265+
console.log(`⚠️ API route warm-up returned status ${response.status}, but continuing...`);
266+
return true; // Still continue as the route might be ready
267+
}
268+
} catch (error) {
269+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
270+
console.log(`⚠️ API route warm-up failed: ${errorMessage}, but continuing...`);
271+
return true; // Still continue as warm-up is optional
272+
}
273+
}
274+
231275
private getFriendlyErrorName(errorType: string): string {
232276
const errorMap: Record<string, string> = {
233277
'wallet_build_failed': 'Wallet Build Failed',
@@ -308,9 +352,21 @@ class BatchSnapshotOrchestrator {
308352
return null;
309353
}
310354

311-
// For 405 errors (Method Not Allowed), wait longer as it might be a server-side issue
312-
const waitTime = errorMessage.includes('405') ? this.config.delayBetweenBatches * 2 : this.config.delayBetweenBatches;
313-
console.log(` ⏳ Waiting ${waitTime}s before retry...`);
355+
// Calculate wait time with exponential backoff for cold start issues
356+
let waitTime = this.config.delayBetweenBatches;
357+
358+
if (errorMessage.includes('405') || errorMessage.includes('cold start') || errorMessage.includes('503') || errorMessage.includes('502')) {
359+
// Cold start issue - use exponential backoff
360+
waitTime = Math.min(this.config.delayBetweenBatches * Math.pow(2, attempt - 1), 60);
361+
console.log(` 🥶 Cold start detected, using exponential backoff: ${waitTime}s`);
362+
} else if (errorMessage.includes('timeout')) {
363+
// Timeout issue - wait longer
364+
waitTime = this.config.delayBetweenBatches * 2;
365+
console.log(` ⏰ Timeout detected, waiting longer: ${waitTime}s`);
366+
} else {
367+
console.log(` ⏳ Standard retry delay: ${waitTime}s`);
368+
}
369+
314370
await this.delay(waitTime);
315371
}
316372
}
@@ -326,6 +382,15 @@ class BatchSnapshotOrchestrator {
326382
console.log('🔄 Starting batch snapshot orchestration...');
327383
console.log(`📊 Configuration: batch_size=${this.config.batchSize}, delay=${this.config.delayBetweenBatches}s`);
328384

385+
// Warm up the API route to prevent cold start issues (if enabled)
386+
if (this.config.enableWarmUp) {
387+
await this.warmUpApiRoute();
388+
// Small delay after warm-up to ensure route is fully ready
389+
await this.delay(2);
390+
} else {
391+
console.log('🔥 Warm-up disabled via ENABLE_WARM_UP=false');
392+
}
393+
329394
// First, get the total number of batches by processing batch 1
330395
console.log('📋 Determining total batches...');
331396
const firstBatch = await this.processBatch(1, batchId);

src/pages/api/v1/stats/README.md

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,7 @@ The endpoint requires authentication using the `SNAPSHOT_AUTH_TOKEN` environment
2424
- **Method**: POST
2525
- **Purpose**: Processes a batch of wallets for balance snapshots (main endpoint)
2626
- **Authentication**: Required (Bearer token)
27-
- **Content-Type**: `application/json`
28-
- **Body**:
29-
```json
30-
{
31-
"batchId": "string",
32-
"batchNumber": number,
33-
"batchSize": number
34-
}
35-
```
27+
- **Parameters**: passed via query string (no request body)
3628
- **Query Parameters**:
3729
- `batchId`: Unique identifier for the batch session
3830
- `batchNumber`: Current batch number (1-based, must be ≥ 1)
@@ -74,7 +66,12 @@ The endpoint requires authentication using the `SNAPSHOT_AUTH_TOKEN` environment
7466
"isArchived": boolean,
7567
"verified": number,
7668
"hasDRepKeys": boolean,
77-
"hasClarityApiKey": boolean
69+
"scriptCborLength": number,
70+
"stakeCredentialLength": number,
71+
"signersAddressesLength": number,
72+
"signersStakeKeysLength": number,
73+
"signersDRepKeysLength": number,
74+
"signersDescriptionsLength": number
7875
}
7976
}
8077
]
@@ -83,6 +80,14 @@ The endpoint requires authentication using the `SNAPSHOT_AUTH_TOKEN` environment
8380
}
8481
```
8582

83+
#### Example (curl)
84+
85+
```bash
86+
curl -X POST \
87+
"$API_BASE_URL/api/v1/stats/run-snapshots-batch?batchId=snapshot-$(date +%s)&batchNumber=1&batchSize=5" \
88+
-H "Authorization: Bearer $SNAPSHOT_AUTH_TOKEN"
89+
```
90+
8691
## Batch Processing System
8792

8893
The new system processes wallets in small batches to avoid timeout issues:
@@ -94,6 +99,8 @@ The new system processes wallets in small batches to avoid timeout issues:
9499
4. **Fault Tolerant**: Failed batches can be retried individually
95100
5. **Input Validation**: Comprehensive validation for batch parameters
96101
6. **Error Tracking**: Detailed error reporting with wallet structure information
102+
7. **Network Fallback**: If no UTxOs are found on the inferred network, the opposite network is tried
103+
8. **Wallet Build Strategy**: Uses ordered keys via `MultisigWallet` when `signersStakeKeys` exist; otherwise falls back to legacy `buildWallet`
97104

98105
### Orchestrator Script
99106
The `scripts/batch-snapshot-orchestrator.ts` script manages the entire process:
@@ -112,7 +119,7 @@ The `scripts/batch-snapshot-orchestrator.ts` script manages the entire process:
112119
- **`BATCH_SIZE`**: Wallets per batch (default: 5, range: 1-5)
113120
- **`DELAY_BETWEEN_BATCHES`**: Seconds between batches (default: 10)
114121
- **`MAX_RETRIES`**: Retry attempts for failed batches (default: 3)
115-
- **`REQUEST_TIMEOUT`**: Request timeout in seconds (default: 60)
122+
- **`REQUEST_TIMEOUT`**: Request timeout in seconds (default: 45)
116123

117124
## GitHub Actions Integration
118125

@@ -182,11 +189,13 @@ The orchestrator will:
182189

183190
### Type Safety & Validation
184191
- **Fixed Decimal Type**: Proper handling of Decimal types in database operations
185-
- **Input Validation**: Comprehensive validation for batch parameters (batch number ≥ 1, batch size 1-100)
192+
- **Input Validation**: Comprehensive validation for batch parameters (batch number ≥ 1, batch size 1-5)
186193
- **Error Tracking**: Enhanced error handling with detailed wallet structure information
187194

188195
### Configuration & Reliability
189196
- **Configurable Timeouts**: Request timeout now configurable via `REQUEST_TIMEOUT` environment variable
190197
- **Enhanced Error Handling**: UTxO fetch failures are now properly tracked and reported
191198
- **Network-Specific Reporting**: Separate tracking for mainnet and testnet wallets and balances
192-
- **Improved Documentation**: Updated documentation to reflect all recent changes
199+
- **Improved Documentation**: Updated documentation to reflect all recent changes
200+
- **Network Fallback**: Attempts the opposite network if no UTxOs are found
201+
- **Wallet Build Logic**: Uses ordered keys with `MultisigWallet` when stake keys are available, with legacy fallback (no stake keys)

src/pages/api/v1/stats/run-snapshots-batch.ts

Lines changed: 53 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { cors, addCorsCacheBustingHeaders } from "@/lib/cors";
22
import type { NextApiRequest, NextApiResponse } from "next";
33
import { db } from "@/server/db";
4-
import { buildMultisigWallet } from "@/utils/common";
4+
import { buildWallet } from "@/utils/common";
5+
import { MultisigWallet, type MultisigKey } from "@/utils/multisigSDK";
56
import { getProvider } from "@/utils/get-provider";
6-
import { resolvePaymentKeyHash, serializeNativeScript } from "@meshsdk/core";
7-
import type { UTxO, NativeScript } from "@meshsdk/core";
7+
import { resolvePaymentKeyHash, resolveStakeKeyHash, type UTxO } from "@meshsdk/core";
88
import { getBalance } from "@/utils/getBalance";
99
import { addressToNetwork } from "@/utils/multisigSDK";
1010
import type { Wallet as DbWallet } from "@prisma/client";
@@ -259,76 +259,76 @@ export default async function handler(
259259
try {
260260
console.log(` Processing wallet: (${wallet.id.slice(0, 8)}...)`);
261261

262-
// Determine network from signer addresses
262+
// Determine network from signer addresses, fallback to signer stake keys
263263
let network = 1; // Default to mainnet
264264
if (wallet.signersAddresses.length > 0) {
265265
const signerAddr = wallet.signersAddresses[0]!;
266266
network = addressToNetwork(signerAddr);
267+
} else if (wallet.signersStakeKeys && wallet.signersStakeKeys.length > 0) {
268+
const stakeAddr = wallet.signersStakeKeys.find((s) => !!s);
269+
if (stakeAddr) {
270+
network = addressToNetwork(stakeAddr);
271+
}
267272
}
268273

269-
// Build multisig wallet for address determination
270-
const walletData = {
271-
id: wallet.id,
272-
name: wallet.name,
273-
signersAddresses: wallet.signersAddresses,
274-
numRequiredSigners: wallet.numRequiredSigners!,
275-
type: wallet.type || "atLeast",
276-
stakeCredentialHash: wallet.stakeCredentialHash,
277-
isArchived: wallet.isArchived,
278-
description: wallet.description,
279-
signersStakeKeys: wallet.signersStakeKeys,
280-
signersDRepKeys: wallet.signersDRepKeys,
281-
signersDescriptions: wallet.signersDescriptions,
282-
clarityApiKey: wallet.clarityApiKey,
283-
drepKey: null,
284-
scriptType: null,
285-
scriptCbor: wallet.scriptCbor,
286-
verified: wallet.verified,
287-
};
288-
289-
const mWallet = buildMultisigWallet(walletData, network);
290-
if (!mWallet) {
291-
console.error(`Failed to build multisig wallet for ${wallet.id.slice(0, 8)}...`);
274+
// Build wallet conditionally: use MultisigSDK ordering if signersStakeKeys exist
275+
let walletAddress: string;
276+
try {
277+
const hasStakeKeys = !!(wallet.signersStakeKeys && wallet.signersStakeKeys.length > 0);
278+
if (hasStakeKeys) {
279+
// Build MultisigSDK wallet with ordered keys
280+
const keys: MultisigKey[] = [];
281+
wallet.signersAddresses.forEach((addr: string, i: number) => {
282+
if (!addr) return;
283+
try {
284+
keys.push({ keyHash: resolvePaymentKeyHash(addr), role: 0, name: wallet.signersDescriptions[i] || "" });
285+
} catch {}
286+
});
287+
wallet.signersStakeKeys?.forEach((stakeKey: string, i: number) => {
288+
if (!stakeKey) return;
289+
try {
290+
keys.push({ keyHash: resolveStakeKeyHash(stakeKey), role: 2, name: wallet.signersDescriptions[i] || "" });
291+
} catch {}
292+
});
293+
if (keys.length === 0 && !wallet.stakeCredentialHash) {
294+
throw new Error("No valid keys or stakeCredentialHash provided");
295+
}
296+
const mWallet = new MultisigWallet(
297+
wallet.name,
298+
keys,
299+
wallet.description ?? "",
300+
wallet.numRequiredSigners ?? 1,
301+
network,
302+
wallet.stakeCredentialHash as undefined | string,
303+
(wallet.type as any) || "atLeast"
304+
);
305+
walletAddress = mWallet.getScript().address;
306+
} else {
307+
// Fallback: build the wallet without enforcing key ordering (legacy payment-script build)
308+
const builtWallet = buildWallet(wallet, network);
309+
walletAddress = builtWallet.address;
310+
}
311+
} catch (error) {
312+
const errorMessage = error instanceof Error ? error.message : 'Unknown wallet build error';
313+
console.error(`Failed to build wallet for ${wallet.id.slice(0, 8)}...:`, errorMessage);
314+
292315
failures.push({
293316
walletId: wallet.id.slice(0, 8),
294317
errorType: "wallet_build_failed",
295-
errorMessage: "Unable to build multisig wallet from provided data",
318+
errorMessage: "Unable to build wallet from provided data",
296319
walletStructure: getWalletStructure(wallet)
297320
});
298321
failedInBatch++;
299322
continue;
300323
}
301324

302-
// Generate addresses from the built wallet
303-
const nativeScript = {
304-
type: wallet.type || "atLeast",
305-
scripts: wallet.signersAddresses.map((addr: string) => ({
306-
type: "sig",
307-
keyHash: resolvePaymentKeyHash(addr),
308-
})),
309-
};
310-
if (nativeScript.type == "atLeast") {
311-
//@ts-ignore
312-
nativeScript.required = wallet.numRequiredSigners!;
313-
}
314-
315-
const paymentAddress = serializeNativeScript(
316-
nativeScript as NativeScript,
317-
wallet.stakeCredentialHash as undefined | string,
318-
network,
319-
).address;
320-
321-
const stakeableAddress = mWallet.getScript().address;
322-
323325
// Determine which address to use
324326
const blockchainProvider = getProvider(network);
325327

326-
let paymentUtxos: UTxO[] = [];
327-
let stakeableUtxos: UTxO[] = [];
328+
let utxos: UTxO[] = [];
328329

329330
try {
330-
paymentUtxos = await blockchainProvider.fetchAddressUTxOs(paymentAddress);
331-
stakeableUtxos = await blockchainProvider.fetchAddressUTxOs(stakeableAddress);
331+
utxos = await blockchainProvider.fetchAddressUTxOs(walletAddress);
332332
} catch (utxoError) {
333333
const errorMessage = utxoError instanceof Error ? utxoError.message : 'Unknown UTxO fetch error';
334334
console.error(`Failed to fetch UTxOs for wallet ${wallet.id.slice(0, 8)}...:`, errorMessage);
@@ -343,16 +343,6 @@ export default async function handler(
343343
failedInBatch++;
344344
continue;
345345
}
346-
347-
const paymentAddrEmpty = paymentUtxos.length === 0;
348-
let walletAddress = paymentAddress;
349-
350-
if (paymentAddrEmpty && mWallet.stakingEnabled()) {
351-
walletAddress = stakeableAddress;
352-
}
353-
354-
// Use the UTxOs from the selected address
355-
let utxos: UTxO[] = walletAddress === stakeableAddress ? stakeableUtxos : paymentUtxos;
356346

357347
// If we still have no UTxOs, try the other network as fallback
358348
if (utxos.length === 0) {

0 commit comments

Comments
 (0)