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
|
@@ -0,0 +1,184 @@
|
|
|
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
|
+
await suiMaster.requestSuiFromFaucet();
|
|
27
|
+
|
|
28
|
+
contract = suiMaster.addPackage({
|
|
29
|
+
path: path.join(__dirname, './test_move_contracts/suidouble_chat/'),
|
|
30
|
+
});
|
|
31
|
+
await contract.build();
|
|
32
|
+
await contract.publish();
|
|
33
|
+
t.ok(contract.address, 'contract published');
|
|
34
|
+
|
|
35
|
+
chatShopId = suiMaster.objectStorage.findMostRecentByTypeName('ChatShop')?.id;
|
|
36
|
+
t.ok(chatShopId, 'ChatShop found after publish');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('moveCall rejects with a readable error message when transaction aborts', async t => {
|
|
40
|
+
// 500 chars exceeds suidouble_chat's free-post limit → Move abort (FailedTransaction).
|
|
41
|
+
// Using 10000 chars would exceed the gRPC transaction size limit instead, producing a
|
|
42
|
+
// transport-level error rather than a Move execution error.
|
|
43
|
+
const tooLongText = 'x'.repeat(500);
|
|
44
|
+
|
|
45
|
+
let thrownError = null;
|
|
46
|
+
await contract.moveCall('suidouble_chat', 'post', [
|
|
47
|
+
chatShopId,
|
|
48
|
+
contract.arg('string', tooLongText),
|
|
49
|
+
contract.arg('string', 'metadata'),
|
|
50
|
+
]).catch(e => { thrownError = e; });
|
|
51
|
+
|
|
52
|
+
t.ok(thrownError, 'moveCall rejected on aborted transaction');
|
|
53
|
+
t.ok(thrownError instanceof Error, 'thrown value is an Error');
|
|
54
|
+
t.ok(typeof thrownError.message === 'string' && thrownError.message.length > 0, 'error has a non-empty message');
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
// the message should carry something useful from the RPC — an abort code or description
|
|
58
|
+
const msg = thrownError.message.toLowerCase();
|
|
59
|
+
const hasUsefulInfo = msg.includes('abort') || msg.includes('error') || msg.includes('failed') || msg.includes('overflow');
|
|
60
|
+
t.ok(hasUsefulInfo, `error message is readable: "${thrownError.message}"`);
|
|
61
|
+
|
|
62
|
+
// digest — present if the transaction was committed on chain (may be null for pre-consensus failures)
|
|
63
|
+
t.ok(thrownError.hasOwnProperty('digest'), 'error has a digest property');
|
|
64
|
+
if (thrownError.digest) {
|
|
65
|
+
t.ok(typeof thrownError.digest === 'string' && thrownError.digest.length > 0,
|
|
66
|
+
`error carries the failed tx digest: "${thrownError.digest}"`);
|
|
67
|
+
} else {
|
|
68
|
+
t.pass('digest is null — transaction did not reach consensus (expected for some abort types)');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// executionError — full SDK error object
|
|
72
|
+
t.ok(thrownError.executionError !== undefined, 'error has executionError property');
|
|
73
|
+
t.ok(thrownError.executionError !== null, 'executionError is not null');
|
|
74
|
+
t.ok(typeof thrownError.executionError.message === 'string',
|
|
75
|
+
'executionError.message is a string');
|
|
76
|
+
|
|
77
|
+
// suidouble_chat abort code for oversized text — should be a MoveAbort
|
|
78
|
+
if (thrownError.executionError.MoveAbort) {
|
|
79
|
+
const abort = thrownError.executionError.MoveAbort;
|
|
80
|
+
t.ok(typeof abort === 'object', 'MoveAbort is an object');
|
|
81
|
+
console.log('MoveAbort details:', JSON.stringify(abort));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// command index — tells which PTB command aborted (if present)
|
|
85
|
+
if (thrownError.executionError.command != null) {
|
|
86
|
+
t.ok(typeof thrownError.executionError.command === 'number',
|
|
87
|
+
`executionError.command is a number: ${thrownError.executionError.command}`);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('objectStorage is not updated after a failed moveCall', async t => {
|
|
92
|
+
const tooLongText = 'x'.repeat(500);
|
|
93
|
+
const countBefore = suiMaster.objectStorage.asArray().length;
|
|
94
|
+
|
|
95
|
+
await contract.moveCall('suidouble_chat', 'post', [
|
|
96
|
+
chatShopId,
|
|
97
|
+
contract.arg('string', tooLongText),
|
|
98
|
+
contract.arg('string', 'metadata'),
|
|
99
|
+
]).catch(() => {});
|
|
100
|
+
|
|
101
|
+
const countAfter = suiMaster.objectStorage.asArray().length;
|
|
102
|
+
t.equal(countAfter, countBefore, 'objectStorage unchanged after failed transaction');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('module "added" event is not emitted after a failed moveCall', async t => {
|
|
106
|
+
const tooLongText = 'x'.repeat(500);
|
|
107
|
+
const emitted = [];
|
|
108
|
+
const onAdded = (ev) => emitted.push(ev.detail);
|
|
109
|
+
contract.modules.suidouble_chat.addEventListener('added', onAdded);
|
|
110
|
+
|
|
111
|
+
await contract.moveCall('suidouble_chat', 'post', [
|
|
112
|
+
chatShopId,
|
|
113
|
+
contract.arg('string', tooLongText),
|
|
114
|
+
contract.arg('string', 'metadata'),
|
|
115
|
+
]).catch(() => {});
|
|
116
|
+
|
|
117
|
+
contract.modules.suidouble_chat.removeEventListener('added', onAdded);
|
|
118
|
+
t.equal(emitted.length, 0, '"added" event not emitted after failed transaction');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('executeTransaction rejects when the built tx aborts', async t => {
|
|
122
|
+
const tooLongText = 'x'.repeat(500);
|
|
123
|
+
const tx = new Transaction();
|
|
124
|
+
tx.moveCall({
|
|
125
|
+
target: `${contract.address}::suidouble_chat::post`,
|
|
126
|
+
arguments: [
|
|
127
|
+
tx.object(chatShopId),
|
|
128
|
+
txInput(tx, 'string', tooLongText),
|
|
129
|
+
txInput(tx, 'string', 'metadata'),
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await t.rejects(
|
|
134
|
+
contract.modules.suidouble_chat.executeTransaction(tx),
|
|
135
|
+
'executeTransaction rejects on aborted transaction'
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('gRPC transport error — extremely large input rejected before simulation', async t => {
|
|
140
|
+
// 10Mb causes the gRPC transport to reject the request before it reaches the node.
|
|
141
|
+
// This is a different failure path from Move abort (SimulationError) and FailedTransaction:
|
|
142
|
+
// - no executionError (no Move execution took place)
|
|
143
|
+
// - no digest (tx was never submitted)
|
|
144
|
+
// - error is typically a gRPC StatusError (RESOURCE_EXHAUSTED or INVALID_ARGUMENT)
|
|
145
|
+
const giantText = 'x'.repeat(10_000_000);
|
|
146
|
+
|
|
147
|
+
let thrownError = null;
|
|
148
|
+
await contract.moveCall('suidouble_chat', 'post', [
|
|
149
|
+
chatShopId,
|
|
150
|
+
contract.arg('string', giantText),
|
|
151
|
+
contract.arg('string', 'metadata'),
|
|
152
|
+
]).catch(e => { thrownError = e; });
|
|
153
|
+
|
|
154
|
+
t.ok(thrownError, 'moveCall rejected with a giant input');
|
|
155
|
+
t.ok(thrownError instanceof Error, 'thrown value is an Error');
|
|
156
|
+
t.ok(typeof thrownError.message === 'string' && thrownError.message.length > 0,
|
|
157
|
+
'error has a message');
|
|
158
|
+
|
|
159
|
+
// gRPC transport errors don't carry executionError — it never reached Move execution
|
|
160
|
+
t.equal(thrownError.executionError, undefined,
|
|
161
|
+
'no executionError — tx was rejected by transport, not by Move VM');
|
|
162
|
+
|
|
163
|
+
// similarly no digest
|
|
164
|
+
t.equal(thrownError.digest, undefined,
|
|
165
|
+
'no digest — tx was never submitted to the network');
|
|
166
|
+
|
|
167
|
+
console.log(`gRPC error type: ${thrownError.constructor.name}, message: ${thrownError.message}`);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('a succeeding moveCall still works after a failed one', async t => {
|
|
171
|
+
const result = await contract.moveCall('suidouble_chat', 'post', [
|
|
172
|
+
chatShopId,
|
|
173
|
+
contract.arg('string', 'short valid message'),
|
|
174
|
+
contract.arg('string', 'metadata'),
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
t.ok(result.isSuccessful(), 'transaction is successful');
|
|
178
|
+
t.ok(result.created.length > 0, 'objects created by the successful call');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('stops local test node', async t => {
|
|
182
|
+
await SuiLocalTestValidator.stop().catch(() => {});
|
|
183
|
+
t.pass('stopped');
|
|
184
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
import t from 'tap';
|
|
4
|
+
import { SuiMaster } from '../index.js';
|
|
5
|
+
|
|
6
|
+
const { test } = t;
|
|
7
|
+
|
|
8
|
+
const KNOWN_ADDRESS = '0x1eb7c57e3f2bd0fc6cb9dcffd143ea957e4d98f805c358733f76dee0667fe0b1';
|
|
9
|
+
const KNOWN_NAME = 'adeniyi.sui';
|
|
10
|
+
|
|
11
|
+
test('defaultNameServiceName — returns null when no address connected', async t => {
|
|
12
|
+
const suiMaster = new SuiMaster({ client: 'mainnet' });
|
|
13
|
+
await suiMaster.initialize();
|
|
14
|
+
|
|
15
|
+
const name = await suiMaster.defaultNameServiceName();
|
|
16
|
+
t.equal(name, null, 'returns null in read-only mode (no address connected)');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('defaultNameServiceName — resolves the primary name for a known address', async t => {
|
|
20
|
+
const suiMaster = new SuiMaster({ client: 'mainnet' });
|
|
21
|
+
await suiMaster.initialize();
|
|
22
|
+
|
|
23
|
+
suiMaster._address = KNOWN_ADDRESS;
|
|
24
|
+
|
|
25
|
+
const name = await suiMaster.defaultNameServiceName();
|
|
26
|
+
|
|
27
|
+
t.equal(name, `${KNOWN_NAME}`, `resolved name is exactly "${KNOWN_NAME}"`);
|
|
28
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
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
|
+
|
|
17
|
+
test('spawn local test node', async t => {
|
|
18
|
+
suiLocalTestValidator = await SuiLocalTestValidator.launch({ testFallbackEnabled: true });
|
|
19
|
+
t.ok(suiLocalTestValidator.active);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('setup — init, fund, publish, post, fill (creates 60 owned ChatResponse objects)', async t => {
|
|
23
|
+
suiMaster = new SuiMaster({ client: suiLocalTestValidator, as: 'somebody', debug: false });
|
|
24
|
+
await suiMaster.initialize();
|
|
25
|
+
|
|
26
|
+
await suiMaster.requestSuiFromFaucet();
|
|
27
|
+
|
|
28
|
+
contract = suiMaster.addPackage({
|
|
29
|
+
path: path.join(__dirname, './test_move_contracts/suidouble_chat/'),
|
|
30
|
+
});
|
|
31
|
+
await contract.build();
|
|
32
|
+
await contract.publish();
|
|
33
|
+
t.ok(contract.address, 'contract published');
|
|
34
|
+
|
|
35
|
+
const chatShopId = suiMaster.objectStorage.findMostRecentByTypeName('ChatShop')?.id;
|
|
36
|
+
t.ok(chatShopId, 'ChatShop found after publish');
|
|
37
|
+
|
|
38
|
+
// post creates a ChatTopMessage (shared) and a ChatResponse (owned)
|
|
39
|
+
await contract.moveCall('suidouble_chat', 'post', [
|
|
40
|
+
chatShopId,
|
|
41
|
+
contract.arg('string', 'hello'),
|
|
42
|
+
contract.arg('string', 'meta'),
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const chatTopMessage = suiMaster.objectStorage.findMostRecentByTypeName('ChatTopMessage');
|
|
46
|
+
t.ok(chatTopMessage, 'ChatTopMessage found after post');
|
|
47
|
+
|
|
48
|
+
// fill creates 60 ChatResponse objects owned by the caller — exceeds the default page limit of 50
|
|
49
|
+
const fillResult = await contract.moveCall('suidouble_chat', 'fill', [
|
|
50
|
+
chatTopMessage.id,
|
|
51
|
+
contract.arg('string', 'fill response'),
|
|
52
|
+
contract.arg('string', 'meta'),
|
|
53
|
+
]);
|
|
54
|
+
t.ok(fillResult.created.length >= 60, `fill created ${fillResult.created.length} objects (≥ 60)`);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('wait for GraphQL indexer to catch up', async t => {
|
|
58
|
+
await new Promise((res) => setTimeout(res, 1000));
|
|
59
|
+
t.pass('waited');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('getOwnedObjects — no type filter: forEach counts all objects across pages (> 50)', async t => {
|
|
63
|
+
const result = await suiMaster.getOwnedObjects({
|
|
64
|
+
owner: suiMaster.address,
|
|
65
|
+
limit: 50,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
t.ok(Array.isArray(result.data), 'data is an array');
|
|
69
|
+
|
|
70
|
+
let total = 0;
|
|
71
|
+
await result.forEach((obj) => {
|
|
72
|
+
t.ok(obj.id, 'obj has an id');
|
|
73
|
+
total++;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
t.ok(total > 50, `total owned objects ${total} > 50 (pagination exercised)`);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('getOwnedObjects — package scope: forEach counts all package objects across pages (> 50)', async t => {
|
|
80
|
+
const result = await contract.getOwnedObjects({ limit: 50 });
|
|
81
|
+
|
|
82
|
+
t.ok(Array.isArray(result.data), 'data is an array');
|
|
83
|
+
|
|
84
|
+
const originalPackageId = await contract.getOriginalPackageId();
|
|
85
|
+
let total = 0;
|
|
86
|
+
await result.forEach((obj) => {
|
|
87
|
+
t.ok(
|
|
88
|
+
obj.type && obj.type.startsWith(originalPackageId + '::'),
|
|
89
|
+
`"${obj.typeName}" belongs to the package`
|
|
90
|
+
);
|
|
91
|
+
total++;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
t.ok(total > 50, `total package-scoped objects ${total} > 50 (pagination exercised)`);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('getOwnedObjects — module scope: forEach counts module objects across pages (> 50)', async t => {
|
|
98
|
+
const result = await contract.modules.suidouble_chat.getOwnedObjects({ limit: 50 });
|
|
99
|
+
|
|
100
|
+
t.ok(Array.isArray(result.data), 'data is an array');
|
|
101
|
+
|
|
102
|
+
const originalPackageId = await contract.getOriginalPackageId();
|
|
103
|
+
const modulePrefix = `${originalPackageId}::suidouble_chat::`;
|
|
104
|
+
let total = 0;
|
|
105
|
+
await result.forEach((obj) => {
|
|
106
|
+
t.ok(
|
|
107
|
+
obj.type && obj.type.startsWith(modulePrefix),
|
|
108
|
+
`"${obj.typeName}" belongs to suidouble_chat module`
|
|
109
|
+
);
|
|
110
|
+
total++;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
t.ok(total > 50, `total module-scoped objects ${total} > 50 (pagination exercised)`);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('getOwnedObjects — struct type (ChatResponse): forEach returns only ChatResponse > 50', async t => {
|
|
117
|
+
const originalPackageId = await contract.getOriginalPackageId();
|
|
118
|
+
|
|
119
|
+
const result = await contract.modules.suidouble_chat.getOwnedObjects({
|
|
120
|
+
typeName: 'ChatResponse',
|
|
121
|
+
limit: 50,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
t.ok(Array.isArray(result.data), 'data is an array');
|
|
125
|
+
|
|
126
|
+
let total = 0;
|
|
127
|
+
await result.forEach((obj) => {
|
|
128
|
+
t.equal(obj.typeName, 'ChatResponse', 'typeName is ChatResponse');
|
|
129
|
+
t.ok(
|
|
130
|
+
obj.type && obj.type.startsWith(`${originalPackageId}::suidouble_chat::ChatResponse`),
|
|
131
|
+
'full type is correct'
|
|
132
|
+
);
|
|
133
|
+
total++;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
t.ok(total > 50, `total ChatResponse objects ${total} > 50 (pagination exercised)`);
|
|
137
|
+
|
|
138
|
+
// struct-typed ≤ module-scoped
|
|
139
|
+
const moduleResult = await contract.modules.suidouble_chat.getOwnedObjects({ limit: 50 });
|
|
140
|
+
let moduleTotal = 0;
|
|
141
|
+
await moduleResult.forEach(() => { moduleTotal++; });
|
|
142
|
+
t.ok(total <= moduleTotal, 'ChatResponse count ≤ module-scope count');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('stops local test node', async t => {
|
|
146
|
+
await SuiLocalTestValidator.stop().catch(() => {});
|
|
147
|
+
t.pass('stopped');
|
|
148
|
+
});
|
package/test/rpc.test.js
CHANGED
|
@@ -14,12 +14,9 @@ const __dirname = path.dirname(__filename);
|
|
|
14
14
|
let suiMaster = null;
|
|
15
15
|
|
|
16
16
|
test('spawn local test node', async t => {
|
|
17
|
-
const rpcClient = SuiMaster.SuiUtils.suiClientForRPC({
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
rpc: {
|
|
21
|
-
// headers: {"x-allthatnode-api-key": "xxxxxxxxxx"},
|
|
22
|
-
}
|
|
17
|
+
const rpcClient = SuiMaster.SuiUtils.suiClientForRPC('mainnet', {
|
|
18
|
+
baseUrl: 'https://fullnode.mainnet.sui.io:443',
|
|
19
|
+
// fetchInit: { headers: { "x-allthatnode-api-key": "xxxxxxxxxx" } },
|
|
23
20
|
});
|
|
24
21
|
|
|
25
22
|
const suiMaster = new SuiMaster({
|
|
@@ -60,12 +60,12 @@ test('initialization', async t => {
|
|
|
60
60
|
t.ok(suiMaster.connectedChain); // but there's chain
|
|
61
61
|
|
|
62
62
|
// by default, SuiInBrowser gets you devnet connection (it's overloaded by Wallet Extension current chain)
|
|
63
|
-
t.equal(suiMaster.connectedChain, '
|
|
63
|
+
t.equal(suiMaster.connectedChain, 'devnet');
|
|
64
64
|
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
test('initialization via mainnet', async t => {
|
|
68
68
|
const suiInBrowser = new SuiInBrowser({defaultChain: 'sui:mainnet'});
|
|
69
69
|
const suiMaster = await suiInBrowser.getSuiMaster();
|
|
70
|
-
t.equal(suiMaster.connectedChain, '
|
|
70
|
+
t.equal(suiMaster.connectedChain, 'mainnet');
|
|
71
71
|
});
|
|
@@ -11,7 +11,6 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
11
11
|
const __dirname = path.dirname(__filename);
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
|
|
15
14
|
test('initialization', async t => {
|
|
16
15
|
t.plan(2);
|
|
17
16
|
|
|
@@ -121,20 +120,20 @@ test('connecting to different chains', async t => {
|
|
|
121
120
|
const suiMaster = new SuiMaster({client: 'test', as: 'somebody'});
|
|
122
121
|
await suiMaster.initialize();
|
|
123
122
|
|
|
124
|
-
t.equal(suiMaster.connectedChain, '
|
|
123
|
+
t.equal(suiMaster.connectedChain, 'testnet');
|
|
125
124
|
|
|
126
125
|
const suiMaster2 = new SuiMaster({client: 'dev', as: 'somebody'});
|
|
127
126
|
await suiMaster2.initialize();
|
|
128
127
|
|
|
129
|
-
t.equal(suiMaster2.connectedChain, '
|
|
128
|
+
t.equal(suiMaster2.connectedChain, 'devnet');
|
|
130
129
|
|
|
131
130
|
const suiMaster3 = new SuiMaster({client: 'main', as: 'somebody'});
|
|
132
131
|
await suiMaster3.initialize();
|
|
133
132
|
|
|
134
|
-
t.equal(suiMaster3.connectedChain, '
|
|
133
|
+
t.equal(suiMaster3.connectedChain, 'mainnet');
|
|
135
134
|
|
|
136
135
|
const suiMaster4 = new SuiMaster({client: 'local', as: 'somebody'});
|
|
137
136
|
await suiMaster4.initialize();
|
|
138
137
|
|
|
139
|
-
t.equal(suiMaster4.connectedChain, '
|
|
138
|
+
t.equal(suiMaster4.connectedChain, 'localnet');
|
|
140
139
|
});
|
|
@@ -199,24 +199,21 @@ test('execute contract methods', async t => {
|
|
|
199
199
|
// by suidouble_chat contract design, ChatTopMessage is an object representing a thread,
|
|
200
200
|
// it always has at least one ChatResponse (with text of the very first message in thread)
|
|
201
201
|
let foundChatTopMessage = null;
|
|
202
|
-
let
|
|
203
|
-
let foundText = null;
|
|
202
|
+
let foundChatResponseObj = null;
|
|
204
203
|
moveCallResult.created.forEach((obj)=>{
|
|
205
204
|
if (obj.typeName == 'ChatTopMessage') {
|
|
206
205
|
foundChatTopMessage = true;
|
|
207
206
|
}
|
|
208
207
|
if (obj.typeName == 'ChatResponse') {
|
|
209
|
-
|
|
210
|
-
foundText = obj.fields.text;
|
|
208
|
+
foundChatResponseObj = obj;
|
|
211
209
|
}
|
|
212
210
|
});
|
|
213
211
|
|
|
214
212
|
t.ok(foundChatTopMessage);
|
|
215
|
-
t.ok(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
foundText = new TextDecoder().decode(new Uint8Array(foundText));
|
|
213
|
+
t.ok(foundChatResponseObj);
|
|
214
|
+
|
|
215
|
+
await foundChatResponseObj.fetchFields();
|
|
216
|
+
const foundText = suiMaster.utils.bytesFieldToString(foundChatResponseObj.fields.text);
|
|
220
217
|
|
|
221
218
|
t.equal(foundText, 'the message');
|
|
222
219
|
|
|
@@ -238,17 +235,16 @@ test('execute contract methods', async t => {
|
|
|
238
235
|
// there're at least some object created
|
|
239
236
|
t.ok(moveCallResult2.created.length > 0);
|
|
240
237
|
|
|
241
|
-
let
|
|
238
|
+
let responseObj = null;
|
|
242
239
|
moveCallResult2.created.forEach((obj)=>{
|
|
243
240
|
if (obj.typeName == 'ChatResponse') {
|
|
244
|
-
|
|
245
|
-
|
|
241
|
+
responseObj = obj;
|
|
246
242
|
chatResponseToDelete = obj.id; // ChatResponse is moved to be owned by author, so we can store id to try burn_response later
|
|
247
243
|
}
|
|
248
244
|
});
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
responseText =
|
|
245
|
+
|
|
246
|
+
await responseObj.fetchFields();
|
|
247
|
+
const responseText = suiMaster.utils.bytesFieldToString(responseObj.fields.text);
|
|
252
248
|
|
|
253
249
|
t.equal(responseText, 'ขอบคุณครับ, 🇺🇦');
|
|
254
250
|
});
|
|
@@ -261,6 +257,9 @@ test('testing paginatedResponse', async t => {
|
|
|
261
257
|
const moveCallResult = await contract.moveCall('suidouble_chat', 'fill', [chatTopMessage.id, contract.arg('string', 'the message response'), contract.arg('string', 'metadata')]);
|
|
262
258
|
t.ok(moveCallResult.created.length >= 60); // it's 60 in move code, but let's keep chat flexible
|
|
263
259
|
|
|
260
|
+
// GraphQL indexer processes events asynchronously — wait for it to catch up
|
|
261
|
+
await new Promise((res) => setTimeout(res, 1000));
|
|
262
|
+
|
|
264
263
|
const eventsResponse = await contract.modules.suidouble_chat.fetchEvents({eventTypeName: 'ChatResponseCreated'});
|
|
265
264
|
const idsInEventsDict = {};
|
|
266
265
|
let responsesInEventsCount = 0;
|
|
@@ -292,7 +291,6 @@ test('testing paginatedResponse', async t => {
|
|
|
292
291
|
|
|
293
292
|
test('find owned module objects with query', async t => {
|
|
294
293
|
const module = await contract.getModule('suidouble_chat');
|
|
295
|
-
await module.getNormalizedMoveFunction('fill');
|
|
296
294
|
|
|
297
295
|
const paginatedResponse = await module.getOwnedObjects();
|
|
298
296
|
|
|
@@ -342,22 +340,21 @@ test('testing move call with coins', async t => {
|
|
|
342
340
|
// by suidouble_chat contract design, ChatTopMessage is an object representing a thread,
|
|
343
341
|
// it always has at least one ChatResponse (with text of the very first message in thread)
|
|
344
342
|
let foundChatTopMessage = null;
|
|
345
|
-
let
|
|
346
|
-
let foundText = null;
|
|
343
|
+
let foundChatResponseObj2 = null;
|
|
347
344
|
moveCallResult.created.forEach((obj)=>{
|
|
348
345
|
if (obj.typeName == 'ChatTopMessage') {
|
|
349
346
|
foundChatTopMessage = true;
|
|
350
347
|
}
|
|
351
348
|
if (obj.typeName == 'ChatResponse') {
|
|
352
|
-
|
|
353
|
-
foundText = obj.fields.text;
|
|
349
|
+
foundChatResponseObj2 = obj;
|
|
354
350
|
}
|
|
355
351
|
});
|
|
356
352
|
|
|
357
353
|
t.ok(foundChatTopMessage);
|
|
358
|
-
t.ok(
|
|
354
|
+
t.ok(foundChatResponseObj2);
|
|
359
355
|
|
|
360
|
-
|
|
356
|
+
await foundChatResponseObj2.fetchFields();
|
|
357
|
+
const foundText = suiMaster.utils.bytesFieldToString(foundChatResponseObj2.fields.text);
|
|
361
358
|
|
|
362
359
|
t.equal(foundText, longMessageYouCanNotPostForFree);
|
|
363
360
|
|
|
@@ -379,6 +376,71 @@ test('testing move call with vector<Coin<..>>', async t => {
|
|
|
379
376
|
t.ok( balanceNow <= (balanceWas - 400000000000n) ); // vector<Coin<SUI>> paid
|
|
380
377
|
});
|
|
381
378
|
|
|
379
|
+
test('getUpgradeCapId — returns a 0x-prefixed id string', async t => {
|
|
380
|
+
const upgradeCapId = await contract.getUpgradeCapId();
|
|
381
|
+
|
|
382
|
+
t.ok(upgradeCapId, 'upgradeCapId is truthy');
|
|
383
|
+
t.equal(typeof upgradeCapId, 'string', 'upgradeCapId is a string');
|
|
384
|
+
t.ok(upgradeCapId.startsWith('0x'), 'upgradeCapId starts with 0x');
|
|
385
|
+
|
|
386
|
+
// calling again should return the same cached value
|
|
387
|
+
const upgradeCapId2 = await contract.getUpgradeCapId();
|
|
388
|
+
t.equal(upgradeCapId, upgradeCapId2, 'second call returns the same id (cached)');
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test('contract.fetchEvents — package-scope returns events from all modules', async t => {
|
|
392
|
+
const packageEvents = await contract.fetchEvents();
|
|
393
|
+
|
|
394
|
+
t.ok(Array.isArray(packageEvents.data), 'data is an array');
|
|
395
|
+
t.ok(packageEvents.data.length > 0, 'at least one event found at package scope');
|
|
396
|
+
|
|
397
|
+
// should include at least ChatShopCreated emitted at publish time
|
|
398
|
+
let foundChatShopCreated = false;
|
|
399
|
+
for (const event of packageEvents.data) {
|
|
400
|
+
t.ok(event.isSuiEvent, 'each event has isSuiEvent brand');
|
|
401
|
+
t.ok(typeof event.type === 'string' && event.type.length > 0, 'each event has a type');
|
|
402
|
+
t.ok(event.type.split('::').length >= 3, 'event type is fully qualified');
|
|
403
|
+
if (event.typeName === 'ChatShopCreated') {
|
|
404
|
+
foundChatShopCreated = true;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
t.ok(foundChatShopCreated, 'ChatShopCreated event present at package scope');
|
|
408
|
+
|
|
409
|
+
// module-scoped fetchEvents should return a subset of package-scoped events
|
|
410
|
+
const moduleEvents = await contract.modules.suidouble_chat.fetchEvents();
|
|
411
|
+
t.ok(moduleEvents.data.length <= packageEvents.data.length, 'module events ≤ package events');
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test('SuiObject.fetchTransactions — transaction history via GraphQL', async t => {
|
|
415
|
+
// use the most recently created ChatTopMessage, which has been touched by at least
|
|
416
|
+
// the post() call and possibly the fill() call in the paginatedResponse test
|
|
417
|
+
const chatTopMessage = contract.objectStorage.findMostRecentByTypeName('ChatTopMessage');
|
|
418
|
+
t.ok(chatTopMessage, 'found a ChatTopMessage in objectStorage');
|
|
419
|
+
|
|
420
|
+
// GraphQL indexer processes transactions asynchronously — wait for it to catch up
|
|
421
|
+
await new Promise((res) => setTimeout(res, 1000));
|
|
422
|
+
|
|
423
|
+
const txResponse = await chatTopMessage.fetchTransactions({ limit: 20 });
|
|
424
|
+
|
|
425
|
+
t.ok(Array.isArray(txResponse.data), 'data is an array');
|
|
426
|
+
t.ok(txResponse.data.length > 0, 'at least one transaction touched the ChatTopMessage');
|
|
427
|
+
|
|
428
|
+
for (const tx of txResponse.data) {
|
|
429
|
+
t.ok(typeof tx.digest === 'string' && tx.digest.length > 0, 'each tx has a digest');
|
|
430
|
+
t.ok(typeof tx.timestampMs === 'number' && tx.timestampMs > 0, 'each tx has a positive timestampMs');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ascending order should give the oldest transaction first
|
|
434
|
+
const asc = await chatTopMessage.fetchTransactions({ limit: 20, order: 'asc' });
|
|
435
|
+
const desc = await chatTopMessage.fetchTransactions({ limit: 20, order: 'desc' });
|
|
436
|
+
|
|
437
|
+
t.ok(asc.data.length > 0, 'ascending query returns results');
|
|
438
|
+
t.ok(desc.data.length > 0, 'descending query returns results');
|
|
439
|
+
|
|
440
|
+
// first tx in ascending should be older than (or equal to) first tx in descending
|
|
441
|
+
t.ok(asc.data[0].timestampMs <= desc.data[0].timestampMs, 'ascending oldest ≤ descending newest');
|
|
442
|
+
});
|
|
443
|
+
|
|
382
444
|
test('testing move call deleting object', async t => {
|
|
383
445
|
|
|
384
446
|
console.error('chatResponseToDelete', chatResponseToDelete);
|