Skip to content

Commit

Permalink
dcr: Allow ticket purchasing for rpc spv wallets
Browse files Browse the repository at this point in the history
  • Loading branch information
JoeGruffins committed May 24, 2024
1 parent e8dc621 commit 7c7bfc6
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 31 deletions.
30 changes: 29 additions & 1 deletion client/asset/dcr/dcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions client/asset/dcr/dcr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
67 changes: 53 additions & 14 deletions client/asset/dcr/rpcwallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -53,6 +57,7 @@ const (
methodSignRawTransaction = "signrawtransaction"
methodSyncStatus = "syncstatus"
methodGetPeerInfo = "getpeerinfo"
methodWalletInfo = "walletinfo"
)

// rpcWallet implements Wallet functionality using an rpc client to communicate
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -446,6 +457,7 @@ func (w *rpcWallet) Connect(ctx context.Context) error {
}

w.spvMode = spv
w.hasSPVTicketFunctions = hasSPVTicketFunctions

return nil
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1090,7 +1110,6 @@ func (w *rpcWallet) Tickets(ctx context.Context) ([]*asset.Ticket, error) {
// Spender: ?,
})
}

return tickets, nil
}

Expand Down Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion dex/testing/dcr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions dex/testing/dcr/create-wallet.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -75,6 +76,13 @@ ticketbuyer.balancetomaintainabsolute=1000
EOF
fi

if [ "${VSP_PUBKEY}" != "_" ]; then
cat >> "${WALLET_DIR}/${NAME}.conf" <<EOF
vsp.url=http://127.0.0.1:19591
vsp.pubkey=${VSP_PUBKEY}
EOF
fi

if [ "${HTTPPROF_PORT}" != "_" ]; then
echo "profile=127.0.0.1:${HTTPPROF_PORT}" >> "${WALLET_DIR}/${NAME}.conf"
fi
Expand Down
34 changes: 19 additions & 15 deletions dex/testing/dcr/harness.sh
Original file line number Diff line number Diff line change
Expand Up @@ -302,50 +302,54 @@ 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
# from the archive, so we must mine a couple blocks first, but only now after the
# 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"
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

Expand Down

0 comments on commit 7c7bfc6

Please sign in to comment.