From a0d56a7e066774d17c7b5d543bed4fc941bc8231 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 20 Jul 2022 16:57:32 +0300 Subject: [PATCH] feat: login and send commands to HWW --- .../extensions/watchonly/static/js/index.js | 250 ++++++++++++------ .../extensions/watchonly/static/js/tables.js | 1 + .../extensions/watchonly/static/js/utils.js | 13 +- .../watchonly/templates/watchonly/index.html | 170 ++++++++++-- 4 files changed, 336 insertions(+), 98 deletions(-) diff --git a/lnbits/extensions/watchonly/static/js/index.js b/lnbits/extensions/watchonly/static/js/index.js index f55fc23a..98f19971 100644 --- a/lnbits/extensions/watchonly/static/js/index.js +++ b/lnbits/extensions/watchonly/static/js/index.js @@ -44,6 +44,13 @@ new Vue({ config: {} }, + hww: { + password: null, + authenticated: false, + showPasswordDialog: false, + showConsole: false + }, + formDialog: { show: false, data: {} @@ -512,6 +519,67 @@ new Vue({ 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 () { + console.log('### broadcastTransaction', this.payment.signedTxHex) + + 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.$q.notify({ + type: 'positive', + message: 'Transaction broadcasted!', + caption: `${data}`, + timeout: 10000 + }) + } catch (error) { + this.$q.notify({ + type: 'warning', + message: 'Failed to broadcast!', + caption: `${error}`, + timeout: 10000 + }) + } + }, + //################### SERIAL PORT ################### checkSerialPortSupported: function () { if (!navigator.serial) { this.$q.notify({ @@ -586,23 +654,6 @@ new Vue({ }) } }, - sendPsbtToSerialPort: async function () { - try { - await this.serial.writer.write(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 - }) - } - }, startSerialPortReading: async function () { const port = this.serial.selectedPort @@ -620,8 +671,7 @@ new Vue({ const {value, done} = await readStringUntil('\n') console.log('### value', value) if (value) { - const isPsbt = value.startsWith(PSBT_BASE64_PREFIX) - if (isPsbt) this.updateSignedPsbt(value) + this.handleSerialPortResponse(value) this.updateSerialPortConsole(value) } console.log('### startSerialPortReading DONE', done) @@ -638,43 +688,14 @@ new Vue({ } console.log('### startSerialPortReading port', port) }, - 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 - this.$q.notify({ - type: 'positive', - message: 'PSBT received from serial port device!', - timeout: 10000 - }) - 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 - } + + 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 console.log('### handleSerialPortResponse', value) }, updateSerialPortConsole: function (value) { this.serial.receivedData += value + '\n' @@ -686,34 +707,115 @@ new Vue({ sharePsbtWithAnimatedQRCode: async function () { console.log('### sharePsbtWithAnimatedQRCode') }, - - broadcastTransaction: async function () { - console.log('### broadcastTransaction', this.payment.signedTxHex) - + //################### HARDWARE WALLET ################### + hwwLogin: 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} + await this.serial.writer.write( + COMMAND_PASSWORD + ' ' + this.hww.password + '\n' ) - this.$q.notify({ - type: 'positive', - message: 'Transaction broadcasted!', - caption: `${data}`, - timeout: 10000 - }) } catch (error) { this.$q.notify({ type: 'warning', - message: 'Failed to broadcast!', + message: 'Failed to send password to hardware wallet!', + caption: `${error}`, + timeout: 10000 + }) + } finally { + this.hww.showPasswordDialog = false + this.hww.password = null + } + }, + 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 + }) + } + }, + hwwToggleAuth: function () { + if (this.hww.authenticated) { + this.hwwLogout() + } else { + this.hww.showPasswordDialog = true + } + }, + hwwSendPsbt: async function () { + try { + 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 + }) + } + }, + hwwSignPsbt: async function () { + try { + 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.updateSignedPsbt(res) + if (this.hww.authenticated) { + this.$q.notify({ + type: 'positive', + message: 'Transaction Signed', + timeout: 10000 + }) + } + }, //################### UTXOs ################### scanAllAddresses: async function () { await this.refreshAddresses() diff --git a/lnbits/extensions/watchonly/static/js/tables.js b/lnbits/extensions/watchonly/static/js/tables.js index 616b7060..e03a2c1a 100644 --- a/lnbits/extensions/watchonly/static/js/tables.js +++ b/lnbits/extensions/watchonly/static/js/tables.js @@ -259,6 +259,7 @@ const tableData = { }, fee: 0, txSize: 0, + tx: null, psbtBase64: '', psbtBase64Signed: '', signedTx: null, diff --git a/lnbits/extensions/watchonly/static/js/utils.js b/lnbits/extensions/watchonly/static/js/utils.js index c5ddb9c2..58803acc 100644 --- a/lnbits/extensions/watchonly/static/js/utils.js +++ b/lnbits/extensions/watchonly/static/js/utils.js @@ -1,4 +1,8 @@ const PSBT_BASE64_PREFIX = 'cHNidP8' +const COMMAND_PASSWORD = '/password' +const COMMAND_PASSWORD_CLEAR = '/password-clear' +const COMMAND_SEND_PSBT = '/psbt' +const COMMAND_SIGN_PSBT = '/sign' const blockTimeToDate = blockTime => blockTime ? moment(blockTime * 1000).format('LLL') : '' @@ -105,8 +109,7 @@ const readFromSerialPort = serial => { let fulliness = [] const readStringUntil = async (separator = '\n') => { - console.log('### fulliness', fulliness) - if (fulliness.length) return fulliness.shift() + if (fulliness.length) return fulliness.shift().trim() const chunks = [] if (partialChunk) { // leftovers from previous read @@ -115,7 +118,7 @@ const readFromSerialPort = serial => { } while (true) { const {value, done} = await serial.reader.read() - console.log('### value 1', value) + console.log('### serial read', value) if (value) { const values = value.split(separator) // found one or more separators @@ -123,11 +126,11 @@ const readFromSerialPort = serial => { chunks.push(values.shift()) // first element partialChunk = values.pop() // last element fulliness = values // full lines - return {value: chunks.join(''), done: false} + return {value: chunks.join('').trim(), done: false} } chunks.push(value) } - if (done) return {value: chunks.join(''), done: true} + if (done) return {value: chunks.join('').trim(), done: true} } } return readStringUntil diff --git a/lnbits/extensions/watchonly/templates/watchonly/index.html b/lnbits/extensions/watchonly/templates/watchonly/index.html index ab4872ea..ea57da9e 100644 --- a/lnbits/extensions/watchonly/templates/watchonly/index.html +++ b/lnbits/extensions/watchonly/templates/watchonly/index.html @@ -1015,13 +1015,14 @@
Create PSBTCheck Transaction
+
- +
-
Disconnect
-
+
+
+
+ +
- Send PSBT to Device + + + + Login + Enter password for HWW. + + + + + + Logout + Clear password for HWW. + + + + + Sign + Sign PSBT on Hardware Wallet. + + + + + Restore + Restore wallet from existing word + list. + + + + + Wipe + Clean-up the wallet. New random + seed. + + + + + Help + View available comands. + + + +
+
+ +
+
+
-
Message from device
+
+
-
PSBT from device
-
- +
+ Sign +
+ +
+ Please check transaction data on the Hardware Wallet + Display +
+
+ Confirm
+
+ + + + + Enter password for Hardware Wallet + + +
+ Login + Cancel +
+
+
+
{% endraw %}