@shelby-protocol/sdk 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/browser/index.d.ts +9 -9
  2. package/dist/browser/index.mjs +1068 -589
  3. package/dist/core/clients/ShelbyBlobClient.d.ts +155 -40
  4. package/dist/core/clients/ShelbyBlobClient.mjs +314 -112
  5. package/dist/core/clients/ShelbyClient.d.ts +27 -4
  6. package/dist/core/clients/ShelbyClient.mjs +869 -551
  7. package/dist/core/clients/ShelbyClientConfig.d.ts +9 -1
  8. package/dist/core/clients/ShelbyMetadataClient.d.ts +38 -6
  9. package/dist/core/clients/ShelbyMetadataClient.mjs +95 -13
  10. package/dist/core/clients/ShelbyMicropaymentChannelClient.d.ts +42 -0
  11. package/dist/core/clients/ShelbyMicropaymentChannelClient.mjs +62 -7
  12. package/dist/core/clients/ShelbyPlacementGroupClient.mjs +10 -7
  13. package/dist/core/clients/ShelbyRPCClient.d.ts +55 -66
  14. package/dist/core/clients/ShelbyRPCClient.mjs +344 -360
  15. package/dist/core/clients/index.d.ts +3 -2
  16. package/dist/core/clients/index.mjs +933 -551
  17. package/dist/core/clients/utils.mjs +1 -1
  18. package/dist/core/commitments.d.ts +12 -1
  19. package/dist/core/commitments.mjs +7 -0
  20. package/dist/core/constants.d.ts +1 -1
  21. package/dist/core/constants.mjs +1 -1
  22. package/dist/core/erasure/constants.d.ts +16 -1
  23. package/dist/core/erasure/constants.mjs +15 -1
  24. package/dist/core/erasure/index.d.ts +1 -1
  25. package/dist/core/erasure/index.mjs +15 -1
  26. package/dist/core/errors.d.ts +20 -1
  27. package/dist/core/errors.mjs +29 -0
  28. package/dist/core/index.d.ts +9 -9
  29. package/dist/core/index.mjs +1068 -589
  30. package/dist/core/operations/generated/sdk.d.ts +798 -42
  31. package/dist/core/operations/generated/sdk.mjs +93 -9
  32. package/dist/core/operations/index.d.ts +1 -1
  33. package/dist/core/operations/index.mjs +94 -10
  34. package/dist/core/rpc-responses.d.ts +1 -57
  35. package/dist/core/rpc-responses.mjs +1 -21
  36. package/dist/core/sp/chunk_proof.d.ts +23 -0
  37. package/dist/core/sp/chunk_proof.mjs +113 -0
  38. package/dist/core/sp/index.d.ts +3 -0
  39. package/dist/core/sp/index.mjs +402 -0
  40. package/dist/core/sp/sp_write_client.d.ts +53 -0
  41. package/dist/core/sp/sp_write_client.mjs +302 -0
  42. package/dist/core/types/blobs.d.ts +24 -5
  43. package/dist/core/types/blobs.mjs +24 -0
  44. package/dist/core/types/index.d.ts +2 -2
  45. package/dist/core/types/index.mjs +46 -2
  46. package/dist/core/types/payments.mjs +1 -1
  47. package/dist/core/types/storage_providers.d.ts +32 -6
  48. package/dist/core/types/storage_providers.mjs +22 -0
  49. package/dist/gen/rpc_server_pb.d.ts +295 -0
  50. package/dist/gen/rpc_server_pb.mjs +28 -0
  51. package/dist/node/clients/ShelbyNodeClient.d.ts +1 -0
  52. package/dist/node/clients/ShelbyNodeClient.mjs +869 -551
  53. package/dist/node/clients/index.d.ts +1 -0
  54. package/dist/node/clients/index.mjs +867 -551
  55. package/dist/node/index.d.ts +9 -9
  56. package/dist/node/index.mjs +1068 -589
  57. package/dist/node/parallel/commitment_worker.mjs +36 -249
  58. package/dist/node/parallel/commitment_worker_pool.mjs +56 -3
  59. package/dist/node/parallel/coverage_flush.d.ts +16 -0
  60. package/dist/node/parallel/coverage_flush.mjs +43 -0
  61. package/dist/node/parallel/default_commitment_worker_pool.mjs +56 -3
  62. package/dist/node/parallel/index.d.ts +1 -0
  63. package/dist/node/parallel/index.mjs +72 -4
  64. package/dist/node/parallel/parallel_commitments.mjs +56 -3
  65. package/dist/node/parallel/worker_pool.mjs +56 -3
  66. package/package.json +10 -3
@@ -3,6 +3,65 @@ import {
3
3
  Hex as Hex3
4
4
  } from "@aptos-labs/ts-sdk";
5
5
 
