Skip to content

Commit 65baa14

Browse files
[TWAP] Update Hermes binary response format, add TwapMessage parsing to price-service-sdk (#2158)
* feat: change hermes twap binary response, add twap data model and parsing to price-service-sdk * feat: reexport parseTwapMessage * test: add parse twap msg test * test: update parse twap test * refactor: pack both start & end updatedatas into a single binaryupdate * refactor: snake -> camel case
1 parent e371428 commit 65baa14

File tree

9 files changed

+110
-42
lines changed

9 files changed

+110
-42
lines changed

apps/hermes/server/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/hermes/server/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "hermes"
3-
version = "0.8.0"
3+
version = "0.8.1"
44
description = "Hermes is an agent that provides Verified Prices from the Pythnet Pyth Oracle."
55
edition = "2021"
66

apps/hermes/server/src/api/rest/v2/latest_twaps.rs

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -131,22 +131,17 @@ where
131131
})?;
132132

133133
let twap_update_data = twaps_with_update_data.update_data;
134-
let binary: Vec<BinaryUpdate> = twap_update_data
134+
let encoded_data = twap_update_data
135135
.into_iter()
136-
.map(|data_vec| {
137-
let encoded_data = data_vec
138-
.into_iter()
139-
.map(|data| match params.encoding {
140-
EncodingType::Base64 => base64_standard_engine.encode(data),
141-
EncodingType::Hex => hex::encode(data),
142-
})
143-
.collect();
144-
BinaryUpdate {
145-
encoding: params.encoding,
146-
data: encoded_data,
147-
}
136+
.map(|data| match params.encoding {
137+
EncodingType::Base64 => base64_standard_engine.encode(data),
138+
EncodingType::Hex => hex::encode(data),
148139
})
149140
.collect();
141+
let binary = BinaryUpdate {
142+
encoding: params.encoding,
143+
data: encoded_data,
144+
};
150145

151146
let parsed: Option<Vec<ParsedPriceFeedTwap>> = if params.parsed {
152147
Some(

apps/hermes/server/src/api/types.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,9 +281,9 @@ impl From<PriceFeedTwap> for ParsedPriceFeedTwap {
281281

282282
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
283283
pub struct TwapsResponse {
284-
/// Each BinaryUpdate contains the start & end cumulative price updates used to
284+
/// Contains the start & end cumulative price updates used to
285285
/// calculate a given price feed's TWAP.
286-
pub binary: Vec<BinaryUpdate>,
286+
pub binary: BinaryUpdate,
287287

288288
/// The calculated TWAPs for each price ID
289289
#[serde(skip_serializing_if = "Option::is_none")]

apps/hermes/server/src/state/aggregate.rs

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ pub struct PublisherStakeCapsWithUpdateData {
206206
#[derive(Debug)]
207207
pub struct TwapsWithUpdateData {
208208
pub twaps: Vec<PriceFeedTwap>,
209-
pub update_data: Vec<Vec<Vec<u8>>>,
209+
pub update_data: Vec<Vec<u8>>,
210210
}
211211

212212
#[derive(Debug, Serialize)]
@@ -652,7 +652,6 @@ where
652652
}
653653

654654
let mut twaps = Vec::new();
655-
let mut update_data = Vec::new();
656655

657656
// Iterate through start and end messages together
658657
for (start_message, end_message) in start_messages.iter().zip(end_messages.iter()) {
@@ -676,34 +675,27 @@ where
676675
end_timestamp: end_twap.publish_time,
677676
down_slots_ratio,
678677
});
679-
680-
// Combine messages for update data
681-
let mut messages = Vec::new();
682-
messages.push(start_message.clone().into());
683-
messages.push(end_message.clone().into());
684-
685-
if let Ok(update) = construct_update_data(messages) {
686-
update_data.push(update);
687-
} else {
688-
tracing::warn!(
689-
"Failed to construct update data for price feed {:?}",
690-
start_twap.feed_id
691-
);
692-
continue;
693-
}
694678
}
695679
Err(e) => {
696-
tracing::warn!(
680+
return Err(anyhow!(
697681
"Failed to calculate TWAP for price feed {:?}: {}",
698682
start_twap.feed_id,
699683
e
700-
);
701-
continue;
684+
));
702685
}
703686
}
704687
}
705688
}
706689

690+
// Construct update data.
691+
// update_data[0] contains the start VAA and merkle proofs
692+
// update_data[1] contains the end VAA and merkle proofs
693+
let mut update_data =
694+
construct_update_data(start_messages.into_iter().map(Into::into).collect())?;
695+
update_data.extend(construct_update_data(
696+
end_messages.into_iter().map(Into::into).collect(),
697+
)?);
698+
707699
Ok(TwapsWithUpdateData { twaps, update_data })
708700
}
709701

