pyth2wormhole: implement wire format v3rev0 (#189)

* pyth2wormhole: implement wire format v3rev0

commit-id:c106d3b3

* pyth.js: Run prettier formatting

commit-id:fc9fb160

* ethereum: Fix tab indentation in Pyth contracts

commit-id:3d2ab7d8

* p2w-terra contract: fix formatting

commit-id:685dd14f

* p2w-sdk: Use wasm in the JS package, detect bundler/node via `window`

This commit makes sure that our wasm usage is more robust. We ensure
that the JSON representation renders all important numbers in their
string decimal form. b

commit-id:75f9c224

* p2w-sdk: review suggestions: field ranems, limit serde attributes

commit-id:9f596b80

* p2w-sdk/rust: Remove utils.rs and the helper type

commit-id:e72c6345
This commit is contained in:
Stanisław Drozd 2022-04-25 18:32:25 +02:00 committed by GitHub
parent 5b13be3bbf
commit a4c749b99c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 501 additions and 685 deletions

View File

@ -43,4 +43,5 @@ RUN --mount=type=cache,target=/root/.cache \
FROM scratch AS export
COPY --from=build /usr/src/bridge/third_party/pyth/p2w-sdk/rust/bundler third_party/pyth/p2w-sdk/js/src/solana/p2w-core
COPY --from=build /usr/src/bridge/third_party/pyth/p2w-sdk/rust/bundler third_party/pyth/p2w-sdk/js/src/solana/p2w-core/bundler
COPY --from=build /usr/src/bridge/third_party/pyth/p2w-sdk/rust/nodejs third_party/pyth/p2w-sdk/js/src/solana/p2w-core/nodejs

View File

@ -37,7 +37,7 @@ contract Pyth is PythGetters, PythSetters, AbstractPyth {
PythInternalStructs.PriceInfo memory latestPrice = latestPriceInfo(attestation.priceId);
if(attestation.timestamp > latestPrice.attestationTime) {
if(attestation.attestationTime > latestPrice.attestationTime) {
setLatestPriceInfo(attestation.priceId, newPriceInfo(attestation));
}
}
@ -46,22 +46,21 @@ contract Pyth is PythGetters, PythSetters, AbstractPyth {
}
function newPriceInfo(PythInternalStructs.PriceAttestation memory pa) private view returns (PythInternalStructs.PriceInfo memory info) {
info.attestationTime = pa.timestamp;
info.attestationTime = pa.attestationTime;
info.publishTime = pa.publishTime;
info.arrivalTime = block.timestamp;
info.arrivalBlock = block.number;
info.priceFeed.id = pa.priceId;
info.priceFeed.price = pa.price;
info.priceFeed.conf = pa.confidenceInterval;
info.priceFeed.conf = pa.conf;
info.priceFeed.expo = pa.expo;
info.priceFeed.status = PythStructs.PriceStatus(pa.status);
info.priceFeed.expo = pa.exponent;
info.priceFeed.emaPrice = pa.emaPrice.value;
info.priceFeed.emaConf = uint64(pa.emaConf.value);
info.priceFeed.emaPrice = pa.emaPrice;
info.priceFeed.emaConf = pa.emaConf;
info.priceFeed.productId = pa.productId;
// These aren't sent in the wire format yet
info.priceFeed.numPublishers = 0;
info.priceFeed.maxNumPublishers = 0;
info.priceFeed.numPublishers = pa.numPublishers;
info.priceFeed.maxNumPublishers = pa.maxNumPublishers;
return info;
}
@ -84,14 +83,38 @@ contract Pyth is PythGetters, PythSetters, AbstractPyth {
index += 4;
require(bpa.header.magic == 0x50325748, "invalid magic value");
bpa.header.version = encoded.toUint16(index);
bpa.header.versionMajor = encoded.toUint16(index);
index += 2;
require(bpa.header.version == 2, "invalid version");
require(bpa.header.versionMajor == 3, "invalid version major, expected 3");
bpa.header.versionMinor = encoded.toUint16(index);
index += 2;
require(bpa.header.versionMinor >= 0, "invalid version minor, expected 0 or more");
bpa.header.hdrSize = encoded.toUint16(index);
index += 2;
// NOTE(2022-04-19): Currently, only payloadId comes after
// hdrSize. Future extra header fields must be read using a
// separate offset to respect hdrSize, i.e.:
//
// uint hdrIndex = 0;
// bpa.header.payloadId = encoded.toUint8(index + hdrIndex);
// hdrIndex += 1;
//
// bpa.header.someNewField = encoded.toUint32(index + hdrIndex);
// hdrIndex += 4;
//
// // Skip remaining unknown header bytes
// index += bpa.header.hdrSize;
bpa.header.payloadId = encoded.toUint8(index);
index += 1;
// Payload ID of 2 required for batch header
require(bpa.header.payloadId == 2, "invalid payload ID");
// Skip remaining unknown header bytes
index += bpa.header.hdrSize;
// Payload ID of 2 required for batch headerBa
require(bpa.header.payloadId == 2, "invalid payload ID, expected 2 for BatchPriceAttestation");
// Parse the number of attestations
bpa.nAttestations = encoded.toUint16(index);
@ -106,24 +129,10 @@ contract Pyth is PythGetters, PythSetters, AbstractPyth {
// Deserialize each attestation
for (uint j=0; j < bpa.nAttestations; j++) {
// NOTE: We don't advance the global index immediately.
// attestationIndex is an attestation-local offset used
// for readability and easier debugging.
uint attestationIndex = 0;
// Header
bpa.attestations[j].header.magic = encoded.toUint32(index + attestationIndex);
attestationIndex += 4;
require(bpa.attestations[j].header.magic == 0x50325748, "invalid magic value");
bpa.attestations[j].header.version = encoded.toUint16(index + attestationIndex);
attestationIndex += 2;
require(bpa.attestations[j].header.version == 2, "invalid version");
bpa.attestations[j].header.payloadId = encoded.toUint8(index + attestationIndex);
attestationIndex += 1;
// Payload ID of 1 required for individual attestation
require(bpa.attestations[j].header.payloadId == 1, "invalid payload ID");
// NOTE: We don't advance the global index immediately.
// attestationIndex is an attestation-local offset used
// for readability and easier debugging.
uint attestationIndex = 0;
// Attestation
bpa.attestations[j].productId = encoded.toBytes32(index + attestationIndex);
@ -131,62 +140,50 @@ contract Pyth is PythGetters, PythSetters, AbstractPyth {
bpa.attestations[j].priceId = encoded.toBytes32(index + attestationIndex);
attestationIndex += 32;
bpa.attestations[j].priceType = encoded.toUint8(index + attestationIndex);
attestationIndex += 1;
bpa.attestations[j].price = int64(encoded.toUint64(index + attestationIndex));
attestationIndex += 8;
bpa.attestations[j].exponent = int32(encoded.toUint32(index + attestationIndex));
bpa.attestations[j].conf = encoded.toUint64(index + attestationIndex);
attestationIndex += 8;
bpa.attestations[j].expo = int32(encoded.toUint32(index + attestationIndex));
attestationIndex += 4;
bpa.attestations[j].emaPrice.value = int64(encoded.toUint64(index + attestationIndex));
attestationIndex += 8;
bpa.attestations[j].emaPrice.numerator = int64(encoded.toUint64(index + attestationIndex));
attestationIndex += 8;
bpa.attestations[j].emaPrice.denominator = int64(encoded.toUint64(index + attestationIndex));
bpa.attestations[j].emaPrice = int64(encoded.toUint64(index + attestationIndex));
attestationIndex += 8;
bpa.attestations[j].emaConf.value = int64(encoded.toUint64(index + attestationIndex));
attestationIndex += 8;
bpa.attestations[j].emaConf.numerator = int64(encoded.toUint64(index + attestationIndex));
attestationIndex += 8;
bpa.attestations[j].emaConf.denominator = int64(encoded.toUint64(index + attestationIndex));
attestationIndex += 8;
bpa.attestations[j].confidenceInterval = encoded.toUint64(index + attestationIndex);
bpa.attestations[j].emaConf = encoded.toUint64(index + attestationIndex);
attestationIndex += 8;
bpa.attestations[j].status = encoded.toUint8(index + attestationIndex);
attestationIndex += 1;
bpa.attestations[j].corpAct = encoded.toUint8(index + attestationIndex);
attestationIndex += 1;
bpa.attestations[j].timestamp = encoded.toUint64(index + attestationIndex);
attestationIndex += 8;
bpa.attestations[j].num_publishers = encoded.toUint32(index + attestationIndex);
bpa.attestations[j].numPublishers = encoded.toUint32(index + attestationIndex);
attestationIndex += 4;
bpa.attestations[j].max_num_publishers = encoded.toUint32(index + attestationIndex);
bpa.attestations[j].maxNumPublishers = encoded.toUint32(index + attestationIndex);
attestationIndex += 4;
bpa.attestations[j].publish_time = encoded.toUint64(index + attestationIndex);
bpa.attestations[j].attestationTime = encoded.toUint64(index + attestationIndex);
attestationIndex += 8;
bpa.attestations[j].prev_publish_time = encoded.toUint64(index + attestationIndex);
bpa.attestations[j].publishTime = encoded.toUint64(index + attestationIndex);
attestationIndex += 8;
bpa.attestations[j].prev_price = int64(encoded.toUint64(index + attestationIndex));
bpa.attestations[j].prevPublishTime = encoded.toUint64(index + attestationIndex);
attestationIndex += 8;
bpa.attestations[j].prev_conf = encoded.toUint64(index + attestationIndex);
bpa.attestations[j].prevPrice = int64(encoded.toUint64(index + attestationIndex));
attestationIndex += 8;
require(attestationIndex <= bpa.attestationSize, "INTERNAL: Consumed more than `attestationSize` bytes");
bpa.attestations[j].prevConf = encoded.toUint64(index + attestationIndex);
attestationIndex += 8;
// Respect specified attestation size for forward-compat
index += bpa.attestationSize;
require(attestationIndex <= bpa.attestationSize, "INTERNAL: Consumed more than `attestationSize` bytes");
// Respect specified attestation size for forward-compat
index += bpa.attestationSize;
}
}

View File

@ -19,35 +19,28 @@ contract PythInternalStructs {
struct Header {
uint32 magic;
uint16 version;
uint16 versionMajor;
uint16 versionMinor;
uint16 hdrSize;
uint8 payloadId;
}
struct PriceAttestation {
Header header;
bytes32 productId;
bytes32 priceId;
uint8 priceType;
int64 price;
int32 exponent;
Rational emaPrice;
Rational emaConf;
uint64 confidenceInterval;
uint64 conf;
int32 expo;
int64 emaPrice;
uint64 emaConf;
uint8 status;
uint8 corpAct;
uint64 timestamp;
uint32 num_publishers;
uint32 max_num_publishers;
uint64 publish_time;
uint64 prev_publish_time;
int64 prev_price;
uint64 prev_conf;
uint32 numPublishers;
uint32 maxNumPublishers;
uint64 attestationTime;
uint64 publishTime;
uint64 prevPublishTime;
int64 prevPrice;
uint64 prevConf;
}
struct Rational {
@ -67,6 +60,7 @@ contract PythInternalStructs {
struct PriceInfo {
PythStructs.PriceFeed priceFeed;
uint256 attestationTime;
uint256 publishTime;
uint256 arrivalTime;
uint256 arrivalBlock;
}

View File

@ -1,49 +1,51 @@
const jsonfile = require('jsonfile');
const elliptic = require('elliptic');
const BigNumber = require('bignumber.js');
const jsonfile = require("jsonfile");
const elliptic = require("elliptic");
const BigNumber = require("bignumber.js");
const PythStructs = artifacts.require("PythStructs");
const { deployProxy, upgradeProxy } = require('@openzeppelin/truffle-upgrades');
const {expectRevert} = require('@openzeppelin/test-helpers');
const { deployProxy, upgradeProxy } = require("@openzeppelin/truffle-upgrades");
const { expectRevert } = require("@openzeppelin/test-helpers");
const Wormhole = artifacts.require("Wormhole");
const PythUpgradable = artifacts.require("PythUpgradable");
const MockPythUpgrade = artifacts.require("MockPythUpgrade");
const testSigner1PK = "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0";
const testSigner2PK = "892330666a850761e7370376430bb8c2aa1494072d3bfeaed0c4fa3d5a9135fe";
const testSigner1PK =
"cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0";
const testSigner2PK =
"892330666a850761e7370376430bb8c2aa1494072d3bfeaed0c4fa3d5a9135fe";
contract("Pyth", function () {
const testSigner1 = web3.eth.accounts.privateKeyToAccount(testSigner1PK);
const testSigner2 = web3.eth.accounts.privateKeyToAccount(testSigner2PK);
const testGovernanceChainId = "3";
const testGovernanceContract = "0x0000000000000000000000000000000000000000000000000000000000000004";
const testGovernanceContract =
"0x0000000000000000000000000000000000000000000000000000000000000004";
const testPyth2WormholeChainId = "1";
const testPyth2WormholeEmitter = "0x71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b";
const notOwnerError = "Ownable: caller is not the owner -- Reason given: Ownable: caller is not the owner.";
const testPyth2WormholeEmitter =
"0x71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b";
const notOwnerError =
"Ownable: caller is not the owner -- Reason given: Ownable: caller is not the owner.";
beforeEach(async function () {
this.pythProxy = await deployProxy(
PythUpgradable,
[
(await Wormhole.deployed()).address,
testPyth2WormholeChainId,
testPyth2WormholeEmitter,
]
);
this.pythProxy = await deployProxy(PythUpgradable, [
(await Wormhole.deployed()).address,
testPyth2WormholeChainId,
testPyth2WormholeEmitter,
]);
});
it("should be initialized with the correct signers and values", async function(){
it("should be initialized with the correct signers and values", async function () {
// pyth2wormhole
const pyth2wormChain = await this.pythProxy.pyth2WormholeChainId();
assert.equal(pyth2wormChain, testPyth2WormholeChainId);
const pyth2wormEmitter = await this.pythProxy.pyth2WormholeEmitter();
assert.equal(pyth2wormEmitter, testPyth2WormholeEmitter);
})
});
it("should allow upgrades from the owner", async function(){
it("should allow upgrades from the owner", async function () {
// Check that the owner is the default account Truffle
// has configured for the network. upgradeProxy will send
// transactions from the default account.
@ -54,14 +56,16 @@ contract("Pyth", function () {
// Try and upgrade the proxy
const newImplementation = await upgradeProxy(
this.pythProxy.address, MockPythUpgrade);
this.pythProxy.address,
MockPythUpgrade
);
// Check that the new upgrade is successful
assert.equal(await newImplementation.isUpgradeActive(), true);
assert.equal(this.pythProxy.address, newImplementation.address);
})
});
it("should allow ownership transfer", async function(){
it("should allow ownership transfer", async function () {
// Check that the owner is the default account Truffle
// has configured for the network.
const accounts = await web3.eth.getAccounts();
@ -69,21 +73,35 @@ contract("Pyth", function () {
assert.equal(await this.pythProxy.owner(), defaultAccount);
// Check that another account can't transfer the ownership
await expectRevert(this.pythProxy.transferOwnership(accounts[1], {from: accounts[1]}), notOwnerError);
await expectRevert(
this.pythProxy.transferOwnership(accounts[1], {
from: accounts[1],
}),
notOwnerError
);
// Transfer the ownership to another account
await this.pythProxy.transferOwnership(accounts[2], {from: defaultAccount});
await this.pythProxy.transferOwnership(accounts[2], {
from: defaultAccount,
});
assert.equal(await this.pythProxy.owner(), accounts[2]);
// Check that the original account can't transfer the ownership back to itself
await expectRevert(this.pythProxy.transferOwnership(defaultAccount, {from: defaultAccount}), notOwnerError);
await expectRevert(
this.pythProxy.transferOwnership(defaultAccount, {
from: defaultAccount,
}),
notOwnerError
);
// Check that the new owner can transfer the ownership back to the original account
await this.pythProxy.transferOwnership(defaultAccount, {from: accounts[2]});
await this.pythProxy.transferOwnership(defaultAccount, {
from: accounts[2],
});
assert.equal(await this.pythProxy.owner(), defaultAccount);
})
});
it("should not allow upgrades from the another account", async function(){
it("should not allow upgrades from the another account", async function () {
// This test is slightly convoluted as, due to a limitation of Truffle,
// we cannot specify which account upgradeProxy send transactions from:
// it will always use the default account.
@ -99,13 +117,18 @@ contract("Pyth", function () {
// Transfer the ownership to another account
const newOwnerAccount = accounts[1];
await this.pythProxy.transferOwnership(newOwnerAccount, {from: defaultAccount});
await this.pythProxy.transferOwnership(newOwnerAccount, {
from: defaultAccount,
});
assert.equal(await this.pythProxy.owner(), newOwnerAccount);
// Try and upgrade using the default account, which will fail
// because we are no longer the owner.
await expectRevert(upgradeProxy(this.pythProxy.address, MockPythUpgrade), notOwnerError);
})
await expectRevert(
upgradeProxy(this.pythProxy.address, MockPythUpgrade),
notOwnerError
);
});
// NOTE(2022-04-11): Raw hex payload obtained from format serialization unit tests in `p2w-sdk/rust`
// Latest known addition: num_publishers, max_num_publishers
@ -123,9 +146,11 @@ contract("Pyth", function () {
// 3rd price = "0xFCFCFC[...]"
const RAW_BATCH_TIMESTAMP_REGEX = /DEADBEEFFADEDEED/g;
const RAW_BATCH_PRICE_REGEX = /0000002BAD2FEED7/g;
const RAW_PRICE_ATTESTATION_SIZE = 190;
const RAW_PRICE_ATTESTATION_SIZE = 149;
const RAW_BATCH_ATTESTATION_COUNT = 10;
const rawBatchPriceAttestation = "0x" + "50325748000202000A00BE503257480002010101010101010101010101010101010101010101010101010101010101010101FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFE010000002BAD2FEED7FFFFFFFDFFFFFFFFFFFFFFD6000000000000000F0000000000000025000000000000002A000000000000045700000000000008AE00000000000000650100DEADBEEFFADEDEED0001E14C0004E6D000000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF503257480002010202020202020202020202020202020202020202020202020202020202020202FDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFD010000002BAD2FEED7FFFFFFFDFFFFFFFFFFFFFFD6000000000000000F0000000000000025000000000000002A000000000000045700000000000008AE00000000000000650100DEADBEEFFADEDEED0001E14C0004E6D000000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF503257480002010303030303030303030303030303030303030303030303030303030303030303FCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFC010000002BAD2FEED7FFFFFFFDFFFFFFFFFFFFFFD6000000000000000F0000000000000025000000000000002A000000000000045700000000000008AE00000000000000650100DEADBEEFFADEDEED0001E14C0004E6D000000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF503257480002010404040404040404040404040404040404040404040404040404040404040404FBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFB010000002BAD2FEED7FFFFFFFDFFFFFFFFFFFFFFD6000000000000000F0000000000000025000000000000002A000000000000045700000000000008AE00000000000000650100DEADBEEFFADEDEED0001E14C0004E6D000000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF503257480002010505050505050505050505050505050505050505050505050505050505050505FAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFA010000002BAD2FEED7FFFFFFFDFFFFFFFFFFFFFFD6000000000000000F0000000000000025000000000000002A000000000000045700000000000008AE00000000000000650100DEADBEEFFADEDEED0001E14C0004E6D000000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF503257480002010606060606060606060606060606060606060606060606060606060606060606F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9010000002BAD2FEED7FFFFFFFDFFFFFFFFFFFFFFD6000000000000000F0000000000000025000000000000002A000000000000045700000000000008AE00000000000000650100DEADBEEFFADEDEED0001E14C0004E6D000000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF503257480002010707070707070707070707070707070707070707070707070707070707070707F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8010000002BAD2FEED7FFFFFFFDFFFFFFFFFFFFFFD6000000000000000F0000000000000025000000000000002A000000000000045700000000000008AE00000000000000650100DEADBEEFFADEDEED0001E14C0004E6D000000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF503257480002010808080808080808080808080808080808080808080808080808080808080808F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7010000002BAD2FEED7FFFFFFFDFFFFFFFFFFFFFFD6000000000000000F0000000000000025000000000000002A000000000000045700000000000008AE00000000000000650100DEADBEEFFADEDEED0001E14C0004E6D000000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF503257480002010909090909090909090909090909090909090909090909090909090909090909F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6010000002BAD2FEED7FFFFFFFDFFFFFFFFFFFFFFD6000000000000000F0000000000000025000000000000002A000000000000045700000000000008AE00000000000000650100DEADBEEFFADEDEED0001E14C0004E6D000000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF503257480002010A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0AF5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5010000002BAD2FEED7FFFFFFFDFFFFFFFFFFFFFFD6000000000000000F0000000000000025000000000000002A000000000000045700000000000008AE00000000000000650100DEADBEEFFADEDEED0001E14C0004E6D000000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF";
const rawBatchPriceAttestation =
"0x" +
"5032574800030000000102000A00950101010101010101010101010101010101010101010101010101010101010101FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFE0000002BAD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D0DEADBEEFFADEDEED00000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0202020202020202020202020202020202020202020202020202020202020202FDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFD0000002BAD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D0DEADBEEFFADEDEED00000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0303030303030303030303030303030303030303030303030303030303030303FCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFC0000002BAD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D0DEADBEEFFADEDEED00000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0404040404040404040404040404040404040404040404040404040404040404FBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFB0000002BAD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D0DEADBEEFFADEDEED00000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0505050505050505050505050505050505050505050505050505050505050505FAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFA0000002BAD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D0DEADBEEFFADEDEED00000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0606060606060606060606060606060606060606060606060606060606060606F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F90000002BAD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D0DEADBEEFFADEDEED00000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0707070707070707070707070707070707070707070707070707070707070707F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F80000002BAD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D0DEADBEEFFADEDEED00000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0808080808080808080808080808080808080808080808080808080808080808F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F70000002BAD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D0DEADBEEFFADEDEED00000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0909090909090909090909090909090909090909090909090909090909090909F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F60000002BAD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D0DEADBEEFFADEDEED00000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0AF5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F50000002BAD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D0DEADBEEFFADEDEED00000000DEADBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF";
// Takes an unsigned 64-bit integer, converts it to hex with 0-padding
function u64ToHex(timestamp) {
@ -137,23 +162,25 @@ contract("Pyth", function () {
const ts = u64ToHex(timestamp);
const price = u64ToHex(priceVal);
const replaced = rawBatchPriceAttestation
.replace(RAW_BATCH_TIMESTAMP_REGEX, ts)
.replace(RAW_BATCH_PRICE_REGEX, price);
.replace(RAW_BATCH_TIMESTAMP_REGEX, ts)
.replace(RAW_BATCH_PRICE_REGEX, price);
return replaced;
}
it("should parse batch price attestation correctly", async function() {
it("should parse batch price attestation correctly", async function () {
const magic = 0x50325748;
const version = 2;
const versionMajor = 3;
const versionMinor = 0;
let timestamp = 1647273460;
let attestationTime = 1647273460;
let priceVal = 1337;
let rawBatch = generateRawBatchAttestation(timestamp, priceVal);
let rawBatch = generateRawBatchAttestation(attestationTime, priceVal);
let parsed = await this.pythProxy.parseBatchPriceAttestation(rawBatch);
// Check the header
assert.equal(parsed.header.magic, magic);
assert.equal(parsed.header.version, version);
assert.equal(parsed.header.versionMajor, versionMajor);
assert.equal(parsed.header.versionMinor, versionMinor);
assert.equal(parsed.header.payloadId, 2);
assert.equal(parsed.nAttestations, RAW_BATCH_ATTESTATION_COUNT);
@ -162,39 +189,36 @@ contract("Pyth", function () {
assert.equal(parsed.attestations.length, parsed.nAttestations);
for (var i = 0; i < parsed.attestations.length; ++i) {
const prodId = "0x" + (i+1).toString(16).padStart(2, "0").repeat(32);
const priceByte = 255 - ((i+1) % 256);
const priceId = "0x" + priceByte.toString(16).padStart(2, "0").repeat(32);
const prodId =
"0x" + (i + 1).toString(16).padStart(2, "0").repeat(32);
const priceByte = 255 - ((i + 1) % 256);
const priceId =
"0x" + priceByte.toString(16).padStart(2, "0").repeat(32);
assert.equal(parsed.attestations[i].header.magic, magic);
assert.equal(parsed.attestations[i].header.version, version);
assert.equal(parsed.attestations[i].header.payloadId, 1);
assert.equal(parsed.attestations[i].productId, prodId);
assert.equal(parsed.attestations[i].priceId, priceId);
assert.equal(parsed.attestations[i].priceType, 1);
assert.equal(parsed.attestations[i].price, priceVal);
assert.equal(parsed.attestations[i].exponent, -3);
assert.equal(parsed.attestations[i].emaPrice.value, -42);
assert.equal(parsed.attestations[i].emaPrice.numerator, 15);
assert.equal(parsed.attestations[i].emaPrice.denominator, 37);
assert.equal(parsed.attestations[i].emaConf.value, 42);
assert.equal(parsed.attestations[i].emaConf.numerator, 1111);
assert.equal(parsed.attestations[i].emaConf.denominator, 2222);
assert.equal(parsed.attestations[i].confidenceInterval, 101);
assert.equal(parsed.attestations[i].conf, 101);
assert.equal(parsed.attestations[i].expo, -3);
assert.equal(parsed.attestations[i].emaPrice, -42);
assert.equal(parsed.attestations[i].emaConf, 42);
assert.equal(parsed.attestations[i].status, 1);
assert.equal(parsed.attestations[i].corpAct, 0);
assert.equal(parsed.attestations[i].timestamp, timestamp);
assert.equal(parsed.attestations[i].num_publishers, 123212);
assert.equal(parsed.attestations[i].max_num_publishers, 321232);
assert.equal(parsed.attestations[i].publish_time, 0xdeadbeef);
assert.equal(parsed.attestations[i].prev_publish_time, 0xdeadbabe);
assert.equal(parsed.attestations[i].prev_price, 0xdeadfacebeef);
assert.equal(parsed.attestations[i].prev_conf, 0xbadbadbeef);
assert.equal(parsed.attestations[i].numPublishers, 123212);
assert.equal(parsed.attestations[i].maxNumPublishers, 321232);
assert.equal(
parsed.attestations[i].attestationTime,
attestationTime
);
assert.equal(parsed.attestations[i].publishTime, 0xdeadbeef);
assert.equal(parsed.attestations[i].prevPublishTime, 0xdeadbabe);
assert.equal(parsed.attestations[i].prevPrice, 0xdeadfacebeef);
assert.equal(parsed.attestations[i].prevConf, 0xbadbadbeef);
console.debug(`attestation ${i + 1}/${parsed.attestations.length} parsed OK`);
}
})
console.debug(
`attestation ${i + 1}/${parsed.attestations.length} parsed OK`
);
}
});
async function attest(contract, data) {
const vm = await signAndEncodeVM(
@ -204,30 +228,28 @@ contract("Pyth", function () {
testPyth2WormholeEmitter,
0,
data,
[
testSigner1PK
],
[testSigner1PK],
0,
0
);
await contract.updatePriceBatchFromVm("0x"+vm);
await contract.updatePriceBatchFromVm("0x" + vm);
}
it("should attest price updates over wormhole", async function() {
it("should attest price updates over wormhole", async function () {
let rawBatch = generateRawBatchAttestation(1647273460, 1337);
await attest(this.pythProxy, rawBatch);
})
});
it("should cache price updates", async function() {
it("should cache price updates", async function () {
let currentTimestamp = (await web3.eth.getBlock("latest")).timestamp;
let priceVal = 521;
let rawBatch = generateRawBatchAttestation(currentTimestamp, priceVal);
await attest(this.pythProxy, rawBatch);
let first_prod_id = "0x" + "01".repeat(32)
let first_price_id = "0x" + "fe".repeat(32)
let second_prod_id = "0x" + "02".repeat(32)
let second_price_id = "0x" + "fd".repeat(32)
let first_prod_id = "0x" + "01".repeat(32);
let first_price_id = "0x" + "fe".repeat(32);
let second_prod_id = "0x" + "02".repeat(32);
let second_price_id = "0x" + "fd".repeat(32);
// Confirm that previously non-existent feeds are created
let first = await this.pythProxy.queryPriceFeed(first_price_id);
@ -239,7 +261,10 @@ contract("Pyth", function () {
// Confirm the price is bumped after a new attestation updates each record
let nextTimestamp = currentTimestamp + 1;
let rawBatch2 = generateRawBatchAttestation(nextTimestamp, priceVal + 5);
let rawBatch2 = generateRawBatchAttestation(
nextTimestamp,
priceVal + 5
);
await attest(this.pythProxy, rawBatch2);
first = await this.pythProxy.queryPriceFeed(first_price_id);
@ -249,7 +274,10 @@ contract("Pyth", function () {
assert.equal(second.price, priceVal + 5);
// Confirm that only strictly larger timestamps trigger updates
let rawBatch3 = generateRawBatchAttestation(nextTimestamp, priceVal + 10);
let rawBatch3 = generateRawBatchAttestation(
nextTimestamp,
priceVal + 10
);
await attest(this.pythProxy, rawBatch3);
first = await this.pythProxy.queryPriceFeed(first_price_id);
@ -259,39 +287,50 @@ contract("Pyth", function () {
second = await this.pythProxy.queryPriceFeed(second_price_id);
assert.equal(second.price, priceVal + 5);
assert.notEqual(second.price, priceVal + 10);
})
});
it("should fail transaction if a price is not found", async function() {
it("should fail transaction if a price is not found", async function () {
await expectRevert(
this.pythProxy.queryPriceFeed(
"0xdeadfeeddeadfeeddeadfeeddeadfeeddeadfeeddeadfeeddeadfeeddeadfeed"),
"no price feed found for the given price id");
})
"0xdeadfeeddeadfeeddeadfeeddeadfeeddeadfeeddeadfeeddeadfeeddeadfeed"
),
"no price feed found for the given price id"
);
});
it("should show stale cached prices as unknown", async function() {
it("should show stale cached prices as unknown", async function () {
let smallestTimestamp = 1;
let rawBatch = generateRawBatchAttestation(smallestTimestamp, 1337);
await attest(this.pythProxy, rawBatch);
for (var i = 1; i <= RAW_BATCH_ATTESTATION_COUNT; i++) {
const price_id = "0x" + (255 - (i % 256)).toString(16).padStart(2, "0").repeat(32);
const price_id =
"0x" +
(255 - (i % 256)).toString(16).padStart(2, "0").repeat(32);
let priceFeedResult = await this.pythProxy.queryPriceFeed(price_id);
assert.equal(priceFeedResult.status.toString(), PythStructs.PriceStatus.UNKNOWN.toString());
assert.equal(
priceFeedResult.status.toString(),
PythStructs.PriceStatus.UNKNOWN.toString()
);
}
})
});
it("should show cached prices too far into the future as unknown", async function() {
it("should show cached prices too far into the future as unknown", async function () {
let largestTimestamp = 4294967295;
let rawBatch = generateRawBatchAttestation(largestTimestamp, 1337);
await attest(this.pythProxy, rawBatch);
for (var i = 1; i <= RAW_BATCH_ATTESTATION_COUNT; i++) {
const price_id = "0x" + (255 - (i % 256)).toString(16).padStart(2, "0").repeat(32);
const price_id =
"0x" +
(255 - (i % 256)).toString(16).padStart(2, "0").repeat(32);
let priceFeedResult = await this.pythProxy.queryPriceFeed(price_id);
assert.equal(priceFeedResult.status.toString(), PythStructs.PriceStatus.UNKNOWN.toString());
assert.equal(
priceFeedResult.status.toString(),
PythStructs.PriceStatus.UNKNOWN.toString()
);
}
})
});
});
const signAndEncodeVM = async function (
@ -306,45 +345,61 @@ const signAndEncodeVM = async function (
consistencyLevel
) {
const body = [
web3.eth.abi.encodeParameter("uint32", timestamp).substring(2 + (64 - 8)),
web3.eth.abi
.encodeParameter("uint32", timestamp)
.substring(2 + (64 - 8)),
web3.eth.abi.encodeParameter("uint32", nonce).substring(2 + (64 - 8)),
web3.eth.abi.encodeParameter("uint16", emitterChainId).substring(2 + (64 - 4)),
web3.eth.abi
.encodeParameter("uint16", emitterChainId)
.substring(2 + (64 - 4)),
web3.eth.abi.encodeParameter("bytes32", emitterAddress).substring(2),
web3.eth.abi.encodeParameter("uint64", sequence).substring(2 + (64 - 16)),
web3.eth.abi.encodeParameter("uint8", consistencyLevel).substring(2 + (64 - 2)),
data.substr(2)
]
web3.eth.abi
.encodeParameter("uint64", sequence)
.substring(2 + (64 - 16)),
web3.eth.abi
.encodeParameter("uint8", consistencyLevel)
.substring(2 + (64 - 2)),
data.substr(2),
];
const hash = web3.utils.soliditySha3(web3.utils.soliditySha3("0x" + body.join("")))
const hash = web3.utils.soliditySha3(
web3.utils.soliditySha3("0x" + body.join(""))
);
let signatures = "";
for (let i in signers) {
const ec = new elliptic.ec("secp256k1");
const key = ec.keyFromPrivate(signers[i]);
const signature = key.sign(hash.substr(2), {canonical: true});
const signature = key.sign(hash.substr(2), { canonical: true });
const packSig = [
web3.eth.abi.encodeParameter("uint8", i).substring(2 + (64 - 2)),
zeroPadBytes(signature.r.toString(16), 32),
zeroPadBytes(signature.s.toString(16), 32),
web3.eth.abi.encodeParameter("uint8", signature.recoveryParam).substr(2 + (64 - 2)),
]
web3.eth.abi
.encodeParameter("uint8", signature.recoveryParam)
.substr(2 + (64 - 2)),
];
signatures += packSig.join("")
signatures += packSig.join("");
}
const vm = [
web3.eth.abi.encodeParameter("uint8", 1).substring(2 + (64 - 2)),
web3.eth.abi.encodeParameter("uint32", guardianSetIndex).substring(2 + (64 - 8)),
web3.eth.abi.encodeParameter("uint8", signers.length).substring(2 + (64 - 2)),
web3.eth.abi
.encodeParameter("uint32", guardianSetIndex)
.substring(2 + (64 - 8)),
web3.eth.abi
.encodeParameter("uint8", signers.length)
.substring(2 + (64 - 2)),
signatures,
body.join("")
body.join(""),
].join("");
return vm
}
return vm;
};
function zeroPadBytes(value, length) {
while (value.length < 2 * length) {

View File

@ -9,8 +9,8 @@ use cosmwasm_std::{
QueryRequest,
Response,
StdResult,
WasmQuery,
Timestamp,
WasmQuery,
};
use pyth_sdk::{
@ -23,8 +23,8 @@ use crate::{
ExecuteMsg,
InstantiateMsg,
MigrateMsg,
QueryMsg,
PriceFeedResponse,
QueryMsg,
},
state::{
config,
@ -37,16 +37,12 @@ use crate::{
},
};
use p2w_sdk::{
BatchPriceAttestation,
};
use p2w_sdk::BatchPriceAttestation;
use wormhole::{
error::ContractError,
msg::QueryMsg as WormholeQueryMsg,
state::{
ParsedVAA,
},
state::ParsedVAA,
};
#[cfg_attr(not(feature = "library"), entry_point)]
@ -130,21 +126,21 @@ fn process_batch_attestation(
let price_feed = PriceFeed::new(
price_attestation.price_id.to_bytes(),
price_attestation.status,
price_attestation.publish_time,
price_attestation.publish_time,
price_attestation.expo,
price_attestation.max_num_publishers, // max_num_publishers data is currently unavailable
price_attestation.num_publishers, // num_publishers data is currently unavailable
price_attestation.max_num_publishers,
price_attestation.num_publishers,
price_attestation.product_id.to_bytes(),
price_attestation.price,
price_attestation.confidence_interval,
price_attestation.ema_price.val,
price_attestation.ema_conf.val as u64,
price_attestation.prev_price,
price_attestation.prev_conf,
price_attestation.prev_publish_time,
price_attestation.conf,
price_attestation.ema_price,
price_attestation.ema_conf,
price_attestation.prev_price,
price_attestation.prev_conf,
price_attestation.prev_publish_time,
);
let attestation_time = Timestamp::from_seconds(price_attestation.timestamp as u64);
let attestation_time = Timestamp::from_seconds(price_attestation.attestation_time as u64);
if update_price_feed_if_new(&mut deps, &env, price_feed, attestation_time)? {
new_attestations_cnt += 1;
@ -205,9 +201,7 @@ fn update_price_feed_if_new(
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::PriceFeed { id } => {
to_binary(&query_price_info(deps, env, id.as_ref())?)
}
QueryMsg::PriceFeed { id } => to_binary(&query_price_info(deps, env, id.as_ref())?),
}
}
@ -222,21 +216,20 @@ pub fn query_price_info(deps: Deps, env: Env, address: &[u8]) -> StdResult<Price
// but more than that is set to unknown, the reason is huge clock difference means there exists a
// problem in a either Terra or Solana blockchain and if it is Solana we don't want to propagate
// Solana internal problems to Terra
let time_abs_diff = if env.block.time.seconds() > terra_price_info.attestation_time.seconds() {
env.block.time.seconds() - terra_price_info.attestation_time.seconds()
} else {
terra_price_info.attestation_time.seconds() - env.block.time.seconds()
};
let time_abs_diff =
if env.block.time.seconds() > terra_price_info.attestation_time.seconds() {
env.block.time.seconds() - terra_price_info.attestation_time.seconds()
} else {
terra_price_info.attestation_time.seconds() - env.block.time.seconds()
};
if time_abs_diff > VALID_TIME_PERIOD.as_secs() {
terra_price_info.price_feed.status = PriceStatus::Unknown;
}
Ok(
PriceFeedResponse {
price_feed: terra_price_info.price_feed,
}
)
Ok(PriceFeedResponse {
price_feed: terra_price_info.price_feed,
})
}
Err(_) => ContractError::AssetNotFound.std_err(),
}

View File

@ -141,9 +141,9 @@ async function readinessProbeRoutine(port: number) {
console.log(`[seqno ${poolEntry}] Parsed VAA:\n`, parsedVaa);
let parsedAttestations = parseBatchPriceAttestation(Buffer.from(parsedVaa.payload));
let parsedAttestations = (await parseBatchPriceAttestation(Buffer.from(parsedVaa.payload))).priceAttestations;
console.log(`[seqno ${poolEntry}] Parsed ${parsedAttestations.nAttestations} price attestations:\n`, parsedAttestations);
console.log(`[seqno ${poolEntry}] Parsed ${parsedAttestations.length} prices in batch:\n`, parsedAttestations);
// try {
// let tx = await p2w_eth.attestPrice(vaaResponse.vaaBytes, {gasLimit: 1000000});

View File

@ -153,7 +153,7 @@ async function processVaa(vaaBytes: string) {
let batchAttestation;
try {
batchAttestation = parseBatchPriceAttestation(
batchAttestation = await parseBatchPriceAttestation(
Buffer.from(parsedVAA.payload)
);
} catch (e: any) {

View File

@ -32,12 +32,12 @@ export class EvmRelay implements Relay {
for (let i = 0; i < signedVAAs.length; ++i) {
let batchNo = i + 1;
let parsedVAA = parse_vaa(hexToUint8Array(signedVAAs[i]));
let parsedBatch = parseBatchPriceAttestation(
let parsedBatch = await parseBatchPriceAttestation(
Buffer.from(parsedVAA.payload)
);
let priceIds: PriceId[] = [];
for (let j = 0; j < parsedBatch.nAttestations; ++j) {
for (let j = 0; j < parsedBatch.priceAttestations.length; ++j) {
priceIds.push(parsedBatch.priceAttestations[j].priceId);
}

View File

@ -3,95 +3,19 @@ import { zeroPad } from "ethers/lib/utils";
import { PublicKey } from "@solana/web3.js";
import { PriceFeed, PriceStatus } from "@pythnetwork/pyth-sdk-js";
let _P2W_WASM: any = undefined;
/*
// Definitions exist in p2w-sdk/rust/
struct Rational {
int64 value;
int64 numerator;
int64 denominator;
}
struct PriceAttestation {
uint32 magic; // constant "P2WH"
uint16 version;
// PayloadID uint8 = 1
uint8 payloadId;
bytes32 productId;
bytes32 priceId;
uint8 priceType;
int64 price;
int32 exponent;
Rational emaPrice;
Rational emaConfidence;
uint64 confidenceInterval;
uint8 status;
uint8 corpAct;
uint64 timestamp;
}
0 uint32 magic // constant "P2WH"
4 u16 version
6 u8 payloadId // 1
7 [u8; 32] productId
39 [u8; 32] priceId
71 u8 priceType
72 i64 price
80 i32 exponent
84 PythRational emaPrice
108 PythRational emaConfidence
132 u64 confidenceInterval
140 u8 status
141 u8 corpAct
142 u64 timestamp
In version 2 prices are sent in batch with the following structure:
struct BatchPriceAttestation {
uint32 magic; // constant "P2WH"
uint16 version;
// PayloadID uint8 = 2
uint8 payloadId;
// number of attestations
uint16 nAttestations;
// Length of each price attestation in bytes
//
// This field is provided for forwards compatibility. Fields in future may be added in
// an append-only way allowing for parsers to continue to work by parsing only up to
// the fields they know, leaving unread input in the buffer. Code may still need to work
// with the full size of the value however, such as when iterating over lists of attestations,
// for these use-cases the structure size is included as a field.
//
// attestation_size >= 150
uint16 attestationSize;
priceAttestations: PriceAttestation[]
}
0 uint32 magic // constant "P2WH"
4 u16 version
6 u8 payloadId // 2
7 u16 n_attestations
9 u16 attestation_size // >= 150
11 .. price_attestation (Size: attestation_size x [n_attestations])
*/
export const PYTH_PRICE_ATTESTATION_MIN_LENGTH: number = 150;
export const PYTH_BATCH_PRICE_ATTESTATION_MIN_LENGTH: number =
11 + PYTH_PRICE_ATTESTATION_MIN_LENGTH;
async function importWasm() {
if (!_P2W_WASM) {
if (typeof window === 'undefined') {
_P2W_WASM = await import("./solana/p2w-core/nodejs/p2w_sdk");
} else {
_P2W_WASM = await import("./solana/p2w-core/bundler/p2w_sdk");
}
}
return _P2W_WASM;
}
export type Rational = {
value: BigInt;
@ -100,123 +24,35 @@ export type Rational = {
};
export type PriceAttestation = {
magic: number;
version: number;
payloadId: number;
productId: string;
priceId: string;
priceType: number;
price: BigInt;
exponent: number;
emaPrice: Rational;
emaConfidence: Rational;
confidenceInterval: BigInt;
status: number;
corpAct: number;
timestamp: BigInt;
conf: BigInt;
expo: number;
emaPrice: BigInt;
emaConf: BigInt;
status: PriceStatus;
numPublishers: BigInt;
maxNumPublishers: BigInt;
attestationTime: BigInt;
publishTime: BigInt;
prevPublishTime: BigInt;
prevPrice: BigInt;
prevConf: BigInt;
};
export type BatchPriceAttestation = {
magic: number;
version: number;
payloadId: number;
nAttestations: number;
attestationSize: number;
priceAttestations: PriceAttestation[];
};
export const PYTH_MAGIC: number = 0x50325748;
function isPyth(payload: Buffer): boolean {
if (payload.length < 4) return false;
if (
payload[0] === 80 &&
payload[1] === 50 &&
payload[2] === 87 &&
payload[3] === 72
) {
// The numbers correspond to "P2WH"
return true;
}
return false;
}
export function parsePriceAttestation(arr: Buffer): PriceAttestation {
return {
magic: arr.readUInt32BE(0),
version: arr.readUInt16BE(4),
payloadId: arr[6],
productId: arr.slice(7, 7 + 32).toString("hex"),
priceId: arr.slice(39, 39 + 32).toString("hex"),
priceType: arr[71],
price: arr.readBigInt64BE(72),
exponent: arr.readInt32BE(80),
emaPrice: {
value: arr.readBigInt64BE(84),
numerator: arr.readBigInt64BE(92),
denominator: arr.readBigInt64BE(100),
},
emaConfidence: {
value: arr.readBigInt64BE(108),
numerator: arr.readBigInt64BE(116),
denominator: arr.readBigInt64BE(124),
},
confidenceInterval: arr.readBigUInt64BE(132),
status: arr[140],
corpAct: arr[141],
timestamp: arr.readBigUInt64BE(142),
};
}
export function parseBatchPriceAttestation(
export async function parseBatchPriceAttestation(
arr: Buffer
): BatchPriceAttestation {
if (!isPyth(arr)) {
throw new Error(
"Cannot parse payload. Header mismatch: This is not a Pyth 2 Wormhole message"
);
}
): Promise<BatchPriceAttestation> {
if (arr.length < PYTH_BATCH_PRICE_ATTESTATION_MIN_LENGTH) {
throw new Error(
"Cannot parse payload. Payload length is wrong: length: " +
arr.length +
", expected length to be at least:" +
PYTH_BATCH_PRICE_ATTESTATION_MIN_LENGTH
);
}
let wasm = await importWasm();
let rawVal = await wasm.parse_batch_attestation(arr);
const magic = arr.readUInt32BE(0);
const version = arr.readUInt16BE(4);
const payloadId = arr[6];
const nAttestations = arr.readUInt16BE(7);
const attestationSize = arr.readUInt16BE(9);
if (attestationSize < PYTH_PRICE_ATTESTATION_MIN_LENGTH) {
throw new Error(
`Cannot parse payload. Size of attestation ${attestationSize} is less than V2 length ${PYTH_PRICE_ATTESTATION_MIN_LENGTH}`
);
}
let priceAttestations: PriceAttestation[] = [];
let offset = 11;
for (let i = 0; i < nAttestations; i += 1) {
priceAttestations.push(
parsePriceAttestation(arr.subarray(offset, offset + attestationSize))
);
offset += attestationSize;
}
return {
magic,
version,
payloadId,
nAttestations,
attestationSize,
priceAttestations,
};
return rawVal;
}
// Returns a hash of all priceIds within the batch, it can be used to identify whether there is a
@ -233,17 +69,17 @@ export function getBatchAttestationHashKey(
}
export function getBatchSummary(
batchAttestation: BatchPriceAttestation
batch: BatchPriceAttestation
): string {
let abstractRepresentation = {
num_attestations: batchAttestation.nAttestations,
prices: batchAttestation.priceAttestations.map((priceAttestation) => {
num_attestations: batch.priceAttestations.length,
prices: batch.priceAttestations.map((priceAttestation) => {
return {
price_id: priceAttestation.priceId,
price: computePrice(priceAttestation.price, priceAttestation.exponent),
price: computePrice(priceAttestation.price, priceAttestation.expo),
conf: computePrice(
priceAttestation.confidenceInterval,
priceAttestation.exponent
priceAttestation.conf,
priceAttestation.expo
),
};
}),
@ -260,34 +96,22 @@ export async function getSignedAttestation(host: string, p2w_addr: string, seque
export function priceAttestationToPriceFeed(priceAttestation: PriceAttestation): PriceFeed {
let status;
if (priceAttestation.status === 0) {
status = PriceStatus.Unknown;
} else if (priceAttestation.status === 1) {
status = PriceStatus.Trading;
} else if (priceAttestation.status === 2) {
status = PriceStatus.Halted;
} else if (priceAttestation.status === 3) {
status = PriceStatus.Auction;
} else {
throw(new Error(`Invalid attestation status: ${priceAttestation.status}`));
}
// FIXME: populate 0 fields once they are in priceAttestation
return new PriceFeed({
conf: priceAttestation.confidenceInterval.toString(),
emaConf: priceAttestation.emaConfidence.value.toString(),
emaPrice: priceAttestation.emaPrice.value.toString(),
expo: priceAttestation.exponent,
conf: priceAttestation.conf.toString(),
emaConf: priceAttestation.emaConf.toString(),
emaPrice: priceAttestation.emaPrice.toString(),
expo: priceAttestation.expo as any,
id: priceAttestation.priceId,
maxNumPublishers: 0,
numPublishers: 0,
prevConf: "0",
prevPrice: "0",
prevPublishTime: 0,
maxNumPublishers: priceAttestation.maxNumPublishers as any,
numPublishers: priceAttestation.numPublishers as any,
prevConf: priceAttestation.prevConf.toString(),
prevPrice: priceAttestation.prevPrice.toString(),
prevPublishTime: priceAttestation.prevPublishTime as any,
price: priceAttestation.price.toString(),
productId: priceAttestation.productId,
publishTime: 0,
status
publishTime: priceAttestation.publishTime as any,
status: priceAttestation.status,
})
}

View File

@ -6,18 +6,19 @@
//! similar human-readable names and provide a failsafe for some of
//! the probable adversarial scenarios.
use serde::{
Deserialize,
Serialize,
Serializer,
};
use std::borrow::Borrow;
use std::convert::TryInto;
use std::io::Read;
use std::iter::Iterator;
use std::mem;
use pyth_sdk_solana::state::{
CorpAction,
PriceStatus,
PriceType,
Rational,
};
use pyth_sdk_solana::state::PriceStatus;
#[cfg(feature = "solana")]
use solitaire::{
@ -32,13 +33,25 @@ use solana_program::pubkey::Pubkey;
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
pub mod wasm;
#[cfg(feature = "wasm")]
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
use wasm_bindgen::prelude::*;
pub type ErrBox = Box<dyn std::error::Error>;
/// Precedes every message implementing the p2w serialization format
pub const P2W_MAGIC: &'static [u8] = b"P2WH";
/// Format version used and understood by this codebase
pub const P2W_FORMAT_VERSION: u16 = 2;
pub const P2W_FORMAT_VER_MAJOR: u16 = 3;
/// Starting with v3, format introduces a minor version to mark forward-compatible iterations
pub const P2W_FORMAT_VER_MINOR: u16 = 0;
/// Starting with v3, format introduces append-only
/// forward-compatibility to the header. This is the current number of
/// bytes after the hdr_size field.
pub const P2W_FORMAT_HDR_SIZE: u16 = 1;
pub const PUBKEY_LEN: usize = 32;
@ -60,32 +73,46 @@ pub enum PayloadId {
/// Important: For maximum security, *both* product_id and price_id
/// should be used as storage keys for known attestations in target
/// chain logic.
#[derive(Clone, Default, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
///
/// NOTE(2022-04-25): the serde attributes help prevent math errors,
/// and no less annoying low-effort serialization override method is known.
#[derive(Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PriceAttestation {
pub product_id: Pubkey,
pub price_id: Pubkey,
pub price_type: PriceType,
pub price: i64,
pub expo: i32,
pub ema_price: Rational,
pub ema_conf: Rational,
pub confidence_interval: u64,
pub status: PriceStatus,
pub corp_act: CorpAction,
// TODO(2022-04-07) format v3: Rename this aptly named timestamp
// field to attestation_time (it's a grey area in terms of
// compatibility and v3 is due very soon either way)
/// NOTE: SOL on-chain time of attestation
pub timestamp: UnixTimestamp,
pub num_publishers: u32,
pub max_num_publishers: u32,
pub publish_time: UnixTimestamp,
pub prev_publish_time: UnixTimestamp,
pub prev_price: i64,
pub prev_conf: u64,
pub product_id: Pubkey,
pub price_id: Pubkey,
#[serde(serialize_with = "use_to_string")]
pub price: i64,
#[serde(serialize_with = "use_to_string")]
pub conf: u64,
pub expo: i32,
#[serde(serialize_with = "use_to_string")]
pub ema_price: i64,
#[serde(serialize_with = "use_to_string")]
pub ema_conf: u64,
pub status: PriceStatus,
pub num_publishers: u32,
pub max_num_publishers: u32,
pub attestation_time: UnixTimestamp,
pub publish_time: UnixTimestamp,
pub prev_publish_time: UnixTimestamp,
#[serde(serialize_with = "use_to_string")]
pub prev_price: i64,
#[serde(serialize_with = "use_to_string")]
pub prev_conf: u64,
}
/// Helper allowing ToString implementers to be serialized as strings accordingly
pub fn use_to_string<T, S>(val: &T, s: S) -> Result<S::Ok, S::Error>
where
T: ToString,
S: Serializer,
{
s.serialize_str(&val.to_string())
}
#[derive(Clone, Default, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BatchPriceAttestation {
pub price_attestations: Vec<PriceAttestation>,
}
@ -98,8 +125,14 @@ impl BatchPriceAttestation {
// magic
let mut buf = P2W_MAGIC.to_vec();
// version
buf.extend_from_slice(&P2W_FORMAT_VERSION.to_be_bytes()[..]);
// major_version
buf.extend_from_slice(&P2W_FORMAT_VER_MAJOR.to_be_bytes()[..]);
// minor_version
buf.extend_from_slice(&P2W_FORMAT_VER_MINOR.to_be_bytes()[..]);
// hdr_size
buf.extend_from_slice(&P2W_FORMAT_HDR_SIZE.to_be_bytes()[..]);
// payload_id
buf.push(PayloadId::PriceBatchAttestation as u8);
@ -154,20 +187,46 @@ impl BatchPriceAttestation {
.into());
}
let mut version_vec = vec![0u8; mem::size_of_val(&P2W_FORMAT_VERSION)];
bytes.read_exact(version_vec.as_mut_slice())?;
let version = u16::from_be_bytes(version_vec.as_slice().try_into()?);
let mut major_version_vec = vec![0u8; mem::size_of_val(&P2W_FORMAT_VER_MAJOR)];
bytes.read_exact(major_version_vec.as_mut_slice())?;
let major_version = u16::from_be_bytes(major_version_vec.as_slice().try_into()?);
if version != P2W_FORMAT_VERSION {
// Major must match exactly
if major_version != P2W_FORMAT_VER_MAJOR {
return Err(format!(
"Unsupported format version {}, expected {}",
version, P2W_FORMAT_VERSION
"Unsupported format major_version {}, expected {}",
major_version, P2W_FORMAT_VER_MAJOR
)
.into());
}
let mut minor_version_vec = vec![0u8; mem::size_of_val(&P2W_FORMAT_VER_MINOR)];
bytes.read_exact(minor_version_vec.as_mut_slice())?;
let minor_version = u16::from_be_bytes(minor_version_vec.as_slice().try_into()?);
// Only older minors are not okay for this codebase
if minor_version < P2W_FORMAT_VER_MINOR {
return Err(format!(
"Unsupported format minor_version {}, expected {} or more",
minor_version, P2W_FORMAT_VER_MINOR
)
.into());
}
// Read header size value
let mut hdr_size_vec = vec![0u8; mem::size_of_val(&P2W_FORMAT_HDR_SIZE)];
bytes.read_exact(hdr_size_vec.as_mut_slice())?;
let hdr_size = u16::from_be_bytes(hdr_size_vec.as_slice().try_into()?);
// Consume the declared number of remaining header
// bytes. Remaining header fields must be read from hdr_buf
let mut hdr_buf = vec![0u8; hdr_size as usize];
bytes.read_exact(hdr_buf.as_mut_slice())?;
let mut payload_id_vec = vec![0u8; mem::size_of::<PayloadId>()];
bytes.read_exact(payload_id_vec.as_mut_slice())?;
hdr_buf
.as_slice()
.read_exact(payload_id_vec.as_mut_slice())?;
if payload_id_vec[0] != PayloadId::PriceBatchAttestation as u8 {
return Err(format!(
@ -178,6 +237,7 @@ impl BatchPriceAttestation {
.into());
}
// Header consumed, continue with remaining fields
let mut batch_len_vec = vec![0u8; 2];
bytes.read_exact(batch_len_vec.as_mut_slice())?;
let batch_len = u16::from_be_bytes(batch_len_vec.as_slice().try_into()?);
@ -192,8 +252,6 @@ impl BatchPriceAttestation {
let mut attestation_buf = vec![0u8; attestation_size as usize];
bytes.read_exact(attestation_buf.as_mut_slice())?;
dbg!(&attestation_buf.len());
match PriceAttestation::deserialize(attestation_buf.as_slice()) {
Ok(attestation) => ret.push(attestation),
Err(e) => {
@ -208,35 +266,6 @@ impl BatchPriceAttestation {
}
}
pub fn serialize_rational(rational: &Rational) -> Vec<u8> {
let mut v = vec![];
// val
v.extend(&rational.val.to_be_bytes()[..]);
// numer
v.extend(&rational.numer.to_be_bytes()[..]);
// denom
v.extend(&rational.denom.to_be_bytes()[..]);
v
}
pub fn deserialize_rational(mut bytes: impl Read) -> Result<Rational, ErrBox> {
let mut val_vec = vec![0u8; mem::size_of::<i64>()];
bytes.read_exact(val_vec.as_mut_slice())?;
let val = i64::from_be_bytes(val_vec.as_slice().try_into()?);
let mut numer_vec = vec![0u8; mem::size_of::<i64>()];
bytes.read_exact(numer_vec.as_mut_slice())?;
let numer = i64::from_be_bytes(numer_vec.as_slice().try_into()?);
let mut denom_vec = vec![0u8; mem::size_of::<i64>()];
bytes.read_exact(denom_vec.as_mut_slice())?;
let denom = i64::from_be_bytes(denom_vec.as_slice().try_into()?);
Ok(Rational { val, numer, denom })
}
// On-chain data types
@ -251,21 +280,19 @@ impl PriceAttestation {
Ok(PriceAttestation {
product_id: Pubkey::new(&price.prod.val[..]),
price_id,
price_type: price.ptype,
price: price.agg.price,
ema_price: price.ema_price,
ema_conf: price.ema_conf,
conf: price.agg.conf,
expo: price.expo,
confidence_interval: price.agg.conf,
status: price.agg.status,
corp_act: price.agg.corp_act,
timestamp: attestation_time,
ema_price: price.ema_price.val,
ema_conf: price.ema_conf.val as u64,
status: price.agg.status.into(),
num_publishers: price.num_qt,
max_num_publishers: price.num,
publish_time: price.timestamp,
prev_publish_time: price.prev_timestamp,
prev_price: price.prev_price,
prev_conf: price.prev_conf,
attestation_time,
publish_time: price.timestamp,
prev_publish_time: price.prev_timestamp,
prev_price: price.prev_price,
prev_conf: price.prev_conf,
})
}
@ -276,31 +303,22 @@ impl PriceAttestation {
let PriceAttestation {
product_id,
price_id,
price_type,
price,
conf,
expo,
ema_price,
ema_conf,
confidence_interval,
status,
corp_act,
timestamp,
num_publishers,
max_num_publishers,
publish_time,
prev_publish_time,
prev_price,
prev_conf
attestation_time,
publish_time,
prev_publish_time,
prev_price,
prev_conf,
} = self;
// magic
let mut buf = P2W_MAGIC.to_vec();
// version
buf.extend_from_slice(&P2W_FORMAT_VERSION.to_be_bytes()[..]);
// payload_id
buf.push(PayloadId::PriceAttestation as u8);
let mut buf = Vec::new();
// product_id
buf.extend_from_slice(&product_id.to_bytes()[..]);
@ -308,90 +326,48 @@ impl PriceAttestation {
// price_id
buf.extend_from_slice(&price_id.to_bytes()[..]);
// price_type
buf.push(price_type.clone() as u8);
// price
buf.extend_from_slice(&price.to_be_bytes()[..]);
// exponent
// conf
buf.extend_from_slice(&conf.to_be_bytes()[..]);
// expo
buf.extend_from_slice(&expo.to_be_bytes()[..]);
// ema_price
buf.append(&mut serialize_rational(&ema_price));
buf.extend_from_slice(&ema_price.to_be_bytes()[..]);
// ema_conf
buf.append(&mut serialize_rational(&ema_conf));
// confidence_interval
buf.extend_from_slice(&confidence_interval.to_be_bytes()[..]);
buf.extend_from_slice(&ema_conf.to_be_bytes()[..]);
// status
buf.push(status.clone() as u8);
// corp_act
buf.push(corp_act.clone() as u8);
// timestamp
buf.extend_from_slice(&timestamp.to_be_bytes()[..]);
// num_publishers
buf.extend_from_slice(&num_publishers.to_be_bytes()[..]);
// max_num_publishers
buf.extend_from_slice(&max_num_publishers.to_be_bytes()[..]);
// publish_time
// attestation_time
buf.extend_from_slice(&attestation_time.to_be_bytes()[..]);
// publish_time
buf.extend_from_slice(&publish_time.to_be_bytes()[..]);
// prev_publish_time
// prev_publish_time
buf.extend_from_slice(&prev_publish_time.to_be_bytes()[..]);
// prev_price
buf.extend_from_slice(&prev_price.to_be_bytes()[..]);
// prev_conf
// prev_conf
buf.extend_from_slice(&prev_conf.to_be_bytes()[..]);
buf
}
pub fn deserialize(mut bytes: impl Read) -> Result<Self, ErrBox> {
let mut magic_vec = vec![0u8; P2W_MAGIC.len()];
bytes.read_exact(magic_vec.as_mut_slice())?;
if magic_vec.as_slice() != P2W_MAGIC {
return Err(format!(
"Invalid magic {:02X?}, expected {:02X?}",
magic_vec, P2W_MAGIC,
)
.into());
}
let mut version_vec = vec![0u8; mem::size_of_val(&P2W_FORMAT_VERSION)];
bytes.read_exact(version_vec.as_mut_slice())?;
let version = u16::from_be_bytes(version_vec.as_slice().try_into()?);
if version != P2W_FORMAT_VERSION {
return Err(format!(
"Unsupported format version {}, expected {}",
version, P2W_FORMAT_VERSION
)
.into());
}
let mut payload_id_vec = vec![0u8; mem::size_of::<PayloadId>()];
bytes.read_exact(payload_id_vec.as_mut_slice())?;
if PayloadId::PriceAttestation as u8 != payload_id_vec[0] {
return Err(format!(
"Invalid Payload ID {}, expected {}",
payload_id_vec[0],
PayloadId::PriceAttestation as u8,
)
.into());
}
let mut product_id_vec = vec![0u8; PUBKEY_LEN];
bytes.read_exact(product_id_vec.as_mut_slice())?;
let product_id = Pubkey::new(product_id_vec.as_slice());
@ -400,32 +376,25 @@ impl PriceAttestation {
bytes.read_exact(price_id_vec.as_mut_slice())?;
let price_id = Pubkey::new(price_id_vec.as_slice());
let mut price_type_vec = vec![0u8];
bytes.read_exact(price_type_vec.as_mut_slice())?;
let price_type = match price_type_vec[0] {
a if a == PriceType::Price as u8 => PriceType::Price,
a if a == PriceType::Unknown as u8 => PriceType::Unknown,
other => {
return Err(format!("Invalid price_type value {}", other).into());
}
};
let mut price_vec = vec![0u8; mem::size_of::<i64>()];
bytes.read_exact(price_vec.as_mut_slice())?;
let price = i64::from_be_bytes(price_vec.as_slice().try_into()?);
let mut conf_vec = vec![0u8; mem::size_of::<u64>()];
bytes.read_exact(conf_vec.as_mut_slice())?;
let conf = u64::from_be_bytes(conf_vec.as_slice().try_into()?);
let mut expo_vec = vec![0u8; mem::size_of::<i32>()];
bytes.read_exact(expo_vec.as_mut_slice())?;
let expo = i32::from_be_bytes(expo_vec.as_slice().try_into()?);
let ema_price = deserialize_rational(&mut bytes)?;
let ema_conf = deserialize_rational(&mut bytes)?;
let mut ema_price_vec = vec![0u8; mem::size_of::<i64>()];
bytes.read_exact(ema_price_vec.as_mut_slice())?;
let ema_price = i64::from_be_bytes(ema_price_vec.as_slice().try_into()?);
println!("twac OK");
let mut confidence_interval_vec = vec![0u8; mem::size_of::<u64>()];
bytes.read_exact(confidence_interval_vec.as_mut_slice())?;
let confidence_interval =
u64::from_be_bytes(confidence_interval_vec.as_slice().try_into()?);
let mut ema_conf_vec = vec![0u8; mem::size_of::<u64>()];
bytes.read_exact(ema_conf_vec.as_mut_slice())?;
let ema_conf = u64::from_be_bytes(ema_conf_vec.as_slice().try_into()?);
let mut status_vec = vec![0u8];
bytes.read_exact(status_vec.as_mut_slice())?;
@ -439,19 +408,6 @@ impl PriceAttestation {
}
};
let mut corp_act_vec = vec![0u8];
bytes.read_exact(corp_act_vec.as_mut_slice())?;
let corp_act = match corp_act_vec[0] {
a if a == CorpAction::NoCorpAct as u8 => CorpAction::NoCorpAct,
other => {
return Err(format!("Invalid corp_act value {}", other).into());
}
};
let mut timestamp_vec = vec![0u8; mem::size_of::<UnixTimestamp>()];
bytes.read_exact(timestamp_vec.as_mut_slice())?;
let timestamp = UnixTimestamp::from_be_bytes(timestamp_vec.as_slice().try_into()?);
let mut num_publishers_vec = vec![0u8; mem::size_of::<u32>()];
bytes.read_exact(num_publishers_vec.as_mut_slice())?;
let num_publishers = u32::from_be_bytes(num_publishers_vec.as_slice().try_into()?);
@ -460,13 +416,19 @@ impl PriceAttestation {
bytes.read_exact(max_num_publishers_vec.as_mut_slice())?;
let max_num_publishers = u32::from_be_bytes(max_num_publishers_vec.as_slice().try_into()?);
let mut attestation_time_vec = vec![0u8; mem::size_of::<UnixTimestamp>()];
bytes.read_exact(attestation_time_vec.as_mut_slice())?;
let attestation_time =
UnixTimestamp::from_be_bytes(attestation_time_vec.as_slice().try_into()?);
let mut publish_time_vec = vec![0u8; mem::size_of::<UnixTimestamp>()];
bytes.read_exact(publish_time_vec.as_mut_slice())?;
let publish_time = UnixTimestamp::from_be_bytes(publish_time_vec.as_slice().try_into()?);
let mut prev_publish_time_vec = vec![0u8; mem::size_of::<UnixTimestamp>()];
bytes.read_exact(prev_publish_time_vec.as_mut_slice())?;
let prev_publish_time = UnixTimestamp::from_be_bytes(prev_publish_time_vec.as_slice().try_into()?);
let prev_publish_time =
UnixTimestamp::from_be_bytes(prev_publish_time_vec.as_slice().try_into()?);
let mut prev_price_vec = vec![0u8; mem::size_of::<i64>()];
bytes.read_exact(prev_price_vec.as_mut_slice())?;
@ -479,21 +441,19 @@ impl PriceAttestation {
Ok(Self {
product_id,
price_id,
price_type,
price,
conf,
expo,
ema_price,
ema_conf,
confidence_interval,
status,
corp_act,
timestamp,
status: status.into(),
num_publishers,
max_num_publishers,
publish_time,
prev_publish_time,
prev_price,
prev_conf,
attestation_time,
publish_time,
prev_publish_time,
prev_price,
prev_conf,
})
}
}
@ -504,41 +464,27 @@ impl PriceAttestation {
#[cfg(test)]
mod tests {
use super::*;
use pyth_sdk_solana::state::{
PriceStatus,
PriceType,
Rational,
};
use pyth_sdk_solana::state::PriceStatus;
fn mock_attestation(prod: Option<[u8; 32]>, price: Option<[u8; 32]>) -> PriceAttestation {
let product_id_bytes = prod.unwrap_or([21u8; 32]);
let price_id_bytes = price.unwrap_or([222u8; 32]);
PriceAttestation {
product_id: Pubkey::new_from_array(product_id_bytes),
price_id: Pubkey::new_from_array(price_id_bytes),
price: 0x2bad2feed7,
price_type: PriceType::Price,
ema_price: Rational {
val: -42,
numer: 15,
denom: 37,
},
ema_conf: Rational {
val: 42,
numer: 1111,
denom: 2222,
},
expo: -3,
status: PriceStatus::Trading,
confidence_interval: 101,
corp_act: CorpAction::NoCorpAct,
timestamp: (0xdeadbeeffadedeedu64) as i64,
num_publishers: 123212u32,
max_num_publishers: 321232u32,
publish_time: 0xdeadbeefi64,
prev_publish_time: 0xdeadbabei64,
prev_price: 0xdeadfacebeefi64,
prev_conf: 0xbadbadbeefu64, // I could do this all day -SD
product_id: Pubkey::new_from_array(product_id_bytes),
price_id: Pubkey::new_from_array(price_id_bytes),
price: 0x2bad2feed7,
conf: 101,
ema_price: -42,
ema_conf: 42,
expo: -3,
status: PriceStatus::Trading.into(),
num_publishers: 123212u32,
max_num_publishers: 321232u32,
attestation_time: (0xdeadbeeffadedeedu64) as i64,
publish_time: 0xdeadbeefi64,
prev_publish_time: 0xdeadbabei64,
prev_price: 0xdeadfacebeefi64,
prev_conf: 0xbadbadbeefu64, // I could do this all day -SD
}
}
@ -574,12 +520,18 @@ mod tests {
#[test]
fn test_batch_serde() -> Result<(), ErrBox> {
let attestations: Vec<_> = (1..=10)
.map(|i| mock_attestation(Some([(i % 256) as u8; 32]), Some([(255 - (i % 256)) as u8; 32])))
.map(|i| {
mock_attestation(
Some([(i % 256) as u8; 32]),
Some([(255 - (i % 256)) as u8; 32]),
)
})
.collect();
let batch_attestation = BatchPriceAttestation {
price_attestations: attestations,
};
println!("Batch hex struct: {:#02X?}", batch_attestation);
let serialized = batch_attestation.serialize()?;
println!("Batch hex Bytes: {:02X?}", serialized);

View File

@ -145,7 +145,7 @@ export class Listener implements PriceFeedPriceInfo {
let batchAttestation;
try {
batchAttestation = parseBatchPriceAttestation(
batchAttestation = await parseBatchPriceAttestation(
Buffer.from(parsedVAA.payload)
);
} catch (e: any) {