feat: show address details on device
This commit is contained in:
parent
c432671ab7
commit
8c87e84b68
|
@ -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,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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 '{}';")
|
||||||
|
|
|
@ -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":
|
||||||
|
|
|
@ -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
|
||||||
|
@ -428,31 +453,7 @@
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
<br />
|
<br />
|
||||||
<q-toggle
|
|
||||||
label="Passphrase (optional)"
|
|
||||||
color="secodary"
|
|
||||||
v-model="hww.hasPassphrase"
|
|
||||||
></q-toggle>
|
|
||||||
<br />
|
|
||||||
<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>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<br />
|
|
||||||
<span>Enter new password (8 numbers/letters)</span>
|
<span>Enter new password (8 numbers/letters)</span>
|
||||||
<q-input
|
<q-input
|
||||||
v-model.trim="hww.password"
|
v-model.trim="hww.password"
|
||||||
|
|
|
@ -403,7 +403,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 +417,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 +454,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) {
|
||||||
|
@ -800,17 +821,9 @@ async function serialSigner(path) {
|
||||||
},
|
},
|
||||||
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 +835,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
|
||||||
|
|
|
@ -125,14 +125,35 @@
|
||||||
<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-8">
|
||||||
<q-input
|
<q-input v-model="props.row.masterpub" filled readonly />
|
||||||
v-model="props.row.masterpub"
|
</div>
|
||||||
filled
|
<div class="col-2 q-pr-lg">
|
||||||
readonly
|
<q-btn
|
||||||
type="textarea"
|
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-8">
|
||||||
|
<q-input v-model="props.row.meta.xpub" filled readonly />
|
||||||
|
</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>
|
||||||
|
|
|
@ -118,9 +118,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 +131,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',
|
||||||
|
@ -247,7 +250,7 @@ async function walletList(path) {
|
||||||
|
|
||||||
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 +286,16 @@ 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'
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: async function () {
|
created: async function () {
|
||||||
|
|
|
@ -172,10 +172,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()
|
||||||
|
@ -380,6 +376,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
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
@ -148,7 +148,8 @@
|
||||||
<q-card>
|
<q-card>
|
||||||
<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 <small>(v0.100)</small>
|
{{SITE_TITLE}} Onchain Wallet (watch-only) Extension
|
||||||
|
<small>(v0.100)</small>
|
||||||
</h6>
|
</h6>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
|
|
|
@ -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##########################
|
||||||
|
|
Loading…
Reference in New Issue
Block a user