transfer_balance_entropy_chains.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. import yargs from "yargs";
  2. import { hideBin } from "yargs/helpers";
  3. import { DefaultStore } from "../src/node/utils/store";
  4. import { PrivateKey, toPrivateKey } from "../src/core/base";
  5. import { EvmChain } from "../src/core/chains";
  6. import Web3 from "web3";
  7. interface TransferResult {
  8. chain: string;
  9. success: boolean;
  10. sourceAddress: string;
  11. destinationAddress: string;
  12. originalBalance: string;
  13. transferAmount: string;
  14. remainingBalance: string;
  15. transactionHash?: string;
  16. error?: string;
  17. }
  18. const parser = yargs(hideBin(process.argv))
  19. .usage(
  20. "Multi-Chain Balance Transfer Tool for Pyth Entropy Chains\n\nUsage: $0 --source-private-key <key> --destination-address <addr> [chain-selection] [transfer-method] [options]",
  21. )
  22. .options({
  23. "source-private-key": {
  24. type: "string",
  25. demandOption: true,
  26. desc: "Private key of the source wallet to transfer from",
  27. },
  28. "destination-address": {
  29. type: "string",
  30. demandOption: true,
  31. desc: "Public address of the destination wallet",
  32. },
  33. chain: {
  34. type: "array",
  35. string: true,
  36. desc: "Specific chain IDs to transfer on (e.g., --chain optimism_sepolia --chain avalanche)",
  37. },
  38. testnets: {
  39. type: "boolean",
  40. default: false,
  41. desc: "Transfer on all testnet entropy chains",
  42. },
  43. mainnets: {
  44. type: "boolean",
  45. default: false,
  46. desc: "Transfer on all mainnet entropy chains",
  47. },
  48. amount: {
  49. type: "number",
  50. desc: "Exact amount in ETH to transfer from each chain",
  51. },
  52. ratio: {
  53. type: "number",
  54. desc: "Ratio of available balance to transfer (0-1, e.g., 0.5 for half, 1.0 for all)",
  55. },
  56. "min-balance": {
  57. type: "number",
  58. default: 0.001,
  59. desc: "Minimum balance in ETH required before attempting transfer",
  60. },
  61. "gas-multiplier": {
  62. type: "number",
  63. default: 2,
  64. desc: "Gas multiplier for transaction safety",
  65. },
  66. "dry-run": {
  67. type: "boolean",
  68. default: false,
  69. desc: "Preview transfers without executing transactions",
  70. },
  71. })
  72. .group(
  73. ["chain", "testnets", "mainnets"],
  74. "Chain Selection (choose exactly one):",
  75. )
  76. .group(["amount", "ratio"], "Transfer Method (choose exactly one):")
  77. .group(["min-balance", "gas-multiplier", "dry-run"], "Optional Parameters:")
  78. .example([
  79. [
  80. "$0 --source-private-key abc123... --destination-address 0x742d35... --mainnets --amount 0.1",
  81. "Transfer 0.1 ETH from all mainnet chains",
  82. ],
  83. [
  84. "$0 --source-private-key abc123... --destination-address 0x742d35... --testnets --ratio 0.75",
  85. "Transfer 75% of balance from all testnet chains",
  86. ],
  87. [
  88. "$0 --source-private-key abc123... --destination-address 0x742d35... --chain ethereum --chain avalanche --amount 0.05",
  89. "Transfer 0.05 ETH from specific chains",
  90. ],
  91. [
  92. "$0 --source-private-key abc123... --destination-address 0x742d35... --testnets --ratio 0.5 --dry-run",
  93. "Preview 50% transfer on all testnet chains",
  94. ],
  95. ])
  96. .help()
  97. .alias("help", "h")
  98. .version(false);
  99. async function transferOnChain(
  100. chain: EvmChain,
  101. sourcePrivateKey: PrivateKey,
  102. destinationAddress: string,
  103. minBalance: number,
  104. gasMultiplier: number,
  105. dryRun: boolean,
  106. transferAmount?: number,
  107. transferRatio?: number,
  108. ): Promise<TransferResult> {
  109. const web3 = chain.getWeb3();
  110. const signer = web3.eth.accounts.privateKeyToAccount(sourcePrivateKey);
  111. const sourceAddress = signer.address;
  112. try {
  113. // Get balance
  114. const balanceWei = await web3.eth.getBalance(sourceAddress);
  115. const balanceEth = Number(web3.utils.fromWei(balanceWei, "ether"));
  116. console.log(`\n${chain.getId()}: Checking balance for ${sourceAddress}`);
  117. console.log(` Balance: ${balanceEth.toFixed(6)} ETH`);
  118. if (balanceEth < minBalance) {
  119. console.log(
  120. ` Balance below minimum threshold (${minBalance} ETH), skipping`,
  121. );
  122. return {
  123. chain: chain.getId(),
  124. success: false,
  125. sourceAddress,
  126. destinationAddress,
  127. originalBalance: balanceEth.toFixed(6),
  128. transferAmount: "0",
  129. remainingBalance: balanceEth.toFixed(6),
  130. error: `Balance ${balanceEth.toFixed(6)} ETH below minimum ${minBalance} ETH`,
  131. };
  132. }
  133. // Calculate gas costs
  134. const gasPrice = await web3.eth.getGasPrice();
  135. const estimatedGas = await web3.eth.estimateGas({
  136. from: sourceAddress,
  137. to: destinationAddress,
  138. value: "1", // Minimal value for estimation
  139. });
  140. const gasCostWei =
  141. BigInt(estimatedGas) * BigInt(gasPrice) * BigInt(gasMultiplier);
  142. const gasCostEth = Number(
  143. web3.utils.fromWei(gasCostWei.toString(), "ether"),
  144. );
  145. // Calculate transfer amount
  146. let transferAmountEth: number;
  147. if (transferAmount !== undefined) {
  148. transferAmountEth = transferAmount;
  149. } else {
  150. // transferRatio is guaranteed to be defined at this point
  151. transferAmountEth = (balanceEth - gasCostEth) * transferRatio!;
  152. }
  153. // Validate transfer amount
  154. if (transferAmountEth <= 0) {
  155. console.log(
  156. ` Not enough balance to cover transfer and gas costs, skipping`,
  157. );
  158. return {
  159. chain: chain.getId(),
  160. success: false,
  161. sourceAddress,
  162. destinationAddress,
  163. originalBalance: balanceEth.toFixed(6),
  164. transferAmount: "0",
  165. remainingBalance: balanceEth.toFixed(6),
  166. error: `Insufficient balance for transfer amount and gas costs (${gasCostEth.toFixed(6)} ETH)`,
  167. };
  168. }
  169. if (transferAmountEth + gasCostEth > balanceEth) {
  170. console.log(` Transfer amount plus gas costs exceed balance, skipping`);
  171. return {
  172. chain: chain.getId(),
  173. success: false,
  174. sourceAddress,
  175. destinationAddress,
  176. originalBalance: balanceEth.toFixed(6),
  177. transferAmount: "0",
  178. remainingBalance: balanceEth.toFixed(6),
  179. error: `Transfer amount ${transferAmountEth.toFixed(6)} ETH plus gas ${gasCostEth.toFixed(6)} ETH exceeds balance`,
  180. };
  181. }
  182. const transferAmountWei = web3.utils.toWei(
  183. transferAmountEth.toString(),
  184. "ether",
  185. );
  186. console.log(` Transfer amount: ${transferAmountEth.toFixed(6)} ETH`);
  187. console.log(` Estimated gas cost: ${gasCostEth.toFixed(6)} ETH`);
  188. console.log(` Destination: ${destinationAddress}`);
  189. if (dryRun) {
  190. console.log(
  191. ` DRY RUN: Would transfer ${transferAmountEth.toFixed(6)} ETH`,
  192. );
  193. return {
  194. chain: chain.getId(),
  195. success: true,
  196. sourceAddress,
  197. destinationAddress,
  198. originalBalance: balanceEth.toFixed(6),
  199. transferAmount: transferAmountEth.toFixed(6),
  200. remainingBalance: (balanceEth - transferAmountEth).toFixed(6),
  201. };
  202. }
  203. // Perform the transfer
  204. web3.eth.accounts.wallet.add(signer);
  205. console.log(` Executing transfer...`);
  206. const tx = await web3.eth.sendTransaction({
  207. from: sourceAddress,
  208. to: destinationAddress,
  209. value: transferAmountWei,
  210. gas: Number(estimatedGas) * gasMultiplier,
  211. gasPrice: gasPrice,
  212. });
  213. // Get updated balance
  214. const newBalanceWei = await web3.eth.getBalance(sourceAddress);
  215. const newBalanceEth = Number(web3.utils.fromWei(newBalanceWei, "ether"));
  216. console.log(` Transfer successful!`);
  217. console.log(` Transaction hash: ${tx.transactionHash}`);
  218. console.log(` New balance: ${newBalanceEth.toFixed(6)} ETH`);
  219. return {
  220. chain: chain.getId(),
  221. success: true,
  222. sourceAddress,
  223. destinationAddress,
  224. originalBalance: balanceEth.toFixed(6),
  225. transferAmount: transferAmountEth.toFixed(6),
  226. remainingBalance: newBalanceEth.toFixed(6),
  227. transactionHash: tx.transactionHash,
  228. };
  229. } catch (error) {
  230. console.log(` Transfer failed: ${error}`);
  231. return {
  232. chain: chain.getId(),
  233. success: false,
  234. sourceAddress,
  235. destinationAddress,
  236. originalBalance: "unknown",
  237. transferAmount: "0",
  238. remainingBalance: "unknown",
  239. error: error instanceof Error ? error.message : String(error),
  240. };
  241. }
  242. }
  243. function getSelectedChains(argv: {
  244. chain?: string[];
  245. testnets: boolean;
  246. mainnets: boolean;
  247. }): EvmChain[] {
  248. // Check for mutually exclusive options
  249. const optionCount =
  250. (argv.testnets ? 1 : 0) + (argv.mainnets ? 1 : 0) + (argv.chain ? 1 : 0);
  251. if (optionCount !== 1) {
  252. throw new Error(
  253. "Must specify exactly one of: --testnets, --mainnets, or --chain",
  254. );
  255. }
  256. // Get all entropy contract chains
  257. const allEntropyChains: EvmChain[] = [];
  258. for (const contract of Object.values(DefaultStore.entropy_contracts)) {
  259. const chain = contract.getChain();
  260. if (chain instanceof EvmChain) {
  261. allEntropyChains.push(chain);
  262. }
  263. }
  264. let selectedChains: EvmChain[];
  265. if (argv.testnets) {
  266. selectedChains = allEntropyChains.filter((chain) => !chain.isMainnet());
  267. } else if (argv.mainnets) {
  268. selectedChains = allEntropyChains.filter((chain) => chain.isMainnet());
  269. } else {
  270. // Specific chains
  271. const entropyChainIds = new Set(
  272. allEntropyChains.map((chain) => chain.getId()),
  273. );
  274. selectedChains = [];
  275. for (const chainId of argv.chain!) {
  276. if (!entropyChainIds.has(chainId)) {
  277. throw new Error(
  278. `Chain ${chainId} does not have entropy contracts deployed`,
  279. );
  280. }
  281. const chain = DefaultStore.chains[chainId];
  282. if (!(chain instanceof EvmChain)) {
  283. throw new Error(`Chain ${chainId} is not an EVM chain`);
  284. }
  285. selectedChains.push(chain);
  286. }
  287. }
  288. if (selectedChains.length === 0) {
  289. const mode = argv.testnets
  290. ? "testnet"
  291. : argv.mainnets
  292. ? "mainnet"
  293. : "specified";
  294. throw new Error(`No valid ${mode} entropy chains found`);
  295. }
  296. return selectedChains;
  297. }
  298. async function main() {
  299. const argv = await parser.argv;
  300. // Validate inputs
  301. if (!Web3.utils.isAddress(argv.destinationAddress)) {
  302. throw new Error("Invalid destination address format");
  303. }
  304. // Validate transfer amount options
  305. if (argv.amount !== undefined && argv.ratio !== undefined) {
  306. throw new Error("Cannot specify both --amount and --ratio options");
  307. }
  308. if (argv.amount === undefined && argv.ratio === undefined) {
  309. throw new Error("Must specify either --amount or --ratio option");
  310. }
  311. if (argv.ratio !== undefined && (argv.ratio <= 0 || argv.ratio > 1)) {
  312. throw new Error(
  313. "Ratio must be between 0 and 1 (exclusive of 0, inclusive of 1)",
  314. );
  315. }
  316. if (argv.amount !== undefined && argv.amount <= 0) {
  317. throw new Error("Amount must be greater than 0");
  318. }
  319. const sourcePrivateKey = toPrivateKey(argv.sourcePrivateKey);
  320. const selectedChains = getSelectedChains(argv);
  321. // Determine transfer method for display
  322. let transferMethod: string;
  323. if (argv.amount !== undefined) {
  324. transferMethod = `${argv.amount} ETH (fixed amount)`;
  325. } else {
  326. transferMethod = `${(argv.ratio! * 100).toFixed(1)}% of available balance`;
  327. }
  328. console.log(`\nConfiguration:`);
  329. console.log(
  330. ` Network: ${argv.testnets ? "Testnet" : argv.mainnets ? "Mainnet" : "Specific chains"}`,
  331. );
  332. console.log(` Destination: ${argv.destinationAddress}`);
  333. console.log(` Transfer method: ${transferMethod}`);
  334. console.log(` Minimum balance: ${argv.minBalance} ETH`);
  335. console.log(` Gas multiplier: ${argv.gasMultiplier}x`);
  336. console.log(` Dry run: ${argv.dryRun ? "Yes" : "No"}`);
  337. console.log(` Chains: ${selectedChains.map((c) => c.getId()).join(", ")}`);
  338. if (argv.dryRun) {
  339. console.log(`\nRUNNING IN DRY-RUN MODE - NO TRANSACTIONS WILL BE EXECUTED`);
  340. }
  341. const results: TransferResult[] = [];
  342. // Process each chain
  343. for (const chain of selectedChains) {
  344. const result = await transferOnChain(
  345. chain,
  346. sourcePrivateKey,
  347. argv.destinationAddress,
  348. argv.minBalance,
  349. argv.gasMultiplier,
  350. argv.dryRun,
  351. argv.amount,
  352. argv.ratio,
  353. );
  354. results.push(result);
  355. }
  356. // Summary
  357. console.log("\nTRANSFER SUMMARY");
  358. console.log("==================");
  359. const successful = results.filter((r) => r.success);
  360. const failed = results.filter((r) => !r.success);
  361. console.log(`Successful transfers: ${successful.length}`);
  362. console.log(`Failed transfers: ${failed.length}`);
  363. console.log(
  364. `Total transferred: ${successful.reduce((sum, r) => sum + parseFloat(r.transferAmount), 0).toFixed(6)} ETH`,
  365. );
  366. if (successful.length > 0) {
  367. console.log("\nSuccessful Transfers:");
  368. console.table(
  369. successful.map((r) => ({
  370. Chain: r.chain,
  371. "Transfer Amount (ETH)": r.transferAmount,
  372. "TX Hash": r.transactionHash || "N/A (dry run)",
  373. "Remaining Balance (ETH)": r.remainingBalance,
  374. })),
  375. );
  376. }
  377. if (failed.length > 0) {
  378. console.log("\nFailed Transfers:");
  379. console.table(
  380. failed.map((r) => ({
  381. Chain: r.chain,
  382. "Original Balance (ETH)": r.originalBalance,
  383. Error: r.error,
  384. })),
  385. );
  386. }
  387. console.log("\nTransfer process completed!");
  388. }
  389. main().catch((error) => {
  390. console.error("Script failed:", error);
  391. process.exit(1);
  392. });