@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
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { Connection, Keypair } from "@solana/web3.js";
|
|
2
|
-
import type { SignWithPublisherKeypair } from "@solana-mobile/dapp-store-publishing-tools";
|
|
3
|
-
import { publishUpdate } from "@solana-mobile/dapp-store-publishing-tools";
|
|
4
|
-
import { checkMintedStatus, showMessage } from "../../CliUtils.js";
|
|
5
|
-
import nacl from "tweetnacl";
|
|
6
|
-
import { loadPublishDetailsWithChecks, writeToPublishDetails } from "../../config/PublishDetails.js";
|
|
7
|
-
|
|
8
|
-
type PublishUpdateCommandInput = {
|
|
9
|
-
appMintAddress: string;
|
|
10
|
-
releaseMintAddress: string;
|
|
11
|
-
signer: Keypair;
|
|
12
|
-
url: string;
|
|
13
|
-
dryRun: boolean;
|
|
14
|
-
compliesWithSolanaDappStorePolicies: boolean;
|
|
15
|
-
requestorIsAuthorized: boolean;
|
|
16
|
-
critical: boolean;
|
|
17
|
-
alphaTest?: boolean;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export const publishUpdateCommand = async ({
|
|
21
|
-
appMintAddress,
|
|
22
|
-
releaseMintAddress,
|
|
23
|
-
signer,
|
|
24
|
-
url,
|
|
25
|
-
dryRun = false,
|
|
26
|
-
compliesWithSolanaDappStorePolicies = false,
|
|
27
|
-
requestorIsAuthorized = false,
|
|
28
|
-
critical = false,
|
|
29
|
-
alphaTest,
|
|
30
|
-
}: PublishUpdateCommandInput) => {
|
|
31
|
-
|
|
32
|
-
showMessage(
|
|
33
|
-
`Publishing Estimates`,
|
|
34
|
-
"App update approvals take around 1-2 business days for review.",
|
|
35
|
-
"warning"
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
if (!compliesWithSolanaDappStorePolicies) {
|
|
39
|
-
console.error(
|
|
40
|
-
"ERROR: Cannot submit a request for which the requestor does not attest that it complies with Solana dApp Store policies"
|
|
41
|
-
);
|
|
42
|
-
return;
|
|
43
|
-
} else if (!requestorIsAuthorized) {
|
|
44
|
-
console.error(
|
|
45
|
-
"ERROR: Cannot submit a request for which the requestor does not attest they are authorized to do so"
|
|
46
|
-
);
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const connection = new Connection(
|
|
51
|
-
url,
|
|
52
|
-
{
|
|
53
|
-
commitment: "confirmed",
|
|
54
|
-
}
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
const {
|
|
58
|
-
publisher: publisherDetails,
|
|
59
|
-
app: appDetails,
|
|
60
|
-
release: releaseDetails,
|
|
61
|
-
solana_mobile_dapp_publisher_portal: solanaMobileDappPublisherPortalDetails,
|
|
62
|
-
lastUpdatedVersionOnStore: lastUpdatedVersionOnStore
|
|
63
|
-
} = await loadPublishDetailsWithChecks();
|
|
64
|
-
|
|
65
|
-
if (alphaTest && solanaMobileDappPublisherPortalDetails.alpha_testers == undefined) {
|
|
66
|
-
throw new Error(`Alpha test submission without specifying any testers.\nAdd field alpha_testers in your 'config.yaml' file.`)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const sign = ((buf: Buffer) =>
|
|
70
|
-
nacl.sign(buf, signer.secretKey)) as SignWithPublisherKeypair;
|
|
71
|
-
|
|
72
|
-
const appAddr = appMintAddress ?? appDetails.address;
|
|
73
|
-
const releaseAddr = releaseMintAddress ?? releaseDetails.address;
|
|
74
|
-
|
|
75
|
-
if (lastUpdatedVersionOnStore != null && releaseAddr === lastUpdatedVersionOnStore.address) {
|
|
76
|
-
throw new Error(`You've already submitted this version for review.`);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
await checkMintedStatus(connection, appAddr, releaseAddr);
|
|
80
|
-
|
|
81
|
-
await publishUpdate(
|
|
82
|
-
{ connection, sign },
|
|
83
|
-
{
|
|
84
|
-
appMintAddress: appMintAddress ?? appDetails.address,
|
|
85
|
-
releaseMintAddress: releaseMintAddress ?? releaseDetails.address,
|
|
86
|
-
publisherDetails,
|
|
87
|
-
solanaMobileDappPublisherPortalDetails,
|
|
88
|
-
compliesWithSolanaDappStorePolicies,
|
|
89
|
-
requestorIsAuthorized,
|
|
90
|
-
criticalUpdate: critical,
|
|
91
|
-
alphaTest,
|
|
92
|
-
},
|
|
93
|
-
dryRun
|
|
94
|
-
);
|
|
95
|
-
if (!alphaTest) {
|
|
96
|
-
await writeToPublishDetails(
|
|
97
|
-
{
|
|
98
|
-
lastUpdatedVersionOnStore: { address: releaseAddr }
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
};
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
export * from "./PublishCliRemove.js";
|
|
2
|
-
export * from "./PublishCliSubmit.js";
|
|
3
|
-
export * from "./PublishCliSupport.js";
|
|
4
|
-
export * from "./PublishCliUpdate.js";
|
|
5
|
-
|
|
6
|
-
/*
|
|
7
|
-
* Module responsible for submitting requests to the Solana dApp Store publisher portal
|
|
8
|
-
* Anything that is out-of-order will be prompted back into order
|
|
9
|
-
* And steps that happen more than once will do their best to remember as much information as possible.
|
|
10
|
-
* We will ask questions and do our best to answer anything that's already been configured, and prompt for anything that's not
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
// We'll never ask for private keys or seed phrases
|
|
14
|
-
// You must use the same signer(s) when submitting requests to the publisher portal as was used to publish
|
|
15
|
-
// your app on-chain.
|
|
16
|
-
|
|
17
|
-
// The Solana Mobile dApp publisher portal supports 4 different requests: `submit`, `update`, `remove`, and `support`.
|
|
18
|
-
// Each request includes:
|
|
19
|
-
// - a 32-digit randomly generated unique identifier
|
|
20
|
-
// - an attestation payload, signed with the private key of the dApp collection update authority
|
|
21
|
-
// - the dApp release NFT address
|
|
22
|
-
// - contact and company information for the requestor
|
|
23
|
-
// - a self-attestation that the requestor is authorized to make this request
|
|
24
|
-
// - additional fields, specific to the request type in question
|
|
25
|
-
|
|
26
|
-
// We'll attempt to read as much as possible from a provided `.yml` file
|
|
27
|
-
// If there are provided folders that are well-structured, we'll opt to use that too.
|
|
28
|
-
|
|
29
|
-
// Requests and responses are logged to the console, to facilitate use in an automated CI/CD environment.
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import yaml, { dump } from "js-yaml";
|
|
2
|
-
|
|
3
|
-
import { readFile } from 'fs/promises';
|
|
4
|
-
const releaseSchema = JSON.parse((await readFile(new URL("../../generated/config_obj.json", import.meta.url))).toString());
|
|
5
|
-
import fs from "fs";
|
|
6
|
-
import path from "path";
|
|
7
|
-
import { Constants } from "../../CliUtils.js";
|
|
8
|
-
|
|
9
|
-
export const initScaffold = (): string => {
|
|
10
|
-
const outputYaml = Constants.CONFIG_FILE_NAME;
|
|
11
|
-
const outFile = path.join(process.cwd(), outputYaml);
|
|
12
|
-
|
|
13
|
-
if (fs.existsSync(outFile)) {
|
|
14
|
-
throw Error("Configuration file already present; please use to intialize a new config file.");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
fs.writeFileSync(outFile, dump(releaseSchema));
|
|
18
|
-
|
|
19
|
-
return `Your configuration file was created: ${outputYaml}`;
|
|
20
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./ScaffoldInit.js";
|
package/src/commands/utils.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type Metaplex,
|
|
3
|
-
type TransactionBuilder,
|
|
4
|
-
FailedToConfirmTransactionError,
|
|
5
|
-
} from "@metaplex-foundation/js";
|
|
6
|
-
import { TransactionExpiredBlockheightExceededError } from "@solana/web3.js";
|
|
7
|
-
|
|
8
|
-
export async function sendAndConfirmTransaction(
|
|
9
|
-
metaplex: Metaplex,
|
|
10
|
-
builder: TransactionBuilder
|
|
11
|
-
): ReturnType<TransactionBuilder["sendAndConfirm"]> {
|
|
12
|
-
for (let i = 0; i < 10; i++) {
|
|
13
|
-
try {
|
|
14
|
-
return await builder.sendAndConfirm(metaplex);
|
|
15
|
-
} catch (e: unknown) {
|
|
16
|
-
if (isTransientError(e)) {
|
|
17
|
-
continue;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
throw e;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
throw new Error("Unable to send transaction. Please try later.");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function isTransientError(e: unknown): boolean {
|
|
28
|
-
return (
|
|
29
|
-
e instanceof FailedToConfirmTransactionError &&
|
|
30
|
-
(e.cause instanceof TransactionExpiredBlockheightExceededError ||
|
|
31
|
-
/blockhash not found/i.test(e.cause?.message ?? ""))
|
|
32
|
-
);
|
|
33
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import * as dotenv from "dotenv";
|
|
2
|
-
|
|
3
|
-
export type S3Config = {
|
|
4
|
-
accessKey: string;
|
|
5
|
-
secretKey: string;
|
|
6
|
-
bucketName: string;
|
|
7
|
-
regionName: string;
|
|
8
|
-
}
|
|
9
|
-
export class EnvVariables {
|
|
10
|
-
|
|
11
|
-
public get hasAndroidTools(): boolean {
|
|
12
|
-
return process.env.ANDROID_TOOLS_DIR !== undefined;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
public get androidToolsDir(): string {
|
|
16
|
-
return process.env.ANDROID_TOOLS_DIR as string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
public get hasS3EnvArgs(): boolean {
|
|
20
|
-
return process.env.STORAGE_TYPE == "s3" &&
|
|
21
|
-
process.env.S3_ACCESS_KEY != undefined &&
|
|
22
|
-
process.env.S3_SECRET_KEY != undefined &&
|
|
23
|
-
process.env.S3_BUCKET != undefined &&
|
|
24
|
-
process.env.S3_REGION != undefined;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
public get s3Config(): S3Config {
|
|
28
|
-
return {
|
|
29
|
-
accessKey: process.env.S3_ACCESS_KEY as string,
|
|
30
|
-
secretKey: process.env.S3_SECRET_KEY as string,
|
|
31
|
-
bucketName: process.env.S3_BUCKET as string,
|
|
32
|
-
regionName: process.env.S3_REGION as string
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
constructor() {
|
|
37
|
-
dotenv.config();
|
|
38
|
-
}
|
|
39
|
-
}
|
|
@@ -1,456 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AndroidDetails,
|
|
3
|
-
App,
|
|
4
|
-
LastSubmittedVersionOnChain,
|
|
5
|
-
LastUpdatedVersionOnStore,
|
|
6
|
-
Publisher,
|
|
7
|
-
Release,
|
|
8
|
-
SolanaMobileDappPublisherPortal
|
|
9
|
-
} from "@solana-mobile/dapp-store-publishing-tools";
|
|
10
|
-
import { dump, load } from "js-yaml";
|
|
11
|
-
import Ajv from "ajv";
|
|
12
|
-
// eslint-disable-next-line require-extensions/require-extensions
|
|
13
|
-
import { readFile } from 'fs/promises';
|
|
14
|
-
const schemaJson = JSON.parse((await readFile(new URL("../generated/config_schema.json", import.meta.url))).toString());
|
|
15
|
-
import fs from "fs";
|
|
16
|
-
import path from "path";
|
|
17
|
-
import { toMetaplexFile } from "@metaplex-foundation/js";
|
|
18
|
-
import { Constants, showMessage } from "../CliUtils.js";
|
|
19
|
-
import util from "util";
|
|
20
|
-
import { imageSize } from "image-size";
|
|
21
|
-
import { exec } from "child_process";
|
|
22
|
-
import getVideoDimensions from "get-video-dimensions";
|
|
23
|
-
import { PublicKey } from "@solana/web3.js";
|
|
24
|
-
|
|
25
|
-
const runImgSize = util.promisify(imageSize);
|
|
26
|
-
const runExec = util.promisify(exec);
|
|
27
|
-
|
|
28
|
-
export interface PublishDetails {
|
|
29
|
-
publisher: Publisher;
|
|
30
|
-
app: App;
|
|
31
|
-
release: Release;
|
|
32
|
-
solana_mobile_dapp_publisher_portal: SolanaMobileDappPublisherPortal;
|
|
33
|
-
lastSubmittedVersionOnChain: LastSubmittedVersionOnChain
|
|
34
|
-
lastUpdatedVersionOnStore: LastUpdatedVersionOnStore,
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const AaptPrefixes = {
|
|
38
|
-
quoteRegex: "'(.*?)'",
|
|
39
|
-
quoteNonLazyRegex: "'(.*)'",
|
|
40
|
-
packagePrefix: "package: name=",
|
|
41
|
-
verCodePrefix: "versionCode=",
|
|
42
|
-
verNamePrefix: "versionName=",
|
|
43
|
-
sdkPrefix: "(?:minSdk|sdk)Version:",
|
|
44
|
-
debuggableApkPrefix: "application-debuggable",
|
|
45
|
-
localePrefix: "locales: ",
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
type SaveToConfigArgs = {
|
|
49
|
-
publisher?: Pick<Publisher, "address">;
|
|
50
|
-
app?: Pick<App, "address">;
|
|
51
|
-
release?: Release;
|
|
52
|
-
lastSubmittedVersionOnChain?: LastSubmittedVersionOnChain;
|
|
53
|
-
lastUpdatedVersionOnStore?: LastUpdatedVersionOnStore;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const ajv = new Ajv({ strictTuples: false });
|
|
57
|
-
const validate = ajv.compile(schemaJson);
|
|
58
|
-
|
|
59
|
-
export const loadPublishDetails = async (configPath: string) => {
|
|
60
|
-
const configFile = await fs.promises.readFile(configPath, "utf-8");
|
|
61
|
-
|
|
62
|
-
const valid = validate(load(configFile) as object);
|
|
63
|
-
|
|
64
|
-
if (!valid) {
|
|
65
|
-
console.error(validate.errors);
|
|
66
|
-
process.exit(1);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return load(configFile) as PublishDetails;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
export const loadPublishDetailsWithChecks = async (
|
|
73
|
-
buildToolsDir: string | null = null
|
|
74
|
-
): Promise<PublishDetails> => {
|
|
75
|
-
const config = await loadPublishDetails(Constants.getConfigFilePath());
|
|
76
|
-
|
|
77
|
-
// We validate that the config is going to have at least one installable asset
|
|
78
|
-
const apkEntry = config.release.files.find(
|
|
79
|
-
(asset: PublishDetails["release"]["files"][0]) => asset.purpose === "install"
|
|
80
|
-
)!;
|
|
81
|
-
const apkPath = path.join(process.cwd(), apkEntry?.uri);
|
|
82
|
-
if (!fs.existsSync(apkPath)) {
|
|
83
|
-
throw new Error("Invalid path to APK file.");
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const developerOverridenLocales = config.release?.android_details?.locales
|
|
87
|
-
|
|
88
|
-
if (buildToolsDir) {
|
|
89
|
-
config.release.android_details = await getAndroidDetails(
|
|
90
|
-
buildToolsDir,
|
|
91
|
-
apkPath,
|
|
92
|
-
developerOverridenLocales
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const appIcon = config.app.media?.find(
|
|
97
|
-
(asset: any) => asset.purpose === "icon"
|
|
98
|
-
)?.uri;
|
|
99
|
-
|
|
100
|
-
if (appIcon) {
|
|
101
|
-
const iconPath = path.join(process.cwd(), appIcon);
|
|
102
|
-
await checkIconCompatibility(iconPath, "App");
|
|
103
|
-
|
|
104
|
-
const iconBuffer = await fs.promises.readFile(iconPath);
|
|
105
|
-
config.app.icon = toMetaplexFile(iconBuffer, appIcon);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const releaseIcon = config.release.media?.find(
|
|
109
|
-
(asset: any) => asset.purpose === "icon"
|
|
110
|
-
)?.uri;
|
|
111
|
-
|
|
112
|
-
if (releaseIcon) {
|
|
113
|
-
const iconPath = path.join(process.cwd(), releaseIcon);
|
|
114
|
-
await checkIconCompatibility(iconPath, "Release");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (!appIcon && !releaseIcon) {
|
|
118
|
-
throw new Error("Please specify at least one media entry of type icon in your configuration file");
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const banner = config.release.media?.find(
|
|
122
|
-
(asset: any) => asset.purpose === "banner"
|
|
123
|
-
)?.uri;
|
|
124
|
-
|
|
125
|
-
if (banner) {
|
|
126
|
-
const bannerPath = path.join(process.cwd(), banner);
|
|
127
|
-
await checkBannerCompatibility(bannerPath);
|
|
128
|
-
} else {
|
|
129
|
-
throw new Error("Please specify banner image of size 1200x600 in your configuration file");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const featureGraphic = config.release.media?.find(
|
|
133
|
-
(asset: any) => asset.purpose === "featureGraphic"
|
|
134
|
-
)?.uri;
|
|
135
|
-
|
|
136
|
-
if (featureGraphic) {
|
|
137
|
-
const featureGraphicPath = path.join(process.cwd(), featureGraphic);
|
|
138
|
-
await checkFeatureGraphicCompatibility(featureGraphicPath);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
config.release.media.forEach((item: PublishDetails["release"]["media"][0]) => {
|
|
142
|
-
const mediaPath = path.join(process.cwd(), item.uri);
|
|
143
|
-
if (!fs.existsSync(mediaPath)) {
|
|
144
|
-
throw new Error(`File doesnt exist: ${item.uri}.`)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (item.purpose == "screenshot" && !checkImageExtension(mediaPath)) {
|
|
148
|
-
throw new Error(`Please ensure the file ${item.uri} is a jpeg, png, or webp file.`)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (item.purpose == "video" && !checkVideoExtension(mediaPath)) {
|
|
152
|
-
throw new Error(`Please ensure the file ${item.uri} is a mp4.`)
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
const screenshots = config.release.media?.filter(
|
|
158
|
-
(asset: any) => asset.purpose === "screenshot"
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
for (const item of screenshots) {
|
|
162
|
-
const mediaPath = path.join(process.cwd(), item.uri);
|
|
163
|
-
if (await checkScreenshotDimensions(mediaPath)) {
|
|
164
|
-
throw new Error(`Screenshot ${mediaPath} must be at least 1080px in width and height.`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const videos = config.release.media?.filter(
|
|
169
|
-
(asset: any) => asset.purpose === "video"
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
for (const video of videos) {
|
|
173
|
-
const mediaPath = path.join(process.cwd(), video.uri);
|
|
174
|
-
if (await checkVideoDimensions(mediaPath)) {
|
|
175
|
-
throw new Error(`Video ${mediaPath} must be at least 720px in width and height.`);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (screenshots.length + videos.length < 4) {
|
|
180
|
-
throw new Error(`At least 4 screenshots or videos are required for publishing a new release. Found only ${screenshots.length + videos.length}`)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
validateLocalizableResources(config);
|
|
184
|
-
|
|
185
|
-
const googlePkg = config.solana_mobile_dapp_publisher_portal.google_store_package;
|
|
186
|
-
if (googlePkg?.length) {
|
|
187
|
-
const pkgCompare = new RegExp("[a-zA-Z0-9_]+(\\.[a-zA-Z0-9_]+)+").exec(googlePkg);
|
|
188
|
-
|
|
189
|
-
if (!pkgCompare?.length) {
|
|
190
|
-
throw new Error("Please provide a valid Google store package name in the Publisher Portal section of your configuration file.");
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const alpha_testers = config.solana_mobile_dapp_publisher_portal.alpha_testers;
|
|
195
|
-
if (alpha_testers !== undefined) {
|
|
196
|
-
for (const wallet of alpha_testers) {
|
|
197
|
-
try {
|
|
198
|
-
void new PublicKey(wallet.address);
|
|
199
|
-
} catch (e: unknown) {
|
|
200
|
-
throw new Error(`invalid alpha tester wallet address <${wallet}>`);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (alpha_testers.size > 10) {
|
|
205
|
-
throw new Error(`Alpha testers are limited to 10 per app submission`);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return config;
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const checkIconCompatibility = async (path: string, typeString: string) => {
|
|
213
|
-
if (!fs.existsSync(path) || !checkImageExtension(path)) {
|
|
214
|
-
throw new Error(`Please check the path to your ${typeString} icon and ensure the file is a jpeg, png, or webp file.`);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (await checkIconDimensions(path)) {
|
|
218
|
-
throw new Error("Icons must be 512px by 512px.");
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
const checkBannerCompatibility = async (path: string) => {
|
|
223
|
-
if (!fs.existsSync(path) || !checkImageExtension(path)) {
|
|
224
|
-
throw new Error(`Please check the path to your banner image and ensure the file is a jpeg, png, or webp file.`);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (await checkBannerDimensions(path)) {
|
|
228
|
-
throw new Error("Banner must be 1200px by 600px.");
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
const checkFeatureGraphicCompatibility = async (path: string) => {
|
|
233
|
-
if (!fs.existsSync(path) || !checkImageExtension(path)) {
|
|
234
|
-
throw new Error(`Please check the path to your featureGraphic image and ensure the file is a jpeg, png, or webp file.`);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (await checkFeatureGraphicDimensions(path)) {
|
|
238
|
-
throw new Error("Feature Graphic must be 1200px by 1200px.");
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
const checkImageExtension = (uri: string): boolean => {
|
|
243
|
-
const fileExt = path.extname(uri).toLowerCase();
|
|
244
|
-
return (
|
|
245
|
-
fileExt == ".png" ||
|
|
246
|
-
fileExt == ".jpg" ||
|
|
247
|
-
fileExt == ".jpeg" ||
|
|
248
|
-
fileExt == ".webp"
|
|
249
|
-
);
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
const checkVideoExtension = (uri: string): boolean => {
|
|
253
|
-
const fileExt = path.extname(uri).toLowerCase();
|
|
254
|
-
return (
|
|
255
|
-
fileExt == ".mp4"
|
|
256
|
-
);
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* We need to pre-check some things in the localized resources before we move forward
|
|
261
|
-
*/
|
|
262
|
-
const validateLocalizableResources = (config: PublishDetails) => {
|
|
263
|
-
if (!config.release.catalog["en-US"]) {
|
|
264
|
-
throw new Error("Please ensure you have the en-US locale strings in your configuration file.");
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const baselineSize = Object.keys(config.release.catalog["en-US"]).length;
|
|
268
|
-
Object.keys(config.release.catalog).forEach((locale) => {
|
|
269
|
-
const size = Object.keys(config.release.catalog[locale]).length;
|
|
270
|
-
|
|
271
|
-
if (size != baselineSize) {
|
|
272
|
-
throw new Error("Please ensure you have included all localized strings for all locales in your configuration file.");
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
const descsWrongLength = Object.values(config.release.catalog)
|
|
277
|
-
.map((x) => x.short_description)
|
|
278
|
-
.filter((desc) => !desc?.length || desc.length > 30);
|
|
279
|
-
|
|
280
|
-
if (descsWrongLength.length > 0) {
|
|
281
|
-
throw new Error("Please ensure all translations of short_description are between 0 and 30 characters");
|
|
282
|
-
}
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
const checkIconDimensions = async (iconPath: string): Promise<boolean> => {
|
|
286
|
-
const size = await runImgSize(iconPath);
|
|
287
|
-
|
|
288
|
-
return size?.width != size?.height || (size?.width ?? 0) != 512;
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
const checkScreenshotDimensions = async (imagePath: string): Promise<boolean> => {
|
|
292
|
-
const size = await runImgSize(imagePath);
|
|
293
|
-
|
|
294
|
-
return (size?.width ?? 0) < 1080 || (size?.height ?? 0) < 1080;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const checkBannerDimensions = async (imagePath: string): Promise<boolean> => {
|
|
298
|
-
const size = await runImgSize(imagePath);
|
|
299
|
-
|
|
300
|
-
return (size?.width ?? 0) != 1200 || (size?.height ?? 0) != 600;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const checkFeatureGraphicDimensions = async (imagePath: string): Promise<boolean> => {
|
|
304
|
-
const size = await runImgSize(imagePath);
|
|
305
|
-
|
|
306
|
-
return (size?.width ?? 0) != 1200 || (size?.height ?? 0) != 1200;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const checkVideoDimensions = async (imagePath: string): Promise<boolean> => {
|
|
310
|
-
const size = await getVideoDimensions(imagePath);
|
|
311
|
-
|
|
312
|
-
return (size?.width ?? 0) < 720 || (size?.height ?? 0) < 720;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const getAndroidDetails = async (
|
|
317
|
-
aaptDir: string,
|
|
318
|
-
apkPath: string,
|
|
319
|
-
developerOverridenLocales: [string]
|
|
320
|
-
): Promise<AndroidDetails> => {
|
|
321
|
-
try {
|
|
322
|
-
const { stdout } = await runExec(`${aaptDir}/aapt2 dump badging "${apkPath}"`);
|
|
323
|
-
|
|
324
|
-
const appPackage = new RegExp(
|
|
325
|
-
AaptPrefixes.packagePrefix + AaptPrefixes.quoteRegex
|
|
326
|
-
).exec(stdout);
|
|
327
|
-
const versionCode = new RegExp(
|
|
328
|
-
AaptPrefixes.verCodePrefix + AaptPrefixes.quoteRegex
|
|
329
|
-
).exec(stdout);
|
|
330
|
-
const versionName = new RegExp(
|
|
331
|
-
AaptPrefixes.verNamePrefix + AaptPrefixes.quoteRegex
|
|
332
|
-
).exec(stdout);
|
|
333
|
-
const minSdk = new RegExp(
|
|
334
|
-
AaptPrefixes.sdkPrefix + AaptPrefixes.quoteRegex
|
|
335
|
-
).exec(stdout);
|
|
336
|
-
const permissions = [...stdout.matchAll(/(?:uses-permission|uses-permission-sdk-23): name='([^']*)'/g)].flatMap(permission => permission[1]);
|
|
337
|
-
const locales = new RegExp(
|
|
338
|
-
AaptPrefixes.localePrefix + AaptPrefixes.quoteNonLazyRegex
|
|
339
|
-
).exec(stdout);
|
|
340
|
-
const isDebuggable = new RegExp(
|
|
341
|
-
AaptPrefixes.debuggableApkPrefix
|
|
342
|
-
).exec(stdout);
|
|
343
|
-
|
|
344
|
-
if (isDebuggable != null) {
|
|
345
|
-
throw new TypeError("Debug apks are not supported on Solana dApp store.\nSubmit a signed release apk")
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
let localeArray = Array.from(locales?.values() ?? []);
|
|
349
|
-
if (localeArray.length == 2) {
|
|
350
|
-
const localesSrc = localeArray[1];
|
|
351
|
-
localeArray = ["en-US"].concat(localesSrc.split("' '").slice(1));
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (permissions.includes("android.permission.INSTALL_PACKAGES") || permissions.includes("android.permission.DELETE_PACKAGES")) {
|
|
355
|
-
showMessage(
|
|
356
|
-
"App requests system app install/delete permission",
|
|
357
|
-
"Your app requests system install/delete permission which is managed by Solana dApp Store.\nThis app will be not approved for listing on Solana dApp Store.",
|
|
358
|
-
"error"
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (permissions.includes("android.permission.REQUEST_INSTALL_PACKAGES") || permissions.includes("android.permission.REQUEST_DELETE_PACKAGES")) {
|
|
363
|
-
showMessage(
|
|
364
|
-
"App requests install or delete permission",
|
|
365
|
-
"App will be subject to additional security reviews for listing on Solana dApp Store and processing time may be beyond regular review time",
|
|
366
|
-
"warning"
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if (permissions.includes("com.solanamobile.seedvault.ACCESS_SEED_VAULT") || permissions.includes("com.solanamobile.seedvault.ACCESS_SEED_VAULT_PRIVILEGED")) {
|
|
371
|
-
showMessage(
|
|
372
|
-
"App requests Seed Vault permission",
|
|
373
|
-
"If this is not a wallet application, your app maybe rejected from listing on Solana dApp Store.",
|
|
374
|
-
"warning"
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (developerOverridenLocales == null && localeArray.length >= 60 || developerOverridenLocales?.length >= 60) {
|
|
379
|
-
showMessage(
|
|
380
|
-
"Excessive language support detected",
|
|
381
|
-
"The bundle apk claims supports for following locales \n" +
|
|
382
|
-
localeArray +
|
|
383
|
-
"\nYou config.yaml claims support for following locales\n" +
|
|
384
|
-
developerOverridenLocales +
|
|
385
|
-
"\nIf this release does not support all these locales the release may be rejected\n." +
|
|
386
|
-
"You can override this list of supported locales in config.yaml" +
|
|
387
|
-
"\nSee details at https://developer.android.com/guide/topics/resources/multilingual-support#design for configuring the supported locales in your apk file",
|
|
388
|
-
"warning"
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
return {
|
|
393
|
-
android_package: appPackage?.[1] ?? "",
|
|
394
|
-
min_sdk: parseInt(minSdk?.[1] ?? "0", 10),
|
|
395
|
-
version_code: parseInt(versionCode?.[1] ?? "0", 10),
|
|
396
|
-
version: versionName?.[1] ?? "0",
|
|
397
|
-
cert_fingerprint: await extractCertFingerprint(aaptDir, apkPath),
|
|
398
|
-
permissions: permissions,
|
|
399
|
-
locales: developerOverridenLocales ?? localeArray
|
|
400
|
-
};
|
|
401
|
-
} catch (e) {
|
|
402
|
-
if (e instanceof TypeError) {
|
|
403
|
-
throw e
|
|
404
|
-
} else {
|
|
405
|
-
throw new Error(`There was an error parsing your APK. Please ensure you have installed Java and provided a valid Android tools directory containing AAPT2.\n` + e);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
export const extractCertFingerprint = async (aaptDir: string, apkPath: string): Promise<string> => {
|
|
411
|
-
const { stdout } = await runExec(`${aaptDir}/apksigner verify --print-certs -v "${apkPath}"`);
|
|
412
|
-
|
|
413
|
-
const regex = /Signer #1 certificate SHA-256 digest:\s*([a-fA-F0-9]+)/;
|
|
414
|
-
const match = stdout.match(regex);
|
|
415
|
-
|
|
416
|
-
if (match && match[1]) {
|
|
417
|
-
return match[1];
|
|
418
|
-
} else {
|
|
419
|
-
throw new Error("Could not obtain cert fingerprint")
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
export const writeToPublishDetails = async ({ app, release, lastSubmittedVersionOnChain, lastUpdatedVersionOnStore }: SaveToConfigArgs) => {
|
|
424
|
-
const currentConfig = await loadPublishDetailsWithChecks();
|
|
425
|
-
|
|
426
|
-
delete currentConfig.publisher.icon;
|
|
427
|
-
delete currentConfig.app.icon;
|
|
428
|
-
|
|
429
|
-
const newConfig: PublishDetails = {
|
|
430
|
-
publisher: {
|
|
431
|
-
...currentConfig.publisher,
|
|
432
|
-
},
|
|
433
|
-
app: {
|
|
434
|
-
...currentConfig.app,
|
|
435
|
-
address: app?.address ?? currentConfig.app.address
|
|
436
|
-
},
|
|
437
|
-
release: {
|
|
438
|
-
...currentConfig.release,
|
|
439
|
-
address: release?.address ?? currentConfig.release.address,
|
|
440
|
-
android_details: {
|
|
441
|
-
cert_fingerprint: release?.android_details?.cert_fingerprint ?? currentConfig.release.android_details?.cert_fingerprint,
|
|
442
|
-
min_sdk: release?.android_details?.min_sdk ?? currentConfig.release.android_details?.min_sdk,
|
|
443
|
-
version: release?.android_details?.version ?? currentConfig.release.android_details?.version,
|
|
444
|
-
version_code: release?.android_details?.version_code ?? currentConfig.release.android_details?.version_code,
|
|
445
|
-
locales: release?.android_details?.locales ?? currentConfig.release.android_details?.locales
|
|
446
|
-
}
|
|
447
|
-
},
|
|
448
|
-
solana_mobile_dapp_publisher_portal: currentConfig.solana_mobile_dapp_publisher_portal,
|
|
449
|
-
lastSubmittedVersionOnChain: lastSubmittedVersionOnChain ?? currentConfig.lastSubmittedVersionOnChain,
|
|
450
|
-
lastUpdatedVersionOnStore: lastUpdatedVersionOnStore ?? currentConfig.lastUpdatedVersionOnStore
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
fs.writeFileSync(Constants.getConfigFilePath(), dump(newConfig, {
|
|
454
|
-
lineWidth: -1
|
|
455
|
-
}));
|
|
456
|
-
};
|