491 lines
22 KiB
TypeScript
491 lines
22 KiB
TypeScript
import { ethers } from 'ethers';
|
|
import { Connection, PublicKey, Keypair, Transaction, SystemProgram, clusterApiUrl, LAMPORTS_PER_SOL } from '@solana/web3.js';
|
|
import { getAssociatedTokenAddress, getAccount } from '@solana/spl-token';
|
|
import bs58 from 'bs58';
|
|
import cryptoConfig from './crypto-config.json';
|
|
const { TronWeb } = require('tronweb');
|
|
|
|
// ERC20 ABI for checking USDT/USDC balances
|
|
const ERC20_ABI = [
|
|
"function balanceOf(address owner) view returns (uint256)",
|
|
"function decimals() view returns (uint8)",
|
|
"function symbol() view returns (string)",
|
|
"function transfer(address to, uint256 value) public returns (bool)"
|
|
];
|
|
|
|
export class CryptoEngine {
|
|
private provider!: ethers.JsonRpcProvider;
|
|
private solConnection!: Connection;
|
|
private tronWeb: any;
|
|
private network: string;
|
|
private config: any;
|
|
|
|
constructor(networkId: string = 'POLYGON') {
|
|
this.network = networkId;
|
|
this.config = cryptoConfig.networks.find(n => n.id === networkId);
|
|
|
|
if (!this.config) throw new Error(`Network ${networkId} not found in config.`);
|
|
|
|
if (this.network === 'SOLANA') {
|
|
this.solConnection = new Connection(this.config.rpc, 'confirmed');
|
|
} else if (this.network === 'TRON') {
|
|
this.tronWeb = new TronWeb({
|
|
fullHost: this.config.rpc,
|
|
headers: { "TRON-PRO-API-KEY": process.env.TRON_GRID_API_KEY || "" }
|
|
});
|
|
} else if (this.network === 'BITCOIN') {
|
|
// Bitcoin usually handled via Electrum OR simple Public API
|
|
} else {
|
|
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
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to get token config from JSON
|
|
*/
|
|
private getTokenConfig(symbol: string) {
|
|
return this.config.tokens.find((t: any) => t.symbol === symbol);
|
|
}
|
|
|
|
/**
|
|
* Generates a temporary wallet for a transaction.
|
|
*/
|
|
static async createTemporaryWallet(network: string = 'POLYGON'): Promise<{ address: string, privateKey: string }> {
|
|
if (network === 'SOLANA') {
|
|
const keypair = Keypair.generate();
|
|
return {
|
|
address: keypair.publicKey.toBase58(),
|
|
privateKey: bs58.encode(keypair.secretKey)
|
|
};
|
|
} else if (network === 'TRON') {
|
|
const tempTronWeb = new TronWeb({ fullHost: 'https://api.trongrid.io' });
|
|
const account = await tempTronWeb.createAccount();
|
|
return {
|
|
address: account.address.base58,
|
|
privateKey: account.privateKey
|
|
};
|
|
} else if (network === 'BITCOIN') {
|
|
// Mock SegWit address for logic
|
|
return {
|
|
address: `bc1q${Math.random().toString(36).substring(2, 12)}...MOCK`,
|
|
privateKey: 'MOCK_BTC_PRIVATE_KEY'
|
|
};
|
|
} else {
|
|
const wallet = ethers.Wallet.createRandom();
|
|
return {
|
|
address: wallet.address,
|
|
privateKey: wallet.privateKey
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sweeps funds from temporary wallet to Platform and Merchant
|
|
*/
|
|
async sweepFunds(
|
|
tempWalletPrivateKey: string,
|
|
platformAddress: string,
|
|
tokenSymbol: string = 'USDT'
|
|
) {
|
|
if (this.network === 'SOLANA') {
|
|
return this.sweepSolana(tempWalletPrivateKey, platformAddress, tokenSymbol);
|
|
} else if (this.network === 'TRON') {
|
|
return this.sweepTron(tempWalletPrivateKey, platformAddress, tokenSymbol);
|
|
} else if (this.network === 'BITCOIN') {
|
|
return this.sweepBitcoin(tempWalletPrivateKey, platformAddress, tokenSymbol);
|
|
} else {
|
|
return this.sweepEVM(tempWalletPrivateKey, platformAddress, tokenSymbol);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
tokenSymbol: string
|
|
) {
|
|
const tempWallet = new ethers.Wallet(tempWalletPrivateKey, this.provider);
|
|
const tokenConfig = this.getTokenConfig(tokenSymbol);
|
|
if (!tokenConfig) throw new Error(`Unsupported token ${tokenSymbol} on ${this.network}`);
|
|
|
|
console.log(`[Sweep EVM] Sweeping 100% to Platform Treasury: ${platformAddress}`);
|
|
|
|
// Mocking the real transfer for demo
|
|
return {
|
|
success: true,
|
|
txHash: '0x' + Math.random().toString(16).slice(2, 66)
|
|
};
|
|
}
|
|
|
|
async fuelWallet(targetAddress: string, amount: string) {
|
|
const gasTankKey = process.env.CRYPTO_GAS_TANK_KEY;
|
|
if (!gasTankKey) {
|
|
console.warn("[CryptoEngine] No CRYPTO_GAS_TANK_KEY provided. Fueling skipped (Demo mode).");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const gasTank = new ethers.Wallet(gasTankKey, this.provider);
|
|
const tx = await gasTank.sendTransaction({
|
|
to: targetAddress,
|
|
value: ethers.parseEther(amount)
|
|
});
|
|
await tx.wait();
|
|
console.log(`[CryptoEngine] Fueled ${targetAddress} with ${amount} native currency. Hash: ${tx.hash}`);
|
|
} catch (error) {
|
|
console.error("[CryptoEngine] Fueling failed:", error);
|
|
}
|
|
}
|
|
|
|
private async sweepSolana(
|
|
senderPrivateKey: string,
|
|
destinationAddress: string,
|
|
tokenSymbol: string
|
|
) {
|
|
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(
|
|
tempWalletPrivateKey: string,
|
|
platformAddress: string,
|
|
tokenSymbol: string
|
|
) {
|
|
console.log(`[Sweep TRON] Sweeping 100% to Platform Treasury: ${platformAddress}`);
|
|
return {
|
|
success: true,
|
|
txHash: 'tron_mock_tx_' + Math.random().toString(36).substring(7)
|
|
};
|
|
}
|
|
|
|
private async sweepBitcoin(
|
|
tempWalletPrivateKey: string,
|
|
platformAddress: string,
|
|
tokenSymbol: string
|
|
) {
|
|
console.log(`[Sweep BTC] Sweeping 100% to Platform Treasury: ${platformAddress}`);
|
|
return {
|
|
success: true,
|
|
txHash: 'btc_mock_tx_' + Math.random().toString(36).substring(7)
|
|
};
|
|
}
|
|
|
|
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.
|
|
*/
|
|
async verifyPayment(address: string, expectedAmount: string, tokenSymbol?: string): Promise<{
|
|
success: boolean;
|
|
txHash?: string;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const tokenConfig = this.getTokenConfig(tokenSymbol || 'USDT');
|
|
if (!tokenConfig) return { success: false, error: "Token not supported in config" };
|
|
|
|
if (this.network === 'SOLANA') {
|
|
const pubKey = new PublicKey(address);
|
|
if (tokenConfig.address === 'NATIVE') {
|
|
const balance = await this.solConnection.getBalance(pubKey);
|
|
const balanceInSol = balance / LAMPORTS_PER_SOL;
|
|
if (balanceInSol >= parseFloat(expectedAmount)) return { success: true };
|
|
} else {
|
|
const tokenMint = new PublicKey(tokenConfig.address);
|
|
const ata = await getAssociatedTokenAddress(tokenMint, pubKey);
|
|
try {
|
|
const accountInfo = await getAccount(this.solConnection, ata);
|
|
const balance = Number(accountInfo.amount) / Math.pow(10, tokenConfig.decimals);
|
|
if (balance >= parseFloat(expectedAmount)) return { success: true };
|
|
} catch (e) {}
|
|
}
|
|
} else if (this.network === 'TRON') {
|
|
if (tokenConfig.address === 'NATIVE') {
|
|
const balance = await this.tronWeb.trx.getBalance(address);
|
|
const balanceInTrx = balance / 1000000;
|
|
if (balanceInTrx >= parseFloat(expectedAmount)) return { success: true };
|
|
} else {
|
|
const contract = await this.tronWeb.contract().at(tokenConfig.address);
|
|
const balance = await contract.balanceOf(address).call();
|
|
const formattedBalance = Number(balance) / Math.pow(10, tokenConfig.decimals);
|
|
if (formattedBalance >= parseFloat(expectedAmount)) return { success: true };
|
|
}
|
|
} else if (this.network === 'BITCOIN') {
|
|
try {
|
|
const res = await fetch(`https://blockchain.info/rawaddr/${address}`);
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
const balanceInBtc = data.total_received / 100000000;
|
|
if (balanceInBtc >= parseFloat(expectedAmount)) return { success: true };
|
|
}
|
|
} catch (e) {
|
|
console.warn("[BTC Verify] Public API failed, using mock success for demo if address is not empty");
|
|
if (address && address.length > 20) return { success: true };
|
|
}
|
|
} else {
|
|
if (tokenConfig.address === 'NATIVE') {
|
|
const balance = await this.provider.getBalance(address);
|
|
const balanceInEth = ethers.formatEther(balance);
|
|
if (parseFloat(balanceInEth) >= parseFloat(expectedAmount)) return { success: true };
|
|
} else {
|
|
const contract = new ethers.Contract(tokenConfig.address, ERC20_ABI, this.provider);
|
|
const balance = await contract.balanceOf(address);
|
|
const formattedBalance = ethers.formatUnits(balance, tokenConfig.decimals);
|
|
if (parseFloat(formattedBalance) >= parseFloat(expectedAmount)) return { success: true };
|
|
}
|
|
}
|
|
return { success: false };
|
|
} catch (error: any) {
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
}
|