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  }