[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:
Ali Behjati 2023-01-25 17:17:32 +01:00 committed by GitHub
parent 16b7977179
commit b4cf527f00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 22590 additions and 41 deletions

View File

@ -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

View File

@ -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),

View File

@ -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",
},
};

1
price_service/client/js/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
lib/

9175
price_service/client/js/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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();

View File

@ -0,0 +1,12 @@
export {
DurationInMs,
PriceServiceConnection,
PriceServiceConnectionConfig,
} from "./PriceServiceConnection";
export {
HexString,
PriceFeed,
Price,
UnixTimestamp,
} from "@pythnetwork/pyth-sdk-js";

View File

@ -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;
}
}

View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"declaration": true,
"outDir": "./lib",
"strict": true
},
"include": ["src"],
"exclude": ["node_modules", "**/__tests__/*"]
}

View File

@ -0,0 +1,3 @@
node_modules/
lib/
**/schemas

View File

@ -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",
},
};

2
price_service/sdk/js/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
lib

View File

@ -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.

View File

@ -0,0 +1,5 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
};

11742
price_service/sdk/js/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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"
);
});

View File

@ -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;
}
}

View File

@ -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"
),
};

View File

@ -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"
}
}
}
}
}

View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"declaration": true,
"outDir": "./lib",
"strict": true
},
"include": ["src"],
"exclude": ["node_modules", "**/__tests__/*"]
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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: |

View File

@ -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: |

View File

@ -0,0 +1,5 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
};

View File

@ -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",

View File

@ -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",

View File

@ -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):

View File

@ -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