From d75b8825fb4ffcf5df0ffb93f96146462d548f83 Mon Sep 17 00:00:00 2001 From: Ralph Pichler Date: Tue, 26 Jan 2021 16:07:32 +0100 Subject: [PATCH] postage: add create endpoint (#1142) --- cmd/bee/cmd/cmd.go | 70 +++---- cmd/bee/cmd/start.go | 2 +- cmd/internal/file/io_test.go | 2 +- pkg/api/api.go | 47 ++--- pkg/api/api_test.go | 6 +- pkg/api/export_test.go | 1 + pkg/api/postage.go | 52 +++++ pkg/api/postage_test.go | 95 +++++++++ pkg/api/router.go | 7 + pkg/node/node.go | 75 ++++---- pkg/postage/postagecontract/contract.go | 191 +++++++++++++++++++ pkg/postage/postagecontract/contract_test.go | 113 +++++++++++ pkg/postage/postagecontract/export_test.go | 10 + pkg/postage/postagecontract/mock/contract.go | 40 ++++ 14 files changed, 616 insertions(+), 95 deletions(-) create mode 100644 pkg/api/postage.go create mode 100644 pkg/api/postage_test.go create mode 100644 pkg/postage/postagecontract/contract.go create mode 100644 pkg/postage/postagecontract/contract_test.go create mode 100644 pkg/postage/postagecontract/export_test.go create mode 100644 pkg/postage/postagecontract/mock/contract.go diff --git a/cmd/bee/cmd/cmd.go b/cmd/bee/cmd/cmd.go index 6c36431d7bf..3bee0cd9c07 100644 --- a/cmd/bee/cmd/cmd.go +++ b/cmd/bee/cmd/cmd.go @@ -17,40 +17,40 @@ import ( ) const ( - optionNameDataDir = "data-dir" - optionNameDBCapacity = "db-capacity" - optionNamePassword = "password" - optionNamePasswordFile = "password-file" - optionNameAPIAddr = "api-addr" - optionNameP2PAddr = "p2p-addr" - optionNameNATAddr = "nat-addr" - optionNameP2PWSEnable = "p2p-ws-enable" - optionNameP2PQUICEnable = "p2p-quic-enable" - optionNameDebugAPIEnable = "debug-api-enable" - optionNameDebugAPIAddr = "debug-api-addr" - optionNameBootnodes = "bootnode" - optionNameNetworkID = "network-id" - optionWelcomeMessage = "welcome-message" - optionCORSAllowedOrigins = "cors-allowed-origins" - optionNameStandalone = "standalone" - optionNameTracingEnabled = "tracing-enable" - optionNameTracingEndpoint = "tracing-endpoint" - optionNameTracingServiceName = "tracing-service-name" - optionNameVerbosity = "verbosity" - optionNameGlobalPinningEnabled = "global-pinning-enable" - optionNamePaymentThreshold = "payment-threshold" - optionNamePaymentTolerance = "payment-tolerance" - optionNamePaymentEarly = "payment-early" - optionNameResolverEndpoints = "resolver-options" - optionNameGatewayMode = "gateway-mode" - optionNameClefSignerEnable = "clef-signer-enable" - optionNameClefSignerEndpoint = "clef-signer-endpoint" - optionNameSwapEndpoint = "swap-endpoint" - optionNameSwapFactoryAddress = "swap-factory-address" - optionNameSwapInitialDeposit = "swap-initial-deposit" - optionNameSwapEnable = "swap-enable" - optionNamePostageStampAddress = "postage-stamp-address" - optionNamePriceOracleAddress = "price-oracle-address" + optionNameDataDir = "data-dir" + optionNameDBCapacity = "db-capacity" + optionNamePassword = "password" + optionNamePasswordFile = "password-file" + optionNameAPIAddr = "api-addr" + optionNameP2PAddr = "p2p-addr" + optionNameNATAddr = "nat-addr" + optionNameP2PWSEnable = "p2p-ws-enable" + optionNameP2PQUICEnable = "p2p-quic-enable" + optionNameDebugAPIEnable = "debug-api-enable" + optionNameDebugAPIAddr = "debug-api-addr" + optionNameBootnodes = "bootnode" + optionNameNetworkID = "network-id" + optionWelcomeMessage = "welcome-message" + optionCORSAllowedOrigins = "cors-allowed-origins" + optionNameStandalone = "standalone" + optionNameTracingEnabled = "tracing-enable" + optionNameTracingEndpoint = "tracing-endpoint" + optionNameTracingServiceName = "tracing-service-name" + optionNameVerbosity = "verbosity" + optionNameGlobalPinningEnabled = "global-pinning-enable" + optionNamePaymentThreshold = "payment-threshold" + optionNamePaymentTolerance = "payment-tolerance" + optionNamePaymentEarly = "payment-early" + optionNameResolverEndpoints = "resolver-options" + optionNameGatewayMode = "gateway-mode" + optionNameClefSignerEnable = "clef-signer-enable" + optionNameClefSignerEndpoint = "clef-signer-endpoint" + optionNameSwapEndpoint = "swap-endpoint" + optionNameSwapFactoryAddress = "swap-factory-address" + optionNameSwapInitialDeposit = "swap-initial-deposit" + optionNameSwapEnable = "swap-enable" + optionNamePostageContractAddress = "postage-stamp-address" + optionNamePriceOracleAddress = "price-oracle-address" ) func init() { @@ -206,6 +206,6 @@ func (c *command) setAllFlags(cmd *cobra.Command) { cmd.Flags().String(optionNameSwapFactoryAddress, "", "swap factory address") cmd.Flags().Uint64(optionNameSwapInitialDeposit, 100000000, "initial deposit if deploying a new chequebook") cmd.Flags().Bool(optionNameSwapEnable, true, "enable swap") - cmd.Flags().String(optionNamePostageStampAddress, "", "postage stamp address") + cmd.Flags().String(optionNamePostageContractAddress, "", "postage stamp contract address") cmd.Flags().String(optionNamePriceOracleAddress, "", "price oracle address") } diff --git a/cmd/bee/cmd/start.go b/cmd/bee/cmd/start.go index e50ce9e2ce2..26ae409036f 100644 --- a/cmd/bee/cmd/start.go +++ b/cmd/bee/cmd/start.go @@ -146,7 +146,7 @@ Welcome to the Swarm.... Bzzz Bzzzz Bzzzz SwapFactoryAddress: c.config.GetString(optionNameSwapFactoryAddress), SwapInitialDeposit: c.config.GetUint64(optionNameSwapInitialDeposit), SwapEnable: c.config.GetBool(optionNameSwapEnable), - PostageStampAddress: c.config.GetString(optionNamePostageStampAddress), + PostageContractAddress: c.config.GetString(optionNamePostageContractAddress), PriceOracleAddress: c.config.GetString(optionNamePriceOracleAddress), }) if err != nil { diff --git a/cmd/internal/file/io_test.go b/cmd/internal/file/io_test.go index b126e79067e..04c1f4554eb 100644 --- a/cmd/internal/file/io_test.go +++ b/cmd/internal/file/io_test.go @@ -161,7 +161,7 @@ func newTestServer(t *testing.T, storer storage.Storer) *url.URL { signer := crypto.NewDefaultSigner(pk) mockPostage := mockpost.New() - s := api.New(tags.NewTags(store, logger), storer, nil, nil, nil, mockPostage, signer, logger, nil, api.Options{}) + s := api.New(tags.NewTags(store, logger), storer, nil, nil, nil, mockPostage, nil, signer, logger, nil, api.Options{}) ts := httptest.NewServer(s) srvUrl, err := url.Parse(ts.URL) if err != nil { diff --git a/pkg/api/api.go b/pkg/api/api.go index 29930d6235d..d821abb372a 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -22,6 +22,7 @@ import ( "github.com/ethersphere/bee/pkg/logging" m "github.com/ethersphere/bee/pkg/metrics" "github.com/ethersphere/bee/pkg/postage" + "github.com/ethersphere/bee/pkg/postage/postagecontract" "github.com/ethersphere/bee/pkg/pss" "github.com/ethersphere/bee/pkg/resolver" "github.com/ethersphere/bee/pkg/storage" @@ -66,15 +67,16 @@ type Service interface { } type server struct { - Tags *tags.Tags - Storer storage.Storer - Resolver resolver.Interface - Pss pss.Interface - Traversal traversal.Service - Logger logging.Logger - Tracer *tracing.Tracer - signer crypto.Signer - post postage.Service + Tags *tags.Tags + Storer storage.Storer + Resolver resolver.Interface + Pss pss.Interface + Traversal traversal.Service + Logger logging.Logger + Tracer *tracing.Tracer + signer crypto.Signer + post postage.Service + postageContract postagecontract.Interface Options http.Handler metrics metrics @@ -95,20 +97,21 @@ const ( ) // New will create a and initialize a new API service. -func New(tags *tags.Tags, storer storage.Storer, resolver resolver.Interface, pss pss.Interface, traversalService traversal.Service, post postage.Service, signer crypto.Signer, logger logging.Logger, tracer *tracing.Tracer, o Options) Service { +func New(tags *tags.Tags, storer storage.Storer, resolver resolver.Interface, pss pss.Interface, traversalService traversal.Service, post postage.Service, postageContract postagecontract.Interface, signer crypto.Signer, logger logging.Logger, tracer *tracing.Tracer, o Options) Service { s := &server{ - Tags: tags, - Storer: storer, - Resolver: resolver, - Pss: pss, - Traversal: traversalService, - post: post, - signer: signer, - Options: o, - Logger: logger, - Tracer: tracer, - metrics: newMetrics(), - quit: make(chan struct{}), + Tags: tags, + Storer: storer, + Resolver: resolver, + Pss: pss, + Traversal: traversalService, + post: post, + postageContract: postageContract, + signer: signer, + Options: o, + Logger: logger, + Tracer: tracer, + metrics: newMetrics(), + quit: make(chan struct{}), } s.setupRouting() diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index c17b222df39..31617b98b15 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -19,6 +19,7 @@ import ( "github.com/ethersphere/bee/pkg/jsonhttp/jsonhttptest" "github.com/ethersphere/bee/pkg/logging" mockpost "github.com/ethersphere/bee/pkg/postage/mock" + "github.com/ethersphere/bee/pkg/postage/postagecontract" "github.com/ethersphere/bee/pkg/pss" "github.com/ethersphere/bee/pkg/resolver" resolverMock "github.com/ethersphere/bee/pkg/resolver/mock" @@ -37,6 +38,7 @@ type testServerOptions struct { Resolver resolver.Interface Pss pss.Interface Traversal traversal.Service + PostageContract postagecontract.Interface WsPath string Tags *tags.Tags GatewayMode bool @@ -59,7 +61,7 @@ func newTestServer(t *testing.T, o testServerOptions) (*http.Client, *websocket. if o.WsPingPeriod == 0 { o.WsPingPeriod = 60 * time.Second } - s := api.New(o.Tags, o.Storer, o.Resolver, o.Pss, o.Traversal, mockPostage, signer, o.Logger, nil, api.Options{ + s := api.New(o.Tags, o.Storer, o.Resolver, o.Pss, o.Traversal, mockPostage, o.PostageContract, signer, o.Logger, nil, api.Options{ GatewayMode: o.GatewayMode, WsPingPeriod: o.WsPingPeriod, }) @@ -160,7 +162,7 @@ func TestParseName(t *testing.T) { signer := crypto.NewDefaultSigner(pk) mockPostage := mockpost.New() - s := api.New(nil, nil, tC.res, nil, nil, mockPostage, signer, log, nil, api.Options{}).(*api.Server) + s := api.New(nil, nil, tC.res, nil, nil, mockPostage, nil, signer, log, nil, api.Options{}).(*api.Server) t.Run(tC.desc, func(t *testing.T) { got, err := s.ResolveNameOrAddress(tC.name) diff --git a/pkg/api/export_test.go b/pkg/api/export_test.go index 2c2408613c8..14eed35bc58 100644 --- a/pkg/api/export_test.go +++ b/pkg/api/export_test.go @@ -16,6 +16,7 @@ type ( PinnedChunk = pinnedChunk ListPinnedChunksResponse = listPinnedChunksResponse UpdatePinCounter = updatePinCounter + PostageCreateResponse = postageCreateResponse ) var ( diff --git a/pkg/api/postage.go b/pkg/api/postage.go new file mode 100644 index 00000000000..bb4ec79e574 --- /dev/null +++ b/pkg/api/postage.go @@ -0,0 +1,52 @@ +// Copyright 2021 The Swarm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package api + +import ( + "math/big" + "net/http" + "strconv" + + "github.com/ethersphere/bee/pkg/jsonhttp" + "github.com/gorilla/mux" +) + +type postageCreateResponse struct { + BatchID []byte `json:"batchID"` +} + +func (s *server) postageCreateHandler(w http.ResponseWriter, r *http.Request) { + depthStr := mux.Vars(r)["depth"] + + amount, ok := big.NewInt(0).SetString(mux.Vars(r)["amount"], 10) + if !ok { + s.Logger.Error("create batch: invalid amount") + jsonhttp.BadRequest(w, "invalid postage amount") + return + + } + + depth, err := strconv.ParseUint(depthStr, 10, 8) + if err != nil { + s.Logger.Debugf("create batch: invalid depth: %v", err) + s.Logger.Error("create batch: invalid depth") + jsonhttp.BadRequest(w, "invalid depth") + return + } + + label := r.URL.Query().Get("label") + + batchID, err := s.postageContract.CreateBatch(r.Context(), amount, uint8(depth), label) + if err != nil { + s.Logger.Debugf("create batch: failed to create: %v", err) + s.Logger.Error("create batch: failed to create") + jsonhttp.InternalServerError(w, "cannot create batch") + return + } + + jsonhttp.OK(w, &postageCreateResponse{ + BatchID: batchID, + }) +} diff --git a/pkg/api/postage_test.go b/pkg/api/postage_test.go new file mode 100644 index 00000000000..d7179fb0c05 --- /dev/null +++ b/pkg/api/postage_test.go @@ -0,0 +1,95 @@ +// Copyright 2021 The Swarm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package api_test + +import ( + "context" + "errors" + "fmt" + "math/big" + "net/http" + "testing" + + "github.com/ethersphere/bee/pkg/api" + "github.com/ethersphere/bee/pkg/jsonhttp" + "github.com/ethersphere/bee/pkg/jsonhttp/jsonhttptest" + contractMock "github.com/ethersphere/bee/pkg/postage/postagecontract/mock" +) + +func TestPostageCreateStamp(t *testing.T) { + batchID := []byte{1, 2, 3, 4} + initialBalance := int64(1000) + depth := uint8(1) + label := "label" + createBatch := func(amount int64, depth uint8, label string) string { + return fmt.Sprintf("/stamps/%d/%d?label=%s", amount, depth, label) + } + + t.Run("ok", func(t *testing.T) { + contract := contractMock.New( + contractMock.WithCreateBatchFunc(func(ctx context.Context, ib *big.Int, d uint8, l string) ([]byte, error) { + if ib.Cmp(big.NewInt(initialBalance)) != 0 { + return nil, fmt.Errorf("called with wrong initial balance. wanted %d, got %d", initialBalance, ib) + } + if d != depth { + return nil, fmt.Errorf("called with wrong depth. wanted %d, got %d", depth, d) + } + if l != label { + return nil, fmt.Errorf("called with wrong label. wanted %s, got %s", label, l) + } + return batchID, nil + }), + ) + client, _, _ := newTestServer(t, testServerOptions{ + PostageContract: contract, + }) + + jsonhttptest.Request(t, client, http.MethodPost, createBatch(initialBalance, depth, label), http.StatusOK, + jsonhttptest.WithExpectedJSONResponse(&api.PostageCreateResponse{ + BatchID: batchID, + }), + ) + }) + + t.Run("with-error", func(t *testing.T) { + contract := contractMock.New( + contractMock.WithCreateBatchFunc(func(ctx context.Context, ib *big.Int, d uint8, l string) ([]byte, error) { + return nil, errors.New("err") + }), + ) + client, _, _ := newTestServer(t, testServerOptions{ + PostageContract: contract, + }) + + jsonhttptest.Request(t, client, http.MethodPost, createBatch(initialBalance, depth, label), http.StatusInternalServerError, + jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{ + Code: http.StatusInternalServerError, + Message: "cannot create batch", + }), + ) + }) + + t.Run("invalid depth", func(t *testing.T) { + client, _, _ := newTestServer(t, testServerOptions{}) + + jsonhttptest.Request(t, client, http.MethodPost, "/stamps/1000/ab", http.StatusBadRequest, + jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{ + Code: http.StatusBadRequest, + Message: "invalid depth", + }), + ) + }) + + t.Run("invalid balance", func(t *testing.T) { + client, _, _ := newTestServer(t, testServerOptions{}) + + jsonhttptest.Request(t, client, http.MethodPost, "/stamps/abcd/2", http.StatusBadRequest, + jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{ + Code: http.StatusBadRequest, + Message: "invalid postage amount", + }), + ) + }) +} diff --git a/pkg/api/router.go b/pkg/api/router.go index 242719b35f8..96ee80e32a2 100644 --- a/pkg/api/router.go +++ b/pkg/api/router.go @@ -170,6 +170,13 @@ func (s *server) setupRouting() { })), ) + handle(router, "/stamps/{amount}/{depth}", web.ChainHandlers( + s.gatewayModeForbidEndpointHandler, + web.FinalHandler(jsonhttp.MethodHandler{ + "POST": http.HandlerFunc(s.postageCreateHandler), + })), + ) + s.Handler = web.ChainHandlers( httpaccess.NewHTTPAccessLogHandler(s.Logger, logrus.InfoLevel, s.Tracer, "api access"), handlers.CompressHandler, diff --git a/pkg/node/node.go b/pkg/node/node.go index a02ca162941..a79ab525728 100644 --- a/pkg/node/node.go +++ b/pkg/node/node.go @@ -11,6 +11,7 @@ import ( "fmt" "io" "log" + "math/big" "net" "net/http" "path/filepath" @@ -35,6 +36,7 @@ import ( "github.com/ethersphere/bee/pkg/postage/batchservice" "github.com/ethersphere/bee/pkg/postage/batchstore" "github.com/ethersphere/bee/pkg/postage/listener" + "github.com/ethersphere/bee/pkg/postage/postagecontract" "github.com/ethersphere/bee/pkg/pricing" "github.com/ethersphere/bee/pkg/pss" "github.com/ethersphere/bee/pkg/puller" @@ -111,7 +113,7 @@ type Options struct { SwapFactoryAddress string SwapInitialDeposit uint64 SwapEnable bool - PostageStampAddress string + PostageContractAddress string PriceOracleAddress string } @@ -150,19 +152,18 @@ func NewBee(addr string, swarmAddress swarm.Address, publicKey ecdsa.PublicKey, var chequeStore chequebook.ChequeStore var cashoutService chequebook.CashoutService var overlayEthAddress common.Address - if o.SwapEnable { - swapBackend, err := ethclient.Dial(o.SwapEndpoint) - if err != nil { - return nil, err - } + var erc20Address common.Address + var transactionService transaction.Service + var swapBackend *ethclient.Client + var chainID *big.Int - chainID, err := swapBackend.ChainID(p2pCtx) + if !o.Standalone { + swapBackend, err = ethclient.Dial(o.SwapEndpoint) if err != nil { - logger.Infof("could not connect to backend at %v. A working blockchain node (for goerli network in production) is required. Check your node or specify another node using --swap-endpoint.", o.SwapEndpoint) - return nil, fmt.Errorf("could not get chain id from ethereum backend: %w", err) + return nil, err } - transactionService, err := transaction.NewService(logger, swapBackend, signer) + transactionService, err = transaction.NewService(logger, swapBackend, signer) if err != nil { return nil, err } @@ -171,6 +172,14 @@ func NewBee(addr string, swarmAddress swarm.Address, publicKey ecdsa.PublicKey, return nil, err } + chainID, err = swapBackend.ChainID(p2pCtx) + if err != nil { + logger.Errorf("could not connect to backend at %v. A working blockchain node (for goerli network in production) is required. Check your node or specify another node using --swap-endpoint.", o.SwapEndpoint) + return nil, fmt.Errorf("could not get chain id from ethereum backend: %w", err) + } + } + + if !o.Standalone && o.SwapEnable { var factoryAddress common.Address if o.SwapFactoryAddress == "" { var found bool @@ -216,28 +225,25 @@ func NewBee(addr string, swarmAddress swarm.Address, publicKey ecdsa.PublicKey, if err != nil { return nil, err } - } - - batchStore := batchstore.New(stateStore) - if !o.Standalone { - swapBackend, err := ethclient.Dial(o.SwapEndpoint) + erc20Address, err = chequebookFactory.ERC20Address(p2pCtx) if err != nil { return nil, err } + } - chainID, err := swapBackend.ChainID(p2pCtx) - if err != nil { - logger.Infof("could not connect to backend at %v. A working blockchain node (for goerli network in production) is required. Check your node or specify another node using --swap-endpoint.", o.SwapEndpoint) - return nil, fmt.Errorf("could not get chain id from ethereum backend: %w", err) - } + batchStore := batchstore.New(stateStore) - postageStampAddress, priceOracleAddress, found := listener.DiscoverAddresses(chainID.Int64()) - if o.PostageStampAddress != "" { - if !common.IsHexAddress(o.PostageStampAddress) { + post := postage.NewService(stateStore, chainID.Int64()) + + var postageContractService postagecontract.Interface + if !o.Standalone { + postageContractAddress, priceOracleAddress, found := listener.DiscoverAddresses(chainID.Int64()) + if o.PostageContractAddress != "" { + if !common.IsHexAddress(o.PostageContractAddress) { return nil, errors.New("malformed postage stamp address") } - postageStampAddress = common.HexToAddress(o.PostageStampAddress) + postageContractAddress = common.HexToAddress(o.PostageContractAddress) } if o.PriceOracleAddress != "" { if !common.IsHexAddress(o.PriceOracleAddress) { @@ -245,11 +251,11 @@ func NewBee(addr string, swarmAddress swarm.Address, publicKey ecdsa.PublicKey, } priceOracleAddress = common.HexToAddress(o.PriceOracleAddress) } - if (o.PostageStampAddress == "" || o.PriceOracleAddress == "") && !found { + if (o.PostageContractAddress == "" || o.PriceOracleAddress == "") && !found { return nil, errors.New("no known postage stamp addresses for this network") } - eventListener := listener.New(logger, swapBackend, postageStampAddress, priceOracleAddress) + eventListener := listener.New(logger, swapBackend, postageContractAddress, priceOracleAddress) b.listenerCloser = eventListener batchService, err := batchservice.New(batchStore, logger, eventListener) @@ -260,6 +266,14 @@ func NewBee(addr string, swarmAddress swarm.Address, publicKey ecdsa.PublicKey, if err != nil { return nil, err } + + postageContractService = postagecontract.New( + overlayEthAddress, + postageContractAddress, + erc20Address, + transactionService, + post, + ) } p2ps, err := libp2p.New(p2pCtx, signer, networkID, swarmAddress, addr, addressbook, stateStore, logger, tracer, libp2p.Options{ @@ -438,17 +452,10 @@ func NewBee(addr string, swarmAddress swarm.Address, publicKey ecdsa.PublicKey, ) b.resolverCloser = multiResolver - post := postage.NewService(stateStore, 1) // do we have a config for this? which chain id are we using?? - - // this is needed until postage API gets wired in (since our actual API - // falls back to a 32 byte slice of zeros as batch id - fallbackBatch := make([]byte, 32) - post.Add(postage.NewStampIssuer("empty batch", "", fallbackBatch, 32, 8)) - var apiService api.Service if o.APIAddr != "" { // API server - apiService = api.New(tagService, ns, multiResolver, pssService, traversalService, post, signer, logger, tracer, api.Options{ + apiService = api.New(tagService, ns, multiResolver, pssService, traversalService, post, postageContractService, signer, logger, tracer, api.Options{ CORSAllowedOrigins: o.CORSAllowedOrigins, GatewayMode: o.GatewayMode, WsPingPeriod: 60 * time.Second, diff --git a/pkg/postage/postagecontract/contract.go b/pkg/postage/postagecontract/contract.go new file mode 100644 index 00000000000..8637a403db7 --- /dev/null +++ b/pkg/postage/postagecontract/contract.go @@ -0,0 +1,191 @@ +// Copyright 2021 The Swarm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package postagecontract + +import ( + "context" + "crypto/rand" + "errors" + "fmt" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethersphere/bee/pkg/postage" + "github.com/ethersphere/bee/pkg/postage/listener" + "github.com/ethersphere/bee/pkg/settlement/swap/transaction" + "github.com/ethersphere/sw3-bindings/v2/simpleswapfactory" +) + +var ( + postageStampABI = parseABI(listener.PostageStampABIJSON) + erc20ABI = parseABI(simpleswapfactory.ERC20ABI) + batchCreatedTopic = postageStampABI.Events["BatchCreated"].ID + + ErrBatchCreate = errors.New("batch creation failed") +) + +type Interface interface { + CreateBatch(ctx context.Context, initialBalance *big.Int, depth uint8, label string) ([]byte, error) +} + +type postageContract struct { + owner common.Address + postageContractAddress common.Address + bzzTokenAddress common.Address + transactionService transaction.Service + postageService postage.Service +} + +func New( + owner common.Address, + postageContractAddress common.Address, + bzzTokenAddress common.Address, + transactionService transaction.Service, + postageService postage.Service, +) Interface { + return &postageContract{ + owner: owner, + postageContractAddress: postageContractAddress, + bzzTokenAddress: bzzTokenAddress, + transactionService: transactionService, + postageService: postageService, + } +} + +func (c *postageContract) sendApproveTransaction(ctx context.Context, amount *big.Int) (*types.Receipt, error) { + callData, err := erc20ABI.Pack("approve", c.postageContractAddress, amount) + if err != nil { + return nil, err + } + + txHash, err := c.transactionService.Send(ctx, &transaction.TxRequest{ + To: c.bzzTokenAddress, + Data: callData, + GasPrice: nil, + GasLimit: 0, + Value: big.NewInt(0), + }) + 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) sendCreateBatchTransaction(ctx context.Context, owner common.Address, initialBalance *big.Int, depth uint8, nonce common.Hash) (*types.Receipt, error) { + callData, err := postageStampABI.Pack("createBatch", owner, initialBalance, depth, nonce) + if err != nil { + return nil, err + } + + request := &transaction.TxRequest{ + To: c.postageContractAddress, + Data: callData, + GasPrice: nil, + GasLimit: 0, + Value: big.NewInt(0), + } + + 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) CreateBatch(ctx context.Context, initialBalance *big.Int, depth uint8, label string) ([]byte, error) { + _, err := c.sendApproveTransaction(ctx, big.NewInt(0).Mul(initialBalance, big.NewInt(int64(1<