consolidate: remove gRPC APIs

This commit is contained in:
Roy Lee 2022-09-19 15:55:57 -07:00
parent efb8e0b699
commit 2b0d245b1f
12 changed files with 10 additions and 5457 deletions

View file

@ -75,7 +75,7 @@ func walletMain() error {
// Create and start HTTP server to serve wallet client connections. // Create and start HTTP server to serve wallet client connections.
// This will be updated with the wallet and chain server RPC client // This will be updated with the wallet and chain server RPC client
// created below after each is created. // created below after each is created.
rpcs, legacyRPCServer, err := startRPCServers(loader) legacyRPCServer, err := startRPCServers(loader)
if err != nil { if err != nil {
log.Errorf("Unable to create RPC servers: %v", err) log.Errorf("Unable to create RPC servers: %v", err)
return err return err
@ -88,7 +88,7 @@ func walletMain() error {
} }
loader.RunAfterLoad(func(w *wallet.Wallet) { loader.RunAfterLoad(func(w *wallet.Wallet) {
startWalletRPCServices(w, rpcs, legacyRPCServer) startWalletRPCServices(w, legacyRPCServer)
}) })
if !cfg.NoInitialLoad { if !cfg.NoInitialLoad {
@ -110,15 +110,6 @@ func walletMain() error {
log.Errorf("Failed to close wallet: %v", err) log.Errorf("Failed to close wallet: %v", err)
} }
}) })
if rpcs != nil {
addInterruptHandler(func() {
// TODO: Does this need to wait for the grpc server to
// finish up any requests?
log.Warn("Stopping RPC server...")
rpcs.Stop()
log.Info("RPC server shutdown")
})
}
if legacyRPCServer != nil { if legacyRPCServer != nil {
addInterruptHandler(func() { addInterruptHandler(func() {
log.Warn("Stopping legacy RPC server...") log.Warn("Stopping legacy RPC server...")

2
log.go
View file

@ -15,7 +15,6 @@ import (
"github.com/lbryio/lbcd/rpcclient" "github.com/lbryio/lbcd/rpcclient"
"github.com/lbryio/lbcwallet/chain" "github.com/lbryio/lbcwallet/chain"
"github.com/lbryio/lbcwallet/rpc/legacyrpc" "github.com/lbryio/lbcwallet/rpc/legacyrpc"
"github.com/lbryio/lbcwallet/rpc/rpcserver"
"github.com/lbryio/lbcwallet/wallet" "github.com/lbryio/lbcwallet/wallet"
"github.com/lbryio/lbcwallet/wtxmgr" "github.com/lbryio/lbcwallet/wtxmgr"
) )
@ -67,7 +66,6 @@ func init() {
wtxmgr.UseLogger(txmgrLog) wtxmgr.UseLogger(txmgrLog)
chain.UseLogger(chainLog) chain.UseLogger(chainLog)
rpcclient.UseLogger(chainLog) rpcclient.UseLogger(chainLog)
rpcserver.UseLogger(grpcLog)
legacyrpc.UseLogger(legacyRPCLog) legacyrpc.UseLogger(legacyRPCLog)
} }

View file

@ -1,319 +0,0 @@
syntax = "proto3";
package walletrpc;
service VersionService {
rpc Version (VersionRequest) returns (VersionResponse);
}
message VersionRequest {}
message VersionResponse {
string version_string = 1;
uint32 major = 2;
uint32 minor = 3;
uint32 patch = 4;
string prerelease = 5;
string build_metadata = 6;
}
service WalletService {
// Queries
rpc Ping (PingRequest) returns (PingResponse);
rpc Network (NetworkRequest) returns (NetworkResponse);
rpc AccountNumber (AccountNumberRequest) returns (AccountNumberResponse);
rpc Accounts (AccountsRequest) returns (AccountsResponse);
rpc Balance (BalanceRequest) returns (BalanceResponse);
rpc GetTransactions (GetTransactionsRequest) returns (GetTransactionsResponse);
// Notifications
rpc TransactionNotifications (TransactionNotificationsRequest) returns (stream TransactionNotificationsResponse);
rpc SpentnessNotifications (SpentnessNotificationsRequest) returns (stream SpentnessNotificationsResponse);
rpc AccountNotifications (AccountNotificationsRequest) returns (stream AccountNotificationsResponse);
// Control
rpc ChangePassphrase (ChangePassphraseRequest) returns (ChangePassphraseResponse);
rpc RenameAccount (RenameAccountRequest) returns (RenameAccountResponse);
rpc NextAccount (NextAccountRequest) returns (NextAccountResponse);
rpc NextAddress (NextAddressRequest) returns (NextAddressResponse);
rpc ImportPrivateKey (ImportPrivateKeyRequest) returns (ImportPrivateKeyResponse);
rpc FundTransaction (FundTransactionRequest) returns (FundTransactionResponse);
rpc SignTransaction (SignTransactionRequest) returns (SignTransactionResponse);
rpc PublishTransaction (PublishTransactionRequest) returns (PublishTransactionResponse);
}
service WalletLoaderService {
rpc WalletExists (WalletExistsRequest) returns (WalletExistsResponse);
rpc CreateWallet (CreateWalletRequest) returns (CreateWalletResponse);
rpc OpenWallet (OpenWalletRequest) returns (OpenWalletResponse);
rpc CloseWallet (CloseWalletRequest) returns (CloseWalletResponse);
rpc StartConsensusRpc (StartConsensusRpcRequest) returns (StartConsensusRpcResponse);
}
message TransactionDetails {
message Input {
uint32 index = 1;
uint32 previous_account = 2;
int64 previous_amount = 3;
}
message Output {
uint32 index = 1;
uint32 account = 2;
bool internal = 3;
}
bytes hash = 1;
bytes transaction = 2;
repeated Input debits = 3;
repeated Output credits = 4;
int64 fee = 5;
int64 timestamp = 6; // May be earlier than a block timestamp, but never later.
}
message BlockDetails {
bytes hash = 1;
int32 height = 2;
int64 timestamp = 3;
repeated TransactionDetails transactions = 4;
}
message AccountBalance {
uint32 account = 1;
int64 total_balance = 2;
}
message PingRequest {}
message PingResponse {}
message NetworkRequest {}
message NetworkResponse {
uint32 active_network = 1;
}
message AccountNumberRequest {
string account_name = 1;
}
message AccountNumberResponse {
uint32 account_number = 1;
}
message AccountsRequest {}
message AccountsResponse {
message Account {
uint32 account_number = 1;
string account_name = 2;
int64 total_balance = 3;
uint32 external_key_count = 4;
uint32 internal_key_count = 5;
uint32 imported_key_count = 6;
}
repeated Account accounts = 1;
bytes current_block_hash = 2;
int32 current_block_height = 3;
}
message RenameAccountRequest {
uint32 account_number = 1;
string new_name = 2;
}
message RenameAccountResponse {}
message NextAccountRequest {
bytes passphrase = 1;
string account_name = 2;
}
message NextAccountResponse {
uint32 account_number = 1;
}
message NextAddressRequest {
uint32 account = 1;
enum Kind {
BIP0044_EXTERNAL = 0;
BIP0044_INTERNAL = 1;
}
Kind kind = 2;
}
message NextAddressResponse {
string address = 1;
}
message ImportPrivateKeyRequest {
bytes passphrase = 1;
uint32 account = 2;
string private_key_wif = 3;
bool rescan = 4;
}
message ImportPrivateKeyResponse {
}
message BalanceRequest {
uint32 account_number = 1;
int32 required_confirmations = 2;
}
message BalanceResponse {
int64 total = 1;
int64 spendable = 2;
int64 immature_reward = 3;
}
message GetTransactionsRequest {
// Optionally specify the starting block from which to begin including all transactions.
// Either the starting block hash or height may be specified, but not both.
// If a block height is specified and is negative, the absolute value becomes the number of
// last blocks to include. That is, given a current chain height of 1000 and a starting block
// height of -3, transaction notifications will be created for blocks 998, 999, and 1000.
// If both options are excluded, transaction results are created for transactions since the
// genesis block.
bytes starting_block_hash = 1;
sint32 starting_block_height = 2;
// Optionally specify the last block that transaction results may appear in.
// Either the ending block hash or height may be specified, but not both.
// If both are excluded, transaction results are created for all transactions
// through the best block, and include all unmined transactions.
bytes ending_block_hash = 3;
int32 ending_block_height = 4;
// Include at least this many of the newest transactions if they exist.
// Cannot be used when the ending block hash is specified.
//
// TODO: remove until spec adds it back in some way.
int32 minimum_recent_transactions = 5;
// TODO: limit max number of txs?
}
message GetTransactionsResponse {
repeated BlockDetails mined_transactions = 1;
repeated TransactionDetails unmined_transactions = 2;
}
message ChangePassphraseRequest {
enum Key {
PRIVATE = 0;
PUBLIC = 1;
}
Key key = 1;
bytes old_passphrase = 2;
bytes new_passphrase = 3;
}
message ChangePassphraseResponse {}
message FundTransactionRequest {
uint32 account = 1;
int64 target_amount = 2;
int32 required_confirmations = 3;
bool include_immature_coinbases = 4;
bool include_change_script = 5;
bool include_stakes = 6;
}
message FundTransactionResponse {
message PreviousOutput {
bytes transaction_hash = 1;
uint32 output_index = 2;
int64 amount = 3;
bytes pk_script = 4;
int64 receive_time = 5;
bool from_coinbase = 6;
}
repeated PreviousOutput selected_outputs = 1;
int64 total_amount = 2;
bytes change_pk_script = 3;
}
message SignTransactionRequest {
bytes passphrase = 1;
bytes serialized_transaction = 2;
// If no indexes are specified, signatures scripts will be added for
// every input. If any input indexes are specified, only those inputs
// will be signed. Rather than returning an incompletely signed
// transaction if any of the inputs to be signed can not be, the RPC
// immediately errors.
repeated uint32 input_indexes = 3;
}
message SignTransactionResponse {
bytes transaction = 1;
repeated uint32 unsigned_input_indexes = 2;
}
message PublishTransactionRequest {
bytes signed_transaction = 1;
}
message PublishTransactionResponse {}
message TransactionNotificationsRequest {}
message TransactionNotificationsResponse {
// Sorted by increasing height. This is a repeated field so many new blocks
// in a new best chain can be notified at once during a reorganize.
repeated BlockDetails attached_blocks = 1;
// If there was a chain reorganize, there may have been blocks with wallet
// transactions that are no longer in the best chain. These are those
// block's hashes.
repeated bytes detached_blocks = 2;
// Any new unmined transactions are included here. These unmined transactions
// refer to the current best chain, so transactions from detached blocks may
// be moved to mempool and included here if they are not mined or double spent
// in the new chain. Additonally, if no new blocks were attached but a relevant
// unmined transaction is seen by the wallet, it will be reported here.
repeated TransactionDetails unmined_transactions = 3;
// Instead of notifying all of the removed unmined transactions,
// just send all of the current hashes.
repeated bytes unmined_transaction_hashes = 4;
}
message SpentnessNotificationsRequest {
uint32 account = 1;
bool no_notify_unspent = 2;
bool no_notify_spent = 3;
}
message SpentnessNotificationsResponse {
bytes transaction_hash = 1;
uint32 output_index = 2;
message Spender {
bytes transaction_hash = 1;
uint32 input_index = 2;
}
Spender spender = 3;
}
message AccountNotificationsRequest {}
message AccountNotificationsResponse {
uint32 account_number = 1;
string account_name = 2;
uint32 external_key_count = 3;
uint32 internal_key_count = 4;
uint32 imported_key_count = 5;
}
message CreateWalletRequest {
bytes public_passphrase = 1;
bytes private_passphrase = 2;
bytes seed = 3;
}
message CreateWalletResponse {}
message OpenWalletRequest {
bytes public_passphrase = 1;
}
message OpenWalletResponse {}
message CloseWalletRequest {}
message CloseWalletResponse {}
message WalletExistsRequest {}
message WalletExistsResponse {
bool exists = 1;
}
message StartConsensusRpcRequest {
string network_address = 1;
string username = 2;
bytes password = 3;
bytes certificate = 4;
bool skipverify = 5;
}
message StartConsensusRpcResponse {}

View file

@ -1,16 +0,0 @@
# RPC Documentation
This project provides a [gRPC](http://www.grpc.io/) server for Remote Procedure
Call (RPC) access from other processes. This is intended to be the primary
means by which users, through other client programs, interact with the wallet.
These documents cover the documentation for both consumers of the server and
developers who must make changes or additions to the API and server
implementation:
- [API specification](./api.md)
- [Client usage](./clientusage.md)
- [Making API changes](./serverchanges.md)
A legacy RPC server based on the JSON-RPC API of Bitcoin Core's wallet is also
available, but documenting its usage is out of scope for these documents.

View file

@ -1,999 +0,0 @@
# RPC API Specification
Version: 2.0.1
=======
**Note:** This document assumes the reader is familiar with gRPC concepts.
Refer to the [gRPC Concepts documentation](http://www.grpc.io/docs/guides/concepts.html)
for any unfamiliar terms.
**Note:** The naming style used for autogenerated identifiers may differ
depending on the language being used. This document follows the naming style
used by Google in their Protocol Buffers and gRPC documentation as well as this
project's `.proto` files. That is, CamelCase is used for services, methods, and
messages, lower_snake_case for message fields, and SCREAMING_SNAKE_CASE for
enums.
**Note:** The entierty of the RPC API is currently considered unstable and may
change anytime. Stability will be gradually added based on correctness,
perceived usefulness and ease-of-use over alternatives, and user feedback.
This document is the authoritative source on the RPC API's definitions and
semantics. Any divergence from this document is an implementation error. API
fixes and additions require a version increase according to the rules of
[Semantic Versioning 2.0.0](http://semver.org/).
Only optional proto3 message fields are used (the `required` keyword is never
used in the `.proto` file). If a message field must be set to something other
than the default value, or any other values are invalid, the error must occur in
the application's message handling. This prevents accidentally introducing
parsing errors if a previously optional field is missing or a new required field
is added.
Functionality is grouped into gRPC services. Depending on what functions are
currently callable, different services will be running. As an example, the
server may be running without a loaded wallet, in which case the Wallet service
is not running and the Loader service must be used to create a new or load an
existing wallet.
- [`VersionService`](#versionservice)
- [`LoaderService`](#loaderservice)
- [`WalletService`](#walletservice)
## `VersionService`
The `VersionService` service provides the caller with versioning information
regarding the RPC server. It has no dependencies and is always running.
**Methods:**
- [`Version`](#version)
### Methods
#### `Version`
The `Version` method returns the RPC server version. Versioning follows the
rules of Semantic Versioning (SemVer) 2.0.0.
**Request:** `VersionRequest`
**Response:** `VersionResponse`
- `string version_string`: The version encoded as a string.
- `uint32 major`: The SemVer major version number.
- `uint32 minor`: The SemVer minor version number.
- `uint32 patch`: The SemVer patch version number.
- `string prerelease`: The SemVer pre-release version identifier, if any.
- `string build_metadata`: Extra SemVer build metadata, if any.
**Expected errors:** None
**Stability:** Stable
## `LoaderService`
The `LoaderService` service provides the caller with functions related to the
management of the wallet and its connection to the Bitcoin network. It has no
dependencies and is always running.
**Methods:**
- [`WalletExists`](#walletexists)
- [`CreateWallet`](#createwallet)
- [`OpenWallet`](#openwallet)
- [`CloseWallet`](#closewallet)
- [`StartConsensusRpc`](#startconsensusrpc)
**Shared messages:**
- [`BlockDetails`](#blockdetails)
- [`TransactionDetails`](#transactiondetails)
### Methods
#### `WalletExists`
The `WalletExists` method returns whether a file at the wallet database's file
path exists. Clients that must load wallets with this service are expected to
call this RPC to query whether `OpenWallet` should be used to open an existing
wallet, or `CreateWallet` to create a new wallet.
**Request:** `WalletExistsRequest`
**Response:** `WalletExistsResponse`
- `bool exists`: Whether the wallet file exists.
**Expected errors:** None
**Stability:** Unstable
___
#### `CreateWallet`
The `CreateWallet` method is used to create a wallet that is protected by two
levels of encryption: the public passphrase (for data that is made public on the
blockchain) and the private passphrase (for private keys). Since the seed is
not saved in the wallet database and clients should make their users backup the
seed, it needs to be passed as part of the request.
After creating a wallet, the `WalletService` service begins running.
**Request:** `CreateWalletRequest`
- `bytes public_passphrase`: The passphrase used for the outer wallet
encryption. This passphrase protects data that is made public on the
blockchain. If this passphrase has zero length, an insecure default is used
instead.
- `bytes private_passphrase`: The passphrase used for the inner wallet
encryption. This is the passphrase used for data that must always remain
private, such as private keys. The length of this field must not be zero.
- `bytes seed`: The BIP0032 seed used to derive all wallet keys. The length of
this field must be between 16 and 64 bytes, inclusive.
**Response:** `CreateWalletReponse`
**Expected errors:**
- `FailedPrecondition`: The wallet is currently open.
- `AlreadyExists`: A file already exists at the wallet database file path.
- `InvalidArgument`: A private passphrase was not included in the request, or
the seed is of incorrect length.
**Stability:** Unstable: There needs to be a way to recover all keys and
transactions of a wallet being recovered by its seed. It is unclear whether
it should be part of this method or a `WalletService` method.
___
#### `OpenWallet`
The `OpenWallet` method is used to open an existing wallet database. If the
wallet is protected by a public passphrase, it can not be successfully opened if
the public passphrase parameter is missing or incorrect.
After opening a wallet, the `WalletService` service begins running.
**Request:** `OpenWalletRequest`
- `bytes public_passphrase`: The passphrase used for the outer wallet
encryption. This passphrase protects data that is made public on the
blockchain. If this passphrase has zero length, an insecure default is used
instead.
**Response:** `OpenWalletResponse`
**Expected errors:**
- `FailedPrecondition`: The wallet is currently open.
- `NotFound`: The wallet database file does not exist.
- `InvalidArgument`: The public encryption passphrase was missing or incorrect.
**Stability:** Unstable
___
#### `CloseWallet`
The `CloseWallet` method is used to cleanly stop all wallet operations on a
loaded wallet and close the database. After closing, the `WalletService`
service will remain running but any operations that require the database will be
unusable.
**Request:** `CloseWalletRequest`
**Response:** `CloseWalletResponse`
**Expected errors:**
- `FailedPrecondition`: The wallet is not currently open.
**Stability:** Unstable: It would be preferable to stop the `WalletService`
after closing, but there does not appear to be any way to do so currently. It
may also be a good idea to limit under what conditions a wallet can be closed,
such as only closing wallets loaded by `LoaderService` and/or using a secret
to authenticate the operation.
___
#### `StartConsensusRpc`
The `StartConsensusRpc` method is used to provide clients the ability to dynamically
start the RPC client. This RPC client is used for wallet syncing and
publishing transactions to the Bitcoin network.
**Request:** `StartConsensusRpcRequest`
- `string network_address`: The host/IP and optional port of the RPC server to
connect to. IP addresses may be IPv4 or IPv6. If the port is missing, a
default port is chosen corresponding to the default RPC port of the
active Bitcoin network.
- `string username`: The RPC username required to authenticate to the RPC
server.
- `bytes password`: The RPC password required to authenticate to the RPC server.
- `bytes certificate`: The consensus RPC server's TLS certificate. If this
field has zero length and the network address describes a loopback connection
(`localhost`, `127.0.0.1`, or `::1`) TLS will be disabled.
**Response:** `StartConsensusRpcResponse`
**Expected errors:**
- `FailedPrecondition`: A consensus RPC client is already active.
- `InvalidArgument`: The network address is ill-formatted or does not contain a
valid IP address.
- `NotFound`: The consensus RPC server is unreachable. This condition may not
return `Unavailable` as that refers to `LoaderService` itself being
unavailable.
- `InvalidArgument`: The username, password, or certificate are invalid. This
condition may not be return `Unauthenticated` as that refers to the client not
having the credentials to call this method.
**Stability:** Unstable: It is unknown if the consensus RPC client will remain
used after the project gains SPV support.
## `WalletService`
The WalletService service provides RPCs for the wallet itself. The service
depends on a loaded wallet and does not run when the wallet has not been created
or opened yet.
The service provides the following methods:
- [RPC API Specification](#rpc-api-specification)
- [Version: 2.0.1](#version-201)
- [`VersionService`](#versionservice)
- [Methods](#methods)
- [`Version`](#version)
- [`LoaderService`](#loaderservice)
- [Methods](#methods-1)
- [`WalletExists`](#walletexists)
- [`CreateWallet`](#createwallet)
- [`OpenWallet`](#openwallet)
- [`CloseWallet`](#closewallet)
- [`StartConsensusRpc`](#startconsensusrpc)
- [`WalletService`](#walletservice)
- [`Ping`](#ping)
- [`Network`](#network)
- [`AccountNumber`](#accountnumber)
- [`Accounts`](#accounts)
- [`Balance`](#balance)
- [`GetTransactions`](#gettransactions)
- [`ChangePassphrase`](#changepassphrase)
- [`RenameAccount`](#renameaccount)
- [`NextAccount`](#nextaccount)
- [`NextAddress`](#nextaddress)
- [`ImportPrivateKey`](#importprivatekey)
- [`FundTransaction`](#fundtransaction)
- [`SignTransaction`](#signtransaction)
- [`PublishTransaction`](#publishtransaction)
- [`TransactionNotifications`](#transactionnotifications)
- [`SpentnessNotifications`](#spentnessnotifications)
- [`AccountNotifications`](#accountnotifications)
- [Shared messages](#shared-messages)
- [`BlockDetails`](#blockdetails)
- [`TransactionDetails`](#transactiondetails)
#### `Ping`
The `Ping` method checks whether the service is active.
**Request:** `PingRequest`
**Response:** `PingResponse`
**Expected errors:** None
**Stability:** Unstable: This may be moved to another service as it does not
depend on the wallet.
___
#### `Network`
The `Network` method returns the network identifier constant describing the
server's active network.
**Request:** `NetworkRequest`
**Response:** `NetworkResponse`
- `uint32 active_network`: The network identifier.
**Expected errors:** None
**Stability:** Unstable: This may be moved to another service as it does not
depend on the wallet.
___
#### `AccountNumber`
The `AccountNumber` method looks up a BIP0044 account number by an account's
unique name.
**Request:** `AccountNumberRequest`
- `string account_name`: The name of the account being queried.
**Response:** `AccountNumberResponse`
- `uint32 account_number`: The BIP0044 account number.
**Expected errors:**
- `Aborted`: The wallet database is closed.
- `NotFound`: No accounts exist by the name in the request.
**Stability:** Unstable
___
#### `Accounts`
The `Accounts` method returns the current properties of all accounts managed in
the wallet.
**Request:** `AccountsRequest`
**Response:** `AccountsResponse`
- `repeated Account accounts`: Account properties grouped into `Account` nested
message types, one per account, ordered by increasing account numbers.
**Nested message:** `Account`
- `uint32 account_number`: The BIP0044 account number.
- `string account_name`: The name of the account.
- `int64 total_balance`: The total (zero-conf and immature) balance, counted
in Satoshis.
- `uint32 external_key_count`: The number of derived keys in the external
key chain.
- `uint32 internal_key_count`: The number of derived keys in the internal
key chain.
- `uint32 imported_key_count`: The number of imported keys.
- `bytes current_block_hash`: The hash of the block wallet is considered to
be synced with.
- `int32 current_block_height`: The height of the block wallet is considered
to be synced with.
**Expected errors:**
- `Aborted`: The wallet database is closed.
**Stability:** Unstable
___
#### `Balance`
The `Balance` method queries the wallet for an account's balance. Balances are
returned as combination of total, spendable (by consensus and request policy),
and unspendable immature coinbase balances.
**Request:** `BalanceRequest`
- `uint32 account_number`: The account number to query.
- `int32 required_confirmations`: The number of confirmations required before an
unspent transaction output's value is included in the spendable balance. This
may not be negative.
**Response:** `BalanceResponse`
- `int64 total`: The total (zero-conf and immature) balance, counted in
Satoshis.
- `int64 spendable`: The spendable balance, given some number of required
confirmations, counted in Satoshis. This equals the total balance when the
required number of confirmations is zero and there are no immature coinbase
outputs.
- `int64 immature_reward`: The total value of all immature coinbase outputs,
counted in Satoshis.
**Expected errors:**
- `InvalidArgument`: The required number of confirmations is negative.
- `Aborted`: The wallet database is closed.
- `NotFound`: The account does not exist.
**Stability:** Unstable: It may prove useful to modify this RPC to query
multiple accounts together.
___
#### `GetTransactions`
The `GetTransactions` method queries the wallet for relevant transactions. The
query set may be specified using a block range, inclusive, with the heights or
hashes of the minimum and maximum block. Transaction results are grouped
grouped by the block they are mined in, or grouped together with other unmined
transactions.
**Request:** `GetTransactionsRequest`
- `bytes starting_block_hash`: The block hash of the block to begin including
transactions from. If this field is set to the default, the
`starting_block_height` field is used instead. If changed, the byte array
must have length 32 and `starting_block_height` must be zero.
- `sint32 starting_block_height`: The block height to begin including
transactions from. If this field is non-zero, `starting_block_hash` must be
set to its default value to avoid ambiguity. If positive, the field is
interpreted as a block height. If negative, the height is subtracted from the
block wallet considers itself in sync with.
- `bytes ending_block_hash`: The block hash of the last block to include
transactions from. If this default is set to the default, the
`ending_block_height` field is used instead. If changed, the byte array must
have length 32 and `ending_block_height` must be zero.
- `int32 ending_block_height`: The block height of the last block to include
transactions from. If non-zero, the `ending_block_hash` field must be set to
its default value to avoid ambiguity. If both this field and
`ending_block_hash` are set to their default values, no upper block limit is
used and transactions through the best block and all unmined transactions are
included.
**Response:** `GetTransactionsResponse`
- `repeated BlockDetails mined_transactions`: All mined transactions, organized
by blocks in the order they appear in the blockchain.
The `BlockDetails` message is used by other methods and is documented
[here](#blockdetails).
- `repeated TransactionDetails unmined_transactions`: All unmined transactions.
The ordering is unspecified.
The `TransactionDetails` message is used by other methods and is documented
[here](#transactiondetails).
**Expected errors:**
- `InvalidArgument`: A non-default block hash field did not have the correct length.
- `Aborted`: The wallet database is closed.
- `NotFound`: A block, specified by its height or hash, is unknown to the
wallet.
**Stability:** Unstable
- There is currently no way to get only unmined transactions due to the way
the block range is specified.
- It would be useful to ignore the block range and return some minimum number of
the most recent transaction, but it is unclear if that should be added to this
method's request object, or to make a new method.
- A specified ordering (such as dependency order) for all returned unmined
transactions would be useful.
___
#### `ChangePassphrase`
The `ChangePassphrase` method requests a change to either the public (outer) or
private (inner) encryption passphrases.
**Request:** `ChangePassphraseRequest`
- `Key key`: The key being changed.
**Nested enum:** `Key`
- `PRIVATE`: The request specifies to change the private (inner) encryption
passphrase.
- `PUBLIC`: The request specifies to change the public (outer) encryption
passphrase.
- `bytes old_passphrase`: The current passphrase for the encryption key. This
is the value being modified. If the public passphrase is being modified and
this value is the default value, an insecure default is used instead.
- `bytes new_passphrase`: The replacement passphrase. This field may only have
zero length if the public passphrase is being changed, in which case an
insecure default will be used instead.
**Response:** `ChangePassphraseResponse`
**Expected errors:**
- `InvalidArgument`: A zero length passphrase was specified when changing the
private passphrase, or the old passphrase was incorrect.
- `Aborted`: The wallet database is closed.
**Stability:** Unstable
___
#### `RenameAccount`
The `RenameAccount` method requests a change to an account's name property.
**Request:** `RenameAccountRequest`
- `uint32 account_number`: The number of the account being modified.
- `string new_name`: The new name for the account.
**Response:** `RenameAccountResponse`
**Expected errors:**
- `Aborted`: The wallet database is closed.
- `InvalidArgument`: The new account name is a reserved name.
- `NotFound`: The account does not exist.
- `AlreadyExists`: An account by the same name already exists.
**Stability:** Unstable: There should be a way to specify a starting block or
time to begin the rescan at. Additionally, since the client is expected to be
able to do asynchronous RPC, it may be useful for the response to block on the
rescan finishing before returning.
___
#### `NextAccount`
The `NextAccount` method generates the next BIP0044 account for the wallet.
**Request:** `NextAccountRequest`
- `bytes passphrase`: The private passphrase required to derive the next
account's key.
- `string account_name`: The name to give the new account.
**Response:** `NextAccountResponse`
- `uint32 account_number`: The number of the newly-created account.
**Expected errors:**
- `Aborted`: The wallet database is closed.
- `InvalidArgument`: The private passphrase is incorrect.
- `InvalidArgument`: The new account name is a reserved name.
- `AlreadyExists`: An account by the same name already exists.
**Stability:** Unstable
___
#### `NextAddress`
The `NextAddress` method generates the next deterministic address for the
wallet.
**Request:** `NextAddressRequest`
- `uint32 account`: The number of the account to derive the next address for.
- `Kind kind`: The type of address to generate.
**Nested enum:** `Kind`
- `BIP0044_EXTERNAL`: The request specifies to generate the next address for
the account's BIP0044 external key chain.
- `BIP0044_INTERNAL`: The request specifies to generate the next address for
the account's BIP0044 internal key chain.
**Response:** `NextAddressResponse`
- `string address`: The payment address string.
**Expected errors:**
- `Aborted`: The wallet database is closed.
- `NotFound`: The account does not exist.
**Stability:** Unstable
___
#### `ImportPrivateKey`
The `ImportPrivateKey` method imports a private key in Wallet Import Format
(WIF) encoding to a wallet account. A rescan may optionally be started to
search for transactions involving the private key's associated payment address.
**Request:** `ImportPrivateKeyRequest`
- `bytes passphrase`: The wallet's private passphrase.
- `uint32 account`: The account number to associate the imported key with.
- `string private_key_wif`: The private key, encoded using WIF.
- `bool rescan`: Whether or not to perform a blockchain rescan for the imported
key.
**Response:** `ImportPrivateKeyResponse`
**Expected errors:**
- `InvalidArgument`: The private key WIF string is not a valid WIF encoding.
- `Aborted`: The wallet database is closed.
- `InvalidArgument`: The private passphrase is incorrect.
- `NotFound`: The account does not exist.
**Stability:** Unstable
___
#### `FundTransaction`
The `FundTransaction` method queries the wallet for unspent transaction outputs
controlled by some account. Results may be refined by setting a target output
amount and limiting the required confirmations. The selection algorithm is
unspecified.
Output results are always created even if a minimum target output amount could
not be reached. This allows this method to behave similar to the `Balance`
method while also including the outputs that make up that balance.
Change outputs can optionally be returned by this method as well. This can
provide the caller with everything necessary to construct an unsigned
transaction paying to already known addresses or scripts.
**Request:** `FundTransactionRequest`
- `uint32 account`: Account number containing the keys controlling the output
set to query.
- `int64 target_amount`: If positive, the service may limit output results to
those that sum to at least this amount (counted in Satoshis). If zero, all
outputs not excluded by other arguments are returned. This may not be
negative.
- `int32 required_confirmations`: The minimum number of block confirmations
needed to consider including an output in the return set. This may not be
negative.
- `bool include_immature_coinbases`: If true, immature coinbase outputs will
also be included.
- `bool include_change_script`: If true, a change script is included in the
response object.
**Response:** `FundTransactionResponse`
- `repeated PreviousOutput selected_outputs`: The output set returned as a list
of `PreviousOutput` nested message objects.
**Nested message:** `PreviousOutput`
- `bytes transaction_hash`: The hash of the transaction this output originates
from.
- `uint32 output_index`: The output index of the transaction this output
originates from.
- `int64 amount`: The output value (counted in Satoshis) of the unspent
transaction output.
- `bytes pk_script`: The output script of the unspent transaction output.
- `int64 receive_time`: The earliest Unix time the wallet became aware of the
transaction containing this output.
- `bool from_coinbase`: Whether the output is a coinbase output.
- `int64 total_amount`: The sum of all returned output amounts. This may be
less than a positive target amount if there were not enough eligible outputs
available.
- `bytes change_pk_script`: A transaction output script used to pay the
remaining amount to a newly-generated change address for the account. This is
null if `include_change_script` was false or the target amount was not
exceeded.
**Expected errors:**
- `InvalidArgument`: The target amount is negative.
- `InvalidArgument`: The required confirmations is negative.
- `Aborted`: The wallet database is closed.
- `NotFound`: The account does not exist.
**Stability:** Unstable
___
#### `SignTransaction`
The `SignTransaction` method adds transaction input signatures to a serialized
transaction using a wallet private keys.
**Request:** `SignTransactionRequest`
- `bytes passphrase`: The wallet's private passphrase.
- `bytes serialized_transaction`: The transaction to add input signatures to.
- `repeated uint32 input_indexes`: The input indexes that signature scripts must
be created for. If there are no indexes, input scripts are created for every
input that is missing an input script.
**Response:** `SignTransactionResponse`
- `bytes transaction`: The serialized transaction with added input scripts.
- `repeated uint32 unsigned_input_indexes`: The indexes of every input that an
input script could not be created for.
**Expected errors:**
- `InvalidArgument`: The serialized transaction can not be decoded.
- `Aborted`: The wallet database is closed.
- `InvalidArgument`: The private passphrase is incorrect.
**Stability:** Unstable: It is unclear if the request should include an account,
and only secrets of that account are used when creating input scripts. It's
also missing options similar to Core's signrawtransaction, such as the sighash
flags and additional keys.
___
#### `PublishTransaction`
The `PublishTransaction` method publishes a signed, serialized transaction to
the Bitcoin network. If the transaction spends any of the wallet's unspent
outputs or creates a new output controlled by the wallet, it is saved by the
wallet and republished later if it or a double spend are not mined.
**Request:** `PublishTransactionRequest`
- `bytes signed_transaction`: The signed transaction to publish.
**Response:** `PublishTransactionResponse`
**Expected errors:**
- `InvalidArgument`: The serialized transaction can not be decoded or is missing
input scripts.
- `Aborted`: The wallet database is closed.
**Stability:** Unstable
___
#### `TransactionNotifications`
The `TransactionNotifications` method returns a stream of notifications
regarding changes to the blockchain and transactions relevant to the wallet.
**Request:** `TransactionNotificationsRequest`
**Response:** `stream TransactionNotificationsResponse`
- `repeated BlockDetails attached_blocks`: A list of blocks attached to the main
chain, sorted by increasing height. All newly mined transactions are included
in these messages, in the message corresponding to the block that contains
them. If this field has zero length, the notification is due to an unmined
transaction being added to the wallet.
The `BlockDetails` message is used by other methods and is documented
[here](#blockdetails).
- `repeated bytes detached_blocks`: The hashes of every block that was
reorganized out of the main chain. These are sorted by heights in decreasing
order (newest blocks first).
- `repeated TransactionDetails unmined_transactions`: All newly added unmined
transactions. When relevant transactions are reorganized out and not included
in (or double-spent by) the new chain, they are included here.
The `TransactionDetails` message is used by other methods and is documented
[here](#transactiondetails).
- `repeated bytes unmined_transaction_hashes`: The hashes of every
currently-unmined transaction. This differs from the `unmined_transactions`
field by including every unmined transaction, rather than those newly added to
the unmined set.
**Expected errors:**
- `Aborted`: The wallet database is closed.
**Stability:** Unstable: This method could use a better name.
___
#### `SpentnessNotifications`
The `SpentnessNotifications` method returns a stream of notifications regarding
the spending of unspent outputs and/or the discovery of new unspent outputs for
an account.
**Request:** `SpentnessNotificationsRequest`
- `uint32 account`: The account to create notifications for.
- `bool no_notify_unspent`: If true, do not send any notifications for
newly-discovered unspent outputs controlled by the account.
- `bool no_notify_spent`: If true, do not send any notifications for newly-spent
transactions controlled by the account.
**Response:** `stream SpentnessNotificationsResponse`
- `bytes transaction_hash`: The hash of the serialized transaction containing
the output being reported.
- `uint32 output_index`: The output index of the output being reported.
- `Spender spender`: If null, the output is a newly-discovered unspent output.
If not null, the message records the transaction input that spends the
previously-unspent output.
**Nested message:** `Spender`
- `bytes transaction_hash`: The hash of the serialized transaction that spends
the reported output.
- `uint32 input_index`: The index of the input that spends the reported
output.
**Expected errors:**
- `InvalidArgument`: The `no_notify_unspent` and `no_notify_spent` request
fields are both true.
- `Aborted`: The wallet database is closed.
**Stability:** Unstable
___
#### `AccountNotifications`
The `AccountNotifications` method returns a stream of notifications for account
property changes, such as name and key counts.
**Request:** `AccountNotificationsRequest`
**Response:** `stream AccountNotificationsResponse`
- `uint32 account_number`: The BIP0044 account being reported.
- `string account_name`: The current account name.
- `uint32 external_key_count`: The current number of BIP0032 external keys
derived for the account.
- `uint32 internal_key_count`: The current number of BIP0032 internal keys
derived for the account.
- `uint32 imported_key_count`: The current number of private keys imported into
the account.
**Expected errors:**
- `Aborted`: The wallet database is closed.
**Stability:** Unstable: This should probably share a message with the
`Accounts` method.
___
### Shared messages
The following messages are used by multiple methods. To avoid unnecessary
duplication, they are documented once here.
#### `BlockDetails`
The `BlockDetails` message is included in responses to report a block and the
wallet's relevant transactions contained therein.
- `bytes hash`: The hash of the block being reported.
- `int32 height`: The height of the block being reported.
- `int64 timestamp`: The Unix time included in the block header.
- `repeated TransactionDetails transactions`: All transactions relevant to the
wallet that are mined in this block. Transactions are sorted by their block
index in increasing order.
The `TransactionDetails` message is used by other methods and is documented
[here](#transactiondetails).
**Stability**: Unstable: This should probably include the block version.
___
#### `TransactionDetails`
The `TransactionDetails` message is included in responses to report transactions
relevant to the wallet. The message includes details such as which previous
wallet inputs are spent by this transaction, whether each output is controlled
by the wallet or not, the total fee (if calculable), and the earlist time the
transaction was seen.
- `bytes hash`: The hash of the serialized transaction.
- `bytes transaction`: The serialized transaction.
- `repeated Input debits`: Properties for every previously-unspent wallet output
spent by this transaction.
**Nested message:** `Input`
- `uint32 index`: The transaction input index of the input being reported.
- `uint32 previous_account`: The account that controlled the now-spent output.
- `int64 previous_amount`: The previous output value.
- `repeated Output credits`: Properties for every output controlled by the wallet.
**Nested message:** `Output`
- `uint32 index`: The transaction output index of the output being reported.
- `uint32 account`: The account number of the controlled output.
- `bool internal`: Whether the output pays to an address derived from the
account's internal key series. This often means the output is a change
output.
- `int64 fee`: The transaction fee, if calculable. The fee is only calculable
when every previous output spent by this transaction is also recorded by
wallet. Otherwise, this field is zero.
- `int64 timestamp`: The Unix time of the earliest time this transaction was
seen.
**Stability**: Unstable: Since the caller is expected to decode the serialized
transaction, and would have access to every output script, the output
properties could be changed to only include outputs controlled by the wallet.

View file

@ -1,439 +0,0 @@
# Client usage
Clients use RPC to interact with the wallet. A client may be implemented in any
language directly supported by [gRPC](http://www.grpc.io/), languages capable of
performing [FFI](https://en.wikipedia.org/wiki/Foreign_function_interface) with
these, and languages that share a common runtime (e.g. Scala, Kotlin, and Ceylon
for the JVM, F# for the CLR, etc.). Exact instructions differ slightly
depending on the language being used, but the general process is the same for
each. In short summary, to call RPC server methods, a client must:
1. Generate client bindings specific for the [wallet RPC server API](./api.md)
2. Import or include the gRPC dependency
3. (Optional) Wrap the client bindings with application-specific types
4. Open a gRPC channel using the wallet server's self-signed TLS certificate
The only exception to these steps is if the client is being written in Go. In
that case, the first step may be omitted by importing the bindings from
lbcwallet itself.
The rest of this document provides short examples of how to quickly get started
by implementing a basic client that fetches the balance of the default account
(account 0) from a testnet3 wallet listening on `localhost:19244` in several
different languages:
- [Client usage](#client-usage)
- [Go](#go)
- [C++](#c)
- [C#](#c-1)
- [Node.js](#nodejs)
- [Python](#python)
Unless otherwise stated under the language example, it is assumed that
gRPC is already already installed. The gRPC installation procedure
can vary greatly depending on the operating system being used and
whether a gRPC source install is required. Follow the [gRPC install
instructions](https://github.com/grpc/grpc/blob/master/INSTALL) if
gRPC is not already installed. A full gRPC install also includes
[Protocol Buffers](https://github.com/google/protobuf) (compiled with
support for the proto3 language version), which contains the protoc
tool and language plugins used to compile this project's `.proto`
files to language-specific bindings.
## Go
The native gRPC library (gRPC Core) is not required for Go clients (a
pure Go implementation is used instead) and no additional setup is
required to generate Go bindings.
```Go
package main
import (
"fmt"
"path/filepath"
pb "github.com/lbryio/lbcwallet/rpc/walletrpc"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
btcutil "github.com/lbryio/lbcutil"
)
var certificateFile = filepath.Join(btcutil.AppDataDir("lbcwallet", false), "rpc.cert")
func main() {
creds, err := credentials.NewClientTLSFromFile(certificateFile, "localhost")
if err != nil {
fmt.Println(err)
return
}
conn, err := grpc.Dial("localhost:19244", grpc.WithTransportCredentials(creds))
if err != nil {
fmt.Println(err)
return
}
defer conn.Close()
c := pb.NewWalletServiceClient(conn)
balanceRequest := &pb.BalanceRequest{
AccountNumber: 0,
RequiredConfirmations: 1,
}
balanceResponse, err := c.Balance(context.Background(), balanceRequest)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("Spendable balance: ", btcutil.Amount(balanceResponse.Spendable))
}
```
<a name="cpp"/>
## C++
**Note:** Protocol Buffers and gRPC require at least C++11. The example client
is written using C++14.
**Note:** The following instructions assume the client is being written on a
Unix-like platform (with instructions using the `sh` shell and Unix-isms in the
example source code) with a source gRPC install in `/usr/local`.
First, generate the C++ language bindings by compiling the `.proto`:
```bash
$ protoc -I/path/to/lbcwallet/rpc --cpp_out=. --grpc_out=. \
--plugin=protoc-gen-grpc=$(which grpc_cpp_plugin) \
/path/to/lbcwallet/rpc/api.proto
```
Once the `.proto` file has been compiled, the example client can be completed.
Note that the following code uses synchronous calls which will block the main
thread on all gRPC IO.
```C++
// example.cc
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <grpc++/grpc++.h>
#include "api.grpc.pb.h"
using namespace std::string_literals;
struct NoHomeDirectoryException : std::exception {
char const* what() const noexcept override {
return "Failed to lookup home directory";
}
};
auto read_file(std::string const& file_path) -> std::string {
std::ifstream in{file_path};
std::stringstream ss{};
ss << in.rdbuf();
return ss.str();
}
auto main() -> int {
// Before the gRPC native library (gRPC Core) is lazily loaded and
// initialized, an environment variable must be set so BoringSSL is
// configured to use ECDSA TLS certificates (required by lbcwallet).
setenv("GRPC_SSL_CIPHER_SUITES", "HIGH+ECDSA", 1);
// Note: This path is operating system-dependent. This can be created
// portably using boost::filesystem or the experimental filesystem class
// expected to ship in C++17.
auto wallet_tls_cert_file = []{
auto pw = getpwuid(getuid());
if (pw == nullptr || pw->pw_dir == nullptr) {
throw NoHomeDirectoryException{};
}
return pw->pw_dir + "/.lbcwallet/rpc.cert"s;
}();
grpc::SslCredentialsOptions cred_options{
.pem_root_certs = read_file(wallet_tls_cert_file),
};
auto creds = grpc::SslCredentials(cred_options);
auto channel = grpc::CreateChannel("localhost:19244", creds);
auto stub = walletrpc::WalletService::NewStub(channel);
grpc::ClientContext context{};
walletrpc::BalanceRequest request{};
request.set_account_number(0);
request.set_required_confirmations(1);
walletrpc::BalanceResponse response{};
auto status = stub->Balance(&context, request, &response);
if (!status.ok()) {
std::cout << status.error_message() << std::endl;
} else {
std::cout << "Spendable balance: " << response.spendable() << " Satoshis" << std::endl;
}
}
```
The example can then be built with the following commands:
```bash
$ c++ -std=c++14 -I/usr/local/include -pthread -c -o api.pb.o api.pb.cc
$ c++ -std=c++14 -I/usr/local/include -pthread -c -o api.grpc.pb.o api.grpc.pb.cc
$ c++ -std=c++14 -I/usr/local/include -pthread -c -o example.o example.cc
$ c++ *.o -L/usr/local/lib -lgrpc++ -lgrpc -lgpr -lprotobuf -lpthread -ldl -o example
```
<a name="csharp"/>
## C&#35;
The quickest way of generating client bindings in a Windows .NET environment is
by using the protoc binary included in the gRPC NuGet package. From the NuGet
package manager PowerShell console, this can be performed with:
```
PM> Install-Package Grpc
```
The protoc and C# plugin binaries can then be found in the packages directory.
For example, `.\packages\Google.Protobuf.x.x.x\tools\protoc.exe` and
`.\packages\Grpc.Tools.x.x.x\tools\grpc_csharp_plugin.exe`.
When writing a client on other platforms (e.g. Mono on OS X), or when doing a
full gRPC source install on Windows, protoc and the C# plugin must be installed
by other means. Consult the [official documentation](https://github.com/grpc/grpc/blob/master/src/csharp/README.md)
for these steps.
Once protoc and the C# plugin have been obtained, client bindings can be
generated. The following command generates the files `Api.cs` and `ApiGrpc.cs`
in the `Example` project directory using the `Walletrpc` namespace:
```PowerShell
PS> & protoc.exe -I \Path\To\lbcwallet\rpc --csharp_out=Example --grpc_out=Example `
--plugin=protoc-gen-grpc=\Path\To\grpc_csharp_plugin.exe `
\Path\To\lbcwallet\rpc\api.proto
```
Once references have been added to the project for the `Google.Protobuf` and
`Grpc.Core` assemblies, the example client can be implemented.
```C#
using Grpc.Core;
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Walletrpc;
namespace Example
{
static class Program
{
static void Main(string[] args)
{
ExampleAsync().Wait();
}
static async Task ExampleAsync()
{
// Before the gRPC native library (gRPC Core) is lazily loaded and initialized,
// an environment variable must be set so BoringSSL is configured to use ECDSA TLS
// certificates (required by lbcwallet).
Environment.SetEnvironmentVariable("GRPC_SSL_CIPHER_SUITES", "HIGH+ECDSA");
var walletAppData = Portability.LocalAppData(Environment.OSVersion.Platform, "lbcwallet");
var walletTlsCertFile = Path.Combine(walletAppData, "rpc.cert");
var cert = await FileUtils.ReadFileAsync(walletTlsCertFile);
var channel = new Channel("localhost:19244", new SslCredentials(cert));
try
{
var c = WalletService.NewClient(channel);
var balanceRequest = new BalanceRequest
{
AccountNumber = 0,
RequiredConfirmations = 1,
};
var balanceResponse = await c.BalanceAsync(balanceRequest);
Console.WriteLine($"Spendable balance: {balanceResponse.Spendable} Satoshis");
}
finally
{
await channel.ShutdownAsync();
}
}
}
static class FileUtils
{
public static async Task<string> ReadFileAsync(string filePath)
{
using (var r = new StreamReader(filePath, Encoding.UTF8))
{
return await r.ReadToEndAsync();
}
}
}
static class Portability
{
public static string LocalAppData(PlatformID platform, string processName)
{
if (processName == null)
throw new ArgumentNullException(nameof(processName));
if (processName.Length == 0)
throw new ArgumentException(nameof(processName) + " may not have zero length");
switch (platform)
{
case PlatformID.Win32NT:
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
ToUpper(processName));
case PlatformID.MacOSX:
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal),
"Library", "Application Support", ToUpper(processName));
case PlatformID.Unix:
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal),
ToDotLower(processName));
default:
throw new PlatformNotSupportedException($"PlatformID={platform}");
}
}
private static string ToUpper(string value)
{
var firstChar = value[0];
if (char.IsUpper(firstChar))
return value;
else
return char.ToUpper(firstChar) + value.Substring(1);
}
private static string ToDotLower(string value)
{
var firstChar = value[0];
return "." + char.ToLower(firstChar) + value.Substring(1);
}
}
}
```
## Node.js
First, install gRPC (either by building the latest source release, or
by installing a gRPC binary development package through your operating
system's package manager). This is required to install the npm module
as it wraps the native C library (gRPC Core) with C++ bindings.
Installing the [grpc module](https://www.npmjs.com/package/grpc) to
your project can then be done by executing:
```
npm install grpc
```
A Node.js client does not require generating JavaScript stub files for
the wallet's API from the `.proto`. Instead, a call to `grpc.load`
with the `.proto` file path dynamically loads the Protobuf descriptor
and generates bindings for each service. Either copy the `.proto` to
the client project directory, or reference the file from the
`lbcwallet` project directory.
```JavaScript
// Before the gRPC native library (gRPC Core) is lazily loaded and
// initialized, an environment variable must be set so BoringSSL is
// configured to use ECDSA TLS certificates (required by lbcwallet).
process.env['GRPC_SSL_CIPHER_SUITES'] = 'HIGH+ECDSA';
var fs = require('fs');
var path = require('path');
var os = require('os');
var grpc = require('grpc');
var protoDescriptor = grpc.load('./api.proto');
var walletrpc = protoDescriptor.walletrpc;
var certPath = path.join(process.env.HOME, '.lbcwallet', 'rpc.cert');
if (os.platform == 'win32') {
certPath = path.join(process.env.LOCALAPPDATA, 'lbcwallet', 'rpc.cert');
} else if (os.platform == 'darwin') {
certPath = path.join(process.env.HOME, 'Library', 'Application Support',
'lbcwallet', 'rpc.cert');
}
var cert = fs.readFileSync(certPath);
var creds = grpc.credentials.createSsl(cert);
var client = new walletrpc.WalletService('localhost:19244', creds);
var request = {
account_number: 0,
required_confirmations: 1
};
client.balance(request, function(err, response) {
if (err) {
console.error(err);
} else {
console.log('Spendable balance:', response.spendable, 'Satoshis');
}
});
```
## Python
**Note:** gRPC requires Python 2.7.
After installing gRPC Core and Python development headers, `pip`
should be used to install the `grpc` module and its dependencies.
Full instructions for this procedure can be found
[here](https://github.com/grpc/grpc/blob/master/src/python/README.md).
Generate Python stubs from the `.proto`:
```bash
$ protoc -I /path/to/lbryio/lbcwallet/rpc --python_out=. --grpc_out=. \
--plugin=protoc-gen-grpc=$(which grpc_python_plugin) \
/path/to/lbcwallet/rpc/api.proto
```
Implement the client:
```Python
import os
import platform
from grpc.beta import implementations
import api_pb2 as walletrpc
timeout = 1 # seconds
def main():
# Before the gRPC native library (gRPC Core) is lazily loaded and
# initialized, an environment variable must be set so BoringSSL is
# configured to use ECDSA TLS certificates (required by lbcwallet).
os.environ['GRPC_SSL_CIPHER_SUITES'] = 'HIGH+ECDSA'
cert_file_path = os.path.join(os.environ['HOME'], '.lbcwallet', 'rpc.cert')
if platform.system() == 'Windows':
cert_file_path = os.path.join(os.environ['LOCALAPPDATA'], "lbcwallet", "rpc.cert")
elif platform.system() == 'Darwin':
cert_file_path = os.path.join(os.environ['HOME'], 'Library', 'Application Support',
'lbcwallet', 'rpc.cert')
with open(cert_file_path, 'r') as f:
cert = f.read()
creds = implementations.ssl_client_credentials(cert, None, None)
channel = implementations.secure_channel('localhost', 19244, creds)
stub = walletrpc.beta_create_WalletService_stub(channel)
request = walletrpc.BalanceRequest(account_number = 0, required_confirmations = 1)
response = stub.Balance(request, timeout)
print 'Spendable balance: %d Satoshis' % response.spendable
if __name__ == '__main__':
main()
```

View file

@ -1,94 +0,0 @@
# Making API Changes
This document describes the process of how lbcwallet developers must make
changes to the RPC API and server. Due to the use of gRPC and Protocol Buffers
for the RPC implementation, changes to this API require extra dependencies and
steps before changes to the server can be implemented.
## Requirements
- The Protocol Buffer compiler `protoc` installed with support for the `proto3`
language
The `protoc` tool is part of the Protocol Buffers project. This can be
installed [from source](https://github.com/google/protobuf/blob/master/INSTALL.txt),
from an [official binary release](https://github.com/google/protobuf/releases),
or through an operating system's package manager.
- The gRPC `protoc` plugin for Go
This plugin is written in Go and can be installed using `go get`:
```
go get github.com/golang/protobuf/protoc-gen-go
```
- Knowledge of Protocol Buffers version 3 (proto3)
Note that a full installation of gRPC Core is not required, and only the
`protoc` compiler and Go plugins are necessary. This is due to the project
using a pure Go gRPC implementation instead of wrapping the C library from gRPC
Core.
## Step 1: Modify the `.proto`
Once the developer dependencies have been met, changes can be made to the API by
modifying the Protocol Buffers descriptor file [`api.proto`](../api.proto).
The API is versioned according to the rules of [Semantic Versioning
2.0](http://semver.org/). After any changes, bump the API version in the [API
specification](./api.md) and add the changes to the spec.
Unless backwards compatibility is broken (and the version is bumped to represent
this change), message fields must never be removed or changed, and new fields
must always be appended.
It is forbidden to use the `required` attribute on a message field as this can
cause errors during parsing when the new API is used by an older client.
Instead, the (implicit) optional attribute is used, and the server
implementation must return an appropriate error if the new request field is not
set to a valid value.
## Step 2: Compile the `.proto`
Once changes to the descriptor file and API specification have been made, the
`protoc` compiler must be used to compile the descriptor into a Go package.
This code contains interfaces (stubs) for each service (to be implemented by the
wallet) and message types used for each RPC. This same code can also be
imported by a Go client that then calls same interface methods to perform RPC
with the wallet.
By committing the autogenerated package to the project repo, the `proto3`
compiler and plugin are not needed by users installing the project by source or
by other developers not making changes to the RPC API.
A `sh` shell script is included to compile the Protocol Buffers descriptor. It
must be run from the `rpc` directory.
```bash
$ sh regen.sh
```
If a `sh` shell is unavailable, the command can be run manually instead (again
from the `rpc` directory).
```
protoc -I. api.proto --go_out=plugins=grpc:walletrpc
```
TODO(jrick): This step could be simplified and be more portable by putting the
commands in a Go source file and executing them with `go generate`. It should,
however, only be run when API changes are performed (not with `go generate
./...` in the project root) since not all developers are expected to have
`protoc` installed.
## Step 3: Implement the API change in the RPC server
After the Go code for the API has been regenated, the necessary changes can be
implemented in the [`rpcserver`](../rpcserver/) package.
## Additional Resources
- [Protocol Buffers Language Guide (proto3)](https://developers.google.com/protocol-buffers/docs/proto3)
- [Protocol Buffers Basics: Go](https://developers.google.com/protocol-buffers/docs/gotutorial)
- [gRPC Basics: Go](http://www.grpc.io/docs/tutorials/basic/go.html)

View file

@ -1,3 +0,0 @@
#!/bin/sh
protoc -I. api.proto --go_out=plugins=grpc:walletrpc

View file

@ -1,81 +0,0 @@
// Copyright (c) 2015-2016 The btcsuite developers
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package rpcserver
import (
"os"
"strings"
"google.golang.org/grpc/grpclog"
"github.com/btcsuite/btclog"
)
// UseLogger sets the logger to use for the gRPC server.
func UseLogger(l btclog.Logger) {
grpclog.SetLogger(logger{l}) // nolint:staticcheck
}
// logger uses a btclog.Logger to implement the grpclog.Logger interface.
type logger struct {
btclog.Logger
}
// stripGrpcPrefix removes the package prefix for all logs made to the grpc
// logger, since these are already included as the btclog subsystem name.
func stripGrpcPrefix(logstr string) string {
return strings.TrimPrefix(logstr, "grpc: ")
}
// stripGrpcPrefixArgs removes the package prefix from the first argument, if it
// exists and is a string, returning the same arg slice after reassigning the
// first arg.
func stripGrpcPrefixArgs(args ...interface{}) []interface{} {
if len(args) == 0 {
return args
}
firstArgStr, ok := args[0].(string)
if ok {
args[0] = stripGrpcPrefix(firstArgStr)
}
return args
}
func (l logger) Fatal(args ...interface{}) {
l.Critical(stripGrpcPrefixArgs(args)...)
os.Exit(1)
}
func (l logger) Fatalf(format string, args ...interface{}) {
l.Criticalf(stripGrpcPrefix(format), args...)
os.Exit(1)
}
func (l logger) Fatalln(args ...interface{}) {
l.Critical(stripGrpcPrefixArgs(args)...)
os.Exit(1)
}
func (l logger) Print(args ...interface{}) {
l.Info(stripGrpcPrefixArgs(args)...)
}
func (l logger) Printf(format string, args ...interface{}) {
l.Infof(stripGrpcPrefix(format), args...)
}
func (l logger) Println(args ...interface{}) {
l.Info(stripGrpcPrefixArgs(args)...)
}

View file

@ -1,810 +0,0 @@
// Copyright (c) 2015-2016 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
// Package rpcserver implements the RPC API and is used by the main package to
// start gRPC services.
//
// Full documentation of the API implemented by this package is maintained in a
// language-agnostic document:
//
// https://github.com/lbryio/lbcwallet/blob/master/rpc/documentation/api.md
//
// Any API changes must be performed according to the steps listed here:
//
// https://github.com/lbryio/lbcwallet/blob/master/rpc/documentation/serverchanges.md
package rpcserver
import (
"bytes"
"errors"
"sync"
"time"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/lbryio/lbcd/chaincfg/chainhash"
"github.com/lbryio/lbcd/rpcclient"
"github.com/lbryio/lbcd/txscript"
"github.com/lbryio/lbcd/wire"
btcutil "github.com/lbryio/lbcutil"
"github.com/lbryio/lbcutil/hdkeychain"
"github.com/lbryio/lbcwallet/chain"
"github.com/lbryio/lbcwallet/internal/cfgutil"
"github.com/lbryio/lbcwallet/internal/zero"
"github.com/lbryio/lbcwallet/netparams"
pb "github.com/lbryio/lbcwallet/rpc/walletrpc"
"github.com/lbryio/lbcwallet/waddrmgr"
"github.com/lbryio/lbcwallet/wallet"
"github.com/lbryio/lbcwallet/walletdb"
)
// Public API version constants
const (
semverString = "2.0.1"
semverMajor = 2
semverMinor = 0
semverPatch = 1
)
// translateError creates a new gRPC error with an appropriate error code for
// recognized errors.
//
// This function is by no means complete and should be expanded based on other
// known errors. Any RPC handler not returning a gRPC error (with grpc.Errorf)
// should return this result instead.
func translateError(err error) error {
code := errorCode(err)
return status.Errorf(code, "%s", err.Error())
}
func errorCode(err error) codes.Code {
// waddrmgr.IsError is convenient, but not granular enough when the
// underlying error has to be checked. Unwrap the underlying error
// if it exists.
if e, ok := err.(waddrmgr.ManagerError); ok {
// For these waddrmgr error codes, the underlying error isn't
// needed to determine the grpc error code.
switch e.ErrorCode {
case waddrmgr.ErrWrongPassphrase: // public and private
return codes.InvalidArgument
case waddrmgr.ErrAccountNotFound:
return codes.NotFound
case waddrmgr.ErrInvalidAccount: // reserved account
return codes.InvalidArgument
case waddrmgr.ErrDuplicateAccount:
return codes.AlreadyExists
}
err = e.Err
}
switch err {
case wallet.ErrLoaded:
return codes.FailedPrecondition
case walletdb.ErrDbNotOpen:
return codes.Aborted
case walletdb.ErrDbExists:
return codes.AlreadyExists
case walletdb.ErrDbDoesNotExist:
return codes.NotFound
case hdkeychain.ErrInvalidSeedLen:
return codes.InvalidArgument
default:
return codes.Unknown
}
}
// versionServer provides RPC clients with the ability to query the RPC server
// version.
type versionServer struct {
}
// walletServer provides wallet services for RPC clients.
type walletServer struct {
wallet *wallet.Wallet
}
// loaderServer provides RPC clients with the ability to load and close wallets,
// as well as establishing a RPC connection to a consensus server.
type loaderServer struct {
loader *wallet.Loader
activeNet *netparams.Params
rpcClient *chain.RPCClient
mu sync.Mutex
}
// StartVersionService creates an implementation of the VersionService and
// registers it with the gRPC server.
func StartVersionService(server *grpc.Server) {
pb.RegisterVersionServiceServer(server, &versionServer{})
}
func (*versionServer) Version(ctx context.Context, req *pb.VersionRequest) (*pb.VersionResponse, error) {
return &pb.VersionResponse{
VersionString: semverString,
Major: semverMajor,
Minor: semverMinor,
Patch: semverPatch,
}, nil
}
// StartWalletService creates an implementation of the WalletService and
// registers it with the gRPC server.
func StartWalletService(server *grpc.Server, wallet *wallet.Wallet) {
service := &walletServer{wallet}
pb.RegisterWalletServiceServer(server, service)
}
func (s *walletServer) Ping(ctx context.Context, req *pb.PingRequest) (*pb.PingResponse, error) {
return &pb.PingResponse{}, nil
}
func (s *walletServer) Network(ctx context.Context, req *pb.NetworkRequest) (
*pb.NetworkResponse, error) {
return &pb.NetworkResponse{ActiveNetwork: uint32(s.wallet.ChainParams().Net)}, nil
}
func (s *walletServer) AccountNumber(ctx context.Context, req *pb.AccountNumberRequest) (
*pb.AccountNumberResponse, error) {
accountNum, err := s.wallet.AccountNumber(waddrmgr.KeyScopeBIP0044, req.AccountName)
if err != nil {
return nil, translateError(err)
}
return &pb.AccountNumberResponse{AccountNumber: accountNum}, nil
}
func (s *walletServer) Accounts(ctx context.Context, req *pb.AccountsRequest) (
*pb.AccountsResponse, error) {
resp, err := s.wallet.Accounts(waddrmgr.KeyScopeBIP0044)
if err != nil {
return nil, translateError(err)
}
accounts := make([]*pb.AccountsResponse_Account, len(resp.Accounts))
for i := range resp.Accounts {
a := &resp.Accounts[i]
accounts[i] = &pb.AccountsResponse_Account{
AccountNumber: a.AccountNumber,
AccountName: a.AccountName,
TotalBalance: int64(a.TotalBalance),
ExternalKeyCount: a.ExternalKeyCount,
InternalKeyCount: a.InternalKeyCount,
ImportedKeyCount: a.ImportedKeyCount,
}
}
return &pb.AccountsResponse{
Accounts: accounts,
CurrentBlockHash: resp.CurrentBlockHash[:],
CurrentBlockHeight: resp.CurrentBlockHeight,
}, nil
}
func (s *walletServer) RenameAccount(ctx context.Context, req *pb.RenameAccountRequest) (
*pb.RenameAccountResponse, error) {
err := s.wallet.RenameAccount(waddrmgr.KeyScopeBIP0044, req.AccountNumber, req.NewName)
if err != nil {
return nil, translateError(err)
}
return &pb.RenameAccountResponse{}, nil
}
func (s *walletServer) NextAccount(ctx context.Context, req *pb.NextAccountRequest) (
*pb.NextAccountResponse, error) {
defer zero.Bytes(req.Passphrase)
if req.AccountName == "" {
return nil, status.Errorf(codes.InvalidArgument, "account name may not be empty")
}
lock := make(chan time.Time, 1)
defer func() {
lock <- time.Time{} // send matters, not the value
}()
err := s.wallet.Unlock(req.Passphrase, lock)
if err != nil {
return nil, translateError(err)
}
account, err := s.wallet.NextAccount(waddrmgr.KeyScopeBIP0044, req.AccountName)
if err != nil {
return nil, translateError(err)
}
return &pb.NextAccountResponse{AccountNumber: account}, nil
}
func (s *walletServer) NextAddress(ctx context.Context, req *pb.NextAddressRequest) (
*pb.NextAddressResponse, error) {
var (
addr btcutil.Address
err error
)
switch req.Kind {
case pb.NextAddressRequest_BIP0044_EXTERNAL:
addr, err = s.wallet.NewAddress(req.Account, waddrmgr.KeyScopeBIP0044)
case pb.NextAddressRequest_BIP0044_INTERNAL:
addr, err = s.wallet.NewChangeAddress(req.Account, waddrmgr.KeyScopeBIP0044)
default:
return nil, status.Errorf(codes.InvalidArgument, "kind=%v", req.Kind)
}
if err != nil {
return nil, translateError(err)
}
return &pb.NextAddressResponse{Address: addr.EncodeAddress()}, nil
}
func (s *walletServer) ImportPrivateKey(ctx context.Context, req *pb.ImportPrivateKeyRequest) (
*pb.ImportPrivateKeyResponse, error) {
defer zero.Bytes(req.Passphrase)
wif, err := btcutil.DecodeWIF(req.PrivateKeyWif)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument,
"Invalid WIF-encoded private key: %v", err)
}
lock := make(chan time.Time, 1)
defer func() {
lock <- time.Time{} // send matters, not the value
}()
err = s.wallet.Unlock(req.Passphrase, lock)
if err != nil {
return nil, translateError(err)
}
// At the moment, only the special-cased import account can be used to
// import keys.
if req.Account != waddrmgr.ImportedAddrAccount {
return nil, status.Errorf(codes.InvalidArgument,
"Only the imported account accepts private key imports")
}
_, err = s.wallet.ImportPrivateKey(waddrmgr.KeyScopeBIP0044, wif, nil, req.Rescan)
if err != nil {
return nil, translateError(err)
}
return &pb.ImportPrivateKeyResponse{}, nil
}
func (s *walletServer) Balance(ctx context.Context, req *pb.BalanceRequest) (
*pb.BalanceResponse, error) {
account := req.AccountNumber
reqConfs := req.RequiredConfirmations
bals, err := s.wallet.CalculateAccountBalances(account, reqConfs)
if err != nil {
return nil, translateError(err)
}
// TODO: Spendable currently includes multisig outputs that may not
// actually be spendable without additional keys.
resp := &pb.BalanceResponse{
Total: int64(bals.Total),
Spendable: int64(bals.Spendable),
ImmatureReward: int64(bals.ImmatureReward),
}
return resp, nil
}
func (s *walletServer) FundTransaction(ctx context.Context, req *pb.FundTransactionRequest) (
*pb.FundTransactionResponse, error) {
policy := wallet.OutputSelectionPolicy{
Account: req.Account,
RequiredConfirmations: req.RequiredConfirmations,
IncludeStakes: req.IncludeStakes,
}
unspentOutputs, err := s.wallet.UnspentOutputs(policy)
if err != nil {
return nil, translateError(err)
}
selectedOutputs := make([]*pb.FundTransactionResponse_PreviousOutput, 0, len(unspentOutputs))
var totalAmount btcutil.Amount
for _, output := range unspentOutputs {
selectedOutputs = append(selectedOutputs, &pb.FundTransactionResponse_PreviousOutput{
TransactionHash: output.OutPoint.Hash[:],
OutputIndex: output.OutPoint.Index,
Amount: output.Output.Value,
PkScript: output.Output.PkScript,
ReceiveTime: output.ReceiveTime.Unix(),
FromCoinbase: output.OutputKind == wallet.OutputKindCoinbase,
})
totalAmount += btcutil.Amount(output.Output.Value)
if req.TargetAmount != 0 && totalAmount > btcutil.Amount(req.TargetAmount) {
break
}
}
var changeScript []byte
if req.IncludeChangeScript && totalAmount > btcutil.Amount(req.TargetAmount) {
changeAddr, err := s.wallet.NewChangeAddress(req.Account, waddrmgr.KeyScopeBIP0044)
if err != nil {
return nil, translateError(err)
}
changeScript, err = txscript.PayToAddrScript(changeAddr)
if err != nil {
return nil, translateError(err)
}
}
return &pb.FundTransactionResponse{
SelectedOutputs: selectedOutputs,
TotalAmount: int64(totalAmount),
ChangePkScript: changeScript,
}, nil
}
func marshalGetTransactionsResult(wresp *wallet.GetTransactionsResult) (
*pb.GetTransactionsResponse, error) {
resp := &pb.GetTransactionsResponse{
MinedTransactions: marshalBlocks(wresp.MinedTransactions),
UnminedTransactions: marshalTransactionDetails(wresp.UnminedTransactions),
}
return resp, nil
}
// BUGS:
// - MinimumRecentTransactions is ignored.
// - Wrong error codes when a block height or hash is not recognized
func (s *walletServer) GetTransactions(ctx context.Context, req *pb.GetTransactionsRequest) (
resp *pb.GetTransactionsResponse, err error) {
var startBlock, endBlock *wallet.BlockIdentifier
if req.StartingBlockHash != nil && req.StartingBlockHeight != 0 { // nolint:gocritic
return nil, errors.New(
"starting block hash and height may not be specified simultaneously")
} else if req.StartingBlockHash != nil {
startBlockHash, err := chainhash.NewHash(req.StartingBlockHash)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "%s", err.Error())
}
startBlock = wallet.NewBlockIdentifierFromHash(startBlockHash)
} else if req.StartingBlockHeight != 0 {
startBlock = wallet.NewBlockIdentifierFromHeight(req.StartingBlockHeight)
}
if req.EndingBlockHash != nil && req.EndingBlockHeight != 0 { // nolint:gocritic
return nil, status.Errorf(codes.InvalidArgument,
"ending block hash and height may not be specified simultaneously")
} else if req.EndingBlockHash != nil {
endBlockHash, err := chainhash.NewHash(req.EndingBlockHash)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "%s", err.Error())
}
endBlock = wallet.NewBlockIdentifierFromHash(endBlockHash)
} else if req.EndingBlockHeight != 0 {
endBlock = wallet.NewBlockIdentifierFromHeight(req.EndingBlockHeight)
}
var minRecentTxs int
if req.MinimumRecentTransactions != 0 {
if endBlock != nil {
return nil, status.Errorf(codes.InvalidArgument,
"ending block and minimum number of recent transactions "+
"may not be specified simultaneously")
}
minRecentTxs = int(req.MinimumRecentTransactions)
if minRecentTxs < 0 {
return nil, status.Errorf(codes.InvalidArgument,
"minimum number of recent transactions may not be negative")
}
}
_ = minRecentTxs
gtr, err := s.wallet.GetTransactions(startBlock, endBlock, "", ctx.Done())
if err != nil {
return nil, translateError(err)
}
return marshalGetTransactionsResult(gtr)
}
func (s *walletServer) ChangePassphrase(ctx context.Context, req *pb.ChangePassphraseRequest) (
*pb.ChangePassphraseResponse, error) {
defer func() {
zero.Bytes(req.OldPassphrase)
zero.Bytes(req.NewPassphrase)
}()
var err error
switch req.Key {
case pb.ChangePassphraseRequest_PRIVATE:
err = s.wallet.ChangePrivatePassphrase(req.OldPassphrase, req.NewPassphrase)
case pb.ChangePassphraseRequest_PUBLIC:
err = s.wallet.ChangePublicPassphrase(req.OldPassphrase, req.NewPassphrase)
default:
return nil, status.Errorf(codes.InvalidArgument, "Unknown key type (%d)", req.Key)
}
if err != nil {
return nil, translateError(err)
}
return &pb.ChangePassphraseResponse{}, nil
}
// BUGS:
// - InputIndexes request field is ignored.
func (s *walletServer) SignTransaction(ctx context.Context, req *pb.SignTransactionRequest) (
*pb.SignTransactionResponse, error) {
defer zero.Bytes(req.Passphrase)
var tx wire.MsgTx
err := tx.Deserialize(bytes.NewReader(req.SerializedTransaction))
if err != nil {
return nil, status.Errorf(codes.InvalidArgument,
"Bytes do not represent a valid raw transaction: %v", err)
}
lock := make(chan time.Time, 1)
defer func() {
lock <- time.Time{} // send matters, not the value
}()
err = s.wallet.Unlock(req.Passphrase, lock)
if err != nil {
return nil, translateError(err)
}
invalidSigs, err := s.wallet.SignTransaction(&tx, txscript.SigHashAll, nil, nil, nil)
if err != nil {
return nil, translateError(err)
}
invalidInputIndexes := make([]uint32, len(invalidSigs))
for i, e := range invalidSigs {
invalidInputIndexes[i] = e.InputIndex
}
var serializedTransaction bytes.Buffer
serializedTransaction.Grow(tx.SerializeSize())
err = tx.Serialize(&serializedTransaction)
if err != nil {
return nil, translateError(err)
}
resp := &pb.SignTransactionResponse{
Transaction: serializedTransaction.Bytes(),
UnsignedInputIndexes: invalidInputIndexes,
}
return resp, nil
}
// BUGS:
// - The transaction is not inspected to be relevant before publishing using
// sendrawtransaction, so connection errors to could result in the tx
// never being added to the wallet database.
// - Once the above bug is fixed, wallet will require a way to purge invalid
// transactions from the database when they are rejected by the network, other
// than double spending them.
func (s *walletServer) PublishTransaction(ctx context.Context, req *pb.PublishTransactionRequest) (
*pb.PublishTransactionResponse, error) {
var msgTx wire.MsgTx
err := msgTx.Deserialize(bytes.NewReader(req.SignedTransaction))
if err != nil {
return nil, status.Errorf(codes.InvalidArgument,
"Bytes do not represent a valid raw transaction: %v", err)
}
err = s.wallet.PublishTransaction(&msgTx, "")
if err != nil {
return nil, translateError(err)
}
return &pb.PublishTransactionResponse{}, nil
}
func marshalTransactionInputs(v []wallet.TransactionSummaryInput) []*pb.TransactionDetails_Input {
inputs := make([]*pb.TransactionDetails_Input, len(v))
for i := range v {
input := &v[i]
inputs[i] = &pb.TransactionDetails_Input{
Index: input.Index,
PreviousAccount: input.PreviousAccount,
PreviousAmount: int64(input.PreviousAmount),
}
}
return inputs
}
func marshalTransactionOutputs(v []wallet.TransactionSummaryOutput) []*pb.TransactionDetails_Output {
outputs := make([]*pb.TransactionDetails_Output, len(v))
for i := range v {
output := &v[i]
outputs[i] = &pb.TransactionDetails_Output{
Index: output.Index,
Account: output.Account,
Internal: output.Internal,
}
}
return outputs
}
func marshalTransactionDetails(v []wallet.TransactionSummary) []*pb.TransactionDetails {
txs := make([]*pb.TransactionDetails, len(v))
for i := range v {
tx := &v[i]
txs[i] = &pb.TransactionDetails{
Hash: tx.Hash[:],
Transaction: tx.Transaction,
Debits: marshalTransactionInputs(tx.MyInputs),
Credits: marshalTransactionOutputs(tx.MyOutputs),
Fee: int64(tx.Fee),
Timestamp: tx.Timestamp,
}
}
return txs
}
func marshalBlocks(v []wallet.Block) []*pb.BlockDetails {
blocks := make([]*pb.BlockDetails, len(v))
for i := range v {
block := &v[i]
blocks[i] = &pb.BlockDetails{
Hash: block.Hash[:],
Height: block.Height,
Timestamp: block.Timestamp,
Transactions: marshalTransactionDetails(block.Transactions),
}
}
return blocks
}
func marshalHashes(v []*chainhash.Hash) [][]byte {
hashes := make([][]byte, len(v))
for i, hash := range v {
hashes[i] = hash[:]
}
return hashes
}
func (s *walletServer) TransactionNotifications(req *pb.TransactionNotificationsRequest,
svr pb.WalletService_TransactionNotificationsServer) error {
n := s.wallet.NtfnServer.TransactionNotifications()
defer n.Done()
ctxDone := svr.Context().Done()
for {
select {
case v := <-n.C:
resp := pb.TransactionNotificationsResponse{
AttachedBlocks: marshalBlocks(v.AttachedBlocks),
DetachedBlocks: marshalHashes(v.DetachedBlocks),
UnminedTransactions: marshalTransactionDetails(v.UnminedTransactions),
UnminedTransactionHashes: marshalHashes(v.UnminedTransactionHashes),
}
err := svr.Send(&resp)
if err != nil {
return translateError(err)
}
case <-ctxDone:
return nil
}
}
}
func (s *walletServer) SpentnessNotifications(req *pb.SpentnessNotificationsRequest,
svr pb.WalletService_SpentnessNotificationsServer) error {
if req.NoNotifyUnspent && req.NoNotifySpent {
return status.Errorf(codes.InvalidArgument,
"no_notify_unspent and no_notify_spent may not both be true")
}
n := s.wallet.NtfnServer.AccountSpentnessNotifications(req.Account)
defer n.Done()
ctxDone := svr.Context().Done()
for {
select {
case v := <-n.C:
spenderHash, spenderIndex, spent := v.Spender()
if (spent && req.NoNotifySpent) || (!spent && req.NoNotifyUnspent) {
continue
}
index := v.Index()
resp := pb.SpentnessNotificationsResponse{
TransactionHash: v.Hash()[:],
OutputIndex: index,
}
if spent {
resp.Spender = &pb.SpentnessNotificationsResponse_Spender{
TransactionHash: spenderHash[:],
InputIndex: spenderIndex,
}
}
err := svr.Send(&resp)
if err != nil {
return translateError(err)
}
case <-ctxDone:
return nil
}
}
}
func (s *walletServer) AccountNotifications(req *pb.AccountNotificationsRequest,
svr pb.WalletService_AccountNotificationsServer) error {
n := s.wallet.NtfnServer.AccountNotifications()
defer n.Done()
ctxDone := svr.Context().Done()
for {
select {
case v := <-n.C:
resp := pb.AccountNotificationsResponse{
AccountNumber: v.AccountNumber,
AccountName: v.AccountName,
ExternalKeyCount: v.ExternalKeyCount,
InternalKeyCount: v.InternalKeyCount,
ImportedKeyCount: v.ImportedKeyCount,
}
err := svr.Send(&resp)
if err != nil {
return translateError(err)
}
case <-ctxDone:
return nil
}
}
}
// StartWalletLoaderService creates an implementation of the WalletLoaderService
// and registers it with the gRPC server.
func StartWalletLoaderService(server *grpc.Server, loader *wallet.Loader,
activeNet *netparams.Params) {
service := &loaderServer{loader: loader, activeNet: activeNet}
pb.RegisterWalletLoaderServiceServer(server, service)
}
func (s *loaderServer) CreateWallet(ctx context.Context, req *pb.CreateWalletRequest) (
*pb.CreateWalletResponse, error) {
defer func() {
zero.Bytes(req.PrivatePassphrase)
zero.Bytes(req.Seed)
}()
// Use an insecure public passphrase when the request's is empty.
pubPassphrase := req.PublicPassphrase
if len(pubPassphrase) == 0 {
pubPassphrase = []byte(wallet.InsecurePubPassphrase)
}
wallet, err := s.loader.CreateNewWallet(
pubPassphrase, req.PrivatePassphrase, req.Seed, time.Now(),
)
if err != nil {
return nil, translateError(err)
}
s.mu.Lock()
if s.rpcClient != nil {
wallet.SynchronizeRPC(s.rpcClient)
}
s.mu.Unlock()
return &pb.CreateWalletResponse{}, nil
}
func (s *loaderServer) OpenWallet(ctx context.Context, req *pb.OpenWalletRequest) (
*pb.OpenWalletResponse, error) {
// Use an insecure public passphrase when the request's is empty.
pubPassphrase := req.PublicPassphrase
if len(pubPassphrase) == 0 {
pubPassphrase = []byte(wallet.InsecurePubPassphrase)
}
wallet, err := s.loader.OpenExistingWallet(pubPassphrase, false)
if err != nil {
return nil, translateError(err)
}
s.mu.Lock()
if s.rpcClient != nil {
wallet.SynchronizeRPC(s.rpcClient)
}
s.mu.Unlock()
return &pb.OpenWalletResponse{}, nil
}
func (s *loaderServer) WalletExists(ctx context.Context, req *pb.WalletExistsRequest) (
*pb.WalletExistsResponse, error) {
exists, err := s.loader.WalletExists()
if err != nil {
return nil, translateError(err)
}
return &pb.WalletExistsResponse{Exists: exists}, nil
}
func (s *loaderServer) CloseWallet(ctx context.Context, req *pb.CloseWalletRequest) (
*pb.CloseWalletResponse, error) {
err := s.loader.UnloadWallet()
if err == wallet.ErrNotLoaded {
return nil, status.Errorf(codes.FailedPrecondition, "wallet is not loaded")
}
if err != nil {
return nil, translateError(err)
}
return &pb.CloseWalletResponse{}, nil
}
func (s *loaderServer) StartConsensusRpc(ctx context.Context, // nolint:golint
req *pb.StartConsensusRpcRequest) (*pb.StartConsensusRpcResponse,
error) {
defer zero.Bytes(req.Password)
defer s.mu.Unlock()
s.mu.Lock()
if s.rpcClient != nil {
return nil, status.Errorf(codes.FailedPrecondition, "RPC client already created")
}
networkAddress, err := cfgutil.NormalizeAddress(req.NetworkAddress,
s.activeNet.RPCClientPort)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument,
"Network address is ill-formed: %v", err)
}
// Error if the wallet is already syncing with the network.
wallet, walletLoaded := s.loader.LoadedWallet()
if walletLoaded && wallet.SynchronizingToNetwork() {
return nil, status.Errorf(codes.FailedPrecondition,
"wallet is loaded and already synchronizing")
}
rpcClient, err := chain.NewRPCClient(s.activeNet.Params, networkAddress, req.Username,
string(req.Password), req.Certificate, len(req.Certificate) == 0, req.SkipVerify, 1)
if err != nil {
return nil, translateError(err)
}
err = rpcClient.Start()
if err != nil {
if err == rpcclient.ErrInvalidAuth {
return nil, status.Errorf(codes.InvalidArgument,
"Invalid RPC credentials: %v", err)
}
return nil, status.Errorf(codes.NotFound,
"Connection to RPC server failed: %v", err)
}
s.rpcClient = rpcClient
if walletLoaded {
wallet.SynchronizeRPC(rpcClient)
}
return &pb.StartConsensusRpcResponse{}, nil
}

File diff suppressed because it is too large Load diff

View file

@ -18,10 +18,7 @@ import (
btcutil "github.com/lbryio/lbcutil" btcutil "github.com/lbryio/lbcutil"
"github.com/lbryio/lbcwallet/rpc/legacyrpc" "github.com/lbryio/lbcwallet/rpc/legacyrpc"
"github.com/lbryio/lbcwallet/rpc/rpcserver"
"github.com/lbryio/lbcwallet/wallet" "github.com/lbryio/lbcwallet/wallet"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
) )
// openRPCKeyPair creates or loads the RPC TLS keypair specified by the // openRPCKeyPair creates or loads the RPC TLS keypair specified by the
@ -102,9 +99,8 @@ func generateRPCKeyPair(writeKey bool) (tls.Certificate, error) {
return keyPair, nil return keyPair, nil
} }
func startRPCServers(walletLoader *wallet.Loader) (*grpc.Server, *legacyrpc.Server, error) { func startRPCServers(walletLoader *wallet.Loader) (*legacyrpc.Server, error) {
var ( var (
server *grpc.Server
legacyServer *legacyrpc.Server legacyServer *legacyrpc.Server
legacyListen = net.Listen legacyListen = net.Listen
keyPair tls.Certificate keyPair tls.Certificate
@ -115,7 +111,7 @@ func startRPCServers(walletLoader *wallet.Loader) (*grpc.Server, *legacyrpc.Serv
} else { } else {
keyPair, err = openRPCKeyPair() keyPair, err = openRPCKeyPair()
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
// Change the standard net.Listen function to the tls one. // Change the standard net.Listen function to the tls one.
@ -128,27 +124,6 @@ func startRPCServers(walletLoader *wallet.Loader) (*grpc.Server, *legacyrpc.Serv
return tls.Listen(net, laddr, tlsConfig) return tls.Listen(net, laddr, tlsConfig)
} }
if len(cfg.ExperimentalRPCListeners) != 0 {
listeners := makeListeners(cfg.ExperimentalRPCListeners, net.Listen)
if len(listeners) == 0 {
err := errors.New("failed to create listeners for RPC server")
return nil, nil, err
}
creds := credentials.NewServerTLSFromCert(&keyPair)
server = grpc.NewServer(grpc.Creds(creds))
rpcserver.StartVersionService(server)
rpcserver.StartWalletLoaderService(server, walletLoader, activeNet)
for _, lis := range listeners {
lis := lis
go func() {
log.Infof("Experimental RPC server listening on %s",
lis.Addr())
err := server.Serve(lis)
log.Tracef("Finished serving expimental RPC: %v",
err)
}()
}
}
} }
if cfg.Username == "" || cfg.Password == "" { if cfg.Username == "" || cfg.Password == "" {
@ -157,7 +132,7 @@ func startRPCServers(walletLoader *wallet.Loader) (*grpc.Server, *legacyrpc.Serv
listeners := makeListeners(cfg.LegacyRPCListeners, legacyListen) listeners := makeListeners(cfg.LegacyRPCListeners, legacyListen)
if len(listeners) == 0 { if len(listeners) == 0 {
err := errors.New("failed to create listeners for legacy RPC server") err := errors.New("failed to create listeners for legacy RPC server")
return nil, nil, err return nil, err
} }
opts := legacyrpc.Options{ opts := legacyrpc.Options{
Username: cfg.Username, Username: cfg.Username,
@ -169,11 +144,11 @@ func startRPCServers(walletLoader *wallet.Loader) (*grpc.Server, *legacyrpc.Serv
} }
// Error when neither the GRPC nor legacy RPC servers can be started. // Error when neither the GRPC nor legacy RPC servers can be started.
if server == nil && legacyServer == nil { if legacyServer == nil {
return nil, nil, errors.New("no suitable RPC services can be started") return nil, errors.New("no suitable RPC services can be started")
} }
return server, legacyServer, nil return legacyServer, nil
} }
type listenFunc func(net string, laddr string) (net.Listener, error) type listenFunc func(net string, laddr string) (net.Listener, error)
@ -244,11 +219,6 @@ func makeListeners(normalizedListenAddrs []string, listen listenFunc) []net.List
// with a wallet to enable remote wallet access. For the GRPC server, this // with a wallet to enable remote wallet access. For the GRPC server, this
// registers the WalletService service, and for the legacy JSON-RPC server it // registers the WalletService service, and for the legacy JSON-RPC server it
// enables methods that require a loaded wallet. // enables methods that require a loaded wallet.
func startWalletRPCServices(wallet *wallet.Wallet, server *grpc.Server, legacyServer *legacyrpc.Server) { func startWalletRPCServices(wallet *wallet.Wallet, legacyServer *legacyrpc.Server) {
if server != nil { legacyServer.RegisterWallet(wallet)
rpcserver.StartWalletService(server, wallet)
}
if legacyServer != nil {
legacyServer.RegisterWallet(wallet)
}
} }