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
@@ -9,9 +9,13 @@ const loadModule = async (modulePath) => {
9
9
  }
10
10
 
11
11
  /**
12
- * Wrapper to get system function available on node.js side only, returning null on the browser side
13
- * @param {string} name - execSync, spawn, fs, path, net
14
- * @returns {(function | null)}
12
+ * Load a node.js built-in on the server side. Returns null in environments where the module is not available (e.g. browser).
13
+ *
14
+ * For 'execSync' and 'spawn' resolves to the named function from `child_process`.
15
+ * For 'fs', 'path', 'net' resolves to the whole module's default export.
16
+ *
17
+ * @param {'execSync' | 'spawn' | 'fs' | 'path' | 'net'} name
18
+ * @returns {Promise<Function | object | null>} the function or module, or null if unavailable
15
19
  */
16
20
  const getSystemFunction = async (name) => {
17
21
  try {
@@ -64,7 +68,7 @@ export default class SuiCliCommands {
64
68
  __waitPortPromiseResolver(false);
65
69
  });
66
70
 
67
- socket.connect(port, "0.0.0.0");
71
+ socket.connect(port, "127.0.0.1");
68
72
 
69
73
  const portIsThere = await __waitPortPromise;
70
74
  socket.destroy();
@@ -76,7 +80,7 @@ export default class SuiCliCommands {
76
80
  const doSpawn = await getSystemFunction('spawn');
77
81
 
78
82
  if (!doSpawn) {
79
- throw new Error('can not spawn a proccess in this env');
83
+ throw new Error('can not spawn a process in this env');
80
84
  }
81
85
 
82
86
  return await new Promise((res,rej)=>{
@@ -84,7 +88,8 @@ export default class SuiCliCommands {
84
88
  let e = null;
85
89
  const proc = doSpawn(command, params, {
86
90
  env: {
87
- ...process.env,
91
+ // @ts-ignore — process is a Node.js global; not available in TypeScript without @types/node
92
+ ...(process ? process.env : {}),
88
93
  ...envVars,
89
94
  }
90
95
  });
@@ -101,25 +106,13 @@ export default class SuiCliCommands {
101
106
  }
102
107
  }, 100);
103
108
  });
104
-
105
- // const proc = doSpawn(command, [], {
106
- // env: {
107
- // ...process.env,
108
- // ...envVars,
109
- // }
110
- // });
111
- // proc.on('error', function(err) {
112
- // console.log('Oh noez, teh errurz: ' + err);
113
- // });
114
-
115
- // return proc;
116
109
  }
117
110
 
118
111
  static async exec(command) {
119
112
  const doExecSync = await getSystemFunction('execSync');
120
113
 
121
114
  if (!doExecSync) {
122
- throw new Error('can not exec a proccess in this env');
115
+ throw new Error('can not exec a process in this env');
123
116
  }
124
117
 
125
118
  return doExecSync(
@@ -137,19 +130,19 @@ export default class SuiCliCommands {
137
130
  }
138
131
 
139
132
  try {
140
- const buildPathContent = await doFs.promises.readdir(path.join(this._path, 'build'));
141
-
133
+ const buildPathContent = await doFs.promises.readdir(doPath.join(path, 'build'));
134
+
142
135
  // @todo: there may be some junk folders and we'd have to get project name from Move.toml ?
143
136
  const buildPath = buildPathContent[0];
144
-
145
- const dirents = await doFs.promises.readdir(doPath.join(this._path, 'build', buildPath, 'bytecode_modules'), { withFileTypes: true });
137
+
138
+ const dirents = await doFs.promises.readdir(doPath.join(path, 'build', buildPath, 'bytecode_modules'), { withFileTypes: true });
146
139
  const names = dirents
147
140
  .filter(dirent => dirent.isFile())
148
141
  .map(dirent => dirent.name.split('.mv').join(''));
149
-
142
+
150
143
  return names;
151
144
  } catch (e) {
152
- throw new Error('can not get modules names from local package path');
145
+ throw new Error('can not get modules names from local package path: ' + (e && e.message ? e.message : e));
153
146
  }
154
147
  }
155
148
  };
