Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e5b2a48
loadtest: check for preconf after sending tx
manav2401 Dec 30, 2025
18ae4d9
util: hardcode multicall gas fee and tip cap
manav2401 Dec 30, 2025
760a1f8
util: hardcode multicall gas fee and tip cap
manav2401 Dec 30, 2025
e399bc9
util: hardcode multicall gas fee and tip cap
manav2401 Dec 30, 2025
6ed9a40
util: limit accounts to fund per tx
manav2401 Dec 30, 2025
e54199d
util: skip multicall for funding accounts
manav2401 Dec 30, 2025
e6ee695
cmd/loadtest: log
manav2401 Dec 31, 2025
add1b17
cmd/loadtest: dump private keys to file
manav2401 Dec 31, 2025
b4e1c09
cmd/loadtest: implement preconf tracker
manav2401 Jan 2, 2026
5dc3467
logs
manav2401 Jan 2, 2026
8d309dd
better setup for preconf tracker
manav2401 Jan 2, 2026
12d062f
update timeouts and retries for preconf tracker
manav2401 Jan 2, 2026
9b478a8
typo
manav2401 Jan 2, 2026
d3919e4
move metrics for better accuracy
manav2401 Jan 2, 2026
812c53e
dump block diffs
manav2401 Jan 2, 2026
0b38117
Merge branch 'main' into preconf-loadtest
minhd-vu Jan 28, 2026
c68cd80
fix: make dumping sending account private keys more generic
minhd-vu Jan 28, 2026
fdba6a7
fix: simplify wait preconf and wait receipt
minhd-vu Jan 28, 2026
6d80101
revert: multicall3 changes
minhd-vu Jan 28, 2026
b0f2ec0
revert: default accounts to fund per tx
minhd-vu Jan 28, 2026
6a2aa56
feat: add accounts per funding tx flag
minhd-vu Jan 28, 2026
f855a86
chore: simplify code
minhd-vu Jan 28, 2026
e38401b
fix: flag for preconf stats file
minhd-vu Jan 28, 2026
0719923
revert: multicall3 tops
minhd-vu Jan 28, 2026
89dbf68
fix: cleanup fund
minhd-vu Jan 28, 2026
be6ed7d
feat: output to json
minhd-vu Jan 28, 2026
cf51d83
fix: simplify code
minhd-vu Jan 28, 2026
34f87f6
refactor: rename preconf_tracker to preconf
minhd-vu Jan 28, 2026
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
4 changes: 4 additions & 0 deletions cmd/fund/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ type cmdFundParams struct {
TokenAmount *big.Int
ApproveSpender string
ApproveAmount *big.Int

// Multicall3 batching
AccountsPerFundingTx uint64
}

