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

     1  // Copyright 2021 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  	"bytes"
     9  	"context"
    10  	"encoding/hex"
    11  	"errors"
    12  	"fmt"
    13  	"math/big"
    14  	"net/http"
    15  	"strconv"
    16  	"testing"
    17  	"time"
    18  
    19  	"github.com/ethereum/go-ethereum/common"
    20  
    21  	"github.com/ethersphere/bee/v2/pkg/api"
    22  	"github.com/ethersphere/bee/v2/pkg/bigint"
    23  	"github.com/ethersphere/bee/v2/pkg/jsonhttp"
    24  	"github.com/ethersphere/bee/v2/pkg/jsonhttp/jsonhttptest"
    25  	"github.com/ethersphere/bee/v2/pkg/postage"
    26  	"github.com/ethersphere/bee/v2/pkg/postage/batchstore/mock"
    27  	mockpost "github.com/ethersphere/bee/v2/pkg/postage/mock"
    28  	"github.com/ethersphere/bee/v2/pkg/postage/postagecontract"
    29  	contractMock "github.com/ethersphere/bee/v2/pkg/postage/postagecontract/mock"
    30  	postagetesting "github.com/ethersphere/bee/v2/pkg/postage/testing"
    31  	"github.com/ethersphere/bee/v2/pkg/sctx"
    32  	mockstorer "github.com/ethersphere/bee/v2/pkg/storer/mock"
    33  	"github.com/ethersphere/bee/v2/pkg/transaction/backendmock"
    34  )
    35  
    36  func TestPostageCreateStamp(t *testing.T) {
    37  	t.Parallel()
    38  
    39  	batchID := []byte{1, 2, 3, 4}
    40  	initialBalance := int64(1000)
    41  	depth := uint8(24)
    42  	label := "label"
    43  	txHash := common.HexToHash("0x1234")
    44  	createBatch := func(amount int64, depth uint8, label string) string {
    45  		return fmt.Sprintf("/stamps/%d/%d?label=%s", amount, depth, label)
    46  	}
    47  
    48  	t.Run("ok", func(t *testing.T) {
    49  		t.Parallel()
    50  
    51  		var immutable bool
    52  		contract := contractMock.New(
    53  			contractMock.WithCreateBatchFunc(func(ctx context.Context, ib *big.Int, d uint8, i bool, l string) (common.Hash, []byte, error) {
    54  				if ib.Cmp(big.NewInt(initialBalance)) != 0 {
    55  					return common.Hash{}, nil, fmt.Errorf("called with wrong initial balance. wanted %d, got %d", initialBalance, ib)
    56  				}
    57  				immutable = i
    58  				if d != depth {
    59  					return common.Hash{}, nil, fmt.Errorf("called with wrong depth. wanted %d, got %d", depth, d)
    60  				}
    61  				if l != label {
    62  					return common.Hash{}, nil, fmt.Errorf("called with wrong label. wanted %s, got %s", label, l)
    63  				}
    64  				return txHash, batchID, nil
    65  			}),
    66  		)
    67  		ts, _, _, _ := newTestServer(t, testServerOptions{
    68  			PostageContract: contract,
    69  		})
    70  
    71  		jsonhttptest.Request(t, ts, http.MethodPost, createBatch(initialBalance, depth, label), http.StatusCreated,
    72  			jsonhttptest.WithExpectedJSONResponse(&api.PostageCreateResponse{
    73  				BatchID: batchID,
    74  				TxHash:  txHash.String(),
    75  			}),
    76  		)
    77  
    78  		if !immutable {
    79  			t.Fatalf("default batch should be immutable")
    80  		}
    81  	})
    82  
    83  	t.Run("with-error", func(t *testing.T) {
    84  		t.Parallel()
    85  
    86  		contract := contractMock.New(
    87  			contractMock.WithCreateBatchFunc(func(ctx context.Context, ib *big.Int, d uint8, i bool, l string) (common.Hash, []byte, error) {
    88  				return common.Hash{}, nil, errors.New("err")
    89  			}),
    90  		)
    91  		ts, _, _, _ := newTestServer(t, testServerOptions{
    92  			PostageContract: contract,
    93  		})
    94  
    95  		jsonhttptest.Request(t, ts, http.MethodPost, createBatch(initialBalance, depth, label), http.StatusInternalServerError,
    96  			jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{
    97  				Code:    http.StatusInternalServerError,
    98  				Message: "cannot create batch",
    99  			}),
   100  		)
   101  	})
   102  
   103  	t.Run("out-of-funds", func(t *testing.T) {
   104  		t.Parallel()
   105  
   106  		contract := contractMock.New(
   107  			contractMock.WithCreateBatchFunc(func(ctx context.Context, ib *big.Int, d uint8, i bool, l string) (common.Hash, []byte, error) {
   108  				return common.Hash{}, nil, postagecontract.ErrInsufficientFunds
   109  			}),
   110  		)
   111  		ts, _, _, _ := newTestServer(t, testServerOptions{
   112  			PostageContract: contract,
   113  		})
   114  
   115  		jsonhttptest.Request(t, ts, http.MethodPost, createBatch(initialBalance, depth, label), http.StatusBadRequest,
   116  			jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{
   117  				Code:    http.StatusBadRequest,
   118  				Message: "out of funds",
   119  			}),
   120  		)
   121  	})
   122  
   123  	t.Run("depth less than bucket depth", func(t *testing.T) {
   124  		t.Parallel()
   125  
   126  		contract := contractMock.New(
   127  			contractMock.WithCreateBatchFunc(func(ctx context.Context, ib *big.Int, d uint8, i bool, l string) (common.Hash, []byte, error) {
   128  				return common.Hash{}, nil, postagecontract.ErrInvalidDepth
   129  			}),
   130  		)
   131  		ts, _, _, _ := newTestServer(t, testServerOptions{
   132  			PostageContract: contract,
   133  		})
   134  
   135  		jsonhttptest.Request(t, ts, http.MethodPost, "/stamps/1000/9", http.StatusBadRequest,
   136  			jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{
   137  				Code:    http.StatusBadRequest,
   138  				Message: "invalid path params",
   139  				Reasons: []jsonhttp.Reason{
   140  					{
   141  						Field: "depth",
   142  						Error: "want min:17",
   143  					},
   144  				},
   145  			}),
   146  		)
   147  	})
   148  
   149  	t.Run("mutable header", func(t *testing.T) {
   150  		t.Parallel()
   151  
   152  		var immutable bool
   153  		contract := contractMock.New(
   154  			contractMock.WithCreateBatchFunc(func(ctx context.Context, _ *big.Int, _ uint8, i bool, _ string) (common.Hash, []byte, error) {
   155  				immutable = i
   156  				return txHash, batchID, nil
   157  			}),
   158  		)
   159  		ts, _, _, _ := newTestServer(t, testServerOptions{
   160  			PostageContract: contract,
   161  		})
   162  
   163  		jsonhttptest.Request(t, ts, http.MethodPost, "/stamps/1000/24", http.StatusCreated,
   164  			jsonhttptest.WithRequestHeader(api.ImmutableHeader, "false"),
   165  			jsonhttptest.WithExpectedJSONResponse(&api.PostageCreateResponse{
   166  				BatchID: batchID,
   167  				TxHash:  txHash.String(),
   168  			}),
   169  		)
   170  
   171  		if immutable {
   172  			t.Fatalf("want false, got %v", immutable)
   173  		}
   174  	})
   175  
   176  	t.Run("syncing in progress", func(t *testing.T) {
   177  		t.Parallel()
   178  
   179  		ts, _, _, _ := newTestServer(t, testServerOptions{
   180  			SyncStatus: func() (bool, error) { return false, nil },
   181  		})
   182  
   183  		jsonhttptest.Request(t, ts, http.MethodPost, createBatch(initialBalance, depth, label), http.StatusServiceUnavailable,
   184  			jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{
   185  				Message: "syncing in progress",
   186  				Code:    503,
   187  			}),
   188  		)
   189  	})
   190  	t.Run("syncing failed", func(t *testing.T) {
   191  		t.Parallel()
   192  
   193  		ts, _, _, _ := newTestServer(t, testServerOptions{
   194  			SyncStatus: func() (bool, error) { return true, errors.New("oops") },
   195  		})
   196  
   197  		jsonhttptest.Request(t, ts, http.MethodPost, createBatch(initialBalance, depth, label), http.StatusServiceUnavailable,
   198  			jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{
   199  				Message: "syncing failed",
   200  				Code:    503,
   201  			}),
   202  		)
   203  	})
   204  }
   205  
   206  func TestPostageGetStamps(t *testing.T) {
   207  	t.Parallel()
   208  
   209  	b := postagetesting.MustNewBatch(postagetesting.WithValue(20))
   210  
   211  	si := postage.NewStampIssuer("", "", b.ID, big.NewInt(3), 11, 10, 1000, true)
   212  	mp := mockpost.New(mockpost.WithIssuer(si))
   213  	cs := &postage.ChainState{Block: 10, TotalAmount: big.NewInt(5), CurrentPrice: big.NewInt(2)}
   214  
   215  	t.Run("single stamp", func(t *testing.T) {
   216  		t.Parallel()
   217  
   218  		bs := mock.New(mock.WithChainState(cs), mock.WithBatch(b))
   219  		ts, _, _, _ := newTestServer(t, testServerOptions{Post: mp, BatchStore: bs, BlockTime: 2 * time.Second})
   220  
   221  		jsonhttptest.Request(t, ts, http.MethodGet, "/stamps", http.StatusOK,
   222  			jsonhttptest.WithExpectedJSONResponse(&api.PostageStampsResponse{
   223  				Stamps: []api.PostageStampResponse{
   224  					{
   225  						BatchID:       b.ID,
   226  						Utilization:   si.Utilization(),
   227  						Usable:        true,
   228  						Label:         si.Label(),
   229  						Depth:         si.Depth(),
   230  						Amount:        bigint.Wrap(si.Amount()),
   231  						BucketDepth:   si.BucketDepth(),
   232  						BlockNumber:   si.BlockNumber(),
   233  						ImmutableFlag: si.ImmutableFlag(),
   234  						Exists:        true,
   235  						BatchTTL:      15, // ((value-totalAmount)/pricePerBlock)*blockTime=((20-5)/2)*2.
   236  					},
   237  				},
   238  			}),
   239  		)
   240  	})
   241  
   242  	t.Run("single expired Stamp", func(t *testing.T) {
   243  		t.Parallel()
   244  
   245  		eb := postagetesting.MustNewBatch(postagetesting.WithValue(20))
   246  
   247  		esi := postage.NewStampIssuer("", "", eb.ID, big.NewInt(3), 11, 10, 1000, true)
   248  		emp := mockpost.New(mockpost.WithIssuer(esi))
   249  		err := emp.HandleStampExpiry(context.Background(), eb.ID)
   250  		if err != nil {
   251  			t.Fatal(err)
   252  		}
   253  		ecs := &postage.ChainState{Block: 10, TotalAmount: big.NewInt(15), CurrentPrice: big.NewInt(12)}
   254  		ebs := mock.New(mock.WithChainState(ecs))
   255  		ts, _, _, _ := newTestServer(t, testServerOptions{Post: emp, BatchStore: ebs, BlockTime: 2 * time.Second})
   256  
   257  		jsonhttptest.Request(t, ts, http.MethodGet, "/stamps/"+hex.EncodeToString(eb.ID), http.StatusNotFound,
   258  			jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{
   259  				Message: "issuer does not exist",
   260  				Code:    404,
   261  			}),
   262  		)
   263  	})
   264  }
   265  
   266  // TestGetAllBatches tests that the endpoint that returns all living
   267  // batches functions correctly.
   268  func TestGetAllBatches(t *testing.T) {
   269  	t.Parallel()
   270  
   271  	b := postagetesting.MustNewBatch()
   272  	b.Value = big.NewInt(20)
   273  	si := postage.NewStampIssuer("", "", b.ID, big.NewInt(3), 11, 10, 1000, true)
   274  	mp := mockpost.New(mockpost.WithIssuer(si))
   275  	cs := &postage.ChainState{Block: 10, TotalAmount: big.NewInt(5), CurrentPrice: big.NewInt(2)}
   276  	bs := mock.New(mock.WithChainState(cs), mock.WithBatch(b))
   277  	ts, _, _, _ := newTestServer(t, testServerOptions{Post: mp, BatchStore: bs, BlockTime: 2 * time.Second})
   278  
   279  	oneBatch := struct {
   280  		Batches []api.PostageBatchResponse `json:"batches"`
   281  	}{
   282  		Batches: []api.PostageBatchResponse{
   283  			{
   284  				BatchID:     b.ID,
   285  				Value:       bigint.Wrap(b.Value),
   286  				Start:       b.Start,
   287  				Owner:       b.Owner,
   288  				Depth:       b.Depth,
   289  				BucketDepth: b.BucketDepth,
   290  				Immutable:   b.Immutable,
   291  				BatchTTL:    15, // ((value-totalAmount)/pricePerBlock)*blockTime=((20-5)/2)*2.
   292  			},
   293  		},
   294  	}
   295  
   296  	t.Run("all stamps", func(t *testing.T) {
   297  		t.Parallel()
   298  
   299  		jsonhttptest.Request(t, ts, http.MethodGet, "/batches", http.StatusOK,
   300  			jsonhttptest.WithExpectedJSONResponse(oneBatch),
   301  		)
   302  	})
   303  }
   304  
   305  func TestPostageGetStamp(t *testing.T) {
   306  	t.Parallel()
   307  
   308  	b := postagetesting.MustNewBatch()
   309  	b.Value = big.NewInt(20)
   310  	si := postage.NewStampIssuer("", "", b.ID, big.NewInt(3), 11, 10, 1000, true)
   311  	mp := mockpost.New(mockpost.WithIssuer(si))
   312  	cs := &postage.ChainState{Block: 10, TotalAmount: big.NewInt(5), CurrentPrice: big.NewInt(2)}
   313  	bs := mock.New(mock.WithChainState(cs), mock.WithBatch(b))
   314  	ts, _, _, _ := newTestServer(t, testServerOptions{Post: mp, BatchStore: bs, BlockTime: 2 * time.Second})
   315  
   316  	t.Run("ok", func(t *testing.T) {
   317  		t.Parallel()
   318  
   319  		jsonhttptest.Request(t, ts, http.MethodGet, "/stamps/"+hex.EncodeToString(b.ID), http.StatusOK,
   320  			jsonhttptest.WithExpectedJSONResponse(&api.PostageStampResponse{
   321  				BatchID:       b.ID,
   322  				Utilization:   si.Utilization(),
   323  				Usable:        true,
   324  				Label:         si.Label(),
   325  				Depth:         si.Depth(),
   326  				Amount:        bigint.Wrap(si.Amount()),
   327  				BucketDepth:   si.BucketDepth(),
   328  				BlockNumber:   si.BlockNumber(),
   329  				ImmutableFlag: si.ImmutableFlag(),
   330  				Exists:        true,
   331  				BatchTTL:      15, // ((value-totalAmount)/pricePerBlock)*blockTime=((20-5)/2)*2.
   332  			}),
   333  		)
   334  	})
   335  }
   336  
   337  func TestPostageGetBuckets(t *testing.T) {
   338  	t.Parallel()
   339  
   340  	si := postage.NewStampIssuer("", "", batchOk, big.NewInt(3), 11, 10, 1000, true)
   341  	mp := mockpost.New(mockpost.WithIssuer(si))
   342  	ts, _, _, _ := newTestServer(t, testServerOptions{Post: mp})
   343  	buckets := make([]api.BucketData, 1024)
   344  	for i := range buckets {
   345  		buckets[i] = api.BucketData{BucketID: uint32(i)}
   346  	}
   347  
   348  	t.Run("ok", func(t *testing.T) {
   349  		t.Parallel()
   350  
   351  		jsonhttptest.Request(t, ts, http.MethodGet, "/stamps/"+batchOkStr+"/buckets", http.StatusOK,
   352  			jsonhttptest.WithExpectedJSONResponse(&api.PostageStampBucketsResponse{
   353  				Depth:            si.Depth(),
   354  				BucketDepth:      si.BucketDepth(),
   355  				BucketUpperBound: si.BucketUpperBound(),
   356  				Buckets:          buckets,
   357  			}),
   358  		)
   359  	})
   360  
   361  	t.Run("batch not found", func(t *testing.T) {
   362  		t.Parallel()
   363  
   364  		mpNotFound := mockpost.New()
   365  		tsNotFound, _, _, _ := newTestServer(t, testServerOptions{Post: mpNotFound})
   366  
   367  		jsonhttptest.Request(t, tsNotFound, http.MethodGet, "/stamps/"+batchOkStr+"/buckets", http.StatusNotFound)
   368  	})
   369  
   370  }
   371  
   372  func TestReserveState(t *testing.T) {
   373  	t.Parallel()
   374  
   375  	t.Run("ok", func(t *testing.T) {
   376  		t.Parallel()
   377  
   378  		ts, _, _, _ := newTestServer(t, testServerOptions{
   379  			BatchStore: mock.New(mock.WithRadius(5)),
   380  			Storer:     mockstorer.New(),
   381  		})
   382  		jsonhttptest.Request(t, ts, http.MethodGet, "/reservestate", http.StatusOK,
   383  			jsonhttptest.WithExpectedJSONResponse(&api.ReserveStateResponse{
   384  				Radius: 5,
   385  			}),
   386  		)
   387  	})
   388  	t.Run("empty", func(t *testing.T) {
   389  		t.Parallel()
   390  
   391  		ts, _, _, _ := newTestServer(t, testServerOptions{
   392  			BatchStore: mock.New(),
   393  			Storer:     mockstorer.New(),
   394  		})
   395  		jsonhttptest.Request(t, ts, http.MethodGet, "/reservestate", http.StatusOK,
   396  			jsonhttptest.WithExpectedJSONResponse(&api.ReserveStateResponse{}),
   397  		)
   398  	})
   399  }
   400  func TestChainState(t *testing.T) {
   401  	t.Parallel()
   402  
   403  	t.Run("ok", func(t *testing.T) {
   404  		t.Parallel()
   405  
   406  		cs := &postage.ChainState{
   407  			Block:        123456,
   408  			TotalAmount:  big.NewInt(50),
   409  			CurrentPrice: big.NewInt(5),
   410  		}
   411  		ts, _, _, _ := newTestServer(t, testServerOptions{
   412  			BatchStore: mock.New(mock.WithChainState(cs)),
   413  			BackendOpts: []backendmock.Option{backendmock.WithBlockNumberFunc(func(ctx context.Context) (uint64, error) {
   414  				return 1, nil
   415  			})},
   416  		})
   417  		jsonhttptest.Request(t, ts, http.MethodGet, "/chainstate", http.StatusOK,
   418  			jsonhttptest.WithExpectedJSONResponse(&api.ChainStateResponse{
   419  				ChainTip:     1,
   420  				Block:        123456,
   421  				TotalAmount:  bigint.Wrap(big.NewInt(50)),
   422  				CurrentPrice: bigint.Wrap(big.NewInt(5)),
   423  			}),
   424  		)
   425  	})
   426  
   427  }
   428  
   429  func TestPostageTopUpStamp(t *testing.T) {
   430  	t.Parallel()
   431  
   432  	txHash := common.HexToHash("0x1234")
   433  	topupAmount := int64(1000)
   434  	topupBatch := func(id string, amount int64) string {
   435  		return fmt.Sprintf("/stamps/topup/%s/%d", id, amount)
   436  	}
   437  
   438  	t.Run("ok", func(t *testing.T) {
   439  		t.Parallel()
   440  
   441  		contract := contractMock.New(
   442  			contractMock.WithTopUpBatchFunc(func(ctx context.Context, id []byte, ib *big.Int) (common.Hash, error) {
   443  				if !bytes.Equal(id, batchOk) {
   444  					return common.Hash{}, errors.New("incorrect batch ID in call")
   445  				}
   446  				if ib.Cmp(big.NewInt(topupAmount)) != 0 {
   447  					return common.Hash{}, fmt.Errorf("called with wrong topup amount. wanted %d, got %d", topupAmount, ib)
   448  				}
   449  				return txHash, nil
   450  			}),
   451  		)
   452  		ts, _, _, _ := newTestServer(t, testServerOptions{
   453  			PostageContract: contract,
   454  		})
   455  
   456  		jsonhttptest.Request(t, ts, http.MethodPatch, topupBatch(batchOkStr, topupAmount), http.StatusAccepted,
   457  			jsonhttptest.WithExpectedJSONResponse(&api.PostageCreateResponse{
   458  				BatchID: batchOk,
   459  				TxHash:  txHash.String(),
   460  			}),
   461  		)
   462  	})
   463  
   464  	t.Run("with-custom-gas", func(t *testing.T) {
   465  		t.Parallel()
   466  
   467  		contract := contractMock.New(
   468  			contractMock.WithTopUpBatchFunc(func(ctx context.Context, id []byte, ib *big.Int) (common.Hash, error) {
   469  				if !bytes.Equal(id, batchOk) {
   470  					return common.Hash{}, errors.New("incorrect batch ID in call")
   471  				}
   472  				if ib.Cmp(big.NewInt(topupAmount)) != 0 {
   473  					return common.Hash{}, fmt.Errorf("called with wrong topup amount. wanted %d, got %d", topupAmount, ib)
   474  				}
   475  				if sctx.GetGasPrice(ctx).Cmp(big.NewInt(10000)) != 0 {
   476  					return common.Hash{}, fmt.Errorf("called with wrong gas price. wanted %d, got %d", 10000, sctx.GetGasPrice(ctx))
   477  				}
   478  				return txHash, nil
   479  			}),
   480  		)
   481  		ts, _, _, _ := newTestServer(t, testServerOptions{
   482  			PostageContract: contract,
   483  		})
   484  
   485  		jsonhttptest.Request(t, ts, http.MethodPatch, topupBatch(batchOkStr, topupAmount), http.StatusAccepted,
   486  			jsonhttptest.WithRequestHeader(api.GasPriceHeader, "10000"),
   487  			jsonhttptest.WithExpectedJSONResponse(&api.PostageCreateResponse{
   488  				BatchID: batchOk,
   489  				TxHash:  txHash.String(),
   490  			}),
   491  		)
   492  	})
   493  
   494  	t.Run("with-error", func(t *testing.T) {
   495  		t.Parallel()
   496  
   497  		contract := contractMock.New(
   498  			contractMock.WithTopUpBatchFunc(func(ctx context.Context, id []byte, ib *big.Int) (common.Hash, error) {
   499  				return common.Hash{}, errors.New("err")
   500  			}),
   501  		)
   502  		ts, _, _, _ := newTestServer(t, testServerOptions{
   503  			PostageContract: contract,
   504  		})
   505  
   506  		jsonhttptest.Request(t, ts, http.MethodPatch, topupBatch(batchOkStr, topupAmount), http.StatusInternalServerError,
   507  			jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{
   508  				Code:    http.StatusInternalServerError,
   509  				Message: "cannot topup batch",
   510  			}),
   511  		)
   512  	})
   513  
   514  	t.Run("out-of-funds", func(t *testing.T) {
   515  		t.Parallel()
   516  
   517  		contract := contractMock.New(
   518  			contractMock.WithTopUpBatchFunc(func(ctx context.Context, id []byte, ib *big.Int) (common.Hash, error) {
   519  				return common.Hash{}, postagecontract.ErrInsufficientFunds
   520  			}),
   521  		)
   522  		ts, _, _, _ := newTestServer(t, testServerOptions{
   523  			PostageContract: contract,
   524  		})
   525  
   526  		jsonhttptest.Request(t, ts, http.MethodPatch, topupBatch(batchOkStr, topupAmount), http.StatusPaymentRequired,
   527  			jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{
   528  				Code:    http.StatusPaymentRequired,
   529  				Message: "out of funds",
   530  			}),
   531  		)
   532  	})
   533  
   534  	t.Run("gas limit header", func(t *testing.T) {
   535  		t.Parallel()
   536  
   537  		contract := contractMock.New(
   538  			contractMock.WithTopUpBatchFunc(func(ctx context.Context, id []byte, ib *big.Int) (common.Hash, error) {
   539  				if sctx.GetGasLimit(ctx) != 10000 {
   540  					return common.Hash{}, fmt.Errorf("called with wrong gas price. wanted %d, got %d", 10000, sctx.GetGasLimit(ctx))
   541  				}
   542  				return txHash, nil
   543  			}),
   544  		)
   545  		ts, _, _, _ := newTestServer(t, testServerOptions{
   546  			PostageContract: contract,
   547  		})
   548  
   549  		jsonhttptest.Request(t, ts, http.MethodPatch, topupBatch(batchOkStr, topupAmount), http.StatusAccepted,
   550  			jsonhttptest.WithRequestHeader(api.GasLimitHeader, "10000"),
   551  			jsonhttptest.WithExpectedJSONResponse(&api.PostageCreateResponse{
   552  				BatchID: batchOk,
   553  				TxHash:  txHash.String(),
   554  			}),
   555  		)
   556  	})
   557  }
   558  
   559  func TestPostageDiluteStamp(t *testing.T) {
   560  	t.Parallel()
   561  
   562  	txHash := common.HexToHash("0x1234")
   563  	newBatchDepth := uint8(17)
   564  	diluteBatch := func(id string, depth uint8) string {
   565  		return fmt.Sprintf("/stamps/dilute/%s/%d", id, depth)
   566  	}
   567  
   568  	t.Run("ok", func(t *testing.T) {
   569  		t.Parallel()
   570  
   571  		contract := contractMock.New(
   572  			contractMock.WithDiluteBatchFunc(func(ctx context.Context, id []byte, newDepth uint8) (common.Hash, error) {
   573  				if !bytes.Equal(id, batchOk) {
   574  					return common.Hash{}, errors.New("incorrect batch ID in call")
   575  				}
   576  				if newDepth != newBatchDepth {
   577  					return common.Hash{}, fmt.Errorf("called with wrong depth. wanted %d, got %d", newBatchDepth, newDepth)
   578  				}
   579  				return txHash, nil
   580  			}),
   581  		)
   582  		ts, _, _, _ := newTestServer(t, testServerOptions{
   583  			PostageContract: contract,
   584  		})
   585  
   586  		jsonhttptest.Request(t, ts, http.MethodPatch, diluteBatch(batchOkStr, newBatchDepth), http.StatusAccepted,
   587  			jsonhttptest.WithExpectedJSONResponse(&api.PostageCreateResponse{
   588  				BatchID: batchOk,
   589  				TxHash:  txHash.String(),
   590  			}),
   591  		)
   592  	})
   593  
   594  	t.Run("with-custom-gas", func(t *testing.T) {
   595  		t.Parallel()
   596  
   597  		contract := contractMock.New(
   598  			contractMock.WithDiluteBatchFunc(func(ctx context.Context, id []byte, newDepth uint8) (common.Hash, error) {
   599  				if !bytes.Equal(id, batchOk) {
   600  					return common.Hash{}, errors.New("incorrect batch ID in call")
   601  				}
   602  				if newDepth != newBatchDepth {
   603  					return common.Hash{}, fmt.Errorf("called with wrong depth. wanted %d, got %d", newBatchDepth, newDepth)
   604  				}
   605  				if sctx.GetGasPrice(ctx).Cmp(big.NewInt(10000)) != 0 {
   606  					return common.Hash{}, fmt.Errorf("called with wrong gas price. wanted %d, got %d", 10000, sctx.GetGasPrice(ctx))
   607  				}
   608  				return txHash, nil
   609  			}),
   610  		)
   611  		ts, _, _, _ := newTestServer(t, testServerOptions{
   612  			PostageContract: contract,
   613  		})
   614  
   615  		jsonhttptest.Request(t, ts, http.MethodPatch, diluteBatch(batchOkStr, newBatchDepth), http.StatusAccepted,
   616  			jsonhttptest.WithRequestHeader(api.GasPriceHeader, "10000"),
   617  			jsonhttptest.WithExpectedJSONResponse(&api.PostageCreateResponse{
   618  				BatchID: batchOk,
   619  				TxHash:  txHash.String(),
   620  			}),
   621  		)
   622  	})
   623  
   624  	t.Run("with-error", func(t *testing.T) {
   625  		t.Parallel()
   626  
   627  		contract := contractMock.New(
   628  			contractMock.WithDiluteBatchFunc(func(ctx context.Context, id []byte, newDepth uint8) (common.Hash, error) {
   629  				return common.Hash{}, errors.New("err")
   630  			}),
   631  		)
   632  		ts, _, _, _ := newTestServer(t, testServerOptions{
   633  			PostageContract: contract,
   634  		})
   635  
   636  		jsonhttptest.Request(t, ts, http.MethodPatch, diluteBatch(batchOkStr, newBatchDepth), http.StatusInternalServerError,
   637  			jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{
   638  				Code:    http.StatusInternalServerError,
   639  				Message: "cannot dilute batch",
   640  			}),
   641  		)
   642  	})
   643  
   644  	t.Run("with depth error", func(t *testing.T) {
   645  		t.Parallel()
   646  
   647  		contract := contractMock.New(
   648  			contractMock.WithDiluteBatchFunc(func(ctx context.Context, id []byte, newDepth uint8) (common.Hash, error) {
   649  				return common.Hash{}, postagecontract.ErrInvalidDepth
   650  			}),
   651  		)
   652  		ts, _, _, _ := newTestServer(t, testServerOptions{
   653  			PostageContract: contract,
   654  		})
   655  
   656  		jsonhttptest.Request(t, ts, http.MethodPatch, diluteBatch(batchOkStr, newBatchDepth), http.StatusBadRequest,
   657  			jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{
   658  				Code:    http.StatusBadRequest,
   659  				Message: "invalid depth",
   660  			}),
   661  		)
   662  	})
   663  
   664  	t.Run("gas limit header", func(t *testing.T) {
   665  		t.Parallel()
   666  
   667  		contract := contractMock.New(
   668  			contractMock.WithDiluteBatchFunc(func(ctx context.Context, _ []byte, _ uint8) (common.Hash, error) {
   669  				if sctx.GetGasLimit(ctx) != 10000 {
   670  					return common.Hash{}, fmt.Errorf("called with wrong gas price. wanted %d, got %d", 10000, sctx.GetGasLimit(ctx))
   671  				}
   672  				return txHash, nil
   673  			}),
   674  		)
   675  		ts, _, _, _ := newTestServer(t, testServerOptions{
   676  			PostageContract: contract,
   677  		})
   678  
   679  		jsonhttptest.Request(t, ts, http.MethodPatch, diluteBatch(batchOkStr, newBatchDepth), http.StatusAccepted,
   680  			jsonhttptest.WithRequestHeader(api.GasLimitHeader, "10000"),
   681  			jsonhttptest.WithExpectedJSONResponse(&api.PostageCreateResponse{
   682  				BatchID: batchOk,
   683  				TxHash:  txHash.String(),
   684  			}),
   685  		)
   686  
   687  	})
   688  }
   689  
   690  // Tests the postageAccessHandler middleware for any set of operations that are guarded
   691  // by the postage semaphore
   692  func TestPostageAccessHandler(t *testing.T) {
   693  	t.Parallel()
   694  
   695  	txHash := common.HexToHash("0x1234")
   696  
   697  	type operation struct {
   698  		name     string
   699  		method   string
   700  		url      string
   701  		respCode int
   702  		resp     interface{}
   703  	}
   704  
   705  	success := []operation{
   706  		{
   707  			name:     "create batch ok",
   708  			method:   http.MethodPost,
   709  			url:      "/stamps/1000/24?label=test",
   710  			respCode: http.StatusCreated,
   711  			resp: &api.PostageCreateResponse{
   712  				BatchID: batchOk,
   713  				TxHash:  txHash.String(),
   714  			},
   715  		},
   716  		{
   717  			name:     "topup batch ok",
   718  			method:   http.MethodPatch,
   719  			url:      fmt.Sprintf("/stamps/topup/%s/10", batchOkStr),
   720  			respCode: http.StatusAccepted,
   721  			resp: &api.PostageCreateResponse{
   722  				BatchID: batchOk,
   723  				TxHash:  txHash.String(),
   724  			},
   725  		},
   726  		{
   727  			name:     "dilute batch ok",
   728  			method:   http.MethodPatch,
   729  			url:      fmt.Sprintf("/stamps/dilute/%s/18", batchOkStr),
   730  			respCode: http.StatusAccepted,
   731  			resp: &api.PostageCreateResponse{
   732  				BatchID: batchOk,
   733  				TxHash:  txHash.String(),
   734  			},
   735  		},
   736  	}
   737  
   738  	failure := []operation{
   739  		{
   740  			name:     "create batch not ok",
   741  			method:   http.MethodPost,
   742  			url:      "/stamps/1000/24?label=test",
   743  			respCode: http.StatusTooManyRequests,
   744  			resp: &jsonhttp.StatusResponse{
   745  				Code:    http.StatusTooManyRequests,
   746  				Message: "simultaneous on-chain operations not supported",
   747  			},
   748  		},
   749  		{
   750  			name:     "topup batch not ok",
   751  			method:   http.MethodPatch,
   752  			url:      fmt.Sprintf("/stamps/topup/%s/10", batchOkStr),
   753  			respCode: http.StatusTooManyRequests,
   754  			resp: &jsonhttp.StatusResponse{
   755  				Code:    http.StatusTooManyRequests,
   756  				Message: "simultaneous on-chain operations not supported",
   757  			},
   758  		},
   759  		{
   760  			name:     "dilute batch not ok",
   761  			method:   http.MethodPatch,
   762  			url:      fmt.Sprintf("/stamps/dilute/%s/18", batchOkStr),
   763  			respCode: http.StatusTooManyRequests,
   764  			resp: &jsonhttp.StatusResponse{
   765  				Code:    http.StatusTooManyRequests,
   766  				Message: "simultaneous on-chain operations not supported",
   767  			},
   768  		},
   769  	}
   770  
   771  	for _, op1 := range success {
   772  		for _, op2 := range failure {
   773  			op1 := op1
   774  			op2 := op2
   775  			t.Run(op1.name+"-"+op2.name, func(t *testing.T) {
   776  				t.Parallel()
   777  
   778  				wait, done := make(chan struct{}), make(chan struct{})
   779  				contract := contractMock.New(
   780  					contractMock.WithCreateBatchFunc(func(ctx context.Context, ib *big.Int, d uint8, i bool, l string) (common.Hash, []byte, error) {
   781  						<-wait
   782  						return txHash, batchOk, nil
   783  					}),
   784  					contractMock.WithTopUpBatchFunc(func(ctx context.Context, id []byte, ib *big.Int) (common.Hash, error) {
   785  						<-wait
   786  						return txHash, nil
   787  					}),
   788  					contractMock.WithDiluteBatchFunc(func(ctx context.Context, id []byte, newDepth uint8) (common.Hash, error) {
   789  						<-wait
   790  						return txHash, nil
   791  					}),
   792  				)
   793  
   794  				ts, _, _, _ := newTestServer(t, testServerOptions{
   795  					PostageContract: contract,
   796  				})
   797  
   798  				go func() {
   799  					defer close(done)
   800  
   801  					jsonhttptest.Request(t, ts, op1.method, op1.url, op1.respCode, jsonhttptest.WithExpectedJSONResponse(op1.resp))
   802  				}()
   803  
   804  				time.Sleep(time.Millisecond * 100)
   805  
   806  				jsonhttptest.Request(t, ts, op2.method, op2.url, op2.respCode, jsonhttptest.WithExpectedJSONResponse(op2.resp))
   807  
   808  				close(wait)
   809  				<-done
   810  			})
   811  		}
   812  	}
   813  }
   814  
   815  //nolint:tparallel
   816  func Test_postageCreateHandler_invalidInputs(t *testing.T) {
   817  	t.Parallel()
   818  
   819  	client, _, _, _ := newTestServer(t, testServerOptions{})
   820  
   821  	tests := []struct {
   822  		name   string
   823  		amount string
   824  		depth  string
   825  		want   jsonhttp.StatusResponse
   826  	}{{
   827  		name:   "amount - invalid value",
   828  		amount: "a",
   829  		depth:  "1",
   830  		want: jsonhttp.StatusResponse{
   831  			Code:    http.StatusBadRequest,
   832  			Message: "invalid path params",
   833  			Reasons: []jsonhttp.Reason{
   834  				{
   835  					Field: "amount",
   836  					Error: "invalid value",
   837  				},
   838  			},
   839  		},
   840  	}, {
   841  		name:   "depth - invalid value",
   842  		amount: "1",
   843  		depth:  "a",
   844  		want: jsonhttp.StatusResponse{
   845  			Code:    http.StatusBadRequest,
   846  			Message: "invalid path params",
   847  			Reasons: []jsonhttp.Reason{
   848  				{
   849  					Field: "depth",
   850  					Error: strconv.ErrSyntax.Error(),
   851  				},
   852  			},
   853  		},
   854  	}}
   855  
   856  	//nolint:paralleltest
   857  	for _, tc := range tests {
   858  		t.Run(tc.name, func(t *testing.T) {
   859  			jsonhttptest.Request(t, client, http.MethodPost, "/stamps/"+tc.amount+"/"+tc.depth, tc.want.Code,
   860  				jsonhttptest.WithExpectedJSONResponse(tc.want),
   861  			)
   862  		})
   863  	}
   864  }
   865  
   866  func Test_postageGetStampBucketsHandler_invalidInputs(t *testing.T) {
   867  	t.Parallel()
   868  
   869  	client, _, _, _ := newTestServer(t, testServerOptions{})
   870  
   871  	tests := []struct {
   872  		name    string
   873  		batchID string
   874  		want    jsonhttp.StatusResponse
   875  	}{{
   876  		name:    "batch_id - odd hex string",
   877  		batchID: "123",
   878  		want: jsonhttp.StatusResponse{
   879  			Code:    http.StatusBadRequest,
   880  			Message: "invalid path params",
   881  			Reasons: []jsonhttp.Reason{
   882  				{
   883  					Field: "batch_id",
   884  					Error: api.ErrHexLength.Error(),
   885  				},
   886  			},
   887  		},
   888  	}, {
   889  		name:    "batch_id - invalid hex character",
   890  		batchID: "123G",
   891  		want: jsonhttp.StatusResponse{
   892  			Code:    http.StatusBadRequest,
   893  			Message: "invalid path params",
   894  			Reasons: []jsonhttp.Reason{
   895  				{
   896  					Field: "batch_id",
   897  					Error: api.HexInvalidByteError('G').Error(),
   898  				},
   899  			},
   900  		},
   901  	}, {
   902  		name:    "batch_id - invalid length",
   903  		batchID: "1234",
   904  		want: jsonhttp.StatusResponse{
   905  			Code:    http.StatusBadRequest,
   906  			Message: "invalid path params",
   907  			Reasons: []jsonhttp.Reason{
   908  				{
   909  					Field: "batch_id",
   910  					Error: "want len:32",
   911  				},
   912  			},
   913  		},
   914  	}}
   915  
   916  	for _, tc := range tests {
   917  		tc := tc
   918  		t.Run(tc.name, func(t *testing.T) {
   919  			t.Parallel()
   920  
   921  			jsonhttptest.Request(t, client, http.MethodGet, "/stamps/"+tc.batchID+"/buckets", tc.want.Code,
   922  				jsonhttptest.WithExpectedJSONResponse(tc.want),
   923  			)
   924  		})
   925  	}
   926  }
   927  
   928  func Test_postageGetStampHandler_invalidInputs(t *testing.T) {
   929  	t.Parallel()
   930  
   931  	client, _, _, _ := newTestServer(t, testServerOptions{})
   932  
   933  	tests := []struct {
   934  		name    string
   935  		batchID string
   936  		want    jsonhttp.StatusResponse
   937  	}{{
   938  		name:    "batch_id - odd hex string",
   939  		batchID: "123",
   940  		want: jsonhttp.StatusResponse{
   941  			Code:    http.StatusBadRequest,
   942  			Message: "invalid path params",
   943  			Reasons: []jsonhttp.Reason{
   944  				{
   945  					Field: "batch_id",
   946  					Error: api.ErrHexLength.Error(),
   947  				},
   948  			},
   949  		},
   950  	}, {
   951  		name:    "batch_id - invalid hex character",
   952  		batchID: "123G",
   953  		want: jsonhttp.StatusResponse{
   954  			Code:    http.StatusBadRequest,
   955  			Message: "invalid path params",
   956  			Reasons: []jsonhttp.Reason{
   957  				{
   958  					Field: "batch_id",
   959  					Error: api.HexInvalidByteError('G').Error(),
   960  				},
   961  			},
   962  		},
   963  	}, {
   964  		name:    "batch_id - invalid length",
   965  		batchID: "1234",
   966  		want: jsonhttp.StatusResponse{
   967  			Code:    http.StatusBadRequest,
   968  			Message: "invalid path params",
   969  			Reasons: []jsonhttp.Reason{
   970  				{
   971  					Field: "batch_id",
   972  					Error: "want len:32",
   973  				},
   974  			},
   975  		},
   976  	}}
   977  
   978  	for _, tc := range tests {
   979  		tc := tc
   980  		t.Run(tc.name, func(t *testing.T) {
   981  			t.Parallel()
   982  
   983  			jsonhttptest.Request(t, client, http.MethodGet, "/stamps/"+tc.batchID, tc.want.Code,
   984  				jsonhttptest.WithExpectedJSONResponse(tc.want),
   985  			)
   986  		})
   987  	}
   988  }
   989  
   990  //nolint:tparallel
   991  func Test_postageTopUpHandler_invalidInputs(t *testing.T) {
   992  	t.Parallel()
   993  
   994  	client, _, _, _ := newTestServer(t, testServerOptions{})
   995  
   996  	tests := []struct {
   997  		name    string
   998  		batchID string
   999  		amount  string
  1000  		want    jsonhttp.StatusResponse
  1001  	}{{
  1002  		name:    "batch_id - odd hex string",
  1003  		batchID: "123",
  1004  		amount:  "1",
  1005  		want: jsonhttp.StatusResponse{
  1006  			Code:    http.StatusBadRequest,
  1007  			Message: "invalid path params",
  1008  			Reasons: []jsonhttp.Reason{
  1009  				{
  1010  					Field: "batch_id",
  1011  					Error: api.ErrHexLength.Error(),
  1012  				},
  1013  			},
  1014  		},
  1015  	}, {
  1016  		name:    "batch_id - invalid hex character",
  1017  		batchID: "123G",
  1018  		amount:  "1",
  1019  		want: jsonhttp.StatusResponse{
  1020  			Code:    http.StatusBadRequest,
  1021  			Message: "invalid path params",
  1022  			Reasons: []jsonhttp.Reason{
  1023  				{
  1024  					Field: "batch_id",
  1025  					Error: api.HexInvalidByteError('G').Error(),
  1026  				},
  1027  			},
  1028  		},
  1029  	}, {
  1030  		name:    "batch_id - invalid length",
  1031  		batchID: "1234",
  1032  		amount:  "1",
  1033  		want: jsonhttp.StatusResponse{
  1034  			Code:    http.StatusBadRequest,
  1035  			Message: "invalid path params",
  1036  			Reasons: []jsonhttp.Reason{
  1037  				{
  1038  					Field: "batch_id",
  1039  					Error: "want len:32",
  1040  				},
  1041  			},
  1042  		},
  1043  	}, {
  1044  		name:    "amount - invalid value",
  1045  		batchID: hex.EncodeToString([]byte{31: 0}),
  1046  		amount:  "a",
  1047  		want: jsonhttp.StatusResponse{
  1048  			Code:    http.StatusBadRequest,
  1049  			Message: "invalid path params",
  1050  			Reasons: []jsonhttp.Reason{
  1051  				{
  1052  					Field: "amount",
  1053  					Error: "invalid value",
  1054  				},
  1055  			},
  1056  		},
  1057  	}}
  1058  
  1059  	//nolint:paralleltest
  1060  	for _, tc := range tests {
  1061  		t.Run(tc.name, func(t *testing.T) {
  1062  			jsonhttptest.Request(t, client, http.MethodPatch, "/stamps/topup/"+tc.batchID+"/"+tc.amount, tc.want.Code,
  1063  				jsonhttptest.WithExpectedJSONResponse(tc.want),
  1064  			)
  1065  		})
  1066  	}
  1067  }
  1068  
  1069  //nolint:tparallel
  1070  func Test_postageDiluteHandler_invalidInputs(t *testing.T) {
  1071  	t.Parallel()
  1072  
  1073  	client, _, _, _ := newTestServer(t, testServerOptions{})
  1074  
  1075  	tests := []struct {
  1076  		name    string
  1077  		batchID string
  1078  		depth   string
  1079  		want    jsonhttp.StatusResponse
  1080  	}{{
  1081  		name:    "batch_id - odd hex string",
  1082  		batchID: "123",
  1083  		depth:   "1",
  1084  		want: jsonhttp.StatusResponse{
  1085  			Code:    http.StatusBadRequest,
  1086  			Message: "invalid path params",
  1087  			Reasons: []jsonhttp.Reason{
  1088  				{
  1089  					Field: "batch_id",
  1090  					Error: api.ErrHexLength.Error(),
  1091  				},
  1092  			},
  1093  		},
  1094  	}, {
  1095  		name:    "batch_id - invalid hex character",
  1096  		batchID: "123G",
  1097  		depth:   "1",
  1098  		want: jsonhttp.StatusResponse{
  1099  			Code:    http.StatusBadRequest,
  1100  			Message: "invalid path params",
  1101  			Reasons: []jsonhttp.Reason{
  1102  				{
  1103  					Field: "batch_id",
  1104  					Error: api.HexInvalidByteError('G').Error(),
  1105  				},
  1106  			},
  1107  		},
  1108  	}, {
  1109  		name:    "batch_id - invalid length",
  1110  		batchID: "1234",
  1111  		depth:   "1",
  1112  		want: jsonhttp.StatusResponse{
  1113  			Code:    http.StatusBadRequest,
  1114  			Message: "invalid path params",
  1115  			Reasons: []jsonhttp.Reason{
  1116  				{
  1117  					Field: "batch_id",
  1118  					Error: "want len:32",
  1119  				},
  1120  			},
  1121  		},
  1122  	}, {
  1123  		name:    "depth - invalid syntax",
  1124  		batchID: hex.EncodeToString([]byte{31: 0}),
  1125  		depth:   "a",
  1126  		want: jsonhttp.StatusResponse{
  1127  			Code:    http.StatusBadRequest,
  1128  			Message: "invalid path params",
  1129  			Reasons: []jsonhttp.Reason{
  1130  				{
  1131  					Field: "depth",
  1132  					Error: strconv.ErrSyntax.Error(),
  1133  				},
  1134  			},
  1135  		},
  1136  	}}
  1137  
  1138  	//nolint:paralleltest
  1139  	for _, tc := range tests {
  1140  		t.Run(tc.name, func(t *testing.T) {
  1141  			jsonhttptest.Request(t, client, http.MethodPatch, "/stamps/dilute/"+tc.batchID+"/"+tc.depth, tc.want.Code,
  1142  				jsonhttptest.WithExpectedJSONResponse(tc.want),
  1143  			)
  1144  		})
  1145  	}
  1146  }