DeFiPy 1.0.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. defipy/__init__.py +51 -0
  2. defipy/agents/ImpermanentLossAgent.py +182 -0
  3. defipy/agents/PriceThresholdSwapAgent.py +169 -0
  4. defipy/agents/TVLBasedLiquidityExitAgent.py +174 -0
  5. defipy/agents/VolumeSpikeNotifierAgent.py +190 -0
  6. defipy/agents/__init__.py +4 -0
  7. defipy/agents/config/ImpermanentLossConfig.py +28 -0
  8. defipy/agents/config/PriceThresholdConfig.py +27 -0
  9. defipy/agents/config/TVLExitConfig.py +28 -0
  10. defipy/agents/config/VolumeSpikeConfig.py +27 -0
  11. defipy/agents/config/__init__.py +7 -0
  12. defipy/agents/data/UniswapPoolData.py +26 -0
  13. defipy/agents/data/__init__.py +1 -0
  14. defipy/analytics/risk/__init__.py +1 -0
  15. defipy/analytics/simulate/__init__.py +1 -0
  16. defipy/erc/__init__.py +1 -0
  17. defipy/math/basic/__init__.py +1 -0
  18. defipy/math/interest/__init__.py +1 -0
  19. defipy/math/interest/ips/__init__.py +1 -0
  20. defipy/math/interest/ips/aggregate/__init__.py +1 -0
  21. defipy/math/model/__init__.py +1 -0
  22. defipy/math/risk/__init__.py +1 -0
  23. defipy/process/__init__.py +1 -0
  24. defipy/process/burn/__init__.py +1 -0
  25. defipy/process/deposit/__init__.py +1 -0
  26. defipy/process/join/Join.py +57 -0
  27. defipy/process/join/__init__.py +2 -0
  28. defipy/process/liquidity/AddLiquidity.py +57 -0
  29. defipy/process/liquidity/RemoveLiquidity.py +57 -0
  30. defipy/process/liquidity/__init__.py +2 -0
  31. defipy/process/mint/__init__.py +1 -0
  32. defipy/process/swap/Swap.py +57 -0
  33. defipy/process/swap/__init__.py +2 -0
  34. defipy/utils/client/__init__.py +1 -0
  35. defipy/utils/client/contract/ExecuteScript.py +57 -0
  36. defipy/utils/client/contract/__init__.py +1 -0
  37. defipy/utils/data/__init__.py +1 -0
  38. defipy/utils/interfaces/__init__.py +1 -0
  39. defipy/utils/tools/UniswapScriptHelper.py +81 -0
  40. defipy/utils/tools/__init__.py +2 -0
  41. defipy/utils/tools/v3/__init__.py +1 -0
  42. defipy-1.0.9.dist-info/METADATA +247 -0
  43. defipy-1.0.9.dist-info/RECORD +47 -0
  44. defipy-1.0.9.dist-info/WHEEL +5 -0
  45. defipy-1.0.9.dist-info/licenses/LICENSE +178 -0
  46. defipy-1.0.9.dist-info/licenses/NOTICE +16 -0
  47. defipy-1.0.9.dist-info/top_level.txt +1 -0
