Feature: initial devhelper extension (#1547)

* initial devhelper extension

* change logo

* adding fee-selection

* moar structure and tx-table refactoring

* error-message

* improving handling

* easier and more consistent

* strange

* moving devhelp to cryptoadvance.secterext

* fix moving issues

* a bit more explanation
This commit is contained in:
Kim Neunert 2022-02-25 10:30:30 +01:00 committed by GitHub
parent ce07015375
commit 22042f4b12
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 853 additions and 145 deletions

View file

@ -168,6 +168,7 @@ class BaseConfig(object):
EXTENSION_LIST = [
"cryptoadvance.specter.services.swan.service",
"cryptoadvance.specter.services.bitcoinreserve.service",
"cryptoadvance.specterext.devhelp.service",
]
# This is just a placeholder in order to be aware that you cannot set this

View file

@ -39,6 +39,7 @@
{% endif %}
}
</style>
<script type="text/javascript" src="{{ url_for('static', filename='helpers.js') }}"></script>
{% block head %}
{% endblock %}
</head>
@ -263,7 +264,6 @@
{% endblock %}
</main>
</div>
<script type="text/javascript" src="{{ url_for('static', filename='helpers.js') }}"></script>
{% if not hwi_bridge %}
{# url routes are not defined in hwi_bridge mode. `url_for` will fail. #}

View file

@ -40,6 +40,15 @@
this.note = clone.querySelector(".note");
this.info = clone.querySelector(".address-data-info");
this.note.innerText = `{{ _("Loading address") }}: ${this.address} {{ _("details") }}...`;
// Attach the created element to the shadow dom
shadow.appendChild(clone);
}
connectedCallback() {
// Read input "data-*" attributes
this.isVerifyQR = this.getAttribute('data-verify-qr') == 'True';
this.isVerifyHwi = this.getAttribute('data-verify-hwi') == 'True';
@ -51,12 +60,21 @@
this.label = this.getAttribute('data-label');
this.serviceId = this.getAttribute('data-service-id');
this.wallet = this.getAttribute('data-address-wallet');
this.fetchAddressData();
}
static get observedAttributes() {return ['data-address-wallet', 'data-address']; }
attributeChangedCallback(name, oldValue, newValue) {
if (name=="data-address") {
this.address = newValue
}
if (name=="data-address-wallet") {
this.wallet = newValue
}
this.info.innerHTML = "<br>"
this.note.innerText = `{{ _("Loading address") }}: ${this.address} {{ _("details") }}...`;
this.fetchAddressData();
// Attach the created element to the shadow dom
shadow.appendChild(clone);
}
async fetchAddressData() {
@ -64,6 +82,7 @@
var formData = new FormData();
formData.append('address', this.address);
formData.append('csrf_token', '{{ csrf_token() }}');
console.log( this.address)
try {
const response = await fetch(
url,

View file

@ -94,6 +94,18 @@
shadow.appendChild(clone);
}
static get observedAttributes() {return ['data-wallet', 'data-address']; }
attributeChangedCallback(name, oldValue, newValue) {
if (name=="data-address") {
this.address = newValue
}
if (name=="data-wallet") {
this.wallet = newValue
}
//ToDo: Implement more here
}
connectedCallback() {
this.address = this.getAttribute('data-address');
this.wallet = this.getAttribute('data-wallet');

View file

@ -298,7 +298,10 @@
if (this.hideColumns != "null") {
this.hideColumns.split(" ").forEach((column_name) => {
if (column_name) {
this.shadowRoot.getElementById("column-"+ column_name).remove()
let column = this.shadowRoot.getElementById("column-"+ column_name)
if (column) {
column.remove()
}
}
})
}

View file

@ -329,14 +329,14 @@
this.paginationNext.onclick = () => {
this.idx++;
console.log("paginationNext fetch");
this.fetchTxItems();
}
// Previous page action
this.paginationBack.onclick = () => {
this.idx--;
console.log("paginationBack fetch");
this.fetchTxItems();
this.update();
}
// Listen to changes in page number selection input
@ -348,7 +348,7 @@
}
this.idx = newIdx - 1;
console.log("paginationIdxInput fetch");
this.fetchTxItems();
this.update();
}
// Listen to changes in search bar
@ -356,14 +356,14 @@
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}`;
console.log("searchInput fetch");
this.fetchTxItems();
this.update();
}
// Listen to changes in page limit
this.pageLimitSelect.onchange = () => {
this.limit = this.pageLimitSelect.value;
console.log("pageLimitSelect fetch");
this.fetchTxItems();
this.update();
}
// Setup sorting functionality for each column of the table header
@ -397,7 +397,6 @@
this.export.href = this.export.href.split('?')[0] + `?exportPrices=${this.exportPricesSwitch.checked}&search=${this.search}&sortby=${this.sortby}&sortdir=${this.sortdir}`;
}
console.log("header fetch")
this.fetchTxItems();
}
}
@ -468,6 +467,7 @@
if (this.hideSwitcher && this.hideSwitcher.toLowerCase() == "true") {
this.shadowRoot.querySelector(".switcher").classList.add('hidden')
}
this.update();
}
getSelectedTxs() {
@ -498,8 +498,6 @@
}
/**
* TODO: This is being called 7 times (once for each attribute) when the page is loaded,
* causing up to 7 redundant fetchTxItems() hits to the server.
*
* Listens to changes on the following attributes:
* - blockhash: Should show blockhash column. Either "true" or "false"
@ -542,7 +540,10 @@
if (!this.listType) {
return
}
this.fetchTxItems();
if (oldValue) {
this.update();
}
}
}
if (attrName == "selected-coins" && newValue != undefined && newValue != "undefined") {
@ -579,27 +580,25 @@
return false
}
update() {
if (this.getAttribute('tx-data')) {
// faking a jsonResponse for the render-method
let jsonResponse = {
pageCount: 1,
txlist: this.getAttribute('tx-data')
}
let showBlockhash = !this.blockhashHeader.classList.contains('hidden');
this.render(jsonResponse, showBlockhash)
} else {
this.fetchTxItems()
}
}
/**
* Fetches txlist from the Specter API and loads the result into TxRowElements
*/
async fetchTxItems() {
this.callId++;
// Clean up from existing transaction list
this.summaryRow.classList.add('hidden');
this.emptyRow.children[1].innerText = '{{ _("Fetching transactions...") }}';
this.emptyRow.classList.remove('hidden');
this.tbody.querySelectorAll('tx-row').forEach((row) => {row.remove()});
if (!this.listType) {
this.emptyRow.children[1].innerText = '{{ _("Failed to fetch transactions...") }}';
return
}
if (this.listType == "txlist") {
this.txlistViewBtn.classList.add("checked");
} else {
this.utxoViewBtn.classList.add("checked");
}
// Select the correct URL based on the list type and selected wallet
let url;
switch (this.listType) {
@ -662,120 +661,7 @@
let showBlockhash = !this.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 class="txrowitem"
data-btc-unit="${this.btcUnit ? this.btcUnit : 'btc'}"
data-price="${this.price ? this.price : 0}"
data-symbol="${this.symbol ? this.symbol : ''}"
data-tx='${JSON.stringify(tx).replace(/[\(]/g, "&lpar;").replace(/[\)]/g, "&rpar;").replace(/[\/]/g, "&sol;").replace(/[\']/g, "&apos;")}'
data-wallet="${(this.wallet ? this.wallet : tx.wallet_alias)}"
data-show-blockhash="${showBlockhash}"
data-mode="${this.getAttribute('type') + (this.wallet ? "" : "-overview")}"
data-hide-sensitive-info="${this.hideSensitiveInfo}"
hide-columns="${this.hideColumns}">
</tx-row>
`)
this.tbody.append(txRow);
summaryTxCount++;
summaryAmount += parseFloat(this.tbody.children[this.tbody.children.length - 1].shadowRoot.querySelector('.amount').innerText.replace(',', ''));
summaryPrice += parseFloat(this.tbody.children[this.tbody.children.length - 1].shadowRoot.querySelector('.amount-price').innerText.replace(`(${this.symbol}`, '').replace(')', '').replace(',', ''));
}
if (summaryTxCount > 0 && this.search) {
this.summaryTxCount.innerText = `Transactions count: ${summaryTxCount}`;
this.summaryAmount.innerHTML = `Total amount: ${numberWithCommas(summaryAmount.toFixed(8))}`;
if (summaryPrice) {
this.summaryAmount.innerHTML += ` <span class="note">(${this.symbol}${numberWithCommas(summaryPrice.toFixed(2))})</span>`
}
this.summaryRow.classList.remove('hidden');
}
// Pagination
this.pageCount = jsonResponse.pageCount;
// Show empty row if no transaction found
if (this.pageCount == 0 || txlist.length == 0) {
this.emptyRow.classList.remove('hidden');
this.emptyRow.children[1].innerText = '{{ _("No transactions found...") }}';
this.pageCount = 1;
} else {
this.emptyRow.classList.add('hidden');
}
// If index is greater than the pages count fetch again for the last page
if (this.idx >= this.pageCount) {
this.idx = this.pageCount - 1;
console.log("index greater than pages count fetch");
this.fetchTxItems()
}
this.paginationIdxInput.value = this.idx + 1;
// If there are multiple pages show the pagination bar
if (this.pageCount > 1) {
this.paginationContainer.classList.remove('hidden');
this.paginationCounter.innerText = `of ${this.pageCount}`;
this.paginationIdxInput.max = this.pageCount;
} else {
this.paginationContainer.classList.add('hidden');
}
// Disable pagination back button if on the first page
if (this.idx == 0) {
this.paginationBack.classList.add('pagination-disabled');
} else {
this.paginationBack.classList.remove('pagination-disabled');
}
// Disable pagination next button if on the last page
if (this.idx + 1 == this.pageCount) {
this.paginationNext.classList.add('pagination-disabled');
} else {
this.paginationNext.classList.remove('pagination-disabled');
}
this.selectedRows = [];
this.selectedRowsActionBox.classList.add('hidden');
// if preselectedCoins are there, we need to dispatch an event
let dispatchLater = false
for (let txRow of this.shadowRoot.querySelectorAll('tx-row')) {
// Preselect if necessary
if (this.checkForSelectedCoin(txRow.tx.txid)) {
txRow.select()
dispatchLater = true
}
txRow.addEventListener('txRowCustomSelected', (e) => {
this.fireCustomSelectedEvent()
})
txRow.addEventListener('txRowSelected', e=>{
let selectedRow = e.target;
if (e.detail.selected) {
this.selectedRows.push({
txid: e.detail.txid,
vout: e.detail.vout,
amount: e.detail.amount.toFixed(8)
});
} else {
this.selectedRows = this.selectedRows.filter(row => !(row.txid === e.detail.txid && row.vout === e.detail.vout))
}
if (this.selectedRows.length == 0) {
this.selectedRowsActionBox.classList.add('hidden');
} else {
this.selectedRowsActionBox.classList.remove('hidden');
}
});
}
if (dispatchLater) {
this.fireCustomSelectedEvent()
}
}
this.render(jsonResponse, showBlockhash)
return
};
showError(`{{ _("Failed to fetch transactions list.") }}`)
@ -784,7 +670,147 @@
showError(`{{ _("Failed to fetch transactions list.") }}: ${e}`);
}
}
/**
render(jsonResponse, showBlockhash) {
// jsonresponse is something like:
// {
// "pageCount": 1,
// "txlist": "[{\"txid\": \"1f2b4f9b6762ab37d3b0495cda51d8733c68a5e3a01356f0a67f215a29b2f05e\", \"blockhash\": \"05d7c1f7c4fa2a530e0916dcd15551657c22f4c30df175292b0c0b0f320adc10\", \"blockheight\": 722, \"time\": 1644162928, \"conflicts\": [], \"bip125-replaceable\": \"no\", \"vsize\": null, \"category\": \"receive\", \"address\": \"bcrt1qc3z4j7kksr3wm2heaahzx62yjrj8fn83s7x3ps\", \"amount\": 20.0, \"ismine\": true, \"confirmations\": 132, \"label\": \"Address #7\", \"validated_blockhash\": \"\", \"wallet_alias\": \"myhot_2\"}, {\"txid\": \"aafaecb085944a6fce4556885c1f10c1cd97bac146a2a0e676a2b12bcf428d3b\", \"blockhash\": \"201eea173347cdc8e8effeaf96053b5867860cec163f3eacec7c8918c94bdaaa\", \"blockheight\": 721, \"time\": 1644162928, \"conflicts\": [], \"bip125-replaceable\": \"no\", \"vsize\": null, \"category\": \"receive\", \"address\": \"bcrt1qyncynwdxmeynk245wa3f4urul3nm2v0ulfueek\", \"amount\": 20.0, \"ismine\": true, \"confirmations\": 133, \"label\": \"Address #6\", \"validated_blockhash\": \"\", \"wallet_alias\": \"myhot_2\"}, {\"txid\": \"bf34b090996dfcd2971c88f645edd4a0a0ad180e15096389d8841cf2bac0ecde\", \"blockhash\": \"07952e41edea8a9507722531e922b9d217518d6116ba4fba47c9e62ce12dee9e\", \"blockheight\": 305, \"time\": 1644159720, \"conflicts\": [], \"bip125-replaceable\": \"no\", \"vsize\": null, \"category\": \"receive\", \"address\": \"bcrt1qyncynwdxmeynk245wa3f4urul3nm2v0ulfueek\", \"amount\": 20.0, \"ismine\": true, \"confirmations\": 549, \"label\": \"Address #6\", \"validated_blockhash\": \"\", \"wallet_alias\": \"myhot_2\"}]"
// }
// Clean up from existing transaction list
this.summaryRow.classList.add('hidden');
this.emptyRow.children[1].innerText = '{{ _("Fetching transactions...") }}';
this.emptyRow.classList.remove('hidden');
this.tbody.querySelectorAll('tx-row').forEach((row) => {row.remove()});
if (!this.listType) {
this.emptyRow.children[1].innerText = '{{ _("Failed to fetch transactions...") }}';
return
}
if (this.listType == "txlist") {
this.txlistViewBtn.classList.add("checked");
} else {
this.utxoViewBtn.classList.add("checked");
}
// 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 class="txrowitem"
data-btc-unit="${this.btcUnit ? this.btcUnit : 'btc'}"
data-price="${this.price ? this.price : 0}"
data-symbol="${this.symbol ? this.symbol : ''}"
data-tx='${JSON.stringify(tx).replace(/[\(]/g, "&lpar;").replace(/[\)]/g, "&rpar;").replace(/[\/]/g, "&sol;").replace(/[\']/g, "&apos;")}'
data-wallet="${(this.wallet ? this.wallet : tx.wallet_alias)}"
data-show-blockhash="${showBlockhash}"
data-mode="${this.getAttribute('type') + (this.wallet ? "" : "-overview")}"
data-hide-sensitive-info="${this.hideSensitiveInfo}"
hide-columns="${this.hideColumns}">
</tx-row>
`)
this.tbody.append(txRow);
summaryTxCount++;
summaryAmount += parseFloat(this.tbody.children[this.tbody.children.length - 1].shadowRoot.querySelector('.amount').innerText.replace(',', ''));
summaryPrice += parseFloat(this.tbody.children[this.tbody.children.length - 1].shadowRoot.querySelector('.amount-price').innerText.replace(`(${this.symbol}`, '').replace(')', '').replace(',', ''));
}
if (summaryTxCount > 0 && this.search) {
this.summaryTxCount.innerText = `Transactions count: ${summaryTxCount}`;
this.summaryAmount.innerHTML = `Total amount: ${numberWithCommas(summaryAmount.toFixed(8))}`;
if (summaryPrice) {
this.summaryAmount.innerHTML += ` <span class="note">(${this.symbol}${numberWithCommas(summaryPrice.toFixed(2))})</span>`
}
this.summaryRow.classList.remove('hidden');
}
// Pagination
this.pageCount = jsonResponse.pageCount;
// Show empty row if no transaction found
if (this.pageCount == 0 || txlist.length == 0) {
this.emptyRow.classList.remove('hidden');
this.emptyRow.children[1].innerText = '{{ _("No transactions found...") }}';
this.pageCount = 1;
} else {
this.emptyRow.classList.add('hidden');
}
// If index is greater than the pages count fetch again for the last page
if (this.idx >= this.pageCount) {
this.idx = this.pageCount - 1;
console.log("index greater than pages count fetch");
this.fetchTxItems()
}
this.paginationIdxInput.value = this.idx + 1;
// If there are multiple pages show the pagination bar
if (this.pageCount > 1) {
this.paginationContainer.classList.remove('hidden');
this.paginationCounter.innerText = `of ${this.pageCount}`;
this.paginationIdxInput.max = this.pageCount;
} else {
this.paginationContainer.classList.add('hidden');
}
// Disable pagination back button if on the first page
if (this.idx == 0) {
this.paginationBack.classList.add('pagination-disabled');
} else {
this.paginationBack.classList.remove('pagination-disabled');
}
// Disable pagination next button if on the last page
if (this.idx + 1 == this.pageCount) {
this.paginationNext.classList.add('pagination-disabled');
} else {
this.paginationNext.classList.remove('pagination-disabled');
}
this.selectedRows = [];
this.selectedRowsActionBox.classList.add('hidden');
// if preselectedCoins are there, we need to dispatch an event
let dispatchLater = false
for (let txRow of this.shadowRoot.querySelectorAll('tx-row')) {
// Preselect if necessary
if (this.checkForSelectedCoin(txRow.tx.txid)) {
txRow.select()
dispatchLater = true
}
txRow.addEventListener('txRowCustomSelected', (e) => {
this.fireCustomSelectedEvent()
})
txRow.addEventListener('txRowSelected', e=>{
let selectedRow = e.target;
if (e.detail.selected) {
this.selectedRows.push({
txid: e.detail.txid,
vout: e.detail.vout,
amount: e.detail.amount.toFixed(8)
});
} else {
this.selectedRows = this.selectedRows.filter(row => !(row.txid === e.detail.txid && row.vout === e.detail.vout))
}
if (this.selectedRows.length == 0) {
this.selectedRowsActionBox.classList.add('hidden');
} else {
this.selectedRowsActionBox.classList.remove('hidden');
}
});
}
if (dispatchLater) {
this.fireCustomSelectedEvent()
}
}
}
/**
* If this event is fired, then some row got ticked/unticked in the custom-selection-column
*/
fireCustomSelectedEvent() {

View file

@ -0,0 +1,70 @@
import logging
from flask import Flask, Response, redirect, render_template, request, url_for
from flask import current_app as app
from flask_login import login_required, current_user
from cryptoadvance.specter.services.controller import user_secret_decrypted_required
from .service import DevhelpService
from cryptoadvance.specter.wallet import Wallet
logger = logging.getLogger(__name__)
devhelp_endpoint = DevhelpService.blueprint
@devhelp_endpoint.route("/")
@login_required
@user_secret_decrypted_required
def index():
return render_template(
"devhelp/index.jinja",
)
@devhelp_endpoint.route("/html/<html_component>")
@login_required
@user_secret_decrypted_required
def html_component(html_component):
associated_wallet: Wallet = DevhelpService.get_associated_wallet()
return render_template(
f"devhelp/html/{html_component}",
wallet=associated_wallet,
services=app.specter.service_manager.services,
address=associated_wallet.get_address(3),
)
@devhelp_endpoint.route("/settings", methods=["GET"])
@login_required
@user_secret_decrypted_required
def settings_get():
associated_wallet: Wallet = DevhelpService.get_associated_wallet()
# Get the user's Wallet objs, sorted by Wallet.name
wallet_names = sorted(current_user.wallet_manager.wallets.keys())
wallets = [current_user.wallet_manager.wallets[name] for name in wallet_names]
return render_template(
"devhelp/settings.jinja",
associated_wallet=associated_wallet,
wallets=wallets,
cookies=request.cookies,
)
@devhelp_endpoint.route("/settings", methods=["POST"])
@login_required
@user_secret_decrypted_required
def settings_post():
show_menu = request.form["show_menu"]
user = app.specter.user_manager.get_user()
if show_menu == "yes":
user.add_service(DevhelpService.id)
else:
user.remove_service(DevhelpService.id)
used_wallet_alias = request.form.get("used_wallet")
if used_wallet_alias != None:
wallet = current_user.wallet_manager.get_by_alias(used_wallet_alias)
DevhelpService.set_associated_wallet(wallet)
return redirect(url_for(f"{DevhelpService.get_blueprint_name()}.settings_get"))

View file

@ -0,0 +1,45 @@
import logging
from flask import current_app as app
from cryptoadvance.specter.services.service import Service, devstatus_alpha
from cryptoadvance.specter.specter_error import SpecterError
from cryptoadvance.specter.wallet import Wallet
logger = logging.getLogger(__name__)
class DevhelpService(Service):
id = "devhelp"
name = "Development Helper"
icon = "devhelp/img/orange-wrench.png"
logo = "devhelp/img/orange-wrench.png"
desc = "Wrenches at work."
has_blueprint = True
blueprint_module = "cryptoadvance.specterext.devhelp.controller"
devstatus = devstatus_alpha
sort_priority = 2
# ServiceEncryptedStorage field names for Swan
SPECTER_WALLET_ALIAS = "wallet"
@classmethod
def get_associated_wallet(cls) -> Wallet:
"""Get the Specter `Wallet` that is currently associated with this service"""
service_data = cls.get_current_user_service_data()
if not service_data or cls.SPECTER_WALLET_ALIAS not in service_data:
# Service is not initialized; nothing to do
return
try:
return app.specter.wallet_manager.get_by_alias(
service_data[cls.SPECTER_WALLET_ALIAS]
)
except SpecterError as e:
logger.debug(e)
# Referenced an unknown walletfrom cryptoadvance.specter.wallet import Wallet
# TODO: keep ignoring or remove the unknown wallet from service_data?
return
@classmethod
def set_associated_wallet(cls, wallet: Wallet):
"""Set the Specter `Wallet` that is currently associated with Swan auto-withdrawals"""
cls.update_current_user_service_data({cls.SPECTER_WALLET_ALIAS: wallet.alias})

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,16 @@
{% from 'components/menu_item.jinja' import menu_item %}
{#
vaultoro_menu - Tabs menu to navigate between the vaultoro screens.
Parameters:
- active_menuitem: Current active tab. Options: 'general', 'bitcoin_core', 'auth'.
#}
{% macro devhelp_menu(active_menuitem) -%}
<nav class="row collapse-on-mobile">
{{ menu_item(service.id, 'index', 'Main', active_menuitem, isLeft=true) }}
{{ menu_item(service.id, 'settings_get', 'Settings', active_menuitem, isRight=true) }}
<a href="javascript:void(0);" class="mobile-nav-icon" onclick="toggleMobileNav(this, `{{ url_for('static', filename='img/expand-more.svg') }}`, `{{ url_for('static', filename='img/expand-less.svg') }}`)">
<img style="width: 36px;" src="{{ url_for('static', filename='img/expand-more.svg') }}"/>
</a>
</nav>
{%- endmacro %}

View file

@ -0,0 +1,23 @@
{% extends "base.jinja" %}
{% block main %}
<img src="{{ url_for(service.id +'_endpoint' + '.static', filename=service.logo) }}" width="50"/>
{% from 'devhelp/components/devhelp_menu.jinja' import devhelp_menu with context %}
{{ devhelp_menu(tab) }}
<style>
/* ToDo: Refactor to style.css */
a {
/* Copied from .note */
font-size: 0.9em;
text-decoration: underline;
color: inherit;
}
a:hover {
color: #fff ;
}
textarea {
font-size: 1.2em;
}
</style>
{% block content %}
{% endblock %}
{% endblock %}

View file

@ -0,0 +1,59 @@
{% extends "devhelp/components/devhelp_tab.jinja" %}
{% block title %}address-data{% endblock %}
{% set tab = 'html' %}
{% block content %}
<h1>address-data</h1>
{% include "includes/address-data.html" %}
<code class="html"><textarea rows="40" cols="90" style="height: 200px">
<address-data
id="someid"
data-verify-qr="False"
data-verify-hwi="False"
data-used="false"
data-utxo="0"
data-amount="0"
data-amount-price=""
data-address="lq1qqde0h6pykv3m0754qyfjfz7lcd7hxhyah9urn3zhlszaplcjetk6la7xpl2ezwn59runn290qqnhyws3ckhuwj3nllgd0wsgf"
data-service-id="undefined"
data-label="Address #5"
data-address-wallet="myliquidhot">
</address-data>
</textarea>
</code>
<p>
Address-Data needs an existing wallet-alias and a valid address as the component is making an Ajax-call to <code>{{ url_for('wallets_endpoint_api.addressinfo', wallet_alias='WALLET_ALIAS') }}</code>
wallet-alias and address need to be defined in order to get a valid response. You can do that here:
</p>
{% include "devhelp/includes/set-wallet.html" %}
<br>
{% include "devhelp/includes/set-address.html" %}
<h2>Render example</h2>
<br><br>
<address-data
id="someid"
data-verify-qr="False"
data-verify-hwi="False"
data-used="false"
data-utxo="0"
data-amount="0"
data-amount-price=""
data-address="lq1qqde0h6pykv3m0754qyfjfz7lcd7hxhyah9urn3zhlszaplcjetk6la7xpl2ezwn59runn290qqnhyws3ckhuwj3nllgd0wsgf"
data-service-id="undefined"
data-label="Address #5"
data-address-wallet="myliquidhot"></address-data>
{% endblock %}
{% block scripts %}
{% endblock %}

View file

@ -0,0 +1,52 @@
{% extends "devhelp/components/devhelp_tab.jinja" %}
{% block title %}address-data{% endblock %}
{% set tab = 'html' %}
{% block content %}
<h1>address-label</h1>
{% include "includes/address-label.html" %}
<code class="html"><textarea rows="40" cols="90" style="height: 200px">
<address-label
data-address="lq1qq259d9ahc6apt9fcgjx4fmfl5lsfr2vkzrvyy2uuukhrreulzs7xxsyv70jzt0h7rv2s7dwsll8ln2ypf4qujur5m94edx6jv"
data-label="Address #0"
data-wallet="myliquidhot">
</address-label>
</textarea>
</code>
<p>
Address-label changes the label of an address.
wallet-alias and address need to be defined in order to get a valid response.
Don't set an data-label attribute because otherwise the initial label doesn't get fetched (bug?!).
It's using Ajax-call to:
<ul>
<li>
<code>{{ url_for('wallets_endpoint_api.get_label', wallet_alias='WALLET_ALIAS') }}</code>
</li>
<li>
<code>{{ url_for('wallets_endpoint_api.set_label', wallet_alias='WALLET_ALIAS') }}</code>
</li>
</ul>
You set the most important params here:
</p>
{% include "devhelp/includes/set-wallet.html" %}
<br>
{% include "devhelp/includes/set-address.html" %}
<address-label
id="someid"
data-address="{{ wallet.get_address(2) }}"
data-wallet="{{wallet.alias}}">
</address-label>
{% endblock %}

View file

@ -0,0 +1,51 @@
{% extends "devhelp/components/devhelp_tab.jinja" %}
{% block title %}address-row{% endblock %}
{% set tab = 'html' %}
{% block content %}
<h1>address-row</h1>
{% include "includes/address-row.html" %}
<code class="html"><textarea rows="40" cols="90" style="height: 200px">
<table>
<address-row
data-verify-qr="False"
data-verify-hwi="False"
data-btc-unit="btc"
data-price="0"
data-symbol=""
data-address='{"index":0,"address":"bcrt1qreur2f3v9q8ylznanxwkzkhy0gl7vym3zqkwaw","label":"Address #0","amount":20,"used":true,"utxo":1,"type":"receive","service_id":null}'
data-hide-sensitive-info="false"
data-wallet="syfdds_2">
</address-row>
</table>
</textarea>
</code>
<p>
Address-rows are used in address-tables. as discussed in this <a href="https://stackoverflow.com/posts/45765748/revisions">answer</a>, custom elements are not allowed in table-tags.
It's not clear how the address-table achieves it that it works nevertheless. So in the example below, we'll render the component without a table-tag.
The data-address takes json. The data-wallet is used to enable clicking on the address which will show a address-data if the code is properly imported.
The component is passive otherwise.
<h1>Render with data-address</h1>
<address-row
id="someid"
data-verify-qr="False"
data-verify-hwi="False"
data-btc-unit="btc"
data-price="0"
data-symbol=""
data-address='{"index":0,"address":"bcrt1qreur2f3v9q8ylznanxwkzkhy0gl7vym3zqkwaw","label":"Address #0","amount":20,"used":true,"utxo":1,"type":"receive","service_id":null}'
data-hide-sensitive-info="false"
data-wallet="syfdds_2">
<!-- -->
</address-row>
{% endblock %}

View file

@ -0,0 +1,44 @@
{% extends "base.jinja" %}
{% block main %}
<h1>address-label</h1>
{% include "includes/fee-selection.html" %}
<code class="html"><textarea rows="40" cols="90" style="height: 200px">
<fee-selection id="fee-selection-component"/>
</textarea>
</code>
<p>
fee-selection let you choose to change the fee you'd like to have. It's using Ajax-call to:
<ul>
<li>
<code>{{ url_for('wallets_endpoint_api.fees') }}</code> : Grab the current fees
</li>
</ul>
</p>
It's designed to be used in a form and will "drop" what the user has chosen in form-fields which are stored outside the shadow-DOM in the "lightdom". This will look like this:
<code class="html"><textarea rows="40" cols="90" style="height: 200px">
<input type="hidden" value="dynamic" name="fee_option">
<input type="hidden" class="fee_rate hidden" name="fee_rate" id="fee_rate" min="1" step="0.1" value="1" autocomplete="off">
<input type="hidden" id="fee_rate_dynamic" name="fee_rate_dynamic" value="4" class="hidden">
<input type="hidden" class="rbf-checkbox inline" name="rbf" id="rbf" value="true">
<input type="hidden" class="inline" name="subtract" id="subtract" value="false">
<input id="subtract_from_input" name="subtract_from" type="hidden" min="1" value="1" step="1" style="width: 80px; min-width: 80px;">
<span id="subtract_from" style="display: none">
<br>Subtract from recipient number:
<input id="subtract_from_input" name="subtract_from" type="number" min="1" value="1" step="1" style="width: 80px; min-width: 80px;"><br>
</span>
</textarea>
</code>
<h2>Rendering</h2>
<fee-selection id="fee-selection-component"/>
{% endblock %}

View file

@ -0,0 +1,127 @@
{% extends "devhelp/components/devhelp_tab.jinja" %}
{% block title %}tx-table{% endblock %}
{% set tab = 'html' %}
{% block content %}
<h1>tx-table</h1>
<!-- The service-data include needs a service -->
{% include "includes/services-data.html" %}
{% include "includes/address-label.html" %}
{% include "includes/tx-row.html" %}
{% include "includes/tx-data.html" %}
{% include "includes/explorer-link.html" %}
{% include "includes/tx-table.html" %}
<p>
Tx-table needs either an existing wallet-alias making an Ajax-call to
<code> /wallets/wallet/myhot_2/history/</code> or you pass tx-data via
its tx-data attribute. You can insert a service-id attribute which will do?!
</p>
<code class="html"><textarea rows="40" cols="200" style="max-height: 500px;height: 400px;resize: none;overflow-y: scroll;white-space:pre;">
<div class="table-holder">
<!-- These are the necessary includes (replace ? with % )-->
<!-- The next include needs services as template-variable-->
{? include "includes/services-data.html" ?}
{? include "includes/address-label.html" ?}
{? include "includes/tx-row.html" ?}
{? include "includes/tx-data.html" ?}
{? include "includes/explorer-link.html" ?}
{? include "includes/tx-table.html" ?}
<tx-table
price="41559.45"
symbol="$"
btc-unit="btc"
blockhash="false"
type="txlist"
hide-sensitive-info="false"
service-id="someservice"
type="txlist"
tx-data='[
{"txid": "aafaecb085944a6fce4556885c1f10c1cd97bac146a2a0e676a2b12bcf428d3b", "blockhash": "201eea173347cdc8e8effeaf96053b5867860cec163f3eacec7c8918c94bdaaa", "blockheight": 721, "time": 1644162928, "conflicts": [], "bip125-replaceable": "no", "vsize": null, "category": "receive", "address": "bcrt1qyncynwdxmeynk245wa3f4urul3nm2v0ulfueek", "amount": 20.0, "ismine": true, "confirmations": 780, "label": "Address #6", "validated_blockhash": ""},
{"txid": "1f2b4f9b6762ab37d3b0495cda51d8733c68a5e3a01356f0a67f215a29b2f05e", "blockhash": "05d7c1f7c4fa2a530e0916dcd15551657c22f4c30df175292b0c0b0f320adc10", "blockheight": 722, "time": 1644162928, "conflicts": [], "bip125-replaceable": "no", "vsize": null, "category": "receive", "address": "bcrt1qc3z4j7kksr3wm2heaahzx62yjrj8fn83s7x3ps", "amount": 20.0, "ismine": true, "confirmations": 779, "label": "Address #7", "validated_blockhash": ""}]"'
/>
</div>
<div class="table-holder">
<tx-table
price="41559.45"
symbol="$"
btc-unit="btc"
blockhash="false"
type="txlist"
hide-sensitive-info="false"
wallet="myhot_2">
</tx-table>
</div>
</textarea>
</code>
<div class="card" style="float: left;">
{% include "devhelp/includes/set-wallet.html" %}
<br>
Tx-Data (Does not work yet): <input type="text" id="txdata" site=50/ value='[
{"txid": "aafaecb085944a6fce4556885c1f10c1cd97bac146a2a0e676a2b12bcf428d3b", "blockhash": "201eea173347cdc8e8effeaf96053b5867860cec163f3eacec7c8918c94bdaaa", "blockheight": 721, "time": 1644162928, "conflicts": [], "bip125-replaceable": "no", "vsize": null, "category": "receive", "address": "bcrt1qyncynwdxmeynk245wa3f4urul3nm2v0ulfueek", "amount": 20.0, "ismine": true, "confirmations": 780, "label": "Address #6", "validated_blockhash": ""},
{"txid": "1f2b4f9b6762ab37d3b0495cda51d8733c68a5e3a01356f0a67f215a29b2f05e", "blockhash": "05d7c1f7c4fa2a530e0916dcd15551657c22f4c30df175292b0c0b0f320adc10", "blockheight": 722, "time": 1644162928, "conflicts": [], "bip125-replaceable": "no", "vsize": null, "category": "receive", "address": "bcrt1qc3z4j7kksr3wm2heaahzx62yjrj8fn83s7x3ps", "amount": 20.0, "ismine": true, "confirmations": 779, "label": "Address #7", "validated_blockhash": ""},
]'> <button id="settxdata">SetTxdata</button>
</div>
<h1>Render example</h1>
<h2>with tx-data</h2>
<br><br>
<tx-table
id="someid"
price="41559.45"
symbol="$"
btc-unit="btc"
blockhash="false"
type="txlist"
hide-sensitive-info="false"
service-id="someservice"
type="txlist"
tx-data='[
{"txid": "aafaecb085944a6fce4556885c1f10c1cd97bac146a2a0e676a2b12bcf428d3b", "blockhash": "201eea173347cdc8e8effeaf96053b5867860cec163f3eacec7c8918c94bdaaa", "blockheight": 721, "time": 1644162928, "conflicts": [], "bip125-replaceable": "no", "vsize": null, "category": "receive", "address": "bcrt1qyncynwdxmeynk245wa3f4urul3nm2v0ulfueek", "amount": 20.0, "ismine": true, "confirmations": 780, "label": "Address #6", "validated_blockhash": ""},
{"txid": "1f2b4f9b6762ab37d3b0495cda51d8733c68a5e3a01356f0a67f215a29b2f05e", "blockhash": "05d7c1f7c4fa2a530e0916dcd15551657c22f4c30df175292b0c0b0f320adc10", "blockheight": 722, "time": 1644162928, "conflicts": [], "bip125-replaceable": "no", "vsize": null, "category": "receive", "address": "bcrt1qc3z4j7kksr3wm2heaahzx62yjrj8fn83s7x3ps", "amount": 20.0, "ismine": true, "confirmations": 779, "label": "Address #7", "validated_blockhash": ""}
]'
>
</tx-table>
<h2>with fetchdata/Ajax</h2>
<br><br>
<tx-table
id="someotherid"
price="41559.45"
symbol="$"
btc-unit="btc"
blockhash="false"
type="txlist"
hide-sensitive-info="false"
type="txlist"
wallet="{{ wallet.alias }}">
</tx-table>
{% endblock %}
{% block scripts %}
<script type="text/javascript">
var setTxdataButton = document.getElementById('settxdata')
setTxdataButton.addEventListener("click", (event) => {
var txdata = document.getElementById('txdata').value
document.getElementById('someid').setAttribute("tx-data", address)
console.log(walletAlias)
});
</script>
{% endblock %}

