mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
* Default bitcoind timeout to 60s for all platforms * Create bitcoind_setup_tasks.py * Missing prop setter bugfix Fixes #1221 * Early progress check-in * Docs update, initial FR translation file * Lots more text wrapping, babel.cfg fix for html * docs update * Updated translator procedures, added initial German, Hebrew, and Portugese files * Update README.md * Update README.md * Update messages.po Started the translation process using Poedit. Lines * Test first modif in ES First modification (only adding one line here) using Poedit and following the translation steps given by kdmukai. If everything is working well here I am ready to start translating. * Update messages.po * Bugfix; multi-language mnemonic support * Explicit language selection * Translation support for strings with variable insertions * Bugfixes; define supported langages in only one place * Cleanup * Update messages.po * Update messages.po * Latest translations integrated * Regenerating messages; disabling fuzzy matching * Compiled messages * template bugfixes * Use jinja's "tojson" to safely escape account_map device labels * Update wallet_pdf.jinja * Update helpers.py * Update node.py * Bugfixes * Updated requirements.txt; bugfix on language selection on Settings * Update wallets.py * Require `mnemonic==0.19` for multi-language BIP39 wordlists * Update iw translation * "he" language code fix for Hebrew * Update README.md * Adding blank Greek file; minor README update * Update messages.po 50% on this ... My ass is flat. * Spanish translation file updated * Update messages.po * Extracted from the latest code; recompiled with the latest translations * Added missing wrapped strings * Update messages.po * Updated Spanish translations * Adding blank Dutch ("nl") translation file * Blank Korean ("ko") translation file * Blank Portugese ("pt") file updated * Blank Turkish ("tr") translation file * Adding blank Serbian ("sr") translation file * Update messages.po * Compiled latest Spanish translations * Translation progress Translated the first 181 strings * Blank Italian ("it") translation file * More translation work done 6667810 strings translated. Nice number, guess I'll stop here :) * Compiled first batch of German translations * Update messages.po * Translation 100% All done. Would be extra pleased if my name popped up somewhere in credits. Not required though :) * Update messages.po * Update messages.po All done !!! :) * Compiled in initial Greek translations * Updated completion of German translation * Initial Dutch translations compiled in * Compiled full Spanish translations * Re-ran extraction, update, compile * Update Dutch translation * Blank Icelandic ("is") translation file * Blank Chinese (zh_Hans_CN and zh_hant_TW) translation files * Compiled latest Dutch translations * Add missing strings to translation files * Blank Swedish (sv) translation file * Blank Polish ("pl") translation file * Blank Bulgarian ("bg") translation file * Blank Russian ("ru") translation file * Swedish 30% done * Update messages.po * Blank Croatian ("hr") translation file * Delete messages.mo * Added translator Also fixed some warnings in poedit * Babel docs update * Compiled latest Dutch and Swedish translations * Update messages.po Completed 100% * Update wallets.py * Compiled in latest German translations * Update messages.po * Extracted, updated, compiled translation files * Compiled in first batch of Polish translations * Update messages.po More translations. * Compiled latest Polish translations * Hindi, Tamil, Malayalam translation files * Completed Swedish translation * 80% complete * Blank Finnish ("fi") translation file * Compiled latest Swedish translations * Full update and recompile of Swedish * Compiled Polish translations * Full update and recompile for Polish * Update messages.po * Updating Poedit docs * Removed word wrap; compiled latest Dutch * Hindi Translation --initial * updated w/no line wrap; compiled first Hindi translations * Update messages.po * Update messages.po Initial and partial test Bulgarian commit * updated and compiled initial Bulgarian translations. * Bugfix on text wrapping * Update messages.po * Update messages.po BG translation ~25% complete * Fully completed polish translation (first draft) Probably needs some further tweaking here and there. * Start Russian translation * Russian translation 30% complete * Format fixes * Compiled latest French translations * Compiled latest Chinese (Traditional) translations * Compiled latest Bulgarian translations * Compiled latest Polish translations * Compiled latest Russian translations * Update French to 36% * Update messages.po * Compiled in latest French translations * Compiled latest Dutch translations * Russian 70% complete * Update messages.po * Russian 100% complete * Update French to 44% Slowly but surely * Adds in-progress languages; latest Russian translations compiled * Extracted latest; updated Russian translation file. * Compiled latest Chinese (Traditional) translations * Updated and compiled Chinese (Traditional) * Compiled latest Russian translations * Updated and compiled latest French translations * Updated all translation files * Fix double quotes format issue Co-authored-by: Kim Neunert <k9ert@gmx.de> Co-authored-by: 2a3dex <58116837+2a3dex@users.noreply.github.com> Co-authored-by: rafa1239 <raphael.altieri@gmail.com> Co-authored-by: benk10 <ben.kaufman10@gmail.com> Co-authored-by: Volker Herminghaus <volker@bitcorn.wtf> Co-authored-by: glowleaf <69010742+glowleaf@users.noreply.github.com> Co-authored-by: DirkVdk <86686185+DirkVdk@users.noreply.github.com> Co-authored-by: mutatrum <mutatrum@gmail.com> Co-authored-by: Kryptoministern <77808545+Kryptoministern@users.noreply.github.com> Co-authored-by: PommbearBTC <86745004+PommbearBTC@users.noreply.github.com> Co-authored-by: Gummybear <holuan@gmail.com> Co-authored-by: sreshta suresh <sreshtasuresh@gmail.com> Co-authored-by: KST-Energy <82534243+KST-Energy@users.noreply.github.com> Co-authored-by: alltheseas <64376233+alltheseas@users.noreply.github.com> Co-authored-by: dudezoo <86764634+dudezoo@users.noreply.github.com> Co-authored-by: Sergei Tikhomirov <sergey.s.tikhomirov@gmail.com> Co-authored-by: Bitpaint <67663265+bitpaint@users.noreply.github.com>
698 lines
31 KiB
HTML
698 lines
31 KiB
HTML
<template id="tx-table">
|
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.css') }}">
|
|
<style>
|
|
.tx-table {
|
|
display: table;
|
|
}
|
|
tx-row {
|
|
display: contents;
|
|
}
|
|
.pagination-container {
|
|
text-align: center;
|
|
width: 100%;
|
|
}
|
|
.pagination-next, .pagination-back {
|
|
cursor: pointer;
|
|
}
|
|
input::-webkit-outer-spin-button,
|
|
input::-webkit-inner-spin-button {
|
|
-webkit-appearance: none;
|
|
margin: 0;
|
|
}
|
|
|
|
/* Firefox */
|
|
input[type=number] {
|
|
-moz-appearance: textfield;
|
|
}
|
|
|
|
.pagination-idx {
|
|
min-width: 50px;
|
|
max-width: 50px;
|
|
width: 50px;
|
|
margin: 7px 7px 7px 3px;
|
|
text-align: center;
|
|
height: 35px;
|
|
}
|
|
.search {
|
|
/* float: right; */
|
|
margin: auto;
|
|
width: 100%;
|
|
max-width: 350px;
|
|
}
|
|
.search-container {
|
|
width: 100%;
|
|
display: flex;
|
|
margin-bottom: 0.5em;
|
|
}
|
|
.up-arrow {
|
|
width: 0;
|
|
height: 0;
|
|
border: solid 5px transparent;
|
|
background: transparent;
|
|
border-bottom: solid 7px #fff;
|
|
border-top-width: 0;
|
|
cursor: pointer;
|
|
float: right;
|
|
margin-right: 50px;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.down-arrow {
|
|
width: 0;
|
|
height: 0;
|
|
border: solid 5px transparent;
|
|
background: transparent;
|
|
border-top: solid 7px #fff;
|
|
border-bottom-width: 0;
|
|
margin-top:1px;
|
|
cursor: pointer;
|
|
float: right;
|
|
margin-right: 50px;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.export-container {
|
|
max-width: 50%;
|
|
margin: 20px auto;
|
|
border: 1px solid #fff;
|
|
padding: 20px;
|
|
text-align: center;
|
|
border-radius: 7px;
|
|
}
|
|
.export {
|
|
max-width: 200px;
|
|
margin: auto;
|
|
}
|
|
.selected-rows-action-box {
|
|
float: right;
|
|
}
|
|
.selected-rows-action-box .btn {
|
|
width: 200px;
|
|
margin: 5px;
|
|
height: 38px;
|
|
}
|
|
</style>
|
|
<nav class="row">
|
|
<button
|
|
type="button"
|
|
class="txlist-view-btn btn radio left">
|
|
{{ _("History") }}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="utxo-view-btn btn radio right">
|
|
UTXO
|
|
</button>
|
|
</nav>
|
|
<div class="pagination-container">
|
|
<a class="pagination-back pagination-arrow pagination-disabled">‹</a>
|
|
{{ _("Page") }} <input class="pagination-idx" type="number" step="1" min="1" value="1"></input><span class="pagination-counter"></span>
|
|
<a class="pagination-next pagination-arrow pagination-disabled">›</a>
|
|
<br>
|
|
</div>
|
|
<div class="search-container">
|
|
<!--Left part of the toolbar-->
|
|
<div style="flex: 1;">
|
|
{% include "includes/page-limit-select.html" %}
|
|
</div>
|
|
<!--Middle part of the toolbar-->
|
|
<input class="search" type="text" placeholder="Search..." />
|
|
<!--Placeholder for right part of the toolbar-->
|
|
<div style="flex: 1;"></div>
|
|
</div>
|
|
<div class="selected-rows-action-box row hidden">
|
|
<form class="freeze-tx-form" method="POST">
|
|
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
|
|
<button type="button" class="btn freeze-tx-btn">
|
|
<tool-tip title="Freezing UTXOs">
|
|
{{ _("Freezing a UTXO will make Specter disallow spending it.") }}<br><br>
|
|
{{ _("This is useful especially for dealing with a dust attack, where you don't want to accidentally spend the dust received.") }}"
|
|
</tool-tip>
|
|
Un/Freeze UTXOs
|
|
</button>
|
|
</form>
|
|
<form class="compose-tx-form" method="POST">
|
|
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
|
|
<button type="button" class="btn compose-tx-btn">{{ _("Create new transaction") }}</button>
|
|
</form>
|
|
</div>
|
|
<div class="export-container hidden">
|
|
<h1>{{ _("Export transactions to CSV") }}</h1>
|
|
<div class="{% if not specter.price_check or not (specter.alt_rate and specter.alt_symbol) %}hidden{% endif %}">
|
|
{{ _("Fetch historical price data") }}:
|
|
<label class="switch">
|
|
<input type="checkbox" class="export-prices">
|
|
<span class="slider"></span>
|
|
</label><br><br>
|
|
<div class="note">
|
|
{{ _("If enabled, specter will try to fetch historical prices for each transaction date from the configured price provider.") }}
|
|
<br><b>{{ _("This has privacy implications!") }}</b>
|
|
</div><br>
|
|
</div>
|
|
<a class="export btn" download>{{ _("Export") }}</a>
|
|
</div>
|
|
<table class="tx-table">
|
|
<thead>
|
|
<tr>
|
|
<th value="category" class="category-header"><div class="category-arrow"></div></th>
|
|
<th value="txid" class="optional txid-header">TxID<div class="txid-arrow"></div></th>
|
|
<th value="label" class="optional label-header">{{ _("Label") }}<div class="label-arrow"></div></th>
|
|
<th value="amount" class="amount-header">{{ _("Amount") }}<div class="amount-arrow"></div></th>
|
|
<th value="time" class="time-header">{{ _("Time") }}<div class="time-arrow up-arrow"></div></th>
|
|
<th value="confirmations" class="confirmations-header">{{ _("Confirmations") }}<div class="confirmations-arrow"></div></th>
|
|
<th value="blockhash" class="optional hidden blockhash-header">
|
|
{% include "includes/merkletooltip.html" %}
|
|
{{ _("Block Hash") }}
|
|
</th>
|
|
<th style="width: 50px; padding: 10px;"><button type="button" class="export-btn btn" style="width: 50px;">{{ _("Export") }}</button></th>
|
|
</tr>
|
|
</thead>
|
|
<tr class="empty">
|
|
<td></td>
|
|
<td>{{ _("Fetching transactions...") }}</td>
|
|
<td class="optional"></td>
|
|
<td class="optional"></td>
|
|
<td></td>
|
|
<td></td>
|
|
<td class="optional hidden blockhash-empty"></td>
|
|
<td></td>
|
|
</tr>
|
|
<tr class="summary-row hidden">
|
|
<td></td>
|
|
<td class="summary-tx-count"></td>
|
|
<td class="optional"></td>
|
|
<td class="summary-amount"></td>
|
|
<td class="optional"></td>
|
|
<td></td>
|
|
<td class="optional hidden blockhash-summary"></td>
|
|
<td></td>
|
|
</tr>
|
|
<tbody class="tx-tbody">
|
|
</tbody>
|
|
</table>
|
|
</template>
|
|
|
|
<script type="text/javascript">
|
|
/**
|
|
* Custom element for showing a table of transactions.
|
|
* Can be configured to show:
|
|
* - The transaction history of a wallet (set attribute `type` to "txlist" and `wallet` to the wallet's alias)
|
|
* - The UTXO list of a wallet (set attribute `type` to "utxo" and `wallet` to the wallet's alias)
|
|
* - The transaction history of all the user's wallets (set attribute `type` to "txlist" and `wallet` to null)
|
|
* - The UTXO list of all the user's wallets (set attribute `type` to "utxo" and `wallet` to null)
|
|
* The uses Specter's API to fetch transactions every time the user changes its parameters.
|
|
* These parameters are configured using components included in this table element:
|
|
* - Tabs for switching between History (`type="txlist"`) and UTXO (`type="utxo"`) list types
|
|
* - Sorting transactions by any column (ascending or descending)
|
|
* - Search bar to filter transactions using free text search
|
|
* - Configuring limit for transactions displayed per page
|
|
* - Pagination, jumping between pages and showing the total pages count
|
|
* - Export - export the full (unfiltered) list from which the table is drawing its data as CSV (optionally with price history)
|
|
*
|
|
* The table also supports:
|
|
* - Showing amounts in either BTC or sats (set attribute `btc-unit` to either "btc" or "sat")
|
|
* - Showing prices next to the amounts (set attribute `price` to the BTC price and symbol to the symbol of the currency you're pricing at)
|
|
* - Showing validated blockhash column (set attribute `blockhash` to either "true" or "false")
|
|
*/
|
|
class TxTableElement extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
// Create a shadow root
|
|
var shadow = this.attachShadow({mode: 'open'});
|
|
var style = document.getElementById('tx-table').content;
|
|
var clone = style.cloneNode(true);
|
|
this.el = clone.querySelector(".tx-table");
|
|
this.tbody = clone.querySelector(".tx-tbody");
|
|
|
|
// Empty row element
|
|
this.emptyRow = clone.querySelector(".empty");
|
|
|
|
// Blockhash header and empty column
|
|
this.blockhashHeader = clone.querySelector(".blockhash-header");
|
|
this.blockhashEmpty = clone.querySelector(".blockhash-empty");
|
|
this.blockhashSummary = clone.querySelector(".blockhash-summary");
|
|
|
|
// Tabs (History/ UTXO)
|
|
this.txlistViewBtn = clone.querySelector(".txlist-view-btn");
|
|
this.utxoViewBtn = clone.querySelector(".utxo-view-btn");
|
|
|
|
// Pagination
|
|
this.paginationNext = clone.querySelector(".pagination-next");
|
|
this.paginationBack = clone.querySelector(".pagination-back");
|
|
this.paginationContainer = clone.querySelector(".pagination-container");
|
|
this.paginationCounter = clone.querySelector(".pagination-counter");
|
|
this.paginationIdxInput = clone.querySelector(".pagination-idx");
|
|
this.pageLimitSelect = clone.querySelector(".page-limit-select");
|
|
|
|
// Search bar element
|
|
this.searchInput = clone.querySelector(".search");
|
|
|
|
// Sorting headers and arrows
|
|
this.categoryHeader = clone.querySelector(".category-header");
|
|
this.txidHeader = clone.querySelector(".txid-header");
|
|
this.labelHeader = clone.querySelector(".label-header");
|
|
this.amountHeader = clone.querySelector(".amount-header");
|
|
this.timeHeader = clone.querySelector(".time-header");
|
|
this.confirmationsHeader = clone.querySelector(".confirmations-header");
|
|
this.categoryArrow = clone.querySelector(".category-arrow");
|
|
this.txidArrow = clone.querySelector(".txid-arrow");
|
|
this.labelArrow = clone.querySelector(".label-arrow");
|
|
this.amountArrow = clone.querySelector(".amount-arrow");
|
|
this.timeArrow = clone.querySelector(".time-arrow");
|
|
this.confirmationsArrow = clone.querySelector(".confirmations-arrow");
|
|
|
|
// Summary row
|
|
this.summaryRow = clone.querySelector(".summary-row");
|
|
this.summaryTxCount = clone.querySelector(".summary-tx-count");
|
|
this.summaryAmount = clone.querySelector(".summary-amount");
|
|
|
|
// Export button
|
|
this.exportBtn = clone.querySelector(".export-btn");
|
|
this.exportContainer = clone.querySelector(".export-container");
|
|
this.export = clone.querySelector(".export");
|
|
this.exportPricesSwitch = clone.querySelector(".export-prices");
|
|
|
|
// Selected rows
|
|
this.selectedRowsActionBox = clone.querySelector(".selected-rows-action-box");
|
|
this.composeTxForm = clone.querySelector(".compose-tx-form");
|
|
this.composeTxBtn = clone.querySelector(".compose-tx-btn");
|
|
this.freezeTxForm = clone.querySelector(".freeze-tx-form");
|
|
this.freezeTxBtn = clone.querySelector(".freeze-tx-btn");
|
|
this.selectedRows = [];
|
|
|
|
this.txlistViewBtn.onclick = () => {
|
|
if (!this.txlistViewBtn.classList.contains("checked")) {
|
|
this.utxoViewBtn.classList.remove("checked");
|
|
this.txlistViewBtn.classList.add("checked");
|
|
this.setAttribute("type","txlist");
|
|
}
|
|
}
|
|
|
|
this.utxoViewBtn.onclick = () => {
|
|
if (!this.utxoViewBtn.classList.contains("checked")) {
|
|
this.txlistViewBtn.classList.remove("checked");
|
|
this.utxoViewBtn.classList.add("checked");
|
|
this.setAttribute("type","utxo");
|
|
}
|
|
}
|
|
|
|
// Init call id to avoid fetch returning after another one triggered
|
|
this.callId = 0
|
|
|
|
|
|
// Initialize default sort, search, limit and page parameters
|
|
this.idx = 0;
|
|
this.limit = 50;
|
|
this.search = "";
|
|
this.sortby = "time";
|
|
this.sortdir = "desc";
|
|
this.pageCount = 1;
|
|
|
|
// Next page action
|
|
this.paginationNext.onclick = () => {
|
|
this.idx++;
|
|
fetchTxItems(this);
|
|
}
|
|
|
|
// Previous page action
|
|
this.paginationBack.onclick = () => {
|
|
this.idx--;
|
|
fetchTxItems(this);
|
|
}
|
|
|
|
// Listen to changes in page number selection input
|
|
this.paginationIdxInput.onchange = () => {
|
|
let newIdx = parseInt(this.paginationIdxInput.value);
|
|
if (isNaN(newIdx) || (newIdx < 1 || newIdx > this.pageCount)) {
|
|
this.paginationIdxInput.value = this.idx + 1;
|
|
return;
|
|
}
|
|
this.idx = newIdx - 1;
|
|
fetchTxItems(this);
|
|
}
|
|
|
|
// Listen to changes in search bar
|
|
this.searchInput.onchange = () => {
|
|
this.search = this.searchInput.value;
|
|
this.export.href = this.export.href.split('?')[0] + `?exportPrices=${this.exportPricesSwitch.checked}&search=${this.search}&sortby=${this.sortby}&sortdir=${this.sortdir}`;
|
|
fetchTxItems(this);
|
|
}
|
|
|
|
// Listen to changes in page limit
|
|
this.pageLimitSelect.onchange = () => {
|
|
this.limit = this.pageLimitSelect.value;
|
|
fetchTxItems(this);
|
|
}
|
|
|
|
// Setup sorting functionality for each column of the table header
|
|
let sortHeaders = [
|
|
this.categoryHeader,
|
|
this.txidHeader,
|
|
this.labelHeader,
|
|
this.amountHeader,
|
|
this.timeHeader,
|
|
this.confirmationsHeader
|
|
];
|
|
|
|
for (let header of sortHeaders) {
|
|
header.onclick = () => {
|
|
if (header.classList.toString().indexOf(this.sortby) != -1) {
|
|
// flip direction
|
|
this.el.querySelector(`.${this.sortby}-arrow`).classList.remove(this.sortdir == 'asc' ? 'down-arrow' : 'up-arrow');
|
|
this.el.querySelector(`.${this.sortby}-arrow`).classList.add(this.sortdir == 'asc' ? 'up-arrow' : 'down-arrow');
|
|
this.sortdir = this.sortdir == 'asc' ? 'desc' : 'asc';
|
|
this.export.href = this.export.href.split('?')[0] + `?exportPrices=${this.exportPricesSwitch.checked}&search=${this.search}&sortby=${this.sortby}&sortdir=${this.sortdir}`;
|
|
} else {
|
|
sortHeaders.forEach(el => {
|
|
let existingArrow = el.querySelector(`.${this.sortby}-arrow`)
|
|
if (existingArrow) {
|
|
existingArrow.classList.remove('up-arrow');
|
|
existingArrow.classList.remove('down-arrow');
|
|
}
|
|
});
|
|
this.sortby = header.getAttribute('value');
|
|
this.el.querySelector(`.${this.sortby}-arrow`).classList.add(this.sortdir == 'asc' ? 'down-arrow' : 'up-arrow');
|
|
this.export.href = this.export.href.split('?')[0] + `?exportPrices=${this.exportPricesSwitch.checked}&search=${this.search}&sortby=${this.sortby}&sortdir=${this.sortdir}`;
|
|
}
|
|
fetchTxItems(this);
|
|
}
|
|
}
|
|
|
|
// Export data
|
|
this.exportBtn.onclick = () => {
|
|
if (this.exportBtn.innerText == 'Done') {
|
|
this.exportContainer.classList.add('hidden');
|
|
this.exportBtn.innerText = 'Export';
|
|
} else {
|
|
this.exportContainer.classList.remove('hidden');
|
|
this.exportBtn.innerText = 'Done';
|
|
}
|
|
}
|
|
|
|
this.export.onclick = () => {
|
|
this.exportContainer.classList.add('hidden');
|
|
this.exportBtn.innerText = 'Export';
|
|
}
|
|
|
|
// Export prices switch toggle
|
|
this.exportPricesSwitch.onchange = () => {
|
|
this.export.href = this.export.href.split('?')[0] + `?exportPrices=${this.exportPricesSwitch.checked}&search=${this.search}&sortby=${this.sortby}&sortdir=${this.sortdir}`;
|
|
}
|
|
|
|
this.composeTxBtn.onclick = () => {
|
|
for (let tx of this.selectedRows) {
|
|
this.composeTxForm.innerHTML += `
|
|
<input type="checkbox" checked class="hidden" name="coinselect" value="${tx.txid}, ${tx.vout}">
|
|
`
|
|
}
|
|
this.composeTxForm.action = `{{ url_for('wallets_endpoint.send_new', wallet_alias='WALLET_ALIAS') }}`.replace("WALLET_ALIAS", this.wallet);
|
|
this.composeTxForm.submit();
|
|
}
|
|
|
|
this.freezeTxBtn.onclick = () => {
|
|
for (let tx of this.selectedRows) {
|
|
this.freezeTxForm.innerHTML += `
|
|
<input type="checkbox" checked class="hidden" name="selected_utxo" value="${tx.txid}:${tx.vout}">
|
|
`
|
|
}
|
|
this.freezeTxForm.innerHTML += `<input type="hidden" name="action" value="freezeutxo"/>`
|
|
this.freezeTxForm.action = `{{ url_for('wallets_endpoint.history', wallet_alias='WALLET_ALIAS') }}`.replace("WALLET_ALIAS", this.wallet);
|
|
this.freezeTxForm.submit();
|
|
}
|
|
|
|
// Attach the created element to the shadow dom
|
|
shadow.appendChild(clone);
|
|
}
|
|
|
|
static get observedAttributes() {
|
|
return ['blockhash', 'btc-unit', 'price', 'symbol', 'type', 'hide-sensitive-info', 'wallet'];
|
|
}
|
|
|
|
/**
|
|
* Listens to changes on the following attributes:
|
|
* - blockhash: Should show blockhash column. Either "true" or "false"
|
|
* - btc-unit: Bitcoin unit to display amounts with. Either "btc" or "sat"
|
|
* - price: BTC price for price calculations
|
|
* - symbol: Currency symbol for price calculations
|
|
* - type: Transactions list type to load. Either "txlist" or "utxo"
|
|
* - hide-sensitive-info: Mask user sensitive info. Either "true" or "false"
|
|
* - wallet: The wallet alias (null to get all wallets combined)
|
|
*/
|
|
attributeChangedCallback(attrName, oldValue, newValue) {
|
|
if (this.getAttribute('blockhash') == 'true' && this.getAttribute('type') == "txlist" && this.getAttribute('wallet')) {
|
|
this.blockhashHeader.classList.remove('hidden');
|
|
this.blockhashEmpty.classList.remove('hidden');
|
|
this.blockhashSummary.classList.remove('hidden');
|
|
} else {
|
|
this.blockhashHeader.classList.add('hidden');
|
|
this.blockhashEmpty.classList.add('hidden');
|
|
this.blockhashSummary.classList.add('hidden');
|
|
}
|
|
if (
|
|
this.blockhash != this.getAttribute('blockhash') ||
|
|
this.btcUnit != this.getAttribute('btc-unit') ||
|
|
this.price != this.getAttribute('price') ||
|
|
this.symbol != this.getAttribute('symbol') ||
|
|
this.listType != this.getAttribute('type') ||
|
|
this.hideSensitiveInfo != this.getAttribute('hide-sensitive-info') ||
|
|
this.wallet != this.getAttribute('wallet')
|
|
) {
|
|
this.blockhash = this.getAttribute('blockhash');
|
|
this.btcUnit = this.getAttribute('btc-unit');
|
|
this.price = this.getAttribute('price');
|
|
this.symbol = this.getAttribute('symbol');
|
|
this.listType = this.getAttribute('type');
|
|
this.hideSensitiveInfo = this.getAttribute('hide-sensitive-info') == 'true';
|
|
this.wallet = this.getAttribute('wallet');
|
|
if (!this.listType) {
|
|
return
|
|
}
|
|
fetchTxItems(this);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Fetches txlist from the Specter API and loads the result into TxRowElement
|
|
* @param self - The TxTableElement itself.
|
|
*/
|
|
async function fetchTxItems(self) {
|
|
self.callId++;
|
|
// Clean up from existing transaction list
|
|
self.summaryRow.classList.add('hidden');
|
|
self.emptyRow.children[1].innerText = '{{ _("Fetching transactions...") }}';
|
|
self.emptyRow.classList.remove('hidden');
|
|
self.tbody.querySelectorAll('tx-row').forEach((row) => {row.remove()});
|
|
if (!self.listType) {
|
|
self.emptyRow.children[1].innerText = '{{ _("Failed to fetch transactions...") }}';
|
|
return
|
|
}
|
|
|
|
if (self.listType == "txlist") {
|
|
self.txlistViewBtn.classList.add("checked");
|
|
} else {
|
|
self.utxoViewBtn.classList.add("checked");
|
|
}
|
|
|
|
// Select the correct URL based on the list type and selected wallet
|
|
let url;
|
|
switch (self.listType) {
|
|
case "txlist":
|
|
if (self.wallet) {
|
|
self.export.href = `{{ url_for('wallets_endpoint.tx_history_csv', wallet_alias='WALLET_ALIAS') }}`.replace("WALLET_ALIAS", self.wallet);
|
|
url = `{{ url_for('wallets_endpoint.txlist', wallet_alias='WALLET_ALIAS') }}`.replace("WALLET_ALIAS", self.wallet);
|
|
} else {
|
|
self.export.href = `{{ url_for('wallets_endpoint.wallet_overview_txs_csv') }}`;
|
|
url = `{{ url_for('wallets_endpoint.wallets_overview_txlist') }}`;
|
|
}
|
|
break;
|
|
case "utxo":
|
|
if (self.wallet) {
|
|
self.export.href = `{{ url_for('wallets_endpoint.utxo_csv', wallet_alias='WALLET_ALIAS') }}`.replace("WALLET_ALIAS", self.wallet);
|
|
url = `{{ url_for('wallets_endpoint.utxo_list', wallet_alias='WALLET_ALIAS') }}`.replace("WALLET_ALIAS", self.wallet);
|
|
} else {
|
|
self.export.href = `{{ url_for('wallets_endpoint.wallet_overview_utxo_csv') }}`;
|
|
url = `{{ url_for('wallets_endpoint.wallets_overview_utxo_list') }}`;
|
|
}
|
|
break
|
|
default:
|
|
self.export.href = ``
|
|
self.emptyRow.children[1].innerText = '{{ _("Failed to fetch transactions...") }} ';
|
|
return
|
|
}
|
|
|
|
if (self.export.href != '') {
|
|
self.export.href += `?exportPrices=${self.exportPricesSwitch.checked}&search=${self.search}&sortby=${self.sortby}&sortdir=${self.sortdir}`;
|
|
}
|
|
|
|
// Prepare form data with all relevant parameters
|
|
var formData = new FormData();
|
|
formData.append('idx', self.idx);
|
|
formData.append('limit', self.limit);
|
|
formData.append('search', self.search);
|
|
formData.append('sortby', self.sortby);
|
|
formData.append('sortdir', self.sortdir);
|
|
formData.append('csrf_token', '{{ csrf_token() }}');
|
|
try {
|
|
let callId = self.callId;
|
|
const response = await fetch(
|
|
url,
|
|
{
|
|
method: 'POST',
|
|
body: formData
|
|
}
|
|
);
|
|
if (callId != self.callId) {
|
|
return;
|
|
}
|
|
if(response.status != 200){
|
|
showError(await response.text());
|
|
return;
|
|
}
|
|
const jsonResponse = await response.json();
|
|
let showBlockhash = !self.blockhashHeader.classList.contains('hidden');
|
|
|
|
if ("txlist" in jsonResponse) {
|
|
// Populate the table with TXRowElement objects using the fetched txlist
|
|
if (jsonResponse.txlist) {
|
|
let txlist = JSON.parse(jsonResponse.txlist);
|
|
let summaryTxCount = 0;
|
|
let summaryAmount = 0;
|
|
let summaryPrice = 0;
|
|
for (let tx of txlist) {
|
|
let txRow = document.createRange().createContextualFragment(`
|
|
<tx-row
|
|
data-btc-unit="${self.btcUnit ? self.btcUnit : 'btc'}"
|
|
data-price="${self.price ? self.price : 0}"
|
|
data-symbol="${self.symbol ? self.symbol : ''}"
|
|
data-tx='${JSON.stringify(tx).replace(/[\(]/g, "(").replace(/[\)]/g, ")").replace(/[\/]/g, "/").replace(/[\']/g, "'")}'
|
|
data-wallet="${(self.wallet ? self.wallet : tx.wallet_alias)}"
|
|
data-show-blockhash="${showBlockhash}"
|
|
data-mode="${self.getAttribute('type') + (self.wallet ? "" : "-overview")}"
|
|
data-hide-sensitive-info="${self.hideSensitiveInfo}">
|
|
</tx-row>
|
|
`)
|
|
self.tbody.append(txRow);
|
|
summaryTxCount++;
|
|
summaryAmount += parseFloat(self.tbody.children[self.tbody.children.length - 1].shadowRoot.querySelector('.amount').innerText.replace(',', ''));
|
|
summaryPrice += parseFloat(self.tbody.children[self.tbody.children.length - 1].shadowRoot.querySelector('.amount-price').innerText.replace(`(${self.symbol}`, '').replace(')', '').replace(',', ''));
|
|
}
|
|
|
|
if (summaryTxCount > 0 && self.search) {
|
|
self.summaryTxCount.innerText = `Transactions count: ${summaryTxCount}`;
|
|
self.summaryAmount.innerHTML = `Total amount: ${numberWithCommas(summaryAmount.toFixed(8))}`;
|
|
if (summaryPrice) {
|
|
self.summaryAmount.innerHTML += ` <span class="note">(${self.symbol}${numberWithCommas(summaryPrice.toFixed(2))})</span>`
|
|
}
|
|
self.summaryRow.classList.remove('hidden');
|
|
}
|
|
// Pagination
|
|
self.pageCount = jsonResponse.pageCount;
|
|
// Show empty row if no transaction found
|
|
if (self.pageCount == 0 || txlist.length == 0) {
|
|
self.emptyRow.classList.remove('hidden');
|
|
self.emptyRow.children[1].innerText = '{{ _("No transactions found...") }}';
|
|
self.pageCount = 1;
|
|
} else {
|
|
self.emptyRow.classList.add('hidden');
|
|
}
|
|
|
|
// If index is greater than the pages count fetch again for the last page
|
|
if (self.idx >= self.pageCount) {
|
|
self.idx = self.pageCount - 1;
|
|
fetchTxItems(self)
|
|
}
|
|
|
|
self.paginationIdxInput.value = self.idx + 1;
|
|
|
|
// If there are multiple pages show the pagination bar
|
|
if (self.pageCount > 1) {
|
|
self.paginationContainer.classList.remove('hidden');
|
|
self.paginationCounter.innerText = `of ${self.pageCount}`;
|
|
self.paginationIdxInput.max = self.pageCount;
|
|
} else {
|
|
self.paginationContainer.classList.add('hidden');
|
|
}
|
|
|
|
// Disable pagination back button if on the first page
|
|
if (self.idx == 0) {
|
|
self.paginationBack.classList.add('pagination-disabled');
|
|
} else {
|
|
self.paginationBack.classList.remove('pagination-disabled');
|
|
}
|
|
|
|
// Disable pagination next button if on the last page
|
|
if (self.idx + 1 == self.pageCount) {
|
|
self.paginationNext.classList.add('pagination-disabled');
|
|
} else {
|
|
self.paginationNext.classList.remove('pagination-disabled');
|
|
}
|
|
|
|
self.selectedRows = [];
|
|
self.selectedRowsActionBox.classList.add('hidden');
|
|
|
|
for (let txRow of self.shadowRoot.querySelectorAll('tx-row')) {
|
|
txRow.addEventListener('txRowSelected', e=>{
|
|
let selectedRow = e.target;
|
|
if (e.detail.selected) {
|
|
self.selectedRows.push({
|
|
txid: e.detail.txid,
|
|
vout: e.detail.vout,
|
|
amount: e.detail.amount.toFixed(8)
|
|
});
|
|
} else {
|
|
self.selectedRows = self.selectedRows.filter(row => !(row.txid === e.detail.txid && row.vout === e.detail.vout))
|
|
}
|
|
if (self.selectedRows.length == 0) {
|
|
self.selectedRowsActionBox.classList.add('hidden');
|
|
} else {
|
|
self.selectedRowsActionBox.classList.remove('hidden');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
return
|
|
};
|
|
showError('{{ _("Failed to fetch transactions list.") }}')
|
|
} catch(e) {
|
|
console.log("Caught error: ", e);
|
|
showError(`{{ _("Failed to fetch transactions list.") }}: ${e}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shows a <tx-data> element popup for specified txid.
|
|
* @param btcUnit - Bitcoin unit to display amounts with. Either "btc" or "sat"
|
|
* @param price - BTC price for price calculations
|
|
* @param symbol - Currency symbol for price calculations
|
|
* @param txid - ID of the transaction to show.
|
|
* @param wallet - The wallet alias of the wallet the transaction is associated with.
|
|
*/
|
|
function showTxData(btcUnit, price, symbol, txid, wallet) {
|
|
if (wallet) {
|
|
let txDataPopup = document.getElementById('tx-popup');
|
|
txDataPopup.innerHTML = `
|
|
<tx-data
|
|
data-btc-unit="${btcUnit ? btcUnit : 'btc'}"
|
|
data-price="${price ? price : 0}"
|
|
data-symbol="${symbol ? symbol : ''}"
|
|
data-txid="${txid}"
|
|
data-tx-wallet="${wallet}">
|
|
</tx-data>`;
|
|
showPageOverlay('tx-popup');
|
|
} else {
|
|
copyText(txid, `{{ _("Copied transaction") }}: ${ txid }`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Triggered when a tx-row is marked as selected/ unselected .
|
|
*
|
|
*/
|
|
function txRowToggled(txRow) {
|
|
// if ()
|
|
}
|
|
|
|
customElements.define('tx-table', TxTableElement);
|
|
</script>
|