olas-operate-middleware 0.1.0rc59__py3-none-any.whl → 0.13.2__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.
- olas_operate_middleware-0.13.2.dist-info/METADATA +75 -0
- olas_operate_middleware-0.13.2.dist-info/RECORD +101 -0
- {olas_operate_middleware-0.1.0rc59.dist-info → olas_operate_middleware-0.13.2.dist-info}/WHEEL +1 -1
- operate/__init__.py +17 -0
- operate/account/user.py +35 -9
- operate/bridge/bridge_manager.py +470 -0
- operate/bridge/providers/lifi_provider.py +377 -0
- operate/bridge/providers/native_bridge_provider.py +677 -0
- operate/bridge/providers/provider.py +469 -0
- operate/bridge/providers/relay_provider.py +457 -0
- operate/cli.py +1565 -417
- operate/constants.py +60 -12
- operate/data/README.md +19 -0
- operate/data/contracts/{service_staking_token → dual_staking_token}/__init__.py +2 -2
- operate/data/contracts/dual_staking_token/build/DualStakingToken.json +443 -0
- operate/data/contracts/dual_staking_token/contract.py +132 -0
- operate/data/contracts/dual_staking_token/contract.yaml +23 -0
- operate/{ledger/base.py → data/contracts/foreign_omnibridge/__init__.py} +2 -19
- operate/data/contracts/foreign_omnibridge/build/ForeignOmnibridge.json +1372 -0
- operate/data/contracts/foreign_omnibridge/contract.py +130 -0
- operate/data/contracts/foreign_omnibridge/contract.yaml +23 -0
- operate/{ledger/solana.py → data/contracts/home_omnibridge/__init__.py} +2 -20
- operate/data/contracts/home_omnibridge/build/HomeOmnibridge.json +1421 -0
- operate/data/contracts/home_omnibridge/contract.py +80 -0
- operate/data/contracts/home_omnibridge/contract.yaml +23 -0
- operate/data/contracts/l1_standard_bridge/__init__.py +20 -0
- operate/data/contracts/l1_standard_bridge/build/L1StandardBridge.json +831 -0
- operate/data/contracts/l1_standard_bridge/contract.py +158 -0
- operate/data/contracts/l1_standard_bridge/contract.yaml +23 -0
- operate/data/contracts/l2_standard_bridge/__init__.py +20 -0
- operate/data/contracts/l2_standard_bridge/build/L2StandardBridge.json +626 -0
- operate/data/contracts/l2_standard_bridge/contract.py +130 -0
- operate/data/contracts/l2_standard_bridge/contract.yaml +23 -0
- operate/data/contracts/mech_activity/__init__.py +20 -0
- operate/data/contracts/mech_activity/build/MechActivity.json +111 -0
- operate/data/contracts/mech_activity/contract.py +44 -0
- operate/data/contracts/mech_activity/contract.yaml +23 -0
- operate/data/contracts/optimism_mintable_erc20/__init__.py +20 -0
- operate/data/contracts/optimism_mintable_erc20/build/OptimismMintableERC20.json +491 -0
- operate/data/contracts/optimism_mintable_erc20/contract.py +45 -0
- operate/data/contracts/optimism_mintable_erc20/contract.yaml +23 -0
- operate/data/contracts/recovery_module/__init__.py +20 -0
- operate/data/contracts/recovery_module/build/RecoveryModule.json +811 -0
- operate/data/contracts/recovery_module/contract.py +61 -0
- operate/data/contracts/recovery_module/contract.yaml +23 -0
- operate/data/contracts/requester_activity_checker/__init__.py +20 -0
- operate/data/contracts/requester_activity_checker/build/RequesterActivityChecker.json +111 -0
- operate/data/contracts/requester_activity_checker/contract.py +33 -0
- operate/data/contracts/requester_activity_checker/contract.yaml +23 -0
- operate/data/contracts/staking_token/__init__.py +20 -0
- operate/data/contracts/staking_token/build/StakingToken.json +1336 -0
- operate/data/contracts/{service_staking_token → staking_token}/contract.py +27 -13
- operate/data/contracts/staking_token/contract.yaml +23 -0
- operate/data/contracts/uniswap_v2_erc20/contract.yaml +3 -1
- operate/data/contracts/uniswap_v2_erc20/tests/__init__.py +20 -0
- operate/data/contracts/uniswap_v2_erc20/tests/test_contract.py +363 -0
- operate/keys.py +118 -33
- operate/ledger/__init__.py +159 -56
- operate/ledger/profiles.py +321 -18
- operate/migration.py +555 -0
- operate/{http → operate_http}/__init__.py +3 -2
- operate/{http → operate_http}/exceptions.py +6 -4
- operate/operate_types.py +544 -0
- operate/pearl.py +13 -1
- operate/quickstart/analyse_logs.py +118 -0
- operate/quickstart/claim_staking_rewards.py +104 -0
- operate/quickstart/reset_configs.py +106 -0
- operate/quickstart/reset_password.py +70 -0
- operate/quickstart/reset_staking.py +145 -0
- operate/quickstart/run_service.py +726 -0
- operate/quickstart/stop_service.py +72 -0
- operate/quickstart/terminate_on_chain_service.py +83 -0
- operate/quickstart/utils.py +298 -0
- operate/resource.py +62 -3
- operate/services/agent_runner.py +202 -0
- operate/services/deployment_runner.py +868 -0
- operate/services/funding_manager.py +929 -0
- operate/services/health_checker.py +280 -0
- operate/services/manage.py +2356 -620
- operate/services/protocol.py +1246 -340
- operate/services/service.py +756 -391
- operate/services/utils/mech.py +103 -0
- operate/services/utils/tendermint.py +86 -12
- operate/settings.py +70 -0
- operate/utils/__init__.py +135 -0
- operate/utils/gnosis.py +407 -80
- operate/utils/single_instance.py +226 -0
- operate/utils/ssl.py +133 -0
- operate/wallet/master.py +708 -123
- operate/wallet/wallet_recovery_manager.py +507 -0
- olas_operate_middleware-0.1.0rc59.dist-info/METADATA +0 -304
- olas_operate_middleware-0.1.0rc59.dist-info/RECORD +0 -41
- operate/data/contracts/service_staking_token/build/ServiceStakingToken.json +0 -1273
- operate/data/contracts/service_staking_token/contract.yaml +0 -23
- operate/ledger/ethereum.py +0 -48
- operate/types.py +0 -260
- {olas_operate_middleware-0.1.0rc59.dist-info → olas_operate_middleware-0.13.2.dist-info}/entry_points.txt +0 -0
- {olas_operate_middleware-0.1.0rc59.dist-info → olas_operate_middleware-0.13.2.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# ------------------------------------------------------------------------------
|
|
3
|
+
#
|
|
4
|
+
# Copyright 2025 Valory AG
|
|
5
|
+
#
|
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
# you may not use this file except in compliance with the License.
|
|
8
|
+
# You may obtain a copy of the License at
|
|
9
|
+
#
|
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
#
|
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
# See the License for the specific language governing permissions and
|
|
16
|
+
# limitations under the License.
|
|
17
|
+
#
|
|
18
|
+
# ------------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
"""Funding manager"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# pylint: disable=too-many-locals,too-many-statements
|
|
24
|
+
|
|
25
|
+
import asyncio
|
|
26
|
+
import threading
|
|
27
|
+
import traceback
|
|
28
|
+
import typing as t
|
|
29
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
30
|
+
from logging import Logger
|
|
31
|
+
from time import time
|
|
32
|
+
|
|
33
|
+
from aea_ledger_ethereum import defaultdict
|
|
34
|
+
from autonomy.chain.base import registry_contracts
|
|
35
|
+
from autonomy.chain.config import CHAIN_PROFILES, ChainType
|
|
36
|
+
from web3 import Web3
|
|
37
|
+
|
|
38
|
+
from operate.constants import (
|
|
39
|
+
DEFAULT_FUNDING_REQUESTS_COOLDOWN_SECONDS,
|
|
40
|
+
MASTER_EOA_PLACEHOLDER,
|
|
41
|
+
MASTER_SAFE_PLACEHOLDER,
|
|
42
|
+
MIN_AGENT_BOND,
|
|
43
|
+
MIN_SECURITY_DEPOSIT,
|
|
44
|
+
NO_STAKING_PROGRAM_ID,
|
|
45
|
+
SERVICE_SAFE_PLACEHOLDER,
|
|
46
|
+
ZERO_ADDRESS,
|
|
47
|
+
)
|
|
48
|
+
from operate.keys import KeysManager
|
|
49
|
+
from operate.ledger import get_currency_denom, get_default_ledger_api
|
|
50
|
+
from operate.ledger.profiles import (
|
|
51
|
+
CONTRACTS,
|
|
52
|
+
DEFAULT_EOA_THRESHOLD,
|
|
53
|
+
DEFAULT_EOA_TOPUPS,
|
|
54
|
+
DEFAULT_EOA_TOPUPS_WITHOUT_SAFE,
|
|
55
|
+
OLAS,
|
|
56
|
+
USDC,
|
|
57
|
+
WRAPPED_NATIVE_ASSET,
|
|
58
|
+
get_asset_name,
|
|
59
|
+
)
|
|
60
|
+
from operate.operate_types import Chain, ChainAmounts, LedgerType, OnChainState
|
|
61
|
+
from operate.services.protocol import EthSafeTxBuilder, StakingManager, StakingState
|
|
62
|
+
from operate.services.service import NON_EXISTENT_TOKEN, Service
|
|
63
|
+
from operate.utils.gnosis import drain_eoa, get_asset_balance, get_owners
|
|
64
|
+
from operate.utils.gnosis import transfer as transfer_from_safe
|
|
65
|
+
from operate.utils.gnosis import transfer_erc20_from_safe
|
|
66
|
+
from operate.wallet.master import InsufficientFundsException, MasterWalletManager
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if t.TYPE_CHECKING:
|
|
70
|
+
from operate.services.manage import ServiceManager # pylint: disable=unused-import
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class FundingInProgressError(RuntimeError):
|
|
74
|
+
"""Raised when an attempt is made to fund a service that is already being funded."""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class FundingManager:
|
|
78
|
+
"""FundingManager"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
keys_manager: KeysManager,
|
|
83
|
+
wallet_manager: MasterWalletManager,
|
|
84
|
+
logger: Logger,
|
|
85
|
+
funding_requests_cooldown_seconds: int = DEFAULT_FUNDING_REQUESTS_COOLDOWN_SECONDS,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Initialize funding manager."""
|
|
88
|
+
self.keys_manager = keys_manager
|
|
89
|
+
self.wallet_manager = wallet_manager
|
|
90
|
+
self.logger = logger
|
|
91
|
+
self.funding_requests_cooldown_seconds = funding_requests_cooldown_seconds
|
|
92
|
+
self._lock = threading.Lock()
|
|
93
|
+
self._funding_in_progress: t.Dict[str, bool] = {}
|
|
94
|
+
self._funding_requests_cooldown_until: t.Dict[str, float] = {}
|
|
95
|
+
self.is_for_quickstart = False
|
|
96
|
+
|
|
97
|
+
def drain_agents_eoas(
|
|
98
|
+
self, service: Service, withdrawal_address: str, chain: Chain
|
|
99
|
+
) -> None:
|
|
100
|
+
"""Drain the funds out of the service agents EOAs."""
|
|
101
|
+
service_config_id = service.service_config_id
|
|
102
|
+
ledger_api = get_default_ledger_api(chain)
|
|
103
|
+
self.logger.info(
|
|
104
|
+
f"Draining service agents {service.name} ({service_config_id=})"
|
|
105
|
+
)
|
|
106
|
+
for agent_address in service.agent_addresses:
|
|
107
|
+
ethereum_crypto = self.keys_manager.get_crypto_instance(agent_address)
|
|
108
|
+
balance = ledger_api.get_balance(agent_address)
|
|
109
|
+
self.logger.info(
|
|
110
|
+
f"Draining {balance} (approx) {get_currency_denom(chain)} from {agent_address} (agent) to {withdrawal_address}"
|
|
111
|
+
)
|
|
112
|
+
drain_eoa(
|
|
113
|
+
ledger_api=ledger_api,
|
|
114
|
+
crypto=ethereum_crypto,
|
|
115
|
+
withdrawal_address=withdrawal_address,
|
|
116
|
+
chain_id=chain.id,
|
|
117
|
+
)
|
|
118
|
+
self.logger.info(f"{service.name} signer drained")
|
|
119
|
+
|
|
120
|
+
def drain_service_safe( # pylint: disable=too-many-locals
|
|
121
|
+
self, service: Service, withdrawal_address: str, chain: Chain
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Drain the funds out of the service safe."""
|
|
124
|
+
service_config_id = service.service_config_id
|
|
125
|
+
self.logger.info(f"Draining service safe {service.name} ({service_config_id=})")
|
|
126
|
+
chain_config = service.chain_configs[chain.value]
|
|
127
|
+
chain_data = chain_config.chain_data
|
|
128
|
+
ledger_api = get_default_ledger_api(chain)
|
|
129
|
+
withdrawal_address = Web3.to_checksum_address(withdrawal_address)
|
|
130
|
+
service_safe = chain_data.multisig
|
|
131
|
+
wallet = self.wallet_manager.load(chain.ledger_type)
|
|
132
|
+
master_safe = wallet.safes[chain]
|
|
133
|
+
ledger_config = chain_config.ledger_config
|
|
134
|
+
sftxb = EthSafeTxBuilder(
|
|
135
|
+
rpc=ledger_config.rpc,
|
|
136
|
+
wallet=wallet,
|
|
137
|
+
contracts=CONTRACTS[ledger_config.chain],
|
|
138
|
+
chain_type=ChainType(ledger_config.chain.value),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
owners = get_owners(ledger_api=ledger_api, safe=service_safe)
|
|
142
|
+
|
|
143
|
+
# Drain ERC20 tokens from service Safe
|
|
144
|
+
tokens = {
|
|
145
|
+
WRAPPED_NATIVE_ASSET[chain],
|
|
146
|
+
OLAS[chain],
|
|
147
|
+
USDC[chain],
|
|
148
|
+
} | service.chain_configs[
|
|
149
|
+
chain.value
|
|
150
|
+
].chain_data.user_params.fund_requirements.keys()
|
|
151
|
+
tokens.discard(ZERO_ADDRESS)
|
|
152
|
+
|
|
153
|
+
for token_address in tokens:
|
|
154
|
+
token_instance = registry_contracts.erc20.get_instance(
|
|
155
|
+
ledger_api=ledger_api,
|
|
156
|
+
contract_address=token_address,
|
|
157
|
+
)
|
|
158
|
+
balance = token_instance.functions.balanceOf(service_safe).call()
|
|
159
|
+
token_name = get_asset_name(chain, token_address)
|
|
160
|
+
if balance == 0:
|
|
161
|
+
self.logger.info(
|
|
162
|
+
f"No {token_name} to drain from service safe: {service_safe}"
|
|
163
|
+
)
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
self.logger.info(
|
|
167
|
+
f"Draining {balance} {token_name} from {service_safe} (service safe) to {withdrawal_address}"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Safe not swapped
|
|
171
|
+
if set(owners) == set(service.agent_addresses):
|
|
172
|
+
ethereum_crypto = self.keys_manager.get_crypto_instance(
|
|
173
|
+
service.agent_addresses[0]
|
|
174
|
+
)
|
|
175
|
+
transfer_erc20_from_safe(
|
|
176
|
+
ledger_api=ledger_api,
|
|
177
|
+
crypto=ethereum_crypto,
|
|
178
|
+
safe=chain_data.multisig,
|
|
179
|
+
token=token_address,
|
|
180
|
+
to=withdrawal_address,
|
|
181
|
+
amount=balance,
|
|
182
|
+
)
|
|
183
|
+
elif set(owners) == {master_safe}:
|
|
184
|
+
messages = sftxb.get_safe_b_erc20_transfer_messages(
|
|
185
|
+
safe_b_address=service_safe,
|
|
186
|
+
token=token_address,
|
|
187
|
+
to=withdrawal_address,
|
|
188
|
+
amount=balance,
|
|
189
|
+
)
|
|
190
|
+
tx = sftxb.new_tx()
|
|
191
|
+
for message in messages:
|
|
192
|
+
tx.add(message)
|
|
193
|
+
tx.settle()
|
|
194
|
+
|
|
195
|
+
else:
|
|
196
|
+
raise RuntimeError(
|
|
197
|
+
f"Cannot drain service safe: unrecognized owner set {owners=}"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Drain native asset from service Safe
|
|
201
|
+
balance = ledger_api.get_balance(service_safe)
|
|
202
|
+
if balance == 0:
|
|
203
|
+
self.logger.info(
|
|
204
|
+
f"No {get_currency_denom(chain)} to drain from service safe: {service_safe}"
|
|
205
|
+
)
|
|
206
|
+
else:
|
|
207
|
+
self.logger.info(
|
|
208
|
+
f"Draining {balance} {get_currency_denom(chain)} from {service_safe} (service safe) to {withdrawal_address}"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
if set(owners) == set(service.agent_addresses):
|
|
212
|
+
ethereum_crypto = self.keys_manager.get_crypto_instance(
|
|
213
|
+
service.agent_addresses[0]
|
|
214
|
+
)
|
|
215
|
+
transfer_from_safe(
|
|
216
|
+
ledger_api=ledger_api,
|
|
217
|
+
crypto=ethereum_crypto,
|
|
218
|
+
safe=chain_data.multisig,
|
|
219
|
+
to=withdrawal_address,
|
|
220
|
+
amount=balance,
|
|
221
|
+
)
|
|
222
|
+
elif set(owners) == {master_safe}:
|
|
223
|
+
messages = sftxb.get_safe_b_native_transfer_messages(
|
|
224
|
+
safe_b_address=service_safe,
|
|
225
|
+
to=withdrawal_address,
|
|
226
|
+
amount=balance,
|
|
227
|
+
)
|
|
228
|
+
tx = sftxb.new_tx()
|
|
229
|
+
for message in messages:
|
|
230
|
+
tx.add(message)
|
|
231
|
+
tx.settle()
|
|
232
|
+
|
|
233
|
+
else:
|
|
234
|
+
raise RuntimeError(
|
|
235
|
+
f"Cannot drain service safe: unrecognized owner set {owners=}"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
self.logger.info(f"Service safe {service.name} drained ({service_config_id=})")
|
|
239
|
+
|
|
240
|
+
# -------------------------------------------------------------------------------------
|
|
241
|
+
# -------------------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
def _compute_protocol_asset_requirements(self, service: Service) -> ChainAmounts:
|
|
244
|
+
"""Computes the protocol asset requirements to deploy on-chain and stake (if necessary)"""
|
|
245
|
+
|
|
246
|
+
self.logger.info(
|
|
247
|
+
f"[FUNDING MANAGER] Computing protocol asset requirements for service {service.service_config_id}"
|
|
248
|
+
)
|
|
249
|
+
protocol_asset_requirements = ChainAmounts()
|
|
250
|
+
|
|
251
|
+
for chain, chain_config in service.chain_configs.items():
|
|
252
|
+
user_params = chain_config.chain_data.user_params
|
|
253
|
+
number_of_agents = len(service.agent_addresses)
|
|
254
|
+
|
|
255
|
+
requirements: defaultdict = defaultdict(int)
|
|
256
|
+
|
|
257
|
+
if (
|
|
258
|
+
not user_params.use_staking
|
|
259
|
+
or not user_params.staking_program_id
|
|
260
|
+
or user_params.staking_program_id == NO_STAKING_PROGRAM_ID
|
|
261
|
+
):
|
|
262
|
+
protocol_agent_bonds = (
|
|
263
|
+
max(MIN_AGENT_BOND, user_params.cost_of_bond) * number_of_agents
|
|
264
|
+
)
|
|
265
|
+
protocol_security_deposit = max(
|
|
266
|
+
MIN_SECURITY_DEPOSIT, user_params.cost_of_bond
|
|
267
|
+
)
|
|
268
|
+
staking_agent_bonds = 0
|
|
269
|
+
staking_security_deposit = 0
|
|
270
|
+
else:
|
|
271
|
+
protocol_agent_bonds = MIN_AGENT_BOND * number_of_agents
|
|
272
|
+
protocol_security_deposit = MIN_SECURITY_DEPOSIT
|
|
273
|
+
|
|
274
|
+
staking_manager = StakingManager(chain=Chain(chain))
|
|
275
|
+
staking_params = staking_manager.get_staking_params(
|
|
276
|
+
staking_contract=staking_manager.get_staking_contract(
|
|
277
|
+
staking_program_id=user_params.staking_program_id,
|
|
278
|
+
),
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
staking_agent_bonds = (
|
|
282
|
+
staking_params["min_staking_deposit"] * number_of_agents
|
|
283
|
+
)
|
|
284
|
+
staking_security_deposit = staking_params["min_staking_deposit"]
|
|
285
|
+
staking_token = staking_params["staking_token"]
|
|
286
|
+
requirements[staking_token] += staking_agent_bonds
|
|
287
|
+
requirements[staking_token] += staking_security_deposit
|
|
288
|
+
|
|
289
|
+
for token, amount in staking_params[
|
|
290
|
+
"additional_staking_tokens"
|
|
291
|
+
].items():
|
|
292
|
+
requirements[token] = amount
|
|
293
|
+
|
|
294
|
+
requirements[ZERO_ADDRESS] += protocol_agent_bonds
|
|
295
|
+
requirements[ZERO_ADDRESS] += protocol_security_deposit
|
|
296
|
+
|
|
297
|
+
master_safe = self._resolve_master_safe(Chain(chain))
|
|
298
|
+
|
|
299
|
+
protocol_asset_requirements[chain] = {master_safe: dict(requirements)}
|
|
300
|
+
|
|
301
|
+
return protocol_asset_requirements
|
|
302
|
+
|
|
303
|
+
# TODO address this comment
|
|
304
|
+
# This computation assumes the service will be/has been minted with these
|
|
305
|
+
# parameters. Otherwise, these values should be retrieved on-chain as follows:
|
|
306
|
+
# - agent_bonds: by combining the output of ServiceRegistry .getAgentParams .getService
|
|
307
|
+
# and ServiceRegistryTokenUtility .getAgentBond
|
|
308
|
+
# - security_deposit: as the maximum agent bond.
|
|
309
|
+
|
|
310
|
+
def _compute_protocol_bonded_assets( # pylint: disable=too-many-locals,too-many-statements
|
|
311
|
+
self, service: Service
|
|
312
|
+
) -> ChainAmounts:
|
|
313
|
+
"""Computes the bonded assets: current agent bonds and current security deposit"""
|
|
314
|
+
|
|
315
|
+
protocol_bonded_assets = ChainAmounts()
|
|
316
|
+
|
|
317
|
+
for chain, chain_config in service.chain_configs.items():
|
|
318
|
+
bonded_assets: defaultdict = defaultdict(int)
|
|
319
|
+
ledger_config = chain_config.ledger_config
|
|
320
|
+
user_params = chain_config.chain_data.user_params
|
|
321
|
+
|
|
322
|
+
if not self.wallet_manager.exists(ledger_config.chain.ledger_type):
|
|
323
|
+
protocol_bonded_assets[chain] = {
|
|
324
|
+
MASTER_SAFE_PLACEHOLDER: dict(bonded_assets)
|
|
325
|
+
}
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
wallet = self.wallet_manager.load(ledger_config.chain.ledger_type)
|
|
329
|
+
ledger_api = get_default_ledger_api(Chain(chain))
|
|
330
|
+
staking_manager = StakingManager(Chain(chain))
|
|
331
|
+
|
|
332
|
+
if Chain(chain) not in wallet.safes:
|
|
333
|
+
protocol_bonded_assets[chain] = {
|
|
334
|
+
MASTER_SAFE_PLACEHOLDER: dict(bonded_assets)
|
|
335
|
+
}
|
|
336
|
+
continue
|
|
337
|
+
|
|
338
|
+
master_safe = wallet.safes[Chain(chain)]
|
|
339
|
+
|
|
340
|
+
service_id = chain_config.chain_data.token
|
|
341
|
+
if service_id == NON_EXISTENT_TOKEN:
|
|
342
|
+
protocol_bonded_assets[chain] = {master_safe: dict(bonded_assets)}
|
|
343
|
+
continue
|
|
344
|
+
|
|
345
|
+
# os.environ["CUSTOM_CHAIN_RPC"] = ledger_config.rpc # TODO do we need this?
|
|
346
|
+
|
|
347
|
+
# Determine bonded native amount
|
|
348
|
+
service_registry_address = CHAIN_PROFILES[chain]["service_registry"]
|
|
349
|
+
service_registry = registry_contracts.service_registry.get_instance(
|
|
350
|
+
ledger_api=ledger_api,
|
|
351
|
+
contract_address=service_registry_address,
|
|
352
|
+
)
|
|
353
|
+
service_info = service_registry.functions.getService(service_id).call()
|
|
354
|
+
security_deposit = service_info[0]
|
|
355
|
+
service_state = service_info[6]
|
|
356
|
+
agent_ids = service_info[7]
|
|
357
|
+
|
|
358
|
+
if (
|
|
359
|
+
OnChainState.ACTIVE_REGISTRATION
|
|
360
|
+
<= service_state
|
|
361
|
+
< OnChainState.TERMINATED_BONDED
|
|
362
|
+
):
|
|
363
|
+
bonded_assets[ZERO_ADDRESS] += security_deposit
|
|
364
|
+
|
|
365
|
+
operator_balance = service_registry.functions.getOperatorBalance(
|
|
366
|
+
master_safe, service_id
|
|
367
|
+
).call()
|
|
368
|
+
bonded_assets[ZERO_ADDRESS] += operator_balance
|
|
369
|
+
|
|
370
|
+
# Determine bonded token amount for staking programs
|
|
371
|
+
current_staking_program = staking_manager.get_current_staking_program(
|
|
372
|
+
service_id=service_id,
|
|
373
|
+
)
|
|
374
|
+
target_staking_program = user_params.staking_program_id
|
|
375
|
+
staking_contract = staking_manager.get_staking_contract(
|
|
376
|
+
staking_program_id=current_staking_program or target_staking_program,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
if not staking_contract:
|
|
380
|
+
return dict(bonded_assets)
|
|
381
|
+
|
|
382
|
+
staking_manager = StakingManager(Chain(chain))
|
|
383
|
+
staking_params = staking_manager.get_staking_params(
|
|
384
|
+
staking_contract=staking_manager.get_staking_contract(
|
|
385
|
+
staking_program_id=user_params.staking_program_id,
|
|
386
|
+
),
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
service_registry_token_utility_address = staking_params[
|
|
390
|
+
"service_registry_token_utility"
|
|
391
|
+
]
|
|
392
|
+
service_registry_token_utility = (
|
|
393
|
+
registry_contracts.service_registry_token_utility.get_instance(
|
|
394
|
+
ledger_api=ledger_api,
|
|
395
|
+
contract_address=service_registry_token_utility_address,
|
|
396
|
+
)
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
agent_bonds = 0
|
|
400
|
+
for agent_id in agent_ids:
|
|
401
|
+
num_agent_instances = service_registry.functions.getInstancesForAgentId(
|
|
402
|
+
service_id, agent_id
|
|
403
|
+
).call()[0]
|
|
404
|
+
agent_bond = service_registry_token_utility.functions.getAgentBond(
|
|
405
|
+
service_id, agent_id
|
|
406
|
+
).call()
|
|
407
|
+
agent_bonds += num_agent_instances * agent_bond
|
|
408
|
+
|
|
409
|
+
if service_state == OnChainState.TERMINATED_BONDED:
|
|
410
|
+
num_agent_instances = service_info[5]
|
|
411
|
+
token_bond = (
|
|
412
|
+
service_registry_token_utility.functions.getOperatorBalance(
|
|
413
|
+
master_safe,
|
|
414
|
+
service_id,
|
|
415
|
+
).call()
|
|
416
|
+
)
|
|
417
|
+
agent_bonds += num_agent_instances * token_bond
|
|
418
|
+
|
|
419
|
+
security_deposit = 0
|
|
420
|
+
if (
|
|
421
|
+
OnChainState.ACTIVE_REGISTRATION
|
|
422
|
+
<= service_state
|
|
423
|
+
< OnChainState.TERMINATED_BONDED
|
|
424
|
+
):
|
|
425
|
+
security_deposit = (
|
|
426
|
+
service_registry_token_utility.functions.mapServiceIdTokenDeposit(
|
|
427
|
+
service_id
|
|
428
|
+
).call()[1]
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
bonded_assets[staking_params["staking_token"]] += agent_bonds
|
|
432
|
+
bonded_assets[staking_params["staking_token"]] += security_deposit
|
|
433
|
+
|
|
434
|
+
staking_state = staking_manager.staking_state(
|
|
435
|
+
service_id=service_id,
|
|
436
|
+
staking_contract=staking_params["staking_contract"],
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
if staking_state in (StakingState.STAKED, StakingState.EVICTED):
|
|
440
|
+
for token, amount in staking_params[
|
|
441
|
+
"additional_staking_tokens"
|
|
442
|
+
].items():
|
|
443
|
+
bonded_assets[token] += amount
|
|
444
|
+
|
|
445
|
+
protocol_bonded_assets[chain] = {master_safe: dict(bonded_assets)}
|
|
446
|
+
|
|
447
|
+
return protocol_bonded_assets
|
|
448
|
+
|
|
449
|
+
@staticmethod
|
|
450
|
+
def _compute_shortfalls(
|
|
451
|
+
balances: ChainAmounts,
|
|
452
|
+
thresholds: ChainAmounts,
|
|
453
|
+
topups: ChainAmounts,
|
|
454
|
+
) -> ChainAmounts:
|
|
455
|
+
"""Compute shortfall per chain/address/asset: if balance < threshold, shortfall = topup - balance, else 0"""
|
|
456
|
+
shortfalls = ChainAmounts()
|
|
457
|
+
|
|
458
|
+
for chain, addresses in thresholds.items():
|
|
459
|
+
shortfalls.setdefault(chain, {})
|
|
460
|
+
for address, assets in addresses.items():
|
|
461
|
+
shortfalls[chain].setdefault(address, {})
|
|
462
|
+
for asset, threshold in assets.items():
|
|
463
|
+
balance = balances.get(chain, {}).get(address, {}).get(asset, 0)
|
|
464
|
+
topup = topups.get(chain, {}).get(address, {}).get(asset, 0)
|
|
465
|
+
if balance < threshold:
|
|
466
|
+
shortfalls[chain][address][asset] = max(topup - balance, 0)
|
|
467
|
+
else:
|
|
468
|
+
shortfalls[chain][address][asset] = 0
|
|
469
|
+
|
|
470
|
+
return shortfalls
|
|
471
|
+
|
|
472
|
+
def _resolve_master_eoa(self, chain: Chain) -> str:
|
|
473
|
+
if self.wallet_manager.exists(chain.ledger_type):
|
|
474
|
+
wallet = self.wallet_manager.load(chain.ledger_type)
|
|
475
|
+
return wallet.address
|
|
476
|
+
return MASTER_EOA_PLACEHOLDER
|
|
477
|
+
|
|
478
|
+
def _resolve_master_safe(self, chain: Chain) -> str:
|
|
479
|
+
if self.wallet_manager.exists(chain.ledger_type):
|
|
480
|
+
wallet = self.wallet_manager.load(chain.ledger_type)
|
|
481
|
+
if chain in wallet.safes:
|
|
482
|
+
return wallet.safes[chain]
|
|
483
|
+
return MASTER_SAFE_PLACEHOLDER
|
|
484
|
+
|
|
485
|
+
def _aggregate_as_master_safe_amounts(self, *amounts: ChainAmounts) -> ChainAmounts:
|
|
486
|
+
output = ChainAmounts()
|
|
487
|
+
for amts in amounts:
|
|
488
|
+
for chain_str, addresses in amts.items():
|
|
489
|
+
chain = Chain(chain_str)
|
|
490
|
+
master_safe = self._resolve_master_safe(chain)
|
|
491
|
+
master_safe_dict = output.setdefault(chain_str, {}).setdefault(
|
|
492
|
+
master_safe, {}
|
|
493
|
+
)
|
|
494
|
+
for _, assets in addresses.items():
|
|
495
|
+
for asset, amount in assets.items():
|
|
496
|
+
master_safe_dict[asset] = (
|
|
497
|
+
master_safe_dict.get(asset, 0) + amount
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
return output
|
|
501
|
+
|
|
502
|
+
def _split_excess_assets_master_eoa_balances(
|
|
503
|
+
self, balances: ChainAmounts
|
|
504
|
+
) -> t.Tuple[ChainAmounts, ChainAmounts]:
|
|
505
|
+
"""Splits excess balances from master EOA only on chains without a Master Safe."""
|
|
506
|
+
excess_balance = ChainAmounts()
|
|
507
|
+
remaining_balance = ChainAmounts()
|
|
508
|
+
|
|
509
|
+
for chain_str, addresses in balances.items():
|
|
510
|
+
chain = Chain(chain_str)
|
|
511
|
+
master_safe = self._resolve_master_safe(chain)
|
|
512
|
+
for address, assets in addresses.items():
|
|
513
|
+
for asset, amount in assets.items():
|
|
514
|
+
if master_safe == MASTER_SAFE_PLACEHOLDER:
|
|
515
|
+
remaining = min(
|
|
516
|
+
amount, DEFAULT_EOA_TOPUPS[chain].get(asset, 0)
|
|
517
|
+
) # When transferring, the Master Safe will be already created, that is why we are only retaining DEFAULT_EOA_TOPUPS
|
|
518
|
+
excess = amount - remaining
|
|
519
|
+
else:
|
|
520
|
+
remaining = amount
|
|
521
|
+
excess = 0
|
|
522
|
+
|
|
523
|
+
excess_balance.setdefault(chain_str, {}).setdefault(
|
|
524
|
+
master_safe, {}
|
|
525
|
+
)[asset] = excess
|
|
526
|
+
remaining_balance.setdefault(chain_str, {}).setdefault(address, {})[
|
|
527
|
+
asset
|
|
528
|
+
] = remaining
|
|
529
|
+
|
|
530
|
+
return excess_balance, remaining_balance
|
|
531
|
+
|
|
532
|
+
@staticmethod
|
|
533
|
+
def _split_critical_eoa_shortfalls(
|
|
534
|
+
balances: ChainAmounts, shortfalls: ChainAmounts
|
|
535
|
+
) -> t.Tuple[ChainAmounts, ChainAmounts]:
|
|
536
|
+
"""Splits critical EOA shortfalls in two: the first split containins the native shortfalls whose balance is < threshold / 2. The second one, contains the remaining shortfalls. This is to ensure EOA operational balance."""
|
|
537
|
+
critical_shortfalls = ChainAmounts()
|
|
538
|
+
remaining_shortfalls = ChainAmounts()
|
|
539
|
+
|
|
540
|
+
for chain_str, addresses in shortfalls.items():
|
|
541
|
+
chain = Chain(chain_str)
|
|
542
|
+
for address, assets in addresses.items():
|
|
543
|
+
for asset, amount in assets.items():
|
|
544
|
+
if asset == ZERO_ADDRESS and balances[chain_str][address][
|
|
545
|
+
asset
|
|
546
|
+
] < int(
|
|
547
|
+
DEFAULT_EOA_TOPUPS[chain][asset] / 4
|
|
548
|
+
): # TODO Ensure that this is enough to pay a transfer tx at least.
|
|
549
|
+
critical_shortfalls.setdefault(chain_str, {}).setdefault(
|
|
550
|
+
address, {}
|
|
551
|
+
)[asset] = amount
|
|
552
|
+
remaining_shortfalls.setdefault(chain_str, {}).setdefault(
|
|
553
|
+
address, {}
|
|
554
|
+
)[asset] = 0
|
|
555
|
+
else:
|
|
556
|
+
critical_shortfalls.setdefault(chain_str, {}).setdefault(
|
|
557
|
+
address, {}
|
|
558
|
+
)[asset] = 0
|
|
559
|
+
remaining_shortfalls.setdefault(chain_str, {}).setdefault(
|
|
560
|
+
address, {}
|
|
561
|
+
)[asset] = amount
|
|
562
|
+
|
|
563
|
+
return critical_shortfalls, remaining_shortfalls
|
|
564
|
+
|
|
565
|
+
def _get_master_safe_balances(self, thresholds: ChainAmounts) -> ChainAmounts:
|
|
566
|
+
output = ChainAmounts()
|
|
567
|
+
for chain_str, addresses in thresholds.items():
|
|
568
|
+
chain = Chain(chain_str)
|
|
569
|
+
master_safe = self._resolve_master_safe(chain)
|
|
570
|
+
master_safe_dict = output.setdefault(chain_str, {}).setdefault(
|
|
571
|
+
master_safe, {}
|
|
572
|
+
)
|
|
573
|
+
for _, assets in addresses.items():
|
|
574
|
+
for asset, _ in assets.items():
|
|
575
|
+
master_safe_dict[asset] = get_asset_balance(
|
|
576
|
+
ledger_api=get_default_ledger_api(chain),
|
|
577
|
+
asset_address=asset,
|
|
578
|
+
address=master_safe,
|
|
579
|
+
raise_on_invalid_address=False,
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
return output
|
|
583
|
+
|
|
584
|
+
def _get_master_eoa_balances(self, thresholds: ChainAmounts) -> ChainAmounts:
|
|
585
|
+
output = ChainAmounts()
|
|
586
|
+
for chain_str, addresses in thresholds.items():
|
|
587
|
+
chain = Chain(chain_str)
|
|
588
|
+
master_eoa = self._resolve_master_eoa(chain)
|
|
589
|
+
master_eoa_dict = output.setdefault(chain_str, {}).setdefault(
|
|
590
|
+
master_eoa, {}
|
|
591
|
+
)
|
|
592
|
+
for _, assets in addresses.items():
|
|
593
|
+
for asset, _ in assets.items():
|
|
594
|
+
master_eoa_dict[asset] = get_asset_balance(
|
|
595
|
+
ledger_api=get_default_ledger_api(chain),
|
|
596
|
+
asset_address=asset,
|
|
597
|
+
address=master_eoa,
|
|
598
|
+
raise_on_invalid_address=False,
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
return output
|
|
602
|
+
|
|
603
|
+
def fund_master_eoa(self) -> None:
|
|
604
|
+
"""Fund Master EOA"""
|
|
605
|
+
if not self.wallet_manager.exists(LedgerType.ETHEREUM):
|
|
606
|
+
self.logger.warning(
|
|
607
|
+
"[FUNDING MANAGER] Cannot fund Master EOA: No Ethereum wallet available."
|
|
608
|
+
)
|
|
609
|
+
return
|
|
610
|
+
|
|
611
|
+
master_wallet = self.wallet_manager.load(
|
|
612
|
+
ledger_type=LedgerType.ETHEREUM
|
|
613
|
+
) # Only for ethereum for now
|
|
614
|
+
self.logger.info(
|
|
615
|
+
f"[FUNDING MANAGER] Funding Master EOA {master_wallet.address}"
|
|
616
|
+
)
|
|
617
|
+
master_eoa_topups = ChainAmounts(
|
|
618
|
+
{
|
|
619
|
+
chain.value: {
|
|
620
|
+
self._resolve_master_eoa(chain): dict(DEFAULT_EOA_TOPUPS[chain])
|
|
621
|
+
}
|
|
622
|
+
for chain in master_wallet.safes
|
|
623
|
+
}
|
|
624
|
+
)
|
|
625
|
+
master_eoa_balances = self._get_master_eoa_balances(master_eoa_topups)
|
|
626
|
+
master_eoa_shortfalls = self._compute_shortfalls(
|
|
627
|
+
balances=master_eoa_balances,
|
|
628
|
+
thresholds=master_eoa_topups * DEFAULT_EOA_THRESHOLD,
|
|
629
|
+
topups=master_eoa_topups,
|
|
630
|
+
)
|
|
631
|
+
self.fund_chain_amounts(master_eoa_shortfalls)
|
|
632
|
+
|
|
633
|
+
def funding_requirements(self, service: Service) -> t.Dict:
|
|
634
|
+
"""Funding requirements"""
|
|
635
|
+
balances: ChainAmounts
|
|
636
|
+
protocol_bonded_assets: ChainAmounts
|
|
637
|
+
protocol_asset_requirements: ChainAmounts
|
|
638
|
+
refill_requirements: ChainAmounts
|
|
639
|
+
total_requirements: ChainAmounts
|
|
640
|
+
chains = [Chain(chain_str) for chain_str in service.chain_configs.keys()]
|
|
641
|
+
|
|
642
|
+
# Protocol shortfall
|
|
643
|
+
protocol_thresholds = self._compute_protocol_asset_requirements(service)
|
|
644
|
+
protocol_balances = self._compute_protocol_bonded_assets(service)
|
|
645
|
+
protocol_topups = protocol_thresholds
|
|
646
|
+
protocol_shortfalls = self._compute_shortfalls(
|
|
647
|
+
balances=protocol_balances,
|
|
648
|
+
thresholds=protocol_thresholds,
|
|
649
|
+
topups=protocol_topups,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
# Initial service shortfall
|
|
653
|
+
# We assume that if the service safe is created in any chain,
|
|
654
|
+
# we have requested the funding already.
|
|
655
|
+
service_initial_topup = service.get_initial_funding_amounts()
|
|
656
|
+
if self.is_for_quickstart:
|
|
657
|
+
service_initial_shortfalls = self.compute_service_initial_shortfalls(
|
|
658
|
+
service
|
|
659
|
+
)
|
|
660
|
+
elif not all(
|
|
661
|
+
SERVICE_SAFE_PLACEHOLDER in addresses
|
|
662
|
+
for addresses in service_initial_topup.values()
|
|
663
|
+
):
|
|
664
|
+
service_initial_shortfalls = ChainAmounts()
|
|
665
|
+
else:
|
|
666
|
+
service_initial_shortfalls = service_initial_topup
|
|
667
|
+
|
|
668
|
+
# Service funding requests
|
|
669
|
+
service_config_id = service.service_config_id
|
|
670
|
+
funding_in_progress = self._funding_in_progress.get(service_config_id, False)
|
|
671
|
+
now = time()
|
|
672
|
+
if funding_in_progress:
|
|
673
|
+
funding_requests = ChainAmounts()
|
|
674
|
+
funding_requests_cooldown = False
|
|
675
|
+
elif now < self._funding_requests_cooldown_until.get(service_config_id, 0):
|
|
676
|
+
funding_requests = ChainAmounts()
|
|
677
|
+
funding_requests_cooldown = True
|
|
678
|
+
elif self.is_for_quickstart:
|
|
679
|
+
funding_requests = ChainAmounts()
|
|
680
|
+
funding_requests_cooldown = False
|
|
681
|
+
else:
|
|
682
|
+
funding_requests = service.get_funding_requests()
|
|
683
|
+
funding_requests_cooldown = False
|
|
684
|
+
|
|
685
|
+
# Master EOA shortfall
|
|
686
|
+
master_eoa_topups = ChainAmounts()
|
|
687
|
+
for chain in chains:
|
|
688
|
+
chain_str = chain.value
|
|
689
|
+
master_eoa = self._resolve_master_eoa(chain)
|
|
690
|
+
master_safe = self._resolve_master_safe(chain)
|
|
691
|
+
|
|
692
|
+
if master_safe != MASTER_SAFE_PLACEHOLDER:
|
|
693
|
+
master_eoa_topups[chain_str] = {
|
|
694
|
+
master_eoa: dict(DEFAULT_EOA_TOPUPS[chain])
|
|
695
|
+
}
|
|
696
|
+
else:
|
|
697
|
+
master_eoa_topups[chain_str] = {
|
|
698
|
+
master_eoa: dict(DEFAULT_EOA_TOPUPS_WITHOUT_SAFE[chain])
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
# Set the topup for MasterEOA for remaining tokens to 0 if they don't exist
|
|
702
|
+
# This ensures that the balances of MasterEOA are collected for relevant tokens
|
|
703
|
+
all_assets = {ZERO_ADDRESS} | {
|
|
704
|
+
asset
|
|
705
|
+
for addresses in (
|
|
706
|
+
protocol_topups[chain_str],
|
|
707
|
+
service_initial_topup[chain_str],
|
|
708
|
+
)
|
|
709
|
+
for assets in addresses.values()
|
|
710
|
+
for asset in assets
|
|
711
|
+
}
|
|
712
|
+
for asset in all_assets:
|
|
713
|
+
master_eoa_topups[chain_str][master_eoa].setdefault(asset, 0)
|
|
714
|
+
|
|
715
|
+
master_eoa_thresholds = master_eoa_topups // 2
|
|
716
|
+
master_eoa_balances = self._get_master_eoa_balances(master_eoa_thresholds)
|
|
717
|
+
|
|
718
|
+
# BEGIN Bridging patch: remove excess balances for chains without a Safe:
|
|
719
|
+
(
|
|
720
|
+
excess_master_eoa_balances,
|
|
721
|
+
master_eoa_balances,
|
|
722
|
+
) = self._split_excess_assets_master_eoa_balances(master_eoa_balances)
|
|
723
|
+
# END Bridging patch
|
|
724
|
+
|
|
725
|
+
master_eoa_shortfalls = self._compute_shortfalls(
|
|
726
|
+
balances=master_eoa_balances,
|
|
727
|
+
thresholds=master_eoa_thresholds,
|
|
728
|
+
topups=master_eoa_topups,
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
(
|
|
732
|
+
master_eoa_critical_shortfalls,
|
|
733
|
+
master_eoa_shortfalls,
|
|
734
|
+
) = self._split_critical_eoa_shortfalls(
|
|
735
|
+
balances=master_eoa_balances, shortfalls=master_eoa_shortfalls
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
# Master Safe shortfall
|
|
739
|
+
master_safe_thresholds = self._aggregate_as_master_safe_amounts(
|
|
740
|
+
master_eoa_shortfalls,
|
|
741
|
+
protocol_shortfalls,
|
|
742
|
+
service_initial_shortfalls,
|
|
743
|
+
)
|
|
744
|
+
master_safe_topup = master_safe_thresholds
|
|
745
|
+
master_safe_balances = ChainAmounts.add(
|
|
746
|
+
self._get_master_safe_balances(master_safe_thresholds),
|
|
747
|
+
self._aggregate_as_master_safe_amounts(excess_master_eoa_balances),
|
|
748
|
+
)
|
|
749
|
+
master_safe_shortfalls = self._compute_shortfalls(
|
|
750
|
+
balances=master_safe_balances,
|
|
751
|
+
thresholds=master_safe_thresholds,
|
|
752
|
+
topups=master_safe_topup,
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
# Prepare output values
|
|
756
|
+
protocol_bonded_assets = protocol_balances
|
|
757
|
+
protocol_asset_requirements = protocol_thresholds
|
|
758
|
+
refill_requirements = ChainAmounts.add(
|
|
759
|
+
master_eoa_critical_shortfalls,
|
|
760
|
+
master_safe_shortfalls,
|
|
761
|
+
)
|
|
762
|
+
total_requirements = ChainAmounts.add( # TODO Review if this is correct
|
|
763
|
+
master_eoa_critical_shortfalls,
|
|
764
|
+
master_safe_thresholds,
|
|
765
|
+
)
|
|
766
|
+
balances = ChainAmounts.add(
|
|
767
|
+
master_eoa_balances,
|
|
768
|
+
master_safe_balances,
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
# Compute boolean flags
|
|
772
|
+
is_refill_required = any(
|
|
773
|
+
amount > 0
|
|
774
|
+
for address in refill_requirements.values()
|
|
775
|
+
for assets in address.values()
|
|
776
|
+
for amount in assets.values()
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
allow_start_agent = True
|
|
780
|
+
if any(
|
|
781
|
+
MASTER_SAFE_PLACEHOLDER in addresses
|
|
782
|
+
for addresses in refill_requirements.values()
|
|
783
|
+
) or any(
|
|
784
|
+
amount > 0
|
|
785
|
+
for address in master_eoa_critical_shortfalls.values()
|
|
786
|
+
for assets in address.values()
|
|
787
|
+
for amount in assets.values()
|
|
788
|
+
):
|
|
789
|
+
allow_start_agent = False
|
|
790
|
+
|
|
791
|
+
return {
|
|
792
|
+
"balances": balances,
|
|
793
|
+
"bonded_assets": protocol_bonded_assets,
|
|
794
|
+
"total_requirements": total_requirements,
|
|
795
|
+
"refill_requirements": refill_requirements,
|
|
796
|
+
"protocol_asset_requirements": protocol_asset_requirements,
|
|
797
|
+
"is_refill_required": is_refill_required,
|
|
798
|
+
"allow_start_agent": allow_start_agent,
|
|
799
|
+
"agent_funding_requests": funding_requests,
|
|
800
|
+
"agent_funding_requests_cooldown": funding_requests_cooldown,
|
|
801
|
+
"agent_funding_in_progress": funding_in_progress,
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
def fund_service_initial(self, service: Service) -> None:
|
|
805
|
+
"""Fund service initially"""
|
|
806
|
+
self.fund_chain_amounts(service.get_initial_funding_amounts())
|
|
807
|
+
|
|
808
|
+
def compute_service_initial_shortfalls(self, service: Service) -> ChainAmounts:
|
|
809
|
+
"""Compute service initial shortfalls"""
|
|
810
|
+
initial_funding_amounts = service.get_initial_funding_amounts()
|
|
811
|
+
service_balances = service.get_balances()
|
|
812
|
+
return self._compute_shortfalls(
|
|
813
|
+
balances=service_balances,
|
|
814
|
+
thresholds=initial_funding_amounts,
|
|
815
|
+
topups=initial_funding_amounts,
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
def topup_service_initial(self, service: Service) -> None:
|
|
819
|
+
"""Fund service enough to reach initial funding amounts"""
|
|
820
|
+
service_initial_shortfalls = self.compute_service_initial_shortfalls(service)
|
|
821
|
+
self.fund_chain_amounts(service_initial_shortfalls)
|
|
822
|
+
|
|
823
|
+
def fund_chain_amounts(self, amounts: ChainAmounts) -> None:
|
|
824
|
+
"""Fund chain amounts"""
|
|
825
|
+
required = self._aggregate_as_master_safe_amounts(amounts)
|
|
826
|
+
balances = self._get_master_safe_balances(required)
|
|
827
|
+
|
|
828
|
+
if balances < required:
|
|
829
|
+
raise InsufficientFundsException(
|
|
830
|
+
f"Insufficient funds in Master Safe to perform funding. Required: {amounts}, Available: {balances}"
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
for chain_str, addresses in amounts.items():
|
|
834
|
+
chain = Chain(chain_str)
|
|
835
|
+
wallet = self.wallet_manager.load(chain.ledger_type)
|
|
836
|
+
for address, assets in addresses.items():
|
|
837
|
+
for asset, amount in assets.items():
|
|
838
|
+
if amount <= 0:
|
|
839
|
+
continue
|
|
840
|
+
|
|
841
|
+
self.logger.info(
|
|
842
|
+
f"[FUNDING MANAGER] Funding {amount} of {asset} to {address} on chain {chain.value} from Master Safe {wallet.safes.get(chain, 'N/A')}"
|
|
843
|
+
)
|
|
844
|
+
wallet.transfer(
|
|
845
|
+
chain=chain,
|
|
846
|
+
to=address,
|
|
847
|
+
asset=asset,
|
|
848
|
+
amount=amount,
|
|
849
|
+
from_safe=True,
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
def fund_service(self, service: Service, amounts: ChainAmounts) -> None:
|
|
853
|
+
"""Fund service-related wallets."""
|
|
854
|
+
service_config_id = service.service_config_id
|
|
855
|
+
|
|
856
|
+
# Atomic, thread-safe get-and-set of the _funding_in_progress boolean.
|
|
857
|
+
# This ensures only one funding operation per service at a time, and
|
|
858
|
+
# any call to fund_service while a funding operation is in progress will
|
|
859
|
+
# raise a FundingInProgressError (instead of blocking and piling up calls).
|
|
860
|
+
with self._lock:
|
|
861
|
+
if self._funding_in_progress.get(service_config_id, False):
|
|
862
|
+
raise FundingInProgressError(
|
|
863
|
+
f"Funding already in progress for service {service_config_id}."
|
|
864
|
+
)
|
|
865
|
+
self._funding_in_progress[service_config_id] = True
|
|
866
|
+
|
|
867
|
+
try:
|
|
868
|
+
for chain_str, addresses in amounts.items():
|
|
869
|
+
for address in addresses:
|
|
870
|
+
if (
|
|
871
|
+
address not in service.agent_addresses
|
|
872
|
+
and address
|
|
873
|
+
!= service.chain_configs[chain_str].chain_data.multisig
|
|
874
|
+
):
|
|
875
|
+
raise ValueError(
|
|
876
|
+
f"Failed to fund from Master Safe: Address {address} is not an agent EOA or service Safe for service {service.service_config_id}."
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
self.fund_chain_amounts(amounts)
|
|
880
|
+
self._funding_requests_cooldown_until[service_config_id] = (
|
|
881
|
+
time() + self.funding_requests_cooldown_seconds
|
|
882
|
+
)
|
|
883
|
+
finally:
|
|
884
|
+
with self._lock:
|
|
885
|
+
self._funding_in_progress[service_config_id] = False
|
|
886
|
+
|
|
887
|
+
async def funding_job(
|
|
888
|
+
self,
|
|
889
|
+
service_manager: "ServiceManager",
|
|
890
|
+
loop: t.Optional[asyncio.AbstractEventLoop] = None,
|
|
891
|
+
) -> None:
|
|
892
|
+
"""Start a background funding job."""
|
|
893
|
+
loop = loop or asyncio.get_event_loop()
|
|
894
|
+
with ThreadPoolExecutor() as executor:
|
|
895
|
+
last_claim = 0.0
|
|
896
|
+
last_master_eoa_funding = 0.0
|
|
897
|
+
while True:
|
|
898
|
+
# try claiming rewards every hour
|
|
899
|
+
if last_claim + 3600 < time():
|
|
900
|
+
try:
|
|
901
|
+
await loop.run_in_executor(
|
|
902
|
+
executor,
|
|
903
|
+
service_manager.claim_all_on_chain_from_safe,
|
|
904
|
+
)
|
|
905
|
+
except Exception: # pylint: disable=broad-except
|
|
906
|
+
self.logger.info(
|
|
907
|
+
f"Error occured while claiming rewards\n{traceback.format_exc()}"
|
|
908
|
+
)
|
|
909
|
+
last_claim = time()
|
|
910
|
+
|
|
911
|
+
# fund Master EOA every hour
|
|
912
|
+
if last_master_eoa_funding + 3600 < time():
|
|
913
|
+
try:
|
|
914
|
+
await loop.run_in_executor(
|
|
915
|
+
executor,
|
|
916
|
+
self.fund_master_eoa,
|
|
917
|
+
)
|
|
918
|
+
except Exception: # pylint: disable=broad-except
|
|
919
|
+
self.logger.info(
|
|
920
|
+
f"Error occured while funding Master EOA\n{traceback.format_exc()}"
|
|
921
|
+
)
|
|
922
|
+
last_master_eoa_funding = time()
|
|
923
|
+
|
|
924
|
+
await asyncio.sleep(60)
|
|
925
|
+
|
|
926
|
+
# TODO Below this line - pending finish funding Job for Master EOA
|
|
927
|
+
# TODO cache _resolve methods to avoid loading multiple times file.
|
|
928
|
+
# TODO refactor: move protocol_ methods to protocol class, refactor to accomodate arbitrary owner and operators,
|
|
929
|
+
# refactor to manage multiple chains with different master safes, etc.
|