Skip to content

Commit 42ddfb6

Browse files
jayantkJayant Krishnamurthy
andauthored
Get price feed endpoint (#764)
* Add get price feed endpoint * fix stuff * lint --------- Co-authored-by: Jayant Krishnamurthy <[email protected]>
1 parent 85b00ff commit 42ddfb6

File tree

3 files changed

+145
-27
lines changed

3 files changed

+145
-27
lines changed

price_service/server/src/listen.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
getBatchSummary,
1515
parseBatchPriceAttestation,
1616
priceAttestationToPriceFeed,
17+
PriceAttestation,
1718
} from "@pythnetwork/wormhole-attester-sdk";
1819
import { HexString, PriceFeed } from "@pythnetwork/price-service-sdk";
1920
import LRUCache from "lru-cache";
@@ -31,6 +32,24 @@ export type PriceInfo = {
3132
priceServiceReceiveTime: number;
3233
};
3334

35+
export function createPriceInfo(
36+
priceAttestation: PriceAttestation,
37+
vaa: Buffer,
38+
sequence: bigint,
39+
emitterChain: number
40+
): PriceInfo {
41+
const priceFeed = priceAttestationToPriceFeed(priceAttestation);
42+
return {
43+
seqNum: Number(sequence),
44+
vaa,
45+
publishTime: priceAttestation.publishTime,
46+
attestationTime: priceAttestation.attestationTime,
47+
priceFeed,
48+
emitterChainId: emitterChain,
49+
priceServiceReceiveTime: Math.floor(new Date().getTime() / 1000),
50+
};
51+
}
52+
3453
export interface PriceStore {
3554
getPriceIds(): Set<HexString>;
3655
getLatestPriceInfo(priceFeedId: HexString): PriceInfo | undefined;
@@ -324,17 +343,12 @@ export class Listener implements PriceStore {
324343
for (const priceAttestation of batchAttestation.priceAttestations) {
325344
const key = priceAttestation.priceId;
326345

327-
const priceFeed = priceAttestationToPriceFeed(priceAttestation);
328-
const priceInfo = {
329-
seqNum: Number(parsedVaa.sequence),
346+
const priceInfo = createPriceInfo(
347+
priceAttestation,
330348
vaa,
331-
publishTime: priceAttestation.publishTime,
332-
attestationTime: priceAttestation.attestationTime,
333-
priceFeed,
334-
emitterChainId: parsedVaa.emitterChain,
335-
priceServiceReceiveTime: Math.floor(new Date().getTime() / 1000),
336-
};
337-
349+
parsedVaa.sequence,
350+
parsedVaa.emitterChain
351+
);
338352
const cachedPriceInfo = this.priceFeedVaaMap.get(key);
339353

340354
if (this.isNewPriceInfo(cachedPriceInfo, priceInfo)) {

price_service/server/src/rest.ts

Lines changed: 119 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@ import { Server } from "http";
66
import { StatusCodes } from "http-status-codes";
77
import morgan from "morgan";
88
import fetch from "node-fetch";
9+
import {
10+
parseBatchPriceAttestation,
11+
priceAttestationToPriceFeed,
12+
} from "@pythnetwork/wormhole-attester-sdk";
913
import { removeLeading0x, TimestampInSec } from "./helpers";
10-
import { PriceStore, VaaConfig } from "./listen";
14+
import { createPriceInfo, PriceInfo, PriceStore, VaaConfig } from "./listen";
1115
import { logger } from "./logging";
1216
import { PromClient } from "./promClient";
1317
import { retry } from "ts-retry-promise";
18+
import { parseVaa } from "@certusone/wormhole-sdk";
1419

1520
const MORGAN_LOG_FORMAT =
1621
':remote-addr - :remote-user ":method :url HTTP/:http-version"' +
@@ -71,7 +76,10 @@ export class RestAPI {
7176
this.promClient = promClient;
7277
}
7378

74-
async getVaaWithDbLookup(priceFeedId: string, publishTime: TimestampInSec) {
79+
async getVaaWithDbLookup(
80+
priceFeedId: string,
81+
publishTime: TimestampInSec
82+
): Promise<VaaConfig | undefined> {
7583
// Try to fetch the vaa from the local cache
7684
let vaa = this.priceFeedVaaInfo.getVaa(priceFeedId, publishTime);
7785

@@ -104,6 +112,56 @@ export class RestAPI {
104112
return vaa;
105113
}
106114

115+
vaaToPriceInfo(priceFeedId: string, vaa: Buffer): PriceInfo | undefined {
116+
const parsedVaa = parseVaa(vaa);
117+
118+
let batchAttestation;
119+
120+
try {
121+
batchAttestation = parseBatchPriceAttestation(
122+
Buffer.from(parsedVaa.payload)
123+
);
124+
} catch (e: any) {
125+
logger.error(e, e.stack);
126+
logger.error("Parsing historical VAA failed: %o", parsedVaa);
127+
return undefined;
128+
}
129+
130+
for (const priceAttestation of batchAttestation.priceAttestations) {
131+
if (priceAttestation.priceId === priceFeedId) {
132+
return createPriceInfo(
133+
priceAttestation,
134+
vaa,
135+
parsedVaa.sequence,
136+
parsedVaa.emitterChain
137+
);
138+
}
139+
}
140+
141+
return undefined;
142+
}
143+
144+
priceInfoToJson(
145+
priceInfo: PriceInfo,
146+
verbose: boolean,
147+
binary: boolean
148+
): object {
149+
return {
150+
...priceInfo.priceFeed.toJson(),
151+
...(verbose && {
152+
metadata: {
153+
emitter_chain: priceInfo.emitterChainId,
154+
attestation_time: priceInfo.attestationTime,
155+
sequence_number: priceInfo.seqNum,
156+
price_service_receive_time: priceInfo.priceServiceReceiveTime,
157+
},
158+
}),
159+
...(binary && {
160+
vaa: priceInfo.vaa.toString("base64"),
161+
}),
162+
};
163+
}
164+
107165
// Run this function without blocking (`await`) if you want to run it async.
108166
async createApp() {
109167
const app = express();
@@ -283,21 +341,9 @@ export class RestAPI {
283341
continue;
284342
}
285343

286-
responseJson.push({
287-
...latestPriceInfo.priceFeed.toJson(),
288-
...(verbose && {
289-
metadata: {
290-
emitter_chain: latestPriceInfo.emitterChainId,
291-
attestation_time: latestPriceInfo.attestationTime,
292-
sequence_number: latestPriceInfo.seqNum,
293-
price_service_receive_time:
294-
latestPriceInfo.priceServiceReceiveTime,
295-
},
296-
}),
297-
...(binary && {
298-
vaa: latestPriceInfo.vaa.toString("base64"),
299-
}),
300-
});
344+
responseJson.push(
345+
this.priceInfoToJson(latestPriceInfo, verbose, binary)
346+
);
301347
}
302348

303349
if (notFoundIds.length > 0) {
@@ -317,6 +363,62 @@ export class RestAPI {
317363
"api/latest_price_feeds?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&..&verbose=true&binary=true"
318364
);
319365

366+
const getPriceFeedInputSchema: schema = {
367+
query: Joi.object({
368+
id: Joi.string()
369+
.regex(/^(0x)?[a-f0-9]{64}$/)
370+
.required(),
371+
publish_time: Joi.number().required(),
372+
verbose: Joi.boolean(),
373+
binary: Joi.boolean(),
374+
}).required(),
375+
};
376+
377+
app.get(
378+
"/api/get_price_feed",
379+
validate(getPriceFeedInputSchema),
380+
asyncWrapper(async (req: Request, res: Response) => {
381+
const priceFeedId = removeLeading0x(req.query.id as string);
382+
const publishTime = Number(req.query.publish_time as string);
383+
// verbose is optional, default to false
384+
const verbose = req.query.verbose === "true";
385+
// binary is optional, default to false
386+
const binary = req.query.binary === "true";
387+
388+
if (
389+
this.priceFeedVaaInfo.getLatestPriceInfo(priceFeedId) === undefined
390+
) {
391+
throw RestException.PriceFeedIdNotFound([priceFeedId]);
392+
}
393+
394+
const vaa = await this.getVaaWithDbLookup(priceFeedId, publishTime);
395+
if (vaa === undefined) {
396+
throw RestException.VaaNotFound();
397+
}
398+
399+
const priceInfo = this.vaaToPriceInfo(
400+
priceFeedId,
401+
Buffer.from(vaa.vaa, "base64")
402+
);
403+
404+
if (priceInfo === undefined) {
405+
throw RestException.VaaNotFound();
406+
} else {
407+
res.json(this.priceInfoToJson(priceInfo, verbose, binary));
408+
}
409+
})
410+
);
411+
412+
endpoints.push(
413+
"api/get_price_feed?id=<price_feed_id>&publish_time=<publish_time_in_unix_timestamp>"
414+
);
415+
endpoints.push(
416+
"api/get_price_feed?id=<price_feed_id>&publish_time=<publish_time_in_unix_timestamp>&verbose=true"
417+
);
418+
endpoints.push(
419+
"api/get_price_feed?id=<price_feed_id>&publish_time=<publish_time_in_unix_timestamp>&binary=true"
420+
);
421+
320422
app.get("/api/price_feed_ids", (req: Request, res: Response) => {
321423
const availableIds = this.priceFeedVaaInfo.getPriceIds();
322424
res.json([...availableIds]);

tilt_devnet/docker_images/Dockerfile.lerna

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ COPY ./tsconfig.base.json ./
1818

1919
FROM node:18.13.0@sha256:d9061fd0205c20cd47f70bdc879a7a84fb472b822d3ad3158aeef40698d2ce36 as lerna
2020

21+
RUN apt-get update && apt-get install -y libusb-dev
22+
2123
# 1000 is the uid and gid of the node user
2224
USER 1000
2325
RUN mkdir -p /home/node/.npm

0 commit comments

Comments
 (0)