diff --git a/Tiltfile b/Tiltfile index d5b3ad115c..c46f31350b 100644 --- a/Tiltfile +++ b/Tiltfile @@ -338,3 +338,16 @@ k8s_resource( labels = ["solana"], trigger_mode = trigger_mode, ) + +# Pyth Price Client JS e2e test +docker_build( + ref = "pyth-price-client-js", + context = ".", + dockerfile = "price_service/client/js/Dockerfile", +) +k8s_yaml_with_ns("tilt_devnet/k8s/pyth-price-client-js.yaml") +k8s_resource( + "pyth-price-client-js", + resource_deps = ["pyth-price-server"], + labels = ["pyth"] +) diff --git a/price_service/client/js/Dockerfile b/price_service/client/js/Dockerfile new file mode 100644 index 0000000000..60eed617a9 --- /dev/null +++ b/price_service/client/js/Dockerfile @@ -0,0 +1,17 @@ +# Defined in tilt_devnet/docker_images/Dockerfile.lerna +FROM lerna + +USER root +RUN apt-get update && apt-get install -y ncat + +WORKDIR /home/node/ +USER 1000 + +COPY --chown=1000:1000 price_service/client/js price_service/client/js +COPY --chown=1000:1000 price_service/sdk/js price_service/sdk/js + +RUN npx lerna run build --scope="@pythnetwork/price-service-client" --include-dependencies + +WORKDIR /home/node/price_service/client/js + +ENTRYPOINT ["npm"] diff --git a/price_service/client/js/package.json b/price_service/client/js/package.json index a552dbfc53..8e1eae5c71 100644 --- a/price_service/client/js/package.json +++ b/price_service/client/js/package.json @@ -13,7 +13,8 @@ ], "repository": "https://github.com/pyth-network/pyth-crosschain", "scripts": { - "test": "jest --passWithNoTests", + "test": "jest --testPathIgnorePatterns=.*.e2e.test.ts --passWithNoTests", + "test:e2e": "jest --testPathPattern=.*.e2e.test.ts", "build": "tsc", "example": "npm run build && node lib/examples/PriceServiceClient.js", "format": "prettier --write \"src/**/*.ts\"", diff --git a/price_service/client/js/src/__tests__/connection.e2e.test.ts b/price_service/client/js/src/__tests__/connection.e2e.test.ts new file mode 100644 index 0000000000..2f8783fd93 --- /dev/null +++ b/price_service/client/js/src/__tests__/connection.e2e.test.ts @@ -0,0 +1,196 @@ +import { + DurationInMs, + Price, + PriceFeed, + PriceFeedMetadata, + PriceServiceConnection, +} from "../index"; + +async function sleep(duration: DurationInMs): Promise { + return new Promise((res) => setTimeout(res, duration)); +} + +// The endpoint is set to the price service endpoint in Tilt. +// Please note that if you change it to a mainnet/testnet endpoint +// some tests might fail due to the huge response size of a request +// , i.e. requesting latest price feeds or vaas of all price ids. +const PRICE_SERVICE_ENDPOINT = "http://pyth-price-server:4200"; + +describe("Test http endpoints", () => { + test("Get price feed (without verbose/binary) works", async () => { + const connection = new PriceServiceConnection(PRICE_SERVICE_ENDPOINT); + const ids = await connection.getPriceFeedIds(); + expect(ids.length).toBeGreaterThan(0); + + const priceFeeds = await connection.getLatestPriceFeeds(ids); + expect(priceFeeds).toBeDefined(); + expect(priceFeeds!.length).toEqual(ids.length); + + for (const priceFeed of priceFeeds!) { + expect(priceFeed.id.length).toBe(64); // 32 byte address has size 64 in hex + expect(priceFeed).toBeInstanceOf(PriceFeed); + expect(priceFeed.getPriceUnchecked()).toBeInstanceOf(Price); + expect(priceFeed.getEmaPriceUnchecked()).toBeInstanceOf(Price); + expect(priceFeed.getMetadata()).toBeUndefined(); + expect(priceFeed.getVAA()).toBeUndefined(); + } + }); + + test("Get price feed with verbose flag works", async () => { + const connection = new PriceServiceConnection(PRICE_SERVICE_ENDPOINT, { + priceFeedRequestConfig: { verbose: true }, + }); + + const ids = await connection.getPriceFeedIds(); + expect(ids.length).toBeGreaterThan(0); + + const priceFeeds = await connection.getLatestPriceFeeds(ids); + expect(priceFeeds).toBeDefined(); + expect(priceFeeds!.length).toEqual(ids.length); + + for (const priceFeed of priceFeeds!) { + expect(priceFeed.getMetadata()).toBeInstanceOf(PriceFeedMetadata); + expect(priceFeed.getVAA()).toBeUndefined(); + } + }); + + test("Get price feed with binary flag works", async () => { + const connection = new PriceServiceConnection(PRICE_SERVICE_ENDPOINT, { + priceFeedRequestConfig: { binary: true }, + }); + + const ids = await connection.getPriceFeedIds(); + expect(ids.length).toBeGreaterThan(0); + + const priceFeeds = await connection.getLatestPriceFeeds(ids); + expect(priceFeeds).toBeDefined(); + expect(priceFeeds!.length).toEqual(ids.length); + + for (const priceFeed of priceFeeds!) { + expect(priceFeed.getMetadata()).toBeUndefined(); + expect(priceFeed.getVAA()?.length).toBeGreaterThan(0); + } + }); + + test("Get latest vaa works", async () => { + const connection = new PriceServiceConnection(PRICE_SERVICE_ENDPOINT, { + priceFeedRequestConfig: { binary: true }, + }); + + const ids = await connection.getPriceFeedIds(); + expect(ids.length).toBeGreaterThan(0); + + const vaas = await connection.getLatestVaas(ids); + expect(vaas.length).toBeGreaterThan(0); + + for (const vaa of vaas) { + expect(vaa.length).toBeGreaterThan(0); + } + }); + + test("Get vaa works", async () => { + const connection = new PriceServiceConnection(PRICE_SERVICE_ENDPOINT, { + priceFeedRequestConfig: { binary: true }, + }); + + const ids = await connection.getPriceFeedIds(); + expect(ids.length).toBeGreaterThan(0); + + const publishTime10SecAgo = Math.floor(new Date().getTime() / 1000) - 10; + const [vaa, vaaPublishTime] = await connection.getVaa( + ids[0], + publishTime10SecAgo + ); + + expect(vaa.length).toBeGreaterThan(0); + expect(vaaPublishTime).toBeGreaterThanOrEqual(publishTime10SecAgo); + }); +}); + +describe("Test websocket endpoints", () => { + jest.setTimeout(60 * 1000); + + test.concurrent( + "websocket subscription works without verbose and binary", + async () => { + const connection = new PriceServiceConnection(PRICE_SERVICE_ENDPOINT); + + const ids = await connection.getPriceFeedIds(); + expect(ids.length).toBeGreaterThan(0); + + const counter: Map = new Map(); + let totalCounter = 0; + + await connection.subscribePriceFeedUpdates(ids, (priceFeed) => { + expect(priceFeed.id.length).toBe(64); // 32 byte address has size 64 in hex + expect(priceFeed.getMetadata()).toBeUndefined(); + expect(priceFeed.getVAA()).toBeUndefined(); + + counter.set(priceFeed.id, (counter.get(priceFeed.id) ?? 0) + 1); + totalCounter += 1; + }); + + // Wait for 30 seconds + await sleep(30000); + connection.closeWebSocket(); + + expect(totalCounter).toBeGreaterThan(30); + + for (const id of ids) { + expect(counter.get(id)).toBeDefined(); + // Make sure it receives more than 1 update + expect(counter.get(id)).toBeGreaterThan(1); + } + } + ); + + test.concurrent("websocket subscription works with verbose", async () => { + const connection = new PriceServiceConnection(PRICE_SERVICE_ENDPOINT, { + priceFeedRequestConfig: { verbose: true }, + }); + + const ids = await connection.getPriceFeedIds(); + expect(ids.length).toBeGreaterThan(0); + + const observedFeeds: Set = new Set(); + + await connection.subscribePriceFeedUpdates(ids, (priceFeed) => { + expect(priceFeed.getMetadata()).toBeInstanceOf(PriceFeedMetadata); + expect(priceFeed.getVAA()).toBeUndefined(); + observedFeeds.add(priceFeed.id); + }); + + // Wait for 20 seconds + await sleep(20000); + await connection.unsubscribePriceFeedUpdates(ids); + + for (const id of ids) { + expect(observedFeeds.has(id)).toBe(true); + } + }); + + test.concurrent("websocket subscription works with binary", async () => { + const connection = new PriceServiceConnection(PRICE_SERVICE_ENDPOINT, { + priceFeedRequestConfig: { binary: true }, + }); + + const ids = await connection.getPriceFeedIds(); + expect(ids.length).toBeGreaterThan(0); + + const observedFeeds: Set = new Set(); + + await connection.subscribePriceFeedUpdates(ids, (priceFeed) => { + expect(priceFeed.getMetadata()).toBeUndefined(); + expect(priceFeed.getVAA()?.length).toBeGreaterThan(0); + observedFeeds.add(priceFeed.id); + }); + + // Wait for 20 seconds + await sleep(20000); + connection.closeWebSocket(); + + for (const id of ids) { + expect(observedFeeds.has(id)).toBe(true); + } + }); +}); diff --git a/price_service/client/js/src/index.ts b/price_service/client/js/src/index.ts index 48e0aec32a..fdb29bf309 100644 --- a/price_service/client/js/src/index.ts +++ b/price_service/client/js/src/index.ts @@ -6,6 +6,7 @@ export { export { HexString, + PriceFeedMetadata, PriceFeed, Price, UnixTimestamp, diff --git a/third_party/pyth/p2w_autoattest.py b/third_party/pyth/p2w_autoattest.py index da89c0429d..b18e7def87 100755 --- a/third_party/pyth/p2w_autoattest.py +++ b/third_party/pyth/p2w_autoattest.py @@ -114,7 +114,7 @@ min_rpc_interval_ms: 0 # RIP RPC max_batch_jobs: 1000 # Where we're going there's no oomkiller default_attestation_conditions: - min_interval_secs: 60 + min_interval_secs: 10 symbol_groups: - group_name: fast_interval_only conditions: diff --git a/tilt_devnet/k8s/pyth-price-client-js.yaml b/tilt_devnet/k8s/pyth-price-client-js.yaml new file mode 100644 index 0000000000..2b014a4098 --- /dev/null +++ b/tilt_devnet/k8s/pyth-price-client-js.yaml @@ -0,0 +1,32 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: pyth-price-client-js +spec: + selector: + matchLabels: + app: pyth-price-client-js + serviceName: pyth-price-client-js + replicas: 1 + template: + metadata: + labels: + app: pyth-price-client-js + spec: + terminationGracePeriodSeconds: 0 + containers: + - name: tests + image: pyth-price-client-js + command: + - /bin/sh + - -c + - "npm run test:e2e && nc -lk 0.0.0.0 2000" + readinessProbe: + periodSeconds: 5 + failureThreshold: 300 + tcpSocket: + port: 2000 + resources: + limits: + cpu: "2" + memory: 1Gi