Merge pull request #988 from motorina0/watchonly_improvements

Watchonly improvements
This commit is contained in:
Arc 2022-09-21 10:42:54 +01:00 committed by GitHub
commit 3fe33a0be8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 2494 additions and 77 deletions

View file

@ -23,9 +23,10 @@ async def create_watch_wallet(w: WalletAccount) -> WalletAccount:
type, type,
address_no, address_no,
balance, balance,
network network,
meta
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (
wallet_id, wallet_id,
@ -37,6 +38,7 @@ async def create_watch_wallet(w: WalletAccount) -> WalletAccount:
w.address_no, w.address_no,
w.balance, w.balance,
w.network, w.network,
w.meta,
), ),
) )

View file

@ -93,3 +93,10 @@ async def m006_drop_mempool_table(db):
Mempool data is now part of `config` Mempool data is now part of `config`
""" """
await db.execute("DROP TABLE watchonly.mempool;") await db.execute("DROP TABLE watchonly.mempool;")
async def m007_add_wallet_meta_data(db):
"""
Add 'meta' for storing various metadata about the wallet
"""
await db.execute("ALTER TABLE watchonly.wallets ADD COLUMN meta TEXT DEFAULT '{}';")

View file

@ -9,6 +9,7 @@ class CreateWallet(BaseModel):
masterpub: str = Query("") masterpub: str = Query("")
title: str = Query("") title: str = Query("")
network: str = "Mainnet" network: str = "Mainnet"
meta: str = "{}"
class WalletAccount(BaseModel): class WalletAccount(BaseModel):
@ -21,6 +22,7 @@ class WalletAccount(BaseModel):
balance: int balance: int
type: Optional[str] = "" type: Optional[str] = ""
network: str = "Mainnet" network: str = "Mainnet"
meta: str = "{}"
@classmethod @classmethod
def from_row(cls, row: Row) -> "WalletAccount": def from_row(cls, row: Row) -> "WalletAccount":

View file