@@ -1316,10 +1308,8 @@ mod test {
13161308
assert_eq!(twap_2.start_timestamp, 100);
13171309
assert_eq!(twap_2.end_timestamp, 200);
13181310

1319-
// Verify update data contains both start and end messages for both feeds
1311+
// update_data should have 2 elements, one for the start block and one for the end block.
13201312
assert_eq!(result.update_data.len(), 2);
1321-
assert_eq!(result.update_data[0].len(), 2); // Should contain 2 messages
1322-
assert_eq!(result.update_data[1].len(), 2); // Should contain 2 messages
13231313
}
13241314
#[tokio::test]
13251315

price_service/sdk/js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pythnetwork/price-service-sdk",
3-
"version": "1.7.1",
3+
"version": "1.8.0",
44
"description": "Pyth price service SDK",
55
"homepage": "https://pyth.network",
66
"main": "lib/index.js",

price_service/sdk/js/src/AccumulatorUpdateData.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ const MAJOR_VERSION = 1;
55
const MINOR_VERSION = 0;
66
const KECCAK160_HASH_SIZE = 20;
77
const PRICE_FEED_MESSAGE_VARIANT = 0;
8+
const TWAP_MESSAGE_VARIANT = 1;
89

910
export type AccumulatorUpdateData = {
1011
vaa: Buffer;
1112
updates: { message: Buffer; proof: number[][] }[];
1213
};
13-
1414
export type PriceFeedMessage = {
1515
feedId: Buffer;
1616
price: BN;
@@ -22,13 +22,25 @@ export type PriceFeedMessage = {
2222
emaConf: BN;
2323
};
2424

25+
export type TwapMessage = {
26+
feedId: Buffer;
27+
cumulativePrice: BN;
28+
cumulativeConf: BN;
29+
numDownSlots: BN;
30+
exponent: number;
31+
publishTime: BN;
32+
prevPublishTime: BN;
33+
publishSlot: BN;
34+
};
35+
2536
export function isAccumulatorUpdateData(updateBytes: Buffer): boolean {
2637
return (
2738
updateBytes.toString("hex").slice(0, 8) === ACCUMULATOR_MAGIC &&
2839
updateBytes[4] === MAJOR_VERSION &&
2940
updateBytes[5] === MINOR_VERSION
3041
);
3142
}
43+
3244
export function parsePriceFeedMessage(message: Buffer): PriceFeedMessage {
3345
let cursor = 0;
3446
const variant = message.readUInt8(cursor);
@@ -64,6 +76,41 @@ export function parsePriceFeedMessage(message: Buffer): PriceFeedMessage {
6476
};
6577
}
6678

79+
export function parseTwapMessage(message: Buffer): TwapMessage {
80+
let cursor = 0;
81+
const variant = message.readUInt8(cursor);
82+
if (variant !== TWAP_MESSAGE_VARIANT) {
83+
throw new Error("Not a twap message");
84+
}
85+
cursor += 1;
86+
const feedId = message.subarray(cursor, cursor + 32);
87+
cursor += 32;
88+
const cumulativePrice = new BN(message.subarray(cursor, cursor + 16), "be");
89+
cursor += 16;
90+
const cumulativeConf = new BN(message.subarray(cursor, cursor + 16), "be");
91+
cursor += 16;
92+
const numDownSlots = new BN(message.subarray(cursor, cursor + 8), "be");
93+
cursor += 8;
94+
const exponent = message.readInt32BE(cursor);
95+
cursor += 4;
96+
const publishTime = new BN(message.subarray(cursor, cursor + 8), "be");
97+
cursor += 8;
98+
const prevPublishTime = new BN(message.subarray(cursor, cursor + 8), "be");
99+
cursor += 8;
100+
const publishSlot = new BN(message.subarray(cursor, cursor + 8), "be");
101+
cursor += 8;
102+
return {
103+
feedId,
104+
cumulativePrice,
105+
cumulativeConf,
106+
numDownSlots,
107+
exponent,
108+
publishTime,
109+
prevPublishTime,
110+
publishSlot,
111+
};
112+
}
113+
67114
/**
68115
* An AccumulatorUpdateData contains a VAA and a list of updates. This function returns a new serialized AccumulatorUpdateData with only the updates in the range [start, end).
69116
*/

price_service/sdk/js/src/__tests__/AccumulatorUpdateData.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
parseAccumulatorUpdateData,
33
parsePriceFeedMessage,
4+
parseTwapMessage,
45
sliceAccumulatorUpdateData,
56
} from "../AccumulatorUpdateData";
67

