// 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/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}, "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}, "move": {handler: unsupported, noHelp: true}, "setaccount": {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. func getAddressesByAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) { cmd := icmd.(*btcjson.GetAddressesByAccountCmd) account, err := w.AccountNumber(waddrmgr.KeyScopeBIP0044, cmd.Account) if err != nil { return nil, err } addrs, err := w.AccountAddresses(account) 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 { if waddrmgr.IsError(err, waddrmgr.ErrWatchingOnly) { result.IsWatchOnly = true } 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) 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.AccountNumber(waddrmgr.KeyScopeBIP0044, 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 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 = float64(txrules.DefaultRelayFeePerKb) // 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(waddrmgr.KeyScopeBIP0044, 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(waddrmgr.KeyScopeBIP0044, cmd.Account) if err != nil { return nil, err } addr, err := w.CurrentAddress(account, waddrmgr.KeyScopeBIP0044) 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 := defaultAccountName if cmd.Account != nil { acctName = *cmd.Account } account, err := w.AccountNumber(waddrmgr.KeyScopeBIP0044, 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(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 } _, err := w.NextAccount(waddrmgr.KeyScopeBIP0044, 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 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(waddrmgr.KeyScopeBIP0044, cmd.OldAccount) if err != nil { return nil, err } return nil, w.RenameAccount(waddrmgr.KeyScopeBIP0044, account, cmd.NewAccount) } func lookupKeyScope(kind *string) (waddrmgr.KeyScope, error) { if kind == nil { return waddrmgr.KeyScopeBIP0044, nil } switch strings.ToLower(*kind) { case "legacy", "p2pkh": // could add "default" but it might be confused with the 1st parameter return waddrmgr.KeyScopeBIP0044, nil case "p2sh-p2wpkh", "p2sh-p2wkh", "p2sh-segwit": return waddrmgr.KeyScopeBIP0049Plus, nil case "p2wpkh", "p2wkh", "bech32": return waddrmgr.KeyScopeBIP0084, nil } return waddrmgr.KeyScopeBIP0044, fmt.Errorf("unrecognized address type: %s", *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 } keyScope, err := lookupKeyScope(cmd.AddressType) if err != nil { return nil, err } account, err := w.AccountNumber(keyScope, acctName) if err != nil { return nil, err } addr, err := w.NewAddress(account, keyScope) 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 := defaultAccountName if cmd.Account != nil { acctName = *cmd.Account } account, err := w.AccountNumber(waddrmgr.KeyScopeBIP0044, acctName) if err != nil { return nil, err } addr, err := w.NewChangeAddress(account, waddrmgr.KeyScopeBIP0044) 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(waddrmgr.KeyScopeBIP0044, cmd.Account) if err != nil { return nil, err } // TODO: This is more inefficient that it could be, but the entire // algorithm is already dominated by reading every transaction in the // wallet's history. results, err := w.TotalReceivedForAccounts( waddrmgr.KeyScopeBIP0044, int32(*cmd.MinConf), ) if err != nil { return nil, err } acctIndex := int(account) if account == waddrmgr.ImportedAddrAccount { acctIndex = len(results) - 1 } return results[acctIndex].TotalReceived.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) 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 } // 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, 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(waddrmgr.KeyScopeBIP0044, 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]float64{} results, err := w.AccountBalances(waddrmgr.KeyScopeBIP0044, int32(*cmd.MinConf)) if err != nil { return nil, err } for _, result := range results { accountBalances[result.AccountName] = result.AccountBalance.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) results, err := w.TotalReceivedForAccounts( waddrmgr.KeyScopeBIP0044, int32(*cmd.MinConf), ) if err != nil { return nil, err } jsonResults := make([]btcjson.ListReceivedByAccountResult, 0, len(results)) for _, result := range results { jsonResults = append(jsonResults, btcjson.ListReceivedByAccountResult{ Account: result.AccountName, Amount: result.TotalReceived.ToBTC(), Confirmations: uint64(result.LastConfirmation), }) } 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, Account: addrData.account, } 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) // 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) 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 } // 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( waddrmgr.KeyScopeBIP0044, 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, waddrmgr.KeyScopeBIP0044, 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(waddrmgr.KeyScopeBIP0044, 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 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, waddrmgr.KeyScopeBIP0044, 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, } // sendtoaddress always spends from the default account, this matches bitcoind return sendPairs(w, pairs, waddrmgr.KeyScopeBIP0044, 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 } 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( waddrmgr.KeyScopeBIP0044, 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 (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.ChangePrivatePassphrase([]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 }