Last active
February 28, 2023 11:47
-
-
Save shellcatt/1b8dea163e1f238837f515431f505a1a to your computer and use it in GitHub Desktop.
ETH Token Consolidation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Get private stuff from my .env file | |
require('dotenv').config(); | |
const Config = process.env; | |
// Utils | |
const fs = require('fs'); | |
const util = require('util'); | |
const BigNumber = require('bignumber.js'); | |
// Ethereum javascript libraries needed | |
const Tx = require('ethereumjs-tx'); | |
const Web3 = require('web3'); | |
// Rather than using a local copy of geth, interact with the ethereum blockchain via infura.io | |
const w3 = new Web3(Web3.givenProvider || `https://mainnet.infura.io/v3/` + Config.INFURA_API_KEY) | |
const tokenContract = new w3.eth.Contract(getMinABI(), Config.CONTRACT_ADDRESS); | |
const DEBUG = true; | |
/** | |
* @description Round trip to all source addresses & then to the destination | |
* @param contract Token Smart contract address | |
* @param fromKeys Source wallets private keys | |
* @param toAddress Destination wallet public key | |
* @param feesFrom Fees wallet private key | |
*/ | |
module.exports.roundCollect = async function roundCollect (contract, fromKeys, toAddress, feesFrom = null) { | |
// This code was written and tested using web3 version 1.0.0-beta.26 | |
console.log(`web3 version: ${w3.version}`) | |
// Check `feesFrom` access & balance | |
// Have private keys in a wallet of accounts & setup address ring | |
let fromAddresses = []; | |
const wallet = w3.eth.accounts.wallet.create(0); | |
fromKeys.forEach(key => { | |
let account = w3.eth.accounts.privateKeyToAccount(hexpad(key)); | |
fromAddresses.push(account.address); | |
wallet.add(account); | |
}); // console.log(wallet); | |
// Setup sponsor account for the consolidation | |
let feesFromAccount = w3.eth.accounts.privateKeyToAccount(hexpad(feesFrom)); | |
// Get current gas price | |
const gasPrice = await getGasPrice(w3); | |
// Estimage gas for first token transfer only & multiply it by the number of hops | |
let gasEst = await sendEstimateGas(contract, feesFromAccount.address, wallet[0].address); | |
let estFee = gasEst * gasPrice; | |
// Estimate gas for number of addresses to sweap | |
// plus fee for first, last & n=(2) insurances on top, in case gas cost changes during execution | |
var totalTransferCost = (estFee * (fromKeys.length + 1 + 1 + 2)); | |
console.log('single fee ', w3.utils.fromWei(estFee.toString()), | |
' ~ $' + w3.utils.fromWei((estFee * Config.ETH_PRICE).toString())); | |
console.log('total costs', w3.utils.fromWei(totalTransferCost.toString()), | |
' ~ $' + w3.utils.fromWei((totalTransferCost * Config.ETH_PRICE).toString())); | |
// return; | |
// Send totalTransferCost to the first on the ring | |
let fundingGasEst = await w3.eth.estimateGas({ | |
from: feesFromAccount.address, | |
to: wallet[0].address, | |
value: totalTransferCost | |
}); | |
// console.log('fundingFeeEst', web3.utils.fromWei((fundingGasEst*gasPrice).toString()), | |
// ' ~ $' + web3.utils.fromWei(((fundingGasEst*gasPrice) * ETH_PRICE).toString())); | |
let tripFee = fundingGasEst * gasPrice; | |
if (!DEBUG) // Mind actual execution | |
await sendEther(feesFromAccount, wallet[0].address, totalTransferCost - tripFee); | |
console.log( "\n", | |
util.format('%s sends %s ether to %s for %s fee', | |
feesFromAccount.address, | |
w3.utils.fromWei((totalTransferCost - tripFee).toString()), | |
wallet[0].address, | |
w3.utils.fromWei( tripFee.toString() ) | |
), "\n" | |
) | |
// Go through all accounts | |
var totalBalance = new BigNumber(0), | |
totalValue = new BigNumber(totalTransferCost.toString()); | |
for (let i = 0; i < fromAddresses.length; i++) { | |
const element = fromAddresses[i]; | |
const len = fromAddresses.length; | |
const prev = fromAddresses[ (i + len - 1) % len ]; | |
const next = fromAddresses[ (i + 1) % len ]; | |
// Recalculate fee every time | |
let hopGasEst = await sendEstimateGas(contract, element, wallet[next].address); | |
// Soft balances storage just in case | |
let hopBalance = await contract.methods.balanceOf(element).call(); | |
totalBalance = totalBalance.plus(hopBalance); | |
totalValue = new BigNumber( (totalTransferCost - hopGasEst*gasPrice * (i+1)) ); | |
let hopValue = totalValue; | |
// Stop on final element | |
if (next == prev) continue; // break; | |
if (!DEBUG) // Mind actual execution | |
await sendToken(contract, wallet[element], wallet[next].address, hopBalance, hopValue.toString()); | |
console.log( | |
util.format(' %s sends %s ether & %s tokens to %s for %s fee', | |
element, | |
w3.utils.fromWei(hopValue.toString()), | |
w3.utils.fromWei(hopBalance.toString()), | |
wallet[next].address, | |
w3.utils.fromWei( (hopGasEst*gasPrice).toString() ) | |
) | |
) | |
} | |
// Send all funds (ether & tokens) to destination address | |
// await sendEther(feesFromAccount, wallet[0].address, totalTransferCost - tripFee); | |
gasEst = await sendEstimateGas(contract, wallet[wallet.length-1].address, toAddress); | |
console.log( "\n", | |
util.format('%s sends %s ether & %s tokens back to %s for %s fee', | |
wallet[wallet.length-1].address, | |
w3.utils.fromWei(totalValue.toString()), | |
w3.utils.fromWei(totalBalance.toString()), | |
toAddress, | |
w3.utils.fromWei((gasEst*gasPrice).toString()) | |
) | |
) | |
let estTransferCost = new BigNumber(totalTransferCost.toString()); | |
console.log( "\n", | |
// web3.utils.fromWei(estTransferCost.toString()), | |
// web3.utils.fromWei( totalValue.toString() ), | |
// web3.utils.fromWei( estTransferCost.minus(totalValue).toString() ), | |
'leftovers', w3.utils.fromWei( estTransferCost.minus(totalValue).toString() ), | |
' ~ $' + w3.utils.fromWei( (estTransferCost.minus(totalValue).times(Config.ETH_PRICE)).toString() ), | |
); | |
} | |
/** | |
* @description Parallel collection from all source addresses to the destination | |
* @param contract Token Smart contract address | |
* @param fromKeys Source wallets private keys | |
* @param toAddress Destination wallet public key | |
* @param feesFrom Fees wallet private key | |
*/ | |
module.exports.rayCollect = async function rayCollect (contract, fromKeys, toAddress, feesFrom = null) { | |
// This code was written and tested using web3 version 1.0.0-beta.26 | |
console.log(`web3 version: ${w3.version}`) | |
// Check `feesFrom` access & balance | |
// Have private keys in a wallet of accounts & setup address ring | |
let fromAddresses = []; | |
const wallet = w3.eth.accounts.wallet.create(0); | |
fromKeys.forEach(key => { | |
let account = w3.eth.accounts.privateKeyToAccount(hexpad(key)); | |
fromAddresses.push(account.address); | |
wallet.add(account); | |
}); // console.log(wallet); | |
// Setup sponsor account for the consolidation | |
let feesFromAccount = w3.eth.accounts.privateKeyToAccount(hexpad(feesFrom)); | |
// Get current gas price | |
const gasPrice = await getGasPrice(w3); | |
// Estimage gas for first token transfer only & multiply it by the number of hops | |
let fundGasEst = await w3.eth.estimateGas({ | |
from: feesFromAccount.address, | |
to: wallet[0].address, value: w3.utils.toWei('1', 'ether') | |
}); | |
var collectGasEst = await sendEstimateGas(contract, wallet[0].address, toAddress); | |
var estFee = (collectGasEst + fundGasEst) * gasPrice; | |
// Estimate gas for number of addresses to sweap | |
// plus fee for first, last & n=(2) insurances on top, jic | |
var totalTransferCost = (estFee * (fromKeys.length)); | |
console.log("funding fee \t\t", w3.utils.fromWei((fundGasEst * gasPrice).toString()), | |
' ~ $' + w3.utils.fromWei((fundGasEst * gasPrice * Config.ETH_PRICE).toString())); | |
console.log("collecting fee \t\t", w3.utils.fromWei((collectGasEst * gasPrice).toString()), | |
' ~ $' + w3.utils.fromWei((collectGasEst * gasPrice * Config.ETH_PRICE).toString())); | |
console.log("total transfer fees\t", w3.utils.fromWei(totalTransferCost.toString()), | |
' ~ $' + w3.utils.fromWei((totalTransferCost * Config.ETH_PRICE).toString()), "\n"); | |
// return; | |
totalTransferCost = 0; | |
// Go through all accounts | |
for (let i = 0; i < fromAddresses.length; i++) { | |
const element = fromAddresses[i]; | |
// console.log('private/public', wallet[element].privateKey, element); | |
// console.log(" > prev/next", wallet[prev].address, wallet[next].address); | |
// Recalculate fee every time | |
let hopGasEst = await sendEstimateGas(contract, element, toAddress); | |
let tripFee = (fundGasEst + hopGasEst) * gasPrice; | |
// Soft balances storage just in case | |
let hopBalance = await contract.methods.balanceOf(element).call(); | |
let hopValue = await w3.eth.getBalance(element); // supposed to be near 0 | |
// console.log('>> totalBalance = ', totalBalance); | |
// console.log('>> totalValue = ', web3.utils.fromWei(totalValue.toString())); | |
// Fund account for sending | |
if (!DEBUG) // Mind actual execution | |
await sendEther(feesFromAccount, element, tripFee); | |
console.log( | |
util.format('--> %s sends %s ether to %s for %s fee', | |
feesFromAccount.address, | |
w3.utils.fromWei(tripFee.toString()), | |
element, | |
w3.utils.fromWei( (fundGasEst*gasPrice).toString() ) | |
) | |
) | |
// Send tokens to destination address | |
if (!DEBUG) // Mind actual execution | |
await sendToken(contract, wallet[element], toAddress, hopBalance, hopValue); | |
console.log( | |
util.format('<-- %s sends %s tokens to %s for %s fee', | |
element, | |
w3.utils.fromWei(hopBalance.toString()), | |
toAddress, | |
w3.utils.fromWei( (hopGasEst*gasPrice).toString() ) | |
) | |
); | |
// Calculate total transfer fee | |
totalTransferCost += fundGasEst*gasPrice + hopGasEst*gasPrice; | |
} | |
console.log("\n", "actual transfer costs\t", w3.utils.fromWei(totalTransferCost.toString()), | |
' ~ $' + w3.utils.fromWei((totalTransferCost * Config.ETH_PRICE).toString())); | |
} | |
async function sendEther(fromAccount, toAddress, value) { | |
// Estimate Gas | |
const gasPrice = await getGasPrice(w3); | |
let gasEst = await w3.eth.estimateGas({ | |
from: fromAccount.address, | |
to: toAddress, | |
value: value | |
}); | |
// Determine the nonce as late in the execution as possible | |
let nonce = await getNonce(fromAccount.address); | |
const signedTransaction = await fromAccount.signTransaction({ | |
to: toAddress, | |
value: value, | |
gasPrice: gasPrice, | |
gas: gasEst, | |
nonce: nonce | |
}); | |
return w3.eth.sendSignedTransaction(signedTransaction.rawTransaction).catch((error) => { | |
console.error(error); | |
}); | |
} | |
/** | |
* @description Move ERC20 Token from ${fromAddress} to ${toAddress}, | |
* when having previously ensured that there's enough ETH in the ${fromAddress} for the fee. | |
* The rest of the remaining ETH is also moved to ${toAddress-} | |
* @global web3 | |
* @global minABI | |
* @param contract | |
* @param fromAddress | |
* @param toAddress | |
*/ | |
async function sendToken(contract, fromAccount, toAddress, amount = 0, value = '0x0') { | |
// How many tokens do I have before sending? | |
let balance = amount || await contract.methods.balanceOf(fromAccount.address).call(); | |
// Get decimals | |
let decimals = await contract.methods.decimals().call();//.then((error, decimals) => { | |
console.log(`Balance before send: ${balance / (10**decimals)}`); | |
// Estimate Gas | |
const gasPrice = await getGasPrice(w3); | |
let gasEst = await sendEstimateGas(contract, fromAccount.address, toAddress, balance); | |
// Determine the nonce as late in the execution as possible | |
let nonce = await getNonce(fromAccount.address); | |
// I chose gas price and gas limit based on what ethereum wallet was recommending for a similar transaction. | |
// You may need to change the gas price! | |
let rawTransaction = { | |
"from": fromAccount.address, | |
"nonce": nonce, | |
"gasPrice": w3.utils.toHex(gasPrice), | |
"gas": w3.utils.toHex(gasEst), | |
"to": contract.options.address, | |
"value": value, | |
"data": contract.methods.transfer(toAddress, balance).encodeABI(), | |
"chainId": Config.CHAIN_ID | |
}; | |
// The private key must be for fromAddress | |
const privKey = Buffer.from(fromAccount.privateKey.substr(2), 'hex'); | |
let tx = new Tx(rawTransaction); | |
tx.sign(privKey); | |
let serializedTx = tx.serialize(); | |
// console.log('raw transaction', rawTransaction); | |
// return; | |
// Comment out these three lines if you don't really want to send the TX right now | |
console.log(`Attempting to send signed tx: ${serializedTx.toString('hex')}`); | |
var receipt = await w3.eth.sendSignedTransaction(hexpad(serializedTx.toString('hex'))); | |
console.log(`Receipt info: ${JSON.stringify(receipt, null, '\t')}`); | |
// // The balance may not be updated yet, but let's check | |
// balance = await contract.methods.balanceOf(fromAddress).call(); | |
// console.log(`Balance after send: ${balance}`); | |
} | |
/// Helper functions | |
async function sendEstimateGas(contract, fromAddress, toAddress, balance = null, value = 0x0) { | |
if (!balance) { | |
balance = await contract.methods.balanceOf(fromAddress).call(); | |
} | |
let estGas = await contract.methods.transfer(toAddress, balance).estimateGas({ | |
// let estGas = await web3.eth.estimateGas({ | |
from: fromAddress, | |
to: toAddress, | |
balance: balance, | |
value: value, | |
data: contract.methods.transfer(toAddress, balance).encodeABI(), | |
}, (error, result) => { | |
///TODO: throw exception to handle on top | |
}); | |
return estGas; | |
} | |
async function getGasPrice(web3) { | |
let gasPrice = await web3.eth.getGasPrice(); | |
if (gasPrice && gasPrice < Config.ETH_MIN_GAS_PRICE) { | |
gasPrice = Config.ETH_MIN_GAS_PRICE; | |
} | |
return gasPrice ? gasPrice : Config.ETH_AVG_GAS_PRICE; | |
} | |
/** | |
* @description Determine the nonce, include pending transactions also | |
* @param address origin | |
*/ | |
async function getNonce(address) { | |
const count = await w3.eth.getTransactionCount(address, 'pending'); | |
return w3.utils.toHex(count); | |
} | |
/** | |
* @description Used as a replacement to the actual contract ABI, better off since ERC20 defines these as a standard | |
* @returns {object} ABI JSON | |
*/ | |
function getMinABI () { | |
return [ // https://github.com/ethereum/wiki/wiki/Contract-ERC20-ABI | |
{ // balanceOf | |
"constant":true, | |
"inputs":[{"name":"_owner","type":"address"}], | |
"name":"balanceOf", | |
"outputs":[{"name":"balance","type":"uint256"}], | |
"type":"function" | |
}, | |
{ // decimals | |
"constant":true, | |
"inputs":[], | |
"name":"decimals", | |
"outputs":[{"name":"","type":"uint8"}], | |
"type":"function" | |
}, | |
{ // transfer | |
"constant": false, | |
"inputs": [{"name": "_to","type": "address"}, {"name": "_value","type": "uint256"}], | |
"name": "transfer", | |
"outputs": [{"name": "","type": "bool"}], | |
"payable": false, | |
"stateMutability": "nonpayable", | |
"type": "function" | |
} | |
]; | |
}; | |
function hexpad(k) { | |
return (k.substr(2) == '0x' ? k : ('0x' + k)); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment