New Pricecaster Modularization

This commit is contained in:
Hernán Di Pietro 2021-10-07 14:41:19 -03:00
parent f83e592556
commit a3b33abe4b
16 changed files with 300 additions and 143 deletions

25
backend/IPriceFetcher.ts Normal file
View File

@ -0,0 +1,25 @@
/**
* Pricecaster Service.
*
* Fetcher backend component.
*
* (c) 2021 Randlabs, Inc.
*/
import { PriceTicker } from './PriceTicker'
export interface IPriceFetcher {
start(): void
stop(): void
/**
* Set price aggregation strategy for this fetcher.
* @param IStrategy The local price aggregation strategy
*/
setStrategy(IStrategy)
/**
* Get the current price, according to running strategy.
*/
queryTicker(): PriceTicker
}

18
backend/IPublisher.ts Normal file
View File

@ -0,0 +1,18 @@
/**
* Pricecaster Service.
*
* Fetcher backend component.
*
* (c) 2021 Randlabs, Inc.
*/
import { PriceTicker } from './PriceTicker'
export class PublishInfo {
block: BigInt = BigInt(0)
txid: string = ''
}
export interface IPublisher {
publish(tick: PriceTicker): Promise<PublishInfo>
}

59
backend/PriceTicker.ts Normal file
View File

@ -0,0 +1,59 @@
/**
* Pricecaster Service.
*
* Fetcher backend component.
*
* (c) 2021 Randlabs, Inc.
*/
/**
* A generic Price ticker information class
*/
export class PriceTicker {
constructor (price: BigInt, confidence: BigInt, exponent: number, networkTime: BigInt) {
this._price = price
this._confidence = confidence
this._exponent = exponent
this._networkTime = networkTime
}
/** price */
private _price: BigInt;
public get price (): BigInt {
return this._price
}
public set price (value: BigInt) {
this._price = value
}
/** a confidence interval */
private _confidence: BigInt;
public get confidence (): BigInt {
return this._confidence
}
public set confidence (value: BigInt) {
this._confidence = value
}
/** exponent (fixed point) */
private _exponent: number;
public get exponent (): number {
return this._exponent
}
public set exponent (value: number) {
this._exponent = value
}
/** time in blockchain network units */
private _networkTime: BigInt;
public get networkTime (): BigInt {
return this._networkTime
}
public set networkTime (value: BigInt) {
this._networkTime = value
}
}

View File

@ -0,0 +1,56 @@
/**
* Pricecaster Service.
*
* Fetcher backend component.
*
* (c) 2021 Randlabs, Inc.
*/
import { IPriceFetcher } from './IPriceFetcher'
import { IStrategy } from './strategy/strategy'
import { getPythProgramKeyForCluster, PriceData, Product, PythConnection } from '@pythnetwork/client'
import { Cluster, clusterApiUrl, Connection } from '@solana/web3.js'
import { PriceTicker } from './PriceTicker'
const settings = require('../settings')
export class PythPriceFetcher implements IPriceFetcher {
private strategy: IStrategy
private symbol: string
private pythConnection: PythConnection
constructor (symbol: string, strategy: IStrategy) {
const SOLANA_CLUSTER_NAME: Cluster = settings.pyth.solanaClusterName as Cluster
const connection = new Connection(clusterApiUrl(SOLANA_CLUSTER_NAME))
const pythPublicKey = getPythProgramKeyForCluster(SOLANA_CLUSTER_NAME)
this.pythConnection = new PythConnection(connection, pythPublicKey)
this.strategy = strategy
this.symbol = symbol
}
start (): void {
this.pythConnection.start()
this.pythConnection.onPriceChange((product: Product, price: PriceData) => {
if (product.symbol === this.symbol) {
this.onPriceChange(price)
}
})
}
stop (): void {
throw new Error('Method not implemented.')
}
setStrategy (s: IStrategy) {
this.strategy = s
}
queryTicker (): PriceTicker {
return this.strategy.getPrice()
}
private onPriceChange (price: PriceData) {
const pt: PriceTicker = new PriceTicker(price.priceComponent,
price.confidenceComponent, price.exponent, price.publishSlot)
this.strategy.put(pt)
}
}

View File

@ -6,7 +6,32 @@
* (c) 2021 Randlabs, Inc.
*/
import { PriceFetcher } from './pricefetch'
import { PythPriceFetcher } from './PythPriceFetcher'
import { StdAlgoPublisher } from './publisher/StdAlgoPublisher'
import { StrategyLastPrice } from './strategy/strategyLastPrice'
const settings = require('../settings')
const algosdk = require('algosdk')
const pricefetcher = new PriceFetcher()
pricefetcher.run()
console.log('Pricecaster Service Fetcher -- (c) 2021 Randlabs.io\n')
const fetchers: { [key: string]: PythPriceFetcher } = {}
const publishers: { [key: string]: StdAlgoPublisher } = {}
for (const sym in settings.symbols) {
console.log(`Setting up fetcher/publisher for ${sym}`)
publishers[sym] = new StdAlgoPublisher(sym,
settings.symbols[sym].priceKeeperAppId,
settings.symbols[sym].validator,
algosdk.mnemonicToSecretKey(settings.symbols[sym].mnemo)
)
fetchers[sym] = new PythPriceFetcher(sym, new StrategyLastPrice())
}
// const pricefetcher = new PythPriceFetcher('BTC/USD', new StrategyLastPrice(10))
// const publisher = new StdAlgoPublisher('BTC/USD', 38888888, )
// async function processTick() {
// const tick = pricefetcher.queryTicker()
// const publishInfo = await publisher.publish(tick)
// setTimeout(processTick, 1000)
// })

