Skip to content

Instantly share code, notes, and snippets.

@nshen
Last active February 14, 2025 03:49
Show Gist options
  • Save nshen/3ed70e81917a2e5bd2328033335fca8d to your computer and use it in GitHub Desktop.
Save nshen/3ed70e81917a2e5bd2328033335fca8d to your computer and use it in GitHub Desktop.
get-wallet-details.ts
/*
tsx --env-file=.env ./get-wallet-details.ts
接口2: 输入钱包地址,返回钱包的详情
每笔买入交易得到钱包余额,钱包地址,买入金额,买入单价,买入数量,交易签名,代币地址,代币名称,
卖出交易得到钱包余额,钱包地址,卖出金额,卖出单价,卖出数量,交易签名,代币地址,代币名称(最好能算出总价值))
使用方法
try {
// const res = await getWalletDetails('4wBbK41H6Pe9E7DxMefLz9Y7ge2TJAn5RSYmwb1vB9jU')
const res = await getWalletDetails('J6e9tmh36UmzXHjXoyRLBpWMgF1QgsDU6FvGWH8jADxb')
console.log(res);
} catch (error) {
console.error(error.toString());
}
*/
import { Connection, ParsedTransactionWithMeta, PublicKey } from "@solana/web3.js";
interface PumpFunTrade {
type: 'buy' | 'sell';
// 代币地址
tokenAddress: string;
// 代币名称
tokenName: string;
// 交易后钱包余额
walletBalance: string;
// 买入金额 (总花费包含了各种费)
solAmount: string;
// 买入或卖出数量
tokenAmount: string;
// 买入或卖出单价
tokenPrice: string;
// 交易签名
signature: string;
// 距离第一次买入的时间
timestamp: number;
}
interface TokenMetrics {
// 币地址
tokenAddress: string;
// 币名称
tokenName: string;
// 买入单个币的数量
tokenBuyAmount: number;
// 平均买入单个币的价格
tokenBuyPrice: number;
// 平均卖出单个币的价格
tokenSellPrice: number;
// 单个币的数量(买入的和卖出的币数量里面较少的数,如卖出10个币,买入20个,这个值为10)
tokenAmount: number;
// 单个币的利润(如果tokenAmount为0,这个值也为0,计算公式是(tokenSellPrice-tokenBuyPrice)* tokenAmount)
tokenProfit: number;
// 单个币的利润率(如果tokenAmount为0,这个值也为0,计算公式是(tokenSellPrice - tokenBuyPrice) / tokenBuyPrice)
tokenProfitRate: number;
};
interface TokenBalanceChange {
mint: string;
owner: string;
preAmount: number;
postAmount: number;
decimals: number;
}
interface TradeEventType {
mint: string;
solAmount: bigint;
tokenAmount: bigint;
isBuy: boolean;
user: string;
timestamp: bigint;
virtualSolReserves: bigint;
virtualTokenReserves: bigint;
}
async function batchFetchData(signatures: string[], batchSize: number = 20) {
const allData: ParsedTransactionWithMeta[] = [];
for (let i = 0; i < signatures.length; i += batchSize) {
const batch = signatures.slice(i, i + batchSize);
const batchResults = (await connection.getParsedTransactions(batch, { maxSupportedTransactionVersion: 0 }))
.filter((result): result is ParsedTransactionWithMeta => result !== null);
allData.push(...batchResults);
console.log(`Completed fetched ${i / batchSize + 1}. Total items: ${allData.length}`);
}
return allData;
}
const PUMP_PROGRAM = new PublicKey("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
const connection = new Connection(process.env.HTTP_PROVIDER as string)
export function isPumpFunTransaction(tx: ParsedTransactionWithMeta): boolean {
if (!tx.meta) return false
if (!tx.transaction.message) return false;
const hasProgramCall = tx.transaction.message.instructions
.some(ix => ix.programId.toBase58() === PUMP_PROGRAM.toBase58());
// 检查日志特征
const hasLogPattern = tx.meta.logMessages?.some(msg =>
msg.includes('Program log: Instruction: Buy') ||
msg.includes('Program log: Instruction: Sell')
// || msg.includes('Program log: Instruction: CreateToken')
) || false;
return hasProgramCall && hasLogPattern;
}
function parseTradeEvent(buffer: Buffer): TradeEventType | null {
// TradeEvent 结构:
// mint: publicKey (32 bytes)
// solAmount: u64 (8 bytes)
// tokenAmount: u64 (8 bytes)
// isBuy: bool (1 byte)
// user: publicKey (32 bytes)
// timestamp: i64 (8 bytes)
// virtualSolReserves: u64 (8 bytes)
// virtualTokenReserves: u64 (8 bytes)
if (buffer.length < 8) {
return null
}
let offset = 8;
const mint = new PublicKey(buffer.slice(offset, offset + 32)).toString();
offset += 32;
const solAmount = buffer.readBigUInt64LE(offset);
offset += 8;
const tokenAmount = buffer.readBigUInt64LE(offset);
offset += 8;
const isBuy = buffer.readUInt8(offset) === 1;
offset += 1;
const user = new PublicKey(buffer.slice(offset, offset + 32)).toString();
offset += 32;
const timestamp = buffer.readBigInt64LE(offset);
offset += 8;
const virtualSolReserves = buffer.readBigUInt64LE(offset);
offset += 8
const virtualTokenReserves = buffer.readBigUInt64LE(offset);
return {
mint,
solAmount,
tokenAmount,
isBuy,
user,
timestamp,
virtualSolReserves,
virtualTokenReserves
}
}
export async function parsePumpFunTx(tx: ParsedTransactionWithMeta): Promise<PumpFunTrade | null> {
if (tx.meta == null) {
console.log('meta is null');
return null;
}
if (!isPumpFunTransaction(tx))
return null;
try {
const signature = tx.transaction.signatures[0];
// const walletAddress = tx.transaction.message.accountKeys[0].pubkey.toBase58();
const fee = tx.meta.fee / 1e9;
const dataLog = tx.meta.logMessages?.find(log => log.startsWith('Program data:'));
if (!dataLog) {
// console.log('dataLog is null', tx.meta.logMessages);
return null;
}
const base64Data = dataLog.split('Program data: ')[1];
const decodedData = Buffer.from(base64Data, 'base64');
const event = parseTradeEvent(decodedData)
if (!event) {
console.log('event is null', decodedData);
return null;
}
const solReserves = Number(event.virtualSolReserves) / 1e9;
const tokenReserves = Number(event.virtualTokenReserves) / 1e6;
const unitPrice = (solReserves / tokenReserves).toFixed(10);
const isBuy = event.isBuy;
// 代币余额变化分析
const tokenChanges = tx.meta.postTokenBalances?.map((post, i: number) => {
const pre = tx.meta?.preTokenBalances?.[i];
return {
mint: post.mint,
owner: post.owner,
preAmount: pre?.uiTokenAmount.uiAmount,
postAmount: post.uiTokenAmount.uiAmount,
decimals: post.uiTokenAmount.decimals
} as TokenBalanceChange;
});
// SOL 余额变化
const solBalanceChange = (tx.meta.preBalances[0] - tx.meta.postBalances[0]) / 1e9 // - fee;
// 找到主代币交易对
const mainTokenChange = tokenChanges?.find(t => t.mint === event.mint);
// 计算需要指标
if (mainTokenChange) {
// const tokenDelta = mainTokenChange.postAmount - mainTokenChange.preAmount;
const solDelta = Math.abs(solBalanceChange);
// const unitPrice = solDelta / tokenDeltaAbs;
// 取 metadata
const tokenInfo = await getTokenInfo(mainTokenChange.mint);
return {
type: isBuy ? 'buy' : 'sell',
walletBalance: (tx.meta.postBalances[0] / 1e9).toFixed(10),
// walletAddress,
solAmount: solDelta.toFixed(10),
tokenPrice: unitPrice,
tokenAmount: (Number(event.tokenAmount) / 1e6).toFixed(10), //tokenDeltaAbs.toFixed(10),
signature,
tokenAddress: mainTokenChange.mint,
tokenName: tokenInfo.symbol,
timestamp: Number(event.timestamp) * 1000,
};
}
return null;
} catch (error) {
console.error('解析失败:', error);
return null;
}
}
const _symbolCache = {}
async function getTokenInfo(mint: string): Promise<{ symbol: string, supply: number }> {
if (_symbolCache[mint]) return _symbolCache[mint];
const provider = process.env.HTTP_PROVIDER as string
const key = provider.split('api-key=')[1];
const response = await fetch('https://mainnet.helius-rpc.com/?api-key=' + key, {
method: 'POST',
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
"jsonrpc": "2.0",
"id": "test",
"method": "getAsset",
"params": {
"id": mint
}
}),
});
const data = await response.json();
if (data?.result?.token_info) {
if (!data.result.token_info.symbol)
data.result.token_info.symbol = 'UNKNOWN';
// cache
_symbolCache[mint] = data.result.token_info;
return _symbolCache[mint];
}
return {
symbol: 'UNKNOWN',
supply: 0,
}
}
async function getTrades(connection: Connection, publicKey: PublicKey, limit: number): Promise<PumpFunTrade[]> {
const signaturesInfo = await connection.getSignaturesForAddress(
publicKey,
{
limit,
}
);
const signatures = signaturesInfo.map(s => s.signature);
const txs: ParsedTransactionWithMeta[] = await batchFetchData(signatures, 20);
const pumpTxs = txs.filter(isPumpFunTransaction);
const pumpTrads = await Promise.all(pumpTxs.map(async (tx) => await parsePumpFunTx(tx)));
return pumpTrads.filter((r): r is PumpFunTrade => r !== null);
}
function timeSince(date: Date): string {
const seconds: number = Math.floor((new Date().getTime() - date.getTime()) / 1000);
let interval: number = seconds / 31536000; // 年
if (interval > 1)
return Math.floor(interval) + " 年前";
interval = seconds / 2592000; // 月
if (interval > 1)
return Math.floor(interval) + " 个月前";
interval = seconds / 86400; // 天
if (interval > 1)
return Math.floor(interval) + " 天前";
interval = seconds / 3600; // 小时
if (interval > 1)
return Math.floor(interval) + " 小时前";
interval = seconds / 60; // 分钟
if (interval > 1)
return Math.floor(interval) + " 分钟前";
return Math.floor(seconds) + " 秒前";
}
function calculateROI(trades: PumpFunTrade[]): TokenMetrics[] {
const tokenMap = new Map<string, {
buyTotal: number;
buyQty: number;
sellTotal: number;
sellQty: number;
symbol: string;
}>();
// 第一步:按代币分组统计
for (const trade of trades) {
const address = trade.tokenAddress;
const price = parseFloat(trade.tokenPrice);
const amount = parseFloat(trade.tokenAmount);
if (!tokenMap.has(address)) {
tokenMap.set(address, {
buyTotal: 0,
buyQty: 0,
sellTotal: 0,
sellQty: 0,
symbol: trade.tokenName
});
}
const data = tokenMap.get(address)!;
if (trade.type === 'buy') {
data.buyTotal += price * amount;
data.buyQty += amount;
} else {
data.sellTotal += price * amount;
data.sellQty += amount;
}
}
// 第二步:关键数据
return Array.from(tokenMap.entries()).map(([address, data]) => {
const avgBuyPrice = data.buyQty > 0 ? data.buyTotal / data.buyQty : 0;
const avgSellPrice = data.sellQty > 0 ? data.sellTotal / data.sellQty : 0;
const matchedAmount = Math.min(data.buyQty, data.sellQty);
const profit = matchedAmount > 0
? (avgSellPrice - avgBuyPrice) * matchedAmount
: 0;
const profitRate = matchedAmount > 0 && avgBuyPrice > 0
? ((avgSellPrice - avgBuyPrice) / avgBuyPrice) : 0;
return {
tokenAddress: address,
tokenName: data.symbol,
tokenBuyAmount: data.buyQty,
tokenBuyPrice: avgBuyPrice,
tokenSellPrice: avgSellPrice,
tokenAmount: matchedAmount,
tokenProfit: profit,
tokenProfitRate: profitRate
};
}).filter(t => (t.tokenSellPrice > 0 && t.tokenBuyPrice > 0));
;
}
function getLatestBuyTime(trades: PumpFunTrade[]): number {
const buyTrade = trades.find(trade => trade.type === "buy");
return buyTrade ? new Date(Number(buyTrade.timestamp) * 1000).getTime() : 0;
}
// 需求
// a = 求和((tokenBuyPrice * tokenAmount)) / 求和 (tokenAmount)
// b =(tokenSellPrice * tokenAmount)/ tokenAmount
// totalProfitRate = (b - a)/a
function calculateTotalProfitRateBoss(roi: TokenMetrics[]): number {
let totalCost = 0;
let totalRevenue = 0;
let totalAmount = 0;
roi.forEach(metric => {
totalCost += metric.tokenBuyPrice * metric.tokenAmount;
totalRevenue += metric.tokenSellPrice * metric.tokenAmount;
totalAmount += metric.tokenAmount;
});
const avgBuyPrice = totalCost / totalAmount; // a
const avgSellPrice = totalRevenue / totalAmount; // b
return (avgSellPrice - avgBuyPrice) / avgBuyPrice; // (b - a) / a
}
export async function getWalletDetails(walletAddress: string, limit: number = 1000) {
// 检查地址长度
if (walletAddress.length !== 44)
throw "❌ 地址无效:长度必须为 44 个字符";
// 检查 Base58 编码合法性
if (!/^[1-9A-HJ-NP-Za-km-z]{44}$/.test(walletAddress))
throw "❌ 地址无效:包含非法字符";
const publicKey = new PublicKey(walletAddress);
const accountInfo = await connection.getAccountInfo(publicKey, { commitment: "confirmed" });
if (!accountInfo)
throw "⚠️ 空账户/不存在";
if (accountInfo.owner.toBase58() !== "11111111111111111111111111111111")
throw "❌ 非用户钱包地址";
const trades = await getTrades(connection, publicKey, limit);
const roi = calculateROI(trades);
const topProfit = Math.max(...roi.map(t => t.tokenProfit));
const topProfitRate = Math.max(...roi.map(t => t.tokenProfitRate));
const totalProfit = roi.reduce((acc, t) => acc + t.tokenProfit, 0);
const totalProfitRate = calculateTotalProfitRateBoss(roi);
return {
walletAddress,
walletBalance: accountInfo.lamports / 1e9,
timestamp: new Date().getTime() - getLatestBuyTime(trades),
totalProfit, // 总利润(单位SOL)
totalProfitRate, // 总利润率
topProfit, //最高利润(单位SOL)(某个代币买入卖出实现最高盈利多少SOL)
topProfitRate, // 最高利润率(某个代币买入卖出实现最高利润多少)
trades,
roi
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment