New Pricecaster Modularization
This commit is contained in:
parent
f83e592556
commit
a3b33abe4b
|
@ -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
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
// })
|
||||
|
|
|
@ -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}`)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
26
settings.js
26
settings.js
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in New Issue