Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 68 additions & 22 deletions target_chains/ton/contracts/contracts/Pyth.fc
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,19 @@ int parse_pyth_payload_in_wormhole_vm(slice payload) impure {
throw_unless(ERROR_INSUFFICIENT_FEE, remaining_msg_value >= update_fee);
}

(int) find_price_id_index(tuple price_ids, int price_id) {
int len = price_ids.tlen();
int i = 0;
while (i < len) {
if (price_ids.at(i) == price_id) {
return i;
}
i += 1;
}
return -1; ;; Not found
}


tuple parse_price_feeds_from_data(int msg_value, slice data, tuple price_ids, int min_publish_time, int max_publish_time, int unique) {
slice cs = read_and_verify_header(data);

Expand Down Expand Up @@ -198,34 +211,54 @@ tuple parse_price_feeds_from_data(int msg_value, slice data, tuple price_ids, in

int root_digest = parse_pyth_payload_in_wormhole_vm(payload);

;; Create tuple to store price feeds
tuple price_feeds = empty_tuple();
;; Create dictionary to store price feeds in order (dict has a udict_get_next? method which returns the next key in order)
cell ordered_feeds = new_dict();
;; Track which price IDs we've found
cell found_price_ids = new_dict();

int index = 0;

repeat(num_updates) {
(int price_id, int price, int conf, int expo, int publish_time, int prev_publish_time, int ema_price, int ema_conf, slice new_cs) = read_and_verify_message(cs, root_digest);
cs = new_cs;
int should_include = (price_ids.tlen() == 0)
| ((price_ids.tlen() > 0)
& (publish_time >= min_publish_time)
& (publish_time <= max_publish_time)
& ((unique == 0) | (min_publish_time > prev_publish_time)));

if (should_include) {
;; Create price feed cell containing both current and EMA prices
cell price_feed_cell = begin_cell()
.store_ref(store_price(price, conf, expo, publish_time))
.store_ref(store_price(ema_price, ema_conf, expo, publish_time))
.end_cell();

tuple price_feed = empty_tuple();
price_feed~tpush(price_id);
price_feed~tpush(price_feed_cell);
price_feeds~tpush(price_feed);

;; Mark this price ID as found
found_price_ids~udict_set(256, price_id, begin_cell().store_int(true, 1).end_cell().begin_parse());
int price_ids_len = price_ids.tlen();

;; Check if we've already processed this price_id to avoid duplicates
(_, int already_processed?) = found_price_ids.udict_get?(256, price_id);
if (~ already_processed?) { ;; Only process if we haven't seen this price_id yet
int should_include = (price_ids_len == 0)
| ((price_ids_len > 0)
& (publish_time >= min_publish_time)
& (publish_time <= max_publish_time)
& ((unique == 0) | (min_publish_time > prev_publish_time)));

if (should_include) {
;; Create price feed cell containing both current and EMA prices
cell price_feed_cell = begin_cell()
.store_ref(store_price(price, conf, expo, publish_time))
.store_ref(store_price(ema_price, ema_conf, expo, publish_time))
.end_cell();

if (price_ids_len == 0) {
ordered_feeds~udict_set(8, index, begin_cell()
.store_uint(price_id, 256)
.store_ref(price_feed_cell)
.end_cell().begin_parse());
index += 1;
} else {
Comment on lines +243 to +249
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably when having a top level if to return in this case makes the code cleaner.

index = find_price_id_index(price_ids, price_id);
if (index >= 0) {
ordered_feeds~udict_set(8, index, begin_cell()
.store_uint(price_id, 256)
.store_ref(price_feed_cell)
.end_cell().begin_parse());
}
}

;; Mark this price ID as found
found_price_ids~udict_set(256, price_id, begin_cell().store_int(true, 1).end_cell().begin_parse());
}
}
}

Expand All @@ -242,6 +275,19 @@ tuple parse_price_feeds_from_data(int msg_value, slice data, tuple price_ids, in
}
}

;; Create final ordered tuple from dictionary
tuple price_feeds = empty_tuple();
int index = -1;
do {
(index, slice value, int success) = ordered_feeds.udict_get_next?(8, index);
if (success) {
tuple price_feed = empty_tuple();
price_feed~tpush(value~load_uint(256)); ;; price_id
price_feed~tpush(value~load_ref()); ;; price_feed_cell
price_feeds~tpush(price_feed);
}
} until (~ success);

return price_feeds;
}

Expand Down Expand Up @@ -378,7 +424,7 @@ cell create_price_feed_cell_chain(tuple price_feeds) {
slice price_feed = price_feed_cell.begin_parse();
slice price = price_feed~load_ref().begin_parse();
slice ema_price = price_feed~load_ref().begin_parse();
(_, _, _, int publish_time) = parse_price(price);
(int price_, int conf, int expo, int publish_time) = parse_price(price);

(slice latest_price_info, int found?) = latest_price_feeds.udict_get?(256, price_id);
int latest_publish_time = 0;
Expand Down
201 changes: 201 additions & 0 deletions target_chains/ton/contracts/tests/PythTest.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1262,4 +1262,205 @@ describe("PythTest", () => {
exitCode: 2020, // ERROR_PRICE_FEED_NOT_FOUND_WITHIN_RANGE
});
});

