Skip to content

Commit 1020125

Browse files
authored
Merge pull request #424 from Gearbox-protocol/updatable-feeds-prices
feat: add prices to updatable price fetchers
2 parents ef5071e + 19481b1 commit 1020125

File tree

5 files changed

+120
-12
lines changed

5 files changed

+120
-12
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"@redstone-finance/evm-connector": "^0.9.0",
5656
"@redstone-finance/protocol": "^0.9.0",
5757
"@redstone-finance/sdk": "^0.9.0",
58+
"@redstone-finance/utils": "^0.9.0",
5859
"@types/bn.js": "^5.2.0",
5960
"abitype": "^1.2.3",
6061
"bn.js": "^5.2.2",

src/sdk/market/pricefeeds/updates/fetchPythPayloads.ts

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import {
77
parsePriceFeedMessage,
88
sliceAccumulatorUpdateData,
99
} from "./PythAccumulatorUpdateData.js";
10-
import type { TimestampedCalldata } from "./types.js";
10+
import type {
11+
TimestampedCalldata,
12+
TimestampedCalldataWithPrice,
13+
} from "./types.js";
1114

1215
export interface FetchPythPayloadsOptions {
1316
/**
@@ -30,22 +33,48 @@ export interface FetchPythPayloadsOptions {
3033
* Logger to use
3134
*/
3235
logger?: ILogger;
36+
/**
37+
* Custom fetch function to use instead of global fetch.
38+
* Can be used to add custom headers, authentication, or use a different HTTP client.
39+
*/
40+
customFetch?: typeof fetch;
41+
42+
/**
43+
* When true, returns the price data for each feed.
44+
*/
45+
returnPrices?: boolean;
3346
}
3447

