@solana-mobile/dapp-store-cli 0.1.9 → 0.3.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 (72) hide show
  1. package/README.md +18 -53
  2. package/lib/esm/commands/create/app.js +2 -2
  3. package/lib/esm/commands/create/app.js.map +1 -1
  4. package/lib/esm/commands/create/publisher.js +2 -2
  5. package/lib/esm/commands/create/publisher.js.map +1 -1
  6. package/lib/esm/commands/create/release.js +2 -2
  7. package/lib/esm/commands/create/release.js.map +1 -1
  8. package/lib/esm/commands/publish/remove.js +6 -2
  9. package/lib/esm/commands/publish/remove.js.map +1 -1
  10. package/lib/esm/commands/publish/submit.js +8 -4
  11. package/lib/esm/commands/publish/submit.js.map +1 -1
  12. package/lib/esm/commands/publish/support.js +6 -2
  13. package/lib/esm/commands/publish/support.js.map +1 -1
  14. package/lib/esm/commands/publish/update.js +6 -2
  15. package/lib/esm/commands/publish/update.js.map +1 -1
  16. package/lib/esm/commands/scaffolding/index.js +2 -0
  17. package/lib/esm/commands/scaffolding/index.js.map +1 -0
  18. package/lib/esm/commands/scaffolding/init.js +15 -0
  19. package/lib/esm/commands/scaffolding/init.js.map +1 -0
  20. package/lib/esm/commands/validate.js +6 -15
  21. package/lib/esm/commands/validate.js.map +1 -1
  22. package/lib/esm/config/index.js +1 -2
  23. package/lib/esm/config/index.js.map +1 -1
  24. package/lib/esm/generated/config_obj.json +1 -0
  25. package/lib/esm/generated/config_schema.json +1 -0
  26. package/lib/esm/index.js +38 -17
  27. package/lib/esm/index.js.map +1 -1
  28. package/lib/esm/package.json +6 -3
  29. package/lib/esm/utils.js +63 -23
  30. package/lib/esm/utils.js.map +1 -1
  31. package/lib/types/commands/create/app.d.ts +1 -1
  32. package/lib/types/commands/create/app.d.ts.map +1 -1
  33. package/lib/types/commands/create/release.d.ts +1 -1
  34. package/lib/types/commands/create/release.d.ts.map +1 -1
  35. package/lib/types/commands/publish/remove.d.ts +1 -1
  36. package/lib/types/commands/publish/remove.d.ts.map +1 -1
  37. package/lib/types/commands/publish/submit.d.ts +1 -1
  38. package/lib/types/commands/publish/submit.d.ts.map +1 -1
  39. package/lib/types/commands/publish/support.d.ts +1 -1
  40. package/lib/types/commands/publish/support.d.ts.map +1 -1
  41. package/lib/types/commands/publish/update.d.ts +1 -1
  42. package/lib/types/commands/publish/update.d.ts.map +1 -1
  43. package/lib/types/commands/scaffolding/index.d.ts +2 -0
  44. package/lib/types/commands/scaffolding/index.d.ts.map +1 -0
  45. package/lib/types/commands/scaffolding/init.d.ts +2 -0
  46. package/lib/types/commands/scaffolding/init.d.ts.map +1 -0
  47. package/lib/types/commands/validate.d.ts.map +1 -1
  48. package/lib/types/config/index.d.ts.map +1 -1
  49. package/lib/types/upload/CachedStorageDriver.d.ts +3 -3
  50. package/lib/types/upload/CachedStorageDriver.d.ts.map +1 -1
  51. package/lib/types/utils.d.ts +8 -3
  52. package/lib/types/utils.d.ts.map +1 -1
  53. package/package.json +6 -3
  54. package/src/commands/create/app.ts +2 -2
  55. package/src/commands/create/publisher.ts +2 -2
  56. package/src/commands/create/release.ts +2 -2
  57. package/src/commands/publish/remove.ts +9 -2
  58. package/src/commands/publish/submit.ts +13 -5
  59. package/src/commands/publish/support.ts +9 -2
  60. package/src/commands/publish/update.ts +9 -2
  61. package/src/commands/scaffolding/index.ts +1 -0
  62. package/src/commands/scaffolding/init.ts +19 -0
  63. package/src/commands/validate.ts +7 -14
  64. package/src/config/index.ts +1 -2
  65. package/src/generated/config_obj.json +1 -0
  66. package/src/generated/config_schema.json +1 -0
  67. package/src/index.ts +54 -19
  68. package/src/prebuild_schema/publishing_source.yaml +46 -0
  69. package/src/prebuild_schema/schemagen.js +27 -0
  70. package/src/utils.ts +80 -28
  71. package/lib/esm/config/schema.json +0 -195
  72. package/src/config/schema.json +0 -195
@@ -1,7 +1,7 @@
1
1
  import { Connection, Keypair } from "@solana/web3.js";
