diff --git a/client/src/PublicRequest.ts b/client/src/PublicRequest.ts index ec69ffda6..c039726d5 100644 --- a/client/src/PublicRequest.ts +++ b/client/src/PublicRequest.ts @@ -194,6 +194,11 @@ export type SignStakingRequest = SimpleRequest & { transaction: Uint8Array | Uint8Array[], // An array is only allowed for retire_stake + remove_stake transactions senderLabel?: string, recipientLabel?: string, + validatorAddress?: string, + validatorImageUrl?: string, + fromValidatorAddress?: string, + fromValidatorImageUrl?: string, + amount?: number, }; export type SignBtcTransactionRequestStandard = SimpleRequest & BitcoinTransactionInfo & { diff --git a/src/lib/RequestParser.js b/src/lib/RequestParser.js index fa7862a81..c07f85445 100644 --- a/src/lib/RequestParser.js +++ b/src/lib/RequestParser.js @@ -389,7 +389,7 @@ class RequestParser { // eslint-disable-line no-unused-vars */ _parseUrl(url, parameterName) { const parsedUrl = new URL(url); - const whitelistedProtocols = ['https:', 'http:', 'chrome-extension:', 'moz-extension:']; + const whitelistedProtocols = ['https:', 'http:', 'chrome-extension:', 'moz-extension:', 'data:']; if (!whitelistedProtocols.includes(parsedUrl.protocol)) { const protocolString = whitelistedProtocols.join(', '); throw new Errors.InvalidRequestError(`${parameterName} protocol must be one of: ${protocolString}`); diff --git a/src/request/sign-staking/SignStaking.css b/src/request/sign-staking/SignStaking.css index 3a2135db0..e025ee0a0 100644 --- a/src/request/sign-staking/SignStaking.css +++ b/src/request/sign-staking/SignStaking.css @@ -27,7 +27,6 @@ flex-direction: column; justify-content: center; align-items: center; - padding-top: 2rem; flex-grow: 1; } @@ -118,8 +117,6 @@ #confirm-staking .accounts { flex-direction: row; align-items: flex-start; - padding-bottom: 2rem; - border-bottom: 1px solid rgba(31, 35, 72, 0.1); flex-shrink: 0; } @@ -147,10 +144,3 @@ opacity: 0.5; margin-bottom: 0.25rem; } - -#confirm-staking .data-section { - margin: 1rem 3rem; - text-align: center; - max-width: 100%; - overflow-wrap: break-word; -} diff --git a/src/request/sign-staking/SignStaking.js b/src/request/sign-staking/SignStaking.js index 3fb3f370e..863e5f8cf 100644 --- a/src/request/sign-staking/SignStaking.js +++ b/src/request/sign-staking/SignStaking.js @@ -8,6 +8,7 @@ /* global AddressInfo */ /* global NumberFormatting */ /* global lunasToCoins */ +/* global I18n */ /** * @callback SignStaking.resolve @@ -24,29 +25,175 @@ class SignStaking { this._request = request; this.$el = /** @type {HTMLElement} */ (document.getElementById(SignStaking.Pages.CONFIRM_STAKING)); - const transaction = request.plain[request.plain.length - 1]; - + this.$headline = /** @type {HTMLElement} */ (this.$el.querySelector('#headline')); this.$accountDetails = /** @type {HTMLElement} */ (this.$el.querySelector('#account-details')); const $sender = /** @type {HTMLLinkElement} */ (this.$el.querySelector('.accounts .sender')); - this._senderAddressInfo = new AddressInfo({ - userFriendlyAddress: transaction.sender, - label: request.senderLabel || null, - imageUrl: null, - accountLabel: request.keyLabel || null, - }); + const $recipient = /** @type {HTMLLinkElement} */ (this.$el.querySelector('.accounts .recipient')); + + const transaction = request.plain[request.plain.length - 1]; + + /** @type {Nimiq.Address | undefined} */ + let validatorAddress; + + let displayValue = transaction.value; + + if (transaction.recipientType === 'staking') { + switch (transaction.data.type) { + case 'create-staker': + if (transaction.data.delegation) { + validatorAddress = Nimiq.Address.fromUserFriendlyAddress(transaction.data.delegation); + } + case 'add-stake': // eslint-disable-line no-fallthrough + validatorAddress = validatorAddress || request.validatorAddress; + + if (!validatorAddress) { + throw new Errors.InvalidRequestError('No delegation or validatorAddress provided'); + } + + this.$headline.textContent = I18n.translatePhrase('sign-staking-heading-stake'); + this._senderAddressInfo = new AddressInfo({ // From user + userFriendlyAddress: transaction.sender, + label: request.senderLabel || null, + imageUrl: null, + accountLabel: request.keyLabel || null, + }); + this._recipientAddressInfo = new AddressInfo({ // To validator + userFriendlyAddress: validatorAddress.toUserFriendlyAddress(), + label: request.recipientLabel || null, + imageUrl: request.validatorImageUrl || null, + accountLabel: null, + }); + break; + case 'update-staker': { // Change validator + if (transaction.data.newDelegation) { + validatorAddress = Nimiq.Address.fromUserFriendlyAddress(transaction.data.newDelegation); + } + + if (!validatorAddress) { + throw new Errors.InvalidRequestError('No newDelegation provided'); + } + + if (!request.amount) { + throw new Errors.InvalidRequestError('No amount provided'); + } + displayValue = request.amount; + + const fromValidatorAddress = request.validatorAddress; + if (!fromValidatorAddress) { + throw new Errors.InvalidRequestError('No fromValidatorAddress provided'); + } + + this.$headline.textContent = I18n.translatePhrase('sign-staking-heading-change'); + this._senderAddressInfo = new AddressInfo({ // From previous validator + userFriendlyAddress: fromValidatorAddress.toUserFriendlyAddress(), + label: request.senderLabel || null, + imageUrl: request.fromValidatorImageUrl || null, + accountLabel: request.keyLabel || null, + }); + this._recipientAddressInfo = new AddressInfo({ // To new validator + userFriendlyAddress: validatorAddress.toUserFriendlyAddress(), + label: request.recipientLabel || null, + imageUrl: request.validatorImageUrl || null, + accountLabel: null, + }); + break; + } + case 'set-active-stake': + case 'retire-stake': + validatorAddress = request.validatorAddress; + + if (!validatorAddress) { + throw new Errors.InvalidRequestError('No validatorAddress provided'); + } + + if (!request.amount) { + throw new Errors.InvalidRequestError('No amount provided'); + } + displayValue = request.amount; + + this.$headline.textContent = I18n.translatePhrase('sign-staking-heading-unstake'); + this._senderAddressInfo = new AddressInfo({ // From validator + userFriendlyAddress: validatorAddress.toUserFriendlyAddress(), + label: request.recipientLabel || null, + imageUrl: request.validatorImageUrl || null, + accountLabel: null, + }); + this._recipientAddressInfo = new AddressInfo({ // To User + userFriendlyAddress: transaction.sender, + label: request.senderLabel || null, + imageUrl: null, + accountLabel: request.keyLabel || null, + }); + break; + case 'create-validator': + case 'update-validator': + case 'deactivate-validator': + case 'reactivate-validator': + case 'retire-validator': + default: + this.$headline.textContent = I18n.translatePhrase('sign-tx-heading-tx'); + this._senderAddressInfo = new AddressInfo({ + userFriendlyAddress: transaction.sender, + label: request.senderLabel || null, + imageUrl: null, + accountLabel: request.keyLabel || null, + }); + this._recipientAddressInfo = new AddressInfo({ + userFriendlyAddress: transaction.recipient, + label: request.recipientLabel || null, + imageUrl: null, + accountLabel: null, + }); + break; + } + } else { + switch (transaction.senderData.type) { + case 'remove-stake': + validatorAddress = request.validatorAddress; + + if (!validatorAddress) { + throw new Errors.InvalidRequestError('No validatorAddress provided'); + } + + this.$headline.textContent = I18n.translatePhrase('sign-staking-heading-unstake'); + this._senderAddressInfo = new AddressInfo({ // From validator + userFriendlyAddress: validatorAddress.toUserFriendlyAddress(), + label: request.senderLabel || null, + imageUrl: request.validatorImageUrl || null, + accountLabel: null, + }); + this._recipientAddressInfo = new AddressInfo({ // To User + userFriendlyAddress: transaction.recipient, + label: request.recipientLabel || null, + imageUrl: null, + accountLabel: request.keyLabel || null, + }); + break; + case 'delete-validator': + default: + this.$headline.textContent = I18n.translatePhrase('sign-tx-heading-tx'); + this._senderAddressInfo = new AddressInfo({ + userFriendlyAddress: transaction.sender, + label: request.senderLabel || null, + imageUrl: null, + accountLabel: null, + }); + this._recipientAddressInfo = new AddressInfo({ + userFriendlyAddress: transaction.recipient, + label: request.recipientLabel || null, + imageUrl: null, + accountLabel: request.keyLabel || null, + }); + break; + } + } + this._senderAddressInfo.renderTo($sender); $sender.addEventListener('click', () => { this._openDetails(this._senderAddressInfo); }); - const $recipient = /** @type {HTMLLinkElement} */ (this.$el.querySelector('.accounts .recipient')); - this._recipientAddressInfo = new AddressInfo({ - userFriendlyAddress: transaction.recipient, - label: request.recipientLabel || null, - imageUrl: null, - accountLabel: null, - }); this._recipientAddressInfo.renderTo($recipient); $recipient.addEventListener('click', () => { this._openDetails(this._recipientAddressInfo); @@ -57,23 +204,15 @@ class SignStaking { const $value = /** @type {HTMLDivElement} */ (this.$el.querySelector('#value')); const $fee = /** @type {HTMLDivElement} */ (this.$el.querySelector('#fee')); - const $data = /** @type {HTMLDivElement} */ (this.$el.querySelector('#data')); // Set value and fee. - $value.textContent = NumberFormatting.formatNumber(lunasToCoins(transaction.value)); + $value.textContent = NumberFormatting.formatNumber(lunasToCoins(displayValue)); if ($fee && transaction.fee > 0) { $fee.textContent = NumberFormatting.formatNumber(lunasToCoins(transaction.fee)); const $feeSection = /** @type {HTMLDivElement} */ (this.$el.querySelector('.fee-section')); $feeSection.classList.remove('display-none'); } - if ($data && transaction.data.raw.length) { - // Set transaction extra data. - $data.textContent = this._formatData(transaction); - const $dataSection = /** @type {HTMLDivElement} */ (this.$el.querySelector('.data-section')); - $dataSection.classList.remove('display-none'); - } - // Set up password box. const $passwordBox = /** @type {HTMLFormElement} */ (document.querySelector('#password-box')); this._passwordBox = new PasswordBox($passwordBox, { @@ -159,114 +298,6 @@ class SignStaking { // Go to start page window.location.hash = SignStaking.Pages.CONFIRM_STAKING; } - - /** - * @param {Nimiq.PlainTransaction} plain - * @returns {string} - */ - _formatData(plain) { - console.log(plain); - // That either the recipient or the sender is a staking account type is validated in SignStakingApi - if (plain.recipientType === 'basic') { - switch (plain.data.type) { - case 'create-staker': { - let text = 'Start staking'; - const { delegation } = plain.data; - if (delegation) { - text += ` with validator ${delegation}`; - } else { - text += ' with no validator'; - } - return text; - } - case 'update-staker': { - let text = 'Change validator'; - const { newDelegation, reactivateAllStake } = plain.data; - if (newDelegation) { - text += ` to validator ${newDelegation}`; - } else { - text += ' to no validator'; - } - if (reactivateAllStake) { - text += ' and reactivate all stake'; - } - return text; - } - case 'add-stake': { - const { staker } = plain.data; - return `Add stake to ${staker}`; - } - case 'set-active-stake': { - const { newActiveBalance } = plain.data; - return `Set active stake to ${newActiveBalance / 1e5} NIM`; - } - case 'retire-stake': { - const { retireStake } = plain.data; - return `Retire ${retireStake / 1e5} NIM stake`; - } - case 'create-validator': { - let text = `Create validator ${plain.sender}`; - const { rewardAddress } = plain.data; - if (rewardAddress !== plain.sender) { - text += ` with reward address ${rewardAddress}`; - } - // TODO: Somehow let users see validator key, signing key, and signal data that they are signing - return text; - } - case 'update-validator': { - let text = `Update validator ${plain.sender}`; - const { - newRewardAddress, - newVotingKey, - newSigningKey, - newSignalData, - } = plain.data; - text += ` ${plain.sender}`; - if (newRewardAddress) { - text += `, updating reward address to ${newRewardAddress}`; - } - if (newVotingKey) { - text += ', updating voting key'; - } - if (newSigningKey) { - text += ', updating signing key'; - } - if (newSignalData) { - text += ', updating signal data'; - } - return text; - } - case 'deactivate-validator': { - const { validator } = plain.data; - return `Deactivate validator ${validator}`; - } - case 'reactivate-validator': { - const { validator } = plain.data; - return `Reactivate validator ${validator}`; - } - case 'retire-validator': { - return `Retire validator ${plain.sender}`; - } - default: { - return `Unrecognized in-staking data: ${plain.data.type} - ${plain.data.raw}`; - } - } - } else { - switch (plain.senderData.type) { - case 'remove-stake': { - return 'Unstake'; - } - case 'delete-validator': { - // Cannot show the validator address here, as the recipient can be any address and the validator - // address is the signer, which the Keyguard only knows after password entry. - return 'Delete validator'; - } - default: { - return `Unrecognized out-staking data: ${plain.senderData.type} - ${plain.senderData.raw}`; - } - } - } - } } SignStaking.Pages = { diff --git a/src/request/sign-staking/SignStakingApi.js b/src/request/sign-staking/SignStakingApi.js index 374f0a506..511c7c0d8 100644 --- a/src/request/sign-staking/SignStakingApi.js +++ b/src/request/sign-staking/SignStakingApi.js @@ -40,6 +40,33 @@ class SignStakingApi extends TopLevelApi { // eslint-disable-line no-unused-vars } } + if (request.validatorAddress) { + parsedRequest.validatorAddress = this.parseAddress(request.validatorAddress, 'validatorAddress', false); + } + + if (request.validatorImageUrl) { + parsedRequest.validatorImageUrl = this._parseUrl(request.validatorImageUrl, 'validatorImageUrl'); + } + + if (request.fromValidatorAddress) { + parsedRequest.validatorAddress = this.parseAddress( + request.fromValidatorAddress, + 'fromValidatorAddress', + false, + ); + } + + if (request.fromValidatorImageUrl) { + parsedRequest.fromValidatorImageUrl = this._parseUrl( + request.fromValidatorImageUrl, + 'fromValidatorImageUrl', + ); + } + + if (request.amount) { + parsedRequest.amount = this.parseNonNegativeFiniteNumber(request.amount, true, 'amount'); + } + return parsedRequest; } diff --git a/src/request/sign-staking/index.html b/src/request/sign-staking/index.html index 91c18220d..ba1e0a801 100644 --- a/src/request/sign-staking/index.html +++ b/src/request/sign-staking/index.html @@ -84,7 +84,7 @@
@@ -102,8 +102,6 @@

Confirm Transaction

- -
diff --git a/src/translations/de.json b/src/translations/de.json index 9cd976e46..119bb3db0 100644 --- a/src/translations/de.json +++ b/src/translations/de.json @@ -69,6 +69,10 @@ "sign-tx-fee": "Gebühr", "sign-tx-cancel-payment": "Zahlung abbrechen", + "sign-staking-heading-stake": "Stake NIM", + "sign-staking-heading-unstake": "Unstake NIM", + "sign-staking-heading-change": "Change Validator", + "sign-msg-heading": "Nachricht signieren", "sign-msg-signer": "Unterzeichner", diff --git a/src/translations/en.json b/src/translations/en.json index 934d9bbba..3a433fade 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -69,6 +69,10 @@ "sign-tx-fee": "fee", "sign-tx-cancel-payment": "Cancel payment", + "sign-staking-heading-stake": "Stake NIM", + "sign-staking-heading-unstake": "Unstake NIM", + "sign-staking-heading-change": "Change Validator", + "sign-msg-heading": "Sign Message", "sign-msg-signer": "Signer", diff --git a/src/translations/es.json b/src/translations/es.json index 733b059ce..b31fee59c 100644 --- a/src/translations/es.json +++ b/src/translations/es.json @@ -69,6 +69,10 @@ "sign-tx-fee": "cuota", "sign-tx-cancel-payment": "Cancelar pago", + "sign-staking-heading-stake": "Stake NIM", + "sign-staking-heading-unstake": "Unstake NIM", + "sign-staking-heading-change": "Change Validator", + "sign-msg-heading": "Firmar Mensaje", "sign-msg-signer": "Firmador", diff --git a/src/translations/fr.json b/src/translations/fr.json index 4312b0407..46381775e 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -69,6 +69,10 @@ "sign-tx-fee": "frais", "sign-tx-cancel-payment": "Annuler le paiement", + "sign-staking-heading-stake": "Stake NIM", + "sign-staking-heading-unstake": "Unstake NIM", + "sign-staking-heading-change": "Change Validator", + "sign-msg-heading": "Signer le Message", "sign-msg-signer": "Signataire", diff --git a/src/translations/nl.json b/src/translations/nl.json index fc1a98d56..6a97d5433 100644 --- a/src/translations/nl.json +++ b/src/translations/nl.json @@ -69,6 +69,10 @@ "sign-tx-fee": "kosten", "sign-tx-cancel-payment": "Annuleer betaling", + "sign-staking-heading-stake": "Stake NIM", + "sign-staking-heading-unstake": "Unstake NIM", + "sign-staking-heading-change": "Change Validator", + "sign-msg-heading": "Bericht ondertekenen", "sign-msg-signer": "Ondertekenaar", diff --git a/src/translations/pt.json b/src/translations/pt.json index 61a149987..995617045 100644 --- a/src/translations/pt.json +++ b/src/translations/pt.json @@ -69,6 +69,10 @@ "sign-tx-fee": "taxa", "sign-tx-cancel-payment": "Cancelar pagamento", + "sign-staking-heading-stake": "Stake NIM", + "sign-staking-heading-unstake": "Unstake NIM", + "sign-staking-heading-change": "Change Validator", + "sign-msg-heading": "Assinar Mensagem", "sign-msg-signer": "Signatário", diff --git a/src/translations/ru.json b/src/translations/ru.json index 500747735..11af42be5 100644 --- a/src/translations/ru.json +++ b/src/translations/ru.json @@ -69,6 +69,10 @@ "sign-tx-fee": "комиссия", "sign-tx-cancel-payment": "Отменить платёж", + "sign-staking-heading-stake": "Stake NIM", + "sign-staking-heading-unstake": "Unstake NIM", + "sign-staking-heading-change": "Change Validator", + "sign-msg-heading": "Подписать сообщение", "sign-msg-signer": "Отправитель", diff --git a/src/translations/uk.json b/src/translations/uk.json index 5dc453124..4d6e78228 100644 --- a/src/translations/uk.json +++ b/src/translations/uk.json @@ -69,6 +69,10 @@ "sign-tx-fee": "комісія", "sign-tx-cancel-payment": "Скасувати платіж", + "sign-staking-heading-stake": "Stake NIM", + "sign-staking-heading-unstake": "Unstake NIM", + "sign-staking-heading-change": "Change Validator", + "sign-msg-heading": "Підписати повідомлення", "sign-msg-signer": "Підписант", diff --git a/src/translations/zh.json b/src/translations/zh.json index cd037003f..a093c8c5a 100644 --- a/src/translations/zh.json +++ b/src/translations/zh.json @@ -69,6 +69,10 @@ "sign-tx-fee": "手续费", "sign-tx-cancel-payment": "取消付款", + "sign-staking-heading-stake": "Stake NIM", + "sign-staking-heading-unstake": "Unstake NIM", + "sign-staking-heading-change": "Change Validator", + "sign-msg-heading": "签署信息", "sign-msg-signer": "签署者", diff --git a/types/Keyguard.d.ts b/types/Keyguard.d.ts index 831398264..4b9cedab2 100644 --- a/types/Keyguard.d.ts +++ b/types/Keyguard.d.ts @@ -286,7 +286,13 @@ type Parsed = T extends Is ? Transform & { plain: Nimiq.PlainTransaction[], - }, 'transaction', { transactions: Nimiq.Transaction[] }> : + }, 'transaction' | 'validatorAddress' | 'validatorImageUrl' | 'fromValidatorAddress' | 'fromValidatorImageUrl', { + transactions: Nimiq.Transaction[], + validatorAddress?: Nimiq.Address, + validatorImageUrl?: URL, + fromValidatorAddress?: Nimiq.Address, + fromValidatorImageUrl?: URL, + }> : T extends Is ? Transform< KeyId2KeyInfo,