View file

@ -0,0 +1,20 @@
potential addresses:
<ul>
<li>
{{ wallet.get_address(1)}}
</li>
<li>
{{ wallet.get_address(2)}}
</li>
</ul>
Address: <input type="text" id="address" site=50 value="{{address}}"/> <button id="setaddress">SetAddress</button>
<script type="text/javascript">
var setAddressButton = document.getElementById('setaddress')
setAddressButton.addEventListener("click", (event) => {
var address = document.getElementById('address').value
document.getElementById('someid').setAttribute("data-address", address)
});
</script>

View file

@ -0,0 +1,11 @@
WalletAlias: <input type="text" id="walletalias" site=50 value="{{ wallet.alias}}"/> <button id="setwalletalias">SetWalletAlias</button>
<script type="text/javascript">
var setWalletAliasButton = document.getElementById('setwalletalias')
setWalletAliasButton.addEventListener("click", (event) => {
var walletAlias = document.getElementById('walletalias').value
document.getElementById('someid').setAttribute("data-address-wallet", walletAlias)
console.log(walletAlias)
});
</script>

View file

@ -0,0 +1,20 @@
{% extends "devhelp/components/devhelp_tab.jinja" %}
{% block title %}address-data{% endblock %}
{% set tab = 'index' %}
{% block content %}
<p>
This Extension is only useful for Developing and testing purposes.
</p>
<p>
You can checkout and test the different HTML-Components. Nothing else to see here.
</p>
<ul>
<li><a href="html/address-data.jinja">address-data</a></li>
<li><a href="html/address-label.jinja">address-label</a></li>
<li><a href="html/fee-selection.jinja">fee-selection</a></li>
<li><a href="html/tx-table.jinja">tx-table</a></li>
<li><a href="html/address-row.jinja">address-row</a></li>
</ul>
{% endblock %}

