decred.org/dcrdex@v1.0.3/client/asset/dcr/rpcwallet.go (about) 1 // This code is available on the terms of the project LICENSE.md file, 2 // also available online at https://blueoakcouncil.org/license/1.0.0. 3 4 package dcr 5 6 import ( 7 "bytes" 8 "context" 9 "encoding/hex" 10 "encoding/json" 11 "errors" 12 "fmt" 13 "math" 14 "os" 15 "strings" 16 "sync" 17 "sync/atomic" 18 "time" 19 20 "decred.org/dcrdex/client/asset" 21 "decred.org/dcrdex/dex" 22 "decred.org/dcrwallet/v4/rpc/client/dcrwallet" 23 walletjson "decred.org/dcrwallet/v4/rpc/jsonrpc/types" 24 "decred.org/dcrwallet/v4/wallet" 25 "github.com/decred/dcrd/chaincfg/chainhash" 26 "github.com/decred/dcrd/chaincfg/v3" 27 "github.com/decred/dcrd/dcrec/secp256k1/v4" 28 "github.com/decred/dcrd/dcrjson/v4" 29 "github.com/decred/dcrd/dcrutil/v4" 30 "github.com/decred/dcrd/gcs/v4" 31 "github.com/decred/dcrd/gcs/v4/blockcf2" 32 chainjson "github.com/decred/dcrd/rpc/jsonrpc/types/v4" 33 "github.com/decred/dcrd/rpcclient/v8" 34 "github.com/decred/dcrd/txscript/v4/stdaddr" 35 "github.com/decred/dcrd/wire" 36 ) 37 38 var ( 39 compatibleWalletRPCVersions = []dex.Semver{ 40 {Major: 9, Minor: 0, Patch: 0}, // 1.8-pre 41 {Major: 8, Minor: 8, Patch: 0}, // 1.7 release, min for getcurrentnet 42 } 43 compatibleNodeRPCVersions = []dex.Semver{ 44 {Major: 8, Minor: 0, Patch: 0}, // 1.8-pre, just dropped unused ticket RPCs 45 {Major: 7, Minor: 0, Patch: 0}, // 1.7 release, new gettxout args 46 } 47 // From vspWithSPVWalletRPCVersion and later the wallet's current "vsp" 48 // is included in the walletinfo response and the wallet will no longer 49 // error on GetTickets with an spv wallet. 50 vspWithSPVWalletRPCVersion = dex.Semver{Major: 9, Minor: 2, Patch: 0} 51 ) 52 53 // RawRequest RPC methods 54 const ( 55 methodGetCFilterV2 = "getcfilterv2" 56 methodListUnspent = "listunspent" 57 methodListLockUnspent = "listlockunspent" 58 methodSignRawTransaction = "signrawtransaction" 59 methodSyncStatus = "syncstatus" 60 methodGetPeerInfo = "getpeerinfo" 61 methodWalletInfo = "walletinfo" 62 ) 63 64 // rpcWallet implements Wallet functionality using an rpc client to communicate 65 // with the json-rpc server of an external dcrwallet daemon. 66 type rpcWallet struct { 67 chainParams *chaincfg.Params 68 log dex.Logger 69 rpcCfg *rpcclient.ConnConfig 70 accountsV atomic.Value // XCWalletAccounts 71 72 hasSPVTicketFunctions bool 73 74 rpcMtx sync.RWMutex 75 spvMode bool 76 // rpcConnector is a rpcclient.Client, does not need to be 77 // set for testing. 78 rpcConnector rpcConnector 79 // rpcClient is a combined rpcclient.Client+dcrwallet.Client, 80 // or a stub for testing. 81 rpcClient rpcClient 82 83 connectCount uint32 // atomic 84 } 85 86 // Ensure rpcWallet satisfies the Wallet interface. 87 var _ Wallet = (*rpcWallet)(nil) 88 var _ Mempooler = (*rpcWallet)(nil) 89 var _ FeeRateEstimator = (*rpcWallet)(nil) 90 91 type walletClient = dcrwallet.Client 92 93 type combinedClient struct { 94 *rpcclient.Client 95 *walletClient 96 } 97 98 func newCombinedClient(nodeRPCClient *rpcclient.Client, chainParams *chaincfg.Params) *combinedClient { 99 return &combinedClient{ 100 nodeRPCClient, 101 dcrwallet.NewClient(dcrwallet.RawRequestCaller(nodeRPCClient), chainParams), 102 } 103 } 104 105 // Ensure combinedClient satisfies the rpcClient interface. 106 var _ rpcClient = (*combinedClient)(nil) 107 108 // ValidateAddress disambiguates the node and wallet methods of the same name. 109 func (cc *combinedClient) ValidateAddress(ctx context.Context, address stdaddr.Address) (*walletjson.ValidateAddressWalletResult, error) { 110 return cc.walletClient.ValidateAddress(ctx, address) 111 } 112 113 // rpcConnector defines methods required by *rpcWallet for connecting and 114 // disconnecting the rpcClient to/from the json-rpc server. 115 type rpcConnector interface { 116 Connect(ctx context.Context, retry bool) error 117 Version(ctx context.Context) (map[string]chainjson.VersionResult, error) 118 Disconnected() bool 119 Shutdown() 120 WaitForShutdown() 121 } 122 123 // rpcClient defines rpc request methods that are used by *rpcWallet. 124 // This is a *combinedClient or a stub for testing. 125 type rpcClient interface { 126 GetCurrentNet(ctx context.Context) (wire.CurrencyNet, error) 127 EstimateSmartFee(ctx context.Context, confirmations int64, mode chainjson.EstimateSmartFeeMode) (*chainjson.EstimateSmartFeeResult, error) 128 SendRawTransaction(ctx context.Context, tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error) 129 GetTxOut(ctx context.Context, txHash *chainhash.Hash, index uint32, tree int8, mempool bool) (*chainjson.GetTxOutResult, error) 130 GetBalanceMinConf(ctx context.Context, account string, minConfirms int) (*walletjson.GetBalanceResult, error) 131 GetBestBlock(ctx context.Context) (*chainhash.Hash, int64, error) 132 GetBlockHash(ctx context.Context, blockHeight int64) (*chainhash.Hash, error) 133 GetBlock(ctx context.Context, blockHash *chainhash.Hash) (*wire.MsgBlock, error) 134 GetBlockHeaderVerbose(ctx context.Context, blockHash *chainhash.Hash) (*chainjson.GetBlockHeaderVerboseResult, error) 135 GetBlockHeader(ctx context.Context, blockHash *chainhash.Hash) (*wire.BlockHeader, error) 136 GetRawMempool(ctx context.Context, txType chainjson.GetRawMempoolTxTypeCmd) ([]*chainhash.Hash, error) 137 LockUnspent(ctx context.Context, unlock bool, ops []*wire.OutPoint) error 138 GetRawChangeAddress(ctx context.Context, account string, net stdaddr.AddressParams) (stdaddr.Address, error) 139 GetNewAddressGapPolicy(ctx context.Context, account string, gap dcrwallet.GapPolicy) (stdaddr.Address, error) 140 DumpPrivKey(ctx context.Context, address stdaddr.Address) (*dcrutil.WIF, error) 141 GetTransaction(ctx context.Context, txHash *chainhash.Hash) (*walletjson.GetTransactionResult, error) // Should return asset.CoinNotFoundError if tx is not found. 142 WalletLock(ctx context.Context) error 143 WalletPassphrase(ctx context.Context, passphrase string, timeoutSecs int64) error 144 AccountUnlocked(ctx context.Context, account string) (*walletjson.AccountUnlockedResult, error) 145 LockAccount(ctx context.Context, account string) error 146 UnlockAccount(ctx context.Context, account, passphrase string) error 147 RawRequest(ctx context.Context, method string, params []json.RawMessage) (json.RawMessage, error) 148 WalletInfo(ctx context.Context) (*walletjson.WalletInfoResult, error) 149 ValidateAddress(ctx context.Context, address stdaddr.Address) (*walletjson.ValidateAddressWalletResult, error) 150 GetStakeInfo(ctx context.Context) (*walletjson.GetStakeInfoResult, error) 151 PurchaseTicket(ctx context.Context, fromAccount string, spendLimit dcrutil.Amount, minConf *int, 152 ticketAddress stdaddr.Address, numTickets *int, poolAddress stdaddr.Address, poolFees *dcrutil.Amount, 153 expiry *int, ticketChange *bool, ticketFee *dcrutil.Amount) ([]*chainhash.Hash, error) 154 GetTickets(ctx context.Context, includeImmature bool) ([]*chainhash.Hash, error) 155 GetVoteChoices(ctx context.Context) (*walletjson.GetVoteChoicesResult, error) 156 SetVoteChoice(ctx context.Context, agendaID, choiceID string) error 157 SetTxFee(ctx context.Context, fee dcrutil.Amount) error 158 ListSinceBlock(ctx context.Context, hash *chainhash.Hash) (*walletjson.ListSinceBlockResult, error) 159 } 160 161 // newRPCWallet creates an rpcClient and uses it to construct a new instance 162 // of the rpcWallet. The rpcClient isn't connected to the server yet, use the 163 // Connect method of the returned *rpcWallet to connect the rpcClient to the 164 // server. 165 func newRPCWallet(settings map[string]string, logger dex.Logger, net dex.Network) (*rpcWallet, error) { 166 cfg, chainParams, err := loadRPCConfig(settings, net) 167 if err != nil { 168 return nil, fmt.Errorf("error parsing config: %w", err) 169 } 170 171 // Check rpc connection config values 172 missing := "" 173 if cfg.RPCUser == "" { 174 missing += " username" 175 } 176 if cfg.RPCPass == "" { 177 missing += " password" 178 } 179 if missing != "" { 180 return nil, fmt.Errorf("missing dcrwallet rpc credentials:%s", missing) 181 } 182 183 log := logger.SubLogger("RPC") 184 rpcw := &rpcWallet{ 185 chainParams: chainParams, 186 log: log, 187 } 188 189 certs, err := os.ReadFile(cfg.RPCCert) 190 if err != nil { 191 return nil, fmt.Errorf("TLS certificate read error: %w", err) 192 } 193 194 log.Infof("Setting up rpc client to communicate with dcrwallet at %s with TLS certificate %q.", 195 cfg.RPCListen, cfg.RPCCert) 196 rpcw.rpcCfg = &rpcclient.ConnConfig{ 197 Host: cfg.RPCListen, 198 Endpoint: "ws", 199 User: cfg.RPCUser, 200 Pass: cfg.RPCPass, 201 Certificates: certs, 202 DisableConnectOnNew: true, // don't start until Connect 203 } 204 // Validate the RPC client config, and create a placeholder (non-nil) RPC 205 // connector and client that will be replaced on Connect. Any method calls 206 // prior to Connect will be met with rpcclient.ErrClientNotConnected rather 207 // than a panic. 208 nodeRPCClient, err := rpcclient.New(rpcw.rpcCfg, nil) 209 if err != nil { 210 return nil, fmt.Errorf("error setting up rpc client: %w", err) 211 } 212 rpcw.rpcConnector = nodeRPCClient 213 rpcw.rpcClient = newCombinedClient(nodeRPCClient, chainParams) 214 215 rpcw.accountsV.Store(XCWalletAccounts{ 216 PrimaryAccount: cfg.PrimaryAccount, 217 UnmixedAccount: cfg.UnmixedAccount, 218 TradingAccount: cfg.TradingAccount, 219 }) 220 221 return rpcw, nil 222 } 223 224 // Accounts returns the names of the accounts for use by the exchange wallet. 225 func (w *rpcWallet) Accounts() XCWalletAccounts { 226 return w.accountsV.Load().(XCWalletAccounts) 227 } 228 229 // Reconfigure updates the wallet to user a new configuration. 230 func (w *rpcWallet) Reconfigure(ctx context.Context, cfg *asset.WalletConfig, net dex.Network, currentAddress string) (restart bool, err error) { 231 if !(cfg.Type == walletTypeDcrwRPC || cfg.Type == walletTypeLegacy) { 232 return true, nil 233 } 234 235 rpcCfg, chainParams, err := loadRPCConfig(cfg.Settings, net) 236 if err != nil { 237 return false, fmt.Errorf("error parsing config: %w", err) 238 } 239 240 walletCfg := new(walletConfig) 241 _, err = loadConfig(cfg.Settings, net, walletCfg) 242 if err != nil { 243 return false, err 244 } 245 246 if chainParams.Net != w.chainParams.Net { 247 return false, errors.New("cannot reconfigure to use different network") 248 } 249 250 certs, err := os.ReadFile(rpcCfg.RPCCert) 251 if err != nil { 252 return false, fmt.Errorf("TLS certificate read error: %w", err) 253 } 254 255 var allOk bool 256 defer func() { 257 if allOk { // update the account names as the last step 258 w.accountsV.Store(XCWalletAccounts{ 259 PrimaryAccount: rpcCfg.PrimaryAccount, 260 UnmixedAccount: rpcCfg.UnmixedAccount, 261 TradingAccount: rpcCfg.TradingAccount, 262 }) 263 } 264 }() 265 266 currentAccts := w.accountsV.Load().(XCWalletAccounts) 267 268 if rpcCfg.RPCUser == w.rpcCfg.User && 269 rpcCfg.RPCPass == w.rpcCfg.Pass && 270 bytes.Equal(certs, w.rpcCfg.Certificates) && 271 rpcCfg.RPCListen == w.rpcCfg.Host && 272 rpcCfg.PrimaryAccount == currentAccts.PrimaryAccount && 273 rpcCfg.UnmixedAccount == currentAccts.UnmixedAccount && 274 rpcCfg.TradingAccount == currentAccts.TradingAccount { 275 allOk = true 276 return false, nil 277 } 278 279 newWallet, err := newRPCWallet(cfg.Settings, w.log, net) 280 if err != nil { 281 return false, err 282 } 283 284 err = newWallet.Connect(ctx) 285 if err != nil { 286 return false, fmt.Errorf("error connecting new wallet") 287 } 288 289 defer func() { 290 if !allOk { 291 newWallet.Disconnect() 292 } 293 }() 294 295 for _, acctName := range []string{rpcCfg.PrimaryAccount, rpcCfg.TradingAccount, rpcCfg.UnmixedAccount} { 296 if acctName == "" { 297 continue 298 } 299 if _, err := newWallet.AccountUnlocked(ctx, acctName); err != nil { 300 return false, fmt.Errorf("error checking lock status on account %q: %v", acctName, err) 301 } 302 } 303 304 a, err := stdaddr.DecodeAddress(currentAddress, w.chainParams) 305 if err != nil { 306 return false, err 307 } 308 var depositAccount string 309 if rpcCfg.UnmixedAccount != "" { 310 depositAccount = rpcCfg.UnmixedAccount 311 } else { 312 depositAccount = rpcCfg.PrimaryAccount 313 } 314 owns, err := newWallet.AccountOwnsAddress(ctx, a, depositAccount) 315 if err != nil { 316 return false, err 317 } 318 if !owns { 319 if walletCfg.ActivelyUsed { 320 return false, errors.New("cannot reconfigure to different wallet while there are active trades") 321 } 322 return true, nil 323 } 324 325 w.rpcMtx.Lock() 326 defer w.rpcMtx.Unlock() 327 w.chainParams = newWallet.chainParams 328 w.rpcCfg = newWallet.rpcCfg 329 w.spvMode = newWallet.spvMode 330 w.rpcConnector = newWallet.rpcConnector 331 w.rpcClient = newWallet.rpcClient 332 333 allOk = true 334 return false, nil 335 } 336 337 func (w *rpcWallet) handleRPCClientReconnection(ctx context.Context) { 338 connectCount := atomic.AddUint32(&w.connectCount, 1) 339 if connectCount == 1 { 340 // first connection, below check will be performed 341 // by *rpcWallet.Connect. 342 return 343 } 344 345 w.log.Debugf("dcrwallet reconnected (%d)", connectCount-1) 346 w.rpcMtx.RLock() 347 defer w.rpcMtx.RUnlock() 348 spv, hasSPVTicketFunctions, err := checkRPCConnection(ctx, w.rpcConnector, w.rpcClient, w.log) 349 if err != nil { 350 w.log.Errorf("dcrwallet reconnect handler error: %v", err) 351 } 352 w.spvMode = spv 353 w.hasSPVTicketFunctions = hasSPVTicketFunctions 354 } 355 356 // checkRPCConnection verifies the dcrwallet connection with the walletinfo RPC 357 // and sets the spvMode flag accordingly. The spvMode flag is only set after a 358 // successful check. This method is not safe for concurrent access, and the 359 // rpcMtx must be at least read locked. 360 func checkRPCConnection(ctx context.Context, connector rpcConnector, client rpcClient, log dex.Logger) (bool, bool, error) { 361 // Check the required API versions. 362 versions, err := connector.Version(ctx) 363 if err != nil { 364 return false, false, fmt.Errorf("dcrwallet version fetch error: %w", err) 365 } 366 367 ver, exists := versions["dcrwalletjsonrpcapi"] 368 if !exists { 369 return false, false, fmt.Errorf("dcrwallet.Version response missing 'dcrwalletjsonrpcapi'") 370 } 371 walletSemver := dex.NewSemver(ver.Major, ver.Minor, ver.Patch) 372 if !dex.SemverCompatibleAny(compatibleWalletRPCVersions, walletSemver) { 373 return false, false, fmt.Errorf("advertised dcrwallet JSON-RPC version %v incompatible with %v", 374 walletSemver, compatibleWalletRPCVersions) 375 } 376 377 hasSPVTicketFunctions := walletSemver.Major >= vspWithSPVWalletRPCVersion.Major && 378 walletSemver.Minor >= vspWithSPVWalletRPCVersion.Minor 379 380 ver, exists = versions["dcrdjsonrpcapi"] 381 if exists { 382 nodeSemver := dex.NewSemver(ver.Major, ver.Minor, ver.Patch) 383 if !dex.SemverCompatibleAny(compatibleNodeRPCVersions, nodeSemver) { 384 return false, false, fmt.Errorf("advertised dcrd JSON-RPC version %v incompatible with %v", 385 nodeSemver, compatibleNodeRPCVersions) 386 } 387 log.Infof("Connected to dcrwallet (JSON-RPC API v%s) proxying dcrd (JSON-RPC API v%s)", 388 walletSemver, nodeSemver) 389 return false, false, nil 390 } 391 392 // SPV maybe? 393 walletInfo, err := client.WalletInfo(ctx) 394 if err != nil { 395 return false, false, fmt.Errorf("walletinfo rpc error: %w", translateRPCCancelErr(err)) 396 } 397 if !walletInfo.SPV { 398 return false, false, fmt.Errorf("dcrwallet.Version response missing 'dcrdjsonrpcapi' for non-spv wallet") 399 } 400 log.Infof("Connected to dcrwallet (JSON-RPC API v%s) in SPV mode", walletSemver) 401 return true, hasSPVTicketFunctions, nil 402 } 403 404 // Connect establishes a connection to the previously created rpc client. The 405 // wallet must not already be connected. 406 func (w *rpcWallet) Connect(ctx context.Context) error { 407 w.rpcMtx.Lock() 408 defer w.rpcMtx.Unlock() 409 410 // NOTE: rpcclient.(*Client).Disconnected() returns false prior to connect, 411 // so we cannot block incorrect Connect calls on that basis. However, it is 412 // always safe to call Shutdown, so do it just in case. 413 w.rpcConnector.Shutdown() 414 415 // Prepare a fresh RPC client. 416 ntfnHandlers := &rpcclient.NotificationHandlers{ 417 // Setup an on-connect handler for logging (re)connects. 418 OnClientConnected: func() { w.handleRPCClientReconnection(ctx) }, 419 } 420 nodeRPCClient, err := rpcclient.New(w.rpcCfg, ntfnHandlers) 421 if err != nil { // should never fail since we validated the config in newRPCWallet 422 return fmt.Errorf("failed to create dcrwallet RPC client: %w", err) 423 } 424 425 atomic.StoreUint32(&w.connectCount, 0) // handleRPCClientReconnection should skip checkRPCConnection on first Connect 426 427 w.rpcConnector = nodeRPCClient 428 w.rpcClient = newCombinedClient(nodeRPCClient, w.chainParams) 429 430 err = nodeRPCClient.Connect(ctx, false) // no retry 431 if err != nil { 432 return fmt.Errorf("dcrwallet connect error: %w", err) 433 } 434 435 net, err := w.rpcClient.GetCurrentNet(ctx) 436 if err != nil { 437 return translateRPCCancelErr(err) 438 } 439 if net != w.chainParams.Net { 440 return fmt.Errorf("unexpected wallet network %s, expected %s", net, w.chainParams.Net) 441 } 442 443 // The websocket client is connected now, so if the following check 444 // fails and we return with a non-nil error, we must shutdown the 445 // rpc client otherwise subsequent reconnect attempts will be met 446 // with "websocket client has already connected". 447 spv, hasSPVTicketFunctions, err := checkRPCConnection(ctx, w.rpcConnector, w.rpcClient, w.log) 448 if err != nil { 449 // The client should still be connected, but if not, do not try to 450 // shutdown and wait as it could hang. 451 if !errors.Is(err, rpcclient.ErrClientShutdown) { 452 // Using w.Disconnect would deadlock with rpcMtx already locked. 453 w.rpcConnector.Shutdown() 454 w.rpcConnector.WaitForShutdown() 455 } 456 return err 457 } 458 459 w.spvMode = spv 460 w.hasSPVTicketFunctions = hasSPVTicketFunctions 461 462 return nil 463 } 464 465 // Disconnect shuts down access to the wallet by disconnecting the rpc client. 466 // Part of the Wallet interface. 467 func (w *rpcWallet) Disconnect() { 468 w.rpcMtx.Lock() 469 defer w.rpcMtx.Unlock() 470 471 w.rpcConnector.Shutdown() // rpcclient.(*Client).Disconnect is a no-op with auto-reconnect 472 w.rpcConnector.WaitForShutdown() 473 474 // NOTE: After rpcclient shutdown, the rpcConnector and rpcClient are dead 475 // and cannot be started again. Connect must recreate them. 476 } 477 478 // client is a thread-safe accessor to the wallet's rpcClient. 479 func (w *rpcWallet) client() rpcClient { 480 w.rpcMtx.RLock() 481 defer w.rpcMtx.RUnlock() 482 return w.rpcClient 483 } 484 485 // Network returns the network of the connected wallet. 486 // Part of the Wallet interface. 487 func (w *rpcWallet) Network(ctx context.Context) (wire.CurrencyNet, error) { 488 net, err := w.client().GetCurrentNet(ctx) 489 return net, translateRPCCancelErr(err) 490 } 491 492 // SpvMode returns true if the wallet is connected to the Decred 493 // network via SPV peers. 494 // Part of the Wallet interface. 495 func (w *rpcWallet) SpvMode() bool { 496 w.rpcMtx.RLock() 497 defer w.rpcMtx.RUnlock() 498 return w.spvMode 499 } 500 501 // AddressInfo returns information for the provided address. It is an error 502 // if the address is not owned by the wallet. 503 // Part of the Wallet interface. 504 func (w *rpcWallet) AddressInfo(ctx context.Context, address string) (*AddressInfo, error) { 505 a, err := stdaddr.DecodeAddress(address, w.chainParams) 506 if err != nil { 507 return nil, err 508 } 509 res, err := w.client().ValidateAddress(ctx, a) 510 if err != nil { 511 return nil, translateRPCCancelErr(err) 512 } 513 if !res.IsValid { 514 return nil, fmt.Errorf("address is invalid") 515 } 516 if !res.IsMine { 517 return nil, fmt.Errorf("address does not belong to this wallet") 518 } 519 if res.Branch == nil { 520 return nil, fmt.Errorf("no account branch info for address") 521 } 522 return &AddressInfo{Account: res.Account, Branch: *res.Branch}, nil 523 } 524 525 // AccountOwnsAddress checks if the provided address belongs to the specified 526 // account. 527 // Part of the Wallet interface. 528 func (w *rpcWallet) AccountOwnsAddress(ctx context.Context, addr stdaddr.Address, acctName string) (bool, error) { 529 va, err := w.rpcClient.ValidateAddress(ctx, addr) 530 if err != nil { 531 return false, translateRPCCancelErr(err) 532 } 533 534 return va.IsMine && va.Account == acctName, nil 535 } 536 537 // WalletOwnsAddress returns whether any of the account controlled by this 538 // wallet owns the specified address. 539 func (w *rpcWallet) WalletOwnsAddress(ctx context.Context, addr stdaddr.Address) (bool, error) { 540 va, err := w.rpcClient.ValidateAddress(ctx, addr) 541 if err != nil { 542 return false, translateRPCCancelErr(err) 543 } 544 545 return va.IsMine, nil 546 } 547 548 // AccountBalance returns the balance breakdown for the specified account. 549 // Part of the Wallet interface. 550 func (w *rpcWallet) AccountBalance(ctx context.Context, confirms int32, acctName string) (*walletjson.GetAccountBalanceResult, error) { 551 balances, err := w.rpcClient.GetBalanceMinConf(ctx, acctName, int(confirms)) 552 if err != nil { 553 return nil, translateRPCCancelErr(err) 554 } 555 556 for i := range balances.Balances { 557 ab := &balances.Balances[i] 558 if ab.AccountName == acctName { 559 return ab, nil 560 } 561 } 562 563 return nil, fmt.Errorf("account not found: %q", acctName) 564 } 565 566 // LockedOutputs fetches locked outputs for the specified account using rpc 567 // RawRequest. 568 // Part of the Wallet interface. 569 func (w *rpcWallet) LockedOutputs(ctx context.Context, acctName string) ([]chainjson.TransactionInput, error) { 570 var locked []chainjson.TransactionInput 571 err := w.rpcClientRawRequest(ctx, methodListLockUnspent, anylist{acctName}, &locked) 572 return locked, translateRPCCancelErr(err) 573 } 574 575 // EstimateSmartFeeRate returns a smart feerate estimate using the 576 // estimatesmartfee rpc. 577 // Part of the Wallet interface. 578 func (w *rpcWallet) EstimateSmartFeeRate(ctx context.Context, confTarget int64, mode chainjson.EstimateSmartFeeMode) (float64, error) { 579 // estimatesmartfee 1 returns extremely high rates (e.g. 0.00817644). 580 if confTarget < 2 { 581 confTarget = 2 582 } 583 estimateFeeResult, err := w.client().EstimateSmartFee(ctx, confTarget, mode) 584 if err != nil { 585 return 0, translateRPCCancelErr(err) 586 } 587 return estimateFeeResult.FeeRate, nil 588 } 589 590 // Unspents fetches unspent outputs for the specified account using rpc 591 // RawRequest. 592 // Part of the Wallet interface. 593 func (w *rpcWallet) Unspents(ctx context.Context, acctName string) ([]*walletjson.ListUnspentResult, error) { 594 var unspents []*walletjson.ListUnspentResult 595 // minconf, maxconf (rpcdefault=9999999), [address], account 596 params := anylist{0, 9999999, nil, acctName} 597 err := w.rpcClientRawRequest(ctx, methodListUnspent, params, &unspents) 598 return unspents, err 599 } 600 601 // InternalAddress returns a change address from the specified account. 602 // Part of the Wallet interface. 603 func (w *rpcWallet) InternalAddress(ctx context.Context, acctName string) (stdaddr.Address, error) { 604 addr, err := w.rpcClient.GetRawChangeAddress(ctx, acctName, w.chainParams) 605 return addr, translateRPCCancelErr(err) 606 } 607 608 // ExternalAddress returns an external address from the specified account using 609 // GapPolicyWrap. The dcrwallet user should set --gaplimit= as needed to prevent 610 // address reused depending on their needs. Part of the Wallet interface. 611 func (w *rpcWallet) ExternalAddress(ctx context.Context, acctName string) (stdaddr.Address, error) { 612 addr, err := w.rpcClient.GetNewAddressGapPolicy(ctx, acctName, dcrwallet.GapPolicyWrap) 613 if err != nil { 614 return nil, translateRPCCancelErr(err) 615 } 616 return addr, nil 617 } 618 619 // LockUnspent locks or unlocks the specified outpoint. 620 // Part of the Wallet interface. 621 func (w *rpcWallet) LockUnspent(ctx context.Context, unlock bool, ops []*wire.OutPoint) error { 622 return translateRPCCancelErr(w.client().LockUnspent(ctx, unlock, ops)) 623 } 624 625 // UnspentOutput returns information about an unspent tx output, if found 626 // and unspent. Use wire.TxTreeUnknown if the output tree is unknown, the 627 // correct tree will be returned if the unspent output is found. 628 // This method is only guaranteed to return results for outputs that pay to 629 // the wallet. Returns asset.CoinNotFoundError if the unspent output cannot 630 // be located. 631 // Part of the Wallet interface. 632 func (w *rpcWallet) UnspentOutput(ctx context.Context, txHash *chainhash.Hash, index uint32, tree int8) (*TxOutput, error) { 633 var checkTrees []int8 634 switch { 635 case tree == wire.TxTreeUnknown: 636 checkTrees = []int8{wire.TxTreeRegular, wire.TxTreeStake} 637 case tree == wire.TxTreeRegular || tree == wire.TxTreeStake: 638 checkTrees = []int8{tree} 639 default: 640 return nil, fmt.Errorf("invalid tx tree %d", tree) 641 } 642 643 for _, tree := range checkTrees { 644 txOut, err := w.client().GetTxOut(ctx, txHash, index, tree, true) 645 if err != nil { 646 return nil, translateRPCCancelErr(err) 647 } 648 if txOut == nil { 649 continue 650 } 651 652 amount, err := dcrutil.NewAmount(txOut.Value) 653 if err != nil { 654 return nil, fmt.Errorf("invalid amount %f: %v", txOut.Value, err) 655 } 656 pkScript, err := hex.DecodeString(txOut.ScriptPubKey.Hex) 657 if err != nil { 658 return nil, fmt.Errorf("invalid ScriptPubKey %s: %v", txOut.ScriptPubKey.Hex, err) 659 } 660 output := &TxOutput{ 661 TxOut: newTxOut(int64(amount), txOut.ScriptPubKey.Version, pkScript), 662 Tree: tree, 663 Addresses: txOut.ScriptPubKey.Addresses, 664 Confirmations: uint32(txOut.Confirmations), 665 } 666 return output, nil 667 } 668 669 return nil, asset.CoinNotFoundError 670 } 671 672 // SignRawTransaction signs the provided transaction using rpc RawRequest. 673 // Part of the Wallet interface. 674 func (w *rpcWallet) SignRawTransaction(ctx context.Context, inTx *wire.MsgTx) (*wire.MsgTx, error) { 675 baseTx := inTx.Copy() 676 txHex, err := msgTxToHex(baseTx) 677 if err != nil { 678 return nil, fmt.Errorf("failed to encode MsgTx: %w", err) 679 } 680 var res walletjson.SignRawTransactionResult 681 err = w.rpcClientRawRequest(ctx, methodSignRawTransaction, anylist{txHex}, &res) 682 if err != nil { 683 return nil, err 684 } 685 686 for i := range res.Errors { 687 sigErr := &res.Errors[i] 688 return nil, fmt.Errorf("signing %v:%d, seq = %d, sigScript = %v, failed: %v (is wallet locked?)", 689 sigErr.TxID, sigErr.Vout, sigErr.Sequence, sigErr.ScriptSig, sigErr.Error) 690 // Will be incomplete below, so log each SignRawTransactionError and move on. 691 } 692 693 if !res.Complete { 694 baseTxB, _ := baseTx.Bytes() 695 w.log.Errorf("Incomplete raw transaction signatures (input tx: %x / incomplete signed tx: %s)", 696 baseTxB, res.Hex) 697 return nil, fmt.Errorf("incomplete raw tx signatures (is wallet locked?)") 698 } 699 700 return msgTxFromHex(res.Hex) 701 } 702 703 // SendRawTransaction broadcasts the provided transaction to the Decred network. 704 // Part of the Wallet interface. 705 func (w *rpcWallet) SendRawTransaction(ctx context.Context, tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error) { 706 hash, err := w.client().SendRawTransaction(ctx, tx, allowHighFees) 707 return hash, translateRPCCancelErr(err) 708 } 709 710 // GetBlockHeader generates a *BlockHeader for the specified block hash. The 711 // returned block header is a wire.BlockHeader with the addition of the block's 712 // median time. 713 func (w *rpcWallet) GetBlockHeader(ctx context.Context, blockHash *chainhash.Hash) (*BlockHeader, error) { 714 hdr, err := w.rpcClient.GetBlockHeader(ctx, blockHash) 715 if err != nil { 716 return nil, err 717 } 718 verboseHdr, err := w.rpcClient.GetBlockHeaderVerbose(ctx, blockHash) 719 if err != nil { 720 return nil, err 721 } 722 var nextHash *chainhash.Hash 723 if verboseHdr.NextHash != "" { 724 nextHash, err = chainhash.NewHashFromStr(verboseHdr.NextHash) 725 if err != nil { 726 return nil, fmt.Errorf("invalid next block hash %v: %w", 727 verboseHdr.NextHash, err) 728 } 729 } 730 return &BlockHeader{ 731 BlockHeader: hdr, 732 MedianTime: verboseHdr.MedianTime, 733 Confirmations: verboseHdr.Confirmations, 734 NextHash: nextHash, 735 }, nil 736 } 737 738 // GetBlock returns the MsgBlock. 739 // Part of the Wallet interface. 740 func (w *rpcWallet) GetBlock(ctx context.Context, blockHash *chainhash.Hash) (*wire.MsgBlock, error) { 741 blk, err := w.rpcClient.GetBlock(ctx, blockHash) 742 return blk, translateRPCCancelErr(err) 743 } 744 745 // GetTransaction returns the details of a wallet tx, if the wallet contains a 746 // tx with the provided hash. Returns asset.CoinNotFoundError if the tx is not 747 // found in the wallet. 748 // Part of the Wallet interface. 749 func (w *rpcWallet) GetTransaction(ctx context.Context, txHash *chainhash.Hash) (*WalletTransaction, error) { 750 tx, err := w.client().GetTransaction(ctx, txHash) 751 if err != nil { 752 if isTxNotFoundErr(err) { 753 return nil, asset.CoinNotFoundError 754 } 755 return nil, fmt.Errorf("error finding transaction %s in wallet: %w", txHash, translateRPCCancelErr(err)) 756 } 757 msgTx, err := msgTxFromHex(tx.Hex) 758 if err != nil { 759 return nil, fmt.Errorf("invalid tx hex %s: %w", tx.Hex, err) 760 } 761 return &WalletTransaction{ 762 Confirmations: tx.Confirmations, 763 BlockHash: tx.BlockHash, 764 Details: tx.Details, 765 MsgTx: msgTx, 766 }, nil 767 } 768 769 func (w *rpcWallet) ListSinceBlock(ctx context.Context, start int32) ([]ListTransactionsResult, error) { 770 hash, err := w.GetBlockHash(ctx, int64(start)) 771 if err != nil { 772 return nil, err 773 } 774 775 res, err := w.client().ListSinceBlock(ctx, hash) 776 if err != nil { 777 return nil, err 778 } 779 780 toReturn := make([]ListTransactionsResult, 0, len(res.Transactions)) 781 for _, tx := range res.Transactions { 782 toReturn = append(toReturn, ListTransactionsResult{ 783 TxID: tx.TxID, 784 BlockIndex: tx.BlockIndex, 785 BlockTime: tx.BlockTime, 786 Send: tx.Category == "send", 787 TxType: tx.TxType, 788 Fee: tx.Fee, 789 }) 790 } 791 792 return toReturn, nil 793 } 794 795 // GetRawMempool returns hashes for all txs of the specified type in the node's 796 // mempool. 797 // Part of the Wallet interface. 798 func (w *rpcWallet) GetRawMempool(ctx context.Context) ([]*chainhash.Hash, error) { 799 mempoolTxs, err := w.rpcClient.GetRawMempool(ctx, chainjson.GRMAll) 800 return mempoolTxs, translateRPCCancelErr(err) 801 } 802 803 // GetBestBlock returns the hash and height of the wallet's best block. 804 // Part of the Wallet interface. 805 func (w *rpcWallet) GetBestBlock(ctx context.Context) (*chainhash.Hash, int64, error) { 806 hash, height, err := w.client().GetBestBlock(ctx) 807 return hash, height, translateRPCCancelErr(err) 808 } 809 810 // GetBlockHash returns the hash of the mainchain block at the specified height. 811 // Part of the Wallet interface. 812 func (w *rpcWallet) GetBlockHash(ctx context.Context, blockHeight int64) (*chainhash.Hash, error) { 813 bh, err := w.client().GetBlockHash(ctx, blockHeight) 814 return bh, translateRPCCancelErr(err) 815 } 816 817 // MatchAnyScript looks for any of the provided scripts in the block specified. 818 // Part of the Wallet interface. 819 func (w *rpcWallet) MatchAnyScript(ctx context.Context, blockHash *chainhash.Hash, scripts [][]byte) (bool, error) { 820 var cfRes walletjson.GetCFilterV2Result 821 err := w.rpcClientRawRequest(ctx, methodGetCFilterV2, anylist{blockHash.String()}, &cfRes) 822 if err != nil { 823 return false, err 824 } 825 826 bf, key := cfRes.Filter, cfRes.Key 827 filterB, err := hex.DecodeString(bf) 828 if err != nil { 829 return false, fmt.Errorf("error decoding block filter: %w", err) 830 } 831 keyB, err := hex.DecodeString(key) 832 if err != nil { 833 return false, fmt.Errorf("error decoding block filter key: %w", err) 834 } 835 filter, err := gcs.FromBytesV2(blockcf2.B, blockcf2.M, filterB) 836 if err != nil { 837 return false, fmt.Errorf("error deserializing block filter: %w", err) 838 } 839 840 var bcf2Key [gcs.KeySize]byte 841 copy(bcf2Key[:], keyB) 842 843 return filter.MatchAny(bcf2Key, scripts), nil 844 845 } 846 847 // lockWallet locks the wallet. 848 func (w *rpcWallet) lockWallet(ctx context.Context) error { 849 return translateRPCCancelErr(w.rpcClient.WalletLock(ctx)) 850 } 851 852 // unlockWallet unlocks the wallet. 853 func (w *rpcWallet) unlockWallet(ctx context.Context, passphrase string, timeoutSecs int64) error { 854 return translateRPCCancelErr(w.rpcClient.WalletPassphrase(ctx, passphrase, timeoutSecs)) 855 } 856 857 // AccountUnlocked returns true if the account is unlocked. 858 // Part of the Wallet interface. 859 func (w *rpcWallet) AccountUnlocked(ctx context.Context, acctName string) (bool, error) { 860 // First return locked status of the account, falling back to walletinfo if 861 // the account is not individually password protected. 862 res, err := w.rpcClient.AccountUnlocked(ctx, acctName) 863 if err != nil { 864 return false, err 865 } 866 if res.Encrypted { 867 return *res.Unlocked, nil 868 } 869 // The account is not individually encrypted, so check wallet lock status. 870 walletInfo, err := w.rpcClient.WalletInfo(ctx) 871 if err != nil { 872 return false, fmt.Errorf("walletinfo error: %w", err) 873 } 874 return walletInfo.Unlocked, nil 875 } 876 877 // LockAccount locks the specified account. 878 // Part of the Wallet interface. 879 func (w *rpcWallet) LockAccount(ctx context.Context, acctName string) error { 880 if w.rpcConnector.Disconnected() { 881 return asset.ErrConnectionDown 882 } 883 884 // Since hung calls to Lock() may block shutdown of the consumer and thus 885 // cancellation of the ExchangeWallet subsystem's Context, dcr.ctx, give 886 // this a timeout in case the connection goes down or the RPC hangs for 887 // other reasons. 888 ctx, cancel := context.WithTimeout(ctx, 5*time.Second) 889 defer cancel() 890 891 res, err := w.rpcClient.AccountUnlocked(ctx, acctName) 892 if err != nil { 893 return err 894 } 895 if !res.Encrypted { 896 return w.lockWallet(ctx) 897 } 898 if res.Unlocked != nil && !*res.Unlocked { 899 return nil 900 } 901 902 err = w.rpcClient.LockAccount(ctx, acctName) 903 if isAccountLockedErr(err) { 904 return nil // it's already locked 905 } 906 return translateRPCCancelErr(err) 907 } 908 909 // UnlockAccount unlocks the specified account or the wallet if account is not 910 // encrypted. Part of the Wallet interface. 911 func (w *rpcWallet) UnlockAccount(ctx context.Context, pw []byte, acctName string) error { 912 res, err := w.rpcClient.AccountUnlocked(ctx, acctName) 913 if err != nil { 914 return err 915 } 916 if res.Encrypted { 917 return translateRPCCancelErr(w.rpcClient.UnlockAccount(ctx, acctName, string(pw))) 918 } 919 return w.unlockWallet(ctx, string(pw), 0) 920 921 } 922 923 // SyncStatus returns the wallet's sync status. 924 // Part of the Wallet interface. 925 func (w *rpcWallet) SyncStatus(ctx context.Context) (*asset.SyncStatus, error) { 926 syncStatus := new(walletjson.SyncStatusResult) 927 if err := w.rpcClientRawRequest(ctx, methodSyncStatus, nil, syncStatus); err != nil { 928 return nil, fmt.Errorf("rawrequest error: %w", err) 929 } 930 peers, err := w.PeerInfo(ctx) 931 if err != nil { 932 return nil, fmt.Errorf("error getting peer info: %w", err) 933 } 934 if syncStatus.Synced { 935 _, targetHeight, err := w.client().GetBestBlock(ctx) 936 if err != nil { 937 return nil, fmt.Errorf("error getting block count: %w", err) 938 } 939 return &asset.SyncStatus{ 940 Synced: len(peers) > 0 && !syncStatus.InitialBlockDownload, 941 TargetHeight: uint64(targetHeight), 942 Blocks: uint64(targetHeight), 943 }, nil 944 } 945 var targetHeight int64 946 for _, p := range peers { 947 if p.StartingHeight > targetHeight { 948 targetHeight = p.StartingHeight 949 } 950 } 951 if targetHeight == 0 { 952 return new(asset.SyncStatus), nil 953 } 954 return &asset.SyncStatus{ 955 Synced: syncStatus.Synced && !syncStatus.InitialBlockDownload, 956 TargetHeight: uint64(targetHeight), 957 Blocks: uint64(math.Round(float64(syncStatus.HeadersFetchProgress) * float64(targetHeight))), 958 }, nil 959 } 960 961 func (w *rpcWallet) PeerInfo(ctx context.Context) (peerInfo []*walletjson.GetPeerInfoResult, _ error) { 962 return peerInfo, w.rpcClientRawRequest(ctx, methodGetPeerInfo, nil, &peerInfo) 963 } 964 965 // PeerCount returns the number of network peers to which the wallet or its 966 // backing node are connected. 967 func (w *rpcWallet) PeerCount(ctx context.Context) (uint32, error) { 968 peerInfo, err := w.PeerInfo(ctx) 969 if err != nil { 970 return 0, err 971 } 972 return uint32(len(peerInfo)), err 973 } 974 975 // AddressPrivKey fetches the privkey for the specified address. 976 // Part of the Wallet interface. 977 func (w *rpcWallet) AddressPrivKey(ctx context.Context, address stdaddr.Address) (*secp256k1.PrivateKey, error) { 978 wif, err := w.rpcClient.DumpPrivKey(ctx, address) 979 if err != nil { 980 return nil, translateRPCCancelErr(err) 981 } 982 var priv secp256k1.PrivateKey 983 if overflow := priv.Key.SetByteSlice(wif.PrivKey()); overflow || priv.Key.IsZero() { 984 return nil, errors.New("invalid private key") 985 } 986 return &priv, nil 987 } 988 989 // StakeInfo returns the current gestakeinfo results. 990 func (w *rpcWallet) StakeInfo(ctx context.Context) (*wallet.StakeInfoData, error) { 991 res, err := w.rpcClient.GetStakeInfo(ctx) 992 if err != nil { 993 return nil, err 994 } 995 sdiff, err := dcrutil.NewAmount(res.Difficulty) 996 if err != nil { 997 return nil, err 998 } 999 totalSubsidy, err := dcrutil.NewAmount(res.TotalSubsidy) 1000 if err != nil { 1001 return nil, err 1002 } 1003 return &wallet.StakeInfoData{ 1004 BlockHeight: res.BlockHeight, 1005 TotalSubsidy: totalSubsidy, 1006 Sdiff: sdiff, 1007 OwnMempoolTix: res.OwnMempoolTix, 1008 Unspent: res.Unspent, 1009 Voted: res.Voted, 1010 Revoked: res.Revoked, 1011 UnspentExpired: res.UnspentExpired, 1012 PoolSize: res.PoolSize, 1013 AllMempoolTix: res.AllMempoolTix, 1014 Immature: res.Immature, 1015 Live: res.Live, 1016 Missed: res.Missed, 1017 Expired: res.Expired, 1018 }, nil 1019 } 1020 1021 // PurchaseTickets purchases n amount of tickets. Returns the purchased ticket 1022 // hashes if successful. 1023 func (w *rpcWallet) PurchaseTickets(ctx context.Context, n int, _, _ string, _ bool) ([]*asset.Ticket, error) { 1024 hashes, err := w.rpcClient.PurchaseTicket( 1025 ctx, 1026 "default", 1027 0, // spendLimit 1028 nil, // minConf 1029 nil, // ticketAddress 1030 &n, // numTickets 1031 nil, // poolAddress 1032 nil, // poolFees 1033 nil, // expiry 1034 nil, // ticketChange 1035 nil, // ticketFee 1036 ) 1037 if err != nil { 1038 return nil, err 1039 } 1040 1041 now := uint64(time.Now().Unix()) 1042 tickets := make([]*asset.Ticket, len(hashes)) 1043 for i, h := range hashes { 1044 // Need to get the ticket price 1045 tx, err := w.rpcClient.GetTransaction(ctx, h) 1046 if err != nil { 1047 return nil, fmt.Errorf("error getting transaction for new ticket %s: %w", h, err) 1048 } 1049 msgTx, err := msgTxFromHex(tx.Hex) 1050 if err != nil { 1051 return nil, fmt.Errorf("error decoding ticket %s tx hex: %v", h, err) 1052 } 1053 if len(msgTx.TxOut) == 0 { 1054 return nil, fmt.Errorf("malformed ticket transaction %s", h) 1055 } 1056 var fees uint64 1057 for _, txIn := range msgTx.TxIn { 1058 fees += uint64(txIn.ValueIn) 1059 } 1060 for _, txOut := range msgTx.TxOut { 1061 fees -= uint64(txOut.Value) 1062 } 1063 tickets[i] = &asset.Ticket{ 1064 Tx: asset.TicketTransaction{ 1065 Hash: h.String(), 1066 TicketPrice: uint64(msgTx.TxOut[0].Value), 1067 Fees: fees, 1068 Stamp: now, 1069 BlockHeight: -1, 1070 }, 1071 Status: asset.TicketStatusUnmined, 1072 } 1073 } 1074 return tickets, nil 1075 } 1076 1077 var oldSPVWalletErr = errors.New("wallet is an older spv wallet") 1078 1079 // Tickets returns active tickets. 1080 func (w *rpcWallet) Tickets(ctx context.Context) ([]*asset.Ticket, error) { 1081 return w.tickets(ctx, true) 1082 } 1083 1084 func (w *rpcWallet) tickets(ctx context.Context, includeImmature bool) ([]*asset.Ticket, error) { 1085 // GetTickets only works for spv clients after version 9.2.0 1086 if w.spvMode && !w.hasSPVTicketFunctions { 1087 return nil, oldSPVWalletErr 1088 } 1089 hashes, err := w.rpcClient.GetTickets(ctx, includeImmature) 1090 if err != nil { 1091 return nil, err 1092 } 1093 tickets := make([]*asset.Ticket, 0, len(hashes)) 1094 for _, h := range hashes { 1095 tx, err := w.client().GetTransaction(ctx, h) 1096 if err != nil { 1097 w.log.Errorf("GetTransaction error for ticket %s: %v", h, err) 1098 continue 1099 } 1100 blockHeight := int64(-1) 1101 // If the transaction is not yet mined we do not know the block hash. 1102 if tx.BlockHash != "" { 1103 blkHash, err := chainhash.NewHashFromStr(tx.BlockHash) 1104 if err != nil { 1105 w.log.Errorf("Invalid block hash %v for ticket %v: %w", tx.BlockHash, h, err) 1106 continue 1107 } 1108 // dcrwallet returns do not include the block height. 1109 hdr, err := w.client().GetBlockHeader(ctx, blkHash) 1110 if err != nil { 1111 w.log.Errorf("GetBlockHeader error for ticket %s: %v", h, err) 1112 continue 1113 } 1114 blockHeight = int64(hdr.Height) 1115 } 1116 msgTx, err := msgTxFromHex(tx.Hex) 1117 if err != nil { 1118 w.log.Errorf("Error decoding ticket %s tx hex: %v", h, err) 1119 continue 1120 } 1121 if len(msgTx.TxOut) < 1 { 1122 w.log.Errorf("No outputs for ticket %s", h) 1123 continue 1124 } 1125 // Fee is always negative. 1126 feeAmt, _ := dcrutil.NewAmount(-tx.Fee) 1127 1128 tickets = append(tickets, &asset.Ticket{ 1129 Tx: asset.TicketTransaction{ 1130 Hash: h.String(), 1131 TicketPrice: uint64(msgTx.TxOut[0].Value), 1132 Fees: uint64(feeAmt), 1133 Stamp: uint64(tx.Time), 1134 BlockHeight: blockHeight, 1135 }, 1136 // The walletjson.GetTransactionResult returned from GetTransaction 1137 // actually has a TicketStatus string field, but it doesn't appear 1138 // to ever be populated by dcrwallet. 1139 // Status: somehowConvertFromString(tx.TicketStatus), 1140 1141 // Not sure how to get the spender through RPC. 1142 // Spender: ?, 1143 }) 1144 } 1145 return tickets, nil 1146 } 1147 1148 // VotingPreferences returns current wallet voting preferences. 1149 func (w *rpcWallet) VotingPreferences(ctx context.Context) ([]*walletjson.VoteChoice, []*asset.TBTreasurySpend, []*walletjson.TreasuryPolicyResult, error) { 1150 // Get consensus vote choices. 1151 choices, err := w.rpcClient.GetVoteChoices(ctx) 1152 if err != nil { 1153 return nil, nil, nil, fmt.Errorf("unable to get vote choices: %v", err) 1154 } 1155 voteChoices := make([]*walletjson.VoteChoice, len(choices.Choices)) 1156 for i, v := range choices.Choices { 1157 vc := v 1158 voteChoices[i] = &vc 1159 } 1160 // Get tspend voting policy. 1161 const tSpendPolicyMethod = "tspendpolicy" 1162 var tSpendRes []walletjson.TSpendPolicyResult 1163 err = w.rpcClientRawRequest(ctx, tSpendPolicyMethod, nil, &tSpendRes) 1164 if err != nil { 1165 return nil, nil, nil, fmt.Errorf("unable to get treasury spend policy: %v", err) 1166 } 1167 tSpendPolicy := make([]*asset.TBTreasurySpend, len(tSpendRes)) 1168 for i, tp := range tSpendRes { 1169 // TODO: Find a way to get the tspend total value? Probably only 1170 // possible with a full node and txindex. 1171 tSpendPolicy[i] = &asset.TBTreasurySpend{ 1172 Hash: tp.Hash, 1173 CurrentPolicy: tp.Policy, 1174 } 1175 } 1176 // Get treasury voting policy. 1177 const treasuryPolicyMethod = "treasurypolicy" 1178 var treasuryRes []walletjson.TreasuryPolicyResult 1179 err = w.rpcClientRawRequest(ctx, treasuryPolicyMethod, nil, &treasuryRes) 1180 if err != nil { 1181 return nil, nil, nil, fmt.Errorf("unable to get treasury policy: %v", err) 1182 } 1183 treasuryPolicy := make([]*walletjson.TreasuryPolicyResult, len(treasuryRes)) 1184 for i, v := range treasuryRes { 1185 tp := v 1186 treasuryPolicy[i] = &tp 1187 } 1188 return voteChoices, tSpendPolicy, treasuryPolicy, nil 1189 } 1190 1191 // SetVotingPreferences sets voting preferences. 1192 // 1193 // NOTE: Will fail for communication problems with VSPs unlike internal wallets. 1194 func (w *rpcWallet) SetVotingPreferences(ctx context.Context, choices, tSpendPolicy, 1195 treasuryPolicy map[string]string) error { 1196 for k, v := range choices { 1197 if err := w.rpcClient.SetVoteChoice(ctx, k, v); err != nil { 1198 return fmt.Errorf("unable to set vote choice: %v", err) 1199 } 1200 } 1201 const setTSpendPolicyMethod = "settspendpolicy" 1202 for k, v := range tSpendPolicy { 1203 if err := w.rpcClientRawRequest(ctx, setTSpendPolicyMethod, anylist{k, v}, nil); err != nil { 1204 return fmt.Errorf("unable to set tspend policy: %v", err) 1205 } 1206 } 1207 const setTreasuryPolicyMethod = "settreasurypolicy" 1208 for k, v := range treasuryPolicy { 1209 if err := w.rpcClientRawRequest(ctx, setTreasuryPolicyMethod, anylist{k, v}, nil); err != nil { 1210 return fmt.Errorf("unable to set treasury policy: %v", err) 1211 } 1212 } 1213 return nil 1214 } 1215 1216 func (w *rpcWallet) SetTxFee(ctx context.Context, feePerKB dcrutil.Amount) error { 1217 return w.rpcClient.SetTxFee(ctx, feePerKB) 1218 } 1219 1220 // anylist is a list of RPC parameters to be converted to []json.RawMessage and 1221 // sent via nodeRawRequest. 1222 type anylist []any 1223 1224 // rpcClientRawRequest is used to marshal parameters and send requests to the 1225 // RPC server via rpcClient.RawRequest. If `thing` is non-nil, the result will 1226 // be marshaled into `thing`. 1227 func (w *rpcWallet) rpcClientRawRequest(ctx context.Context, method string, args anylist, thing any) error { 1228 params := make([]json.RawMessage, 0, len(args)) 1229 for i := range args { 1230 p, err := json.Marshal(args[i]) 1231 if err != nil { 1232 return err 1233 } 1234 params = append(params, p) 1235 } 1236 b, err := w.client().RawRequest(ctx, method, params) 1237 if err != nil { 1238 return fmt.Errorf("rawrequest error: %w", translateRPCCancelErr(err)) 1239 } 1240 if thing != nil { 1241 return json.Unmarshal(b, thing) 1242 } 1243 return nil 1244 } 1245 1246 // The rpcclient package functions will return a rpcclient.ErrRequestCanceled 1247 // error if the context is canceled. Translate these to asset.ErrRequestTimeout. 1248 func translateRPCCancelErr(err error) error { 1249 if err == nil { 1250 return nil 1251 } 1252 if errors.Is(err, rpcclient.ErrRequestCanceled) { 1253 err = asset.ErrRequestTimeout 1254 } 1255 return err 1256 } 1257 1258 // isTxNotFoundErr will return true if the error indicates that the requested 1259 // transaction is not known. 1260 func isTxNotFoundErr(err error) bool { 1261 var rpcErr *dcrjson.RPCError 1262 return errors.As(err, &rpcErr) && rpcErr.Code == dcrjson.ErrRPCNoTxInfo 1263 } 1264 1265 func isAccountLockedErr(err error) bool { 1266 var rpcErr *dcrjson.RPCError 1267 return errors.As(err, &rpcErr) && rpcErr.Code == dcrjson.ErrRPCWalletUnlockNeeded && 1268 strings.Contains(rpcErr.Message, "account is already locked") 1269 } 1270 1271 func (w *rpcWallet) walletInfo(ctx context.Context) (*walletjson.WalletInfoResult, error) { 1272 var walletInfo walletjson.WalletInfoResult 1273 err := w.rpcClientRawRequest(ctx, methodWalletInfo, nil, &walletInfo) 1274 return &walletInfo, translateRPCCancelErr(err) 1275 } 1276 1277 var _ ticketPager = (*rpcWallet)(nil) 1278 1279 func (w *rpcWallet) TicketPage(ctx context.Context, scanStart int32, n, skipN int) ([]*asset.Ticket, error) { 1280 return make([]*asset.Ticket, 0), nil 1281 }