diff --git a/client/src/PublicRequest.ts b/client/src/PublicRequest.ts index 0f3c508db..31ee16581 100644 --- a/client/src/PublicRequest.ts +++ b/client/src/PublicRequest.ts @@ -188,19 +188,10 @@ export type SignTransactionRequest | SignTransactionRequestCheckout | SignTransactionRequestCashlink; -export type SignStakingRequest = SignTransactionRequestCommon & { - type: number, // See SignStakingApi for types - - // For createStaker and updateStaker transactions - delegation?: string, - - // For updateStaker transactions - reactivateAllStake?: boolean, - - // For inactivateStake transactions - newInactiveBalance?: number, - - // For unstake transactions +export type SignStakingRequest = SimpleRequest & { + keyPath: string, + transaction: Uint8Array, + senderLabel?: string, recipientLabel?: string, }; @@ -522,8 +513,7 @@ export type SignTransactionResult = SignatureResult & { serializedTx: Uint8Array, }; export type SignStakingResult = SignatureResult & { - data: Uint8Array, - serializedTx: Uint8Array, + transaction: Uint8Array, }; export type SimpleResult = { success: boolean }; export type SignedBitcoinTransaction = { diff --git a/src/request/sign-staking/SignStaking.js b/src/request/sign-staking/SignStaking.js index bcba30233..cf8c78bbc 100644 --- a/src/request/sign-staking/SignStaking.js +++ b/src/request/sign-staking/SignStaking.js @@ -3,7 +3,6 @@ /* global Key */ /* global KeyStore */ /* global PasswordBox */ -/* global SignStakingApi */ /* global Errors */ /* global Utf8Tools */ /* global TopLevelApi */ @@ -26,7 +25,7 @@ class SignStaking { /** @type {HTMLElement} */ this.$el = (document.getElementById(SignStaking.Pages.CONFIRM_STAKING)); - const transaction = request.transaction; + const transaction = request.plain; /** @type {HTMLElement} */ this.$accountDetails = (this.$el.querySelector('#account-details')); @@ -34,7 +33,7 @@ class SignStaking { /** @type {HTMLLinkElement} */ const $sender = (this.$el.querySelector('.accounts .sender')); this._senderAddressInfo = new AddressInfo({ - userFriendlyAddress: transaction.sender.toUserFriendlyAddress(), + userFriendlyAddress: transaction.sender, label: request.senderLabel || null, imageUrl: null, accountLabel: request.keyLabel || null, @@ -46,13 +45,9 @@ class SignStaking { /** @type {HTMLLinkElement} */ const $recipient = (this.$el.querySelector('.accounts .recipient')); - const recipientAddress = transaction.recipient.toUserFriendlyAddress(); - const recipientLabel = 'recipientLabel' in request && !!request.recipientLabel - ? request.recipientLabel - : null; this._recipientAddressInfo = new AddressInfo({ - userFriendlyAddress: recipientAddress, - label: recipientLabel, + userFriendlyAddress: transaction.recipient, + label: request.recipientLabel || null, imageUrl: null, accountLabel: null, }); @@ -81,7 +76,7 @@ class SignStaking { $feeSection.classList.remove('display-none'); } - if ($data && transaction.data.byteLength > 0) { + if ($data && transaction.data.raw.length) { // Set transaction extra data. $data.textContent = this._formatData(transaction); /** @type {HTMLDivElement} */ @@ -155,70 +150,13 @@ class SignStaking { const privateKey = Albatross.PrivateKey.unserialize(powPrivateKey.serialize()); const keyPair = Albatross.KeyPair.derive(privateKey); - /** @type {Albatross.Transaction} */ - let tx; - - switch (request.type) { - case SignStakingApi.IncomingStakingType.CREATE_STAKER: - tx = Albatross.TransactionBuilder.newCreateStaker( - keyPair.toAddress(), - Albatross.Address.fromString(/** @type {Nimiq.Address} */ (request.delegation).toHex()), - BigInt(request.transaction.value), - BigInt(request.transaction.fee), - request.transaction.validityStartHeight, - request.transaction.networkId, - ); - break; - case SignStakingApi.IncomingStakingType.ADD_STAKE: - tx = Albatross.TransactionBuilder.newStake( - keyPair.toAddress(), - keyPair.toAddress(), - BigInt(request.transaction.value), - BigInt(request.transaction.fee), - request.transaction.validityStartHeight, - request.transaction.networkId, - ); - break; - case SignStakingApi.IncomingStakingType.UPDATE_STAKER: - tx = Albatross.TransactionBuilder.newUpdateStaker( - keyPair.toAddress(), - Albatross.Address.fromString(/** @type {Nimiq.Address} */ (request.delegation).toHex()), - Boolean(request.reactivateAllStake), - BigInt(request.transaction.fee), - request.transaction.validityStartHeight, - request.transaction.networkId, - ); - break; - case SignStakingApi.IncomingStakingType.SET_INACTIVE_STAKE: - tx = Albatross.TransactionBuilder.newSetInactiveStake( - keyPair.toAddress(), - BigInt(request.newInactiveBalance), - BigInt(request.transaction.fee), - request.transaction.validityStartHeight, - request.transaction.networkId, - ); - break; - case SignStakingApi.IncomingStakingType.UNSTAKE: - tx = Albatross.TransactionBuilder.newUnstake( - keyPair.toAddress(), - BigInt(request.transaction.value), - BigInt(request.transaction.fee), - request.transaction.validityStartHeight, - request.transaction.networkId, - ); - break; - default: - throw new Errors.KeyguardError('Unreachable'); - } - - tx.sign(keyPair); + request.transaction.sign(keyPair); /** @type {KeyguardRequest.SignStakingResult} */ const result = { publicKey: keyPair.publicKey.serialize(), - signature: tx.proof.subarray(tx.proof.length - 64), - data: request.transaction.data, - serializedTx: tx.serialize(), + signature: request.transaction.proof.subarray(request.transaction.proof.length - 64), + transaction: request.transaction.serialize(), }; resolve(result); } @@ -229,47 +167,107 @@ class SignStaking { } /** - * @param {Nimiq.Transaction} transaction + * @param {Albatross.PlainTransaction} plain * @returns {string} */ - _formatData(transaction) { - const buf = new Nimiq.SerialBuffer(transaction.data); - const type = buf.readUint8(); - switch (type) { - case SignStakingApi.IncomingStakingType.CREATE_STAKER: { - let text = 'Start staking'; - const hasDelegation = buf.readUint8() === 1; - if (hasDelegation) { - const delegation = Nimiq.Address.unserialize(buf); - text += ` with validator ${delegation.toUserFriendlyAddress()}`; - } else { - text += ' with no validator'; + _formatData(plain) { + console.log(plain); + // That either the recipient or the sender is a staking account type is validated in SignStakingApi + // @ts-ignore Wrong type definition + if (plain.recipientType === 3) { + 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; } - return text; - } - case SignStakingApi.IncomingStakingType.UPDATE_STAKER: { - let text = 'Change validator'; - const hasDelegation = buf.readUint8() === 1; - if (hasDelegation) { - const delegation = Nimiq.Address.unserialize(buf); - text += ` to validator ${delegation.toUserFriendlyAddress()}`; - } else { - text += ' to no validator'; + 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; } - if (buf.readUint8() === 1) { - text += ' and reactivate all stake'; + case 'add-stake': { + const { staker } = plain.data; + return `Add stake to ${staker}`; + } + case 'set-inactive-stake': { + const { newInactiveBalance } = plain.data; + return `Set inactive stake to ${newInactiveBalance / 1e5} NIM`; + } + 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}`; } - return text; - } - case SignStakingApi.IncomingStakingType.ADD_STAKE: { - const staker = Nimiq.Address.unserialize(buf); - return `Add stake for ${staker.toUserFriendlyAddress()}`; } - case SignStakingApi.IncomingStakingType.SET_INACTIVE_STAKE: { - const inactiveBalance = buf.readUint64(); - return `Set inactive stake to ${inactiveBalance / 1e5} NIM`; + } 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}`; + } } - default: return Nimiq.BufferUtils.toHex(transaction.data); } } } diff --git a/src/request/sign-staking/SignStakingApi.js b/src/request/sign-staking/SignStakingApi.js index dbd2bd729..9aa2227b1 100644 --- a/src/request/sign-staking/SignStakingApi.js +++ b/src/request/sign-staking/SignStakingApi.js @@ -2,9 +2,10 @@ /* global SignStaking */ /* global Errors */ /* global Nimiq */ +/* global Albatross */ /** @extends {TopLevelApi} */ -class SignStakingApi extends TopLevelApi { +class SignStakingApi extends TopLevelApi { // eslint-disable-line no-unused-vars /** * @param {KeyguardRequest.SignStakingRequest} request * @returns {Promise>} @@ -23,104 +24,38 @@ class SignStakingApi extends TopLevelApi { parsedRequest.senderLabel = this.parseLabel(request.senderLabel); parsedRequest.recipientLabel = this.parseLabel(request.recipientLabel); - const type = this.parseStakingType(request.type); - parsedRequest.type = type; - let isSignalling = false; - switch (type) { - case SignStakingApi.IncomingStakingType.CREATE_STAKER: - case SignStakingApi.IncomingStakingType.UPDATE_STAKER: { - parsedRequest.delegation = this.parseAddress(request.delegation, 'delegation'); - parsedRequest.reactivateAllStake = this.parseBoolean(request.reactivateAllStake); - const data = new Nimiq.SerialBuffer( - 1 // Data type - + 1 // Option<> indicator - + Nimiq.Address.SERIALIZED_SIZE // Validator address (delegation) - + (type === SignStakingApi.IncomingStakingType.UPDATE_STAKER ? 1 : 0) // Stake reactivation boolean - + Nimiq.SignatureProof.SINGLE_SIG_SIZE, // Staker signature - ); - data.writeUint8(type); - data.writeUint8(1); // Delegation is optional, this signals that we are including it. - data.write(parsedRequest.delegation.serialize()); - if (type === SignStakingApi.IncomingStakingType.UPDATE_STAKER) { - data.writeUint8(parsedRequest.reactivateAllStake ? 1 : 0); - } - request.data = data; - isSignalling = type === SignStakingApi.IncomingStakingType.UPDATE_STAKER; - break; - } - case SignStakingApi.IncomingStakingType.ADD_STAKE: { - const sender = this.parseAddress(request.sender, 'sender'); - const data = new Nimiq.SerialBuffer( - 1 // Data type - + Nimiq.Address.SERIALIZED_SIZE, // Staker address - ); - data.writeUint8(type); - data.write(sender.serialize()); - request.data = data; - break; - } - case SignStakingApi.IncomingStakingType.SET_INACTIVE_STAKE: { - parsedRequest.newInactiveBalance = this.parseNonNegativeFiniteNumber( - request.newInactiveBalance, - false, - 'newInactiveBalance', - ); - const data = new Nimiq.SerialBuffer( - 1 // Data type - + 8 // u64 size - + Nimiq.SignatureProof.SINGLE_SIG_SIZE, // Staker signature - ); - data.writeUint8(type); - data.writeUint64(/** @type {number} */ (parsedRequest.newInactiveBalance)); - request.data = data; - isSignalling = true; - break; - } - case SignStakingApi.IncomingStakingType.UNSTAKE: { - // No special data format is required for unstaking - break; - } - default: - throw new Errors.KeyguardError('Unreachable'); - } - - parsedRequest.transaction = this.parseTransaction(request); - - if (isSignalling) { - // @ts-ignore Private property access - parsedRequest.transaction._value = 0; - // @ts-ignore Private property access - parsedRequest.transaction._flags = 0b10; // Signalling flag - } + parsedRequest.transaction = this.parseStakingTransaction(request.transaction); + parsedRequest.plain = parsedRequest.transaction.toPlain(); return parsedRequest; } /** * Checks that the given layout is valid - * @param {unknown} type - * @returns {number} + * @param {unknown} transaction + * @returns {Albatross.Transaction} */ - parseStakingType(type) { - if (!type || typeof type !== 'number') { - throw new Errors.InvalidRequestError('Staking type must be a number'); + parseStakingTransaction(transaction) { + if (!transaction) { + throw new Errors.InvalidRequestError('transaction is required'); } - if (!Object.values(SignStakingApi.IncomingStakingType).includes(type)) { - throw new Errors.InvalidRequestError('Invalid staking type'); + + if (!(transaction instanceof Uint8Array)) { + throw new Errors.InvalidRequestError('transaction must be a Uint8Array'); } - return type; + const tx = Albatross.Transaction.fromAny(Nimiq.BufferUtils.toHex(transaction)); + + if (tx.senderType !== Albatross.AccountType.Staking && tx.recipientType !== Albatross.AccountType.Staking) { + throw new Errors.InvalidRequestError('transaction must be a staking transaction'); + } + + // Parsing the transaction does not validate any of it's fields. + // TODO: Validate all fields like tx.verify() would? + + return tx; } get Handler() { return SignStaking; } } - -SignStakingApi.IncomingStakingType = { - UNSTAKE: 1, - - CREATE_STAKER: 5, - ADD_STAKE: 6, - UPDATE_STAKER: 7, - SET_INACTIVE_STAKE: 8, -}; diff --git a/types/Albatross.d.ts b/types/Albatross.d.ts index 93c064f39..8f475f290 100644 --- a/types/Albatross.d.ts +++ b/types/Albatross.d.ts @@ -1,4 +1,4 @@ -import * as _Albatross from '../node_modules/@nimiq/albatross-wasm/types/index'; +import * as _Albatross from '../node_modules/@nimiq/albatross-wasm/types/web'; export as namespace Albatross; export = _Albatross; diff --git a/types/Keyguard.d.ts b/types/Keyguard.d.ts index 64c69f417..80e817803 100644 --- a/types/Keyguard.d.ts +++ b/types/Keyguard.d.ts @@ -240,11 +240,9 @@ type Parsed = T extends Is ? ConstructTransaction> : T extends Is ? - Transform< - ConstructTransaction>, - 'delegation', - { delegation?: Nimiq.Address } - > : + Transform & { + plain: Albatross.PlainTransaction, + }, 'transaction', { transaction: Albatross.Transaction }> : T extends Is ? Transform< KeyId2KeyInfo,