Skip to content

Commit 4282d6a

Browse files
author
Grégory Saive
authored
Merge pull request #106 from rg911/task/g4_check_act_has_all_sosignatories
Task/g4 check aggregate complete transaction has all cosignatories
2 parents 54d8778 + ab012c8 commit 4282d6a

File tree

5 files changed

+724
-8
lines changed

5 files changed

+724
-8
lines changed

src/infrastructure/transaction/CreateTransactionFromPayload.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -376,8 +376,8 @@ const CreateTransaction = (type: number, transactionData: string, networkType: N
376376
}),
377377
networkType,
378378
consignatureArray ? consignatureArray.map((cosignature) => new AggregateTransactionCosignature(
379-
cosignature.substring(0, 64),
380-
PublicAccount.createFromPublicKey(cosignature.substring(64, 192), networkType),
379+
cosignature.substring(64, 192),
380+
PublicAccount.createFromPublicKey(cosignature.substring(0, 64), networkType),
381381
)) : [],
382382
);
383383
case TransactionType.AGGREGATE_BONDED:
@@ -400,8 +400,8 @@ const CreateTransaction = (type: number, transactionData: string, networkType: N
400400
}),
401401
networkType,
402402
bondedConsignatureArray ? bondedConsignatureArray.map((cosignature) => new AggregateTransactionCosignature(
403-
cosignature.substring(0, 64),
404-
PublicAccount.createFromPublicKey(cosignature.substring(64, 192), networkType),
403+
cosignature.substring(64, 192),
404+
PublicAccount.createFromPublicKey(cosignature.substring(0, 64), networkType),
405405
)) : [],
406406
);
407407
default:
@@ -456,10 +456,10 @@ const parseInnerTransactionFromBinary = (innerTransactionBinary: string): string
456456
let innerBinary = innerTransactionBinary;
457457

458458
while (innerBinary.length) {
459-
const payloadSize = parseInt(convert.uint8ToHex(convert.hexToUint8(innerTransactionBinary.substring(0, 8)).reverse()), 16) * 2;
460-
const innerTransaction = innerTransactionBinary.substring(8, 8 + payloadSize);
459+
const payloadSize = parseInt(convert.uint8ToHex(convert.hexToUint8(innerBinary.substring(0, 8)).reverse()), 16) * 2;
460+
const innerTransaction = innerBinary.substring(8, payloadSize);
461461
embeddedTransaction.push(innerTransaction);
462-
innerBinary = innerTransactionBinary.substring(8 + payloadSize);
462+
innerBinary = innerBinary.substring(payloadSize);
463463
}
464464
return embeddedTransaction;
465465
};

src/model/transaction/Transaction.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ export abstract class Transaction {
211211
const commonTransactionObject = {
212212
type: this.type,
213213
networkType: this.networkType,
214-
version: this.version,
214+
version: this.versionToDTO(),
215215
maxFee: this.maxFee.toDTO(),
216216
deadline: this.deadline.toDTO(),
217217
signature: this.signature ? this.signature : '',
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* Copyright 2019 NEM
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {from as observableFrom , Observable, of as observableOf} from 'rxjs';
18+
import { flatMap, map, mergeMap, toArray} from 'rxjs/operators';
19+
import { TransactionMapping } from '../core/utils/TransactionMapping';
20+
import { AccountHttp } from '../infrastructure/AccountHttp';
21+
import { MultisigAccountGraphInfo } from '../model/account/MultisigAccountGraphInfo';
22+
import { AggregateTransaction } from '../model/transaction/AggregateTransaction';
23+
import { InnerTransaction } from '../model/transaction/InnerTransaction';
24+
import { ModifyMultisigAccountTransaction } from '../model/transaction/ModifyMultisigAccountTransaction';
25+
import { MultisigCosignatoryModificationType } from '../model/transaction/MultisigCosignatoryModificationType';
26+
import { SignedTransaction } from '../model/transaction/SignedTransaction';
27+
import { TransactionType } from '../model/transaction/TransactionType';
28+
29+
/**
30+
* Aggregated Transaction service
31+
*/
32+
export class AggregateTransactionService {
33+
34+
/**
35+
* Constructor
36+
* @param accountHttp
37+
*/
38+
constructor(private readonly accountHttp: AccountHttp) {
39+
}
40+
41+
/**
42+
* Check if an aggregate complete transaction has all cosignatories attached
43+
* @param signedTransaction - The signed aggregate transaction (complete) to be verified
44+
* @returns {Observable<boolean>}
45+
*/
46+
public isComplete(signedTransaction: SignedTransaction): Observable<boolean> {
47+
const aggregateTransaction = TransactionMapping.createFromPayload(signedTransaction.payload) as AggregateTransaction;
48+
/**
49+
* Include both initiator & cosigners
50+
*/
51+
const signers = (aggregateTransaction.cosignatures.map((cosigner) => cosigner.signer.publicKey));
52+
if (signedTransaction.signer) {
53+
signers.push(signedTransaction.signer);
54+
}
55+
return observableFrom(aggregateTransaction.innerTransactions).pipe(
56+
mergeMap((innerTransaction) => this.accountHttp.getMultisigAccountInfo(innerTransaction.signer.address)
57+
.pipe(
58+
/**
59+
* For multisig account, we need to get the graph info in case it has multiple levels
60+
*/
61+
mergeMap((_) => _.minApproval !== 0 && _.minRemoval !== 0 ?
62+
this.accountHttp.getMultisigAccountGraphInfo(_.account.address)
63+
.pipe(
64+
map((graphInfo) => this.validateCosignatories(graphInfo, signers, innerTransaction)),
65+
) : observableOf(signers.find((s) => s === _.account.publicKey ) !== undefined),
66+
),
67+
),
68+
),
69+
toArray(),
70+
).pipe(
71+
flatMap((results) => {
72+
return observableOf(results.every((isComplete) => isComplete));
73+
}),
74+
);
75+
}
76+
77+
/**
78+
* Validate cosignatories against multisig Account(s)
79+
* @param graphInfo - multisig account graph info
80+
* @param cosignatories - array of cosignatories extracted from aggregated transaction
81+
* @param innerTransaction - the inner transaction of the aggregated transaction
82+
* @returns {boolean}
83+
*/
84+
private validateCosignatories(graphInfo: MultisigAccountGraphInfo,
85+
cosignatories: string[],
86+
innerTransaction: InnerTransaction): boolean {
87+
/**
88+
* Validate cosignatories from bottom level to top
89+
*/
90+
const sortedKeys = Array.from(graphInfo.multisigAccounts.keys()).sort((a, b) => b - a);
91+
const cosignatoriesReceived = cosignatories;
92+
let validationResult = false;
93+
94+
let isMultisigRemoval = false;
95+
96+
/**
97+
* Check inner transaction. If remove cosigner from multisig account,
98+
* use minRemoval instead of minApproval for cosignatories validation.
99+
*/
100+
if (innerTransaction.type === TransactionType.MODIFY_MULTISIG_ACCOUNT) {
101+
if ((innerTransaction as ModifyMultisigAccountTransaction).modifications
102+
.find((modification) => modification.type === MultisigCosignatoryModificationType.Remove) !== undefined) {
103+
isMultisigRemoval = true;
104+
}
105+
}
106+
107+
sortedKeys.forEach((key) => {
108+
const multisigInfo = graphInfo.multisigAccounts.get(key);
109+
if (multisigInfo && !validationResult) {
110+
multisigInfo.forEach((multisig) => {
111+
if (multisig.minApproval >= 1 && multisig.minRemoval) { // To make sure it is multisig account
112+
const matchedCosignatories = this.compareArrays(cosignatoriesReceived,
113+
multisig.cosignatories.map((cosig) => cosig.publicKey));
114+
115+
/**
116+
* if minimal signature requirement met at current level, push the multisig account
117+
* into the received signatories array for next level validation.
118+
* Otherwise return validation failed.
119+
*/
120+
if ((matchedCosignatories.length >= multisig.minApproval && !isMultisigRemoval) ||
121+
(matchedCosignatories.length >= multisig.minRemoval && isMultisigRemoval)) {
122+
if (cosignatoriesReceived.indexOf(multisig.account.publicKey) === -1) {
123+
cosignatoriesReceived.push(multisig.account.publicKey);
124+
}
125+
validationResult = true;
126+
} else {
127+
validationResult = false;
128+
}
129+
}
130+
});
131+
}
132+
});
133+
134+
return validationResult;
135+
}
136+
137+
/**
138+
* Compare two string arrays
139+
* @param array1 - base array
140+
* @param array2 - array to be matched
141+
* @returns {string[]} - array of matched elements
142+
*/
143+
private compareArrays(array1: string[], array2: string[]): string[] {
144+
const results: string[] = [];
145+
array1.forEach((a1) => array2.forEach((a2) => {
146+
if (a1 === a2) {
147+
results.push(a1);
148+
}
149+
}));
150+
151+
return results;
152+
}
153+
}

src/service/service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@
1616

1717
export * from './NamespaceService';
1818
export * from './MosaicService';
19+
export * from './AggregateTransactionService';

0 commit comments

Comments
 (0)