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

chequebook: change cashout api to include uncashed amount (#1402)

parent b4811d8c
......@@ -351,16 +351,14 @@ components:
properties:
peer:
$ref: "#/components/schemas/SwarmAddress"
chequebook:
$ref: "#/components/schemas/EthereumAddress"
cumulativePayout:
type: integer
beneficiary:
$ref: "#/components/schemas/EthereumAddress"
lastCashedCheque:
$ref: "#/components/schemas/Cheque"
transactionHash:
$ref: "#/components/schemas/TransactionHash"
result:
$ref: "#/components/schemas/SwapCashoutResult"
uncashedAmount:
type: integer
TagName:
type: string
......
......@@ -251,12 +251,11 @@ type swapCashoutStatusResult struct {
}
type swapCashoutStatusResponse struct {
Peer swarm.Address `json:"peer"`
Chequebook common.Address `json:"chequebook"`
CumulativePayout *big.Int `json:"cumulativePayout"`
Beneficiary common.Address `json:"beneficiary"`
TransactionHash common.Hash `json:"transactionHash"`
Result *swapCashoutStatusResult `json:"result"`
Peer swarm.Address `json:"peer"`
Cheque *chequebookLastChequePeerResponse `json:"lastCashedCheque"`
TransactionHash *common.Hash `json:"transactionHash"`
Result *swapCashoutStatusResult `json:"result"`
UncashedAmount *big.Int `json:"uncashedAmount"`
}
func (s *Service) swapCashoutStatusHandler(w http.ResponseWriter, r *http.Request) {
......@@ -290,21 +289,30 @@ func (s *Service) swapCashoutStatusHandler(w http.ResponseWriter, r *http.Reques
}
var result *swapCashoutStatusResult
if status.Result != nil {
result = &swapCashoutStatusResult{
Recipient: status.Result.Recipient,
LastPayout: status.Result.TotalPayout,
Bounced: status.Result.Bounced,
var txHash *common.Hash
var chequeResponse *chequebookLastChequePeerResponse
if status.Last != nil {
if status.Last.Result != nil {
result = &swapCashoutStatusResult{
Recipient: status.Last.Result.Recipient,
LastPayout: status.Last.Result.TotalPayout,
Bounced: status.Last.Result.Bounced,
}
}
chequeResponse = &chequebookLastChequePeerResponse{
Chequebook: status.Last.Cheque.Chequebook.String(),
Payout: status.Last.Cheque.CumulativePayout,
Beneficiary: status.Last.Cheque.Beneficiary.String(),
}
txHash = &status.Last.TxHash
}
jsonhttp.OK(w, swapCashoutStatusResponse{
Peer: peer,
TransactionHash: status.TxHash,
Chequebook: status.Cheque.Chequebook,
CumulativePayout: status.Cheque.CumulativePayout,
Beneficiary: status.Cheque.Beneficiary,
Result: result,
Peer: peer,
TransactionHash: txHash,
Cheque: chequeResponse,
Result: result,
UncashedAmount: status.UncashedAmount,
})
}
......
......@@ -481,6 +481,7 @@ func TestChequebookCashoutStatus(t *testing.T) {
recipientAddress := common.HexToAddress("efff")
totalPayout := big.NewInt(100)
cumulativePayout := big.NewInt(700)
uncashedAmount := big.NewInt(200)
chequebookAddress := common.HexToAddress("0xcfec")
peer := swarm.MustParseHexAddress("1000000000000000000000000000000000000000000000000000000000000000")
......@@ -504,44 +505,120 @@ func TestChequebookCashoutStatus(t *testing.T) {
Bounced: false,
}
cashoutStatusFunc := func(ctx context.Context, peer swarm.Address) (*chequebook.CashoutStatus, error) {
status := &chequebook.CashoutStatus{
TxHash: actionTxHash,
Cheque: *cheque,
Result: result,
Reverted: false,
t.Run("with result", func(t *testing.T) {
cashoutStatusFunc := func(ctx context.Context, peer swarm.Address) (*chequebook.CashoutStatus, error) {
status := &chequebook.CashoutStatus{
Last: &chequebook.LastCashout{
TxHash: actionTxHash,
Cheque: *cheque,
Result: result,
Reverted: false,
},
UncashedAmount: uncashedAmount,
}
return status, nil
}
return status, nil
}
testServer := newTestServer(t, testServerOptions{
SwapOpts: []swapmock.Option{swapmock.WithCashoutStatusFunc(cashoutStatusFunc)},
testServer := newTestServer(t, testServerOptions{
SwapOpts: []swapmock.Option{swapmock.WithCashoutStatusFunc(cashoutStatusFunc)},
})
expected := &debugapi.SwapCashoutStatusResponse{
Peer: peer,
TransactionHash: &actionTxHash,
Cheque: &debugapi.ChequebookLastChequePeerResponse{
Chequebook: chequebookAddress.String(),
Payout: cumulativePayout,
Beneficiary: cheque.Beneficiary.String(),
},
Result: &debugapi.SwapCashoutStatusResult{
Recipient: recipientAddress,
LastPayout: totalPayout,
Bounced: false,
},
UncashedAmount: uncashedAmount,
}
var got *debugapi.SwapCashoutStatusResponse
jsonhttptest.Request(t, testServer.Client, http.MethodGet, "/chequebook/cashout/"+addr.String(), http.StatusOK,
jsonhttptest.WithUnmarshalJSONResponse(&got),
)
if !reflect.DeepEqual(got, expected) {
t.Fatalf("Got: \n %+v \n\n Expected: \n %+v \n\n", got, expected)
}
})
statusResult := &debugapi.SwapCashoutStatusResult{
Recipient: recipientAddress,
LastPayout: totalPayout,
Bounced: false,
}
t.Run("without result", func(t *testing.T) {
cashoutStatusFunc := func(ctx context.Context, peer swarm.Address) (*chequebook.CashoutStatus, error) {
status := &chequebook.CashoutStatus{
Last: &chequebook.LastCashout{
TxHash: actionTxHash,
Cheque: *cheque,
Result: nil,
Reverted: false,
},
UncashedAmount: uncashedAmount,
}
return status, nil
}
expected := &debugapi.SwapCashoutStatusResponse{
Peer: peer,
TransactionHash: actionTxHash,
Chequebook: chequebookAddress,
CumulativePayout: cumulativePayout,
Beneficiary: cheque.Beneficiary,
Result: statusResult,
}
testServer := newTestServer(t, testServerOptions{
SwapOpts: []swapmock.Option{swapmock.WithCashoutStatusFunc(cashoutStatusFunc)},
})
expected := &debugapi.SwapCashoutStatusResponse{
Peer: peer,
TransactionHash: &actionTxHash,
Cheque: &debugapi.ChequebookLastChequePeerResponse{
Chequebook: chequebookAddress.String(),
Payout: cumulativePayout,
Beneficiary: cheque.Beneficiary.String(),
},
Result: nil,
UncashedAmount: uncashedAmount,
}
var got *debugapi.SwapCashoutStatusResponse
jsonhttptest.Request(t, testServer.Client, http.MethodGet, "/chequebook/cashout/"+addr.String(), http.StatusOK,
jsonhttptest.WithUnmarshalJSONResponse(&got),
)
var got *debugapi.SwapCashoutStatusResponse
jsonhttptest.Request(t, testServer.Client, http.MethodGet, "/chequebook/cashout/"+addr.String(), http.StatusOK,
jsonhttptest.WithUnmarshalJSONResponse(&got),
)
if !reflect.DeepEqual(got, expected) {
t.Fatalf("Got: \n %+v \n\n Expected: \n %+v \n\n", got, expected)
}
if !reflect.DeepEqual(got, expected) {
t.Fatalf("Got: \n %+v \n\n Expected: \n %+v \n\n", got, expected)
}
})
t.Run("without last", func(t *testing.T) {
cashoutStatusFunc := func(ctx context.Context, peer swarm.Address) (*chequebook.CashoutStatus, error) {
status := &chequebook.CashoutStatus{
Last: nil,
UncashedAmount: uncashedAmount,
}
return status, nil
}
testServer := newTestServer(t, testServerOptions{
SwapOpts: []swapmock.Option{swapmock.WithCashoutStatusFunc(cashoutStatusFunc)},
})
expected := &debugapi.SwapCashoutStatusResponse{
Peer: peer,
TransactionHash: nil,
Cheque: nil,
Result: nil,
UncashedAmount: uncashedAmount,
}
var got *debugapi.SwapCashoutStatusResponse
jsonhttptest.Request(t, testServer.Client, http.MethodGet, "/chequebook/cashout/"+addr.String(), http.StatusOK,
jsonhttptest.WithUnmarshalJSONResponse(&got),
)
if !reflect.DeepEqual(got, expected) {
t.Fatalf("Got: \n %+v \n\n Expected: \n %+v \n\n", got, expected)
}
})
}
func LastChequesEqual(a, b *debugapi.ChequebookLastChequesResponse) bool {
......
......@@ -11,6 +11,7 @@ import (
"math/big"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethersphere/bee/pkg/sctx"
......@@ -38,14 +39,20 @@ type cashoutService struct {
chequeStore ChequeStore
}
// CashoutStatus is the action plus its result
type CashoutStatus struct {
// LastCashout contains information about the last cashout
type LastCashout struct {
TxHash common.Hash
Cheque SignedCheque // the cheque that was used to cashout which may be different from the latest cheque
Result *CashChequeResult
Reverted bool
}
// CashoutStatus is information about the last cashout and uncashed amounts
type CashoutStatus struct {
Last *LastCashout // last cashout for a chequebook
UncashedAmount *big.Int // amount not yet cashed out
}
// CashChequeResult summarizes the result of a CashCheque or CashChequeBeneficiary call
type CashChequeResult struct {
Beneficiary common.Address // beneficiary of the cheque
......@@ -91,6 +98,37 @@ func cashoutActionKey(chequebook common.Address) string {
return fmt.Sprintf("swap_cashout_%x", chequebook)
}
func (s *cashoutService) paidOut(ctx context.Context, chequebook, beneficiary common.Address) (*big.Int, error) {
callData, err := chequebookABI.Pack("paidOut", beneficiary)
if err != nil {
return nil, err
}
output, err := s.transactionService.Call(ctx, &transaction.TxRequest{
To: &chequebook,
Data: callData,
})
if err != nil {
return nil, err
}
results, err := chequebookABI.Unpack("paidOut", output)
if err != nil {
return nil, err
}
if len(results) != 1 {
return nil, errDecodeABI
}
paidOut, ok := abi.ConvertType(results[0], new(big.Int)).(*big.Int)
if !ok || paidOut == nil {
return nil, errDecodeABI
}
return paidOut, nil
}
// CashCheque sends a cashout transaction for the last cheque of the chequebook
func (s *cashoutService) CashCheque(ctx context.Context, chequebook, recipient common.Address) (common.Hash, error) {
cheque, err := s.chequeStore.LastCheque(chequebook)
......@@ -133,11 +171,19 @@ func (s *cashoutService) CashCheque(ctx context.Context, chequebook, recipient c
// CashoutStatus gets the status of the latest cashout transaction for the chequebook
func (s *cashoutService) CashoutStatus(ctx context.Context, chequebookAddress common.Address) (*CashoutStatus, error) {
var action *cashoutAction
err := s.store.Get(cashoutActionKey(chequebookAddress), &action)
cheque, err := s.chequeStore.LastCheque(chequebookAddress)
if err != nil {
return nil, err
}
var action cashoutAction
err = s.store.Get(cashoutActionKey(chequebookAddress), &action)
if err != nil {
if errors.Is(err, storage.ErrNotFound) {
return nil, ErrNoCashout
return &CashoutStatus{
Last: nil,
UncashedAmount: cheque.CumulativePayout, // if we never cashed out, assume everything is uncashed
}, nil
}
return nil, err
}
......@@ -153,10 +199,14 @@ func (s *cashoutService) CashoutStatus(ctx context.Context, chequebookAddress co
if pending {
return &CashoutStatus{
TxHash: action.TxHash,
Cheque: action.Cheque,
Result: nil,
Reverted: false,
Last: &LastCashout{
TxHash: action.TxHash,
Cheque: action.Cheque,
Result: nil,
Reverted: false,
},
// uncashed is the difference since the last sent cashout. we assume that the entire cheque will clear in the pending transaction.
UncashedAmount: new(big.Int).Sub(cheque.CumulativePayout, action.Cheque.CumulativePayout),
}, nil
}
......@@ -166,11 +216,21 @@ func (s *cashoutService) CashoutStatus(ctx context.Context, chequebookAddress co
}
if receipt.Status == types.ReceiptStatusFailed {
// if a tx failed (should be almost impossible in practice) we no longer have the necessary information to compute uncashed locally
// assume there are no pending transactions and that the on-chain paidOut is the last cashout action
paidOut, err := s.paidOut(ctx, chequebookAddress, cheque.Beneficiary)
if err != nil {
return nil, err
}
return &CashoutStatus{
TxHash: action.TxHash,
Cheque: action.Cheque,
Result: nil,
Reverted: true,
Last: &LastCashout{
TxHash: action.TxHash,
Cheque: action.Cheque,
Result: nil,
Reverted: true,
},
UncashedAmount: new(big.Int).Sub(cheque.CumulativePayout, paidOut),
}, nil
}
......@@ -180,10 +240,14 @@ func (s *cashoutService) CashoutStatus(ctx context.Context, chequebookAddress co
}
return &CashoutStatus{
TxHash: action.TxHash,
Cheque: action.Cheque,
Result: result,
Reverted: false,
Last: &LastCashout{
TxHash: action.TxHash,
Cheque: action.Cheque,
Result: result,
Reverted: false,
},
// uncashed is the difference since the last sent (and confirmed) cashout.
UncashedAmount: new(big.Int).Sub(cheque.CumulativePayout, result.CumulativePayout),
}, nil
}
......
......@@ -75,15 +75,7 @@ func TestCashout(t *testing.T) {
}),
),
transactionmock.New(
transactionmock.WithSendFunc(func(c context.Context, request *transaction.TxRequest) (common.Hash, error) {
if request.To != nil && *request.To != chequebookAddress {
t.Fatalf("sending to wrong contract. wanted %x, got %x", chequebookAddress, request.To)
}
if request.Value.Cmp(big.NewInt(0)) != 0 {
t.Fatal("sending ether to chequebook contract")
}
return txHash, nil
}),
transactionmock.WithABISend(&chequebookABI, txHash, chequebookAddress, big.NewInt(0), "cashChequeBeneficiary", recipientAddress, cheque.CumulativePayout, cheque.Signature),
),
chequestoremock.NewChequeStore(
chequestoremock.WithLastChequeFunc(func(c common.Address) (*chequebook.SignedCheque, error) {
......@@ -109,35 +101,23 @@ func TestCashout(t *testing.T) {
t.Fatal(err)
}
if status.Reverted {
t.Fatal("reported reverted transaction")
}
if status.TxHash != txHash {
t.Fatalf("wrong transaction hash. wanted %v, got %v", txHash, status.TxHash)
}
if !status.Cheque.Equal(cheque) {
t.Fatalf("wrong cheque in status. wanted %v, got %v", cheque, status.Cheque)
}
if status.Result == nil {
t.Fatal("missing result")
}
expectedResult := &chequebook.CashChequeResult{
Beneficiary: cheque.Beneficiary,
Recipient: recipientAddress,
Caller: cheque.Beneficiary,
TotalPayout: totalPayout,
CumulativePayout: cumulativePayout,
CallerPayout: big.NewInt(0),
Bounced: false,
}
if !status.Result.Equal(expectedResult) {
t.Fatalf("wrong result. wanted %v, got %v", expectedResult, status.Result)
}
verifyStatus(t, status, chequebook.CashoutStatus{
Last: &chequebook.LastCashout{
TxHash: txHash,
Cheque: *cheque,
Result: &chequebook.CashChequeResult{
Beneficiary: cheque.Beneficiary,
Recipient: recipientAddress,
Caller: cheque.Beneficiary,
TotalPayout: totalPayout,
CumulativePayout: cumulativePayout,
CallerPayout: big.NewInt(0),
Bounced: false,
},
Reverted: false,
},
UncashedAmount: big.NewInt(0),
})
}
func TestCashoutBounced(t *testing.T) {
......@@ -193,15 +173,7 @@ func TestCashoutBounced(t *testing.T) {
}),
),
transactionmock.New(
transactionmock.WithSendFunc(func(c context.Context, request *transaction.TxRequest) (common.Hash, error) {
if request.To != nil && *request.To != chequebookAddress {
t.Fatalf("sending to wrong contract. wanted %x, got %x", chequebookAddress, request.To)
}
if request.Value.Cmp(big.NewInt(0)) != 0 {
t.Fatal("sending ether to chequebook contract")
}
return txHash, nil
}),
transactionmock.WithABISend(&chequebookABI, txHash, chequebookAddress, big.NewInt(0), "cashChequeBeneficiary", recipientAddress, cheque.CumulativePayout, cheque.Signature),
),
chequestoremock.NewChequeStore(
chequestoremock.WithLastChequeFunc(func(c common.Address) (*chequebook.SignedCheque, error) {
......@@ -227,35 +199,23 @@ func TestCashoutBounced(t *testing.T) {
t.Fatal(err)
}
if status.Reverted {
t.Fatal("reported reverted transaction")
}
if status.TxHash != txHash {
t.Fatalf("wrong transaction hash. wanted %v, got %v", txHash, status.TxHash)
}
if !status.Cheque.Equal(cheque) {
t.Fatalf("wrong cheque in status. wanted %v, got %v", cheque, status.Cheque)
}
if status.Result == nil {
t.Fatal("missing result")
}
expectedResult := &chequebook.CashChequeResult{
Beneficiary: cheque.Beneficiary,
Recipient: recipientAddress,
Caller: cheque.Beneficiary,
TotalPayout: totalPayout,
CumulativePayout: cumulativePayout,
CallerPayout: big.NewInt(0),
Bounced: true,
}
if !status.Result.Equal(expectedResult) {
t.Fatalf("wrong result. wanted %v, got %v", expectedResult, status.Result)
}
verifyStatus(t, status, chequebook.CashoutStatus{
Last: &chequebook.LastCashout{
TxHash: txHash,
Cheque: *cheque,
Result: &chequebook.CashChequeResult{
Beneficiary: cheque.Beneficiary,
Recipient: recipientAddress,
Caller: cheque.Beneficiary,
TotalPayout: totalPayout,
CumulativePayout: cumulativePayout,
CallerPayout: big.NewInt(0),
Bounced: true,
},
Reverted: false,
},
UncashedAmount: big.NewInt(0),
})
}
func TestCashoutStatusReverted(t *testing.T) {
......@@ -263,10 +223,12 @@ func TestCashoutStatusReverted(t *testing.T) {
recipientAddress := common.HexToAddress("efff")
txHash := common.HexToHash("dddd")
cumulativePayout := big.NewInt(500)
onChainPaidOut := big.NewInt(100)
beneficiary := common.HexToAddress("aaaa")
cheque := &chequebook.SignedCheque{
Cheque: chequebook.Cheque{
Beneficiary: common.HexToAddress("aaaa"),
Beneficiary: beneficiary,
CumulativePayout: cumulativePayout,
Chequebook: chequebookAddress,
},
......@@ -293,9 +255,8 @@ func TestCashoutStatusReverted(t *testing.T) {
}),
),
transactionmock.New(
transactionmock.WithSendFunc(func(ctx context.Context, request *transaction.TxRequest) (common.Hash, error) {
return txHash, nil
}),
transactionmock.WithABISend(&chequebookABI, txHash, chequebookAddress, big.NewInt(0), "cashChequeBeneficiary", recipientAddress, cheque.CumulativePayout, cheque.Signature),
transactionmock.WithABICall(&chequebookABI, onChainPaidOut.FillBytes(make([]byte, 32)), "paidOut", beneficiary),
),
chequestoremock.NewChequeStore(
chequestoremock.WithLastChequeFunc(func(c common.Address) (*chequebook.SignedCheque, error) {
......@@ -321,17 +282,14 @@ func TestCashoutStatusReverted(t *testing.T) {
t.Fatal(err)
}
if !status.Reverted {
t.Fatal("did not report failed transaction as reverted")
}
if status.TxHash != txHash {
t.Fatalf("wrong transaction hash. wanted %v, got %v", txHash, status.TxHash)
}
if !status.Cheque.Equal(cheque) {
t.Fatalf("wrong cheque in status. wanted %v, got %v", cheque, status.Cheque)
}
verifyStatus(t, status, chequebook.CashoutStatus{
Last: &chequebook.LastCashout{
Reverted: true,
TxHash: txHash,
Cheque: *cheque,
},
UncashedAmount: new(big.Int).Sub(cheque.CumulativePayout, onChainPaidOut),
})
}
func TestCashoutStatusPending(t *testing.T) {
......@@ -361,9 +319,7 @@ func TestCashoutStatusPending(t *testing.T) {
}),
),
transactionmock.New(
transactionmock.WithSendFunc(func(c context.Context, request *transaction.TxRequest) (common.Hash, error) {
return txHash, nil
}),
transactionmock.WithABISend(&chequebookABI, txHash, chequebookAddress, big.NewInt(0), "cashChequeBeneficiary", recipientAddress, cheque.CumulativePayout, cheque.Signature),
),
chequestoremock.NewChequeStore(
chequestoremock.WithLastChequeFunc(func(c common.Address) (*chequebook.SignedCheque, error) {
......@@ -389,19 +345,45 @@ func TestCashoutStatusPending(t *testing.T) {
t.Fatal(err)
}
if status.Reverted {
t.Fatal("did report pending transaction as reverted")
}
if status.TxHash != txHash {
t.Fatalf("wrong transaction hash. wanted %v, got %v", txHash, status.TxHash)
}
verifyStatus(t, status, chequebook.CashoutStatus{
Last: &chequebook.LastCashout{
Reverted: false,
TxHash: txHash,
Cheque: *cheque,
Result: nil,
},
UncashedAmount: big.NewInt(0),
})
if !status.Cheque.Equal(cheque) {
t.Fatalf("wrong cheque in status. wanted %v, got %v", cheque, status.Cheque)
}
}
if status.Result != nil {
t.Fatalf("got result for pending cashout: %v", status.Result)
func verifyStatus(t *testing.T, status *chequebook.CashoutStatus, expected chequebook.CashoutStatus) {
if expected.Last == nil {
if status.Last != nil {
t.Fatal("unexpected last cashout")
}
} else {
if status.Last == nil {
t.Fatal("no last cashout")
}
if status.Last.Reverted != expected.Last.Reverted {
t.Fatalf("wrong reverted value. wanted %v, got %v", expected.Last.Reverted, status.Last.Reverted)
}
if status.Last.TxHash != expected.Last.TxHash {
t.Fatalf("wrong transaction hash. wanted %v, got %v", expected.Last.TxHash, status.Last.TxHash)
}
if !status.Last.Cheque.Equal(&expected.Last.Cheque) {
t.Fatalf("wrong cheque in status. wanted %v, got %v", expected.Last.Cheque, status.Last.Cheque)
}
if expected.Last.Result != nil {
if !expected.Last.Result.Equal(status.Last.Result) {
t.Fatalf("wrong result. wanted %v, got %v", expected.Last.Result, status.Last.Result)
}
}
}
if status.UncashedAmount.Cmp(expected.UncashedAmount) != 0 {
t.Fatalf("wrong uncashed amount. wanted %d, got %d", expected.UncashedAmount, status.UncashedAmount)
}
}
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