Commit 81988b16 authored by aloknerurkar's avatar aloknerurkar Committed by GitHub

feat(debugapi, postage): topup batch handling (#2401)

parent 136d9f2c
......@@ -871,3 +871,40 @@ paths:
$ref: "SwarmCommon.yaml#/components/responses/500"
default:
description: Default response
"/stamps/topup/{id}/{amount}":
patch:
summary: Top up an existing postage batch.
description: Be aware, this endpoint creates on-chain transactions and transfers BZZ from the node's Ethereum account and hence directly manipulates the wallet balance!
tags:
- Postage Stamps
parameters:
- in: path
name: id
schema:
$ref: "SwarmCommon.yaml#/components/schemas/BatchID"
required: true
description: Batch ID to top up
- in: path
name: amount
schema:
type: integer
required: true
description: Amount of BZZ per chunk to top up to an existing postage batch.
responses:
"202":
description: Returns the postage batch ID that was topped up
content:
application/json:
schema:
$ref: "SwarmCommon.yaml#/components/schemas/BatchIDResponse"
"400":
$ref: "SwarmCommon.yaml#/components/responses/400"
"429":
$ref: "SwarmCommon.yaml#/components/responses/429"
"402":
$ref: "SwarmCommon.yaml#/components/responses/402"
"500":
$ref: "SwarmCommon.yaml#/components/responses/500"
default:
description: Default response
......@@ -321,3 +321,63 @@ func (s *Service) estimateBatchTTL(id []byte) (int64, error) {
return ttl.Int64(), nil
}
func (s *Service) postageTopUpHandler(w http.ResponseWriter, r *http.Request) {
idStr := mux.Vars(r)["id"]
if idStr == "" || len(idStr) != 64 {
s.logger.Error("topup batch: invalid batchID")
jsonhttp.BadRequest(w, "invalid batchID")
return
}
id, err := hex.DecodeString(idStr)
if err != nil {
s.logger.Debugf("topup batch: invalid batchID: %v", err)
s.logger.Error("topup batch: invalid batchID")
jsonhttp.BadRequest(w, "invalid batchID")
return
}
amount, ok := big.NewInt(0).SetString(mux.Vars(r)["amount"], 10)
if !ok {
s.logger.Error("topup batch: invalid amount")
jsonhttp.BadRequest(w, "invalid postage amount")
return
}
ctx := r.Context()
if price, ok := r.Header[gasPriceHeader]; ok {
p, ok := big.NewInt(0).SetString(price[0], 10)
if !ok {
s.logger.Error("topup batch: bad gas price")
jsonhttp.BadRequest(w, errBadGasPrice)
return
}
ctx = sctx.SetGasPrice(ctx, p)
}
if !s.postageCreateSem.TryAcquire(1) {
s.logger.Debug("topup batch: simultaneous on-chain operations not supported")
s.logger.Error("topup batch: simultaneous on-chain operations not supported")
jsonhttp.TooManyRequests(w, "simultaneous on-chain operations not supported")
return
}
defer s.postageCreateSem.Release(1)
err = s.postageContract.TopUpBatch(ctx, id, amount)
if err != nil {
if errors.Is(err, postagecontract.ErrInsufficientFunds) {
s.logger.Debugf("topup batch: out of funds: %v", err)
s.logger.Error("topup batch: out of funds")
jsonhttp.PaymentRequired(w, "out of funds")
return
}
s.logger.Debugf("topup batch: failed to create: %v", err)
s.logger.Error("topup batch: failed to create")
jsonhttp.InternalServerError(w, "cannot topup batch")
return
}
jsonhttp.Accepted(w, &postageCreateResponse{
BatchID: id,
})
}
......@@ -5,6 +5,7 @@
package debugapi_test
import (
"bytes"
"context"
"encoding/hex"
"errors"
......@@ -368,3 +369,120 @@ func TestChainState(t *testing.T) {
)
})
}
func TestPostageTopUpStamp(t *testing.T) {
topupAmount := int64(1000)
topupBatch := func(id string, amount int64) string {
return fmt.Sprintf("/stamps/topup/%s/%d", id, amount)
}
t.Run("ok", func(t *testing.T) {
contract := contractMock.New(
contractMock.WithTopUpBatchFunc(func(ctx context.Context, id []byte, ib *big.Int) error {
if !bytes.Equal(id, batchOk) {
return errors.New("incorrect batch ID in call")
}
if ib.Cmp(big.NewInt(topupAmount)) != 0 {
return fmt.Errorf("called with wrong topup amount. wanted %d, got %d", topupAmount, ib)
}
return nil
}),
)
ts := newTestServer(t, testServerOptions{
PostageContract: contract,
})
jsonhttptest.Request(t, ts.Client, http.MethodPatch, topupBatch(batchOkStr, topupAmount), http.StatusAccepted,
jsonhttptest.WithExpectedJSONResponse(&debugapi.PostageCreateResponse{
BatchID: batchOk,
}),
)
})
t.Run("with-custom-gas", func(t *testing.T) {
contract := contractMock.New(
contractMock.WithTopUpBatchFunc(func(ctx context.Context, id []byte, ib *big.Int) error {
if !bytes.Equal(id, batchOk) {
return errors.New("incorrect batch ID in call")
}
if ib.Cmp(big.NewInt(topupAmount)) != 0 {
return fmt.Errorf("called with wrong topup amount. wanted %d, got %d", topupAmount, ib)
}
if sctx.GetGasPrice(ctx).Cmp(big.NewInt(10000)) != 0 {
return fmt.Errorf("called with wrong gas price. wanted %d, got %d", 10000, sctx.GetGasPrice(ctx))
}
return nil
}),
)
ts := newTestServer(t, testServerOptions{
PostageContract: contract,
})
jsonhttptest.Request(t, ts.Client, http.MethodPatch, topupBatch(batchOkStr, topupAmount), http.StatusAccepted,
jsonhttptest.WithRequestHeader("Gas-Price", "10000"),
jsonhttptest.WithExpectedJSONResponse(&debugapi.PostageCreateResponse{
BatchID: batchOk,
}),
)
})
t.Run("with-error", func(t *testing.T) {
contract := contractMock.New(
contractMock.WithTopUpBatchFunc(func(ctx context.Context, id []byte, ib *big.Int) error {
return errors.New("err")
}),
)
ts := newTestServer(t, testServerOptions{
PostageContract: contract,
})
jsonhttptest.Request(t, ts.Client, http.MethodPatch, topupBatch(batchOkStr, topupAmount), http.StatusInternalServerError,
jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{
Code: http.StatusInternalServerError,
Message: "cannot topup batch",
}),
)
})
t.Run("out-of-funds", func(t *testing.T) {
contract := contractMock.New(
contractMock.WithTopUpBatchFunc(func(ctx context.Context, id []byte, ib *big.Int) error {
return postagecontract.ErrInsufficientFunds
}),
)
ts := newTestServer(t, testServerOptions{
PostageContract: contract,
})
jsonhttptest.Request(t, ts.Client, http.MethodPatch, topupBatch(batchOkStr, topupAmount), http.StatusPaymentRequired,
jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{
Code: http.StatusPaymentRequired,
Message: "out of funds",
}),
)
})
t.Run("invalid batch id", func(t *testing.T) {
ts := newTestServer(t, testServerOptions{})
jsonhttptest.Request(t, ts.Client, http.MethodPatch, "/stamps/topup/abcd/2", http.StatusBadRequest,
jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{
Code: http.StatusBadRequest,
Message: "invalid batchID",
}),
)
})
t.Run("invalid amount", func(t *testing.T) {
ts := newTestServer(t, testServerOptions{})
wrongURL := fmt.Sprintf("/stamps/topup/%s/amount", batchOkStr)
jsonhttptest.Request(t, ts.Client, http.MethodPatch, wrongURL, http.StatusBadRequest,
jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{
Code: http.StatusBadRequest,
Message: "invalid postage amount",
}),
)
})
}
......@@ -210,6 +210,12 @@ func (s *Service) newRouter() *mux.Router {
})),
)
router.Handle("/stamps/topup/{id}/{amount}", web.ChainHandlers(
web.FinalHandler(jsonhttp.MethodHandler{
"PATCH": http.HandlerFunc(s.postageTopUpHandler),
})),
)
return router
}
......
......@@ -200,28 +200,52 @@ func NewDevBee(logger logging.Logger, o *DevOptions) (b *DevBee, err error) {
}
post := mockPost.New()
postageContract := mockPostContract.New(mockPostContract.WithCreateBatchFunc(
func(ctx context.Context, initialBalance *big.Int, depth uint8, immutable bool, label string) ([]byte, error) {
id := postagetesting.MustNewID()
b := &postage.Batch{
ID: id,
Owner: overlayEthAddress.Bytes(),
Value: big.NewInt(0),
Depth: depth,
Immutable: immutable,
}
err := batchStore.Put(b, initialBalance, depth)
if err != nil {
return nil, err
}
stampIssuer := postage.NewStampIssuer(label, string(overlayEthAddress.Bytes()), id, initialBalance, depth, 0, 0, immutable)
post.Add(stampIssuer)
return id, nil
},
))
postageContract := mockPostContract.New(
mockPostContract.WithCreateBatchFunc(
func(ctx context.Context, initialBalance *big.Int, depth uint8, immutable bool, label string) ([]byte, error) {
id := postagetesting.MustNewID()
b := &postage.Batch{
ID: id,
Owner: overlayEthAddress.Bytes(),
Value: big.NewInt(0),
Depth: depth,
Immutable: immutable,
}
totalAmount := big.NewInt(0).Mul(initialBalance, big.NewInt(int64(1<<depth)))
err := batchStore.Put(b, totalAmount, depth)
if err != nil {
return nil, err
}
stampIssuer := postage.NewStampIssuer(label, string(overlayEthAddress.Bytes()), id, totalAmount, depth, 0, 0, immutable)
post.Add(stampIssuer)
return id, nil
},
),
mockPostContract.WithTopUpBatchFunc(
func(ctx context.Context, batchID []byte, topupAmount *big.Int) error {
batch, err := batchStore.Get(batchID)
if err != nil {
return err
}
totalAmount := big.NewInt(0).Mul(topupAmount, big.NewInt(int64(1<<batch.Depth)))
newBalance := big.NewInt(0).Add(totalAmount, batch.Value)
err = batchStore.Put(batch, newBalance, batch.Depth)
if err != nil {
return err
}
post.HandleTopUp(batch.ID, newBalance)
return nil
},
),
)
feedFactory := factory.New(storer)
......
......@@ -464,6 +464,7 @@ func NewBee(addr string, publicKey *ecdsa.PublicKey, signer crypto.Signer, netwo
erc20Address,
transactionService,
post,
batchStore,
)
if natManager := p2ps.NATManager(); natManager != nil {
......
......@@ -29,7 +29,7 @@ type batchService struct {
logger logging.Logger
listener postage.Listener
owner []byte
batchListener postage.BatchCreationListener
batchListener postage.BatchEventListener
checksum hash.Hash // checksum hasher
resync bool
......@@ -46,7 +46,7 @@ func New(
logger logging.Logger,
listener postage.Listener,
owner []byte,
batchListener postage.BatchCreationListener,
batchListener postage.BatchEventListener,
checksumFunc func() hash.Hash,
resync bool,
) (Interface, error) {
......@@ -110,8 +110,9 @@ func (svc *batchService) Create(id, owner []byte, normalisedBalance *big.Int, de
}
if bytes.Equal(svc.owner, owner) && svc.batchListener != nil {
svc.batchListener.Handle(b)
svc.batchListener.HandleCreate(b)
}
cs, err := svc.updateChecksum(txHash)
if err != nil {
return fmt.Errorf("update checksum: %w", err)
......@@ -133,6 +134,11 @@ func (svc *batchService) TopUp(id []byte, normalisedBalance *big.Int, txHash []b
if err != nil {
return fmt.Errorf("put: %w", err)
}
if bytes.Equal(svc.owner, b.Owner) && svc.batchListener != nil {
svc.batchListener.HandleTopUp(id, normalisedBalance)
}
cs, err := svc.updateChecksum(txHash)
if err != nil {
return fmt.Errorf("update checksum: %w", err)
......
......@@ -38,12 +38,17 @@ func newMockListener() *mockListener {
return &mockListener{}
}
type mockBatchCreationHandler struct {
count int
type mockBatchListener struct {
createCount int
topupCount int
}
func (m *mockBatchCreationHandler) Handle(b *postage.Batch) {
m.count++
func (m *mockBatchListener) HandleCreate(b *postage.Batch) {
m.createCount++
}
func (m *mockBatchListener) HandleTopUp(_ []byte, _ *big.Int) {
m.topupCount++
}
func TestBatchServiceCreate(t *testing.T) {
......@@ -51,7 +56,7 @@ func TestBatchServiceCreate(t *testing.T) {
t.Run("expect put create put error", func(t *testing.T) {
testBatch := postagetesting.MustNewBatch()
testBatchListener := &mockBatchCreationHandler{}
testBatchListener := &mockBatchListener{}
svc, _, _ := newTestStoreAndServiceWithListener(
t,
testBatch.Owner,
......@@ -71,8 +76,8 @@ func TestBatchServiceCreate(t *testing.T) {
); err == nil {
t.Fatalf("expected error")
}
if testBatchListener.count != 0 {
t.Fatalf("unexpected batch listener count, exp %d found %d", 0, testBatchListener.count)
if testBatchListener.createCount != 0 {
t.Fatalf("unexpected batch listener count, exp %d found %d", 0, testBatchListener.createCount)
}
})
......@@ -107,7 +112,7 @@ func TestBatchServiceCreate(t *testing.T) {
t.Run("passes", func(t *testing.T) {
testBatch := postagetesting.MustNewBatch()
testBatchListener := &mockBatchCreationHandler{}
testBatchListener := &mockBatchListener{}
svc, batchStore, _ := newTestStoreAndServiceWithListener(
t,
testBatch.Owner,
......@@ -126,8 +131,8 @@ func TestBatchServiceCreate(t *testing.T) {
); err != nil {
t.Fatalf("got error %v", err)
}
if testBatchListener.count != 1 {
t.Fatalf("unexpected batch listener count, exp %d found %d", 1, testBatchListener.count)
if testBatchListener.createCount != 1 {
t.Fatalf("unexpected batch listener count, exp %d found %d", 1, testBatchListener.createCount)
}
validateBatch(t, testBatch, batchStore)
......@@ -135,7 +140,7 @@ func TestBatchServiceCreate(t *testing.T) {
t.Run("passes without recovery", func(t *testing.T) {
testBatch := postagetesting.MustNewBatch()
testBatchListener := &mockBatchCreationHandler{}
testBatchListener := &mockBatchListener{}
// create a owner different from the batch owner
owner := make([]byte, 32)
rand.Read(owner)
......@@ -158,8 +163,8 @@ func TestBatchServiceCreate(t *testing.T) {
); err != nil {
t.Fatalf("got error %v", err)
}
if testBatchListener.count != 0 {
t.Fatalf("unexpected batch listener count, exp %d found %d", 1, testBatchListener.count)
if testBatchListener.createCount != 0 {
t.Fatalf("unexpected batch listener count, exp %d found %d", 0, testBatchListener.createCount)
}
validateBatch(t, testBatch, batchStore)
......@@ -171,19 +176,28 @@ func TestBatchServiceTopUp(t *testing.T) {
testNormalisedBalance := big.NewInt(2000000000000)
t.Run("expect get error", func(t *testing.T) {
svc, _, _ := newTestStoreAndService(
testBatchListener := &mockBatchListener{}
svc, _, _ := newTestStoreAndServiceWithListener(
t,
testBatch.Owner,
testBatchListener,
mock.WithGetErr(errTest, 0),
)
if err := svc.TopUp(testBatch.ID, testNormalisedBalance, testTxHash); err == nil {
t.Fatal("expected error")
}
if testBatchListener.topupCount != 0 {
t.Fatalf("unexpected batch listener count, exp %d found %d", 0, testBatchListener.topupCount)
}
})
t.Run("expect put error", func(t *testing.T) {
svc, batchStore, _ := newTestStoreAndService(
testBatchListener := &mockBatchListener{}
svc, batchStore, _ := newTestStoreAndServiceWithListener(
t,
testBatch.Owner,
testBatchListener,
mock.WithPutErr(errTest, 1),
)
putBatch(t, batchStore, testBatch)
......@@ -191,10 +205,18 @@ func TestBatchServiceTopUp(t *testing.T) {
if err := svc.TopUp(testBatch.ID, testNormalisedBalance, testTxHash); err == nil {
t.Fatal("expected error")
}
if testBatchListener.topupCount != 0 {
t.Fatalf("unexpected batch listener count, exp %d found %d", 0, testBatchListener.topupCount)
}
})
t.Run("passes", func(t *testing.T) {
svc, batchStore, _ := newTestStoreAndService(t)
testBatchListener := &mockBatchListener{}
svc, batchStore, _ := newTestStoreAndServiceWithListener(
t,
testBatch.Owner,
testBatchListener,
)
putBatch(t, batchStore, testBatch)
want := testNormalisedBalance
......@@ -211,6 +233,43 @@ func TestBatchServiceTopUp(t *testing.T) {
if got.Value.Cmp(want) != 0 {
t.Fatalf("topped up amount: got %v, want %v", got.Value, want)
}
if testBatchListener.topupCount != 1 {
t.Fatalf("unexpected batch listener count, exp %d found %d", 1, testBatchListener.topupCount)
}
})
// if a batch with a different owner is topped up we should not see any event fired in the
// batch service
t.Run("passes without BatchEventListener update", func(t *testing.T) {
testBatchListener := &mockBatchListener{}
// create a owner different from the batch owner
owner := make([]byte, 32)
rand.Read(owner)
svc, batchStore, _ := newTestStoreAndServiceWithListener(
t,
owner,
testBatchListener,
)
putBatch(t, batchStore, testBatch)
want := testNormalisedBalance
if err := svc.TopUp(testBatch.ID, testNormalisedBalance, testTxHash); err != nil {
t.Fatalf("top up: %v", err)
}
got, err := batchStore.Get(testBatch.ID)
if err != nil {
t.Fatalf("batch store get: %v", err)
}
if got.Value.Cmp(want) != 0 {
t.Fatalf("topped up amount: got %v, want %v", got.Value, want)
}
if testBatchListener.topupCount != 0 {
t.Fatalf("unexpected batch listener count, exp %d found %d", 0, testBatchListener.topupCount)
}
})
}
......@@ -435,7 +494,7 @@ func TestChecksumResync(t *testing.T) {
func newTestStoreAndServiceWithListener(
t *testing.T,
owner []byte,
batchListener postage.BatchCreationListener,
batchListener postage.BatchEventListener,
opts ...mock.Option,
) (postage.EventUpdater, *mock.BatchStore, storage.StateStorer) {
t.Helper()
......
......@@ -50,6 +50,7 @@ type Listener interface {
Listen(from uint64, updater EventUpdater) <-chan struct{}
}
type BatchCreationListener interface {
Handle(*Batch)
type BatchEventListener interface {
HandleCreate(*Batch)
HandleTopUp(id []byte, newBalance *big.Int)
}
......@@ -88,7 +88,9 @@ func (m *mockPostage) IssuerUsable(_ *postage.StampIssuer) bool {
return true
}
func (m *mockPostage) Handle(_ *postage.Batch) {}
func (m *mockPostage) HandleCreate(_ *postage.Batch) {}
func (m *mockPostage) HandleTopUp(_ []byte, _ *big.Int) {}
func (m *mockPostage) Close() error {
return nil
......
......@@ -28,14 +28,17 @@ var (
postageStampABI = parseABI(postageabi.PostageStampABIv0_3_0)
erc20ABI = parseABI(sw3abi.ERC20ABIv0_3_1)
batchCreatedTopic = postageStampABI.Events["BatchCreated"].ID
batchTopUpTopic = postageStampABI.Events["BatchTopUp"].ID
ErrBatchCreate = errors.New("batch creation failed")
ErrInsufficientFunds = errors.New("insufficient token balance")
ErrInvalidDepth = errors.New("invalid depth")
ErrBatchTopUp = errors.New("batch topUp failed")
)
type Interface interface {
CreateBatch(ctx context.Context, initialBalance *big.Int, depth uint8, immutable bool, label string) ([]byte, error)
TopUpBatch(ctx context.Context, batchID []byte, topupBalance *big.Int) error
}
type postageContract struct {
......@@ -44,6 +47,7 @@ type postageContract struct {
bzzTokenAddress common.Address
transactionService transaction.Service
postageService postage.Service
postageStorer postage.Storer
}
func New(
......@@ -52,6 +56,7 @@ func New(
bzzTokenAddress common.Address,
transactionService transaction.Service,
postageService postage.Service,
postageStorer postage.Storer,
) Interface {
return &postageContract{
owner: owner,
......@@ -59,6 +64,7 @@ func New(
bzzTokenAddress: bzzTokenAddress,
transactionService: transactionService,
postageService: postageService,
postageStorer: postageStorer,
}
}
......@@ -69,11 +75,12 @@ func (c *postageContract) sendApproveTransaction(ctx context.Context, amount *bi
}
txHash, err := c.transactionService.Send(ctx, &transaction.TxRequest{
To: &c.bzzTokenAddress,
Data: callData,
GasPrice: sctx.GetGasPrice(ctx),
GasLimit: 65000,
Value: big.NewInt(0),
To: &c.bzzTokenAddress,
Data: callData,
GasPrice: sctx.GetGasPrice(ctx),
GasLimit: 65000,
Value: big.NewInt(0),
Description: "Approve tokens for postage operations",
})
if err != nil {
return nil, err
......@@ -99,11 +106,12 @@ func (c *postageContract) sendCreateBatchTransaction(ctx context.Context, owner
}
request := &transaction.TxRequest{
To: &c.postageContractAddress,
Data: callData,
GasPrice: sctx.GetGasPrice(ctx),
GasLimit: 160000,
Value: big.NewInt(0),
To: &c.postageContractAddress,
Data: callData,
GasPrice: sctx.GetGasPrice(ctx),
GasLimit: 160000,
Value: big.NewInt(0),
Description: "Postage batch creation",
}
txHash, err := c.transactionService.Send(ctx, request)
......@@ -123,6 +131,39 @@ func (c *postageContract) sendCreateBatchTransaction(ctx context.Context, owner
return receipt, nil
}
func (c *postageContract) sendTopUpBatchTransaction(ctx context.Context, batchID []byte, topUpAmount *big.Int) (*types.Receipt, error) {
callData, err := postageStampABI.Pack("topUp", common.BytesToHash(batchID), topUpAmount)
if err != nil {
return nil, err
}
request := &transaction.TxRequest{
To: &c.postageContractAddress,
Data: callData,
GasPrice: sctx.GetGasPrice(ctx),
GasLimit: 160000,
Value: big.NewInt(0),
Description: "Postage batch top up",
}
txHash, err := c.transactionService.Send(ctx, request)
if err != nil {
return nil, err
}
receipt, err := c.transactionService.WaitForReceipt(ctx, txHash)
if err != nil {
return nil, err
}
if receipt.Status == 0 {
return nil, transaction.ErrTransactionReverted
}
return receipt, nil
}
func (c *postageContract) getBalance(ctx context.Context) (*big.Int, error) {
callData, err := erc20ABI.Pack("balanceOf", c.owner)
if err != nil {
......@@ -204,6 +245,42 @@ func (c *postageContract) CreateBatch(ctx context.Context, initialBalance *big.I
return nil, ErrBatchCreate
}
func (c *postageContract) TopUpBatch(ctx context.Context, batchID []byte, topUpAmount *big.Int) error {
batch, err := c.postageStorer.Get(batchID)
if err != nil {
return err
}
totalAmount := big.NewInt(0).Mul(topUpAmount, big.NewInt(int64(1<<batch.Depth)))
balance, err := c.getBalance(ctx)
if err != nil {
return err
}
if balance.Cmp(totalAmount) < 0 {
return ErrInsufficientFunds
}
_, err = c.sendApproveTransaction(ctx, totalAmount)
if err != nil {
return err
}
receipt, err := c.sendTopUpBatchTransaction(ctx, batch.ID, topUpAmount)
if err != nil {
return err
}
for _, ev := range receipt.Logs {
if ev.Address == c.postageContractAddress && len(ev.Topics) > 0 && ev.Topics[0] == batchTopUpTopic {
return nil
}
}
return ErrBatchTopUp
}
type batchCreatedEvent struct {
BatchId [32]byte
TotalAmount *big.Int
......
......@@ -14,8 +14,11 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethersphere/bee/pkg/postage"
postagestoreMock "github.com/ethersphere/bee/pkg/postage/batchstore/mock"
postageMock "github.com/ethersphere/bee/pkg/postage/mock"
"github.com/ethersphere/bee/pkg/postage/postagecontract"
postagetesting "github.com/ethersphere/bee/pkg/postage/testing"
"github.com/ethersphere/bee/pkg/transaction"
transactionMock "github.com/ethersphere/bee/pkg/transaction/mock"
)
......@@ -85,6 +88,7 @@ func TestCreateBatch(t *testing.T) {
}),
),
postageMock,
postagestoreMock.New(),
)
returnedID, err := contract.CreateBatch(ctx, initialBalance, depth, false, label)
......@@ -115,6 +119,7 @@ func TestCreateBatch(t *testing.T) {
bzzTokenAddress,
transactionMock.New(),
postageMock.New(),
postagestoreMock.New(),
)
_, err := contract.CreateBatch(ctx, initialBalance, depth, false, label)
......@@ -140,6 +145,7 @@ func TestCreateBatch(t *testing.T) {
}),
),
postageMock.New(),
postagestoreMock.New(),
)
_, err := contract.CreateBatch(ctx, initialBalance, depth, false, label)
......@@ -192,3 +198,157 @@ func TestLookupERC20Address(t *testing.T) {
t.Fatalf("got wrong erc20 address. wanted %v, got %v", erc20Address, addr)
}
}
func TestTopUpBatch(t *testing.T) {
defer func(b uint8) {
postagecontract.BucketDepth = b
}(postagecontract.BucketDepth)
postagecontract.BucketDepth = 9
owner := common.HexToAddress("abcd")
postageStampAddress := common.HexToAddress("ffff")
bzzTokenAddress := common.HexToAddress("eeee")
ctx := context.Background()
topupBalance := big.NewInt(100)
t.Run("ok", func(t *testing.T) {
totalAmount := big.NewInt(102400)
txHashApprove := common.HexToHash("abb0")
txHashTopup := common.HexToHash("c3a7")
batch := postagetesting.MustNewBatch(postagetesting.WithOwner(owner.Bytes()))
batch.Depth = uint8(10)
batch.BucketDepth = uint8(9)
postageMock := postageMock.New(postageMock.WithIssuer(postage.NewStampIssuer(
"label",
"keyID",
batch.ID,
batch.Value,
batch.Depth,
batch.BucketDepth,
batch.Start,
batch.Immutable,
)))
batchStoreMock := postagestoreMock.New(postagestoreMock.WithBatch(batch))
expectedCallData, err := postagecontract.PostageStampABI.Pack("topUp", common.BytesToHash(batch.ID), topupBalance)
if err != nil {
t.Fatal(err)
}
contract := postagecontract.New(
owner,
postageStampAddress,
bzzTokenAddress,
transactionMock.New(
transactionMock.WithSendFunc(func(ctx context.Context, request *transaction.TxRequest) (txHash common.Hash, err error) {
if *request.To == bzzTokenAddress {
return txHashApprove, nil
} else if *request.To == postageStampAddress {
if !bytes.Equal(expectedCallData[:64], request.Data[:64]) {
return common.Hash{}, fmt.Errorf("got wrong call data. wanted %x, got %x", expectedCallData, request.Data)
}
return txHashTopup, 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
} else if txHash == txHashTopup {
return &types.Receipt{
Logs: []*types.Log{
newTopUpEvent(postageStampAddress, batch),
},
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 == bzzTokenAddress {
return totalAmount.FillBytes(make([]byte, 32)), nil
}
return nil, errors.New("unexpected call")
}),
),
postageMock,
batchStoreMock,
)
err = contract.TopUpBatch(ctx, batch.ID, topupBalance)
if err != nil {
t.Fatal(err)
}
si, err := postageMock.GetStampIssuer(batch.ID)
if err != nil {
t.Fatal(err)
}
if si == nil {
t.Fatal("stamp issuer not set")
}
})
t.Run("batch doesnt exist", func(t *testing.T) {
errNotFound := errors.New("not found")
contract := postagecontract.New(
owner,
postageStampAddress,
bzzTokenAddress,
transactionMock.New(),
postageMock.New(),
postagestoreMock.New(postagestoreMock.WithGetErr(errNotFound, 0)),
)
err := contract.TopUpBatch(ctx, postagetesting.MustNewID(), topupBalance)
if !errors.Is(err, errNotFound) {
t.Fatal("expected error on topup of non existent batch")
}
})
t.Run("insufficient funds", func(t *testing.T) {
totalAmount := big.NewInt(102399)
batch := postagetesting.MustNewBatch(postagetesting.WithOwner(owner.Bytes()))
batchStoreMock := postagestoreMock.New(postagestoreMock.WithBatch(batch))
contract := postagecontract.New(
owner,
postageStampAddress,
bzzTokenAddress,
transactionMock.New(
transactionMock.WithCallFunc(func(ctx context.Context, request *transaction.TxRequest) (result []byte, err error) {
if *request.To == bzzTokenAddress {
return big.NewInt(0).Sub(totalAmount, big.NewInt(1)).FillBytes(make([]byte, 32)), nil
}
return nil, errors.New("unexpected call")
}),
),
postageMock.New(),
batchStoreMock,
)
err := contract.TopUpBatch(ctx, batch.ID, topupBalance)
if !errors.Is(err, postagecontract.ErrInsufficientFunds) {
t.Fatalf("expected error %v. got %v", postagecontract.ErrInsufficientFunds, err)
}
})
}
func newTopUpEvent(postageContractAddress common.Address, batch *postage.Batch) *types.Log {
b, err := postagecontract.PostageStampABI.Events["BatchTopUp"].Inputs.NonIndexed().Pack(
big.NewInt(0),
big.NewInt(0),
)
if err != nil {
panic(err)
}
return &types.Log{
Address: postageContractAddress,
Data: b,
Topics: []common.Hash{postagecontract.BatchTopUpTopic, common.BytesToHash(batch.ID)},
BlockNumber: batch.Start + 1,
}
}
......@@ -7,4 +7,5 @@ package postagecontract
var (
PostageStampABI = postageStampABI
BatchCreatedTopic = batchCreatedTopic
BatchTopUpTopic = batchTopUpTopic
)
......@@ -13,12 +13,17 @@ import (
type contractMock struct {
createBatch func(ctx context.Context, initialBalance *big.Int, depth uint8, immutable bool, label string) ([]byte, error)
topupBatch func(ctx context.Context, id []byte, amount *big.Int) error
}
func (c *contractMock) CreateBatch(ctx context.Context, initialBalance *big.Int, depth uint8, immutable bool, label string) ([]byte, error) {
return c.createBatch(ctx, initialBalance, depth, immutable, label)
}
func (c *contractMock) TopUpBatch(ctx context.Context, batchID []byte, amount *big.Int) error {
return c.topupBatch(ctx, batchID, amount)
}
// Option is a an option passed to New
type Option func(*contractMock)
......@@ -38,3 +43,9 @@ func WithCreateBatchFunc(f func(ctx context.Context, initialBalance *big.Int, de
m.createBatch = f
}
}
func WithTopUpBatchFunc(f func(ctx context.Context, batchID []byte, amount *big.Int) error) Option {
return func(m *contractMock) {
m.topupBatch = f
}
}
......@@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"io"
"math/big"
"sync"
"github.com/ethersphere/bee/pkg/storage"
......@@ -34,7 +35,7 @@ type Service interface {
StampIssuers() []*StampIssuer
GetStampIssuer([]byte) (*StampIssuer, error)
IssuerUsable(*StampIssuer) bool
BatchCreationListener
BatchEventListener
io.Closer
}
......@@ -87,10 +88,10 @@ func (ps *service) Add(st *StampIssuer) {
ps.issuers = append(ps.issuers, st)
}
// Handle implements the BatchCreationListener interface. This is fired on receiving
// HandleCreate implements the BatchEventListener interface. This is fired on receiving
// a batch creation event from the blockchain listener to ensure that if a stamp
// issuer was not created initially, we will create it here.
func (ps *service) Handle(b *Batch) {
func (ps *service) HandleCreate(b *Batch) {
ps.Add(NewStampIssuer(
"recovered",
string(b.Owner),
......@@ -103,6 +104,22 @@ func (ps *service) Handle(b *Batch) {
))
}
// HandleTopUp implements the BatchEventListener interface. This is fired on receiving
// a batch topup event from the blockchain to update stampissuer details
func (ps *service) HandleTopUp(batchID []byte, newValue *big.Int) {
ps.lock.Lock()
defer ps.lock.Unlock()
for _, v := range ps.issuers {
if bytes.Equal(batchID, v.data.BatchID) {
if newValue.Cmp(v.data.BatchAmount) > 0 {
v.data.BatchAmount = newValue
}
return
}
}
}
// StampIssuers returns the currently active stamp issuers.
func (ps *service) StampIssuers() []*StampIssuer {
ps.lock.Lock()
......
......@@ -83,9 +83,6 @@ func TestGetStampIssuer(t *testing.T) {
}
ps.Add(postage.NewStampIssuer(string(id), "", id, big.NewInt(3), 16, 8, validBlockNumber+shift, true))
}
b := postagetesting.MustNewBatch()
b.Start = validBlockNumber
ps.Handle(b)
t.Run("found", func(t *testing.T) {
for _, id := range ids[1:4] {
st, err := ps.GetStampIssuer(id)
......@@ -112,6 +109,9 @@ func TestGetStampIssuer(t *testing.T) {
}
})
t.Run("recovered", func(t *testing.T) {
b := postagetesting.MustNewBatch()
b.Start = validBlockNumber
ps.HandleCreate(b)
st, err := ps.GetStampIssuer(b.ID)
if err != nil {
t.Fatalf("expected no error, got %v", err)
......@@ -120,4 +120,14 @@ func TestGetStampIssuer(t *testing.T) {
t.Fatal("wrong issuer returned")
}
})
t.Run("topup", func(t *testing.T) {
ps.HandleTopUp(ids[1], big.NewInt(10))
_, err := ps.GetStampIssuer(ids[1])
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if ps.StampIssuers()[0].Amount().Cmp(big.NewInt(10)) != 0 {
t.Fatalf("expected amount %d got %d", 10, ps.StampIssuers()[0].Amount().Int64())
}
})
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment