diff --git a/README.md b/README.md index 017bb54..e46d637 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,11 @@ When verified the pyth contract verifies the merkle proof and extracts the price ### Oracle contract Proof logic taken from: + [Wormhole proof logic Cosmwasm](https://github.com/wormhole-foundation/wormhole/tree/main/cosmwasm/contracts/wormhole/src) + [Pyth merkle proof logic Cosmwasm](https://github.com/pyth-network/pyth-crosschain/tree/main/target_chains/cosmwasm/contracts/pyth/src) + [Pyth merkle proof logic Solidity](https://github.com/pyth-network/pyth-crosschain/tree/main/target_chains/ethereum/contracts/contracts/pyth) diff --git a/price-service/README.md b/price-service/README.md new file mode 100644 index 0000000..f9a8cd7 --- /dev/null +++ b/price-service/README.md @@ -0,0 +1,61 @@ +# Price monitor + +## Start polling service +``` bash +npm run start-poll +``` + +## Start streaming service +``` bash +npm run start-stream +``` +## Configuring service + +- **pyth_url**: The URL for the Pyth network hermes. + - Example: `https://hermes.pyth.network` + +- **icon_url**: The URL for the ICON network API. + - Example: `https://lisbon.net.solidwallet.io/api/v3` + +- **icon_pk**: Wallet private key for the ICON network. + - Example: `""` + +- **address**: The blockchain address for the contract on the ICON network, + - Example: `cx7380205103a9076aae26d1c761a8bb6652ecf30f` + +- **nid**: Network ID for the ICON network, specifying the network environment (e.g., mainnet, testnet). + - Example: `0x2` + +- **priceIds**: A list of identifiers for specific data feeds provided by the Pyth network. [Feeds](https://pyth.network/price-feeds) + - Example: + ``` json + [ + "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43" + "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace" + "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d" + "0xb7a8eba68a997cd0210c2e1e4ee811ad2d174b3611c22d9ebf16f4cb7e9ba850" + ] + ``` +- **priceChangeThreshold**: The value price change threshold used in streaming, in %, determining when prices should be updated. + - Example: `0.2` + +- **interval**: The time interval, in seconds, between price updates. In streaming is only used if priceChangeThreshold is not hit within the interval + - Example: `300` + +# How to setup a price delivery service using PM2 +To for example setup a price monitor on a EC2 instance: + +## Setup Polling service +``` bash +pm2 start ./node_modules/.bin/ts-node --name "price-monitor" -- src/servicePoll.ts +``` +## Setup Streaming service +``` bash +pm2 start ./node_modules/.bin/ts-node --name "price-monitor" -- src/serviceStreaming.ts +``` + +Configure service to start on startup +``` bash +pm2 save +pm2 startup +``` diff --git a/price-service/example_config.json b/price-service/example_config.json index e8d6f3e..14f96bd 100644 --- a/price-service/example_config.json +++ b/price-service/example_config.json @@ -2,6 +2,8 @@ "pyth_url": "https://hermes.pyth.network", "icon_url": "https://lisbon.net.solidwallet.io/api/v3", "icon_pk": "", + "address": "cx7380205103a9076aae26d1c761a8bb6652ecf30f", + "nid": "0x2", "priceIds": [ "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43", "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace", @@ -14,7 +16,7 @@ "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d", "0xb7a8eba68a997cd0210c2e1e4ee811ad2d174b3611c22d9ebf16f4cb7e9ba850" ], - "threshold": 0.2, + "priceChangeThreshold": 0.2, "interval": 300 } \ No newline at end of file diff --git a/price-service/package.json b/price-service/package.json index 6afff95..218661b 100644 --- a/price-service/package.json +++ b/price-service/package.json @@ -4,7 +4,8 @@ "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "start": "ts-node src/index.ts" + "start-stream": "ts-node src/serviceStream.ts", + "start-poll": "ts-node src/servicePoll.ts" }, "keywords": [], "author": "", diff --git a/price-service/src/index.ts b/price-service/src/index.ts index 87880ac..6d3f9b3 100644 --- a/price-service/src/index.ts +++ b/price-service/src/index.ts @@ -20,11 +20,13 @@ const priceMonitor = new PriceMonitor(config); priceMonitor.onPriceUpdate(data); }; - // Error handling for the event source - // eventSource.onerror = (err) => { - // }; + eventSource.onerror = (error: any) => { + console.log("Data stream failed: "+ error) + process.exit(1); + }; } catch (error) { + console.log("Data stream failed: "+ error) process.exit(1); } })(); \ No newline at end of file diff --git a/price-service/src/priceFeed.ts b/price-service/src/priceFeed.ts index 954bd29..80fe075 100644 --- a/price-service/src/priceFeed.ts +++ b/price-service/src/priceFeed.ts @@ -1,5 +1,3 @@ -// dataInterfaces.ts - export interface BinaryData { encoding: string; data: string[]; diff --git a/price-service/src/priceMonitor.ts b/price-service/src/priceMonitor.ts index 979267b..b03264f 100644 --- a/price-service/src/priceMonitor.ts +++ b/price-service/src/priceMonitor.ts @@ -1,8 +1,9 @@ import { Price, PriceFeed } from "./priceFeed"; +import { ServiceConfig } from "./serviceConfig"; import IconService, { HttpProvider, CallTransactionBuilder, Wallet, SignedTransaction} from 'icon-sdk-js'; - export class PriceMonitor { + private DEFAULT_STEP_LIMIT: number = 400000000 private prices: Map = new Map(); private threshold: number; private minInterval: number; @@ -10,43 +11,48 @@ export class PriceMonitor { private provider:HttpProvider; private iconService: IconService; private wallet: Wallet; + private address: string; + private nid: string; + private stepLimit: number; - constructor(config: any) { - this.threshold = config.threshold / 100; + constructor(config: ServiceConfig) { + this.threshold = config.priceChangeThreshold / 100; this.minInterval = config.interval; this.provider = new HttpProvider(config.icon_url); this.iconService = new IconService(this.provider); this.wallet = Wallet.loadPrivateKey(config.icon_pk); + this.address = config.address; + this.nid = config.nid; + this.stepLimit = config.stepLimit || this.DEFAULT_STEP_LIMIT; } public async onPriceUpdate(feed: PriceFeed): Promise { - if (this.processing) { - console.log(`Skipping update`); return; } this.processing = true try { - const parsed = feed.parsed; - - for (let priceEntry of parsed) { + for (let priceEntry of feed.parsed) { let lastUpdate = this.prices.get(priceEntry.id); - if (lastUpdate) { + if (lastUpdate) { const priceChange = Math.abs(priceEntry.price.price - lastUpdate.price) / lastUpdate.price; - console.log(priceChange); - - if (priceChange >= this.threshold || priceEntry.price.publish_time - lastUpdate.publish_time >= this.minInterval) { - await this.updatePrice(feed); - return; + const timeSinceLastUpdate = priceEntry.price.publish_time - lastUpdate.publish_time + if (priceChange < this.threshold && timeSinceLastUpdate < this.minInterval) { + continue; } - } else { - await this.updatePrice(feed); - return; } + if (!await this.updatePrice(feed)) { + return + } + + feed.parsed.forEach(priceEntry => { + this.prices.set(priceEntry.id, priceEntry.price) + }) + return; }; } finally { @@ -54,14 +60,14 @@ export class PriceMonitor { } } - private async updatePrice(feed: PriceFeed): Promise { + public async updatePrice(feed: PriceFeed): Promise { const timestamp = (new Date()).getTime() * 1000; let tx = new CallTransactionBuilder() - .nid("0x2") + .nid(this.nid) .from(this.wallet.getAddress()) - .stepLimit(400000000) + .stepLimit(this.stepLimit) .timestamp(timestamp) - .to("cx7380205103a9076aae26d1c761a8bb6652ecf30f") + .to(this.address) .method("updatePriceFeed") .params({ "data": feed.binary.data, @@ -69,12 +75,34 @@ export class PriceMonitor { .version("0x3") .build(); - const signedTransaction: SignedTransaction = new SignedTransaction(tx, this.wallet); - const res = await this.iconService.sendTransaction(signedTransaction).execute(); - console.log(res); - const parsed = feed.parsed; - parsed.forEach(priceEntry => { - this.prices.set(priceEntry.id, priceEntry.price) - }) + const signedTransaction: SignedTransaction = new SignedTransaction(tx, this.wallet); + const txHash = await this.iconService.sendTransaction(signedTransaction).execute(); + const transactionResult = await this.getTxResult(txHash); + const res = transactionResult.status === 1; + if (!res) { + console.log(transactionResult) + } + + console.log(txHash) + return res + } + + private async getTxResult(txHash: string): Promise { + let attempt = 0; + let maxRetries = 10; + while (attempt < maxRetries) { + try { + const result = await this.iconService.getTransactionResult(txHash).execute(); + return result; // If the function is successful, return the result + } catch (error) { + attempt++; + if (attempt >= maxRetries) { + throw new Error(`Failed to resolve ${txHash}: ${error}`); + } + await new Promise(resolve => setTimeout(resolve, 1000)); // Wait before retrying + } + } + + throw new Error(`Failed after ${attempt} attempts`); } } diff --git a/price-service/src/serviceConfig.ts b/price-service/src/serviceConfig.ts new file mode 100644 index 0000000..118be9b --- /dev/null +++ b/price-service/src/serviceConfig.ts @@ -0,0 +1,11 @@ +export interface ServiceConfig { + pyth_url: string; + icon_url: string; + icon_pk: string; + address: string; + nid: string; + stepLimit: number; + priceIds: string[]; + priceChangeThreshold: number; + interval: number; +} \ No newline at end of file diff --git a/price-service/src/servicePoll.ts b/price-service/src/servicePoll.ts new file mode 100644 index 0000000..6e79d58 --- /dev/null +++ b/price-service/src/servicePoll.ts @@ -0,0 +1,21 @@ +import { PriceMonitor } from "./priceMonitor"; + +const { HermesClient } = require('@pythnetwork/hermes-client'); +const fs = require('fs'); + +const config = JSON.parse(fs.readFileSync('config.json', 'utf8')); +const priceMonitor = new PriceMonitor(config); +const connection = new HermesClient(config.pyth_url); + +async function poll() { + try { + const priceUpdates = await connection.getLatestPriceUpdates(config.priceIds); + priceMonitor.updatePrice(priceUpdates) + + } catch (error) { + console.log("Data stream failed: "+ error) + process.exit(1); + } +} + +setInterval(poll, config.interval*1000) diff --git a/price-service/src/serviceStream.ts b/price-service/src/serviceStream.ts new file mode 100644 index 0000000..a1c6b9c --- /dev/null +++ b/price-service/src/serviceStream.ts @@ -0,0 +1,20 @@ +import { PriceMonitor } from "./priceMonitor"; + +const { HermesClient } = require('@pythnetwork/hermes-client'); +const fs = require('fs'); + +const config = JSON.parse(fs.readFileSync('config.json', 'utf8')); +const priceMonitor = new PriceMonitor(config); +(async () => { + try { + const connection = new HermesClient(config.pyth_url); + const eventSource = await connection.getPriceUpdatesStream(config.priceIds); + eventSource.onmessage = (event: MessageEvent) => { + const data = JSON.parse(event.data); + priceMonitor.onPriceUpdate(data); + }; + + } catch (error) { + process.exit(1); + } +})(); \ No newline at end of file