feat: add Solana USDT/USDC support and refine admin payouts UI

This commit is contained in:
mstfyldz
2026-03-13 05:17:04 +03:00
parent 5f0df83686
commit 641498957c
16 changed files with 1335 additions and 120 deletions

View File

@@ -36,7 +36,13 @@ export class CryptoEngine {
} else if (this.network === 'BITCOIN') {
// Bitcoin usually handled via Electrum OR simple Public API
} else {
this.provider = new ethers.JsonRpcProvider(this.config.rpc);
const options = this.config.chainId
? { chainId: this.config.chainId, name: this.config.name.toLowerCase() }
: undefined;
this.provider = new ethers.JsonRpcProvider(this.config.rpc, options, {
staticNetwork: !!this.config.chainId
});
}
}
@@ -98,6 +104,107 @@ export class CryptoEngine {
}
}
/**
* Send a specific amount from treasury to a destination address (for payouts)
*/
async sendPayout(
senderPrivateKey: string,
destinationAddress: string,
amount: string,
tokenSymbol: string = 'SOL'
): Promise<{ success: boolean; txHash: string | null; error?: string }> {
console.log(`[Payout] Sending ${amount} ${tokenSymbol} on ${this.network} to ${destinationAddress}`);
if (this.network === 'SOLANA') {
return this.sendSolanaPayout(senderPrivateKey, destinationAddress, amount, tokenSymbol);
} else {
// EVM / TRON / BTC - placeholder for now
console.warn(`[Payout] Real transfer not yet implemented for ${this.network}. Using mock.`);
return { success: true, txHash: `mock_${this.network}_${Date.now()}` };
}
}
private async sendSolanaPayout(
senderPrivateKey: string,
destinationAddress: string,
amount: string,
tokenSymbol: string
): Promise<{ success: boolean; txHash: string | null; error?: string }> {
try {
// Decode private key (support both base64 and base58)
let secretKey: Uint8Array;
try {
secretKey = Uint8Array.from(Buffer.from(senderPrivateKey, 'base64'));
if (secretKey.length !== 64) throw new Error('not base64');
} catch {
secretKey = bs58.decode(senderPrivateKey);
}
const senderKeypair = Keypair.fromSecretKey(secretKey);
const destPubKey = new PublicKey(destinationAddress);
const parsedAmount = parseFloat(amount);
console.log(`[Payout SOL] From: ${senderKeypair.publicKey.toBase58()} -> To: ${destinationAddress} | ${parsedAmount} ${tokenSymbol}`);
if (tokenSymbol === 'SOL' || tokenSymbol === 'NATIVE') {
const lamports = Math.floor(parsedAmount * LAMPORTS_PER_SOL);
const balance = await this.solConnection.getBalance(senderKeypair.publicKey);
if (balance < lamports + 5000) {
return { success: false, txHash: null, error: `Insufficient SOL. Have: ${balance / LAMPORTS_PER_SOL}, Need: ${parsedAmount}` };
}
const transaction = new Transaction().add(
SystemProgram.transfer({
fromPubkey: senderKeypair.publicKey,
toPubkey: destPubKey,
lamports
})
);
const { blockhash } = await this.solConnection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
transaction.feePayer = senderKeypair.publicKey;
transaction.sign(senderKeypair);
const txHash = await this.solConnection.sendRawTransaction(transaction.serialize());
await this.solConnection.confirmTransaction(txHash);
console.log(`[Payout SOL] ✅ Sent ${parsedAmount} SOL | TX: ${txHash}`);
return { success: true, txHash };
} else {
// SPL token payout
const tokenConfig = this.getTokenConfig(tokenSymbol);
if (!tokenConfig) return { success: false, txHash: null, error: `Token ${tokenSymbol} not found` };
const { createTransferInstruction, getAssociatedTokenAddress: getATA } = require('@solana/spl-token');
const mintPubKey = new PublicKey(tokenConfig.address);
const senderATA = await getATA(mintPubKey, senderKeypair.publicKey);
const destATA = await getATA(mintPubKey, destPubKey);
const tokenAmount = Math.floor(parsedAmount * Math.pow(10, tokenConfig.decimals));
const transaction = new Transaction().add(
createTransferInstruction(senderATA, destATA, senderKeypair.publicKey, tokenAmount)
);
const { blockhash } = await this.solConnection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
transaction.feePayer = senderKeypair.publicKey;
transaction.sign(senderKeypair);
const txHash = await this.solConnection.sendRawTransaction(transaction.serialize());
await this.solConnection.confirmTransaction(txHash);
console.log(`[Payout SOL] ✅ Sent ${parsedAmount} ${tokenSymbol} | TX: ${txHash}`);
return { success: true, txHash };
}
} catch (e: any) {
console.error(`[Payout SOL] ❌ Error:`, e.message);
return { success: false, txHash: null, error: e.message };
}
}
private async sweepEVM(
tempWalletPrivateKey: string,
platformAddress: string,
@@ -137,15 +244,95 @@ export class CryptoEngine {
}
private async sweepSolana(
tempWalletPrivateKey: string,
platformAddress: string,
senderPrivateKey: string,
destinationAddress: string,
tokenSymbol: string
) {
console.log(`[Sweep SOLANA] Sweeping 100% to Platform Treasury: ${platformAddress}`);
return {
success: true,
txHash: 'sol_mock_tx_' + Math.random().toString(36).substring(7)
};
console.log(`[Sweep SOLANA] Sending ${tokenSymbol} to ${destinationAddress}`);
try {
// Decode private key (support both base64 and base58)
let secretKey: Uint8Array;
try {
secretKey = Uint8Array.from(Buffer.from(senderPrivateKey, 'base64'));
if (secretKey.length !== 64) throw new Error('not base64');
} catch {
secretKey = bs58.decode(senderPrivateKey);
}
const senderKeypair = Keypair.fromSecretKey(secretKey);
const destPubKey = new PublicKey(destinationAddress);
if (tokenSymbol === 'SOL' || tokenSymbol === 'NATIVE') {
// Get balance and send almost all (leave 5000 lamports for rent)
const balance = await this.solConnection.getBalance(senderKeypair.publicKey);
const sendAmount = balance - 10000; // leave 0.00001 SOL for fees
if (sendAmount <= 0) {
return { success: false, txHash: null, error: 'Insufficient SOL balance' };
}
const transaction = new Transaction().add(
SystemProgram.transfer({
fromPubkey: senderKeypair.publicKey,
toPubkey: destPubKey,
lamports: sendAmount
})
);
const { blockhash } = await this.solConnection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
transaction.feePayer = senderKeypair.publicKey;
transaction.sign(senderKeypair);
const txHash = await this.solConnection.sendRawTransaction(transaction.serialize());
await this.solConnection.confirmTransaction(txHash);
console.log(`[Sweep SOLANA] ✅ Sent ${sendAmount / LAMPORTS_PER_SOL} SOL | TX: ${txHash}`);
return { success: true, txHash };
} else {
// SPL Token transfer (USDT, USDC) - requires ATA
const tokenConfig = this.getTokenConfig(tokenSymbol);
if (!tokenConfig) throw new Error(`Token ${tokenSymbol} not found`);
const { createTransferInstruction, getAssociatedTokenAddress: getATA, getOrCreateAssociatedTokenAccount } = require('@solana/spl-token');
const mintPubKey = new PublicKey(tokenConfig.address);
const senderATA = await getATA(mintPubKey, senderKeypair.publicKey);
const destATA = await getATA(mintPubKey, destPubKey);
// Check sender token balance
try {
const accountInfo = await getAccount(this.solConnection, senderATA);
const tokenBalance = Number(accountInfo.amount);
if (tokenBalance <= 0) {
return { success: false, txHash: null, error: `No ${tokenSymbol} balance` };
}
const transaction = new Transaction().add(
createTransferInstruction(senderATA, destATA, senderKeypair.publicKey, tokenBalance)
);
const { blockhash } = await this.solConnection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
transaction.feePayer = senderKeypair.publicKey;
transaction.sign(senderKeypair);
const txHash = await this.solConnection.sendRawTransaction(transaction.serialize());
await this.solConnection.confirmTransaction(txHash);
console.log(`[Sweep SOLANA] ✅ Sent ${tokenBalance} ${tokenSymbol} | TX: ${txHash}`);
return { success: true, txHash };
} catch (e: any) {
console.error(`[Sweep SOLANA] Token transfer error:`, e.message);
return { success: false, txHash: null, error: e.message };
}
}
} catch (e: any) {
console.error(`[Sweep SOLANA] Error:`, e.message);
return { success: false, txHash: null, error: e.message };
}
}
private async sweepTron(
@@ -172,6 +359,67 @@ export class CryptoEngine {
};
}
async getBalance(address: string, tokenSymbol: string = 'NATIVE'): Promise<string> {
try {
const tokenConfig = this.getTokenConfig(tokenSymbol);
if (!tokenConfig) return "0.00";
if (this.network === 'SOLANA') {
const pubKey = new PublicKey(address);
if (tokenConfig.address === 'NATIVE') {
const balance = await this.solConnection.getBalance(pubKey);
return (balance / LAMPORTS_PER_SOL).toFixed(4);
} else {
const tokenMint = new PublicKey(tokenConfig.address);
const ata = await getAssociatedTokenAddress(tokenMint, pubKey);
try {
const accountInfo = await getAccount(this.solConnection, ata);
return (Number(accountInfo.amount) / Math.pow(10, tokenConfig.decimals)).toFixed(4);
} catch (e) { return "0.00"; }
}
} else if (this.network === 'TRON') {
try {
if (tokenConfig.address === 'NATIVE') {
const balance = await this.tronWeb.trx.getBalance(address);
return (balance / 1000000).toFixed(2);
} else {
const contract = await this.tronWeb.contract().at(tokenConfig.address);
const balance = await contract.balanceOf(address).call();
return (Number(balance) / Math.pow(10, tokenConfig.decimals)).toFixed(2);
}
} catch (e: any) {
console.warn(`[CryptoEngine] TRON balance check failed (likely API key or address issue):`, e.message);
return "0.00";
}
} else if (this.network === 'BITCOIN') {
try {
const btcRes = await fetch(`https://blockchain.info/q/addressbalance/${address}`).then(r => r.text());
return (parseInt(btcRes) / 1e8).toFixed(8);
} catch (e) { return "0.00"; }
} else {
// EVM
try {
const safeAddress = ethers.getAddress(address);
if (tokenConfig.address === 'NATIVE') {
const balance = await this.provider.getBalance(safeAddress);
return ethers.formatEther(balance);
} else {
const safeTokenAddr = ethers.getAddress(tokenConfig.address);
const contract = new ethers.Contract(safeTokenAddr, ERC20_ABI, this.provider);
const balance = await contract.balanceOf(safeAddress);
return ethers.formatUnits(balance, tokenConfig.decimals);
}
} catch (e: any) {
console.warn(`[CryptoEngine] EVM balance check failed for ${tokenSymbol}:`, e.message);
return "0.00";
}
}
} catch (e: any) {
console.error(`[CryptoEngine] getBalance error for ${tokenSymbol}:`, e);
return "0.00";
}
}
/**
* Verifies if a specific amount has arrived at the address.
*/