Commit 9e237f07 authored by metacertain's avatar metacertain Committed by GitHub

Surplus balances for peers (#870)

* Oversettle in debit and payment receive
* Surplus balances for peers, debug API addons (#972)
Additional helper endpoints for integration tests and overdraft safety for surpluses, Openapi update, Fixes
parent 27e96b0e
......@@ -42,7 +42,7 @@ paths:
'/balances':
get:
summary: Get the balances with all known peers
summary: Get the balances with all known peers including prepaid services
tags:
- Balance
responses:
......@@ -59,7 +59,7 @@ paths:
'/balances/{address}':
get:
summary: Get the balances with a specific peer
summary: Get the balances with a specific peer including prepaid services
tags:
- Balance
parameters:
......@@ -71,7 +71,50 @@ paths:
description: Swarm address of peer
responses:
'200':
description: Peer is known
description: Balance with the specific peer
content:
application/json:
schema:
$ref: 'SwarmCommon.yaml#/components/schemas/Balance'
'404':
$ref: 'SwarmCommon.yaml#/components/responses/404'
'500':
$ref: 'SwarmCommon.yaml#/components/responses/500'
default:
description: Default response
'/consumed':
get:
summary: Get the past due consumption balances with all known peers
tags:
- Balance
responses:
'200':
description: Own past due consumption balances with all known peers
content:
application/json:
schema:
$ref: 'SwarmCommon.yaml#/components/schemas/Balances'
'500':
$ref: 'SwarmCommon.yaml#/components/responses/500'
default:
description: Default response
'/consumed/{address}':
get:
summary: Get the past due consumption balance with a specific peer
tags:
- Balance
parameters:
- in: path
name: address
schema:
$ref: 'SwarmCommon.yaml#/components/schemas/SwarmAddress'
required: true
description: Swarm address of peer
responses:
'200':
description: Past-due consumption balance with the specific peer
content:
application/json:
schema:
......
......@@ -22,8 +22,9 @@ import (
)
var (
_ Interface = (*Accounting)(nil)
balancesPrefix string = "balance_"
_ Interface = (*Accounting)(nil)
balancesPrefix string = "balance_"
balancesSurplusPrefix string = "surplusbalance_"
)
// Interface is the Accounting interface.
......@@ -42,8 +43,14 @@ type Interface interface {
Debit(peer swarm.Address, price uint64) error
// Balance returns the current balance for the given peer.
Balance(peer swarm.Address) (int64, error)
// SurplusBalance returns the current surplus balance for the given peer.
SurplusBalance(peer swarm.Address) (int64, error)
// Balances returns balances for all known peers.
Balances() (map[string]int64, error)
// CompensatedBalance returns the current balance deducted by current surplus balance for the given peer.
CompensatedBalance(peer swarm.Address) (int64, error)
// CompensatedBalances returns the compensated balances for all known peers.
CompensatedBalances() (map[string]int64, error)
}
// accountingPeer holds all in-memory accounting information for one peer.
......@@ -83,6 +90,8 @@ var (
ErrPeerNoBalance = errors.New("no balance for peer")
// ErrOverflow denotes an arithmetic operation overflowed.
ErrOverflow = errors.New("overflow error")
// ErrInvalidValue denotes an invalid value read from store
ErrInvalidValue = errors.New("invalid value")
)
// NewAccounting creates a new Accounting instance with the provided options.
......@@ -160,10 +169,25 @@ func (a *Accounting) Reserve(ctx context.Context, peer swarm.Address, price uint
threshold = 0
}
additionalDebt, err := a.SurplusBalance(peer)
if err != nil {
return fmt.Errorf("failed to load surplus balance: %w", err)
}
// uint64 conversion of surplusbalance is safe because surplusbalance is always positive
if additionalDebt < 0 {
return ErrInvalidValue
}
increasedExpectedDebt, err := addI64pU64(expectedDebt, uint64(additionalDebt))
if err != nil {
return err
}
// If our expected debt is less than earlyPayment away from our payment threshold
// and we are actually in debt, trigger settlement.
// we pay early to avoid needlessly blocking request later when concurrent requests occur and we are already close to the payment threshold.
if expectedDebt >= int64(threshold) && currentBalance < 0 {
if increasedExpectedDebt >= int64(threshold) && currentBalance < 0 {
err = a.settle(ctx, peer, accountingPeer)
if err != nil {
return fmt.Errorf("failed to settle with peer %v: %v", peer, err)
......@@ -171,11 +195,15 @@ func (a *Accounting) Reserve(ctx context.Context, peer swarm.Address, price uint
// if we settled successfully our balance is back at 0
// and the expected debt therefore equals next reserved amount
expectedDebt = int64(nextReserved)
increasedExpectedDebt, err = addI64pU64(expectedDebt, uint64(additionalDebt))
if err != nil {
return err
}
}
// if expectedDebt would still exceed the paymentThreshold at this point block this request
// this can happen if there is a large number of concurrent requests to the same peer
if expectedDebt > int64(a.paymentThreshold) {
if increasedExpectedDebt > int64(a.paymentThreshold) {
a.metrics.AccountingBlocksCount.Inc()
return ErrOverdraft
}
......@@ -299,6 +327,57 @@ func (a *Accounting) Debit(peer swarm.Address, price uint64) error {
accountingPeer.lock.Lock()
defer accountingPeer.lock.Unlock()
cost := price
// see if peer has surplus balance to deduct this transaction of
surplusBalance, err := a.SurplusBalance(peer)
if err != nil {
return fmt.Errorf("failed to get surplus balance: %w", err)
}
if surplusBalance > 0 {
// get new surplus balance after deduct
newSurplusBalance, err := subtractI64mU64(surplusBalance, price)
if err != nil {
return err
}
// if nothing left for debiting, store new surplus balance and return from debit
if newSurplusBalance >= 0 {
a.logger.Tracef("surplus debiting peer %v with value %d, new surplus balance is %d", peer, price, newSurplusBalance)
err = a.store.Put(peerSurplusBalanceKey(peer), newSurplusBalance)
if err != nil {
return fmt.Errorf("failed to persist surplus balance: %w", err)
}
// count debit operations, terminate early
a.metrics.TotalDebitedAmount.Add(float64(price))
a.metrics.DebitEventsCount.Inc()
return nil
}
// if surplus balance didn't cover full transaction, let's continue with leftover part as cost
debitIncrease, err := subtractU64mI64(price, surplusBalance)
if err != nil {
return err
}
// conversion to uint64 is safe because we know the relationship between the values by now, but let's make a sanity check
if debitIncrease <= 0 {
return fmt.Errorf("sanity check failed for partial debit after surplus balance drawn")
}
cost = uint64(debitIncrease)
// if we still have something to debit, than have run out of surplus balance,
// let's store 0 as surplus balance
a.logger.Tracef("surplus debiting peer %v with value %d, new surplus balance is 0", peer, debitIncrease)
err = a.store.Put(peerSurplusBalanceKey(peer), 0)
if err != nil {
return fmt.Errorf("failed to persist surplus balance: %w", err)
}
}
currentBalance, err := a.Balance(peer)
if err != nil {
if !errors.Is(err, ErrPeerNoBalance) {
......@@ -307,7 +386,7 @@ func (a *Accounting) Debit(peer swarm.Address, price uint64) error {
}
// Get nextBalance by safely increasing current balance with price
nextBalance, err := addI64pU64(currentBalance, price)
nextBalance, err := addI64pU64(currentBalance, cost)
if err != nil {
return err
}
......@@ -345,11 +424,52 @@ func (a *Accounting) Balance(peer swarm.Address) (balance int64, err error) {
return balance, nil
}
// SurplusBalance returns the current balance for the given peer.
func (a *Accounting) SurplusBalance(peer swarm.Address) (balance int64, err error) {
err = a.store.Get(peerSurplusBalanceKey(peer), &balance)
if err != nil {
if errors.Is(err, storage.ErrNotFound) {
return 0, nil
}
return 0, err
}
return balance, nil
}
// CompensatedBalance returns balance decreased by surplus balance
func (a *Accounting) CompensatedBalance(peer swarm.Address) (compensated int64, err error) {
balance, err := a.Balance(peer)
if err != nil {
return 0, err
}
surplus, err := a.SurplusBalance(peer)
if err != nil {
return 0, err
}
if surplus < 0 {
return 0, ErrInvalidValue
}
// Compensated balance is balance decreased by surplus balance
compensated, err = subtractI64mU64(balance, uint64(surplus))
if err != nil {
return 0, err
}
return compensated, nil
}
// peerBalanceKey returns the balance storage key for the given peer.
func peerBalanceKey(peer swarm.Address) string {
return fmt.Sprintf("%s%s", balancesPrefix, peer.String())
}
// peerSurplusBalanceKey returns the surplus balance storage key for the given peer
func peerSurplusBalanceKey(peer swarm.Address) string {
return fmt.Sprintf("%s%s", balancesSurplusPrefix, peer.String())
}
// getAccountingPeer returns the accountingPeer for a given swarm address.
// If not found in memory it will initialize it.
func (a *Accounting) getAccountingPeer(peer swarm.Address) (*accountingPeer, error) {
......@@ -399,6 +519,55 @@ func (a *Accounting) Balances() (map[string]int64, error) {
return s, nil
}
// Balances gets balances for all peers from store.
func (a *Accounting) CompensatedBalances() (map[string]int64, error) {
s := make(map[string]int64)
err := a.store.Iterate(balancesPrefix, func(key, val []byte) (stop bool, err error) {
addr, err := balanceKeyPeer(key)
if err != nil {
return false, fmt.Errorf("parse address from key: %s: %v", string(key), err)
}
if _, ok := s[addr.String()]; !ok {
value, err := a.CompensatedBalance(addr)
if err != nil {
return false, fmt.Errorf("get peer %s balance: %v", addr.String(), err)
}
s[addr.String()] = value
}
return false, nil
})
if err != nil {
return nil, err
}
err = a.store.Iterate(balancesSurplusPrefix, func(key, val []byte) (stop bool, err error) {
addr, err := surplusBalanceKeyPeer(key)
if err != nil {
return false, fmt.Errorf("parse address from key: %s: %v", string(key), err)
}
if _, ok := s[addr.String()]; !ok {
value, err := a.CompensatedBalance(addr)
if err != nil {
return false, fmt.Errorf("get peer %s balance: %v", addr.String(), err)
}
s[addr.String()] = value
}
return false, nil
})
if err != nil {
return nil, err
}
return s, nil
}
// balanceKeyPeer returns the embedded peer from the balance storage key.
func balanceKeyPeer(key []byte) (swarm.Address, error) {
k := string(key)
......@@ -416,6 +585,22 @@ func balanceKeyPeer(key []byte) (swarm.Address, error) {
return addr, nil
}
func surplusBalanceKeyPeer(key []byte) (swarm.Address, error) {
k := string(key)
split := strings.SplitAfter(k, balancesSurplusPrefix)
if len(split) != 2 {
return swarm.ZeroAddress, errors.New("no peer in key")
}
addr, err := swarm.ParseHexAddress(split[1])
if err != nil {
return swarm.ZeroAddress, err
}
return addr, nil
}
// NotifyPayment implements the PaymentObserver interface. It is called by
// Settlement when we receive a payment.
func (a *Accounting) NotifyPayment(peer swarm.Address, amount uint64) error {
......@@ -432,18 +617,41 @@ func (a *Accounting) NotifyPayment(peer swarm.Address, amount uint64) error {
if !errors.Is(err, ErrPeerNoBalance) {
return err
}
}
// if balance is already negative or zero, we credit full amount received to surplus balance and terminate early
if currentBalance <= 0 {
surplus, err := a.SurplusBalance(peer)
if err != nil {
return fmt.Errorf("failed to get surplus balance: %w", err)
}
increasedSurplus, err := addI64pU64(surplus, amount)
if err != nil {
return err
}
a.logger.Tracef("surplus crediting peer %v with amount %d due to payment, new surplus balance is %d", peer, amount, increasedSurplus)
nextBalance, err := subtractI64mU64(currentBalance, amount)
err = a.store.Put(peerSurplusBalanceKey(peer), increasedSurplus)
if err != nil {
return fmt.Errorf("failed to persist surplus balance: %w", err)
}
return nil
}
// if current balance is positive, let's make a partial credit to
newBalance, err := subtractI64mU64(currentBalance, amount)
if err != nil {
return err
}
// Don't allow a payment to put use more into debt than the tolerance.
// Don't allow a payment to put us into debt
// This is to prevent another node tricking us into settling by settling
// first (e.g. send a bouncing cheque to trigger an honest cheque in swap).
if nextBalance < -int64(a.paymentTolerance) {
return fmt.Errorf("refusing to accept payment which would put us too much in debt, new balance would have been %d", nextBalance)
nextBalance := newBalance
if newBalance < 0 {
nextBalance = 0
}
a.logger.Tracef("crediting peer %v with amount %d due to payment, new balance is %d", peer, amount, nextBalance)
......@@ -453,6 +661,32 @@ func (a *Accounting) NotifyPayment(peer swarm.Address, amount uint64) error {
return fmt.Errorf("failed to persist balance: %w", err)
}
// If payment would have put us into debt, rather, let's add to surplusBalance,
// so as that an oversettlement attempt creates balance for future forwarding services
// charges to be deducted of
if newBalance < 0 {
surplusGrowth, err := subtractU64mI64(amount, currentBalance)
if err != nil {
return err
}
surplus, err := a.SurplusBalance(peer)
if err != nil {
return fmt.Errorf("failed to get surplus balance: %w", err)
}
increasedSurplus := surplus + surplusGrowth
if increasedSurplus < surplus {
return ErrOverflow
}
a.logger.Tracef("surplus crediting peer %v with amount %d due to payment, new surplus balance is %d", peer, surplusGrowth, increasedSurplus)
err = a.store.Put(peerSurplusBalanceKey(peer), increasedSurplus)
if err != nil {
return fmt.Errorf("failed to persist surplus balance: %w", err)
}
}
return nil
}
......@@ -482,6 +716,22 @@ func subtractI64mU64(base int64, subtracted uint64) (result int64, err error) {
return result, nil
}
func subtractU64mI64(base uint64, subtracted int64) (result int64, err error) {
if base > math.MaxInt64 {
return 0, ErrOverflow
}
// base is positive, overflow can happen by subtracting negative number
result = int64(base) - subtracted
if subtracted < 0 {
if result < int64(base) {
return 0, ErrOverflow
}
}
return result, nil
}
// addI64pU64 is a helper function for safe addition of Int64 + Uint64
// It checks for
// - overflow safety in conversion of uint64 to int64
......
......@@ -225,7 +225,7 @@ func TestAccountingOverflowReserve(t *testing.T) {
}
}
func TestAccountingOverflowNotifyPayment(t *testing.T) {
func TestAccountingOverflowSurplusBalance(t *testing.T) {
logger := logging.New(ioutil.Discard, 0)
store := mock.NewStateStore()
......@@ -241,11 +241,32 @@ func TestAccountingOverflowNotifyPayment(t *testing.T) {
if err != nil {
t.Fatal(err)
}
// Try Crediting a large amount to peer so balance is negative
err = acc.Credit(peer1Addr, math.MaxInt64)
// Try Debiting a large amount to peer so balance is large positive
err = acc.Debit(peer1Addr, testPaymentThresholdLarge-1)
if err != nil {
t.Fatal(err)
}
// Notify of incoming payment from same peer, so balance goes to 0 with surplusbalance 2
err = acc.NotifyPayment(peer1Addr, math.MaxInt64)
if err != nil {
t.Fatal("Unexpected overflow from NotifyPayment")
}
// sanity check surplus balance
val, err := acc.SurplusBalance(peer1Addr)
if err != nil {
t.Fatal("Error checking Surplusbalance")
}
if val != 2 {
t.Fatal("Not expected surplus balance")
}
// sanity check balance
val, err = acc.Balance(peer1Addr)
if err != nil {
t.Fatal("Error checking Balance")
}
if val != 0 {
t.Fatal("Unexpected balance")
}
// Notify of incoming payment from same peer, further decreasing balance, this should overflow
err = acc.NotifyPayment(peer1Addr, math.MaxInt64)
if err == nil {
......@@ -253,10 +274,44 @@ func TestAccountingOverflowNotifyPayment(t *testing.T) {
}
// If we had other error, assert fail
if !errors.Is(err, accounting.ErrOverflow) {
t.Fatalf("expected overflow error from Debit, got %v", err)
t.Fatal("Expected overflow error from NotifyPayment")
}
}
func TestAccountingOverflowNotifyPayment(t *testing.T) {
logger := logging.New(ioutil.Discard, 0)
store := mock.NewStateStore()
defer store.Close()
settlement := mockSettlement.NewSettlement()
acc, err := accounting.NewAccounting(testPaymentThresholdLarge, 0, 0, logger, store, settlement, nil)
if err != nil {
t.Fatal(err)
}
peer1Addr, err := swarm.ParseHexAddress("00112233")
if err != nil {
t.Fatal(err)
}
// Try Crediting a large amount to peer so balance is negative
err = acc.Credit(peer1Addr, math.MaxInt64)
if err != nil {
t.Fatal(err)
}
// NotifyPayment for peer should now fill the surplus balance
err = acc.NotifyPayment(peer1Addr, math.MaxInt64)
if err != nil {
t.Fatalf("Expected no error but got one: %v", err)
}
// Notify of incoming payment from same peer, further increasing the surplus balance into an overflow
err = acc.NotifyPayment(peer1Addr, 1)
if !errors.Is(err, accounting.ErrOverflow) {
t.Fatalf("expected overflow error from Debit, got %v", err)
}
}
func TestAccountingOverflowDebit(t *testing.T) {
logger := logging.New(ioutil.Discard, 0)
......@@ -528,6 +583,113 @@ func TestAccountingCallSettlementEarly(t *testing.T) {
}
}
func TestAccountingSurplusBalance(t *testing.T) {
logger := logging.New(ioutil.Discard, 0)
store := mock.NewStateStore()
defer store.Close()
settlement := mockSettlement.NewSettlement()
acc, err := accounting.NewAccounting(testPaymentThreshold, 0, 0, logger, store, settlement, nil)
if err != nil {
t.Fatal(err)
}
peer1Addr, err := swarm.ParseHexAddress("00112233")
if err != nil {
t.Fatal(err)
}
// Try Debiting a large amount to peer so balance is large positive
err = acc.Debit(peer1Addr, testPaymentThreshold-1)
if err != nil {
t.Fatal(err)
}
// Notify of incoming payment from same peer, so balance goes to 0 with surplusbalance 2
err = acc.NotifyPayment(peer1Addr, testPaymentThreshold+1)
if err != nil {
t.Fatal("Unexpected overflow from doable NotifyPayment")
}
//sanity check surplus balance
val, err := acc.SurplusBalance(peer1Addr)
if err != nil {
t.Fatal("Error checking Surplusbalance")
}
if val != 2 {
t.Fatal("Not expected surplus balance")
}
//sanity check balance
val, err = acc.Balance(peer1Addr)
if err != nil {
t.Fatal("Error checking Balance")
}
if val != 0 {
t.Fatal("Not expected balance")
}
// Notify of incoming payment from same peer, so balance goes to 0 with surplusbalance 10002 (testpaymentthreshold+2)
err = acc.NotifyPayment(peer1Addr, testPaymentThreshold)
if err != nil {
t.Fatal("Unexpected error from NotifyPayment")
}
//sanity check surplus balance
val, err = acc.SurplusBalance(peer1Addr)
if err != nil {
t.Fatal("Error checking Surplusbalance")
}
if val != testPaymentThreshold+2 {
t.Fatal("Unexpected surplus balance")
}
//sanity check balance
val, err = acc.Balance(peer1Addr)
if err != nil {
t.Fatal("Error checking Balance")
}
if val != 0 {
t.Fatal("Not expected balance, expected 0")
}
// Debit for same peer, so balance stays 0 with surplusbalance decreasing to 2
err = acc.Debit(peer1Addr, testPaymentThreshold)
if err != nil {
t.Fatal("Unexpected error from Credit")
}
// samity check surplus balance
val, err = acc.SurplusBalance(peer1Addr)
if err != nil {
t.Fatal("Error checking Surplusbalance")
}
if val != 2 {
t.Fatal("Unexpected surplus balance")
}
//sanity check balance
val, err = acc.Balance(peer1Addr)
if err != nil {
t.Fatal("Error checking Balance")
}
if val != 0 {
t.Fatal("Not expected balance, expected 0")
}
// Debit for same peer, so balance goes to 9998 (testpaymentthreshold - 2) with surplusbalance decreasing to 0
err = acc.Debit(peer1Addr, testPaymentThreshold)
if err != nil {
t.Fatal("Unexpected error from NotifyPayment")
}
// samity check surplus balance
val, err = acc.SurplusBalance(peer1Addr)
if err != nil {
t.Fatal("Error checking Surplusbalance")
}
if val != 0 {
t.Fatal("Unexpected surplus balance")
}
//sanity check balance
val, err = acc.Balance(peer1Addr)
if err != nil {
t.Fatal("Error checking Balance")
}
if val != testPaymentThreshold-2 {
t.Fatal("Not expected balance, expected 0")
}
}
// TestAccountingNotifyPayment tests that payments adjust the balance and payment which put us into debt are rejected
func TestAccountingNotifyPayment(t *testing.T) {
logger := logging.New(ioutil.Discard, 0)
......@@ -562,8 +724,8 @@ func TestAccountingNotifyPayment(t *testing.T) {
}
err = acc.NotifyPayment(peer1Addr, debtAmount+testPaymentTolerance+1)
if err == nil {
t.Fatal("expected payment to be rejected")
if err != nil {
t.Fatal(err)
}
}
......
......@@ -14,14 +14,18 @@ import (
// Service is the mock Accounting service.
type Service struct {
lock sync.Mutex
balances map[string]int64
reserveFunc func(ctx context.Context, peer swarm.Address, price uint64) error
releaseFunc func(peer swarm.Address, price uint64)
creditFunc func(peer swarm.Address, price uint64) error
debitFunc func(peer swarm.Address, price uint64) error
balanceFunc func(swarm.Address) (int64, error)
balancesFunc func() (map[string]int64, error)
lock sync.Mutex
balances map[string]int64
reserveFunc func(ctx context.Context, peer swarm.Address, price uint64) error
releaseFunc func(peer swarm.Address, price uint64)
creditFunc func(peer swarm.Address, price uint64) error
debitFunc func(peer swarm.Address, price uint64) error
balanceFunc func(swarm.Address) (int64, error)
balancesFunc func() (map[string]int64, error)
compensatedBalanceFunc func(swarm.Address) (int64, error)
compensatedBalancesFunc func() (map[string]int64, error)
balanceSurplusFunc func(swarm.Address) (int64, error)
}
// WithReserveFunc sets the mock Reserve function
......@@ -66,6 +70,27 @@ func WithBalancesFunc(f func() (map[string]int64, error)) Option {
})
}
// WithCompensatedBalanceFunc sets the mock Balance function
func WithCompensatedBalanceFunc(f func(swarm.Address) (int64, error)) Option {
return optionFunc(func(s *Service) {
s.compensatedBalanceFunc = f
})
}
// WithCompensatedBalancesFunc sets the mock Balances function
func WithCompensatedBalancesFunc(f func() (map[string]int64, error)) Option {
return optionFunc(func(s *Service) {
s.compensatedBalancesFunc = f
})
}
// WithBalanceSurplusFunc sets the mock SurplusBalance function
func WithBalanceSurplusFunc(f func(swarm.Address) (int64, error)) Option {
return optionFunc(func(s *Service) {
s.balanceSurplusFunc = f
})
}
// NewAccounting creates the mock accounting implementation
func NewAccounting(opts ...Option) accounting.Interface {
mock := new(Service)
......@@ -131,6 +156,34 @@ func (s *Service) Balances() (map[string]int64, error) {
return s.balances, nil
}
// CompensatedBalance is the mock function wrapper that calls the set implementation
func (s *Service) CompensatedBalance(peer swarm.Address) (int64, error) {
if s.compensatedBalanceFunc != nil {
return s.compensatedBalanceFunc(peer)
}
s.lock.Lock()
defer s.lock.Unlock()
return s.balances[peer.String()], nil
}
// CompensatedBalances is the mock function wrapper that calls the set implementation
func (s *Service) CompensatedBalances() (map[string]int64, error) {
if s.compensatedBalancesFunc != nil {
return s.compensatedBalancesFunc()
}
return s.balances, nil
}
//
func (s *Service) SurplusBalance(peer swarm.Address) (int64, error) {
if s.balanceFunc != nil {
return s.balanceSurplusFunc(peer)
}
s.lock.Lock()
defer s.lock.Unlock()
return 0, nil
}
// Option is the option passed to the mock accounting service
type Option interface {
apply(*Service)
......
......@@ -79,3 +79,53 @@ func (s *server) peerBalanceHandler(w http.ResponseWriter, r *http.Request) {
Balance: balance,
})
}
func (s *server) compensatedBalancesHandler(w http.ResponseWriter, r *http.Request) {
balances, err := s.Accounting.CompensatedBalances()
if err != nil {
jsonhttp.InternalServerError(w, errCantBalances)
s.Logger.Debugf("debug api: compensated balances: %v", err)
s.Logger.Error("debug api: can not get compensated balances")
return
}
balResponses := make([]balanceResponse, len(balances))
i := 0
for k := range balances {
balResponses[i] = balanceResponse{
Peer: k,
Balance: balances[k],
}
i++
}
jsonhttp.OK(w, balancesResponse{Balances: balResponses})
}
func (s *server) compensatedPeerBalanceHandler(w http.ResponseWriter, r *http.Request) {
addr := mux.Vars(r)["peer"]
peer, err := swarm.ParseHexAddress(addr)
if err != nil {
s.Logger.Debugf("debug api: compensated balances peer: invalid peer address %s: %v", addr, err)
s.Logger.Errorf("debug api: compensated balances peer: invalid peer address %s", addr)
jsonhttp.NotFound(w, errInvaliAddress)
return
}
balance, err := s.Accounting.CompensatedBalance(peer)
if err != nil {
if errors.Is(err, accounting.ErrPeerNoBalance) {
jsonhttp.NotFound(w, errNoBalance)
return
}
s.Logger.Debugf("debug api: compensated balances peer: get peer %s balance: %v", peer.String(), err)
s.Logger.Errorf("debug api: compensated balances peer: can't get peer %s balance", peer.String())
jsonhttp.InternalServerError(w, errCantBalance)
return
}
jsonhttp.OK(w, balanceResponse{
Peer: peer.String(),
Balance: balance,
})
}
......@@ -19,7 +19,7 @@ import (
)
func TestBalances(t *testing.T) {
balancesFunc := func() (ret map[string]int64, err error) {
compensatedBalancesFunc := func() (ret map[string]int64, err error) {
ret = make(map[string]int64)
ret["DEAD"] = 1000000000000000000
ret["BEEF"] = -100000000000000000
......@@ -27,7 +27,7 @@ func TestBalances(t *testing.T) {
return ret, err
}
testServer := newTestServer(t, testServerOptions{
AccountingOpts: []mock.Option{mock.WithBalancesFunc(balancesFunc)},
AccountingOpts: []mock.Option{mock.WithCompensatedBalancesFunc(compensatedBalancesFunc)},
})
expected := &debugapi.BalancesResponse{
......@@ -61,11 +61,11 @@ func TestBalances(t *testing.T) {
func TestBalancesError(t *testing.T) {
wantErr := errors.New("ASDF")
balancesFunc := func() (ret map[string]int64, err error) {
compensatedBalancesFunc := func() (ret map[string]int64, err error) {
return nil, wantErr
}
testServer := newTestServer(t, testServerOptions{
AccountingOpts: []mock.Option{mock.WithBalancesFunc(balancesFunc)},
AccountingOpts: []mock.Option{mock.WithCompensatedBalancesFunc(compensatedBalancesFunc)},
})
jsonhttptest.Request(t, testServer.Client, http.MethodGet, "/balances", http.StatusInternalServerError,
......@@ -78,11 +78,11 @@ func TestBalancesError(t *testing.T) {
func TestBalancesPeers(t *testing.T) {
peer := "bff2c89e85e78c38bd89fca1acc996afb876c21bf5a8482ad798ce15f1c223fa"
balanceFunc := func(swarm.Address) (int64, error) {
compensatedBalanceFunc := func(swarm.Address) (int64, error) {
return 1000000000000000000, nil
}
testServer := newTestServer(t, testServerOptions{
AccountingOpts: []mock.Option{mock.WithBalanceFunc(balanceFunc)},
AccountingOpts: []mock.Option{mock.WithCompensatedBalanceFunc(compensatedBalanceFunc)},
})
jsonhttptest.Request(t, testServer.Client, http.MethodGet, "/balances/"+peer, http.StatusOK,
......@@ -96,11 +96,11 @@ func TestBalancesPeers(t *testing.T) {
func TestBalancesPeersError(t *testing.T) {
peer := "bff2c89e85e78c38bd89fca1acc996afb876c21bf5a8482ad798ce15f1c223fa"
wantErr := errors.New("Error")
balanceFunc := func(swarm.Address) (int64, error) {
compensatedBalanceFunc := func(swarm.Address) (int64, error) {
return 0, wantErr
}
testServer := newTestServer(t, testServerOptions{
AccountingOpts: []mock.Option{mock.WithBalanceFunc(balanceFunc)},
AccountingOpts: []mock.Option{mock.WithCompensatedBalanceFunc(compensatedBalanceFunc)},
})
jsonhttptest.Request(t, testServer.Client, http.MethodGet, "/balances/"+peer, http.StatusInternalServerError,
......@@ -113,11 +113,11 @@ func TestBalancesPeersError(t *testing.T) {
func TestBalancesPeersNoBalance(t *testing.T) {
peer := "bff2c89e85e78c38bd89fca1acc996afb876c21bf5a8482ad798ce15f1c223fa"
balanceFunc := func(swarm.Address) (int64, error) {
compensatedBalanceFunc := func(swarm.Address) (int64, error) {
return 0, accounting.ErrPeerNoBalance
}
testServer := newTestServer(t, testServerOptions{
AccountingOpts: []mock.Option{mock.WithBalanceFunc(balanceFunc)},
AccountingOpts: []mock.Option{mock.WithCompensatedBalanceFunc(compensatedBalanceFunc)},
})
jsonhttptest.Request(t, testServer.Client, http.MethodGet, "/balances/"+peer, http.StatusNotFound,
......@@ -170,3 +170,126 @@ func equalBalances(a, b *debugapi.BalancesResponse) bool {
return true
}
func TestConsumedBalances(t *testing.T) {
balancesFunc := func() (ret map[string]int64, err error) {
ret = make(map[string]int64)
ret["DEAD"] = 1000000000000000000
ret["BEEF"] = -100000000000000000
ret["PARTY"] = 0
return ret, err
}
testServer := newTestServer(t, testServerOptions{
AccountingOpts: []mock.Option{mock.WithBalancesFunc(balancesFunc)},
})
expected := &debugapi.BalancesResponse{
[]debugapi.BalanceResponse{
{
Peer: "DEAD",
Balance: 1000000000000000000,
},
{
Peer: "BEEF",
Balance: -100000000000000000,
},
{
Peer: "PARTY",
Balance: 0,
},
},
}
// We expect a list of items unordered by peer:
var got *debugapi.BalancesResponse
jsonhttptest.Request(t, testServer.Client, http.MethodGet, "/consumed", http.StatusOK,
jsonhttptest.WithUnmarshalJSONResponse(&got),
)
if !equalBalances(got, expected) {
t.Errorf("got balances: %v, expected: %v", got, expected)
}
}
func TestConsumedError(t *testing.T) {
wantErr := errors.New("ASDF")
balancesFunc := func() (ret map[string]int64, err error) {
return nil, wantErr
}
testServer := newTestServer(t, testServerOptions{
AccountingOpts: []mock.Option{mock.WithBalancesFunc(balancesFunc)},
})
jsonhttptest.Request(t, testServer.Client, http.MethodGet, "/consumed", http.StatusInternalServerError,
jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
Message: debugapi.ErrCantBalances,
Code: http.StatusInternalServerError,
}),
)
}
func TestConsumedPeers(t *testing.T) {
peer := "bff2c89e85e78c38bd89fca1acc996afb876c21bf5a8482ad798ce15f1c223fa"
balanceFunc := func(swarm.Address) (int64, error) {
return 1000000000000000000, nil
}
testServer := newTestServer(t, testServerOptions{
AccountingOpts: []mock.Option{mock.WithBalanceFunc(balanceFunc)},
})
jsonhttptest.Request(t, testServer.Client, http.MethodGet, "/consumed/"+peer, http.StatusOK,
jsonhttptest.WithExpectedJSONResponse(debugapi.BalanceResponse{
Peer: peer,
Balance: 1000000000000000000,
}),
)
}
func TestConsumedPeersError(t *testing.T) {
peer := "bff2c89e85e78c38bd89fca1acc996afb876c21bf5a8482ad798ce15f1c223fa"
wantErr := errors.New("Error")
balanceFunc := func(swarm.Address) (int64, error) {
return 0, wantErr
}
testServer := newTestServer(t, testServerOptions{
AccountingOpts: []mock.Option{mock.WithBalanceFunc(balanceFunc)},
})
jsonhttptest.Request(t, testServer.Client, http.MethodGet, "/consumed/"+peer, http.StatusInternalServerError,
jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
Message: debugapi.ErrCantBalance,
Code: http.StatusInternalServerError,
}),
)
}
func TestConsumedPeersNoBalance(t *testing.T) {
peer := "bff2c89e85e78c38bd89fca1acc996afb876c21bf5a8482ad798ce15f1c223fa"
balanceFunc := func(swarm.Address) (int64, error) {
return 0, accounting.ErrPeerNoBalance
}
testServer := newTestServer(t, testServerOptions{
AccountingOpts: []mock.Option{mock.WithBalanceFunc(balanceFunc)},
})
jsonhttptest.Request(t, testServer.Client, http.MethodGet, "/consumed/"+peer, http.StatusNotFound,
jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
Message: debugapi.ErrNoBalance,
Code: http.StatusNotFound,
}),
)
}
func TestConsumedInvalidAddress(t *testing.T) {
peer := "bad peer address"
testServer := newTestServer(t, testServerOptions{})
jsonhttptest.Request(t, testServer.Client, http.MethodGet, "/consumed/"+peer, http.StatusNotFound,
jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
Message: debugapi.ErrInvaliAddress,
Code: http.StatusNotFound,
}),
)
}
......@@ -84,16 +84,27 @@ func (s *server) setupRouting() {
web.FinalHandlerFunc(s.setWelcomeMessageHandler),
),
})
router.Handle("/balances", jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.balancesHandler),
"GET": http.HandlerFunc(s.compensatedBalancesHandler),
})
router.Handle("/balances/{peer}", jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.compensatedPeerBalanceHandler),
})
router.Handle("/consumed", jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.balancesHandler),
})
router.Handle("/consumed/{peer}", jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.peerBalanceHandler),
})
router.Handle("/settlements", jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.settlementsHandler),
})
router.Handle("/settlements/{peer}", jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.peerSettlementsHandler),
})
......
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