@solana-mobile/dapp-store-cli 0.1.8 → 0.2.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 (62) hide show
  1. package/README.md +18 -53
  2. package/lib/esm/commands/publish/remove.js +5 -1
  3. package/lib/esm/commands/publish/remove.js.map +1 -1
  4. package/lib/esm/commands/publish/submit.js +7 -3
  5. package/lib/esm/commands/publish/submit.js.map +1 -1
  6. package/lib/esm/commands/publish/support.js +5 -1
  7. package/lib/esm/commands/publish/support.js.map +1 -1
  8. package/lib/esm/commands/publish/update.js +5 -1
  9. package/lib/esm/commands/publish/update.js.map +1 -1
  10. package/lib/esm/commands/scaffolding/index.js +2 -0
  11. package/lib/esm/commands/scaffolding/index.js.map +1 -0
  12. package/lib/esm/commands/scaffolding/init.js +15 -0
  13. package/lib/esm/commands/scaffolding/init.js.map +1 -0
  14. package/lib/esm/commands/validate.js +4 -13
  15. package/lib/esm/commands/validate.js.map +1 -1
  16. package/lib/esm/config/index.js +1 -2
  17. package/lib/esm/config/index.js.map +1 -1
  18. package/lib/esm/generated/config_obj.json +1 -0
  19. package/lib/esm/generated/config_schema.json +1 -0
  20. package/lib/esm/index.js +24 -10
  21. package/lib/esm/index.js.map +1 -1
  22. package/lib/esm/package.json +6 -3
  23. package/lib/esm/utils.js +54 -19
  24. package/lib/esm/utils.js.map +1 -1
  25. package/lib/types/commands/create/app.d.ts +1 -1
  26. package/lib/types/commands/create/app.d.ts.map +1 -1
  27. package/lib/types/commands/create/release.d.ts +1 -1
  28. package/lib/types/commands/create/release.d.ts.map +1 -1
  29. package/lib/types/commands/publish/remove.d.ts +1 -1
  30. package/lib/types/commands/publish/remove.d.ts.map +1 -1
  31. package/lib/types/commands/publish/submit.d.ts +1 -1
  32. package/lib/types/commands/publish/submit.d.ts.map +1 -1
  33. package/lib/types/commands/publish/support.d.ts +1 -1
  34. package/lib/types/commands/publish/support.d.ts.map +1 -1
  35. package/lib/types/commands/publish/update.d.ts +1 -1
  36. package/lib/types/commands/publish/update.d.ts.map +1 -1
  37. package/lib/types/commands/scaffolding/index.d.ts +2 -0
  38. package/lib/types/commands/scaffolding/index.d.ts.map +1 -0
  39. package/lib/types/commands/scaffolding/init.d.ts +2 -0
  40. package/lib/types/commands/scaffolding/init.d.ts.map +1 -0
  41. package/lib/types/commands/validate.d.ts.map +1 -1
  42. package/lib/types/config/index.d.ts.map +1 -1
  43. package/lib/types/upload/CachedStorageDriver.d.ts +3 -3
  44. package/lib/types/upload/CachedStorageDriver.d.ts.map +1 -1
  45. package/lib/types/utils.d.ts +9 -1
  46. package/lib/types/utils.d.ts.map +1 -1
  47. package/package.json +6 -3
  48. package/src/commands/publish/remove.ts +8 -1
  49. package/src/commands/publish/submit.ts +12 -4
  50. package/src/commands/publish/support.ts +8 -1
  51. package/src/commands/publish/update.ts +8 -1
  52. package/src/commands/scaffolding/index.ts +1 -0
  53. package/src/commands/scaffolding/init.ts +19 -0
  54. package/src/commands/validate.ts +5 -12
  55. package/src/config/index.ts +1 -2
  56. package/src/generated/config_obj.json +1 -0
  57. package/src/generated/config_schema.json +1 -0
  58. package/src/index.ts +34 -10
  59. package/src/prebuild_schema/publishing_source.yaml +44 -0
  60. package/src/prebuild_schema/schemagen.js +20 -0
  61. package/src/utils.ts +69 -22
  62. package/lib/esm/config/schema.json +0 -195
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, generateNetworkSuffix, getConfigFile, parseKeypair, showMessage } from "./utils.js";
10
+ import {
11
+ checkForSelfUpdate,
12
+ checkSubmissionNetwork,
13
+ Constants,
14
+ generateNetworkSuffix,
15
+ getConfigFile,
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
 
@@ -49,9 +58,20 @@ async function tryWithErrorMessage(block: () => Promise<any>) {
49
58
  async function main() {
50
59
  program
51
60
  .name("dapp-store")
52
- .version("0.1.8")
61
+ .version(Constants.CLI_VERSION)
53
62
  .description("CLI to assist with publishing to the Saga Dapp Store");
54
63
 
64
+ const initCommand = program
65
+ .command("init")
66
+ .description("First-time initialization of tooling configuration")
67
+ .action(async () => {
68
+ tryWithErrorMessage(async () => {
69
+ const msg = initScaffold();
70
+
71
+ showMessage("Initialized", msg);
72
+ })
73
+ });
74
+
55
75
  const createCommand = program
56
76
  .command("create")
57
77
  .description("Create a `publisher`, `app`, or `release`");
@@ -226,11 +246,11 @@ async function main() {
226
246
  )
227
247
  .option(
228
248
  "-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."
249
+ "The mint address of the app NFT. If not specified, the value from your config file will be used."
230
250
  )
231
251
  .option(
232
252
  "-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."
253
+ "The mint address of the release NFT. If not specified, the value from your config file will be used."
234
254
  )
235
255
  .option("-u, --url <url>", "RPC URL", "https://devnet.genesysgo.net")
236
256
  .option(
@@ -249,6 +269,7 @@ async function main() {
249
269
  }) => {
250
270
  tryWithErrorMessage(async () => {
251
271
  await checkForSelfUpdate();
272
+ await checkSubmissionNetwork(url);
252
273
 
253
274
  const config = await getConfigFile();
254
275
 
@@ -294,11 +315,11 @@ async function main() {
294
315
  )
295
316
  .option(
296
317
  "-a, --app-mint-address <app-mint-address>",
297
- "The mint address of the app NFT. If not specified, the value from config.yaml will be used."
318
+ "The mint address of the app NFT. If not specified, the value from your config file will be used."
298
319
  )
299
320
  .option(
300
321
  "-r, --release-mint-address <release-mint-address>",
301
- "The mint address of the release NFT. If not specified, the value from config.yaml will be used."
322
+ "The mint address of the release NFT. If not specified, the value from your config file will be used."
302
323
  )
303
324
  .option("-c, --critical", "Flag for a critical app update request")
304
325
  .option("-u, --url <url>", "RPC URL", "https://devnet.genesysgo.net")
@@ -319,6 +340,7 @@ async function main() {
319
340
  }) => {
320
341
  tryWithErrorMessage(async () => {
321
342
  await checkForSelfUpdate();
343
+ await checkSubmissionNetwork(url);
322
344
 
323
345
  const config = await getConfigFile();
324
346
 
@@ -361,11 +383,11 @@ async function main() {
361
383
  )
362
384
  .option(
363
385
  "-a, --app-mint-address <app-mint-address>",
364
- "The mint address of the app NFT. If not specified, the value from config.yaml will be used."
386
+ "The mint address of the app NFT. If not specified, the value from your config file will be used."
365
387
  )
366
388
  .option(
367
389
  "-r, --release-mint-address <release-mint-address>",
368
- "The mint address of the release NFT. If not specified, the value from config.yaml will be used."
390
+ "The mint address of the release NFT. If not specified, the value from your config file will be used."
369
391
  )
370
392
  .option("-c, --critical", "Flag for a critical app removal request")
371
393
  .option("-u, --url <url>", "RPC URL", "https://devnet.genesysgo.net")
@@ -385,6 +407,7 @@ async function main() {
385
407
  }) => {
386
408
  tryWithErrorMessage(async () => {
387
409
  await checkForSelfUpdate();
410
+ await checkSubmissionNetwork(url);
388
411
 
389
412
  const config = await getConfigFile();
390
413
 
@@ -426,11 +449,11 @@ async function main() {
426
449
  )
427
450
  .option(
428
451
  "-a, --app-mint-address <app-mint-address>",
429
- "The mint address of the app NFT. If not specified, the value from config.yaml will be used."
452
+ "The mint address of the app NFT. If not specified, the value from your config file will be used."
430
453
  )
431
454
  .option(
432
455
  "-r, --release-mint-address <release-mint-address>",
433
- "The mint address of the release NFT. If not specified, the value from config.yaml will be used."
456
+ "The mint address of the release NFT. If not specified, the value from your config file will be used."
434
457
  )
435
458
  .option("-u, --url <url>", "RPC URL", "https://devnet.genesysgo.net")
436
459
  .option(
@@ -444,6 +467,7 @@ async function main() {
444
467
  ) => {
445
468
  tryWithErrorMessage(async () => {
446
469
  await checkForSelfUpdate();
470
+ await checkSubmissionNetwork(url);
447
471
 
448
472
  const config = await getConfigFile();
449
473
 
@@ -0,0 +1,44 @@
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
+ long_description: |
36
+ <<LONG_APP_DESCRIPTION>>
37
+ new_in_version: |
38
+ <<WHATS_NEW_IN_THIS_VERSION>>
39
+ saga_features: |
40
+ <<ANY_FEATURES_ONLY_AVAILBLE_WHEN_RUNNING_ON_SAGA>>
41
+ solana_mobile_dapp_publisher_portal:
42
+ google_store_package: <<ANDROID_PACKAGE_NAME_OF_GOOGLE_PLAY_STORE_VERSION_IF_DIFFERENT>>
43
+ testing_instructions: >
44
+ <<TESTING_INSTRUCTIONS>>
@@ -0,0 +1,20 @@
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
+
12
+ // Generator adds some keys/values we don't need & mess up validation
13
+ delete schema.$schema;
14
+ delete schema.title;
15
+
16
+ const toWrite = Buffer.from(JSON.stringify(schema));
17
+ fs.writeFileSync('./src/generated/config_schema.json', toWrite, 'utf-8');
18
+ } catch (e) {
19
+ console.log(":: Schema generation step failed ::");
20
+ }
package/src/utils.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from "fs";
2
2
  import type { AndroidDetails, App, Publisher, Release } 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.2.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");
@@ -56,7 +78,7 @@ const AaptPrefixes = {
56
78
  export const getConfigFile = 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]) => {
@@ -122,6 +145,16 @@ export const getConfigFile = async (
122
145
  return config;
123
146
  };
124
147
 
148
+ const checkIconCompatibility = async (path: string, typeString: string) => {
149
+ if (!fs.existsSync(path) || !checkImageExtension(path)) {
150
+ throw new Error(`Please check the path to your ${typeString} icon and ensure the file is a jpeg, png, or webp file.`)
151
+ }
152
+
153
+ if (await checkIconDimensions(path)) {
154
+ throw new Error("Icons must have square dimensions and be no greater than 512px by 512px.")
155
+ }
156
+ };
157
+
125
158
  const checkImageExtension = (uri: string): boolean => {
126
159
  const fileExt = path.extname(uri).toLowerCase();
127
160
  return (
@@ -132,14 +165,28 @@ const checkImageExtension = (uri: string): boolean => {
132
165
  );
133
166
  };
134
167
 
168
+ export const isDevnet = (rpcUrl: string): boolean => {
169
+ return rpcUrl.indexOf("devnet") != -1;
170
+ };
171
+
172
+ export const isTestnet = (rpcUrl: string): boolean => {
173
+ return rpcUrl.indexOf("testnet") != -1;
174
+ };
175
+
176
+ export const checkSubmissionNetwork = (rpcUrl: string) => {
177
+ if (isDevnet(rpcUrl) || isTestnet(rpcUrl)) {
178
+ throw new Error("It looks like you are attempting to submit a request with a devnet or testnet RPC endpoint. Please ensure that your NFTs are minted on mainnet beta, and re-run with a mainnet beta RPC endpoint.");
179
+ }
180
+ };
181
+
135
182
  export const generateNetworkSuffix = (rpcUrl: string): string => {
136
183
  let suffix = "";
137
184
 
138
- if (rpcUrl.indexOf("devnet") != -1) {
185
+ if (isDevnet(rpcUrl)) {
139
186
  suffix = "?cluster=devnet";
140
- } else if (rpcUrl.indexOf("testnet") != -1) {
187
+ } else if (isTestnet(rpcUrl)) {
141
188
  suffix = "?cluster=testnet";
142
- } else if (rpcUrl.indexOf("mainnet") != -1) {
189
+ } else {
143
190
  suffix = "?cluster=mainnet";
144
191
  }
145
192
 
@@ -248,7 +295,7 @@ export const saveToConfig = async ({
248
295
  };
249
296
 
250
297
  // TODO(jon): Verify the contents of the YAML file
251
- fs.writeFileSync(`${process.cwd()}/config.yaml`, dump(newConfig));
298
+ fs.writeFileSync(`${process.cwd()}/${Constants.CONFIG_FILE_NAME}`, dump(newConfig));
252
299
  };
253
300
 
254
301
  export const getMetaplexInstance = (
@@ -1,195 +0,0 @@
1
- {
2
- "type": "object",
3
- "properties": {
4
- "publisher": {
5
- "type": "object",
6
- "properties": {
7
- "name": {
8
- "type": "string"
9
- },
10
- "address": {
11
- "type": "string"
12
- },
13
- "website": {
14
- "type": "string"
15
- },
16
- "email": {
17
- "type": "string"
18
- },
19
- "media": {
20
- "type": "array",
21
- "items": [
22
- {
23
- "type": "object",
24
- "properties": {
25
- "purpose": {
26
- "type": "string"
27
- },
28
- "uri": {
29
- "type": "string"
30
- }
31
- },
32
- "required": ["purpose", "uri"]
33
- }
34
- ]
35
- }
36
- },
37
- "required": ["name", "website", "email", "media"]
38
- },
39
- "app": {
40
- "type": "object",
41
- "properties": {
42
- "name": {
43
- "type": "string"
44
- },
45
- "address": {
46
- "type": "string"
47
- },
48
- "android_package": {
49
- "type": "string"
50
- },
51
- "urls": {
52
- "type": "object",
53
- "properties": {
54
- "license_url": {
55
- "type": "string"
56
- },
57
- "copyright_url": {
58
- "type": "string"
59
- },
60
- "privacy_policy_url": {
61
- "type": "string"
62
- },
63
- "website": {
64
- "type": "string"
65
- }
66
- },
67
- "required": [
68
- "license_url",
69
- "copyright_url",
70
- "privacy_policy_url",
71
- "website"
72
- ]
73
- },
74
- "media": {
75
- "type": "array",
76
- "items": [
77
- {
78
- "type": "object",
79
- "properties": {
80
- "purpose": {
81
- "type": "string"
82
- },
83
- "uri": {
84
- "type": "string"
85
- }
86
- },
87
- "required": ["purpose", "uri"]
88
- }
89
- ]
90
- }
91
- },
92
- "required": ["name", "android_package", "urls", "media"]
93
- },
94
- "release": {
95
- "type": "object",
96
- "properties": {
97
- "address": {
98
- "type": "string"
99
- },
100
- "media": {
101
- "type": "array",
102
- "items": [
103
- {
104
- "type": "object",
105
- "properties": {
106
- "purpose": {
107
- "type": "string"
108
- },
109
- "uri": {
110
- "type": "string"
111
- }
112
- },
113
- "required": ["purpose", "uri"]
114
- }
115
- ]
116
- },
117
- "files": {
118
- "type": "array",
119
- "minItems": 1,
120
- "contains": {
121
- "type": "object",
122
- "properties": {
123
- "purpose": {
124
- "type": "string",
125
- "const": "install"
126
- }
127
- }
128
- },
129
- "items": [
130
- {
131
- "type": "object",
132
- "properties": {
133
- "purpose": {
134
- "type": "string"
135
- },
136
- "uri": {
137
- "type": "string"
138
- }
139
- },
140
- "required": ["purpose", "uri"]
141
- }
142
- ]
143
- },
144
- "catalog": {
145
- "type": "object",
146
- "patternProperties": {
147
- "^[a-zA-Z]{2,8}(-[a-zA-Z0-9]{2,8})*$": {
148
- "type": "object",
149
- "properties": {
150
- "name": {
151
- "type": "string"
152
- },
153
- "long_description": {
154
- "type": "string"
155
- },
156
- "new_in_version": {
157
- "type": "string"
158
- },
159
- "saga_features": {
160
- "type": "string"
161
- }
162
- },
163
- "required": [
164
- "name",
165
- "long_description",
166
- "new_in_version",
167
- "saga_features"
168
- ]
169
- }
170
- },
171
- "additionalProperties": false
172
- }
173
- },
174
- "required": ["media", "files", "catalog"]
175
- },
176
- "solana_mobile_dapp_publisher_portal": {
177
- "type": "object",
178
- "properties": {
179
- "google_store_package": {
180
- "type": "string"
181
- },
182
- "testing_instructions": {
183
- "type": "string"
184
- }
185
- },
186
- "required": ["testing_instructions"]
187
- }
188
- },
189
- "required": [
190
- "publisher",
191
- "app",
192
- "release",
193
- "solana_mobile_dapp_publisher_portal"
194
- ]
195
- }