consolidate: remove gRPC APIs
This commit is contained in:
parent
efb8e0b699
commit
2b0d245b1f
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.
|
// Create and start HTTP server to serve wallet client connections.
|
||||||
// This will be updated with the wallet and chain server RPC client
|
// This will be updated with the wallet and chain server RPC client
|
||||||
// created below after each is created.
|
// created below after each is created.
|
||||||
rpcs, legacyRPCServer, err := startRPCServers(loader)
|
legacyRPCServer, err := startRPCServers(loader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Unable to create RPC servers: %v", err)
|
log.Errorf("Unable to create RPC servers: %v", err)
|
||||||
return err
|
return err
|
||||||
|
@ -88,7 +88,7 @@ func walletMain() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
loader.RunAfterLoad(func(w *wallet.Wallet) {
|
loader.RunAfterLoad(func(w *wallet.Wallet) {
|
||||||
startWalletRPCServices(w, rpcs, legacyRPCServer)
|
startWalletRPCServices(w, legacyRPCServer)
|
||||||
})
|
})
|
||||||
|
|
||||||
if !cfg.NoInitialLoad {
|
if !cfg.NoInitialLoad {
|
||||||
|
@ -110,15 +110,6 @@ func walletMain() error {
|
||||||
log.Errorf("Failed to close wallet: %v", err)
|
log.Errorf("Failed to close wallet: %v", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if rpcs != nil {
|
|
||||||
addInterruptHandler(func() {
|
|
||||||
// TODO: Does this need to wait for the grpc server to
|
|
||||||
// finish up any requests?
|
|
||||||
log.Warn("Stopping RPC server...")
|
|
||||||
rpcs.Stop()
|
|
||||||
log.Info("RPC server shutdown")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if legacyRPCServer != nil {
|
if legacyRPCServer != nil {
|
||||||
addInterruptHandler(func() {
|
addInterruptHandler(func() {
|
||||||
log.Warn("Stopping legacy RPC server...")
|
log.Warn("Stopping legacy RPC server...")
|
||||||
|
|
2
log.go
2
log.go
|
@ -15,7 +15,6 @@ import (
|
||||||
"github.com/lbryio/lbcd/rpcclient"
|
"github.com/lbryio/lbcd/rpcclient"
|
||||||
"github.com/lbryio/lbcwallet/chain"
|
"github.com/lbryio/lbcwallet/chain"
|
||||||
"github.com/lbryio/lbcwallet/rpc/legacyrpc"
|
"github.com/lbryio/lbcwallet/rpc/legacyrpc"
|
||||||
"github.com/lbryio/lbcwallet/rpc/rpcserver"
|
|
||||||
"github.com/lbryio/lbcwallet/wallet"
|
"github.com/lbryio/lbcwallet/wallet"
|
||||||
"github.com/lbryio/lbcwallet/wtxmgr"
|
"github.com/lbryio/lbcwallet/wtxmgr"
|
||||||
)
|
)
|
||||||
|
@ -67,7 +66,6 @@ func init() {
|
||||||
wtxmgr.UseLogger(txmgrLog)
|
wtxmgr.UseLogger(txmgrLog)
|
||||||
chain.UseLogger(chainLog)
|
chain.UseLogger(chainLog)
|
||||||
rpcclient.UseLogger(chainLog)
|
rpcclient.UseLogger(chainLog)
|
||||||
rpcserver.UseLogger(grpcLog)
|
|
||||||
legacyrpc.UseLogger(legacyRPCLog)
|
legacyrpc.UseLogger(legacyRPCLog)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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
46
rpcserver.go
46
rpcserver.go
|
@ -18,10 +18,7 @@ import (
|
||||||
|
|
||||||
btcutil "github.com/lbryio/lbcutil"
|
btcutil "github.com/lbryio/lbcutil"
|
||||||
"github.com/lbryio/lbcwallet/rpc/legacyrpc"
|
"github.com/lbryio/lbcwallet/rpc/legacyrpc"
|
||||||
"github.com/lbryio/lbcwallet/rpc/rpcserver"
|
|
||||||
"github.com/lbryio/lbcwallet/wallet"
|
"github.com/lbryio/lbcwallet/wallet"
|
||||||
"google.golang.org/grpc"
|
|
||||||
"google.golang.org/grpc/credentials"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// openRPCKeyPair creates or loads the RPC TLS keypair specified by the
|
// openRPCKeyPair creates or loads the RPC TLS keypair specified by the
|
||||||
|
@ -102,9 +99,8 @@ func generateRPCKeyPair(writeKey bool) (tls.Certificate, error) {
|
||||||
return keyPair, nil
|
return keyPair, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func startRPCServers(walletLoader *wallet.Loader) (*grpc.Server, *legacyrpc.Server, error) {
|
func startRPCServers(walletLoader *wallet.Loader) (*legacyrpc.Server, error) {
|
||||||
var (
|
var (
|
||||||
server *grpc.Server
|
|
||||||
legacyServer *legacyrpc.Server
|
legacyServer *legacyrpc.Server
|
||||||
legacyListen = net.Listen
|
legacyListen = net.Listen
|
||||||
keyPair tls.Certificate
|
keyPair tls.Certificate
|
||||||
|
@ -115,7 +111,7 @@ func startRPCServers(walletLoader *wallet.Loader) (*grpc.Server, *legacyrpc.Serv
|
||||||
} else {
|
} else {
|
||||||
keyPair, err = openRPCKeyPair()
|
keyPair, err = openRPCKeyPair()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change the standard net.Listen function to the tls one.
|
// Change the standard net.Listen function to the tls one.
|
||||||
|
@ -128,27 +124,6 @@ func startRPCServers(walletLoader *wallet.Loader) (*grpc.Server, *legacyrpc.Serv
|
||||||
return tls.Listen(net, laddr, tlsConfig)
|
return tls.Listen(net, laddr, tlsConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cfg.ExperimentalRPCListeners) != 0 {
|
|
||||||
listeners := makeListeners(cfg.ExperimentalRPCListeners, net.Listen)
|
|
||||||
if len(listeners) == 0 {
|
|
||||||
err := errors.New("failed to create listeners for RPC server")
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
creds := credentials.NewServerTLSFromCert(&keyPair)
|
|
||||||
server = grpc.NewServer(grpc.Creds(creds))
|
|
||||||
rpcserver.StartVersionService(server)
|
|
||||||
rpcserver.StartWalletLoaderService(server, walletLoader, activeNet)
|
|
||||||
for _, lis := range listeners {
|
|
||||||
lis := lis
|
|
||||||
go func() {
|
|
||||||
log.Infof("Experimental RPC server listening on %s",
|
|
||||||
lis.Addr())
|
|
||||||
err := server.Serve(lis)
|
|
||||||
log.Tracef("Finished serving expimental RPC: %v",
|
|
||||||
err)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Username == "" || cfg.Password == "" {
|
if cfg.Username == "" || cfg.Password == "" {
|
||||||
|
@ -157,7 +132,7 @@ func startRPCServers(walletLoader *wallet.Loader) (*grpc.Server, *legacyrpc.Serv
|
||||||
listeners := makeListeners(cfg.LegacyRPCListeners, legacyListen)
|
listeners := makeListeners(cfg.LegacyRPCListeners, legacyListen)
|
||||||
if len(listeners) == 0 {
|
if len(listeners) == 0 {
|
||||||
err := errors.New("failed to create listeners for legacy RPC server")
|
err := errors.New("failed to create listeners for legacy RPC server")
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
opts := legacyrpc.Options{
|
opts := legacyrpc.Options{
|
||||||
Username: cfg.Username,
|
Username: cfg.Username,
|
||||||
|
@ -169,11 +144,11 @@ func startRPCServers(walletLoader *wallet.Loader) (*grpc.Server, *legacyrpc.Serv
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error when neither the GRPC nor legacy RPC servers can be started.
|
// Error when neither the GRPC nor legacy RPC servers can be started.
|
||||||
if server == nil && legacyServer == nil {
|
if legacyServer == nil {
|
||||||
return nil, nil, errors.New("no suitable RPC services can be started")
|
return nil, errors.New("no suitable RPC services can be started")
|
||||||
}
|
}
|
||||||
|
|
||||||
return server, legacyServer, nil
|
return legacyServer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type listenFunc func(net string, laddr string) (net.Listener, error)
|
type listenFunc func(net string, laddr string) (net.Listener, error)
|
||||||
|
@ -244,11 +219,6 @@ func makeListeners(normalizedListenAddrs []string, listen listenFunc) []net.List
|
||||||
// with a wallet to enable remote wallet access. For the GRPC server, this
|
// with a wallet to enable remote wallet access. For the GRPC server, this
|
||||||
// registers the WalletService service, and for the legacy JSON-RPC server it
|
// registers the WalletService service, and for the legacy JSON-RPC server it
|
||||||
// enables methods that require a loaded wallet.
|
// enables methods that require a loaded wallet.
|
||||||
func startWalletRPCServices(wallet *wallet.Wallet, server *grpc.Server, legacyServer *legacyrpc.Server) {
|
func startWalletRPCServices(wallet *wallet.Wallet, legacyServer *legacyrpc.Server) {
|
||||||
if server != nil {
|
legacyServer.RegisterWallet(wallet)
|
||||||
rpcserver.StartWalletService(server, wallet)
|
|
||||||
}
|
|
||||||
if legacyServer != nil {
|
|
||||||
legacyServer.RegisterWallet(wallet)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue