Skip to content

Commit 18a5f2a

Browse files
committed
1. Addes service for Aggregated Transaction - isComplete 2. Fixed couple of bugs in TransactionMappings
1 parent 1cef7f8 commit 18a5f2a

File tree

2 files changed

+369
-0
lines changed

2 files changed

+369
-0
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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 { map, mergeMap} 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 { SignedTransaction } from '../model/transaction/SignedTransaction';
24+
25+
/**
26+
* Aggregated Transaction service
27+
*/
28+
export class AggregatedTransactionService {
29+
30+
/**
31+
* Constructor
32+
* @param accountHttp
33+
*/
34+
constructor(private readonly accountHttp: AccountHttp) {
35+
}
36+
37+
/**
38+
* Check if an aggregate complete transaction has all cosignatories attached
39+
* @param signedTransaction - The signed aggregate transaction (complete) to be verified
40+
* @returns {Observable<boolean>}
41+
*/
42+
public isComplete(signedTransaction: SignedTransaction): Observable<boolean> {
43+
const aggregateTransaction = TransactionMapping.createFromPayload(signedTransaction.payload) as AggregateTransaction;
44+
/**
45+
* Include both initiator & cosigners
46+
*/
47+
const signers = (aggregateTransaction.cosignatures.map((cosigner) => cosigner.signer.publicKey));
48+
if (signedTransaction.signer) {
49+
signers.push(signedTransaction.signer);
50+
}
51+
52+
return observableFrom(aggregateTransaction.innerTransactions).pipe(
53+
mergeMap((innerTransaction) => this.accountHttp.getMultisigAccountInfo(innerTransaction.signer.address)
54+
.pipe(
55+
/**
56+
* For multisig account, we need to get the graph info in case it has multiple levels
57+
*/
58+
mergeMap((_) => _.minApproval !== 0 && _.minRemoval !== 0 ?
59+
this.accountHttp.getMultisigAccountGraphInfo(_.account.address)
60+
.pipe(
61+
map((graphInfo) => this.validateCosignatories(graphInfo, signers)),
62+
) : observableOf(true),
63+
),
64+
),
65+
),
66+
);
67+
}
68+
69+
/**
70+
* Validate cosignatories against multisig Account(s)
71+
* @param graphInfo - multisig account graph info
72+
* @param cosignatories - array of cosignatories extracted from aggregated transaction
73+
* @returns {boolean}
74+
*/
75+
private validateCosignatories(graphInfo: MultisigAccountGraphInfo, cosignatories: string[]): boolean {
76+
/**
77+
* Validate cosignatories from bottom level to top
78+
*/
79+
const sortedKeys = Array.from(graphInfo.multisigAccounts.keys()).sort((a, b) => b - a);
80+
const cosignatoriesReceived = cosignatories;
81+
let validationResult = true;
82+
83+
sortedKeys.forEach((key) => {
84+
const multisigInfo = graphInfo.multisigAccounts.get(key);
85+
if (multisigInfo && validationResult) {
86+
multisigInfo.forEach((multisig) => {
87+
if (multisig.minApproval >= 1) {
88+
const matchedCosignatories = this.compareArrays(cosignatoriesReceived,
89+
multisig.cosignatories.map((cosig) => cosig.publicKey));
90+
91+
/**
92+
* if minimal signature requirement met at current level, push the multisig account
93+
* into the received signatories array for next level validation.
94+
* Otherwise return validation failed.
95+
*/
96+
if (matchedCosignatories.length >= multisig.minApproval) {
97+
if (cosignatoriesReceived.indexOf(multisig.account.publicKey) === -1) {
98+
cosignatoriesReceived.push(multisig.account.publicKey);
99+
}
100+
} else {
101+
validationResult = false;
102+
}
103+
}
104+
});
105+
}
106+
});
107+
108+
return validationResult;
109+
}
110+
111+
/**
112+
* Compare two string arrays
113+
* @param array1 - base array
114+
* @param array2 - array to be matched
115+
* @returns {string[]} - array of matched elements
116+
*/
117+
private compareArrays(array1: string[], array2: string[]): string[] {
118+
const results: string[] = [];
119+
array1.forEach((a1) => array2.forEach((a2) => {
120+
if (a1 === a2) {
121+
results.push(a1);
122+
}
123+
}));
124+
125+
return results;
126+
}
127+
}
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
/*
2+
* Copyright 2018 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 { expect } from 'chai';
18+
import { ChronoUnit } from 'js-joda';
19+
import {of as observableOf} from 'rxjs';
20+
import {deepEqual, instance, mock, when} from 'ts-mockito';
21+
import { AccountHttp } from '../../src/infrastructure/AccountHttp';
22+
import { Account } from '../../src/model/account/Account';
23+
import { Address } from '../../src/model/account/Address';
24+
import { MultisigAccountGraphInfo } from '../../src/model/account/MultisigAccountGraphInfo';
25+
import { MultisigAccountInfo } from '../../src/model/account/MultisigAccountInfo';
26+
import {NetworkType} from '../../src/model/blockchain/NetworkType';
27+
import { AggregateTransaction } from '../../src/model/transaction/AggregateTransaction';
28+
import { Deadline } from '../../src/model/transaction/Deadline';
29+
import { PlainMessage } from '../../src/model/transaction/PlainMessage';
30+
import { TransferTransaction } from '../../src/model/transaction/TransferTransaction';
31+
import { AggregatedTransactionService } from '../../src/service/AggregatedTransactionService';
32+
33+
/**
34+
* For multi level multisig scenario visit: https://github.com/nemtech/nem2-docs/issues/10
35+
*/
36+
describe('AggregatedTransactionService', () => {
37+
let aggregatedTransactionService: AggregatedTransactionService;
38+
39+
/**
40+
* Multisig2 Account: SBROWP-7YMG2M-K45RO6-Q7ZPK7-G7GXWQ-JK5VNQ-OSUX
41+
* Public Key: 5E628EA59818D97AA4118780D9A88C5512FCE7A21C195E1574727EFCE5DF7C0D
42+
* Private Key: 22A1D67F8519D1A45BD7116600BB6E857786E816FE0B45E4C5B9FFF3D64BC177
43+
*
44+
*
45+
* Multisig1 Account: SAK32M-5JQ43R-WYHWEH-WRBCW4-RXERT2-DLASGL-EANS
46+
* Public Key: BFDF2610C5666A626434FE12FB4A9D896D2B9B033F5F84CCEABE82E043A6307E
47+
* Private Key: 8B0622C2CCFC5CCC5A74B500163E3C68F3AD3643DB12932FC931143EAC67280D
48+
*/
49+
50+
/**
51+
* Test accounts:
52+
* Multisig1 (1/1): Account2, Account3
53+
* Multisig2 (2/1): Account1, Multisig1
54+
* Stranger Account: Account4
55+
*/
56+
57+
const account1 = Account.createFromPrivateKey('82DB2528834C9926F0FCCE042466B24A266F5B685CB66D2869AF6648C043E950',
58+
NetworkType.MIJIN_TEST);
59+
const multisig1 = Account.createFromPrivateKey('8B0622C2CCFC5CCC5A74B500163E3C68F3AD3643DB12932FC931143EAC67280D',
60+
NetworkType.MIJIN_TEST);
61+
const multisig2 = Account.createFromPrivateKey('22A1D67F8519D1A45BD7116600BB6E857786E816FE0B45E4C5B9FFF3D64BC177',
62+
NetworkType.MIJIN_TEST);
63+
64+
const account2 = Account.createFromPrivateKey('A4D410270E01CECDCDEADCDE32EC79C8D9CDEA4DCD426CB1EB666EFEF148FBCE',
65+
NetworkType.MIJIN_TEST);
66+
const account3 = Account.createFromPrivateKey('336AB45EE65A6AFFC0E7ADC5342F91E34BACA0B901A1D9C876FA25A1E590077E',
67+
NetworkType.MIJIN_TEST);
68+
69+
const account4 = Account.createFromPrivateKey('4D8B3756592532753344E11E2B7541317BCCFBBCF4444274CDBF359D2C4AE0F1',
70+
NetworkType.MIJIN_TEST);
71+
before(() => {
72+
const mockedAccountHttp = mock(AccountHttp);
73+
74+
when(mockedAccountHttp.getMultisigAccountInfo(deepEqual(account1.address)))
75+
.thenReturn(observableOf(givenAccount1Info()));
76+
when(mockedAccountHttp.getMultisigAccountInfo(deepEqual(account4.address)))
77+
.thenReturn(observableOf(givenAccount4Info()));
78+
when(mockedAccountHttp.getMultisigAccountInfo(deepEqual(multisig2.address)))
79+
.thenReturn(observableOf(givenMultisig2AccountInfo()));
80+
when(mockedAccountHttp.getMultisigAccountGraphInfo(deepEqual(multisig2.address)))
81+
.thenReturn(observableOf(givenMultisig2AccountGraphInfo()));
82+
83+
const accountHttp = instance(mockedAccountHttp);
84+
aggregatedTransactionService = new AggregatedTransactionService(accountHttp);
85+
});
86+
87+
it('should return isComplete: true for aggregated complete transaction - 2 levels Multisig', () => {
88+
const transferTransaction = TransferTransaction.create(
89+
Deadline.create(1, ChronoUnit.HOURS),
90+
Address.createFromRawAddress('SBILTA367K2LX2FEXG5TFWAS7GEFYAGY7QLFBYKC'),
91+
[],
92+
PlainMessage.create('test-message'),
93+
NetworkType.MIJIN_TEST,
94+
);
95+
96+
const aggregateTransaction = AggregateTransaction.createComplete(
97+
Deadline.create(),
98+
[transferTransaction.toAggregate(multisig2.publicAccount)],
99+
NetworkType.MIJIN_TEST,
100+
[]);
101+
102+
const signedTransaction = aggregateTransaction.signTransactionWithCosignatories(account1, [account2]);
103+
aggregatedTransactionService.isComplete(signedTransaction).toPromise().then((isComplete) => {
104+
expect(isComplete).to.be.true;
105+
});
106+
});
107+
108+
it('should return isComplete: false for aggregated complete transaction - 2 levels Multisig', () => {
109+
const transferTransaction = TransferTransaction.create(
110+
Deadline.create(1, ChronoUnit.HOURS),
111+
Address.createFromRawAddress('SBILTA367K2LX2FEXG5TFWAS7GEFYAGY7QLFBYKC'),
112+
[],
113+
PlainMessage.create('test-message'),
114+
NetworkType.MIJIN_TEST,
115+
);
116+
117+
const aggregateTransaction = AggregateTransaction.createComplete(
118+
Deadline.create(),
119+
[transferTransaction.toAggregate(multisig2.publicAccount)],
120+
NetworkType.MIJIN_TEST,
121+
[]);
122+
123+
const signedTransaction = aggregateTransaction.signTransactionWithCosignatories(account1, []);
124+
aggregatedTransactionService.isComplete(signedTransaction).toPromise().then((isComplete) => {
125+
expect(isComplete).to.be.false;
126+
});
127+
});
128+
129+
it('should return isComplete: false for aggregated complete transaction - 2 levels Multisig', () => {
130+
const transferTransaction = TransferTransaction.create(
131+
Deadline.create(1, ChronoUnit.HOURS),
132+
Address.createFromRawAddress('SBILTA367K2LX2FEXG5TFWAS7GEFYAGY7QLFBYKC'),
133+
[],
134+
PlainMessage.create('test-message'),
135+
NetworkType.MIJIN_TEST,
136+
);
137+
138+
const aggregateTransaction = AggregateTransaction.createComplete(
139+
Deadline.create(),
140+
[transferTransaction.toAggregate(multisig2.publicAccount)],
141+
NetworkType.MIJIN_TEST,
142+
[]);
143+
144+
const signedTransaction = aggregateTransaction.signTransactionWithCosignatories(account1, [account4]);
145+
aggregatedTransactionService.isComplete(signedTransaction).toPromise().then((isComplete) => {
146+
expect(isComplete).to.be.false;
147+
});
148+
});
149+
150+
it('should return correct isComplete status for aggregated complete transaction - 2 levels Multisig, multi inner transaction', () => {
151+
const transferTransaction = TransferTransaction.create(
152+
Deadline.create(1, ChronoUnit.HOURS),
153+
Address.createFromRawAddress('SBILTA367K2LX2FEXG5TFWAS7GEFYAGY7QLFBYKC'),
154+
[],
155+
PlainMessage.create('test-message'),
156+
NetworkType.MIJIN_TEST,
157+
);
158+
159+
const transferTransaction2 = TransferTransaction.create(
160+
Deadline.create(1, ChronoUnit.HOURS),
161+
Address.createFromRawAddress('SBILTA367K2LX2FEXG5TFWAS7GEFYAGY7QLFBYKC'),
162+
[],
163+
PlainMessage.create('test-message'),
164+
NetworkType.MIJIN_TEST,
165+
);
166+
167+
const aggregateTransaction = AggregateTransaction.createComplete(
168+
Deadline.create(),
169+
[transferTransaction.toAggregate(multisig2.publicAccount),
170+
transferTransaction.toAggregate(account4.publicAccount)],
171+
NetworkType.MIJIN_TEST,
172+
[]);
173+
174+
const signedTransaction = aggregateTransaction.signTransactionWithCosignatories(account1, [account2]);
175+
aggregatedTransactionService.isComplete(signedTransaction).toPromise().then((isComplete) => {
176+
expect(isComplete).to.be.true;
177+
});
178+
});
179+
180+
it('should return correct isComplete status for aggregated complete transaction - none multisig', () => {
181+
const transferTransaction = TransferTransaction.create(
182+
Deadline.create(1, ChronoUnit.HOURS),
183+
Address.createFromRawAddress('SBILTA367K2LX2FEXG5TFWAS7GEFYAGY7QLFBYKC'),
184+
[],
185+
PlainMessage.create('test-message'),
186+
NetworkType.MIJIN_TEST,
187+
);
188+
189+
const aggregateTransaction = AggregateTransaction.createComplete(
190+
Deadline.create(),
191+
[transferTransaction.toAggregate(account4.publicAccount)],
192+
NetworkType.MIJIN_TEST,
193+
[]);
194+
195+
const signedTransaction = aggregateTransaction.signWith(account1);
196+
aggregatedTransactionService.isComplete(signedTransaction).toPromise().then((isComplete) => {
197+
expect(isComplete).to.be.true;
198+
});
199+
});
200+
201+
function givenMultisig2AccountInfo(): MultisigAccountInfo {
202+
return new MultisigAccountInfo(multisig2.publicAccount,
203+
2, 1,
204+
[multisig1.publicAccount,
205+
account1.publicAccount],
206+
[],
207+
);
208+
}
209+
210+
function givenAccount1Info(): MultisigAccountInfo {
211+
return new MultisigAccountInfo(account1.publicAccount,
212+
0, 0,
213+
[],
214+
[multisig2.publicAccount],
215+
);
216+
}
217+
function givenAccount4Info(): MultisigAccountInfo {
218+
return new MultisigAccountInfo(account4.publicAccount,
219+
0, 0,
220+
[],
221+
[],
222+
);
223+
}
224+
225+
function givenMultisig2AccountGraphInfo(): MultisigAccountGraphInfo {
226+
const map = new Map<number, MultisigAccountInfo[]>();
227+
map.set(0, [new MultisigAccountInfo(multisig2.publicAccount,
228+
2, 1,
229+
[multisig1.publicAccount,
230+
account1.publicAccount],
231+
[],
232+
)])
233+
.set(1, [new MultisigAccountInfo(multisig1.publicAccount,
234+
1, 1,
235+
[account2.publicAccount, account3.publicAccount],
236+
[multisig2.publicAccount],
237+
)]);
238+
239+
return new MultisigAccountGraphInfo(map);
240+
}
241+
242+
});

0 commit comments

Comments
 (0)