View file

@ -0,0 +1,109 @@
{% extends "devhelp/components/devhelp_tab.jinja" %}
{% block title %}Settings{% endblock %}
{% set tab = 'settings_get' %}
{% block content %}
<br/>
<style>
.big_option {
display: inline-block;
width: 14em;
padding: 1em;
vertical-align: middle;
height: 3.5em;
border: 4px solid var(--cmap-border);
border-radius: 0.5em;
background-color: var(--cmap-bg-lighter);
font-size: 1.5em;
margin: 1em;
position: relative;
}
.big_option:hover {
background-color: var(--cmap-bg-lightest);
border: 4px solid var(--main-color);
cursor: pointer;
}
.big_option_text {
margin: 0;
position: absolute;
top: 50%;
left: 50%;
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
.instructions {
color: #999;
font-style: italic;
}
{# TODO: End Remove #}
.css-1rbd7t8 {
box-sizing: border-box;
margin: 60px 0px 24px;
min-width: 0px;
}
.css-yn7azs {
box-sizing: border-box;
margin: 0px;
min-width: 0px;
-moz-box-pack: justify;
justify-content: space-between;
position: relative;
font-family: "Source Serif Pro", serif;
font-weight: 600;
line-height: 1.5;
letter-spacing: -0.02em;
font-variant: common-ligatures;
text-rendering: optimizelegibility;
font-size: 16px;
display: flex;
}
</style>
<div class="card">
<h1>{{ _("Configure your extension") }}</h1>
<br>
<div class="note">
{{ _("- Maybe you want to put some notes for the user") }}<br/>
{{ _("- One important setting is whether your extension gets a menu-point on the left") }}<br/>
{{ _("- Let us assume you want the user to use a specific wallet. That should go here. ") }}<br/>
</div>
<br/>
<form action="{{ url_for(service.get_blueprint_name() + '.settings_post') }}" method="POST" role="form">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<div>Show Menu Item:</div>
<select name="show_menu">
<option value="yes" {% if show_menu == 'yes' %}selected{% endif %}>Yes</option>
<option value="no" {% if show_menu == 'no' %}selected{% endif %}>No</option>
</select>
<br/>
<br/>
<br/>
{{ _("Choose which wallet should be used:") }}:<br>
<select name="used_wallet">
{% for wallet in wallets %}
<option value="{{ wallet.alias }}" {% if associated_wallet == wallet %}selected{% endif %}>{{ wallet.name }}</option>
{% endfor %}
</select>
<br/>
<br/>
<br/>
<div class="row">
<button type="submit" class="btn">{{ _("Save") }}</button>
</div>
</form>
</div>
<br/>
<br/>
<br/>
{% endblock %}