@aztec/blob-client 0.0.1-commit.03f7ef2
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/README.md +62 -0
- package/dest/archive/blobscan_archive_client.d.ts +146 -0
- package/dest/archive/blobscan_archive_client.d.ts.map +1 -0
- package/dest/archive/blobscan_archive_client.js +138 -0
- package/dest/archive/config.d.ts +7 -0
- package/dest/archive/config.d.ts.map +1 -0
- package/dest/archive/config.js +11 -0
- package/dest/archive/factory.d.ts +4 -0
- package/dest/archive/factory.d.ts.map +1 -0
- package/dest/archive/factory.js +7 -0
- package/dest/archive/index.d.ts +3 -0
- package/dest/archive/index.d.ts.map +1 -0
- package/dest/archive/index.js +2 -0
- package/dest/archive/instrumentation.d.ts +11 -0
- package/dest/archive/instrumentation.d.ts.map +1 -0
- package/dest/archive/instrumentation.js +33 -0
- package/dest/archive/interface.d.ts +13 -0
- package/dest/archive/interface.d.ts.map +1 -0
- package/dest/archive/interface.js +1 -0
- package/dest/blobstore/blob_store_test_suite.d.ts +3 -0
- package/dest/blobstore/blob_store_test_suite.d.ts.map +1 -0
- package/dest/blobstore/blob_store_test_suite.js +133 -0
- package/dest/blobstore/index.d.ts +3 -0
- package/dest/blobstore/index.d.ts.map +1 -0
- package/dest/blobstore/index.js +2 -0
- package/dest/blobstore/interface.d.ts +12 -0
- package/dest/blobstore/interface.d.ts.map +1 -0
- package/dest/blobstore/interface.js +1 -0
- package/dest/blobstore/memory_blob_store.d.ts +8 -0
- package/dest/blobstore/memory_blob_store.d.ts.map +1 -0
- package/dest/blobstore/memory_blob_store.js +24 -0
- package/dest/client/bin/index.d.ts +3 -0
- package/dest/client/bin/index.d.ts.map +1 -0
- package/dest/client/bin/index.js +30 -0
- package/dest/client/config.d.ts +50 -0
- package/dest/client/config.d.ts.map +1 -0
- package/dest/client/config.js +55 -0
- package/dest/client/factory.d.ts +39 -0
- package/dest/client/factory.d.ts.map +1 -0
- package/dest/client/factory.js +53 -0
- package/dest/client/http.d.ts +63 -0
- package/dest/client/http.d.ts.map +1 -0
- package/dest/client/http.js +533 -0
- package/dest/client/index.d.ts +6 -0
- package/dest/client/index.d.ts.map +1 -0
- package/dest/client/index.js +5 -0
- package/dest/client/interface.d.ts +22 -0
- package/dest/client/interface.d.ts.map +1 -0
- package/dest/client/interface.js +1 -0
- package/dest/client/local.d.ts +11 -0
- package/dest/client/local.d.ts.map +1 -0
- package/dest/client/local.js +16 -0
- package/dest/client/tests.d.ts +11 -0
- package/dest/client/tests.d.ts.map +1 -0
- package/dest/client/tests.js +51 -0
- package/dest/encoding/index.d.ts +15 -0
- package/dest/encoding/index.d.ts.map +1 -0
- package/dest/encoding/index.js +19 -0
- package/dest/filestore/factory.d.ts +50 -0
- package/dest/filestore/factory.d.ts.map +1 -0
- package/dest/filestore/factory.js +67 -0
- package/dest/filestore/filestore_blob_client.d.ts +55 -0
- package/dest/filestore/filestore_blob_client.d.ts.map +1 -0
- package/dest/filestore/filestore_blob_client.js +91 -0
- package/dest/filestore/index.d.ts +3 -0
- package/dest/filestore/index.d.ts.map +1 -0
- package/dest/filestore/index.js +2 -0
- package/package.json +94 -0
- package/src/archive/blobscan_archive_client.ts +176 -0
- package/src/archive/config.ts +14 -0
- package/src/archive/factory.ts +11 -0
- package/src/archive/fixtures/blobscan_get_blob_data.json +1 -0
- package/src/archive/fixtures/blobscan_get_block.json +56 -0
- package/src/archive/index.ts +2 -0
- package/src/archive/instrumentation.ts +41 -0
- package/src/archive/interface.ts +9 -0
- package/src/blobstore/blob_store_test_suite.ts +110 -0
- package/src/blobstore/index.ts +2 -0
- package/src/blobstore/interface.ts +12 -0
- package/src/blobstore/memory_blob_store.ts +31 -0
- package/src/client/bin/index.ts +35 -0
- package/src/client/config.ts +117 -0
- package/src/client/factory.ts +88 -0
- package/src/client/http.ts +616 -0
- package/src/client/index.ts +5 -0
- package/src/client/interface.ts +23 -0
- package/src/client/local.ts +25 -0
- package/src/client/tests.ts +62 -0
- package/src/encoding/index.ts +21 -0
- package/src/filestore/factory.ts +145 -0
- package/src/filestore/filestore_blob_client.ts +121 -0
- package/src/filestore/index.ts +2 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { makeRandomBlob } from '@aztec/blob-lib/testing';
|
|
2
|
+
/**
|
|
3
|
+
* Shared test suite for blob clients
|
|
4
|
+
* @param createClient - Function that creates a client instance for testing
|
|
5
|
+
* @param cleanup - Optional cleanup function to run after each test
|
|
6
|
+
*/ export function runBlobClientTests(createClient) {
|
|
7
|
+
let blockId;
|
|
8
|
+
let client;
|
|
9
|
+
let cleanup;
|
|
10
|
+
beforeEach(async ()=>{
|
|
11
|
+
blockId = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';
|
|
12
|
+
const setup = await createClient();
|
|
13
|
+
client = setup.client;
|
|
14
|
+
cleanup = setup.cleanup;
|
|
15
|
+
});
|
|
16
|
+
afterEach(async ()=>{
|
|
17
|
+
await cleanup();
|
|
18
|
+
});
|
|
19
|
+
it('should send and retrieve blobs by hash', async ()=>{
|
|
20
|
+
const blob = makeRandomBlob(5);
|
|
21
|
+
const blobHash = blob.getEthVersionedBlobHash();
|
|
22
|
+
await client.sendBlobsToFilestore([
|
|
23
|
+
blob
|
|
24
|
+
]);
|
|
25
|
+
const retrievedBlobs = await client.getBlobSidecar(blockId, [
|
|
26
|
+
blobHash
|
|
27
|
+
]);
|
|
28
|
+
expect(retrievedBlobs).toHaveLength(1);
|
|
29
|
+
expect(retrievedBlobs[0]).toEqual(blob);
|
|
30
|
+
});
|
|
31
|
+
it('should handle multiple blobs', async ()=>{
|
|
32
|
+
const blobs = Array.from({
|
|
33
|
+
length: 3
|
|
34
|
+
}, ()=>makeRandomBlob(7));
|
|
35
|
+
const blobHashes = blobs.map((blob)=>blob.getEthVersionedBlobHash());
|
|
36
|
+
await client.sendBlobsToFilestore(blobs);
|
|
37
|
+
const retrievedBlobs = await client.getBlobSidecar(blockId, blobHashes);
|
|
38
|
+
expect(retrievedBlobs.length).toBe(3);
|
|
39
|
+
for(let i = 0; i < blobs.length; i++){
|
|
40
|
+
expect(retrievedBlobs[i]).toEqual(blobs[i]);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
it('should return empty array for non-existent blob hash', async ()=>{
|
|
44
|
+
const nonExistentHash = Buffer.alloc(32);
|
|
45
|
+
nonExistentHash.fill(0xff);
|
|
46
|
+
const retrievedBlobs = await client.getBlobSidecar(blockId, [
|
|
47
|
+
nonExistentHash
|
|
48
|
+
]);
|
|
49
|
+
expect(retrievedBlobs).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snappy decompress the blob buffer
|
|
3
|
+
*
|
|
4
|
+
* @param data - The blob buffer
|
|
5
|
+
* @returns The decompressed blob buffer
|
|
6
|
+
*/
|
|
7
|
+
export declare function inboundTransform(data: Buffer): Buffer;
|
|
8
|
+
/**
|
|
9
|
+
* Snappy compress the blob buffer
|
|
10
|
+
*
|
|
11
|
+
* @param data - The blob buffer
|
|
12
|
+
* @returns The compressed blob buffer
|
|
13
|
+
*/
|
|
14
|
+
export declare function outboundTransform(data: Buffer): Buffer;
|
|
15
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9lbmNvZGluZy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFFQTs7Ozs7R0FLRztBQUNILHdCQUFnQixnQkFBZ0IsQ0FBQyxJQUFJLEVBQUUsTUFBTSxHQUFHLE1BQU0sQ0FFckQ7QUFFRDs7Ozs7R0FLRztBQUNILHdCQUFnQixpQkFBaUIsQ0FBQyxJQUFJLEVBQUUsTUFBTSxHQUFHLE1BQU0sQ0FFdEQifQ==
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/encoding/index.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtD"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { compressSync, uncompressSync } from 'snappy';
|
|
2
|
+
/**
|
|
3
|
+
* Snappy decompress the blob buffer
|
|
4
|
+
*
|
|
5
|
+
* @param data - The blob buffer
|
|
6
|
+
* @returns The decompressed blob buffer
|
|
7
|
+
*/ export function inboundTransform(data) {
|
|
8
|
+
return Buffer.from(uncompressSync(data, {
|
|
9
|
+
asBuffer: true
|
|
10
|
+
}));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Snappy compress the blob buffer
|
|
14
|
+
*
|
|
15
|
+
* @param data - The blob buffer
|
|
16
|
+
* @returns The compressed blob buffer
|
|
17
|
+
*/ export function outboundTransform(data) {
|
|
18
|
+
return Buffer.from(compressSync(data));
|
|
19
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type Logger } from '@aztec/foundation/log';
|
|
2
|
+
import { FileStoreBlobClient } from './filestore_blob_client.js';
|
|
3
|
+
/**
|
|
4
|
+
* Metadata required to construct the base path for blob storage.
|
|
5
|
+
* Path format: aztec-{l1ChainId}-{rollupVersion}-{rollupAddress}/
|
|
6
|
+
*/
|
|
7
|
+
export interface BlobFileStoreMetadata {
|
|
8
|
+
/** The L1 chain ID */
|
|
9
|
+
l1ChainId: number;
|
|
10
|
+
/** The rollup version */
|
|
11
|
+
rollupVersion: number;
|
|
12
|
+
/** The rollup contract address (with or without 0x prefix) */
|
|
13
|
+
rollupAddress: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Constructs the base path for blob storage.
|
|
17
|
+
* Format: aztec-{l1ChainId}-{rollupVersion}-{rollupAddress}
|
|
18
|
+
*/
|
|
19
|
+
export declare function makeBlobBasePath(metadata: BlobFileStoreMetadata): string;
|
|
20
|
+
/**
|
|
21
|
+
* Creates a read-only FileStoreBlobClient for fetching blobs.
|
|
22
|
+
*
|
|
23
|
+
* @param storeUrl - The URL of the filestore (s3://, gs://, file://, https://)
|
|
24
|
+
* @param metadata - Chain metadata for constructing the base path
|
|
25
|
+
* @param logger - Optional logger
|
|
26
|
+
* @returns A FileStoreBlobClient for reading blobs, or undefined if storeUrl is undefined
|
|
27
|
+
*/
|
|
28
|
+
export declare function createReadOnlyFileStoreBlobClient(storeUrl: string, metadata: BlobFileStoreMetadata, logger?: Logger): Promise<FileStoreBlobClient>;
|
|
29
|
+
export declare function createReadOnlyFileStoreBlobClient(storeUrl: string | undefined, metadata: BlobFileStoreMetadata, logger?: Logger): Promise<FileStoreBlobClient | undefined>;
|
|
30
|
+
/**
|
|
31
|
+
* Creates multiple read-only FileStoreBlobClients from an array of URLs.
|
|
32
|
+
*
|
|
33
|
+
* @param storeUrls - Array of filestore URLs
|
|
34
|
+
* @param metadata - Chain metadata for constructing the base path
|
|
35
|
+
* @param logger - Optional logger
|
|
36
|
+
* @returns Array of FileStoreBlobClients (excludes any that failed to create)
|
|
37
|
+
*/
|
|
38
|
+
export declare function createReadOnlyFileStoreBlobClients(storeUrls: string[] | undefined, metadata: BlobFileStoreMetadata, logger?: Logger): Promise<FileStoreBlobClient[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Creates a writable FileStoreBlobClient for uploading blobs.
|
|
41
|
+
* Note: https:// URLs are not supported for upload, only s3://, gs://, or file://.
|
|
42
|
+
*
|
|
43
|
+
* @param storeUrl - The URL of the filestore (s3://, gs://, file://)
|
|
44
|
+
* @param metadata - Chain metadata for constructing the base path
|
|
45
|
+
* @param logger - Optional logger
|
|
46
|
+
* @returns A writable FileStoreBlobClient, or undefined if storeUrl is undefined
|
|
47
|
+
*/
|
|
48
|
+
export declare function createWritableFileStoreBlobClient(storeUrl: string, metadata: BlobFileStoreMetadata, logger?: Logger): Promise<FileStoreBlobClient>;
|
|
49
|
+
export declare function createWritableFileStoreBlobClient(storeUrl: string | undefined, metadata: BlobFileStoreMetadata, logger?: Logger): Promise<FileStoreBlobClient | undefined>;
|
|
50
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmFjdG9yeS5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2ZpbGVzdG9yZS9mYWN0b3J5LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxLQUFLLE1BQU0sRUFBZ0IsTUFBTSx1QkFBdUIsQ0FBQztBQVFsRSxPQUFPLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSw0QkFBNEIsQ0FBQztBQUVqRTs7O0dBR0c7QUFDSCxNQUFNLFdBQVcscUJBQXFCO0lBQ3BDLHNCQUFzQjtJQUN0QixTQUFTLEVBQUUsTUFBTSxDQUFDO0lBQ2xCLHlCQUF5QjtJQUN6QixhQUFhLEVBQUUsTUFBTSxDQUFDO0lBQ3RCLDhEQUE4RDtJQUM5RCxhQUFhLEVBQUUsTUFBTSxDQUFDO0NBQ3ZCO0FBRUQ7OztHQUdHO0FBQ0gsd0JBQWdCLGdCQUFnQixDQUFDLFFBQVEsRUFBRSxxQkFBcUIsR0FBRyxNQUFNLENBS3hFO0FBRUQ7Ozs7Ozs7R0FPRztBQUNILHdCQUFzQixpQ0FBaUMsQ0FDckQsUUFBUSxFQUFFLE1BQU0sRUFDaEIsUUFBUSxFQUFFLHFCQUFxQixFQUMvQixNQUFNLENBQUMsRUFBRSxNQUFNLEdBQ2QsT0FBTyxDQUFDLG1CQUFtQixDQUFDLENBQUM7QUFDaEMsd0JBQXNCLGlDQUFpQyxDQUNyRCxRQUFRLEVBQUUsTUFBTSxHQUFHLFNBQVMsRUFDNUIsUUFBUSxFQUFFLHFCQUFxQixFQUMvQixNQUFNLENBQUMsRUFBRSxNQUFNLEdBQ2QsT0FBTyxDQUFDLG1CQUFtQixHQUFHLFNBQVMsQ0FBQyxDQUFDO0FBbUI1Qzs7Ozs7OztHQU9HO0FBQ0gsd0JBQXNCLGtDQUFrQyxDQUN0RCxTQUFTLEVBQUUsTUFBTSxFQUFFLEdBQUcsU0FBUyxFQUMvQixRQUFRLEVBQUUscUJBQXFCLEVBQy9CLE1BQU0sQ0FBQyxFQUFFLE1BQU0sR0FDZCxPQUFPLENBQUMsbUJBQW1CLEVBQUUsQ0FBQyxDQW9CaEM7QUFFRDs7Ozs7Ozs7R0FRRztBQUNILHdCQUFzQixpQ0FBaUMsQ0FDckQsUUFBUSxFQUFFLE1BQU0sRUFDaEIsUUFBUSxFQUFFLHFCQUFxQixFQUMvQixNQUFNLENBQUMsRUFBRSxNQUFNLEdBQ2QsT0FBTyxDQUFDLG1CQUFtQixDQUFDLENBQUM7QUFDaEMsd0JBQXNCLGlDQUFpQyxDQUNyRCxRQUFRLEVBQUUsTUFBTSxHQUFHLFNBQVMsRUFDNUIsUUFBUSxFQUFFLHFCQUFxQixFQUMvQixNQUFNLENBQUMsRUFBRSxNQUFNLEdBQ2QsT0FBTyxDQUFDLG1CQUFtQixHQUFHLFNBQVMsQ0FBQyxDQUFDIn0=
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/filestore/factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAgB,MAAM,uBAAuB,CAAC;AAQlE,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEjE;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,sBAAsB;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,yBAAyB;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,MAAM,CAKxE;AAED;;;;;;;GAOG;AACH,wBAAsB,iCAAiC,CACrD,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,qBAAqB,EAC/B,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAChC,wBAAsB,iCAAiC,CACrD,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,QAAQ,EAAE,qBAAqB,EAC/B,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAAC;AAmB5C;;;;;;;GAOG;AACH,wBAAsB,kCAAkC,CACtD,SAAS,EAAE,MAAM,EAAE,GAAG,SAAS,EAC/B,QAAQ,EAAE,qBAAqB,EAC/B,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAoBhC;AAED;;;;;;;;GAQG;AACH,wBAAsB,iCAAiC,CACrD,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,qBAAqB,EAC/B,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAChC,wBAAsB,iCAAiC,CACrD,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,QAAQ,EAAE,qBAAqB,EAC/B,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
2
|
+
import { createFileStore, createReadOnlyFileStore } from '@aztec/stdlib/file-store';
|
|
3
|
+
import { FileStoreBlobClient } from './filestore_blob_client.js';
|
|
4
|
+
/**
|
|
5
|
+
* Constructs the base path for blob storage.
|
|
6
|
+
* Format: aztec-{l1ChainId}-{rollupVersion}-{rollupAddress}
|
|
7
|
+
*/ export function makeBlobBasePath(metadata) {
|
|
8
|
+
const { l1ChainId, rollupVersion, rollupAddress } = metadata;
|
|
9
|
+
// Normalize rollup address to lowercase without 0x prefix for consistency
|
|
10
|
+
const normalizedAddress = rollupAddress.toLowerCase().replace(/^0x/, '');
|
|
11
|
+
return `aztec-${l1ChainId}-${rollupVersion}-0x${normalizedAddress}`;
|
|
12
|
+
}
|
|
13
|
+
export async function createReadOnlyFileStoreBlobClient(storeUrl, metadata, logger) {
|
|
14
|
+
if (!storeUrl) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const log = logger ?? createLogger('blob-client:filestore-factory');
|
|
18
|
+
const basePath = makeBlobBasePath(metadata);
|
|
19
|
+
log.debug(`Creating read-only filestore blob client`, {
|
|
20
|
+
storeUrl,
|
|
21
|
+
basePath
|
|
22
|
+
});
|
|
23
|
+
const store = await createReadOnlyFileStore(storeUrl, log);
|
|
24
|
+
return new FileStoreBlobClient(store, basePath, log);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Creates multiple read-only FileStoreBlobClients from an array of URLs.
|
|
28
|
+
*
|
|
29
|
+
* @param storeUrls - Array of filestore URLs
|
|
30
|
+
* @param metadata - Chain metadata for constructing the base path
|
|
31
|
+
* @param logger - Optional logger
|
|
32
|
+
* @returns Array of FileStoreBlobClients (excludes any that failed to create)
|
|
33
|
+
*/ export async function createReadOnlyFileStoreBlobClients(storeUrls, metadata, logger) {
|
|
34
|
+
if (!storeUrls || storeUrls.length === 0) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
const log = logger ?? createLogger('blob-client:filestore-factory');
|
|
38
|
+
const clients = [];
|
|
39
|
+
for (const storeUrl of storeUrls){
|
|
40
|
+
try {
|
|
41
|
+
const client = await createReadOnlyFileStoreBlobClient(storeUrl, metadata, log);
|
|
42
|
+
if (client) {
|
|
43
|
+
clients.push(client);
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
log.error(`Failed to create read-only filestore blob client for ${storeUrl}`, err);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return clients;
|
|
50
|
+
}
|
|
51
|
+
export async function createWritableFileStoreBlobClient(storeUrl, metadata, logger) {
|
|
52
|
+
if (!storeUrl) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const log = logger ?? createLogger('blob-client:filestore-factory');
|
|
56
|
+
const basePath = makeBlobBasePath(metadata);
|
|
57
|
+
log.debug(`Creating writable filestore blob client`, {
|
|
58
|
+
storeUrl,
|
|
59
|
+
basePath
|
|
60
|
+
});
|
|
61
|
+
const store = await createFileStore(storeUrl, log);
|
|
62
|
+
if (!store) {
|
|
63
|
+
log.warn(`Failed to create writable filestore for ${storeUrl}`);
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
return new FileStoreBlobClient(store, basePath, log);
|
|
67
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Blob, type BlobJson } from '@aztec/blob-lib';
|
|
2
|
+
import { type Logger } from '@aztec/foundation/log';
|
|
3
|
+
import type { FileStore, ReadOnlyFileStore } from '@aztec/stdlib/file-store';
|
|
4
|
+
/**
|
|
5
|
+
* A blob client that uses a FileStore (S3/GCS/local) as the data source.
|
|
6
|
+
* Blobs are stored as JSON files keyed by their versioned blob hash.
|
|
7
|
+
*/
|
|
8
|
+
export declare class FileStoreBlobClient {
|
|
9
|
+
private readonly store;
|
|
10
|
+
private readonly basePath;
|
|
11
|
+
private readonly log;
|
|
12
|
+
constructor(store: ReadOnlyFileStore | FileStore, basePath: string, logger?: Logger);
|
|
13
|
+
/**
|
|
14
|
+
* Get the path for a blob file.
|
|
15
|
+
* Format: basePath/blobs/{versionedBlobHash}.data
|
|
16
|
+
*/
|
|
17
|
+
private blobPath;
|
|
18
|
+
/**
|
|
19
|
+
* Fetch blobs by their versioned hashes.
|
|
20
|
+
* @param blobHashes - Array of versioned blob hashes (0x-prefixed hex strings)
|
|
21
|
+
* @returns Array of BlobJson objects for found blobs
|
|
22
|
+
*/
|
|
23
|
+
getBlobsByHashes(blobHashes: string[]): Promise<BlobJson[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Check if a blob exists in the store.
|
|
26
|
+
* @param versionedBlobHash - The versioned blob hash (0x-prefixed hex string)
|
|
27
|
+
*/
|
|
28
|
+
exists(versionedBlobHash: string): Promise<boolean>;
|
|
29
|
+
/**
|
|
30
|
+
* Save a single blob to the store.
|
|
31
|
+
* @param blob - The blob to save
|
|
32
|
+
* @param skipIfExists - Skip saving if blob already exists (default: true)
|
|
33
|
+
* @throws Error if the store is read-only
|
|
34
|
+
*/
|
|
35
|
+
saveBlob(blob: Blob, skipIfExists?: boolean): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Save multiple blobs to the store in parallel.
|
|
38
|
+
* @param blobs - The blobs to save
|
|
39
|
+
* @param skipIfExists - Skip saving if blob already exists (default: true)
|
|
40
|
+
*/
|
|
41
|
+
saveBlobs(blobs: Blob[], skipIfExists?: boolean): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Get the base URL/path of the filestore.
|
|
44
|
+
*/
|
|
45
|
+
getBaseUrl(): string;
|
|
46
|
+
/**
|
|
47
|
+
* Test if the filestore connection is working.
|
|
48
|
+
*/
|
|
49
|
+
testConnection(): Promise<boolean>;
|
|
50
|
+
/**
|
|
51
|
+
* Check if the store supports write operations.
|
|
52
|
+
*/
|
|
53
|
+
private isWritable;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmlsZXN0b3JlX2Jsb2JfY2xpZW50LmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvZmlsZXN0b3JlL2ZpbGVzdG9yZV9ibG9iX2NsaWVudC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsSUFBSSxFQUFFLEtBQUssUUFBUSxFQUErQixNQUFNLGlCQUFpQixDQUFDO0FBQ25GLE9BQU8sRUFBRSxLQUFLLE1BQU0sRUFBZ0IsTUFBTSx1QkFBdUIsQ0FBQztBQUNsRSxPQUFPLEtBQUssRUFBRSxTQUFTLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQztBQUk3RTs7O0dBR0c7QUFDSCxxQkFBYSxtQkFBbUI7SUFJNUIsT0FBTyxDQUFDLFFBQVEsQ0FBQyxLQUFLO0lBQ3RCLE9BQU8sQ0FBQyxRQUFRLENBQUMsUUFBUTtJQUozQixPQUFPLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBUztJQUU3QixZQUNtQixLQUFLLEVBQUUsaUJBQWlCLEdBQUcsU0FBUyxFQUNwQyxRQUFRLEVBQUUsTUFBTSxFQUNqQyxNQUFNLENBQUMsRUFBRSxNQUFNLEVBR2hCO0lBRUQ7OztPQUdHO0lBQ0gsT0FBTyxDQUFDLFFBQVE7SUFJaEI7Ozs7T0FJRztJQUNHLGdCQUFnQixDQUFDLFVBQVUsRUFBRSxNQUFNLEVBQUUsR0FBRyxPQUFPLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FtQmhFO0lBRUQ7OztPQUdHO0lBQ0gsTUFBTSxDQUFDLGlCQUFpQixFQUFFLE1BQU0sR0FBRyxPQUFPLENBQUMsT0FBTyxDQUFDLENBRWxEO0lBRUQ7Ozs7O09BS0c7SUFDRyxRQUFRLENBQUMsSUFBSSxFQUFFLElBQUksRUFBRSxZQUFZLFVBQU8sR0FBRyxPQUFPLENBQUMsSUFBSSxDQUFDLENBa0I3RDtJQUVEOzs7O09BSUc7SUFDRyxTQUFTLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxFQUFFLFlBQVksVUFBTyxHQUFHLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FFakU7SUFFRDs7T0FFRztJQUNILFVBQVUsSUFBSSxNQUFNLENBRW5CO0lBRUQ7O09BRUc7SUFDSCxjQUFjLElBQUksT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUlqQztJQUVEOztPQUVHO0lBQ0gsT0FBTyxDQUFDLFVBQVU7Q0FHbkIifQ==
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filestore_blob_client.d.ts","sourceRoot":"","sources":["../../src/filestore/filestore_blob_client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,KAAK,QAAQ,EAA+B,MAAM,iBAAiB,CAAC;AACnF,OAAO,EAAE,KAAK,MAAM,EAAgB,MAAM,uBAAuB,CAAC;AAClE,OAAO,KAAK,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAI7E;;;GAGG;AACH,qBAAa,mBAAmB;IAI5B,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAJ3B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAE7B,YACmB,KAAK,EAAE,iBAAiB,GAAG,SAAS,EACpC,QAAQ,EAAE,MAAM,EACjC,MAAM,CAAC,EAAE,MAAM,EAGhB;IAED;;;OAGG;IACH,OAAO,CAAC,QAAQ;IAIhB;;;;OAIG;IACG,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAmBhE;IAED;;;OAGG;IACH,MAAM,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAElD;IAED;;;;;OAKG;IACG,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,UAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAkB7D;IAED;;;;OAIG;IACG,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,YAAY,UAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAEjE;IAED;;OAEG;IACH,UAAU,IAAI,MAAM,CAEnB;IAED;;OAEG;IACH,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,CAIjC;IAED;;OAEG;IACH,OAAO,CAAC,UAAU;CAGnB"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { computeEthVersionedBlobHash } from '@aztec/blob-lib';
|
|
2
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
3
|
+
import { inboundTransform, outboundTransform } from '../encoding/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* A blob client that uses a FileStore (S3/GCS/local) as the data source.
|
|
6
|
+
* Blobs are stored as JSON files keyed by their versioned blob hash.
|
|
7
|
+
*/ export class FileStoreBlobClient {
|
|
8
|
+
store;
|
|
9
|
+
basePath;
|
|
10
|
+
log;
|
|
11
|
+
constructor(store, basePath, logger){
|
|
12
|
+
this.store = store;
|
|
13
|
+
this.basePath = basePath;
|
|
14
|
+
this.log = logger ?? createLogger('blob-client:filestore-client');
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Get the path for a blob file.
|
|
18
|
+
* Format: basePath/blobs/{versionedBlobHash}.data
|
|
19
|
+
*/ blobPath(versionedBlobHash) {
|
|
20
|
+
return `${this.basePath}/blobs/${versionedBlobHash}.data`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Fetch blobs by their versioned hashes.
|
|
24
|
+
* @param blobHashes - Array of versioned blob hashes (0x-prefixed hex strings)
|
|
25
|
+
* @returns Array of BlobJson objects for found blobs
|
|
26
|
+
*/ async getBlobsByHashes(blobHashes) {
|
|
27
|
+
const blobs = [];
|
|
28
|
+
for(let i = 0; i < blobHashes.length; i++){
|
|
29
|
+
try {
|
|
30
|
+
const path = this.blobPath(blobHashes[i]);
|
|
31
|
+
if (!await this.store.exists(path)) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const data = await this.store.read(path);
|
|
35
|
+
const json = JSON.parse(inboundTransform(data).toString());
|
|
36
|
+
blobs.push(json);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
this.log.warn(`Failed to read blob ${blobHashes[i]} from filestore`, err);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return blobs;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Check if a blob exists in the store.
|
|
45
|
+
* @param versionedBlobHash - The versioned blob hash (0x-prefixed hex string)
|
|
46
|
+
*/ exists(versionedBlobHash) {
|
|
47
|
+
return this.store.exists(this.blobPath(versionedBlobHash));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Save a single blob to the store.
|
|
51
|
+
* @param blob - The blob to save
|
|
52
|
+
* @param skipIfExists - Skip saving if blob already exists (default: true)
|
|
53
|
+
* @throws Error if the store is read-only
|
|
54
|
+
*/ async saveBlob(blob, skipIfExists = true) {
|
|
55
|
+
if (!this.isWritable()) {
|
|
56
|
+
throw new Error('FileStore is read-only');
|
|
57
|
+
}
|
|
58
|
+
const versionedHash = `0x${computeEthVersionedBlobHash(blob.commitment).toString('hex')}`;
|
|
59
|
+
if (skipIfExists && await this.store.exists(this.blobPath(versionedHash))) {
|
|
60
|
+
this.log.trace(`Blob ${versionedHash} already exists, skipping`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const json = blob.toJSON();
|
|
64
|
+
await this.store.save(this.blobPath(versionedHash), outboundTransform(Buffer.from(JSON.stringify(json))));
|
|
65
|
+
this.log.debug(`Saved blob ${versionedHash} to filestore`);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Save multiple blobs to the store in parallel.
|
|
69
|
+
* @param blobs - The blobs to save
|
|
70
|
+
* @param skipIfExists - Skip saving if blob already exists (default: true)
|
|
71
|
+
*/ async saveBlobs(blobs, skipIfExists = true) {
|
|
72
|
+
await Promise.all(blobs.map((blob)=>this.saveBlob(blob, skipIfExists)));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get the base URL/path of the filestore.
|
|
76
|
+
*/ getBaseUrl() {
|
|
77
|
+
return this.basePath;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Test if the filestore connection is working.
|
|
81
|
+
*/ testConnection() {
|
|
82
|
+
// This implementation will be improved in a separate PR
|
|
83
|
+
// Currently underlying filestore implementations do not expose an easy way to test connectivitiy
|
|
84
|
+
return Promise.resolve(true);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Check if the store supports write operations.
|
|
88
|
+
*/ isWritable() {
|
|
89
|
+
return 'save' in this.store;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export * from './filestore_blob_client.js';
|
|
2
|
+
export * from './factory.js';
|
|
3
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9maWxlc3RvcmUvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLGNBQWMsQ0FBQyJ9
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/filestore/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,cAAc,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aztec/blob-client",
|
|
3
|
+
"version": "0.0.1-commit.03f7ef2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": "./dest/client/bin/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
"./client": "./dest/client/index.js",
|
|
8
|
+
"./client/config": "./dest/client/config.js",
|
|
9
|
+
"./encoding": "./dest/encoding/index.js",
|
|
10
|
+
"./filestore": "./dest/filestore/index.js"
|
|
11
|
+
},
|
|
12
|
+
"inherits": [
|
|
13
|
+
"../package.common.json"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "yarn clean && ../scripts/tsc.sh",
|
|
17
|
+
"build:dev": "../scripts/tsc.sh --watch",
|
|
18
|
+
"clean": "rm -rf ./dest .tsbuildinfo",
|
|
19
|
+
"test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests --maxWorkers=${JEST_MAX_WORKERS:-8}"
|
|
20
|
+
},
|
|
21
|
+
"jest": {
|
|
22
|
+
"moduleNameMapper": {
|
|
23
|
+
"^(\\.{1,2}/.*)\\.[cm]?js$": "$1"
|
|
24
|
+
},
|
|
25
|
+
"testRegex": "./src/.*\\.test\\.(js|mjs|ts)$",
|
|
26
|
+
"rootDir": "./src",
|
|
27
|
+
"transform": {
|
|
28
|
+
"^.+\\.tsx?$": [
|
|
29
|
+
"@swc/jest",
|
|
30
|
+
{
|
|
31
|
+
"jsc": {
|
|
32
|
+
"parser": {
|
|
33
|
+
"syntax": "typescript",
|
|
34
|
+
"decorators": true
|
|
35
|
+
},
|
|
36
|
+
"transform": {
|
|
37
|
+
"decoratorVersion": "2022-03"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"extensionsToTreatAsEsm": [
|
|
44
|
+
".ts"
|
|
45
|
+
],
|
|
46
|
+
"reporters": [
|
|
47
|
+
"default"
|
|
48
|
+
],
|
|
49
|
+
"testTimeout": 120000,
|
|
50
|
+
"setupFiles": [
|
|
51
|
+
"../../foundation/src/jest/setup.mjs"
|
|
52
|
+
],
|
|
53
|
+
"testEnvironment": "../../foundation/src/jest/env.mjs",
|
|
54
|
+
"setupFilesAfterEnv": [
|
|
55
|
+
"../../foundation/src/jest/setupAfterEnv.mjs"
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@aztec/blob-lib": "0.0.1-commit.03f7ef2",
|
|
60
|
+
"@aztec/ethereum": "0.0.1-commit.03f7ef2",
|
|
61
|
+
"@aztec/foundation": "0.0.1-commit.03f7ef2",
|
|
62
|
+
"@aztec/kv-store": "0.0.1-commit.03f7ef2",
|
|
63
|
+
"@aztec/stdlib": "0.0.1-commit.03f7ef2",
|
|
64
|
+
"@aztec/telemetry-client": "0.0.1-commit.03f7ef2",
|
|
65
|
+
"express": "^4.21.2",
|
|
66
|
+
"snappy": "^7.2.2",
|
|
67
|
+
"source-map-support": "^0.5.21",
|
|
68
|
+
"tslib": "^2.4.0",
|
|
69
|
+
"viem": "npm:@aztec/viem@2.38.2",
|
|
70
|
+
"zod": "^3.23.8"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@jest/globals": "^30.0.0",
|
|
74
|
+
"@types/jest": "^30.0.0",
|
|
75
|
+
"@types/node": "^22.15.17",
|
|
76
|
+
"@types/source-map-support": "^0.5.10",
|
|
77
|
+
"@types/supertest": "^6.0.2",
|
|
78
|
+
"@typescript/native-preview": "7.0.0-dev.20251126.1",
|
|
79
|
+
"jest": "^30.0.0",
|
|
80
|
+
"jest-mock-extended": "^4.0.0",
|
|
81
|
+
"supertest": "^7.0.0",
|
|
82
|
+
"ts-node": "^10.9.1",
|
|
83
|
+
"typescript": "^5.3.3"
|
|
84
|
+
},
|
|
85
|
+
"files": [
|
|
86
|
+
"dest",
|
|
87
|
+
"src",
|
|
88
|
+
"!*.test.*"
|
|
89
|
+
],
|
|
90
|
+
"types": "./dest/index.d.ts",
|
|
91
|
+
"engines": {
|
|
92
|
+
"node": ">=20.10"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { BlobJson } from '@aztec/blob-lib/types';
|
|
2
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
3
|
+
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
4
|
+
import { schemas, zodFor } from '@aztec/foundation/schemas';
|
|
5
|
+
import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
|
|
6
|
+
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
|
|
9
|
+
import { BlobArchiveClientInstrumentation } from './instrumentation.js';
|
|
10
|
+
import type { BlobArchiveClient } from './interface.js';
|
|
11
|
+
|
|
12
|
+
export const BlobscanBlockResponseSchema = zodFor<BlobJson[]>()(
|
|
13
|
+
z
|
|
14
|
+
.object({
|
|
15
|
+
hash: z.string(),
|
|
16
|
+
slot: z.number().int(),
|
|
17
|
+
number: z.number().int(),
|
|
18
|
+
transactions: z.array(
|
|
19
|
+
z.object({
|
|
20
|
+
hash: z.string(),
|
|
21
|
+
blobs: z.array(
|
|
22
|
+
z.object({
|
|
23
|
+
versionedHash: z.string(),
|
|
24
|
+
data: z.string(),
|
|
25
|
+
commitment: z.string(),
|
|
26
|
+
proof: z.string(),
|
|
27
|
+
size: z.number().int(),
|
|
28
|
+
index: z.number().int().optional(), // Unused, kept for schema compatibility with blobscan API
|
|
29
|
+
}),
|
|
30
|
+
),
|
|
31
|
+
}),
|
|
32
|
+
),
|
|
33
|
+
})
|
|
34
|
+
.transform(data =>
|
|
35
|
+
data.transactions.flatMap(tx =>
|
|
36
|
+
tx.blobs.map(blob => ({
|
|
37
|
+
blob: blob.data,
|
|
38
|
+
// eslint-disable-next-line camelcase
|
|
39
|
+
kzg_commitment: blob.commitment,
|
|
40
|
+
})),
|
|
41
|
+
),
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Response from https://api.blobscan.com/blocks?sort=desc&type=canonical
|
|
46
|
+
export const BlobscanBlocksResponseSchema = z.object({
|
|
47
|
+
blocks: z.array(
|
|
48
|
+
z.object({
|
|
49
|
+
hash: z.string(),
|
|
50
|
+
slot: z.number().int(),
|
|
51
|
+
number: z.number().int(),
|
|
52
|
+
}),
|
|
53
|
+
),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export class BlobscanArchiveClient implements BlobArchiveClient {
|
|
57
|
+
private readonly logger = createLogger('blob-client:blobscan-archive-client');
|
|
58
|
+
private readonly fetchOpts = { headers: { accept: 'application/json' } };
|
|
59
|
+
private readonly fetch = async (...args: Parameters<typeof fetch>): Promise<Response> => {
|
|
60
|
+
return await retry(
|
|
61
|
+
() => fetch(...args),
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
63
|
+
`Fetching ${args[0]}`,
|
|
64
|
+
makeBackoff([1, 1, 3]),
|
|
65
|
+
this.logger,
|
|
66
|
+
/*failSilently=*/ false,
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
private readonly baseUrl: URL;
|
|
71
|
+
|
|
72
|
+
private instrumentation: BlobArchiveClientInstrumentation;
|
|
73
|
+
|
|
74
|
+
constructor(baseUrl: string, telemetry: TelemetryClient = getTelemetryClient()) {
|
|
75
|
+
this.baseUrl = new URL(baseUrl);
|
|
76
|
+
if (this.baseUrl.protocol !== 'https:') {
|
|
77
|
+
throw new TypeError('BaseURL must be secure: ' + baseUrl);
|
|
78
|
+
}
|
|
79
|
+
this.instrumentation = new BlobArchiveClientInstrumentation(
|
|
80
|
+
telemetry,
|
|
81
|
+
this.baseUrl.origin,
|
|
82
|
+
'BlobscanArchiveClient',
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public async getLatestBlock(): Promise<{ hash: string; number: number; slot: number }> {
|
|
87
|
+
const url = new URL('blocks', this.baseUrl);
|
|
88
|
+
url.searchParams.set('sort', 'desc');
|
|
89
|
+
url.searchParams.set('type', 'canonical');
|
|
90
|
+
url.searchParams.set('p', '1');
|
|
91
|
+
url.searchParams.set('ps', '1');
|
|
92
|
+
|
|
93
|
+
this.logger.trace(`Fetching latest block from ${url.href}`);
|
|
94
|
+
const response = await this.fetch(url, this.fetchOpts);
|
|
95
|
+
|
|
96
|
+
if (response.status !== 200) {
|
|
97
|
+
throw new Error(`Failed to fetch latest block: ${response.statusText} (${response.status})`, {
|
|
98
|
+
cause: {
|
|
99
|
+
httpResponse: {
|
|
100
|
+
status: response.status,
|
|
101
|
+
body: await response.text().catch(() => 'Failed to read response body'),
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const parsed = await response.json().then((data: any) => BlobscanBlocksResponseSchema.parse(data));
|
|
108
|
+
if (parsed.blocks.length === 0) {
|
|
109
|
+
throw new Error(`No blocks found at ${this.baseUrl}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return parsed.blocks[0];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public getBaseUrl(): string {
|
|
116
|
+
return this.baseUrl.href;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public async getBlobsFromBlock(blockId: string): Promise<BlobJson[] | undefined> {
|
|
120
|
+
const url = new URL(`blocks/${blockId}`, this.baseUrl);
|
|
121
|
+
url.searchParams.set('type', 'canonical');
|
|
122
|
+
url.searchParams.set('expand', 'blob,blob_data');
|
|
123
|
+
|
|
124
|
+
this.logger.trace(`Fetching blobs for block ${blockId} from ${url.href}`);
|
|
125
|
+
const response = await this.fetch(url, this.fetchOpts);
|
|
126
|
+
|
|
127
|
+
this.instrumentation.incRequest('blocks', response.status);
|
|
128
|
+
|
|
129
|
+
if (response.status === 404) {
|
|
130
|
+
this.logger.debug(`No blobs found for block ${blockId} at ${this.baseUrl}`);
|
|
131
|
+
return undefined;
|
|
132
|
+
} else if (response.status !== 200) {
|
|
133
|
+
throw new Error(`Failed to fetch blobs for block ${blockId}: ${response.statusText} (${response.status})`, {
|
|
134
|
+
cause: {
|
|
135
|
+
httpResponse: {
|
|
136
|
+
status: response.status,
|
|
137
|
+
body: await response.text().catch(() => 'Failed to read response body'),
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
} else {
|
|
142
|
+
const result = await response.json().then((data: any) => BlobscanBlockResponseSchema.parse(data));
|
|
143
|
+
this.logger.debug(`Fetched ${result.length} blobs for block ${blockId} from ${this.baseUrl}`);
|
|
144
|
+
this.instrumentation.incRetrievedBlobs(result.length);
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public async getBlobData(id: string): Promise<Buffer | undefined> {
|
|
150
|
+
const url = new URL(`blobs/${id}/data`, this.baseUrl);
|
|
151
|
+
|
|
152
|
+
const response = await this.fetch(url, this.fetchOpts);
|
|
153
|
+
this.instrumentation.incRequest('blobs', response.status);
|
|
154
|
+
if (response.status === 404) {
|
|
155
|
+
return undefined;
|
|
156
|
+
} else if (response.status !== 200) {
|
|
157
|
+
throw new Error(`Failed to fetch blob data for blob ${id}: ${response.statusText} (${response.status})`, {
|
|
158
|
+
cause: {
|
|
159
|
+
httpResponse: {
|
|
160
|
+
status: response.status,
|
|
161
|
+
body: await response.text().catch(err => {
|
|
162
|
+
this.logger.warn('Failed to read response body', err);
|
|
163
|
+
return '';
|
|
164
|
+
}),
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
} else {
|
|
169
|
+
return await response.json().then((data: any) => {
|
|
170
|
+
const blob = schemas.BufferHex.parse(data);
|
|
171
|
+
this.instrumentation.incRetrievedBlobs(1);
|
|
172
|
+
return blob;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type L1ReaderConfig, l1ReaderConfigMappings } from '@aztec/ethereum/l1-reader';
|
|
2
|
+
import { type ConfigMappingsType, pickConfigMappings } from '@aztec/foundation/config';
|
|
3
|
+
|
|
4
|
+
export type BlobArchiveApiConfig = {
|
|
5
|
+
archiveApiUrl?: string;
|
|
6
|
+
} & Partial<Pick<L1ReaderConfig, 'l1ChainId'>>;
|
|
7
|
+
|
|
8
|
+
export const blobArchiveApiConfigMappings: ConfigMappingsType<BlobArchiveApiConfig> = {
|
|
9
|
+
archiveApiUrl: {
|
|
10
|
+
env: 'BLOB_SINK_ARCHIVE_API_URL',
|
|
11
|
+
description: 'The URL of the archive API',
|
|
12
|
+
},
|
|
13
|
+
...pickConfigMappings(l1ReaderConfigMappings, ['l1ChainId']),
|
|
14
|
+
};
|