package/lib/SuiCoin.js CHANGED
@@ -1,8 +1,9 @@
1
- import { TransactionCommands as Commands, Transaction } from '@mysten/sui/transactions';
1
+ import { Transaction } from '@mysten/sui/transactions';
2
2
 
3
3
  /**
4
4
  * @typedef {import("@mysten/sui/transactions").TransactionObjectArgument} TransactionObjectArgument
5
5
  *
6
+ * @import { SuiClientTypes } from "@mysten/sui/client"
6
7
  *
7
8
  * @typedef CoinMeta
8
9
  * @type {object}
@@ -15,17 +16,16 @@ import { TransactionCommands as Commands, Transaction } from '@mysten/sui/transa
15
16
  * @property {string} [type] - Coin type string
16
17
  *
17
18
  *
18
- * @typedef SuidoubleCoinBalance
19
- * @type {object}
20
- * @property {SuiCoin} coin
21
- * @property {string} coinType
22
- * @property {number} coinObjectCount
23
- * @property {bigint} totalBalance
24
- * @property {Object.<string,string>} lockedBalance
25
- *
19
+ * @typedef {SuiClientTypes.Balance & { coin: SuiCoin, totalBalance: bigint }} SuidoubleCoinBalance
20
+ * @typedef {import("./SuiMaster.js").default} SuiMaster
21
+ * @typedef {import("./SuiCoins.js").default} SuiCoins
26
22
  */
27
23
 
