@solana-mobile/dapp-store-cli 0.14.0 → 0.16.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/lib/CliUtils.js +1 -1
- package/lib/package.json +3 -3
- package/lib/upload/CachedStorageDriver.js +194 -29
- package/lib/upload/TurboStorageDriver.js +44 -22
- package/lib/upload/__tests__/CachedStorageDriver.test.js +437 -0
- package/lib/upload/__tests__/TurboStorageDriver.test.js +17 -0
- package/lib/upload/__tests__/contentGateway.test.js +17 -0
- package/lib/upload/contentGateway.js +23 -0
- package/package.json +3 -3
- package/src/CliUtils.ts +1 -1
- package/src/upload/CachedStorageDriver.ts +93 -13
- package/src/upload/TurboStorageDriver.ts +58 -23
- package/src/upload/__tests__/CachedStorageDriver.test.ts +246 -0
- package/src/upload/__tests__/TurboStorageDriver.test.ts +15 -0
- package/src/upload/__tests__/contentGateway.test.ts +31 -0
- package/src/upload/contentGateway.ts +37 -0
|
@@ -3,6 +3,7 @@ import type { MetaplexFile } from "@metaplex-foundation/js";
|
|
|
3
3
|
import { TurboFactory, lamportToTokenAmount } from "@ardrive/turbo-sdk";
|
|
4
4
|
import bs58 from "bs58";
|
|
5
5
|
import debugModule from "debug";
|
|
6
|
+
import { buildPublicContentUrl, type StorageNetwork } from "./contentGateway.js";
|
|
6
7
|
|
|
7
8
|
const debug = debugModule("cli:turbo-storage");
|
|
8
9
|
|
|
@@ -22,18 +23,33 @@ interface TurboClient {
|
|
|
22
23
|
}): Promise<{ id: string }>;
|
|
23
24
|
}
|
|
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
|
+
|
|
25
36
|
const CONSTANTS = {
|
|
26
37
|
FREE_UPLOAD_LIMIT: 97_280, // 95 KiB
|
|
27
38
|
UPLOAD_DELAY_MS: 2000,
|
|
28
39
|
MAX_RETRIES: 5,
|
|
29
|
-
SOL_IN_LAMPORTS
|
|
40
|
+
SOL_IN_LAMPORTS,
|
|
41
|
+
MIN_TOP_UP_SOL,
|
|
42
|
+
MIN_TOP_UP_LAMPORTS,
|
|
30
43
|
BACKOFF: {
|
|
31
44
|
BASE_MS: 500,
|
|
32
45
|
MAX_MS: 8000,
|
|
33
46
|
},
|
|
34
|
-
|
|
35
|
-
devnet:
|
|
36
|
-
|
|
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
|
+
},
|
|
37
53
|
},
|
|
38
54
|
} as const;
|
|
39
55
|
|
|
@@ -43,7 +59,7 @@ const delay = (ms: number): Promise<void> =>
|
|
|
43
59
|
export class TurboStorageDriver {
|
|
44
60
|
private turbo: TurboClient;
|
|
45
61
|
private bufferPercentage: number;
|
|
46
|
-
private network:
|
|
62
|
+
private network: StorageNetwork;
|
|
47
63
|
|
|
48
64
|
private uploadQueue: Array<{
|
|
49
65
|
file: MetaplexFile;
|
|
@@ -54,7 +70,7 @@ export class TurboStorageDriver {
|
|
|
54
70
|
|
|
55
71
|
constructor(
|
|
56
72
|
keypair: Keypair,
|
|
57
|
-
network:
|
|
73
|
+
network: StorageNetwork = "mainnet",
|
|
58
74
|
bufferPercentage = 20
|
|
59
75
|
) {
|
|
60
76
|
this.network = network;
|
|
@@ -63,18 +79,10 @@ export class TurboStorageDriver {
|
|
|
63
79
|
this.turbo = TurboFactory.authenticated({
|
|
64
80
|
privateKey: bs58.encode(keypair.secretKey),
|
|
65
81
|
token: "solana",
|
|
66
|
-
...
|
|
82
|
+
...getTurboServiceConfig(network),
|
|
67
83
|
}) as TurboClient;
|
|
68
84
|
}
|
|
69
85
|
|
|
70
|
-
private getServiceUrls(isDev: boolean) {
|
|
71
|
-
const base = isDev ? "ardrive.dev" : "ardrive.io";
|
|
72
|
-
return {
|
|
73
|
-
uploadUrl: `https://upload.${base}`,
|
|
74
|
-
paymentUrl: `https://payment.${base}`,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
86
|
async getUploadPrice(bytes: number): Promise<bigint> {
|
|
79
87
|
if (bytes <= CONSTANTS.FREE_UPLOAD_LIMIT) return BigInt(0);
|
|
80
88
|
|
|
@@ -131,6 +139,11 @@ export class TurboStorageDriver {
|
|
|
131
139
|
}
|
|
132
140
|
|
|
133
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
|
+
|
|
134
147
|
try {
|
|
135
148
|
await this.withRetry(async () => {
|
|
136
149
|
const exchangeRate = await this.turbo.getWincForToken?.({
|
|
@@ -142,17 +155,35 @@ export class TurboStorageDriver {
|
|
|
142
155
|
}
|
|
143
156
|
|
|
144
157
|
const wincPerSol = BigInt(String(exchangeRate.winc));
|
|
145
|
-
|
|
146
|
-
|
|
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;
|
|
147
178
|
|
|
148
179
|
debug(
|
|
149
|
-
`Buying ${wincAmount} Winston Credits
|
|
150
|
-
Number(lamportsNeeded) / 1e9
|
|
151
|
-
} SOL`
|
|
180
|
+
`Buying at least ${wincAmount} Winston Credits (~${solAmount.toFixed(9)} SOL / ${lamportsToUse} lamports)`
|
|
152
181
|
);
|
|
153
182
|
|
|
154
183
|
await this.turbo.topUpWithTokens?.({
|
|
155
|
-
tokenAmount: String(
|
|
184
|
+
tokenAmount: String(
|
|
185
|
+
lamportToTokenAmount(lamportsToUse.toString())
|
|
186
|
+
),
|
|
156
187
|
});
|
|
157
188
|
|
|
158
189
|
debug(`Top-up initiated for ${wincAmount} Winston Credits`);
|
|
@@ -219,8 +250,7 @@ export class TurboStorageDriver {
|
|
|
219
250
|
dataItemOpts: { tags },
|
|
220
251
|
});
|
|
221
252
|
|
|
222
|
-
const
|
|
223
|
-
const url = `${gateway}/${uploadResult.id}`;
|
|
253
|
+
const url = buildPublicContentUrl(uploadResult.id, this.network);
|
|
224
254
|
debug(`Upload complete: ${url}`);
|
|
225
255
|
item.resolve(url);
|
|
226
256
|
|
|
@@ -246,3 +276,8 @@ export class TurboStorageDriver {
|
|
|
246
276
|
});
|
|
247
277
|
}
|
|
248
278
|
}
|
|
279
|
+
|
|
280
|
+
export const getTurboServiceConfig = (
|
|
281
|
+
network: StorageNetwork
|
|
282
|
+
): TurboServiceConfig =>
|
|
283
|
+
network === "devnet" ? CONSTANTS.SERVICE_URLS.devnet : {};
|
|
@@ -0,0 +1,246 @@
|
|
|
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");
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { getTurboServiceConfig } from "../TurboStorageDriver";
|
|
2
|
+
|
|
3
|
+
describe("TurboStorageDriver", () => {
|
|
4
|
+
test("uses the latest Turbo SDK service config shape for devnet", () => {
|
|
5
|
+
expect(getTurboServiceConfig("devnet")).toEqual({
|
|
6
|
+
gatewayUrl: "https://api.devnet.solana.com",
|
|
7
|
+
uploadServiceConfig: { url: "https://upload.ardrive.dev" },
|
|
8
|
+
paymentServiceConfig: { url: "https://payment.ardrive.dev" },
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("relies on the SDK defaults for mainnet service URLs", () => {
|
|
13
|
+
expect(getTurboServiceConfig("mainnet")).toEqual({});
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { buildPublicContentUrl, normalizePublicContentUrl } from "../contentGateway";
|
|
2
|
+
|
|
3
|
+
describe("contentGateway", () => {
|
|
4
|
+
test("builds a mainnet dappstorecontent URL from an upload id", () => {
|
|
5
|
+
expect(buildPublicContentUrl("abc123", "mainnet")).toBe(
|
|
6
|
+
"https://dappstorecontent.com/abc123"
|
|
7
|
+
);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("builds a devnet Turbo raw URL from an upload id", () => {
|
|
11
|
+
expect(buildPublicContentUrl("abc123", "devnet")).toBe(
|
|
12
|
+
"https://turbo.ardrive.dev/raw/abc123"
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("normalizes arweave.net and arweave.com URLs to dappstorecontent", () => {
|
|
17
|
+
expect(normalizePublicContentUrl("https://arweave.net/abc123")).toBe(
|
|
18
|
+
"https://dappstorecontent.com/abc123"
|
|
19
|
+
);
|
|
20
|
+
expect(
|
|
21
|
+
normalizePublicContentUrl("https://www.arweave.com/abc123?ext=png")
|
|
22
|
+
).toBe("https://dappstorecontent.com/abc123?ext=png");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("leaves non-arweave URLs unchanged", () => {
|
|
26
|
+
expect(
|
|
27
|
+
normalizePublicContentUrl("https://turbo.ardrive.dev/raw/abc123")
|
|
28
|
+
).toBe("https://turbo.ardrive.dev/raw/abc123");
|
|
29
|
+
expect(normalizePublicContentUrl("not-a-url")).toBe("not-a-url");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type StorageNetwork = "devnet" | "mainnet";
|
|
2
|
+
|
|
3
|
+
const MAINNET_PUBLIC_CONTENT_GATEWAY = "https://dappstorecontent.com";
|
|
4
|
+
const DEVNET_PUBLIC_CONTENT_GATEWAY = "https://turbo.ardrive.dev/raw";
|
|
5
|
+
|
|
6
|
+
const LEGACY_MAINNET_CONTENT_HOSTS = new Set([
|
|
7
|
+
"arweave.net",
|
|
8
|
+
"www.arweave.net",
|
|
9
|
+
"arweave.com",
|
|
10
|
+
"www.arweave.com",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
export const buildPublicContentUrl = (
|
|
14
|
+
id: string,
|
|
15
|
+
network: StorageNetwork
|
|
16
|
+
): string => {
|
|
17
|
+
const gateway =
|
|
18
|
+
network === "devnet"
|
|
19
|
+
? DEVNET_PUBLIC_CONTENT_GATEWAY
|
|
20
|
+
: MAINNET_PUBLIC_CONTENT_GATEWAY;
|
|
21
|
+
|
|
22
|
+
return `${gateway}/${id}`;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const normalizePublicContentUrl = (url: string): string => {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = new URL(url);
|
|
28
|
+
|
|
29
|
+
if (!LEGACY_MAINNET_CONTENT_HOSTS.has(parsed.host.toLowerCase())) {
|
|
30
|
+
return url;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return `${MAINNET_PUBLIC_CONTENT_GATEWAY}${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
34
|
+
} catch {
|
|
35
|
+
return url;
|
|
36
|
+
}
|
|
37
|
+
};
|