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