refactor: extract send-to component

This commit is contained in:
Vlad Stan 2022-07-26 10:11:29 +03:00
parent 3e7ce185f6
commit 3765900be0
8 changed files with 203 additions and 155 deletions

View file

@ -158,7 +158,6 @@ async function addressList(path) {
created: async function () { created: async function () {
await this.refreshAddresses() await this.refreshAddresses()
// this.$emit('update:addresses', this.addresses)
} }
}) })
} }

View file

@ -9,7 +9,6 @@
:rules="[val => !!val || 'Field is required']" :rules="[val => !!val || 'Field is required']"
type="number" type="number"
label="sats/vbyte" label="sats/vbyte"
@input="feeRateChanged"
></q-input> ></q-input>
</div> </div>
<div class="col-7"> <div class="col-7">
@ -23,7 +22,6 @@
:label-value="getFeeRateLabel(feeRate)" :label-value="getFeeRateLabel(feeRate)"
:min="1" :min="1"
:max="recommededFees.fastestFee" :max="recommededFees.fastestFee"
@input="feeRateChanged"
/> />
</div> </div>
</div> </div>

View file

@ -1,20 +1,30 @@
async function feeRate(path) { async function feeRate(path) {
const template = await loadTemplateAsync(path) const template = await loadTemplateAsync(path)
Vue.component('fees', { Vue.component('fee-rate', {
name: 'fees', name: 'fee-rate',
template, template,
props: ['totalfee', 'sats_denominated'], props: ['rate', 'totalfee', 'sats_denominated'],
watch: { watch: {
immediate: true, immediate: true,
totalfee: function (newVal, oldVal) { totalfee: function (newVal, oldVal) {
console.log('### ', newVal, oldVal) console.log('### ', newVal, oldVal)
} }
}, },
computed: {
feeRate: {
get: function () {
return this['rate']
},
set: function (value) {
console.log('### computed update rate')
this.$emit('update:rate', +value)
}
}
},
data: function () { data: function () {
return { return {
feeRate: 1,
recommededFees: { recommededFees: {
fastestFee: 1, fastestFee: 1,
halfHourFee: 1, halfHourFee: 1,
@ -29,10 +39,7 @@ async function feeRate(path) {
satBtc(val, showUnit = true) { satBtc(val, showUnit = true) {
return satOrBtc(val, showUnit, this['sats_denominated']) return satOrBtc(val, showUnit, this['sats_denominated'])
}, },
feeRateChanged: function (newFeeRate) {
console.log('### value', newFeeRate)
this.$emit('update:fee-rate', +newFeeRate)
},
refreshRecommendedFees: async function () { refreshRecommendedFees: async function () {
const { const {
bitcoin: {fees: feesAPI} bitcoin: {fees: feesAPI}
@ -56,7 +63,6 @@ async function feeRate(path) {
console.log('### created fees ') console.log('### created fees ')
await this.refreshRecommendedFees() await this.refreshRecommendedFees()
this.feeRate = this.recommededFees.halfHourFee this.feeRate = this.recommededFees.halfHourFee
this.feeRateChanged(this.recommededFees.halfHourFee)
} }
}) })
} }

View file

@ -0,0 +1,78 @@
<div class="row items-center no-wrap q-mb-md">
<div class="col-12">
<q-table
flat
dense
hide-header
:data="data"
:columns="paymentTable.columns"
:pagination.sync="paymentTable.pagination"
>
<template v-slot:body="props">
<q-tr :props="props">
<div class="row no-wrap">
<div class="col-1">
<q-btn
flat
dense
size="l"
@click="deletePaymentAddress(props.row)"
icon="cancel"
color="pink"
class="q-mt-sm"
></q-btn>
</div>
<div class="col-7 q-pr-lg">
<q-input
filled
dense
v-model.trim="props.row.address"
type="text"
label="Address"
:rules="[val => !!val || 'Field is required']"
></q-input>
</div>
<div class="col-3 q-pr-lg">
<q-input
filled
dense
v-model.number="props.row.amount"
type="number"
step="1"
label="Amount (sats)"
:rules="[val => !!val || 'Field is required', val => +val > DUST_LIMIT || 'Amount to small (below dust limit)'] "
></q-input>
</div>
<div class="col-1">
<q-btn outline color="grey" @click="sendMaxToAddress(props.row)"
>Max</q-btn
>
</div>
</div>
</q-tr>
</template>
<template v-slot:bottom-row>
<q-tr>
<q-td colspan="100%">
<div class="row items-center no-wrap">
<div class="col-3 q-pr-lg">
<q-btn outline color="grey" @click="addPaymentAddress"
>Add Send Address</q-btn
>
</div>
<div class="col-4 q-pr-lg"></div>
<div class="col-5">
<div class="float-right">
<span class="text-weight-bold">Payed Amount: </span>
<q-badge class="text-subtitle2 q-ml-lg" color="blue">
{{satBtc(getTotalPaymentAmount())}}
</q-badge>
</div>
</div>
</div>
</q-td>
</q-tr>
</template>
</q-table>
</div>
</div>

View file

@ -0,0 +1,70 @@
async function sendTo(path) {
const template = await loadTemplateAsync(path)
Vue.component('send-to', {
name: 'send-to',
template,
props: ['data', 'tx-size', 'total-amount', 'fee-rate', 'sats_denominated'],
computed: {
dataLocal: {
get: function () {
return this.data
},
set: function (value) {
console.log('### computed update data')
this.$emit('update:data', value)
}
}
},
data: function () {
return {
amount: 0,
paymentTable: {
columns: [
{
name: 'data',
align: 'left'
}
],
pagination: {
rowsPerPage: 10
},
filter: ''
}
}
},
methods: {
satBtc(val, showUnit = true) {
return satOrBtc(val, showUnit, this['sats_denominated'])
},
addPaymentAddress: function () {
this.dataLocal.push({address: '', amount: undefined})
},
deletePaymentAddress: function (v) {
const index = this.dataLocal.indexOf(v)
if (index !== -1) {
this.dataLocal.splice(index, 1)
}
},
getTotalPaymentAmount: function () {
return this.dataLocal.reduce((t, a) => t + (a.amount || 0), 0)
},
sendMaxToAddress: function (paymentAddress = {}) {
this.amount = 0
// const tx = this.createTx(true)
// this.payment.txSize = Math.round(txSize(tx))
const fee = this['fee-rate'] * this['tx-size']
const inputAmount = this['total-amount']
const payedAmount = this.getTotalPaymentAmount()
paymentAddress.amount = Math.max(0, inputAmount - payedAmount - fee)
}
},
created: async function () {
this.dataLocal = [{address: '', amount: undefined}]
}
})
}

View file

@ -7,6 +7,7 @@ const watchOnly = async () => {
await history('static/components/history/history.html') await history('static/components/history/history.html')
await utxoList('static/components/utxo-list/utxo-list.html') await utxoList('static/components/utxo-list/utxo-list.html')
await feeRate('static/components/fee-rate/fee-rate.html') await feeRate('static/components/fee-rate/fee-rate.html')
await sendTo('static/components/send-to/send-to.html')
await payment('static/components/payment/payment.html') await payment('static/components/payment/payment.html')
Vue.filter('reverse', function (value) { Vue.filter('reverse', function (value) {
@ -85,7 +86,22 @@ const watchOnly = async () => {
addressNote: '', addressNote: '',
showPayment: false, showPayment: false,
showCustomFee: false, showCustomFee: false,
feeValue: 0 feeRate: 1,
sendToList: []
}
},
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
} }
}, },
@ -191,7 +207,7 @@ const watchOnly = async () => {
//################### PAYMENT ################### //################### PAYMENT ###################
createTx: function (excludeChange = false) { createTx: function (excludeChange = false) {
const tx = { const tx = {
fee_rate: this.payment.feeRate, fee_rate: this.feeRate,
tx_size: this.payment.txSize, tx_size: this.payment.txSize,
masterpubs: this.walletAccounts.map(w => ({ masterpubs: this.walletAccounts.map(w => ({
public_key: w.masterpub, public_key: w.masterpub,
@ -205,7 +221,7 @@ const watchOnly = async () => {
a.tx_id < b.tx_id ? -1 : a.tx_id > b.tx_id ? 1 : a.vout - b.vout 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 => ({ tx.outputs = this.sendToList.map(out => ({
address: out.address, address: out.address,
amount: out.amount amount: out.amount
})) }))
@ -221,37 +237,27 @@ const watchOnly = async () => {
} }
// Only sort by amount on UI level (no lib for address decode) // Only sort by amount on UI level (no lib for address decode)
// Should sort by scriptPubKey (as byte array) on the backend // Should sort by scriptPubKey (as byte array) on the backend
// todo: just shuffle
tx.outputs.sort((a, b) => a.amount - b.amount) tx.outputs.sort((a, b) => a.amount - b.amount)
return tx return tx
}, },
createChangeOutput: function () { createChangeOutput: function () {
const change = this.payment.changeAddress const change = this.payment.changeAddress
const fee = this.payment.feeRate * this.payment.txSize // const inputAmount = this.getTotalSelectedUtxoAmount() // todo: set amount separately
const inputAmount = this.getTotalSelectedUtxoAmount() // const payedAmount = this.getTotalPaymentAmount()
const payedAmount = this.getTotalPaymentAmount()
const walletAcount = const walletAcount =
this.walletAccounts.find(w => w.id === change.wallet) || {} this.walletAccounts.find(w => w.id === change.wallet) || {}
return { return {
address: change.address, address: change.address,
amount: inputAmount - payedAmount - fee, // amount: inputAmount - payedAmount - this.feeValue,
addressIndex: change.addressIndex, addressIndex: change.addressIndex,
addressIndex: change.addressIndex, addressIndex: change.addressIndex,
masterpub_fingerprint: walletAcount.fingerprint masterpub_fingerprint: walletAcount.fingerprint
} }
}, },
computeFee: function (feeRate) {
const tx = this.createTx()
this.payment.txSize = Math.round(txSize(tx))
return 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 () { initPaymentData: async function () {
if (!this.payment.show) return if (!this.payment.show) return
await this.$refs.addressList.refreshAddresses() await this.$refs.addressList.refreshAddresses()
@ -263,9 +269,6 @@ const watchOnly = async () => {
this.payment.feeRate = this.payment.recommededFees.halfHourFee this.payment.feeRate = this.payment.recommededFees.halfHourFee
}, },
addPaymentAddress: function () {
this.payment.data.push({address: '', amount: undefined})
},
getTotalPaymentAmount: function () { getTotalPaymentAmount: function () {
return this.payment.data.reduce((t, a) => t + (a.amount || 0), 0) return this.payment.data.reduce((t, a) => t + (a.amount || 0), 0)
}, },
@ -281,22 +284,14 @@ const watchOnly = async () => {
// this.tab = 'utxos' // this.tab = 'utxos'
await this.initPaymentData() 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)
},
//################### PSBT ################### //################### PSBT ###################
createPsbt: async function () { createPsbt: async function () {
const wallet = this.g.user.wallets[0] const wallet = this.g.user.wallets[0]
try { try {
this.computeFee(this.payment.feeRate) // this.computeFee(this.feeRate)
const tx = this.createTx() const tx = this.createTx()
txSize(tx) // txSize(tx)
for (const input of tx.inputs) { for (const input of tx.inputs) {
input.tx_hex = await this.fetchTxHex(input.tx_id) input.tx_hex = await this.fetchTxHex(input.tx_id)
} }
@ -960,10 +955,6 @@ const watchOnly = async () => {
this.addresses = addresses this.addresses = addresses
await this.scanAddressWithAmount() await this.scanAddressWithAmount()
}, },
handleFeeRateChanged: function (newFeeRate) {
console.log('### newFeeRate', newFeeRate)
this.feeValue = this.computeFee(newFeeRate)
}
}, },
created: async function () { created: async function () {
if (this.g.user.wallets.length) { if (this.g.user.wallets.length) {

View file

@ -1,16 +1,4 @@
const tables = { const tables = {
paymentTable: {
columns: [
{
name: 'data',
align: 'left'
}
],
pagination: {
rowsPerPage: 10
},
filter: ''
},
summaryTable: { summaryTable: {
columns: [ columns: [
{ {
@ -43,7 +31,7 @@ const tableData = {
total: 0 total: 0
}, },
payment: { payment: {
data: [{address: '', amount: undefined}], data: [{address: '', amount: undefined}], // todo: remove
changeWallet: null, changeWallet: null,
changeAddress: {}, changeAddress: {},
changeAmount: 0, changeAmount: 0,

View file

@ -506,96 +506,12 @@
<q-tab-panel name="destination"> <q-tab-panel name="destination">
<q-card> <q-card>
<q-card-section> <q-card-section>
<div class="row items-center no-wrap q-mb-md"> {{sendToList}}
<div class="col-12"> <send-to
<q-table :data.sync="sendToList"
flat :tx:size="txSizeNoChange"
dense :sats-denominated="config.data.sats_denominated"
hide-header ></send-to>
:data="payment.data"
:columns="paymentTable.columns"
:pagination.sync="paymentTable.pagination"
>
<template v-slot:body="props">
<q-tr :props="props">
<div class="row no-wrap">
<div class="col-1">
<q-btn
flat
dense
size="l"
@click="deletePaymentAddress(props.row)"
icon="cancel"
color="pink"
class="q-mt-sm"
></q-btn>
</div>
<div class="col-7 q-pr-lg">
<q-input
filled
dense
v-model.trim="props.row.address"
type="text"
label="Address"
:rules="[val => !!val || 'Field is required']"
></q-input>
</div>
<div class="col-3 q-pr-lg">
<q-input
filled
dense
v-model.number="props.row.amount"
type="number"
step="1"
label="Amount (sats)"
:rules="[val => !!val || 'Field is required', val => +val > DUST_LIMIT || 'Amount to small (below dust limit)'] "
></q-input>
</div>
<div class="col-1">
<q-btn
outline
color="grey"
@click="sendMaxToAddress(props.row)"
>Max</q-btn
>
</div>
</div>
</q-tr>
</template>
<template v-slot:bottom-row>
<q-tr>
<q-td colspan="100%">
<div class="row items-center no-wrap">
<div class="col-3 q-pr-lg">
<q-btn
outline
color="grey"
@click="addPaymentAddress"
>Add Send Address</q-btn
>
</div>
<div class="col-4 q-pr-lg"></div>
<div class="col-5">
<div class="float-right">
<span class="text-weight-bold"
>Payed Amount:
</span>
<q-badge
class="text-subtitle2 q-ml-lg"
color="blue"
>
{{satBtc(getTotalPaymentAmount())}}
</q-badge>
</div>
</div>
</div>
</q-td>
</q-tr>
</template>
</q-table>
</div>
</div>
<!-- <div class="row items-center no-wrap q-mb-md"> <!-- <div class="row items-center no-wrap q-mb-md">
<div class="col-12"> <div class="col-12">
<q-table <q-table
@ -617,7 +533,7 @@
</q-td> </q-td>
<q-td key="fees" :props="props"> <q-td key="fees" :props="props">
<q-badge class="text-subtitle2" color="orange"> <q-badge class="text-subtitle2" color="orange">
{{satBtc(computeFee())}} {{satBtc(feeValue)}}
</q-badge> </q-badge>
</q-td> </q-td>
<q-td key="change" :props="props"> <q-td key="change" :props="props">
@ -673,10 +589,11 @@
<q-card-section> <q-card-section>
<div class="row items-center no-wrap q-mb-md q-pt-md"> <div class="row items-center no-wrap q-mb-md q-pt-md">
<div class="col-12"> <div class="col-12">
<fees {{feeRate}}
<fee-rate
:totalfee="feeValue" :totalfee="feeValue"
@update:fee-rate="handleFeeRateChanged" :rate.sync="feeRate"
></fees> ></fee-rate>
</div> </div>
</div> </div>
</q-card-section> </q-card-section>
@ -950,6 +867,7 @@
<script src="{{ url_for('watchonly_static', path='components/history/history.js') }}"></script> <script src="{{ url_for('watchonly_static', path='components/history/history.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='components/utxo-list/utxo-list.js') }}"></script> <script src="{{ url_for('watchonly_static', path='components/utxo-list/utxo-list.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='components/fee-rate/fee-rate.js') }}"></script> <script src="{{ url_for('watchonly_static', path='components/fee-rate/fee-rate.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='components/send-to/send-to.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='components/payment/payment.js') }}"></script> <script src="{{ url_for('watchonly_static', path='components/payment/payment.js') }}"></script>
<script src="{{ url_for('watchonly_static', path='js/index.js') }}"></script> <script src="{{ url_for('watchonly_static', path='js/index.js') }}"></script>
{% endblock %} {% endblock %}