@solana-mobile/dapp-store-cli 0.8.2 → 0.9.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.
package/lib/CliSetup.js CHANGED
@@ -126,7 +126,7 @@ import { Command } from "commander";
126
126
  import { validateCommand } from "./commands/index.js";
127
127
  import { createAppCommand, createPublisherCommand, createReleaseCommand } from "./commands/create/index.js";
128
128
  import { publishRemoveCommand, publishSubmitCommand, publishSupportCommand, publishUpdateCommand } from "./commands/publish/index.js";
129
- import { checkForSelfUpdate, checkSubmissionNetwork, Constants, dryRunSuccessMessage, generateNetworkSuffix, parseKeypair, showMessage } from "./CliUtils.js";
129
+ import { checkForSelfUpdate, checkSubmissionNetwork, Constants, dryRunSuccessMessage, generateNetworkSuffix, parseKeypair, showMessage, showNetworkWarningIfApplicable } from "./CliUtils.js";
130
130
  import { LAMPORTS_PER_SOL } from "@solana/web3.js";
131
131
  import * as dotenv from "dotenv";
132
132
  import { initScaffold } from "./commands/scaffolding/index.js";
@@ -153,6 +153,7 @@ function resolveBuildToolsPath(buildToolsPath) {
153
153
  * This method should be updated with each new release of the CLI, and just do nothing when there isn't anything to report
154
154
  */ function latestReleaseMessage() {
155
155
  var messages = [
156
+ "- App details page now supports video files. (mp4 file format only and minimum resolution 720p)",
156
157
  "- priority fee has been updated to ".concat(Constants.DEFAULT_PRIORITY_FEE, " lamports = ").concat(Constants.DEFAULT_PRIORITY_FEE / LAMPORTS_PER_SOL, ' SOL. To adjust this value use param "-p" or "--priority-fee-lamports"'),
157
158
  "- At least 4 screenshots are now required to update or release a new app",
158
159
  "- App icons should be exactly 512x512."
@@ -243,6 +244,7 @@ export var createPublisherCliCmd = createCliCmd.command("publisher").description
243
244
  return _ts_generator(this, function(_state) {
244
245
  switch(_state.label){
245
246
  case 0:
247
+ showNetworkWarningIfApplicable(url);
246
248
  latestReleaseMessage();
247
249
  return [
248
250
  4,
@@ -310,6 +312,7 @@ export var createAppCliCmd = createCliCmd.command("app").description("Create a a
310
312
  return _ts_generator(this, function(_state) {
311
313
  switch(_state.label){
312
314
  case 0:
315
+ showNetworkWarningIfApplicable(url);
313
316
  latestReleaseMessage();
314
317
  return [
315
318
  4,
@@ -387,6 +390,7 @@ export var createReleaseCliCmd = createCliCmd.command("release").description("Cr
387
390
  return _ts_generator(this, function(_state) {
388
391
  switch(_state.label){
389
392
  case 0:
393
+ showNetworkWarningIfApplicable(url);
390
394
  latestReleaseMessage();
391
395
  return [
392
396
  4,
package/lib/CliUtils.js CHANGED
@@ -143,7 +143,7 @@ function _ts_generator(thisArg, body) {
143
143
  import fs from "fs";
144
144
  import { Keypair, PublicKey } from "@solana/web3.js";
145
145
  import debugModule from "debug";
146
- import { BundlrStorageDriver, keypairIdentity, Metaplex } from "@metaplex-foundation/js";
146
+ import { IrysStorageDriver, keypairIdentity, Metaplex } from "@metaplex-foundation/js";
147
147
  import updateNotifier from "update-notifier";
148
148
  import cliPackage from "./package.json" assert {
149
149
  type: "json"
@@ -159,7 +159,7 @@ export var Constants = function Constants() {
159
159
  "use strict";
160
160
  _class_call_check(this, Constants);
161
161
  };
162
- _define_property(Constants, "CLI_VERSION", "0.8.2");
162
+ _define_property(Constants, "CLI_VERSION", "0.9.0");
163
163
  _define_property(Constants, "CONFIG_FILE_NAME", "config.yaml");
164
164
  _define_property(Constants, "DEFAULT_RPC_DEVNET", "https://api.devnet.solana.com");
165
165
  _define_property(Constants, "DEFAULT_PRIORITY_FEE", 500000);
@@ -262,6 +262,13 @@ export var generateNetworkSuffix = function(rpcUrl) {
262
262
  export var dryRunSuccessMessage = function() {
263
263
  showMessage("Dry run", "Dry run was successful", "standard");
264
264
  };
265
+ export var showNetworkWarningIfApplicable = function(rpcUrl) {
266
+ if (isDevnet(rpcUrl)) {
267
+ showMessage("Devnet Mode", "Running on Devnet", "warning");
268
+ } else if (isTestnet(rpcUrl)) {
269
+ showMessage("Testnet Mode", "Running on Testnet", "warning");
270
+ }
271
+ };
265
272
  export var showMessage = function() {
266
273
  var titleMessage = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : "", contentMessage = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : "", type = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : "standard";
267
274
  var color = "cyan";
@@ -300,13 +307,13 @@ export var getMetaplexInstance = function(connection, keypair) {
300
307
  var bucketPlugin = awsStorage(awsClient, s3Mgr.s3Config.bucketName);
301
308
  metaplex.use(bucketPlugin);
302
309
  } else {
303
- var bundlrStorageDriver = isDevnet ? new BundlrStorageDriver(metaplex, {
310
+ var irysStorageDriver = isDevnet ? new IrysStorageDriver(metaplex, {
304
311
  address: "https://turbo.ardrive.dev",
305
312
  providerUrl: Constants.DEFAULT_RPC_DEVNET
306
- }) : new BundlrStorageDriver(metaplex, {
313
+ }) : new IrysStorageDriver(metaplex, {
307
314
  address: "https://turbo.ardrive.io"
308
315
  });
309
- metaplex.storage().setDriver(bundlrStorageDriver);
316
+ metaplex.storage().setDriver(irysStorageDriver);
310
317
  }
311
318
  metaplex.storage().setDriver(new CachedStorageDriver(metaplex.storage().driver(), {
312
319
  assetManifestPath: isDevnet ? "./.asset-manifest-devnet.json" : "./.asset-manifest.json"
@@ -153,7 +153,6 @@ export var validateCommand = function() {
153
153
  }, metaplexFileReplacer, 2));
154
154
  try {
155
155
  validatePublisher(publisherJson);
156
- console.info("Publisher JSON valid!");
157
156
  } catch (e) {
158
157
  ;
159
158
  errorMsg = (_e_message = e === null || e === void 0 ? void 0 : e.message) !== null && _e_message !== void 0 ? _e_message : "";
@@ -172,7 +171,6 @@ export var validateCommand = function() {
172
171
  }, metaplexFileReplacer, 2));
173
172
  try {
174
173
  validateApp(appJson);
175
- console.info("App JSON valid!");
176
174
  } catch (e) {
177
175
  ;
178
176
  errorMsg1 = (_e_message1 = e === null || e === void 0 ? void 0 : e.message) !== null && _e_message1 !== void 0 ? _e_message1 : "";
@@ -201,7 +199,6 @@ export var validateCommand = function() {
201
199
  debug("releaseJson=", objStringified);
202
200
  try {
203
201
  validateRelease(JSON.parse(objStringified));
204
- console.info("Release JSON valid!");
205
202
  } catch (e) {
206
203
  ;
207
204
  errorMsg2 = (_e_message2 = e === null || e === void 0 ? void 0 : e.message) !== null && _e_message2 !== void 0 ? _e_message2 : "";
@@ -219,6 +219,7 @@ import { Constants, showMessage } from "../CliUtils.js";
219
219
  import util from "util";
220
220
  import { imageSize } from "image-size";
221
221
  import { exec } from "child_process";
222
+ import getVideoDimensions from "get-video-dimensions";
222
223
  var runImgSize = util.promisify(imageSize);
223
224
  var runExec = util.promisify(exec);
224
225
  var AaptPrefixes = {
@@ -265,7 +266,7 @@ export var loadPublishDetails = function() {
265
266
  }();
266
267
  export var loadPublishDetailsWithChecks = function() {
267
268
  var _ref = _async_to_generator(function() {
268
- var buildToolsDir, _config_publisher_media_find, _config_publisher_media, _config_app_media_find, _config_app_media, _config_release_media_find, _config_release_media, _config_release_media1, config, apkEntry, apkPath, _, publisherIcon, iconPath, iconBuffer, appIcon, iconPath1, iconBuffer1, releaseIcon, iconPath2, previewMediaFiles, googlePkg, pkgCompare;
269
+ var buildToolsDir, _config_publisher_media_find, _config_publisher_media, _config_app_media_find, _config_app_media, _config_release_media_find, _config_release_media, _config_release_media1, _config_release_media2, config, apkEntry, apkPath, _, publisherIcon, iconPath, iconBuffer, appIcon, iconPath1, iconBuffer1, releaseIcon, iconPath2, screenshots, _iteratorNormalCompletion, _didIteratorError, _iteratorError, _iterator, _step, item, mediaPath, err, videos, _iteratorNormalCompletion1, _didIteratorError1, _iteratorError1, _iterator1, _step1, video, mediaPath1, err, googlePkg, pkgCompare;
269
270
  var _arguments = arguments;
270
271
  return _ts_generator(this, function(_state) {
271
272
  switch(_state.label){
@@ -363,16 +364,145 @@ export var loadPublishDetailsWithChecks = function() {
363
364
  throw new Error("Please specify at least one media entry of type icon in your configuration file");
364
365
  }
365
366
  config.release.media.forEach(function(item) {
366
- var imagePath = path.join(process.cwd(), item.uri);
367
- if (!fs.existsSync(imagePath) || !checkImageExtension(imagePath)) {
368
- throw new Error("Invalid media path or file type: ".concat(item.uri, ". Please ensure the file is a jpeg, png, or webp file."));
367
+ var mediaPath = path.join(process.cwd(), item.uri);
368
+ if (!fs.existsSync(mediaPath)) {
369
+ throw new Error("File doesnt exist: ".concat(item.uri, "."));
370
+ }
371
+ if (item.purpose == "screenshot" && !checkImageExtension(mediaPath)) {
372
+ throw new Error("Please ensure the file ".concat(item.uri, " is a jpeg, png, or webp file."));
373
+ }
374
+ if (item.purpose == "video" && !checkVideoExtension(mediaPath)) {
375
+ throw new Error("Please ensure the file ".concat(item.uri, " is a mp4."));
369
376
  }
370
377
  });
371
- previewMediaFiles = (_config_release_media1 = config.release.media) === null || _config_release_media1 === void 0 ? void 0 : _config_release_media1.filter(function(asset) {
372
- return asset.purpose === "screenshot" || asset.purpose === "video";
378
+ screenshots = (_config_release_media1 = config.release.media) === null || _config_release_media1 === void 0 ? void 0 : _config_release_media1.filter(function(asset) {
379
+ return asset.purpose === "screenshot";
373
380
  });
374
- if (previewMediaFiles.length < 4) {
375
- showMessage("Preview media requirements changing in version 0.9.0", "At least 4 screenshots or videos are required for publishing a new release. Found only ".concat(previewMediaFiles.length), "warning");
381
+ _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
382
+ _state.label = 12;
383
+ case 12:
384
+ _state.trys.push([
385
+ 12,
386
+ 17,
387
+ 18,
388
+ 19
389
+ ]);
390
+ _iterator = screenshots[Symbol.iterator]();
391
+ _state.label = 13;
392
+ case 13:
393
+ if (!!(_iteratorNormalCompletion = (_step = _iterator.next()).done)) return [
394
+ 3,
395
+ 16
396
+ ];
397
+ item = _step.value;
398
+ mediaPath = path.join(process.cwd(), item.uri);
399
+ return [
400
+ 4,
401
+ checkScreenshotSize(mediaPath)
402
+ ];
403
+ case 14:
404
+ if (_state.sent()) {
405
+ throw new Error("Screenshot ".concat(mediaPath, " must be at least 1080px in width and height."));
406
+ }
407
+ _state.label = 15;
408
+ case 15:
409
+ _iteratorNormalCompletion = true;
410
+ return [
411
+ 3,
412
+ 13
413
+ ];
414
+ case 16:
415
+ return [
416
+ 3,
417
+ 19
418
+ ];
419
+ case 17:
420
+ err = _state.sent();
421
+ _didIteratorError = true;
422
+ _iteratorError = err;
423
+ return [
424
+ 3,
425
+ 19
426
+ ];
427
+ case 18:
428
+ try {
429
+ if (!_iteratorNormalCompletion && _iterator.return != null) {
430
+ _iterator.return();
431
+ }
432
+ } finally{
433
+ if (_didIteratorError) {
434
+ throw _iteratorError;
435
+ }
436
+ }
437
+ return [
438
+ 7
439
+ ];
440
+ case 19:
441
+ videos = (_config_release_media2 = config.release.media) === null || _config_release_media2 === void 0 ? void 0 : _config_release_media2.filter(function(asset) {
442
+ return asset.purpose === "video";
443
+ });
444
+ _iteratorNormalCompletion1 = true, _didIteratorError1 = false, _iteratorError1 = undefined;
445
+ _state.label = 20;
446
+ case 20:
447
+ _state.trys.push([
448
+ 20,
449
+ 25,
450
+ 26,
451
+ 27
452
+ ]);
453
+ _iterator1 = videos[Symbol.iterator]();
454
+ _state.label = 21;
455
+ case 21:
456
+ if (!!(_iteratorNormalCompletion1 = (_step1 = _iterator1.next()).done)) return [
457
+ 3,
458
+ 24
459
+ ];
460
+ video = _step1.value;
461
+ mediaPath1 = path.join(process.cwd(), video.uri);
462
+ return [
463
+ 4,
464
+ checkVideoSize(mediaPath1)
465
+ ];
466
+ case 22:
467
+ if (_state.sent()) {
468
+ throw new Error("Video ".concat(mediaPath1, " must be at least 720px in width and height."));
469
+ }
470
+ _state.label = 23;
471
+ case 23:
472
+ _iteratorNormalCompletion1 = true;
473
+ return [
474
+ 3,
475
+ 21
476
+ ];
477
+ case 24:
478
+ return [
479
+ 3,
480
+ 27
481
+ ];
482
+ case 25:
483
+ err = _state.sent();
484
+ _didIteratorError1 = true;
485
+ _iteratorError1 = err;
486
+ return [
487
+ 3,
488
+ 27
489
+ ];
490
+ case 26:
491
+ try {
492
+ if (!_iteratorNormalCompletion1 && _iterator1.return != null) {
493
+ _iterator1.return();
494
+ }
495
+ } finally{
496
+ if (_didIteratorError1) {
497
+ throw _iteratorError1;
498
+ }
499
+ }
500
+ return [
501
+ 7
502
+ ];
503
+ case 27:
504
+ if (screenshots.length + videos.length < 4) {
505
+ throw new Error("At least 4 screenshots or videos are required for publishing a new release. Found only ".concat(screenshots.length + videos.length));
376
506
  }
377
507
  validateLocalizableResources(config);
378
508
  googlePkg = config.solana_mobile_dapp_publisher_portal.google_store_package;
@@ -423,6 +553,10 @@ var checkImageExtension = function(uri) {
423
553
  var fileExt = path.extname(uri).toLowerCase();
424
554
  return fileExt == ".png" || fileExt == ".jpg" || fileExt == ".jpeg" || fileExt == ".webp";
425
555
  };
556
+ var checkVideoExtension = function(uri) {
557
+ var fileExt = path.extname(uri).toLowerCase();
558
+ return fileExt == ".mp4";
559
+ };
426
560
  /**
427
561
  * We need to pre-check some things in the localized resources before we move forward
428
562
  */ var validateLocalizableResources = function(config) {
@@ -468,6 +602,52 @@ var checkIconDimensions = function() {
468
602
  return _ref.apply(this, arguments);
469
603
  };
470
604
  }();
605
+ var checkScreenshotSize = function() {
606
+ var _ref = _async_to_generator(function(imagePath) {
607
+ var size, _size_width, _size_height;
608
+ return _ts_generator(this, function(_state) {
609
+ switch(_state.label){
610
+ case 0:
611
+ return [
612
+ 4,
613
+ runImgSize(imagePath)
614
+ ];
615
+ case 1:
616
+ size = _state.sent();
617
+ return [
618
+ 2,
619
+ ((_size_width = size === null || size === void 0 ? void 0 : size.width) !== null && _size_width !== void 0 ? _size_width : 0) < 1080 || ((_size_height = size === null || size === void 0 ? void 0 : size.height) !== null && _size_height !== void 0 ? _size_height : 0) < 1080
620
+ ];
621
+ }
622
+ });
623
+ });
624
+ return function checkScreenshotSize(imagePath) {
625
+ return _ref.apply(this, arguments);
626
+ };
627
+ }();
628
+ var checkVideoSize = function() {
629
+ var _ref = _async_to_generator(function(imagePath) {
630
+ var size, _size_width, _size_height;
631
+ return _ts_generator(this, function(_state) {
632
+ switch(_state.label){
633
+ case 0:
634
+ return [
635
+ 4,
636
+ getVideoDimensions(imagePath)
637
+ ];
638
+ case 1:
639
+ size = _state.sent();
640
+ return [
641
+ 2,
642
+ ((_size_width = size === null || size === void 0 ? void 0 : size.width) !== null && _size_width !== void 0 ? _size_width : 0) < 720 || ((_size_height = size === null || size === void 0 ? void 0 : size.height) !== null && _size_height !== void 0 ? _size_height : 0) < 720
643
+ ];
644
+ }
645
+ });
646
+ });
647
+ return function checkVideoSize(imagePath) {
648
+ return _ref.apply(this, arguments);
649
+ };
650
+ }();
471
651
  var getAndroidDetails = function() {
472
652
  var _ref = _async_to_generator(function(aaptDir, apkPath) {
473
653
  var stdout, appPackage, versionCode, versionName, minSdk, permissions, locales, isDebuggable, _locales_values, localeArray, localesSrc, _appPackage_, _minSdk_, _versionCode_, _versionName_, _tmp, e;
@@ -517,6 +697,7 @@ var getAndroidDetails = function() {
517
697
  if (localeArray.length >= 60) {
518
698
  showMessage("The bundle apk claims supports for following locales", "Claim for supported locales::\n" + localeArray + "\nIf this release does not support all these locales the release may be rejected" + "\nSee details at https://developer.android.com/guide/topics/resources/multilingual-support#design for configuring the supported locales", "warning");
519
699
  }
700
+ checkAbis(apkPath);
520
701
  _tmp = {
521
702
  android_package: (_appPackage_ = appPackage === null || appPackage === void 0 ? void 0 : appPackage[1]) !== null && _appPackage_ !== void 0 ? _appPackage_ : "",
522
703
  min_sdk: parseInt((_minSdk_ = minSdk === null || minSdk === void 0 ? void 0 : minSdk[1]) !== null && _minSdk_ !== void 0 ? _minSdk_ : "0", 10),
@@ -554,6 +735,70 @@ var getAndroidDetails = function() {
554
735
  return _ref.apply(this, arguments);
555
736
  };
556
737
  }();
738
+ var checkAbis = function() {
739
+ var _ref = _async_to_generator(function(apkPath) {
740
+ var stdout, amV7libs, x86libs, x8664libs, messages, e;
741
+ return _ts_generator(this, function(_state) {
742
+ switch(_state.label){
743
+ case 0:
744
+ _state.trys.push([
745
+ 0,
746
+ 2,
747
+ ,
748
+ 3
749
+ ]);
750
+ return [
751
+ 4,
752
+ runExec("zipinfo -s ".concat(apkPath, " | grep .so$"))
753
+ ];
754
+ case 1:
755
+ stdout = _state.sent().stdout;
756
+ amV7libs = _to_consumable_array(stdout.matchAll(/lib\/armeabi-v7a\/(.*)/g)).flatMap(function(permission) {
757
+ return permission[1];
758
+ });
759
+ x86libs = _to_consumable_array(stdout.matchAll(/lib\/x86\/(.*)/g)).flatMap(function(permission) {
760
+ return permission[1];
761
+ });
762
+ x8664libs = _to_consumable_array(stdout.matchAll(/lib\/x86_64\/(.*)/g)).flatMap(function(permission) {
763
+ return permission[1];
764
+ });
765
+ if (amV7libs.length > 0 || x86libs.length > 0 || x8664libs.length > 0) {
766
+ messages = [
767
+ "Solana dApp Store only supports arm64-v8a abi.",
768
+ "Your apk file contains following unsupported abis"
769
+ ].concat(_to_consumable_array(amV7libs.length > 0 ? [
770
+ "\narmeabi-v7a:\n" + amV7libs
771
+ ] : []), _to_consumable_array(x86libs.length > 0 ? [
772
+ "\nx86:\n" + x86libs
773
+ ] : []), _to_consumable_array(x8664libs.length > 0 ? [
774
+ "\nx86_64:\n" + x8664libs
775
+ ] : []), [
776
+ "\n\nAlthough your app works fine on Saga, these library files are unused and increase the size of apk file making the download and update time longer for your app.",
777
+ "\n\nSee https://developer.android.com/games/optimize/64-bit#build-with-64-bit for how to optimize your app."
778
+ ]).join("\n");
779
+ showMessage("Unsupported files found in apk", messages, "warning");
780
+ }
781
+ return [
782
+ 3,
783
+ 3
784
+ ];
785
+ case 2:
786
+ e = _state.sent();
787
+ return [
788
+ 3,
789
+ 3
790
+ ];
791
+ case 3:
792
+ return [
793
+ 2
794
+ ];
795
+ }
796
+ });
797
+ });
798
+ return function checkAbis(apkPath) {
799
+ return _ref.apply(this, arguments);
800
+ };
801
+ }();
557
802
  export var extractCertFingerprint = function() {
558
803
  var _ref = _async_to_generator(function(aaptDir, apkPath) {
559
804
  var stdout, regex, match;
package/lib/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solana-mobile/dapp-store-cli",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -34,9 +34,9 @@
34
34
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
35
35
  },
36
36
  "devDependencies": {
37
- "@metaplex-foundation/js": "0.18.3",
38
37
  "@jest/globals": "^29.5.0",
39
38
  "@jest/types": "^29.5.0",
39
+ "@metaplex-foundation/js": "0.20.0",
40
40
  "@swc/jest": "^0.2.26",
41
41
  "@types/commander": "^2.12.2",
42
42
  "@types/debug": "^4.1.7",
@@ -52,8 +52,8 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@aws-sdk/client-s3": "^3.321.1",
55
- "@metaplex-foundation/js-plugin-aws": "^0.18.3",
56
- "@solana-mobile/dapp-store-publishing-tools": "workspace:0.8.2",
55
+ "@metaplex-foundation/js-plugin-aws": "^0.20.0",
56
+ "@solana-mobile/dapp-store-publishing-tools": "workspace:0.9.0",
57
57
  "@solana/web3.js": "1.68.0",
58
58
  "@types/semver": "^7.3.13",
59
59
  "ajv": "^8.11.0",
@@ -64,6 +64,7 @@
64
64
  "dotenv": "^16.0.3",
65
65
  "esm": "^3.2.25",
66
66
  "generate-schema": "^2.6.0",
67
+ "get-video-dimensions": "^1.0.0",
67
68
  "image-size": "^1.0.2",
68
69
  "js-yaml": "^4.1.0",
69
70
  "semver": "^7.3.8",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solana-mobile/dapp-store-cli",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -27,9 +27,9 @@
27
27
  "access": "public"
28
28
  },
29
29
  "devDependencies": {
30
- "@metaplex-foundation/js": "0.18.3",
31
30
  "@jest/globals": "^29.5.0",
32
31
  "@jest/types": "^29.5.0",
32
+ "@metaplex-foundation/js": "0.20.0",
33
33
  "@swc/jest": "^0.2.26",
34
34
  "@types/commander": "^2.12.2",
35
35
  "@types/debug": "^4.1.7",
@@ -45,8 +45,8 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@aws-sdk/client-s3": "^3.321.1",
48
- "@metaplex-foundation/js-plugin-aws": "^0.18.3",
49
- "@solana-mobile/dapp-store-publishing-tools": "0.8.2",
48
+ "@metaplex-foundation/js-plugin-aws": "^0.20.0",
49
+ "@solana-mobile/dapp-store-publishing-tools": "0.9.0",
50
50
  "@solana/web3.js": "1.68.0",
51
51
  "@types/semver": "^7.3.13",
52
52
  "ajv": "^8.11.0",
@@ -57,6 +57,7 @@
57
57
  "dotenv": "^16.0.3",
58
58
  "esm": "^3.2.25",
59
59
  "generate-schema": "^2.6.0",
60
+ "get-video-dimensions": "^1.0.0",
60
61
  "image-size": "^1.0.2",
61
62
  "js-yaml": "^4.1.0",
62
63
  "semver": "^7.3.8",
package/src/CliSetup.ts CHANGED
@@ -14,7 +14,8 @@ import {
14
14
  dryRunSuccessMessage,
15
15
  generateNetworkSuffix,
16
16
  parseKeypair,
17
- showMessage
17
+ showMessage,
18
+ showNetworkWarningIfApplicable
18
19
  } from "./CliUtils.js";
19
20
  import { LAMPORTS_PER_SOL } from "@solana/web3.js"
20
21
  import * as dotenv from "dotenv";
@@ -49,6 +50,7 @@ function resolveBuildToolsPath(buildToolsPath: string | undefined) {
49
50
  */
50
51
  function latestReleaseMessage() {
51
52
  const messages = [
53
+ `- App details page now supports video files. (mp4 file format only and minimum resolution 720p)`,
52
54
  `- priority fee has been updated to ${Constants.DEFAULT_PRIORITY_FEE} lamports = ${Constants.DEFAULT_PRIORITY_FEE / LAMPORTS_PER_SOL} SOL. To adjust this value use param "-p" or "--priority-fee-lamports"`,
53
55
  `- At least 4 screenshots are now required to update or release a new app`,
54
56
  `- App icons should be exactly 512x512.`
@@ -104,6 +106,7 @@ export const createPublisherCliCmd = createCliCmd
104
106
  .option("-p, --priority-fee-lamports <priority-fee-lamports>", "Priority Fee lamports")
105
107
  .action(async ({ keypair, url, dryRun, storageConfig, priorityFeeLamports }) => {
106
108
  await tryWithErrorMessage(async () => {
109
+ showNetworkWarningIfApplicable(url)
107
110
  latestReleaseMessage();
108
111
  await checkForSelfUpdate();
109
112
 
@@ -141,6 +144,7 @@ export const createAppCliCmd = createCliCmd
141
144
  .option("-p, --priority-fee-lamports <priority-fee-lamports>", "Priority Fee lamports")
142
145
  .action(async ({ publisherMintAddress, keypair, url, dryRun, storageConfig, priorityFeeLamports }) => {
143
146
  await tryWithErrorMessage(async () => {
147
+ showNetworkWarningIfApplicable(url)
144
148
  latestReleaseMessage();
145
149
  await checkForSelfUpdate();
146
150
 
@@ -194,6 +198,7 @@ export const createReleaseCliCmd = createCliCmd
194
198
  .option("-p, --priority-fee-lamports <priority-fee-lamports>", "Priority Fee lamports")
195
199
  .action(async ({ appMintAddress, keypair, url, dryRun, buildToolsPath, storageConfig, priorityFeeLamports }) => {
196
200
  await tryWithErrorMessage(async () => {
201
+ showNetworkWarningIfApplicable(url)
197
202
  latestReleaseMessage();
198
203
  await checkForSelfUpdate();
199
204
 
package/src/CliUtils.ts CHANGED
@@ -3,7 +3,7 @@ import type { Connection } from "@solana/web3.js";
3
3
  import { Keypair, PublicKey } from "@solana/web3.js";
4
4
  import debugModule from "debug";
5
5
  import {
6
- BundlrStorageDriver,
6
+ IrysStorageDriver,
7
7
  keypairIdentity,
8
8
  Metaplex,
9
9
  } from "@metaplex-foundation/js";
@@ -18,7 +18,7 @@ import { awsStorage } from "@metaplex-foundation/js-plugin-aws";
18
18
  import { S3StorageManager } from "./config/index.js";
19
19
 
20
20
  export class Constants {
21
- static CLI_VERSION = "0.8.2";
21
+ static CLI_VERSION = "0.9.0";
22
22
  static CONFIG_FILE_NAME = "config.yaml";
23
23
  static DEFAULT_RPC_DEVNET = "https://api.devnet.solana.com";
24
24
  static DEFAULT_PRIORITY_FEE = 500000;
@@ -117,6 +117,14 @@ export const dryRunSuccessMessage = () => {
117
117
  showMessage("Dry run", "Dry run was successful", "standard")
118
118
  }
119
119
 
120
+ export const showNetworkWarningIfApplicable = (rpcUrl: string) => {
121
+ if (isDevnet(rpcUrl)) {
122
+ showMessage("Devnet Mode", "Running on Devnet", "warning")
123
+ } else if (isTestnet(rpcUrl)) {
124
+ showMessage("Testnet Mode", "Running on Testnet", "warning")
125
+ }
126
+ }
127
+
120
128
  export const showMessage = (
121
129
  titleMessage = "",
122
130
  contentMessage = "",
@@ -167,16 +175,16 @@ export const getMetaplexInstance = (
167
175
  const bucketPlugin = awsStorage(awsClient, s3Mgr.s3Config.bucketName);
168
176
  metaplex.use(bucketPlugin);
169
177
  } else {
170
- const bundlrStorageDriver = isDevnet
171
- ? new BundlrStorageDriver(metaplex, {
178
+ const irysStorageDriver = isDevnet
179
+ ? new IrysStorageDriver(metaplex, {
172
180
  address: "https://turbo.ardrive.dev",
173
181
  providerUrl: Constants.DEFAULT_RPC_DEVNET,
174
182
  })
175
- : new BundlrStorageDriver(metaplex, {
183
+ : new IrysStorageDriver(metaplex, {
176
184
  address: "https://turbo.ardrive.io",
177
185
  });
178
186
 
179
- metaplex.storage().setDriver(bundlrStorageDriver);
187
+ metaplex.storage().setDriver(irysStorageDriver);
180
188
  }
181
189
 
182
190
  metaplex.storage().setDriver(
@@ -36,7 +36,6 @@ export const validateCommand = async ({
36
36
 
37
37
  try {
38
38
  validatePublisher(publisherJson);
39
- console.info(`Publisher JSON valid!`);
40
39
  } catch (e) {
41
40
  const errorMsg = (e as Error | null)?.message ?? "";
42
41
  showMessage(
@@ -55,7 +54,6 @@ export const validateCommand = async ({
55
54
 
56
55
  try {
57
56
  validateApp(appJson);
58
- console.info(`App JSON valid!`);
59
57
  } catch (e) {
60
58
  const errorMsg = (e as Error | null)?.message ?? "";
61
59
  showMessage(
@@ -86,7 +84,6 @@ export const validateCommand = async ({
86
84
 
87
85
  try {
88
86
  validateRelease(JSON.parse(objStringified));
89
- console.info(`Release JSON valid!`);
90
87
  } catch (e) {
91
88
  const errorMsg = (e as Error | null)?.message ?? "";
92
89
  showMessage(
@@ -18,6 +18,7 @@ import { Constants, showMessage } from "../CliUtils.js";
18
18
  import util from "util";
19
19
  import { imageSize } from "image-size";
20
20
  import { exec } from "child_process";
21
+ import getVideoDimensions from "get-video-dimensions";
21
22
 
22
23
  const runImgSize = util.promisify(imageSize);
23
24
  const runExec = util.promisify(exec);
@@ -125,23 +126,45 @@ export const loadPublishDetailsWithChecks = async (
125
126
  }
126
127
 
127
128
  config.release.media.forEach((item: PublishDetails["release"]["media"][0]) => {
128
- const imagePath = path.join(process.cwd(), item.uri);
129
- if (!fs.existsSync(imagePath) || !checkImageExtension(imagePath)) {
130
- throw new Error(`Invalid media path or file type: ${item.uri}. Please ensure the file is a jpeg, png, or webp file.`);
129
+ const mediaPath = path.join(process.cwd(), item.uri);
130
+ if (!fs.existsSync(mediaPath)) {
131
+ throw new Error(`File doesnt exist: ${item.uri}.`)
132
+ }
133
+
134
+ if (item.purpose == "screenshot" && !checkImageExtension(mediaPath)) {
135
+ throw new Error(`Please ensure the file ${item.uri} is a jpeg, png, or webp file.`)
136
+ }
137
+
138
+ if (item.purpose == "video" && !checkVideoExtension(mediaPath)) {
139
+ throw new Error(`Please ensure the file ${item.uri} is a mp4.`)
131
140
  }
132
141
  }
133
142
  );
134
143
 
135
- const previewMediaFiles = config.release.media?.filter(
136
- (asset: any) => asset.purpose === "screenshot" || asset.purpose === "video"
144
+ const screenshots = config.release.media?.filter(
145
+ (asset: any) => asset.purpose === "screenshot"
146
+ )
147
+
148
+ for (const item of screenshots) {
149
+ const mediaPath = path.join(process.cwd(), item.uri);
150
+ if (await checkScreenshotSize(mediaPath)) {
151
+ throw new Error(`Screenshot ${mediaPath} must be at least 1080px in width and height.`);
152
+ }
153
+ }
154
+
155
+ const videos = config.release.media?.filter(
156
+ (asset: any) => asset.purpose === "video"
137
157
  )
138
158
 
139
- if (previewMediaFiles.length < 4) {
140
- showMessage(
141
- "Preview media requirements changing in version 0.9.0",
142
- `At least 4 screenshots or videos are required for publishing a new release. Found only ${previewMediaFiles.length}`,
143
- "warning"
144
- )
159
+ for (const video of videos) {
160
+ const mediaPath = path.join(process.cwd(), video.uri);
161
+ if (await checkVideoSize(mediaPath)) {
162
+ throw new Error(`Video ${mediaPath} must be at least 720px in width and height.`);
163
+ }
164
+ }
165
+
166
+ if (screenshots.length + videos.length < 4) {
167
+ throw new Error(`At least 4 screenshots or videos are required for publishing a new release. Found only ${screenshots.length + videos.length}`)
145
168
  }
146
169
 
147
170
  validateLocalizableResources(config);
@@ -178,6 +201,13 @@ const checkImageExtension = (uri: string): boolean => {
178
201
  );
179
202
  };
180
203
 
204
+ const checkVideoExtension = (uri: string): boolean => {
205
+ const fileExt = path.extname(uri).toLowerCase();
206
+ return (
207
+ fileExt == ".mp4"
208
+ );
209
+ };
210
+
181
211
  /**
182
212
  * We need to pre-check some things in the localized resources before we move forward
183
213
  */
@@ -210,6 +240,19 @@ const checkIconDimensions = async (iconPath: string): Promise<boolean> => {
210
240
  return size?.width != size?.height || (size?.width ?? 0) != 512;
211
241
  };
212
242
 
243
+ const checkScreenshotSize = async (imagePath: string): Promise<boolean> => {
244
+ const size = await runImgSize(imagePath);
245
+
246
+ return (size?.width ?? 0) < 1080 || (size?.height ?? 0) < 1080;
247
+ }
248
+
249
+ const checkVideoSize = async (imagePath: string): Promise<boolean> => {
250
+ const size = await getVideoDimensions(imagePath);
251
+
252
+ return (size?.width ?? 0) < 720 || (size?.height ?? 0) < 720;
253
+ }
254
+
255
+
213
256
  const getAndroidDetails = async (
214
257
  aaptDir: string,
215
258
  apkPath: string
@@ -282,6 +325,8 @@ const getAndroidDetails = async (
282
325
  );
283
326
  }
284
327
 
328
+ checkAbis(apkPath);
329
+
285
330
  return {
286
331
  android_package: appPackage?.[1] ?? "",
287
332
  min_sdk: parseInt(minSdk?.[1] ?? "0", 10),
@@ -300,6 +345,35 @@ const getAndroidDetails = async (
300
345
  }
301
346
  };
302
347
 
348
+ const checkAbis = async (apkPath: string) => {
349
+ try {
350
+ const { stdout } = await runExec(`zipinfo -s ${apkPath} | grep \.so$`);
351
+ const amV7libs = [...stdout.matchAll(/lib\/armeabi-v7a\/(.*)/g)].flatMap(permission => permission[1]);
352
+ const x86libs = [...stdout.matchAll(/lib\/x86\/(.*)/g)].flatMap(permission => permission[1]);
353
+ const x8664libs = [...stdout.matchAll(/lib\/x86_64\/(.*)/g)].flatMap(permission => permission[1]);
354
+ if (amV7libs.length > 0 || x86libs.length > 0 || x8664libs.length > 0) {
355
+
356
+ const messages = [
357
+ `Solana dApp Store only supports arm64-v8a abi.`,
358
+ `Your apk file contains following unsupported abis`,
359
+ ... amV7libs.length > 0 ? [`\narmeabi-v7a:\n` + amV7libs] : [],
360
+ ... x86libs.length > 0 ? [`\nx86:\n` + x86libs] : [],
361
+ ... x8664libs.length > 0 ? [`\nx86_64:\n` + x8664libs] : [],
362
+ `\n\nAlthough your app works fine on Saga, these library files are unused and increase the size of apk file making the download and update time longer for your app.`,
363
+ `\n\nSee https://developer.android.com/games/optimize/64-bit#build-with-64-bit for how to optimize your app.`,
364
+ ].join('\n')
365
+
366
+ showMessage(
367
+ `Unsupported files found in apk`,
368
+ messages,
369
+ `warning`
370
+ )
371
+ }
372
+ } catch (e) {
373
+ // Ignore this error.
374
+ }
375
+ }
376
+
303
377
  export const extractCertFingerprint = async (aaptDir: string, apkPath: string): Promise<string> => {
304
378
  const { stdout } = await runExec(`${aaptDir}/apksigner verify --print-certs -v "${apkPath}"`);
305
379