lbcwallet/rpc/rpcserver/server.go

810 lines
23 KiB
Go

// 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, 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
}