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
@@ -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 { 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 { Commands, Transaction } from '@mysten/sui/transactions';
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
@@ -209,20 +225,31 @@ export default class SuiCoin {
209
225
  * @returns {boolean} if processed ok
210
226
  */
211
227
  setMetadata(meta) {
212
- if (meta && meta.decimals && meta.decimals > 0 && meta.name && meta.symbol) {
228
+ if (SuiCoin.isValidMetadata(meta)) {
213
229
  this._metadata = meta;
214
230
  this._exists = true;
215
231
  return true;
216
232
  }
233
+ return false;
234
+ }
217
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
+ */
241
+ static isValidMetadata(meta) {
242
+ if (meta && meta.decimals && meta.decimals > 0 && meta.name && meta.symbol) {
243
+ return true;
244
+ }
218
245
  return false;
219
246
  }
220
247
 
221
248
  /**
222
- * Load coin metadata from the blockchain. As a good pattern, it's better to have metadata hard-coded or cached
223
- * and set via suiMaster.suiCoins.setCoinMetas(...)
224
- *
225
- * @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
226
253
  */
227
254
  async getMetadata() {
228
255
  if (this._metadata) {
@@ -237,25 +264,24 @@ export default class SuiCoin {
237
264
  this.__getMetadataResolver = null;
238
265
  this.__getMetadataPromise = new Promise((res)=>{ this.__getMetadataResolver = res; });
239
266
 
240
- let result = null;
241
267
  try {
242
- result = await this.suiMaster.client.getCoinMetadata({
268
+ const result = await this.suiMaster.client.getCoinMetadata({
243
269
  coinType: this.coinType,
244
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
+ }
245
277
  } catch (e) {
246
278
  console.error(e);
247
- result = null;
248
- }
249
- if (!result) {
250
279
  this._exists = false;
251
- } else {
252
- this._metadata = result;
253
- this._exists = true;
254
280
  }
255
281
 
256
- this.__getMetadataResolver(true);
282
+ this.__getMetadataResolver(this._metadata);
257
283
 
258
- return true;
284
+ return this._metadata;
259
285
  }
260
286
 
261
287
  /**
@@ -265,111 +291,33 @@ export default class SuiCoin {
265
291
  * @returns {Promise.<bigint>}
266
292
  */
267
293
  async getBalance(owner) {
268
- const coins = [];
269
- let result = null;
270
- let cursor = null;
271
- do {
272
- result = await this.suiMaster.client.getCoins({
273
- owner: owner,
274
- coinType: this.coinType,
275
- limit: 50,
276
- cursor: cursor,
277
- });
278
- coins.push(...result.data);
279
-
280
- cursor = result.nextCursor;
281
- } while (result.hasNextPage);
282
-
283
- let totalAmount = BigInt(0);
284
- for (const coin of coins) {
285
- 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);
286
302
  }
287
-
288
- return totalAmount;
289
303
  }
290
304
 
291
305
 
292
306
  /**
293
- * Returns TransactionObjectArgument with Coin of amount to be used in tranasctions
294
- *
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
+ *
295
311
  * @param {Transaction} txb - Native SUI SDK Transaction
296
- * @param {string} owner - address of the owner
297
- * @param {BigInt|string} amount - amount of coin. BigIng or String to be normalized via Coin decimals, "0.05" for 0.05 sui
298
- * @param {boolean} addEmptyCoins - attach coins == 0 to the list
299
- *
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)
300
314
  * @returns {Promise.<TransactionObjectArgument>}
301
315
  */
302
- async coinOfAmountToTxCoin(txb, owner, amount, addEmptyCoins = false) {
316
+ async coinOfAmountToTxCoin(txb, _owner, amount) {
303
317
  const normalizedAmount = await this.lazyNormalizeAmount(amount);
304
-
305
- const expectedAmountAsBigInt = BigInt(normalizedAmount);
306
- const coinIds = await this.coinObjectsEnoughForAmount(owner, expectedAmountAsBigInt, addEmptyCoins);
307
-
308
- if (!coinIds || !coinIds.length) {
309
- throw new Error('you do not have enough coins of type '+this.coinType);
310
- }
311
-
312
- if (coinIds.length == 1) {
313
- // only one coin object enough to cover the expense
314
- if (this.isSUI()) {
315
- const coinInput = txb.add(Commands.SplitCoins(txb.gas, [txb.pure.u64(expectedAmountAsBigInt)]));
316
- return coinInput;
317
- } else {
318
- // some other coin
319
- const coinInput = txb.add(Commands.SplitCoins(txb.object(coinIds[0]), [txb.pure.u64(expectedAmountAsBigInt)]));
320
- return coinInput;
321
- }
322
- } else {
323
- // few coin objects to merge
324
- const coinIdToMergeIn = coinIds.shift();
325
- txb.add(Commands.MergeCoins(txb.object(coinIdToMergeIn), coinIds.map((id)=>{return txb.object(id);})));
326
- const coinInputSplet = txb.add(Commands.SplitCoins(txb.object(coinIdToMergeIn), [txb.pure.u64(expectedAmountAsBigInt)]));
327
-
328
- return coinInputSplet;
329
- }
330
- }
331
-
332
- async coinObjectsEnoughForAmount(owner, expectedAmount, addEmptyCoins = false) {
333
- const expectedAmountAsBigInt = BigInt(expectedAmount);
334
-
335
- const coinIds = [];
336
- const coins = [];
337
-
338
- let result = null;
339
- let cursor = null;
340
- do {
341
- result = await this.suiMaster.client.getCoins({
342
- owner: owner,
343
- coinType: this.coinType,
344
- limit: 50,
345
- cursor: cursor,
346
- });
347
- coins.push(...result.data);
348
-
349
- cursor = result.nextCursor;
350
- } while (result.hasNextPage);
351
-
352
- coins.sort((a, b) => {
353
- // From big to small
354
- return Number(b.balance) - Number(a.balance);
318
+ return txb.coin({
319
+ type: this.coinType,
320
+ balance: BigInt(normalizedAmount),
355
321
  });
356
-
357
- let totalAmount = BigInt(0);
358
- for (const coin of coins) {
359
- if (totalAmount <= expectedAmountAsBigInt) {
360
- coinIds.push(coin.coinObjectId);
361
- totalAmount = totalAmount + BigInt(coin.balance);// totalAmount.add(coin.balance);
362
- } else {
363
- if (addEmptyCoins && BigInt(coin.balance) == 0n) {
364
- coinIds.push(coin.coinObjectId);
365
- }
366
- }
367
- }
368
-
369
- if (totalAmount >= expectedAmountAsBigInt) {
370
- return coinIds;
371
- }
372
-
373
- return null;
374
322
  }
375
323
  };
package/lib/SuiCoins.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import SuiCoin from './SuiCoin.js';
2
2
  import SuiCommonMethods from './SuiCommonMethods.js';
3
- import { allCoinMetas } from '@polymedia/coinmeta';
4
3
  import { normalizeStructTag } from "@mysten/sui/utils";
5
-
4
+ import { allCoinMetas } from '@polymedia/coinmeta';
6
5
 
7
6
  /**
8
7
  * @typedef {import("./SuiCoin.js").CoinMeta} CoinMeta
@@ -22,10 +21,11 @@ export default class SuiCoins extends SuiCommonMethods {
22
21
  * SuiCoins constructor
23
22
  * @param {Object} params - Initialization parameters
24
23
  * @param {SuiMaster} params.suiMaster - instance of SuiMaster
24
+ * @param {boolean} [params.debug]
25
25
  */
26
- constructor(params = {}) {
26
+ constructor(params) {
27
27
  super(params);
28
-
28
+
29
29
  /** @type {SuiMaster} */
30
30
  this._suiMaster = params.suiMaster;
31
31
  if (!this._suiMaster) {
@@ -41,17 +41,22 @@ export default class SuiCoins extends SuiCommonMethods {
41
41
  }
42
42
  }
43
43
 
44
+ /**
45
+ * @type {SuiMaster}
46
+ */
44
47
  get suiMaster() {
45
48
  return this._suiMaster;
46
49
  }
47
50
 
51
+ /** @returns {Object.<string, SuiCoin>} map of coinType → SuiCoin instance */
48
52
  get coins() {
49
53
  return this._coins;
50
54
  }
51
55
 
52
56
  /**
53
- * set predefined coin metas so they will not be fetched from RPC
54
- * @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
55
60
  * @returns {number} count of processed items
56
61
  */
57
62
  setCoinMetas(coinMetas) {
@@ -82,12 +87,45 @@ export default class SuiCoins extends SuiCommonMethods {
82
87
  }
83
88
 
84
89
  /**
85
- * Get all CoinBalance[] for the specific address or connected address
86
- *
87
- * @param {Object} params
88
- * @param {string?} params.owner - owner address, if skipped - will query for connected wallet address
89
- *
90
- * @returns {Promise.<Array.<SuidoubleCoinBalance>>}
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
+ */
95
+ static async prefetchMainnetCoinMetas() {
96
+ if (this.__preloadPromise) {
97
+ return this.__preloadPromise;
98
+ }
99
+
100
+ this.__preloadPromiseResolver = null;
101
+ this.__preloadPromise = new Promise((resolve) => {
102
+ this.__preloadPromiseResolver = resolve;
103
+ });
104
+
105
+ const preloadUrl = "https://coinmeta.polymedia.app/api/data.json";
106
+ const fetched = [];
107
+ try {
108
+ const response = await fetch(preloadUrl);
109
+ if (response.ok) {
110
+ const coinMetas = await response.json();
111
+ if (Array.isArray(coinMetas) && coinMetas.length) {
112
+ fetched.push(...coinMetas);
113
+ }
114
+ }
115
+ } catch (e) {
116
+ // ignore
117
+ }
118
+
119
+ this.__preloadPromiseResolver(fetched);
120
+ return fetched;
121
+ }
122
+
123
+ /**
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>>}
91
129
  */
92
130
  async getAllBalances(params = {}) {
93
131
  let owner = params.owner;
@@ -98,21 +136,22 @@ export default class SuiCoins extends SuiCommonMethods {
98
136
  /** @type {Array.<SuidoubleCoinBalance>} */
99
137
  const ret = [];
100
138
 
101
- const nativeCoinBalances = await this._suiMaster.client.getAllBalances({
102
- owner,
103
- });
104
-
105
- for (const nativeCoinBalance of nativeCoinBalances) {
106
- /** @type {SuidoubleCoinBalance} */
107
- const coinBalance = {
108
- coin: this.get(nativeCoinBalance.coinType),
109
- coinType: nativeCoinBalance.coinType,
110
- coinObjectCount: nativeCoinBalance.coinObjectCount,
111
- totalBalance: BigInt(nativeCoinBalance.totalBalance),
112
- lockedBalance: nativeCoinBalance.lockedBalance,
113
- };
114
- ret.push(coinBalance);
115
- }
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);
116
155
 
117
156
  return ret;
118
157
  }
@@ -141,10 +180,10 @@ export default class SuiCoins extends SuiCommonMethods {
141
180
  }
142
181
 
143
182
  /**
144
- * Return instance of SuiCoin of specific type
145
- *
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
+ *
146
186
  * @param {string} coinType - MoveType, or 'SUI' as helper
147
- *
148
187
  * @returns {SuiCoin}
149
188
  */
150
189
  get(coinType) {
@@ -173,7 +212,7 @@ export default class SuiCoins extends SuiCommonMethods {
173
212
  * @param {SuiMaster} params.suiMaster - instance of SuiMaster
174
213
  * @returns {SuiCoins}
175
214
  */
176
- static getSingleton(params = {}) {
215
+ static getSingleton(params) {
177
216
  const suiMaster = params.suiMaster;
178
217
  const connectedChain = suiMaster.connectedChain;
179
218