| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454 |
- import Web3 from "web3";
- import yargs from "yargs";
- import { hideBin } from "yargs/helpers";
- import { PrivateKey, toPrivateKey } from "../src/core/base";
- import { EvmChain } from "../src/core/chains";
- import { DefaultStore } from "../src/node/utils/store";
- type TransferResult = {
- chain: string;
- success: boolean;
- sourceAddress: string;
- destinationAddress: string;
- originalBalance: string;
- transferAmount: string;
- remainingBalance: string;
- transactionHash?: string;
- error?: string;
- }
- const parser = yargs(hideBin(process.argv))
- .usage(
- "Multi-Chain Balance Transfer Tool for Pyth Entropy Chains\n\nUsage: $0 --source-private-key <key> --destination-address <addr> [chain-selection] [transfer-method] [options]",
- )
- .options({
- "source-private-key": {
- type: "string",
- demandOption: true,
- desc: "Private key of the source wallet to transfer from",
- },
- "destination-address": {
- type: "string",
- demandOption: true,
- desc: "Public address of the destination wallet",
- },
- chain: {
- type: "array",
- string: true,
- desc: "Specific chain IDs to transfer on (e.g., --chain optimism_sepolia --chain avalanche)",
- },
- testnets: {
- type: "boolean",
- default: false,
- desc: "Transfer on all testnet entropy chains",
- },
- mainnets: {
- type: "boolean",
- default: false,
- desc: "Transfer on all mainnet entropy chains",
- },
- amount: {
- type: "number",
- desc: "Exact amount in ETH to transfer from each chain",
- },
- ratio: {
- type: "number",
- desc: "Ratio of available balance to transfer (0-1, e.g., 0.5 for half, 1.0 for all)",
- },
- "min-balance": {
- type: "number",
- default: 0.001,
- desc: "Minimum balance in ETH required before attempting transfer",
- },
- "gas-multiplier": {
- type: "number",
- default: 2,
- desc: "Gas multiplier for transaction safety",
- },
- "dry-run": {
- type: "boolean",
- default: false,
- desc: "Preview transfers without executing transactions",
- },
- })
- .group(
- ["chain", "testnets", "mainnets"],
- "Chain Selection (choose exactly one):",
- )
- .group(["amount", "ratio"], "Transfer Method (choose exactly one):")
- .group(["min-balance", "gas-multiplier", "dry-run"], "Optional Parameters:")
- .example([
- [
- "$0 --source-private-key abc123... --destination-address 0x742d35... --mainnets --amount 0.1",
- "Transfer 0.1 ETH from all mainnet chains",
- ],
- [
- "$0 --source-private-key abc123... --destination-address 0x742d35... --testnets --ratio 0.75",
- "Transfer 75% of balance from all testnet chains",
- ],
- [
- "$0 --source-private-key abc123... --destination-address 0x742d35... --chain ethereum --chain avalanche --amount 0.05",
- "Transfer 0.05 ETH from specific chains",
- ],
- [
- "$0 --source-private-key abc123... --destination-address 0x742d35... --testnets --ratio 0.5 --dry-run",
- "Preview 50% transfer on all testnet chains",
- ],
- ])
- .help()
- .alias("help", "h")
- .version(false);
- async function transferOnChain(
- chain: EvmChain,
- sourcePrivateKey: PrivateKey,
- destinationAddress: string,
- minBalance: number,
- gasMultiplier: number,
- dryRun: boolean,
- transferAmount?: number,
- transferRatio?: number,
- ): Promise<TransferResult> {
- const web3 = chain.getWeb3();
- const signer = web3.eth.accounts.privateKeyToAccount(sourcePrivateKey);
- const sourceAddress = signer.address;
- try {
- // Get balance
- const balanceWei = await web3.eth.getBalance(sourceAddress);
- const balanceEth = Number(web3.utils.fromWei(balanceWei, "ether"));
- console.log(`\n${chain.getId()}: Checking balance for ${sourceAddress}`);
- console.log(` Balance: ${balanceEth.toFixed(6)} ETH`);
- if (balanceEth < minBalance) {
- console.log(
- ` Balance below minimum threshold (${minBalance} ETH), skipping`,
- );
- return {
- chain: chain.getId(),
- success: false,
- sourceAddress,
- destinationAddress,
- originalBalance: balanceEth.toFixed(6),
- transferAmount: "0",
- remainingBalance: balanceEth.toFixed(6),
- error: `Balance ${balanceEth.toFixed(6)} ETH below minimum ${minBalance} ETH`,
- };
- }
- // Calculate gas costs
- const gasPrice = await web3.eth.getGasPrice();
- const estimatedGas = await web3.eth.estimateGas({
- from: sourceAddress,
- to: destinationAddress,
- value: "1", // Minimal value for estimation
- });
- const gasCostWei =
- BigInt(estimatedGas) * BigInt(gasPrice) * BigInt(gasMultiplier);
- const gasCostEth = Number(
- web3.utils.fromWei(gasCostWei.toString(), "ether"),
- );
- // Calculate transfer amount
- let transferAmountEth: number;
- if (transferAmount === undefined) {
- // transferRatio is guaranteed to be defined at this point
- if (transferRatio === undefined) {
- throw new Error(
- "Transfer ratio must be defined when amount is not specified",
- );
- }
- transferAmountEth = (balanceEth - gasCostEth) * transferRatio;
- } else {
- transferAmountEth = transferAmount;
- }
- // Round to 10 decimal places to avoid Web3 conversion errors
- transferAmountEth = Math.round(transferAmountEth * 1e10) / 1e10;
- // Validate transfer amount
- if (transferAmountEth <= 0) {
- console.log(
- ` Not enough balance to cover transfer and gas costs, skipping`,
- );
- return {
- chain: chain.getId(),
- success: false,
- sourceAddress,
- destinationAddress,
- originalBalance: balanceEth.toFixed(6),
- transferAmount: "0",
- remainingBalance: balanceEth.toFixed(6),
- error: `Insufficient balance for transfer amount and gas costs (${gasCostEth.toFixed(6)} ETH)`,
- };
- }
- if (transferAmountEth + gasCostEth > balanceEth) {
- console.log(` Transfer amount plus gas costs exceed balance, skipping`);
- return {
- chain: chain.getId(),
- success: false,
- sourceAddress,
- destinationAddress,
- originalBalance: balanceEth.toFixed(6),
- transferAmount: "0",
- remainingBalance: balanceEth.toFixed(6),
- error: `Transfer amount ${transferAmountEth.toFixed(6)} ETH plus gas ${gasCostEth.toFixed(6)} ETH exceeds balance`,
- };
- }
- const transferAmountWei = web3.utils.toWei(
- transferAmountEth.toString(),
- "ether",
- );
- console.log(` Transfer amount: ${transferAmountEth.toFixed(6)} ETH`);
- console.log(` Estimated gas cost: ${gasCostEth.toFixed(6)} ETH`);
- console.log(` Destination: ${destinationAddress}`);
- if (dryRun) {
- console.log(
- ` DRY RUN: Would transfer ${transferAmountEth.toFixed(6)} ETH`,
- );
- return {
- chain: chain.getId(),
- success: true,
- sourceAddress,
- destinationAddress,
- originalBalance: balanceEth.toFixed(6),
- transferAmount: transferAmountEth.toFixed(6),
- remainingBalance: (balanceEth - transferAmountEth).toFixed(6),
- };
- }
- // Perform the transfer
- web3.eth.accounts.wallet.add(signer);
- console.log(` Executing transfer...`);
- const tx = await web3.eth.sendTransaction({
- from: sourceAddress,
- to: destinationAddress,
- value: transferAmountWei,
- gas: Number(estimatedGas) * gasMultiplier,
- gasPrice: gasPrice,
- });
- // Get updated balance
- const newBalanceWei = await web3.eth.getBalance(sourceAddress);
- const newBalanceEth = Number(web3.utils.fromWei(newBalanceWei, "ether"));
- console.log(` Transfer successful!`);
- console.log(` Transaction hash: ${tx.transactionHash}`);
- console.log(` New balance: ${newBalanceEth.toFixed(6)} ETH`);
- return {
- chain: chain.getId(),
- success: true,
- sourceAddress,
- destinationAddress,
- originalBalance: balanceEth.toFixed(6),
- transferAmount: transferAmountEth.toFixed(6),
- remainingBalance: newBalanceEth.toFixed(6),
- transactionHash: tx.transactionHash,
- };
- } catch (error) {
- console.log(` Transfer failed: ${error}`);
- return {
- chain: chain.getId(),
- success: false,
- sourceAddress,
- destinationAddress,
- originalBalance: "unknown",
- transferAmount: "0",
- remainingBalance: "unknown",
- error: error instanceof Error ? error.message : String(error),
- };
- }
- }
- function getSelectedChains(argv: {
- chain?: string[];
- testnets: boolean;
- mainnets: boolean;
- }): EvmChain[] {
- // Check for mutually exclusive options
- const optionCount =
- (argv.testnets ? 1 : 0) + (argv.mainnets ? 1 : 0) + (argv.chain ? 1 : 0);
- if (optionCount !== 1) {
- throw new Error(
- "Must specify exactly one of: --testnets, --mainnets, or --chain",
- );
- }
- // Get all entropy contract chains
- const allEntropyChains: EvmChain[] = [];
- for (const contract of Object.values(DefaultStore.entropy_contracts)) {
- const chain = contract.getChain();
- if (chain instanceof EvmChain) {
- allEntropyChains.push(chain);
- }
- }
- let selectedChains: EvmChain[];
- if (argv.testnets) {
- selectedChains = allEntropyChains.filter((chain) => !chain.isMainnet());
- } else if (argv.mainnets) {
- selectedChains = allEntropyChains.filter((chain) => chain.isMainnet());
- } else {
- // Specific chains
- const entropyChainIds = new Set(
- allEntropyChains.map((chain) => chain.getId()),
- );
- selectedChains = [];
- if (!argv.chain) {
- throw new Error(
- "Chain argument must be defined for specific chain selection",
- );
- }
- for (const chainId of argv.chain) {
- if (!entropyChainIds.has(chainId)) {
- throw new Error(
- `Chain ${chainId} does not have entropy contracts deployed`,
- );
- }
- const chain = DefaultStore.chains[chainId];
- if (!(chain instanceof EvmChain)) {
- throw new TypeError(`Chain ${chainId} is not an EVM chain`);
- }
- selectedChains.push(chain);
- }
- }
- if (selectedChains.length === 0) {
- const mode = argv.testnets
- ? "testnet"
- : (argv.mainnets
- ? "mainnet"
- : "specified");
- throw new Error(`No valid ${mode} entropy chains found`);
- }
- return selectedChains;
- }
- async function main() {
- const argv = await parser.argv;
- // Validate inputs
- if (!Web3.utils.isAddress(argv.destinationAddress)) {
- throw new Error("Invalid destination address format");
- }
- // Validate transfer amount options
- if (argv.amount !== undefined && argv.ratio !== undefined) {
- throw new Error("Cannot specify both --amount and --ratio options");
- }
- if (argv.amount === undefined && argv.ratio === undefined) {
- throw new Error("Must specify either --amount or --ratio option");
- }
- if (argv.ratio !== undefined && (argv.ratio <= 0 || argv.ratio > 1)) {
- throw new Error(
- "Ratio must be between 0 and 1 (exclusive of 0, inclusive of 1)",
- );
- }
- if (argv.amount !== undefined && argv.amount <= 0) {
- throw new Error("Amount must be greater than 0");
- }
- const sourcePrivateKey = toPrivateKey(argv.sourcePrivateKey);
- const selectedChains = getSelectedChains(argv);
- // Determine transfer method for display
- let transferMethod: string;
- if (argv.amount === undefined) {
- if (argv.ratio === undefined) {
- throw new Error("Ratio must be defined when amount is not specified");
- }
- transferMethod = `${(argv.ratio * 100).toFixed(1)}% of available balance`;
- } else {
- transferMethod = `${argv.amount} ETH (fixed amount)`;
- }
- console.log(`\nConfiguration:`);
- console.log(
- ` Network: ${argv.testnets ? "Testnet" : (argv.mainnets ? "Mainnet" : "Specific chains")}`,
- );
- console.log(` Destination: ${argv.destinationAddress}`);
- console.log(` Transfer method: ${transferMethod}`);
- console.log(` Minimum balance: ${argv.minBalance} ETH`);
- console.log(` Gas multiplier: ${argv.gasMultiplier}x`);
- console.log(` Dry run: ${argv.dryRun ? "Yes" : "No"}`);
- console.log(` Chains: ${selectedChains.map((c) => c.getId()).join(", ")}`);
- if (argv.dryRun) {
- console.log(`\nRUNNING IN DRY-RUN MODE - NO TRANSACTIONS WILL BE EXECUTED`);
- }
- const results: TransferResult[] = [];
- // Process each chain
- for (const chain of selectedChains) {
- const result = await transferOnChain(
- chain,
- sourcePrivateKey,
- argv.destinationAddress,
- argv.minBalance,
- argv.gasMultiplier,
- argv.dryRun,
- argv.amount,
- argv.ratio,
- );
- results.push(result);
- }
- // Summary
- console.log("\nTRANSFER SUMMARY");
- console.log("==================");
- const successful = results.filter((r) => r.success);
- const failed = results.filter((r) => !r.success);
- console.log(`Successful transfers: ${successful.length}`);
- console.log(`Failed transfers: ${failed.length}`);
- console.log(
- `Total transferred: ${successful.reduce((sum, r) => sum + Number.parseFloat(r.transferAmount), 0).toFixed(6)} ETH`,
- );
- if (successful.length > 0) {
- console.log("\nSuccessful Transfers:");
- console.table(
- successful.map((r) => ({
- Chain: r.chain,
- "Transfer Amount (ETH)": r.transferAmount,
- "TX Hash": r.transactionHash || "N/A (dry run)",
- "Remaining Balance (ETH)": r.remainingBalance,
- })),
- );
- }
- if (failed.length > 0) {
- console.log("\nFailed Transfers:");
- console.table(
- failed.map((r) => ({
- Chain: r.chain,
- "Original Balance (ETH)": r.originalBalance,
- Error: r.error,
- })),
- );
- }
- console.log("\nTransfer process completed!");
- }
- main().catch((error) => {
- console.error("Script failed:", error);
- process.exit(1);
- });
|