Commit 7060397e authored by Ralph Pichler's avatar Ralph Pichler Committed by GitHub

add withdraw and deposit endpoints (#760)

parent f9818d27
......@@ -405,4 +405,56 @@ paths:
'500':
$ref: 'SwarmCommon.yaml#/components/responses/500'
default:
description: Default response
\ No newline at end of file
description: Default response
'/chequebook/deposit':
post:
summary: Deposit tokens from overlay address into chequebook
parameters:
- in: query
name: amount
schema:
type: integer
required: true
description: amount of tokens to deposit
tags:
- Chequebook
responses:
'200':
description: Transaction hash of the deposit transaction
content:
application/json:
schema:
$ref: 'SwarmCommon.yaml#/components/schemas/TransactionResponse'
'400':
$ref: 'SwarmCommon.yaml#/components/responses/404'
'500':
$ref: 'SwarmCommon.yaml#/components/responses/500'
default:
description: Default response
'/chequebook/withdraw':
post:
summary: Withdraw tokens from the chequebook to the overlay address
parameters:
- in: query
name: amount
schema:
type: integer
required: true
description: amount of tokens to withdraw
tags:
- Chequebook
responses:
'200':
description: Transaction hash of the withdraw transaction
content:
application/json:
schema:
$ref: 'SwarmCommon.yaml#/components/schemas/TransactionResponse'
'400':
$ref: 'SwarmCommon.yaml#/components/responses/404'
'500':
$ref: 'SwarmCommon.yaml#/components/responses/500'
default:
description: Default response
\ No newline at end of file
......@@ -18,13 +18,16 @@ import (
)
var (
errChequebookBalance = "cannot get chequebook balance"
errCantLastChequePeer = "cannot get last cheque for peer"
errCantLastCheque = "cannot get last cheque for all peers"
errCannotCash = "cannot cash cheque"
errCannotCashStatus = "cannot get cashout status"
errNoCashout = "no prior cashout"
errNoCheque = "no prior cheque"
errChequebookBalance = "cannot get chequebook balance"
errChequebookNoAmount = "did not specify amount"
errChequebookNoWithdraw = "cannot withdraw"
errChequebookNoDeposit = "cannot deposit"
errCantLastChequePeer = "cannot get last cheque for peer"
errCantLastCheque = "cannot get last cheque for all peers"
errCannotCash = "cannot cash cheque"
errCannotCashStatus = "cannot get cashout status"
errNoCashout = "no prior cashout"
errNoCheque = "no prior cheque"
)
type chequebookBalanceResponse struct {
......@@ -274,3 +277,59 @@ func (s *server) swapCashoutStatusHandler(w http.ResponseWriter, r *http.Request
Result: result,
})
}
type chequebookTxResponse struct {
TransactionHash common.Hash `json:"transactionHash"`
}
func (s *server) chequebookWithdrawHandler(w http.ResponseWriter, r *http.Request) {
amountStr := r.URL.Query().Get("amount")
if amountStr == "" {
jsonhttp.BadRequest(w, errChequebookNoAmount)
s.Logger.Error("debug api: no withdraw amount")
return
}
amount, ok := big.NewInt(0).SetString(amountStr, 10)
if !ok {
jsonhttp.BadRequest(w, errChequebookNoAmount)
s.Logger.Error("debug api: invalid withdraw amount")
return
}
txHash, err := s.Chequebook.Withdraw(r.Context(), amount)
if err != nil {
jsonhttp.InternalServerError(w, errChequebookNoWithdraw)
s.Logger.Debugf("debug api: chequebook withdraw: %v", err)
s.Logger.Error("debug api: cannot withdraw from chequebook")
return
}
jsonhttp.OK(w, chequebookTxResponse{TransactionHash: txHash})
}
func (s *server) chequebookDepositHandler(w http.ResponseWriter, r *http.Request) {
amountStr := r.URL.Query().Get("amount")
if amountStr == "" {
jsonhttp.BadRequest(w, errChequebookNoAmount)
s.Logger.Error("debug api: no deposit amount")
return
}
amount, ok := big.NewInt(0).SetString(amountStr, 10)
if !ok {
jsonhttp.BadRequest(w, errChequebookNoAmount)
s.Logger.Error("debug api: invalid deposit amount")
return
}
txHash, err := s.Chequebook.Deposit(r.Context(), amount)
if err != nil {
jsonhttp.InternalServerError(w, errChequebookNoDeposit)
s.Logger.Debugf("debug api: chequebook deposit: %v", err)
s.Logger.Error("debug api: cannot deposit from chequebook")
return
}
jsonhttp.OK(w, chequebookTxResponse{TransactionHash: txHash})
}
......@@ -46,7 +46,7 @@ func TestChequebookBalance(t *testing.T) {
TotalBalance: returnedBalance,
AvailableBalance: returnedAvailableBalance,
}
// We expect a list of items unordered by peer:
var got *debugapi.ChequebookBalanceResponse
jsonhttptest.Request(t, testServer.Client, http.MethodGet, "/chequebook/balance", http.StatusOK,
jsonhttptest.WithUnmarshalJSONResponse(&got),
......@@ -123,7 +123,60 @@ func TestChequebookAddress(t *testing.T) {
if !reflect.DeepEqual(got, expected) {
t.Errorf("got address: %+v, expected: %+v", got, expected)
}
}
func TestChequebookWithdraw(t *testing.T) {
txHash := common.HexToHash("0xfffff")
chequebookWithdrawFunc := func(ctx context.Context, amount *big.Int) (hash common.Hash, err error) {
if amount.Cmp(big.NewInt(500)) == 0 {
return txHash, nil
}
return common.Hash{}, nil
}
testServer := newTestServer(t, testServerOptions{
ChequebookOpts: []mock.Option{mock.WithChequebookWithdrawFunc(chequebookWithdrawFunc)},
})
expected := &debugapi.ChequebookTxResponse{TransactionHash: txHash}
var got *debugapi.ChequebookTxResponse
jsonhttptest.Request(t, testServer.Client, http.MethodPost, "/chequebook/withdraw?amount=500", http.StatusOK,
jsonhttptest.WithUnmarshalJSONResponse(&got),
)
if !reflect.DeepEqual(got, expected) {
t.Errorf("got address: %+v, expected: %+v", got, expected)
}
}
func TestChequebookDeposit(t *testing.T) {
txHash := common.HexToHash("0xfffff")
chequebookDepositFunc := func(ctx context.Context, amount *big.Int) (hash common.Hash, err error) {
if amount.Cmp(big.NewInt(700)) == 0 {
return txHash, nil
}
return common.Hash{}, nil
}
testServer := newTestServer(t, testServerOptions{
ChequebookOpts: []mock.Option{mock.WithChequebookDepositFunc(chequebookDepositFunc)},
})
expected := &debugapi.ChequebookTxResponse{TransactionHash: txHash}
var got *debugapi.ChequebookTxResponse
jsonhttptest.Request(t, testServer.Client, http.MethodPost, "/chequebook/deposit?amount=700", http.StatusOK,
jsonhttptest.WithUnmarshalJSONResponse(&got),
)
if !reflect.DeepEqual(got, expected) {
t.Errorf("got address: %+v, expected: %+v", got, expected)
}
}
func TestChequebookLastCheques(t *testing.T) {
......
......@@ -21,6 +21,7 @@ type (
ChequebookLastChequePeerResponse = chequebookLastChequePeerResponse
ChequebookLastChequesResponse = chequebookLastChequesResponse
ChequebookLastChequesPeerResponse = chequebookLastChequesPeerResponse
ChequebookTxResponse = chequebookTxResponse
SwapCashoutResponse = swapCashoutResponse
SwapCashoutStatusResponse = swapCashoutStatusResponse
SwapCashoutStatusResult = swapCashoutStatusResult
......
......@@ -103,19 +103,28 @@ func (s *server) setupRouting() {
"GET": http.HandlerFunc(s.chequebookAddressHandler),
})
router.Handle("/chequebook/cheque/{peer}", jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.chequebookLastPeerHandler),
router.Handle("/chequebook/deposit", jsonhttp.MethodHandler{
"POST": http.HandlerFunc(s.chequebookDepositHandler),
})
router.Handle("/chequebook/cheque", jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.chequebookAllLastHandler),
})
router.Handle("/chequebook/cashout/{peer}", jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.swapCashoutStatusHandler),
"POST": http.HandlerFunc(s.swapCashoutHandler),
router.Handle("/chequebook/withdraw", jsonhttp.MethodHandler{
"POST": http.HandlerFunc(s.chequebookWithdrawHandler),
})
}
router.Handle("/chequebook/cheque/{peer}", jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.chequebookLastPeerHandler),
})
router.Handle("/chequebook/cheque", jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.chequebookAllLastHandler),
})
router.Handle("/chequebook/cashout/{peer}", jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.swapCashoutStatusHandler),
"POST": http.HandlerFunc(s.swapCashoutHandler),
})
baseRouter.Handle("/", web.ChainHandlers(
logging.NewHTTPAccessLogHandler(s.Logger, logrus.InfoLevel, "debug api access"),
handlers.CompressHandler,
......
......@@ -30,12 +30,16 @@ const (
var (
// ErrOutOfFunds is the error when the chequebook has not enough free funds for a cheque
ErrOutOfFunds = errors.New("chequebook out of funds")
// ErrInsufficientFunds is the error when the chequebook has not enough free funds for a user action
ErrInsufficientFunds = errors.New("insufficient token balance")
)
// 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)
// Withdraw starts withdrawing erc20 token from the chequebook. This returns once the transactions has been broadcast.
Withdraw(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.
......@@ -123,7 +127,7 @@ func (s *service) Deposit(ctx context.Context, amount *big.Int) (hash common.Has
// check we can afford this so we don't waste gas
if balance.Cmp(amount) < 0 {
return common.Hash{}, errors.New("insufficient token balance")
return common.Hash{}, ErrInsufficientFunds
}
callData, err := s.erc20ABI.Pack("transfer", s.address, amount)
......@@ -325,3 +329,35 @@ func (s *service) LastCheques() (map[common.Address]*SignedCheque, error) {
}
return result, nil
}
func (s *service) Withdraw(ctx context.Context, amount *big.Int) (hash common.Hash, err error) {
availableBalance, err := s.AvailableBalance(ctx)
if err != nil {
return common.Hash{}, err
}
// check we can afford this so we don't waste gas and don't risk bouncing cheques
if availableBalance.Cmp(amount) < 0 {
return common.Hash{}, ErrInsufficientFunds
}
callData, err := s.chequebookABI.Pack("withdraw", amount)
if err != nil {
return common.Hash{}, err
}
request := &TxRequest{
To: s.address,
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
}
......@@ -485,3 +485,113 @@ func TestChequebookIssueOutOfFunds(t *testing.T) {
t.Fatalf("wrong error. wanted %v, got %v", chequebook.ErrNoCheque, err)
}
}
func TestChequebookWithdraw(t *testing.T) {
address := common.HexToAddress("0xabcd")
erc20address := common.HexToAddress("0xefff")
ownerAdress := common.HexToAddress("0xfff")
balance := big.NewInt(30)
withdrawAmount := big.NewInt(20)
txHash := common.HexToHash("0xdddd")
store := storemock.NewStateStore()
chequebookService, err := newTestChequebook(
t,
&backendMock{},
&transactionServiceMock{
send: func(c context.Context, request *chequebook.TxRequest) (common.Hash, error) {
if request.To != address {
t.Fatalf("sending to wrong contract. wanted %x, got %x", address, request.To)
}
if request.Value.Cmp(big.NewInt(0)) != 0 {
t.Fatal("sending ether to token contract")
}
return txHash, nil
},
},
address,
erc20address,
ownerAdress,
store,
&chequeSignerMock{},
&simpleSwapBindingMock{
balance: func(*bind.CallOpts) (*big.Int, error) {
return balance, nil
},
totalPaidOut: func(*bind.CallOpts) (*big.Int, error) {
return big.NewInt(0), nil
},
},
&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.Withdraw(context.Background(), withdrawAmount)
if err != nil {
t.Fatal(err)
}
if txHash != returnedTxHash {
t.Fatalf("returned wrong transaction hash. wanted %v, got %v", txHash, returnedTxHash)
}
}
func TestChequebookWithdrawInsufficientFunds(t *testing.T) {
address := common.HexToAddress("0xabcd")
erc20address := common.HexToAddress("0xefff")
ownerAdress := common.HexToAddress("0xfff")
balance := big.NewInt(30)
withdrawAmount := big.NewInt(20)
txHash := common.HexToHash("0xdddd")
store := storemock.NewStateStore()
chequebookService, err := newTestChequebook(
t,
&backendMock{},
&transactionServiceMock{
send: func(c context.Context, request *chequebook.TxRequest) (common.Hash, error) {
if request.To != address {
t.Fatalf("sending to wrong contract. wanted %x, got %x", address, request.To)
}
if request.Value.Cmp(big.NewInt(0)) != 0 {
t.Fatal("sending ether to token contract")
}
return txHash, nil
},
},
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{
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)
}
_, err = chequebookService.Withdraw(context.Background(), withdrawAmount)
if !errors.Is(err, chequebook.ErrInsufficientFunds) {
t.Fatalf("got wrong error. wanted %v, got %v", chequebook.ErrInsufficientFunds, err)
}
}
......@@ -19,6 +19,8 @@ type Service struct {
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
chequebookWithdrawFunc func(ctx context.Context, amount *big.Int) (hash common.Hash, err error)
chequebookDepositFunc func(ctx context.Context, amount *big.Int) (hash common.Hash, err error)
}
// WithChequebook*Functions set the mock chequebook functions
......@@ -40,12 +42,24 @@ func WithChequebookAddressFunc(f func() common.Address) Option {
})
}
func WithChequebookDepositFunc(f func(ctx context.Context, amount *big.Int) (hash common.Hash, err error)) Option {
return optionFunc(func(s *Service) {
s.chequebookDepositFunc = f
})
}
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
})
}
func WithChequebookWithdrawFunc(f func(ctx context.Context, amount *big.Int) (hash common.Hash, err error)) Option {
return optionFunc(func(s *Service) {
s.chequebookWithdrawFunc = f
})
}
// NewChequebook creates the mock chequebook implementation
func NewChequebook(opts ...Option) chequebook.Service {
mock := new(Service)
......@@ -72,6 +86,9 @@ func (s *Service) AvailableBalance(ctx context.Context) (bal *big.Int, err error
// Deposit mocks the chequebook .Deposit function
func (s *Service) Deposit(ctx context.Context, amount *big.Int) (hash common.Hash, err error) {
if s.chequebookDepositFunc != nil {
return s.chequebookDepositFunc(ctx, amount)
}
return common.Hash{}, errors.New("Error")
}
......@@ -103,6 +120,10 @@ func (s *Service) LastCheques() (map[common.Address]*chequebook.SignedCheque, er
return nil, errors.New("Error")
}
func (s *Service) Withdraw(ctx context.Context, amount *big.Int) (hash common.Hash, err error) {
return s.chequebookWithdrawFunc(ctx, amount)
}
// Option is the option passed to the mock Chequebook service
type Option interface {
apply(*Service)
......
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