diff --git a/lnbits/extensions/watchonly/models.py b/lnbits/extensions/watchonly/models.py index bc10e421..d499c504 100644 --- a/lnbits/extensions/watchonly/models.py +++ b/lnbits/extensions/watchonly/models.py @@ -1,6 +1,5 @@ from sqlite3 import Row -from typing import List - +from typing import List, Optional from fastapi.param_functions import Query from pydantic import BaseModel @@ -82,6 +81,15 @@ class CreatePsbt(BaseModel): tx_size: int +class ExtractPsbt(BaseModel): + psbtBase64 = "" + + +class SignedTransaction(BaseModel): + tx_hex: Optional[str] + tx_json: Optional[str] + + class Config(BaseModel): mempool_endpoint = "https://mempool.space" receive_gap_limit = 20 diff --git a/lnbits/extensions/watchonly/static/js/index.js b/lnbits/extensions/watchonly/static/js/index.js index b3d69299..d6b21f31 100644 --- a/lnbits/extensions/watchonly/static/js/index.js +++ b/lnbits/extensions/watchonly/static/js/index.js @@ -587,7 +587,7 @@ new Vue({ await this.serial.writer.write(this.payment.psbtBase64 + '\n') this.$q.notify({ type: 'positive', - message: 'Data sent to serial port!', + message: 'Data sent to serial port device!', timeout: 5000 }) } catch (error) { @@ -609,19 +609,42 @@ new Vue({ textDecoder.writable ) this.serial.reader = textDecoder.readable.getReader() + let psbtChunks = [] try { while (true) { console.log('### reader.read()') const {value, done} = await this.serial.reader.read() + console.log('### value', value) if (value) { - console.log(value) - this.$q.notify({ - type: 'warning', - message: 'Received data from serial port (not psbt)', - caption: value.slice(0, 80) + '...', - timeout: 5000 - }) + const data = value.split('\n') + console.log('### xxx', data) + const isPsbtStartChunk = data[0].startsWith(PSBT_BASE64_PREFIX) + if (isPsbtStartChunk) { + psbtChunks = [data[0]] + } 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) { return @@ -638,6 +661,33 @@ new Vue({ } 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 () { console.log('### sharePsbtWithAnimatedQRCode') }, diff --git a/lnbits/extensions/watchonly/static/js/tables.js b/lnbits/extensions/watchonly/static/js/tables.js index 26182912..49597bd2 100644 --- a/lnbits/extensions/watchonly/static/js/tables.js +++ b/lnbits/extensions/watchonly/static/js/tables.js @@ -271,7 +271,7 @@ const tableData = { utxoSelectionMode: 'Manual', signModes: [ { - label: 'Serial Port', + label: 'Serial Port Device', value: 'serial-port' }, { diff --git a/lnbits/extensions/watchonly/static/js/utils.js b/lnbits/extensions/watchonly/static/js/utils.js index 26bebac6..3726d867 100644 --- a/lnbits/extensions/watchonly/static/js/utils.js +++ b/lnbits/extensions/watchonly/static/js/utils.js @@ -1,3 +1,5 @@ +const PSBT_BASE64_PREFIX = 'cHNidP8' + const blockTimeToDate = blockTime => blockTime ? moment(blockTime * 1000).format('LLL') : '' diff --git a/lnbits/extensions/watchonly/templates/watchonly/index.html b/lnbits/extensions/watchonly/templates/watchonly/index.html index f140494a..c5993d2a 100644 --- a/lnbits/extensions/watchonly/templates/watchonly/index.html +++ b/lnbits/extensions/watchonly/templates/watchonly/index.html @@ -1044,7 +1044,7 @@ class="row items-center no-wrap q-mb-md q-mt-lg" >
-
+
+ +
+
Send PSBTSend to Device
-
- -
diff --git a/lnbits/extensions/watchonly/views_api.py b/lnbits/extensions/watchonly/views_api.py index ae656540..2c097022 100644 --- a/lnbits/extensions/watchonly/views_api.py +++ b/lnbits/extensions/watchonly/views_api.py @@ -1,34 +1,40 @@ 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.params import Depends 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.extensions.watchonly import watchonly_ext from .crud import ( - create_config, - create_fresh_addresses, + create_mempool, create_watch_wallet, - delete_addresses_for_wallet, delete_watch_wallet, get_addresses, - get_config, get_fresh_address, + create_fresh_addresses, + update_address, + delete_addresses_for_wallet, + get_mempool, get_watch_wallet, get_watch_wallets, - update_address, - update_config, + update_mempool, update_watch_wallet, + create_config, + get_config, + update_config, ) +from .models import SignedTransaction, CreateWallet, CreatePsbt, Config, WalletAccount, ExtractPsbt from .helpers import parse_key -from .models import Config, CreatePsbt, CreateWallet, WalletAccount + ###################WALLETS############################# @@ -261,6 +267,36 @@ async def api_psbt_create( 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########################## @@ -278,3 +314,23 @@ async def api_get_config(w: WalletTypeInfo = Depends(get_key_type)): if not config: config = await create_config(user=w.wallet.user) 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()