Skip to content

Commit fbdbc0e

Browse files
authored
maxFee in Aggregate transactions (#539)
* Fixed #533 - Added setMaxFeeForAggregate - Changed setMaxFee for stand alone tx only - Addes getMaxCosignatures - Added getNetworkMaxCosignaturesPerAggregate * Fixed typo * Fixe typo * Fixed PR review comments * Added tests
1 parent 24cae72 commit fbdbc0e

File tree

7 files changed

+157
-8
lines changed

7 files changed

+157
-8
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/model/transaction/AggregateTransaction.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,12 +394,31 @@ export class AggregateTransaction extends Transaction {
394394
* @internal
395395
* @returns {AggregateTransaction}
396396
*/
397-
resolveAliases(statement: Statement): AggregateTransaction {
397+
public resolveAliases(statement: Statement): AggregateTransaction {
398398
const transactionInfo = this.checkTransactionHeightAndIndex();
399399
return DtoMapping.assign(this, {
400400
innerTransactions: this.innerTransactions
401401
.map((tx) => tx.resolveAliases(statement, transactionInfo.index))
402402
.sort((a, b) => a.transactionInfo!.index - b.transactionInfo!.index),
403403
});
404404
}
405+
406+
/**
407+
* Set transaction maxFee using fee multiplier for **ONLY AGGREGATE TRANSACTIONS**
408+
* @param feeMultiplier The fee multiplier
409+
* @param requiredCosignatures Required number of cosignatures
410+
* @returns {AggregateTransaction}
411+
*/
412+
public setMaxFeeForAggregate(feeMultiplier: number, requiredCosignatures: number): AggregateTransaction {
413+
if (this.type !== TransactionType.AGGREGATE_BONDED && this.type !== TransactionType.AGGREGATE_COMPLETE) {
414+
throw new Error('setMaxFeeForAggregate can only be used for aggregate transactions.');
415+
}
416+
// Check if current cosignature count is greater than requiredCosignatures.
417+
const calculatedCosignatures = requiredCosignatures > this.cosignatures.length ? requiredCosignatures : this.cosignatures.length;
418+
// Remove current cosignature length and use the calculated one.
419+
const calculatedSize = this.size - this.cosignatures.length * 96 + calculatedCosignatures * 96;
420+
return DtoMapping.assign(this, {
421+
maxFee: UInt64.fromUint(calculatedSize * feeMultiplier),
422+
});
423+
}
405424
}

src/model/transaction/Transaction.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,11 +191,14 @@ export abstract class Transaction {
191191
abstract resolveAliases(statement?: Statement, aggregateTransactionIndex?: number): Transaction;
192192

193193
/**
194-
* Set transaction maxFee using fee multiplier
194+
* Set transaction maxFee using fee multiplier for **ONLY NONE AGGREGATE TRANSACTIONS**
195195
* @param feeMultiplier The fee multiplier
196196
* @returns {TransferTransaction}
197197
*/
198198
public setMaxFee(feeMultiplier: number): Transaction {
199+
if (this.type === TransactionType.AGGREGATE_BONDED || this.type === TransactionType.AGGREGATE_COMPLETE) {
200+
throw new Error('setMaxFee can only be used for none aggregate transactions.');
201+
}
199202
return DtoMapping.assign(this, {
200203
maxFee: UInt64.fromUint(this.size * feeMultiplier),
201204
});

src/service/AggregateTransactionService.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,24 @@ import { InnerTransaction } from '../model/transaction/InnerTransaction';
2424
import { MultisigAccountModificationTransaction } from '../model/transaction/MultisigAccountModificationTransaction';
2525
import { SignedTransaction } from '../model/transaction/SignedTransaction';
2626
import { TransactionType } from '../model/transaction/TransactionType';
27+
import { Address } from '../model/account/Address';
28+
import { RepositoryFactory } from '../infrastructure/RepositoryFactory';
29+
import { NetworkRepository } from '../infrastructure/NetworkRepository';
2730

2831
/**
2932
* Aggregated Transaction service
3033
*/
3134
export class AggregateTransactionService {
35+
private readonly multisigRepository: MultisigRepository;
36+
private readonly networkRepository: NetworkRepository;
3237
/**
3338
* Constructor
3439
* @param multisigRepository
3540
*/
36-
constructor(private readonly multisigRepository: MultisigRepository) {}
41+
constructor(repositoryFactory: RepositoryFactory) {
42+
this.multisigRepository = repositoryFactory.createMultisigRepository();
43+
this.networkRepository = repositoryFactory.createNetworkRepository();
44+
}
3745

3846
/**
3947
* Check if an aggregate complete transaction has all cosignatories attached
@@ -74,6 +82,40 @@ export class AggregateTransactionService {
7482
);
7583
}
7684

85+
/**
86+
* Get total multisig account cosigner count
87+
* @param address multisig account address
88+
* @returns {Observable<number>}
89+
*/
90+
public getMaxCosignatures(address: Address): Observable<number> {
91+
return this.multisigRepository.getMultisigAccountGraphInfo(address).pipe(
92+
map((graph) => {
93+
let count = 0;
94+
graph.multisigAccounts.forEach((multisigInfo) => {
95+
multisigInfo.map((multisig) => {
96+
count += multisig.cosignatories.length;
97+
});
98+
});
99+
return count;
100+
}),
101+
);
102+
}
103+
104+
/**
105+
* Get max cosignatures allowed per aggregate from network properties
106+
* @returns {Observable<number>}
107+
*/
108+
public getNetworkMaxCosignaturesPerAggregate(): Observable<number> {
109+
return this.networkRepository.getNetworkProperties().pipe(
110+
map((properties) => {
111+
if (!properties.plugins.aggregate?.maxCosignaturesPerAggregate) {
112+
throw new Error('Cannot get maxCosignaturesPerAggregate from network properties.');
113+
}
114+
return parseInt(properties.plugins.aggregate?.maxCosignaturesPerAggregate.replace(`'`, ''));
115+
}),
116+
);
117+
}
118+
77119
/**
78120
* Validate cosignatories against multisig Account(s)
79121
* @param graphInfo - multisig account graph info

test/model/transaction/AggregateTransaction.spec.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -607,13 +607,32 @@ describe('AggregateTransaction', () => {
607607
[transferTransaction.toAggregate(account.publicAccount)],
608608
NetworkType.MIJIN_TEST,
609609
[],
610-
).setMaxFee(2);
611-
expect(aggregateTransaction.maxFee.compact()).to.be.equal(560);
610+
).setMaxFeeForAggregate(2, 10);
611+
expect(aggregateTransaction.maxFee.compact()).to.be.equal(560 + 960 * 2);
612612

613613
const signedTransaction = aggregateTransaction.signWith(account, generationHash);
614614
expect(signedTransaction.hash).not.to.be.undefined;
615615
});
616616

617+
it('Test set maxFee using multiplier', () => {
618+
const transferTransaction = TransferTransaction.create(
619+
Deadline.create(1, ChronoUnit.HOURS),
620+
unresolvedAddress,
621+
[new Mosaic(unresolvedMosaicId, UInt64.fromUint(1))],
622+
PlainMessage.create('test-message'),
623+
NetworkType.MIJIN_TEST,
624+
);
625+
626+
expect(() => {
627+
AggregateTransaction.createComplete(
628+
Deadline.create(),
629+
[transferTransaction.toAggregate(account.publicAccount)],
630+
NetworkType.MIJIN_TEST,
631+
[],
632+
).setMaxFee(2);
633+
}).to.throw();
634+
});
635+
617636
it('Test resolveAlias can resolve', () => {
618637
const transferTransaction = new TransferTransaction(
619638
NetworkType.MIJIN_TEST,

test/model/transaction/TransferTransaction.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { Deadline } from '../../../src/model/transaction/Deadline';
3535
import { TransferTransaction } from '../../../src/model/transaction/TransferTransaction';
3636
import { UInt64 } from '../../../src/model/UInt64';
3737
import { TestingAccount } from '../../conf/conf.spec';
38+
import { AggregateTransaction } from '../../../src/model/transaction/AggregateTransaction';
3839

3940
describe('TransferTransaction', () => {
4041
let account: Account;
@@ -382,6 +383,25 @@ describe('TransferTransaction', () => {
382383
expect(signedTransaction.hash).not.to.be.undefined;
383384
});
384385

386+
it('Test set maxFee using multiplier to throw', () => {
387+
const transferTransaction = TransferTransaction.create(
388+
Deadline.create(),
389+
Address.createFromRawAddress('SBILTA367K2LX2FEXG5TFWAS7GEFYAGY7QLFBYKC'),
390+
[NetworkCurrencyLocal.createAbsolute(1)],
391+
PlainMessage.create('test-message'),
392+
NetworkType.MIJIN_TEST,
393+
);
394+
395+
expect(() => {
396+
AggregateTransaction.createComplete(
397+
Deadline.create(),
398+
[transferTransaction.toAggregate(account.publicAccount)],
399+
NetworkType.MIJIN_TEST,
400+
[],
401+
).setMaxFee(2);
402+
}).to.throw();
403+
});
404+
385405
it('Test resolveAlias can resolve', () => {
386406
const transferTransaction = new TransferTransaction(
387407
NetworkType.MIJIN_TEST,

test/service/AggregateTransactionService.spec.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ import { Deadline } from '../../src/model/transaction/Deadline';
3131
import { MultisigAccountModificationTransaction } from '../../src/model/transaction/MultisigAccountModificationTransaction';
3232
import { TransferTransaction } from '../../src/model/transaction/TransferTransaction';
3333
import { AggregateTransactionService } from '../../src/service/AggregateTransactionService';
34+
import { RepositoryFactory } from '../../src/infrastructure/RepositoryFactory';
35+
import { NetworkRepository } from '../../src/infrastructure/NetworkRepository';
36+
import { NetworkConfigurationDTO, PluginsPropertiesDTO, AggregateNetworkPropertiesDTO } from 'symbol-openapi-typescript-node-client';
3437

3538
/**
3639
* For multi level multisig scenario visit: https://github.com/nemtech/symbol-docs/issues/10
@@ -131,8 +134,20 @@ describe('AggregateTransactionService', () => {
131134
return new MultisigAccountGraphInfo(map);
132135
}
133136

137+
function getNetworkProperties(input: string): NetworkConfigurationDTO {
138+
const body = new NetworkConfigurationDTO();
139+
const plugin = new PluginsPropertiesDTO();
140+
plugin.aggregate = new AggregateNetworkPropertiesDTO();
141+
plugin.aggregate.maxCosignaturesPerAggregate = input;
142+
body.plugins = plugin;
143+
return body;
144+
}
145+
146+
let mockNetworkRepository: NetworkRepository;
134147
before(() => {
135-
const mockedAccountRepository: MultisigRepository = mock();
148+
mockNetworkRepository = mock<NetworkRepository>();
149+
const mockRepoFactory = mock<RepositoryFactory>();
150+
const mockedAccountRepository: MultisigRepository = mock<MultisigRepository>();
136151

137152
when(mockedAccountRepository.getMultisigAccountInfo(deepEqual(account1.address))).thenReturn(observableOf(givenAccount1Info()));
138153
when(mockedAccountRepository.getMultisigAccountInfo(deepEqual(account4.address))).thenReturn(observableOf(givenAccount4Info()));
@@ -152,7 +167,11 @@ describe('AggregateTransactionService', () => {
152167
when(mockedAccountRepository.getMultisigAccountInfo(deepEqual(account3.address))).thenReturn(observableOf(givenAccount3Info()));
153168

154169
const accountRepository = instance(mockedAccountRepository);
155-
aggregateTransactionService = new AggregateTransactionService(accountRepository);
170+
const networkRespository = instance(mockNetworkRepository);
171+
const repoFactory = instance(mockRepoFactory);
172+
when(mockRepoFactory.createMultisigRepository()).thenReturn(accountRepository);
173+
when(mockRepoFactory.createNetworkRepository()).thenReturn(networkRespository);
174+
aggregateTransactionService = new AggregateTransactionService(repoFactory);
156175
});
157176

158177
it('should return isComplete: true for aggregated complete transaction - 2 levels Multisig', () => {
@@ -580,4 +599,31 @@ describe('AggregateTransactionService', () => {
580599
expect(isComplete).to.be.false;
581600
});
582601
});
602+
603+
it('should call getNetworkMaxCosignaturesPerAggregate and returns', async () => {
604+
when(mockNetworkRepository.getNetworkProperties()).thenReturn(observableOf(getNetworkProperties('15')));
605+
const max = await aggregateTransactionService.getNetworkMaxCosignaturesPerAggregate().toPromise();
606+
expect(max).to.be.equal(15);
607+
});
608+
609+
it('should call getNetworkMaxCosignaturesPerAggregate and returns with single quote', async () => {
610+
when(mockNetworkRepository.getNetworkProperties()).thenReturn(observableOf(getNetworkProperties(`1'000`)));
611+
const max = await aggregateTransactionService.getNetworkMaxCosignaturesPerAggregate().toPromise();
612+
expect(max).to.be.equal(1000);
613+
});
614+
615+
it('should call getNetworkMaxCosignaturesPerAggregate and throw', () => {
616+
when(mockNetworkRepository.getNetworkProperties()).thenReturn(observableOf(getNetworkProperties('')));
617+
aggregateTransactionService
618+
.getNetworkMaxCosignaturesPerAggregate()
619+
.toPromise()
620+
.catch((error) => {
621+
expect(error).not.to.be.undefined;
622+
});
623+
});
624+
625+
it('should call getMaxCosignatures and returns', async () => {
626+
const max = await aggregateTransactionService.getMaxCosignatures(multisig2.address).toPromise();
627+
expect(max).to.be.equal(4);
628+
});
583629
});

0 commit comments

Comments
 (0)