lnbits-legend/lnbits/extensions/watchonly/static/components/payment/payment.js
2022-08-04 13:47:20 +03:00

336 lines
9.5 KiB
JavaScript

async function payment(path) {
const t = await loadTemplateAsync(path)
Vue.component('payment', {
name: 'payment',
template: t,
props: [
'accounts',
'addresses',
'utxos',
'mempool-endpoint',
'sats-denominated',
'serial-signer-ref',
'adminkey'
],
watch: {
immediate: true,
accounts() {
this.updateChangeAddress()
},
addresses() {
this.updateChangeAddress()
}
},
data: function () {
return {
DUST_LIMIT: 546,
tx: null,
psbtBase64: null,
psbtBase64Signed: null,
signedTx: null,
signedTxHex: null,
sentTxId: null,
signedTxId: null,
paymentTab: 'destination',
sendToList: [{address: '', amount: undefined}],
changeWallet: null,
changeAddress: {},
showCustomFee: false,
showCoinSelect: false,
showChecking: false,
showChange: false,
showPsbt: false,
showFinalTx: false,
feeRate: 1
}
},
computed: {
txSize: function () {
const tx = this.createTx()
return Math.round(txSize(tx))
},
txSizeNoChange: function () {
const tx = this.createTx(true)
return Math.round(txSize(tx))
},
feeValue: function () {
return this.feeRate * this.txSize
},
selectedAmount: function () {
return this.utxos
.filter(utxo => utxo.selected)
.reduce((t, a) => t + (a.amount || 0), 0)
},
changeAmount: function () {
return (
this.selectedAmount -
this.totalPayedAmount -
this.feeRate * this.txSize
)
},
balance: function () {
return this.utxos.reduce((t, a) => t + (a.amount || 0), 0)
},
totalPayedAmount: function () {
return this.sendToList.reduce((t, a) => t + (a.amount || 0), 0)
}
},
methods: {
satBtc(val, showUnit = true) {
return satOrBtc(val, showUnit, this.satsDenominated)
},
checkAndSend: async function () {
this.showChecking = true
try {
if (!this.serialSignerRef.isConnected()) {
const portOpen = await this.serialSignerRef.openSerialPort()
if (!portOpen) return
}
if (!this.serialSignerRef.isAuthenticated()) {
await this.serialSignerRef.hwwShowPasswordDialog()
const authenticated = await this.serialSignerRef.isAuthenticating()
if (!authenticated) return
}
await this.createPsbt()
if (this.psbtBase64) {
const txData = {
outputs: this.tx.outputs,
feeRate: this.tx.fee_rate,
feeValue: this.feeValue
}
await this.serialSignerRef.hwwSendPsbt(this.psbtBase64, txData)
await this.serialSignerRef.isSendingPsbt()
}
} catch (error) {
this.$q.notify({
type: 'warning',
message: 'Cannot check and sign PSBT!',
caption: `${error}`,
timeout: 10000
})
} finally {
this.showChecking = false
this.psbtBase64 = null
}
},
showPsbtDialog: async function () {
try {
const valid = await this.$refs.paymentFormRef.validate()
if (!valid) return
const data = await this.createPsbt()
if (data) {
this.showPsbt = true
}
} catch (error) {
this.$q.notify({
type: 'warning',
message: 'Failed to create PSBT!',
caption: `${error}`,
timeout: 10000
})
}
},
createPsbt: async function () {
try {
console.log('### this.createPsbt')
this.tx = this.createTx()
for (const input of this.tx.inputs) {
input.tx_hex = await this.fetchTxHex(input.tx_id)
}
const changeOutput = this.tx.outputs.find(o => o.branch_index === 1)
if (changeOutput) changeOutput.amount = this.changeAmount
const {data} = await LNbits.api.request(
'POST',
'/watchonly/api/v1/psbt',
this.adminkey,
this.tx
)
this.psbtBase64 = data
return data
} catch (err) {
LNbits.utils.notifyApiError(err)
}
},
createTx: function (excludeChange = false) {
const tx = {
fee_rate: this.feeRate,
masterpubs: this.accounts.map(w => ({
id: w.id,
public_key: w.masterpub,
fingerprint: w.fingerprint
}))
}
tx.inputs = this.utxos
.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.sendToList.map(out => ({
address: out.address,
amount: out.amount
}))
if (!excludeChange) {
const change = this.createChangeOutput()
const diffAmount = this.selectedAmount - this.totalPayedAmount
if (diffAmount >= this.DUST_LIMIT) {
tx.outputs.push(change)
}
}
tx.tx_size = Math.round(txSize(tx))
tx.inputs = _.shuffle(tx.inputs)
tx.outputs = _.shuffle(tx.outputs)
return tx
},
createChangeOutput: function () {
const change = this.changeAddress
const walletAcount =
this.accounts.find(w => w.id === change.wallet) || {}
return {
address: change.address,
address_index: change.addressIndex,
branch_index: change.isChange ? 1 : 0,
wallet: walletAcount.id
}
},
selectChangeAddress: function (account) {
if (!account) this.changeAddress = ''
this.changeAddress =
this.addresses.find(
a => a.wallet === account.id && a.isChange && !a.hasActivity
) || {}
},
updateChangeAddress: function () {
if (this.changeWallet) {
const changeAccount = (this.accounts || []).find(
w => w.id === this.changeWallet.id
)
// change account deleted
if (!changeAccount) {
this.changeWallet = this.accounts[0]
}
} else {
this.changeWallet = this.accounts[0]
}
this.selectChangeAddress(this.changeWallet)
},
updateSignedPsbt: async function (psbtBase64) {
try {
this.showChecking = true
this.psbtBase64Signed = psbtBase64
console.log('### payment updateSignedPsbt psbtBase64', psbtBase64)
const data = await this.extractTxFromPsbt(psbtBase64)
this.showFinalTx = true
if (data) {
this.signedTx = JSON.parse(data.tx_json)
this.signedTxHex = data.tx_hex
} else {
this.signedTx = null
this.signedTxHex = null
}
} finally {
this.showChecking = false
}
},
extractTxFromPsbt: async function (psbtBase64) {
console.log('### extractTxFromPsbt psbtBase64', psbtBase64)
try {
const {data} = await LNbits.api.request(
'PUT',
'/watchonly/api/v1/psbt/extract',
this.adminkey,
{
psbtBase64,
inputs: this.tx.inputs
}
)
console.log('### extractTxFromPsbt data', data)
return data
} catch (error) {
console.log('### error', error)
this.$q.notify({
type: 'warning',
message: 'Cannot finalize PSBT!',
timeout: 10000
})
LNbits.utils.notifyApiError(error)
}
},
broadcastTransaction: async function () {
try {
const {data} = await LNbits.api.request(
'POST',
'/watchonly/api/v1/tx',
this.adminkey,
{tx_hex: this.signedTxHex}
)
this.sentTxId = data
this.$q.notify({
type: 'positive',
message: 'Transaction broadcasted!',
caption: `${data}`,
timeout: 10000
})
// todo: event rescan with amount
// todo: display tx id
} catch (error) {
this.sentTxId = null
this.$q.notify({
type: 'warning',
message: 'Failed to broadcast!',
caption: `${error}`,
timeout: 10000
})
} finally {
this.showFinalTx = false
}
},
fetchTxHex: async function (txId) {
const {
bitcoin: {transactions: transactionsAPI}
} = mempoolJS({
hostname: this.mempoolEndpoint
})
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
}
},
handleOutputsChange: function () {
this.$refs.utxoList.refreshUtxoSelection(this.totalPayedAmount)
},
getTotalPaymentAmount: function () {
return this.sendToList.reduce((t, a) => t + (a.amount || 0), 0)
}
},
created: async function () {}
})
}