Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TOML config loading package #28

Merged
merged 1 commit into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
34 changes: 34 additions & 0 deletions pkg/config/auditors.go
Original file line number Diff line number Diff line change
@@ -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()
}
}
29 changes: 29 additions & 0 deletions pkg/config/coinmarketcap.go
Original file line number Diff line number Diff line change
@@ -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
}
136 changes: 136 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
@@ -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
}
108 changes: 108 additions & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading