2022-07-04 17:40:47 +03:00
|
|
|
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
|
|
|
|
},
|
|
|
|
|
2022-07-13 17:22:45 +03:00
|
|
|
serial: {
|
|
|
|
selectedPort: null,
|
|
|
|
writableStreamClosed: null,
|
|
|
|
writer: null,
|
|
|
|
readableStreamClosed: null,
|
|
|
|
reader: null,
|
|
|
|
showAdvancedConfig: false,
|
|
|
|
config: {}
|
|
|
|
},
|
|
|
|
|
2022-07-04 17:40:47 +03:00
|
|
|
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
|
2022-07-05 15:18:00 +03:00
|
|
|
|
2022-07-04 17:40:47 +03:00
|
|
|
await this.refreshWalletAccounts()
|
2022-07-05 15:18:00 +03:00
|
|
|
await this.refreshAddresses()
|
|
|
|
|
|
|
|
if (!this.payment.changeWallett) {
|
|
|
|
this.payment.changeWallet = this.walletAccounts[0]
|
|
|
|
this.selectChangeAddress(this.payment.changeWallet)
|
|
|
|
}
|
2022-07-04 17:40:47 +03:00
|
|
|
} catch (error) {
|
|
|
|
LNbits.utils.notifyApiError(error)
|
|
|
|
}
|
|
|
|
},
|
2022-07-05 15:18:00 +03:00
|
|
|
deleteWalletAccount: function (walletAccountId) {
|
2022-07-04 17:40:47 +03:00
|
|
|
LNbits.utils
|
|
|
|
.confirmDialog(
|
|
|
|
'Are you sure you want to delete this watch only wallet?'
|
|
|
|
)
|
|
|
|
.onOk(async () => {
|
|
|
|
try {
|
|
|
|
await LNbits.api.request(
|
|
|
|
'DELETE',
|
2022-07-05 15:18:00 +03:00
|
|
|
'/watchonly/api/v1/wallet/' + walletAccountId,
|
2022-07-04 17:40:47 +03:00
|
|
|
this.g.user.wallets[0].adminkey
|
|
|
|
)
|
|
|
|
this.walletAccounts = _.reject(this.walletAccounts, function (obj) {
|
2022-07-05 15:18:00 +03:00
|
|
|
return obj.id === walletAccountId
|
2022-07-04 17:40:47 +03:00
|
|
|
})
|
|
|
|
await this.refreshWalletAccounts()
|
|
|
|
await this.refreshAddresses()
|
2022-07-05 15:18:00 +03:00
|
|
|
if (
|
|
|
|
this.payment.changeWallet &&
|
|
|
|
this.payment.changeWallet.id === walletAccountId
|
|
|
|
) {
|
|
|
|
this.payment.changeWallet = this.walletAccounts[0]
|
|
|
|
this.selectChangeAddress(this.payment.changeWallet)
|
|
|
|
}
|
2022-07-04 17:40:47 +03:00
|
|
|
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 =
|
2022-07-05 15:18:00 +03:00
|
|
|
uniqueAddresses.filter(a => !a.isChange && a.hasActivity).pop() || {}
|
2022-07-04 17:40:47 +03:00
|
|
|
|
|
|
|
uniqueAddresses.forEach(a => {
|
|
|
|
a.expanded = false
|
|
|
|
a.accountType = type
|
|
|
|
a.gapLimitExceeded =
|
2022-07-05 13:24:34 +03:00
|
|
|
!a.isChange &&
|
2022-07-04 17:40:47 +03:00
|
|
|
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
|
2022-07-05 13:24:34 +03:00
|
|
|
if (!addressData.isChange) {
|
2022-07-04 17:40:47 +03:00
|
|
|
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 =>
|
2022-07-05 13:24:34 +03:00
|
|
|
(includeChangeAddrs || !a.isChange) &&
|
2022-07-04 17:40:47 +03:00
|
|
|
(includeGapAddrs ||
|
2022-07-05 13:24:34 +03:00
|
|
|
a.isChange ||
|
2022-07-04 17:40:47 +03:00
|
|
|
a.addressIndex <= walletsLimit[`_${a.wallet}`]) &&
|
|
|
|
!(excludeNoAmount && a.amount === 0) &&
|
|
|
|
(!selectedWalletId || a.wallet === selectedWalletId)
|
|
|
|
)
|
|
|
|
return addresses
|
|
|
|
},
|
|
|
|
openGetFreshAddressDialog: async function (walletId) {
|
2022-07-05 13:24:34 +03:00
|
|
|
const {data} = await LNbits.api.request(
|
2022-07-04 17:40:47 +03:00
|
|
|
'GET',
|
|
|
|
`/watchonly/api/v1/address/${walletId}`,
|
|
|
|
this.g.user.wallets[0].inkey
|
|
|
|
)
|
2022-07-05 13:24:34 +03:00
|
|
|
const addressData = mapAddressesData(data)
|
2022-07-04 17:40:47 +03:00
|
|
|
|
|
|
|
addressData.note = `Shared on ${currentDateTime()}`
|
|
|
|
const lastAcctiveAddress =
|
|
|
|
this.addresses.data
|
|
|
|
.filter(
|
2022-07-05 15:18:00 +03:00
|
|
|
a => a.wallet === addressData.wallet && !a.isChange && a.hasActivity
|
2022-07-04 17:40:47 +03:00
|
|
|
)
|
|
|
|
.pop() || {}
|
|
|
|
addressData.gapLimitExceeded =
|
2022-07-05 15:18:00 +03:00
|
|
|
!addressData.isChange &&
|
2022-07-04 17:40:47 +03:00
|
|
|
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
|
|
|
|
},
|
|
|
|
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]
|
2022-07-05 15:18:00 +03:00
|
|
|
this.selectChangeAddress(this.payment.changeWallet)
|
2022-07-04 17:40:47 +03:00
|
|
|
|
|
|
|
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)
|
|
|
|
},
|
2022-07-05 15:18:00 +03:00
|
|
|
selectChangeAddress: function (wallet = {}) {
|
2022-07-04 17:40:47 +03:00
|
|
|
this.payment.changeAddress =
|
|
|
|
this.addresses.data.find(
|
2022-07-05 15:18:00 +03:00
|
|
|
a => a.wallet === wallet.id && a.isChange && !a.hasActivity
|
2022-07-04 17:40:47 +03:00
|
|
|
) || {}
|
|
|
|
},
|
|
|
|
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)
|
|
|
|
},
|
2022-07-12 15:46:30 +03:00
|
|
|
//################### PSBT ###################
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
},
|
2022-07-13 17:22:45 +03:00
|
|
|
checkSerialPortSupported: function () {
|
2022-07-12 17:34:40 +03:00
|
|
|
if (!navigator.serial) {
|
|
|
|
this.$q.notify({
|
|
|
|
type: 'warning',
|
|
|
|
message: 'Serial port communication not supported!',
|
|
|
|
caption:
|
|
|
|
'Make sure your browser supports Serial Port and that you are using HTTPS.',
|
|
|
|
timeout: 10000
|
|
|
|
})
|
2022-07-13 17:22:45 +03:00
|
|
|
return false
|
2022-07-12 17:34:40 +03:00
|
|
|
}
|
2022-07-13 17:22:45 +03:00
|
|
|
return true
|
|
|
|
},
|
|
|
|
openSerialPort: async function () {
|
|
|
|
if (!this.checkSerialPortSupported()) return
|
|
|
|
console.log('### openSerialPort', this.serial.selectedPort)
|
2022-07-13 12:45:38 +03:00
|
|
|
try {
|
2022-07-13 17:22:45 +03:00
|
|
|
navigator.serial.addEventListener('connect', event => {
|
|
|
|
console.log('### navigator.serial event: connected!', event)
|
|
|
|
})
|
2022-07-12 17:34:40 +03:00
|
|
|
|
2022-07-13 17:22:45 +03:00
|
|
|
navigator.serial.addEventListener('disconnect', event => {
|
|
|
|
console.log('### navigator.serial event: disconnected!', event)
|
|
|
|
})
|
|
|
|
this.serial.selectedPort = await navigator.serial.requestPort()
|
2022-07-13 12:45:38 +03:00
|
|
|
// Wait for the serial port to open.
|
2022-07-13 17:22:45 +03:00
|
|
|
await this.serial.selectedPort.open({baudRate: 9600})
|
|
|
|
this.startSerialPortReading()
|
2022-07-12 17:34:40 +03:00
|
|
|
|
2022-07-13 17:22:45 +03:00
|
|
|
const textEncoder = new TextEncoderStream()
|
|
|
|
this.serial.writableStreamClosed = textEncoder.readable.pipeTo(
|
|
|
|
this.serial.selectedPort.writable
|
2022-07-13 12:45:38 +03:00
|
|
|
)
|
2022-07-12 17:34:40 +03:00
|
|
|
|
2022-07-13 17:22:45 +03:00
|
|
|
this.serial.writer = textEncoder.writable.getWriter()
|
|
|
|
} catch (error) {
|
|
|
|
this.$q.notify({
|
|
|
|
type: 'warning',
|
|
|
|
message: 'Cannot open serial port!',
|
|
|
|
caption: `${error}`,
|
|
|
|
timeout: 10000
|
|
|
|
})
|
|
|
|
}
|
|
|
|
},
|
|
|
|
closeSerialPort: async function () {
|
|
|
|
try {
|
|
|
|
console.log('### closeSerialPort', this.serial.selectedPort)
|
|
|
|
if (this.serial.writer) this.serial.writer.close()
|
|
|
|
if (this.serial.writableStreamClosed)
|
|
|
|
await this.serial.writableStreamClosed
|
2022-07-13 17:56:12 +03:00
|
|
|
if (this.serial.reader) this.serial.reader.cancel()
|
2022-07-13 17:22:45 +03:00
|
|
|
if (this.serial.readableStreamClosed)
|
2022-07-13 17:56:12 +03:00
|
|
|
await this.serial.readableStreamClosed.catch(() => {
|
|
|
|
/* Ignore the error */
|
|
|
|
})
|
2022-07-13 17:22:45 +03:00
|
|
|
if (this.serial.selectedPort) await this.serial.selectedPort.close()
|
|
|
|
this.serial.selectedPort = null
|
2022-07-13 17:56:12 +03:00
|
|
|
this.$q.notify({
|
|
|
|
type: 'positive',
|
|
|
|
message: 'Serial port disconnected!',
|
|
|
|
timeout: 5000
|
|
|
|
})
|
2022-07-13 17:22:45 +03:00
|
|
|
} catch (error) {
|
2022-07-13 17:56:12 +03:00
|
|
|
console.log('### error', error)
|
2022-07-13 17:22:45 +03:00
|
|
|
this.$q.notify({
|
|
|
|
type: 'warning',
|
|
|
|
message: 'Cannot close serial port!',
|
|
|
|
caption: `${error}`,
|
|
|
|
timeout: 10000
|
|
|
|
})
|
|
|
|
}
|
|
|
|
},
|
|
|
|
sendPsbtToSerialPort: async function () {
|
|
|
|
try {
|
|
|
|
await this.serial.writer.write(this.payment.psbtBase64 + '\n')
|
|
|
|
this.$q.notify({
|
|
|
|
type: 'positive',
|
2022-07-14 14:15:11 +03:00
|
|
|
message: 'Data sent to serial port device!',
|
2022-07-13 17:22:45 +03:00
|
|
|
timeout: 5000
|
|
|
|
})
|
2022-07-13 12:45:38 +03:00
|
|
|
} catch (error) {
|
|
|
|
this.$q.notify({
|
|
|
|
type: 'warning',
|
2022-07-13 17:22:45 +03:00
|
|
|
message: 'Failed to send data to serial port!',
|
2022-07-13 12:45:38 +03:00
|
|
|
caption: `${error}`,
|
|
|
|
timeout: 10000
|
|
|
|
})
|
|
|
|
}
|
2022-07-12 15:46:30 +03:00
|
|
|
},
|
2022-07-13 17:22:45 +03:00
|
|
|
|
|
|
|
startSerialPortReading: async function () {
|
|
|
|
const port = this.serial.selectedPort
|
|
|
|
|
|
|
|
while (port && port.readable) {
|
|
|
|
const textDecoder = new TextDecoderStream()
|
|
|
|
this.serial.readableStreamClosed = port.readable.pipeTo(
|
|
|
|
textDecoder.writable
|
|
|
|
)
|
|
|
|
this.serial.reader = textDecoder.readable.getReader()
|
2022-07-14 14:15:11 +03:00
|
|
|
let psbtChunks = []
|
2022-07-13 17:22:45 +03:00
|
|
|
|
|
|
|
try {
|
|
|
|
while (true) {
|
|
|
|
console.log('### reader.read()')
|
|
|
|
const {value, done} = await this.serial.reader.read()
|
2022-07-14 14:15:11 +03:00
|
|
|
console.log('### value', value)
|
2022-07-13 17:22:45 +03:00
|
|
|
if (value) {
|
2022-07-14 14:15:11 +03:00
|
|
|
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)
|
2022-07-14 17:37:36 +03:00
|
|
|
this.payment.psbtBase64Signed = psbtChunks.join('')
|
2022-07-14 14:15:11 +03:00
|
|
|
this.$q.notify({
|
|
|
|
type: 'positive',
|
|
|
|
message: 'PSBT received from serial port device!',
|
|
|
|
timeout: 10000
|
|
|
|
})
|
2022-07-14 17:37:36 +03:00
|
|
|
const data = await this.etractTxFromPsbt(
|
|
|
|
this.payment.psbtBase64Signed
|
2022-07-14 14:15:11 +03:00
|
|
|
)
|
2022-07-14 17:37:36 +03:00
|
|
|
this.payment.signedTx = JSON.parse(data.tx_json)
|
2022-07-14 14:15:11 +03:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
psbtChunks = []
|
|
|
|
this.$q.notify({
|
|
|
|
type: 'warning',
|
|
|
|
message: 'Received data from serial port (not psbt)',
|
|
|
|
caption: value.slice(0, 80) + '...',
|
|
|
|
timeout: 5000
|
|
|
|
})
|
|
|
|
}
|
2022-07-13 17:22:45 +03:00
|
|
|
}
|
|
|
|
if (done) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
this.$q.notify({
|
|
|
|
type: 'warning',
|
|
|
|
message: 'Serial port communication error!',
|
|
|
|
caption: `${error}`,
|
|
|
|
timeout: 10000
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
console.log('### startSerialPortReading DONE')
|
|
|
|
},
|
2022-07-14 14:15:11 +03:00
|
|
|
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
|
|
|
|
}
|
|
|
|
)
|
|
|
|
return data
|
|
|
|
} catch (error) {
|
|
|
|
LNbits.utils.notifyApiError(error)
|
|
|
|
}
|
|
|
|
},
|
2022-07-12 15:46:30 +03:00
|
|
|
sharePsbtWithAnimatedQRCode: async function () {
|
|
|
|
console.log('### sharePsbtWithAnimatedQRCode')
|
|
|
|
},
|
2022-07-04 17:40:47 +03:00
|
|
|
|
|
|
|
//################### 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}
|
2022-07-11 10:35:27 +03:00
|
|
|
} = mempoolJS({
|
|
|
|
hostname: new URL(this.config.data.mempool_endpoint).hostname
|
|
|
|
})
|
2022-07-04 17:40:47 +03:00
|
|
|
|
|
|
|
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}
|
2022-07-11 10:35:27 +03:00
|
|
|
} = mempoolJS({
|
|
|
|
hostname: new URL(this.config.data.mempool_endpoint).hostname
|
|
|
|
})
|
2022-07-04 17:40:47 +03:00
|
|
|
|
|
|
|
const fn = async () => feesAPI.getFeesRecommended()
|
|
|
|
this.payment.recommededFees = await retryWithDelay(fn)
|
|
|
|
},
|
|
|
|
getAddressTxsUtxoDelayed: async function (address) {
|
|
|
|
const {
|
|
|
|
bitcoin: {addresses: addressesAPI}
|
2022-07-11 10:35:27 +03:00
|
|
|
} = mempoolJS({
|
|
|
|
hostname: new URL(this.config.data.mempool_endpoint).hostname
|
|
|
|
})
|
2022-07-04 17:40:47 +03:00
|
|
|
|
|
|
|
const fn = async () =>
|
|
|
|
addressesAPI.getAddressTxsUtxo({
|
|
|
|
address
|
|
|
|
})
|
|
|
|
return retryWithDelay(fn)
|
|
|
|
},
|
|
|
|
fetchTxHex: async function (txId) {
|
|
|
|
const {
|
|
|
|
bitcoin: {transactions: transactionsAPI}
|
2022-07-11 10:35:27 +03:00
|
|
|
} = mempoolJS({
|
|
|
|
hostname: new URL(this.config.data.mempool_endpoint).hostname
|
|
|
|
})
|
2022-07-04 17:40:47 +03:00
|
|
|
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|