Skip to content

Commit 9c6e207

Browse files
authored
support accumulator update data (#38)
* support accumulator update data * bump * address comments * update comment
1 parent 81af45c commit 9c6e207

File tree

3 files changed

+149
-9
lines changed

3 files changed

+149
-9
lines changed

pythclient/price_feeds.py

+128-8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import base64
22
import binascii
3-
import logging
43
from struct import unpack
5-
from typing import List
4+
from typing import Any, Dict, List, Optional
65

76
from Crypto.Hash import keccak
7+
from loguru import logger
88

99
P2W_FORMAT_MAGIC = "P2WH"
1010
P2W_FORMAT_VER_MAJOR = 3
@@ -13,6 +13,8 @@
1313

1414
DEFAULT_VAA_ENCODING = "hex"
1515

16+
ACCUMULATOR_MAGIC = "504e4155"
17+
1618

1719
class Price:
1820
def __init__(self, conf, expo, price, publish_time):
@@ -110,7 +112,7 @@ def to_dict(self, verbose=False, vaa_format=DEFAULT_VAA_ENCODING):
110112
return result
111113

112114

113-
# Referenced from https://github.com/pyth-network/pyth-crosschain/blob/main/price_service/server/src/encoding.ts#L24
115+
# Referenced from https://github.com/pyth-network/pyth-crosschain/blob/110caed6be3be7885773d2f6070b143cc13fb0ee/price_service/server/src/encoding.ts#L24
114116
def encode_vaa_for_chain(vaa, vaa_format, buffer=False):
115117
# check if vaa is already in vaa_format
116118
if isinstance(vaa, str):
@@ -337,12 +339,9 @@ def parse_price_attestation(bytes_):
337339
}
338340

339341

340-
# Referenced from https://github.com/pyth-network/pyth-crosschain/blob/main/price_service/server/src/rest.ts#L139
342+
# Referenced from https://github.com/pyth-network/pyth-crosschain/blob/110caed6be3be7885773d2f6070b143cc13fb0ee/price_service/server/src/rest.ts#L139
341343
def vaa_to_price_infos(vaa, encoding=DEFAULT_VAA_ENCODING) -> List[PriceInfo]:
342344
parsed_vaa = parse_vaa(vaa, encoding)
343-
344-
# TODO: support accumulators
345-
346345
batch_attestation = parse_batch_price_attestation(parsed_vaa["payload"])
347346
price_infos = []
348347
for price_attestation in batch_attestation["price_attestations"]:
@@ -359,6 +358,8 @@ def vaa_to_price_infos(vaa, encoding=DEFAULT_VAA_ENCODING) -> List[PriceInfo]:
359358

360359

361360
def vaa_to_price_info(price_feed_id, vaa, encoding=DEFAULT_VAA_ENCODING) -> PriceInfo:
361+
if encode_vaa_for_chain(vaa, encoding, buffer=True)[:4].hex() == ACCUMULATOR_MAGIC:
362+
return extract_price_info_from_accumulator_update(price_feed_id, vaa, encoding)
362363
price_infos = vaa_to_price_infos(vaa, encoding)
363364
for price_info in price_infos:
364365
if price_info.price_feed.id == price_feed_id:
@@ -367,7 +368,7 @@ def vaa_to_price_info(price_feed_id, vaa, encoding=DEFAULT_VAA_ENCODING) -> Pric
367368
return None
368369

369370

370-
# Referenced from https://github.com/pyth-network/pyth-crosschain/blob/main/price_service/server/src/listen.ts#L37
371+
# Referenced from https://github.com/pyth-network/pyth-crosschain/blob/110caed6be3be7885773d2f6070b143cc13fb0ee/price_service/server/src/listen.ts#L37
371372
def create_price_info(price_attestation, vaa, sequence, emitter_chain):
372373
price_feed = price_attestation_to_price_feed(price_attestation)
373374
return PriceInfo(
@@ -407,3 +408,122 @@ def price_attestation_to_price_feed(price_attestation):
407408
ema_price.publish_time = price_attestation["prev_publish_time"]
408409

409410
return PriceUpdate(ema_price, price_attestation["price_id"], price)
411+
412+
413+
# Referenced from https://github.com/pyth-network/pyth-crosschain/blob/1a00598334e52fc5faf967eb1170d7fc23ad828b/price_service/server/src/rest.ts#L137
414+
def extract_price_info_from_accumulator_update(
415+
price_feed_id, update_data, encoding
416+
) -> Optional[Dict[str, Any]]:
417+
encoded_update_data = encode_vaa_for_chain(update_data, encoding, buffer=True)
418+
offset = 0
419+
offset += 4 # magic
420+
offset += 1 # major version
421+
offset += 1 # minor version
422+
423+
trailing_header_size = encoded_update_data[offset]
424+
offset += 1 + trailing_header_size
425+
426+
update_type = encoded_update_data[offset]
427+
offset += 1
428+
429+
if update_type != 0:
430+
logger.info(f"Invalid accumulator update type: {update_type}")
431+
return None
432+
433+
vaa_length = int.from_bytes(
434+
encoded_update_data[offset : offset + 2], byteorder="big"
435+
)
436+
offset += 2
437+
438+
vaa_buffer = encoded_update_data[offset : offset + vaa_length]
439+
# convert vaa_buffer to string based on encoding
440+
if encoding == "hex":
441+
vaa_str = vaa_buffer.hex()
442+
elif encoding == "base64":
443+
vaa_str = base64.b64encode(vaa_buffer).decode("ascii")
444+
parsed_vaa = parse_vaa(vaa_str, encoding)
445+
offset += vaa_length
446+
447+
num_updates = encoded_update_data[offset]
448+
offset += 1
449+
450+
for _ in range(num_updates):
451+
message_length = int.from_bytes(
452+
encoded_update_data[offset : offset + 2], byteorder="big"
453+
)
454+
offset += 2
455+
456+
message = encoded_update_data[offset : offset + message_length]
457+
offset += message_length
458+
459+
proof_length = encoded_update_data[offset]
460+
offset += 1
461+
offset += proof_length # ignore proofs
462+
463+
message_offset = 0
464+
message_type = message[message_offset]
465+
message_offset += 1
466+
467+
# Message Type 0 is a price update and we ignore the rest
468+
if message_type != 0:
469+
continue
470+
471+
price_id = message[message_offset : message_offset + 32].hex()
472+
message_offset += 32
473+
474+
if price_id != price_feed_id:
475+
continue
476+
477+
price = int.from_bytes(
478+
message[message_offset : message_offset + 8], byteorder="big", signed=True
479+
)
480+
message_offset += 8
481+
conf = int.from_bytes(
482+
message[message_offset : message_offset + 8], byteorder="big", signed=False
483+
)
484+
message_offset += 8
485+
expo = int.from_bytes(
486+
message[message_offset : message_offset + 4], byteorder="big", signed=True
487+
)
488+
message_offset += 4
489+
publish_time = int.from_bytes(
490+
message[message_offset : message_offset + 8], byteorder="big", signed=True
491+
)
492+
message_offset += 8
493+
prev_publish_time = int.from_bytes(
494+
message[message_offset : message_offset + 8], byteorder="big", signed=True
495+
)
496+
message_offset += 8
497+
ema_price = int.from_bytes(
498+
message[message_offset : message_offset + 8], byteorder="big", signed=True
499+
)
500+
message_offset += 8
501+
ema_conf = int.from_bytes(
502+
message[message_offset : message_offset + 8], byteorder="big", signed=False
503+
)
504+
505+
return PriceInfo(
506+
seq_num=parsed_vaa["sequence"],
507+
vaa=update_data,
508+
publish_time=publish_time,
509+
attestation_time=publish_time,
510+
last_attested_publish_time=prev_publish_time,
511+
price_feed=PriceUpdate(
512+
ema_price=Price(
513+
price=str(ema_price),
514+
conf=str(ema_conf),
515+
expo=expo,
516+
publish_time=publish_time,
517+
),
518+
price_id=price_id,
519+
price=Price(
520+
price=str(price),
521+
conf=str(conf),
522+
expo=expo,
523+
publish_time=publish_time,
524+
),
525+
),
526+
emitter_chain_id=parsed_vaa["emitter_chain"],
527+
)
528+
529+
return None

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
setup(
99
name='pythclient',
10-
version='0.1.10',
10+
version='0.1.11',
1111
packages=['pythclient'],
1212
author='Pyth Developers',
1313
author_email='[email protected]',

tests/test_price_feeds.py

+20
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
ID = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43" # BTC/USD
44
HEX_VAA = "01000000030d00ca037dde1419ea373de7474bff80268e41f55e84dd8e4fa5e5167808111e63e4064efcfd93075d9d162090bc478a017603ef56c4d26e0c46e0af6128c08984470002e9abd7c4e62bbc6b453c5ca014b7313bbadb17d6443190d6679b7c0ccb03bae619d21dcb5d6d5a484f9e4fd5586dca7c20403dc80d61dbe682a436b4d15d10bc0003a5e748513b75e34d9db61c6fd0fa55533e8424d88fbe6cda8e7cf61d50cb9467192a5cbd47b14b205b38123ad4b7c39d6ce7d6bf02c4a1452d135dc29fc83ef600049e4cd56e3c2e0cfc4cc831bc4ec54410590c15b4b20b63cc91be3436a4d09c0c22be4ab96962ad6e94d83e14c770e122aebae6fdbdea97f98cec7da5d30ed2a40106a7565c6387a700e02ee09652b40bdeef044441ca1e3b3c9376d4a129d22ca861501c3f5c0e8469c9a0e5d1b09d9f84c6517c0a2b400c0b47552006fff1dad3a5000a4db87004c483f899b5fd766c14334dfb5ca2fa5698964cf9644669b325bd3485207cbc4180a360023d1412da68bb11a0a82fee70a6bf03dda30b7aae53e0e465010ba3a6e45c9d8ef1d1041fdc7a926a9f41075531d45824144bbc720d111ee7270a77dd6dd65558b30d0f03692e075bd7d96cdfb24f5a68fecc22e441ded230c9cc010c09380e394e2b30fd036f13152b115dab7a206270d52255dfbbf0505c67bf510e70d0a6075f9bae19235eaf8a0893a4af9ed0df1b8cd67e1fe7b2ec61178d3ca4010dc491600d07d10a6468fb5955d94bc114efab46104e2ae530931231fea52cf7e32964a1c8bfe0ee38aaa8abfe8edcb7c079b6dd97b2c317c9d71cb5973bb53c72010f787e3c59ac484fdca7d5e41b29cebee08cb1789d61a0f29ccd0353118fd667ab1473a626eb6c237cff70ffb1eb2a556862197b08f183d5852168f5ce0f92632b0110f7ee4abdedc936ebebe86b3493292a9fa6625ab910b4a1340b46478a819508d1261f3d559d5cc95dead635c215b80b1cb2df348639d1ca572d3d14f07dc38908011103e3cdc9936ffbb7c0af5d77a4c092c5c42de161c9254919d19af718defd71a757fcbb1e3772e72c3a8c8291ab36f628a060030abf8ffb43923bb1a05cf9605d0112ddea2ce8ec77b9e222db5f1a95861c3da2ed3f54f7e937008bcc14b2458b98990eeb5910c7e9b2a27ff47a9568d0a3fedc12f357323905cbc8a1be6acbc5986b0064c37bca00000000001af8cd23c2ab91237730770bbea08d61005cdda0984348f3f6eecb559638c0bba0000000002144b1420150325748000300010001020005009d2efa1235ab86c0935cb424b102be4f217e74d1109df9e75dfa8338fc0f0908782f95862b045670cd22bee3114c39763a4a08beeb663b145d283c31d7d1101c4f000000059cc51c400000000000e4e1bffffffff8000000059b3f3c700000000000eae895010000001a0000001e0000000064c37bca0000000064c37bca0000000064c37bc9000000059cc51c400000000000e4e1bf0000000064c37bc948d6033d733e27950c2e0351e2505491cd9154824f716d9513514c74b9f98f583dd2b63686a450ec7290df3a1e0b583c0481f651351edfa7636f39aed55cf8a300000005a7462c060000000000d206a2fffffff800000005a5c499380000000000f44b7d010000001c000000200000000064c37bca0000000064c37bca0000000064c37bc900000005a74653cc0000000000d1dedc0000000064c37bc83515b3861e8fe93e5f540ba4077c216404782b86d5e78077b3cbfd27313ab3bce62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43000002a724c9d1000000000032396fc5fffffff8000002a6e3e0fec0000000002ee4815c010000001b000000200000000064c37bca0000000064c37bca0000000064c37bc9000002a724c9d1000000000032396fc50000000064c37bc99b5f73e0075e7d70376012180ddba94272f68d85eae4104e335561c982253d41a19d04ac696c7a6616d291c7e5d1377cc8be437c327b75adb5dc1bad745fcae800000000045152eb000000000001bb63fffffff800000000044de0b500000000000185df0100000015000000160000000064c37bca0000000064c37bca0000000064c37bc900000000045152eb000000000001bb630000000064c37bc8e876fcd130add8984a33aab52af36bc1b9f822c9ebe376f3aa72d630974e15f0dcef50dd0a4cd2dcc17e45df1676dcb336a11a61c69df7a0299b0150c672d25c000000000074990500000000000011d9fffffff80000000000748c2f000000000000116f010000001b000000200000000064c37bca0000000064c37bca0000000064c37bc900000000007498d400000000000011a80000000064c37bc9"
55
BASE64_VAA = "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"
6+
ACCUMULATOR_UPDATE_DATA = "UE5BVQEAAAADuAEAAAADDQAsKPsmb7Vz7io3taJQKgoi1m/z0kqKgtpmlkv+ZuunX2Iegsf+8fuUtpHPLKgCWPU8PN2x9NyAZz5BY9M3SWwJAALYlM0U7f2GFWfEjKwSJlHZ5sf+n6KXCocVC66ImS2o0TD0SBhTWcp0KdcuzR1rY1jfIHaFpVneroRLbTjNrk/WAAMuAYxPVPf1DR30wYQo12Dbf+in3akTjhKERNQ+nPwRjxAyIQD+52LU3Rh2VL7nOIStMNTiBMaiWHywaPoXowWAAQbillhhX4MR+7h81PfxHIbiXBmER4c5M7spilWKkROb+VXhrqnVJL162t9TdhYk56PDIhvXO1Tm/ldjVJw130y0AAk6qpccfsxDZEmVN8LI4z8739Ni/kb+CB3yW2l2dWhKTjBeNanhK6TCCoNH/jRzWfrjrEk5zjNrUr82JwL4fR1OAQrYZescxbH26m8QHiH+RHzwlXpUKJgbHD5NnWtB7oFb9AFM15jbjd4yIEBEtAlXPE0Q4j+X+DLnCtZbLSQiYNh5AQvz70LTbYry1lEExuUcO+IRJiysw5AFyqZ9Y1E//WKIqgEysfcnHwoOxtDtAc5Z9sTUEYfPqQ1d27k3Yk0X7dvCAQ10cdG0qYHb+bQrYRIKKnb0aeCjkCs0HZQY2fXYmimyfTNfECclmPW9k+CfOvW0JKuFxC1l11zJ3zjsgN/peA8BAQ5oIFQGjq9qmf5gegE1DjuzXsGksKao6nsjTXYIspCczCe2h5KNQ9l5hws11hauUKS20JoOYjHwxPD2x0adJKvkAQ+4UjVcZgVEQP8y3caqUDH81Ikcadz2bESpYg93dpnzZTH6A7Ue+RL34PTNx6cCRzukwQuhiStuyL1WYEIrLI4nABAjGv3EBXjWaPLUj59OzVnGkzxkr6C4KDjMmpsYNzx7I2lp2iQV46TM78El8i9h7twiEDUOSdC5CmfQjRpkP72yABGVAQELUm2/SjkpF0O+/rVDgA/Y2/wMacD1ZDahdyvSNSFThn5NyRYA1JXGgIDxoYeAZgkr1gL1cjCLWiO+Bs9QARIiCvHfIkn2aYhYHQq/u6cHB/2DxE3OgbCZyTv8OVO55hQDkJ1gDwAec+IJ4M5Od4OxWEu+OywhJT7zUmwZko9MAGTeJ+kAAAAAABrhAfrtrFhR4yubI7X5QRqMK6xKrj7U3XuBHdGnLqSqcQAAAAAAWllxAUFVV1YAAAAAAAVZ/XAAACcQ8Xfx5wQ+nj1rn6IeTUAy+VER1nUBAFUA5i32yLSoX+GmfbRNwS3l2zMPesZrctxliv7fD0pBW0MAAAKUUTJXzwAAAADDrAZ1////+AAAAABk3ifoAAAAAGTeJ+cAAAKWepR2oAAAAAC/b8SsCasjFzENKvXWwOycuzCVaDWfm0IuuuesmamDKl2lNXss15orlNN+xHVNEEIIq7Xg8GRZGVLt43fkg7xli6EPQ/Nyxl6SixiYteNt1uTTh4M1lQTUjPxKnkE5JEea4RnhOWgmSAWMf8ft4KgE7hvRifV1JP0rOsNgsOYFRbs6iDKW1qLpxgZLMAiOclwS3Tjw2hj8sPfq1NHeVttsBEK5SIM14GjAuD/p2V0+NqHqMHxU/kfftg=="
67

78

89
def test_valid_hex_vaa_to_price_info():
@@ -66,3 +67,22 @@ def test_encode_vaa_for_chain():
6667
# Test that encoding a base64 VAA as hex returns hex VAA
6768
encoded_vaa = encode_vaa_for_chain(BASE64_VAA, "hex")
6869
assert encoded_vaa == HEX_VAA
70+
71+
72+
def test_valid_accumulator_vaa_to_price_info():
73+
price_info = vaa_to_price_info(ID, ACCUMULATOR_UPDATE_DATA, "base64")
74+
assert price_info.seq_num == 5921137
75+
assert price_info.vaa == ACCUMULATOR_UPDATE_DATA
76+
assert price_info.publish_time == 1692280808
77+
assert price_info.attestation_time == 1692280808
78+
assert price_info.last_attested_publish_time == 1692280807
79+
assert price_info.price_feed.ema_price.price == "2845324900000"
80+
assert price_info.price_feed.ema_price.conf == "3211773100"
81+
assert price_info.price_feed.ema_price.expo == -8
82+
assert price_info.price_feed.ema_price.publish_time == 1692280808
83+
assert price_info.price_feed.id == ID
84+
assert price_info.price_feed.price.price == "2836040669135"
85+
assert price_info.price_feed.price.conf == "3282830965"
86+
assert price_info.price_feed.price.expo == -8
87+
assert price_info.price_feed.price.publish_time == 1692280808
88+
assert price_info.emitter_chain_id == 26

0 commit comments

Comments
 (0)