66#include "common/merkle_tree.fc";
77#include "common/governance_actions.fc";
88#include "common/gas.fc";
9+ #include "common/op.fc";
910#include "./Wormhole.fc";
1011
1112cell store_price(int price, int conf, int expo, int publish_time) {
@@ -156,16 +157,7 @@ int parse_pyth_payload_in_wormhole_vm(slice payload) impure {
156157 return payload~load_uint(160); ;; Return root_digest
157158}
158159
159-
160- () update_price_feeds(int msg_value, slice data) impure {
161- load_data();
162- slice cs = read_and_verify_header(data);
163-
164- int wormhole_proof_size_bytes = cs~load_uint(16);
165- (cell wormhole_proof, slice new_cs) = read_and_store_large_data(cs, wormhole_proof_size_bytes * 8);
166- cs = new_cs;
167-
168- int num_updates = cs~load_uint(8);
160+ () calculate_and_validate_fees(int msg_value, int num_updates) impure {
169161 int update_fee = single_update_fee * num_updates;
170162 int compute_fee = get_compute_fee(
171163 WORKCHAIN,
@@ -176,30 +168,264 @@ int parse_pyth_payload_in_wormhole_vm(slice payload) impure {
176168
177169 ;; Check if the sender has sent enough TON to cover the update_fee
178170 throw_unless(ERROR_INSUFFICIENT_FEE, remaining_msg_value >= update_fee);
171+ }
172+
173+ (int) find_price_id_index(tuple price_ids, int price_id) {
174+ int len = price_ids.tlen();
175+ int i = 0;
176+ while (i < len) {
177+ if (price_ids.at(i) == price_id) {
178+ return i;
179+ }
180+ i += 1;
181+ }
182+ return -1; ;; Not found
183+ }
184+
185+
186+ tuple parse_price_feeds_from_data(int msg_value, slice data, tuple price_ids, int min_publish_time, int max_publish_time, int unique) {
187+ slice cs = read_and_verify_header(data);
188+
189+ int wormhole_proof_size_bytes = cs~load_uint(16);
190+ (cell wormhole_proof, slice new_cs) = read_and_store_large_data(cs, wormhole_proof_size_bytes * 8);
191+ cs = new_cs;
192+
193+ int num_updates = cs~load_uint(8);
194+
195+ calculate_and_validate_fees(msg_value, num_updates);
179196
180197 (_, _, _, _, int emitter_chain_id, int emitter_address, _, _, slice payload, _) = parse_and_verify_wormhole_vm(wormhole_proof.begin_parse());
181198
182199 ;; Check if the data source is valid
183200 cell data_source = begin_cell()
184201 .store_uint(emitter_chain_id, 16)
185202 .store_uint(emitter_address, 256)
186- .end_cell();
203+ .end_cell();
187204
188205 ;; Dictionary doesn't support cell as key, so we use cell_hash to create a 256-bit integer key
189206 int data_source_key = cell_hash(data_source);
190-
191207 (slice value, int found?) = is_valid_data_source.udict_get?(256, data_source_key);
192208 throw_unless(ERROR_UPDATE_DATA_SOURCE_NOT_FOUND, found?);
193209 int valid = value~load_int(1);
194210 throw_unless(ERROR_INVALID_UPDATE_DATA_SOURCE, valid);
195211
196-
197212 int root_digest = parse_pyth_payload_in_wormhole_vm(payload);
198213
214+ ;; Create dictionary to store price feeds in order (dict has a udict_get_next? method which returns the next key in order)
215+ cell ordered_feeds = new_dict();
216+ ;; Track which price IDs we've found
217+ cell found_price_ids = new_dict();
218+
219+ int index = 0;
220+
199221 repeat(num_updates) {
200- (int price_id, int price, int conf, int expo, int publish_time, _ , int ema_price, int ema_conf, slice new_cs) = read_and_verify_message(cs, root_digest);
222+ (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);
201223 cs = new_cs;
202224
225+ int price_ids_len = price_ids.tlen();
226+
227+ ;; Check if we've already processed this price_id to avoid duplicates
228+ (_, int already_processed?) = found_price_ids.udict_get?(256, price_id);
229+ if (~ already_processed?) { ;; Only process if we haven't seen this price_id yet
230+ int should_include = (price_ids_len == 0)
231+ | ((price_ids_len > 0)
232+ & (publish_time >= min_publish_time)
233+ & (publish_time <= max_publish_time)
234+ & ((unique == 0) | (min_publish_time > prev_publish_time)));
235+
236+ if (should_include) {
237+ ;; Create price feed cell containing both current and EMA prices
238+ cell price_feed_cell = begin_cell()
239+ .store_ref(store_price(price, conf, expo, publish_time))
240+ .store_ref(store_price(ema_price, ema_conf, expo, publish_time))
241+ .end_cell();
242+
243+ if (price_ids_len == 0) {
244+ ordered_feeds~udict_set(8, index, begin_cell()
245+ .store_uint(price_id, 256)
246+ .store_ref(price_feed_cell)
247+ .end_cell().begin_parse());
248+ index += 1;
249+ } else {
250+ index = find_price_id_index(price_ids, price_id);
251+ if (index >= 0) {
252+ ordered_feeds~udict_set(8, index, begin_cell()
253+ .store_uint(price_id, 256)
254+ .store_ref(price_feed_cell)
255+ .end_cell().begin_parse());
256+ }
257+ }
258+
259+ ;; Mark this price ID as found
260+ found_price_ids~udict_set(256, price_id, begin_cell().store_int(true, 1).end_cell().begin_parse());
261+ }
262+ }
263+ }
264+
265+ throw_if(ERROR_INVALID_UPDATE_DATA_LENGTH, ~ cs.slice_empty?());
266+
267+ ;; Verify all requested price IDs were found
268+ if (price_ids.tlen() > 0) {
269+ int i = 0;
270+ repeat(price_ids.tlen()) {
271+ int requested_id = price_ids.at(i);
272+ (_, int found?) = found_price_ids.udict_get?(256, requested_id);
273+ throw_unless(ERROR_PRICE_FEED_NOT_FOUND_WITHIN_RANGE, found?);
274+ i += 1;
275+ }
276+ }
277+
278+ ;; Create final ordered tuple from dictionary
279+ tuple price_feeds = empty_tuple();
280+ int index = -1;
281+ do {
282+ (index, slice value, int success) = ordered_feeds.udict_get_next?(8, index);
283+ if (success) {
284+ tuple price_feed = empty_tuple();
285+ price_feed~tpush(value~load_uint(256)); ;; price_id
286+ price_feed~tpush(value~load_ref()); ;; price_feed_cell
287+ price_feeds~tpush(price_feed);
288+ }
289+ } until (~ success);
290+
291+ return price_feeds;
292+ }
293+
294+ ;; Creates a chain of cells from price feeds, with each cell containing exactly one price_id (256 bits)
295+ ;; and one ref to the price feed cell. Returns the head of the chain.
296+ ;; Each cell now contains exactly:
297+ ;; - One price_id (256 bits)
298+ ;; - One ref to price_feed_cell
299+ ;; - One optional ref to next cell in chain
300+ ;; This approach is:
301+ ;; - More consistent with TON's cell model
302+ ;; - Easier to traverse and read individual price feeds
303+ ;; - Cleaner separation of data
304+ ;; - More predictable in terms of cell structure
305+ cell create_price_feed_cell_chain(tuple price_feeds) {
306+ cell result = null();
307+
308+ int i = price_feeds.tlen() - 1;
309+ while (i >= 0) {
310+ tuple price_feed = price_feeds.at(i);
311+ int price_id = price_feed.at(0);
312+ cell price_feed_cell = price_feed.at(1);
313+
314+ ;; Create new cell with single price feed and chain to previous result
315+ builder current_builder = begin_cell()
316+ .store_uint(price_id, 256) ;; Store price_id
317+ .store_ref(price_feed_cell); ;; Store price data ref
318+
319+ ;; Chain to previous cells if they exist
320+ if (~ cell_null?(result)) {
321+ current_builder = current_builder.store_ref(result);
322+ }
323+
324+ result = current_builder.end_cell();
325+ i -= 1;
326+ }
327+
328+ return result;
329+ }
330+
331+ () send_price_feeds_response(tuple price_feeds, int msg_value, int op, slice sender_address) impure {
332+ ;; Build response cell with price feeds
333+ builder response = begin_cell()
334+ .store_uint(op, 32) ;; Response op
335+ .store_uint(price_feeds.tlen(), 8); ;; Number of price feeds
336+
337+ ;; Create and store price feed cell chain
338+ cell price_feeds_cell = create_price_feed_cell_chain(price_feeds);
339+ response = response.store_ref(price_feeds_cell);
340+
341+ ;; Build the complete message cell (https://docs.ton.org/v3/documentation/smart-contracts/message-management/sending-messages#message-layout)
342+ cell msg = begin_cell()
343+ .store_uint(0x18, 6)
344+ .store_slice(sender_address)
345+ .store_coins(0) ;; Will fill in actual amount after fee calculations
346+ .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1)
347+ .store_ref(response.end_cell())
348+ .end_cell();
349+
350+ int num_price_feeds = price_feeds.tlen();
351+
352+ ;; Number of cells in the message
353+ ;; - 2 cells: msg + response
354+ int cells = 2 + num_price_feeds;
355+
356+ ;; Bit layout per TL-B spec (https://github.com/ton-blockchain/ton/blob/master/crypto/block/block.tlb):
357+ ;; - 6 bits: optimized way of serializing the tag and the first 4 fields
358+ ;; - 256 bits: owner address
359+ ;; - 128 bits: coins (VarUInteger 16) from grams$_ amount:(VarUInteger 16) = Grams
360+ ;; - 107 bits: other data (extra_currencies + ihr_fee + fwd_fee + lt of transaction + unixtime of transaction + no init-field flag + inplace message body flag)
361+ ;; - PRICE_FEED_BITS * num_price_feeds: space for each price feed
362+ int bits = 6 + 256 + 128 + 107 + (PRICE_FEED_BITS * num_price_feeds);
363+ int fwd_fee = get_forward_fee(cells, bits, WORKCHAIN);
364+
365+ ;; Calculate all fees
366+ int compute_fee = get_compute_fee(WORKCHAIN, get_gas_consumed());
367+ int update_fee = single_update_fee * price_feeds.tlen();
368+
369+ ;; Calculate total fees and remaining excess
370+ int total_fees = compute_fee + update_fee + fwd_fee;
371+ int excess = msg_value - total_fees;
372+
373+ ;; Send response message back to sender with exact excess amount
374+ send_raw_message(begin_cell()
375+ .store_uint(0x18, 6)
376+ .store_slice(sender_address)
377+ .store_coins(excess)
378+ .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1)
379+ .store_ref(response.end_cell())
380+ .end_cell(),
381+ 0);
382+ }
383+
384+ () parse_price_feed_updates(int msg_value, slice update_data_slice, slice price_ids_slice, int min_publish_time, int max_publish_time, slice sender_address) impure {
385+ load_data();
386+
387+ ;; Load price_ids tuple
388+ int price_ids_len = price_ids_slice~load_uint(8);
389+ tuple price_ids = empty_tuple();
390+ repeat(price_ids_len) {
391+ int price_id = price_ids_slice~load_uint(256);
392+ price_ids~tpush(price_id);
393+ }
394+
395+ tuple price_feeds = parse_price_feeds_from_data(msg_value, update_data_slice, price_ids, min_publish_time, max_publish_time, false);
396+ send_price_feeds_response(price_feeds, msg_value, OP_PARSE_PRICE_FEED_UPDATES, sender_address);
397+ }
398+
399+ () parse_unique_price_feed_updates(int msg_value, slice update_data_slice, slice price_ids_slice, int publish_time, int max_staleness, slice sender_address) impure {
400+ load_data();
401+
402+ ;; Load price_ids tuple
403+ int price_ids_len = price_ids_slice~load_uint(8);
404+ tuple price_ids = empty_tuple();
405+ repeat(price_ids_len) {
406+ int price_id = price_ids_slice~load_uint(256);
407+ price_ids~tpush(price_id);
408+ }
409+
410+ tuple price_feeds = parse_price_feeds_from_data(msg_value, update_data_slice, price_ids, publish_time, publish_time + max_staleness, true);
411+ send_price_feeds_response(price_feeds, msg_value, OP_PARSE_UNIQUE_PRICE_FEED_UPDATES, sender_address);
412+ }
413+
414+ () update_price_feeds(int msg_value, slice data) impure {
415+ load_data();
416+ tuple price_feeds = parse_price_feeds_from_data(msg_value, data, empty_tuple(), 0, 0, false);
417+ int num_updates = price_feeds.tlen();
418+
419+ int i = 0;
420+ while(i < num_updates) {
421+ tuple price_feed = price_feeds.at(i);
422+ int price_id = price_feed.at(0);
423+ cell price_feed_cell = price_feed.at(1);
424+ slice price_feed = price_feed_cell.begin_parse();
425+ slice price = price_feed~load_ref().begin_parse();
426+ slice ema_price = price_feed~load_ref().begin_parse();
427+ (int price_, int conf, int expo, int publish_time) = parse_price(price);
428+
203429 (slice latest_price_info, int found?) = latest_price_feeds.udict_get?(256, price_id);
204430 int latest_publish_time = 0;
205431 if (found?) {
@@ -213,17 +439,11 @@ int parse_pyth_payload_in_wormhole_vm(slice payload) impure {
213439 }
214440
215441 if (publish_time > latest_publish_time) {
216- cell price_feed = begin_cell()
217- .store_ref(store_price(price, conf, expo, publish_time))
218- .store_ref(store_price(ema_price, ema_conf, expo, publish_time))
219- .end_cell();
220-
221- latest_price_feeds~udict_set(256, price_id, begin_cell().store_ref(price_feed).end_cell().begin_parse());
442+ latest_price_feeds~udict_set(256, price_id, begin_cell().store_ref(price_feed_cell).end_cell().begin_parse());
222443 }
444+ i += 1;
223445 }
224446
225- throw_if(ERROR_INVALID_UPDATE_DATA_LENGTH, ~ cs.slice_empty?());
226-
227447 store_data();
228448}
229449
0 commit comments