Skip to content

Commit 12e6679

Browse files
committed
feat(aggregate-balances): endpoint to fetch all wallet balances
- Get all wallets from db - Generate all addresses - Fetch all balances
1 parent bbbfa3a commit 12e6679

File tree

1 file changed

+230
-0
lines changed

1 file changed

+230
-0
lines changed
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import { cors, addCorsCacheBustingHeaders } from "@/lib/cors";
2+
import type { Wallet as DbWallet } from "@prisma/client";
3+
import type { NextApiRequest, NextApiResponse } from "next";
4+
import { buildMultisigWallet } from "@/utils/common";
5+
import { getProvider } from "@/utils/get-provider";
6+
import { addressToNetwork } from "@/utils/multisigSDK";
7+
import type { UTxO, NativeScript } from "@meshsdk/core";
8+
import { resolvePaymentKeyHash, serializeNativeScript } from "@meshsdk/core";
9+
import { db } from "@/server/db";
10+
import { getBalance } from "@/utils/getBalance";
11+
12+
interface WalletBalance {
13+
walletId: string;
14+
walletName: string;
15+
address: string;
16+
balance: Record<string, string>;
17+
adaBalance: number;
18+
isArchived: boolean;
19+
}
20+
21+
interface TVLResponse {
22+
totalValueLocked: {
23+
ada: number;
24+
assets: Record<string, string>;
25+
};
26+
walletCount: number;
27+
activeWalletCount: number;
28+
archivedWalletCount: number;
29+
walletBalances: WalletBalance[];
30+
}
31+
32+
export default async function handler(
33+
req: NextApiRequest,
34+
res: NextApiResponse<TVLResponse | { error: string }>,
35+
) {
36+
// Add cache-busting headers for CORS
37+
addCorsCacheBustingHeaders(res);
38+
39+
await cors(req, res);
40+
if (req.method === "OPTIONS") {
41+
return res.status(200).end();
42+
}
43+
if (req.method !== "GET") {
44+
return res.status(405).json({ error: "Method Not Allowed" });
45+
}
46+
47+
try {
48+
// Get ALL wallets from the database for TVL calculation
49+
const allWallets: DbWallet[] = await db.wallet.findMany();
50+
51+
if (!allWallets || allWallets.length === 0) {
52+
return res.status(200).json({
53+
totalValueLocked: {
54+
ada: 0,
55+
assets: {},
56+
},
57+
walletCount: 0,
58+
activeWalletCount: 0,
59+
archivedWalletCount: 0,
60+
walletBalances: [],
61+
});
62+
}
63+
64+
const walletBalances: WalletBalance[] = [];
65+
const totalAssets: Record<string, number> = {};
66+
let totalAdaBalance = 0;
67+
let activeWalletCount = 0;
68+
let archivedWalletCount = 0;
69+
70+
// Process each wallet
71+
for (const wallet of allWallets) {
72+
try {
73+
// Determine network from signer addresses
74+
let network = 1; // Default to mainnet
75+
if (wallet.signersAddresses.length > 0) {
76+
const signerAddr = wallet.signersAddresses[0]!;
77+
network = addressToNetwork(signerAddr);
78+
console.log(`Network detection for wallet ${wallet.id}:`, {
79+
signerAddress: signerAddr,
80+
detectedNetwork: network,
81+
isTestnet: signerAddr.includes("test")
82+
});
83+
}
84+
85+
const mWallet = buildMultisigWallet(wallet, network);
86+
if (!mWallet) {
87+
console.warn(`Failed to build multisig wallet for wallet ${wallet.id}`);
88+
continue;
89+
}
90+
91+
// Use the same address logic as the frontend buildWallet function
92+
const nativeScript = {
93+
type: wallet.type ? wallet.type : "atLeast",
94+
scripts: wallet.signersAddresses.map((addr) => ({
95+
type: "sig",
96+
keyHash: resolvePaymentKeyHash(addr),
97+
})),
98+
};
99+
if (nativeScript.type == "atLeast") {
100+
//@ts-ignore
101+
nativeScript.required = wallet.numRequiredSigners!;
102+
}
103+
104+
const paymentAddress = serializeNativeScript(
105+
nativeScript as NativeScript,
106+
wallet.stakeCredentialHash as undefined | string,
107+
network,
108+
).address;
109+
110+
let walletAddress = paymentAddress;
111+
const stakeableAddress = mWallet.getScript().address;
112+
113+
// Check if payment address is empty and use stakeable address if staking is enabled
114+
// We'll fetch UTxOs for both addresses to determine which one to use
115+
const blockchainProvider = getProvider(network);
116+
117+
let paymentUtxos: UTxO[] = [];
118+
let stakeableUtxos: UTxO[] = [];
119+
120+
try {
121+
paymentUtxos = await blockchainProvider.fetchAddressUTxOs(paymentAddress);
122+
stakeableUtxos = await blockchainProvider.fetchAddressUTxOs(stakeableAddress);
123+
} catch (utxoError) {
124+
console.error(`Failed to fetch UTxOs for wallet ${wallet.id}:`, utxoError);
125+
// Continue with empty UTxOs
126+
}
127+
128+
const paymentAddrEmpty = paymentUtxos.length === 0;
129+
if (paymentAddrEmpty && mWallet.stakingEnabled()) {
130+
walletAddress = stakeableAddress;
131+
}
132+
133+
console.log(`Processing wallet ${wallet.id}:`, {
134+
walletName: wallet.name,
135+
signerAddresses: wallet.signersAddresses,
136+
network,
137+
paymentAddress,
138+
stakeableAddress,
139+
selectedAddress: walletAddress,
140+
paymentUtxos: paymentUtxos.length,
141+
stakeableUtxos: stakeableUtxos.length,
142+
});
143+
144+
// Use the UTxOs from the selected address
145+
let utxos: UTxO[] = walletAddress === stakeableAddress ? stakeableUtxos : paymentUtxos;
146+
147+
// If we still have no UTxOs, try the other network as fallback
148+
if (utxos.length === 0) {
149+
const fallbackNetwork = network === 0 ? 1 : 0;
150+
try {
151+
const fallbackProvider = getProvider(fallbackNetwork);
152+
utxos = await fallbackProvider.fetchAddressUTxOs(walletAddress);
153+
console.log(`Successfully fetched ${utxos.length} UTxOs for wallet ${wallet.id} on fallback network ${fallbackNetwork}`);
154+
} catch (fallbackError) {
155+
console.error(`Failed to fetch UTxOs for wallet ${wallet.id} on fallback network ${fallbackNetwork}:`, fallbackError);
156+
// Continue with empty UTxOs - this wallet will show 0 balance
157+
}
158+
}
159+
160+
// Get balance for this wallet
161+
const balance = getBalance(utxos);
162+
163+
// Calculate ADA balance
164+
const adaBalance = balance.lovelace ? parseInt(balance.lovelace) / 1000000 : 0;
165+
const roundedAdaBalance = Math.round(adaBalance * 100) / 100;
166+
167+
// Count wallet types
168+
if (wallet.isArchived) {
169+
archivedWalletCount++;
170+
} else {
171+
activeWalletCount++;
172+
}
173+
174+
// Add to wallet balances
175+
walletBalances.push({
176+
walletId: wallet.id,
177+
walletName: wallet.name,
178+
address: walletAddress,
179+
balance,
180+
adaBalance: roundedAdaBalance,
181+
isArchived: wallet.isArchived,
182+
});
183+
184+
// Aggregate total balances
185+
totalAdaBalance += roundedAdaBalance;
186+
187+
// Aggregate all assets
188+
Object.entries(balance).forEach(([asset, amount]) => {
189+
const numericAmount = parseFloat(amount);
190+
if (totalAssets[asset]) {
191+
totalAssets[asset] += numericAmount;
192+
} else {
193+
totalAssets[asset] = numericAmount;
194+
}
195+
});
196+
197+
} catch (error) {
198+
console.error(`Error processing wallet ${wallet.id}:`, error);
199+
// Continue with other wallets even if one fails
200+
}
201+
}
202+
203+
// Convert total assets back to string format
204+
const totalAssetsString = Object.fromEntries(
205+
Object.entries(totalAssets).map(([key, value]) => [
206+
key,
207+
value.toString(),
208+
]),
209+
);
210+
211+
const response: TVLResponse = {
212+
totalValueLocked: {
213+
ada: Math.round(totalAdaBalance * 100) / 100,
214+
assets: totalAssetsString,
215+
},
216+
walletCount: allWallets.length,
217+
activeWalletCount,
218+
archivedWalletCount,
219+
walletBalances,
220+
};
221+
222+
res.status(200).json(response);
223+
} catch (error) {
224+
console.error("Error in aggregatedBalances handler", {
225+
message: (error as Error)?.message,
226+
stack: (error as Error)?.stack,
227+
});
228+
res.status(500).json({ error: "Internal Server Error" });
229+
}
230+
}

0 commit comments

Comments
 (0)