Cascade Pyth SDK JS update to downstream packages (#326)

* Fix annoying ethereum problem

* Update p2w-sdk

* rename + lint + format + bump to 1.0.0

* Update price service

* Bump version + format + add lint

* Fix price service linting issues

* Fix package rename issue

* Update package lock of dependencies

local dependency is aweful!

* Fix exclusion bug
This commit is contained in:
Ali Behjati 2022-10-06 07:21:04 +00:00 committed by GitHub
parent 5ff244941c
commit d7f436a856
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 340 additions and 287 deletions

View File

@ -23,7 +23,6 @@ WORKDIR /home/node/ethereum
# Only invalidate the npm install step if package.json changed
ADD --chown=node:node ethereum/package.json .
ADD --chown=node:node ethereum/package-lock.json .
ADD --chown=node:node ethereum/.env.test .env
# We want to cache node_modules *and* incorporate it into the final image.
RUN --mount=type=cache,uid=1000,gid=1000,target=/home/node/.npm \
@ -37,4 +36,4 @@ RUN --mount=type=cache,uid=1000,gid=1000,target=/home/node/.npm \
RUN rm -rf node_modules && mv node_modules_cache node_modules
ADD --chown=node:node ethereum/ .
ADD --chown=node:node ethereum/.env.test .env

View File

@ -9,9 +9,9 @@
"version": "1.0.0",
"license": "Apache-2.0",
"dependencies": {
"@certusone/p2w-sdk": "file:../p2w-sdk/js",
"@certusone/wormhole-sdk": "^0.1.4",
"@certusone/wormhole-spydk": "^0.0.1",
"@pythnetwork/p2w-sdk-js": "file:../p2w-sdk/js",
"@solana/spl-token": "^0.1.8",
"@solana/web3.js": "^1.24.0",
"@terra-money/terra.js": "^3.1.3",
@ -44,13 +44,13 @@
}
},
"../p2w-sdk/js": {
"name": "@certusone/p2w-sdk",
"version": "0.1.0",
"name": "@pythnetwork/p2w-sdk-js",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@certusone/wormhole-sdk": "0.2.1",
"@improbable-eng/grpc-web-node-http-transport": "^0.14.1",
"@pythnetwork/pyth-sdk-js": "^0.1.0"
"@pythnetwork/pyth-sdk-js": "^1.0.0"
},
"devDependencies": {
"@openzeppelin/contracts": "^4.2.0",
@ -699,10 +699,6 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
"node_modules/@certusone/p2w-sdk": {
"resolved": "../p2w-sdk/js",
"link": true
},
"node_modules/@certusone/wormhole-sdk": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.1.4.tgz",
@ -2060,6 +2056,10 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA="
},
"node_modules/@pythnetwork/p2w-sdk-js": {
"resolved": "../p2w-sdk/js",
"link": true
},
"node_modules/@sinonjs/commons": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz",
@ -8387,24 +8387,6 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
"@certusone/p2w-sdk": {
"version": "file:../p2w-sdk/js",
"requires": {
"@certusone/wormhole-sdk": "0.2.1",
"@improbable-eng/grpc-web-node-http-transport": "^0.14.1",
"@openzeppelin/contracts": "^4.2.0",
"@pythnetwork/pyth-sdk-js": "^0.1.0",
"@typechain/ethers-v5": "^7.1.2",
"@types/long": "^4.0.1",
"@types/node": "^16.6.1",
"copy-dir": "^1.3.0",
"find": "^0.3.0",
"prettier": "^2.3.2",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"typescript": "^4.3.5"
}
},
"@certusone/wormhole-sdk": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.1.4.tgz",
@ -9317,6 +9299,24 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA="
},
"@pythnetwork/p2w-sdk-js": {
"version": "file:../p2w-sdk/js",
"requires": {
"@certusone/wormhole-sdk": "0.2.1",
"@improbable-eng/grpc-web-node-http-transport": "^0.14.1",
"@openzeppelin/contracts": "^4.2.0",
"@pythnetwork/pyth-sdk-js": "^1.0.0",
"@typechain/ethers-v5": "^7.1.2",
"@types/long": "^4.0.1",
"@types/node": "^16.6.1",
"copy-dir": "^1.3.0",
"find": "^0.3.0",
"prettier": "^2.3.2",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"typescript": "^4.3.5"
}
},
"@sinonjs/commons": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz",

View File

