@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
|
@@ -135,6 +135,18 @@ var ERASURE_CODE_PARAMS = {
|
|
|
135
135
|
enumIndex: 1
|
|
136
136
|
}
|
|
137
137
|
};
|
|
138
|
+
function findErasureSchemeByErasureN(erasureN) {
|
|
139
|
+
return Object.entries(ERASURE_CODE_PARAMS).find(
|
|
140
|
+
([, params]) => params.erasure_n === erasureN
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
function requiredAckCount(erasureN) {
|
|
144
|
+
const scheme = findErasureSchemeByErasureN(erasureN);
|
|
145
|
+
if (!scheme) {
|
|
146
|
+
throw new Error(`Unknown erasure coding scheme with erasure_n=${erasureN}`);
|
|
147
|
+
}
|
|
148
|
+
return scheme[1].erasure_d;
|
|
149
|
+
}
|
|
138
150
|
var DEFAULT_ERASURE_N = ERASURE_CODE_PARAMS["ClayCode_16Total_10Data_13Helper" /* ClayCode_16Total_10Data_13Helper */].erasure_n;
|
|
139
151
|
var DEFAULT_ERASURE_K = ERASURE_CODE_PARAMS["ClayCode_16Total_10Data_13Helper" /* ClayCode_16Total_10Data_13Helper */].erasure_k;
|
|
140
152
|
var DEFAULT_ERASURE_D = ERASURE_CODE_PARAMS["ClayCode_16Total_10Data_13Helper" /* ClayCode_16Total_10Data_13Helper */].erasure_d;
|
|
@@ -431,6 +443,35 @@ function erasureCodingConfig(encodingScheme) {
|
|
|
431
443
|
import { createWasmReedSolomonBinding } from "@shelby-protocol/reed-solomon";
|
|
432
444
|
var DEFAULT_CHUNK_SIZE_BYTES2 = 2 * 1024 * 1024;
|
|
433
445
|
|
|
446
|
+
// src/core/errors.ts
|
|
447
|
+
var ShelbyLocationErrorCodes = {
|
|
448
|
+
E_LOCATION_NOT_FOUND: "E_LOCATION_NOT_FOUND",
|
|
449
|
+
E_SELECTED_LOCATION_CONFLICTS_WITH_LOCK: "E_SELECTED_LOCATION_CONFLICTS_WITH_LOCK",
|
|
450
|
+
E_NO_LOCATION_SELECTED: "E_NO_LOCATION_SELECTED",
|
|
451
|
+
E_LOCATION_WRITES_FROZEN: "E_LOCATION_WRITES_FROZEN",
|
|
452
|
+
E_LOCATION_NOT_ACTIVATED: "E_LOCATION_NOT_ACTIVATED"
|
|
453
|
+
};
|
|
454
|
+
function describeLocationError(errorMessage) {
|
|
455
|
+
if (errorMessage.includes(ShelbyLocationErrorCodes.E_LOCATION_WRITES_FROZEN)) {
|
|
456
|
+
return "Cannot write: the resolved location is frozen and not currently accepting new data.";
|
|
457
|
+
}
|
|
458
|
+
if (errorMessage.includes(ShelbyLocationErrorCodes.E_LOCATION_NOT_ACTIVATED)) {
|
|
459
|
+
return "Cannot write: the resolved location is not activated yet.";
|
|
460
|
+
}
|
|
461
|
+
if (errorMessage.includes(
|
|
462
|
+
ShelbyLocationErrorCodes.E_SELECTED_LOCATION_CONFLICTS_WITH_LOCK
|
|
463
|
+
)) {
|
|
464
|
+
return "Cannot write: the selected location conflicts with the account's locked location preference.";
|
|
465
|
+
}
|
|
466
|
+
if (errorMessage.includes(ShelbyLocationErrorCodes.E_NO_LOCATION_SELECTED)) {
|
|
467
|
+
return "No write location could be resolved: none was selected and the account has no default location or location hint.";
|
|
468
|
+
}
|
|
469
|
+
if (errorMessage.includes(ShelbyLocationErrorCodes.E_LOCATION_NOT_FOUND)) {
|
|
470
|
+
return "Cannot write: the requested location does not exist.";
|
|
471
|
+
}
|
|
472
|
+
return void 0;
|
|
473
|
+
}
|
|
474
|
+
|
|
434
475
|
// src/core/clients/ShelbyBlobClient.ts
|
|
435
476
|
import {
|
|
436
477
|
AccountAddress as AccountAddress2,
|
|
@@ -442,7 +483,7 @@ import {
|
|
|
442
483
|
// src/core/constants.ts
|
|
443
484
|
import { Network } from "@aptos-labs/ts-sdk";
|
|
444
485
|
var NetworkToShelbyRPCBaseUrl = {
|
|
445
|
-
[Network.SHELBYNET]: "https://
|
|
486
|
+
[Network.SHELBYNET]: "https://shelby.shelbynet.shelby.xyz/shelby",
|
|
446
487
|
[Network.NETNA]: void 0,
|
|
447
488
|
[Network.DEVNET]: void 0,
|
|
448
489
|
[Network.TESTNET]: "https://api.testnet.shelby.xyz/shelby",
|
|
@@ -471,6 +512,27 @@ var NetworkToGasStationBaseUrl = {
|
|
|
471
512
|
var SHELBY_DEPLOYER = "0x85fdb9a176ab8ef1d9d9c1b60d60b3924f0800ac1de1cc2085fb0b8bb4988e6a";
|
|
472
513
|
var MICROPAYMENTS_DEPLOYER = "0x1ae7275148bf6ef742b658fd9cbcc2e094201606f4a7bc707bab0201da8043ee";
|
|
473
514
|
|
|
515
|
+
// src/core/types/blobs.ts
|
|
516
|
+
var BLOB_ENCRYPTION_TO_MOVE_ENUM_INDEX = {
|
|
517
|
+
Unencrypted: 0,
|
|
518
|
+
AES_GCM_V1: 1
|
|
519
|
+
};
|
|
520
|
+
function blobEncryptionToMoveEnumIndex(encryption) {
|
|
521
|
+
return BLOB_ENCRYPTION_TO_MOVE_ENUM_INDEX[encryption];
|
|
522
|
+
}
|
|
523
|
+
function blobEncryptionFromMoveVariant(variant) {
|
|
524
|
+
switch (variant) {
|
|
525
|
+
case "Unencrypted":
|
|
526
|
+
return "Unencrypted";
|
|
527
|
+
case "AES_GCM_V1":
|
|
528
|
+
return "AES_GCM_V1";
|
|
529
|
+
default:
|
|
530
|
+
throw new Error(
|
|
531
|
+
"Could not parse encryption from Shelby Smart Contract, this SDK is out of date."
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
474
536
|
// src/core/operations/index.ts
|
|
475
537
|
import { Network as Network2 } from "@aptos-labs/ts-sdk";
|
|
476
538
|
import { GraphQLClient } from "graphql-request";
|
|
@@ -514,18 +576,20 @@ import gql from "graphql-tag";
|
|
|
514
576
|
var GetBlobsDocument = gql`
|
|
515
577
|
query getBlobs($where: blobs_bool_exp, $orderBy: [blobs_order_by!], $limit: Int, $offset: Int) {
|
|
516
578
|
blobs(where: $where, order_by: $orderBy, limit: $limit, offset: $offset) {
|
|
579
|
+
uid
|
|
580
|
+
object_name
|
|
517
581
|
owner
|
|
518
582
|
blob_commitment
|
|
519
|
-
blob_name
|
|
520
583
|
created_at
|
|
521
584
|
expires_at
|
|
585
|
+
updated_at
|
|
522
586
|
num_chunksets
|
|
523
|
-
is_deleted
|
|
524
|
-
is_written
|
|
525
|
-
placement_group
|
|
526
587
|
size
|
|
527
|
-
updated_at
|
|
528
588
|
slice_address
|
|
589
|
+
placement_group
|
|
590
|
+
is_persisted
|
|
591
|
+
is_committed
|
|
592
|
+
is_deleted
|
|
529
593
|
}
|
|
530
594
|
}
|
|
531
595
|
`;
|
|
@@ -537,7 +601,8 @@ var GetBlobActivitiesDocument = gql`
|
|
|
537
601
|
limit: $limit
|
|
538
602
|
offset: $offset
|
|
539
603
|
) {
|
|
540
|
-
|
|
604
|
+
uid
|
|
605
|
+
object_name
|
|
541
606
|
event_index
|
|
542
607
|
event_type
|
|
543
608
|
transaction_hash
|
|
@@ -683,6 +748,22 @@ var MissingTransactionSubmitterError = class extends Error {
|
|
|
683
748
|
this.name = "MissingTransactionSubmitterError";
|
|
684
749
|
}
|
|
685
750
|
};
|
|
751
|
+
var COMMIT_REJECTION_REASONS = /* @__PURE__ */ new Set([
|
|
752
|
+
"AlreadyExists",
|
|
753
|
+
"NoPriorVersion",
|
|
754
|
+
"EtagMismatch"
|
|
755
|
+
]);
|
|
756
|
+
function encryptionFunctionArgs(params) {
|
|
757
|
+
if (params.omitEncryptionArg) {
|
|
758
|
+
if (params.encryption && params.encryption !== "Unencrypted") {
|
|
759
|
+
throw new Error(
|
|
760
|
+
`Blob encryption (${params.encryption}) is not supported on this network: the deployed contract predates the encryption upgrade (#1739).`
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
return [];
|
|
764
|
+
}
|
|
765
|
+
return [blobEncryptionToMoveEnumIndex(params.encryption ?? "Unencrypted")];
|
|
766
|
+
}
|
|
686
767
|
var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
687
768
|
aptos;
|
|
688
769
|
deployer;
|
|
@@ -731,7 +812,10 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
731
812
|
this.aptos = new Aptos(getAptosConfig(config));
|
|
732
813
|
this.deployer = config.deployer ?? AccountAddress2.fromString(SHELBY_DEPLOYER);
|
|
733
814
|
this.indexer = getShelbyIndexerClient(config);
|
|
734
|
-
this.defaultOptions =
|
|
815
|
+
this.defaultOptions = {
|
|
816
|
+
locationHint: config.locationHint,
|
|
817
|
+
...defaultOptions
|
|
818
|
+
};
|
|
735
819
|
this.orderless = config.orderless ?? false;
|
|
736
820
|
}
|
|
737
821
|
/**
|
|
@@ -742,7 +826,9 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
742
826
|
build: options?.build ?? this.defaultOptions.build,
|
|
743
827
|
submit: options?.submit ?? this.defaultOptions.submit,
|
|
744
828
|
usdSponsor: options?.usdSponsor ?? this.defaultOptions.usdSponsor,
|
|
745
|
-
chunksetSizeBytes: options?.chunksetSizeBytes ?? this.defaultOptions.chunksetSizeBytes
|
|
829
|
+
chunksetSizeBytes: options?.chunksetSizeBytes ?? this.defaultOptions.chunksetSizeBytes,
|
|
830
|
+
selectedLocation: options?.selectedLocation ?? this.defaultOptions.selectedLocation,
|
|
831
|
+
locationHint: options?.locationHint ?? this.defaultOptions.locationHint
|
|
746
832
|
};
|
|
747
833
|
}
|
|
748
834
|
/**
|
|
@@ -784,17 +870,17 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
784
870
|
*
|
|
785
871
|
* @example
|
|
786
872
|
* ```typescript
|
|
787
|
-
* const metadata = await client.
|
|
873
|
+
* const metadata = await client.getFullObjectMetadata({
|
|
788
874
|
* account: AccountAddress.fromString("0x1"),
|
|
789
875
|
* name: "foo/bar.txt",
|
|
790
876
|
* });
|
|
791
877
|
* ```
|
|
792
878
|
*/
|
|
793
|
-
async
|
|
879
|
+
async getFullObjectMetadata(params) {
|
|
794
880
|
try {
|
|
795
881
|
const rawMetadata = await this.aptos.view({
|
|
796
882
|
payload: {
|
|
797
|
-
function: `${this.deployer.toString()}::blob_metadata::
|
|
883
|
+
function: `${this.deployer.toString()}::blob_metadata::get_full_object_metadata`,
|
|
798
884
|
functionArguments: [
|
|
799
885
|
createBlobKey({
|
|
800
886
|
account: params.account,
|
|
@@ -806,47 +892,94 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
806
892
|
if (!rawMetadata?.[0]?.vec?.[0]) {
|
|
807
893
|
return void 0;
|
|
808
894
|
}
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
encoding = {
|
|
813
|
-
variant: "clay",
|
|
814
|
-
...ERASURE_CODE_PARAMS[metadata.encoding.__variant__],
|
|
815
|
-
...ERASURE_CODE_AND_CHUNK_MAPPING[metadata.encoding.__variant__]
|
|
816
|
-
};
|
|
817
|
-
} else if (metadata.encoding.__variant__ === "ClayCode_4Total_2Data_3Helper") {
|
|
818
|
-
encoding = {
|
|
819
|
-
variant: "clay",
|
|
820
|
-
...ERASURE_CODE_PARAMS[metadata.encoding.__variant__],
|
|
821
|
-
...ERASURE_CODE_AND_CHUNK_MAPPING[metadata.encoding.__variant__]
|
|
822
|
-
};
|
|
823
|
-
} else {
|
|
824
|
-
throw new Error(
|
|
825
|
-
"Could not parse encoding from Shelby Smart Contract, this SDK is out of date."
|
|
826
|
-
);
|
|
827
|
-
}
|
|
828
|
-
return {
|
|
829
|
-
blobMerkleRoot: Hex3.fromHexInput(
|
|
830
|
-
metadata.blob_commitment
|
|
831
|
-
).toUint8Array(),
|
|
832
|
-
owner: normalizeAddress(metadata.owner),
|
|
895
|
+
const view = rawMetadata[0].vec[0];
|
|
896
|
+
return this.parseBlobMetadata(view.blob_metadata, {
|
|
897
|
+
uid: BigInt(view.object_metadata.current_blob_uid),
|
|
833
898
|
name: params.name,
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
creationMicros: Number(metadata.creation_micros),
|
|
839
|
-
sliceAddress: normalizeAddress(metadata.slice.inner),
|
|
840
|
-
isWritten: metadata.is_written
|
|
841
|
-
};
|
|
899
|
+
// Any blob bound under an object name has cleared the commit-time
|
|
900
|
+
// `is_written()` check, so a resolved object is always written.
|
|
901
|
+
isWritten: true
|
|
902
|
+
});
|
|
842
903
|
} catch (error) {
|
|
843
904
|
if (error instanceof Error && // Depending on the network, the error message may show up differently.
|
|
844
|
-
(error.message?.includes("sub_status: Some(404)") || error.message?.includes("EBLOB_NOT_FOUND"))) {
|
|
905
|
+
(error.message?.includes("sub_status: Some(404)") || error.message?.includes("EBLOB_NOT_FOUND") || error.message?.includes("EOBJECT_NOT_FOUND"))) {
|
|
906
|
+
return void 0;
|
|
907
|
+
}
|
|
908
|
+
throw error;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Retrieves blob metadata directly by its on-chain UID, including blobs in
|
|
913
|
+
* the pending (registered-but-not-yet-committed) state that
|
|
914
|
+
* {@link getFullObjectMetadata} cannot resolve by object name. Returns `undefined`
|
|
915
|
+
* if no blob has that UID.
|
|
916
|
+
*
|
|
917
|
+
* The returned `name`/`blobNameSuffix` are empty: the blob layer is keyed by
|
|
918
|
+
* UID and carries no object name (a name binding is established only at
|
|
919
|
+
* commit). `isWritten` reflects whether the blob has been committed.
|
|
920
|
+
*/
|
|
921
|
+
async getFullObjectMetadataByUid(uid) {
|
|
922
|
+
try {
|
|
923
|
+
const rawMetadata = await this.aptos.view(
|
|
924
|
+
{
|
|
925
|
+
payload: {
|
|
926
|
+
function: `${this.deployer.toString()}::blob_metadata::get_blob_metadata`,
|
|
927
|
+
functionArguments: [uid]
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
);
|
|
931
|
+
if (!rawMetadata?.[0]?.vec?.[0]) {
|
|
932
|
+
return void 0;
|
|
933
|
+
}
|
|
934
|
+
const raw = rawMetadata[0].vec[0];
|
|
935
|
+
return this.parseBlobMetadata(raw, {
|
|
936
|
+
uid,
|
|
937
|
+
name: "",
|
|
938
|
+
isWritten: raw.state.__variant__ === "CommittedObject"
|
|
939
|
+
});
|
|
940
|
+
} catch (error) {
|
|
941
|
+
if (error instanceof Error && (error.message?.includes("sub_status: Some(404)") || error.message?.includes("EBLOB_NOT_FOUND"))) {
|
|
845
942
|
return void 0;
|
|
846
943
|
}
|
|
847
944
|
throw error;
|
|
848
945
|
}
|
|
849
946
|
}
|
|
947
|
+
/**
|
|
948
|
+
* Parse the on-chain `BlobMetadata::V1` view shape into the SDK
|
|
949
|
+
* {@link FullObjectMetadata}. `uid` / `name` / `isWritten` are supplied by the
|
|
950
|
+
* caller since they depend on the lookup path (by object name vs by UID).
|
|
951
|
+
*/
|
|
952
|
+
parseBlobMetadata(raw, extra) {
|
|
953
|
+
const { content } = raw;
|
|
954
|
+
const variant = content.encoding.__variant__;
|
|
955
|
+
if (variant !== "ClayCode_16Total_10Data_13Helper" && variant !== "ClayCode_4Total_2Data_3Helper") {
|
|
956
|
+
throw new Error(
|
|
957
|
+
"Could not parse encoding from Shelby Smart Contract, this SDK is out of date."
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
const encoding = {
|
|
961
|
+
variant: "clay",
|
|
962
|
+
...ERASURE_CODE_PARAMS[variant],
|
|
963
|
+
...ERASURE_CODE_AND_CHUNK_MAPPING[variant]
|
|
964
|
+
};
|
|
965
|
+
const encryption = blobEncryptionFromMoveVariant(
|
|
966
|
+
content.encryption?.__variant__ ?? "Unencrypted"
|
|
967
|
+
);
|
|
968
|
+
return {
|
|
969
|
+
uid: extra.uid,
|
|
970
|
+
blobMerkleRoot: Hex3.fromHexInput(content.blob_commitment).toUint8Array(),
|
|
971
|
+
owner: normalizeAddress(raw.owner),
|
|
972
|
+
name: extra.name,
|
|
973
|
+
blobNameSuffix: extra.name ? getBlobNameSuffix(extra.name) : "",
|
|
974
|
+
size: Number(content.blob_size),
|
|
975
|
+
encoding,
|
|
976
|
+
encryption,
|
|
977
|
+
expirationMicros: Number(raw.expiration_micros),
|
|
978
|
+
creationMicros: Number(raw.creation_micros),
|
|
979
|
+
sliceAddress: normalizeAddress(raw.slice.inner),
|
|
980
|
+
isWritten: extra.isWritten
|
|
981
|
+
};
|
|
982
|
+
}
|
|
850
983
|
/**
|
|
851
984
|
* Retrieves all the blobs and their metadata for an account from the
|
|
852
985
|
* blockchain.
|
|
@@ -858,7 +991,7 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
858
991
|
*
|
|
859
992
|
* @example
|
|
860
993
|
* ```typescript
|
|
861
|
-
* //
|
|
994
|
+
* // FullObjectMetadata[]
|
|
862
995
|
* const blobs = await client.getAccountBlobs({
|
|
863
996
|
* account: AccountAddress.fromString("0x1"),
|
|
864
997
|
* });
|
|
@@ -875,6 +1008,25 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
875
1008
|
orderBy: rest.orderBy
|
|
876
1009
|
});
|
|
877
1010
|
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Object-facing default filter: only committed, non-deleted, unexpired rows.
|
|
1013
|
+
* The blobs table is UID-keyed, so during an atomic overwrite a single
|
|
1014
|
+
* object_name transiently has two non-deleted rows — the currently committed
|
|
1015
|
+
* blob and the new pending (is_committed = "0") blob. Filtering on
|
|
1016
|
+
* is_committed keeps name lookups/listings pinned to the committed object and
|
|
1017
|
+
* avoids returning or duplicating the in-flight pending row. Applied to
|
|
1018
|
+
* getBlobs *and* the getBlobsCount / getTotalBlobsSize aggregates so counts
|
|
1019
|
+
* and sizes agree with the listing mid-overwrite. Callers can override any
|
|
1020
|
+
* key (e.g. is_committed) via their own `where`.
|
|
1021
|
+
*/
|
|
1022
|
+
activeBlobsWhere(where) {
|
|
1023
|
+
const defaultActiveFilter = {
|
|
1024
|
+
expires_at: { _gte: String(Date.now() * 1e3) },
|
|
1025
|
+
is_deleted: { _eq: "0" },
|
|
1026
|
+
is_committed: { _eq: "1" }
|
|
1027
|
+
};
|
|
1028
|
+
return { ...defaultActiveFilter, ...where };
|
|
1029
|
+
}
|
|
878
1030
|
/**
|
|
879
1031
|
* Retrieves blobs and their metadata from the blockchain.
|
|
880
1032
|
*
|
|
@@ -885,7 +1037,7 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
885
1037
|
*
|
|
886
1038
|
* @example
|
|
887
1039
|
* ```typescript
|
|
888
|
-
* //
|
|
1040
|
+
* // FullObjectMetadata[]
|
|
889
1041
|
* const blobs = await client.getBlobs({
|
|
890
1042
|
* where: { owner: { _eq: AccountAddress.fromString("0x1").toString() } },
|
|
891
1043
|
* });
|
|
@@ -894,12 +1046,7 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
894
1046
|
async getBlobs(params = {}) {
|
|
895
1047
|
const { limit, offset } = params.pagination ?? {};
|
|
896
1048
|
const { orderBy, where } = params;
|
|
897
|
-
const
|
|
898
|
-
const defaultActiveFilter = {
|
|
899
|
-
expires_at: { _gte: currentMicros },
|
|
900
|
-
is_deleted: { _eq: "0" }
|
|
901
|
-
};
|
|
902
|
-
const finalWhere = where !== void 0 ? { ...defaultActiveFilter, ...where } : defaultActiveFilter;
|
|
1049
|
+
const finalWhere = this.activeBlobsWhere(where);
|
|
903
1050
|
const { blobs } = await this.indexer.getBlobs({
|
|
904
1051
|
where: finalWhere,
|
|
905
1052
|
limit,
|
|
@@ -908,9 +1055,10 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
908
1055
|
});
|
|
909
1056
|
return blobs.map(
|
|
910
1057
|
(blob) => ({
|
|
1058
|
+
uid: BigInt(blob.uid),
|
|
911
1059
|
owner: normalizeAddress(blob.owner),
|
|
912
|
-
name: blob.
|
|
913
|
-
blobNameSuffix: getBlobNameSuffix(blob.
|
|
1060
|
+
name: blob.object_name,
|
|
1061
|
+
blobNameSuffix: getBlobNameSuffix(blob.object_name),
|
|
914
1062
|
blobMerkleRoot: Hex3.fromHexInput(blob.blob_commitment).toUint8Array(),
|
|
915
1063
|
size: Number(blob.size),
|
|
916
1064
|
// TODO: Add encoding when supported in NCI
|
|
@@ -922,7 +1070,7 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
922
1070
|
expirationMicros: Number(blob.expires_at),
|
|
923
1071
|
creationMicros: Number(blob.created_at),
|
|
924
1072
|
sliceAddress: normalizeAddress(blob.slice_address),
|
|
925
|
-
isWritten: Boolean(Number(blob.
|
|
1073
|
+
isWritten: Boolean(Number(blob.is_persisted)),
|
|
926
1074
|
isDeleted: Boolean(Number(blob.is_deleted))
|
|
927
1075
|
})
|
|
928
1076
|
);
|
|
@@ -940,12 +1088,15 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
940
1088
|
[`${this.deployer.toStringLong()}::blob_metadata::BlobRegisteredEvent`]: "register_blob",
|
|
941
1089
|
[`${this.deployer.toStringLong()}::blob_metadata::BlobDeletedEvent`]: "delete_blob",
|
|
942
1090
|
[`${this.deployer.toStringLong()}::blob_metadata::BlobExpirationExtendedEvent`]: "extend_blob_expiration",
|
|
943
|
-
[`${this.deployer.toStringLong()}::blob_metadata::
|
|
1091
|
+
[`${this.deployer.toStringLong()}::blob_metadata::BlobPersistedEvent`]: "write_blob",
|
|
1092
|
+
[`${this.deployer.toStringLong()}::blob_metadata::ObjectCommittedEvent`]: "commit_object",
|
|
1093
|
+
[`${this.deployer.toStringLong()}::blob_metadata::ObjectDeletedEvent`]: "delete_object",
|
|
1094
|
+
[`${this.deployer.toStringLong()}::blob_metadata::ObjectCommitRejectedEvent`]: "reject_object_commit"
|
|
944
1095
|
};
|
|
945
1096
|
return blob_activities.map(
|
|
946
1097
|
(activity) => ({
|
|
947
|
-
blobName: activity.
|
|
948
|
-
accountAddress: normalizeAddress(activity.
|
|
1098
|
+
blobName: activity.object_name,
|
|
1099
|
+
accountAddress: normalizeAddress(activity.object_name.substring(1, 65)),
|
|
949
1100
|
type: activityTypeMapping[activity.event_type] ?? "unknown",
|
|
950
1101
|
eventType: activity.event_type,
|
|
951
1102
|
eventIndex: Number(activity.event_index),
|
|
@@ -968,9 +1119,10 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
968
1119
|
* const count = await client.getBlobsCount();
|
|
969
1120
|
* ```
|
|
970
1121
|
*/
|
|
971
|
-
async getBlobsCount(params) {
|
|
972
|
-
const {
|
|
973
|
-
|
|
1122
|
+
async getBlobsCount(params = {}) {
|
|
1123
|
+
const { blobs_aggregate } = await this.indexer.getBlobsCount({
|
|
1124
|
+
where: this.activeBlobsWhere(params.where)
|
|
1125
|
+
});
|
|
974
1126
|
return blobs_aggregate?.aggregate?.count ?? 0;
|
|
975
1127
|
}
|
|
976
1128
|
/**
|
|
@@ -985,8 +1137,9 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
985
1137
|
* ```
|
|
986
1138
|
*/
|
|
987
1139
|
async getTotalBlobsSize(params = {}) {
|
|
988
|
-
const {
|
|
989
|
-
|
|
1140
|
+
const { blobs_aggregate } = await this.indexer.getTotalBlobsSize({
|
|
1141
|
+
where: this.activeBlobsWhere(params.where)
|
|
1142
|
+
});
|
|
990
1143
|
return Number(blobs_aggregate?.aggregate?.sum?.size ?? 0);
|
|
991
1144
|
}
|
|
992
1145
|
/**
|
|
@@ -1045,12 +1198,15 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1045
1198
|
deployer: this.deployer,
|
|
1046
1199
|
account: params.account.accountAddress,
|
|
1047
1200
|
blobName: params.blobName,
|
|
1201
|
+
selectedLocation: options.selectedLocation,
|
|
1202
|
+
locationHint: options.locationHint,
|
|
1048
1203
|
blobSize: params.size,
|
|
1049
1204
|
blobMerkleRoot: params.blobMerkleRoot,
|
|
1050
1205
|
numChunksets: expectedTotalChunksets(params.size, chunksetSize),
|
|
1051
1206
|
expirationMicros: params.expirationMicros,
|
|
1052
1207
|
useSponsoredUsdVariant: options.usdSponsor !== void 0,
|
|
1053
|
-
encoding: config.enumIndex
|
|
1208
|
+
encoding: config.enumIndex,
|
|
1209
|
+
encryption: params.encryption
|
|
1054
1210
|
}),
|
|
1055
1211
|
sender: params.account.accountAddress
|
|
1056
1212
|
};
|
|
@@ -1078,16 +1234,16 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1078
1234
|
* @example
|
|
1079
1235
|
* ```typescript
|
|
1080
1236
|
*
|
|
1081
|
-
* const { transaction } = await client.
|
|
1237
|
+
* const { transaction } = await client.deleteObject({
|
|
1082
1238
|
* account: signer,
|
|
1083
1239
|
* blobName: "foo/bar.txt",
|
|
1084
1240
|
* });
|
|
1085
1241
|
* ```
|
|
1086
1242
|
*/
|
|
1087
|
-
async
|
|
1243
|
+
async deleteObject(params) {
|
|
1088
1244
|
const transaction = await this.aptos.transaction.build.simple({
|
|
1089
1245
|
options: this.orderlessTxOptions(params.options),
|
|
1090
|
-
data: _ShelbyBlobClient.
|
|
1246
|
+
data: _ShelbyBlobClient.createDeleteObjectPayload({
|
|
1091
1247
|
deployer: this.deployer,
|
|
1092
1248
|
blobName: params.blobName
|
|
1093
1249
|
}),
|
|
@@ -1103,10 +1259,6 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1103
1259
|
/**
|
|
1104
1260
|
* Deletes multiple blobs on the blockchain in a single atomic transaction.
|
|
1105
1261
|
*
|
|
1106
|
-
* **Note:** This function requires the `delete_multiple_blobs` entry function
|
|
1107
|
-
* which will be deployed to the smart contract on 2026-02-04. Using this
|
|
1108
|
-
* function before that date will result in a transaction failure.
|
|
1109
|
-
*
|
|
1110
1262
|
* This operation is atomic: if any blob deletion fails (e.g., blob not found),
|
|
1111
1263
|
* the entire transaction fails and no blobs are deleted.
|
|
1112
1264
|
*
|
|
@@ -1121,16 +1273,16 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1121
1273
|
* @example
|
|
1122
1274
|
* ```typescript
|
|
1123
1275
|
*
|
|
1124
|
-
* const { transaction } = await client.
|
|
1276
|
+
* const { transaction } = await client.deleteMultipleObjects({
|
|
1125
1277
|
* account: signer,
|
|
1126
1278
|
* blobNames: ["foo/bar.txt", "baz.txt"],
|
|
1127
1279
|
* });
|
|
1128
1280
|
* ```
|
|
1129
1281
|
*/
|
|
1130
|
-
async
|
|
1282
|
+
async deleteMultipleObjects(params) {
|
|
1131
1283
|
const transaction = await this.aptos.transaction.build.simple({
|
|
1132
1284
|
options: this.orderlessTxOptions(params.options),
|
|
1133
|
-
data: _ShelbyBlobClient.
|
|
1285
|
+
data: _ShelbyBlobClient.createDeleteMultipleObjectsPayload({
|
|
1134
1286
|
deployer: this.deployer,
|
|
1135
1287
|
blobNames: params.blobNames
|
|
1136
1288
|
}),
|
|
@@ -1188,6 +1340,8 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1188
1340
|
data: _ShelbyBlobClient.createBatchRegisterBlobsPayload({
|
|
1189
1341
|
deployer: this.deployer,
|
|
1190
1342
|
account: params.account.accountAddress,
|
|
1343
|
+
selectedLocation: options.selectedLocation,
|
|
1344
|
+
locationHint: options.locationHint,
|
|
1191
1345
|
expirationMicros: params.expirationMicros,
|
|
1192
1346
|
blobs: params.blobs.map((blob) => ({
|
|
1193
1347
|
blobName: blob.blobName,
|
|
@@ -1196,7 +1350,8 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1196
1350
|
numChunksets: expectedTotalChunksets(blob.blobSize, chunksetSize)
|
|
1197
1351
|
})),
|
|
1198
1352
|
useSponsoredUsdVariant: options.usdSponsor !== void 0,
|
|
1199
|
-
encoding: config.enumIndex
|
|
1353
|
+
encoding: config.enumIndex,
|
|
1354
|
+
encryption: params.encryption
|
|
1200
1355
|
})
|
|
1201
1356
|
};
|
|
1202
1357
|
const transaction = options.usdSponsor ? await this.aptos.transaction.build.multiAgent({
|
|
@@ -1211,6 +1366,55 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1211
1366
|
})
|
|
1212
1367
|
};
|
|
1213
1368
|
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Extracts the on-chain UIDs assigned at registration from a committed
|
|
1371
|
+
* register transaction's events.
|
|
1372
|
+
*
|
|
1373
|
+
* `register_blob` / `register_multiple_blobs` create *pending* blobs and emit
|
|
1374
|
+
* one `BlobRegisteredEvent` per blob. The UID is published only on this event
|
|
1375
|
+
* (the blob is not yet in the `objects` map, so it cannot be read back by
|
|
1376
|
+
* name), so callers must parse it here before uploading bytes or committing.
|
|
1377
|
+
*
|
|
1378
|
+
* @param events - The committed transaction's events (from `waitForTransaction`).
|
|
1379
|
+
* @param deployer - The contract deployer address.
|
|
1380
|
+
* @returns One entry per registered blob, keyed by its full object name
|
|
1381
|
+
* (`@<owner>/<suffix>`, matching {@link createBlobKey}).
|
|
1382
|
+
*/
|
|
1383
|
+
static registeredBlobUids(events, deployer) {
|
|
1384
|
+
const eventType = `${deployer.toStringLong()}::blob_metadata::BlobRegisteredEvent`;
|
|
1385
|
+
return events.filter((event) => event.type === eventType).map((event) => {
|
|
1386
|
+
const data = event.data;
|
|
1387
|
+
return { objectName: data.object_name, uid: BigInt(data.uid) };
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Detects whether `commit_object` rejected the write for `uid` rather than
|
|
1392
|
+
* applying it. A rejected commit is still a *successful* transaction — the
|
|
1393
|
+
* contract tears down the pending blob and emits `ObjectCommitRejectedEvent`
|
|
1394
|
+
* instead of aborting — so callers must inspect the finalized transaction's
|
|
1395
|
+
* events to tell a durable write apart from a silent no-op.
|
|
1396
|
+
*
|
|
1397
|
+
* @param events - The committed transaction's events (from `waitForTransaction`).
|
|
1398
|
+
* @param deployer - The contract deployer address.
|
|
1399
|
+
* @param uid - The UID passed to `commit_object`.
|
|
1400
|
+
* @returns The rejection reason, or `undefined` if the commit was applied.
|
|
1401
|
+
*/
|
|
1402
|
+
static findObjectCommitRejection(events, deployer, uid) {
|
|
1403
|
+
const eventType = `${deployer.toStringLong()}::blob_metadata::ObjectCommitRejectedEvent`;
|
|
1404
|
+
const rejection = events.find(
|
|
1405
|
+
(event) => event.type === eventType && BigInt(event.data.uid) === uid
|
|
1406
|
+
);
|
|
1407
|
+
if (!rejection) {
|
|
1408
|
+
return void 0;
|
|
1409
|
+
}
|
|
1410
|
+
const reason = rejection.data.rejection_reason.__variant__;
|
|
1411
|
+
if (!COMMIT_REJECTION_REASONS.has(reason)) {
|
|
1412
|
+
throw new Error(
|
|
1413
|
+
`Unrecognized ObjectCommitRejectedEvent rejection_reason '${reason}' for uid ${uid}`
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
return reason;
|
|
1417
|
+
}
|
|
1214
1418
|
/**
|
|
1215
1419
|
* Creates a transaction payload to register a blob on the blockchain.
|
|
1216
1420
|
* This is a static helper method for constructing the Move function call payload.
|
|
@@ -1224,8 +1428,6 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1224
1428
|
* @param params.numChunksets - The total number of chunksets in the blob.
|
|
1225
1429
|
*
|
|
1226
1430
|
* @returns An Aptos transaction payload data object for the register_blob Move function.
|
|
1227
|
-
*
|
|
1228
|
-
* @see https://github.com/shelby/shelby/blob/e08e84742cf2b80ad8bb7227deb3013398076d53/move/shelby_contract/sources/global_metadata.move#L357
|
|
1229
1431
|
*/
|
|
1230
1432
|
static createRegisterBlobPayload(params) {
|
|
1231
1433
|
const functionName = params.useSponsoredUsdVariant ? "register_blob_with_sponsor" : "register_blob";
|
|
@@ -1233,6 +1435,8 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1233
1435
|
function: `${(params.deployer ?? SHELBY_DEPLOYER).toString()}::blob_metadata::${functionName}`,
|
|
1234
1436
|
functionArguments: [
|
|
1235
1437
|
params.blobName,
|
|
1438
|
+
params.selectedLocation ?? null,
|
|
1439
|
+
params.locationHint ?? null,
|
|
1236
1440
|
params.expirationMicros,
|
|
1237
1441
|
Hex3.fromHexString(params.blobMerkleRoot).toUint8Array(),
|
|
1238
1442
|
params.numChunksets,
|
|
@@ -1240,7 +1444,8 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1240
1444
|
// TODO
|
|
1241
1445
|
0,
|
|
1242
1446
|
// payment tier
|
|
1243
|
-
params.encoding
|
|
1447
|
+
params.encoding,
|
|
1448
|
+
...encryptionFunctionArgs(params)
|
|
1244
1449
|
]
|
|
1245
1450
|
};
|
|
1246
1451
|
}
|
|
@@ -1258,8 +1463,6 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1258
1463
|
* @param params.blobs.numChunksets - The total number of chunksets in the blob.
|
|
1259
1464
|
*
|
|
1260
1465
|
* @returns An Aptos transaction payload data object for the register_multiple_blobs Move function.
|
|
1261
|
-
*
|
|
1262
|
-
* @see https://github.com/shelby/shelby/blob/e08e84742cf2b80ad8bb7227deb3013398076d53/move/shelby_contract/sources/global_metadata.move#L357
|
|
1263
1466
|
*/
|
|
1264
1467
|
static createBatchRegisterBlobsPayload(params) {
|
|
1265
1468
|
const functionName = params.useSponsoredUsdVariant ? "register_multiple_blobs_with_sponsor" : "register_multiple_blobs";
|
|
@@ -1279,6 +1482,8 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1279
1482
|
function: `${(params.deployer ?? SHELBY_DEPLOYER).toString()}::blob_metadata::${functionName}`,
|
|
1280
1483
|
functionArguments: [
|
|
1281
1484
|
blobNames,
|
|
1485
|
+
params.selectedLocation ?? null,
|
|
1486
|
+
params.locationHint ?? null,
|
|
1282
1487
|
params.expirationMicros,
|
|
1283
1488
|
blobMerkleRoots,
|
|
1284
1489
|
blobNumChunksets,
|
|
@@ -1286,7 +1491,8 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1286
1491
|
// TODO
|
|
1287
1492
|
0,
|
|
1288
1493
|
// payment tier
|
|
1289
|
-
params.encoding
|
|
1494
|
+
params.encoding,
|
|
1495
|
+
...encryptionFunctionArgs(params)
|
|
1290
1496
|
]
|
|
1291
1497
|
};
|
|
1292
1498
|
}
|
|
@@ -1297,24 +1503,20 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1297
1503
|
* @param params.deployer - Optional deployer account address. Defaults to SHELBY_DEPLOYER.
|
|
1298
1504
|
* @param params.blobName - The blob name (e.g. "bar.txt", without the account address prefix).
|
|
1299
1505
|
*
|
|
1300
|
-
* @returns An Aptos transaction payload data object for the
|
|
1301
|
-
*
|
|
1302
|
-
* @see https://github.com/shelby/shelby/blob/64e9d7b4f0005e586faeb1e4085c79159234b6b6/move/shelby_contract/sources/global_metadata.move#L616
|
|
1506
|
+
* @returns An Aptos transaction payload data object for the delete_object Move function.
|
|
1303
1507
|
*/
|
|
1304
|
-
static
|
|
1508
|
+
static createDeleteObjectPayload(params) {
|
|
1305
1509
|
return {
|
|
1306
|
-
function: `${(params.deployer ?? SHELBY_DEPLOYER).toString()}::blob_metadata::
|
|
1307
|
-
|
|
1510
|
+
function: `${(params.deployer ?? SHELBY_DEPLOYER).toString()}::blob_metadata::delete_object`,
|
|
1511
|
+
// Second arg is `if_match_etag: Option<vector<u8>>`; `null` => None
|
|
1512
|
+
// (unconditional delete).
|
|
1513
|
+
functionArguments: [params.blobName, null]
|
|
1308
1514
|
};
|
|
1309
1515
|
}
|
|
1310
1516
|
/**
|
|
1311
1517
|
* Creates a transaction payload to delete multiple blobs on the blockchain.
|
|
1312
1518
|
* This is a static helper method for constructing the Move function call payload.
|
|
1313
1519
|
*
|
|
1314
|
-
* **Note:** This function requires the `delete_multiple_blobs` entry function
|
|
1315
|
-
* which will be deployed to the smart contract on 2026-02-04. Using this
|
|
1316
|
-
* function before that date will result in a transaction failure.
|
|
1317
|
-
*
|
|
1318
1520
|
* This operation is atomic: if any blob deletion fails (e.g., blob not found),
|
|
1319
1521
|
* the entire transaction fails and no blobs are deleted.
|
|
1320
1522
|
*
|
|
@@ -1323,42 +1525,71 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
|
|
|
1323
1525
|
* (e.g. ["foo/bar.txt", "baz.txt"], NOT ["0x1/foo/bar.txt"]). The account address
|
|
1324
1526
|
* prefix is automatically derived from the transaction sender.
|
|
1325
1527
|
*
|
|
1326
|
-
* @returns An Aptos transaction payload data object for the
|
|
1327
|
-
*
|
|
1328
|
-
* @see https://github.com/shelby/shelby/blob/main/move/shelby_contract/sources/blob_metadata.move
|
|
1528
|
+
* @returns An Aptos transaction payload data object for the delete_multiple_objects Move function.
|
|
1329
1529
|
*/
|
|
1330
|
-
static
|
|
1530
|
+
static createDeleteMultipleObjectsPayload(params) {
|
|
1331
1531
|
return {
|
|
1332
|
-
function: `${(params.deployer ?? SHELBY_DEPLOYER).toString()}::blob_metadata::
|
|
1333
|
-
|
|
1532
|
+
function: `${(params.deployer ?? SHELBY_DEPLOYER).toString()}::blob_metadata::delete_multiple_objects`,
|
|
1533
|
+
// Second arg is `if_match_etags: vector<Option<vector<u8>>>`; an empty
|
|
1534
|
+
// vector means "no etag check on any object" (unconditional delete).
|
|
1535
|
+
functionArguments: [params.blobNames, []]
|
|
1334
1536
|
};
|
|
1335
1537
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1538
|
+
/**
|
|
1539
|
+
* Sort acks by slot and reduce to the `(ack_bits, signatures)` pair the
|
|
1540
|
+
* contract expects: it walks the set bits low-to-high and consumes the
|
|
1541
|
+
* signatures in that same order.
|
|
1542
|
+
*/
|
|
1543
|
+
static encodeAcks(storageProviderAcks) {
|
|
1544
|
+
const sortedAcks = [...storageProviderAcks].sort((a, b) => a.slot - b.slot);
|
|
1545
|
+
const ackBitMask = sortedAcks.reduce(
|
|
1338
1546
|
(acc, ack) => acc | 1 << ack.slot,
|
|
1339
1547
|
0
|
|
1340
1548
|
);
|
|
1341
1549
|
return {
|
|
1342
|
-
|
|
1550
|
+
ackBits: new U32(Number(ackBitMask)),
|
|
1551
|
+
signatures: sortedAcks.map((ack) => ack.signature)
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Payload for `commit_object(uid, object_name_suffix, overwrite, if_match_etag,
|
|
1556
|
+
* ack_bits, signatures)` — binds a written pending blob under its object name,
|
|
1557
|
+
* finalizing the upload. SP acks may be batched in here (the contract applies
|
|
1558
|
+
* them before the `is_written` check), so register → upload → commit needs
|
|
1559
|
+
* only a single finalize transaction.
|
|
1560
|
+
*
|
|
1561
|
+
* @param params.uid - The blob UID returned at registration.
|
|
1562
|
+
* @param params.blobName - The object name suffix the blob was registered under.
|
|
1563
|
+
* @param params.overwrite - Allow replacing an existing binding under this name.
|
|
1564
|
+
* @param params.storageProviderAcks - Acks applied atomically with the commit.
|
|
1565
|
+
*/
|
|
1566
|
+
static createCommitObjectPayload(params) {
|
|
1567
|
+
const { ackBits, signatures } = _ShelbyBlobClient.encodeAcks(
|
|
1568
|
+
params.storageProviderAcks
|
|
1569
|
+
);
|
|
1570
|
+
return {
|
|
1571
|
+
function: `${(params.deployer ?? SHELBY_DEPLOYER).toString()}::blob_metadata::commit_object`,
|
|
1343
1572
|
functionArguments: [
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1573
|
+
params.uid,
|
|
1574
|
+
params.blobName,
|
|
1575
|
+
params.overwrite,
|
|
1576
|
+
// `if_match_etag: Option<vector<u8>>` is `null` => None; conditional
|
|
1577
|
+
// (CAS) writes are not yet exposed by the SDK.
|
|
1578
|
+
null,
|
|
1579
|
+
ackBits,
|
|
1580
|
+
signatures
|
|
1351
1581
|
]
|
|
1352
1582
|
};
|
|
1353
1583
|
}
|
|
1354
|
-
async
|
|
1584
|
+
async commitObject(params) {
|
|
1355
1585
|
const transaction = await this.aptos.transaction.build.simple({
|
|
1356
1586
|
...params.options?.build,
|
|
1357
1587
|
options: this.orderlessTxOptions(params.options?.build?.options),
|
|
1358
|
-
data: _ShelbyBlobClient.
|
|
1359
|
-
|
|
1588
|
+
data: _ShelbyBlobClient.createCommitObjectPayload({
|
|
1589
|
+
deployer: this.deployer,
|
|
1590
|
+
uid: params.uid,
|
|
1360
1591
|
blobName: params.blobName,
|
|
1361
|
-
|
|
1592
|
+
overwrite: params.overwrite,
|
|
1362
1593
|
storageProviderAcks: params.storageProviderAcks
|
|
1363
1594
|
}),
|
|
1364
1595
|
sender: params.account.accountAddress
|
|
@@ -1378,6 +1609,29 @@ import {
|
|
|
1378
1609
|
Aptos as Aptos2,
|
|
1379
1610
|
Hex as Hex4
|
|
1380
1611
|
} from "@aptos-labs/ts-sdk";
|
|
1612
|
+
|
|
1613
|
+
// src/core/types/storage_providers.ts
|
|
1614
|
+
var ACTIVE_PROVIDER_CONDITIONS = [
|
|
1615
|
+
"Normal",
|
|
1616
|
+
"Faulty",
|
|
1617
|
+
"Leaving",
|
|
1618
|
+
"Evicted",
|
|
1619
|
+
"PendingLeaving"
|
|
1620
|
+
];
|
|
1621
|
+
function isActiveProviderCondition(value) {
|
|
1622
|
+
return typeof value === "string" && ACTIVE_PROVIDER_CONDITIONS.includes(value);
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// src/core/clients/ShelbyMetadataClient.ts
|
|
1626
|
+
function parseActiveProviderCondition(raw) {
|
|
1627
|
+
const variant = raw?.__variant__;
|
|
1628
|
+
if (!isActiveProviderCondition(variant)) {
|
|
1629
|
+
throw new Error(
|
|
1630
|
+
`Unknown ActiveProviderCondition variant: ${JSON.stringify(raw)}`
|
|
1631
|
+
);
|
|
1632
|
+
}
|
|
1633
|
+
return variant;
|
|
1634
|
+
}
|
|
1381
1635
|
function parseStorageProviderState(raw) {
|
|
1382
1636
|
switch (raw.__variant__) {
|
|
1383
1637
|
case "Active":
|
|
@@ -1385,8 +1639,7 @@ function parseStorageProviderState(raw) {
|
|
|
1385
1639
|
variant: "Active",
|
|
1386
1640
|
quota: raw.quota.value,
|
|
1387
1641
|
stakeAtStartOfStakingEpoch: raw.stake_at_start_of_staking_epoch,
|
|
1388
|
-
|
|
1389
|
-
leaving: raw.leaving
|
|
1642
|
+
condition: parseActiveProviderCondition(raw.condition)
|
|
1390
1643
|
};
|
|
1391
1644
|
case "Waitlisted":
|
|
1392
1645
|
return {
|
|
@@ -1399,6 +1652,10 @@ function parseStorageProviderState(raw) {
|
|
|
1399
1652
|
frozenFrom: raw.frozen_from,
|
|
1400
1653
|
frozenTill: raw.frozen_till
|
|
1401
1654
|
};
|
|
1655
|
+
default:
|
|
1656
|
+
throw new Error(
|
|
1657
|
+
`Unknown StorageProviderStateDetails variant: ${JSON.stringify(raw)}`
|
|
1658
|
+
);
|
|
1402
1659
|
}
|
|
1403
1660
|
}
|
|
1404
1661
|
var ShelbyMetadataClient = class {
|
|
@@ -1457,28 +1714,52 @@ var ShelbyMetadataClient = class {
|
|
|
1457
1714
|
}
|
|
1458
1715
|
}
|
|
1459
1716
|
/**
|
|
1460
|
-
* Retrieves the
|
|
1717
|
+
* Retrieves the names of every activated location (region). Locations that are
|
|
1718
|
+
* registered but not yet brought online are admin-internal and not listed.
|
|
1719
|
+
*
|
|
1720
|
+
* @returns The location name list.
|
|
1721
|
+
*
|
|
1722
|
+
* @example
|
|
1723
|
+
* ```typescript
|
|
1724
|
+
* const locations = await client.getLocationNames();
|
|
1725
|
+
* ```
|
|
1726
|
+
*/
|
|
1727
|
+
async getLocationNames() {
|
|
1728
|
+
const names = await this.aptos.view({
|
|
1729
|
+
payload: {
|
|
1730
|
+
function: `${this.deployer.toString()}::location::activated_location_names`,
|
|
1731
|
+
functionArguments: []
|
|
1732
|
+
}
|
|
1733
|
+
});
|
|
1734
|
+
return names[0];
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Retrieves the list of placement group addresses in a location.
|
|
1461
1738
|
*
|
|
1739
|
+
* @param locationName - The location whose placement groups to list.
|
|
1462
1740
|
* @returns The placement group address list, or an empty array if none exist.
|
|
1463
1741
|
*
|
|
1464
1742
|
* @example
|
|
1465
1743
|
* ```typescript
|
|
1466
|
-
* const pgList = await client.getPlacementGroupAddresses();
|
|
1744
|
+
* const pgList = await client.getPlacementGroupAddresses("us-east-1");
|
|
1467
1745
|
* ```
|
|
1468
1746
|
*/
|
|
1469
|
-
async getPlacementGroupAddresses() {
|
|
1747
|
+
async getPlacementGroupAddresses(locationName) {
|
|
1470
1748
|
try {
|
|
1471
1749
|
const pgSizeMetadata = await this.aptos.view({
|
|
1472
1750
|
payload: {
|
|
1473
1751
|
function: `${this.deployer.toString()}::placement_group_registry::get_number_of_placement_groups`,
|
|
1474
|
-
functionArguments: []
|
|
1752
|
+
functionArguments: [locationName]
|
|
1475
1753
|
}
|
|
1476
1754
|
});
|
|
1755
|
+
if (Number(pgSizeMetadata[0]) === 0) {
|
|
1756
|
+
return [];
|
|
1757
|
+
}
|
|
1477
1758
|
const finalPlacementGroupIndex = pgSizeMetadata[0] - 1;
|
|
1478
1759
|
const addressMetadataArray = await this.aptos.view({
|
|
1479
1760
|
payload: {
|
|
1480
1761
|
function: `${this.deployer.toString()}::placement_group_registry::get_placement_group_addresses`,
|
|
1481
|
-
functionArguments: [0, finalPlacementGroupIndex]
|
|
1762
|
+
functionArguments: [locationName, 0, finalPlacementGroupIndex]
|
|
1482
1763
|
}
|
|
1483
1764
|
});
|
|
1484
1765
|
const metadata = addressMetadataArray[0];
|
|
@@ -1492,28 +1773,32 @@ var ShelbyMetadataClient = class {
|
|
|
1492
1773
|
}
|
|
1493
1774
|
}
|
|
1494
1775
|
/**
|
|
1495
|
-
* Retrieves the list of slice addresses.
|
|
1776
|
+
* Retrieves the list of slice addresses in a location.
|
|
1496
1777
|
*
|
|
1778
|
+
* @param locationName - The location whose slices to list.
|
|
1497
1779
|
* @returns The slice group list, or an empty array if none exist.
|
|
1498
1780
|
*
|
|
1499
1781
|
* @example
|
|
1500
1782
|
* ```typescript
|
|
1501
|
-
* const pgList = await client.getSliceAddresses();
|
|
1783
|
+
* const pgList = await client.getSliceAddresses("us-east-1");
|
|
1502
1784
|
* ```
|
|
1503
1785
|
*/
|
|
1504
|
-
async getSliceAddresses() {
|
|
1786
|
+
async getSliceAddresses(locationName) {
|
|
1505
1787
|
try {
|
|
1506
1788
|
const sliceSizeMetadata = await this.aptos.view({
|
|
1507
1789
|
payload: {
|
|
1508
1790
|
function: `${this.deployer.toString()}::slice_registry::get_number_of_slices`,
|
|
1509
|
-
functionArguments: []
|
|
1791
|
+
functionArguments: [locationName]
|
|
1510
1792
|
}
|
|
1511
1793
|
});
|
|
1794
|
+
if (Number(sliceSizeMetadata[0]) === 0) {
|
|
1795
|
+
return [];
|
|
1796
|
+
}
|
|
1512
1797
|
const finalSliceIndex = sliceSizeMetadata[0] - 1;
|
|
1513
1798
|
const addressMetadataArray = await this.aptos.view({
|
|
1514
1799
|
payload: {
|
|
1515
1800
|
function: `${this.deployer.toString()}::slice_registry::get_slice_addresses`,
|
|
1516
|
-
functionArguments: [0, finalSliceIndex]
|
|
1801
|
+
functionArguments: [locationName, 0, finalSliceIndex]
|
|
1517
1802
|
}
|
|
1518
1803
|
});
|
|
1519
1804
|
const metadata = addressMetadataArray[0];
|
|
@@ -1573,6 +1858,36 @@ var ShelbyMetadataClient = class {
|
|
|
1573
1858
|
(opt) => opt.vec.length > 0 ? normalizeAddress(opt.vec[0]) : null
|
|
1574
1859
|
);
|
|
1575
1860
|
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Retrieves the active storage providers for a slice.
|
|
1863
|
+
*
|
|
1864
|
+
* Active SPs have a complete copy of the data for the slot, and are not in any data transfer/repair/reconstruction phase.
|
|
1865
|
+
* Active SP can be audited for the data it contains.
|
|
1866
|
+
* Each slot has at most one active SP.
|
|
1867
|
+
*
|
|
1868
|
+
* @param params.account - The address of the slice account.
|
|
1869
|
+
* @returns An array where result[i] is the active SP for slot i, or null if no SP is active.
|
|
1870
|
+
*
|
|
1871
|
+
* @example
|
|
1872
|
+
* ```typescript
|
|
1873
|
+
* const providers = await client.getActiveStorageProvidersForSlice({ account: sliceAddress });
|
|
1874
|
+
* ```
|
|
1875
|
+
*/
|
|
1876
|
+
async getActiveStorageProvidersForSlice(params) {
|
|
1877
|
+
const placementGroupAddress = await this.getPlacementGroupAddressForSlice(
|
|
1878
|
+
params.account
|
|
1879
|
+
);
|
|
1880
|
+
const rawMetadata = await this.aptos.view({
|
|
1881
|
+
payload: {
|
|
1882
|
+
function: `${this.deployer.toString()}::placement_group::get_active_storage_providers`,
|
|
1883
|
+
functionArguments: [placementGroupAddress]
|
|
1884
|
+
}
|
|
1885
|
+
});
|
|
1886
|
+
const providers = rawMetadata[0];
|
|
1887
|
+
return providers.map(
|
|
1888
|
+
(opt) => opt.vec.length > 0 ? normalizeAddress(opt.vec[0]) : null
|
|
1889
|
+
);
|
|
1890
|
+
}
|
|
1576
1891
|
/**
|
|
1577
1892
|
* Retrieves the serving storage providers for a slice.
|
|
1578
1893
|
*
|
|
@@ -1632,22 +1947,6 @@ var ChallengeResponseSchema = z3.object({
|
|
|
1632
1947
|
challenge: z3.string(),
|
|
1633
1948
|
expiresAt: z3.number()
|
|
1634
1949
|
});
|
|
1635
|
-
var MultipartUploadStatusResponseSchema = z3.object({
|
|
1636
|
-
uploadId: z3.string(),
|
|
1637
|
-
completedParts: z3.array(z3.number()),
|
|
1638
|
-
partSize: z3.number(),
|
|
1639
|
-
nParts: z3.number(),
|
|
1640
|
-
uploadedBytes: z3.number()
|
|
1641
|
-
});
|
|
1642
|
-
var StartMultipartUploadResponseSchema = z3.object({
|
|
1643
|
-
uploadId: z3.string()
|
|
1644
|
-
});
|
|
1645
|
-
var UploadPartResponseSchema = z3.object({
|
|
1646
|
-
success: z3.literal(true)
|
|
1647
|
-
});
|
|
1648
|
-
var CompleteMultipartUploadResponseSchema = z3.object({
|
|
1649
|
-
success: z3.literal(true)
|
|
1650
|
-
});
|
|
1651
1950
|
var RPCErrorResponseSchema = z3.object({
|
|
1652
1951
|
error: z3.string()
|
|
1653
1952
|
});
|
|
@@ -1864,6 +2163,7 @@ var BLOB_OWNER_AUTH_SCHEME_HEADER = "X-Shelby-Auth-Scheme";
|
|
|
1864
2163
|
var BLOB_OWNER_IDENTITY_HEADER = "X-Shelby-Identity";
|
|
1865
2164
|
var BLOB_OWNER_DOMAIN_HEADER = "X-Shelby-Domain";
|
|
1866
2165
|
var BLOB_OWNER_AUTH_FUNCTION_HEADER = "X-Shelby-Auth-Function";
|
|
2166
|
+
var INCLUSION_PROOF_HEADER = "X-Shelby-Inclusion-Proof";
|
|
1867
2167
|
function buildAuthHeaders(auth) {
|
|
1868
2168
|
const signatureBase64 = btoa(
|
|
1869
2169
|
Array.from(auth.signature, (byte) => String.fromCharCode(byte)).join("")
|
|
@@ -1887,6 +2187,58 @@ function buildAuthHeaders(auth) {
|
|
|
1887
2187
|
function encodeURIComponentKeepSlashes(str) {
|
|
1888
2188
|
return encodeURIComponent(str).replace(/%2F/g, "/");
|
|
1889
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
|
+
}
|
|
1890
2242
|
function validateTotalBytes(totalBytes) {
|
|
1891
2243
|
if (!Number.isInteger(totalBytes) || totalBytes < 0) {
|
|
1892
2244
|
throw new Error("totalBytes must be a non-negative integer");
|
|
@@ -1918,7 +2270,7 @@ var ShelbyRPCClient = class {
|
|
|
1918
2270
|
* @param config - The client configuration object.
|
|
1919
2271
|
* @param config.network - The Shelby network to use.
|
|
1920
2272
|
* @param options.signChallengeHandler - Optional override for challenge
|
|
1921
|
-
* signing. When set, `
|
|
2273
|
+
* signing. When set, `putBlobChunksets` uses this instead of the built-in
|
|
1922
2274
|
* `signChallenge`. Intended for kit-level overrides (e.g. Solana DAA).
|
|
1923
2275
|
*
|
|
1924
2276
|
* @example
|
|
@@ -1969,43 +2321,6 @@ var ShelbyRPCClient = class {
|
|
|
1969
2321
|
}
|
|
1970
2322
|
return ChallengeResponseSchema.parse(await response.json());
|
|
1971
2323
|
}
|
|
1972
|
-
/**
|
|
1973
|
-
* Check if there's an existing multipart upload for a blob.
|
|
1974
|
-
* Returns the upload status including which parts have been uploaded.
|
|
1975
|
-
*
|
|
1976
|
-
* @param account - The account that owns the blob.
|
|
1977
|
-
* @param blobName - The name of the blob.
|
|
1978
|
-
* @returns The upload status, or undefined if no pending upload exists.
|
|
1979
|
-
*
|
|
1980
|
-
* @example
|
|
1981
|
-
* ```typescript
|
|
1982
|
-
* const status = await client.getMultipartUploadStatus(account, "myblob.txt");
|
|
1983
|
-
* if (status) {
|
|
1984
|
-
* console.log(`Resuming upload ${status.uploadId}, ${status.completedParts.length} parts done`);
|
|
1985
|
-
* }
|
|
1986
|
-
* ```
|
|
1987
|
-
*/
|
|
1988
|
-
async getMultipartUploadStatus(account, blobName) {
|
|
1989
|
-
const url = new URL(buildRequestUrl("/v1/multipart-uploads", this.baseUrl));
|
|
1990
|
-
url.searchParams.set("account", account.toString());
|
|
1991
|
-
url.searchParams.set("blobName", blobName);
|
|
1992
|
-
const response = await fetch(url.toString(), {
|
|
1993
|
-
method: "GET",
|
|
1994
|
-
headers: {
|
|
1995
|
-
...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
|
|
1996
|
-
}
|
|
1997
|
-
});
|
|
1998
|
-
if (response.status === 404) {
|
|
1999
|
-
return void 0;
|
|
2000
|
-
}
|
|
2001
|
-
if (!response.ok) {
|
|
2002
|
-
const errorBody = await response.text().catch(() => "");
|
|
2003
|
-
throw new Error(
|
|
2004
|
-
`Failed to get multipart upload status: status ${response.status}, body: ${errorBody}`
|
|
2005
|
-
);
|
|
2006
|
-
}
|
|
2007
|
-
return MultipartUploadStatusResponseSchema.parse(await response.json());
|
|
2008
|
-
}
|
|
2009
2324
|
/**
|
|
2010
2325
|
* Sign a challenge using the given account and return auth credentials.
|
|
2011
2326
|
*
|
|
@@ -2022,269 +2337,142 @@ var ShelbyRPCClient = class {
|
|
|
2022
2337
|
publicKey: account.publicKey.toUint8Array()
|
|
2023
2338
|
};
|
|
2024
2339
|
}
|
|
2025
|
-
async #uploadPart(uploadId, partIdx, partData) {
|
|
2026
|
-
const nRetries = 5;
|
|
2027
|
-
let lastResponse;
|
|
2028
|
-
let lastError;
|
|
2029
|
-
let attempts = 0;
|
|
2030
|
-
const partUrl = buildRequestUrl(
|
|
2031
|
-
`/v1/multipart-uploads/${uploadId}/parts/${partIdx}`,
|
|
2032
|
-
this.baseUrl
|
|
2033
|
-
);
|
|
2034
|
-
for (let i = 0; i < nRetries; ++i) {
|
|
2035
|
-
attempts++;
|
|
2036
|
-
try {
|
|
2037
|
-
lastResponse = await fetch(partUrl, {
|
|
2038
|
-
method: "PUT",
|
|
2039
|
-
headers: {
|
|
2040
|
-
"Content-Type": "application/octet-stream",
|
|
2041
|
-
...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
|
|
2042
|
-
},
|
|
2043
|
-
body: partData
|
|
2044
|
-
});
|
|
2045
|
-
lastError = void 0;
|
|
2046
|
-
} catch (error) {
|
|
2047
|
-
lastError = error;
|
|
2048
|
-
if (i < nRetries - 1) {
|
|
2049
|
-
const delay = 2 ** i * 100;
|
|
2050
|
-
await sleep(delay);
|
|
2051
|
-
continue;
|
|
2052
|
-
}
|
|
2053
|
-
break;
|
|
2054
|
-
}
|
|
2055
|
-
if (lastResponse.ok) return;
|
|
2056
|
-
if (!isRetryableStatus(lastResponse.status)) {
|
|
2057
|
-
break;
|
|
2058
|
-
}
|
|
2059
|
-
if (i < nRetries - 1) {
|
|
2060
|
-
const delay = 2 ** i * 100;
|
|
2061
|
-
await sleep(delay);
|
|
2062
|
-
}
|
|
2063
|
-
}
|
|
2064
|
-
if (lastError !== void 0) {
|
|
2065
|
-
const errorCode = getErrorCode(lastError);
|
|
2066
|
-
const errorMessage = lastError instanceof Error ? lastError.message : String(lastError);
|
|
2067
|
-
throw new Error(
|
|
2068
|
-
`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}`,
|
|
2069
|
-
{ cause: lastError }
|
|
2070
|
-
);
|
|
2071
|
-
}
|
|
2072
|
-
const errorBody = await lastResponse?.text().catch(() => "");
|
|
2073
|
-
throw new Error(
|
|
2074
|
-
`Failed to upload part ${partIdx} for multipart upload ${uploadId} after ${attempts} attempt${attempts === 1 ? "" : "s"}. status: ${lastResponse?.status}, body: ${errorBody}`
|
|
2075
|
-
);
|
|
2076
|
-
}
|
|
2077
|
-
async #putBlobMultipart(account, blobName, blobData, totalBytes, partSize = 5 * 1024 * 1024, onProgress) {
|
|
2078
|
-
validateTotalBytes(totalBytes);
|
|
2079
|
-
const startResponse = await fetch(
|
|
2080
|
-
buildRequestUrl("/v1/multipart-uploads", this.baseUrl),
|
|
2081
|
-
{
|
|
2082
|
-
method: "POST",
|
|
2083
|
-
headers: {
|
|
2084
|
-
"Content-Type": "application/json",
|
|
2085
|
-
...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
|
|
2086
|
-
},
|
|
2087
|
-
body: JSON.stringify({
|
|
2088
|
-
rawAccount: account.toString(),
|
|
2089
|
-
rawBlobName: blobName,
|
|
2090
|
-
rawPartSize: partSize
|
|
2091
|
-
})
|
|
2092
|
-
}
|
|
2093
|
-
);
|
|
2094
|
-
if (!startResponse.ok) {
|
|
2095
|
-
let errorBodyText = "Could not read error body";
|
|
2096
|
-
try {
|
|
2097
|
-
errorBodyText = await startResponse.text();
|
|
2098
|
-
} catch (_e) {
|
|
2099
|
-
}
|
|
2100
|
-
throw new Error(
|
|
2101
|
-
`Failed to start multipart upload! status: ${startResponse.status}, body: ${errorBodyText}`
|
|
2102
|
-
);
|
|
2103
|
-
}
|
|
2104
|
-
const { uploadId } = StartMultipartUploadResponseSchema.parse(
|
|
2105
|
-
await startResponse.json()
|
|
2106
|
-
);
|
|
2107
|
-
const totalParts = Math.ceil(totalBytes / partSize);
|
|
2108
|
-
let uploadedBytes = 0;
|
|
2109
|
-
for await (const [partIdx, partData] of readInChunks(blobData, partSize)) {
|
|
2110
|
-
await this.#uploadPart(uploadId, partIdx, partData);
|
|
2111
|
-
uploadedBytes += partData.length;
|
|
2112
|
-
onProgress?.({
|
|
2113
|
-
phase: "uploading",
|
|
2114
|
-
partIdx,
|
|
2115
|
-
totalParts,
|
|
2116
|
-
partBytes: partData.length,
|
|
2117
|
-
uploadedBytes,
|
|
2118
|
-
totalBytes
|
|
2119
|
-
});
|
|
2120
|
-
}
|
|
2121
|
-
if (uploadedBytes !== totalBytes) {
|
|
2122
|
-
throw new Error(
|
|
2123
|
-
`Uploaded bytes (${uploadedBytes}) did not match declared totalBytes (${totalBytes})`
|
|
2124
|
-
);
|
|
2125
|
-
}
|
|
2126
|
-
const finalPartIdx = totalParts > 0 ? totalParts - 1 : 0;
|
|
2127
|
-
onProgress?.({
|
|
2128
|
-
phase: "finalizing",
|
|
2129
|
-
partIdx: finalPartIdx,
|
|
2130
|
-
totalParts,
|
|
2131
|
-
// no part uploaded in this phase
|
|
2132
|
-
partBytes: 0,
|
|
2133
|
-
uploadedBytes: totalBytes,
|
|
2134
|
-
totalBytes
|
|
2135
|
-
});
|
|
2136
|
-
const completeResponse = await fetch(
|
|
2137
|
-
buildRequestUrl(
|
|
2138
|
-
`/v1/multipart-uploads/${uploadId}/complete`,
|
|
2139
|
-
this.baseUrl
|
|
2140
|
-
),
|
|
2141
|
-
{
|
|
2142
|
-
method: "POST",
|
|
2143
|
-
headers: {
|
|
2144
|
-
"Content-Type": "application/json",
|
|
2145
|
-
...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
|
|
2146
|
-
}
|
|
2147
|
-
}
|
|
2148
|
-
);
|
|
2149
|
-
if (!completeResponse.ok) {
|
|
2150
|
-
let errorBodyText = "Could not read error body";
|
|
2151
|
-
try {
|
|
2152
|
-
errorBodyText = await completeResponse.text();
|
|
2153
|
-
} catch (_e) {
|
|
2154
|
-
}
|
|
2155
|
-
throw new Error(
|
|
2156
|
-
`Failed to complete multipart upload! status: ${completeResponse.status}, body: ${errorBodyText}`
|
|
2157
|
-
);
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
/**
|
|
2161
|
-
* Uploads blob data to the Shelby RPC node for storage by storage providers.
|
|
2162
|
-
* This method should be called after blob commitments have been registered on the blockchain.
|
|
2163
|
-
* Uses multipart upload for efficient handling of large files.
|
|
2164
|
-
*
|
|
2165
|
-
* @param params.account - The account that owns the blob.
|
|
2166
|
-
* @param params.blobName - The name/path of the blob (e.g. "folder/file.txt").
|
|
2167
|
-
* @param params.blobData - The raw blob data as a Uint8Array or ReadableStream.
|
|
2168
|
-
* @param params.totalBytes - Total byte length. Required for streams; optional for Uint8Array.
|
|
2169
|
-
*
|
|
2170
|
-
* @example
|
|
2171
|
-
* ```typescript
|
|
2172
|
-
* const blobData = new TextEncoder().encode("Hello, world!");
|
|
2173
|
-
*
|
|
2174
|
-
* await client.putBlob({
|
|
2175
|
-
* account: AccountAddress.from("0x1"),
|
|
2176
|
-
* blobName: "greetings/hello.txt",
|
|
2177
|
-
* blobData,
|
|
2178
|
-
* });
|
|
2179
|
-
* ```
|
|
2180
|
-
*/
|
|
2181
|
-
async putBlob(params) {
|
|
2182
|
-
BlobNameSchema.parse(params.blobName);
|
|
2183
|
-
let totalBytes;
|
|
2184
|
-
if (params.blobData instanceof Uint8Array) {
|
|
2185
|
-
totalBytes = params.totalBytes ?? params.blobData.length;
|
|
2186
|
-
if (totalBytes !== params.blobData.length) {
|
|
2187
|
-
throw new Error(
|
|
2188
|
-
"totalBytes must match blobData.length when blobData is a Uint8Array"
|
|
2189
|
-
);
|
|
2190
|
-
}
|
|
2191
|
-
} else {
|
|
2192
|
-
if (params.totalBytes === void 0) {
|
|
2193
|
-
throw new Error(
|
|
2194
|
-
"totalBytes is required when blobData is a ReadableStream"
|
|
2195
|
-
);
|
|
2196
|
-
}
|
|
2197
|
-
totalBytes = params.totalBytes;
|
|
2198
|
-
}
|
|
2199
|
-
validateTotalBytes(totalBytes);
|
|
2200
|
-
await this.#putBlobMultipart(
|
|
2201
|
-
params.account,
|
|
2202
|
-
params.blobName,
|
|
2203
|
-
params.blobData,
|
|
2204
|
-
totalBytes,
|
|
2205
|
-
void 0,
|
|
2206
|
-
params.onProgress
|
|
2207
|
-
);
|
|
2208
|
-
}
|
|
2209
2340
|
/**
|
|
2210
|
-
* Upload a
|
|
2341
|
+
* Upload a single chunkset via the v2 chunkset API.
|
|
2211
2342
|
*/
|
|
2212
|
-
async #
|
|
2213
|
-
const
|
|
2343
|
+
async #uploadChunkset(params) {
|
|
2344
|
+
const maxNonBusyRetries = 5;
|
|
2214
2345
|
let lastResponse;
|
|
2215
2346
|
let lastError;
|
|
2216
2347
|
let attempts = 0;
|
|
2217
|
-
|
|
2218
|
-
|
|
2348
|
+
let consecutive429s = 0;
|
|
2349
|
+
let nonBusyRetries = 0;
|
|
2350
|
+
const chunksetUrl = buildRequestUrl(
|
|
2351
|
+
`/v2/chunksets/${params.account}/${params.chunksetIndex}/${params.uid}`,
|
|
2219
2352
|
this.baseUrl
|
|
2220
2353
|
);
|
|
2221
|
-
const authHeaders = buildAuthHeaders(auth);
|
|
2222
|
-
|
|
2354
|
+
const authHeaders = buildAuthHeaders(params.auth);
|
|
2355
|
+
while (true) {
|
|
2356
|
+
if (params.signal?.aborted) {
|
|
2357
|
+
throw new DOMException("Upload aborted", "AbortError");
|
|
2358
|
+
}
|
|
2223
2359
|
attempts++;
|
|
2224
2360
|
try {
|
|
2225
|
-
lastResponse = await fetch(
|
|
2361
|
+
lastResponse = await fetch(chunksetUrl, {
|
|
2226
2362
|
method: "PUT",
|
|
2227
2363
|
headers: {
|
|
2228
2364
|
"Content-Type": "application/octet-stream",
|
|
2229
2365
|
...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {},
|
|
2230
|
-
...authHeaders
|
|
2366
|
+
...authHeaders,
|
|
2367
|
+
[INCLUSION_PROOF_HEADER]: params.inclusionProof
|
|
2231
2368
|
},
|
|
2232
|
-
body:
|
|
2369
|
+
body: params.chunksetData,
|
|
2370
|
+
signal: params.signal
|
|
2233
2371
|
});
|
|
2234
2372
|
lastError = void 0;
|
|
2235
2373
|
} catch (error) {
|
|
2374
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
2375
|
+
throw error;
|
|
2376
|
+
}
|
|
2236
2377
|
lastError = error;
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2378
|
+
consecutive429s = 0;
|
|
2379
|
+
nonBusyRetries++;
|
|
2380
|
+
if (nonBusyRetries < maxNonBusyRetries) {
|
|
2381
|
+
const delay2 = 2 ** nonBusyRetries * 100;
|
|
2382
|
+
await sleep(delay2);
|
|
2240
2383
|
continue;
|
|
2241
2384
|
}
|
|
2242
2385
|
break;
|
|
2243
2386
|
}
|
|
2244
|
-
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;
|
|
2245
2414
|
if (!isRetryableStatus(lastResponse.status)) {
|
|
2246
2415
|
break;
|
|
2247
2416
|
}
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2417
|
+
nonBusyRetries++;
|
|
2418
|
+
if (nonBusyRetries >= maxNonBusyRetries) {
|
|
2419
|
+
break;
|
|
2251
2420
|
}
|
|
2421
|
+
const delay = 2 ** nonBusyRetries * 100;
|
|
2422
|
+
await sleep(delay);
|
|
2252
2423
|
}
|
|
2253
2424
|
if (lastError !== void 0) {
|
|
2254
2425
|
const errorCode = getErrorCode(lastError);
|
|
2255
2426
|
const errorMessage = lastError instanceof Error ? lastError.message : String(lastError);
|
|
2256
2427
|
throw new Error(
|
|
2257
|
-
`Failed to upload
|
|
2428
|
+
`Failed to upload chunkset ${params.chunksetIndex} after ${attempts} attempt${attempts === 1 ? "" : "s"}. ${errorCode ? `(${errorCode}) ` : ""}Last error: ${errorMessage}`,
|
|
2258
2429
|
{ cause: lastError }
|
|
2259
2430
|
);
|
|
2260
2431
|
}
|
|
2261
2432
|
const errorBody = await lastResponse?.text().catch(() => "");
|
|
2262
2433
|
throw new Error(
|
|
2263
|
-
`Failed to upload
|
|
2434
|
+
`Failed to upload chunkset ${params.chunksetIndex} after ${attempts} attempt${attempts === 1 ? "" : "s"}. status: ${lastResponse?.status}, body: ${errorBody}`
|
|
2264
2435
|
);
|
|
2265
2436
|
}
|
|
2266
2437
|
/**
|
|
2267
|
-
* Uploads blob data to the Shelby RPC node
|
|
2268
|
-
* 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)
|
|
2269
2446
|
*
|
|
2270
2447
|
* @param params.account - The Aptos Account (with signing capability) that owns the blob.
|
|
2271
|
-
* @param params.
|
|
2448
|
+
* @param params.uid - The blob's on-chain UID (from `BlobRegisteredEvent` at registration).
|
|
2272
2449
|
* @param params.blobData - The raw blob data as a Uint8Array or ReadableStream.
|
|
2450
|
+
* @param params.commitments - Pre-computed blob commitments (from generateCommitments).
|
|
2273
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.
|
|
2274
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.
|
|
2275
2456
|
*
|
|
2276
2457
|
* @example
|
|
2277
2458
|
* ```typescript
|
|
2278
|
-
*
|
|
2279
|
-
*
|
|
2280
|
-
*
|
|
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,
|
|
2281
2469
|
* blobData: fileData,
|
|
2282
|
-
*
|
|
2470
|
+
* commitments,
|
|
2471
|
+
* chunksetConcurrency: 8,
|
|
2283
2472
|
* });
|
|
2284
2473
|
* ```
|
|
2285
2474
|
*/
|
|
2286
|
-
async
|
|
2287
|
-
BlobNameSchema.parse(params.blobName);
|
|
2475
|
+
async putBlobChunksets(params) {
|
|
2288
2476
|
let totalBytes;
|
|
2289
2477
|
if (params.blobData instanceof Uint8Array) {
|
|
2290
2478
|
totalBytes = params.totalBytes ?? params.blobData.length;
|
|
@@ -2302,110 +2490,146 @@ var ShelbyRPCClient = class {
|
|
|
2302
2490
|
totalBytes = params.totalBytes;
|
|
2303
2491
|
}
|
|
2304
2492
|
validateTotalBytes(totalBytes);
|
|
2305
|
-
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
|
+
);
|
|
2306
2504
|
const { challenge } = await this.getChallenge(
|
|
2307
2505
|
params.account.accountAddress
|
|
2308
2506
|
);
|
|
2309
2507
|
const signFn = this.#signChallengeOverride ?? this.signChallenge.bind(this);
|
|
2310
2508
|
const auth = signFn(params.account, challenge);
|
|
2311
|
-
const
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
uploadId = existingUpload.uploadId;
|
|
2321
|
-
completedPartsSet = new Set(existingUpload.completedParts);
|
|
2322
|
-
totalParts = existingUpload.nParts;
|
|
2323
|
-
if (existingUpload.partSize !== partSize) {
|
|
2324
|
-
throw new Error(
|
|
2325
|
-
`Cannot resume upload: part size mismatch. Existing upload uses ${existingUpload.partSize} bytes, but ${partSize} was requested.`
|
|
2326
|
-
);
|
|
2327
|
-
}
|
|
2328
|
-
} else {
|
|
2329
|
-
const startResponse = await fetch(
|
|
2330
|
-
buildRequestUrl("/v1/multipart-uploads", this.baseUrl),
|
|
2331
|
-
{
|
|
2332
|
-
method: "POST",
|
|
2333
|
-
headers: {
|
|
2334
|
-
"Content-Type": "application/json",
|
|
2335
|
-
...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {},
|
|
2336
|
-
...authHeaders
|
|
2337
|
-
},
|
|
2338
|
-
body: JSON.stringify({
|
|
2339
|
-
rawAccount: params.account.accountAddress.toString(),
|
|
2340
|
-
rawBlobName: params.blobName,
|
|
2341
|
-
rawPartSize: partSize
|
|
2342
|
-
})
|
|
2343
|
-
}
|
|
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}`
|
|
2344
2518
|
);
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
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.`
|
|
2353
2528
|
);
|
|
2354
|
-
uploadId = parsed.uploadId;
|
|
2355
|
-
completedPartsSet = /* @__PURE__ */ new Set();
|
|
2356
|
-
totalParts = Math.ceil(totalBytes / partSize);
|
|
2357
2529
|
}
|
|
2358
2530
|
let uploadedBytes = 0;
|
|
2359
|
-
let
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
if (completedPartsSet.has(partIdx)) {
|
|
2366
|
-
uploadedBytes += partData.length;
|
|
2367
|
-
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;
|
|
2368
2537
|
}
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
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
|
|
2378
2548
|
});
|
|
2379
2549
|
}
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
`/v1/multipart-uploads/${uploadId}/complete`,
|
|
2392
|
-
this.baseUrl
|
|
2393
|
-
),
|
|
2394
|
-
{
|
|
2395
|
-
method: "POST",
|
|
2396
|
-
headers: {
|
|
2397
|
-
"Content-Type": "application/json",
|
|
2398
|
-
...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {},
|
|
2399
|
-
...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;
|
|
2400
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);
|
|
2401
2616
|
}
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2617
|
+
if (inFlight.size > 0) {
|
|
2618
|
+
await Promise.race(inFlight);
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
if (firstError) {
|
|
2622
|
+
throw firstError;
|
|
2623
|
+
}
|
|
2624
|
+
if (bytesReadFromSource !== totalBytes) {
|
|
2405
2625
|
throw new Error(
|
|
2406
|
-
`
|
|
2626
|
+
`Data source produced ${bytesReadFromSource} bytes, but expected ${totalBytes} bytes. The stream may have ended prematurely, resulting in an incomplete upload.`
|
|
2407
2627
|
);
|
|
2408
2628
|
}
|
|
2629
|
+
const spAcks = Array.from(
|
|
2630
|
+
aggregatedAcks.entries()
|
|
2631
|
+
).map(([slot, signature]) => ({ slot, signature }));
|
|
2632
|
+
return { spAcks };
|
|
2409
2633
|
}
|
|
2410
2634
|
/**
|
|
2411
2635
|
* Downloads a blob from the Shelby RPC node.
|
|
@@ -2657,6 +2881,59 @@ var ShelbyClient = class {
|
|
|
2657
2881
|
}
|
|
2658
2882
|
return this._provider;
|
|
2659
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
|
+
}
|
|
2660
2937
|
/**
|
|
2661
2938
|
* The base URL for the Shelby RPC node.
|
|
2662
2939
|
*/
|
|
@@ -2670,8 +2947,8 @@ var ShelbyClient = class {
|
|
|
2670
2947
|
*
|
|
2671
2948
|
* Note: This method accepts only `Uint8Array` and buffers the entire blob in memory.
|
|
2672
2949
|
* For streaming uploads of large files (e.g. >2 GiB), orchestrate the steps manually
|
|
2673
|
-
* using `generateCommitments()`, `coordination.registerBlob()`,
|
|
2674
|
-
* with a `ReadableStream`.
|
|
2950
|
+
* using `generateCommitments()`, `coordination.registerBlob()`, `rpc.putBlobChunksets()`,
|
|
2951
|
+
* and `coordination.commitObject()` with a `ReadableStream`.
|
|
2675
2952
|
*
|
|
2676
2953
|
* @param params.blobData - The raw data to upload as a Uint8Array.
|
|
2677
2954
|
* @param params.signer - The account that signs and pays for the transaction.
|
|
@@ -2694,34 +2971,57 @@ var ShelbyClient = class {
|
|
|
2694
2971
|
* ```
|
|
2695
2972
|
*/
|
|
2696
2973
|
async upload(params) {
|
|
2697
|
-
const
|
|
2698
|
-
|
|
2699
|
-
|
|
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
|
|
2700
2987
|
});
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
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`
|
|
2706
3007
|
);
|
|
2707
|
-
const { transaction: pendingRegisterBlobTransaction } = await this.coordination.registerBlob({
|
|
2708
|
-
account: params.signer,
|
|
2709
|
-
blobName: params.blobName,
|
|
2710
|
-
blobMerkleRoot: blobCommitments.blob_merkle_root,
|
|
2711
|
-
size: params.blobData.length,
|
|
2712
|
-
expirationMicros: params.expirationMicros,
|
|
2713
|
-
config: provider.config,
|
|
2714
|
-
options: params.options
|
|
2715
|
-
});
|
|
2716
|
-
await this.coordination.aptos.waitForTransaction({
|
|
2717
|
-
transactionHash: pendingRegisterBlobTransaction.hash
|
|
2718
|
-
});
|
|
2719
3008
|
}
|
|
2720
|
-
await this.
|
|
3009
|
+
const { transaction: pendingCommitTransaction } = await this.coordination.commitObject({
|
|
2721
3010
|
account: params.signer,
|
|
3011
|
+
uid,
|
|
2722
3012
|
blobName: params.blobName,
|
|
2723
|
-
|
|
3013
|
+
overwrite: true,
|
|
3014
|
+
storageProviderAcks: spAcks,
|
|
3015
|
+
options: this.orderlessCommitOptions()
|
|
2724
3016
|
});
|
|
3017
|
+
const confirmedTx = await this.coordination.aptos.waitForTransaction({
|
|
3018
|
+
transactionHash: pendingCommitTransaction.hash
|
|
3019
|
+
});
|
|
3020
|
+
if (!confirmedTx.success) {
|
|
3021
|
+
throw new Error(
|
|
3022
|
+
`commit_object tx failed for '${params.blobName}': ${confirmedTx.vm_status}`
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
2725
3025
|
}
|
|
2726
3026
|
/**
|
|
2727
3027
|
* Uploads a batch of blobs to the Shelby network.
|
|
@@ -2730,8 +3030,8 @@ var ShelbyClient = class {
|
|
|
2730
3030
|
*
|
|
2731
3031
|
* Note: This method accepts only `Uint8Array` and buffers each blob in memory.
|
|
2732
3032
|
* For streaming uploads of large files, orchestrate the steps manually using
|
|
2733
|
-
* `generateCommitments()`, `coordination.registerBlob()`,
|
|
2734
|
-
* with a `ReadableStream`.
|
|
3033
|
+
* `generateCommitments()`, `coordination.registerBlob()`, `rpc.putBlobChunksets()`,
|
|
3034
|
+
* and `coordination.commitObject()` with a `ReadableStream`.
|
|
2735
3035
|
*
|
|
2736
3036
|
* @param params.blobs - The blobs to upload.
|
|
2737
3037
|
* @param params.blobs.blobData - The raw data to upload as a Uint8Array.
|
|
@@ -2756,59 +3056,77 @@ var ShelbyClient = class {
|
|
|
2756
3056
|
* ```
|
|
2757
3057
|
*/
|
|
2758
3058
|
async batchUpload(params) {
|
|
2759
|
-
const
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
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
|
|
2770
3076
|
});
|
|
2771
|
-
const
|
|
2772
|
-
|
|
2773
|
-
(existingBlob) => existingBlob.name === createBlobKey({
|
|
2774
|
-
account: params.signer.accountAddress,
|
|
2775
|
-
blobName: blob.blobName
|
|
2776
|
-
})
|
|
2777
|
-
)
|
|
3077
|
+
const registerTx = await this.waitForRegistration(
|
|
3078
|
+
pendingRegisterBlobTransaction.hash
|
|
2778
3079
|
);
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
const { transaction: pendingRegisterBlobTransaction } = await this.coordination.batchRegisterBlobs({
|
|
2787
|
-
account: params.signer,
|
|
2788
|
-
expirationMicros: params.expirationMicros,
|
|
2789
|
-
blobs: blobsToRegister.map((blob, index) => ({
|
|
2790
|
-
blobName: blob.blobName,
|
|
2791
|
-
blobSize: blob.blobData.length,
|
|
2792
|
-
blobMerkleRoot: blobCommitments[index].blob_merkle_root
|
|
2793
|
-
})),
|
|
2794
|
-
config: provider.config,
|
|
2795
|
-
options: params.options
|
|
2796
|
-
});
|
|
2797
|
-
await this.coordination.aptos.waitForTransaction({
|
|
2798
|
-
transactionHash: pendingRegisterBlobTransaction.hash,
|
|
2799
|
-
options: { waitForIndexer: true }
|
|
2800
|
-
});
|
|
2801
|
-
}
|
|
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);
|
|
2802
3087
|
const limit = pLimit(3);
|
|
2803
3088
|
await Promise.all(
|
|
2804
|
-
|
|
2805
|
-
(
|
|
2806
|
-
|
|
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({
|
|
2807
3102
|
account: params.signer,
|
|
2808
|
-
|
|
2809
|
-
blobData: blob.blobData
|
|
2810
|
-
|
|
2811
|
-
|
|
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
|
+
})
|
|
2812
3130
|
)
|
|
2813
3131
|
);
|
|
2814
3132
|
}
|