feat: login and send commands to HWW

This commit is contained in:
Vlad Stan 2022-07-20 16:57:32 +03:00
parent b7c4a411b1
commit a0d56a7e06
4 changed files with 336 additions and 98 deletions

View file

@ -44,6 +44,13 @@ new Vue({
config: {} config: {}
}, },
hww: {
password: null,
authenticated: false,
showPasswordDialog: false,
showConsole: false
},
formDialog: { formDialog: {
show: false, show: false,
data: {} data: {}
@ -512,6 +519,67 @@ new Vue({
LNbits.utils.notifyApiError(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 () {
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 () { checkSerialPortSupported: function () {
if (!navigator.serial) { if (!navigator.serial) {
this.$q.notify({ 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 () { startSerialPortReading: async function () {
const port = this.serial.selectedPort const port = this.serial.selectedPort
@ -620,8 +671,7 @@ new Vue({
const {value, done} = await readStringUntil('\n') const {value, done} = await readStringUntil('\n')
console.log('### value', value) console.log('### value', value)
if (value) { if (value) {
const isPsbt = value.startsWith(PSBT_BASE64_PREFIX) this.handleSerialPortResponse(value)
if (isPsbt) this.updateSignedPsbt(value)
this.updateSerialPortConsole(value) this.updateSerialPortConsole(value)
} }
console.log('### startSerialPortReading DONE', done) console.log('### startSerialPortReading DONE', done)
@ -638,43 +688,14 @@ new Vue({
} }
console.log('### startSerialPortReading port', port) console.log('### startSerialPortReading port', port)
}, },
extractTxFromPsbt: async function (psbtBase64) {
const wallet = this.g.user.wallets[0] handleSerialPortResponse: function (value) {
try { const msg = value.split(' ')
const {data} = await LNbits.api.request( if (msg[0] == COMMAND_SIGN_PSBT) this.handleSignResponse(msg[1])
'PUT', else if (msg[0] == COMMAND_PASSWORD) this.handleLoginResponse(msg[1])
'/watchonly/api/v1/psbt/extract', else if (msg[0] == COMMAND_PASSWORD_CLEAR)
wallet.adminkey, this.handleLogoutResponse(msg[1])
{ else console.log('### handleSerialPortResponse', value)
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
}
}, },
updateSerialPortConsole: function (value) { updateSerialPortConsole: function (value) {
this.serial.receivedData += value + '\n' this.serial.receivedData += value + '\n'
@ -686,34 +707,115 @@ new Vue({
sharePsbtWithAnimatedQRCode: async function () { sharePsbtWithAnimatedQRCode: async function () {
console.log('### sharePsbtWithAnimatedQRCode') console.log('### sharePsbtWithAnimatedQRCode')
}, },
//################### HARDWARE WALLET ###################
broadcastTransaction: async function () { hwwLogin: async function () {
console.log('### broadcastTransaction', this.payment.signedTxHex)
try { try {
const wallet = this.g.user.wallets[0] await this.serial.writer.write(
const {data} = await LNbits.api.request( COMMAND_PASSWORD + ' ' + this.hww.password + '\n'
'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) { } catch (error) {
this.$q.notify({ this.$q.notify({
type: 'warning', 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}`, caption: `${error}`,
timeout: 10000 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 ################### //################### UTXOs ###################
scanAllAddresses: async function () { scanAllAddresses: async function () {
await this.refreshAddresses() await this.refreshAddresses()

View file

@ -259,6 +259,7 @@ const tableData = {
}, },
fee: 0, fee: 0,
txSize: 0, txSize: 0,
tx: null,
psbtBase64: '', psbtBase64: '',
psbtBase64Signed: '', psbtBase64Signed: '',
signedTx: null, signedTx: null,

View file

@ -1,4 +1,8 @@
const PSBT_BASE64_PREFIX = 'cHNidP8' 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 => const blockTimeToDate = blockTime =>
blockTime ? moment(blockTime * 1000).format('LLL') : '' blockTime ? moment(blockTime * 1000).format('LLL') : ''
@ -105,8 +109,7 @@ const readFromSerialPort = serial => {
let fulliness = [] let fulliness = []
const readStringUntil = async (separator = '\n') => { const readStringUntil = async (separator = '\n') => {
console.log('### fulliness', fulliness) if (fulliness.length) return fulliness.shift().trim()
if (fulliness.length) return fulliness.shift()
const chunks = [] const chunks = []
if (partialChunk) { if (partialChunk) {
// leftovers from previous read // leftovers from previous read
@ -115,7 +118,7 @@ const readFromSerialPort = serial => {
} }
while (true) { while (true) {
const {value, done} = await serial.reader.read() const {value, done} = await serial.reader.read()
console.log('### value 1', value) console.log('### serial read', value)
if (value) { if (value) {
const values = value.split(separator) const values = value.split(separator)
// found one or more separators // found one or more separators
@ -123,11 +126,11 @@ const readFromSerialPort = serial => {
chunks.push(values.shift()) // first element chunks.push(values.shift()) // first element
partialChunk = values.pop() // last element partialChunk = values.pop() // last element
fulliness = values // full lines fulliness = values // full lines
return {value: chunks.join(''), done: false} return {value: chunks.join('').trim(), done: false}
} }
chunks.push(value) chunks.push(value)
} }
if (done) return {value: chunks.join(''), done: true} if (done) return {value: chunks.join('').trim(), done: true}
} }
} }
return readStringUntil return readStringUntil

View file

@ -1015,13 +1015,14 @@
<div class="row items-center no-wrap q-mb-md"> <div class="row items-center no-wrap q-mb-md">
<div class="col-3 q-pr-lg"> <div class="col-3 q-pr-lg">
<q-btn unelevated color="secondary" type="submit" <q-btn unelevated color="secondary" type="submit"
>Create PSBT</q-btn >Check Transaction</q-btn
> >
</div> </div>
<div class="col-9"> <div class="col-9">
<q-input v-model="payment.psbtBase64" filled /> <q-input v-model="payment.psbtBase64" filled />
</div> </div>
</div> </div>
<q-separator v-if="payment.psbtBase64"></q-separator>
<div <div
v-if="payment.psbtBase64" v-if="payment.psbtBase64"
class="row items-center no-wrap q-mb-md" class="row items-center no-wrap q-mb-md"
@ -1037,12 +1038,11 @@
></q-option-group> ></q-option-group>
</div> </div>
</div> </div>
<q-separator></q-separator> <q-separator v-if="payment.psbtBase64 && payment.signMode === 'serial-port'"></q-separator>
<div <div
v-if="payment.psbtBase64 && payment.signMode === 'serial-port'" v-if="payment.psbtBase64 && payment.signMode === 'serial-port'"
class="row items-center no-wrap q-mb-md q-mt-lg" class="row items-center no-wrap q-mb-md q-mt-lg"
> >
<!-- <div class="col-3"></div> -->
<div class="col-3"> <div class="col-3">
<q-btn <q-btn
v-if="!serial.selectedPort" v-if="!serial.selectedPort"
@ -1059,29 +1059,118 @@
>Disconnect</q-btn >Disconnect</q-btn
> >
</div> </div>
<div class="col-6"> <div class="col-3">
<q-toggle <q-toggle
label="Advanced" label="Advanced"
disabled
color="secodary float-left" color="secodary float-left"
class="q-ml-lg" class="q-ml-lg"
v-model="serial.showAdvancedConfig" v-model="serial.showAdvancedConfig"
></q-toggle> ></q-toggle>
</div> </div>
<div class="col-6"></div>
</div>
<q-separator v-if="payment.psbtBase64 && payment.signMode === 'serial-port'"></q-separator>
<div
v-if="payment.psbtBase64 && payment.signMode === 'serial-port'"
class="row items-center no-wrap q-mb-md q-mt-lg"
>
<div class="col-3"> <div class="col-3">
<q-btn <q-btn-dropdown
v-if="serial.selectedPort" v-if="serial.selectedPort"
@click="sendPsbtToSerialPort()" split
unelevated class="glossy float-left"
color="secondary float-right" color="secondary"
>Send PSBT to Device</q-btn :label="hww.authenticated ? 'Logout' : 'Login'"
@click="hwwToggleAuth()"
> >
<q-list>
<q-item
v-if="!hww.authenticated"
clickable
v-close-popup
@click="hww.showPasswordDialog = true"
>
<q-item-section>
<q-item-label>Login</q-item-label>
<q-item-label caption
>Enter password for HWW.</q-item-label
>
</q-item-section>
</q-item>
<q-item
v-if="hww.authenticated"
clickable
v-close-popup
@click="hwwLogout()"
>
<q-item-section>
<q-item-label>Logout</q-item-label>
<q-item-label caption
>Clear password for HWW.</q-item-label
>
</q-item-section>
</q-item>
<q-item
:disabled="!hww.authenticated"
clickable
v-close-popup
@click="hww.showPasswordDialog = true"
>
<q-item-section>
<q-item-label>Sign</q-item-label>
<q-item-label caption
>Sign PSBT on Hardware Wallet.</q-item-label
>
</q-item-section>
</q-item>
<q-item clickable v-close-popup>
<q-item-section>
<q-item-label>Restore</q-item-label>
<q-item-label caption
>Restore wallet from existing word
list.</q-item-label
>
</q-item-section>
</q-item>
<q-item clickable v-close-popup>
<q-item-section>
<q-item-label>Wipe</q-item-label>
<q-item-label caption
>Clean-up the wallet. New random
seed.</q-item-label
>
</q-item-section>
</q-item>
<q-item clickable v-close-popup>
<q-item-section>
<q-item-label>Help</q-item-label>
<q-item-label caption
>View available comands.</q-item-label
>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</div> </div>
<div class="col-3">
<q-toggle
v-if="serial.selectedPort"
label="Show Console"
color="secodary float-left"
class="q-ml-lg"
v-model="hww.showConsole"
></q-toggle>
</div>
<div class="col-3"></div>
<div class="col-3"></div>
</div> </div>
<div <div
v-if="serial.showAdvancedConfig" v-if="hww.showConsole"
class="row items-center no-wrap q-mb-md" class="row items-center no-wrap q-mb-md"
> >
<div class="col-3 q-pr-lg">Message from device</div> <div class="col-3 q-pr-lg"></div>
<div class="col-9"> <div class="col-9">
<q-input <q-input
for="watchonly-serial-port-data-input" for="watchonly-serial-port-data-input"
@ -1091,19 +1180,34 @@
/> />
</div> </div>
</div> </div>
<q-separator v-if="hww.authenticated"></q-separator>
<div <div
v-if="payment.psbtBase64Signed" v-if="hww.authenticated"
class="row items-center no-wrap q-mb-md" class="row items-center no-wrap q-mb-md"
> >
<div class="col-3 q-pr-lg">PSBT from device</div> <div class="col-3 q-pr-lg">
<div class="col-9"> <q-btn @click="hwwSendPsbt()" unelevated color="secondary"
<q-input >Sign</q-btn
v-model="payment.psbtBase64Signed" >
filled </div>
readonly
/> <div class="col-6">
<q-badge color="blue"
>Please check transaction data on the Hardware Wallet
Display</q-badge
>
</div>
<div class="col-3">
<q-btn
@click="hwwSignPsbt()"
unelevated
color="green"
class="float-right text-subtitle1"
>Confirm</q-btn
>
</div> </div>
</div> </div>
<q-separator v-if="payment.signedTx"></q-separator>
<div <div
v-if="payment.signedTx" v-if="payment.signedTx"
class="row items-center no-wrap q-mb-md" class="row items-center no-wrap q-mb-md"
@ -1338,6 +1442,34 @@
<div class="row q-mt-lg q-gutter-sm"></div> <div class="row q-mt-lg q-gutter-sm"></div>
</q-card> </q-card>
</q-dialog> </q-dialog>
<q-dialog v-model="hww.showPasswordDialog" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="hwwLogin" class="q-gutter-md">
<span>Enter password for Hardware Wallet</span>
<q-input
filled
dense
v-model.trim="hww.password"
type="password"
label="Password"
></q-input>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="!serial.selectedPort"
type="submit"
>Login</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
{% endraw %} {% endraw %}
</div> </div>