From df93e7fafabf162af949ff3460ad4b8739035fbb Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 30 Jul 2024 07:00:03 -0600 Subject: [PATCH] TOML config loading package --- go.mod | 3 +- go.sum | 10 ++- pkg/config/auditors.go | 34 ++++++++ pkg/config/coinmarketcap.go | 29 +++++++ pkg/config/config.go | 136 ++++++++++++++++++++++++++++++ pkg/config/config_test.go | 108 ++++++++++++++++++++++++ pkg/config/eth.go | 101 ++++++++++++++++++++++ pkg/config/payers.go | 34 ++++++++ pkg/config/testdata/defaults.toml | 30 +++++++ pkg/config/testdata/override.toml | 32 +++++++ pkg/config/types.go | 72 ++++++++++++++++ pkg/config/utils.go | 56 ++++++++++++ pkg/config/zksync.go | 71 ++++++++++++++++ pkg/config/zksync_era.go | 71 ++++++++++++++++ 14 files changed, 785 insertions(+), 2 deletions(-) create mode 100644 pkg/config/auditors.go create mode 100644 pkg/config/coinmarketcap.go create mode 100644 pkg/config/config.go create mode 100644 pkg/config/config_test.go create mode 100644 pkg/config/eth.go create mode 100644 pkg/config/payers.go create mode 100644 pkg/config/testdata/defaults.toml create mode 100644 pkg/config/testdata/override.toml create mode 100644 pkg/config/types.go create mode 100644 pkg/config/utils.go create mode 100644 pkg/config/zksync.go create mode 100644 pkg/config/zksync_era.go diff --git a/go.mod b/go.mod index d100a79..44bd14a 100644 --- a/go.mod +++ b/go.mod @@ -8,9 +8,10 @@ require ( github.com/manifoldco/promptui v0.8.0 github.com/mattn/go-sqlite3 v1.14.22 github.com/mitchellh/go-homedir v1.1.0 + github.com/pelletier/go-toml/v2 v2.2.2 github.com/shopspring/decimal v1.2.0 github.com/spf13/cobra v1.5.0 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 github.com/zeebo/errs v1.2.2 github.com/zeebo/errs/v2 v2.0.3 github.com/zksync-sdk/zksync-sdk-go v0.0.0-20211119083613-58613b4d3d77 diff --git a/go.sum b/go.sum index b17cc9b..aa1c13e 100644 --- a/go.sum +++ b/go.sum @@ -435,6 +435,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -506,13 +508,19 @@ github.com/stephenlacy/go-ethereum-hdwallet v0.0.0-20230913225845-a4fa94429863 h github.com/stephenlacy/go-ethereum-hdwallet v0.0.0-20230913225845-a4fa94429863/go.mod h1:oPTjPNrRucLv9mU27iNPj6n0CWWcNFhoXFOLVGJwHCA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= diff --git a/pkg/config/auditors.go b/pkg/config/auditors.go new file mode 100644 index 0000000..9acbdca --- /dev/null +++ b/pkg/config/auditors.go @@ -0,0 +1,34 @@ +package config + +import "storj.io/crypto-batch-payment/pkg/payer" + +type Auditor interface { + payer.Auditor + Close() +} + +type auditorWrapper struct { + payer.Auditor + closeFunc func() +} + +func (w auditorWrapper) Close() { + if w.closeFunc != nil { + w.closeFunc() + } +} + +type Auditors map[payer.Type]Auditor + +func (as *Auditors) Add(t payer.Type, a Auditor) { + if *as == nil { + *as = make(map[payer.Type]Auditor) + } + (*as)[t] = a +} + +func (as Auditors) Close() { + for _, a := range as { + a.Close() + } +} diff --git a/pkg/config/coinmarketcap.go b/pkg/config/coinmarketcap.go new file mode 100644 index 0000000..61c6ab5 --- /dev/null +++ b/pkg/config/coinmarketcap.go @@ -0,0 +1,29 @@ +package config + +import ( + "time" + + "github.com/zeebo/errs" + + "storj.io/crypto-batch-payment/pkg/coinmarketcap" +) + +type CoinMarketCap struct { + APIURL string `toml:"api_url"` + APIKeyPath Path `toml:"api_key_path"` + CacheExpiry Duration `toml:"cache_expiry"` +} + +func (c CoinMarketCap) NewQuoter() (coinmarketcap.Quoter, error) { + apiKey, err := loadFirstLine(string(c.APIKeyPath)) + if err != nil { + return nil, errs.New("failed to load CoinMarketCap key: %v\n", err) + } + + quoter, err := coinmarketcap.NewCachingClient(c.APIURL, apiKey, time.Duration(c.CacheExpiry)) + if err != nil { + return nil, errs.New("failed instantiate coinmarketcap client: %v\n", err) + } + + return quoter, nil +} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..09e748c --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,136 @@ +package config + +import ( + "bytes" + "context" + "fmt" + "os" + "time" + + "github.com/pelletier/go-toml/v2" + + "storj.io/crypto-batch-payment/pkg/coinmarketcap" + "storj.io/crypto-batch-payment/pkg/payer" + "storj.io/crypto-batch-payment/pkg/pipeline" +) + +type Config struct { + Pipeline Pipeline `toml:"pipeline"` + CoinMarketCap CoinMarketCap `toml:"coinmarketcap"` + Eth *Eth `toml:"eth"` + ZkSync *ZkSync `toml:"zksync"` + ZkSyncEra *ZkSyncEra `toml:"zksync-era"` +} + +func (c *Config) NewPayers(ctx context.Context) (_ Payers, err error) { + var payers Payers + defer func() { + if err != nil { + payers.Close() + } + }() + + if c.Eth != nil { + p, err := c.Eth.NewPayer(ctx) + if err != nil { + return nil, fmt.Errorf("failed to init eth payer: %w", err) + } + payers.Add(payer.Eth, p) + } + + if c.ZkSync != nil { + p, err := c.ZkSync.NewPayer(ctx) + if err != nil { + return nil, fmt.Errorf("failed to init zksync payer: %w", err) + } + payers.Add(payer.ZkSync, p) + } + + if c.ZkSyncEra != nil { + p, err := c.ZkSyncEra.NewPayer(ctx) + if err != nil { + return nil, fmt.Errorf("failed to init zksync-era payer: %w", err) + } + payers.Add(payer.ZkSyncEra, p) + } + + return payers, nil +} + +func (c *Config) NewAuditors(ctx context.Context) (_ Auditors, err error) { + var auditors Auditors + defer func() { + if err != nil { + auditors.Close() + } + }() + + if c.Eth != nil { + p, err := c.Eth.NewAuditor(ctx) + if err != nil { + return nil, fmt.Errorf("failed to init eth auditor: %w", err) + } + auditors.Add(payer.Eth, p) + } + + if c.ZkSync != nil { + p, err := c.ZkSync.NewAuditor(ctx) + if err != nil { + return nil, fmt.Errorf("failed to init zksync auditor: %w", err) + } + auditors.Add(payer.ZkSync, p) + } + + if c.ZkSyncEra != nil { + p, err := c.ZkSyncEra.NewAuditor(ctx) + if err != nil { + return nil, fmt.Errorf("failed to init zksync-era auditor: %w", err) + } + auditors.Add(payer.ZkSyncEra, p) + } + + return auditors, nil +} + +type Pipeline struct { + DepthLimit int `toml:"depth_limit"` + TxDelay Duration `toml:"tx_delay"` +} + +func Load(path string) (Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return Config{}, fmt.Errorf("failed to read config: %w", err) + } + return Parse(data) +} + +func Parse(data []byte) (Config, error) { + const ( + defaultPipelineDepthLimit = pipeline.DefaultLimit + defaultPipelineTxDelay = Duration(pipeline.DefaultTxDelay) + defaultCoinMarketCapAPIURL = coinmarketcap.ProductionAPIURL + defaultCoinMarketCapKeyPath = "~/.coinmarketcap" + defaultCoinMarketCapCacheExpiry = time.Second * 5 + ) + + config := Config{ + Pipeline: Pipeline{ + DepthLimit: defaultPipelineDepthLimit, + TxDelay: defaultPipelineTxDelay, + }, + CoinMarketCap: CoinMarketCap{ + APIURL: defaultCoinMarketCapAPIURL, + APIKeyPath: ToPath(defaultCoinMarketCapKeyPath), + CacheExpiry: Duration(defaultCoinMarketCapCacheExpiry), + }, + } + + d := toml.NewDecoder(bytes.NewReader(data)) + d.DisallowUnknownFields() + if err := d.Decode(&config); err != nil { + return Config{}, fmt.Errorf("failed to unmarshal config: %w", err) + } + + return config, nil +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000..415481c --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,108 @@ +package config_test + +import ( + "math/big" + "os/user" + "path/filepath" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "storj.io/crypto-batch-payment/pkg/config" +) + +func TestLoad_Defaults(t *testing.T) { + currentUser, err := user.Current() + require.NoError(t, err) + + homePath := func(suffix string) config.Path { + return config.Path(filepath.Join(currentUser.HomeDir, suffix)) + } + + cfg, err := config.Load("./testdata/defaults.toml") + require.NoError(t, err) + + assert.Equal(t, config.Config{ + Pipeline: config.Pipeline{ + DepthLimit: 16, + TxDelay: 0, + }, + CoinMarketCap: config.CoinMarketCap{ + APIURL: "https://pro-api.coinmarketcap.com", + APIKeyPath: homePath(".coinmarketcap"), + CacheExpiry: 5000000000, + }, + Eth: &config.Eth{ + NodeAddress: "https://someaddress.test", + SpenderKeyPath: homePath("some.key"), + ERC20ContractAddress: common.HexToAddress("0x1111111111111111111111111111111111111111"), + ChainID: 0, + Owner: nil, + MaxGas: nil, + GasTipCap: nil, + }, + ZkSync: &config.ZkSync{ + NodeAddress: "https://api.zksync.io", + SpenderKeyPath: homePath("some.key"), + ChainID: 0, + MaxFee: nil, + }, + ZkSyncEra: &config.ZkSyncEra{ + NodeAddress: "https://mainnet.era.zksync.io", + SpenderKeyPath: homePath("some.key"), + ERC20ContractAddress: common.HexToAddress("0x2222222222222222222222222222222222222222"), + ChainID: 0, + MaxFee: nil, + PaymasterAddress: nil, + PaymasterPayload: nil, + }, + }, cfg) +} + +func TestLoad_Overrides(t *testing.T) { + cfg, err := config.Load("./testdata/override.toml") + require.NoError(t, err) + + assert.Equal(t, config.Config{ + Pipeline: config.Pipeline{ + DepthLimit: 24, + TxDelay: config.Duration(time.Minute), + }, + CoinMarketCap: config.CoinMarketCap{ + APIURL: "https://override.test", + APIKeyPath: "override", + CacheExpiry: 5000000000, + }, + Eth: &config.Eth{ + NodeAddress: "https://override.test", + SpenderKeyPath: "override", + ERC20ContractAddress: common.HexToAddress("0xe66652d41EE7e81d3fcAe1dF7F9B9f9411ac835e"), + ChainID: 12345, + Owner: ptrOf(common.HexToAddress("0xe66652d41EE7e81d3fcAe1dF7F9B9f9411ac835e")), + MaxGas: big.NewInt(80_000_000_000), + GasTipCap: big.NewInt(2_000_000_000), + }, + ZkSync: &config.ZkSync{ + NodeAddress: "https://override.test", + SpenderKeyPath: "override", + ChainID: 12345, + MaxFee: big.NewInt(1234), + }, + ZkSyncEra: &config.ZkSyncEra{ + NodeAddress: "https://override.test", + SpenderKeyPath: "override", + ERC20ContractAddress: common.HexToAddress("0xe66652d41EE7e81d3fcAe1dF7F9B9f9411ac835e"), + ChainID: 12345, + MaxFee: big.NewInt(5678), + PaymasterAddress: ptrOf(common.HexToAddress("0xe66652d41EE7e81d3fcAe1dF7F9B9f9411ac835e")), + PaymasterPayload: []byte("\x01\x23"), + }, + }, cfg) +} + +func ptrOf[T any](t T) *T { + return &t +} diff --git a/pkg/config/eth.go b/pkg/config/eth.go new file mode 100644 index 0000000..0c3aa72 --- /dev/null +++ b/pkg/config/eth.go @@ -0,0 +1,101 @@ +package config + +import ( + "context" + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/zeebo/errs" + + "storj.io/crypto-batch-payment/pkg/eth" +) + +const ( + defaultEthChainID = 1 + defaultMaxGas = "70_000_000_000" + defaultGasTipCap = "1_000_000_000" +) + +type Eth struct { + NodeAddress string `toml:"node_address"` + SpenderKeyPath Path `toml:"spender_key_path"` + ERC20ContractAddress common.Address `toml:"erc20_contract_address"` + ChainID int `toml:"chain_id"` + Owner *common.Address `toml:"owner"` + MaxGas *big.Int `toml:"max_gas"` + GasTipCap *big.Int `toml:"gas_tip_cap"` +} + +func (c Eth) NewPayer(ctx context.Context) (_ Payer, err error) { + // Check for required parameters + if c.NodeAddress == "" { + return nil, errors.New("node_address is not configured") + } + if c.ERC20ContractAddress == zeroAddress { + return nil, errors.New("erc20_contract_address is not configured") + } + + // Apply defaults + if c.ChainID == 0 { + c.ChainID = defaultEthChainID + } + if c.MaxGas == nil { + c.MaxGas, _ = new(big.Int).SetString(defaultMaxGas, 0) + } + if c.GasTipCap == nil { + c.GasTipCap, _ = new(big.Int).SetString(defaultGasTipCap, 0) + } + + spenderKey, spenderAddress, err := loadSpenderKey(string(c.SpenderKeyPath)) + if err != nil { + return nil, err + } + + owner := spenderAddress + if c.Owner != nil { + owner = *c.Owner + } + + client, err := ethclient.Dial(c.NodeAddress) + if err != nil { + return nil, errs.Wrap(err) + } + defer func() { + if err != nil { + client.Close() + } + }() + + ethPayer, err := eth.NewPayer(ctx, + client, + c.ERC20ContractAddress, + owner, + spenderKey, + big.NewInt(int64(c.ChainID)), + c.GasTipCap, + c.MaxGas, + ) + if err != nil { + return nil, errs.Wrap(err) + } + + return &payerWrapper{ + Payer: ethPayer, + closeFunc: client.Close, + }, nil +} + +func (c Eth) NewAuditor(ctx context.Context) (_ Auditor, err error) { + // Check for required parameters + if c.NodeAddress == "" { + return nil, errors.New("node_address is not configured") + } + + ethAuditor, err := eth.NewAuditor(c.NodeAddress) + if err != nil { + return nil, errs.Wrap(err) + } + return ethAuditor, nil +} diff --git a/pkg/config/payers.go b/pkg/config/payers.go new file mode 100644 index 0000000..f0028ad --- /dev/null +++ b/pkg/config/payers.go @@ -0,0 +1,34 @@ +package config + +import "storj.io/crypto-batch-payment/pkg/payer" + +type Payer interface { + payer.Payer + Close() +} + +type payerWrapper struct { + payer.Payer + closeFunc func() +} + +func (w payerWrapper) Close() { + if w.closeFunc != nil { + w.closeFunc() + } +} + +type Payers map[payer.Type]Payer + +func (ps *Payers) Add(t payer.Type, p Payer) { + if *ps == nil { + *ps = make(map[payer.Type]Payer) + } + (*ps)[t] = p +} + +func (ps Payers) Close() { + for _, p := range ps { + p.Close() + } +} diff --git a/pkg/config/testdata/defaults.toml b/pkg/config/testdata/defaults.toml new file mode 100644 index 0000000..de7e1fa --- /dev/null +++ b/pkg/config/testdata/defaults.toml @@ -0,0 +1,30 @@ +[pipeline] +# depth_limit = 16 +# tx_delay = 0 + +[coinmarketcap] +# api_url = "https://pro-api.coinmarketcap.com" +# api_key_path = "~/.coinmarketcap" +# cache_expiry = "5s" + +[eth] +node_address = "https://someaddress.test" +spender_key_path = "~/some.key" +erc20_contract_address = "0x1111111111111111111111111111111111111111" +# owner = "" +# max_gas = "70_000_000_000" +# gas_tip_cap = "1_000_000_000" + +[zksync] +node_address = "https://api.zksync.io" +spender_key_path = "~/some.key" +# max_fee = "" + +[zksync-era] +node_address = "https://mainnet.era.zksync.io" +spender_key_path = "~/some.key" +erc20_contract_address = "0x2222222222222222222222222222222222222222" +# chain_id = 324 +# max_fee = "" +# paymaster_address = "" +# paymaster_payload = "" diff --git a/pkg/config/testdata/override.toml b/pkg/config/testdata/override.toml new file mode 100644 index 0000000..d8c7b6c --- /dev/null +++ b/pkg/config/testdata/override.toml @@ -0,0 +1,32 @@ +[pipeline] +depth_limit = 24 +tx_delay = "1m" + +[coinmarketcap] +api_url = "https://override.test" +api_key_path = "override" +cache_expiry = "5s" + +[eth] +node_address = "https://override.test" +spender_key_path = "override" +erc20_contract_address = "0xe66652d41EE7e81d3fcAe1dF7F9B9f9411ac835e" +chain_id = 12345 +owner = "0xe66652d41EE7e81d3fcAe1dF7F9B9f9411ac835e" +max_gas = "80_000_000_000" +gas_tip_cap = "2_000_000_000" + +[zksync] +node_address = "https://override.test" +spender_key_path = "override" +chain_id = 12345 +max_fee = "1234" + +[zksync-era] +node_address = "https://override.test" +spender_key_path = "override" +erc20_contract_address = "0xe66652d41EE7e81d3fcAe1dF7F9B9f9411ac835e" +chain_id = 12345 +max_fee = "5678" +paymaster_address = "0xe66652d41EE7e81d3fcAe1dF7F9B9f9411ac835e" +paymaster_payload = "0123" diff --git a/pkg/config/types.go b/pkg/config/types.go new file mode 100644 index 0000000..d1093cc --- /dev/null +++ b/pkg/config/types.go @@ -0,0 +1,72 @@ +package config + +import ( + "encoding/hex" + "os" + "os/user" + "path/filepath" + "strings" + "time" +) + +type Duration time.Duration + +func (d *Duration) UnmarshalText(b []byte) error { + v, err := time.ParseDuration(string(b)) + if err != nil { + return err + } + *d = Duration(v) + return nil +} + +type HexString []byte + +func (d *HexString) UnmarshalText(b []byte) error { + v, err := hex.DecodeString(string(b)) + if err != nil { + return err + } + *d = HexString(v) + return nil +} + +type Path string + +func (p *Path) UnmarshalText(b []byte) error { + *p = ToPath(string(b)) + return nil +} + +func ToPath(path string) Path { + return Path(expandHomeVar(path)) +} + +func expandHomeVar(path string) string { + segments := strings.Split(path, string(os.PathSeparator)) + if len(segments) == 0 { + return path + } + username, isHomePath := strings.CutPrefix(segments[0], "~") + if !isHomePath { + return path + } + + var u *user.User + var err error + if username == "" { + u, err = user.Current() + } else { + u, err = user.Lookup(username) + } + if err != nil { + return path + } + + if u.HomeDir == "" { + return path + } + + segments[0] = u.HomeDir + return filepath.Join(segments...) +} diff --git a/pkg/config/utils.go b/pkg/config/utils.go new file mode 100644 index 0000000..eb835f9 --- /dev/null +++ b/pkg/config/utils.go @@ -0,0 +1,56 @@ +package config + +import ( + "bufio" + "crypto/ecdsa" + "errors" + "io/fs" + "os" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/zeebo/errs" +) + +var ( + zeroAddress common.Address +) + +func loadSpenderKey(path string) (*ecdsa.PrivateKey, common.Address, error) { + return loadETHKey(path, "spender_key_path") +} + +func loadETHKey(path, which string) (*ecdsa.PrivateKey, common.Address, error) { + fi, err := os.Stat(path) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return nil, common.Address{}, errs.New("%s: %s not found\n", which, path) + } + return nil, common.Address{}, errs.New("unable to stat %s key: %v\n", which, err) + } + + if (fi.Mode() & 0177) != 0 { + return nil, common.Address{}, errs.New("%s mode %#o is too permissive (set to 0600)\n", path, fi.Mode()) + } + + key, err := crypto.LoadECDSA(path) + if err != nil { + return nil, common.Address{}, errs.New("unable to load %s key: %v\n", which, err) + } + return key, crypto.PubkeyToAddress(key.PublicKey), nil +} + +func loadFirstLine(p string) (_ string, err error) { + f, err := os.Open(p) + if err != nil { + return "", errs.Wrap(err) + } + defer func() { + err = errs.Combine(err, f.Close()) + }() + scanner := bufio.NewScanner(f) + if !scanner.Scan() { + return "", errs.Wrap(scanner.Err()) + } + return scanner.Text(), nil +} diff --git a/pkg/config/zksync.go b/pkg/config/zksync.go new file mode 100644 index 0000000..8736a0d --- /dev/null +++ b/pkg/config/zksync.go @@ -0,0 +1,71 @@ +package config + +import ( + "context" + "errors" + "math/big" + + "github.com/zeebo/errs" + + "storj.io/crypto-batch-payment/pkg/zksync" +) + +type ZkSync struct { + NodeAddress string `toml:"node_address"` + SpenderKeyPath Path `toml:"spender_key_path"` + ChainID int `toml:"chain_id"` + MaxFee *big.Int `toml:"max_fee"` +} + +func (c ZkSync) NewPayer(ctx context.Context) (_ Payer, err error) { + // Check for required parameters + if c.NodeAddress == "" { + return nil, errors.New("zksync node_address is not configured") + } + + // Apply defaults + if c.ChainID == 0 { + c.ChainID = defaultEthChainID + } + + spenderKey, _, err := loadSpenderKey(string(c.SpenderKeyPath)) + if err != nil { + return nil, err + } + + zkPayer, err := zksync.NewPayer( + ctx, + c.NodeAddress, + spenderKey, + c.ChainID, + false, + c.MaxFee) + if err != nil { + return nil, errs.Wrap(err) + } + + return &payerWrapper{ + Payer: zkPayer, + }, nil +} + +func (c ZkSync) NewAuditor(ctx context.Context) (_ Auditor, err error) { + // Check for required parameters + if c.NodeAddress == "" { + return nil, errors.New("zksync node_address is not configured") + } + + // Apply defaults + if c.ChainID == 0 { + c.ChainID = defaultEthChainID + } + + zkAuditor, err := zksync.NewAuditor(c.NodeAddress, c.ChainID) + if err != nil { + return nil, errs.Wrap(err) + } + + return &auditorWrapper{ + Auditor: zkAuditor, + }, nil +} diff --git a/pkg/config/zksync_era.go b/pkg/config/zksync_era.go new file mode 100644 index 0000000..7807642 --- /dev/null +++ b/pkg/config/zksync_era.go @@ -0,0 +1,71 @@ +package config + +import ( + "context" + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + + "storj.io/crypto-batch-payment/pkg/zksyncera" +) + +const ( + defaultZksyncEraChainID = 324 +) + +type ZkSyncEra struct { + NodeAddress string `toml:"node_address"` + SpenderKeyPath Path `toml:"spender_key_path"` + ERC20ContractAddress common.Address `toml:"erc20_contract_address"` + ChainID int `toml:"chain_id"` + MaxFee *big.Int `toml:"max_fee"` + PaymasterAddress *common.Address `toml:"paymaster_address"` + PaymasterPayload HexString `toml:"paymaster_payload"` +} + +func (c ZkSyncEra) NewPayer(ctx context.Context) (_ Payer, err error) { + // Check for required parameters + if c.NodeAddress == "" { + return nil, errors.New("node_address is not configured") + } + if c.ERC20ContractAddress == zeroAddress { + return nil, errors.New("erc20_contract_address is not configured") + } + + // Apply defaults + if c.ChainID == 0 { + c.ChainID = defaultZksyncEraChainID + } + + spenderKey, _, err := loadSpenderKey(string(c.SpenderKeyPath)) + if err != nil { + return nil, err + } + + payer, err := zksyncera.NewPayer( + c.ERC20ContractAddress, + c.NodeAddress, + spenderKey, + c.ChainID, + c.PaymasterAddress, + c.PaymasterPayload, + c.MaxFee) + if err != nil { + return nil, err + } + + return &payerWrapper{ + Payer: payer, + }, nil +} + +func (c ZkSyncEra) NewAuditor(ctx context.Context) (_ Auditor, err error) { + auditor, err := zksyncera.NewAuditor(c.NodeAddress) + if err != nil { + return nil, err + } + return &auditorWrapper{ + Auditor: auditor, + }, nil +}