@massalabs/multisig-contract 0.0.2-dev.20260414091108

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Dusa Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # Massa Multisignature Wallet
2
+
3
+ The purpose of multisig wallets is to increase security by requiring multiple parties to agree on transactions before execution. Transactions can be executed only when confirmed by a predefined number of owners.
4
+
5
+ Features
6
+ Can hold Massa and all kinds of tokens
7
+ Integration with web3 wallets
8
+ Interacting with any contracts
9
+ @dev Most important concepts:
10
+ * Threshold/required: Number of required confirmations for a Multisig transaction.
11
+ * Owners: List of addresses that control the Multisig. They are the only ones that
12
+ can submit, approve, and execute transactions.
13
+ * UpgradeDelay: Delay necessary between an upgrade proposition and the actual upgrade
14
+ * executionDelay: Delay necessary between the time the threshold is met for a specific transaction and its execution.
15
+ * Id: Each transaction has a different ID to prevent replay attacks.
16
+ * Signature: A valid signature of an owner of the Multisig for a transaction hash.
17
+ * Owners can only be added/removed by the multisig (same for changing the threshold
18
+ and upgrading the contract)
19
+ * Change the owners, required,& upgradeDelay to your needs in src/deploy.ts
20
+
21
+ The executionDelay starts once the threshold is met, and is not reset when another wallet approves the tx. It can be reset if a wallet revokes approval and the approval count goes below the threshold.
22
+
23
+ Anyone can send coins to the multisig using the receiveCoins functions.
24
+ Only owners can call submit, approve, execute & revoke functions.
25
+ Only the multisig itself can call addOwner, removeOwner, replaceOwner, changeRequirement, changeExecutionDelay, changeUpgradeDelay, proposeUpgrade & upgrade functions. Thus, they can only be called by submitting the call through the multisig, approving & executing it.
26
+
27
+ ## Security
28
+
29
+ The code was fully audited for security by a third party professional security firm.
30
+ The report is publicly available as the [security_audit.pdf](security_audit.pdf) file at the root of the repository.
31
+
32
+ ## Build
33
+
34
+ By default this will build all files in `assembly/contracts` directory.
35
+
36
+ ```shell
37
+ npm run build
38
+ ```
39
+
40
+ ## Deploy the multisig
41
+
42
+ Prerequisites :
43
+
44
+ - You must add a `.env` file at the root of the repository with the following keys set to valid values:
45
+ - `PRIVATE_KEY="wallet_private_key"`
46
+ - `RPC_URL="https://..."` (optional, defaults to buildnet)
47
+
48
+ These keys will be the ones used by the deployment script to interact with the blockchain.
49
+
50
+ Adapt `required`, `owners` & `upgradeDelay` to your liking (cf. important concepts) in `src/deploy.ts`.
51
+
52
+ The following command will build contracts in `assembly/contracts` directory and execute the deployment script
53
+ `src/deploy.ts`. This script deploys `Multisig.wasm` directly and automatically runs its constructor.
54
+
55
+ ```shell
56
+ npm run deploy
57
+ ```
58
+
59
+ ## Proposal lifecycle (important)
60
+
61
+ Submitting a proposal does **not** count as an approval for the proposing owner. The proposer must call approve separately if they want their confirmation included toward the threshold.
62
+
63
+ The approve flow only records confirmations. When the number of approvals reaches the threshold, the proposal is **not** executed automatically. Someone with the right to execute must call execute explicitly (and respect any `executionDelay` enforced on-chain after the threshold is met).
64
+
65
+ These behaviors are intentional in the current design. Automatically approving on behalf of the submitter, or automatically executing once the threshold is reached, could be explored as future improvements but are not implemented today.
66
+
67
+ ## Create an add-member proposal
68
+
69
+ The repository also includes a helper script to submit a multisig proposal that adds a new owner.
70
+
71
+ ```shell
72
+ npm run propose:add-member -- <multisig-address> <new-member-address>
73
+ ```
74
+
75
+ The script uses the same `.env` configuration as deployment and prints the submitted operation id plus the final events emitted by the contract.
76
+
77
+ ## Approve a proposal
78
+
79
+ The repository also includes a helper script to approve an existing multisig proposal by id.
80
+
81
+ ```shell
82
+ npm run approve:proposal -- <multisig-address> <proposal-id>
83
+ ```
84
+
85
+ The script uses the same `.env` configuration as deployment and prints the approval operation id plus the final events emitted by the contract.
86
+
87
+ ## Execute a proposal
88
+
89
+ The repository also includes a helper script to execute an existing multisig proposal by id.
90
+
91
+ ```shell
92
+ npm run execute:proposal -- <multisig-address> <proposal-id>
93
+ ```
94
+
95
+ The script uses the same `.env` configuration as deployment and prints the execution operation id plus the final events emitted by the contract.
96
+
97
+ ## List proposals
98
+
99
+ The repository also includes a read-only helper script to retrieve all multisig proposals and their current statuses.
100
+
101
+ ```shell
102
+ npm run list:proposals -- <multisig-address>
103
+ ```
104
+
105
+ The script prints a JSON array with each proposal's id, target, method, value, approvals, timestamp, execution flag, and derived status.
106
+
107
+ ## Get multisig parameters
108
+
109
+ The repository also includes a read-only helper script to retrieve the multisig members, threshold, and execution delay directly from storage.
110
+
111
+ ```shell
112
+ npm run get:multisig-parameters -- <multisig-address>
113
+ ```
114
+
115
+ The script prints a JSON object containing the `members`, `threshold`, and `delay`.
116
+
117
+ ## Unit tests
118
+
119
+ The test framework documentation is available here: [as-pect docs](https://as-pect.gitbook.io/as-pect)
120
+
121
+ ```shell
122
+ npm run test
123
+ ```
124
+
125
+ ## Format code
126
+
127
+ ```shell
128
+ npm run fmt
129
+ ```
@@ -0,0 +1 @@
1
+ /// <reference types="@as-pect/assembly/types/as-pect" />
@@ -0,0 +1,447 @@
1
+ import {
2
+ submit,
3
+ approve,
4
+ constructor,
5
+ getTransactions,
6
+ } from '../contracts/Multisig';
7
+ import { Storage, mockAdminContext, Address } from '@massalabs/massa-as-sdk';
8
+ import {
9
+ Args,
10
+ u64ToBytes,
11
+ stringToBytes,
12
+ bytesToString,
13
+ serializableObjectsArrayToBytes,
14
+ bytesToSerializableObjectArray,
15
+ Serializable,
16
+ Result,
17
+ bytesToU32,
18
+ } from '@massalabs/as-types';
19
+ import {
20
+ changeCallStack,
21
+ resetStorage,
22
+ } from '@massalabs/massa-as-sdk/assembly/vm-mock/storage';
23
+ import { Transaction } from '../structs/Transaction';
24
+ import { getApprovalCount, hasApproved } from '../contracts/multisig-internals';
25
+ import { OWNERS, REQUIRED } from '../storage/Multisig';
26
+
27
+ // address of admin caller set in vm-mock. must match with adminAddress of @massalabs/massa-as-sdk/vm-mock/vm.js
28
+ const deployerAddress = 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq';
29
+
30
+ // address of the contract set in vm-mock. must match with contractAddr of @massalabs/massa-as-sdk/vm-mock/vm.js
31
+ const contractAddr = 'AS12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1eT';
32
+
33
+ // nb of confirmations required
34
+ const nbConfirmations: i32 = 2;
35
+
36
+ // the multisig owners
37
+ const owners: Array<string> = [
38
+ 'A12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq',
39
+ 'AU1qDAxGJ387ETi9JRQzZWSPKYq4YPXrFvdiE4VoXUaiAt38JFEC',
40
+ 'AU125TiSrnD2YatYfEyRAWnBdD7TEuVbvGFkFgDuaYc2bdKyqKtb',
41
+ ];
42
+
43
+ // where operation funds are sent when a transaction operation is executed
44
+ const destination = 'AU155TiSrnD2YatYfEyRAWnBdD7TEuVbvGFkFgDuaYc2bdKyqKtb';
45
+
46
+ // owners declared to the constructor for testing.
47
+ const ownerList = [owners[0], owners[1], owners[2]];
48
+
49
+ // transactions
50
+ const transactions: Array<Transaction> = [
51
+ new Transaction(new Address(destination), '', u64(15000), [], 0, false),
52
+ new Transaction(
53
+ new Address(destination),
54
+ 'getValueAt',
55
+ u64(15000),
56
+ new Args().add(42).serialize(),
57
+ 0,
58
+ false,
59
+ ),
60
+ ];
61
+
62
+ // ======================================================== //
63
+ // ==== HELPER FUNCTIONS ==== //
64
+ // ======================================================== //
65
+
66
+ function retrieveOperation(opIndex: i32): Transaction {
67
+ let operationList = bytesToSerializableObjectArray<Transaction>(
68
+ getTransactions([]),
69
+ ).unwrap();
70
+ return operationList[opIndex];
71
+ }
72
+
73
+ // string are not serializable by default, we need this helper class
74
+ class SerializableString implements Serializable {
75
+ s: string;
76
+
77
+ constructor(s: string = '') {
78
+ this.s = s;
79
+ }
80
+
81
+ public serialize(): StaticArray<u8> {
82
+ return stringToBytes(this.s);
83
+ }
84
+
85
+ public deserialize(data: StaticArray<u8>, _offset: i32): Result<i32> {
86
+ this.s = bytesToString(data);
87
+ return new Result<i32>(0);
88
+ }
89
+ }
90
+
91
+ function switchUser(user: string): void {
92
+ changeCallStack(user + ' , ' + contractAddr);
93
+ }
94
+
95
+ beforeAll(() => {
96
+ resetStorage();
97
+ mockAdminContext(true);
98
+ });
99
+
100
+ describe('Multisig contract tests', () => {
101
+ test('constructor', () => {
102
+ // ---------------------------
103
+ // check invalid constructors
104
+
105
+ // 0 confirmations
106
+ expect(() => {
107
+ const serializedArgs = new Args()
108
+ .add<Array<string>>(ownerList)
109
+ .add(i32(0))
110
+ .add(u64(0))
111
+ .add(u64(0))
112
+ .serialize();
113
+ constructor(serializedArgs);
114
+ }).toThrow();
115
+
116
+ // no owners
117
+ expect(() => {
118
+ const serializedArgs = new Args()
119
+ .add<Array<string>>([])
120
+ .add(i32(1))
121
+ .add(u64(0))
122
+ .add(u64(0))
123
+ .serialize();
124
+ constructor(serializedArgs);
125
+ }).toThrow();
126
+
127
+ // no Delay
128
+ expect(() => {
129
+ const serializedArgs = new Args()
130
+ .add<Array<string>>([])
131
+ .add(i32(1))
132
+ .add(u64(0))
133
+ .serialize();
134
+ constructor(serializedArgs);
135
+ }).toThrow();
136
+
137
+ // invalid args
138
+ expect(() => {
139
+ constructor([]);
140
+ }).toThrow();
141
+
142
+ resetStorage();
143
+
144
+ // -------------------------------------------------------
145
+ // define a valid constructor for a 2:4 multisig
146
+ const serializedArgs = new Args()
147
+ .add<Array<string>>(ownerList)
148
+ .add(nbConfirmations)
149
+ .add(u64(0))
150
+ .add(u64(0))
151
+ .serialize();
152
+ constructor(serializedArgs);
153
+
154
+ // check the nb of confirmations required is properly stored
155
+ expect(bytesToU32(Storage.get(REQUIRED))).toBe(nbConfirmations);
156
+
157
+ // compare the array of addresses as string to the array of Address in storage
158
+ let serializableStringList: Array<SerializableString> = [];
159
+ for (let i = 0; i < ownerList.length; ++i)
160
+ serializableStringList.push(new SerializableString(ownerList[i]));
161
+ let ownersFromStorage = new Args(Storage.get(OWNERS))
162
+ .nextStringArray()
163
+ .unwrap();
164
+ let serializableOwnerStringList: Array<SerializableString> = [];
165
+ for (let i = 0; i < ownersFromStorage.length; ++i)
166
+ serializableOwnerStringList.push(
167
+ new SerializableString(ownersFromStorage[i].toString()),
168
+ );
169
+ expect(
170
+ serializableObjectsArrayToBytes<SerializableString>(
171
+ serializableOwnerStringList,
172
+ ),
173
+ ).toStrictEqual(
174
+ serializableObjectsArrayToBytes<SerializableString>(
175
+ serializableStringList,
176
+ ),
177
+ );
178
+
179
+ // check that there are no operation registered yet
180
+ let operationList = bytesToSerializableObjectArray<Transaction>(
181
+ getTransactions([]),
182
+ ).unwrap();
183
+ expect(operationList.length).toBe(0);
184
+ });
185
+
186
+ test('submit operation by non owner', () => {
187
+ // expect the operation submission to fail
188
+ expect(() => {
189
+ submit(new Args().add(transactions[0]).serialize());
190
+ }).toThrow();
191
+ });
192
+
193
+ test('submit transaction operation', () => {
194
+ // pick owners[1] as the operation creator
195
+ switchUser(owners[1]);
196
+
197
+ // expect the operation index to be 1
198
+ expect(submit(new Args().add(transactions[0]).serialize())).toStrictEqual(
199
+ u64ToBytes(0),
200
+ );
201
+
202
+ let transaction = retrieveOperation(0);
203
+
204
+ // check the transaction content
205
+ expect(transaction.to).toBe(new Address(destination));
206
+ expect(transaction.value).toBe(u64(15000));
207
+ expect(transaction.executed).toBe(false);
208
+ });
209
+
210
+ // non validated operation
211
+ test('confirm transaction operation [owners[0]]', () => {
212
+ // pick owners[1] as the operation creator
213
+ switchUser(owners[1]);
214
+
215
+ let confirmingOwnersIndexes: Array<u8>;
216
+ let opIndex: u64;
217
+
218
+ confirmingOwnersIndexes = [0];
219
+ opIndex = 1;
220
+
221
+ expect(submit(new Args().add(transactions[0]).serialize())).toStrictEqual(
222
+ u64ToBytes(opIndex),
223
+ );
224
+
225
+ let ownerAddress = owners[confirmingOwnersIndexes[0]];
226
+ switchUser(ownerAddress);
227
+ approve(new Args().add(opIndex).serialize());
228
+
229
+ switchUser(deployerAddress);
230
+ expect(hasApproved(opIndex, new Address(ownerAddress)));
231
+ expect(retrieveOperation(1).timestamp).toBe(0);
232
+ });
233
+
234
+ // validated operation
235
+ test('confirm transaction operation [owners[1], owners[2]]', () => {
236
+ // pick owners[1] as the operation creator
237
+ switchUser(owners[1]);
238
+
239
+ let confirmingOwnersIndexes: Array<u8>;
240
+ let opIndex: u64;
241
+
242
+ confirmingOwnersIndexes = [1, 2];
243
+ opIndex = 2;
244
+
245
+ expect(submit(new Args().add(transactions[0]).serialize())).toStrictEqual(
246
+ u64ToBytes(opIndex),
247
+ );
248
+
249
+ for (let i = 0; i < confirmingOwnersIndexes.length; ++i) {
250
+ let ownerAddress = owners[confirmingOwnersIndexes[i]];
251
+ switchUser(ownerAddress);
252
+ approve(new Args().add(opIndex).serialize());
253
+
254
+ expect(hasApproved(opIndex, new Address(ownerAddress)));
255
+ }
256
+
257
+ switchUser(deployerAddress);
258
+ expect(getApprovalCount(opIndex)).toBe(2);
259
+ expect(retrieveOperation(2).timestamp).toBeGreaterThan(0);
260
+ });
261
+
262
+ // validated operation 2
263
+ test('confirm transaction operation [owners[1], owners[2]]', () => {
264
+ // pick owners[1] as the operation creator
265
+ switchUser(owners[1]);
266
+
267
+ let confirmingOwnersIndexes: Array<u8>;
268
+ let opIndex: u64;
269
+
270
+ confirmingOwnersIndexes = [1, 2];
271
+ opIndex = 3;
272
+
273
+ expect(submit(new Args().add(transactions[0]).serialize())).toStrictEqual(
274
+ u64ToBytes(opIndex),
275
+ );
276
+
277
+ for (let i = 0; i < confirmingOwnersIndexes.length; ++i) {
278
+ let ownerAddress = owners[confirmingOwnersIndexes[i]];
279
+ switchUser(ownerAddress);
280
+ approve(new Args().add(opIndex).serialize());
281
+
282
+ expect(hasApproved(opIndex, new Address(ownerAddress)));
283
+ }
284
+
285
+ switchUser(deployerAddress);
286
+ expect(getApprovalCount(opIndex)).toBe(2);
287
+ expect(retrieveOperation(3).timestamp).toBeGreaterThan(0);
288
+ });
289
+
290
+ // test of the call operation constructor
291
+ test('submit call operation', () => {
292
+ // pick owners[1] as the operation creator
293
+ switchUser(owners[1]);
294
+
295
+ expect(submit(new Args().add(transactions[1]).serialize())).toStrictEqual(
296
+ u64ToBytes(4),
297
+ );
298
+
299
+ // check that the operation is correctly stored
300
+ let transaction = retrieveOperation(4);
301
+
302
+ // check the operation content
303
+ expect(transaction.to).toBe(new Address(destination));
304
+ expect(transaction.value).toBe(u64(15000));
305
+ expect(transaction.method).toBe('getValueAt');
306
+ expect(transaction.data).toStrictEqual(new Args().add(42).serialize());
307
+ });
308
+
309
+ // TODO: can't test transferCoins & call operation on massa for now
310
+ // // operation 2 is validated, let's execute it
311
+ // test('execute transaction operation with success', () => {
312
+ // let destinationBalance = Coins.balanceOf(destination);
313
+ // let contractBalance = Coins.balanceOf(contractAddr);
314
+ // let initDestinationBalance = destinationBalance;
315
+ // let initContractBalance = contractBalance;
316
+
317
+ // switchUser(owners[1]);
318
+ // generateEvent(
319
+ // createEvent('BALANCES BEFORE', [
320
+ // initDestinationBalance.toString(),
321
+ // initContractBalance.toString(),
322
+ // ]),
323
+ // );
324
+
325
+ // expect(() => {
326
+ // execute(new Args().add(u64(2)).serialize());
327
+ // }).not.toThrow();
328
+
329
+ // // retrieve the operation and check that it is marked as executed
330
+ // let transaction = retrieveOperation(i32(2));
331
+ // expect(transaction.executed).toBe(true);
332
+
333
+ // destinationBalance = Coins.balanceOf(destination);
334
+ // contractBalance = Coins.balanceOf(contractAddr);
335
+ // generateEvent(
336
+ // createEvent('BALANCES AFTER', [
337
+ // destinationBalance.toString(),
338
+ // contractBalance.toString(),
339
+ // ]),
340
+ // );
341
+
342
+ // // check that the transfer has been done
343
+ // expect(destinationBalance).toBe(initDestinationBalance + 15000);
344
+ // expect(contractBalance + 15000).toBe(initContractBalance);
345
+ // });
346
+
347
+ // // operation 1 is not validated, let's try to execute it
348
+ // test('execute transaction operation with failure', () => {
349
+ // let destinationBalance = Coins.balanceOf(destination);
350
+ // let contractBalance = Coins.balanceOf(contractAddr);
351
+ // let initDestinationBalance = destinationBalance;
352
+ // let initContractBalance = contractBalance;
353
+
354
+ // switchUser(owners[1]);
355
+ // generateEvent(
356
+ // createEvent('BALANCES BEFORE', [
357
+ // initDestinationBalance.toString(),
358
+ // initContractBalance.toString(),
359
+ // ]),
360
+ // );
361
+
362
+ // expect(() => {
363
+ // execute(new Args().add(u64(1)).serialize());
364
+ // }).toThrow();
365
+
366
+ // // the operation is not supposed to be deleted
367
+ // expect(() => {
368
+ // retrieveOperation(i32(1));
369
+ // }).not.toThrow();
370
+
371
+ // destinationBalance = Coins.balanceOf(destination);
372
+ // contractBalance = Coins.balanceOf(contractAddr);
373
+ // generateEvent(
374
+ // createEvent('BALANCES AFTER', [
375
+ // destinationBalance.toString(),
376
+ // contractBalance.toString(),
377
+ // ]),
378
+ // );
379
+
380
+ // // check that the transfer has not been done
381
+ // expect(destinationBalance).toBe(initDestinationBalance);
382
+ // expect(contractBalance).toBe(initContractBalance);
383
+ // });
384
+
385
+ // // operation 3 is validated by owners[1] & owners[2].
386
+ // // now owners[1] will revoke it and we will try to execute it.
387
+ // test('revoke operation', () => {
388
+ // let operationListLenght = bytesToSerializableObjectArray<Transaction>(
389
+ // getTransactions([]),
390
+ // ).unwrap().length;
391
+
392
+ // let destinationBalance = Coins.balanceOf(destination);
393
+ // let contractBalance = Coins.balanceOf(contractAddr);
394
+ // let initDestinationBalance = destinationBalance;
395
+ // let initContractBalance = contractBalance;
396
+
397
+ // switchUser(owners[1]);
398
+ // expect(() => {
399
+ // revoke(new Args().add(u64(3)).serialize());
400
+ // }).not.toThrow();
401
+
402
+ // switchUser(deployerAddress);
403
+ // generateEvent(
404
+ // createEvent('BALANCES BEFORE', [
405
+ // initDestinationBalance.toString(),
406
+ // initContractBalance.toString(),
407
+ // ]),
408
+ // );
409
+
410
+ // expect(() => {
411
+ // execute(new Args().add(u64(3)).serialize());
412
+ // }).toThrow();
413
+
414
+ // // the operation should not have been deleted
415
+ // let operationList = bytesToSerializableObjectArray<Transaction>(
416
+ // getTransactions([]),
417
+ // ).unwrap();
418
+ // expect(operationList.length).toBe(operationListLenght);
419
+
420
+ // // retrieve the operation in its current state in Storage
421
+ // let operation = retrieveOperation(i32(3));
422
+
423
+ // expect(operation.to).toBe(new Address(destination));
424
+ // expect(operation.value).toBe(u64(15000));
425
+ // expect(operation.executed).toBe(false);
426
+
427
+ // destinationBalance = Coins.balanceOf(destination);
428
+ // contractBalance = Coins.balanceOf(contractAddr);
429
+ // generateEvent(
430
+ // createEvent('BALANCES AFTER', [
431
+ // destinationBalance.toString(),
432
+ // contractBalance.toString(),
433
+ // ]),
434
+ // );
435
+
436
+ // // check that the transfer has not been done
437
+ // expect(destinationBalance).toBe(initDestinationBalance);
438
+ // expect(contractBalance).toBe(initContractBalance);
439
+ // });
440
+
441
+ test('check operation list', () => {
442
+ let operationList = bytesToSerializableObjectArray<Transaction>(
443
+ getTransactions([]),
444
+ ).unwrap();
445
+ expect(operationList.length).toBe(5);
446
+ });
447
+ });