1 // Copyright (c) 2011-2018 The Bitcoin Core developers
2 // Distributed under the MIT software license, see the accompanying
3 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4 
5 #ifdef HAVE_CONFIG_H
6 #include <config/bitcoin-config.h>
7 #endif
8 
9 #include <qt/transactiondesc.h>
10 
11 #include <qt/bitcoinunits.h>
12 #include <qt/guiutil.h>
13 #include <qt/paymentserver.h>
14 #include <qt/transactionrecord.h>
15 
16 #include <consensus/consensus.h>
17 #include <interfaces/node.h>
18 #include <key_io.h>
19 #include <validation.h>
20 #include <script/script.h>
21 #include <timedata.h>
22 #include <util/system.h>
23 #include <wallet/db.h>
24 #include <wallet/wallet.h>
25 #include <policy/policy.h>
26 
27 #include <stdint.h>
28 #include <string>
29 
FormatTxStatus(const interfaces::WalletTx & wtx,const interfaces::WalletTxStatus & status,bool inMempool,int numBlocks)30 QString TransactionDesc::FormatTxStatus(const interfaces::WalletTx& wtx, const interfaces::WalletTxStatus& status, bool inMempool, int numBlocks)
31 {
32     if (!status.is_final)
33     {
34         if (wtx.tx->nLockTime < LOCKTIME_THRESHOLD)
35             return tr("Open for %n more block(s)", "", wtx.tx->nLockTime - numBlocks);
36         else
37             return tr("Open until %1").arg(GUIUtil::dateTimeStr(wtx.tx->nLockTime));
38     }
39     else
40     {
41         int nDepth = status.depth_in_main_chain;
42         if (nDepth < 0)
43             return tr("conflicted with a transaction with %1 confirmations").arg(-nDepth);
44         else if (nDepth == 0)
45             return tr("0/unconfirmed, %1").arg((inMempool ? tr("in memory pool") : tr("not in memory pool"))) + (status.is_abandoned ? ", "+tr("abandoned") : "");
46         else if (nDepth < 6)
47             return tr("%1/unconfirmed").arg(nDepth);
48         else
49             return tr("%1 confirmations").arg(nDepth);
50     }
51 }
52 
toHTML(interfaces::Node & node,interfaces::Wallet & wallet,TransactionRecord * rec,int unit)53 QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wallet, TransactionRecord *rec, int unit)
54 {
55     int numBlocks;
56     interfaces::WalletTxStatus status;
57     interfaces::WalletOrderForm orderForm;
58     bool inMempool;
59     interfaces::WalletTx wtx = wallet.getWalletTxDetails(rec->hash, status, orderForm, inMempool, numBlocks);
60 
61     QString strHTML;
62 
63     strHTML.reserve(4000);
64     strHTML += "<html><font face='verdana, arial, helvetica, sans-serif'>";
65 
66     int64_t nTime = wtx.time;
67     CAmount nCredit = wtx.credit;
68     CAmount nDebit = wtx.debit;
69     CAmount nNet = nCredit - nDebit;
70 
71     strHTML += "<b>" + tr("Status") + ":</b> " + FormatTxStatus(wtx, status, inMempool, numBlocks);
72     strHTML += "<br>";
73 
74     strHTML += "<b>" + tr("Date") + ":</b> " + (nTime ? GUIUtil::dateTimeStr(nTime) : "") + "<br>";
75 
76     //
77     // From
78     //
79     if (wtx.is_coinbase)
80     {
81         strHTML += "<b>" + tr("Source") + ":</b> " + tr("Generated") + "<br>";
82     }
83     else if (wtx.value_map.count("from") && !wtx.value_map["from"].empty())
84     {
85         // Online transaction
86         strHTML += "<b>" + tr("From") + ":</b> " + GUIUtil::HtmlEscape(wtx.value_map["from"]) + "<br>";
87     }
88     else
89     {
90         // Offline transaction
91         if (nNet > 0)
92         {
93             // Credit
94             CTxDestination address = DecodeDestination(rec->address);
95             if (IsValidDestination(address)) {
96                 std::string name;
97                 isminetype ismine;
98                 if (wallet.getAddress(address, &name, &ismine, /* purpose= */ nullptr))
99                 {
100                     strHTML += "<b>" + tr("From") + ":</b> " + tr("unknown") + "<br>";
101                     strHTML += "<b>" + tr("To") + ":</b> ";
102                     strHTML += GUIUtil::HtmlEscape(rec->address);
103                     QString addressOwned = ismine == ISMINE_SPENDABLE ? tr("own address") : tr("watch-only");
104                     if (!name.empty())
105                         strHTML += " (" + addressOwned + ", " + tr("label") + ": " + GUIUtil::HtmlEscape(name) + ")";
106                     else
107                         strHTML += " (" + addressOwned + ")";
108                     strHTML += "<br>";
109                 }
110             }
111         }
112     }
113 
114     //
115     // To
116     //
117     if (wtx.value_map.count("to") && !wtx.value_map["to"].empty())
118     {
119         // Online transaction
120         std::string strAddress = wtx.value_map["to"];
121         strHTML += "<b>" + tr("To") + ":</b> ";
122         CTxDestination dest = DecodeDestination(strAddress);
123         std::string name;
124         if (wallet.getAddress(
125                 dest, &name, /* is_mine= */ nullptr, /* purpose= */ nullptr) && !name.empty())
126             strHTML += GUIUtil::HtmlEscape(name) + " ";
127         strHTML += GUIUtil::HtmlEscape(strAddress) + "<br>";
128     }
129 
130     //
131     // Amount
132     //
133     if (wtx.is_coinbase && nCredit == 0)
134     {
135         //
136         // Coinbase
137         //
138         CAmount nUnmatured = 0;
139         for (const CTxOut& txout : wtx.tx->vout)
140             nUnmatured += wallet.getCredit(txout, ISMINE_ALL);
141         strHTML += "<b>" + tr("Credit") + ":</b> ";
142         if (status.is_in_main_chain)
143             strHTML += BitcoinUnits::formatHtmlWithUnit(unit, nUnmatured)+ " (" + tr("matures in %n more block(s)", "", status.blocks_to_maturity) + ")";
144         else
145             strHTML += "(" + tr("not accepted") + ")";
146         strHTML += "<br>";
147     }
148     else if (nNet > 0)
149     {
150         //
151         // Credit
152         //
153         strHTML += "<b>" + tr("Credit") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, nNet) + "<br>";
154     }
155     else
156     {
157         isminetype fAllFromMe = ISMINE_SPENDABLE;
158         for (const isminetype mine : wtx.txin_is_mine)
159         {
160             if(fAllFromMe > mine) fAllFromMe = mine;
161         }
162 
163         isminetype fAllToMe = ISMINE_SPENDABLE;
164         for (const isminetype mine : wtx.txout_is_mine)
165         {
166             if(fAllToMe > mine) fAllToMe = mine;
167         }
168 
169         if (fAllFromMe)
170         {
171             if(fAllFromMe & ISMINE_WATCH_ONLY)
172                 strHTML += "<b>" + tr("From") + ":</b> " + tr("watch-only") + "<br>";
173 
174             //
175             // Debit
176             //
177             auto mine = wtx.txout_is_mine.begin();
178             for (const CTxOut& txout : wtx.tx->vout)
179             {
180                 // Ignore change
181                 isminetype toSelf = *(mine++);
182                 if ((toSelf == ISMINE_SPENDABLE) && (fAllFromMe == ISMINE_SPENDABLE))
183                     continue;
184 
185                 if (!wtx.value_map.count("to") || wtx.value_map["to"].empty())
186                 {
187                     // Offline transaction
188                     CTxDestination address;
189                     if (ExtractDestination(txout.scriptPubKey, address))
190                     {
191                         strHTML += "<b>" + tr("To") + ":</b> ";
192                         std::string name;
193                         if (wallet.getAddress(
194                                 address, &name, /* is_mine= */ nullptr, /* purpose= */ nullptr) && !name.empty())
195                             strHTML += GUIUtil::HtmlEscape(name) + " ";
196                         strHTML += GUIUtil::HtmlEscape(EncodeDestination(address));
197                         if(toSelf == ISMINE_SPENDABLE)
198                             strHTML += " (own address)";
199                         else if(toSelf & ISMINE_WATCH_ONLY)
200                             strHTML += " (watch-only)";
201                         strHTML += "<br>";
202                     }
203                 }
204 
205                 strHTML += "<b>" + tr("Debit") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, -txout.nValue) + "<br>";
206                 if(toSelf)
207                     strHTML += "<b>" + tr("Credit") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, txout.nValue) + "<br>";
208             }
209 
210             if (fAllToMe)
211             {
212                 // Payment to self
213                 CAmount nChange = wtx.change;
214                 CAmount nValue = nCredit - nChange;
215                 strHTML += "<b>" + tr("Total debit") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, -nValue) + "<br>";
216                 strHTML += "<b>" + tr("Total credit") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, nValue) + "<br>";
217             }
218 
219             CAmount nTxFee = nDebit - wtx.tx->GetValueOut();
220             if (nTxFee > 0)
221                 strHTML += "<b>" + tr("Transaction fee") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, -nTxFee) + "<br>";
222         }
223         else
224         {
225             //
226             // Mixed debit transaction
227             //
228             auto mine = wtx.txin_is_mine.begin();
229             for (const CTxIn& txin : wtx.tx->vin) {
230                 if (*(mine++)) {
231                     strHTML += "<b>" + tr("Debit") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, -wallet.getDebit(txin, ISMINE_ALL)) + "<br>";
232                 }
233             }
234             mine = wtx.txout_is_mine.begin();
235             for (const CTxOut& txout : wtx.tx->vout) {
236                 if (*(mine++)) {
237                     strHTML += "<b>" + tr("Credit") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, wallet.getCredit(txout, ISMINE_ALL)) + "<br>";
238                 }
239             }
240         }
241     }
242 
243     strHTML += "<b>" + tr("Net amount") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, nNet, true) + "<br>";
244 
245     //
246     // Message
247     //
248     if (wtx.value_map.count("message") && !wtx.value_map["message"].empty())
249         strHTML += "<br><b>" + tr("Message") + ":</b><br>" + GUIUtil::HtmlEscape(wtx.value_map["message"], true) + "<br>";
250     if (wtx.value_map.count("comment") && !wtx.value_map["comment"].empty())
251         strHTML += "<br><b>" + tr("Comment") + ":</b><br>" + GUIUtil::HtmlEscape(wtx.value_map["comment"], true) + "<br>";
252 
253     strHTML += "<b>" + tr("Transaction ID") + ":</b> " + rec->getTxHash() + "<br>";
254     strHTML += "<b>" + tr("Transaction total size") + ":</b> " + QString::number(wtx.tx->GetTotalSize()) + " bytes<br>";
255     strHTML += "<b>" + tr("Transaction virtual size") + ":</b> " + QString::number(GetVirtualTransactionSize(*wtx.tx)) + " bytes<br>";
256     strHTML += "<b>" + tr("Output index") + ":</b> " + QString::number(rec->getOutputIndex()) + "<br>";
257 
258     // Message from normal bitcoin:URI (bitcoin:123...?message=example)
259     for (const std::pair<std::string, std::string>& r : orderForm)
260         if (r.first == "Message")
261             strHTML += "<br><b>" + tr("Message") + ":</b><br>" + GUIUtil::HtmlEscape(r.second, true) + "<br>";
262 
263 #ifdef ENABLE_BIP70
264     //
265     // PaymentRequest info:
266     //
267     for (const std::pair<std::string, std::string>& r : orderForm)
268     {
269         if (r.first == "PaymentRequest")
270         {
271             PaymentRequestPlus req;
272             req.parse(QByteArray::fromRawData(r.second.data(), r.second.size()));
273             QString merchant;
274             if (req.getMerchant(PaymentServer::getCertStore(), merchant))
275                 strHTML += "<b>" + tr("Merchant") + ":</b> " + GUIUtil::HtmlEscape(merchant) + "<br>";
276         }
277     }
278 #endif
279 
280     if (wtx.is_coinbase)
281     {
282         quint32 numBlocksToMaturity = COINBASE_MATURITY +  1;
283         strHTML += "<br>" + tr("Generated coins must mature %1 blocks before they can be spent. When you generated this block, it was broadcast to the network to be added to the block chain. If it fails to get into the chain, its state will change to \"not accepted\" and it won't be spendable. This may occasionally happen if another node generates a block within a few seconds of yours.").arg(QString::number(numBlocksToMaturity)) + "<br>";
284     }
285 
286     //
287     // Debug view
288     //
289     if (node.getLogCategories() != BCLog::NONE)
290     {
291         strHTML += "<hr><br>" + tr("Debug information") + "<br><br>";
292         for (const CTxIn& txin : wtx.tx->vin)
293             if(wallet.txinIsMine(txin))
294                 strHTML += "<b>" + tr("Debit") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, -wallet.getDebit(txin, ISMINE_ALL)) + "<br>";
295         for (const CTxOut& txout : wtx.tx->vout)
296             if(wallet.txoutIsMine(txout))
297                 strHTML += "<b>" + tr("Credit") + ":</b> " + BitcoinUnits::formatHtmlWithUnit(unit, wallet.getCredit(txout, ISMINE_ALL)) + "<br>";
298 
299         strHTML += "<br><b>" + tr("Transaction") + ":</b><br>";
300         strHTML += GUIUtil::HtmlEscape(wtx.tx->ToString(), true);
301 
302         strHTML += "<br><b>" + tr("Inputs") + ":</b>";
303         strHTML += "<ul>";
304 
305         for (const CTxIn& txin : wtx.tx->vin)
306         {
307             COutPoint prevout = txin.prevout;
308 
309             Coin prev;
310             if(node.getUnspentOutput(prevout, prev))
311             {
312                 {
313                     strHTML += "<li>";
314                     const CTxOut &vout = prev.out;
315                     CTxDestination address;
316                     if (ExtractDestination(vout.scriptPubKey, address))
317                     {
318                         std::string name;
319                         if (wallet.getAddress(address, &name, /* is_mine= */ nullptr, /* purpose= */ nullptr) && !name.empty())
320                             strHTML += GUIUtil::HtmlEscape(name) + " ";
321                         strHTML += QString::fromStdString(EncodeDestination(address));
322                     }
323                     strHTML = strHTML + " " + tr("Amount") + "=" + BitcoinUnits::formatHtmlWithUnit(unit, vout.nValue);
324                     strHTML = strHTML + " IsMine=" + (wallet.txoutIsMine(vout) & ISMINE_SPENDABLE ? tr("true") : tr("false")) + "</li>";
325                     strHTML = strHTML + " IsWatchOnly=" + (wallet.txoutIsMine(vout) & ISMINE_WATCH_ONLY ? tr("true") : tr("false")) + "</li>";
326                 }
327             }
328         }
329 
330         strHTML += "</ul>";
331     }
332 
333     strHTML += "</font></html>";
334     return strHTML;
335 }
336