Skip to content

Instantly share code, notes, and snippets.

@shellcatt
Last active February 28, 2023 11:47
Show Gist options
  • Save shellcatt/1b8dea163e1f238837f515431f505a1a to your computer and use it in GitHub Desktop.
Save shellcatt/1b8dea163e1f238837f515431f505a1a to your computer and use it in GitHub Desktop.
ETH Token Consolidation
// 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