consolidate: remove gRPC APIs #35
12 changed files with 10 additions and 5457 deletions
13
lbcwallet.go
13
lbcwallet.go
|
@ -75,7 +75,7 @@ func walletMain() error {
|
|||
// Create and start HTTP server to serve wallet client connections.
|
||||
// This will be updated with the wallet and chain server RPC client
|
||||
// created below after each is created.
|
||||
rpcs, legacyRPCServer, err := startRPCServers(loader)
|
||||
legacyRPCServer, err := startRPCServers(loader)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to create RPC servers: %v", err)
|
||||
return err
|
||||
|
@ -88,7 +88,7 @@ func walletMain() error {
|
|||
}
|
||||
|
||||
loader.RunAfterLoad(func(w *wallet.Wallet) {
|
||||
startWalletRPCServices(w, rpcs, legacyRPCServer)
|
||||
startWalletRPCServices(w, legacyRPCServer)
|
||||
})
|
||||
|
||||
if !cfg.NoInitialLoad {
|
||||
|
@ -110,15 +110,6 @@ func walletMain() error {
|
|||
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 {
|
||||
addInterruptHandler(func() {
|
||||
log.Warn("Stopping legacy RPC server...")
|
||||
|
|
2
log.go
2
log.go
|
@ -15,7 +15,6 @@ import (
|
|||
"github.com/lbryio/lbcd/rpcclient"
|
||||
"github.com/lbryio/lbcwallet/chain"
|
||||
"github.com/lbryio/lbcwallet/rpc/legacyrpc"
|
||||
"github.com/lbryio/lbcwallet/rpc/rpcserver"
|
||||
"github.com/lbryio/lbcwallet/wallet"
|
||||
"github.com/lbryio/lbcwallet/wtxmgr"
|
||||
)
|
||||
|
@ -67,7 +66,6 @@ func init() {
|
|||
wtxmgr.UseLogger(txmgrLog)
|
||||
chain.UseLogger(chainLog)
|
||||
rpcclient.UseLogger(chainLog)
|
||||
rpcserver.UseLogger(grpcLog)
|
||||
legacyrpc.UseLogger(legacyRPCLog)
|
||||
}
|
||||
|
||||
|
|
319
rpc/api.proto
319
rpc/api.proto
|
@ -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 {}
|
|
@ -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.
|
|
@ -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.
|
|
@ -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#
|
||||
|
||||
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()
|
||||
```
|
|
@ -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)
|
|
@ -1,3 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
protoc -I. api.proto --go_out=plugins=grpc:walletrpc
|
|
@ -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)...)
|
||||
}
|
|
@ -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
44
rpcserver.go
44
rpcserver.go
|
@ -18,10 +18,7 @@ import (
|
|||
|
||||
btcutil "github.com/lbryio/lbcutil"
|
||||
"github.com/lbryio/lbcwallet/rpc/legacyrpc"
|
||||
"github.com/lbryio/lbcwallet/rpc/rpcserver"
|
||||
"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
|
||||
|
@ -102,9 +99,8 @@ func generateRPCKeyPair(writeKey bool) (tls.Certificate, error) {
|
|||
return keyPair, nil
|
||||
}
|
||||
|
||||
func startRPCServers(walletLoader *wallet.Loader) (*grpc.Server, *legacyrpc.Server, error) {
|
||||
func startRPCServers(walletLoader *wallet.Loader) (*legacyrpc.Server, error) {
|
||||
var (
|
||||
server *grpc.Server
|
||||
legacyServer *legacyrpc.Server
|
||||
legacyListen = net.Listen
|
||||
keyPair tls.Certificate
|
||||
|
@ -115,7 +111,7 @@ func startRPCServers(walletLoader *wallet.Loader) (*grpc.Server, *legacyrpc.Serv
|
|||
} else {
|
||||
keyPair, err = openRPCKeyPair()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
|
@ -157,7 +132,7 @@ func startRPCServers(walletLoader *wallet.Loader) (*grpc.Server, *legacyrpc.Serv
|
|||
listeners := makeListeners(cfg.LegacyRPCListeners, legacyListen)
|
||||
if len(listeners) == 0 {
|
||||
err := errors.New("failed to create listeners for legacy RPC server")
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
opts := legacyrpc.Options{
|
||||
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.
|
||||
if server == nil && legacyServer == nil {
|
||||
return nil, nil, errors.New("no suitable RPC services can be started")
|
||||
if legacyServer == nil {
|
||||
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)
|
||||
|
@ -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
|
||||
// registers the WalletService service, and for the legacy JSON-RPC server it
|
||||
// enables methods that require a loaded wallet.
|
||||
func startWalletRPCServices(wallet *wallet.Wallet, server *grpc.Server, legacyServer *legacyrpc.Server) {
|
||||
if server != nil {
|
||||
rpcserver.StartWalletService(server, wallet)
|
||||
}
|
||||
if legacyServer != nil {
|
||||
func startWalletRPCServices(wallet *wallet.Wallet, legacyServer *legacyrpc.Server) {
|
||||
legacyServer.RegisterWallet(wallet)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue