Skip to content

Commit 55bc1b9

Browse files
authored
[sui 2/x]: state, batch_price_attestation, set (#710)
* state getters and setters, change Move.toml dependency to sui/integration_v2 * finish state.move * add new line to pyth * use deployer cap pattern for state module * indent
1 parent 614d35e commit 55bc1b9

File tree

5 files changed

+413
-1
lines changed

5 files changed

+413
-1
lines changed

target_chains/sui/contracts/Move.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ rev = "157ac72030d014f17d76cefe81f3915b4afab2c9"
1010
[dependencies.Wormhole]
1111
git = "https://github.com/wormhole-foundation/wormhole.git"
1212
subdir = "sui/wormhole"
13-
rev = "sui/wormhole-cleanup"
13+
rev = "sui/integration_v2"
1414

1515
[addresses]
1616
pyth = "0x250"
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
module pyth::batch_price_attestation {
2+
3+
use sui::tx_context::{Self, TxContext};
4+
5+
use pyth::price_feed::{Self};
6+
use pyth::price_info::{Self, PriceInfo};
7+
use pyth::price_identifier::{Self};
8+
use pyth::price_status;
9+
use pyth::deserialize::{Self};
10+
// TODO - Import Sui clock and use it for timekeeping instead of tx_context::epoch.
11+
// Replace epoch in deserialize_price_info with sui clock timestamp, and usage
12+
// of epoch in test_deserialize_batch_price_attestation.
13+
// TODO - Use specific error messages in this module, specifically
14+
// for invalid_attestation_magic_value and invalid_batch_attestation_header_size.
15+
use wormhole::cursor::{Self, Cursor};
16+
use wormhole::bytes::{Self};
17+
18+
use std::vector::{Self};
19+
20+
#[test_only]
21+
use pyth::price;
22+
#[test_only]
23+
use pyth::i64;
24+
25+
const MAGIC: u64 = 0x50325748; // "P2WH" (Pyth2Wormhole) raw ASCII bytes
26+
27+
struct BatchPriceAttestation {
28+
header: Header,
29+
attestation_size: u64,
30+
attestation_count: u64,
31+
price_infos: vector<PriceInfo>,
32+
}
33+
34+
struct Header {
35+
magic: u64,
36+
version_major: u64,
37+
version_minor: u64,
38+
header_size: u64,
39+
payload_id: u8,
40+
}
41+
42+
fun deserialize_header(cur: &mut Cursor<u8>): Header {
43+
let magic = (deserialize::deserialize_u32(cur) as u64);
44+
assert!(magic == MAGIC, 0); // TODO - add specific error value - error::invalid_attestation_magic_value()
45+
let version_major = deserialize::deserialize_u16(cur);
46+
let version_minor = deserialize::deserialize_u16(cur);
47+
let header_size = deserialize::deserialize_u16(cur);
48+
let payload_id = deserialize::deserialize_u8(cur);
49+
50+
assert!(header_size >= 1, 0); // TODO - add specific error value - error::invalid_batch_attestation_header_size()
51+
let unknown_header_bytes = header_size - 1;
52+
let _unknown = bytes::take_bytes(cur, (unknown_header_bytes as u64));
53+
54+
Header {
55+
magic: magic,
56+
header_size: (header_size as u64),
57+
version_minor: (version_minor as u64),
58+
version_major: (version_major as u64),
59+
payload_id: payload_id,
60+
}
61+
}
62+
63+
public fun destroy(batch: BatchPriceAttestation): vector<PriceInfo> {
64+
let BatchPriceAttestation {
65+
header: Header {
66+
magic: _,
67+
version_major: _,
68+
version_minor: _,
69+
header_size: _,
70+
payload_id: _,
71+
},
72+
attestation_size: _,
73+
attestation_count: _,
74+
price_infos,
75+
} = batch;
76+
price_infos
77+
}
78+
79+
public fun get_attestation_count(batch: &BatchPriceAttestation): u64 {
80+
batch.attestation_count
81+
}
82+
83+
public fun get_price_info(batch: &BatchPriceAttestation, index: u64): &PriceInfo {
84+
vector::borrow(&batch.price_infos, index)
85+
}
86+
87+
public fun deserialize(bytes: vector<u8>, ctx: &mut TxContext): BatchPriceAttestation {
88+
let cur = cursor::new(bytes);
89+
let header = deserialize_header(&mut cur);
90+
91+
let attestation_count = deserialize::deserialize_u16(&mut cur);
92+
let attestation_size = deserialize::deserialize_u16(&mut cur);
93+
let price_infos = vector::empty();
94+
95+
let i = 0;
96+
while (i < attestation_count) {
97+
let price_info = deserialize_price_info(&mut cur, ctx);
98+
vector::push_back(&mut price_infos, price_info);
99+
100+
// Consume any excess bytes
101+
let parsed_bytes = 32+32+8+8+4+8+8+1+4+4+8+8+8+8+8;
102+
let _excess = bytes::take_bytes(&mut cur, (attestation_size - parsed_bytes as u64));
103+
104+
i = i + 1;
105+
};
106+
cursor::destroy_empty(cur);
107+
108+
BatchPriceAttestation {
109+
header,
110+
attestation_count: (attestation_count as u64),
111+
attestation_size: (attestation_size as u64),
112+
price_infos: price_infos,
113+
}
114+
}
115+
116+
fun deserialize_price_info(cur: &mut Cursor<u8>, ctx: &mut TxContext): PriceInfo {
117+
118+
// Skip obselete field
119+
let _product_identifier = deserialize::deserialize_vector(cur, 32);
120+
let price_identifier = price_identifier::from_byte_vec(deserialize::deserialize_vector(cur, 32));
121+
let price = deserialize::deserialize_i64(cur);
122+
let conf = deserialize::deserialize_u64(cur);
123+
let expo = deserialize::deserialize_i32(cur);
124+
let ema_price = deserialize::deserialize_i64(cur);
125+
let ema_conf = deserialize::deserialize_u64(cur);
126+
let status = price_status::from_u64((deserialize::deserialize_u8(cur) as u64));
127+
128+
// Skip obselete fields
129+
let _num_publishers = deserialize::deserialize_u32(cur);
130+
let _max_num_publishers = deserialize::deserialize_u32(cur);
131+
132+
let attestation_time = deserialize::deserialize_u64(cur);
133+
let publish_time = deserialize::deserialize_u64(cur); //
134+
let prev_publish_time = deserialize::deserialize_u64(cur);
135+
let prev_price = deserialize::deserialize_i64(cur);
136+
let prev_conf = deserialize::deserialize_u64(cur);
137+
138+
// Handle the case where the status is not trading. This logic will soon be moved into
139+
// the attester.
140+
141+
// If status is trading, use the current price.
142+
// If not, use the the last known trading price.
143+
let current_price = pyth::price::new(price, conf, expo, publish_time);
144+
if (status != price_status::new_trading()) {
145+
current_price = pyth::price::new(prev_price, prev_conf, expo, prev_publish_time);
146+
};
147+
148+
// If status is trading, use the timestamp of the aggregate as the timestamp for the
149+
// EMA price. If not, the EMA will have last been updated when the aggregate last had
150+
// trading status, so use prev_publish_time (the time when the aggregate last had trading status).
151+
let ema_timestamp = publish_time;
152+
if (status != price_status::new_trading()) {
153+
ema_timestamp = prev_publish_time;
154+
};
155+
156+
price_info::new(
157+
attestation_time,
158+
tx_context::epoch(ctx), //TODO - use Sui Clock to get timestamp in seconds
159+
price_feed::new(
160+
price_identifier,
161+
current_price,
162+
pyth::price::new(ema_price, ema_conf, expo, ema_timestamp),
163+
)
164+
)
165+
}
166+
167+
#[test]
168+
#[expected_failure]
169+
fun test_deserialize_batch_price_attestation_invalid_magic() {
170+
use sui::test_scenario::{Self, ctx};
171+
let test = test_scenario::begin(@0x1234);
172+
173+
// A batch price attestation with a magic number of 0x50325749
174+
let bytes = x"5032574900030000000102000400951436e0be37536be96f0896366089506a59763d036728332d3e3038047851aea7c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1000000000000049a0000000000000008fffffffb00000000000005dc0000000000000003000000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000006150000000000000007215258d81468614f6b7e194c5d145609394f67b041e93e6695dcc616faadd0603b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe000000000000041a0000000000000003fffffffb00000000000005cb0000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e4000000000000048600000000000000078ac9cf3ab299af710d735163726fdae0db8465280502eb9f801f74b3c1bd190333832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d00000000000003f20000000000000002fffffffb00000000000005e70000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e40000000000000685000000000000000861db714e9ff987b6fedf00d01f9fea6db7c30632d6fc83b7bc9459d7192bc44a21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db800000000000006cb0000000000000001fffffffb00000000000005e40000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000007970000000000000001";
175+
destroy(deserialize(bytes, ctx(&mut test)));
176+
test_scenario::end(test);
177+
}
178+
179+
#[test]
180+
fun test_deserialize_batch_price_attestation() {
181+
use sui::test_scenario::{Self, ctx};
182+
// Set the arrival time
183+
let test = test_scenario::begin(@0x1234);
184+
let arrival_time = tx_context::epoch(ctx(&mut test));
185+
186+
// A raw batch price attestation
187+
// The first attestation has a status of UNKNOWN
188+
let bytes = x"5032574800030000000102000400951436e0be37536be96f0896366089506a59763d036728332d3e3038047851aea7c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1000000000000049a0000000000000008fffffffb00000000000005dc0000000000000003000000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000006150000000000000007215258d81468614f6b7e194c5d145609394f67b041e93e6695dcc616faadd0603b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe000000000000041a0000000000000003fffffffb00000000000005cb0000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e4000000000000048600000000000000078ac9cf3ab299af710d735163726fdae0db8465280502eb9f801f74b3c1bd190333832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d00000000000003f20000000000000002fffffffb00000000000005e70000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e40000000000000685000000000000000861db714e9ff987b6fedf00d01f9fea6db7c30632d6fc83b7bc9459d7192bc44a21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db800000000000006cb0000000000000001fffffffb00000000000005e40000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000007970000000000000001";
189+
190+
let expected = BatchPriceAttestation {
191+
header: Header {
192+
magic: 0x50325748,
193+
version_major: 3,
194+
version_minor: 0,
195+
payload_id: 2,
196+
header_size: 1,
197+
},
198+
attestation_count: 4,
199+
attestation_size: 149,
200+
price_infos: vector<PriceInfo>[
201+
price_info::new(
202+
1663680747,
203+
arrival_time,
204+
price_feed::new(
205+
price_identifier::from_byte_vec(x"c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1"),
206+
price::new(i64::new(1557, false), 7, i64::new(5, true), 1663680740),
207+
price::new(i64::new(1500, false), 3, i64::new(5, true), 1663680740),
208+
),
209+
),
210+
price_info::new(
211+
1663680747,
212+
arrival_time,
213+
price_feed::new(
214+
price_identifier::from_byte_vec(x"3b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe"),
215+
price::new(i64::new(1050, false), 3, i64::new(5, true), 1663680745),
216+
price::new(i64::new(1483, false), 3, i64::new(5, true), 1663680745),
217+
),
218+
),
219+
price_info::new(
220+
1663680747,
221+
arrival_time,
222+
price_feed::new(
223+
price_identifier::from_byte_vec(x"33832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d"),
224+
price::new(i64::new(1010, false), 2, i64::new(5, true), 1663680745),
225+
price::new(i64::new(1511, false), 3, i64::new(5, true), 1663680745),
226+
),
227+
),
228+
price_info::new(
229+
1663680747,
230+
arrival_time,
231+
price_feed::new(
232+
price_identifier::from_byte_vec(x"21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db8"),
233+
price::new(i64::new(1739, false), 1, i64::new(5, true), 1663680745),
234+
price::new(i64::new(1508, false), 3, i64::new(5, true), 1663680745),
235+
),
236+
),
237+
],
238+
};
239+
240+
let deserialized = deserialize(bytes, ctx(&mut test));
241+
242+
assert!(&expected == &deserialized, 1);
243+
destroy(expected);
244+
destroy(deserialized);
245+
246+
test_scenario::end(test);
247+
}
248+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module pyth::pyth {}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/// A set data structure.
2+
module pyth::set {
3+
use sui::table::{Self, Table};
4+
use sui::tx_context::{TxContext};
5+
use std::vector;
6+
7+
/// Empty struct. Used as the value type in mappings to encode a set
8+
struct Unit has store, copy, drop {}
9+
10+
/// A set containing elements of type `A` with support for membership
11+
/// checking.
12+
struct Set<A: store + copy + drop> has store {
13+
keys: vector<A>,
14+
elems: Table<A, Unit>
15+
}
16+
17+
/// Create a new Set.
18+
public fun new<A: store + copy + drop>(ctx: &mut TxContext): Set<A> {
19+
Set {
20+
keys: vector::empty<A>(),
21+
elems: table::new(ctx),
22+
}
23+
}
24+
25+
/// Add a new element to the set.
26+
/// Aborts if the element already exists
27+
public fun add<A: store + copy + drop>(set: &mut Set<A>, key: A) {
28+
table::add(&mut set.elems, key, Unit {});
29+
vector::push_back(&mut set.keys, key);
30+
}
31+
32+
/// Returns true iff `set` contains an entry for `key`.
33+
public fun contains<A: store + copy + drop>(set: &Set<A>, key: A): bool {
34+
table::contains(&set.elems, key)
35+
}
36+
37+
/// Removes all elements from the set
38+
public fun empty<A: store + copy + drop>(set: &mut Set<A>) {
39+
while (!vector::is_empty(&set.keys)) {
40+
table::remove(&mut set.elems, vector::pop_back(&mut set.keys));
41+
}
42+
}
43+
44+
// TODO: destroy_empty, but this is tricky because std::table doesn't
45+
// have this functionality.
46+
}

0 commit comments

Comments
 (0)