@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.
- package/LICENSE +201 -0
- package/README.md +142 -0
- package/bin/dapp-store.js +3 -0
- package/lib/esm/commands/create/app.js +37 -0
- package/lib/esm/commands/create/app.js.map +1 -0
- package/lib/esm/commands/create/index.js +44 -0
- package/lib/esm/commands/create/index.js.map +1 -0
- package/lib/esm/commands/create/publisher.js +33 -0
- package/lib/esm/commands/create/publisher.js.map +1 -0
- package/lib/esm/commands/create/release.js +48 -0
- package/lib/esm/commands/create/release.js.map +1 -0
- package/lib/esm/commands/index.js +4 -0
- package/lib/esm/commands/index.js.map +1 -0
- package/lib/esm/commands/publish/index.js +25 -0
- package/lib/esm/commands/publish/index.js.map +1 -0
- package/lib/esm/commands/publish/remove.js +20 -0
- package/lib/esm/commands/publish/remove.js.map +1 -0
- package/lib/esm/commands/publish/submit.js +25 -0
- package/lib/esm/commands/publish/submit.js.map +1 -0
- package/lib/esm/commands/publish/support.js +20 -0
- package/lib/esm/commands/publish/support.js.map +1 -0
- package/lib/esm/commands/publish/update.js +26 -0
- package/lib/esm/commands/publish/update.js.map +1 -0
- package/lib/esm/commands/validate.js +40 -0
- package/lib/esm/commands/validate.js.map +1 -0
- package/lib/esm/config/index.js +19 -0
- package/lib/esm/config/index.js.map +1 -0
- package/lib/esm/config/schema.json +228 -0
- package/lib/esm/index.js +259 -0
- package/lib/esm/index.js.map +1 -0
- package/lib/esm/upload/CachedStorageDriver.js +64 -0
- package/lib/esm/upload/CachedStorageDriver.js.map +1 -0
- package/lib/esm/upload/index.js +2 -0
- package/lib/esm/upload/index.js.map +1 -0
- package/lib/esm/utils.js +171 -0
- package/lib/esm/utils.js.map +1 -0
- package/lib/types/commands/create/app.d.ts +12 -0
- package/lib/types/commands/create/app.d.ts.map +1 -0
- package/lib/types/commands/create/index.d.ts +4 -0
- package/lib/types/commands/create/index.d.ts.map +1 -0
- package/lib/types/commands/create/publisher.d.ts +9 -0
- package/lib/types/commands/create/publisher.d.ts.map +1 -0
- package/lib/types/commands/create/release.d.ts +14 -0
- package/lib/types/commands/create/release.d.ts.map +1 -0
- package/lib/types/commands/index.d.ts +4 -0
- package/lib/types/commands/index.d.ts.map +1 -0
- package/lib/types/commands/publish/index.d.ts +5 -0
- package/lib/types/commands/publish/index.d.ts.map +1 -0
- package/lib/types/commands/publish/remove.d.ts +12 -0
- package/lib/types/commands/publish/remove.d.ts.map +1 -0
- package/lib/types/commands/publish/submit.d.ts +12 -0
- package/lib/types/commands/publish/submit.d.ts.map +1 -0
- package/lib/types/commands/publish/support.d.ts +12 -0
- package/lib/types/commands/publish/support.d.ts.map +1 -0
- package/lib/types/commands/publish/update.d.ts +13 -0
- package/lib/types/commands/publish/update.d.ts.map +1 -0
- package/lib/types/commands/validate.d.ts +6 -0
- package/lib/types/commands/validate.d.ts.map +1 -0
- package/lib/types/config/index.d.ts +10 -0
- package/lib/types/config/index.d.ts.map +1 -0
- package/lib/types/index.d.ts +2 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/upload/CachedStorageDriver.d.ts +30 -0
- package/lib/types/upload/CachedStorageDriver.d.ts.map +1 -0
- package/lib/types/upload/index.d.ts +2 -0
- package/lib/types/upload/index.d.ts.map +1 -0
- package/lib/types/utils.d.ts +21 -0
- package/lib/types/utils.d.ts.map +1 -0
- package/package.json +49 -0
- package/src/commands/create/app.ts +88 -0
- package/src/commands/create/index.ts +48 -0
- package/src/commands/create/publisher.ts +78 -0
- package/src/commands/create/release.ts +107 -0
- package/src/commands/index.ts +3 -0
- package/src/commands/publish/index.ts +29 -0
- package/src/commands/publish/remove.ts +46 -0
- package/src/commands/publish/submit.ts +55 -0
- package/src/commands/publish/support.ts +46 -0
- package/src/commands/publish/update.ts +58 -0
- package/src/commands/validate.ts +67 -0
- package/src/config/index.ts +40 -0
- package/src/config/schema.json +228 -0
- package/src/index.ts +435 -0
- package/src/upload/CachedStorageDriver.ts +104 -0
- package/src/upload/index.ts +1 -0
- 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
|
+
};
|