2242 lines
68 KiB
Go
2242 lines
68 KiB
Go
// Copyright (c) 2013-2017 The btcsuite developers
|
|
// Copyright (c) 2016 The Decred developers
|
|
// Use of this source code is governed by an ISC
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package legacyrpc
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/lbryio/lbcd/blockchain"
|
|
"github.com/lbryio/lbcd/btcec"
|
|
"github.com/lbryio/lbcd/btcjson"
|
|
"github.com/lbryio/lbcd/chaincfg"
|
|
"github.com/lbryio/lbcd/chaincfg/chainhash"
|
|
"github.com/lbryio/lbcd/rpcclient"
|
|
"github.com/lbryio/lbcd/txscript"
|
|
"github.com/lbryio/lbcd/wire"
|
|
btcutil "github.com/lbryio/lbcutil"
|
|
"github.com/lbryio/lbcwallet/chain"
|
|
"github.com/lbryio/lbcwallet/waddrmgr"
|
|
"github.com/lbryio/lbcwallet/wallet"
|
|
"github.com/lbryio/lbcwallet/wallet/txrules"
|
|
"github.com/lbryio/lbcwallet/wtxmgr"
|
|
)
|
|
|
|
const (
|
|
// defaultAccountName is the name of the wallet's default account.
|
|
defaultAccountName = "default"
|
|
)
|
|
|
|
// 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 appropriate 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},
|
|
"getaddressinfo": {handler: getAddressInfo},
|
|
"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},
|
|
"rescanblockchain": {handlerWithChain: rescanBlockchain},
|
|
"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 lbcwallet due to
|
|
// design decision differences
|
|
"encryptwallet": {handler: unsupported, noHelp: true},
|
|
|
|
// Extensions to the reference client JSON-RPC API
|
|
"createnewaccount": {handler: createNewAccount},
|
|
"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
|
|
// appropriate 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 lbcwallet due to design differences.
|
|
func unsupported(interface{}, *wallet.Wallet) (interface{}, error) {
|
|
return nil, &btcjson.RPCError{
|
|
Code: -1,
|
|
Message: "Request unsupported by lbcwallet",
|
|
}
|
|
}
|
|
|
|
// 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.Interface) 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
|
|
}
|
|
switch client := chainClient.(type) {
|
|
case *chain.RPCClient:
|
|
resp, err := handlerData.handlerWithChain(cmd,
|
|
w, client)
|
|
if err != nil {
|
|
return nil, jsonError(err)
|
|
}
|
|
return resp, nil
|
|
default:
|
|
return nil, &btcjson.RPCError{
|
|
Code: -1,
|
|
Message: "Chain RPC is inactive",
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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",
|
|
}
|
|
}
|
|
switch client := chainClient.(type) {
|
|
case *chain.RPCClient:
|
|
resp, err := client.RawRequest(request.Method,
|
|
request.Params)
|
|
if err != nil {
|
|
return nil, jsonError(err)
|
|
}
|
|
return &resp, nil
|
|
default:
|
|
return nil, &btcjson.RPCError{
|
|
Code: -1,
|
|
Message: "Chain RPC is inactive",
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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:
|
|
if e.ErrorCode == 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
|
|
default:
|
|
pubKey, err := w.PubKeyForAddress(addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pubKeyAddr, err := btcutil.NewAddressPubKey(
|
|
pubKey.SerializeCompressed(), w.ChainParams())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
keysesPrecious[i] = pubKeyAddr
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
secp256k1Addrs := make([]btcutil.Address, len(cmd.Keys))
|
|
for i, k := range cmd.Keys {
|
|
addr, err := decodeAddress(k, w.ChainParams())
|
|
if err != nil {
|
|
return nil, ParseError{err}
|
|
}
|
|
secp256k1Addrs[i] = addr
|
|
}
|
|
|
|
script, err := w.MakeMultiSigScript(secp256k1Addrs, cmd.NRequired)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
p2shAddr, err := w.ImportP2SHRedeemScript(script)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return p2shAddr.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 appropriate 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
|
|
}
|
|
|
|
// getAddressesByAccount handles a getaddressesbyaccount request by returning
|
|
// all addresses for an account, or an error if the requested account does
|
|
// not exist. If addresstype is also specified, only those address types are
|
|
// returned.
|
|
func getAddressesByAccount(icmd interface{}, w *wallet.Wallet) (
|
|
interface{}, error) {
|
|
|
|
cmd := icmd.(*btcjson.GetAddressesByAccountCmd)
|
|
|
|
account, err := w.AccountNumber(*cmd.Account)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Use specified scope, if provided.
|
|
scope, err := lookupKeyScope(cmd.AddressType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
addrs, err := w.AccountAddresses(account, scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
addrStrs := make([]string, len(addrs))
|
|
for i, a := range addrs {
|
|
addrStrs[i] = a.EncodeAddress()
|
|
}
|
|
|
|
return addrStrs, nil
|
|
}
|
|
|
|
// getAddressInfo handles a getaddressinfo request by returning
|
|
// information about the given address.
|
|
// Some of the information will only be present if the address
|
|
// is in the active wallet.
|
|
func getAddressInfo(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
|
|
cmd := icmd.(*btcjson.GetAddressInfoCmd)
|
|
|
|
var result btcjson.GetAddressInfoResult
|
|
|
|
addr, err := decodeAddress(cmd.Address, w.ChainParams())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result.Address = addr.EncodeAddress()
|
|
|
|
buf := bytes.NewBuffer(nil)
|
|
switch a := addr.(type) {
|
|
case *btcutil.AddressPubKey:
|
|
pubKey := a.PubKey().SerializeCompressed()
|
|
pubKeyStr := hex.EncodeToString(pubKey)
|
|
result.PubKey = &pubKeyStr
|
|
isCompressed := true
|
|
result.IsCompressed = &isCompressed
|
|
case *btcutil.AddressPubKeyHash:
|
|
buf.WriteByte(0x76) // OP_DUP
|
|
buf.WriteByte(0xA9) // OP_HASH160
|
|
buf.WriteByte(0x14) // OP_DATA_20
|
|
buf.Write(addr.ScriptAddress())
|
|
buf.WriteByte(0x88) // OP_EQUALVERIFY
|
|
buf.WriteByte(0xAC) // OP_CHECKSIG
|
|
case *btcutil.AddressScriptHash:
|
|
buf.WriteByte(0xA9) // OP_HASH160
|
|
buf.Write(a.ScriptAddress())
|
|
buf.WriteByte(0x87) // OP_EQUAL
|
|
result.IsScript = true
|
|
case *btcutil.AddressWitnessPubKeyHash:
|
|
buf.WriteByte(0x00) // OP_0
|
|
buf.Write(a.ScriptAddress())
|
|
result.IsWitness = true
|
|
program := hex.EncodeToString(a.WitnessProgram())
|
|
result.WitnessProgram = &program
|
|
result.WitnessVersion = int(a.WitnessVersion())
|
|
case *btcutil.AddressWitnessScriptHash:
|
|
buf.WriteByte(0x00) // OP_0
|
|
buf.Write(a.ScriptAddress())
|
|
result.IsScript = true
|
|
result.IsWitness = true
|
|
program := hex.EncodeToString(a.WitnessProgram())
|
|
result.WitnessProgram = &program
|
|
result.WitnessVersion = int(a.WitnessVersion())
|
|
}
|
|
result.ScriptPubKey = hex.EncodeToString(buf.Bytes())
|
|
|
|
ainfo, err := w.AddressInfo(addr)
|
|
if err != nil {
|
|
if waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) {
|
|
return result, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
result.IsMine = true
|
|
result.IsChange = ainfo.Internal()
|
|
isCompressed := ainfo.Compressed()
|
|
result.IsCompressed = &isCompressed
|
|
|
|
switch ma := ainfo.(type) {
|
|
case waddrmgr.ManagedPubKeyAddress:
|
|
pubKey := ma.ExportPubKey()
|
|
result.PubKey = &pubKey
|
|
|
|
scope, path, isHD := ma.DerivationInfo()
|
|
if isHD {
|
|
|
|
hdPath := fmt.Sprintf("%s/%d/%d/%d",
|
|
scope.String(), path.Account, path.Branch, path.Index)
|
|
if path.Account >= 0x80000000 {
|
|
hdPath = fmt.Sprintf("%s/%d'/%d/%d",
|
|
scope.String(), path.Account-0x80000000, path.Branch, path.Index)
|
|
|
|
}
|
|
result.HDKeyPath = &hdPath
|
|
}
|
|
|
|
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
|
|
}
|
|
hexScript := hex.EncodeToString(script)
|
|
result.Hex = &hexScript
|
|
|
|
// 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 {
|
|
class = txscript.NonStandardTy
|
|
result.ScriptType = &class
|
|
break
|
|
}
|
|
addrStrings := make([]string, len(addrs))
|
|
for i, a := range addrs {
|
|
addrStrings[i] = a.EncodeAddress()
|
|
}
|
|
result.PubKeys = &addrStrings
|
|
|
|
// Multi-signature scripts also provide the number of required
|
|
// signatures.
|
|
result.ScriptType = &class
|
|
if class == txscript.MultiSigTy {
|
|
result.SignaturesRequired = &reqSigs
|
|
}
|
|
}
|
|
// type embeddedAddressInfo struct {
|
|
// # Address string `json:"address"`
|
|
// # ScriptPubKey string `json:"scriptPubKey"`
|
|
// Descriptor *string `json:"desc,omitempty"`
|
|
// IsScript bool `json:"isscript"`
|
|
// IsChange bool `json:"ischange"`
|
|
// IsWitness bool `json:"iswitness"`
|
|
// WitnessVersion int `json:"witness_version,omitempty"`
|
|
// WitnessProgram *string `json:"witness_program,omitempty"`
|
|
// ScriptType *txscript.ScriptClass `json:"script,omitempty"`
|
|
// Hex *string `json:"hex,omitempty"`
|
|
// PubKeys *[]string `json:"pubkeys,omitempty"`
|
|
// SignaturesRequired *int `json:"sigsrequired,omitempty"`
|
|
// PubKey *string `json:"pubkey,omitempty"`
|
|
// IsCompressed *bool `json:"iscompressed,omitempty"`
|
|
// HDMasterFingerprint *string `json:"hdmasterfingerprint,omitempty"`
|
|
// Labels []string `json:"labels"`
|
|
// }
|
|
|
|
// type GetAddressInfoResult struct {
|
|
// embeddedAddressInfo
|
|
// IsMine bool `json:"ismine"`
|
|
// IsWatchOnly bool `json:"iswatchonly"`
|
|
// Timestamp *int `json:"timestamp,omitempty"`
|
|
// HDKeyPath *string `json:"hdkeypath,omitempty"`
|
|
// HDSeedID *string `json:"hdseedid,omitempty"`
|
|
// Embedded *embeddedAddressInfo `json:"embedded,omitempty"`
|
|
// }
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// 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)
|
|
|
|
if *cmd.Account == "*" {
|
|
balance, _, err := w.CalculateBalance(int32(*cmd.MinConf))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return balance.ToBTC(), nil
|
|
}
|
|
|
|
account, err := w.AccountNumber(*cmd.Account)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bals, err := w.CalculateAccountBalances(account, int32(*cmd.MinConf))
|
|
|
|
return bals.Spendable.ToBTC(), err
|
|
}
|
|
|
|
// 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 lbcwallet.
|
|
// exist.
|
|
func getInfo(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCClient) (interface{}, error) {
|
|
// Call down to for all of the information in this command known
|
|
// by them.
|
|
info, err := chainClient.GetInfo()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bal, staked, 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.Staked = staked.ToBTC()
|
|
info.PaytxFee = 0
|
|
// 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.AccountOfAddress(addr)
|
|
if err != nil {
|
|
return nil, &ErrAddressNotInWallet
|
|
}
|
|
|
|
acctName, err := w.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 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.AccountNumber(*cmd.Account)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
scope, err := lookupKeyScope(cmd.AddressType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
addr, err := w.CurrentAddress(account, *scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return addr.EncodeAddress(), nil
|
|
}
|
|
|
|
// 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)
|
|
|
|
account, err := w.AccountNumber(*cmd.Account)
|
|
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(waddrmgr.KeyScopeBIP0044, 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
|
|
}
|
|
|
|
fn := func(scope waddrmgr.KeyScope) error {
|
|
_, err := w.NextAccount(scope, cmd.Account)
|
|
return err
|
|
}
|
|
err := forEachKeyScope(fn)
|
|
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, nil
|
|
}
|
|
|
|
// renameAccount handles a renameaccount request by renaming an account.
|
|
// If the account does not exist an appropriate 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.AccountNumber(cmd.OldAccount)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Interate over all key scopes and rename the account.
|
|
fn := func(scope waddrmgr.KeyScope) error {
|
|
return w.RenameAccount(scope, account, cmd.NewAccount)
|
|
}
|
|
err = forEachKeyScope(fn)
|
|
|
|
return nil, err
|
|
}
|
|
|
|
func lookupKeyScope(kind *string) (*waddrmgr.KeyScope, error) {
|
|
if kind == nil {
|
|
return nil, nil
|
|
}
|
|
switch strings.ToLower(*kind) {
|
|
case "*":
|
|
return nil, nil
|
|
case "legacy":
|
|
return &waddrmgr.KeyScopeBIP0044, nil
|
|
case "p2sh-segwit":
|
|
return &waddrmgr.KeyScopeBIP0049, nil
|
|
case "bech32":
|
|
return &waddrmgr.KeyScopeBIP0084, nil
|
|
}
|
|
return &waddrmgr.KeyScopeBIP0044, fmt.Errorf("unrecognized address type: %s. Must be legacy, p2sh-segwit, or bech32", *kind)
|
|
}
|
|
|
|
// getNewAddress handles a getnewaddress request by returning a new
|
|
// address for an account. If the account does not exist an appropriate
|
|
// 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 := defaultAccountName
|
|
if cmd.Account != nil && len(*cmd.Account) > 0 {
|
|
acctName = *cmd.Account
|
|
}
|
|
|
|
account, err := w.AccountNumber(acctName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
scope, err := lookupKeyScope(cmd.AddressType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
addr, err := w.NewAddress(account, *scope)
|
|
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)
|
|
|
|
account, err := w.AccountNumber(*cmd.Account)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Use specified scope, if provided.
|
|
scope, err := lookupKeyScope(cmd.AddressType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if scope == nil {
|
|
scope = &waddrmgr.DefaultKeyScope
|
|
}
|
|
|
|
addr, err := w.NewChangeAddress(account, *scope)
|
|
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.AccountNumber(*cmd.Account)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
totalReceived := 0.0
|
|
|
|
var results []wallet.AccountTotalReceivedResult
|
|
|
|
fn := func(scope waddrmgr.KeyScope) error {
|
|
|
|
results, err = w.TotalReceivedForAccounts(
|
|
scope, int32(*cmd.MinConf))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
acctIndex := int(account)
|
|
if account == waddrmgr.ImportedAddrAccount {
|
|
acctIndex = len(results) - 1
|
|
}
|
|
|
|
totalReceived += results[acctIndex].TotalReceived.ToBTC()
|
|
|
|
return nil
|
|
}
|
|
err = forEachKeyScope(fn)
|
|
|
|
return totalReceived, err
|
|
}
|
|
|
|
// 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)
|
|
|
|
txHash, err := chainhash.NewHashFromStr(cmd.Txid)
|
|
if err != nil {
|
|
return nil, &btcjson.RPCError{
|
|
Code: btcjson.ErrRPCDecodeHexString,
|
|
Message: "Transaction hash string decode failed: " + err.Error(),
|
|
}
|
|
}
|
|
|
|
details, err := wallet.UnstableAPI(w).TxDetails(txHash)
|
|
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
|
|
}
|
|
|
|
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, w.ChainParams()).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.AccountOfAddress(addr)
|
|
if err == nil {
|
|
name, err := w.AccountName(account)
|
|
if err == nil {
|
|
accountName = name
|
|
}
|
|
}
|
|
}
|
|
|
|
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 appropriate function is looked up when help text is first needed
|
|
// using the current locale and saved to the global below for further 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{}, _ *wallet.Wallet, chainClient *chain.RPCClient) (interface{}, error) {
|
|
cmd := icmd.(*btcjson.HelpCmd)
|
|
|
|
// returns different help messages depending on the kind of
|
|
// connection the client is using. Only methods available to HTTP POST
|
|
// clients are available to be used by wallet clients, even though
|
|
// wallet itself is a websocket client to . 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 package.
|
|
postClient := func() *rpcclient.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]map[string]float64{}
|
|
|
|
fn := func(scope waddrmgr.KeyScope) error {
|
|
results, err := w.AccountBalances(scope, int32(*cmd.MinConf))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, result := range results {
|
|
if accountBalances[result.AccountName] == nil {
|
|
accountBalances[result.AccountName] = map[string]float64{}
|
|
}
|
|
accountBalances[result.AccountName][scope.String()] += result.AccountBalance.ToBTC()
|
|
accountBalances[result.AccountName]["total"] += result.AccountBalance.ToBTC()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
scope, err := lookupKeyScope(cmd.AddressType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if scope != nil {
|
|
err = fn(*scope)
|
|
} else {
|
|
err = forEachKeyScope(fn)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Return the map. This will be marshaled into a JSON object.
|
|
return accountBalances, err
|
|
}
|
|
|
|
// 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)
|
|
|
|
accounts := map[uint32]*wallet.AccountTotalReceivedResult{}
|
|
err := forEachKeyScope(func(scope waddrmgr.KeyScope) error {
|
|
results, err := w.TotalReceivedForAccounts(
|
|
scope, int32(*cmd.MinConf),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, result := range results {
|
|
account := accounts[result.AccountNumber]
|
|
if account == nil {
|
|
dup := result
|
|
account = &dup
|
|
accounts[result.AccountNumber] = account
|
|
}
|
|
account.TotalReceived += result.TotalReceived
|
|
if account.LastConfirmation < result.LastConfirmation {
|
|
account.LastConfirmation = result.LastConfirmation
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
jsonResults := make([]btcjson.ListReceivedByAccountResult, 0, len(accounts))
|
|
for _, account := range accounts {
|
|
jsonResult := btcjson.ListReceivedByAccountResult{
|
|
Account: account.AccountName,
|
|
Amount: account.TotalReceived.ToBTC(),
|
|
Confirmations: uint64(account.LastConfirmation),
|
|
}
|
|
jsonResults = append(jsonResults, jsonResult)
|
|
}
|
|
|
|
return jsonResults, 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 = wallet.UnstableAPI(w).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)
|
|
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 := chainhash.NewHashFromStr(*cmd.BlockHash)
|
|
if err != nil {
|
|
return nil, DeserializationError{err}
|
|
}
|
|
block, err := chainClient.GetBlockVerboseTx(hash)
|
|
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)
|
|
|
|
return w.ListTransactions(*cmd.Account, *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)
|
|
|
|
// 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(*cmd.Account, 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)
|
|
|
|
return w.ListAllTransactions(*cmd.Account)
|
|
}
|
|
|
|
// listUnspent handles the listunspent command.
|
|
func listUnspent(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
|
|
cmd := icmd.(*btcjson.ListUnspentCmd)
|
|
|
|
if cmd.Addresses != nil && len(*cmd.Addresses) > 0 {
|
|
return nil, &btcjson.RPCError{
|
|
Code: btcjson.ErrRPCInvalidParameter,
|
|
Message: "Filtering by addresses has been deprecated",
|
|
}
|
|
}
|
|
|
|
return w.ListUnspent(int32(*cmd.MinConf), int32(*cmd.MaxConf), "")
|
|
}
|
|
|
|
// 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 {
|
|
txHash, err := chainhash.NewHashFromStr(input.Txid)
|
|
if err != nil {
|
|
return nil, ParseError{err}
|
|
}
|
|
op := wire.OutPoint{Hash: *txHash, Index: input.Vout}
|
|
if cmd.Unlock {
|
|
w.UnlockOutpoint(op)
|
|
} else {
|
|
w.LockOutpoint(op)
|
|
}
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// makeOutputs creates a slice of transaction outputs from a pair of address
|
|
// strings to amounts. This is used to create the outputs to include in newly
|
|
// created transactions from a JSON object describing the output destinations
|
|
// and amounts.
|
|
func makeOutputs(pairs map[string]btcutil.Amount, chainParams *chaincfg.Params) ([]*wire.TxOut, error) {
|
|
outputs := make([]*wire.TxOut, 0, len(pairs))
|
|
for addrStr, amt := range pairs {
|
|
addr, err := btcutil.DecodeAddress(addrStr, chainParams)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot decode address: %s", err)
|
|
}
|
|
|
|
pkScript, err := txscript.PayToAddrScript(addr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot create txout script: %s", err)
|
|
}
|
|
|
|
outputs = append(outputs, wire.NewTxOut(int64(amt), pkScript))
|
|
}
|
|
return outputs, nil
|
|
}
|
|
|
|
// rescanBlockchain handles a rescanblockhain RPC request.
|
|
func rescanBlockchain(icmd interface{}, w *wallet.Wallet,
|
|
chainClient *chain.RPCClient) (interface{}, error) {
|
|
|
|
cmd := icmd.(*btcjson.RescanBlockchainCmd)
|
|
|
|
_, bestHeight, err := chainClient.GetBestBlock()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
startHeight := *cmd.StartHeight
|
|
if startHeight < 0 || startHeight > bestHeight {
|
|
return nil, &btcjson.RPCError{
|
|
Code: btcjson.ErrRPCInvalidParameter,
|
|
Message: "Invalid start height",
|
|
}
|
|
}
|
|
|
|
// Scan to the best block if no stopHeight is specified.
|
|
stopHeight := bestHeight
|
|
if cmd.StopHeight != nil {
|
|
stopHeight = *cmd.StopHeight
|
|
}
|
|
if stopHeight < 0 || stopHeight > bestHeight {
|
|
return nil, &btcjson.RPCError{
|
|
Code: btcjson.ErrRPCInvalidParameter,
|
|
Message: "Invalid stop height",
|
|
}
|
|
}
|
|
|
|
startHeight, stopHeight, err = w.RescanBlockchain(chainClient,
|
|
startHeight, stopHeight)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("rescanblockchain: %w", err)
|
|
}
|
|
ret := btcjson.RescanBlockchainResult{
|
|
StartHeight: startHeight,
|
|
StoptHeight: stopHeight,
|
|
}
|
|
return ret, 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,
|
|
keyScope *waddrmgr.KeyScope, account uint32, minconf int32,
|
|
feeSatPerKb btcutil.Amount) (string, error) {
|
|
|
|
outputs, err := makeOutputs(amounts, w.ChainParams())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
tx, err := w.SendOutputs(
|
|
outputs, keyScope, account, minconf, feeSatPerKb,
|
|
wallet.CoinSelectionLargest, "",
|
|
)
|
|
if err != nil {
|
|
if err == txrules.ErrAmountNegative {
|
|
return "", ErrNeedPositiveAmount
|
|
}
|
|
if waddrmgr.IsError(err, waddrmgr.ErrLocked) {
|
|
return "", &ErrWalletUnlockNeeded
|
|
}
|
|
if _, ok := err.(btcjson.RPCError); ok {
|
|
return "", err
|
|
}
|
|
|
|
return "", &btcjson.RPCError{
|
|
Code: btcjson.ErrRPCInternal.Code,
|
|
Message: err.Error(),
|
|
}
|
|
}
|
|
|
|
txHashStr := tx.TxHash().String()
|
|
log.Infof("Successfully sent transaction %v", txHashStr)
|
|
return txHashStr, 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.AccountNumber(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,
|
|
}
|
|
|
|
// Use specified scope, if provided.
|
|
scope, err := lookupKeyScope(cmd.AddressType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return sendPairs(w, pairs, scope, account, minConf,
|
|
txrules.DefaultRelayFeePerKb)
|
|
}
|
|
|
|
// 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.AccountNumber(cmd.FromAccount)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check that minconf is positive.
|
|
minConf := int32(*cmd.MinConf)
|
|
if minConf < 0 {
|
|
return nil, ErrNeedPositiveMinconf
|
|
}
|
|
|
|
// Use specified scope, if provided.
|
|
scope, err := lookupKeyScope(cmd.AddressType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Recreate address/amount pairs, using dcrutil.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, scope, account, minConf, txrules.DefaultRelayFeePerKb)
|
|
}
|
|
|
|
// 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,
|
|
}
|
|
|
|
// Use specified scope, if provided.
|
|
scope, err := lookupKeyScope(cmd.AddressType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// sendtoaddress always spends from the default account, this matches bitcoind
|
|
return sendPairs(w, pairs, scope, waddrmgr.DefaultAccountNum, 1,
|
|
txrules.DefaultRelayFeePerKb)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
privKey, err := w.PrivKeyForAddress(addr)
|
|
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 := chainhash.DoubleHashB(buf.Bytes())
|
|
sigbytes, err := btcec.SignCompact(btcec.S256(), privKey,
|
|
messageHash, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return base64.StdEncoding.EncodeToString(sigbytes), nil
|
|
}
|
|
|
|
// 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
|
|
}
|
|
var tx wire.MsgTx
|
|
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 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 {
|
|
inputHash, err := chainhash.NewHashFromStr(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: *inputHash,
|
|
Index: rti.Vout,
|
|
}] = script
|
|
}
|
|
|
|
// Now we go and look for any inputs that we were not provided by
|
|
// querying 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.OutPoint]rpcclient.FutureGetTxOutResult)
|
|
for _, txIn := range tx.TxIn {
|
|
// Did we get this outpoint from the arguments?
|
|
if _, ok := inputs[txIn.PreviousOutPoint]; ok {
|
|
continue
|
|
}
|
|
|
|
// Asynchronously request the output script.
|
|
requested[txIn.PreviousOutPoint] = chainClient.GetTxOutAsync(
|
|
&txIn.PreviousOutPoint.Hash, txIn.PreviousOutPoint.Index,
|
|
true)
|
|
}
|
|
|
|
// 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 outPoint, resp := range requested {
|
|
result, err := resp.Receive()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if result == nil {
|
|
return nil, &btcjson.RPCError{
|
|
Code: btcjson.ErrRPCNoTxInfo,
|
|
Message: "Input %s not found" + outPoint.String(),
|
|
}
|
|
}
|
|
script, err := hex.DecodeString(result.ScriptPubKey.Hex)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
inputs[outPoint] = script
|
|
}
|
|
|
|
// 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) encountered 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.AddressInfo(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.AccountName(ainfo.InternalAccount())
|
|
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 := chainhash.DoubleHashB(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.
|
|
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
|
|
}
|
|
|
|
// forEachKeyScope calls the given function with each default key scopes
|
|
// breaking early on error.
|
|
func forEachKeyScope(fn func(scope waddrmgr.KeyScope) error) error {
|
|
|
|
for _, scope := range waddrmgr.DefaultKeyScopes {
|
|
err := fn(scope)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|