Commit d2c668e9 authored by Ralph Pichler's avatar Ralph Pichler Committed by GitHub

feat: add various transaction endpoint and track pending transactions (#1469)

parent 20a93521
......@@ -438,6 +438,37 @@ components:
properties:
transactionHash:
$ref: "#/components/schemas/TransactionHash"
TransactionInfoResponse:
type: object
properties:
transactionHash:
$ref: "#/components/schemas/TransactionHash"
to:
$ref: "#/components/schemas/EthereumAddress"
nonce:
type: integer
gasPrice:
type: "#/components/schemas/BigInt"
gasLimit:
type: integer
data:
type: string
created:
type: "#/components/schemas/DateTime"
description:
type: string
value:
type: "#/components/schemas/BigInt"
PendingTransactionReponse:
type: object
properties:
pendingTransactions:
type: array
nullable: true
items:
$ref: "#/components/schemas/TransactionHash"
Uid:
type: integer
......
......@@ -669,3 +669,70 @@ paths:
$ref: "SwarmCommon.yaml#/components/responses/500"
default:
description: Default response
"/transaction":
get:
summary: Get list of pending transactions
tags:
- Transaction
responses:
"200":
description: List of pending transactions
content:
application/json:
schema:
$ref: "SwarmCommon.yaml#/components/schemas/PendingTransactionReponse"
"500":
$ref: "SwarmCommon.yaml#/components/responses/500"
default:
description: Default response
"/transaction/{txHash}":
get:
summary: Get information about a sent transaction
parameters:
- in: path
name: txHash
schema:
$ref: "SwarmCommon.yaml#/components/schemas/TransactionHash"
required: true
description: Hash of the transaction
tags:
- Transaction
responses:
"200":
description: Get info about transaction
content:
application/json:
schema:
$ref: "SwarmCommon.yaml#/components/schemas/TransactionInfoResponse"
"404":
$ref: "SwarmCommon.yaml#/components/responses/404"
"500":
$ref: "SwarmCommon.yaml#/components/responses/500"
default:
description: Default response
post:
summary: Rebroadcast existing transaction
parameters:
- in: path
name: txHash
schema:
$ref: "SwarmCommon.yaml#/components/schemas/TransactionHash"
required: true
description: Hash of the transaction
tags:
- Transaction
responses:
"200":
description: Hash of the transaction
content:
application/json:
schema:
$ref: "SwarmCommon.yaml#/components/schemas/TransactionResponse"
"404":
$ref: "SwarmCommon.yaml#/components/responses/404"
"500":
$ref: "SwarmCommon.yaml#/components/responses/500"
default:
description: Default response
\ No newline at end of file
......@@ -27,6 +27,7 @@ import (
"github.com/ethersphere/bee/pkg/topology"
"github.com/ethersphere/bee/pkg/topology/lightnode"
"github.com/ethersphere/bee/pkg/tracing"
"github.com/ethersphere/bee/pkg/transaction"
"github.com/prometheus/client_golang/prometheus"
)
......@@ -49,6 +50,7 @@ type Service struct {
chequebook chequebook.Service
swap swap.Interface
batchStore postage.Storer
transaction transaction.Service
corsAllowedOrigins []string
metricsRegistry *prometheus.Registry
lightNodes *lightnode.Container
......@@ -80,7 +82,7 @@ func New(overlay swarm.Address, publicKey, pssPublicKey ecdsa.PublicKey, ethereu
// Configure injects required dependencies and configuration parameters and
// constructs HTTP routes that depend on them. It is intended and safe to call
// this method only once.
func (s *Service) Configure(p2p p2p.DebugService, pingpong pingpong.Interface, topologyDriver topology.Driver, lightNodes *lightnode.Container, storer storage.Storer, tags *tags.Tags, accounting accounting.Interface, pseudosettle settlement.Interface, chequebookEnabled bool, swap swap.Interface, chequebook chequebook.Service, batchStore postage.Storer) {
func (s *Service) Configure(p2p p2p.DebugService, pingpong pingpong.Interface, topologyDriver topology.Driver, lightNodes *lightnode.Container, storer storage.Storer, tags *tags.Tags, accounting accounting.Interface, pseudosettle settlement.Interface, chequebookEnabled bool, swap swap.Interface, chequebook chequebook.Service, batchStore postage.Storer, transaction transaction.Service) {
s.p2p = p2p
s.pingpong = pingpong
s.topologyDriver = topologyDriver
......@@ -93,6 +95,7 @@ func (s *Service) Configure(p2p p2p.DebugService, pingpong pingpong.Interface, t
s.lightNodes = lightNodes
s.batchStore = batchStore
s.pseudosettle = pseudosettle
s.transaction = transaction
s.setRouter(s.newRouter())
}
......
......@@ -32,6 +32,7 @@ import (
"github.com/ethersphere/bee/pkg/tags"
"github.com/ethersphere/bee/pkg/topology/lightnode"
topologymock "github.com/ethersphere/bee/pkg/topology/mock"
transactionmock "github.com/ethersphere/bee/pkg/transaction/mock"
"github.com/multiformats/go-multiaddr"
"resenje.org/web"
)
......@@ -53,6 +54,7 @@ type testServerOptions struct {
ChequebookOpts []chequebookmock.Option
SwapOpts []swapmock.Option
BatchStore postage.Storer
TransactionOpts []transactionmock.Option
}
type testServer struct {
......@@ -66,9 +68,10 @@ func newTestServer(t *testing.T, o testServerOptions) *testServer {
settlement := swapmock.New(o.SettlementOpts...)
chequebook := chequebookmock.NewChequebook(o.ChequebookOpts...)
swapserv := swapmock.New(o.SwapOpts...)
transaction := transactionmock.New(o.TransactionOpts...)
ln := lightnode.NewContainer(o.Overlay)
s := debugapi.New(o.Overlay, o.PublicKey, o.PSSPublicKey, o.EthereumAddress, logging.New(ioutil.Discard, 0), nil, o.CORSAllowedOrigins)
s.Configure(o.P2P, o.Pingpong, topologyDriver, ln, o.Storer, o.Tags, acc, settlement, true, swapserv, chequebook, o.BatchStore)
s.Configure(o.P2P, o.Pingpong, topologyDriver, ln, o.Storer, o.Tags, acc, settlement, true, swapserv, chequebook, o.BatchStore, transaction)
ts := httptest.NewServer(s)
t.Cleanup(ts.Close)
......@@ -134,6 +137,7 @@ func TestServer_Configure(t *testing.T) {
chequebook := chequebookmock.NewChequebook(o.ChequebookOpts...)
swapserv := swapmock.New(o.SwapOpts...)
ln := lightnode.NewContainer(o.Overlay)
transaction := transactionmock.New(o.TransactionOpts...)
s := debugapi.New(o.Overlay, o.PublicKey, o.PSSPublicKey, o.EthereumAddress, logging.New(ioutil.Discard, 0), nil, nil)
ts := httptest.NewServer(s)
t.Cleanup(ts.Close)
......@@ -166,7 +170,7 @@ func TestServer_Configure(t *testing.T) {
}),
)
s.Configure(o.P2P, o.Pingpong, topologyDriver, ln, o.Storer, o.Tags, acc, settlement, true, swapserv, chequebook, nil)
s.Configure(o.P2P, o.Pingpong, topologyDriver, ln, o.Storer, o.Tags, acc, settlement, true, swapserv, chequebook, nil, transaction)
testBasicRouter(t, client)
jsonhttptest.Request(t, client, http.MethodGet, "/readiness", http.StatusOK,
......
......@@ -25,17 +25,24 @@ type (
SwapCashoutResponse = swapCashoutResponse
SwapCashoutStatusResponse = swapCashoutStatusResponse
SwapCashoutStatusResult = swapCashoutStatusResult
TransactionInfo = transactionInfo
TransactionPendingList = transactionPendingList
TransactionHashResponse = transactionHashResponse
TagResponse = tagResponse
ReserveStateResponse = reserveStateResponse
ChainStateResponse = chainStateResponse
)
var (
ErrCantBalance = errCantBalance
ErrCantBalances = errCantBalances
ErrNoBalance = errNoBalance
ErrCantSettlementsPeer = errCantSettlementsPeer
ErrCantSettlements = errCantSettlements
ErrChequebookBalance = errChequebookBalance
ErrInvalidAddress = errInvalidAddress
ErrCantBalance = errCantBalance
ErrCantBalances = errCantBalances
ErrNoBalance = errNoBalance
ErrCantSettlementsPeer = errCantSettlementsPeer
ErrCantSettlements = errCantSettlements
ErrChequebookBalance = errChequebookBalance
ErrInvalidAddress = errInvalidAddress
ErrUnknownTransaction = errUnknownTransaction
ErrCantGetTransaction = errCantGetTransaction
ErrCantResendTransaction = errCantResendTransaction
ErrAlreadyImported = errAlreadyImported
)
......@@ -169,6 +169,14 @@ func (s *Service) newRouter() *mux.Router {
"GET": http.HandlerFunc(s.swapCashoutStatusHandler),
"POST": http.HandlerFunc(s.swapCashoutHandler),
})
router.Handle("/transactions", jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.transactionListHandler),
})
router.Handle("/transactions/{hash}", jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.transactionDetailHandler),
"POST": http.HandlerFunc(s.transactionResendHandler),
})
}
router.Handle("/tags/{id}", jsonhttp.MethodHandler{
......
// Copyright 2020 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 debugapi
import (
"errors"
"net/http"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethersphere/bee/pkg/bigint"
"github.com/ethersphere/bee/pkg/jsonhttp"
"github.com/ethersphere/bee/pkg/transaction"
"github.com/gorilla/mux"
)
const (
errCantGetTransaction = "cannot get transaction"
errUnknownTransaction = "unknown transaction"
errAlreadyImported = "already imported"
errCantResendTransaction = "can't resend transaction"
)
type transactionInfo struct {
TransactionHash common.Hash `json:"transactionHash"`
To *common.Address `json:"to"`
Nonce uint64 `json:"nonce"`
GasPrice *bigint.BigInt `json:"gasPrice"`
GasLimit uint64 `json:"gasLimit"`
Data string `json:"data"`
Created time.Time `json:"created"`
Description string `json:"description"`
Value *bigint.BigInt `json:"value"`
}
type transactionPendingList struct {
PendingTransactions []transactionInfo `json:"pendingTransactions"`
}
func (s *Service) transactionListHandler(w http.ResponseWriter, r *http.Request) {
txHashes, err := s.transaction.PendingTransactions()
if err != nil {
s.logger.Debugf("debug api: transactions: get pending transactions: %v", err)
s.logger.Errorf("debug api: transactions: can't get pending transactions")
jsonhttp.InternalServerError(w, errCantGetTransaction)
return
}
var transactionInfos []transactionInfo = make([]transactionInfo, 0)
for _, txHash := range txHashes {
storedTransaction, err := s.transaction.StoredTransaction(txHash)
if err != nil {
s.logger.Debugf("debug api: transactions: get stored transaction %x: %v", txHash, err)
s.logger.Errorf("debug api: transactions: can't get stored transaction %x", txHash)
jsonhttp.InternalServerError(w, errCantGetTransaction)
return
}
transactionInfos = append(transactionInfos, transactionInfo{
TransactionHash: txHash,
To: storedTransaction.To,
Nonce: storedTransaction.Nonce,
GasPrice: bigint.Wrap(storedTransaction.GasPrice),
GasLimit: storedTransaction.GasLimit,
Data: hexutil.Encode(storedTransaction.Data),
Created: time.Unix(storedTransaction.Created, 0),
Description: storedTransaction.Description,
Value: bigint.Wrap(storedTransaction.Value),
})
}
jsonhttp.OK(w, transactionPendingList{
PendingTransactions: transactionInfos,
})
}
func (s *Service) transactionDetailHandler(w http.ResponseWriter, r *http.Request) {
hash := mux.Vars(r)["hash"]
txHash := common.HexToHash(hash)
storedTransaction, err := s.transaction.StoredTransaction(txHash)
if err != nil {
s.logger.Debugf("debug api: transactions: get transaction %x: %v", txHash, err)
s.logger.Errorf("debug api: transactions: can't get transaction %x", txHash)
if errors.Is(err, transaction.ErrUnknownTransaction) {
jsonhttp.NotFound(w, errUnknownTransaction)
} else {
jsonhttp.InternalServerError(w, errCantGetTransaction)
}
return
}
jsonhttp.OK(w, transactionInfo{
TransactionHash: txHash,
To: storedTransaction.To,
Nonce: storedTransaction.Nonce,
GasPrice: bigint.Wrap(storedTransaction.GasPrice),
GasLimit: storedTransaction.GasLimit,
Data: hexutil.Encode(storedTransaction.Data),
Created: time.Unix(storedTransaction.Created, 0),
Description: storedTransaction.Description,
Value: bigint.Wrap(storedTransaction.Value),
})
}
type transactionHashResponse struct {
TransactionHash common.Hash `json:"transactionHash"`
}
func (s *Service) transactionResendHandler(w http.ResponseWriter, r *http.Request) {
hash := mux.Vars(r)["hash"]
txHash := common.HexToHash(hash)
err := s.transaction.ResendTransaction(txHash)
if err != nil {
s.logger.Debugf("debug api: transactions: resend %x: %v", txHash, err)
s.logger.Errorf("debug api: transactions: can't resend transaction %x", txHash)
if errors.Is(err, transaction.ErrUnknownTransaction) {
jsonhttp.NotFound(w, errUnknownTransaction)
} else if errors.Is(err, transaction.ErrAlreadyImported) {
jsonhttp.BadRequest(w, errAlreadyImported)
} else {
jsonhttp.InternalServerError(w, errCantResendTransaction)
}
return
}
jsonhttp.OK(w, transactionHashResponse{
TransactionHash: txHash,
})
}
// Copyright 2020 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 debugapi_test
import (
"errors"
"math/big"
"net/http"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethersphere/bee/pkg/bigint"
"github.com/ethersphere/bee/pkg/debugapi"
"github.com/ethersphere/bee/pkg/jsonhttp"
"github.com/ethersphere/bee/pkg/jsonhttp/jsonhttptest"
"github.com/ethersphere/bee/pkg/transaction"
"github.com/ethersphere/bee/pkg/transaction/mock"
)
func TestTransactionStoredTransaction(t *testing.T) {
txHashStr := "0xabcd"
txHash := common.HexToHash(txHashStr)
dataStr := "abdd"
data := common.Hex2Bytes(dataStr)
created := int64(1616451040)
recipient := common.HexToAddress("fffe")
gasPrice := big.NewInt(23)
gasLimit := uint64(200)
value := big.NewInt(50)
nonce := uint64(12)
description := "test"
t.Run("found", func(t *testing.T) {
testServer := newTestServer(t, testServerOptions{
TransactionOpts: []mock.Option{
mock.WithStoredTransactionFunc(func(txHash common.Hash) (*transaction.StoredTransaction, error) {
return &transaction.StoredTransaction{
To: &recipient,
Created: created,
Data: data,
GasPrice: gasPrice,
GasLimit: gasLimit,
Value: value,
Nonce: nonce,
Description: description,
}, nil
}),
},
})
jsonhttptest.Request(t, testServer.Client, http.MethodGet, "/transactions/"+txHashStr, http.StatusOK,
jsonhttptest.WithExpectedJSONResponse(debugapi.TransactionInfo{
TransactionHash: txHash,
Created: time.Unix(created, 0),
Data: "0x" + dataStr,
To: &recipient,
GasPrice: bigint.Wrap(gasPrice),
GasLimit: gasLimit,
Value: bigint.Wrap(value),
Nonce: nonce,
Description: description,
}),
)
})
t.Run("not found", func(t *testing.T) {
testServer := newTestServer(t, testServerOptions{
TransactionOpts: []mock.Option{
mock.WithStoredTransactionFunc(func(txHash common.Hash) (*transaction.StoredTransaction, error) {
return nil, transaction.ErrUnknownTransaction
}),
},
})
jsonhttptest.Request(t, testServer.Client, http.MethodGet, "/transactions/"+txHashStr, http.StatusNotFound,
jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
Message: debugapi.ErrUnknownTransaction,
Code: http.StatusNotFound,
}))
})
t.Run("other errors", func(t *testing.T) {
testServer := newTestServer(t, testServerOptions{
TransactionOpts: []mock.Option{
mock.WithStoredTransactionFunc(func(txHash common.Hash) (*transaction.StoredTransaction, error) {
return nil, errors.New("err")
}),
},
})
jsonhttptest.Request(t, testServer.Client, http.MethodGet, "/transactions/"+txHashStr, http.StatusInternalServerError,
jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
Message: debugapi.ErrCantGetTransaction,
Code: http.StatusInternalServerError,
}))
})
}
func TestTransactionList(t *testing.T) {
recipient := common.HexToAddress("dfff")
txHash1 := common.HexToHash("abcd")
txHash2 := common.HexToHash("efff")
storedTransactions := map[common.Hash]*transaction.StoredTransaction{
txHash1: {
To: &recipient,
Created: 1,
Data: []byte{1, 2, 3, 4},
GasPrice: big.NewInt(12),
GasLimit: 5345,
Value: big.NewInt(4),
Nonce: 3,
Description: "test",
},
txHash2: {
To: &recipient,
Created: 2,
Data: []byte{3, 2, 3, 4},
GasPrice: big.NewInt(42),
GasLimit: 53451,
Value: big.NewInt(41),
Nonce: 32,
Description: "test2",
},
}
testServer := newTestServer(t, testServerOptions{
TransactionOpts: []mock.Option{
mock.WithPendingTransactionsFunc(func() ([]common.Hash, error) {
return []common.Hash{txHash1, txHash2}, nil
}),
mock.WithStoredTransactionFunc(func(txHash common.Hash) (*transaction.StoredTransaction, error) {
return storedTransactions[txHash], nil
}),
},
})
jsonhttptest.Request(t, testServer.Client, http.MethodGet, "/transactions", http.StatusOK,
jsonhttptest.WithExpectedJSONResponse(debugapi.TransactionPendingList{
PendingTransactions: []debugapi.TransactionInfo{
{
TransactionHash: txHash1,
Created: time.Unix(storedTransactions[txHash1].Created, 0),
Data: hexutil.Encode(storedTransactions[txHash1].Data),
To: storedTransactions[txHash1].To,
GasPrice: bigint.Wrap(storedTransactions[txHash1].GasPrice),
GasLimit: storedTransactions[txHash1].GasLimit,
Value: bigint.Wrap(storedTransactions[txHash1].Value),
Nonce: storedTransactions[txHash1].Nonce,
Description: storedTransactions[txHash1].Description,
},
{
TransactionHash: txHash2,
Created: time.Unix(storedTransactions[txHash2].Created, 0),
Data: hexutil.Encode(storedTransactions[txHash2].Data),
To: storedTransactions[txHash2].To,
GasPrice: bigint.Wrap(storedTransactions[txHash2].GasPrice),
GasLimit: storedTransactions[txHash2].GasLimit,
Value: bigint.Wrap(storedTransactions[txHash2].Value),
Nonce: storedTransactions[txHash2].Nonce,
Description: storedTransactions[txHash2].Description,
},
},
}),
)
}
func TestTransactionListError(t *testing.T) {
txHash1 := common.HexToHash("abcd")
t.Run("pending transactions error", func(t *testing.T) {
testServer := newTestServer(t, testServerOptions{
TransactionOpts: []mock.Option{
mock.WithPendingTransactionsFunc(func() ([]common.Hash, error) {
return nil, errors.New("err")
}),
mock.WithStoredTransactionFunc(func(txHash common.Hash) (*transaction.StoredTransaction, error) {
return nil, nil
}),
},
})
jsonhttptest.Request(t, testServer.Client, http.MethodGet, "/transactions", http.StatusInternalServerError,
jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
Code: http.StatusInternalServerError,
Message: debugapi.ErrCantGetTransaction,
}),
)
})
t.Run("pending transactions error", func(t *testing.T) {
testServer := newTestServer(t, testServerOptions{
TransactionOpts: []mock.Option{
mock.WithPendingTransactionsFunc(func() ([]common.Hash, error) {
return []common.Hash{txHash1}, nil
}),
mock.WithStoredTransactionFunc(func(txHash common.Hash) (*transaction.StoredTransaction, error) {
return nil, errors.New("error")
}),
},
})
jsonhttptest.Request(t, testServer.Client, http.MethodGet, "/transactions", http.StatusInternalServerError,
jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
Code: http.StatusInternalServerError,
Message: debugapi.ErrCantGetTransaction,
}),
)
})
}
func TestTransactionResend(t *testing.T) {
txHash := common.HexToHash("abcd")
t.Run("ok", func(t *testing.T) {
testServer := newTestServer(t, testServerOptions{
TransactionOpts: []mock.Option{
mock.WithResendTransactionFunc(func(txHash common.Hash) error {
return nil
}),
},
})
jsonhttptest.Request(t, testServer.Client, http.MethodPost, "/transactions/"+txHash.String(), http.StatusOK,
jsonhttptest.WithExpectedJSONResponse(debugapi.TransactionHashResponse{
TransactionHash: txHash,
}),
)
})
t.Run("unknown transaction", func(t *testing.T) {
testServer := newTestServer(t, testServerOptions{
TransactionOpts: []mock.Option{
mock.WithResendTransactionFunc(func(txHash common.Hash) error {
return transaction.ErrUnknownTransaction
}),
},
})
jsonhttptest.Request(t, testServer.Client, http.MethodPost, "/transactions/"+txHash.String(), http.StatusNotFound,
jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
Code: http.StatusNotFound,
Message: debugapi.ErrUnknownTransaction,
}),
)
})
t.Run("already imported", func(t *testing.T) {
testServer := newTestServer(t, testServerOptions{
TransactionOpts: []mock.Option{
mock.WithResendTransactionFunc(func(txHash common.Hash) error {
return transaction.ErrAlreadyImported
}),
},
})
jsonhttptest.Request(t, testServer.Client, http.MethodPost, "/transactions/"+txHash.String(), http.StatusBadRequest,
jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
Code: http.StatusBadRequest,
Message: debugapi.ErrAlreadyImported,
}),
)
})
t.Run("other error", func(t *testing.T) {
testServer := newTestServer(t, testServerOptions{
TransactionOpts: []mock.Option{
mock.WithResendTransactionFunc(func(txHash common.Hash) error {
return errors.New("err")
}),
},
})
jsonhttptest.Request(t, testServer.Client, http.MethodPost, "/transactions/"+txHash.String(), http.StatusInternalServerError,
jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
Code: http.StatusInternalServerError,
Message: debugapi.ErrCantResendTransaction,
}),
)
})
}
......@@ -102,6 +102,7 @@ type Bee struct {
pssCloser io.Closer
ethClientCloser func()
transactionMonitorCloser io.Closer
transactionCloser io.Closer
recoveryHandleCleanup func()
listenerCloser io.Closer
postageServiceCloser io.Closer
......@@ -259,6 +260,7 @@ func NewBee(addr string, swarmAddress swarm.Address, publicKey ecdsa.PublicKey,
return nil, fmt.Errorf("init chain: %w", err)
}
b.ethClientCloser = swapBackend.Close
b.transactionCloser = tracerCloser
b.transactionMonitorCloser = transactionMonitor
}
......@@ -717,7 +719,7 @@ func NewBee(addr string, swarmAddress swarm.Address, publicKey ecdsa.PublicKey,
}
// inject dependencies and configure full debug api http path routes
debugAPIService.Configure(p2ps, pingPong, kad, lightNodes, storer, tagService, acc, pseudosettleService, o.SwapEnable, swapService, chequebookService, batchStore)
debugAPIService.Configure(p2ps, pingPong, kad, lightNodes, storer, tagService, acc, pseudosettleService, o.SwapEnable, swapService, chequebookService, batchStore, transactionService)
}
if err := kad.Start(p2pCtx); err != nil {
......@@ -819,6 +821,7 @@ func (b *Bee) Shutdown(ctx context.Context) error {
go func() {
defer wg.Done()
tryClose(b.transactionMonitorCloser, "transaction monitor")
tryClose(b.transactionCloser, "transaction")
}()
go func() {
defer wg.Done()
......
......@@ -146,11 +146,12 @@ func (s *cashoutService) CashCheque(ctx context.Context, chequebook, recipient c
lim = 300000
}
request := &transaction.TxRequest{
To: &chequebook,
Data: callData,
GasPrice: sctx.GetGasPrice(ctx),
GasLimit: lim,
Value: big.NewInt(0),
To: &chequebook,
Data: callData,
GasPrice: sctx.GetGasPrice(ctx),
GasLimit: lim,
Value: big.NewInt(0),
Description: "cheque cashout",
}
txHash, err := s.transactionService.Send(ctx, request)
......
......@@ -323,11 +323,12 @@ func (s *service) Withdraw(ctx context.Context, amount *big.Int) (hash common.Ha
}
request := &transaction.TxRequest{
To: &s.address,
Data: callData,
GasPrice: sctx.GetGasPrice(ctx),
GasLimit: 95000,
Value: big.NewInt(0),
To: &s.address,
Data: callData,
GasPrice: sctx.GetGasPrice(ctx),
GasLimit: 95000,
Value: big.NewInt(0),
Description: fmt.Sprintf("chequebook withdrawal of %d BZZ", amount),
}
txHash, err := s.transactionService.Send(ctx, request)
......
......@@ -79,11 +79,12 @@ func (c *factory) Deploy(ctx context.Context, issuer common.Address, defaultHard
}
request := &transaction.TxRequest{
To: &c.address,
Data: callData,
GasPrice: sctx.GetGasPrice(ctx),
GasLimit: 175000,
Value: big.NewInt(0),
To: &c.address,
Data: callData,
GasPrice: sctx.GetGasPrice(ctx),
GasLimit: 175000,
Value: big.NewInt(0),
Description: "chequebook deployment",
}
txHash, err := c.transactionService.Send(ctx, request)
......
......@@ -77,11 +77,12 @@ func (c *erc20Service) Transfer(ctx context.Context, address common.Address, val
}
request := &transaction.TxRequest{
To: &c.address,
Data: callData,
GasPrice: sctx.GetGasPrice(ctx),
GasLimit: 90000,
Value: big.NewInt(0),
To: &c.address,
Data: callData,
GasPrice: sctx.GetGasPrice(ctx),
GasLimit: 90000,
Value: big.NewInt(0),
Description: "token transfer",
}
txHash, err := c.transactionService.Send(ctx, request)
......
......@@ -4,8 +4,6 @@
package transaction
type StoredTransaction = storedTransaction
var (
StoredTransactionKey = storedTransactionKey
)
......@@ -22,6 +22,9 @@ type transactionServiceMock struct {
waitForReceipt func(ctx context.Context, txHash common.Hash) (receipt *types.Receipt, err error)
watchSentTransaction func(txHash common.Hash) (chan types.Receipt, chan error, error)
call func(ctx context.Context, request *transaction.TxRequest) (result []byte, err error)
pendingTransactions func() ([]common.Hash, error)
resendTransaction func(txHash common.Hash) error
storedTransaction func(txHash common.Hash) (*transaction.StoredTransaction, error)
}
func (m *transactionServiceMock) Send(ctx context.Context, request *transaction.TxRequest) (txHash common.Hash, err error) {
......@@ -52,6 +55,31 @@ func (m *transactionServiceMock) Call(ctx context.Context, request *transaction.
return nil, errors.New("not implemented")
}
func (m *transactionServiceMock) PendingTransactions() ([]common.Hash, error) {
if m.pendingTransactions != nil {
return m.pendingTransactions()
}
return nil, errors.New("not implemented")
}
func (m *transactionServiceMock) ResendTransaction(txHash common.Hash) error {
if m.resendTransaction != nil {
return m.resendTransaction(txHash)
}
return errors.New("not implemented")
}
func (m *transactionServiceMock) StoredTransaction(txHash common.Hash) (*transaction.StoredTransaction, error) {
if m.storedTransaction != nil {
return m.storedTransaction(txHash)
}
return nil, errors.New("not implemented")
}
func (m *transactionServiceMock) Close() error {
return nil
}
// Option is the option passed to the mock Chequebook service
type Option interface {
apply(*transactionServiceMock)
......@@ -79,6 +107,24 @@ func WithCallFunc(f func(ctx context.Context, request *transaction.TxRequest) (r
})
}
func WithStoredTransactionFunc(f func(txHash common.Hash) (*transaction.StoredTransaction, error)) Option {
return optionFunc(func(s *transactionServiceMock) {
s.storedTransaction = f
})
}
func WithPendingTransactionsFunc(f func() ([]common.Hash, error)) Option {
return optionFunc(func(s *transactionServiceMock) {
s.pendingTransactions = f
})
}
func WithResendTransactionFunc(f func(txHash common.Hash) error) Option {
return optionFunc(func(s *transactionServiceMock) {
s.resendTransaction = f
})
}
func New(opts ...Option) transaction.Service {
mock := new(transactionServiceMock)
for _, o := range opts {
......
......@@ -7,8 +7,11 @@ package transaction
import (
"errors"
"fmt"
"io"
"math/big"
"strings"
"sync"
"time"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
......@@ -20,37 +23,44 @@ import (
)
const (
noncePrefix = "transaction_nonce_"
storedTransactionPrefix = "transaction_stored_"
noncePrefix = "transaction_nonce_"
storedTransactionPrefix = "transaction_stored_"
pendingTransactionPrefix = "transaction_pending_"
)
var (
// ErrTransactionReverted denotes that the sent transaction has been
// reverted.
ErrTransactionReverted = errors.New("transaction reverted")
ErrUnknownTransaction = errors.New("unknown transaction")
ErrAlreadyImported = errors.New("already imported")
)
// TxRequest describes a request for a transaction that can be executed.
type TxRequest struct {
To *common.Address // recipient of the transaction
Data []byte // transaction data
GasPrice *big.Int // gas price or nil if suggested gas price should be used
GasLimit uint64 // gas limit or 0 if it should be estimated
Value *big.Int // amount of wei to send
To *common.Address // recipient of the transaction
Data []byte // transaction data
GasPrice *big.Int // gas price or nil if suggested gas price should be used
GasLimit uint64 // gas limit or 0 if it should be estimated
Value *big.Int // amount of wei to send
Description string // optional description
}
type storedTransaction struct {
To *common.Address // recipient of the transaction
Data []byte // transaction data
GasPrice *big.Int // used gas price
GasLimit uint64 // used gas limit
Value *big.Int // amount of wei to send
Nonce uint64 // used nonce
type StoredTransaction struct {
To *common.Address // recipient of the transaction
Data []byte // transaction data
GasPrice *big.Int // used gas price
GasLimit uint64 // used gas limit
Value *big.Int // amount of wei to send
Nonce uint64 // used nonce
Created int64 // creation timestamp
Description string // description
}
// Service is the service to send transactions. It takes care of gas price, gas
// limit and nonce management.
type Service interface {
io.Closer
// Send creates a transaction based on the request and sends it.
Send(ctx context.Context, request *TxRequest) (txHash common.Hash, err error)
// Call simulate a transaction based on the request.
......@@ -62,10 +72,20 @@ type Service interface {
// This wraps the monitors watch function by loading the correct nonce from the store.
// This is only valid for transaction sent by this service.
WatchSentTransaction(txHash common.Hash) (<-chan types.Receipt, <-chan error, error)
// StoredTransaction retrieves the stored information for the transaction
StoredTransaction(txHash common.Hash) (*StoredTransaction, error)
// PendingTransactions retrieves the list of all pending transaction hashes
PendingTransactions() ([]common.Hash, error)
// Resend resends a previously sent transaction
// This operation can be useful if for some reason the transaction vanished from the eth networks pending pool
ResendTransaction(txHash common.Hash) error
}
type transactionService struct {
lock sync.Mutex
wg sync.WaitGroup
lock sync.Mutex
ctx context.Context
cancel context.CancelFunc
logger logging.Logger
backend Backend
......@@ -83,7 +103,11 @@ func NewService(logger logging.Logger, backend Backend, signer crypto.Signer, st
return nil, err
}
return &transactionService{
ctx, cancel := context.WithCancel(context.Background())
t := &transactionService{
ctx: ctx,
cancel: cancel,
logger: logger,
backend: backend,
signer: signer,
......@@ -91,7 +115,17 @@ func NewService(logger logging.Logger, backend Backend, signer crypto.Signer, st
store: store,
chainID: chainID,
monitor: monitor,
}, nil
}
pendingTxs, err := t.PendingTransactions()
if err != nil {
return nil, err
}
for _, txHash := range pendingTxs {
t.waitForPendingTx(txHash)
}
return t, nil
}
// Send creates and signs a transaction based on the request and sends it.
......@@ -128,21 +162,51 @@ func (t *transactionService) Send(ctx context.Context, request *TxRequest) (txHa
txHash = signedTx.Hash()
err = t.store.Put(storedTransactionKey(txHash), storedTransaction{
To: signedTx.To(),
Data: signedTx.Data(),
GasPrice: signedTx.GasPrice(),
GasLimit: signedTx.Gas(),
Value: signedTx.Value(),
Nonce: signedTx.Nonce(),
err = t.store.Put(storedTransactionKey(txHash), StoredTransaction{
To: signedTx.To(),
Data: signedTx.Data(),
GasPrice: signedTx.GasPrice(),
GasLimit: signedTx.Gas(),
Value: signedTx.Value(),
Nonce: signedTx.Nonce(),
Created: time.Now().Unix(),
Description: request.Description,
})
if err != nil {
return common.Hash{}, err
}
err = t.store.Put(pendingTransactionKey(txHash), struct{}{})
if err != nil {
return common.Hash{}, err
}
t.waitForPendingTx(txHash)
return signedTx.Hash(), nil
}
func (t *transactionService) waitForPendingTx(txHash common.Hash) {
t.wg.Add(1)
go func() {
defer t.wg.Done()
_, err := t.WaitForReceipt(t.ctx, txHash)
if err != nil {
if !errors.Is(err, ErrTransactionCancelled) {
t.logger.Errorf("error while waiting for pending transaction %x: %v", txHash, err)
}
t.logger.Warningf("pending transaction %x cancelled", txHash)
} else {
t.logger.Tracef("pending transaction %x confirmed", txHash)
}
err = t.store.Delete(pendingTransactionKey(txHash))
if err != nil {
t.logger.Errorf("error while unregistering transaction as pending %x: %v", txHash, err)
}
}()
}
func (t *transactionService) Call(ctx context.Context, request *TxRequest) ([]byte, error) {
msg := ethereum.CallMsg{
From: t.sender,
......@@ -160,10 +224,13 @@ func (t *transactionService) Call(ctx context.Context, request *TxRequest) ([]by
return data, nil
}
func (t *transactionService) getStoredTransaction(txHash common.Hash) (*storedTransaction, error) {
var tx storedTransaction
func (t *transactionService) StoredTransaction(txHash common.Hash) (*StoredTransaction, error) {
var tx StoredTransaction
err := t.store.Get(storedTransactionKey(txHash), &tx)
if err != nil {
if errors.Is(err, storage.ErrNotFound) {
return nil, ErrUnknownTransaction
}
return nil, err
}
return &tx, nil
......@@ -226,6 +293,10 @@ func storedTransactionKey(txHash common.Hash) string {
return fmt.Sprintf("%s%x", storedTransactionPrefix, txHash)
}
func pendingTransactionKey(txHash common.Hash) string {
return fmt.Sprintf("%s%x", pendingTransactionPrefix, txHash)
}
func (t *transactionService) nextNonce(ctx context.Context) (uint64, error) {
onchainNonce, err := t.backend.PendingNonceAt(ctx, t.sender)
if err != nil {
......@@ -278,10 +349,73 @@ func (t *transactionService) WatchSentTransaction(txHash common.Hash) (<-chan ty
// loading the tx here guarantees it was in fact sent from this transaction service
// also it allows us to avoid having to load the transaction during the watch loop
storedTransaction, err := t.getStoredTransaction(txHash)
storedTransaction, err := t.StoredTransaction(txHash)
if err != nil {
return nil, nil, err
}
return t.monitor.WatchTransaction(txHash, storedTransaction.Nonce)
}
func (t *transactionService) PendingTransactions() ([]common.Hash, error) {
var txHashes []common.Hash = make([]common.Hash, 0)
err := t.store.Iterate(pendingTransactionPrefix, func(key, value []byte) (stop bool, err error) {
txHash := common.HexToHash(strings.TrimPrefix(string(key), pendingTransactionPrefix))
txHashes = append(txHashes, txHash)
return false, nil
})
if err != nil {
return nil, err
}
return txHashes, nil
}
func (t *transactionService) ResendTransaction(txHash common.Hash) error {
storedTransaction, err := t.StoredTransaction(txHash)
if err != nil {
return err
}
var tx *types.Transaction
if storedTransaction.To != nil {
tx = types.NewTransaction(
storedTransaction.Nonce,
*storedTransaction.To,
storedTransaction.Value,
storedTransaction.GasLimit,
storedTransaction.GasPrice,
storedTransaction.Data,
)
} else {
tx = types.NewContractCreation(
storedTransaction.Nonce,
storedTransaction.Value,
storedTransaction.GasLimit,
storedTransaction.GasPrice,
storedTransaction.Data,
)
}
signedTx, err := t.signer.SignTx(tx, t.chainID)
if err != nil {
return err
}
if signedTx.Hash() != txHash {
return errors.New("transaction hash changed")
}
err = t.backend.SendTransaction(t.ctx, signedTx)
if err != nil {
if strings.Contains(err.Error(), "already imported") {
return ErrAlreadyImported
}
}
return nil
}
func (t *transactionService) Close() error {
t.cancel()
t.wg.Wait()
return nil
}
......@@ -124,6 +124,7 @@ func TestTransactionSend(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer transactionService.Close()
txHash, err := transactionService.Send(context.Background(), request)
if err != nil {
......@@ -142,6 +143,47 @@ func TestTransactionSend(t *testing.T) {
if storedNonce != nonce+1 {
t.Fatalf("nonce not stored correctly: want %d, got %d", nonce+1, storedNonce)
}
storedTransaction, err := transactionService.StoredTransaction(txHash)
if err != nil {
t.Fatal(err)
}
if storedTransaction.To == nil || *storedTransaction.To != recipient {
t.Fatalf("got wrong recipient in stored transaction. wanted %x, got %x", recipient, storedTransaction.To)
}
if !bytes.Equal(storedTransaction.Data, request.Data) {
t.Fatalf("got wrong data in stored transaction. wanted %x, got %x", request.Data, storedTransaction.Data)
}
if storedTransaction.Description != request.Description {
t.Fatalf("got wrong description in stored transaction. wanted %x, got %x", request.Description, storedTransaction.Description)
}
if storedTransaction.GasLimit != estimatedGasLimit {
t.Fatalf("got wrong gas limit in stored transaction. wanted %d, got %d", estimatedGasLimit, storedTransaction.GasLimit)
}
if suggestedGasPrice.Cmp(storedTransaction.GasPrice) != 0 {
t.Fatalf("got wrong gas price in stored transaction. wanted %d, got %d", suggestedGasPrice, storedTransaction.GasPrice)
}
if storedTransaction.Nonce != nonce {
t.Fatalf("got wrong nonce in stored transaction. wanted %d, got %d", nonce, storedTransaction.Nonce)
}
pending, err := transactionService.PendingTransactions()
if err != nil {
t.Fatal(err)
}
if len(pending) != 1 {
t.Fatalf("expected one pending transaction, got %d", len(pending))
}
if pending[0] != txHash {
t.Fatalf("got wrong pending transaction. wanted %x, got %x", txHash, pending[0])
}
})
t.Run("send_no_nonce", func(t *testing.T) {
......@@ -185,6 +227,7 @@ func TestTransactionSend(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer transactionService.Close()
txHash, err := transactionService.Send(context.Background(), request)
if err != nil {
......@@ -311,6 +354,7 @@ func TestTransactionSend(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer transactionService.Close()
txHash, err := transactionService.Send(context.Background(), request)
if err != nil {
......@@ -369,6 +413,7 @@ func TestTransactionWaitForReceipt(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer transactionService.Close()
receipt, err := transactionService.WaitForReceipt(context.Background(), txHash)
if err != nil {
......@@ -379,3 +424,55 @@ func TestTransactionWaitForReceipt(t *testing.T) {
t.Fatal("got wrong receipt")
}
}
func TestTransactionResend(t *testing.T) {
logger := logging.New(ioutil.Discard, 0)
recipient := common.HexToAddress("0xbbbddd")
chainID := big.NewInt(5)
nonce := uint64(10)
data := []byte{1, 2, 3, 4}
gasPrice := big.NewInt(0)
gasLimit := uint64(100000)
value := big.NewInt(0)
store := storemock.NewStateStore()
defer store.Close()
signedTx := types.NewTransaction(nonce, recipient, value, gasLimit, gasPrice, data)
err := store.Put(transaction.StoredTransactionKey(signedTx.Hash()), transaction.StoredTransaction{
Nonce: nonce,
To: &recipient,
Data: data,
GasPrice: gasPrice,
GasLimit: gasLimit,
Value: value,
})
if err != nil {
t.Fatal(err)
}
transactionService, err := transaction.NewService(logger,
backendmock.New(
backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error {
if tx != signedTx {
t.Fatal("not sending signed transaction")
}
return nil
}),
),
signerMockForTransaction(signedTx, recipient, chainID, t),
store,
chainID,
monitormock.New(),
)
if err != nil {
t.Fatal(err)
}
defer transactionService.Close()
err = transactionService.ResendTransaction(signedTx.Hash())
if err != nil {
t.Fatal(err)
}
}
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