suidouble 0.0.3

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.
@@ -0,0 +1,520 @@
1
+ const sui = require('@mysten/sui.js');
2
+
3
+ const SuiCliCommands = require('./SuiCliCommands.js');
4
+ const SuiObject = require('./SuiObject.js');
5
+ const SuiPackageModule = require('./SuiPackageModule.js');
6
+ const SuiPaginatedResponse = require('./SuiPaginatedResponse.js');
7
+
8
+ // fromB64, toB64
9
+
10
+ class SuiPackage extends SuiObject {
11
+ constructor(params = {}) {
12
+ super(params);
13
+
14
+ // set in super() :
15
+ // this._id
16
+ // this._suiMaster
17
+
18
+ this._path = params.path;
19
+ this._id = params.id || null;
20
+ this._expectedModules = params.modules || null;
21
+
22
+ this._isPublished = false;
23
+ this._publishedVersion = null;
24
+
25
+ this._upgradeCap = null;
26
+ this._upgradeCapId = null;
27
+
28
+ this._isBuilt = false;
29
+ this._builtModules = null;
30
+ this._builtDependencies = null;
31
+ this._builtDigest = null;
32
+
33
+ this._modules = {
34
+
35
+ };
36
+ }
37
+
38
+ get objectStorage() {
39
+ return this._suiMaster.objectStorage;
40
+ }
41
+
42
+ get modules() {
43
+ return this._modules;
44
+ }
45
+
46
+ async isOnChain() {
47
+ try {
48
+ await this.checkOnChainIfNeeded();
49
+ } catch (e) {
50
+ console.error(e);
51
+ }
52
+
53
+ if (this._publishedVersion && this._isPublished && this.address) {
54
+ return true;
55
+ }
56
+
57
+ return false;
58
+ }
59
+
60
+ async moveCall(moduleName, methodName, params) {
61
+ await this.checkOnChainIfNeeded();
62
+ return await this.modules[moduleName].moveCall(methodName, params);
63
+ }
64
+
65
+ async fetchEvents(moduleName, params = {}) {
66
+ await this.checkOnChainIfNeeded();
67
+ return await this.modules[moduleName].fetchEvents(params);
68
+ }
69
+
70
+ async checkOnChainIfNeeded() {
71
+ if (this._isPublished) {
72
+ return true;
73
+ }
74
+
75
+ if (!this._id && !this._expectedModules && this._path) {
76
+ // we can get needed modules names from local package path
77
+ this._expectedModules = await this.getModulesNamesFromBuild();
78
+ }
79
+
80
+ if (!this._id && this._expectedModules) {
81
+ // we can get most recent version of package published on blockchain using names of needed modules in it
82
+ this._id = await this.tryToFindByExpectedModules();
83
+ }
84
+
85
+ if (!this._id) {
86
+ // if we really can not find any address on blockchain, means we need to publish it first
87
+ throw new Error('no package id, nothing to check. Maybe lets start with .publish() ?');
88
+ }
89
+
90
+ const version = await this.getVersionOnChain();
91
+ if (version) {
92
+ this._isPublished = true;
93
+ return true;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Try to find package on chain using its modules names.
99
+ * Search for packages you own, in last versions of it
100
+ * List all UpgradeCap -> List packages -> Filter ( max version, all modules )
101
+ * @returns id of package
102
+ */
103
+ async tryToFindByExpectedModules() {
104
+ this.log('trying to find Package by expected modules in its content...');
105
+
106
+ // normalize expected modules. May be an array or comma separated string
107
+ const expectModules = [];
108
+
109
+ let arr = this._expectedModules;
110
+ if (!Array.isArray(this._expectedModules)) {
111
+ //
112
+ arr = (''+this._expectedModules).split(',');
113
+ }
114
+ arr.forEach((item)=>{
115
+ if (item.trim()) {
116
+ if (expectModules.indexOf(item.trim()) === -1) {
117
+ expectModules.push(item.trim());
118
+ }
119
+ }
120
+ });
121
+
122
+ this.log('looking for modules', expectModules);
123
+
124
+ const packagesOnChainIds = []; // ids of packages with most recent versions
125
+
126
+ // UpgradeCap references to most recent version of packages. But there're no modules fields in it
127
+ // So what we do is getting list of UpgradeCap first
128
+ const queryParams = {
129
+ owner: this._suiMaster.address,
130
+ filter: { StructType: '0x2::package::UpgradeCap', },
131
+ limit: 50, // max limit is 50
132
+ options: {
133
+ showType: true,
134
+ showContent: true,
135
+ showOwner: true,
136
+ showDisplay: true,
137
+ },
138
+ };
139
+
140
+ const paginatedResponse = new SuiPaginatedResponse({
141
+ debug: this._debug,
142
+ suiMaster: this._suiMaster,
143
+ params: queryParams,
144
+ method: 'getOwnedObjects',
145
+ });
146
+
147
+ do {
148
+ await paginatedResponse.nextPage();
149
+
150
+ if (paginatedResponse.data && paginatedResponse.data.length) {
151
+ for (const object of paginatedResponse.data) {
152
+ if (object?.data?.content?.fields?.package) {
153
+ if (packagesOnChainIds.indexOf(object?.data?.content?.fields?.package) === -1) {
154
+ packagesOnChainIds.push(object?.data?.content?.fields?.package);
155
+ }
156
+ }
157
+ }
158
+ }
159
+ } while(paginatedResponse.hasNextPage);
160
+
161
+ // queriing packages out of the loop, as not sure if pagination cursor works ok with mixed calls, @todo: check
162
+ // @todo: what is the max count of ids here?
163
+ const packagesResult = await this._suiMaster._provider.multiGetObjects({
164
+ ids: packagesOnChainIds,
165
+ // only fetch the object type
166
+ options: { showType: true, showContent: true, },
167
+ });
168
+
169
+ let maxVersion = BigInt(0);
170
+ let packageIdWithMaxVersion = null;
171
+ let packagesWithOkModulesCount = 0; // just to log
172
+
173
+ // find package with highest version which has all needed modules
174
+ for (const packagesResultItem of packagesResult) {
175
+ let allNeededModules = true;
176
+ expectModules.forEach((expectModuleName)=>{
177
+ if (!packagesResultItem?.data?.content?.disassembled[expectModuleName]) {
178
+ allNeededModules = false;
179
+ }
180
+ });
181
+
182
+ const version = BigInt(packagesResultItem.data.version);
183
+
184
+ if (allNeededModules) {
185
+ packagesWithOkModulesCount++;
186
+ }
187
+
188
+ if (version > maxVersion) {
189
+ maxVersion = version;
190
+ packageIdWithMaxVersion = packagesResultItem.data.objectId;
191
+ }
192
+ }
193
+
194
+ this.log('found packages with needed modules', packagesWithOkModulesCount);
195
+ if (packageIdWithMaxVersion) {
196
+ this.log('the one with most recent Publisher version is', packageIdWithMaxVersion, 'version', maxVersion);
197
+
198
+ return packageIdWithMaxVersion;
199
+ }
200
+
201
+ return null;
202
+ }
203
+
204
+ /**
205
+ * Get published package version
206
+ * @returns Number
207
+ */
208
+ async getVersionOnChain() {
209
+ this.log('geting package version previously published on chain...');
210
+
211
+ const provider = await this._suiMaster.getProvider();
212
+
213
+ const result = await provider.getObject({
214
+ id: this.address, // normalized id
215
+ options: {
216
+ showType: true,
217
+ showContent: true,
218
+ showOwner: true,
219
+ showDisplay: true,
220
+ "showPreviousTransaction": true,
221
+ "showBcs": false,
222
+ "showStorageRebate": true
223
+ },
224
+ });
225
+
226
+ console.log('result', result);
227
+
228
+
229
+ if (result?.data?.version) {
230
+ this._publishedVersion = BigInt(result?.data?.version); // not sure, but it's string in response, so let's convert it to bigint, who knows
231
+ this._isPublished = true;
232
+ }
233
+
234
+ if (result?.data?.content?.disassembled) {
235
+ for (const key in result?.data?.content?.disassembled) {
236
+ this.attachModule(key);
237
+ // if (!this._modules[key]) {
238
+ // this._modules[key] = new SuiPackageModule({
239
+ // suiMaster: this._suiMaster,
240
+ // debug: this._debug,
241
+ // moduleName: key,
242
+ // package: this,
243
+ // });
244
+ // }
245
+ }
246
+ }
247
+
248
+ this.log('on chain version', this._publishedVersion, 'with modules', Object.keys(this._modules));
249
+
250
+ return this._publishedVersion;
251
+ }
252
+
253
+ /**
254
+ * Attach module to this package and add event listeners over it
255
+ * @param {String} moduleName
256
+ */
257
+ attachModule(moduleName) {
258
+ if (this._modules[moduleName]) {
259
+ return false;
260
+ }
261
+
262
+ this._modules[moduleName] = new SuiPackageModule({
263
+ suiMaster: this._suiMaster,
264
+ debug: this._debug,
265
+ moduleName: moduleName,
266
+ package: this,
267
+ });
268
+ this._modules[moduleName].addEventListener('added', (data)=>{
269
+ const object = data.detail;
270
+ this.emit('added', object);
271
+ });
272
+
273
+
274
+ return true;
275
+ }
276
+
277
+
278
+ /**
279
+ * UpgradeCap is capability object required to publish updates for a package.
280
+ * We are trying to find it in owned objects with this function
281
+ * @returns address of UpgradeCap for this package
282
+ */
283
+ async getUpgradeCapId() {
284
+ if (this._upgradeCap) {
285
+ return this._upgradeCap.address;
286
+ }
287
+
288
+ this.log('trying to find UpgradeCap for this package in owned objects...');
289
+
290
+ let hasNextPage = false;
291
+ let nextCursor = null;
292
+
293
+ do {
294
+ const queryParams = {
295
+ owner: this._suiMaster.address,
296
+ filter: { StructType: '0x2::package::UpgradeCap', },
297
+ limit: 50, // max limit is 50
298
+ options: {
299
+ showType: true,
300
+ showContent: true,
301
+ showOwner: true,
302
+ showDisplay: true,
303
+ },
304
+ };
305
+
306
+ if (nextCursor) {
307
+ queryParams.cursor = nextCursor;
308
+ }
309
+
310
+ const result = await this._suiMaster._provider.getOwnedObjects(queryParams);
311
+
312
+ if (result.hasNextPage && result.nextCursor) {
313
+ hasNextPage = true;
314
+ nextCursor = result.nextCursor;
315
+ } else {
316
+ hasNextPage = false;
317
+ }
318
+
319
+ for (const object of result.data) {
320
+ if (object?.data?.content?.fields?.package == this._id) {
321
+ this._upgradeCap = new SuiObject({
322
+ id: object.data.objectId,
323
+ suiMaster: this._suiMaster,
324
+ debug: this._debug,
325
+ });
326
+
327
+ this.log('found UpgradeCap', this._upgradeCap.address);
328
+
329
+ return this._upgradeCap.address;
330
+ }
331
+ }
332
+ } while(hasNextPage && !this._upgradeCap);
333
+
334
+ this.log('no UpgradeCap for this package found. Are you sure you work with most recent version of the package?');
335
+
336
+ return null;
337
+ }
338
+
339
+ async storeInfoFromPublishResult(result) {
340
+
341
+ if (result && result.objectChanges && result.objectChanges.length) {
342
+ for (const objectChange of result.objectChanges) {
343
+ if (objectChange.type === 'published' && objectChange.packageId) {
344
+ this._id = sui.normalizeSuiAddress(objectChange.packageId);
345
+ this._isPublished = true;
346
+
347
+ if (objectChange.version) {
348
+ this._publishedVersion = BigInt(objectChange.version);
349
+ }
350
+
351
+ if (objectChange.modules) {
352
+ for (const module of objectChange.modules) {
353
+ this.attachModule(module);
354
+ }
355
+ }
356
+ }
357
+
358
+ if (objectChange.type === 'created' && objectChange.objectType.indexOf('::package::UpgradeCap') !== -1) {
359
+ this._upgradeCapId = objectChange.objectId;
360
+ this.log('UpgradeCap', this._upgradeCapId);
361
+ }
362
+ }
363
+
364
+ // now as we have modules stored, we can try to push objects to them
365
+ for (const objectChange of result.objectChanges) {
366
+ if (objectChange.objectId && objectChange.objectType && objectChange.type && (objectChange.type == 'created' || objectChange.type == 'mutated')) {
367
+ // : not sure if it's good decision, but lets add objects to all modules we published
368
+ for (const moduleName in this._modules) {
369
+ const object = this._modules[moduleName].pushObject(objectChange.objectId);
370
+ if (object) {
371
+ object.tryToFillDataFromObjectChange(objectChange);
372
+ }
373
+ }
374
+ }
375
+ }
376
+
377
+ this.log('got results:', this.address, 'version', this._publishedVersion, 'with modules', Object.keys(this._modules));
378
+
379
+ return true;
380
+ } else {
381
+ this.log('nothing is found in publish result. storing old values');
382
+
383
+ return false;
384
+ }
385
+ }
386
+
387
+ async publish() {
388
+ if (!this._isBuilt) {
389
+ await this.build();
390
+ }
391
+ if (this.address) {
392
+ throw new Error('already published. Maybe you need to upgrade() it?');
393
+ }
394
+
395
+ this.log('publishing package...');
396
+
397
+ const tx = new sui.TransactionBlock();
398
+ const [upgradeCap] = tx.publish({
399
+ modules: this._builtModules,
400
+ dependencies: this._builtDependencies,
401
+ });
402
+
403
+ tx.transferObjects([upgradeCap], tx.pure(this._suiMaster.address));
404
+
405
+ const result = await this._suiMaster.signer.signAndExecuteTransactionBlock({
406
+ transactionBlock: tx,
407
+ requestType: 'WaitForLocalExecution',
408
+ options: {
409
+ "showEffects": true, // @todo: remove?
410
+ "showEvents": true, // @todo: remove?
411
+ "showObjectChanges": true,
412
+ },
413
+ });
414
+
415
+ const success = await this.storeInfoFromPublishResult(result);
416
+
417
+ if (success) {
418
+ this.log('published');
419
+ }
420
+
421
+ return this.address;
422
+ }
423
+
424
+ async upgrade() {
425
+ await this.checkOnChainIfNeeded();
426
+
427
+ if (!this._isBuilt) {
428
+ await this.build();
429
+ }
430
+
431
+ this.log('upgrading package...');
432
+
433
+ const tx = new sui.TransactionBlock();
434
+
435
+ const cap = tx.object(await this.getUpgradeCapId());
436
+ // export enum UpgradePolicy {
437
+ // COMPATIBLE = 0,
438
+ // ADDITIVE = 128,
439
+ // DEP_ONLY = 192,
440
+ // }
441
+ const UpgradePolicyCOMPATIBLE = 0;
442
+
443
+ const ticket = tx.moveCall({
444
+ target: '0x2::package::authorize_upgrade',
445
+ arguments: [cap, tx.pure(UpgradePolicyCOMPATIBLE), tx.pure(this._builtDigest)],
446
+ });
447
+
448
+ const receipt = tx.upgrade({
449
+ modules: this._builtModules,
450
+ dependencies: this._builtDependencies,
451
+ packageId: this.address, // normalized id
452
+ ticket,
453
+ });
454
+
455
+ tx.moveCall({
456
+ target: '0x2::package::commit_upgrade',
457
+ arguments: [cap, receipt],
458
+ });
459
+
460
+ const result = await this._suiMaster.signer.signAndExecuteTransactionBlock({
461
+ transactionBlock: tx,
462
+ options: {
463
+ showEffects: true,
464
+ showObjectChanges: true,
465
+ },
466
+ });
467
+
468
+ const success = await this.storeInfoFromPublishResult(result);
469
+
470
+ if (success) {
471
+ this.log('upgraded');
472
+ }
473
+
474
+ return this.address;
475
+ }
476
+
477
+ /**
478
+ * Build a Move project using `sui move build`
479
+ * @returns Boolean true on success
480
+ */
481
+ async build() {
482
+ this.log('builing a package...');
483
+
484
+ const path = this._path;
485
+
486
+ if (!path) {
487
+ throw new Error('Cant build a package with no path defined');
488
+ }
489
+
490
+ const buildResult = await SuiCliCommands.exec(`sui move build --dump-bytecode-as-base64 --path ${path}`);
491
+ const { modules, dependencies, digest } = JSON.parse(buildResult);
492
+
493
+ this._builtModules = modules;
494
+ this._builtDependencies = dependencies;
495
+ this._builtDigest = digest;
496
+
497
+ this._isBuilt = true;
498
+
499
+ this.log('package built');
500
+
501
+ return true;
502
+ }
503
+
504
+ /**
505
+ * Get list of expected modules from local package path
506
+ * @returns array of module names
507
+ */
508
+ async getModulesNamesFromBuild() {
509
+ this.log('tring to get modules names from local package path...');
510
+
511
+ try {
512
+ return SuiCliCommands.getModulesNamesFromPackagePath(this._path);
513
+ } catch (e) {
514
+ this.log(e);
515
+ throw new Error('can not get modules names from local package path');
516
+ }
517
+ }
518
+ }
519
+
520
+ module.exports = SuiPackage;