mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
* fix create_order issue * fix url and better error-handling * basic balances tab * activating services * Service management * Improve settings page and default to it if token unset * Fix history tab * Fix trade error handling * Add withdraw * calling specter-cloud for creating vaultoro orders * Improve trade screen and fixes * sidebar fix * black * refactor Service integration * refactor to have Service Classes like manifests * maturity * dynamic initialisation of service-classes and blueprints * fix sidebar_services * migrated templates and static into vaultoro folder * refactor config to manifest * some minor things * swan initial * rename and fix test * adding ServiceApiKeyStorageUserAware * fix * store the access token * directory indirection to shield templates from each others blueprint * first attempts with automatic withdrawals * proper tab highlighting * Update service_apikey_storage.py * Update oauth2_success.jinja * Awaiting refresh_token support * Service logo display on Addresses * Associate addr with a Service * address-data component reorg Separates the presentation html from the data as much as possible. * Services data/icon added to tx History * Now hitting the updated Swan endpoint to save deposit addrs * Simplified injecting Services data into JS * Reducing js calls back to server in tx-data; templatizing utxo in/outs * renaming "reserving" to "associating" an Address with a Service. * rename `manifest.py` files to `service.py`. * rename "api_data" to "service_data" to make the storage a bit more generalized. `ServiceApiKeyStorage` is now `ServiceEncryptedStorage` to match. * Beginning of factoring out Swan api to its own `api.py` file; need to rectify with `swan_client.py`. * Removed tx-table/row/data changes and address-table/row/data Kept only the bare minimum changes required to display the Services icon, plus optimizations. * deleted no longer used CustomElement * Adding services docs to mkdocs * Configuration for Services * more clever configuration * fix test * deleted swan_client * Changing the address abbreviation format to 7...7 and little big fix for not vertically aligned addresses in Firefox. * Better state management if Auth method changes * Cleanup, better user messaging; pulling Service methods out of controller and User * Redirect to services endpoint after setting up authentication. * PR cleanup, bug fixes, test suite updates * Fixed test case problem * First fix for delete API key button. * Service hooks; Option to fully remove Swan Integration; Logout clears plaintext_user_secret * Restoring bugfix from @moneymanolis * cleanup and black * Make service-decovery in AppImage work * import hashlib, maybe fix cypress * Update service_encrypted_storage.py * further bugfix on update * TODO: remove debugging in client.py before first release * fix controller-test * fix singleton testing issues Co-authored-by: Kim Neunert <k9ert@gmx.de> Co-authored-by: benk10 <ben.kaufman10@gmail.com> Co-authored-by: Kim Neunert <kneunert@gmail.com> Co-authored-by: moneymanolis <moneymanolis@protonmail.com>
376 lines
18 KiB
HTML
376 lines
18 KiB
HTML
<template id="tx-row">
|
|
<style>
|
|
.frozen {
|
|
background: #546773;
|
|
}
|
|
.svg-send {
|
|
filter: invert(63%) sepia(67%) saturate(851%) hue-rotate(346deg) brightness(107%) contrast(92%);
|
|
}
|
|
.svg-receive, .svg-immature, .svg-generate {
|
|
filter: invert(61%) sepia(48%) saturate(3609%) hue-rotate(189deg) brightness(91%) contrast(94%);
|
|
}
|
|
.svg-selftransfer, .svg-mixed {
|
|
filter: invert(77%) sepia(68%) saturate(3384%) hue-rotate(44deg) brightness(112%) contrast(74%);
|
|
}
|
|
.svg-cancelled {
|
|
filter: invert(20%) sepia(32%) saturate(4838%) hue-rotate(332deg) brightness(81%) contrast(97%);
|
|
}
|
|
|
|
.tx-row:hover {
|
|
background-color: var(--cmap-bg-lightest);
|
|
}
|
|
.address {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.service-icon {
|
|
margin-top: -5px;
|
|
margin-right: 0.5em;
|
|
height:24px;
|
|
vertical-align: middle;
|
|
}
|
|
</style>
|
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.css') }}">
|
|
<tr class="tx-row">
|
|
<td id="column-callbackaction" class="callbackaction">
|
|
<input class="checkbox coin_select_checkbox" type="checkbox" name="coinselect" value="" >
|
|
</td>
|
|
<td id="column-category">
|
|
<img class="category" style="vertical-align: middle; margin-right: 10px;"/>
|
|
<img class="frozen-img hidden svg-white" style="vertical-align: middle; height: 28px;" src="{{ url_for('static', filename='img/snowflake.svg') }}"/>
|
|
</td>
|
|
<td id="column-txid" class="tx scroll optional txid">
|
|
<span class="explorer-link"></span>
|
|
</td>
|
|
<td id="column-label" class="tx optional address"></td>
|
|
<td id="column-amount">
|
|
<span class="amount"></span>
|
|
<span class="amount-price note hidden">()</span>
|
|
</td>
|
|
<td id="column-time" class="time"></td>
|
|
<td id="column-confirmations" >
|
|
<span class="confirmations"></span>
|
|
<button class="rbf btn optional hidden" style="width: 130px; float: right;" type="button">{{ _("Speed up") }}</button>
|
|
<button class="rbf-cancel danger btn optional hidden" style="width: 130px; float: right; margin-right: 10px;" type="button">{{ _("Cancel transaction") }}</button>
|
|
</td>
|
|
<td class="hidden optional blockhash"></td>
|
|
<td id="column-action"><input class="select-tx-value" type="hidden" value=""><img style="vertical-align: middle;" class="select-tx-img" src="{{ url_for('static', filename='img/checkbox-untick.svg') }}" width="25px"></tool-tip></td>
|
|
</tr>
|
|
</template>
|
|
|
|
<script type="text/javascript">
|
|
class TxRowElement extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
// Create a shadow root
|
|
var shadow = this.attachShadow({mode: 'open'});
|
|
var style = document.getElementById('tx-row').content;
|
|
var clone = style.cloneNode(true);
|
|
this.el = clone.querySelector(".tx-row");
|
|
|
|
this.callbackaction = clone.querySelector(".callbackaction");
|
|
this.category = clone.querySelector(".category");
|
|
this.txid = clone.querySelector(".txid .explorer-link");
|
|
this.address = clone.querySelector(".address");
|
|
this.serviceIcon = clone.querySelector(".service-icon");
|
|
this.amountText = clone.querySelector(".amount");
|
|
this.amountPrice = clone.querySelector(".amount-price");
|
|
this.time = clone.querySelector(".time");
|
|
this.confirmations = clone.querySelector(".confirmations");
|
|
this.blockhash = clone.querySelector(".blockhash");
|
|
this.rbf = clone.querySelector(".rbf");
|
|
this.rbfCancel = clone.querySelector(".rbf-cancel");
|
|
this.selectTxValue = clone.querySelector(".select-tx-value");
|
|
this.selectTxImg = clone.querySelector(".select-tx-img");
|
|
this.frozenImg = clone.querySelector(".frozen-img");
|
|
|
|
// Attach the created element to the shadow dom
|
|
shadow.appendChild(clone);
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.tx = JSON.parse(this.getAttribute('data-tx'));
|
|
this.wallet = this.getAttribute('data-wallet');
|
|
this.showBlockhash = this.getAttribute('data-show-blockhash') == 'true';
|
|
this.btcUnit = this.getAttribute('data-btc-unit');
|
|
this.price = this.getAttribute('data-price');
|
|
this.symbol = this.getAttribute('data-symbol');
|
|
this.mode = this.getAttribute('data-mode');
|
|
this.hideSensitiveInfo = this.getAttribute('data-hide-sensitive-info') == 'true';
|
|
|
|
// Set data for customaction
|
|
this.callbackaction.childNodes[1].value=`${this.tx['txid']},${this.tx['vout']}`
|
|
this.callbackaction.childNodes[1].amount = `${this.tx['amount']}` // a bit hackish, doing that to have access via this in next line
|
|
this.callbackaction.childNodes[1].addEventListener("change", (e) => {
|
|
let event = new CustomEvent('txRowCustomSelected', { detail: {
|
|
txid: this.tx.txid,
|
|
vout: this.tx.vout,
|
|
amount: this.amount
|
|
} }
|
|
);
|
|
this.dispatchEvent(event);
|
|
})
|
|
|
|
// Set category image
|
|
this.category.src = this.getCategoryImg(this.tx.category, this.tx.confirmations > 0);
|
|
this.category.classList.add('svg-' + this.tx.category)
|
|
|
|
// TODO:
|
|
// Setup tx popup for mobile
|
|
if (this.tx.locked) {
|
|
this.el.classList.add('frozen');
|
|
this.frozenImg.classList.remove('hidden')
|
|
}
|
|
|
|
|
|
// Set txid
|
|
this.txid.innerHTML = this.hideSensitiveInfo ? '############################################' : this.tx.txid.slice(0,11) + '…';
|
|
if (!this.hideSensitiveInfo) {
|
|
this.txid.onclick = () => {
|
|
showTxData(
|
|
this.btcUnit,
|
|
this.price,
|
|
this.symbol,
|
|
this.tx.txid,
|
|
this.wallet
|
|
);
|
|
}
|
|
}
|
|
|
|
if (window.innerWidth < 690) {
|
|
this.el.onclick = () => {
|
|
showTxData(
|
|
this.btcUnit,
|
|
this.price,
|
|
this.symbol,
|
|
this.tx.txid,
|
|
this.wallet
|
|
);
|
|
}
|
|
}
|
|
|
|
// Set address-label
|
|
if (Array.isArray(this.tx.address)) {
|
|
this.address.innerText = this.hideSensitiveInfo ? '#########' : `${this.tx.address.length} {{ _("Recipients") }}`;
|
|
} else {
|
|
if (this.hideSensitiveInfo) {
|
|
this.address.innerHTML = '#########';
|
|
} else {
|
|
if ("service_id" in this.tx) {
|
|
this.address.innerHTML = `<img class="service-icon" src="/svc/${this.tx.service_id}/static/${services[this.tx.service_id].icon}">`;
|
|
}
|
|
if (this.tx.label !== undefined) {
|
|
this.address.innerHTML += this.tx.label;
|
|
} else {
|
|
this.address.innerHTML += this.tx.address;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set amount
|
|
if (Array.isArray(this.tx.amount)) {
|
|
this.amount = parseFloat(this.tx.amount.reduce((a, b) => a + b, 0).toFixed(8))
|
|
} else {
|
|
this.amount = parseFloat(this.tx.amount.toFixed(8));
|
|
}
|
|
|
|
if (!this.price || !this.symbol || this.amount == 1e-8 || this.hideSensitiveInfo) {
|
|
this.amountPrice.innerText = '';
|
|
this.amountPrice.classList.add('hidden');
|
|
} else {
|
|
if (this.symbol == "$" || this.symbol == "£") {
|
|
this.amountPrice.innerText = `(${this.symbol}${numberWithCommas((parseFloat(this.price) * this.amount).toFixed(2))})`;
|
|
} else {
|
|
this.amountPrice.innerText = `(${numberWithCommas((parseFloat(this.price) * this.amount).toFixed(2))}${this.symbol})`;
|
|
}
|
|
if(!("assetlabel" in this.tx) || this.tx.assetlabel == "LBTC"){
|
|
this.amountPrice.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
if (this.btcUnit == 'sat' && this.amount != 1e-8) {
|
|
this.amount = parseInt(this.amount * 1e8);
|
|
}
|
|
|
|
if (this.amount == 1e-8 ) { //|| this.amount == 0) {
|
|
this.amountText.innerText = '{{ _("Confidential") }}';
|
|
} else {
|
|
if(this.hideSensitiveInfo){
|
|
this.amountText.innerHTML = '#########';
|
|
}else{
|
|
this.amountText.innerHTML = `${numberWithCommas(this.amount.toString())}`;
|
|
if("assetlabel" in this.tx){ // liquid
|
|
if (Array.isArray(this.tx.assetlabel)) { // multiple assets
|
|
let unique_assets = this.tx.assetlabel.filter((v, i, a) => { return a.indexOf(v) === i });
|
|
if (unique_assets.length == 1){
|
|
this.amountText.innerHTML += ` <asset-label data-asset="${this.tx.asset[0]}" data-label="${this.tx.assetlabel[0]}"></asset-label>`;
|
|
}else{
|
|
this.amountText.innerHTML = `${unique_assets.length} assets`;
|
|
}
|
|
}else{
|
|
this.amountText.innerHTML += ` <asset-label data-asset="${this.tx.asset}" data-label="${this.tx.assetlabel}"></asset-label>`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set confirmations
|
|
if (this.tx.confirmations > 0) {
|
|
this.confirmations.innerHTML = this.hideSensitiveInfo ? '########' : `${this.tx.confirmations}`;
|
|
} else if (this.tx.confirmations == 0) {
|
|
this.el.classList.add('unconfirmed');
|
|
this.confirmations.innerHTML = this.hideSensitiveInfo ? '########' : `{{ _("Unconfirmed") }}`;
|
|
} else {
|
|
this.confirmations.innerHTML = this.hideSensitiveInfo ? '########' : `{{ _("Cancelled (replaced by sender)") }}`;
|
|
this.category.src = `{{ url_for('static', filename='img') }}/cross.svg`;
|
|
this.category.classList.remove('svg-' + this.tx.category);
|
|
this.category.classList.add('svg-cancelled');
|
|
}
|
|
|
|
if ((this.tx.category == "send" || this.tx.category == "selftransfer") && this.tx["bip125-replaceable"] == "yes") {
|
|
this.rbf.classList.remove('hidden');
|
|
if (this.tx.category == "selftransfer") {
|
|
this.rbfCancel.classList.add('hidden');
|
|
} else {
|
|
this.rbfCancel.classList.remove('hidden');
|
|
this.rbfCancel.onclick = () => {
|
|
this.rbfPopup('cancel');
|
|
}
|
|
}
|
|
this.rbf.onclick = () => {
|
|
this.rbfPopup('speedup');
|
|
}
|
|
} else {
|
|
this.rbf.classList.add('hidden');
|
|
this.rbfCancel.classList.add('hidden');
|
|
}
|
|
|
|
// Set time
|
|
this.time.innerText = this.hideSensitiveInfo ? '###########' : (new Date(this.tx.time * 1000)).toLocaleString()
|
|
|
|
// Show blockhash
|
|
if (this.showBlockhash) {
|
|
this.blockhash.classList.remove('hidden');
|
|
if (
|
|
this.tx.blockhash &&
|
|
this.tx.validated_blockhash &&
|
|
this.tx.validated_blockhash == this.tx.blockhash
|
|
) {
|
|
this.blockhash.innerHTML = this.hideSensitiveInfo ? '############################################' : `
|
|
<explorer-link style="word-break: break-all;" data-type="block" data-value="${this.tx.blockhash}"></explorer-link><br>
|
|
`;
|
|
}
|
|
} else {
|
|
this.blockhash.classList.add('hidden');
|
|
}
|
|
|
|
// Select tx option
|
|
if (this.mode == 'utxo') {
|
|
this.selectTxImg.classList.remove('hidden');
|
|
this.selectTxImg.onclick = () => {
|
|
this.selectTxImg.src = this.selectTxValue.value ? `{{ url_for('static', filename='img/checkbox-untick.svg') }}` : `{{ url_for('static', filename='img/checkbox-tick.svg') }}`;
|
|
this.selectTxValue.value = this.selectTxValue.value ? "" : "true";
|
|
let event = new CustomEvent('txRowSelected', { detail: {
|
|
selected: this.selectTxValue.value == "true",
|
|
txid: this.tx.txid,
|
|
vout: this.tx.vout,
|
|
amount: this.amount
|
|
} });
|
|
this.dispatchEvent(event);
|
|
}
|
|
} else {
|
|
this.selectTxImg.classList.add('hidden');
|
|
}
|
|
|
|
// Hide columns
|
|
this.hideColumns = this.getAttribute("hide-columns")
|
|
if (this.hideColumns != "null") {
|
|
this.hideColumns.split(" ").forEach((column_name) => {
|
|
if (column_name) {
|
|
this.shadowRoot.getElementById("column-"+ column_name).remove()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a json from this item
|
|
*/
|
|
getTxItem() {
|
|
return this.tx
|
|
}
|
|
|
|
isSelected() {
|
|
return this.callbackaction.childNodes[1].checked
|
|
}
|
|
|
|
unselect() {
|
|
this.callbackaction.childNodes[1].checked = false
|
|
}
|
|
|
|
select() {
|
|
this.callbackaction.childNodes[1].checked = true
|
|
}
|
|
|
|
rbfPopup(rbfType) {
|
|
let txDataPopup = document.getElementById('tx-popup');
|
|
let url = `{{ url_for('wallets_endpoint.send_new', wallet_alias='WALLET_ALIAS') }}`.replace("WALLET_ALIAS", this.wallet);
|
|
let newFee = parseFloat((((this.tx.fee * -1) / this.tx.vsize) * 1e8).toFixed(2));
|
|
if (newFee <= 1.02) {
|
|
newFee = 1;
|
|
}
|
|
// TODO: get fee diff from specter
|
|
newFee += 2;
|
|
txDataPopup.innerHTML = `
|
|
<form action="${url}" method="POST">
|
|
<h1>${rbfType == 'cancel' ? '{{ _("Cancel") }}' : '{{ _("Speed up") }}'} {{ _("the Transaction") }}</h1>
|
|
<p>{{ _("You can") }} ${rbfType == 'cancel' ? `{{ _("cancel") }}` : `{{ _("speed up") }}`} {{ _("the transaction by increasing its fee rate") }}${rbfType == 'cancel' ? ` {{ _("and sending the funds back to yourself") }}` : ''}:</p>
|
|
<input type="hidden" name="rbf_tx_id" value='${this.tx.txid}' />
|
|
<input type="number" min="${newFee}" value="${newFee}" class="fee_rate" name="rbf_fee_rate" id="rbf_fee_rate" min="1" step="any" autocomplete="off"> sat/vbyte
|
|
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
|
|
<br>
|
|
<br>
|
|
<button type="submit" name="action" value="${rbfType == 'cancel' ? 'rbf_cancel' : 'rbf'}" class="btn centered ${rbfType == 'cancel' ? 'danger' : ''}">${rbfType == 'cancel' ? `{{ _("Cancel transaction") }}` : `{{ _("Speed up!") }}`}</button>
|
|
<br>
|
|
<span class="toggle_advanced_rbf" style="cursor: pointer;">{{ _("Advanced") }} {% if show_advanced_settings %}▼{% else %}▶{% endif %}</span>
|
|
<div class="advanced_rbf hidden warning">
|
|
<p style="max-width: 400px;">{{ _("If you would like further customization, you can click here to fully edit the transaction.") }}<br>{{ _("(advanced, not recommended for new users)") }}</p>
|
|
<button type="submit" name="action" value="rbf_edit" class="btn centered">{{ _("Edit the transaction (advanced)") }}</button>
|
|
</div>
|
|
</form>
|
|
`;
|
|
txDataPopup.querySelector('.toggle_advanced_rbf').onclick = () => {
|
|
let advancedButton = txDataPopup.querySelector('.toggle_advanced_rbf')
|
|
let advancedSettings = txDataPopup.querySelector('.advanced_rbf')
|
|
if (advancedSettings.classList.contains('hidden')) {
|
|
advancedSettings.classList.remove('hidden')
|
|
advancedButton.innerHTML = `{{ _("Advanced") }} ▼`;
|
|
} else {
|
|
advancedSettings.classList.add('hidden')
|
|
advancedButton.innerHTML = `{{ _("Advanced") }} ▶`;
|
|
}
|
|
}
|
|
|
|
showPageOverlay('tx-popup');
|
|
}
|
|
|
|
getCategoryImg(category, isConfirmed) {
|
|
if (!isConfirmed) {
|
|
return `{{ url_for('static', filename='img') }}/clock.svg`;
|
|
}
|
|
switch (category) {
|
|
case "send":
|
|
return `{{ url_for('static', filename='img') }}/send.svg`;
|
|
case "receive":
|
|
case "immature":
|
|
case "generate":
|
|
return `{{ url_for('static', filename='img') }}/receive.svg`;
|
|
case "selftransfer":
|
|
return `{{ url_for('static', filename='img') }}/transfer.svg`;
|
|
case "mixed":
|
|
return `{{ url_for('static', filename='img') }}/mixed.svg`;
|
|
}
|
|
}
|
|
}
|
|
customElements.define('tx-row', TxRowElement);
|
|
</script>
|