From 9826f8e1feabbf92c88ce81d732b8a7a1606a07e Mon Sep 17 00:00:00 2001 From: swarmHaseeb Date: Fri, 16 Dec 2022 18:50:43 +0500 Subject: [PATCH 1/8] refactor: enable deleting stake --- pkg/api/router.go | 4 +- pkg/api/staking.go | 24 ++ pkg/api/staking_test.go | 71 +++- pkg/storageincentives/staking/contract.go | 79 +++- .../staking/contract_test.go | 402 ++++++++++++++++++ .../staking/mock/contract.go | 14 +- 6 files changed, 588 insertions(+), 6 deletions(-) diff --git a/pkg/api/router.go b/pkg/api/router.go index 5bf51509d5a..f7085f8070f 100644 --- a/pkg/api/router.go +++ b/pkg/api/router.go @@ -562,8 +562,10 @@ func (s *Service) mountBusinessDebug(restricted bool) { handle("/stake", web.ChainHandlers( s.stakingAccessHandler, + s.gasConfigMiddleware("get or delete stake"), web.FinalHandler(jsonhttp.MethodHandler{ - "GET": http.HandlerFunc(s.getStakedAmountHandler), + "GET": http.HandlerFunc(s.getStakedAmountHandler), + "DELETE": http.HandlerFunc(s.deleteStakeHandler), })), ) } diff --git a/pkg/api/staking.go b/pkg/api/staking.go index 1e50f0baff1..37862f36f6f 100644 --- a/pkg/api/staking.go +++ b/pkg/api/staking.go @@ -36,6 +36,10 @@ type stakeDepositResponse struct { TxHash string `json:"txhash"` } +type deleteStakeResponse struct { + TxHash string `json:"txhash"` +} + func (s *Service) stakingDepositHandler(w http.ResponseWriter, r *http.Request) { logger := s.logger.WithName("post_stake_deposit").Build() @@ -90,3 +94,23 @@ func (s *Service) getStakedAmountHandler(w http.ResponseWriter, r *http.Request) jsonhttp.OK(w, getStakeResponse{StakedAmount: bigint.Wrap(stakedAmount)}) } + +func (s *Service) deleteStakeHandler(w http.ResponseWriter, r *http.Request) { + logger := s.logger.WithName("delete_stake").Build() + + txHash, err := s.stakingContract.DeleteStake(r.Context()) + if err != nil { + if errors.Is(err, staking.ErrInsufficientStake) { + logger.Debug("insufficient stake", "overlayAddr", s.overlay, "error", err) + logger.Error(nil, "insufficient stake") + jsonhttp.BadRequest(w, "insufficient stake to delete") + return + } + logger.Debug("delete stake failed", "error", err) + logger.Error(nil, "delete stake failed") + jsonhttp.InternalServerError(w, "cannot delete stake") + return + } + + jsonhttp.OK(w, deleteStakeResponse{TxHash: txHash.String()}) +} diff --git a/pkg/api/staking_test.go b/pkg/api/staking_test.go index 51477c94b81..a9c7d3aff25 100644 --- a/pkg/api/staking_test.go +++ b/pkg/api/staking_test.go @@ -7,12 +7,13 @@ package api_test import ( "context" "fmt" - "github.com/ethereum/go-ethereum/common" - "github.com/ethersphere/bee/pkg/bigint" "math/big" "net/http" "testing" + "github.com/ethereum/go-ethereum/common" + "github.com/ethersphere/bee/pkg/bigint" + "github.com/ethersphere/bee/pkg/api" "github.com/ethersphere/bee/pkg/jsonhttp" "github.com/ethersphere/bee/pkg/jsonhttp/jsonhttptest" @@ -170,3 +171,69 @@ func Test_stakingDepositHandler_invalidInputs(t *testing.T) { }) } } + +func TestDeleteStake(t *testing.T) { + t.Parallel() + + txHash := common.HexToHash("0x1234") + + t.Run("ok", func(t *testing.T) { + t.Parallel() + + contract := stakingContractMock.New( + stakingContractMock.WithDeleteStake(func(ctx context.Context) (common.Hash, error) { + return txHash, nil + }), + ) + ts, _, _, _ := newTestServer(t, testServerOptions{DebugAPI: true, StakingContract: contract}) + jsonhttptest.Request(t, ts, http.MethodDelete, "/stake", http.StatusOK) + }) + + t.Run("with invalid stake amount", func(t *testing.T) { + t.Parallel() + + contract := stakingContractMock.New( + stakingContractMock.WithDeleteStake(func(ctx context.Context) (common.Hash, error) { + return common.Hash{}, staking.ErrInsufficientStake + }), + ) + ts, _, _, _ := newTestServer(t, testServerOptions{DebugAPI: true, StakingContract: contract}) + jsonhttptest.Request(t, ts, http.MethodDelete, "/stake", http.StatusBadRequest, + jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusBadRequest, Message: "insufficient stake to delete"})) + }) + + t.Run("internal error", func(t *testing.T) { + t.Parallel() + + contract := stakingContractMock.New( + stakingContractMock.WithDeleteStake(func(ctx context.Context) (common.Hash, error) { + return common.Hash{}, fmt.Errorf("some error") + }), + ) + ts, _, _, _ := newTestServer(t, testServerOptions{DebugAPI: true, StakingContract: contract}) + jsonhttptest.Request(t, ts, http.MethodDelete, "/stake", http.StatusInternalServerError) + jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusInternalServerError, Message: "cannot delete stake"}) + }) + + t.Run("gas limit header", func(t *testing.T) { + t.Parallel() + + contract := stakingContractMock.New( + stakingContractMock.WithDeleteStake(func(ctx context.Context) (common.Hash, error) { + gasLimit := sctx.GetGasLimit(ctx) + if gasLimit != 2000000 { + t.Fatalf("want 2000000, got %d", gasLimit) + } + return txHash, nil + }), + ) + ts, _, _, _ := newTestServer(t, testServerOptions{ + DebugAPI: true, + StakingContract: contract, + }) + + jsonhttptest.Request(t, ts, http.MethodDelete, "/stake", http.StatusOK, + jsonhttptest.WithRequestHeader("Gas-Limit", "2000000"), + ) + }) +} diff --git a/pkg/storageincentives/staking/contract.go b/pkg/storageincentives/staking/contract.go index b12a3bf89bf..319f02be9bc 100644 --- a/pkg/storageincentives/staking/contract.go +++ b/pkg/storageincentives/staking/contract.go @@ -27,15 +27,19 @@ var ( ErrInsufficientStakeAmount = errors.New("insufficient stake amount") ErrInsufficientFunds = errors.New("insufficient token balance") + ErrInsufficientStake = errors.New("insufficient stake") ErrNotImplemented = errors.New("not implemented") + ErrNotPaused = errors.New("contract is not paused") - approveDescription = "Approve tokens for stake deposit operations" - depositStakeDescription = "Deposit Stake" + approveDescription = "Approve tokens for stake deposit operations" + depositStakeDescription = "Deposit Stake" + withdrawStakeDescription = "Withdraw stake" ) type Contract interface { DepositStake(ctx context.Context, stakedAmount *big.Int) (common.Hash, error) GetStake(ctx context.Context) (*big.Int, error) + DeleteStake(ctx context.Context) (common.Hash, error) } type contract struct { @@ -224,3 +228,74 @@ func (c *contract) getBalance(ctx context.Context) (*big.Int, error) { } return abi.ConvertType(results[0], new(big.Int)).(*big.Int), nil } + +func (c *contract) DeleteStake(ctx context.Context) (txHash common.Hash, err error) { + isPaused, err := c.paused(ctx) + if err != nil { + return + } + if !isPaused { + err = ErrNotPaused + return + } + + stakedAmount, err := c.getStake(ctx, c.overlay) + if err != nil { + return + } + + if stakedAmount.Cmp(big.NewInt(0)) <= 0 { + err = ErrInsufficientStake + return + } + + _, err = c.sendApproveTransaction(ctx, stakedAmount) + if err != nil { + return + } + + receipt, err := c.withdrawFromStake(ctx, stakedAmount) + if receipt != nil { + txHash = receipt.TxHash + } + return +} + +func (c *contract) withdrawFromStake(ctx context.Context, stakedAmount *big.Int) (*types.Receipt, error) { + var overlayAddr [32]byte + copy(overlayAddr[:], c.overlay.Bytes()) + + callData, err := c.stakingContractABI.Pack("withdrawFromStake", overlayAddr, stakedAmount) + if err != nil { + return nil, err + } + + receipt, err := c.sendTransaction(ctx, callData, withdrawStakeDescription) + if err != nil { + return nil, fmt.Errorf("withdraw stake: stakedAmount %d: %w", stakedAmount, err) + } + + return receipt, nil +} + +func (c *contract) paused(ctx context.Context) (bool, error) { + callData, err := c.stakingContractABI.Pack("paused") + if err != nil { + return false, err + } + + result, err := c.transactionService.Call(ctx, &transaction.TxRequest{ + To: &c.stakingContractAddress, + Data: callData, + }) + if err != nil { + return false, err + } + + results, err := c.stakingContractABI.Unpack("paused", result) + if err != nil { + return false, err + } + + return results[0].(bool), nil +} diff --git a/pkg/storageincentives/staking/contract_test.go b/pkg/storageincentives/staking/contract_test.go index b924de003d5..cee58405ef0 100644 --- a/pkg/storageincentives/staking/contract_test.go +++ b/pkg/storageincentives/staking/contract_test.go @@ -635,3 +635,405 @@ func TestGetStake(t *testing.T) { } }) } + +func TestDeleteStake(t *testing.T) { + t.Parallel() + + ctx := context.Background() + owner := common.HexToAddress("abcd") + stakingContractAddress := common.HexToAddress("ffff") + bzzTokenAddress := common.HexToAddress("eeee") + nonce := common.BytesToHash(make([]byte, 32)) + stakedAmount := big.NewInt(100000000000000000) + addr := swarm.MustParseHexAddress("f30c0aa7e9e2a0ef4c9b1b750ebfeaeb7c7c24da700bb089da19a46e3677824b") + txHashApprove := common.HexToHash("abb0") + + t.Run("ok", func(t *testing.T) { + t.Parallel() + txHashWithdrawn := common.HexToHash("c3a1") + expected := big.NewInt(1) + + expectedCallDataForPaused, err := stakingContractABI.Pack("paused") + if err != nil { + t.Fatal(err) + } + + expectedCallDataForWithdraw, err := stakingContractABI.Pack("withdrawFromStake", common.BytesToHash(addr.Bytes()), stakedAmount) + if err != nil { + t.Fatal(err) + } + + expectedCallDataForGetStake, err := stakingContractABI.Pack("stakeOfOverlay", common.BytesToHash(addr.Bytes())) + if err != nil { + t.Fatal(err) + } + + contract := staking.New( + addr, + owner, + stakingContractAddress, + stakingContractABI, + bzzTokenAddress, + transactionMock.New( + transactionMock.WithSendFunc(func(ctx context.Context, request *transaction.TxRequest, boost int) (txHash common.Hash, err error) { + if *request.To == bzzTokenAddress { + return txHashApprove, nil + } + if *request.To == stakingContractAddress { + if !bytes.Equal(expectedCallDataForWithdraw[:], request.Data[:]) { + return common.Hash{}, fmt.Errorf("got wrong call data. wanted %x, got %x", expectedCallDataForWithdraw, request.Data) + } + return txHashWithdrawn, nil + } + return common.Hash{}, errors.New("sent to wrong contract") + }), + transactionMock.WithWaitForReceiptFunc(func(ctx context.Context, txHash common.Hash) (receipt *types.Receipt, err error) { + if txHash == txHashApprove { + return &types.Receipt{ + Status: 1, + }, nil + } + if txHash == txHashWithdrawn { + return &types.Receipt{ + Status: 1, + }, nil + } + return nil, errors.New("unknown tx hash") + }), + transactionMock.WithCallFunc(func(ctx context.Context, request *transaction.TxRequest) (result []byte, err error) { + if *request.To == stakingContractAddress { + if bytes.Equal(expectedCallDataForPaused[:], request.Data[:]) { + return expected.FillBytes(make([]byte, 32)), nil + } + if bytes.Equal(expectedCallDataForGetStake[:64], request.Data[:64]) { + return stakedAmount.FillBytes(make([]byte, 32)), nil + } + } + return nil, errors.New("unexpected call") + }), + ), + nonce, + ) + + _, err = contract.DeleteStake(ctx) + if err != nil { + t.Fatal(err) + } + }) + + t.Run("is paused", func(t *testing.T) { + t.Parallel() + expected := big.NewInt(0) + + expectedCallDataForPaused, err := stakingContractABI.Pack("paused") + if err != nil { + t.Fatal(err) + } + + contract := staking.New( + addr, + owner, + stakingContractAddress, + stakingContractABI, + bzzTokenAddress, + transactionMock.New( + transactionMock.WithCallFunc(func(ctx context.Context, request *transaction.TxRequest) (result []byte, err error) { + if *request.To == stakingContractAddress { + if bytes.Equal(expectedCallDataForPaused[:], request.Data[:]) { + return expected.FillBytes(make([]byte, 32)), nil + } + } + return nil, errors.New("unexpected call") + }), + ), + nonce, + ) + + _, err = contract.DeleteStake(ctx) + if !errors.Is(err, staking.ErrNotPaused) { + t.Fatal(err) + } + }) + + t.Run("has no stake", func(t *testing.T) { + t.Parallel() + expected := big.NewInt(1) + + expectedCallDataForPaused, err := stakingContractABI.Pack("paused") + if err != nil { + t.Fatal(err) + } + + invalidStakedAmount := big.NewInt(0) + + expectedCallDataForGetStake, err := stakingContractABI.Pack("stakeOfOverlay", common.BytesToHash(addr.Bytes())) + if err != nil { + t.Fatal(err) + } + + contract := staking.New( + addr, + owner, + stakingContractAddress, + stakingContractABI, + bzzTokenAddress, + transactionMock.New( + transactionMock.WithCallFunc(func(ctx context.Context, request *transaction.TxRequest) (result []byte, err error) { + if *request.To == stakingContractAddress { + if bytes.Equal(expectedCallDataForPaused[:], request.Data[:]) { + return expected.FillBytes(make([]byte, 32)), nil + } + if bytes.Equal(expectedCallDataForGetStake[:64], request.Data[:64]) { + return invalidStakedAmount.FillBytes(make([]byte, 32)), nil + } + } + return nil, errors.New("unexpected call") + }), + ), + nonce, + ) + + _, err = contract.DeleteStake(ctx) + if !errors.Is(err, staking.ErrInsufficientStake) { + t.Fatal(err) + } + }) + + t.Run("invalid call data", func(t *testing.T) { + t.Parallel() + _, err := stakingContractABI.Pack("paused", addr) + if err == nil { + t.Fatal(err) + } + _, err = stakingContractABI.Pack("withdrawFromStake", stakedAmount) + if err == nil { + t.Fatal(err) + } + + _, err = stakingContractABI.Pack("stakeOfOverlay", stakedAmount) + if err == nil { + t.Fatal(err) + } + }) + + t.Run("send tx failed", func(t *testing.T) { + t.Parallel() + txHashWithdrawn := common.HexToHash("c3a1") + expected := big.NewInt(1) + + expectedCallDataForPaused, err := stakingContractABI.Pack("paused") + if err != nil { + t.Fatal(err) + } + + expectedCallDataForWithdraw, err := stakingContractABI.Pack("withdrawFromStake", common.BytesToHash(addr.Bytes()), stakedAmount) + if err != nil { + t.Fatal(err) + } + + expectedCallDataForGetStake, err := stakingContractABI.Pack("stakeOfOverlay", common.BytesToHash(addr.Bytes())) + if err != nil { + t.Fatal(err) + } + + contract := staking.New( + addr, + owner, + stakingContractAddress, + stakingContractABI, + bzzTokenAddress, + transactionMock.New( + transactionMock.WithSendFunc(func(ctx context.Context, request *transaction.TxRequest, boost int) (txHash common.Hash, err error) { + if *request.To == bzzTokenAddress { + return txHashApprove, nil + } + if *request.To == stakingContractAddress { + if !bytes.Equal(expectedCallDataForWithdraw[:], request.Data[:]) { + return common.Hash{}, fmt.Errorf("got wrong call data. wanted %x, got %x", expectedCallDataForWithdraw, request.Data) + } + return common.Hash{}, errors.New("send tx failed") + } + return common.Hash{}, errors.New("sent to wrong contract") + }), + transactionMock.WithWaitForReceiptFunc(func(ctx context.Context, txHash common.Hash) (receipt *types.Receipt, err error) { + if txHash == txHashApprove { + return &types.Receipt{ + Status: 1, + }, nil + } + if txHash == txHashWithdrawn { + return &types.Receipt{ + Status: 1, + }, nil + } + return nil, errors.New("unknown tx hash") + }), + transactionMock.WithCallFunc(func(ctx context.Context, request *transaction.TxRequest) (result []byte, err error) { + if *request.To == stakingContractAddress { + if bytes.Equal(expectedCallDataForPaused[:], request.Data[:]) { + return expected.FillBytes(make([]byte, 32)), nil + } + if bytes.Equal(expectedCallDataForGetStake[:64], request.Data[:64]) { + return stakedAmount.FillBytes(make([]byte, 32)), nil + } + } + return nil, errors.New("unexpected call") + }), + ), + nonce, + ) + + _, err = contract.DeleteStake(ctx) + if err == nil { + t.Fatal(err) + } + }) + + t.Run("tx reverted", func(t *testing.T) { + t.Parallel() + txHashWithdrawn := common.HexToHash("c3a1") + expected := big.NewInt(1) + + expectedCallDataForPaused, err := stakingContractABI.Pack("paused") + if err != nil { + t.Fatal(err) + } + + expectedCallDataForGetStake, err := stakingContractABI.Pack("stakeOfOverlay", common.BytesToHash(addr.Bytes())) + if err != nil { + t.Fatal(err) + } + + expectedCallDataForWithdraw, err := stakingContractABI.Pack("withdrawFromStake", common.BytesToHash(addr.Bytes()), stakedAmount) + if err != nil { + t.Fatal(err) + } + + contract := staking.New( + addr, + owner, + stakingContractAddress, + stakingContractABI, + bzzTokenAddress, + transactionMock.New( + transactionMock.WithSendFunc(func(ctx context.Context, request *transaction.TxRequest, boost int) (txHash common.Hash, err error) { + if *request.To == bzzTokenAddress { + return txHashApprove, nil + } + if *request.To == stakingContractAddress { + if !bytes.Equal(expectedCallDataForWithdraw[:], request.Data[:]) { + return common.Hash{}, fmt.Errorf("got wrong call data. wanted %x, got %x", expectedCallDataForWithdraw, request.Data) + } + return txHashWithdrawn, nil + } + return common.Hash{}, errors.New("sent to wrong contract") + }), + transactionMock.WithWaitForReceiptFunc(func(ctx context.Context, txHash common.Hash) (receipt *types.Receipt, err error) { + if txHash == txHashApprove { + return &types.Receipt{ + Status: 1, + }, nil + } + if txHash == txHashWithdrawn { + return &types.Receipt{ + Status: 0, + }, nil + } + return nil, errors.New("unknown tx hash") + }), + transactionMock.WithCallFunc(func(ctx context.Context, request *transaction.TxRequest) (result []byte, err error) { + if *request.To == stakingContractAddress { + if bytes.Equal(expectedCallDataForPaused[:], request.Data[:]) { + return expected.FillBytes(make([]byte, 32)), nil + } + if bytes.Equal(expectedCallDataForGetStake[:64], request.Data[:64]) { + return stakedAmount.FillBytes(make([]byte, 32)), nil + } + } + return nil, errors.New("unexpected call") + }), + ), + nonce, + ) + + _, err = contract.DeleteStake(ctx) + fmt.Println(err) + if err == nil { + t.Fatal(err) + } + }) + + t.Run("is paused with err", func(t *testing.T) { + t.Parallel() + expectedCallDataForPaused, err := stakingContractABI.Pack("paused") + if err != nil { + t.Fatal(err) + } + + contract := staking.New( + addr, + owner, + stakingContractAddress, + stakingContractABI, + bzzTokenAddress, + transactionMock.New( + transactionMock.WithCallFunc(func(ctx context.Context, request *transaction.TxRequest) (result []byte, err error) { + if *request.To == stakingContractAddress { + if bytes.Equal(expectedCallDataForPaused[:], request.Data[:]) { + return nil, fmt.Errorf("some error") + } + } + return nil, errors.New("unexpected call") + }), + ), + nonce, + ) + + _, err = contract.DeleteStake(ctx) + if err == nil { + t.Fatal(err) + } + }) + + t.Run("get stake with err", func(t *testing.T) { + t.Parallel() + expected := big.NewInt(1) + + expectedCallDataForPaused, err := stakingContractABI.Pack("paused") + if err != nil { + t.Fatal(err) + } + + expectedCallDataForGetStake, err := stakingContractABI.Pack("stakeOfOverlay", common.BytesToHash(addr.Bytes())) + if err != nil { + t.Fatal(err) + } + + contract := staking.New( + addr, + owner, + stakingContractAddress, + stakingContractABI, + bzzTokenAddress, + transactionMock.New( + transactionMock.WithCallFunc(func(ctx context.Context, request *transaction.TxRequest) (result []byte, err error) { + if *request.To == stakingContractAddress { + if bytes.Equal(expectedCallDataForPaused[:], request.Data[:]) { + return expected.FillBytes(make([]byte, 32)), nil + } + if bytes.Equal(expectedCallDataForGetStake[:64], request.Data[:64]) { + return nil, fmt.Errorf("some error") + } + } + return nil, errors.New("unexpected call") + }), + ), + nonce, + ) + + _, err = contract.DeleteStake(ctx) + if err == nil { + t.Fatal(err) + } + }) +} diff --git a/pkg/storageincentives/staking/mock/contract.go b/pkg/storageincentives/staking/mock/contract.go index eaf8730930a..c9ba2cd8249 100644 --- a/pkg/storageincentives/staking/mock/contract.go +++ b/pkg/storageincentives/staking/mock/contract.go @@ -6,15 +6,17 @@ package mock import ( "context" - "github.com/ethereum/go-ethereum/common" "math/big" + "github.com/ethereum/go-ethereum/common" + "github.com/ethersphere/bee/pkg/storageincentives/staking" ) type stakingContractMock struct { depositStake func(ctx context.Context, stakedAmount *big.Int) (common.Hash, error) getStake func(ctx context.Context) (*big.Int, error) + deleteStake func(ctx context.Context) (common.Hash, error) } func (s *stakingContractMock) DepositStake(ctx context.Context, stakedAmount *big.Int) (common.Hash, error) { @@ -25,6 +27,10 @@ func (s *stakingContractMock) GetStake(ctx context.Context) (*big.Int, error) { return s.getStake(ctx) } +func (s *stakingContractMock) DeleteStake(ctx context.Context) (common.Hash, error) { + return s.deleteStake(ctx) +} + // Option is a an option passed to New type Option func(mock *stakingContractMock) @@ -50,3 +56,9 @@ func WithGetStake(f func(ctx context.Context) (*big.Int, error)) Option { mock.getStake = f } } + +func WithDeleteStake(f func(ctx context.Context) (common.Hash, error)) Option { + return func(mock *stakingContractMock) { + mock.deleteStake = f + } +} From 6a5ce2a9b9739014ff6d8e2432068635ff758a31 Mon Sep 17 00:00:00 2001 From: swarmHaseeb Date: Fri, 16 Dec 2022 18:58:37 +0500 Subject: [PATCH 2/8] refactor: enable deleting stake --- openapi/SwarmCommon.yaml | 12 ++++++++++++ openapi/SwarmDebug.yaml | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/openapi/SwarmCommon.yaml b/openapi/SwarmCommon.yaml index f0b4b8ed719..b6dd107b73d 100644 --- a/openapi/SwarmCommon.yaml +++ b/openapi/SwarmCommon.yaml @@ -586,6 +586,18 @@ components: stakedAmount: $ref: "#/components/schemas/BigInt" + StakeDepositResponse: + type: object + properties: + txHash: + $ref: "#/components/schemas/TransactionHash" + + DeleteStakeResponse: + type: object + properties: + txHash: + $ref: "#/components/schemas/TransactionHash" + SwarmOnlyReference: oneOf: - $ref: "#/components/schemas/SwarmAddress" diff --git a/openapi/SwarmDebug.yaml b/openapi/SwarmDebug.yaml index 907659b3f99..2ee36dffcdb 100644 --- a/openapi/SwarmDebug.yaml +++ b/openapi/SwarmDebug.yaml @@ -1057,6 +1057,24 @@ paths: default: description: Default response + delete: + summary: Delete all staked amount. + description: Be aware, this endpoint creates an on-chain transactions and transfers BZZ from the node's Ethereum account and hence directly manipulates the wallet balance. + tags: + - Staking + parameters: + - $ref: "SwarmCommon.yaml#/components/parameters/GasPriceParameter" + - $ref: "SwarmCommon.yaml#/components/parameters/GasLimitParameter" + responses: + "200": + $ref: "SwarmCommon.yaml#/components/schemas/DeleteStakeResponse" + "400": + $ref: "SwarmCommon.yaml#/components/responses/400" + "500": + $ref: "SwarmCommon.yaml#/components/responses/500" + default: + description: Default response + "/loggers": get: summary: Get all available loggers. From c9c806697b2798e42462d761cd8bcd64bd942eac Mon Sep 17 00:00:00 2001 From: swarmHaseeb Date: Fri, 16 Dec 2022 19:00:51 +0500 Subject: [PATCH 3/8] refactor: enable deleting stake --- openapi/SwarmDebug.yaml | 1 - pkg/storageincentives/staking/contract_test.go | 1 - 2 files changed, 2 deletions(-) diff --git a/openapi/SwarmDebug.yaml b/openapi/SwarmDebug.yaml index 2ee36dffcdb..3c3d163810f 100644 --- a/openapi/SwarmDebug.yaml +++ b/openapi/SwarmDebug.yaml @@ -1056,7 +1056,6 @@ paths: $ref: "SwarmCommon.yaml#/components/responses/500" default: description: Default response - delete: summary: Delete all staked amount. description: Be aware, this endpoint creates an on-chain transactions and transfers BZZ from the node's Ethereum account and hence directly manipulates the wallet balance. diff --git a/pkg/storageincentives/staking/contract_test.go b/pkg/storageincentives/staking/contract_test.go index cee58405ef0..5c6b0d680c9 100644 --- a/pkg/storageincentives/staking/contract_test.go +++ b/pkg/storageincentives/staking/contract_test.go @@ -957,7 +957,6 @@ func TestDeleteStake(t *testing.T) { ) _, err = contract.DeleteStake(ctx) - fmt.Println(err) if err == nil { t.Fatal(err) } From 3b20b24416ecfcb58e509a7e20a9715fe272bc3f Mon Sep 17 00:00:00 2001 From: swarmHaseeb Date: Fri, 16 Dec 2022 19:07:39 +0500 Subject: [PATCH 4/8] refactor: naming convention --- openapi/SwarmCommon.yaml | 2 +- openapi/SwarmDebug.yaml | 2 +- pkg/api/router.go | 2 +- pkg/api/staking.go | 19 ++++++++++--------- pkg/api/staking_test.go | 12 ++++++------ pkg/storageincentives/staking/contract.go | 4 ++-- .../staking/contract_test.go | 14 +++++++------- .../staking/mock/contract.go | 14 +++++++------- 8 files changed, 35 insertions(+), 34 deletions(-) diff --git a/openapi/SwarmCommon.yaml b/openapi/SwarmCommon.yaml index b6dd107b73d..8bb49e61777 100644 --- a/openapi/SwarmCommon.yaml +++ b/openapi/SwarmCommon.yaml @@ -592,7 +592,7 @@ components: txHash: $ref: "#/components/schemas/TransactionHash" - DeleteStakeResponse: + WithdrawAllStakeResponse: type: object properties: txHash: diff --git a/openapi/SwarmDebug.yaml b/openapi/SwarmDebug.yaml index 3c3d163810f..fc2b2c6c1d1 100644 --- a/openapi/SwarmDebug.yaml +++ b/openapi/SwarmDebug.yaml @@ -1066,7 +1066,7 @@ paths: - $ref: "SwarmCommon.yaml#/components/parameters/GasLimitParameter" responses: "200": - $ref: "SwarmCommon.yaml#/components/schemas/DeleteStakeResponse" + $ref: "SwarmCommon.yaml#/components/schemas/WithdrawAllStakeResponse" "400": $ref: "SwarmCommon.yaml#/components/responses/400" "500": diff --git a/pkg/api/router.go b/pkg/api/router.go index f7085f8070f..a61dcc255ea 100644 --- a/pkg/api/router.go +++ b/pkg/api/router.go @@ -565,7 +565,7 @@ func (s *Service) mountBusinessDebug(restricted bool) { s.gasConfigMiddleware("get or delete stake"), web.FinalHandler(jsonhttp.MethodHandler{ "GET": http.HandlerFunc(s.getStakedAmountHandler), - "DELETE": http.HandlerFunc(s.deleteStakeHandler), + "DELETE": http.HandlerFunc(s.withdrawAllStakeHandler), })), ) } diff --git a/pkg/api/staking.go b/pkg/api/staking.go index 37862f36f6f..9bae2a1f345 100644 --- a/pkg/api/staking.go +++ b/pkg/api/staking.go @@ -6,10 +6,11 @@ package api import ( "errors" - "github.com/ethersphere/bee/pkg/bigint" "math/big" "net/http" + "github.com/ethersphere/bee/pkg/bigint" + "github.com/ethersphere/bee/pkg/jsonhttp" "github.com/ethersphere/bee/pkg/storageincentives/staking" "github.com/gorilla/mux" @@ -36,7 +37,7 @@ type stakeDepositResponse struct { TxHash string `json:"txhash"` } -type deleteStakeResponse struct { +type withdrawAllStakeResponse struct { TxHash string `json:"txhash"` } @@ -95,22 +96,22 @@ func (s *Service) getStakedAmountHandler(w http.ResponseWriter, r *http.Request) jsonhttp.OK(w, getStakeResponse{StakedAmount: bigint.Wrap(stakedAmount)}) } -func (s *Service) deleteStakeHandler(w http.ResponseWriter, r *http.Request) { - logger := s.logger.WithName("delete_stake").Build() +func (s *Service) withdrawAllStakeHandler(w http.ResponseWriter, r *http.Request) { + logger := s.logger.WithName("withdraw_all_stake").Build() - txHash, err := s.stakingContract.DeleteStake(r.Context()) + txHash, err := s.stakingContract.WithdrawAllStake(r.Context()) if err != nil { if errors.Is(err, staking.ErrInsufficientStake) { logger.Debug("insufficient stake", "overlayAddr", s.overlay, "error", err) logger.Error(nil, "insufficient stake") - jsonhttp.BadRequest(w, "insufficient stake to delete") + jsonhttp.BadRequest(w, "insufficient stake to withdraw") return } logger.Debug("delete stake failed", "error", err) - logger.Error(nil, "delete stake failed") - jsonhttp.InternalServerError(w, "cannot delete stake") + logger.Error(nil, "withdraw stake failed") + jsonhttp.InternalServerError(w, "cannot withdraw stake") return } - jsonhttp.OK(w, deleteStakeResponse{TxHash: txHash.String()}) + jsonhttp.OK(w, withdrawAllStakeResponse{TxHash: txHash.String()}) } diff --git a/pkg/api/staking_test.go b/pkg/api/staking_test.go index a9c7d3aff25..3e3836b5270 100644 --- a/pkg/api/staking_test.go +++ b/pkg/api/staking_test.go @@ -172,7 +172,7 @@ func Test_stakingDepositHandler_invalidInputs(t *testing.T) { } } -func TestDeleteStake(t *testing.T) { +func TestWithdrawAllStake(t *testing.T) { t.Parallel() txHash := common.HexToHash("0x1234") @@ -181,7 +181,7 @@ func TestDeleteStake(t *testing.T) { t.Parallel() contract := stakingContractMock.New( - stakingContractMock.WithDeleteStake(func(ctx context.Context) (common.Hash, error) { + stakingContractMock.WithWithdrawAllStake(func(ctx context.Context) (common.Hash, error) { return txHash, nil }), ) @@ -193,20 +193,20 @@ func TestDeleteStake(t *testing.T) { t.Parallel() contract := stakingContractMock.New( - stakingContractMock.WithDeleteStake(func(ctx context.Context) (common.Hash, error) { + stakingContractMock.WithWithdrawAllStake(func(ctx context.Context) (common.Hash, error) { return common.Hash{}, staking.ErrInsufficientStake }), ) ts, _, _, _ := newTestServer(t, testServerOptions{DebugAPI: true, StakingContract: contract}) jsonhttptest.Request(t, ts, http.MethodDelete, "/stake", http.StatusBadRequest, - jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusBadRequest, Message: "insufficient stake to delete"})) + jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusBadRequest, Message: "insufficient stake to withdraw"})) }) t.Run("internal error", func(t *testing.T) { t.Parallel() contract := stakingContractMock.New( - stakingContractMock.WithDeleteStake(func(ctx context.Context) (common.Hash, error) { + stakingContractMock.WithWithdrawAllStake(func(ctx context.Context) (common.Hash, error) { return common.Hash{}, fmt.Errorf("some error") }), ) @@ -219,7 +219,7 @@ func TestDeleteStake(t *testing.T) { t.Parallel() contract := stakingContractMock.New( - stakingContractMock.WithDeleteStake(func(ctx context.Context) (common.Hash, error) { + stakingContractMock.WithWithdrawAllStake(func(ctx context.Context) (common.Hash, error) { gasLimit := sctx.GetGasLimit(ctx) if gasLimit != 2000000 { t.Fatalf("want 2000000, got %d", gasLimit) diff --git a/pkg/storageincentives/staking/contract.go b/pkg/storageincentives/staking/contract.go index 319f02be9bc..cce2838e868 100644 --- a/pkg/storageincentives/staking/contract.go +++ b/pkg/storageincentives/staking/contract.go @@ -39,7 +39,7 @@ var ( type Contract interface { DepositStake(ctx context.Context, stakedAmount *big.Int) (common.Hash, error) GetStake(ctx context.Context) (*big.Int, error) - DeleteStake(ctx context.Context) (common.Hash, error) + WithdrawAllStake(ctx context.Context) (common.Hash, error) } type contract struct { @@ -229,7 +229,7 @@ func (c *contract) getBalance(ctx context.Context) (*big.Int, error) { return abi.ConvertType(results[0], new(big.Int)).(*big.Int), nil } -func (c *contract) DeleteStake(ctx context.Context) (txHash common.Hash, err error) { +func (c *contract) WithdrawAllStake(ctx context.Context) (txHash common.Hash, err error) { isPaused, err := c.paused(ctx) if err != nil { return diff --git a/pkg/storageincentives/staking/contract_test.go b/pkg/storageincentives/staking/contract_test.go index 5c6b0d680c9..f6645e1875e 100644 --- a/pkg/storageincentives/staking/contract_test.go +++ b/pkg/storageincentives/staking/contract_test.go @@ -715,7 +715,7 @@ func TestDeleteStake(t *testing.T) { nonce, ) - _, err = contract.DeleteStake(ctx) + _, err = contract.WithdrawAllStake(ctx) if err != nil { t.Fatal(err) } @@ -749,7 +749,7 @@ func TestDeleteStake(t *testing.T) { nonce, ) - _, err = contract.DeleteStake(ctx) + _, err = contract.WithdrawAllStake(ctx) if !errors.Is(err, staking.ErrNotPaused) { t.Fatal(err) } @@ -793,7 +793,7 @@ func TestDeleteStake(t *testing.T) { nonce, ) - _, err = contract.DeleteStake(ctx) + _, err = contract.WithdrawAllStake(ctx) if !errors.Is(err, staking.ErrInsufficientStake) { t.Fatal(err) } @@ -883,7 +883,7 @@ func TestDeleteStake(t *testing.T) { nonce, ) - _, err = contract.DeleteStake(ctx) + _, err = contract.WithdrawAllStake(ctx) if err == nil { t.Fatal(err) } @@ -956,7 +956,7 @@ func TestDeleteStake(t *testing.T) { nonce, ) - _, err = contract.DeleteStake(ctx) + _, err = contract.WithdrawAllStake(ctx) if err == nil { t.Fatal(err) } @@ -988,7 +988,7 @@ func TestDeleteStake(t *testing.T) { nonce, ) - _, err = contract.DeleteStake(ctx) + _, err = contract.WithdrawAllStake(ctx) if err == nil { t.Fatal(err) } @@ -1030,7 +1030,7 @@ func TestDeleteStake(t *testing.T) { nonce, ) - _, err = contract.DeleteStake(ctx) + _, err = contract.WithdrawAllStake(ctx) if err == nil { t.Fatal(err) } diff --git a/pkg/storageincentives/staking/mock/contract.go b/pkg/storageincentives/staking/mock/contract.go index c9ba2cd8249..58e78b9ca65 100644 --- a/pkg/storageincentives/staking/mock/contract.go +++ b/pkg/storageincentives/staking/mock/contract.go @@ -14,9 +14,9 @@ import ( ) type stakingContractMock struct { - depositStake func(ctx context.Context, stakedAmount *big.Int) (common.Hash, error) - getStake func(ctx context.Context) (*big.Int, error) - deleteStake func(ctx context.Context) (common.Hash, error) + depositStake func(ctx context.Context, stakedAmount *big.Int) (common.Hash, error) + getStake func(ctx context.Context) (*big.Int, error) + withdrawAllStake func(ctx context.Context) (common.Hash, error) } func (s *stakingContractMock) DepositStake(ctx context.Context, stakedAmount *big.Int) (common.Hash, error) { @@ -27,8 +27,8 @@ func (s *stakingContractMock) GetStake(ctx context.Context) (*big.Int, error) { return s.getStake(ctx) } -func (s *stakingContractMock) DeleteStake(ctx context.Context) (common.Hash, error) { - return s.deleteStake(ctx) +func (s *stakingContractMock) WithdrawAllStake(ctx context.Context) (common.Hash, error) { + return s.withdrawAllStake(ctx) } // Option is a an option passed to New @@ -57,8 +57,8 @@ func WithGetStake(f func(ctx context.Context) (*big.Int, error)) Option { } } -func WithDeleteStake(f func(ctx context.Context) (common.Hash, error)) Option { +func WithWithdrawAllStake(f func(ctx context.Context) (common.Hash, error)) Option { return func(mock *stakingContractMock) { - mock.deleteStake = f + mock.withdrawAllStake = f } } From 4c93952968846f59a28ff1b6f1491158865c2d5c Mon Sep 17 00:00:00 2001 From: swarmHaseeb Date: Fri, 16 Dec 2022 19:08:07 +0500 Subject: [PATCH 5/8] refactor: naming convention --- openapi/SwarmDebug.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi/SwarmDebug.yaml b/openapi/SwarmDebug.yaml index fc2b2c6c1d1..38ff38587f4 100644 --- a/openapi/SwarmDebug.yaml +++ b/openapi/SwarmDebug.yaml @@ -1057,7 +1057,7 @@ paths: default: description: Default response delete: - summary: Delete all staked amount. + summary: Withdraw all staked amount. description: Be aware, this endpoint creates an on-chain transactions and transfers BZZ from the node's Ethereum account and hence directly manipulates the wallet balance. tags: - Staking From 01aa48b7b2448365a3df753430cc211617a10f8e Mon Sep 17 00:00:00 2001 From: swarmHaseeb Date: Fri, 16 Dec 2022 19:10:54 +0500 Subject: [PATCH 6/8] refactor: naming convention --- pkg/api/router.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/router.go b/pkg/api/router.go index a61dcc255ea..e7574f54fb3 100644 --- a/pkg/api/router.go +++ b/pkg/api/router.go @@ -562,7 +562,7 @@ func (s *Service) mountBusinessDebug(restricted bool) { handle("/stake", web.ChainHandlers( s.stakingAccessHandler, - s.gasConfigMiddleware("get or delete stake"), + s.gasConfigMiddleware("get or withdraw stake"), web.FinalHandler(jsonhttp.MethodHandler{ "GET": http.HandlerFunc(s.getStakedAmountHandler), "DELETE": http.HandlerFunc(s.withdrawAllStakeHandler), From e7e54e5d90081757499a757f39b11e950122ef60 Mon Sep 17 00:00:00 2001 From: swarmHaseeb Date: Fri, 16 Dec 2022 19:56:34 +0500 Subject: [PATCH 7/8] refactor: naming convention --- pkg/api/staking.go | 2 +- pkg/api/staking_test.go | 2 +- pkg/storageincentives/staking/contract_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/api/staking.go b/pkg/api/staking.go index 9bae2a1f345..8252416f2a8 100644 --- a/pkg/api/staking.go +++ b/pkg/api/staking.go @@ -107,7 +107,7 @@ func (s *Service) withdrawAllStakeHandler(w http.ResponseWriter, r *http.Request jsonhttp.BadRequest(w, "insufficient stake to withdraw") return } - logger.Debug("delete stake failed", "error", err) + logger.Debug("withdraw stake failed", "error", err) logger.Error(nil, "withdraw stake failed") jsonhttp.InternalServerError(w, "cannot withdraw stake") return diff --git a/pkg/api/staking_test.go b/pkg/api/staking_test.go index 3e3836b5270..eb4a39dfead 100644 --- a/pkg/api/staking_test.go +++ b/pkg/api/staking_test.go @@ -212,7 +212,7 @@ func TestWithdrawAllStake(t *testing.T) { ) ts, _, _, _ := newTestServer(t, testServerOptions{DebugAPI: true, StakingContract: contract}) jsonhttptest.Request(t, ts, http.MethodDelete, "/stake", http.StatusInternalServerError) - jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusInternalServerError, Message: "cannot delete stake"}) + jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusInternalServerError, Message: "cannot withdraw stake"}) }) t.Run("gas limit header", func(t *testing.T) { diff --git a/pkg/storageincentives/staking/contract_test.go b/pkg/storageincentives/staking/contract_test.go index f6645e1875e..96d5986040e 100644 --- a/pkg/storageincentives/staking/contract_test.go +++ b/pkg/storageincentives/staking/contract_test.go @@ -636,7 +636,7 @@ func TestGetStake(t *testing.T) { }) } -func TestDeleteStake(t *testing.T) { +func TestWithdrawStake(t *testing.T) { t.Parallel() ctx := context.Background() From 1b4b77b02b6728ec1702e16ae2858051d9866620 Mon Sep 17 00:00:00 2001 From: swarmHaseeb Date: Fri, 16 Dec 2022 21:09:52 +0500 Subject: [PATCH 8/8] refactor: minor changes and bug fixes --- pkg/api/export_test.go | 1 + pkg/api/staking.go | 2 +- pkg/api/staking_test.go | 3 +- pkg/storageincentives/staking/contract.go | 48 +++++++++++++++-------- 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/pkg/api/export_test.go b/pkg/api/export_test.go index f5d6fe16ec0..75fba7c4e24 100644 --- a/pkg/api/export_test.go +++ b/pkg/api/export_test.go @@ -100,6 +100,7 @@ type ( BucketData = bucketData WalletResponse = walletResponse GetStakeResponse = getStakeResponse + WithdrawAllStakeResponse = withdrawAllStakeResponse ) var ( diff --git a/pkg/api/staking.go b/pkg/api/staking.go index 8252416f2a8..77e8a7e836c 100644 --- a/pkg/api/staking.go +++ b/pkg/api/staking.go @@ -97,7 +97,7 @@ func (s *Service) getStakedAmountHandler(w http.ResponseWriter, r *http.Request) } func (s *Service) withdrawAllStakeHandler(w http.ResponseWriter, r *http.Request) { - logger := s.logger.WithName("withdraw_all_stake").Build() + logger := s.logger.WithName("delete_withdraw_all_stake").Build() txHash, err := s.stakingContract.WithdrawAllStake(r.Context()) if err != nil { diff --git a/pkg/api/staking_test.go b/pkg/api/staking_test.go index eb4a39dfead..2ca045cbcaa 100644 --- a/pkg/api/staking_test.go +++ b/pkg/api/staking_test.go @@ -186,7 +186,8 @@ func TestWithdrawAllStake(t *testing.T) { }), ) ts, _, _, _ := newTestServer(t, testServerOptions{DebugAPI: true, StakingContract: contract}) - jsonhttptest.Request(t, ts, http.MethodDelete, "/stake", http.StatusOK) + jsonhttptest.Request(t, ts, http.MethodDelete, "/stake", http.StatusOK, jsonhttptest.WithExpectedJSONResponse( + &api.WithdrawAllStakeResponse{TxHash: txHash.String()})) }) t.Run("with invalid stake amount", func(t *testing.T) { diff --git a/pkg/storageincentives/staking/contract.go b/pkg/storageincentives/staking/contract.go index cce2838e868..fcf99935277 100644 --- a/pkg/storageincentives/staking/contract.go +++ b/pkg/storageincentives/staking/contract.go @@ -162,42 +162,46 @@ func (c *contract) getStake(ctx context.Context, overlay swarm.Address) (*big.In if err != nil { return nil, err } + + if len(results) == 0 { + return nil, errors.New("unexpected empty results") + } + return abi.ConvertType(results[0], new(big.Int)).(*big.Int), nil } -func (c *contract) DepositStake(ctx context.Context, stakedAmount *big.Int) (txHash common.Hash, err error) { +func (c *contract) DepositStake(ctx context.Context, stakedAmount *big.Int) (common.Hash, error) { prevStakedAmount, err := c.GetStake(ctx) if err != nil { - return + return common.Hash{}, err } if len(prevStakedAmount.Bits()) == 0 { if stakedAmount.Cmp(MinimumStakeAmount) == -1 { - err = ErrInsufficientStakeAmount - return + return common.Hash{}, ErrInsufficientStakeAmount } } balance, err := c.getBalance(ctx) if err != nil { - return + return common.Hash{}, err } if balance.Cmp(stakedAmount) < 0 { - err = ErrInsufficientFunds - return + return common.Hash{}, ErrInsufficientFunds } _, err = c.sendApproveTransaction(ctx, stakedAmount) if err != nil { - return + return common.Hash{}, err } receipt, err := c.sendDepositStakeTransaction(ctx, c.owner, stakedAmount, c.overlayNonce) - if receipt != nil { - txHash = receipt.TxHash + if err != nil { + return common.Hash{}, err } - return + + return receipt.TxHash, nil } func (c *contract) GetStake(ctx context.Context) (*big.Int, error) { @@ -226,6 +230,11 @@ func (c *contract) getBalance(ctx context.Context) (*big.Int, error) { if err != nil { return nil, err } + + if len(results) == 0 { + return nil, errors.New("unexpected empty results") + } + return abi.ConvertType(results[0], new(big.Int)).(*big.Int), nil } @@ -235,8 +244,7 @@ func (c *contract) WithdrawAllStake(ctx context.Context) (txHash common.Hash, er return } if !isPaused { - err = ErrNotPaused - return + return common.Hash{}, ErrNotPaused } stakedAmount, err := c.getStake(ctx, c.overlay) @@ -245,20 +253,22 @@ func (c *contract) WithdrawAllStake(ctx context.Context) (txHash common.Hash, er } if stakedAmount.Cmp(big.NewInt(0)) <= 0 { - err = ErrInsufficientStake - return + return common.Hash{}, ErrInsufficientStake } _, err = c.sendApproveTransaction(ctx, stakedAmount) if err != nil { - return + return common.Hash{}, err } receipt, err := c.withdrawFromStake(ctx, stakedAmount) + if err != nil { + return common.Hash{}, err + } if receipt != nil { txHash = receipt.TxHash } - return + return txHash, nil } func (c *contract) withdrawFromStake(ctx context.Context, stakedAmount *big.Int) (*types.Receipt, error) { @@ -297,5 +307,9 @@ func (c *contract) paused(ctx context.Context) (bool, error) { return false, err } + if len(results) == 0 { + return false, errors.New("unexpected empty results") + } + return results[0].(bool), nil }