github.com/ethersphere/bee/v2@v2.2.0/pkg/api/staking_test.go (about)

     1  // Copyright 2022 The Swarm Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package api_test
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"math/big"
    11  	"net/http"
    12  	"testing"
    13  
    14  	"github.com/ethereum/go-ethereum/common"
    15  	"github.com/ethersphere/bee/v2/pkg/bigint"
    16  
    17  	"github.com/ethersphere/bee/v2/pkg/api"
    18  	"github.com/ethersphere/bee/v2/pkg/jsonhttp"
    19  	"github.com/ethersphere/bee/v2/pkg/jsonhttp/jsonhttptest"
    20  	"github.com/ethersphere/bee/v2/pkg/sctx"
    21  	"github.com/ethersphere/bee/v2/pkg/storageincentives/staking"
    22  	stakingContractMock "github.com/ethersphere/bee/v2/pkg/storageincentives/staking/mock"
    23  )
    24  
    25  func TestDepositStake(t *testing.T) {
    26  	t.Parallel()
    27  
    28  	txHash := common.HexToHash("0x1234")
    29  	minStake := big.NewInt(100000000000000000).String()
    30  	depositStake := func(amount string) string {
    31  		return fmt.Sprintf("/stake/%s", amount)
    32  	}
    33  
    34  	t.Run("ok", func(t *testing.T) {
    35  		t.Parallel()
    36  
    37  		contract := stakingContractMock.New(
    38  			stakingContractMock.WithDepositStake(func(ctx context.Context, stakedAmount *big.Int) (common.Hash, error) {
    39  				return txHash, nil
    40  			}),
    41  		)
    42  		ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract})
    43  		jsonhttptest.Request(t, ts, http.MethodPost, depositStake(minStake), http.StatusOK)
    44  	})
    45  
    46  	t.Run("with invalid stake amount", func(t *testing.T) {
    47  		t.Parallel()
    48  
    49  		invalidMinStake := big.NewInt(0).String()
    50  		contract := stakingContractMock.New(
    51  			stakingContractMock.WithDepositStake(func(ctx context.Context, stakedAmount *big.Int) (common.Hash, error) {
    52  				return common.Hash{}, staking.ErrInsufficientStakeAmount
    53  			}),
    54  		)
    55  		ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract})
    56  		jsonhttptest.Request(t, ts, http.MethodPost, depositStake(invalidMinStake), http.StatusBadRequest,
    57  			jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusBadRequest, Message: "insufficient stake amount"}))
    58  	})
    59  
    60  	t.Run("out of funds", func(t *testing.T) {
    61  		t.Parallel()
    62  
    63  		contract := stakingContractMock.New(
    64  			stakingContractMock.WithDepositStake(func(ctx context.Context, stakedAmount *big.Int) (common.Hash, error) {
    65  				return common.Hash{}, staking.ErrInsufficientFunds
    66  			}),
    67  		)
    68  		ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract})
    69  		jsonhttptest.Request(t, ts, http.MethodPost, depositStake(minStake), http.StatusBadRequest)
    70  		jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusBadRequest, Message: "out of funds"})
    71  	})
    72  
    73  	t.Run("internal error", func(t *testing.T) {
    74  		t.Parallel()
    75  
    76  		contract := stakingContractMock.New(
    77  			stakingContractMock.WithDepositStake(func(ctx context.Context, stakedAmount *big.Int) (common.Hash, error) {
    78  				return common.Hash{}, fmt.Errorf("some error")
    79  			}),
    80  		)
    81  		ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract})
    82  		jsonhttptest.Request(t, ts, http.MethodPost, depositStake(minStake), http.StatusInternalServerError)
    83  		jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusInternalServerError, Message: "cannot stake"})
    84  	})
    85  
    86  	t.Run("gas limit header", func(t *testing.T) {
    87  		t.Parallel()
    88  
    89  		contract := stakingContractMock.New(
    90  			stakingContractMock.WithDepositStake(func(ctx context.Context, stakedAmount *big.Int) (common.Hash, error) {
    91  				gasLimit := sctx.GetGasLimit(ctx)
    92  				if gasLimit != 2000000 {
    93  					t.Fatalf("want 2000000, got %d", gasLimit)
    94  				}
    95  				return txHash, nil
    96  			}),
    97  		)
    98  		ts, _, _, _ := newTestServer(t, testServerOptions{
    99  			StakingContract: contract,
   100  		})
   101  
   102  		jsonhttptest.Request(t, ts, http.MethodPost, depositStake(minStake), http.StatusOK,
   103  			jsonhttptest.WithRequestHeader(api.GasLimitHeader, "2000000"),
   104  		)
   105  	})
   106  }
   107  
   108  func TestGetStakeCommitted(t *testing.T) {
   109  	t.Parallel()
   110  
   111  	t.Run("ok", func(t *testing.T) {
   112  		t.Parallel()
   113  
   114  		contract := stakingContractMock.New(
   115  			stakingContractMock.WithGetStake(func(ctx context.Context) (*big.Int, error) {
   116  				return big.NewInt(1), nil
   117  			}),
   118  		)
   119  		ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract})
   120  		jsonhttptest.Request(t, ts, http.MethodGet, "/stake", http.StatusOK,
   121  			jsonhttptest.WithExpectedJSONResponse(&api.GetStakeResponse{StakedAmount: bigint.Wrap(big.NewInt(1))}))
   122  	})
   123  
   124  	t.Run("with error", func(t *testing.T) {
   125  		t.Parallel()
   126  
   127  		contractWithError := stakingContractMock.New(
   128  			stakingContractMock.WithGetStake(func(ctx context.Context) (*big.Int, error) {
   129  				return big.NewInt(0), fmt.Errorf("get stake failed")
   130  			}),
   131  		)
   132  		ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contractWithError})
   133  		jsonhttptest.Request(t, ts, http.MethodGet, "/stake", http.StatusInternalServerError,
   134  			jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusInternalServerError, Message: "get staked amount failed"}))
   135  	})
   136  }
   137  
   138  func TestGetStakeWithdrawable(t *testing.T) {
   139  	t.Parallel()
   140  
   141  	t.Run("ok", func(t *testing.T) {
   142  		t.Parallel()
   143  
   144  		contract := stakingContractMock.New(
   145  			stakingContractMock.WithGetStake(func(ctx context.Context) (*big.Int, error) {
   146  				return big.NewInt(1), nil
   147  			}),
   148  		)
   149  		ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract})
   150  		jsonhttptest.Request(t, ts, http.MethodGet, "/stake/withdrawable", http.StatusOK,
   151  			jsonhttptest.WithExpectedJSONResponse(&api.GetWithdrawableResponse{WithdrawableAmount: bigint.Wrap(big.NewInt(1))}))
   152  	})
   153  
   154  	t.Run("with error", func(t *testing.T) {
   155  		t.Parallel()
   156  
   157  		contractWithError := stakingContractMock.New(
   158  			stakingContractMock.WithGetStake(func(ctx context.Context) (*big.Int, error) {
   159  				return big.NewInt(0), fmt.Errorf("get stake failed")
   160  			}),
   161  		)
   162  		ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contractWithError})
   163  		jsonhttptest.Request(t, ts, http.MethodGet, "/stake/withdrawable", http.StatusInternalServerError,
   164  			jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusInternalServerError, Message: "get staked amount failed"}))
   165  	})
   166  }
   167  
   168  func Test_stakingDepositHandler_invalidInputs(t *testing.T) {
   169  	t.Parallel()
   170  
   171  	client, _, _, _ := newTestServer(t, testServerOptions{})
   172  
   173  	tests := []struct {
   174  		name   string
   175  		amount string
   176  		want   jsonhttp.StatusResponse
   177  	}{{
   178  		name:   "amount - invalid value",
   179  		amount: "a",
   180  		want: jsonhttp.StatusResponse{
   181  			Code:    http.StatusBadRequest,
   182  			Message: "invalid path params",
   183  			Reasons: []jsonhttp.Reason{
   184  				{
   185  					Field: "amount",
   186  					Error: "invalid value",
   187  				},
   188  			},
   189  		},
   190  	}}
   191  
   192  	for _, tc := range tests {
   193  		tc := tc
   194  		t.Run(tc.name, func(t *testing.T) {
   195  			t.Parallel()
   196  
   197  			jsonhttptest.Request(t, client, http.MethodPost, "/stake/"+tc.amount, tc.want.Code,
   198  				jsonhttptest.WithExpectedJSONResponse(tc.want),
   199  			)
   200  		})
   201  	}
   202  }
   203  
   204  func TestWithdrawStake(t *testing.T) {
   205  	t.Parallel()
   206  
   207  	txHash := common.HexToHash("0x1234")
   208  
   209  	t.Run("ok", func(t *testing.T) {
   210  		t.Parallel()
   211  
   212  		contract := stakingContractMock.New(
   213  			stakingContractMock.WithWithdrawStake(func(ctx context.Context) (common.Hash, error) {
   214  				return txHash, nil
   215  			}),
   216  		)
   217  		ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract})
   218  		jsonhttptest.Request(t, ts, http.MethodDelete, "/stake/withdrawable", http.StatusOK, jsonhttptest.WithExpectedJSONResponse(
   219  			&api.StakeTransactionReponse{TxHash: txHash.String()}))
   220  	})
   221  
   222  	t.Run("with invalid stake amount", func(t *testing.T) {
   223  		t.Parallel()
   224  
   225  		contract := stakingContractMock.New(
   226  			stakingContractMock.WithWithdrawStake(func(ctx context.Context) (common.Hash, error) {
   227  				return common.Hash{}, staking.ErrInsufficientStake
   228  			}),
   229  		)
   230  		ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract})
   231  		jsonhttptest.Request(t, ts, http.MethodDelete, "/stake/withdrawable", http.StatusBadRequest,
   232  			jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusBadRequest, Message: "insufficient stake to withdraw"}))
   233  	})
   234  
   235  	t.Run("internal error", func(t *testing.T) {
   236  		t.Parallel()
   237  
   238  		contract := stakingContractMock.New(
   239  			stakingContractMock.WithWithdrawStake(func(ctx context.Context) (common.Hash, error) {
   240  				return common.Hash{}, fmt.Errorf("some error")
   241  			}),
   242  		)
   243  		ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract})
   244  		jsonhttptest.Request(t, ts, http.MethodDelete, "/stake/withdrawable", http.StatusInternalServerError)
   245  		jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusInternalServerError, Message: "cannot withdraw stake"})
   246  	})
   247  
   248  	t.Run("gas limit header", func(t *testing.T) {
   249  		t.Parallel()
   250  
   251  		contract := stakingContractMock.New(
   252  			stakingContractMock.WithWithdrawStake(func(ctx context.Context) (common.Hash, error) {
   253  				gasLimit := sctx.GetGasLimit(ctx)
   254  				if gasLimit != 2000000 {
   255  					t.Fatalf("want 2000000, got %d", gasLimit)
   256  				}
   257  				return txHash, nil
   258  			}),
   259  		)
   260  		ts, _, _, _ := newTestServer(t, testServerOptions{
   261  			StakingContract: contract,
   262  		})
   263  
   264  		jsonhttptest.Request(t, ts, http.MethodDelete, "/stake/withdrawable", http.StatusOK,
   265  			jsonhttptest.WithRequestHeader(api.GasLimitHeader, "2000000"),
   266  		)
   267  	})
   268  }
   269  
   270  func TestMigrateStake(t *testing.T) {
   271  	t.Parallel()
   272  
   273  	txHash := common.HexToHash("0x1234")
   274  
   275  	t.Run("ok", func(t *testing.T) {
   276  		t.Parallel()
   277  
   278  		contract := stakingContractMock.New(
   279  			stakingContractMock.WithMigrateStake(func(ctx context.Context) (common.Hash, error) {
   280  				return txHash, nil
   281  			}),
   282  		)
   283  		ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract})
   284  		jsonhttptest.Request(t, ts, http.MethodDelete, "/stake", http.StatusOK, jsonhttptest.WithExpectedJSONResponse(
   285  			&api.StakeTransactionReponse{TxHash: txHash.String()}))
   286  	})
   287  
   288  	t.Run("with invalid stake amount", func(t *testing.T) {
   289  		t.Parallel()
   290  
   291  		contract := stakingContractMock.New(
   292  			stakingContractMock.WithMigrateStake(func(ctx context.Context) (common.Hash, error) {
   293  				return common.Hash{}, staking.ErrInsufficientStake
   294  			}),
   295  		)
   296  		ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract})
   297  		jsonhttptest.Request(t, ts, http.MethodDelete, "/stake", http.StatusBadRequest,
   298  			jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusBadRequest, Message: "insufficient stake to migrate"}))
   299  	})
   300  
   301  	t.Run("internal error", func(t *testing.T) {
   302  		t.Parallel()
   303  
   304  		contract := stakingContractMock.New(
   305  			stakingContractMock.WithMigrateStake(func(ctx context.Context) (common.Hash, error) {
   306  				return common.Hash{}, fmt.Errorf("some error")
   307  			}),
   308  		)
   309  		ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract})
   310  		jsonhttptest.Request(t, ts, http.MethodDelete, "/stake", http.StatusInternalServerError)
   311  		jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusInternalServerError, Message: "cannot withdraw stake"})
   312  	})
   313  
   314  	t.Run("gas limit header", func(t *testing.T) {
   315  		t.Parallel()
   316  
   317  		contract := stakingContractMock.New(
   318  			stakingContractMock.WithMigrateStake(func(ctx context.Context) (common.Hash, error) {
   319  				gasLimit := sctx.GetGasLimit(ctx)
   320  				if gasLimit != 2000000 {
   321  					t.Fatalf("want 2000000, got %d", gasLimit)
   322  				}
   323  				return txHash, nil
   324  			}),
   325  		)
   326  		ts, _, _, _ := newTestServer(t, testServerOptions{
   327  			StakingContract: contract,
   328  		})
   329  
   330  		jsonhttptest.Request(t, ts, http.MethodDelete, "/stake", http.StatusOK,
   331  			jsonhttptest.WithRequestHeader(api.GasLimitHeader, "2000000"),
   332  		)
   333  	})
   334  }