Skip to content

Commit 2630c1f

Browse files
authored
Multisig multilevel subscription (#800)
* fix #798 - fixed multilevel multisig description. - updated rxjs version. * clean package.json * update function return type and add docs * fix sonarcloud bugs * add type for multisig children tree * refactor and adding docs * linting
1 parent 81f5397 commit 2630c1f

File tree

9 files changed

+165
-24
lines changed

9 files changed

+165
-24
lines changed

.DS_Store

6 KB
Binary file not shown.

.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ ts-docs/
1515
.travis
1616
*.iml
1717
*.log
18+
.npm-pack-all-tmp

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@
110110
"minimist": "^1.2.5",
111111
"node-fetch": "^2.6.0",
112112
"ripemd160": "^2.0.2",
113-
"rxjs": "^6.6.3",
113+
"rxjs": "^6.6.7",
114114
"rxjs-compat": "^6.6.3",
115115
"symbol-openapi-typescript-fetch-client": "1.0.1-SNAPSHOT.202106160954",
116116
"tweetnacl": "^1.0.3",

src/core/format/Utilities.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,18 @@ export const decodeBlock = (input: any, inputOffset: number, output: any, output
173173
output[outputOffset + 3] = ((bytes[4] & 0x01) << 7) | (bytes[5] << 2) | (bytes[6] >> 3);
174174
output[outputOffset + 4] = ((bytes[6] & 0x07) << 5) | bytes[7];
175175
};
176+
177+
/**
178+
* Traverses the tree object to pick addresses strings.
179+
* @param {array} array of multisig children
180+
* @param {parse} function to parse tree and pick children addresses
181+
*/
182+
export const parseObjectProperties = (obj: [], parse): any => {
183+
for (const k in obj) {
184+
if (typeof obj[k] === 'object' && obj[k] !== null) {
185+
parseObjectProperties(obj[k], parse);
186+
} else if (Object.prototype.hasOwnProperty.call(obj, k)) {
187+
parse(k, obj[k]);
188+
}
189+
}
190+
};

src/core/utils/MultisigGraphUtils.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2020 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 { MultisigAccountGraphInfo } from '../../model/account/MultisigAccountGraphInfo';
18+
import { MultisigAccountInfo } from '../../model/account/MultisigAccountInfo';
19+
20+
/**
21+
* MultisigGraph utilities
22+
*/
23+
24+
// Type for Multisig Tree children Object
25+
export type MultisigChildrenTreeObject = {
26+
address: string;
27+
children: []; // children array.
28+
};
29+
30+
export class MultisigGraphUtils {
31+
/**
32+
* creates a structred Tree object containing Current multisig account with children
33+
* @param {MultisigAccountInfo[][]} multisigEnteries
34+
* @returns {MultisigChildrenTreeObject[]} Array of multisigChildrentTree objects
35+
*/
36+
public static getMultisigChildren(multisigAccountGraphInfoMapped: MultisigAccountInfo[][]): MultisigChildrenTreeObject[] {
37+
if (multisigAccountGraphInfoMapped) {
38+
const mappedTree: MultisigChildrenTreeObject[] = [];
39+
multisigAccountGraphInfoMapped.forEach((level: MultisigAccountInfo[]) => {
40+
level.forEach((entry: MultisigAccountInfo) => {
41+
mappedTree.push({
42+
address: entry.accountAddress.plain(),
43+
children: [],
44+
});
45+
// find the entry matching with address matching cosignatory address and update his children
46+
const updateRecursively = (address: string, object: MultisigChildrenTreeObject) => (obj): any => {
47+
if (obj.address === address) {
48+
obj.children.push(object);
49+
} else if (obj.children) {
50+
obj.children.forEach(updateRecursively(address, object));
51+
}
52+
};
53+
entry.cosignatoryAddresses.forEach((addressVal) => {
54+
mappedTree.forEach(
55+
updateRecursively(addressVal['address'], {
56+
address: entry.accountAddress.plain(),
57+
children: [],
58+
}),
59+
);
60+
});
61+
});
62+
});
63+
return mappedTree;
64+
}
65+
return [];
66+
}
67+
/**
68+
* sort entries based on tree hierarchy from top to bottom
69+
* @param {Map<number, MultisigAccountInfo[]>} multisigEnteries
70+
* @returns {MultisigAccountInfo[]} sorted multisig graph
71+
*/
72+
private static getMultisigGraphArraySorted(multisigEntries: Map<number, MultisigAccountInfo[]>): MultisigAccountInfo[][] {
73+
return [...multisigEntries.keys()]
74+
.sort((a, b) => b - a) // Get addresses from top to bottom
75+
.map((key) => multisigEntries.get(key) || [])
76+
.filter((x) => x.length > 0);
77+
}
78+
/**
79+
* returns sorted tree entries
80+
* @param {MultisigAccountGraphInfo} graphInfo
81+
* @returns {MultisigAccountInfo[][]} array of sorted multisigInfo
82+
*/
83+
public static getMultisigInfoFromMultisigGraphInfo(graphInfo: MultisigAccountGraphInfo): MultisigAccountInfo[][] {
84+
const { multisigEntries } = graphInfo;
85+
return [...this.getMultisigGraphArraySorted(multisigEntries)].map((item) => item);
86+
}
87+
}

src/core/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22

33
export * from './DtoMapping';
44
export * from './LockHashUtils';
5+
export * from './MultisigGraphUtils';
56
export * from './TransactionMapping';
67
export * from './UnresolvedMapping';

src/infrastructure/Listener.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ import { Observable, of, Subject } from 'rxjs';
1818
import { catchError, distinctUntilChanged, filter, map, mergeMap, share, switchMap } from 'rxjs/operators';
1919
import { BlockInfoDTO } from 'symbol-openapi-typescript-fetch-client';
2020
import * as WebSocket from 'ws';
21-
import { UnresolvedAddress } from '../model';
21+
import { parseObjectProperties } from '../core/format/Utilities';
22+
import { MultisigChildrenTreeObject, MultisigGraphUtils } from '../core/utils';
23+
import { MultisigAccountInfo, UnresolvedAddress } from '../model';
2224
import { Address } from '../model/account/Address';
2325
import { PublicAccount } from '../model/account/PublicAccount';
2426
import { FinalizedBlock } from '../model/blockchain/FinalizedBlock';
@@ -34,7 +36,6 @@ import { MultisigHttp } from './MultisigHttp';
3436
import { MultisigRepository } from './MultisigRepository';
3537
import { NamespaceRepository } from './NamespaceRepository';
3638
import { CreateTransactionFromDTO } from './transaction/CreateTransactionFromDTO';
37-
3839
export enum ListenerChannelName {
3940
block = 'block',
4041
confirmedAdded = 'confirmedAdded',
@@ -596,9 +597,18 @@ export class Listener implements IListener {
596597
}
597598
return this.getResolvedAddress(cosigner).pipe(
598599
mergeMap((address: Address) => {
599-
return this.multisigRepository!.getMultisigAccountInfo(address).pipe(
600+
return this.multisigRepository!.getMultisigAccountGraphInfo(address).pipe(
600601
map((multisigInfo) => {
601-
const subscribers = [cosigner].concat(multisigInfo.multisigAddresses);
602+
const multisigGraphInfo: MultisigAccountInfo[][] = MultisigGraphUtils.getMultisigInfoFromMultisigGraphInfo(
603+
multisigInfo,
604+
);
605+
const multisigChildren: MultisigChildrenTreeObject[] = MultisigGraphUtils.getMultisigChildren(multisigGraphInfo);
606+
const subscribers: Address[] = [address];
607+
if (!!multisigChildren.length && multisigChildren[0].children) {
608+
parseObjectProperties(multisigChildren[0].children, (k, prop: string) => {
609+
subscribers.push(Address.createFromRawAddress(prop));
610+
});
611+
}
602612
subscribers.forEach((m) => {
603613
this.subscribeTo(`${channel.toString()}/${m.plain()}`);
604614
});

test/infrastructure/Listener.spec.ts

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { deepEqual } from 'assert';
1818
import { expect } from 'chai';
1919
import { Observable, of as observableOf } from 'rxjs';
2020
import { deepEqual as deepEqualParam, instance, mock, verify, when } from 'ts-mockito';
21-
import { UnresolvedAddress } from '../../src';
21+
import { MultisigAccountGraphInfo, UnresolvedAddress } from '../../src';
2222
import { Listener, ListenerChannelName } from '../../src/infrastructure/Listener';
2323
import { MultisigRepository } from '../../src/infrastructure/MultisigRepository';
2424
import { NamespaceRepository } from '../../src/infrastructure/NamespaceRepository';
@@ -43,7 +43,29 @@ describe('Listener', () => {
4343
'26b64cb10f005e5988a36744ca19e20d835ccc7c105aaa5f3b212da593180930',
4444
NetworkType.PRIVATE_TEST,
4545
);
46+
const account1 = Address.createFromPublicKey(
47+
'68B3FBB18729C1FDE225C57F8CE080FA828F0067E451A3FD81FA628842B0B763',
48+
NetworkType.PRIVATE_TEST,
49+
);
50+
const account2 = Address.createFromPublicKey(
51+
'DAB1C38C3E1642494FCCB33138B95E81867B5FB59FC4277A1D53761C8B9F6D14',
52+
NetworkType.PRIVATE_TEST,
53+
);
54+
55+
const account3 = Address.createFromPublicKey(
56+
'1674016C27FE2C2EB5DFA73996FA54A183B38AED0AA64F756A3918BAF08E061B',
57+
NetworkType.PRIVATE_TEST,
58+
);
59+
60+
const multisig1 = Address.createFromPublicKey(
61+
'B694186EE4AB0558CA4AFCFDD43B42114AE71094F5A1FC4A913FE9971CACD21D',
62+
NetworkType.PRIVATE_TEST,
63+
);
4664

65+
const multisig2 = Address.createFromPublicKey(
66+
'CF893FFCC47C33E7F68AB1DB56365C156B0736824A0C1E273F9E00B8DF8F01EB',
67+
NetworkType.PRIVATE_TEST,
68+
);
4769
let namespaceRepoMock: NamespaceRepository;
4870
let namespaceRepo: NamespaceRepository;
4971
let multisigRepoMock: MultisigRepository;
@@ -57,6 +79,14 @@ describe('Listener', () => {
5779
multisigRepo = instance(multisigRepoMock);
5880
});
5981

82+
function givenMultisig2AccountGraphInfo(): MultisigAccountGraphInfo {
83+
const map = new Map<number, MultisigAccountInfo[]>();
84+
map.set(0, [new MultisigAccountInfo(1, multisig2, 2, 1, [multisig1, account1], [])]).set(1, [
85+
new MultisigAccountInfo(1, multisig1, 1, 1, [account2, account3], [multisig2]),
86+
]);
87+
88+
return new MultisigAccountGraphInfo(map);
89+
}
6090
it('should createComplete a WebSocket instance given url parameter', () => {
6191
const listener = new Listener('http://localhost:3000/ws', namespaceRepo, epochAdjustment);
6292
expect('http://localhost:3000/ws').to.be.equal(listener.url);
@@ -215,7 +245,6 @@ describe('Listener', () => {
215245

216246
[ListenerChannelName.confirmedAdded, ListenerChannelName.partialAdded, ListenerChannelName.unconfirmedAdded].forEach((name) => {
217247
const subscribedAddress = account.address;
218-
const multisigAccount = Account.generateNewAccount(NetworkType.PRIVATE_TEST);
219248
class WebSocketMock {
220249
constructor(public readonly url: string) {}
221250
send(payload: string): void {
@@ -259,14 +288,15 @@ describe('Listener', () => {
259288
describe(`${name} transaction subscription`, () => {
260289
it('subscribe multsig', () => {
261290
const alias = new NamespaceId('test');
262-
const multisigInfo = new MultisigAccountInfo(1, subscribedAddress, 1, 1, [], [multisigAccount.address]);
263-
when(multisigRepoMock.getMultisigAccountInfo(deepEqualParam(subscribedAddress))).thenReturn(observableOf(multisigInfo));
264-
when(namespaceRepoMock.getAccountsNames(deepEqualParam([subscribedAddress]))).thenReturn(
265-
observableOf([new AccountNames(subscribedAddress, [new NamespaceName(alias, 'test')])]),
291+
when(multisigRepoMock.getMultisigAccountGraphInfo(deepEqualParam(account2))).thenReturn(
292+
observableOf(givenMultisig2AccountGraphInfo()),
293+
);
294+
when(namespaceRepoMock.getAccountsNames(deepEqualParam([account2]))).thenReturn(
295+
observableOf([new AccountNames(account2, [new NamespaceName(alias, 'test')])]),
266296
);
267297
const transferTransaction = TransferTransaction.create(
268298
Deadline.create(epochAdjustment),
269-
multisigAccount.address,
299+
multisig2,
270300
[],
271301
PlainMessage.create('test-message'),
272302
NetworkType.PRIVATE_TEST,
@@ -277,7 +307,7 @@ describe('Listener', () => {
277307

278308
const listener = new Listener('http://localhost:3000', namespaceRepo, WebSocketMultisigMock, multisigRepo);
279309
listener.open();
280-
subscriptionMethod(listener, subscribedAddress, hash, true).subscribe();
310+
subscriptionMethod(listener, account2, hash, true).subscribe();
281311

282312
listener.handleMessage(
283313
{
@@ -377,8 +407,6 @@ describe('Listener', () => {
377407
});
378408

379409
it('Using invalid no hash', () => {
380-
const subscribedAddress = account.address;
381-
382410
const alias = new NamespaceId('test');
383411
const transferTransaction = TransferTransaction.create(
384412
Deadline.create(epochAdjustment),
@@ -511,7 +539,6 @@ describe('Listener', () => {
511539

512540
[ListenerChannelName.unconfirmedRemoved, ListenerChannelName.partialRemoved].forEach((name) => {
513541
const subscribedAddress = account.address;
514-
const multisigAccount = Account.generateNewAccount(NetworkType.PRIVATE_TEST);
515542
class WebSocketMock {
516543
constructor(public readonly url: string) {}
517544
send(payload: string): void {
@@ -539,20 +566,20 @@ describe('Listener', () => {
539566

540567
describe(`${name} transaction subscription`, () => {
541568
it('subscribe multsig', () => {
542-
const multisigInfo = new MultisigAccountInfo(1, subscribedAddress, 1, 1, [], [multisigAccount.address]);
543-
when(multisigRepoMock.getMultisigAccountInfo(deepEqualParam(subscribedAddress))).thenReturn(observableOf(multisigInfo));
569+
when(multisigRepoMock.getMultisigAccountGraphInfo(deepEqualParam(account2))).thenReturn(
570+
observableOf(givenMultisig2AccountGraphInfo()),
571+
);
544572
const hash = 'abc';
545573
const message = {
546-
topic: `${name.toString()}/${subscribedAddress.plain()}`,
574+
topic: `${name.toString()}/${account2.plain()}`,
547575
data: { meta: { height: '1', hash: hash } },
548576
};
549577

550578
const listener = new Listener('http://localhost:3000', namespaceRepo, WebSocketMultisigMock, multisigRepo);
551579
listener.open();
552-
subscriptionMethod(listener, subscribedAddress, hash, true).subscribe();
580+
subscriptionMethod(listener, account2, hash, true).subscribe();
553581

554582
listener.handleMessage(message, null);
555-
556583
expect(multisigIndex).to.be.equal(2);
557584
});
558585
});

0 commit comments

Comments
 (0)