Merge pull request #988 from motorina0/watchonly_improvements
Watchonly improvements
This commit is contained in:
commit
3fe33a0be8
17 changed files with 2494 additions and 77 deletions
|
|
@ -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":
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -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,8 +459,15 @@
|
||||||
>
|
>
|
||||||
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 />
|
<br />
|
||||||
<span>Enter new word list separated by space</span>
|
<q-toggle
|
||||||
|
label="Enter word list separated by space"
|
||||||
|
color="secodary"
|
||||||
|
v-model="hww.quickMnemonicInput"
|
||||||
|
></q-toggle>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div v-if="hww.quickMnemonicInput">
|
||||||
<q-input
|
<q-input
|
||||||
v-model.trim="hww.mnemonic"
|
v-model.trim="hww.mnemonic"
|
||||||
filled
|
filled
|
||||||
|
|
@ -427,30 +484,10 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<seed-input v-else @on-seed-input-done="seedInputDone"></seed-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>
|
<q-separator></q-separator>
|
||||||
<br />
|
<br />
|
||||||
<span>Enter new password (8 numbers/letters)</span>
|
<span>Enter new password (8 numbers/letters)</span>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 () {
|
||||||
|
|
|
||||||
2050
lnbits/extensions/watchonly/static/js/bip39-word-list.js
Normal file
2050
lnbits/extensions/watchonly/static/js/bip39-word-list.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue