@solana-mobile/dapp-store-cli 0.1.1-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 (86) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +142 -0
  3. package/bin/dapp-store.js +3 -0
  4. package/lib/esm/commands/create/app.js +37 -0
  5. package/lib/esm/commands/create/app.js.map +1 -0
  6. package/lib/esm/commands/create/index.js +44 -0
  7. package/lib/esm/commands/create/index.js.map +1 -0
  8. package/lib/esm/commands/create/publisher.js +33 -0
  9. package/lib/esm/commands/create/publisher.js.map +1 -0
  10. package/lib/esm/commands/create/release.js +48 -0
  11. package/lib/esm/commands/create/release.js.map +1 -0
  12. package/lib/esm/commands/index.js +4 -0
  13. package/lib/esm/commands/index.js.map +1 -0
  14. package/lib/esm/commands/publish/index.js +25 -0
  15. package/lib/esm/commands/publish/index.js.map +1 -0
  16. package/lib/esm/commands/publish/remove.js +20 -0
  17. package/lib/esm/commands/publish/remove.js.map +1 -0
  18. package/lib/esm/commands/publish/submit.js +25 -0
  19. package/lib/esm/commands/publish/submit.js.map +1 -0
  20. package/lib/esm/commands/publish/support.js +20 -0
  21. package/lib/esm/commands/publish/support.js.map +1 -0
  22. package/lib/esm/commands/publish/update.js +26 -0
  23. package/lib/esm/commands/publish/update.js.map +1 -0
  24. package/lib/esm/commands/validate.js +40 -0
  25. package/lib/esm/commands/validate.js.map +1 -0
  26. package/lib/esm/config/index.js +19 -0
  27. package/lib/esm/config/index.js.map +1 -0
  28. package/lib/esm/config/schema.json +228 -0
  29. package/lib/esm/index.js +259 -0
  30. package/lib/esm/index.js.map +1 -0
  31. package/lib/esm/upload/CachedStorageDriver.js +64 -0
  32. package/lib/esm/upload/CachedStorageDriver.js.map +1 -0
  33. package/lib/esm/upload/index.js +2 -0
  34. package/lib/esm/upload/index.js.map +1 -0
  35. package/lib/esm/utils.js +171 -0
  36. package/lib/esm/utils.js.map +1 -0
  37. package/lib/types/commands/create/app.d.ts +12 -0
  38. package/lib/types/commands/create/app.d.ts.map +1 -0
  39. package/lib/types/commands/create/index.d.ts +4 -0
  40. package/lib/types/commands/create/index.d.ts.map +1 -0
  41. package/lib/types/commands/create/publisher.d.ts +9 -0
  42. package/lib/types/commands/create/publisher.d.ts.map +1 -0
  43. package/lib/types/commands/create/release.d.ts +14 -0
  44. package/lib/types/commands/create/release.d.ts.map +1 -0
  45. package/lib/types/commands/index.d.ts +4 -0
  46. package/lib/types/commands/index.d.ts.map +1 -0
  47. package/lib/types/commands/publish/index.d.ts +5 -0
  48. package/lib/types/commands/publish/index.d.ts.map +1 -0
  49. package/lib/types/commands/publish/remove.d.ts +12 -0
  50. package/lib/types/commands/publish/remove.d.ts.map +1 -0
  51. package/lib/types/commands/publish/submit.d.ts +12 -0
  52. package/lib/types/commands/publish/submit.d.ts.map +1 -0
  53. package/lib/types/commands/publish/support.d.ts +12 -0
  54. package/lib/types/commands/publish/support.d.ts.map +1 -0
  55. package/lib/types/commands/publish/update.d.ts +13 -0
  56. package/lib/types/commands/publish/update.d.ts.map +1 -0
  57. package/lib/types/commands/validate.d.ts +6 -0
  58. package/lib/types/commands/validate.d.ts.map +1 -0
  59. package/lib/types/config/index.d.ts +10 -0
  60. package/lib/types/config/index.d.ts.map +1 -0
  61. package/lib/types/index.d.ts +2 -0
  62. package/lib/types/index.d.ts.map +1 -0
  63. package/lib/types/upload/CachedStorageDriver.d.ts +30 -0
  64. package/lib/types/upload/CachedStorageDriver.d.ts.map +1 -0
  65. package/lib/types/upload/index.d.ts +2 -0
  66. package/lib/types/upload/index.d.ts.map +1 -0
  67. package/lib/types/utils.d.ts +21 -0
  68. package/lib/types/utils.d.ts.map +1 -0
  69. package/package.json +49 -0
  70. package/src/commands/create/app.ts +88 -0
  71. package/src/commands/create/index.ts +48 -0
  72. package/src/commands/create/publisher.ts +78 -0
  73. package/src/commands/create/release.ts +107 -0
  74. package/src/commands/index.ts +3 -0
  75. package/src/commands/publish/index.ts +29 -0
  76. package/src/commands/publish/remove.ts +46 -0
  77. package/src/commands/publish/submit.ts +55 -0
  78. package/src/commands/publish/support.ts +46 -0
  79. package/src/commands/publish/update.ts +58 -0
  80. package/src/commands/validate.ts +67 -0
  81. package/src/config/index.ts +40 -0
  82. package/src/config/schema.json +228 -0
  83. package/src/index.ts +435 -0
  84. package/src/upload/CachedStorageDriver.ts +104 -0
  85. package/src/upload/index.ts +1 -0
  86. package/src/utils.ts +275 -0
