decred.org/dcrdex@v1.0.5/client/asset/btc/electrum/wallet_methods.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 electrum 5 6 import ( 7 "context" 8 "encoding/hex" 9 "errors" 10 "fmt" 11 "strconv" 12 "strings" 13 ) 14 15 const ( 16 // Wallet-agnostic commands 17 methodCommands = "commands" // list of supported methods 18 methodGetInfo = "getinfo" 19 methodGetServers = "getservers" 20 methodGetFeeRate = "getfeerate" 21 methodGetAddressHistory = "getaddresshistory" 22 methodGetAddressUnspent = "getaddressunspent" 23 methodBroadcast = "broadcast" 24 methodValidateAddress = "validateaddress" 25 26 // Wallet-specific commands 27 methodCreateNewAddress = "createnewaddress" // beyond gap limit, makes recovery difficult 28 methodGetUnusedAddress = "getunusedaddress" 29 methodGetTransaction = "gettransaction" 30 methodListUnspent = "listunspent" 31 methodGetPrivateKeys = "getprivatekeys" // requires password for protected wallets 32 methodPayTo = "payto" // requires password for protected wallets 33 methodAddLocalTx = "addtransaction" 34 methodRemoveLocalTx = "removelocaltx" 35 methodGetTxStatus = "get_tx_status" // only wallet txns 36 methodGetBalance = "getbalance" 37 methodIsMine = "ismine" 38 methodSignTransaction = "signtransaction" // requires password for protected wallets 39 methodFreezeUTXO = "freeze_utxo" 40 methodUnfreezeUTXO = "unfreeze_utxo" 41 methodOnchainHistory = "onchain_history" 42 methodVersion = "version" 43 ) 44 45 // Commands gets a list of the supported wallet RPCs. 46 func (wc *WalletClient) Commands(ctx context.Context) ([]string, error) { 47 var res string 48 err := wc.Call(ctx, methodCommands, nil, &res) 49 if err != nil { 50 return nil, err 51 } 52 return strings.Split(res, " "), nil 53 } 54 55 // GetInfo gets basic Electrum wallet info. 56 func (wc *WalletClient) GetInfo(ctx context.Context) (*GetInfoResult, error) { 57 var res GetInfoResult 58 err := wc.Call(ctx, methodGetInfo, nil, &res) 59 if err != nil { 60 return nil, err 61 } 62 return &res, nil 63 } 64 65 // GetServers gets the electrum servers known to the wallet. These are the 66 // possible servers to which Electrum may connect. This includes the currently 67 // connected server named in the GetInfo result. 68 func (wc *WalletClient) GetServers(ctx context.Context) ([]*GetServersResult, error) { 69 type getServersResult struct { 70 Pruning string `json:"pruning"` // oldest block or "-" for no pruning 71 SSL string `json:"s"` // port, as a string for some reason 72 TCP string `json:"t"` 73 Version string `json:"version"` // e.g. "1.4.2" 74 } 75 var res map[string]*getServersResult 76 err := wc.Call(ctx, methodGetServers, nil, &res) 77 if err != nil { 78 return nil, err 79 } 80 81 servers := make([]*GetServersResult, 0, len(res)) 82 for host, info := range res { 83 var ssl, tcp uint16 84 if info.SSL != "" { 85 sslP, err := strconv.ParseUint(info.SSL, 10, 16) 86 if err == nil { 87 ssl = uint16(sslP) 88 } else { 89 fmt.Println(err) 90 } 91 } 92 if info.TCP != "" { 93 tcpP, err := strconv.ParseUint(info.TCP, 10, 16) 94 if err == nil { 95 tcp = uint16(tcpP) 96 } else { 97 fmt.Println(err) 98 } 99 } 100 servers = append(servers, &GetServersResult{ 101 Host: host, 102 Pruning: info.Pruning, 103 SSL: ssl, 104 TCP: tcp, 105 Version: info.Version, 106 }) 107 } 108 109 return servers, nil 110 } 111 112 // FeeRate gets a fee rate estimate for a block confirmation target, where 1 113 // indicates the next block. 114 func (wc *WalletClient) FeeRate(ctx context.Context, _ int64) (int64, error) { 115 var res struct { 116 Method string `json:"method"` 117 SatPerKB int64 `json:"sat/kvB"` 118 Tooltip string `json:"tooltip"` 119 Value int64 `json:"value"` 120 } 121 err := wc.Call(ctx, methodGetFeeRate, nil, &res) 122 if err != nil { 123 return 0, err 124 } 125 return res.SatPerKB, nil 126 } 127 128 type walletReq struct { 129 Wallet string `json:"wallet,omitempty"` 130 } 131 132 // CreateNewAddress generates a new address, ignoring the gap limit. NOTE: There 133 // is no method to retrieve a change address (makes recovery difficult). 134 func (wc *WalletClient) CreateNewAddress(ctx context.Context) (string, error) { 135 var res string 136 err := wc.Call(ctx, methodCreateNewAddress, &walletReq{wc.walletFile}, &res) 137 if err != nil { 138 return "", err 139 } 140 return res, nil 141 } 142 143 // GetUnusedAddress gets the next unused address from the wallet. It may have 144 // already been requested. 145 func (wc *WalletClient) GetUnusedAddress(ctx context.Context) (string, error) { 146 var res string 147 err := wc.Call(ctx, methodGetUnusedAddress, &walletReq{wc.walletFile}, &res) 148 if err != nil { 149 return "", err 150 } 151 return res, nil 152 } 153 154 type addrReq struct { 155 Addr string `json:"address"` 156 Wallet string `json:"wallet,omitempty"` 157 } 158 159 // CheckAddress validates the address and reports if it belongs to the wallet. 160 func (wc *WalletClient) CheckAddress(ctx context.Context, addr string) (valid, mine bool, err error) { 161 err = wc.Call(ctx, methodIsMine, addrReq{Addr: addr, Wallet: wc.walletFile}, &mine) 162 if err != nil { 163 return 164 } 165 err = wc.Call(ctx, methodValidateAddress, positional{addr}, &valid) // no wallet arg for validateaddress 166 if err != nil { 167 return 168 } 169 return 170 } 171 172 // GetAddressHistory returns the history an address. Confirmed transactions will 173 // have a nil Fee field, while unconfirmed transactions will have a Fee and a 174 // value of zero for Height. 175 func (wc *WalletClient) GetAddressHistory(ctx context.Context, addr string) ([]*GetAddressHistoryResult, error) { 176 var res []*GetAddressHistoryResult 177 err := wc.Call(ctx, methodGetAddressHistory, positional{addr}, &res) // no wallet arg for getaddresshistory 178 if err != nil { 179 return nil, err 180 } 181 return res, nil 182 } 183 184 // GetAddressUnspent returns the unspent outputs for an address. Unconfirmed 185 // outputs will have a value of zero for Height. 186 func (wc *WalletClient) GetAddressUnspent(ctx context.Context, addr string) ([]*GetAddressUnspentResult, error) { 187 var res []*GetAddressUnspentResult 188 err := wc.Call(ctx, methodGetAddressUnspent, positional{addr}, &res) // no wallet arg for getaddressunspent 189 if err != nil { 190 return nil, err 191 } 192 return res, nil 193 } 194 195 type utxoReq struct { 196 UTXO string `json:"coin"` 197 Wallet string `json:"wallet,omitempty"` 198 } 199 200 // FreezeUTXO freezes/locks a single UTXO. It will still be reported by 201 // listunspent while locked. 202 func (wc *WalletClient) FreezeUTXO(ctx context.Context, txid string, out uint32) error { 203 utxo := txid + ":" + strconv.FormatUint(uint64(out), 10) 204 var res bool 205 err := wc.Call(ctx, methodFreezeUTXO, &utxoReq{UTXO: utxo, Wallet: wc.walletFile}, &res) 206 if err != nil { 207 return err 208 } 209 if !res { // always returns true in all forks I've checked 210 return fmt.Errorf("wallet could not freeze utxo %v", utxo) 211 } 212 return nil 213 } 214 215 // UnfreezeUTXO unfreezes/unlocks a single UTXO. 216 func (wc *WalletClient) UnfreezeUTXO(ctx context.Context, txid string, out uint32) error { 217 utxo := txid + ":" + strconv.FormatUint(uint64(out), 10) 218 var res bool 219 err := wc.Call(ctx, methodUnfreezeUTXO, &utxoReq{UTXO: utxo, Wallet: wc.walletFile}, &res) 220 if err != nil { 221 return err 222 } 223 if !res { // always returns true in all forks I've checked 224 return fmt.Errorf("wallet could not unfreeze utxo %v", utxo) 225 } 226 return nil 227 } 228 229 type txidReq struct { 230 TxID string `json:"txid"` 231 Wallet string `json:"wallet,omitempty"` 232 } 233 234 // GetRawTransaction retrieves the serialized transaction identified by txid. 235 func (wc *WalletClient) GetRawTransaction(ctx context.Context, txid string) ([]byte, error) { 236 var res string 237 err := wc.Call(ctx, methodGetTransaction, &txidReq{TxID: txid, Wallet: wc.walletFile}, &res) 238 if err != nil { 239 return nil, err 240 } 241 tx, err := hex.DecodeString(res) 242 if err != nil { 243 return nil, err 244 } 245 return tx, nil 246 } 247 248 // GetWalletTxConfs will get the confirmations on the wallet-related 249 // transaction. This function will error if it is either not a wallet 250 // transaction or not known to the wallet. 251 func (wc *WalletClient) GetWalletTxConfs(ctx context.Context, txid string) (int, error) { 252 var res struct { 253 Confs int `json:"confirmations"` 254 } 255 err := wc.Call(ctx, methodGetTxStatus, &txidReq{TxID: txid, Wallet: wc.walletFile}, &res) 256 if err != nil { 257 return 0, err 258 } 259 return res.Confs, nil 260 } 261 262 // ListUnspent returns details on all unspent outputs for the wallet. Note that 263 // the pkScript is not included, and the user would have to retrieve it with 264 // GetRawTransaction for PrevOutHash if the output is of interest. 265 func (wc *WalletClient) ListUnspent(ctx context.Context) ([]*ListUnspentResult, error) { 266 var res []*ListUnspentResult 267 err := wc.Call(ctx, methodListUnspent, &walletReq{wc.walletFile}, &res) 268 if err != nil { 269 return nil, err 270 } 271 return res, nil 272 } 273 274 // GetBalance returns the result of the getbalance wallet RPC. 275 func (wc *WalletClient) GetBalance(ctx context.Context) (*Balance, error) { 276 var res struct { 277 Confirmed floatString `json:"confirmed"` 278 Unconfirmed floatString `json:"unconfirmed"` 279 Immature floatString `json:"unmatured"` // yes, unmatured! 280 } 281 err := wc.Call(ctx, methodGetBalance, &walletReq{wc.walletFile}, &res) 282 if err != nil { 283 return nil, err 284 } 285 return &Balance{ 286 Confirmed: float64(res.Confirmed), 287 Unconfirmed: float64(res.Unconfirmed), 288 Immature: float64(res.Immature), 289 }, nil 290 } 291 292 // payto(self, destination, amount, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None, 293 // nocheck=False, unsigned=False, rbf=None, password=None, locktime=None, addtransaction=False, wallet: Abstract_Wallet = None): 294 type paytoReq struct { 295 Addr string `json:"destination"` 296 Amount string `json:"amount"` // BTC, or "!" for max 297 Fee *float64 `json:"fee,omitempty"` 298 FeeRate *float64 `json:"feerate,omitempty"` // sat/vB, gets multiplied by 1000 for extra precision, omit for high prio 299 ChangeAddr string `json:"change_addr,omitempty"` 300 // FromAddr omitted 301 FromUTXOs string `json:"from_coins,omitempty"` 302 NoCheck bool `json:"nocheck"` 303 Unsigned bool `json:"unsigned"` // unsigned returns a base64 psbt thing 304 RBF bool `json:"rbf,omitempty"` // default to null 305 Password string `json:"password,omitempty"` 306 LockTime *int64 `json:"locktime,omitempty"` 307 AddTransaction bool `json:"addtransaction"` 308 Wallet string `json:"wallet,omitempty"` 309 } 310 311 // PayTo sends the specified amount in BTC (or the conventional unit for the 312 // assets e.g. LTC) to an address using a certain fee rate. The transaction is 313 // not broadcasted; the raw bytes of the signed transaction are returned. After 314 // the caller verifies the transaction, it may be sent with Broadcast. 315 func (wc *WalletClient) PayTo(ctx context.Context, walletPass string, addr string, amtBTC float64, feeRate float64) ([]byte, error) { 316 if feeRate < 1 { 317 return nil, errors.New("fee rate in sat/vB too low") 318 } 319 amt := strconv.FormatFloat(amtBTC, 'f', 8, 64) 320 var res string 321 err := wc.Call(ctx, methodPayTo, &paytoReq{ 322 Addr: addr, 323 Amount: amt, 324 FeeRate: &feeRate, 325 Password: walletPass, 326 // AddTransaction adds the transaction to Electrum as a "local" txn 327 // before broadcasting. If we don't, rapid back-to-back sends can result 328 // in a mempool conflict from spending the same prevouts. 329 AddTransaction: true, 330 Wallet: wc.walletFile, 331 }, &res) 332 if err != nil { 333 return nil, err 334 } 335 txRaw, err := hex.DecodeString(res) 336 if err != nil { 337 return nil, err 338 } 339 return txRaw, nil 340 } 341 342 // PayToFromAbsFee allows specifying prevouts (in txid:vout format) and an 343 // absolute fee in BTC instead of a fee rate. This combination allows specifying 344 // precisely how much will be withdrawn from the wallet (subtracting fees), 345 // unless the change is dust and omitted. The transaction is not broadcasted; 346 // the raw bytes of the signed transaction are returned. After the caller 347 // verifies the transaction, it may be sent with Broadcast. 348 func (wc *WalletClient) PayToFromCoinsAbsFee(ctx context.Context, walletPass string, fromCoins []string, addr string, amtBTC float64, absFee float64) ([]byte, error) { 349 if absFee > 1 { 350 return nil, errors.New("abs fee too high") 351 } 352 amt := strconv.FormatFloat(amtBTC, 'f', 8, 64) 353 var res string 354 err := wc.Call(ctx, methodPayTo, &paytoReq{ 355 Addr: addr, 356 Amount: amt, 357 Fee: &absFee, 358 Password: walletPass, 359 FromUTXOs: strings.Join(fromCoins, ","), 360 AddTransaction: true, 361 Wallet: wc.walletFile, 362 }, &res) 363 if err != nil { 364 return nil, err 365 } 366 txRaw, err := hex.DecodeString(res) 367 if err != nil { 368 return nil, err 369 } 370 return txRaw, nil 371 } 372 373 // Sweep sends all available funds to an address with a specified fee rate. No 374 // change output is created. The transaction is not broadcasted; the raw bytes 375 // of the signed transaction are returned. After the caller verifies the 376 // transaction, it may be sent with Broadcast. 377 func (wc *WalletClient) Sweep(ctx context.Context, walletPass string, addr string, feeRate float64) ([]byte, error) { 378 if feeRate < 1 { 379 return nil, errors.New("fee rate in sat/vB too low") 380 } 381 var res string 382 err := wc.Call(ctx, methodPayTo, &paytoReq{ 383 Addr: addr, 384 Amount: "!", // special "max" indicator, creating no change output 385 FeeRate: &feeRate, 386 Password: walletPass, 387 AddTransaction: true, 388 Wallet: wc.walletFile, 389 }, &res) 390 if err != nil { 391 return nil, err 392 } 393 txRaw, err := hex.DecodeString(res) 394 if err != nil { 395 return nil, err 396 } 397 return txRaw, nil 398 } 399 400 type signTransactionArgs struct { 401 Tx string `json:"tx"` 402 Pass string `json:"password,omitempty"` 403 // 4.0.9 has privkey in this request, but 4.2 does not since it has a 404 // signtransaction_with_privkey request. (this RPC should not use positional 405 // arguments) 406 // Privkey string `json:"privkey,omitempty"` // sign with wallet if empty 407 Wallet string `json:"wallet,omitempty"` 408 IgnoreWarnings bool `json:"iknowwhatimdoing,omitempty"` 409 } 410 411 // SetIncludeIgnoreWarnings sets the includeIgnoreWarnings bool. Needed for btc 412 // at 4.5.5 but causes ltc at 4.2.2 to fail. 413 func (wc *WalletClient) SetIncludeIgnoreWarnings(include bool) { 414 wc.includeIgnoreWarnings.Store(include) 415 } 416 417 // SignTx signs the base-64 encoded PSBT with the wallet's keys, returning the 418 // signed transaction. 419 func (wc *WalletClient) SignTx(ctx context.Context, walletPass string, psbtB64 string) ([]byte, error) { 420 var res string 421 req := &signTransactionArgs{ 422 Tx: psbtB64, 423 Pass: walletPass, 424 Wallet: wc.walletFile} 425 if wc.includeIgnoreWarnings.Load() { 426 req.IgnoreWarnings = true 427 } 428 err := wc.Call(ctx, methodSignTransaction, req, &res) 429 if err != nil { 430 return nil, err 431 } 432 txRaw, err := hex.DecodeString(res) 433 if err != nil { 434 return nil, err 435 } 436 return txRaw, nil 437 } 438 439 // Broadcast submits the transaction to the network. 440 func (wc *WalletClient) Broadcast(ctx context.Context, tx []byte) (string, error) { 441 txStr := hex.EncodeToString(tx) 442 var res string 443 err := wc.Call(ctx, methodBroadcast, positional{txStr}, &res) // no wallet arg 444 if err != nil { 445 return "", err 446 } 447 return res, nil 448 } 449 450 type rawTxReq struct { 451 RawTx string `json:"tx"` 452 Wallet string `json:"wallet,omitempty"` 453 } 454 455 // AddLocalTx is used to add a "local" transaction to the Electrum wallet DB. 456 // This does not broadcast it. 457 func (wc *WalletClient) AddLocalTx(ctx context.Context, tx []byte) (string, error) { 458 txStr := hex.EncodeToString(tx) 459 var txid string 460 err := wc.Call(ctx, methodAddLocalTx, &rawTxReq{RawTx: txStr, Wallet: wc.walletFile}, &txid) 461 if err != nil { 462 return "", err 463 } 464 return txid, nil 465 } 466 467 // RemoveLocalTx is used to remove a "local" transaction from the Electrum 468 // wallet DB. This can only be done if the tx was not broadcasted. This is 469 // required if using AddLocalTx or a payTo method that added the local 470 // transaction but either it failed to broadcast or the user no longer wants to 471 // send it after inspecting the raw transaction. Calling RemoveLocalTx with an 472 // already broadcast or non-existent txid will not generate an error. 473 func (wc *WalletClient) RemoveLocalTx(ctx context.Context, txid string) error { 474 return wc.Call(ctx, methodRemoveLocalTx, &txidReq{TxID: txid, Wallet: wc.walletFile}, nil) 475 } 476 477 type getPrivKeyArgs struct { 478 Addr string `json:"address"` 479 Pass string `json:"password,omitempty"` 480 Wallet string `json:"wallet,omitempty"` 481 } 482 483 // GetPrivateKeys uses the getprivatekeys RPC to retrieve the keys for a given 484 // address. The returned string is WIF-encoded. 485 func (wc *WalletClient) GetPrivateKeys(ctx context.Context, walletPass, addr string) (string, error) { 486 var res string 487 err := wc.Call(ctx, methodGetPrivateKeys, &getPrivKeyArgs{ 488 Addr: addr, 489 Pass: walletPass, 490 Wallet: wc.walletFile}, 491 &res) 492 if err != nil { 493 return "", err 494 } 495 privSplit := strings.Split(res, ":") 496 if len(privSplit) != 2 { 497 return "", errors.New("bad key") 498 } 499 return privSplit[1], nil 500 } 501 502 type onchainHistoryReq struct { 503 Wallet string `json:"wallet,omitempty"` 504 From int64 `json:"from_height,omitempty"` 505 To int64 `json:"to_height,omitempty"` 506 } 507 508 func (wc *WalletClient) OnchainHistory(ctx context.Context, from, to int64) ([]TransactionResult, error) { 509 // A balance summary is included but left out here. 510 var res struct { 511 Transactions []TransactionResult `json:"transactions"` 512 } 513 err := wc.Call(ctx, methodOnchainHistory, &onchainHistoryReq{Wallet: wc.walletFile, From: from, To: to}, &res) 514 if err != nil { 515 return nil, err 516 } 517 return res.Transactions, nil 518 } 519 520 func (wc *WalletClient) Version(ctx context.Context) (string, error) { 521 var res string 522 err := wc.Call(ctx, methodVersion, &walletReq{wc.walletFile}, &res) 523 if err != nil { 524 return "", err 525 } 526 return res, nil 527 }