diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 3852b8e02..020b71f3f 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -3515,6 +3515,172 @@ UniValue z_gettotalbalance(const UniValue& params, bool fHelp) return result; } +UniValue z_viewtransaction(const UniValue& params, bool fHelp) +{ + if (!EnsureWalletIsAvailable(fHelp)) + return NullUniValue; + + if (fHelp || params.size() != 1) + throw runtime_error( + "z_viewtransaction \"txid\"\n" + "\nGet detailed shielded information about in-wallet transaction \n" + "\nArguments:\n" + "1. \"txid\" (string, required) The transaction id\n" + "\nResult:\n" + "{\n" + " \"txid\" : \"transactionid\", (string) The transaction id\n" + " \"spends\" : [\n" + " {\n" + " \"type\" : \"sprout|sapling\", (string) The type of address\n" + " \"js\" : n, (numeric, sprout) the index of the JSDescription within vJoinSplit\n" + " \"jsSpend\" : n, (numeric, sprout) the index of the spend within the JSDescription\n" + " \"spend\" : n, (numeric, sapling) the index of the spend within vShieldedSpend\n" + " \"txidPrev\" : \"transactionid\", (string) The id for the transaction this note was created in\n" + " \"jsPrev\" : n, (numeric, sprout) the index of the JSDescription within vJoinSplit\n" + " \"jsOutputPrev\" : n, (numeric, sprout) the index of the output within the JSDescription\n" + " \"outputPrev\" : n, (numeric, sapling) the index of the output within the vShieldedOutput\n" + " \"address\" : \"zcashaddress\", (string) The Zcash address involved in the transaction\n" + " \"value\" : x.xxx (numeric) The amount in " + CURRENCY_UNIT + "\n" + " \"valueZat\" : xxxx (numeric) The amount in zatoshis\n" + " }\n" + " ,...\n" + " ],\n" + " \"outputs\" : [\n" + " {\n" + " \"type\" : \"sprout|sapling\", (string) The type of address\n" + " \"js\" : n, (numeric, sprout) the index of the JSDescription within vJoinSplit\n" + " \"jsOutput\" : n, (numeric, sprout) the index of the output within the JSDescription\n" + " \"output\" : n, (numeric, sapling) the index of the output within the vShieldedOutput\n" + " \"address\" : \"zcashaddress\", (string) The Zcash address involved in the transaction\n" + " \"value\" : x.xxx (numeric) The amount in " + CURRENCY_UNIT + "\n" + " \"valueZat\" : xxxx (numeric) The amount in zatoshis\n" + " \"memo\" : \"hexmemo\", (string) Hexademical string representation of the memo field\n" + " }\n" + " ,...\n" + " ],\n" + "}\n" + + "\nExamples:\n" + + HelpExampleCli("z_viewtransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"") + + HelpExampleRpc("z_viewtransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"") + ); + + LOCK2(cs_main, pwalletMain->cs_wallet); + + uint256 hash; + hash.SetHex(params[0].get_str()); + + UniValue entry(UniValue::VOBJ); + if (!pwalletMain->mapWallet.count(hash)) + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id"); + const CWalletTx& wtx = pwalletMain->mapWallet[hash]; + + entry.push_back(Pair("txid", hash.GetHex())); + + UniValue spends(UniValue::VARR); + UniValue outputs(UniValue::VARR); + + // Sprout spends + for (size_t i = 0; i < wtx.vJoinSplit.size(); ++i) { + for (size_t j = 0; j < wtx.vJoinSplit[i].nullifiers.size(); ++j) { + auto nullifier = wtx.vJoinSplit[i].nullifiers[j]; + + // Fetch the note that is being spent, if ours + auto res = pwalletMain->mapSproutNullifiersToNotes.find(nullifier); + if (res == pwalletMain->mapSproutNullifiersToNotes.end()) { + continue; + } + auto jsop = res->second; + auto wtxPrev = pwalletMain->mapWallet.at(jsop.hash); + + auto decrypted = wtxPrev.DecryptSproutNote(jsop); + auto notePt = decrypted.first; + auto pa = decrypted.second; + + UniValue entry(UniValue::VOBJ); + entry.push_back(Pair("type", ADDR_TYPE_SPROUT)); + entry.push_back(Pair("js", (int)i)); + entry.push_back(Pair("jsSpend", (int)j)); + entry.push_back(Pair("txidPrev", jsop.hash.GetHex())); + entry.push_back(Pair("jsPrev", (int)jsop.js)); + entry.push_back(Pair("jsOutputPrev", (int)jsop.n)); + entry.push_back(Pair("address", EncodePaymentAddress(pa))); + entry.push_back(Pair("value", ValueFromAmount(notePt.value()))); + entry.push_back(Pair("valueZat", notePt.value())); + spends.push_back(entry); + } + } + + // Sprout outputs + for (auto & pair : wtx.mapSproutNoteData) { + JSOutPoint jsop = pair.first; + + auto decrypted = wtx.DecryptSproutNote(jsop); + auto notePt = decrypted.first; + auto pa = decrypted.second; + + UniValue entry(UniValue::VOBJ); + entry.push_back(Pair("type", ADDR_TYPE_SPROUT)); + entry.push_back(Pair("js", (int)jsop.js)); + entry.push_back(Pair("jsOutput", (int)jsop.n)); + entry.push_back(Pair("address", EncodePaymentAddress(pa))); + entry.push_back(Pair("value", ValueFromAmount(notePt.value()))); + entry.push_back(Pair("valueZat", notePt.value())); + entry.push_back(Pair("memo", HexStr(notePt.memo()))); + outputs.push_back(entry); + } + + // Sapling spends + for (size_t i = 0; i < wtx.vShieldedSpend.size(); ++i) { + auto spend = wtx.vShieldedSpend[i]; + + // Fetch the note that is being spent + auto res = pwalletMain->mapSaplingNullifiersToNotes.find(spend.nullifier); + if (res == pwalletMain->mapSaplingNullifiersToNotes.end()) { + continue; + } + auto op = res->second; + auto wtxPrev = pwalletMain->mapWallet.at(op.hash); + + auto decrypted = wtxPrev.DecryptSaplingNote(op); + auto notePt = decrypted.first; + auto pa = decrypted.second; + + UniValue entry(UniValue::VOBJ); + entry.push_back(Pair("type", ADDR_TYPE_SAPLING)); + entry.push_back(Pair("spend", (int)i)); + entry.push_back(Pair("txidPrev", op.hash.GetHex())); + entry.push_back(Pair("outputPrev", (int)op.n)); + entry.push_back(Pair("address", EncodePaymentAddress(pa))); + entry.push_back(Pair("value", ValueFromAmount(notePt.value()))); + entry.push_back(Pair("valueZat", notePt.value())); + spends.push_back(entry); + } + + // Sapling outputs + for (auto & pair : wtx.mapSaplingNoteData) { + SaplingOutPoint op = pair.first; + + auto decrypted = wtx.DecryptSaplingNote(op); + auto notePt = decrypted.first; + auto pa = decrypted.second; + + UniValue entry(UniValue::VOBJ); + entry.push_back(Pair("type", ADDR_TYPE_SAPLING)); + entry.push_back(Pair("output", (int)op.n)); + entry.push_back(Pair("address", EncodePaymentAddress(pa))); + entry.push_back(Pair("value", ValueFromAmount(notePt.value()))); + entry.push_back(Pair("valueZat", notePt.value())); + entry.push_back(Pair("memo", HexStr(notePt.memo()))); + outputs.push_back(entry); + } + + entry.push_back(Pair("spends", spends)); + entry.push_back(Pair("outputs", outputs)); + + return entry; +} + UniValue z_getoperationresult(const UniValue& params, bool fHelp) { if (!EnsureWalletIsAvailable(fHelp)) @@ -4816,6 +4982,7 @@ static const CRPCCommand commands[] = { "wallet", "z_importviewingkey", &z_importviewingkey, true }, { "wallet", "z_exportwallet", &z_exportwallet, true }, { "wallet", "z_importwallet", &z_importwallet, true }, + { "wallet", "z_viewtransaction", &z_viewtransaction, false }, // TODO: rearrange into another category { "disclosure", "z_getpaymentdisclosure", &z_getpaymentdisclosure, true }, { "disclosure", "z_validatepaymentdisclosure", &z_validatepaymentdisclosure, true } diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index f79ce4b1d..e6fbbc526 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -2218,6 +2218,67 @@ void CWalletTx::SetSaplingNoteData(mapSaplingNoteData_t ¬eData) } } +std::pair CWalletTx::DecryptSproutNote( + JSOutPoint jsop) const +{ + LOCK(pwallet->cs_wallet); + + auto nd = this->mapSproutNoteData.at(jsop); + SproutPaymentAddress pa = nd.address; + + // Get cached decryptor + ZCNoteDecryption decryptor; + if (!pwallet->GetNoteDecryptor(pa, decryptor)) { + // Note decryptors are created when the wallet is loaded, so it should always exist + throw std::runtime_error(strprintf( + "Could not find note decryptor for payment address %s", + EncodePaymentAddress(pa))); + } + + auto hSig = this->vJoinSplit[jsop.js].h_sig(*pzcashParams, this->joinSplitPubKey); + try { + SproutNotePlaintext plaintext = SproutNotePlaintext::decrypt( + decryptor, + this->vJoinSplit[jsop.js].ciphertexts[jsop.n], + this->vJoinSplit[jsop.js].ephemeralKey, + hSig, + (unsigned char) jsop.n); + + return std::make_pair(plaintext, pa); + } catch (const note_decryption_failed &err) { + // Couldn't decrypt with this spending key + throw std::runtime_error(strprintf( + "Could not decrypt note for payment address %s", + EncodePaymentAddress(pa))); + } catch (const std::exception &exc) { + // Unexpected failure + throw std::runtime_error(strprintf( + "Error while decrypting note for payment address %s: %s", + EncodePaymentAddress(pa), exc.what())); + } +} + +std::pair CWalletTx::DecryptSaplingNote( + SaplingOutPoint op) const +{ + auto output = this->vShieldedOutput[op.n]; + auto nd = this->mapSaplingNoteData.at(op); + + auto maybe_pt = SaplingNotePlaintext::decrypt( + output.encCiphertext, + nd.ivk, + output.ephemeralKey, + output.cm); + assert(static_cast(maybe_pt)); + auto notePt = maybe_pt.get(); + + auto maybe_pa = nd.ivk.address(notePt.d); + assert(static_cast(maybe_pa)); + auto pa = maybe_pa.get(); + + return std::make_pair(notePt, pa); +} + int64_t CWalletTx::GetTxTime() const { int64_t n = nTimeSmart; diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 74a1c3d6a..f45b42a05 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -555,6 +555,11 @@ public: void SetSproutNoteData(mapSproutNoteData_t ¬eData); void SetSaplingNoteData(mapSaplingNoteData_t ¬eData); + std::pair DecryptSproutNote( + JSOutPoint jsop) const; + std::pair DecryptSaplingNote( + SaplingOutPoint op) const; + //! filter decides which addresses will count towards the debit CAmount GetDebit(const isminefilter& filter) const; CAmount GetCredit(const isminefilter& filter) const;