@solana-mobile/dapp-store-cli 0.15.0 → 0.16.1
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/bin/dapp-store.js +3 -1
- package/lib/CliSetup.js +304 -505
- package/lib/CliUtils.js +6 -376
- package/lib/__tests__/CliSetupTest.js +484 -74
- package/lib/cli/__tests__/parseErrors.test.js +25 -0
- package/lib/cli/__tests__/signer.test.js +436 -0
- package/lib/cli/constants.js +23 -0
- package/lib/cli/messages.js +21 -0
- package/lib/cli/parseErrors.js +41 -0
- package/lib/{commands/publish/PublishCliSupport.js → cli/selfUpdate.js} +72 -38
- package/lib/{commands/publish/PublishCliRemove.js → cli/signer.js} +35 -56
- package/lib/index.js +96 -5
- package/lib/package.json +5 -24
- package/lib/portal/__tests__/releaseMetadata.test.js +647 -0
- package/lib/portal/__tests__/translators.test.js +76 -0
- package/lib/portal/__tests__/workflowClient.test.js +457 -0
- package/lib/portal/attestationClient.js +143 -0
- package/lib/portal/files.js +64 -0
- package/lib/portal/http.js +364 -0
- package/lib/portal/records.js +64 -0
- package/lib/portal/releaseMetadata.js +748 -0
- package/lib/portal/translators.js +460 -0
- package/lib/portal/types.js +1 -0
- package/lib/portal/workflowClient.js +704 -0
- package/lib/publication/PublicationProgressReporter.js +1051 -0
- package/lib/publication/__tests__/PublicationProgressReporter.test.js +174 -0
- package/lib/{commands/ValidateCommand.js → publication/__tests__/fundingPreflight.test.js} +90 -66
- package/lib/publication/__tests__/publicationSummary.test.js +26 -0
- package/lib/publication/cliValidation.js +482 -0
- package/lib/publication/fundingPreflight.js +246 -0
- package/lib/publication/publicationSummary.js +99 -0
- package/lib/{commands/utils.js → publication/runPublicationWorkflow.js} +16 -46
- package/package.json +5 -24
- package/src/CliSetup.ts +370 -505
- package/src/CliUtils.ts +9 -233
- package/src/__tests__/CliSetupTest.ts +272 -120
- package/src/cli/__tests__/parseErrors.test.ts +34 -0
- package/src/cli/__tests__/signer.test.ts +359 -0
- package/src/cli/constants.ts +3 -0
- package/src/cli/messages.ts +27 -0
- package/src/cli/parseErrors.ts +62 -0
- package/src/cli/selfUpdate.ts +59 -0
- package/src/cli/signer.ts +38 -0
- package/src/index.ts +31 -4
- package/src/portal/__tests__/releaseMetadata.test.ts +508 -0
- package/src/portal/__tests__/translators.test.ts +82 -0
- package/src/portal/__tests__/workflowClient.test.ts +278 -0
- package/src/portal/attestationClient.ts +19 -0
- package/src/portal/files.ts +73 -0
- package/src/portal/http.ts +170 -0
- package/src/portal/records.ts +38 -0
- package/src/portal/releaseMetadata.ts +489 -0
- package/src/portal/translators.ts +750 -0
- package/src/portal/types.ts +27 -0
- package/src/portal/workflowClient.ts +575 -0
- package/src/publication/PublicationProgressReporter.ts +1026 -0
- package/src/publication/__tests__/PublicationProgressReporter.test.ts +210 -0
- package/src/publication/__tests__/fundingPreflight.test.ts +78 -0
- package/src/publication/__tests__/publicationSummary.test.ts +30 -0
- package/src/publication/cliValidation.ts +264 -0
- package/src/publication/fundingPreflight.ts +123 -0
- package/src/publication/publicationSummary.ts +26 -0
- package/src/publication/runPublicationWorkflow.ts +46 -0
- package/lib/commands/create/CreateCliApp.js +0 -223
- package/lib/commands/create/CreateCliRelease.js +0 -290
- package/lib/commands/create/index.js +0 -40
- package/lib/commands/index.js +0 -3
- package/lib/commands/publish/PublishCliSubmit.js +0 -208
- package/lib/commands/publish/PublishCliUpdate.js +0 -211
- package/lib/commands/publish/index.js +0 -22
- package/lib/commands/scaffolding/ScaffoldInit.js +0 -15
- package/lib/commands/scaffolding/index.js +0 -1
- package/lib/config/EnvVariables.js +0 -59
- package/lib/config/PublishDetails.js +0 -915
- package/lib/config/S3StorageManager.js +0 -93
- package/lib/config/index.js +0 -2
- package/lib/generated/config_obj.json +0 -1
- package/lib/generated/config_schema.json +0 -1
- package/lib/prebuild_schema/publishing_source.yaml +0 -64
- package/lib/prebuild_schema/schemagen.js +0 -25
- package/lib/upload/CachedStorageDriver.js +0 -293
- package/lib/upload/TurboStorageDriver.js +0 -718
- package/lib/upload/index.js +0 -2
- package/src/commands/ValidateCommand.ts +0 -82
- package/src/commands/create/CreateCliApp.ts +0 -93
- package/src/commands/create/CreateCliRelease.ts +0 -149
- package/src/commands/create/index.ts +0 -47
- package/src/commands/index.ts +0 -3
- package/src/commands/publish/PublishCliRemove.ts +0 -66
- package/src/commands/publish/PublishCliSubmit.ts +0 -93
- package/src/commands/publish/PublishCliSupport.ts +0 -66
- package/src/commands/publish/PublishCliUpdate.ts +0 -101
- package/src/commands/publish/index.ts +0 -29
- package/src/commands/scaffolding/ScaffoldInit.ts +0 -20
- package/src/commands/scaffolding/index.ts +0 -1
- package/src/commands/utils.ts +0 -33
- package/src/config/EnvVariables.ts +0 -39
- package/src/config/PublishDetails.ts +0 -456
- package/src/config/S3StorageManager.ts +0 -47
- package/src/config/index.ts +0 -2
- package/src/prebuild_schema/publishing_source.yaml +0 -64
- package/src/prebuild_schema/schemagen.js +0 -31
- package/src/upload/CachedStorageDriver.ts +0 -99
- package/src/upload/TurboStorageDriver.ts +0 -277
- package/src/upload/index.ts +0 -2
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
PublicationBundle,
|
|
8
|
+
PublicationCreateUploadTargetInput,
|
|
9
|
+
PublicationCreateUploadTargetResult,
|
|
10
|
+
} from "@solana-mobile/dapp-store-publishing-tools";
|
|
11
|
+
import getVideoDimensions from "get-video-dimensions";
|
|
12
|
+
import { imageSize } from "image-size";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
ensureHttpsUrl,
|
|
16
|
+
inferFileNameFromUrl,
|
|
17
|
+
inferMimeType,
|
|
18
|
+
} from "./files.js";
|
|
19
|
+
import { uploadBytes } from "./http.js";
|
|
20
|
+
import { firstString, readDeep } from "./records.js";
|
|
21
|
+
import type { PortalSourceKind } from "./types.js";
|
|
22
|
+
|
|
23
|
+
const NFT_SCHEMA_VERSION = "0.4.0";
|
|
24
|
+
const DEFAULT_R2_PUBLIC_HOSTS = [
|
|
25
|
+
"r2.solanamobiledappstore.com",
|
|
26
|
+
"r2-staging.solanamobiledappstore.com",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
type RemoteFilePayload = {
|
|
30
|
+
data: string;
|
|
31
|
+
fileName: string;
|
|
32
|
+
mimeType: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type ReleaseMetadataPortalClient = {
|
|
36
|
+
createUploadTarget(
|
|
37
|
+
input: PublicationCreateUploadTargetInput
|
|
38
|
+
): Promise<PublicationCreateUploadTargetResult>;
|
|
39
|
+
fetchRemoteFile(input: {
|
|
40
|
+
url: string;
|
|
41
|
+
fileName?: string;
|
|
42
|
+
expectedMimeType?: string;
|
|
43
|
+
}): Promise<RemoteFilePayload>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type PublicationMediaPurpose =
|
|
47
|
+
| "icon"
|
|
48
|
+
| "screenshot"
|
|
49
|
+
| "banner"
|
|
50
|
+
| "featureGraphic";
|
|
51
|
+
|
|
52
|
+
function asStringArray(value: unknown): string[] {
|
|
53
|
+
return Array.isArray(value)
|
|
54
|
+
? value.filter((item): item is string => typeof item === "string")
|
|
55
|
+
: [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeMimeType(value?: string | null): string | undefined {
|
|
59
|
+
return value?.split(";")[0]?.trim().toLowerCase();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function toPositiveNumber(value: unknown): number | undefined {
|
|
63
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
64
|
+
? value
|
|
65
|
+
: undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isR2PublicUrl(url: string): boolean {
|
|
69
|
+
try {
|
|
70
|
+
const hostname = new URL(url).hostname.toLowerCase();
|
|
71
|
+
return DEFAULT_R2_PUBLIC_HOSTS.includes(hostname);
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function inferExtensionFromMimeType(mimeType: string): string | undefined {
|
|
78
|
+
switch (mimeType) {
|
|
79
|
+
case "image/png":
|
|
80
|
+
return "png";
|
|
81
|
+
case "image/jpeg":
|
|
82
|
+
return "jpg";
|
|
83
|
+
case "image/webp":
|
|
84
|
+
return "webp";
|
|
85
|
+
case "image/gif":
|
|
86
|
+
return "gif";
|
|
87
|
+
case "image/svg+xml":
|
|
88
|
+
return "svg";
|
|
89
|
+
case "video/mp4":
|
|
90
|
+
return "mp4";
|
|
91
|
+
case "video/webm":
|
|
92
|
+
return "webm";
|
|
93
|
+
case "video/quicktime":
|
|
94
|
+
return "mov";
|
|
95
|
+
case "application/json":
|
|
96
|
+
return "json";
|
|
97
|
+
default: {
|
|
98
|
+
const subtype = mimeType
|
|
99
|
+
.split("/")[1]
|
|
100
|
+
?.split("+")[0]
|
|
101
|
+
?.trim()
|
|
102
|
+
.toLowerCase();
|
|
103
|
+
return subtype?.replace(/[^a-z0-9]/g, "") || undefined;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function inferUploadFileExtension(fileName: string, mimeType: string): string {
|
|
109
|
+
const byName = fileName.split(".").pop()?.trim().toLowerCase();
|
|
110
|
+
if (byName && /^[a-z0-9]+$/.test(byName)) {
|
|
111
|
+
return byName;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return inferExtensionFromMimeType(mimeType) || "bin";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function normalizeOptionalUrl(value: string | null | undefined) {
|
|
118
|
+
if (!value) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const trimmed = value.trim();
|
|
123
|
+
if (!trimmed) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return ensureHttpsUrl(trimmed);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeReleaseName(value: string): string {
|
|
131
|
+
const trimmed = value.trim();
|
|
132
|
+
return trimmed.length <= 32 ? trimmed : trimmed.slice(0, 32);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isVideoMimeType(mimeType: string): boolean {
|
|
136
|
+
return mimeType.startsWith("video/");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function getVideoMediaDimensions(
|
|
140
|
+
fileBytes: Uint8Array,
|
|
141
|
+
fileName: string,
|
|
142
|
+
mimeType: string
|
|
143
|
+
): Promise<{ width: number; height: number }> {
|
|
144
|
+
const tempFilePath = path.join(
|
|
145
|
+
os.tmpdir(),
|
|
146
|
+
`dapp-store-media-${process.pid}-${Date.now()}-${Math.random()
|
|
147
|
+
.toString(36)
|
|
148
|
+
.slice(2)}.${inferUploadFileExtension(fileName, mimeType)}`
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
await fs.writeFile(tempFilePath, fileBytes);
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const dimensions = await getVideoDimensions(tempFilePath);
|
|
155
|
+
const width = toPositiveNumber(dimensions.width);
|
|
156
|
+
const height = toPositiveNumber(dimensions.height);
|
|
157
|
+
|
|
158
|
+
if (!width || !height) {
|
|
159
|
+
throw new Error(`Unable to determine video dimensions for ${fileName}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { width, height };
|
|
163
|
+
} finally {
|
|
164
|
+
await fs.rm(tempFilePath, { force: true });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function getMediaDimensions(
|
|
169
|
+
fileBytes: Uint8Array,
|
|
170
|
+
fileName: string,
|
|
171
|
+
mimeType: string
|
|
172
|
+
): Promise<{ width: number; height: number }> {
|
|
173
|
+
if (isVideoMimeType(mimeType)) {
|
|
174
|
+
return await getVideoMediaDimensions(fileBytes, fileName, mimeType);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const dimensions = imageSize(fileBytes);
|
|
178
|
+
const width = toPositiveNumber(dimensions.width);
|
|
179
|
+
const height = toPositiveNumber(dimensions.height);
|
|
180
|
+
|
|
181
|
+
if (!width || !height) {
|
|
182
|
+
throw new Error(`Unable to determine image dimensions for ${fileName}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { width, height };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getPreferredFeatureGraphicUrl(bundle: PublicationBundle): string {
|
|
189
|
+
return (
|
|
190
|
+
firstString(bundle, [
|
|
191
|
+
"dapp.editorsChoiceGraphicUrl",
|
|
192
|
+
"dapp.featureGraphicUrl",
|
|
193
|
+
]) || ""
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function resolveMediaItem(
|
|
198
|
+
client: ReleaseMetadataPortalClient,
|
|
199
|
+
input: {
|
|
200
|
+
defaultMimeType?: string;
|
|
201
|
+
expectedMimeType?: string;
|
|
202
|
+
fallbackFileName: string;
|
|
203
|
+
purpose: PublicationMediaPurpose;
|
|
204
|
+
uri: string;
|
|
205
|
+
}
|
|
206
|
+
) {
|
|
207
|
+
const resolvedUri = ensureHttpsUrl(input.uri);
|
|
208
|
+
const isAlreadyPublicR2 = isR2PublicUrl(resolvedUri);
|
|
209
|
+
|
|
210
|
+
let remoteFile: RemoteFilePayload;
|
|
211
|
+
try {
|
|
212
|
+
remoteFile = await client.fetchRemoteFile({
|
|
213
|
+
url: resolvedUri,
|
|
214
|
+
fileName: input.fallbackFileName,
|
|
215
|
+
expectedMimeType: input.expectedMimeType,
|
|
216
|
+
});
|
|
217
|
+
} catch (error) {
|
|
218
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
219
|
+
throw new Error(
|
|
220
|
+
`Failed to fetch ${input.purpose} media from ${resolvedUri}: ${message}`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
const fileBytes = Buffer.from(remoteFile.data, "base64");
|
|
224
|
+
|
|
225
|
+
if (fileBytes.byteLength === 0) {
|
|
226
|
+
throw new Error(`Remote media file is empty: ${resolvedUri}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const remoteFileName = remoteFile.fileName || input.fallbackFileName;
|
|
230
|
+
const resolvedMimeType =
|
|
231
|
+
normalizeMimeType(remoteFile.mimeType) ||
|
|
232
|
+
input.defaultMimeType ||
|
|
233
|
+
inferMimeType(remoteFileName);
|
|
234
|
+
const fileHash = createHash("sha256").update(fileBytes).digest("hex");
|
|
235
|
+
const dimensions = await getMediaDimensions(
|
|
236
|
+
fileBytes,
|
|
237
|
+
remoteFileName,
|
|
238
|
+
resolvedMimeType
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
if (isAlreadyPublicR2) {
|
|
242
|
+
return {
|
|
243
|
+
mime: resolvedMimeType,
|
|
244
|
+
purpose: input.purpose,
|
|
245
|
+
uri: resolvedUri,
|
|
246
|
+
width: dimensions.width,
|
|
247
|
+
height: dimensions.height,
|
|
248
|
+
sha256: fileHash,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const uploadTarget = await client.createUploadTarget({
|
|
253
|
+
fileHash,
|
|
254
|
+
fileExtension: inferUploadFileExtension(remoteFileName, resolvedMimeType),
|
|
255
|
+
contentType: resolvedMimeType,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
if (!uploadTarget.uploadUrl || !uploadTarget.publicUrl) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`The portal did not return a valid upload target for ${input.purpose}.`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
await uploadBytes(uploadTarget.uploadUrl, fileBytes, resolvedMimeType);
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
mime: resolvedMimeType,
|
|
268
|
+
purpose: input.purpose,
|
|
269
|
+
uri: uploadTarget.publicUrl,
|
|
270
|
+
width: dimensions.width,
|
|
271
|
+
height: dimensions.height,
|
|
272
|
+
sha256: fileHash,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function buildReleaseMetadataDocument(
|
|
277
|
+
client: ReleaseMetadataPortalClient,
|
|
278
|
+
bundle: PublicationBundle,
|
|
279
|
+
sourceKind: PortalSourceKind
|
|
280
|
+
): Promise<Record<string, unknown>> {
|
|
281
|
+
const releaseName = normalizeReleaseName(
|
|
282
|
+
bundle.metadata?.localizedName ||
|
|
283
|
+
bundle.release.localizedName ||
|
|
284
|
+
bundle.release.releaseName ||
|
|
285
|
+
bundle.dapp.dappName ||
|
|
286
|
+
"Release update"
|
|
287
|
+
);
|
|
288
|
+
const shortDescription =
|
|
289
|
+
bundle.metadata?.shortDescription ||
|
|
290
|
+
bundle.release.shortDescription ||
|
|
291
|
+
bundle.dapp.subtitle ||
|
|
292
|
+
"Release NFT";
|
|
293
|
+
const longDescription =
|
|
294
|
+
bundle.metadata?.longDescription ||
|
|
295
|
+
bundle.release.longDescription ||
|
|
296
|
+
bundle.dapp.description ||
|
|
297
|
+
shortDescription;
|
|
298
|
+
const newInVersion =
|
|
299
|
+
bundle.metadata?.newInVersion || bundle.release.newInVersion || "";
|
|
300
|
+
const publisherAddress =
|
|
301
|
+
bundle.signerAuthority.dappWalletAddress ||
|
|
302
|
+
bundle.signerAuthority.collectionAuthority ||
|
|
303
|
+
bundle.dapp.walletAddress ||
|
|
304
|
+
"";
|
|
305
|
+
const publisherName = bundle.publisher.name || "";
|
|
306
|
+
const publisherWebsite = normalizeOptionalUrl(
|
|
307
|
+
bundle.metadata?.publisherWebsite ||
|
|
308
|
+
bundle.publisher.website ||
|
|
309
|
+
bundle.dapp.appWebsite ||
|
|
310
|
+
bundle.dapp.website ||
|
|
311
|
+
undefined
|
|
312
|
+
);
|
|
313
|
+
const publisherContact =
|
|
314
|
+
bundle.publisher.email ||
|
|
315
|
+
bundle.dapp.contactEmail ||
|
|
316
|
+
bundle.dapp.supportEmail ||
|
|
317
|
+
"";
|
|
318
|
+
const publisherSupportEmail =
|
|
319
|
+
bundle.publisher.supportEmail ||
|
|
320
|
+
bundle.metadata?.supportEmail ||
|
|
321
|
+
bundle.dapp.supportEmail ||
|
|
322
|
+
bundle.dapp.contactEmail ||
|
|
323
|
+
bundle.publisher.email ||
|
|
324
|
+
publisherContact;
|
|
325
|
+
const iconUri = bundle.dapp.dappIconUrl || "";
|
|
326
|
+
|
|
327
|
+
if (!iconUri) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
"Publication bundle did not include a public app icon URL."
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const installUri =
|
|
334
|
+
bundle.installFile.uri || bundle.release.releaseFileUrl || "";
|
|
335
|
+
const installMimeType =
|
|
336
|
+
bundle.installFile.mimeType ||
|
|
337
|
+
inferMimeType(inferFileNameFromUrl(installUri));
|
|
338
|
+
const installSize =
|
|
339
|
+
bundle.installFile.size ?? bundle.release.releaseFileSize ?? 0;
|
|
340
|
+
const installSha256 =
|
|
341
|
+
bundle.installFile.sha256 || bundle.release.releaseFileHash || "";
|
|
342
|
+
const androidPackage =
|
|
343
|
+
bundle.release.androidPackage || bundle.dapp.androidPackage;
|
|
344
|
+
const versionName = bundle.release.versionName || "";
|
|
345
|
+
const versionCode = bundle.release.versionCode ?? 0;
|
|
346
|
+
const minSdkVersion = bundle.release.minSdkVersion ?? 1;
|
|
347
|
+
const targetSdkRaw = readDeep(bundle, "release.targetSdkVersion");
|
|
348
|
+
const targetSdkVersion =
|
|
349
|
+
typeof targetSdkRaw === "number"
|
|
350
|
+
? targetSdkRaw
|
|
351
|
+
: Number(targetSdkRaw || 0) || null;
|
|
352
|
+
const certificateFingerprint = bundle.release.certificateFingerprint || "";
|
|
353
|
+
const permissions = bundle.release.permissions ?? [];
|
|
354
|
+
const locales =
|
|
355
|
+
bundle.release.locales && bundle.release.locales.length > 0
|
|
356
|
+
? bundle.release.locales
|
|
357
|
+
: bundle.metadata?.locales && bundle.metadata.locales.length > 0
|
|
358
|
+
? bundle.metadata.locales
|
|
359
|
+
: bundle.dapp.languages && bundle.dapp.languages.length > 0
|
|
360
|
+
? bundle.dapp.languages
|
|
361
|
+
: ["en-US"];
|
|
362
|
+
const previewUris = bundle.dapp.dappPreviewUrls ?? [];
|
|
363
|
+
const bannerUri = bundle.dapp.bannerUrl || "";
|
|
364
|
+
const featureGraphicUri = getPreferredFeatureGraphicUrl(bundle);
|
|
365
|
+
|
|
366
|
+
const media: Array<Record<string, unknown>> = [];
|
|
367
|
+
const iconMedia = await resolveMediaItem(client, {
|
|
368
|
+
defaultMimeType: "image/png",
|
|
369
|
+
fallbackFileName: "release-icon.png",
|
|
370
|
+
purpose: "icon",
|
|
371
|
+
uri: iconUri,
|
|
372
|
+
});
|
|
373
|
+
media.push(iconMedia);
|
|
374
|
+
|
|
375
|
+
for (const [index, previewUri] of previewUris.entries()) {
|
|
376
|
+
media.push(
|
|
377
|
+
await resolveMediaItem(client, {
|
|
378
|
+
fallbackFileName: `release-screenshot-${index + 1}`,
|
|
379
|
+
purpose: "screenshot",
|
|
380
|
+
uri: previewUri,
|
|
381
|
+
})
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (bannerUri) {
|
|
386
|
+
media.push(
|
|
387
|
+
await resolveMediaItem(client, {
|
|
388
|
+
defaultMimeType: inferMimeType(inferFileNameFromUrl(bannerUri)),
|
|
389
|
+
fallbackFileName: "release-banner.png",
|
|
390
|
+
purpose: "banner",
|
|
391
|
+
uri: bannerUri,
|
|
392
|
+
})
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (featureGraphicUri) {
|
|
397
|
+
media.push(
|
|
398
|
+
await resolveMediaItem(client, {
|
|
399
|
+
defaultMimeType: inferMimeType(
|
|
400
|
+
inferFileNameFromUrl(featureGraphicUri)
|
|
401
|
+
),
|
|
402
|
+
fallbackFileName: "release-feature-graphic.png",
|
|
403
|
+
purpose: "featureGraphic",
|
|
404
|
+
uri: featureGraphicUri,
|
|
405
|
+
})
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const licenseUrl = normalizeOptionalUrl(
|
|
410
|
+
bundle.metadata?.legal.licenseUrl || bundle.dapp.licenseUrl || undefined
|
|
411
|
+
);
|
|
412
|
+
const copyrightUrl =
|
|
413
|
+
bundle.metadata?.legal.copyrightUrl ||
|
|
414
|
+
bundle.dapp.copyrightUrl ||
|
|
415
|
+
undefined;
|
|
416
|
+
const privacyPolicyUrl = normalizeOptionalUrl(
|
|
417
|
+
bundle.metadata?.legal.privacyPolicyUrl ||
|
|
418
|
+
bundle.dapp.privacyPolicyUrl ||
|
|
419
|
+
undefined
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
schema_version: NFT_SCHEMA_VERSION,
|
|
424
|
+
name: releaseName,
|
|
425
|
+
description: shortDescription,
|
|
426
|
+
image: iconMedia.uri,
|
|
427
|
+
...(publisherWebsite ? { external_url: publisherWebsite } : {}),
|
|
428
|
+
properties: {
|
|
429
|
+
category: "dApp",
|
|
430
|
+
creators: [
|
|
431
|
+
{
|
|
432
|
+
address: publisherAddress,
|
|
433
|
+
share: 100,
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
},
|
|
437
|
+
extensions: {
|
|
438
|
+
solana_dapp_store: {
|
|
439
|
+
publisher_details: {
|
|
440
|
+
name: publisherName,
|
|
441
|
+
...(publisherWebsite ? { website: publisherWebsite } : {}),
|
|
442
|
+
contact: publisherContact,
|
|
443
|
+
support_email: publisherSupportEmail,
|
|
444
|
+
},
|
|
445
|
+
release_details: {
|
|
446
|
+
updated_on: new Date().toISOString(),
|
|
447
|
+
...(licenseUrl ? { license_url: licenseUrl } : {}),
|
|
448
|
+
...(copyrightUrl ? { copyright_url: copyrightUrl } : {}),
|
|
449
|
+
...(privacyPolicyUrl ? { privacy_policy_url: privacyPolicyUrl } : {}),
|
|
450
|
+
localized_resources: {
|
|
451
|
+
long_description: "1",
|
|
452
|
+
new_in_version: "2",
|
|
453
|
+
name: "4",
|
|
454
|
+
short_description: "5",
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
media,
|
|
458
|
+
files: [
|
|
459
|
+
{
|
|
460
|
+
mime: installMimeType,
|
|
461
|
+
purpose: "install",
|
|
462
|
+
size: installSize,
|
|
463
|
+
sha256: installSha256,
|
|
464
|
+
uri: installUri,
|
|
465
|
+
},
|
|
466
|
+
],
|
|
467
|
+
android_details: {
|
|
468
|
+
android_package: androidPackage,
|
|
469
|
+
version: versionName,
|
|
470
|
+
version_code: versionCode,
|
|
471
|
+
min_sdk: minSdkVersion,
|
|
472
|
+
target_sdk: targetSdkVersion,
|
|
473
|
+
cert_fingerprint: certificateFingerprint,
|
|
474
|
+
permissions: asStringArray(permissions),
|
|
475
|
+
locales: asStringArray(locales),
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
i18n: {
|
|
479
|
+
"en-US": {
|
|
480
|
+
"1": longDescription,
|
|
481
|
+
"2": newInVersion,
|
|
482
|
+
"4": releaseName,
|
|
483
|
+
"5": shortDescription.slice(0, 50),
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
__origin: sourceKind,
|
|
488
|
+
};
|
|
489
|
+
}
|