diff --git a/backend/main.ts b/backend/main.ts new file mode 100644 index 000000000..138a4139b --- /dev/null +++ b/backend/main.ts @@ -0,0 +1,12 @@ +/** + * Pricecaster Service. + * + * Fetcher backend component. + * + * (c) 2021 Randlabs, Inc. + */ + +import { PriceFetcher } from './pricefetch' + +const pricefetcher = new PriceFetcher() +pricefetcher.run() diff --git a/backend/pricefetch.ts b/backend/pricefetch.ts index 86cb3697f..00cf3f1e2 100644 --- a/backend/pricefetch.ts +++ b/backend/pricefetch.ts @@ -1,24 +1,69 @@ +/** + * Pricecaster Service. + * + * Fetcher backend component. + * + * (c) 2021 Randlabs, Inc. + */ + import { getPythProgramKeyForCluster, PriceData, Product, PythConnection } from '@pythnetwork/client' -import {Cluster, clusterApiUrl, Connection, PublicKey} from '@solana/web3.js' +import { Cluster, clusterApiUrl, Connection } from '@solana/web3.js' +import algosdk from 'algosdk' +const PricecasterLib = require('../lib/pricecaster') +const settings = require('../settings') -const SOLANA_CLUSTER_NAME: Cluster = 'devnet' -const connection = new Connection(clusterApiUrl(SOLANA_CLUSTER_NAME)) -const pythPublicKey = getPythProgramKeyForCluster(SOLANA_CLUSTER_NAME) +/** + * The main Price fetcher service class. + */ +export class PriceFetcher { + private pclib: any + pythConnection: PythConnection -const algosdk = require('algosdk') + constructor () { + const SOLANA_CLUSTER_NAME: Cluster = settings.pyth.solanaClusterName as Cluster + const connection = new Connection(clusterApiUrl(SOLANA_CLUSTER_NAME)) + const pythPublicKey = getPythProgramKeyForCluster(SOLANA_CLUSTER_NAME) + const algodClient = new algosdk.Algodv2(settings.token, settings.api, settings.port) + this.pythConnection = new PythConnection(connection, pythPublicKey) + this.pclib = new PricecasterLib.PricecasterLib(algodClient) + } + /** + * Starts the service. + */ + run () { + console.log('Pricecaster Service Fetcher -- (c) 2021 Randlabs.io\n') + console.log('AlgoClient Configuration: ') + console.log(`API: '${settings.api}' PORT:'${settings.port}'`) - -const pythConnection = new PythConnection(connection, pythPublicKey) -pythConnection.onPriceChange((product: Product, price: PriceData) => { - if (product.symbol == 'BTC/USD') { - // sample output: - // SRM/USD: $8.68725 ±$0.0131 - // tslint:disable-next-line:no-console - console.log(`${product.symbol}: $${price.price} \xB1$${price.confidence}`) + if (this.preflightCheck()) { + console.log('Preflight check passed, starting Pyth listener...') + this.pythConnection.start() + this.pythConnection.onPriceChange((product: Product, price: PriceData) => { + this.onPriceChange(product, price) + }) } -}) -// tslint:disable-next-line:no-console -console.log("Reading from Pyth price feed...") -pythConnection.start() \ No newline at end of file + console.log('Booting done.') + } + + /** + * Executes a preflight check of configuration parameters. + * @returns True if parameters are ok, false otherwise. + */ + preflightCheck () { + return true + } + + /** + * Price reception handler. + * @param product The reported symbol/pair + * @param price The reported price data + */ + onPriceChange (product: Product, price: PriceData) { + // eslint-disable-next-line no-prototype-builtins + if (settings.symbols.hasOwnProperty(product.symbol)) { + console.log(`${product.symbol}: $${price.price} \xB1$${price.confidence}`) + } + } +} diff --git a/backend/strategy.ts b/backend/strategy.ts new file mode 100644 index 000000000..186bf9849 --- /dev/null +++ b/backend/strategy.ts @@ -0,0 +1,38 @@ +/** + * Pricecaster Service. + * + * Fetcher backend component. + * + * (c) 2021 Randlabs, Inc. + */ + +import { PriceData } from '@pythnetwork/client' + +/** + * Implements a strategy for obtaining an asset price from + * a set of received prices in a buffer. + */ +export interface IStrategy { + /** + * + * @param size The size of the buffer + */ + createBuffer(size: number): void + + /** + * Clear price buffer + */ + clearBuffer(): void + + /** + * Put a new price in buffer. + * @param priceData The price data to put + * @returns true if successful. + */ + put(priceData: PriceData): boolean + + /** + * Get the calculated price according to selected strategy. + */ + getPrice(): number +} diff --git a/backend/strategyAvgPrice.ts b/backend/strategyAvgPrice.ts new file mode 100644 index 000000000..7c60f2045 --- /dev/null +++ b/backend/strategyAvgPrice.ts @@ -0,0 +1,15 @@ +/** + * Pricecaster Service. + * + * Fetcher backend component. + * + * (c) 2021 Randlabs, Inc. + */ + +import { StrategyBaseQueue } from './strategyqueuebase' + +class StrategyAveragePrice extends StrategyBaseQueue { + getPrice (): number { + return 0 + } +} diff --git a/backend/strategyBase.ts b/backend/strategyBase.ts new file mode 100644 index 000000000..8da35158a --- /dev/null +++ b/backend/strategyBase.ts @@ -0,0 +1,23 @@ +/** + * Pricecaster Service. + * + * Fetcher backend component. + * + * (c) 2021 Randlabs, Inc. + */ + +import { PriceData } from '@pythnetwork/client' +import { IStrategy } from './strategy' + +export abstract class StrategyBase implements IStrategy { + protected symbol: string + constructor (symbol: string, bufSize: number) { + this.symbol = symbol + this.createBuffer(bufSize) + } + + abstract put(priceData: PriceData): boolean + abstract createBuffer(size: number): void + abstract clearBuffer(): void + abstract getPrice(): number +} diff --git a/backend/strategyQueueBase.ts b/backend/strategyQueueBase.ts new file mode 100644 index 000000000..6d8103e30 --- /dev/null +++ b/backend/strategyQueueBase.ts @@ -0,0 +1,36 @@ +/** + * Pricecaster Service. + * + * Fetcher backend component. + * + * (c) 2021 Randlabs, Inc. + */ + +import { PriceData } from '@pythnetwork/client' +import { StrategyBase } from './strategyBase' + +/** + * A base class for queue-based buffer strategies + */ +export abstract class StrategyBaseQueue extends StrategyBase { + private buffer: PriceData[] + private bufSize: number + + createBuffer (maxSize: number): void { + this.bufSize = maxSize + } + + clearBuffer (): void { + this.buffer.length = 0 + } + + put (priceData: PriceData): boolean { + if (this.buffer.length === this.bufSize) { + this.buffer.shift() + } + this.buffer.push(priceData) + return true + } + + abstract getPrice(): number +} diff --git a/lib/pricecaster.d.ts b/lib/pricecaster.d.ts new file mode 100644 index 000000000..c231e58d5 --- /dev/null +++ b/lib/pricecaster.d.ts @@ -0,0 +1 @@ +declare module 'PricecasterLib'; \ No newline at end of file diff --git a/package.json b/package.json index 816257a0f..8bfa3385e 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Experimentation with pyth", "main": "index.js", "scripts": { - "start": "npx ts-node backend/pricefetch.ts", + "start": "npx ts-node backend/main.ts", "test": "mocha --timeout 60000" }, "author": "", diff --git a/settings.js b/settings.js index a9732e6ab..b47f70b09 100644 --- a/settings.js +++ b/settings.js @@ -1,13 +1,26 @@ module.exports = { + pyth: { + solanaClusterName: 'devnet' + }, token: '', api: 'https://api.betanet.algoexplorer.io', port: '', - 'BTC/USD': { - appId: 3020301, - verifierpk: 'OPDM7ACAW64Q4VBWAL77Z5SHSJVZZ44V3BAN7W44U43SUXEOUENZMZYOQU', - buffersize: 50, - ratio: 'asap', - strategy: 'maxconf', - phony: false + symbols: { + 'BTC/USD': { + appId: 3020301, + verifierpk: 'OPDM7ACAW64Q4VBWAL77Z5SHSJVZZ44V3BAN7W44U43SUXEOUENZMZYOQU', + buffersize: 50, + ratio: 'asap', + strategy: 'maxconf', + phony: false + }, + 'ETH/USD': { + appId: 3020301, + verifierpk: 'OPDM7ACAW64Q4VBWAL77Z5SHSJVZZ44V3BAN7W44U43SUXEOUENZMZYOQU', + buffersize: 50, + ratio: 'asap', + strategy: 'maxconf', + phony: false + } } }