decred.org/dcrdex@v1.0.5/client/asset/btc/electrum/wallet.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  	"bytes"
     8  	"context"
     9  	"encoding/base64"
    10  	"encoding/json"
    11  	"fmt"
    12  	"io"
    13  	"net/http"
    14  	"sync/atomic"
    15  	"time"
    16  )
    17  
    18  const defaultWalletTimeout = 10 * time.Second
    19  
    20  // WalletClient is an Electrum wallet HTTP JSON-RPC client.
    21  type WalletClient struct {
    22  	reqID      uint64
    23  	url        string
    24  	auth       string
    25  	walletFile string
    26  
    27  	// HTTPClient may be set by the user to a custom http.Client. The
    28  	// constructor sets a vanilla client.
    29  	HTTPClient *http.Client
    30  	// Timeout is the timeout on http requests. A 10 second default is set by
    31  	// the constructor.
    32  	Timeout time.Duration
    33  
    34  	includeIgnoreWarnings atomic.Bool
    35  }
    36  
    37  // NewWalletClient constructs a new Electrum wallet RPC client with the given
    38  // authorization information and endpoint. The endpoint should include the
    39  // protocol, e.g. http://127.0.0.1:4567. To specify a custom http.Client or
    40  // request timeout, the fields may be set after construction. The full path to
    41  // the wallet file, if provided, will be passed directly to Electrum in RPCs
    42  // that have an optional "wallet" field.
    43  func NewWalletClient(user, pass, endpoint, walletFile string) *WalletClient {
    44  	// Prepare the HTTP Basic Authorization request header. This avoids
    45  	// re-encoding it for every request with (*http.Request).SetBasicAuth.
    46  	auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+pass))
    47  	return &WalletClient{
    48  		url:        endpoint,
    49  		auth:       auth,
    50  		walletFile: walletFile,
    51  		HTTPClient: &http.Client{},
    52  		Timeout:    defaultWalletTimeout,
    53  	}
    54  }
    55  
    56  func (ec *WalletClient) nextID() uint64 {
    57  	return atomic.AddUint64(&ec.reqID, 1)
    58  }
    59  
    60  // Call makes a JSON-RPC request for the given method with the provided
    61  // arguments. args may be a struct or slice that marshalls to JSON. If it is a
    62  // slice, it represents positional arguments. If it is a struct or pointer to a
    63  // struct, it represents "named" parameters in key-value format. Any arguments
    64  // should have their fields appropriately tagged for JSON marshalling. The
    65  // result is marshaled into result if it is non-nil, otherwise the result is
    66  // discarded.
    67  func (ec *WalletClient) Call(ctx context.Context, method string, args any, result any) error {
    68  	reqMsg, err := prepareRequest(ec.nextID(), method, args)
    69  	if err != nil {
    70  		return err
    71  	}
    72  
    73  	bodyReader := bytes.NewReader(reqMsg)
    74  	ctx, cancel := context.WithTimeout(ctx, ec.Timeout)
    75  	defer cancel()
    76  	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, ec.url, bodyReader)
    77  	if err != nil {
    78  		return err
    79  	}
    80  	httpReq.Close = true
    81  	httpReq.Header.Set("Content-Type", "application/json")
    82  	httpReq.Header.Set("Authorization", ec.auth) // httpReq.SetBasicAuth(ec.user, ec.pass)
    83  
    84  	resp, err := ec.HTTPClient.Do(httpReq)
    85  	if err != nil {
    86  		return err
    87  	}
    88  	defer resp.Body.Close()
    89  
    90  	if resp.StatusCode != http.StatusOK {
    91  		b, _ := io.ReadAll(resp.Body)
    92  		return fmt.Errorf("%d: %s", resp.StatusCode, string(b))
    93  	}
    94  
    95  	jsonResp := &response{}
    96  	err = json.NewDecoder(resp.Body).Decode(jsonResp)
    97  	if err != nil {
    98  		return err
    99  	}
   100  	if jsonResp.Error != nil {
   101  		return jsonResp.Error
   102  	}
   103  
   104  	if result != nil {
   105  		return json.Unmarshal(jsonResp.Result, result)
   106  	}
   107  	return nil
   108  }