diff --git a/main.go b/main.go
new file mode 100644
index 0000000..ed5f364
--- /dev/null
+++ b/main.go
@@ -0,0 +1,22 @@
+// Copyright © 2020 Vulcanize, Inc
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package main
+
+import "github.com/vulcanize/tx_spammer/cmd"
+
+func main() {
+ cmd.Execute()
+}
diff --git a/pkg/config.go b/pkg/config.go
new file mode 100644
index 0000000..cca9416
--- /dev/null
+++ b/pkg/config.go
@@ -0,0 +1,227 @@
+// VulcanizeDB
+// Copyright © 2020 Vulcanize
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package tx_spammer
+
+import (
+ "crypto/ecdsa"
+ "fmt"
+ "math/big"
+ "strings"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/rpc"
+ "github.com/spf13/viper"
+)
+
+type TxParams struct {
+ // Name of this tx in the .toml file
+ Name string
+
+ // HTTP Client for this tx type
+ Client *rpc.Client
+
+ // Type of the tx
+ Type TxType
+
+ // Universal tx fields
+ To *common.Address
+ GasLimit uint64
+ GasPrice *big.Int // nil if eip1559
+ Amount *big.Int
+ Data []byte
+ Sender common.Address
+
+ // Optimism-specific metadata fields
+ L1SenderAddr *common.Address
+ L1RollupTxId uint64
+ SigHashType uint8
+
+ // EIP1559-specific fields
+ GasPremium *big.Int
+ FeeCap *big.Int
+
+ // Sender key, if left the senderKeyPath is empty we generate a new key
+ SenderKey *ecdsa.PrivateKey
+
+ // Sending params
+ // How often we send a tx of this type
+ Frequency time.Duration
+ // Total number of txs of this type to send
+ TotalNumber uint64
+ // Txs of different types will be sent according to their order (starting at 0)
+ Order uint64
+}
+
+const (
+ ETH_TX_LIST = "ETH_TX_LIST"
+
+ typeSuffix = ".type"
+ httpPathSuffix = ".http"
+ toSuffix = ".to"
+ amountSuffix = ".amount"
+ gasLimitSuffix = ".gasLimit"
+ gasPriceSuffix = ".gasPrice"
+ gasPremiumSuffix = ".gasPremium"
+ feeCapSuffix = ".feeCap"
+ dataSuffix = ".data"
+ senderKeyPathSuffix = ".senderKeyPath"
+ writeSenderPathSuffix = ".writeSenderPath"
+ l1SenderSuffix = ".l1Sender"
+ l1RollupTxIdSuffix = ".l1RollupTxId"
+ sigHashTypeSuffix = ".sigHashType"
+ frequencySuffix = ".frequency"
+ totalNumberSuffix = ".totalNumber"
+)
+
+// NewConfig returns a new tx spammer config
+func NewTxParams() ([]TxParams, error) {
+ viper.BindEnv("eth.txs", ETH_TX_LIST)
+
+ txs := viper.GetStringSlice("eth.txs")
+ txParams := make([]TxParams, len(txs))
+ for i, txName := range txs {
+ // Get http client
+ httpPathStr := viper.GetString(txName+httpPathSuffix)
+ if httpPathStr == "" {
+ return nil, fmt.Errorf("tx %s is missing an httpPath", txName)
+ }
+ if !strings.HasPrefix(httpPathStr, "http://") {
+ httpPathStr = "http://" + httpPathStr
+ }
+ rpcClient, err := rpc.Dial(httpPathStr)
+ if err != nil {
+ return nil, err
+ }
+
+ // Get tx type
+ txTypeStr := viper.GetString(txName+typeSuffix)
+ if txTypeStr == "" {
+ return nil, fmt.Errorf("need tx type for tx %s", txName)
+ }
+ txType, err := TxTypeFromString(txTypeStr)
+ if err != nil {
+ return nil, err
+ }
+
+ // Get basic fields
+ toStr := viper.GetString(txName+toSuffix)
+ var toAddr *common.Address
+ if toStr != "" {
+ to := common.HexToAddress(toStr)
+ toAddr = &to
+ }
+ amountStr := viper.GetString(txName+amountSuffix)
+ amount := new(big.Int)
+ if amountStr != "" {
+ if _, ok := amount.SetString(amountStr, 10); !ok {
+ return nil, fmt.Errorf("amount (%s) for tx %s is not valid", amountStr, txName)
+ }
+ }
+ gasPriceStr := viper.GetString(txName+gasPriceSuffix)
+ var gasPrice *big.Int
+ if gasPriceStr != "" {
+ gasPrice = new(big.Int)
+ if _, ok := gasPrice.SetString(gasPriceStr, 10); !ok {
+ return nil, fmt.Errorf("gasPrice (%s) for tx %s is not valid", gasPriceStr, txName)
+ }
+ }
+ gasLimit := viper.GetUint64(txName+gasLimitSuffix)
+ hex := viper.GetString(txName+dataSuffix)
+ data := make([]byte, 0)
+ if hex != "" {
+ data = common.Hex2Bytes(hex)
+ }
+
+ // Load or generate sender key
+ senderKeyPath := viper.GetString(txName+senderKeyPathSuffix)
+ var key *ecdsa.PrivateKey
+ if senderKeyPath != "" {
+ key, err = crypto.LoadECDSA(senderKeyPath)
+ if err != nil {
+ return nil, fmt.Errorf("unable to load ecdsa at %s key for tx %s", senderKeyPath, txName)
+ }
+ } else {
+ key, err = crypto.GenerateKey()
+ if err != nil {
+ return nil, fmt.Errorf("unable to generate ecdsa key for tx %s", txName)
+ }
+ writePath := viper.GetString(txName+writeSenderPathSuffix)
+ if writePath != "" {
+ if err := crypto.SaveECDSA(writePath, key); err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ // Attempt to load Optimism fields
+ l1SenderStr := viper.GetString(txName+l1SenderSuffix)
+ var l1Sender *common.Address
+ if l1SenderStr != "" {
+ sender := common.HexToAddress(l1SenderStr)
+ l1Sender = &sender
+ }
+
+ l1RollupTxId := viper.GetUint64(txName+l1RollupTxIdSuffix)
+ sigHashType := viper.GetUint(txName+sigHashTypeSuffix)
+
+ // If gasPrice was empty, attempt to load EIP1559 fields
+ var feeCap, gasPremium *big.Int
+ if gasPrice == nil {
+ feeCapStr := viper.GetString(txName+feeCapSuffix)
+ gasPremiumString := viper.GetString(txName+gasPremiumSuffix)
+ if feeCapStr == "" {
+ return nil, fmt.Errorf("tx %s is missing feeCapStr", txName)
+ }
+ if gasPremiumString == "" {
+ return nil, fmt.Errorf("tx %s is missing gasPremiumStr", txName)
+ }
+ feeCap = new(big.Int)
+ gasPremium = new(big.Int)
+ if _, ok := feeCap.SetString(feeCapStr, 10); !ok {
+ return nil, fmt.Errorf("unable to set feeCap to %s for tx %s", feeCapStr, txName)
+ }
+ if _, ok := gasPremium.SetString(gasPremiumString, 10); !ok {
+ return nil, fmt.Errorf("unable to set gasPremium to %s for tx %s", gasPremiumString, txName)
+ }
+ }
+
+ // Load the sending paramas
+ frequency := viper.GetDuration(txName+frequencySuffix)
+ totalNumber := viper.GetUint64(txName+totalNumberSuffix)
+
+ txParams[i] = TxParams{
+ Client: rpcClient,
+ Type: txType,
+ Name: txName,
+ To: toAddr,
+ Amount: amount,
+ GasLimit: gasLimit,
+ GasPrice: gasPrice,
+ GasPremium: gasPremium,
+ FeeCap: feeCap,
+ Data: data,
+ L1SenderAddr: l1Sender,
+ L1RollupTxId: l1RollupTxId,
+ SigHashType: uint8(sigHashType),
+ Frequency: frequency,
+ TotalNumber: totalNumber,
+ }
+ }
+ return txParams, nil
+}
diff --git a/pkg/sender.go b/pkg/sender.go
new file mode 100644
index 0000000..3c1bd25
--- /dev/null
+++ b/pkg/sender.go
@@ -0,0 +1,99 @@
+// VulcanizeDB
+// Copyright © 2020 Vulcanize
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package tx_spammer
+
+import (
+ "context"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/rpc"
+)
+
+// SendTxArgs represents the arguments to submit a transaction
+type SendTxArgs struct {
+ From common.MixedcaseAddress `json:"from"`
+ To *common.MixedcaseAddress `json:"to"`
+ Gas hexutil.Uint64 `json:"gas"`
+ GasPrice hexutil.Big `json:"gasPrice"`
+ Value hexutil.Big `json:"value"`
+ Nonce hexutil.Uint64 `json:"nonce"`
+ // We accept "data" and "input" for backwards-compatibility reasons.
+ Data *hexutil.Bytes `json:"data"`
+ Input *hexutil.Bytes `json:"input,omitempty"`
+}
+
+/*
+// SendTransaction creates a transaction for the given argument, sign it and submit it to the
+// transaction pool.
+func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args SendTxArgs) (common.Hash, error) {
+ // Look up the wallet containing the requested signer
+ account := accounts.Account{Address: args.From}
+
+ wallet, err := s.b.AccountManager().Find(account)
+ if err != nil {
+ return common.Hash{}, err
+ }
+
+ if args.Nonce == nil {
+ // Hold the addresse's mutex around signing to prevent concurrent assignment of
+ // the same nonce to multiple accounts.
+ s.nonceLock.LockAddr(args.From)
+ defer s.nonceLock.UnlockAddr(args.From)
+ }
+
+ // Set some sanity defaults and terminate on failure
+ if err := args.setDefaults(ctx, s.b); err != nil {
+ return common.Hash{}, err
+ }
+ // Assemble the transaction and sign with the wallet
+ tx := args.toTransaction()
+
+ signed, err := wallet.SignTx(account, tx, s.b.ChainConfig().ChainID)
+ if err != nil {
+ return common.Hash{}, err
+ }
+ return SubmitTransaction(ctx, s.b, signed)
+}
+ */
+
+type TxSender struct {
+ TxGen *TxGenerator
+}
+
+func NewTxSender(params []TxParams) *TxSender {
+ return &TxSender{
+ TxGen: NewTxGenerator(params),
+ }
+}
+func (s *TxSender) Send() <-chan error {
+ errChan := make(chan error)
+ go func() {
+ for s.TxGen.Next() {
+ if err := sendRawTransaction(s.TxGen.Current()); err != nil {
+ errChan <- err
+ }
+ }
+ if s.TxGen.Error() != nil {
+ errChan <- s.TxGen.Error()
+ }
+ }()
+}
+
+func sendRawTransaction(rpcClient *rpc.Client, txRlp []byte) error {
+ return rpcClient.CallContext(context.Background(), nil, "eth_sendRawTransaction", hexutil.Encode(txRlp))
+}
\ No newline at end of file
diff --git a/pkg/service.go b/pkg/service.go
new file mode 100644
index 0000000..380c257
--- /dev/null
+++ b/pkg/service.go
@@ -0,0 +1,34 @@
+// VulcanizeDB
+// Copyright © 2020 Vulcanize
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package tx_spammer
+
+import "sync"
+
+type Service interface {
+ Loop(wg *sync.WaitGroup) error
+}
+
+type Tx struct {
+ Spammer *Sender
+ Generator *TxGenerator
+ Config *Config
+}
+
+func NewTxSpammer(params []TxParams) (TxSpammer, error) {
+
+ return &txSpammer{}, nil
+}
diff --git a/pkg/touch b/pkg/touch
new file mode 100644
index 0000000..e69de29
diff --git a/pkg/tx_generator.go b/pkg/tx_generator.go
new file mode 100644
index 0000000..b81a3f4
--- /dev/null
+++ b/pkg/tx_generator.go
@@ -0,0 +1,49 @@
+// VulcanizeDB
+// Copyright © 2020 Vulcanize
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package tx_spammer
+
+import "github.com/ethereum/go-ethereum/rpc"
+
+// TxGenerator generates and signs txs
+type TxGenerator struct {
+ TxParams []TxParams
+ currentTx []byte
+ currentClient *rpc.Client
+ err error
+}
+
+func NewTxGenerator(params []TxParams) *TxGenerator {
+ return &TxGenerator{
+ TxParams: params,
+ }
+}
+
+func (gen TxGenerator) Next() bool {
+ return false
+}
+
+func (gen TxGenerator) Current() (*rpc.Client, []byte) {
+ return gen.currentClient, gen.currentTx
+}
+
+func (gen TxGenerator) Error() error {
+ return gen.err
+}
+
+func (gen TxGenerator) gen(params TxParams) ([]byte, error) {
+ return nil, nil
+}
\ No newline at end of file
diff --git a/pkg/tx_type.go b/pkg/tx_type.go
new file mode 100644
index 0000000..860d60a
--- /dev/null
+++ b/pkg/tx_type.go
@@ -0,0 +1,48 @@
+// VulcanizeDB
+// Copyright © 2020 Vulcanize
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package tx_spammer
+
+import (
+ "fmt"
+ "strings"
+)
+
+type TxType int
+
+const (
+ Unkown TxType = iota
+ Standard
+ OptimismL2
+ OptimismL1ToL2
+ EIP1559
+)
+
+// TxTypeFromString returns the tx enum type from provided string
+func TxTypeFromString(str string) (TxType, error) {
+ switch strings.ToLower(str) {
+ case "geth", "standard", "parity":
+ return Standard, nil
+ case "l2":
+ return OptimismL2, nil
+ case "l1", "l2tol1":
+ return OptimismL1ToL2, nil
+ case "eip1559":
+ return EIP1559, nil
+ default:
+ return Unkown, fmt.Errorf("unsupported tx type: %s", str)
+ }
+}
\ No newline at end of file
diff --git a/pkg/util.go b/pkg/util.go
new file mode 100644
index 0000000..6dc418a
--- /dev/null
+++ b/pkg/util.go
@@ -0,0 +1,43 @@
+package tx_spammer
+
+import (
+ "fmt"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/params"
+ "math/big"
+)
+
+// ChainConfig returns the appropriate ethereum chain config for the provided chain id
+func ChainConfig(chainID uint64) (*params.ChainConfig, error) {
+ switch chainID {
+ case 1:
+ return params.MainnetChainConfig, nil
+ case 3:
+ return params.TestnetChainConfig, nil // Ropsten
+ case 4:
+ return params.RinkebyChainConfig, nil
+ case 5:
+ return params.GoerliChainConfig, nil
+ case 420:
+ default:
+ return nil, fmt.Errorf("chain config for chainid %d not available", chainID)
+ }
+}
+
+// ChainConfig returns the appropriate ethereum chain config for the provided chain id
+func TxSigner(chainID uint64) (types.Signer, error) {
+ switch chainID {
+ case 1:
+ return params.MainnetChainConfig, nil
+ case 3:
+ return params.TestnetChainConfig, nil // Ropsten
+ case 4:
+ return params.RinkebyChainConfig, nil
+ case 5:
+ return params.GoerliChainConfig, nil
+ case 420:
+ return types.NewOVMSigner(big.NewInt()), nil
+ default:
+ return nil, fmt.Errorf("chain config for chainid %d not available", chainID)
+ }
+}
\ No newline at end of file