diff --git a/pysdk/.pre-commit-config.yaml b/pysdk/.pre-commit-config.yaml index cb77aaf8..47d82000 100644 --- a/pysdk/.pre-commit-config.yaml +++ b/pysdk/.pre-commit-config.yaml @@ -7,14 +7,14 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - - id: end-of-file-fixer - exclude: nibiru/proto/.+ + - id: end-of-file-fixer + exclude: nibiru/proto/.+ - - id: trailing-whitespace - exclude: nibiru/proto/.+ + - id: trailing-whitespace + exclude: nibiru/proto/.+ - repo: https://github.com/psf/black - rev: 22.12.0 + rev: 23.7.0 hooks: - id: black files: \.py$ @@ -26,9 +26,10 @@ repos: - id: isort files: \.py$ exclude: nibiru/proto/.+ - args: [ "--profile", "black" ] + args: ["--profile", "black"] - repo: https://github.com/pycqa/flake8 rev: 6.0.0 hooks: - id: flake8 + args: ["--max-line-length", "120"] diff --git a/pysdk/pysdk/query_clients/perp.py b/pysdk/pysdk/query_clients/perp.py index 1039d012..4ef83ac0 100644 --- a/pysdk/pysdk/query_clients/perp.py +++ b/pysdk/pysdk/query_clients/perp.py @@ -6,12 +6,13 @@ from nibiru_proto.nibiru.perp.v2 import query_pb2_grpc as perp_query from pysdk.query_clients.util import QueryClient, deserialize -from pysdk.utils import from_sdk_dec +from pysdk.utils import from_sdk_dec, update_nested_fields class PerpQueryClient(QueryClient): """ - Perp allows to query the endpoints made available by the Nibiru Chain's PERP module. + Perp allows to query the endpoints made available by the Nibiru Chain's + PERP module. """ def __init__(self, channel: Channel): @@ -57,9 +58,41 @@ def params(self): return output + def markets(self): + """ + Get the all markets infromation. + """ + proto_output: perp_type.QueryMarketsResponse = self.query( + api_callable=self.api.QueryMarkets, + req=perp_type.QueryMarketsRequest(), + should_deserialize=False, + ) + + output = MessageToDict(proto_output) + + fields = [ + "ammMarkets.amm.baseReserve", + "ammMarkets.amm.quoteReserve", + "ammMarkets.amm.sqrtDepth", + "ammMarkets.amm.priceMultiplier", + "ammMarkets.amm.totalLong", + "ammMarkets.amm.totalShort", + "ammMarkets.market.maintenanceMarginRatio", + "ammMarkets.market.maxLeverage", + "ammMarkets.market.latestCumulativePremiumFraction", + "ammMarkets.market.exchangeFeeRatio", + "ammMarkets.market.ecosystemFundFeeRatio", + "ammMarkets.market.liquidationFeeRatio", + "ammMarkets.market.partialLiquidationRatio", + ] + + output = update_nested_fields(output, fields, from_sdk_dec) + return output + def position(self, pair: str, trader: str) -> dict: """ - Get the trader position. Returns information about position notional, margin ratio + Get the trader position. Returns information about position notional, + margin ratio unrealized pnl, size of the position etc. Args: diff --git a/pysdk/pysdk/utils.py b/pysdk/pysdk/utils.py index fbfc8e0b..579cb2df 100644 --- a/pysdk/pysdk/utils.py +++ b/pysdk/pysdk/utils.py @@ -12,7 +12,7 @@ # reimplementation of cosmos-sdk/types/decimal.go def to_sdk_dec(dec: float) -> str: - ''' + """ create a decimal from an input decimal. valid must come in the form: (-) whole integers (.) decimal integers @@ -26,52 +26,52 @@ def to_sdk_dec(dec: float) -> str: are provided in the string than the constant Precision. CONTRACT - This function does not mutate the input str. - ''' + """ dec_str = str(dec) if len(dec_str) == 0: - raise TypeError(f'Expected decimal string but got: {dec_str}') + raise TypeError(f"Expected decimal string but got: {dec_str}") # first extract any negative symbol neg = False - if dec_str[0] == '-': + if dec_str[0] == "-": neg = True dec_str = dec_str[1:] if len(dec_str) == 0: - raise TypeError(f'Expected decimal string but got: {dec_str}') + raise TypeError(f"Expected decimal string but got: {dec_str}") - strs = dec_str.split('.') + strs = dec_str.split(".") len_decs = 0 combined_str = strs[0] if len(strs) == 2: # has a decimal place len_decs = len(strs[1]) if len_decs == 0 or len(combined_str) == 0: - raise TypeError(f'Expected decimal string but got: {dec_str}') + raise TypeError(f"Expected decimal string but got: {dec_str}") combined_str += strs[1] elif len(strs) > 2: - raise TypeError(f'Expected decimal string but got: {dec_str}') + raise TypeError(f"Expected decimal string but got: {dec_str}") if len_decs > PRECISION: raise TypeError( - f'value \'{dec_str}\' exceeds max precision by {PRECISION-len_decs} decimal places: max precision {PRECISION}' + f"value '{dec_str}' exceeds max precision by {PRECISION-len_decs} decimal places: max precision {PRECISION}" ) # add some extra zero's to correct to the Precision factor zeros_to_add = PRECISION - len_decs - zeros = '0' * zeros_to_add + zeros = "0" * zeros_to_add combined_str += zeros try: int(combined_str, 10) except ValueError as err: raise ValueError( - f'failed to set decimal string with base 10: {combined_str}' + f"failed to set decimal string with base 10: {combined_str}" ) from err if neg: - return '-' + combined_str + return "-" + combined_str return combined_str @@ -120,31 +120,31 @@ def format_fields_nested( def from_sdk_dec(dec_str: str) -> float: - if dec_str is None or dec_str == '': + if dec_str is None or dec_str == "": return 0 - if '.' in dec_str: - raise TypeError(f'expected a decimal string but got {dec_str} containing \'.\'') + if "." in dec_str: + raise TypeError(f"expected a decimal string but got {dec_str} containing '.'") try: int(dec_str) except ValueError as err: - raise ValueError(f'failed to convert {dec_str} to a number') from err + raise ValueError(f"failed to convert {dec_str} to a number") from err neg = False - if dec_str[0] == '-': + if dec_str[0] == "-": neg = True dec_str = dec_str[1:] input_size = len(dec_str) - bz_str = '' + bz_str = "" # case 1, purely decimal if input_size <= PRECISION: # 0. prefix - bz_str = '0.' + bz_str = "0." # set relevant digits to 0 - bz_str += '0' * (PRECISION - input_size) + bz_str += "0" * (PRECISION - input_size) # set final digits bz_str += dec_str @@ -153,11 +153,11 @@ def from_sdk_dec(dec_str: str) -> float: dec_point_place = input_size - PRECISION bz_str = dec_str[:dec_point_place] # pre-decimal digits - bz_str += '.' # decimal point + bz_str += "." # decimal point bz_str += dec_str[dec_point_place:] # pre-decimal digits if neg: - bz_str = '-' + bz_str + bz_str = "-" + bz_str return float(bz_str) @@ -386,3 +386,73 @@ def _count_diff_hashable(actual, expected): diff = _Mismatch(0, cnt_t, elem) result.append(diff) return result + + +def update_nested_fields( + d: dict, fields: List[str], func: Callable[[Any], Any] +) -> dict: + """ + Update specified fields in a nested dictionary. + + This function recursively traverses a dictionary and updates specified fields + using a provided function. Field names should be specified as a list of strings, + where each string represents a nested path to the key that should be updated + (e.g., "key1.key2.key3"). + + Args: + d: The dictionary to update. + fields: A list of fields to update, specified as strings representing key paths. + func: A function to apply to each specified field. + + Returns: + The updated dictionary. + """ + fields_set = {tuple(field.split(".")) for field in fields} + + def helper(d: Union[dict, list], keys: tuple): + if isinstance(d, dict): + for key, value in d.items(): + new_keys = keys + (key,) + if new_keys in fields_set: + d[key] = func(value) + elif isinstance(value, (dict, list)): + helper(value, new_keys) + elif isinstance(d, list): + for item in d: + helper(item, keys) + + helper(d, ()) + return d + + +def assert_subset(result, expected, path=None): + """ + Check if all values in expected are equal to the values in result. + + This function iteratively checks if the expected values match the values in result. It traverses nested dictionaries + and lists, checking for matching values while ignoring missing keys in the expected dictionary. + + Args: + result: The result dictionary. + expected: The expected dictionary. + path: The path to the current key (used for error messages). + + Raises: + AssertionError: If a value in expected does not match the corresponding value in result. + """ + if path is None: + path = [] + + for key, value in expected.items(): + if isinstance(value, dict): + assert key in result, f"Key {key} not found in result at path {path}" + assert_subset(result[key], value, path + [key]) + elif isinstance(value, list): + assert key in result, f"Key {key} not found in result at path {path}" + for i, item in enumerate(value): + assert_subset(result[key][i], item, path + [key, i]) + else: + assert key in result, f"Key {key} not found in result at path {path}" + assert ( + result[key] == value + ), f"Value {result[key]} at path {path + [key]} does not match expected value {value}" diff --git a/pysdk/scripts/docgen.sh b/pysdk/scripts/docgen.sh old mode 100644 new mode 100755 diff --git a/pysdk/scripts/fmt.sh b/pysdk/scripts/fmt.sh old mode 100644 new mode 100755 diff --git a/pysdk/scripts/get_nibid.sh b/pysdk/scripts/get_nibid.sh old mode 100644 new mode 100755 diff --git a/pysdk/scripts/get_pricefeeder.sh b/pysdk/scripts/get_pricefeeder.sh old mode 100644 new mode 100755 diff --git a/pysdk/scripts/localnet.sh b/pysdk/scripts/localnet.sh old mode 100644 new mode 100755 index 0a718efa..a2799055 --- a/pysdk/scripts/localnet.sh +++ b/pysdk/scripts/localnet.sh @@ -36,9 +36,9 @@ echo_success() { # Flag parsing: --flag-name (BASH_VAR_NAME) # -# --no-build ($FLAG_NO_BUILD): toggles whether to build from source. The default -# behavior of the script is to run make install. -FLAG_NO_BUILD=false +# --no-build ($FLAG_NO_BUILD): toggles whether to build from source. The default +# behavior of the script is to run make install. +FLAG_NO_BUILD=false build_from_source() { echo_info "Building from source..." diff --git a/pysdk/scripts/run_pricefeed.sh b/pysdk/scripts/run_pricefeed.sh old mode 100644 new mode 100755 diff --git a/pysdk/tests/perp_test.py b/pysdk/tests/perp_test.py index 598f9f0e..72a1778b 100644 --- a/pysdk/tests/perp_test.py +++ b/pysdk/tests/perp_test.py @@ -7,6 +7,7 @@ import tests from pysdk import Msg from pysdk import pytypes as pt +from pysdk.utils import assert_subset PRECISION = 6 @@ -81,6 +82,66 @@ def test_perp_query_position(sdk_val: pysdk.Sdk): tests.raises(ok_errors, err) +def test_perp_query_market(sdk_val: pysdk.Sdk): + output = sdk_val.query.perp.markets() + + expected_to_be_equal = { + "ammMarkets": [ + { + "market": { + "pair": "ubtc:unusd", + "enabled": True, + "maintenanceMarginRatio": 0.0625, + "maxLeverage": 10.0, + "latestCumulativePremiumFraction": 0.0, + "exchangeFeeRatio": 0.001, + "ecosystemFundFeeRatio": 0.001, + "liquidationFeeRatio": 0.05, + "partialLiquidationRatio": 0.5, + "fundingRateEpochId": "30 min", + "twapLookbackWindow": "1800s", + "prepaidBadDebt": {"denom": "unusd", "amount": "0"}, + }, + "amm": { + "pair": "ubtc:unusd", + "baseReserve": 30000000000000.0, + "quoteReserve": 30000000000000.0, + "sqrtDepth": 30000000000000.0, + "totalLong": 0.0, + "totalShort": 0.0, + }, + }, + { + "market": { + "pair": "ueth:unusd", + "enabled": True, + "maintenanceMarginRatio": 0.0625, + "maxLeverage": 10.0, + "latestCumulativePremiumFraction": 0.0, + "exchangeFeeRatio": 0.001, + "ecosystemFundFeeRatio": 0.001, + "liquidationFeeRatio": 0.05, + "partialLiquidationRatio": 0.5, + "fundingRateEpochId": "30 min", + "twapLookbackWindow": "1800s", + "prepaidBadDebt": {"denom": "unusd", "amount": "0"}, + }, + "amm": { + "pair": "ueth:unusd", + "baseReserve": 30000000000000.0, + "quoteReserve": 30000000000000.0, + "sqrtDepth": 30000000000000.0, + "totalLong": 0.0, + "totalShort": 0.0, + }, + }, + ] + } + + # have to assert subset since i don't want to check the price which can change in loclanet + assert_subset(output, expected_to_be_equal) + + @pytest.mark.order(after="test_perp_query_position") def test_perp_query_all_positions(sdk_val: pysdk.Sdk): positions_map: Dict[str, dict] = sdk_val.query.perp.all_positions( @@ -96,10 +157,10 @@ def test_perp_query_all_positions(sdk_val: pysdk.Sdk): tests.dict_keys_must_match( position_resp, [ - 'position', - 'position_notional', - 'unrealized_pnl', - 'margin_ratio', + "position", + "position_notional", + "unrealized_pnl", + "margin_ratio", ], ) diff --git a/pysdk/tests/utils_test.py b/pysdk/tests/utils_test.py index 8bafaa10..b154aad9 100644 --- a/pysdk/tests/utils_test.py +++ b/pysdk/tests/utils_test.py @@ -14,18 +14,18 @@ @pytest.mark.parametrize( "test_name,float_val,sdk_dec_val,should_fail", [ - ('empty string', '', '', True), + ("empty string", "", "", True), # valid numbers - ('number 0', 0, '0' + '0' * 18, False), - ('number 10', 10, '10' + '0' * 18, False), - ('number 123', 123, '123' + '0' * 18, False), - ('neg. number 123', -123, '-123' + '0' * 18, False), + ("number 0", 0, "0" + "0" * 18, False), + ("number 10", 10, "10" + "0" * 18, False), + ("number 123", 123, "123" + "0" * 18, False), + ("neg. number 123", -123, "-123" + "0" * 18, False), # with fractional - ('missing mantisse', 0.3, '03' + '0' * 17, False), - ('number 0.5', 0.5, '05' + '0' * 17, False), - ('number 13.235', 13.235, '13235' + '0' * 15, False), - ('neg. number 13.235', -13.235, '-13235' + '0' * 15, False), - ('number 1574.00005', 1574.00005, '157400005' + '0' * 13, False), + ("missing mantisse", 0.3, "03" + "0" * 17, False), + ("number 0.5", 0.5, "05" + "0" * 17, False), + ("number 13.235", 13.235, "13235" + "0" * 15, False), + ("neg. number 13.235", -13.235, "-13235" + "0" * 15, False), + ("number 1574.00005", 1574.00005, "157400005" + "0" * 13, False), ], ) def test_to_sdk_dec( @@ -45,23 +45,23 @@ def test_to_sdk_dec( @pytest.mark.parametrize( "test_name,sdk_dec_val,float_val,should_fail", [ - ('number with \'.\'', '.3', '', True), - ('number with \'.\'', '5.3', '', True), - ('invalid number', 'hello', '', True), + ("number with '.'", ".3", "", True), + ("number with '.'", "5.3", "", True), + ("invalid number", "hello", "", True), # valid numbers - ('empty string', '', 0, False), - ('empty string', None, 0, False), - ('number 0', '0' * 5, 0, False), - ('number 0', '0' * 22, 0, False), - ('number 10', '10' + '0' * 18, 10, False), - ('neg. number 10', '-10' + '0' * 18, -10, False), - ('number 123', '123' + '0' * 18, 123, False), + ("empty string", "", 0, False), + ("empty string", None, 0, False), + ("number 0", "0" * 5, 0, False), + ("number 0", "0" * 22, 0, False), + ("number 10", "10" + "0" * 18, 10, False), + ("neg. number 10", "-10" + "0" * 18, -10, False), + ("number 123", "123" + "0" * 18, 123, False), # with fractional - ('number 0.5', '05' + '0' * 17, 0.5, False), - ('fractional only 0.00596', '596' + '0' * 13, 0.00596, False), - ('number 13.5', '135' + '0' * 17, 13.5, False), - ('neg. number 13.5', '-135' + '0' * 17, -13.5, False), - ('number 1574.00005', '157400005' + '0' * 13, 1574.00005, False), + ("number 0.5", "05" + "0" * 17, 0.5, False), + ("fractional only 0.00596", "596" + "0" * 13, 0.00596, False), + ("number 13.5", "135" + "0" * 17, 13.5, False), + ("neg. number 13.5", "-135" + "0" * 17, -13.5, False), + ("number 1574.00005", "157400005" + "0" * 13, 1574.00005, False), ], ) def test_from_sdk_dec(test_name, sdk_dec_val, float_val, should_fail): @@ -141,3 +141,79 @@ def url_to_host(url: str) -> str: if not parsed_url.hostname: raise ReferenceError(f"Url {parsed_url} hostname is empty.") return parsed_url.hostname + + +def test_update_nested_fields(): + json_data = { + "amm_markets": [ + { + "market": { + "pair": "ubtc:unusd", + }, + "amm": { + "pair": "ubtc:unusd", + "base_reserve": "3000000000000000000", + "sqrt_depth": "30000000000000.000000000000000000", + }, + }, + { + "market": { + "pair": "ueth:unusd", + }, + "amm": { + "base_reserve": "3000000000000000000", + "quote_reserve": "30000000000000.000000000000000000", + }, + }, + ] + } + + fields_to_update = [ + "amm_markets.amm.base_reserve", + ] + expected_output = { + "amm_markets": [ + { + "market": { + "pair": "ubtc:unusd", + }, + "amm": { + "pair": "ubtc:unusd", + "base_reserve": 3, + "sqrt_depth": "30000000000000.000000000000000000", + }, + }, + { + "market": { + "pair": "ueth:unusd", + }, + "amm": { + "base_reserve": 3, + "quote_reserve": "30000000000000.000000000000000000", + }, + }, + ] + } + result = utils.update_nested_fields(json_data, fields_to_update, utils.from_sdk_dec) + + assert ( + result == expected_output + ), f"Error: Expected output {expected_output}, but got {result}" + + +def test_assert_subset(): + result = { + "key1": "value1", + "key2": {"nested_key1": "nested_value1", "nested_key2": "nested_value2"}, + "key3": ["item1", "item2", "item3"], + } + + expected = { + "key1": "value1", + "key2": {"nested_key1": "nested_value1"}, + } + + try: + utils.assert_subset(result, expected) + except AssertionError as e: + pytest.fail(str(e))