Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: bulk fund crypto wallets #159

Merged
merged 10 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ Note: Do not modify this section! It is auto-generated by `cobra` using `make ge

- [polycli fork](doc/polycli_fork.md) - Take a forked block and walk up the chain to do analysis.

- [polycli fund](doc/polycli_fund.md) - Bulk fund many crypto wallets automatically.

- [polycli hash](doc/polycli_hash.md) - Provide common crypto hashing functions.

- [polycli loadtest](doc/polycli_loadtest.md) - Run a generic load test against an Eth/EVM style JSON-RPC endpoint.
Expand Down
317 changes: 317 additions & 0 deletions cmd/fund/fund.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
package fund

import (
"crypto/ecdsa"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"math/big"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"

_ "embed"

"github.com/chenzhijie/go-web3"
rebelArtists marked this conversation as resolved.
Show resolved Hide resolved
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)

var (
//go:embed usage.md
usage string
walletCount int
fundingWalletPK string
fundingWalletPublicKey *ecdsa.PublicKey
chainRPC string
concurrencyLevel int
walletFundingAmt float64
walletFundingGas uint64
nonceMutex sync.Mutex
globalNonce uint64
nonceInitialized bool
outputFileFlag string
)

// Wallet struct to hold public key, private key, and address
type Wallet struct {
PublicKey *ecdsa.PublicKey
PrivateKey *ecdsa.PrivateKey
Address common.Address
}

func getChainIDFromNode(chainRPC string) (int64, error) {
// Create an HTTP client
client := &http.Client{}

// Prepare the JSON-RPC request payload
payload := `{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}`

// Create the HTTP request
req, err := http.NewRequest("POST", chainRPC, strings.NewReader(payload))
if err != nil {
return 0, err
}

// Set the required headers
req.Header.Set("Content-Type", "application/json")

// Send the request
resp, err := client.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()

// Read the response body
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return 0, err
}

// Parse the JSON response
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return 0, err
}

// Extract the chain ID from the response
chainIDHex, ok := result["result"].(string)
if !ok {
return 0, fmt.Errorf("unable to extract chain ID from response")
}

// Convert the chain ID from hex to int64
int64ChainID, err := strconv.ParseInt(chainIDHex, 0, 64)
if err != nil {
return 0, err
}

return int64ChainID, nil
}

func generateNonce(web3Client *web3.Web3) (uint64, error) {
nonceMutex.Lock()
defer nonceMutex.Unlock()

if nonceInitialized {
globalNonce++
} else {
// Derive the public key from the funding wallet's private key
fundingWalletECDSA, ecdsaErr := crypto.HexToECDSA(fundingWalletPK)
if ecdsaErr != nil {
log.Error().Err(ecdsaErr).Msg("Error getting ECDSA from funding wallet private key")
return 0, ecdsaErr
}

fundingWalletPublicKey = &fundingWalletECDSA.PublicKey
// Convert ecdsa.PublicKey to common.Address
fundingAddress := crypto.PubkeyToAddress(*fundingWalletPublicKey)

nonce, err := web3Client.Eth.GetNonce(fundingAddress, nil)
if err != nil {
log.Error().Err(err).Msg("Error getting nonce")
return 0, err
}
globalNonce = nonce
nonceInitialized = true
}

return globalNonce, nil
}

func generateWallets(numWallets int) ([]Wallet, error) {
wallets := make([]Wallet, numWallets)

for i := 0; i < numWallets; i++ {
account, err := crypto.GenerateKey()
if err != nil {
log.Error().Err(err).Msg("Error generating key")
return nil, err
}

addr := crypto.PubkeyToAddress(account.PublicKey)
wallet := Wallet{
PublicKey: &account.PublicKey,
PrivateKey: account,
Address: addr,
}

wallets[i] = wallet
}
return wallets, nil
}

func fundWallets(web3Client *web3.Web3, wallets []Wallet, amountWei *big.Int, walletFundingGas uint64, concurrency int) error {
// Create a channel to control concurrency
walletChan := make(chan Wallet, len(wallets))
for _, wallet := range wallets {
walletChan <- wallet
}
close(walletChan)

// Wait group to ensure all goroutines finish before returning
var wg sync.WaitGroup
wg.Add(concurrency)

// Function to fund wallets
fundWallet := func() {
defer wg.Done()
for wallet := range walletChan {
nonce, err := generateNonce(web3Client)
if err != nil {
log.Error().Err(err).Msg("Error getting nonce")
return
}

// Fund the wallet using the obtained nonce
_, err = web3Client.Eth.SyncSendRawTransaction(
wallet.Address,
amountWei,
nonce,
walletFundingGas,
web3Client.Utils.ToGWei(1),
nil,
)
if err != nil {
log.Error().Err(err).Str("wallet", wallet.Address.Hex()).Msg("Error funding wallet")
return
}

log.Info().Str("wallet", wallet.Address.Hex()).Msgf("Funded with %s wei", amountWei.String())
}
}

// Start funding the wallets concurrently
for i := 0; i < concurrency; i++ {
go fundWallet()
}

// Wait for all goroutines to finish
wg.Wait()
return nil
}

// fundCmd represents the fund command
var FundCmd = &cobra.Command{
Use: "fund",
Short: "Bulk fund many crypto wallets automatically.",
Long: usage,
Run: func(cmd *cobra.Command, args []string) {
rebelArtists marked this conversation as resolved.
Show resolved Hide resolved
if err := runFunding(cmd); err != nil {
log.Error().Err(err).Msg("Error funding wallets")
}
},
}

func runFunding(cmd *cobra.Command) error {
// Capture the start time
startTime := time.Now()

// Remove '0x' prefix from fundingWalletPK if present
fundingWalletPK = strings.TrimPrefix(fundingWalletPK, "0x")

// setup new web3 session with remote rpc node
web3Client, clientErr := web3.NewWeb3(chainRPC)
if clientErr != nil {
cmd.PrintErrf("There was an error creating web3 client: %s", clientErr.Error())
return clientErr
}

// add pk to session for sending signed transactions
if setAcctErr := web3Client.Eth.SetAccount(fundingWalletPK); setAcctErr != nil {
cmd.PrintErrf("There was an error setting account with pk: %s", setAcctErr.Error())
return setAcctErr
}

// Query the chain ID from the rpc node
chainID, chainIDErr := getChainIDFromNode(chainRPC)
if chainIDErr != nil {
log.Error().Err(chainIDErr).Msg("Error getting chain ID")
return chainIDErr
}

// Set proper chainId for corresponding chainRPC
web3Client.Eth.SetChainId(chainID)

// generate set of new wallet objects
wallets, genWalletErr := generateWallets(walletCount)
if genWalletErr != nil {
cmd.PrintErrf("There was an error generating wallet objects: %s", genWalletErr.Error())
return genWalletErr
}

// fund all crypto wallets
log.Info().Msg("Starting to fund loadtest wallets...")
fundWalletErr := fundWallets(web3Client, wallets, big.NewInt(int64(walletFundingAmt*1e18)), uint64(walletFundingGas), concurrencyLevel)
if fundWalletErr != nil {
log.Error().Err(fundWalletErr).Msg("Error funding wallets")
return fundWalletErr
}

// Save wallet details to a file
outputFile := outputFileFlag // You can modify the file format or name as needed

type WalletDetails struct {
Address string `json:"Address"`
PrivateKey string `json:"PrivateKey"`
}

walletDetails := make([]WalletDetails, len(wallets))
for i, w := range wallets {
rebelArtists marked this conversation as resolved.
Show resolved Hide resolved
privateKey := hex.EncodeToString(w.PrivateKey.D.Bytes()) // Convert private key to hex
walletDetails[i] = WalletDetails{
Address: w.Address.Hex(),
PrivateKey: privateKey,
}
}

// Convert walletDetails to JSON
walletsJSON, jsonErr := json.MarshalIndent(walletDetails, "", " ")
if jsonErr != nil {
log.Error().Err(jsonErr).Msg("Error converting wallet details to JSON")
return jsonErr
}

// Write JSON data to a file
file, createErr := os.Create(outputFile)
if createErr != nil {
log.Error().Err(createErr).Msg("Error creating file")
return createErr
}
defer file.Close()

_, writeErr := file.Write(walletsJSON)
if writeErr != nil {
log.Error().Err(writeErr).Msg("Error writing wallet details to file")
return writeErr
}

log.Info().Msgf("Wallet details have been saved to %s", outputFile)

// Calculate the duration
duration := time.Since(startTime)
log.Info().Msgf("Total execution time: %s", duration)

return nil
}

func init() {
// Configure zerolog to output to os.Stdout
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout})

FundCmd.Flags().IntVar(&walletCount, "wallet-count", 2, "Number of wallets to fund")
FundCmd.Flags().StringVar(&fundingWalletPK, "funding-wallet-pk", "", "Corresponding private key for funding wallet address, ensure you remove leading 0x")
rebelArtists marked this conversation as resolved.
Show resolved Hide resolved
FundCmd.Flags().StringVar(&chainRPC, "rpc-url", "http://localhost:8545", "The RPC endpoint url")
FundCmd.Flags().IntVar(&concurrencyLevel, "concurrency", 2, "Concurrency level for speeding up funding wallets")
FundCmd.Flags().Float64Var(&walletFundingAmt, "wallet-funding-amt", 0.05, "Amount to fund each wallet with")
FundCmd.Flags().Uint64Var(&walletFundingGas, "wallet-funding-gas", 100000, "Gas for each wallet funding transaction")
FundCmd.Flags().StringVar(&outputFileFlag, "output-file", "funded_wallets.json", "Specify the output JSON file name")
}
10 changes: 10 additions & 0 deletions cmd/fund/usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
```bash
$ polycli fund \
--rpc-url="https://rootchain-devnetsub.zkevmdev.net" \
--funding-wallet-pk="REPLACE" \
--wallet-count=5 \
--wallet-funding-amt=0.00015 \
--wallet-funding-gas=50000 \
--concurrency=5 \
--output-file="/opt/funded_wallets.json"
```
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/maticnetwork/polygon-cli/cmd/abi"
"github.com/maticnetwork/polygon-cli/cmd/dumpblocks"
"github.com/maticnetwork/polygon-cli/cmd/enr"
"github.com/maticnetwork/polygon-cli/cmd/fund"
"github.com/maticnetwork/polygon-cli/cmd/hash"
"github.com/maticnetwork/polygon-cli/cmd/loadtest"
"github.com/maticnetwork/polygon-cli/cmd/metricsToDash"
Expand Down Expand Up @@ -106,6 +107,7 @@ func NewPolycliCommand() *cobra.Command {
abi.ABICmd,
dumpblocks.DumpblocksCmd,
fork.ForkCmd,
fund.FundCmd,
hash.HashCmd,
enr.ENRCmd,
dbbench.DBBenchCmd,
Expand Down
2 changes: 2 additions & 0 deletions doc/polycli.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ Polycli is a collection of tools that are meant to be useful while building, tes

- [polycli fork](polycli_fork.md) - Take a forked block and walk up the chain to do analysis.

- [polycli fund](polycli_fund.md) - Bulk fund many crypto wallets automatically.

- [polycli hash](polycli_hash.md) - Provide common crypto hashing functions.

- [polycli loadtest](polycli_loadtest.md) - Run a generic load test against an Eth/EVM style JSON-RPC endpoint.
Expand Down
Loading