it("should successfully parse price feed updates in price ids order", async () => {
await deployContract();
await updateGuardianSets(pythTest, deployer);

const sentValue = toNano("1");
const result = await pythTest.sendParsePriceFeedUpdates(
deployer.getSender(),
Buffer.from(HERMES_BTC_ETH_UPDATE, "hex"),
sentValue,
[ETH_PRICE_FEED_ID, BTC_PRICE_FEED_ID],
HERMES_BTC_PUBLISH_TIME,
HERMES_BTC_PUBLISH_TIME
);

// Verify transaction success and message count
expect(result.transactions).toHaveTransaction({
from: deployer.address,
to: pythTest.address,
success: true,
outMessagesCount: 1,
});

// Get the output message
const outMessage = result.transactions[1].outMessages.values()[0];

// Verify excess value is returned
expect(
(outMessage.info as CommonMessageInfoInternal).value.coins
).toBeGreaterThan(0);

const cs = outMessage.body.beginParse();

// Verify message header
const op = cs.loadUint(32);
expect(op).toBe(5); // OP_PARSE_PRICE_FEED_UPDATES

// Verify number of price feeds
const numPriceFeeds = cs.loadUint(8);
expect(numPriceFeeds).toBe(2); // We expect BTC and ETH price feeds

// Load and verify price feeds
const priceFeedsCell = cs.loadRef();
let currentCell = priceFeedsCell;

// First price feed (ETH)
const ethCs = currentCell.beginParse();
const ethPriceId =
"0x" + ethCs.loadUintBig(256).toString(16).padStart(64, "0");
expect(ethPriceId).toBe(ETH_PRICE_FEED_ID);

const ethPriceFeedCell = ethCs.loadRef();
const ethPriceFeedSlice = ethPriceFeedCell.beginParse();

// Verify ETH current price
const ethCurrentPriceCell = ethPriceFeedSlice.loadRef();
const ethCurrentPrice = ethCurrentPriceCell.beginParse();
expect(ethCurrentPrice.loadInt(64)).toBe(HERMES_ETH_PRICE);
expect(ethCurrentPrice.loadUint(64)).toBe(HERMES_ETH_CONF);
expect(ethCurrentPrice.loadInt(32)).toBe(HERMES_ETH_EXPO);
expect(ethCurrentPrice.loadUint(64)).toBe(HERMES_ETH_PUBLISH_TIME);

// Verify ETH EMA price
const ethEmaPriceCell = ethPriceFeedSlice.loadRef();
const ethEmaPrice = ethEmaPriceCell.beginParse();
expect(ethEmaPrice.loadInt(64)).toBe(HERMES_ETH_EMA_PRICE);
expect(ethEmaPrice.loadUint(64)).toBe(HERMES_ETH_EMA_CONF);
expect(ethEmaPrice.loadInt(32)).toBe(HERMES_ETH_EMA_EXPO);
expect(ethEmaPrice.loadUint(64)).toBe(HERMES_ETH_PUBLISH_TIME);

// Move to ETH price feed
currentCell = ethCs.loadRef();

// Second price feed (BTC)
const btcCs = currentCell.beginParse();
const btcPriceId =
"0x" + btcCs.loadUintBig(256).toString(16).padStart(64, "0");
expect(btcPriceId).toBe(BTC_PRICE_FEED_ID);

const btcPriceFeedCell = btcCs.loadRef();
const btcPriceFeedSlice = btcPriceFeedCell.beginParse();

// Verify BTC current price
const btcCurrentPriceCell = btcPriceFeedSlice.loadRef();
const btcCurrentPrice = btcCurrentPriceCell.beginParse();
expect(btcCurrentPrice.loadInt(64)).toBe(HERMES_BTC_PRICE);
expect(btcCurrentPrice.loadUint(64)).toBe(HERMES_BTC_CONF);
expect(btcCurrentPrice.loadInt(32)).toBe(HERMES_BTC_EXPO);
expect(btcCurrentPrice.loadUint(64)).toBe(HERMES_BTC_PUBLISH_TIME);

// Verify BTC EMA price
const btcEmaPriceCell = btcPriceFeedSlice.loadRef();
const btcEmaPrice = btcEmaPriceCell.beginParse();
expect(btcEmaPrice.loadInt(64)).toBe(HERMES_BTC_EMA_PRICE);
expect(btcEmaPrice.loadUint(64)).toBe(HERMES_BTC_EMA_CONF);
expect(btcEmaPrice.loadInt(32)).toBe(HERMES_BTC_EMA_EXPO);
expect(btcEmaPrice.loadUint(64)).toBe(HERMES_BTC_PUBLISH_TIME);

// Verify this is the end of the chain
expect(ethCs.remainingRefs).toBe(0);
});

