From 7c7bfc69535b6024127cde312f23eca3188a248f Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Thu, 2 May 2024 16:02:41 +0900 Subject: [PATCH] dcr: Allow ticket purchasing for rpc spv wallets --- client/asset/dcr/dcr.go | 30 +++++++++++++- client/asset/dcr/dcr_test.go | 3 ++ client/asset/dcr/rpcwallet.go | 67 +++++++++++++++++++++++++------- dex/testing/dcr/README.md | 2 +- dex/testing/dcr/create-wallet.sh | 8 ++++ dex/testing/dcr/harness.sh | 34 +++++++++------- 6 files changed, 113 insertions(+), 31 deletions(-) diff --git a/client/asset/dcr/dcr.go b/client/asset/dcr/dcr.go index 53e819be5f..3b438955f5 100644 --- a/client/asset/dcr/dcr.go +++ b/client/asset/dcr/dcr.go @@ -5306,9 +5306,13 @@ func (dcr *ExchangeWallet) StakeStatus() (*asset.TicketStakingStatus, error) { if !dcr.connected.Load() { return nil, errors.New("not connected, login first") } - // Try to get tickets first, because this will error for RPC + SPV wallets. + // Try to get tickets first, because this will error for older RPC + SPV + // wallets. tickets, err := dcr.tickets(dcr.ctx) if err != nil { + if errors.Is(err, oldSPVWalletErr) { + return nil, nil + } return nil, fmt.Errorf("error retrieving tickets: %w", err) } sinfo, err := dcr.wallet.StakeInfo(dcr.ctx) @@ -5322,6 +5326,16 @@ func (dcr *ExchangeWallet) StakeStatus() (*asset.TicketStakingStatus, error) { if v := dcr.vspV.Load(); v != nil { vspURL = v.(*vsp).URL } + } else { + rpcW, ok := dcr.wallet.(*rpcWallet) + if !ok { + return nil, errors.New("wallet not an *rpcWallet") + } + walletInfo, err := rpcW.walletInfo(dcr.ctx) + if err != nil { + return nil, fmt.Errorf("error retrieving wallet info: %w", err) + } + vspURL = walletInfo.VSP } voteChoices, tSpends, treasuryPolicy, err := dcr.wallet.VotingPreferences(dcr.ctx) if err != nil { @@ -5470,6 +5484,20 @@ func (dcr *ExchangeWallet) PurchaseTickets(n int, feeSuggestion uint64) error { if err != nil { return fmt.Errorf("error getting balance: %v", err) } + isRPC := !dcr.isNative() + if isRPC { + rpcW, ok := dcr.wallet.(*rpcWallet) + if !ok { + return errors.New("wallet not an *rpcWallet") + } + walletInfo, err := rpcW.walletInfo(dcr.ctx) + if err != nil { + return fmt.Errorf("error retrieving wallet info: %w", err) + } + if walletInfo.SPV && walletInfo.VSP == "" { + return errors.New("a vsp must best set to purchase tickets with an spv wallet") + } + } sinfo, err := dcr.wallet.StakeInfo(dcr.ctx) if err != nil { return fmt.Errorf("stakeinfo error: %v", err) diff --git a/client/asset/dcr/dcr_test.go b/client/asset/dcr/dcr_test.go index d7edcb8809..ab1a5411df 100644 --- a/client/asset/dcr/dcr_test.go +++ b/client/asset/dcr/dcr_test.go @@ -705,6 +705,9 @@ func (c *tRPCClient) RawRequest(_ context.Context, method string, params []json. Complete: complete, } return json.Marshal(&res) + + case methodWalletInfo: + return json.Marshal(new(walletjson.WalletInfoResult)) } return nil, fmt.Errorf("method %v not implemented by (*tRPCClient).RawRequest", method) diff --git a/client/asset/dcr/rpcwallet.go b/client/asset/dcr/rpcwallet.go index e77b6c14a8..308b0c3437 100644 --- a/client/asset/dcr/rpcwallet.go +++ b/client/asset/dcr/rpcwallet.go @@ -43,6 +43,10 @@ var ( {Major: 8, Minor: 0, Patch: 0}, // 1.8-pre, just dropped unused ticket RPCs {Major: 7, Minor: 0, Patch: 0}, // 1.7 release, new gettxout args } + // From vspWithSPVWalletRPCVersion and later the wallet's current "vsp" + // is included in the walletinfo response and the wallet will no longer + // error on GetTickets with an spv wallet. + vspWithSPVWalletRPCVersion = dex.Semver{Major: 9, Minor: 2, Patch: 0} ) // RawRequest RPC methods @@ -53,6 +57,7 @@ const ( methodSignRawTransaction = "signrawtransaction" methodSyncStatus = "syncstatus" methodGetPeerInfo = "getpeerinfo" + methodWalletInfo = "walletinfo" ) // rpcWallet implements Wallet functionality using an rpc client to communicate @@ -63,6 +68,8 @@ type rpcWallet struct { rpcCfg *rpcclient.ConnConfig accountsV atomic.Value // XCWalletAccounts + hasSPVTicketFunctions bool + rpcMtx sync.RWMutex spvMode bool // rpcConnector is a rpcclient.Client, does not need to be @@ -338,56 +345,60 @@ func (w *rpcWallet) handleRPCClientReconnection(ctx context.Context) { w.log.Debugf("dcrwallet reconnected (%d)", connectCount-1) w.rpcMtx.RLock() defer w.rpcMtx.RUnlock() - spv, err := checkRPCConnection(ctx, w.rpcConnector, w.rpcClient, w.log) + spv, hasSPVTicketFunctions, err := checkRPCConnection(ctx, w.rpcConnector, w.rpcClient, w.log) if err != nil { w.log.Errorf("dcrwallet reconnect handler error: %v", err) } w.spvMode = spv + w.hasSPVTicketFunctions = hasSPVTicketFunctions } // checkRPCConnection verifies the dcrwallet connection with the walletinfo RPC // and sets the spvMode flag accordingly. The spvMode flag is only set after a // successful check. This method is not safe for concurrent access, and the // rpcMtx must be at least read locked. -func checkRPCConnection(ctx context.Context, connector rpcConnector, client rpcClient, log dex.Logger) (bool, error) { +func checkRPCConnection(ctx context.Context, connector rpcConnector, client rpcClient, log dex.Logger) (bool, bool, error) { // Check the required API versions. versions, err := connector.Version(ctx) if err != nil { - return false, fmt.Errorf("dcrwallet version fetch error: %w", err) + return false, false, fmt.Errorf("dcrwallet version fetch error: %w", err) } ver, exists := versions["dcrwalletjsonrpcapi"] if !exists { - return false, fmt.Errorf("dcrwallet.Version response missing 'dcrwalletjsonrpcapi'") + return false, false, fmt.Errorf("dcrwallet.Version response missing 'dcrwalletjsonrpcapi'") } walletSemver := dex.NewSemver(ver.Major, ver.Minor, ver.Patch) if !dex.SemverCompatibleAny(compatibleWalletRPCVersions, walletSemver) { - return false, fmt.Errorf("advertised dcrwallet JSON-RPC version %v incompatible with %v", + return false, false, fmt.Errorf("advertised dcrwallet JSON-RPC version %v incompatible with %v", walletSemver, compatibleWalletRPCVersions) } + hasSPVTicketFunctions := walletSemver.Major >= vspWithSPVWalletRPCVersion.Major && + walletSemver.Minor >= vspWithSPVWalletRPCVersion.Minor + ver, exists = versions["dcrdjsonrpcapi"] if exists { nodeSemver := dex.NewSemver(ver.Major, ver.Minor, ver.Patch) if !dex.SemverCompatibleAny(compatibleNodeRPCVersions, nodeSemver) { - return false, fmt.Errorf("advertised dcrd JSON-RPC version %v incompatible with %v", + return false, false, fmt.Errorf("advertised dcrd JSON-RPC version %v incompatible with %v", nodeSemver, compatibleNodeRPCVersions) } log.Infof("Connected to dcrwallet (JSON-RPC API v%s) proxying dcrd (JSON-RPC API v%s)", walletSemver, nodeSemver) - return false, nil + return false, false, nil } // SPV maybe? walletInfo, err := client.WalletInfo(ctx) if err != nil { - return false, fmt.Errorf("walletinfo rpc error: %w", translateRPCCancelErr(err)) + return false, false, fmt.Errorf("walletinfo rpc error: %w", translateRPCCancelErr(err)) } if !walletInfo.SPV { - return false, fmt.Errorf("dcrwallet.Version response missing 'dcrdjsonrpcapi' for non-spv wallet") + return false, false, fmt.Errorf("dcrwallet.Version response missing 'dcrdjsonrpcapi' for non-spv wallet") } log.Infof("Connected to dcrwallet (JSON-RPC API v%s) in SPV mode", walletSemver) - return true, nil + return true, hasSPVTicketFunctions, nil } // Connect establishes a connection to the previously created rpc client. The @@ -433,7 +444,7 @@ func (w *rpcWallet) Connect(ctx context.Context) error { // fails and we return with a non-nil error, we must shutdown the // rpc client otherwise subsequent reconnect attempts will be met // with "websocket client has already connected". - spv, err := checkRPCConnection(ctx, w.rpcConnector, w.rpcClient, w.log) + spv, hasSPVTicketFunctions, err := checkRPCConnection(ctx, w.rpcConnector, w.rpcClient, w.log) if err != nil { // The client should still be connected, but if not, do not try to // shutdown and wait as it could hang. @@ -446,6 +457,7 @@ func (w *rpcWallet) Connect(ctx context.Context) error { } w.spvMode = spv + w.hasSPVTicketFunctions = hasSPVTicketFunctions return nil } @@ -1030,10 +1042,18 @@ func (w *rpcWallet) PurchaseTickets(ctx context.Context, n int, _, _ string, _ * return tickets, nil } +var oldSPVWalletErr = errors.New("wallet is an older spv wallet") + // Tickets returns active tickets. func (w *rpcWallet) Tickets(ctx context.Context) ([]*asset.Ticket, error) { - const includeImmature = true - // GetTickets only works for clients with a dcrd backend. + return w.tickets(ctx, true) +} + +func (w *rpcWallet) tickets(ctx context.Context, includeImmature bool) ([]*asset.Ticket, error) { + // GetTickets only works for spv clients after version 9.2.0 + if w.spvMode && !w.hasSPVTicketFunctions { + return nil, oldSPVWalletErr + } hashes, err := w.rpcClient.GetTickets(ctx, includeImmature) if err != nil { return nil, err @@ -1090,7 +1110,6 @@ func (w *rpcWallet) Tickets(ctx context.Context) ([]*asset.Ticket, error) { // Spender: ?, }) } - return tickets, nil } @@ -1216,3 +1235,23 @@ func isAccountLockedErr(err error) bool { return errors.As(err, &rpcErr) && rpcErr.Code == dcrjson.ErrRPCWalletUnlockNeeded && strings.Contains(rpcErr.Message, "account is already locked") } + +// newWalletInfo is walletinfo with a new field found in version 9.2.0+. +// +// TODO: Just use *walletjson.WalletInfoResult after we update to dcrwallet/v4. +type newWalletInfo struct { + *walletjson.WalletInfoResult + VSP string `json:"vsp"` +} + +func (w *rpcWallet) walletInfo(ctx context.Context) (*newWalletInfo, error) { + var walletInfo newWalletInfo + err := w.rpcClientRawRequest(ctx, methodWalletInfo, nil, &walletInfo) + return &walletInfo, translateRPCCancelErr(err) +} + +var _ ticketPager = (*rpcWallet)(nil) + +func (w *rpcWallet) TicketPage(ctx context.Context, scanStart int32, n, skipN int) ([]*asset.Ticket, error) { + return make([]*asset.Ticket, 0), nil +} diff --git a/dex/testing/dcr/README.md b/dex/testing/dcr/README.md index f32114bf29..5996577541 100644 --- a/dex/testing/dcr/README.md +++ b/dex/testing/dcr/README.md @@ -5,7 +5,7 @@ sandboxed environment for testing dex swap transactions. ## Dependencies -The harness depends on [dcrd](https://github.com/decred/dcrd), [dcrwallet](https://github.com/decred/dcrwallet) and [dcrctl](https://github.com/decred/dcrd/tree/master/cmd/dcrctl) to run. +The harness depends on [dcrd](https://github.com/decred/dcrd), [dcrwallet](https://github.com/decred/dcrwallet) and [dcrctl](https://github.com/decred/dcrd/tree/master/cmd/dcrctl) to run. Also requires curl and jq. ## Using diff --git a/dex/testing/dcr/create-wallet.sh b/dex/testing/dcr/create-wallet.sh index ae6d269ddf..8cf482c904 100755 --- a/dex/testing/dcr/create-wallet.sh +++ b/dex/testing/dcr/create-wallet.sh @@ -11,6 +11,7 @@ USE_SPV=$5 ENABLE_VOTING=$6 HTTPPROF_PORT=$7 MANUAL_TICKETS=$8 +VSP_PUBKEY=$9 WALLET_DIR="${NODES_ROOT}/${NAME}" mkdir -p ${WALLET_DIR} @@ -75,6 +76,13 @@ ticketbuyer.balancetomaintainabsolute=1000 EOF fi +if [ "${VSP_PUBKEY}" != "_" ]; then +cat >> "${WALLET_DIR}/${NAME}.conf" <> "${WALLET_DIR}/${NAME}.conf" fi diff --git a/dex/testing/dcr/harness.sh b/dex/testing/dcr/harness.sh index 15e92188a0..15ddd9f4fb 100755 --- a/dex/testing/dcr/harness.sh +++ b/dex/testing/dcr/harness.sh @@ -302,7 +302,7 @@ ENABLE_VOTING="2" # 2 = enable voting and ticket buyer MANUAL_TICKETS="0" "${HARNESS_DIR}/create-wallet.sh" "$SESSION:3" "alpha" ${ALPHA_WALLET_SEED} \ ${ALPHA_WALLET_RPC_PORT} ${USE_SPV} ${ENABLE_VOTING} ${ALPHA_WALLET_HTTPPROF_PORT} \ -${MANUAL_TICKETS} +${MANUAL_TICKETS} "_" # alpha uses walletpassphrase/walletlock. # SPV wallets will declare peers stalled and disconnect with only ancient blocks @@ -310,13 +310,28 @@ ${MANUAL_TICKETS} # voting wallet (alpha) is running. tmux send-keys -t $SESSION:0 "./mine-alpha 2${WAIT}" C-m\; wait-for donedcr +echo "Creating simnet vspd wallet" +USE_SPV="0" +ENABLE_VOTING="1" +MANUAL_TICKETS="1" +"${HARNESS_DIR}/create-wallet.sh" "$SESSION:7" "vspdwallet" ${VSPD_WALLET_SEED} \ +${VSPD_WALLET_RPC_PORT} ${USE_SPV} ${ENABLE_VOTING} "_" ${MANUAL_TICKETS} "_" + +echo "Creating simnet vspd" +ALPHA_WALLET_PUBKEY=$("${NODES_ROOT}/harness-ctl/alpha" "getmasterpubkey") +"${HARNESS_DIR}/create-vspd.sh" "$SESSION:8" "${VSPD_PORT}" "${ALPHA_WALLET_PUBKEY}" + +sleep 3 + +VSP_PUBKEY=$(curl -sS "http://127.0.0.1:19591/api/v3/vspinfo" | jq -r '.pubkey') + echo "Creating simnet beta wallet" USE_SPV="1" ENABLE_VOTING="0" MANUAL_TICKETS="0" "${HARNESS_DIR}/create-wallet.sh" "$SESSION:4" "beta" ${BETA_WALLET_SEED} \ ${BETA_WALLET_RPC_PORT} ${USE_SPV} ${ENABLE_VOTING} ${BETA_WALLET_HTTPPROF_PORT} \ -${MANUAL_TICKETS} +${MANUAL_TICKETS} ${VSP_PUBKEY} # The trading wallets need to be created from scratch every time. echo "Creating simnet trading wallet 1" @@ -324,28 +339,17 @@ USE_SPV="1" ENABLE_VOTING="0" MANUAL_TICKETS="0" "${HARNESS_DIR}/create-wallet.sh" "$SESSION:5" "trading1" ${TRADING_WALLET1_SEED} \ -${TRADING_WALLET1_PORT} ${USE_SPV} ${ENABLE_VOTING} "_" ${MANUAL_TICKETS} +${TRADING_WALLET1_PORT} ${USE_SPV} ${ENABLE_VOTING} "_" ${MANUAL_TICKETS} "_" echo "Creating simnet trading wallet 2" USE_SPV="1" ENABLE_VOTING="0" MANUAL_TICKETS="0" "${HARNESS_DIR}/create-wallet.sh" "$SESSION:6" "trading2" ${TRADING_WALLET2_SEED} \ -${TRADING_WALLET2_PORT} ${USE_SPV} ${ENABLE_VOTING} "_" ${MANUAL_TICKETS} - -echo "Creating simnet vspd wallet" -USE_SPV="0" -ENABLE_VOTING="1" -MANUAL_TICKETS="1" -"${HARNESS_DIR}/create-wallet.sh" "$SESSION:7" "vspdwallet" ${VSPD_WALLET_SEED} \ -${VSPD_WALLET_RPC_PORT} ${USE_SPV} ${ENABLE_VOTING} "_" ${MANUAL_TICKETS} +${TRADING_WALLET2_PORT} ${USE_SPV} ${ENABLE_VOTING} "_" ${MANUAL_TICKETS} "_" sleep 3 -echo "Creating simnet vspd" -ALPHA_WALLET_PUBKEY=$("${NODES_ROOT}/harness-ctl/alpha" "getmasterpubkey") -"${HARNESS_DIR}/create-vspd.sh" "$SESSION:8" "${VSPD_PORT}" "${ALPHA_WALLET_PUBKEY}" - # Give beta's "default" account a password, so it uses unlockaccount/lockaccount. tmux send-keys -t $SESSION:0 "./beta setaccountpassphrase default ${WALLET_PASS}${WAIT}" C-m\; wait-for donedcr