@shelby-protocol/cli 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/entry.js +1338 -926
  2. package/package.json +9 -7
package/bin/entry.js CHANGED
@@ -4,12 +4,73 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // package.json
7
- var version = "0.0.12";
7
+ var version = "0.0.14";
8
8
 
9
9
  // src/commands/account.tsx
10
10
  import readline from "readline";
11
11
  import { Account as Account3, AptosApiError as AptosApiError2, Ed25519PrivateKey as Ed25519PrivateKey4 } from "@aptos-labs/ts-sdk";
12
12
 
13
+ // ../../packages/sdk/dist/chunk-RBFWGDMY.mjs
14
+ import { AptosConfig, Network } from "@aptos-labs/ts-sdk";
15
+ var getAptosConfig = (config) => {
16
+ if (config.aptos) {
17
+ return new AptosConfig(config.aptos);
18
+ }
19
+ let aptosConfig;
20
+ switch (config.network) {
21
+ case "local":
22
+ aptosConfig = new AptosConfig({
23
+ network: Network.LOCAL
24
+ });
25
+ break;
26
+ case "shelbynet":
27
+ aptosConfig = new AptosConfig({
28
+ network: Network.SHELBYNET,
29
+ clientConfig: {
30
+ API_KEY: config.apiKey
31
+ }
32
+ });
33
+ break;
34
+ default:
35
+ throw new Error(`Unsupported network: ${config.network}`);
36
+ }
37
+ return aptosConfig;
38
+ };
39
+
40
+ // ../../packages/sdk/dist/chunk-SEXQTDX6.mjs
41
+ import { Network as Network2 } from "@aptos-labs/ts-sdk";
42
+ var NetworkToShelbyRPCBaseUrl = {
43
+ [Network2.SHELBYNET]: "https://api.shelbynet.shelby.xyz/shelby",
44
+ [Network2.DEVNET]: void 0,
45
+ [Network2.TESTNET]: void 0,
46
+ [Network2.MAINNET]: void 0,
47
+ [Network2.LOCAL]: void 0,
48
+ [Network2.CUSTOM]: void 0
49
+ };
50
+ var NetworkToShelbyBlobIndexerBaseUrl = {
51
+ [Network2.SHELBYNET]: "https://api.shelbynet.aptoslabs.com/nocode/v1/public/cmforrguw0042s601fn71f9l2/v1/graphql",
52
+ [Network2.DEVNET]: void 0,
53
+ [Network2.TESTNET]: void 0,
54
+ [Network2.MAINNET]: void 0,
55
+ [Network2.LOCAL]: void 0,
56
+ [Network2.CUSTOM]: void 0
57
+ };
58
+ var SHELBY_DEPLOYER = "0xc63d6a5efb0080a6029403131715bd4971e1149f7cc099aac69bb0069b3ddbf5";
59
+ var SHELBYUSD_TOKEN_NAME = "ShelbyUSD";
60
+ var SHELBYUSD_FA_METADATA_ADDRESS = "0x1b18363a9f1fe5e6ebf247daba5cc1c18052bb232efdc4c50f556053922d98e1";
61
+
62
+ // ../../packages/sdk/dist/chunk-CTGCK3H2.mjs
63
+ import {
64
+ AccountAddress,
65
+ Aptos,
66
+ Hex
67
+ } from "@aptos-labs/ts-sdk";
68
+
69
+ // ../../packages/sdk/dist/chunk-I6NG5GNL.mjs
70
+ function sleep(ms) {
71
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
72
+ }
73
+
13
74
  // ../../node_modules/.pnpm/tslib@2.8.1/node_modules/tslib/tslib.es6.mjs
