suidouble 2.5.0 → 2.17.0-1

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 (57) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/README.md +222 -131
  3. package/index.js +0 -2
  4. package/lib/SuiCliCommands.js +18 -25
  5. package/lib/SuiCoin.js +79 -137
  6. package/lib/SuiCoins.js +41 -29
  7. package/lib/SuiCommonMethods.js +40 -3
  8. package/lib/SuiEvent.js +54 -6
  9. package/lib/SuiInBrowser.js +161 -16
  10. package/lib/SuiInBrowserAdapter.js +192 -40
  11. package/lib/SuiLocalTestValidator.js +76 -14
  12. package/lib/SuiMaster.js +335 -139
  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 -127
  21. package/package.json +29 -13
  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 -0
  35. package/test/test_move_contracts/suidouble_chat/Move.lock +18 -0
  36. package/tsconfig.json +15 -0
  37. package/types/index.d.ts +15 -0
  38. package/types/lib/SuiCliCommands.d.ts +6 -0
  39. package/types/lib/SuiCoin.d.ts +183 -0
  40. package/types/lib/SuiCoins.d.ts +93 -0
  41. package/types/lib/SuiCommonMethods.d.ts +37 -0
  42. package/types/lib/SuiEvent.d.ts +95 -0
  43. package/types/lib/SuiInBrowser.d.ts +195 -0
  44. package/types/lib/SuiInBrowserAdapter.d.ts +173 -0
  45. package/types/lib/SuiLocalTestValidator.d.ts +92 -0
  46. package/types/lib/SuiMaster.d.ts +333 -0
  47. package/types/lib/SuiMemoryObjectStorage.d.ts +96 -0
  48. package/types/lib/SuiObject.d.ts +135 -0
  49. package/types/lib/SuiPackage.d.ts +233 -0
  50. package/types/lib/SuiPackageModule.d.ts +139 -0
  51. package/types/lib/SuiPaginatedResponse.d.ts +148 -0
  52. package/types/lib/SuiPseudoRandomAddress.d.ts +33 -0
  53. package/types/lib/SuiTransaction.d.ts +92 -0
  54. package/types/lib/SuiUtils.d.ts +152 -0
  55. package/types/lib/data/icons.d.ts +12 -0
  56. package/lib/SuiTestScenario.js +0 -169
  57. package/test/sui_test_scenario.test.js +0 -61
@@ -2,38 +2,84 @@ import SuiCommonMethods from './SuiCommonMethods.js';
2
2
  import SuiEvent from './SuiEvent.js';
3
3
  import SuiTransaction from './SuiTransaction.js';
4
4
 
