[price-service] Add price service client and sdk to this repo (#525)
* Move price service to server directory * Update references to price service * Add pyth-common-js as the service client * Update package names * Add pyth-sdk-js * Rename the sdk * Change readme a bit
This commit is contained in:
parent
16b7977179
commit
b4cf527f00
|
@ -2,7 +2,7 @@ name: Build and Push Price Service Image
|
|||
on:
|
||||
push:
|
||||
tags:
|
||||
- pyth-price-service-v*
|
||||
- pyth-price-server-v*
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dispatch_description:
|
||||
|
@ -13,18 +13,18 @@ permissions:
|
|||
contents: read
|
||||
id-token: write
|
||||
jobs:
|
||||
price-service-image:
|
||||
price-server-image:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set image tag to version of the git tag
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/pyth-price-service-v') }}
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/pyth-price-server-v') }}
|
||||
run: |
|
||||
PREFIX="refs/tags/pyth-price-service-"
|
||||
PREFIX="refs/tags/pyth-price-server-"
|
||||
VERSION="${GITHUB_REF:${#PREFIX}}"
|
||||
echo "IMAGE_TAG=${VERSION}" >> "${GITHUB_ENV}"
|
||||
- name: Set image tag to the git commit hash
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/pyth-price-service-v') }}
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/pyth-price-server-v') }}
|
||||
run: |
|
||||
echo "IMAGE_TAG=${{ github.sha }}" >> "${GITHUB_ENV}"
|
||||
- uses: aws-actions/configure-aws-credentials@8a84b07f2009032ade05a88a28750d733cc30db1
|
||||
|
@ -38,7 +38,7 @@ jobs:
|
|||
AWS_REGION: us-east-1
|
||||
- run: |
|
||||
DOCKER_BUILDKIT=1 docker build -f tilt_devnet/docker_images/Dockerfile.wasm -o type=local,dest=. .
|
||||
DOCKER_BUILDKIT=1 docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f price_service/Dockerfile.price_service .
|
||||
DOCKER_BUILDKIT=1 docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f price_service/server/Dockerfile .
|
||||
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
|
||||
env:
|
||||
ECR_REGISTRY: public.ecr.aws
|
12
Tiltfile
12
Tiltfile
|
@ -206,7 +206,7 @@ docker_build(
|
|||
k8s_yaml_with_ns("tilt_devnet/k8s/check-attestations.yaml")
|
||||
k8s_resource(
|
||||
"check-attestations",
|
||||
resource_deps = ["pyth-price-service", "pyth", "p2w-attest"],
|
||||
resource_deps = ["pyth-price-server", "pyth", "p2w-attest"],
|
||||
labels = ["pyth"],
|
||||
trigger_mode = trigger_mode,
|
||||
)
|
||||
|
@ -237,15 +237,15 @@ k8s_resource(
|
|||
labels = ["pyth"]
|
||||
)
|
||||
|
||||
# Pyth Price service
|
||||
# Pyth Price server
|
||||
docker_build(
|
||||
ref = "pyth-price-service",
|
||||
ref = "pyth-price-server",
|
||||
context = ".",
|
||||
dockerfile = "price_service/Dockerfile.price_service",
|
||||
dockerfile = "price_service/server/Dockerfile",
|
||||
)
|
||||
k8s_yaml_with_ns("tilt_devnet/k8s/pyth-price-service.yaml")
|
||||
k8s_yaml_with_ns("tilt_devnet/k8s/pyth-price-server.yaml")
|
||||
k8s_resource(
|
||||
"pyth-price-service",
|
||||
"pyth-price-server",
|
||||
resource_deps = ["pyth", "p2w-attest", "spy", "eth-devnet", "wasm-gen"],
|
||||
port_forwards = [
|
||||
port_forward(4202, container_port = 4200, name = "Rest API (Status + Query) [:4202]", host = webHost),
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint"],
|
||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
},
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
lib/
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"name": "@pythnetwork/price-service-client",
|
||||
"version": "1.4.0",
|
||||
"description": "Pyth price service client",
|
||||
"author": {
|
||||
"name": "Pyth Data Association"
|
||||
},
|
||||
"homepage": "https://pyth.network",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"files": [
|
||||
"lib/**/*"
|
||||
],
|
||||
"repository": "https://github.com/pyth-network/pyth-crosschain",
|
||||
"scripts": {
|
||||
"test": "jest --passWithNoTests",
|
||||
"build": "tsc",
|
||||
"example": "npm run build && node lib/examples/PriceServiceClient.js",
|
||||
"format": "prettier --write \"src/**/*.ts\"",
|
||||
"lint": "eslint src/",
|
||||
"prepare": "npm run build",
|
||||
"prepublishOnly": "npm test && npm run lint",
|
||||
"preversion": "npm run lint",
|
||||
"version": "npm run format && git add -A src"
|
||||
},
|
||||
"keywords": [
|
||||
"pyth",
|
||||
"oracle"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/yargs": "^17.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.21.0",
|
||||
"@typescript-eslint/parser": "^5.21.0",
|
||||
"eslint": "^8.14.0",
|
||||
"jest": "^27.5.1",
|
||||
"prettier": "^2.6.2",
|
||||
"ts-jest": "^27.1.4",
|
||||
"typescript": "^4.6.3",
|
||||
"yargs": "^17.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pythnetwork/pyth-sdk-js": "^1.2.0",
|
||||
"@types/ws": "^8.5.3",
|
||||
"axios": "^0.26.1",
|
||||
"axios-retry": "^3.2.4",
|
||||
"isomorphic-ws": "^4.0.1",
|
||||
"ts-log": "^2.2.4",
|
||||
"ws": "^8.6.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,376 @@
|
|||
import { HexString, PriceFeed } from "@pythnetwork/pyth-sdk-js";
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
import axiosRetry from "axios-retry";
|
||||
import * as WebSocket from "isomorphic-ws";
|
||||
import { Logger } from "ts-log";
|
||||
import { ResilientWebSocket } from "./ResillientWebSocket";
|
||||
import { makeWebsocketUrl, removeLeading0xIfExists } from "./utils";
|
||||
|
||||
export type DurationInMs = number;
|
||||
|
||||
export type PriceFeedRequestConfig = {
|
||||
/* Optional verbose to request for verbose information from the service */
|
||||
verbose?: boolean;
|
||||
/* Optional binary to include the price feeds binary update data */
|
||||
binary?: boolean;
|
||||
};
|
||||
|
||||
export type PriceServiceConnectionConfig = {
|
||||
/* Timeout of each request (for all of retries). Default: 5000ms */
|
||||
timeout?: DurationInMs;
|
||||
/**
|
||||
* Number of times a HTTP request will be retried before the API returns a failure. Default: 3.
|
||||
*
|
||||
* The connection uses exponential back-off for the delay between retries. However,
|
||||
* it will timeout regardless of the retries at the configured `timeout` time.
|
||||
*/
|
||||
httpRetries?: number;
|
||||
/* Optional logger (e.g: console or any logging library) to log internal events */
|
||||
logger?: Logger;
|
||||
/* Deprecated: please use priceFeedRequestConfig.verbose instead */
|
||||
verbose?: boolean;
|
||||
/* Configuration for the price feed requests */
|
||||
priceFeedRequestConfig?: PriceFeedRequestConfig;
|
||||
};
|
||||
|
||||
type ClientMessage = {
|
||||
type: "subscribe" | "unsubscribe";
|
||||
ids: HexString[];
|
||||
verbose?: boolean;
|
||||
binary?: boolean;
|
||||
};
|
||||
|
||||
type ServerResponse = {
|
||||
type: "response";
|
||||
status: "success" | "error";
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type ServerPriceUpdate = {
|
||||
type: "price_update";
|
||||
price_feed: any;
|
||||
};
|
||||
|
||||
type ServerMessage = ServerResponse | ServerPriceUpdate;
|
||||
|
||||
export type PriceFeedUpdateCallback = (priceFeed: PriceFeed) => void;
|
||||
|
||||
export class PriceServiceConnection {
|
||||
private httpClient: AxiosInstance;
|
||||
|
||||
private priceFeedCallbacks: Map<HexString, Set<PriceFeedUpdateCallback>>;
|
||||
private wsClient: undefined | ResilientWebSocket;
|
||||
private wsEndpoint: undefined | string;
|
||||
|
||||
private logger: undefined | Logger;
|
||||
|
||||
private priceFeedRequestConfig: PriceFeedRequestConfig;
|
||||
|
||||
/**
|
||||
* Custom handler for web socket errors (connection and message parsing).
|
||||
*
|
||||
* Default handler only logs the errors.
|
||||
*/
|
||||
onWsError: (error: Error) => void;
|
||||
|
||||
/**
|
||||
* Constructs a new Connection.
|
||||
*
|
||||
* @param endpoint endpoint URL to the price service. Example: https://website/example/
|
||||
* @param config Optional PriceServiceConnectionConfig for custom configurations.
|
||||
*/
|
||||
constructor(endpoint: string, config?: PriceServiceConnectionConfig) {
|
||||
this.httpClient = axios.create({
|
||||
baseURL: endpoint,
|
||||
timeout: config?.timeout || 5000,
|
||||
});
|
||||
axiosRetry(this.httpClient, {
|
||||
retries: config?.httpRetries || 3,
|
||||
retryDelay: axiosRetry.exponentialDelay,
|
||||
});
|
||||
|
||||
this.priceFeedRequestConfig = {
|
||||
binary: config?.priceFeedRequestConfig?.binary,
|
||||
verbose: config?.priceFeedRequestConfig?.verbose ?? config?.verbose,
|
||||
};
|
||||
|
||||
this.priceFeedCallbacks = new Map();
|
||||
|
||||
this.logger = config?.logger;
|
||||
this.onWsError = (error: Error) => {
|
||||
this.logger?.error(error);
|
||||
};
|
||||
|
||||
this.wsEndpoint = makeWebsocketUrl(endpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Latest PriceFeeds of given price ids.
|
||||
* This will throw an axios error if there is a network problem or the price service returns a non-ok response (e.g: Invalid price ids)
|
||||
*
|
||||
* @param priceIds Array of hex-encoded price ids.
|
||||
* @returns Array of PriceFeeds
|
||||
*/
|
||||
async getLatestPriceFeeds(
|
||||
priceIds: HexString[]
|
||||
): Promise<PriceFeed[] | undefined> {
|
||||
if (priceIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const response = await this.httpClient.get("/api/latest_price_feeds", {
|
||||
params: {
|
||||
ids: priceIds,
|
||||
verbose: this.priceFeedRequestConfig.verbose,
|
||||
binary: this.priceFeedRequestConfig.binary,
|
||||
},
|
||||
});
|
||||
const priceFeedsJson = response.data as any[];
|
||||
return priceFeedsJson.map((priceFeedJson) =>
|
||||
PriceFeed.fromJson(priceFeedJson)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch latest VAA of given price ids.
|
||||
* This will throw an axios error if there is a network problem or the price service returns a non-ok response (e.g: Invalid price ids)
|
||||
*
|
||||
* This function is coupled to wormhole implemntation.
|
||||
*
|
||||
* @param priceIds Array of hex-encoded price ids.
|
||||
* @returns Array of base64 encoded VAAs.
|
||||
*/
|
||||
async getLatestVaas(priceIds: HexString[]): Promise<string[]> {
|
||||
const response = await this.httpClient.get("/api/latest_vaas", {
|
||||
params: {
|
||||
ids: priceIds,
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the earliest VAA of the given price id that is published since the given publish time.
|
||||
* This will throw an error if the given publish time is in the future, or if the publish time
|
||||
* is old and the price service endpoint does not have a db backend for historical requests.
|
||||
* This will throw an axios error if there is a network problem or the price service returns a non-ok response (e.g: Invalid price id)
|
||||
*
|
||||
* This function is coupled to wormhole implemntation.
|
||||
*
|
||||
* @param priceId Hex-encoded price id.
|
||||
* @param publishTime Epoch timestamp in seconds.
|
||||
* @returns Tuple of VAA and publishTime.
|
||||
*/
|
||||
async getVaa(
|
||||
priceId: HexString,
|
||||
publishTime: EpochTimeStamp
|
||||
): Promise<[string, EpochTimeStamp]> {
|
||||
const response = await this.httpClient.get("/api/get_vaa", {
|
||||
params: {
|
||||
id: priceId,
|
||||
publish_time: publishTime,
|
||||
},
|
||||
});
|
||||
return [response.data.vaa, response.data.publishTime];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the list of available price feed ids.
|
||||
* This will throw an axios error if there is a network problem or the price service returns a non-ok response.
|
||||
*
|
||||
* @returns Array of hex-encoded price ids.
|
||||
*/
|
||||
async getPriceFeedIds(): Promise<HexString[]> {
|
||||
const response = await this.httpClient.get("/api/price_feed_ids");
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to updates for given price ids.
|
||||
*
|
||||
* It will start a websocket connection if it's not started yet.
|
||||
* Also, it won't throw any exception if given price ids are invalid or connection errors. Instead,
|
||||
* it calls `connection.onWsError`. If you want to handle the errors you should set the
|
||||
* `onWsError` function to your custom error handler.
|
||||
*
|
||||
* @param priceIds Array of hex-encoded price ids.
|
||||
* @param cb Callback function that is called with a PriceFeed upon updates to given price ids.
|
||||
*/
|
||||
async subscribePriceFeedUpdates(
|
||||
priceIds: HexString[],
|
||||
cb: PriceFeedUpdateCallback
|
||||
) {
|
||||
if (this.wsClient === undefined) {
|
||||
await this.startWebSocket();
|
||||
}
|
||||
|
||||
priceIds = priceIds.map((priceId) => removeLeading0xIfExists(priceId));
|
||||
|
||||
const newPriceIds: HexString[] = [];
|
||||
|
||||
for (const id of priceIds) {
|
||||
if (!this.priceFeedCallbacks.has(id)) {
|
||||
this.priceFeedCallbacks.set(id, new Set());
|
||||
newPriceIds.push(id);
|
||||
}
|
||||
|
||||
this.priceFeedCallbacks.get(id)!.add(cb);
|
||||
}
|
||||
|
||||
const message: ClientMessage = {
|
||||
ids: newPriceIds,
|
||||
type: "subscribe",
|
||||
verbose: this.priceFeedRequestConfig.verbose,
|
||||
binary: this.priceFeedRequestConfig.binary,
|
||||
};
|
||||
|
||||
await this.wsClient?.send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from updates for given price ids.
|
||||
*
|
||||
* It will close the websocket connection if it's not subscribed to any price feed updates anymore.
|
||||
* Also, it won't throw any exception if given price ids are invalid or connection errors. Instead,
|
||||
* it calls `connection.onWsError`. If you want to handle the errors you should set the
|
||||
* `onWsError` function to your custom error handler.
|
||||
*
|
||||
* @param priceIds Array of hex-encoded price ids.
|
||||
* @param cb Optional callback, if set it will only unsubscribe this callback from updates for given price ids.
|
||||
*/
|
||||
async unsubscribePriceFeedUpdates(
|
||||
priceIds: HexString[],
|
||||
cb?: PriceFeedUpdateCallback
|
||||
) {
|
||||
if (this.wsClient === undefined) {
|
||||
await this.startWebSocket();
|
||||
}
|
||||
|
||||
priceIds = priceIds.map((priceId) => removeLeading0xIfExists(priceId));
|
||||
|
||||
const removedPriceIds: HexString[] = [];
|
||||
|
||||
for (const id of priceIds) {
|
||||
if (this.priceFeedCallbacks.has(id)) {
|
||||
let idRemoved = false;
|
||||
|
||||
if (cb === undefined) {
|
||||
this.priceFeedCallbacks.delete(id);
|
||||
idRemoved = true;
|
||||
} else {
|
||||
this.priceFeedCallbacks.get(id)!.delete(cb);
|
||||
|
||||
if (this.priceFeedCallbacks.get(id)!.size === 0) {
|
||||
this.priceFeedCallbacks.delete(id);
|
||||
idRemoved = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (idRemoved) {
|
||||
removedPriceIds.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const message: ClientMessage = {
|
||||
ids: removedPriceIds,
|
||||
type: "unsubscribe",
|
||||
};
|
||||
|
||||
await this.wsClient?.send(JSON.stringify(message));
|
||||
|
||||
if (this.priceFeedCallbacks.size === 0) {
|
||||
this.closeWebSocket();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts connection websocket.
|
||||
*
|
||||
* This function is called automatically upon subscribing to price feed updates.
|
||||
*/
|
||||
async startWebSocket() {
|
||||
if (this.wsEndpoint === undefined) {
|
||||
throw new Error("Websocket endpoint is undefined.");
|
||||
}
|
||||
|
||||
this.wsClient = new ResilientWebSocket(this.wsEndpoint, this.logger);
|
||||
|
||||
this.wsClient.onError = this.onWsError;
|
||||
|
||||
this.wsClient.onReconnect = () => {
|
||||
if (this.priceFeedCallbacks.size > 0) {
|
||||
const message: ClientMessage = {
|
||||
ids: Array.from(this.priceFeedCallbacks.keys()),
|
||||
type: "subscribe",
|
||||
verbose: this.priceFeedRequestConfig.verbose,
|
||||
binary: this.priceFeedRequestConfig.binary,
|
||||
};
|
||||
|
||||
this.logger?.info("Resubscribing to existing price feeds.");
|
||||
this.wsClient?.send(JSON.stringify(message));
|
||||
}
|
||||
};
|
||||
|
||||
this.wsClient.onMessage = (data: WebSocket.Data) => {
|
||||
this.logger?.info(`Received message ${data.toString()}`);
|
||||
|
||||
let message: ServerMessage;
|
||||
|
||||
try {
|
||||
message = JSON.parse(data.toString()) as ServerMessage;
|
||||
} catch (e: any) {
|
||||
this.logger?.error(`Error parsing message ${data.toString()} as JSON.`);
|
||||
this.logger?.error(e);
|
||||
this.onWsError(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === "response") {
|
||||
if (message.status === "error") {
|
||||
this.logger?.error(
|
||||
`Error response from the websocket server ${message.error}.`
|
||||
);
|
||||
this.onWsError(new Error(message.error));
|
||||
}
|
||||
} else if (message.type === "price_update") {
|
||||
let priceFeed;
|
||||
try {
|
||||
priceFeed = PriceFeed.fromJson(message.price_feed);
|
||||
} catch (e: any) {
|
||||
this.logger?.error(
|
||||
`Error parsing price feeds from message ${data.toString()}.`
|
||||
);
|
||||
this.logger?.error(e);
|
||||
this.onWsError(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.priceFeedCallbacks.has(priceFeed.id)) {
|
||||
for (const cb of this.priceFeedCallbacks.get(priceFeed.id)!) {
|
||||
cb(priceFeed);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.logger?.warn(
|
||||
`Ignoring unsupported server response ${data.toString()}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
await this.wsClient.startWebSocket();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes connection websocket.
|
||||
*
|
||||
* At termination, the websocket should be closed to finish the
|
||||
* process elegantly. It will automatically close when the connection
|
||||
* is subscribed to no price feeds.
|
||||
*/
|
||||
closeWebSocket() {
|
||||
this.wsClient?.closeWebSocket();
|
||||
this.wsClient = undefined;
|
||||
this.priceFeedCallbacks.clear();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
import * as WebSocket from "isomorphic-ws";
|
||||
import { Logger } from "ts-log";
|
||||
|
||||
const PING_TIMEOUT_DURATION = 30000 + 3000; // It is 30s on the server and 3s is added for delays
|
||||
|
||||
/**
|
||||
* This class wraps websocket to provide a resilient web socket client.
|
||||
*
|
||||
* It will reconnect if connection fails with exponential backoff. Also, in node, it will reconnect
|
||||
* if it receives no ping request from server within a while as indication of timeout (assuming
|
||||
* the server sends it regularly).
|
||||
*
|
||||
* This class also logs events if logger is given and by replacing onError method you can handle
|
||||
* connection errors yourself (e.g: do not retry and close the connection).
|
||||
*/
|
||||
export class ResilientWebSocket {
|
||||
private endpoint: string;
|
||||
private wsClient: undefined | WebSocket;
|
||||
private wsUserClosed: boolean;
|
||||
private wsFailedAttempts: number;
|
||||
private pingTimeout: undefined | NodeJS.Timeout;
|
||||
private logger: undefined | Logger;
|
||||
|
||||
onError: (error: Error) => void;
|
||||
onMessage: (data: WebSocket.Data) => void;
|
||||
onReconnect: () => void;
|
||||
|
||||
constructor(endpoint: string, logger?: Logger) {
|
||||
this.endpoint = endpoint;
|
||||
this.logger = logger;
|
||||
|
||||
this.wsFailedAttempts = 0;
|
||||
this.onError = (error: Error) => {
|
||||
this.logger?.error(error);
|
||||
};
|
||||
this.wsUserClosed = true;
|
||||
this.onMessage = () => {};
|
||||
this.onReconnect = () => {};
|
||||
}
|
||||
|
||||
async send(data: any) {
|
||||
this.logger?.info(`Sending ${data}`);
|
||||
|
||||
await this.waitForMaybeReadyWebSocket();
|
||||
|
||||
if (this.wsClient === undefined) {
|
||||
this.logger?.error(
|
||||
"Couldn't connect to the websocket server. Error callback is called."
|
||||
);
|
||||
} else {
|
||||
this.wsClient?.send(data);
|
||||
}
|
||||
}
|
||||
|
||||
async startWebSocket() {
|
||||
if (this.wsClient !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger?.info(`Creating Web Socket client`);
|
||||
|
||||
this.wsClient = new WebSocket(this.endpoint);
|
||||
this.wsUserClosed = false;
|
||||
|
||||
this.wsClient.onopen = () => {
|
||||
this.wsFailedAttempts = 0;
|
||||
// Ping handler is undefined in browser side so heartbeat is disabled.
|
||||
if (this.wsClient!.on !== undefined) {
|
||||
this.heartbeat();
|
||||
}
|
||||
};
|
||||
|
||||
this.wsClient.onerror = (event) => {
|
||||
this.onError(event.error);
|
||||
};
|
||||
|
||||
this.wsClient.onmessage = (event) => {
|
||||
this.onMessage(event.data);
|
||||
};
|
||||
|
||||
this.wsClient.onclose = async () => {
|
||||
if (this.pingTimeout !== undefined) {
|
||||
clearInterval(this.pingTimeout);
|
||||
}
|
||||
|
||||
if (this.wsUserClosed === false) {
|
||||
this.wsFailedAttempts += 1;
|
||||
this.wsClient = undefined;
|
||||
const waitTime = expoBackoff(this.wsFailedAttempts);
|
||||
|
||||
this.logger?.error(
|
||||
`Connection closed unexpectedly or because of timeout. Reconnecting after ${waitTime}ms.`
|
||||
);
|
||||
|
||||
await sleep(waitTime);
|
||||
this.restartUnexpectedClosedWebsocket();
|
||||
} else {
|
||||
this.logger?.info("The connection has been closed successfully.");
|
||||
}
|
||||
};
|
||||
|
||||
if (this.wsClient.on !== undefined) {
|
||||
// Ping handler is undefined in browser side
|
||||
this.wsClient.on("ping", this.heartbeat.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Heartbeat is only enabled in node clients because they support handling
|
||||
* ping-pong events.
|
||||
*
|
||||
* This approach only works when server constantly pings the clients which.
|
||||
* Otherwise you might consider sending ping and acting on pong responses
|
||||
* yourself.
|
||||
*/
|
||||
private heartbeat() {
|
||||
this.logger?.info("Heartbeat");
|
||||
|
||||
if (this.pingTimeout !== undefined) {
|
||||
clearTimeout(this.pingTimeout);
|
||||
}
|
||||
|
||||
this.pingTimeout = setTimeout(() => {
|
||||
this.logger?.warn(`Connection timed out. Reconnecting...`);
|
||||
this.wsClient?.terminate();
|
||||
this.restartUnexpectedClosedWebsocket();
|
||||
}, PING_TIMEOUT_DURATION);
|
||||
}
|
||||
|
||||
private async waitForMaybeReadyWebSocket() {
|
||||
let waitedTime = 0;
|
||||
while (
|
||||
this.wsClient !== undefined &&
|
||||
this.wsClient.readyState !== this.wsClient.OPEN
|
||||
) {
|
||||
if (waitedTime > 1000) {
|
||||
this.wsClient.close();
|
||||
return;
|
||||
} else {
|
||||
waitedTime += 10;
|
||||
await sleep(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async restartUnexpectedClosedWebsocket() {
|
||||
if (this.wsUserClosed === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.startWebSocket();
|
||||
await this.waitForMaybeReadyWebSocket();
|
||||
|
||||
if (this.wsClient === undefined) {
|
||||
this.logger?.error(
|
||||
"Couldn't reconnect to websocket. Error callback is called."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.onReconnect();
|
||||
}
|
||||
|
||||
closeWebSocket() {
|
||||
if (this.wsClient !== undefined) {
|
||||
const client = this.wsClient;
|
||||
this.wsClient = undefined;
|
||||
client.close();
|
||||
}
|
||||
this.wsUserClosed = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function expoBackoff(attempts: number): number {
|
||||
return 2 ** attempts * 100;
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
import yargs from "yargs";
|
||||
import { hideBin } from "yargs/helpers";
|
||||
|
||||
import { PriceServiceConnection } from "../index";
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
const argv = yargs(hideBin(process.argv))
|
||||
.option("endpoint", {
|
||||
description:
|
||||
"Endpoint URL for the price service. e.g: https://endpoint/example",
|
||||
type: "string",
|
||||
required: true,
|
||||
})
|
||||
.option("price-ids", {
|
||||
description:
|
||||
"Space separated price feed ids (in hex without leading 0x) to fetch." +
|
||||
" e.g: f9c0172ba10dfa4d19088d...",
|
||||
type: "array",
|
||||
required: true,
|
||||
})
|
||||
.help()
|
||||
.alias("help", "h")
|
||||
.parserConfiguration({
|
||||
"parse-numbers": false,
|
||||
})
|
||||
.parseSync();
|
||||
|
||||
async function run() {
|
||||
const connection = new PriceServiceConnection(argv.endpoint, {
|
||||
logger: console, // Providing logger will allow the connection to log it's events.
|
||||
priceFeedRequestConfig: {
|
||||
binary: true,
|
||||
},
|
||||
});
|
||||
|
||||
const priceIds = argv.priceIds as string[];
|
||||
const priceFeeds = await connection.getLatestPriceFeeds(priceIds);
|
||||
console.log(priceFeeds);
|
||||
console.log(priceFeeds?.at(0)?.getPriceNoOlderThan(60));
|
||||
|
||||
console.log("Subscribing to price feed updates.");
|
||||
|
||||
await connection.subscribePriceFeedUpdates(priceIds, (priceFeed) => {
|
||||
console.log(
|
||||
`Current price for ${priceFeed.id}: ${JSON.stringify(
|
||||
priceFeed.getPriceNoOlderThan(60)
|
||||
)}.`
|
||||
);
|
||||
console.log(priceFeed.getVAA());
|
||||
});
|
||||
|
||||
await sleep(600000);
|
||||
|
||||
// To close the websocket you should either unsubscribe from all
|
||||
// price feeds or call `connection.stopWebSocket()` directly.
|
||||
|
||||
console.log("Unsubscribing from price feed updates.");
|
||||
await connection.unsubscribePriceFeedUpdates(priceIds);
|
||||
|
||||
// connection.closeWebSocket();
|
||||
}
|
||||
|
||||
run();
|
|
@ -0,0 +1,12 @@
|
|||
export {
|
||||
DurationInMs,
|
||||
PriceServiceConnection,
|
||||
PriceServiceConnectionConfig,
|
||||
} from "./PriceServiceConnection";
|
||||
|
||||
export {
|
||||
HexString,
|
||||
PriceFeed,
|
||||
Price,
|
||||
UnixTimestamp,
|
||||
} from "@pythnetwork/pyth-sdk-js";
|
|
@ -0,0 +1,24 @@
|
|||
import { HexString } from "@pythnetwork/pyth-sdk-js";
|
||||
|
||||
/**
|
||||
* Convert http(s) endpoint to ws(s) endpoint.
|
||||
*
|
||||
* @param endpoint Http(s) protocol endpoint
|
||||
* @returns Ws(s) protocol endpoint of the same address
|
||||
*/
|
||||
export function makeWebsocketUrl(endpoint: string) {
|
||||
const url = new URL("ws", endpoint);
|
||||
const useHttps = url.protocol === "https:";
|
||||
|
||||
url.protocol = useHttps ? "wss:" : "ws:";
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
export function removeLeading0xIfExists(id: HexString): HexString {
|
||||
if (id.startsWith("0x")) {
|
||||
return id.substring(2);
|
||||
} else {
|
||||
return id;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"outDir": "./lib",
|
||||
"strict": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "**/__tests__/*"]
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
lib/
|
||||
**/schemas
|
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint"],
|
||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
},
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
lib
|
|
@ -0,0 +1,12 @@
|
|||
# Pyth SDK JS
|
||||
|
||||
The Pyth JavaScript SDK provides definitions and utilities for Pyth data structures.
|
||||
|
||||
Please see the [pyth.network documentation](https://docs.pyth.network/) for more information on how to use Pyth prices in various blockchains.
|
||||
|
||||
## Releases
|
||||
|
||||
We use [Semantic Versioning](https://semver.org/) for our releases. In order to release a new version of this package and publish it to npm, follow these steps:
|
||||
|
||||
1. Run `npm version <new version number> --no-git-tag-version`. This command will update the version of the package. Then push your changes to github.
|
||||
2. Once your change is merged into `main`, create a release with tag `v<new version number>` like `v1.5.2`, and a github action will automatically publish the new version of this package to npm.
|
|
@ -0,0 +1,5 @@
|
|||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"name": "@pythnetwork/price-service-sdk",
|
||||
"version": "1.2.0",
|
||||
"description": "Pyth price service SDK",
|
||||
"homepage": "https://pyth.network",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"files": [
|
||||
"lib/**/*"
|
||||
],
|
||||
"repository": "https://github.com/pyth-network/pyth-sdk-js",
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"build": "tsc",
|
||||
"format": "prettier --write \"src/**/*.ts\"",
|
||||
"gen-ts-schema": "quicktype --src-lang schema src/schemas/price_feed.json -o src/schemas/PriceFeed.ts --raw-type any --converters all-objects && prettier --write \"src/schemas/*.ts\"",
|
||||
"lint": "eslint src/",
|
||||
"prepare": "npm run build",
|
||||
"prepublishOnly": "npm test && npm run lint",
|
||||
"preversion": "npm run lint",
|
||||
"version": "npm run format && git add -A src"
|
||||
},
|
||||
"keywords": [
|
||||
"pyth",
|
||||
"oracle"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.4.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.20.0",
|
||||
"@typescript-eslint/parser": "^5.20.0",
|
||||
"eslint": "^8.13.0",
|
||||
"jest": "^27.5.1",
|
||||
"prettier": "^2.6.2",
|
||||
"quicktype": "^15.0.261",
|
||||
"ts-jest": "^27.1.4",
|
||||
"typescript": "^4.6.3"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
import { Price, PriceFeed, PriceFeedMetadata } from "../index";
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
test("Parsing Price Feed works as expected", () => {
|
||||
const data = {
|
||||
ema_price: {
|
||||
conf: "2",
|
||||
expo: 4,
|
||||
price: "3",
|
||||
publish_time: 11,
|
||||
},
|
||||
id: "abcdef0123456789",
|
||||
price: {
|
||||
conf: "1",
|
||||
expo: 4,
|
||||
price: "10",
|
||||
publish_time: 11,
|
||||
},
|
||||
};
|
||||
|
||||
const priceFeed = PriceFeed.fromJson(data);
|
||||
expect(priceFeed.id).toBe("abcdef0123456789");
|
||||
|
||||
const expectedPrice = new Price({
|
||||
conf: "1",
|
||||
expo: 4,
|
||||
price: "10",
|
||||
publishTime: 11,
|
||||
});
|
||||
expect(priceFeed.getPriceUnchecked()).toStrictEqual(expectedPrice);
|
||||
|
||||
const expectedEmaPrice = new Price({
|
||||
conf: "2",
|
||||
expo: 4,
|
||||
price: "3",
|
||||
publishTime: 11,
|
||||
});
|
||||
expect(priceFeed.getEmaPriceUnchecked()).toStrictEqual(expectedEmaPrice);
|
||||
|
||||
jest.setSystemTime(20000);
|
||||
expect(priceFeed.getPriceNoOlderThan(15)).toStrictEqual(expectedPrice);
|
||||
expect(priceFeed.getPriceNoOlderThan(5)).toBeUndefined();
|
||||
expect(priceFeed.getEmaPriceNoOlderThan(15)).toStrictEqual(expectedEmaPrice);
|
||||
expect(priceFeed.getEmaPriceNoOlderThan(5)).toBeUndefined();
|
||||
|
||||
expect(priceFeed.toJson()).toEqual(data);
|
||||
});
|
||||
|
||||
test("getMetadata returns PriceFeedMetadata as expected", () => {
|
||||
const data = {
|
||||
ema_price: {
|
||||
conf: "2",
|
||||
expo: 4,
|
||||
price: "3",
|
||||
publish_time: 11,
|
||||
},
|
||||
id: "abcdef0123456789",
|
||||
price: {
|
||||
conf: "1",
|
||||
expo: 4,
|
||||
price: "10",
|
||||
publish_time: 11,
|
||||
},
|
||||
metadata: {
|
||||
attestation_time: 7,
|
||||
emitter_chain: 8,
|
||||
price_service_receive_time: 9,
|
||||
sequence_number: 10,
|
||||
something_else: 11, // Ensuring the code is future compatible.
|
||||
},
|
||||
};
|
||||
|
||||
const priceFeed = PriceFeed.fromJson(data);
|
||||
|
||||
expect(priceFeed.getMetadata()).toStrictEqual(
|
||||
PriceFeedMetadata.fromJson({
|
||||
attestation_time: 7,
|
||||
emitter_chain: 8,
|
||||
price_service_receive_time: 9,
|
||||
sequence_number: 10,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("getVAA returns string as expected", () => {
|
||||
const data = {
|
||||
ema_price: {
|
||||
conf: "2",
|
||||
expo: 4,
|
||||
price: "3",
|
||||
publish_time: 11,
|
||||
},
|
||||
id: "abcdef0123456789",
|
||||
price: {
|
||||
conf: "1",
|
||||
expo: 4,
|
||||
price: "10",
|
||||
publish_time: 11,
|
||||
},
|
||||
vaa: "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef",
|
||||
};
|
||||
|
||||
const priceFeed = PriceFeed.fromJson(data);
|
||||
|
||||
expect(priceFeed.getVAA()).toStrictEqual(
|
||||
"abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef"
|
||||
);
|
||||
});
|
|
@ -0,0 +1,313 @@
|
|||
import {
|
||||
Convert,
|
||||
Price as JsonPrice,
|
||||
PriceFeed as JsonPriceFeed,
|
||||
PriceFeedMetadata as JsonPriceFeedMetadata,
|
||||
} from "./schemas/PriceFeed";
|
||||
|
||||
export type UnixTimestamp = number;
|
||||
export type DurationInSeconds = number;
|
||||
export type HexString = string;
|
||||
|
||||
/**
|
||||
* A Pyth Price represented as `${price} ± ${conf} * 10^${expo}` published at `publishTime`.
|
||||
*/
|
||||
export class Price {
|
||||
conf: string;
|
||||
expo: number;
|
||||
price: string;
|
||||
publishTime: UnixTimestamp;
|
||||
|
||||
constructor(rawPrice: {
|
||||
conf: string;
|
||||
expo: number;
|
||||
price: string;
|
||||
publishTime: UnixTimestamp;
|
||||
}) {
|
||||
this.conf = rawPrice.conf;
|
||||
this.expo = rawPrice.expo;
|
||||
this.price = rawPrice.price;
|
||||
this.publishTime = rawPrice.publishTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price as number. Warning: this conversion might result in an inaccurate number.
|
||||
* We store price and confidence values in our Oracle at 64-bit precision, but the JavaScript
|
||||
* number type can only represent numbers with 52-bit precision. So if a price or confidence
|
||||
* is larger than 52-bits, the conversion will lose the most insignificant bits.
|
||||
*
|
||||
* @returns a floating point number representing the price
|
||||
*/
|
||||
getPriceAsNumberUnchecked(): number {
|
||||
return Number(this.price) * 10 ** this.expo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price as number. Warning: this conversion might result in an inaccurate number.
|
||||
* Explanation is the same as `priceAsNumberUnchecked()` documentation.
|
||||
*
|
||||
* @returns a floating point number representing the price
|
||||
*/
|
||||
getConfAsNumberUnchecked(): number {
|
||||
return Number(this.conf) * 10 ** this.expo;
|
||||
}
|
||||
|
||||
static fromJson(json: any): Price {
|
||||
const jsonPrice: JsonPrice = Convert.toPrice(json);
|
||||
return new Price({
|
||||
conf: jsonPrice.conf,
|
||||
expo: jsonPrice.expo,
|
||||
price: jsonPrice.price,
|
||||
publishTime: jsonPrice.publish_time,
|
||||
});
|
||||
}
|
||||
|
||||
toJson(): any {
|
||||
const jsonPrice: JsonPrice = {
|
||||
conf: this.conf,
|
||||
expo: this.expo,
|
||||
price: this.price,
|
||||
publish_time: this.publishTime,
|
||||
};
|
||||
// this is done to avoid sending undefined values to the server
|
||||
return Convert.priceToJson(jsonPrice);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata about the price
|
||||
*
|
||||
* Represents metadata of a price feed.
|
||||
*/
|
||||
export class PriceFeedMetadata {
|
||||
/**
|
||||
* Attestation time of the price
|
||||
*/
|
||||
attestationTime: number;
|
||||
/**
|
||||
* Chain of the emitter
|
||||
*/
|
||||
emitterChain: number;
|
||||
/**
|
||||
* The time that the price service received the price
|
||||
*/
|
||||
priceServiceReceiveTime: number;
|
||||
/**
|
||||
* Sequence number of the price
|
||||
*/
|
||||
sequenceNumber: number;
|
||||
|
||||
constructor(metadata: {
|
||||
attestationTime: number;
|
||||
emitterChain: number;
|
||||
receiveTime: number;
|
||||
sequenceNumber: number;
|
||||
}) {
|
||||
this.attestationTime = metadata.attestationTime;
|
||||
this.emitterChain = metadata.emitterChain;
|
||||
this.priceServiceReceiveTime = metadata.receiveTime;
|
||||
this.sequenceNumber = metadata.sequenceNumber;
|
||||
}
|
||||
|
||||
static fromJson(json: any): PriceFeedMetadata | undefined {
|
||||
if (json === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const jsonFeed: JsonPriceFeedMetadata = Convert.toPriceFeedMetadata(json);
|
||||
return new PriceFeedMetadata({
|
||||
attestationTime: jsonFeed.attestation_time,
|
||||
emitterChain: jsonFeed.emitter_chain,
|
||||
receiveTime: jsonFeed.price_service_receive_time,
|
||||
sequenceNumber: jsonFeed.sequence_number,
|
||||
});
|
||||
}
|
||||
|
||||
toJson(): any {
|
||||
const jsonFeed: JsonPriceFeedMetadata = {
|
||||
attestation_time: this.attestationTime,
|
||||
emitter_chain: this.emitterChain,
|
||||
price_service_receive_time: this.priceServiceReceiveTime,
|
||||
sequence_number: this.sequenceNumber,
|
||||
};
|
||||
// this is done to avoid sending undefined values to the server
|
||||
return Convert.priceFeedMetadataToJson(jsonFeed);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pyth Price Feed
|
||||
*
|
||||
* Represents a current aggregation price from pyth publisher feeds.
|
||||
*/
|
||||
|
||||
export class PriceFeed {
|
||||
/**
|
||||
* Exponentially-weighted moving average Price
|
||||
*/
|
||||
private emaPrice: Price;
|
||||
/**
|
||||
* Unique identifier for this price.
|
||||
*/
|
||||
id: HexString;
|
||||
/**
|
||||
* Metadata of the price
|
||||
*/
|
||||
metadata?: PriceFeedMetadata;
|
||||
/**
|
||||
* VAA of the price
|
||||
*/
|
||||
vaa?: string;
|
||||
/**
|
||||
* Price
|
||||
*/
|
||||
private price: Price;
|
||||
|
||||
constructor(rawFeed: {
|
||||
emaPrice: Price;
|
||||
id: HexString;
|
||||
metadata?: PriceFeedMetadata;
|
||||
vaa?: string;
|
||||
price: Price;
|
||||
}) {
|
||||
this.emaPrice = rawFeed.emaPrice;
|
||||
this.id = rawFeed.id;
|
||||
this.metadata = rawFeed.metadata;
|
||||
this.vaa = rawFeed.vaa;
|
||||
this.price = rawFeed.price;
|
||||
}
|
||||
|
||||
static fromJson(json: any): PriceFeed {
|
||||
const jsonFeed: JsonPriceFeed = Convert.toPriceFeed(json);
|
||||
return new PriceFeed({
|
||||
emaPrice: Price.fromJson(jsonFeed.ema_price),
|
||||
id: jsonFeed.id,
|
||||
metadata: PriceFeedMetadata.fromJson(jsonFeed.metadata),
|
||||
vaa: jsonFeed.vaa,
|
||||
price: Price.fromJson(jsonFeed.price),
|
||||
});
|
||||
}
|
||||
|
||||
toJson(): any {
|
||||
const jsonFeed: JsonPriceFeed = {
|
||||
ema_price: this.emaPrice.toJson(),
|
||||
id: this.id,
|
||||
metadata: this.metadata?.toJson(),
|
||||
price: this.price.toJson(),
|
||||
};
|
||||
return Convert.priceFeedToJson(jsonFeed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the price and confidence interval as fixed-point numbers of the form a * 10^e.
|
||||
* This function returns the current best estimate of the price at the time that this `PriceFeed` was
|
||||
* published (`publishTime`). The returned price can be from arbitrarily far in the past; this function
|
||||
* makes no guarantees that the returned price is recent or useful for any particular application.
|
||||
*
|
||||
* Users of this function should check the returned `publishTime` to ensure that the returned price is
|
||||
* sufficiently recent for their application. If you are considering using this function, it may be
|
||||
* safer / easier to use `getPriceNoOlderThan` method.
|
||||
*
|
||||
* @returns a Price that contains the price and confidence interval along with
|
||||
* the exponent for them, and publish time of the price.
|
||||
*/
|
||||
getPriceUnchecked(): Price {
|
||||
return this.price;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the exponentially-weighted moving average (EMA) price and confidence interval.
|
||||
*
|
||||
* This function returns the current best estimate of the price at the time that this `PriceFeed` was
|
||||
* published (`publishTime`). The returned price can be from arbitrarily far in the past; this function
|
||||
* makes no guarantees that the returned price is recent or useful for any particular application.
|
||||
*
|
||||
* Users of this function should check the returned `publishTime` to ensure that the returned price is
|
||||
* sufficiently recent for their application. If you are considering using this function, it may be
|
||||
* safer / easier to use `getEmaPriceNoOlderThan` method.
|
||||
*
|
||||
* At the moment, the confidence interval returned by this method is computed in
|
||||
* a somewhat questionable way, so we do not recommend using it for high-value applications.
|
||||
*
|
||||
* @returns a Price that contains the EMA price and confidence interval along with
|
||||
* the exponent for them, and publish time of the price.
|
||||
*/
|
||||
getEmaPriceUnchecked(): Price {
|
||||
return this.emaPrice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the price if it was updated no older than `age` seconds of the current time.
|
||||
*
|
||||
* This function is a sanity-checked version of `getPriceUnchecked` which is useful in
|
||||
* applications that require a sufficiently-recent price. Returns `undefined` if the price
|
||||
* is not recent enough.
|
||||
*
|
||||
* @param age return a price as long as it has been updated within this number of seconds
|
||||
* @returns a Price struct containing the price, confidence interval along with the exponent for
|
||||
* both numbers, and its publish time, or `undefined` if no price update occurred within `age` seconds of the current time.
|
||||
*/
|
||||
getPriceNoOlderThan(age: DurationInSeconds): Price | undefined {
|
||||
const price = this.getPriceUnchecked();
|
||||
|
||||
const currentTime: UnixTimestamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
// This checks the absolute difference as a sanity check
|
||||
// for the cases that the system time is behind or price
|
||||
// feed timestamp happen to be in the future (a bug).
|
||||
if (Math.abs(currentTime - price.publishTime) > age) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return price;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the exponentially-weighted moving average (EMA) price if it was updated no older than
|
||||
* `age` seconds of the current time.
|
||||
*
|
||||
* This function is a sanity-checked version of `getEmaPriceUnchecked` which is useful in
|
||||
* applications that require a sufficiently-recent price. Returns `undefined` if the price
|
||||
* is not recent enough.
|
||||
*
|
||||
* At the moment, the confidence interval returned by this method is computed in
|
||||
* a somewhat questionable way, so we do not recommend using it for high-value applications.
|
||||
*
|
||||
* @param age return a price as long as it has been updated within this number of seconds
|
||||
* @returns a Price struct containing the EMA price, confidence interval along with the exponent for
|
||||
* both numbers, and its publish time, or `undefined` if no price update occurred within `age` seconds of the current time.
|
||||
*/
|
||||
getEmaPriceNoOlderThan(age: DurationInSeconds): Price | undefined {
|
||||
const emaPrice = this.getEmaPriceUnchecked();
|
||||
|
||||
const currentTime: UnixTimestamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
// This checks the absolute difference as a sanity check
|
||||
// for the cases that the system time is behind or price
|
||||
// feed timestamp happen to be in the future (a bug).
|
||||
if (Math.abs(currentTime - emaPrice.publishTime) > age) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return emaPrice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the price feed metadata.
|
||||
*
|
||||
* @returns a struct containing the attestation time, emitter chain, and the sequence number.
|
||||
* Returns `undefined` if metadata is currently unavailable.
|
||||
*/
|
||||
getMetadata(): PriceFeedMetadata | undefined {
|
||||
return this.metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the price feed vaa.
|
||||
*
|
||||
* @returns vaa in base64.
|
||||
* Returns `undefined` if vaa is unavailable.
|
||||
*/
|
||||
getVAA(): string | undefined {
|
||||
return this.vaa;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,298 @@
|
|||
// To parse this data:
|
||||
//
|
||||
// import { Convert, PriceFeed } from "./file";
|
||||
//
|
||||
// const priceFeed = Convert.toPriceFeed(json);
|
||||
//
|
||||
// These functions will throw an error if the JSON doesn't
|
||||
// match the expected interface, even if the JSON is valid.
|
||||
|
||||
/**
|
||||
* Represents an aggregate price from Pyth publisher feeds.
|
||||
*/
|
||||
export interface PriceFeed {
|
||||
/**
|
||||
* Exponentially-weighted moving average Price
|
||||
*/
|
||||
ema_price: Price;
|
||||
/**
|
||||
* Unique identifier for this price.
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Metadata of the price
|
||||
*/
|
||||
metadata?: PriceFeedMetadata;
|
||||
/**
|
||||
* Price
|
||||
*/
|
||||
price: Price;
|
||||
/**
|
||||
* VAA of the price
|
||||
*/
|
||||
vaa?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exponentially-weighted moving average Price
|
||||
*
|
||||
* Represents a Pyth price
|
||||
*
|
||||
* Price
|
||||
*/
|
||||
export interface Price {
|
||||
/**
|
||||
* Confidence interval around the price.
|
||||
*/
|
||||
conf: string;
|
||||
/**
|
||||
* Price exponent.
|
||||
*/
|
||||
expo: number;
|
||||
/**
|
||||
* Price.
|
||||
*/
|
||||
price: string;
|
||||
/**
|
||||
* Publish Time of the price
|
||||
*/
|
||||
publish_time: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata of the price
|
||||
*
|
||||
* Represents metadata of a price feed.
|
||||
*/
|
||||
export interface PriceFeedMetadata {
|
||||
/**
|
||||
* Attestation time of the price
|
||||
*/
|
||||
attestation_time: number;
|
||||
/**
|
||||
* Chain of the emitter
|
||||
*/
|
||||
emitter_chain: number;
|
||||
/**
|
||||
* The time that the price service received the price
|
||||
*/
|
||||
price_service_receive_time: number;
|
||||
/**
|
||||
* Sequence number of the price
|
||||
*/
|
||||
sequence_number: number;
|
||||
}
|
||||
|
||||
// Converts JSON types to/from your types
|
||||
// and asserts the results at runtime
|
||||
export class Convert {
|
||||
public static toPriceFeed(json: any): PriceFeed {
|
||||
return cast(json, r("PriceFeed"));
|
||||
}
|
||||
|
||||
public static priceFeedToJson(value: PriceFeed): any {
|
||||
return uncast(value, r("PriceFeed"));
|
||||
}
|
||||
|
||||
public static toPrice(json: any): Price {
|
||||
return cast(json, r("Price"));
|
||||
}
|
||||
|
||||
public static priceToJson(value: Price): any {
|
||||
return uncast(value, r("Price"));
|
||||
}
|
||||
|
||||
public static toPriceFeedMetadata(json: any): PriceFeedMetadata {
|
||||
return cast(json, r("PriceFeedMetadata"));
|
||||
}
|
||||
|
||||
public static priceFeedMetadataToJson(value: PriceFeedMetadata): any {
|
||||
return uncast(value, r("PriceFeedMetadata"));
|
||||
}
|
||||
}
|
||||
|
||||
function invalidValue(typ: any, val: any, key: any = ""): never {
|
||||
if (key) {
|
||||
throw Error(
|
||||
`Invalid value for key "${key}". Expected type ${JSON.stringify(
|
||||
typ
|
||||
)} but got ${JSON.stringify(val)}`
|
||||
);
|
||||
}
|
||||
throw Error(
|
||||
`Invalid value ${JSON.stringify(val)} for type ${JSON.stringify(typ)}`
|
||||
);
|
||||
}
|
||||
|
||||
function jsonToJSProps(typ: any): any {
|
||||
if (typ.jsonToJS === undefined) {
|
||||
const map: any = {};
|
||||
typ.props.forEach((p: any) => (map[p.json] = { key: p.js, typ: p.typ }));
|
||||
typ.jsonToJS = map;
|
||||
}
|
||||
return typ.jsonToJS;
|
||||
}
|
||||
|
||||
function jsToJSONProps(typ: any): any {
|
||||
if (typ.jsToJSON === undefined) {
|
||||
const map: any = {};
|
||||
typ.props.forEach((p: any) => (map[p.js] = { key: p.json, typ: p.typ }));
|
||||
typ.jsToJSON = map;
|
||||
}
|
||||
return typ.jsToJSON;
|
||||
}
|
||||
|
||||
function transform(val: any, typ: any, getProps: any, key: any = ""): any {
|
||||
function transformPrimitive(typ: string, val: any): any {
|
||||
if (typeof typ === typeof val) return val;
|
||||
return invalidValue(typ, val, key);
|
||||
}
|
||||
|
||||
function transformUnion(typs: any[], val: any): any {
|
||||
// val must validate against one typ in typs
|
||||
const l = typs.length;
|
||||
for (let i = 0; i < l; i++) {
|
||||
const typ = typs[i];
|
||||
try {
|
||||
return transform(val, typ, getProps);
|
||||
} catch (_) {}
|
||||
}
|
||||
return invalidValue(typs, val);
|
||||
}
|
||||
|
||||
function transformEnum(cases: string[], val: any): any {
|
||||
if (cases.indexOf(val) !== -1) return val;
|
||||
return invalidValue(cases, val);
|
||||
}
|
||||
|
||||
function transformArray(typ: any, val: any): any {
|
||||
// val must be an array with no invalid elements
|
||||
if (!Array.isArray(val)) return invalidValue("array", val);
|
||||
return val.map((el) => transform(el, typ, getProps));
|
||||
}
|
||||
|
||||
function transformDate(val: any): any {
|
||||
if (val === null) {
|
||||
return null;
|
||||
}
|
||||
const d = new Date(val);
|
||||
if (isNaN(d.valueOf())) {
|
||||
return invalidValue("Date", val);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
function transformObject(
|
||||
props: { [k: string]: any },
|
||||
additional: any,
|
||||
val: any
|
||||
): any {
|
||||
if (val === null || typeof val !== "object" || Array.isArray(val)) {
|
||||
return invalidValue("object", val);
|
||||
}
|
||||
const result: any = {};
|
||||
Object.getOwnPropertyNames(props).forEach((key) => {
|
||||
const prop = props[key];
|
||||
const v = Object.prototype.hasOwnProperty.call(val, key)
|
||||
? val[key]
|
||||
: undefined;
|
||||
result[prop.key] = transform(v, prop.typ, getProps, prop.key);
|
||||
});
|
||||
Object.getOwnPropertyNames(val).forEach((key) => {
|
||||
if (!Object.prototype.hasOwnProperty.call(props, key)) {
|
||||
result[key] = transform(val[key], additional, getProps, key);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
if (typ === "any") return val;
|
||||
if (typ === null) {
|
||||
if (val === null) return val;
|
||||
return invalidValue(typ, val);
|
||||
}
|
||||
if (typ === false) return invalidValue(typ, val);
|
||||
while (typeof typ === "object" && typ.ref !== undefined) {
|
||||
typ = typeMap[typ.ref];
|
||||
}
|
||||
if (Array.isArray(typ)) return transformEnum(typ, val);
|
||||
if (typeof typ === "object") {
|
||||
return typ.hasOwnProperty("unionMembers")
|
||||
? transformUnion(typ.unionMembers, val)
|
||||
: typ.hasOwnProperty("arrayItems")
|
||||
? transformArray(typ.arrayItems, val)
|
||||
: typ.hasOwnProperty("props")
|
||||
? transformObject(getProps(typ), typ.additional, val)
|
||||
: invalidValue(typ, val);
|
||||
}
|
||||
// Numbers can be parsed by Date but shouldn't be.
|
||||
if (typ === Date && typeof val !== "number") return transformDate(val);
|
||||
return transformPrimitive(typ, val);
|
||||
}
|
||||
|
||||
function cast<T>(val: any, typ: any): T {
|
||||
return transform(val, typ, jsonToJSProps);
|
||||
}
|
||||
|
||||
function uncast<T>(val: T, typ: any): any {
|
||||
return transform(val, typ, jsToJSONProps);
|
||||
}
|
||||
|
||||
function a(typ: any) {
|
||||
return { arrayItems: typ };
|
||||
}
|
||||
|
||||
function u(...typs: any[]) {
|
||||
return { unionMembers: typs };
|
||||
}
|
||||
|
||||
function o(props: any[], additional: any) {
|
||||
return { props, additional };
|
||||
}
|
||||
|
||||
function m(additional: any) {
|
||||
return { props: [], additional };
|
||||
}
|
||||
|
||||
function r(name: string) {
|
||||
return { ref: name };
|
||||
}
|
||||
|
||||
const typeMap: any = {
|
||||
PriceFeed: o(
|
||||
[
|
||||
{ json: "ema_price", js: "ema_price", typ: r("Price") },
|
||||
{ json: "id", js: "id", typ: "" },
|
||||
{
|
||||
json: "metadata",
|
||||
js: "metadata",
|
||||
typ: u(undefined, r("PriceFeedMetadata")),
|
||||
},
|
||||
{ json: "price", js: "price", typ: r("Price") },
|
||||
{ json: "vaa", js: "vaa", typ: u(undefined, "") },
|
||||
],
|
||||
"any"
|
||||
),
|
||||
Price: o(
|
||||
[
|
||||
{ json: "conf", js: "conf", typ: "" },
|
||||
{ json: "expo", js: "expo", typ: 0 },
|
||||
{ json: "price", js: "price", typ: "" },
|
||||
{ json: "publish_time", js: "publish_time", typ: 0 },
|
||||
],
|
||||
"any"
|
||||
),
|
||||
PriceFeedMetadata: o(
|
||||
[
|
||||
{ json: "attestation_time", js: "attestation_time", typ: 0 },
|
||||
{ json: "emitter_chain", js: "emitter_chain", typ: 0 },
|
||||
{
|
||||
json: "price_service_receive_time",
|
||||
js: "price_service_receive_time",
|
||||
typ: 0,
|
||||
},
|
||||
{ json: "sequence_number", js: "sequence_number", typ: 0 },
|
||||
],
|
||||
"any"
|
||||
),
|
||||
};
|
|
@ -0,0 +1,91 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PriceFeed",
|
||||
"description": "Represents an aggregate price from Pyth publisher feeds.",
|
||||
"type": "object",
|
||||
"required": ["id", "price", "ema_price"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Unique identifier for this price.",
|
||||
"$ref": "#/definitions/Identifier"
|
||||
},
|
||||
"price": {
|
||||
"description": "Price",
|
||||
"$ref": "#/definitions/Price"
|
||||
},
|
||||
"ema_price": {
|
||||
"description": "Exponentially-weighted moving average Price",
|
||||
"$ref": "#/definitions/Price"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Metadata of the price",
|
||||
"$ref": "#/definitions/PriceFeedMetadata"
|
||||
},
|
||||
"vaa": {
|
||||
"description": "VAA of the price",
|
||||
"$ref": "#/definitions/Identifier"
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"Identifier": {
|
||||
"type": "string"
|
||||
},
|
||||
"Price": {
|
||||
"description": "Represents a Pyth price",
|
||||
"type": "object",
|
||||
"required": ["conf", "expo", "price", "publish_time"],
|
||||
"properties": {
|
||||
"conf": {
|
||||
"description": "Confidence interval around the price.",
|
||||
"type": "string"
|
||||
},
|
||||
"expo": {
|
||||
"description": "Price exponent.",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"price": {
|
||||
"description": "Price.",
|
||||
"type": "string"
|
||||
},
|
||||
"publish_time": {
|
||||
"description": "Publish Time of the price",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"PriceFeedMetadata": {
|
||||
"description": "Represents metadata of a price feed.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"attestation_time",
|
||||
"emitter_chain",
|
||||
"price_service_receive_time",
|
||||
"sequence_number"
|
||||
],
|
||||
"properties": {
|
||||
"attestation_time": {
|
||||
"description": "Attestation time of the price",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"emitter_chain": {
|
||||
"description": "Chain of the emitter",
|
||||
"type": "integer",
|
||||
"format": "int16"
|
||||
},
|
||||
"price_service_receive_time": {
|
||||
"description": "The time that the price service received the price",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"sequence_number": {
|
||||
"description": "Sequence number of the price",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"outDir": "./lib",
|
||||
"strict": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "**/__tests__/*"]
|
||||
}
|
|
@ -3,8 +3,8 @@ SPY_SERVICE_HOST=0.0.0.0:7072
|
|||
|
||||
# Filters (if provided) should be valid JSON like below:
|
||||
# These filters tell the spy to only retrieve messages sent from certain chains/contracts.
|
||||
# See the docker-compose.<network>.yaml files in the price_service directory for the appropriate
|
||||
# configuration for a testnet/mainnet pyth price_service deployment.
|
||||
# See the docker-compose.<network>.yaml files for the appropriate configuration for a
|
||||
# testnet/mainnet pyth price_service deployment.
|
||||
SPY_SERVICE_FILTERS=[{"chain_id":1,"emitter_address":"71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b"}]
|
||||
|
||||
# Number of seconds to sync with spy to be sure to have latest messages
|
|
@ -10,7 +10,7 @@ WORKDIR ${BASE_PATH}/${P2W_SDK_REL_PATH}
|
|||
COPY --chown=pyth:pyth ${P2W_SDK_REL_PATH} .
|
||||
RUN npm ci && npm run build && npm cache clean --force
|
||||
|
||||
ARG PRICE_SERVICE_REL_PATH=price_service
|
||||
ARG PRICE_SERVICE_REL_PATH=price_service/server
|
||||
WORKDIR ${BASE_PATH}/${PRICE_SERVICE_REL_PATH}
|
||||
COPY --chown=pyth:pyth ${PRICE_SERVICE_REL_PATH} .
|
||||
RUN npm ci && npm run build && npm cache clean --force
|
|
@ -42,7 +42,7 @@ The compose files use a public release of Pyth price service and spy. If you wis
|
|||
price service you should:
|
||||
|
||||
1. Build an image for using it first according to the section below.
|
||||
2. Change the price service image to your local docker image (e.g., `pyth_price_service`)
|
||||
2. Change the price service image to your local docker image (e.g., `pyth_price_server`)
|
||||
|
||||
## Build an image
|
||||
|
||||
|
@ -54,11 +54,11 @@ docker buildx build -f Dockerfile.wasm -o type=local,dest=. .
|
|||
```
|
||||
|
||||
Then, build the image from [the repo root](../../../) like below. It will create a
|
||||
local image named `pyth_price_service`.
|
||||
local image named `pyth_price_server`.
|
||||
|
||||
```
|
||||
$ docker buildx build -f price_service/Dockerfile.price_service -t pyth_price_service .
|
||||
$ docker buildx build -f price_service/server/Dockerfile -t pyth_price_server .
|
||||
```
|
||||
|
||||
If you wish to build price service without docker, please follow the instruction of the price service
|
||||
[`Dockerfile`](./Dockerfile.price_service)
|
||||
[`Dockerfile`](./Dockerfile)
|
|
@ -18,7 +18,7 @@ services:
|
|||
# Find latest price service images https://gallery.ecr.aws/pyth-network/xc-server
|
||||
image: public.ecr.aws/pyth-network/xc-server:v2.2.3
|
||||
# Or alternatively use a locally built image
|
||||
# image: pyth_price_service
|
||||
# image: pyth_price_server
|
||||
environment:
|
||||
SPY_SERVICE_HOST: "spy:7072"
|
||||
SPY_SERVICE_FILTERS: |
|
|
@ -18,7 +18,7 @@ services:
|
|||
# Find latest price service images https://gallery.ecr.aws/pyth-network/xc-server
|
||||
image: public.ecr.aws/pyth-network/xc-server:v2.2.3
|
||||
# Or alternatively use a locally built image
|
||||
# image: pyth_price_service
|
||||
# image: pyth_price_server
|
||||
environment:
|
||||
SPY_SERVICE_HOST: "spy:7072"
|
||||
SPY_SERVICE_FILTERS: |
|
|
@ -0,0 +1,5 @@
|
|||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
};
|
|
@ -1,17 +1,17 @@
|
|||
{
|
||||
"name": "@pythnetwork/pyth-price-service",
|
||||
"name": "@pythnetwork/price-service-server",
|
||||
"version": "2.3.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@pythnetwork/pyth-price-service",
|
||||
"name": "@pythnetwork/price-service-server",
|
||||
"version": "2.3.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@certusone/wormhole-sdk": "^0.9.9",
|
||||
"@certusone/wormhole-spydk": "^0.0.1",
|
||||
"@pythnetwork/p2w-sdk-js": "file:../third_party/pyth/p2w-sdk/js",
|
||||
"@pythnetwork/p2w-sdk-js": "file:../../third_party/pyth/p2w-sdk/js",
|
||||
"@pythnetwork/pyth-sdk-js": "^1.1.0",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/express": "^4.17.13",
|
||||
|
@ -79,7 +79,7 @@
|
|||
"@solana/web3.js": "^1.24.0"
|
||||
}
|
||||
},
|
||||
"../third_party/pyth/p2w-sdk/js": {
|
||||
"../../third_party/pyth/p2w-sdk/js": {
|
||||
"name": "@pythnetwork/p2w-sdk-js",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
|
@ -2827,7 +2827,7 @@
|
|||
"integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA="
|
||||
},
|
||||
"node_modules/@pythnetwork/p2w-sdk-js": {
|
||||
"resolved": "../third_party/pyth/p2w-sdk/js",
|
||||
"resolved": "../../third_party/pyth/p2w-sdk/js",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@pythnetwork/pyth-sdk-js": {
|
||||
|
@ -12341,7 +12341,7 @@
|
|||
"integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA="
|
||||
},
|
||||
"@pythnetwork/p2w-sdk-js": {
|
||||
"version": "file:../third_party/pyth/p2w-sdk/js",
|
||||
"version": "file:../../third_party/pyth/p2w-sdk/js",
|
||||
"requires": {
|
||||
"@certusone/wormhole-sdk": "0.2.1",
|
||||
"@improbable-eng/grpc-web-node-http-transport": "^0.14.1",
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@pythnetwork/pyth-price-service",
|
||||
"name": "@pythnetwork/price-service-server",
|
||||
"version": "2.3.3",
|
||||
"description": "Pyth Price Service",
|
||||
"description": "Pyth price pervice server",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"format": "prettier --write \"src/**/*.ts\"",
|
||||
|
@ -31,7 +31,7 @@
|
|||
"dependencies": {
|
||||
"@certusone/wormhole-sdk": "^0.9.9",
|
||||
"@certusone/wormhole-spydk": "^0.0.1",
|
||||
"@pythnetwork/p2w-sdk-js": "file:../third_party/pyth/p2w-sdk/js",
|
||||
"@pythnetwork/p2w-sdk-js": "file:../../third_party/pyth/p2w-sdk/js",
|
||||
"@pythnetwork/pyth-sdk-js": "^1.1.0",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/express": "^4.17.13",
|
|
@ -15,7 +15,7 @@ logging.basicConfig(
|
|||
PYTH_TEST_ACCOUNTS_HOST = "pyth"
|
||||
PYTH_TEST_ACCOUNTS_PORT = 4242
|
||||
|
||||
PRICE_SERVICE_HOST = "pyth-price-service"
|
||||
PRICE_SERVICE_HOST = "pyth-price-server"
|
||||
PRICE_SERVICE_PORT = 4200
|
||||
|
||||
def base58_to_hex(base58_string):
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: pyth-price-service
|
||||
name: pyth-price-server
|
||||
labels:
|
||||
app: pyth-price-service
|
||||
app: pyth-price-server
|
||||
spec:
|
||||
ports:
|
||||
- port: 8081
|
||||
|
@ -15,27 +15,27 @@ spec:
|
|||
protocol: TCP
|
||||
clusterIP: None
|
||||
selector:
|
||||
app: pyth-price-service
|
||||
app: pyth-price-server
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: pyth-price-service
|
||||
name: pyth-price-server
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: pyth-price-service
|
||||
serviceName: pyth-price-service
|
||||
app: pyth-price-server
|
||||
serviceName: pyth-price-server
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: pyth-price-service
|
||||
app: pyth-price-server
|
||||
spec:
|
||||
terminationGracePeriodSeconds: 0
|
||||
containers:
|
||||
- name: pyth-price-service
|
||||
image: pyth-price-service
|
||||
- name: pyth-price-server
|
||||
image: pyth-price-server
|
||||
ports:
|
||||
- containerPort: 8081
|
||||
name: prometheus
|
||||
|
@ -77,7 +77,7 @@ spec:
|
|||
- name: CACHE_TTL_SECONDS
|
||||
value: "300"
|
||||
- name: tests
|
||||
image: pyth-price-service
|
||||
image: pyth-price-server
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
Loading…
Reference in New Issue