suidouble 2.5.0 → 2.16.0
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/.claude/settings.local.json +7 -0
- package/README.md +222 -131
- package/index.js +0 -2
- package/lib/SuiCliCommands.js +18 -25
- package/lib/SuiCoin.js +79 -137
- package/lib/SuiCoins.js +41 -29
- package/lib/SuiCommonMethods.js +40 -3
- package/lib/SuiEvent.js +54 -6
- package/lib/SuiInBrowser.js +143 -15
- package/lib/SuiInBrowserAdapter.js +164 -37
- package/lib/SuiLocalTestValidator.js +76 -14
- package/lib/SuiMaster.js +335 -139
- package/lib/SuiMemoryObjectStorage.js +66 -73
- package/lib/SuiObject.js +128 -153
- package/lib/SuiPackage.js +292 -187
- package/lib/SuiPackageModule.js +176 -221
- package/lib/SuiPaginatedResponse.js +288 -25
- package/lib/SuiPseudoRandomAddress.js +29 -2
- package/lib/SuiTransaction.js +115 -70
- package/lib/SuiUtils.js +179 -127
- package/package.json +29 -13
- package/test/build_modules.test.js +41 -0
- package/test/coins.test.js +17 -16
- package/test/custom_transaction.test.js +167 -0
- package/test/event_listeners.test.js +171 -0
- package/test/failed_transaction.test.js +184 -0
- package/test/name_service.test.js +28 -0
- package/test/owned_objects.test.js +148 -0
- package/test/rpc.test.js +3 -6
- package/test/sui_in_browser.test.js +2 -2
- package/test/sui_master_basic.test.js +4 -5
- package/test/sui_master_onlocal.test.js +84 -22
- package/test/sui_object_properties.test.js +85 -0
- package/tsconfig.json +15 -0
- package/types/index.d.ts +15 -0
- package/types/lib/SuiCliCommands.d.ts +6 -0
- package/types/lib/SuiCoin.d.ts +183 -0
- package/types/lib/SuiCoins.d.ts +93 -0
- package/types/lib/SuiCommonMethods.d.ts +37 -0
- package/types/lib/SuiEvent.d.ts +95 -0
- package/types/lib/SuiInBrowser.d.ts +189 -0
- package/types/lib/SuiInBrowserAdapter.d.ts +167 -0
- package/types/lib/SuiLocalTestValidator.d.ts +92 -0
- package/types/lib/SuiMaster.d.ts +333 -0
- package/types/lib/SuiMemoryObjectStorage.d.ts +96 -0
- package/types/lib/SuiObject.d.ts +135 -0
- package/types/lib/SuiPackage.d.ts +233 -0
- package/types/lib/SuiPackageModule.d.ts +139 -0
- package/types/lib/SuiPaginatedResponse.d.ts +148 -0
- package/types/lib/SuiPseudoRandomAddress.d.ts +33 -0
- package/types/lib/SuiTransaction.d.ts +92 -0
- package/types/lib/SuiUtils.d.ts +152 -0
- package/types/lib/data/icons.d.ts +12 -0
- package/lib/SuiTestScenario.js +0 -169
- package/test/sui_test_scenario.test.js +0 -61
package/package.json
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "suidouble",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.16.0",
|
|
4
|
+
"description": "JavaScript library for Sui Move smart contracts. Publish, upgrade, and test packages; call contract methods; query objects and events — same code works on Node.js and in the browser.",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"types": "types/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./types/index.d.ts",
|
|
10
|
+
"default": "./index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
6
13
|
"type": "module",
|
|
7
14
|
"scripts": {
|
|
8
15
|
"test": "tap -j1 -t120 ./test/*.test.js",
|
|
9
|
-
"
|
|
16
|
+
"typecheck": "npx tsc --noEmit --checkJs",
|
|
17
|
+
"build:types": "npx tsc",
|
|
18
|
+
"prepublishOnly": "npx tsc"
|
|
10
19
|
},
|
|
11
20
|
"keywords": [
|
|
12
21
|
"sui",
|
|
@@ -14,28 +23,35 @@
|
|
|
14
23
|
"move",
|
|
15
24
|
"smart contract",
|
|
16
25
|
"smart contracts",
|
|
17
|
-
"
|
|
26
|
+
"blockchain",
|
|
18
27
|
"web3",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
28
|
+
"dapp",
|
|
29
|
+
"grpc",
|
|
30
|
+
"graphql"
|
|
21
31
|
],
|
|
22
32
|
"author": "suidouble (https://github.com/suidouble)",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/suidouble/suidouble.git"
|
|
36
|
+
},
|
|
23
37
|
"license": "Apache-2.0",
|
|
24
38
|
"dependencies": {
|
|
25
|
-
"@mysten/bcs": "^2.0.
|
|
26
|
-
"@mysten/sui": "^2.
|
|
39
|
+
"@mysten/bcs": "^2.0.3",
|
|
40
|
+
"@mysten/sui": "^2.16.0",
|
|
27
41
|
"@polymedia/coinmeta": "^0.0.24",
|
|
28
42
|
"@scure/bip39": "^1.6.0",
|
|
29
|
-
"@wallet-standard/core": "^1.1.1"
|
|
30
|
-
"websocket": "^1.0.35"
|
|
43
|
+
"@wallet-standard/core": "^1.1.1"
|
|
31
44
|
},
|
|
32
45
|
"devDependencies": {
|
|
33
|
-
"tap": "^21.5.0"
|
|
46
|
+
"tap": "^21.5.0",
|
|
47
|
+
"typescript": "^5.9.3"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18"
|
|
34
51
|
},
|
|
35
52
|
"browser": {
|
|
36
53
|
"child_process": false,
|
|
37
54
|
"fs": false,
|
|
38
55
|
"path": false
|
|
39
|
-
}
|
|
40
|
-
"tap": {}
|
|
56
|
+
}
|
|
41
57
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
import t from 'tap';
|
|
4
|
+
import { SuiMaster, SuiLocalTestValidator } from '../index.js';
|
|
5
|
+
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
const { test } = t;
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
const PACKAGE_PATH = path.join(__dirname, './test_move_contracts/suidouble_chat/');
|
|
14
|
+
|
|
15
|
+
let suiLocalTestValidator = null;
|
|
16
|
+
let suiMaster = null;
|
|
17
|
+
let contract = null;
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
test('getModulesNamesFromBuild — reads module names from compiled artifacts', async t => {
|
|
21
|
+
suiMaster = new SuiMaster({ client: 'local', as: 'somebody' });
|
|
22
|
+
contract = suiMaster.addPackage({ path: PACKAGE_PATH });
|
|
23
|
+
await contract.build();
|
|
24
|
+
|
|
25
|
+
const names = await contract.getModulesNamesFromBuild();
|
|
26
|
+
|
|
27
|
+
// returns an array
|
|
28
|
+
t.ok(Array.isArray(names), 'returns an array');
|
|
29
|
+
|
|
30
|
+
// at least one module
|
|
31
|
+
t.ok(names.length > 0, 'at least one module found');
|
|
32
|
+
|
|
33
|
+
// expected module is present
|
|
34
|
+
t.ok(names.includes('suidouble_chat'), 'includes suidouble_chat');
|
|
35
|
+
|
|
36
|
+
// every entry is a non-empty string with no .mv suffix
|
|
37
|
+
for (const name of names) {
|
|
38
|
+
t.ok(typeof name === 'string' && name.length > 0, `module name "${name}" is a non-empty string`);
|
|
39
|
+
t.notOk(name.endsWith('.mv'), `module name "${name}" has no .mv suffix`);
|
|
40
|
+
}
|
|
41
|
+
});
|
package/test/coins.test.js
CHANGED
|
@@ -26,6 +26,7 @@ test('init suiMaster and connect it to local test validator', async t => {
|
|
|
26
26
|
t.ok(`${suiMaster.address}`.indexOf('0x') === 0); // adress is string starting with '0x'
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
+
|
|
29
30
|
test('type is normalized for SUI', async t => {
|
|
30
31
|
// eveything should be the same:
|
|
31
32
|
const suiCoin1 = suiMaster.suiCoins.get('sui');
|
|
@@ -207,27 +208,27 @@ test('have some in balance query', async t => {
|
|
|
207
208
|
});
|
|
208
209
|
|
|
209
210
|
|
|
210
|
-
test('getting coin objects for a transaction', async t => {
|
|
211
|
-
|
|
211
|
+
// test('getting coin objects for a transaction', async t => {
|
|
212
|
+
// const suiCoin = suiMaster.suiCoins.get('sui');
|
|
212
213
|
|
|
213
|
-
|
|
214
|
+
// const wasBalance = await suiCoin.getBalance(suiMaster.address);
|
|
214
215
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
// const tx = new Transaction();
|
|
217
|
+
// const coinInput = await suiCoin.coinOfAmountToTxCoin(tx, suiMaster.address, suiMaster.MIST_PER_SUI); // pick 1 SUI
|
|
218
|
+
// tx.transferObjects([coinInput], '0x1d20dcdb2bca4f508ea9613994683eb4e76e9c4ed371169677c1be02aaf0b12a'); // send it anywhere
|
|
218
219
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
220
|
+
// const result = await suiMaster.signAndExecuteTransaction({
|
|
221
|
+
// transaction: tx,
|
|
222
|
+
// requestType: 'WaitForLocalExecution',
|
|
223
|
+
// options: {
|
|
224
|
+
// },
|
|
225
|
+
// });
|
|
225
226
|
|
|
226
|
-
|
|
227
|
+
// const nowBalance = await suiCoin.getBalance(suiMaster.address);
|
|
227
228
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
});
|
|
229
|
+
// t.ok(nowBalance < wasBalance); /// would be better to calculate everthing + fees + storage rebate, but let's just assume it works for now.
|
|
230
|
+
// // @todo : cover better
|
|
231
|
+
// });
|
|
231
232
|
|
|
232
233
|
|
|
233
234
|
test('stops local test node', async t => {
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
import t from 'tap';
|
|
4
|
+
import { SuiMaster, SuiLocalTestValidator, Transaction, txInput } from '../index.js';
|
|
5
|
+
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
const { test } = t;
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
let suiLocalTestValidator = null;
|
|
14
|
+
let suiMaster = null;
|
|
15
|
+
let contract = null;
|
|
16
|
+
let chatShopId = null;
|
|
17
|
+
|
|
18
|
+
test('spawn local test node', async t => {
|
|
19
|
+
suiLocalTestValidator = await SuiLocalTestValidator.launch({ testFallbackEnabled: true });
|
|
20
|
+
t.ok(suiLocalTestValidator.active);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('setup — init, fund, publish', async t => {
|
|
24
|
+
suiMaster = new SuiMaster({ client: suiLocalTestValidator, as: 'somebody', debug: false });
|
|
25
|
+
await suiMaster.initialize();
|
|
26
|
+
|
|
27
|
+
await suiMaster.requestSuiFromFaucet();
|
|
28
|
+
|
|
29
|
+
contract = suiMaster.addPackage({
|
|
30
|
+
path: path.join(__dirname, './test_move_contracts/suidouble_chat/'),
|
|
31
|
+
});
|
|
32
|
+
await contract.build();
|
|
33
|
+
await contract.publish();
|
|
34
|
+
t.ok(contract.address, 'contract published');
|
|
35
|
+
|
|
36
|
+
chatShopId = suiMaster.objectStorage.findMostRecentByTypeName('ChatShop')?.id;
|
|
37
|
+
t.ok(chatShopId, 'ChatShop found after publish');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('executeTransaction — custom Transaction built with suiMaster.Transaction', async t => {
|
|
41
|
+
// build the tx using the Transaction class exposed via suiMaster
|
|
42
|
+
const tx = new suiMaster.Transaction();
|
|
43
|
+
|
|
44
|
+
tx.moveCall({
|
|
45
|
+
target: `${contract.address}::suidouble_chat::post`,
|
|
46
|
+
arguments: [
|
|
47
|
+
tx.object(chatShopId),
|
|
48
|
+
txInput(tx, 'string', 'custom tx message'),
|
|
49
|
+
txInput(tx, 'string', 'custom tx metadata'),
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// execute via module.executeTransaction so objectStorage + events are synced
|
|
54
|
+
const result = await contract.modules.suidouble_chat.executeTransaction(tx);
|
|
55
|
+
|
|
56
|
+
t.ok(result.created.length > 0, 'transaction created at least one object');
|
|
57
|
+
|
|
58
|
+
let foundTopMessage = false;
|
|
59
|
+
let foundResponse = false;
|
|
60
|
+
for (const obj of result.created) {
|
|
61
|
+
if (obj.typeName === 'ChatTopMessage') foundTopMessage = true;
|
|
62
|
+
if (obj.typeName === 'ChatResponse') foundResponse = true;
|
|
63
|
+
}
|
|
64
|
+
t.ok(foundTopMessage, 'ChatTopMessage created');
|
|
65
|
+
t.ok(foundResponse, 'ChatResponse created');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('executeTransaction — custom Transaction built with imported Transaction class', async t => {
|
|
69
|
+
// same test using the directly imported Transaction from suidouble
|
|
70
|
+
const tx = new Transaction();
|
|
71
|
+
|
|
72
|
+
tx.moveCall({
|
|
73
|
+
target: `${contract.address}::suidouble_chat::post`,
|
|
74
|
+
arguments: [
|
|
75
|
+
tx.object(chatShopId),
|
|
76
|
+
txInput(tx, 'string', 'imported Transaction message'),
|
|
77
|
+
txInput(tx, 'string', 'meta'),
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const result = await contract.modules.suidouble_chat.executeTransaction(tx);
|
|
82
|
+
|
|
83
|
+
t.ok(result.created.length > 0, 'transaction created objects');
|
|
84
|
+
t.ok(result.isSuccessful(), 'transaction is marked successful');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('executeTransaction — objectStorage is updated after custom tx', async t => {
|
|
88
|
+
const before = suiMaster.objectStorage.asArray().length;
|
|
89
|
+
|
|
90
|
+
const tx = new Transaction();
|
|
91
|
+
tx.moveCall({
|
|
92
|
+
target: `${contract.address}::suidouble_chat::post`,
|
|
93
|
+
arguments: [
|
|
94
|
+
tx.object(chatShopId),
|
|
95
|
+
txInput(tx, 'string', 'storage sync test'),
|
|
96
|
+
txInput(tx, 'string', 'meta'),
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await contract.modules.suidouble_chat.executeTransaction(tx);
|
|
101
|
+
|
|
102
|
+
const after = suiMaster.objectStorage.asArray().length;
|
|
103
|
+
t.ok(after > before, `objectStorage grew from ${before} to ${after} after custom tx`);
|
|
104
|
+
|
|
105
|
+
// created objects should be findable in objectStorage by type
|
|
106
|
+
const topMessage = suiMaster.objectStorage.findMostRecentByTypeName('ChatTopMessage');
|
|
107
|
+
t.ok(topMessage, 'ChatTopMessage is findable in objectStorage after custom tx');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('suiMaster.signAndExecuteTransaction — low-level path, no objectStorage or event sync', async t => {
|
|
111
|
+
// module.executeTransaction(tx) is the recommended high-level path:
|
|
112
|
+
// - signs and executes the transaction
|
|
113
|
+
// - pushes created/mutated objects into suiMaster.objectStorage
|
|
114
|
+
// - emits 'added' / 'created' / 'deleted' events on the module
|
|
115
|
+
//
|
|
116
|
+
// suiMaster.signAndExecuteTransaction(options) is the raw path underneath it:
|
|
117
|
+
// - only signs and submits — returns a SuiTransaction
|
|
118
|
+
// - does NOT update objectStorage
|
|
119
|
+
// - does NOT emit module events
|
|
120
|
+
// - use this when you want full control (e.g. multi-step PTBs, custom include flags)
|
|
121
|
+
|
|
122
|
+
const storeBefore = suiMaster.objectStorage.asArray().length;
|
|
123
|
+
|
|
124
|
+
const addedViaRaw = [];
|
|
125
|
+
const onAdded = (ev) => addedViaRaw.push(ev.detail);
|
|
126
|
+
contract.modules.suidouble_chat.addEventListener('added', onAdded);
|
|
127
|
+
|
|
128
|
+
const tx = new Transaction();
|
|
129
|
+
tx.moveCall({
|
|
130
|
+
target: `${contract.address}::suidouble_chat::post`,
|
|
131
|
+
arguments: [
|
|
132
|
+
tx.object(chatShopId),
|
|
133
|
+
txInput(tx, 'string', 'raw signAndExecute message'),
|
|
134
|
+
txInput(tx, 'string', 'meta'),
|
|
135
|
+
],
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const suiTransaction = await suiMaster.signAndExecuteTransaction({
|
|
139
|
+
transaction: tx,
|
|
140
|
+
include: {
|
|
141
|
+
effects: true,
|
|
142
|
+
objectTypes: true,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
contract.modules.suidouble_chat.removeEventListener('added', onAdded);
|
|
147
|
+
|
|
148
|
+
// the transaction itself succeeded and has a digest
|
|
149
|
+
t.ok(suiTransaction, 'signAndExecuteTransaction returned a SuiTransaction');
|
|
150
|
+
t.ok(typeof suiTransaction.digest === 'string' && suiTransaction.digest.length > 0,
|
|
151
|
+
'SuiTransaction has a digest');
|
|
152
|
+
|
|
153
|
+
// created objects are accessible on the result
|
|
154
|
+
t.ok(suiTransaction.created.length > 0, 'SuiTransaction.created is populated');
|
|
155
|
+
|
|
156
|
+
// objectStorage was NOT updated — raw path skips that
|
|
157
|
+
const storeAfter = suiMaster.objectStorage.asArray().length;
|
|
158
|
+
t.equal(storeAfter, storeBefore, 'objectStorage was NOT updated by signAndExecuteTransaction');
|
|
159
|
+
|
|
160
|
+
// module 'added' event was NOT emitted — raw path skips that too
|
|
161
|
+
t.equal(addedViaRaw.length, 0, 'module "added" event was NOT emitted by signAndExecuteTransaction');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('stops local test node', async t => {
|
|
165
|
+
await SuiLocalTestValidator.stop().catch(() => {});
|
|
166
|
+
t.pass('stopped');
|
|
167
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
import t from 'tap';
|
|
4
|
+
import { SuiMaster, SuiLocalTestValidator } from '../index.js';
|
|
5
|
+
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
const { test } = t;
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
let suiLocalTestValidator = null;
|
|
14
|
+
let suiMaster = null;
|
|
15
|
+
let contract = null;
|
|
16
|
+
let chatShopId = null;
|
|
17
|
+
|
|
18
|
+
test('spawn local test node', async t => {
|
|
19
|
+
suiLocalTestValidator = await SuiLocalTestValidator.launch({ testFallbackEnabled: true });
|
|
20
|
+
t.ok(suiLocalTestValidator.active);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('setup — init, fund, publish', async t => {
|
|
24
|
+
suiMaster = new SuiMaster({ client: suiLocalTestValidator, as: 'somebody', debug: false });
|
|
25
|
+
await suiMaster.initialize();
|
|
26
|
+
|
|
27
|
+
await suiMaster.requestSuiFromFaucet();
|
|
28
|
+
|
|
29
|
+
contract = suiMaster.addPackage({
|
|
30
|
+
path: path.join(__dirname, './test_move_contracts/suidouble_chat/'),
|
|
31
|
+
});
|
|
32
|
+
await contract.build();
|
|
33
|
+
await contract.publish();
|
|
34
|
+
t.ok(contract.address, 'contract published');
|
|
35
|
+
|
|
36
|
+
chatShopId = suiMaster.objectStorage.findMostRecentByTypeName('ChatShop')?.id;
|
|
37
|
+
t.ok(chatShopId, 'ChatShop found after publish');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('module addEventListener — added/created fire for each created object', async t => {
|
|
41
|
+
const moduleAdded = [];
|
|
42
|
+
const moduleCreated = [];
|
|
43
|
+
|
|
44
|
+
const onAdded = (ev) => moduleAdded.push(ev.detail);
|
|
45
|
+
const onCreated = (ev) => moduleCreated.push(ev.detail);
|
|
46
|
+
|
|
47
|
+
contract.modules.suidouble_chat.addEventListener('added', onAdded);
|
|
48
|
+
contract.modules.suidouble_chat.addEventListener('created', onCreated);
|
|
49
|
+
|
|
50
|
+
const result = await contract.moveCall('suidouble_chat', 'post', [
|
|
51
|
+
chatShopId,
|
|
52
|
+
contract.arg('string', 'listener test'),
|
|
53
|
+
contract.arg('string', 'meta'),
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
contract.modules.suidouble_chat.removeEventListener('added', onAdded);
|
|
57
|
+
contract.modules.suidouble_chat.removeEventListener('created', onCreated);
|
|
58
|
+
|
|
59
|
+
t.ok(result.created.length > 0, 'moveCall created at least one object');
|
|
60
|
+
|
|
61
|
+
// 'created' fires exactly once per chain-created object
|
|
62
|
+
t.equal(moduleCreated.length, result.created.length,
|
|
63
|
+
`'created' fired ${moduleCreated.length} times — once per created object`);
|
|
64
|
+
|
|
65
|
+
// 'added' fires at least once per chain-created object (may include first-seen mutated)
|
|
66
|
+
t.ok(moduleAdded.length >= result.created.length,
|
|
67
|
+
`'added' fired ${moduleAdded.length} times (≥ created count)`);
|
|
68
|
+
|
|
69
|
+
// every created object must appear in both listeners
|
|
70
|
+
for (const created of result.created) {
|
|
71
|
+
t.ok(moduleAdded.some((o) => o?.id === created.id),
|
|
72
|
+
`'added' fired for ${created.typeName}`);
|
|
73
|
+
t.ok(moduleCreated.some((o) => o?.id === created.id),
|
|
74
|
+
`'created' fired for ${created.typeName}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// every payload is a SuiObject with a 0x-prefixed id
|
|
78
|
+
for (const obj of moduleAdded) {
|
|
79
|
+
t.ok(obj && typeof obj.id === 'string' && obj.id.startsWith('0x'),
|
|
80
|
+
`'added' payload has a valid id`);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('package addEventListener — added is forwarded from module', async t => {
|
|
85
|
+
const packageAdded = [];
|
|
86
|
+
const moduleAdded = [];
|
|
87
|
+
|
|
88
|
+
const onPackageAdded = (ev) => packageAdded.push(ev.detail);
|
|
89
|
+
const onModuleAdded = (ev) => moduleAdded.push(ev.detail);
|
|
90
|
+
|
|
91
|
+
contract.addEventListener('added', onPackageAdded);
|
|
92
|
+
contract.modules.suidouble_chat.addEventListener('added', onModuleAdded);
|
|
93
|
+
|
|
94
|
+
await contract.moveCall('suidouble_chat', 'post', [
|
|
95
|
+
chatShopId,
|
|
96
|
+
contract.arg('string', 'package listener test'),
|
|
97
|
+
contract.arg('string', 'meta'),
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
contract.removeEventListener('added', onPackageAdded);
|
|
101
|
+
contract.modules.suidouble_chat.removeEventListener('added', onModuleAdded);
|
|
102
|
+
|
|
103
|
+
t.ok(moduleAdded.length > 0, 'module "added" fired');
|
|
104
|
+
t.equal(packageAdded.length, moduleAdded.length,
|
|
105
|
+
'package "added" forwarded every module "added"');
|
|
106
|
+
|
|
107
|
+
for (const obj of packageAdded) {
|
|
108
|
+
t.ok(obj && typeof obj.id === 'string' && obj.id.startsWith('0x'),
|
|
109
|
+
`package 'added' payload has a valid id`);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('removeEventListener — listener stops receiving events after removal', async t => {
|
|
114
|
+
const received = [];
|
|
115
|
+
const onAdded = (ev) => received.push(ev.detail);
|
|
116
|
+
|
|
117
|
+
contract.modules.suidouble_chat.addEventListener('added', onAdded);
|
|
118
|
+
contract.modules.suidouble_chat.removeEventListener('added', onAdded);
|
|
119
|
+
|
|
120
|
+
await contract.moveCall('suidouble_chat', 'post', [
|
|
121
|
+
chatShopId,
|
|
122
|
+
contract.arg('string', 'after removal'),
|
|
123
|
+
contract.arg('string', 'meta'),
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
t.equal(received.length, 0, 'no events received after removeEventListener');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('module addEventListener — deleted fires for burned object', async t => {
|
|
130
|
+
// create a ChatResponse to burn
|
|
131
|
+
const postResult = await contract.moveCall('suidouble_chat', 'post', [
|
|
132
|
+
chatShopId,
|
|
133
|
+
contract.arg('string', 'to be burned'),
|
|
134
|
+
contract.arg('string', 'meta'),
|
|
135
|
+
]);
|
|
136
|
+
const chatResponse = postResult.created.find((o) => o.typeName === 'ChatResponse');
|
|
137
|
+
t.ok(chatResponse, 'ChatResponse found in created objects');
|
|
138
|
+
|
|
139
|
+
// get a reply-owned ChatResponse (burn_response requires an owned ChatResponse)
|
|
140
|
+
const chatTopMessage = postResult.created.find((o) => o.typeName === 'ChatTopMessage');
|
|
141
|
+
const replyResult = await contract.moveCall('suidouble_chat', 'reply', [
|
|
142
|
+
chatTopMessage.id,
|
|
143
|
+
contract.arg('string', 'reply to burn'),
|
|
144
|
+
contract.arg('string', 'meta'),
|
|
145
|
+
]);
|
|
146
|
+
const replyResponse = replyResult.created.find((o) => o.typeName === 'ChatResponse');
|
|
147
|
+
t.ok(replyResponse, 'reply ChatResponse found');
|
|
148
|
+
|
|
149
|
+
const moduleDeleted = [];
|
|
150
|
+
const onDeleted = (ev) => moduleDeleted.push(ev.detail);
|
|
151
|
+
contract.modules.suidouble_chat.addEventListener('deleted', onDeleted);
|
|
152
|
+
|
|
153
|
+
const burnResult = await contract.moveCall('suidouble_chat', 'burn_response', [replyResponse.id]);
|
|
154
|
+
|
|
155
|
+
contract.modules.suidouble_chat.removeEventListener('deleted', onDeleted);
|
|
156
|
+
|
|
157
|
+
t.ok(burnResult.deleted.length > 0, 'burn recorded at least one deleted object');
|
|
158
|
+
t.equal(moduleDeleted.length, burnResult.deleted.length,
|
|
159
|
+
`'deleted' fired ${moduleDeleted.length} times — once per deleted object`);
|
|
160
|
+
|
|
161
|
+
for (const obj of moduleDeleted) {
|
|
162
|
+
t.ok(obj && obj.isDeleted, 'deleted payload is marked isDeleted');
|
|
163
|
+
t.ok(typeof obj.id === 'string' && obj.id.startsWith('0x'),
|
|
164
|
+
'deleted payload has a valid id');
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('stops local test node', async t => {
|
|
169
|
+
await SuiLocalTestValidator.stop().catch(() => {});
|
|
170
|
+
t.pass('stopped');
|
|
171
|
+
});
|