Skip to content

Commit 3c1348c

Browse files
authored
feat(price-service-sdk) implement accumulator update data deserialization (#1308)
* Export * Cleanup sui *  Add comment * Bump packages
1 parent c9a578b commit 3c1348c

File tree

8 files changed

+110
-40
lines changed

8 files changed

+110
-40
lines changed

price_service/client/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-client",
3-
"version": "1.8.2",
3+
"version": "1.9.0",
44
"description": "Pyth price service client",
55
"author": {
66
"name": "Pyth Data Association"

price_service/client/js/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@ export {
1010
PriceFeed,
1111
Price,
1212
UnixTimestamp,
13+
isAccumulatorUpdateData,
14+
parseAccumulatorUpdateData,
15+
AccumulatorUpdateData,
1316
} from "@pythnetwork/price-service-sdk";

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.4.1",
3+
"version": "1.5.0",
44
"description": "Pyth price service SDK",
55
"homepage": "https://pyth.network",
66
"main": "lib/index.js",
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
const ACCUMULATOR_MAGIC = "504e4155";
2+
const MAJOR_VERSION = 1;
3+
const MINOR_VERSION = 0;
4+
const KECCAK160_HASH_SIZE = 20;
5+
6+
export type AccumulatorUpdateData = {
7+
vaa: Buffer;
8+
updates: { message: Buffer; proof: Buffer[] }[];
9+
};
10+
11+
export function isAccumulatorUpdateData(updateBytes: Buffer): boolean {
12+
return (
13+
updateBytes.toString("hex").slice(0, 8) === ACCUMULATOR_MAGIC &&
14+
updateBytes[4] === MAJOR_VERSION &&
15+
updateBytes[5] === MINOR_VERSION
16+
);
17+
}
18+
19+
export function parseAccumulatorUpdateData(
20+
data: Buffer
21+
): AccumulatorUpdateData {
22+
if (!isAccumulatorUpdateData(data)) {
23+
throw new Error("Invalid accumulator message");
24+
}
25+
26+
let cursor = 6;
27+
const trailingPayloadSize = data.readUint8(cursor);
28+
cursor += 1 + trailingPayloadSize;
29+
30+
// const proofType = data.readUint8(cursor);
31+
cursor += 1;
32+
33+
const vaaSize = data.readUint16BE(cursor);
34+
cursor += 2;
35+
36+
const vaa = data.subarray(cursor, cursor + vaaSize);
37+
cursor += vaaSize;
38+
39+
const numUpdates = data.readUInt8(cursor);
40+
const updates = [];
41+
cursor += 1;
42+
43+
for (let i = 0; i < numUpdates; i++) {
44+
const messageSize = data.readUint16BE(cursor);
45+
cursor += 2;
46+
const message = data.subarray(cursor, cursor + messageSize);
47+
cursor += messageSize;
48+
49+
const numProofs = data.readUInt8(cursor);
50+
cursor += 1;
51+
const proof = [];
52+
for (let j = 0; j < numProofs; j++) {
53+
proof.push(data.subarray(cursor, cursor + KECCAK160_HASH_SIZE));
54+
cursor += KECCAK160_HASH_SIZE;
55+
}
56+
57+
updates.push({ message, proof });
58+
}
59+
60+
if (cursor !== data.length) {
61+
throw new Error("Didn't reach the end of the message");
62+
}
63+
64+
return { vaa, updates };
65+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { parseAccumulatorUpdateData } from "../AccumulatorUpdateData";
2+
3+
// This is just a sample update data from hermes
4+
const TEST_ACCUMULATOR_UPDATE_DATA =
5+
"UE5BVQEAAAADuAEAAAADDQCRWcud7VE0FQkptV7iZh1Ls8O4dszQ4HmdOZLNDVQiQnMB5Q9jVd52Y9IMI8k1QhOhT2xn82hveqZ6AIn+c6vPAQL4nN+ynbzaJnbGFpWW5ysEA811BblKr+DXO5I5tD3SgEjPmByPIEVPHRqdgnq7M8r6AG4q8qfbmeonROr67i4eAAPVteKrUXD6f13GG/Qj0xHcJ/NuR+xwrbs6KGmYZHq0fHv4m0C3LPIOgVo9iy2ednK5IB/pAEMoaGK/fwoL2ouSAQSulF0XQGZ0J5oFKvCwPBSZOYtITwEQSicnwIWu9a+j7SjMh/zF4vtqWFAqfkFatVMZI6/dkQkmwlcMkEkGHvN5AQZYiYD8teZVpmCzn9jxZo/qTF4qrWgrHWv3/i4kZsXmkDSq1QTiYd7ikQQVWVxgH3PKl03SPFvqoc7SmwKIZKyyAQjfPTwpqeTTi0zFRyyb9HKMYjcbXEcuRXn7uOaNF83ry1s+cudCcWsiaCNYEPzv1BvHxgYYXcx2MkNxUbXiLlmoAQpQSpOkNb9780k2EsrUjZd/ieD+sTQA6P0iZWL5jA8ONEi46mAufCfRlAO2a5jfUvjuN4Z/ZOklgT9eZ7v3JoleAAv/2wkZ5rQx+cl/jlL9k6rbzrDU8sYLTJnlFTsuOr66/iVUqCe0Clwv682NgvH8yLbtw9He/vdn3OeLn19eDU0qAQxk47DIhc9EAtNrdhFSyAoEBtQtgcxRvSnjIIMPTGIhIzv52WFY/I2CwyKcQLhERdjjfh7EhZvBUXHTFRk2xjc2AA3wNaGbjUXsJqL8VyBQg7t0dILbUQ8AiOZJQVfx+L+1mFVZAc4v8/0BWsIF5b7+YmoN6psArWCvZcd9Hkjuxda4AQ5Rxgs32U2Jm43W4voTk42MibgvPMas3xQbuCW88pH1skdSTfvtgoIOa6BdoS3YEUJu78a0X3AiIUem1fDOdOs7AA/lHyqNz4vwuTNs8U6G51VqO2g1yEJyRwrMqsjEvK9VC0EjieacqPBwPL9/DMssbHU01bL+YzEY5XTxi1QiBeyFABJuE+6jHgEh9WvwaPDZe7me9sl5EiPDUxAAryErsB0LDTrnzls7qgDymCp+MSJur8U4I08ul/mL1rVesK3uUqqtAGV20Z0AAAAAABrhAfrtrFhR4yubI7X5QRqMK6xKrj7U3XuBHdGnLqSqcQAAAAAB1IYyAUFVV1YAAAAAAAbbm1gAACcQralfDHbB9c321F6ngWz+RspcmdsBAFUAyWRY05P+net6fWOgrEHiiYpnp3UNvRZmcyeeBsho3woAAAAAAFEIJAAAAAAAAAxT////+AAAAABldtGcAAAAAGV20ZsAAAAAAFEWJQAAAAAAAAu6CqMvc3++cZquwewCu6kJe8aPB1SFPI41uwi10MgNwqRbCue30EvorUjF4mKpFB+Cwx8KH5bFnAAX13DPmu7OCbX7k0LdKtr9pb8zPVsXwlx+BteFyBWNtJmeLIx7tG88H2uARL/B+MJw2GcVujs6qdnIQkIjjBdDIR3XRtY2zMfK58eeXuiAkJDHIQ3H41GmYRAVe8FtPvtMWTY51Q63Tkmfq60qsB1yy4Srd5QI/x60eBnOlAYC67+gjB0sGHLrjSapbXzGUf//";
6+
7+
describe("Test parse accumulator update", () => {
8+
test("Happy path", async () => {
9+
parseAccumulatorUpdateData(
10+
Buffer.from(TEST_ACCUMULATOR_UPDATE_DATA, "base64")
11+
);
12+
});
13+
14+
test("Wrong magic number", async () => {
15+
const data = Buffer.from(TEST_ACCUMULATOR_UPDATE_DATA, "base64");
16+
data[0] = 0;
17+
expect(() => parseAccumulatorUpdateData(data)).toThrow(
18+
"Invalid accumulator message"
19+
);
20+
});
21+
});

price_service/sdk/js/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isAccumulatorUpdateData } from "./AccumulatorUpdateData";
12
import {
23
Convert,
34
Price as JsonPrice,
@@ -9,6 +10,12 @@ export type UnixTimestamp = number;
910
export type DurationInSeconds = number;
1011
export type HexString = string;
1112

13+
export {
14+
isAccumulatorUpdateData,
15+
parseAccumulatorUpdateData,
16+
AccumulatorUpdateData,
17+
} from "./AccumulatorUpdateData";
18+
1219
/**
1320
* A Pyth Price represented as `${price} ± ${conf} * 10^${expo}` published at `publishTime`.
1421
*/

target_chains/sui/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/pyth-sui-js",
3-
"version": "1.2.4",
3+
"version": "1.2.5",
44
"description": "Pyth Network Sui Utilities",
55
"homepage": "https://pyth.network",
66
"author": {

target_chains/sui/sdk/js/src/client.ts

Lines changed: 11 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import {
55
SUI_CLOCK_OBJECT_ID,
66
TransactionBlock,
77
} from "@mysten/sui.js";
8-
import { HexString } from "@pythnetwork/price-service-client";
8+
import {
9+
HexString,
10+
isAccumulatorUpdateData,
11+
parseAccumulatorUpdateData,
12+
} from "@pythnetwork/price-service-client";
913
import { Buffer } from "buffer";
1014

1115
const MAX_ARGUMENT_SIZE = 16 * 1024;
@@ -115,13 +119,13 @@ export class SuiPythClient {
115119
const packageId = await this.getPythPackageId();
116120

117121
let priceUpdatesHotPotato;
118-
if (updates.every((update) => this.isAccumulatorMsg(update))) {
122+
if (updates.every((update) => isAccumulatorUpdateData(update))) {
119123
if (updates.length > 1) {
120124
throw new Error(
121125
"SDK does not support sending multiple accumulator messages in a single transaction"
122126
);
123127
}
124-
const vaa = this.extractVaaBytesFromAccumulatorMessage(updates[0]);
128+
const vaa = parseAccumulatorUpdateData(updates[0]).vaa;
125129
const verifiedVaas = await this.verifyVaas([vaa], tx);
126130
[priceUpdatesHotPotato] = tx.moveCall({
127131
target: `${packageId}::pyth::create_authenticated_price_infos_using_accumulator`,
@@ -138,7 +142,7 @@ export class SuiPythClient {
138142
tx.object(SUI_CLOCK_OBJECT_ID),
139143
],
140144
});
141-
} else if (updates.every((vaa) => !this.isAccumulatorMsg(vaa))) {
145+
} else if (updates.every((vaa) => !isAccumulatorUpdateData(vaa))) {
142146
const verifiedVaas = await this.verifyVaas(updates, tx);
143147
[priceUpdatesHotPotato] = tx.moveCall({
144148
target: `${packageId}::pyth::create_price_infos_hot_potato`,
@@ -192,13 +196,13 @@ export class SuiPythClient {
192196
async createPriceFeed(tx: TransactionBlock, updates: Buffer[]) {
193197
const wormholePackageId = await this.getWormholePackageId();
194198
const packageId = await this.getPythPackageId();
195-
if (updates.every((update) => this.isAccumulatorMsg(update))) {
199+
if (updates.every((update) => isAccumulatorUpdateData(update))) {
196200
if (updates.length > 1) {
197201
throw new Error(
198202
"SDK does not support sending multiple accumulator messages in a single transaction"
199203
);
200204
}
201-
const vaa = this.extractVaaBytesFromAccumulatorMessage(updates[0]);
205+
const vaa = parseAccumulatorUpdateData(updates[0]).vaa;
202206
const verifiedVaas = await this.verifyVaas([vaa], tx);
203207
tx.moveCall({
204208
target: `${packageId}::pyth::create_price_feeds_using_accumulator`,
@@ -215,7 +219,7 @@ export class SuiPythClient {
215219
tx.object(SUI_CLOCK_OBJECT_ID),
216220
],
217221
});
218-
} else if (updates.every((vaa) => !this.isAccumulatorMsg(vaa))) {
222+
} else if (updates.every((vaa) => !isAccumulatorUpdateData(vaa))) {
219223
const verifiedVaas = await this.verifyVaas(updates, tx);
220224
tx.moveCall({
221225
target: `${packageId}::pyth::create_price_feeds`,
@@ -311,34 +315,4 @@ export class SuiPythClient {
311315
}
312316
return this.priceTableInfo;
313317
}
314-
315-
/**
316-
* Checks if a message is an accumulator message or not
317-
* @param msg - update message from price service
318-
*/
319-
isAccumulatorMsg(msg: Buffer) {
320-
const ACCUMULATOR_MAGIC = "504e4155";
321-
return msg.toString("hex").slice(0, 8) === ACCUMULATOR_MAGIC;
322-
}
323-
324-
/**
325-
* Obtains the vaa bytes embedded in an accumulator message.
326-
* @param accumulatorMessage - the accumulator price update message
327-
* @returns vaa bytes as a uint8 array
328-
*/
329-
extractVaaBytesFromAccumulatorMessage(accumulatorMessage: Buffer): Buffer {
330-
if (!this.isAccumulatorMsg(accumulatorMessage)) {
331-
throw new Error("Not an accumulator message");
332-
}
333-
// the first 6 bytes in the accumulator message encode the header, major, and minor bytes
334-
// we ignore them, since we are only interested in the VAA bytes
335-
const trailingPayloadSize = accumulatorMessage.readUint8(6);
336-
const vaaSizeOffset =
337-
7 + // header bytes (header(4) + major(1) + minor(1) + trailing payload size(1))
338-
trailingPayloadSize + // trailing payload (variable number of bytes)
339-
1; // proof_type (1 byte)
340-
const vaaSize = accumulatorMessage.readUint16BE(vaaSizeOffset);
341-
const vaaOffset = vaaSizeOffset + 2;
342-
return accumulatorMessage.subarray(vaaOffset, vaaOffset + vaaSize);
343-
}
344318
}

0 commit comments

Comments
 (0)