Resolve "BCMR Adapter: Create BaseBCMR and HTTP implementation"
This commit is contained in:
committed by
Emergent Reasons
parent
6d0a36248d
commit
e4020a996c
@@ -0,0 +1,130 @@
|
||||
import { BaseBCMR } from './base-bcmr.js';
|
||||
|
||||
import { MetadataRegistry, MetadataRegistrySchema } from './bcmr-helpers.js';
|
||||
|
||||
import { type BaseStore, StoreInMemory } from '../storage/index.js';
|
||||
|
||||
export type BCMRHttpOpts = {
|
||||
store?: BaseStore<CachedMetadataRegistry>;
|
||||
indexerUri: string;
|
||||
expireCacheAfterSeconds?: number;
|
||||
endpointEndsWithDotJson?: boolean;
|
||||
};
|
||||
|
||||
type CachedMetadataRegistry = {
|
||||
timestampInSeconds: number;
|
||||
metadataRegistry: MetadataRegistry;
|
||||
};
|
||||
|
||||
// The proposal is to expire cache after 7 days.
|
||||
// https://github.com/bitjson/chip-bcmr?tab=readme-ov-file#well-known-uri
|
||||
const DEFAULT_EXPIRE_CACHE_AFTER_SECONDS = 60 * 60 * 24 * 7;
|
||||
|
||||
// Define the options for our POST request.
|
||||
const fetchOptions = {
|
||||
method: 'GET',
|
||||
cache: 'no-cache' as const,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
|
||||
export class BCMRHttp extends BaseBCMR {
|
||||
// Options
|
||||
private store: BaseStore<CachedMetadataRegistry>;
|
||||
private cacheExpirationInSeconds: number;
|
||||
private indexerUri: string;
|
||||
private endpointEndsWithDotJson: boolean;
|
||||
|
||||
constructor(opts: BCMRHttpOpts) {
|
||||
// Check that given indexer url is valid.
|
||||
BCMRHttp.isUrl(opts.indexerUri);
|
||||
|
||||
super();
|
||||
|
||||
// Set the store, or use the memory store by default.
|
||||
this.store = opts.store ?? new StoreInMemory();
|
||||
|
||||
this.cacheExpirationInSeconds =
|
||||
opts.expireCacheAfterSeconds ?? DEFAULT_EXPIRE_CACHE_AFTER_SECONDS;
|
||||
|
||||
const endChar = opts.indexerUri.slice(-1) === '/' ? '' : '/';
|
||||
this.indexerUri = `${opts.indexerUri}${endChar}`;
|
||||
|
||||
this.endpointEndsWithDotJson = Boolean(opts.endpointEndsWithDotJson);
|
||||
}
|
||||
|
||||
async getTokenData(categoryId: string) {
|
||||
// Define the cache key.
|
||||
const cacheKey = `tokenMetadata.${categoryId}`;
|
||||
|
||||
const cachedTokenMetadata = await this.store.get(cacheKey);
|
||||
|
||||
if (cachedTokenMetadata) {
|
||||
// Check if cache is not expired
|
||||
if (
|
||||
Date.now() <
|
||||
(cachedTokenMetadata.timestampInSeconds +
|
||||
this.cacheExpirationInSeconds) *
|
||||
1000
|
||||
)
|
||||
return cachedTokenMetadata.metadataRegistry;
|
||||
|
||||
// Otherwise if expired...
|
||||
await this.store.delete(cacheKey);
|
||||
}
|
||||
|
||||
try {
|
||||
const metadataRegistry = await this.fetchMetadata(categoryId);
|
||||
|
||||
const cachedMetadataRegistry: CachedMetadataRegistry = {
|
||||
timestampInSeconds: Date.now() / 1000,
|
||||
metadataRegistry,
|
||||
};
|
||||
|
||||
// Cache token metadata.
|
||||
await this.store.set(cacheKey, cachedMetadataRegistry);
|
||||
|
||||
return metadataRegistry;
|
||||
} catch (error: unknown) {
|
||||
throw new Error(
|
||||
`Error trying to fetch token metadata. Detailed error: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to fetch the metadata of a given token 'categoryId' using the given indexer.
|
||||
*
|
||||
* @throws {Error} If the fetch fails.
|
||||
* @throws {Error} If the response fails to be parsed.
|
||||
*/
|
||||
private async fetchMetadata(categoryId: string): Promise<MetadataRegistry> {
|
||||
// Prepare the uri to fetch the token metadata.
|
||||
const uri = `${this.indexerUri}${categoryId}${this.endpointEndsWithDotJson ? '.json' : ''}`;
|
||||
|
||||
const response = await fetch(uri, fetchOptions);
|
||||
|
||||
// If the request was not successful...
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
`Error trying to fetch token (${categoryId}) metadata at ${uri}`,
|
||||
);
|
||||
|
||||
// Validate the payload body.
|
||||
try{
|
||||
const metadata = MetadataRegistrySchema.parse(await response.json());
|
||||
|
||||
return metadata;
|
||||
} catch(error: unknown) {
|
||||
throw new Error(`Unable to parse the metadata registry format. Detailed error: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
static isUrl(value: string): boolean {
|
||||
// Validate as URL using try-catch
|
||||
try {
|
||||
return Boolean(new URL(value));
|
||||
} catch (error: unknown) {
|
||||
throw new Error(`Invalid URL. Detailed error: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user