5
+ /**
6
+ * @import { SuiClientTypes } from "@mysten/sui/client"
7
+ */
8
+
9
+ /**
10
+ * @typedef {import('./SuiObject.js').default} SuiObject
11
+ * @typedef {import('./SuiMaster.js').default} SuiMaster
12
+ * @typedef {{ filter?: object, limit?: number }} GraphqlEventsParams
13
+ * @typedef {{ owner: string, type?: string, limit?: number }} GraphqlOwnedObjectsParams
14
+ * @typedef {{ filter?: object, limit?: number }} GraphqlTransactionsParams
15
+ * @typedef {SuiClientTypes.ListOwnedObjectsOptions<SuiClientTypes.ObjectInclude>
16
+ * | SuiClientTypes.ListCoinsOptions
17
+ * | SuiClientTypes.ListDynamicFieldsOptions
18
+ * | GraphqlEventsParams
19
+ * | GraphqlOwnedObjectsParams
20
+ * | GraphqlTransactionsParams } SuiPaginatedResponseParams
21
+ */
22
+
23
+ /**
24
+ * @template {SuiObject | SuiTransaction | SuiEvent} T
25
+ */
5
26
  export default class SuiPaginatedResponse extends SuiCommonMethods {
6
- constructor(params = {}) {
27
+
28
+ /**
29
+ * @param {Object} params
30
+ * @param {SuiMaster} params.suiMaster
31
+ * @param {SuiPaginatedResponseParams} params.params
32
+ * @param {string} [params.method]
33
+ * @param {string} [params.order]
34
+ * @param {boolean} [params.debug]
35
+ */
36
+ constructor(params) {
7
37
  super(params);
8
38
 
9
39
  this._suiMaster = params.suiMaster;
10
40
  if (!this._suiMaster) {
11
- throw new Error('suiMaster is requried for SuiPaginatedResponse');
41
+ throw new Error('suiMaster is required for SuiPaginatedResponse');
12
42
  }
13
43
 
14
44
  this._method = params.method;
45
+
46
+ /** @type {SuiPaginatedResponseParams} */
15
47
  this._params = params.params;
16
- this._order = params.order || 'descending'; // default - newest first, pass {order: 'ascending'} for oldest first
48
+ this._order = SuiPaginatedResponse._normalizeOrder(params.order);
17
49
 
18
50
  this._hasNextPage = true;
19
51
  this._nextCursor = null;
20
52
 
53
+ /** @type {T[]} */
21
54
  this._data = [];
22
55
  }
23
56
 
57
+ /**
58
+ * Accept 'asc' | 'ascending' | 'desc' | 'descending' (case-insensitive).
59
+ * Defaults to 'descending' (newest first).
60
+ * @param {?string} input
61
+ * @returns {'ascending' | 'descending'}
62
+ */
63
+ static _normalizeOrder(input) {
64
+ if (!input) return 'descending';
65
+ const v = (''+input).toLowerCase();
66
+ if (v === 'asc' || v === 'ascending') return 'ascending';
67
+ if (v === 'desc' || v === 'descending') return 'descending';
68
+ return 'descending';
69
+ }
70
+
24
71
  /**
25
72
  * Simple itterator to go over all list of items, not caring about pagination/cursors etc. It fetches next page when needed
26
73
  * Optional maxLimit second parameter to stop when reached count
27
- * @param {function} callbackFunc
28
- * @param {Number} maxLimit
74
+ * @param {(item: T) => (void | Promise<void>)} callbackFunc
75
+ * @param {Number} maxLimit
29
76
  */
30
77
  async forEach(callbackFunc, maxLimit = null) {
31
78
  let curN = 0;
32
79
  do {
33
80
  for (const item of this._data) {
34
- if (!maxLimit || curN < maxLimit) {
35
- await callbackFunc(item);
36
- }
81
+ if (maxLimit && curN >= maxLimit) break;
82
+ await callbackFunc(item);
37
83
  curN++;
38
84
  }
39
85
  } while( (!maxLimit || curN < maxLimit) && (await this.nextPage()) );
@@ -43,6 +89,7 @@ export default class SuiPaginatedResponse extends SuiCommonMethods {
43
89
  return this._hasNextPage;
44
90
  }
45
91
 
92
+ /** @returns {T[]} */
46
93
  get data() {
47
94
  return this._data;
48
95
  }
@@ -56,8 +103,17 @@ export default class SuiPaginatedResponse extends SuiCommonMethods {
56
103
  }
57
104
 
58
105
  async fetch(params = {}) {
59
- const paramsCopy = Object.assign({}, this._params);
60
- // paramsCopy.limit = 3;
106
+ if (this._method === 'graphqlEvents') {
107
+ return await this._fetchEventsGraphql(params);
108
+ }
109
+ if (this._method === 'graphqlOwnedObjects') {
110
+ return await this._fetchOwnedObjectsGraphql(params);
111
+ }
112
+ if (this._method === 'graphqlTransactions') {
113
+ return await this._fetchTransactionsGraphql(params);
114
+ }
115
+
116
+ const paramsCopy = /** @type {any} */ (Object.assign({}, this._params));
61
117
 
62
118
  if (params.cursor) {
63
119
  paramsCopy.cursor = params.cursor;
@@ -65,34 +121,241 @@ export default class SuiPaginatedResponse extends SuiCommonMethods {
65
121
  paramsCopy.order = this._order;
66
122
 
67
123
  const response = await this._suiMaster.client[this._method](paramsCopy);
68
- let responseCount = 0;
69
- if (response.data && response.data.length) {
70
- responseCount = response.data.length;
71
- }
72
124
 
73
125
  if (response.hasNextPage) {
74
126
  this._hasNextPage = true;
75
- this._nextCursor = response.nextCursor;
127
+ this._nextCursor = response.cursor ?? null;
76
128
  } else {
77
129
  this._hasNextPage = false;
78
130
  this._nextCursor = null;
79
131
  }
80
132
 
81
- this.log('got', responseCount, 'items. Has next page: ', response.hasNextPage);
82
-
83
- if (this._method === 'queryEvents') {
84
- // convert data to SuiEvent instances
85
- this._data = response.data.map((raw)=>(new SuiEvent({data: raw, suiMaster: this._suiMaster, debug: this._debug})));
86
- } else if (this._method === 'queryTransactionBlocks') {
87
- // convert data to SuiTransaction instances
88
- this._data = response.data.map((raw)=>(new SuiTransaction({data: raw, suiMaster: this._suiMaster, debug: this._debug})));
89
- } else if (this._method === 'getOwnedObjects') {
133
+ if (this._method === 'listOwnedObjects') {
90
134
  // convert data to SuiObject instances
91
- this._data = response.data.map((raw)=>(new (this._suiMaster.SuiObject)({suiMaster: this._suiMaster, debug: this._debug, objectChange: raw})));
135
+ this._data = response.objects.map((raw)=>(new (this._suiMaster.SuiObject)({suiMaster: this._suiMaster, debug: this._debug, raw: raw})));
136
+ } else if (this._method === 'listDynamicFields') {
137
+ this._data = response.dynamicFields || [];
92
138
  } else {
93
139
  this._data = response.data;
94
140
  }
95
141
 
96
142
  return this._data;
97
143
  }
144
+
145
+ /**
146
+ * Shared Relay-style GraphQL pagination:
147
+ * - ascending → `first` + `after`, `hasNextPage` / `endCursor`
148
+ * - descending → `last` + `before`, `hasPreviousPage` / `startCursor`
149
+ * `last` returns nodes ascending too, so descending callers get a reversed slice.
150
+ *
151
+ * @param {Object} config
152
+ * @param {string} config.query - GraphQL query declaring $first/$after/$last/$before + any extra vars
153
+ * @param {Object} [config.variables] - extra variables merged into the request (e.g. filter, owner)
154
+ * @param {(data: any) => any} config.extractConnection - pull the Connection (pageInfo + nodes) out of response.data
155
+ * @param {(node: any) => T} config.mapNode - turn a node into the wrapped T instance
156
+ * @param {string} [config.errorLabel='GraphQL query'] - prefix for errors
157
+ * @param {Object} [params]
158
+ * @param {?string} [params.cursor]
159
+ */
160
+ async _fetchGraphqlPaginated(config, params = {}) {
161
+ const pageSize = this._params.limit || 50;
162
+ const cursor = params.cursor || null;
163
+ const ascending = this._order === 'ascending';
164
+
165
+ const pageVars = ascending
166
+ ? { first: pageSize, after: cursor, last: null, before: null }
167
+ : { last: pageSize, before: cursor, first: null, after: null };
168
+
169
+ const response = await this._suiMaster.graphqlClient.query({
170
+ query: config.query,
171
+ variables: { ...(config.variables || {}), ...pageVars },
172
+ });
173
+
174
+ if (response.errors && response.errors.length) {
175
+ const label = config.errorLabel || 'GraphQL query';
176
+ throw new Error(`${label} failed: ` + response.errors.map((e) => e.message).join('; '));
177
+ }
178
+
179
+ const connection = config.extractConnection(response.data);
180
+ if (!connection) {
181
+ this._hasNextPage = false;
182
+ this._nextCursor = null;
183
+ this._data = [];
184
+ return this._data;
185
+ }
186
+
187
+ const pageInfo = connection.pageInfo || {};
188
+ if (ascending) {
189
+ this._hasNextPage = !!pageInfo.hasNextPage;
190
+ this._nextCursor = pageInfo.hasNextPage ? pageInfo.endCursor : null;
191
+ } else {
192
+ this._hasNextPage = !!pageInfo.hasPreviousPage;
193
+ this._nextCursor = pageInfo.hasPreviousPage ? pageInfo.startCursor : null;
194
+ }
195
+
196
+ const nodes = connection.nodes || [];
197
+ const ordered = ascending ? nodes : nodes.slice().reverse();
198
+
199
+ this._data = ordered.map(config.mapNode);
200
+ return this._data;
201
+ }
202
+
203
+ /**
204
+ * Paginate the GraphQL `events` query. `this._params` = { filter, limit } where
205
+ * `filter` is a GraphQL `EventFilter` (module/type/sender/checkpoint fields).
206
+ */
207
+ async _fetchEventsGraphql(params = {}) {
208
+ const query = `
209
+ query Events($filter: EventFilter, $first: Int, $after: String, $last: Int, $before: String) {
210
+ events(filter: $filter, first: $first, after: $after, last: $last, before: $before) {
211
+ pageInfo { hasNextPage endCursor hasPreviousPage startCursor }
212
+ nodes {
213
+ sequenceNumber
214
+ timestamp
215
+ sender { address }
216
+ transaction { digest }
217
+ contents {
218
+ type { repr }
219
+ json
220
+ bcs
221
+ }
222
+ }
223
+ }
224
+ }
225
+ `;
226
+
227
+ return await this._fetchGraphqlPaginated({
228
+ query,
229
+ variables: { filter: /** @type {any} */ (this._params).filter || {} },
230
+ extractConnection: (data) => data?.events,
231
+ mapNode: (node) => /** @type {any} */ (new SuiEvent({
232
+ suiMaster: this._suiMaster,
233
+ debug: this._debug,
234
+ data: {
235
+ type: node.contents?.type?.repr,
236
+ parsedJson: node.contents?.json,
237
+ timestampMs: node.timestamp ? new Date(node.timestamp).getTime() : null,
238
+ sender: node.sender?.address,
239
+ transactionDigest: node.transaction?.digest,
240
+ sequenceNumber: node.sequenceNumber,
241
+ bcs: node.contents?.bcs,
242
+ },
243
+ })),
244
+ errorLabel: 'GraphQL events query',
245
+ }, params);
246
+ }
247
+
248
+ /**
249
+ * Paginate `address(address).objects(filter)` via GraphQL. `this._params` = { owner, type, limit }
250
+ * where `type` is a GraphQL `ObjectFilter.type` string.
251
+ */
252
+ async _fetchOwnedObjectsGraphql(params = {}) {
253
+ const p = /** @type {any} */ (this._params);
254
+ const owner = p.owner;
255
+ const filter = p.type ? { type: p.type } : null;
256
+
257
+ const query = `
258
+ query OwnedObjects($owner: SuiAddress!, $filter: ObjectFilter, $first: Int, $after: String, $last: Int, $before: String) {
259
+ address(address: $owner) {
260
+ objects(first: $first, after: $after, last: $last, before: $before, filter: $filter) {
261
+ pageInfo { hasNextPage endCursor hasPreviousPage startCursor }
262
+ nodes {
263
+ address
264
+ version
265
+ digest
266
+ contents {
267
+ type { repr }
268
+ json
269
+ }
270
+ owner {
271
+ __typename
272
+ ... on AddressOwner { address { address } }
273
+ ... on ObjectOwner { address { address } }
274
+ ... on ConsensusAddressOwner { address { address } }
275
+ }
276
+ }
277
+ }
278
+ }
279
+ }
280
+ `;
281
+
282
+ return await this._fetchGraphqlPaginated({
283
+ query,
284
+ variables: { owner, filter },
285
+ extractConnection: (data) => data?.address?.objects,
286
+ mapNode: (node) => /** @type {any} */ (new (this._suiMaster.SuiObject)({
287
+ suiMaster: this._suiMaster,
288
+ debug: this._debug,
289
+ raw: /** @type {any} */ ({
290
+ objectId: node.address,
291
+ version: node.version,
292
+ type: node.contents?.type?.repr,
293
+ owner: SuiPaginatedResponse._normalizeGraphqlOwner(node.owner),
294
+ json: node.contents?.json,
295
+ }),
296
+ })),
297
+ errorLabel: 'GraphQL owned-objects query',
298
+ }, params);
299
+ }
300
+
301
+ /**
302
+ * Paginate the GraphQL root `transactions(filter: TransactionFilter)` query.
303
+ * `this._params` = { filter, limit } where `filter` is a GraphQL `TransactionFilter`
304
+ * (`sentAddress`, `affectedAddress`, `affectedObject`, `function`, checkpoint bounds, `kind`).
305
+ */
306
+ async _fetchTransactionsGraphql(params = {}) {
307
+ const query = `
308
+ query Transactions($filter: TransactionFilter, $first: Int, $after: String, $last: Int, $before: String) {
309
+ transactions(filter: $filter, first: $first, after: $after, last: $last, before: $before) {
310
+ pageInfo { hasNextPage endCursor hasPreviousPage startCursor }
311
+ nodes {
312
+ digest
313
+ sender { address }
314
+ effects {
315
+ status
316
+ timestamp
317
+ epoch { epochId }
318
+ }
319
+ }
320
+ }
321
+ }
322
+ `;
323
+
324
+ return await this._fetchGraphqlPaginated({
325
+ query,
326
+ variables: { filter: /** @type {any} */ (this._params).filter || {} },
327
+ extractConnection: (data) => data?.transactions,
328
+ mapNode: (node) => /** @type {any} */ (new SuiTransaction({
329
+ suiMaster: this._suiMaster,
330
+ debug: this._debug,
331
+ data: /** @type {any} */ ({
332
+ digest: node.digest,
333
+ epoch: node.effects?.epoch?.epochId,
334
+ timestampMs: node.effects?.timestamp ? new Date(node.effects.timestamp).getTime() : null,
335
+ status: { success: node.effects?.status === 'SUCCESS' },
336
+ sender: node.sender?.address,
337
+ }),
338
+ })),
339
+ errorLabel: 'GraphQL transactions query',
340
+ }, params);
341
+ }
342
+
343
+ /**
344
+ * Map a GraphQL Owner union node to the gRPC-style owner shape SuiObject consumes.
345
+ */
346
+ static _normalizeGraphqlOwner(owner) {
347
+ if (!owner || !owner.__typename) return null;
348
+ if (owner.__typename === 'AddressOwner') {
349
+ return { AddressOwner: owner.address?.address || null };
350
+ }
351
+ if (owner.__typename === 'ObjectOwner') {
352
+ return { ObjectOwner: owner.address?.address || null };
353
+ }
354
+ if (owner.__typename === 'ConsensusAddressOwner') {
355
+ return { ConsensusAddressOwner: owner.address?.address || null };
356
+ }
357
+ if (owner.__typename === 'Shared') return { Shared: {} };
358
+ if (owner.__typename === 'Immutable') return 'Immutable';
359
+ return null;
360
+ }
98
361
  };
@@ -2,12 +2,40 @@ import { entropyToMnemonic } from '@scure/bip39';
2
2
  import { wordlist} from '@scure/bip39/wordlists/english';
3
3
  import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
4
4
 
5
+ /**
6
+ * Deterministically derives an Ed25519 keypair (and thus a Sui address) from an arbitrary
7
+ * string seed. Useful for tests and local dev where you need a stable, human-readable
8
+ * "name" instead of a raw private key.
9
+ *
10
+ * The derivation is deterministic but **not cryptographically secure** — the hash function
11
+ * is intentionally simple. Do not use for production key management.
12
+ */
5
13
  export default class SuiPseudoRandomAddress {
14
+ /**
15
+ * Derive an Ed25519 keypair from an arbitrary string seed.
16
+ * The same string always produces the same keypair (and Sui address).
17
+ *
18
+ * @param {string} as - any string seed, e.g. `'alice'`, `'test-wallet-1'`
19
+ * @returns {Ed25519Keypair}
20
+ */
6
21
  static stringToKeyPair(as) {
7
22
  const pseudoRandomPhrase = SuiPseudoRandomAddress.stringToPhrase(as);
8
23
  return Ed25519Keypair.deriveKeypair(pseudoRandomPhrase);
9
24
  }
10
25
 
26
+ /**
27
+ * Convert an arbitrary string seed into a BIP-39 mnemonic phrase by mapping the string
28
+ * to 32 pseudo-random bytes and running them through `entropyToMnemonic`.
29
+ *
30
+ * The byte derivation:
31
+ * 1. Repeats the string until it is at least 32 chars (appending `*"` so `'test'` and
32
+ * `'testtest'` produce different entropy).
33
+ * 2. Maps each character to its char-code.
34
+ * 3. Folds any bytes beyond position 31 back into the first 32 via modular addition.
35
+ *
36
+ * @param {string} as - any string seed
37
+ * @returns {string} 24-word BIP-39 mnemonic derived from the seed
38
+ */
11
39
  static stringToPhrase(as) {
12
40
  let asToHash = `${as}`;
13
41
  // calculate very simple 32 bytes hash of a string
@@ -22,9 +50,8 @@ export default class SuiPseudoRandomAddress {
22
50
  asBytes[addToPos] = (asBytes[addToPos] + asBytes[i]) % 256;
23
51
  }
24
52
  }
25
- const asUint8Array = new Uint8Array(32); // see .slice(0,32) below
53
+ const asUint8Array = new Uint8Array(32);
26
54
  asUint8Array.set(asBytes.slice(0,32));
27
- // console.log(asUint8Array);
28
55
  // use @scure/bip39 to get mnemonic out of pseudo-random array:
29
56
  const phrase = entropyToMnemonic(asUint8Array, wordlist);
30
57