846 lines
24 KiB
Go
846 lines
24 KiB
Go
|
// 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.
|
||
|
|
||
|
// This package 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/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"
|
||
|
)
|
||
|
|
||
|
// 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
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
}
|
||
|
|
||
|
// StartWalletService creates a 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) {
|
||
|
|
||
|
addr, err := s.wallet.NewAddress(req.Account)
|
||
|
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
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
if req.TargetAmount != 0 && totalAmount < btcutil.Amount(req.TargetAmount) {
|
||
|
return nil, errors.New("insufficient output value to reach target")
|
||
|
}
|
||
|
|
||
|
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 := wire.NewShaHash(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 := wire.NewShaHash(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]
|
||
|
|
||
|
var addresses []string
|
||
|
if len(output.Addresses) != 0 {
|
||
|
addresses = make([]string, 0, len(output.Addresses))
|
||
|
for _, a := range output.Addresses {
|
||
|
addresses = append(addresses, a.EncodeAddress())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
outputs[i] = &pb.TransactionDetails_Output{
|
||
|
Mine: output.Mine,
|
||
|
Account: output.Account,
|
||
|
Internal: output.Internal,
|
||
|
Addresses: addresses,
|
||
|
}
|
||
|
}
|
||
|
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),
|
||
|
Outputs: 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 []*wire.ShaHash) [][]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 a 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) {
|
||
|
|
||
|
loadedWallet, ok := s.loader.LoadedWallet()
|
||
|
if !ok {
|
||
|
return nil, grpc.Errorf(codes.FailedPrecondition, "wallet is not loaded")
|
||
|
}
|
||
|
|
||
|
loadedWallet.Stop()
|
||
|
loadedWallet.WaitForShutdown()
|
||
|
|
||
|
return &pb.CloseWalletResponse{}, nil
|
||
|
}
|
||
|
|
||
|
func (s *loaderServer) StartBtcdRpc(ctx context.Context, req *pb.StartBtcdRpcRequest) (
|
||
|
*pb.StartBtcdRpcResponse, 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)
|
||
|
} else {
|
||
|
return nil, grpc.Errorf(codes.NotFound,
|
||
|
"Connection to RPC server failed: %v", err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
s.rpcClient = rpcClient
|
||
|
|
||
|
if walletLoaded {
|
||
|
wallet.SynchronizeRPC(rpcClient)
|
||
|
}
|
||
|
|
||
|
return &pb.StartBtcdRpcResponse{}, nil
|
||
|
}
|