14
75
  var __assign = function() {
15
76
  __assign = Object.assign || function __assign2(t) {
@@ -3209,7 +3270,7 @@ var extras = {
3209
3270
  gql["default"] = gql;
3210
3271
  var lib_default = gql;
3211
3272
 
3212
- // ../../packages/sdk/dist/chunk-DLMDDEWF.mjs
3273
+ // ../../packages/sdk/dist/chunk-WJKSPJSS.mjs
3213
3274
  var GetBlobsDocument = lib_default`
3214
3275
  query getBlobs($where: blobs_bool_exp, $orderBy: [blobs_order_by!], $limit: Int, $offset: Int) {
3215
3276
  blobs(where: $where, order_by: $orderBy, limit: $limit, offset: $offset) {
@@ -3245,6 +3306,24 @@ var GetBlobActivitiesDocument = lib_default`
3245
3306
  }
3246
3307
  }
3247
3308
  `;
3309
+ var GetBlobsCountDocument = lib_default`
3310
+ query getBlobsCount($where: blobs_bool_exp) {
3311
+ blobs_aggregate(where: $where) {
3312
+ aggregate {
3313
+ count
3314
+ }
3315
+ }
3316
+ }
3317
+ `;
3318
+ var GetBlobActivitiesCountDocument = lib_default`
3319
+ query getBlobActivitiesCount($where: blob_activities_bool_exp) {
3320
+ blob_activities_aggregate(where: $where) {
3321
+ aggregate {
3322
+ count
3323
+ }
3324
+ }
3325
+ }
3326
+ `;
3248
3327
  var defaultWrapper = (action, _operationName, _operationType, _variables) => action();
3249
3328
  function getSdk(client, withWrapper = defaultWrapper) {
3250
3329
  return {
@@ -3253,60 +3332,17 @@ function getSdk(client, withWrapper = defaultWrapper) {
3253
3332
  },
3254
3333
  getBlobActivities(variables, requestHeaders, signal) {
3255
3334
  return withWrapper((wrappedRequestHeaders) => client.request({ document: GetBlobActivitiesDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), "getBlobActivities", "query", variables);
3335
+ },
3336
+ getBlobsCount(variables, requestHeaders, signal) {
3337
+ return withWrapper((wrappedRequestHeaders) => client.request({ document: GetBlobsCountDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), "getBlobsCount", "query", variables);
3338
+ },
3339
+ getBlobActivitiesCount(variables, requestHeaders, signal) {
3340
+ return withWrapper((wrappedRequestHeaders) => client.request({ document: GetBlobActivitiesCountDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), "getBlobActivitiesCount", "query", variables);
3256
3341
  }
3257
3342
  };
3258
3343
  }
3259
3344
 
3260
- // ../../packages/sdk/dist/chunk-RBFWGDMY.mjs
3261
- import { AptosConfig, Network } from "@aptos-labs/ts-sdk";
3262
- var getAptosConfig = (config) => {
3263
- if (config.aptos) {
3264
- return new AptosConfig(config.aptos);
3265
- }
3266
- let aptosConfig;
3267
- switch (config.network) {
3268
- case "local":
3269
- aptosConfig = new AptosConfig({
3270
- network: Network.LOCAL
3271
- });
3272
- break;
3273
- case "shelbynet":
3274
- aptosConfig = new AptosConfig({
3275
- network: Network.SHELBYNET,
3276
- clientConfig: {
3277
- API_KEY: config.apiKey
3278
- }
3279
- });
3280
- break;
3281
- default:
3282
- throw new Error(`Unsupported network: ${config.network}`);
3283
- }
3284
- return aptosConfig;
3285
- };
3286
-
3287
- // ../../packages/sdk/dist/chunk-SEXQTDX6.mjs
3288
- import { Network as Network2 } from "@aptos-labs/ts-sdk";
3289
- var NetworkToShelbyRPCBaseUrl = {
3290
- [Network2.SHELBYNET]: "https://api.shelbynet.shelby.xyz/shelby",
3291
- [Network2.DEVNET]: void 0,
3292
- [Network2.TESTNET]: void 0,
3293
- [Network2.MAINNET]: void 0,
3294
- [Network2.LOCAL]: void 0,
3295
- [Network2.CUSTOM]: void 0
3296
- };
3297
- var NetworkToShelbyBlobIndexerBaseUrl = {
3298
- [Network2.SHELBYNET]: "https://api.shelbynet.aptoslabs.com/nocode/v1/public/cmforrguw0042s601fn71f9l2/v1/graphql",
3299
- [Network2.DEVNET]: void 0,
3300
- [Network2.TESTNET]: void 0,
3301
- [Network2.MAINNET]: void 0,
3302
- [Network2.LOCAL]: void 0,
3303
- [Network2.CUSTOM]: void 0
3304
- };
3305
- var SHELBY_DEPLOYER = "0xc63d6a5efb0080a6029403131715bd4971e1149f7cc099aac69bb0069b3ddbf5";
3306
- var SHELBYUSD_TOKEN_NAME = "ShelbyUSD";
3307
- var SHELBYUSD_FA_METADATA_ADDRESS = "0x1b18363a9f1fe5e6ebf247daba5cc1c18052bb232efdc4c50f556053922d98e1";
3308
-
3309
- // ../../packages/sdk/dist/chunk-VPT45MTZ.mjs
3345
+ // ../../packages/sdk/dist/chunk-T6TVHFJO.mjs
3310
3346
  import { Network as Network3 } from "@aptos-labs/ts-sdk";
3311
3347
 
3312
3348
  // ../../node_modules/.pnpm/graphql-request@7.2.0_graphql@16.11.0/node_modules/graphql-request/build/legacy/classes/ClientError.js
@@ -3821,7 +3857,7 @@ var parseRequestArgs = (documentOrOptions, variables, requestHeaders) => {
3821
3857
  };
3822
3858
  };
3823
3859
 
3824
- // ../../packages/sdk/dist/chunk-VPT45MTZ.mjs
3860
+ // ../../packages/sdk/dist/chunk-T6TVHFJO.mjs
3825
3861
  function createShelbyIndexerClient(baseUrl, options) {
3826
3862
  const graphqlClient = new GraphQLClient(baseUrl, options);
3827
3863
  return getSdk(graphqlClient);
@@ -3854,14 +3890,8 @@ function getShelbyIndexerClient(config) {
3854
3890
  });
3855
3891
  }
3856
3892
 
3857
- // ../../packages/sdk/dist/chunk-OTBLZL2S.mjs
3858
- import { AccountAddress } from "@aptos-labs/ts-sdk";
3859
- var createBlobKey = (params) => {
3860
- return `@${AccountAddress.from(params.account).toStringLongWithoutPrefix()}/${params.blobName}`;
3861
- };
3862
-
3863
- // ../../packages/sdk/dist/chunk-KBUWZXFA.mjs
3864
- import { Hex } from "@aptos-labs/ts-sdk";
3893
+ // ../../packages/sdk/dist/chunk-4JZO2D7T.mjs
3894
+ import { Hex as Hex2 } from "@aptos-labs/ts-sdk";
3865
3895
  async function* readInChunks(input, chunkSize) {
3866
3896
  let idx = 0;
3867
3897
  if (isReadableStream(input)) {
@@ -3921,7 +3951,7 @@ function zeroPadBytes(buffer, desiredLength) {
3921
3951
  return paddedBuffer;
3922
3952
  }
3923
3953
  async function concatHashes(parts) {
3924
- const chunks = parts.map((part) => Hex.fromHexInput(part).toUint8Array());
3954
+ const chunks = parts.map((part) => Hex2.fromHexInput(part).toUint8Array());
3925
3955
  const totalLength = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
3926
3956
  const combined = new Uint8Array(totalLength);
3927
3957
  let offset = 0;
@@ -3929,7 +3959,7 @@ async function concatHashes(parts) {
3929
3959
  combined.set(chunk, offset);
3930
3960
  offset += chunk.byteLength;
3931
3961
  }
3932
- return Hex.fromHexInput(
3962
+ return Hex2.fromHexInput(
3933
3963
  new Uint8Array(await crypto.subtle.digest("SHA-256", combined))
3934
3964
  );
3935
3965
  }
@@ -3945,6 +3975,10 @@ function buildRequestUrl(path6, baseUrl) {
3945
3975
  const safePath = path6.replace(/^\/+/, "");
3946
3976
  return new URL(safePath, safeBase);
3947
3977
  }
3978
+ function getBlobNameSuffix(blobName) {
3979
+ const parts = blobName.split("/");
3980
+ return parts.slice(1).join("/") || "";
3981
+ }
3948
3982
 
3949
3983
  // ../../packages/sdk/dist/chunk-ZPW742E7.mjs
3950
3984
  var ERASURE_CODE_PARAMS = {
@@ -4125,143 +4159,420 @@ function buildClayConfig(input) {
4125
4159
  };
4126
4160
  }
4127
4161
 
4128
- // ../../packages/sdk/dist/chunk-LTV26KU4.mjs
4162
+ // ../../packages/sdk/dist/chunk-FIFKKWXV.mjs
4163
+ import { AccountAddress as AccountAddress2 } from "@aptos-labs/ts-sdk";
4129
4164
  import { z } from "zod";
4130
- var ChunksetCommitmentSchema = z.object({
4131
- // Chunkset root (vector commitment of child chunks)
4132
- chunkset_root: z.string().nullable(),
4133
- // the size is known statically from the current configuration
4134
- chunk_commitments: z.array(z.string())
4135
- }).refine(
4136
- (data) => {
4137
- return data.chunk_commitments.length === DEFAULT_ERASURE_K + DEFAULT_ERASURE_M;
4138
- },
4139
- {
4140
- message: `Chunkset must have exactly ${DEFAULT_ERASURE_K + DEFAULT_ERASURE_M} chunks (ERASURE_K + ERASURE_M = ${DEFAULT_ERASURE_K} + ${DEFAULT_ERASURE_M})`,
4141
- path: ["chunk_commitments"]
4142
- }
4143
- );
4144
- function expectedTotalChunksets(rawSize, chunksetSize = DEFAULT_CHUNKSET_SIZE_BYTES) {
4145
- if (chunksetSize <= 0) {
4146
- throw new Error("chunksetSize must be positive");
4147
- }
4148
- if (rawSize === 0) return 1;
4149
- return Math.ceil(rawSize / chunksetSize);
4150
- }
4151
- var BlobCommitmentsSchema = z.object({
4152
- schema_version: z.string(),
4153
- raw_data_size: z.number(),
4154
- // FIXME I am not sure about this being here, or if it should be somewhere else
4155
- blob_merkle_root: z.string(),
4156
- chunkset_commitments: z.array(ChunksetCommitmentSchema)
4157
- }).refine(
4158
- (data) => {
4159
- return expectedTotalChunksets(data.raw_data_size) === data.chunkset_commitments.length;
4160
- },
4161
- {
4162
- message: "Total chunkset count mismatches with raw data size",
4163
- // FIXME put more details in here
4164
- path: ["chunkset_commitments"]
4165
- }
4166
- );
4167
- async function generateChunksetCommitments(shouldPad, chunksetIdx, chunksetData, expectedChunksetSize, provider, onChunk) {
4168
- const { erasure_n } = provider.config;
4169
- const chunkCommitments = [];
4170
- const chunksetPayload = shouldPad ? zeroPadBytes(chunksetData, expectedChunksetSize) : validatePrePaddedChunkset(
4171
- chunksetData,
4172
- expectedChunksetSize,
4173
- chunksetIdx
4174
- );
4175
- const { chunks } = provider.encode(chunksetPayload);
4176
- if (chunks.length !== erasure_n) {
4177
- throw new Error(
4178
- `Erasure provider produced ${chunks.length} chunks, expected ${erasure_n}.`
4179
- );
4180
- }
4181
- let chunkIdx = 0;
4182
- for (const chunkData of chunks) {
4183
- if (onChunk !== void 0) {
4184
- await onChunk(chunksetIdx, chunkIdx, chunkData);
4185
- }
4186
- const chunkHash = await concatHashes([chunkData]);
4187
- chunkCommitments.push(chunkHash);
4188
- chunkIdx += 1;
4189
- }
4190
- const h = await concatHashes(
4191
- chunkCommitments.map((chunk) => chunk.toUint8Array())
4192
- );
4193
- const entry = {
4194
- chunkset_root: h.toString(),
4195
- chunk_commitments: chunkCommitments.map((chunk) => chunk.toString())
4196
- };
4197
- return { h, entry };
4198
- }
4199
- async function generateCommitments(provider, fullData, onChunk, options) {
4200
- const expectedChunksetSize = DEFAULT_CHUNKSET_SIZE_BYTES;
4201
- const shouldPad = options?.pad ?? true;
4202
- const chunksetCommitments = [];
4203
- const chunksetCommitmentHashes = [];
4204
- let rawDataSize = 0;
4205
- const chunksetGen = readInChunks(fullData, expectedChunksetSize);
4206
- for await (const [chunksetIdx, chunksetData] of chunksetGen) {
4207
- rawDataSize += chunksetData.length;
4208
- const { h, entry } = await generateChunksetCommitments(
4209
- shouldPad,
4210
- chunksetIdx,
4211
- chunksetData,
4212
- expectedChunksetSize,
4213
- provider,
4214
- onChunk
4215
- );
4216
- chunksetCommitments.push(entry);
4217
- chunksetCommitmentHashes.push(h);
4218
- }
4219
- if (rawDataSize === 0) {
4220
- const zeroChunkset = new Uint8Array(expectedChunksetSize);
4221
- const { h, entry } = await generateChunksetCommitments(
4222
- shouldPad,
4223
- 0,
4224
- zeroChunkset,
4225
- expectedChunksetSize,
4226
- provider,
4227
- onChunk
4228
- );
4229
- chunksetCommitments.push(entry);
4230
- chunksetCommitmentHashes.push(h);
4231
- }
4232
- return {
4233
- schema_version: "1.3",
4234
- raw_data_size: rawDataSize,
4235
- blob_merkle_root: (await concatHashes(
4236
- chunksetCommitmentHashes.map((chunk) => chunk.toUint8Array())
4237
- )).toString(),
4238
- chunkset_commitments: chunksetCommitments
4239
- };
4240
- }
4241
- function validatePrePaddedChunkset(chunkset, expectedSize, chunksetIdx) {
4242
- if (chunkset.byteLength !== expectedSize) {
4243
- throw new Error(
4244
- `Chunkset ${chunksetIdx} has size ${chunkset.byteLength} bytes but expected ${expectedSize} bytes. Enable padding or supply pre-padded data before calling generateCommitments.`
4245
- );
4246
- }
4247
- return chunkset;
4248
- }
4165
+ var BlobNameSchema = z.string().min(1, "Blob name path parameter cannot be empty.").max(1024, "Blob name cannot exceed 1024 characters.").refine((name) => !name.endsWith("/"), {
4166
+ message: "Blob name cannot end with a slash"
4167
+ });
4249
4168
 
4250
- // ../../packages/sdk/dist/chunk-3QINXXV6.mjs
4251
- import {
4252
- AccountAddress as AccountAddress2,
4253
- Aptos,
4254
- AptosConfig as AptosConfig2,
4255
- Hex as Hex2,
4256
- MoveVector
4257
- } from "@aptos-labs/ts-sdk";
4258
- var ShelbyBlobClient = class _ShelbyBlobClient {
4259
- aptos;
4260
- deployer;
4169
+ // ../../packages/sdk/dist/chunk-L7H6EKII.mjs
4170
+ import { AccountAddress as AccountAddress3 } from "@aptos-labs/ts-sdk";
4171
+ function encodeURIComponentKeepSlashes(str) {
4172
+ return encodeURIComponent(str).replace(/%2F/g, "/");
4173
+ }
4174
+ var ShelbyRPCClient = class {
4175
+ baseUrl;
4176
+ apiKey;
4177
+ rpcConfig;
4261
4178
  indexer;
4262
4179
  /**
4263
- * The ShelbyBlobClient is used to interact with the Shelby contract on the Aptos blockchain. This
4264
- * includes functions for registering blob commitments and retrieving blob metadata.
4180
+ * Creates a new ShelbyRPCClient for interacting with Shelby RPC nodes.
4181
+ * This client handles blob storage operations including upload and download.
4182
+ *
4183
+ * @param config - The client configuration object.
4184
+ * @param config.network - The Shelby network to use.
4185
+ *
4186
+ * @example
4187
+ * ```typescript
4188
+ * const client = new ShelbyRPCClient({
4189
+ * network: Network.SHELBYNET,
4190
+ * apiKey: "AG-***",
4191
+ * });
4192
+ * ```
4193
+ */
4194
+ constructor(config) {
4195
+ this.baseUrl = config.rpc?.baseUrl ?? NetworkToShelbyRPCBaseUrl.shelbynet;
4196
+ this.apiKey = config.apiKey ?? config.rpc?.apiKey;
4197
+ this.rpcConfig = config.rpc ?? {};
4198
+ this.indexer = getShelbyIndexerClient(config);
4199
+ }
4200
+ async #uploadPart(uploadId, partIdx, partData) {
4201
+ const nRetries = 5;
4202
+ for (let i = 0; i < nRetries; ++i) {
4203
+ const partResponse = await fetch(
4204
+ buildRequestUrl(
4205
+ `/v1/multipart-uploads/${uploadId}/parts/${partIdx}`,
4206
+ this.baseUrl
4207
+ ),
4208
+ {
4209
+ method: "PUT",
4210
+ headers: {
4211
+ "Content-Type": "application/octet-stream",
4212
+ ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
4213
+ },
4214
+ body: partData
4215
+ }
4216
+ );
4217
+ if (partResponse.ok) return;
4218
+ if (i < nRetries - 1) {
4219
+ const delay = 2 ** i * 100;
4220
+ await sleep(delay);
4221
+ }
4222
+ }
4223
+ throw new Error(`Failed to upload part ${partIdx}.`);
4224
+ }
4225
+ async #putBlobMultipart(account, blobName, blobData, partSize = 5 * 1024 * 1024) {
4226
+ const startResponse = await fetch(
4227
+ buildRequestUrl("/v1/multipart-uploads", this.baseUrl),
4228
+ {
4229
+ method: "POST",
4230
+ headers: {
4231
+ "Content-Type": "application/json",
4232
+ ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
4233
+ },
4234
+ body: JSON.stringify({
4235
+ rawAccount: account.toString(),
4236
+ rawBlobName: blobName,
4237
+ rawPartSize: partSize
4238
+ })
4239
+ }
4240
+ );
4241
+ if (!startResponse.ok) {
4242
+ let errorBodyText = "Could not read error body";
4243
+ try {
4244
+ errorBodyText = await startResponse.text();
4245
+ } catch (_e) {
4246
+ }
4247
+ throw new Error(
4248
+ `Failed to start multipart upload! status: ${startResponse.status}, body: ${errorBodyText}`
4249
+ );
4250
+ }
4251
+ const { uploadId } = await startResponse.json();
4252
+ const totalParts = Math.ceil(blobData.length / partSize);
4253
+ for (let partIdx = 0; partIdx < totalParts; partIdx++) {
4254
+ const start = partIdx * partSize;
4255
+ const end = Math.min(start + partSize, blobData.length);
4256
+ const partData = blobData.slice(start, end);
4257
+ await this.#uploadPart(uploadId, partIdx, partData);
4258
+ }
4259
+ const completeResponse = await fetch(
4260
+ buildRequestUrl(
4261
+ `/v1/multipart-uploads/${uploadId}/complete`,
4262
+ this.baseUrl
4263
+ ),
4264
+ {
4265
+ method: "POST",
4266
+ headers: {
4267
+ "Content-Type": "application/json",
4268
+ ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
4269
+ }
4270
+ }
4271
+ );
4272
+ if (!completeResponse.ok) {
4273
+ let errorBodyText = "Could not read error body";
4274
+ try {
4275
+ errorBodyText = await completeResponse.text();
4276
+ } catch (_e) {
4277
+ }
4278
+ throw new Error(
4279
+ `Failed to complete multipart upload! status: ${completeResponse.status}, body: ${errorBodyText}`
4280
+ );
4281
+ }
4282
+ }
4283
+ /**
4284
+ * Uploads blob data to the Shelby RPC node for storage by storage providers.
4285
+ * This method should be called after blob commitments have been registered on the blockchain.
4286
+ * Uses multipart upload for efficient handling of large files.
4287
+ *
4288
+ * @param params.account - The account that owns the blob.
4289
+ * @param params.blobName - The name/path of the blob (e.g. "folder/file.txt").
4290
+ * @param params.blobData - The raw blob data as a Uint8Array.
4291
+ *
4292
+ * @example
4293
+ * ```typescript
4294
+ * const blobData = new TextEncoder().encode("Hello, world!");
4295
+ *
4296
+ * await client.putBlob({
4297
+ * account: AccountAddress.from("0x1"),
4298
+ * blobName: "greetings/hello.txt",
4299
+ * blobData
4300
+ * });
4301
+ * ```
4302
+ */
4303
+ async putBlob(params) {
4304
+ BlobNameSchema.parse(params.blobName);
4305
+ await this.#putBlobMultipart(
4306
+ params.account,
4307
+ params.blobName,
4308
+ params.blobData
4309
+ );
4310
+ }
4311
+ // FIXME make this possible to stream in put ^^^
4312
+ /**
4313
+ * Downloads a blob from the Shelby RPC node.
4314
+ * Returns a streaming response with validation to ensure data integrity.
4315
+ *
4316
+ * @param params.account - The account that owns the blob.
4317
+ * @param params.blobName - The name/path of the blob (e.g. "folder/file.txt").
4318
+ * @param params.range - Optional byte range for partial downloads.
4319
+ * @param params.range.start - Starting byte position (inclusive).
4320
+ * @param params.range.end - Ending byte position (inclusive, optional).
4321
+ *
4322
+ * @returns A ShelbyBlob object containing the account, name, readable stream, and content length.
4323
+ *
4324
+ * @throws Error if the download fails or content length doesn't match.
4325
+ *
4326
+ * @example
4327
+ * ```typescript
4328
+ * // Download entire blob
4329
+ * const blob = await client.getBlob({
4330
+ * account: AccountAddress.from("0x1"),
4331
+ * blobName: "documents/report.pdf"
4332
+ * });
4333
+ *
4334
+ * // Download partial content (bytes 100-199)
4335
+ * const partial = await client.getBlob({
4336
+ * account: AccountAddress.from("0x1"),
4337
+ * blobName: "large-file.bin",
4338
+ * range: { start: 100, end: 199 }
4339
+ * });
4340
+ * ```
4341
+ */
4342
+ async getBlob(params) {
4343
+ BlobNameSchema.parse(params.blobName);
4344
+ const url = buildRequestUrl(
4345
+ `/v1/blobs/${params.account.toString()}/${encodeURIComponentKeepSlashes(
4346
+ params.blobName
4347
+ )}`,
4348
+ this.baseUrl
4349
+ );
4350
+ const requestInit = {};
4351
+ if (params.range !== void 0) {
4352
+ const headers = new Headers();
4353
+ const { start, end } = params.range;
4354
+ if (end === void 0) {
4355
+ headers.set("Range", `bytes=${start}-`);
4356
+ } else {
4357
+ if (end < start) {
4358
+ throw new Error("Range end cannot be less than start.");
4359
+ }
4360
+ headers.set("Range", `bytes=${start}-${end}`);
4361
+ }
4362
+ requestInit.headers = headers;
4363
+ }
4364
+ const response = await fetch(url, {
4365
+ ...requestInit,
4366
+ headers: {
4367
+ ...requestInit.headers,
4368
+ ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
4369
+ }
4370
+ });
4371
+ if (!response.ok) {
4372
+ throw new Error(
4373
+ `Failed to download blob: ${response.status} ${response.statusText}`
4374
+ );
4375
+ }
4376
+ if (!response.body) {
4377
+ throw new Error("Response body is null");
4378
+ }
4379
+ const contentLengthHeader = response.headers.get("content-length");
4380
+ if (contentLengthHeader === null) {
4381
+ throw new Error(
4382
+ "Response did not have content-length header, which is required"
4383
+ );
4384
+ }
4385
+ const expectedContentLength = Number.parseInt(contentLengthHeader, 10);
4386
+ if (Number.isNaN(expectedContentLength)) {
4387
+ throw new Error(
4388
+ `Invalid content-length header received: ${contentLengthHeader}`
4389
+ );
4390
+ }
4391
+ const validatingStream = new ReadableStream({
4392
+ start(controller) {
4393
+ const maybeReader = response.body?.getReader();
4394
+ if (!maybeReader) {
4395
+ controller.error(new Error("Response body reader is unavailable"));
4396
+ return;
4397
+ }
4398
+ const reader = maybeReader;
4399
+ let bytesReceived = 0;
4400
+ function pump() {
4401
+ return reader.read().then(({ done, value }) => {
4402
+ if (done) {
4403
+ if (bytesReceived !== expectedContentLength) {
4404
+ controller.error(
4405
+ new Error(
4406
+ `Downloaded data size (${bytesReceived} bytes) does not match content-length header (${expectedContentLength} bytes). This might indicate a partial or corrupted download.`
4407
+ )
4408
+ );
4409
+ return;
4410
+ }
4411
+ controller.close();
4412
+ return;
4413
+ }
4414
+ bytesReceived += value.byteLength;
4415
+ controller.enqueue(value);
4416
+ return pump();
4417
+ }).catch((error) => {
4418
+ controller.error(error);
4419
+ });
4420
+ }
4421
+ return pump();
4422
+ }
4423
+ });
4424
+ return {
4425
+ account: AccountAddress3.from(params.account),
4426
+ name: params.blobName,
4427
+ readable: validatingStream,
4428
+ contentLength: expectedContentLength
4429
+ };
4430
+ }
4431
+ };
4432
+
4433
+ // ../../packages/sdk/dist/chunk-OTBLZL2S.mjs
4434
+ import { AccountAddress as AccountAddress4 } from "@aptos-labs/ts-sdk";
4435
+ var createBlobKey = (params) => {
4436
+ return `@${AccountAddress4.from(params.account).toStringLongWithoutPrefix()}/${params.blobName}`;
4437
+ };
4438
+
4439
+ // ../../packages/sdk/dist/chunk-PCNLFNAT.mjs
4440
+ import { z as z2 } from "zod";
4441
+ var ChunksetCommitmentSchema = z2.object({
4442
+ // Chunkset root (vector commitment of child chunks)
4443
+ chunkset_root: z2.string().nullable(),
4444
+ // the size is known statically from the current configuration
4445
+ chunk_commitments: z2.array(z2.string())
4446
+ }).refine(
4447
+ (data) => {
4448
+ return data.chunk_commitments.length === DEFAULT_ERASURE_K + DEFAULT_ERASURE_M;
4449
+ },
4450
+ {
4451
+ message: `Chunkset must have exactly ${DEFAULT_ERASURE_K + DEFAULT_ERASURE_M} chunks (ERASURE_K + ERASURE_M = ${DEFAULT_ERASURE_K} + ${DEFAULT_ERASURE_M})`,
4452
+ path: ["chunk_commitments"]
4453
+ }
4454
+ );
4455
+ function expectedTotalChunksets(rawSize, chunksetSize = DEFAULT_CHUNKSET_SIZE_BYTES) {
4456
+ if (chunksetSize <= 0) {
4457
+ throw new Error("chunksetSize must be positive");
4458
+ }
4459
+ if (rawSize === 0) return 1;
4460
+ return Math.ceil(rawSize / chunksetSize);
4461
+ }
4462
+ var BlobCommitmentsSchema = z2.object({
4463
+ schema_version: z2.string(),
4464
+ raw_data_size: z2.number(),
4465
+ // FIXME I am not sure about this being here, or if it should be somewhere else
4466
+ blob_merkle_root: z2.string(),
4467
+ chunkset_commitments: z2.array(ChunksetCommitmentSchema)
4468
+ }).refine(
4469
+ (data) => {
4470
+ return expectedTotalChunksets(data.raw_data_size) === data.chunkset_commitments.length;
4471
+ },
4472
+ {
4473
+ message: "Total chunkset count mismatches with raw data size",
4474
+ // FIXME put more details in here
4475
+ path: ["chunkset_commitments"]
4476
+ }
4477
+ );
4478
+ async function generateChunksetCommitments(shouldPad, chunksetIdx, chunksetData, expectedChunksetSize, provider, onChunk) {
4479
+ const { erasure_n } = provider.config;
4480
+ const chunkCommitments = [];
4481
+ const chunksetPayload = shouldPad ? zeroPadBytes(chunksetData, expectedChunksetSize) : validatePrePaddedChunkset(
4482
+ chunksetData,
4483
+ expectedChunksetSize,
4484
+ chunksetIdx
4485
+ );
4486
+ const { chunks } = provider.encode(chunksetPayload);
4487
+ if (chunks.length !== erasure_n) {
4488
+ throw new Error(
4489
+ `Erasure provider produced ${chunks.length} chunks, expected ${erasure_n}.`
4490
+ );
4491
+ }
4492
+ let chunkIdx = 0;
4493
+ for (const chunkData of chunks) {
4494
+ if (onChunk !== void 0) {
4495
+ await onChunk(chunksetIdx, chunkIdx, chunkData);
4496
+ }
4497
+ const chunkHash = await concatHashes([chunkData]);
4498
+ chunkCommitments.push(chunkHash);
4499
+ chunkIdx += 1;
4500
+ }
4501
+ const h = await concatHashes(
4502
+ chunkCommitments.map((chunk) => chunk.toUint8Array())
4503
+ );
4504
+ const entry = {
4505
+ chunkset_root: h.toString(),
4506
+ chunk_commitments: chunkCommitments.map((chunk) => chunk.toString())
4507
+ };
4508
+ return { h, entry };
4509
+ }
4510
+ async function generateCommitments(provider, fullData, onChunk, options) {
4511
+ const expectedChunksetSize = DEFAULT_CHUNKSET_SIZE_BYTES;
4512
+ const shouldPad = options?.pad ?? true;
4513
+ const chunksetCommitments = [];
4514
+ const chunksetCommitmentHashes = [];
4515
+ let rawDataSize = 0;
4516
+ const chunksetGen = readInChunks(fullData, expectedChunksetSize);
4517
+ for await (const [chunksetIdx, chunksetData] of chunksetGen) {
4518
+ rawDataSize += chunksetData.length;
4519
+ const { h, entry } = await generateChunksetCommitments(
4520
+ shouldPad,
4521
+ chunksetIdx,
4522
+ chunksetData,
4523
+ expectedChunksetSize,
4524
+ provider,
4525
+ onChunk
4526
+ );
4527
+ chunksetCommitments.push(entry);
4528
+ chunksetCommitmentHashes.push(h);
4529
+ }
4530
+ if (rawDataSize === 0) {
4531
+ const zeroChunkset = new Uint8Array(expectedChunksetSize);
4532
+ const { h, entry } = await generateChunksetCommitments(
4533
+ shouldPad,
4534
+ 0,
4535
+ zeroChunkset,
4536
+ expectedChunksetSize,
4537
+ provider,
4538
+ onChunk
4539
+ );
4540
+ chunksetCommitments.push(entry);
4541
+ chunksetCommitmentHashes.push(h);
4542
+ }
4543
+ return {
4544
+ schema_version: "1.3",
4545
+ raw_data_size: rawDataSize,
4546
+ blob_merkle_root: (await concatHashes(
4547
+ chunksetCommitmentHashes.map((chunk) => chunk.toUint8Array())
4548
+ )).toString(),
4549
+ chunkset_commitments: chunksetCommitments
4550
+ };
4551
+ }
4552
+ function validatePrePaddedChunkset(chunkset, expectedSize, chunksetIdx) {
4553
+ if (chunkset.byteLength !== expectedSize) {
4554
+ throw new Error(
4555
+ `Chunkset ${chunksetIdx} has size ${chunkset.byteLength} bytes but expected ${expectedSize} bytes. Enable padding or supply pre-padded data before calling generateCommitments.`
4556
+ );
4557
+ }
4558
+ return chunkset;
4559
+ }
4560
+
4561
+ // ../../packages/sdk/dist/chunk-Y7KTNPPR.mjs
4562
+ import {
4563
+ AccountAddress as AccountAddress5,
4564
+ Aptos as Aptos2,
4565
+ AptosConfig as AptosConfig2,
4566
+ Hex as Hex3,
4567
+ MoveVector
4568
+ } from "@aptos-labs/ts-sdk";
4569
+ var ShelbyBlobClient = class _ShelbyBlobClient {
4570
+ aptos;
4571
+ deployer;
4572
+ indexer;
4573
+ /**
4574
+ * The ShelbyBlobClient is used to interact with the Shelby contract on the Aptos blockchain. This
4575
+ * includes functions for registering blob commitments and retrieving blob metadata.
4265
4576
  *
4266
4577
  * @param config - The client configuration object.
4267
4578
  * @param config.network - The Shelby network to use.
@@ -4288,8 +4599,8 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
4288
4599
  API_KEY: baseAptosConfig.clientConfig?.API_KEY ?? config.apiKey
4289
4600
  }
4290
4601
  });
4291
- this.aptos = new Aptos(aptosConfig);
4292
- this.deployer = config.deployer ?? AccountAddress2.fromString(SHELBY_DEPLOYER);
4602
+ this.aptos = new Aptos2(aptosConfig);
4603
+ this.deployer = config.deployer ?? AccountAddress5.fromString(SHELBY_DEPLOYER);
4293
4604
  this.indexer = getShelbyIndexerClient(config);
4294
4605
  }
4295
4606
  /**
@@ -4338,15 +4649,16 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
4338
4649
  );
4339
4650
  }
4340
4651
  return {
4341
- blobMerkleRoot: Hex2.fromHexInput(
4652
+ blobMerkleRoot: Hex3.fromHexInput(
4342
4653
  metadata.blob_commitment
4343
4654
  ).toUint8Array(),
4344
- owner: AccountAddress2.fromString(metadata.owner),
4655
+ owner: AccountAddress5.fromString(metadata.owner),
4345
4656
  name: params.name,
4657
+ blobNameSuffix: getBlobNameSuffix(params.name),
4346
4658
  size: Number(metadata.blob_size),
4347
4659
  encoding,
4348
4660
  expirationMicros: metadata.expiration_micros,
4349
- sliceAddress: AccountAddress2.fromString(metadata.slice.inner),
4661
+ sliceAddress: AccountAddress5.fromString(metadata.slice.inner),
4350
4662
  isWritten: metadata.is_written
4351
4663
  };
4352
4664
  } catch (error) {
@@ -4375,13 +4687,14 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
4375
4687
  * ```
4376
4688
  */
4377
4689
  getAccountBlobs(params) {
4690
+ const { where, ...rest } = params;
4378
4691
  return this.getBlobs({
4379
4692
  where: {
4380
- ...params.where,
4381
- owner: { _eq: AccountAddress2.from(params.account).toString() }
4693
+ ...where ?? {},
4694
+ owner: { _eq: AccountAddress5.from(params.account).toString() }
4382
4695
  },
4383
- pagination: params.pagination,
4384
- orderBy: params.orderBy
4696
+ pagination: rest.pagination,
4697
+ orderBy: rest.orderBy
4385
4698
  });
4386
4699
  }
4387
4700
  /**
@@ -4403,17 +4716,24 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
4403
4716
  async getBlobs(params = {}) {
4404
4717
  const { limit, offset } = params.pagination ?? {};
4405
4718
  const { orderBy, where } = params;
4719
+ const currentMicros = Date.now() * 1e3;
4720
+ const defaultActiveFilter = {
4721
+ expires_at: { _gte: currentMicros },
4722
+ is_deleted: { _eq: "0" }
4723
+ };
4724
+ const finalWhere = where !== void 0 ? { ...defaultActiveFilter, ...where } : defaultActiveFilter;
4406
4725
  const { blobs } = await this.indexer.getBlobs({
4407
- where,
4726
+ where: finalWhere,
4408
4727
  limit,
4409
4728
  offset,
4410
4729
  orderBy
4411
4730
  });
4412
4731
  return blobs.map(
4413
4732
  (blob) => ({
4414
- owner: AccountAddress2.from(blob.owner),
4733
+ owner: AccountAddress5.from(blob.owner),
4415
4734
  name: blob.blob_name,
4416
- blobMerkleRoot: Hex2.fromHexInput(blob.blob_commitment).toUint8Array(),
4735
+ blobNameSuffix: getBlobNameSuffix(blob.blob_name),
4736
+ blobMerkleRoot: Hex3.fromHexInput(blob.blob_commitment).toUint8Array(),
4417
4737
  size: Number(blob.size),
4418
4738
  // TODO: Add encoding when supported in NCI
4419
4739
  encoding: {
@@ -4422,7 +4742,7 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
4422
4742
  ...ERASURE_CODE_AND_CHUNK_MAPPING.ClayCode_16Total_10Data_13Helper
4423
4743
  },
4424
4744
  expirationMicros: Number(blob.expires_at),
4425
- sliceAddress: AccountAddress2.from(blob.slice_address),
4745
+ sliceAddress: AccountAddress5.from(blob.slice_address),
4426
4746
  isWritten: Boolean(Number(blob.is_written))
4427
4747
  })
4428
4748
  );
@@ -4445,7 +4765,7 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
4445
4765
  return blob_activities.map(
4446
4766
  (activity) => ({
4447
4767
  blobName: activity.blob_name,
4448
- accountAddress: AccountAddress2.from(
4768
+ accountAddress: AccountAddress5.from(
4449
4769
  activity.blob_name.substring(1, 65)
4450
4770
  ),
4451
4771
  type: activityTypeMapping[activity.event_type] ?? "unknown",
@@ -4458,28 +4778,36 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
4458
4778
  );
4459
4779
  }
4460
4780
  /**
4461
- * Retrieves the blob chunks for a given blob from the blockchain. The blob chunk will contain
4462
- * the commitment, the storage provider location, and the status of the chunk (stored or pending).
4781
+ * Retrieves the total number of blobs from the blockchain.
4463
4782
  *
4464
- * @deprecated Will need to get reworked when we are acking blobs from storage providers.
4465
- *
4466
- * @param params.account - The account namespace the blob is stored in (e.g. "0x1")
4467
- * @param params.name - The name of the blob (e.g. "foo/bar")
4468
- * @returns The chunks that make up the blob.
4783
+ * @param params.where (optional) - The where clause to filter the blobs by.
4784
+ * @returns The total number of blobs.
4469
4785
  *
4470
4786
  * @example
4471
4787
  * ```typescript
4472
- * // BlobChunk[]
4473
- * const chunks = await client.getBlobChunks({
4474
- * account: AccountAddress.fromString("0x1"),
4475
- * name: "foo/bar.txt",
4476
- * });
4788
+ * const count = await client.getBlobsCount();
4789
+ * ```
4790
+ */
4791
+ async getBlobsCount(params) {
4792
+ const { where } = params;
4793
+ const { blobs_aggregate } = await this.indexer.getBlobsCount({ where });
4794
+ return blobs_aggregate?.aggregate?.count ?? 0;
4795
+ }
4796
+ /**
4797
+ * Retrieves the total number of blob activities from the blockchain.
4798
+ *
4799
+ * @param params.where (optional) - The where clause to filter the blob activities by.
4800
+ * @returns The total number of blob activities.
4477
4801
  *
4478
- * const isStored = chunks.every((c) => c.location.variant === "stored");
4802
+ * @example
4803
+ * ```typescript
4804
+ * const count = await client.getBlobActivitiesCount();
4479
4805
  * ```
4480
4806
  */
4481
- getBlobChunks(_params) {
4482
- throw new Error("Not implemented");
4807
+ async getBlobActivitiesCount(params) {
4808
+ const { where } = params;
4809
+ const { blob_activities_aggregate } = await this.indexer.getBlobActivitiesCount({ where });
4810
+ return blob_activities_aggregate?.aggregate?.count ?? 0;
4483
4811
  }
4484
4812
  /**
4485
4813
  * Registers a blob on the blockchain by writing its merkle root and metadata.
@@ -4533,360 +4861,101 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
4533
4861
  }
4534
4862
  /**
4535
4863
  * Creates a transaction payload to register a blob on the blockchain.
4536
- * This is a static helper method for constructing the Move function call payload.
4537
- *
4538
- * @param params.deployer - Optional deployer account address. Defaults to SHELBY_DEPLOYER.
4539
- * @param params.account - The account that will own the blob.
4540
- * @param params.blobName - The name/path of the blob (e.g. "foo/bar.txt").
4541
- * @param params.blobSize - The size of the blob in bytes.
4542
- * @param params.blobMerkleRoot - The merkle root of the blob commitments as a hex string.
4543
- * @param params.expirationMicros - The expiration time of the blob in microseconds.
4544
- * @param params.numChunksets - The total number of chunksets in the blob.
4545
- *
4546
- * @returns An Aptos transaction payload data object for the register_blob Move function.
4547
- *
4548
- * @see https://github.com/shelby/shelby/blob/e08e84742cf2b80ad8bb7227deb3013398076d53/move/shelby_contract/sources/global_metadata.move#L357
4549
- */
4550
- static createRegisterBlobPayload(params) {
4551
- return {
4552
- function: `${(params.deployer ?? SHELBY_DEPLOYER).toString()}::global_metadata::register_blob`,
4553
- functionArguments: [
4554
- params.blobName,
4555
- params.expirationMicros,
4556
- MoveVector.U8(params.blobMerkleRoot),
4557
- params.numChunksets,
4558
- params.blobSize,
4559
- // TODO
4560
- 0,
4561
- // payment tier
4562
- 0
4563
- // encoding
4564
- ]
4565
- };
4566
- }
4567
- /**
4568
- * Creates a transaction payload to register multiple blobs on the blockchain.
4569
- * This is a static helper method for constructing the Move function call payload.
4570
- *
4571
- * @param params.deployer - Optional deployer account address. Defaults to SHELBY_DEPLOYER.
4572
- * @param params.account - The account that will own the blobs.
4573
- * @param params.expirationMicros - The expiration time of the blobs in microseconds.
4574
- * @param params.blobs - The blobs to register.
4575
- * @param params.blobs.blobName - The name/path of the blob (e.g. "foo/bar.txt").
4576
- * @param params.blobs.blobSize - The size of the blob in bytes.
4577
- * @param params.blobs.blobMerkleRoot - The merkle root of the blob commitments as a hex string.
4578
- * @param params.blobs.numChunksets - The total number of chunksets in the blob.
4579
- *
4580
- * @returns An Aptos transaction payload data object for the register_multiple_blobs Move function.
4581
- *
4582
- * @see https://github.com/shelby/shelby/blob/e08e84742cf2b80ad8bb7227deb3013398076d53/move/shelby_contract/sources/global_metadata.move#L357
4583
- */
4584
- static createBatchRegisterBlobsPayload(params) {
4585
- const blobNames = [];
4586
- const blobMerkleRoots = [];
4587
- const blobNumChunksets = [];
4588
- const blobSizes = [];
4589
- params.blobs.forEach((blob) => {
4590
- blobNames.push(blob.blobName);
4591
- blobMerkleRoots.push(MoveVector.U8(blob.blobMerkleRoot));
4592
- blobNumChunksets.push(blob.numChunksets);
4593
- blobSizes.push(blob.blobSize);
4594
- });
4595
- return {
4596
- function: `${(params.deployer ?? SHELBY_DEPLOYER).toString()}::global_metadata::register_multiple_blobs`,
4597
- functionArguments: [
4598
- blobNames,
4599
- params.expirationMicros,
4600
- blobMerkleRoots,
4601
- blobNumChunksets,
4602
- blobSizes,
4603
- // TODO
4604
- 0,
4605
- 0
4606
- ]
4607
- };
4608
- }
4609
- };
4610
-
4611
- // ../../packages/sdk/dist/chunk-FIFKKWXV.mjs
4612
- import { AccountAddress as AccountAddress3 } from "@aptos-labs/ts-sdk";
4613
- import { z as z2 } from "zod";
4614
- var BlobNameSchema = z2.string().min(1, "Blob name path parameter cannot be empty.").max(1024, "Blob name cannot exceed 1024 characters.").refine((name) => !name.endsWith("/"), {
4615
- message: "Blob name cannot end with a slash"
4616
- });
4617
-
4618
- // ../../packages/sdk/dist/chunk-I6NG5GNL.mjs
4619
- function sleep(ms) {
4620
- return new Promise((resolve2) => setTimeout(resolve2, ms));
4621
- }
4622
-
4623
- // ../../packages/sdk/dist/chunk-GKRHKR3J.mjs
4624
- import { AccountAddress as AccountAddress4 } from "@aptos-labs/ts-sdk";
4625
- function encodeURIComponentKeepSlashes(str) {
4626
- return encodeURIComponent(str).replace(/%2F/g, "/");
4627
- }
4628
- var ShelbyRPCClient = class {
4629
- baseUrl;
4630
- apiKey;
4631
- rpcConfig;
4632
- indexer;
4633
- /**
4634
- * Creates a new ShelbyRPCClient for interacting with Shelby RPC nodes.
4635
- * This client handles blob storage operations including upload and download.
4636
- *
4637
- * @param config - The client configuration object.
4638
- * @param config.network - The Shelby network to use.
4639
- *
4640
- * @example
4641
- * ```typescript
4642
- * const client = new ShelbyRPCClient({
4643
- * network: Network.SHELBYNET,
4644
- * apiKey: "AG-***",
4645
- * });
4646
- * ```
4647
- */
4648
- constructor(config) {
4649
- this.baseUrl = config.rpc?.baseUrl ?? NetworkToShelbyRPCBaseUrl.shelbynet;
4650
- this.apiKey = config.apiKey ?? config.rpc?.apiKey;
4651
- this.rpcConfig = config.rpc ?? {};
4652
- this.indexer = getShelbyIndexerClient(config);
4653
- }
4654
- async #uploadPart(uploadId, partIdx, partData) {
4655
- const nRetries = 5;
4656
- for (let i = 0; i < nRetries; ++i) {
4657
- const partResponse = await fetch(
4658
- buildRequestUrl(
4659
- `/v1/multipart-uploads/${uploadId}/parts/${partIdx}`,
4660
- this.baseUrl
4661
- ),
4662
- {
4663
- method: "PUT",
4664
- headers: {
4665
- "Content-Type": "application/octet-stream",
4666
- ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
4667
- },
4668
- body: partData
4669
- }
4670
- );
4671
- if (partResponse.ok) return;
4672
- if (i < nRetries - 1) {
4673
- const delay = 2 ** i * 100;
4674
- await sleep(delay);
4675
- }
4676
- }
4677
- throw new Error(`Failed to upload part ${partIdx}.`);
4678
- }
4679
- async #putBlobMultipart(account, blobName, blobData, partSize = 5 * 1024 * 1024) {
4680
- const startResponse = await fetch(
4681
- buildRequestUrl("/v1/multipart-uploads", this.baseUrl),
4682
- {
4683
- method: "POST",
4684
- headers: {
4685
- "Content-Type": "application/json",
4686
- ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
4687
- },
4688
- body: JSON.stringify({
4689
- rawAccount: account.toString(),
4690
- rawBlobName: blobName,
4691
- rawPartSize: partSize
4692
- })
4693
- }
4694
- );
4695
- if (!startResponse.ok) {
4696
- let errorBodyText = "Could not read error body";
4697
- try {
4698
- errorBodyText = await startResponse.text();
4699
- } catch (_e) {
4700
- }
4701
- throw new Error(
4702
- `Failed to start multipart upload! status: ${startResponse.status}, body: ${errorBodyText}`
4703
- );
4704
- }
4705
- const { uploadId } = await startResponse.json();
4706
- const totalParts = Math.ceil(blobData.length / partSize);
4707
- for (let partIdx = 0; partIdx < totalParts; partIdx++) {
4708
- const start = partIdx * partSize;
4709
- const end = Math.min(start + partSize, blobData.length);
4710
- const partData = blobData.slice(start, end);
4711
- await this.#uploadPart(uploadId, partIdx, partData);
4712
- }
4713
- const completeResponse = await fetch(
4714
- buildRequestUrl(
4715
- `/v1/multipart-uploads/${uploadId}/complete`,
4716
- this.baseUrl
4717
- ),
4718
- {
4719
- method: "POST",
4720
- headers: {
4721
- "Content-Type": "application/json",
4722
- ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
4723
- }
4724
- }
4725
- );
4726
- if (!completeResponse.ok) {
4727
- let errorBodyText = "Could not read error body";
4728
- try {
4729
- errorBodyText = await completeResponse.text();
4730
- } catch (_e) {
4731
- }
4732
- throw new Error(
4733
- `Failed to complete multipart upload! status: ${completeResponse.status}, body: ${errorBodyText}`
4734
- );
4735
- }
4736
- }
4737
- /**
4738
- * Uploads blob data to the Shelby RPC node for storage by storage providers.
4739
- * This method should be called after blob commitments have been registered on the blockchain.
4740
- * Uses multipart upload for efficient handling of large files.
4864
+ * This is a static helper method for constructing the Move function call payload.
4741
4865
  *
4742
- * @param params.account - The account that owns the blob.
4743
- * @param params.blobName - The name/path of the blob (e.g. "folder/file.txt").
4744
- * @param params.blobData - The raw blob data as a Uint8Array.
4866
+ * @param params.deployer - Optional deployer account address. Defaults to SHELBY_DEPLOYER.
4867
+ * @param params.account - The account that will own the blob.
4868
+ * @param params.blobName - The name/path of the blob (e.g. "foo/bar.txt").
4869
+ * @param params.blobSize - The size of the blob in bytes.
4870
+ * @param params.blobMerkleRoot - The merkle root of the blob commitments as a hex string.
4871
+ * @param params.expirationMicros - The expiration time of the blob in microseconds.
4872
+ * @param params.numChunksets - The total number of chunksets in the blob.
4745
4873
  *
4746
- * @example
4747
- * ```typescript
4748
- * const blobData = new TextEncoder().encode("Hello, world!");
4874
+ * @returns An Aptos transaction payload data object for the register_blob Move function.
4749
4875
  *
4750
- * await client.putBlob({
4751
- * account: AccountAddress.from("0x1"),
4752
- * blobName: "greetings/hello.txt",
4753
- * blobData
4754
- * });
4755
- * ```
4876
+ * @see https://github.com/shelby/shelby/blob/e08e84742cf2b80ad8bb7227deb3013398076d53/move/shelby_contract/sources/global_metadata.move#L357
4756
4877
  */
4757
- async putBlob(params) {
4758
- BlobNameSchema.parse(params.blobName);
4759
- await this.#putBlobMultipart(
4760
- params.account,
4761
- params.blobName,
4762
- params.blobData
4763
- );
4878
+ static createRegisterBlobPayload(params) {
4879
+ return {
4880
+ function: `${(params.deployer ?? SHELBY_DEPLOYER).toString()}::global_metadata::register_blob`,
4881
+ functionArguments: [
4882
+ params.blobName,
4883
+ params.expirationMicros,
4884
+ MoveVector.U8(params.blobMerkleRoot),
4885
+ params.numChunksets,
4886
+ params.blobSize,
4887
+ // TODO
4888
+ 0,
4889
+ // payment tier
4890
+ 0
4891
+ // encoding
4892
+ ]
4893
+ };
4764
4894
  }
4765
- // FIXME make this possible to stream in put ^^^
4766
4895
  /**
4767
- * Downloads a blob from the Shelby RPC node.
4768
- * Returns a streaming response with validation to ensure data integrity.
4896
+ * Creates a transaction payload to register multiple blobs on the blockchain.
4897
+ * This is a static helper method for constructing the Move function call payload.
4769
4898
  *
4770
- * @param params.account - The account that owns the blob.
4771
- * @param params.blobName - The name/path of the blob (e.g. "folder/file.txt").
4772
- * @param params.range - Optional byte range for partial downloads.
4773
- * @param params.range.start - Starting byte position (inclusive).
4774
- * @param params.range.end - Ending byte position (inclusive, optional).
4899
+ * @param params.deployer - Optional deployer account address. Defaults to SHELBY_DEPLOYER.
4900
+ * @param params.account - The account that will own the blobs.
4901
+ * @param params.expirationMicros - The expiration time of the blobs in microseconds.
4902
+ * @param params.blobs - The blobs to register.
4903
+ * @param params.blobs.blobName - The name/path of the blob (e.g. "foo/bar.txt").
4904
+ * @param params.blobs.blobSize - The size of the blob in bytes.
4905
+ * @param params.blobs.blobMerkleRoot - The merkle root of the blob commitments as a hex string.
4906
+ * @param params.blobs.numChunksets - The total number of chunksets in the blob.
4775
4907
  *
4776
- * @returns A ShelbyBlob object containing the account, name, readable stream, and content length.
4908
+ * @returns An Aptos transaction payload data object for the register_multiple_blobs Move function.
4777
4909
  *
4778
- * @throws Error if the download fails or content length doesn't match.
4910
+ * @see https://github.com/shelby/shelby/blob/e08e84742cf2b80ad8bb7227deb3013398076d53/move/shelby_contract/sources/global_metadata.move#L357
4911
+ */
4912
+ static createBatchRegisterBlobsPayload(params) {
4913
+ const blobNames = [];
4914
+ const blobMerkleRoots = [];
4915
+ const blobNumChunksets = [];
4916
+ const blobSizes = [];
4917
+ params.blobs.forEach((blob) => {
4918
+ blobNames.push(blob.blobName);
4919
+ blobMerkleRoots.push(MoveVector.U8(blob.blobMerkleRoot));
4920
+ blobNumChunksets.push(blob.numChunksets);
4921
+ blobSizes.push(blob.blobSize);
4922
+ });
4923
+ return {
4924
+ function: `${(params.deployer ?? SHELBY_DEPLOYER).toString()}::global_metadata::register_multiple_blobs`,
4925
+ functionArguments: [
4926
+ blobNames,
4927
+ params.expirationMicros,
4928
+ blobMerkleRoots,
4929
+ blobNumChunksets,
4930
+ blobSizes,
4931
+ // TODO
4932
+ 0,
4933
+ 0
4934
+ ]
4935
+ };
4936
+ }
4937
+ /**
4938
+ * Creates a transaction payload to delete a blob on the blockchain.
4939
+ * This is a static helper method for constructing the Move function call payload.
4779
4940
  *
4780
- * @example
4781
- * ```typescript
4782
- * // Download entire blob
4783
- * const blob = await client.getBlob({
4784
- * account: AccountAddress.from("0x1"),
4785
- * blobName: "documents/report.pdf"
4786
- * });
4941
+ * @param params.deployer - Optional deployer account address. Defaults to SHELBY_DEPLOYER.
4942
+ * @param params.blobNameSuffix - The blob name suffix (e.g. "bar.txt").
4787
4943
  *
4788
- * // Download partial content (bytes 100-199)
4789
- * const partial = await client.getBlob({
4790
- * account: AccountAddress.from("0x1"),
4791
- * blobName: "large-file.bin",
4792
- * range: { start: 100, end: 199 }
4793
- * });
4794
- * ```
4944
+ * @returns An Aptos transaction payload data object for the delete_blob Move function.
4945
+ *
4946
+ * @see https://github.com/shelby/shelby/blob/64e9d7b4f0005e586faeb1e4085c79159234b6b6/move/shelby_contract/sources/global_metadata.move#L616
4795
4947
  */
