Merge branch '41-bcmr-adapter-create-basebcmr-and-http-implementation' into 'development'
Resolve "BCMR Adapter: Create BaseBCMR and HTTP implementation" Closes #41 See merge request GeneralProtocols/xo/stack!124
This commit is contained in:
@@ -0,0 +1,42 @@
|
|||||||
|
import { MetadataRegistry } from './bcmr-helpers.js';
|
||||||
|
|
||||||
|
import { findAllKeyOcurrences } from '../utils/index.js';
|
||||||
|
|
||||||
|
export abstract class BaseBCMR {
|
||||||
|
/**
|
||||||
|
* Retrieves the metadata for a given token categoryId.
|
||||||
|
*
|
||||||
|
* @param {string} categoryId The token category id
|
||||||
|
*
|
||||||
|
* @throws {Error} If the fetching fails.
|
||||||
|
* @returns {Promise<MetadataRegistry>} A promise that resolves with the Metadata Registry.
|
||||||
|
*/
|
||||||
|
abstract getTokenData(categoryId: string): Promise<MetadataRegistry>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a list of all the ocurrences for a given token and field.
|
||||||
|
*
|
||||||
|
* @param {string} categoryId The token category id
|
||||||
|
* @param {string} field The field to lookup to.
|
||||||
|
*
|
||||||
|
* @throws {Error} If the fetching fails.
|
||||||
|
* @throws {Error} If there is not entry for the given 'categoryId' registered on the indexer.
|
||||||
|
* @returns {Promise<Array<unknown>>} A promise that resolves with the list of values for the given field.
|
||||||
|
*/
|
||||||
|
async searchField(
|
||||||
|
categoryId: string,
|
||||||
|
field: string,
|
||||||
|
): Promise<Array<unknown>> {
|
||||||
|
try {
|
||||||
|
const data = await this.getTokenData(categoryId);
|
||||||
|
|
||||||
|
const occurrencesList = findAllKeyOcurrences(data, field);
|
||||||
|
|
||||||
|
return occurrencesList;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
throw new Error(
|
||||||
|
`Error trying to get token metadata for ${categoryId}. Detailed error: ${error}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { BaseBCMR } from './base-bcmr.js';
|
||||||
|
|
||||||
|
import { BCMRHttp } from './bcmr-http.js';
|
||||||
|
|
||||||
|
export class BCMRComposite extends BaseBCMR {
|
||||||
|
constructor(public readonly bridges: Array<BCMRHttp>) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTokenData(categoryId: string) {
|
||||||
|
const getTokenDataPromises = this.bridges.map((bridge) =>
|
||||||
|
bridge.getTokenData(categoryId),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await Promise.any(getTokenDataPromises);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
throw new Error(
|
||||||
|
`Unable to get token metadata from any of the BCMR indexers. Detailed error: ${error}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { z } from 'zod/v4';
|
||||||
|
|
||||||
|
export const MetadataRegistrySchema = z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
uris: z
|
||||||
|
.object({
|
||||||
|
// Since 'icon' and 'web' are strongly recommended uri identifiers(https://github.com/bitjson/chip-bcmr?tab=readme-ov-file#recommended-uri-identifiers)
|
||||||
|
// We require it as non optional.
|
||||||
|
icon: z.url(),
|
||||||
|
web: z.url(),
|
||||||
|
})
|
||||||
|
// Allows any other keys with any values.
|
||||||
|
.catchall(z.unknown()),
|
||||||
|
token: z.object({
|
||||||
|
symbol: z.string(),
|
||||||
|
category: z.string(),
|
||||||
|
decimals: z.number().optional(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.catchall(z.unknown());
|
||||||
|
|
||||||
|
export type MetadataRegistry = z.infer<typeof MetadataRegistrySchema>;
|
||||||
@@ -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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// NOTE: Do not export specific providers in this file.
|
||||||
|
// Do this in the parent index.ts file instead (otherwise, we mingle interfaces and providers, which can create circular dependencies)..
|
||||||
|
export * from './base-bcmr.js';
|
||||||
@@ -79,11 +79,9 @@ export class BlockchainChaingraph
|
|||||||
extends EventEmitter<BlockchainEvents>
|
extends EventEmitter<BlockchainEvents>
|
||||||
implements
|
implements
|
||||||
Omit<
|
Omit<
|
||||||
BaseBlockchain<
|
// TODO: Need to make 'BaseBlockchain' able to notify that is able to return this?
|
||||||
ChaingraphLockscriptNotification
|
// CustomSubscriptionNotification
|
||||||
// TODO: Need to make 'BaseBlockchain' able to notify that is able to return this?
|
BaseBlockchain<ChaingraphLockscriptNotification>,
|
||||||
// CustomSubscriptionNotification
|
|
||||||
>,
|
|
||||||
'fetchAddressHistory'
|
'fetchAddressHistory'
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
@@ -101,10 +99,8 @@ export class BlockchainChaingraph
|
|||||||
> = new Map();
|
> = new Map();
|
||||||
|
|
||||||
// Transaction Subscription State.
|
// Transaction Subscription State.
|
||||||
transactionSubscriptions: Map<
|
transactionSubscriptions: Map<string, Set<BlockchainCallback<undefined>>> =
|
||||||
string,
|
new Map();
|
||||||
Set<BlockchainCallback<undefined>>
|
|
||||||
> = new Map();
|
|
||||||
|
|
||||||
// Custom Subscriptions.
|
// Custom Subscriptions.
|
||||||
customSubscriptions: Map<
|
customSubscriptions: Map<
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
export * from '@xocash/primitives';
|
export * from '@xocash/primitives';
|
||||||
|
|
||||||
// Export all of our stack-specific modules.
|
// Export all of our stack-specific modules.
|
||||||
|
export * from './bcmr/index.js';
|
||||||
export * from './blockchain/index.js';
|
export * from './blockchain/index.js';
|
||||||
export * from './protocols/index.js';
|
export * from './protocols/index.js';
|
||||||
export * from './rates/index.js';
|
export * from './rates/index.js';
|
||||||
@@ -12,6 +13,8 @@ export * from './wallet/index.js';
|
|||||||
// Export providers that work across both Browser and NodeJS.
|
// Export providers that work across both Browser and NodeJS.
|
||||||
// NOTE: For browser-only providers, export these from index.browser.ts.
|
// NOTE: For browser-only providers, export these from index.browser.ts.
|
||||||
// For node-only providers, export these from index.node.ts.
|
// For node-only providers, export these from index.node.ts.
|
||||||
|
export * from './bcmr/bcmr-composite.js';
|
||||||
|
export * from './bcmr/bcmr-http.js';
|
||||||
export * from './blockchain/blockchain-electrum.js';
|
export * from './blockchain/blockchain-electrum.js';
|
||||||
export * from './blockchain/blockchain-chaingraph.js';
|
export * from './blockchain/blockchain-chaingraph.js';
|
||||||
export * from './storage/store-remote-http.js';
|
export * from './storage/store-remote-http.js';
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export type StoreValueProxy<T> = {
|
|||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export abstract class BaseStore extends EventEmitter<StoreEvents> {
|
export abstract class BaseStore<K = unknown> extends EventEmitter<StoreEvents> {
|
||||||
/**
|
/**
|
||||||
* Returns an array of all keys in the store.
|
* Returns an array of all keys in the store.
|
||||||
*/
|
*/
|
||||||
@@ -91,7 +91,7 @@ export abstract class BaseStore extends EventEmitter<StoreEvents> {
|
|||||||
* @template T The type of the value to retrieve
|
* @template T The type of the value to retrieve
|
||||||
* @param key The key to lookup
|
* @param key The key to lookup
|
||||||
*/
|
*/
|
||||||
abstract get<T>(key: string): Promise<T | undefined>;
|
abstract get<T extends K>(key: string): Promise<T | undefined>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores a value in the store with the specified key.
|
* Stores a value in the store with the specified key.
|
||||||
@@ -100,7 +100,7 @@ export abstract class BaseStore extends EventEmitter<StoreEvents> {
|
|||||||
* @param key The key under which to store the value
|
* @param key The key under which to store the value
|
||||||
* @param value The value to store
|
* @param value The value to store
|
||||||
*/
|
*/
|
||||||
abstract set<T>(key: string, value: T): Promise<void>;
|
abstract set<T extends K>(key: string, value: T): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a value from the store by its key.
|
* Removes a value from the store by its key.
|
||||||
@@ -114,7 +114,7 @@ export abstract class BaseStore extends EventEmitter<StoreEvents> {
|
|||||||
*
|
*
|
||||||
* @template T The type of the values in the store
|
* @template T The type of the values in the store
|
||||||
*/
|
*/
|
||||||
async all<T>(): Promise<{ [key: string]: T }> {
|
async all<T extends K>(): Promise<{ [key: string]: T }> {
|
||||||
const keys = await this.keys();
|
const keys = await this.keys();
|
||||||
const values = await Promise.all(keys.map((key) => this.get<T>(key)));
|
const values = await Promise.all(keys.map((key) => this.get<T>(key)));
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@ export abstract class BaseStore extends EventEmitter<StoreEvents> {
|
|||||||
* @template T The type of the values to retrieve
|
* @template T The type of the values to retrieve
|
||||||
* @param keys Array of keys to retrieve
|
* @param keys Array of keys to retrieve
|
||||||
*/
|
*/
|
||||||
async batchGet<T>(keys: Array<string>): Promise<Map<string, T>> {
|
async batchGet<T extends K>(keys: Array<string>): Promise<Map<string, T>> {
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
keys.map(async (key) => {
|
keys.map(async (key) => {
|
||||||
const value = await this.get(key);
|
const value = await this.get(key);
|
||||||
@@ -174,7 +174,7 @@ export abstract class BaseStore extends EventEmitter<StoreEvents> {
|
|||||||
* @template T The type of the values to store
|
* @template T The type of the values to store
|
||||||
* @param items Map of key-value pairs to store
|
* @param items Map of key-value pairs to store
|
||||||
*/
|
*/
|
||||||
async batchSet<T>(items: Map<string, T>): Promise<void> {
|
async batchSet<T extends K>(items: Map<string, T>): Promise<void> {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Array.from(items).map(([key, value]) => {
|
Array.from(items).map(([key, value]) => {
|
||||||
return this.set(key, value);
|
return this.set(key, value);
|
||||||
@@ -197,7 +197,7 @@ export abstract class BaseStore extends EventEmitter<StoreEvents> {
|
|||||||
* @template T The type of the value to retrieve
|
* @template T The type of the value to retrieve
|
||||||
* @param key The key to lookup
|
* @param key The key to lookup
|
||||||
*/
|
*/
|
||||||
async getOrFail<T>(key: string): Promise<T> {
|
async getOrFail<T extends K>(key: string): Promise<T> {
|
||||||
// Attempt to get the value from the store.
|
// Attempt to get the value from the store.
|
||||||
const value = await this.get<T>(key);
|
const value = await this.get<T>(key);
|
||||||
|
|
||||||
@@ -217,7 +217,10 @@ export abstract class BaseStore extends EventEmitter<StoreEvents> {
|
|||||||
* @param key The key to lookup
|
* @param key The key to lookup
|
||||||
* @param setter Function to generate the value if it doesn't exist
|
* @param setter Function to generate the value if it doesn't exist
|
||||||
*/
|
*/
|
||||||
async getOrSet<T>(key: string, setter: () => T | Promise<T>): Promise<T> {
|
async getOrSet<T extends K>(
|
||||||
|
key: string,
|
||||||
|
setter: () => T | Promise<T>,
|
||||||
|
): Promise<T> {
|
||||||
// Attempt to get the value from the store.
|
// Attempt to get the value from the store.
|
||||||
let value = await this.get<T>(key);
|
let value = await this.get<T>(key);
|
||||||
|
|
||||||
@@ -246,7 +249,7 @@ export abstract class BaseStore extends EventEmitter<StoreEvents> {
|
|||||||
* await numValue.set(42); // sets the stored value
|
* await numValue.set(42); // sets the stored value
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
proxy<T>(key: string): StoreValueProxy<T> {
|
proxy<T extends K>(key: string): StoreValueProxy<T> {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -39,3 +39,75 @@ export function hasDuplicateParams(url: string | URL): boolean {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given object is a Record.
|
||||||
|
*
|
||||||
|
* @param object The variable to check.
|
||||||
|
* @returns True if the object is a record.
|
||||||
|
*/
|
||||||
|
export function isRecord(
|
||||||
|
value: unknown,
|
||||||
|
): value is Record<string | number | symbol, unknown> {
|
||||||
|
if (typeof value !== 'object' || value === null) return false;
|
||||||
|
|
||||||
|
// Check if its a plain object (not a class instance, array, etc.)
|
||||||
|
const prototype = Object.getPrototypeOf(value);
|
||||||
|
return prototype === null || prototype === Object.prototype;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all the ocurrences of a given key in a plain JS object.
|
||||||
|
* NOTE: This function is intended to search keys in JSON objects
|
||||||
|
* and it will not search the key walking through collection object structures(Set, Map, etc.)
|
||||||
|
* NOTE: This function is intended to be used to traverse not too nested JSON objects.
|
||||||
|
*
|
||||||
|
* @param object The plain JS Object to traverse.
|
||||||
|
* @param key The Key to search.
|
||||||
|
*
|
||||||
|
* @throws If object can't be serializated as JSON.
|
||||||
|
* @throws If object is more than 100 levels nested.
|
||||||
|
* @returns A list of values for the given key.
|
||||||
|
*/
|
||||||
|
export function findAllKeyOcurrences(
|
||||||
|
object: Record<string | number | symbol, unknown>,
|
||||||
|
key: string,
|
||||||
|
): Array<unknown> {
|
||||||
|
// Check object serialization as JSON to avoid circular reference, BigInt values or other object structures(Set, Map, Date, etc..)
|
||||||
|
JSON.stringify(object);
|
||||||
|
|
||||||
|
// Under 100 the resource allocation is excessively fine for any JS environment or hardware.
|
||||||
|
// NOTE: If for some reason this needs to be changed be sure to add proper test cases to measure performance.
|
||||||
|
// Currently(11/09/2025) the JS stack size can handle roughly 10000 recursive calls on browsers and NodeJS.
|
||||||
|
const allowedNestedLevels = 100;
|
||||||
|
|
||||||
|
const keyValues: Array<unknown> = [];
|
||||||
|
|
||||||
|
function traverse(currentObj: Record<string | number | symbol, unknown>, depth: number = 0) {
|
||||||
|
// Stop if depth exceeds the amount of 'allowedNestedLevels'.
|
||||||
|
if (depth > allowedNestedLevels)
|
||||||
|
throw new Error(`Maximum nesting level of ${allowedNestedLevels} reached.`);
|
||||||
|
|
||||||
|
// Stop when find a non object value.
|
||||||
|
if (typeof currentObj !== 'object' || currentObj === null) return;
|
||||||
|
|
||||||
|
if (Array.isArray(currentObj)) {
|
||||||
|
currentObj.forEach((item) => traverse(item, depth + 1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if current object has the key property we are looking
|
||||||
|
if (key in currentObj) {
|
||||||
|
keyValues.push(currentObj[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively traverse all properties
|
||||||
|
Object.values(currentObj).forEach((value) => {
|
||||||
|
if (isRecord(value)) traverse(value, depth + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse(object);
|
||||||
|
|
||||||
|
return keyValues;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { test, expect, describe } from 'vitest';
|
||||||
|
|
||||||
|
import { BCMRComposite } from '../../src/bcmr/bcmr-composite.js';
|
||||||
|
import { BCMRHttp } from '../../src/index.js';
|
||||||
|
|
||||||
|
// musd-v1
|
||||||
|
const categoryId =
|
||||||
|
'b38a33f750f84c5c169a6f23cb873e6e79605021585d4f3408789689ed87f366';
|
||||||
|
const musdTokenField = {
|
||||||
|
x: 'https://x.com/MoriaMoney/',
|
||||||
|
web: 'https://moria.money',
|
||||||
|
icon: 'https://meta.moria.money/m1.png',
|
||||||
|
telegram: 'https://t.me/moriamoney',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('BCMRComposite', () => {
|
||||||
|
const bcmrComposite = new BCMRComposite([
|
||||||
|
new BCMRHttp({ indexerUri: 'https://bcmr.paytaca.com/api/tokens/' }),
|
||||||
|
new BCMRHttp({
|
||||||
|
indexerUri: 'https://bcmr.flowee.cash/',
|
||||||
|
endpointEndsWithDotJson: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
test('should return token metadata', async () => {
|
||||||
|
const metadata = await bcmrComposite.getTokenData(categoryId);
|
||||||
|
|
||||||
|
expect(metadata).toBeDefined();
|
||||||
|
expect(metadata?.token.symbol).toBeDefined();
|
||||||
|
expect(metadata?.token.symbol).toBe('MUSD');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('metadata should have a uris field that matches', async () => {
|
||||||
|
const matches = await bcmrComposite.searchField(categoryId, 'uris');
|
||||||
|
|
||||||
|
expect(Array.isArray(matches)).toBeTruthy();
|
||||||
|
expect((matches as Array<unknown>).length).toBeGreaterThan(0);
|
||||||
|
expect((matches as Array<unknown>)[0]).toMatchObject(musdTokenField);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fail if cantegoryId is not a registered entry', async () => {
|
||||||
|
const invalidCategoryId = 'asd123';
|
||||||
|
|
||||||
|
await expect(bcmrComposite.getTokenData(invalidCategoryId)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"lib": ["dom"]
|
"lib": ["dom", "es2021"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
|
|||||||
Reference in New Issue
Block a user