feat: add on-chain watch-only functionality
This commit is contained in:
parent
92e7dde500
commit
c1289c43a4
|
@ -1,116 +1,66 @@
|
|||
import json
|
||||
from typing import List, Optional
|
||||
|
||||
from embit.descriptor import Descriptor, Key # type: ignore
|
||||
from embit.descriptor.arguments import AllowedDerivation # type: ignore
|
||||
from embit.networks import NETWORKS # type: ignore
|
||||
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from . import db
|
||||
from .models import Addresses, Mempool, Wallets
|
||||
from .models import Address, Config, Mempool, WalletAccount
|
||||
from .helpers import parse_key, derive_address
|
||||
|
||||
|
||||
##########################WALLETS####################
|
||||
|
||||
|
||||
def detect_network(k):
|
||||
version = k.key.version
|
||||
for network_name in NETWORKS:
|
||||
net = NETWORKS[network_name]
|
||||
# not found in this network
|
||||
if version in [net["xpub"], net["ypub"], net["zpub"], net["Zpub"], net["Ypub"]]:
|
||||
return net
|
||||
|
||||
|
||||
def parse_key(masterpub: str):
|
||||
"""Parses masterpub or descriptor and returns a tuple: (Descriptor, network)
|
||||
To create addresses use descriptor.derive(num).address(network=network)
|
||||
"""
|
||||
network = None
|
||||
# probably a single key
|
||||
if "(" not in masterpub:
|
||||
k = Key.from_string(masterpub)
|
||||
if not k.is_extended:
|
||||
raise ValueError("The key is not a master public key")
|
||||
if k.is_private:
|
||||
raise ValueError("Private keys are not allowed")
|
||||
# check depth
|
||||
if k.key.depth != 3:
|
||||
raise ValueError(
|
||||
"Non-standard depth. Only bip44, bip49 and bip84 are supported with bare xpubs. For custom derivation paths use descriptors."
|
||||
)
|
||||
# if allowed derivation is not provided use default /{0,1}/*
|
||||
if k.allowed_derivation is None:
|
||||
k.allowed_derivation = AllowedDerivation.default()
|
||||
# get version bytes
|
||||
version = k.key.version
|
||||
for network_name in NETWORKS:
|
||||
net = NETWORKS[network_name]
|
||||
# not found in this network
|
||||
if version in [net["xpub"], net["ypub"], net["zpub"]]:
|
||||
network = net
|
||||
if version == net["xpub"]:
|
||||
desc = Descriptor.from_string("pkh(%s)" % str(k))
|
||||
elif version == net["ypub"]:
|
||||
desc = Descriptor.from_string("sh(wpkh(%s))" % str(k))
|
||||
elif version == net["zpub"]:
|
||||
desc = Descriptor.from_string("wpkh(%s)" % str(k))
|
||||
break
|
||||
# we didn't find correct version
|
||||
if network is None:
|
||||
raise ValueError("Unknown master public key version")
|
||||
else:
|
||||
desc = Descriptor.from_string(masterpub)
|
||||
if not desc.is_wildcard:
|
||||
raise ValueError("Descriptor should have wildcards")
|
||||
for k in desc.keys:
|
||||
if k.is_extended:
|
||||
net = detect_network(k)
|
||||
if net is None:
|
||||
raise ValueError(f"Unknown version: {k}")
|
||||
if network is not None and network != net:
|
||||
raise ValueError("Keys from different networks")
|
||||
network = net
|
||||
return desc, network
|
||||
|
||||
|
||||
async def create_watch_wallet(user: str, masterpub: str, title: str) -> Wallets:
|
||||
async def create_watch_wallet(user: str, masterpub: str, title: str) -> WalletAccount:
|
||||
# check the masterpub is fine, it will raise an exception if not
|
||||
parse_key(masterpub)
|
||||
(descriptor, _) = parse_key(masterpub)
|
||||
|
||||
type = descriptor.scriptpubkey_type()
|
||||
fingerprint = descriptor.keys[0].fingerprint.hex()
|
||||
wallet_id = urlsafe_short_hash()
|
||||
|
||||
wallets = await get_watch_wallets(user)
|
||||
w = next((w for w in wallets if w.fingerprint == fingerprint), None)
|
||||
if w:
|
||||
raise ValueError("Account '{}' has the same master pulic key".format(w.title))
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO watchonly.wallets (
|
||||
id,
|
||||
"user",
|
||||
masterpub,
|
||||
fingerprint,
|
||||
title,
|
||||
type,
|
||||
address_no,
|
||||
balance
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
# address_no is -1 so fresh address on empty wallet can get address with index 0
|
||||
(wallet_id, user, masterpub, title, -1, 0),
|
||||
(wallet_id, user, masterpub, fingerprint, title, type, -1, 0),
|
||||
)
|
||||
|
||||
return await get_watch_wallet(wallet_id)
|
||||
|
||||
|
||||
async def get_watch_wallet(wallet_id: str) -> Optional[Wallets]:
|
||||
async def get_watch_wallet(wallet_id: str) -> Optional[WalletAccount]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM watchonly.wallets WHERE id = ?", (wallet_id,)
|
||||
)
|
||||
return Wallets.from_row(row) if row else None
|
||||
return WalletAccount.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_watch_wallets(user: str) -> List[Wallets]:
|
||||
async def get_watch_wallets(user: str) -> List[WalletAccount]:
|
||||
rows = await db.fetchall(
|
||||
"""SELECT * FROM watchonly.wallets WHERE "user" = ?""", (user,)
|
||||
)
|
||||
return [Wallets(**row) for row in rows]
|
||||
return [WalletAccount(**row) for row in rows]
|
||||
|
||||
|
||||
async def update_watch_wallet(wallet_id: str, **kwargs) -> Optional[Wallets]:
|
||||
async def update_watch_wallet(wallet_id: str, **kwargs) -> Optional[WalletAccount]:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
|
||||
await db.execute(
|
||||
|
@ -119,7 +69,7 @@ async def update_watch_wallet(wallet_id: str, **kwargs) -> Optional[Wallets]:
|
|||
row = await db.fetchone(
|
||||
"SELECT * FROM watchonly.wallets WHERE id = ?", (wallet_id,)
|
||||
)
|
||||
return Wallets.from_row(row) if row else None
|
||||
return WalletAccount.from_row(row) if row else None
|
||||
|
||||
|
||||
async def delete_watch_wallet(wallet_id: str) -> None:
|
||||
|
@ -128,56 +78,173 @@ async def delete_watch_wallet(wallet_id: str) -> None:
|
|||
########################ADDRESSES#######################
|
||||
|
||||
|
||||
async def get_derive_address(wallet_id: str, num: int):
|
||||
wallet = await get_watch_wallet(wallet_id)
|
||||
key = wallet.masterpub
|
||||
desc, network = parse_key(key)
|
||||
return desc.derive(num).address(network=network)
|
||||
|
||||
|
||||
async def get_fresh_address(wallet_id: str) -> Optional[Addresses]:
|
||||
async def get_fresh_address(wallet_id: str) -> Optional[Address]:
|
||||
wallet = await get_watch_wallet(wallet_id)
|
||||
|
||||
if not wallet:
|
||||
return None
|
||||
|
||||
address = await get_derive_address(wallet_id, wallet.address_no + 1)
|
||||
wallet_addresses = await get_addresses(wallet_id)
|
||||
receive_addresses = list(
|
||||
filter(
|
||||
lambda addr: addr.branch_index == 0 and addr.amount != 0, wallet_addresses
|
||||
)
|
||||
)
|
||||
last_receive_index = (
|
||||
receive_addresses.pop().address_index if receive_addresses else -1
|
||||
)
|
||||
address_index = (
|
||||
last_receive_index
|
||||
if last_receive_index > wallet.address_no
|
||||
else wallet.address_no
|
||||
)
|
||||
|
||||
await update_watch_wallet(wallet_id=wallet_id, address_no=wallet.address_no + 1)
|
||||
masterpub_id = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
address = await get_address_at_index(wallet_id, 0, address_index + 1)
|
||||
|
||||
if not address:
|
||||
addresses = await create_fresh_addresses(
|
||||
wallet_id, address_index + 1, address_index + 2
|
||||
)
|
||||
address = addresses.pop()
|
||||
|
||||
await update_watch_wallet(wallet_id, **{"address_no": address_index + 1})
|
||||
|
||||
return address
|
||||
|
||||
|
||||
async def create_fresh_addresses(
|
||||
wallet_id: str,
|
||||
start_address_index: int,
|
||||
end_address_index: int,
|
||||
change_address=False,
|
||||
) -> List[Address]:
|
||||
if start_address_index > end_address_index:
|
||||
return None
|
||||
|
||||
wallet = await get_watch_wallet(wallet_id)
|
||||
if not wallet:
|
||||
return None
|
||||
|
||||
branch_index = 1 if change_address else 0
|
||||
|
||||
for address_index in range(start_address_index, end_address_index):
|
||||
address = await derive_address(wallet.masterpub, address_index, branch_index)
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO watchonly.addresses (
|
||||
id,
|
||||
address,
|
||||
wallet,
|
||||
amount
|
||||
amount,
|
||||
branch_index,
|
||||
address_index
|
||||
)
|
||||
VALUES (?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(masterpub_id, address, wallet_id, 0),
|
||||
(urlsafe_short_hash(), address, wallet_id, 0, branch_index, address_index),
|
||||
)
|
||||
|
||||
# return fresh addresses
|
||||
rows = await db.fetchall(
|
||||
"""
|
||||
SELECT * FROM watchonly.addresses
|
||||
WHERE wallet = ? AND branch_index = ? AND address_index >= ? AND address_index < ?
|
||||
ORDER BY branch_index, address_index
|
||||
""",
|
||||
(wallet_id, branch_index, start_address_index, end_address_index),
|
||||
)
|
||||
|
||||
return await get_address(address)
|
||||
return [Address(**row) for row in rows]
|
||||
|
||||
|
||||
async def get_address(address: str) -> Optional[Addresses]:
|
||||
async def get_address(address: str) -> Optional[Address]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM watchonly.addresses WHERE address = ?", (address,)
|
||||
)
|
||||
return Addresses.from_row(row) if row else None
|
||||
return Address.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_addresses(wallet_id: str) -> List[Addresses]:
|
||||
rows = await db.fetchall(
|
||||
"SELECT * FROM watchonly.addresses WHERE wallet = ?", (wallet_id,)
|
||||
async def get_address_at_index(
|
||||
wallet_id: str, branch_index: int, address_index: int
|
||||
) -> Optional[Address]:
|
||||
row = await db.fetchone(
|
||||
"""
|
||||
SELECT * FROM watchonly.addresses
|
||||
WHERE wallet = ? AND branch_index = ? AND address_index = ?
|
||||
""",
|
||||
(
|
||||
wallet_id,
|
||||
branch_index,
|
||||
address_index,
|
||||
),
|
||||
)
|
||||
return [Addresses(**row) for row in rows]
|
||||
return Address.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_addresses(wallet_id: str) -> List[Address]:
|
||||
rows = await db.fetchall(
|
||||
"""
|
||||
SELECT * FROM watchonly.addresses WHERE wallet = ?
|
||||
ORDER BY branch_index, address_index
|
||||
""",
|
||||
(wallet_id,),
|
||||
)
|
||||
|
||||
return [Address(**row) for row in rows]
|
||||
|
||||
|
||||
async def update_address(id: str, **kwargs) -> Optional[Address]:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
|
||||
await db.execute(
|
||||
f"""UPDATE watchonly.addresses SET {q} WHERE id = ? """,
|
||||
(*kwargs.values(), id),
|
||||
)
|
||||
row = await db.fetchone("SELECT * FROM watchonly.addresses WHERE id = ?", (id))
|
||||
return Address.from_row(row) if row else None
|
||||
|
||||
|
||||
async def delete_addresses_for_wallet(wallet_id: str) -> None:
|
||||
await db.execute("DELETE FROM watchonly.addresses WHERE wallet = ?", (wallet_id,))
|
||||
|
||||
|
||||
######################CONFIG#######################
|
||||
async def create_config(user: str) -> Config:
|
||||
config = Config()
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO watchonly.config ("user", json_data)
|
||||
VALUES (?, ?)
|
||||
""",
|
||||
(user, json.dumps(config.dict())),
|
||||
)
|
||||
row = await db.fetchone(
|
||||
"""SELECT json_data FROM watchonly.config WHERE "user" = ?""", (user,)
|
||||
)
|
||||
return json.loads(row[0], object_hook=lambda d: Config(**d))
|
||||
|
||||
|
||||
async def update_config(config: Config, user: str) -> Optional[Config]:
|
||||
await db.execute(
|
||||
f"""UPDATE watchonly.config SET json_data = ? WHERE "user" = ?""",
|
||||
(json.dumps(config.dict()), user),
|
||||
)
|
||||
row = await db.fetchone(
|
||||
"""SELECT json_data FROM watchonly.config WHERE "user" = ?""", (user,)
|
||||
)
|
||||
return json.loads(row[0], object_hook=lambda d: Config(**d))
|
||||
|
||||
|
||||
async def get_config(user: str) -> Optional[Config]:
|
||||
row = await db.fetchone(
|
||||
"""SELECT json_data FROM watchonly.config WHERE "user" = ?""", (user,)
|
||||
)
|
||||
return json.loads(row[0], object_hook=lambda d: Config(**d)) if row else None
|
||||
|
||||
|
||||
######################MEMPOOL#######################
|
||||
|
||||
|
||||
### TODO: fix statspay dependcy and remove
|
||||
async def create_mempool(user: str) -> Optional[Mempool]:
|
||||
await db.execute(
|
||||
"""
|
||||
|
@ -192,6 +259,7 @@ async def create_mempool(user: str) -> Optional[Mempool]:
|
|||
return Mempool.from_row(row) if row else None
|
||||
|
||||
|
||||
### TODO: fix statspay dependcy and remove
|
||||
async def update_mempool(user: str, **kwargs) -> Optional[Mempool]:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
|
||||
|
@ -205,6 +273,7 @@ async def update_mempool(user: str, **kwargs) -> Optional[Mempool]:
|
|||
return Mempool.from_row(row) if row else None
|
||||
|
||||
|
||||
### TODO: fix statspay dependcy and remove
|
||||
async def get_mempool(user: str) -> Mempool:
|
||||
row = await db.fetchone(
|
||||
"""SELECT * FROM watchonly.mempool WHERE "user" = ?""", (user,)
|
||||
|
|
69
lnbits/extensions/watchonly/helpers.py
Normal file
69
lnbits/extensions/watchonly/helpers.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
from embit.descriptor import Descriptor, Key # type: ignore
|
||||
from embit.descriptor.arguments import AllowedDerivation # type: ignore
|
||||
from embit.networks import NETWORKS # type: ignore
|
||||
|
||||
|
||||
def detect_network(k):
|
||||
version = k.key.version
|
||||
for network_name in NETWORKS:
|
||||
net = NETWORKS[network_name]
|
||||
# not found in this network
|
||||
if version in [net["xpub"], net["ypub"], net["zpub"], net["Zpub"], net["Ypub"]]:
|
||||
return net
|
||||
|
||||
|
||||
def parse_key(masterpub: str) -> Descriptor:
|
||||
"""Parses masterpub or descriptor and returns a tuple: (Descriptor, network)
|
||||
To create addresses use descriptor.derive(num).address(network=network)
|
||||
"""
|
||||
network = None
|
||||
# probably a single key
|
||||
if "(" not in masterpub:
|
||||
k = Key.from_string(masterpub)
|
||||
if not k.is_extended:
|
||||
raise ValueError("The key is not a master public key")
|
||||
if k.is_private:
|
||||
raise ValueError("Private keys are not allowed")
|
||||
# check depth
|
||||
if k.key.depth != 3:
|
||||
raise ValueError(
|
||||
"Non-standard depth. Only bip44, bip49 and bip84 are supported with bare xpubs. For custom derivation paths use descriptors."
|
||||
)
|
||||
# if allowed derivation is not provided use default /{0,1}/*
|
||||
if k.allowed_derivation is None:
|
||||
k.allowed_derivation = AllowedDerivation.default()
|
||||
# get version bytes
|
||||
version = k.key.version
|
||||
for network_name in NETWORKS:
|
||||
net = NETWORKS[network_name]
|
||||
# not found in this network
|
||||
if version in [net["xpub"], net["ypub"], net["zpub"]]:
|
||||
network = net
|
||||
if version == net["xpub"]:
|
||||
desc = Descriptor.from_string("pkh(%s)" % str(k))
|
||||
elif version == net["ypub"]:
|
||||
desc = Descriptor.from_string("sh(wpkh(%s))" % str(k))
|
||||
elif version == net["zpub"]:
|
||||
desc = Descriptor.from_string("wpkh(%s)" % str(k))
|
||||
break
|
||||
# we didn't find correct version
|
||||
if network is None:
|
||||
raise ValueError("Unknown master public key version")
|
||||
else:
|
||||
desc = Descriptor.from_string(masterpub)
|
||||
if not desc.is_wildcard:
|
||||
raise ValueError("Descriptor should have wildcards")
|
||||
for k in desc.keys:
|
||||
if k.is_extended:
|
||||
net = detect_network(k)
|
||||
if net is None:
|
||||
raise ValueError(f"Unknown version: {k}")
|
||||
if network is not None and network != net:
|
||||
raise ValueError("Keys from different networks")
|
||||
network = net
|
||||
return desc, network
|
||||
|
||||
|
||||
async def derive_address(masterpub: str, num: int, branch_index=0):
|
||||
desc, network = parse_key(masterpub)
|
||||
return desc.derive(num, branch_index).address(network=network)
|
|
@ -1,5 +1,5 @@
|
|||
from sqlite3 import Row
|
||||
|
||||
from typing import List
|
||||
from fastapi.param_functions import Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
@ -9,19 +9,22 @@ class CreateWallet(BaseModel):
|
|||
title: str = Query("")
|
||||
|
||||
|
||||
class Wallets(BaseModel):
|
||||
class WalletAccount(BaseModel):
|
||||
id: str
|
||||
user: str
|
||||
masterpub: str
|
||||
fingerprint: str
|
||||
title: str
|
||||
address_no: int
|
||||
balance: int
|
||||
type: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Wallets":
|
||||
def from_row(cls, row: Row) -> "WalletAccount":
|
||||
return cls(**dict(row))
|
||||
|
||||
|
||||
### TODO: fix statspay dependcy and remove
|
||||
class Mempool(BaseModel):
|
||||
user: str
|
||||
endpoint: str
|
||||
|
@ -31,12 +34,55 @@ class Mempool(BaseModel):
|
|||
return cls(**dict(row))
|
||||
|
||||
|
||||
class Addresses(BaseModel):
|
||||
class Address(BaseModel):
|
||||
id: str
|
||||
address: str
|
||||
wallet: str
|
||||
amount: int
|
||||
amount: int = 0
|
||||
branch_index: int = 0
|
||||
address_index: int
|
||||
note: str = None
|
||||
has_activity: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Addresses":
|
||||
def from_row(cls, row: Row) -> "Address":
|
||||
return cls(**dict(row))
|
||||
|
||||
|
||||
class TransactionInput(BaseModel):
|
||||
tx_id: str
|
||||
vout: int
|
||||
amount: int
|
||||
address: str
|
||||
branch_index: int
|
||||
address_index: int
|
||||
masterpub_fingerprint: str
|
||||
tx_hex: str
|
||||
|
||||
|
||||
class TransactionOutput(BaseModel):
|
||||
amount: int
|
||||
address: str
|
||||
branch_index: int = None
|
||||
address_index: int = None
|
||||
masterpub_fingerprint: str = None
|
||||
|
||||
|
||||
class MasterPublicKey(BaseModel):
|
||||
public_key: str
|
||||
fingerprint: str
|
||||
|
||||
|
||||
class CreatePsbt(BaseModel):
|
||||
masterpubs: List[MasterPublicKey]
|
||||
inputs: List[TransactionInput]
|
||||
outputs: List[TransactionOutput]
|
||||
fee_rate: int
|
||||
tx_size: int
|
||||
|
||||
|
||||
class Config(BaseModel):
|
||||
mempool_endpoint = "https://mempool.space"
|
||||
receive_gap_limit = 20
|
||||
change_gap_limit = 5
|
||||
sats_denominated = True
|
||||
|
|
726
lnbits/extensions/watchonly/static/js/index.js
Normal file
726
lnbits/extensions/watchonly/static/js/index.js
Normal file
|
@ -0,0 +1,726 @@
|
|||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
Vue.filter('reverse', function (value) {
|
||||
// slice to make a copy of array, then reverse the copy
|
||||
return value.slice().reverse()
|
||||
})
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
DUST_LIMIT: 546,
|
||||
filter: '',
|
||||
|
||||
scan: {
|
||||
scanning: false,
|
||||
scanCount: 0,
|
||||
scanIndex: 0
|
||||
},
|
||||
|
||||
currentAddress: null,
|
||||
|
||||
tab: 'addresses',
|
||||
|
||||
config: {
|
||||
data: {
|
||||
mempool_endpoint: 'https://mempool.space',
|
||||
receive_gap_limit: 20,
|
||||
change_gap_limit: 5
|
||||
},
|
||||
DEFAULT_RECEIVE_GAP_LIMIT: 20,
|
||||
show: false
|
||||
},
|
||||
|
||||
formDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
},
|
||||
|
||||
qrCodeDialog: {
|
||||
show: false,
|
||||
data: null
|
||||
},
|
||||
...tables,
|
||||
...tableData
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
//################### CONFIG ###################
|
||||
getConfig: async function () {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
'/watchonly/api/v1/config',
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
this.config.data = data
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
updateConfig: async function () {
|
||||
const wallet = this.g.user.wallets[0]
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'PUT',
|
||||
'/watchonly/api/v1/config',
|
||||
wallet.adminkey,
|
||||
this.config.data
|
||||
)
|
||||
this.config.show = false
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
|
||||
//################### WALLETS ###################
|
||||
getWalletName: function (walletId) {
|
||||
const wallet = this.walletAccounts.find(wl => wl.id === walletId)
|
||||
return wallet ? wallet.title : 'unknown'
|
||||
},
|
||||
addWalletAccount: async function () {
|
||||
const wallet = this.g.user.wallets[0]
|
||||
const data = _.omit(this.formDialog.data, 'wallet')
|
||||
await this.createWalletAccount(wallet, data)
|
||||
},
|
||||
createWalletAccount: async function (wallet, data) {
|
||||
try {
|
||||
const response = await LNbits.api.request(
|
||||
'POST',
|
||||
'/watchonly/api/v1/wallet',
|
||||
wallet.adminkey,
|
||||
data
|
||||
)
|
||||
this.walletAccounts.push(mapWalletAccount(response.data))
|
||||
this.formDialog.show = false
|
||||
await this.refreshWalletAccounts()
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
deleteWalletAccount: function (linkId) {
|
||||
LNbits.utils
|
||||
.confirmDialog(
|
||||
'Are you sure you want to delete this watch only wallet?'
|
||||
)
|
||||
.onOk(async () => {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'DELETE',
|
||||
'/watchonly/api/v1/wallet/' + linkId,
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
this.walletAccounts = _.reject(this.walletAccounts, function (obj) {
|
||||
return obj.id === linkId
|
||||
})
|
||||
await this.refreshWalletAccounts()
|
||||
await this.refreshAddresses()
|
||||
console.log('### 111')
|
||||
await this.scanAddressWithAmount()
|
||||
} catch (error) {
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Error while deleting wallet account. Please try again.',
|
||||
timeout: 10000
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
getAddressesForWallet: async function (walletId) {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
'/watchonly/api/v1/addresses/' + walletId,
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
return data.map(mapAddressesData)
|
||||
} catch (err) {
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message: `Failed to fetch addresses for wallet with id ${walletId}.`,
|
||||
timeout: 10000
|
||||
})
|
||||
LNbits.utils.notifyApiError(err)
|
||||
}
|
||||
return []
|
||||
},
|
||||
getWatchOnlyWallets: async function () {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
'/watchonly/api/v1/wallet',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
return data
|
||||
} catch (error) {
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Failed to fetch wallets.',
|
||||
timeout: 10000
|
||||
})
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
return []
|
||||
},
|
||||
refreshWalletAccounts: async function () {
|
||||
const wallets = await this.getWatchOnlyWallets()
|
||||
this.walletAccounts = wallets.map(w => mapWalletAccount(w))
|
||||
},
|
||||
getAmmountForWallet: function (walletId) {
|
||||
const amount = this.addresses.data
|
||||
.filter(a => a.wallet === walletId)
|
||||
.reduce((t, a) => t + a.amount || 0, 0)
|
||||
return this.satBtc(amount)
|
||||
},
|
||||
|
||||
//################### ADDRESSES ###################
|
||||
|
||||
refreshAddresses: async function () {
|
||||
const wallets = await this.getWatchOnlyWallets()
|
||||
this.addresses.data = []
|
||||
for (const {id, type} of wallets) {
|
||||
const newAddresses = await this.getAddressesForWallet(id)
|
||||
const uniqueAddresses = newAddresses.filter(
|
||||
newAddr =>
|
||||
!this.addresses.data.find(a => a.address === newAddr.address)
|
||||
)
|
||||
|
||||
const lastAcctiveAddress =
|
||||
uniqueAddresses
|
||||
.filter(a => a.addressIndex === 0 && a.hasActivity)
|
||||
.pop() || {}
|
||||
|
||||
uniqueAddresses.forEach(a => {
|
||||
a.expanded = false
|
||||
a.accountType = type
|
||||
a.gapLimitExceeded =
|
||||
a.branchIndex === 0 &&
|
||||
a.addressIndex >
|
||||
lastAcctiveAddress.addressIndex +
|
||||
this.config.DEFAULT_RECEIVE_GAP_LIMIT
|
||||
})
|
||||
this.addresses.data.push(...uniqueAddresses)
|
||||
}
|
||||
},
|
||||
updateAmountForAddress: async function (addressData, amount = 0) {
|
||||
try {
|
||||
const wallet = this.g.user.wallets[0]
|
||||
addressData.amount = amount
|
||||
if (addressData.branchIndex === 0) {
|
||||
const addressWallet = this.walletAccounts.find(
|
||||
w => w.id === addressData.wallet
|
||||
)
|
||||
if (
|
||||
addressWallet &&
|
||||
addressWallet.address_no < addressData.addressIndex
|
||||
) {
|
||||
addressWallet.address_no = addressData.addressIndex
|
||||
}
|
||||
}
|
||||
|
||||
await LNbits.api.request(
|
||||
'PUT',
|
||||
`/watchonly/api/v1/address/${addressData.id}`,
|
||||
wallet.adminkey,
|
||||
{amount}
|
||||
)
|
||||
} catch (err) {
|
||||
addressData.error = 'Failed to refresh amount for address'
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message: `Failed to refresh amount for address ${addressData.address}`,
|
||||
timeout: 10000
|
||||
})
|
||||
LNbits.utils.notifyApiError(err)
|
||||
}
|
||||
},
|
||||
updateNoteForAddress: async function (addressData, note) {
|
||||
try {
|
||||
const wallet = this.g.user.wallets[0]
|
||||
await LNbits.api.request(
|
||||
'PUT',
|
||||
`/watchonly/api/v1/address/${addressData.id}`,
|
||||
wallet.adminkey,
|
||||
{note: addressData.note}
|
||||
)
|
||||
const updatedAddress =
|
||||
this.addresses.data.find(a => a.id === addressData.id) || {}
|
||||
updatedAddress.note = note
|
||||
} catch (err) {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
}
|
||||
},
|
||||
getFilteredAddresses: function () {
|
||||
const selectedWalletId = this.addresses.selectedWallet?.id
|
||||
const filter = this.addresses.filterValues || []
|
||||
const includeChangeAddrs = filter.includes('Show Change Addresses')
|
||||
const includeGapAddrs = filter.includes('Show Gap Addresses')
|
||||
const excludeNoAmount = filter.includes('Only With Amount')
|
||||
|
||||
const walletsLimit = this.walletAccounts.reduce((r, w) => {
|
||||
r[`_${w.id}`] = w.address_no
|
||||
return r
|
||||
}, {})
|
||||
|
||||
const addresses = this.addresses.data.filter(
|
||||
a =>
|
||||
(includeChangeAddrs || a.addressIndex === 0) &&
|
||||
(includeGapAddrs ||
|
||||
a.addressIndex === 1 ||
|
||||
a.addressIndex <= walletsLimit[`_${a.wallet}`]) &&
|
||||
!(excludeNoAmount && a.amount === 0) &&
|
||||
(!selectedWalletId || a.wallet === selectedWalletId)
|
||||
)
|
||||
return addresses
|
||||
},
|
||||
openGetFreshAddressDialog: async function (walletId) {
|
||||
const {data: addressData} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/watchonly/api/v1/address/${walletId}`,
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
|
||||
addressData.note = `Shared on ${currentDateTime()}`
|
||||
const lastAcctiveAddress =
|
||||
this.addresses.data
|
||||
.filter(
|
||||
a =>
|
||||
a.wallet === addressData.wallet &&
|
||||
a.addressIndex === 0 &&
|
||||
a.hasActivity
|
||||
)
|
||||
.pop() || {}
|
||||
addressData.gapLimitExceeded =
|
||||
addressData.addressIndex === 0 &&
|
||||
addressData.addressIndex >
|
||||
lastAcctiveAddress.addressIndex +
|
||||
this.config.DEFAULT_RECEIVE_GAP_LIMIT
|
||||
|
||||
this.openQrCodeDialog(addressData)
|
||||
const wallet = this.walletAccounts.find(w => w.id === walletId) || {}
|
||||
wallet.address_no = addressData.addressIndex
|
||||
await this.refreshAddresses()
|
||||
},
|
||||
|
||||
//################### ADDRESS HISTORY ###################
|
||||
addressHistoryFromTxs: function (addressData, txs) {
|
||||
const addressHistory = []
|
||||
txs.forEach(tx => {
|
||||
const sent = tx.vin
|
||||
.filter(
|
||||
vin => vin.prevout.scriptpubkey_address === addressData.address
|
||||
)
|
||||
.map(vin => mapInputToSentHistory(tx, addressData, vin))
|
||||
|
||||
const received = tx.vout
|
||||
.filter(vout => vout.scriptpubkey_address === addressData.address)
|
||||
.map(vout => mapOutputToReceiveHistory(tx, addressData, vout))
|
||||
addressHistory.push(...sent, ...received)
|
||||
})
|
||||
return addressHistory
|
||||
},
|
||||
getFilteredAddressesHistory: function () {
|
||||
return this.addresses.history.filter(
|
||||
a => (!a.isChange || a.sent) && !a.isSubItem
|
||||
)
|
||||
},
|
||||
exportHistoryToCSV: function () {
|
||||
const history = this.getFilteredAddressesHistory().map(a => ({
|
||||
...a,
|
||||
action: a.sent ? 'Sent' : 'Received'
|
||||
}))
|
||||
LNbits.utils.exportCSV(
|
||||
this.historyTable.exportColums,
|
||||
history,
|
||||
'address-history'
|
||||
)
|
||||
},
|
||||
markSameTxAddressHistory: function () {
|
||||
this.addresses.history
|
||||
.filter(s => s.sent)
|
||||
.forEach((el, i, arr) => {
|
||||
if (el.isSubItem) return
|
||||
|
||||
const sameTxItems = arr.slice(i + 1).filter(e => e.txId === el.txId)
|
||||
if (!sameTxItems.length) return
|
||||
sameTxItems.forEach(e => {
|
||||
e.isSubItem = true
|
||||
})
|
||||
|
||||
el.totalAmount =
|
||||
el.amount + sameTxItems.reduce((t, e) => (t += e.amount || 0), 0)
|
||||
el.sameTxItems = sameTxItems
|
||||
})
|
||||
},
|
||||
showAddressHistoryDetails: function (addressHistory) {
|
||||
addressHistory.expanded = true
|
||||
},
|
||||
|
||||
//################### PAYMENT ###################
|
||||
createTx: function (excludeChange = false) {
|
||||
const tx = {
|
||||
fee_rate: this.payment.feeRate,
|
||||
tx_size: this.payment.txSize,
|
||||
masterpubs: this.walletAccounts.map(w => ({
|
||||
public_key: w.masterpub,
|
||||
fingerprint: w.fingerprint
|
||||
}))
|
||||
}
|
||||
tx.inputs = this.utxos.data
|
||||
.filter(utxo => utxo.selected)
|
||||
.map(mapUtxoToPsbtInput)
|
||||
.sort((a, b) =>
|
||||
a.tx_id < b.tx_id ? -1 : a.tx_id > b.tx_id ? 1 : a.vout - b.vout
|
||||
)
|
||||
|
||||
tx.outputs = this.payment.data.map(out => ({
|
||||
address: out.address,
|
||||
amount: out.amount
|
||||
}))
|
||||
|
||||
if (excludeChange) {
|
||||
this.payment.changeAmount = 0
|
||||
} else {
|
||||
const change = this.createChangeOutput()
|
||||
this.payment.changeAmount = change.amount
|
||||
if (change.amount >= this.DUST_LIMIT) {
|
||||
tx.outputs.push(change)
|
||||
}
|
||||
}
|
||||
// Only sort by amount on UI level (no lib for address decode)
|
||||
// Should sort by scriptPubKey (as byte array) on the backend
|
||||
tx.outputs.sort((a, b) => a.amount - b.amount)
|
||||
|
||||
return tx
|
||||
},
|
||||
createChangeOutput: function () {
|
||||
const change = this.payment.changeAddress
|
||||
const fee = this.payment.feeRate * this.payment.txSize
|
||||
const inputAmount = this.getTotalSelectedUtxoAmount()
|
||||
const payedAmount = this.getTotalPaymentAmount()
|
||||
const walletAcount =
|
||||
this.walletAccounts.find(w => w.id === change.wallet) || {}
|
||||
|
||||
return {
|
||||
address: change.address,
|
||||
amount: inputAmount - payedAmount - fee,
|
||||
addressIndex: change.addressIndex,
|
||||
addressIndex: change.addressIndex,
|
||||
masterpub_fingerprint: walletAcount.fingerprint
|
||||
}
|
||||
},
|
||||
computeFee: function () {
|
||||
const tx = this.createTx()
|
||||
this.payment.txSize = Math.round(txSize(tx))
|
||||
return this.payment.feeRate * this.payment.txSize
|
||||
},
|
||||
createPsbt: async function () {
|
||||
const wallet = this.g.user.wallets[0]
|
||||
try {
|
||||
this.computeFee()
|
||||
const tx = this.createTx()
|
||||
txSize(tx)
|
||||
for (const input of tx.inputs) {
|
||||
input.tx_hex = await this.fetchTxHex(input.tx_id)
|
||||
}
|
||||
|
||||
const {data} = await LNbits.api.request(
|
||||
'POST',
|
||||
'/watchonly/api/v1/psbt',
|
||||
wallet.adminkey,
|
||||
tx
|
||||
)
|
||||
|
||||
this.payment.psbtBase64 = data
|
||||
} catch (err) {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
}
|
||||
},
|
||||
deletePaymentAddress: function (v) {
|
||||
const index = this.payment.data.indexOf(v)
|
||||
if (index !== -1) {
|
||||
this.payment.data.splice(index, 1)
|
||||
}
|
||||
},
|
||||
initPaymentData: async function () {
|
||||
if (!this.payment.show) return
|
||||
await this.refreshAddresses()
|
||||
|
||||
this.payment.showAdvanced = false
|
||||
this.payment.changeWallet = this.walletAccounts[0]
|
||||
this.selectChangeAccount(this.payment.changeWallet)
|
||||
|
||||
await this.refreshRecommendedFees()
|
||||
this.payment.feeRate = this.payment.recommededFees.halfHourFee
|
||||
},
|
||||
getFeeRateLabel: function (feeRate) {
|
||||
const fees = this.payment.recommededFees
|
||||
if (feeRate >= fees.fastestFee) return `High Priority (${feeRate} sat/vB)`
|
||||
if (feeRate >= fees.halfHourFee)
|
||||
return `Medium Priority (${feeRate} sat/vB)`
|
||||
if (feeRate >= fees.hourFee) return `Low Priority (${feeRate} sat/vB)`
|
||||
return `No Priority (${feeRate} sat/vB)`
|
||||
},
|
||||
addPaymentAddress: function () {
|
||||
this.payment.data.push({address: '', amount: undefined})
|
||||
},
|
||||
getTotalPaymentAmount: function () {
|
||||
return this.payment.data.reduce((t, a) => t + (a.amount || 0), 0)
|
||||
},
|
||||
selectChangeAccount: function (wallet) {
|
||||
this.payment.changeAddress =
|
||||
this.addresses.data.find(
|
||||
a => a.wallet === wallet.id && a.addressIndex === 1 && !a.hasActivity
|
||||
) || {}
|
||||
},
|
||||
goToPaymentView: async function () {
|
||||
this.payment.show = true
|
||||
this.tab = 'utxos'
|
||||
await this.initPaymentData()
|
||||
},
|
||||
sendMaxToAddress: function (paymentAddress = {}) {
|
||||
paymentAddress.amount = 0
|
||||
const tx = this.createTx(true)
|
||||
this.payment.txSize = Math.round(txSize(tx))
|
||||
const fee = this.payment.feeRate * this.payment.txSize
|
||||
const inputAmount = this.getTotalSelectedUtxoAmount()
|
||||
const payedAmount = this.getTotalPaymentAmount()
|
||||
paymentAddress.amount = Math.max(0, inputAmount - payedAmount - fee)
|
||||
},
|
||||
|
||||
//################### UTXOs ###################
|
||||
scanAllAddresses: async function () {
|
||||
await this.refreshAddresses()
|
||||
this.addresses.history = []
|
||||
let addresses = this.addresses.data
|
||||
this.utxos.data = []
|
||||
this.utxos.total = 0
|
||||
// Loop while new funds are found on the gap adresses.
|
||||
// Use 1000 limit as a safety check (scan 20 000 addresses max)
|
||||
for (let i = 0; i < 1000 && addresses.length; i++) {
|
||||
await this.updateUtxosForAddresses(addresses)
|
||||
const oldAddresses = this.addresses.data.slice()
|
||||
await this.refreshAddresses()
|
||||
const newAddresses = this.addresses.data.slice()
|
||||
// check if gap addresses have been extended
|
||||
addresses = newAddresses.filter(
|
||||
newAddr => !oldAddresses.find(oldAddr => oldAddr.id === newAddr.id)
|
||||
)
|
||||
if (addresses.length) {
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Funds found! Scanning for more...',
|
||||
timeout: 10000
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
scanAddressWithAmount: async function () {
|
||||
this.utxos.data = []
|
||||
this.utxos.total = 0
|
||||
this.addresses.history = []
|
||||
const addresses = this.addresses.data.filter(a => a.hasActivity)
|
||||
await this.updateUtxosForAddresses(addresses)
|
||||
},
|
||||
scanAddress: async function (addressData) {
|
||||
this.updateUtxosForAddresses([addressData])
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Address Rescanned',
|
||||
timeout: 10000
|
||||
})
|
||||
},
|
||||
updateUtxosForAddresses: async function (addresses = []) {
|
||||
this.scan = {scanning: true, scanCount: addresses.length, scanIndex: 0}
|
||||
|
||||
try {
|
||||
for (addrData of addresses) {
|
||||
const addressHistory = await this.getAddressTxsDelayed(addrData)
|
||||
// remove old entries
|
||||
this.addresses.history = this.addresses.history.filter(
|
||||
h => h.address !== addrData.address
|
||||
)
|
||||
|
||||
// add new entrie
|
||||
this.addresses.history.push(...addressHistory)
|
||||
this.addresses.history.sort((a, b) =>
|
||||
!a.height ? -1 : b.height - a.height
|
||||
)
|
||||
this.markSameTxAddressHistory()
|
||||
|
||||
if (addressHistory.length) {
|
||||
// search only if it ever had any activity
|
||||
const utxos = await this.getAddressTxsUtxoDelayed(addrData.address)
|
||||
this.updateUtxosForAddress(addrData, utxos)
|
||||
}
|
||||
|
||||
this.scan.scanIndex++
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Failed to scan addresses',
|
||||
timeout: 10000
|
||||
})
|
||||
} finally {
|
||||
this.scan.scanning = false
|
||||
}
|
||||
},
|
||||
updateUtxosForAddress: function (addressData, utxos = []) {
|
||||
const wallet =
|
||||
this.walletAccounts.find(w => w.id === addressData.wallet) || {}
|
||||
|
||||
const newUtxos = utxos.map(utxo =>
|
||||
mapAddressDataToUtxo(wallet, addressData, utxo)
|
||||
)
|
||||
// remove old utxos
|
||||
this.utxos.data = this.utxos.data.filter(
|
||||
u => u.address !== addressData.address
|
||||
)
|
||||
// add new utxos
|
||||
this.utxos.data.push(...newUtxos)
|
||||
if (utxos.length) {
|
||||
this.utxos.data.sort((a, b) => b.sort - a.sort)
|
||||
this.utxos.total = this.utxos.data.reduce(
|
||||
(total, y) => (total += y?.amount || 0),
|
||||
0
|
||||
)
|
||||
}
|
||||
const addressTotal = utxos.reduce(
|
||||
(total, y) => (total += y?.value || 0),
|
||||
0
|
||||
)
|
||||
this.updateAmountForAddress(addressData, addressTotal)
|
||||
},
|
||||
getTotalSelectedUtxoAmount: function () {
|
||||
const total = this.utxos.data
|
||||
.filter(u => u.selected)
|
||||
.reduce((t, a) => t + (a.amount || 0), 0)
|
||||
return total
|
||||
},
|
||||
applyUtxoSelectionMode: function () {
|
||||
const payedAmount = this.getTotalPaymentAmount()
|
||||
const mode = this.payment.utxoSelectionMode
|
||||
this.utxos.data.forEach(u => (u.selected = false))
|
||||
const isManual = mode === 'Manual'
|
||||
if (isManual || !payedAmount) return
|
||||
|
||||
const isSelectAll = mode === 'Select All'
|
||||
if (isSelectAll || payedAmount >= this.utxos.total) {
|
||||
this.utxos.data.forEach(u => (u.selected = true))
|
||||
return
|
||||
}
|
||||
const isSmallerFirst = mode === 'Smaller Inputs First'
|
||||
const isLargerFirst = mode === 'Larger Inputs First'
|
||||
|
||||
let selectedUtxos = this.utxos.data.slice()
|
||||
if (isSmallerFirst || isLargerFirst) {
|
||||
const sortFn = isSmallerFirst
|
||||
? (a, b) => a.amount - b.amount
|
||||
: (a, b) => b.amount - a.amount
|
||||
selectedUtxos.sort(sortFn)
|
||||
} else {
|
||||
// default to random order
|
||||
selectedUtxos = _.shuffle(selectedUtxos)
|
||||
}
|
||||
selectedUtxos.reduce((total, utxo) => {
|
||||
utxo.selected = total < payedAmount
|
||||
total += utxo.amount
|
||||
return total
|
||||
}, 0)
|
||||
},
|
||||
|
||||
//################### MEMPOOL API ###################
|
||||
getAddressTxsDelayed: async function (addrData) {
|
||||
const {
|
||||
bitcoin: {addresses: addressesAPI}
|
||||
} = mempoolJS()
|
||||
|
||||
const fn = async () =>
|
||||
addressesAPI.getAddressTxs({
|
||||
address: addrData.address
|
||||
})
|
||||
const addressTxs = await retryWithDelay(fn)
|
||||
return this.addressHistoryFromTxs(addrData, addressTxs)
|
||||
},
|
||||
|
||||
refreshRecommendedFees: async function () {
|
||||
const {
|
||||
bitcoin: {fees: feesAPI}
|
||||
} = mempoolJS()
|
||||
|
||||
const fn = async () => feesAPI.getFeesRecommended()
|
||||
this.payment.recommededFees = await retryWithDelay(fn)
|
||||
},
|
||||
getAddressTxsUtxoDelayed: async function (address) {
|
||||
const {
|
||||
bitcoin: {addresses: addressesAPI}
|
||||
} = mempoolJS()
|
||||
|
||||
const fn = async () =>
|
||||
addressesAPI.getAddressTxsUtxo({
|
||||
address
|
||||
})
|
||||
return retryWithDelay(fn)
|
||||
},
|
||||
fetchTxHex: async function (txId) {
|
||||
const {
|
||||
bitcoin: {transactions: transactionsAPI}
|
||||
} = mempoolJS()
|
||||
|
||||
try {
|
||||
const response = await transactionsAPI.getTxHex({txid: txId})
|
||||
return response
|
||||
} catch (error) {
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message: `Failed to fetch transaction details for tx id: '${txId}'`,
|
||||
timeout: 10000
|
||||
})
|
||||
LNbits.utils.notifyApiError(error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
//################### OTHER ###################
|
||||
closeFormDialog: function () {
|
||||
this.formDialog.data = {
|
||||
is_unique: false
|
||||
}
|
||||
},
|
||||
openQrCodeDialog: function (addressData) {
|
||||
this.currentAddress = addressData
|
||||
this.addresses.note = addressData.note || ''
|
||||
this.addresses.show = true
|
||||
},
|
||||
searchInTab: function (tab, value) {
|
||||
this.tab = tab
|
||||
this[`${tab}Table`].filter = value
|
||||
},
|
||||
|
||||
satBtc(val, showUnit = true) {
|
||||
const value = this.config.data.sats_denominated
|
||||
? LNbits.utils.formatSat(val)
|
||||
: val == 0
|
||||
? 0.0
|
||||
: (val / 100000000).toFixed(8)
|
||||
if (!showUnit) return value
|
||||
return this.config.data.sats_denominated ? value + ' sat' : value + ' BTC'
|
||||
},
|
||||
getAccountDescription: function (accountType) {
|
||||
return getAccountDescription(accountType)
|
||||
}
|
||||
},
|
||||
created: async function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
await this.getConfig()
|
||||
await this.refreshWalletAccounts()
|
||||
await this.refreshAddresses()
|
||||
await this.scanAddressWithAmount()
|
||||
}
|
||||
}
|
||||
})
|
81
lnbits/extensions/watchonly/static/js/map.js
Normal file
81
lnbits/extensions/watchonly/static/js/map.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
const mapAddressesData = a => ({
|
||||
id: a.id,
|
||||
address: a.address,
|
||||
amount: a.amount,
|
||||
wallet: a.wallet,
|
||||
note: a.note,
|
||||
|
||||
branchIndex: a.branch_index,
|
||||
addressIndex: a.address_index,
|
||||
hasActivity: a.has_activity
|
||||
})
|
||||
|
||||
const mapInputToSentHistory = (tx, addressData, vin) => ({
|
||||
sent: true,
|
||||
txId: tx.txid,
|
||||
address: addressData.address,
|
||||
isChange: addressData.branchIndex === 1,
|
||||
amount: vin.prevout.value,
|
||||
date: blockTimeToDate(tx.status.block_time),
|
||||
height: tx.status.block_height,
|
||||
confirmed: tx.status.confirmed,
|
||||
fee: tx.fee,
|
||||
expanded: false
|
||||
})
|
||||
|
||||
const mapOutputToReceiveHistory = (tx, addressData, vout) => ({
|
||||
received: true,
|
||||
txId: tx.txid,
|
||||
address: addressData.address,
|
||||
isChange: addressData.branchIndex === 1,
|
||||
amount: vout.value,
|
||||
date: blockTimeToDate(tx.status.block_time),
|
||||
height: tx.status.block_height,
|
||||
confirmed: tx.status.confirmed,
|
||||
fee: tx.fee,
|
||||
expanded: false
|
||||
})
|
||||
|
||||
const mapUtxoToPsbtInput = utxo => ({
|
||||
tx_id: utxo.txId,
|
||||
vout: utxo.vout,
|
||||
amount: utxo.amount,
|
||||
address: utxo.address,
|
||||
branch_index: utxo.branchIndex,
|
||||
address_index: utxo.addressIndex,
|
||||
masterpub_fingerprint: utxo.masterpubFingerprint,
|
||||
accountType: utxo.accountType,
|
||||
txHex: ''
|
||||
})
|
||||
|
||||
const mapAddressDataToUtxo = (wallet, addressData, utxo) => ({
|
||||
id: addressData.id,
|
||||
address: addressData.address,
|
||||
isChange: addressData.branchIndex === 1,
|
||||
addressIndex: addressData.addressIndex,
|
||||
branchIndex: addressData.branchIndex,
|
||||
wallet: addressData.wallet,
|
||||
accountType: addressData.accountType,
|
||||
masterpubFingerprint: wallet.fingerprint,
|
||||
txId: utxo.txid,
|
||||
vout: utxo.vout,
|
||||
confirmed: utxo.status.confirmed,
|
||||
amount: utxo.value,
|
||||
date: blockTimeToDate(utxo.status?.block_time),
|
||||
sort: utxo.status?.block_time,
|
||||
expanded: false,
|
||||
selected: false
|
||||
})
|
||||
|
||||
const mapWalletAccount = function (obj) {
|
||||
obj._data = _.clone(obj)
|
||||
obj.date = obj.time
|
||||
? Quasar.utils.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
: ''
|
||||
obj.label = obj.title // for drop-downs
|
||||
obj.expanded = false
|
||||
return obj
|
||||
}
|
277
lnbits/extensions/watchonly/static/js/tables.js
Normal file
277
lnbits/extensions/watchonly/static/js/tables.js
Normal file
|
@ -0,0 +1,277 @@
|
|||
const tables = {
|
||||
walletsTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'new',
|
||||
align: 'left',
|
||||
label: ''
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
align: 'left',
|
||||
label: 'Title',
|
||||
field: 'title'
|
||||
},
|
||||
{
|
||||
name: 'amount',
|
||||
align: 'left',
|
||||
label: 'Amount'
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
align: 'left',
|
||||
label: 'Type',
|
||||
field: 'type'
|
||||
},
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
},
|
||||
filter: ''
|
||||
},
|
||||
utxosTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'expand',
|
||||
align: 'left',
|
||||
label: ''
|
||||
},
|
||||
{
|
||||
name: 'selected',
|
||||
align: 'left',
|
||||
label: ''
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
align: 'center',
|
||||
label: 'Status',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'address',
|
||||
align: 'left',
|
||||
label: 'Address',
|
||||
field: 'address',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'amount',
|
||||
align: 'left',
|
||||
label: 'Amount',
|
||||
field: 'amount',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
align: 'left',
|
||||
label: 'Date',
|
||||
field: 'date',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'wallet',
|
||||
align: 'left',
|
||||
label: 'Account',
|
||||
field: 'wallet',
|
||||
sortable: true
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
},
|
||||
filter: ''
|
||||
},
|
||||
paymentTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'data',
|
||||
align: 'left'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
},
|
||||
filter: ''
|
||||
},
|
||||
summaryTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'totalInputs',
|
||||
align: 'center',
|
||||
label: 'Selected Amount'
|
||||
},
|
||||
{
|
||||
name: 'totalOutputs',
|
||||
align: 'center',
|
||||
label: 'Payed Amount'
|
||||
},
|
||||
{
|
||||
name: 'fees',
|
||||
align: 'center',
|
||||
label: 'Fees'
|
||||
},
|
||||
{
|
||||
name: 'change',
|
||||
align: 'center',
|
||||
label: 'Change'
|
||||
}
|
||||
]
|
||||
},
|
||||
addressesTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'expand',
|
||||
align: 'left',
|
||||
label: ''
|
||||
},
|
||||
{
|
||||
name: 'address',
|
||||
align: 'left',
|
||||
label: 'Address',
|
||||
field: 'address',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'amount',
|
||||
align: 'left',
|
||||
label: 'Amount',
|
||||
field: 'amount',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'note',
|
||||
align: 'left',
|
||||
label: 'Note',
|
||||
field: 'note',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'wallet',
|
||||
align: 'left',
|
||||
label: 'Account',
|
||||
field: 'wallet',
|
||||
sortable: true
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 0,
|
||||
sortBy: 'amount',
|
||||
descending: true
|
||||
},
|
||||
filter: ''
|
||||
},
|
||||
historyTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'expand',
|
||||
align: 'left',
|
||||
label: ''
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
align: 'left',
|
||||
label: 'Status'
|
||||
},
|
||||
{
|
||||
name: 'amount',
|
||||
align: 'left',
|
||||
label: 'Amount',
|
||||
field: 'amount',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'address',
|
||||
align: 'left',
|
||||
label: 'Address',
|
||||
field: 'address',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
align: 'left',
|
||||
label: 'Date',
|
||||
field: 'date',
|
||||
sortable: true
|
||||
}
|
||||
],
|
||||
exportColums: [
|
||||
{
|
||||
label: 'Action',
|
||||
field: 'action'
|
||||
},
|
||||
{
|
||||
label: 'Date&Time',
|
||||
field: 'date'
|
||||
},
|
||||
{
|
||||
label: 'Amount',
|
||||
field: 'amount'
|
||||
},
|
||||
{
|
||||
label: 'Fee',
|
||||
field: 'fee'
|
||||
},
|
||||
{
|
||||
label: 'Transaction Id',
|
||||
field: 'txId'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 0
|
||||
},
|
||||
filter: ''
|
||||
}
|
||||
}
|
||||
|
||||
const tableData = {
|
||||
walletAccounts: [],
|
||||
addresses: {
|
||||
show: false,
|
||||
data: [],
|
||||
history: [],
|
||||
selectedWallet: null,
|
||||
note: '',
|
||||
filterOptions: [
|
||||
'Show Change Addresses',
|
||||
'Show Gap Addresses',
|
||||
'Only With Amount'
|
||||
],
|
||||
filterValues: []
|
||||
},
|
||||
utxos: {
|
||||
data: [],
|
||||
total: 0
|
||||
},
|
||||
payment: {
|
||||
data: [{address: '', amount: undefined}],
|
||||
changeWallet: null,
|
||||
changeAddress: {},
|
||||
changeAmount: 0,
|
||||
|
||||
feeRate: 1,
|
||||
recommededFees: {
|
||||
fastestFee: 1,
|
||||
halfHourFee: 1,
|
||||
hourFee: 1,
|
||||
economyFee: 1,
|
||||
minimumFee: 1
|
||||
},
|
||||
fee: 0,
|
||||
txSize: 0,
|
||||
psbtBase64: '',
|
||||
utxoSelectionModes: [
|
||||
'Manual',
|
||||
'Random',
|
||||
'Select All',
|
||||
'Smaller Inputs First',
|
||||
'Larger Inputs First'
|
||||
],
|
||||
utxoSelectionMode: 'Manual',
|
||||
show: false,
|
||||
showAdvanced: false
|
||||
},
|
||||
summary: {
|
||||
data: [{totalInputs: 0, totalOutputs: 0, fees: 0, change: 0}]
|
||||
}
|
||||
}
|
99
lnbits/extensions/watchonly/static/js/utils.js
Normal file
99
lnbits/extensions/watchonly/static/js/utils.js
Normal file
|
@ -0,0 +1,99 @@
|
|||
const blockTimeToDate = blockTime =>
|
||||
blockTime ? moment(blockTime * 1000).format('LLL') : ''
|
||||
|
||||
const currentDateTime = () => moment().format('LLL')
|
||||
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
||||
|
||||
const retryWithDelay = async function (fn, retryCount = 0) {
|
||||
try {
|
||||
await sleep(25)
|
||||
// Do not return the call directly, use result.
|
||||
// Otherwise the error will not be cought in this try-catch block.
|
||||
const result = await fn()
|
||||
return result
|
||||
} catch (err) {
|
||||
if (retryCount > 100) throw err
|
||||
await sleep((retryCount + 1) * 1000)
|
||||
return retryWithDelay(fn, retryCount + 1)
|
||||
}
|
||||
}
|
||||
|
||||
const txSize = tx => {
|
||||
// https://bitcoinops.org/en/tools/calc-size/
|
||||
// overhead size
|
||||
const nVersion = 4
|
||||
const inCount = 1
|
||||
const outCount = 1
|
||||
const nlockTime = 4
|
||||
const hasSegwit = !!tx.inputs.find(inp =>
|
||||
['p2wsh', 'p2wpkh', 'p2tr'].includes(inp.accountType)
|
||||
)
|
||||
const segwitFlag = hasSegwit ? 0.5 : 0
|
||||
const overheadSize = nVersion + inCount + outCount + nlockTime + segwitFlag
|
||||
|
||||
// inputs size
|
||||
const outpoint = 36 // txId plus vout index number
|
||||
const scriptSigLength = 1
|
||||
const nSequence = 4
|
||||
const inputsSize = tx.inputs.reduce((t, inp) => {
|
||||
const scriptSig =
|
||||
inp.accountType === 'p2pkh' ? 107 : inp.accountType === 'p2sh' ? 254 : 0
|
||||
const witnessItemCount = hasSegwit ? 0.25 : 0
|
||||
const witnessItems =
|
||||
inp.accountType === 'p2wpkh'
|
||||
? 27
|
||||
: inp.accountType === 'p2wsh'
|
||||
? 63.5
|
||||
: inp.accountType === 'p2tr'
|
||||
? 16.5
|
||||
: 0
|
||||
t +=
|
||||
outpoint +
|
||||
scriptSigLength +
|
||||
nSequence +
|
||||
scriptSig +
|
||||
witnessItemCount +
|
||||
witnessItems
|
||||
return t
|
||||
}, 0)
|
||||
|
||||
// outputs size
|
||||
const nValue = 8
|
||||
const scriptPubKeyLength = 1
|
||||
|
||||
const outputsSize = tx.outputs.reduce((t, out) => {
|
||||
const type = guessAddressType(out.address)
|
||||
|
||||
const scriptPubKey =
|
||||
type === 'p2pkh'
|
||||
? 25
|
||||
: type === 'p2wpkh'
|
||||
? 22
|
||||
: type === 'p2sh'
|
||||
? 23
|
||||
: type === 'p2wsh'
|
||||
? 34
|
||||
: 34 // default to the largest size (p2tr included)
|
||||
t += nValue + scriptPubKeyLength + scriptPubKey
|
||||
return t
|
||||
}, 0)
|
||||
|
||||
return overheadSize + inputsSize + outputsSize
|
||||
}
|
||||
const guessAddressType = (a = '') => {
|
||||
if (a.startsWith('1') || a.startsWith('n')) return 'p2pkh'
|
||||
if (a.startsWith('3') || a.startsWith('2')) return 'p2sh'
|
||||
if (a.startsWith('bc1q') || a.startsWith('tb1q'))
|
||||
return a.length === 42 ? 'p2wpkh' : 'p2wsh'
|
||||
if (a.startsWith('bc1p') || a.startsWith('tb1p')) return 'p2tr'
|
||||
}
|
||||
// bc1qwpdwyaqweavs5fpa8rujqaxmrmt9lvmydmcnt7
|
||||
const ACCOUNT_TYPES = {
|
||||
p2tr: 'Taproot, BIP86, P2TR, Bech32m',
|
||||
p2wpkh: 'SegWit, BIP84, P2WPKH, Bech32',
|
||||
p2sh: 'BIP49, P2SH-P2WPKH, Base58',
|
||||
p2pkh: 'Legacy, BIP44, P2PKH, Base58'
|
||||
}
|
||||
|
||||
const getAccountDescription = type => ACCOUNT_TYPES[type] || 'nonstandard'
|
File diff suppressed because it is too large
Load Diff
|
@ -1,9 +1,15 @@
|
|||
from http import HTTPStatus
|
||||
|
||||
from fastapi import Query
|
||||
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
|
||||
|
||||
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||
from lnbits.extensions.watchonly import watchonly_ext
|
||||
|
||||
|
@ -13,12 +19,21 @@ from .crud import (
|
|||
delete_watch_wallet,
|
||||
get_addresses,
|
||||
get_fresh_address,
|
||||
create_fresh_addresses,
|
||||
update_address,
|
||||
delete_addresses_for_wallet,
|
||||
get_mempool,
|
||||
get_watch_wallet,
|
||||
get_watch_wallets,
|
||||
update_mempool,
|
||||
update_watch_wallet,
|
||||
create_config,
|
||||
get_config,
|
||||
update_config,
|
||||
)
|
||||
from .models import CreateWallet
|
||||
from .models import CreateWallet, CreatePsbt, Config
|
||||
from .helpers import parse_key
|
||||
|
||||
|
||||
###################WALLETS#############################
|
||||
|
||||
|
@ -48,18 +63,19 @@ async def api_wallet_retrieve(
|
|||
|
||||
@watchonly_ext.post("/api/v1/wallet")
|
||||
async def api_wallet_create_or_update(
|
||||
data: CreateWallet, wallet_id=None, w: WalletTypeInfo = Depends(require_admin_key)
|
||||
data: CreateWallet, w: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
try:
|
||||
wallet = await create_watch_wallet(
|
||||
user=w.wallet.user, masterpub=data.masterpub, title=data.title
|
||||
)
|
||||
await api_get_addresses(wallet.id, w)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
|
||||
|
||||
mempool = await get_mempool(w.wallet.user)
|
||||
if not mempool:
|
||||
create_mempool(user=w.wallet.user)
|
||||
config = await get_config(w.wallet.user)
|
||||
if not config:
|
||||
await create_config(user=w.wallet.user)
|
||||
return wallet.dict()
|
||||
|
||||
|
||||
|
@ -73,6 +89,7 @@ async def api_wallet_delete(wallet_id, w: WalletTypeInfo = Depends(require_admin
|
|||
)
|
||||
|
||||
await delete_watch_wallet(wallet_id)
|
||||
await delete_addresses_for_wallet(wallet_id)
|
||||
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
|
||||
|
@ -83,31 +100,171 @@ async def api_wallet_delete(wallet_id, w: WalletTypeInfo = Depends(require_admin
|
|||
@watchonly_ext.get("/api/v1/address/{wallet_id}")
|
||||
async def api_fresh_address(wallet_id, w: WalletTypeInfo = Depends(get_key_type)):
|
||||
address = await get_fresh_address(wallet_id)
|
||||
return address.dict()
|
||||
|
||||
return [address.dict()]
|
||||
|
||||
@watchonly_ext.put("/api/v1/address/{id}")
|
||||
async def api_update_address(
|
||||
id: str, req: Request, w: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
body = await req.json()
|
||||
params = {}
|
||||
# amout is only updated if the address has history
|
||||
if "amount" in body:
|
||||
params["amount"] = int(body["amount"])
|
||||
params["has_activity"] = True
|
||||
|
||||
if "note" in body:
|
||||
params["note"] = str(body["note"])
|
||||
|
||||
address = await update_address(**params, id=id)
|
||||
|
||||
wallet = (
|
||||
await get_watch_wallet(address.wallet)
|
||||
if address.branch_index == 0 and address.amount != 0
|
||||
else None
|
||||
)
|
||||
|
||||
if wallet and wallet.address_no < address.address_index:
|
||||
await update_watch_wallet(
|
||||
address.wallet, **{"address_no": address.address_index}
|
||||
)
|
||||
return address
|
||||
|
||||
|
||||
@watchonly_ext.get("/api/v1/addresses/{wallet_id}")
|
||||
async def api_get_addresses(wallet_id, w: WalletTypeInfo = Depends(get_key_type)):
|
||||
wallet = await get_watch_wallet(wallet_id)
|
||||
|
||||
if not wallet:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Wallet does not exist."
|
||||
)
|
||||
|
||||
addresses = await get_addresses(wallet_id)
|
||||
config = await get_config(w.wallet.user)
|
||||
|
||||
if not addresses:
|
||||
await get_fresh_address(wallet_id)
|
||||
await create_fresh_addresses(wallet_id, 0, config.receive_gap_limit)
|
||||
await create_fresh_addresses(wallet_id, 0, config.change_gap_limit, True)
|
||||
addresses = await get_addresses(wallet_id)
|
||||
|
||||
receive_addresses = list(filter(lambda addr: addr.branch_index == 0, addresses))
|
||||
change_addresses = list(filter(lambda addr: addr.branch_index == 1, addresses))
|
||||
|
||||
last_receive_address = list(
|
||||
filter(lambda addr: addr.has_activity, receive_addresses)
|
||||
)[-1:]
|
||||
last_change_address = list(
|
||||
filter(lambda addr: addr.has_activity, change_addresses)
|
||||
)[-1:]
|
||||
|
||||
if last_receive_address:
|
||||
current_index = receive_addresses[-1].address_index
|
||||
address_index = last_receive_address[0].address_index
|
||||
await create_fresh_addresses(
|
||||
wallet_id, current_index + 1, address_index + config.receive_gap_limit + 1
|
||||
)
|
||||
|
||||
if last_change_address:
|
||||
current_index = change_addresses[-1].address_index
|
||||
address_index = last_change_address[0].address_index
|
||||
await create_fresh_addresses(
|
||||
wallet_id,
|
||||
current_index + 1,
|
||||
address_index + config.change_gap_limit + 1,
|
||||
True,
|
||||
)
|
||||
|
||||
addresses = await get_addresses(wallet_id)
|
||||
return [address.dict() for address in addresses]
|
||||
|
||||
|
||||
#############################PSBT##########################
|
||||
|
||||
|
||||
@watchonly_ext.post("/api/v1/psbt")
|
||||
async def api_psbt_create(
|
||||
data: CreatePsbt, w: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
try:
|
||||
vin = [
|
||||
TransactionInput(bytes.fromhex(inp.tx_id), inp.vout) for inp in data.inputs
|
||||
]
|
||||
vout = [
|
||||
TransactionOutput(out.amount, script.address_to_scriptpubkey(out.address))
|
||||
for out in data.outputs
|
||||
]
|
||||
|
||||
descriptors = {}
|
||||
for _, masterpub in enumerate(data.masterpubs):
|
||||
descriptors[masterpub.fingerprint] = parse_key(masterpub.public_key)
|
||||
|
||||
inputs_extra = []
|
||||
bip32_derivations = {}
|
||||
for i, inp in enumerate(data.inputs):
|
||||
descriptor = descriptors[inp.masterpub_fingerprint][0]
|
||||
d = descriptor.derive(inp.address_index, inp.branch_index)
|
||||
for k in d.keys:
|
||||
bip32_derivations[PublicKey.parse(k.sec())] = DerivationPath(
|
||||
k.origin.fingerprint, k.origin.derivation
|
||||
)
|
||||
inputs_extra.append(
|
||||
{
|
||||
"bip32_derivations": bip32_derivations,
|
||||
"non_witness_utxo": Transaction.from_string(inp.tx_hex),
|
||||
}
|
||||
)
|
||||
|
||||
tx = Transaction(vin=vin, vout=vout)
|
||||
psbt = PSBT(tx)
|
||||
|
||||
for i, inp in enumerate(inputs_extra):
|
||||
psbt.inputs[i].bip32_derivations = inp["bip32_derivations"]
|
||||
psbt.inputs[i].non_witness_utxo = inp.get("non_witness_utxo", None)
|
||||
|
||||
outputs_extra = []
|
||||
bip32_derivations = {}
|
||||
for i, out in enumerate(data.outputs):
|
||||
if out.branch_index == 1:
|
||||
descriptor = descriptors[out.masterpub_fingerprint][0]
|
||||
d = descriptor.derive(out.address_index, out.branch_index)
|
||||
for k in d.keys:
|
||||
bip32_derivations[PublicKey.parse(k.sec())] = DerivationPath(
|
||||
k.origin.fingerprint, k.origin.derivation
|
||||
)
|
||||
outputs_extra.append({"bip32_derivations": bip32_derivations})
|
||||
|
||||
for i, out in enumerate(outputs_extra):
|
||||
psbt.outputs[i].bip32_derivations = out["bip32_derivations"]
|
||||
|
||||
return psbt.to_string()
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
|
||||
|
||||
|
||||
#############################CONFIG##########################
|
||||
|
||||
|
||||
@watchonly_ext.put("/api/v1/config")
|
||||
async def api_update_config(
|
||||
data: Config, w: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
config = await update_config(data, user=w.wallet.user)
|
||||
return config.dict()
|
||||
|
||||
|
||||
@watchonly_ext.get("/api/v1/config")
|
||||
async def api_get_config(w: WalletTypeInfo = Depends(get_key_type)):
|
||||
config = await get_config(w.wallet.user)
|
||||
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)
|
||||
|
@ -116,6 +273,7 @@ async def api_update_mempool(
|
|||
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)
|
||||
|
|
Loading…
Reference in New Issue
Block a user