it("should successfully parse unique price feed updates in price ids order", async () => {
await deployContract();
await updateGuardianSets(pythTest, deployer);

const sentValue = toNano("1");
const result = await pythTest.sendParseUniquePriceFeedUpdates(
deployer.getSender(),
Buffer.from(HERMES_BTC_ETH_UNIQUE_UPDATE, "hex"),
sentValue,
[ETH_PRICE_FEED_ID, BTC_PRICE_FEED_ID],
HERMES_BTC_PUBLISH_TIME,
60
);

// Verify transaction success and message count
expect(result.transactions).toHaveTransaction({
from: deployer.address,
to: pythTest.address,
success: true,
outMessagesCount: 1,
});

// Get the output message
const outMessage = result.transactions[1].outMessages.values()[0];

// Verify excess value is returned
expect(
(outMessage.info as CommonMessageInfoInternal).value.coins
).toBeGreaterThan(0);

const cs = outMessage.body.beginParse();

// Verify message header
const op = cs.loadUint(32);
expect(op).toBe(6); // OP_PARSE_UNIQUE_PRICE_FEED_UPDATES

// Verify number of price feeds
const numPriceFeeds = cs.loadUint(8);
expect(numPriceFeeds).toBe(2); // We expect BTC and ETH price feeds

// Load and verify price feeds
const priceFeedsCell = cs.loadRef();
let currentCell = priceFeedsCell;

// First price feed (ETH)
const ethCs = currentCell.beginParse();
const ethPriceId =
"0x" + ethCs.loadUintBig(256).toString(16).padStart(64, "0");
expect(ethPriceId).toBe(ETH_PRICE_FEED_ID);

const ethPriceFeedCell = ethCs.loadRef();
const ethPriceFeedSlice = ethPriceFeedCell.beginParse();

// Verify ETH current price
const ethCurrentPriceCell = ethPriceFeedSlice.loadRef();
const ethCurrentPrice = ethCurrentPriceCell.beginParse();
expect(ethCurrentPrice.loadInt(64)).toBe(HERMES_ETH_UNIQUE_PRICE);
expect(ethCurrentPrice.loadUint(64)).toBe(HERMES_ETH_UNIQUE_CONF);
expect(ethCurrentPrice.loadInt(32)).toBe(HERMES_ETH_UNIQUE_EXPO);
expect(ethCurrentPrice.loadUint(64)).toBe(HERMES_ETH_UNIQUE_PUBLISH_TIME);

// Verify ETH EMA price
const ethEmaPriceCell = ethPriceFeedSlice.loadRef();
const ethEmaPrice = ethEmaPriceCell.beginParse();
expect(ethEmaPrice.loadInt(64)).toBe(HERMES_ETH_UNIQUE_EMA_PRICE);
expect(ethEmaPrice.loadUint(64)).toBe(HERMES_ETH_UNIQUE_EMA_CONF);
expect(ethEmaPrice.loadInt(32)).toBe(HERMES_ETH_UNIQUE_EMA_EXPO);
expect(ethEmaPrice.loadUint(64)).toBe(HERMES_ETH_UNIQUE_EMA_PUBLISH_TIME);

currentCell = ethCs.loadRef();

// Second price feed (BTC)
const btcCs = currentCell.beginParse();
const btcPriceId =
"0x" + btcCs.loadUintBig(256).toString(16).padStart(64, "0");
expect(btcPriceId).toBe(BTC_PRICE_FEED_ID);

const btcPriceFeedCell = btcCs.loadRef();
const btcPriceFeedSlice = btcPriceFeedCell.beginParse();

// Verify BTC current price
const btcCurrentPriceCell = btcPriceFeedSlice.loadRef();
const btcCurrentPrice = btcCurrentPriceCell.beginParse();
expect(btcCurrentPrice.loadInt(64)).toBe(HERMES_BTC_UNIQUE_PRICE);
expect(btcCurrentPrice.loadUint(64)).toBe(HERMES_BTC_UNIQUE_CONF);
expect(btcCurrentPrice.loadInt(32)).toBe(HERMES_BTC_UNIQUE_EXPO);
expect(btcCurrentPrice.loadUint(64)).toBe(HERMES_BTC_UNIQUE_PUBLISH_TIME);

// Verify BTC EMA price
const btcEmaPriceCell = btcPriceFeedSlice.loadRef();
const btcEmaPrice = btcEmaPriceCell.beginParse();
expect(btcEmaPrice.loadInt(64)).toBe(HERMES_BTC_UNIQUE_EMA_PRICE);
expect(btcEmaPrice.loadUint(64)).toBe(HERMES_BTC_UNIQUE_EMA_CONF);
expect(btcEmaPrice.loadInt(32)).toBe(HERMES_BTC_UNIQUE_EMA_EXPO);
expect(btcEmaPrice.loadUint(64)).toBe(HERMES_BTC_UNIQUE_EMA_PUBLISH_TIME);

// Verify this is the end of the chain
expect(btcCs.remainingRefs).toBe(0);
});
});
Loading