diff --git a/lnbits/extensions/watchonly/static/js/components/my-checkbox/my-checkbox.html b/lnbits/extensions/watchonly/static/js/components/my-checkbox/my-checkbox.html
new file mode 100644
index 00000000..0561e09a
--- /dev/null
+++ b/lnbits/extensions/watchonly/static/js/components/my-checkbox/my-checkbox.html
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/lnbits/extensions/watchonly/static/js/components/my-checkbox/my-checkbox.js b/lnbits/extensions/watchonly/static/js/components/my-checkbox/my-checkbox.js
new file mode 100644
index 00000000..e24581d6
--- /dev/null
+++ b/lnbits/extensions/watchonly/static/js/components/my-checkbox/my-checkbox.js
@@ -0,0 +1,18 @@
+async function initMyCheckbox(path) {
+ const t = await loadTemplateAsync(path)
+ Vue.component('my-checkbox', {
+ name:'my-checkbox',
+ template: t,
+ data() {
+ return { checked: false, title: 'Check me' }
+ },
+ methods: {
+ check() {
+ this.checked = !this.checked;
+ console.log('### checked', this.checked)
+ }
+ }
+ });
+}
+
+
diff --git a/lnbits/extensions/watchonly/static/js/index.js b/lnbits/extensions/watchonly/static/js/index.js
index 9db60c83..63c765f6 100644
--- a/lnbits/extensions/watchonly/static/js/index.js
+++ b/lnbits/extensions/watchonly/static/js/index.js
@@ -1,1219 +1,1230 @@
-Vue.component(VueQrcode.name, VueQrcode)
+const watchOnly = async () => {
+ Vue.component(VueQrcode.name, VueQrcode)
+ await initMyCheckbox('static/js/components/my-checkbox/my-checkbox.html')
-Vue.filter('reverse', function (value) {
- // slice to make a copy of array, then reverse the copy
- return value.slice().reverse()
-})
+ Vue.filter('reverse', function (value) {
+ // slice to make a copy of array, then reverse the copy
+ return value.slice().reverse()
+ })
-new Vue({
- el: '#vue',
- mixins: [windowMixin],
- data: function () {
- return {
- DUST_LIMIT: 546,
- filter: '',
-
- scan: {
- scanning: false,
- scanCount: 0,
- scanIndex: 0
- },
-
- currentAddress: null,
-
- tab: 'addresses',
-
- config: {
- data: {
- mempool_endpoint: 'https://mempool.space',
- receive_gap_limit: 20,
- change_gap_limit: 5
- },
- DEFAULT_RECEIVE_GAP_LIMIT: 20,
- show: false
- },
-
- serial: {
- selectedPort: null,
- writableStreamClosed: null,
- writer: null,
- readableStreamClosed: null,
- reader: null,
- showAdvancedConfig: false,
- receivedData: '',
- config: {}
- },
-
- hww: {
- password: null,
- showPassword: false,
- mnemonic: null,
- showMnemonic: false,
- authenticated: false,
- showPasswordDialog: false,
- showWipeDialog: false,
- showRestoreDialog: false,
- showConsole: false,
- showSignedPsbt: false,
- sendingPsbt: false,
- signingPsbt: false,
- psbtSent: false
- },
-
- formDialog: {
- show: false,
- data: {}
- },
-
- qrCodeDialog: {
- show: false,
- data: null
- },
- ...tables,
- ...tableData
- }
- },
-
- methods: {
- //################### CONFIG ###################
- getConfig: async function () {
- try {
- const {data} = await LNbits.api.request(
- 'GET',
- '/watchonly/api/v1/config',
- this.g.user.wallets[0].adminkey
- )
- this.config.data = data
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
+ new Vue({
+ el: '#vue',
+ mixins: [windowMixin],
+ mounted: function () {
+ console.log('### mounted')
},
- updateConfig: async function () {
- const wallet = this.g.user.wallets[0]
- try {
- await LNbits.api.request(
- 'PUT',
- '/watchonly/api/v1/config',
- wallet.adminkey,
- this.config.data
- )
- this.config.show = false
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- },
-
- //################### WALLETS ###################
- getWalletName: function (walletId) {
- const wallet = this.walletAccounts.find(wl => wl.id === walletId)
- return wallet ? wallet.title : 'unknown'
- },
- addWalletAccount: async function () {
- const wallet = this.g.user.wallets[0]
- const data = _.omit(this.formDialog.data, 'wallet')
- await this.createWalletAccount(wallet, data)
- },
- createWalletAccount: async function (wallet, data) {
- try {
- const response = await LNbits.api.request(
- 'POST',
- '/watchonly/api/v1/wallet',
- wallet.adminkey,
- data
- )
- this.walletAccounts.push(mapWalletAccount(response.data))
- this.formDialog.show = false
-
- await this.refreshWalletAccounts()
- await this.refreshAddresses()
-
- if (!this.payment.changeWallett) {
- this.payment.changeWallet = this.walletAccounts[0]
- this.selectChangeAddress(this.payment.changeWallet)
- }
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- },
- deleteWalletAccount: function (walletAccountId) {
- LNbits.utils
- .confirmDialog(
- 'Are you sure you want to delete this watch only wallet?'
- )
- .onOk(async () => {
- try {
- await LNbits.api.request(
- 'DELETE',
- '/watchonly/api/v1/wallet/' + walletAccountId,
- this.g.user.wallets[0].adminkey
- )
- this.walletAccounts = _.reject(this.walletAccounts, function (obj) {
- return obj.id === walletAccountId
- })
- await this.refreshWalletAccounts()
- await this.refreshAddresses()
- if (
- this.payment.changeWallet &&
- this.payment.changeWallet.id === walletAccountId
- ) {
- this.payment.changeWallet = this.walletAccounts[0]
- this.selectChangeAddress(this.payment.changeWallet)
- }
- await this.scanAddressWithAmount()
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Error while deleting wallet account. Please try again.',
- timeout: 10000
- })
- }
- })
- },
- getAddressesForWallet: async function (walletId) {
- try {
- const {data} = await LNbits.api.request(
- 'GET',
- '/watchonly/api/v1/addresses/' + walletId,
- this.g.user.wallets[0].inkey
- )
- return data.map(mapAddressesData)
- } catch (err) {
- this.$q.notify({
- type: 'warning',
- message: `Failed to fetch addresses for wallet with id ${walletId}.`,
- timeout: 10000
- })
- LNbits.utils.notifyApiError(err)
- }
- return []
- },
- getWatchOnlyWallets: async function () {
- try {
- const {data} = await LNbits.api.request(
- 'GET',
- '/watchonly/api/v1/wallet',
- this.g.user.wallets[0].inkey
- )
- return data
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to fetch wallets.',
- timeout: 10000
- })
- LNbits.utils.notifyApiError(error)
- }
- return []
- },
- refreshWalletAccounts: async function () {
- const wallets = await this.getWatchOnlyWallets()
- this.walletAccounts = wallets.map(w => mapWalletAccount(w))
- },
- getAmmountForWallet: function (walletId) {
- const amount = this.addresses.data
- .filter(a => a.wallet === walletId)
- .reduce((t, a) => t + a.amount || 0, 0)
- return this.satBtc(amount)
- },
-
- //################### ADDRESSES ###################
-
- refreshAddresses: async function () {
- const wallets = await this.getWatchOnlyWallets()
- this.addresses.data = []
- for (const {id, type} of wallets) {
- const newAddresses = await this.getAddressesForWallet(id)
- const uniqueAddresses = newAddresses.filter(
- newAddr =>
- !this.addresses.data.find(a => a.address === newAddr.address)
- )
-
- const lastAcctiveAddress =
- uniqueAddresses.filter(a => !a.isChange && a.hasActivity).pop() || {}
-
- uniqueAddresses.forEach(a => {
- a.expanded = false
- a.accountType = type
- a.gapLimitExceeded =
- !a.isChange &&
- a.addressIndex >
- lastAcctiveAddress.addressIndex +
- this.config.DEFAULT_RECEIVE_GAP_LIMIT
- })
- this.addresses.data.push(...uniqueAddresses)
- }
- },
- updateAmountForAddress: async function (addressData, amount = 0) {
- try {
- const wallet = this.g.user.wallets[0]
- addressData.amount = amount
- if (!addressData.isChange) {
- const addressWallet = this.walletAccounts.find(
- w => w.id === addressData.wallet
- )
- if (
- addressWallet &&
- addressWallet.address_no < addressData.addressIndex
- ) {
- addressWallet.address_no = addressData.addressIndex
- }
- }
-
- await LNbits.api.request(
- 'PUT',
- `/watchonly/api/v1/address/${addressData.id}`,
- wallet.adminkey,
- {amount}
- )
- } catch (err) {
- addressData.error = 'Failed to refresh amount for address'
- this.$q.notify({
- type: 'warning',
- message: `Failed to refresh amount for address ${addressData.address}`,
- timeout: 10000
- })
- LNbits.utils.notifyApiError(err)
- }
- },
- updateNoteForAddress: async function (addressData, note) {
- try {
- const wallet = this.g.user.wallets[0]
- await LNbits.api.request(
- 'PUT',
- `/watchonly/api/v1/address/${addressData.id}`,
- wallet.adminkey,
- {note: addressData.note}
- )
- const updatedAddress =
- this.addresses.data.find(a => a.id === addressData.id) || {}
- updatedAddress.note = note
- } catch (err) {
- LNbits.utils.notifyApiError(err)
- }
- },
- getFilteredAddresses: function () {
- const selectedWalletId = this.addresses.selectedWallet?.id
- const filter = this.addresses.filterValues || []
- const includeChangeAddrs = filter.includes('Show Change Addresses')
- const includeGapAddrs = filter.includes('Show Gap Addresses')
- const excludeNoAmount = filter.includes('Only With Amount')
-
- const walletsLimit = this.walletAccounts.reduce((r, w) => {
- r[`_${w.id}`] = w.address_no
- return r
- }, {})
-
- const addresses = this.addresses.data.filter(
- a =>
- (includeChangeAddrs || !a.isChange) &&
- (includeGapAddrs ||
- a.isChange ||
- a.addressIndex <= walletsLimit[`_${a.wallet}`]) &&
- !(excludeNoAmount && a.amount === 0) &&
- (!selectedWalletId || a.wallet === selectedWalletId)
- )
- return addresses
- },
- openGetFreshAddressDialog: async function (walletId) {
- const {data} = await LNbits.api.request(
- 'GET',
- `/watchonly/api/v1/address/${walletId}`,
- this.g.user.wallets[0].inkey
- )
- const addressData = mapAddressesData(data)
-
- addressData.note = `Shared on ${currentDateTime()}`
- const lastAcctiveAddress =
- this.addresses.data
- .filter(
- a => a.wallet === addressData.wallet && !a.isChange && a.hasActivity
- )
- .pop() || {}
- addressData.gapLimitExceeded =
- !addressData.isChange &&
- addressData.addressIndex >
- lastAcctiveAddress.addressIndex +
- this.config.DEFAULT_RECEIVE_GAP_LIMIT
-
- this.openQrCodeDialog(addressData)
- const wallet = this.walletAccounts.find(w => w.id === walletId) || {}
- wallet.address_no = addressData.addressIndex
- await this.refreshAddresses()
- },
-
- //################### ADDRESS HISTORY ###################
- addressHistoryFromTxs: function (addressData, txs) {
- const addressHistory = []
- txs.forEach(tx => {
- const sent = tx.vin
- .filter(
- vin => vin.prevout.scriptpubkey_address === addressData.address
- )
- .map(vin => mapInputToSentHistory(tx, addressData, vin))
-
- const received = tx.vout
- .filter(vout => vout.scriptpubkey_address === addressData.address)
- .map(vout => mapOutputToReceiveHistory(tx, addressData, vout))
- addressHistory.push(...sent, ...received)
- })
- return addressHistory
- },
- getFilteredAddressesHistory: function () {
- return this.addresses.history.filter(
- a => (!a.isChange || a.sent) && !a.isSubItem
- )
- },
- exportHistoryToCSV: function () {
- const history = this.getFilteredAddressesHistory().map(a => ({
- ...a,
- action: a.sent ? 'Sent' : 'Received'
- }))
- LNbits.utils.exportCSV(
- this.historyTable.exportColums,
- history,
- 'address-history'
- )
- },
- markSameTxAddressHistory: function () {
- this.addresses.history
- .filter(s => s.sent)
- .forEach((el, i, arr) => {
- if (el.isSubItem) return
-
- const sameTxItems = arr.slice(i + 1).filter(e => e.txId === el.txId)
- if (!sameTxItems.length) return
- sameTxItems.forEach(e => {
- e.isSubItem = true
- })
-
- el.totalAmount =
- el.amount + sameTxItems.reduce((t, e) => (t += e.amount || 0), 0)
- el.sameTxItems = sameTxItems
- })
- },
- showAddressHistoryDetails: function (addressHistory) {
- addressHistory.expanded = true
- },
-
- //################### PAYMENT ###################
- createTx: function (excludeChange = false) {
- const tx = {
- fee_rate: this.payment.feeRate,
- tx_size: this.payment.txSize,
- masterpubs: this.walletAccounts.map(w => ({
- public_key: w.masterpub,
- fingerprint: w.fingerprint
- }))
- }
- tx.inputs = this.utxos.data
- .filter(utxo => utxo.selected)
- .map(mapUtxoToPsbtInput)
- .sort((a, b) =>
- a.tx_id < b.tx_id ? -1 : a.tx_id > b.tx_id ? 1 : a.vout - b.vout
- )
-
- tx.outputs = this.payment.data.map(out => ({
- address: out.address,
- amount: out.amount
- }))
-
- if (excludeChange) {
- this.payment.changeAmount = 0
- } else {
- const change = this.createChangeOutput()
- this.payment.changeAmount = change.amount
- if (change.amount >= this.DUST_LIMIT) {
- tx.outputs.push(change)
- }
- }
- // Only sort by amount on UI level (no lib for address decode)
- // Should sort by scriptPubKey (as byte array) on the backend
- tx.outputs.sort((a, b) => a.amount - b.amount)
-
- return tx
- },
- createChangeOutput: function () {
- const change = this.payment.changeAddress
- const fee = this.payment.feeRate * this.payment.txSize
- const inputAmount = this.getTotalSelectedUtxoAmount()
- const payedAmount = this.getTotalPaymentAmount()
- const walletAcount =
- this.walletAccounts.find(w => w.id === change.wallet) || {}
-
+ // components: ['my-checkbox'],
+ data: function () {
return {
- address: change.address,
- amount: inputAmount - payedAmount - fee,
- addressIndex: change.addressIndex,
- addressIndex: change.addressIndex,
- masterpub_fingerprint: walletAcount.fingerprint
+ DUST_LIMIT: 546,
+ filter: '',
+
+ scan: {
+ scanning: false,
+ scanCount: 0,
+ scanIndex: 0
+ },
+
+ currentAddress: null,
+
+ tab: 'addresses',
+
+ config: {
+ data: {
+ mempool_endpoint: 'https://mempool.space',
+ receive_gap_limit: 20,
+ change_gap_limit: 5
+ },
+ DEFAULT_RECEIVE_GAP_LIMIT: 20,
+ show: false
+ },
+
+ serial: {
+ selectedPort: null,
+ writableStreamClosed: null,
+ writer: null,
+ readableStreamClosed: null,
+ reader: null,
+ showAdvancedConfig: false,
+ receivedData: '',
+ config: {}
+ },
+
+ hww: {
+ password: null,
+ showPassword: false,
+ mnemonic: null,
+ showMnemonic: false,
+ authenticated: false,
+ showPasswordDialog: false,
+ showWipeDialog: false,
+ showRestoreDialog: false,
+ showConsole: false,
+ showSignedPsbt: false,
+ sendingPsbt: false,
+ signingPsbt: false,
+ psbtSent: false
+ },
+
+ formDialog: {
+ show: false,
+ data: {}
+ },
+
+ qrCodeDialog: {
+ show: false,
+ data: null
+ },
+ ...tables,
+ ...tableData
}
},
- computeFee: function () {
- const tx = this.createTx()
- this.payment.txSize = Math.round(txSize(tx))
- return this.payment.feeRate * this.payment.txSize
- },
- deletePaymentAddress: function (v) {
- const index = this.payment.data.indexOf(v)
- if (index !== -1) {
- this.payment.data.splice(index, 1)
- }
- },
- initPaymentData: async function () {
- if (!this.payment.show) return
- await this.refreshAddresses()
- this.payment.showAdvanced = false
- this.payment.changeWallet = this.walletAccounts[0]
- this.selectChangeAddress(this.payment.changeWallet)
-
- await this.refreshRecommendedFees()
- this.payment.feeRate = this.payment.recommededFees.halfHourFee
- },
- getFeeRateLabel: function (feeRate) {
- const fees = this.payment.recommededFees
- if (feeRate >= fees.fastestFee) return `High Priority (${feeRate} sat/vB)`
- if (feeRate >= fees.halfHourFee)
- return `Medium Priority (${feeRate} sat/vB)`
- if (feeRate >= fees.hourFee) return `Low Priority (${feeRate} sat/vB)`
- return `No Priority (${feeRate} sat/vB)`
- },
- addPaymentAddress: function () {
- this.payment.data.push({address: '', amount: undefined})
- },
- getTotalPaymentAmount: function () {
- return this.payment.data.reduce((t, a) => t + (a.amount || 0), 0)
- },
- selectChangeAddress: function (wallet = {}) {
- this.payment.changeAddress =
- this.addresses.data.find(
- a => a.wallet === wallet.id && a.isChange && !a.hasActivity
- ) || {}
- },
- goToPaymentView: async function () {
- this.payment.show = true
- this.tab = 'utxos'
- await this.initPaymentData()
- },
- sendMaxToAddress: function (paymentAddress = {}) {
- paymentAddress.amount = 0
- const tx = this.createTx(true)
- this.payment.txSize = Math.round(txSize(tx))
- const fee = this.payment.feeRate * this.payment.txSize
- const inputAmount = this.getTotalSelectedUtxoAmount()
- const payedAmount = this.getTotalPaymentAmount()
- paymentAddress.amount = Math.max(0, inputAmount - payedAmount - fee)
- },
- //################### PSBT ###################
- createPsbt: async function () {
- const wallet = this.g.user.wallets[0]
- try {
- this.computeFee()
- const tx = this.createTx()
- txSize(tx)
- for (const input of tx.inputs) {
- input.tx_hex = await this.fetchTxHex(input.tx_id)
+ methods: {
+ //################### CONFIG ###################
+ getConfig: async function () {
+ try {
+ const {data} = await LNbits.api.request(
+ 'GET',
+ '/watchonly/api/v1/config',
+ this.g.user.wallets[0].adminkey
+ )
+ this.config.data = data
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
}
-
- this.payment.tx = tx
- const {data} = await LNbits.api.request(
- 'POST',
- '/watchonly/api/v1/psbt',
- wallet.adminkey,
- tx
- )
-
- this.payment.psbtBase64 = data
- } catch (err) {
- LNbits.utils.notifyApiError(err)
- }
- },
- extractTxFromPsbt: async function (psbtBase64) {
- const wallet = this.g.user.wallets[0]
- try {
- const {data} = await LNbits.api.request(
- 'PUT',
- '/watchonly/api/v1/psbt/extract',
- wallet.adminkey,
- {
- psbtBase64,
- inputs: this.payment.tx.inputs
- }
- )
- return data
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Cannot finalize PSBT!',
- timeout: 10000
- })
- LNbits.utils.notifyApiError(error)
- }
- },
- updateSignedPsbt: async function (value) {
- this.payment.psbtBase64Signed = value
-
- const data = await this.extractTxFromPsbt(this.payment.psbtBase64Signed)
- if (data) {
- this.payment.signedTx = JSON.parse(data.tx_json)
- this.payment.signedTxHex = data.tx_hex
- } else {
- this.payment.signedTx = null
- this.payment.signedTxHex = null
- }
- },
- broadcastTransaction: async function () {
- try {
+ },
+ updateConfig: async function () {
const wallet = this.g.user.wallets[0]
- const {data} = await LNbits.api.request(
- 'POST',
- '/watchonly/api/v1/tx',
- wallet.adminkey,
- {tx_hex: this.payment.signedTxHex}
- )
- this.payment.sentTxId = data
+ try {
+ await LNbits.api.request(
+ 'PUT',
+ '/watchonly/api/v1/config',
+ wallet.adminkey,
+ this.config.data
+ )
+ this.config.show = false
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ }
+ },
- this.$q.notify({
- type: 'positive',
- message: 'Transaction broadcasted!',
- caption: `${data}`,
- timeout: 10000
- })
+ //################### WALLETS ###################
+ getWalletName: function (walletId) {
+ const wallet = this.walletAccounts.find(wl => wl.id === walletId)
+ return wallet ? wallet.title : 'unknown'
+ },
+ addWalletAccount: async function () {
+ const wallet = this.g.user.wallets[0]
+ const data = _.omit(this.formDialog.data, 'wallet')
+ await this.createWalletAccount(wallet, data)
+ },
+ createWalletAccount: async function (wallet, data) {
+ try {
+ const response = await LNbits.api.request(
+ 'POST',
+ '/watchonly/api/v1/wallet',
+ wallet.adminkey,
+ data
+ )
+ this.walletAccounts.push(mapWalletAccount(response.data))
+ this.formDialog.show = false
- this.hww.psbtSent = false
- this.payment.psbtBase64Signed = null
- this.payment.signedTxHex = null
- this.payment.signedTx = null
- this.payment.psbtBase64 = null
+ await this.refreshWalletAccounts()
+ await this.refreshAddresses()
- await this.scanAddressWithAmount()
- } catch (error) {
- this.payment.sentTxId = null
- this.$q.notify({
- type: 'warning',
- message: 'Failed to broadcast!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- //################### SERIAL PORT ###################
- checkSerialPortSupported: function () {
- if (!navigator.serial) {
- this.$q.notify({
- type: 'warning',
- message: 'Serial port communication not supported!',
- caption:
- 'Make sure your browser supports Serial Port and that you are using HTTPS.',
- timeout: 10000
- })
- return false
- }
- return true
- },
- openSerialPort: async function () {
- if (!this.checkSerialPortSupported()) return
- console.log('### openSerialPort', this.serial.selectedPort)
- try {
- navigator.serial.addEventListener('connect', event => {
- console.log('### navigator.serial event: connected!', event)
- })
-
- navigator.serial.addEventListener('disconnect', () => {
- this.hww.authenticated = false
+ if (!this.payment.changeWallett) {
+ this.payment.changeWallet = this.walletAccounts[0]
+ this.selectChangeAddress(this.payment.changeWallet)
+ }
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ }
+ },
+ deleteWalletAccount: function (walletAccountId) {
+ LNbits.utils
+ .confirmDialog(
+ 'Are you sure you want to delete this watch only wallet?'
+ )
+ .onOk(async () => {
+ try {
+ await LNbits.api.request(
+ 'DELETE',
+ '/watchonly/api/v1/wallet/' + walletAccountId,
+ this.g.user.wallets[0].adminkey
+ )
+ this.walletAccounts = _.reject(this.walletAccounts, function (
+ obj
+ ) {
+ return obj.id === walletAccountId
+ })
+ await this.refreshWalletAccounts()
+ await this.refreshAddresses()
+ if (
+ this.payment.changeWallet &&
+ this.payment.changeWallet.id === walletAccountId
+ ) {
+ this.payment.changeWallet = this.walletAccounts[0]
+ this.selectChangeAddress(this.payment.changeWallet)
+ }
+ await this.scanAddressWithAmount()
+ } catch (error) {
+ this.$q.notify({
+ type: 'warning',
+ message:
+ 'Error while deleting wallet account. Please try again.',
+ timeout: 10000
+ })
+ }
+ })
+ },
+ getAddressesForWallet: async function (walletId) {
+ try {
+ const {data} = await LNbits.api.request(
+ 'GET',
+ '/watchonly/api/v1/addresses/' + walletId,
+ this.g.user.wallets[0].inkey
+ )
+ return data.map(mapAddressesData)
+ } catch (err) {
this.$q.notify({
type: 'warning',
- message: 'Disconnected from Serial Port!',
+ message: `Failed to fetch addresses for wallet with id ${walletId}.`,
timeout: 10000
})
- })
- this.serial.selectedPort = await navigator.serial.requestPort()
- // Wait for the serial port to open.
- await this.serial.selectedPort.open({baudRate: 9600})
- this.startSerialPortReading()
-
- const textEncoder = new TextEncoderStream()
- this.serial.writableStreamClosed = textEncoder.readable.pipeTo(
- this.serial.selectedPort.writable
- )
-
- this.serial.writer = textEncoder.writable.getWriter()
- } catch (error) {
- this.serial.selectedPort = null
- this.$q.notify({
- type: 'warning',
- message: 'Cannot open serial port!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- closeSerialPort: async function () {
- try {
- console.log('### closeSerialPort', this.serial.selectedPort)
- if (this.serial.writer) this.serial.writer.close()
- if (this.serial.writableStreamClosed)
- await this.serial.writableStreamClosed
- if (this.serial.reader) this.serial.reader.cancel()
- if (this.serial.readableStreamClosed)
- await this.serial.readableStreamClosed.catch(() => {
- /* Ignore the error */
- })
- if (this.serial.selectedPort) await this.serial.selectedPort.close()
- this.serial.selectedPort = null
- this.$q.notify({
- type: 'positive',
- message: 'Serial port disconnected!',
- timeout: 5000
- })
- } catch (error) {
- this.serial.selectedPort = null
- console.log('### error', error)
- this.$q.notify({
- type: 'warning',
- message: 'Cannot close serial port!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
-
- startSerialPortReading: async function () {
- const port = this.serial.selectedPort
-
- while (port && port.readable) {
- const textDecoder = new TextDecoderStream()
- this.serial.readableStreamClosed = port.readable.pipeTo(
- textDecoder.writable
- )
- this.serial.reader = textDecoder.readable.getReader()
- const readStringUntil = readFromSerialPort(this.serial)
-
+ LNbits.utils.notifyApiError(err)
+ }
+ return []
+ },
+ getWatchOnlyWallets: async function () {
try {
- while (true) {
- const {value, done} = await readStringUntil('\n')
- console.log('### value', value)
- if (value) {
- this.handleSerialPortResponse(value)
- this.updateSerialPortConsole(value)
- }
- console.log('### startSerialPortReading DONE', done)
- if (done) return
- }
+ const {data} = await LNbits.api.request(
+ 'GET',
+ '/watchonly/api/v1/wallet',
+ this.g.user.wallets[0].inkey
+ )
+ return data
} catch (error) {
this.$q.notify({
type: 'warning',
- message: 'Serial port communication error!',
+ message: 'Failed to fetch wallets.',
+ timeout: 10000
+ })
+ LNbits.utils.notifyApiError(error)
+ }
+ return []
+ },
+ refreshWalletAccounts: async function () {
+ const wallets = await this.getWatchOnlyWallets()
+ this.walletAccounts = wallets.map(w => mapWalletAccount(w))
+ },
+ getAmmountForWallet: function (walletId) {
+ const amount = this.addresses.data
+ .filter(a => a.wallet === walletId)
+ .reduce((t, a) => t + a.amount || 0, 0)
+ return this.satBtc(amount)
+ },
+
+ //################### ADDRESSES ###################
+
+ refreshAddresses: async function () {
+ const wallets = await this.getWatchOnlyWallets()
+ this.addresses.data = []
+ for (const {id, type} of wallets) {
+ const newAddresses = await this.getAddressesForWallet(id)
+ const uniqueAddresses = newAddresses.filter(
+ newAddr =>
+ !this.addresses.data.find(a => a.address === newAddr.address)
+ )
+
+ const lastAcctiveAddress =
+ uniqueAddresses.filter(a => !a.isChange && a.hasActivity).pop() ||
+ {}
+
+ uniqueAddresses.forEach(a => {
+ a.expanded = false
+ a.accountType = type
+ a.gapLimitExceeded =
+ !a.isChange &&
+ a.addressIndex >
+ lastAcctiveAddress.addressIndex +
+ this.config.DEFAULT_RECEIVE_GAP_LIMIT
+ })
+ this.addresses.data.push(...uniqueAddresses)
+ }
+ },
+ updateAmountForAddress: async function (addressData, amount = 0) {
+ try {
+ const wallet = this.g.user.wallets[0]
+ addressData.amount = amount
+ if (!addressData.isChange) {
+ const addressWallet = this.walletAccounts.find(
+ w => w.id === addressData.wallet
+ )
+ if (
+ addressWallet &&
+ addressWallet.address_no < addressData.addressIndex
+ ) {
+ addressWallet.address_no = addressData.addressIndex
+ }
+ }
+
+ await LNbits.api.request(
+ 'PUT',
+ `/watchonly/api/v1/address/${addressData.id}`,
+ wallet.adminkey,
+ {amount}
+ )
+ } catch (err) {
+ addressData.error = 'Failed to refresh amount for address'
+ this.$q.notify({
+ type: 'warning',
+ message: `Failed to refresh amount for address ${addressData.address}`,
+ timeout: 10000
+ })
+ LNbits.utils.notifyApiError(err)
+ }
+ },
+ updateNoteForAddress: async function (addressData, note) {
+ try {
+ const wallet = this.g.user.wallets[0]
+ await LNbits.api.request(
+ 'PUT',
+ `/watchonly/api/v1/address/${addressData.id}`,
+ wallet.adminkey,
+ {note: addressData.note}
+ )
+ const updatedAddress =
+ this.addresses.data.find(a => a.id === addressData.id) || {}
+ updatedAddress.note = note
+ } catch (err) {
+ LNbits.utils.notifyApiError(err)
+ }
+ },
+ getFilteredAddresses: function () {
+ const selectedWalletId = this.addresses.selectedWallet?.id
+ const filter = this.addresses.filterValues || []
+ const includeChangeAddrs = filter.includes('Show Change Addresses')
+ const includeGapAddrs = filter.includes('Show Gap Addresses')
+ const excludeNoAmount = filter.includes('Only With Amount')
+
+ const walletsLimit = this.walletAccounts.reduce((r, w) => {
+ r[`_${w.id}`] = w.address_no
+ return r
+ }, {})
+
+ const addresses = this.addresses.data.filter(
+ a =>
+ (includeChangeAddrs || !a.isChange) &&
+ (includeGapAddrs ||
+ a.isChange ||
+ a.addressIndex <= walletsLimit[`_${a.wallet}`]) &&
+ !(excludeNoAmount && a.amount === 0) &&
+ (!selectedWalletId || a.wallet === selectedWalletId)
+ )
+ return addresses
+ },
+ openGetFreshAddressDialog: async function (walletId) {
+ const {data} = await LNbits.api.request(
+ 'GET',
+ `/watchonly/api/v1/address/${walletId}`,
+ this.g.user.wallets[0].inkey
+ )
+ const addressData = mapAddressesData(data)
+
+ addressData.note = `Shared on ${currentDateTime()}`
+ const lastAcctiveAddress =
+ this.addresses.data
+ .filter(
+ a =>
+ a.wallet === addressData.wallet && !a.isChange && a.hasActivity
+ )
+ .pop() || {}
+ addressData.gapLimitExceeded =
+ !addressData.isChange &&
+ addressData.addressIndex >
+ lastAcctiveAddress.addressIndex +
+ this.config.DEFAULT_RECEIVE_GAP_LIMIT
+
+ this.openQrCodeDialog(addressData)
+ const wallet = this.walletAccounts.find(w => w.id === walletId) || {}
+ wallet.address_no = addressData.addressIndex
+ await this.refreshAddresses()
+ },
+
+ //################### ADDRESS HISTORY ###################
+ addressHistoryFromTxs: function (addressData, txs) {
+ const addressHistory = []
+ txs.forEach(tx => {
+ const sent = tx.vin
+ .filter(
+ vin => vin.prevout.scriptpubkey_address === addressData.address
+ )
+ .map(vin => mapInputToSentHistory(tx, addressData, vin))
+
+ const received = tx.vout
+ .filter(vout => vout.scriptpubkey_address === addressData.address)
+ .map(vout => mapOutputToReceiveHistory(tx, addressData, vout))
+ addressHistory.push(...sent, ...received)
+ })
+ return addressHistory
+ },
+ getFilteredAddressesHistory: function () {
+ return this.addresses.history.filter(
+ a => (!a.isChange || a.sent) && !a.isSubItem
+ )
+ },
+ exportHistoryToCSV: function () {
+ const history = this.getFilteredAddressesHistory().map(a => ({
+ ...a,
+ action: a.sent ? 'Sent' : 'Received'
+ }))
+ LNbits.utils.exportCSV(
+ this.historyTable.exportColums,
+ history,
+ 'address-history'
+ )
+ },
+ markSameTxAddressHistory: function () {
+ this.addresses.history
+ .filter(s => s.sent)
+ .forEach((el, i, arr) => {
+ if (el.isSubItem) return
+
+ const sameTxItems = arr.slice(i + 1).filter(e => e.txId === el.txId)
+ if (!sameTxItems.length) return
+ sameTxItems.forEach(e => {
+ e.isSubItem = true
+ })
+
+ el.totalAmount =
+ el.amount + sameTxItems.reduce((t, e) => (t += e.amount || 0), 0)
+ el.sameTxItems = sameTxItems
+ })
+ },
+ showAddressHistoryDetails: function (addressHistory) {
+ addressHistory.expanded = true
+ },
+
+ //################### PAYMENT ###################
+ createTx: function (excludeChange = false) {
+ const tx = {
+ fee_rate: this.payment.feeRate,
+ tx_size: this.payment.txSize,
+ masterpubs: this.walletAccounts.map(w => ({
+ public_key: w.masterpub,
+ fingerprint: w.fingerprint
+ }))
+ }
+ tx.inputs = this.utxos.data
+ .filter(utxo => utxo.selected)
+ .map(mapUtxoToPsbtInput)
+ .sort((a, b) =>
+ a.tx_id < b.tx_id ? -1 : a.tx_id > b.tx_id ? 1 : a.vout - b.vout
+ )
+
+ tx.outputs = this.payment.data.map(out => ({
+ address: out.address,
+ amount: out.amount
+ }))
+
+ if (excludeChange) {
+ this.payment.changeAmount = 0
+ } else {
+ const change = this.createChangeOutput()
+ this.payment.changeAmount = change.amount
+ if (change.amount >= this.DUST_LIMIT) {
+ tx.outputs.push(change)
+ }
+ }
+ // Only sort by amount on UI level (no lib for address decode)
+ // Should sort by scriptPubKey (as byte array) on the backend
+ tx.outputs.sort((a, b) => a.amount - b.amount)
+
+ return tx
+ },
+ createChangeOutput: function () {
+ const change = this.payment.changeAddress
+ const fee = this.payment.feeRate * this.payment.txSize
+ const inputAmount = this.getTotalSelectedUtxoAmount()
+ const payedAmount = this.getTotalPaymentAmount()
+ const walletAcount =
+ this.walletAccounts.find(w => w.id === change.wallet) || {}
+
+ return {
+ address: change.address,
+ amount: inputAmount - payedAmount - fee,
+ addressIndex: change.addressIndex,
+ addressIndex: change.addressIndex,
+ masterpub_fingerprint: walletAcount.fingerprint
+ }
+ },
+ computeFee: function () {
+ const tx = this.createTx()
+ this.payment.txSize = Math.round(txSize(tx))
+ return this.payment.feeRate * this.payment.txSize
+ },
+ deletePaymentAddress: function (v) {
+ const index = this.payment.data.indexOf(v)
+ if (index !== -1) {
+ this.payment.data.splice(index, 1)
+ }
+ },
+ initPaymentData: async function () {
+ if (!this.payment.show) return
+ await this.refreshAddresses()
+
+ this.payment.showAdvanced = false
+ this.payment.changeWallet = this.walletAccounts[0]
+ this.selectChangeAddress(this.payment.changeWallet)
+
+ await this.refreshRecommendedFees()
+ this.payment.feeRate = this.payment.recommededFees.halfHourFee
+ },
+ getFeeRateLabel: function (feeRate) {
+ const fees = this.payment.recommededFees
+ if (feeRate >= fees.fastestFee)
+ return `High Priority (${feeRate} sat/vB)`
+ if (feeRate >= fees.halfHourFee)
+ return `Medium Priority (${feeRate} sat/vB)`
+ if (feeRate >= fees.hourFee) return `Low Priority (${feeRate} sat/vB)`
+ return `No Priority (${feeRate} sat/vB)`
+ },
+ addPaymentAddress: function () {
+ this.payment.data.push({address: '', amount: undefined})
+ },
+ getTotalPaymentAmount: function () {
+ return this.payment.data.reduce((t, a) => t + (a.amount || 0), 0)
+ },
+ selectChangeAddress: function (wallet = {}) {
+ this.payment.changeAddress =
+ this.addresses.data.find(
+ a => a.wallet === wallet.id && a.isChange && !a.hasActivity
+ ) || {}
+ },
+ goToPaymentView: async function () {
+ this.payment.show = true
+ this.tab = 'utxos'
+ await this.initPaymentData()
+ },
+ sendMaxToAddress: function (paymentAddress = {}) {
+ paymentAddress.amount = 0
+ const tx = this.createTx(true)
+ this.payment.txSize = Math.round(txSize(tx))
+ const fee = this.payment.feeRate * this.payment.txSize
+ const inputAmount = this.getTotalSelectedUtxoAmount()
+ const payedAmount = this.getTotalPaymentAmount()
+ paymentAddress.amount = Math.max(0, inputAmount - payedAmount - fee)
+ },
+ //################### PSBT ###################
+ createPsbt: async function () {
+ const wallet = this.g.user.wallets[0]
+ try {
+ this.computeFee()
+ const tx = this.createTx()
+ txSize(tx)
+ for (const input of tx.inputs) {
+ input.tx_hex = await this.fetchTxHex(input.tx_id)
+ }
+
+ this.payment.tx = tx
+ const {data} = await LNbits.api.request(
+ 'POST',
+ '/watchonly/api/v1/psbt',
+ wallet.adminkey,
+ tx
+ )
+
+ this.payment.psbtBase64 = data
+ } catch (err) {
+ LNbits.utils.notifyApiError(err)
+ }
+ },
+ extractTxFromPsbt: async function (psbtBase64) {
+ const wallet = this.g.user.wallets[0]
+ try {
+ const {data} = await LNbits.api.request(
+ 'PUT',
+ '/watchonly/api/v1/psbt/extract',
+ wallet.adminkey,
+ {
+ psbtBase64,
+ inputs: this.payment.tx.inputs
+ }
+ )
+ return data
+ } catch (error) {
+ this.$q.notify({
+ type: 'warning',
+ message: 'Cannot finalize PSBT!',
+ timeout: 10000
+ })
+ LNbits.utils.notifyApiError(error)
+ }
+ },
+ updateSignedPsbt: async function (value) {
+ this.payment.psbtBase64Signed = value
+
+ const data = await this.extractTxFromPsbt(this.payment.psbtBase64Signed)
+ if (data) {
+ this.payment.signedTx = JSON.parse(data.tx_json)
+ this.payment.signedTxHex = data.tx_hex
+ } else {
+ this.payment.signedTx = null
+ this.payment.signedTxHex = null
+ }
+ },
+ broadcastTransaction: async function () {
+ try {
+ const wallet = this.g.user.wallets[0]
+ const {data} = await LNbits.api.request(
+ 'POST',
+ '/watchonly/api/v1/tx',
+ wallet.adminkey,
+ {tx_hex: this.payment.signedTxHex}
+ )
+ this.payment.sentTxId = data
+
+ this.$q.notify({
+ type: 'positive',
+ message: 'Transaction broadcasted!',
+ caption: `${data}`,
+ timeout: 10000
+ })
+
+ this.hww.psbtSent = false
+ this.payment.psbtBase64Signed = null
+ this.payment.signedTxHex = null
+ this.payment.signedTx = null
+ this.payment.psbtBase64 = null
+
+ await this.scanAddressWithAmount()
+ } catch (error) {
+ this.payment.sentTxId = null
+ this.$q.notify({
+ type: 'warning',
+ message: 'Failed to broadcast!',
caption: `${error}`,
timeout: 10000
})
}
- }
- console.log('### startSerialPortReading port', port)
- },
-
- handleSerialPortResponse: function (value) {
- const msg = value.split(' ')
- if (msg[0] == COMMAND_SIGN_PSBT) this.handleSignResponse(msg[1])
- else if (msg[0] == COMMAND_PASSWORD) this.handleLoginResponse(msg[1])
- else if (msg[0] == COMMAND_PASSWORD_CLEAR)
- this.handleLogoutResponse(msg[1])
- else if (msg[0] == COMMAND_SEND_PSBT) this.handleSendPsbtResponse(msg[1])
- else if (msg[0] == COMMAND_WIPE) this.handleWipeResponse(msg[1])
- else console.log('### handleSerialPortResponse', value)
- },
- updateSerialPortConsole: function (value) {
- this.serial.receivedData += value + '\n'
- const textArea = document.getElementById(
- 'watchonly-serial-port-data-input'
- )
- if (textArea) textArea.scrollTop = textArea.scrollHeight
- },
- sharePsbtWithAnimatedQRCode: async function () {
- console.log('### sharePsbtWithAnimatedQRCode')
- },
- //################### HARDWARE WALLET ###################
- hwwShowPasswordDialog: async function () {
- try {
- this.hww.showPasswordDialog = true
- await this.serial.writer.write(COMMAND_PASSWORD + '\n')
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to connect to Hardware Wallet!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- hwwShowWipeDialog: async function () {
- try {
- this.hww.showWipeDialog = true
- await this.serial.writer.write(COMMAND_WIPE + '\n')
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to connect to Hardware Wallet!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- hwwShowRestoreDialog: async function () {
- try {
- this.hww.showRestoreDialog = true
- await this.serial.writer.write(COMMAND_WIPE + '\n')
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to connect to Hardware Wallet!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- hwwLogin: async function () {
- try {
- await this.serial.writer.write(
- COMMAND_PASSWORD + ' ' + this.hww.password + '\n'
- )
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to send password to Hardware Wallet!',
- caption: `${error}`,
- timeout: 10000
- })
- } finally {
- this.hww.showPasswordDialog = false
- this.hww.password = null
- this.hww.showPassword = false
- }
- },
- handleLoginResponse: function (res = '') {
- this.hww.authenticated = res.trim() === '1'
- if (this.hww.authenticated) {
- this.$q.notify({
- type: 'positive',
- message: 'Login successfull!',
- timeout: 10000
- })
- } else {
- this.$q.notify({
- type: 'warning',
- message: 'Wrong password, try again!',
- timeout: 10000
- })
- }
- },
- hwwLogout: async function () {
- try {
- await this.serial.writer.write(COMMAND_PASSWORD_CLEAR + '\n')
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to logout from Hardware Wallet!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- handleLogoutResponse: function (res = '') {
- this.hww.authenticated = !(res.trim() === '1')
- if (this.hww.authenticated) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to logout from Hardware Wallet',
- timeout: 10000
- })
- }
- },
- hwwExecuteDefaultCommand: function () {
- if (this.hww.authenticated) {
- this.hwwSendPsbt()
- } else {
- this.hwwShowPasswordDialog()
- }
- },
- hwwSendPsbt: async function () {
- try {
- this.hww.sendingPsbt = true
- await this.serial.writer.write(
- COMMAND_SEND_PSBT + ' ' + this.payment.psbtBase64 + '\n'
- )
- this.$q.notify({
- type: 'positive',
- message: 'Data sent to serial port device!',
- timeout: 5000
- })
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to send data to serial port!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- handleSendPsbtResponse: function (res = '') {
- this.hww.psbtSent = true
- this.hww.sendingPsbt = false
- },
- hwwSignPsbt: async function () {
- try {
- this.hww.signingPsbt = true
- await this.serial.writer.write(COMMAND_SIGN_PSBT + '\n')
- this.$q.notify({
- type: 'positive',
- message: 'PSBT signed!',
- timeout: 5000
- })
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to sign PSBT!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- handleSignResponse: function (res = '') {
- this.hww.signingPsbt = false
- this.updateSignedPsbt(res)
- if (this.hww.authenticated) {
- this.$q.notify({
- type: 'positive',
- message: 'Transaction Signed',
- timeout: 10000
- })
- }
- },
- hwwHelp: async function () {
- try {
- await this.serial.writer.write(COMMAND_HELP + '\n')
- this.$q.notify({
- type: 'positive',
- message: 'Check display or console for details!',
- timeout: 5000
- })
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to ask for help!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- hwwWipe: async function () {
- try {
- this.hww.showWipeDialog = false
- await this.serial.writer.write(
- COMMAND_WIPE + ' ' + this.hww.password + '\n'
- )
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to ask for help!',
- caption: `${error}`,
- timeout: 10000
- })
- } finally {
- this.hww.password = null
- this.hww.showPassword = false
- }
- },
- handleWipeResponse: function (res = '') {
- const wiped = res.trim() === '1'
- console.log('### wiped', wiped)
- if (wiped) {
- this.$q.notify({
- type: 'positive',
- message: 'Wallet wiped!',
- timeout: 10000
- })
- } else {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to wipe wallet!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- hwwShowSeed: async function () {
- try {
- await this.serial.writer.write(COMMAND_SEED + '\n')
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to show seed!',
- caption: `${error}`,
- timeout: 10000
- })
- }
- },
- hwwRestore: async function () {
- try {
- await this.serial.writer.write(
- COMMAND_RESTORE + ' ' + this.hww.mnemonic + '\n'
- )
- await this.serial.writer.write(
- COMMAND_PASSWORD + ' ' + this.hww.password + '\n'
- )
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: 'Failed to restore from seed!',
- caption: `${error}`,
- timeout: 10000
- })
- } finally {
- this.hww.showRestoreDialog = false
- this.hww.mnemonic = null
- this.hww.showMnemonic = false
- this.hww.password = null
- this.hww.showPassword = false
- }
- },
- //################### UTXOs ###################
- scanAllAddresses: async function () {
- await this.refreshAddresses()
- this.addresses.history = []
- let addresses = this.addresses.data
- this.utxos.data = []
- this.utxos.total = 0
- // Loop while new funds are found on the gap adresses.
- // Use 1000 limit as a safety check (scan 20 000 addresses max)
- for (let i = 0; i < 1000 && addresses.length; i++) {
- await this.updateUtxosForAddresses(addresses)
- const oldAddresses = this.addresses.data.slice()
- await this.refreshAddresses()
- const newAddresses = this.addresses.data.slice()
- // check if gap addresses have been extended
- addresses = newAddresses.filter(
- newAddr => !oldAddresses.find(oldAddr => oldAddr.id === newAddr.id)
- )
- if (addresses.length) {
+ },
+ //################### SERIAL PORT ###################
+ checkSerialPortSupported: function () {
+ if (!navigator.serial) {
this.$q.notify({
- type: 'positive',
- message: 'Funds found! Scanning for more...',
+ type: 'warning',
+ message: 'Serial port communication not supported!',
+ caption:
+ 'Make sure your browser supports Serial Port and that you are using HTTPS.',
+ timeout: 10000
+ })
+ return false
+ }
+ return true
+ },
+ openSerialPort: async function () {
+ if (!this.checkSerialPortSupported()) return
+ console.log('### openSerialPort', this.serial.selectedPort)
+ try {
+ navigator.serial.addEventListener('connect', event => {
+ console.log('### navigator.serial event: connected!', event)
+ })
+
+ navigator.serial.addEventListener('disconnect', () => {
+ this.hww.authenticated = false
+ this.$q.notify({
+ type: 'warning',
+ message: 'Disconnected from Serial Port!',
+ timeout: 10000
+ })
+ })
+ this.serial.selectedPort = await navigator.serial.requestPort()
+ // Wait for the serial port to open.
+ await this.serial.selectedPort.open({baudRate: 9600})
+ this.startSerialPortReading()
+
+ const textEncoder = new TextEncoderStream()
+ this.serial.writableStreamClosed = textEncoder.readable.pipeTo(
+ this.serial.selectedPort.writable
+ )
+
+ this.serial.writer = textEncoder.writable.getWriter()
+ } catch (error) {
+ this.serial.selectedPort = null
+ this.$q.notify({
+ type: 'warning',
+ message: 'Cannot open serial port!',
+ caption: `${error}`,
timeout: 10000
})
}
- }
- },
- scanAddressWithAmount: async function () {
- this.utxos.data = []
- this.utxos.total = 0
- this.addresses.history = []
- const addresses = this.addresses.data.filter(a => a.hasActivity)
- await this.updateUtxosForAddresses(addresses)
- },
- scanAddress: async function (addressData) {
- this.updateUtxosForAddresses([addressData])
- this.$q.notify({
- type: 'positive',
- message: 'Address Rescanned',
- timeout: 10000
- })
- },
- updateUtxosForAddresses: async function (addresses = []) {
- this.scan = {scanning: true, scanCount: addresses.length, scanIndex: 0}
-
- try {
- for (addrData of addresses) {
- const addressHistory = await this.getAddressTxsDelayed(addrData)
- // remove old entries
- this.addresses.history = this.addresses.history.filter(
- h => h.address !== addrData.address
- )
-
- // add new entrie
- this.addresses.history.push(...addressHistory)
- this.addresses.history.sort((a, b) =>
- !a.height ? -1 : b.height - a.height
- )
- this.markSameTxAddressHistory()
-
- if (addressHistory.length) {
- // search only if it ever had any activity
- const utxos = await this.getAddressTxsUtxoDelayed(addrData.address)
- this.updateUtxosForAddress(addrData, utxos)
- }
-
- this.scan.scanIndex++
+ },
+ closeSerialPort: async function () {
+ try {
+ console.log('### closeSerialPort', this.serial.selectedPort)
+ if (this.serial.writer) this.serial.writer.close()
+ if (this.serial.writableStreamClosed)
+ await this.serial.writableStreamClosed
+ if (this.serial.reader) this.serial.reader.cancel()
+ if (this.serial.readableStreamClosed)
+ await this.serial.readableStreamClosed.catch(() => {
+ /* Ignore the error */
+ })
+ if (this.serial.selectedPort) await this.serial.selectedPort.close()
+ this.serial.selectedPort = null
+ this.$q.notify({
+ type: 'positive',
+ message: 'Serial port disconnected!',
+ timeout: 5000
+ })
+ } catch (error) {
+ this.serial.selectedPort = null
+ console.log('### error', error)
+ this.$q.notify({
+ type: 'warning',
+ message: 'Cannot close serial port!',
+ caption: `${error}`,
+ timeout: 10000
+ })
}
- } catch (error) {
- console.error(error)
+ },
+
+ startSerialPortReading: async function () {
+ const port = this.serial.selectedPort
+
+ while (port && port.readable) {
+ const textDecoder = new TextDecoderStream()
+ this.serial.readableStreamClosed = port.readable.pipeTo(
+ textDecoder.writable
+ )
+ this.serial.reader = textDecoder.readable.getReader()
+ const readStringUntil = readFromSerialPort(this.serial)
+
+ try {
+ while (true) {
+ const {value, done} = await readStringUntil('\n')
+ console.log('### value', value)
+ if (value) {
+ this.handleSerialPortResponse(value)
+ this.updateSerialPortConsole(value)
+ }
+ console.log('### startSerialPortReading DONE', done)
+ if (done) return
+ }
+ } catch (error) {
+ this.$q.notify({
+ type: 'warning',
+ message: 'Serial port communication error!',
+ caption: `${error}`,
+ timeout: 10000
+ })
+ }
+ }
+ console.log('### startSerialPortReading port', port)
+ },
+
+ handleSerialPortResponse: function (value) {
+ const msg = value.split(' ')
+ if (msg[0] == COMMAND_SIGN_PSBT) this.handleSignResponse(msg[1])
+ else if (msg[0] == COMMAND_PASSWORD) this.handleLoginResponse(msg[1])
+ else if (msg[0] == COMMAND_PASSWORD_CLEAR)
+ this.handleLogoutResponse(msg[1])
+ else if (msg[0] == COMMAND_SEND_PSBT)
+ this.handleSendPsbtResponse(msg[1])
+ else if (msg[0] == COMMAND_WIPE) this.handleWipeResponse(msg[1])
+ else console.log('### handleSerialPortResponse', value)
+ },
+ updateSerialPortConsole: function (value) {
+ this.serial.receivedData += value + '\n'
+ const textArea = document.getElementById(
+ 'watchonly-serial-port-data-input'
+ )
+ if (textArea) textArea.scrollTop = textArea.scrollHeight
+ },
+ sharePsbtWithAnimatedQRCode: async function () {
+ console.log('### sharePsbtWithAnimatedQRCode')
+ },
+ //################### HARDWARE WALLET ###################
+ hwwShowPasswordDialog: async function () {
+ try {
+ this.hww.showPasswordDialog = true
+ await this.serial.writer.write(COMMAND_PASSWORD + '\n')
+ } catch (error) {
+ this.$q.notify({
+ type: 'warning',
+ message: 'Failed to connect to Hardware Wallet!',
+ caption: `${error}`,
+ timeout: 10000
+ })
+ }
+ },
+ hwwShowWipeDialog: async function () {
+ try {
+ this.hww.showWipeDialog = true
+ await this.serial.writer.write(COMMAND_WIPE + '\n')
+ } catch (error) {
+ this.$q.notify({
+ type: 'warning',
+ message: 'Failed to connect to Hardware Wallet!',
+ caption: `${error}`,
+ timeout: 10000
+ })
+ }
+ },
+ hwwShowRestoreDialog: async function () {
+ try {
+ this.hww.showRestoreDialog = true
+ await this.serial.writer.write(COMMAND_WIPE + '\n')
+ } catch (error) {
+ this.$q.notify({
+ type: 'warning',
+ message: 'Failed to connect to Hardware Wallet!',
+ caption: `${error}`,
+ timeout: 10000
+ })
+ }
+ },
+ hwwLogin: async function () {
+ try {
+ await this.serial.writer.write(
+ COMMAND_PASSWORD + ' ' + this.hww.password + '\n'
+ )
+ } catch (error) {
+ this.$q.notify({
+ type: 'warning',
+ message: 'Failed to send password to Hardware Wallet!',
+ caption: `${error}`,
+ timeout: 10000
+ })
+ } finally {
+ this.hww.showPasswordDialog = false
+ this.hww.password = null
+ this.hww.showPassword = false
+ }
+ },
+ handleLoginResponse: function (res = '') {
+ this.hww.authenticated = res.trim() === '1'
+ if (this.hww.authenticated) {
+ this.$q.notify({
+ type: 'positive',
+ message: 'Login successfull!',
+ timeout: 10000
+ })
+ } else {
+ this.$q.notify({
+ type: 'warning',
+ message: 'Wrong password, try again!',
+ timeout: 10000
+ })
+ }
+ },
+ hwwLogout: async function () {
+ try {
+ await this.serial.writer.write(COMMAND_PASSWORD_CLEAR + '\n')
+ } catch (error) {
+ this.$q.notify({
+ type: 'warning',
+ message: 'Failed to logout from Hardware Wallet!',
+ caption: `${error}`,
+ timeout: 10000
+ })
+ }
+ },
+ handleLogoutResponse: function (res = '') {
+ this.hww.authenticated = !(res.trim() === '1')
+ if (this.hww.authenticated) {
+ this.$q.notify({
+ type: 'warning',
+ message: 'Failed to logout from Hardware Wallet',
+ timeout: 10000
+ })
+ }
+ },
+ hwwExecuteDefaultCommand: function () {
+ if (this.hww.authenticated) {
+ this.hwwSendPsbt()
+ } else {
+ this.hwwShowPasswordDialog()
+ }
+ },
+ hwwSendPsbt: async function () {
+ try {
+ this.hww.sendingPsbt = true
+ await this.serial.writer.write(
+ COMMAND_SEND_PSBT + ' ' + this.payment.psbtBase64 + '\n'
+ )
+ this.$q.notify({
+ type: 'positive',
+ message: 'Data sent to serial port device!',
+ timeout: 5000
+ })
+ } catch (error) {
+ this.$q.notify({
+ type: 'warning',
+ message: 'Failed to send data to serial port!',
+ caption: `${error}`,
+ timeout: 10000
+ })
+ }
+ },
+ handleSendPsbtResponse: function (res = '') {
+ this.hww.psbtSent = true
+ this.hww.sendingPsbt = false
+ },
+ hwwSignPsbt: async function () {
+ try {
+ this.hww.signingPsbt = true
+ await this.serial.writer.write(COMMAND_SIGN_PSBT + '\n')
+ this.$q.notify({
+ type: 'positive',
+ message: 'PSBT signed!',
+ timeout: 5000
+ })
+ } catch (error) {
+ this.$q.notify({
+ type: 'warning',
+ message: 'Failed to sign PSBT!',
+ caption: `${error}`,
+ timeout: 10000
+ })
+ }
+ },
+ handleSignResponse: function (res = '') {
+ this.hww.signingPsbt = false
+ this.updateSignedPsbt(res)
+ if (this.hww.authenticated) {
+ this.$q.notify({
+ type: 'positive',
+ message: 'Transaction Signed',
+ timeout: 10000
+ })
+ }
+ },
+ hwwHelp: async function () {
+ try {
+ await this.serial.writer.write(COMMAND_HELP + '\n')
+ this.$q.notify({
+ type: 'positive',
+ message: 'Check display or console for details!',
+ timeout: 5000
+ })
+ } catch (error) {
+ this.$q.notify({
+ type: 'warning',
+ message: 'Failed to ask for help!',
+ caption: `${error}`,
+ timeout: 10000
+ })
+ }
+ },
+ hwwWipe: async function () {
+ try {
+ this.hww.showWipeDialog = false
+ await this.serial.writer.write(
+ COMMAND_WIPE + ' ' + this.hww.password + '\n'
+ )
+ } catch (error) {
+ this.$q.notify({
+ type: 'warning',
+ message: 'Failed to ask for help!',
+ caption: `${error}`,
+ timeout: 10000
+ })
+ } finally {
+ this.hww.password = null
+ this.hww.showPassword = false
+ }
+ },
+ handleWipeResponse: function (res = '') {
+ const wiped = res.trim() === '1'
+ console.log('### wiped', wiped)
+ if (wiped) {
+ this.$q.notify({
+ type: 'positive',
+ message: 'Wallet wiped!',
+ timeout: 10000
+ })
+ } else {
+ this.$q.notify({
+ type: 'warning',
+ message: 'Failed to wipe wallet!',
+ caption: `${error}`,
+ timeout: 10000
+ })
+ }
+ },
+ hwwShowSeed: async function () {
+ try {
+ await this.serial.writer.write(COMMAND_SEED + '\n')
+ } catch (error) {
+ this.$q.notify({
+ type: 'warning',
+ message: 'Failed to show seed!',
+ caption: `${error}`,
+ timeout: 10000
+ })
+ }
+ },
+ hwwRestore: async function () {
+ try {
+ await this.serial.writer.write(
+ COMMAND_RESTORE + ' ' + this.hww.mnemonic + '\n'
+ )
+ await this.serial.writer.write(
+ COMMAND_PASSWORD + ' ' + this.hww.password + '\n'
+ )
+ } catch (error) {
+ this.$q.notify({
+ type: 'warning',
+ message: 'Failed to restore from seed!',
+ caption: `${error}`,
+ timeout: 10000
+ })
+ } finally {
+ this.hww.showRestoreDialog = false
+ this.hww.mnemonic = null
+ this.hww.showMnemonic = false
+ this.hww.password = null
+ this.hww.showPassword = false
+ }
+ },
+ //################### UTXOs ###################
+ scanAllAddresses: async function () {
+ await this.refreshAddresses()
+ this.addresses.history = []
+ let addresses = this.addresses.data
+ this.utxos.data = []
+ this.utxos.total = 0
+ // Loop while new funds are found on the gap adresses.
+ // Use 1000 limit as a safety check (scan 20 000 addresses max)
+ for (let i = 0; i < 1000 && addresses.length; i++) {
+ await this.updateUtxosForAddresses(addresses)
+ const oldAddresses = this.addresses.data.slice()
+ await this.refreshAddresses()
+ const newAddresses = this.addresses.data.slice()
+ // check if gap addresses have been extended
+ addresses = newAddresses.filter(
+ newAddr => !oldAddresses.find(oldAddr => oldAddr.id === newAddr.id)
+ )
+ if (addresses.length) {
+ this.$q.notify({
+ type: 'positive',
+ message: 'Funds found! Scanning for more...',
+ timeout: 10000
+ })
+ }
+ }
+ },
+ scanAddressWithAmount: async function () {
+ this.utxos.data = []
+ this.utxos.total = 0
+ this.addresses.history = []
+ const addresses = this.addresses.data.filter(a => a.hasActivity)
+ await this.updateUtxosForAddresses(addresses)
+ },
+ scanAddress: async function (addressData) {
+ this.updateUtxosForAddresses([addressData])
this.$q.notify({
- type: 'warning',
- message: 'Failed to scan addresses',
+ type: 'positive',
+ message: 'Address Rescanned',
timeout: 10000
})
- } finally {
- this.scan.scanning = false
- }
- },
- updateUtxosForAddress: function (addressData, utxos = []) {
- const wallet =
- this.walletAccounts.find(w => w.id === addressData.wallet) || {}
+ },
+ updateUtxosForAddresses: async function (addresses = []) {
+ this.scan = {scanning: true, scanCount: addresses.length, scanIndex: 0}
- const newUtxos = utxos.map(utxo =>
- mapAddressDataToUtxo(wallet, addressData, utxo)
- )
- // remove old utxos
- this.utxos.data = this.utxos.data.filter(
- u => u.address !== addressData.address
- )
- // add new utxos
- this.utxos.data.push(...newUtxos)
- if (utxos.length) {
- this.utxos.data.sort((a, b) => b.sort - a.sort)
- this.utxos.total = this.utxos.data.reduce(
- (total, y) => (total += y?.amount || 0),
+ try {
+ for (addrData of addresses) {
+ const addressHistory = await this.getAddressTxsDelayed(addrData)
+ // remove old entries
+ this.addresses.history = this.addresses.history.filter(
+ h => h.address !== addrData.address
+ )
+
+ // add new entrie
+ this.addresses.history.push(...addressHistory)
+ this.addresses.history.sort((a, b) =>
+ !a.height ? -1 : b.height - a.height
+ )
+ this.markSameTxAddressHistory()
+
+ if (addressHistory.length) {
+ // search only if it ever had any activity
+ const utxos = await this.getAddressTxsUtxoDelayed(
+ addrData.address
+ )
+ this.updateUtxosForAddress(addrData, utxos)
+ }
+
+ this.scan.scanIndex++
+ }
+ } catch (error) {
+ console.error(error)
+ this.$q.notify({
+ type: 'warning',
+ message: 'Failed to scan addresses',
+ timeout: 10000
+ })
+ } finally {
+ this.scan.scanning = false
+ }
+ },
+ updateUtxosForAddress: function (addressData, utxos = []) {
+ const wallet =
+ this.walletAccounts.find(w => w.id === addressData.wallet) || {}
+
+ const newUtxos = utxos.map(utxo =>
+ mapAddressDataToUtxo(wallet, addressData, utxo)
+ )
+ // remove old utxos
+ this.utxos.data = this.utxos.data.filter(
+ u => u.address !== addressData.address
+ )
+ // add new utxos
+ this.utxos.data.push(...newUtxos)
+ if (utxos.length) {
+ this.utxos.data.sort((a, b) => b.sort - a.sort)
+ this.utxos.total = this.utxos.data.reduce(
+ (total, y) => (total += y?.amount || 0),
+ 0
+ )
+ }
+ const addressTotal = utxos.reduce(
+ (total, y) => (total += y?.value || 0),
0
)
- }
- const addressTotal = utxos.reduce(
- (total, y) => (total += y?.value || 0),
- 0
- )
- this.updateAmountForAddress(addressData, addressTotal)
- },
- getTotalSelectedUtxoAmount: function () {
- const total = this.utxos.data
- .filter(u => u.selected)
- .reduce((t, a) => t + (a.amount || 0), 0)
- return total
- },
- applyUtxoSelectionMode: function () {
- const payedAmount = this.getTotalPaymentAmount()
- const mode = this.payment.utxoSelectionMode
- this.utxos.data.forEach(u => (u.selected = false))
- const isManual = mode === 'Manual'
- if (isManual || !payedAmount) return
-
- const isSelectAll = mode === 'Select All'
- if (isSelectAll || payedAmount >= this.utxos.total) {
- this.utxos.data.forEach(u => (u.selected = true))
- return
- }
- const isSmallerFirst = mode === 'Smaller Inputs First'
- const isLargerFirst = mode === 'Larger Inputs First'
-
- let selectedUtxos = this.utxos.data.slice()
- if (isSmallerFirst || isLargerFirst) {
- const sortFn = isSmallerFirst
- ? (a, b) => a.amount - b.amount
- : (a, b) => b.amount - a.amount
- selectedUtxos.sort(sortFn)
- } else {
- // default to random order
- selectedUtxos = _.shuffle(selectedUtxos)
- }
- selectedUtxos.reduce((total, utxo) => {
- utxo.selected = total < payedAmount
- total += utxo.amount
+ this.updateAmountForAddress(addressData, addressTotal)
+ },
+ getTotalSelectedUtxoAmount: function () {
+ const total = this.utxos.data
+ .filter(u => u.selected)
+ .reduce((t, a) => t + (a.amount || 0), 0)
return total
- }, 0)
- },
+ },
+ applyUtxoSelectionMode: function () {
+ const payedAmount = this.getTotalPaymentAmount()
+ const mode = this.payment.utxoSelectionMode
+ this.utxos.data.forEach(u => (u.selected = false))
+ const isManual = mode === 'Manual'
+ if (isManual || !payedAmount) return
- //################### MEMPOOL API ###################
- getAddressTxsDelayed: async function (addrData) {
- const {
- bitcoin: {addresses: addressesAPI}
- } = mempoolJS({
- hostname: new URL(this.config.data.mempool_endpoint).hostname
- })
+ const isSelectAll = mode === 'Select All'
+ if (isSelectAll || payedAmount >= this.utxos.total) {
+ this.utxos.data.forEach(u => (u.selected = true))
+ return
+ }
+ const isSmallerFirst = mode === 'Smaller Inputs First'
+ const isLargerFirst = mode === 'Larger Inputs First'
- const fn = async () =>
- addressesAPI.getAddressTxs({
- address: addrData.address
- })
- const addressTxs = await retryWithDelay(fn)
- return this.addressHistoryFromTxs(addrData, addressTxs)
- },
+ let selectedUtxos = this.utxos.data.slice()
+ if (isSmallerFirst || isLargerFirst) {
+ const sortFn = isSmallerFirst
+ ? (a, b) => a.amount - b.amount
+ : (a, b) => b.amount - a.amount
+ selectedUtxos.sort(sortFn)
+ } else {
+ // default to random order
+ selectedUtxos = _.shuffle(selectedUtxos)
+ }
+ selectedUtxos.reduce((total, utxo) => {
+ utxo.selected = total < payedAmount
+ total += utxo.amount
+ return total
+ }, 0)
+ },
- refreshRecommendedFees: async function () {
- const {
- bitcoin: {fees: feesAPI}
- } = mempoolJS({
- hostname: new URL(this.config.data.mempool_endpoint).hostname
- })
+ //################### MEMPOOL API ###################
+ getAddressTxsDelayed: async function (addrData) {
+ const {
+ bitcoin: {addresses: addressesAPI}
+ } = mempoolJS()
- const fn = async () => feesAPI.getFeesRecommended()
- this.payment.recommededFees = await retryWithDelay(fn)
- },
- getAddressTxsUtxoDelayed: async function (address) {
- const {
- bitcoin: {addresses: addressesAPI}
- } = mempoolJS({
- hostname: new URL(this.config.data.mempool_endpoint).hostname
- })
+ const fn = async () =>
+ addressesAPI.getAddressTxs({
+ address: addrData.address
+ })
+ const addressTxs = await retryWithDelay(fn)
+ return this.addressHistoryFromTxs(addrData, addressTxs)
+ },
- const fn = async () =>
- addressesAPI.getAddressTxsUtxo({
- address
- })
- return retryWithDelay(fn)
- },
- fetchTxHex: async function (txId) {
- const {
- bitcoin: {transactions: transactionsAPI}
- } = mempoolJS({
- hostname: new URL(this.config.data.mempool_endpoint).hostname
- })
+ refreshRecommendedFees: async function () {
+ const {
+ bitcoin: {fees: feesAPI}
+ } = mempoolJS()
- try {
- const response = await transactionsAPI.getTxHex({txid: txId})
- return response
- } catch (error) {
- this.$q.notify({
- type: 'warning',
- message: `Failed to fetch transaction details for tx id: '${txId}'`,
- timeout: 10000
- })
- LNbits.utils.notifyApiError(error)
- throw error
+ const fn = async () => feesAPI.getFeesRecommended()
+ this.payment.recommededFees = await retryWithDelay(fn)
+ },
+ getAddressTxsUtxoDelayed: async function (address) {
+ const {
+ bitcoin: {addresses: addressesAPI}
+ } = mempoolJS()
+
+ const fn = async () =>
+ addressesAPI.getAddressTxsUtxo({
+ address
+ })
+ return retryWithDelay(fn)
+ },
+ fetchTxHex: async function (txId) {
+ const {
+ bitcoin: {transactions: transactionsAPI}
+ } = mempoolJS()
+
+ try {
+ const response = await transactionsAPI.getTxHex({txid: txId})
+ return response
+ } catch (error) {
+ this.$q.notify({
+ type: 'warning',
+ message: `Failed to fetch transaction details for tx id: '${txId}'`,
+ timeout: 10000
+ })
+ LNbits.utils.notifyApiError(error)
+ throw error
+ }
+ },
+
+ //################### OTHER ###################
+ closeFormDialog: function () {
+ this.formDialog.data = {
+ is_unique: false
+ }
+ },
+ openQrCodeDialog: function (addressData) {
+ this.currentAddress = addressData
+ this.addresses.note = addressData.note || ''
+ this.addresses.show = true
+ },
+ searchInTab: function (tab, value) {
+ this.tab = tab
+ this[`${tab}Table`].filter = value
+ },
+
+ satBtc(val, showUnit = true) {
+ const value = this.config.data.sats_denominated
+ ? LNbits.utils.formatSat(val)
+ : val == 0
+ ? 0.0
+ : (val / 100000000).toFixed(8)
+ if (!showUnit) return value
+ return this.config.data.sats_denominated
+ ? value + ' sat'
+ : value + ' BTC'
+ },
+ getAccountDescription: function (accountType) {
+ return getAccountDescription(accountType)
}
},
-
- //################### OTHER ###################
- closeFormDialog: function () {
- this.formDialog.data = {
- is_unique: false
+ created: async function () {
+ if (this.g.user.wallets.length) {
+ await this.getConfig()
+ await this.refreshWalletAccounts()
+ await this.refreshAddresses()
+ await this.scanAddressWithAmount()
}
- },
- openQrCodeDialog: function (addressData) {
- this.currentAddress = addressData
- this.addresses.note = addressData.note || ''
- this.addresses.show = true
- },
- searchInTab: function (tab, value) {
- this.tab = tab
- this[`${tab}Table`].filter = value
- },
-
- satBtc(val, showUnit = true) {
- const value = this.config.data.sats_denominated
- ? LNbits.utils.formatSat(val)
- : val == 0
- ? 0.0
- : (val / 100000000).toFixed(8)
- if (!showUnit) return value
- return this.config.data.sats_denominated ? value + ' sat' : value + ' BTC'
- },
- getAccountDescription: function (accountType) {
- return getAccountDescription(accountType)
}
- },
- created: async function () {
- if (this.g.user.wallets.length) {
- await this.getConfig()
- await this.refreshWalletAccounts()
- await this.refreshAddresses()
- await this.scanAddressWithAmount()
- }
- }
-})
+ })
+}
+watchOnly()
diff --git a/lnbits/extensions/watchonly/static/js/utils.js b/lnbits/extensions/watchonly/static/js/utils.js
index 3a606fe4..a4efc607 100644
--- a/lnbits/extensions/watchonly/static/js/utils.js
+++ b/lnbits/extensions/watchonly/static/js/utils.js
@@ -139,3 +139,23 @@ const readFromSerialPort = serial => {
}
return readStringUntil
}
+
+function loadTemplateAsync(path) {
+ const result = new Promise(resolve => {
+ const xhttp = new XMLHttpRequest()
+ console.log('### 300')
+
+ xhttp.onreadystatechange = function () {
+ if (this.readyState == 4) {
+ if (this.status == 200) resolve(this.responseText)
+
+ if (this.status == 404) resolve('Page not found.')
+ }
+ }
+
+ xhttp.open('GET', path, true)
+ xhttp.send()
+ })
+
+ return result
+}
diff --git a/lnbits/extensions/watchonly/templates/watchonly/index.html b/lnbits/extensions/watchonly/templates/watchonly/index.html
index 8e978043..00d816b2 100644
--- a/lnbits/extensions/watchonly/templates/watchonly/index.html
+++ b/lnbits/extensions/watchonly/templates/watchonly/index.html
@@ -31,7 +31,9 @@
-
+
+
+
+
{% endblock %}