Skip to content

Commit e6c35ab

Browse files
committed
feat(balance-snapshot): add BalanceSnapshot model and remove aggregatedBalances endpoint
- Introduced BalanceSnapshot model to track wallet balances over time. - Removed the aggregatedBalances API endpoint as it is no longer needed.
1 parent 12e6679 commit e6c35ab

File tree

9 files changed

+1083
-230
lines changed

9 files changed

+1083
-230
lines changed
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
name: Daily Balance Snapshots
2+
3+
# This workflow takes daily snapshots of wallet balances and stores them in the database.
4+
# API requests require SNAPSHOT_AUTH_TOKEN secret to be set in GitHub repository settings.
5+
6+
on:
7+
#schedule:
8+
# Run at midnight UTC every day
9+
#- cron: '0 0 * * *'
10+
# Allow manual triggering for testing
11+
workflow_dispatch:
12+
13+
jobs:
14+
snapshot-balances:
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- name: Take balance snapshots
19+
run: |
20+
# Configuration - adjust these values based on your needs
21+
API_BASE_URL="https://multisig.meshjs.dev"
22+
AUTH_TOKEN="${{ secrets.SNAPSHOT_AUTH_TOKEN }}"
23+
24+
# Rate limiting configuration
25+
BATCH_SIZE=3 # Wallets per batch (adjust based on Blockfrost plan)
26+
DELAY_BETWEEN_REQUESTS=3 # Seconds between requests (20/min = 3s)
27+
DELAY_BETWEEN_BATCHES=15 # Seconds between batches
28+
MAX_RETRIES=3 # Max retries for failed requests
29+
REQUEST_TIMEOUT=30 # Request timeout in seconds
30+
31+
echo "🔄 Starting daily balance snapshot process..."
32+
echo "📊 Configuration: batch_size=$BATCH_SIZE, request_delay=${DELAY_BETWEEN_REQUESTS}s, batch_delay=${DELAY_BETWEEN_BATCHES}s"
33+
34+
# Step 1: Get all wallets
35+
echo "📋 Fetching all wallets..."
36+
wallets_response=$(curl -s -w "\n%{http_code}" \
37+
-H "Authorization: Bearer $AUTH_TOKEN" \
38+
"$API_BASE_URL/api/v1/aggregatedBalances/wallets")
39+
40+
wallets_http_code=$(echo "$wallets_response" | tail -n1)
41+
wallets_body=$(echo "$wallets_response" | head -n -1)
42+
43+
echo "Wallets HTTP Status: $wallets_http_code"
44+
45+
if [ "$wallets_http_code" -ne 200 ]; then
46+
echo "❌ Failed to fetch wallets. HTTP Status: $wallets_http_code"
47+
echo "Response: $wallets_body"
48+
exit 1
49+
fi
50+
51+
# Extract wallet data
52+
wallet_count=$(echo "$wallets_body" | jq -r '.walletCount')
53+
echo "✅ Found $wallet_count wallets"
54+
55+
if [ "$wallet_count" -eq 0 ]; then
56+
echo "ℹ️ No wallets found, skipping snapshot process"
57+
exit 0
58+
fi
59+
60+
# Step 2: Get balances for each wallet with rate limiting
61+
echo "💰 Fetching balances for each wallet with rate limiting..."
62+
63+
# Create temporary files for collecting results
64+
temp_balances="/tmp/wallet_balances.json"
65+
temp_failed="/tmp/failed_wallets.txt"
66+
echo "[]" > "$temp_balances"
67+
echo "0" > "$temp_failed"
68+
69+
# Process wallets in batches to respect rate limits
70+
batch_size=$BATCH_SIZE
71+
delay_between_requests=$DELAY_BETWEEN_REQUESTS
72+
delay_between_batches=$DELAY_BETWEEN_BATCHES
73+
74+
# Convert wallets to array for batch processing
75+
wallets_array=$(echo "$wallets_body" | jq -r '.wallets')
76+
total_wallets=$(echo "$wallets_array" | jq 'length')
77+
echo "Processing $total_wallets wallets in batches of $batch_size"
78+
79+
# Process wallets in batches
80+
for ((i=0; i<total_wallets; i+=batch_size)); do
81+
batch_end=$((i + batch_size))
82+
if [ $batch_end -gt $total_wallets ]; then
83+
batch_end=$total_wallets
84+
fi
85+
86+
echo "📦 Processing batch $((i/batch_size + 1)): wallets $((i+1))-$batch_end"
87+
88+
# Process each wallet in the current batch
89+
for ((j=i; j<batch_end; j++)); do
90+
wallet=$(echo "$wallets_array" | jq -r ".[$j]")
91+
wallet_id=$(echo "$wallet" | jq -r '.walletId')
92+
wallet_name=$(echo "$wallet" | jq -r '.walletName')
93+
94+
echo " Processing wallet: $wallet_name ($wallet_id)"
95+
96+
# Build query parameters
97+
query_params="walletId=$(echo "$wallet" | jq -r '.walletId')"
98+
query_params="$query_params&walletName=$(echo "$wallet" | jq -r '.walletName')"
99+
query_params="$query_params&signersAddresses=$(echo "$wallet" | jq -r '.signersAddresses | @json')"
100+
query_params="$query_params&numRequiredSigners=$(echo "$wallet" | jq -r '.numRequiredSigners')"
101+
query_params="$query_params&type=$(echo "$wallet" | jq -r '.type')"
102+
query_params="$query_params&stakeCredentialHash=$(echo "$wallet" | jq -r '.stakeCredentialHash // ""')"
103+
query_params="$query_params&isArchived=$(echo "$wallet" | jq -r '.isArchived')"
104+
query_params="$query_params&network=$(echo "$wallet" | jq -r '.network')"
105+
query_params="$query_params&paymentAddress=$(echo "$wallet" | jq -r '.paymentAddress')"
106+
query_params="$query_params&stakeableAddress=$(echo "$wallet" | jq -r '.stakeableAddress')"
107+
108+
# Fetch balance for this wallet with retry logic
109+
max_retries=$MAX_RETRIES
110+
retry_count=0
111+
success=false
112+
113+
while [ $retry_count -lt $max_retries ] && [ "$success" = false ]; do
114+
balance_response=$(curl -s -w "\n%{http_code}" \
115+
--max-time $REQUEST_TIMEOUT \
116+
--connect-timeout 10 \
117+
-H "Authorization: Bearer $AUTH_TOKEN" \
118+
"$API_BASE_URL/api/v1/aggregatedBalances/balance?$query_params")
119+
120+
balance_http_code=$(echo "$balance_response" | tail -n1)
121+
balance_body=$(echo "$balance_response" | head -n -1)
122+
123+
if [ "$balance_http_code" -eq 200 ]; then
124+
wallet_balance=$(echo "$balance_body" | jq -r '.walletBalance')
125+
echo " ✅ Balance: $(echo "$wallet_balance" | jq -r '.adaBalance') ADA"
126+
127+
# Add to balances array
128+
current_balances=$(cat "$temp_balances")
129+
updated_balances=$(echo "$current_balances" | jq ". + [$(echo "$wallet_balance" | jq -c .)]")
130+
echo "$updated_balances" > "$temp_balances"
131+
132+
success=true
133+
elif [ "$balance_http_code" -eq 429 ]; then
134+
# Rate limited - wait longer before retry
135+
retry_delay=$((delay_between_requests * (retry_count + 1) * 2))
136+
echo " ⚠️ Rate limited (429). Waiting ${retry_delay}s before retry $((retry_count + 1))/$max_retries"
137+
sleep $retry_delay
138+
retry_count=$((retry_count + 1))
139+
else
140+
echo " ❌ Failed to fetch balance for wallet $wallet_id: $balance_http_code"
141+
failed_count=$(cat "$temp_failed")
142+
echo $((failed_count + 1)) > "$temp_failed"
143+
success=true # Don't retry on non-rate-limit errors
144+
fi
145+
done
146+
147+
if [ "$success" = false ]; then
148+
echo " ❌ Max retries exceeded for wallet $wallet_id"
149+
failed_count=$(cat "$temp_failed")
150+
echo $((failed_count + 1)) > "$temp_failed"
151+
fi
152+
153+
# Delay between requests within a batch
154+
if [ $((j+1)) -lt $batch_end ]; then
155+
sleep $delay_between_requests
156+
fi
157+
done
158+
159+
# Delay between batches (except for the last batch)
160+
if [ $batch_end -lt $total_wallets ]; then
161+
echo " ⏳ Waiting ${delay_between_batches}s before next batch..."
162+
sleep $delay_between_batches
163+
fi
164+
done
165+
166+
# Read final results
167+
wallet_balances=$(cat "$temp_balances")
168+
failed_wallets=$(cat "$temp_failed")
169+
170+
echo "📊 Balance fetching completed. Failed wallets: $failed_wallets"
171+
echo "✅ Successfully processed: $(echo "$wallet_balances" | jq 'length') wallets"
172+
173+
# Step 3: Store snapshots using the collected balances
174+
echo "💾 Storing balance snapshots..."
175+
snapshots_response=$(curl -s -w "\n%{http_code}" \
176+
-H "Authorization: Bearer $AUTH_TOKEN" \
177+
-H "Content-Type: application/json" \
178+
-d "{\"walletBalances\": $wallet_balances}" \
179+
"$API_BASE_URL/api/v1/aggregatedBalances/snapshots")
180+
181+
snapshots_http_code=$(echo "$snapshots_response" | tail -n1)
182+
snapshots_body=$(echo "$snapshots_response" | head -n -1)
183+
184+
echo "Snapshots HTTP Status: $snapshots_http_code"
185+
echo "Response: $snapshots_body"
186+
187+
# Check if the request was successful
188+
if [ "$snapshots_http_code" -eq 200 ]; then
189+
# Parse the response to get the number of snapshots stored
190+
snapshots_stored=$(echo "$snapshots_body" | jq -r '.snapshotsStored // 0')
191+
total_tvl=$(echo "$snapshots_body" | jq -r '.totalValueLocked.ada')
192+
echo "✅ Successfully stored $snapshots_stored balance snapshots"
193+
echo "💰 Total Value Locked: $total_tvl ADA"
194+
195+
# Optional: Send notification on success (you can add Discord/Slack webhook here)
196+
# curl -X POST -H 'Content-type: application/json' \
197+
# --data "{\"text\":\"✅ Daily balance snapshots completed: $snapshots_stored snapshots stored, TVL: $total_tvl ADA\"}" \
198+
# ${{ secrets.DISCORD_WEBHOOK_URL }}
199+
else
200+
echo "❌ Failed to store balance snapshots. HTTP Status: $snapshots_http_code"
201+
echo "Response: $snapshots_body"
202+
exit 1
203+
fi
204+
205+
- name: Notify on failure
206+
if: failure()
207+
run: |
208+
echo "❌ Daily balance snapshot job failed"
209+
# Optional: Send failure notification
210+
# curl -X POST -H 'Content-type: application/json' \
211+
# --data "{\"text\":\"❌ Daily balance snapshots failed. Check the GitHub Actions logs.\"}" \
212+
# ${{ secrets.DISCORD_WEBHOOK_URL }}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
-- AlterTable
2+
ALTER TABLE "NewWallet" ADD COLUMN "stakeCredentialHash" TEXT;
3+
4+
-- CreateTable
5+
CREATE TABLE "BalanceSnapshot" (
6+
"id" TEXT NOT NULL,
7+
"walletId" TEXT NOT NULL,
8+
"walletName" TEXT NOT NULL,
9+
"address" TEXT NOT NULL,
10+
"adaBalance" DECIMAL(65,30) NOT NULL,
11+
"assetBalances" JSONB NOT NULL,
12+
"isArchived" BOOLEAN NOT NULL,
13+
"snapshotDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
14+
15+
CONSTRAINT "BalanceSnapshot_pkey" PRIMARY KEY ("id")
16+
);
17+
18+
-- CreateIndex
19+
CREATE INDEX "BalanceSnapshot_snapshotDate_idx" ON "BalanceSnapshot"("snapshotDate");
20+
21+
-- CreateIndex
22+
CREATE INDEX "BalanceSnapshot_walletId_idx" ON "BalanceSnapshot"("walletId");

prisma/schema.prisma

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,14 @@ model Ballot {
9797
type Int
9898
createdAt DateTime @default(now())
9999
}
100+
101+
model BalanceSnapshot {
102+
id String @id @default(cuid())
103+
walletId String
104+
walletName String
105+
address String
106+
adaBalance Decimal
107+
assetBalances Json
108+
isArchived Boolean
109+
snapshotDate DateTime @default(now())
110+
}

0 commit comments

Comments
 (0)