Commit 5b44ec7f authored by Ralph Pichler's avatar Ralph Pichler Committed by GitHub

check available balance when issuing cheques (#739)

parent 04075a19
......@@ -16,7 +16,8 @@ var (
)
type chequebookBalanceResponse struct {
Balance *big.Int `json:"balance"`
TotalBalance *big.Int `json:"totalBalance"`
AvailableBalance *big.Int `json:"availableBalance"`
}
type chequebookAddressResponse struct {
......@@ -32,7 +33,15 @@ func (s *server) chequebookBalanceHandler(w http.ResponseWriter, r *http.Request
return
}
jsonhttp.OK(w, chequebookBalanceResponse{Balance: balance})
availableBalance, err := s.Chequebook.AvailableBalance(r.Context())
if err != nil {
jsonhttp.InternalServerError(w, errChequebookBalance)
s.Logger.Debugf("debug api: chequebook availableBalance: %v", err)
s.Logger.Error("debug api: cannot get chequebook availableBalance")
return
}
jsonhttp.OK(w, chequebookBalanceResponse{TotalBalance: balance, AvailableBalance: availableBalance})
}
func (s *server) chequebookAddressHandler(w http.ResponseWriter, r *http.Request) {
......
......@@ -21,18 +21,26 @@ import (
func TestChequebookBalance(t *testing.T) {
returnedBalance := big.NewInt(9000)
returnedAvailableBalance := big.NewInt(1000)
chequebookBalanceFunc := func(context.Context) (ret *big.Int, err error) {
return returnedBalance, nil
}
chequebookAvailableBalanceFunc := func(context.Context) (ret *big.Int, err error) {
return returnedAvailableBalance, nil
}
testServer := newTestServer(t, testServerOptions{
ChequebookOpts: []mock.Option{mock.WithChequebookBalanceFunc(chequebookBalanceFunc)},
ChequebookOpts: []mock.Option{
mock.WithChequebookBalanceFunc(chequebookBalanceFunc),
mock.WithChequebookAvailableBalanceFunc(chequebookAvailableBalanceFunc),
},
})
expected := &debugapi.ChequebookBalanceResponse{
Balance: returnedBalance,
TotalBalance: returnedBalance,
AvailableBalance: returnedAvailableBalance,
}
// We expect a list of items unordered by peer:
var got *debugapi.ChequebookBalanceResponse
......@@ -64,6 +72,30 @@ func TestChequebookBalanceError(t *testing.T) {
)
}
func TestChequebookAvailableBalanceError(t *testing.T) {
chequebookBalanceFunc := func(context.Context) (ret *big.Int, err error) {
return big.NewInt(0), nil
}
chequebookAvailableBalanceFunc := func(context.Context) (ret *big.Int, err error) {
return nil, errors.New("New errors")
}
testServer := newTestServer(t, testServerOptions{
ChequebookOpts: []mock.Option{
mock.WithChequebookBalanceFunc(chequebookBalanceFunc),
mock.WithChequebookAvailableBalanceFunc(chequebookAvailableBalanceFunc),
},
})
jsonhttptest.Request(t, testServer.Client, http.MethodGet, "/chequebook/balance", http.StatusInternalServerError,
jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
Message: debugapi.ErrChequebookBalance,
Code: http.StatusInternalServerError,
}),
)
}
func TestChequebookAddress(t *testing.T) {
chequebookAddressFunc := func() common.Address {
return common.HexToAddress("0xfffff")
......
......@@ -17,6 +17,7 @@ import (
type SimpleSwapBinding interface {
Balance(*bind.CallOpts) (*big.Int, error)
Issuer(*bind.CallOpts) (common.Address, error)
TotalPaidOut(*bind.CallOpts) (*big.Int, error)
}
type SimpleSwapBindingFunc = func(common.Address, bind.ContractBackend) (SimpleSwapBinding, error)
......
......@@ -24,6 +24,12 @@ type SendChequeFunc func(cheque *SignedCheque) error
const (
lastIssuedChequeKeyPrefix = "chequebook_last_issued_cheque_"
totalIssuedKey = "chequebook_total_issued_"
)
var (
// ErrOutOfFunds is the error when the chequebook has not enough free funds for a cheque
ErrOutOfFunds = errors.New("chequebook out of funds")
)
// Service is the main interface for interacting with the nodes chequebook.
......@@ -34,10 +40,12 @@ type Service interface {
WaitForDeposit(ctx context.Context, txHash common.Hash) error
// Balance returns the token balance of the chequebook.
Balance(ctx context.Context) (*big.Int, error)
// AvailableBalance returns the token balance of the chequebook which is not yet used for uncashed cheques.
AvailableBalance(ctx context.Context) (*big.Int, error)
// Address returns the address of the used chequebook contract.
Address() common.Address
// Issue a new cheque for the beneficiary with an cumulativePayout amount higher than the last.
Issue(beneficiary common.Address, amount *big.Int, sendChequeFunc SendChequeFunc) error
Issue(ctx context.Context, beneficiary common.Address, amount *big.Int, sendChequeFunc SendChequeFunc) error
// LastCheque returns the last cheque we issued for the beneficiary.
LastCheque(beneficiary common.Address) (*SignedCheque, error)
// LastCheque returns the last cheques for all beneficiaries.
......@@ -146,6 +154,32 @@ func (s *service) Balance(ctx context.Context) (*big.Int, error) {
})
}
// AvailableBalance returns the token balance of the chequebook which is not yet used for uncashed cheques.
func (s *service) AvailableBalance(ctx context.Context) (*big.Int, error) {
totalIssued, err := s.totalIssued()
if err != nil {
return nil, err
}
balance, err := s.Balance(ctx)
if err != nil {
return nil, err
}
totalPaidOut, err := s.chequebookInstance.TotalPaidOut(&bind.CallOpts{
Context: ctx,
})
if err != nil {
return nil, err
}
// balance plus totalPaidOut is the total amount ever put into the chequebook (ignoring deposits and withdrawals which cancelled out)
// minus the total amount we issued from this chequebook this gives use the portion of the balance not covered by any cheques
availableBalance := big.NewInt(0).Add(balance, totalPaidOut)
availableBalance = availableBalance.Sub(availableBalance, totalIssued)
return availableBalance, nil
}
// 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)
......@@ -165,11 +199,20 @@ func lastIssuedChequeKey(beneficiary common.Address) string {
// Issue issues a new cheque and passes it to sendChequeFunc
// if sendChequeFunc succeeds the cheque is considered sent and saved
func (s *service) Issue(beneficiary common.Address, amount *big.Int, sendChequeFunc SendChequeFunc) error {
func (s *service) Issue(ctx context.Context, beneficiary common.Address, amount *big.Int, sendChequeFunc SendChequeFunc) error {
// don't allow concurrent issuing of cheques
// this would be sufficient on a per beneficiary basis
s.lock.Lock()
defer s.lock.Unlock()
availableBalance, err := s.AvailableBalance(ctx)
if err != nil {
return err
}
if amount.Cmp(availableBalance) > 0 {
return ErrOutOfFunds
}
var cumulativePayout *big.Int
lastCheque, err := s.LastCheque(beneficiary)
if err != nil {
......@@ -209,7 +252,29 @@ func (s *service) Issue(beneficiary common.Address, amount *big.Int, sendChequeF
return err
}
return s.store.Put(lastIssuedChequeKey(beneficiary), cheque)
err = s.store.Put(lastIssuedChequeKey(beneficiary), cheque)
if err != nil {
return err
}
totalIssued, err := s.totalIssued()
if err != nil {
return err
}
totalIssued = totalIssued.Add(totalIssued, amount)
return s.store.Put(totalIssuedKey, totalIssued)
}
// returns the total amount in cheques issued so far
func (s *service) totalIssued() (totalIssued *big.Int, err error) {
err = s.store.Get(totalIssuedKey, &totalIssued)
if err != nil {
if err != storage.ErrNotFound {
return nil, err
}
return big.NewInt(0), nil
}
return totalIssued, nil
}
// LastCheque returns the last cheque we issued for the beneficiary.
......
......@@ -259,7 +259,14 @@ func TestChequebookIssue(t *testing.T) {
ownerAdress,
store,
chequeSigner,
&simpleSwapBindingMock{},
&simpleSwapBindingMock{
balance: func(*bind.CallOpts) (*big.Int, error) {
return big.NewInt(100), nil
},
totalPaidOut: func(*bind.CallOpts) (*big.Int, error) {
return big.NewInt(0), nil
},
},
&erc20BindingMock{})
if err != nil {
t.Fatal(err)
......@@ -282,7 +289,7 @@ func TestChequebookIssue(t *testing.T) {
return sig, nil
}
err = chequebookService.Issue(beneficiary, amount, func(cheque *chequebook.SignedCheque) error {
err = chequebookService.Issue(context.Background(), beneficiary, amount, func(cheque *chequebook.SignedCheque) error {
if !cheque.Equal(expectedCheque) {
t.Fatalf("wrong cheque. wanted %v got %v", expectedCheque, cheque)
}
......@@ -318,7 +325,7 @@ func TestChequebookIssue(t *testing.T) {
return sig, nil
}
err = chequebookService.Issue(beneficiary, amount2, func(cheque *chequebook.SignedCheque) error {
err = chequebookService.Issue(context.Background(), beneficiary, amount2, func(cheque *chequebook.SignedCheque) error {
if !cheque.Equal(expectedCheque) {
t.Fatalf("wrong cheque. wanted %v got %v", expectedCheque, cheque)
}
......@@ -354,7 +361,7 @@ func TestChequebookIssue(t *testing.T) {
return sig, nil
}
err = chequebookService.Issue(ownerAdress, amount, func(cheque *chequebook.SignedCheque) error {
err = chequebookService.Issue(context.Background(), ownerAdress, amount, func(cheque *chequebook.SignedCheque) error {
if !cheque.Equal(expectedChequeOwner) {
t.Fatalf("wrong cheque. wanted %v got %v", expectedChequeOwner, cheque)
}
......@@ -403,7 +410,14 @@ func TestChequebookIssueErrorSend(t *testing.T) {
ownerAdress,
store,
chequeSigner,
&simpleSwapBindingMock{},
&simpleSwapBindingMock{
balance: func(*bind.CallOpts) (*big.Int, error) {
return amount, nil
},
totalPaidOut: func(*bind.CallOpts) (*big.Int, error) {
return big.NewInt(0), nil
},
},
&erc20BindingMock{})
if err != nil {
t.Fatal(err)
......@@ -413,7 +427,7 @@ func TestChequebookIssueErrorSend(t *testing.T) {
return sig, nil
}
err = chequebookService.Issue(beneficiary, amount, func(cheque *chequebook.SignedCheque) error {
err = chequebookService.Issue(context.Background(), beneficiary, amount, func(cheque *chequebook.SignedCheque) error {
return errors.New("err")
})
if err == nil {
......@@ -422,9 +436,51 @@ func TestChequebookIssueErrorSend(t *testing.T) {
// verify the cheque was not saved
_, err = chequebookService.LastCheque(beneficiary)
if err == nil {
t.Fatal("expected error")
if !errors.Is(err, chequebook.ErrNoCheque) {
t.Fatalf("wrong error. wanted %v, got %v", chequebook.ErrNoCheque, err)
}
}
func TestChequebookIssueOutOfFunds(t *testing.T) {
address := common.HexToAddress("0xabcd")
erc20address := common.HexToAddress("0xefff")
beneficiary := common.HexToAddress("0xdddd")
ownerAdress := common.HexToAddress("0xfff")
store := storemock.NewStateStore()
amount := big.NewInt(20)
chequebookService, err := newTestChequebook(
t,
&backendMock{},
&transactionServiceMock{},
address,
erc20address,
ownerAdress,
store,
&chequeSignerMock{},
&simpleSwapBindingMock{
balance: func(*bind.CallOpts) (*big.Int, error) {
return big.NewInt(0), nil
},
totalPaidOut: func(*bind.CallOpts) (*big.Int, error) {
return big.NewInt(0), nil
},
},
&erc20BindingMock{})
if err != nil {
t.Fatal(err)
}
err = chequebookService.Issue(context.Background(), beneficiary, amount, func(cheque *chequebook.SignedCheque) error {
return nil
})
if !errors.Is(err, chequebook.ErrOutOfFunds) {
t.Fatalf("wrong error. wanted %v, got %v", chequebook.ErrOutOfFunds, err)
}
// verify the cheque was not saved
_, err = chequebookService.LastCheque(beneficiary)
if !errors.Is(err, chequebook.ErrNoCheque) {
t.Fatalf("wrong error. wanted %v, got %v", chequebook.ErrNoCheque, err)
}
......
......@@ -98,8 +98,9 @@ func (m *simpleSwapFactoryBindingMock) ERC20Address(o *bind.CallOpts) (common.Ad
}
type simpleSwapBindingMock struct {
balance func(*bind.CallOpts) (*big.Int, error)
issuer func(*bind.CallOpts) (common.Address, error)
balance func(*bind.CallOpts) (*big.Int, error)
issuer func(*bind.CallOpts) (common.Address, error)
totalPaidOut func(o *bind.CallOpts) (*big.Int, error)
}
func (m *simpleSwapBindingMock) Balance(o *bind.CallOpts) (*big.Int, error) {
......@@ -110,6 +111,10 @@ func (m *simpleSwapBindingMock) Issuer(o *bind.CallOpts) (common.Address, error)
return m.issuer(o)
}
func (m *simpleSwapBindingMock) TotalPaidOut(o *bind.CallOpts) (*big.Int, error) {
return m.totalPaidOut(o)
}
type erc20BindingMock struct {
balanceOf func(*bind.CallOpts, common.Address) (*big.Int, error)
}
......
......@@ -15,9 +15,10 @@ import (
// Service is the mock chequebook service.
type Service struct {
chequebookBalanceFunc func(context.Context) (*big.Int, error)
chequebookAddressFunc func() common.Address
chequebookIssueFunc func(beneficiary common.Address, amount *big.Int, sendChequeFunc chequebook.SendChequeFunc) error
chequebookBalanceFunc func(context.Context) (*big.Int, error)
chequebookAvailableBalanceFunc func(context.Context) (*big.Int, error)
chequebookAddressFunc func() common.Address
chequebookIssueFunc func(ctx context.Context, beneficiary common.Address, amount *big.Int, sendChequeFunc chequebook.SendChequeFunc) error
}
// WithChequebook*Functions set the mock chequebook functions
......@@ -27,13 +28,19 @@ func WithChequebookBalanceFunc(f func(ctx context.Context) (*big.Int, error)) Op
})
}
func WithChequebookAvailableBalanceFunc(f func(ctx context.Context) (*big.Int, error)) Option {
return optionFunc(func(s *Service) {
s.chequebookAvailableBalanceFunc = f
})
}
func WithChequebookAddressFunc(f func() common.Address) Option {
return optionFunc(func(s *Service) {
s.chequebookAddressFunc = f
})
}
func WithChequebookIssueFunc(f func(beneficiary common.Address, amount *big.Int, sendChequeFunc chequebook.SendChequeFunc) error) Option {
func WithChequebookIssueFunc(f func(ctx context.Context, beneficiary common.Address, amount *big.Int, sendChequeFunc chequebook.SendChequeFunc) error) Option {
return optionFunc(func(s *Service) {
s.chequebookIssueFunc = f
})
......@@ -56,6 +63,13 @@ func (s *Service) Balance(ctx context.Context) (bal *big.Int, err error) {
return big.NewInt(0), errors.New("Error")
}
func (s *Service) AvailableBalance(ctx context.Context) (bal *big.Int, err error) {
if s.chequebookAvailableBalanceFunc != nil {
return s.chequebookAvailableBalanceFunc(ctx)
}
return big.NewInt(0), errors.New("Error")
}
// Deposit mocks the chequebook .Deposit function
func (s *Service) Deposit(ctx context.Context, amount *big.Int) (hash common.Hash, err error) {
return common.Hash{}, errors.New("Error")
......@@ -74,9 +88,9 @@ func (s *Service) Address() common.Address {
return common.Address{}
}
func (s *Service) Issue(beneficiary common.Address, amount *big.Int, sendChequeFunc chequebook.SendChequeFunc) error {
func (s *Service) Issue(ctx context.Context, beneficiary common.Address, amount *big.Int, sendChequeFunc chequebook.SendChequeFunc) error {
if s.chequebookIssueFunc != nil {
return s.chequebookIssueFunc(beneficiary, amount, sendChequeFunc)
return s.chequebookIssueFunc(ctx, beneficiary, amount, sendChequeFunc)
}
return nil
}
......
......@@ -92,7 +92,7 @@ func (s *Service) Pay(ctx context.Context, peer swarm.Address, amount uint64) er
if !known {
return ErrUnknownBeneficary
}
err = s.chequebook.Issue(beneficiary, big.NewInt(int64(amount)), func(signedCheque *chequebook.SignedCheque) error {
err = s.chequebook.Issue(ctx, beneficiary, big.NewInt(int64(amount)), func(signedCheque *chequebook.SignedCheque) error {
return s.proto.EmitCheque(ctx, peer, signedCheque)
})
if err != nil {
......
......@@ -266,7 +266,7 @@ func TestPay(t *testing.T) {
peer := swarm.MustParseHexAddress("abcd")
var chequebookCalled bool
chequebookService := mockchequebook.NewChequebook(
mockchequebook.WithChequebookIssueFunc(func(b common.Address, a *big.Int, sendChequeFunc chequebook.SendChequeFunc) error {
mockchequebook.WithChequebookIssueFunc(func(ctx context.Context, b common.Address, a *big.Int, sendChequeFunc chequebook.SendChequeFunc) error {
if b != beneficiary {
t.Fatalf("issuing cheque for wrong beneficiary. wanted %v, got %v", beneficiary, b)
}
......@@ -334,7 +334,7 @@ func TestPayIssueError(t *testing.T) {
peer := swarm.MustParseHexAddress("abcd")
errReject := errors.New("reject")
chequebookService := mockchequebook.NewChequebook(
mockchequebook.WithChequebookIssueFunc(func(b common.Address, a *big.Int, sendChequeFunc chequebook.SendChequeFunc) error {
mockchequebook.WithChequebookIssueFunc(func(ctx context.Context, b common.Address, a *big.Int, sendChequeFunc chequebook.SendChequeFunc) error {
return errReject
}),
)
......
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