lbcwallet/rpc/legacyrpc/methods.go
Josh Rickmar d2e93f9427 Make walletpassphrase with timeout=0 never lock the wallet.
This broke when the Unlock API changed to replace the timeout in
seconds with a time.Time channel.
2016-02-15 11:35:28 -05:00

2004 lines
62 KiB
Go

/*
* Copyright (c) 2013-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 legacyrpc
import (
"bytes"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"sync"
"time"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcrpcclient"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/chain"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/wallet"
"github.com/btcsuite/btcwallet/wtxmgr"
)
const (
// maxEmptyAccounts is the number of accounts to scan even if they have no
// transaction history. This is a deviation from BIP044 to make account
// creation easier by allowing a limited number of empty accounts.
maxEmptyAccounts = 100
)
// 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
}
}
// requestHandler is a handler function to handle an unmarshaled and parsed
// request into a marshalable response. If the error is a *btcjson.RPCError
// or any of the above special error classes, the server will respond with
// the JSON-RPC appropiate error code. All other errors use the wallet
// catch-all error code, btcjson.ErrRPCWallet.
type requestHandler func(interface{}, *wallet.Wallet) (interface{}, error)
// requestHandlerChain is a requestHandler that also takes a parameter for
type requestHandlerChainRequired func(interface{}, *wallet.Wallet, *chain.RPCClient) (interface{}, error)
var rpcHandlers = map[string]struct {
handler requestHandler
handlerWithChain requestHandlerChainRequired
// Function variables cannot be compared against anything but nil, so
// use a boolean to record whether help generation is necessary. This
// is used by the tests to ensure that help can be generated for every
// implemented method.
//
// A single map and this bool is here is used rather than several maps
// for the unimplemented handlers so every method has exactly one
// handler function.
noHelp bool
}{
// Reference implementation wallet methods (implemented)
"addmultisigaddress": {handler: AddMultiSigAddress},
"createmultisig": {handler: CreateMultiSig},
"dumpprivkey": {handler: DumpPrivKey},
"getaccount": {handler: GetAccount},
"getaccountaddress": {handler: GetAccountAddress},
"getaddressesbyaccount": {handler: GetAddressesByAccount},
"getbalance": {handler: GetBalance},
"getbestblockhash": {handler: GetBestBlockHash},
"getblockcount": {handler: GetBlockCount},
"getinfo": {handlerWithChain: GetInfo},
"getnewaddress": {handler: GetNewAddress},
"getrawchangeaddress": {handler: GetRawChangeAddress},
"getreceivedbyaccount": {handler: GetReceivedByAccount},
"getreceivedbyaddress": {handler: GetReceivedByAddress},
"gettransaction": {handler: GetTransaction},
"help": {handler: HelpNoChainRPC, handlerWithChain: HelpWithChainRPC},
"importprivkey": {handler: ImportPrivKey},
"keypoolrefill": {handler: KeypoolRefill},
"listaccounts": {handler: ListAccounts},
"listlockunspent": {handler: ListLockUnspent},
"listreceivedbyaccount": {handler: ListReceivedByAccount},
"listreceivedbyaddress": {handler: ListReceivedByAddress},
"listsinceblock": {handlerWithChain: ListSinceBlock},
"listtransactions": {handler: ListTransactions},
"listunspent": {handler: ListUnspent},
"lockunspent": {handler: LockUnspent},
"sendfrom": {handlerWithChain: SendFrom},
"sendmany": {handler: SendMany},
"sendtoaddress": {handler: SendToAddress},
"settxfee": {handler: SetTxFee},
"signmessage": {handler: SignMessage},
"signrawtransaction": {handlerWithChain: SignRawTransaction},
"validateaddress": {handler: ValidateAddress},
"verifymessage": {handler: VerifyMessage},
"walletlock": {handler: WalletLock},
"walletpassphrase": {handler: WalletPassphrase},
"walletpassphrasechange": {handler: WalletPassphraseChange},
// Reference implementation methods (still unimplemented)
"backupwallet": {handler: Unimplemented, noHelp: true},
"dumpwallet": {handler: Unimplemented, noHelp: true},
"getwalletinfo": {handler: Unimplemented, noHelp: true},
"importwallet": {handler: Unimplemented, noHelp: true},
"listaddressgroupings": {handler: Unimplemented, noHelp: true},
// Reference methods which can't be implemented by btcwallet due to
// design decision differences
"encryptwallet": {handler: Unsupported, noHelp: true},
"move": {handler: Unsupported, noHelp: true},
"setaccount": {handler: Unsupported, noHelp: true},
// Extensions to the reference client JSON-RPC API
"createnewaccount": {handler: CreateNewAccount},
"exportwatchingwallet": {handler: ExportWatchingWallet},
"getbestblock": {handler: GetBestBlock},
// This was an extension but the reference implementation added it as
// well, but with a different API (no account parameter). It's listed
// here because it hasn't been update to use the reference
// implemenation's API.
"getunconfirmedbalance": {handler: GetUnconfirmedBalance},
"listaddresstransactions": {handler: ListAddressTransactions},
"listalltransactions": {handler: ListAllTransactions},
"renameaccount": {handler: RenameAccount},
"walletislocked": {handler: WalletIsLocked},
}
// Unimplemented handles an unimplemented RPC request with the
// appropiate error.
func Unimplemented(interface{}, *wallet.Wallet) (interface{}, error) {
return nil, &btcjson.RPCError{
Code: btcjson.ErrRPCUnimplemented,
Message: "Method unimplemented",
}
}
// Unsupported handles a standard bitcoind RPC request which is
// unsupported by btcwallet due to design differences.
func Unsupported(interface{}, *wallet.Wallet) (interface{}, error) {
return nil, &btcjson.RPCError{
Code: -1,
Message: "Request unsupported by btcwallet",
}
}
// lazyHandler is a closure over a requestHandler or passthrough request with
// the RPC server's wallet and chain server variables as part of the closure
// context.
type lazyHandler func() (interface{}, *btcjson.RPCError)
// lazyApplyHandler looks up the best request handler func for the method,
// returning a closure that will execute it with the (required) wallet and
// (optional) consensus RPC server. If no handlers are found and the
// chainClient is not nil, the returned handler performs RPC passthrough.
func lazyApplyHandler(request *btcjson.Request, w *wallet.Wallet, chainClient *chain.RPCClient) lazyHandler {
handlerData, ok := rpcHandlers[request.Method]
if ok && handlerData.handlerWithChain != nil && w != nil && chainClient != nil {
return func() (interface{}, *btcjson.RPCError) {
cmd, err := btcjson.UnmarshalCmd(request)
if err != nil {
return nil, btcjson.ErrRPCInvalidRequest
}
resp, err := handlerData.handlerWithChain(cmd, w, chainClient)
if err != nil {
return nil, jsonError(err)
}
return resp, nil
}
}
if ok && handlerData.handler != nil && w != nil {
return func() (interface{}, *btcjson.RPCError) {
cmd, err := btcjson.UnmarshalCmd(request)
if err != nil {
return nil, btcjson.ErrRPCInvalidRequest
}
resp, err := handlerData.handler(cmd, w)
if err != nil {
return nil, jsonError(err)
}
return resp, nil
}
}
// Fallback to RPC passthrough
return func() (interface{}, *btcjson.RPCError) {
if chainClient == nil {
return nil, &btcjson.RPCError{
Code: -1,
Message: "Chain RPC is inactive",
}
}
resp, err := chainClient.RawRequest(request.Method, request.Params)
if err != nil {
return nil, jsonError(err)
}
return &resp, nil
}
}
// makeResponse makes the JSON-RPC response struct for the result and error
// returned by a requestHandler. The returned response is not ready for
// marshaling and sending off to a client, but must be
func makeResponse(id, result interface{}, err error) btcjson.Response {
idPtr := idPointer(id)
if err != nil {
return btcjson.Response{
ID: idPtr,
Error: jsonError(err),
}
}
resultBytes, err := json.Marshal(result)
if err != nil {
return btcjson.Response{
ID: idPtr,
Error: &btcjson.RPCError{
Code: btcjson.ErrRPCInternal.Code,
Message: "Unexpected error marshalling result",
},
}
}
return btcjson.Response{
ID: idPtr,
Result: json.RawMessage(resultBytes),
}
}
// jsonError creates a JSON-RPC error from the Go error.
func jsonError(err error) *btcjson.RPCError {
if err == nil {
return nil
}
code := btcjson.ErrRPCWallet
switch e := err.(type) {
case btcjson.RPCError:
return &e
case *btcjson.RPCError:
return e
case DeserializationError:
code = btcjson.ErrRPCDeserialization
case InvalidParameterError:
code = btcjson.ErrRPCInvalidParameter
case ParseError:
code = btcjson.ErrRPCParse.Code
case waddrmgr.ManagerError:
switch e.ErrorCode {
case waddrmgr.ErrWrongPassphrase:
code = btcjson.ErrRPCWalletPassphraseIncorrect
}
}
return &btcjson.RPCError{
Code: code,
Message: err.Error(),
}
}
// makeMultiSigScript is a helper function to combine common logic for
// AddMultiSig and CreateMultiSig.
func makeMultiSigScript(w *wallet.Wallet, keys []string, nRequired int) ([]byte, error) {
keysesPrecious := make([]*btcutil.AddressPubKey, len(keys))
// The address list will made up either of addreseses (pubkey hash), for
// which we need to look up the keys in wallet, straight pubkeys, or a
// mixture of the two.
for i, a := range keys {
// try to parse as pubkey address
a, err := decodeAddress(a, w.ChainParams())
if err != nil {
return nil, err
}
switch addr := a.(type) {
case *btcutil.AddressPubKey:
keysesPrecious[i] = addr
case *btcutil.AddressPubKeyHash:
ainfo, err := w.Manager.Address(addr)
if err != nil {
return nil, err
}
apkinfo := ainfo.(waddrmgr.ManagedPubKeyAddress)
// This will be an addresspubkey
a, err := decodeAddress(apkinfo.ExportPubKey(),
w.ChainParams())
if err != nil {
return nil, err
}
apk := a.(*btcutil.AddressPubKey)
keysesPrecious[i] = apk
default:
return nil, err
}
}
return txscript.MultiSigScript(keysesPrecious, nRequired)
}
// AddMultiSigAddress handles an addmultisigaddress request by adding a
// multisig address to the given wallet.
func AddMultiSigAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.AddMultisigAddressCmd)
// If an account is specified, ensure that is the imported account.
if cmd.Account != nil && *cmd.Account != waddrmgr.ImportedAddrAccountName {
return nil, &ErrNotImportedAccount
}
script, err := makeMultiSigScript(w, cmd.Keys, cmd.NRequired)
if err != nil {
return nil, ParseError{err}
}
// TODO(oga) blockstamp current block?
bs := &waddrmgr.BlockStamp{
Hash: *w.ChainParams().GenesisHash,
Height: 0,
}
addr, err := w.Manager.ImportScript(script, bs)
if err != nil {
return nil, err
}
return addr.Address().EncodeAddress(), nil
}
// CreateMultiSig handles an createmultisig request by returning a
// multisig address for the given inputs.
func CreateMultiSig(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.CreateMultisigCmd)
script, err := makeMultiSigScript(w, cmd.Keys, cmd.NRequired)
if err != nil {
return nil, ParseError{err}
}
address, err := btcutil.NewAddressScriptHash(script, w.ChainParams())
if err != nil {
// above is a valid script, shouldn't happen.
return nil, err
}
return btcjson.CreateMultiSigResult{
Address: address.EncodeAddress(),
RedeemScript: hex.EncodeToString(script),
}, nil
}
// DumpPrivKey handles a dumpprivkey request with the private key
// for a single address, or an appropiate error if the wallet
// is locked.
func DumpPrivKey(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.DumpPrivKeyCmd)
addr, err := decodeAddress(cmd.Address, w.ChainParams())
if err != nil {
return nil, err
}
key, err := w.DumpWIFPrivateKey(addr)
if waddrmgr.IsError(err, waddrmgr.ErrLocked) {
// Address was found, but the private key isn't
// accessible.
return nil, &ErrWalletUnlockNeeded
}
return key, err
}
// DumpWallet handles a dumpwallet request by returning all private
// keys in a wallet, or an appropiate error if the wallet is locked.
// TODO: finish this to match bitcoind by writing the dump to a file.
func DumpWallet(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
keys, err := w.DumpPrivKeys()
if waddrmgr.IsError(err, waddrmgr.ErrLocked) {
return nil, &ErrWalletUnlockNeeded
}
return keys, err
}
// ExportWatchingWallet handles an exportwatchingwallet request by exporting the
// current wallet as a watching wallet (with no private keys), and returning
// base64-encoding of serialized account files.
func ExportWatchingWallet(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.ExportWatchingWalletCmd)
if cmd.Account != nil && *cmd.Account != "*" {
return nil, &btcjson.RPCError{
Code: btcjson.ErrRPCWallet,
Message: "Individual accounts can not be exported as watching-only",
}
}
return w.ExportWatchingWallet()
}
// GetAddressesByAccount handles a getaddressesbyaccount request by returning
// all addresses for an account, or an error if the requested account does
// not exist.
func GetAddressesByAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.GetAddressesByAccountCmd)
account, err := w.Manager.LookupAccount(cmd.Account)
if err != nil {
return nil, err
}
var addrStrs []string
err = w.Manager.ForEachAccountAddress(account,
func(maddr waddrmgr.ManagedAddress) error {
addrStrs = append(addrStrs, maddr.Address().EncodeAddress())
return nil
})
return addrStrs, err
}
// GetBalance handles a getbalance request by returning the balance for an
// account (wallet), or an error if the requested account does not
// exist.
func GetBalance(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.GetBalanceCmd)
var balance btcutil.Amount
var err error
accountName := "*"
if cmd.Account != nil {
accountName = *cmd.Account
}
if accountName == "*" {
balance, err = w.CalculateBalance(int32(*cmd.MinConf))
if err != nil {
return nil, err
}
} else {
var account uint32
account, err = w.Manager.LookupAccount(accountName)
if err != nil {
return nil, err
}
bals, err := w.CalculateAccountBalances(account, int32(*cmd.MinConf))
if err != nil {
return nil, err
}
balance = bals.Spendable
}
return balance.ToBTC(), nil
}
// GetBestBlock handles a getbestblock request by returning a JSON object
// with the height and hash of the most recently processed block.
func GetBestBlock(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
blk := w.Manager.SyncedTo()
result := &btcjson.GetBestBlockResult{
Hash: blk.Hash.String(),
Height: blk.Height,
}
return result, nil
}
// GetBestBlockHash handles a getbestblockhash request by returning the hash
// of the most recently processed block.
func GetBestBlockHash(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
blk := w.Manager.SyncedTo()
return blk.Hash.String(), nil
}
// GetBlockCount handles a getblockcount request by returning the chain height
// of the most recently processed block.
func GetBlockCount(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
blk := w.Manager.SyncedTo()
return blk.Height, nil
}
// GetInfo handles a getinfo request by returning the a structure containing
// information about the current state of btcwallet.
// exist.
func GetInfo(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCClient) (interface{}, error) {
// Call down to btcd for all of the information in this command known
// by them.
info, err := chainClient.GetInfo()
if err != nil {
return nil, err
}
bal, err := w.CalculateBalance(1)
if err != nil {
return nil, err
}
// TODO(davec): This should probably have a database version as opposed
// to using the manager version.
info.WalletVersion = int32(waddrmgr.LatestMgrVersion)
info.Balance = bal.ToBTC()
info.PaytxFee = w.FeeIncrement.ToBTC()
// We don't set the following since they don't make much sense in the
// wallet architecture:
// - unlocked_until
// - errors
return info, nil
}
func decodeAddress(s string, params *chaincfg.Params) (btcutil.Address, error) {
addr, err := btcutil.DecodeAddress(s, params)
if err != nil {
msg := fmt.Sprintf("Invalid address %q: decode failed with %#q", s, err)
return nil, &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidAddressOrKey,
Message: msg,
}
}
if !addr.IsForNet(params) {
msg := fmt.Sprintf("Invalid address %q: not intended for use on %s",
addr, params.Name)
return nil, &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidAddressOrKey,
Message: msg,
}
}
return addr, nil
}
// GetAccount handles a getaccount request by returning the account name
// associated with a single address.
func GetAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.GetAccountCmd)
addr, err := decodeAddress(cmd.Address, w.ChainParams())
if err != nil {
return nil, err
}
// Fetch the associated account
account, err := w.Manager.AddrAccount(addr)
if err != nil {
return nil, &ErrAddressNotInWallet
}
acctName, err := w.Manager.AccountName(account)
if err != nil {
return nil, &ErrAccountNameNotFound
}
return acctName, nil
}
// GetAccountAddress handles a getaccountaddress by returning the most
// recently-created chained address that has not yet been used (does not yet
// appear in the blockchain, or any tx that has arrived in the btcd mempool).
// If the most recently-requested address has been used, a new address (the
// next chained address in the keypool) is used. This can fail if the keypool
// runs out (and will return btcjson.ErrRPCWalletKeypoolRanOut if that happens).
func GetAccountAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.GetAccountAddressCmd)
account, err := w.Manager.LookupAccount(cmd.Account)
if err != nil {
return nil, err
}
addr, err := w.CurrentAddress(account)
if err != nil {
return nil, err
}
return addr.EncodeAddress(), err
}
// GetUnconfirmedBalance handles a getunconfirmedbalance extension request
// by returning the current unconfirmed balance of an account.
func GetUnconfirmedBalance(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.GetUnconfirmedBalanceCmd)
acctName := "default"
if cmd.Account != nil {
acctName = *cmd.Account
}
account, err := w.Manager.LookupAccount(acctName)
if err != nil {
return nil, err
}
bals, err := w.CalculateAccountBalances(account, 1)
if err != nil {
return nil, err
}
return (bals.Total - bals.Spendable).ToBTC(), nil
}
// ImportPrivKey handles an importprivkey request by parsing
// a WIF-encoded private key and adding it to an account.
func ImportPrivKey(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.ImportPrivKeyCmd)
// Ensure that private keys are only imported to the correct account.
//
// Yes, Label is the account name.
if cmd.Label != nil && *cmd.Label != waddrmgr.ImportedAddrAccountName {
return nil, &ErrNotImportedAccount
}
wif, err := btcutil.DecodeWIF(cmd.PrivKey)
if err != nil {
return nil, &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidAddressOrKey,
Message: "WIF decode failed: " + err.Error(),
}
}
if !wif.IsForNet(w.ChainParams()) {
return nil, &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidAddressOrKey,
Message: "Key is not intended for " + w.ChainParams().Name,
}
}
// Import the private key, handling any errors.
_, err = w.ImportPrivateKey(wif, nil, *cmd.Rescan)
switch {
case waddrmgr.IsError(err, waddrmgr.ErrDuplicateAddress):
// Do not return duplicate key errors to the client.
return nil, nil
case waddrmgr.IsError(err, waddrmgr.ErrLocked):
return nil, &ErrWalletUnlockNeeded
}
return nil, err
}
// KeypoolRefill handles the keypoolrefill command. Since we handle the keypool
// automatically this does nothing since refilling is never manually required.
func KeypoolRefill(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
return nil, nil
}
// CreateNewAccount handles a createnewaccount request by creating and
// returning a new account. If the last account has no transaction history
// as per BIP 0044 a new account cannot be created so an error will be returned.
func CreateNewAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.CreateNewAccountCmd)
// The wildcard * is reserved by the rpc server with the special meaning
// of "all accounts", so disallow naming accounts to this string.
if cmd.Account == "*" {
return nil, &ErrReservedAccountName
}
// Check that we are within the maximum allowed non-empty accounts limit.
account, err := w.Manager.LastAccount()
if err != nil {
return nil, err
}
if account > maxEmptyAccounts {
used, err := w.AccountUsed(account)
if err != nil {
return nil, err
}
if !used {
return nil, errors.New("cannot create account: " +
"previous account has no transaction history")
}
}
_, err = w.NextAccount(cmd.Account)
if waddrmgr.IsError(err, waddrmgr.ErrLocked) {
return nil, &btcjson.RPCError{
Code: btcjson.ErrRPCWalletUnlockNeeded,
Message: "Creating an account requires the wallet to be unlocked. " +
"Enter the wallet passphrase with walletpassphrase to unlock",
}
}
return nil, err
}
// RenameAccount handles a renameaccount request by renaming an account.
// If the account does not exist an appropiate error will be returned.
func RenameAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.RenameAccountCmd)
// The wildcard * is reserved by the rpc server with the special meaning
// of "all accounts", so disallow naming accounts to this string.
if cmd.NewAccount == "*" {
return nil, &ErrReservedAccountName
}
// Check that given account exists
account, err := w.Manager.LookupAccount(cmd.OldAccount)
if err != nil {
return nil, err
}
return nil, w.RenameAccount(account, cmd.NewAccount)
}
// GetNewAddress handles a getnewaddress request by returning a new
// address for an account. If the account does not exist an appropiate
// error is returned.
// TODO: Follow BIP 0044 and warn if number of unused addresses exceeds
// the gap limit.
func GetNewAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.GetNewAddressCmd)
acctName := "default"
if cmd.Account != nil {
acctName = *cmd.Account
}
account, err := w.Manager.LookupAccount(acctName)
if err != nil {
return nil, err
}
addr, err := w.NewAddress(account)
if err != nil {
return nil, err
}
// Return the new payment address string.
return addr.EncodeAddress(), nil
}
// GetRawChangeAddress handles a getrawchangeaddress request by creating
// and returning a new change address for an account.
//
// Note: bitcoind allows specifying the account as an optional parameter,
// but ignores the parameter.
func GetRawChangeAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.GetRawChangeAddressCmd)
acctName := "default"
if cmd.Account != nil {
acctName = *cmd.Account
}
account, err := w.Manager.LookupAccount(acctName)
if err != nil {
return nil, err
}
addr, err := w.NewChangeAddress(account)
if err != nil {
return nil, err
}
// Return the new payment address string.
return addr.EncodeAddress(), nil
}
// GetReceivedByAccount handles a getreceivedbyaccount request by returning
// the total amount received by addresses of an account.
func GetReceivedByAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.GetReceivedByAccountCmd)
account, err := w.Manager.LookupAccount(cmd.Account)
if err != nil {
return nil, err
}
bal, _, err := w.TotalReceivedForAccount(account, int32(*cmd.MinConf))
if err != nil {
return nil, err
}
return bal.ToBTC(), nil
}
// GetReceivedByAddress handles a getreceivedbyaddress request by returning
// the total amount received by a single address.
func GetReceivedByAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.GetReceivedByAddressCmd)
addr, err := decodeAddress(cmd.Address, w.ChainParams())
if err != nil {
return nil, err
}
total, err := w.TotalReceivedForAddr(addr, int32(*cmd.MinConf))
if err != nil {
return nil, err
}
return total.ToBTC(), nil
}
// GetTransaction handles a gettransaction request by returning details about
// a single transaction saved by wallet.
func GetTransaction(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.GetTransactionCmd)
txSha, err := wire.NewShaHashFromStr(cmd.Txid)
if err != nil {
return nil, &btcjson.RPCError{
Code: btcjson.ErrRPCDecodeHexString,
Message: "Transaction hash string decode failed: " + err.Error(),
}
}
details, err := w.TxStore.TxDetails(txSha)
if err != nil {
return nil, err
}
if details == nil {
return nil, &ErrNoTransactionInfo
}
syncBlock := w.Manager.SyncedTo()
// TODO: The serialized transaction is already in the DB, so
// reserializing can be avoided here.
var txBuf bytes.Buffer
txBuf.Grow(details.MsgTx.SerializeSize())
err = details.MsgTx.Serialize(&txBuf)
if err != nil {
return nil, err
}
// TODO: Add a "generated" field to this result type. "generated":true
// is only added if the transaction is a coinbase.
ret := btcjson.GetTransactionResult{
TxID: cmd.Txid,
Hex: hex.EncodeToString(txBuf.Bytes()),
Time: details.Received.Unix(),
TimeReceived: details.Received.Unix(),
WalletConflicts: []string{}, // Not saved
//Generated: blockchain.IsCoinBaseTx(&details.MsgTx),
}
if details.Block.Height != -1 {
ret.BlockHash = details.Block.Hash.String()
ret.BlockTime = details.Block.Time.Unix()
ret.Confirmations = int64(confirms(details.Block.Height, syncBlock.Height))
}
var (
debitTotal btcutil.Amount
creditTotal btcutil.Amount // Excludes change
fee btcutil.Amount
feeF64 float64
)
for _, deb := range details.Debits {
debitTotal += deb.Amount
}
for _, cred := range details.Credits {
if !cred.Change {
creditTotal += cred.Amount
}
}
// Fee can only be determined if every input is a debit.
if len(details.Debits) == len(details.MsgTx.TxIn) {
var outputTotal btcutil.Amount
for _, output := range details.MsgTx.TxOut {
outputTotal += btcutil.Amount(output.Value)
}
fee = debitTotal - outputTotal
feeF64 = fee.ToBTC()
}
if len(details.Debits) == 0 {
// Credits must be set later, but since we know the full length
// of the details slice, allocate it with the correct cap.
ret.Details = make([]btcjson.GetTransactionDetailsResult, 0, len(details.Credits))
} else {
ret.Details = make([]btcjson.GetTransactionDetailsResult, 1, len(details.Credits)+1)
ret.Details[0] = btcjson.GetTransactionDetailsResult{
// Fields left zeroed:
// InvolvesWatchOnly
// Account
// Address
// Vout
//
// TODO(jrick): Address and Vout should always be set,
// but we're doing the wrong thing here by not matching
// core. Instead, gettransaction should only be adding
// details for transaction outputs, just like
// listtransactions (but using the short result format).
Category: "send",
Amount: (-debitTotal).ToBTC(), // negative since it is a send
Fee: &feeF64,
}
ret.Fee = feeF64
}
credCat := wallet.RecvCategory(details, syncBlock.Height).String()
for _, cred := range details.Credits {
// Change is ignored.
if cred.Change {
continue
}
var address string
var accountName string
_, addrs, _, err := txscript.ExtractPkScriptAddrs(
details.MsgTx.TxOut[cred.Index].PkScript, w.ChainParams())
if err == nil && len(addrs) == 1 {
addr := addrs[0]
address = addr.EncodeAddress()
account, err := w.Manager.AddrAccount(addr)
if err == nil {
accountName, err = w.Manager.AccountName(account)
if err != nil {
accountName = ""
}
}
}
ret.Details = append(ret.Details, btcjson.GetTransactionDetailsResult{
// Fields left zeroed:
// InvolvesWatchOnly
// Fee
Account: accountName,
Address: address,
Category: credCat,
Amount: cred.Amount.ToBTC(),
Vout: cred.Index,
})
}
ret.Amount = creditTotal.ToBTC()
return ret, nil
}
// These generators create the following global variables in this package:
//
// var localeHelpDescs map[string]func() map[string]string
// var requestUsages string
//
// localeHelpDescs maps from locale strings (e.g. "en_US") to a function that
// builds a map of help texts for each RPC server method. This prevents help
// text maps for every locale map from being rooted and created during init.
// Instead, the appropiate function is looked up when help text is first needed
// using the current locale and saved to the global below for futher reuse.
//
// requestUsages contains single line usages for every supported request,
// separated by newlines. It is set during init. These usages are used for all
// locales.
//
//go:generate go run ../../internal/rpchelp/genrpcserverhelp.go legacyrpc
//go:generate gofmt -w rpcserverhelp.go
var helpDescs map[string]string
var helpDescsMu sync.Mutex // Help may execute concurrently, so synchronize access.
// HelpWithChainRPC handles the help request when the RPC server has been
// associated with a consensus RPC client. The additional RPC client is used to
// include help messages for methods implemented by the consensus server via RPC
// passthrough.
func HelpWithChainRPC(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCClient) (interface{}, error) {
return help(icmd, w, chainClient)
}
// HelpNoChainRPC handles the help request when the RPC server has not been
// associated with a consensus RPC client. No help messages are included for
// passthrough requests.
func HelpNoChainRPC(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
return help(icmd, w, nil)
}
// help handles the help request by returning one line usage of all available
// methods, or full help for a specific method. The chainClient is optional,
// and this is simply a helper function for the HelpNoChainRPC and
// HelpWithChainRPC handlers.
func help(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCClient) (interface{}, error) {
cmd := icmd.(*btcjson.HelpCmd)
// btcd returns different help messages depending on the kind of
// connection the client is using. Only methods availble to HTTP POST
// clients are available to be used by wallet clients, even though
// wallet itself is a websocket client to btcd. Therefore, create a
// POST client as needed.
//
// Returns nil if chainClient is currently nil or there is an error
// creating the client.
//
// This is hacky and is probably better handled by exposing help usage
// texts in a non-internal btcd package.
postClient := func() *btcrpcclient.Client {
if chainClient == nil {
return nil
}
c, err := chainClient.POSTClient()
if err != nil {
return nil
}
return c
}
if cmd.Command == nil || *cmd.Command == "" {
// Prepend chain server usage if it is available.
usages := requestUsages
client := postClient()
if client != nil {
rawChainUsage, err := client.RawRequest("help", nil)
var chainUsage string
if err == nil {
_ = json.Unmarshal([]byte(rawChainUsage), &chainUsage)
}
if chainUsage != "" {
usages = "Chain server usage:\n\n" + chainUsage + "\n\n" +
"Wallet server usage (overrides chain requests):\n\n" +
requestUsages
}
}
return usages, nil
}
defer helpDescsMu.Unlock()
helpDescsMu.Lock()
if helpDescs == nil {
// TODO: Allow other locales to be set via config or detemine
// this from environment variables. For now, hardcode US
// English.
helpDescs = localeHelpDescs["en_US"]()
}
helpText, ok := helpDescs[*cmd.Command]
if ok {
return helpText, nil
}
// Return the chain server's detailed help if possible.
var chainHelp string
client := postClient()
if client != nil {
param := make([]byte, len(*cmd.Command)+2)
param[0] = '"'
copy(param[1:], *cmd.Command)
param[len(param)-1] = '"'
rawChainHelp, err := client.RawRequest("help", []json.RawMessage{param})
if err == nil {
_ = json.Unmarshal([]byte(rawChainHelp), &chainHelp)
}
}
if chainHelp != "" {
return chainHelp, nil
}
return nil, &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidParameter,
Message: fmt.Sprintf("No help for method '%s'", *cmd.Command),
}
}
// ListAccounts handles a listaccounts request by returning a map of account
// names to their balances.
func ListAccounts(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.ListAccountsCmd)
accountBalances := map[string]float64{}
var accounts []uint32
err := w.Manager.ForEachAccount(func(account uint32) error {
accounts = append(accounts, account)
return nil
})
if err != nil {
return nil, err
}
minConf := int32(*cmd.MinConf)
for _, account := range accounts {
acctName, err := w.Manager.AccountName(account)
if err != nil {
return nil, &ErrAccountNameNotFound
}
bals, err := w.CalculateAccountBalances(account, minConf)
if err != nil {
return nil, err
}
accountBalances[acctName] = bals.Spendable.ToBTC()
}
// Return the map. This will be marshaled into a JSON object.
return accountBalances, nil
}
// ListLockUnspent handles a listlockunspent request by returning an slice of
// all locked outpoints.
func ListLockUnspent(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
return w.LockedOutpoints(), nil
}
// ListReceivedByAccount handles a listreceivedbyaccount request by returning
// a slice of objects, each one containing:
// "account": the receiving account;
// "amount": total amount received by the account;
// "confirmations": number of confirmations of the most recent transaction.
// It takes two parameters:
// "minconf": minimum number of confirmations to consider a transaction -
// default: one;
// "includeempty": whether or not to include addresses that have no transactions -
// default: false.
func ListReceivedByAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.ListReceivedByAccountCmd)
var accounts []uint32
err := w.Manager.ForEachAccount(func(account uint32) error {
accounts = append(accounts, account)
return nil
})
if err != nil {
return nil, err
}
ret := make([]btcjson.ListReceivedByAccountResult, 0, len(accounts))
minConf := int32(*cmd.MinConf)
for _, account := range accounts {
acctName, err := w.Manager.AccountName(account)
if err != nil {
return nil, &ErrAccountNameNotFound
}
bal, confirmations, err := w.TotalReceivedForAccount(account,
minConf)
if err != nil {
return nil, err
}
ret = append(ret, btcjson.ListReceivedByAccountResult{
Account: acctName,
Amount: bal.ToBTC(),
Confirmations: uint64(confirmations),
})
}
return ret, nil
}
// ListReceivedByAddress handles a listreceivedbyaddress request by returning
// a slice of objects, each one containing:
// "account": the account of the receiving address;
// "address": the receiving address;
// "amount": total amount received by the address;
// "confirmations": number of confirmations of the most recent transaction.
// It takes two parameters:
// "minconf": minimum number of confirmations to consider a transaction -
// default: one;
// "includeempty": whether or not to include addresses that have no transactions -
// default: false.
func ListReceivedByAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.ListReceivedByAddressCmd)
// Intermediate data for each address.
type AddrData struct {
// Total amount received.
amount btcutil.Amount
// Number of confirmations of the last transaction.
confirmations int32
// Hashes of transactions which include an output paying to the address
tx []string
// Account which the address belongs to
account string
}
syncBlock := w.Manager.SyncedTo()
// Intermediate data for all addresses.
allAddrData := make(map[string]AddrData)
// Create an AddrData entry for each active address in the account.
// Otherwise we'll just get addresses from transactions later.
sortedAddrs, err := w.SortedActivePaymentAddresses()
if err != nil {
return nil, err
}
for _, address := range sortedAddrs {
// There might be duplicates, just overwrite them.
allAddrData[address] = AddrData{}
}
minConf := *cmd.MinConf
var endHeight int32
if minConf == 0 {
endHeight = -1
} else {
endHeight = syncBlock.Height - int32(minConf) + 1
}
err = w.TxStore.RangeTransactions(0, endHeight, func(details []wtxmgr.TxDetails) (bool, error) {
confirmations := confirms(details[0].Block.Height, syncBlock.Height)
for _, tx := range details {
for _, cred := range tx.Credits {
pkScript := tx.MsgTx.TxOut[cred.Index].PkScript
_, addrs, _, err := txscript.ExtractPkScriptAddrs(
pkScript, w.ChainParams())
if err != nil {
// Non standard script, skip.
continue
}
for _, addr := range addrs {
addrStr := addr.EncodeAddress()
addrData, ok := allAddrData[addrStr]
if ok {
addrData.amount += cred.Amount
// Always overwrite confirmations with newer ones.
addrData.confirmations = confirmations
} else {
addrData = AddrData{
amount: cred.Amount,
confirmations: confirmations,
}
}
addrData.tx = append(addrData.tx, tx.Hash.String())
allAddrData[addrStr] = addrData
}
}
}
return false, nil
})
if err != nil {
return nil, err
}
// Massage address data into output format.
numAddresses := len(allAddrData)
ret := make([]btcjson.ListReceivedByAddressResult, numAddresses, numAddresses)
idx := 0
for address, addrData := range allAddrData {
ret[idx] = btcjson.ListReceivedByAddressResult{
Address: address,
Amount: addrData.amount.ToBTC(),
Confirmations: uint64(addrData.confirmations),
TxIDs: addrData.tx,
}
idx++
}
return ret, nil
}
// ListSinceBlock handles a listsinceblock request by returning an array of maps
// with details of sent and received wallet transactions since the given block.
func ListSinceBlock(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCClient) (interface{}, error) {
cmd := icmd.(*btcjson.ListSinceBlockCmd)
syncBlock := w.Manager.SyncedTo()
targetConf := int64(*cmd.TargetConfirmations)
// For the result we need the block hash for the last block counted
// in the blockchain due to confirmations. We send this off now so that
// it can arrive asynchronously while we figure out the rest.
gbh := chainClient.GetBlockHashAsync(int64(syncBlock.Height) + 1 - targetConf)
var start int32
if cmd.BlockHash != nil {
hash, err := wire.NewShaHashFromStr(*cmd.BlockHash)
if err != nil {
return nil, DeserializationError{err}
}
block, err := chainClient.GetBlockVerbose(hash, false)
if err != nil {
return nil, err
}
start = int32(block.Height) + 1
}
txInfoList, err := w.ListSinceBlock(start, -1, syncBlock.Height)
if err != nil {
return nil, err
}
// Done with work, get the response.
blockHash, err := gbh.Receive()
if err != nil {
return nil, err
}
res := btcjson.ListSinceBlockResult{
Transactions: txInfoList,
LastBlock: blockHash.String(),
}
return res, nil
}
// ListTransactions handles a listtransactions request by returning an
// array of maps with details of sent and recevied wallet transactions.
func ListTransactions(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.ListTransactionsCmd)
// TODO: ListTransactions does not currently understand the difference
// between transactions pertaining to one account from another. This
// will be resolved when wtxmgr is combined with the waddrmgr namespace.
if cmd.Account != nil && *cmd.Account != "*" {
// For now, don't bother trying to continue if the user
// specified an account, since this can't be (easily or
// efficiently) calculated.
return nil, &btcjson.RPCError{
Code: btcjson.ErrRPCWallet,
Message: "Transactions are not yet grouped by account",
}
}
return w.ListTransactions(*cmd.From, *cmd.Count)
}
// ListAddressTransactions handles a listaddresstransactions request by
// returning an array of maps with details of spent and received wallet
// transactions. The form of the reply is identical to listtransactions,
// but the array elements are limited to transaction details which are
// about the addresess included in the request.
func ListAddressTransactions(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.ListAddressTransactionsCmd)
if cmd.Account != nil && *cmd.Account != "*" {
return nil, &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidParameter,
Message: "Listing transactions for addresses may only be done for all accounts",
}
}
// Decode addresses.
hash160Map := make(map[string]struct{})
for _, addrStr := range cmd.Addresses {
addr, err := decodeAddress(addrStr, w.ChainParams())
if err != nil {
return nil, err
}
hash160Map[string(addr.ScriptAddress())] = struct{}{}
}
return w.ListAddressTransactions(hash160Map)
}
// ListAllTransactions handles a listalltransactions request by returning
// a map with details of sent and recevied wallet transactions. This is
// similar to ListTransactions, except it takes only a single optional
// argument for the account name and replies with all transactions.
func ListAllTransactions(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.ListAllTransactionsCmd)
if cmd.Account != nil && *cmd.Account != "*" {
return nil, &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidParameter,
Message: "Listing all transactions may only be done for all accounts",
}
}
return w.ListAllTransactions()
}
// ListUnspent handles the listunspent command.
func ListUnspent(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.ListUnspentCmd)
var addresses map[string]struct{}
if cmd.Addresses != nil {
addresses = make(map[string]struct{})
// confirm that all of them are good:
for _, as := range *cmd.Addresses {
a, err := decodeAddress(as, w.ChainParams())
if err != nil {
return nil, err
}
addresses[a.EncodeAddress()] = struct{}{}
}
}
return w.ListUnspent(int32(*cmd.MinConf), int32(*cmd.MaxConf), addresses)
}
// LockUnspent handles the lockunspent command.
func LockUnspent(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.LockUnspentCmd)
switch {
case cmd.Unlock && len(cmd.Transactions) == 0:
w.ResetLockedOutpoints()
default:
for _, input := range cmd.Transactions {
txSha, err := wire.NewShaHashFromStr(input.Txid)
if err != nil {
return nil, ParseError{err}
}
op := wire.OutPoint{Hash: *txSha, Index: input.Vout}
if cmd.Unlock {
w.UnlockOutpoint(op)
} else {
w.LockOutpoint(op)
}
}
}
return true, nil
}
// sendPairs creates and sends payment transactions.
// It returns the transaction hash in string format upon success
// All errors are returned in btcjson.RPCError format
func sendPairs(w *wallet.Wallet, amounts map[string]btcutil.Amount,
account uint32, minconf int32) (string, error) {
txSha, err := w.SendPairs(amounts, account, minconf)
if err != nil {
if err == wallet.ErrNonPositiveAmount {
return "", ErrNeedPositiveAmount
}
if waddrmgr.IsError(err, waddrmgr.ErrLocked) {
return "", &ErrWalletUnlockNeeded
}
switch err.(type) {
case btcjson.RPCError:
return "", err
}
return "", &btcjson.RPCError{
Code: btcjson.ErrRPCInternal.Code,
Message: err.Error(),
}
}
txShaStr := txSha.String()
log.Infof("Successfully sent transaction %v", txShaStr)
return txShaStr, nil
}
func isNilOrEmpty(s *string) bool {
return s == nil || *s == ""
}
// SendFrom handles a sendfrom RPC request by creating a new transaction
// spending unspent transaction outputs for a wallet to another payment
// address. Leftover inputs not sent to the payment address or a fee for
// the miner are sent back to a new address in the wallet. Upon success,
// the TxID for the created transaction is returned.
func SendFrom(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCClient) (interface{}, error) {
cmd := icmd.(*btcjson.SendFromCmd)
// Transaction comments are not yet supported. Error instead of
// pretending to save them.
if !isNilOrEmpty(cmd.Comment) || !isNilOrEmpty(cmd.CommentTo) {
return nil, &btcjson.RPCError{
Code: btcjson.ErrRPCUnimplemented,
Message: "Transaction comments are not yet supported",
}
}
account, err := w.Manager.LookupAccount(cmd.FromAccount)
if err != nil {
return nil, err
}
// Check that signed integer parameters are positive.
if cmd.Amount < 0 {
return nil, ErrNeedPositiveAmount
}
minConf := int32(*cmd.MinConf)
if minConf < 0 {
return nil, ErrNeedPositiveMinconf
}
// Create map of address and amount pairs.
amt, err := btcutil.NewAmount(cmd.Amount)
if err != nil {
return nil, err
}
pairs := map[string]btcutil.Amount{
cmd.ToAddress: amt,
}
return sendPairs(w, pairs, account, minConf)
}
// SendMany handles a sendmany RPC request by creating a new transaction
// spending unspent transaction outputs for a wallet to any number of
// payment addresses. Leftover inputs not sent to the payment address
// or a fee for the miner are sent back to a new address in the wallet.
// Upon success, the TxID for the created transaction is returned.
func SendMany(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.SendManyCmd)
// Transaction comments are not yet supported. Error instead of
// pretending to save them.
if !isNilOrEmpty(cmd.Comment) {
return nil, &btcjson.RPCError{
Code: btcjson.ErrRPCUnimplemented,
Message: "Transaction comments are not yet supported",
}
}
account, err := w.Manager.LookupAccount(cmd.FromAccount)
if err != nil {
return nil, err
}
// Check that minconf is positive.
minConf := int32(*cmd.MinConf)
if minConf < 0 {
return nil, ErrNeedPositiveMinconf
}
// Recreate address/amount pairs, using btcutil.Amount.
pairs := make(map[string]btcutil.Amount, len(cmd.Amounts))
for k, v := range cmd.Amounts {
amt, err := btcutil.NewAmount(v)
if err != nil {
return nil, err
}
pairs[k] = amt
}
return sendPairs(w, pairs, account, minConf)
}
// SendToAddress handles a sendtoaddress RPC request by creating a new
// transaction spending unspent transaction outputs for a wallet to another
// payment address. Leftover inputs not sent to the payment address or a fee
// for the miner are sent back to a new address in the wallet. Upon success,
// the TxID for the created transaction is returned.
func SendToAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.SendToAddressCmd)
// Transaction comments are not yet supported. Error instead of
// pretending to save them.
if !isNilOrEmpty(cmd.Comment) || !isNilOrEmpty(cmd.CommentTo) {
return nil, &btcjson.RPCError{
Code: btcjson.ErrRPCUnimplemented,
Message: "Transaction comments are not yet supported",
}
}
amt, err := btcutil.NewAmount(cmd.Amount)
if err != nil {
return nil, err
}
// Check that signed integer parameters are positive.
if amt < 0 {
return nil, ErrNeedPositiveAmount
}
// Mock up map of address and amount pairs.
pairs := map[string]btcutil.Amount{
cmd.Address: amt,
}
// sendtoaddress always spends from the default account, this matches bitcoind
return sendPairs(w, pairs, waddrmgr.DefaultAccountNum, 1)
}
// SetTxFee sets the transaction fee per kilobyte added to transactions.
func SetTxFee(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.SetTxFeeCmd)
// Check that amount is not negative.
if cmd.Amount < 0 {
return nil, ErrNeedPositiveAmount
}
incr, err := btcutil.NewAmount(cmd.Amount)
if err != nil {
return nil, err
}
w.FeeIncrement = incr
// A boolean true result is returned upon success.
return true, nil
}
// SignMessage signs the given message with the private key for the given
// address
func SignMessage(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.SignMessageCmd)
addr, err := decodeAddress(cmd.Address, w.ChainParams())
if err != nil {
return nil, err
}
ainfo, err := w.Manager.Address(addr)
if err != nil {
return nil, err
}
pka, ok := ainfo.(waddrmgr.ManagedPubKeyAddress)
if !ok {
msg := fmt.Sprintf("Address '%s' does not have an associated private key", addr)
return nil, &btcjson.RPCError{
Code: btcjson.ErrRPCInvalidAddressOrKey,
Message: msg,
}
}
privKey, err := pka.PrivKey()
if err != nil {
return nil, err
}
var buf bytes.Buffer
wire.WriteVarString(&buf, 0, "Bitcoin Signed Message:\n")
wire.WriteVarString(&buf, 0, cmd.Message)
messageHash := wire.DoubleSha256(buf.Bytes())
sigbytes, err := btcec.SignCompact(btcec.S256(), privKey,
messageHash, ainfo.Compressed())
if err != nil {
return nil, err
}
return base64.StdEncoding.EncodeToString(sigbytes), nil
}
// pendingTx is used for async fetching of transaction dependancies in
// SignRawTransaction.
type pendingTx struct {
resp btcrpcclient.FutureGetRawTransactionResult
inputs []uint32 // list of inputs that care about this tx.
}
// SignRawTransaction handles the signrawtransaction command.
func SignRawTransaction(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCClient) (interface{}, error) {
cmd := icmd.(*btcjson.SignRawTransactionCmd)
serializedTx, err := decodeHexStr(cmd.RawTx)
if err != nil {
return nil, err
}
tx := wire.NewMsgTx()
err = tx.Deserialize(bytes.NewBuffer(serializedTx))
if err != nil {
e := errors.New("TX decode failed")
return nil, DeserializationError{e}
}
var hashType txscript.SigHashType
switch *cmd.Flags {
case "ALL":
hashType = txscript.SigHashAll
case "NONE":
hashType = txscript.SigHashNone
case "SINGLE":
hashType = txscript.SigHashSingle
case "ALL|ANYONECANPAY":
hashType = txscript.SigHashAll | txscript.SigHashAnyOneCanPay
case "NONE|ANYONECANPAY":
hashType = txscript.SigHashNone | txscript.SigHashAnyOneCanPay
case "SINGLE|ANYONECANPAY":
hashType = txscript.SigHashSingle | txscript.SigHashAnyOneCanPay
default:
e := errors.New("Invalid sighash parameter")
return nil, InvalidParameterError{e}
}
// TODO: really we probably should look these up with btcd anyway to
// make sure that they match the blockchain if present.
inputs := make(map[wire.OutPoint][]byte)
scripts := make(map[string][]byte)
var cmdInputs []btcjson.RawTxInput
if cmd.Inputs != nil {
cmdInputs = *cmd.Inputs
}
for _, rti := range cmdInputs {
inputSha, err := wire.NewShaHashFromStr(rti.Txid)
if err != nil {
return nil, DeserializationError{err}
}
script, err := decodeHexStr(rti.ScriptPubKey)
if err != nil {
return nil, err
}
// redeemScript is only actually used iff the user provided
// private keys. In which case, it is used to get the scripts
// for signing. If the user did not provide keys then we always
// get scripts from the wallet.
// Empty strings are ok for this one and hex.DecodeString will
// DTRT.
if cmd.PrivKeys != nil && len(*cmd.PrivKeys) != 0 {
redeemScript, err := decodeHexStr(rti.RedeemScript)
if err != nil {
return nil, err
}
addr, err := btcutil.NewAddressScriptHash(redeemScript,
w.ChainParams())
if err != nil {
return nil, DeserializationError{err}
}
scripts[addr.String()] = redeemScript
}
inputs[wire.OutPoint{
Hash: *inputSha,
Index: rti.Vout,
}] = script
}
// Now we go and look for any inputs that we were not provided by
// querying btcd with getrawtransaction. We queue up a bunch of async
// requests and will wait for replies after we have checked the rest of
// the arguments.
requested := make(map[wire.ShaHash]*pendingTx)
for _, txIn := range tx.TxIn {
// Did we get this txin from the arguments?
if _, ok := inputs[txIn.PreviousOutPoint]; ok {
continue
}
// Are we already fetching this tx? If so mark us as interested
// in this outpoint. (N.B. that any *sane* tx will only
// reference each outpoint once, since anything else is a double
// spend. We don't check this ourselves to save having to scan
// the array, it will fail later if so).
if ptx, ok := requested[txIn.PreviousOutPoint.Hash]; ok {
ptx.inputs = append(ptx.inputs,
txIn.PreviousOutPoint.Index)
continue
}
// Never heard of this one before, request it.
prevHash := &txIn.PreviousOutPoint.Hash
requested[txIn.PreviousOutPoint.Hash] = &pendingTx{
resp: chainClient.GetRawTransactionAsync(prevHash),
inputs: []uint32{txIn.PreviousOutPoint.Index},
}
}
// Parse list of private keys, if present. If there are any keys here
// they are the keys that we may use for signing. If empty we will
// use any keys known to us already.
var keys map[string]*btcutil.WIF
if cmd.PrivKeys != nil {
keys = make(map[string]*btcutil.WIF)
for _, key := range *cmd.PrivKeys {
wif, err := btcutil.DecodeWIF(key)
if err != nil {
return nil, DeserializationError{err}
}
if !wif.IsForNet(w.ChainParams()) {
s := "key network doesn't match wallet's"
return nil, DeserializationError{errors.New(s)}
}
addr, err := btcutil.NewAddressPubKey(wif.SerializePubKey(),
w.ChainParams())
if err != nil {
return nil, DeserializationError{err}
}
keys[addr.EncodeAddress()] = wif
}
}
// We have checked the rest of the args. now we can collect the async
// txs. TODO: If we don't mind the possibility of wasting work we could
// move waiting to the following loop and be slightly more asynchronous.
for txid, ptx := range requested {
tx, err := ptx.resp.Receive()
if err != nil {
return nil, err
}
for _, input := range ptx.inputs {
if input >= uint32(len(tx.MsgTx().TxOut)) {
e := fmt.Errorf("input %s:%d is not in tx",
txid.String(), input)
return nil, InvalidParameterError{e}
}
inputs[wire.OutPoint{
Hash: txid,
Index: input,
}] = tx.MsgTx().TxOut[input].PkScript
}
}
// All args collected. Now we can sign all the inputs that we can.
// `complete' denotes that we successfully signed all outputs and that
// all scripts will run to completion. This is returned as part of the
// reply.
signErrs, err := w.SignTransaction(tx, hashType, inputs, keys, scripts)
if err != nil {
return nil, err
}
var buf bytes.Buffer
buf.Grow(tx.SerializeSize())
// All returned errors (not OOM, which panics) encounted during
// bytes.Buffer writes are unexpected.
if err = tx.Serialize(&buf); err != nil {
panic(err)
}
signErrors := make([]btcjson.SignRawTransactionError, 0, len(signErrs))
for _, e := range signErrs {
input := tx.TxIn[e.InputIndex]
signErrors = append(signErrors, btcjson.SignRawTransactionError{
TxID: input.PreviousOutPoint.Hash.String(),
Vout: input.PreviousOutPoint.Index,
ScriptSig: hex.EncodeToString(input.SignatureScript),
Sequence: input.Sequence,
Error: e.Error.Error(),
})
}
return btcjson.SignRawTransactionResult{
Hex: hex.EncodeToString(buf.Bytes()),
Complete: len(signErrors) == 0,
Errors: signErrors,
}, nil
}
// ValidateAddress handles the validateaddress command.
func ValidateAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.ValidateAddressCmd)
result := btcjson.ValidateAddressWalletResult{}
addr, err := decodeAddress(cmd.Address, w.ChainParams())
if err != nil {
// Use result zero value (IsValid=false).
return result, nil
}
// We could put whether or not the address is a script here,
// by checking the type of "addr", however, the reference
// implementation only puts that information if the script is
// "ismine", and we follow that behaviour.
result.Address = addr.EncodeAddress()
result.IsValid = true
ainfo, err := w.Manager.Address(addr)
if err != nil {
if waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) {
// No additional information available about the address.
return result, nil
}
return nil, err
}
// The address lookup was successful which means there is further
// information about it available and it is "mine".
result.IsMine = true
acctName, err := w.Manager.AccountName(ainfo.Account())
if err != nil {
return nil, &ErrAccountNameNotFound
}
result.Account = acctName
switch ma := ainfo.(type) {
case waddrmgr.ManagedPubKeyAddress:
result.IsCompressed = ma.Compressed()
result.PubKey = ma.ExportPubKey()
case waddrmgr.ManagedScriptAddress:
result.IsScript = true
// The script is only available if the manager is unlocked, so
// just break out now if there is an error.
script, err := ma.Script()
if err != nil {
break
}
result.Hex = hex.EncodeToString(script)
// This typically shouldn't fail unless an invalid script was
// imported. However, if it fails for any reason, there is no
// further information available, so just set the script type
// a non-standard and break out now.
class, addrs, reqSigs, err := txscript.ExtractPkScriptAddrs(
script, w.ChainParams())
if err != nil {
result.Script = txscript.NonStandardTy.String()
break
}
addrStrings := make([]string, len(addrs))
for i, a := range addrs {
addrStrings[i] = a.EncodeAddress()
}
result.Addresses = addrStrings
// Multi-signature scripts also provide the number of required
// signatures.
result.Script = class.String()
if class == txscript.MultiSigTy {
result.SigsRequired = int32(reqSigs)
}
}
return result, nil
}
// VerifyMessage handles the verifymessage command by verifying the provided
// compact signature for the given address and message.
func VerifyMessage(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.VerifyMessageCmd)
addr, err := decodeAddress(cmd.Address, w.ChainParams())
if err != nil {
return nil, err
}
// decode base64 signature
sig, err := base64.StdEncoding.DecodeString(cmd.Signature)
if err != nil {
return nil, err
}
// Validate the signature - this just shows that it was valid at all.
// we will compare it with the key next.
var buf bytes.Buffer
wire.WriteVarString(&buf, 0, "Bitcoin Signed Message:\n")
wire.WriteVarString(&buf, 0, cmd.Message)
expectedMessageHash := wire.DoubleSha256(buf.Bytes())
pk, wasCompressed, err := btcec.RecoverCompact(btcec.S256(), sig,
expectedMessageHash)
if err != nil {
return nil, err
}
var serializedPubKey []byte
if wasCompressed {
serializedPubKey = pk.SerializeCompressed()
} else {
serializedPubKey = pk.SerializeUncompressed()
}
// Verify that the signed-by address matches the given address
switch checkAddr := addr.(type) {
case *btcutil.AddressPubKeyHash: // ok
return bytes.Equal(btcutil.Hash160(serializedPubKey), checkAddr.Hash160()[:]), nil
case *btcutil.AddressPubKey: // ok
return string(serializedPubKey) == checkAddr.String(), nil
default:
return nil, errors.New("address type not supported")
}
}
// WalletIsLocked handles the walletislocked extension request by
// returning the current lock state (false for unlocked, true for locked)
// of an account.
func WalletIsLocked(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
return w.Locked(), nil
}
// WalletLock handles a walletlock request by locking the all account
// wallets, returning an error if any wallet is not encrypted (for example,
// a watching-only wallet).
func WalletLock(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
w.Lock()
return nil, nil
}
// WalletPassphrase responds to the walletpassphrase request by unlocking
// the wallet. The decryption key is saved in the wallet until timeout
// seconds expires, after which the wallet is locked.
func WalletPassphrase(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.WalletPassphraseCmd)
timeout := time.Second * time.Duration(cmd.Timeout)
var unlockAfter <-chan time.Time
if timeout != 0 {
unlockAfter = time.After(timeout)
}
err := w.Unlock([]byte(cmd.Passphrase), unlockAfter)
return nil, err
}
// WalletPassphraseChange responds to the walletpassphrasechange request
// by unlocking all accounts with the provided old passphrase, and
// re-encrypting each private key with an AES key derived from the new
// passphrase.
//
// If the old passphrase is correct and the passphrase is changed, all
// wallets will be immediately locked.
func WalletPassphraseChange(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.WalletPassphraseChangeCmd)
err := w.ChangePassphrase([]byte(cmd.OldPassphrase),
[]byte(cmd.NewPassphrase))
if waddrmgr.IsError(err, waddrmgr.ErrWrongPassphrase) {
return nil, &btcjson.RPCError{
Code: btcjson.ErrRPCWalletPassphraseIncorrect,
Message: "Incorrect passphrase",
}
}
return nil, err
}
// decodeHexStr decodes the hex encoding of a string, possibly prepending a
// leading '0' character if there is an odd number of bytes in the hex string.
// This is to prevent an error for an invalid hex string when using an odd
// number of bytes when calling hex.Decode.
func decodeHexStr(hexStr string) ([]byte, error) {
if len(hexStr)%2 != 0 {
hexStr = "0" + hexStr
}
decoded, err := hex.DecodeString(hexStr)
if err != nil {
return nil, &btcjson.RPCError{
Code: btcjson.ErrRPCDecodeHexString,
Message: "Hex string decode failed: " + err.Error(),
}
}
return decoded, nil
}