Skip to content

Commit

Permalink
client/rpcserver: Add withdraw route
Browse files Browse the repository at this point in the history
  • Loading branch information
JoeGruffins committed Jun 15, 2020
1 parent 558c54a commit eac1b9c
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 33 deletions.
1 change: 1 addition & 0 deletions client/cmd/dexcctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ var promptPasswords = map[string][]string{
"openwallet": {"App password:"},
"register": {"App password:"},
"trade": {"App password:"},
"withdraw": {"App password:"},
}

// optionalTextFiles is a map of routes to arg index for routes that should read
Expand Down
46 changes: 41 additions & 5 deletions client/rpcserver/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const (
tradeRoute = "trade"
versionRoute = "version"
walletsRoute = "wallets"
withdrawRoute = "withdraw"
)

const (
Expand Down Expand Up @@ -73,6 +74,7 @@ var routes = map[string]func(s *RPCServer, params *RawParams) *msgjson.ResponseP
tradeRoute: handleTrade,
versionRoute: handleVersion,
walletsRoute: handleWallets,
withdrawRoute: handleWithdraw,
}

// handleHelp handles requests for help. Returns general help for all commands
Expand Down Expand Up @@ -143,8 +145,7 @@ func handleNewWallet(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
form.WalletPass.Clear()
}()

