getPortfolio
Retrieve a wallet's complete asset portfolio including native and ERC-20 tokens with USD values and allocation percentages across multiple EVM chains.
Instructions
지갑의 전체 자산 포트폴리오(네이티브 + ERC-20 토큰, USD 가치, 비율)를 한 번에 조회합니다
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| address | Yes | 조회할 지갑 주소 (0x...) | |
| chain | No | EVM 체인 | ethereum |
Implementation Reference
- src/tools/getPortfolio.ts:52-164 (handler)The main handler function for the getPortfolio tool. It fetches native and ERC-20 balances for a given address on a specific chain, calculates USD values, and structures the response data.
async function handler(args: z.infer<typeof inputSchema>): Promise<ToolResult<PortfolioData>> { const { address, chain } = args; if (!isAddress(address)) { return makeError(`Invalid address: ${address}`, "INVALID_INPUT"); } const cacheKey = `portfolio:${chain}:${address}`; const cached = cache.get<PortfolioData>(cacheKey); if (cached.hit) return makeSuccess(chain, cached.data, true); try { const client = getClient(chain); const chainConfig = chains[chain]; const assets: PortfolioAsset[] = []; // 네이티브 토큰 잔고 조회 const nativeBalanceWei = await client.getBalance({ address: address as Address }); const nativeFormatted = formatUnits(nativeBalanceWei, 18); const nativeSymbol = chainConfig?.nativeCurrency?.symbol ?? "ETH"; const nativeName = chainConfig?.nativeCurrency?.name ?? "Ether"; let nativeValueUsd = 0; try { const nativeId = chainConfig?.nativeCurrency?.coingeckoId; if (nativeId) { const price = await getPrice(nativeId); nativeValueUsd = parseFloat(nativeFormatted) * price.priceUsd; } } catch { // 가격 조회 실패 무시 } if (nativeBalanceWei > 0n) { assets.push({ symbol: nativeSymbol, name: nativeName, balance: nativeFormatted, balanceUsd: Math.round(nativeValueUsd * 100) / 100, percentage: 0, // 나중에 계산 type: "native", }); } // 해당 체인에 주소가 있는 모든 토큰의 잔고 조회 for (const tokenInfo of tokensData) { const tokenAddress = tokenInfo.addresses[chain]; if (!tokenAddress) continue; try { const balance = await client.readContract({ address: tokenAddress as Address, abi: ERC20_BALANCE_ABI, functionName: "balanceOf", args: [address as Address], }); // 잔고가 0인 토큰은 건너뜀 if (balance === 0n) continue; const formatted = formatUnits(balance, tokenInfo.decimals); let valueUsd = 0; try { const price = await getPrice(tokenInfo.coingeckoId); valueUsd = parseFloat(formatted) * price.priceUsd; } catch { // 가격 조회 실패 무시 } assets.push({ symbol: tokenInfo.symbol, name: tokenInfo.name, balance: formatted, balanceUsd: Math.round(valueUsd * 100) / 100, percentage: 0, // 나중에 계산 type: "erc20", address: tokenAddress, }); } catch { // 개별 토큰 조회 실패는 건너뜀 } } // balanceUsd 내림차순 정렬 assets.sort((a, b) => b.balanceUsd - a.balanceUsd); // 총 포트폴리오 가치 합산 const totalValueUsd = Math.round(assets.reduce((sum, a) => sum + a.balanceUsd, 0) * 100) / 100; // 비율 계산 if (totalValueUsd > 0) { for (const asset of assets) { asset.percentage = Math.round((asset.balanceUsd / totalValueUsd) * 10000) / 100; } } const data: PortfolioData = { address, chain, totalValueUsd, assets, assetCount: assets.length, }; cache.set(cacheKey, data, PORTFOLIO_CACHE_TTL); return makeSuccess(chain, data, false); } catch (err) { const message = sanitizeError(err); return makeError(`Failed to fetch portfolio: ${message}`, "RPC_ERROR"); } } - src/tools/getPortfolio.ts:47-50 (schema)Input schema definition using Zod for the getPortfolio tool parameters.
const inputSchema = z.object({ address: z.string().describe("조회할 지갑 주소 (0x...)"), chain: z.enum(SUPPORTED_CHAINS).default("ethereum").describe("EVM 체인"), }); - src/tools/getPortfolio.ts:166-176 (registration)Registration function for the getPortfolio tool, which adds the tool to the MCP server.
export function register(server: McpServer) { server.tool( "getPortfolio", "지갑의 전체 자산 포트폴리오(네이티브 + ERC-20 토큰, USD 가치, 비율)를 한 번에 조회합니다", inputSchema.shape, async (args) => { const result = await handler(args as z.infer<typeof inputSchema>); return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; }, ); }