e92f94dcd1
This updates all code to make use of the new chainhash package since the old wire.ShaHash type and related functions have been removed in favor of the abstracted package. Also, while here, rename all variables that included sha in their name to include hash instead. Finally, update glide.lock to use the required version of btcd, btcutil, and btcrpcclient.
860 lines
24 KiB
Go
860 lines
24 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/btcsuite/btcwallet/blob/master/rpc/documentation/api.md
|
|
//
|
|
// Any API changes must be performed according to the steps listed here:
|
|
//
|
|
// https://github.com/btcsuite/btcwallet/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"
|
|
|
|
"github.com/btcsuite/btcd/blockchain"
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/btcsuite/btcd/txscript"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/btcsuite/btcrpcclient"
|
|
"github.com/btcsuite/btcutil"
|
|
"github.com/btcsuite/btcutil/hdkeychain"
|
|
"github.com/btcsuite/btcwallet/chain"
|
|
"github.com/btcsuite/btcwallet/internal/cfgutil"
|
|
"github.com/btcsuite/btcwallet/internal/zero"
|
|
"github.com/btcsuite/btcwallet/netparams"
|
|
pb "github.com/btcsuite/btcwallet/rpc/walletrpc"
|
|
"github.com/btcsuite/btcwallet/waddrmgr"
|
|
"github.com/btcsuite/btcwallet/wallet"
|
|
"github.com/btcsuite/btcwallet/walletdb"
|
|
)
|
|
|
|
// Public API version constants
|
|
const (
|
|
semverString = "2.0.1"
|
|
semverMajor = 2
|
|
semverMinor = 0
|
|
semverPatch = 1
|
|
)
|
|
|
|
// translateError creates a new gRPC error with an appropiate 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 grpc.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 btcd 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.Manager.LookupAccount(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()
|
|
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(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, grpc.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(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)
|
|
case pb.NextAddressRequest_BIP0044_INTERNAL:
|
|
addr, err = s.wallet.NewChangeAddress(req.Account)
|
|
default:
|
|
return nil, grpc.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, grpc.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, grpc.Errorf(codes.InvalidArgument,
|
|
"Only the imported account accepts private key imports")
|
|
}
|
|
|
|
_, err = s.wallet.ImportPrivateKey(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
|
|
}
|
|
|
|
// confirmed checks whether a transaction at height txHeight has met minconf
|
|
// confirmations for a blockchain at height curHeight.
|
|
func confirmed(minconf, txHeight, curHeight int32) bool {
|
|
return confirms(txHeight, curHeight) >= minconf
|
|
}
|
|
|
|
// confirms returns the number of confirmations for a transaction in a block at
|
|
// height txHeight (or -1 for an unconfirmed tx) given the chain height
|
|
// curHeight.
|
|
func confirms(txHeight, curHeight int32) int32 {
|
|
switch {
|
|
case txHeight == -1, txHeight > curHeight:
|
|
return 0
|
|
default:
|
|
return curHeight - txHeight + 1
|
|
}
|
|
}
|
|
|
|
func (s *walletServer) FundTransaction(ctx context.Context, req *pb.FundTransactionRequest) (
|
|
*pb.FundTransactionResponse, error) {
|
|
|
|
// TODO: A predicate function for selecting outputs should be created
|
|
// and passed to a database view of just a particular account's utxos to
|
|
// prevent reading every unspent transaction output from every account
|
|
// into memory at once.
|
|
|
|
syncBlock := s.wallet.Manager.SyncedTo()
|
|
|
|
outputs, err := s.wallet.TxStore.UnspentOutputs()
|
|
if err != nil {
|
|
return nil, translateError(err)
|
|
}
|
|
|
|
selectedOutputs := make([]*pb.FundTransactionResponse_PreviousOutput, 0, len(outputs))
|
|
var totalAmount btcutil.Amount
|
|
for i := range outputs {
|
|
output := &outputs[i]
|
|
|
|
if !confirmed(req.RequiredConfirmations, output.Height, syncBlock.Height) {
|
|
continue
|
|
}
|
|
if !req.IncludeImmatureCoinbases && output.FromCoinBase &&
|
|
!confirmed(blockchain.CoinbaseMaturity, output.Height, syncBlock.Height) {
|
|
continue
|
|
}
|
|
|
|
_, addrs, _, err := txscript.ExtractPkScriptAddrs(
|
|
output.PkScript, s.wallet.ChainParams())
|
|
if err != nil || len(addrs) == 0 {
|
|
// Cannot determine which account this belongs to
|
|
// without a valid address. Fix this by saving
|
|
// outputs per account (per-account wtxmgr).
|
|
continue
|
|
}
|
|
outputAcct, err := s.wallet.Manager.AddrAccount(addrs[0])
|
|
if err != nil {
|
|
return nil, translateError(err)
|
|
}
|
|
if outputAcct != req.Account {
|
|
continue
|
|
}
|
|
|
|
selectedOutputs = append(selectedOutputs, &pb.FundTransactionResponse_PreviousOutput{
|
|
TransactionHash: output.OutPoint.Hash[:],
|
|
OutputIndex: output.Index,
|
|
Amount: int64(output.Amount),
|
|
PkScript: output.PkScript,
|
|
ReceiveTime: output.Received.Unix(),
|
|
FromCoinbase: output.FromCoinBase,
|
|
})
|
|
totalAmount += output.Amount
|
|
|
|
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)
|
|
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 {
|
|
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, grpc.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 {
|
|
return nil, grpc.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, grpc.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, grpc.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, grpc.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)
|
|
}()
|
|
|
|
err := s.wallet.Manager.ChangePassphrase(req.OldPassphrase, req.NewPassphrase,
|
|
req.Key != pb.ChangePassphraseRequest_PUBLIC, &waddrmgr.DefaultScryptOptions)
|
|
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, grpc.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 btcd 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, grpc.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 marshalAccountBalances(v []wallet.AccountBalance) []*pb.AccountBalance {
|
|
balances := make([]*pb.AccountBalance, len(v))
|
|
for i := range v {
|
|
balance := &v[i]
|
|
balances[i] = &pb.AccountBalance{
|
|
Account: balance.Account,
|
|
TotalBalance: int64(balance.TotalBalance),
|
|
}
|
|
}
|
|
return balances
|
|
}
|
|
|
|
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 grpc.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)
|
|
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, grpc.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, req *pb.StartConsensusRpcRequest) (
|
|
*pb.StartConsensusRpcResponse, error) {
|
|
|
|
defer zero.Bytes(req.Password)
|
|
|
|
defer s.mu.Unlock()
|
|
s.mu.Lock()
|
|
|
|
if s.rpcClient != nil {
|
|
return nil, grpc.Errorf(codes.FailedPrecondition, "RPC client already created")
|
|
}
|
|
|
|
networkAddress, err := cfgutil.NormalizeAddress(req.NetworkAddress,
|
|
s.activeNet.RPCClientPort)
|
|
if err != nil {
|
|
return nil, grpc.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, grpc.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 == btcrpcclient.ErrInvalidAuth {
|
|
return nil, grpc.Errorf(codes.InvalidArgument,
|
|
"Invalid RPC credentials: %v", err)
|
|
}
|
|
return nil, grpc.Errorf(codes.NotFound,
|
|
"Connection to RPC server failed: %v", err)
|
|
}
|
|
|
|
s.rpcClient = rpcClient
|
|
|
|
if walletLoaded {
|
|
wallet.SynchronizeRPC(rpcClient)
|
|
}
|
|
|
|
return &pb.StartConsensusRpcResponse{}, nil
|
|
}
|