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 }