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

chequebook deployment (#703)

parent db5a1bba
......@@ -44,6 +44,10 @@ const (
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"
)
func init() {
......@@ -190,4 +194,8 @@ func (c *command) setAllFlags(cmd *cobra.Command) {
cmd.Flags().Bool(optionNameGatewayMode, false, "disable a set of sensitive features in the api")
cmd.Flags().Bool(optionNameClefSignerEnable, false, "enable clef signer")
cmd.Flags().String(optionNameClefSignerEndpoint, "", "clef signer endpoint")
cmd.Flags().String(optionNameSwapEndpoint, "http://localhost:8545", "swap ethereum blockchain endpoint")
cmd.Flags().String(optionNameSwapFactoryAddress, "", "swap factory address")
cmd.Flags().Uint64(optionNameSwapInitialDeposit, 0, "initial deposit if deploying a new chequebook")
cmd.Flags().Bool(optionNameSwapEnable, false, "enable swap")
}
......@@ -202,6 +202,10 @@ Welcome to the Swarm.... Bzzz Bzzzz Bzzzz
PaymentTolerance: c.config.GetUint64(optionNamePaymentTolerance),
ResolverConnectionCfgs: resolverCfgs,
GatewayMode: c.config.GetBool(optionNameGatewayMode),
SwapEndpoint: c.config.GetString(optionNameSwapEndpoint),
SwapFactoryAddress: c.config.GetString(optionNameSwapFactoryAddress),
SwapInitialDeposit: c.config.GetUint64(optionNameSwapInitialDeposit),
SwapEnable: c.config.GetBool(optionNameSwapEnable),
})
if err != nil {
return err
......
......@@ -10,6 +10,7 @@ require (
github.com/ethereum/go-ethereum v1.9.20
github.com/ethersphere/bmt v0.1.2
github.com/ethersphere/manifest v0.2.0
github.com/ethersphere/sw3-bindings/v2 v2.0.0
github.com/gogo/protobuf v1.3.1
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
......@@ -57,6 +58,7 @@ require (
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect
golang.org/x/mod v0.3.0 // indirect
golang.org/x/net v0.0.0-20200707034311-ab3426394381
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect
golang.org/x/text v0.3.3 // indirect
......
......@@ -167,6 +167,8 @@ github.com/ethersphere/bmt v0.1.2 h1:FEuvQY9xuK+rDp3VwDVyde8T396Matv/u9PdtKa2r9Q
github.com/ethersphere/bmt v0.1.2/go.mod h1:fqRBDmYwn3lX2MH4lkImXQgFWeNP8ikLkS/hgi/HRws=
github.com/ethersphere/manifest v0.2.0 h1:HD2ufiIaw/5Vgrl4XyeGduDJ5tn50wIhqMQoWdT2GME=
github.com/ethersphere/manifest v0.2.0/go.mod h1:ygAx0KLhXYmKqsjUab95RCbXf8UcO7yMDjyfP0lY76Y=
github.com/ethersphere/sw3-bindings/v2 v2.0.0 h1:uc+wBqEMMq7c4NWj+MSkKkkpObgrUYxfAxz6FYJWkI4=
github.com/ethersphere/sw3-bindings/v2 v2.0.0/go.mod h1:OA34yk7ludjNag+yBDY9Gp3czWoFUVMsiK7gUXnZ26U=
github.com/fatih/color v1.3.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fjl/memsize v0.0.0-20180418122429-ca190fb6ffbc/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0=
......
......@@ -7,11 +7,14 @@ package clef
import (
"crypto/ecdsa"
"errors"
"math/big"
"os"
"path/filepath"
"runtime"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethersphere/bee/pkg/crypto"
)
......@@ -23,6 +26,7 @@ var (
// ExternalSignerInterface is the interface for the clef client from go-ethereum
type ExternalSignerInterface interface {
SignData(account accounts.Account, mimeType string, data []byte) ([]byte, error)
SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error)
Accounts() []accounts.Account
}
......@@ -94,3 +98,14 @@ func (c *clefSigner) PublicKey() (*ecdsa.PublicKey, error) {
func (c *clefSigner) Sign(data []byte) ([]byte, error) {
return c.clef.SignData(c.account, accounts.MimetypeTextPlain, data)
}
// SignTx signs an ethereum transaction
func (c *clefSigner) SignTx(transaction *types.Transaction) (*types.Transaction, error) {
// chainId is nil here because it is set on the clef side
return c.clef.SignTx(c.account, transaction, nil)
}
// EthereumAddress returns the ethereum address this signer uses
func (c *clefSigner) EthereumAddress() (common.Address, error) {
return c.account.Address, nil
}
......@@ -8,10 +8,12 @@ import (
"bytes"
"crypto/ecdsa"
"errors"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/crypto/clef"
)
......@@ -36,6 +38,10 @@ func (m *mockClef) Accounts() []accounts.Account {
return m.accounts
}
func (m *mockClef) SignTx(account accounts.Account, transaction *types.Transaction, chainId *big.Int) (*types.Transaction, error) {
return nil, nil
}
func TestNewClefSigner(t *testing.T) {
ethAddress := common.HexToAddress("0x31415b599f636129AD03c196cef9f8f8b184D5C7")
testSignature := make([]byte, 65)
......
......@@ -10,6 +10,8 @@ import (
"fmt"
"github.com/btcsuite/btcd/btcec"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
var (
......@@ -17,8 +19,14 @@ var (
)
type Signer interface {
// Sign signs data with ethereum prefix (eip191 type 0x45)
Sign(data []byte) ([]byte, error)
// SignTx signs an ethereum transaction
SignTx(transaction *types.Transaction) (*types.Transaction, error)
// PublicKey returns the public key this signer uses
PublicKey() (*ecdsa.PublicKey, error)
// EthereumAddress returns the ethereum address this signer uses
EthereumAddress() (common.Address, error)
}
// addEthereumPrefix adds the ethereum prefix to the data
......@@ -61,10 +69,12 @@ func NewDefaultSigner(key *ecdsa.PrivateKey) Signer {
}
}
// PublicKey returns the public key this signer uses
func (d *defaultSigner) PublicKey() (*ecdsa.PublicKey, error) {
return &d.key.PublicKey, nil
}
// Sign signs data with ethereum prefix (eip191 type 0x45)
func (d *defaultSigner) Sign(data []byte) (signature []byte, err error) {
hash, err := hashWithEthereumPrefix(data)
if err != nil {
......@@ -82,3 +92,35 @@ func (d *defaultSigner) Sign(data []byte) (signature []byte, err error) {
signature[64] = v
return signature, nil
}
// SignTx signs an ethereum transaction
func (d *defaultSigner) SignTx(transaction *types.Transaction) (*types.Transaction, error) {
hash := (&types.HomesteadSigner{}).Hash(transaction).Bytes()
// isCompressedKey is false here so we get the expected v value (27 or 28)
signature, err := btcec.SignCompact(btcec.S256(), (*btcec.PrivateKey)(d.key), hash, false)
if err != nil {
return nil, err
}
// Convert to Ethereum signature format with 'recovery id' v at the end.
v := signature[0]
copy(signature, signature[1:])
// v value needs to be adjusted by 27 as transaction.WithSignature expects it to be 0 or 1
signature[64] = v - 27
return transaction.WithSignature(&types.HomesteadSigner{}, signature)
}
// EthereumAddress returns the ethereum address this signer uses
func (d *defaultSigner) EthereumAddress() (common.Address, error) {
publicKey, err := d.PublicKey()
if err != nil {
return common.Address{}, err
}
eth, err := NewEthereumAddress(*publicKey)
if err != nil {
return common.Address{}, err
}
var ethAddress common.Address
copy(ethAddress[:], eth)
return ethAddress, nil
}
......@@ -5,9 +5,14 @@
package crypto_test
import (
"encoding/hex"
"errors"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethersphere/bee/pkg/crypto"
)
......@@ -56,3 +61,64 @@ func TestDefaultSigner(t *testing.T) {
}
})
}
func TestDefaultSignerEthereumAddress(t *testing.T) {
data, err := hex.DecodeString("634fb5a872396d9693e5c9f9d7233cfa93f395c093371017ff44aa9ae6564cdd")
if err != nil {
t.Fatal(err)
}
privKey, err := crypto.DecodeSecp256k1PrivateKey(data)
if err != nil {
t.Fatal(err)
}
signer := crypto.NewDefaultSigner(privKey)
ethAddress, err := signer.EthereumAddress()
if err != nil {
t.Fatal(err)
}
expected := common.HexToAddress("8d3766440f0d7b949a5e32995d09619a7f86e632")
if ethAddress != expected {
t.Fatalf("wrong signature. expected %x, got %x", expected, ethAddress)
}
}
func TestDefaultSignerSignTx(t *testing.T) {
data, err := hex.DecodeString("634fb5a872396d9693e5c9f9d7233cfa93f395c093371017ff44aa9ae6564cdd")
if err != nil {
t.Fatal(err)
}
privKey, err := crypto.DecodeSecp256k1PrivateKey(data)
if err != nil {
t.Fatal(err)
}
signer := crypto.NewDefaultSigner(privKey)
beneficiary := common.HexToAddress("8d3766440f0d7b949a5e32995d09619a7f86e632")
tx, err := signer.SignTx(types.NewTransaction(0, beneficiary, big.NewInt(0), 21000, big.NewInt(1), []byte{1}))
if err != nil {
t.Fatal(err)
}
expectedR := math.MustParseBig256("0x28815033e9b5b7ec32e40e3c90b6cd499c12de8a7da261fdad8b800c845b88ef")
expectedS := math.MustParseBig256("0x71f1c08f754ee36e0c9743a2240d4b6640ea4d78c8dc2d83a599bdcf80ef9d5f")
expectedV := math.MustParseBig256("0x1c")
v, r, s := tx.RawSignatureValues()
if expectedV.Cmp(v) != 0 {
t.Fatalf("wrong v value. expected %x, got %x", expectedV, v)
}
if expectedR.Cmp(r) != 0 {
t.Fatalf("wrong r value. expected %x, got %x", expectedR, r)
}
if expectedS.Cmp(s) != 0 {
t.Fatalf("wrong s value. expected %x, got %x", expectedS, s)
}
}
......@@ -6,6 +6,7 @@ package node
import (
"context"
"errors"
"fmt"
"io"
"log"
......@@ -14,6 +15,8 @@ import (
"path/filepath"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethersphere/bee/pkg/accounting"
"github.com/ethersphere/bee/pkg/addressbook"
"github.com/ethersphere/bee/pkg/api"
......@@ -39,6 +42,7 @@ import (
"github.com/ethersphere/bee/pkg/resolver/multiresolver"
"github.com/ethersphere/bee/pkg/retrieval"
"github.com/ethersphere/bee/pkg/settlement/pseudosettle"
"github.com/ethersphere/bee/pkg/settlement/swap/chequebook"
"github.com/ethersphere/bee/pkg/soc"
"github.com/ethersphere/bee/pkg/statestore/leveldb"
mockinmem "github.com/ethersphere/bee/pkg/statestore/mock"
......@@ -94,6 +98,10 @@ type Options struct {
PaymentTolerance uint64
ResolverConnectionCfgs []multiresolver.ConnectionConfig
GatewayMode bool
SwapEndpoint string
SwapFactoryAddress string
SwapInitialDeposit uint64
SwapEnable bool
}
func NewBee(addr string, swarmAddress swarm.Address, keystore keystore.Service, signer crypto.Signer, networkID uint64, logger logging.Logger, o Options) (*Bee, error) {
......@@ -138,6 +146,53 @@ func NewBee(addr string, swarmAddress swarm.Address, keystore keystore.Service,
b.stateStoreCloser = stateStore
addressbook := addressbook.New(stateStore)
if o.SwapEnable {
swapBackend, err := ethclient.Dial(o.SwapEndpoint)
if err != nil {
return nil, err
}
transactionService, err := chequebook.NewTransactionService(logger, swapBackend, signer)
if err != nil {
return nil, err
}
overlayEthAddress, err := signer.EthereumAddress()
if err != nil {
return nil, err
}
// print ethereum address so users know which address we need to fund
logger.Infof("using ethereum address %x", overlayEthAddress)
// TODO: factory address discovery for well-known networks (goerli for beta)
if o.SwapFactoryAddress == "" {
return nil, errors.New("no known factory address")
} else if !common.IsHexAddress(o.SwapFactoryAddress) {
return nil, errors.New("invalid factory address")
}
chequebookFactory, err := chequebook.NewFactory(swapBackend, transactionService, common.HexToAddress(o.SwapFactoryAddress), chequebook.NewSimpleSwapFactoryBindingFunc)
if err != nil {
return nil, err
}
// initialize chequebook logic
// return value is ignored because we don't do anything yet after initialization. this will be passed into swap settlement.
_, err = chequebook.Init(p2pCtx,
chequebookFactory,
stateStore,
logger,
o.SwapInitialDeposit,
transactionService,
swapBackend,
overlayEthAddress,
chequebook.NewSimpleSwapBindings,
chequebook.NewERC20Bindings)
if err != nil {
return nil, err
}
}
p2ps, err := libp2p.New(p2pCtx, signer, networkID, swarmAddress, addr, addressbook, stateStore, logger, tracer, libp2p.Options{
PrivateKey: libp2pPrivateKey,
NATAddr: o.NATAddr,
......
// 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 chequebook
import (
"math/big"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethersphere/sw3-bindings/v2/simpleswapfactory"
)
// SimpleSwapBinding is the interface for the generated go bindings for ERC20SimpleSwap
type SimpleSwapBinding interface {
Balance(*bind.CallOpts) (*big.Int, error)
}
type SimpleSwapBindingFunc = func(common.Address, bind.ContractBackend) (SimpleSwapBinding, error)
// NewSimpleSwapBindings generates the default go bindings
func NewSimpleSwapBindings(address common.Address, backend bind.ContractBackend) (SimpleSwapBinding, error) {
return simpleswapfactory.NewERC20SimpleSwap(address, backend)
}
// ERC20Binding is the interface for the generated go bindings for ERC20
type ERC20Binding interface {
BalanceOf(*bind.CallOpts, common.Address) (*big.Int, error)
}
type ERC20BindingFunc = func(common.Address, bind.ContractBackend) (ERC20Binding, error)
// NewERC20Bindings generates the default go bindings
func NewERC20Bindings(address common.Address, backend bind.ContractBackend) (ERC20Binding, error) {
return simpleswapfactory.NewERC20(address, backend)
}
// SimpleSwapFactoryBinding is the interface for the generated go bindings for SimpleSwapFactory
type SimpleSwapFactoryBinding interface {
ParseSimpleSwapDeployed(types.Log) (*simpleswapfactory.SimpleSwapFactorySimpleSwapDeployed, error)
DeployedContracts(*bind.CallOpts, common.Address) (bool, error)
ERC20Address(*bind.CallOpts) (common.Address, error)
}
type SimpleSwapFactoryBindingFunc = func(common.Address, bind.ContractBackend) (SimpleSwapFactoryBinding, error)
// NewSimpleSwapFactoryBindingFunc generates the default go bindings
func NewSimpleSwapFactoryBindingFunc(address common.Address, backend bind.ContractBackend) (SimpleSwapFactoryBinding, error) {
return simpleswapfactory.NewSimpleSwapFactory(address, backend)
}
// 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 chequebook
import (
"context"
"errors"
"math/big"
"strings"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethersphere/sw3-bindings/v2/simpleswapfactory"
)
// Service is the main interface for interacting with the nodes chequebook
type Service interface {
// Deposit starts depositing erc20 token into the chequebook. This returns once the transactions has been broadcast.
Deposit(ctx context.Context, amount *big.Int) (hash common.Hash, err error)
// WaitForDeposit waits for the deposit transaction to confirm and verifies the result
WaitForDeposit(ctx context.Context, txHash common.Hash) error
// Balance returns the token balance of the chequebook
Balance(ctx context.Context) (*big.Int, error)
// Address returns the address of the used chequebook contract
Address() common.Address
}
type service struct {
backend Backend
transactionService TransactionService
address common.Address
chequebookABI abi.ABI
chequebookInstance SimpleSwapBinding
ownerAddress common.Address
erc20Address common.Address
erc20ABI abi.ABI
erc20Instance ERC20Binding
}
// New creates a new chequebook service for the provided chequebook contract
func New(backend Backend, transactionService TransactionService, address, erc20Address, ownerAddress common.Address, simpleSwapBindingFunc SimpleSwapBindingFunc, erc20BindingFunc ERC20BindingFunc) (Service, error) {
chequebookABI, err := abi.JSON(strings.NewReader(simpleswapfactory.ERC20SimpleSwapABI))
if err != nil {
return nil, err
}
erc20ABI, err := abi.JSON(strings.NewReader(simpleswapfactory.ERC20ABI))
if err != nil {
return nil, err
}
chequebookInstance, err := simpleSwapBindingFunc(address, backend)
if err != nil {
return nil, err
}
erc20Instance, err := erc20BindingFunc(erc20Address, backend)
if err != nil {
return nil, err
}
return &service{
backend: backend,
transactionService: transactionService,
address: address,
chequebookABI: chequebookABI,
chequebookInstance: chequebookInstance,
ownerAddress: ownerAddress,
erc20Address: erc20Address,
erc20ABI: erc20ABI,
erc20Instance: erc20Instance,
}, nil
}
// Address returns the address of the used chequebook contract
func (s *service) Address() common.Address {
return s.address
}
// Deposit starts depositing erc20 token into the chequebook. This returns once the transactions has been broadcast.
func (s *service) Deposit(ctx context.Context, amount *big.Int) (hash common.Hash, err error) {
balance, err := s.erc20Instance.BalanceOf(&bind.CallOpts{
Context: ctx,
}, s.ownerAddress)
if err != nil {
return common.Hash{}, err
}
// check we can afford this so we don't waste gas
if balance.Cmp(amount) < 0 {
return common.Hash{}, errors.New("insufficient token balance")
}
callData, err := s.erc20ABI.Pack("transfer", s.address, amount)
if err != nil {
return common.Hash{}, err
}
request := &TxRequest{
To: s.erc20Address,
Data: callData,
GasPrice: nil,
GasLimit: 0,
Value: big.NewInt(0),
}
txHash, err := s.transactionService.Send(ctx, request)
if err != nil {
return common.Hash{}, err
}
return txHash, nil
}
// Balance returns the token balance of the chequebook
func (s *service) Balance(ctx context.Context) (*big.Int, error) {
return s.chequebookInstance.Balance(&bind.CallOpts{
Context: ctx,
})
}
// WaitForDeposit waits for the deposit transaction to confirm and verifies the result
func (s *service) WaitForDeposit(ctx context.Context, txHash common.Hash) error {
receipt, err := s.transactionService.WaitForReceipt(ctx, txHash)
if err != nil {
return err
}
if receipt.Status != 1 {
return ErrTransactionReverted
}
return nil
}
// 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 chequebook_test
import (
"context"
"errors"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethersphere/bee/pkg/settlement/swap/chequebook"
)
func newTestChequebook(
t *testing.T,
backend chequebook.Backend,
transactionService chequebook.TransactionService,
address common.Address,
erc20address common.Address,
ownerAdress common.Address,
simpleSwapBinding chequebook.SimpleSwapBinding,
erc20Binding chequebook.ERC20Binding) (chequebook.Service, error) {
return chequebook.New(
backend,
transactionService,
address,
erc20address,
ownerAdress,
func(addr common.Address, b bind.ContractBackend) (chequebook.SimpleSwapBinding, error) {
if addr != address {
t.Fatalf("initialised binding with wrong address. wanted %x, got %x", address, addr)
}
if b != backend {
t.Fatal("initialised binding with wrong backend")
}
return simpleSwapBinding, nil
},
func(addr common.Address, b bind.ContractBackend) (chequebook.ERC20Binding, error) {
if addr != erc20address {
t.Fatalf("initialised binding with wrong address. wanted %x, got %x", erc20address, addr)
}
if b != backend {
t.Fatal("initialised binding with wrong backend")
}
return erc20Binding, nil
},
)
}
func TestChequebookAddress(t *testing.T) {
address := common.HexToAddress("0xabcd")
erc20address := common.HexToAddress("0xefff")
ownerAdress := common.HexToAddress("0xfff")
chequebookService, err := newTestChequebook(
t,
&backendMock{},
&transactionServiceMock{},
address,
erc20address,
ownerAdress,
&simpleSwapBindingMock{},
&erc20BindingMock{})
if err != nil {
t.Fatal(err)
}
if chequebookService.Address() != address {
t.Fatalf("returned wrong address. wanted %x, got %x", address, chequebookService.Address())
}
}
func TestChequebookBalance(t *testing.T) {
address := common.HexToAddress("0xabcd")
erc20address := common.HexToAddress("0xefff")
ownerAdress := common.HexToAddress("0xfff")
balance := big.NewInt(10)
chequebookService, err := newTestChequebook(
t,
&backendMock{},
&transactionServiceMock{},
address,
erc20address,
ownerAdress,
&simpleSwapBindingMock{
balance: func(*bind.CallOpts) (*big.Int, error) {
return balance, nil
},
},
&erc20BindingMock{})
if err != nil {
t.Fatal(err)
}
returnedBalance, err := chequebookService.Balance(context.Background())
if err != nil {
t.Fatal(err)
}
if returnedBalance.Cmp(balance) != 0 {
t.Fatalf("returned wrong balance. wanted %d, got %d", balance, returnedBalance)
}
}
func TestChequebookDeposit(t *testing.T) {
address := common.HexToAddress("0xabcd")
erc20address := common.HexToAddress("0xefff")
ownerAdress := common.HexToAddress("0xfff")
balance := big.NewInt(30)
depositAmount := big.NewInt(20)
txHash := common.HexToHash("0xdddd")
chequebookService, err := newTestChequebook(
t,
&backendMock{},
&transactionServiceMock{
send: func(c context.Context, request *chequebook.TxRequest) (common.Hash, error) {
if request.To != erc20address {
t.Fatalf("sending to wrong contract. wanted %x, got %x", erc20address, request.To)
}
if request.Value.Cmp(big.NewInt(0)) != 0 {
t.Fatal("sending ether to token contract")
}
return txHash, nil
},
},
address,
erc20address,
ownerAdress,
&simpleSwapBindingMock{},
&erc20BindingMock{
balanceOf: func(b *bind.CallOpts, addr common.Address) (*big.Int, error) {
if addr != ownerAdress {
t.Fatalf("looking up balance of wrong account. wanted %x, got %x", ownerAdress, addr)
}
return balance, nil
},
})
if err != nil {
t.Fatal(err)
}
returnedTxHash, err := chequebookService.Deposit(context.Background(), depositAmount)
if err != nil {
t.Fatal(err)
}
if txHash != returnedTxHash {
t.Fatalf("returned wrong transaction hash. wanted %v, got %v", txHash, returnedTxHash)
}
}
func TestChequebookWaitForDeposit(t *testing.T) {
address := common.HexToAddress("0xabcd")
erc20address := common.HexToAddress("0xefff")
ownerAdress := common.HexToAddress("0xfff")
txHash := common.HexToHash("0xdddd")
chequebookService, err := newTestChequebook(
t,
&backendMock{},
&transactionServiceMock{
waitForReceipt: func(ctx context.Context, tx common.Hash) (*types.Receipt, error) {
if tx != txHash {
t.Fatalf("waiting for wrong transaction. wanted %x, got %x", txHash, tx)
}
return &types.Receipt{
Status: 1,
}, nil
},
},
address,
erc20address,
ownerAdress,
&simpleSwapBindingMock{},
&erc20BindingMock{})
if err != nil {
t.Fatal(err)
}
err = chequebookService.WaitForDeposit(context.Background(), txHash)
if err != nil {
t.Fatal(err)
}
}
func TestChequebookWaitForDepositReverted(t *testing.T) {
address := common.HexToAddress("0xabcd")
erc20address := common.HexToAddress("0xefff")
ownerAdress := common.HexToAddress("0xfff")
txHash := common.HexToHash("0xdddd")
chequebookService, err := newTestChequebook(
t,
&backendMock{},
&transactionServiceMock{
waitForReceipt: func(ctx context.Context, tx common.Hash) (*types.Receipt, error) {
if tx != txHash {
t.Fatalf("waiting for wrong transaction. wanted %x, got %x", txHash, tx)
}
return &types.Receipt{
Status: 0,
}, nil
},
},
address,
erc20address,
ownerAdress,
&simpleSwapBindingMock{},
&erc20BindingMock{})
if err != nil {
t.Fatal(err)
}
err = chequebookService.WaitForDeposit(context.Background(), txHash)
if err == nil {
t.Fatal("expected reverted error")
}
if !errors.Is(err, chequebook.ErrTransactionReverted) {
t.Fatalf("wrong error. wanted %v, got %v", chequebook.ErrTransactionReverted, err)
}
}
// 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 chequebook_test
import (
"context"
"math/big"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethersphere/bee/pkg/settlement/swap/chequebook"
"github.com/ethersphere/sw3-bindings/v2/simpleswapfactory"
)
type backendMock struct {
codeAt func(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error)
sendTransaction func(ctx context.Context, tx *types.Transaction) error
suggestGasPrice func(ctx context.Context) (*big.Int, error)
estimateGas func(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error)
transactionReceipt func(ctx context.Context, txHash common.Hash) (*types.Receipt, error)
pendingNonceAt func(ctx context.Context, account common.Address) (uint64, error)
}
func (m *backendMock) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) {
return m.codeAt(ctx, contract, blockNumber)
}
func (*backendMock) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) {
panic("not implemented")
}
func (*backendMock) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) {
panic("not implemented")
}
func (m *backendMock) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) {
return m.pendingNonceAt(ctx, account)
}
func (m *backendMock) SuggestGasPrice(ctx context.Context) (*big.Int, error) {
return m.suggestGasPrice(ctx)
}
func (m *backendMock) EstimateGas(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) {
return m.estimateGas(ctx, call)
}
func (m *backendMock) SendTransaction(ctx context.Context, tx *types.Transaction) error {
return m.sendTransaction(ctx, tx)
}
func (*backendMock) FilterLogs(ctx context.Context, query ethereum.FilterQuery) ([]types.Log, error) {
panic("not implemented")
}
func (*backendMock) SubscribeFilterLogs(ctx context.Context, query ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) {
panic("not implemented")
}
func (m *backendMock) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) {
return m.transactionReceipt(ctx, txHash)
}
type transactionServiceMock struct {
send func(ctx context.Context, request *chequebook.TxRequest) (txHash common.Hash, err error)
waitForReceipt func(ctx context.Context, txHash common.Hash) (receipt *types.Receipt, err error)
}
func (m *transactionServiceMock) Send(ctx context.Context, request *chequebook.TxRequest) (txHash common.Hash, err error) {
return m.send(ctx, request)
}
func (m *transactionServiceMock) WaitForReceipt(ctx context.Context, txHash common.Hash) (receipt *types.Receipt, err error) {
return m.waitForReceipt(ctx, txHash)
}
type simpleSwapFactoryBindingMock struct {
erc20Address func(*bind.CallOpts) (common.Address, error)
deployedContracts func(*bind.CallOpts, common.Address) (bool, error)
parseSimpleSwapDeployed func(types.Log) (*simpleswapfactory.SimpleSwapFactorySimpleSwapDeployed, error)
}
func (m *simpleSwapFactoryBindingMock) ParseSimpleSwapDeployed(l types.Log) (*simpleswapfactory.SimpleSwapFactorySimpleSwapDeployed, error) {
return m.parseSimpleSwapDeployed(l)
}
func (m *simpleSwapFactoryBindingMock) DeployedContracts(o *bind.CallOpts, a common.Address) (bool, error) {
return m.deployedContracts(o, a)
}
func (m *simpleSwapFactoryBindingMock) ERC20Address(o *bind.CallOpts) (common.Address, error) {
return m.erc20Address(o)
}
type simpleSwapBindingMock struct {
balance func(*bind.CallOpts) (*big.Int, error)
}
func (m *simpleSwapBindingMock) Balance(o *bind.CallOpts) (*big.Int, error) {
return m.balance(o)
}
type erc20BindingMock struct {
balanceOf func(*bind.CallOpts, common.Address) (*big.Int, error)
}
func (m *erc20BindingMock) BalanceOf(o *bind.CallOpts, a common.Address) (*big.Int, error) {
return m.balanceOf(o, a)
}
// 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 chequebook
import (
"bytes"
"errors"
"math/big"
"strings"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethersphere/sw3-bindings/v2/simpleswapfactory"
"golang.org/x/net/context"
)
var (
ErrInvalidFactory = errors.New("not a valid factory contract")
ErrNotDeployedByFactory = errors.New("chequebook not deployed by factory")
)
// Factory is the main interface for interacting with the chequebook factory
type Factory interface {
// ERC20Address returns the token for which this factory deploys chequebooks
ERC20Address(ctx context.Context) (common.Address, error)
// Deploy deploys a new chequebook and returns once confirmed
Deploy(ctx context.Context, issuer common.Address, defaultHardDepositTimeoutDuration *big.Int) (common.Address, error)
// VerifyBytecode checks that the factory is valid
VerifyBytecode(ctx context.Context) error
// VerifyChequebook checks that the supplied chequebook has been deployed by this factory
VerifyChequebook(ctx context.Context, chequebook common.Address) error
}
type factory struct {
backend Backend
transactionService TransactionService
address common.Address
ABI abi.ABI
instance SimpleSwapFactoryBinding
}
// NewFactory creates a new factory service for the provided factory contract
func NewFactory(backend Backend, transactionService TransactionService, address common.Address, simpleSwapFactoryBindingFunc SimpleSwapFactoryBindingFunc) (Factory, error) {
ABI, err := abi.JSON(strings.NewReader(simpleswapfactory.SimpleSwapFactoryABI))
if err != nil {
return nil, err
}
instance, err := simpleSwapFactoryBindingFunc(address, backend)
if err != nil {
return nil, err
}
return &factory{
backend: backend,
transactionService: transactionService,
address: address,
ABI: ABI,
instance: instance,
}, nil
}
// Deploy deploys a new chequebook and returns once confirmed
func (c *factory) Deploy(ctx context.Context, issuer common.Address, defaultHardDepositTimeoutDuration *big.Int) (common.Address, error) {
callData, err := c.ABI.Pack("deploySimpleSwap", issuer, big.NewInt(0).Set(defaultHardDepositTimeoutDuration))
if err != nil {
return common.Address{}, err
}
request := &TxRequest{
To: c.address,
Data: callData,
GasPrice: nil,
GasLimit: 0,
Value: big.NewInt(0),
}
txHash, err := c.transactionService.Send(ctx, request)
if err != nil {
return common.Address{}, err
}
receipt, err := c.transactionService.WaitForReceipt(ctx, txHash)
if err != nil {
return common.Address{}, err
}
chequebookAddress, err := c.parseDeployReceipt(receipt)
if err != nil {
return common.Address{}, err
}
return chequebookAddress, nil
}
// parseDeployReceipt parses the address of the deployed chequebook from the receipt
func (c *factory) parseDeployReceipt(receipt *types.Receipt) (address common.Address, err error) {
if receipt.Status != 1 {
return common.Address{}, ErrTransactionReverted
}
for _, log := range receipt.Logs {
if log.Address != c.address {
continue
}
if event, err := c.instance.ParseSimpleSwapDeployed(*log); err == nil {
address = event.ContractAddress
break
}
}
if (address == common.Address{}) {
return common.Address{}, errors.New("contract deployment failed")
}
return address, nil
}
// VerifyBytecode checks that the factory is valid
func (c *factory) VerifyBytecode(ctx context.Context) (err error) {
code, err := c.backend.CodeAt(ctx, c.address, nil)
if err != nil {
return err
}
referenceCode := common.FromHex(simpleswapfactory.SimpleSwapFactoryDeployedCode)
if !bytes.Equal(code, referenceCode) {
return ErrInvalidFactory
}
return nil
}
// VerifyChequebook checks that the supplied chequebook has been deployed by this factory
func (c *factory) VerifyChequebook(ctx context.Context, chequebook common.Address) error {
deployed, err := c.instance.DeployedContracts(&bind.CallOpts{
Context: ctx,
}, chequebook)
if err != nil {
return err
}
if !deployed {
return ErrNotDeployedByFactory
}
return nil
}
// ERC20Address returns the token for which this factory deploys chequebooks
func (c *factory) ERC20Address(ctx context.Context) (common.Address, error) {
erc20Address, err := c.instance.ERC20Address(&bind.CallOpts{
Context: ctx,
})
if err != nil {
return common.Address{}, err
}
return erc20Address, nil
}
// 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 chequebook_test
import (
"bytes"
"context"
"errors"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethersphere/bee/pkg/settlement/swap/chequebook"
"github.com/ethersphere/sw3-bindings/v2/simpleswapfactory"
)
func newTestFactory(t *testing.T, factoryAddress common.Address, backend chequebook.Backend, transactionService chequebook.TransactionService, simpleSwapFactoryBinding chequebook.SimpleSwapFactoryBinding) (chequebook.Factory, error) {
return chequebook.NewFactory(backend, transactionService, factoryAddress,
func(addr common.Address, b bind.ContractBackend) (chequebook.SimpleSwapFactoryBinding, error) {
if addr != factoryAddress {
t.Fatalf("initialised binding with wrong address. wanted %x, got %x", factoryAddress, addr)
}
if b != backend {
t.Fatal("initialised binding with wrong backend")
}
return simpleSwapFactoryBinding, nil
})
}
func TestFactoryERC20Address(t *testing.T) {
factoryAddress := common.HexToAddress("0xabcd")
erc20Address := common.HexToAddress("0xeffff")
factory, err := newTestFactory(
t,
factoryAddress,
&backendMock{},
&transactionServiceMock{},
&simpleSwapFactoryBindingMock{
erc20Address: func(*bind.CallOpts) (common.Address, error) {
return erc20Address, nil
},
})
if err != nil {
t.Fatal(err)
}
addr, err := factory.ERC20Address(context.Background())
if err != nil {
t.Fatal(err)
}
if addr != erc20Address {
t.Fatalf("wrong erc20Address. wanted %x, got %x", erc20Address, addr)
}
}
func TestFactoryVerifySelf(t *testing.T) {
factoryAddress := common.HexToAddress("0xabcd")
factory, err := newTestFactory(
t,
factoryAddress,
&backendMock{
codeAt: func(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) {
if contract != factoryAddress {
t.Fatalf("called with wrong address. wanted %x, got %x", factoryAddress, contract)
}
if blockNumber != nil {
t.Fatal("not called for latest block")
}
return common.FromHex(simpleswapfactory.SimpleSwapFactoryDeployedCode), nil
},
},
&transactionServiceMock{},
&simpleSwapFactoryBindingMock{})
if err != nil {
t.Fatal(err)
}
err = factory.VerifyBytecode(context.Background())
if err != nil {
t.Fatal(err)
}
}
func TestFactoryVerifySelfInvalidCode(t *testing.T) {
factoryAddress := common.HexToAddress("0xabcd")
factory, err := newTestFactory(
t,
factoryAddress,
&backendMock{
codeAt: func(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) {
if contract != factoryAddress {
t.Fatalf("called with wrong address. wanted %x, got %x", factoryAddress, contract)
}
if blockNumber != nil {
t.Fatal("not called for latest block")
}
return common.FromHex(simpleswapfactory.AddressBin), nil
},
},
&transactionServiceMock{},
&simpleSwapFactoryBindingMock{})
if err != nil {
t.Fatal(err)
}
err = factory.VerifyBytecode(context.Background())
if err == nil {
t.Fatal("verified invalid factory")
}
if !errors.Is(err, chequebook.ErrInvalidFactory) {
t.Fatalf("wrong error. wanted %v, got %v", chequebook.ErrInvalidFactory, err)
}
}
func TestFactoryVerifyChequebook(t *testing.T) {
factoryAddress := common.HexToAddress("0xabcd")
chequebookAddress := common.HexToAddress("0xefff")
factory, err := newTestFactory(
t,
factoryAddress,
&backendMock{},
&transactionServiceMock{},
&simpleSwapFactoryBindingMock{
deployedContracts: func(o *bind.CallOpts, address common.Address) (bool, error) {
if address != chequebookAddress {
t.Fatalf("checked for wrong contract. wanted %v, got %v", chequebookAddress, address)
}
return true, nil
},
})
if err != nil {
t.Fatal(err)
}
err = factory.VerifyChequebook(context.Background(), chequebookAddress)
if err != nil {
t.Fatal(err)
}
}
func TestFactoryVerifyChequebookInvalid(t *testing.T) {
factoryAddress := common.HexToAddress("0xabcd")
chequebookAddress := common.HexToAddress("0xefff")
factory, err := newTestFactory(
t,
factoryAddress,
&backendMock{},
&transactionServiceMock{},
&simpleSwapFactoryBindingMock{
deployedContracts: func(o *bind.CallOpts, address common.Address) (bool, error) {
if address != chequebookAddress {
t.Fatalf("checked for wrong contract. wanted %v, got %v", chequebookAddress, address)
}
return false, nil
},
})
if err != nil {
t.Fatal(err)
}
err = factory.VerifyChequebook(context.Background(), chequebookAddress)
if err == nil {
t.Fatal("verified invalid chequebook")
}
if !errors.Is(err, chequebook.ErrNotDeployedByFactory) {
t.Fatalf("wrong error. wanted %v, got %v", chequebook.ErrNotDeployedByFactory, err)
}
}
func TestFactoryDeploy(t *testing.T) {
factoryAddress := common.HexToAddress("0xabcd")
issuerAddress := common.HexToAddress("0xefff")
defaultTimeout := big.NewInt(1)
deployTransactionHash := common.HexToHash("0xffff")
deployAddress := common.HexToAddress("0xdddd")
logData := common.Hex2Bytes("0xcccc")
factory, err := newTestFactory(
t,
factoryAddress,
&backendMock{},
&transactionServiceMock{
send: func(ctx context.Context, request *chequebook.TxRequest) (txHash common.Hash, err error) {
if request.To != factoryAddress {
t.Fatalf("sending to wrong address. wanted %x, got %x", factoryAddress, request.To)
}
if request.Value.Cmp(big.NewInt(0)) != 0 {
t.Fatal("trying to send ether")
}
return deployTransactionHash, nil
},
waitForReceipt: func(ctx context.Context, txHash common.Hash) (receipt *types.Receipt, err error) {
if txHash != deployTransactionHash {
t.Fatalf("waiting for wrong transaction. wanted %x, got %x", deployTransactionHash, txHash)
}
return &types.Receipt{
Status: 1,
Logs: []*types.Log{
{
Data: logData,
},
{
Address: factoryAddress,
Data: logData,
},
},
}, nil
},
},
&simpleSwapFactoryBindingMock{
parseSimpleSwapDeployed: func(log types.Log) (*simpleswapfactory.SimpleSwapFactorySimpleSwapDeployed, error) {
if !bytes.Equal(log.Data, logData) {
t.Fatal("trying to parse wrong log")
}
return &simpleswapfactory.SimpleSwapFactorySimpleSwapDeployed{
ContractAddress: deployAddress,
}, nil
},
})
if err != nil {
t.Fatal(err)
}
chequebookAddress, err := factory.Deploy(context.Background(), issuerAddress, defaultTimeout)
if err != nil {
t.Fatal(err)
}
if chequebookAddress != deployAddress {
t.Fatalf("returning wrong address. wanted %x, got %x", deployAddress, chequebookAddress)
}
}
func TestFactoryDeployReverted(t *testing.T) {
factoryAddress := common.HexToAddress("0xabcd")
issuerAddress := common.HexToAddress("0xefff")
defaultTimeout := big.NewInt(1)
deployTransactionHash := common.HexToHash("0xffff")
factory, err := newTestFactory(
t,
factoryAddress,
&backendMock{},
&transactionServiceMock{
send: func(ctx context.Context, request *chequebook.TxRequest) (txHash common.Hash, err error) {
if request.To != factoryAddress {
t.Fatalf("sending to wrong address. wanted %x, got %x", factoryAddress, request.To)
}
if request.Value.Cmp(big.NewInt(0)) != 0 {
t.Fatal("trying to send ether")
}
return deployTransactionHash, nil
},
waitForReceipt: func(ctx context.Context, txHash common.Hash) (receipt *types.Receipt, err error) {
if txHash != deployTransactionHash {
t.Fatalf("waiting for wrong transaction. wanted %x, got %x", deployTransactionHash, txHash)
}
return &types.Receipt{
Status: 0,
}, nil
},
},
&simpleSwapFactoryBindingMock{})
if err != nil {
t.Fatal(err)
}
_, err = factory.Deploy(context.Background(), issuerAddress, defaultTimeout)
if err == nil {
t.Fatal("returned failed chequebook deployment")
}
if !errors.Is(err, chequebook.ErrTransactionReverted) {
t.Fatalf("wrong error. wanted %v, got %v", chequebook.ErrTransactionReverted, err)
}
}
// 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 chequebook
import (
"context"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethersphere/bee/pkg/logging"
"github.com/ethersphere/bee/pkg/storage"
)
const chequebookKey = "chequebook"
// Init initialises the chequebook service
func Init(
ctx context.Context,
chequebookFactory Factory,
stateStore storage.StateStorer,
logger logging.Logger,
swapInitialDeposit uint64,
transactionService TransactionService,
swapBackend Backend,
overlayEthAddress common.Address,
simpleSwapBindingFunc SimpleSwapBindingFunc,
erc20BindingFunc ERC20BindingFunc) (chequebookService Service, err error) {
// verify that the supplied factory is valid
err = chequebookFactory.VerifyBytecode(ctx)
if err != nil {
return nil, err
}
erc20Address, err := chequebookFactory.ERC20Address(ctx)
if err != nil {
return nil, err
}
var chequebookAddress common.Address
err = stateStore.Get(chequebookKey, &chequebookAddress)
if err != nil {
if err != storage.ErrNotFound {
return nil, err
}
// if we don't yet have a chequebook, deploy a new one
logger.Info("deploying new chequebook")
chequebookAddress, err = chequebookFactory.Deploy(ctx, overlayEthAddress, big.NewInt(0))
if err != nil {
return nil, err
}
logger.Infof("deployed chequebook at address %x", chequebookAddress)
// save the address for later use
err = stateStore.Put(chequebookKey, chequebookAddress)
if err != nil {
return nil, err
}
chequebookService, err = New(swapBackend, transactionService, chequebookAddress, erc20Address, overlayEthAddress, simpleSwapBindingFunc, erc20BindingFunc)
if err != nil {
return nil, err
}
if swapInitialDeposit != 0 {
logger.Info("depositing into new chequebook")
depositHash, err := chequebookService.Deposit(ctx, big.NewInt(int64(swapInitialDeposit)))
if err != nil {
return nil, err
}
err = chequebookService.WaitForDeposit(ctx, depositHash)
if err != nil {
return nil, err
}
logger.Infof("deposited to chequebook %x in transaction %x", chequebookAddress, depositHash)
}
} else {
chequebookService, err = New(swapBackend, transactionService, chequebookAddress, erc20Address, overlayEthAddress, simpleSwapBindingFunc, erc20BindingFunc)
if err != nil {
return nil, err
}
logger.Infof("using existing chequebook %x", chequebookAddress)
}
// regardless of how the chequebook service was initialised make sure that the chequebook is valid
err = chequebookFactory.VerifyChequebook(ctx, chequebookService.Address())
if err != nil {
return nil, err
}
return chequebookService, nil
}
// 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 chequebook
import (
"errors"
"math/big"
"time"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/logging"
"golang.org/x/net/context"
)
var (
ErrTransactionReverted = errors.New("transaction reverted")
)
// Backend is the minimum of blockchain backend functions we need
type Backend interface {
bind.ContractBackend
TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error)
}
// 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
}
// TransactionService is the service to send transactions. It takes care of gas price, gas limit and nonce management.
type TransactionService interface {
// Send creates a transaction based on the request and sends it
Send(ctx context.Context, request *TxRequest) (txHash common.Hash, err error)
// WaitForReceipt waits until either the transaction with the given hash has been mined or the context is cancelled
WaitForReceipt(ctx context.Context, txHash common.Hash) (receipt *types.Receipt, err error)
}
type transactionService struct {
logger logging.Logger
backend Backend
signer crypto.Signer
sender common.Address
}
// NewTransactionService creates a new transaction service
func NewTransactionService(logger logging.Logger, backend Backend, signer crypto.Signer) (TransactionService, error) {
senderAddress, err := signer.EthereumAddress()
if err != nil {
return nil, err
}
return &transactionService{
logger: logger,
backend: backend,
signer: signer,
sender: senderAddress,
}, nil
}
// Send creates and signs a transaction based on the request and sends it
func (t *transactionService) Send(ctx context.Context, request *TxRequest) (txHash common.Hash, err error) {
tx, err := prepareTransaction(ctx, request, t.sender, t.backend)
if err != nil {
return common.Hash{}, err
}
signedTx, err := t.signer.SignTx(tx)
if err != nil {
return common.Hash{}, err
}
err = t.backend.SendTransaction(ctx, signedTx)
if err != nil {
return common.Hash{}, err
}
return signedTx.Hash(), nil
}
// WaitForReceipt waits until either the transaction with the given hash has been mined or the context is cancelled
func (t *transactionService) WaitForReceipt(ctx context.Context, txHash common.Hash) (receipt *types.Receipt, err error) {
for {
receipt, err := t.backend.TransactionReceipt(ctx, txHash)
if receipt != nil {
return receipt, nil
}
if err != nil {
// some node implementations return an error if the transaction is not yet mined
t.logger.Tracef("waiting for transaction %x to be mined: %v", txHash, err)
} else {
t.logger.Tracef("waiting for transaction %x to be mined", txHash)
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(1 * time.Second):
}
}
}
// prepareTransaction creates a signable transaction based on a request
func prepareTransaction(ctx context.Context, request *TxRequest, from common.Address, backend Backend) (tx *types.Transaction, err error) {
var gasLimit uint64
if request.GasLimit == 0 {
gasLimit, err = backend.EstimateGas(ctx, ethereum.CallMsg{
From: from,
To: &request.To,
Data: request.Data,
})
if err != nil {
return nil, err
}
} else {
gasLimit = request.GasLimit
}
var gasPrice *big.Int
if request.GasPrice == nil {
gasPrice, err = backend.SuggestGasPrice(ctx)
if err != nil {
return nil, err
}
} else {
gasPrice = request.GasPrice
}
nonce, err := backend.PendingNonceAt(ctx, from)
if err != nil {
return nil, err
}
return types.NewTransaction(
nonce,
request.To,
request.Value,
gasLimit,
gasPrice,
request.Data,
), nil
}
// 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 chequebook_test
import (
"bytes"
"context"
"crypto/ecdsa"
"io/ioutil"
"math/big"
"testing"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethersphere/bee/pkg/logging"
"github.com/ethersphere/bee/pkg/settlement/swap/chequebook"
)
type signerMock struct {
signTx func(transaction *types.Transaction) (*types.Transaction, error)
}
func (*signerMock) EthereumAddress() (common.Address, error) {
return common.Address{}, nil
}
func (*signerMock) Sign(data []byte) ([]byte, error) {
return nil, nil
}
func (m *signerMock) SignTx(transaction *types.Transaction) (*types.Transaction, error) {
return m.signTx(transaction)
}
func (*signerMock) PublicKey() (*ecdsa.PublicKey, error) {
return nil, nil
}
func TestTransactionSend(t *testing.T) {
logger := logging.New(ioutil.Discard, 0)
recipient := common.HexToAddress("0xabcd")
signedTx := types.NewTransaction(0, recipient, big.NewInt(0), 0, nil, nil)
txData := common.Hex2Bytes("0xabcdee")
value := big.NewInt(1)
suggestedGasPrice := big.NewInt(2)
estimatedGasLimit := uint64(3)
nonce := uint64(2)
request := &chequebook.TxRequest{
To: recipient,
Data: txData,
Value: value,
}
transactionService, err := chequebook.NewTransactionService(logger,
&backendMock{
sendTransaction: func(ctx context.Context, tx *types.Transaction) error {
if tx != signedTx {
t.Fatal("not sending signed transaction")
}
return nil
},
estimateGas: func(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) {
if !bytes.Equal(call.To.Bytes(), recipient.Bytes()) {
t.Fatalf("estimating with wrong recipient. wanted %x, got %x", recipient, call.To)
}
if !bytes.Equal(call.Data, txData) {
t.Fatal("estimating with wrong data")
}
return estimatedGasLimit, nil
},
suggestGasPrice: func(ctx context.Context) (*big.Int, error) {
return suggestedGasPrice, nil
},
pendingNonceAt: func(ctx context.Context, account common.Address) (uint64, error) {
return nonce, nil
},
},
&signerMock{
signTx: func(transaction *types.Transaction) (*types.Transaction, error) {
if !bytes.Equal(transaction.To().Bytes(), recipient.Bytes()) {
t.Fatalf("signing transaction with wrong recipient. wanted %x, got %x", recipient, transaction.To())
}
if !bytes.Equal(transaction.Data(), txData) {
t.Fatalf("signing transaction with wrong data. wanted %x, got %x", txData, transaction.Data())
}
if transaction.Value().Cmp(value) != 0 {
t.Fatalf("signing transaction with wrong value. wanted %d, got %d", value, transaction.Value())
}
if transaction.Gas() != estimatedGasLimit {
t.Fatalf("signing transaction with wrong gas. wanted %d, got %d", estimatedGasLimit, transaction.Gas())
}
if transaction.GasPrice().Cmp(suggestedGasPrice) != 0 {
t.Fatalf("signing transaction with wrong gasprice. wanted %d, got %d", suggestedGasPrice, transaction.GasPrice())
}
if transaction.Nonce() != nonce {
t.Fatalf("signing transaction with wrong nonce. wanted %d, got %d", nonce, transaction.Nonce())
}
return signedTx, nil
},
})
if err != nil {
t.Fatal(err)
}
txHash, err := transactionService.Send(context.Background(), request)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(txHash.Bytes(), signedTx.Hash().Bytes()) {
t.Fatal("returning wrong transaction hash")
}
}
func TestTransactionWaitForReceipt(t *testing.T) {
logger := logging.New(ioutil.Discard, 0)
txHash := common.HexToHash("0xabcdee")
transactionService, err := chequebook.NewTransactionService(logger,
&backendMock{
transactionReceipt: func(ctx context.Context, txHash common.Hash) (*types.Receipt, error) {
return &types.Receipt{
TxHash: txHash,
}, nil
},
},
&signerMock{})
if err != nil {
t.Fatal(err)
}
receipt, err := transactionService.WaitForReceipt(context.Background(), txHash)
if err != nil {
t.Fatal(err)
}
if receipt.TxHash != txHash {
t.Fatal("got wrong receipt")
}
}
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