@@ -0,0 +1,104 @@
1
+ import fs from "fs";
2
+ import type { MetaplexFile, StorageDriver } from "@metaplex-foundation/js";
3
+ import { createHash } from "crypto";
4
+
5
+ type URI = string;
6
+
7
+ type Asset = {
8
+ path: string;
9
+ sha256: string;
10
+ uri: URI;
11
+ };
12
+
13
+ export type AssetManifestSchema = {
14
+ schema_version: string;
15
+ assets: {
16
+ [path: string]: Asset;
17
+ };
18
+ };
19
+
20
+ // TODO(jon): We need to manage the removal / replacement of assets in the manifest
21
+ export class CachedStorageDriver implements StorageDriver {
22
+ static readonly SCHEMA_VERSION = "0.2.2";
23
+ assetManifest: AssetManifestSchema;
24
+ assetManifestPath: string;
25
+ storageDriver: StorageDriver;
26
+
27
+ constructor(
28
+ storageDriver: StorageDriver,
29
+ { assetManifestPath }: { assetManifestPath: string }
30
+ ) {
31
+ this.assetManifestPath = assetManifestPath;
32
+ console.info({ loading: true });
33
+ this.assetManifest = this.loadAssetManifest(assetManifestPath) ?? {
34
+ schema_version: CachedStorageDriver.SCHEMA_VERSION,
35
+ assets: {},
36
+ };
37
+ this.storageDriver = storageDriver;
38
+ }
39
+
40
+ async getUploadPrice(bytes: number) {
41
+ return this.storageDriver.getUploadPrice(bytes);
42
+ }
43
+
44
+ loadAssetManifest(filename: string): AssetManifestSchema | undefined {
45
+ try {
46
+ return JSON.parse(
47
+ fs.readFileSync(filename, "utf-8")
48
+ ) as AssetManifestSchema;
49
+ } catch (error) {
50
+ console.warn(`Failed opening ${filename}; initializing with a blank asset manifest`);
51
+ return;
52
+ }
53
+ }
54
+
55
+ uploadedAsset(filename: string, { sha256 }: { sha256: string }) {
56
+ if (this.assetManifest.assets[filename]?.sha256 === sha256) {
57
+ return this.assetManifest.assets[filename];
58
+ }
59
+ return null;
60
+ }
61
+
62
+ async upload(file: MetaplexFile): Promise<string> {
63
+ // `inline.json` is the NFT-related metadata. This data is not stable so we'll skip the caching step
64
+ if (file.fileName === "inline.json") {
65
+ return await this.storageDriver.upload(file);
66
+ }
67
+ const hash = createHash("sha256").update(file.buffer).digest("base64");
68
+
69
+ console.info(
70
+ JSON.stringify({
71
+ file: {
72
+ name: file.fileName,
73
+ disn: file.displayName,
74
+ un: file.uniqueName,
75
+ },
76
+ })
77
+ );
78
+ const uploadedAsset = this.uploadedAsset(file.fileName, { sha256: hash });
79
+ if (uploadedAsset) {
80
+ console.log(
81
+ `Asset ${file.fileName} already uploaded at ${uploadedAsset.uri}`
82
+ );
83
+ return uploadedAsset.uri;
84
+ }
85
+
86
+ console.log(`Uploading ${file.fileName}`);
87
+ const uri = await this.storageDriver.upload(file);
88
+
89
+ this.assetManifest.assets[file.fileName] = {
90
+ path: file.fileName,
91
+ sha256: hash,
92
+ uri,
93
+ };
94
+
95
+ await fs.promises.writeFile(
96
+ `${process.cwd()}/${this.assetManifestPath}`,
97
+ // Something is really weird, I can't seem to stringify `this.assetManifest` straight-up. Here be dragons
98
+ JSON.stringify({ assets: { ...this.assetManifest.assets } }, null, 2),
99
+ "utf-8"
100
+ );
101
+
102
+ return uri;
103
+ }
104
+ }
@@ -0,0 +1 @@
1
+ export * from "./CachedStorageDriver.js";
package/src/utils.ts ADDED
@@ -0,0 +1,275 @@
1
+ import fs from "fs";
2
+ import type {
3
+ AndroidDetails,
4
+ App,
5
+ Publisher,
6
+ Release,
7
+ } from "@solana-mobile/dapp-store-publishing-tools";
8
+ import type { Connection } from "@solana/web3.js";
9
+ import { Keypair } from "@solana/web3.js";
10
+ import type { CLIConfig } from "./config/index.js";
11
+ import { getConfig } from "./config/index.js";
12
+ import debugModule from "debug";
13
+ import { dump } from "js-yaml";
14
+ import * as util from "util";
15
+ import { exec } from "child_process";
16
+ import * as path from "path";
17
+ import {
18
+ BundlrStorageDriver,
19
+ keypairIdentity,
20
+ Metaplex,
21
+ toMetaplexFile,
22
+ } from "@metaplex-foundation/js";
23
+ import { imageSize } from "image-size";
24
+
25
+ import { CachedStorageDriver } from "./upload/CachedStorageDriver.js";
26
+
27
+ const runImgSize = util.promisify(imageSize);
28
+ const runExec = util.promisify(exec);
29
+
30
+ export const debug = debugModule("CLI");
31
+
32
+ export const parseKeypair = (pathToKeypairFile: string) => {
33
+ try {
34
+ const keypairFile = fs.readFileSync(pathToKeypairFile, "utf-8");
35
+ return Keypair.fromSecretKey(Buffer.from(JSON.parse(keypairFile)));
36
+ } catch (e) {
37
+ console.error(
38
+ `Something went wrong when attempting to retrieve the keypair at ${pathToKeypairFile}`
39
+ );
40
+ }
41
+ };
42
+
43
+ const AaptPrefixes = {
44
+ quoteRegex: "'(.*?)'",
45
+ quoteNonLazyRegex: "'(.*)'",
46
+ packagePrefix: "package: name=",
47
+ verCodePrefix: "versionCode=",
48
+ verNamePrefix: "versionName=",
49
+ sdkPrefix: "sdkVersion:",
50
+ permissionPrefix: "uses-permission: name=",
51
+ localePrefix: "locales: ",
52
+ };
53
+
54
+ export const getConfigFile = async (
55
+ buildToolsDir: string | null = null
56
+ ): Promise<CLIConfig & { isValid: boolean }> => {
57
+ const configFilePath = `${process.cwd()}/config.yaml`;
58
+
59
+ const config = await getConfig(configFilePath);
60
+ config.isValid = true;
61
+
62
+ console.info(`Pulling details from ${configFilePath}`);
63
+
64
+ if (buildToolsDir && fs.lstatSync(buildToolsDir).isDirectory()) {
65
+ // We validate that the config is going to have at least one installable asset
66
+ const apkEntry = config.release.files.find(
67
+ (asset: CLIConfig["release"]["files"][0]) => asset.purpose === "install"
68
+ )!;
69
+ const apkPath = path.join(process.cwd(), apkEntry?.uri);
70
+ if (!fs.existsSync(apkPath)) {
71
+ showUserErrorMessage("Invalid path to APK file.");
72
+ config.isValid = false;
73
+ return config;
74
+ }
75
+
76
+ config.release.android_details = await getAndroidDetails(
77
+ buildToolsDir,
78
+ apkPath
79
+ );
80
+ }
81
+
82
+ const publisherIcon = config.publisher.media?.find(
83
+ (asset: any) => asset.purpose === "icon"
84
+ )?.uri;
85
+ if (publisherIcon) {
86
+ const iconPath = path.join(process.cwd(), publisherIcon);
87
+ if (!fs.existsSync(iconPath) || !checkImageExtension(iconPath)) {
88
+ showUserErrorMessage(
89
+ "Please check the path to your Publisher icon ensure the file is a jpeg, png, or webp file."
90
+ );
91
+ config.isValid = false;
92
+ return config;
93
+ }
94
+
95
+ const iconBuffer = await fs.promises.readFile(iconPath);
96
+
97
+ if (await checkIconDimensions(iconPath)) {
98
+ showUserErrorMessage(
99
+ "Icons must have square dimensions and be no greater than 512px by 512px."
100
+ );
101
+ config.isValid = false;
102
+ return config;
103
+ }
104
+
105
+ config.publisher.icon = toMetaplexFile(
106
+ iconBuffer,
107
+ path.join("media", publisherIcon)
108
+ );
109
+ }
110
+
111
+ const appIcon = config.app.media?.find(
112
+ (asset: any) => asset.purpose === "icon"
113
+ )?.uri;
114
+ if (appIcon) {
115
+ const iconPath = path.join(process.cwd(), appIcon);
116
+ if (!fs.existsSync(iconPath) || !checkImageExtension(iconPath)) {
117
+ showUserErrorMessage(
118
+ "Please check the path to your App icon ensure the file is a jpeg, png, or webp file."
119
+ );
120
+ config.isValid = false;
121
+ return config;
122
+ }
123
+
124
+ const iconBuffer = await fs.promises.readFile(iconPath);
125
+
126
+ if (await checkIconDimensions(iconPath)) {
127
+ showUserErrorMessage(
128
+ "Icons must have square dimensions and be no greater than 512px by 512px."
129
+ );
130
+ config.isValid = false;
131
+ return config;
132
+ }
133
+
134
+ config.app.icon = toMetaplexFile(iconBuffer, path.join("media", appIcon));
135
+ }
136
+
137
+ config.release.media.forEach((item: CLIConfig["release"]["media"][0]) => {
138
+ const imagePath = path.join(process.cwd(), item.uri);
139
+ if (!fs.existsSync(imagePath) || !checkImageExtension(imagePath)) {
140
+ showUserErrorMessage(
141
+ `Invalid media path or file type: ${item.uri}. Please ensure the file is a jpeg, png, or webp file.`
142
+ );
143
+ config.isValid = false;
144
+ return config;
145
+ }
146
+ });
147
+
148
+ return config;
149
+ };
150
+
151
+ const checkImageExtension = (uri: string): boolean => {
152
+ const fileExt = path.extname(uri).toLowerCase();
153
+ return (
154
+ fileExt == ".png" ||
155
+ fileExt == ".jpg" ||
156
+ fileExt == ".jpeg" ||
157
+ fileExt == ".webp"
158
+ );
159
+ };
160
+
161
+ export const showUserErrorMessage = (msg: string) => {
162
+ console.error("\n:::: Solana Publish CLI: Error Message ::::");
163
+ console.error(msg);
164
+ console.error("");
165
+ };
166
+
167
+ const checkIconDimensions = async (iconPath: string): Promise<boolean> => {
168
+ const size = await runImgSize(iconPath);
169
+
170
+ return size?.width != size?.height || (size?.width ?? 0) > 512;
171
+ };
172
+
173
+ const getAndroidDetails = async (
174
+ aaptDir: string,
175
+ apkPath: string
176
+ ): Promise<AndroidDetails> => {
177
+ const { stdout } = await runExec(`${aaptDir}/aapt2 dump badging ${apkPath}`);
178
+
179
+ const appPackage = new RegExp(
180
+ AaptPrefixes.packagePrefix + AaptPrefixes.quoteRegex
181
+ ).exec(stdout);
182
+ const versionCode = new RegExp(
183
+ AaptPrefixes.verCodePrefix + AaptPrefixes.quoteRegex
184
+ ).exec(stdout);
185
+ //TODO: Return this and use automatically replacing command line arg
186
+ //const versionName = new RegExp(prefixes.verNamePrefix + prefixes.quoteRegex).exec(stdout);
187
+ const minSdk = new RegExp(
188
+ AaptPrefixes.sdkPrefix + AaptPrefixes.quoteRegex
189
+ ).exec(stdout);
190
+ const permissions = new RegExp(
191
+ AaptPrefixes.permissionPrefix + AaptPrefixes.quoteNonLazyRegex
192
+ ).exec(stdout);
193
+ const locales = new RegExp(
194
+ AaptPrefixes.localePrefix + AaptPrefixes.quoteNonLazyRegex
195
+ ).exec(stdout);
196
+
197
+ let permissionArray = Array.from(permissions?.values() ?? []);
198
+ if (permissionArray.length >= 2) {
199
+ permissionArray = permissionArray.slice(1);
200
+ }
201
+
202
+ let localeArray = Array.from(locales?.values() ?? []);
203
+ if (localeArray.length == 2) {
204
+ const localesSrc = localeArray[1];
205
+ localeArray = localesSrc.split("' '").slice(1);
206
+ }
207
+
208
+ return {
209
+ android_package: appPackage?.[1] ?? "",
210
+ min_sdk: parseInt(minSdk?.[1] ?? "0", 10),
211
+ version_code: parseInt(versionCode?.[1] ?? "0", 10),
212
+ permissions: permissionArray,
213
+ locales: localeArray,
214
+ };
215
+ };
216
+
217
+ type SaveToConfigArgs = {
218
+ publisher?: Pick<Publisher, "address">;
219
+ app?: Pick<App, "address">;
220
+ release?: Pick<Release, "address" | "version">;
221
+ };
222
+
223
+ export const saveToConfig = async ({
224
+ publisher,
225
+ app,
226
+ release,
227
+ }: SaveToConfigArgs) => {
228
+ const currentConfig = await getConfigFile();
229
+
230
+ delete currentConfig.publisher.icon;
231
+ delete currentConfig.app.icon;
232
+
233
+ const newConfig: CLIConfig = {
234
+ publisher: {
235
+ ...currentConfig.publisher,
236
+ address: publisher?.address ?? currentConfig.publisher.address,
237
+ },
238
+ app: {
239
+ ...currentConfig.app,
240
+ address: app?.address ?? currentConfig.app.address,
241
+ },
242
+ release: {
243
+ ...currentConfig.release,
244
+ address: release?.address ?? currentConfig.release.address,
245
+ version: release?.version ?? currentConfig.release.version,
246
+ },
247
+ solana_mobile_dapp_publisher_portal:
248
+ currentConfig.solana_mobile_dapp_publisher_portal,
249
+ isValid: currentConfig.isValid,
250
+ };
251
+
252
+ // TODO(jon): Verify the contents of the YAML file
253
+ fs.writeFileSync(`${process.cwd()}/config.yaml`, dump(newConfig));
254
+ };
255
+
256
+ export const getMetaplexInstance = (
257
+ connection: Connection,
258
+ keypair: Keypair
259
+ ) => {
260
+ const metaplex = Metaplex.make(connection).use(keypairIdentity(keypair));
261
+
262
+ const bundlrStorageDriver = connection.rpcEndpoint.includes("devnet")
263
+ ? new BundlrStorageDriver(metaplex, {
264
+ address: "https://devnet.bundlr.network",
265
+ providerUrl: "https://api.devnet.solana.com",
266
+ })
267
+ : new BundlrStorageDriver(metaplex);
268
+
269
+ metaplex.storage().setDriver(
270
+ new CachedStorageDriver(bundlrStorageDriver, {
271
+ assetManifestPath: "./.asset-manifest.json",
272
+ })
273
+ );
274
+ return metaplex;
275
+ };