Merge branch 'add-oracle-rates-adapter' into 'development'
Add Oracle Rates Adapter See merge request GeneralProtocols/xo/stack!99
This commit is contained in:
Generated
+14
-3
@@ -1156,6 +1156,16 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@generalprotocols/oracle-client": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@generalprotocols/oracle-client/-/oracle-client-0.0.1.tgz",
|
||||
"integrity": "sha512-TmnPCUm1VYeWK7SkWV8w3jAjUOW9SFLPgF8ni06ouaGYyer/35oZ5OW+6R3kpFtRWhO4rlnT9HYL8SHE3Yj0+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@bitauth/libauth": "^3.0.0",
|
||||
"zod": "^3.24.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@gerrit0/mini-shiki": {
|
||||
"version": "1.24.4",
|
||||
"resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-1.24.4.tgz",
|
||||
@@ -14695,9 +14705,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.24.2",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
|
||||
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
|
||||
"version": "3.25.64",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.64.tgz",
|
||||
"integrity": "sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
@@ -14748,6 +14758,7 @@
|
||||
"dependencies": {
|
||||
"@electrum-cash/network": "^4.1.1",
|
||||
"@electrum-cash/protocol": "2.1.0-development.8075178089",
|
||||
"@generalprotocols/oracle-client": "^0.0.1",
|
||||
"@xocash/primitives": "*",
|
||||
"better-sqlite3": "^11.8.1",
|
||||
"idb": "8.0.3",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"dependencies": {
|
||||
"@electrum-cash/network": "^4.1.1",
|
||||
"@electrum-cash/protocol": "2.1.0-development.8075178089",
|
||||
"@generalprotocols/oracle-client": "^0.0.1",
|
||||
"@xocash/primitives": "*",
|
||||
"better-sqlite3": "^11.8.1",
|
||||
"idb": "8.0.3",
|
||||
|
||||
@@ -16,6 +16,7 @@ export * from './storage/store-remote-http.js';
|
||||
export * from './rates/rates-bitpay.js';
|
||||
export * from './rates/rates-coinbase.js';
|
||||
export * from './rates/rates-cryptocompare.js';
|
||||
export * from './rates/rates-oracle.js';
|
||||
export * from './wallet/wallet-hd.js';
|
||||
export * from './wallet/wallet-p2pkh.js';
|
||||
export * from './wallet/wallet-p2sh20.js';
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user