exists := s.core.WalletState(form.AssetID) != nil
if exists {
if s.core.WalletState(form.AssetID) != nil {
errMsg := fmt.Sprintf("error creating %s wallet: wallet already exists",
dex.BipIDSymbol(form.AssetID))
resErr := msgjson.NewError(msgjson.RPCWalletExistsError, errMsg)
Expand Down Expand Up @@ -385,11 +386,30 @@ func handleCancel(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
if err := s.core.Cancel(form.AppPass, form.OrderID); err != nil {
errMsg := fmt.Sprintf("unable to cancel order %q: %v", form.OrderID, err)
resErr := msgjson.NewError(msgjson.RPCCancelError, errMsg)
return createResponse(tradeRoute, nil, resErr)
return createResponse(cancelRoute, nil, resErr)
}
res := fmt.Sprintf(canceledOrderStr, form.OrderID)

return createResponse(cancelRoute, &res, nil)
}

// handleWithdraw handles requests for withdraw. *msgjson.ResponsePayload.Error
// is empty if successful.
func handleWithdraw(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
form, err := parseWithdrawArgs(params)
if err != nil {
return usage(withdrawRoute, err)
}
defer form.AppPass.Clear()
coin, err := s.core.Withdraw(form.AppPass, form.AssetID, form.Value, form.Address)
if err != nil {
errMsg := fmt.Sprintf("unable to withdraw: %v", err)
resErr := msgjson.NewError(msgjson.RPCWithdrawError, errMsg)
return createResponse(withdrawRoute, nil, resErr)
}
resp := fmt.Sprintf(canceledOrderStr, form.OrderID)
res := coin.String()

return createResponse(cancelRoute, &resp, nil)
return createResponse(withdrawRoute, &res, nil)
}

// format concatenates thing and tail. If thing is empty, returns an empty
Expand Down Expand Up @@ -739,4 +759,20 @@ Registration is complete after the fee transaction has been confirmed.`,
returns: `Returns:
string: The message "` + fmt.Sprintf(canceledOrderStr, "[order ID]") + `"`,
},
withdrawRoute: {
pwArgsShort: `"appPass"`,
argsShort: `assetID value "address"`,
cmdSummary: `Withdraw value from an exchange wallet to address.`,
pwArgsLong: `Password Args:
appPass (string): The DEX client password.`,
argsLong: `Args:
assetID (int): The asset's BIP-44 registered coin index. Used to identify
which wallet to withdraw from. e.g. 42 for DCR. See
https://github.com/satoshilabs/slips/blob/master/slip-0044.md
value (int): The amount to withdraw in units of the asset's smallest
denomination (e.g. satoshis, atoms, etc.)"
address (string): The address to which withdrawn funds are sent.`,
returns: `Returns:
string: "[coin ID]"`,
},
}
71 changes: 71 additions & 0 deletions client/rpcserver/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import (
"reflect"
"testing"

"decred.org/dcrdex/client/asset"
"decred.org/dcrdex/client/core"
"decred.org/dcrdex/dex"
"decred.org/dcrdex/dex/encode"
"decred.org/dcrdex/dex/msgjson"
)
Expand Down Expand Up @@ -717,3 +719,72 @@ func TestHandleCancel(t *testing.T) {
}
}
}

// tCoin satifies the asset.Coin interface.
type tCoin struct{}

func (tCoin) ID() dex.Bytes {
return nil
}
func (tCoin) String() string {
return ""
}
func (tCoin) Value() uint64 {
return 0
}
func (tCoin) Confirmations() (uint32, error) {
return 0, nil
}
func (tCoin) Redeem() dex.Bytes {
return nil
}

func TestHandleWithdraw(t *testing.T) {
pw := encode.PassBytes("password123")
params := &RawParams{
PWArgs: []encode.PassBytes{pw},
Args: []string{
"42",
"1000",
"abc",
},
}
tests := []struct {
name string
params *RawParams
walletState *core.WalletState
coin asset.Coin
withdrawErr error
wantErrCode int
}{{
name: "ok",
params: params,
walletState: &core.WalletState{},
coin: tCoin{},
wantErrCode: -1,
}, {
name: "core.Withdraw error",
params: params,
walletState: &core.WalletState{},
coin: tCoin{},
withdrawErr: errors.New("error"),
wantErrCode: msgjson.RPCWithdrawError,
}, {
name: "bad params",
params: &RawParams{},
wantErrCode: msgjson.RPCArgumentsError,
}}
for _, test := range tests {
tc := &TCore{
walletState: test.walletState,
coin: test.coin,
withdrawErr: test.withdrawErr,
}
r := &RPCServer{core: tc}
payload := handleWithdraw(r, test.params)
res := ""
if err := verifyResponse(payload, &res, test.wantErrCode); err != nil {
t.Fatal(err)
}
}
}
2 changes: 2 additions & 0 deletions client/rpcserver/rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"sync"
"time"

"decred.org/dcrdex/client/asset"
"decred.org/dcrdex/client/core"
"decred.org/dcrdex/client/db"
"decred.org/dcrdex/dex"
Expand Down Expand Up @@ -67,6 +68,7 @@ type ClientCore interface {
Trade(appPass []byte, form *core.TradeForm) (order *core.Order, err error)
WalletState(assetID uint32) (walletState *core.WalletState)
Wallets() (walletsStates []*core.WalletState)
Withdraw(appPass []byte, assetID uint32, value uint64, addr string) (asset.Coin, error)
}

// marketSyncer is used to synchronize market subscriptions. The marketSyncer
Expand Down
6 changes: 6 additions & 0 deletions client/rpcserver/rpcserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"testing"
"time"

"decred.org/dcrdex/client/asset"
"decred.org/dcrdex/client/core"
"decred.org/dcrdex/client/db"
"decred.org/dcrdex/dex"
Expand Down Expand Up @@ -57,6 +58,8 @@ type TCore struct {
order *core.Order
tradeErr error
cancelErr error
coin asset.Coin
withdrawErr error
}

func (c *TCore) Balance(uint32) (uint64, error) {
Expand Down Expand Up @@ -105,6 +108,9 @@ func (c *TCore) Wallets() []*core.WalletState {
func (c *TCore) WalletState(assetID uint32) *core.WalletState {
return c.walletState
}
func (c *TCore) Withdraw(pw []byte, assetID uint32, value uint64, addr string) (asset.Coin, error) {
return c.coin, c.withdrawErr
}

type TWriter struct {
b []byte
Expand Down
29 changes: 29 additions & 0 deletions client/rpcserver/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ type cancelForm struct {
OrderID string `json:"orderID"`
}

// withdrawForm is information necessary to withdraw funds.
type withdrawForm struct {
AppPass encode.PassBytes `json:"appPass"`
AssetID uint32 `json:"assetID"`
Value uint64 `json:"value"`
Address string `json:"address"`
}

// checkNArgs checks that args and pwArgs are the correct length.
func checkNArgs(params *RawParams, nPWArgs, nArgs []int) error {
// For want, one integer indicates an exact match, two are the min and max.
Expand Down Expand Up @@ -296,3 +304,24 @@ func parseCancelArgs(params *RawParams) (*cancelForm, error) {
}
return &cancelForm{AppPass: params.PWArgs[0], OrderID: id}, nil
}

func parseWithdrawArgs(params *RawParams) (*withdrawForm, error) {
if err := checkNArgs(params, []int{1}, []int{3}); err != nil {
return nil, err
}
assetID, err := checkUIntArg(params.Args[0], "assetID", 32)
if err != nil {
return nil, err
}
value, err := checkUIntArg(params.Args[1], "value", 64)
if err != nil {
return nil, err
}
req := &withdrawForm{
AppPass: params.PWArgs[0],
AssetID: uint32(assetID),
Value: value,
Address: params.Args[2],
}
return req, nil
}
47 changes: 47 additions & 0 deletions client/rpcserver/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -465,3 +465,50 @@ func TestParseCancelArgs(t *testing.T) {
}
}
}

func TestParseWithdrawArgs(t *testing.T) {
paramsWithArgs := func(id, value string) *RawParams {
pw := encode.PassBytes("password123")
pwArgs := []encode.PassBytes{pw}
args := []string{
id,
value,
"abc",
}
return &RawParams{PWArgs: pwArgs, Args: args}
}
tests := []struct {
name string
params *RawParams
wantErr error
}{{
name: "ok",
params: paramsWithArgs("42", "5000"),
}, {
name: "assetID is not int",
params: paramsWithArgs("42.1", "5000"),
wantErr: errArgs,
}}
for _, test := range tests {
res, err := parseWithdrawArgs(test.params)
if err != nil {
if !errors.Is(err, test.wantErr) {
t.Fatalf("unexpected error %v for test %s",
err, test.name)
}
continue
}
if !bytes.Equal(res.AppPass, test.params.PWArgs[0]) {
t.Fatalf("appPass doesn't match")
}
if fmt.Sprint(res.AssetID) != test.params.Args[0] {
t.Fatalf("assetID doesn't match")
}
if fmt.Sprint(res.Value) != test.params.Args[1] {
t.Fatalf("value doesn't match")
}
if res.Address != test.params.Args[2] {
t.Fatalf("address doesn't match")
}
}
}
57 changes: 29 additions & 28 deletions dex/msgjson/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,34 +32,35 @@ const (
RPCArgumentsError // 16
RPCTradeError // 17
RPCCancelError // 18
SignatureError // 19
SerializationError // 20
TransactionUndiscovered // 21
ContractError // 22
SettlementSequenceError // 23
ResultLengthError // 24
IDMismatchError // 25
RedemptionError // 26
IDTypeError // 27
AckCountError // 28
UnknownResponseID // 29
OrderParameterError // 30
UnknownMarketError // 31
ClockRangeError // 32
FundingError // 33
CoinAuthError // 34
UnknownMarket // 35
NotSubscribedError // 36
UnauthorizedConnection // 37
AuthenticationError // 38
PubKeyParseError // 39
FeeError // 40
InvalidPreimage // 41
PreimageCommitmentMismatch // 42
UnknownMessageType // 43
AccountClosedError // 44
MarketNotRunningError // 45
TryAgainLaterError // 46
RPCWithdrawError // 19
SignatureError // 20
SerializationError // 21
TransactionUndiscovered // 22
ContractError // 23
SettlementSequenceError // 24
ResultLengthError // 25
IDMismatchError // 26
RedemptionError // 27
IDTypeError // 28
AckCountError // 29
UnknownResponseID // 30
OrderParameterError // 31
UnknownMarketError // 32
ClockRangeError // 33
FundingError // 34
CoinAuthError // 35
UnknownMarket // 36
NotSubscribedError // 37
UnauthorizedConnection // 38
AuthenticationError // 39
PubKeyParseError // 40
FeeError // 41
InvalidPreimage // 42
PreimageCommitmentMismatch // 43
UnknownMessageType // 44
AccountClosedError // 45
MarketNotRunningError // 46
TryAgainLaterError // 47
)

// Routes are destinations for a "payload" of data. The type of data being
Expand Down

0 comments on commit eac1b9c

Please sign in to comment.