Skip to content

Commit 8627f1f

Browse files
authored
feat: Add a key file to the fund command (#672)
* feat: add key-file to fund command * fund key-file flag changes * review adjusts * make lint
1 parent e2b5f73 commit 8627f1f

File tree

9 files changed

+396
-308
lines changed

9 files changed

+396
-308
lines changed

cmd/fund/cmd.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ type cmdFundParams struct {
3131
FundingAmountInWei *big.Int
3232
OutputFile *string
3333

34+
KeyFile *string
35+
3436
FunderAddress *string
3537
}
3638

@@ -71,12 +73,16 @@ func init() {
7173
p.WalletAddresses = flagSet.StringSlice("addresses", nil, "Comma-separated list of wallet addresses to fund")
7274
p.FundingAmountInWei = defaultFundingInWei
7375
flagSet.Var(&flag_loader.BigIntValue{Val: p.FundingAmountInWei}, "eth-amount", "The amount of wei to send to each wallet")
76+
p.KeyFile = flagSet.String("key-file", "", "The file containing the accounts private keys, one per line.")
7477

7578
p.OutputFile = flagSet.StringP("file", "f", "wallets.json", "The output JSON file path for storing the addresses and private keys of funded wallets")
7679

7780
// Marking flags as mutually exclusive
7881
FundCmd.MarkFlagsMutuallyExclusive("addresses", "number")
7982
FundCmd.MarkFlagsMutuallyExclusive("addresses", "hd-derivation")
83+
FundCmd.MarkFlagsMutuallyExclusive("key-file", "addresses")
84+
FundCmd.MarkFlagsMutuallyExclusive("key-file", "number")
85+
FundCmd.MarkFlagsMutuallyExclusive("key-file", "hd-derivation")
8086

8187
// Funder contract parameters.
8288
p.FunderAddress = flagSet.String("contract-address", "", "The address of a pre-deployed Funder contract")
@@ -98,10 +104,14 @@ func checkFlags() error {
98104
return errors.New("the private key is empty")
99105
}
100106

101-
// Check wallet flags.
102-
if params.WalletsNumber != nil && *params.WalletsNumber == 0 {
103-
return errors.New("the number of wallets to fund is set to zero")
107+
// Check that exactly one method is used to specify target accounts
108+
hasAddresses := params.WalletAddresses != nil && len(*params.WalletAddresses) > 0
109+
hasKeyFile := params.KeyFile != nil && *params.KeyFile != ""
110+
hasNumberFlag := params.WalletsNumber != nil && *params.WalletsNumber > 0
111+
if !hasAddresses && !hasKeyFile && !hasNumberFlag {
112+
return errors.New("must specify target accounts via --addresses, --key-file, or --number")
104113
}
114+
105115
minValue := big.NewInt(1000000000)
106116
if params.FundingAmountInWei != nil && params.FundingAmountInWei.Cmp(minValue) <= 0 {
107117
return errors.New("the funding amount must be greater than 1000000000")

cmd/fund/fund.go

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/hex"
77
"encoding/json"
88
"errors"
9+
"fmt"
910
"math/big"
1011
"os"
1112
"strings"
@@ -49,21 +50,14 @@ func runFunding(ctx context.Context) error {
4950
return err
5051
}
5152

52-
// Derive or generate a set of wallets.
5353
var addresses []common.Address
54-
if params.WalletAddresses != nil && *params.WalletAddresses != nil {
55-
log.Info().Msg("Using addresses provided by the user")
56-
addresses = make([]common.Address, len(*params.WalletAddresses))
57-
for i, address := range *params.WalletAddresses {
58-
addresses[i] = common.HexToAddress(address)
59-
}
60-
} else if *params.UseHDDerivation {
61-
log.Info().Msg("Deriving wallets from the default mnemonic")
62-
addresses, err = deriveHDWallets(int(*params.WalletsNumber))
63-
} else {
64-
log.Info().Msg("Generating random wallets")
65-
addresses, err = generateWallets(int(*params.WalletsNumber))
54+
55+
if len(*params.KeyFile) > 0 { // get addresses from key-file
56+
addresses, err = getAddressesFromKeyFile(*params.KeyFile)
57+
} else { // get addresses from private key
58+
addresses, err = getAddressesFromPrivateKey(ctx, c)
6659
}
60+
// check errors after getting addresses
6761
if err != nil {
6862
return err
6963
}
@@ -85,6 +79,57 @@ func runFunding(ctx context.Context) error {
8579
return nil
8680
}
8781

82+
func getAddressesFromKeyFile(keyFilePath string) ([]common.Address, error) {
83+
if len(keyFilePath) == 0 {
84+
return nil, errors.New("the key file path is empty")
85+
}
86+
87+
log.Trace().
88+
Str("keyFilePath", keyFilePath).
89+
Msg("getting addresses from key file")
90+
91+
privateKeys, iErr := util.ReadPrivateKeysFromFile(keyFilePath)
92+
if iErr != nil {
93+
log.Error().
94+
Err(iErr).
95+
Msg("Unable to read private keys from key file")
96+
return nil, fmt.Errorf("unable to read private keys from key file. %w", iErr)
97+
}
98+
addresses := make([]common.Address, len(privateKeys))
99+
for i, privateKey := range privateKeys {
100+
addresses[i] = util.GetAddress(context.Background(), privateKey)
101+
log.Trace().
102+
Interface("address", addresses[i]).
103+
Str("privateKey", hex.EncodeToString(privateKey.D.Bytes())).
104+
Msg("New wallet derived from key file")
105+
}
106+
log.Info().Int("count", len(addresses)).Msg("Wallet(s) derived from key file")
107+
return addresses, nil
108+
}
109+
110+
func getAddressesFromPrivateKey(ctx context.Context, c *ethclient.Client) ([]common.Address, error) {
111+
// Derive or generate a set of wallets.
112+
var addresses []common.Address
113+
var err error
114+
if params.WalletAddresses != nil && *params.WalletAddresses != nil {
115+
log.Info().Msg("Using addresses provided by the user")
116+
addresses = make([]common.Address, len(*params.WalletAddresses))
117+
for i, address := range *params.WalletAddresses {
118+
addresses[i] = common.HexToAddress(address)
119+
}
120+
} else if *params.UseHDDerivation {
121+
log.Info().Msg("Deriving wallets from the default mnemonic")
122+
addresses, err = deriveHDWallets(int(*params.WalletsNumber))
123+
} else {
124+
log.Info().Msg("Generating random wallets")
125+
addresses, err = generateWallets(int(*params.WalletsNumber))
126+
}
127+
if err != nil {
128+
return nil, err
129+
}
130+
return addresses, nil
131+
}
132+
88133
// dialRpc dials the Ethereum RPC server and return an Ethereum client.
89134
func dialRpc(ctx context.Context) (*ethclient.Client, error) {
90135
rpc, err := rpc.DialContext(ctx, *params.RpcUrl)

cmd/fund/usage.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ $ polycli fund --number 20 --hd-derivation=false
2424
3:58PM INF Wallets' address(es) and private key(s) saved to file fileName=wallets.json
2525
3:58PM INF Wallet(s) funded! 💸
2626
3:58PM INF Total execution time: 1.027506s
27+
28+
# Fund wallets from a key file (one private key in hex per line).
29+
$ polycli fund --key-file=keys.txt
30+
3:58PM INF Starting bulk funding wallets
31+
3:58PM INF Wallet(s) derived from key file count=3
32+
3:58PM INF Wallet(s) funded! 💸
33+
3:58PM INF Total execution time: 1.2s
2734
```
2835
2936
Extract from `wallets.json`.

cmd/loadtest/account.go

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"sync"
1212
"time"
1313

14+
"github.com/0xPolygon/polygon-cli/util"
1415
"github.com/ethereum/go-ethereum/accounts/abi/bind"
1516
"github.com/ethereum/go-ethereum/common"
1617
"github.com/ethereum/go-ethereum/core/types"
@@ -202,7 +203,7 @@ func (ap *AccountPool) Add(ctx context.Context, privateKey *ecdsa.PrivateKey, st
202203
return fmt.Errorf("failed to create account: %w", err)
203204
}
204205

205-
addressHex, privateKeyHex := getAddressAndPrivateKeyHex(ctx, privateKey)
206+
addressHex, privateKeyHex := util.GetAddressAndPrivateKeyHex(ctx, privateKey)
206207
log.Debug().
207208
Str("address", addressHex).
208209
Str("privateKey", privateKeyHex).
@@ -504,7 +505,7 @@ func (ap *AccountPool) ReturnFunds(ctx context.Context) error {
504505
txCh := make(chan *types.Transaction, len(ap.accounts))
505506
errCh := make(chan error, len(ap.accounts))
506507

507-
fundingAddressHex, _ := getAddressAndPrivateKeyHex(ctx, ap.fundingPrivateKey)
508+
fundingAddressHex, _ := util.GetAddressAndPrivateKeyHex(ctx, ap.fundingPrivateKey)
508509
fundingAddress := common.HexToAddress(fundingAddressHex)
509510

510511
err = ap.clientRateLimiter.Wait(ctx)
@@ -934,14 +935,3 @@ func (ap *AccountPool) createEOATransferTx(ctx context.Context, sender *ecdsa.Pr
934935

935936
return signedTx, nil
936937
}
937-
938-
// Returns the address and private key of the given private key
939-
func getAddressAndPrivateKeyHex(ctx context.Context, privateKey *ecdsa.PrivateKey) (string, string) {
940-
privateKeyBytes := crypto.FromECDSA(privateKey)
941-
privateKeyHex := fmt.Sprintf("0x%x", privateKeyBytes)
942-
943-
publicKey := privateKey.Public().(*ecdsa.PublicKey)
944-
address := crypto.PubkeyToAddress(*publicKey)
945-
946-
return address.String(), privateKeyHex
947-
}

cmd/loadtest/loadtest.go

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package loadtest
22

33
import (
4-
"bufio"
54
"context"
6-
"crypto/ecdsa"
75
_ "embed"
86
"encoding/hex"
97
"encoding/json"
@@ -317,7 +315,7 @@ func initializeLoadTestParams(ctx context.Context, c *ethclient.Client) error {
317315
Str("sendingAccountsFile", sendingAccountsFile).
318316
Msg("Adding accounts from file to the account pool")
319317

320-
privateKeys, iErr := readPrivateKeysFromFile(sendingAccountsFile)
318+
privateKeys, iErr := util.ReadPrivateKeysFromFile(sendingAccountsFile)
321319
if iErr != nil {
322320
log.Error().
323321
Err(iErr).
@@ -388,35 +386,6 @@ func initializeLoadTestParams(ctx context.Context, c *ethclient.Client) error {
388386
return nil
389387
}
390388

391-
func readPrivateKeysFromFile(sendingAccountsFile string) ([]*ecdsa.PrivateKey, error) {
392-
file, err := os.Open(sendingAccountsFile)
393-
if err != nil {
394-
return nil, fmt.Errorf("unable to open sending accounts file: %w", err)
395-
}
396-
defer file.Close()
397-
398-
var privateKeys []*ecdsa.PrivateKey
399-
scanner := bufio.NewScanner(file)
400-
for scanner.Scan() {
401-
line := strings.TrimSpace(scanner.Text())
402-
if len(line) == 0 {
403-
continue
404-
}
405-
privateKey, err := ethcrypto.HexToECDSA(strings.TrimPrefix(line, "0x"))
406-
if err != nil {
407-
log.Error().Err(err).Str("key", line).Msg("Unable to parse private key")
408-
return nil, fmt.Errorf("unable to parse private key: %w", err)
409-
}
410-
privateKeys = append(privateKeys, privateKey)
411-
}
412-
413-
if err := scanner.Err(); err != nil {
414-
return nil, fmt.Errorf("error reading sending accounts file: %w", err)
415-
}
416-
417-
return privateKeys, nil
418-
}
419-
420389
func completeLoadTest(ctx context.Context, c *ethclient.Client, rpc *ethrpc.Client) error {
421390
if *inputLoadTestParams.FireAndForget {
422391
log.Info().

doc/polycli_fund.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ $ polycli fund --number 20 --hd-derivation=false
4545
3:58PM INF Wallets' address(es) and private key(s) saved to file fileName=wallets.json
4646
3:58PM INF Wallet(s) funded! 💸
4747
3:58PM INF Total execution time: 1.027506s
48+
49+
# Fund wallets from a key file (one private key in hex per line).
50+
$ polycli fund --key-file=keys.txt
51+
3:58PM INF Starting bulk funding wallets
52+
3:58PM INF Wallet(s) derived from key file count=3
53+
3:58PM INF Wallet(s) funded! 💸
54+
3:58PM INF Total execution time: 1.2s
4855
```
4956
5057
Extract from `wallets.json`.
@@ -83,6 +90,7 @@ $ cast balance 0x5D8121cf716B70d3e345adB58157752304eED5C3
8390
-f, --file string The output JSON file path for storing the addresses and private keys of funded wallets (default "wallets.json")
8491
--hd-derivation Derive wallets to fund from the private key in a deterministic way (default true)
8592
-h, --help help for fund
93+
--key-file string The file containing the accounts private keys, one per line.
8694
-n, --number uint The number of wallets to fund (default 10)
8795
--private-key string The hex encoded private key that we'll use to send transactions (default "0x42b6e34dc21598a807dc19d7784c71b2a7a01f6480dc6f58258f78e539f1a1fa")
8896
-r, --rpc-url string The RPC endpoint url (default "http://localhost:8545")

0 commit comments

Comments
 (0)