@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.
- package/dist/browser/index.d.ts +9 -9
- package/dist/browser/index.mjs +1068 -589
- package/dist/core/clients/ShelbyBlobClient.d.ts +155 -40
- package/dist/core/clients/ShelbyBlobClient.mjs +314 -112
- package/dist/core/clients/ShelbyClient.d.ts +27 -4
- package/dist/core/clients/ShelbyClient.mjs +869 -551
- package/dist/core/clients/ShelbyClientConfig.d.ts +9 -1
- package/dist/core/clients/ShelbyMetadataClient.d.ts +38 -6
- package/dist/core/clients/ShelbyMetadataClient.mjs +95 -13
- package/dist/core/clients/ShelbyMicropaymentChannelClient.d.ts +42 -0
- package/dist/core/clients/ShelbyMicropaymentChannelClient.mjs +62 -7
- package/dist/core/clients/ShelbyPlacementGroupClient.mjs +10 -7
- package/dist/core/clients/ShelbyRPCClient.d.ts +55 -66
- package/dist/core/clients/ShelbyRPCClient.mjs +344 -360
- package/dist/core/clients/index.d.ts +3 -2
- package/dist/core/clients/index.mjs +933 -551
- package/dist/core/clients/utils.mjs +1 -1
- package/dist/core/commitments.d.ts +12 -1
- package/dist/core/commitments.mjs +7 -0
- package/dist/core/constants.d.ts +1 -1
- package/dist/core/constants.mjs +1 -1
- package/dist/core/erasure/constants.d.ts +16 -1
- package/dist/core/erasure/constants.mjs +15 -1
- package/dist/core/erasure/index.d.ts +1 -1
- package/dist/core/erasure/index.mjs +15 -1
- package/dist/core/errors.d.ts +20 -1
- package/dist/core/errors.mjs +29 -0
- package/dist/core/index.d.ts +9 -9
- package/dist/core/index.mjs +1068 -589
- package/dist/core/operations/generated/sdk.d.ts +798 -42
- package/dist/core/operations/generated/sdk.mjs +93 -9
- package/dist/core/operations/index.d.ts +1 -1
- package/dist/core/operations/index.mjs +94 -10
- package/dist/core/rpc-responses.d.ts +1 -57
- package/dist/core/rpc-responses.mjs +1 -21
- package/dist/core/sp/chunk_proof.d.ts +23 -0
- package/dist/core/sp/chunk_proof.mjs +113 -0
- package/dist/core/sp/index.d.ts +3 -0
- package/dist/core/sp/index.mjs +402 -0
- package/dist/core/sp/sp_write_client.d.ts +53 -0
- package/dist/core/sp/sp_write_client.mjs +302 -0
- package/dist/core/types/blobs.d.ts +24 -5
- package/dist/core/types/blobs.mjs +24 -0
- package/dist/core/types/index.d.ts +2 -2
- package/dist/core/types/index.mjs +46 -2
- package/dist/core/types/payments.mjs +1 -1
- package/dist/core/types/storage_providers.d.ts +32 -6
- package/dist/core/types/storage_providers.mjs +22 -0
- package/dist/gen/rpc_server_pb.d.ts +295 -0
- package/dist/gen/rpc_server_pb.mjs +28 -0
- package/dist/node/clients/ShelbyNodeClient.d.ts +1 -0
- package/dist/node/clients/ShelbyNodeClient.mjs +869 -551
- package/dist/node/clients/index.d.ts +1 -0
- package/dist/node/clients/index.mjs +867 -551
- package/dist/node/index.d.ts +9 -9
- package/dist/node/index.mjs +1068 -589
- package/dist/node/parallel/commitment_worker.mjs +36 -249
- package/dist/node/parallel/commitment_worker_pool.mjs +56 -3
- package/dist/node/parallel/coverage_flush.d.ts +16 -0
- package/dist/node/parallel/coverage_flush.mjs +43 -0
- package/dist/node/parallel/default_commitment_worker_pool.mjs +56 -3
- package/dist/node/parallel/index.d.ts +1 -0
- package/dist/node/parallel/index.mjs +72 -4
- package/dist/node/parallel/parallel_commitments.mjs +56 -3
- package/dist/node/parallel/worker_pool.mjs +56 -3
- 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://
|
|
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
|
-
|
|
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, `
|
|
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
|
-
*
|
|
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 #
|
|
908
|
-
const
|
|
798
|
+
async #uploadChunkset(params) {
|
|
799
|
+
const maxNonBusyRetries = 5;
|
|
909
800
|
let lastResponse;
|
|
910
801
|
let lastError;
|
|
911
802
|
let attempts = 0;
|
|
912
|
-
|
|
913
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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)
|
|
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
|
-
|
|
944
|
-
|
|
945
|
-
|
|
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
|
|
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
|
|
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
|
|
963
|
-
* This method authenticates using challenge-response and
|
|
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.
|
|
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
|
-
*
|
|
974
|
-
*
|
|
975
|
-
*
|
|
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
|
-
*
|
|
925
|
+
* commitments,
|
|
926
|
+
* chunksetConcurrency: 8,
|
|
978
927
|
* });
|
|
979
928
|
* ```
|
|
980
929
|
*/
|
|
981
|
-
async
|
|
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
|
|
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
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
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
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
-
|
|
1099
|
-
|
|
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
|
-
`
|
|
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.
|