4796
- async getBlob(params) {
4797
- BlobNameSchema.parse(params.blobName);
4798
- const url = buildRequestUrl(
4799
- `/v1/blobs/${params.account.toString()}/${encodeURIComponentKeepSlashes(
4800
- params.blobName
4801
- )}`,
4802
- this.baseUrl
4803
- );
4804
- const requestInit = {};
4805
- if (params.range !== void 0) {
4806
- const headers = new Headers();
4807
- const { start, end } = params.range;
4808
- if (end === void 0) {
4809
- headers.set("Range", `bytes=${start}-`);
4810
- } else {
4811
- if (end < start) {
4812
- throw new Error("Range end cannot be less than start.");
4813
- }
4814
- headers.set("Range", `bytes=${start}-${end}`);
4815
- }
4816
- requestInit.headers = headers;
4817
- }
4818
- const response = await fetch(url, {
4819
- ...requestInit,
4820
- headers: {
4821
- ...requestInit.headers,
4822
- ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
4823
- }
4824
- });
4825
- if (!response.ok) {
4826
- throw new Error(
4827
- `Failed to download blob: ${response.status} ${response.statusText}`
4828
- );
4829
- }
4830
- if (!response.body) {
4831
- throw new Error("Response body is null");
4832
- }
4833
- const contentLengthHeader = response.headers.get("content-length");
4834
- if (contentLengthHeader === null) {
4835
- throw new Error(
4836
- "Response did not have content-length header, which is required"
4837
- );
4838
- }
4839
- const expectedContentLength = Number.parseInt(contentLengthHeader, 10);
4840
- if (Number.isNaN(expectedContentLength)) {
4841
- throw new Error(
4842
- `Invalid content-length header received: ${contentLengthHeader}`
4843
- );
4844
- }
4845
- const validatingStream = new ReadableStream({
4846
- start(controller) {
4847
- const maybeReader = response.body?.getReader();
4848
- if (!maybeReader) {
4849
- controller.error(new Error("Response body reader is unavailable"));
4850
- return;
4851
- }
4852
- const reader = maybeReader;
4853
- let bytesReceived = 0;
4854
- function pump() {
4855
- return reader.read().then(({ done, value }) => {
4856
- if (done) {
4857
- if (bytesReceived !== expectedContentLength) {
4858
- controller.error(
4859
- new Error(
4860
- `Downloaded data size (${bytesReceived} bytes) does not match content-length header (${expectedContentLength} bytes). This might indicate a partial or corrupted download.`
4861
- )
4862
- );
4863
- return;
4864
- }
4865
- controller.close();
4866
- return;
4867
- }
4868
- bytesReceived += value.byteLength;
4869
- controller.enqueue(value);
4870
- return pump();
4871
- }).catch((error) => {
4872
- controller.error(error);
4873
- });
4874
- }
4875
- return pump();
4876
- }
4877
- });
4948
+ static createDeleteBlobPayload(params) {
4878
4949
  return {
4879
- account: AccountAddress4.from(params.account),
4880
- name: params.blobName,
4881
- readable: validatingStream,
4882
- contentLength: expectedContentLength
4950
+ function: `${(params.deployer ?? SHELBY_DEPLOYER).toString()}::global_metadata::delete_blob`,
4951
+ functionArguments: [params.blobNameSuffix]
4883
4952
  };
4884
4953
  }
4885
4954
  };
4886
4955
 
4887
- // ../../packages/sdk/dist/chunk-4W3FFVXZ.mjs
4956
+ // ../../packages/sdk/dist/chunk-TPGMXZRD.mjs
4888
4957
  import {
4889
- Aptos as Aptos2
4958
+ Aptos as Aptos3
4890
4959
  } from "@aptos-labs/ts-sdk";