var (
Expand Down Expand Up @@ -117,6 +120,7 @@ func init() {
// Contract parameters.
f.StringVar(&params.FunderAddress, "funder-address", "", "address of pre-deployed funder contract")
f.StringVar(&params.Multicall3Address, "multicall3-address", "", "address of pre-deployed multicall3 contract")
f.Uint64Var(&params.AccountsPerFundingTx, "accounts-per-funding-tx", 400, "number of accounts to fund per multicall3 transaction")

// RPC parameters.
f.Float64Var(&params.RateLimit, "rate-limit", 4, "requests per second limit (use negative value to remove limit)")
Expand Down
28 changes: 15 additions & 13 deletions cmd/fund/fund.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func runFunding(ctx context.Context) error {
} else if len(params.Seed) > 0 { // get addresses from seed
addresses, privateKeys, err = getAddressesAndKeysFromSeed(params.Seed, int(params.WalletsNumber))
} else { // get addresses from private key
addresses, privateKeys, err = getAddressesAndKeysFromPrivateKey(ctx, c)
addresses, privateKeys, err = getAddressesAndKeysFromPrivateKey()
}
// check errors after getting addresses
if err != nil {
Expand Down Expand Up @@ -141,7 +141,7 @@ func getAddressesAndKeysFromKeyFile(keyFilePath string) ([]common.Address, []*ec
return addresses, privateKeys, nil
}

func getAddressesAndKeysFromPrivateKey(ctx context.Context, c *ethclient.Client) ([]common.Address, []*ecdsa.PrivateKey, error) {
func getAddressesAndKeysFromPrivateKey() ([]common.Address, []*ecdsa.PrivateKey, error) {
// Derive or generate a set of wallets.
var addresses []common.Address
var privateKeys []*ecdsa.PrivateKey
Expand Down Expand Up @@ -276,7 +276,7 @@ func generateWalletsWithKeys(n int) ([]common.Address, []*ecdsa.PrivateKey, erro
// Generate private keys.
privateKeys := make([]*ecdsa.PrivateKey, n)
addresses := make([]common.Address, n)
for i := 0; i < n; i++ {
for i := range n {
pk, err := crypto.GenerateKey()
if err != nil {
log.Error().Err(err).Msg("Error generating key")
Expand Down Expand Up @@ -320,7 +320,7 @@ func saveToFile(fileName string, privateKeys []*ecdsa.PrivateKey) error {
}

// fundWallets funds multiple wallets using the provided Funder contract.
func fundWallets(ctx context.Context, c *ethclient.Client, tops *bind.TransactOpts, contract *funder.Funder, wallets []common.Address) error {
func fundWallets(tops *bind.TransactOpts, contract *funder.Funder, wallets []common.Address) error {
// Fund wallets.
switch len(wallets) {
case 0:
Expand All @@ -346,7 +346,7 @@ func fundWalletsWithFunder(ctx context.Context, c *ethclient.Client, tops *bind.
// If ERC20 mode is enabled, fund with tokens instead of ETH
if params.TokenAddress != "" {
log.Info().Str("tokenAddress", params.TokenAddress).Msg("Starting ERC20 token funding (ETH funding disabled)")
if err = fundWalletsWithERC20(ctx, c, tops, privateKey, addresses, privateKeys); err != nil {
if err = fundWalletsWithERC20(ctx, c, tops, addresses, privateKeys); err != nil {
return err
}
log.Info().Msg("Wallet(s) funded with ERC20 tokens! 🪙")
Expand All @@ -357,7 +357,7 @@ func fundWalletsWithFunder(ctx context.Context, c *ethclient.Client, tops *bind.
if err != nil {
return err
}
if err = fundWallets(ctx, c, tops, contract, addresses); err != nil {
if err = fundWallets(tops, contract, addresses); err != nil {
return err
}
}
Expand All @@ -368,13 +368,15 @@ func fundWalletsWithMulticall3(ctx context.Context, c *ethclient.Client, tops *b
log.Debug().
Msg("funding wallets with multicall3")

const defaultAccsToFundPerTx = 400
accsToFundPerTx, err := util.Multicall3MaxAccountsToFundPerTx(ctx, c)
if err != nil {
log.Warn().Err(err).
Uint64("defaultAccsToFundPerTx", defaultAccsToFundPerTx).
Msg("failed to get multicall3 max accounts to fund per tx, falling back to default")
accsToFundPerTx = defaultAccsToFundPerTx
Uint64("fallback", params.AccountsPerFundingTx).
Msg("failed to get multicall3 max accounts to fund per tx, falling back to flag value")
accsToFundPerTx = params.AccountsPerFundingTx
}
if params.AccountsPerFundingTx > 0 && params.AccountsPerFundingTx < accsToFundPerTx {
accsToFundPerTx = params.AccountsPerFundingTx
}
log.Debug().Uint64("accsToFundPerTx", accsToFundPerTx).Msg("multicall3 max accounts to fund per tx")
chSize := (uint64(len(wallets)) / accsToFundPerTx) + 1
Expand All @@ -394,7 +396,7 @@ func fundWalletsWithMulticall3(ctx context.Context, c *ethclient.Client, tops *b
if params.RateLimit <= 0.0 {
rl = nil
}
for i := 0; i < len(wallets); i++ {
for i := range wallets {
wallet := wallets[i]
// if account is the funding account, skip it
if wallet == tops.From {
Expand Down Expand Up @@ -508,7 +510,7 @@ func getAddressesAndKeysFromSeed(seed string, numWallets int) ([]common.Address,
addresses := make([]common.Address, numWallets)
privateKeys := make([]*ecdsa.PrivateKey, numWallets)

for i := 0; i < numWallets; i++ {
for i := range numWallets {
// Create a deterministic string by combining seed with index and current date
// Format: seed_index_YYYYMMDD (e.g., "ephemeral_test_0_20241010")
currentDate := time.Now().Format("20060102") // YYYYMMDD format
Expand Down Expand Up @@ -539,7 +541,7 @@ func getAddressesAndKeysFromSeed(seed string, numWallets int) ([]common.Address,
}

// fundWalletsWithERC20 funds multiple wallets with ERC20 tokens by minting directly to each wallet and optionally approving a spender.
func fundWalletsWithERC20(ctx context.Context, c *ethclient.Client, tops *bind.TransactOpts, privateKey *ecdsa.PrivateKey, wallets []common.Address, walletsPrivateKeys []*ecdsa.PrivateKey) error {
func fundWalletsWithERC20(ctx context.Context, c *ethclient.Client, tops *bind.TransactOpts, wallets []common.Address, walletsPrivateKeys []*ecdsa.PrivateKey) error {
if len(wallets) == 0 {
return errors.New("no wallet to fund with ERC20 tokens")
}
Expand Down
4 changes: 4 additions & 0 deletions cmd/loadtest/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ func initPersistentFlags() {
pf.BoolVar(&cfg.LegacyTxMode, "legacy", false, "send a legacy transaction instead of an EIP1559 transaction")
pf.BoolVar(&cfg.FireAndForget, "fire-and-forget", false, "send transactions and load without waiting for it to be mined")
pf.BoolVar(&cfg.FireAndForget, "send-only", false, "alias for --fire-and-forget")
pf.BoolVar(&cfg.CheckForPreconf, "check-preconf", false, "check for preconf status after sending tx")
pf.StringVar(&cfg.PreconfStatsFile, "preconf-stats-file", "", "base path for preconf stats JSON output (e.g., 'preconf' creates preconf-{timestamp}.json)")

initGasManagerFlags()
}
Expand Down Expand Up @@ -149,6 +151,8 @@ func initFlags() {
f.BoolVar(&cfg.PreFundSendingAccounts, "pre-fund-sending-accounts", false, "fund all sending accounts at start instead of on first use")
f.BoolVar(&cfg.RefundRemainingFunds, "refund-remaining-funds", false, "refund remaining balance to funding account after completion")
f.StringVar(&cfg.SendingAccountsFile, "sending-accounts-file", "", "file with sending account private keys, one per line (avoids pool queue and preserves accounts across runs)")
f.StringVar(&cfg.DumpSendingAccountsFile, "dump-sending-accounts-file", "", "file path to dump generated private keys when using --sending-accounts-count")
f.Uint64Var(&cfg.AccountsPerFundingTx, "accounts-per-funding-tx", 400, "number of accounts to fund per multicall3 transaction")
f.Uint64Var(&cfg.MaxBaseFeeWei, "max-base-fee-wei", 0, "maximum base fee in wei (pause sending new transactions when exceeded, useful during network congestion)")
f.StringSliceVarP(&cfg.Modes, "mode", "m", []string{"t"}, `testing mode (can specify multiple like "d,t"):
2, erc20 - send ERC20 tokens
Expand Down
35 changes: 18 additions & 17 deletions doc/polycli_fund.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,23 +84,24 @@ $ cast balance 0x5D8121cf716B70d3e345adB58157752304eED5C3
## Flags

```bash
--addresses strings comma-separated list of wallet addresses to fund
--approve-amount big.Int amount of ERC20 tokens to approve for the spender (default 1000000000000000000000)
--approve-spender string address to approve for spending tokens from each funded wallet
--eth-amount big.Int amount of wei to send to each wallet (default 50000000000000000)
-f, --file string output JSON file path for storing addresses and private keys of funded wallets (default "wallets.json")
--funder-address string address of pre-deployed funder contract
--hd-derivation derive wallets to fund from private key in deterministic way (default true)
-h, --help help for fund
--key-file string file containing accounts private keys, one per line
--multicall3-address string address of pre-deployed multicall3 contract
-n, --number uint number of wallets to fund (default 10)
--private-key string hex encoded private key to use for sending transactions (default "0x42b6e34dc21598a807dc19d7784c71b2a7a01f6480dc6f58258f78e539f1a1fa")
--rate-limit float requests per second limit (use negative value to remove limit) (default 4)
-r, --rpc-url string RPC endpoint URL (default "http://localhost:8545")
--seed string seed string for deterministic wallet generation (e.g., 'ephemeral_test')
--token-address string address of the ERC20 token contract to mint and fund (if provided, enables ERC20 mode)
--token-amount big.Int amount of ERC20 tokens to transfer from private-key wallet to each wallet (default 1000000000000000000)
--accounts-per-funding-tx uint number of accounts to fund per multicall3 transaction (default 400)
--addresses strings comma-separated list of wallet addresses to fund
--approve-amount big.Int amount of ERC20 tokens to approve for the spender (default 1000000000000000000000)
--approve-spender string address to approve for spending tokens from each funded wallet
--eth-amount big.Int amount of wei to send to each wallet (default 50000000000000000)
-f, --file string output JSON file path for storing addresses and private keys of funded wallets (default "wallets.json")
--funder-address string address of pre-deployed funder contract
--hd-derivation derive wallets to fund from private key in deterministic way (default true)
-h, --help help for fund
--key-file string file containing accounts private keys, one per line
--multicall3-address string address of pre-deployed multicall3 contract
-n, --number uint number of wallets to fund (default 10)
--private-key string hex encoded private key to use for sending transactions (default "0x42b6e34dc21598a807dc19d7784c71b2a7a01f6480dc6f58258f78e539f1a1fa")
--rate-limit float requests per second limit (use negative value to remove limit) (default 4)
-r, --rpc-url string RPC endpoint URL (default "http://localhost:8545")
--seed string seed string for deterministic wallet generation (e.g., 'ephemeral_test')
--token-address string address of the ERC20 token contract to mint and fund (if provided, enables ERC20 mode)
--token-amount big.Int amount of ERC20 tokens to transfer from private-key wallet to each wallet (default 1000000000000000000)
```

The command also inherits flags from parent commands.
Expand Down
4 changes: 4 additions & 0 deletions doc/polycli_loadtest.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ The codebase has a contract that used for load testing. It's written in Solidity

```bash
--account-funding-amount big.Int amount in wei to fund sending accounts (set to 0 to disable)
--accounts-per-funding-tx uint number of accounts to fund per multicall3 transaction (default 400)
--adaptive-backoff-factor float multiplicative decrease factor for adaptive rate limiting (default 2)
--adaptive-cycle-duration-seconds uint interval in seconds to check queue size and adjust rates for adaptive rate limiting (default 10)
--adaptive-rate-limit enable AIMD-style congestion control to automatically adjust request rate
Expand All @@ -118,9 +119,11 @@ The codebase has a contract that used for load testing. It's written in Solidity
--calldata string hex encoded calldata: function signature + encoded arguments (requires --mode contract-call and --contract-address)
--chain-id uint chain ID for the transactions
--check-balance-before-funding check account balance before funding sending accounts (saves gas when accounts are already funded)
--check-preconf check for preconf status after sending tx
-c, --concurrency int number of requests to perform concurrently (default: one at a time) (default 1)
--contract-address string contract address for --mode contract-call (requires --calldata)
--contract-call-payable mark function as payable using value from --eth-amount-in-wei (requires --mode contract-call and --contract-address)
--dump-sending-accounts-file string file path to dump generated private keys when using --sending-accounts-count
--erc20-address string address of pre-deployed ERC20 contract
--erc721-address string address of pre-deployed ERC721 contract
--eth-amount-in-wei uint amount of ether in wei to send per transaction
Expand Down Expand Up @@ -159,6 +162,7 @@ The codebase has a contract that used for load testing. It's written in Solidity
--output-mode string format mode for summary output (json | text) (default "text")
--output-raw-tx-only output raw signed transaction hex without sending (works with most modes except RPC and UniswapV3)
--pre-fund-sending-accounts fund all sending accounts at start instead of on first use
--preconf-stats-file string base path for preconf stats JSON output (e.g., 'preconf' creates preconf-{timestamp}.json)
--priority-gas-price uint gas tip price for EIP-1559 transactions
--private-key string hex encoded private key to use for sending transactions (default "42b6e34dc21598a807dc19d7784c71b2a7a01f6480dc6f58258f78e539f1a1fa")
--proxy string use the proxy specified
Expand Down
2 changes: 2 additions & 0 deletions doc/polycli_loadtest_uniswapv3.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ The command also inherits flags from parent commands.
--adaptive-target-size uint target queue size for adaptive rate limiting (speed up if smaller, back off if larger) (default 1000)
--batch-size uint batch size for receipt fetching (default: 999) (default 999)
--chain-id uint chain ID for the transactions
--check-preconf check for preconf status after sending tx
-c, --concurrency int number of requests to perform concurrently (default: one at a time) (default 1)
--config string config file (default is $HOME/.polygon-cli.yaml)
--eth-amount-in-wei uint amount of ether in wei to send per transaction
Expand All @@ -104,6 +105,7 @@ The command also inherits flags from parent commands.
--nonce uint use this flag to manually set the starting nonce
--output-mode string format mode for summary output (json | text) (default "text")
--output-raw-tx-only output raw signed transaction hex without sending (works with most modes except RPC and UniswapV3)
--preconf-stats-file string base path for preconf stats JSON output (e.g., 'preconf' creates preconf-{timestamp}.json)
--pretty-logs output logs in pretty format instead of JSON (default true)
--priority-gas-price uint gas tip price for EIP-1559 transactions
--private-key string hex encoded private key to use for sending transactions (default "42b6e34dc21598a807dc19d7784c71b2a7a01f6480dc6f58258f78e539f1a1fa")
Expand Down
27 changes: 23 additions & 4 deletions loadtest/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type AccountPoolConfig struct {
RefundRemainingFunds bool
CheckBalanceBeforeFunding bool
LegacyTxMode bool
AccountsPerFundingTx uint64
// Gas override settings
ForceGasPrice uint64
ForcePriorityGasPrice uint64
Expand Down Expand Up @@ -229,6 +230,18 @@ func (ap *AccountPool) AddRandomN(ctx context.Context, n uint64) error {
return nil
}

// GetPrivateKeys returns the private keys of all accounts in the pool.
func (ap *AccountPool) GetPrivateKeys() []*ecdsa.PrivateKey {
ap.mu.Lock()
defer ap.mu.Unlock()

pks := make([]*ecdsa.PrivateKey, len(ap.accounts))
for i, acc := range ap.accounts {
pks[i] = acc.privateKey
}
return pks
}

// AddRandom adds a random account to the pool.
func (ap *AccountPool) AddRandom(ctx context.Context) error {
privateKey, err := crypto.GenerateKey()
Expand Down Expand Up @@ -477,13 +490,19 @@ func (ap *AccountPool) fundAccountsWithMulticall3(ctx context.Context, tops *bin
log.Debug().
Msg("funding sending accounts with multicall3")

const defaultAccsToFundPerTx = 400
fallbackAccsPerTx := ap.cfg.AccountsPerFundingTx
if fallbackAccsPerTx == 0 {
fallbackAccsPerTx = 400
}
accsToFundPerTx, err := util.Multicall3MaxAccountsToFundPerTx(ctx, ap.client)
if err != nil {
log.Warn().Err(err).
Uint64("defaultAccsToFundPerTx", defaultAccsToFundPerTx).
Msg("failed to get multicall3 max accounts to fund per tx, falling back to default")
accsToFundPerTx = defaultAccsToFundPerTx
Uint64("fallback", fallbackAccsPerTx).
Msg("failed to get multicall3 max accounts to fund per tx, falling back to flag value")
accsToFundPerTx = fallbackAccsPerTx
}
if fallbackAccsPerTx > 0 && fallbackAccsPerTx < accsToFundPerTx {
accsToFundPerTx = fallbackAccsPerTx
}
log.Debug().Uint64("accsToFundPerTx", accsToFundPerTx).Msg("multicall3 max accounts to fund per tx")
chSize := (uint64(len(ap.accounts)) / accsToFundPerTx) + 1
Expand Down
4 changes: 4 additions & 0 deletions loadtest/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ type Config struct {
RandomRecipients bool
LegacyTxMode bool
FireAndForget bool
CheckForPreconf bool
PreconfStatsFile string
WaitForReceipt bool
ReceiptRetryMax uint
ReceiptRetryDelay uint // initial delay in milliseconds
Expand Down Expand Up @@ -102,6 +104,8 @@ type Config struct {
RefundRemainingFunds bool
SendingAccountsFile string
CheckBalanceBeforeFunding bool
DumpSendingAccountsFile string
AccountsPerFundingTx uint64

// Summary output
ShouldProduceSummary bool
Expand Down
Loading