@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 +21 -0
- package/README.md +129 -0
- package/assembly/__tests__/as-pect.d.ts +1 -0
- package/assembly/__tests__/basic-tests.spec.ts +447 -0
- package/assembly/contracts/Multisig.ts +373 -0
- package/assembly/contracts/multisig-internals.ts +106 -0
- package/assembly/interfaces/IMultisig.ts +100 -0
- package/assembly/libraries/PersistentMap.ts +307 -0
- package/assembly/libraries/SafeMath.ts +73 -0
- package/assembly/libraries/Upgradeable.ts +81 -0
- package/assembly/storage/Multisig.ts +11 -0
- package/assembly/structs/Transaction.ts +37 -0
- package/assembly/tsconfig.json +14 -0
- package/index.ts +1 -0
- package/package.json +57 -0
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
|
+
});
|