@ -30,7 +30,7 @@
"typescript": "^4.3.5"
},
"dependencies": {
"@certusone/p2w-sdk": "file:../p2w-sdk/js",
"@pythnetwork/p2w-sdk-js": "file:../p2w-sdk/js",
"@certusone/wormhole-sdk": "^0.1.4",
"@certusone/wormhole-spydk": "^0.0.1",
"@solana/spl-token": "^0.1.8",

View File

@ -14,7 +14,7 @@ import {
subscribeSignedVAA,
} from "@certusone/wormhole-spydk";
import { parseBatchPriceAttestation, getBatchSummary } from "@certusone/p2w-sdk";
import { parseBatchPriceAttestation, getBatchSummary } from "@pythnetwork/p2w-sdk-js";
import { importCoreWasm } from "@certusone/wormhole-sdk/lib/cjs/solana/wasm";

View File

@ -5,7 +5,7 @@ import { hexToUint8Array } from "@certusone/wormhole-sdk";
import { importCoreWasm } from "@certusone/wormhole-sdk/lib/cjs/solana/wasm";
import { PythUpgradable__factory, PythUpgradable } from "../evm/bindings/";
import { parseBatchPriceAttestation } from "@certusone/p2w-sdk";
import { parseBatchPriceAttestation } from "@pythnetwork/p2w-sdk-js";
let WH_WASM: any = null;

View File

@ -8,7 +8,7 @@ import { Relay, RelayResult, RelayRetcode } from "./relay/iface";
import * as helpers from "./helpers";
import { logger } from "./helpers";
import { PromHelper } from "./promHelpers";
import { BatchPriceAttestation, getBatchAttestationHashKey, getBatchSummary } from "@certusone/p2w-sdk";
import { BatchPriceAttestation, getBatchAttestationHashKey, getBatchSummary } from "@pythnetwork/p2w-sdk-js";
const mutex = new Mutex();
let condition = new CondVar();

View File

@ -1,17 +1,17 @@
{
"name": "@certusone/p2w-sdk",
"version": "0.1.0",
"name": "@pythnetwork/p2w-sdk-js",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@certusone/p2w-sdk",
"version": "0.1.0",
"name": "@pythnetwork/p2w-sdk-js",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@certusone/wormhole-sdk": "0.2.1",
"@improbable-eng/grpc-web-node-http-transport": "^0.14.1",
"@pythnetwork/pyth-sdk-js": "^0.3.0"
"@pythnetwork/pyth-sdk-js": "^1.0.0"
},
"devDependencies": {
"@openzeppelin/contracts": "^4.2.0",
@ -913,9 +913,9 @@
"integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA="
},
"node_modules/@pythnetwork/pyth-sdk-js": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@pythnetwork/pyth-sdk-js/-/pyth-sdk-js-0.3.0.tgz",
"integrity": "sha512-7xsSM5PWD8+ez8lB5R0ofpaP1J1bRrtVkp9zm7Ry8QtKq5dOFfQqSqOjh9tLTX2h8i2xD93//0EnXXw35pzCkg=="
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@pythnetwork/pyth-sdk-js/-/pyth-sdk-js-1.0.0.tgz",
"integrity": "sha512-nZ3tmn5EhR7Y6177cAE7p7iQJK40bipMUI4ZBwRhgTONOcg35jG0fsvlETZYgij0baQ1PMJQE6dIqZ50EMZpJw=="
},
"node_modules/@solana/buffer-layout": {
"version": "4.0.0",
@ -3236,9 +3236,9 @@
"integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA="
},
"@pythnetwork/pyth-sdk-js": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@pythnetwork/pyth-sdk-js/-/pyth-sdk-js-0.3.0.tgz",
"integrity": "sha512-7xsSM5PWD8+ez8lB5R0ofpaP1J1bRrtVkp9zm7Ry8QtKq5dOFfQqSqOjh9tLTX2h8i2xD93//0EnXXw35pzCkg=="
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@pythnetwork/pyth-sdk-js/-/pyth-sdk-js-1.0.0.tgz",
"integrity": "sha512-nZ3tmn5EhR7Y6177cAE7p7iQJK40bipMUI4ZBwRhgTONOcg35jG0fsvlETZYgij0baQ1PMJQE6dIqZ50EMZpJw=="
},
"@solana/buffer-layout": {
"version": "4.0.0",

View File

@ -1,6 +1,6 @@
{
"name": "@certusone/p2w-sdk",
"version": "0.1.0",
"name": "@pythnetwork/p2w-sdk-js",
"version": "1.0.0",
"description": "TypeScript library for interacting with Pyth2Wormhole",
"types": "lib/index.d.ts",
"main": "lib/index.js",
@ -11,6 +11,7 @@
"build": "npm run build-lib",
"build-lib": "npm run copy-artifacts && tsc",
"build-watch": "npm run copy-artifacts && tsc --watch",
"format": "prettier --write \"src/**/*.ts\"",
"copy-artifacts": "node scripts/copyWasm.cjs",
"lint": "tslint -p tsconfig.json",
"postversion": "git push && git push --tags",
@ -41,7 +42,7 @@
"dependencies": {
"@certusone/wormhole-sdk": "0.2.1",
"@improbable-eng/grpc-web-node-http-transport": "^0.14.1",
"@pythnetwork/pyth-sdk-js": "^0.3.0"
"@pythnetwork/pyth-sdk-js": "^1.0.0"
},
"bugs": {
"url": "https://github.com/pyth-network/pyth-crosschain/issues"

View File

@ -1,116 +1,141 @@
import { getSignedVAA, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
import { zeroPad } from "ethers/lib/utils";
import { PublicKey } from "@solana/web3.js";
import { PriceFeed, PriceStatus, UnixTimestamp } from "@pythnetwork/pyth-sdk-js";
let _P2W_WASM: any = undefined;
import { PriceFeed, Price, UnixTimestamp } from "@pythnetwork/pyth-sdk-js";
let _P2W_WASM: any;
async function importWasm() {
if (!_P2W_WASM) {
if (typeof window === 'undefined') {
_P2W_WASM = await import("./solana/p2w-core/nodejs/p2w_sdk");
} else {
_P2W_WASM = await import("./solana/p2w-core/bundler/p2w_sdk");
}
if (!_P2W_WASM) {
if (typeof window === "undefined") {
_P2W_WASM = await import("./solana/p2w-core/nodejs/p2w_sdk");
} else {
_P2W_WASM = await import("./solana/p2w-core/bundler/p2w_sdk");
}
return _P2W_WASM;
}
return _P2W_WASM;
}
export type PriceAttestation = {
productId: string;
priceId: string;
price: string;
conf: string;
expo: number;
emaPrice: string;
emaConf: string;
status: PriceStatus;
numPublishers: number;
maxNumPublishers: number;
attestationTime: UnixTimestamp;
publishTime: UnixTimestamp;
prevPublishTime: UnixTimestamp;
prevPrice: string;
prevConf: string;
productId: string;
priceId: string;
price: string;
conf: string;
expo: number;
emaPrice: string;
emaConf: string;
status: number;
numPublishers: number;
maxNumPublishers: number;
attestationTime: UnixTimestamp;
publishTime: UnixTimestamp;
prevPublishTime: UnixTimestamp;
prevPrice: string;
prevConf: string;
};
export type BatchPriceAttestation = {
priceAttestations: PriceAttestation[];
priceAttestations: PriceAttestation[];
};
export async function parseBatchPriceAttestation(
arr: Buffer
arr: Buffer
): Promise<BatchPriceAttestation> {
let wasm = await importWasm();
let rawVal = await wasm.parse_batch_attestation(arr);
const wasm = await importWasm();
const rawVal = await wasm.parse_batch_attestation(arr);
return rawVal;
return rawVal;
}
// Returns a hash of all priceIds within the batch, it can be used to identify whether there is a
// new batch with exact same symbols (and ignore the old one)
export function getBatchAttestationHashKey(
batchAttestation: BatchPriceAttestation
batchAttestation: BatchPriceAttestation
): string {
const priceIds: string[] = batchAttestation.priceAttestations.map(
(priceAttestation) => priceAttestation.priceId
);
priceIds.sort();
const priceIds: string[] = batchAttestation.priceAttestations.map(
(priceAttestation) => priceAttestation.priceId
);
priceIds.sort();
return priceIds.join("#");
return priceIds.join("#");
}
export function getBatchSummary(
batch: BatchPriceAttestation
): string {
let abstractRepresentation = {
num_attestations: batch.priceAttestations.length,
prices: batch.priceAttestations.map((priceAttestation) => {
return {
price_id: priceAttestation.priceId,
price: computePrice(priceAttestation.price, priceAttestation.expo),
conf: computePrice(
priceAttestation.conf,
priceAttestation.expo
),
};
}),
};
return JSON.stringify(abstractRepresentation);
export function getBatchSummary(batch: BatchPriceAttestation): string {
const abstractRepresentation = {
num_attestations: batch.priceAttestations.length,
prices: batch.priceAttestations.map((priceAttestation) => {
const priceFeed = priceAttestationToPriceFeed(priceAttestation);
return {
price_id: priceFeed.id,
price: priceFeed.getPriceUnchecked().getPriceAsNumberUnchecked(),
conf: priceFeed.getEmaPriceUnchecked().getConfAsNumberUnchecked(),
};
}),
};
return JSON.stringify(abstractRepresentation);
}
export async function getSignedAttestation(host: string, p2w_addr: string, sequence: number, extraGrpcOpts = {}): Promise<any> {
let [emitter, _] = await PublicKey.findProgramAddress([Buffer.from("p2w-emitter")], new PublicKey(p2w_addr));
export async function getSignedAttestation(
host: string,
p2wAddr: string,
sequence: number,
extraGrpcOpts = {}
): Promise<any> {
const [emitter, _] = await PublicKey.findProgramAddress(
[Buffer.from("p2w-emitter")],
new PublicKey(p2wAddr)
);
let emitterHex = sol_addr2buf(emitter).toString("hex");
return await getSignedVAA(host, CHAIN_ID_SOLANA, emitterHex, "" + sequence, extraGrpcOpts);
const emitterHex = sol_addr2buf(emitter).toString("hex");
return await getSignedVAA(
host,
CHAIN_ID_SOLANA,
emitterHex,
"" + sequence,
extraGrpcOpts
);
}
export function priceAttestationToPriceFeed(priceAttestation: PriceAttestation): PriceFeed {
return new PriceFeed({
conf: priceAttestation.conf.toString(),
emaConf: priceAttestation.emaConf.toString(),
emaPrice: priceAttestation.emaPrice.toString(),
expo: priceAttestation.expo as any,
id: priceAttestation.priceId,
maxNumPublishers: priceAttestation.maxNumPublishers as any,
numPublishers: priceAttestation.numPublishers as any,
prevConf: priceAttestation.prevConf.toString(),
prevPrice: priceAttestation.prevPrice.toString(),
prevPublishTime: priceAttestation.prevPublishTime as any,
price: priceAttestation.price.toString(),
productId: priceAttestation.productId,
publishTime: priceAttestation.publishTime as any,
status: priceAttestation.status,
})
}
export function priceAttestationToPriceFeed(
priceAttestation: PriceAttestation
): PriceFeed {
const emaPrice: Price = new Price({
conf: priceAttestation.emaConf,
expo: priceAttestation.expo,
price: priceAttestation.emaPrice,
publishTime: priceAttestation.publishTime,
});
function computePrice(rawPrice: string, expo: number): number {
return Number(rawPrice) * 10 ** expo;
let price: Price;
if (priceAttestation.status === 1) {
// 1 means trading
price = new Price({
conf: priceAttestation.conf,
expo: priceAttestation.expo,
price: priceAttestation.price,
publishTime: priceAttestation.publishTime,
});
} else {
price = new Price({
conf: priceAttestation.prevConf,
expo: priceAttestation.expo,
price: priceAttestation.prevPrice,
publishTime: priceAttestation.prevPublishTime,
});
// emaPrice won't get updated if the status is unknown and hence it uses
// the previous publish time
emaPrice.publishTime = priceAttestation.prevPublishTime;
}
return new PriceFeed({
emaPrice,
id: priceAttestation.priceId,
price,
});
}
function sol_addr2buf(addr: PublicKey): Buffer {
return Buffer.from(zeroPad(addr.toBytes(), 32));
return Buffer.from(zeroPad(addr.toBytes(), 32));
}

View File

@ -1,18 +1,18 @@
{
"name": "@pythnetwork/pyth-price-service",
"version": "1.4.1",
"version": "2.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@pythnetwork/pyth-price-service",
"version": "1.4.1",
"version": "2.0.0",
"license": "Apache-2.0",
"dependencies": {
"@certusone/p2w-sdk": "file:../p2w-sdk/js",
"@certusone/wormhole-sdk": "^0.1.4",
"@certusone/wormhole-spydk": "^0.0.1",
"@pythnetwork/pyth-sdk-js": "^0.3.0",
"@pythnetwork/p2w-sdk-js": "file:../p2w-sdk/js",
"@pythnetwork/pyth-sdk-js": "^1.0.0",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.13",
"@types/morgan": "^1.9.3",
@ -50,13 +50,13 @@
}
},
"../p2w-sdk/js": {
"name": "@certusone/p2w-sdk",
"version": "0.1.0",
"name": "@pythnetwork/p2w-sdk-js",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@certusone/wormhole-sdk": "0.2.1",
"@improbable-eng/grpc-web-node-http-transport": "^0.14.1",
"@pythnetwork/pyth-sdk-js": "^0.3.0"
"@pythnetwork/pyth-sdk-js": "^1.0.0"
},
"devDependencies": {
"@openzeppelin/contracts": "^4.2.0",
@ -616,10 +616,6 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
"node_modules/@certusone/p2w-sdk": {
"resolved": "../p2w-sdk/js",
"link": true
},
"node_modules/@certusone/wormhole-sdk": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.1.6.tgz",
@ -2200,10 +2196,14 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA="
},
"node_modules/@pythnetwork/p2w-sdk-js": {
"resolved": "../p2w-sdk/js",
"link": true
},
"node_modules/@pythnetwork/pyth-sdk-js": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@pythnetwork/pyth-sdk-js/-/pyth-sdk-js-0.3.0.tgz",
"integrity": "sha512-7xsSM5PWD8+ez8lB5R0ofpaP1J1bRrtVkp9zm7Ry8QtKq5dOFfQqSqOjh9tLTX2h8i2xD93//0EnXXw35pzCkg=="
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@pythnetwork/pyth-sdk-js/-/pyth-sdk-js-1.0.0.tgz",
"integrity": "sha512-nZ3tmn5EhR7Y6177cAE7p7iQJK40bipMUI4ZBwRhgTONOcg35jG0fsvlETZYgij0baQ1PMJQE6dIqZ50EMZpJw=="
},
"node_modules/@sideway/address": {
"version": "4.1.4",
@ -9435,24 +9435,6 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
"@certusone/p2w-sdk": {
"version": "file:../p2w-sdk/js",
"requires": {
"@certusone/wormhole-sdk": "0.2.1",
"@improbable-eng/grpc-web-node-http-transport": "^0.14.1",
"@openzeppelin/contracts": "^4.2.0",
"@pythnetwork/pyth-sdk-js": "^0.3.0",
"@typechain/ethers-v5": "^7.1.2",
"@types/long": "^4.0.1",
"@types/node": "^16.6.1",
"copy-dir": "^1.3.0",
"find": "^0.3.0",
"prettier": "^2.3.2",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"typescript": "^4.3.5"
}
},
"@certusone/wormhole-sdk": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.1.6.tgz",
@ -10544,10 +10526,28 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA="
},
"@pythnetwork/p2w-sdk-js": {
"version": "file:../p2w-sdk/js",
"requires": {
"@certusone/wormhole-sdk": "0.2.1",
"@improbable-eng/grpc-web-node-http-transport": "^0.14.1",
"@openzeppelin/contracts": "^4.2.0",
"@pythnetwork/pyth-sdk-js": "^1.0.0",
"@typechain/ethers-v5": "^7.1.2",
"@types/long": "^4.0.1",
"@types/node": "^16.6.1",
"copy-dir": "^1.3.0",
"find": "^0.3.0",
"prettier": "^2.3.2",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"typescript": "^4.3.5"
}
},
"@pythnetwork/pyth-sdk-js": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@pythnetwork/pyth-sdk-js/-/pyth-sdk-js-0.3.0.tgz",
"integrity": "sha512-7xsSM5PWD8+ez8lB5R0ofpaP1J1bRrtVkp9zm7Ry8QtKq5dOFfQqSqOjh9tLTX2h8i2xD93//0EnXXw35pzCkg=="
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@pythnetwork/pyth-sdk-js/-/pyth-sdk-js-1.0.0.tgz",
"integrity": "sha512-nZ3tmn5EhR7Y6177cAE7p7iQJK40bipMUI4ZBwRhgTONOcg35jG0fsvlETZYgij0baQ1PMJQE6dIqZ50EMZpJw=="
},
"@sideway/address": {
"version": "4.1.4",

View File

@ -1,13 +1,16 @@
{
"name": "@pythnetwork/pyth-price-service",
"version": "1.4.1",
"version": "2.0.0",
"description": "Pyth Price Service",
"main": "index.js",
"scripts": {
"format": "prettier --write \"src/**/*.ts\"",
"build": "tsc",
"start": "node lib/index.js",
"test": "jest src/"
"test": "jest src/",
"lint": "tslint -p tsconfig.json",
"preversion": "npm run lint",
"version": "npm run format && git add -A src"
},
"author": "",
"license": "Apache-2.0",
@ -25,10 +28,10 @@
"typescript": "^4.3.5"
},
"dependencies": {
"@certusone/p2w-sdk": "file:../p2w-sdk/js",
"@pythnetwork/p2w-sdk-js": "file:../p2w-sdk/js",
"@certusone/wormhole-sdk": "^0.1.4",
"@certusone/wormhole-spydk": "^0.0.1",
"@pythnetwork/pyth-sdk-js": "^0.3.0",
"@pythnetwork/pyth-sdk-js": "^1.0.0",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.13",
"@types/morgan": "^1.9.3",

View File

@ -1,4 +1,4 @@
import { HexString, PriceFeed, PriceStatus } from "@pythnetwork/pyth-sdk-js";
import { HexString, PriceFeed, Price } from "@pythnetwork/pyth-sdk-js";
import { PriceStore, PriceInfo } from "../listen";
import { RestAPI } from "../rest";
import { Express } from "express";
@ -14,20 +14,19 @@ function expandTo64Len(id: string): string {
function dummyPriceFeed(id: string): PriceFeed {
return new PriceFeed({
conf: "0",
emaConf: "1",
emaPrice: "2",
expo: 4,
emaPrice: new Price({
conf: "1",
expo: 2,
price: "3",
publishTime: 4,
}),
id,
maxNumPublishers: 7,
numPublishers: 6,
prevConf: "8",
prevPrice: "9",
prevPublishTime: 10,
price: "11",
productId: "def456",
publishTime: 13,
status: PriceStatus.Trading,
price: new Price({
conf: "5",
expo: 6,
price: "7",
publishTime: 8,
}),
});
}
@ -56,11 +55,11 @@ beforeAll(async () => {
dummyPriceInfoPair(expandTo64Len("10101"), 3, "bidbidbid"),
]);
let priceInfo: PriceStore = {
const priceInfo: PriceStore = {
getLatestPriceInfo: (priceFeedId: string) => {
return priceInfoMap.get(priceFeedId);
},
addUpdateListener: (_callback: (priceInfo: PriceInfo) => any) => {},
addUpdateListener: (_callback: (priceInfo: PriceInfo) => any) => undefined,
getPriceIds: () => new Set(),
};
@ -72,7 +71,9 @@ beforeAll(async () => {
describe("Latest Price Feed Endpoint", () => {
test("When called with valid ids, returns correct price feed", async () => {
const ids = [expandTo64Len("abcd"), expandTo64Len("3456")];
const resp = await request(app).get("/api/latest_price_feeds").query({ ids });
const resp = await request(app)
.get("/api/latest_price_feeds")
.query({ ids });
expect(resp.status).toBe(StatusCodes.OK);
expect(resp.body.length).toBe(2);
expect(resp.body).toContainEqual(dummyPriceFeed(ids[0]).toJson());
@ -85,7 +86,9 @@ describe("Latest Price Feed Endpoint", () => {
expandTo64Len("3456"),
expandTo64Len("effe"),
];
const resp = await request(app).get("/api/latest_price_feeds").query({ ids });
const resp = await request(app)
.get("/api/latest_price_feeds")
.query({ ids });
expect(resp.status).toBe(StatusCodes.BAD_REQUEST);
expect(resp.body.message).toContain(ids[0]);
expect(resp.body.message).not.toContain(ids[1]);

View File

@ -1,4 +1,4 @@
import { HexString, PriceFeed, PriceStatus } from "@pythnetwork/pyth-sdk-js";
import { HexString, PriceFeed } from "@pythnetwork/pyth-sdk-js";
import { Server } from "http";
import { WebSocket, WebSocketServer } from "ws";
import { sleep } from "../helpers";
@ -33,12 +33,12 @@ function dummyPriceMetadata(
function dummyPriceInfo(
id: HexString,
vaa: HexString,
priceMetadata: any
dummyPriceMetadataValue: any
): PriceInfo {
return {
seqNum: priceMetadata.sequence_number,
attestationTime: priceMetadata.attestation_time,
emitterChainId: priceMetadata.emitter_chain,
seqNum: dummyPriceMetadataValue.sequence_number,
attestationTime: dummyPriceMetadataValue.attestation_time,
emitterChainId: dummyPriceMetadataValue.emitter_chain,
priceFeed: dummyPriceFeed(id),
vaaBytes: Buffer.from(vaa, "hex").toString("binary"),
};
@ -46,20 +46,19 @@ function dummyPriceInfo(
function dummyPriceFeed(id: string): PriceFeed {
return PriceFeed.fromJson({
conf: "0",
ema_conf: "1",
ema_price: "2",
expo: 3,
ema_price: {
conf: "1",
expo: 2,
price: "3",
publish_time: 4,
},
id,
max_num_publishers: 5,
num_publishers: 6,
prev_conf: "7",
prev_price: "8",
prev_publish_time: 9,
price: "10",
product_id: "def456",
publish_time: 12,
status: PriceStatus.Trading,
price: {
conf: "5",
expo: 6,
price: "7",
publish_time: 8,
},
});
}
@ -101,11 +100,10 @@ beforeAll(async () => {
dummyPriceInfo(expandTo64Len("6789"), "bidbidbid", priceMetadata),
];
let priceInfo: PriceStore = {
const priceInfo: PriceStore = {
getLatestPriceInfo: (_priceFeedId: string) => undefined,
addUpdateListener: (_callback: (priceInfo: PriceInfo) => any) => undefined,
getPriceIds: () =>
new Set(priceInfos.map((priceInfo) => priceInfo.priceFeed.id)),
getPriceIds: () => new Set(priceInfos.map((info) => info.priceFeed.id)),
};
api = new WebSocketAPI(priceInfo);
@ -123,9 +121,9 @@ afterAll(async () => {
describe("Client receives data", () => {
test("When subscribes with valid ids without verbose flag, returns correct price feed", async () => {
let [client, serverMessages] = await createSocketClient();
const [client, serverMessages] = await createSocketClient();
let message: ClientMessage = {
const message: ClientMessage = {
ids: [priceInfos[0].priceFeed.id, priceInfos[1].priceFeed.id],
type: "subscribe",
};
@ -162,9 +160,9 @@ describe("Client receives data", () => {
});
test("When subscribes with valid ids and verbose flag set to true, returns correct price feed with metadata", async () => {
let [client, serverMessages] = await createSocketClient();
const [client, serverMessages] = await createSocketClient();
let message: ClientMessage = {
const message: ClientMessage = {
ids: [priceInfos[0].priceFeed.id, priceInfos[1].priceFeed.id],
type: "subscribe",
verbose: true,
@ -208,9 +206,9 @@ describe("Client receives data", () => {
});
test("When subscribes with valid ids and verbose flag set to false, returns correct price feed without metadata", async () => {
let [client, serverMessages] = await createSocketClient();
const [client, serverMessages] = await createSocketClient();
let message: ClientMessage = {
const message: ClientMessage = {
ids: [priceInfos[0].priceFeed.id, priceInfos[1].priceFeed.id],
type: "subscribe",
verbose: false,
@ -248,9 +246,9 @@ describe("Client receives data", () => {
});
test("When subscribes with invalid ids, returns error", async () => {
let [client, serverMessages] = await createSocketClient();
const [client, serverMessages] = await createSocketClient();
let message: ClientMessage = {
const message: ClientMessage = {
ids: [expandTo64Len("aaaa")],
type: "subscribe",
};
@ -268,9 +266,9 @@ describe("Client receives data", () => {
});
test("When subscribes for Price Feed A, doesn't receive updates for Price Feed B", async () => {
let [client, serverMessages] = await createSocketClient();
const [client, serverMessages] = await createSocketClient();
let message: ClientMessage = {
const message: ClientMessage = {
ids: [priceInfos[0].priceFeed.id],
type: "subscribe",
};
@ -305,7 +303,7 @@ describe("Client receives data", () => {
});
test("When subscribes for Price Feed A, receives updated and when unsubscribes stops receiving", async () => {
let [client, serverMessages] = await createSocketClient();
const [client, serverMessages] = await createSocketClient();
let message: ClientMessage = {
ids: [priceInfos[0].priceFeed.id],
@ -355,9 +353,9 @@ describe("Client receives data", () => {
});
test("Unsubscribe on not subscribed price feed is ok", async () => {
let [client, serverMessages] = await createSocketClient();
const [client, serverMessages] = await createSocketClient();
let message: ClientMessage = {
const message: ClientMessage = {
ids: [priceInfos[0].priceFeed.id],
type: "unsubscribe",
};
@ -376,17 +374,17 @@ describe("Client receives data", () => {
});
test("Multiple clients with different price feed works", async () => {
let [client1, serverMessages1] = await createSocketClient();
let [client2, serverMessages2] = await createSocketClient();
const [client1, serverMessages1] = await createSocketClient();
const [client2, serverMessages2] = await createSocketClient();
let message1: ClientMessage = {
const message1: ClientMessage = {
ids: [priceInfos[0].priceFeed.id],
type: "subscribe",
};
client1.send(JSON.stringify(message1));
let message2: ClientMessage = {
const message2: ClientMessage = {
ids: [priceInfos[1].priceFeed.id],
type: "subscribe",
};

View File

@ -9,9 +9,9 @@ export function sleep(ms: number) {
// Shorthand for optional/mandatory envs
export function envOrErr(env: string): string {
let val = process.env[env];
const val = process.env[env];
if (!val) {
throw `environment variable "${env}" must be set`;
throw new Error(`environment variable "${env}" must be set`);
}
return String(process.env[env]);
}

View File

@ -12,7 +12,9 @@ if (process.env.PYTH_PRICE_SERVICE_CONFIG) {
configFile = process.env.PYTH_PRICE_SERVICE_CONFIG;
}
// tslint:disable:no-console
console.log("Loading config file [%s]", configFile);
// tslint:disable:no-var-requires
require("dotenv").config({ path: configFile });
setDefaultWasm("node");
@ -23,7 +25,7 @@ initLogger({ logLevel: process.env.LOG_LEVEL });
async function run() {
const promClient = new PromClient({
name: "price_service",
port: parseInt(envOrErr("PROM_PORT")),
port: parseInt(envOrErr("PROM_PORT"), 10),
});
const listener = new Listener(
@ -32,9 +34,13 @@ async function run() {
filtersRaw: process.env.SPY_SERVICE_FILTERS,
readiness: {
spySyncTimeSeconds: parseInt(
envOrErr("READINESS_SPY_SYNC_TIME_SECONDS")
envOrErr("READINESS_SPY_SYNC_TIME_SECONDS"),
10
),
numLoadedSymbols: parseInt(
envOrErr("READINESS_NUM_LOADED_SYMBOLS"),
10
),
numLoadedSymbols: parseInt(envOrErr("READINESS_NUM_LOADED_SYMBOLS")),
},
},
promClient
@ -45,7 +51,7 @@ async function run() {
const restAPI = new RestAPI(
{
port: parseInt(envOrErr("REST_PORT")),
port: parseInt(envOrErr("REST_PORT"), 10),
},
listener,
isReady,

View File

@ -1,12 +1,12 @@
import {
ChainId,
hexToUint8Array,
uint8ArrayToHex
uint8ArrayToHex,
} from "@certusone/wormhole-sdk";
import {
createSpyRPCServiceClient,
subscribeSignedVAA
subscribeSignedVAA,
} from "@certusone/wormhole-spydk";
import { importCoreWasm } from "@certusone/wormhole-sdk/lib/cjs/solana/wasm";
@ -14,11 +14,11 @@ import { importCoreWasm } from "@certusone/wormhole-sdk/lib/cjs/solana/wasm";
import {
getBatchSummary,
parseBatchPriceAttestation,
priceAttestationToPriceFeed
} from "@certusone/p2w-sdk";
priceAttestationToPriceFeed,
} from "@pythnetwork/p2w-sdk-js";
import {
FilterEntry,
SubscribeSignedVAAResponse
SubscribeSignedVAAResponse,
} from "@certusone/wormhole-spydk/lib/cjs/proto/spy/v1/spy";
import { ClientReadableStream } from "@grpc/grpc-js";
import { HexString, PriceFeed } from "@pythnetwork/pyth-sdk-js";
@ -75,12 +75,12 @@ export class Listener implements PriceStore {
return;
}
const parsedJsonFilters = eval(filtersRaw);
const parsedJsonFilters = JSON.parse(filtersRaw);
for (let i = 0; i < parsedJsonFilters.length; i++) {
let myChainId = parseInt(parsedJsonFilters[i].chain_id) as ChainId;
let myEmitterAddress = parsedJsonFilters[i].emitter_address;
let myEmitterFilter: FilterEntry = {
for (const filter of parsedJsonFilters) {
const myChainId = parseInt(filter.chain_id, 10) as ChainId;
const myEmitterAddress = filter.emitter_address;
const myEmitterFilter: FilterEntry = {
emitterFilter: {
chainId: myChainId,
emitterAddress: myEmitterAddress,
@ -165,10 +165,10 @@ export class Listener implements PriceStore {
return;
}
let isAnyPriceNew = batchAttestation.priceAttestations.some(
const isAnyPriceNew = batchAttestation.priceAttestations.some(
(priceAttestation) => {
const key = priceAttestation.priceId;
let lastAttestationTime =
const lastAttestationTime =
this.priceFeedVaaMap.get(key)?.attestationTime;
return (
lastAttestationTime === undefined ||
@ -181,10 +181,11 @@ export class Listener implements PriceStore {
return;
}
for (let priceAttestation of batchAttestation.priceAttestations) {
for (const priceAttestation of batchAttestation.priceAttestations) {
const key = priceAttestation.priceId;
let lastAttestationTime = this.priceFeedVaaMap.get(key)?.attestationTime;
const lastAttestationTime =
this.priceFeedVaaMap.get(key)?.attestationTime;
if (
lastAttestationTime === undefined ||
@ -193,14 +194,14 @@ export class Listener implements PriceStore {
const priceFeed = priceAttestationToPriceFeed(priceAttestation);
const priceInfo = {
seqNum: parsedVAA.sequence,
vaaBytes: vaaBytes,
vaaBytes,
attestationTime: priceAttestation.attestationTime,
priceFeed,
emitterChainId: parsedVAA.emitter_chain,
};
this.priceFeedVaaMap.set(key, priceInfo);
for (let callback of this.updateCallbacks) {
for (const callback of this.updateCallbacks) {
callback(priceInfo);
}
}
@ -233,7 +234,7 @@ export class Listener implements PriceStore {
}
isReady(): boolean {
let currentTime: TimestampInSec = Math.floor(Date.now() / 1000);
const currentTime: TimestampInSec = Math.floor(Date.now() / 1000);
if (
this.spyConnectionTime === undefined ||
currentTime <

View File

@ -12,6 +12,7 @@ export function initLogger(config?: { logLevel?: string }) {
}
let transport: any;
// tslint:disable:no-console
console.log("p2w_api is logging to the console at level [%s]", logLevel);
transport = new winston.transports.Console({

View File

@ -73,8 +73,8 @@ export class PromClient {
addResponseTime(path: string, status: number, duration: DurationInMs) {
this.apiResponseTimeSummary.observe(
{
path: path,
status: status,
path,
status,
},
duration
);
@ -87,7 +87,7 @@ export class PromClient {
) {
this.apiRequestsPriceFreshnessHistogram.observe(
{
path: path,
path,
price_id: priceId,
},
duration
@ -96,8 +96,8 @@ export class PromClient {
addWebSocketInteraction(type: string, status: "ok" | "err") {
this.webSocketInteractionCounter.inc({
type: type,
status: status,
type,
status,
});
}
}

View File

@ -71,7 +71,7 @@ export class RestAPI {
})
);
let endpoints: string[] = [];
const endpoints: string[] = [];
const latestVaasInputSchema: schema = {
query: Joi.object({
@ -84,20 +84,20 @@ export class RestAPI {
"/api/latest_vaas",
validate(latestVaasInputSchema),
(req: Request, res: Response) => {
let priceIds = req.query.ids as string[];
const priceIds = req.query.ids as string[];
// Multiple price ids might share same vaa, we use sequence number as
// key of a vaa and deduplicate using a map of seqnum to vaa bytes.
let vaaMap = new Map<number, string>();
const vaaMap = new Map<number, string>();
let notFoundIds: string[] = [];
const notFoundIds: string[] = [];
for (let id of priceIds) {
if (id.startsWith("0x")) {
id = id.substring(2);
}
let latestPriceInfo = this.priceFeedVaaInfo.getLatestPriceInfo(id);
const latestPriceInfo = this.priceFeedVaaInfo.getLatestPriceInfo(id);
if (latestPriceInfo === undefined) {
notFoundIds.push(id);
@ -105,7 +105,8 @@ export class RestAPI {
}
const freshness: DurationInSec =
new Date().getTime() / 1000 - latestPriceInfo.priceFeed.publishTime;
new Date().getTime() / 1000 -
latestPriceInfo.priceFeed.getPriceUnchecked().publishTime;
this.promClient?.addApiRequestsPriceFreshness(
req.path,
id,
@ -142,20 +143,20 @@ export class RestAPI {
"/api/latest_price_feeds",
validate(latestPriceFeedsInputSchema),
(req: Request, res: Response) => {
let priceIds = req.query.ids as string[];
const priceIds = req.query.ids as string[];
// verbose is optional, default to false
let verbose = req.query.verbose === "true";
const verbose = req.query.verbose === "true";
let responseJson = [];
const responseJson = [];
let notFoundIds: string[] = [];
const notFoundIds: string[] = [];
for (let id of priceIds) {
if (id.startsWith("0x")) {
id = id.substring(2);
}
let latestPriceInfo = this.priceFeedVaaInfo.getLatestPriceInfo(id);
const latestPriceInfo = this.priceFeedVaaInfo.getLatestPriceInfo(id);
if (latestPriceInfo === undefined) {
notFoundIds.push(id);
@ -163,7 +164,8 @@ export class RestAPI {
}
const freshness: DurationInSec =
new Date().getTime() / 1000 - latestPriceInfo.priceFeed.publishTime;
new Date().getTime() / 1000 -
latestPriceInfo.priceFeed.getEmaPriceUnchecked().publishTime;
this.promClient?.addApiRequestsPriceFreshness(
req.path,
id,
@ -209,29 +211,30 @@ export class RestAPI {
threshold: Joi.number().required(),
}).required(),
};
app.get("/api/stale_feeds",
app.get(
"/api/stale_feeds",
validate(staleFeedsInputSchema),
(req: Request, res: Response) => {
let stalenessThresholdSeconds = Number(req.query.threshold as string);
const stalenessThresholdSeconds = Number(req.query.threshold as string);
let currentTime: TimestampInSec = Math.floor(Date.now() / 1000);
const currentTime: TimestampInSec = Math.floor(Date.now() / 1000);
let priceIds = [...this.priceFeedVaaInfo.getPriceIds()];
let stalePrices: Record<HexString, number> = {}
const priceIds = [...this.priceFeedVaaInfo.getPriceIds()];
const stalePrices: Record<HexString, number> = {};
for (let priceId of priceIds) {
const latency = currentTime - this.priceFeedVaaInfo.getLatestPriceInfo(priceId)!.attestationTime
for (const priceId of priceIds) {
const latency =
currentTime -
this.priceFeedVaaInfo.getLatestPriceInfo(priceId)!.attestationTime;
if (latency > stalenessThresholdSeconds) {
stalePrices[priceId] = latency
stalePrices[priceId] = latency;
}
}
res.json(stalePrices);
}
);
endpoints.push(
"/api/stale_feeds?threshold=<staleness_threshold_seconds>"
);
endpoints.push("/api/stale_feeds?threshold=<staleness_threshold_seconds>");
app.get("/ready", (_, res: Response) => {
if (this.isReady!()) {
@ -252,7 +255,7 @@ export class RestAPI {
app.get("/", (_, res: Response) => res.json(endpoints));
app.use(function (err: any, _: Request, res: Response, next: NextFunction) {
app.use((err: any, _: Request, res: Response, next: NextFunction) => {
if (err instanceof ValidationError) {
return res.status(err.statusCode).json(err);
}
@ -268,7 +271,7 @@ export class RestAPI {
}
async run(): Promise<Server> {
let app = await this.createApp();
const app = await this.createApp();
return app.listen(this.port, () =>
logger.debug("listening on REST port " + this.port)
);

View File

@ -82,21 +82,25 @@ export class WebSocketAPI {
return;
}
const clients: Set<WebSocket> = this.priceFeedClients.get(priceInfo.priceFeed.id)!;
const clients: Set<WebSocket> = this.priceFeedClients.get(
priceInfo.priceFeed.id
)!;
logger.info(
`Sending ${priceInfo.priceFeed.id} price update to ${
clients.size
} clients: ${Array.from(clients.values()).map((ws, _idx, _arr) => this.wsId.get(ws))}`
} clients: ${Array.from(clients.values()).map((ws, _idx, _arr) =>
this.wsId.get(ws)
)}`
);
for (let client of clients.values()) {
for (const client of clients.values()) {
this.promClient?.addWebSocketInteraction("server_update", "ok");
let verbose = this.priceFeedClientsVerbosity
const verbose = this.priceFeedClientsVerbosity
.get(priceInfo.priceFeed.id)!
.get(client);
let priceUpdate: ServerPriceUpdate = verbose
const priceUpdate: ServerPriceUpdate = verbose
? {
type: "price_update",
price_feed: {
@ -118,7 +122,7 @@ export class WebSocketAPI {
}
clientClose(ws: WebSocket) {
for (let clients of this.priceFeedClients.values()) {
for (const clients of this.priceFeedClients.values()) {
if (clients.has(ws)) {
clients.delete(ws);
}
@ -130,13 +134,13 @@ export class WebSocketAPI {
handleMessage(ws: WebSocket, data: RawData) {
try {
let jsonData = JSON.parse(data.toString());
let validationResult = ClientMessageSchema.validate(jsonData);
const jsonData = JSON.parse(data.toString());
const validationResult = ClientMessageSchema.validate(jsonData);
if (validationResult.error !== undefined) {
throw validationResult.error;
}
let message = jsonData as ClientMessage;
const message = jsonData as ClientMessage;
message.ids = message.ids.map((id) => {
if (id.startsWith("0x")) {
@ -146,7 +150,7 @@ export class WebSocketAPI {
});
const availableIds = this.priceFeedVaaInfo.getPriceIds();
let notFoundIds = message.ids.filter((id) => !availableIds.has(id));
const notFoundIds = message.ids.filter((id) => !availableIds.has(id));
if (notFoundIds.length > 0) {
throw new Error(
@ -154,7 +158,7 @@ export class WebSocketAPI {
);
}
if (message.type == "subscribe") {
if (message.type === "subscribe") {
message.ids.forEach((id) =>
this.addPriceFeedClient(ws, id, message.verbose === true)
);
@ -162,7 +166,7 @@ export class WebSocketAPI {
message.ids.forEach((id) => this.delPriceFeedClient(ws, id));
}
} catch (e: any) {
let response: ServerResponse = {
const errorResponse: ServerResponse = {
type: "response",
status: "error",
error: e.message,
@ -173,7 +177,7 @@ export class WebSocketAPI {
);
this.promClient?.addWebSocketInteraction("client_message", "err");
ws.send(JSON.stringify(response));
ws.send(JSON.stringify(errorResponse));
return;
}
@ -182,7 +186,7 @@ export class WebSocketAPI {
);
this.promClient?.addWebSocketInteraction("client_message", "ok");
let response: ServerResponse = {
const response: ServerResponse = {
type: "response",
status: "success",
};

View File

@ -0,0 +1,9 @@
{
"extends": ["tslint:recommended", "tslint-config-prettier"],
"rules": {
"max-classes-per-file": {
"severity": "off"
}
}
}