6
+ // src/core/erasure/constants.ts
7
+ var ERASURE_CODE_PARAMS = {
8
+ ["ClayCode_16Total_10Data_13Helper" /* ClayCode_16Total_10Data_13Helper */]: {
9
+ // total chunks (data + parity)
10
+ erasure_n: 16,
11
+ // data chunks
12
+ erasure_k: 10,
13
+ // helper nodes
14
+ erasure_d: 13,
15
+ // enum index
16
+ enumIndex: 0
17
+ },
18
+ ["ClayCode_4Total_2Data_3Helper" /* ClayCode_4Total_2Data_3Helper */]: {
19
+ // total chunks (data + parity)
20
+ erasure_n: 4,
21
+ // data chunks
22
+ erasure_k: 2,
23
+ // helper nodes
24
+ erasure_d: 3,
25
+ // enum index
26
+ enumIndex: 1
27
+ }
28
+ };
29
+ function findErasureSchemeByErasureN(erasureN) {
30
+ return Object.entries(ERASURE_CODE_PARAMS).find(
31
+ ([, params]) => params.erasure_n === erasureN
32
+ );
33
+ }
34
+ var DEFAULT_ERASURE_N = ERASURE_CODE_PARAMS["ClayCode_16Total_10Data_13Helper" /* ClayCode_16Total_10Data_13Helper */].erasure_n;
35
+ var DEFAULT_ERASURE_K = ERASURE_CODE_PARAMS["ClayCode_16Total_10Data_13Helper" /* ClayCode_16Total_10Data_13Helper */].erasure_k;
36
+ var DEFAULT_ERASURE_D = ERASURE_CODE_PARAMS["ClayCode_16Total_10Data_13Helper" /* ClayCode_16Total_10Data_13Helper */].erasure_d;
37
+ var DEFAULT_ERASURE_M = DEFAULT_ERASURE_N - DEFAULT_ERASURE_K;
38
+
39
+ // src/core/chunk.ts
40
+ var CHUNK_SIZE_PARAMS = {
41
+ ["ChunkSet10MiB_Chunk1MiB" /* ChunkSet10MiB_Chunk1MiB */]: {
42
+ // 1MiB
43
+ chunkSizeBytes: 1 * 1024 * 1024,
44
+ // 10MiB
45
+ chunksetSizeBytes: 10 * 1024 * 1024
46
+ },
47
+ ["ChunkSet2MiB_Chunk1MiB" /* ChunkSet2MiB_Chunk1MiB */]: {
48
+ // 1MiB
49
+ chunkSizeBytes: 1 * 1024 * 1024,
50
+ // 2MiB
51
+ chunksetSizeBytes: 2 * 1024 * 1024
52
+ }
53
+ };
54
+ var DEFAULT_CHUNK_SIZE_BYTES = CHUNK_SIZE_PARAMS["ChunkSet10MiB_Chunk1MiB" /* ChunkSet10MiB_Chunk1MiB */].chunkSizeBytes;
55
+ var DEFAULT_CHUNKSET_SIZE_BYTES = CHUNK_SIZE_PARAMS["ChunkSet10MiB_Chunk1MiB" /* ChunkSet10MiB_Chunk1MiB */].chunksetSizeBytes;
56
+ var ERASURE_CODE_AND_CHUNK_MAPPING = {
57
+ ["ClayCode_16Total_10Data_13Helper" /* ClayCode_16Total_10Data_13Helper */]: {
58
+ ...CHUNK_SIZE_PARAMS.ChunkSet10MiB_Chunk1MiB
59
+ },
60
+ ["ClayCode_4Total_2Data_3Helper" /* ClayCode_4Total_2Data_3Helper */]: {
61
+ ...CHUNK_SIZE_PARAMS.ChunkSet2MiB_Chunk1MiB
62
+ }
63
+ };
64
+
6
65
  // src/core/layout.ts
7
66
  import { z } from "zod";
