diff --git a/config/config.yaml b/config/config.yaml index 8a489dd69..da46699ab 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -215,6 +215,11 @@ checks: options: timeout: 5m type: pingpong + withdraw: + options: + target-address: 0xec44cb15b1b033e74d55ac5d0e24d861bde54532 + timeout: 5m + type: withdraw pss: options: address-prefix: 2 diff --git a/config/local.yaml b/config/local.yaml index 12caa42a3..a4b0c5c31 100644 --- a/config/local.yaml +++ b/config/local.yaml @@ -194,6 +194,7 @@ bee-configs: welcome-message: "Welcome to the Swarm, this is a local cluster!" warmup-time: 0s allow-private-cidrs: true + withdrawal-addresses-whitelist: 0xec44cb15b1b033e74d55ac5d0e24d861bde54532 bootnode-local: _inherit: "bee-local" @@ -263,6 +264,11 @@ checks: options: timeout: 5m type: pingpong + ci-withdraw: + options: + target-address: 0xec44cb15b1b033e74d55ac5d0e24d861bde54532 + timeout: 5m + type: withdraw ci-pss: options: count: 3 diff --git a/pkg/bee/api/auth.go b/pkg/bee/api/auth.go index 3b1a91f7b..00357a41d 100644 --- a/pkg/bee/api/auth.go +++ b/pkg/bee/api/auth.go @@ -155,6 +155,8 @@ var policies = [][]string{ {"maintainer", "/chequebook/cheque", "GET"}, {"maintainer", "/chequebook/address", "GET"}, {"maintainer", "/chequebook/balance", "GET"}, + {"maintainer", "/wallet", "GET"}, + {"maintainer", "/wallet/withdraw/*", "POST"}, {"maintainer", "/chunks/*", "(GET)|(DELETE)"}, {"maintainer", "/reservestate", "GET"}, {"maintainer", "/chainstate", "GET"}, diff --git a/pkg/bee/client.go b/pkg/bee/client.go index b696c6ed5..ed5bb6538 100644 --- a/pkg/bee/client.go +++ b/pkg/bee/client.go @@ -13,6 +13,7 @@ import ( "sync" "time" + "github.com/ethereum/go-ethereum/common" "github.com/ethersphere/bee/pkg/swarm" "github.com/ethersphere/beekeeper/pkg/bee/api" "github.com/ethersphere/beekeeper/pkg/bee/debugapi" @@ -847,3 +848,33 @@ func (c *Client) GetStake(ctx context.Context) (*big.Int, error) { func (c *Client) WithdrawStake(ctx context.Context) (string, error) { return c.debug.Stake.WithdrawStake(ctx) } + +// WalletBalance fetches the balance for the given token +func (c *Client) WalletBalance(ctx context.Context, token string) (*big.Int, error) { + resp, err := c.debug.Node.Wallet(ctx) + if err != nil { + return nil, err + } + + if token == "BZZ" { + return resp.BZZ.Int, nil + } + + return resp.NativeToken.Int, nil +} + +// Withdraw transfers token from eth address to the provided address +func (c *Client) Withdraw(ctx context.Context, token, addr string, amount int64) error { + resp, err := c.debug.Node.Withdraw(ctx, token, addr, amount) + if err != nil { + return err + } + + var zeroHash common.Hash + + if resp == zeroHash { + return errors.New("withdraw returned zero hash") + } + + return nil +} diff --git a/pkg/bee/debugapi/node.go b/pkg/bee/debugapi/node.go index ef4ab2fe1..c1fef49f3 100644 --- a/pkg/bee/debugapi/node.go +++ b/pkg/bee/debugapi/node.go @@ -2,9 +2,11 @@ package debugapi import ( "context" + "fmt" "net/http" "time" + "github.com/ethereum/go-ethereum/common" "github.com/ethersphere/beekeeper/pkg/bigint" "github.com/ethersphere/bee/pkg/swarm" @@ -249,3 +251,29 @@ func (n *NodeService) Topology(ctx context.Context) (resp Topology, err error) { return } + +type Wallet struct { + BZZ *bigint.BigInt `json:"bzzBalance"` + NativeToken *bigint.BigInt `json:"nativeTokenBalance"` +} + +// Wallet returns the wallet state +func (n *NodeService) Wallet(ctx context.Context) (resp Wallet, err error) { + err = n.client.requestJSON(ctx, http.MethodGet, "/wallet", nil, &resp) + return +} + +// Withdraw calls wallet withdraw endpoint +func (n *NodeService) Withdraw(ctx context.Context, token, addr string, amount int64) (tx common.Hash, err error) { + endpoint := fmt.Sprintf("/wallet/withdraw/%s?address=%s&amount=%d", token, addr, amount) + + r := struct { + TransactionHash common.Hash `json:"transactionHash"` + }{} + + if err = n.client.requestJSON(ctx, http.MethodPost, endpoint, nil, &r); err != nil { + return + } + + return r.TransactionHash, nil +} diff --git a/pkg/check/withdraw/withdraw.go b/pkg/check/withdraw/withdraw.go new file mode 100644 index 000000000..11983caa1 --- /dev/null +++ b/pkg/check/withdraw/withdraw.go @@ -0,0 +1,81 @@ +package withdraw + +import ( + "context" + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethersphere/beekeeper/pkg/beekeeper" + "github.com/ethersphere/beekeeper/pkg/logging" + "github.com/ethersphere/beekeeper/pkg/orchestration" + test "github.com/ethersphere/beekeeper/pkg/test" +) + +// Options represents check options +type Options struct { + TargetAddr string +} + +// NewDefaultOptions returns new default options +func NewDefaultOptions() Options { + return Options{} +} + +// compile check whether Check implements interface +var _ beekeeper.Action = (*Check)(nil) + +// Check instance +type Check struct { + logger logging.Logger +} + +// NewCheck returns new check +func NewCheck(logger logging.Logger) beekeeper.Action { + return &Check{ + logger: logger, + } +} + +func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, opts interface{}) (err error) { + o, ok := opts.(Options) + if !ok { + return fmt.Errorf("invalid options type") + } + + var checkCase *test.CheckCase + + if checkCase, err = test.NewCheckCase(ctx, cluster, test.CaseOptions{}, c.logger); err != nil { + return err + } + + target := checkCase.Bee(1) + + c.logger.Infof("target is %s", target.Name()) + + c.logger.Info("withdrawing native...") + + if err := target.Withdraw(ctx, "NativeToken", o.TargetAddr); err != nil { + return fmt.Errorf("withdraw native: %w", err) + } + + c.logger.Info("success") + c.logger.Info("withdrawing to a non whitelisted address") + + var zeroAddr common.Address + + if err := target.Withdraw(ctx, "NativeToken", zeroAddr.String()); err == nil { + return errors.New("withdraw to non-whitelisted address expected to fail") + } + + c.logger.Info("success") + c.logger.Info("withdrawing bzz...") + + if err := target.Withdraw(ctx, "BZZ", o.TargetAddr); err != nil { + return fmt.Errorf("withdraw bzz: %w", err) + } + + c.logger.Info("success") + + return nil +} diff --git a/pkg/config/bee.go b/pkg/config/bee.go index f69a5557f..2d2536348 100644 --- a/pkg/config/bee.go +++ b/pkg/config/bee.go @@ -60,6 +60,7 @@ type BeeConfig struct { Verbosity *uint64 `yaml:"verbosity"` WelcomeMessage *string `yaml:"welcome-message"` WarmupTime *time.Duration `yaml:"warmup-time"` + WithdrawAddress *string `yaml:"withdrawal-addresses-whitelist"` } // Export exports BeeConfig to orchestration.Config diff --git a/pkg/config/check.go b/pkg/config/check.go index 4b69a1507..a6ecc6269 100644 --- a/pkg/config/check.go +++ b/pkg/config/check.go @@ -31,6 +31,7 @@ import ( "github.com/ethersphere/beekeeper/pkg/check/settlements" "github.com/ethersphere/beekeeper/pkg/check/smoke" "github.com/ethersphere/beekeeper/pkg/check/soc" + "github.com/ethersphere/beekeeper/pkg/check/withdraw" "github.com/ethersphere/beekeeper/pkg/logging" "github.com/ethersphere/beekeeper/pkg/random" "gopkg.in/yaml.v3" @@ -574,6 +575,24 @@ var Checks = map[string]CheckType{ return nil, fmt.Errorf("applying options: %w", err) } + return opts, nil + }, + }, + "withdraw": { + NewAction: withdraw.NewCheck, + NewOptions: func(checkGlobalConfig CheckGlobalConfig, check Check) (interface{}, error) { + checkOpts := new(struct { + TargetAddr *string `yaml:"target-address"` + }) + if err := check.Options.Decode(checkOpts); err != nil { + return nil, fmt.Errorf("decoding check %s options: %w", check.Type, err) + } + opts := withdraw.NewDefaultOptions() + + if err := applyCheckConfig(checkGlobalConfig, checkOpts, &opts); err != nil { + return nil, fmt.Errorf("applying options: %w", err) + } + return opts, nil }, }, diff --git a/pkg/orchestration/k8s/helpers.go b/pkg/orchestration/k8s/helpers.go index 267abeba6..f2c27dc4f 100644 --- a/pkg/orchestration/k8s/helpers.go +++ b/pkg/orchestration/k8s/helpers.go @@ -59,6 +59,7 @@ tracing-service-name: {{.TracingServiceName}} verbosity: {{.Verbosity}} welcome-message: {{.WelcomeMessage}} warmup-time: {{.WarmupTime}} +withdrawal-addresses-whitelist: {{.WithdrawAddress}} ` ) diff --git a/pkg/orchestration/node.go b/pkg/orchestration/node.go index 7f42f8ffd..0263f87f6 100644 --- a/pkg/orchestration/node.go +++ b/pkg/orchestration/node.go @@ -161,4 +161,5 @@ type Config struct { Verbosity uint64 // log verbosity level 0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=trace WelcomeMessage string // send a welcome message string during handshakes WarmupTime time.Duration // warmup time pull/pushsync protocols + WithdrawAddress string // allowed addresses for wallet withdrawal } diff --git a/pkg/test/node.go b/pkg/test/node.go index 69948efd0..64f2caf65 100644 --- a/pkg/test/node.go +++ b/pkg/test/node.go @@ -4,7 +4,9 @@ import ( "bytes" "context" "fmt" + "math/big" "math/rand" + "time" "github.com/ethersphere/bee/pkg/swarm" "github.com/ethersphere/beekeeper/pkg/bee" @@ -96,3 +98,35 @@ func (b *BeeV2) NewChunkUploader(ctx context.Context) (*ChunkUploader, error) { logger: b.logger, }, nil } + +type Wallet struct { + BZZ, Native *big.Int +} + +const amount int64 = 1000000 + +func (b *BeeV2) Withdraw(ctx context.Context, token, addr string) error { + before, err := b.client.WalletBalance(ctx, token) + if err != nil { + return fmt.Errorf("(%s) wallet balance %w", b.name, err) + } + + if err := b.client.Withdraw(ctx, token, addr, amount); err != nil { + return fmt.Errorf("(%s) withdraw balance %w", b.name, err) + } + + time.Sleep(3 * time.Second) + + after, err := b.client.WalletBalance(ctx, token) + if err != nil { + return fmt.Errorf("(%s) wallet balance %w", b.name, err) + } + + want := big.NewInt(0).Sub(before, big.NewInt(amount)) + + if after.Cmp(want) > 0 { + return fmt.Errorf("incorrect balance after withdraw:\ngot %d\nwant %d", after, want) + } + + return nil +}