Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5984c19
cherrypick part of changes
Dec 1, 2021
613a469
create indexer.py
Dec 1, 2021
c0d0e6c
remove solana_receipts_update.py
Dec 1, 2021
3f6b5c4
Cherry pick files from old branch
Dec 1, 2021
0790298
add requirement
Dec 1, 2021
340c854
fix refactoring issues
Dec 1, 2021
7449b38
Fix inspection issues
Dec 1, 2021
b3dacfa
fix last issue
Dec 1, 2021
a50cd47
Merge branch '336_indexer_refactoring' into 337_сreate_base_airdroppe…
Dec 1, 2021
2b8f879
Merge remote-tracking branch 'origin/develop' into 337_сreate_base_ai…
Dec 2, 2021
f51f2ed
simplify tests
Dec 2, 2021
6678924
add test
Dec 2, 2021
5d454b7
Merge remote-tracking branch 'origin/develop' into 337_сreate_base_ai…
Dec 3, 2021
add136a
add price provider
Dec 1, 2021
9a4be44
fix PriceProvider, add test
Dec 1, 2021
07aaca8
Add tests. Check worn on all nets
Dec 1, 2021
7a46c12
refactoring
Dec 1, 2021
2fc1424
integrate price_provider into airdropper
Dec 2, 2021
d157d67
integrate price provider
Dec 2, 2021
8a6abfd
use new faucet method
Dec 3, 2021
3d4dec9
add new parameter to airdropper main
Dec 3, 2021
5c29832
Test discriptions for airdropper
Dec 3, 2021
6a4efdc
Comments for price provider tests
Dec 3, 2021
14aeeed
remove unnecessary comment
Dec 3, 2021
bd35791
Merge remote-tracking branch 'origin/develop' into 338_create_sol_pri…
Dec 3, 2021
ff1f557
Merge remote-tracking branch 'origin/develop' into 338_create_sol_pri…
Dec 6, 2021
c28ca8e
fix error
Dec 6, 2021
8c68755
Merge remote-tracking branch 'origin/develop' into 338_create_sol_pri…
Dec 7, 2021
5325895
Merge remote-tracking branch 'origin/develop' into 338_create_sol_pri…
Dec 8, 2021
a1cbad2
fix airdropper run
Dec 10, 2021
4ed6d78
remove duplicated code
Dec 15, 2021
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
Prev Previous commit
Next Next commit
Add tests. Check worn on all nets
  • Loading branch information
ivanl committed Dec 3, 2021
commit 07aaca8f79d6b05f5de74428f1fb82c280e83855
41 changes: 34 additions & 7 deletions proxy/indexer/price_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,49 @@ def _unpack_field(raw_data: bytes, field_name: str):
return struct.unpack(field['format'], raw_data[start_idx:stop_idx])[0]


price_accounts = {
'SOL/USD': 'H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG'
}
mainnet_solana = "https://api.mainnet-beta.solana.com"
devnet_solana = "https://api.devnet.solana.com"
testnet_solana = "https://api.testnet.solana.com"


# See available price accounts at https://pyth.network/developers/accounts/
mainnet_price_accounts = { 'SOL/USD': 'H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG' }
devnet_price_accounts = { 'SOL/USD': "J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix" }
testnet_price_accounts = { 'SOL/USD': "7VJsBtJzgTftYzEeooSDYyjKXvYRWJHdwvbwfBvTg9K" }


PRICE_STATUS_UNKNOWN = 0
PRICE_STATUS_TRADING = 1


class PriceProvider:
def __init__(self, solana_url, default_upd_int):
self.client = Client(solana_url)
def __init__(self, net: str, default_upd_int: int, price_accounts=None):
self.default_upd_int = default_upd_int
self.prices = {}

if net == "mainnet":
self.client = Client(mainnet_solana)
self.price_accounts = mainnet_price_accounts
elif net == "devnet":
self.client = Client(devnet_solana)
self.price_accounts = devnet_price_accounts
elif net == "testnet":
self.client = Client(testnet_solana)
self.price_accounts = testnet_price_accounts
else:
self.client = Client(net)
if price_accounts is None:
self.price_accounts = {}
else:
self.price_accounts = price_accounts


def _get_current_time(self):
return datetime.now().timestamp()


def _read_price(self, pairname):
acc_id = price_accounts.get(pairname, None)
acc_id = self.price_accounts.get(pairname, None)
if acc_id is None:
logger.warning(f'No account found for pair {pairname}')
return None
Expand Down Expand Up @@ -83,7 +110,7 @@ def _read_price(self, pairname):

