@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
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
// src/core/constants.ts
|
|
9
9
|
import { Network } from "@aptos-labs/ts-sdk";
|
|
10
10
|
var NetworkToShelbyRPCBaseUrl = {
|
|
11
|
-
[Network.SHELBYNET]: "https://
|
|
11
|
+
[Network.SHELBYNET]: "https://shelby.shelbynet.shelby.xyz/shelby",
|
|
12
12
|
[Network.NETNA]: void 0,
|
|
13
13
|
[Network.DEVNET]: void 0,
|
|
14
14
|
[Network.TESTNET]: "https://api.testnet.shelby.xyz/shelby",
|
|
@@ -37,6 +37,18 @@ var NetworkToGasStationBaseUrl = {
|
|
|
37
37
|
var SHELBY_DEPLOYER = "0x85fdb9a176ab8ef1d9d9c1b60d60b3924f0800ac1de1cc2085fb0b8bb4988e6a";
|
|
38
38
|
var MICROPAYMENTS_DEPLOYER = "0x1ae7275148bf6ef742b658fd9cbcc2e094201606f4a7bc707bab0201da8043ee";
|
|
39
39
|
|
|
40
|
+
// src/core/types/storage_providers.ts
|
|
41
|
+
var ACTIVE_PROVIDER_CONDITIONS = [
|
|
42
|
+
"Normal",
|
|
43
|
+
"Faulty",
|
|
44
|
+
"Leaving",
|
|
45
|
+
"Evicted",
|
|
46
|
+
"PendingLeaving"
|
|
47
|
+
];
|
|
48
|
+
function isActiveProviderCondition(value) {
|
|
49
|
+
return typeof value === "string" && ACTIVE_PROVIDER_CONDITIONS.includes(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
// src/core/utils.ts
|
|
41
53
|
import {
|
|
42
54
|
AccountAddress,
|
|
@@ -168,6 +180,15 @@ var getShelbyRPCBaseUrl = (config) => {
|
|
|
168
180
|
};
|
|
169
181
|
|
|
170
182
|
// src/core/clients/ShelbyMetadataClient.ts
|
|
183
|
+
function parseActiveProviderCondition(raw) {
|
|
184
|
+
const variant = raw?.__variant__;
|
|
185
|
+
if (!isActiveProviderCondition(variant)) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`Unknown ActiveProviderCondition variant: ${JSON.stringify(raw)}`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
return variant;
|
|
191
|
+
}
|
|
171
192
|
function parseStorageProviderState(raw) {
|
|
172
193
|
switch (raw.__variant__) {
|
|
173
194
|
case "Active":
|
|
@@ -175,8 +196,7 @@ function parseStorageProviderState(raw) {
|
|
|
175
196
|
variant: "Active",
|
|
176
197
|
quota: raw.quota.value,
|
|
177
198
|
stakeAtStartOfStakingEpoch: raw.stake_at_start_of_staking_epoch,
|
|
178
|
-
|
|
179
|
-
leaving: raw.leaving
|
|
199
|
+
condition: parseActiveProviderCondition(raw.condition)
|
|
180
200
|
};
|
|
181
201
|
case "Waitlisted":
|
|
182
202
|
return {
|
|
@@ -189,6 +209,10 @@ function parseStorageProviderState(raw) {
|
|
|
189
209
|
frozenFrom: raw.frozen_from,
|
|
190
210
|
frozenTill: raw.frozen_till
|
|
191
211
|
};
|
|
212
|
+
default:
|
|
213
|
+
throw new Error(
|
|
214
|
+
`Unknown StorageProviderStateDetails variant: ${JSON.stringify(raw)}`
|
|
215
|
+
);
|
|
192
216
|
}
|
|
193
217
|
}
|
|
194
218
|
var ShelbyMetadataClient = class {
|
|
@@ -247,28 +271,52 @@ var ShelbyMetadataClient = class {
|
|
|
247
271
|
}
|
|
248
272
|
}
|
|
249
273
|
/**
|
|
250
|
-
* Retrieves the
|
|
274
|
+
* Retrieves the names of every activated location (region). Locations that are
|
|
275
|
+
* registered but not yet brought online are admin-internal and not listed.
|
|
276
|
+
*
|
|
277
|
+
* @returns The location name list.
|
|
251
278
|
*
|
|
279
|
+
* @example
|
|
280
|
+
* ```typescript
|
|
281
|
+
* const locations = await client.getLocationNames();
|
|
282
|
+
* ```
|
|
283
|
+
*/
|
|
284
|
+
async getLocationNames() {
|
|
285
|
+
const names = await this.aptos.view({
|
|
286
|
+
payload: {
|
|
287
|
+
function: `${this.deployer.toString()}::location::activated_location_names`,
|
|
288
|
+
functionArguments: []
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
return names[0];
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Retrieves the list of placement group addresses in a location.
|
|
295
|
+
*
|
|
296
|
+
* @param locationName - The location whose placement groups to list.
|
|
252
297
|
* @returns The placement group address list, or an empty array if none exist.
|
|
253
298
|
*
|
|
254
299
|
* @example
|
|
255
300
|
* ```typescript
|
|
256
|
-
* const pgList = await client.getPlacementGroupAddresses();
|
|
301
|
+
* const pgList = await client.getPlacementGroupAddresses("us-east-1");
|
|
257
302
|
* ```
|
|
258
303
|
*/
|
|
259
|
-
async getPlacementGroupAddresses() {
|
|
304
|
+
async getPlacementGroupAddresses(locationName) {
|
|
260
305
|
try {
|
|
261
306
|
const pgSizeMetadata = await this.aptos.view({
|
|
262
307
|
payload: {
|
|
263
308
|
function: `${this.deployer.toString()}::placement_group_registry::get_number_of_placement_groups`,
|
|
264
|
-
functionArguments: []
|
|
309
|
+
functionArguments: [locationName]
|
|
265
310
|
}
|
|
266
311
|
});
|
|
312
|
+
if (Number(pgSizeMetadata[0]) === 0) {
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
267
315
|
const finalPlacementGroupIndex = pgSizeMetadata[0] - 1;
|
|
268
316
|
const addressMetadataArray = await this.aptos.view({
|
|
269
317
|
payload: {
|
|
270
318
|
function: `${this.deployer.toString()}::placement_group_registry::get_placement_group_addresses`,
|
|
271
|
-
functionArguments: [0, finalPlacementGroupIndex]
|
|
319
|
+
functionArguments: [locationName, 0, finalPlacementGroupIndex]
|
|
272
320
|
}
|
|
273
321
|
});
|
|
274
322
|
const metadata = addressMetadataArray[0];
|
|
@@ -282,28 +330,32 @@ var ShelbyMetadataClient = class {
|
|
|
282
330
|
}
|
|
283
331
|
}
|
|
284
332
|
/**
|
|
285
|
-
* Retrieves the list of slice addresses.
|
|
333
|
+
* Retrieves the list of slice addresses in a location.
|
|
286
334
|
*
|
|
335
|
+
* @param locationName - The location whose slices to list.
|
|
287
336
|
* @returns The slice group list, or an empty array if none exist.
|
|
288
337
|
*
|
|
289
338
|
* @example
|
|
290
339
|
* ```typescript
|
|
291
|
-
* const pgList = await client.getSliceAddresses();
|
|
340
|
+
* const pgList = await client.getSliceAddresses("us-east-1");
|
|
292
341
|
* ```
|
|
293
342
|
*/
|
|
294
|
-
async getSliceAddresses() {
|
|
343
|
+
async getSliceAddresses(locationName) {
|
|
295
344
|
try {
|
|
296
345
|
const sliceSizeMetadata = await this.aptos.view({
|
|
297
346
|
payload: {
|
|
298
347
|
function: `${this.deployer.toString()}::slice_registry::get_number_of_slices`,
|
|
299
|
-
functionArguments: []
|
|
348
|
+
functionArguments: [locationName]
|
|
300
349
|
}
|
|
301
350
|
});
|
|
351
|
+
if (Number(sliceSizeMetadata[0]) === 0) {
|
|
352
|
+
return [];
|
|
353
|
+
}
|
|
302
354
|
const finalSliceIndex = sliceSizeMetadata[0] - 1;
|
|
303
355
|
const addressMetadataArray = await this.aptos.view({
|
|
304
356
|
payload: {
|
|
305
357
|
function: `${this.deployer.toString()}::slice_registry::get_slice_addresses`,
|
|
306
|
-
functionArguments: [0, finalSliceIndex]
|
|
358
|
+
functionArguments: [locationName, 0, finalSliceIndex]
|
|
307
359
|
}
|
|
308
360
|
});
|
|
309
361
|
const metadata = addressMetadataArray[0];
|
|
@@ -363,6 +415,36 @@ var ShelbyMetadataClient = class {
|
|
|
363
415
|
(opt) => opt.vec.length > 0 ? normalizeAddress(opt.vec[0]) : null
|
|
364
416
|
);
|
|
365
417
|
}
|
|
418
|
+
/**
|
|
419
|
+
* Retrieves the active storage providers for a slice.
|
|
420
|
+
*
|
|
421
|
+
* Active SPs have a complete copy of the data for the slot, and are not in any data transfer/repair/reconstruction phase.
|
|
422
|
+
* Active SP can be audited for the data it contains.
|
|
423
|
+
* Each slot has at most one active SP.
|
|
424
|
+
*
|
|
425
|
+
* @param params.account - The address of the slice account.
|
|
426
|
+
* @returns An array where result[i] is the active SP for slot i, or null if no SP is active.
|
|
427
|
+
*
|
|
428
|
+
* @example
|
|
429
|
+
* ```typescript
|
|
430
|
+
* const providers = await client.getActiveStorageProvidersForSlice({ account: sliceAddress });
|
|
431
|
+
* ```
|
|
432
|
+
*/
|
|
433
|
+
async getActiveStorageProvidersForSlice(params) {
|
|
434
|
+
const placementGroupAddress = await this.getPlacementGroupAddressForSlice(
|
|
435
|
+
params.account
|
|
436
|
+
);
|
|
437
|
+
const rawMetadata = await this.aptos.view({
|
|
438
|
+
payload: {
|
|
439
|
+
function: `${this.deployer.toString()}::placement_group::get_active_storage_providers`,
|
|
440
|
+
functionArguments: [placementGroupAddress]
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
const providers = rawMetadata[0];
|
|
444
|
+
return providers.map(
|
|
445
|
+
(opt) => opt.vec.length > 0 ? normalizeAddress(opt.vec[0]) : null
|
|
446
|
+
);
|
|
447
|
+
}
|
|
366
448
|
/**
|
|
367
449
|
* Retrieves the serving storage providers for a slice.
|
|
368
450
|
*
|
|
@@ -438,6 +520,18 @@ var ERASURE_CODE_PARAMS = {
|
|
|
438
520
|
enumIndex: 1
|
|
439
521
|
}
|
|
440
522
|
};
|
|
523
|
+
function findErasureSchemeByErasureN(erasureN) {
|
|
524
|
+
return Object.entries(ERASURE_CODE_PARAMS).find(
|
|
525
|
+
([, params]) => params.erasure_n === erasureN
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
function requiredAckCount(erasureN) {
|
|
529
|
+
const scheme = findErasureSchemeByErasureN(erasureN);
|
|
530
|
+
if (!scheme) {
|
|
531
|
+
throw new Error(`Unknown erasure coding scheme with erasure_n=${erasureN}`);
|
|
532
|
+
}
|
|
533
|
+
return scheme[1].erasure_d;
|
|
534
|
+
}
|
|
441
535
|
var DEFAULT_ERASURE_N = ERASURE_CODE_PARAMS["ClayCode_16Total_10Data_13Helper" /* ClayCode_16Total_10Data_13Helper */].erasure_n;
|
|
442
536
|
var DEFAULT_ERASURE_K = ERASURE_CODE_PARAMS["ClayCode_16Total_10Data_13Helper" /* ClayCode_16Total_10Data_13Helper */].erasure_k;
|
|
443
537
|
var DEFAULT_ERASURE_D = ERASURE_CODE_PARAMS["ClayCode_16Total_10Data_13Helper" /* ClayCode_16Total_10Data_13Helper */].erasure_d;
|
|
@@ -734,6 +828,35 @@ function erasureCodingConfig(encodingScheme) {
|
|
|
734
828
|
import { createWasmReedSolomonBinding } from "@shelby-protocol/reed-solomon";
|
|
735
829
|
var DEFAULT_CHUNK_SIZE_BYTES2 = 2 * 1024 * 1024;
|
|
736
830
|
|
|
831
|
+
// src/core/errors.ts
|
|
832
|
+
var ShelbyLocationErrorCodes = {
|
|
833
|
+
E_LOCATION_NOT_FOUND: "E_LOCATION_NOT_FOUND",
|
|
834
|
+
E_SELECTED_LOCATION_CONFLICTS_WITH_LOCK: "E_SELECTED_LOCATION_CONFLICTS_WITH_LOCK",
|
|
835
|
+
E_NO_LOCATION_SELECTED: "E_NO_LOCATION_SELECTED",
|
|
836
|
+
E_LOCATION_WRITES_FROZEN: "E_LOCATION_WRITES_FROZEN",
|
|
837
|
+
E_LOCATION_NOT_ACTIVATED: "E_LOCATION_NOT_ACTIVATED"
|
|
838
|
+
};
|
|
839
|
+
function describeLocationError(errorMessage) {
|
|
840
|
+
if (errorMessage.includes(ShelbyLocationErrorCodes.E_LOCATION_WRITES_FROZEN)) {
|
|
841
|
+
return "Cannot write: the resolved location is frozen and not currently accepting new data.";
|
|
842
|
+
}
|
|
843
|
+
if (errorMessage.includes(ShelbyLocationErrorCodes.E_LOCATION_NOT_ACTIVATED)) {
|
|
844
|
+
return "Cannot write: the resolved location is not activated yet.";
|
|
845
|
+
}
|
|
846
|
+
if (errorMessage.includes(
|
|
847
|
+
ShelbyLocationErrorCodes.E_SELECTED_LOCATION_CONFLICTS_WITH_LOCK
|
|
848
|
+
)) {
|
|
849
|
+
return "Cannot write: the selected location conflicts with the account's locked location preference.";
|
|
850
|
+
}
|
|
851
|
+
if (errorMessage.includes(ShelbyLocationErrorCodes.E_NO_LOCATION_SELECTED)) {
|
|
852
|
+
return "No write location could be resolved: none was selected and the account has no default location or location hint.";
|
|
853
|
+
}
|
|
854
|
+
if (errorMessage.includes(ShelbyLocationErrorCodes.E_LOCATION_NOT_FOUND)) {
|
|
855
|
+
return "Cannot write: the requested location does not exist.";
|
|
856
|
+
}
|
|
857
|
+
return void 0;
|
|
858
|
+
}
|
|
859
|
+
|
|
737
860
|
// src/core/clients/ShelbyBlobClient.ts
|
|
738
861
|
import {
|
|
739
862
|
AccountAddress as AccountAddress3,
|
|
@@ -742,6 +865,27 @@ import {
|
|
|
742
865
|
U32
|
|
743
866
|
} from "@aptos-labs/ts-sdk";
|
|
744
867
|
|
|
868
|
+
// src/core/types/blobs.ts
|
|
869
|
+
var BLOB_ENCRYPTION_TO_MOVE_ENUM_INDEX = {
|
|
870
|
+
Unencrypted: 0,
|
|
871
|
+
AES_GCM_V1: 1
|
|
872
|
+
};
|
|
873
|
+
function blobEncryptionToMoveEnumIndex(encryption) {
|
|
874
|
+
return BLOB_ENCRYPTION_TO_MOVE_ENUM_INDEX[encryption];
|
|
875
|
+
}
|
|
876
|
+
function blobEncryptionFromMoveVariant(variant) {
|
|
877
|
+
switch (variant) {
|
|
878
|
+
case "Unencrypted":
|
|
879
|
+
return "Unencrypted";
|
|
880
|
+
case "AES_GCM_V1":
|
|
881
|
+
return "AES_GCM_V1";
|
|
882
|
+
default:
|
|
883
|
+
throw new Error(
|
|
884
|
+
"Could not parse encryption from Shelby Smart Contract, this SDK is out of date."
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
745
889
|
// src/core/operations/index.ts
|
|
746
890
|
import { Network as Network2 } from "@aptos-labs/ts-sdk";
|
|
747
891
|
import { GraphQLClient } from "graphql-request";
|
|
@@ -751,18 +895,20 @@ import gql from "graphql-tag";
|
|
|
751
895
|
var GetBlobsDocument = gql`
|
|
752
896
|
query getBlobs($where: blobs_bool_exp, $orderBy: [blobs_order_by!], $limit: Int, $offset: Int) {
|
|
753
897
|
blobs(where: $where, order_by: $orderBy, limit: $limit, offset: $offset) {
|
|
898
|
+
uid
|
|
899
|
+
object_name
|
|
754
900
|
owner
|
|
755
901
|
blob_commitment
|
|
756
|
-
blob_name
|
|
757
902
|
created_at
|
|
758
903
|
expires_at
|
|
904
|
+
updated_at
|
|
759
905
|
num_chunksets
|
|
760
|
-
is_deleted
|
|
761
|
-
is_written
|
|
762
|
-
placement_group
|
|
763
906
|
size
|
|
764
|
-
updated_at
|
|
765
907
|
slice_address
|
|
908
|
+
placement_group
|
|
909
|
+
is_persisted
|
|
910
|
+
is_committed
|
|
911
|
+
is_deleted
|
|
766
912
|
}
|
|
767
913
|
}
|
|
768
914
|
`;
|
|
@@ -774,7 +920,8 @@ var GetBlobActivitiesDocument = gql`
|
|
|
774
920
|
limit: $limit
|
|
775
921
|
offset: $offset
|
|
776
922
|
) {
|
|
777
|
-
|
|
923
|
+
uid
|
|
924
|
+
object_name
|
|
778
925
|
event_index
|
|
779
926
|
event_type
|
|
780
927
|
transaction_hash
|
|
@@ -920,6 +1067,22 @@ var MissingTransactionSubmitterError = class extends Error {
|
|
|
920
1067
|
this.name = "MissingTransactionSubmitterError";
|
|
921
1068
|
}
|
|
922
1069
|
};
|
|
1070
|
+
var COMMIT_REJECTION_REASONS = /* @__PURE__ */ new Set([
|
|
1071
|
+
"AlreadyExists",
|
|
1072
|
+
"NoPriorVersion",
|
|
1073
|
+
"EtagMismatch"
|
|
1074
|
+
]);
|
|
1075
|
+
function encryptionFunctionArgs(params) {
|
|
1076
|
+
if (params.omitEncryptionArg) {
|
|
1077
|
+
if (params.encryption && params.encryption !== "Unencrypted") {
|
|
1078
|
+
throw new Error(
|
|
1079
|
+
`Blob encryption (${params.encryption}) is not supported on this network: the deployed contract predates the encryption upgrade (#1739).`
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
return [];
|
|
1083
|
+
}
|
|
1084
|
+
return [blobEncryptionToMoveEnumIndex(params.encryption ?? "Unencrypted")];
|
|
1085
|
+
}
|
|
923
1086
|
var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
924
1087
|
aptos;
|
|
925
1088
|
deployer;
|
|
@@ -968,7 +1131,10 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
968
1131
|
this.aptos = new Aptos2(getAptosConfig(config));
|
|
969
1132
|
this.deployer = config.deployer ?? AccountAddress3.fromString(SHELBY_DEPLOYER);
|
|
970
1133
|
this.indexer = getShelbyIndexerClient(config);
|
|
971
|
-
this.defaultOptions =
|
|
1134
|
+
this.defaultOptions = {
|
|
1135
|
+
locationHint: config.locationHint,
|
|
1136
|
+
...defaultOptions
|
|
1137
|
+
};
|
|
972
1138
|
this.orderless = config.orderless ?? false;
|
|
973
1139
|
}
|
|
974
1140
|
/**
|
|
@@ -979,7 +1145,9 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
979
1145
|
build: options?.build ?? this.defaultOptions.build,
|
|
980
1146
|
submit: options?.submit ?? this.defaultOptions.submit,
|
|
981
1147
|
usdSponsor: options?.usdSponsor ?? this.defaultOptions.usdSponsor,
|
|
982
|
-
chunksetSizeBytes: options?.chunksetSizeBytes ?? this.defaultOptions.chunksetSizeBytes
|
|
1148
|
+
chunksetSizeBytes: options?.chunksetSizeBytes ?? this.defaultOptions.chunksetSizeBytes,
|
|
1149
|
+
selectedLocation: options?.selectedLocation ?? this.defaultOptions.selectedLocation,
|
|
1150
|
+
locationHint: options?.locationHint ?? this.defaultOptions.locationHint
|
|
983
1151
|
};
|
|
984
1152
|
}
|
|
985
1153
|
/**
|
|
@@ -1021,17 +1189,17 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1021
1189
|
*
|
|
1022
1190
|
* @example
|
|
1023
1191
|
* ```typescript
|
|
1024
|
-
* const metadata = await client.
|
|
1192
|
+
* const metadata = await client.getFullObjectMetadata({
|
|
1025
1193
|
* account: AccountAddress.fromString("0x1"),
|
|
1026
1194
|
* name: "foo/bar.txt",
|
|
1027
1195
|
* });
|
|
1028
1196
|
* ```
|
|
1029
1197
|
*/
|
|
1030
|
-
async
|
|
1198
|
+
async getFullObjectMetadata(params) {
|
|
1031
1199
|
try {
|
|
1032
1200
|
const rawMetadata = await this.aptos.view({
|
|
1033
1201
|
payload: {
|
|
1034
|
-
function: `${this.deployer.toString()}::blob_metadata::
|
|
1202
|
+
function: `${this.deployer.toString()}::blob_metadata::get_full_object_metadata`,
|
|
1035
1203
|
functionArguments: [
|
|
1036
1204
|
createBlobKey({
|
|
1037
1205
|
account: params.account,
|
|
@@ -1043,47 +1211,94 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1043
1211
|
if (!rawMetadata?.[0]?.vec?.[0]) {
|
|
1044
1212
|
return void 0;
|
|
1045
1213
|
}
|
|
1046
|
-
const
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
encoding = {
|
|
1050
|
-
variant: "clay",
|
|
1051
|
-
...ERASURE_CODE_PARAMS[metadata.encoding.__variant__],
|
|
1052
|
-
...ERASURE_CODE_AND_CHUNK_MAPPING[metadata.encoding.__variant__]
|
|
1053
|
-
};
|
|
1054
|
-
} else if (metadata.encoding.__variant__ === "ClayCode_4Total_2Data_3Helper") {
|
|
1055
|
-
encoding = {
|
|
1056
|
-
variant: "clay",
|
|
1057
|
-
...ERASURE_CODE_PARAMS[metadata.encoding.__variant__],
|
|
1058
|
-
...ERASURE_CODE_AND_CHUNK_MAPPING[metadata.encoding.__variant__]
|
|
1059
|
-
};
|
|
1060
|
-
} else {
|
|
1061
|
-
throw new Error(
|
|
1062
|
-
"Could not parse encoding from Shelby Smart Contract, this SDK is out of date."
|
|
1063
|
-
);
|
|
1064
|
-
}
|
|
1065
|
-
return {
|
|
1066
|
-
blobMerkleRoot: Hex4.fromHexInput(
|
|
1067
|
-
metadata.blob_commitment
|
|
1068
|
-
).toUint8Array(),
|
|
1069
|
-
owner: normalizeAddress(metadata.owner),
|
|
1214
|
+
const view = rawMetadata[0].vec[0];
|
|
1215
|
+
return this.parseBlobMetadata(view.blob_metadata, {
|
|
1216
|
+
uid: BigInt(view.object_metadata.current_blob_uid),
|
|
1070
1217
|
name: params.name,
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
creationMicros: Number(metadata.creation_micros),
|
|
1076
|
-
sliceAddress: normalizeAddress(metadata.slice.inner),
|
|
1077
|
-
isWritten: metadata.is_written
|
|
1078
|
-
};
|
|
1218
|
+
// Any blob bound under an object name has cleared the commit-time
|
|
1219
|
+
// `is_written()` check, so a resolved object is always written.
|
|
1220
|
+
isWritten: true
|
|
1221
|
+
});
|
|
1079
1222
|
} catch (error) {
|
|
1080
1223
|
if (error instanceof Error && // Depending on the network, the error message may show up differently.
|
|
1081
|
-
(error.message?.includes("sub_status: Some(404)") || error.message?.includes("EBLOB_NOT_FOUND"))) {
|
|
1224
|
+
(error.message?.includes("sub_status: Some(404)") || error.message?.includes("EBLOB_NOT_FOUND") || error.message?.includes("EOBJECT_NOT_FOUND"))) {
|
|
1225
|
+
return void 0;
|
|
1226
|
+
}
|
|
1227
|
+
throw error;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Retrieves blob metadata directly by its on-chain UID, including blobs in
|
|
1232
|
+
* the pending (registered-but-not-yet-committed) state that
|
|
1233
|
+
* {@link getFullObjectMetadata} cannot resolve by object name. Returns `undefined`
|
|
1234
|
+
* if no blob has that UID.
|
|
1235
|
+
*
|
|
1236
|
+
* The returned `name`/`blobNameSuffix` are empty: the blob layer is keyed by
|
|
1237
|
+
* UID and carries no object name (a name binding is established only at
|
|
1238
|
+
* commit). `isWritten` reflects whether the blob has been committed.
|
|
1239
|
+
*/
|
|
1240
|
+
async getFullObjectMetadataByUid(uid) {
|
|
1241
|
+
try {
|
|
1242
|
+
const rawMetadata = await this.aptos.view(
|
|
1243
|
+
{
|
|
1244
|
+
payload: {
|
|
1245
|
+
function: `${this.deployer.toString()}::blob_metadata::get_blob_metadata`,
|
|
1246
|
+
functionArguments: [uid]
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
);
|
|
1250
|
+
if (!rawMetadata?.[0]?.vec?.[0]) {
|
|
1251
|
+
return void 0;
|
|
1252
|
+
}
|
|
1253
|
+
const raw = rawMetadata[0].vec[0];
|
|
1254
|
+
return this.parseBlobMetadata(raw, {
|
|
1255
|
+
uid,
|
|
1256
|
+
name: "",
|
|
1257
|
+
isWritten: raw.state.__variant__ === "CommittedObject"
|
|
1258
|
+
});
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
if (error instanceof Error && (error.message?.includes("sub_status: Some(404)") || error.message?.includes("EBLOB_NOT_FOUND"))) {
|
|
1082
1261
|
return void 0;
|
|
1083
1262
|
}
|
|
1084
1263
|
throw error;
|
|
1085
1264
|
}
|
|
1086
1265
|
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Parse the on-chain `BlobMetadata::V1` view shape into the SDK
|
|
1268
|
+
* {@link FullObjectMetadata}. `uid` / `name` / `isWritten` are supplied by the
|
|
1269
|
+
* caller since they depend on the lookup path (by object name vs by UID).
|
|
1270
|
+
*/
|
|
1271
|
+
parseBlobMetadata(raw, extra) {
|
|
1272
|
+
const { content } = raw;
|
|
1273
|
+
const variant = content.encoding.__variant__;
|
|
1274
|
+
if (variant !== "ClayCode_16Total_10Data_13Helper" && variant !== "ClayCode_4Total_2Data_3Helper") {
|
|
1275
|
+
throw new Error(
|
|
1276
|
+
"Could not parse encoding from Shelby Smart Contract, this SDK is out of date."
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
const encoding = {
|
|
1280
|
+
variant: "clay",
|
|
1281
|
+
...ERASURE_CODE_PARAMS[variant],
|
|
1282
|
+
...ERASURE_CODE_AND_CHUNK_MAPPING[variant]
|
|
1283
|
+
};
|
|
1284
|
+
const encryption = blobEncryptionFromMoveVariant(
|
|
1285
|
+
content.encryption?.__variant__ ?? "Unencrypted"
|
|
1286
|
+
);
|
|
1287
|
+
return {
|
|
1288
|
+
uid: extra.uid,
|
|
1289
|
+
blobMerkleRoot: Hex4.fromHexInput(content.blob_commitment).toUint8Array(),
|
|
1290
|
+
owner: normalizeAddress(raw.owner),
|
|
1291
|
+
name: extra.name,
|
|
1292
|
+
blobNameSuffix: extra.name ? getBlobNameSuffix(extra.name) : "",
|
|
1293
|
+
size: Number(content.blob_size),
|
|
1294
|
+
encoding,
|
|
1295
|
+
encryption,
|
|
1296
|
+
expirationMicros: Number(raw.expiration_micros),
|
|
1297
|
+
creationMicros: Number(raw.creation_micros),
|
|
1298
|
+
sliceAddress: normalizeAddress(raw.slice.inner),
|
|
1299
|
+
isWritten: extra.isWritten
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1087
1302
|
/**
|
|
1088
1303
|
* Retrieves all the blobs and their metadata for an account from the
|
|
1089
1304
|
* blockchain.
|
|
@@ -1095,7 +1310,7 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1095
1310
|
*
|
|
1096
1311
|
* @example
|
|
1097
1312
|
* ```typescript
|
|
1098
|
-
* //
|
|
1313
|
+
* // FullObjectMetadata[]
|
|
1099
1314
|
* const blobs = await client.getAccountBlobs({
|
|
1100
1315
|
* account: AccountAddress.fromString("0x1"),
|
|
1101
1316
|
* });
|
|
@@ -1112,6 +1327,25 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1112
1327
|
orderBy: rest.orderBy
|
|
1113
1328
|
});
|
|
1114
1329
|
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Object-facing default filter: only committed, non-deleted, unexpired rows.
|
|
1332
|
+
* The blobs table is UID-keyed, so during an atomic overwrite a single
|
|
1333
|
+
* object_name transiently has two non-deleted rows — the currently committed
|
|
1334
|
+
* blob and the new pending (is_committed = "0") blob. Filtering on
|
|
1335
|
+
* is_committed keeps name lookups/listings pinned to the committed object and
|
|
1336
|
+
* avoids returning or duplicating the in-flight pending row. Applied to
|
|
1337
|
+
* getBlobs *and* the getBlobsCount / getTotalBlobsSize aggregates so counts
|
|
1338
|
+
* and sizes agree with the listing mid-overwrite. Callers can override any
|
|
1339
|
+
* key (e.g. is_committed) via their own `where`.
|
|
1340
|
+
*/
|
|
1341
|
+
activeBlobsWhere(where) {
|
|
1342
|
+
const defaultActiveFilter = {
|
|
1343
|
+
expires_at: { _gte: String(Date.now() * 1e3) },
|
|
1344
|
+
is_deleted: { _eq: "0" },
|
|
1345
|
+
is_committed: { _eq: "1" }
|
|
1346
|
+
};
|
|
1347
|
+
return { ...defaultActiveFilter, ...where };
|
|
1348
|
+
}
|
|
1115
1349
|
/**
|
|
1116
1350
|
* Retrieves blobs and their metadata from the blockchain.
|
|
1117
1351
|
*
|
|
@@ -1122,7 +1356,7 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1122
1356
|
*
|
|
1123
1357
|
* @example
|
|
1124
1358
|
* ```typescript
|
|
1125
|
-
* //
|
|
1359
|
+
* // FullObjectMetadata[]
|
|
1126
1360
|
* const blobs = await client.getBlobs({
|
|
1127
1361
|
* where: { owner: { _eq: AccountAddress.fromString("0x1").toString() } },
|
|
1128
1362
|
* });
|
|
@@ -1131,12 +1365,7 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1131
1365
|
async getBlobs(params = {}) {
|
|
1132
1366
|
const { limit, offset } = params.pagination ?? {};
|
|
1133
1367
|
const { orderBy, where } = params;
|
|
1134
|
-
const
|
|
1135
|
-
const defaultActiveFilter = {
|
|
1136
|
-
expires_at: { _gte: currentMicros },
|
|
1137
|
-
is_deleted: { _eq: "0" }
|
|
1138
|
-
};
|
|
1139
|
-
const finalWhere = where !== void 0 ? { ...defaultActiveFilter, ...where } : defaultActiveFilter;
|
|
1368
|
+
const finalWhere = this.activeBlobsWhere(where);
|
|
1140
1369
|
const { blobs } = await this.indexer.getBlobs({
|
|
1141
1370
|
where: finalWhere,
|
|
1142
1371
|
limit,
|
|
@@ -1145,9 +1374,10 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1145
1374
|
});
|
|
1146
1375
|
return blobs.map(
|
|
1147
1376
|
(blob) => ({
|
|
1377
|
+
uid: BigInt(blob.uid),
|
|
1148
1378
|
owner: normalizeAddress(blob.owner),
|
|
1149
|
-
name: blob.
|
|
1150
|
-
blobNameSuffix: getBlobNameSuffix(blob.
|
|
1379
|
+
name: blob.object_name,
|
|
1380
|
+
blobNameSuffix: getBlobNameSuffix(blob.object_name),
|
|
1151
1381
|
blobMerkleRoot: Hex4.fromHexInput(blob.blob_commitment).toUint8Array(),
|
|
1152
1382
|
size: Number(blob.size),
|
|
1153
1383
|
// TODO: Add encoding when supported in NCI
|
|
@@ -1159,7 +1389,7 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1159
1389
|
expirationMicros: Number(blob.expires_at),
|
|
1160
1390
|
creationMicros: Number(blob.created_at),
|
|
1161
1391
|
sliceAddress: normalizeAddress(blob.slice_address),
|
|
1162
|
-
isWritten: Boolean(Number(blob.
|
|
1392
|
+
isWritten: Boolean(Number(blob.is_persisted)),
|
|
1163
1393
|
isDeleted: Boolean(Number(blob.is_deleted))
|
|
1164
1394
|
})
|
|
1165
1395
|
);
|
|
@@ -1177,12 +1407,15 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1177
1407
|
[`${this.deployer.toStringLong()}::blob_metadata::BlobRegisteredEvent`]: "register_blob",
|
|
1178
1408
|
[`${this.deployer.toStringLong()}::blob_metadata::BlobDeletedEvent`]: "delete_blob",
|
|
1179
1409
|
[`${this.deployer.toStringLong()}::blob_metadata::BlobExpirationExtendedEvent`]: "extend_blob_expiration",
|
|
1180
|
-
[`${this.deployer.toStringLong()}::blob_metadata::
|
|
1410
|
+
[`${this.deployer.toStringLong()}::blob_metadata::BlobPersistedEvent`]: "write_blob",
|
|
1411
|
+
[`${this.deployer.toStringLong()}::blob_metadata::ObjectCommittedEvent`]: "commit_object",
|
|
1412
|
+
[`${this.deployer.toStringLong()}::blob_metadata::ObjectDeletedEvent`]: "delete_object",
|
|
1413
|
+
[`${this.deployer.toStringLong()}::blob_metadata::ObjectCommitRejectedEvent`]: "reject_object_commit"
|
|
1181
1414
|
};
|
|
1182
1415
|
return blob_activities.map(
|
|
1183
1416
|
(activity) => ({
|
|
1184
|
-
blobName: activity.
|
|
1185
|
-
accountAddress: normalizeAddress(activity.
|
|
1417
|
+
blobName: activity.object_name,
|
|
1418
|
+
accountAddress: normalizeAddress(activity.object_name.substring(1, 65)),
|
|
1186
1419
|
type: activityTypeMapping[activity.event_type] ?? "unknown",
|
|
1187
1420
|
eventType: activity.event_type,
|
|
1188
1421
|
eventIndex: Number(activity.event_index),
|
|
@@ -1205,9 +1438,10 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1205
1438
|
* const count = await client.getBlobsCount();
|
|
1206
1439
|
* ```
|
|
1207
1440
|
*/
|
|
1208
|
-
async getBlobsCount(params) {
|
|
1209
|
-
const {
|
|
1210
|
-
|
|
1441
|
+
async getBlobsCount(params = {}) {
|
|
1442
|
+
const { blobs_aggregate } = await this.indexer.getBlobsCount({
|
|
1443
|
+
where: this.activeBlobsWhere(params.where)
|
|
1444
|
+
});
|
|
1211
1445
|
return blobs_aggregate?.aggregate?.count ?? 0;
|
|
1212
1446
|
}
|
|
1213
1447
|
/**
|
|
@@ -1222,8 +1456,9 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1222
1456
|
* ```
|
|
1223
1457
|
*/
|
|
1224
1458
|
async getTotalBlobsSize(params = {}) {
|
|
1225
|
-
const {
|
|
1226
|
-
|
|
1459
|
+
const { blobs_aggregate } = await this.indexer.getTotalBlobsSize({
|
|
1460
|
+
where: this.activeBlobsWhere(params.where)
|
|
1461
|
+
});
|
|
1227
1462
|
return Number(blobs_aggregate?.aggregate?.sum?.size ?? 0);
|
|
1228
1463
|
}
|
|
1229
1464
|
/**
|
|
@@ -1282,12 +1517,15 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1282
1517
|
deployer: this.deployer,
|
|
1283
1518
|
account: params.account.accountAddress,
|
|
1284
1519
|
blobName: params.blobName,
|
|
1520
|
+
selectedLocation: options.selectedLocation,
|
|
1521
|
+
locationHint: options.locationHint,
|
|
1285
1522
|
blobSize: params.size,
|
|
1286
1523
|
blobMerkleRoot: params.blobMerkleRoot,
|
|
1287
1524
|
numChunksets: expectedTotalChunksets(params.size, chunksetSize),
|
|
1288
1525
|
expirationMicros: params.expirationMicros,
|
|
1289
1526
|
useSponsoredUsdVariant: options.usdSponsor !== void 0,
|
|
1290
|
-
encoding: config.enumIndex
|
|
1527
|
+
encoding: config.enumIndex,
|
|
1528
|
+
encryption: params.encryption
|
|
1291
1529
|
}),
|
|
1292
1530
|
sender: params.account.accountAddress
|
|
1293
1531
|
};
|
|
@@ -1315,16 +1553,16 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1315
1553
|
* @example
|
|
1316
1554
|
* ```typescript
|
|
1317
1555
|
*
|
|
1318
|
-
* const { transaction } = await client.
|
|
1556
|
+
* const { transaction } = await client.deleteObject({
|
|
1319
1557
|
* account: signer,
|
|
1320
1558
|
* blobName: "foo/bar.txt",
|
|
1321
1559
|
* });
|
|
1322
1560
|
* ```
|
|
1323
1561
|
*/
|
|
1324
|
-
async
|
|
1562
|
+
async deleteObject(params) {
|
|
1325
1563
|
const transaction = await this.aptos.transaction.build.simple({
|
|
1326
1564
|
options: this.orderlessTxOptions(params.options),
|
|
1327
|
-
data: _ShelbyBlobClient.
|
|
1565
|
+
data: _ShelbyBlobClient.createDeleteObjectPayload({
|
|
1328
1566
|
deployer: this.deployer,
|
|
1329
1567
|
blobName: params.blobName
|
|
1330
1568
|
}),
|
|
@@ -1340,10 +1578,6 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1340
1578
|
/**
|
|
1341
1579
|
* Deletes multiple blobs on the blockchain in a single atomic transaction.
|
|
1342
1580
|
*
|
|
1343
|
-
* **Note:** This function requires the `delete_multiple_blobs` entry function
|
|
1344
|
-
* which will be deployed to the smart contract on 2026-02-04. Using this
|
|
1345
|
-
* function before that date will result in a transaction failure.
|
|
1346
|
-
*
|
|
1347
1581
|
* This operation is atomic: if any blob deletion fails (e.g., blob not found),
|
|
1348
1582
|
* the entire transaction fails and no blobs are deleted.
|
|
1349
1583
|
*
|
|
@@ -1358,16 +1592,16 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1358
1592
|
* @example
|
|
1359
1593
|
* ```typescript
|
|
1360
1594
|
*
|
|
1361
|
-
* const { transaction } = await client.
|
|
1595
|
+
* const { transaction } = await client.deleteMultipleObjects({
|
|
1362
1596
|
* account: signer,
|
|
1363
1597
|
* blobNames: ["foo/bar.txt", "baz.txt"],
|
|
1364
1598
|
* });
|
|
1365
1599
|
* ```
|
|
1366
1600
|
*/
|
|
1367
|
-
async
|
|
1601
|
+
async deleteMultipleObjects(params) {
|
|
1368
1602
|
const transaction = await this.aptos.transaction.build.simple({
|
|
1369
1603
|
options: this.orderlessTxOptions(params.options),
|
|
1370
|
-
data: _ShelbyBlobClient.
|
|
1604
|
+
data: _ShelbyBlobClient.createDeleteMultipleObjectsPayload({
|
|
1371
1605
|
deployer: this.deployer,
|
|
1372
1606
|
blobNames: params.blobNames
|
|
1373
1607
|
}),
|
|
@@ -1425,6 +1659,8 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1425
1659
|
data: _ShelbyBlobClient.createBatchRegisterBlobsPayload({
|
|
1426
1660
|
deployer: this.deployer,
|
|
1427
1661
|
account: params.account.accountAddress,
|
|
1662
|
+
selectedLocation: options.selectedLocation,
|
|
1663
|
+
locationHint: options.locationHint,
|
|
1428
1664
|
expirationMicros: params.expirationMicros,
|
|
1429
1665
|
blobs: params.blobs.map((blob) => ({
|
|
1430
1666
|
blobName: blob.blobName,
|
|
@@ -1433,7 +1669,8 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1433
1669
|
numChunksets: expectedTotalChunksets(blob.blobSize, chunksetSize)
|
|
1434
1670
|
})),
|
|
1435
1671
|
useSponsoredUsdVariant: options.usdSponsor !== void 0,
|
|
1436
|
-
encoding: config.enumIndex
|
|
1672
|
+
encoding: config.enumIndex,
|
|
1673
|
+
encryption: params.encryption
|
|
1437
1674
|
})
|
|
1438
1675
|
};
|
|
1439
1676
|
const transaction = options.usdSponsor ? await this.aptos.transaction.build.multiAgent({
|
|
@@ -1448,6 +1685,55 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1448
1685
|
})
|
|
1449
1686
|
};
|
|
1450
1687
|
}
|
|
1688
|
+
/**
|
|
1689
|
+
* Extracts the on-chain UIDs assigned at registration from a committed
|
|
1690
|
+
* register transaction's events.
|
|
1691
|
+
*
|
|
1692
|
+
* `register_blob` / `register_multiple_blobs` create *pending* blobs and emit
|
|
1693
|
+
* one `BlobRegisteredEvent` per blob. The UID is published only on this event
|
|
1694
|
+
* (the blob is not yet in the `objects` map, so it cannot be read back by
|
|
1695
|
+
* name), so callers must parse it here before uploading bytes or committing.
|
|
1696
|
+
*
|
|
1697
|
+
* @param events - The committed transaction's events (from `waitForTransaction`).
|
|
1698
|
+
* @param deployer - The contract deployer address.
|
|
1699
|
+
* @returns One entry per registered blob, keyed by its full object name
|
|
1700
|
+
* (`@<owner>/<suffix>`, matching {@link createBlobKey}).
|
|
1701
|
+
*/
|
|
1702
|
+
static registeredBlobUids(events, deployer) {
|
|
1703
|
+
const eventType = `${deployer.toStringLong()}::blob_metadata::BlobRegisteredEvent`;
|
|
1704
|
+
return events.filter((event) => event.type === eventType).map((event) => {
|
|
1705
|
+
const data = event.data;
|
|
1706
|
+
return { objectName: data.object_name, uid: BigInt(data.uid) };
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Detects whether `commit_object` rejected the write for `uid` rather than
|
|
1711
|
+
* applying it. A rejected commit is still a *successful* transaction — the
|
|
1712
|
+
* contract tears down the pending blob and emits `ObjectCommitRejectedEvent`
|
|
1713
|
+
* instead of aborting — so callers must inspect the finalized transaction's
|
|
1714
|
+
* events to tell a durable write apart from a silent no-op.
|
|
1715
|
+
*
|
|
1716
|
+
* @param events - The committed transaction's events (from `waitForTransaction`).
|
|
1717
|
+
* @param deployer - The contract deployer address.
|
|
1718
|
+
* @param uid - The UID passed to `commit_object`.
|
|
1719
|
+
* @returns The rejection reason, or `undefined` if the commit was applied.
|
|
1720
|
+
*/
|
|
1721
|
+
static findObjectCommitRejection(events, deployer, uid) {
|
|
1722
|
+
const eventType = `${deployer.toStringLong()}::blob_metadata::ObjectCommitRejectedEvent`;
|
|
1723
|
+
const rejection = events.find(
|
|
1724
|
+
(event) => event.type === eventType && BigInt(event.data.uid) === uid
|
|
1725
|
+
);
|
|
1726
|
+
if (!rejection) {
|
|
1727
|
+
return void 0;
|
|
1728
|
+
}
|
|
1729
|
+
const reason = rejection.data.rejection_reason.__variant__;
|
|
1730
|
+
if (!COMMIT_REJECTION_REASONS.has(reason)) {
|
|
1731
|
+
throw new Error(
|
|
1732
|
+
`Unrecognized ObjectCommitRejectedEvent rejection_reason '${reason}' for uid ${uid}`
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
return reason;
|
|
1736
|
+
}
|
|
1451
1737
|
/**
|
|
1452
1738
|
* Creates a transaction payload to register a blob on the blockchain.
|
|
1453
1739
|
* This is a static helper method for constructing the Move function call payload.
|
|
@@ -1461,8 +1747,6 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1461
1747
|
* @param params.numChunksets - The total number of chunksets in the blob.
|
|
1462
1748
|
*
|
|
1463
1749
|
* @returns An Aptos transaction payload data object for the register_blob Move function.
|
|
1464
|
-
*
|
|
1465
|
-
* @see https://github.com/shelby/shelby/blob/e08e84742cf2b80ad8bb7227deb3013398076d53/move/shelby_contract/sources/global_metadata.move#L357
|
|
1466
1750
|
*/
|
|
1467
1751
|
static createRegisterBlobPayload(params) {
|
|
1468
1752
|
const functionName = params.useSponsoredUsdVariant ? "register_blob_with_sponsor" : "register_blob";
|
|
@@ -1470,6 +1754,8 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1470
1754
|
function: `${(params.deployer ?? SHELBY_DEPLOYER).toString()}::blob_metadata::${functionName}`,
|
|
1471
1755
|
functionArguments: [
|
|
1472
1756
|
params.blobName,
|
|
1757
|
+
params.selectedLocation ?? null,
|
|
1758
|
+
params.locationHint ?? null,
|
|
1473
1759
|
params.expirationMicros,
|
|
1474
1760
|
Hex4.fromHexString(params.blobMerkleRoot).toUint8Array(),
|
|
1475
1761
|
params.numChunksets,
|
|
@@ -1477,7 +1763,8 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1477
1763
|
// TODO
|
|
1478
1764
|
0,
|
|
1479
1765
|
// payment tier
|
|
1480
|
-
params.encoding
|
|
1766
|
+
params.encoding,
|
|
1767
|
+
...encryptionFunctionArgs(params)
|
|
1481
1768
|
]
|
|
1482
1769
|
};
|
|
1483
1770
|
}
|
|
@@ -1495,8 +1782,6 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1495
1782
|
* @param params.blobs.numChunksets - The total number of chunksets in the blob.
|
|
1496
1783
|
*
|
|
1497
1784
|
* @returns An Aptos transaction payload data object for the register_multiple_blobs Move function.
|
|
1498
|
-
*
|
|
1499
|
-
* @see https://github.com/shelby/shelby/blob/e08e84742cf2b80ad8bb7227deb3013398076d53/move/shelby_contract/sources/global_metadata.move#L357
|
|
1500
1785
|
*/
|
|
1501
1786
|
static createBatchRegisterBlobsPayload(params) {
|
|
1502
1787
|
const functionName = params.useSponsoredUsdVariant ? "register_multiple_blobs_with_sponsor" : "register_multiple_blobs";
|
|
@@ -1516,6 +1801,8 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1516
1801
|
function: `${(params.deployer ?? SHELBY_DEPLOYER).toString()}::blob_metadata::${functionName}`,
|
|
1517
1802
|
functionArguments: [
|
|
1518
1803
|
blobNames,
|
|
1804
|
+
params.selectedLocation ?? null,
|
|
1805
|
+
params.locationHint ?? null,
|
|
1519
1806
|
params.expirationMicros,
|
|
1520
1807
|
blobMerkleRoots,
|
|
1521
1808
|
blobNumChunksets,
|
|
@@ -1523,7 +1810,8 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1523
1810
|
// TODO
|
|
1524
1811
|
0,
|
|
1525
1812
|
// payment tier
|
|
1526
|
-
params.encoding
|
|
1813
|
+
params.encoding,
|
|
1814
|
+
...encryptionFunctionArgs(params)
|
|
1527
1815
|
]
|
|
1528
1816
|
};
|
|
1529
1817
|
}
|
|
@@ -1534,24 +1822,20 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1534
1822
|
* @param params.deployer - Optional deployer account address. Defaults to SHELBY_DEPLOYER.
|
|
1535
1823
|
* @param params.blobName - The blob name (e.g. "bar.txt", without the account address prefix).
|
|
1536
1824
|
*
|
|
1537
|
-
* @returns An Aptos transaction payload data object for the
|
|
1538
|
-
*
|
|
1539
|
-
* @see https://github.com/shelby/shelby/blob/64e9d7b4f0005e586faeb1e4085c79159234b6b6/move/shelby_contract/sources/global_metadata.move#L616
|
|
1825
|
+
* @returns An Aptos transaction payload data object for the delete_object Move function.
|
|
1540
1826
|
*/
|
|
1541
|
-
static
|
|
1827
|
+
static createDeleteObjectPayload(params) {
|
|
1542
1828
|
return {
|
|
1543
|
-
function: `${(params.deployer ?? SHELBY_DEPLOYER).toString()}::blob_metadata::
|
|
1544
|
-
|
|
1829
|
+
function: `${(params.deployer ?? SHELBY_DEPLOYER).toString()}::blob_metadata::delete_object`,
|
|
1830
|
+
// Second arg is `if_match_etag: Option<vector<u8>>`; `null` => None
|
|
1831
|
+
// (unconditional delete).
|
|
1832
|
+
functionArguments: [params.blobName, null]
|
|
1545
1833
|
};
|
|
1546
1834
|
}
|
|
1547
1835
|
/**
|
|
1548
1836
|
* Creates a transaction payload to delete multiple blobs on the blockchain.
|
|
1549
1837
|
* This is a static helper method for constructing the Move function call payload.
|
|
1550
1838
|
*
|
|
1551
|
-
* **Note:** This function requires the `delete_multiple_blobs` entry function
|
|
1552
|
-
* which will be deployed to the smart contract on 2026-02-04. Using this
|
|
1553
|
-
* function before that date will result in a transaction failure.
|
|
1554
|
-
*
|
|
1555
1839
|
* This operation is atomic: if any blob deletion fails (e.g., blob not found),
|
|
1556
1840
|
* the entire transaction fails and no blobs are deleted.
|
|
1557
1841
|
*
|
|
@@ -1560,42 +1844,71 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1560
1844
|
* (e.g. ["foo/bar.txt", "baz.txt"], NOT ["0x1/foo/bar.txt"]). The account address
|
|
1561
1845
|
* prefix is automatically derived from the transaction sender.
|
|
1562
1846
|
*
|
|
1563
|
-
* @returns An Aptos transaction payload data object for the
|
|
1564
|
-
*
|
|
1565
|
-
* @see https://github.com/shelby/shelby/blob/main/move/shelby_contract/sources/blob_metadata.move
|
|
1847
|
+
* @returns An Aptos transaction payload data object for the delete_multiple_objects Move function.
|
|
1566
1848
|
*/
|
|
1567
|
-
static
|
|
1849
|
+
static createDeleteMultipleObjectsPayload(params) {
|
|
1568
1850
|
return {
|
|
1569
|
-
function: `${(params.deployer ?? SHELBY_DEPLOYER).toString()}::blob_metadata::
|
|
1570
|
-
|
|
1851
|
+
function: `${(params.deployer ?? SHELBY_DEPLOYER).toString()}::blob_metadata::delete_multiple_objects`,
|
|
1852
|
+
// Second arg is `if_match_etags: vector<Option<vector<u8>>>`; an empty
|
|
1853
|
+
// vector means "no etag check on any object" (unconditional delete).
|
|
1854
|
+
functionArguments: [params.blobNames, []]
|
|
1571
1855
|
};
|
|
1572
1856
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1857
|
+
/**
|
|
1858
|
+
* Sort acks by slot and reduce to the `(ack_bits, signatures)` pair the
|
|
1859
|
+
* contract expects: it walks the set bits low-to-high and consumes the
|
|
1860
|
+
* signatures in that same order.
|
|
1861
|
+
*/
|
|
1862
|
+
static encodeAcks(storageProviderAcks) {
|
|
1863
|
+
const sortedAcks = [...storageProviderAcks].sort((a, b) => a.slot - b.slot);
|
|
1864
|
+
const ackBitMask = sortedAcks.reduce(
|
|
1575
1865
|
(acc, ack) => acc | 1 << ack.slot,
|
|
1576
1866
|
0
|
|
1577
1867
|
);
|
|
1578
1868
|
return {
|
|
1579
|
-
|
|
1869
|
+
ackBits: new U32(Number(ackBitMask)),
|
|
1870
|
+
signatures: sortedAcks.map((ack) => ack.signature)
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
/**
|
|
1874
|
+
* Payload for `commit_object(uid, object_name_suffix, overwrite, if_match_etag,
|
|
1875
|
+
* ack_bits, signatures)` — binds a written pending blob under its object name,
|
|
1876
|
+
* finalizing the upload. SP acks may be batched in here (the contract applies
|
|
1877
|
+
* them before the `is_written` check), so register → upload → commit needs
|
|
1878
|
+
* only a single finalize transaction.
|
|
1879
|
+
*
|
|
1880
|
+
* @param params.uid - The blob UID returned at registration.
|
|
1881
|
+
* @param params.blobName - The object name suffix the blob was registered under.
|
|
1882
|
+
* @param params.overwrite - Allow replacing an existing binding under this name.
|
|
1883
|
+
* @param params.storageProviderAcks - Acks applied atomically with the commit.
|
|
1884
|
+
*/
|
|
1885
|
+
static createCommitObjectPayload(params) {
|
|
1886
|
+
const { ackBits, signatures } = _ShelbyBlobClient.encodeAcks(
|
|
1887
|
+
params.storageProviderAcks
|
|
1888
|
+
);
|
|
1889
|
+
return {
|
|
1890
|
+
function: `${(params.deployer ?? SHELBY_DEPLOYER).toString()}::blob_metadata::commit_object`,
|
|
1580
1891
|
functionArguments: [
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1892
|
+
params.uid,
|
|
1893
|
+
params.blobName,
|
|
1894
|
+
params.overwrite,
|
|
1895
|
+
// `if_match_etag: Option<vector<u8>>` is `null` => None; conditional
|
|
1896
|
+
// (CAS) writes are not yet exposed by the SDK.
|
|
1897
|
+
null,
|
|
1898
|
+
ackBits,
|
|
1899
|
+
signatures
|
|
1588
1900
|
]
|
|
1589
1901
|
};
|
|
1590
1902
|
}
|
|
1591
|
-
async
|
|
1903
|
+
async commitObject(params) {
|
|
1592
1904
|
const transaction = await this.aptos.transaction.build.simple({
|
|
1593
1905
|
...params.options?.build,
|
|
1594
1906
|
options: this.orderlessTxOptions(params.options?.build?.options),
|
|
1595
|
-
data: _ShelbyBlobClient.
|
|
1596
|
-
|
|
1907
|
+
data: _ShelbyBlobClient.createCommitObjectPayload({
|
|
1908
|
+
deployer: this.deployer,
|
|
1909
|
+
uid: params.uid,
|
|
1597
1910
|
blobName: params.blobName,
|
|
1598
|
-
|
|
1911
|
+
overwrite: params.overwrite,
|
|
1599
1912
|
storageProviderAcks: params.storageProviderAcks
|
|
1600
1913
|
}),
|
|
1601
1914
|
sender: params.account.accountAddress
|
|
@@ -1634,22 +1947,6 @@ var ChallengeResponseSchema = z3.object({
|
|
|
1634
1947
|
challenge: z3.string(),
|
|
1635
1948
|
expiresAt: z3.number()
|
|
1636
1949
|
});
|
|
1637
|
-
var MultipartUploadStatusResponseSchema = z3.object({
|
|
1638
|
-
uploadId: z3.string(),
|
|
1639
|
-
completedParts: z3.array(z3.number()),
|
|
1640
|
-
partSize: z3.number(),
|
|
1641
|
-
nParts: z3.number(),
|
|
1642
|
-
uploadedBytes: z3.number()
|
|
1643
|
-
});
|
|
1644
|
-
var StartMultipartUploadResponseSchema = z3.object({
|
|
1645
|
-
uploadId: z3.string()
|
|
1646
|
-
});
|
|
1647
|
-
var UploadPartResponseSchema = z3.object({
|
|
1648
|
-
success: z3.literal(true)
|
|
1649
|
-
});
|
|
1650
|
-
var CompleteMultipartUploadResponseSchema = z3.object({
|
|
1651
|
-
success: z3.literal(true)
|
|
1652
|
-
});
|
|
1653
1950
|
var RPCErrorResponseSchema = z3.object({
|
|
1654
1951
|
error: z3.string()
|
|
1655
1952
|
});
|
|
@@ -1866,6 +2163,7 @@ var BLOB_OWNER_AUTH_SCHEME_HEADER = "X-Shelby-Auth-Scheme";
|
|
|
1866
2163
|
var BLOB_OWNER_IDENTITY_HEADER = "X-Shelby-Identity";
|
|
1867
2164
|
var BLOB_OWNER_DOMAIN_HEADER = "X-Shelby-Domain";
|
|
1868
2165
|
var BLOB_OWNER_AUTH_FUNCTION_HEADER = "X-Shelby-Auth-Function";
|
|
2166
|
+
var INCLUSION_PROOF_HEADER = "X-Shelby-Inclusion-Proof";
|
|
1869
2167
|
function buildAuthHeaders(auth) {
|
|
1870
2168
|
const signatureBase64 = btoa(
|
|
1871
2169
|
Array.from(auth.signature, (byte) => String.fromCharCode(byte)).join("")
|
|
@@ -1889,6 +2187,58 @@ function buildAuthHeaders(auth) {
|
|
|
1889
2187
|
function encodeURIComponentKeepSlashes(str) {
|
|
1890
2188
|
return encodeURIComponent(str).replace(/%2F/g, "/");
|
|
1891
2189
|
}
|
|
2190
|
+
async function generateChunksetInclusionProof(chunksetRoots, chunksetIndex) {
|
|
2191
|
+
if (chunksetRoots.length === 0) {
|
|
2192
|
+
throw new Error("Cannot generate inclusion proof for empty chunkset roots");
|
|
2193
|
+
}
|
|
2194
|
+
if (chunksetIndex < 0 || chunksetIndex >= chunksetRoots.length) {
|
|
2195
|
+
throw new Error(
|
|
2196
|
+
`Chunkset index ${chunksetIndex} out of range [0, ${chunksetRoots.length})`
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2199
|
+
if (chunksetRoots.length === 1) {
|
|
2200
|
+
return [];
|
|
2201
|
+
}
|
|
2202
|
+
const zeroHash = new Uint8Array(32);
|
|
2203
|
+
const siblings = [];
|
|
2204
|
+
let currentLeaves = chunksetRoots.map((h) => h.toUint8Array());
|
|
2205
|
+
let currentIdx = chunksetIndex;
|
|
2206
|
+
while (currentLeaves.length > 1) {
|
|
2207
|
+
if (currentLeaves.length % 2 !== 0) {
|
|
2208
|
+
currentLeaves.push(zeroHash);
|
|
2209
|
+
}
|
|
2210
|
+
const siblingIdx = currentIdx % 2 === 0 ? currentIdx + 1 : currentIdx - 1;
|
|
2211
|
+
siblings.push(currentLeaves[siblingIdx]);
|
|
2212
|
+
const nextLeaves = [];
|
|
2213
|
+
for (let i = 0; i < currentLeaves.length; i += 2) {
|
|
2214
|
+
const combined = await concatHashes([
|
|
2215
|
+
currentLeaves[i],
|
|
2216
|
+
currentLeaves[i + 1]
|
|
2217
|
+
]);
|
|
2218
|
+
nextLeaves.push(combined.toUint8Array());
|
|
2219
|
+
}
|
|
2220
|
+
currentLeaves = nextLeaves;
|
|
2221
|
+
currentIdx = Math.floor(currentIdx / 2);
|
|
2222
|
+
}
|
|
2223
|
+
return siblings;
|
|
2224
|
+
}
|
|
2225
|
+
function encodeInclusionProof(siblings) {
|
|
2226
|
+
if (siblings.length === 0) {
|
|
2227
|
+
return "NONE";
|
|
2228
|
+
}
|
|
2229
|
+
const totalBytes = siblings.length * 32;
|
|
2230
|
+
const combined = new Uint8Array(totalBytes);
|
|
2231
|
+
let offset = 0;
|
|
2232
|
+
for (const sibling of siblings) {
|
|
2233
|
+
combined.set(sibling, offset);
|
|
2234
|
+
offset += 32;
|
|
2235
|
+
}
|
|
2236
|
+
const binaryString = Array.from(
|
|
2237
|
+
combined,
|
|
2238
|
+
(byte) => String.fromCharCode(byte)
|
|
2239
|
+
).join("");
|
|
2240
|
+
return btoa(binaryString);
|
|
2241
|
+
}
|
|
1892
2242
|
function validateTotalBytes(totalBytes) {
|
|
1893
2243
|
if (!Number.isInteger(totalBytes) || totalBytes < 0) {
|
|
1894
2244
|
throw new Error("totalBytes must be a non-negative integer");
|
|
@@ -1920,7 +2270,7 @@ var ShelbyRPCClient = class {
|
|
|
1920
2270
|
* @param config - The client configuration object.
|
|
1921
2271
|
* @param config.network - The Shelby network to use.
|
|
1922
2272
|
* @param options.signChallengeHandler - Optional override for challenge
|
|
1923
|
-
* signing. When set, `
|
|
2273
|
+
* signing. When set, `putBlobChunksets` uses this instead of the built-in
|
|
1924
2274
|
* `signChallenge`. Intended for kit-level overrides (e.g. Solana DAA).
|
|
1925
2275
|
*
|
|
1926
2276
|
* @example
|
|
@@ -1971,43 +2321,6 @@ var ShelbyRPCClient = class {
|
|
|
1971
2321
|
}
|
|
1972
2322
|
return ChallengeResponseSchema.parse(await response.json());
|
|
1973
2323
|
}
|
|
1974
|
-
/**
|
|
1975
|
-
* Check if there's an existing multipart upload for a blob.
|
|
1976
|
-
* Returns the upload status including which parts have been uploaded.
|
|
1977
|
-
*
|
|
1978
|
-
* @param account - The account that owns the blob.
|
|
1979
|
-
* @param blobName - The name of the blob.
|
|
1980
|
-
* @returns The upload status, or undefined if no pending upload exists.
|
|
1981
|
-
*
|
|
1982
|
-
* @example
|
|
1983
|
-
* ```typescript
|
|
1984
|
-
* const status = await client.getMultipartUploadStatus(account, "myblob.txt");
|
|
1985
|
-
* if (status) {
|
|
1986
|
-
* console.log(`Resuming upload ${status.uploadId}, ${status.completedParts.length} parts done`);
|
|
1987
|
-
* }
|
|
1988
|
-
* ```
|
|
1989
|
-
*/
|
|
1990
|
-
async getMultipartUploadStatus(account, blobName) {
|
|
1991
|
-
const url = new URL(buildRequestUrl("/v1/multipart-uploads", this.baseUrl));
|
|
1992
|
-
url.searchParams.set("account", account.toString());
|
|
1993
|
-
url.searchParams.set("blobName", blobName);
|
|
1994
|
-
const response = await fetch(url.toString(), {
|
|
1995
|
-
method: "GET",
|
|
1996
|
-
headers: {
|
|
1997
|
-
...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
|
|
1998
|
-
}
|
|
1999
|
-
});
|
|
2000
|
-
if (response.status === 404) {
|
|
2001
|
-
return void 0;
|
|
2002
|
-
}
|
|
2003
|
-
if (!response.ok) {
|
|
2004
|
-
const errorBody = await response.text().catch(() => "");
|
|
2005
|
-
throw new Error(
|
|
2006
|
-
`Failed to get multipart upload status: status ${response.status}, body: ${errorBody}`
|
|
2007
|
-
);
|
|
2008
|
-
}
|
|
2009
|
-
return MultipartUploadStatusResponseSchema.parse(await response.json());
|
|
2010
|
-
}
|
|
2011
2324
|
/**
|
|
2012
2325
|
* Sign a challenge using the given account and return auth credentials.
|
|
2013
2326
|
*
|
|
@@ -2024,269 +2337,142 @@ var ShelbyRPCClient = class {
|
|
|
2024
2337
|
publicKey: account.publicKey.toUint8Array()
|
|
2025
2338
|
};
|
|
2026
2339
|
}
|
|
2027
|
-
async #uploadPart(uploadId, partIdx, partData) {
|
|
2028
|
-
const nRetries = 5;
|
|
2029
|
-
let lastResponse;
|
|
2030
|
-
let lastError;
|
|
2031
|
-
let attempts = 0;
|
|
2032
|
-
const partUrl = buildRequestUrl(
|
|
2033
|
-
`/v1/multipart-uploads/${uploadId}/parts/${partIdx}`,
|
|
2034
|
-
this.baseUrl
|
|
2035
|
-
);
|
|
2036
|
-
for (let i = 0; i < nRetries; ++i) {
|
|
2037
|
-
attempts++;
|
|
2038
|
-
try {
|
|
2039
|
-
lastResponse = await fetch(partUrl, {
|
|
2040
|
-
method: "PUT",
|
|
2041
|
-
headers: {
|
|
2042
|
-
"Content-Type": "application/octet-stream",
|
|
2043
|
-
...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
|
|
2044
|
-
},
|
|
2045
|
-
body: partData
|
|
2046
|
-
});
|
|
2047
|
-
lastError = void 0;
|
|
2048
|
-
} catch (error) {
|
|
2049
|
-
lastError = error;
|
|
2050
|
-
if (i < nRetries - 1) {
|
|
2051
|
-
const delay = 2 ** i * 100;
|
|
2052
|
-
await sleep(delay);
|
|
2053
|
-
continue;
|
|
2054
|
-
}
|
|
2055
|
-
break;
|
|
2056
|
-
}
|
|
2057
|
-
if (lastResponse.ok) return;
|
|
2058
|
-
if (!isRetryableStatus(lastResponse.status)) {
|
|
2059
|
-
break;
|
|
2060
|
-
}
|
|
2061
|
-
if (i < nRetries - 1) {
|
|
2062
|
-
const delay = 2 ** i * 100;
|
|
2063
|
-
await sleep(delay);
|
|
2064
|
-
}
|
|
2065
|
-
}
|
|
2066
|
-
if (lastError !== void 0) {
|
|
2067
|
-
const errorCode = getErrorCode(lastError);
|
|
2068
|
-
const errorMessage = lastError instanceof Error ? lastError.message : String(lastError);
|
|
2069
|
-
throw new Error(
|
|
2070
|
-
`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}`,
|
|
2071
|
-
{ cause: lastError }
|
|
2072
|
-
);
|
|
2073
|
-
}
|
|
2074
|
-
const errorBody = await lastResponse?.text().catch(() => "");
|
|
2075
|
-
throw new Error(
|
|
2076
|
-
`Failed to upload part ${partIdx} for multipart upload ${uploadId} after ${attempts} attempt${attempts === 1 ? "" : "s"}. status: ${lastResponse?.status}, body: ${errorBody}`
|
|
2077
|
-
);
|
|
2078
|
-
}
|
|
2079
|
-
async #putBlobMultipart(account, blobName, blobData, totalBytes, partSize = 5 * 1024 * 1024, onProgress) {
|
|
2080
|
-
validateTotalBytes(totalBytes);
|
|
2081
|
-
const startResponse = await fetch(
|
|
2082
|
-
buildRequestUrl("/v1/multipart-uploads", this.baseUrl),
|
|
2083
|
-
{
|
|
2084
|
-
method: "POST",
|
|
2085
|
-
headers: {
|
|
2086
|
-
"Content-Type": "application/json",
|
|
2087
|
-
...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
|
|
2088
|
-
},
|
|
2089
|
-
body: JSON.stringify({
|
|
2090
|
-
rawAccount: account.toString(),
|
|
2091
|
-
rawBlobName: blobName,
|
|
2092
|
-
rawPartSize: partSize
|
|
2093
|
-
})
|
|
2094
|
-
}
|
|
2095
|
-
);
|
|
2096
|
-
if (!startResponse.ok) {
|
|
2097
|
-
let errorBodyText = "Could not read error body";
|
|
2098
|
-
try {
|
|
2099
|
-
errorBodyText = await startResponse.text();
|
|
2100
|
-
} catch (_e) {
|
|
2101
|
-
}
|
|
2102
|
-
throw new Error(
|
|
2103
|
-
`Failed to start multipart upload! status: ${startResponse.status}, body: ${errorBodyText}`
|
|
2104
|
-
);
|
|
2105
|
-
}
|
|
2106
|
-
const { uploadId } = StartMultipartUploadResponseSchema.parse(
|
|
2107
|
-
await startResponse.json()
|
|
2108
|
-
);
|
|
2109
|
-
const totalParts = Math.ceil(totalBytes / partSize);
|
|
2110
|
-
let uploadedBytes = 0;
|
|
2111
|
-
for await (const [partIdx, partData] of readInChunks(blobData, partSize)) {
|
|
2112
|
-
await this.#uploadPart(uploadId, partIdx, partData);
|
|
2113
|
-
uploadedBytes += partData.length;
|
|
2114
|
-
onProgress?.({
|
|
2115
|
-
phase: "uploading",
|
|
2116
|
-
partIdx,
|
|
2117
|
-
totalParts,
|
|
2118
|
-
partBytes: partData.length,
|
|
2119
|
-
uploadedBytes,
|
|
2120
|
-
totalBytes
|
|
2121
|
-
});
|
|
2122
|
-
}
|
|
2123
|
-
if (uploadedBytes !== totalBytes) {
|
|
2124
|
-
throw new Error(
|
|
2125
|
-
`Uploaded bytes (${uploadedBytes}) did not match declared totalBytes (${totalBytes})`
|
|
2126
|
-
);
|
|
2127
|
-
}
|
|
2128
|
-
const finalPartIdx = totalParts > 0 ? totalParts - 1 : 0;
|
|
2129
|
-
onProgress?.({
|
|
2130
|
-
phase: "finalizing",
|
|
2131
|
-
partIdx: finalPartIdx,
|
|
2132
|
-
totalParts,
|
|
2133
|
-
// no part uploaded in this phase
|
|
2134
|
-
partBytes: 0,
|
|
2135
|
-
uploadedBytes: totalBytes,
|
|
2136
|
-
totalBytes
|
|
2137
|
-
});
|
|
2138
|
-
const completeResponse = await fetch(
|
|
2139
|
-
buildRequestUrl(
|
|
2140
|
-
`/v1/multipart-uploads/${uploadId}/complete`,
|
|
2141
|
-
this.baseUrl
|
|
2142
|
-
),
|
|
2143
|
-
{
|
|
2144
|
-
method: "POST",
|
|
2145
|
-
headers: {
|
|
2146
|
-
"Content-Type": "application/json",
|
|
2147
|
-
...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
|
|
2148
|
-
}
|
|
2149
|
-
}
|
|
2150
|
-
);
|
|
2151
|
-
if (!completeResponse.ok) {
|
|
2152
|
-
let errorBodyText = "Could not read error body";
|
|
2153
|
-
try {
|
|
2154
|
-
errorBodyText = await completeResponse.text();
|
|
2155
|
-
} catch (_e) {
|
|
2156
|
-
}
|
|
2157
|
-
throw new Error(
|
|
2158
|
-
`Failed to complete multipart upload! status: ${completeResponse.status}, body: ${errorBodyText}`
|
|
2159
|
-
);
|
|
2160
|
-
}
|
|
2161
|
-
}
|
|
2162
|
-
/**
|
|
2163
|
-
* Uploads blob data to the Shelby RPC node for storage by storage providers.
|
|
2164
|
-
* This method should be called after blob commitments have been registered on the blockchain.
|
|
2165
|
-
* Uses multipart upload for efficient handling of large files.
|
|
2166
|
-
*
|
|
2167
|
-
* @param params.account - The account that owns the blob.
|
|
2168
|
-
* @param params.blobName - The name/path of the blob (e.g. "folder/file.txt").
|
|
2169
|
-
* @param params.blobData - The raw blob data as a Uint8Array or ReadableStream.
|
|
2170
|
-
* @param params.totalBytes - Total byte length. Required for streams; optional for Uint8Array.
|
|
2171
|
-
*
|
|
2172
|
-
* @example
|
|
2173
|
-
* ```typescript
|
|
2174
|
-
* const blobData = new TextEncoder().encode("Hello, world!");
|
|
2175
|
-
*
|
|
2176
|
-
* await client.putBlob({
|
|
2177
|
-
* account: AccountAddress.from("0x1"),
|
|
2178
|
-
* blobName: "greetings/hello.txt",
|
|
2179
|
-
* blobData,
|
|
2180
|
-
* });
|
|
2181
|
-
* ```
|
|
2182
|
-
*/
|
|
2183
|
-
async putBlob(params) {
|
|
2184
|
-
BlobNameSchema.parse(params.blobName);
|
|
2185
|
-
let totalBytes;
|
|
2186
|
-
if (params.blobData instanceof Uint8Array) {
|
|
2187
|
-
totalBytes = params.totalBytes ?? params.blobData.length;
|
|
2188
|
-
if (totalBytes !== params.blobData.length) {
|
|
2189
|
-
throw new Error(
|
|
2190
|
-
"totalBytes must match blobData.length when blobData is a Uint8Array"
|
|
2191
|
-
);
|
|
2192
|
-
}
|
|
2193
|
-
} else {
|
|
2194
|
-
if (params.totalBytes === void 0) {
|
|
2195
|
-
throw new Error(
|
|
2196
|
-
"totalBytes is required when blobData is a ReadableStream"
|
|
2197
|
-
);
|
|
2198
|
-
}
|
|
2199
|
-
totalBytes = params.totalBytes;
|
|
2200
|
-
}
|
|
2201
|
-
validateTotalBytes(totalBytes);
|
|
2202
|
-
await this.#putBlobMultipart(
|
|
2203
|
-
params.account,
|
|
2204
|
-
params.blobName,
|
|
2205
|
-
params.blobData,
|
|
2206
|
-
totalBytes,
|
|
2207
|
-
void 0,
|
|
2208
|
-
params.onProgress
|
|
2209
|
-
);
|
|
2210
|
-
}
|
|
2211
2340
|
/**
|
|
2212
|
-
* Upload a
|
|
2341
|
+
* Upload a single chunkset via the v2 chunkset API.
|
|
2213
2342
|
*/
|
|
2214
|
-
async #
|
|
2215
|
-
const
|
|
2343
|
+
async #uploadChunkset(params) {
|
|
2344
|
+
const maxNonBusyRetries = 5;
|
|
2216
2345
|
let lastResponse;
|
|
2217
2346
|
let lastError;
|
|
2218
2347
|
let attempts = 0;
|
|
2219
|
-
|
|
2220
|
-
|
|
2348
|
+
let consecutive429s = 0;
|
|
2349
|
+
let nonBusyRetries = 0;
|
|
2350
|
+
const chunksetUrl = buildRequestUrl(
|
|
2351
|
+
`/v2/chunksets/${params.account}/${params.chunksetIndex}/${params.uid}`,
|
|
2221
2352
|
this.baseUrl
|
|
2222
2353
|
);
|
|
2223
|
-
const authHeaders = buildAuthHeaders(auth);
|
|
2224
|
-
|
|
2354
|
+
const authHeaders = buildAuthHeaders(params.auth);
|
|
2355
|
+
while (true) {
|
|
2356
|
+
if (params.signal?.aborted) {
|
|
2357
|
+
throw new DOMException("Upload aborted", "AbortError");
|
|
2358
|
+
}
|
|
2225
2359
|
attempts++;
|
|
2226
2360
|
try {
|
|
2227
|
-
lastResponse = await fetch(
|
|
2361
|
+
lastResponse = await fetch(chunksetUrl, {
|
|
2228
2362
|
method: "PUT",
|
|
2229
2363
|
headers: {
|
|
2230
2364
|
"Content-Type": "application/octet-stream",
|
|
2231
2365
|
...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {},
|
|
2232
|
-
...authHeaders
|
|
2366
|
+
...authHeaders,
|
|
2367
|
+
[INCLUSION_PROOF_HEADER]: params.inclusionProof
|
|
2233
2368
|
},
|
|
2234
|
-
body:
|
|
2369
|
+
body: params.chunksetData,
|
|
2370
|
+
signal: params.signal
|
|
2235
2371
|
});
|
|
2236
2372
|
lastError = void 0;
|
|
2237
2373
|
} catch (error) {
|
|
2374
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
2375
|
+
throw error;
|
|
2376
|
+
}
|
|
2238
2377
|
lastError = error;
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2378
|
+
consecutive429s = 0;
|
|
2379
|
+
nonBusyRetries++;
|
|
2380
|
+
if (nonBusyRetries < maxNonBusyRetries) {
|
|
2381
|
+
const delay2 = 2 ** nonBusyRetries * 100;
|
|
2382
|
+
await sleep(delay2);
|
|
2242
2383
|
continue;
|
|
2243
2384
|
}
|
|
2244
2385
|
break;
|
|
2245
2386
|
}
|
|
2246
|
-
if (lastResponse.ok)
|
|
2387
|
+
if (lastResponse.ok) {
|
|
2388
|
+
const json = await lastResponse.json();
|
|
2389
|
+
const spAcks = json.spAcks?.map((ack) => ({
|
|
2390
|
+
slot: ack.slot,
|
|
2391
|
+
signature: Uint8Array.from(
|
|
2392
|
+
atob(ack.signature),
|
|
2393
|
+
(c) => c.charCodeAt(0)
|
|
2394
|
+
)
|
|
2395
|
+
}));
|
|
2396
|
+
return {
|
|
2397
|
+
success: json.success,
|
|
2398
|
+
acksReceived: json.acksReceived,
|
|
2399
|
+
spAcks
|
|
2400
|
+
};
|
|
2401
|
+
}
|
|
2402
|
+
if (lastResponse.status === 429) {
|
|
2403
|
+
consecutive429s++;
|
|
2404
|
+
const baseDelay = 500;
|
|
2405
|
+
const jitter = Math.random() * 200;
|
|
2406
|
+
const delay2 = Math.min(
|
|
2407
|
+
3e4,
|
|
2408
|
+
2 ** consecutive429s * baseDelay + jitter
|
|
2409
|
+
);
|
|
2410
|
+
await sleep(delay2);
|
|
2411
|
+
continue;
|
|
2412
|
+
}
|
|
2413
|
+
consecutive429s = 0;
|
|
2247
2414
|
if (!isRetryableStatus(lastResponse.status)) {
|
|
2248
2415
|
break;
|
|
2249
2416
|
}
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2417
|
+
nonBusyRetries++;
|
|
2418
|
+
if (nonBusyRetries >= maxNonBusyRetries) {
|
|
2419
|
+
break;
|
|
2253
2420
|
}
|
|
2421
|
+
const delay = 2 ** nonBusyRetries * 100;
|
|
2422
|
+
await sleep(delay);
|
|
2254
2423
|
}
|
|
2255
2424
|
if (lastError !== void 0) {
|
|
2256
2425
|
const errorCode = getErrorCode(lastError);
|
|
2257
2426
|
const errorMessage = lastError instanceof Error ? lastError.message : String(lastError);
|
|
2258
2427
|
throw new Error(
|
|
2259
|
-
`Failed to upload
|
|
2428
|
+
`Failed to upload chunkset ${params.chunksetIndex} after ${attempts} attempt${attempts === 1 ? "" : "s"}. ${errorCode ? `(${errorCode}) ` : ""}Last error: ${errorMessage}`,
|
|
2260
2429
|
{ cause: lastError }
|
|
2261
2430
|
);
|
|
2262
2431
|
}
|
|
2263
2432
|
const errorBody = await lastResponse?.text().catch(() => "");
|
|
2264
2433
|
throw new Error(
|
|
2265
|
-
`Failed to upload
|
|
2434
|
+
`Failed to upload chunkset ${params.chunksetIndex} after ${attempts} attempt${attempts === 1 ? "" : "s"}. status: ${lastResponse?.status}, body: ${errorBody}`
|
|
2266
2435
|
);
|
|
2267
2436
|
}
|
|
2268
2437
|
/**
|
|
2269
|
-
* Uploads blob data to the Shelby RPC node
|
|
2270
|
-
* This method authenticates using challenge-response and
|
|
2438
|
+
* Uploads blob data to the Shelby RPC node using the v2 chunkset API.
|
|
2439
|
+
* This method authenticates using challenge-response and uploads chunksets
|
|
2440
|
+
* directly to storage providers via the RPC's worker pool.
|
|
2441
|
+
*
|
|
2442
|
+
* This method:
|
|
2443
|
+
* - Sends raw chunkset data directly to the RPC for erasure encoding
|
|
2444
|
+
* - Requires pre-computed blob commitments to generate inclusion proofs
|
|
2445
|
+
* - Does not support resume (each chunkset is idempotent on the SP side)
|
|
2271
2446
|
*
|
|
2272
2447
|
* @param params.account - The Aptos Account (with signing capability) that owns the blob.
|
|
2273
|
-
* @param params.
|
|
2448
|
+
* @param params.uid - The blob's on-chain UID (from `BlobRegisteredEvent` at registration).
|
|
2274
2449
|
* @param params.blobData - The raw blob data as a Uint8Array or ReadableStream.
|
|
2450
|
+
* @param params.commitments - Pre-computed blob commitments (from generateCommitments).
|
|
2275
2451
|
* @param params.totalBytes - Total byte length. Required for streams; optional for Uint8Array.
|
|
2452
|
+
* @param params.chunksetConcurrency - Number of chunksets to upload in parallel. Defaults to 4.
|
|
2276
2453
|
* @param params.onProgress - Optional callback for upload progress.
|
|
2454
|
+
* @param params.signal - Optional AbortSignal for cancellation. When aborted, in-flight
|
|
2455
|
+
* HTTP requests are cancelled and an AbortError is thrown.
|
|
2277
2456
|
*
|
|
2278
2457
|
* @example
|
|
2279
2458
|
* ```typescript
|
|
2280
|
-
*
|
|
2281
|
-
*
|
|
2282
|
-
*
|
|
2459
|
+
* // First, generate commitments for the blob
|
|
2460
|
+
* const commitments = await generateCommitments(provider, fileData);
|
|
2461
|
+
*
|
|
2462
|
+
* // Register the blob on chain, then read its UID from the register tx's
|
|
2463
|
+
* // BlobRegisteredEvent (see ShelbyBlobClient.registeredBlobUids).
|
|
2464
|
+
*
|
|
2465
|
+
* // Upload using chunkset API
|
|
2466
|
+
* await rpcClient.putBlobChunksets({
|
|
2467
|
+
* account: myAccount,
|
|
2468
|
+
* uid: blobUid,
|
|
2283
2469
|
* blobData: fileData,
|
|
2284
|
-
*
|
|
2470
|
+
* commitments,
|
|
2471
|
+
* chunksetConcurrency: 8,
|
|
2285
2472
|
* });
|
|
2286
2473
|
* ```
|
|
2287
2474
|
*/
|
|
2288
|
-
async
|
|
2289
|
-
BlobNameSchema.parse(params.blobName);
|
|
2475
|
+
async putBlobChunksets(params) {
|
|
2290
2476
|
let totalBytes;
|
|
2291
2477
|
if (params.blobData instanceof Uint8Array) {
|
|
2292
2478
|
totalBytes = params.totalBytes ?? params.blobData.length;
|
|
@@ -2304,110 +2490,146 @@ var ShelbyRPCClient = class {
|
|
|
2304
2490
|
totalBytes = params.totalBytes;
|
|
2305
2491
|
}
|
|
2306
2492
|
validateTotalBytes(totalBytes);
|
|
2307
|
-
const
|
|
2493
|
+
const chunksetConcurrency = params.chunksetConcurrency ?? 4;
|
|
2494
|
+
const totalChunksets = params.commitments.chunkset_commitments.length;
|
|
2495
|
+
const rawDataSizeFromCommitments = params.commitments.raw_data_size;
|
|
2496
|
+
if (rawDataSizeFromCommitments !== totalBytes) {
|
|
2497
|
+
throw new Error(
|
|
2498
|
+
`Data size mismatch: commitments were generated for ${rawDataSizeFromCommitments} bytes, but totalBytes=${totalBytes}. This will cause inclusion proof verification to fail.`
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2501
|
+
const chunksetRoots = params.commitments.chunkset_commitments.map(
|
|
2502
|
+
(c) => Hex6.fromHexString(c.chunkset_root)
|
|
2503
|
+
);
|
|
2308
2504
|
const { challenge } = await this.getChallenge(
|
|
2309
2505
|
params.account.accountAddress
|
|
2310
2506
|
);
|
|
2311
2507
|
const signFn = this.#signChallengeOverride ?? this.signChallenge.bind(this);
|
|
2312
2508
|
const auth = signFn(params.account, challenge);
|
|
2313
|
-
const
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
uploadId = existingUpload.uploadId;
|
|
2323
|
-
completedPartsSet = new Set(existingUpload.completedParts);
|
|
2324
|
-
totalParts = existingUpload.nParts;
|
|
2325
|
-
if (existingUpload.partSize !== partSize) {
|
|
2326
|
-
throw new Error(
|
|
2327
|
-
`Cannot resume upload: part size mismatch. Existing upload uses ${existingUpload.partSize} bytes, but ${partSize} was requested.`
|
|
2328
|
-
);
|
|
2329
|
-
}
|
|
2330
|
-
} else {
|
|
2331
|
-
const startResponse = await fetch(
|
|
2332
|
-
buildRequestUrl("/v1/multipart-uploads", this.baseUrl),
|
|
2333
|
-
{
|
|
2334
|
-
method: "POST",
|
|
2335
|
-
headers: {
|
|
2336
|
-
"Content-Type": "application/json",
|
|
2337
|
-
...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {},
|
|
2338
|
-
...authHeaders
|
|
2339
|
-
},
|
|
2340
|
-
body: JSON.stringify({
|
|
2341
|
-
rawAccount: params.account.accountAddress.toString(),
|
|
2342
|
-
rawBlobName: params.blobName,
|
|
2343
|
-
rawPartSize: partSize
|
|
2344
|
-
})
|
|
2345
|
-
}
|
|
2509
|
+
const firstChunkset = params.commitments.chunkset_commitments[0];
|
|
2510
|
+
if (!firstChunkset) {
|
|
2511
|
+
throw new Error("Commitments must have at least one chunkset");
|
|
2512
|
+
}
|
|
2513
|
+
const erasure_n = firstChunkset.chunk_commitments.length;
|
|
2514
|
+
const matchingEntry = findErasureSchemeByErasureN(erasure_n);
|
|
2515
|
+
if (!matchingEntry) {
|
|
2516
|
+
throw new Error(
|
|
2517
|
+
`Unknown erasure coding scheme with erasure_n=${erasure_n}`
|
|
2346
2518
|
);
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2519
|
+
}
|
|
2520
|
+
const [schemeKey, matchingParams] = matchingEntry;
|
|
2521
|
+
const erasure_k = matchingParams.erasure_k;
|
|
2522
|
+
const { chunkSizeBytes } = ERASURE_CODE_AND_CHUNK_MAPPING[schemeKey];
|
|
2523
|
+
const chunksetSize = erasure_k * chunkSizeBytes;
|
|
2524
|
+
const expectedChunksetCount = totalBytes === 0 ? 1 : Math.ceil(totalBytes / chunksetSize);
|
|
2525
|
+
if (expectedChunksetCount !== totalChunksets) {
|
|
2526
|
+
throw new Error(
|
|
2527
|
+
`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.`
|
|
2355
2528
|
);
|
|
2356
|
-
uploadId = parsed.uploadId;
|
|
2357
|
-
completedPartsSet = /* @__PURE__ */ new Set();
|
|
2358
|
-
totalParts = Math.ceil(totalBytes / partSize);
|
|
2359
2529
|
}
|
|
2360
2530
|
let uploadedBytes = 0;
|
|
2361
|
-
let
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
if (completedPartsSet.has(partIdx)) {
|
|
2368
|
-
uploadedBytes += partData.length;
|
|
2369
|
-
continue;
|
|
2531
|
+
let bytesReadFromSource = 0;
|
|
2532
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
2533
|
+
async function* chunksetsFromBlob() {
|
|
2534
|
+
if (totalBytes === 0) {
|
|
2535
|
+
yield [0, new Uint8Array(chunksetSize)];
|
|
2536
|
+
return;
|
|
2370
2537
|
}
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2538
|
+
yield* readInChunks(params.blobData, chunksetSize);
|
|
2539
|
+
}
|
|
2540
|
+
const chunksetIterator = chunksetsFromBlob();
|
|
2541
|
+
const aggregatedAcks = /* @__PURE__ */ new Map();
|
|
2542
|
+
const internalAbort = new AbortController();
|
|
2543
|
+
if (params.signal?.aborted) {
|
|
2544
|
+
internalAbort.abort();
|
|
2545
|
+
} else if (params.signal) {
|
|
2546
|
+
params.signal.addEventListener("abort", () => internalAbort.abort(), {
|
|
2547
|
+
once: true
|
|
2380
2548
|
});
|
|
2381
2549
|
}
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
`/v1/multipart-uploads/${uploadId}/complete`,
|
|
2394
|
-
this.baseUrl
|
|
2395
|
-
),
|
|
2396
|
-
{
|
|
2397
|
-
method: "POST",
|
|
2398
|
-
headers: {
|
|
2399
|
-
"Content-Type": "application/json",
|
|
2400
|
-
...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {},
|
|
2401
|
-
...authHeaders
|
|
2550
|
+
let iteratorDone = false;
|
|
2551
|
+
let firstError = null;
|
|
2552
|
+
while (!iteratorDone || inFlight.size > 0) {
|
|
2553
|
+
if (internalAbort.signal.aborted) {
|
|
2554
|
+
throw firstError ?? new DOMException("Upload aborted", "AbortError");
|
|
2555
|
+
}
|
|
2556
|
+
while (inFlight.size < chunksetConcurrency && !iteratorDone) {
|
|
2557
|
+
const { value, done } = await chunksetIterator.next();
|
|
2558
|
+
if (done) {
|
|
2559
|
+
iteratorDone = true;
|
|
2560
|
+
break;
|
|
2402
2561
|
}
|
|
2562
|
+
const [chunksetIdx, chunksetData] = value;
|
|
2563
|
+
bytesReadFromSource += totalBytes === 0 ? 0 : chunksetData.byteLength;
|
|
2564
|
+
const expectedCommitment = params.commitments.chunkset_commitments[chunksetIdx];
|
|
2565
|
+
if (!expectedCommitment) {
|
|
2566
|
+
throw new Error(
|
|
2567
|
+
`Chunkset index ${chunksetIdx} out of range. Commitments only have ${totalChunksets} chunksets. This suggests a chunkset size mismatch between commitment generation and upload.`
|
|
2568
|
+
);
|
|
2569
|
+
}
|
|
2570
|
+
const proofSiblings = await generateChunksetInclusionProof(
|
|
2571
|
+
chunksetRoots,
|
|
2572
|
+
chunksetIdx
|
|
2573
|
+
);
|
|
2574
|
+
const inclusionProof = encodeInclusionProof(proofSiblings);
|
|
2575
|
+
const chunksetDataCopy = new Uint8Array(chunksetData);
|
|
2576
|
+
const chunksetBytes = chunksetDataCopy.length;
|
|
2577
|
+
const uploadPromise = (async () => {
|
|
2578
|
+
const result = await this.#uploadChunkset({
|
|
2579
|
+
account: params.account.accountAddress.toString(),
|
|
2580
|
+
uid: params.uid,
|
|
2581
|
+
chunksetIndex: chunksetIdx,
|
|
2582
|
+
inclusionProof,
|
|
2583
|
+
chunksetData: chunksetDataCopy,
|
|
2584
|
+
auth,
|
|
2585
|
+
signal: internalAbort.signal
|
|
2586
|
+
});
|
|
2587
|
+
const chunksetSpAcks = result.spAcks?.map((ack) => ({
|
|
2588
|
+
slot: ack.slot,
|
|
2589
|
+
signature: ack.signature
|
|
2590
|
+
}));
|
|
2591
|
+
if (chunksetSpAcks) {
|
|
2592
|
+
for (const ack of chunksetSpAcks) {
|
|
2593
|
+
aggregatedAcks.set(ack.slot, ack.signature);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
uploadedBytes += chunksetBytes;
|
|
2597
|
+
params.onProgress?.({
|
|
2598
|
+
phase: "uploading",
|
|
2599
|
+
chunksetIdx,
|
|
2600
|
+
totalChunksets,
|
|
2601
|
+
chunksetBytes,
|
|
2602
|
+
uploadedBytes,
|
|
2603
|
+
totalBytes,
|
|
2604
|
+
acksReceived: result.acksReceived,
|
|
2605
|
+
spAcks: chunksetSpAcks
|
|
2606
|
+
});
|
|
2607
|
+
})().catch((err) => {
|
|
2608
|
+
if (!firstError) {
|
|
2609
|
+
firstError = err;
|
|
2610
|
+
internalAbort.abort();
|
|
2611
|
+
}
|
|
2612
|
+
}).finally(() => {
|
|
2613
|
+
inFlight.delete(uploadPromise);
|
|
2614
|
+
});
|
|
2615
|
+
inFlight.add(uploadPromise);
|
|
2403
2616
|
}
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2617
|
+
if (inFlight.size > 0) {
|
|
2618
|
+
await Promise.race(inFlight);
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
if (firstError) {
|
|
2622
|
+
throw firstError;
|
|
2623
|
+
}
|
|
2624
|
+
if (bytesReadFromSource !== totalBytes) {
|
|
2407
2625
|
throw new Error(
|
|
2408
|
-
`
|
|
2626
|
+
`Data source produced ${bytesReadFromSource} bytes, but expected ${totalBytes} bytes. The stream may have ended prematurely, resulting in an incomplete upload.`
|
|
2409
2627
|
);
|
|
2410
2628
|
}
|
|
2629
|
+
const spAcks = Array.from(
|
|
2630
|
+
aggregatedAcks.entries()
|
|
2631
|
+
).map(([slot, signature]) => ({ slot, signature }));
|
|
2632
|
+
return { spAcks };
|
|
2411
2633
|
}
|
|
2412
2634
|
/**
|
|
2413
2635
|
* Downloads a blob from the Shelby RPC node.
|
|
@@ -2659,6 +2881,59 @@ var ShelbyClient = class {
|
|
|
2659
2881
|
}
|
|
2660
2882
|
return this._provider;
|
|
2661
2883
|
}
|
|
2884
|
+
/**
|
|
2885
|
+
* Build orderless transaction options (a random replay-protection nonce) for
|
|
2886
|
+
* the per-blob commit transactions. batchUpload finalizes blobs
|
|
2887
|
+
* concurrently, so without orderless replay protection the parallel
|
|
2888
|
+
* same-account submissions collide on the sequence number ("transaction
|
|
2889
|
+
* already in mempool with a different payload"). The nonce makes each commit
|
|
2890
|
+
* independent regardless of the client's `orderless` config flag.
|
|
2891
|
+
*/
|
|
2892
|
+
orderlessCommitOptions() {
|
|
2893
|
+
return {
|
|
2894
|
+
build: {
|
|
2895
|
+
options: {
|
|
2896
|
+
replayProtectionNonce: crypto.getRandomValues(new Uint32Array(1))[0]
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2901
|
+
/**
|
|
2902
|
+
* Resolve the on-chain UID assigned to `blobName` from a committed register
|
|
2903
|
+
* transaction. `register_blob` only publishes the uid -> name mapping via
|
|
2904
|
+
* `BlobRegisteredEvent` (a pending blob is not yet in the `objects` map, so it
|
|
2905
|
+
* cannot be read back by name), so the upload flow must parse it here before
|
|
2906
|
+
* uploading bytes or committing.
|
|
2907
|
+
*/
|
|
2908
|
+
uidFromRegisterTx(tx, account, blobName) {
|
|
2909
|
+
const events = "events" in tx ? tx.events : [];
|
|
2910
|
+
const objectName = createBlobKey({ account, blobName });
|
|
2911
|
+
const match = ShelbyBlobClient.registeredBlobUids(
|
|
2912
|
+
events,
|
|
2913
|
+
this.coordination.deployer
|
|
2914
|
+
).find((registered) => registered.objectName === objectName);
|
|
2915
|
+
if (match === void 0) {
|
|
2916
|
+
throw new Error(
|
|
2917
|
+
`No BlobRegisteredEvent for '${blobName}' in register transaction ${tx.hash}`
|
|
2918
|
+
);
|
|
2919
|
+
}
|
|
2920
|
+
return match.uid;
|
|
2921
|
+
}
|
|
2922
|
+
/**
|
|
2923
|
+
* Await a blob-registration transaction, rephrasing on-chain location
|
|
2924
|
+
* failures into a human-readable message.
|
|
2925
|
+
*/
|
|
2926
|
+
async waitForRegistration(transactionHash) {
|
|
2927
|
+
try {
|
|
2928
|
+
return await this.coordination.aptos.waitForTransaction({
|
|
2929
|
+
transactionHash
|
|
2930
|
+
});
|
|
2931
|
+
} catch (error) {
|
|
2932
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2933
|
+
const described = describeLocationError(message);
|
|
2934
|
+
throw described ? new Error(described, { cause: error }) : error;
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2662
2937
|
/**
|
|
2663
2938
|
* The base URL for the Shelby RPC node.
|
|
2664
2939
|
*/
|
|
@@ -2672,8 +2947,8 @@ var ShelbyClient = class {
|
|
|
2672
2947
|
*
|
|
2673
2948
|
* Note: This method accepts only `Uint8Array` and buffers the entire blob in memory.
|
|
2674
2949
|
* For streaming uploads of large files (e.g. >2 GiB), orchestrate the steps manually
|
|
2675
|
-
* using `generateCommitments()`, `coordination.registerBlob()`,
|
|
2676
|
-
* with a `ReadableStream`.
|
|
2950
|
+
* using `generateCommitments()`, `coordination.registerBlob()`, `rpc.putBlobChunksets()`,
|
|
2951
|
+
* and `coordination.commitObject()` with a `ReadableStream`.
|
|
2677
2952
|
*
|
|
2678
2953
|
* @param params.blobData - The raw data to upload as a Uint8Array.
|
|
2679
2954
|
* @param params.signer - The account that signs and pays for the transaction.
|
|
@@ -2696,34 +2971,57 @@ var ShelbyClient = class {
|
|
|
2696
2971
|
* ```
|
|
2697
2972
|
*/
|
|
2698
2973
|
async upload(params) {
|
|
2699
|
-
const
|
|
2700
|
-
|
|
2701
|
-
|
|
2974
|
+
const provider = await this.getProvider();
|
|
2975
|
+
const blobCommitments = await generateCommitments(
|
|
2976
|
+
provider,
|
|
2977
|
+
params.blobData
|
|
2978
|
+
);
|
|
2979
|
+
const { transaction: pendingRegisterBlobTransaction } = await this.coordination.registerBlob({
|
|
2980
|
+
account: params.signer,
|
|
2981
|
+
blobName: params.blobName,
|
|
2982
|
+
blobMerkleRoot: blobCommitments.blob_merkle_root,
|
|
2983
|
+
size: params.blobData.length,
|
|
2984
|
+
expirationMicros: params.expirationMicros,
|
|
2985
|
+
config: provider.config,
|
|
2986
|
+
options: params.options
|
|
2702
2987
|
});
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2988
|
+
const registerTx = await this.waitForRegistration(
|
|
2989
|
+
pendingRegisterBlobTransaction.hash
|
|
2990
|
+
);
|
|
2991
|
+
const uid = this.uidFromRegisterTx(
|
|
2992
|
+
registerTx,
|
|
2993
|
+
params.signer.accountAddress,
|
|
2994
|
+
params.blobName
|
|
2995
|
+
);
|
|
2996
|
+
const { spAcks } = await this.rpc.putBlobChunksets({
|
|
2997
|
+
account: params.signer,
|
|
2998
|
+
uid,
|
|
2999
|
+
blobData: params.blobData,
|
|
3000
|
+
commitments: blobCommitments,
|
|
3001
|
+
totalBytes: params.blobData.length
|
|
3002
|
+
});
|
|
3003
|
+
const requiredAcks = requiredAckCount(provider.config.erasure_n);
|
|
3004
|
+
if (spAcks.length < requiredAcks) {
|
|
3005
|
+
throw new Error(
|
|
3006
|
+
`Insufficient storage provider acknowledgements for '${params.blobName}': got ${spAcks.length}, need ${requiredAcks} (erasure_d) to finalize on chain`
|
|
2708
3007
|
);
|
|
2709
|
-
const { transaction: pendingRegisterBlobTransaction } = await this.coordination.registerBlob({
|
|
2710
|
-
account: params.signer,
|
|
2711
|
-
blobName: params.blobName,
|
|
2712
|
-
blobMerkleRoot: blobCommitments.blob_merkle_root,
|
|
2713
|
-
size: params.blobData.length,
|
|
2714
|
-
expirationMicros: params.expirationMicros,
|
|
2715
|
-
config: provider.config,
|
|
2716
|
-
options: params.options
|
|
2717
|
-
});
|
|
2718
|
-
await this.coordination.aptos.waitForTransaction({
|
|
2719
|
-
transactionHash: pendingRegisterBlobTransaction.hash
|
|
2720
|
-
});
|
|
2721
3008
|
}
|
|
2722
|
-
await this.
|
|
3009
|
+
const { transaction: pendingCommitTransaction } = await this.coordination.commitObject({
|
|
2723
3010
|
account: params.signer,
|
|
3011
|
+
uid,
|
|
2724
3012
|
blobName: params.blobName,
|
|
2725
|
-
|
|
3013
|
+
overwrite: true,
|
|
3014
|
+
storageProviderAcks: spAcks,
|
|
3015
|
+
options: this.orderlessCommitOptions()
|
|
3016
|
+
});
|
|
3017
|
+
const confirmedTx = await this.coordination.aptos.waitForTransaction({
|
|
3018
|
+
transactionHash: pendingCommitTransaction.hash
|
|
2726
3019
|
});
|
|
3020
|
+
if (!confirmedTx.success) {
|
|
3021
|
+
throw new Error(
|
|
3022
|
+
`commit_object tx failed for '${params.blobName}': ${confirmedTx.vm_status}`
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
2727
3025
|
}
|
|
2728
3026
|
/**
|
|
2729
3027
|
* Uploads a batch of blobs to the Shelby network.
|
|
@@ -2732,8 +3030,8 @@ var ShelbyClient = class {
|
|
|
2732
3030
|
*
|
|
2733
3031
|
* Note: This method accepts only `Uint8Array` and buffers each blob in memory.
|
|
2734
3032
|
* For streaming uploads of large files, orchestrate the steps manually using
|
|
2735
|
-
* `generateCommitments()`, `coordination.registerBlob()`,
|
|
2736
|
-
* with a `ReadableStream`.
|
|
3033
|
+
* `generateCommitments()`, `coordination.registerBlob()`, `rpc.putBlobChunksets()`,
|
|
3034
|
+
* and `coordination.commitObject()` with a `ReadableStream`.
|
|
2737
3035
|
*
|
|
2738
3036
|
* @param params.blobs - The blobs to upload.
|
|
2739
3037
|
* @param params.blobs.blobData - The raw data to upload as a Uint8Array.
|
|
@@ -2758,59 +3056,77 @@ var ShelbyClient = class {
|
|
|
2758
3056
|
* ```
|
|
2759
3057
|
*/
|
|
2760
3058
|
async batchUpload(params) {
|
|
2761
|
-
const
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
3059
|
+
const provider = await this.getProvider();
|
|
3060
|
+
const prepared = await Promise.all(
|
|
3061
|
+
params.blobs.map(async (blob) => ({
|
|
3062
|
+
blob,
|
|
3063
|
+
commitments: await generateCommitments(provider, blob.blobData)
|
|
3064
|
+
}))
|
|
3065
|
+
);
|
|
3066
|
+
const { transaction: pendingRegisterBlobTransaction } = await this.coordination.batchRegisterBlobs({
|
|
3067
|
+
account: params.signer,
|
|
3068
|
+
expirationMicros: params.expirationMicros,
|
|
3069
|
+
blobs: prepared.map((p) => ({
|
|
3070
|
+
blobName: p.blob.blobName,
|
|
3071
|
+
blobSize: p.blob.blobData.length,
|
|
3072
|
+
blobMerkleRoot: p.commitments.blob_merkle_root
|
|
3073
|
+
})),
|
|
3074
|
+
config: provider.config,
|
|
3075
|
+
options: params.options
|
|
2772
3076
|
});
|
|
2773
|
-
const
|
|
2774
|
-
|
|
2775
|
-
(existingBlob) => existingBlob.name === createBlobKey({
|
|
2776
|
-
account: params.signer.accountAddress,
|
|
2777
|
-
blobName: blob.blobName
|
|
2778
|
-
})
|
|
2779
|
-
)
|
|
3077
|
+
const registerTx = await this.waitForRegistration(
|
|
3078
|
+
pendingRegisterBlobTransaction.hash
|
|
2780
3079
|
);
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
const { transaction: pendingRegisterBlobTransaction } = await this.coordination.batchRegisterBlobs({
|
|
2789
|
-
account: params.signer,
|
|
2790
|
-
expirationMicros: params.expirationMicros,
|
|
2791
|
-
blobs: blobsToRegister.map((blob, index) => ({
|
|
2792
|
-
blobName: blob.blobName,
|
|
2793
|
-
blobSize: blob.blobData.length,
|
|
2794
|
-
blobMerkleRoot: blobCommitments[index].blob_merkle_root
|
|
2795
|
-
})),
|
|
2796
|
-
config: provider.config,
|
|
2797
|
-
options: params.options
|
|
2798
|
-
});
|
|
2799
|
-
await this.coordination.aptos.waitForTransaction({
|
|
2800
|
-
transactionHash: pendingRegisterBlobTransaction.hash,
|
|
2801
|
-
options: { waitForIndexer: true }
|
|
2802
|
-
});
|
|
2803
|
-
}
|
|
3080
|
+
const uidByObjectName = new Map(
|
|
3081
|
+
ShelbyBlobClient.registeredBlobUids(
|
|
3082
|
+
"events" in registerTx ? registerTx.events : [],
|
|
3083
|
+
this.coordination.deployer
|
|
3084
|
+
).map((registered) => [registered.objectName, registered.uid])
|
|
3085
|
+
);
|
|
3086
|
+
const requiredAcks = requiredAckCount(provider.config.erasure_n);
|
|
2804
3087
|
const limit = pLimit(3);
|
|
2805
3088
|
await Promise.all(
|
|
2806
|
-
|
|
2807
|
-
(
|
|
2808
|
-
|
|
3089
|
+
prepared.map(
|
|
3090
|
+
(p) => limit(async () => {
|
|
3091
|
+
const objectName = createBlobKey({
|
|
3092
|
+
account: params.signer.accountAddress,
|
|
3093
|
+
blobName: p.blob.blobName
|
|
3094
|
+
});
|
|
3095
|
+
const uid = uidByObjectName.get(objectName);
|
|
3096
|
+
if (uid === void 0) {
|
|
3097
|
+
throw new Error(
|
|
3098
|
+
`No BlobRegisteredEvent for '${p.blob.blobName}' in batch register transaction ${registerTx.hash}`
|
|
3099
|
+
);
|
|
3100
|
+
}
|
|
3101
|
+
const { spAcks } = await this.rpc.putBlobChunksets({
|
|
2809
3102
|
account: params.signer,
|
|
2810
|
-
|
|
2811
|
-
blobData: blob.blobData
|
|
2812
|
-
|
|
2813
|
-
|
|
3103
|
+
uid,
|
|
3104
|
+
blobData: p.blob.blobData,
|
|
3105
|
+
commitments: p.commitments,
|
|
3106
|
+
totalBytes: p.blob.blobData.length
|
|
3107
|
+
});
|
|
3108
|
+
if (spAcks.length < requiredAcks) {
|
|
3109
|
+
throw new Error(
|
|
3110
|
+
`Insufficient storage provider acknowledgements for '${p.blob.blobName}': got ${spAcks.length}, need ${requiredAcks} (erasure_d) to finalize on chain`
|
|
3111
|
+
);
|
|
3112
|
+
}
|
|
3113
|
+
const { transaction: pendingCommitTransaction } = await this.coordination.commitObject({
|
|
3114
|
+
account: params.signer,
|
|
3115
|
+
uid,
|
|
3116
|
+
blobName: p.blob.blobName,
|
|
3117
|
+
overwrite: true,
|
|
3118
|
+
storageProviderAcks: spAcks,
|
|
3119
|
+
options: this.orderlessCommitOptions()
|
|
3120
|
+
});
|
|
3121
|
+
const confirmedTx = await this.coordination.aptos.waitForTransaction({
|
|
3122
|
+
transactionHash: pendingCommitTransaction.hash
|
|
3123
|
+
});
|
|
3124
|
+
if (!confirmedTx.success) {
|
|
3125
|
+
throw new Error(
|
|
3126
|
+
`commit_object tx failed for '${p.blob.blobName}': ${confirmedTx.vm_status}`
|
|
3127
|
+
);
|
|
3128
|
+
}
|
|
3129
|
+
})
|
|
2814
3130
|
)
|
|
2815
3131
|
);
|
|
2816
3132
|
}
|