Skip to content

Commit 3dfb291

Browse files
committed
feat: fund from seed + fix broken flags
Signed-off-by: Ji Hwan <[email protected]>
1 parent eb01375 commit 3dfb291

File tree

3 files changed

+136
-46
lines changed

3 files changed

+136
-46
lines changed

cmd/fund/cmd.go

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type cmdFundParams struct {
3232
OutputFile string
3333

3434
KeyFile string
35+
Seed string
3536

3637
FunderAddress string
3738
}
@@ -63,33 +64,34 @@ var FundCmd = &cobra.Command{
6364
}
6465

6566
func init() {
66-
p := new(cmdFundParams)
6767
f := FundCmd.Flags()
6868

69-
f.StringVarP(&p.RpcUrl, "rpc-url", "r", "http://localhost:8545", "RPC endpoint URL")
70-
f.StringVar(&p.PrivateKey, "private-key", defaultPrivateKey, "hex encoded private key to use for sending transactions")
69+
f.StringVarP(&params.RpcUrl, "rpc-url", "r", "http://localhost:8545", "RPC endpoint URL")
70+
f.StringVar(&params.PrivateKey, "private-key", defaultPrivateKey, "hex encoded private key to use for sending transactions")
7171

7272
// Wallet parameters.
73-
f.Uint64VarP(&p.WalletsNumber, "number", "n", 10, "number of wallets to fund")
74-
f.BoolVar(&p.UseHDDerivation, "hd-derivation", true, "derive wallets to fund from private key in deterministic way")
75-
f.StringSliceVar(&p.WalletAddresses, "addresses", nil, "comma-separated list of wallet addresses to fund")
76-
p.FundingAmountInWei = defaultFundingInWei
77-
f.Var(&flag_loader.BigIntValue{Val: p.FundingAmountInWei}, "eth-amount", "amount of wei to send to each wallet")
78-
f.StringVar(&p.KeyFile, "key-file", "", "file containing accounts private keys, one per line")
73+
f.Uint64VarP(&params.WalletsNumber, "number", "n", 10, "number of wallets to fund")
74+
f.BoolVar(&params.UseHDDerivation, "hd-derivation", true, "derive wallets to fund from private key in deterministic way")
75+
f.StringSliceVar(&params.WalletAddresses, "addresses", nil, "comma-separated list of wallet addresses to fund")
76+
params.FundingAmountInWei = defaultFundingInWei
77+
f.Var(&flag_loader.BigIntValue{Val: params.FundingAmountInWei}, "eth-amount", "amount of wei to send to each wallet")
78+
f.StringVar(&params.KeyFile, "key-file", "", "file containing accounts private keys, one per line")
79+
f.StringVar(&params.Seed, "seed", "", "seed string for deterministic wallet generation (e.g., 'ephemeral_test')")
7980

80-
f.StringVarP(&p.OutputFile, "file", "f", "wallets.json", "output JSON file path for storing addresses and private keys of funded wallets")
81+
f.StringVarP(&params.OutputFile, "file", "f", "wallets.json", "output JSON file path for storing addresses and private keys of funded wallets")
8182

8283
// Marking flags as mutually exclusive
8384
FundCmd.MarkFlagsMutuallyExclusive("addresses", "number")
8485
FundCmd.MarkFlagsMutuallyExclusive("addresses", "hd-derivation")
86+
FundCmd.MarkFlagsMutuallyExclusive("addresses", "seed")
8587
FundCmd.MarkFlagsMutuallyExclusive("key-file", "addresses")
8688
FundCmd.MarkFlagsMutuallyExclusive("key-file", "number")
8789
FundCmd.MarkFlagsMutuallyExclusive("key-file", "hd-derivation")
90+
FundCmd.MarkFlagsMutuallyExclusive("key-file", "seed")
91+
FundCmd.MarkFlagsMutuallyExclusive("seed", "hd-derivation")
8892

8993
// Funder contract parameters.
90-
f.StringVar(&p.FunderAddress, "contract-address", "", "address of pre-deployed Funder contract")
91-
92-
params = *p
94+
f.StringVar(&params.FunderAddress, "contract-address", "", "address of pre-deployed Funder contract")
9395
}
9496

