From 4b467cb6505b70c0e1c1aee31f37e5aeb5b16c2d Mon Sep 17 00:00:00 2001 From: Steven Liu Date: Sat, 12 Oct 2019 21:09:09 +0100 Subject: [PATCH] JAV-38 [Github #185] changed signer in toAggregate to be optional --- src/model/transaction/AggregateTransaction.ts | 11 +++- src/model/transaction/InnerTransaction.ts | 2 +- src/model/transaction/Transaction.ts | 11 ++-- src/service/AggregateTransactionService.ts | 2 +- .../transaction/AggregateTransaction.spec.ts | 50 +++++++++++++++++++ 5 files changed, 68 insertions(+), 8 deletions(-) diff --git a/src/model/transaction/AggregateTransaction.ts b/src/model/transaction/AggregateTransaction.ts index d400f1cf1e..bb7862d8c4 100644 --- a/src/model/transaction/AggregateTransaction.ts +++ b/src/model/transaction/AggregateTransaction.ts @@ -289,12 +289,19 @@ export class AggregateTransaction extends Transaction { * @internal * @returns {Uint8Array} */ - protected generateBytes(): Uint8Array { + protected generateBytes(signer?: PublicAccount): Uint8Array { const signerBuffer = new Uint8Array(32); const signatureBuffer = new Uint8Array(64); - let transactions = Uint8Array.from([]); this.innerTransactions.forEach((transaction) => { + if (!transaction.signer) { + if (this.type === TransactionType.AGGREGATE_COMPLETE) { + transaction = Object.assign({__proto__: Object.getPrototypeOf(transaction)}, transaction, {signer}); + } else { + throw new Error( + 'InnerTransaction signer must be provide. Only AggregateComplete transaction can use delegated signer.'); + } + } const transactionByte = transaction.toAggregateTransactionBytes(); transactions = GeneratorUtils.concatTypedArrays(transactions, transactionByte); }); diff --git a/src/model/transaction/InnerTransaction.ts b/src/model/transaction/InnerTransaction.ts index fcb779900a..c3d05f9cfa 100644 --- a/src/model/transaction/InnerTransaction.ts +++ b/src/model/transaction/InnerTransaction.ts @@ -20,4 +20,4 @@ import {Transaction} from './Transaction'; /** * Transaction with signer included, used when adding signer to transactions included in an aggregate transaction. */ -export type InnerTransaction = Transaction & {signer: PublicAccount}; +export type InnerTransaction = Transaction & {signer?: PublicAccount}; diff --git a/src/model/transaction/Transaction.ts b/src/model/transaction/Transaction.ts index d9ce19ec56..5349a84681 100644 --- a/src/model/transaction/Transaction.ts +++ b/src/model/transaction/Transaction.ts @@ -105,8 +105,9 @@ export abstract class Transaction { /** * @internal + * @param singer Optional singer for delegated aggregate complete transaction only. */ - protected abstract generateBytes(): Uint8Array; + protected abstract generateBytes(signer?: PublicAccount): Uint8Array; /** * @internal @@ -123,7 +124,7 @@ export abstract class Transaction { public signWith(account: Account, generationHash: string): SignedTransaction { const generationHashBytes = Array.from(Convert.hexToUint8(generationHash)); const signSchema = SHA3Hasher.resolveSignSchema(account.networkType); - const byteBuffer = Array.from(this.generateBytes()); + const byteBuffer = Array.from(this.generateBytes(account.publicAccount)); const signingBytes = generationHashBytes.concat(byteBuffer.slice(4 + 64 + 32)); const keyPairEncoded = KeyPair.createKeyPairFromPrivateKeyString(account.privateKey, signSchema); const signature = Array.from(KeyPair.sign(account, new Uint8Array(signingBytes), signSchema)); @@ -162,10 +163,12 @@ export abstract class Transaction { /** * Convert an aggregate transaction to an inner transaction including transaction signer. - * @param signer - Transaction signer. + * Signer is optional for `AggregateComplete` transaction `ONLY`. + * If no signer provided, aggregate transaction signer will be delegated on signing + * @param signer - Innre transaction signer. (Optional for `AggregateComplete` transaction `ONLY`) * @returns InnerTransaction */ - public toAggregate(signer: PublicAccount): InnerTransaction { + public toAggregate(signer?: PublicAccount): InnerTransaction { if (this.type === TransactionType.AGGREGATE_BONDED || this.type === TransactionType.AGGREGATE_COMPLETE) { throw new Error('Inner transaction cannot be an aggregated transaction.'); } diff --git a/src/service/AggregateTransactionService.ts b/src/service/AggregateTransactionService.ts index 0cf768939c..86f9f36d4e 100644 --- a/src/service/AggregateTransactionService.ts +++ b/src/service/AggregateTransactionService.ts @@ -53,7 +53,7 @@ export class AggregateTransactionService { signers.push(signedTransaction.signerPublicKey); } return observableFrom(aggregateTransaction.innerTransactions).pipe( - mergeMap((innerTransaction) => this.accountHttp.getMultisigAccountInfo(innerTransaction.signer.address) + mergeMap((innerTransaction) => this.accountHttp.getMultisigAccountInfo(innerTransaction.signer!.address) .pipe( /** * For multisig account, we need to get the graph info in case it has multiple levels diff --git a/test/model/transaction/AggregateTransaction.spec.ts b/test/model/transaction/AggregateTransaction.spec.ts index 3f4d2e495c..46730b5b4b 100644 --- a/test/model/transaction/AggregateTransaction.spec.ts +++ b/test/model/transaction/AggregateTransaction.spec.ts @@ -18,6 +18,7 @@ import {expect} from 'chai'; import {ChronoUnit} from 'js-joda'; import { TransactionMapping } from '../../../src/core/utils/TransactionMapping'; import {CreateTransactionFromDTO} from '../../../src/infrastructure/transaction/CreateTransactionFromDTO'; +import { CreateTransactionFromPayload } from '../../../src/infrastructure/transaction/CreateTransactionFromPayload'; import {Account} from '../../../src/model/account/Account'; import {Address} from '../../../src/model/account/Address'; import {PublicAccount} from '../../../src/model/account/PublicAccount'; @@ -116,6 +117,55 @@ describe('AggregateTransaction', () => { )).to.be.equal('019054419050B9837EFAB4BBE8A4B9BB32D812F9885C00D8FC1650E1420D000000746573742D6D657373616765'); }); + it('should createComplete an AggregateTransaction object with delegated signer', () => { + const transferTransaction = TransferTransaction.create( + Deadline.create(1, ChronoUnit.HOURS), + Address.createFromRawAddress('SBILTA367K2LX2FEXG5TFWAS7GEFYAGY7QLFBYKC'), + [], + PlainMessage.create('test-message'), + NetworkType.MIJIN_TEST, + ); + + const aggregateTransaction = AggregateTransaction.createComplete( + Deadline.create(), + [transferTransaction.toAggregate()], + NetworkType.MIJIN_TEST, + []); + expect(aggregateTransaction.innerTransactions[0].signer).to.be.undefined; + + const signedTransaction = aggregateTransaction.signWith(account, generationHash); + + expect(signedTransaction.payload.substring(0, 8)).to.be.equal('CD000000'); + expect(signedTransaction.payload.substring(240, 256)).to.be.equal('5100000051000000'); + expect(signedTransaction.payload.substring( + 320, + signedTransaction.payload.length, + )).to.be.equal('019054419050B9837EFAB4BBE8A4B9BB32D812F9885C00D8FC1650E1420D000000746573742D6D657373616765'); + + const createdFromPayload = CreateTransactionFromPayload(signedTransaction.payload); + expect((createdFromPayload as AggregateTransaction).innerTransactions[0].signer!.publicKey) + .to.be.equal(account.publicKey); + }); + + it('should throw exception with delegated signer', () => { + expect(() => { + const transferTransaction = TransferTransaction.create( + Deadline.create(1, ChronoUnit.HOURS), + Address.createFromRawAddress('SBILTA367K2LX2FEXG5TFWAS7GEFYAGY7QLFBYKC'), + [], + PlainMessage.create('test-message'), + NetworkType.MIJIN_TEST, + ); + const aggregateTransaction = AggregateTransaction.createBonded( + Deadline.create(), + [transferTransaction.toAggregate()], + NetworkType.MIJIN_TEST, + []); + + aggregateTransaction.signWith(account, generationHash); + }).to.throw(Error, 'InnerTransaction signer must be provide. Only AggregateComplete transaction can use delegated signer.'); + }); + it('should createComplete an AggregateTransaction object with NamespaceRegistrationTransaction', () => { const registerNamespaceTransaction = NamespaceRegistrationTransaction.createRootNamespace( Deadline.create(),