def get_price(self, pairname):
price_data = self.prices.get(pairname, None)
current_time = datetime.now().timestamp()
current_time = self._get_current_time()

if price_data == None or current_time - price_data['last_update'] >= self.default_upd_int:
current_price = self._read_price(pairname)
Expand Down
156 changes: 134 additions & 22 deletions proxy/testing/test_price_provider.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
from proxy.indexer.price_provider import PriceProvider, field_info, PRICE_STATUS_TRADING, PRICE_STATUS_UNKNOWN, price_accounts
from proxy.indexer.price_provider import PriceProvider, field_info, PRICE_STATUS_TRADING,\
PRICE_STATUS_UNKNOWN, testnet_price_accounts, devnet_price_accounts
from unittest import TestCase
from unittest.mock import patch, MagicMock, call
from datetime import datetime
from solana.rpc.api import Client
from solana.publickey import PublicKey
from struct import pack
from random import uniform
import base58, base64


class TestPriceProvider(TestCase):
@classmethod
def setUpClass(cls) -> None:
print("Testing PriceProvider")
cls.solana_url = 'https://api.mainnet-beta.solana.com' # use contract in mainnet
cls.default_upd_int = 10


def setUp(self) -> None:
self.price_provider = PriceProvider(self.solana_url,
self.default_upd_int)
self.testnet_price_provider = PriceProvider("testnet", self.default_upd_int)

def _create_price_account_info(self, price: float, status: int):
def _create_price_account_info(self, price: float, status: int, enc: str):
# Follow link https://github.com/pyth-network/pyth-client-rs/blob/main/src/lib.rs
# for details on structure of pyth.network price accounts.
# Current implementation of PriceProvider uses only few fields of account
Expand All @@ -41,40 +40,153 @@ def _create_price_account_info(self, price: float, status: int):
data += pack(field_info['agg.status']['format'], status)
# rest of data array is not used by PriceProvier so no need to fill it

if enc == 'base58':
data = base58.b58encode(data)
elif enc == 'base64':
data = base64.b64encode(data)
else:
raise Exception(f"Unsupported encoding: {enc}")

return {
'result': {
'value': {
'data': [
data,
'base64'
enc
]
}
}
}

@patch.object(Client, 'get_account_info')
@patch.object(datetime, 'now')
def test_success_read_price_two_times_with_small_interval(self, mock_now, mock_get_account_info):
print("Should call get_price two times but will cause only one call to get_account_info")
mock_nowtime = MagicMock()
mock_timestamp = MagicMock()
mock_nowtime.timestamp = mock_timestamp

@patch.object(PriceProvider, '_get_current_time')
def test_success_read_price_two_times_with_small_interval(self, mock_get_current_time, mock_get_account_info):
print("\n\nTesting two sequential calls with small interval. Should read account once")
# some random time
first_call_time = uniform(0, 100000)
# not enough time left to cause second account reload
second_call_time = first_call_time + self.default_upd_int - 1

mock_now.side_effect = [mock_nowtime, mock_nowtime]
mock_timestamp.side_effect = [ first_call_time, second_call_time]
mock_get_current_time.side_effect = [ first_call_time, second_call_time]

current_price = 315.0
mock_get_account_info.side_effect = [self._create_price_account_info(current_price,
PRICE_STATUS_TRADING,
'base58')]

pair_name = 'SOL/USD'
self.assertEqual(self.testnet_price_provider.get_price(pair_name), current_price)
self.assertEqual(self.testnet_price_provider.get_price(pair_name), current_price)

mock_get_current_time.assert_has_calls([call(), call()])
mock_get_account_info.assert_called_once_with(PublicKey(testnet_price_accounts[pair_name]))


@patch.object(Client, 'get_account_info')
@patch.object(PriceProvider, '_get_current_time')
def test_success_read_price_two_times_with_long_interval_diff_encodings(self, mock_get_current_time, mock_get_account_info):
print("\n\nTesting two sequential calls with long interval. Should read account twice")
# some random time
first_call_time = uniform(0, 100000)
# Time interval between 1st and 2nd calls are larger that reload interval
second_call_time = first_call_time + self.default_upd_int + 2

mock_get_current_time.side_effect = [ first_call_time, second_call_time]

current_price = 315.0
mock_get_account_info.side_effect = [self._create_price_account_info(current_price,
PRICE_STATUS_TRADING,
'base58'),
self._create_price_account_info(current_price,
PRICE_STATUS_TRADING,
'base64')]

