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:
parent
5b13be3bbf
commit
a4c749b99c
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
@ -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});
|
||||
|
|
|
@ -153,7 +153,7 @@ async function processVaa(vaaBytes: string) {
|
|||
let batchAttestation;
|
||||
|
||||
try {
|
||||
batchAttestation = parseBatchPriceAttestation(
|
||||
batchAttestation = await parseBatchPriceAttestation(
|
||||
Buffer.from(parsedVAA.payload)
|
||||
);
|
||||
} catch (e: any) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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(×tamp.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);
|
||||
|
|
|
@ -145,7 +145,7 @@ export class Listener implements PriceFeedPriceInfo {
|
|||
let batchAttestation;
|
||||
|
||||
try {
|
||||
batchAttestation = parseBatchPriceAttestation(
|
||||
batchAttestation = await parseBatchPriceAttestation(
|
||||
Buffer.from(parsedVAA.payload)
|
||||
);
|
||||
} catch (e: any) {
|
||||
|
|
Loading…
Reference in New Issue