@@ -113,4 +114,38 @@ describe("Test parse accumulator update", () => {
113114
"Invalid accumulator message"
114115
);
115116
});
117+
118+
test("Parse TWAP message", () => {
119+
// Sample data from the Hermes latest TWAP endpoint.
120+
const testAccumulatorDataTwap =
121+
"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";
122+
const { updates } = parseAccumulatorUpdateData(
123+
Buffer.from(testAccumulatorDataTwap, "base64")
124+
);
125+
126+
// Test that both messages are parsed successfully
127+
const twapMessage1 = parseTwapMessage(updates[0].message);
128+
expect(twapMessage1.feedId.toString("hex")).toBe(
129+
"49f6b65cb1de6b10eaf75e7c03ca029c306d0357e91b5311b175084a5ad55688"
130+
);
131+
expect(twapMessage1.cumulativePrice.toString()).toBe("1760238576144013");
132+
expect(twapMessage1.cumulativeConf.toString()).toBe("5113466755162");
133+
expect(twapMessage1.numDownSlots.toString()).toBe("72037403");
134+
expect(twapMessage1.exponent).toBe(-5);
135+
expect(twapMessage1.publishTime.toString()).toBe("1733155135");
136+
expect(twapMessage1.prevPublishTime.toString()).toBe("1733155134");
137+
expect(twapMessage1.publishSlot.toString()).toBe("181871343");
138+
139+
const twapMessage2 = parseTwapMessage(updates[1].message);
140+
expect(twapMessage2.feedId.toString("hex")).toBe(
141+
"2b9ab1e972a281585084148ba1389800799bd4be63b957507db1349314e47445"
142+
);
143+
expect(twapMessage2.cumulativePrice.toString()).toBe("949830028892149802");
144+
expect(twapMessage2.cumulativeConf.toString()).toBe("973071467813959");
145+
expect(twapMessage2.numDownSlots.toString()).toBe("27140");
146+
expect(twapMessage2.exponent).toBe(-8);
147+
expect(twapMessage2.publishTime.toString()).toBe("1733155135");
148+
expect(twapMessage2.prevPublishTime.toString()).toBe("1733155134");
149+
expect(twapMessage2.publishSlot.toString()).toBe("181871343");
150+
});
116151
});

price_service/sdk/js/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export {
1515
parseAccumulatorUpdateData,
1616
AccumulatorUpdateData,
1717
parsePriceFeedMessage,
18+
parseTwapMessage,
1819
} from "./AccumulatorUpdateData";
1920

2021
/**

0 commit comments

Comments
 (0)