pair_name = 'SOL/USD'
self.assertEqual(self.testnet_price_provider.get_price(pair_name), current_price)
self.assertEqual(self.testnet_price_provider.get_price(pair_name), current_price)

price_acc_key = PublicKey(testnet_price_accounts[pair_name])
mock_get_current_time.assert_has_calls([call(), call()])
mock_get_account_info.assert_has_calls([call(price_acc_key), call(price_acc_key)])

@patch.object(Client, 'get_account_info')
@patch.object(PriceProvider, '_get_current_time')
def test_faile_get_price_price_status_not_trading(self, mock_get_current_time, mock_get_account_info):
print("\n\nget_price call should return None because last price account data is not trading")
# some random time
first_call_time = uniform(0, 100000)

mock_get_current_time.side_effect = [first_call_time]

current_price = 315.0
mock_get_account_info.side_effect = [self._create_price_account_info(current_price, PRICE_STATUS_TRADING)]
mock_get_account_info.side_effect = [self._create_price_account_info(current_price,
PRICE_STATUS_UNKNOWN,
'base58')]

pair_name = 'SOL/USD'
self.assertEqual(self.price_provider.get_price(pair_name), current_price)
self.assertEqual(self.price_provider.get_price(pair_name), current_price)
self.assertEqual(self.testnet_price_provider.get_price(pair_name), None)

price_acc_key = PublicKey(testnet_price_accounts[pair_name])
mock_get_current_time.assert_has_calls([call()])
mock_get_account_info.assert_has_calls([call(price_acc_key)])


@patch.object(Client, 'get_account_info')
@patch.object(PriceProvider, '_get_current_time')
def test_failed_read_account_not_found(self, mock_get_current_time, mock_get_account_info):
print("\n\nAccount reading will fail due to unknown pair provided")
# some random time
first_call_time = uniform(0, 100000)
mock_get_current_time.side_effect = [ first_call_time ]

pair_name = 'RUB/USD' # Unknown pair
self.assertEqual(self.testnet_price_provider.get_price(pair_name), None)

mock_get_current_time.assert_has_calls([call()])
mock_get_account_info.assert_not_called()


@patch.object(Client, 'get_account_info')
@patch.object(PriceProvider, '_get_current_time')
def test_failed_second_acc_read_will_return_previous_result(self, mock_get_current_time, mock_get_account_info):
print("\n\nTesting two sequential calls with long interval. Second call will fail. Provider should return previous price")
# some random time
first_call_time = uniform(0, 100000)
# Time interval between 1st and 2nd calls are larger that reload interval
second_call_time = first_call_time + self.default_upd_int + 2

mock_get_current_time.side_effect = [ first_call_time, second_call_time]

current_price = 315.0
mock_get_account_info.side_effect = [self._create_price_account_info(current_price,
PRICE_STATUS_TRADING,
'base58'),
{'result':{}}] # << Wrong message format

pair_name = 'SOL/USD'
self.assertEqual(self.testnet_price_provider.get_price(pair_name), current_price)
self.assertEqual(self.testnet_price_provider.get_price(pair_name), current_price)

price_acc_key = PublicKey(testnet_price_accounts[pair_name])
mock_get_current_time.assert_has_calls([call(), call()])
mock_get_account_info.assert_has_calls([call(price_acc_key), call(price_acc_key)])


def test_compare_mainnet_testnet_data(self):
print("\n\nShould return correct prices on all Solana nets")
pair_name = 'SOL/USD'

devnet_price_provider = PriceProvider("devnet", self.default_upd_int)
mainnet_price_provider = PriceProvider("mainnet", self.default_upd_int)

devnet_price = devnet_price_provider.get_price(pair_name)
testnet_price = self.testnet_price_provider.get_price(pair_name)
mainnet_price = mainnet_price_provider.get_price(pair_name)

print(f"Solana devnet: SOL/USD = {devnet_price}")
print(f"Solana testnet: SOL/USD = {testnet_price}")
print(f"Solana mainnet: SOL/USD = {mainnet_price}")

mock_now.assert_has_calls([call(),call()])
mock_timestamp.assert_has_calls([call(), call()])
mock_get_account_info.assert_called_once_with(PublicKey(price_accounts[pair_name]))
self.assertTrue(devnet_price is not None)
self.assertTrue(testnet_price is not None)
self.assertTrue(mainnet_price is not None)