From 7b07e5e0143ee2772756cb6b35ef2556fc0e8e79 Mon Sep 17 00:00:00 2001 From: sproxet Date: Tue, 8 Oct 2019 23:41:08 +0700 Subject: [PATCH] Implement a checkbox to subtract fee from transaction amount in Public Spend. Part of #82. --- src/daemon/zcoind.ts | 21 ++- .../components/PaymentSidebars/Send.vue | 146 +++++++++++++++++- .../PaymentSidebars/SendSteps/Confirm.vue | 2 +- 3 files changed, 153 insertions(+), 16 deletions(-) diff --git a/src/daemon/zcoind.ts b/src/daemon/zcoind.ts index 72b98be5..f7600230 100644 --- a/src/daemon/zcoind.ts +++ b/src/daemon/zcoind.ts @@ -365,7 +365,7 @@ export class Zcoind { // Publicly send amount satoshi XZC to recipient. resolve()s with txid, or reject()s if we have insufficient funds // or the call fails for some other reason. - async publicSend(auth: string, label: string, recipient: string, amount: number, feePerKb: number): Promise { + async publicSend(auth: string, label: string, recipient: string, amount: number, feePerKb: number, subtractFeeFromAmount: boolean): Promise { const data: {txid: string} = await this.send(auth, 'create', 'sendZcoin', { addresses: { [recipient]: { @@ -373,7 +373,8 @@ export class Zcoind { amount } }, - feePerKb + feePerKb, + subtractFeeFromAmount }); return data.txid; @@ -406,13 +407,17 @@ export class Zcoind { }); } - // Calculate a transaction fee. addresses is a map of {recipient: satoshiAmount} pairs; feePerKb is the satoshi - // fee per kilobyte for the generated transaction. We reject() the promise if the zcoind call fails or received data - // is invalid. - async calcTxFee(feePerKb: number, addresses: Record): Promise { + // Calculate a transaction fee. feePerKb is the satoshi fee per kilobyte for the generated transaction. + // + // We resolve() with the calculated fee in satoshi. + // We reject() the promise if the zcoind call fails or received data is invalid. + async calcTxFee(feePerKb: number, address: string, amount: number, subtractFeeFromAmount: boolean): Promise { let data = await this.send(null, 'get', 'txFee', { - addresses, - feePerKb + addresses: { + [address]: amount + }, + feePerKb, + subtractFeeFromAmount }); if (typeof data.fee === 'number') { diff --git a/src/renderer/components/PaymentSidebars/Send.vue b/src/renderer/components/PaymentSidebars/Send.vue index 92cdb77d..451f19a1 100644 --- a/src/renderer/components/PaymentSidebars/Send.vue +++ b/src/renderer/components/PaymentSidebars/Send.vue @@ -99,6 +99,20 @@ > Very slightly less will be sent because of fees. +
+ + + +
{{ convertToCoin(availableBalance) }} XZC available for {{ privateOrPublic }} send. @@ -112,6 +126,41 @@
+
+
+ + +
+ {{ convertToCoin(amountToReceive) }} XZC +
+
+ +
+ + +
+ {{ convertToCoin(transactionFee) }} XZC +
+
+ +
+ + +
+ {{ convertToCoin(totalAmount) }} XZC +
+
+
+
Send @@ -190,8 +239,8 @@ v-if="['waitToConfirm', 'confirm'].includes(sendPopoverStep)" :label="label" :address="address" - :amount="satoshiAmount" - :fee="0" + :amount="amountToReceive" + :fee="transactionFee" /> initial // incorrectPassphrase -> initial | passphrase // complete -> initial - sendPopoverStep: 'initial' + sendPopoverStep: 'initial', + + // This will be updated in watch() as computed properties can't use async. + transactionFee: 0, + + // TODO: Right now we're just hardcoding this. It should be made user configurable. + txFeePerKb: 1 } }, @@ -312,11 +368,27 @@ export default { return this.privateOrPublic === 'private' ? this.availableZerocoin : this.availableXzc; }, + // This is the amount the user entered in satoshis. satoshiAmount () { return convertToSatoshi(this.amount); }, + // This is the amount the user will receive. It may be less than satoshiAmount. + amountToReceive () { + return this.subtractFeeFromAmount ? this.satoshiAmount - this.transactionFee : this.satoshiAmount; + }, + + // This is the total amount that will be sent, including transaction fee. + totalAmount () { + return this.subtractFeeFromAmount ? this.satoshiAmount : this.satoshiAmount + this.transactionFee; + }, + + // We can begin the send if the fee has been shown (if required) and the form is valid. canBeginSend () { + return this.isValidated && (!this.privateOrPublic === 'private' || this.transactionFee > 0); + }, + + isValidated () { // this.errors was already calculated when amount and address were entered. return !!(this.amount && this.address && !this.validationErrors.items.length); }, @@ -338,7 +410,7 @@ export default { classes: 'error', show: true }) - }, + } }, watch: { @@ -346,6 +418,20 @@ export default { this.address = to.query.address || ''; this.label = to.query.label || ''; this.amount = to.query.amount || ''; + }, + + address: { + handler: 'maybeShowFee', + immediate: true + }, + + amount: { + handler: 'maybeShowFee', + immediate: true + }, + + isValidated: { + handler: 'maybeShowFee' } }, @@ -394,6 +480,24 @@ export default { methods: { convertToCoin, + maybeShowFee () { + if (this.privateOrPublic === 'private' || !this.isValidated) { + this.transactionFee = 0; + return; + } + + // First set transactionFee to 0. This is so the user can't hit Send before we've shown the fee. + this.transactionFee = 0; + + this.$daemon.calcTxFee(this.txFeePerKb, this.address, this.satoshiAmount, this.subtractFeeFromAmount) + .then(r => { + this.transactionFee = r; + }) + .catch(e => { + this.transactionFee = 0; + }) + }, + cleanupForm () { this.label = ''; this.amount = ''; @@ -440,7 +544,8 @@ export default { if (this.privateOrPublic === 'private') { await this.$daemon.privateSend(passphrase, this.label, this.address, this.satoshiAmount); } else { - await this.$daemon.publicSend(passphrase, this.label, this.address, this.satoshiAmount, 1); + await this.$daemon.publicSend(passphrase, this.label, this.address, this.satoshiAmount, + this.txFeePerKb, this.subtractFeeFromAmount); } } catch (e) { // Error code -14 indicates an incorrect passphrase. @@ -490,7 +595,7 @@ export default { .description { @include description(); - margin-bottom: emRhythm(7); + margin-bottom: 1em; } } @@ -540,6 +645,10 @@ fieldset { font-size: 0.85em; } + .subtract-fee-from-amount-checkbox { + font-weight: bold; + } + .amount-available { text-align: right; color: $color--polo-dark; @@ -547,6 +656,29 @@ fieldset { } } +.totals { + margin-top: -3em; + + .field { + font-weight: bold; + + label, .value { + display: inline; + } + + .value { + color: $color--green-dark; + } + } + + .total { + border-top: { + color: black; + width: 5px; + } + } +} + .button-wrap { padding-bottom: emRhythm(5); } diff --git a/src/renderer/components/PaymentSidebars/SendSteps/Confirm.vue b/src/renderer/components/PaymentSidebars/SendSteps/Confirm.vue index 3137b5c9..31b6208d 100644 --- a/src/renderer/components/PaymentSidebars/SendSteps/Confirm.vue +++ b/src/renderer/components/PaymentSidebars/SendSteps/Confirm.vue @@ -119,7 +119,7 @@ export default { font-weight: bold; } -.fee, .total { +.amount, .fee, .total { text-align: right; }