import { OracleClient, OracleMetadataMap, OracleMetadataMessage, OraclePriceMessage, } from '@generalprotocols/oracle-client'; import { BaseRates } from './index.js'; // TODO: Add RatesHistorical trait since Oracles can provide historical rates. export class RatesOracle extends BaseRates { /** * Create a new rates oracle. * * @param client The underlying oracle client. If not provided, a new client will be created. * @returns The rates oracle. */ static async from(client?: OracleClient) { const ratesOracle = new RatesOracle(client ?? (await OracleClient.from())); return ratesOracle; } private client: OracleClient; private oracles: OracleMetadataMap; private started: boolean = false; private constructor(client: OracleClient) { super(); this.client = client; this.oracles = {}; } /** * Start the rates oracle and the underlying client. */ async start() { if (this.started) { return; } this.started = true; // Create event listeners for the client. this.client.setOnMetadataMessage(this.handleMetadataMessage.bind(this)); this.client.setOnPriceMessage(this.handlePriceMessage.bind(this)); // Get the metadata for the client. this.oracles = await this.client.getMetadataMap(); // Start the client. await this.client.start(); // Refresh the prices. await this.refreshPrices(); } /** * Stop the rates oracle and the underlying client. */ async stop() { if (!this.started) { return; } this.started = false; // Remove event listeners by setting them to empty functions. this.client.setOnMetadataMessage(() => {}); this.client.setOnPriceMessage(() => {}); await this.client.stop(); } /** * List the pairs that we are tracking. * * @returns A set of pairs. */ async listPairs() { return new Set( Object.values(this.oracles).map((oracle) => { return `${oracle.SOURCE_NUMERATOR_UNIT_CODE}/${oracle.SOURCE_DENOMINATOR_UNIT_CODE}`; }), ); } /** * Get the latest prices for all the pairs and emit a rate updated event for each. */ public async refreshPrices() { const oracles = await this.client.getOracles(); // For each oracle, get the lastest dataSequence (price) message and emit a rate updated event. await Promise.all( oracles.map(async (oracle) => { const messages = await this.client.getOracleMessages({ publicKey: oracle.publicKey, minDataSequence: 1, count: 1, }); // We are only expecting a single message back. Just in case, we take the latest one. const message = messages.reduce((latest, msg) => { if ( msg instanceof OraclePriceMessage && msg.messageSequence > latest.messageSequence ) { return msg; } return latest; }, messages[0]); // If the message is a price message, handle it. if (message instanceof OraclePriceMessage) { this.handlePriceMessage(message); } }), ); } /** * Update the metadata map that we use to track the pairs. * * @param message The metadata message. */ private handleMetadataMessage(message: OracleMetadataMessage) { this.oracles = OracleClient.updateMetadataMap(this.oracles, message); } /** * Emit a rate updated event for the given pair. * * @param message The price message. */ private handlePriceMessage(message: OraclePriceMessage) { const oracle = this.oracles[message.toHexObject().publicKey]; // If the oracle doesn't have the required metadata, we can't use it. if ( !oracle || !oracle.SOURCE_NUMERATOR_UNIT_CODE || !oracle.SOURCE_DENOMINATOR_UNIT_CODE || !oracle.ATTESTATION_SCALING ) { return; } // Scale the price const priceValue = message.priceValue / oracle.ATTESTATION_SCALING; this.emit('rateUpdated', { numeratorUnitCode: oracle.SOURCE_NUMERATOR_UNIT_CODE, denominatorUnitCode: oracle.SOURCE_DENOMINATOR_UNIT_CODE, price: priceValue, }); } }