@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,47 +0,0 @@
|
|
|
1
|
-
import { EnvVariables, S3Config } from "./EnvVariables.js";
|
|
2
|
-
|
|
3
|
-
export class S3StorageManager {
|
|
4
|
-
private _config: S3Config | undefined = undefined
|
|
5
|
-
|
|
6
|
-
public get hasS3Config(): boolean {
|
|
7
|
-
return this._config != undefined;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
public get s3Config(): S3Config {
|
|
11
|
-
return this._config as S3Config;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
constructor(private envVars: EnvVariables) {
|
|
15
|
-
if (envVars.hasS3EnvArgs) {
|
|
16
|
-
this._config = {
|
|
17
|
-
accessKey: this.envVars.s3Config.accessKey,
|
|
18
|
-
secretKey: this.envVars.s3Config.secretKey,
|
|
19
|
-
bucketName: this.envVars.s3Config.bucketName,
|
|
20
|
-
regionName: this.envVars.s3Config.regionName
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
parseCmdArg(cmdArg: string) {
|
|
26
|
-
if (!cmdArg || cmdArg == "") return;
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
//This will overwrite any existing parameters already obtained from the .env file
|
|
30
|
-
const parsedArray = JSON.parse(`${cmdArg}`);
|
|
31
|
-
|
|
32
|
-
if (parsedArray instanceof Array && parsedArray[0] == "s3") {
|
|
33
|
-
if (parsedArray.length != 5) throw new Error("Invalid parameters")
|
|
34
|
-
|
|
35
|
-
this._config = {
|
|
36
|
-
accessKey: parsedArray[1],
|
|
37
|
-
secretKey: parsedArray[2],
|
|
38
|
-
bucketName: parsedArray[3],
|
|
39
|
-
regionName: parsedArray[4]
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
} catch (e) {
|
|
43
|
-
throw new Error("There was an error parsing your s3 parameters from the CLI. Please ensure they are formatted correctly.");
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
}
|
package/src/config/index.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
publisher:
|
|
2
|
-
name: <<[REQUIRED] YOUR_PUBLISHER_NAME>>
|
|
3
|
-
website: <<[REQUIRED] URL_OF_PUBLISHER_WEBSITE>>
|
|
4
|
-
email: <<[REQUIRED] EMAIL_ADDRESS_TO_CONTACT_PUBLISHER>>
|
|
5
|
-
support_email: <<[Optional] SUPPORT_EMAIL_ADDRESS_FOR_END_USERS>>
|
|
6
|
-
app:
|
|
7
|
-
name: <<[REQUIRED] APP_NAME>>
|
|
8
|
-
address: ""
|
|
9
|
-
android_package: <<[REQUIRED] ANDROID_PACKAGE_NAME>>
|
|
10
|
-
urls:
|
|
11
|
-
license_url: <<[REQUIRED] URL For App's T&C. Don't put placeholder urls.>>
|
|
12
|
-
copyright_url: <<[REQUIRED] URL For App's Copyright. Don't put placeholder urls.>>
|
|
13
|
-
privacy_policy_url: <<[REQUIRED] URL For App's Privacy Policy. Don't put placeholder urls.>>
|
|
14
|
-
website: <<[REQUIRED] URL_OF_APP_WEBSITE>>
|
|
15
|
-
media:
|
|
16
|
-
- purpose: icon
|
|
17
|
-
uri: <<[REQUIRED] RELATIVE_PATH_TO_APP_ICON>>
|
|
18
|
-
release:
|
|
19
|
-
address: ""
|
|
20
|
-
media:
|
|
21
|
-
- purpose: icon
|
|
22
|
-
uri: <<[REQUIRED] RELATIVE_PATH_TO_RELEASE_ICON>>
|
|
23
|
-
- purpose: banner
|
|
24
|
-
uri: <<[REQUIRED] RELATIVE_PATH_TO_BANNER>>
|
|
25
|
-
- purpose: featureGraphic
|
|
26
|
-
uri: <<[Optional] RELATIVE_PATH_TO_FEATURE_GRAPHIC>>
|
|
27
|
-
- purpose: screenshot
|
|
28
|
-
uri: <<[REQUIRED] RELATIVE_PATH_TO_SCREENSHOT1>>
|
|
29
|
-
- purpose: screenshot
|
|
30
|
-
uri: <<[REQUIRED] RELATIVE_PATH_TO_SCREENSHOT2>>
|
|
31
|
-
- purpose: screenshot
|
|
32
|
-
uri: <<[REQUIRED] RELATIVE_PATH_TO_SCREENSHOT3>>
|
|
33
|
-
- purpose: screenshot
|
|
34
|
-
uri: <<[REQUIRED] RELATIVE_PATH_TO_SCREENSHOT4>>
|
|
35
|
-
- purpose: video
|
|
36
|
-
uri: <<[Optional] RELATIVE_PATH_TO_VIDEO1>>
|
|
37
|
-
files:
|
|
38
|
-
- purpose: install
|
|
39
|
-
uri: <<[REQUIRED] RELATIVE_PATH_TO_APK>>
|
|
40
|
-
catalog:
|
|
41
|
-
en-US:
|
|
42
|
-
name: >-
|
|
43
|
-
<<[REQUIRED] APP_NAME>>
|
|
44
|
-
short_description: >-
|
|
45
|
-
<<[REQUIRED] SHORT_APP_DESCRIPTION>>
|
|
46
|
-
long_description: >-
|
|
47
|
-
<<[REQUIRED] LONG_APP_DESCRIPTION>>
|
|
48
|
-
new_in_version: >-
|
|
49
|
-
<<[REQUIRED] WHATS_NEW_IN_THIS_VERSION>>
|
|
50
|
-
saga_features: >-
|
|
51
|
-
<<[Optional.] ANY_FEATURES_ONLY_AVAILBLE_WHEN_RUNNING_ON_SAGA>>
|
|
52
|
-
android_details:
|
|
53
|
-
locales:
|
|
54
|
-
- en-US
|
|
55
|
-
- <Add more supported locales>
|
|
56
|
-
solana_mobile_dapp_publisher_portal:
|
|
57
|
-
google_store_package: <<[Optional] ANDROID_PACKAGE_NAME_OF_GOOGLE_PLAY_STORE_VERSION>>
|
|
58
|
-
testing_instructions: >-
|
|
59
|
-
<<[REQUIRED] TESTING_INSTRUCTIONS. Please provide any test account details if applicable>>
|
|
60
|
-
alpha_testers:
|
|
61
|
-
- address: <<Optional. genesis token wallet address>>
|
|
62
|
-
comment: <<Optional. For internal use only>>
|
|
63
|
-
- address: <<Optional. genesis token wallet address>>
|
|
64
|
-
comment: <<Optional. For internal use only>>
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import fs, { read } from "fs";
|
|
2
|
-
import yaml from "js-yaml";
|
|
3
|
-
import generateSchema from "generate-schema";
|
|
4
|
-
|
|
5
|
-
try {
|
|
6
|
-
const yamlSrc = fs.readFileSync('./src/prebuild_schema/publishing_source.yaml', 'utf8')
|
|
7
|
-
const convertedYaml = yaml.load(yamlSrc);
|
|
8
|
-
fs.writeFileSync('./src/generated/config_obj.json', Buffer.from(JSON.stringify(convertedYaml)), 'utf-8');
|
|
9
|
-
|
|
10
|
-
const schema = generateSchema.json('result', convertedYaml);
|
|
11
|
-
// CLI 0.3.0: Adding requirement for `short_description` so validation will catch
|
|
12
|
-
schema["properties"]
|
|
13
|
-
["release"]
|
|
14
|
-
["properties"]
|
|
15
|
-
["catalog"]
|
|
16
|
-
["properties"]
|
|
17
|
-
["en-US"].required = ["short_description"];
|
|
18
|
-
|
|
19
|
-
schema["properties"]
|
|
20
|
-
["publisher"]
|
|
21
|
-
.required = ["name", "website", "email"];
|
|
22
|
-
|
|
23
|
-
// Generator adds some keys/values we don't need & mess up validation
|
|
24
|
-
delete schema.$schema;
|
|
25
|
-
delete schema.title;
|
|
26
|
-
|
|
27
|
-
const toWrite = Buffer.from(JSON.stringify(schema));
|
|
28
|
-
fs.writeFileSync('./src/generated/config_schema.json', toWrite, 'utf-8');
|
|
29
|
-
} catch (e) {
|
|
30
|
-
console.log(":: Schema generation step failed ::");
|
|
31
|
-
}
|
|
@@ -1,99 +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
|
-
|
|
6
|
-
type URI = string;
|
|
7
|
-
|
|
8
|
-
type Asset = {
|
|
9
|
-
path: string;
|
|
10
|
-
sha256: string;
|
|
11
|
-
uri: URI;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export type AssetManifestSchema = {
|
|
15
|
-
schema_version: string;
|
|
16
|
-
assets: {
|
|
17
|
-
[path: string]: Asset;
|
|
18
|
-
};
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
// TODO(jon): We need to manage the removal / replacement of assets in the manifest
|
|
22
|
-
export class CachedStorageDriver implements StorageDriver {
|
|
23
|
-
// NOTE: this schema version is independent of the publishing JSON schema. It should be updated
|
|
24
|
-
// when the AssetManifestSchema or Asset types are updated.
|
|
25
|
-
static readonly SCHEMA_VERSION = "0.1";
|
|
26
|
-
|
|
27
|
-
assetManifest: AssetManifestSchema;
|
|
28
|
-
assetManifestPath: string;
|
|
29
|
-
storageDriver: StorageDriver;
|
|
30
|
-
|
|
31
|
-
constructor(
|
|
32
|
-
storageDriver: StorageDriver,
|
|
33
|
-
{ assetManifestPath }: { assetManifestPath: string }
|
|
34
|
-
) {
|
|
35
|
-
this.assetManifestPath = assetManifestPath;
|
|
36
|
-
this.assetManifest = this.loadAssetManifest(assetManifestPath) ?? {
|
|
37
|
-
schema_version: CachedStorageDriver.SCHEMA_VERSION,
|
|
38
|
-
assets: {},
|
|
39
|
-
};
|
|
40
|
-
this.storageDriver = storageDriver;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async getUploadPrice(bytes: number) {
|
|
44
|
-
return this.storageDriver.getUploadPrice(bytes);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
loadAssetManifest(filename: string): AssetManifestSchema | undefined {
|
|
48
|
-
try {
|
|
49
|
-
return JSON.parse(
|
|
50
|
-
fs.readFileSync(filename, "utf-8")
|
|
51
|
-
) as AssetManifestSchema;
|
|
52
|
-
} catch (error) {
|
|
53
|
-
console.warn(`Failed opening ${filename}; initializing with a blank asset manifest`);
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
uploadedAsset(filename: string, { sha256 }: { sha256: string }) {
|
|
59
|
-
if (this.assetManifest.assets[filename]?.sha256 === sha256) {
|
|
60
|
-
return this.assetManifest.assets[filename];
|
|
61
|
-
}
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async upload(file: MetaplexFile): Promise<string> {
|
|
66
|
-
// `inline.json` is the NFT-related metadata. This data is not stable so we'll skip the caching step
|
|
67
|
-
if (file.fileName === "inline.json") {
|
|
68
|
-
return await this.storageDriver.upload(file);
|
|
69
|
-
}
|
|
70
|
-
const hash = createHash("sha256").update(file.buffer).digest("base64");
|
|
71
|
-
|
|
72
|
-
const uploadedAsset = this.uploadedAsset(file.fileName, { sha256: hash });
|
|
73
|
-
if (uploadedAsset) {
|
|
74
|
-
console.log(
|
|
75
|
-
`Asset ${file.fileName} already uploaded at ${uploadedAsset.uri}`
|
|
76
|
-
);
|
|
77
|
-
return uploadedAsset.uri;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
console.log(`Uploading ${file.fileName}`);
|
|
81
|
-
const uri = await this.storageDriver.upload(file);
|
|
82
|
-
|
|
83
|
-
this.assetManifest.assets[file.fileName] = {
|
|
84
|
-
path: file.fileName,
|
|
85
|
-
sha256: hash,
|
|
86
|
-
uri,
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
await fs.promises.writeFile(
|
|
90
|
-
path.join(process.cwd(), this.assetManifestPath),
|
|
91
|
-
// Something is really weird, I can't seem to stringify `this.assetManifest` straight-up. Here be dragons
|
|
92
|
-
JSON.stringify({ assets: { ...this.assetManifest.assets } }, null, 2),
|
|
93
|
-
"utf-8"
|
|
94
|
-
);
|
|
95
|
-
console.log(`${file.fileName} uploaded at ${uri}`)
|
|
96
|
-
|
|
97
|
-
return uri;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
@@ -1,277 +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
|
-
|
|
7
|
-
const debug = debugModule("cli:turbo-storage");
|
|
8
|
-
|
|
9
|
-
interface TurboClient {
|
|
10
|
-
getUploadCosts(args: {
|
|
11
|
-
bytes: number[];
|
|
12
|
-
}): Promise<Array<{ winc: string | number | bigint }>>;
|
|
13
|
-
getBalance(): Promise<{ winc: string | number | bigint }>;
|
|
14
|
-
getWincForToken?(args: {
|
|
15
|
-
tokenAmount: number;
|
|
16
|
-
}): Promise<{ winc: string | number | bigint }>;
|
|
17
|
-
topUpWithTokens?(args: { tokenAmount: string | number }): Promise<unknown>;
|
|
18
|
-
uploadFile(args: {
|
|
19
|
-
fileStreamFactory: () => Buffer;
|
|
20
|
-
fileSizeFactory: () => number;
|
|
21
|
-
dataItemOpts?: { tags?: Array<{ name: string; value: string }> };
|
|
22
|
-
}): Promise<{ id: string }>;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const SOL_IN_LAMPORTS = 1_000_000_000;
|
|
26
|
-
const MIN_TOP_UP_LAMPORTS = 1_000_000;
|
|
27
|
-
const MIN_TOP_UP_SOL = MIN_TOP_UP_LAMPORTS / SOL_IN_LAMPORTS;
|
|
28
|
-
|
|
29
|
-
const CONSTANTS = {
|
|
30
|
-
FREE_UPLOAD_LIMIT: 97_280, // 95 KiB
|
|
31
|
-
UPLOAD_DELAY_MS: 2000,
|
|
32
|
-
MAX_RETRIES: 5,
|
|
33
|
-
SOL_IN_LAMPORTS,
|
|
34
|
-
MIN_TOP_UP_SOL,
|
|
35
|
-
MIN_TOP_UP_LAMPORTS,
|
|
36
|
-
BACKOFF: {
|
|
37
|
-
BASE_MS: 500,
|
|
38
|
-
MAX_MS: 8000,
|
|
39
|
-
},
|
|
40
|
-
GATEWAYS: {
|
|
41
|
-
devnet: "https://turbo.ardrive.dev/raw",
|
|
42
|
-
mainnet: "https://arweave.net",
|
|
43
|
-
},
|
|
44
|
-
} as const;
|
|
45
|
-
|
|
46
|
-
const delay = (ms: number): Promise<void> =>
|
|
47
|
-
new Promise((resolve) => setTimeout(resolve, ms));
|
|
48
|
-
|
|
49
|
-
export class TurboStorageDriver {
|
|
50
|
-
private turbo: TurboClient;
|
|
51
|
-
private bufferPercentage: number;
|
|
52
|
-
private network: "devnet" | "mainnet";
|
|
53
|
-
|
|
54
|
-
private uploadQueue: Array<{
|
|
55
|
-
file: MetaplexFile;
|
|
56
|
-
resolve: (url: string) => void;
|
|
57
|
-
reject: (error: Error) => void;
|
|
58
|
-
}> = [];
|
|
59
|
-
private isProcessingQueue = false;
|
|
60
|
-
|
|
61
|
-
constructor(
|
|
62
|
-
keypair: Keypair,
|
|
63
|
-
network: "devnet" | "mainnet" = "mainnet",
|
|
64
|
-
bufferPercentage = 20
|
|
65
|
-
) {
|
|
66
|
-
this.network = network;
|
|
67
|
-
this.bufferPercentage = bufferPercentage;
|
|
68
|
-
|
|
69
|
-
this.turbo = TurboFactory.authenticated({
|
|
70
|
-
privateKey: bs58.encode(keypair.secretKey),
|
|
71
|
-
token: "solana",
|
|
72
|
-
...this.getServiceUrls(network === "devnet"),
|
|
73
|
-
}) as TurboClient;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
private getServiceUrls(isDev: boolean) {
|
|
77
|
-
const base = isDev ? "ardrive.dev" : "ardrive.io";
|
|
78
|
-
return {
|
|
79
|
-
uploadUrl: `https://upload.${base}`,
|
|
80
|
-
paymentUrl: `https://payment.${base}`,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async getUploadPrice(bytes: number): Promise<bigint> {
|
|
85
|
-
if (bytes <= CONSTANTS.FREE_UPLOAD_LIMIT) return BigInt(0);
|
|
86
|
-
|
|
87
|
-
const [cost] = await this.turbo.getUploadCosts({ bytes: [bytes] });
|
|
88
|
-
const base = BigInt(String(cost.winc));
|
|
89
|
-
return (base * BigInt(100 + this.bufferPercentage)) / BigInt(100);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
private async withRetry<T>(
|
|
93
|
-
operation: () => Promise<T>,
|
|
94
|
-
isRetriable: (error: string) => boolean = (msg) =>
|
|
95
|
-
msg.includes("429") || msg.includes("Too Many Requests")
|
|
96
|
-
): Promise<T> {
|
|
97
|
-
for (let retry = 0; retry <= CONSTANTS.MAX_RETRIES; retry++) {
|
|
98
|
-
try {
|
|
99
|
-
return await operation();
|
|
100
|
-
} catch (error) {
|
|
101
|
-
const errorMessage =
|
|
102
|
-
error instanceof Error ? error.message : String(error);
|
|
103
|
-
|
|
104
|
-
if (retry < CONSTANTS.MAX_RETRIES && isRetriable(errorMessage)) {
|
|
105
|
-
const delayMs = Math.min(
|
|
106
|
-
CONSTANTS.BACKOFF.BASE_MS * Math.pow(2, retry),
|
|
107
|
-
CONSTANTS.BACKOFF.MAX_MS
|
|
108
|
-
);
|
|
109
|
-
console.log(
|
|
110
|
-
`Rate limited, retrying after ${delayMs}ms (attempt ${retry + 1}/${
|
|
111
|
-
CONSTANTS.MAX_RETRIES
|
|
112
|
-
})...`
|
|
113
|
-
);
|
|
114
|
-
await delay(delayMs);
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
throw error;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
throw new Error("Max retries exceeded");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
private formatInsufficientFundsError(errorMessage: string): void {
|
|
124
|
-
const match = errorMessage.match(/insufficient lamports (\d+), need (\d+)/);
|
|
125
|
-
if (!match) return;
|
|
126
|
-
|
|
127
|
-
const [current, needed] = [BigInt(match[1]), BigInt(match[2])];
|
|
128
|
-
const [currentSOL, neededSOL] = [
|
|
129
|
-
Number(current) / 1e9,
|
|
130
|
-
Number(needed) / 1e9,
|
|
131
|
-
];
|
|
132
|
-
|
|
133
|
-
console.error(`\nInsufficient SOL balance for top-up:`);
|
|
134
|
-
console.error(` Current: ${currentSOL.toFixed(9)} SOL`);
|
|
135
|
-
console.error(` Required: ${neededSOL.toFixed(9)} SOL`);
|
|
136
|
-
console.error(` Shortfall: ${(neededSOL - currentSOL).toFixed(9)} SOL\n`);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
private async topUpCredits(wincAmount: bigint): Promise<void> {
|
|
140
|
-
if (wincAmount === 0n) {
|
|
141
|
-
debug("No Winston Credits requested; skipping top-up.");
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
await this.withRetry(async () => {
|
|
147
|
-
const exchangeRate = await this.turbo.getWincForToken?.({
|
|
148
|
-
tokenAmount: CONSTANTS.SOL_IN_LAMPORTS,
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
if (!exchangeRate) {
|
|
152
|
-
throw new Error("Unable to get Winston Credits exchange rate");
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const wincPerSol = BigInt(String(exchangeRate.winc));
|
|
156
|
-
if (wincPerSol <= 0n) {
|
|
157
|
-
throw new Error("Invalid Winston Credits exchange rate");
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const solInLamports = BigInt(CONSTANTS.SOL_IN_LAMPORTS);
|
|
161
|
-
const numerator = wincAmount * solInLamports;
|
|
162
|
-
const lamportsCalculated =
|
|
163
|
-
(numerator + (wincPerSol - 1n)) / wincPerSol;
|
|
164
|
-
|
|
165
|
-
const minLamports = BigInt(CONSTANTS.MIN_TOP_UP_LAMPORTS);
|
|
166
|
-
const lamportsToUse =
|
|
167
|
-
lamportsCalculated < minLamports ? minLamports : lamportsCalculated;
|
|
168
|
-
|
|
169
|
-
if (lamportsToUse > lamportsCalculated) {
|
|
170
|
-
debug(
|
|
171
|
-
`Applying minimum top-up of ${CONSTANTS.MIN_TOP_UP_SOL} SOL (${CONSTANTS.MIN_TOP_UP_LAMPORTS} lamports)`
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
const solAmount =
|
|
175
|
-
Number(lamportsToUse) / CONSTANTS.SOL_IN_LAMPORTS;
|
|
176
|
-
|
|
177
|
-
debug(
|
|
178
|
-
`Buying at least ${wincAmount} Winston Credits (~${solAmount.toFixed(9)} SOL / ${lamportsToUse} lamports)`
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
await this.turbo.topUpWithTokens?.({
|
|
182
|
-
tokenAmount: String(
|
|
183
|
-
lamportToTokenAmount(lamportsToUse.toString())
|
|
184
|
-
),
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
debug(`Top-up initiated for ${wincAmount} Winston Credits`);
|
|
188
|
-
});
|
|
189
|
-
} catch (error) {
|
|
190
|
-
const errorMessage =
|
|
191
|
-
error instanceof Error ? error.message : String(error);
|
|
192
|
-
debug("Top-up failed:", error);
|
|
193
|
-
|
|
194
|
-
if (errorMessage.includes("insufficient lamports")) {
|
|
195
|
-
this.formatInsufficientFundsError(errorMessage);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
throw new Error(
|
|
199
|
-
`Failed to top up ${wincAmount} Winston Credits: ${errorMessage}`
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
private async checkBalanceAndTopUp(requiredWinc: bigint): Promise<void> {
|
|
205
|
-
if (requiredWinc === BigInt(0)) return;
|
|
206
|
-
|
|
207
|
-
const current = BigInt(String((await this.turbo.getBalance()).winc));
|
|
208
|
-
|
|
209
|
-
if (current >= requiredWinc) {
|
|
210
|
-
debug(
|
|
211
|
-
`Sufficient balance: ${current} Winston Credits (required: ${requiredWinc})`
|
|
212
|
-
);
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const deficit = requiredWinc - current;
|
|
217
|
-
debug(
|
|
218
|
-
`Current: ${current}, Required: ${requiredWinc}, Topping up: ${deficit}`
|
|
219
|
-
);
|
|
220
|
-
await this.topUpCredits(deficit);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
private async processQueue(): Promise<void> {
|
|
224
|
-
if (this.isProcessingQueue || !this.uploadQueue.length) return;
|
|
225
|
-
|
|
226
|
-
this.isProcessingQueue = true;
|
|
227
|
-
|
|
228
|
-
while (this.uploadQueue.length > 0) {
|
|
229
|
-
const item = this.uploadQueue.shift();
|
|
230
|
-
if (!item) continue;
|
|
231
|
-
|
|
232
|
-
try {
|
|
233
|
-
debug(
|
|
234
|
-
`Processing upload for ${item.file.fileName} (${item.file.buffer.length} bytes)`
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
const estimated = await this.getUploadPrice(item.file.buffer.length);
|
|
238
|
-
await this.checkBalanceAndTopUp(estimated);
|
|
239
|
-
|
|
240
|
-
const tags = [...(item.file.tags ?? [])];
|
|
241
|
-
if (item.file.contentType) {
|
|
242
|
-
tags.push({ name: "Content-Type", value: item.file.contentType });
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const uploadResult = await this.turbo.uploadFile({
|
|
246
|
-
fileStreamFactory: () => item.file.buffer,
|
|
247
|
-
fileSizeFactory: () => item.file.buffer.byteLength,
|
|
248
|
-
dataItemOpts: { tags },
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
const gateway = CONSTANTS.GATEWAYS[this.network];
|
|
252
|
-
const url = `${gateway}/${uploadResult.id}`;
|
|
253
|
-
debug(`Upload complete: ${url}`);
|
|
254
|
-
item.resolve(url);
|
|
255
|
-
|
|
256
|
-
if (this.uploadQueue.length > 0) {
|
|
257
|
-
debug(`Waiting ${CONSTANTS.UPLOAD_DELAY_MS}ms before next upload...`);
|
|
258
|
-
await delay(CONSTANTS.UPLOAD_DELAY_MS);
|
|
259
|
-
}
|
|
260
|
-
} catch (error) {
|
|
261
|
-
item.reject(error instanceof Error ? error : new Error(String(error)));
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
this.isProcessingQueue = false;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
async upload(file: MetaplexFile): Promise<string> {
|
|
269
|
-
return new Promise((resolve, reject) => {
|
|
270
|
-
debug(
|
|
271
|
-
`Queueing upload for ${file.fileName} (${file.buffer.length} bytes)`
|
|
272
|
-
);
|
|
273
|
-
this.uploadQueue.push({ file, resolve, reject });
|
|
274
|
-
this.processQueue().catch(reject);
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
}
|
package/src/upload/index.ts
DELETED