feat: handle psbt extract
This commit is contained in:
parent
3bf0bb1e63
commit
73adc4a7e8
6 changed files with 149 additions and 32 deletions
|
|
@ -1,6 +1,5 @@
|
||||||
from sqlite3 import Row
|
from sqlite3 import Row
|
||||||
from typing import List
|
from typing import List, Optional
|
||||||
|
|
||||||
from fastapi.param_functions import Query
|
from fastapi.param_functions import Query
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
@ -82,6 +81,15 @@ class CreatePsbt(BaseModel):
|
||||||
tx_size: int
|
tx_size: int
|
||||||
|
|
||||||
|
|
||||||
|
class ExtractPsbt(BaseModel):
|
||||||
|
psbtBase64 = ""
|
||||||
|
|
||||||
|
|
||||||
|
class SignedTransaction(BaseModel):
|
||||||
|
tx_hex: Optional[str]
|
||||||
|
tx_json: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
class Config(BaseModel):
|
class Config(BaseModel):
|
||||||
mempool_endpoint = "https://mempool.space"
|
mempool_endpoint = "https://mempool.space"
|
||||||
receive_gap_limit = 20
|
receive_gap_limit = 20
|
||||||
|
|
|
||||||
|
|
@ -587,7 +587,7 @@ new Vue({
|
||||||
await this.serial.writer.write(this.payment.psbtBase64 + '\n')
|
await this.serial.writer.write(this.payment.psbtBase64 + '\n')
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Data sent to serial port!',
|
message: 'Data sent to serial port device!',
|
||||||
timeout: 5000
|
timeout: 5000
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -609,19 +609,42 @@ new Vue({
|
||||||
textDecoder.writable
|
textDecoder.writable
|
||||||
)
|
)
|
||||||
this.serial.reader = textDecoder.readable.getReader()
|
this.serial.reader = textDecoder.readable.getReader()
|
||||||
|
let psbtChunks = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
console.log('### reader.read()')
|
console.log('### reader.read()')
|
||||||
const {value, done} = await this.serial.reader.read()
|
const {value, done} = await this.serial.reader.read()
|
||||||
|
console.log('### value', value)
|
||||||
if (value) {
|
if (value) {
|
||||||
console.log(value)
|
const data = value.split('\n')
|
||||||
this.$q.notify({
|
console.log('### xxx', data)
|
||||||
type: 'warning',
|
const isPsbtStartChunk = data[0].startsWith(PSBT_BASE64_PREFIX)
|
||||||
message: 'Received data from serial port (not psbt)',
|
if (isPsbtStartChunk) {
|
||||||
caption: value.slice(0, 80) + '...',
|
psbtChunks = [data[0]]
|
||||||
timeout: 5000
|
} else if (psbtChunks.length) {
|
||||||
})
|
psbtChunks.push(data[0])
|
||||||
|
if (data.length > 1) {
|
||||||
|
console.log('### psbtChunks', psbtChunks)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'PSBT received from serial port device!',
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
const transaction = await this.etractTxFromPsbt(
|
||||||
|
psbtChunks.join('')
|
||||||
|
)
|
||||||
|
console.log('### transaction', transaction)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
psbtChunks = []
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Received data from serial port (not psbt)',
|
||||||
|
caption: value.slice(0, 80) + '...',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (done) {
|
if (done) {
|
||||||
return
|
return
|
||||||
|
|
@ -638,6 +661,33 @@ new Vue({
|
||||||
}
|
}
|
||||||
console.log('### startSerialPortReading DONE')
|
console.log('### startSerialPortReading DONE')
|
||||||
},
|
},
|
||||||
|
etractTxFromPsbt: 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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
console.log('### data', data)
|
||||||
|
if (data.error) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Cannot process received PSBT!',
|
||||||
|
caption: data.error,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
console.log('### error', error, JSON.stringify(error))
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
sharePsbtWithAnimatedQRCode: async function () {
|
sharePsbtWithAnimatedQRCode: async function () {
|
||||||
console.log('### sharePsbtWithAnimatedQRCode')
|
console.log('### sharePsbtWithAnimatedQRCode')
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -271,7 +271,7 @@ const tableData = {
|
||||||
utxoSelectionMode: 'Manual',
|
utxoSelectionMode: 'Manual',
|
||||||
signModes: [
|
signModes: [
|
||||||
{
|
{
|
||||||
label: 'Serial Port',
|
label: 'Serial Port Device',
|
||||||
value: 'serial-port'
|
value: 'serial-port'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
const PSBT_BASE64_PREFIX = 'cHNidP8'
|
||||||
|
|
||||||
const blockTimeToDate = blockTime =>
|
const blockTimeToDate = blockTime =>
|
||||||
blockTime ? moment(blockTime * 1000).format('LLL') : ''
|
blockTime ? moment(blockTime * 1000).format('LLL') : ''
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1044,7 +1044,7 @@
|
||||||
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>
|
||||||
<div class="col-3">
|
<div class="col-2">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="!serial.selectedPort"
|
v-if="!serial.selectedPort"
|
||||||
@click="openSerialPort()"
|
@click="openSerialPort()"
|
||||||
|
|
@ -1061,21 +1061,22 @@
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
|
<q-toggle
|
||||||
|
label="Advanced Config"
|
||||||
|
color="secodary float-left"
|
||||||
|
class="q-pl-lg"
|
||||||
|
v-model="serial.showAdvancedConfig"
|
||||||
|
></q-toggle>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="serial.selectedPort"
|
v-if="serial.selectedPort"
|
||||||
@click="sendPsbtToSerialPort()"
|
@click="sendPsbtToSerialPort()"
|
||||||
unelevated
|
unelevated
|
||||||
color="secondary float-right"
|
color="secondary float-right"
|
||||||
>Send PSBT</q-btn
|
>Send to Device</q-btn
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-3">
|
|
||||||
<q-toggle
|
|
||||||
label="Advanced Config"
|
|
||||||
color="secodary float-right"
|
|
||||||
v-model="serial.showAdvancedConfig"
|
|
||||||
></q-toggle>
|
|
||||||
</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-4 q-pr-lg"></div>
|
<div class="col-4 q-pr-lg"></div>
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,40 @@
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
import json
|
||||||
|
|
||||||
from embit import script
|
|
||||||
from embit.descriptor import Descriptor, Key
|
|
||||||
from embit.ec import PublicKey
|
|
||||||
from embit.psbt import PSBT, DerivationPath
|
|
||||||
from embit.transaction import Transaction, TransactionInput, TransactionOutput
|
|
||||||
from fastapi import Query, Request
|
from fastapi import Query, Request
|
||||||
from fastapi.params import Depends
|
from fastapi.params import Depends
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
|
from embit.descriptor import Descriptor, Key
|
||||||
|
from embit.psbt import PSBT, DerivationPath
|
||||||
|
from embit.ec import PublicKey
|
||||||
|
from embit.transaction import Transaction, TransactionInput, TransactionOutput
|
||||||
|
from embit import script, finalizer
|
||||||
|
|
||||||
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||||
from lnbits.extensions.watchonly import watchonly_ext
|
from lnbits.extensions.watchonly import watchonly_ext
|
||||||
|
|
||||||
from .crud import (
|
from .crud import (
|
||||||
create_config,
|
create_mempool,
|
||||||
create_fresh_addresses,
|
|
||||||
create_watch_wallet,
|
create_watch_wallet,
|
||||||
delete_addresses_for_wallet,
|
|
||||||
delete_watch_wallet,
|
delete_watch_wallet,
|
||||||
get_addresses,
|
get_addresses,
|
||||||
get_config,
|
|
||||||
get_fresh_address,
|
get_fresh_address,
|
||||||
|
create_fresh_addresses,
|
||||||
|
update_address,
|
||||||
|
delete_addresses_for_wallet,
|
||||||
|
get_mempool,
|
||||||
get_watch_wallet,
|
get_watch_wallet,
|
||||||
get_watch_wallets,
|
get_watch_wallets,
|
||||||
update_address,
|
update_mempool,
|
||||||
update_config,
|
|
||||||
update_watch_wallet,
|
update_watch_wallet,
|
||||||
|
create_config,
|
||||||
|
get_config,
|
||||||
|
update_config,
|
||||||
)
|
)
|
||||||
|
from .models import SignedTransaction, CreateWallet, CreatePsbt, Config, WalletAccount, ExtractPsbt
|
||||||
from .helpers import parse_key
|
from .helpers import parse_key
|
||||||
from .models import Config, CreatePsbt, CreateWallet, WalletAccount
|
|
||||||
|
|
||||||
###################WALLETS#############################
|
###################WALLETS#############################
|
||||||
|
|
||||||
|
|
@ -261,6 +267,36 @@ async def api_psbt_create(
|
||||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
|
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@watchonly_ext.put("/api/v1/psbt/extract")
|
||||||
|
async def api_psbt_extract_tx(
|
||||||
|
data: ExtractPsbt, w: WalletTypeInfo = Depends(require_admin_key)
|
||||||
|
):
|
||||||
|
res = SignedTransaction()
|
||||||
|
try:
|
||||||
|
psbt = PSBT.from_base64(data.psbtBase64)
|
||||||
|
final_psbt = finalizer.finalize_psbt(psbt)
|
||||||
|
if not final_psbt:
|
||||||
|
raise ValueError("PSBT cannot be finalized!")
|
||||||
|
res.tx_hex = final_psbt.to_string()
|
||||||
|
|
||||||
|
transaction = Transaction.from_string(res.tx_hex)
|
||||||
|
tx = {
|
||||||
|
"locktime": transaction.locktime,
|
||||||
|
"version": transaction.version,
|
||||||
|
"outputs": [],
|
||||||
|
"fee": psbt.fee(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for out in transaction.vout:
|
||||||
|
tx["outputs"].append(
|
||||||
|
{"value": out.value, "address": out.script_pubkey.address()}
|
||||||
|
)
|
||||||
|
res.tx_json = json.dumps(tx)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
|
||||||
|
return res.dict()
|
||||||
|
|
||||||
|
|
||||||
#############################CONFIG##########################
|
#############################CONFIG##########################
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -278,3 +314,23 @@ async def api_get_config(w: WalletTypeInfo = Depends(get_key_type)):
|
||||||
if not config:
|
if not config:
|
||||||
config = await create_config(user=w.wallet.user)
|
config = await create_config(user=w.wallet.user)
|
||||||
return config.dict()
|
return config.dict()
|
||||||
|
|
||||||
|
|
||||||
|
#############################MEMPOOL##########################
|
||||||
|
|
||||||
|
### TODO: fix statspay dependcy and remove
|
||||||
|
@watchonly_ext.put("/api/v1/mempool")
|
||||||
|
async def api_update_mempool(
|
||||||
|
endpoint: str = Query(...), w: WalletTypeInfo = Depends(require_admin_key)
|
||||||
|
):
|
||||||
|
mempool = await update_mempool(**{"endpoint": endpoint}, user=w.wallet.user)
|
||||||
|
return mempool.dict()
|
||||||
|
|
||||||
|
|
||||||
|
### TODO: fix statspay dependcy and remove
|
||||||
|
@watchonly_ext.get("/api/v1/mempool")
|
||||||
|
async def api_get_mempool(w: WalletTypeInfo = Depends(require_admin_key)):
|
||||||
|
mempool = await get_mempool(w.wallet.user)
|
||||||
|
if not mempool:
|
||||||
|
mempool = await create_mempool(user=w.wallet.user)
|
||||||
|
return mempool.dict()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue