-
Notifications
You must be signed in to change notification settings - Fork 419
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
653 additions
and
45 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'; | ||
import { Readable } from 'stream'; | ||
|
||
import { streamToString } from '@hyperlane-xyz/utils'; | ||
|
||
export const S3_BUCKET_REGEX = | ||
/^(?:https?:\/\/)?(.*)\.s3\.(.*)\.amazonaws.com\/?$/; | ||
|
||
export interface S3Receipt<T = unknown> { | ||
data: T; | ||
modified: Date; | ||
} | ||
|
||
export class S3Wrapper { | ||
private readonly client: S3Client; | ||
readonly bucket: string; | ||
readonly region: string; | ||
readonly folder: string | undefined; | ||
|
||
private cache: Record<string, S3Receipt<any>> | undefined; | ||
|
||
static fromBucketUrl(bucketUrl: string): S3Wrapper { | ||
const match = bucketUrl.match(S3_BUCKET_REGEX); | ||
if (!match) throw new Error('Could not parse bucket url'); | ||
return new S3Wrapper(match[1], match[2], undefined); | ||
} | ||
|
||
constructor( | ||
bucket: string, | ||
region: string, | ||
folder: string | undefined, | ||
caching = false, | ||
) { | ||
this.bucket = bucket; | ||
this.region = region; | ||
this.folder = folder; | ||
this.client = new S3Client({ region }); | ||
if (caching) { | ||
this.cache = {}; | ||
} | ||
} | ||
|
||
async getS3Obj<T>(key: string): Promise<S3Receipt<T> | undefined> { | ||
const Key = this.folder ? `${this.folder}/${key}` : key; | ||
if (this.cache?.[Key]) { | ||
return this.cache![Key]; | ||
} | ||
|
||
const command = new GetObjectCommand({ | ||
Bucket: this.bucket, | ||
Key, | ||
}); | ||
try { | ||
const response = await this.client.send(command); | ||
const body: string = await streamToString(response.Body as Readable); | ||
const result = { | ||
data: JSON.parse(body), | ||
modified: response.LastModified!, | ||
}; | ||
if (this.cache) { | ||
this.cache[Key] = result; | ||
} | ||
return result; | ||
} catch (e: any) { | ||
if (e.message.includes('The specified key does not exist.')) { | ||
return; | ||
} | ||
throw e; | ||
} | ||
} | ||
|
||
url(key: string): string { | ||
const Key = this.folder ? `${this.folder}/${key}` : key; | ||
return `https://${this.bucket}.${this.region}.s3.amazonaws.com/${Key}`; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import { | ||
Announcement, | ||
BaseValidator, | ||
S3Announcement, | ||
S3CheckpointWithId, | ||
isS3CheckpointWithId, | ||
} from '@hyperlane-xyz/utils'; | ||
|
||
import { S3Wrapper } from './s3.js'; | ||
|
||
const checkpointWithMessageIdKey = (checkpointIndex: number) => | ||
`checkpoint_${checkpointIndex}_with_id.json`; | ||
const LATEST_KEY = 'checkpoint_latest_index.json'; | ||
const ANNOUNCEMENT_KEY = 'announcement.json'; | ||
const LOCATION_PREFIX = 's3://'; | ||
|
||
/** | ||
* Extension of BaseValidator that includes AWS S3 utilities. | ||
*/ | ||
export class S3Validator extends BaseValidator { | ||
public s3Bucket: S3Wrapper; | ||
|
||
constructor( | ||
address: string, | ||
localDomain: number, | ||
mailbox: string, | ||
s3Bucket: string, | ||
s3Region: string, | ||
s3Folder: string | undefined, | ||
) { | ||
super(address, localDomain, mailbox); | ||
this.s3Bucket = new S3Wrapper(s3Bucket, s3Region, s3Folder, true); // caching enabled | ||
} | ||
|
||
static async fromStorageLocation( | ||
storageLocation: string, | ||
): Promise<S3Validator> { | ||
if (storageLocation.startsWith(LOCATION_PREFIX)) { | ||
const suffix = storageLocation.slice(LOCATION_PREFIX.length); | ||
const pieces = suffix.split('/'); | ||
if (pieces.length >= 2) { | ||
const s3Bucket = new S3Wrapper(pieces[0], pieces[1], pieces[2]); | ||
const announcement = await s3Bucket.getS3Obj<S3Announcement>( | ||
ANNOUNCEMENT_KEY, | ||
); | ||
if (!announcement) { | ||
throw new Error('No announcement found'); | ||
} | ||
|
||
const address = announcement.data.value.validator; | ||
const mailbox = announcement.data.value.mailbox_address; | ||
const localDomain = announcement.data.value.mailbox_domain; | ||
|
||
return new S3Validator( | ||
address, | ||
localDomain, | ||
mailbox, | ||
pieces[0], | ||
pieces[1], | ||
pieces[2], | ||
); | ||
} | ||
} | ||
throw new Error(`Unable to parse location ${storageLocation}`); | ||
} | ||
|
||
async getAnnouncement(): Promise<Announcement> { | ||
const resp = await this.s3Bucket.getS3Obj<S3Announcement>(ANNOUNCEMENT_KEY); | ||
if (!resp) { | ||
throw new Error('No announcement found'); | ||
} | ||
|
||
return resp.data.value; | ||
} | ||
|
||
async getCheckpoint(index: number) { | ||
const key = checkpointWithMessageIdKey(index); | ||
const s3Object = await this.s3Bucket.getS3Obj<S3CheckpointWithId>(key); | ||
if (!s3Object) { | ||
return; | ||
} | ||
|
||
if (isS3CheckpointWithId(s3Object.data)) { | ||
return s3Object.data; | ||
} else { | ||
throw new Error('Failed to parse checkpoint'); | ||
} | ||
} | ||
|
||
async getLatestCheckpointIndex() { | ||
const latestCheckpointIndex = await this.s3Bucket.getS3Obj<number>( | ||
LATEST_KEY, | ||
); | ||
|
||
if (!latestCheckpointIndex) return -1; | ||
|
||
return latestCheckpointIndex.data; | ||
} | ||
|
||
storageLocation(): string { | ||
return `${LOCATION_PREFIX}/${this.s3Bucket.bucket}/${this.s3Bucket.region}`; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.