4891
4960
  var ShelbyClient = class {
4892
4961
  /**
@@ -4943,7 +5012,7 @@ var ShelbyClient = class {
4943
5012
  */
4944
5013
  constructor(config, provider) {
4945
5014
  this.config = config;
4946
- this.aptos = new Aptos2(getAptosConfig(config));
5015
+ this.aptos = new Aptos3(getAptosConfig(config));
4947
5016
  this.coordination = new ShelbyBlobClient(config);
4948
5017
  this.rpc = new ShelbyRPCClient(config);
4949
5018
  this._provider = provider;
@@ -5039,16 +5108,29 @@ var ShelbyClient = class {
5039
5108
  }
5040
5109
  };
5041
5110
 
5042
- // ../../packages/sdk/dist/chunk-MYN7KW2X.mjs
5111
+ // ../../packages/sdk/dist/chunk-2DD6Q5CK.mjs
5043
5112
  var ShelbyNodeClient = class extends ShelbyClient {
5044
5113
  };
5045
5114
 
5046
- // ../../packages/sdk/dist/chunk-CTGCK3H2.mjs
5047
- import {
5048
- AccountAddress as AccountAddress5,
5049
- Aptos as Aptos3,
5050
- Hex as Hex3
5051
- } from "@aptos-labs/ts-sdk";
5115
+ // ../../packages/sdk/dist/chunk-7P6ASYW6.mjs
5116
+ var __defProp = Object.defineProperty;
5117
+ var __export = (target, all) => {
5118
+ for (var name in all)
5119
+ __defProp(target, name, { get: all[name], enumerable: true });
5120
+ };
5121
+
5122
+ // ../../packages/sdk/dist/chunk-A4IG6GSE.mjs
5123
+ var testUtil_exports = {};
5124
+ __export(testUtil_exports, {
5125
+ makeChunk: () => makeChunk
5126
+ });
5127
+ function makeChunk(n) {
5128
+ const c = Buffer.alloc(n);
5129
+ for (let i = 0; i < n; ++i) {
5130
+ c[i] = i % 256;
5131
+ }
5132
+ return c;
5133
+ }
5052
5134
 
5053
5135
  // ../../packages/sdk/dist/chunk-VRLIOKWG.mjs
5054
5136
  import { Network as Network4 } from "@aptos-labs/ts-sdk";
@@ -5078,26 +5160,6 @@ function getShelbyAccountExplorerUrl(network, accountAddress) {
5078
5160
  return `${baseUrl}/account/${accountAddress}`;
5079
5161
  }
5080
5162
 
5081
- // ../../packages/sdk/dist/chunk-7P6ASYW6.mjs
5082
- var __defProp = Object.defineProperty;
5083
- var __export = (target, all) => {
5084
- for (var name in all)
5085
- __defProp(target, name, { get: all[name], enumerable: true });
5086
- };
5087
-
5088
- // ../../packages/sdk/dist/chunk-A4IG6GSE.mjs
5089
- var testUtil_exports = {};
5090
- __export(testUtil_exports, {
5091
- makeChunk: () => makeChunk
5092
- });
5093
- function makeChunk(n) {
5094
- const c = Buffer.alloc(n);
5095
- for (let i = 0; i < n; ++i) {
5096
- c[i] = i % 256;
5097
- }
5098
- return c;
5099
- }
5100
-
5101
5163
  // ../../packages/sdk/dist/chunk-C6RQ3AEU.mjs
5102
5164
  import { Network as Network6 } from "@aptos-labs/ts-sdk";
5103
5165
  function getAptosExplorerBaseUrl(network) {
@@ -5154,6 +5216,7 @@ var DEFAULT_CHUNK_SIZE_BYTES2 = 2 * 1024 * 1024;
5154
5216
 
5155
5217
  // src/commands/account.tsx
5156
5218
  import chalk from "chalk";
5219
+ import { filesize } from "filesize";
5157
5220
  import { render } from "ink";
5158
5221
  import { z as z9 } from "zod";
5159
5222
 
@@ -6266,6 +6329,9 @@ function getErasureCodingProvider() {
6266
6329
 
6267
6330
  // src/utils/paths.ts
6268
6331
  import path2 from "path";
6332
+ function endsWithDirectorySeparator(filePath) {
6333
+ return filePath.endsWith("/") || filePath.endsWith("\\") || filePath.endsWith(path2.sep);
6334
+ }
6269
6335
  function normBlobName(pathModule, inputDirectoryName, filename, blobPrefix) {
6270
6336
  const blobNameWithoutInputDir = pathModule.relative(
6271
6337
  inputDirectoryName,
@@ -7029,10 +7095,10 @@ function askQuestion(question) {
7029
7095
  input: process.stdin,
7030
7096
  output: process.stdout
7031
7097
  });
7032
- return new Promise((resolve2) => {
7098
+ return new Promise((resolve3) => {
7033
7099
  rl.question(question, (answer) => {
7034
7100
  rl.close();
7035
- resolve2(answer.trim());
7101
+ resolve3(answer.trim());
7036
7102
  });
7037
7103
  });
7038
7104
  }
@@ -7235,25 +7301,33 @@ function accountCommand(program) {
7235
7301
  });
7236
7302
  const activeAccount = getCurrentAccount(config, accountName).account;
7237
7303
  const shelbyClient = new ShelbyNodeClient(shelbyConfig);
7238
- console.log(
7239
- `\u{1F50D} Retrieving blobs for ${accountName} (${activeAccount.accountAddress.toString()})`
7304
+ console.log(`\u{1F50D} Retrieving blobs for ${accountName}`);
7305
+ console.log(`\u{1F464} Address: ${activeAccount.accountAddress.toString()}`);
7306
+ const shelbyExplorerUrl = getShelbyAccountExplorerUrl(
7307
+ shelbyConfig.network,
7308
+ activeAccount.accountAddress.toString()
7240
7309
  );
7310
+ console.log(`\u{1F5C2}\uFE0F Shelby Explorer: ${shelbyExplorerUrl}`);
7241
7311
  const blobs = await shelbyClient.coordination.getBlobs({
7242
7312
  where: {
7243
7313
  owner: { _eq: activeAccount.accountAddress.toString() }
7244
7314
  }
7245
7315
  });
7246
7316
  console.log(
7247
- `\u2705 Retrieved ${blobs.length} blob${blobs.length === 1 ? "" : "s"} for ${accountName} (${activeAccount.accountAddress.toString()})`
7317
+ `
7318
+ \u2705 Retrieved ${blobs.length} blob${blobs.length === 1 ? "" : "s"}`
7248
7319
  );
7320
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
7249
7321
  if (blobs.length === 0) {
7250
7322
  console.log("No blobs found.");
7323
+ console.log("\u2728 Done!");
7251
7324
  return;
7252
7325
  }
7326
+ console.log("\u{1F4E6} Stored Blobs");
7253
7327
  const { table, wrapCell } = createResponsiveTable({
7254
7328
  columns: [
7255
7329
  { header: "Name", width: 30, flex: true },
7256
- { header: "Size (bytes)", width: 15 },
7330
+ { header: "Size", width: 12 },
7257
7331
  { header: "Expires", width: 25 }
7258
7332
  ],
7259
7333
  padding: 12
@@ -7262,14 +7336,20 @@ function accountCommand(program) {
7262
7336
  const expirationDate = new Date(blob.expirationMicros / 1e3);
7263
7337
  const blobNameMatch = blob.name.match(/@[^/]+\/(.+)/);
7264
7338
  const displayName = blobNameMatch ? blobNameMatch[1] : blob.name;
7265
- const row = [
7266
- displayName,
7267
- blob.size.toString(),
7268
- expirationDate.toLocaleString()
7269
- ];
7339
+ const formattedSize = filesize(blob.size, { standard: "jedec" });
7340
+ const formattedDate = expirationDate.toLocaleDateString("en-US", {
7341
+ month: "short",
7342
+ day: "numeric",
7343
+ year: "numeric",
7344
+ hour: "numeric",
7345
+ minute: "2-digit",
7346
+ hour12: true
7347
+ });
7348
+ const row = [displayName, formattedSize, formattedDate];
7270
7349
  table.push(row.map((cell, i) => wrapCell(cell, i)));
7271
7350
  }
7272
7351
  console.log(table.toString());
7352
+ console.log("\u2728 Done!");
7273
7353
  } catch (error) {
7274
7354
  let displayMessage;
7275
7355
  if (error instanceof Error && error.message.includes("GraphQL Error (Code: 401)")) {
@@ -7776,52 +7856,99 @@ import { pipeline } from "stream/promises";
7776
7856
  import ora from "ora";
7777
7857
  import { z as z13 } from "zod";
7778
7858
  var denormBlobName2 = (a, b, c) => denormBlobName(path3, a, b, c);
7779
- var endsWithDirectorySeparator = (filePath) => {
7780
- return filePath.endsWith("/") || filePath.endsWith("\\") || filePath.endsWith(path3.sep);
7781
- };
7782
7859
  var DownloadOptionsSchema = z13.object({
7783
- src: z13.string().min(1, "`src`must be a valid blob name or directory prefix").describe("Blob name or directory prefix to download"),
7784
- dst: z13.string().min(1, "`dst` must be a valid filepath").describe("Local path where to save the downloaded content"),
7860
+ source: z13.string({
7861
+ required_error: "\n\u274C Missing Required Argument\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\u26A0\uFE0F Missing source blob name or prefix (first argument)\n\n\u{1F4A1} Usage:\n shelby download <source-blob-name> <destination-path> [options]\n\n\u{1F4DD} Examples:\n shelby download my-blob.txt ./myfile.txt\n shelby download my-folder/ ./my-folder/ -r\n"
7862
+ }).min(1, "Source blob name or directory prefix is required").describe("Blob name or directory prefix to download"),
7863
+ destination: z13.string({
7864
+ required_error: "\n\u274C Missing Required Argument\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\u26A0\uFE0F Missing destination path (second argument)\n\n\u{1F4A1} Usage:\n shelby download <source-blob-name> <destination-path> [options]\n\n\u{1F4DD} Example:\n shelby download my-blob.txt ./myfile.txt\n"
7865
+ }).min(1, "`destination` must be a valid filepath").describe("Local path where to save the downloaded content"),
7785
7866
  recursive: z13.boolean().default(false).describe("Download assuming canonical directory layout and recurse"),
7786
- force: z13.boolean().default(false).describe("Overwrite the dst if it already exists")
7867
+ force: z13.boolean().default(false).describe("Overwrite the destination if it already exists")
7787
7868
  }).refine(
7788
7869
  (data) => {
7789
7870
  if (data.recursive) {
7790
- if (!data.src.endsWith("/") || !endsWithDirectorySeparator(data.dst))
7871
+ if (!data.source.endsWith("/") || !endsWithDirectorySeparator(data.destination))
7791
7872
  return false;
7792
7873
  return true;
7793
7874
  }
7794
7875
  return true;
7795
7876
  },
7796
- {
7797
- message: "Recursive downloads require src prefix and dst name to end in directory separator ('/' or '\\')"
7798
- }
7877
+ (data) => ({
7878
+ message: `
7879
+ \u274C Invalid Download Options
7880
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
7881
+
7882
+ \u26A0\uFE0F Recursive downloads require:
7883
+ - The source prefix must end with '/'
7884
+ - The destination path must end with '/' or '\\'
7885
+
7886
+ \u{1F9FE} Received:
7887
+ Source: ${data.source}
7888
+ Destination: ${data.destination}
7889
+
7890
+ \u{1F4A1} Tip: Add trailing slashes to both paths:
7891
+ \u{1F449} shelby download --recursive ${data.source.endsWith("/") ? data.source : `${data.source}/`} ${endsWithDirectorySeparator(data.destination) ? data.destination : `${data.destination}/`}
7892
+ `
7893
+ })
7799
7894
  ).refine(
7800
7895
  (data) => {
7801
7896
  if (!data.recursive) {
7802
- if (!BlobNameSchema.safeParse(data.src).success) return false;
7803
- if (endsWithDirectorySeparator(data.dst)) return false;
7897
+ if (!BlobNameSchema.safeParse(data.source).success) return false;
7898
+ if (endsWithDirectorySeparator(data.destination)) return false;
7804
7899
  return true;
7805
7900
  }
7806
7901
  return true;
7807
7902
  },
7808
- {
7809
- message: "Blob downloads, non-recursive, require that src blob name and dst path not end in directory separator ('/' or '\\')"
7903
+ (data) => {
7904
+ const sourceEndsWithSlash = data.source.endsWith("/") || data.source.endsWith("\\");
7905
+ const destEndsWithSlash = endsWithDirectorySeparator(data.destination);
7906
+ let tipMessage = "";
7907
+ if (sourceEndsWithSlash && destEndsWithSlash) {
7908
+ tipMessage = `\u{1F4A1} Tip: For downloading a directory, use the --recursive flag:
7909
+ \u{1F449} shelby download --recursive ${data.source} ${data.destination}`;
7910
+ } else if (sourceEndsWithSlash && !destEndsWithSlash) {
7911
+ tipMessage = `\u{1F4A1} Tip: For downloading a directory, both paths need trailing slashes:
7912
+ \u{1F449} shelby download --recursive ${data.source} ${data.destination}/`;
7913
+ } else if (!sourceEndsWithSlash && destEndsWithSlash) {
7914
+ tipMessage = `\u{1F4A1} Choose one of the following:
7915
+
7916
+ To download a single blob (remove trailing slash from destination):
7917
+ \u{1F449} shelby download ${data.source} ${data.destination.replace(/[\\/]+$/, "")}
7918
+
7919
+ To download a directory recursively (add trailing slash to source):
7920
+ \u{1F449} shelby download --recursive ${data.source}/ ${data.destination}`;
7921
+ }
7922
+ return {
7923
+ message: `
7924
+ \u274C Invalid Download Options
7925
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
7926
+
7927
+ \u26A0\uFE0F Blob downloads (non-recursive) require:
7928
+ - The source blob name must NOT end with '/' or '\\'
7929
+ - The destination path must NOT end with '/' or '\\'
7930
+
7931
+ \u{1F9FE} Received:
7932
+ Source: ${data.source}
7933
+ Destination: ${data.destination}
7934
+
7935
+ ` + tipMessage + "\n"
7936
+ };
7810
7937
  }
7811
7938
  );
7812
7939
  async function validateOutput(options) {
7813
- const parentDir = path3.dirname(options.dst);
7940
+ const parentDir = path3.dirname(options.destination);
7814
7941
  try {
7815
7942
  const parentStats = await fs3.stat(parentDir);
7816
7943
  if (!parentStats.isDirectory()) {
7817
7944
  throw new Error(
7818
- `Parent path '${parentDir}' exists but is not a directory. Cannot create dst ${options.recursive ? "directory" : "file"} '${options.dst}'.`
7945
+ `Parent path '${parentDir}' exists but is not a directory. Cannot create destination ${options.recursive ? "directory" : "file"} '${options.destination}'.`
7819
7946
  );
7820
7947
  }
7821
7948
  } catch (error) {
7822
7949
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
7823
7950
  throw new Error(
7824
- `Parent directory '${parentDir}' of dst '${options.dst}' does not exist. Create it first or use a different dst path.`
7951
+ `Parent directory '${parentDir}' of destination '${options.destination}' does not exist. Create it first or use a different destination path.`
7825
7952
  );
7826
7953
  }
7827
7954
  throw error;
@@ -7829,7 +7956,7 @@ async function validateOutput(options) {
7829
7956
  if (options.force) return;
7830
7957
  let outputStats;
7831
7958
  try {
7832
- outputStats = await fs3.stat(options.dst);
7959
+ outputStats = await fs3.stat(options.destination);
7833
7960
  } catch (error) {
7834
7961
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
7835
7962
  return;
@@ -7839,61 +7966,70 @@ async function validateOutput(options) {
7839
7966
  const isDirectory = outputStats.isDirectory();
7840
7967
  if (!options.recursive && isDirectory) {
7841
7968
  throw new Error(
7842
- `dst path '${options.dst}' is a directory but expected a file path to download a single blob (--recursive not provided).`
7969
+ `destination path '${options.destination}' is a directory but expected a file path to download a single blob (--recursive not provided).`
7843
7970
  );
7844
7971
  }
7845
7972
  if (options.recursive && !isDirectory) {
7846
7973
  throw new Error(
7847
- `dst path '${options.dst}' exists but is not a directory. Directory downloads require a directory path.`
7974
+ `destination path '${options.destination}' exists but is not a directory. Directory downloads require a directory path.`
7848
7975
  );
7849
7976
  }
7850
7977
  if (options.recursive) {
7851
- const entries = await fs3.readdir(options.dst);
7978
+ const entries = await fs3.readdir(options.destination);
7852
7979
  if (entries.length > 0) {
7853
7980
  throw new Error(
7854
- `Directory '${options.dst}' exists and is not empty. Use --force to overwrite or choose an empty directory.`
7981
+ `Directory '${options.destination}' exists and is not empty. Use --force to overwrite or choose an empty directory.`
7855
7982
  );
7856
7983
  }
7857
7984
  } else {
7858
7985
  throw new Error(
7859
- `File '${options.dst}' already exists. Use --force to overwrite.`
7986
+ `File '${options.destination}' already exists. Use --force to overwrite.`
7860
7987
  );
7861
7988
  }
7862
7989
  }
7863
7990
  async function createFileList(options, nodeClient, account) {
7864
7991
  const matchingBlobList = [];
7992
+ const stripAccountPrefix = (fullName) => {
7993
+ const blobNameMatch = fullName.match(/^@[^/]+\/(.+)$/);
7994
+ return blobNameMatch ? blobNameMatch[1] : fullName;
7995
+ };
7865
7996
  if (options.recursive) {
7866
- const allBlobs = await nodeClient.coordination.getAccountBlobs({
7997
+ const activeBlobs = await nodeClient.coordination.getAccountBlobs({
7867
7998
  account
7868
7999
  });
7869
- for (const blobMd of allBlobs) {
7870
- const blobNameWithoutAccount = blobMd.name.slice(1 + 64 + 1);
7871
- if (!blobNameWithoutAccount.startsWith(options.src)) continue;
8000
+ for (const blobMd of activeBlobs) {
8001
+ const blobNameWithoutAccount = stripAccountPrefix(blobMd.name);
8002
+ if (!blobNameWithoutAccount.startsWith(options.source)) continue;
7872
8003
  matchingBlobList.push(blobMd);
7873
8004
  }
7874
8005
  if (matchingBlobList.length === 0) {
7875
8006
  throw new Error(
7876
- `No blobs matching prefix ${options.src} were found in account ${account}`
8007
+ `No active blobs matching prefix '${options.source}' were found for account ${account.toString()}. They may have expired or been deleted.`
7877
8008
  );
7878
8009
  }
7879
8010
  } else {
7880
- const md = await nodeClient.coordination.getBlobMetadata({
7881
- account,
7882
- name: options.src
8011
+ const activeBlobs = await nodeClient.coordination.getAccountBlobs({
8012
+ account
7883
8013
  });
7884
- if (md === void 0) {
7885
- throw new Error(`Could not find a blob with name ${options.src}`);
8014
+ const md = activeBlobs.find(
8015
+ (blob) => stripAccountPrefix(blob.name) === options.source
8016
+ );
8017
+ if (!md) {
8018
+ throw new Error(
8019
+ `No active blob named '${options.source}' was found for account ${account.toString()}. It may have expired or been deleted.
8020
+ `
8021
+ );
7886
8022
  }
7887
8023
  matchingBlobList.push(md);
7888
8024
  }
7889
8025
  const ret = [];
7890
8026
  for (const blobMd of matchingBlobList) {
7891
- const blobNameWithoutAccount = blobMd.name.slice(1 + 64 + 1);
8027
+ const blobNameWithoutAccount = stripAccountPrefix(blobMd.name);
7892
8028
  const entry = {
7893
8029
  filename: denormBlobName2(
7894
- options.src,
8030
+ options.source,
7895
8031
  blobNameWithoutAccount,
7896
- options.dst
8032
+ options.destination
7897
8033
  ),
7898
8034
  blobname: blobNameWithoutAccount,
7899
8035
  // FIXME this is nasty
@@ -7935,96 +8071,170 @@ function createProgressTransform(totalBytes, reporter) {
7935
8071
  });
7936
8072
  }
7937
8073
  function downloadCommand(program) {
7938
- program.command("download <src> <dst>").description("Download a file or directory from Shelby RPC").option(
8074
+ program.command("download").description("Download a file or directory from Shelby RPC").argument("[source]", "Source blob name or prefix").argument("[destination]", "Destination path").option(
7939
8075
  "-r, --recursive",
7940
8076
  "Download assuming canonical directory layout using '/' as separators. Produces a directory."
7941
- ).option("-f, --force", "Overwrite the dst").action(
7942
- async (src, dst, rawOptions) => {
7943
- const options = DownloadOptionsSchema.parse({
7944
- ...rawOptions,
7945
- src,
7946
- dst
7947
- });
7948
- options.dst = path3.resolve(options.dst);
7949
- await validateOutput(options);
7950
- if (options.recursive) {
7951
- console.log(
7952
- `Downloading directory: ${options.src} -> ${options.dst}`
7953
- );
7954
- } else {
7955
- console.log(`Downloading blob: ${options.src} -> ${options.dst}`);
7956
- }
7957
- const configPath = program.opts().configFile;
7958
- let config;
8077
+ ).option("-f, --force", "Overwrite the destination").action(
8078
+ async (source, destination, rawOptions) => {
8079
+ let options;
7959
8080
  try {
7960
- config = loadConfig(configPath);
8081
+ options = DownloadOptionsSchema.parse({
8082
+ ...rawOptions,
8083
+ source,
8084
+ destination
8085
+ });
7961
8086
  } catch (error) {
7962
- console.error(`Error: ${error.message}`);
8087
+ if (error instanceof z13.ZodError) {
8088
+ const firstIssue = error.issues[0];
8089
+ if (firstIssue) {
8090
+ console.log(firstIssue.message);
8091
+ } else {
8092
+ console.error("\u26A0\uFE0F Invalid download options provided");
8093
+ }
8094
+ } else {
8095
+ console.error(
8096
+ `\u26A0\uFE0F ${error instanceof Error ? error.message : String(error)}`
8097
+ );
8098
+ }
7963
8099
  process.exit(1);
7964
8100
  }
7965
- const shelbyConfig = getCurrentShelbyConfig(config, {
7966
- context: program.opts().context
7967
- });
7968
- const activeAccount = getCurrentAccount(
7969
- config,
7970
- program.opts().account
7971
- ).account;
7972
- const nodeClient = new ShelbyNodeClient(shelbyConfig);
7973
- const fileList = await createFileList(
7974
- options,
7975
- nodeClient,
7976
- activeAccount.accountAddress
7977
- );
7978
- for (const fileEntry of fileList) {
7979
- console.log(
7980
- `Downloading ${fileEntry.blobname} -> ${fileEntry.filename}`
7981
- );
7982
- }
7983
- if (options.force) {
7984
- console.log(`--force was set, so deleting ${options.dst}`);
7985
- await fs3.rm(options.dst, { recursive: true, force: true });
7986
- }
8101
+ options.destination = path3.resolve(options.destination);
8102
+ let config;
8103
+ let spinner;
7987
8104
  const handleSigint = () => {
7988
- spinner.fail("Quitting due to ctrl-C / SIGINT...");
8105
+ spinner?.fail("Quitting due to ctrl-C / SIGINT...");
7989
8106
  process.exit(1);
7990
8107
  };
7991
- process.removeAllListeners("SIGINT");
7992
- process.on("SIGINT", handleSigint);
7993
- const spinner = ora({
7994
- text: "Creating dst directory tree...",
7995
- discardStdin: false
7996
- }).start();
7997
- await createOutputDirectories(fileList);
7998
- let totalSize = 0;
7999
- for (const fileEntry of fileList) totalSize += fileEntry.sizeBytes;
8000
- const startTime = performance.now();
8001
- let amountDownloaded = 0;
8002
- const formatProgressPercent = () => (100 * (amountDownloaded / totalSize)).toFixed(2);
8003
- const formatProgressRate = () => {
8004
- const mib = amountDownloaded / 1024 / 1024;
8005
- const sec = (performance.now() - startTime) / 1e3;
8006
- return (mib / sec).toFixed(2);
8007
- };
8008
- for (const fileEntry of fileList) {
8009
- const out = fsS.createWriteStream(fileEntry.filename);
8010
- const { readable } = await nodeClient.rpc.getBlob({
8011
- account: activeAccount.accountAddress,
8012
- blobName: fileEntry.blobname
8108
+ try {
8109
+ const configPath = program.opts().configFile;
8110
+ config = loadConfig(configPath);
8111
+ const shelbyConfig = getCurrentShelbyConfig(config, {
8112
+ context: program.opts().context
8013
8113
  });
8014
- const reporter = (filePercent, rateMiB) => {
8015
- spinner.text = `Downloading ${fileEntry.blobname}, ${filePercent}% at ${rateMiB} MiB/s .. (Overall: ${formatProgressPercent()}%, ${formatProgressRate()} MiB/s)`;
8016
- };
8017
- const nodeStream = Readable2.fromWeb(readable);
8018
- await pipeline(
8019
- nodeStream,
8020
- createProgressTransform(fileEntry.sizeBytes, reporter),
8021
- out
8114
+ const activeAccount = getCurrentAccount(
8115
+ config,
8116
+ program.opts().account
8117
+ ).account;
8118
+ const nodeClient = new ShelbyNodeClient(shelbyConfig);
8119
+ const fileList = await createFileList(
8120
+ options,
8121
+ nodeClient,
8122
+ activeAccount.accountAddress
8123
+ );
8124
+ await validateOutput(options);
8125
+ console.log(`
8126
+ \u{1F4E5} Downloading Files (${fileList.length} total)`);
8127
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
8128
+ for (let i = 0; i < fileList.length; i++) {
8129
+ const fileEntry = fileList[i];
8130
+ const blobBaseName = path3.basename(fileEntry.blobname);
8131
+ console.log(`${i + 1}. ${blobBaseName} \u2192 ${fileEntry.filename}`);
8132
+ }
8133
+ console.log();
8134
+ if (options.force) {
8135
+ console.log(`--force was set, so deleting ${options.destination}`);
8136
+ await fs3.rm(options.destination, { recursive: true, force: true });
8137
+ }
8138
+ process.removeAllListeners("SIGINT");
8139
+ process.on("SIGINT", handleSigint);
8140
+ spinner = ora({
8141
+ text: "Creating destination directory tree...",
8142
+ discardStdin: false
8143
+ }).start();
8144
+ await createOutputDirectories(fileList);
8145
+ let totalSize = 0;
8146
+ for (const fileEntry of fileList) totalSize += fileEntry.sizeBytes;
8147
+ const startTime = performance.now();
8148
+ let amountDownloaded = 0;
8149
+ let filesDownloaded = 0;
8150
+ const isSingleFile = fileList.length === 1;
8151
+ const formatProgressPercent = () => totalSize > 0 ? (100 * (amountDownloaded / totalSize)).toFixed(2) : "0.00";
8152
+ for (const fileEntry of fileList) {
8153
+ const out = fsS.createWriteStream(fileEntry.filename);
8154
+ const fileName = path3.basename(fileEntry.blobname);
8155
+ const { readable } = await nodeClient.rpc.getBlob({
8156
+ account: activeAccount.accountAddress,
8157
+ blobName: fileEntry.blobname
8158
+ });
8159
+ const reporter = (filePercent, _rateMiB) => {
8160
+ if (spinner) {
8161
+ if (isSingleFile) {
8162
+ spinner.text = `Downloading ${fileName}... (${filePercent}%)`;
8163
+ } else {
8164
+ spinner.text = `Downloading ${fileName}... (${filesDownloaded}/${fileList.length} files, ${formatProgressPercent()}%)`;
8165
+ }
8166
+ }
8167
+ };
8168
+ const nodeStream = Readable2.fromWeb(readable);
8169
+ await pipeline(
8170
+ nodeStream,
8171
+ createProgressTransform(fileEntry.sizeBytes, reporter),
8172
+ out
8173
+ );
8174
+ amountDownloaded += fileEntry.sizeBytes;
8175
+ filesDownloaded++;
8176
+ }
8177
+ const totalTime = (performance.now() - startTime) / 1e3;
8178
+ spinner?.stop();
8179
+ console.log(
8180
+ `
8181
+ \u2705 All downloads complete \u2014 took ${totalTime.toFixed(1)} seconds`
8022
8182
  );
8023
- amountDownloaded += fileEntry.sizeBytes;
8183
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
8184
+ console.log("\u2728 Done!");
8185
+ } catch (error) {
8186
+ spinner?.stop();
8187
+ console.log("\n\u274C Download Failed");
8188
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
8189
+ if (options.recursive) {
8190
+ console.log(`\u{1F9FE} Directory: ${options.source}`);
8191
+ } else {
8192
+ console.log(`\u{1F9FE} File: ${options.source}`);
8193
+ }
8194
+ if (config) {
8195
+ const activeAccount = getCurrentAccount(
8196
+ config,
8197
+ program.opts().account
8198
+ ).account;
8199
+ console.log(
8200
+ `\u{1F464} Account: ${activeAccount.accountAddress.toString()}`
8201
+ );
8202
+ }
8203
+ console.log();
8204
+ if (error instanceof Error) {
8205
+ if (error.message.includes("GraphQL Error (Code: 401)")) {
8206
+ const contextName = program.opts().context || config?.default_context;
8207
+ console.error("\u26A0\uFE0F Authentication required.");
8208
+ console.error(
8209
+ " Please add an indexer API key to your context configuration."
8210
+ );
8211
+ console.error();
8212
+ console.error("\u{1F4A1} Tip: Update your context with an API key:");
8213
+ console.error(
8214
+ ` \u{1F449} shelby context update ${contextName ?? "default"} --indexer-api-key YOUR_KEY`
8215
+ );
8216
+ } else if (error.message.includes("No active blob") || error.message.includes("may have expired or been deleted")) {
8217
+ const blobName = options.source;
8218
+ console.error(
8219
+ `\u26A0\uFE0F No active blob${options.recursive ? "s" : ""} named '${blobName}' ${options.recursive ? "were" : "was"} found.`
8220
+ );
8221
+ console.error(" It may have expired or been deleted.");
8222
+ console.error();
8223
+ console.error(
8224
+ "\u{1F4A1} Tip: Try listing available blobs before downloading:"
8225
+ );
8226
+ console.error(" \u{1F449} shelby account blobs");
8227
+ } else {
8228
+ console.error(`\u26A0\uFE0F ${error.message}`);
8229
+ }
8230
+ } else {
8231
+ console.error(`\u26A0\uFE0F ${String(error)}`);
8232
+ }
8233
+ console.log("\n");
8234
+ process.exit(1);
8235
+ } finally {
8236
+ process.removeListener("SIGINT", handleSigint);
8024
8237
  }
8025
- const totalTime = (performance.now() - startTime) / 1e3;
8026
- const totalRate = amountDownloaded / 1024 / 1024 / totalTime;
8027
- spinner.succeed(`Final rate ${totalRate.toFixed(2)} MiB/s`);
8028
8238
  }
8029
8239
  );
8030
8240
  }
@@ -8246,13 +8456,63 @@ import * as path5 from "path";
8246
8456
  import { Aptos as Aptos5, AptosConfig as AptosConfig4 } from "@aptos-labs/ts-sdk";
8247
8457
  import * as chrono from "chrono-node";
8248
8458
  import { glob } from "glob";
8459
+ import ignore from "ignore";
8249
8460
  import { Box as Box7, render as render4, Text as Text7 } from "ink";
8250
8461
  import SelectInput4 from "ink-select-input";
8251
8462
  import ora2 from "ora";
8252
8463
  import { z as z15 } from "zod";
8464
+
8465
+ // src/utils/commander-helpers.ts
8466
+ function createExitOverrideHandler(commandName, requiredArgs, requiredOptions, exampleUsage, warningMessage) {
8467
+ return (err) => {
8468
+ if (err.code === "commander.helpDisplayed") {
8469
+ process.exit(0);
8470
+ }
8471
+ const args = process.argv.slice(process.argv.indexOf(commandName) + 1);
8472
+ const hasArguments = args.some((arg) => !arg.startsWith("-"));
8473
+ if (err.code === "commander.missingMandatoryOptionValue") {
8474
+ if (!hasArguments && requiredOptions) {
8475
+ console.error("\n\u274C Missing Required Arguments");
8476
+ console.error("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
8477
+ const warning = warningMessage || "Missing required arguments or options";
8478
+ console.error(`\u26A0\uFE0F ${warning}
8479
+ `);
8480
+ console.error("\u{1F4A1} Usage:");
8481
+ console.error(
8482
+ ` shelby ${commandName} ${requiredArgs} ${requiredOptions}
8483
+ `
8484
+ );
8485
+ if (exampleUsage) {
8486
+ console.error("\u{1F4DD} Examples:");
8487
+ console.error(` ${exampleUsage}`);
8488
+ }
8489
+ console.error("\n");
8490
+ } else {
8491
+ console.error(err.message);
8492
+ }
8493
+ process.exit(1);
8494
+ }
8495
+ if (err.code === "commander.optionMissingArgument") {
8496
+ console.error("\n\u274C Missing Option Value");
8497
+ console.error("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
8498
+ const cleanMessage = err.message.replace(/^error:\s*/i, "");
8499
+ console.error(`\u26A0\uFE0F ${cleanMessage}
8500
+ `);
8501
+ if (exampleUsage) {
8502
+ console.error("\u{1F4A1} Example:");
8503
+ console.error(` ${exampleUsage}`);
8504
+ }
8505
+ console.error("\n");
8506
+ process.exit(1);
8507
+ }
8508
+ throw err;
8509
+ };
8510
+ }
8511
+
8512
+ // src/commands/upload.tsx
8253
8513
  import { jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
8254
8514
  var normBlobName2 = (i, f, b) => normBlobName(path5, i, f, b);
8255
- var flexibleDateSchema = z15.string().transform((val) => {
8515
+ var flexibleDateSchema = z15.string().transform((val, ctx) => {
8256
8516
  const now = /* @__PURE__ */ new Date();
8257
8517
  let parsedDate = null;
8258
8518
  if (/^\d+$/.test(val)) {
@@ -8274,77 +8534,207 @@ var flexibleDateSchema = z15.string().transform((val) => {
8274
8534
  }
8275
8535
  }
8276
8536
  if (!parsedDate || Number.isNaN(parsedDate.getTime())) {
8277
- throw new Error(
8278
- `Invalid date format: "${val}". Try formats like "tomorrow", "in 2 days", "next Friday", "2025-12-31", or a UNIX timestamp.`
8279
- );
8537
+ ctx.addIssue({
8538
+ code: z15.ZodIssueCode.custom,
8539
+ message: `
8540
+ \u274C Upload Failed
8541
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8542
+
8543
+ \u26A0\uFE0F Invalid date format: "${val}"
8544
+
8545
+ \u{1F4A1} Try formats like:
8546
+ \u2022 "tomorrow", "in 2 days", "next Friday"
8547
+ \u2022 "2025-12-31"
8548
+ \u2022 UNIX timestamp
8549
+
8550
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`
8551
+ });
8552
+ return z15.NEVER;
8280
8553
  }
8281
8554
  if (parsedDate.getTime() <= now.getTime()) {
8282
- throw new Error(
8283
- `Expiration date must be in the future. "${val}" resolves to ${parsedDate.toLocaleString()} which is in the past.`
8284
- );
8555
+ ctx.addIssue({
8556
+ code: z15.ZodIssueCode.custom,
8557
+ message: `
8558
+ \u274C Upload Failed
8559
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8560
+
8561
+ \u26A0\uFE0F Expiration date must be in the future
8562
+
8563
+ \u{1F4C5} "${val}" resolves to ${parsedDate.toLocaleString()}
8564
+ which is in the past.
8565
+
8566
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`
8567
+ });
8568
+ return z15.NEVER;
8285
8569
  }
8286
8570
  return parsedDate;
8287
8571
  });
8288
8572
  var UploadOptionsSchema = z15.object({
8289
- src: z15.string().nonempty("`src` positional arg is required"),
8290
- dst: z15.string().nonempty("`dst` position arg is required"),
8573
+ source: z15.string({
8574
+ required_error: '\n\u274C Missing Required Argument\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\u26A0\uFE0F Missing source file or directory path (first argument)\n\n\u{1F4A1} Usage:\n shelby upload <source-file-or-directory> <destination-blob-name> [options]\n\n\u{1F4DD} Examples:\n shelby upload ./myfile.txt my-blob.txt -e tomorrow\n shelby upload ./my-folder/ my-folder/ -r -e "next week"\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
8575
+ }).min(1, "Source file or directory path is required"),
8576
+ destination: z15.string({
8577
+ required_error: "\n\u274C Missing Required Argument\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\u26A0\uFE0F Missing destination blob name (second argument)\n\n\u{1F4A1} Usage:\n shelby upload <source-file-or-directory> <destination-blob-name> [options]\n\n\u{1F4DD} Example:\n shelby upload ./myfile.txt files/my-blob.txt -e tomorrow\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"
8578
+ }).min(1, "Destination blob name is required"),
8291
8579
  expiration: flexibleDateSchema,
8292
8580
  recursive: z15.boolean().optional().default(false),
8293
8581
  assumeYes: z15.boolean().optional().default(false),
8294
8582
  outputCommitments: z15.string().optional()
8295
8583
  }).superRefine(async (data, ctx) => {
8296
- const stats = await fs5.stat(data.src);
8584
+ const stats = await fs5.stat(data.source);
8297
8585
  if (!stats.isFile() && !stats.isDirectory()) {
8298
8586
  ctx.addIssue({
8299
8587
  code: z15.ZodIssueCode.custom,
8300
- message: "`src` must be a file or a directory",
8301
- path: ["src"]
8588
+ message: "\n\u274C Upload Failed\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\u26A0\uFE0F Source path must be a file or directory\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
8589
+ path: ["source"]
8302
8590
  });
8303
8591
  return;
8304
8592
  }
8305
8593
  if (stats.isDirectory()) {
8306
- if (!data.dst.endsWith("/")) {
8594
+ if (!data.destination.endsWith("/")) {
8307
8595
  ctx.addIssue({
8308
8596
  code: z15.ZodIssueCode.custom,
8309
- message: "When src is a directory, dst must end with '/'",
8310
- path: ["dst"]
8597
+ message: `
8598
+ \u274C Upload Failed
8599
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8600
+
8601
+ \u26A0\uFE0F When source is a directory, destination must end with '/'
8602
+
8603
+ \u{1F4A1} Tip: When uploading a directory, add a trailing slash:
8604
+ \u{1F449} shelby upload -r ${data.source} ${data.destination}/
8605
+
8606
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`,
8607
+ path: ["destination"]
8311
8608
  });
8312
8609
  }
8313
8610
  } else if (stats.isFile()) {
8314
- const blobNameResult = BlobNameSchema.safeParse(data.dst);
8611
+ const blobNameResult = BlobNameSchema.safeParse(data.destination);
8315
8612
  if (!blobNameResult.success) {
8316
8613
  ctx.addIssue({
8317
8614
  code: z15.ZodIssueCode.custom,
8318
- message: "When src is a file, dst must be a valid blob name (cannot end with '/')",
8319
- path: ["dst"]
8615
+ message: `
8616
+ \u274C Upload Failed
8617
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8618
+
8619
+ \u26A0\uFE0F When source is a file, destination must be a valid blob name (cannot end with '/')
8620
+
8621
+ \u{1F4A1} Tip: When uploading a file, remove the trailing slash:
8622
+ \u{1F449} shelby upload ${data.source} ${data.destination.replace(/\/$/, "")}
8623
+
8624
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`,
8625
+ path: ["destination"]
8320
8626
  });
8321
8627
  }
8322
8628
  }
8323
8629
  });
8630
+ async function buildGitignoreMap(root) {
8631
+ const resolvedRoot = path5.resolve(root);
8632
+ const gitignoreMap = /* @__PURE__ */ new Map();
8633
+ const ensureMatcher = (dir) => {
8634
+ const existing = gitignoreMap.get(dir);
8635
+ if (existing) {
8636
+ return existing;
8637
+ }
8638
+ const instance = ignore();
8639
+ gitignoreMap.set(dir, instance);
8640
+ return instance;
8641
+ };
8642
+ ensureMatcher(resolvedRoot).add(".git/");
8643
+ const gitignoreFiles = await glob("**/.gitignore", {
8644
+ cwd: resolvedRoot,
8645
+ dot: true,
8646
+ absolute: true,
8647
+ nodir: true
8648
+ });
8649
+ for (const gitignorePath of gitignoreFiles) {
8650
+ try {
8651
+ const contents = await fs5.readFile(gitignorePath, "utf8");
8652
+ const directory = path5.dirname(gitignorePath);
8653
+ ensureMatcher(directory).add(contents);
8654
+ } catch (error) {
8655
+ if (error instanceof Error && Object.hasOwn(error, "code") && // biome-ignore lint/suspicious/noExplicitAny: node fs error type
8656
+ error.code === "ENOENT") {
8657
+ continue;
8658
+ }
8659
+ throw error;
8660
+ }
8661
+ }
8662
+ return gitignoreMap;
8663
+ }
8664
+ function collectDirectoriesFromRoot(root, filePath) {
8665
+ const directories = [];
8666
+ let current = path5.dirname(filePath);
8667
+ while (true) {
8668
+ directories.push(current);
8669
+ if (current === root) {
8670
+ break;
8671
+ }
8672
+ const parent = path5.dirname(current);
8673
+ if (parent === current) {
8674
+ break;
8675
+ }
8676
+ current = parent;
8677
+ }
8678
+ return directories.reverse();
8679
+ }
8680
+ function shouldIgnorePath(gitignoreMap, root, filePath) {
8681
+ const directories = collectDirectoriesFromRoot(root, filePath);
8682
+ let ignored = false;
8683
+ for (const directory of directories) {
8684
+ const matcher = gitignoreMap.get(directory);
8685
+ if (!matcher) {
8686
+ continue;
8687
+ }
8688
+ const relative2 = path5.relative(directory, filePath).split(path5.sep).join("/");
8689
+ const result = matcher.test(relative2);
8690
+ if (result.ignored) {
8691
+ ignored = true;
8692
+ }
8693
+ if (result.unignored) {
8694
+ ignored = false;
8695
+ }
8696
+ }
8697
+ return ignored;
8698
+ }
8324
8699
  async function createFilelist(options) {
8325
- const stats = await fs5.stat(options.src);
8700
+ const stats = await fs5.stat(options.source);
8326
8701
  if (stats.isFile()) {
8327
- const blobname = normBlobName2(options.src, options.src, options.dst);
8702
+ const blobname = normBlobName2(
8703
+ options.source,
8704
+ options.source,
8705
+ options.destination
8706
+ );
8328
8707
  const blobNameValidation = BlobNameSchema.safeParse(blobname);
8329
8708
  if (!blobNameValidation.success) {
8330
8709
  throw new Error(
8331
- `File ${options.src} as blobname ${blobname} would form an invalid blob name: ${blobNameValidation.error.message}`
8710
+ `File ${options.source} as blobname ${blobname} would form an invalid blob name: ${blobNameValidation.error.message}`
8332
8711
  );
8333
8712
  }
8334
- return [{ filename: options.src, blobname, sizeBytes: stats.size }];
8713
+ return [{ filename: options.source, blobname, sizeBytes: stats.size }];
8335
8714
  }
8336
8715
  if (!options.recursive) {
8337
8716
  throw new Error(
8338
- `${options.src} is a directory. Use --recursive to upload directories.`
8717
+ `${options.source} is a directory. Use --recursive to upload directories.`
8339
8718
  );
8340
8719
  }
8720
+ const sourceRoot = path5.resolve(options.source);
8721
+ const gitignoreMap = await buildGitignoreMap(sourceRoot);
8341
8722
  const fileList = [];
8342
- const result = await glob(path5.join(options.src, "**", "*"), {
8343
- nodir: true
8723
+ const result = await glob("**/*", {
8724
+ cwd: sourceRoot,
8725
+ absolute: true,
8726
+ nodir: true,
8727
+ dot: true
8344
8728
  });
8345
8729
  for await (const file of result) {
8730
+ if (shouldIgnorePath(gitignoreMap, sourceRoot, file)) {
8731
+ continue;
8732
+ }
8346
8733
  const stats2 = await fs5.stat(file);
8347
- const blobname = normBlobName2(options.src, file, options.dst);
8734
+ if (!stats2.isFile()) {
8735
+ continue;
8736
+ }
8737
+ const blobname = normBlobName2(sourceRoot, file, options.destination);
8348
8738
  const blobNameValidation = BlobNameSchema.safeParse(blobname);
8349
8739
  if (!blobNameValidation.success) {
8350
8740
  throw new Error(
@@ -8360,7 +8750,7 @@ async function createFilelist(options) {
8360
8750
  return fileList;
8361
8751
  }
8362
8752
  function uploadCommand(program) {
8363
- program.command("upload <src> <dst>").description("Upload a file or directory to the shelby RPC in the config.").requiredOption(
8753
+ const uploadCmd = program.command("upload").description("Upload a file or directory to the shelby RPC in the config.").argument("[source]", "Source file or directory path").argument("[destination]", "Destination blob name").requiredOption(
8364
8754
  "-e, --expiration <datetime>",
8365
8755
  'Expiration date/time (required). Examples: "tomorrow", "in 2 days", "next Friday", "2025-12-31", UNIX timestamp'
8366
8756
  ).option("-r, --recursive", "If uploading a directory, recurse").option(
@@ -8369,205 +8759,227 @@ function uploadCommand(program) {
8369
8759
  ).option(
8370
8760
  "--output-commitments <filename>",
8371
8761
  "Location to store commitments computed as as part of the upload"
8372
- ).action(async (src, dst, options) => {
8373
- let validatedOptions;
8374
- try {
8375
- validatedOptions = await UploadOptionsSchema.parseAsync({
8376
- ...options,
8377
- src,
8378
- dst
8379
- });
8380
- } catch (error) {
8381
- const { displayMessage } = handleError(error);
8382
- console.error(`\u274C ${displayMessage}`);
8383
- process.exit(1);
8384
- }
8385
- const configPath = program.opts().configFile;
8386
- let config;
8387
- try {
8388
- config = loadConfig(configPath);
8389
- } catch (error) {
8390
- console.error(`Error: ${error.message}`);
8391
- process.exit(1);
8392
- }
8393
- const start = performance.now();
8394
- const filelist = await createFilelist(validatedOptions);
8395
- const timeToCreateFilelist = ((performance.now() - start) / 1e3).toFixed(
8396
- 5
8397
- );
8398
- console.log("\n\u{1F680} Upload Summary");
8399
- console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
8400
- if (filelist.length === 1) {
8401
- console.log(`\u{1F4E6} File: ${filelist[0].filename}`);
8402
- console.log(`\u{1F4C1} Blob Name: ${filelist[0].blobname}`);
8403
- } else {
8404
- console.log(`\u{1F4E6} Files: ${filelist.length} files`);
8762
+ ).configureOutput({
8763
+ writeErr: (_str) => {
8405
8764
  }
8406
- console.log("");
8407
- console.log(
8408
- `\u{1F9EE} Filelist created (${filelist.length} ${filelist.length === 1 ? "entry" : "entries"})`
8409
- );
8410
- console.log(`\u23F1\uFE0F Took: ${timeToCreateFilelist}s`);
8411
- if (validatedOptions.assumeYes) {
8412
- console.log("\u2699\uFE0F Flag: --assume-yes (auto-confirmed)");
8413
- }
8414
- if (!validatedOptions.assumeYes) {
8415
- const shouldContinue = await new Promise((resolve2) => {
8416
- const { unmount } = render4(
8417
- /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
8418
- /* @__PURE__ */ jsx13(Text7, { children: "Continue?" }),
8419
- /* @__PURE__ */ jsx13(
8420
- SelectInput4,
8421
- {
8422
- items: [
8423
- { label: "Yes", value: true },
8424
- { label: "No", value: false }
8425
- ],
8426
- onSelect: (item) => {
8427
- unmount();
8428
- resolve2(item.value);
8429
- }
8430
- }
8431
- )
8432
- ] })
8433
- );
8434
- });
8435
- if (!shouldContinue) {
8436
- console.log("Upload cancelled.");
8437
- process.exit(0);
8438
- }
8439
- }
8440
- const shelbyConfig = getCurrentShelbyConfig(config, {
8441
- context: program.opts().context
8442
- });
8443
- const activeAccount = getCurrentAccount(
8444
- config,
8445
- program.opts().account
8446
- ).account;
8447
- const aptos = new Aptos5(new AptosConfig4(shelbyConfig.aptos));
8448
- const shelbyClient = new ShelbyNodeClient(shelbyConfig);
8449
- const handleSigint = () => {
8450
- spinner.fail("Quitting due to ctrl-C / SIGINT...");
8451
- process.exit(1);
8452
- };
8453
- process.removeAllListeners("SIGINT");
8454
- process.on("SIGINT", handleSigint);
8455
- const expireUsec = validatedOptions.expiration.getTime() * 1e3;
8456
- const expirationDate = new Date(expireUsec / 1e3);
8457
- const formattedExpiration = expirationDate.toLocaleDateString("en-US", {
8458
- year: "numeric",
8459
- month: "short",
8460
- day: "numeric",
8461
- hour: "numeric",
8462
- minute: "2-digit",
8463
- second: "2-digit",
8464
- hour12: true
8465
- });
8466
- console.log("");
8467
- console.log(`\u{1F552} Expires: ${formattedExpiration}`);
8468
- const spinner = ora2({
8469
- text: "Uploading...",
8470
- discardStdin: false
8471
- }).start();
8472
- const startTime = performance.now();
8473
- let totalSize = 0;
8474
- let amountUploaded = 0;
8475
- const outputCommitments = {};
8476
- let firstTransactionHash;
8477
- const formatProgressPercent = () => (100 * (amountUploaded / totalSize)).toFixed(2);
8478
- for (const entry of filelist) {
8479
- totalSize += entry.sizeBytes;
8480
- }
8481
- let hasUploadedBlob = false;
8482
- for (const entry of filelist) {
8483
- const fileName = path5.basename(entry.filename);
8484
- spinner.text = `\u{1F4D6} Reading ${fileName}... (${formatProgressPercent()}%)`;
8765
+ });
8766
+ uploadCmd.exitOverride(
8767
+ createExitOverrideHandler(
8768
+ "upload",
8769
+ "<source> <destination>",
8770
+ "-e, --expiration <datetime>",
8771
+ 'shelby upload ./file.txt myblob -e "in 2 days"\n shelby upload ./my-folder/ my-folder/ -r -e tomorrow',
8772
+ "Missing source, destination, and expiration"
8773
+ )
8774
+ );
8775
+ uploadCmd.action(
8776
+ async (source, destination, options) => {
8777
+ let validatedOptions;
8485
8778
  try {
8486
- const blobData = await fs5.readFile(entry.filename);
8487
- if (blobData.length !== entry.sizeBytes) {
8488
- throw new Error(
8489
- `Size of file ${entry.filename} changed after initial scan. Original size was ${entry.sizeBytes} but it is now ${blobData.length}`
8490
- );
8491
- }
8492
- spinner.text = `\u{1F517} Checking if blob exists... (${formatProgressPercent()}%)`;
8493
- const existingBlobMetadata = await shelbyClient.coordination.getBlobMetadata({
8494
- account: activeAccount.accountAddress,
8495
- name: entry.blobname
8779
+ validatedOptions = await UploadOptionsSchema.parseAsync({
8780
+ ...options,
8781
+ source,
8782
+ destination
8496
8783
  });
8497
- if (existingBlobMetadata) {
8498
- spinner.fail(
8499
- `Blob '${entry.blobname}' already exists. Please use a different name or delete the existing blob first.`
8500
- );
8501
- process.exit(1);
8502
- }
8503
- spinner.text = `\u{1F517} Generating commitments for ${fileName}... (${formatProgressPercent()}%)`;
8504
- const provider = await getErasureCodingProvider();
8505
- const blobCommitments = await generateCommitments(provider, blobData);
8506
- if (validatedOptions.outputCommitments) {
8507
- outputCommitments[entry.filename] = blobCommitments;
8508
- }
8509
- spinner.text = `\u{1F517} Registering ${fileName} on L1... (${formatProgressPercent()}%)`;
8510
- const { transaction: pendingRegisterBlobTransaction } = await shelbyClient.coordination.registerBlob({
8511
- account: activeAccount,
8512
- blobName: entry.blobname,
8513
- blobMerkleRoot: blobCommitments.blob_merkle_root,
8514
- size: blobData.length,
8515
- expirationMicros: validatedOptions.expiration.getTime() * 1e3
8516
- });
8517
- const registerTransactionHash = pendingRegisterBlobTransaction.hash;
8518
- if (!firstTransactionHash) {
8519
- firstTransactionHash = registerTransactionHash;
8784
+ } catch (error) {
8785
+ if (error instanceof z15.ZodError) {
8786
+ const firstIssue = error.issues[0];
8787
+ if (firstIssue) {
8788
+ console.log(firstIssue.message);
8789
+ } else {
8790
+ console.error("\u26A0\uFE0F Invalid options provided");
8791
+ }
8792
+ } else {
8793
+ const { displayMessage } = handleError(error);
8794
+ console.error(`\u26A0\uFE0F ${displayMessage}`);
8520
8795
  }
8521
- await aptos.waitForTransaction({
8522
- transactionHash: pendingRegisterBlobTransaction.hash
8523
- });
8524
- spinner.text = `\u{1F4E4} Uploading ${fileName} to Shelby RPC... (${formatProgressPercent()}%)`;
8525
- await shelbyClient.rpc.putBlob({
8526
- account: activeAccount.accountAddress,
8527
- blobName: entry.blobname,
8528
- blobData
8529
- });
8530
- hasUploadedBlob = true;
8531
- amountUploaded += blobData.length;
8796
+ process.exit(1);
8797
+ }
8798
+ const configPath = program.opts().configFile;
8799
+ let config;
8800
+ try {
8801
+ config = loadConfig(configPath);
8532
8802
  } catch (error) {
8533
- const { displayMessage } = handleError(error);
8534
- spinner.fail(displayMessage);
8803
+ console.error(`Error: ${error.message}`);
8535
8804
  process.exit(1);
8536
8805
  }
8537
- }
8538
- if (validatedOptions.outputCommitments) {
8539
- await fs5.writeFile(
8540
- validatedOptions.outputCommitments,
8541
- JSON.stringify(outputCommitments)
8806
+ const start = performance.now();
8807
+ const filelist = await createFilelist(validatedOptions);
8808
+ const timeToCreateFilelist = ((performance.now() - start) / 1e3).toFixed(
8809
+ 5
8542
8810
  );
8543
- }
8544
- if (hasUploadedBlob) {
8811
+ console.log("\n\u{1F680} Upload Summary");
8812
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
8813
+ if (filelist.length === 1) {
8814
+ console.log(`\u{1F4E6} File: ${filelist[0].filename}`);
8815
+ console.log(`\u{1F4C1} Blob Name: ${filelist[0].blobname}`);
8816
+ } else {
8817
+ console.log(`\u{1F4E6} Files: ${filelist.length} files`);
8818
+ }
8819
+ console.log("");
8820
+ console.log(
8821
+ `\u{1F9EE} Filelist created (${filelist.length} ${filelist.length === 1 ? "entry" : "entries"})`
8822
+ );
8823
+ console.log(`\u23F1\uFE0F Took: ${timeToCreateFilelist}s`);
8824
+ if (validatedOptions.assumeYes) {
8825
+ console.log("\u2699\uFE0F Flag: --assume-yes (auto-confirmed)");
8826
+ }
8827
+ if (!validatedOptions.assumeYes) {
8828
+ const shouldContinue = await new Promise((resolve3) => {
8829
+ const { unmount } = render4(
8830
+ /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
8831
+ /* @__PURE__ */ jsx13(Text7, { children: "Continue?" }),
8832
+ /* @__PURE__ */ jsx13(
8833
+ SelectInput4,
8834
+ {
8835
+ items: [
8836
+ { label: "Yes", value: true },
8837
+ { label: "No", value: false }
8838
+ ],
8839
+ onSelect: (item) => {
8840
+ unmount();
8841
+ resolve3(item.value);
8842
+ }
8843
+ }
8844
+ )
8845
+ ] })
8846
+ );
8847
+ });
8848
+ if (!shouldContinue) {
8849
+ console.log("Upload cancelled.");
8850
+ process.exit(0);
8851
+ }
8852
+ }
8853
+ const shelbyConfig = getCurrentShelbyConfig(config, {
8854
+ context: program.opts().context
8855
+ });
8856
+ const activeAccount = getCurrentAccount(
8857
+ config,
8858
+ program.opts().account
8859
+ ).account;
8860
+ const aptos = new Aptos5(new AptosConfig4(shelbyConfig.aptos));
8861
+ const shelbyClient = new ShelbyNodeClient(shelbyConfig);
8862
+ const handleSigint = () => {
8863
+ spinner.fail("Quitting due to ctrl-C / SIGINT...");
8864
+ process.exit(1);
8865
+ };
8866
+ process.removeAllListeners("SIGINT");
8867
+ process.on("SIGINT", handleSigint);
8868
+ const expireUsec = validatedOptions.expiration.getTime() * 1e3;
8869
+ const expirationDate = new Date(expireUsec / 1e3);
8870
+ const formattedExpiration = expirationDate.toLocaleDateString("en-US", {
8871
+ year: "numeric",
8872
+ month: "short",
8873
+ day: "numeric",
8874
+ hour: "numeric",
8875
+ minute: "2-digit",
8876
+ second: "2-digit",
8877
+ hour12: true
8878
+ });
8879
+ console.log("");
8880
+ console.log(`\u{1F552} Expires: ${formattedExpiration}`);
8881
+ const spinner = ora2({
8882
+ text: "Uploading...",
8883
+ discardStdin: false
8884
+ }).start();
8885
+ const startTime = performance.now();
8886
+ let totalSize = 0;
8887
+ let amountUploaded = 0;
8888
+ const outputCommitments = {};
8889
+ let firstTransactionHash;
8890
+ for (const entry of filelist) {
8891
+ totalSize += entry.sizeBytes;
8892
+ }
8893
+ const isSingleFile = filelist.length === 1;
8894
+ const formatProgressPercent = () => totalSize > 0 ? (100 * (amountUploaded / totalSize)).toFixed(2) : "0.00";
8895
+ let filesProcessed = 0;
8896
+ for (const entry of filelist) {
8897
+ const fileName = path5.basename(entry.filename);
8898
+ const fileProgress = isSingleFile ? `(${formatProgressPercent()}%)` : `(${filesProcessed}/${filelist.length} files, ${formatProgressPercent()}%)`;
8899
+ spinner.text = `\u{1F4D6} Reading ${fileName}... ${fileProgress}`;
8900
+ try {
8901
+ const blobData = await fs5.readFile(entry.filename);
8902
+ if (blobData.length !== entry.sizeBytes) {
8903
+ throw new Error(
8904
+ `Size of file ${entry.filename} changed after initial scan. Original size was ${entry.sizeBytes} but it is now ${blobData.length}`
8905
+ );
8906
+ }
8907
+ spinner.text = `\u{1F517} Checking if blob exists... ${fileProgress}`;
8908
+ const existingBlobMetadata = await shelbyClient.coordination.getBlobMetadata({
8909
+ account: activeAccount.accountAddress,
8910
+ name: entry.blobname
8911
+ });
8912
+ if (existingBlobMetadata) {
8913
+ spinner.fail(
8914
+ `Blob '${entry.blobname}' already exists. Please use a different name or delete the existing blob first.`
8915
+ );
8916
+ process.exit(1);
8917
+ }
8918
+ spinner.text = `\u{1F517} Generating commitments for ${fileName}... ${fileProgress}`;
8919
+ const provider = await getErasureCodingProvider();
8920
+ const blobCommitments = await generateCommitments(provider, blobData);
8921
+ if (validatedOptions.outputCommitments) {
8922
+ outputCommitments[entry.filename] = blobCommitments;
8923
+ }
8924
+ spinner.text = `\u{1F517} Registering ${fileName} on L1... ${fileProgress}`;
8925
+ const { transaction: pendingRegisterBlobTransaction } = await shelbyClient.coordination.registerBlob({
8926
+ account: activeAccount,
8927
+ blobName: entry.blobname,
8928
+ blobMerkleRoot: blobCommitments.blob_merkle_root,
8929
+ size: blobData.length,
8930
+ expirationMicros: validatedOptions.expiration.getTime() * 1e3
8931
+ });
8932
+ const registerTransactionHash = pendingRegisterBlobTransaction.hash;
8933
+ if (!firstTransactionHash) {
8934
+ firstTransactionHash = registerTransactionHash;
8935
+ }
8936
+ await aptos.waitForTransaction({
8937
+ transactionHash: pendingRegisterBlobTransaction.hash
8938
+ });
8939
+ spinner.text = `\u{1F4E4} Uploading ${fileName} to Shelby RPC... ${fileProgress}`;
8940
+ await shelbyClient.rpc.putBlob({
8941
+ account: activeAccount.accountAddress,
8942
+ blobName: entry.blobname,
8943
+ blobData
8944
+ });
8945
+ amountUploaded += blobData.length;
8946
+ filesProcessed++;
8947
+ } catch (error) {
8948
+ const { displayMessage } = handleError(error);
8949
+ spinner.fail(displayMessage);
8950
+ process.exit(1);
8951
+ }
8952
+ }
8953
+ if (validatedOptions.outputCommitments) {
8954
+ await fs5.writeFile(
8955
+ validatedOptions.outputCommitments,
8956
+ JSON.stringify(outputCommitments)
8957
+ );
8958
+ }
8545
8959
  const elapsedSec = ((performance.now() - startTime) / 1e3).toFixed(2);
8546
8960
  spinner.succeed(`Upload complete \u2014 took ${elapsedSec}s`);
8547
- } else {
8548
- spinner.succeed("Skipped upload, blob has already been uploaded.");
8549
- }
8550
- console.log("");
8551
- if (firstTransactionHash) {
8552
- const explorerUrl = getAptosTransactionExplorerUrl(
8961
+ console.log("");
8962
+ if (firstTransactionHash) {
8963
+ const explorerUrl = getAptosTransactionExplorerUrl(
8964
+ shelbyConfig.network,
8965
+ firstTransactionHash
8966
+ );
8967
+ console.log("\u{1F310} Aptos Explorer:");
8968
+ console.log(` ${explorerUrl}`);
8969
+ console.log("");
8970
+ }
8971
+ const shelbyExplorerUrl = getShelbyAccountExplorerUrl(
8553
8972
  shelbyConfig.network,
8554
- firstTransactionHash
8973
+ activeAccount.accountAddress.toString()
8555
8974
  );
8556
- console.log("\u{1F310} Aptos Explorer:");
8557
- console.log(` ${explorerUrl}`);
8975
+ console.log("\u{1F5C2}\uFE0F Shelby Explorer:");
8976
+ console.log(` ${shelbyExplorerUrl}`);
8977
+ console.log("");
8978
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
8979
+ console.log("\u2728 Done!");
8558
8980
  console.log("");
8559
8981
  }
8560
- const shelbyExplorerUrl = getShelbyAccountExplorerUrl(
8561
- shelbyConfig.network,
8562
- activeAccount.accountAddress.toString()
8563
- );
8564
- console.log("\u{1F5C2}\uFE0F Shelby Explorer:");
8565
- console.log(` ${shelbyExplorerUrl}`);
8566
- console.log("");
8567
- console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
8568
- console.log("\u2728 Done!");
8569
- console.log("");
8570
- });
8982
+ );
8571
8983
  }
8572
8984
 
8573
8985
  // src/cli.tsx