48+
/**
49+
* Fetches pyth payloads from Hermes API with price data
50+
*/
51+
export async function fetchPythPayloads(
52+
options: FetchPythPayloadsOptions & { returnPrices: true },
53+
): Promise<TimestampedCalldataWithPrice[]>;
54+
55+
/**
56+
* Fetches pyth payloads from Hermes API
57+
*/
58+
export async function fetchPythPayloads(
59+
options: FetchPythPayloadsOptions & { returnPrices?: false },
60+
): Promise<TimestampedCalldata[]>;
61+
3562
/**
3663
* Fetches pyth payloads from Hermes API
3764
* @param dataFeedsIds
3865
* @returns
3966
*/
4067
export async function fetchPythPayloads(
4168
options: FetchPythPayloadsOptions,
42-
): Promise<TimestampedCalldata[]> {
69+
): Promise<TimestampedCalldata[] | TimestampedCalldataWithPrice[]> {
4370
const {
4471
dataFeedsIds,
4572
ignoreMissingFeeds,
4673
historicalTimestampSeconds,
4774
logger,
4875
apiProxy,
76+
customFetch = fetch,
77+
returnPrices,
4978
} = options;
5079
const ids = Array.from(new Set(dataFeedsIds));
5180
if (ids.length === 0) {
@@ -65,7 +94,7 @@ export async function fetchPythPayloads(
6594
}
6695
const resp = await retry(
6796
async () => {
68-
const resp = await fetch(url.toString());
97+
const resp = await customFetch(url.toString());
6998
if (!resp.ok) {
7099
const body = await resp.text();
71100
throw new Error(
@@ -77,7 +106,7 @@ export async function fetchPythPayloads(
77106
},
78107
{ attempts: 3, exponent: 2, interval: 200 },
79108
);
80-
const result = respToCalldata(resp);
109+
const result = respToCalldata(resp, returnPrices);
81110

82111
if (result.length !== ids.length) {
83112
if (ignoreMissingFeeds) {
@@ -126,15 +155,31 @@ interface PythPriceFeedUpdate {
126155
data: Hex;
127156
}
128157

129-
function respToCalldata(resp: PythPriceUpdatesResp): TimestampedCalldata[] {
158+
function respToCalldata(
159+
resp: PythPriceUpdatesResp,
160+
returnPrices?: boolean,
161+
): TimestampedCalldata[] | TimestampedCalldataWithPrice[] {
130162
// edge case when ignoreMissingFeeds is true and we requesting exactly one feed which fails
131163
if (resp.binary.data.length === 0) {
132164
return [];
133165
}
134166

135167
const updates = splitAccumulatorUpdates(resp.binary.data[0]);
168+
169+
// Build a map of feedId -> price info for quick lookup
170+
const priceMap = new Map<string, { price: bigint; decimals: number }>();
171+
if (returnPrices && resp.parsed) {
172+
for (const feed of resp.parsed) {
173+
const feedId = feed.id.startsWith("0x") ? feed.id : `0x${feed.id}`;
174+
priceMap.set(feedId.toLowerCase(), {
175+
price: BigInt(feed.price.price),
176+
decimals: Math.abs(feed.price.expo),
177+
});
178+
}
179+
}
180+
136181
return updates.map(({ data, dataFeedId, timestamp }) => {
137-
return {
182+
const base: TimestampedCalldata = {
138183
dataFeedId,
139184
data: encodeAbiParameters(
140185
[{ type: "uint256" }, { type: "bytes[]" }],
@@ -143,6 +188,20 @@ function respToCalldata(resp: PythPriceUpdatesResp): TimestampedCalldata[] {
143188
timestamp,
144189
cached: false,
145190
};
191+
192+
if (returnPrices) {
193+
const priceInfo = priceMap.get(dataFeedId.toLowerCase());
194+
if (!priceInfo) {
195+
throw new Error(`Price info not found for feed ${dataFeedId}`);
196+
}
197+
return {
198+
...base,
199+
price: priceInfo.price,
200+
decimals: priceInfo.decimals,
201+
} as TimestampedCalldataWithPrice;
202+
}
203+
204+
return base;
146205
});
147206
}
148207

src/sdk/market/pricefeeds/updates/fetchRedstonePayloads.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ import {
77
type DataServiceIds,
88
getSignersForDataServiceId,
99
} from "@redstone-finance/sdk";
10+
import { MathUtils } from "@redstone-finance/utils";
1011
import { encodeAbiParameters, toBytes } from "viem";
1112
import type { ILogger } from "../../../types/logger.js";
1213
import { retry } from "../../../utils/index.js";
13-
import type { TimestampedCalldata } from "./types.js";
14+
import type {
15+
TimestampedCalldata,
16+
TimestampedCalldataWithPrice,
17+
} from "./types.js";
1418

1519
export interface FetchRedstonePayloadsOptions {
1620
/**
@@ -53,11 +57,32 @@ export interface FetchRedstonePayloadsOptions {
5357
* Logger to use
5458
*/
5559
logger?: ILogger;
60+
/**
61+
* When true, returns the price data for each feed.
62+
*/
63+
returnPrices?: boolean;
5664
}
5765

66+
/**
67+
* Fetches redstone payloads with price data
68+
*/
69+
export async function fetchRedstonePayloads(
70+
options: FetchRedstonePayloadsOptions & { returnPrices: true },
71+
): Promise<TimestampedCalldataWithPrice[]>;
72+
73+
/**
74+
* Fetches redstone payloads
75+
*/
76+
export async function fetchRedstonePayloads(
77+
options: FetchRedstonePayloadsOptions & { returnPrices?: false },
78+
): Promise<TimestampedCalldata[]>;
79+
80+
/**
81+
* Fetches redstone payloads from Redstone API
82+
*/
5883
export async function fetchRedstonePayloads(
5984
options: FetchRedstonePayloadsOptions,
60-
): Promise<TimestampedCalldata[]> {
85+
): Promise<TimestampedCalldata[] | TimestampedCalldataWithPrice[]> {
6186
const {
6287
dataServiceId,
6388
dataFeedsIds,
@@ -67,6 +92,7 @@ export async function fetchRedstonePayloads(
6792
ignoreMissingFeeds,
6893
enableLogging,
6994
logger,
95+
returnPrices,
7096
} = options;
7197
const metadataTimestampMs =
7298
historicalTimestampMs ?? options.metadataTimestampMs;
@@ -101,7 +127,7 @@ export async function fetchRedstonePayloads(
101127
const parsed = RedstonePayload.parse(toBytes(`0x${dataPayload}`));
102128
const packagesByDataFeedId = groupDataPackages(parsed.signedDataPackages);
103129

104-
const result: TimestampedCalldata[] = [];
130+
const result: (TimestampedCalldata | TimestampedCalldataWithPrice)[] = [];
105131

106132
for (const dataFeedId of dataFeedsIds) {
107133
const signedDataPackages = packagesByDataFeedId[dataFeedId];
@@ -128,6 +154,7 @@ export async function fetchRedstonePayloads(
128154
dataFeedId,
129155
signedDataPackages,
130156
wrapper.getUnsignedMetadata(),
157+
returnPrices,
131158
),
132159
);
133160
}
@@ -171,7 +198,8 @@ function getCalldataWithTimestamp(
171198
dataFeedId: string,
172199
packages: SignedDataPackage[],
173200
unsignedMetadata: string,
174-
): TimestampedCalldata {
201+
returnPrices?: boolean,
202+
): TimestampedCalldataWithPrice | TimestampedCalldata {
175203
const originalPayload = RedstonePayload.prepare(packages, unsignedMetadata);
176204

177205
// Calculating the number of bytes in the hex representation of payload
@@ -198,7 +226,7 @@ function getCalldataWithTimestamp(
198226
}
199227
}
200228

201-
return {
229+
const base: TimestampedCalldata = {
202230
dataFeedId,
203231
data: encodeAbiParameters(
204232
[{ type: "uint256" }, { type: "bytes" }],
@@ -207,4 +235,18 @@ function getCalldataWithTimestamp(
207235
timestamp,
208236
cached: false,
209237
};
238+
239+
if (returnPrices) {
240+
const prices: bigint[] = packages
241+
.flatMap(p => p.dataPackage.dataPoints)
242+
.map(dp => BigInt(`0x${Buffer.from(dp.value).toString("hex")}`));
243+
const medianPrice = BigInt(MathUtils.getMedianOfBigNumbers(prices));
244+
245+
return {
246+
...base,
247+
price: medianPrice,
248+
decimals: 8,
249+
} as TimestampedCalldataWithPrice;
250+
}
251+
return base;
210252
}

src/sdk/market/pricefeeds/updates/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,8 @@ export interface TimestampedCalldata {
2323
timestamp: number;
2424
cached: boolean;
2525
}
26+
27+
export interface TimestampedCalldataWithPrice extends TimestampedCalldata {
28+
price: bigint;
29+
decimals: number;
30+
}

yarn.lock

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,7 @@ __metadata:
924924
"@redstone-finance/evm-connector": "npm:^0.9.0"
925925
"@redstone-finance/protocol": "npm:^0.9.0"
926926
"@redstone-finance/sdk": "npm:^0.9.0"
927+
"@redstone-finance/utils": "npm:^0.9.0"
927928
"@types/bn.js": "npm:^5.2.0"
928929
"@types/cross-spawn": "npm:^6.0.6"
929930
abitype: "npm:^1.2.3"
@@ -1119,7 +1120,7 @@ __metadata:
11191120
languageName: node
11201121
linkType: hard
11211122

1122-
"@redstone-finance/utils@npm:0.9.0":
1123+
"@redstone-finance/utils@npm:0.9.0, @redstone-finance/utils@npm:^0.9.0":
11231124
version: 0.9.0
11241125
resolution: "@redstone-finance/utils@npm:0.9.0"
11251126
dependencies:

0 commit comments

Comments
 (0)