diff --git a/client/src/PublicRequest.ts b/client/src/PublicRequest.ts index 581724303..4c21a7032 100644 --- a/client/src/PublicRequest.ts +++ b/client/src/PublicRequest.ts @@ -175,6 +175,12 @@ export type SignTransactionRequest export type SignBtcTransactionRequestStandard = SimpleRequest & BitcoinTransactionInfo & { layout?: 'standard', + + // data needed for display + fiatCurrency?: string, + fiatRate?: number, + delay?: number, + feePerByte?: number, }; export type SignBtcTransactionRequestCheckout = SimpleRequest & BitcoinTransactionInfo & { @@ -183,9 +189,16 @@ export type SignBtcTransactionRequestCheckout = SimpleRequest & BitcoinTransacti shopLogoUrl?: string, time?: number, expires?: number, - fiatCurrency?: string, - fiatAmount?: number, vendorMarkup?: number, + + // data needed for display + fiatCurrency: string, + fiatRate: number, + + /** + * @deprecated use fiatRate and sum of inputs instead + */ + fiatAmount?: number, }; export type SignBtcTransactionRequest diff --git a/src/request/sign-btc-transaction/SignBtcTransaction.css b/src/request/sign-btc-transaction/SignBtcTransaction.css index 2378aec77..c6ddcab60 100644 --- a/src/request/sign-btc-transaction/SignBtcTransaction.css +++ b/src/request/sign-btc-transaction/SignBtcTransaction.css @@ -95,7 +95,66 @@ font-weight: bold; } -#confirm-transaction .fee-section { +#confirm-transaction .fee-section, +#confirm-transaction .fiat-section, +#confirm-transaction .estimation-section .speed-gauge, +#confirm-transaction .estimation-section .values, +#confirm-transaction .estimation-section .info-icon { opacity: 0.5; - margin-bottom: 0.25rem; + margin-top: 0; } + +#confirm-transaction .estimation-section { + display: flex; + justify-content: center; + align-items: center; + gap: 1.5rem; +} + +#confirm-transaction .needle, +#confirm-transaction .needle-cover { + transform-origin: 50% 54.1666667%; + transition: transform 0.3s var(--nimiq-ease); +} + +#confirm-transaction .needle-cover { + fill: white; +} + +#confirm-transaction .estimation-section .info-icon { + margin-top: 6px; +} + +#confirm-transaction .tooltip { + margin-top: -.25rem; + align-self: flex-start; +} + +#confirm-transaction .tooltip-box { + --tooltip-width: 250px; + width: var(--tooltip-width); + right: calc(var(--tooltip-width) * -1 / 4); +} + +#confirm-transaction .tx-info:hover .tooltip::after, +#confirm-transaction .tx-info:hover .tooltip .tooltip-box { + visibility: visible; + opacity: 1; +} + +#confirm-transaction .tooltip-box > p, +#confirm-transaction .network-fee .network-fee__btc { + opacity: 0.6; +} + +#confirm-transaction .tooltip-box p:first-child { + margin-top: 0.75rem; +} + + +#confirm-transaction .network-fee { + display: flex; + flex-direction: column; +} + + diff --git a/src/request/sign-btc-transaction/SignBtcTransaction.js b/src/request/sign-btc-transaction/SignBtcTransaction.js index d40664598..93ff94533 100644 --- a/src/request/sign-btc-transaction/SignBtcTransaction.js +++ b/src/request/sign-btc-transaction/SignBtcTransaction.js @@ -3,6 +3,7 @@ /* global SignBtcTransactionApi */ /* global PasswordBox */ /* global Errors */ +/* global TemplateTags */ /* global Utf8Tools */ /* global TopLevelApi */ /* global PaymentInfoLine */ @@ -23,7 +24,7 @@ class SignBtcTransaction { /** - * @param {Parsed} request + * @param {Required>} request * @param {SignBtcTransaction.resolve} resolve * @param {reject} reject */ @@ -94,16 +95,104 @@ class SignBtcTransaction { /** @type {HTMLDivElement} */ const $value = (this.$el.querySelector('#value')); - /** @type {HTMLDivElement} */ - const $fee = (this.$el.querySelector('#fee')); + const amountBTC = BitcoinUtils.satoshisToCoins(recipientOutput.value); + $value.textContent = NumberFormatting.formatNumber(amountBTC, 8); - // Set value and fee. - $value.textContent = NumberFormatting.formatNumber(BitcoinUtils.satoshisToCoins(recipientOutput.value), 8); - if ($fee && fee > 0) { - $fee.textContent = NumberFormatting.formatNumber(BitcoinUtils.satoshisToCoins(fee), 8); + const feeBTC = NumberFormatting.formatNumber(BitcoinUtils.satoshisToCoins(fee), 8); + + // Right now, we have two types of layouts. See #445 + if (request.layout === SignBtcTransactionApi.Layouts.CHECKOUT) { /** @type {HTMLDivElement} */ - const $feeSection = (this.$el.querySelector('.fee-section')); - $feeSection.classList.remove('display-none'); + const $fee = (this.$el.querySelector('#fee')); + + if ($fee && fee > 0) { + $fee.textContent = feeBTC; + /** @type {HTMLDivElement} */ + const $feeSection = (this.$el.querySelector('.fee-section')); + $feeSection.classList.remove('display-none'); + } + } else if (request.layout === SignBtcTransactionApi.Layouts.STANDARD) { + /** @type {HTMLSpanElement|null} */ + const $fiat = (this.$el.querySelector('#fiat')); + if (!$fiat) return; + + const { + fiatCurrency: currency, fiatRate, delay, feePerByte, + } = request; + + const amountFiat = fiatRate * amountBTC; + $fiat.textContent = `~${NumberFormatting.formatCurrency(amountFiat, currency)}`; + + + /** @type {HTMLDivElement|null} */ + const $fiatSection = (this.$el.querySelector('.fiat-section')); + if (!$fiatSection) return; + $fiatSection.classList.remove('display-none'); + + /** @type {HTMLDivElement|null} */ + const $estimationSection = (this.$el.querySelector('.estimation-section')); + if (!$estimationSection) return; + $estimationSection.classList.remove('display-none'); + + let estimatedDuration = ''; + let speed = 50; + if (delay === 1) { estimatedDuration = I18n.translatePhrase('sign-btc-tx-15m'); speed = 100; } + if (delay === 12) { estimatedDuration = I18n.translatePhrase('sign-btc-tx-2-4h'); speed = 50; } + if (delay === 36) { estimatedDuration = I18n.translatePhrase('sign-btc-tx-6h-plus'); speed = 0; } + + const rotation = (Math.min(100, Math.max(0, speed)) / 100 - 1) * 180; + this.$el.querySelectorAll('#speed-gauge-icon g > path').forEach($path => { + // @ts-ignore + $path.style.transform = `rotate(${rotation}deg)`; + }); + + if (!request.changeOutput || !request.changeOutput.value) return; + const fiatAmount = NumberFormatting.formatCurrency(BitcoinUtils.satoshisToCoins(fee) * fiatRate, currency); + + /** @type {HTMLDivElement|null} */ + const $values = ($estimationSection.querySelector('#values')); + if (!$values) return; + $values.innerText = `${estimatedDuration} / ${fiatAmount}`; + + /** @type {HTMLDivElement|null} */ + const $tooltip = document.querySelector('#txInfo'); + if (!$tooltip) return; + $tooltip.classList.add('tooltip', 'top'); + $tooltip.tabIndex = 0; // make the tooltip focusable + + /* eslint-disable indent */ + $tooltip.innerHTML = TemplateTags.hasVars(2)` + + + +
+
+ + + : ${feePerByte} + + + + ${feeBTC} + + +
+

+ Increase the speed of your transaction by paying a higher network fee. + The fees go directly to the miners. +

+

+ Duration and fees are estimates. +

+
+ `; + /* eslint-enable indent */ + + I18n.translateDom($tooltip); } // Set up password box. diff --git a/src/request/sign-btc-transaction/SignBtcTransactionApi.js b/src/request/sign-btc-transaction/SignBtcTransactionApi.js index dfce6c755..e997c962f 100644 --- a/src/request/sign-btc-transaction/SignBtcTransactionApi.js +++ b/src/request/sign-btc-transaction/SignBtcTransactionApi.js @@ -35,6 +35,16 @@ class SignBtcTransactionApi extends BitcoinRequestParserMixin(TopLevelApi) { + 'sequence number < 0xffffffff'); } parsedRequest.layout = this.parseLayout(request.layout); + + parsedRequest.fiatRate = this.parseNonNegativeFiniteNumber(request.fiatRate) || 0; + if (!parsedRequest.fiatRate) { + throw new Errors.InvalidRequestError('fiatRate must be defined and different to 0'); + } + parsedRequest.fiatCurrency = this.parseFiatCurrency(request.fiatCurrency) || ''; + if (!parsedRequest.fiatCurrency) { + throw new Errors.InvalidRequestError('fiatCurrency must be defined and different to empty string'); + } + if (request.layout === SignBtcTransactionApi.Layouts.CHECKOUT && parsedRequest.layout === SignBtcTransactionApi.Layouts.CHECKOUT) { parsedRequest.shopOrigin = this.parseShopOrigin(request.shopOrigin); @@ -44,9 +54,8 @@ class SignBtcTransactionApi extends BitcoinRequestParserMixin(TopLevelApi) { } parsedRequest.fiatAmount = this.parseNonNegativeFiniteNumber(request.fiatAmount); - parsedRequest.fiatCurrency = this.parseFiatCurrency(request.fiatCurrency); - if ((parsedRequest.fiatAmount === undefined) !== (parsedRequest.fiatCurrency === undefined)) { - throw new Errors.InvalidRequestError('fiatAmount and fiatCurrency must be both defined or undefined.'); + if (parsedRequest.fiatCurrency !== undefined) { + throw new Errors.InvalidRequestError('fiatAmount is deprecated. Use fiatRate instead.'); } parsedRequest.vendorMarkup = this.parseVendorMarkup(request.vendorMarkup); @@ -60,6 +69,17 @@ class SignBtcTransactionApi extends BitcoinRequestParserMixin(TopLevelApi) { throw new Errors.InvalidRequestError('`expires` must be greater than `time`'); } } + } else if (request.layout === SignBtcTransactionApi.Layouts.STANDARD + && parsedRequest.layout === SignBtcTransactionApi.Layouts.STANDARD) { + parsedRequest.delay = this.parseNonNegativeFiniteNumber(request.delay) || 0; + if (!parsedRequest.delay) { + throw new Errors.InvalidRequestError('delay must be defined.'); + } + + parsedRequest.feePerByte = this.parseNonNegativeFiniteNumber(request.feePerByte) || 0; + if (!parsedRequest.feePerByte) { + throw new Errors.InvalidRequestError('feePerByte must be defined.'); + } } return parsedRequest; diff --git a/src/request/sign-btc-transaction/index.html b/src/request/sign-btc-transaction/index.html index e2e2b57c8..34043f060 100644 --- a/src/request/sign-btc-transaction/index.html +++ b/src/request/sign-btc-transaction/index.html @@ -114,6 +114,33 @@

Con + + + + + diff --git a/src/translations/en.json b/src/translations/en.json index fbf006a2a..0503c3045 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -236,5 +236,14 @@ "sign-swap-exchange-fee": "Swap fee", "sign-swap-of-exchange-value": "of swap value.", "sign-swap-total-fees": "Total fees", - "sign-swap-your-bank": "Your bank" + "sign-swap-your-bank": "Your bank", + + "sign-btc-tx-15m": "15m", + "sign-btc-tx-2-4h": "2-4h", + "sign-btc-tx-6h-plus": "6h+", + "sign-btc-tx-network-fee": "Network fee", + "sign-btc-tx-sat-byte": "sat/byte", + "sign-btc-tx-fee-description": "Increase the speed of your transaction by paying a higher network fee. The fees go directly to the miners.", + "sign-btc-tx-fee-values-estimations": "Duration and fees are estimates.", + "sign-btc-tx-fee": "fee" }