@solana-mobile/dapp-store-cli 0.16.0 → 1.0.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 (113) hide show
  1. package/bin/dapp-store.js +3 -1
  2. package/lib/CliSetup.js +304 -505
  3. package/lib/CliUtils.js +6 -376
  4. package/lib/__tests__/CliSetupTest.js +484 -74
  5. package/lib/cli/__tests__/parseErrors.test.js +25 -0
  6. package/lib/cli/__tests__/signer.test.js +436 -0
  7. package/lib/cli/constants.js +23 -0
  8. package/lib/cli/messages.js +21 -0
  9. package/lib/cli/parseErrors.js +41 -0
  10. package/lib/{commands/publish/PublishCliSupport.js → cli/selfUpdate.js} +72 -38
  11. package/lib/{commands/publish/PublishCliRemove.js → cli/signer.js} +35 -56
  12. package/lib/index.js +96 -5
  13. package/lib/package.json +5 -24
  14. package/lib/portal/__tests__/releaseMetadata.test.js +647 -0
  15. package/lib/portal/__tests__/translators.test.js +76 -0
  16. package/lib/portal/__tests__/workflowClient.test.js +457 -0
  17. package/lib/portal/attestationClient.js +143 -0
  18. package/lib/portal/files.js +64 -0
  19. package/lib/portal/http.js +364 -0
  20. package/lib/portal/records.js +64 -0
  21. package/lib/portal/releaseMetadata.js +748 -0
  22. package/lib/portal/translators.js +460 -0
  23. package/lib/portal/types.js +1 -0
  24. package/lib/portal/workflowClient.js +704 -0
  25. package/lib/publication/PublicationProgressReporter.js +1051 -0
  26. package/lib/publication/__tests__/PublicationProgressReporter.test.js +174 -0
  27. package/lib/{commands/ValidateCommand.js → publication/__tests__/fundingPreflight.test.js} +90 -66
  28. package/lib/publication/__tests__/publicationSummary.test.js +26 -0
  29. package/lib/publication/cliValidation.js +482 -0
  30. package/lib/publication/fundingPreflight.js +246 -0
  31. package/lib/publication/publicationSummary.js +99 -0
  32. package/lib/{commands/utils.js → publication/runPublicationWorkflow.js} +16 -46
  33. package/package.json +5 -24
  34. package/src/CliSetup.ts +370 -505
  35. package/src/CliUtils.ts +9 -233
  36. package/src/__tests__/CliSetupTest.ts +272 -120
  37. package/src/cli/__tests__/parseErrors.test.ts +34 -0
  38. package/src/cli/__tests__/signer.test.ts +359 -0
  39. package/src/cli/constants.ts +3 -0
  40. package/src/cli/messages.ts +27 -0
  41. package/src/cli/parseErrors.ts +62 -0
  42. package/src/cli/selfUpdate.ts +59 -0
  43. package/src/cli/signer.ts +38 -0
  44. package/src/index.ts +31 -4
  45. package/src/portal/__tests__/releaseMetadata.test.ts +508 -0
  46. package/src/portal/__tests__/translators.test.ts +82 -0
  47. package/src/portal/__tests__/workflowClient.test.ts +278 -0
  48. package/src/portal/attestationClient.ts +19 -0
  49. package/src/portal/files.ts +73 -0
  50. package/src/portal/http.ts +170 -0
  51. package/src/portal/records.ts +38 -0
  52. package/src/portal/releaseMetadata.ts +489 -0
  53. package/src/portal/translators.ts +750 -0
  54. package/src/portal/types.ts +27 -0
  55. package/src/portal/workflowClient.ts +575 -0
  56. package/src/publication/PublicationProgressReporter.ts +1026 -0
  57. package/src/publication/__tests__/PublicationProgressReporter.test.ts +210 -0
  58. package/src/publication/__tests__/fundingPreflight.test.ts +78 -0
  59. package/src/publication/__tests__/publicationSummary.test.ts +30 -0
  60. package/src/publication/cliValidation.ts +264 -0
  61. package/src/publication/fundingPreflight.ts +123 -0
  62. package/src/publication/publicationSummary.ts +26 -0
  63. package/src/publication/runPublicationWorkflow.ts +46 -0
  64. package/lib/commands/create/CreateCliApp.js +0 -223
  65. package/lib/commands/create/CreateCliRelease.js +0 -290
  66. package/lib/commands/create/index.js +0 -40
  67. package/lib/commands/index.js +0 -3
  68. package/lib/commands/publish/PublishCliSubmit.js +0 -208
  69. package/lib/commands/publish/PublishCliUpdate.js +0 -211
  70. package/lib/commands/publish/index.js +0 -22
  71. package/lib/commands/scaffolding/ScaffoldInit.js +0 -15
  72. package/lib/commands/scaffolding/index.js +0 -1
  73. package/lib/config/EnvVariables.js +0 -59
  74. package/lib/config/PublishDetails.js +0 -915
  75. package/lib/config/S3StorageManager.js +0 -93
  76. package/lib/config/index.js +0 -2
  77. package/lib/generated/config_obj.json +0 -1
  78. package/lib/generated/config_schema.json +0 -1
  79. package/lib/prebuild_schema/publishing_source.yaml +0 -64
  80. package/lib/prebuild_schema/schemagen.js +0 -25
  81. package/lib/upload/CachedStorageDriver.js +0 -458
  82. package/lib/upload/TurboStorageDriver.js +0 -718
  83. package/lib/upload/__tests__/CachedStorageDriver.test.js +0 -437
  84. package/lib/upload/__tests__/TurboStorageDriver.test.js +0 -17
  85. package/lib/upload/__tests__/contentGateway.test.js +0 -17
  86. package/lib/upload/contentGateway.js +0 -23
  87. package/lib/upload/index.js +0 -2
  88. package/src/commands/ValidateCommand.ts +0 -82
  89. package/src/commands/create/CreateCliApp.ts +0 -93
  90. package/src/commands/create/CreateCliRelease.ts +0 -149
  91. package/src/commands/create/index.ts +0 -47
  92. package/src/commands/index.ts +0 -3
  93. package/src/commands/publish/PublishCliRemove.ts +0 -66
  94. package/src/commands/publish/PublishCliSubmit.ts +0 -93
  95. package/src/commands/publish/PublishCliSupport.ts +0 -66
  96. package/src/commands/publish/PublishCliUpdate.ts +0 -101
  97. package/src/commands/publish/index.ts +0 -29
  98. package/src/commands/scaffolding/ScaffoldInit.ts +0 -20
  99. package/src/commands/scaffolding/index.ts +0 -1
  100. package/src/commands/utils.ts +0 -33
  101. package/src/config/EnvVariables.ts +0 -39
  102. package/src/config/PublishDetails.ts +0 -456
  103. package/src/config/S3StorageManager.ts +0 -47
  104. package/src/config/index.ts +0 -2
  105. package/src/prebuild_schema/publishing_source.yaml +0 -64
  106. package/src/prebuild_schema/schemagen.js +0 -31
  107. package/src/upload/CachedStorageDriver.ts +0 -179
  108. package/src/upload/TurboStorageDriver.ts +0 -283
  109. package/src/upload/__tests__/CachedStorageDriver.test.ts +0 -246
  110. package/src/upload/__tests__/TurboStorageDriver.test.ts +0 -15
  111. package/src/upload/__tests__/contentGateway.test.ts +0 -31
  112. package/src/upload/contentGateway.ts +0 -37
  113. package/src/upload/index.ts +0 -2
@@ -1,179 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import type { MetaplexFile, StorageDriver } from "@metaplex-foundation/js";
4
- import { createHash } from "crypto";
5
- import { normalizePublicContentUrl } from "./contentGateway.js";
6
-
7
- type URI = string;
8
-
9
- type Asset = {
10
- path: string;
11
- sha256: string;
12
- uri: URI;
13
- };
14
-
15
- export type AssetManifestSchema = {
16
- schema_version: string;
17
- assets: {
18
- [path: string]: Asset;
19
- };
20
- };
21
-
22
- // TODO(jon): We need to manage the removal / replacement of assets in the manifest
23
- export class CachedStorageDriver implements StorageDriver {
24
- // NOTE: this schema version is independent of the publishing JSON schema. It should be updated
25
- // when the AssetManifestSchema or Asset types are updated.
26
- static readonly SCHEMA_VERSION = "0.1";
27
-
28
- assetManifest: AssetManifestSchema;
29
- assetManifestPath: string;
30
- storageDriver: StorageDriver;
31
-
32
- constructor(
33
- storageDriver: StorageDriver,
34
- { assetManifestPath }: { assetManifestPath: string }
35
- ) {
36
- this.assetManifestPath = assetManifestPath;
37
- this.assetManifest = this.loadAssetManifest(assetManifestPath) ?? {
38
- schema_version: CachedStorageDriver.SCHEMA_VERSION,
39
- assets: {},
40
- };
41
- this.storageDriver = storageDriver;
42
- }
43
-
44
- async getUploadPrice(bytes: number) {
45
- return this.storageDriver.getUploadPrice(bytes);
46
- }
47
-
48
- private resolveAssetManifestPath(filename = this.assetManifestPath): string {
49
- return path.resolve(process.cwd(), filename);
50
- }
51
-
52
- private normalizeAsset(
53
- filename: string,
54
- asset: unknown
55
- ): Asset | undefined {
56
- if (!asset || typeof asset !== "object") return;
57
-
58
- const candidate = asset as Partial<Asset>;
59
- const pathValue =
60
- typeof candidate.path === "string" ? candidate.path : filename;
61
-
62
- if (
63
- typeof candidate.sha256 !== "string" ||
64
- typeof candidate.uri !== "string"
65
- ) {
66
- return;
67
- }
68
-
69
- return {
70
- path: pathValue,
71
- sha256: candidate.sha256,
72
- uri: candidate.uri,
73
- };
74
- }
75
-
76
- private normalizeAssetManifest(
77
- assetManifest: Partial<AssetManifestSchema> | undefined
78
- ): AssetManifestSchema | undefined {
79
- if (!assetManifest || typeof assetManifest !== "object") return;
80
-
81
- const assets: Record<string, Asset> = {};
82
- const assetEntries =
83
- assetManifest.assets && typeof assetManifest.assets === "object"
84
- ? Object.entries(assetManifest.assets)
85
- : [];
86
-
87
- for (const [filename, asset] of assetEntries) {
88
- const normalizedAsset = this.normalizeAsset(filename, asset);
89
- if (normalizedAsset) {
90
- assets[filename] = normalizedAsset;
91
- }
92
- }
93
-
94
- return {
95
- schema_version:
96
- typeof assetManifest.schema_version === "string"
97
- ? assetManifest.schema_version
98
- : CachedStorageDriver.SCHEMA_VERSION,
99
- assets,
100
- };
101
- }
102
-
103
- private async writeAssetManifest(): Promise<void> {
104
- const normalizedAssetManifest = this.normalizeAssetManifest(
105
- this.assetManifest
106
- );
107
-
108
- if (!normalizedAssetManifest) {
109
- throw new Error("Asset manifest is not serializable");
110
- }
111
-
112
- this.assetManifest = normalizedAssetManifest;
113
- await fs.promises.writeFile(
114
- this.resolveAssetManifestPath(),
115
- JSON.stringify(this.assetManifest, null, 2),
116
- "utf-8"
117
- );
118
- }
119
-
120
- loadAssetManifest(filename: string): AssetManifestSchema | undefined {
121
- try {
122
- return this.normalizeAssetManifest(
123
- JSON.parse(fs.readFileSync(this.resolveAssetManifestPath(filename), "utf-8"))
124
- );
125
- } catch (error) {
126
- console.warn(`Failed opening ${filename}; initializing with a blank asset manifest`);
127
- return;
128
- }
129
- }
130
-
131
- uploadedAsset(filename: string, { sha256 }: { sha256: string }) {
132
- if (this.assetManifest.assets[filename]?.sha256 === sha256) {
133
- return this.assetManifest.assets[filename];
134
- }
135
- return null;
136
- }
137
-
138
- async upload(file: MetaplexFile): Promise<string> {
139
- // `inline.json` is the NFT-related metadata. This data is not stable so we'll skip the caching step
140
- if (file.fileName === "inline.json") {
141
- return normalizePublicContentUrl(await this.storageDriver.upload(file));
142
- }
143
- const hash = createHash("sha256").update(file.buffer).digest("base64");
144
-
145
- const uploadedAsset = this.uploadedAsset(file.fileName, { sha256: hash });
146
- if (uploadedAsset) {
147
- const normalizedUri = normalizePublicContentUrl(uploadedAsset.uri);
148
- if (normalizedUri !== uploadedAsset.uri) {
149
- uploadedAsset.uri = normalizedUri;
150
- try {
151
- await this.writeAssetManifest();
152
- } catch (error) {
153
- const message = error instanceof Error ? error.message : String(error);
154
- console.warn(
155
- `Failed to rewrite ${this.assetManifestPath}; continuing with normalized URL: ${message}`
156
- );
157
- }
158
- }
159
- console.log(
160
- `Asset ${file.fileName} already uploaded at ${normalizedUri}`
161
- );
162
- return normalizedUri;
163
- }
164
-
165
- console.log(`Uploading ${file.fileName}`);
166
- const uri = normalizePublicContentUrl(await this.storageDriver.upload(file));
167
-
168
- this.assetManifest.assets[file.fileName] = {
169
- path: file.fileName,
170
- sha256: hash,
171
- uri,
172
- };
173
-
174
- await this.writeAssetManifest();
175
- console.log(`${file.fileName} uploaded at ${uri}`)
176
-
177
- return uri;
178
- }
179
- }
@@ -1,283 +0,0 @@
1
- import type { Keypair } from "@solana/web3.js";
2
- import type { MetaplexFile } from "@metaplex-foundation/js";
3
- import { TurboFactory, lamportToTokenAmount } from "@ardrive/turbo-sdk";
4
- import bs58 from "bs58";
5
- import debugModule from "debug";
6
- import { buildPublicContentUrl, type StorageNetwork } from "./contentGateway.js";
7
-
8
- const debug = debugModule("cli:turbo-storage");
9
-
10
- interface TurboClient {
11
- getUploadCosts(args: {
12
- bytes: number[];
13
- }): Promise<Array<{ winc: string | number | bigint }>>;
14
- getBalance(): Promise<{ winc: string | number | bigint }>;
15
- getWincForToken?(args: {
16
- tokenAmount: number;
17
- }): Promise<{ winc: string | number | bigint }>;
18
- topUpWithTokens?(args: { tokenAmount: string | number }): Promise<unknown>;
19
- uploadFile(args: {
20
- fileStreamFactory: () => Buffer;
21
- fileSizeFactory: () => number;
22
- dataItemOpts?: { tags?: Array<{ name: string; value: string }> };
23
- }): Promise<{ id: string }>;
24
- }
25
-
26
- type TurboServiceConfig = {
27
- gatewayUrl?: string;
28
- paymentServiceConfig?: { url: string };
29
- uploadServiceConfig?: { url: string };
30
- };
31
-
32
- const SOL_IN_LAMPORTS = 1_000_000_000;
33
- const MIN_TOP_UP_LAMPORTS = 1_000_000;
34
- const MIN_TOP_UP_SOL = MIN_TOP_UP_LAMPORTS / SOL_IN_LAMPORTS;
35
-
36
- const CONSTANTS = {
37
- FREE_UPLOAD_LIMIT: 97_280, // 95 KiB
38
- UPLOAD_DELAY_MS: 2000,
39
- MAX_RETRIES: 5,
40
- SOL_IN_LAMPORTS,
41
- MIN_TOP_UP_SOL,
42
- MIN_TOP_UP_LAMPORTS,
43
- BACKOFF: {
44
- BASE_MS: 500,
45
- MAX_MS: 8000,
46
- },
47
- SERVICE_URLS: {
48
- devnet: {
49
- gatewayUrl: "https://api.devnet.solana.com",
50
- uploadServiceConfig: { url: "https://upload.ardrive.dev" },
51
- paymentServiceConfig: { url: "https://payment.ardrive.dev" },
52
- },
53
- },
54
- } as const;
55
-
56
- const delay = (ms: number): Promise<void> =>
57
- new Promise((resolve) => setTimeout(resolve, ms));
58
-
59
- export class TurboStorageDriver {
60
- private turbo: TurboClient;
61
- private bufferPercentage: number;
62
- private network: StorageNetwork;
63
-
64
- private uploadQueue: Array<{
65
- file: MetaplexFile;
66
- resolve: (url: string) => void;
67
- reject: (error: Error) => void;
68
- }> = [];
69
- private isProcessingQueue = false;
70
-
71
- constructor(
72
- keypair: Keypair,
73
- network: StorageNetwork = "mainnet",
74
- bufferPercentage = 20
75
- ) {
76
- this.network = network;
77
- this.bufferPercentage = bufferPercentage;
78
-
79
- this.turbo = TurboFactory.authenticated({
80
- privateKey: bs58.encode(keypair.secretKey),
81
- token: "solana",
82
- ...getTurboServiceConfig(network),
83
- }) as TurboClient;
84
- }
85
-
86
- async getUploadPrice(bytes: number): Promise<bigint> {
87
- if (bytes <= CONSTANTS.FREE_UPLOAD_LIMIT) return BigInt(0);
88
-
89
- const [cost] = await this.turbo.getUploadCosts({ bytes: [bytes] });
90
- const base = BigInt(String(cost.winc));
91
- return (base * BigInt(100 + this.bufferPercentage)) / BigInt(100);
92
- }
93
-
94
- private async withRetry<T>(
95
- operation: () => Promise<T>,
96
- isRetriable: (error: string) => boolean = (msg) =>
97
- msg.includes("429") || msg.includes("Too Many Requests")
98
- ): Promise<T> {
99
- for (let retry = 0; retry <= CONSTANTS.MAX_RETRIES; retry++) {
100
- try {
101
- return await operation();
102
- } catch (error) {
103
- const errorMessage =
104
- error instanceof Error ? error.message : String(error);
105
-
106
- if (retry < CONSTANTS.MAX_RETRIES && isRetriable(errorMessage)) {
107
- const delayMs = Math.min(
108
- CONSTANTS.BACKOFF.BASE_MS * Math.pow(2, retry),
109
- CONSTANTS.BACKOFF.MAX_MS
110
- );
111
- console.log(
112
- `Rate limited, retrying after ${delayMs}ms (attempt ${retry + 1}/${
113
- CONSTANTS.MAX_RETRIES
114
- })...`
115
- );
116
- await delay(delayMs);
117
- continue;
118
- }
119
- throw error;
120
- }
121
- }
122
- throw new Error("Max retries exceeded");
123
- }
124
-
125
- private formatInsufficientFundsError(errorMessage: string): void {
126
- const match = errorMessage.match(/insufficient lamports (\d+), need (\d+)/);
127
- if (!match) return;
128
-
129
- const [current, needed] = [BigInt(match[1]), BigInt(match[2])];
130
- const [currentSOL, neededSOL] = [
131
- Number(current) / 1e9,
132
- Number(needed) / 1e9,
133
- ];
134
-
135
- console.error(`\nInsufficient SOL balance for top-up:`);
136
- console.error(` Current: ${currentSOL.toFixed(9)} SOL`);
137
- console.error(` Required: ${neededSOL.toFixed(9)} SOL`);
138
- console.error(` Shortfall: ${(neededSOL - currentSOL).toFixed(9)} SOL\n`);
139
- }
140
-
141
- private async topUpCredits(wincAmount: bigint): Promise<void> {
142
- if (wincAmount === 0n) {
143
- debug("No Winston Credits requested; skipping top-up.");
144
- return;
145
- }
146
-
147
- try {
148
- await this.withRetry(async () => {
149
- const exchangeRate = await this.turbo.getWincForToken?.({
150
- tokenAmount: CONSTANTS.SOL_IN_LAMPORTS,
151
- });
152
-
153
- if (!exchangeRate) {
154
- throw new Error("Unable to get Winston Credits exchange rate");
155
- }
156
-
157
- const wincPerSol = BigInt(String(exchangeRate.winc));
158
- if (wincPerSol <= 0n) {
159
- throw new Error("Invalid Winston Credits exchange rate");
160
- }
161
-
162
- const solInLamports = BigInt(CONSTANTS.SOL_IN_LAMPORTS);
163
- const numerator = wincAmount * solInLamports;
164
- const lamportsCalculated =
165
- (numerator + (wincPerSol - 1n)) / wincPerSol;
166
-
167
- const minLamports = BigInt(CONSTANTS.MIN_TOP_UP_LAMPORTS);
168
- const lamportsToUse =
169
- lamportsCalculated < minLamports ? minLamports : lamportsCalculated;
170
-
171
- if (lamportsToUse > lamportsCalculated) {
172
- debug(
173
- `Applying minimum top-up of ${CONSTANTS.MIN_TOP_UP_SOL} SOL (${CONSTANTS.MIN_TOP_UP_LAMPORTS} lamports)`
174
- );
175
- }
176
- const solAmount =
177
- Number(lamportsToUse) / CONSTANTS.SOL_IN_LAMPORTS;
178
-
179
- debug(
180
- `Buying at least ${wincAmount} Winston Credits (~${solAmount.toFixed(9)} SOL / ${lamportsToUse} lamports)`
181
- );
182
-
183
- await this.turbo.topUpWithTokens?.({
184
- tokenAmount: String(
185
- lamportToTokenAmount(lamportsToUse.toString())
186
- ),
187
- });
188
-
189
- debug(`Top-up initiated for ${wincAmount} Winston Credits`);
190
- });
191
- } catch (error) {
192
- const errorMessage =
193
- error instanceof Error ? error.message : String(error);
194
- debug("Top-up failed:", error);
195
-
196
- if (errorMessage.includes("insufficient lamports")) {
197
- this.formatInsufficientFundsError(errorMessage);
198
- }
199
-
200
- throw new Error(
201
- `Failed to top up ${wincAmount} Winston Credits: ${errorMessage}`
202
- );
203
- }
204
- }
205
-
206
- private async checkBalanceAndTopUp(requiredWinc: bigint): Promise<void> {
207
- if (requiredWinc === BigInt(0)) return;
208
-
209
- const current = BigInt(String((await this.turbo.getBalance()).winc));
210
-
211
- if (current >= requiredWinc) {
212
- debug(
213
- `Sufficient balance: ${current} Winston Credits (required: ${requiredWinc})`
214
- );
215
- return;
216
- }
217
-
218
- const deficit = requiredWinc - current;
219
- debug(
220
- `Current: ${current}, Required: ${requiredWinc}, Topping up: ${deficit}`
221
- );
222
- await this.topUpCredits(deficit);
223
- }
224
-
225
- private async processQueue(): Promise<void> {
226
- if (this.isProcessingQueue || !this.uploadQueue.length) return;
227
-
228
- this.isProcessingQueue = true;
229
-
230
- while (this.uploadQueue.length > 0) {
231
- const item = this.uploadQueue.shift();
232
- if (!item) continue;
233
-
234
- try {
235
- debug(
236
- `Processing upload for ${item.file.fileName} (${item.file.buffer.length} bytes)`
237
- );
238
-
239
- const estimated = await this.getUploadPrice(item.file.buffer.length);
240
- await this.checkBalanceAndTopUp(estimated);
241
-
242
- const tags = [...(item.file.tags ?? [])];
243
- if (item.file.contentType) {
244
- tags.push({ name: "Content-Type", value: item.file.contentType });
245
- }
246
-
247
- const uploadResult = await this.turbo.uploadFile({
248
- fileStreamFactory: () => item.file.buffer,
249
- fileSizeFactory: () => item.file.buffer.byteLength,
250
- dataItemOpts: { tags },
251
- });
252
-
253
- const url = buildPublicContentUrl(uploadResult.id, this.network);
254
- debug(`Upload complete: ${url}`);
255
- item.resolve(url);
256
-
257
- if (this.uploadQueue.length > 0) {
258
- debug(`Waiting ${CONSTANTS.UPLOAD_DELAY_MS}ms before next upload...`);
259
- await delay(CONSTANTS.UPLOAD_DELAY_MS);
260
- }
261
- } catch (error) {
262
- item.reject(error instanceof Error ? error : new Error(String(error)));
263
- }
264
- }
265
-
266
- this.isProcessingQueue = false;
267
- }
268
-
269
- async upload(file: MetaplexFile): Promise<string> {
270
- return new Promise((resolve, reject) => {
271
- debug(
272
- `Queueing upload for ${file.fileName} (${file.buffer.length} bytes)`
273
- );
274
- this.uploadQueue.push({ file, resolve, reject });
275
- this.processQueue().catch(reject);
276
- });
277
- }
278
- }
279
-
280
- export const getTurboServiceConfig = (
281
- network: StorageNetwork
282
- ): TurboServiceConfig =>
283
- network === "devnet" ? CONSTANTS.SERVICE_URLS.devnet : {};
@@ -1,246 +0,0 @@
1
- import fs from "fs";
2
- import os from "os";
3
- import path from "path";
4
- import { createHash } from "crypto";
5
- import { jest } from "@jest/globals";
6
- import type { MetaplexFile, StorageDriver } from "@metaplex-foundation/js";
7
- import { CachedStorageDriver } from "../CachedStorageDriver";
8
-
9
- type MockStorageDriver = Pick<StorageDriver, "getUploadPrice" | "upload">;
10
-
11
- describe("CachedStorageDriver", () => {
12
- const originalCwd = process.cwd();
13
- let tempDir: string;
14
-
15
- beforeEach(() => {
16
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "cached-storage-driver-"));
17
- process.chdir(tempDir);
18
- });
19
-
20
- afterEach(() => {
21
- process.chdir(originalCwd);
22
- fs.rmSync(tempDir, { recursive: true, force: true });
23
- });
24
-
25
- test("normalizes a cached legacy arweave URL and persists the rewritten manifest", async () => {
26
- const file = makeMetaplexFile("icon.png", Buffer.from("cached-asset"));
27
- const hash = hashBuffer(file.buffer);
28
- const manifestPath = ".asset-manifest.json";
29
-
30
- fs.writeFileSync(
31
- path.join(tempDir, manifestPath),
32
- JSON.stringify(
33
- {
34
- assets: {
35
- [file.fileName]: {
36
- path: file.fileName,
37
- sha256: hash,
38
- uri: "https://arweave.net/legacy-id",
39
- },
40
- },
41
- },
42
- null,
43
- 2
44
- )
45
- );
46
-
47
- let uploadCalls = 0;
48
- const storageDriver: MockStorageDriver = {
49
- getUploadPrice: async (_bytes: number) => {
50
- throw new Error("getUploadPrice should not be called in this test");
51
- },
52
- upload: async (_file: MetaplexFile) => {
53
- uploadCalls += 1;
54
- throw new Error("upload should not be called in this test");
55
- },
56
- };
57
-
58
- const driver = new CachedStorageDriver(storageDriver as StorageDriver, {
59
- assetManifestPath: manifestPath,
60
- });
61
-
62
- await expect(driver.upload(file)).resolves.toBe(
63
- "https://dappstorecontent.com/legacy-id"
64
- );
65
- expect(uploadCalls).toBe(0);
66
-
67
- const rewrittenManifest = JSON.parse(
68
- fs.readFileSync(path.join(tempDir, manifestPath), "utf-8")
69
- );
70
- expect(rewrittenManifest.schema_version).toBe("0.1");
71
- expect(rewrittenManifest.assets[file.fileName].uri).toBe(
72
- "https://dappstorecontent.com/legacy-id"
73
- );
74
- });
75
-
76
- test("supports absolute asset manifest paths", async () => {
77
- const file = makeMetaplexFile("absolute-icon.png", Buffer.from("cached-asset"));
78
- const hash = hashBuffer(file.buffer);
79
- const manifestPath = path.join(tempDir, ".asset-manifest.json");
80
-
81
- fs.writeFileSync(
82
- manifestPath,
83
- JSON.stringify(
84
- {
85
- assets: {
86
- [file.fileName]: {
87
- path: file.fileName,
88
- sha256: hash,
89
- uri: "https://arweave.net/absolute-id",
90
- },
91
- },
92
- },
93
- null,
94
- 2
95
- )
96
- );
97
-
98
- let uploadCalls = 0;
99
- const storageDriver: MockStorageDriver = {
100
- getUploadPrice: async (_bytes: number) => {
101
- throw new Error("getUploadPrice should not be called in this test");
102
- },
103
- upload: async (_file: MetaplexFile) => {
104
- uploadCalls += 1;
105
- throw new Error("upload should not be called in this test");
106
- },
107
- };
108
-
109
- const driver = new CachedStorageDriver(storageDriver as StorageDriver, {
110
- assetManifestPath: manifestPath,
111
- });
112
-
113
- await expect(driver.upload(file)).resolves.toBe(
114
- "https://dappstorecontent.com/absolute-id"
115
- );
116
- expect(uploadCalls).toBe(0);
117
-
118
- const rewrittenManifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
119
- expect(rewrittenManifest.assets[file.fileName].uri).toBe(
120
- "https://dappstorecontent.com/absolute-id"
121
- );
122
- });
123
-
124
- test("normalizes fresh uploads before persisting them in the manifest", async () => {
125
- const file = makeMetaplexFile("banner.png", Buffer.from("fresh-asset"));
126
- const manifestPath = ".asset-manifest.json";
127
-
128
- let uploadCalls = 0;
129
- const storageDriver: MockStorageDriver = {
130
- getUploadPrice: async (_bytes: number) => {
131
- throw new Error("getUploadPrice should not be called in this test");
132
- },
133
- upload: async (_file: MetaplexFile) => {
134
- uploadCalls += 1;
135
- return "https://arweave.com/fresh-id";
136
- },
137
- };
138
-
139
- const driver = new CachedStorageDriver(storageDriver as StorageDriver, {
140
- assetManifestPath: manifestPath,
141
- });
142
-
143
- await expect(driver.upload(file)).resolves.toBe(
144
- "https://dappstorecontent.com/fresh-id"
145
- );
146
- expect(uploadCalls).toBe(1);
147
-
148
- const persistedManifest = JSON.parse(
149
- fs.readFileSync(path.join(tempDir, manifestPath), "utf-8")
150
- );
151
- expect(persistedManifest.schema_version).toBe("0.1");
152
- expect(persistedManifest.assets[file.fileName].uri).toBe(
153
- "https://dappstorecontent.com/fresh-id"
154
- );
155
- });
156
-
157
- test("normalizes inline metadata uploads without caching them", async () => {
158
- const file = makeMetaplexFile("inline.json", Buffer.from("{}"));
159
- let uploadCalls = 0;
160
- const storageDriver: MockStorageDriver = {
161
- getUploadPrice: async (_bytes: number) => {
162
- throw new Error("getUploadPrice should not be called in this test");
163
- },
164
- upload: async (_file: MetaplexFile) => {
165
- uploadCalls += 1;
166
- return "https://arweave.net/metadata-id";
167
- },
168
- };
169
-
170
- const driver = new CachedStorageDriver(storageDriver as StorageDriver, {
171
- assetManifestPath: ".asset-manifest.json",
172
- });
173
-
174
- await expect(driver.upload(file)).resolves.toBe(
175
- "https://dappstorecontent.com/metadata-id"
176
- );
177
- expect(uploadCalls).toBe(1);
178
- expect(fs.existsSync(path.join(tempDir, ".asset-manifest.json"))).toBe(
179
- false
180
- );
181
- });
182
-
183
- test("returns a normalized cached URL even when manifest rewrite persistence fails", async () => {
184
- const file = makeMetaplexFile("icon.png", Buffer.from("cached-asset"));
185
- const hash = hashBuffer(file.buffer);
186
- const manifestPath = ".asset-manifest.json";
187
-
188
- fs.writeFileSync(
189
- path.join(tempDir, manifestPath),
190
- JSON.stringify(
191
- {
192
- assets: {
193
- [file.fileName]: {
194
- path: file.fileName,
195
- sha256: hash,
196
- uri: "https://arweave.net/rewrite-failure-id",
197
- },
198
- },
199
- },
200
- null,
201
- 2
202
- )
203
- );
204
-
205
- const writeFileSpy = jest
206
- .spyOn(fs.promises, "writeFile")
207
- .mockRejectedValueOnce(new Error("disk full"));
208
- const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => undefined);
209
-
210
- const storageDriver: MockStorageDriver = {
211
- getUploadPrice: async (_bytes: number) => {
212
- throw new Error("getUploadPrice should not be called in this test");
213
- },
214
- upload: async (_file: MetaplexFile) => {
215
- throw new Error("upload should not be called in this test");
216
- },
217
- };
218
-
219
- const driver = new CachedStorageDriver(storageDriver as StorageDriver, {
220
- assetManifestPath: manifestPath,
221
- });
222
-
223
- try {
224
- await expect(driver.upload(file)).resolves.toBe(
225
- "https://dappstorecontent.com/rewrite-failure-id"
226
- );
227
- expect(warnSpy).toHaveBeenCalledWith(
228
- expect.stringContaining(
229
- "Failed to rewrite .asset-manifest.json; continuing with normalized URL"
230
- )
231
- );
232
- } finally {
233
- writeFileSpy.mockRestore();
234
- warnSpy.mockRestore();
235
- }
236
- });
237
- });
238
-
239
- const makeMetaplexFile = (fileName: string, buffer: Buffer): MetaplexFile =>
240
- ({
241
- fileName,
242
- buffer,
243
- }) as MetaplexFile;
244
-
245
- const hashBuffer = (buffer: Buffer): string =>
246
- createHash("sha256").update(buffer).digest("base64");