|
| 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