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: ...@@ -871,3 +871,40 @@ paths:
$ref: "SwarmCommon.yaml#/components/responses/500" $ref: "SwarmCommon.yaml#/components/responses/500"
default: default:
description: Default response 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) { ...@@ -321,3 +321,63 @@ func (s *Service) estimateBatchTTL(id []byte) (int64, error) {
return ttl.Int64(), nil 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 @@ ...@@ -5,6 +5,7 @@
package debugapi_test package debugapi_test
import ( import (
"bytes"
"context" "context"
"encoding/hex" "encoding/hex"
"errors" "errors"
...@@ -368,3 +369,120 @@ func TestChainState(t *testing.T) { ...@@ -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 { ...@@ -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 return router
} }
......
...@@ -200,28 +200,52 @@ func NewDevBee(logger logging.Logger, o *DevOptions) (b *DevBee, err error) { ...@@ -200,28 +200,52 @@ func NewDevBee(logger logging.Logger, o *DevOptions) (b *DevBee, err error) {
} }
post := mockPost.New() post := mockPost.New()
postageContract := mockPostContract.New(mockPostContract.WithCreateBatchFunc( postageContract := mockPostContract.New(
func(ctx context.Context, initialBalance *big.Int, depth uint8, immutable bool, label string) ([]byte, error) { mockPostContract.WithCreateBatchFunc(
id := postagetesting.MustNewID() func(ctx context.Context, initialBalance *big.Int, depth uint8, immutable bool, label string) ([]byte, error) {
b := &postage.Batch{ id := postagetesting.MustNewID()
ID: id, b := &postage.Batch{
Owner: overlayEthAddress.Bytes(), ID: id,
Value: big.NewInt(0), Owner: overlayEthAddress.Bytes(),
Depth: depth, Value: big.NewInt(0),
Immutable: immutable, Depth: depth,
} Immutable: immutable,
}
err := batchStore.Put(b, initialBalance, depth)
if err != nil { totalAmount := big.NewInt(0).Mul(initialBalance, big.NewInt(int64(1<<depth)))
return nil, err
} err := batchStore.Put(b, totalAmount, depth)
if err != nil {
stampIssuer := postage.NewStampIssuer(label, string(overlayEthAddress.Bytes()), id, initialBalance, depth, 0, 0, immutable) return nil, err
post.Add(stampIssuer) }
return id, nil 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) feedFactory := factory.New(storer)
......
...@@ -464,6 +464,7 @@ func NewBee(addr string, publicKey *ecdsa.PublicKey, signer crypto.Signer, netwo ...@@ -464,6 +464,7 @@ func NewBee(addr string, publicKey *ecdsa.PublicKey, signer crypto.Signer, netwo
erc20Address, erc20Address,
transactionService, transactionService,
post, post,
batchStore,
) )
if natManager := p2ps.NATManager(); natManager != nil { if natManager := p2ps.NATManager(); natManager != nil {
......
...@@ -29,7 +29,7 @@ type batchService struct { ...@@ -29,7 +29,7 @@ type batchService struct {
logger logging.Logger logger logging.Logger
listener postage.Listener listener postage.Listener
owner []byte owner []byte
batchListener postage.BatchCreationListener batchListener postage.BatchEventListener
checksum hash.Hash // checksum hasher checksum hash.Hash // checksum hasher
resync bool resync bool
...@@ -46,7 +46,7 @@ func New( ...@@ -46,7 +46,7 @@ func New(
logger logging.Logger, logger logging.Logger,
listener postage.Listener, listener postage.Listener,
owner []byte, owner []byte,
batchListener postage.BatchCreationListener, batchListener postage.BatchEventListener,
checksumFunc func() hash.Hash, checksumFunc func() hash.Hash,
resync bool, resync bool,
) (Interface, error) { ) (Interface, error) {
...@@ -110,8 +110,9 @@ func (svc *batchService) Create(id, owner []byte, normalisedBalance *big.Int, de ...@@ -110,8 +110,9 @@ func (svc *batchService) Create(id, owner []byte, normalisedBalance *big.Int, de
} }
if bytes.Equal(svc.owner, owner) && svc.batchListener != nil { if bytes.Equal(svc.owner, owner) && svc.batchListener != nil {
svc.batchListener.Handle(b) svc.batchListener.HandleCreate(b)
} }
cs, err := svc.updateChecksum(txHash) cs, err := svc.updateChecksum(txHash)
if err != nil { if err != nil {
return fmt.Errorf("update checksum: %w", err) return fmt.Errorf("update checksum: %w", err)
...@@ -133,6 +134,11 @@ func (svc *batchService) TopUp(id []byte, normalisedBalance *big.Int, txHash []b ...@@ -133,6 +134,11 @@ func (svc *batchService) TopUp(id []byte, normalisedBalance *big.Int, txHash []b
if err != nil { if err != nil {
return fmt.Errorf("put: %w", err) 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) cs, err := svc.updateChecksum(txHash)
if err != nil { if err != nil {
return fmt.Errorf("update checksum: %w", err) return fmt.Errorf("update checksum: %w", err)
......
...@@ -38,12 +38,17 @@ func newMockListener() *mockListener { ...@@ -38,12 +38,17 @@ func newMockListener() *mockListener {
return &mockListener{} return &mockListener{}
} }
type mockBatchCreationHandler struct { type mockBatchListener struct {
count int createCount int
topupCount int
} }
func (m *mockBatchCreationHandler) Handle(b *postage.Batch) { func (m *mockBatchListener) HandleCreate(b *postage.Batch) {
m.count++ m.createCount++
}
func (m *mockBatchListener) HandleTopUp(_ []byte, _ *big.Int) {
m.topupCount++
} }
func TestBatchServiceCreate(t *testing.T) { func TestBatchServiceCreate(t *testing.T) {
...@@ -51,7 +56,7 @@ 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) { t.Run("expect put create put error", func(t *testing.T) {
testBatch := postagetesting.MustNewBatch() testBatch := postagetesting.MustNewBatch()
testBatchListener := &mockBatchCreationHandler{} testBatchListener := &mockBatchListener{}
svc, _, _ := newTestStoreAndServiceWithListener( svc, _, _ := newTestStoreAndServiceWithListener(
t, t,
testBatch.Owner, testBatch.Owner,
...@@ -71,8 +76,8 @@ func TestBatchServiceCreate(t *testing.T) { ...@@ -71,8 +76,8 @@ func TestBatchServiceCreate(t *testing.T) {
); err == nil { ); err == nil {
t.Fatalf("expected error") t.Fatalf("expected error")
} }
if testBatchListener.count != 0 { if testBatchListener.createCount != 0 {
t.Fatalf("unexpected batch listener count, exp %d found %d", 0, testBatchListener.count) t.Fatalf("unexpected batch listener count, exp %d found %d", 0, testBatchListener.createCount)
} }
}) })
...@@ -107,7 +112,7 @@ func TestBatchServiceCreate(t *testing.T) { ...@@ -107,7 +112,7 @@ func TestBatchServiceCreate(t *testing.T) {
t.Run("passes", func(t *testing.T) { t.Run("passes", func(t *testing.T) {
testBatch := postagetesting.MustNewBatch() testBatch := postagetesting.MustNewBatch()
testBatchListener := &mockBatchCreationHandler{} testBatchListener := &mockBatchListener{}
svc, batchStore, _ := newTestStoreAndServiceWithListener( svc, batchStore, _ := newTestStoreAndServiceWithListener(
t, t,
testBatch.Owner, testBatch.Owner,
...@@ -126,8 +131,8 @@ func TestBatchServiceCreate(t *testing.T) { ...@@ -126,8 +131,8 @@ func TestBatchServiceCreate(t *testing.T) {
); err != nil { ); err != nil {
t.Fatalf("got error %v", err) t.Fatalf("got error %v", err)
} }
if testBatchListener.count != 1 { if testBatchListener.createCount != 1 {
t.Fatalf("unexpected batch listener count, exp %d found %d", 1, testBatchListener.count) t.Fatalf("unexpected batch listener count, exp %d found %d", 1, testBatchListener.createCount)
} }
validateBatch(t, testBatch, batchStore) validateBatch(t, testBatch, batchStore)
...@@ -135,7 +140,7 @@ func TestBatchServiceCreate(t *testing.T) { ...@@ -135,7 +140,7 @@ func TestBatchServiceCreate(t *testing.T) {
t.Run("passes without recovery", func(t *testing.T) { t.Run("passes without recovery", func(t *testing.T) {
testBatch := postagetesting.MustNewBatch() testBatch := postagetesting.MustNewBatch()
testBatchListener := &mockBatchCreationHandler{} testBatchListener := &mockBatchListener{}
// create a owner different from the batch owner // create a owner different from the batch owner
owner := make([]byte, 32) owner := make([]byte, 32)
rand.Read(owner) rand.Read(owner)
...@@ -158,8 +163,8 @@ func TestBatchServiceCreate(t *testing.T) { ...@@ -158,8 +163,8 @@ func TestBatchServiceCreate(t *testing.T) {
); err != nil { ); err != nil {
t.Fatalf("got error %v", err) t.Fatalf("got error %v", err)
} }
if testBatchListener.count != 0 { if testBatchListener.createCount != 0 {
t.Fatalf("unexpected batch listener count, exp %d found %d", 1, testBatchListener.count) t.Fatalf("unexpected batch listener count, exp %d found %d", 0, testBatchListener.createCount)
} }
validateBatch(t, testBatch, batchStore) validateBatch(t, testBatch, batchStore)
...@@ -171,19 +176,28 @@ func TestBatchServiceTopUp(t *testing.T) { ...@@ -171,19 +176,28 @@ func TestBatchServiceTopUp(t *testing.T) {
testNormalisedBalance := big.NewInt(2000000000000) testNormalisedBalance := big.NewInt(2000000000000)
t.Run("expect get error", func(t *testing.T) { t.Run("expect get error", func(t *testing.T) {
svc, _, _ := newTestStoreAndService( testBatchListener := &mockBatchListener{}
svc, _, _ := newTestStoreAndServiceWithListener(
t, t,
testBatch.Owner,
testBatchListener,
mock.WithGetErr(errTest, 0), mock.WithGetErr(errTest, 0),
) )
if err := svc.TopUp(testBatch.ID, testNormalisedBalance, testTxHash); err == nil { if err := svc.TopUp(testBatch.ID, testNormalisedBalance, testTxHash); err == nil {
t.Fatal("expected error") 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) { t.Run("expect put error", func(t *testing.T) {
svc, batchStore, _ := newTestStoreAndService( testBatchListener := &mockBatchListener{}
svc, batchStore, _ := newTestStoreAndServiceWithListener(
t, t,
testBatch.Owner,
testBatchListener,
mock.WithPutErr(errTest, 1), mock.WithPutErr(errTest, 1),
) )
putBatch(t, batchStore, testBatch) putBatch(t, batchStore, testBatch)
...@@ -191,10 +205,18 @@ func TestBatchServiceTopUp(t *testing.T) { ...@@ -191,10 +205,18 @@ func TestBatchServiceTopUp(t *testing.T) {
if err := svc.TopUp(testBatch.ID, testNormalisedBalance, testTxHash); err == nil { if err := svc.TopUp(testBatch.ID, testNormalisedBalance, testTxHash); err == nil {
t.Fatal("expected error") 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) { t.Run("passes", func(t *testing.T) {
svc, batchStore, _ := newTestStoreAndService(t) testBatchListener := &mockBatchListener{}
svc, batchStore, _ := newTestStoreAndServiceWithListener(
t,
testBatch.Owner,
testBatchListener,
)
putBatch(t, batchStore, testBatch) putBatch(t, batchStore, testBatch)
want := testNormalisedBalance want := testNormalisedBalance
...@@ -211,6 +233,43 @@ func TestBatchServiceTopUp(t *testing.T) { ...@@ -211,6 +233,43 @@ func TestBatchServiceTopUp(t *testing.T) {
if got.Value.Cmp(want) != 0 { if got.Value.Cmp(want) != 0 {
t.Fatalf("topped up amount: got %v, want %v", got.Value, want) 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) { ...@@ -435,7 +494,7 @@ func TestChecksumResync(t *testing.T) {
func newTestStoreAndServiceWithListener( func newTestStoreAndServiceWithListener(
t *testing.T, t *testing.T,
owner []byte, owner []byte,
batchListener postage.BatchCreationListener, batchListener postage.BatchEventListener,
opts ...mock.Option, opts ...mock.Option,
) (postage.EventUpdater, *mock.BatchStore, storage.StateStorer) { ) (postage.EventUpdater, *mock.BatchStore, storage.StateStorer) {
t.Helper() t.Helper()
......
...@@ -50,6 +50,7 @@ type Listener interface { ...@@ -50,6 +50,7 @@ type Listener interface {
Listen(from uint64, updater EventUpdater) <-chan struct{} Listen(from uint64, updater EventUpdater) <-chan struct{}
} }
type BatchCreationListener interface { type BatchEventListener interface {
Handle(*Batch) HandleCreate(*Batch)
HandleTopUp(id []byte, newBalance *big.Int)
} }
...@@ -88,7 +88,9 @@ func (m *mockPostage) IssuerUsable(_ *postage.StampIssuer) bool { ...@@ -88,7 +88,9 @@ func (m *mockPostage) IssuerUsable(_ *postage.StampIssuer) bool {
return true 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 { func (m *mockPostage) Close() error {
return nil return nil
......
...@@ -28,14 +28,17 @@ var ( ...@@ -28,14 +28,17 @@ var (
postageStampABI = parseABI(postageabi.PostageStampABIv0_3_0) postageStampABI = parseABI(postageabi.PostageStampABIv0_3_0)
erc20ABI = parseABI(sw3abi.ERC20ABIv0_3_1) erc20ABI = parseABI(sw3abi.ERC20ABIv0_3_1)
batchCreatedTopic = postageStampABI.Events["BatchCreated"].ID batchCreatedTopic = postageStampABI.Events["BatchCreated"].ID
batchTopUpTopic = postageStampABI.Events["BatchTopUp"].ID
ErrBatchCreate = errors.New("batch creation failed") ErrBatchCreate = errors.New("batch creation failed")
ErrInsufficientFunds = errors.New("insufficient token balance") ErrInsufficientFunds = errors.New("insufficient token balance")
ErrInvalidDepth = errors.New("invalid depth") ErrInvalidDepth = errors.New("invalid depth")
ErrBatchTopUp = errors.New("batch topUp failed")
) )
type Interface interface { type Interface interface {
CreateBatch(ctx context.Context, initialBalance *big.Int, depth uint8, immutable bool, label string) ([]byte, error) 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 { type postageContract struct {
...@@ -44,6 +47,7 @@ type postageContract struct { ...@@ -44,6 +47,7 @@ type postageContract struct {
bzzTokenAddress common.Address bzzTokenAddress common.Address
transactionService transaction.Service transactionService transaction.Service
postageService postage.Service postageService postage.Service
postageStorer postage.Storer
} }
func New( func New(
...@@ -52,6 +56,7 @@ func New( ...@@ -52,6 +56,7 @@ func New(
bzzTokenAddress common.Address, bzzTokenAddress common.Address,
transactionService transaction.Service, transactionService transaction.Service,
postageService postage.Service, postageService postage.Service,
postageStorer postage.Storer,
) Interface { ) Interface {
return &postageContract{ return &postageContract{
owner: owner, owner: owner,
...@@ -59,6 +64,7 @@ func New( ...@@ -59,6 +64,7 @@ func New(
bzzTokenAddress: bzzTokenAddress, bzzTokenAddress: bzzTokenAddress,
transactionService: transactionService, transactionService: transactionService,
postageService: postageService, postageService: postageService,
postageStorer: postageStorer,
} }
} }
...@@ -69,11 +75,12 @@ func (c *postageContract) sendApproveTransaction(ctx context.Context, amount *bi ...@@ -69,11 +75,12 @@ func (c *postageContract) sendApproveTransaction(ctx context.Context, amount *bi
} }
txHash, err := c.transactionService.Send(ctx, &transaction.TxRequest{ txHash, err := c.transactionService.Send(ctx, &transaction.TxRequest{
To: &c.bzzTokenAddress, To: &c.bzzTokenAddress,
Data: callData, Data: callData,
GasPrice: sctx.GetGasPrice(ctx), GasPrice: sctx.GetGasPrice(ctx),
GasLimit: 65000, GasLimit: 65000,
Value: big.NewInt(0), Value: big.NewInt(0),
Description: "Approve tokens for postage operations",
}) })
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -99,11 +106,12 @@ func (c *postageContract) sendCreateBatchTransaction(ctx context.Context, owner ...@@ -99,11 +106,12 @@ func (c *postageContract) sendCreateBatchTransaction(ctx context.Context, owner
} }
request := &transaction.TxRequest{ request := &transaction.TxRequest{
To: &c.postageContractAddress, To: &c.postageContractAddress,
Data: callData, Data: callData,
GasPrice: sctx.GetGasPrice(ctx), GasPrice: sctx.GetGasPrice(ctx),
GasLimit: 160000, GasLimit: 160000,
Value: big.NewInt(0), Value: big.NewInt(0),
Description: "Postage batch creation",
} }
txHash, err := c.transactionService.Send(ctx, request) txHash, err := c.transactionService.Send(ctx, request)
...@@ -123,6 +131,39 @@ func (c *postageContract) sendCreateBatchTransaction(ctx context.Context, owner ...@@ -123,6 +131,39 @@ func (c *postageContract) sendCreateBatchTransaction(ctx context.Context, owner
return receipt, nil 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) { func (c *postageContract) getBalance(ctx context.Context) (*big.Int, error) {
callData, err := erc20ABI.Pack("balanceOf", c.owner) callData, err := erc20ABI.Pack("balanceOf", c.owner)
if err != nil { if err != nil {
...@@ -204,6 +245,42 @@ func (c *postageContract) CreateBatch(ctx context.Context, initialBalance *big.I ...@@ -204,6 +245,42 @@ func (c *postageContract) CreateBatch(ctx context.Context, initialBalance *big.I
return nil, ErrBatchCreate 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 { type batchCreatedEvent struct {
BatchId [32]byte BatchId [32]byte
TotalAmount *big.Int TotalAmount *big.Int
......
...@@ -14,8 +14,11 @@ import ( ...@@ -14,8 +14,11 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "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" postageMock "github.com/ethersphere/bee/pkg/postage/mock"
"github.com/ethersphere/bee/pkg/postage/postagecontract" "github.com/ethersphere/bee/pkg/postage/postagecontract"
postagetesting "github.com/ethersphere/bee/pkg/postage/testing"
"github.com/ethersphere/bee/pkg/transaction" "github.com/ethersphere/bee/pkg/transaction"
transactionMock "github.com/ethersphere/bee/pkg/transaction/mock" transactionMock "github.com/ethersphere/bee/pkg/transaction/mock"
) )
...@@ -85,6 +88,7 @@ func TestCreateBatch(t *testing.T) { ...@@ -85,6 +88,7 @@ func TestCreateBatch(t *testing.T) {
}), }),
), ),
postageMock, postageMock,
postagestoreMock.New(),
) )
returnedID, err := contract.CreateBatch(ctx, initialBalance, depth, false, label) returnedID, err := contract.CreateBatch(ctx, initialBalance, depth, false, label)
...@@ -115,6 +119,7 @@ func TestCreateBatch(t *testing.T) { ...@@ -115,6 +119,7 @@ func TestCreateBatch(t *testing.T) {
bzzTokenAddress, bzzTokenAddress,
transactionMock.New(), transactionMock.New(),
postageMock.New(), postageMock.New(),
postagestoreMock.New(),
) )
_, err := contract.CreateBatch(ctx, initialBalance, depth, false, label) _, err := contract.CreateBatch(ctx, initialBalance, depth, false, label)
...@@ -140,6 +145,7 @@ func TestCreateBatch(t *testing.T) { ...@@ -140,6 +145,7 @@ func TestCreateBatch(t *testing.T) {
}), }),
), ),
postageMock.New(), postageMock.New(),
postagestoreMock.New(),
) )
_, err := contract.CreateBatch(ctx, initialBalance, depth, false, label) _, err := contract.CreateBatch(ctx, initialBalance, depth, false, label)
...@@ -192,3 +198,157 @@ func TestLookupERC20Address(t *testing.T) { ...@@ -192,3 +198,157 @@ func TestLookupERC20Address(t *testing.T) {
t.Fatalf("got wrong erc20 address. wanted %v, got %v", erc20Address, addr) 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 ...@@ -7,4 +7,5 @@ package postagecontract
var ( var (
PostageStampABI = postageStampABI PostageStampABI = postageStampABI
BatchCreatedTopic = batchCreatedTopic BatchCreatedTopic = batchCreatedTopic
BatchTopUpTopic = batchTopUpTopic
) )
...@@ -13,12 +13,17 @@ import ( ...@@ -13,12 +13,17 @@ import (
type contractMock struct { type contractMock struct {
createBatch func(ctx context.Context, initialBalance *big.Int, depth uint8, immutable bool, label string) ([]byte, error) 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) { 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) 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 // Option is a an option passed to New
type Option func(*contractMock) type Option func(*contractMock)
...@@ -38,3 +43,9 @@ func WithCreateBatchFunc(f func(ctx context.Context, initialBalance *big.Int, de ...@@ -38,3 +43,9 @@ func WithCreateBatchFunc(f func(ctx context.Context, initialBalance *big.Int, de
m.createBatch = f 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 ( ...@@ -9,6 +9,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"math/big"
"sync" "sync"
"github.com/ethersphere/bee/pkg/storage" "github.com/ethersphere/bee/pkg/storage"
...@@ -34,7 +35,7 @@ type Service interface { ...@@ -34,7 +35,7 @@ type Service interface {
StampIssuers() []*StampIssuer StampIssuers() []*StampIssuer
GetStampIssuer([]byte) (*StampIssuer, error) GetStampIssuer([]byte) (*StampIssuer, error)
IssuerUsable(*StampIssuer) bool IssuerUsable(*StampIssuer) bool
BatchCreationListener BatchEventListener
io.Closer io.Closer
} }
...@@ -87,10 +88,10 @@ func (ps *service) Add(st *StampIssuer) { ...@@ -87,10 +88,10 @@ func (ps *service) Add(st *StampIssuer) {
ps.issuers = append(ps.issuers, st) 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 // a batch creation event from the blockchain listener to ensure that if a stamp
// issuer was not created initially, we will create it here. // 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( ps.Add(NewStampIssuer(
"recovered", "recovered",
string(b.Owner), string(b.Owner),
...@@ -103,6 +104,22 @@ func (ps *service) Handle(b *Batch) { ...@@ -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. // StampIssuers returns the currently active stamp issuers.
func (ps *service) StampIssuers() []*StampIssuer { func (ps *service) StampIssuers() []*StampIssuer {
ps.lock.Lock() ps.lock.Lock()
......
...@@ -83,9 +83,6 @@ func TestGetStampIssuer(t *testing.T) { ...@@ -83,9 +83,6 @@ func TestGetStampIssuer(t *testing.T) {
} }
ps.Add(postage.NewStampIssuer(string(id), "", id, big.NewInt(3), 16, 8, validBlockNumber+shift, true)) 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) { t.Run("found", func(t *testing.T) {
for _, id := range ids[1:4] { for _, id := range ids[1:4] {
st, err := ps.GetStampIssuer(id) st, err := ps.GetStampIssuer(id)
...@@ -112,6 +109,9 @@ func TestGetStampIssuer(t *testing.T) { ...@@ -112,6 +109,9 @@ func TestGetStampIssuer(t *testing.T) {
} }
}) })
t.Run("recovered", func(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) st, err := ps.GetStampIssuer(b.ID)
if err != nil { if err != nil {
t.Fatalf("expected no error, got %v", err) t.Fatalf("expected no error, got %v", err)
...@@ -120,4 +120,14 @@ func TestGetStampIssuer(t *testing.T) { ...@@ -120,4 +120,14 @@ func TestGetStampIssuer(t *testing.T) {
t.Fatal("wrong issuer returned") 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