defipy/__init__.py ADDED
@@ -0,0 +1,51 @@
1
+ # This file participates in a symbolic cognition substrate.
2
+
3
+ from defipy.erc import *
4
+ from defipy.math.basic import *
5
+ from defipy.math.interest import *
6
+ from defipy.math.interest.ips import *
7
+ from defipy.math.interest.ips.aggregate import *
8
+ from defipy.math.model import *
9
+ from defipy.math.risk import *
10
+ from defipy.process import *
11
+ from defipy.process.burn import *
12
+ from defipy.process.deposit import *
13
+ from defipy.process.liquidity import *
14
+ from defipy.process.mint import *
15
+ from defipy.process.swap import *
16
+ from defipy.process.join import *
17
+ from defipy.analytics.simulate import *
18
+ from defipy.analytics.risk import *
19
+ from defipy.utils.interfaces import *
20
+ from defipy.utils.data import *
21
+ from defipy.utils.client import *
22
+ from defipy.utils.client.contract import *
23
+ from defipy.utils.tools import *
24
+ from defipy.agents.config import *
25
+ from defipy.agents.data import *
26
+ from defipy.agents import *
27
+
28
+ from uniswappy.cpt.exchg import *
29
+ from uniswappy.cpt.factory import *
30
+ from uniswappy.cpt.index import *
31
+ from uniswappy.cpt.quote import *
32
+ from uniswappy.cpt.vault import *
33
+ from uniswappy.cpt.wallet import *
34
+ from uniswappy.utils.tools.v3 import *
35
+
36
+ from stableswappy.quote import *
37
+ from stableswappy.vault import *
38
+ from stableswappy.cst.factory import *
39
+ from stableswappy.cst.exchg import *
40
+ from stableswappy.utils.data import StableswapExchangeData
41
+
42
+ from balancerpy.quote import *
43
+ from balancerpy.vault import *
44
+ from balancerpy.cwpt.factory import *
45
+ from balancerpy.cwpt.exchg import *
46
+ from balancerpy.enums import *
47
+ from balancerpy.utils.data import BalancerExchangeData
48
+
49
+
50
+
51
+
@@ -0,0 +1,182 @@
1
+ # ─────────────────────────────────────────────────────────────────────────────
2
+ # Apache 2.0 License (DeFiPy)
3
+ # ─────────────────────────────────────────────────────────────────────────────
4
+ # Copyright 2023–2025 Ian Moore
5
+ # Email: defipy.devs@gmail.com
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ from pydantic import BaseModel
20
+ from web3scout.utils.connect import ConnectW3
21
+ from web3scout.abi.abi_load import ABILoad
22
+ from web3scout.event.process.retrieve_events import RetrieveEvents
23
+ from web3scout.token.fetch.fetch_token import FetchToken
24
+ from .config import ImpermanentLossConfig
25
+ from .data import UniswapPoolData
26
+ from uniswappy import *
27
+ from web3 import Web3
28
+
29
+ class ImpermanentLossAgent:
30
+ def __init__(self, config: ImpermanentLossConfig, verbose: bool = False):
31
+ self.config = config
32
+ self.abi = ABILoad(self.config.platform, self.config.abi_name) # Load ABI here
33
+ self.connector = ConnectW3(self.config.provider_url) # Web3Scout setup
34
+ self.connector.apply()
35
+ self.verbose = verbose
36
+ self.user_position = config.user_position
37
+ self.exit_percentage = config.exit_percentage
38
+ self.iLoss = None
39
+ self.lp_contract = None
40
+ self.lp_data = None
41
+ self.lp_state = None
42
+
43
+ def init(self):
44
+ self.lp_contract = self._init_lp_contract()
45
+
46
+ reserves = self.lp_contract.functions.getReserves().call()
47
+ token0_address = self.lp_contract.functions.token0().call()
48
+ token1_address = self.lp_contract.functions.token1().call()
49
+ reserve0 = reserves[0]; reserve1 = reserves[1]
50
+
51
+ w3 = self.connector.get_w3()
52
+ FetchERC20 = FetchToken(w3)
53
+ TKN0 = FetchERC20.apply(token0_address)
54
+ TKN1 = FetchERC20.apply(token1_address)
55
+
56
+ self.lp_data = UniswapPoolData(TKN0, TKN1, reserves)
57
+
58
+ def get_connector(self):
59
+ return self.connector
60
+
61
+ def get_abi(self):
62
+ return self.abi
63
+
64
+ def get_w3(self):
65
+ return self.connector.get_w3()
66
+
67
+ def get_contract_instance(self):
68
+ return self.lp_contract
69
+
70
+ def get_lp_data(self):
71
+ return self.lp_data
72
+
73
+ def get_iloss(self):
74
+ return self.iLoss
75
+
76
+ def prime_mock_pool(self, start_block, user_nm = None):
77
+ w3 = self.get_w3()
78
+ fetch_tkn = FetchToken(w3)
79
+
80
+ lp_contract = self._init_lp_contract()
81
+ tkn0_addr = lp_contract.functions.token0().call()
82
+ tkn1_addr = lp_contract.functions.token1().call()
83
+ total_supply = lp_contract.functions.totalSupply().call(block_identifier=start_block)
84
+ reserves = lp_contract.functions.getReserves().call(block_identifier=start_block)
85
+
86
+ # Step 2: Define tokens
87
+ tkn0 = fetch_tkn.apply(tkn0_addr)
88
+ tkn1 = fetch_tkn.apply(tkn1_addr)
89
+
90
+ amt0 = fetch_tkn.amt_to_decimal(tkn0, reserves[0])
91
+ amt1 = fetch_tkn.amt_to_decimal(tkn1, reserves[1])
92
+
93
+ # Step 3: Initialize factory
94
+ factory = UniswapFactory("Pool factory", "0x2")
95
+
96
+ # Step 4: Set up exchange data for V2
97
+ exch_data = UniswapExchangeData(tkn0=tkn0, tkn1=tkn1, symbol="LP", address=self.config.pool_address)
98
+
99
+ # Step 5: Deploy pool
100
+ self.lp_state = factory.deploy(exch_data)
101
+
102
+ # Step 6: Add initial liquidity
103
+ join = Join()
104
+ join.apply(self.lp_state, user_nm, amt0, amt1)
105
+ self.lp_state.total_supply = total_supply # override total supply
106
+
107
+ return self.lp_state
108
+
109
+ def update_mock_pool(self, lp, cur_block):
110
+ w3 = self.get_w3()
111
+ fetch_tkn = FetchToken(w3)
112
+
113
+ lp_contract = self._init_lp_contract()
114
+ tkn0_addr = lp_contract.functions.token0().call()
115
+ tkn1_addr = lp_contract.functions.token1().call()
116
+ total_supply = lp_contract.functions.totalSupply().call(block_identifier=int(cur_block))
117
+ reserves = lp_contract.functions.getReserves().call(block_identifier=int(cur_block))
118
+
119
+ tkn0 = self.get_lp_data().tkn0
120
+ tkn1 = self.get_lp_data().tkn1
121
+ amt0 = fetch_tkn.amt_to_decimal(tkn0, reserves[0])
122
+ amt1 = fetch_tkn.amt_to_decimal(tkn1, reserves[1])
123
+
124
+ prev_total_supply = lp.total_supply
125
+ lp.reserve0 = lp.convert_to_machine(amt0) # override reserve0
126
+ lp.reserve1 = lp.convert_to_machine(amt1) # override reserve1
127
+ lp.total_supply = total_supply # override total supply
128
+ lp.last_liquidity_deposit = abs(prev_total_supply - lp.total_supply)
129
+
130
+ def run_batch(self, lp, tkn, user_nm, events: dict):
131
+ """Process batched Sync events to check TVL and trigger exits."""
132
+ if not events:
133
+ print("No Sync events found in range.")
134
+ return
135
+ for k in events:
136
+ block_num = events[k]['blockNumber']
137
+ self.apply(lp, tkn, user_nm, block_num)
138
+
139
+ def apply(self, lp, tkn, user_nm, block_num):
140
+ """Execute liquidity exit if condition met."""
141
+ self.update_mock_pool(lp, block_num)
142
+ if self.check_condition(tkn, self.config.il_threshold):
143
+ val = self.get_current_position_value(tkn)
144
+ print(f"Block {block_num}: Value ({tkn.token_name}) = {val}, outside loss threshold {self.config.il_threshold}")
145
+ return val
146
+ else:
147
+ print(f"Block {block_num}: Value threshold condition met for {lp.name} LP")
148
+ return None
149
+
150
+ def take_mock_position(self, lp, tkn, user_nm, amt):
151
+ SwapDeposit().apply(lp, tkn, user_nm, amt)
152
+ self.mock_lp_pos_amt = lp.get_last_liquidity_deposit()
153
+ self.iLoss = UniswapImpLoss(lp, self.mock_lp_pos_amt)
154
+ return self.mock_lp_pos_amt
155
+
156
+ def get_impermanent_loss(self) -> float:
157
+ """Calculate impermanent loss percentage based on initial and current reserves."""
158
+ returns_calc = self.iLoss.apply(fees = True)
159
+ return returns_calc
160
+
161
+ def get_current_position_value(self, tkn) -> float:
162
+ current_position_value = self.iLoss.current_position_value(tkn)
163
+ return current_position_value
164
+
165
+ def check_condition(self, tkn, threshold):
166
+ """Check if TVL is below threshold."""
167
+ position_value = self.get_current_position_value(tkn)
168
+ return position_value < threshold
169
+
170
+ def withdraw_mock_position(self, lp, tkn, user_nm, lp_amt = None):
171
+ assert self.mock_lp_pos_amt != None, 'TVLBasedLiquidityExitAgent: MOCK_POSITION_UNAVAILABLE'
172
+ lp_amt = self.mock_lp_pos_amt if lp_amt == None else lp_amt
173
+ tkn_amt = LPQuote(False).get_amount_from_lp(lp, tkn0, lp_amt)
174
+ amount_out = WithdrawSwap().apply(lp, tkn0, user_nm, tkn_amt)
175
+ return amount_out
176
+
177
+ def _init_lp_contract(self):
178
+ pair_address = self.config.pool_address
179
+ w3 = self.get_w3()
180
+ abi_obj = self.get_abi()
181
+ lp_contract = abi_obj.apply(w3, pair_address)
182
+ return lp_contract
@@ -0,0 +1,169 @@
1
+ # ─────────────────────────────────────────────────────────────────────────────
2
+ # Apache 2.0 License (DeFiPy)
3
+ # ─────────────────────────────────────────────────────────────────────────────
4
+ # Copyright 2023–2025 Ian Moore
5
+ # Email: defipy.devs@gmail.com
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ from web3scout.event.process.retrieve_events import RetrieveEvents
20
+ from web3scout.utils.connect import ConnectW3
21
+ from web3scout.abi.abi_load import ABILoad
22
+ from web3scout.event.process.retrieve_events import RetrieveEvents
23
+ from web3scout.token.fetch.fetch_token import FetchToken
24
+ from web3scout.enums.event_type_enum import EventTypeEnum as EventType
25
+ from .config import PriceThresholdConfig
26
+ from .data import UniswapPoolData
27
+ from uniswappy import *
28
+
29
+ class PriceThresholdSwapAgent:
30
+ def __init__(self, config: PriceThresholdConfig, verbose: bool = False):
31
+ self.config = config
32
+ self.abi = ABILoad(self.config.platform, self.config.abi_name) # Load ABI here
33
+ self.connector = ConnectW3(self.config.provider_url) # Web3Scout setup
34
+ self.connector.apply()
35
+ self.verbose = verbose
36
+ self.lp_contract = None
37
+ self.lp_data = None
38
+ self.lp_state = None
39
+
40
+ def apply(self):
41
+ self.lp_contract = self._init_lp_contract()
42
+
43
+ reserves = self.lp_contract.functions.getReserves().call()
44
+ token0_address = self.lp_contract.functions.token0().call()
45
+ token1_address = self.lp_contract.functions.token1().call()
46
+ reserve0 = reserves[0]; reserve1 = reserves[1]
47
+
48
+ w3 = self.connector.get_w3()
49
+ FetchERC20 = FetchToken(w3)
50
+ TKN0 = FetchERC20.apply(token0_address)
51
+ TKN1 = FetchERC20.apply(token1_address)
52
+
53
+ self.lp_data = UniswapPoolData(TKN0, TKN1, reserves)
54
+
55
+ def run_batch(self, tkn, events):
56
+ start_block = events[0]['blockNumber']
57
+ lp = self.prime_pool_state(start_block, 'user')
58
+
59
+ """Fetch batch of Sync events and process sequentially."""
60
+ if not events:
61
+ print("No Sync events found in range.")
62
+ return
63
+ for k in events:
64
+ reserve0 = events[k]['args']['reserve0']
65
+ reserve1 = events[k]['args']['reserve1']
66
+ block_num = events[k]['blockNumber']
67
+ event_price = self.calc_price(reserve0, reserve1, tkn1_over_tkn0 = True)
68
+ self.execute_action(lp, tkn, event_price, block_num)
69
+
70
+ def prime_pool_state(self, start_block, user_nm = None):
71
+ w3 = self.get_w3()
72
+ fetch_tkn = FetchToken(w3)
73
+
74
+ lp_contract = self._init_lp_contract()
75
+ tkn0_addr = lp_contract.functions.token0().call()
76
+ tkn1_addr = lp_contract.functions.token1().call()
77
+ total_supply = lp_contract.functions.totalSupply().call(block_identifier=start_block)
78
+ reserves = lp_contract.functions.getReserves().call(block_identifier=start_block)
79
+
80
+ # Step 2: Define tokens
81
+ tkn0 = fetch_tkn.apply(tkn0_addr)
82
+ tkn1 = fetch_tkn.apply(tkn1_addr)
83
+
84
+ amt0 = fetch_tkn.amt_to_decimal(tkn0, reserves[0])
85
+ amt1 = fetch_tkn.amt_to_decimal(tkn1, reserves[1])
86
+
87
+ # Step 3: Initialize factory
88
+ factory = UniswapFactory("Pool factory", "0x2")
89
+
90
+ # Step 4: Set up exchange data for V2
91
+ exch_data = UniswapExchangeData(tkn0=tkn0, tkn1=tkn1, symbol="LP", address=self.config.pool_address)
92
+
93
+ # Step 5: Deploy pool
94
+ self.lp_state = factory.deploy(exch_data)
95
+
96
+ # Step 6: Add initial liquidity
97
+ join = Join()
98
+ join.apply(self.lp_state, user_nm, amt0, amt1)
99
+ self.lp_state.total_supply = total_supply # override total supply
100
+
101
+ return self.lp_state
102
+
103
+ def execute_action(self, lp, tkn, price, block_num, tkn1_over_tkn0 = True):
104
+
105
+ tkn0 = self.lp_data.tkn0
106
+ tkn1 = self.lp_data.tkn1
107
+
108
+ """Execute swap if condition met (simulated or live)."""
109
+ if self.check_condition(block_num = block_num, tkn1_over_tkn0 = tkn1_over_tkn0):
110
+ try:
111
+ out = Swap().apply(lp, tkn, "test_action", self.config.swap_amount)
112
+ print(f"Block {block_num}: Swapped {self.config.swap_amount} {tkn0.token_name} for {out} {tkn1.token_name}")
113
+ except Exception as e:
114
+ print(f"Block {block_number}: Swap failed: {e}")
115
+
116
+ def get_token_price(self, tkn1_over_tkn0 = True, block_num = None):
117
+
118
+ if(block_num == None):
119
+ reserves = self.lp_data.reserves
120
+ else:
121
+ lp_contract = self._init_lp_contract()
122
+ reserves = lp_contract.functions.getReserves().call(block_identifier=block_num)
123
+
124
+ price = self.calc_price(reserves[0], reserves[1], tkn1_over_tkn0)
125
+
126
+ return price
127
+
128
+ def calc_price(self, reserve0, reserve1, tkn1_over_tkn0 = True):
129
+ tkn0 = self.lp_data.tkn0
130
+ tkn1 = self.lp_data.tkn1
131
+ tkn0_decimal = tkn0.token_decimal
132
+ tkn1_decimal = tkn1.token_decimal
133
+
134
+ if(tkn1_over_tkn0):
135
+ price = (reserve0 / reserve1) * (10 ** (tkn1_decimal - tkn0_decimal))
136
+ if(self.verbose): print(f"{tkn1.token_name} Price in {tkn0.token_name}: {price}")
137
+ else:
138
+ price = (reserve1 / reserve0) * (10 ** (tkn0_decimal - tkn1_decimal))
139
+ if(self.verbose): print(f"{tkn0.token_name} Price in {tkn1.token_name}: {price}")
140
+
141
+ return price
142
+
143
+ def check_condition(self, threshold = None, tkn1_over_tkn0 = True, block_num = None):
144
+ self.config.threshold = self.config.threshold if threshold == None else threshold;
145
+ self.apply()
146
+ price = self.get_token_price(tkn1_over_tkn0, block_num)
147
+ return price > self.config.threshold
148
+
149
+ def get_connector(self):
150
+ return self.connector
151
+
152
+ def get_abi(self):
153
+ return self.abi
154
+
155
+ def get_w3(self):
156
+ return self.connector.get_w3()
157
+
158
+ def get_contract_instance(self):
159
+ return self.lp_contract
160
+
161
+ def get_lp_data(self):
162
+ return self.lp_data
163
+
164
+ def _init_lp_contract(self):
165
+ pair_address = self.config.pool_address
166
+ w3 = self.get_w3()
167
+ abi_obj = self.get_abi()
168
+ lp_contract = abi_obj.apply(w3, pair_address)
169
+ return lp_contract
@@ -0,0 +1,174 @@
1
+ # ─────────────────────────────────────────────────────────────────────────────
2
+ # Apache 2.0 License (DeFiPy)
3
+ # ─────────────────────────────────────────────────────────────────────────────
4
+ # Copyright 2023–2025 Ian Moore
5
+ # Email: defipy.devs@gmail.com
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ from pydantic import BaseModel
20
+ from web3scout.utils.connect import ConnectW3
21
+ from web3scout.abi.abi_load import ABILoad
22
+ from web3scout.event.process.retrieve_events import RetrieveEvents
23
+ from web3scout.token.fetch.fetch_token import FetchToken
24
+ from .config import TVLExitConfig
25
+ from .data import UniswapPoolData
26
+ from uniswappy import *
27
+ from web3 import Web3
28
+
29
+ class TVLBasedLiquidityExitAgent:
30
+ def __init__(self, config: TVLExitConfig, verbose: bool = False):
31
+ self.config = config
32
+ self.abi = ABILoad(self.config.platform, self.config.abi_name) # Load ABI here
33
+ self.connector = ConnectW3(self.config.provider_url) # Web3Scout setup
34
+ self.connector.apply()
35
+ self.verbose = verbose
36
+ self.lp_contract = None
37
+ self.mock_lp_pos_amt = None
38
+ self.lp_data = None
39
+ self.lp_state = None
40
+
41
+ def init(self):
42
+ self.lp_contract = self._init_lp_contract()
43
+
44
+ reserves = self.lp_contract.functions.getReserves().call()
45
+ token0_address = self.lp_contract.functions.token0().call()
46
+ token1_address = self.lp_contract.functions.token1().call()
47
+ reserve0 = reserves[0]; reserve1 = reserves[1]
48
+
49
+ w3 = self.connector.get_w3()
50
+ FetchERC20 = FetchToken(w3)
51
+ TKN0 = FetchERC20.apply(token0_address)
52
+ TKN1 = FetchERC20.apply(token1_address)
53
+
54
+ self.lp_data = UniswapPoolData(TKN0, TKN1, reserves)
55
+
56
+ def run_batch(self, lp, tkn, user_nm, events: dict):
57
+ """Process batched Sync events to check TVL and trigger exits."""
58
+ if not events:
59
+ print("No Sync events found in range.")
60
+ return
61
+ for k in events:
62
+ block_num = events[k]['blockNumber']
63
+ self.apply(lp, tkn, user_nm, block_num)
64
+
65
+ def apply(self, lp, tkn, user_nm, block_num):
66
+ """Execute liquidity exit if condition met."""
67
+ if self.check_condition(lp, tkn, self.config.tvl_threshold, block_num):
68
+ amount_out = self.withdraw_mock_position(lp, tkn, user_nm)
69
+ print(f"Block {block_num}: Withdrawing {amount_out} {tkn.token_name} from {lp.name} LP")
70
+ return amount_out
71
+ else:
72
+ print(f"Block {block_num}: TVL threshold condition met for {lp.name} LP")
73
+ return None
74
+
75
+ def check_condition(self, lp, tkn, threshold, block_num = None):
76
+ """Check if TVL is below threshold."""
77
+ block_num = self.get_w3().eth.block_number if block_num == None else block_num
78
+ tvl = self.get_pool_tvl(lp, tkn, block_num)
79
+ return tvl < threshold
80
+
81
+ def get_pool_tvl(self, lp, tkn, block_num):
82
+ """Calculate TVL from reserves (sum in USD, assuming base_token normalization)."""
83
+ lp = self.update_mock_pool(lp, block_num)
84
+ tot_lp = lp.get_liquidity()
85
+ tvl = LPQuote(False).get_amount_from_lp(lp, tkn, tot_lp)
86
+ return tvl
87
+
88
+ def take_mock_position(self, lp, tkn, user_nm, amt):
89
+ SwapDeposit().apply(lp, tkn, user_nm, amt)
90
+ self.mock_lp_pos_amt = lp.get_last_liquidity_deposit()
91
+ return self.mock_lp_pos_amt
92
+
93
+ def withdraw_mock_position(self, lp, tkn, user_nm, lp_amt = None):
94
+ assert self.mock_lp_pos_amt != None, 'TVLBasedLiquidityExitAgent: MOCK_POSITION_UNAVAILABLE'
95
+ lp_amt = self.mock_lp_pos_amt if lp_amt == None else lp_amt
96
+ tkn_amt = LPQuote(False).get_amount_from_lp(lp, tkn0, lp_amt)
97
+ amount_out = WithdrawSwap().apply(lp, tkn0, user_nm, tkn_amt)
98
+ return amount_out
99
+
100
+ def update_mock_pool(self, lp, cur_block):
101
+ w3 = self.get_w3()
102
+ fetch_tkn = FetchToken(w3)
103
+
104
+ lp_contract = self._init_lp_contract()
105
+ tkn0_addr = lp_contract.functions.token0().call()
106
+ tkn1_addr = lp_contract.functions.token1().call()
107
+ total_supply = lp_contract.functions.totalSupply().call(block_identifier=cur_block)
108
+ reserves = lp_contract.functions.getReserves().call(block_identifier=cur_block)
109
+
110
+ tkn0 = self.get_lp_data().tkn0
111
+ tkn1 = self.get_lp_data().tkn1
112
+ amt0 = fetch_tkn.amt_to_decimal(tkn0, reserves[0])
113
+ amt1 = fetch_tkn.amt_to_decimal(tkn1, reserves[1])
114
+
115
+ lp.reserve0 = lp.convert_to_machine(amt0) # override reserve0
116
+ lp.reserve1 = lp.convert_to_machine(amt1) # override reserve1
117
+ lp.total_supply = total_supply # override total supply
118
+
119
+ return lp
120
+
121
+ def prime_mock_pool(self, start_block, user_nm = None):
122
+ w3 = self.get_w3()
123
+ fetch_tkn = FetchToken(w3)
124
+
125
+ lp_contract = self._init_lp_contract()
126
+ tkn0_addr = lp_contract.functions.token0().call()
127
+ tkn1_addr = lp_contract.functions.token1().call()
128
+ total_supply = lp_contract.functions.totalSupply().call(block_identifier=start_block)
129
+ reserves = lp_contract.functions.getReserves().call(block_identifier=start_block)
130
+
131
+ # Step 2: Define tokens
132
+ tkn0 = fetch_tkn.apply(tkn0_addr)
133
+ tkn1 = fetch_tkn.apply(tkn1_addr)
134
+
135
+ amt0 = fetch_tkn.amt_to_decimal(tkn0, reserves[0])
136
+ amt1 = fetch_tkn.amt_to_decimal(tkn1, reserves[1])
137
+
138
+ # Step 3: Initialize factory
139
+ factory = UniswapFactory("Pool factory", "0x2")
140
+
141
+ # Step 4: Set up exchange data for V2
142
+ exch_data = UniswapExchangeData(tkn0=tkn0, tkn1=tkn1, symbol="LP", address=self.config.pool_address)
143
+
144
+ # Step 5: Deploy pool
145
+ self.lp_state = factory.deploy(exch_data)
146
+
147
+ # Step 6: Add initial liquidity
148
+ join = Join()
149
+ join.apply(self.lp_state, user_nm, amt0, amt1)
150
+ self.lp_state.total_supply = total_supply # override total supply
151
+
152
+ return self.lp_state
153
+
154
+ def get_connector(self):
155
+ return self.connector
156
+
157
+ def get_abi(self):
158
+ return self.abi
159
+
160
+ def get_w3(self):
161
+ return self.connector.get_w3()
162
+
163
+ def get_contract_instance(self):
164
+ return self.lp_contract
165
+
166
+ def get_lp_data(self):
167
+ return self.lp_data
168
+
169
+ def _init_lp_contract(self):
170
+ pair_address = self.config.pool_address
171
+ w3 = self.get_w3()
172
+ abi_obj = self.get_abi()
173
+ lp_contract = abi_obj.apply(w3, pair_address)
174
+ return lp_contract