View File

@ -1,69 +0,0 @@
/**
* Pricecaster Service.
*
* Fetcher backend component.
*
* (c) 2021 Randlabs, Inc.
*/
import { getPythProgramKeyForCluster, PriceData, Product, PythConnection } from '@pythnetwork/client'
import { Cluster, clusterApiUrl, Connection } from '@solana/web3.js'
import algosdk from 'algosdk'
const PricecasterLib = require('../lib/pricecaster')
const settings = require('../settings')
/**
* The main Price fetcher service class.
*/
export class PriceFetcher {
private pclib: any
pythConnection: PythConnection
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}'`)
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)
})
}
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}`)
}
}
}

View File

@ -0,0 +1,45 @@
import algosdk from 'algosdk'
import { IPublisher, PublishInfo } from '../IPublisher'
import { PriceTicker } from '../PriceTicker'
const PricecasterLib = require('../../lib/pricecaster')
const settings = require('../../settings')
export class StdAlgoPublisher implements IPublisher {
private pclib: any
private symbol: string
private signKey: Uint8Array
private validator: string
constructor (symbol: string, appId: BigInt, validator: string, signKey: Uint8Array) {
this.symbol = symbol
this.signKey = signKey
this.validator = validator
const algodClient = new algosdk.Algodv2(settings.algo.token, settings.algo.api, settings.algo.port)
this.pclib = new PricecasterLib.PricecasterLib(algodClient)
this.pclib.setAppId(appId)
}
signCallback (sender: string, tx: algosdk.Transaction) {
const txSigned = tx.signTxn(this.signKey)
return txSigned
}
async publish (tick: PriceTicker): Promise<PublishInfo> {
const publishInfo = new PublishInfo()
const msg = this.pclib.createMessage(
this.symbol,
tick.price,
tick.exponent,
tick.confidence,
tick.networkTime,
this.signKey)
const txId = await this.pclib.submitMessage(
this.validator,
msg,
this.signCallback
)
publishInfo.txid = txId
return publishInfo
}
}

View File

@ -6,7 +6,7 @@
* (c) 2021 Randlabs, Inc.
*/
import { PriceData } from '@pythnetwork/client'
import { PriceTicker } from '../PriceTicker'
/**
* Implements a strategy for obtaining an asset price from
@ -29,10 +29,10 @@ export interface IStrategy {
* @param priceData The price data to put
* @returns true if successful.
*/
put(priceData: PriceData): boolean
put(ticker: PriceTicker): boolean
/**
* Get the calculated price according to selected strategy.
*/
getPrice(): number
getPrice(): PriceTicker
}

View File

@ -0,0 +1,21 @@
/**
* Pricecaster Service.
*
* Fetcher backend component.
*
* (c) 2021 Randlabs, Inc.
*/
import { PriceTicker } from '../PriceTicker'
import { IStrategy } from './strategy'
export abstract class StrategyBase implements IStrategy {
constructor (bufSize: number = 10) {
this.createBuffer(bufSize)
}
abstract put(priceData: PriceTicker): boolean
abstract createBuffer(size: number): void
abstract clearBuffer(): void
abstract getPrice(): PriceTicker
}

View File

@ -6,15 +6,15 @@
* (c) 2021 Randlabs, Inc.
*/
import { PriceData } from '@pythnetwork/client'
import { PriceTicker } from '../PriceTicker'
import { StrategyBase } from './strategyBase'
/**
* A base class for queue-based buffer strategies
*/
export abstract class StrategyBaseQueue extends StrategyBase {
private buffer: PriceData[]
private bufSize: number
protected buffer: PriceTicker[] = []
private bufSize: number = 0
createBuffer (maxSize: number): void {
this.bufSize = maxSize
@ -24,13 +24,13 @@ export abstract class StrategyBaseQueue extends StrategyBase {
this.buffer.length = 0
}
put (priceData: PriceData): boolean {
put (ticker: PriceTicker): boolean {
if (this.buffer.length === this.bufSize) {
this.buffer.shift()
}
this.buffer.push(priceData)
this.buffer.push(ticker)
return true
}
abstract getPrice(): number
abstract getPrice(): PriceTicker
}

View File

@ -0,0 +1,23 @@
import { PriceTicker } from '../PriceTicker'
import { StrategyBaseQueue } from './strategyBaseQueue'
/**
* Pricecaster Service.
*
* Fetcher backend component.
*
* (c) 2021 Randlabs, Inc.
*/
/**
* This strategy just caches the last provided price,
* acting as a single-item buffer.
*/
export class StrategyLastPrice extends StrategyBaseQueue {
constructor () {
super(1)
}
getPrice (): PriceTicker {
return this.buffer[0]
}
}

View File

@ -1,15 +0,0 @@
/**
* Pricecaster Service.
*
* Fetcher backend component.
*
* (c) 2021 Randlabs, Inc.
*/
import { StrategyBaseQueue } from './strategyqueuebase'
class StrategyAveragePrice extends StrategyBaseQueue {
getPrice (): number {
return 0
}
}

View File

@ -1,23 +0,0 @@
/**
* 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
}

View File

@ -1,4 +1,4 @@
import algosdk, { signBytes } from 'algosdk'
import algosdk from 'algosdk'
import { Buffer } from 'buffer'
import { writeFileSync } from 'fs'
@ -7,13 +7,13 @@ const mnemo = 'assault approve result rare float sugar power float soul kind gal
const appId = BigInt(0x123456789)
const nonce = BigInt(0x1000)
const symbol = "BTC/USD "
const symbol = 'BTC/USD '
const price = 45278.65
const sd = 8.00000000004
// Create message
const buf = Buffer.alloc(131)
buf.write('PRICEDATA',0)
buf.write('PRICEDATA', 0)
// v
buf.writeInt8(1, 9)
// dest
@ -22,7 +22,7 @@ buf.writeBigUInt64BE(appId, 10)
buf.writeBigUInt64BE(nonce, 18)
// symbol
buf.write(symbol, 26)
// price
// price
buf.writeDoubleBE(price, 42)
// sd
buf.writeDoubleBE(sd, 50)

View File

@ -1,26 +1,22 @@
module.exports = {
algo: {
token: '',
api: 'https://api.betanet.algoexplorer.io',
port: ''
},
pyth: {
solanaClusterName: 'devnet'
},
token: '',
api: 'https://api.betanet.algoexplorer.io',
port: '',
symbols: {
'BTC/USD': {
appId: 3020301,
verifierpk: 'OPDM7ACAW64Q4VBWAL77Z5SHSJVZZ44V3BAN7W44U43SUXEOUENZMZYOQU',
buffersize: 50,
ratio: 'asap',
strategy: 'maxconf',
phony: false
priceKeeperAppId: 3020301,
validator: 'OPDM7ACAW64Q4VBWAL77Z5SHSJVZZ44V3BAN7W44U43SUXEOUENZMZYOQU',
mnemo: 'assault approve result rare float sugar power float soul kind galaxy edit unusual pretty tone tilt net range pelican avoid unhappy amused recycle abstract master'
},
'ETH/USD': {
appId: 3020301,
verifierpk: 'OPDM7ACAW64Q4VBWAL77Z5SHSJVZZ44V3BAN7W44U43SUXEOUENZMZYOQU',
buffersize: 50,
ratio: 'asap',
strategy: 'maxconf',
phony: false
priceKeeperAppId: 3020301,
validator: 'OPDM7ACAW64Q4VBWAL77Z5SHSJVZZ44V3BAN7W44U43SUXEOUENZMZYOQU',
mnemo: 'assault approve result rare float sugar power float soul kind galaxy edit unusual pretty tone tilt net range pelican avoid unhappy amused recycle abstract master'
}
}
}

View File

@ -14,10 +14,6 @@ const SIGNATURES = {}
SIGNATURES[VALIDATOR_ADDR] = algosdk.mnemonicToSecretKey(VALIDATOR_MNEMO)
SIGNATURES[OTHER_ADDR] = algosdk.mnemonicToSecretKey(OTHER_MNEMO)
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
const VALID_SYMBOL = 'BTC/USD '
const VALID_PRICE = BigInt(485265555)
const VALID_EXPONENT = BigInt(4)
@ -72,7 +68,7 @@ describe('Price-Keeper contract tests', function () {
const txid = await pclib.submitMessage(VALIDATOR_ADDR, msgBuffer, signCallback)
expect(txid).to.have.length(52)
await pclib.waitForTransactionResponse(txid)
//console.log(await tools.printAppGlobalState(algodClient, appId, VALIDATOR_ADDR))
// console.log(await tools.printAppGlobalState(algodClient, appId, VALIDATOR_ADDR))
const stPrice = await tools.readAppGlobalStateByKey(algodClient, appId, VALIDATOR_ADDR, 'price')
const stExp = await tools.readAppGlobalStateByKey(algodClient, appId, VALIDATOR_ADDR, 'exp')
const stConf = await tools.readAppGlobalStateByKey(algodClient, appId, VALIDATOR_ADDR, 'conf')