@ -104,6 +104,18 @@ async function payment(path) {
}) })
return return
} }
const p2trUtxo = this.utxos.find(
u => u.selected && u.accountType === 'p2tr'
)
if (p2trUtxo) {
this.$q.notify({
type: 'warning',
message: 'Taproot Signing not supported yet!',
caption: 'Please manually deselect the Taproot UTXOs',
timeout: 10000
})
return
}
if (!this.serialSignerRef.isAuthenticated()) { if (!this.serialSignerRef.isAuthenticated()) {
await this.serialSignerRef.hwwShowPasswordDialog() await this.serialSignerRef.hwwShowPasswordDialog()
const authenticated = await this.serialSignerRef.isAuthenticating() const authenticated = await this.serialSignerRef.isAuthenticating()

View file

@ -0,0 +1,80 @@
<div>
<div v-if="done">
<div class="row">
<div class="col-12">Seed Input Done</div>
</div>
</div>
<div v-else>
<div class="row">
<div class="col-3 q-pt-sm">Word Count</div>
<div class="col-6 q-pr-lg">
<q-select
filled
dense
v-model="wordCount"
type="number"
label="Word Count"
:options="wordCountOptions"
@input="initWords"
></q-select>
</div>
<div class="col-3 q-pr-lg"></div>
</div>
<div class="row">
<div class="col-3 q-pr-lg"></div>
<div class="col-6">Enter word at position: {{actualPosition}}</div>
<div class="col-3 q-pr-lg"></div>
</div>
<div class="row">
<div class="col-3 q-pr-lg">
<q-btn
v-if="currentPosition > 0"
@click="previousPosition"
unelevated
class="btn-full"
color="secondary"
>Previous</q-btn
>
</div>
<div class="col-6 q-pr-lg">
<q-select
filled
dense
use-input
hide-selected
fill-input
input-debounce="0"
v-model="currentWord"
:options="options"
@filter="filterFn"
@input-value="setModel"
></q-select>
</div>
<div class="col-3 q-pr-lg">
<q-btn
v-if="currentPosition < wordCount - 1"
@click="nextPosition"
unelevated
class="btn-full"
color="secondary"
>Next</q-btn
>
<q-btn
v-else
@click="seedInputDone"
unelevated
class="btn-full"
color="primary"
>Done</q-btn
>
</div>
<q-linear-progress
:value="currentPosition / (wordCount -1)"
size="5px"
color="primary"
class="q-mt-sm"
></q-linear-progress>
</div>
</div>
</div>

View file

@ -0,0 +1,102 @@
async function seedInput(path) {
const template = await loadTemplateAsync(path)
Vue.component('seed-input', {
name: 'seed-input',
template,
computed: {
actualPosition: function () {
return this.words[this.currentPosition].position
}
},
data: function () {
return {
wordCountOptions: ['12', '15', '18', '21', '24'],
wordCount: 24,
words: [],
currentPosition: 0,
stringOptions: [],
options: [],
currentWord: '',
done: false
}
},
methods: {
filterFn(val, update, abort) {
update(() => {
const needle = val.toLocaleLowerCase()
this.options = this.stringOptions
.filter(v => v.toLocaleLowerCase().indexOf(needle) != -1)
.sort((a, b) => {
if (a.startsWith(needle)) {
if (b.startsWith(needle)) {
return a - b
}
return -1
} else {
if (b.startsWith(needle)) {
return 1
}
return a - b
}
})
})
},
initWords() {
const words = []
for (let i = 1; i <= this.wordCount; i++) {
words.push({
position: i,
value: ''
})
}
this.currentPosition = 0
this.words = _.shuffle(words)
},
setModel(val) {
this.currentWord = val
this.words[this.currentPosition].value = this.currentWord
},
nextPosition() {
if (this.currentPosition < this.wordCount - 1) {
this.currentPosition++
}
this.currentWord = this.words[this.currentPosition].value
},
previousPosition() {
if (this.currentPosition > 0) {
this.currentPosition--
}
this.currentWord = this.words[this.currentPosition].value
},
seedInputDone() {
const badWordPositions = this.words
.filter(w => !w.value || !this.stringOptions.includes(w.value))
.map(w => w.position)
if (badWordPositions.length) {
this.$q.notify({
timeout: 10000,
type: 'warning',
message:
'The seed has incorrect words. Please check at these positions: ',
caption: 'Position: ' + badWordPositions.join(', ')
})
return
}
const mnemonic = this.words
.sort((a, b) => a.position - b.position)
.map(w => w.value)
.join(' ')
this.$emit('on-seed-input-done', mnemonic)
this.done = true
}
},
created: async function () {
this.stringOptions = bip39WordList
this.initWords()
}
})
}

View file

@ -170,6 +170,31 @@
type="password" type="password"
label="Password" label="Password"
></q-input> ></q-input>
<q-separator></q-separator>
<q-toggle
label="Passphrase (optional)"
color="secodary"
v-model="hww.hasPassphrase"
></q-toggle>
<q-input
v-if="hww.hasPassphrase"
v-model.trim="hww.passphrase"
filled
:type="hww.showPassphrase ? 'text' : 'password'"
filled
dense
label="Passphrase"
>
<template v-slot:append>
<q-icon
:name="hww.showPassphrase ? 'visibility' : 'visibility_off'"
class="cursor-pointer"
@click="hww.showPassphrase = !hww.showPassphrase"
/>
</template>
</q-input>
<br />
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<q-btn <q-btn
@ -351,6 +376,18 @@
<q-dialog v-model="showConsole" position="top"> <q-dialog v-model="showConsole" position="top">
<q-card class="q-pa-lg q-pt-xl"> <q-card class="q-pa-lg q-pt-xl">
<div class="row q-mt-lg q-mb-lg">
<div class="col">
<q-badge
class="text-subtitle2 float-right"
color="yellow"
text-color="black"
>
Open the browser Developer Console for more Details!
</q-badge>
</div>
</div>
<q-input <q-input
filled filled
dense dense
@ -361,16 +398,29 @@
cols="200" cols="200"
label="Console" label="Console"
></q-input> ></q-input>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn> <q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div> </div>
</q-card> </q-card>
</q-dialog> </q-dialog>
<q-dialog v-model="hww.showSeedDialog" position="top"> <q-dialog v-model="hww.showSeedDialog" @hide="closeSeedDialog" position="top">
<q-card class="q-pa-lg q-pt-xl"> <q-card class="q-pa-lg q-pt-xl">
<span>Check word at position {{hww.seedWordPosition}} on display</span> <span>Check word at position {{hww.seedWordPosition}} on device</span>
<div class="row q-mt-lg">
<div class="col-12">
<q-toggle
label="Show Seed Word"
color="secodary"
v-model="hww.showSeedWord"
></q-toggle>
</div>
</div>
<div v-if="hww.showSeedWord" class="row q-mt-lg">
<div class="col-12">
<q-input readonly v-model.trim="hww.seedWord"></q-input>
</div>
</div>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<div class="col-4"> <div class="col-4">
@ -409,48 +459,35 @@
> >
For test purposes only. Do not enter word list with real funds!!! For test purposes only. Do not enter word list with real funds!!!
</q-badge> </q-badge>
<br /><br /><br />
<span>Enter new word list separated by space</span>
<q-input
v-model.trim="hww.mnemonic"
filled
:type="hww.showMnemonic ? 'text' : 'password'"
filled
dense
label="Word List"
>
<template v-slot:append>
<q-icon
:name="hww.showMnemonic ? 'visibility' : 'visibility_off'"
class="cursor-pointer"
@click="hww.showMnemonic = !hww.showMnemonic"
/>
</template>
</q-input>
<br /> <br />
<q-toggle <q-toggle
label="Passphrase (optional)" label="Enter word list separated by space"
color="secodary" color="secodary"
v-model="hww.hasPassphrase" v-model="hww.quickMnemonicInput"
></q-toggle> ></q-toggle>
<br /> <br />
<q-input
v-if="hww.hasPassphrase" <div v-if="hww.quickMnemonicInput">
v-model.trim="hww.passphrase" <q-input
filled v-model.trim="hww.mnemonic"
:type="hww.showPassphrase ? 'text' : 'password'" filled
filled :type="hww.showMnemonic ? 'text' : 'password'"
dense filled
label="Passphrase" dense
> label="Word List"
<template v-slot:append> >
<q-icon <template v-slot:append>
:name="hww.showPassphrase ? 'visibility' : 'visibility_off'" <q-icon
class="cursor-pointer" :name="hww.showMnemonic ? 'visibility' : 'visibility_off'"
@click="hww.showPassphrase = !hww.showPassphrase" class="cursor-pointer"
/> @click="hww.showMnemonic = !hww.showMnemonic"
</template> />
</q-input> </template>
</q-input>
</div>
<seed-input v-else @on-seed-input-done="seedInputDone"></seed-input>
<br />
<q-separator></q-separator> <q-separator></q-separator>
<br /> <br />
<span>Enter new password (8 numbers/letters)</span> <span>Enter new password (8 numbers/letters)</span>

View file

@ -22,6 +22,7 @@ async function serialSigner(path) {
showPassword: false, showPassword: false,
mnemonic: null, mnemonic: null,
showMnemonic: false, showMnemonic: false,
quickMnemonicInput: false,
passphrase: null, passphrase: null,
showPassphrase: false, showPassphrase: false,
hasPassphrase: false, hasPassphrase: false,
@ -38,6 +39,8 @@ async function serialSigner(path) {
psbtSentResolve: null, psbtSentResolve: null,
xpubResolve: null, xpubResolve: null,
seedWordPosition: 1, seedWordPosition: 1,
seedWord: null,
showSeedWord: false,
showSeedDialog: false, showSeedDialog: false,
// config: null, // config: null,
@ -172,6 +175,10 @@ async function serialSigner(path) {
isAuthenticated: function () { isAuthenticated: function () {
return this.hww.authenticated return this.hww.authenticated
}, },
seedInputDone: function (mnemonic) {
this.hww.mnemonic = mnemonic
},
isAuthenticating: function () { isAuthenticating: function () {
if (this.isAuthenticated()) return false if (this.isAuthenticated()) return false
return new Promise(resolve => { return new Promise(resolve => {
@ -374,6 +381,10 @@ async function serialSigner(path) {
}) })
} }
}, },
closeSeedDialog: function () {
this.hww.seedWord = null
this.hww.showSeedWord = false
},
hwwConfirmNext: async function () { hwwConfirmNext: async function () {
this.hww.confirm.outputIndex += 1 this.hww.confirm.outputIndex += 1
if (this.hww.confirm.outputIndex >= this.tx.outputs.length) { if (this.hww.confirm.outputIndex >= this.tx.outputs.length) {
@ -403,7 +414,10 @@ async function serialSigner(path) {
}, },
hwwLogin: async function () { hwwLogin: async function () {
try { try {
await this.sendCommandSecure(COMMAND_PASSWORD, [this.hww.password]) await this.sendCommandSecure(COMMAND_PASSWORD, [
this.hww.password,
this.hww.passphrase
])
} catch (error) { } catch (error) {
this.$q.notify({ this.$q.notify({
type: 'warning', type: 'warning',
@ -414,7 +428,9 @@ async function serialSigner(path) {
} finally { } finally {
this.hww.showPasswordDialog = false this.hww.showPasswordDialog = false
this.hww.password = null this.hww.password = null
this.hww.passphrase = null
this.hww.showPassword = false this.hww.showPassword = false
this.hww.showPassphrase = false
} }
}, },
handleLoginResponse: function (res = '') { handleLoginResponse: function (res = '') {
@ -449,6 +465,22 @@ async function serialSigner(path) {
}) })
} }
}, },
hwwShowAddress: async function (path, address) {
try {
await this.sendCommandSecure(COMMAND_ADDRESS, [
this.network,
path,
address
])
} catch (error) {
this.$q.notify({
type: 'warning',
message: 'Failed to logout from Hardware Wallet!',
caption: `${error}`,
timeout: 10000
})
}
},
handleLogoutResponse: function (res = '') { handleLogoutResponse: function (res = '') {
const authenticated = !(res.trim() === '1') const authenticated = !(res.trim() === '1')
if (this.hww.authenticated && !authenticated) { if (this.hww.authenticated && !authenticated) {
@ -796,21 +828,15 @@ async function serialSigner(path) {
await this.sendCommandSecure(COMMAND_SEED, [this.hww.seedWordPosition]) await this.sendCommandSecure(COMMAND_SEED, [this.hww.seedWordPosition])
}, },
handleShowSeedResponse: function (res = '') { handleShowSeedResponse: function (res = '') {
const args = res.trim().split(' ') const [pos, word] = res.trim().split(' ')
this.hww.seedWord = `${pos}. ${word}`
this.hww.seedWordPosition = pos
}, },
hwwRestore: async function () { hwwRestore: async function () {
try { try {
let mnemonicWithPassphrase = this.hww.mnemonic
if (
this.hww.hasPassphrase &&
this.hww.passphrase &&
this.hww.passphrase.length
) {
mnemonicWithPassphrase += '/' + this.hww.passphrase
}
await this.sendCommandSecure(COMMAND_RESTORE, [ await this.sendCommandSecure(COMMAND_RESTORE, [
this.hww.password, this.hww.password,
mnemonicWithPassphrase this.hww.mnemonic
]) ])
} catch (error) { } catch (error) {
this.$q.notify({ this.$q.notify({
@ -822,7 +848,6 @@ async function serialSigner(path) {
} finally { } finally {
this.hww.showRestoreDialog = false this.hww.showRestoreDialog = false
this.hww.mnemonic = null this.hww.mnemonic = null
this.hww.passphrase = null
this.hww.showMnemonic = false this.hww.showMnemonic = false
this.hww.password = null this.hww.password = null
this.hww.confirmedPassword = null this.hww.confirmedPassword = null

View file

@ -97,6 +97,13 @@
<q-badge v-if="props.row.isChange" color="orange" class="q-mr-md"> <q-badge v-if="props.row.isChange" color="orange" class="q-mr-md">
change change
</q-badge> </q-badge>
<q-badge
v-if="props.row.accountType === 'p2tr'"
color="yellow"
text-color="black"
>
taproot
</q-badge>
</div> </div>
</q-td> </q-td>

View file

@ -116,6 +116,7 @@
>New Receive Address</q-btn >New Receive Address</q-btn
> >
</div> </div>
<div class="col-4"> <div class="col-4">
{{getAccountDescription(props.row.type)}} {{getAccountDescription(props.row.type)}}
</div> </div>
@ -124,15 +125,56 @@
<div class="row items-center no-wrap q-mb-md"> <div class="row items-center no-wrap q-mb-md">
<div class="col-2 q-pr-lg">Master Pubkey:</div> <div class="col-2 q-pr-lg">Master Pubkey:</div>
<div class="col-8"> <div class="col-7 q-pr-lg">
<q-input <q-input v-model="props.row.masterpub" filled readonly />
v-model="props.row.masterpub" </div>
filled <div class="col-1">
readonly <q-btn
type="textarea" unelevated
/> dense
size="md"
icon="qr_code"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openQrCodeDialog(props.row.masterpub)"
></q-btn>
</div>
<div class="col-2 q-pr-lg">
<q-btn
outline
color="grey"
icon="content_copy"
@click="copyText(props.row.masterpub)"
class="q-ml-sm"
></q-btn>
</div>
</div>
<div
v-if="props.row.meta?.xpub"
class="row items-center no-wrap q-mb-md"
>
<div class="col-2 q-pr-lg">XPub:</div>
<div class="col-7 q-pr-lg">
<q-input v-model="props.row.meta.xpub" filled readonly />
</div>
<div class="col-1">
<q-btn
unelevated
dense
size="md"
icon="qr_code"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openQrCodeDialog(props.row.meta.xpub)"
></q-btn>
</div>
<div class="col-2 q-pr-lg">
<q-btn
outline
color="grey"
icon="content_copy"
@click="copyText(props.row.meta.xpub)"
class="q-ml-sm"
></q-btn>
</div> </div>
<div class="col-2 q-pr-lg"></div>
</div> </div>
<div class="row items-center no-wrap q-mb-md"> <div class="row items-center no-wrap q-mb-md">
<div class="col-2 q-pr-lg">Last Address Index:</div> <div class="col-2 q-pr-lg">Last Address Index:</div>
@ -229,4 +271,15 @@
</q-form> </q-form>
</q-card> </q-card>
</q-dialog> </q-dialog>
<q-dialog v-model="showQrCodeDialog" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<qrcode
:value="qrCodeValue"
:options="{width: 800}"
class="rounded-borders"
></qrcode>
</q-responsive>
</q-card>
</q-dialog>
</div> </div>

View file

@ -16,6 +16,8 @@ async function walletList(path) {
return { return {
walletAccounts: [], walletAccounts: [],
address: {}, address: {},
showQrCodeDialog: false,
qrCodeValue: null,
formDialog: { formDialog: {
show: false, show: false,
@ -118,9 +120,11 @@ async function walletList(path) {
}, },
createWalletAccount: async function (data) { createWalletAccount: async function (data) {
try { try {
const meta = {accountPath: this.accountPath}
if (this.formDialog.useSerialPort) { if (this.formDialog.useSerialPort) {
const {xpub, fingerprint} = await this.fetchXpubFromHww() const {xpub, fingerprint} = await this.fetchXpubFromHww()
if (!xpub) return if (!xpub) return
meta.xpub = xpub
const path = this.accountPath.substring(2) const path = this.accountPath.substring(2)
const outputType = this.formDialog.addressType.id const outputType = this.formDialog.addressType.id
if (outputType === 'sh') { if (outputType === 'sh') {
@ -129,6 +133,7 @@ async function walletList(path) {
data.masterpub = `${outputType}([${fingerprint}/${path}]${xpub}/{0,1}/*)` data.masterpub = `${outputType}([${fingerprint}/${path}]${xpub}/{0,1}/*)`
} }
} }
data.meta = JSON.stringify(meta)
const response = await LNbits.api.request( const response = await LNbits.api.request(
'POST', 'POST',
'/watchonly/api/v1/wallet', '/watchonly/api/v1/wallet',
@ -233,7 +238,7 @@ async function walletList(path) {
const addressData = mapAddressesData(data) const addressData = mapAddressesData(data)
addressData.note = `Shared on ${currentDateTime()}` addressData.note = `Shared on ${currentDateTime()}`
const lastAcctiveAddress = const lastActiveAddress =
this.addresses this.addresses
.filter( .filter(
a => a =>
@ -243,11 +248,11 @@ async function walletList(path) {
addressData.gapLimitExceeded = addressData.gapLimitExceeded =
!addressData.isChange && !addressData.isChange &&
addressData.addressIndex > addressData.addressIndex >
lastAcctiveAddress.addressIndex + DEFAULT_RECEIVE_GAP_LIMIT lastActiveAddress.addressIndex + DEFAULT_RECEIVE_GAP_LIMIT
const wallet = this.walletAccounts.find(w => w.id === walletId) || {} const wallet = this.walletAccounts.find(w => w.id === walletId) || {}
wallet.address_no = addressData.addressIndex wallet.address_no = addressData.addressIndex
this.$emit('new-receive-address', addressData) this.$emit('new-receive-address', {addressData, wallet})
}, },
showAddAccountDialog: function () { showAddAccountDialog: function () {
this.formDialog.show = true this.formDialog.show = true
@ -283,6 +288,20 @@ async function walletList(path) {
const addressType = const addressType =
this.addressTypeOptions.find(t => t.id === value.id) || {} this.addressTypeOptions.find(t => t.id === value.id) || {}
this.accountPath = addressType[`path${this.network}`] this.accountPath = addressType[`path${this.network}`]
},
// todo: bad. base.js not present in custom components
copyText: function (text, message, position) {
var notify = this.$q.notify
Quasar.utils.copyToClipboard(text).then(function () {
notify({
message: message || 'Copied to clipboard!',
position: position || 'bottom'
})
})
},
openQrCodeDialog: function (qrCodeValue) {
this.qrCodeValue = qrCodeValue
this.showQrCodeDialog = true
} }
}, },
created: async function () { created: async function () {

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,7 @@ const watchOnly = async () => {
await history('static/components/history/history.html') await history('static/components/history/history.html')
await utxoList('static/components/utxo-list/utxo-list.html') await utxoList('static/components/utxo-list/utxo-list.html')
await feeRate('static/components/fee-rate/fee-rate.html') await feeRate('static/components/fee-rate/fee-rate.html')
await seedInput('static/components/seed-input/seed-input.html')
await sendTo('static/components/send-to/send-to.html') await sendTo('static/components/send-to/send-to.html')
await payment('static/components/payment/payment.html') await payment('static/components/payment/payment.html')
await serialSigner('static/components/serial-signer/serial-signer.html') await serialSigner('static/components/serial-signer/serial-signer.html')
@ -172,10 +173,6 @@ const watchOnly = async () => {
this.$refs.paymentRef.updateSignedPsbt(psbtBase64) this.$refs.paymentRef.updateSignedPsbt(psbtBase64)
}, },
//################### SERIAL PORT ###################
//################### HARDWARE WALLET ###################
//################### UTXOs ################### //################### UTXOs ###################
scanAllAddresses: async function () { scanAllAddresses: async function () {
await this.refreshAddresses() await this.refreshAddresses()
@ -227,7 +224,7 @@ const watchOnly = async () => {
newAddr => !this.addresses.find(a => a.address === newAddr.address) newAddr => !this.addresses.find(a => a.address === newAddr.address)
) )
const lastAcctiveAddress = const lastActiveAddress =
uniqueAddresses.filter(a => !a.isChange && a.hasActivity).pop() || uniqueAddresses.filter(a => !a.isChange && a.hasActivity).pop() ||
{} {}
@ -237,7 +234,7 @@ const watchOnly = async () => {
a.gapLimitExceeded = a.gapLimitExceeded =
!a.isChange && !a.isChange &&
a.addressIndex > a.addressIndex >
lastAcctiveAddress.addressIndex + DEFAULT_RECEIVE_GAP_LIMIT lastActiveAddress.addressIndex + DEFAULT_RECEIVE_GAP_LIMIT
}) })
this.addresses.push(...uniqueAddresses) this.addresses.push(...uniqueAddresses)
} }
@ -380,6 +377,26 @@ const watchOnly = async () => {
showAddressDetails: function (addressData) { showAddressDetails: function (addressData) {
this.openQrCodeDialog(addressData) this.openQrCodeDialog(addressData)
}, },
showAddressDetailsWithConfirmation: function ({addressData, wallet}) {
this.showAddressDetails(addressData)
if (this.$refs.serialSigner.isConnected()) {
if (this.$refs.serialSigner.isAuthenticated()) {
if (wallet.meta?.accountPath) {
const branchIndex = addressData.isChange ? 1 : 0
const path =
wallet.meta.accountPath +
`/${branchIndex}/${addressData.addressIndex}`
this.$refs.serialSigner.hwwShowAddress(path, addressData.address)
}
} else {
this.$q.notify({
type: 'warning',
message: 'Please login in order to confirm address on device',
timeout: 10000
})
}
}
},
initUtxos: function (addresses) { initUtxos: function (addresses) {
if (!this.fetchedUtxos && addresses.length) { if (!this.fetchedUtxos && addresses.length) {
this.fetchedUtxos = true this.fetchedUtxos = true

View file

@ -74,6 +74,7 @@ const mapWalletAccount = function (o) {
'YYYY-MM-DD HH:mm' 'YYYY-MM-DD HH:mm'
) )
: '', : '',
meta: o.meta ? JSON.parse(o.meta) : null,
label: o.title, label: o.title,
expanded: false expanded: false
}) })

View file

@ -3,6 +3,7 @@ const PSBT_BASE64_PREFIX = 'cHNidP8'
const COMMAND_PING = '/ping' const COMMAND_PING = '/ping'
const COMMAND_PASSWORD = '/password' const COMMAND_PASSWORD = '/password'
const COMMAND_PASSWORD_CLEAR = '/password-clear' const COMMAND_PASSWORD_CLEAR = '/password-clear'
const COMMAND_ADDRESS = '/address'
const COMMAND_SEND_PSBT = '/psbt' const COMMAND_SEND_PSBT = '/psbt'
const COMMAND_SIGN_PSBT = '/sign' const COMMAND_SIGN_PSBT = '/sign'
const COMMAND_HELP = '/help' const COMMAND_HELP = '/help'

View file

@ -27,7 +27,7 @@
:addresses="addresses" :addresses="addresses"
:serial-signer-ref="$refs.serialSigner" :serial-signer-ref="$refs.serialSigner"
@accounts-update="updateAccounts" @accounts-update="updateAccounts"
@new-receive-address="showAddressDetails" @new-receive-address="showAddressDetailsWithConfirmation"
> >
</wallet-list> </wallet-list>
@ -149,6 +149,7 @@
<q-card-section> <q-card-section>
<h6 class="text-subtitle1 q-my-none"> <h6 class="text-subtitle1 q-my-none">
{{SITE_TITLE}} Onchain Wallet (watch-only) Extension {{SITE_TITLE}} Onchain Wallet (watch-only) Extension
<small>(v0.2)</small>
</h6> </h6>
</q-card-section> </q-card-section>
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
@ -238,6 +239,8 @@
<script src="{{ url_for('watchonly_static', path='js/tables.js') }}"></script> <script src="{{ url_for('watchonly_static', path='js/tables.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='js/map.js') }}"></script> <script src="{{ url_for('watchonly_static', path='js/map.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='js/utils.js') }}"></script> <script src="{{ url_for('watchonly_static', path='js/utils.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='js/bip39-word-list.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='components/my-checkbox/my-checkbox.js') }}"></script> <script src="{{ url_for('watchonly_static', path='components/my-checkbox/my-checkbox.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='components/wallet-config/wallet-config.js') }}"></script> <script src="{{ url_for('watchonly_static', path='components/wallet-config/wallet-config.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='components/wallet-list/wallet-list.js') }}"></script> <script src="{{ url_for('watchonly_static', path='components/wallet-list/wallet-list.js') }}"></script>
@ -245,10 +248,12 @@
<script src="{{ url_for('watchonly_static', path='components/history/history.js') }}"></script> <script src="{{ url_for('watchonly_static', path='components/history/history.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='components/utxo-list/utxo-list.js') }}"></script> <script src="{{ url_for('watchonly_static', path='components/utxo-list/utxo-list.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='components/fee-rate/fee-rate.js') }}"></script> <script src="{{ url_for('watchonly_static', path='components/fee-rate/fee-rate.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='components/seed-input/seed-input.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='components/send-to/send-to.js') }}"></script> <script src="{{ url_for('watchonly_static', path='components/send-to/send-to.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='components/payment/payment.js') }}"></script> <script src="{{ url_for('watchonly_static', path='components/payment/payment.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='components/serial-signer/serial-signer.js') }}"></script> <script src="{{ url_for('watchonly_static', path='components/serial-signer/serial-signer.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='components/serial-port-config/serial-port-config.js') }}"></script> <script src="{{ url_for('watchonly_static', path='components/serial-port-config/serial-port-config.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='js/crypto/noble-secp256k1.js') }}"></script> <script src="{{ url_for('watchonly_static', path='js/crypto/noble-secp256k1.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='js/crypto/aes.js') }}"></script> <script src="{{ url_for('watchonly_static', path='js/crypto/aes.js') }}"></script>

View file

@ -93,6 +93,7 @@ async def api_wallet_create_or_update(
address_no=-1, # so fresh address on empty wallet can get address with index 0 address_no=-1, # so fresh address on empty wallet can get address with index 0
balance=0, balance=0,
network=network["name"], network=network["name"],
meta=data.meta,
) )
wallets = await get_watch_wallets(w.wallet.user, network["name"]) wallets = await get_watch_wallets(w.wallet.user, network["name"])
@ -137,7 +138,7 @@ async def api_wallet_delete(wallet_id, w: WalletTypeInfo = Depends(require_admin
await delete_watch_wallet(wallet_id) await delete_watch_wallet(wallet_id)
await delete_addresses_for_wallet(wallet_id) await delete_addresses_for_wallet(wallet_id)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT) return "", HTTPStatus.NO_CONTENT
#############################ADDRESSES########################## #############################ADDRESSES##########################
@ -268,7 +269,6 @@ async def api_psbt_create(
for i, inp in enumerate(inputs_extra): for i, inp in enumerate(inputs_extra):
psbt.inputs[i].bip32_derivations = inp["bip32_derivations"] psbt.inputs[i].bip32_derivations = inp["bip32_derivations"]
psbt.inputs[i].non_witness_utxo = inp.get("non_witness_utxo", None) psbt.inputs[i].non_witness_utxo = inp.get("non_witness_utxo", None)
print("### ", inp.get("non_witness_utxo", None))
outputs_extra = [] outputs_extra = []
bip32_derivations = {} bip32_derivations = {}
@ -343,11 +343,8 @@ async def api_tx_broadcast(
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
r = await client.post(endpoint + "/api/tx", data=data.tx_hex) r = await client.post(endpoint + "/api/tx", data=data.tx_hex)
tx_id = r.text tx_id = r.text
print("### broadcast tx_id: ", tx_id)
return tx_id return tx_id
# return "0f0f0f0f0f0f0f0f0f0f0f00f0f0f0f0f0f0f0f0f0f00f0f0f0f0f0f0.mock.transaction.id"
except Exception as e: except Exception as e:
print("### broadcast error: ", str(e))
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e)) raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))