8
67
  var BlobNameSchema = z.string().min(1, "Blob name path parameter cannot be empty.").max(
@@ -22,7 +81,7 @@ import { AptosConfig } from "@aptos-labs/ts-sdk";
22
81
  // src/core/constants.ts
23
82
  import { Network } from "@aptos-labs/ts-sdk";
24
83
  var NetworkToShelbyRPCBaseUrl = {
25
- [Network.SHELBYNET]: "https://api.shelbynet.shelby.xyz/shelby",
84
+ [Network.SHELBYNET]: "https://shelby.shelbynet.shelby.xyz/shelby",
26
85
  [Network.NETNA]: void 0,
27
86
  [Network.DEVNET]: void 0,
28
87
  [Network.TESTNET]: "https://api.testnet.shelby.xyz/shelby",
@@ -88,18 +147,20 @@ import gql from "graphql-tag";
88
147
  var GetBlobsDocument = gql`
89
148
  query getBlobs($where: blobs_bool_exp, $orderBy: [blobs_order_by!], $limit: Int, $offset: Int) {
90
149
  blobs(where: $where, order_by: $orderBy, limit: $limit, offset: $offset) {
150
+ uid
151
+ object_name
91
152
  owner
92
153
  blob_commitment
93
- blob_name
94
154
  created_at
95
155
  expires_at
156
+ updated_at
96
157
  num_chunksets
97
- is_deleted
98
- is_written
99
- placement_group
100
158
  size
101
- updated_at
102
159
  slice_address
160
+ placement_group
161
+ is_persisted
162
+ is_committed
163
+ is_deleted
103
164
  }
104
165
  }
105
166
  `;
@@ -111,7 +172,8 @@ var GetBlobActivitiesDocument = gql`
111
172
  limit: $limit
112
173
  offset: $offset
113
174
  ) {
114
- blob_name
175
+ uid
176
+ object_name
115
177
  event_index
116
178
  event_type
117
179
  transaction_hash
@@ -259,22 +321,6 @@ var ChallengeResponseSchema = z2.object({
259
321
  challenge: z2.string(),
260
322
  expiresAt: z2.number()
261
323
  });
262
- var MultipartUploadStatusResponseSchema = z2.object({
263
- uploadId: z2.string(),
264
- completedParts: z2.array(z2.number()),
265
- partSize: z2.number(),
266
- nParts: z2.number(),
267
- uploadedBytes: z2.number()
268
- });
269
- var StartMultipartUploadResponseSchema = z2.object({
270
- uploadId: z2.string()
271
- });
272
- var UploadPartResponseSchema = z2.object({
273
- success: z2.literal(true)
274
- });
275
- var CompleteMultipartUploadResponseSchema = z2.object({
276
- success: z2.literal(true)
277
- });
278
324
  var RPCErrorResponseSchema = z2.object({
279
325
  error: z2.string()
280
326
  });
@@ -534,6 +580,19 @@ async function* readInChunks(input, chunkSize) {
534
580
  ];
535
581
  }
536
582
  }
583
+ async function concatHashes(parts) {
584
+ const chunks = parts.map((part) => Hex2.fromHexInput(part).toUint8Array());
585
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
586
+ const combined = new Uint8Array(totalLength);
587
+ let offset = 0;
588
+ for (const chunk of chunks) {
589
+ combined.set(chunk, offset);
590
+ offset += chunk.byteLength;
591
+ }
592
+ return Hex2.fromHexInput(
593
+ new Uint8Array(await crypto.subtle.digest("SHA-256", combined))
594
+ );
595
+ }
537
596
  function isReadableStream(value) {
538
597
  return typeof value === "object" && value !== null && "getReader" in value && typeof value.getReader === "function";
539
598
  }
@@ -559,6 +618,7 @@ var BLOB_OWNER_AUTH_SCHEME_HEADER = "X-Shelby-Auth-Scheme";
559
618
  var BLOB_OWNER_IDENTITY_HEADER = "X-Shelby-Identity";
560
619
  var BLOB_OWNER_DOMAIN_HEADER = "X-Shelby-Domain";
561
620
  var BLOB_OWNER_AUTH_FUNCTION_HEADER = "X-Shelby-Auth-Function";
621
+ var INCLUSION_PROOF_HEADER = "X-Shelby-Inclusion-Proof";
562
622
  function buildAuthHeaders(auth) {
563
623
  const signatureBase64 = btoa(
564
624
  Array.from(auth.signature, (byte) => String.fromCharCode(byte)).join("")
@@ -582,6 +642,58 @@ function buildAuthHeaders(auth) {
582
642
  function encodeURIComponentKeepSlashes(str) {
583
643
  return encodeURIComponent(str).replace(/%2F/g, "/");
584
644
  }
645
+ async function generateChunksetInclusionProof(chunksetRoots, chunksetIndex) {
646
+ if (chunksetRoots.length === 0) {
647
+ throw new Error("Cannot generate inclusion proof for empty chunkset roots");
648
+ }
649
+ if (chunksetIndex < 0 || chunksetIndex >= chunksetRoots.length) {
650
+ throw new Error(
651
+ `Chunkset index ${chunksetIndex} out of range [0, ${chunksetRoots.length})`
652
+ );
653
+ }
654
+ if (chunksetRoots.length === 1) {
655
+ return [];
656
+ }
657
+ const zeroHash = new Uint8Array(32);
658
+ const siblings = [];
659
+ let currentLeaves = chunksetRoots.map((h) => h.toUint8Array());
660
+ let currentIdx = chunksetIndex;
661
+ while (currentLeaves.length > 1) {
662
+ if (currentLeaves.length % 2 !== 0) {
663
+ currentLeaves.push(zeroHash);
664
+ }
665
+ const siblingIdx = currentIdx % 2 === 0 ? currentIdx + 1 : currentIdx - 1;
666
+ siblings.push(currentLeaves[siblingIdx]);
667
+ const nextLeaves = [];
668
+ for (let i = 0; i < currentLeaves.length; i += 2) {
669
+ const combined = await concatHashes([
670
+ currentLeaves[i],
671
+ currentLeaves[i + 1]
672
+ ]);
673
+ nextLeaves.push(combined.toUint8Array());
674
+ }
675
+ currentLeaves = nextLeaves;
676
+ currentIdx = Math.floor(currentIdx / 2);
677
+ }
678
+ return siblings;
679
+ }
680
+ function encodeInclusionProof(siblings) {
681
+ if (siblings.length === 0) {
682
+ return "NONE";
683
+ }
684
+ const totalBytes = siblings.length * 32;
685
+ const combined = new Uint8Array(totalBytes);
686
+ let offset = 0;
687
+ for (const sibling of siblings) {
688
+ combined.set(sibling, offset);
689
+ offset += 32;
690
+ }
691
+ const binaryString = Array.from(
692
+ combined,
693
+ (byte) => String.fromCharCode(byte)
694
+ ).join("");
695
+ return btoa(binaryString);
696
+ }
585
697
  function validateTotalBytes(totalBytes) {
586
698
  if (!Number.isInteger(totalBytes) || totalBytes < 0) {
587
699
  throw new Error("totalBytes must be a non-negative integer");
@@ -613,7 +725,7 @@ var ShelbyRPCClient = class {
613
725
  * @param config - The client configuration object.
614
726
  * @param config.network - The Shelby network to use.
615
727
  * @param options.signChallengeHandler - Optional override for challenge
616
- * signing. When set, `putBlobResumable` uses this instead of the built-in
728
+ * signing. When set, `putBlobChunksets` uses this instead of the built-in
617
729
  * `signChallenge`. Intended for kit-level overrides (e.g. Solana DAA).
618
730
  *
619
731
  * @example
@@ -664,43 +776,6 @@ var ShelbyRPCClient = class {
664
776
  }
665
777
  return ChallengeResponseSchema.parse(await response.json());
666
778
  }
667
- /**
668
- * Check if there's an existing multipart upload for a blob.
669
- * Returns the upload status including which parts have been uploaded.
670
- *
671
- * @param account - The account that owns the blob.
672
- * @param blobName - The name of the blob.
673
- * @returns The upload status, or undefined if no pending upload exists.
674
- *
675
- * @example
676
- * ```typescript
677
- * const status = await client.getMultipartUploadStatus(account, "myblob.txt");
678
- * if (status) {
679
- * console.log(`Resuming upload ${status.uploadId}, ${status.completedParts.length} parts done`);
680
- * }
681
- * ```
682
- */
683
- async getMultipartUploadStatus(account, blobName) {
684
- const url = new URL(buildRequestUrl("/v1/multipart-uploads", this.baseUrl));
685
- url.searchParams.set("account", account.toString());
686
- url.searchParams.set("blobName", blobName);
687
- const response = await fetch(url.toString(), {
688
- method: "GET",
689
- headers: {
690
- ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
691
- }
692
- });
693
- if (response.status === 404) {
694
- return void 0;
695
- }
696
- if (!response.ok) {
697
- const errorBody = await response.text().catch(() => "");
698
- throw new Error(
699
- `Failed to get multipart upload status: status ${response.status}, body: ${errorBody}`
700
- );
701
- }
702
- return MultipartUploadStatusResponseSchema.parse(await response.json());
703
- }
704
779
  /**
705
780
  * Sign a challenge using the given account and return auth credentials.
706
781
  *
@@ -717,269 +792,142 @@ var ShelbyRPCClient = class {
717
792
  publicKey: account.publicKey.toUint8Array()
718
793
  };
719
794
  }
720
- async #uploadPart(uploadId, partIdx, partData) {
721
- const nRetries = 5;
722
- let lastResponse;
723
- let lastError;
724
- let attempts = 0;
725
- const partUrl = buildRequestUrl(
726
- `/v1/multipart-uploads/${uploadId}/parts/${partIdx}`,
727
- this.baseUrl
728
- );
729
- for (let i = 0; i < nRetries; ++i) {
730
- attempts++;
731
- try {
732
- lastResponse = await fetch(partUrl, {
733
- method: "PUT",
734
- headers: {
735
- "Content-Type": "application/octet-stream",
736
- ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
737
- },
738
- body: partData
739
- });
740
- lastError = void 0;
741
- } catch (error) {
742
- lastError = error;
743
- if (i < nRetries - 1) {
744
- const delay = 2 ** i * 100;
745
- await sleep(delay);
746
- continue;
747
- }
748
- break;
749
- }
750
- if (lastResponse.ok) return;
751
- if (!isRetryableStatus(lastResponse.status)) {
752
- break;
753
- }
754
- if (i < nRetries - 1) {
755
- const delay = 2 ** i * 100;
756
- await sleep(delay);
757
- }
758
- }
759
- if (lastError !== void 0) {
760
- const errorCode = getErrorCode(lastError);
761
- const errorMessage = lastError instanceof Error ? lastError.message : String(lastError);
762
- throw new Error(
763
- `Failed to upload part ${partIdx} for multipart upload ${uploadId} after ${attempts} attempt${attempts === 1 ? "" : "s"}. The connection to the Shelby RPC endpoint was interrupted while sending data${errorCode ? ` (${errorCode})` : ""}. Endpoint: ${partUrl.toString()}, partBytes: ${partData.length}. Last error: ${errorMessage}`,
764
- { cause: lastError }
765
- );
766
- }
767
- const errorBody = await lastResponse?.text().catch(() => "");
768
- throw new Error(
769
- `Failed to upload part ${partIdx} for multipart upload ${uploadId} after ${attempts} attempt${attempts === 1 ? "" : "s"}. status: ${lastResponse?.status}, body: ${errorBody}`
770
- );
771
- }
772
- async #putBlobMultipart(account, blobName, blobData, totalBytes, partSize = 5 * 1024 * 1024, onProgress) {
773
- validateTotalBytes(totalBytes);
774
- const startResponse = await fetch(
775
- buildRequestUrl("/v1/multipart-uploads", this.baseUrl),
776
- {
777
- method: "POST",
778
- headers: {
779
- "Content-Type": "application/json",
780
- ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
781
- },
782
- body: JSON.stringify({
783
- rawAccount: account.toString(),
784
- rawBlobName: blobName,
785
- rawPartSize: partSize
786
- })
787
- }
788
- );
789
- if (!startResponse.ok) {
790
- let errorBodyText = "Could not read error body";
791
- try {
792
- errorBodyText = await startResponse.text();
793
- } catch (_e) {
794
- }
795
- throw new Error(
796
- `Failed to start multipart upload! status: ${startResponse.status}, body: ${errorBodyText}`
797
- );
798
- }
799
- const { uploadId } = StartMultipartUploadResponseSchema.parse(
800
- await startResponse.json()
801
- );
802
- const totalParts = Math.ceil(totalBytes / partSize);
803
- let uploadedBytes = 0;
804
- for await (const [partIdx, partData] of readInChunks(blobData, partSize)) {
805
- await this.#uploadPart(uploadId, partIdx, partData);
806
- uploadedBytes += partData.length;
807
- onProgress?.({
808
- phase: "uploading",
809
- partIdx,
810
- totalParts,
811
- partBytes: partData.length,
812
- uploadedBytes,
813
- totalBytes
814
- });
815
- }
816
- if (uploadedBytes !== totalBytes) {
817
- throw new Error(
818
- `Uploaded bytes (${uploadedBytes}) did not match declared totalBytes (${totalBytes})`
819
- );
820
- }
821
- const finalPartIdx = totalParts > 0 ? totalParts - 1 : 0;
822
- onProgress?.({
823
- phase: "finalizing",
824
- partIdx: finalPartIdx,
825
- totalParts,
826
- // no part uploaded in this phase
827
- partBytes: 0,
828
- uploadedBytes: totalBytes,
829
- totalBytes
830
- });
831
- const completeResponse = await fetch(
832
- buildRequestUrl(
833
- `/v1/multipart-uploads/${uploadId}/complete`,
834
- this.baseUrl
835
- ),
836
- {
837
- method: "POST",
838
- headers: {
839
- "Content-Type": "application/json",
840
- ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
841
- }
842
- }
843
- );
844
- if (!completeResponse.ok) {
845
- let errorBodyText = "Could not read error body";
846
- try {
847
- errorBodyText = await completeResponse.text();
848
- } catch (_e) {
849
- }
850
- throw new Error(
851
- `Failed to complete multipart upload! status: ${completeResponse.status}, body: ${errorBodyText}`
852
- );
853
- }
854
- }
855
795
  /**
856
- * Uploads blob data to the Shelby RPC node for storage by storage providers.
857
- * This method should be called after blob commitments have been registered on the blockchain.
858
- * Uses multipart upload for efficient handling of large files.
859
- *
860
- * @param params.account - The account that owns the blob.
861
- * @param params.blobName - The name/path of the blob (e.g. "folder/file.txt").
862
- * @param params.blobData - The raw blob data as a Uint8Array or ReadableStream.
863
- * @param params.totalBytes - Total byte length. Required for streams; optional for Uint8Array.
864
- *
865
- * @example
866
- * ```typescript
867
- * const blobData = new TextEncoder().encode("Hello, world!");
868
- *
869
- * await client.putBlob({
870
- * account: AccountAddress.from("0x1"),
871
- * blobName: "greetings/hello.txt",
872
- * blobData,
873
- * });
874
- * ```
875
- */
876
- async putBlob(params) {
877
- BlobNameSchema.parse(params.blobName);
878
- let totalBytes;
879
- if (params.blobData instanceof Uint8Array) {
880
- totalBytes = params.totalBytes ?? params.blobData.length;
881
- if (totalBytes !== params.blobData.length) {
882
- throw new Error(
883
- "totalBytes must match blobData.length when blobData is a Uint8Array"
884
- );
885
- }
886
- } else {
887
- if (params.totalBytes === void 0) {
888
- throw new Error(
889
- "totalBytes is required when blobData is a ReadableStream"
890
- );
891
- }
892
- totalBytes = params.totalBytes;
893
- }
894
- validateTotalBytes(totalBytes);
895
- await this.#putBlobMultipart(
896
- params.account,
897
- params.blobName,
898
- params.blobData,
899
- totalBytes,
900
- void 0,
901
- params.onProgress
902
- );
903
- }
904
- /**
905
- * Upload a part with authentication headers.
796
+ * Upload a single chunkset via the v2 chunkset API.
906
797
  */
907
- async #uploadPartWithAuth(uploadId, partIdx, partData, auth) {
908
- const nRetries = 5;
798
+ async #uploadChunkset(params) {
799
+ const maxNonBusyRetries = 5;
909
800
  let lastResponse;
910
801
  let lastError;
911
802
  let attempts = 0;
912
- const partUrl = buildRequestUrl(
913
- `/v1/multipart-uploads/${uploadId}/parts/${partIdx}`,
803
+ let consecutive429s = 0;
804
+ let nonBusyRetries = 0;
805
+ const chunksetUrl = buildRequestUrl(
806
+ `/v2/chunksets/${params.account}/${params.chunksetIndex}/${params.uid}`,
914
807
  this.baseUrl
915
808
  );
916
- const authHeaders = buildAuthHeaders(auth);
917
- for (let i = 0; i < nRetries; ++i) {
809
+ const authHeaders = buildAuthHeaders(params.auth);
810
+ while (true) {
811
+ if (params.signal?.aborted) {
812
+ throw new DOMException("Upload aborted", "AbortError");
813
+ }
918
814
  attempts++;
919
815
  try {
920
- lastResponse = await fetch(partUrl, {
816
+ lastResponse = await fetch(chunksetUrl, {
921
817
  method: "PUT",
922
818
  headers: {
923
819
  "Content-Type": "application/octet-stream",
924
820
  ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {},
925
- ...authHeaders
821
+ ...authHeaders,
822
+ [INCLUSION_PROOF_HEADER]: params.inclusionProof
926
823
  },
927
- body: partData
824
+ body: params.chunksetData,
825
+ signal: params.signal
928
826
  });
929
827
  lastError = void 0;
930
828
  } catch (error) {
829
+ if (error instanceof DOMException && error.name === "AbortError") {
830
+ throw error;
831
+ }
931
832
  lastError = error;
932
- if (i < nRetries - 1) {
933
- const delay = 2 ** i * 100;
934
- await sleep(delay);
833
+ consecutive429s = 0;
834
+ nonBusyRetries++;
835
+ if (nonBusyRetries < maxNonBusyRetries) {
836
+ const delay2 = 2 ** nonBusyRetries * 100;
837
+ await sleep(delay2);
935
838
  continue;
936
839
  }
937
840
  break;
938
841
  }
939
- if (lastResponse.ok) return;
842
+ if (lastResponse.ok) {
843
+ const json = await lastResponse.json();
844
+ const spAcks = json.spAcks?.map((ack) => ({
845
+ slot: ack.slot,
846
+ signature: Uint8Array.from(
847
+ atob(ack.signature),
848
+ (c) => c.charCodeAt(0)
849
+ )
850
+ }));
851
+ return {
852
+ success: json.success,
853
+ acksReceived: json.acksReceived,
854
+ spAcks
855
+ };
856
+ }
857
+ if (lastResponse.status === 429) {
858
+ consecutive429s++;
859
+ const baseDelay = 500;
860
+ const jitter = Math.random() * 200;
861
+ const delay2 = Math.min(
862
+ 3e4,
863
+ 2 ** consecutive429s * baseDelay + jitter
864
+ );
865
+ await sleep(delay2);
866
+ continue;
867
+ }
868
+ consecutive429s = 0;
940
869
  if (!isRetryableStatus(lastResponse.status)) {
941
870
  break;
942
871
  }
943
- if (i < nRetries - 1) {
944
- const delay = 2 ** i * 100;
945
- await sleep(delay);
872
+ nonBusyRetries++;
873
+ if (nonBusyRetries >= maxNonBusyRetries) {
874
+ break;
946
875
  }
876
+ const delay = 2 ** nonBusyRetries * 100;
877
+ await sleep(delay);
947
878
  }
948
879
  if (lastError !== void 0) {
949
880
  const errorCode = getErrorCode(lastError);
950
881
  const errorMessage = lastError instanceof Error ? lastError.message : String(lastError);
951
882
  throw new Error(
952
- `Failed to upload part ${partIdx} for multipart upload ${uploadId} after ${attempts} attempt${attempts === 1 ? "" : "s"}. ${errorCode ? `(${errorCode}) ` : ""}Last error: ${errorMessage}`,
883
+ `Failed to upload chunkset ${params.chunksetIndex} after ${attempts} attempt${attempts === 1 ? "" : "s"}. ${errorCode ? `(${errorCode}) ` : ""}Last error: ${errorMessage}`,
953
884
  { cause: lastError }
954
885
  );
955
886
  }
956
887
  const errorBody = await lastResponse?.text().catch(() => "");
957
888
  throw new Error(
958
- `Failed to upload part ${partIdx} for multipart upload ${uploadId} after ${attempts} attempt${attempts === 1 ? "" : "s"}. status: ${lastResponse?.status}, body: ${errorBody}`
889
+ `Failed to upload chunkset ${params.chunksetIndex} after ${attempts} attempt${attempts === 1 ? "" : "s"}. status: ${lastResponse?.status}, body: ${errorBody}`
959
890
  );
960
891
  }
961
892
  /**
962
- * Uploads blob data to the Shelby RPC node with authentication and resume support.
963
- * This method authenticates using challenge-response and can resume interrupted uploads.
893
+ * Uploads blob data to the Shelby RPC node using the v2 chunkset API.
894
+ * This method authenticates using challenge-response and uploads chunksets
895
+ * directly to storage providers via the RPC's worker pool.
896
+ *
897
+ * This method:
898
+ * - Sends raw chunkset data directly to the RPC for erasure encoding
899
+ * - Requires pre-computed blob commitments to generate inclusion proofs
900
+ * - Does not support resume (each chunkset is idempotent on the SP side)
964
901
  *
965
902
  * @param params.account - The Aptos Account (with signing capability) that owns the blob.
966
- * @param params.blobName - The name/path of the blob (e.g. "folder/file.txt").
903
+ * @param params.uid - The blob's on-chain UID (from `BlobRegisteredEvent` at registration).
967
904
  * @param params.blobData - The raw blob data as a Uint8Array or ReadableStream.
905
+ * @param params.commitments - Pre-computed blob commitments (from generateCommitments).
968
906
  * @param params.totalBytes - Total byte length. Required for streams; optional for Uint8Array.
907
+ * @param params.chunksetConcurrency - Number of chunksets to upload in parallel. Defaults to 4.
969
908
  * @param params.onProgress - Optional callback for upload progress.
909
+ * @param params.signal - Optional AbortSignal for cancellation. When aborted, in-flight
910
+ * HTTP requests are cancelled and an AbortError is thrown.
970
911
  *
971
912
  * @example
972
913
  * ```typescript
973
- * await client.putBlobResumable({
974
- * account: myAccount, // Aptos Account with signing capability
975
- * blobName: "documents/report.pdf",
914
+ * // First, generate commitments for the blob
915
+ * const commitments = await generateCommitments(provider, fileData);
916
+ *
917
+ * // Register the blob on chain, then read its UID from the register tx's
918
+ * // BlobRegisteredEvent (see ShelbyBlobClient.registeredBlobUids).
919
+ *
920
+ * // Upload using chunkset API
921
+ * await rpcClient.putBlobChunksets({
922
+ * account: myAccount,
923
+ * uid: blobUid,
976
924
  * blobData: fileData,
977
- * totalBytes: fileData.length,
925
+ * commitments,
926
+ * chunksetConcurrency: 8,
978
927
  * });
979
928
  * ```
980
929
  */
981
- async putBlobResumable(params) {
982
- BlobNameSchema.parse(params.blobName);
930
+ async putBlobChunksets(params) {
983
931
  let totalBytes;
984
932
  if (params.blobData instanceof Uint8Array) {
985
933
  totalBytes = params.totalBytes ?? params.blobData.length;
@@ -997,110 +945,146 @@ var ShelbyRPCClient = class {
997
945
  totalBytes = params.totalBytes;
998
946
  }
999
947
  validateTotalBytes(totalBytes);
1000
- const partSize = params.partSize ?? 5 * 1024 * 1024;
948
+ const chunksetConcurrency = params.chunksetConcurrency ?? 4;
949
+ const totalChunksets = params.commitments.chunkset_commitments.length;
950
+ const rawDataSizeFromCommitments = params.commitments.raw_data_size;
951
+ if (rawDataSizeFromCommitments !== totalBytes) {
952
+ throw new Error(
953
+ `Data size mismatch: commitments were generated for ${rawDataSizeFromCommitments} bytes, but totalBytes=${totalBytes}. This will cause inclusion proof verification to fail.`
954
+ );
955
+ }
956
+ const chunksetRoots = params.commitments.chunkset_commitments.map(
957
+ (c) => Hex3.fromHexString(c.chunkset_root)
958
+ );
1001
959
  const { challenge } = await this.getChallenge(
1002
960
  params.account.accountAddress
1003
961
  );
1004
962
  const signFn = this.#signChallengeOverride ?? this.signChallenge.bind(this);
1005
963
  const auth = signFn(params.account, challenge);
1006
- const authHeaders = buildAuthHeaders(auth);
1007
- const existingUpload = await this.getMultipartUploadStatus(
1008
- params.account.accountAddress,
1009
- params.blobName
1010
- );
1011
- let uploadId;
1012
- let completedPartsSet;
1013
- let totalParts;
1014
- if (existingUpload) {
1015
- uploadId = existingUpload.uploadId;
1016
- completedPartsSet = new Set(existingUpload.completedParts);
1017
- totalParts = existingUpload.nParts;
1018
- if (existingUpload.partSize !== partSize) {
1019
- throw new Error(
1020
- `Cannot resume upload: part size mismatch. Existing upload uses ${existingUpload.partSize} bytes, but ${partSize} was requested.`
1021
- );
1022
- }
1023
- } else {
1024
- const startResponse = await fetch(
1025
- buildRequestUrl("/v1/multipart-uploads", this.baseUrl),
1026
- {
1027
- method: "POST",
1028
- headers: {
1029
- "Content-Type": "application/json",
1030
- ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {},
1031
- ...authHeaders
1032
- },
1033
- body: JSON.stringify({
1034
- rawAccount: params.account.accountAddress.toString(),
1035
- rawBlobName: params.blobName,
1036
- rawPartSize: partSize
1037
- })
1038
- }
964
+ const firstChunkset = params.commitments.chunkset_commitments[0];
965
+ if (!firstChunkset) {
966
+ throw new Error("Commitments must have at least one chunkset");
967
+ }
968
+ const erasure_n = firstChunkset.chunk_commitments.length;
969
+ const matchingEntry = findErasureSchemeByErasureN(erasure_n);
970
+ if (!matchingEntry) {
971
+ throw new Error(
972
+ `Unknown erasure coding scheme with erasure_n=${erasure_n}`
1039
973
  );
1040
- if (!startResponse.ok) {
1041
- const errorBodyText = await startResponse.text().catch(() => "");
1042
- throw new Error(
1043
- `Failed to start multipart upload! status: ${startResponse.status}, body: ${errorBodyText}`
1044
- );
1045
- }
1046
- const parsed = StartMultipartUploadResponseSchema.parse(
1047
- await startResponse.json()
974
+ }
975
+ const [schemeKey, matchingParams] = matchingEntry;
976
+ const erasure_k = matchingParams.erasure_k;
977
+ const { chunkSizeBytes } = ERASURE_CODE_AND_CHUNK_MAPPING[schemeKey];
978
+ const chunksetSize = erasure_k * chunkSizeBytes;
979
+ const expectedChunksetCount = totalBytes === 0 ? 1 : Math.ceil(totalBytes / chunksetSize);
980
+ if (expectedChunksetCount !== totalChunksets) {
981
+ throw new Error(
982
+ `Chunkset count mismatch: expected ${expectedChunksetCount} chunksets for ${totalBytes} bytes with chunksetSize=${chunksetSize}, but commitments have ${totalChunksets} chunksets. This suggests the commitments were generated with a different encoding scheme.`
1048
983
  );
1049
- uploadId = parsed.uploadId;
1050
- completedPartsSet = /* @__PURE__ */ new Set();
1051
- totalParts = Math.ceil(totalBytes / partSize);
1052
984
  }
1053
985
  let uploadedBytes = 0;
1054
- let partIdx = 0;
1055
- for await (const [idx, partData] of readInChunks(
1056
- params.blobData,
1057
- partSize
1058
- )) {
1059
- partIdx = idx;
1060
- if (completedPartsSet.has(partIdx)) {
1061
- uploadedBytes += partData.length;
1062
- continue;
986
+ let bytesReadFromSource = 0;
987
+ const inFlight = /* @__PURE__ */ new Set();
988
+ async function* chunksetsFromBlob() {
989
+ if (totalBytes === 0) {
990
+ yield [0, new Uint8Array(chunksetSize)];
991
+ return;
1063
992
  }
1064
- await this.#uploadPartWithAuth(uploadId, partIdx, partData, auth);
1065
- uploadedBytes += partData.length;
1066
- params.onProgress?.({
1067
- phase: "uploading",
1068
- partIdx,
1069
- totalParts,
1070
- partBytes: partData.length,
1071
- uploadedBytes,
1072
- totalBytes
993
+ yield* readInChunks(params.blobData, chunksetSize);
994
+ }
995
+ const chunksetIterator = chunksetsFromBlob();
996
+ const aggregatedAcks = /* @__PURE__ */ new Map();
997
+ const internalAbort = new AbortController();
998
+ if (params.signal?.aborted) {
999
+ internalAbort.abort();
1000
+ } else if (params.signal) {
1001
+ params.signal.addEventListener("abort", () => internalAbort.abort(), {
1002
+ once: true
1073
1003
  });
1074
1004
  }
1075
- const finalPartIdx = totalParts > 0 ? totalParts - 1 : 0;
1076
- params.onProgress?.({
1077
- phase: "finalizing",
1078
- partIdx: finalPartIdx,
1079
- totalParts,
1080
- partBytes: 0,
1081
- uploadedBytes: totalBytes,
1082
- totalBytes
1083
- });
1084
- const completeResponse = await fetch(
1085
- buildRequestUrl(
1086
- `/v1/multipart-uploads/${uploadId}/complete`,
1087
- this.baseUrl
1088
- ),
1089
- {
1090
- method: "POST",
1091
- headers: {
1092
- "Content-Type": "application/json",
1093
- ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {},
1094
- ...authHeaders
1005
+ let iteratorDone = false;
1006
+ let firstError = null;
1007
+ while (!iteratorDone || inFlight.size > 0) {
1008
+ if (internalAbort.signal.aborted) {
1009
+ throw firstError ?? new DOMException("Upload aborted", "AbortError");
1010
+ }
1011
+ while (inFlight.size < chunksetConcurrency && !iteratorDone) {
1012
+ const { value, done } = await chunksetIterator.next();
1013
+ if (done) {
1014
+ iteratorDone = true;
1015
+ break;
1095
1016
  }
1017
+ const [chunksetIdx, chunksetData] = value;
1018
+ bytesReadFromSource += totalBytes === 0 ? 0 : chunksetData.byteLength;
1019
+ const expectedCommitment = params.commitments.chunkset_commitments[chunksetIdx];
1020
+ if (!expectedCommitment) {
1021
+ throw new Error(
1022
+ `Chunkset index ${chunksetIdx} out of range. Commitments only have ${totalChunksets} chunksets. This suggests a chunkset size mismatch between commitment generation and upload.`
1023
+ );
1024
+ }
1025
+ const proofSiblings = await generateChunksetInclusionProof(
1026
+ chunksetRoots,
1027
+ chunksetIdx
1028
+ );
1029
+ const inclusionProof = encodeInclusionProof(proofSiblings);
1030
+ const chunksetDataCopy = new Uint8Array(chunksetData);
1031
+ const chunksetBytes = chunksetDataCopy.length;
1032
+ const uploadPromise = (async () => {
1033
+ const result = await this.#uploadChunkset({
1034
+ account: params.account.accountAddress.toString(),
1035
+ uid: params.uid,
1036
+ chunksetIndex: chunksetIdx,
1037
+ inclusionProof,
1038
+ chunksetData: chunksetDataCopy,
1039
+ auth,
1040
+ signal: internalAbort.signal
1041
+ });
1042
+ const chunksetSpAcks = result.spAcks?.map((ack) => ({
1043
+ slot: ack.slot,
1044
+ signature: ack.signature
1045
+ }));
1046
+ if (chunksetSpAcks) {
1047
+ for (const ack of chunksetSpAcks) {
1048
+ aggregatedAcks.set(ack.slot, ack.signature);
1049
+ }
1050
+ }
1051
+ uploadedBytes += chunksetBytes;
1052
+ params.onProgress?.({
1053
+ phase: "uploading",
1054
+ chunksetIdx,
1055
+ totalChunksets,
1056
+ chunksetBytes,
1057
+ uploadedBytes,
1058
+ totalBytes,
1059
+ acksReceived: result.acksReceived,
1060
+ spAcks: chunksetSpAcks
1061
+ });
1062
+ })().catch((err) => {
1063
+ if (!firstError) {
1064
+ firstError = err;
1065
+ internalAbort.abort();
1066
+ }
1067
+ }).finally(() => {
1068
+ inFlight.delete(uploadPromise);
1069
+ });
1070
+ inFlight.add(uploadPromise);
1096
1071
  }
1097
- );
1098
- if (!completeResponse.ok) {
1099
- const errorBodyText = await completeResponse.text().catch(() => "");
1072
+ if (inFlight.size > 0) {
1073
+ await Promise.race(inFlight);
1074
+ }
1075
+ }
1076
+ if (firstError) {
1077
+ throw firstError;
1078
+ }
1079
+ if (bytesReadFromSource !== totalBytes) {
1100
1080
  throw new Error(
1101
- `Failed to complete multipart upload! status: ${completeResponse.status}, body: ${errorBodyText}`
1081
+ `Data source produced ${bytesReadFromSource} bytes, but expected ${totalBytes} bytes. The stream may have ended prematurely, resulting in an incomplete upload.`
1102
1082
  );
1103
1083
  }
1084
+ const spAcks = Array.from(
1085
+ aggregatedAcks.entries()
1086
+ ).map(([slot, signature]) => ({ slot, signature }));
1087
+ return { spAcks };
1104
1088
  }
1105
1089
  /**
1106
1090
  * Downloads a blob from the Shelby RPC node.