decred.org/dcrdex@v1.0.5/client/asset/btc/rpcclient.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 btc 5 6 import ( 7 "bytes" 8 "context" 9 "encoding/hex" 10 "encoding/json" 11 "errors" 12 "fmt" 13 "strings" 14 "sync" 15 "sync/atomic" 16 "time" 17 18 "decred.org/dcrdex/client/asset" 19 "decred.org/dcrdex/dex" 20 "decred.org/dcrdex/dex/config" 21 dexbtc "decred.org/dcrdex/dex/networks/btc" 22 "github.com/btcsuite/btcd/btcec/v2" 23 "github.com/btcsuite/btcd/btcjson" 24 "github.com/btcsuite/btcd/btcutil" 25 "github.com/btcsuite/btcd/chaincfg" 26 "github.com/btcsuite/btcd/chaincfg/chainhash" 27 "github.com/btcsuite/btcd/wire" 28 "github.com/decred/dcrd/dcrjson/v4" // for dcrjson.RPCError returns from rpcclient 29 ) 30 31 const ( 32 methodGetBalances = "getbalances" 33 methodGetBalance = "getbalance" 34 methodListUnspent = "listunspent" 35 methodLockUnspent = "lockunspent" 36 methodListLockUnspent = "listlockunspent" 37 methodChangeAddress = "getrawchangeaddress" 38 methodNewAddress = "getnewaddress" 39 methodSignTx = "signrawtransactionwithwallet" 40 methodSignTxLegacy = "signrawtransaction" 41 methodUnlock = "walletpassphrase" 42 methodLock = "walletlock" 43 methodPrivKeyForAddress = "dumpprivkey" 44 methodGetTransaction = "gettransaction" 45 methodSendToAddress = "sendtoaddress" 46 methodSetTxFee = "settxfee" 47 methodGetWalletInfo = "getwalletinfo" 48 methodGetAddressInfo = "getaddressinfo" 49 methodListDescriptors = "listdescriptors" 50 methodValidateAddress = "validateaddress" 51 methodEstimateSmartFee = "estimatesmartfee" 52 methodSendRawTransaction = "sendrawtransaction" 53 methodGetTxOut = "gettxout" 54 methodGetBlock = "getblock" 55 methodGetBlockHash = "getblockhash" 56 methodGetBestBlockHash = "getbestblockhash" 57 methodGetRawMempool = "getrawmempool" 58 methodGetRawTransaction = "getrawtransaction" 59 methodGetBlockHeader = "getblockheader" 60 methodGetNetworkInfo = "getnetworkinfo" 61 methodGetBlockchainInfo = "getblockchaininfo" 62 methodFundRawTransaction = "fundrawtransaction" 63 methodListSinceBlock = "listsinceblock" 64 methodGetReceivedByAddress = "getreceivedbyaddress" 65 ) 66 67 // IsTxNotFoundErr will return true if the error indicates that the requested 68 // transaction is not known. The error must be dcrjson.RPCError with a numeric 69 // code equal to btcjson.ErrRPCNoTxInfo. WARNING: This is specific to errors 70 // from an RPC to a bitcoind (or clone) using dcrd's rpcclient! 71 func IsTxNotFoundErr(err error) bool { 72 // We are using dcrd's client with Bitcoin Core, so errors will be of type 73 // dcrjson.RPCError, but numeric codes should come from btcjson. 74 const errRPCNoTxInfo = int(btcjson.ErrRPCNoTxInfo) 75 var rpcErr *dcrjson.RPCError 76 return errors.As(err, &rpcErr) && int(rpcErr.Code) == errRPCNoTxInfo 77 } 78 79 // isMethodNotFoundErr will return true if the error indicates that the RPC 80 // method was not found by the RPC server. The error must be dcrjson.RPCError 81 // with a numeric code equal to btcjson.ErrRPCMethodNotFound.Code or a message 82 // containing "method not found". 83 func isMethodNotFoundErr(err error) bool { 84 var errRPCMethodNotFound = int(btcjson.ErrRPCMethodNotFound.Code) 85 var rpcErr *dcrjson.RPCError 86 return errors.As(err, &rpcErr) && 87 (int(rpcErr.Code) == errRPCMethodNotFound || 88 strings.Contains(strings.ToLower(rpcErr.Message), "method not found")) 89 } 90 91 // RawRequester defines decred's rpcclient RawRequest func where all RPC 92 // requests sent through. For testing, it can be satisfied by a stub. 93 type RawRequester interface { 94 RawRequest(context.Context, string, []json.RawMessage) (json.RawMessage, error) 95 } 96 97 // anylist is a list of RPC parameters to be converted to []json.RawMessage and 98 // sent via RawRequest. 99 type anylist []any 100 101 type rpcCore struct { 102 rpcConfig *RPCConfig 103 cloneParams *BTCCloneCFG 104 requesterV atomic.Value // RawRequester 105 segwit bool 106 decodeAddr dexbtc.AddressDecoder 107 stringAddr dexbtc.AddressStringer 108 legacyRawSends bool 109 minNetworkVersion uint64 110 log dex.Logger 111 chainParams *chaincfg.Params 112 omitAddressType bool 113 legacySignTx bool 114 booleanGetBlock bool 115 unlockSpends bool 116 117 deserializeTx func([]byte) (*wire.MsgTx, error) 118 serializeTx func(*wire.MsgTx) ([]byte, error) 119 hashTx func(*wire.MsgTx) *chainhash.Hash 120 numericGetRawTxRPC bool 121 manualMedianTime bool 122 addrFunc func() (btcutil.Address, error) 123 124 deserializeBlock func([]byte) (*wire.MsgBlock, error) 125 legacyValidateAddressRPC bool 126 omitRPCOptionsArg bool 127 privKeyFunc func(addr string) (*btcec.PrivateKey, error) 128 } 129 130 func (c *rpcCore) requester() RawRequester { 131 return c.requesterV.Load().(RawRequester) 132 } 133 134 // rpcClient is a bitcoind JSON RPC client that uses rpcclient.Client's 135 // RawRequest for wallet-related calls. 136 type rpcClient struct { 137 *rpcCore 138 ctx context.Context 139 descriptors bool // set on connect like ctx 140 } 141 142 var _ Wallet = (*rpcClient)(nil) 143 144 // newRPCClient is the constructor for a rpcClient. 145 func newRPCClient(cfg *rpcCore) *rpcClient { 146 return &rpcClient{rpcCore: cfg} 147 } 148 149 // ChainOK is for screening the chain field of the getblockchaininfo result. 150 func ChainOK(net dex.Network, str string) bool { 151 var chainStr string 152 switch net { 153 case dex.Mainnet: 154 chainStr = "main" 155 case dex.Testnet: 156 chainStr = "test" 157 case dex.Regtest: 158 chainStr = "reg" 159 } 160 return strings.Contains(str, chainStr) 161 } 162 163 func (wc *rpcClient) Connect(ctx context.Context, _ *sync.WaitGroup) error { 164 wc.ctx = ctx 165 // Check the version. Do it here, so we can also diagnose a bad connection. 166 netVer, codeVer, err := wc.getVersion() 167 if err != nil { 168 return fmt.Errorf("error getting version: %w", err) 169 } 170 if netVer < wc.minNetworkVersion { 171 return fmt.Errorf("reported node version %d is less than minimum %d", netVer, wc.minNetworkVersion) 172 } 173 // TODO: codeVer is actually asset-dependent. Zcash, for example, is at 174 // 170100. So we're just lucking out here, really. 175 if codeVer < minProtocolVersion { 176 return fmt.Errorf("node software out of date. version %d is less than minimum %d", codeVer, minProtocolVersion) 177 } 178 chainInfo, err := wc.getBlockchainInfo() 179 if err != nil { 180 return fmt.Errorf("getblockchaininfo error: %w", err) 181 } 182 if !ChainOK(wc.cloneParams.Network, chainInfo.Chain) { 183 return errors.New("wrong net") 184 } 185 wiRes, err := wc.GetWalletInfo() 186 if err != nil { 187 return fmt.Errorf("getwalletinfo failure: %w", err) 188 } 189 wc.descriptors = wiRes.Descriptors 190 if wc.descriptors { 191 if netVer < minDescriptorVersion { 192 return fmt.Errorf("reported node version %d is less than minimum %d"+ 193 " for descriptor wallets", netVer, minDescriptorVersion) 194 } 195 wc.log.Debug("Using a descriptor wallet.") 196 } 197 return nil 198 } 199 200 // Reconfigure attempts to reconfigure the rpcClient for the new settings. Live 201 // reconfiguration is only attempted if the new wallet type is walletTypeRPC. If 202 // the special_activelyUsed flag is set, reconfigure will fail if we can't 203 // validate ownership of the current deposit address. 204 func (wc *rpcClient) Reconfigure(cfg *asset.WalletConfig, currentAddress string) (restartRequired bool, err error) { 205 if cfg.Type != wc.cloneParams.WalletCFG.Type { 206 restartRequired = true 207 return 208 } 209 if wc.ctx == nil || wc.ctx.Err() != nil { 210 return true, nil // not connected, ok to reconfigure, but restart required 211 } 212 213 parsedCfg := new(RPCWalletConfig) 214 if err = config.Unmapify(cfg.Settings, parsedCfg); err != nil { 215 return 216 } 217 218 // Check the RPC configuration. 219 newCfg := &parsedCfg.RPCConfig 220 if err = dexbtc.CheckRPCConfig(&newCfg.RPCConfig, wc.cloneParams.WalletInfo.Name, 221 wc.cloneParams.Network, wc.cloneParams.Ports); err != nil { 222 return 223 } 224 225 // If the RPC configuration has changed, try to update the client. 226 oldCfg := wc.rpcConfig 227 if *newCfg != *oldCfg { 228 cl, err := newRPCConnection(parsedCfg, wc.cloneParams.SingularWallet) 229 if err != nil { 230 return false, fmt.Errorf("error creating RPC client with new credentials: %v", err) 231 } 232 233 // Require restart if the wallet does not own or understand our current 234 // address. We can't use wc.ownsAddress because the rpcClient still has 235 // the old requester stored, so we'll call directly. 236 method := methodGetAddressInfo 237 if wc.legacyValidateAddressRPC { 238 method = methodValidateAddress 239 } 240 ai := new(GetAddressInfoResult) 241 if err := Call(wc.ctx, cl, method, anylist{currentAddress}, ai); err != nil { 242 return false, fmt.Errorf("error getting address info with new RPC client: %w", err) 243 } else if !ai.IsMine { 244 // If the wallet is in active use, check the supplied address. 245 if parsedCfg.ActivelyUsed { // deny reconfigure 246 return false, errors.New("cannot reconfigure to a new RPC wallet during active use") 247 } 248 // Allow reconfigure, but restart to trigger dep address refresh and 249 // full connect checks, which include the getblockchaininfo check. 250 return true, nil 251 } // else same wallet, skip full reconnect 252 253 chainInfo := new(GetBlockchainInfoResult) 254 if err := Call(wc.ctx, cl, methodGetBlockchainInfo, nil, chainInfo); err != nil { 255 return false, fmt.Errorf("%s: %w", methodGetBlockchainInfo, err) 256 } 257 if !ChainOK(wc.cloneParams.Network, chainInfo.Chain) { 258 return false, errors.New("wrong net") 259 } 260 261 wc.requesterV.Store(cl) 262 wc.rpcConfig = newCfg 263 264 // No restart required 265 } 266 return 267 } 268 269 // RawRequest passes the request to the wallet's RawRequester. 270 func (wc *rpcClient) RawRequest(ctx context.Context, method string, params []json.RawMessage) (json.RawMessage, error) { 271 return wc.requester().RawRequest(ctx, method, params) 272 } 273 274 // estimateSmartFee requests the server to estimate a fee level based on the 275 // given parameters. 276 func estimateSmartFee(ctx context.Context, rr RawRequester, confTarget uint64, mode *btcjson.EstimateSmartFeeMode) (*btcjson.EstimateSmartFeeResult, error) { 277 res := new(btcjson.EstimateSmartFeeResult) 278 return res, Call(ctx, rr, methodEstimateSmartFee, anylist{confTarget, mode}, res) 279 } 280 281 // SendRawTransactionLegacy broadcasts the transaction with an additional legacy 282 // boolean `allowhighfees` argument set to false. 283 func (wc *rpcClient) SendRawTransactionLegacy(tx *wire.MsgTx) (*chainhash.Hash, error) { 284 txBytes, err := wc.serializeTx(tx) 285 if err != nil { 286 return nil, err 287 } 288 return wc.callHashGetter(methodSendRawTransaction, anylist{ 289 hex.EncodeToString(txBytes), false}) 290 } 291 292 // SendRawTransaction broadcasts the transaction. 293 func (wc *rpcClient) SendRawTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) { 294 b, err := wc.serializeTx(tx) 295 if err != nil { 296 return nil, err 297 } 298 var txid string 299 err = wc.call(methodSendRawTransaction, anylist{hex.EncodeToString(b)}, &txid) 300 if err != nil { 301 return nil, err 302 } 303 return chainhash.NewHashFromStr(txid) 304 } 305 306 // sendRawTransaction sends the MsgTx. 307 func (wc *rpcClient) sendRawTransaction(tx *wire.MsgTx) (txHash *chainhash.Hash, err error) { 308 if wc.legacyRawSends { 309 txHash, err = wc.SendRawTransactionLegacy(tx) 310 } else { 311 txHash, err = wc.SendRawTransaction(tx) 312 } 313 if err != nil { 314 return nil, err 315 } 316 if !wc.unlockSpends { 317 return txHash, nil 318 } 319 320 // TODO: lockUnspent should really just take a []*OutPoint, since it doesn't 321 // need the value. 322 ops := make([]*Output, 0, len(tx.TxIn)) 323 for _, txIn := range tx.TxIn { 324 prevOut := &txIn.PreviousOutPoint 325 ops = append(ops, &Output{Pt: NewOutPoint(&prevOut.Hash, prevOut.Index)}) 326 } 327 if err := wc.LockUnspent(true, ops); err != nil { 328 wc.log.Warnf("error unlocking spent outputs: %v", err) 329 } 330 return txHash, nil 331 } 332 333 // GetTxOut returns the transaction output info if it's unspent and 334 // nil, otherwise. 335 func (wc *rpcClient) GetTxOut(txHash *chainhash.Hash, index uint32, _ []byte, _ time.Time) (*wire.TxOut, uint32, error) { 336 txOut, err := wc.getTxOutput(txHash, index) 337 if err != nil { 338 return nil, 0, fmt.Errorf("getTxOut error: %w", err) 339 } 340 if txOut == nil { 341 return nil, 0, nil 342 } 343 outputScript, _ := hex.DecodeString(txOut.ScriptPubKey.Hex) 344 // Check equivalence of pkScript and outputScript? 345 return wire.NewTxOut(int64(toSatoshi(txOut.Value)), outputScript), uint32(txOut.Confirmations), nil 346 } 347 348 // getTxOut returns the transaction output info if it's unspent and 349 // nil, otherwise. 350 func (wc *rpcClient) getTxOutput(txHash *chainhash.Hash, index uint32) (*btcjson.GetTxOutResult, error) { 351 // Note that we pass to call pointer to a pointer (&res) so that 352 // json.Unmarshal can nil the pointer if the method returns the JSON null. 353 var res *btcjson.GetTxOutResult 354 return res, wc.call(methodGetTxOut, anylist{txHash.String(), index, true}, 355 &res) 356 } 357 358 func (wc *rpcClient) callHashGetter(method string, args anylist) (*chainhash.Hash, error) { 359 var txid string 360 err := wc.call(method, args, &txid) 361 if err != nil { 362 return nil, err 363 } 364 return chainhash.NewHashFromStr(txid) 365 } 366 367 // GetBlock fetches the MsgBlock. 368 func (wc *rpcClient) GetBlock(h chainhash.Hash) (*wire.MsgBlock, error) { 369 var blkB dex.Bytes 370 args := anylist{h.String()} 371 if wc.booleanGetBlock { 372 args = append(args, false) 373 } else { 374 args = append(args, 0) 375 } 376 err := wc.call(methodGetBlock, args, &blkB) 377 if err != nil { 378 return nil, err 379 } 380 381 return wc.deserializeBlock(blkB) 382 } 383 384 // GetBlockHash returns the hash of the block in the best block chain at the 385 // given height. 386 func (wc *rpcClient) GetBlockHash(blockHeight int64) (*chainhash.Hash, error) { 387 return wc.callHashGetter(methodGetBlockHash, anylist{blockHeight}) 388 } 389 390 // GetBestBlockHash returns the hash of the best block in the longest block 391 // chain (aka mainchain). 392 func (wc *rpcClient) GetBestBlockHash() (*chainhash.Hash, error) { 393 return wc.callHashGetter(methodGetBestBlockHash, nil) 394 } 395 396 // GetBestBlockHeader returns the height of the top mainchain block. 397 func (wc *rpcClient) GetBestBlockHeader() (*BlockHeader, error) { 398 tipHash, err := wc.GetBestBlockHash() 399 if err != nil { 400 return nil, err 401 } 402 hdr, _, err := wc.GetBlockHeader(tipHash) 403 return hdr, err 404 } 405 406 // GetBestBlockHeight returns the height of the top mainchain block. 407 func (wc *rpcClient) GetBestBlockHeight() (int32, error) { 408 header, err := wc.GetBestBlockHeader() 409 if err != nil { 410 return -1, err 411 } 412 return int32(header.Height), nil 413 } 414 415 // getChainStamp satisfies chainStamper for manual median time calculations. 416 func (wc *rpcClient) getChainStamp(blockHash *chainhash.Hash) (stamp time.Time, prevHash *chainhash.Hash, err error) { 417 hdr, _, err := wc.GetBlockHeader(blockHash) 418 if err != nil { 419 return 420 } 421 prevHash, err = chainhash.NewHashFromStr(hdr.PreviousBlockHash) 422 if err != nil { 423 return 424 } 425 return time.Unix(hdr.Time, 0).UTC(), prevHash, nil 426 } 427 428 // MedianTime is the median time for the current best block. 429 func (wc *rpcClient) MedianTime() (stamp time.Time, err error) { 430 tipHash, err := wc.GetBestBlockHash() 431 if err != nil { 432 return 433 } 434 if wc.manualMedianTime { 435 return CalcMedianTime(func(blockHash *chainhash.Hash) (stamp time.Time, prevHash *chainhash.Hash, err error) { 436 hdr, _, err := wc.GetBlockHeader(blockHash) 437 if err != nil { 438 return 439 } 440 prevHash, err = chainhash.NewHashFromStr(hdr.PreviousBlockHash) 441 if err != nil { 442 return 443 } 444 return time.Unix(hdr.Time, 0), prevHash, nil 445 }, tipHash) 446 } 447 hdr, err := wc.getRPCBlockHeader(tipHash) 448 if err != nil { 449 return 450 } 451 return time.Unix(hdr.MedianTime, 0).UTC(), nil 452 } 453 454 // GetRawMempool returns the hashes of all transactions in the memory pool. 455 func (wc *rpcClient) GetRawMempool() ([]*chainhash.Hash, error) { 456 var mempool []string 457 err := wc.call(methodGetRawMempool, nil, &mempool) 458 if err != nil { 459 return nil, err 460 } 461 462 // Convert received hex hashes to chainhash.Hash 463 hashes := make([]*chainhash.Hash, 0, len(mempool)) 464 for _, h := range mempool { 465 hash, err := chainhash.NewHashFromStr(h) 466 if err != nil { 467 return nil, err 468 } 469 hashes = append(hashes, hash) 470 } 471 return hashes, nil 472 } 473 474 // GetRawTransaction retrieves the MsgTx. 475 func (wc *rpcClient) GetRawTransaction(txHash *chainhash.Hash) (*wire.MsgTx, error) { 476 var txB dex.Bytes 477 args := anylist{txHash.String(), false} 478 if wc.numericGetRawTxRPC { 479 args[1] = 0 480 } 481 err := wc.call(methodGetRawTransaction, args, &txB) 482 if err != nil { 483 return nil, err 484 } 485 486 return wc.deserializeTx(txB) 487 } 488 489 // Balances retrieves a wallet's balance details. 490 func (wc *rpcClient) Balances() (*GetBalancesResult, error) { 491 var balances GetBalancesResult 492 return &balances, wc.call(methodGetBalances, nil, &balances) 493 } 494 495 // ListUnspent retrieves a list of the wallet's UTXOs. 496 func (wc *rpcClient) ListUnspent() ([]*ListUnspentResult, error) { 497 unspents := make([]*ListUnspentResult, 0) 498 // TODO: listunspent 0 9999999 []string{}, include_unsafe=false 499 return unspents, wc.call(methodListUnspent, anylist{uint8(0)}, &unspents) 500 } 501 502 // LockUnspent locks and unlocks outputs for spending. An output that is part of 503 // an order, but not yet spent, should be locked until spent or until the order 504 // is canceled or fails. 505 func (wc *rpcClient) LockUnspent(unlock bool, ops []*Output) error { 506 var rpcops []*RPCOutpoint // To clear all, this must be nil->null, not empty slice. 507 for _, op := range ops { 508 rpcops = append(rpcops, &RPCOutpoint{ 509 TxID: op.txHash().String(), 510 Vout: op.vout(), 511 }) 512 } 513 var success bool 514 err := wc.call(methodLockUnspent, anylist{unlock, rpcops}, &success) 515 if err == nil && !success { 516 return fmt.Errorf("lockunspent unsuccessful") 517 } 518 return err 519 } 520 521 // ListLockUnspent returns a slice of outpoints for all unspent outputs marked 522 // as locked by a wallet. 523 func (wc *rpcClient) ListLockUnspent() ([]*RPCOutpoint, error) { 524 var unspents []*RPCOutpoint 525 err := wc.call(methodListLockUnspent, nil, &unspents) 526 if err != nil { 527 return nil, err 528 } 529 if !wc.unlockSpends { 530 return unspents, nil 531 } 532 // This is quirky wallet software that does not unlock spent outputs, so 533 // we'll verify that each output is actually unspent. 534 var i int // for in-place filter 535 for _, utxo := range unspents { 536 var gtxo *btcjson.GetTxOutResult 537 err = wc.call(methodGetTxOut, anylist{utxo.TxID, utxo.Vout, true}, >xo) 538 if err != nil { 539 wc.log.Warnf("gettxout(%v:%d): %v", utxo.TxID, utxo.Vout, err) 540 continue 541 } 542 if gtxo != nil { 543 unspents[i] = utxo // unspent, keep it 544 i++ 545 continue 546 } 547 // actually spent, unlock 548 var success bool 549 op := []*RPCOutpoint{{ 550 TxID: utxo.TxID, 551 Vout: utxo.Vout, 552 }} 553 err = wc.call(methodLockUnspent, anylist{true, op}, &success) 554 if err != nil || !success { 555 wc.log.Warnf("lockunspent(unlocking %v:%d): success = %v, err = %v", 556 utxo.TxID, utxo.Vout, success, err) 557 continue 558 } 559 wc.log.Debugf("Unlocked spent outpoint %v:%d", utxo.TxID, utxo.Vout) 560 } 561 unspents = unspents[:i] 562 return unspents, nil 563 } 564 565 // ChangeAddress gets a new internal address from the wallet. The address will 566 // be bech32-encoded (P2WPKH). 567 func (wc *rpcClient) ChangeAddress() (btcutil.Address, error) { 568 var addrStr string 569 var err error 570 switch { 571 case wc.omitAddressType: 572 err = wc.call(methodChangeAddress, nil, &addrStr) 573 case wc.segwit: 574 err = wc.call(methodChangeAddress, anylist{"bech32"}, &addrStr) 575 default: 576 err = wc.call(methodChangeAddress, anylist{"legacy"}, &addrStr) 577 } 578 if err != nil { 579 return nil, err 580 } 581 return wc.decodeAddr(addrStr, wc.chainParams) 582 } 583 584 func (wc *rpcClient) ExternalAddress() (btcutil.Address, error) { 585 if wc.segwit { 586 return wc.address("bech32") 587 } 588 return wc.address("legacy") 589 } 590 591 // address is used internally for fetching addresses of various types from the 592 // wallet. 593 func (wc *rpcClient) address(aType string) (btcutil.Address, error) { 594 var addrStr string 595 args := anylist{""} 596 if !wc.omitAddressType { 597 args = append(args, aType) 598 } 599 err := wc.call(methodNewAddress, args, &addrStr) 600 if err != nil { 601 return nil, err 602 } 603 return wc.decodeAddr(addrStr, wc.chainParams) // we should consider returning a string 604 } 605 606 // SignTx attempts to have the wallet sign the transaction inputs. 607 func (wc *rpcClient) SignTx(inTx *wire.MsgTx) (*wire.MsgTx, error) { 608 txBytes, err := wc.serializeTx(inTx) 609 if err != nil { 610 return nil, fmt.Errorf("tx serialization error: %w", err) 611 } 612 res := new(SignTxResult) 613 method := methodSignTx 614 if wc.legacySignTx { 615 method = methodSignTxLegacy 616 } 617 618 err = wc.call(method, anylist{hex.EncodeToString(txBytes)}, res) 619 if err != nil { 620 return nil, fmt.Errorf("tx signing error: %w", err) 621 } 622 if !res.Complete { 623 sep := "" 624 errMsg := "" 625 for _, e := range res.Errors { 626 errMsg += e.Error + sep 627 sep = ";" 628 } 629 return nil, fmt.Errorf("signing incomplete. %d signing errors encountered: %s", len(res.Errors), errMsg) 630 } 631 outTx, err := wc.deserializeTx(res.Hex) 632 if err != nil { 633 return nil, fmt.Errorf("error deserializing transaction response: %w", err) 634 } 635 return outTx, nil 636 } 637 638 func (wc *rpcClient) listDescriptors(private bool) (*listDescriptorsResult, error) { 639 descriptors := new(listDescriptorsResult) 640 return descriptors, wc.call(methodListDescriptors, anylist{private}, descriptors) 641 } 642 643 func (wc *rpcClient) ListTransactionsSinceBlock(blockHeight int32) ([]*ListTransactionsResult, error) { 644 blockHash, err := wc.GetBlockHash(int64(blockHeight)) 645 if err != nil { 646 return nil, fmt.Errorf("getBlockHash error: %w", err) 647 } 648 result := new(struct { 649 Transactions []btcjson.ListTransactionsResult `json:"transactions"` 650 }) 651 err = wc.call(methodListSinceBlock, anylist{blockHash.String()}, result) 652 if err != nil { 653 return nil, fmt.Errorf("listtransactions error: %w", err) 654 } 655 656 txs := make([]*ListTransactionsResult, 0, len(result.Transactions)) 657 for _, tx := range result.Transactions { 658 var blockHeight uint32 659 if tx.BlockHeight != nil { 660 blockHeight = uint32(*tx.BlockHeight) 661 } 662 txs = append(txs, &ListTransactionsResult{ 663 TxID: tx.TxID, 664 BlockHeight: blockHeight, 665 BlockTime: uint64(tx.BlockTime), 666 Fee: tx.Fee, 667 Send: tx.Category == "send", 668 }) 669 } 670 671 return txs, nil 672 } 673 674 // PrivKeyForAddress retrieves the private key associated with the specified 675 // address. 676 func (wc *rpcClient) PrivKeyForAddress(addr string) (*btcec.PrivateKey, error) { 677 // Use a specialized client's privKey function 678 if wc.privKeyFunc != nil { 679 return wc.privKeyFunc(addr) 680 } 681 // Descriptor wallets do not have dumpprivkey. 682 if !wc.descriptors { 683 var keyHex string 684 err := wc.call(methodPrivKeyForAddress, anylist{addr}, &keyHex) 685 if err != nil { 686 return nil, err 687 } 688 wif, err := btcutil.DecodeWIF(keyHex) 689 if err != nil { 690 return nil, err 691 } 692 return wif.PrivKey, nil 693 } 694 695 // With descriptor wallets, we have to get the address' descriptor from 696 // getaddressinfo, parse out its key origin (fingerprint of the master 697 // private key followed by derivation path to the address) and the pubkey of 698 // the address itself. Then we get the private key using listdescriptors 699 // private=true, which returns a set of master private keys and derivation 700 // paths, one of which corresponds to the fingerprint and path from 701 // getaddressinfo. When the parent master private key is identified, we 702 // derive the private key for the address. 703 ai := new(GetAddressInfoResult) 704 if err := wc.call(methodGetAddressInfo, anylist{addr}, ai); err != nil { 705 return nil, fmt.Errorf("getaddressinfo RPC failure: %w", err) 706 } 707 wc.log.Tracef("Address %v descriptor: %v", addr, ai.Descriptor) 708 desc, err := dexbtc.ParseDescriptor(ai.Descriptor) 709 if err != nil { 710 return nil, fmt.Errorf("failed to parse descriptor %q: %w", ai.Descriptor, err) 711 } 712 if desc.KeyOrigin == nil { 713 return nil, errors.New("address descriptor has no key origin") 714 } 715 // For addresses from imported private keys that have no derivation path in 716 // the key origin, we inspect private keys of type KeyWIFPriv. For addresses 717 // with a derivation path, we match KeyExtended private keys based on the 718 // master key fingerprint and derivation path. 719 fp, addrPath := desc.KeyOrigin.Fingerprint, desc.KeyOrigin.Steps 720 // Should match: 721 // fp, path = ai.HDMasterFingerprint, ai.HDKeyPath 722 // addrPath, _, err = dexbtc.ParsePath(path) 723 bareKey := len(addrPath) == 0 724 725 if desc.KeyFmt != dexbtc.KeyHexPub { 726 return nil, fmt.Errorf("not a hexadecimal pubkey: %v", desc.Key) 727 } 728 // The key was validated by ParseDescriptor, but check again. 729 addrPubKeyB, err := hex.DecodeString(desc.Key) 730 if err != nil { 731 return nil, fmt.Errorf("address pubkey not hexadecimal: %w", err) 732 } 733 addrPubKey, err := btcec.ParsePubKey(addrPubKeyB) 734 if err != nil { 735 return nil, fmt.Errorf("invalid pubkey for address: %w", err) 736 } 737 addrPubKeyC := addrPubKey.SerializeCompressed() // may or may not equal addrPubKeyB 738 739 // Get the private key descriptors. 740 masterDescs, err := wc.listDescriptors(true) 741 if err != nil { 742 return nil, fmt.Errorf("listdescriptors RPC failure: %w", err) 743 } 744 745 // We're going to decode a number of private keys that we need to zero. 746 var toClear []interface{ Zero() } 747 defer func() { 748 for _, k := range toClear { 749 k.Zero() 750 } 751 }() // surprisingly, much cleaner than making the loop body below into a function 752 deferZero := func(z interface{ Zero() }) { toClear = append(toClear, z) } 753 754 masters: 755 for _, d := range masterDescs.Descriptors { 756 masterDesc, err := dexbtc.ParseDescriptor(d.Descriptor) 757 if err != nil { 758 wc.log.Errorf("Failed to parse descriptor %q: %v", d.Descriptor, err) 759 continue // unexpected, but check the others 760 } 761 if bareKey { // match KeyHexPub -> KeyWIFPriv 762 if masterDesc.KeyFmt != dexbtc.KeyWIFPriv { 763 continue 764 } 765 wif, err := btcutil.DecodeWIF(masterDesc.Key) 766 if err != nil { 767 wc.log.Errorf("Invalid WIF private key: %v", err) 768 continue // ParseDescriptor already validated it, so shouldn't happen 769 } 770 if !bytes.Equal(addrPubKeyC, wif.PrivKey.PubKey().SerializeCompressed()) { 771 continue // not the one 772 } 773 return wif.PrivKey, nil 774 } 775 776 // match KeyHexPub -> [fingerprint/path]KeyExtended 777 if masterDesc.KeyFmt != dexbtc.KeyExtended { 778 continue 779 } 780 // Break the key into its parts and compute the fingerprint of the 781 // master private key. 782 xPriv, fingerprint, pathStr, isRange, err := dexbtc.ParseKeyExtended(masterDesc.Key) 783 if err != nil { 784 wc.log.Debugf("Failed to parse descriptor extended key: %v", err) 785 continue 786 } 787 deferZero(xPriv) 788 if fingerprint != fp { 789 continue 790 } 791 if !xPriv.IsPrivate() { // imported xpub with no private key? 792 wc.log.Debugf("Not an extended private key. Fingerprint: %v", fingerprint) 793 continue 794 } 795 // NOTE: After finding the xprv with the matching fingerprint, we could 796 // skip to checking the private key for a match instead of first 797 // matching the path. Let's just check the path too since fingerprint 798 // collision are possible, and the different address types are allowed 799 // to use descriptors with different fingerprints. 800 if !isRange { 801 continue // imported? 802 } 803 path, _, err := dexbtc.ParsePath(pathStr) 804 if err != nil { 805 wc.log.Debugf("Failed to parse descriptor extended key path %q: %v", pathStr, err) 806 continue 807 } 808 if len(addrPath) != len(path)+1 { // addrPath includes index of self 809 continue 810 } 811 for i := range path { 812 if addrPath[i] != path[i] { 813 continue masters // different path 814 } 815 } 816 817 // NOTE: We could conceivably cache the extended private key for this 818 // address range/branch, but it could be a security risk: 819 // childIdx := addrPath[len(addrPath)-1] 820 // branch, err := dexbtc.DeepChild(xPriv, path) 821 // child, err := branch.Derive(childIdx) 822 child, err := dexbtc.DeepChild(xPriv, addrPath) 823 if err != nil { 824 return nil, fmt.Errorf("address key derivation failed: %v", err) // any point in checking the rest? 825 } 826 deferZero(child) 827 privkey, err := child.ECPrivKey() 828 if err != nil { // only errors if the extended key is not private 829 return nil, err // hdkeychain.ErrNotPrivExtKey 830 } 831 // That's the private key, but do a final check that the pubkey matches 832 // the "pubkey" field of the getaddressinfo response. 833 pubkey := privkey.PubKey().SerializeCompressed() 834 if !bytes.Equal(pubkey, addrPubKeyC) { 835 wc.log.Warnf("Derived wrong pubkey for address %v from matching descriptor %v: %x != %x", 836 addr, d.Descriptor, pubkey, addrPubKey) 837 continue // theoretically could be a fingerprint collision (see KeyOrigin docs) 838 } 839 return privkey, nil 840 } 841 842 return nil, errors.New("no private key found for address") 843 } 844 845 // GetWalletTransaction retrieves the JSON-RPC gettransaction result. 846 func (wc *rpcClient) GetWalletTransaction(txHash *chainhash.Hash) (*GetTransactionResult, error) { 847 tx := new(GetTransactionResult) 848 err := wc.call(methodGetTransaction, anylist{txHash.String()}, tx) 849 if err != nil { 850 if IsTxNotFoundErr(err) { 851 return nil, asset.CoinNotFoundError 852 } 853 return nil, err 854 } 855 return tx, nil 856 } 857 858 // WalletUnlock unlocks the wallet. 859 func (wc *rpcClient) WalletUnlock(pw []byte) error { 860 // 100000000 comes from bitcoin-cli help walletpassphrase 861 return wc.call(methodUnlock, anylist{string(pw), 100000000}, nil) 862 } 863 864 // WalletLock locks the wallet. 865 func (wc *rpcClient) WalletLock() error { 866 return wc.call(methodLock, nil, nil) 867 } 868 869 // Locked returns the wallet's lock state. 870 func (wc *rpcClient) Locked() bool { 871 walletInfo, err := wc.GetWalletInfo() 872 if err != nil { 873 wc.log.Errorf("GetWalletInfo error: %w", err) 874 return false 875 } 876 if walletInfo.UnlockedUntil == nil { 877 // This wallet is not encrypted. 878 return false 879 } 880 881 return time.Unix(*walletInfo.UnlockedUntil, 0).Before(time.Now()) 882 } 883 884 // EstimateSendTxFee returns the fee required to send tx using the provided 885 // feeRate. 886 func (wc *rpcClient) EstimateSendTxFee(tx *wire.MsgTx, feeRate uint64, subtract bool) (txfee uint64, err error) { 887 txBytes, err := wc.serializeTx(tx) 888 if err != nil { 889 return 0, fmt.Errorf("tx serialization error: %w", err) 890 } 891 args := anylist{hex.EncodeToString(txBytes)} 892 893 // 1e-5 = 1e-8 for satoshis * 1000 for kB. 894 feeRateOption := float64(feeRate) / 1e5 895 options := &btcjson.FundRawTransactionOpts{ 896 FeeRate: &feeRateOption, 897 } 898 if !wc.omitAddressType { 899 if wc.segwit { 900 options.ChangeType = &btcjson.ChangeTypeBech32 901 } else { 902 options.ChangeType = &btcjson.ChangeTypeLegacy 903 } 904 } 905 if subtract { 906 options.SubtractFeeFromOutputs = []int{0} 907 } 908 args = append(args, options) 909 910 var res struct { 911 TxBytes dex.Bytes `json:"hex"` 912 Fees float64 `json:"fee"` 913 } 914 err = wc.call(methodFundRawTransaction, args, &res) 915 if err != nil { 916 wc.log.Debugf("%s fundrawtranasaction error for args %+v: %v \n", wc.cloneParams.WalletInfo.Name, args, err) 917 // This is a work around for ZEC wallet, which does not support options 918 // argument for fundrawtransaction. 919 if wc.omitRPCOptionsArg { 920 var sendAmount uint64 921 for _, txOut := range tx.TxOut { 922 sendAmount += uint64(txOut.Value) 923 } 924 var bal float64 925 // args: "(dummy)" minconf includeWatchonly inZat 926 // Using default inZat = false for compatibility with ZCL. 927 if err := wc.call(methodGetBalance, anylist{"", 0, false}, &bal); err != nil { 928 return 0, err 929 } 930 if subtract && sendAmount <= toSatoshi(bal) { 931 return 0, errors.New("wallet does not support options") 932 } 933 } 934 return 0, fmt.Errorf("error calculating transaction fee: %w", err) 935 } 936 return toSatoshi(res.Fees), nil 937 } 938 939 // GetWalletInfo gets the getwalletinfo RPC result. 940 func (wc *rpcClient) GetWalletInfo() (*GetWalletInfoResult, error) { 941 wi := new(GetWalletInfoResult) 942 return wi, wc.call(methodGetWalletInfo, nil, wi) 943 } 944 945 // Fingerprint returns an identifier for this wallet. Only HD wallets will have 946 // an identifier. Descriptor wallets will not. 947 func (wc *rpcClient) Fingerprint() (string, error) { 948 walletInfo, err := wc.GetWalletInfo() 949 if err != nil { 950 return "", err 951 } 952 953 if walletInfo.HdSeedID == "" { 954 return "", fmt.Errorf("fingerprint not availble") 955 } 956 957 return walletInfo.HdSeedID, nil 958 } 959 960 // GetAddressInfo gets information about the given address by calling 961 // getaddressinfo RPC command. 962 func (wc *rpcClient) getAddressInfo(addr btcutil.Address, method string) (*GetAddressInfoResult, error) { 963 ai := new(GetAddressInfoResult) 964 addrStr, err := wc.stringAddr(addr, wc.chainParams) 965 if err != nil { 966 return nil, err 967 } 968 return ai, wc.call(method, anylist{addrStr}, ai) 969 } 970 971 // OwnsAddress indicates if an address belongs to the wallet. 972 func (wc *rpcClient) OwnsAddress(addr btcutil.Address) (bool, error) { 973 method := methodGetAddressInfo 974 if wc.legacyValidateAddressRPC { 975 method = methodValidateAddress 976 } 977 ai, err := wc.getAddressInfo(addr, method) 978 if err != nil { 979 return false, err 980 } 981 return ai.IsMine, nil 982 } 983 984 // SyncStatus is information about the blockchain sync status. 985 func (wc *rpcClient) SyncStatus() (*asset.SyncStatus, error) { 986 chainInfo, err := wc.getBlockchainInfo() 987 if err != nil { 988 return nil, fmt.Errorf("getblockchaininfo error: %w", err) 989 } 990 synced := !chainInfo.Syncing() 991 return &asset.SyncStatus{ 992 Synced: synced, 993 TargetHeight: uint64(chainInfo.Headers), 994 Blocks: uint64(chainInfo.Blocks), 995 }, nil 996 } 997 998 // SwapConfirmations gets the number of confirmations for the specified coin ID 999 // by first checking for a unspent output, and if not found, searching indexed 1000 // wallet transactions. 1001 func (wc *rpcClient) SwapConfirmations(txHash *chainhash.Hash, vout uint32, _ []byte, _ time.Time) (confs uint32, spent bool, err error) { 1002 // Check for an unspent output. 1003 txOut, err := wc.getTxOutput(txHash, vout) 1004 if err == nil && txOut != nil { 1005 return uint32(txOut.Confirmations), false, nil 1006 } 1007 // Check wallet transactions. 1008 tx, err := wc.GetWalletTransaction(txHash) 1009 if err != nil { 1010 if IsTxNotFoundErr(err) { 1011 return 0, false, asset.CoinNotFoundError 1012 } 1013 return 0, false, err 1014 } 1015 return uint32(tx.Confirmations), true, nil 1016 } 1017 1018 // getBlockHeader gets the *rpcBlockHeader for the specified block hash. 1019 func (wc *rpcClient) getRPCBlockHeader(blockHash *chainhash.Hash) (*BlockHeader, error) { 1020 blkHeader := new(BlockHeader) 1021 err := wc.call(methodGetBlockHeader, 1022 anylist{blockHash.String(), true}, blkHeader) 1023 if err != nil { 1024 return nil, err 1025 } 1026 1027 return blkHeader, nil 1028 } 1029 1030 // GetBlockHeader gets the *BlockHeader for the specified block hash. It also 1031 // returns a bool value to indicate whether this block is a part of main chain. 1032 // For orphaned blocks header.Confirmations is negative (typically -1). 1033 func (wc *rpcClient) GetBlockHeader(blockHash *chainhash.Hash) (header *BlockHeader, mainchain bool, err error) { 1034 hdr, err := wc.getRPCBlockHeader(blockHash) 1035 if err != nil { 1036 return nil, false, err 1037 } 1038 // RPC wallet must return negative confirmations number for orphaned blocks. 1039 mainchain = hdr.Confirmations >= 0 1040 return hdr, mainchain, nil 1041 } 1042 1043 // GetBlockHeight gets the mainchain height for the specified block. Returns 1044 // error for orphaned blocks. 1045 func (wc *rpcClient) GetBlockHeight(blockHash *chainhash.Hash) (int32, error) { 1046 hdr, _, err := wc.GetBlockHeader(blockHash) 1047 if err != nil { 1048 return -1, err 1049 } 1050 if hdr.Height < 0 { 1051 return -1, fmt.Errorf("block is not a mainchain block") 1052 } 1053 return int32(hdr.Height), nil 1054 } 1055 1056 func (wc *rpcClient) PeerCount() (uint32, error) { 1057 var r struct { 1058 Connections uint32 `json:"connections"` 1059 } 1060 err := wc.call(methodGetNetworkInfo, nil, &r) 1061 if err != nil { 1062 return 0, err 1063 } 1064 return r.Connections, nil 1065 } 1066 1067 // getBlockchainInfo sends the getblockchaininfo request and returns the result. 1068 func (wc *rpcClient) getBlockchainInfo() (*GetBlockchainInfoResult, error) { 1069 chainInfo := new(GetBlockchainInfoResult) 1070 err := wc.call(methodGetBlockchainInfo, nil, chainInfo) 1071 if err != nil { 1072 return nil, err 1073 } 1074 return chainInfo, nil 1075 } 1076 1077 // getVersion gets the current BTC network and protocol versions. 1078 func (wc *rpcClient) getVersion() (uint64, uint64, error) { 1079 r := &struct { 1080 Version uint64 `json:"version"` 1081 SubVersion string `json:"subversion"` 1082 ProtocolVersion uint64 `json:"protocolversion"` 1083 }{} 1084 err := wc.call(methodGetNetworkInfo, nil, r) 1085 if err != nil { 1086 return 0, 0, err 1087 } 1088 // TODO: We might consider checking getnetworkinfo's "subversion" field, 1089 // which is something like "/Satoshi:24.0.1/". 1090 wc.log.Debugf("Node at %v reports subversion \"%v\"", wc.rpcConfig.RPCBind, r.SubVersion) 1091 return r.Version, r.ProtocolVersion, nil 1092 } 1093 1094 // FindRedemptionsInMempool attempts to find spending info for the specified 1095 // contracts by searching every input of all txs in the mempool. 1096 func (wc *rpcClient) FindRedemptionsInMempool(ctx context.Context, reqs map[OutPoint]*FindRedemptionReq) (discovered map[OutPoint]*FindRedemptionResult) { 1097 return FindRedemptionsInMempool(ctx, wc.log, reqs, wc.GetRawMempool, wc.GetRawTransaction, wc.segwit, wc.hashTx, wc.chainParams) 1098 } 1099 1100 func FindRedemptionsInMempool( 1101 ctx context.Context, 1102 log dex.Logger, 1103 reqs map[OutPoint]*FindRedemptionReq, 1104 getMempool func() ([]*chainhash.Hash, error), 1105 getTx func(txHash *chainhash.Hash) (*wire.MsgTx, error), 1106 segwit bool, 1107 hashTx func(*wire.MsgTx) *chainhash.Hash, 1108 chainParams *chaincfg.Params, 1109 1110 ) (discovered map[OutPoint]*FindRedemptionResult) { 1111 contractsCount := len(reqs) 1112 log.Debugf("finding redemptions for %d contracts in mempool", contractsCount) 1113 1114 discovered = make(map[OutPoint]*FindRedemptionResult, len(reqs)) 1115 1116 var totalFound, totalCanceled int 1117 logAbandon := func(reason string) { 1118 // Do not remove the contracts from the findRedemptionQueue 1119 // as they could be subsequently redeemed in some mined tx(s), 1120 // which would be captured when a new tip is reported. 1121 if totalFound+totalCanceled > 0 { 1122 log.Debugf("%d redemptions found, %d canceled out of %d contracts in mempool", 1123 totalFound, totalCanceled, contractsCount) 1124 } 1125 log.Errorf("abandoning mempool redemption search for %d contracts because of %s", 1126 contractsCount-totalFound-totalCanceled, reason) 1127 } 1128 1129 mempoolTxs, err := getMempool() 1130 if err != nil { 1131 logAbandon(fmt.Sprintf("error retrieving transactions: %v", err)) 1132 return 1133 } 1134 1135 for _, txHash := range mempoolTxs { 1136 if ctx.Err() != nil { 1137 return nil 1138 } 1139 tx, err := getTx(txHash) 1140 if err != nil { 1141 logAbandon(fmt.Sprintf("getrawtransaction error for tx hash %v: %v", txHash, err)) 1142 return 1143 } 1144 newlyDiscovered := FindRedemptionsInTxWithHasher(ctx, segwit, reqs, tx, chainParams, hashTx) 1145 for outPt, res := range newlyDiscovered { 1146 discovered[outPt] = res 1147 } 1148 1149 } 1150 return 1151 } 1152 1153 // SearchBlockForRedemptions attempts to find spending info for the specified 1154 // contracts by searching every input of all txs in the provided block range. 1155 func (wc *rpcClient) SearchBlockForRedemptions(ctx context.Context, reqs map[OutPoint]*FindRedemptionReq, blockHash chainhash.Hash) (discovered map[OutPoint]*FindRedemptionResult) { 1156 msgBlock, err := wc.GetBlock(blockHash) 1157 if err != nil { 1158 wc.log.Errorf("RPC GetBlock error: %v", err) 1159 return 1160 } 1161 return SearchBlockForRedemptions(ctx, reqs, msgBlock, wc.segwit, wc.hashTx, wc.chainParams) 1162 } 1163 1164 func SearchBlockForRedemptions( 1165 ctx context.Context, 1166 reqs map[OutPoint]*FindRedemptionReq, 1167 msgBlock *wire.MsgBlock, 1168 segwit bool, 1169 hashTx func(*wire.MsgTx) *chainhash.Hash, 1170 chainParams *chaincfg.Params, 1171 ) (discovered map[OutPoint]*FindRedemptionResult) { 1172 1173 discovered = make(map[OutPoint]*FindRedemptionResult, len(reqs)) 1174 1175 for _, msgTx := range msgBlock.Transactions { 1176 newlyDiscovered := FindRedemptionsInTxWithHasher(ctx, segwit, reqs, msgTx, chainParams, hashTx) 1177 for outPt, res := range newlyDiscovered { 1178 discovered[outPt] = res 1179 } 1180 } 1181 return 1182 } 1183 1184 func (wc *rpcClient) AddressUsed(addr string) (bool, error) { 1185 var recv float64 1186 const minConf = 0 1187 if err := wc.call(methodGetReceivedByAddress, []any{addr, minConf}, &recv); err != nil { 1188 return false, err 1189 } 1190 return recv != 0, nil 1191 } 1192 1193 // call is used internally to marshal parameters and send requests to the RPC 1194 // server via (*rpcclient.Client).RawRequest. If thing is non-nil, the result 1195 // will be marshaled into thing. 1196 func (wc *rpcClient) call(method string, args anylist, thing any) error { 1197 return Call(wc.ctx, wc.requester(), method, args, thing) 1198 } 1199 1200 func Call(ctx context.Context, r RawRequester, method string, args anylist, thing any) error { 1201 params := make([]json.RawMessage, 0, len(args)) 1202 for i := range args { 1203 p, err := json.Marshal(args[i]) 1204 if err != nil { 1205 return err 1206 } 1207 params = append(params, p) 1208 } 1209 1210 b, err := r.RawRequest(ctx, method, params) 1211 if err != nil { 1212 return fmt.Errorf("rawrequest (%v) error: %w", method, err) 1213 } 1214 if thing != nil { 1215 return json.Unmarshal(b, thing) 1216 } 1217 return nil 1218 }