diff --git a/client/src/PublicRequest.ts b/client/src/PublicRequest.ts index 31ee16581..f7b899365 100644 --- a/client/src/PublicRequest.ts +++ b/client/src/PublicRequest.ts @@ -190,7 +190,7 @@ export type SignTransactionRequest export type SignStakingRequest = SimpleRequest & { keyPath: string, - transaction: Uint8Array, + transaction: Uint8Array | Uint8Array[], // An array is only allowed for retire_stake + remove_stake transactions senderLabel?: string, recipientLabel?: string, }; @@ -546,7 +546,7 @@ export type RedirectResult | ExportResult | KeyResult | SignTransactionResult - | SignStakingResult + | SignStakingResult[] | SignedBitcoinTransaction | SignedPolygonTransaction | SimpleResult @@ -560,7 +560,7 @@ export type Result = RedirectResult | IFrameResult; export type ResultType = T extends Is | Is ? SignatureResult : - T extends Is ? SignStakingResult : + T extends Is ? SignStakingResult[] : T extends Is ? DerivedAddress[] : T extends Is | Is | Is ? KeyResult : T extends Is ? ExportResult : @@ -574,7 +574,7 @@ export type ResultType = export type ResultByCommand = T extends KeyguardCommand.SIGN_MESSAGE | KeyguardCommand.SIGN_TRANSACTION ? SignatureResult : - T extends KeyguardCommand.SIGN_STAKING ? SignStakingResult : + T extends KeyguardCommand.SIGN_STAKING ? SignStakingResult[] : T extends KeyguardCommand.DERIVE_ADDRESS ? DerivedAddress[] : T extends KeyguardCommand.CREATE | KeyguardCommand.IMPORT ? KeyResult : T extends KeyguardCommand.EXPORT ? ExportResult : diff --git a/src/request/sign-staking/SignStaking.js b/src/request/sign-staking/SignStaking.js index d6a88900a..7a3f7e4eb 100644 --- a/src/request/sign-staking/SignStaking.js +++ b/src/request/sign-staking/SignStaking.js @@ -11,7 +11,7 @@ /** * @callback SignStaking.resolve - * @param {KeyguardRequest.SignStakingResult} result + * @param {KeyguardRequest.SignStakingResult[]} result */ class SignStaking { @@ -25,7 +25,7 @@ class SignStaking { /** @type {HTMLElement} */ this.$el = (document.getElementById(SignStaking.Pages.CONFIRM_STAKING)); - const transaction = request.plain; + const transaction = request.plain[request.plain.length - 1]; /** @type {HTMLElement} */ this.$accountDetails = (this.$el.querySelector('#account-details')); @@ -150,15 +150,20 @@ class SignStaking { const privateKey = Albatross.PrivateKey.unserialize(powPrivateKey.serialize()); const keyPair = Albatross.KeyPair.derive(privateKey); - request.transaction.sign(keyPair); + const results = request.transactions.map(transaction => { + transaction.sign(keyPair); - /** @type {KeyguardRequest.SignStakingResult} */ - const result = { - publicKey: keyPair.publicKey.serialize(), - signature: request.transaction.proof.subarray(request.transaction.proof.length - 64), - transaction: request.transaction.serialize(), - }; - resolve(result); + /** @type {KeyguardRequest.SignStakingResult} */ + const result = { + publicKey: keyPair.publicKey.serialize(), + signature: transaction.proof.subarray(transaction.proof.length - 64), + transaction: transaction.serialize(), + }; + + return result; + }); + + resolve(results); } run() { @@ -207,6 +212,10 @@ class SignStaking { 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; diff --git a/src/request/sign-staking/SignStakingApi.js b/src/request/sign-staking/SignStakingApi.js index 9aa2227b1..8c1f037ad 100644 --- a/src/request/sign-staking/SignStakingApi.js +++ b/src/request/sign-staking/SignStakingApi.js @@ -24,35 +24,61 @@ class SignStakingApi extends TopLevelApi { // eslint-disable-line no-unused-vars parsedRequest.senderLabel = this.parseLabel(request.senderLabel); parsedRequest.recipientLabel = this.parseLabel(request.recipientLabel); - parsedRequest.transaction = this.parseStakingTransaction(request.transaction); - parsedRequest.plain = parsedRequest.transaction.toPlain(); + parsedRequest.transactions = this.parseStakingTransaction(request.transaction); + parsedRequest.plain = parsedRequest.transactions.map(tx => tx.toPlain()); + + if (parsedRequest.plain.length > 2) { + throw new Errors.InvalidRequestError('Only a maximum of two transactions are allowed in a single request'); + } + + if (parsedRequest.plain.length === 2) { + // Ensure the transactions are for stake retiring and removal, in this order + if (parsedRequest.plain[0].data.type !== 'retire-stake') { + throw new Errors.InvalidRequestError('First transaction must be a retire stake transaction'); + } + if (parsedRequest.plain[1].senderData.type !== 'remove-stake') { + throw new Errors.InvalidRequestError('Second transaction must be a remove stake transaction'); + } + } return parsedRequest; } /** * Checks that the given layout is valid - * @param {unknown} transaction - * @returns {Albatross.Transaction} + * @param {unknown} transactions + * @returns {Albatross.Transaction[]} */ - parseStakingTransaction(transaction) { - if (!transaction) { + parseStakingTransaction(transactions) { + if (!transactions) { throw new Errors.InvalidRequestError('transaction is required'); } - if (!(transaction instanceof Uint8Array)) { - throw new Errors.InvalidRequestError('transaction must be a Uint8Array'); + if (!Array.isArray(transactions)) { + transactions = [transactions]; } - 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'); + if (/** @type {any[]} */ (transactions).length === 0) { + throw new Errors.InvalidRequestError('transaction must not be empty'); } - // Parsing the transaction does not validate any of it's fields. - // TODO: Validate all fields like tx.verify() would? + const txs = /** @type {any[]} */ (transactions).map(transaction => { + if (!(transaction instanceof Uint8Array)) { + throw new Errors.InvalidRequestError('transaction must be a Uint8Array'); + } + 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; + }); - return tx; + return txs; } get Handler() { diff --git a/types/Keyguard.d.ts b/types/Keyguard.d.ts index 80e817803..84cb1c701 100644 --- a/types/Keyguard.d.ts +++ b/types/Keyguard.d.ts @@ -241,8 +241,8 @@ type Parsed = ConstructTransaction> : T extends Is ? Transform & { - plain: Albatross.PlainTransaction, - }, 'transaction', { transaction: Albatross.Transaction }> : + plain: Albatross.PlainTransaction[], + }, 'transaction', { transactions: Albatross.Transaction[] }> : T extends Is ? Transform< KeyId2KeyInfo,