2
2
  import type { SignWithPublisherKeypair } from "@solana-mobile/dapp-store-publishing-tools";
3
3
  import { publishSupport } from "@solana-mobile/dapp-store-publishing-tools";
4
- import { getConfigFile } from "../../utils.js";
4
+ import { checkMintedStatus, getConfigWithChecks } from "../../utils.js";
5
5
  import nacl from "tweetnacl";
6
6
 
7
7
  type PublishSupportCommandInput = {
@@ -35,10 +35,17 @@ export const publishSupportCommand = async ({
35
35
  publisher: publisherDetails,
36
36
  app: appDetails,
37
37
  release: releaseDetails,
38
- } = await getConfigFile();
38
+ } = await getConfigWithChecks();
39
+
39
40
  const sign = ((buf: Buffer) =>
40
41
  nacl.sign(buf, signer.secretKey)) as SignWithPublisherKeypair;
41
42
 
43
+ const pubAddr = publisherDetails.address;
44
+ const appAddr = appMintAddress ?? appDetails.address;
45
+ const releaseAddr = releaseMintAddress ?? releaseDetails.address;
46
+
47
+ await checkMintedStatus(connection, pubAddr, appAddr, releaseAddr);
48
+
42
49
  await publishSupport(
43
50
  { connection, sign },
44
51
  {
@@ -1,7 +1,7 @@
1
1
  import { Connection, Keypair } from "@solana/web3.js";
2
2
  import type { SignWithPublisherKeypair } from "@solana-mobile/dapp-store-publishing-tools";
3
3
  import { publishUpdate } from "@solana-mobile/dapp-store-publishing-tools";
4
- import { getConfigFile } from "../../utils.js";
4
+ import { checkMintedStatus, getConfigWithChecks } from "../../utils.js";
5
5
  import nacl from "tweetnacl";
6
6
 
7
7
  type PublishUpdateCommandInput = {
@@ -43,10 +43,17 @@ export const publishUpdateCommand = async ({
43
43
  app: appDetails,
44
44
  release: releaseDetails,
45
45
  solana_mobile_dapp_publisher_portal: solanaMobileDappPublisherPortalDetails,
46
- } = await getConfigFile();
46
+ } = await getConfigWithChecks();
47
+
47
48
  const sign = ((buf: Buffer) =>
48
49
  nacl.sign(buf, signer.secretKey)) as SignWithPublisherKeypair;
49
50
 
51
+ const pubAddr = publisherDetails.address;
52
+ const appAddr = appMintAddress ?? appDetails.address;
53
+ const releaseAddr = releaseMintAddress ?? releaseDetails.address;
54
+
55
+ await checkMintedStatus(connection, pubAddr, appAddr, releaseAddr);
56
+
50
57
  await publishUpdate(
51
58
  { connection, sign },
52
59
  {
@@ -0,0 +1 @@
1
+ export * from "./init.js";
@@ -0,0 +1,19 @@
1
+ import yaml, { dump } from "js-yaml";
2
+
3
+ // eslint-disable-next-line require-extensions/require-extensions
4
+ import releaseSchema from "../../generated/config_obj.json" assert { type: "json" };
5
+ import fs from "fs";
6
+ import { Constants } from "../../utils.js";
7
+
8
+ export const initScaffold = (): string => {
9
+ const outputYaml = Constants.CONFIG_FILE_NAME;
10
+ const outFile = `${process.cwd()}/${outputYaml}`;
11
+
12
+ if (fs.existsSync(outFile)) {
13
+ throw Error("Configuration file already present; please use to intialize a new config file.");
14
+ }
15
+
16
+ fs.writeFileSync(outFile, dump(releaseSchema));
17
+
18
+ return `Your configuration file was created: ${outputYaml}`;
19
+ };
@@ -5,23 +5,14 @@ import {
5
5
  validateApp,
6
6
  validatePublisher,
7
7
  validateRelease,
8
+ metaplexFileReplacer,
8
9
  } from "@solana-mobile/dapp-store-publishing-tools";
9
- import { debug, getConfigFile } from "../utils.js";
10
+ import { debug, getConfigWithChecks } from "../utils.js";
10
11
 
11
12
  import type { Keypair } from "@solana/web3.js";
12
13
  import type { MetaplexFile } from "@metaplex-foundation/js";
13
14
  import { isMetaplexFile } from "@metaplex-foundation/js";
14
15
 
15
- const metaplexFileReplacer = (k: any, v: any) => {
16
- if (isMetaplexFile(v)) {
17
- return {
18
- ...v,
19
- buffer: "(suppressed)",
20
- };
21
- }
22
- return v;
23
- }
24
-
25
16
  export const validateCommand = async ({
26
17
  signer,
27
18
  buildToolsPath,
@@ -33,7 +24,7 @@ export const validateCommand = async ({
33
24
  publisher: publisherDetails,
34
25
  app: appDetails,
35
26
  release: releaseDetails,
36
- } = await getConfigFile(buildToolsPath);
27
+ } = await getConfigWithChecks(buildToolsPath);
37
28
 
38
29
  debug({ publisherDetails, appDetails, releaseDetails });
39
30
 
@@ -67,10 +58,12 @@ export const validateCommand = async ({
67
58
  { releaseDetails, appDetails, publisherDetails },
68
59
  signer.publicKey
69
60
  );
70
- debug("releaseJson=", JSON.stringify({ releaseJson }, metaplexFileReplacer, 2));
61
+
62
+ const objStringified = JSON.stringify(releaseJson, metaplexFileReplacer, 2);
63
+ debug("releaseJson=", objStringified);
71
64
 
72
65
  try {
73
- validateRelease(releaseJson);
66
+ validateRelease(JSON.parse(objStringified));
74
67
  console.info(`Release JSON valid!`);
75
68
  } catch (e) {
76
69
  console.error(e);
@@ -10,7 +10,7 @@ import { load } from "js-yaml";
10
10
  import Ajv from "ajv";
11
11
 
12
12
  // eslint-disable-next-line require-extensions/require-extensions
13
- import schemaJson from "./schema.json" assert { type: "json" };
13
+ import schemaJson from "../generated/config_schema.json" assert { type: "json" };
14
14
 
15
15
  // TODO: Add version number return here
16
16
  export interface CLIConfig {
@@ -25,7 +25,6 @@ const validate = ajv.compile(schemaJson);
25
25
 
26
26
  export const getConfig = async (configPath: string) => {
27
27
  const configFile = await fs.readFile(configPath, "utf-8");
28
- const configJson = load(configFile);
29
28
 
30
29
  const valid = validate(load(configFile) as object);
31
30
 
@@ -0,0 +1 @@
1
+ {"publisher":{"name":"<<YOUR_PUBLISHER_NAME>>","address":"","website":"<<URL_OF_PUBLISHER_WEBSITE>>","email":"<<EMAIL_ADDRESS_TO_CONTACT_PUBLISHER>>","media":[{"purpose":"icon","uri":"<<RELATIVE_PATH_TO_PUBLISHER_ICON>>"}]},"app":{"name":"<<APP_NAME>>","address":"","android_package":"<<ANDROID_PACKAGE_NAME>>","urls":{"license_url":"<<URL_OF_APP_LICENSE_OR_TERMS_OF_SERVICE>>","copyright_url":"<<URL_OF_COPYRIGHT_DETAILS_FOR_APP>>","privacy_policy_url":"<<URL_OF_APP_PRIVACY_POLICY>>","website":"<<URL_OF_APP_WEBSITE>>"},"media":[{"purpose":"icon","uri":"<<RELATIVE_PATH_TO_APP_ICON>>"}]},"release":{"address":"","media":[{"purpose":"icon","uri":"<<RELATIVE_PATH_TO_RELEASE_ICON>>"},{"purpose":"screenshot","uri":"<<RELATIVE_PATH_TO_SCREENSHOT>>"}],"files":[{"purpose":"install","uri":"<<RELATIVE_PATH_TO_APK>>"}],"catalog":{"en-US":{"name":"<<APP_NAME>>\n","short_description":"<<SHORT_APP_DESCRIPTION>>\n","long_description":"<<LONG_APP_DESCRIPTION>>\n","new_in_version":"<<WHATS_NEW_IN_THIS_VERSION>>\n","saga_features":"<<ANY_FEATURES_ONLY_AVAILBLE_WHEN_RUNNING_ON_SAGA>>\n"}}},"solana_mobile_dapp_publisher_portal":{"google_store_package":"<<ANDROID_PACKAGE_NAME_OF_GOOGLE_PLAY_STORE_VERSION_IF_DIFFERENT>>","testing_instructions":"<<TESTING_INSTRUCTIONS>>\n"}}
@@ -0,0 +1 @@
1
+ {"type":"object","properties":{"publisher":{"type":"object","properties":{"name":{"type":"string"},"address":{"type":"string"},"website":{"type":"string"},"email":{"type":"string"},"media":{"type":"array","items":{"type":"object","properties":{"purpose":{"type":"string"},"uri":{"type":"string"}}}}}},"app":{"type":"object","properties":{"name":{"type":"string"},"address":{"type":"string"},"android_package":{"type":"string"},"urls":{"type":"object","properties":{"license_url":{"type":"string"},"copyright_url":{"type":"string"},"privacy_policy_url":{"type":"string"},"website":{"type":"string"}}},"media":{"type":"array","items":{"type":"object","properties":{"purpose":{"type":"string"},"uri":{"type":"string"}}}}}},"release":{"type":"object","properties":{"address":{"type":"string"},"media":{"type":"array","items":{"type":"object","properties":{"purpose":{"type":"string"},"uri":{"type":"string"}},"required":["purpose","uri"]}},"files":{"type":"array","items":{"type":"object","properties":{"purpose":{"type":"string"},"uri":{"type":"string"}}}},"catalog":{"type":"object","properties":{"en-US":{"type":"object","properties":{"name":{"type":"string"},"short_description":{"type":"string"},"long_description":{"type":"string"},"new_in_version":{"type":"string"},"saga_features":{"type":"string"}},"required":["short_description"]}}}}},"solana_mobile_dapp_publisher_portal":{"type":"object","properties":{"google_store_package":{"type":"string"},"testing_instructions":{"type":"string"}}}}}
package/src/index.ts CHANGED
@@ -7,11 +7,20 @@ import {
7
7
  publishSupportCommand,
8
8
  publishUpdateCommand
9
9
  } from "./commands/publish/index.js";
10
- import { checkForSelfUpdate, checkSubmissionNetwork, generateNetworkSuffix, getConfigFile, parseKeypair, showMessage } from "./utils.js";
10
+ import {
11
+ checkForSelfUpdate,
12
+ checkSubmissionNetwork,
13
+ Constants,
14
+ generateNetworkSuffix,
15
+ getConfigWithChecks,
16
+ parseKeypair,
17
+ showMessage
18
+ } from "./utils.js";
11
19
  import terminalLink from "terminal-link";
12
20
  import boxen from "boxen";
13
21
 
14
22
  import * as dotenv from "dotenv";
23
+ import { initScaffold } from "./commands/scaffolding/index.js";
15
24
 
16
25
  dotenv.config();
17
26
 
@@ -36,25 +45,47 @@ function resolveBuildToolsPath(buildToolsPath: string | undefined) {
36
45
  return;
37
46
  }
38
47
 
48
+ /**
49
+ * This method should be updated with each new release of the CLI, and just do nothing when there isn't anything to report
50
+ */
51
+ function latestReleaseMessage() {
52
+ showMessage(
53
+ `Publishing Tools Version ${ Constants.CLI_VERSION }`,
54
+ "- The new field \`short_description\` has been added to the set of strings required in the `catalog` section for each locale.\n" +
55
+ "- You can now specify a release-specific icon in the release \`media\` section in your configuration file.", "warning"
56
+ )
57
+ }
58
+
39
59
  async function tryWithErrorMessage(block: () => Promise<any>) {
40
60
  try {
41
61
  await block()
42
62
  } catch (e) {
43
63
  const errorMsg = (e as Error | null)?.message ?? "";
44
64
 
45
- showMessage("Error", errorMsg, true);
65
+ showMessage("Error", errorMsg, "error");
46
66
  }
47
67
  }
48
68
 
49
69
  async function main() {
50
70
  program
51
71
  .name("dapp-store")
52
- .version("0.1.9")
53
- .description("CLI to assist with publishing to the Saga Dapp Store");
72
+ .version(Constants.CLI_VERSION)
73
+ .description("CLI to assist with publishing to the Saga Dapp Store")
74
+
75
+ const initCommand = program
76
+ .command("init")
77
+ .description("First-time initialization of tooling configuration")
78
+ .action(async () => {
79
+ tryWithErrorMessage(async () => {
80
+ const msg = initScaffold();
81
+
82
+ showMessage("Initialized", msg);
83
+ })
84
+ });
54
85
 
55
86
  const createCommand = program
56
87
  .command("create")
57
- .description("Create a `publisher`, `app`, or `release`");
88
+ .description("Create a `publisher`, `app`, or `release`")
58
89
 
59
90
  createCommand
60
91
  .command("publisher")
@@ -67,6 +98,7 @@ async function main() {
67
98
  .option("-d, --dry-run", "Flag for dry run. Doesn't mint an NFT")
68
99
  .action(async ({ keypair, url, dryRun }) => {
69
100
  tryWithErrorMessage(async () => {
101
+ latestReleaseMessage();
70
102
  await checkForSelfUpdate();
71
103
 
72
104
  const signer = parseKeypair(keypair);
@@ -96,9 +128,10 @@ async function main() {
96
128
  .option("-d, --dry-run", "Flag for dry run. Doesn't mint an NFT")
97
129
  .action(async ({ publisherMintAddress, keypair, url, dryRun }) => {
98
130
  tryWithErrorMessage(async () => {
131
+ latestReleaseMessage();
99
132
  await checkForSelfUpdate();
100
133
 
101
- const config = await getConfigFile();
134
+ const config = await getConfigWithChecks();
102
135
 
103
136
  if (!hasAddressInConfig(config.publisher) && !publisherMintAddress) {
104
137
  throw new Error("Either specify a publisher mint address in the config file or specify as a CLI argument to this command.")
@@ -140,6 +173,7 @@ async function main() {
140
173
  )
141
174
  .action(async ({ appMintAddress, keypair, url, dryRun, buildToolsPath }) => {
142
175
  tryWithErrorMessage(async () => {
176
+ latestReleaseMessage();
143
177
  await checkForSelfUpdate();
144
178
 
145
179
  const resolvedBuildToolsPath = resolveBuildToolsPath(buildToolsPath);
@@ -147,7 +181,7 @@ async function main() {
147
181
  throw new Error("Please specify an Android build tools directory in the .env file or via the command line argument.")
148
182
  }
149
183
 
150
- const config = await getConfigFile();
184
+ const config = await getConfigWithChecks();
151
185
  if (!hasAddressInConfig(config.app) && !appMintAddress) {
152
186
  throw new Error("Either specify an app mint address in the config file or specify as a CLI argument to this command")
153
187
  }
@@ -184,6 +218,7 @@ async function main() {
184
218
  )
185
219
  .action(async ({ keypair, buildToolsPath }) => {
186
220
  tryWithErrorMessage(async () => {
221
+ latestReleaseMessage();
187
222
  await checkForSelfUpdate();
188
223
 
189
224
  const resolvedBuildToolsPath = resolveBuildToolsPath(buildToolsPath);
@@ -226,11 +261,11 @@ async function main() {
226
261
  )
227
262
  .option(
228
263
  "-a, --app-mint-address <app-mint-address>",
229
- "The mint address of the app NFT. If not specified, the value from config.yaml will be used."
264
+ "The mint address of the app NFT. If not specified, the value from your config file will be used."
230
265
  )
231
266
  .option(
232
267
  "-r, --release-mint-address <release-mint-address>",
233
- "The mint address of the release NFT. If not specified, the value from config.yaml will be used."
268
+ "The mint address of the release NFT. If not specified, the value from your config file will be used."
234
269
  )
235
270
  .option("-u, --url <url>", "RPC URL", "https://devnet.genesysgo.net")
236
271
  .option(
@@ -251,7 +286,7 @@ async function main() {
251
286
  await checkForSelfUpdate();
252
287
  await checkSubmissionNetwork(url);
253
288
 
254
- const config = await getConfigFile();
289
+ const config = await getConfigWithChecks();
255
290
 
256
291
  if (!hasAddressInConfig(config.release) && !releaseMintAddress) {
257
292
  throw new Error("Either specify a release mint address in the config file or specify as a CLI argument to this command.")
@@ -295,11 +330,11 @@ async function main() {
295
330
  )
296
331
  .option(
297
332
  "-a, --app-mint-address <app-mint-address>",
298
- "The mint address of the app NFT. If not specified, the value from config.yaml will be used."
333
+ "The mint address of the app NFT. If not specified, the value from your config file will be used."
299
334
  )
300
335
  .option(
301
336
  "-r, --release-mint-address <release-mint-address>",
302
- "The mint address of the release NFT. If not specified, the value from config.yaml will be used."
337
+ "The mint address of the release NFT. If not specified, the value from your config file will be used."
303
338
  )
304
339
  .option("-c, --critical", "Flag for a critical app update request")
305
340
  .option("-u, --url <url>", "RPC URL", "https://devnet.genesysgo.net")
@@ -322,7 +357,7 @@ async function main() {
322
357
  await checkForSelfUpdate();
323
358
  await checkSubmissionNetwork(url);
324
359
 
325
- const config = await getConfigFile();
360
+ const config = await getConfigWithChecks();
326
361
 
327
362
  if (!hasAddressInConfig(config.release) && !releaseMintAddress) {
328
363
  throw new Error("Either specify a release mint address in the config file or specify as a CLI argument to this command.")
@@ -363,11 +398,11 @@ async function main() {
363
398
  )
364
399
  .option(
365
400
  "-a, --app-mint-address <app-mint-address>",
366
- "The mint address of the app NFT. If not specified, the value from config.yaml will be used."
401
+ "The mint address of the app NFT. If not specified, the value from your config file will be used."
367
402
  )
368
403
  .option(
369
404
  "-r, --release-mint-address <release-mint-address>",
370
- "The mint address of the release NFT. If not specified, the value from config.yaml will be used."
405
+ "The mint address of the release NFT. If not specified, the value from your config file will be used."
371
406
  )
372
407
  .option("-c, --critical", "Flag for a critical app removal request")
373
408
  .option("-u, --url <url>", "RPC URL", "https://devnet.genesysgo.net")
@@ -389,7 +424,7 @@ async function main() {
389
424
  await checkForSelfUpdate();
390
425
  await checkSubmissionNetwork(url);
391
426
 
392
- const config = await getConfigFile();
427
+ const config = await getConfigWithChecks();
393
428
 
394
429
  if (!hasAddressInConfig(config.release) && !releaseMintAddress) {
395
430
  throw new Error("Either specify a release mint address in the config file or specify as a CLI argument to this command.")
@@ -429,11 +464,11 @@ async function main() {
429
464
  )
430
465
  .option(
431
466
  "-a, --app-mint-address <app-mint-address>",
432
- "The mint address of the app NFT. If not specified, the value from config.yaml will be used."
467
+ "The mint address of the app NFT. If not specified, the value from your config file will be used."
433
468
  )
434
469
  .option(
435
470
  "-r, --release-mint-address <release-mint-address>",
436
- "The mint address of the release NFT. If not specified, the value from config.yaml will be used."
471
+ "The mint address of the release NFT. If not specified, the value from your config file will be used."
437
472
  )
438
473
  .option("-u, --url <url>", "RPC URL", "https://devnet.genesysgo.net")
439
474
  .option(
@@ -449,7 +484,7 @@ async function main() {
449
484
  await checkForSelfUpdate();
450
485
  await checkSubmissionNetwork(url);
451
486
 
452
- const config = await getConfigFile();
487
+ const config = await getConfigWithChecks();
453
488
 
454
489
  if (!hasAddressInConfig(config.release) && !releaseMintAddress) {
455
490
  throw new Error("Either specify a release mint address in the config file or specify as a CLI argument to this command.")
@@ -0,0 +1,46 @@
1
+ publisher:
2
+ name: <<YOUR_PUBLISHER_NAME>>
3
+ address: ""
4
+ website: <<URL_OF_PUBLISHER_WEBSITE>>
5
+ email: <<EMAIL_ADDRESS_TO_CONTACT_PUBLISHER>>
6
+ media:
7
+ - purpose: icon
8
+ uri: <<RELATIVE_PATH_TO_PUBLISHER_ICON>>
9
+ app:
10
+ name: <<APP_NAME>>
11
+ address: ""
12
+ android_package: <<ANDROID_PACKAGE_NAME>>
13
+ urls:
14
+ license_url: <<URL_OF_APP_LICENSE_OR_TERMS_OF_SERVICE>>
15
+ copyright_url: <<URL_OF_COPYRIGHT_DETAILS_FOR_APP>>
16
+ privacy_policy_url: <<URL_OF_APP_PRIVACY_POLICY>>
17
+ website: <<URL_OF_APP_WEBSITE>>
18
+ media:
19
+ - purpose: icon
20
+ uri: <<RELATIVE_PATH_TO_APP_ICON>>
21
+ release:
22
+ address: ""
23
+ media:
24
+ - purpose: icon
25
+ uri: <<RELATIVE_PATH_TO_RELEASE_ICON>>
26
+ - purpose: screenshot
27
+ uri: <<RELATIVE_PATH_TO_SCREENSHOT>>
28
+ files:
29
+ - purpose: install
30
+ uri: <<RELATIVE_PATH_TO_APK>>
31
+ catalog:
32
+ en-US:
33
+ name: |
34
+ <<APP_NAME>>
35
+ short_description: |
36
+ <<SHORT_APP_DESCRIPTION>>
37
+ long_description: |
38
+ <<LONG_APP_DESCRIPTION>>
39
+ new_in_version: |
40
+ <<WHATS_NEW_IN_THIS_VERSION>>
41
+ saga_features: |
42
+ <<ANY_FEATURES_ONLY_AVAILBLE_WHEN_RUNNING_ON_SAGA>>
43
+ solana_mobile_dapp_publisher_portal:
44
+ google_store_package: <<ANDROID_PACKAGE_NAME_OF_GOOGLE_PLAY_STORE_VERSION_IF_DIFFERENT>>
45
+ testing_instructions: >
46
+ <<TESTING_INSTRUCTIONS>>
@@ -0,0 +1,27 @@
1
+ import fs, { read } from "fs";
2
+ import yaml from "js-yaml";
3
+ import generateSchema from "generate-schema";
4
+
5
+ try {
6
+ const yamlSrc = fs.readFileSync('./src/prebuild_schema/publishing_source.yaml', 'utf8')
7
+ const convertedYaml = yaml.load(yamlSrc);
8
+ fs.writeFileSync('./src/generated/config_obj.json', Buffer.from(JSON.stringify(convertedYaml)), 'utf-8');
9
+
10
+ const schema = generateSchema.json('result', convertedYaml);
11
+ // CLI 0.3.0: Adding requirement for `short_description` so validation will catch
12
+ schema["properties"]
13
+ ["release"]
14
+ ["properties"]
15
+ ["catalog"]
16
+ ["properties"]
17
+ ["en-US"].required = ["short_description"];
18
+
19
+ // Generator adds some keys/values we don't need & mess up validation
20
+ delete schema.$schema;
21
+ delete schema.title;
22
+
23
+ const toWrite = Buffer.from(JSON.stringify(schema));
24
+ fs.writeFileSync('./src/generated/config_schema.json', toWrite, 'utf-8');
25
+ } catch (e) {
26
+ console.log(":: Schema generation step failed ::");
27
+ }
package/src/utils.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from "fs";
2
- import type { AndroidDetails, App, Publisher, Release } from "@solana-mobile/dapp-store-publishing-tools";
2
+ import type { AndroidDetails, App, Publisher, Release, ReleaseJsonMetadata } from "@solana-mobile/dapp-store-publishing-tools";
3
3
  import type { Connection } from "@solana/web3.js";
4
- import { Keypair } from "@solana/web3.js";
4
+ import { Keypair, PublicKey } from "@solana/web3.js";
5
5
  import type { CLIConfig } from "./config/index.js";
6
6
  import { getConfig } from "./config/index.js";
7
7
  import debugModule from "debug";
@@ -14,23 +14,45 @@ import { imageSize } from "image-size";
14
14
  import updateNotifier from "update-notifier";
15
15
  import cliPackage from "./package.json" assert { type: "json" };
16
16
  import boxen from "boxen";
17
+ import ver from "semver";
17
18
 
18
19
  import { CachedStorageDriver } from "./upload/CachedStorageDriver.js";
19
20
 
20
21
  const runImgSize = util.promisify(imageSize);
21
22
  const runExec = util.promisify(exec);
22
23
 
24
+ export class Constants {
25
+ static CLI_VERSION = "0.3.0";
26
+ static CONFIG_FILE_NAME = "config.yaml";
27
+ }
28
+
23
29
  export const debug = debugModule("CLI");
24
30
 
25
31
  export const checkForSelfUpdate = async () => {
26
32
  const notifier = updateNotifier({ pkg: cliPackage });
27
33
  const updateInfo = await notifier.fetchInfo();
28
34
 
29
- if (updateInfo.current != updateInfo.latest) {
35
+ const latestVer = new ver.SemVer(updateInfo.latest);
36
+ const currentVer = new ver.SemVer(updateInfo.current);
37
+
38
+ if (latestVer.major > currentVer.major || latestVer.minor > currentVer.minor) {
30
39
  throw new Error("Please update to the latest version of the dApp Store CLI before proceeding.");
31
40
  }
32
41
  };
33
42
 
43
+ export const checkMintedStatus = async (conn: Connection, pubAddr: string, appAddr: string, releaseAddr: string) => {
44
+ const results = await conn.getMultipleAccountsInfo([
45
+ new PublicKey(pubAddr),
46
+ new PublicKey(appAddr),
47
+ new PublicKey(releaseAddr),
48
+ ]);
49
+
50
+ const rentAccounts = results.filter((item) => !(item == undefined) && item?.lamports > 0);
51
+ if (rentAccounts?.length != 3) {
52
+ throw new Error("Please ensure you have minted all of your NFTs before submitting to the Solana Mobile dApp publisher portal.");
53
+ }
54
+ };
55
+
34
56
  export const parseKeypair = (pathToKeypairFile: string) => {
35
57
  try {
36
58
  const keypairFile = fs.readFileSync(pathToKeypairFile, "utf-8");
@@ -53,10 +75,10 @@ const AaptPrefixes = {
53
75
  localePrefix: "locales: ",
54
76
  };
55
77
 
56
- export const getConfigFile = async (
78
+ export const getConfigWithChecks = async (
57
79
  buildToolsDir: string | null = null
58
80
  ): Promise<CLIConfig> => {
59
- const configFilePath = `${process.cwd()}/config.yaml`;
81
+ const configFilePath = `${process.cwd()}/${Constants.CONFIG_FILE_NAME}`;
60
82
 
61
83
  const config = await getConfig(configFilePath);
62
84
 
@@ -79,37 +101,38 @@ export const getConfigFile = async (
79
101
  const publisherIcon = config.publisher.media?.find(
80
102
  (asset: any) => asset.purpose === "icon"
81
103
  )?.uri;
104
+
82
105
  if (publisherIcon) {
83
106
  const iconPath = path.join(process.cwd(), publisherIcon);
84
- if (!fs.existsSync(iconPath) || !checkImageExtension(iconPath)) {
85
- throw new Error("Please check the path to your Publisher icon and ensure the file is a jpeg, png, or webp file.");
86
- }
107
+ await checkIconCompatibility(iconPath, "Publisher");
87
108
 
88
109
  const iconBuffer = await fs.promises.readFile(iconPath);
89
-
90
- if (await checkIconDimensions(iconPath)) {
91
- throw new Error("Icons must have square dimensions and be no greater than 512px by 512px.")
92
- }
93
-
94
110
  config.publisher.icon = toMetaplexFile(iconBuffer, publisherIcon);
95
111
  }
96
112
 
97
113
  const appIcon = config.app.media?.find(
98
114
  (asset: any) => asset.purpose === "icon"
99
115
  )?.uri;
116
+
100
117
  if (appIcon) {
101
118
  const iconPath = path.join(process.cwd(), appIcon);
102
- if (!fs.existsSync(iconPath) || !checkImageExtension(iconPath)) {
103
- throw new Error("Please check the path to your App icon and ensure the file is a jpeg, png, or webp file.")
104
- }
119
+ await checkIconCompatibility(iconPath, "App");
105
120
 
106
121
  const iconBuffer = await fs.promises.readFile(iconPath);
122
+ config.app.icon = toMetaplexFile(iconBuffer, appIcon);
123
+ }
107
124
 
108
- if (await checkIconDimensions(iconPath)) {
109
- throw new Error("Icons must have square dimensions and be no greater than 512px by 512px.")
110
- }
125
+ const releaseIcon = config.release.media?.find(
126
+ (asset: any) => asset.purpose === "icon"
127
+ )?.uri;
111
128
 
112
- config.app.icon = toMetaplexFile(iconBuffer, appIcon);
129
+ if (releaseIcon) {
130
+ const iconPath = path.join(process.cwd(), releaseIcon);
131
+ await checkIconCompatibility(iconPath, "Release");
132
+ }
133
+
134
+ if (!appIcon && !releaseIcon) {
135
+ throw new Error("Please specify at least one media entry of type icon in your configuration file");
113
136
  }
114
137
 
115
138
  config.release.media.forEach((item: CLIConfig["release"]["media"][0]) => {
@@ -119,9 +142,28 @@ export const getConfigFile = async (
119
142
  }
120
143
  });
121
144
 
145
+ const baselineSize = Object.keys(config.release.catalog["en-US"]).length;
146
+ Object.keys(config.release.catalog).forEach((locale) => {
147
+ const size = Object.keys(config.release.catalog[locale]).length;
148
+
149
+ if (size != baselineSize) {
150
+ throw new Error("Please ensure you have included all localized strings for all locales in your configuration file.");
151
+ }
152
+ });
153
+
122
154
  return config;
123
155
  };
124
156
 
157
+ const checkIconCompatibility = async (path: string, typeString: string) => {
158
+ if (!fs.existsSync(path) || !checkImageExtension(path)) {
159
+ throw new Error(`Please check the path to your ${typeString} icon and ensure the file is a jpeg, png, or webp file.`)
160
+ }
161
+
162
+ if (await checkIconDimensions(path)) {
163
+ throw new Error("Icons must have square dimensions and be no greater than 512px by 512px.")
164
+ }
165
+ };
166
+
125
167
  const checkImageExtension = (uri: string): boolean => {
126
168
  const fileExt = path.extname(uri).toLowerCase();
127
169
  return (
@@ -163,17 +205,27 @@ export const generateNetworkSuffix = (rpcUrl: string): string => {
163
205
  export const showMessage = (
164
206
  titleMessage = "",
165
207
  contentMessage = "",
166
- isError = false
167
- ) => {
168
- console.log(boxen(contentMessage, {
208
+ type: "standard" | "error" | "warning" = "standard",
209
+ ): string => {
210
+ let color = "cyan";
211
+ if (type == "error") {
212
+ color = "redBright";
213
+ } else if (type == "warning") {
214
+ color = "yellow";
215
+ }
216
+
217
+ const msg = boxen(contentMessage, {
169
218
  title: titleMessage,
170
219
  padding: 1,
171
220
  margin: 1,
172
221
  borderStyle: 'single',
173
- borderColor: isError ? "redBright" : "cyan",
222
+ borderColor: color,
174
223
  textAlignment: "left",
175
- titleAlignment: "center"
176
- }));
224
+ titleAlignment: "center",
225
+ });
226
+
227
+ console.log(msg);
228
+ return msg;
177
229
  };
178
230
 
179
231
  const checkIconDimensions = async (iconPath: string): Promise<boolean> => {
@@ -239,7 +291,7 @@ export const saveToConfig = async ({
239
291
  app,
240
292
  release,
241
293
  }: SaveToConfigArgs) => {
242
- const currentConfig = await getConfigFile();
294
+ const currentConfig = await getConfigWithChecks();
243
295
 
244
296
  delete currentConfig.publisher.icon;
245
297
  delete currentConfig.app.icon;
@@ -262,7 +314,7 @@ export const saveToConfig = async ({
262
314
  };
263
315
 
264
316
  // TODO(jon): Verify the contents of the YAML file
265
- fs.writeFileSync(`${process.cwd()}/config.yaml`, dump(newConfig));
317
+ fs.writeFileSync(`${process.cwd()}/${Constants.CONFIG_FILE_NAME}`, dump(newConfig));
266
318
  };
267
319
 
268
320
  export const getMetaplexInstance = (