trac-msb 0.1.77-msb-r2-dev.3 → 0.1.77-msb-r2-dev.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/msb.mjs CHANGED
@@ -6,7 +6,7 @@ const args = isPear ? Pear.config.args : process.argv.slice(2);
6
6
  const opts = {
7
7
  stores_directory : 'stores/',
8
8
  store_name : typeof process !== "undefined" ? process.argv[2] : Pear.config.args[0],
9
- bootstrap: 'd783e5d61aab13bb353c4910dabc08cc92f143c4aa07e156214188a4cca75a2e',
9
+ bootstrap: 'd783e5d61aab13bb353c4910dabc08cc92f143c4aa07e156214188a4cca75a2e',
10
10
  channel: 'TESTNET0002tracnetworkmainsettlementbus',
11
11
  chain_id: 1,
12
12
  enable_role_requester: false,
@@ -14,7 +14,7 @@ const opts = {
14
14
  enable_wallet: true,
15
15
  enable_validator_observer: true,
16
16
  enable_interactive_mode: true,
17
- disable_rate_limit: false,
17
+ disable_rate_limit: true,
18
18
  enable_txlogs: true,
19
19
  };
20
20
 
@@ -27,6 +27,7 @@ const msb = new MainSettlementBus(args.includes('--rpc') ? rpc_opts : opts);
27
27
 
28
28
  msb.ready().then(async () => {
29
29
  const runRpc = args.includes('--rpc');
30
+
30
31
  if (runRpc) {
31
32
  console.log('Starting RPC server...');
32
33
  const portIndex = args.indexOf('--port');
@@ -39,6 +40,7 @@ msb.ready().then(async () => {
39
40
  } else {
40
41
  console.log('RPC server will not be started.');
41
42
  }
43
+
42
44
  msb.interactiveMode();
43
45
  });
44
46
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "trac-msb",
3
3
  "main": "msb.mjs",
4
- "version": "0.1.77-msb-r2-dev.3",
4
+ "version": "0.1.77-msb-r2-dev.4",
5
5
  "pear": {
6
6
  "name": "trac-msb",
7
7
  "type": "terminal"
@@ -28,7 +28,6 @@
28
28
  "bare-readline": "1.0.7",
29
29
  "bare-tty": "5.0.2",
30
30
  "bech32": "^2.0.0",
31
- "brittle": "^3.17.1",
32
31
  "compact-encoding": "^2.16.0",
33
32
  "corestore": "^7.4.2",
34
33
  "crypto": "npm:bare-node-crypto",
@@ -50,6 +49,7 @@
50
49
  "devDependencies": {
51
50
  "bare-os": "^3.6.1",
52
51
  "bip39-mnemonic": "^2.4.0",
52
+ "brittle": "^3.17.1",
53
53
  "esmock": "^2.7.2",
54
54
  "jest": "^30.1.3",
55
55
  "protocol-buffers": "^4.2.0",
@@ -88,13 +88,14 @@ export function normalizeDecodedPayloadForJson(payload) {
88
88
  }
89
89
 
90
90
  const newPayload = {};
91
+ const addressKeys = ["address", "to", "va", "ia"];
91
92
  for (const key in payload) {
92
93
  if (payload.hasOwnProperty(key)) {
93
94
  const value = payload[key];
94
95
 
95
96
  if (b4a.isBuffer(value)) {
96
97
  // 👇 intercept address buffers by key name (e.g. `address`)
97
- if (key.toLowerCase().includes("address") || key.toLowerCase().includes("to") || key.toLowerCase().includes("va")) {
98
+ if (addressKeys.some(k => key.toLowerCase().includes(k))) {
98
99
  const addr = bufferToAddress(value);
99
100
  newPayload[key] = addr ?? b4a.toString(value, "hex");
100
101
  } else if (key.toLowerCase().includes("am")) {
@@ -29,9 +29,8 @@ const setupNetwork = async () => {
29
29
  store_name: '/admin'
30
30
  }
31
31
 
32
- const peer = await setupMsbAdmin(testKeyPair1, tmpDirectory, rpcOpts)
33
- const writer = await setupMsbWriter(peer, 'writer', testKeyPair2, tmpDirectory, rpcOpts);
34
- await fundPeer(peer, writer, $TNK(100n))
32
+ const peer =await setupMsbAdmin(testKeyPair1, tmpDirectory, rpcOpts)
33
+ const writer = await setupMsbWriter(peer, 'writer', testKeyPair2, tmpDirectory, peer.options);
35
34
  return { writer, peer }
36
35
  }
37
36
 
@@ -53,13 +52,13 @@ describe("API acceptance tests", () => {
53
52
  it("GET /v1/confirmed-length", async () => {
54
53
  const res = await request(server).get("/v1/confirmed-length")
55
54
  expect(res.statusCode).toBe(200)
56
- expect(res.body).toEqual({ confirmed_length: 16 })
55
+ expect(res.body).toEqual({ confirmed_length: expect.any(Number) })
57
56
  })
58
57
 
59
58
  it("GET /v1/unconfirmed-length", async () => {
60
59
  const res = await request(server).get("/v1/unconfirmed-length")
61
60
  expect(res.statusCode).toBe(200)
62
- expect(res.body).toEqual({ unconfirmed_length: 16 })
61
+ expect(res.body).toEqual({ unconfirmed_length: expect.any(Number) })
63
62
  })
64
63
 
65
64
  it("GET /v1/txv", async () => {
@@ -97,7 +96,7 @@ describe("API acceptance tests", () => {
97
96
  it("GET /v1/balance", async () => {
98
97
  const res = await request(server).get(`/v1/balance/${wallet.address}`)
99
98
  expect(res.statusCode).toBe(200)
100
- expect(res.body).toEqual({ address: wallet.address, balance: "100000000000000000000" })
99
+ expect(res.body).toEqual({ address: wallet.address, balance: "9670000000000000000" })
101
100
  })
102
101
 
103
102
  it("POST /v1/broadcast-transaction", async () => {
@@ -124,13 +123,11 @@ describe("API acceptance tests", () => {
124
123
  })
125
124
  })
126
125
 
127
- // TODO: not sure why but test runner does not work, so this will require more attention.
128
- // We can map some of the tx hashes from previous OPs and fetch and assert payload here
129
126
  it("POST /v1/tx-payloads-bulk", async () => {
130
-
131
- const payload = { hashes: [
132
- "test"
133
- ]}
127
+ const result = await msb.state.confirmedTransactionsBetween(0, 40) // This is just an arbitrary range that will most likely contain valid
128
+ const hashes = result.map(({ hash }) => hash)
129
+
130
+ const payload = { hashes }
134
131
 
135
132
  const res = await request(server)
136
133
  .post("/v1/tx-payloads-bulk")
@@ -139,8 +136,12 @@ describe("API acceptance tests", () => {
139
136
 
140
137
  expect(res.statusCode).toBe(200)
141
138
  expect(res.body).toMatchObject({
142
- results: [],
143
- missing:["test"]
139
+ results: expect.arrayOf(
140
+ expect.objectContaining({
141
+ hash: expect.any(String),
142
+ })
143
+ ),
144
+ missing:[]
144
145
  })
145
146
  })
146
147
  })
package/test/all.test.js CHANGED
@@ -13,7 +13,7 @@ async function runTests() {
13
13
  await import('./fileUtils/readBalanceMigrationFile.test.js');
14
14
  // await import('./messageOperations/stateMessageOperations.test.js');
15
15
  await import('./buffer/buffer.test.js')
16
- //await import('./apply/apply.test.js'); Do not work. Unstable.
16
+ // await import('./apply/apply.test.js'); // This test has been disabled because Github CI fails due to lack of resources. This test can still be run locally but sometimes it hangs when destroying resources.
17
17
  test.resume();
18
18
  }
19
19
 
@@ -1,12 +1,11 @@
1
- import {test, hook} from 'brittle';
1
+ import {test, hook} from '../../utils/wrapper.js';
2
2
  import {
3
3
  initMsbAdmin, initTemporaryDirectory, removeTemporaryDirectory, setupMsbPeer, setupMsbWriter, setupMsbIndexer,
4
- tryToSyncWriters, waitForAdminEntry
4
+ tryToSyncWriters
5
5
  } from '../../utils/setupApplyTests.js';
6
-
7
6
  import {randomBytes} from '../../utils/setupApplyTests.js';
8
7
  import CompleteStateMessageOperations from '../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js';
9
- import {testKeyPair1, testKeyPair2, testKeyPair3, testKeyPair4} from '../../fixtures/apply.fixtures.js';
8
+ import {testKeyPair1} from '../../fixtures/apply.fixtures.js';
10
9
  import b4a from 'b4a';
11
10
  import { ADMIN_INITIAL_BALANCE } from '../../../src/utils/constants.js';
12
11
 
@@ -16,6 +15,18 @@ let admin;
16
15
  let tmpDirectory;
17
16
  let randomChannel;
18
17
 
18
+ const sendAddAdmin = async (invoker) => {
19
+ const validity = b4a.from(await admin.msb.state.getIndexerSequenceState(), 'hex')
20
+ const addAdminMessage = await CompleteStateMessageOperations.assembleAddAdminMessage(
21
+ admin.wallet,
22
+ admin.msb.state.writingKey,
23
+ validity
24
+ );
25
+
26
+ // add admin to base
27
+ await invoker.msb.state.append(addAdminMessage); // Send `add admin` request to apply function
28
+ }
29
+
19
30
  hook('Initialize admin for addAdmin tests', async () => {
20
31
  randomChannel = randomBytes(32).toString('hex');
21
32
  const baseOptions = {
@@ -31,35 +42,24 @@ hook('Initialize admin for addAdmin tests', async () => {
31
42
  });
32
43
 
33
44
  test('Apply function addAdmin for the first time - happy path', async (k) => {
34
- try {
35
- const writersLength = await admin.msb.state.getWriterLength();
36
- const adminEntryBefore = await admin.msb.state.getAdminEntry();
37
- k.is(adminEntryBefore, null, 'Admin entry should be null before adding a new admin');
45
+ const writersLength = await admin.msb.state.getWriterLength();
46
+ const adminEntryBefore = await admin.msb.state.getAdminEntry();
47
+ k.is(adminEntryBefore, null, 'Admin entry should be null before adding a new admin');
38
48
 
39
- const addAdminMessage = await CompleteStateMessageOperations.assembleAddAdminMessage(
40
- admin.wallet,
41
- admin.msb.state.writingKey
42
- );
43
-
44
- // add admin to base
45
- await admin.msb.state.append(addAdminMessage); // Send `add admin` request to apply function
46
- await tryToSyncWriters(admin);
47
- const adminEntryAfter = await admin.msb.state.getAdminEntry(); // check if the admin entry was added successfully in the base
48
- const nodeAdminEntry = await admin.msb.state.getNodeEntry(adminEntryAfter.address)
49
- const newWritersLength = await admin.msb.state.getWriterLength();
50
- // check the result
51
- k.ok(adminEntryAfter, 'Result should not be null');
52
- k.ok(adminEntryAfter.address === admin.wallet.address, 'Admin address in base should match admin wallet address');
53
- k.ok(b4a.equals(adminEntryAfter.wk, admin.msb.state.writingKey), 'Admin writing key in base should match admin MSB writing key');
54
- k.ok(b4a.equals(adminEntryAfter.wk, admin.options.bootstrap), 'Admin writing key in base should match bootstrap key');
55
- k.is(nodeAdminEntry.isWriter, true, 'Admin should be writer');
56
- k.is(nodeAdminEntry.isIndexer, true, 'Admin should be indexer');
57
- k.ok(b4a.equals(nodeAdminEntry.balance, ADMIN_INITIAL_BALANCE), 'Admin should have an initial balance');
58
- k.is(newWritersLength, writersLength + 1, 'Admin should increase writers length');
59
-
60
- } catch (error) {
61
- k.fail(error.message);
62
- }
49
+ await sendAddAdmin(admin)
50
+ await tryToSyncWriters(admin);
51
+ const adminEntryAfter = await admin.msb.state.getAdminEntry(); // check if the admin entry was added successfully in the base
52
+ const nodeAdminEntry = await admin.msb.state.getNodeEntry(adminEntryAfter.address)
53
+ const newWritersLength = await admin.msb.state.getWriterLength();
54
+ // check the result
55
+ k.ok(adminEntryAfter, 'Result should not be null');
56
+ k.ok(adminEntryAfter.address === admin.wallet.address, 'Admin address in base should match admin wallet address');
57
+ k.ok(b4a.equals(adminEntryAfter.wk, admin.msb.state.writingKey), 'Admin writing key in base should match admin MSB writing key');
58
+ k.ok(b4a.equals(adminEntryAfter.wk, admin.options.bootstrap), 'Admin writing key in base should match bootstrap key');
59
+ k.is(nodeAdminEntry.isWriter, true, 'Admin should be writer');
60
+ k.is(nodeAdminEntry.isIndexer, true, 'Admin should be indexer');
61
+ k.ok(b4a.equals(nodeAdminEntry.balance, ADMIN_INITIAL_BALANCE), 'Admin should have an initial balance');
62
+ k.is(newWritersLength, writersLength + 1, 'Admin should increase writers length');
63
63
  });
64
64
 
65
65
  hook('Clean up addAdmin recovery setup', async () => {
@@ -1,13 +1,16 @@
1
- import {test, hook} from 'brittle';
1
+ import {test, hook} from '../../utils/wrapper.js';
2
2
  import {
3
- initMsbAdmin, initTemporaryDirectory, removeTemporaryDirectory, setupMsbPeer, setupMsbWriter, setupMsbIndexer,
3
+ initTemporaryDirectory, removeTemporaryDirectory, setupMsbPeer, setupMsbWriter, setupMsbIndexer,
4
4
  tryToSyncWriters, waitForAdminEntry, setupMsbAdmin
5
5
  } from '../../utils/setupApplyTests.js';
6
6
 
7
7
  import {randomBytes} from '../../utils/setupApplyTests.js';
8
8
  import CompleteStateMessageOperations from '../../../src/messages/completeStateMessages/CompleteStateMessageOperations.js';
9
+ import PartialStateMessageOperations from '../../../src/messages/partialStateMessages/PartialStateMessageOperations.js'
9
10
  import {testKeyPair1, testKeyPair2, testKeyPair3, testKeyPair4} from '../../fixtures/apply.fixtures.js';
10
11
  import b4a from 'b4a';
12
+ import { decode as decodeAdmin } from '../../../src/core/state/utils/adminEntry.js';
13
+ import { EntryType } from '../../../src/utils/constants.js';
11
14
  //TODO: ADD TEST WHEN NON-ADMIN NODE FORGES ADD ADMIN OPERATION AND BROADCASTS IT TO THE STATE - SHOULD BE REJECTED
12
15
 
13
16
  let admin, newAdmin;
@@ -28,83 +31,88 @@ hook('Initialize admin for addAdmin tests', async () => {
28
31
  admin = await setupMsbAdmin(testKeyPair1, tmpDirectory, baseOptions);
29
32
 
30
33
  // Setup nodes
31
- writer = await setupMsbWriter(admin, 'writer', testKeyPair2, tmpDirectory, baseOptions);
32
- indexer1 = await setupMsbWriter(admin, 'indexer1', testKeyPair3, tmpDirectory, baseOptions);
33
- indexer2 = await setupMsbWriter(admin, 'indexer2', testKeyPair4, tmpDirectory, baseOptions);
34
+ writer = await setupMsbWriter(admin, 'writer', testKeyPair2, tmpDirectory, admin.options);
35
+ indexer1 = await setupMsbWriter(admin, 'indexer1', testKeyPair3, tmpDirectory, admin.options);
36
+ indexer2 = await setupMsbWriter(admin, 'indexer2', testKeyPair4, tmpDirectory, admin.options);
34
37
 
35
38
  // Setup indexers after network is stable
36
39
  indexer1 = await setupMsbIndexer(indexer1, admin);
37
40
  indexer2 = await setupMsbIndexer(indexer2, admin);
38
41
  });
39
42
 
40
-
41
-
42
43
  test('Apply function addAdmin for recovery - happy path', async (k) => {
43
- try {
44
- await tryToSyncWriters(admin, writer, indexer1, indexer2);
45
-
46
- const adminEntryBefore = await admin.msb.state.getAdminEntry();
47
- const admI1 = await indexer1.msb.state.getAdminEntry();
48
- const admI2 = await indexer2.msb.state.getAdminEntry();
49
- const admW = await writer.msb.state.getAdminEntry();
50
-
51
- await waitForAdminEntry(admin, {
52
- address: admin.wallet.address,
53
- wk: admin.msb.state.writingKey
54
- })
55
-
56
- await waitForAdminEntry(indexer1, {
57
- address: admin.wallet.address,
58
- wk: admin.msb.state.writingKey
59
- })
60
-
61
- await waitForAdminEntry(indexer2, {
62
- address: admin.wallet.address,
63
- wk: admin.msb.state.writingKey
64
- })
65
-
66
- await waitForAdminEntry(writer, {
67
- address: admin.wallet.address,
68
- wk: admin.msb.state.writingKey
69
- })
70
-
71
- k.ok(adminEntryBefore !== null, 'Admin entry should not be null before recovery');
72
- k.ok(b4a.equals(adminEntryBefore.wk, admin.options.bootstrap), 'Admin writing key in base should match bootstrap key');
73
- k.ok(b4a.equals(adminEntryBefore.wk, admI1.wk), 'Admin entry writer key the same as indexer');
74
- k.ok(b4a.equals(admI1.wk, admI2.wk), 'Admin entry should be the same for both indexers');
75
- k.ok(b4a.equals(admI1.wk, admW.wk), 'Admin entry should be the same for writer');
76
- k.ok(adminEntryBefore.address === admI1.address, 'Admin entry address the same as indexer');
77
- k.ok(admI1.address === admI2.address, 'Admin address should be the same for both indexers');
78
- k.ok(admI1.address === admW.address, 'Admin address should be the same for writer');
79
- const adminAddressBeforeRecovery = admin.wallet.address
80
- await admin.msb.close(); // close the admin instance to simulate recovery
81
-
82
- // Simulate recovery by creating a new admin instance
83
- newAdmin = await setupMsbPeer('newAdmin', testKeyPair1, tmpDirectory, admin.options);
84
- await newAdmin.msb.ready();
85
- const addAdminMessage = await CompleteStateMessageOperations.assembleAddAdminMessage(
86
- newAdmin.wallet,
87
- newAdmin.msb.state.writingKey
88
- );
89
- await writer.msb.state.append(addAdminMessage); // Send `add admin` request to apply function
90
- await tryToSyncWriters(newAdmin, writer, indexer1, indexer2);
91
- await waitForAdminEntry(newAdmin, {
92
- address: newAdmin.wallet.address,
93
- wk: newAdmin.msb.state.writingKey
94
- })
95
-
96
- const adminEntryAfter = await newAdmin.msb.state.getAdminEntry(); // check if the admin entry was added successfully in the base
97
- k.ok(adminEntryAfter, 'Result should not be null');
98
- k.ok(adminEntryAfter.address === newAdmin.wallet.address, 'New Admin address in base should match new admin wallet address');
99
- k.ok(adminAddressBeforeRecovery === newAdmin.wallet.address, 'New Admin wallet address should be the same as old admin wallet address');
100
- k.ok(b4a.equals(adminEntryAfter.wk, newAdmin.msb.state.writingKey), 'New Admin writing key in base should match new admin MSB writing key');
101
- k.ok(!b4a.equals(adminEntryBefore.wk, adminEntryAfter.wk), 'New Admin writing key in base should have changed');
102
- k.ok(!b4a.equals(adminEntryAfter.wk, newAdmin.options.bootstrap), 'New Admin should not be bootstrap anymore');
103
- //k.ok(newAdmin.msb.state.isWritable(), 'New Admin should be a writer');
104
- // k.ok(newAdmin.msb.state.isIndexer(), 'New Admin should be an indexer'); // wait until holepunch team will fix bug with rotation.
105
- } catch (error) {
106
- k.fail(error.message);
107
- }
44
+ const formerBootstrap = admin.options.bootstrap
45
+ await tryToSyncWriters(admin, writer, indexer1, indexer2);
46
+
47
+ const adminEntryBefore = await admin.msb.state.getAdminEntry();
48
+ const admI1 = await indexer1.msb.state.getAdminEntry();
49
+ const admI2 = await indexer2.msb.state.getAdminEntry();
50
+ const admW = await writer.msb.state.getAdminEntry();
51
+
52
+ await waitForAdminEntry(admin, {
53
+ address: admin.wallet.address,
54
+ wk: admin.msb.state.writingKey
55
+ })
56
+
57
+ await waitForAdminEntry(indexer1, {
58
+ address: admin.wallet.address,
59
+ wk: admin.msb.state.writingKey
60
+ })
61
+
62
+ await waitForAdminEntry(indexer2, {
63
+ address: admin.wallet.address,
64
+ wk: admin.msb.state.writingKey
65
+ })
66
+
67
+ await waitForAdminEntry(writer, {
68
+ address: admin.wallet.address,
69
+ wk: admin.msb.state.writingKey
70
+ })
71
+
72
+ // waitForIndexersConnection
73
+
74
+ k.ok(adminEntryBefore !== null, 'Admin entry should not be null before recovery');
75
+ k.ok(b4a.equals(adminEntryBefore.wk, admin.options.bootstrap), 'Admin writing key in base should match bootstrap key');
76
+ k.ok(b4a.equals(adminEntryBefore.wk, admI1.wk), 'Admin entry writer key the same as indexer');
77
+ k.ok(b4a.equals(admI1.wk, admI2.wk), 'Admin entry should be the same for both indexers');
78
+ k.ok(b4a.equals(admI1.wk, admW.wk), 'Admin entry should be the same for writer');
79
+ k.ok(adminEntryBefore.address === admI1.address, 'Admin entry address the same as indexer');
80
+ k.ok(admI1.address === admI2.address, 'Admin address should be the same for both indexers');
81
+ k.ok(admI1.address === admW.address, 'Admin address should be the same for writer');
82
+ const adminAddressBeforeRecovery = admin.wallet.address
83
+
84
+ // Simulate recovery by creating a new admin instance
85
+ newAdmin = await setupMsbPeer('newAdmin', testKeyPair1, tmpDirectory, admin.options);
86
+ await admin.msb.close(); // close the admin instance to simulate recovery
87
+ await newAdmin.msb.ready();
88
+ await newAdmin.msb.state.append(null);
89
+ const validity = b4a.toString(await newAdmin.msb.state.getIndexerSequenceState(), 'hex')
90
+ const addAdminMessage = await PartialStateMessageOperations.assembleAdminRecoveryMessage(
91
+ newAdmin.wallet,
92
+ b4a.toString(newAdmin.msb.state.writingKey, 'hex'),
93
+ validity
94
+ );
95
+
96
+ const rawTx = await CompleteStateMessageOperations.assembleAdminRecoveryMessage(
97
+ writer.wallet,
98
+ addAdminMessage.address,
99
+ b4a.from(addAdminMessage.rao.tx, 'hex'),
100
+ b4a.from(addAdminMessage.rao.txv, 'hex'),
101
+ b4a.from(addAdminMessage.rao.iw, 'hex'),
102
+ b4a.from(addAdminMessage.rao.in, 'hex'),
103
+ b4a.from(addAdminMessage.rao.is, 'hex')
104
+ )
105
+ await writer.msb.state.append(rawTx)
106
+ await tryToSyncWriters(writer, indexer1, indexer2, newAdmin);
107
+ const adminEntryAfter = decodeAdmin(await writer.msb.state.get(EntryType.ADMIN)); // check if the admin entry was added successfully in the base
108
+ k.ok(adminEntryAfter, 'Result should not be null');
109
+ k.ok(adminEntryAfter.address === newAdmin.wallet.address, 'New Admin address in base should match new admin wallet address');
110
+ k.ok(adminAddressBeforeRecovery === newAdmin.wallet.address, 'New Admin wallet address should be the same as old admin wallet address');
111
+ k.ok(b4a.equals(adminEntryAfter.wk, newAdmin.msb.state.writingKey), 'New Admin writing key in base should match new admin MSB writing key');
112
+ k.ok(!b4a.equals(adminEntryBefore.wk, adminEntryAfter.wk), 'New Admin writing key in base should have changed');
113
+ k.ok(!b4a.equals(adminEntryAfter.wk, formerBootstrap), 'New Admin should not be bootstrap anymore');
114
+ //k.ok(newAdmin.msb.state.isWritable(), 'New Admin should be a writer');
115
+ // k.ok(newAdmin.msb.state.isIndexer(), 'New Admin should be an indexer'); // wait until holepunch team will fix bug with rotation.
108
116
  });
109
117
 
110
118
  hook('Clean up addAdmin recovery setup', async () => {