From 1788603cba583402b2f501b30d7ac47ee3f9ea06 Mon Sep 17 00:00:00 2001 From: Daniel Chew Date: Mon, 31 Jul 2023 19:16:18 +0800 Subject: [PATCH 1/8] add functions to parse vaa to price info --- .gitignore | 7 + pythclient/price_feeds.py | 427 ++++++++++++++++++++++++++++++++++++++ setup.py | 2 +- tests/test_price_feeds.py | 33 +++ 4 files changed, 468 insertions(+), 1 deletion(-) create mode 100644 pythclient/price_feeds.py create mode 100644 tests/test_price_feeds.py diff --git a/.gitignore b/.gitignore index 9840d09..2386902 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,13 @@ # Created by https://www.toptal.com/developers/gitignore/api/python # Edit at https://www.toptal.com/developers/gitignore?templates=python + +# VS Code +.vscode/ + +# MacOS +.DS_Store + ### Python ### # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/pythclient/price_feeds.py b/pythclient/price_feeds.py new file mode 100644 index 0000000..e1ca9d1 --- /dev/null +++ b/pythclient/price_feeds.py @@ -0,0 +1,427 @@ +import json +import logging +from struct import unpack + +from Crypto.Hash import keccak + +P2W_FORMAT_MAGIC = "P2WH" +P2W_FORMAT_VER_MAJOR = 3 +P2W_FORMAT_VER_MINOR = 0 +P2W_FORMAT_PAYLOAD_ID = 2 + +DEFAULT_VAA_ENCODING = "base64" +CHAIN_TO_ENCODING = { + "evm": "0x", + "cosmos": "base64", + "aptos": "base64", + "default": "base64", +} + + +class Price: + def __init__(self, conf, expo, price, publish_time): + self.conf = conf + self.expo = expo + self.price = price + self.publish_time = publish_time + + def __str__(self): + return f"Price(conf={self.conf}, expo={self.expo}, price={self.price}, publish_time={self.publish_time})" + + def to_dict(self): + return { + "conf": self.conf, + "expo": self.expo, + "price": self.price, + "publish_time": self.publish_time, + } + + def to_json(self): + return json.dumps( + { + "conf": self.conf, + "expo": self.expo, + "price": self.price, + "publish_time": self.publish_time, + } + ) + + +class PriceFeed: + def __init__(self, ema_price, price_id, price): + self.ema_price = ema_price + self.id = price_id + self.price = price + + def __str__(self): + return ( + f"PriceFeed(ema_price={self.ema_price}, id={self.id}, price={self.price})" + ) + + def to_dict(self): + return { + "ema_price": self.ema_price.to_dict(), + "id": self.id, + "price": self.price.to_dict(), + } + + def to_json(self): + return json.dumps( + { + "ema_price": self.ema_price.to_dict(), + "id": self.id, + "price": self.price.to_dict(), + } + ) + + +class PriceInfo: + def __init__( + self, + seq_num, + vaa, + publish_time, + attestation_time, + last_attested_publish_time, + price_feed, + emitter_chain_id, + ): + self.seq_num = seq_num + self.vaa = vaa + self.publish_time = publish_time + self.attestation_time = attestation_time + self.last_attested_publish_time = last_attested_publish_time + self.price_feed = price_feed + self.emitter_chain_id = emitter_chain_id + + def __str__(self): + return ( + f"SeqNum: {self.seq_num}\n" + f"VAA: {self.vaa}\n" + f"Publish Time: {self.publish_time}\n" + f"Attestation Time: {self.attestation_time}\n" + f"Last Attested Publish Time: {self.last_attested_publish_time}\n" + f"Price Feed: {self.price_feed}\n" + f"Emitter Chain ID: {self.emitter_chain_id}\n" + ) + + def to_dict(self): + return { + "seq_num": self.seq_num, + "vaa": self.vaa, + "publish_time": self.publish_time, + "attestation_time": self.attestation_time, + "last_attested_publish_time": self.last_attested_publish_time, + "price_feed": self.price_feed.to_dict(), + "emitter_chain_id": self.emitter_chain_id, + } + + def to_json(self, verbose=False, target_chain=None): + metadata = ( + { + "emitter_chain": self.emitter_chain_id, + "attestation_time": self.attestation_time, + "sequence_number": self.seq_num, + } + if verbose + else {} + ) + + vaa_data = ( + { + "vaa": encode_vaa_for_chain(self.vaa, target_chain), + } + if target_chain + else {} + ) + + result = { + **self.price_feed.to_dict(), + **metadata, + **vaa_data, + } + + return json.dumps(result) + + +def encode_vaa_for_chain(vaa, target_chain): + encoding = CHAIN_TO_ENCODING[target_chain] + + if isinstance(vaa, str): + if encoding == DEFAULT_VAA_ENCODING: + return vaa + else: + vaa_buffer = ( + bytes.fromhex(vaa) + if vaa.startswith("0x") + else bytes(vaa, encoding=DEFAULT_VAA_ENCODING) + ) + else: + vaa_buffer = bytes(vaa) + + if encoding == "0x": + return "0x" + vaa_buffer.hex() + else: + return vaa_buffer.decode(encoding) + + +def parse_vaa(vaa): + if isinstance(vaa, str): + vaa = bytes.fromhex(vaa) + + num_signers = vaa[5] + sig_length = 66 + sig_start = 6 + guardian_signatures = [] + + for i in range(num_signers): + start = sig_start + i * sig_length + index = vaa[start] + signature = vaa[start + 1 : start + 66] + guardian_signatures.append({"index": index, "signature": signature}) + + body = vaa[sig_start + sig_length * num_signers :] + version = vaa[0] + guardian_set_index = unpack(">I", vaa[1:5])[0] + timestamp = unpack(">I", body[0:4])[0] + nonce = unpack(">I", body[4:8])[0] + emitter_chain = unpack(">H", body[8:10])[0] + emitter_address = body[10:42] + sequence = int.from_bytes(body[42:50], byteorder="big") + consistency_level = body[50] + payload = body[51:] + + # Compute the hash using pycryptodome's keccak + keccak_hash = keccak.new(digest_bits=256) + keccak_hash.update(body) + hash_val = keccak_hash.hexdigest() + + return { + "version": version, + "guardian_set_index": guardian_set_index, + "guardian_signatures": guardian_signatures, + "timestamp": timestamp, + "nonce": nonce, + "emitter_chain": emitter_chain, + "emitter_address": emitter_address, + "sequence": sequence, + "consistency_level": consistency_level, + "payload": payload, + "hash": hash_val, + } + + +def parse_batch_price_attestation(bytes_): + offset = 0 + + magic = bytes_[offset : offset + 4].decode("utf-8") + offset += 4 + if magic != P2W_FORMAT_MAGIC: + raise ValueError(f"Invalid magic: {magic}, expected: {P2W_FORMAT_MAGIC}") + + version_major = int.from_bytes(bytes_[offset : offset + 2], byteorder="big") + offset += 2 + if version_major != P2W_FORMAT_VER_MAJOR: + raise ValueError( + f"Unsupported major version: {version_major}, expected: {P2W_FORMAT_VER_MAJOR}" + ) + + version_minor = int.from_bytes(bytes_[offset : offset + 2], byteorder="big") + offset += 2 + if version_minor < P2W_FORMAT_VER_MINOR: + raise ValueError( + f"Unsupported minor version: {version_minor}, expected: {P2W_FORMAT_VER_MINOR}" + ) + + header_size = int.from_bytes(bytes_[offset : offset + 2], byteorder="big") + offset += 2 + header_offset = 0 + + payload_id = int.from_bytes( + bytes_[offset + header_offset : offset + header_offset + 1], byteorder="big" + ) + header_offset += 1 + if payload_id != P2W_FORMAT_PAYLOAD_ID: + raise ValueError( + f"Invalid payload_id: {payload_id}, expected: {P2W_FORMAT_PAYLOAD_ID}" + ) + + offset += header_size + batch_len = int.from_bytes(bytes_[offset : offset + 2], byteorder="big") + offset += 2 + attestation_size = int.from_bytes(bytes_[offset : offset + 2], byteorder="big") + offset += 2 + + price_attestations = [] + for i in range(batch_len): + price_attestations.append( + parse_price_attestation(bytes_[offset : offset + attestation_size]) + ) + offset += attestation_size + + if offset != len(bytes_): + raise ValueError(f"Invalid length: {len(bytes_)}, expected: {offset}") + + return { + "price_attestations": price_attestations, + } + + +def parse_price_attestation(bytes_): + offset = 0 + + product_id = bytes_[offset : offset + 32].hex() + offset += 32 + + price_id = bytes_[offset : offset + 32].hex() + offset += 32 + + price = int.from_bytes(bytes_[offset : offset + 8], byteorder="big", signed=True) + offset += 8 + + conf = int.from_bytes(bytes_[offset : offset + 8], byteorder="big", signed=False) + offset += 8 + + expo = int.from_bytes(bytes_[offset : offset + 4], byteorder="big", signed=True) + offset += 4 + + ema_price = int.from_bytes( + bytes_[offset : offset + 8], byteorder="big", signed=True + ) + offset += 8 + + ema_conf = int.from_bytes( + bytes_[offset : offset + 8], byteorder="big", signed=False + ) + offset += 8 + + status = int.from_bytes(bytes_[offset : offset + 1], byteorder="big") + offset += 1 + + num_publishers = int.from_bytes( + bytes_[offset : offset + 4], byteorder="big", signed=False + ) + offset += 4 + + max_num_publishers = int.from_bytes( + bytes_[offset : offset + 4], byteorder="big", signed=False + ) + offset += 4 + + attestation_time = int.from_bytes( + bytes_[offset : offset + 8], byteorder="big", signed=True + ) + offset += 8 + + publish_time = int.from_bytes( + bytes_[offset : offset + 8], byteorder="big", signed=True + ) + offset += 8 + + prev_publish_time = int.from_bytes( + bytes_[offset : offset + 8], byteorder="big", signed=True + ) + offset += 8 + + prev_price = int.from_bytes( + bytes_[offset : offset + 8], byteorder="big", signed=True + ) + offset += 8 + + prev_conf = int.from_bytes( + bytes_[offset : offset + 8], byteorder="big", signed=False + ) + offset += 8 + + last_attested_publish_time = int.from_bytes( + bytes_[offset : offset + 8], byteorder="big", signed=True + ) + offset += 8 + + return { + "product_id": product_id, + "price_id": price_id, + "price": str(price), + "conf": str(conf), + "expo": expo, + "ema_price": str(ema_price), + "ema_conf": str(ema_conf), + "status": status, + "num_publishers": num_publishers, + "max_num_publishers": max_num_publishers, + "attestation_time": attestation_time, + "publish_time": publish_time, + "prev_publish_time": prev_publish_time, + "prev_price": str(prev_price), + "prev_conf": str(prev_conf), + "last_attested_publish_time": last_attested_publish_time, + } + + +def vaa_to_price_info(price_feed_id, vaa) -> PriceInfo: + parsed_vaa = parse_vaa(vaa) + + try: + batch_attestation = parse_batch_price_attestation(parsed_vaa["payload"]) + except Exception as e: + logging.error(e) + logging.error(f"Parsing historical VAA failed: {parsed_vaa}") + return None + + for price_attestation in batch_attestation["price_attestations"]: + if price_attestation["price_id"] == price_feed_id: + return create_price_info( + price_attestation, + vaa, + parsed_vaa["sequence"], + parsed_vaa["emitter_chain"], + ) + + return None + + +def create_price_info(price_attestation, vaa, sequence, emitter_chain): + price_feed = price_attestation_to_price_feed(price_attestation) + return PriceInfo( + seq_num=int(sequence), + vaa=vaa, + publish_time=price_attestation["publish_time"], + attestation_time=price_attestation["attestation_time"], + last_attested_publish_time=price_attestation["last_attested_publish_time"], + price_feed=price_feed, + emitter_chain_id=emitter_chain, + ) + + +def price_attestation_to_price_feed(price_attestation): + ema_price = Price( + price_attestation["ema_conf"], + price_attestation["expo"], + price_attestation["ema_price"], + price_attestation["publish_time"], + ) + + if price_attestation["status"] == 1: # Assuming 1 means Trading + price = Price( + price_attestation["conf"], + price_attestation["expo"], + price_attestation["price"], + price_attestation["publish_time"], + ) + else: + price = Price( + price_attestation["prev_conf"], + price_attestation["expo"], + price_attestation["prev_price"], + price_attestation["prev_publish_time"], + ) + ema_price.publish_time = price_attestation["prev_publish_time"] + + return PriceFeed(ema_price, price_attestation["price_id"], price) + + +id = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43" +vaa = "01000000030d00ca037dde1419ea373de7474bff80268e41f55e84dd8e4fa5e5167808111e63e4064efcfd93075d9d162090bc478a017603ef56c4d26e0c46e0af6128c08984470002e9abd7c4e62bbc6b453c5ca014b7313bbadb17d6443190d6679b7c0ccb03bae619d21dcb5d6d5a484f9e4fd5586dca7c20403dc80d61dbe682a436b4d15d10bc0003a5e748513b75e34d9db61c6fd0fa55533e8424d88fbe6cda8e7cf61d50cb9467192a5cbd47b14b205b38123ad4b7c39d6ce7d6bf02c4a1452d135dc29fc83ef600049e4cd56e3c2e0cfc4cc831bc4ec54410590c15b4b20b63cc91be3436a4d09c0c22be4ab96962ad6e94d83e14c770e122aebae6fdbdea97f98cec7da5d30ed2a40106a7565c6387a700e02ee09652b40bdeef044441ca1e3b3c9376d4a129d22ca861501c3f5c0e8469c9a0e5d1b09d9f84c6517c0a2b400c0b47552006fff1dad3a5000a4db87004c483f899b5fd766c14334dfb5ca2fa5698964cf9644669b325bd3485207cbc4180a360023d1412da68bb11a0a82fee70a6bf03dda30b7aae53e0e465010ba3a6e45c9d8ef1d1041fdc7a926a9f41075531d45824144bbc720d111ee7270a77dd6dd65558b30d0f03692e075bd7d96cdfb24f5a68fecc22e441ded230c9cc010c09380e394e2b30fd036f13152b115dab7a206270d52255dfbbf0505c67bf510e70d0a6075f9bae19235eaf8a0893a4af9ed0df1b8cd67e1fe7b2ec61178d3ca4010dc491600d07d10a6468fb5955d94bc114efab46104e2ae530931231fea52cf7e32964a1c8bfe0ee38aaa8abfe8edcb7c079b6dd97b2c317c9d71cb5973bb53c72010f787e3c59ac484fdca7d5e41b29cebee08cb1789d61a0f29ccd0353118fd667ab1473a626eb6c237cff70ffb1eb2a556862197b08f183d5852168f5ce0f92632b0110f7ee4abdedc936ebebe86b3493292a9fa6625ab910b4a1340b46478a819508d1261f3d559d5cc95dead635c215b80b1cb2df348639d1ca572d3d14f07dc38908011103e3cdc9936ffbb7c0af5d77a4c092c5c42de161c9254919d19af718defd71a757fcbb1e3772e72c3a8c8291ab36f628a060030abf8ffb43923bb1a05cf9605d0112ddea2ce8ec77b9e222db5f1a95861c3da2ed3f54f7e937008bcc14b2458b98990eeb5910c7e9b2a27ff47a9568d0a3fedc12f357323905cbc8a1be6acbc5986b0064c37bca00000000001af8cd23c2ab91237730770bbea08d61005cdda0984348f3f6eecb559638c0bba0000000002144b1420150325748000300010001020005009d2efa1235ab86c0935cb424b102be4f217e74d1109df9e75dfa8338fc0f0908782f95862b045670cd22bee3114c39763a4a08beeb663b145d283c31d7d1101c4f000000059cc51c400000000000e4e1bffffffff8000000059b3f3c700000000000eae895010000001a0000001e0000000064c37bca0000000064c37bca0000000064c37bc9000000059cc51c400000000000e4e1bf0000000064c37bc948d6033d733e27950c2e0351e2505491cd9154824f716d9513514c74b9f98f583dd2b63686a450ec7290df3a1e0b583c0481f651351edfa7636f39aed55cf8a300000005a7462c060000000000d206a2fffffff800000005a5c499380000000000f44b7d010000001c000000200000000064c37bca0000000064c37bca0000000064c37bc900000005a74653cc0000000000d1dedc0000000064c37bc83515b3861e8fe93e5f540ba4077c216404782b86d5e78077b3cbfd27313ab3bce62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43000002a724c9d1000000000032396fc5fffffff8000002a6e3e0fec0000000002ee4815c010000001b000000200000000064c37bca0000000064c37bca0000000064c37bc9000002a724c9d1000000000032396fc50000000064c37bc99b5f73e0075e7d70376012180ddba94272f68d85eae4104e335561c982253d41a19d04ac696c7a6616d291c7e5d1377cc8be437c327b75adb5dc1bad745fcae800000000045152eb000000000001bb63fffffff800000000044de0b500000000000185df0100000015000000160000000064c37bca0000000064c37bca0000000064c37bc900000000045152eb000000000001bb630000000064c37bc8e876fcd130add8984a33aab52af36bc1b9f822c9ebe376f3aa72d630974e15f0dcef50dd0a4cd2dcc17e45df1676dcb336a11a61c69df7a0299b0150c672d25c000000000074990500000000000011d9fffffff80000000000748c2f000000000000116f010000001b000000200000000064c37bca0000000064c37bca0000000064c37bc900000000007498d400000000000011a80000000064c37bc9" +price_info = vaa_to_price_info(id, vaa) + +print(price_info.to_json()) diff --git a/setup.py b/setup.py index bbb81c9..c8a4eee 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -requirements = ['aiodns', 'aiohttp>=3.7.4', 'backoff', 'base58', 'dnspython', 'flake8', 'loguru', 'typing-extensions', 'pytz'] +requirements = ['aiodns', 'aiohttp>=3.7.4', 'backoff', 'base58', 'dnspython', 'flake8', 'loguru', 'typing-extensions', 'pytz', 'pycryptodome'] with open('README.md', 'r', encoding='utf-8') as fh: long_description = fh.read() diff --git a/tests/test_price_feeds.py b/tests/test_price_feeds.py new file mode 100644 index 0000000..81b7c27 --- /dev/null +++ b/tests/test_price_feeds.py @@ -0,0 +1,33 @@ +from pythclient.price_feeds import vaa_to_price_info + +ID = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43" # BTC/USD +VAA = "01000000030d00ca037dde1419ea373de7474bff80268e41f55e84dd8e4fa5e5167808111e63e4064efcfd93075d9d162090bc478a017603ef56c4d26e0c46e0af6128c08984470002e9abd7c4e62bbc6b453c5ca014b7313bbadb17d6443190d6679b7c0ccb03bae619d21dcb5d6d5a484f9e4fd5586dca7c20403dc80d61dbe682a436b4d15d10bc0003a5e748513b75e34d9db61c6fd0fa55533e8424d88fbe6cda8e7cf61d50cb9467192a5cbd47b14b205b38123ad4b7c39d6ce7d6bf02c4a1452d135dc29fc83ef600049e4cd56e3c2e0cfc4cc831bc4ec54410590c15b4b20b63cc91be3436a4d09c0c22be4ab96962ad6e94d83e14c770e122aebae6fdbdea97f98cec7da5d30ed2a40106a7565c6387a700e02ee09652b40bdeef044441ca1e3b3c9376d4a129d22ca861501c3f5c0e8469c9a0e5d1b09d9f84c6517c0a2b400c0b47552006fff1dad3a5000a4db87004c483f899b5fd766c14334dfb5ca2fa5698964cf9644669b325bd3485207cbc4180a360023d1412da68bb11a0a82fee70a6bf03dda30b7aae53e0e465010ba3a6e45c9d8ef1d1041fdc7a926a9f41075531d45824144bbc720d111ee7270a77dd6dd65558b30d0f03692e075bd7d96cdfb24f5a68fecc22e441ded230c9cc010c09380e394e2b30fd036f13152b115dab7a206270d52255dfbbf0505c67bf510e70d0a6075f9bae19235eaf8a0893a4af9ed0df1b8cd67e1fe7b2ec61178d3ca4010dc491600d07d10a6468fb5955d94bc114efab46104e2ae530931231fea52cf7e32964a1c8bfe0ee38aaa8abfe8edcb7c079b6dd97b2c317c9d71cb5973bb53c72010f787e3c59ac484fdca7d5e41b29cebee08cb1789d61a0f29ccd0353118fd667ab1473a626eb6c237cff70ffb1eb2a556862197b08f183d5852168f5ce0f92632b0110f7ee4abdedc936ebebe86b3493292a9fa6625ab910b4a1340b46478a819508d1261f3d559d5cc95dead635c215b80b1cb2df348639d1ca572d3d14f07dc38908011103e3cdc9936ffbb7c0af5d77a4c092c5c42de161c9254919d19af718defd71a757fcbb1e3772e72c3a8c8291ab36f628a060030abf8ffb43923bb1a05cf9605d0112ddea2ce8ec77b9e222db5f1a95861c3da2ed3f54f7e937008bcc14b2458b98990eeb5910c7e9b2a27ff47a9568d0a3fedc12f357323905cbc8a1be6acbc5986b0064c37bca00000000001af8cd23c2ab91237730770bbea08d61005cdda0984348f3f6eecb559638c0bba0000000002144b1420150325748000300010001020005009d2efa1235ab86c0935cb424b102be4f217e74d1109df9e75dfa8338fc0f0908782f95862b045670cd22bee3114c39763a4a08beeb663b145d283c31d7d1101c4f000000059cc51c400000000000e4e1bffffffff8000000059b3f3c700000000000eae895010000001a0000001e0000000064c37bca0000000064c37bca0000000064c37bc9000000059cc51c400000000000e4e1bf0000000064c37bc948d6033d733e27950c2e0351e2505491cd9154824f716d9513514c74b9f98f583dd2b63686a450ec7290df3a1e0b583c0481f651351edfa7636f39aed55cf8a300000005a7462c060000000000d206a2fffffff800000005a5c499380000000000f44b7d010000001c000000200000000064c37bca0000000064c37bca0000000064c37bc900000005a74653cc0000000000d1dedc0000000064c37bc83515b3861e8fe93e5f540ba4077c216404782b86d5e78077b3cbfd27313ab3bce62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43000002a724c9d1000000000032396fc5fffffff8000002a6e3e0fec0000000002ee4815c010000001b000000200000000064c37bca0000000064c37bca0000000064c37bc9000002a724c9d1000000000032396fc50000000064c37bc99b5f73e0075e7d70376012180ddba94272f68d85eae4104e335561c982253d41a19d04ac696c7a6616d291c7e5d1377cc8be437c327b75adb5dc1bad745fcae800000000045152eb000000000001bb63fffffff800000000044de0b500000000000185df0100000015000000160000000064c37bca0000000064c37bca0000000064c37bc900000000045152eb000000000001bb630000000064c37bc8e876fcd130add8984a33aab52af36bc1b9f822c9ebe376f3aa72d630974e15f0dcef50dd0a4cd2dcc17e45df1676dcb336a11a61c69df7a0299b0150c672d25c000000000074990500000000000011d9fffffff80000000000748c2f000000000000116f010000001b000000200000000064c37bca0000000064c37bca0000000064c37bc900000000007498d400000000000011a80000000064c37bc9" + + +def test_valid_vaa_to_price_info(): + price_info = vaa_to_price_info(ID, VAA) + assert price_info.seq_num == 558149954 + assert price_info.vaa == VAA + assert price_info.publish_time == 1690532810 + assert price_info.attestation_time == 1690532810 + assert price_info.last_attested_publish_time == 1690532809 + assert price_info.price_feed.ema_price.price == "2915811000000" + assert price_info.price_feed.ema_price.conf == "786727260" + assert price_info.price_feed.ema_price.expo == -8 + assert price_info.price_feed.ema_price.publish_time == 1690532810 + assert price_info.price_feed.id == ID + assert price_info.price_feed.price.price == "2916900000000" + assert price_info.price_feed.price.conf == "842624965" + assert price_info.price_feed.price.expo == -8 + assert price_info.price_feed.price.publish_time == 1690532810 + assert price_info.emitter_chain_id == 26 + + +def test_invalid_vaa_to_price_info(): + try: + vaa_to_price_info(ID, VAA + "1") + except ValueError as ve: + assert ( + ve.args[0] + == "non-hexadecimal number found in fromhex() arg at position 3431" + ) From 16f8b3c28a4d7b569ea20b20a63617184b268701 Mon Sep 17 00:00:00 2001 From: Daniel Chew Date: Mon, 31 Jul 2023 19:16:31 +0800 Subject: [PATCH 2/8] bump --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c8a4eee..03d5070 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='pythclient', - version='0.1.9', + version='0.1.10', packages=['pythclient'], author='Pyth Developers', author_email='contact@pyth.network', From 3fcdee377f6ee6cb29aeeb4dee3da4b7fc944d7e Mon Sep 17 00:00:00 2001 From: Daniel Chew Date: Mon, 31 Jul 2023 19:20:50 +0800 Subject: [PATCH 3/8] add comments --- pythclient/price_feeds.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pythclient/price_feeds.py b/pythclient/price_feeds.py index e1ca9d1..c3ce678 100644 --- a/pythclient/price_feeds.py +++ b/pythclient/price_feeds.py @@ -1,3 +1,5 @@ +# Classes and functions here are referenced from pyth-crosschain repo + import json import logging from struct import unpack @@ -165,6 +167,7 @@ def encode_vaa_for_chain(vaa, target_chain): return vaa_buffer.decode(encoding) +# Referenced from https://github.com/wormhole-foundation/wormhole/blob/main/sdk/js/src/vaa/wormhole.ts#L26-L56 def parse_vaa(vaa): if isinstance(vaa, str): vaa = bytes.fromhex(vaa) From fa3d0749c66b56716188d298e051087a2fbc7f2b Mon Sep 17 00:00:00 2001 From: Daniel Chew Date: Tue, 1 Aug 2023 14:50:40 +0800 Subject: [PATCH 4/8] address comments --- pythclient/price_feeds.py | 96 ++++++++++++++------------------------- 1 file changed, 34 insertions(+), 62 deletions(-) diff --git a/pythclient/price_feeds.py b/pythclient/price_feeds.py index c3ce678..b9ddc0a 100644 --- a/pythclient/price_feeds.py +++ b/pythclient/price_feeds.py @@ -1,8 +1,6 @@ -# Classes and functions here are referenced from pyth-crosschain repo - -import json import logging from struct import unpack +from typing import List from Crypto.Hash import keccak @@ -12,12 +10,6 @@ P2W_FORMAT_PAYLOAD_ID = 2 DEFAULT_VAA_ENCODING = "base64" -CHAIN_TO_ENCODING = { - "evm": "0x", - "cosmos": "base64", - "aptos": "base64", - "default": "base64", -} class Price: @@ -38,18 +30,8 @@ def to_dict(self): "publish_time": self.publish_time, } - def to_json(self): - return json.dumps( - { - "conf": self.conf, - "expo": self.expo, - "price": self.price, - "publish_time": self.publish_time, - } - ) - -class PriceFeed: +class PriceUpdate: def __init__(self, ema_price, price_id, price): self.ema_price = ema_price self.id = price_id @@ -57,7 +39,7 @@ def __init__(self, ema_price, price_id, price): def __str__(self): return ( - f"PriceFeed(ema_price={self.ema_price}, id={self.id}, price={self.price})" + f"PriceUpdate(ema_price={self.ema_price}, id={self.id}, price={self.price})" ) def to_dict(self): @@ -67,15 +49,6 @@ def to_dict(self): "price": self.price.to_dict(), } - def to_json(self): - return json.dumps( - { - "ema_price": self.ema_price.to_dict(), - "id": self.id, - "price": self.price.to_dict(), - } - ) - class PriceInfo: def __init__( @@ -107,18 +80,7 @@ def __str__(self): f"Emitter Chain ID: {self.emitter_chain_id}\n" ) - def to_dict(self): - return { - "seq_num": self.seq_num, - "vaa": self.vaa, - "publish_time": self.publish_time, - "attestation_time": self.attestation_time, - "last_attested_publish_time": self.last_attested_publish_time, - "price_feed": self.price_feed.to_dict(), - "emitter_chain_id": self.emitter_chain_id, - } - - def to_json(self, verbose=False, target_chain=None): + def to_dict(self, verbose=False, vaa_format=None): metadata = ( { "emitter_chain": self.emitter_chain_id, @@ -131,9 +93,9 @@ def to_json(self, verbose=False, target_chain=None): vaa_data = ( { - "vaa": encode_vaa_for_chain(self.vaa, target_chain), + "vaa": encode_vaa_for_chain(self.vaa, vaa_format), } - if target_chain + if vaa_format else {} ) @@ -143,14 +105,13 @@ def to_json(self, verbose=False, target_chain=None): **vaa_data, } - return json.dumps(result) - + return result -def encode_vaa_for_chain(vaa, target_chain): - encoding = CHAIN_TO_ENCODING[target_chain] +# Referenced from https://github.com/pyth-network/pyth-crosschain/blob/main/price_service/server/src/encoding.ts#L24 +def encode_vaa_for_chain(vaa, vaa_format): if isinstance(vaa, str): - if encoding == DEFAULT_VAA_ENCODING: + if vaa_format == DEFAULT_VAA_ENCODING: return vaa else: vaa_buffer = ( @@ -161,10 +122,10 @@ def encode_vaa_for_chain(vaa, target_chain): else: vaa_buffer = bytes(vaa) - if encoding == "0x": + if vaa_format == "0x": return "0x" + vaa_buffer.hex() else: - return vaa_buffer.decode(encoding) + return vaa_buffer.decode(vaa_format) # Referenced from https://github.com/wormhole-foundation/wormhole/blob/main/sdk/js/src/vaa/wormhole.ts#L26-L56 @@ -214,6 +175,7 @@ def parse_vaa(vaa): } +# Referenced from https://github.com/pyth-network/pyth-crosschain/blob/main/wormhole_attester/sdk/js/src/index.ts#L122 def parse_batch_price_attestation(bytes_): offset = 0 @@ -270,6 +232,7 @@ def parse_batch_price_attestation(bytes_): } +# Referenced from https://github.com/pyth-network/pyth-crosschain/blob/main/wormhole_attester/sdk/js/src/index.ts#L50 def parse_price_attestation(bytes_): offset = 0 @@ -361,9 +324,12 @@ def parse_price_attestation(bytes_): } -def vaa_to_price_info(price_feed_id, vaa) -> PriceInfo: +# Referenced from https://github.com/pyth-network/pyth-crosschain/blob/main/price_service/server/src/rest.ts#L139 +def vaa_to_price_infos(vaa) -> List[PriceInfo]: parsed_vaa = parse_vaa(vaa) + # TODO: support accumulators + try: batch_attestation = parse_batch_price_attestation(parsed_vaa["payload"]) except Exception as e: @@ -371,18 +337,30 @@ def vaa_to_price_info(price_feed_id, vaa) -> PriceInfo: logging.error(f"Parsing historical VAA failed: {parsed_vaa}") return None + price_infos = [] for price_attestation in batch_attestation["price_attestations"]: - if price_attestation["price_id"] == price_feed_id: - return create_price_info( + price_infos.append( + create_price_info( price_attestation, vaa, parsed_vaa["sequence"], parsed_vaa["emitter_chain"], ) + ) + + return price_infos + + +def vaa_to_price_info(price_feed_id, vaa) -> PriceInfo: + price_infos = vaa_to_price_infos(vaa) + for price_info in price_infos: + if price_info.price_feed.id == price_feed_id: + return price_info return None +# Referenced from https://github.com/pyth-network/pyth-crosschain/blob/main/price_service/server/src/listen.ts#L37 def create_price_info(price_attestation, vaa, sequence, emitter_chain): price_feed = price_attestation_to_price_feed(price_attestation) return PriceInfo( @@ -396,6 +374,7 @@ def create_price_info(price_attestation, vaa, sequence, emitter_chain): ) +# Referenced from https://github.com/pyth-network/pyth-crosschain/blob/main/wormhole_attester/sdk/js/src/index.ts#L218 def price_attestation_to_price_feed(price_attestation): ema_price = Price( price_attestation["ema_conf"], @@ -420,11 +399,4 @@ def price_attestation_to_price_feed(price_attestation): ) ema_price.publish_time = price_attestation["prev_publish_time"] - return PriceFeed(ema_price, price_attestation["price_id"], price) - - -id = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43" -vaa = "01000000030d00ca037dde1419ea373de7474bff80268e41f55e84dd8e4fa5e5167808111e63e4064efcfd93075d9d162090bc478a017603ef56c4d26e0c46e0af6128c08984470002e9abd7c4e62bbc6b453c5ca014b7313bbadb17d6443190d6679b7c0ccb03bae619d21dcb5d6d5a484f9e4fd5586dca7c20403dc80d61dbe682a436b4d15d10bc0003a5e748513b75e34d9db61c6fd0fa55533e8424d88fbe6cda8e7cf61d50cb9467192a5cbd47b14b205b38123ad4b7c39d6ce7d6bf02c4a1452d135dc29fc83ef600049e4cd56e3c2e0cfc4cc831bc4ec54410590c15b4b20b63cc91be3436a4d09c0c22be4ab96962ad6e94d83e14c770e122aebae6fdbdea97f98cec7da5d30ed2a40106a7565c6387a700e02ee09652b40bdeef044441ca1e3b3c9376d4a129d22ca861501c3f5c0e8469c9a0e5d1b09d9f84c6517c0a2b400c0b47552006fff1dad3a5000a4db87004c483f899b5fd766c14334dfb5ca2fa5698964cf9644669b325bd3485207cbc4180a360023d1412da68bb11a0a82fee70a6bf03dda30b7aae53e0e465010ba3a6e45c9d8ef1d1041fdc7a926a9f41075531d45824144bbc720d111ee7270a77dd6dd65558b30d0f03692e075bd7d96cdfb24f5a68fecc22e441ded230c9cc010c09380e394e2b30fd036f13152b115dab7a206270d52255dfbbf0505c67bf510e70d0a6075f9bae19235eaf8a0893a4af9ed0df1b8cd67e1fe7b2ec61178d3ca4010dc491600d07d10a6468fb5955d94bc114efab46104e2ae530931231fea52cf7e32964a1c8bfe0ee38aaa8abfe8edcb7c079b6dd97b2c317c9d71cb5973bb53c72010f787e3c59ac484fdca7d5e41b29cebee08cb1789d61a0f29ccd0353118fd667ab1473a626eb6c237cff70ffb1eb2a556862197b08f183d5852168f5ce0f92632b0110f7ee4abdedc936ebebe86b3493292a9fa6625ab910b4a1340b46478a819508d1261f3d559d5cc95dead635c215b80b1cb2df348639d1ca572d3d14f07dc38908011103e3cdc9936ffbb7c0af5d77a4c092c5c42de161c9254919d19af718defd71a757fcbb1e3772e72c3a8c8291ab36f628a060030abf8ffb43923bb1a05cf9605d0112ddea2ce8ec77b9e222db5f1a95861c3da2ed3f54f7e937008bcc14b2458b98990eeb5910c7e9b2a27ff47a9568d0a3fedc12f357323905cbc8a1be6acbc5986b0064c37bca00000000001af8cd23c2ab91237730770bbea08d61005cdda0984348f3f6eecb559638c0bba0000000002144b1420150325748000300010001020005009d2efa1235ab86c0935cb424b102be4f217e74d1109df9e75dfa8338fc0f0908782f95862b045670cd22bee3114c39763a4a08beeb663b145d283c31d7d1101c4f000000059cc51c400000000000e4e1bffffffff8000000059b3f3c700000000000eae895010000001a0000001e0000000064c37bca0000000064c37bca0000000064c37bc9000000059cc51c400000000000e4e1bf0000000064c37bc948d6033d733e27950c2e0351e2505491cd9154824f716d9513514c74b9f98f583dd2b63686a450ec7290df3a1e0b583c0481f651351edfa7636f39aed55cf8a300000005a7462c060000000000d206a2fffffff800000005a5c499380000000000f44b7d010000001c000000200000000064c37bca0000000064c37bca0000000064c37bc900000005a74653cc0000000000d1dedc0000000064c37bc83515b3861e8fe93e5f540ba4077c216404782b86d5e78077b3cbfd27313ab3bce62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43000002a724c9d1000000000032396fc5fffffff8000002a6e3e0fec0000000002ee4815c010000001b000000200000000064c37bca0000000064c37bca0000000064c37bc9000002a724c9d1000000000032396fc50000000064c37bc99b5f73e0075e7d70376012180ddba94272f68d85eae4104e335561c982253d41a19d04ac696c7a6616d291c7e5d1377cc8be437c327b75adb5dc1bad745fcae800000000045152eb000000000001bb63fffffff800000000044de0b500000000000185df0100000015000000160000000064c37bca0000000064c37bca0000000064c37bc900000000045152eb000000000001bb630000000064c37bc8e876fcd130add8984a33aab52af36bc1b9f822c9ebe376f3aa72d630974e15f0dcef50dd0a4cd2dcc17e45df1676dcb336a11a61c69df7a0299b0150c672d25c000000000074990500000000000011d9fffffff80000000000748c2f000000000000116f010000001b000000200000000064c37bca0000000064c37bca0000000064c37bc900000000007498d400000000000011a80000000064c37bc9" -price_info = vaa_to_price_info(id, vaa) - -print(price_info.to_json()) + return PriceUpdate(ema_price, price_attestation["price_id"], price) From f372c1de3b40fec1be7a6247c1d1eaf5f9ac6066 Mon Sep 17 00:00:00 2001 From: Daniel Chew Date: Wed, 2 Aug 2023 14:21:13 +0800 Subject: [PATCH 5/8] add suport for base64 encoding for parseVaa --- pythclient/price_feeds.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pythclient/price_feeds.py b/pythclient/price_feeds.py index b9ddc0a..34a3ea2 100644 --- a/pythclient/price_feeds.py +++ b/pythclient/price_feeds.py @@ -1,3 +1,4 @@ +import base64 import logging from struct import unpack from typing import List @@ -129,9 +130,12 @@ def encode_vaa_for_chain(vaa, vaa_format): # Referenced from https://github.com/wormhole-foundation/wormhole/blob/main/sdk/js/src/vaa/wormhole.ts#L26-L56 -def parse_vaa(vaa): +def parse_vaa(vaa, encoding): if isinstance(vaa, str): - vaa = bytes.fromhex(vaa) + if encoding == "base64": + vaa = base64.b64decode(vaa) + else: + vaa = bytes.fromhex(vaa) num_signers = vaa[5] sig_length = 66 @@ -325,8 +329,8 @@ def parse_price_attestation(bytes_): # Referenced from https://github.com/pyth-network/pyth-crosschain/blob/main/price_service/server/src/rest.ts#L139 -def vaa_to_price_infos(vaa) -> List[PriceInfo]: - parsed_vaa = parse_vaa(vaa) +def vaa_to_price_infos(vaa, encoding="hex") -> List[PriceInfo]: + parsed_vaa = parse_vaa(vaa, encoding) # TODO: support accumulators @@ -351,8 +355,8 @@ def vaa_to_price_infos(vaa) -> List[PriceInfo]: return price_infos -def vaa_to_price_info(price_feed_id, vaa) -> PriceInfo: - price_infos = vaa_to_price_infos(vaa) +def vaa_to_price_info(price_feed_id, vaa, encoding="hex") -> PriceInfo: + price_infos = vaa_to_price_infos(vaa, encoding) for price_info in price_infos: if price_info.price_feed.id == price_feed_id: return price_info From 9fa96e2cc9e0e1aab2d2fdecdf34926cde34f702 Mon Sep 17 00:00:00 2001 From: Daniel Chew Date: Wed, 2 Aug 2023 14:24:06 +0800 Subject: [PATCH 6/8] add test for base64 vaa --- tests/test_price_feeds.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/tests/test_price_feeds.py b/tests/test_price_feeds.py index 81b7c27..549787e 100644 --- a/tests/test_price_feeds.py +++ b/tests/test_price_feeds.py @@ -1,13 +1,33 @@ from pythclient.price_feeds import vaa_to_price_info ID = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43" # BTC/USD -VAA = "01000000030d00ca037dde1419ea373de7474bff80268e41f55e84dd8e4fa5e5167808111e63e4064efcfd93075d9d162090bc478a017603ef56c4d26e0c46e0af6128c08984470002e9abd7c4e62bbc6b453c5ca014b7313bbadb17d6443190d6679b7c0ccb03bae619d21dcb5d6d5a484f9e4fd5586dca7c20403dc80d61dbe682a436b4d15d10bc0003a5e748513b75e34d9db61c6fd0fa55533e8424d88fbe6cda8e7cf61d50cb9467192a5cbd47b14b205b38123ad4b7c39d6ce7d6bf02c4a1452d135dc29fc83ef600049e4cd56e3c2e0cfc4cc831bc4ec54410590c15b4b20b63cc91be3436a4d09c0c22be4ab96962ad6e94d83e14c770e122aebae6fdbdea97f98cec7da5d30ed2a40106a7565c6387a700e02ee09652b40bdeef044441ca1e3b3c9376d4a129d22ca861501c3f5c0e8469c9a0e5d1b09d9f84c6517c0a2b400c0b47552006fff1dad3a5000a4db87004c483f899b5fd766c14334dfb5ca2fa5698964cf9644669b325bd3485207cbc4180a360023d1412da68bb11a0a82fee70a6bf03dda30b7aae53e0e465010ba3a6e45c9d8ef1d1041fdc7a926a9f41075531d45824144bbc720d111ee7270a77dd6dd65558b30d0f03692e075bd7d96cdfb24f5a68fecc22e441ded230c9cc010c09380e394e2b30fd036f13152b115dab7a206270d52255dfbbf0505c67bf510e70d0a6075f9bae19235eaf8a0893a4af9ed0df1b8cd67e1fe7b2ec61178d3ca4010dc491600d07d10a6468fb5955d94bc114efab46104e2ae530931231fea52cf7e32964a1c8bfe0ee38aaa8abfe8edcb7c079b6dd97b2c317c9d71cb5973bb53c72010f787e3c59ac484fdca7d5e41b29cebee08cb1789d61a0f29ccd0353118fd667ab1473a626eb6c237cff70ffb1eb2a556862197b08f183d5852168f5ce0f92632b0110f7ee4abdedc936ebebe86b3493292a9fa6625ab910b4a1340b46478a819508d1261f3d559d5cc95dead635c215b80b1cb2df348639d1ca572d3d14f07dc38908011103e3cdc9936ffbb7c0af5d77a4c092c5c42de161c9254919d19af718defd71a757fcbb1e3772e72c3a8c8291ab36f628a060030abf8ffb43923bb1a05cf9605d0112ddea2ce8ec77b9e222db5f1a95861c3da2ed3f54f7e937008bcc14b2458b98990eeb5910c7e9b2a27ff47a9568d0a3fedc12f357323905cbc8a1be6acbc5986b0064c37bca00000000001af8cd23c2ab91237730770bbea08d61005cdda0984348f3f6eecb559638c0bba0000000002144b1420150325748000300010001020005009d2efa1235ab86c0935cb424b102be4f217e74d1109df9e75dfa8338fc0f0908782f95862b045670cd22bee3114c39763a4a08beeb663b145d283c31d7d1101c4f000000059cc51c400000000000e4e1bffffffff8000000059b3f3c700000000000eae895010000001a0000001e0000000064c37bca0000000064c37bca0000000064c37bc9000000059cc51c400000000000e4e1bf0000000064c37bc948d6033d733e27950c2e0351e2505491cd9154824f716d9513514c74b9f98f583dd2b63686a450ec7290df3a1e0b583c0481f651351edfa7636f39aed55cf8a300000005a7462c060000000000d206a2fffffff800000005a5c499380000000000f44b7d010000001c000000200000000064c37bca0000000064c37bca0000000064c37bc900000005a74653cc0000000000d1dedc0000000064c37bc83515b3861e8fe93e5f540ba4077c216404782b86d5e78077b3cbfd27313ab3bce62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43000002a724c9d1000000000032396fc5fffffff8000002a6e3e0fec0000000002ee4815c010000001b000000200000000064c37bca0000000064c37bca0000000064c37bc9000002a724c9d1000000000032396fc50000000064c37bc99b5f73e0075e7d70376012180ddba94272f68d85eae4104e335561c982253d41a19d04ac696c7a6616d291c7e5d1377cc8be437c327b75adb5dc1bad745fcae800000000045152eb000000000001bb63fffffff800000000044de0b500000000000185df0100000015000000160000000064c37bca0000000064c37bca0000000064c37bc900000000045152eb000000000001bb630000000064c37bc8e876fcd130add8984a33aab52af36bc1b9f822c9ebe376f3aa72d630974e15f0dcef50dd0a4cd2dcc17e45df1676dcb336a11a61c69df7a0299b0150c672d25c000000000074990500000000000011d9fffffff80000000000748c2f000000000000116f010000001b000000200000000064c37bca0000000064c37bca0000000064c37bc900000000007498d400000000000011a80000000064c37bc9" +HEX_VAA = "01000000030d00ca037dde1419ea373de7474bff80268e41f55e84dd8e4fa5e5167808111e63e4064efcfd93075d9d162090bc478a017603ef56c4d26e0c46e0af6128c08984470002e9abd7c4e62bbc6b453c5ca014b7313bbadb17d6443190d6679b7c0ccb03bae619d21dcb5d6d5a484f9e4fd5586dca7c20403dc80d61dbe682a436b4d15d10bc0003a5e748513b75e34d9db61c6fd0fa55533e8424d88fbe6cda8e7cf61d50cb9467192a5cbd47b14b205b38123ad4b7c39d6ce7d6bf02c4a1452d135dc29fc83ef600049e4cd56e3c2e0cfc4cc831bc4ec54410590c15b4b20b63cc91be3436a4d09c0c22be4ab96962ad6e94d83e14c770e122aebae6fdbdea97f98cec7da5d30ed2a40106a7565c6387a700e02ee09652b40bdeef044441ca1e3b3c9376d4a129d22ca861501c3f5c0e8469c9a0e5d1b09d9f84c6517c0a2b400c0b47552006fff1dad3a5000a4db87004c483f899b5fd766c14334dfb5ca2fa5698964cf9644669b325bd3485207cbc4180a360023d1412da68bb11a0a82fee70a6bf03dda30b7aae53e0e465010ba3a6e45c9d8ef1d1041fdc7a926a9f41075531d45824144bbc720d111ee7270a77dd6dd65558b30d0f03692e075bd7d96cdfb24f5a68fecc22e441ded230c9cc010c09380e394e2b30fd036f13152b115dab7a206270d52255dfbbf0505c67bf510e70d0a6075f9bae19235eaf8a0893a4af9ed0df1b8cd67e1fe7b2ec61178d3ca4010dc491600d07d10a6468fb5955d94bc114efab46104e2ae530931231fea52cf7e32964a1c8bfe0ee38aaa8abfe8edcb7c079b6dd97b2c317c9d71cb5973bb53c72010f787e3c59ac484fdca7d5e41b29cebee08cb1789d61a0f29ccd0353118fd667ab1473a626eb6c237cff70ffb1eb2a556862197b08f183d5852168f5ce0f92632b0110f7ee4abdedc936ebebe86b3493292a9fa6625ab910b4a1340b46478a819508d1261f3d559d5cc95dead635c215b80b1cb2df348639d1ca572d3d14f07dc38908011103e3cdc9936ffbb7c0af5d77a4c092c5c42de161c9254919d19af718defd71a757fcbb1e3772e72c3a8c8291ab36f628a060030abf8ffb43923bb1a05cf9605d0112ddea2ce8ec77b9e222db5f1a95861c3da2ed3f54f7e937008bcc14b2458b98990eeb5910c7e9b2a27ff47a9568d0a3fedc12f357323905cbc8a1be6acbc5986b0064c37bca00000000001af8cd23c2ab91237730770bbea08d61005cdda0984348f3f6eecb559638c0bba0000000002144b1420150325748000300010001020005009d2efa1235ab86c0935cb424b102be4f217e74d1109df9e75dfa8338fc0f0908782f95862b045670cd22bee3114c39763a4a08beeb663b145d283c31d7d1101c4f000000059cc51c400000000000e4e1bffffffff8000000059b3f3c700000000000eae895010000001a0000001e0000000064c37bca0000000064c37bca0000000064c37bc9000000059cc51c400000000000e4e1bf0000000064c37bc948d6033d733e27950c2e0351e2505491cd9154824f716d9513514c74b9f98f583dd2b63686a450ec7290df3a1e0b583c0481f651351edfa7636f39aed55cf8a300000005a7462c060000000000d206a2fffffff800000005a5c499380000000000f44b7d010000001c000000200000000064c37bca0000000064c37bca0000000064c37bc900000005a74653cc0000000000d1dedc0000000064c37bc83515b3861e8fe93e5f540ba4077c216404782b86d5e78077b3cbfd27313ab3bce62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43000002a724c9d1000000000032396fc5fffffff8000002a6e3e0fec0000000002ee4815c010000001b000000200000000064c37bca0000000064c37bca0000000064c37bc9000002a724c9d1000000000032396fc50000000064c37bc99b5f73e0075e7d70376012180ddba94272f68d85eae4104e335561c982253d41a19d04ac696c7a6616d291c7e5d1377cc8be437c327b75adb5dc1bad745fcae800000000045152eb000000000001bb63fffffff800000000044de0b500000000000185df0100000015000000160000000064c37bca0000000064c37bca0000000064c37bc900000000045152eb000000000001bb630000000064c37bc8e876fcd130add8984a33aab52af36bc1b9f822c9ebe376f3aa72d630974e15f0dcef50dd0a4cd2dcc17e45df1676dcb336a11a61c69df7a0299b0150c672d25c000000000074990500000000000011d9fffffff80000000000748c2f000000000000116f010000001b000000200000000064c37bca0000000064c37bca0000000064c37bc900000000007498d400000000000011a80000000064c37bc9" +BASE64_VAA = "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" -def test_valid_vaa_to_price_info(): - price_info = vaa_to_price_info(ID, VAA) +def test_valid_hex_vaa_to_price_info(): + price_info = vaa_to_price_info(ID, HEX_VAA) assert price_info.seq_num == 558149954 - assert price_info.vaa == VAA + assert price_info.vaa == HEX_VAA + assert price_info.publish_time == 1690532810 + assert price_info.attestation_time == 1690532810 + assert price_info.last_attested_publish_time == 1690532809 + assert price_info.price_feed.ema_price.price == "2915811000000" + assert price_info.price_feed.ema_price.conf == "786727260" + assert price_info.price_feed.ema_price.expo == -8 + assert price_info.price_feed.ema_price.publish_time == 1690532810 + assert price_info.price_feed.id == ID + assert price_info.price_feed.price.price == "2916900000000" + assert price_info.price_feed.price.conf == "842624965" + assert price_info.price_feed.price.expo == -8 + assert price_info.price_feed.price.publish_time == 1690532810 + assert price_info.emitter_chain_id == 26 + + +def test_valid_base64_vaa_to_price_info(): + price_info = vaa_to_price_info(ID, BASE64_VAA, "base64") + assert price_info.seq_num == 558149954 + assert price_info.vaa == BASE64_VAA assert price_info.publish_time == 1690532810 assert price_info.attestation_time == 1690532810 assert price_info.last_attested_publish_time == 1690532809 @@ -25,7 +45,7 @@ def test_valid_vaa_to_price_info(): def test_invalid_vaa_to_price_info(): try: - vaa_to_price_info(ID, VAA + "1") + vaa_to_price_info(ID, HEX_VAA + "1") except ValueError as ve: assert ( ve.args[0] From 7419926dae80a25599b34783c1edf926f247a087 Mon Sep 17 00:00:00 2001 From: Daniel Chew Date: Wed, 2 Aug 2023 15:02:18 +0800 Subject: [PATCH 7/8] fix encode_vaa_for_chain --- pythclient/price_feeds.py | 59 ++++++++++++++++++++------------------- tests/test_price_feeds.py | 7 ++--- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/pythclient/price_feeds.py b/pythclient/price_feeds.py index 34a3ea2..17d6570 100644 --- a/pythclient/price_feeds.py +++ b/pythclient/price_feeds.py @@ -1,4 +1,5 @@ import base64 +import binascii import logging from struct import unpack from typing import List @@ -10,7 +11,7 @@ P2W_FORMAT_VER_MINOR = 0 P2W_FORMAT_PAYLOAD_ID = 2 -DEFAULT_VAA_ENCODING = "base64" +DEFAULT_VAA_ENCODING = "hex" class Price: @@ -81,7 +82,7 @@ def __str__(self): f"Emitter Chain ID: {self.emitter_chain_id}\n" ) - def to_dict(self, verbose=False, vaa_format=None): + def to_dict(self, verbose=False, vaa_format=DEFAULT_VAA_ENCODING): metadata = ( { "emitter_chain": self.emitter_chain_id, @@ -110,32 +111,40 @@ def to_dict(self, verbose=False, vaa_format=None): # Referenced from https://github.com/pyth-network/pyth-crosschain/blob/main/price_service/server/src/encoding.ts#L24 -def encode_vaa_for_chain(vaa, vaa_format): +def encode_vaa_for_chain(vaa, vaa_format, buffer=False): + # check if vaa is already in vaa_format if isinstance(vaa, str): if vaa_format == DEFAULT_VAA_ENCODING: - return vaa + try: + vaa_buffer = bytes.fromhex(vaa) + except ValueError: + pass # VAA is not in hex format + else: + # VAA is in hex format, return it as it is + return vaa_buffer if buffer else vaa else: - vaa_buffer = ( - bytes.fromhex(vaa) - if vaa.startswith("0x") - else bytes(vaa, encoding=DEFAULT_VAA_ENCODING) - ) + try: + vaa_buffer = base64.b64decode(vaa) + except binascii.Error: + pass # VAA is not in base64 format + else: + # VAA is in base64 format, return it as it is + return vaa_buffer if buffer else vaa + + # Convert VAA to the specified format + if vaa_format == DEFAULT_VAA_ENCODING: + vaa_buffer = base64.b64decode(vaa) + vaa_str = vaa_buffer.hex() else: - vaa_buffer = bytes(vaa) + vaa_buffer = bytes.fromhex(vaa) + vaa_str = base64.b64encode(vaa_buffer).decode("ascii") - if vaa_format == "0x": - return "0x" + vaa_buffer.hex() - else: - return vaa_buffer.decode(vaa_format) + return vaa_buffer if buffer else vaa_str # Referenced from https://github.com/wormhole-foundation/wormhole/blob/main/sdk/js/src/vaa/wormhole.ts#L26-L56 def parse_vaa(vaa, encoding): - if isinstance(vaa, str): - if encoding == "base64": - vaa = base64.b64decode(vaa) - else: - vaa = bytes.fromhex(vaa) + vaa = encode_vaa_for_chain(vaa, encoding, buffer=True) num_signers = vaa[5] sig_length = 66 @@ -329,18 +338,12 @@ def parse_price_attestation(bytes_): # Referenced from https://github.com/pyth-network/pyth-crosschain/blob/main/price_service/server/src/rest.ts#L139 -def vaa_to_price_infos(vaa, encoding="hex") -> List[PriceInfo]: +def vaa_to_price_infos(vaa, encoding=DEFAULT_VAA_ENCODING) -> List[PriceInfo]: parsed_vaa = parse_vaa(vaa, encoding) # TODO: support accumulators - try: - batch_attestation = parse_batch_price_attestation(parsed_vaa["payload"]) - except Exception as e: - logging.error(e) - logging.error(f"Parsing historical VAA failed: {parsed_vaa}") - return None - + batch_attestation = parse_batch_price_attestation(parsed_vaa["payload"]) price_infos = [] for price_attestation in batch_attestation["price_attestations"]: price_infos.append( @@ -355,7 +358,7 @@ def vaa_to_price_infos(vaa, encoding="hex") -> List[PriceInfo]: return price_infos -def vaa_to_price_info(price_feed_id, vaa, encoding="hex") -> PriceInfo: +def vaa_to_price_info(price_feed_id, vaa, encoding=DEFAULT_VAA_ENCODING) -> PriceInfo: price_infos = vaa_to_price_infos(vaa, encoding) for price_info in price_infos: if price_info.price_feed.id == price_feed_id: diff --git a/tests/test_price_feeds.py b/tests/test_price_feeds.py index 549787e..f760d49 100644 --- a/tests/test_price_feeds.py +++ b/tests/test_price_feeds.py @@ -45,9 +45,6 @@ def test_valid_base64_vaa_to_price_info(): def test_invalid_vaa_to_price_info(): try: - vaa_to_price_info(ID, HEX_VAA + "1") + vaa_to_price_info(ID, HEX_VAA + "10") except ValueError as ve: - assert ( - ve.args[0] - == "non-hexadecimal number found in fromhex() arg at position 3431" - ) + assert ve.args[0] == "Invalid length: 801, expected: 800" From 324b60f61329d9f547cc395332b9868233fb2fb1 Mon Sep 17 00:00:00 2001 From: Daniel Chew Date: Wed, 2 Aug 2023 15:05:39 +0800 Subject: [PATCH 8/8] add test for encode_vaa_for_chain --- tests/test_price_feeds.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/test_price_feeds.py b/tests/test_price_feeds.py index f760d49..3cfa5f6 100644 --- a/tests/test_price_feeds.py +++ b/tests/test_price_feeds.py @@ -1,4 +1,4 @@ -from pythclient.price_feeds import vaa_to_price_info +from pythclient.price_feeds import encode_vaa_for_chain, vaa_to_price_info ID = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43" # BTC/USD HEX_VAA = "01000000030d00ca037dde1419ea373de7474bff80268e41f55e84dd8e4fa5e5167808111e63e4064efcfd93075d9d162090bc478a017603ef56c4d26e0c46e0af6128c08984470002e9abd7c4e62bbc6b453c5ca014b7313bbadb17d6443190d6679b7c0ccb03bae619d21dcb5d6d5a484f9e4fd5586dca7c20403dc80d61dbe682a436b4d15d10bc0003a5e748513b75e34d9db61c6fd0fa55533e8424d88fbe6cda8e7cf61d50cb9467192a5cbd47b14b205b38123ad4b7c39d6ce7d6bf02c4a1452d135dc29fc83ef600049e4cd56e3c2e0cfc4cc831bc4ec54410590c15b4b20b63cc91be3436a4d09c0c22be4ab96962ad6e94d83e14c770e122aebae6fdbdea97f98cec7da5d30ed2a40106a7565c6387a700e02ee09652b40bdeef044441ca1e3b3c9376d4a129d22ca861501c3f5c0e8469c9a0e5d1b09d9f84c6517c0a2b400c0b47552006fff1dad3a5000a4db87004c483f899b5fd766c14334dfb5ca2fa5698964cf9644669b325bd3485207cbc4180a360023d1412da68bb11a0a82fee70a6bf03dda30b7aae53e0e465010ba3a6e45c9d8ef1d1041fdc7a926a9f41075531d45824144bbc720d111ee7270a77dd6dd65558b30d0f03692e075bd7d96cdfb24f5a68fecc22e441ded230c9cc010c09380e394e2b30fd036f13152b115dab7a206270d52255dfbbf0505c67bf510e70d0a6075f9bae19235eaf8a0893a4af9ed0df1b8cd67e1fe7b2ec61178d3ca4010dc491600d07d10a6468fb5955d94bc114efab46104e2ae530931231fea52cf7e32964a1c8bfe0ee38aaa8abfe8edcb7c079b6dd97b2c317c9d71cb5973bb53c72010f787e3c59ac484fdca7d5e41b29cebee08cb1789d61a0f29ccd0353118fd667ab1473a626eb6c237cff70ffb1eb2a556862197b08f183d5852168f5ce0f92632b0110f7ee4abdedc936ebebe86b3493292a9fa6625ab910b4a1340b46478a819508d1261f3d559d5cc95dead635c215b80b1cb2df348639d1ca572d3d14f07dc38908011103e3cdc9936ffbb7c0af5d77a4c092c5c42de161c9254919d19af718defd71a757fcbb1e3772e72c3a8c8291ab36f628a060030abf8ffb43923bb1a05cf9605d0112ddea2ce8ec77b9e222db5f1a95861c3da2ed3f54f7e937008bcc14b2458b98990eeb5910c7e9b2a27ff47a9568d0a3fedc12f357323905cbc8a1be6acbc5986b0064c37bca00000000001af8cd23c2ab91237730770bbea08d61005cdda0984348f3f6eecb559638c0bba0000000002144b1420150325748000300010001020005009d2efa1235ab86c0935cb424b102be4f217e74d1109df9e75dfa8338fc0f0908782f95862b045670cd22bee3114c39763a4a08beeb663b145d283c31d7d1101c4f000000059cc51c400000000000e4e1bffffffff8000000059b3f3c700000000000eae895010000001a0000001e0000000064c37bca0000000064c37bca0000000064c37bc9000000059cc51c400000000000e4e1bf0000000064c37bc948d6033d733e27950c2e0351e2505491cd9154824f716d9513514c74b9f98f583dd2b63686a450ec7290df3a1e0b583c0481f651351edfa7636f39aed55cf8a300000005a7462c060000000000d206a2fffffff800000005a5c499380000000000f44b7d010000001c000000200000000064c37bca0000000064c37bca0000000064c37bc900000005a74653cc0000000000d1dedc0000000064c37bc83515b3861e8fe93e5f540ba4077c216404782b86d5e78077b3cbfd27313ab3bce62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43000002a724c9d1000000000032396fc5fffffff8000002a6e3e0fec0000000002ee4815c010000001b000000200000000064c37bca0000000064c37bca0000000064c37bc9000002a724c9d1000000000032396fc50000000064c37bc99b5f73e0075e7d70376012180ddba94272f68d85eae4104e335561c982253d41a19d04ac696c7a6616d291c7e5d1377cc8be437c327b75adb5dc1bad745fcae800000000045152eb000000000001bb63fffffff800000000044de0b500000000000185df0100000015000000160000000064c37bca0000000064c37bca0000000064c37bc900000000045152eb000000000001bb630000000064c37bc8e876fcd130add8984a33aab52af36bc1b9f822c9ebe376f3aa72d630974e15f0dcef50dd0a4cd2dcc17e45df1676dcb336a11a61c69df7a0299b0150c672d25c000000000074990500000000000011d9fffffff80000000000748c2f000000000000116f010000001b000000200000000064c37bca0000000064c37bca0000000064c37bc900000000007498d400000000000011a80000000064c37bc9" @@ -48,3 +48,21 @@ def test_invalid_vaa_to_price_info(): vaa_to_price_info(ID, HEX_VAA + "10") except ValueError as ve: assert ve.args[0] == "Invalid length: 801, expected: 800" + + +def test_encode_vaa_for_chain(): + # Test that encoding a hex VAA as hex returns the same hex VAA + encoded_vaa = encode_vaa_for_chain(HEX_VAA, "hex") + assert encoded_vaa == HEX_VAA + + # Test that encoding a HEX VAA as base64 returns base64 VAA + encoded_vaa = encode_vaa_for_chain(HEX_VAA, "base64") + assert encoded_vaa == BASE64_VAA + + # Test that encoding a base64 VAA as base64 returns the same base64 VAA + encoded_vaa = encode_vaa_for_chain(BASE64_VAA, "base64") + assert encoded_vaa == BASE64_VAA + + # Test that encoding a base64 VAA as hex returns hex VAA + encoded_vaa = encode_vaa_for_chain(BASE64_VAA, "hex") + assert encoded_vaa == HEX_VAA