suidouble 1.45.2 → 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.
Files changed (59) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/README.md +222 -131
  3. package/index.js +1 -3
  4. package/lib/SuiCliCommands.js +18 -25
  5. package/lib/SuiCoin.js +86 -138
  6. package/lib/SuiCoins.js +70 -31
  7. package/lib/SuiCommonMethods.js +40 -3
  8. package/lib/SuiEvent.js +54 -6
  9. package/lib/SuiInBrowser.js +145 -46
  10. package/lib/SuiInBrowserAdapter.js +164 -37
  11. package/lib/SuiLocalTestValidator.js +78 -25
  12. package/lib/SuiMaster.js +351 -126
  13. package/lib/SuiMemoryObjectStorage.js +66 -73
  14. package/lib/SuiObject.js +128 -153
  15. package/lib/SuiPackage.js +292 -187
  16. package/lib/SuiPackageModule.js +176 -221
  17. package/lib/SuiPaginatedResponse.js +288 -25
  18. package/lib/SuiPseudoRandomAddress.js +29 -2
  19. package/lib/SuiTransaction.js +115 -70
  20. package/lib/SuiUtils.js +179 -124
  21. package/package.json +30 -14
  22. package/test/build_modules.test.js +41 -0
  23. package/test/coins.test.js +17 -16
  24. package/test/custom_transaction.test.js +167 -0
  25. package/test/event_listeners.test.js +171 -0
  26. package/test/failed_transaction.test.js +184 -0
  27. package/test/name_service.test.js +28 -0
  28. package/test/owned_objects.test.js +148 -0
  29. package/test/rpc.test.js +3 -6
  30. package/test/sui_in_browser.test.js +2 -2
  31. package/test/sui_master_basic.test.js +4 -5
  32. package/test/sui_master_onlocal.test.js +84 -22
  33. package/test/sui_object_properties.test.js +85 -0
  34. package/test/test_move_contracts/different_types/Move.lock +18 -21
  35. package/test/test_move_contracts/different_types/sources/different_types.move +12 -12
  36. package/test/test_move_contracts/suidouble_chat/Move.lock +18 -22
  37. package/test/test_move_contracts/suidouble_chat/sources/suidouble_chat.move +9 -8
  38. package/tsconfig.json +15 -0
  39. package/types/index.d.ts +15 -0
  40. package/types/lib/SuiCliCommands.d.ts +6 -0
  41. package/types/lib/SuiCoin.d.ts +183 -0
  42. package/types/lib/SuiCoins.d.ts +93 -0
  43. package/types/lib/SuiCommonMethods.d.ts +37 -0
  44. package/types/lib/SuiEvent.d.ts +95 -0
  45. package/types/lib/SuiInBrowser.d.ts +189 -0
  46. package/types/lib/SuiInBrowserAdapter.d.ts +167 -0
  47. package/types/lib/SuiLocalTestValidator.d.ts +92 -0
  48. package/types/lib/SuiMaster.d.ts +333 -0
  49. package/types/lib/SuiMemoryObjectStorage.d.ts +96 -0
  50. package/types/lib/SuiObject.d.ts +135 -0
  51. package/types/lib/SuiPackage.d.ts +233 -0
  52. package/types/lib/SuiPackageModule.d.ts +139 -0
  53. package/types/lib/SuiPaginatedResponse.d.ts +148 -0
  54. package/types/lib/SuiPseudoRandomAddress.d.ts +33 -0
  55. package/types/lib/SuiTransaction.d.ts +92 -0
  56. package/types/lib/SuiUtils.d.ts +152 -0
  57. package/types/lib/data/icons.d.ts +12 -0
  58. package/lib/SuiTestScenario.js +0 -169
  59. 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
- chain: 'mainnet',
19
- url: 'https://fullnode.mainnet.sui.io',
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, 'sui:devnet');
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, 'sui:mainnet');
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, 'sui:testnet');
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, 'sui:devnet');
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, 'sui:mainnet');
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, 'sui:localnet');
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 foundChatResponse = null;
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
- foundChatResponse = true;
210
- foundText = obj.fields.text;
208
+ foundChatResponseObj = obj;
211
209
  }
212
210
  });
213
211
 
214
212
  t.ok(foundChatTopMessage);
215
- t.ok(foundChatResponse);
216
-
217
- // messageTextAsBytes = [].slice.call(new TextEncoder().encode(messageText)); // regular array with utf data
218
- // suidouble_chat contract store text a bytes (easier to work with unicode things), let's convert it back to js string
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 responseText = null;
238
+ let responseObj = null;
242
239
  moveCallResult2.created.forEach((obj)=>{
243
240
  if (obj.typeName == 'ChatResponse') {
244
- responseText = obj.fields.text;
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
- // messageTextAsBytes = [].slice.call(new TextEncoder().encode(messageText)); // regular array with utf data
250
- // suidouble_chat contract store text a bytes (easier to work with unicode things), let's convert it back to js string
251
- responseText = new TextDecoder().decode(new Uint8Array(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 foundChatResponse = null;
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
- foundChatResponse = true;
353
- foundText = obj.fields.text;
349
+ foundChatResponseObj2 = obj;
354
350
  }
355
351
  });
356
352
 
357
353
  t.ok(foundChatTopMessage);
358
- t.ok(foundChatResponse);
354
+ t.ok(foundChatResponseObj2);
359
355
 
360
- foundText = new TextDecoder().decode(new Uint8Array(foundText));
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);