From 9661a7c6dea4c5563f61619cc2ec84b0eecc28a8 Mon Sep 17 00:00:00 2001 From: Wisdom Arerosuoghene Date: Thu, 20 Aug 2020 11:46:42 +0100 Subject: [PATCH] rpc: implement gettxout JSON-RPC method for SPV mode In RPC mode, the method performs passthrough manually, to keep existing behavior. In SPV mode, only wallet-related unspent outputs can be looked up currently. --- internal/rpc/jsonrpc/methods.go | 108 ++++++++++++++++++++++++++ internal/rpc/jsonrpc/rpcserverhelp.go | 3 +- internal/rpc/rpcserver/server.go | 2 +- internal/rpchelp/helpdescs_en_US.go | 23 ++++++ internal/rpchelp/methods.go | 1 + rpc/jsonrpc/types/methods.go | 2 + wallet/udb/txmined.go | 22 ++++++ wallet/wallet.go | 15 ++++ 8 files changed, 174 insertions(+), 2 deletions(-) diff --git a/internal/rpc/jsonrpc/methods.go b/internal/rpc/jsonrpc/methods.go index a9d2467b6..36ebdb43a 100644 --- a/internal/rpc/jsonrpc/methods.go +++ b/internal/rpc/jsonrpc/methods.go @@ -104,6 +104,7 @@ var handlers = map[string]handler{ "getstakeinfo": {fn: (*Server).getStakeInfo}, "gettickets": {fn: (*Server).getTickets}, "gettransaction": {fn: (*Server).getTransaction}, + "gettxout": {fn: (*Server).getTxOut}, "getunconfirmedbalance": {fn: (*Server).getUnconfirmedBalance}, "getvotechoices": {fn: (*Server).getVoteChoices}, "getwalletfee": {fn: (*Server).getWalletFee}, @@ -2049,6 +2050,113 @@ func (s *Server) getTransaction(ctx context.Context, icmd interface{}) (interfac return ret, nil } +// getTxOut handles a gettxout request by returning details about an unspent +// output. In SPV mode, details are only returned for transaction outputs that +// are relevant to the wallet. +// To match the behavior in RPC mode, (nil, nil) is returned if the transaction +// output could not be found (never existed or was pruned) or is spent by another +// transaction already in the main chain. Mined transactions that are spent by +// a mempool transaction are not affected by this. +func (s *Server) getTxOut(ctx context.Context, icmd interface{}) (interface{}, error) { + cmd := icmd.(*types.GetTxOutCmd) + w, ok := s.walletLoader.LoadedWallet() + if !ok { + return nil, errUnloadedWallet + } + + // Attempt RPC passthrough if connected to DCRD. + n, err := w.NetworkBackend() + if err != nil { + return nil, err + } + if rpc, ok := n.(*dcrd.RPC); ok { + var resp json.RawMessage + err := rpc.Call(ctx, "gettxout", &resp, cmd.Txid, cmd.Vout, cmd.Tree, cmd.IncludeMempool) + return resp, err + } + + txHash, err := chainhash.NewHashFromStr(cmd.Txid) + if err != nil { + return nil, rpcError(dcrjson.ErrRPCDecodeHexString, err) + } + + if cmd.Tree != wire.TxTreeRegular && cmd.Tree != wire.TxTreeStake { + return nil, rpcErrorf(dcrjson.ErrRPCInvalidParameter, "Tx tree must be regular or stake") + } + + // Check if the transaction is known to wallet. + walletTx, err := wallet.UnstableAPI(w).TxDetails(ctx, txHash) + if err != nil && !errors.Is(err, errors.NotExist) { + return nil, err + } + if walletTx == nil { + return nil, nil // Tx not found in wallet. + } + if len(walletTx.MsgTx.TxOut) <= int(cmd.Vout) { + return nil, rpcErrorf(dcrjson.ErrRPCInvalidTxVout, "invalid vout %d", cmd.Vout) + } + + tx := &walletTx.MsgTx + txTree := wire.TxTreeRegular + if stake.DetermineTxType(tx, true) != stake.TxTypeRegular { + txTree = wire.TxTreeStake + } + if txTree != cmd.Tree { + // Not an error because it is technically possible (though extremely unlikely) + // that the required tx (same hash, different tree) exists on the blockchain. + return nil, nil + } + + // Attempt to read the unspent txout info from wallet. + outpoint := wire.OutPoint{Hash: *txHash, Index: cmd.Vout, Tree: cmd.Tree} + walletUnspent, err := w.UnspentOutput(ctx, outpoint, *cmd.IncludeMempool) + if err != nil && !errors.Is(err, errors.NotExist) { + return nil, err + } + if walletUnspent == nil { + return nil, nil // output is spent + } + + txout := tx.TxOut[cmd.Vout] + pkScript := txout.PkScript + scriptVersion := txout.Version + + // Disassemble script into single line printable format. The + // disassembled string will contain [error] inline if the script + // doesn't fully parse, so ignore the error here. + disbuf, _ := txscript.DisasmString(pkScript) + + // Get further info about the script. Ignore the error here since an + // error means the script couldn't parse and there is no additional + // information about it anyways. + scriptClass, addrs, reqSigs, _ := txscript.ExtractPkScriptAddrs( + scriptVersion, pkScript, s.activeNet, true) // Yes treasury + addresses := make([]string, len(addrs)) + for i, addr := range addrs { + addresses[i] = addr.Address() + } + + bestHash, bestHeight := w.MainChainTip(ctx) + var confirmations int64 + if walletTx.Block.Height != -1 { + confirmations = int64(1 + bestHeight - walletTx.Block.Height) + } + + return &dcrdtypes.GetTxOutResult{ + BestBlock: bestHash.String(), + Confirmations: confirmations, + Value: dcrutil.Amount(txout.Value).ToUnit(dcrutil.AmountCoin), + ScriptPubKey: dcrdtypes.ScriptPubKeyResult{ + Asm: disbuf, + Hex: hex.EncodeToString(pkScript), + ReqSigs: int32(reqSigs), + Type: scriptClass.String(), + Addresses: addresses, + }, + Coinbase: blockchain.IsCoinBaseTx(tx, false) || blockchain.IsCoinBaseTx(tx, true), + }, nil +} + // getVoteChoices handles a getvotechoices request by returning configured vote // preferences for each agenda of the latest supported stake version. func (s *Server) getVoteChoices(ctx context.Context, icmd interface{}) (interface{}, error) { diff --git a/internal/rpc/jsonrpc/rpcserverhelp.go b/internal/rpc/jsonrpc/rpcserverhelp.go index 413b67db8..fd522a3e4 100644 --- a/internal/rpc/jsonrpc/rpcserverhelp.go +++ b/internal/rpc/jsonrpc/rpcserverhelp.go @@ -39,6 +39,7 @@ func helpDescsEnUS() map[string]string { "getstakeinfo": "getstakeinfo\n\nReturns statistics about staking from the wallet.\n\nArguments:\nNone\n\nResult:\n{\n \"blockheight\": n, (numeric) Current block height for stake info.\n \"difficulty\": n.nnn, (numeric) Current stake difficulty.\n \"totalsubsidy\": n.nnn, (numeric) Total amount of coins earned by proof-of-stake voting\n \"ownmempooltix\": n, (numeric) Number of tickets submitted by this wallet currently in mempool\n \"immature\": n, (numeric) Number of tickets from this wallet that are in the blockchain but which are not yet mature\n \"unspent\": n, (numeric) Number of unspent tickets\n \"voted\": n, (numeric) Number of votes cast by this wallet\n \"revoked\": n, (numeric) Number of missed tickets that were missed and then revoked\n \"unspentexpired\": n, (numeric) Number of unspent tickets which are past expiry\n \"poolsize\": n, (numeric) Number of live tickets in the ticket pool.\n \"allmempooltix\": n, (numeric) Number of tickets currently in the mempool\n \"live\": n, (numeric) Number of mature, active tickets owned by this wallet\n \"proportionlive\": n.nnn, (numeric) (Live / PoolSize)\n \"missed\": n, (numeric) Number of missed tickets (failure to vote, not including expired)\n \"proportionmissed\": n.nnn, (numeric) (Missed / (Missed + Voted))\n \"expired\": n, (numeric) Number of tickets that have expired\n} \n", "gettickets": "gettickets includeimmature\n\nReturning the hashes of the tickets currently owned by wallet.\n\nArguments:\n1. includeimmature (boolean, required) If true include immature tickets in the results.\n\nResult:\n{\n \"hashes\": [\"value\",...], (array of string) Hashes of the tickets owned by the wallet encoded as strings\n} \n", "gettransaction": "gettransaction \"txid\" (includewatchonly=false)\n\nReturns a JSON object with details regarding a transaction relevant to this wallet.\n\nArguments:\n1. txid (string, required) Hash of the transaction to query\n2. includewatchonly (boolean, optional, default=false) Also consider transactions involving watched addresses\n\nResult:\n{\n \"amount\": n.nnn, (numeric) The total amount this transaction credits to the wallet, valued in decred\n \"fee\": n.nnn, (numeric) The total input value minus the total output value, or 0 if 'txid' is not a sent transaction\n \"confirmations\": n, (numeric) The number of block confirmations of the transaction\n \"blockhash\": \"value\", (string) The hash of the block this transaction is mined in, or the empty string if unmined\n \"blockindex\": n, (numeric) Unset\n \"blocktime\": n, (numeric) The Unix time of the block header this transaction is mined in, or 0 if unmined\n \"txid\": \"value\", (string) The transaction hash\n \"walletconflicts\": [\"value\",...], (array of string) Unset\n \"time\": n, (numeric) The earliest Unix time this transaction was known to exist\n \"timereceived\": n, (numeric) The earliest Unix time this transaction was known to exist\n \"details\": [{ (array of object) Additional details for each recorded wallet credit and debit\n \"account\": \"value\", (string) DEPRECATED -- Unset\n \"address\": \"value\", (string) The address an output was paid to, or the empty string if the output is nonstandard or this detail is regarding a transaction input\n \"amount\": n.nnn, (numeric) The amount of a received output\n \"category\": \"value\", (string) The kind of detail: \"send\" for sent transactions, \"immature\" for immature coinbase outputs, \"generate\" for mature coinbase outputs, or \"recv\" for all other received outputs\n \"involveswatchonly\": true|false, (boolean) Unset\n \"fee\": n.nnn, (numeric) The included fee for a sent transaction\n \"vout\": n, (numeric) The transaction output index\n },...], \n \"hex\": \"value\", (string) The transaction encoded as a hexadecimal string\n \"type\": \"value\", (string) The type of transaction (regular, ticket, vote, or revocation)\n \"ticketstatus\": \"value\", (string) Status of ticket (if transaction is a ticket)\n} \n", + "gettxout": "gettxout \"txid\" vout tree (includemempool=true)\n\nReturns information about an unspent transaction output.\n\nArguments:\n1. txid (string, required) The hash of the transaction\n2. vout (numeric, required) The index of the output\n3. tree (numeric, required) The tree of the transaction\n4. includemempool (boolean, optional, default=true) Include the mempool when true\n\nResult:\n{\n \"bestblock\": \"value\", (string) The block hash that contains the transaction output\n \"confirmations\": n, (numeric) The number of confirmations\n \"value\": n.nnn, (numeric) The transaction amount in DCR\n \"scriptPubKey\": { (object) The public key script used to pay coins as a JSON object\n \"asm\": \"value\", (string) Disassembly of the script\n \"hex\": \"value\", (string) Hex-encoded bytes of the script\n \"reqSigs\": n, (numeric) The number of required signatures\n \"type\": \"value\", (string) The type of the script (e.g. 'pubkeyhash')\n \"addresses\": [\"value\",...], (array of string) The Decred addresses associated with this script\n \"commitamt\": n.nnn, (numeric) The ticket commitment value if the script is for a staking commitment\n }, \n \"coinbase\": true|false, (boolean) Whether or not the transaction is a coinbase\n} \n", "getunconfirmedbalance": "getunconfirmedbalance (\"account\")\n\nCalculates the unspent output value of all unmined transaction outputs for an account.\n\nArguments:\n1. account (string, optional) The account to query the unconfirmed balance for (default=\"default\")\n\nResult:\nn.nnn (numeric) Total amount of all unmined unspent outputs of the account valued in decred.\n", "getvotechoices": "getvotechoices (\"tickethash\")\n\nRetrieve the currently configured default vote choices for the latest supported stake agendas\n\nArguments:\n1. tickethash (string, optional) The hash of the ticket to return vote choices for. If the ticket has no choices set, the default vote choices are returned\n\nResult:\n{\n \"version\": n, (numeric) The latest stake version supported by the software and the version of the included agendas\n \"choices\": [{ (array of object) The currently configured agenda vote choices, including abstaining votes\n \"agendaid\": \"value\", (string) The ID for the agenda the choice concerns\n \"agendadescription\": \"value\", (string) A description of the agenda the choice concerns\n \"choiceid\": \"value\", (string) The ID of the current choice for this agenda\n \"choicedescription\": \"value\", (string) A description of the current choice for this agenda\n },...], \n} \n", "getwalletfee": "getwalletfee\n\nGet currently set transaction fee for the wallet\n\nArguments:\nNone\n\nResult:\nn.nnn (numeric) Current tx fee (in DCR)\n", @@ -105,4 +106,4 @@ var localeHelpDescs = map[string]func() map[string]string{ "en_US": helpDescsEnUS, } -var requestUsages = "abandontransaction \"hash\"\naccountaddressindex \"account\" branch\naccountsyncaddressindex \"account\" branch index\naddmultisigaddress nrequired [\"key\",...] (\"account\")\naddtransaction \"blockhash\" \"transaction\"\nauditreuse (since)\nconsolidate inputs (\"account\" \"address\")\ncreatemultisig nrequired [\"key\",...]\ncreatenewaccount \"account\"\ncreaterawtransaction [{\"amount\":n.nnn,\"txid\":\"value\",\"vout\":n,\"tree\":n},...] {\"address\":amount,...} (locktime expiry)\ncreatesignature \"address\" inputindex hashtype \"previouspkscript\" \"serializedtransaction\"\ndiscoverusage (\"startblock\" discoveraccounts gaplimit)\ndumpprivkey \"address\"\nfundrawtransaction \"hexstring\" \"fundaccount\" ({\"changeaddress\":changeaddress,\"feerate\":feerate,\"conftarget\":conftarget})\ngeneratevote \"blockhash\" height \"tickethash\" votebits \"votebitsext\"\ngetaccount \"address\"\ngetaccountaddress \"account\"\ngetaddressesbyaccount \"account\"\ngetbalance (\"account\" minconf=1)\ngetbestblock\ngetbestblockhash\ngetblockcount\ngetblockhash index\ngetcoinjoinsbyacct\ngetinfo\ngetmasterpubkey (\"account\")\ngetmultisigoutinfo \"hash\" index\ngetnewaddress (\"account\" \"gappolicy\")\ngetpeerinfo\ngetrawchangeaddress (\"account\")\ngetreceivedbyaccount \"account\" (minconf=1)\ngetreceivedbyaddress \"address\" (minconf=1)\ngetstakeinfo\ngettickets includeimmature\ngettransaction \"txid\" (includewatchonly=false)\ngetunconfirmedbalance (\"account\")\ngetvotechoices (\"tickethash\")\ngetwalletfee\nhelp (\"command\")\nimportcfiltersv2 startheight [\"filter\",...]\nimportprivkey \"privkey\" (\"label\" rescan=true scanfrom)\nimportscript \"hex\" (rescan=true scanfrom)\nimportxpub \"name\" \"xpub\"\nlistaccounts (minconf=1)\nlistaddresstransactions [\"address\",...] (\"account\")\nlistalltransactions (\"account\")\nlistlockunspent (\"account\")\nlistreceivedbyaccount (minconf=1 includeempty=false includewatchonly=false)\nlistreceivedbyaddress (minconf=1 includeempty=false includewatchonly=false)\nlistsinceblock (\"blockhash\" targetconfirmations=1 includewatchonly=false)\nlisttransactions (\"account\" count=10 from=0 includewatchonly=false)\nlistunspent (minconf=1 maxconf=9999999 [\"address\",...] \"account\")\nlockaccount \"account\"\nlockunspent unlock [{\"amount\":n.nnn,\"txid\":\"value\",\"vout\":n,\"tree\":n},...]\nmixaccount\nmixoutput \"outpoint\"\npurchaseticket \"fromaccount\" spendlimit (minconf=1 \"ticketaddress\" numtickets=1 \"pooladdress\" poolfees expiry \"comment\" dontsigntx)\nredeemmultisigout \"hash\" index tree (\"address\")\nredeemmultisigouts \"fromscraddress\" (\"toaddress\" number)\nrenameaccount \"oldaccount\" \"newaccount\"\nrescanwallet (beginheight=0)\nrevoketickets\nsendfrom \"fromaccount\" \"toaddress\" amount (minconf=1 \"comment\" \"commentto\")\nsendfromtreasury \"key\" amounts\nsendmany \"fromaccount\" {\"address\":amount,...} (minconf=1 \"comment\")\nsendrawtransaction \"hextx\" (allowhighfees=false)\nsendtoaddress \"address\" amount (\"comment\" \"commentto\")\nsendtomultisig \"fromaccount\" amount [\"pubkey\",...] (nrequired=1 minconf=1 \"comment\")\nsendtotreasury amount\nsetaccountpassphrase \"account\" \"passphrase\"\nsettreasurypolicy \"key\" \"policy\"\nsettspendpolicy \"hash\" \"policy\"\nsettxfee amount\nsetvotechoice \"agendaid\" \"choiceid\" (\"tickethash\")\nsignmessage \"address\" \"message\"\nsignrawtransaction \"rawtx\" ([{\"txid\":\"value\",\"vout\":n,\"tree\":n,\"scriptpubkey\":\"value\",\"redeemscript\":\"value\"},...] [\"privkey\",...] flags=\"ALL\")\nsignrawtransactions [\"rawtx\",...] (send=true)\nstakepooluserinfo \"user\"\nsweepaccount \"sourceaccount\" \"destinationaddress\" (requiredconfirmations feeperkb)\nticketinfo (startheight=0)\nticketsforaddress \"address\"\ntreasurypolicy (\"key\")\ntspendpolicy (\"hash\")\nunlockaccount \"account\" \"passphrase\"\nvalidateaddress \"address\"\nvalidatepredcp0005cf\nverifymessage \"address\" \"signature\" \"message\"\nversion\nwalletinfo\nwalletislocked\nwalletlock\nwalletpassphrase \"passphrase\" timeout\nwalletpassphrasechange \"oldpassphrase\" \"newpassphrase\"\nwalletpubpassphrasechange \"oldpassphrase\" \"newpassphrase\"" +var requestUsages = "abandontransaction \"hash\"\naccountaddressindex \"account\" branch\naccountsyncaddressindex \"account\" branch index\naddmultisigaddress nrequired [\"key\",...] (\"account\")\naddtransaction \"blockhash\" \"transaction\"\nauditreuse (since)\nconsolidate inputs (\"account\" \"address\")\ncreatemultisig nrequired [\"key\",...]\ncreatenewaccount \"account\"\ncreaterawtransaction [{\"amount\":n.nnn,\"txid\":\"value\",\"vout\":n,\"tree\":n},...] {\"address\":amount,...} (locktime expiry)\ncreatesignature \"address\" inputindex hashtype \"previouspkscript\" \"serializedtransaction\"\ndiscoverusage (\"startblock\" discoveraccounts gaplimit)\ndumpprivkey \"address\"\nfundrawtransaction \"hexstring\" \"fundaccount\" ({\"changeaddress\":changeaddress,\"feerate\":feerate,\"conftarget\":conftarget})\ngeneratevote \"blockhash\" height \"tickethash\" votebits \"votebitsext\"\ngetaccount \"address\"\ngetaccountaddress \"account\"\ngetaddressesbyaccount \"account\"\ngetbalance (\"account\" minconf=1)\ngetbestblock\ngetbestblockhash\ngetblockcount\ngetblockhash index\ngetcoinjoinsbyacct\ngetinfo\ngetmasterpubkey (\"account\")\ngetmultisigoutinfo \"hash\" index\ngetnewaddress (\"account\" \"gappolicy\")\ngetpeerinfo\ngetrawchangeaddress (\"account\")\ngetreceivedbyaccount \"account\" (minconf=1)\ngetreceivedbyaddress \"address\" (minconf=1)\ngetstakeinfo\ngettickets includeimmature\ngettransaction \"txid\" (includewatchonly=false)\ngettxout \"txid\" vout tree (includemempool=true)\ngetunconfirmedbalance (\"account\")\ngetvotechoices (\"tickethash\")\ngetwalletfee\nhelp (\"command\")\nimportcfiltersv2 startheight [\"filter\",...]\nimportprivkey \"privkey\" (\"label\" rescan=true scanfrom)\nimportscript \"hex\" (rescan=true scanfrom)\nimportxpub \"name\" \"xpub\"\nlistaccounts (minconf=1)\nlistaddresstransactions [\"address\",...] (\"account\")\nlistalltransactions (\"account\")\nlistlockunspent (\"account\")\nlistreceivedbyaccount (minconf=1 includeempty=false includewatchonly=false)\nlistreceivedbyaddress (minconf=1 includeempty=false includewatchonly=false)\nlistsinceblock (\"blockhash\" targetconfirmations=1 includewatchonly=false)\nlisttransactions (\"account\" count=10 from=0 includewatchonly=false)\nlistunspent (minconf=1 maxconf=9999999 [\"address\",...] \"account\")\nlockaccount \"account\"\nlockunspent unlock [{\"amount\":n.nnn,\"txid\":\"value\",\"vout\":n,\"tree\":n},...]\nmixaccount\nmixoutput \"outpoint\"\npurchaseticket \"fromaccount\" spendlimit (minconf=1 \"ticketaddress\" numtickets=1 \"pooladdress\" poolfees expiry \"comment\" dontsigntx)\nredeemmultisigout \"hash\" index tree (\"address\")\nredeemmultisigouts \"fromscraddress\" (\"toaddress\" number)\nrenameaccount \"oldaccount\" \"newaccount\"\nrescanwallet (beginheight=0)\nrevoketickets\nsendfrom \"fromaccount\" \"toaddress\" amount (minconf=1 \"comment\" \"commentto\")\nsendfromtreasury \"key\" amounts\nsendmany \"fromaccount\" {\"address\":amount,...} (minconf=1 \"comment\")\nsendrawtransaction \"hextx\" (allowhighfees=false)\nsendtoaddress \"address\" amount (\"comment\" \"commentto\")\nsendtomultisig \"fromaccount\" amount [\"pubkey\",...] (nrequired=1 minconf=1 \"comment\")\nsendtotreasury amount\nsetaccountpassphrase \"account\" \"passphrase\"\nsettreasurypolicy \"key\" \"policy\"\nsettspendpolicy \"hash\" \"policy\"\nsettxfee amount\nsetvotechoice \"agendaid\" \"choiceid\" (\"tickethash\")\nsignmessage \"address\" \"message\"\nsignrawtransaction \"rawtx\" ([{\"txid\":\"value\",\"vout\":n,\"tree\":n,\"scriptpubkey\":\"value\",\"redeemscript\":\"value\"},...] [\"privkey\",...] flags=\"ALL\")\nsignrawtransactions [\"rawtx\",...] (send=true)\nstakepooluserinfo \"user\"\nsweepaccount \"sourceaccount\" \"destinationaddress\" (requiredconfirmations feeperkb)\nticketinfo (startheight=0)\nticketsforaddress \"address\"\ntreasurypolicy (\"key\")\ntspendpolicy (\"hash\")\nunlockaccount \"account\" \"passphrase\"\nvalidateaddress \"address\"\nvalidatepredcp0005cf\nverifymessage \"address\" \"signature\" \"message\"\nversion\nwalletinfo\nwalletislocked\nwalletlock\nwalletpassphrase \"passphrase\" timeout\nwalletpassphrasechange \"oldpassphrase\" \"newpassphrase\"\nwalletpubpassphrasechange \"oldpassphrase\" \"newpassphrase\"" diff --git a/internal/rpc/rpcserver/server.go b/internal/rpc/rpcserver/server.go index 8bd031149..d980b6a0d 100644 --- a/internal/rpc/rpcserver/server.go +++ b/internal/rpc/rpcserver/server.go @@ -2042,7 +2042,7 @@ func (s *walletServer) ValidateAddress(ctx context.Context, req *pb.ValidateAddr // further information available, so just set the script type // a non-standard and break out now. class, addrs, reqSigs, err := txscript.ExtractPkScriptAddrs( - 0, script, s.wallet.ChainParams(), false) // No reasury + 0, script, s.wallet.ChainParams(), false) // No treasury if err != nil { result.ScriptType = pb.ValidateAddressResponse_NonStandardTy break diff --git a/internal/rpchelp/helpdescs_en_US.go b/internal/rpchelp/helpdescs_en_US.go index 960900d38..fd029b761 100644 --- a/internal/rpchelp/helpdescs_en_US.go +++ b/internal/rpchelp/helpdescs_en_US.go @@ -332,6 +332,29 @@ var helpDescsEnUS = map[string]string{ "help--result0": "List of commands", "help--result1": "Help for specified command", + // GetTxOutCmd help. + "gettxout--synopsis": "Returns information about an unspent transaction output.", + "gettxout-txid": "The hash of the transaction", + "gettxout-vout": "The index of the output", + "gettxout-tree": "The tree of the transaction", + "gettxout-includemempool": "Include the mempool when true", + + // GetTxOutResult help. + "gettxoutresult-bestblock": "The block hash that contains the transaction output", + "gettxoutresult-confirmations": "The number of confirmations", + "gettxoutresult-value": "The transaction amount in DCR", + "gettxoutresult-scriptPubKey": "The public key script used to pay coins as a JSON object", + "gettxoutresult-version": "The transaction version", + "gettxoutresult-coinbase": "Whether or not the transaction is a coinbase", + + // ScriptPubKeyResult help. + "scriptpubkeyresult-asm": "Disassembly of the script", + "scriptpubkeyresult-hex": "Hex-encoded bytes of the script", + "scriptpubkeyresult-reqSigs": "The number of required signatures", + "scriptpubkeyresult-type": "The type of the script (e.g. 'pubkeyhash')", + "scriptpubkeyresult-addresses": "The Decred addresses associated with this script", + "scriptpubkeyresult-commitamt": "The ticket commitment value if the script is for a staking commitment", + // ImportCFiltersV2Cmd help. "importcfiltersv2--synopsis": "Imports a list of v2 cfilters into the wallet. Does not perform validation on the filters", "importcfiltersv2-startheight": "The starting block height for this list of cfilters", diff --git a/internal/rpchelp/methods.go b/internal/rpchelp/methods.go index 3f75102f4..2a5df90ca 100644 --- a/internal/rpchelp/methods.go +++ b/internal/rpchelp/methods.go @@ -62,6 +62,7 @@ var Methods = []struct { {"getstakeinfo", []interface{}{(*types.GetStakeInfoResult)(nil)}}, {"gettickets", []interface{}{(*types.GetTicketsResult)(nil)}}, {"gettransaction", []interface{}{(*types.GetTransactionResult)(nil)}}, + {"gettxout", []interface{}{(*dcrdtypes.GetTxOutResult)(nil)}}, {"getunconfirmedbalance", returnsNumber}, {"getvotechoices", []interface{}{(*types.GetVoteChoicesResult)(nil)}}, {"getwalletfee", returnsNumber}, diff --git a/rpc/jsonrpc/types/methods.go b/rpc/jsonrpc/types/methods.go index 44c2411d3..54970fd82 100644 --- a/rpc/jsonrpc/types/methods.go +++ b/rpc/jsonrpc/types/methods.go @@ -1261,6 +1261,7 @@ func init() { {"getblockhash", (*GetBlockHashCmd)(nil)}, {"getinfo", (*GetInfoCmd)(nil)}, {"getpeerinfo", (*GetPeerInfoCmd)(nil)}, + {"gettxout", (*GetTxOutCmd)(nil)}, {"help", (*HelpCmd)(nil)}, {"sendrawtransaction", (*SendRawTransactionCmd)(nil)}, {"ticketsforaddress", (*TicketsForAddressCmd)(nil)}, @@ -1292,6 +1293,7 @@ type ( GetBlockHashCmd dcrdtypes.GetBlockHashCmd GetInfoCmd dcrdtypes.GetInfoCmd GetPeerInfoCmd dcrdtypes.GetPeerInfoCmd + GetTxOutCmd dcrdtypes.GetTxOutCmd HelpCmd dcrdtypes.HelpCmd SendRawTransactionCmd dcrdtypes.SendRawTransactionCmd TicketsForAddressCmd dcrdtypes.TicketsForAddressCmd diff --git a/wallet/udb/txmined.go b/wallet/udb/txmined.go index 7179f7277..1abcb5f7e 100644 --- a/wallet/udb/txmined.go +++ b/wallet/udb/txmined.go @@ -2341,6 +2341,28 @@ func (s *Store) UnspentOutputs(dbtx walletdb.ReadTx) ([]*Credit, error) { return unspent, nil } +// UnspentOutput returns details for an unspent received transaction output. +// Returns error NotExist if the specified outpoint cannot be found or has been +// spent by a mined transaction. Mined transactions that are spent by a mempool +// transaction are not affected by this. +func (s *Store) UnspentOutput(ns walletdb.ReadBucket, op wire.OutPoint, includeMempool bool) (*Credit, error) { + k := canonicalOutPoint(&op.Hash, op.Index) + // Check if unspent output is in mempool (if includeMempool == true). + if includeMempool && existsRawUnminedCredit(ns, k) != nil { + return s.outputCreditInfo(ns, op, nil) + } + // Check for unspent output in bucket for mined unspents. + if v := ns.NestedReadBucket(bucketUnspent).Get(k); v != nil { + var block Block + err := readUnspentBlock(v, &block) + if err != nil { + return nil, err + } + return s.outputCreditInfo(ns, op, &block) + } + return nil, errors.E(errors.NotExist, errors.Errorf("no unspent output %v", op)) +} + // ForEachUnspentOutpoint calls f on each UTXO outpoint. // The order is undefined. func (s *Store) ForEachUnspentOutpoint(dbtx walletdb.ReadTx, f func(*wire.OutPoint) error) error { diff --git a/wallet/wallet.go b/wallet/wallet.go index 2ac477945..66f8708b5 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -3333,6 +3333,21 @@ func (w *Wallet) Spender(ctx context.Context, out *wire.OutPoint) (*wire.MsgTx, return spender, spenderIndex, err } +// UnspentOutput returns information about an unspent received transaction +// output. Returns error NotExist if the specified outpoint cannot be found or +// has been spent by a mined transaction. Mined transactions that are spent by +// a mempool transaction are not affected by this. +func (w *Wallet) UnspentOutput(ctx context.Context, op wire.OutPoint, includeMempool bool) (*udb.Credit, error) { + var utxo *udb.Credit + err := walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error { + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + var err error + utxo, err = w.txStore.UnspentOutput(txmgrNs, op, includeMempool) + return err + }) + return utxo, err +} + // AccountProperties contains properties associated with each account, such as // the account name, number, and the nubmer of derived and imported keys. If no // address usage has been recorded on any of the external or internal branches,