28
- /** Coin metadata object */
24
+
25
+ /**
26
+ * Handle for a single coin type (e.g. `0x2::sui::SUI`). Caches metadata, formats amounts
27
+ * via decimals, and builds `Coin<T>` arguments for transactions.
28
+ */
29
29
  export default class SuiCoin {
30
30
 
31
31
  /**
@@ -34,7 +34,7 @@ export default class SuiCoin {
34
34
  * @param {string} params.coinType - sui object type for a coin, without Coin<...>, only the inside type
35
35
  * @param {SuiCoins} params.suiCoins - instance of SuiCoins
36
36
  */
37
- constructor(params = {}) {
37
+ constructor(params) {
38
38
  this._coinType = params.coinType;
39
39
  this._suiCoins = params.suiCoins;
40
40
 
@@ -54,7 +54,7 @@ export default class SuiCoin {
54
54
  throw new Error('Please load coin metadata first');
55
55
  }
56
56
 
57
- return BigInt(Math.floor(parseFloat(amount, 10) * Math.pow(10, this.decimals)));
57
+ return BigInt(Math.floor(parseFloat(amount) * Math.pow(10, this.decimals)));
58
58
  }
59
59
 
60
60
  return BigInt(amount);
@@ -71,13 +71,13 @@ export default class SuiCoin {
71
71
  }
72
72
 
73
73
  /**
74
- * Get readable representation of amount value (system one, bigint or converted from bigint, NOT the '1.99' etc)
75
- * based on coin decimals metadata ( so it expects metadata to be loaded or set).
76
- *
77
- * @param {Object} params - format parameters
78
- * @param {boolean} params.withAbbr - append K, M, B, T for large amounts. Suiet-style
79
- * @param {boolean|string} params.separateThousands - separate thousands, by ',' or by specific delimeter
80
- *
74
+ * Get readable representation of a raw amount (bigint-native, NOT a pre-formatted "1.99" string),
75
+ * based on coin decimals metadata (requires metadata to be loaded or set).
76
+ *
77
+ * @param {bigint|number|string} amount - raw amount in the coin's base units
78
+ * @param {Object} [options] - format options
79
+ * @param {boolean} [options.withAbbr] - append K/M/B/T for large amounts (Suiet-style)
80
+ * @param {boolean|string} [options.separateThousands] - separate thousands by `,` (true) or by a custom delimiter
81
81
  * @returns {string}
82
82
  */
83
83
  amountToString(amount, options = {}) {
@@ -123,14 +123,14 @@ export default class SuiCoin {
123
123
 
124
124
 
125
125
  /**
126
- * Format large amounts
127
- *
126
+ * Format large amounts.
127
+ *
128
128
  * thanks @bruceeewong and @suiet
129
- *
130
- * @param {bigint} amount
131
- * @param {bigint} measureUnit
132
- * @param {string} abbrSymbol
133
- *
129
+ *
130
+ * @param {bigint} amount
131
+ * @param {bigint} measureUnit - divisor for the unit (1_000n for K, 1_000_000n for M, …)
132
+ * @param {string} abbrSymbol - suffix to append (K, M, B, T)
133
+ * @param {boolean|string} [separateThousands=false] - separate thousands by `,` (true) or by a custom delimiter
134
134
  * @returns {string}
135
135
  */
136
136
  formatWithAbbr(amount, measureUnit, abbrSymbol, separateThousands = false) {
@@ -150,10 +150,14 @@ export default class SuiCoin {
150
150
  }
151
151
 
152
152
 
153
+ /**
154
+ * @type {SuiMaster}
155
+ */
153
156
  get suiMaster() {
154
157
  return this._suiCoins.suiMaster;
155
158
  }
156
159
 
160
+ /** @returns {string} coin type string, always prefixed with `0x` */
157
161
  get coinType() {
158
162
  if (this._coinType.indexOf('0x') === 0) {
159
163
  return this._coinType;
@@ -171,6 +175,7 @@ export default class SuiCoin {
171
175
  return '0x2::coin::Coin<'+this.coinType+'>';
172
176
  }
173
177
 
178
+ /** @returns {number | undefined} decimals from loaded metadata, or `undefined` if not yet loaded */
174
179
  get decimals() {
175
180
  if (this.metadata) {
176
181
  return this.metadata.decimals;
@@ -178,10 +183,12 @@ export default class SuiCoin {
178
183
  return undefined;
179
184
  }
180
185
 
186
+ /** @returns {?CoinMeta} loaded coin metadata, or null if not loaded */
181
187
  get metadata() {
182
188
  return this._metadata;
183
189
  }
184
190
 
191
+ /** @returns {?string} coin symbol from metadata, or null if metadata isn't loaded */
185
192
  get symbol() {
186
193
  if (this.metadata) {
187
194
  return this.metadata.symbol;
@@ -190,10 +197,19 @@ export default class SuiCoin {
190
197
  return null;
191
198
  }
192
199
 
200
+ /** @returns {?string} coin name from metadata, or null if metadata isn't loaded */
193
201
  get name() {
194
- return this.metadata.name;
202
+ if (this.metadata) {
203
+ return this.metadata.name;
204
+ }
205
+
206
+ return null;
195
207
  }
196
208
 
209
+ /**
210
+ * True if this coin's type is the normalized SUI type (`0x2::sui::SUI` expanded to 64 hex chars).
211
+ * @returns {boolean}
212
+ */
197
213
  isSUI() {
198
214
  const lc = this._coinType.toLowerCase();
199
215
  if (lc == '0x0000000000000000000000000000000000000000000000000000000000000002::sui::sui') { // as it's normalized
@@ -217,6 +233,11 @@ export default class SuiCoin {
217
233
  return false;
218
234
  }
219
235
 
236
+ /**
237
+ * Returns true if the object looks like a usable CoinMeta (has positive `decimals`, `name`, `symbol`).
238
+ * @param {CoinMeta} meta
239
+ * @returns {boolean}
240
+ */
220
241
  static isValidMetadata(meta) {
221
242
  if (meta && meta.decimals && meta.decimals > 0 && meta.name && meta.symbol) {
222
243
  return true;
@@ -225,10 +246,10 @@ export default class SuiCoin {
225
246
  }
226
247
 
227
248
  /**
228
- * Load coin metadata from the blockchain. As a good pattern, it's better to have metadata hard-coded or cached
229
- * and set via suiMaster.suiCoins.setCoinMetas(...)
230
- *
231
- * @returns {Promise.<boolean>}
249
+ * Load coin metadata from the blockchain. Concurrent callers share the same in-flight request.
250
+ * As a good pattern, prefer hard-coded / cached metadata set via `suiMaster.suiCoins.setCoinMetas(...)`.
251
+ *
252
+ * @returns {Promise<?CoinMeta>} resolved metadata, or `null` if the coin type has none on chain
232
253
  */
233
254
  async getMetadata() {
234
255
  if (this._metadata) {
@@ -243,25 +264,24 @@ export default class SuiCoin {
243
264
  this.__getMetadataResolver = null;
244
265
  this.__getMetadataPromise = new Promise((res)=>{ this.__getMetadataResolver = res; });
245
266
 
246
- let result = null;
247
267
  try {
248
- result = await this.suiMaster.client.getCoinMetadata({
268
+ const result = await this.suiMaster.client.getCoinMetadata({
249
269
  coinType: this.coinType,
250
270
  });
271
+ if (result && result.coinMetadata && result.coinMetadata.decimals) {
272
+ this._exists = true;
273
+ this._metadata = result.coinMetadata;
274
+ } else {
275
+ this._exists = false;
276
+ }
251
277
  } catch (e) {
252
278
  console.error(e);
253
- result = null;
254
- }
255
- if (!result) {
256
279
  this._exists = false;
257
- } else {
258
- this._metadata = result;
259
- this._exists = true;
260
280
  }
261
281
 
262
- this.__getMetadataResolver(true);
282
+ this.__getMetadataResolver(this._metadata);
263
283
 
264
- return true;
284
+ return this._metadata;
265
285
  }
266
286
 
267
287
  /**
@@ -271,111 +291,33 @@ export default class SuiCoin {
271
291
  * @returns {Promise.<bigint>}
272
292
  */
273
293
  async getBalance(owner) {
274
- const coins = [];
275
- let result = null;
276
- let cursor = null;
277
- do {
278
- result = await this.suiMaster.client.getCoins({
279
- owner: owner,
280
- coinType: this.coinType,
281
- limit: 50,
282
- cursor: cursor,
283
- });
284
- coins.push(...result.data);
285
-
286
- cursor = result.nextCursor;
287
- } while (result.hasNextPage);
288
-
289
- let totalAmount = BigInt(0);
290
- for (const coin of coins) {
291
- totalAmount = totalAmount + BigInt(coin.balance);
294
+ const coinBalanceResponse = await this.suiMaster.client.getBalance({
295
+ owner: owner,
296
+ coinType: this.coinType,
297
+ });
298
+ if (coinBalanceResponse?.balance?.balance) {
299
+ return BigInt(coinBalanceResponse.balance.balance);
300
+ } else {
301
+ return BigInt(0);
292
302
  }
293
-
294
- return totalAmount;
295
303
  }
296
304
 
297
305
 
298
306
  /**
299
- * Returns TransactionObjectArgument with Coin of amount to be used in tranasctions
300
- *
307
+ * Return a TransactionObjectArgument with a Coin of the requested amount, ready to use in moveCall.
308
+ * Delegates to the v2 SDK's `tx.coinWithBalance` intent, which at build time fetches all owned
309
+ * coins of this type, merges them (including zero-balance dust), and splits off the requested amount.
310
+ *
301
311
  * @param {Transaction} txb - Native SUI SDK Transaction
302
- * @param {string} owner - address of the owner
303
- * @param {BigInt|string} amount - amount of coin. BigIng or String to be normalized via Coin decimals, "0.05" for 0.05 sui
304
- * @param {boolean} addEmptyCoins - attach coins == 0 to the list
305
- *
312
+ * @param {string} _owner - unused; kept for signature compatibility. The resolver picks coins owned by the tx sender.
313
+ * @param {BigInt|string} amount - amount of coin. BigInt or string normalized via Coin decimals ("0.05" 0.05 SUI)
306
314
  * @returns {Promise.<TransactionObjectArgument>}
307
315
  */
308
- async coinOfAmountToTxCoin(txb, owner, amount, addEmptyCoins = false) {
316
+ async coinOfAmountToTxCoin(txb, _owner, amount) {
309
317
  const normalizedAmount = await this.lazyNormalizeAmount(amount);
310
-
311
- const expectedAmountAsBigInt = BigInt(normalizedAmount);
312
- const coinIds = await this.coinObjectsEnoughForAmount(owner, expectedAmountAsBigInt, addEmptyCoins);
313
-
314
- if (!coinIds || !coinIds.length) {
315
- throw new Error('you do not have enough coins of type '+this.coinType);
316
- }
317
-
318
- if (coinIds.length == 1) {
319
- // only one coin object enough to cover the expense
320
- if (this.isSUI()) {
321
- const coinInput = txb.add(Commands.SplitCoins(txb.gas, [txb.pure.u64(expectedAmountAsBigInt)]));
322
- return coinInput;
323
- } else {
324
- // some other coin
325
- const coinInput = txb.add(Commands.SplitCoins(txb.object(coinIds[0]), [txb.pure.u64(expectedAmountAsBigInt)]));
326
- return coinInput;
327
- }
328
- } else {
329
- // few coin objects to merge
330
- const coinIdToMergeIn = coinIds.shift();
331
- txb.add(Commands.MergeCoins(txb.object(coinIdToMergeIn), coinIds.map((id)=>{return txb.object(id);})));
332
- const coinInputSplet = txb.add(Commands.SplitCoins(txb.object(coinIdToMergeIn), [txb.pure.u64(expectedAmountAsBigInt)]));
333
-
334
- return coinInputSplet;
335
- }
336
- }
337
-
338
- async coinObjectsEnoughForAmount(owner, expectedAmount, addEmptyCoins = false) {
339
- const expectedAmountAsBigInt = BigInt(expectedAmount);
340
-
341
- const coinIds = [];
342
- const coins = [];
343
-
344
- let result = null;
345
- let cursor = null;
346
- do {
347
- result = await this.suiMaster.client.getCoins({
348
- owner: owner,
349
- coinType: this.coinType,
350
- limit: 50,
351
- cursor: cursor,
352
- });
353
- coins.push(...result.data);
354
-
355
- cursor = result.nextCursor;
356
- } while (result.hasNextPage);
357
-
358
- coins.sort((a, b) => {
359
- // From big to small
360
- return Number(b.balance) - Number(a.balance);
318
+ return txb.coin({
319
+ type: this.coinType,
320
+ balance: BigInt(normalizedAmount),
361
321
  });
362
-
363
- let totalAmount = BigInt(0);
364
- for (const coin of coins) {
365
- if (totalAmount <= expectedAmountAsBigInt) {
366
- coinIds.push(coin.coinObjectId);
367
- totalAmount = totalAmount + BigInt(coin.balance);// totalAmount.add(coin.balance);
368
- } else {
369
- if (addEmptyCoins && BigInt(coin.balance) == 0n) {
370
- coinIds.push(coin.coinObjectId);
371
- }
372
- }
373
- }
374
-
375
- if (totalAmount >= expectedAmountAsBigInt) {
376
- return coinIds;
377
- }
378
-
379
- return null;
380
322
  }
381
323
  };
package/lib/SuiCoins.js CHANGED
@@ -21,10 +21,11 @@ export default class SuiCoins extends SuiCommonMethods {
21
21
  * SuiCoins constructor
22
22
  * @param {Object} params - Initialization parameters
23
23
  * @param {SuiMaster} params.suiMaster - instance of SuiMaster
24
+ * @param {boolean} [params.debug]
24
25
  */
25
- constructor(params = {}) {
26
+ constructor(params) {
26
27
  super(params);
27
-
28
+
28
29
  /** @type {SuiMaster} */
29
30
  this._suiMaster = params.suiMaster;
30
31
  if (!this._suiMaster) {
@@ -40,17 +41,22 @@ export default class SuiCoins extends SuiCommonMethods {
40
41
  }
41
42
  }
42
43
 
44
+ /**
45
+ * @type {SuiMaster}
46
+ */
43
47
  get suiMaster() {
44
48
  return this._suiMaster;
45
49
  }
46
50
 
51
+ /** @returns {Object.<string, SuiCoin>} map of coinType → SuiCoin instance */
47
52
  get coins() {
48
53
  return this._coins;
49
54
  }
50
55
 
51
56
  /**
52
- * set predefined coin metas so they will not be fetched from RPC
53
- * @param {(Object.<string, CoinMeta> | Array.<CoinMeta>)}
57
+ * Set predefined coin metas so they will not need to be fetched from RPC.
58
+ *
59
+ * @param {Object.<string, CoinMeta> | Array.<CoinMeta>} coinMetas - keyed map by coin type, or an array of CoinMeta with `type` field
54
60
  * @returns {number} count of processed items
55
61
  */
56
62
  setCoinMetas(coinMetas) {
@@ -80,6 +86,12 @@ export default class SuiCoins extends SuiCommonMethods {
80
86
  return processedCount;
81
87
  }
82
88
 
89
+ /**
90
+ * Fetch the mainnet coin metadata bundle from `coinmeta.polymedia.app` and cache the promise so
91
+ * concurrent callers share a single request. Failures are swallowed and resolve to an empty array.
92
+ *
93
+ * @returns {Promise<Array.<CoinMeta>>} list of CoinMeta records, or an empty array on failure
94
+ */
83
95
  static async prefetchMainnetCoinMetas() {
84
96
  if (this.__preloadPromise) {
85
97
  return this.__preloadPromise;
@@ -109,12 +121,11 @@ export default class SuiCoins extends SuiCommonMethods {
109
121
  }
110
122
 
111
123
  /**
112
- * Get all CoinBalance[] for the specific address or connected address
113
- *
114
- * @param {Object} params
115
- * @param {string?} params.owner - owner address, if skipped - will query for connected wallet address
116
- *
117
- * @returns {Promise.<Array.<SuidoubleCoinBalance>>}
124
+ * Get all CoinBalance[] for the specific address or connected address.
125
+ *
126
+ * @param {Object} [params]
127
+ * @param {?string} [params.owner] - owner address; if omitted, uses the connected SuiMaster address
128
+ * @returns {Promise<Array<SuidoubleCoinBalance>>}
118
129
  */
119
130
  async getAllBalances(params = {}) {
120
131
  let owner = params.owner;
@@ -125,21 +136,22 @@ export default class SuiCoins extends SuiCommonMethods {
125
136
  /** @type {Array.<SuidoubleCoinBalance>} */
126
137
  const ret = [];
127
138
 
128
- const nativeCoinBalances = await this._suiMaster.client.getAllBalances({
129
- owner,
130
- });
131
-
132
- for (const nativeCoinBalance of nativeCoinBalances) {
133
- /** @type {SuidoubleCoinBalance} */
134
- const coinBalance = {
135
- coin: this.get(nativeCoinBalance.coinType),
136
- coinType: nativeCoinBalance.coinType,
137
- coinObjectCount: nativeCoinBalance.coinObjectCount,
138
- totalBalance: BigInt(nativeCoinBalance.totalBalance),
139
- lockedBalance: nativeCoinBalance.lockedBalance,
140
- };
141
- ret.push(coinBalance);
142
- }
139
+ let cursor = null;
140
+ do {
141
+ const response = await this._suiMaster.client.listBalances({ owner, cursor });
142
+ for (const responseItem of (response?.balances ?? [])) {
143
+ /** @type {SuidoubleCoinBalance} */
144
+ ret.push({
145
+ coin: this.get(responseItem.coinType),
146
+ coinType: responseItem.coinType,
147
+ balance: responseItem.balance,
148
+ coinBalance: responseItem.balance,
149
+ addressBalance: responseItem.addressBalance,
150
+ totalBalance: BigInt(responseItem.balance),
151
+ });
152
+ }
153
+ cursor = response?.hasNextPage ? response.cursor : null;
154
+ } while (cursor);
143
155
 
144
156
  return ret;
145
157
  }
@@ -168,10 +180,10 @@ export default class SuiCoins extends SuiCommonMethods {
168
180
  }
169
181
 
170
182
  /**
171
- * Return instance of SuiCoin of specific type
172
- *
183
+ * Return instance of SuiCoin of specific type. The result is cached per normalized coin type,
184
+ * so repeated calls with equivalent inputs return the same instance.
185
+ *
173
186
  * @param {string} coinType - MoveType, or 'SUI' as helper
174
- *
175
187
  * @returns {SuiCoin}
176
188
  */
177
189
  get(coinType) {
@@ -200,7 +212,7 @@ export default class SuiCoins extends SuiCommonMethods {
200
212
  * @param {SuiMaster} params.suiMaster - instance of SuiMaster
201
213
  * @returns {SuiCoins}
202
214
  */
203
- static getSingleton(params = {}) {
215
+ static getSingleton(params) {
204
216
  const suiMaster = params.suiMaster;
205
217
  const connectedChain = suiMaster.connectedChain;
206
218
 
@@ -1,37 +1,74 @@
1
-
2
-
1
+ /**
2
+ * Minimal `CustomEvent` polyfill — `globalThis.CustomEvent` is not universally available
3
+ * in Node runtimes, so suidouble dispatches its own class with a `detail` field.
4
+ */
3
5
  class CustomEvent extends Event {
4
6
  #detail;
5
7
 
8
+ /**
9
+ * @param {string} type
10
+ * @param {EventInit & { detail?: * }} [options]
11
+ */
6
12
  constructor(type, options) {
7
13
  super(type, options);
8
14
  this.#detail = options?.detail ?? null;
9
15
  }
10
16
 
17
+ /** @returns {*} the payload passed via `options.detail` at construction time, or null */
11
18
  get detail() {
12
19
  return this.#detail;
13
20
  }
14
21
  }
15
22
 
23
+ /**
24
+ * Base class for suidouble domain objects. Extends `EventTarget` so subclasses can emit
25
+ * events via `this.emit(...)` and consumers can subscribe via `addEventListener(...)`.
26
+ * Also provides a debug-gated `log(...)` helper.
27
+ */
16
28
  export default class SuiCommonMethods extends EventTarget {
29
+ /**
30
+ * @param {Object} [params]
31
+ * @param {boolean} [params.debug] - enable debug logging via `log(...)`
32
+ */
17
33
  constructor(params = {}) {
18
34
  super();
19
35
 
20
36
  this._debug = !!params.debug;
21
37
  }
22
38
 
39
+ /**
40
+ * Debug logger. No-op unless the instance was constructed with `debug: true`.
41
+ * Prefixes messages with the owning SuiMaster's instance number (if available) and the class name.
42
+ * @param {...any} args
43
+ */
23
44
  log(...args) {
24
45
  if (!this._debug) {
25
46
  return;
26
47
  }
27
48
 
28
- let prefix = (this._suiMaster ? (''+this._suiMaster.instanceN+' |') : (this.instanceN ? ''+this.instanceN+' |' : '') );
49
+ const self = /** @type {any} */ (this);
50
+ let prefix = (self._suiMaster ? (''+self._suiMaster.instanceN+' |') : (self.instanceN ? ''+self.instanceN+' |' : '') );
29
51
 
30
52
  args.unshift(this.constructor.name+' |');
31
53
  args.unshift(prefix);
32
54
  console.info.apply(null, args);
33
55
  }
34
56
 
57
+ /**
58
+ * Dispatch an event to registered `addEventListener` subscribers.
59
+ *
60
+ * If `data` is a `SuiEvent` (has `isSuiEvent === true`) and `forceCustom` is falsy, it is dispatched
61
+ * directly so its own `.type` (the Move event name) is used — this lets consumers
62
+ * `addEventListener('ChatShopCreated', …)` without wrapping. Otherwise dispatches a `CustomEvent`
63
+ * of type `eventType` with `data` placed on the event's `detail` field.
64
+ *
65
+ * Any dispatch errors are caught and logged to `console.error` so a buggy listener can't abort the
66
+ * surrounding flow.
67
+ *
68
+ * @param {string} eventType - event name when wrapping in a CustomEvent (ignored for direct SuiEvent dispatch)
69
+ * @param {*} data - payload; SuiEvent is dispatched directly, anything else is wrapped
70
+ * @param {boolean} [forceCustom=false] - if true, always wrap in a CustomEvent even for SuiEvent payloads
71
+ */
35
72
  emit(eventType, data, forceCustom = false) {
36
73
  try {
37
74
  if (data && data.isSuiEvent && !forceCustom) {