decred.org/dcrwallet/v3@v3.1.0/rpc/documentation/clientusage.md (about)

     1  # Client usage
     2  
     3  Clients use RPC to interact with the wallet.  A client may be implemented in any
     4  language directly supported by [gRPC](https://www.grpc.io/), languages capable of
     5  performing [FFI](https://en.wikipedia.org/wiki/Foreign_function_interface) with
     6  these, and languages that share a common runtime (e.g. Scala, Kotlin, and Ceylon
     7  for the JVM, F# for the CLR, etc.).  Exact instructions differ slightly
     8  depending on the language being used, but the general process is the same for
     9  each.  In short summary, to call RPC server methods, a client must:
    10  
    11  1. Generate client bindings specific for the [wallet RPC server API](./api.md)
    12  2. Import or include the gRPC dependency
    13  3. (Optional) Wrap the client bindings with application-specific types
    14  4. Open a gRPC channel using the wallet server's self-signed TLS certificate
    15  
    16  The only exception to these steps is if the client is being written in Go.  In
    17  that case, the first step may be omitted by importing the bindings from
    18  dcrwallet itself.
    19  
    20  The rest of this document provides short examples of how to quickly get started
    21  by implementing a basic client that fetches the balance of the default account
    22  (account 0) from a testnet wallet listening on `localhost:19111` in several
    23  different languages:
    24  
    25  - [Go](#go)
    26  - [C++](#cpp)
    27  - [C#](#csharp)
    28  - [Node.js](#nodejs)
    29  - [Python](#python)
    30  
    31  Unless otherwise stated under the language example, it is assumed that
    32  gRPC is already already installed.  The gRPC installation procedure
    33  can vary greatly depending on the operating system being used and
    34  whether a gRPC source install is required.  Follow the [gRPC install
    35  instructions](https://github.com/grpc/grpc/blob/master/INSTALL) if
    36  gRPC is not already installed.  A full gRPC install also includes
    37  [Protocol Buffers](https://github.com/google/protobuf) (compiled with
    38  support for the proto3 language version), which contains the protoc
    39  tool and language plugins used to compile this project's `.proto`
    40  files to language-specific bindings.
    41  
    42  ## Go
    43  
    44  The native gRPC library (gRPC Core) is not required for Go clients (a
    45  pure Go implementation is used instead) and no additional setup is
    46  required to generate Go bindings.
    47  
    48  ```Go
    49  package main
    50  
    51  import (
    52  	"context"
    53  	"crypto/tls"
    54  	"crypto/x509"
    55  	"fmt"
    56  	"io/ioutil"
    57  	"path/filepath"
    58  
    59  	pb "decred.org/dcrwallet/v3/rpc/walletrpc"
    60  	"google.golang.org/grpc"
    61  	"google.golang.org/grpc/credentials"
    62  
    63  	"github.com/decred/dcrd/dcrutil/v4"
    64  )
    65  
    66  var (
    67  	certificateFile      = filepath.Join(dcrutil.AppDataDir("dcrwallet", false), "rpc.cert")
    68  	walletClientCertFile = "client.pem" // must be part of ~/.dcrwallet/clients.pem
    69  	walletClientKeyFile  = "client-key.pem"
    70  )
    71  
    72  func main() {
    73  	serverCAs := x509.NewCertPool()
    74  	serverCert, err := ioutil.ReadFile(certificateFile)
    75  	if err != nil {
    76  		fmt.Println(err)
    77  		return
    78  	}
    79  	if !serverCAs.AppendCertsFromPEM(serverCert) {
    80  		fmt.Printf("no certificates found in %s\n", certificateFile)
    81  		return
    82  	}
    83  	keypair, err := tls.LoadX509KeyPair(walletClientCertFile, walletClientKeyFile)
    84  	if err != nil {
    85  		fmt.Println(err)
    86  		return
    87  	}
    88  	creds := credentials.NewTLS(&tls.Config{
    89  		Certificates: []tls.Certificate{keypair},
    90  		RootCAs:      serverCAs,
    91  	})
    92  	conn, err := grpc.Dial("localhost:19111", grpc.WithTransportCredentials(creds))
    93  	if err != nil {
    94  		fmt.Println(err)
    95  		return
    96  	}
    97  	defer conn.Close()
    98  	c := pb.NewWalletServiceClient(conn)
    99  
   100  	balanceRequest := &pb.BalanceRequest{
   101  		AccountNumber:         0,
   102  		RequiredConfirmations: 1,
   103  	}
   104  	balanceResponse, err := c.Balance(context.Background(), balanceRequest)
   105  	if err != nil {
   106  		fmt.Println(err)
   107  		return
   108  	}
   109  
   110  	fmt.Println("Spendable balance: ", dcrutil.Amount(balanceResponse.Spendable))
   111  }
   112  ```
   113  
   114  <a name="cpp"/>
   115  ## C++
   116  
   117  **Note:** Protocol Buffers and gRPC require at least C++11.  The example client
   118  is written using C++14.
   119  
   120  **Note:** The following instructions assume the client is being written on a
   121  Unix-like platform (with instructions using the `sh` shell and Unix-isms in the
   122  example source code) with a source gRPC install in `/usr/local`.
   123  
   124  First, generate the C++ language bindings by compiling the `.proto`:
   125  
   126  ```bash
   127  $ protoc -I/path/to/dcrwallet/rpc --cpp_out=. --grpc_out=. \
   128    --plugin=protoc-gen-grpc=$(which grpc_cpp_plugin) \
   129    /path/to/dcrwallet/rpc/api.proto
   130  ```
   131  
   132  Once the `.proto` file has been compiled, the example client can be completed.
   133  Note that the following code uses synchronous calls which will block the main
   134  thread on all gRPC IO.
   135  
   136  ```C++
   137  // example.cc
   138  #include <unistd.h>
   139  #include <sys/types.h>
   140  #include <pwd.h>
   141  
   142  #include <fstream>
   143  #include <iostream>
   144  #include <sstream>
   145  #include <string>
   146  
   147  #include <grpc++/grpc++.h>
   148  
   149  #include "api.grpc.pb.h"
   150  
   151  using namespace std::string_literals;
   152  
   153  struct NoHomeDirectoryException : std::exception {
   154      char const* what() const noexcept override {
   155          return "Failed to lookup home directory";
   156      }
   157  };
   158  
   159  auto read_file(std::string const& file_path) -> std::string {
   160      std::ifstream in{file_path};
   161      std::stringstream ss{};
   162      ss << in.rdbuf();
   163      return ss.str();
   164  }
   165  
   166  auto main() -> int {
   167      // Before the gRPC native library (gRPC Core) is lazily loaded and
   168      // initialized, an environment variable must be set so BoringSSL is
   169      // configured to use ECDSA TLS certificates (required by dcrwallet).
   170      setenv("GRPC_SSL_CIPHER_SUITES", "HIGH+ECDSA", 1);
   171  
   172      // Note: This path is operating system-dependent.  This can be created
   173      // portably using boost::filesystem or the experimental filesystem class
   174      // expected to ship in C++17.
   175      auto wallet_tls_cert_file = []{
   176          auto pw = getpwuid(getuid());
   177          if (pw == nullptr || pw->pw_dir == nullptr) {
   178              throw NoHomeDirectoryException{};
   179          }
   180          return pw->pw_dir + "/.dcrwallet/rpc.cert"s;
   181      }();
   182  
   183      grpc::SslCredentialsOptions cred_options{
   184          .pem_root_certs = read_file(wallet_tls_cert_file),
   185      };
   186      auto creds = grpc::SslCredentials(cred_options);
   187      auto channel = grpc::CreateChannel("localhost:19111", creds);
   188      auto stub = walletrpc::WalletService::NewStub(channel);
   189  
   190      grpc::ClientContext context{};
   191  
   192      walletrpc::BalanceRequest request{};
   193      request.set_account_number(0);
   194      request.set_required_confirmations(1);
   195  
   196      walletrpc::BalanceResponse response{};
   197      auto status = stub->Balance(&context, request, &response);
   198      if (!status.ok()) {
   199          std::cout << status.error_message() << std::endl;
   200      } else {
   201          std::cout << "Spendable balance: " << response.spendable() << " atoms" << std::endl;
   202      }
   203  }
   204  ```
   205  
   206  The example can then be built with the following commands:
   207  
   208  ```bash
   209  $ c++ -std=c++14 -I/usr/local/include -pthread -c -o api.pb.o api.pb.cc
   210  $ c++ -std=c++14 -I/usr/local/include -pthread -c -o api.grpc.pb.o api.grpc.pb.cc
   211  $ c++ -std=c++14 -I/usr/local/include -pthread -c -o example.o example.cc
   212  $ c++ *.o -L/usr/local/lib -lgrpc++ -lgrpc -lgpr -lprotobuf -lpthread -ldl -o example
   213  ```
   214  
   215  <a name="csharp"/>
   216  ## C&#35;
   217  
   218  The quickest way of generating client bindings in a Windows .NET environment is
   219  by using the protoc binary included in the gRPC NuGet package.  From the NuGet
   220  package manager PowerShell console, this can be performed with:
   221  
   222  ```
   223  PM> Install-Package Grpc
   224  ```
   225  
   226  The protoc and C# plugin binaries can then be found in the packages directory.
   227  For example, `.\packages\Google.Protobuf.x.x.x\tools\protoc.exe` and
   228  `.\packages\Grpc.Tools.x.x.x\tools\grpc_csharp_plugin.exe`.
   229  
   230  When writing a client on other platforms (e.g. Mono on OS X), or when doing a
   231  full gRPC source install on Windows, protoc and the C# plugin must be installed
   232  by other means.  Consult the [official documentation](https://github.com/grpc/grpc/blob/master/src/csharp/README.md)
   233  for these steps.
   234  
   235  Once protoc and the C# plugin have been obtained, client bindings can be
   236  generated.  The following command generates the files `Api.cs` and `ApiGrpc.cs`
   237  in the `Example` project directory using the `Walletrpc` namespace:
   238  
   239  ```PowerShell
   240  PS> & protoc.exe -I \Path\To\dcrwallet\rpc --csharp_out=Example --grpc_out=Example `
   241      --plugin=protoc-gen-grpc=\Path\To\grpc_csharp_plugin.exe `
   242      \Path\To\dcrwallet\rpc\api.proto
   243  ```
   244  
   245  Once references have been added to the project for the `Google.Protobuf` and
   246  `Grpc.Core` assemblies, the example client can be implemented.
   247  
   248  ```C#
   249  using Grpc.Core;
   250  using System;
   251  using System.IO;
   252  using System.Text;
   253  using System.Threading.Tasks;
   254  using Walletrpc;
   255  
   256  namespace Example
   257  {
   258      static class Program
   259      {
   260          static void Main(string[] args)
   261          {
   262              ExampleAsync().Wait();
   263          }
   264  
   265          static async Task ExampleAsync()
   266          {
   267              // Before the gRPC native library (gRPC Core) is lazily loaded and initialized,
   268              // an environment variable must be set so BoringSSL is configured to use ECDSA TLS
   269              // certificates (required by dcrwallet).
   270              Environment.SetEnvironmentVariable("GRPC_SSL_CIPHER_SUITES", "HIGH+ECDSA");
   271  
   272              var walletAppData = Portability.LocalAppData(Environment.OSVersion.Platform, "Dcrwallet");
   273              var walletTlsCertFile = Path.Combine(walletAppData, "rpc.cert");
   274              var cert = await FileUtils.ReadFileAsync(walletTlsCertFile);
   275              var channel = new Channel("localhost:19111", new SslCredentials(cert));
   276              try
   277              {
   278                  var c = WalletService.NewClient(channel);
   279                  var balanceRequest = new BalanceRequest
   280                  {
   281                      AccountNumber = 0,
   282                      RequiredConfirmations = 1,
   283                  };
   284                  var balanceResponse = await c.BalanceAsync(balanceRequest);
   285                  Console.WriteLine($"Spendable balance: {balanceResponse.Spendable} atoms");
   286              }
   287              finally
   288              {
   289                  await channel.ShutdownAsync();
   290              }
   291          }
   292      }
   293  
   294      static class FileUtils
   295      {
   296          public static async Task<string> ReadFileAsync(string filePath)
   297          {
   298              using (var r = new StreamReader(filePath, Encoding.UTF8))
   299              {
   300                  return await r.ReadToEndAsync();
   301              }
   302          }
   303      }
   304  
   305      static class Portability
   306      {
   307          public static string LocalAppData(PlatformID platform, string processName)
   308          {
   309              if (processName == null)
   310                  throw new ArgumentNullException(nameof(processName));
   311              if (processName.Length == 0)
   312                  throw new ArgumentException(nameof(processName) + " may not have zero length");
   313  
   314              switch (platform)
   315              {
   316                  case PlatformID.Win32NT:
   317                      return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
   318                          ToUpper(processName));
   319                  case PlatformID.MacOSX:
   320                      return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal),
   321                          "Library", "Application Support", ToUpper(processName));
   322                  case PlatformID.Unix:
   323                      return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal),
   324                          ToDotLower(processName));
   325                  default:
   326                      throw new PlatformNotSupportedException($"PlatformID={platform}");
   327              }
   328          }
   329  
   330          private static string ToUpper(string value)
   331          {
   332              var firstChar = value[0];
   333              if (char.IsUpper(firstChar))
   334                  return value;
   335              else
   336                  return char.ToUpper(firstChar) + value.Substring(1);
   337          }
   338  
   339          private static string ToDotLower(string value)
   340          {
   341              var firstChar = value[0];
   342              return "." + char.ToLower(firstChar) + value.Substring(1);
   343          }
   344      }
   345  }
   346  ```
   347  
   348  ## Node.js
   349  
   350  First, install the gRPC `npm` package, which includes a pre-compiled gRPC Core
   351  native library and the bindings for Node.js clients:
   352  
   353  ```
   354  npm install grpc
   355  ```
   356  
   357  A Node.js client does not require generating JavaScript stub files for
   358  the wallet's API from the `.proto`.  Instead, a call to `grpc.load`
   359  with the `.proto` file path dynamically loads the Protobuf descriptor
   360  and generates bindings for each service.  Either copy the `.proto` to
   361  the client project directory, or reference the file from the
   362  `dcrwallet` project directory.
   363  
   364  ```JavaScript
   365  // Before the gRPC native library (gRPC Core) is lazily loaded and
   366  // initialized, an environment variable must be set so BoringSSL is
   367  // configured to use ECDSA TLS certificates (required by dcrwallet).
   368  process.env['GRPC_SSL_CIPHER_SUITES'] = 'HIGH+ECDSA';
   369  
   370  var fs = require('fs');
   371  var path = require('path');
   372  var os = require('os');
   373  var grpc = require('grpc');
   374  var protoDescriptor = grpc.load('./api.proto');
   375  var walletrpc = protoDescriptor.walletrpc;
   376  
   377  var certPath = '';
   378  if (os.platform() == 'win32') {
   379    certPath = path.join(process.env.LOCALAPPDATA, 'Dcrwallet', 'rpc.cert');
   380  } else if (os.platform() == 'darwin') {
   381    certPath = path.join(process.env.HOME, 'Library', 'Application Support',
   382      'Dcrwallet', 'rpc.cert');
   383  } else {
   384    certPath = path.join(process.env.HOME, '.dcrwallet', 'rpc.cert');
   385  }
   386  
   387  var cert = fs.readFileSync(certPath);
   388  var creds = grpc.credentials.createSsl(cert);
   389  var client = new walletrpc.WalletService('localhost:19111', creds);
   390  
   391  var request = {
   392    account_number: 0,
   393    required_confirmations: 1
   394  };
   395  client.balance(request, function(err, response) {
   396    if (err) {
   397      console.error(err);
   398    } else {
   399      console.log('Spendable balance:', response.spendable, 'atoms');
   400    }
   401  });
   402  ```
   403  
   404  ## Python
   405  
   406  **Note:** gRPC requires Python 2.7.
   407  
   408  First, install the gRPC `pip` package, which includes a pre-compiled gRPC Core
   409  native library and the bindings for Python clients:
   410  
   411  ```
   412  pip install grpcio
   413  ```
   414  
   415  Generate Python stubs from the `.proto`:
   416  
   417  ```bash
   418  $ protoc -I /path/to/decred/dcrwallet/rpc --python_out=. --grpc_out=. \
   419    --plugin=protoc-gen-grpc=$(which grpc_python_plugin) \
   420    /path/to/dcrwallet/rpc/api.proto
   421  ```
   422  
   423  Implement the client:
   424  
   425  ```Python
   426  import os
   427  import platform
   428  from grpc.beta import implementations
   429  
   430  import api_pb2 as walletrpc
   431  
   432  timeout = 1 # seconds
   433  
   434  def main():
   435      # Before the gRPC native library (gRPC Core) is lazily loaded and
   436      # initialized, an environment variable must be set so BoringSSL is
   437      # configured to use ECDSA TLS certificates (required by dcrwallet).
   438      os.environ['GRPC_SSL_CIPHER_SUITES'] = 'HIGH+ECDSA'
   439  
   440      if platform.system() == 'Windows':
   441          cert_file_path = os.path.join(os.environ['LOCALAPPDATA'], "Dcrwallet", "rpc.cert")
   442      elif platform.system() == 'Darwin':
   443          cert_file_path = os.path.join(os.environ['HOME'], 'Library', 'Application Support',
   444                                        'Dcrwallet', 'rpc.cert')
   445      else:
   446          cert_file_path = os.path.join(os.environ['HOME'], '.dcrwallet', 'rpc.cert')
   447  
   448      with open(cert_file_path, 'r') as f:
   449          cert = f.read()
   450      creds = implementations.ssl_client_credentials(cert, None, None)
   451      channel = implementations.secure_channel('localhost', 19111, creds)
   452      stub = walletrpc.beta_create_WalletService_stub(channel)
   453  
   454      request = walletrpc.BalanceRequest(account_number = 0, required_confirmations = 1)
   455      response = stub.Balance(request, timeout)
   456      print 'Spendable balance: %d atoms' % response.spendable
   457  
   458  if __name__ == '__main__':
   459      main()
   460  ```