9597
func checkFlags() error {
@@ -109,9 +111,33 @@ func checkFlags() error {
109111
// Check that exactly one method is used to specify target accounts
110112
hasAddresses := len(params.WalletAddresses) > 0
111113
hasKeyFile := params.KeyFile != ""
112-
hasNumberFlag := params.WalletsNumber > 0
113-
if !hasAddresses && !hasKeyFile && !hasNumberFlag {
114-
return errors.New("must specify target accounts via --addresses, --key-file, or --number")
114+
hasSeed := params.Seed != ""
115+
hasNumberWithoutSeed := params.WalletsNumber > 0 && !hasSeed
116+
117+
methodCount := 0
118+
if hasAddresses {
119+
methodCount++
120+
}
121+
if hasKeyFile {
122+
methodCount++
123+
}
124+
if hasNumberWithoutSeed {
125+
methodCount++
126+
}
127+
if hasSeed {
128+
methodCount++
129+
}
130+
131+
if methodCount == 0 {
132+
return errors.New("must specify target accounts via --addresses, --key-file, --number, or --seed")
133+
}
134+
if methodCount > 1 {
135+
return errors.New("cannot use multiple wallet specification methods simultaneously")
136+
}
137+
138+
// When using seed, require a number of wallets to generate
139+
if hasSeed && params.WalletsNumber <= 0 {
140+
return errors.New("when using --seed, must also specify --number > 0 to indicate how many wallets to generate")
115141
}
116142

117143
minValue := big.NewInt(1000000000)

cmd/fund/fund.go

Lines changed: 93 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package fund
33
import (
44
"context"
55
"crypto/ecdsa"
6+
"crypto/sha256"
67
"encoding/hex"
78
"encoding/json"
89
"errors"
@@ -51,17 +52,31 @@ func runFunding(ctx context.Context) error {
5152
}
5253

5354
var addresses []common.Address
55+
var privateKeys []*ecdsa.PrivateKey
5456

5557
if len(params.KeyFile) > 0 { // get addresses from key-file
56-
addresses, err = getAddressesFromKeyFile(params.KeyFile)
58+
addresses, privateKeys, err = getAddressesAndKeysFromKeyFile(params.KeyFile)
59+
} else if len(params.Seed) > 0 { // get addresses from seed
60+
addresses, privateKeys, err = getAddressesAndKeysFromSeed(params.Seed, int(params.WalletsNumber))
5761
} else { // get addresses from private key
58-
addresses, err = getAddressesFromPrivateKey(ctx, c)
62+
addresses, privateKeys, err = getAddressesAndKeysFromPrivateKey(ctx, c)
5963
}
6064
// check errors after getting addresses
6165
if err != nil {
6266
return err
6367
}
6468

69+
// Save private and public keys to a file if we have private keys.
70+
if len(privateKeys) > 0 {
71+
go func() {
72+
if err := saveToFile(params.OutputFile, privateKeys); err != nil {
73+
log.Error().Err(err).Msg("Unable to save keys to file")
74+
panic(err)
75+
}
76+
log.Info().Str("fileName", params.OutputFile).Msg("Wallets' address(es) and private key(s) saved to file")
77+
}()
78+
}
79+
6580
// Deploy or instantiate the Funder contract.
6681
var contract *funder.Funder
6782
contract, err = deployOrInstantiateFunderContract(ctx, c, tops, privateKey, len(addresses))
@@ -79,9 +94,9 @@ func runFunding(ctx context.Context) error {
7994
return nil
8095
}
8196

82-
func getAddressesFromKeyFile(keyFilePath string) ([]common.Address, error) {
97+
func getAddressesAndKeysFromKeyFile(keyFilePath string) ([]common.Address, []*ecdsa.PrivateKey, error) {
8398
if len(keyFilePath) == 0 {
84-
return nil, errors.New("the key file path is empty")
99+
return nil, nil, errors.New("the key file path is empty")
85100
}
86101

87102
log.Trace().
@@ -93,7 +108,7 @@ func getAddressesFromKeyFile(keyFilePath string) ([]common.Address, error) {
93108
log.Error().
94109
Err(iErr).
95110
Msg("Unable to read private keys from key file")
96-
return nil, fmt.Errorf("unable to read private keys from key file. %w", iErr)
111+
return nil, nil, fmt.Errorf("unable to read private keys from key file. %w", iErr)
97112
}
98113
addresses := make([]common.Address, len(privateKeys))
99114
for i, privateKey := range privateKeys {
@@ -104,30 +119,33 @@ func getAddressesFromKeyFile(keyFilePath string) ([]common.Address, error) {
104119
Msg("New wallet derived from key file")
105120
}
106121
log.Info().Int("count", len(addresses)).Msg("Wallet(s) derived from key file")
107-
return addresses, nil
122+
return addresses, privateKeys, nil
108123
}
109124

110-
func getAddressesFromPrivateKey(ctx context.Context, c *ethclient.Client) ([]common.Address, error) {
125+
func getAddressesAndKeysFromPrivateKey(ctx context.Context, c *ethclient.Client) ([]common.Address, []*ecdsa.PrivateKey, error) {
111126
// Derive or generate a set of wallets.
112127
var addresses []common.Address
128+
var privateKeys []*ecdsa.PrivateKey
113129
var err error
114130
if len(params.WalletAddresses) > 0 {
115131
log.Info().Msg("Using addresses provided by the user")
116132
addresses = make([]common.Address, len(params.WalletAddresses))
117133
for i, address := range params.WalletAddresses {
118134
addresses[i] = common.HexToAddress(address)
119135
}
136+
// No private keys available when using provided addresses
137+
privateKeys = nil
120138
} else if params.UseHDDerivation {
121139
log.Info().Msg("Deriving wallets from the default mnemonic")
122-
addresses, err = deriveHDWallets(int(params.WalletsNumber))
140+
addresses, privateKeys, err = deriveHDWalletsWithKeys(int(params.WalletsNumber))
123141
} else {
124142
log.Info().Msg("Generating random wallets")
125-
addresses, err = generateWallets(int(params.WalletsNumber))
143+
addresses, privateKeys, err = generateWalletsWithKeys(int(params.WalletsNumber))
126144
}
127145
if err != nil {
128-
return nil, err
146+
return nil, nil, err
129147
}
130-
return addresses, nil
148+
return addresses, privateKeys, nil
131149
}
132150

133151
// dialRpc dials the Ethereum RPC server and return an Ethereum client.
@@ -203,56 +221,55 @@ func deployOrInstantiateFunderContract(ctx context.Context, c *ethclient.Client,
203221
return contract, nil
204222
}
205223

206-
// deriveHDWallets generates and exports a specified number of HD wallet addresses.
207-
func deriveHDWallets(n int) ([]common.Address, error) {
224+
// deriveHDWalletsWithKeys generates and exports a specified number of HD wallet addresses and their private keys.
225+
func deriveHDWalletsWithKeys(n int) ([]common.Address, []*ecdsa.PrivateKey, error) {
208226
wallet, err := hdwallet.NewPolyWallet(defaultMnemonic, defaultPassword)
209227
if err != nil {
210-
return nil, err
228+
return nil, nil, err
211229
}
212230

213231
var derivedWallets *hdwallet.PolyWalletExport
214232
derivedWallets, err = wallet.ExportHDAddresses(n)
215233
if err != nil {
216-
return nil, err
234+
return nil, nil, err
217235
}
218236

219237
addresses := make([]common.Address, n)
238+
privateKeys := make([]*ecdsa.PrivateKey, n)
220239
for i, wallet := range derivedWallets.Addresses {
221240
addresses[i] = common.HexToAddress(wallet.ETHAddress)
241+
// Parse the private key
242+
trimmedHexPrivateKey := strings.TrimPrefix(wallet.HexPrivateKey, "0x")
243+
privateKey, err := crypto.HexToECDSA(trimmedHexPrivateKey)
244+
if err != nil {
245+
return nil, nil, fmt.Errorf("unable to parse private key for wallet %d: %w", i, err)
246+
}
247+
privateKeys[i] = privateKey
222248
log.Trace().Interface("address", addresses[i]).Str("privateKey", wallet.HexPrivateKey).Str("path", wallet.Path).Msg("New wallet derived")
223249
}
224250
log.Info().Int("count", n).Msg("Wallet(s) derived")
225-
return addresses, nil
251+
return addresses, privateKeys, nil
226252
}
227253

228-
// generateWallets generates a specified number of Ethereum wallets with random private keys.
229-
// It returns a slice of common.Address representing the Ethereum addresses of the generated wallets.
230-
func generateWallets(n int) ([]common.Address, error) {
254+
// generateWalletsWithKeys generates a specified number of Ethereum wallets with random private keys.
255+
// It returns a slice of common.Address representing the Ethereum addresses and their corresponding private keys.
256+
func generateWalletsWithKeys(n int) ([]common.Address, []*ecdsa.PrivateKey, error) {
231257
// Generate private keys.
232258
privateKeys := make([]*ecdsa.PrivateKey, n)
233259
addresses := make([]common.Address, n)
234260
for i := 0; i < n; i++ {
235261
pk, err := crypto.GenerateKey()
236262
if err != nil {
237263
log.Error().Err(err).Msg("Error generating key")
238-
return nil, err
264+
return nil, nil, err
239265
}
240266
privateKeys[i] = pk
241267
addresses[i] = crypto.PubkeyToAddress(pk.PublicKey)
242268
log.Trace().Interface("address", addresses[i]).Str("privateKey", hex.EncodeToString(pk.D.Bytes())).Msg("New wallet generated")
243269
}
244270
log.Info().Int("count", n).Msg("Wallet(s) generated")
245271

246-
// Save private and public keys to a file.
247-
go func() {
248-
if err := saveToFile(params.OutputFile, privateKeys); err != nil {
249-
log.Error().Err(err).Msg("Unable to save keys to file")
250-
panic(err)
251-
}
252-
log.Info().Str("fileName", params.OutputFile).Msg("Wallets' address(es) and private key(s) saved to file")
253-
}()
254-
255-
return addresses, nil
272+
return addresses, privateKeys, nil
256273
}
257274

258275
// saveToFile serializes wallet data into the specified JSON format and writes it to the designated file.
@@ -304,3 +321,49 @@ func fundWallets(ctx context.Context, c *ethclient.Client, tops *bind.TransactOp
304321
}
305322
return nil
306323
}
324+
325+
func getAddressesAndKeysFromSeed(seed string, numWallets int) ([]common.Address, []*ecdsa.PrivateKey, error) {
326+
if len(seed) == 0 {
327+
return nil, nil, errors.New("the seed string is empty")
328+
}
329+
if numWallets <= 0 {
330+
return nil, nil, errors.New("number of wallets must be greater than 0")
331+
}
332+
333+
log.Info().
334+
Str("seed", seed).
335+
Int("numWallets", numWallets).
336+
Msg("Generating wallets from seed")
337+
338+
addresses := make([]common.Address, numWallets)
339+
privateKeys := make([]*ecdsa.PrivateKey, numWallets)
340+
341+
for i := 0; i < numWallets; i++ {
342+
// Create a deterministic string by combining seed with index and current date
343+
// Format: seed_index_YYYYMMDD (e.g., "ephemeral_test_0_20241010")
344+
currentDate := time.Now().Format("20060102") // YYYYMMDD format
345+
seedWithIndex := fmt.Sprintf("%s_%d_%s", seed, i, currentDate)
346+
347+
// Generate SHA256 hash of the seed+index+date
348+
hash := sha256.Sum256([]byte(seedWithIndex))
349+
hashHex := hex.EncodeToString(hash[:])
350+
351+
// Create private key from hash
352+
privateKey, err := crypto.HexToECDSA(hashHex)
353+
if err != nil {
354+
return nil, nil, fmt.Errorf("unable to create private key from seed for wallet %d: %w", i, err)
355+
}
356+
357+
privateKeys[i] = privateKey
358+
addresses[i] = crypto.PubkeyToAddress(privateKey.PublicKey)
359+
360+
log.Trace().
361+
Interface("address", addresses[i]).
362+
Str("privateKey", hashHex).
363+
Str("seedWithIndex", seedWithIndex).
364+
Msg("New wallet generated from seed")
365+
}
366+
367+
log.Info().Int("count", numWallets).Msg("Wallet(s) generated from seed")
368+
return addresses, privateKeys, nil
369+
}

doc/polycli_fund.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ $ cast balance 0x5D8121cf716B70d3e345adB58157752304eED5C3
9494
-n, --number uint number of wallets to fund (default 10)
9595
--private-key string hex encoded private key to use for sending transactions (default "0x42b6e34dc21598a807dc19d7784c71b2a7a01f6480dc6f58258f78e539f1a1fa")
9696
-r, --rpc-url string RPC endpoint URL (default "http://localhost:8545")
97+
--seed string seed string for deterministic wallet generation (e.g., 'ephemeral_test')
9798
```
9899
99100
The command also inherits flags from parent commands.

0 commit comments

Comments
 (0)