From 9cb6dede32d12b1fe1c79fb073d677ae2283b0ad Mon Sep 17 00:00:00 2001 From: sarveshotex Date: Thu, 3 Apr 2025 10:40:49 +0530 Subject: [PATCH 1/3] mm-fok-sample-integration --- .../sample_market_maker_liquidity_module.py | 81 +++++++++++++++++++ ...st_sample_market_maker_liquidity_module.py | 60 ++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 modules/sample_market_maker_liquidity_module.py create mode 100644 tests/test_sample_market_maker_liquidity_module.py diff --git a/modules/sample_market_maker_liquidity_module.py b/modules/sample_market_maker_liquidity_module.py new file mode 100644 index 0000000..4dd4e73 --- /dev/null +++ b/modules/sample_market_maker_liquidity_module.py @@ -0,0 +1,81 @@ +from templates.liquidity_module import LiquidityModule, Token +from typing import Dict, Optional +from decimal import Decimal + +class MarketMakerLiquidityModule(LiquidityModule): + def get_amount_out( + self, + pool_states: Dict, + fixed_parameters: Dict, + input_token: Token, + output_token: Token, + input_amount: int, + ) -> tuple[int | None, int | None]: + # Implement logic to calculate output amount given input amount + ## a list of token pairs supported by market maker + token_pairs: list[str] = pool_states.get("token_pairs", []) + + desired_pair: str = input_token.address + "_" + output_token.address + + ## If the desired pair is not in the list of token pairs, return None + if desired_pair not in token_pairs: + return None, None + + ## Get the price levels for the desired pair made available by the market maker + ## The price levels are a dictionary where the key is the token pair and the value is a list of tuples of amount and price + pricelevels: dict[str, list[tuple[int, int]]] = pool_states.get("pricelevels", {}) + + ## If the desired pair is not in the price levels, return None + if desired_pair not in pricelevels: + return None, None + + ## Get the price levels for the desired pair + levels: list[tuple[int, int]] = pricelevels[desired_pair] + ## If the levels are empty, return None + if len(levels) == 0: + return None, None + + if input_amount < levels[0][0]: + return None, None + + ## Iterate through the levels and find the amount and price + remaining_amount: int = input_amount + output_amount: int = 0 + for l in levels: + vol_in_level: int = l[0] + price_in_level: int = l[1] + if remaining_amount <= vol_in_level: + output_amount += remaining_amount * price_in_level + remaining_amount = 0 + else: + output_amount += vol_in_level * price_in_level + remaining_amount -= vol_in_level + + if remaining_amount == 0: + break + + if remaining_amount > 0: + return None, None + + return 0, int(output_amount) + + + + def get_amount_in( + self, + pool_state: Dict, + fixed_parameters: Dict, + input_token: Token, + output_token: Token, + output_amount: int + ) -> tuple[int | None, int | None]: + + pass + + def get_apy(self, pool_state: Dict) -> Decimal: + + pass + + def get_tvl(self, pool_state: Dict, token: Optional[Token] = None) -> Decimal: + + pass \ No newline at end of file diff --git a/tests/test_sample_market_maker_liquidity_module.py b/tests/test_sample_market_maker_liquidity_module.py new file mode 100644 index 0000000..1e77159 --- /dev/null +++ b/tests/test_sample_market_maker_liquidity_module.py @@ -0,0 +1,60 @@ +import unittest +import sys +from os import listdir, path, walk + +sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) +sys.path.append(path.dirname(path.dirname(path.dirname(path.abspath(__file__))))) +sys.path.append(path.dirname(path.dirname(path.dirname(path.dirname(path.abspath(__file__)))))) +sys.path.append(path.dirname(path.dirname(path.dirname(path.dirname(path.dirname(path.abspath(__file__))))))) +sys.path.append(path.dirname(path.dirname(path.dirname(path.dirname(path.dirname(path.dirname(path.abspath(__file__)))))))) +from decimal import Decimal +from templates.liquidity_module import Token +from modules.sample_market_maker_liquidity_module import MarketMakerLiquidityModule # Replace with the actual module name + +class TestMarketMakerLiquidityModule(unittest.TestCase): + def setUp(self): + self.liquidity_module = MarketMakerLiquidityModule() + self.token_a = Token(address="0xTokenA", symbol="TokenA", decimals=18, reference_price=Decimal("0.0001")) + self.token_b = Token(address="0xTokenB", symbol="TokenB", decimals=18, reference_price=Decimal("0.0002")) + self.pool_states = { + "token_pairs": ["0xTokenA_0xTokenB"], + "pricelevels": { + "0xTokenA_0xTokenB": [ + (100, 2000000), # 100 TokenA available at a rate of 2000000 TokenB per TokenA + (200, 1500000), # 200 TokenA available at a rate of 1500000 TokenB per TokenA + ] + } + } + self.fixed_parameters = {} + + def test_get_amount_out_valid(self): + input_amount = 150 # Requesting 150 TokenA + expected_output = int(100 * 2000000 + 50 * 1500000) # 100 at rate 2.000000 + 50 at rate 1.500000 + _, output_amount = self.liquidity_module.get_amount_out( + self.pool_states, self.fixed_parameters, self.token_a, self.token_b, input_amount + ) + self.assertEqual(output_amount, expected_output) + + def test_get_amount_out_insufficient_liquidity(self): + input_amount = 500 # More than available liquidity + result = self.liquidity_module.get_amount_out( + self.pool_states, self.fixed_parameters, self.token_a, self.token_b, input_amount + ) + self.assertEqual(result, (None, None)) + + def test_get_amount_out_no_price_levels(self): + pool_states_no_levels = {"token_pairs": ["0xTokenA_0xTokenB"], "pricelevels": {}} + result = self.liquidity_module.get_amount_out( + pool_states_no_levels, self.fixed_parameters, self.token_a, self.token_b, 50 + ) + self.assertEqual(result, (None, None)) + + def test_get_amount_out_pair_not_supported(self): + token_c = Token(address="0xTokenC", symbol="TokenC", decimals=18, reference_price=Decimal("0.0003")) + result = self.liquidity_module.get_amount_out( + self.pool_states, self.fixed_parameters, token_c, self.token_b, 50 + ) + self.assertEqual(result, (None, None)) + +if __name__ == "__main__": + unittest.main() From e4dae8211698477efd2b312fa52677140310f152 Mon Sep 17 00:00:00 2001 From: sarveshotex Date: Thu, 10 Apr 2025 18:19:33 +0530 Subject: [PATCH 2/3] redone of marketmaker liquidty module --- .../sample_market_maker_liquidity_module.py | 149 ++++++++++-------- 1 file changed, 82 insertions(+), 67 deletions(-) diff --git a/modules/sample_market_maker_liquidity_module.py b/modules/sample_market_maker_liquidity_module.py index 4dd4e73..655c2a2 100644 --- a/modules/sample_market_maker_liquidity_module.py +++ b/modules/sample_market_maker_liquidity_module.py @@ -1,81 +1,96 @@ -from templates.liquidity_module import LiquidityModule, Token -from typing import Dict, Optional +import aiohttp +import asyncio +from templates.liquidity_module import Token +from typing import Dict, Tuple, Literal from decimal import Decimal -class MarketMakerLiquidityModule(LiquidityModule): - def get_amount_out( - self, - pool_states: Dict, +class MarketMakerLiquidityModule: + + async def get_sell_quote( fixed_parameters: Dict, - input_token: Token, + input_token: Token, output_token: Token, - input_amount: int, - ) -> tuple[int | None, int | None]: - # Implement logic to calculate output amount given input amount - ## a list of token pairs supported by market maker - token_pairs: list[str] = pool_states.get("token_pairs", []) - - desired_pair: str = input_token.address + "_" + output_token.address - - ## If the desired pair is not in the list of token pairs, return None - if desired_pair not in token_pairs: - return None, None - - ## Get the price levels for the desired pair made available by the market maker - ## The price levels are a dictionary where the key is the token pair and the value is a list of tuples of amount and price - pricelevels: dict[str, list[tuple[int, int]]] = pool_states.get("pricelevels", {}) - - ## If the desired pair is not in the price levels, return None - if desired_pair not in pricelevels: - return None, None - - ## Get the price levels for the desired pair - levels: list[tuple[int, int]] = pricelevels[desired_pair] - ## If the levels are empty, return None - if len(levels) == 0: - return None, None - - if input_amount < levels[0][0]: - return None, None - - ## Iterate through the levels and find the amount and price - remaining_amount: int = input_amount - output_amount: int = 0 - for l in levels: - vol_in_level: int = l[0] - price_in_level: int = l[1] - if remaining_amount <= vol_in_level: - output_amount += remaining_amount * price_in_level - remaining_amount = 0 - else: - output_amount += vol_in_level * price_in_level - remaining_amount -= vol_in_level - - if remaining_amount == 0: - break - - if remaining_amount > 0: - return None, None - - return 0, int(output_amount) - + input_amount: int, + block: Literal['latest', int] = 'latest' + ) -> Tuple[int | None, int | None]: + + market_maker_api = fixed_parameters.get("market_maker_api") + api_key = fixed_parameters.get("api_key", None) + chain = fixed_parameters.get("chain", "ethereum") + user_address = fixed_parameters.get("user_address") + + # Construct the URL for the API call + url = f"{market_maker_api}/sellQuote" + body = { + "chain": chain, + "sell_token": input_token.address, + "buy_token": output_token.address, + "sell_amounts": input_amount, + "user_address": user_address + } + + + headers = { + "api-key": api_key, + } + + async with aiohttp.ClientSession() as session: + try: + async with session.post(url, headers=headers, json=body) as resp: + data = await resp.json() + + if "SUCCESS" not in data.get("status", ""): + return None, None + + output_amount = data.get("buy_amount") - def get_amount_in( - self, - pool_state: Dict, + return 0, amount_out + + except Exception as e: + print(f"Error fetching sell quote: {e}") + return None, None + + async def get_buy_quote( fixed_parameters: Dict, input_token: Token, output_token: Token, - output_amount: int - ) -> tuple[int | None, int | None]: + output_amount: int, + block: Literal['latest', int] = 'latest' + ) -> Tuple[int | None, int | None]: - pass + market_maker_api = fixed_parameters.get("market_maker_api") + api_key = fixed_parameters.get("api_key", None) + chain = fixed_parameters.get("chain", "ethereum") + user_address = fixed_parameters.get("user_address") + + # Construct the URL for the API call + url = f"{market_maker_api}/buyQuote" + + body = { + "chain": chain, + "buy_token": output_token.address, + "sell_token": input_token.address, + "buy_amount": output_amount, + "user_address": user_address + } - def get_apy(self, pool_state: Dict) -> Decimal: + headers = { + "api-key": api_key, + } - pass + async with aiohttp.ClientSession() as session: + try: + async with session.post(url, headers=headers, json=body) as resp: + data = await resp.json() + + if "SUCCESS" not in data.get("status", ""): + return None, None + + input_amount = data.get("sell_amount") - def get_tvl(self, pool_state: Dict, token: Optional[Token] = None) -> Decimal: + return 0, input_amount - pass \ No newline at end of file + except Exception as e: + print(f"Error fetching buy quote: {e}") + return None, None \ No newline at end of file From 8ca6286747aa03fe96d93639645f72e486535863 Mon Sep 17 00:00:00 2001 From: sarveshotex Date: Fri, 11 Apr 2025 08:45:05 +0530 Subject: [PATCH 3/3] update sample module and tests --- .../sample_market_maker_liquidity_module.py | 2 +- ...st_sample_market_maker_liquidity_module.py | 95 +++++++++++-------- 2 files changed, 59 insertions(+), 38 deletions(-) diff --git a/modules/sample_market_maker_liquidity_module.py b/modules/sample_market_maker_liquidity_module.py index 655c2a2..b413c0e 100644 --- a/modules/sample_market_maker_liquidity_module.py +++ b/modules/sample_market_maker_liquidity_module.py @@ -45,7 +45,7 @@ async def get_sell_quote( output_amount = data.get("buy_amount") - return 0, amount_out + return 0, output_amount except Exception as e: print(f"Error fetching sell quote: {e}") diff --git a/tests/test_sample_market_maker_liquidity_module.py b/tests/test_sample_market_maker_liquidity_module.py index 1e77159..09bd0d2 100644 --- a/tests/test_sample_market_maker_liquidity_module.py +++ b/tests/test_sample_market_maker_liquidity_module.py @@ -1,4 +1,5 @@ import unittest +from unittest.mock import patch, AsyncMock import sys from os import listdir, path, walk @@ -11,50 +12,70 @@ from templates.liquidity_module import Token from modules.sample_market_maker_liquidity_module import MarketMakerLiquidityModule # Replace with the actual module name -class TestMarketMakerLiquidityModule(unittest.TestCase): +class TestMarketMakerLiquidityModule(unittest.IsolatedAsyncioTestCase): def setUp(self): - self.liquidity_module = MarketMakerLiquidityModule() - self.token_a = Token(address="0xTokenA", symbol="TokenA", decimals=18, reference_price=Decimal("0.0001")) - self.token_b = Token(address="0xTokenB", symbol="TokenB", decimals=18, reference_price=Decimal("0.0002")) - self.pool_states = { - "token_pairs": ["0xTokenA_0xTokenB"], - "pricelevels": { - "0xTokenA_0xTokenB": [ - (100, 2000000), # 100 TokenA available at a rate of 2000000 TokenB per TokenA - (200, 1500000), # 200 TokenA available at a rate of 1500000 TokenB per TokenA - ] - } + self.fixed_parameters = { + "market_maker_api": "https://mock.api", + "api_key": "test-api-key", + "chain": "ethereum", + "user_address": "0xuser" } - self.fixed_parameters = {} - - def test_get_amount_out_valid(self): - input_amount = 150 # Requesting 150 TokenA - expected_output = int(100 * 2000000 + 50 * 1500000) # 100 at rate 2.000000 + 50 at rate 1.500000 - _, output_amount = self.liquidity_module.get_amount_out( - self.pool_states, self.fixed_parameters, self.token_a, self.token_b, input_amount + self.input_token = Token(address="0xinput", symbol="InputToken", decimals=18, reference_price=Decimal("0.0001")) + self.output_token = Token(address="0xoutput", symbol="OutputToken", decimals=18, reference_price=Decimal("0.0002")) + + @patch("aiohttp.ClientSession.post") + async def test_get_sell_quote_success(self, mock_post): + mock_response = AsyncMock() + mock_response.json = AsyncMock(return_value={ + "status": "SUCCESS", + "buy_amount": 1000 + }) + mock_post.return_value.__aenter__.return_value = mock_response + + fee, amount = await MarketMakerLiquidityModule.get_sell_quote( + self.fixed_parameters, self.input_token, self.output_token, 500 ) - self.assertEqual(output_amount, expected_output) - def test_get_amount_out_insufficient_liquidity(self): - input_amount = 500 # More than available liquidity - result = self.liquidity_module.get_amount_out( - self.pool_states, self.fixed_parameters, self.token_a, self.token_b, input_amount + self.assertEqual(fee, 0) + self.assertEqual(amount, 1000) + + @patch("aiohttp.ClientSession.post") + async def test_get_sell_quote_failure(self, mock_post): + mock_response = AsyncMock() + mock_response.json = AsyncMock(return_value={"status": "FAILED"}) + mock_post.return_value.__aenter__.return_value = mock_response + + fee, amount = await MarketMakerLiquidityModule.get_sell_quote( + self.fixed_parameters, self.input_token, self.output_token, 500 ) - self.assertEqual(result, (None, None)) + self.assertIsNone(fee) + self.assertIsNone(amount) + + @patch("aiohttp.ClientSession.post") + async def test_get_buy_quote_success(self, mock_post): + mock_response = AsyncMock() + mock_response.json = AsyncMock(return_value={ + "status": "SUCCESS", + "sell_amount": 800 + }) + mock_post.return_value.__aenter__.return_value = mock_response - def test_get_amount_out_no_price_levels(self): - pool_states_no_levels = {"token_pairs": ["0xTokenA_0xTokenB"], "pricelevels": {}} - result = self.liquidity_module.get_amount_out( - pool_states_no_levels, self.fixed_parameters, self.token_a, self.token_b, 50 + fee, amount = await MarketMakerLiquidityModule.get_buy_quote( + self.fixed_parameters, self.input_token, self.output_token, 1000 ) - self.assertEqual(result, (None, None)) + self.assertEqual(fee, 0) + self.assertEqual(amount, 800) + + @patch("aiohttp.ClientSession.post") + async def test_get_buy_quote_failure(self, mock_post): + mock_response = AsyncMock() + mock_response.json = AsyncMock(return_value={"status": "ERROR"}) + mock_post.return_value.__aenter__.return_value = mock_response - def test_get_amount_out_pair_not_supported(self): - token_c = Token(address="0xTokenC", symbol="TokenC", decimals=18, reference_price=Decimal("0.0003")) - result = self.liquidity_module.get_amount_out( - self.pool_states, self.fixed_parameters, token_c, self.token_b, 50 + fee, amount = await MarketMakerLiquidityModule.get_buy_quote( + self.fixed_parameters, self.input_token, self.output_token, 1000 ) - self.assertEqual(result, (None, None)) + self.assertIsNone(fee) + self.assertIsNone(amount) -if __name__ == "__main__": - unittest.main() +unittest.TextTestRunner().run(unittest.defaultTestLoader.loadTestsFromTestCase(TestMarketMakerLiquidityModule))