iwa 0.0.2__py3-none-any.whl → 0.0.11__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.
- iwa/core/chain/interface.py +51 -30
- iwa/core/chain/models.py +9 -15
- iwa/core/contracts/contract.py +8 -2
- iwa/core/pricing.py +10 -8
- iwa/core/services/safe.py +13 -8
- iwa/core/services/transaction.py +211 -7
- iwa/core/utils.py +22 -0
- iwa/core/wallet.py +2 -1
- iwa/plugins/gnosis/safe.py +4 -3
- iwa/plugins/gnosis/tests/test_safe.py +9 -7
- iwa/plugins/olas/contracts/abis/service_registry_token_utility.json +926 -0
- iwa/plugins/olas/contracts/service.py +54 -4
- iwa/plugins/olas/contracts/staking.py +2 -3
- iwa/plugins/olas/plugin.py +14 -7
- iwa/plugins/olas/service_manager/lifecycle.py +382 -85
- iwa/plugins/olas/service_manager/mech.py +1 -1
- iwa/plugins/olas/service_manager/staking.py +229 -82
- iwa/plugins/olas/tests/test_olas_contracts.py +6 -2
- iwa/plugins/olas/tests/test_plugin.py +6 -1
- iwa/plugins/olas/tests/test_plugin_full.py +12 -7
- iwa/plugins/olas/tests/test_service_lifecycle.py +1 -4
- iwa/plugins/olas/tests/test_service_manager.py +59 -89
- iwa/plugins/olas/tests/test_service_manager_errors.py +1 -2
- iwa/plugins/olas/tests/test_service_manager_flows.py +5 -15
- iwa/plugins/olas/tests/test_service_manager_validation.py +16 -15
- iwa/tools/list_contracts.py +2 -2
- iwa/web/dependencies.py +1 -3
- iwa/web/routers/accounts.py +1 -2
- iwa/web/routers/olas/admin.py +1 -3
- iwa/web/routers/olas/funding.py +1 -3
- iwa/web/routers/olas/general.py +1 -3
- iwa/web/routers/olas/services.py +53 -21
- iwa/web/routers/olas/staking.py +27 -24
- iwa/web/routers/swap.py +1 -2
- iwa/web/routers/transactions.py +0 -2
- iwa/web/server.py +8 -6
- iwa/web/static/app.js +22 -0
- iwa/web/tests/test_web_endpoints.py +1 -1
- iwa/web/tests/test_web_olas.py +1 -1
- {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/METADATA +1 -1
- {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/RECORD +58 -56
- tests/test_chain.py +12 -7
- tests/test_chain_interface_coverage.py +3 -2
- tests/test_contract.py +165 -0
- tests/test_keys.py +2 -1
- tests/test_legacy_wallet.py +11 -0
- tests/test_pricing.py +32 -15
- tests/test_safe_coverage.py +3 -3
- tests/test_safe_service.py +3 -6
- tests/test_service_transaction.py +8 -3
- tests/test_staking_router.py +6 -3
- tests/test_transaction_service.py +4 -0
- tools/create_and_stake_service.py +103 -0
- tools/verify_drain.py +1 -4
- {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/WHEEL +0 -0
- {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/entry_points.txt +0 -0
- {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/licenses/LICENSE +0 -0
- {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/top_level.txt +0 -0
|
@@ -100,11 +100,6 @@ def mock_chain_interfaces():
|
|
|
100
100
|
yield mock
|
|
101
101
|
|
|
102
102
|
|
|
103
|
-
@pytest.fixture
|
|
104
|
-
def mock_erc20_contract():
|
|
105
|
-
"""Mock ERC20 contract fixture."""
|
|
106
|
-
with patch("iwa.plugins.olas.service_manager.staking.ERC20Contract") as mock:
|
|
107
|
-
yield mock
|
|
108
103
|
|
|
109
104
|
|
|
110
105
|
@pytest.fixture
|
|
@@ -114,7 +109,7 @@ def service_manager(
|
|
|
114
109
|
mock_registry,
|
|
115
110
|
mock_manager_contract,
|
|
116
111
|
mock_chain_interfaces,
|
|
117
|
-
|
|
112
|
+
|
|
118
113
|
mock_olas_config,
|
|
119
114
|
mock_service,
|
|
120
115
|
):
|
|
@@ -174,12 +169,11 @@ def test_create_no_event(service_manager, mock_wallet):
|
|
|
174
169
|
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
175
170
|
service_manager.registry.extract_events.return_value = []
|
|
176
171
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
assert res is None
|
|
172
|
+
res = service_manager.create(
|
|
173
|
+
token_address_or_tag="0x1111111111111111111111111111111111111111"
|
|
174
|
+
)
|
|
175
|
+
# create() finds no ID, logs error, returns None for service_id.
|
|
176
|
+
assert res is None
|
|
183
177
|
|
|
184
178
|
|
|
185
179
|
def test_activate_registration_success(service_manager, mock_wallet):
|
|
@@ -193,7 +187,7 @@ def test_activate_registration_success(service_manager, mock_wallet):
|
|
|
193
187
|
|
|
194
188
|
# Mock balance/allowance for the new check
|
|
195
189
|
mock_wallet.balance_service = MagicMock()
|
|
196
|
-
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18
|
|
190
|
+
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 100 * 10**18
|
|
197
191
|
mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20
|
|
198
192
|
|
|
199
193
|
assert service_manager.activate_registration() is True
|
|
@@ -287,18 +281,15 @@ def test_stake_success(service_manager, mock_wallet):
|
|
|
287
281
|
"security_deposit": 50000000000000000000,
|
|
288
282
|
}
|
|
289
283
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
294
|
-
staking_contract.extract_events.return_value = [{"name": "ServiceStaked"}]
|
|
295
|
-
staking_contract.get_staking_state.return_value = StakingState.STAKED
|
|
284
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
285
|
+
staking_contract.extract_events.return_value = [{"name": "ServiceStaked"}]
|
|
286
|
+
staking_contract.get_staking_state.return_value = StakingState.STAKED
|
|
296
287
|
|
|
297
|
-
|
|
298
|
-
|
|
288
|
+
# We need to make sure prepare_approve_tx is mocked ON THE REGISTRY INSTANCE
|
|
289
|
+
service_manager.registry.prepare_approve_tx.return_value = {"to": "0xApprove"}
|
|
299
290
|
|
|
300
|
-
|
|
301
|
-
|
|
291
|
+
assert service_manager.stake(staking_contract) is True
|
|
292
|
+
assert service_manager.service.staking_contract_address == TEST_STAKING_ADDR
|
|
302
293
|
|
|
303
294
|
|
|
304
295
|
def test_unstake_success(service_manager, mock_wallet):
|
|
@@ -370,60 +361,15 @@ def test_register_agent_fund_fails(service_manager, mock_wallet):
|
|
|
370
361
|
|
|
371
362
|
def test_spin_up_from_pre_registration_success(service_manager, mock_wallet):
|
|
372
363
|
"""Test full spin_up path from PRE_REGISTRATION to DEPLOYED."""
|
|
373
|
-
#
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}, # spin_up initial
|
|
380
|
-
{
|
|
381
|
-
"state": ServiceState.PRE_REGISTRATION,
|
|
382
|
-
"security_deposit": 50000000000000000000,
|
|
383
|
-
}, # activate_registration check
|
|
384
|
-
{
|
|
385
|
-
"state": ServiceState.PRE_REGISTRATION,
|
|
386
|
-
"security_deposit": 50000000000000000000,
|
|
387
|
-
}, # activate_registration internal (get security deposit)
|
|
388
|
-
{
|
|
389
|
-
"state": ServiceState.ACTIVE_REGISTRATION,
|
|
390
|
-
"security_deposit": 50000000000000000000,
|
|
391
|
-
}, # spin_up verify after activate
|
|
392
|
-
{
|
|
393
|
-
"state": ServiceState.ACTIVE_REGISTRATION,
|
|
394
|
-
"security_deposit": 50000000000000000000,
|
|
395
|
-
}, # register_agent check
|
|
396
|
-
{
|
|
397
|
-
"state": ServiceState.ACTIVE_REGISTRATION,
|
|
398
|
-
"security_deposit": 50000000000000000000,
|
|
399
|
-
}, # register_agent internal
|
|
400
|
-
{
|
|
401
|
-
"state": ServiceState.FINISHED_REGISTRATION,
|
|
402
|
-
"security_deposit": 50000000000000000000,
|
|
403
|
-
}, # spin_up verify after register
|
|
404
|
-
{
|
|
405
|
-
"state": ServiceState.FINISHED_REGISTRATION,
|
|
406
|
-
"security_deposit": 50000000000000000000,
|
|
407
|
-
}, # deploy check
|
|
408
|
-
{
|
|
409
|
-
"state": ServiceState.DEPLOYED,
|
|
410
|
-
"security_deposit": 50000000000000000000,
|
|
411
|
-
}, # spin_up verify after deploy
|
|
412
|
-
{
|
|
413
|
-
"state": ServiceState.DEPLOYED,
|
|
414
|
-
"security_deposit": 50000000000000000000,
|
|
415
|
-
}, # final verification
|
|
364
|
+
# Use a stateful mock that progresses based on extract_events calls
|
|
365
|
+
states = [
|
|
366
|
+
ServiceState.PRE_REGISTRATION, # Before any TX
|
|
367
|
+
ServiceState.ACTIVE_REGISTRATION, # After 1st TX (activate)
|
|
368
|
+
ServiceState.FINISHED_REGISTRATION, # After 2nd TX (register)
|
|
369
|
+
ServiceState.DEPLOYED, # After 3rd TX (deploy)
|
|
416
370
|
]
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
mock_wallet.send.return_value = "0xMockTxHash" # wallet.send returns tx_hash
|
|
420
|
-
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
421
|
-
|
|
422
|
-
# Mock balance/allowance for activate_registration internal call
|
|
423
|
-
mock_wallet.balance_service = MagicMock()
|
|
424
|
-
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18
|
|
425
|
-
mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20
|
|
426
|
-
service_manager.registry.extract_events.side_effect = [
|
|
371
|
+
tx_count = [0] # Track completed transactions
|
|
372
|
+
events_side_effects = [
|
|
427
373
|
[{"name": "ActivateRegistration"}],
|
|
428
374
|
[{"name": "RegisterInstance"}],
|
|
429
375
|
[
|
|
@@ -431,6 +377,32 @@ def test_spin_up_from_pre_registration_success(service_manager, mock_wallet):
|
|
|
431
377
|
{"name": "CreateMultisigWithAgents", "args": {"multisig": TEST_MULTISIG_ADDR}},
|
|
432
378
|
],
|
|
433
379
|
]
|
|
380
|
+
event_idx = [0]
|
|
381
|
+
|
|
382
|
+
def dynamic_state(*args, **kwargs):
|
|
383
|
+
"""Return state based on completed transactions."""
|
|
384
|
+
state = states[min(tx_count[0], len(states) - 1)]
|
|
385
|
+
return {"state": state, "security_deposit": 50000000000000000000}
|
|
386
|
+
|
|
387
|
+
def extract_and_progress(*args, **kwargs):
|
|
388
|
+
"""Return events and advance transaction counter."""
|
|
389
|
+
if event_idx[0] < len(events_side_effects):
|
|
390
|
+
events = events_side_effects[event_idx[0]]
|
|
391
|
+
event_idx[0] += 1
|
|
392
|
+
tx_count[0] += 1
|
|
393
|
+
return events
|
|
394
|
+
return []
|
|
395
|
+
|
|
396
|
+
service_manager.registry.get_service.side_effect = dynamic_state
|
|
397
|
+
service_manager.registry.extract_events.side_effect = extract_and_progress
|
|
398
|
+
|
|
399
|
+
mock_wallet.send.return_value = "0xMockTxHash"
|
|
400
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
401
|
+
|
|
402
|
+
# Mock balance/allowance for activate_registration internal call
|
|
403
|
+
mock_wallet.balance_service = MagicMock()
|
|
404
|
+
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 100 * 10**18
|
|
405
|
+
mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20
|
|
434
406
|
|
|
435
407
|
assert service_manager.spin_up() is True
|
|
436
408
|
|
|
@@ -557,14 +529,12 @@ def test_spin_up_with_staking(service_manager, mock_wallet):
|
|
|
557
529
|
"required_agent_bond": 50000000000000000000, # 50 OLAS
|
|
558
530
|
}
|
|
559
531
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
staking_contract.get_staking_state.return_value = StakingState.STAKED
|
|
565
|
-
service_manager.registry.prepare_approve_tx.return_value = {"to": "0xApprove"}
|
|
532
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
533
|
+
staking_contract.extract_events.return_value = [{"name": "ServiceStaked"}]
|
|
534
|
+
staking_contract.get_staking_state.return_value = StakingState.STAKED
|
|
535
|
+
service_manager.registry.prepare_approve_tx.return_value = {"to": "0xApprove"}
|
|
566
536
|
|
|
567
|
-
|
|
537
|
+
assert service_manager.spin_up(staking_contract=staking_contract) is True
|
|
568
538
|
|
|
569
539
|
|
|
570
540
|
def test_spin_up_activate_fails(service_manager, mock_wallet):
|
|
@@ -587,7 +557,7 @@ def test_spin_up_activate_fails(service_manager, mock_wallet):
|
|
|
587
557
|
|
|
588
558
|
# Mock balance/allowance for activate_registration behavior
|
|
589
559
|
mock_wallet.balance_service = MagicMock()
|
|
590
|
-
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18
|
|
560
|
+
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 100 * 10**18
|
|
591
561
|
mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20
|
|
592
562
|
|
|
593
563
|
mock_wallet.sign_and_send_transaction.return_value = (False, {})
|
|
@@ -931,7 +901,7 @@ def test_activate_registration_token_service_sends_security_deposit_as_value(
|
|
|
931
901
|
|
|
932
902
|
# Mock balance check to pass
|
|
933
903
|
mock_wallet.balance_service = MagicMock()
|
|
934
|
-
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18 # Plenty of balance
|
|
904
|
+
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 100 * 10**18 # Plenty of balance
|
|
935
905
|
|
|
936
906
|
# Mock allowance to pass check (return an int)
|
|
937
907
|
mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20 # Plenty of allowance
|
|
@@ -964,7 +934,7 @@ def test_activate_registration_native_service_sends_security_deposit_as_value(
|
|
|
964
934
|
|
|
965
935
|
# Mock balance/allowance
|
|
966
936
|
mock_wallet.balance_service = MagicMock()
|
|
967
|
-
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18
|
|
937
|
+
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 100 * 10**18
|
|
968
938
|
mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20
|
|
969
939
|
|
|
970
940
|
service_manager.activate_registration()
|
|
@@ -996,7 +966,7 @@ def test_activate_registration_uses_master_account_as_from_address(service_manag
|
|
|
996
966
|
# Mock balance/allowance
|
|
997
967
|
mock_wallet.balance_service = MagicMock()
|
|
998
968
|
mock_wallet.transfer_service = MagicMock()
|
|
999
|
-
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18
|
|
969
|
+
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 100 * 10**18
|
|
1000
970
|
mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20
|
|
1001
971
|
|
|
1002
972
|
service_manager.activate_registration()
|
|
@@ -1028,7 +998,7 @@ def test_activate_registration_uses_master_account_as_signer(service_manager, mo
|
|
|
1028
998
|
# Mock balance/allowance
|
|
1029
999
|
mock_wallet.balance_service = MagicMock()
|
|
1030
1000
|
mock_wallet.transfer_service = MagicMock()
|
|
1031
|
-
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18
|
|
1001
|
+
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 100 * 10**18
|
|
1032
1002
|
mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20
|
|
1033
1003
|
|
|
1034
1004
|
service_manager.activate_registration()
|
|
@@ -1055,7 +1025,7 @@ def test_activate_registration_token_service_approves_token_utility(service_mana
|
|
|
1055
1025
|
|
|
1056
1026
|
# Mock low allowance to trigger approval
|
|
1057
1027
|
mock_wallet.balance_service = MagicMock()
|
|
1058
|
-
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18
|
|
1028
|
+
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 100 * 10**18
|
|
1059
1029
|
mock_wallet.transfer_service.get_erc20_allowance.return_value = 0 # Low allowance
|
|
1060
1030
|
mock_wallet.transfer_service.approve_erc20.return_value = True
|
|
1061
1031
|
|
|
@@ -121,8 +121,7 @@ def test_service_manager_lifecycle_failures(mock_wallet):
|
|
|
121
121
|
"num_agent_instances": 1,
|
|
122
122
|
"required_agent_bond": 50000000000000000000,
|
|
123
123
|
}
|
|
124
|
-
|
|
125
|
-
assert manager.stake(mock_staking) is False
|
|
124
|
+
assert manager.stake(mock_staking) is False
|
|
126
125
|
|
|
127
126
|
# unstake failures
|
|
128
127
|
# Service not staked
|
|
@@ -45,9 +45,8 @@ def mock_config():
|
|
|
45
45
|
@patch("iwa.plugins.olas.service_manager.base.Config")
|
|
46
46
|
@patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract")
|
|
47
47
|
@patch("iwa.plugins.olas.service_manager.base.ServiceManagerContract")
|
|
48
|
-
@patch("iwa.plugins.olas.service_manager.staking.ERC20Contract")
|
|
49
48
|
def test_create_service_success(
|
|
50
|
-
|
|
49
|
+
mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
|
|
51
50
|
):
|
|
52
51
|
"""Test successful service creation."""
|
|
53
52
|
# Setup Config with new OlasConfig structure
|
|
@@ -85,9 +84,8 @@ def test_create_service_success(
|
|
|
85
84
|
@patch("iwa.plugins.olas.service_manager.base.Config")
|
|
86
85
|
@patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract")
|
|
87
86
|
@patch("iwa.plugins.olas.service_manager.base.ServiceManagerContract")
|
|
88
|
-
@patch("iwa.plugins.olas.service_manager.staking.ERC20Contract")
|
|
89
87
|
def test_create_service_failures(
|
|
90
|
-
|
|
88
|
+
mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
|
|
91
89
|
):
|
|
92
90
|
"""Test service creation failure modes."""
|
|
93
91
|
mock_config_inst = mock_config_cls.return_value
|
|
@@ -141,9 +139,8 @@ def test_create_service_failures(
|
|
|
141
139
|
@patch("iwa.plugins.olas.service_manager.base.Config")
|
|
142
140
|
@patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract")
|
|
143
141
|
@patch("iwa.plugins.olas.service_manager.base.ServiceManagerContract")
|
|
144
|
-
@patch("iwa.plugins.olas.service_manager.staking.ERC20Contract")
|
|
145
142
|
def test_create_service_with_approval(
|
|
146
|
-
|
|
143
|
+
mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
|
|
147
144
|
):
|
|
148
145
|
"""Test service creation with token approval."""
|
|
149
146
|
mock_config_inst = mock_config_cls.return_value
|
|
@@ -404,8 +401,7 @@ def test_terminate(mock_sm_contract, mock_registry_contract, mock_config_cls, mo
|
|
|
404
401
|
@patch(
|
|
405
402
|
"iwa.plugins.olas.service_manager.base.ServiceManagerContract"
|
|
406
403
|
) # MUST mock specifically here
|
|
407
|
-
|
|
408
|
-
def test_stake(mock_erc20, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet):
|
|
404
|
+
def test_stake(mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet):
|
|
409
405
|
"""Test service staking."""
|
|
410
406
|
# Setup mock service
|
|
411
407
|
mock_service = MagicMock()
|
|
@@ -445,9 +441,6 @@ def test_stake(mock_erc20, mock_sm_contract, mock_registry_contract, mock_config
|
|
|
445
441
|
"required_agent_bond": 50000000000000000000,
|
|
446
442
|
}
|
|
447
443
|
|
|
448
|
-
# Mock ERC20 balance check (100 OLAS = 100e18 wei, enough for 50 min deposit)
|
|
449
|
-
mock_erc20_inst = mock_erc20.return_value
|
|
450
|
-
mock_erc20_inst.balance_of_wei.return_value = 100000000000000000000 # 100 OLAS
|
|
451
444
|
|
|
452
445
|
success = manager.stake(mock_staking)
|
|
453
446
|
assert success is True
|
|
@@ -471,10 +464,7 @@ def test_stake(mock_erc20, mock_sm_contract, mock_registry_contract, mock_config
|
|
|
471
464
|
assert manager.stake(mock_staking) is False
|
|
472
465
|
mock_staking.get_service_ids.return_value = []
|
|
473
466
|
|
|
474
|
-
|
|
475
|
-
mock_erc20_inst.balance_of_wei.return_value = 50
|
|
476
|
-
assert manager.stake(mock_staking) is False
|
|
477
|
-
mock_erc20_inst.balance_of_wei.return_value = 200
|
|
467
|
+
|
|
478
468
|
|
|
479
469
|
# 4. Approve fail
|
|
480
470
|
mock_wallet.sign_and_send_transaction.return_value = (False, {})
|
|
@@ -45,21 +45,22 @@ def test_drain_service_partial_failures(sm, mock_wallet):
|
|
|
45
45
|
# 2. Safe drain failure
|
|
46
46
|
# 3. Agent drain success
|
|
47
47
|
|
|
48
|
-
with patch
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
48
|
+
with patch("time.sleep"): # Avoid real delays in drain operations
|
|
49
|
+
with patch.object(sm, "claim_rewards", return_value=(True, 10**18)):
|
|
50
|
+
# Wallet.drain is called for Safe and Agent
|
|
51
|
+
def mock_drain(from_address_or_tag=None, to_address_or_tag=None, chain_name=None):
|
|
52
|
+
if from_address_or_tag == VALID_ADDR_2: # Safe
|
|
53
|
+
raise Exception("Safe drain failed")
|
|
54
|
+
return {"native": 0.5}
|
|
55
|
+
|
|
56
|
+
mock_wallet.drain.side_effect = mock_drain
|
|
57
|
+
|
|
58
|
+
result = sm.drain_service()
|
|
59
|
+
|
|
60
|
+
assert "safe" not in result
|
|
61
|
+
assert "agent" in result
|
|
62
|
+
assert result["agent"]["native"] == 0.5
|
|
63
|
+
# Verify it continued after Safe failure
|
|
63
64
|
|
|
64
65
|
|
|
65
66
|
def test_unstake_failed_event_extraction(sm):
|
iwa/tools/list_contracts.py
CHANGED
|
@@ -11,8 +11,8 @@ from iwa.core.utils import configure_logger
|
|
|
11
11
|
from iwa.plugins.olas.constants import OLAS_TRADER_STAKING_CONTRACTS
|
|
12
12
|
from iwa.plugins.olas.contracts.staking import StakingContract
|
|
13
13
|
|
|
14
|
-
# Configure logger
|
|
15
|
-
|
|
14
|
+
# Configure logger and silence noisy third-party loggers
|
|
15
|
+
configure_logger()
|
|
16
16
|
logging.getLogger("web3").setLevel(logging.WARNING)
|
|
17
17
|
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
|
18
18
|
|
iwa/web/dependencies.py
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
"""Shared dependencies for Web API routers."""
|
|
2
2
|
|
|
3
|
-
import logging
|
|
4
3
|
import secrets
|
|
5
4
|
from typing import Optional
|
|
6
5
|
|
|
7
6
|
from fastapi import Header, HTTPException, Security
|
|
8
7
|
from fastapi.security import APIKeyHeader
|
|
8
|
+
from loguru import logger
|
|
9
9
|
|
|
10
10
|
from iwa.core.wallet import Wallet
|
|
11
11
|
|
|
12
|
-
logger = logging.getLogger(__name__)
|
|
13
|
-
|
|
14
12
|
# Singleton wallet instance for the web app
|
|
15
13
|
wallet = Wallet()
|
|
16
14
|
|
iwa/web/routers/accounts.py
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
"""Accounts Router for Web API."""
|
|
2
2
|
|
|
3
|
-
import logging
|
|
4
3
|
import time
|
|
5
4
|
|
|
6
5
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
6
|
+
from loguru import logger
|
|
7
7
|
from slowapi import Limiter
|
|
8
8
|
from slowapi.util import get_remote_address
|
|
9
9
|
|
|
10
10
|
from iwa.web.dependencies import verify_auth, wallet
|
|
11
11
|
from iwa.web.models import AccountCreateRequest, SafeCreateRequest
|
|
12
12
|
|
|
13
|
-
logger = logging.getLogger(__name__)
|
|
14
13
|
router = APIRouter(prefix="/api/accounts", tags=["accounts"])
|
|
15
14
|
|
|
16
15
|
# Rate limiter for this router
|
iwa/web/routers/olas/admin.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"""Olas Admin Router."""
|
|
2
2
|
|
|
3
|
-
import logging
|
|
4
|
-
|
|
5
3
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
4
|
+
from loguru import logger
|
|
6
5
|
from slowapi import Limiter
|
|
7
6
|
from slowapi.util import get_remote_address
|
|
8
7
|
|
|
@@ -10,7 +9,6 @@ from iwa.core.models import Config
|
|
|
10
9
|
from iwa.plugins.olas.models import OlasConfig
|
|
11
10
|
from iwa.web.dependencies import verify_auth, wallet
|
|
12
11
|
|
|
13
|
-
logger = logging.getLogger(__name__)
|
|
14
12
|
router = APIRouter(tags=["olas"])
|
|
15
13
|
limiter = Limiter(key_func=get_remote_address)
|
|
16
14
|
|
iwa/web/routers/olas/funding.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"""Olas Funding Router."""
|
|
2
2
|
|
|
3
|
-
import logging
|
|
4
|
-
|
|
5
3
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
4
|
+
from loguru import logger
|
|
6
5
|
from pydantic import BaseModel, Field
|
|
7
6
|
from slowapi import Limiter
|
|
8
7
|
from slowapi.util import get_remote_address
|
|
@@ -11,7 +10,6 @@ from iwa.core.models import Config
|
|
|
11
10
|
from iwa.plugins.olas.models import OlasConfig
|
|
12
11
|
from iwa.web.dependencies import verify_auth, wallet
|
|
13
12
|
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
13
|
router = APIRouter(tags=["olas"])
|
|
16
14
|
limiter = Limiter(key_func=get_remote_address)
|
|
17
15
|
|
iwa/web/routers/olas/general.py
CHANGED
iwa/web/routers/olas/services.py
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
"""Olas Services Router."""
|
|
2
2
|
|
|
3
|
-
import logging
|
|
4
3
|
from typing import Optional
|
|
5
4
|
|
|
6
5
|
from fastapi import APIRouter, Depends, HTTPException
|
|
6
|
+
from loguru import logger
|
|
7
7
|
from pydantic import BaseModel, Field
|
|
8
8
|
|
|
9
9
|
from iwa.core.models import Config
|
|
10
10
|
from iwa.plugins.olas.models import OlasConfig
|
|
11
11
|
from iwa.web.dependencies import verify_auth, wallet
|
|
12
12
|
|
|
13
|
-
logger = logging.getLogger(__name__)
|
|
14
13
|
router = APIRouter(tags=["olas"])
|
|
15
14
|
|
|
16
15
|
|
|
@@ -29,6 +28,50 @@ class CreateServiceRequest(BaseModel):
|
|
|
29
28
|
)
|
|
30
29
|
|
|
31
30
|
|
|
31
|
+
def _determine_bond_amount(req: CreateServiceRequest) -> int:
|
|
32
|
+
"""Determine the bond amount required for the service."""
|
|
33
|
+
from web3 import Web3
|
|
34
|
+
|
|
35
|
+
from iwa.core.contracts.erc20 import ERC20Contract
|
|
36
|
+
from iwa.plugins.olas.contracts.staking import StakingContract
|
|
37
|
+
from iwa.web.dependencies import wallet
|
|
38
|
+
|
|
39
|
+
# Default to 1 wei of the service token if no staking contract specified
|
|
40
|
+
bond_amount = Web3.to_wei(1, "wei")
|
|
41
|
+
|
|
42
|
+
if req.token_address and req.staking_contract:
|
|
43
|
+
# If a contract is specified, we MUST use its requirements
|
|
44
|
+
logger.info(f"Fetching requirements from {req.staking_contract}...")
|
|
45
|
+
staking_contract = StakingContract(req.staking_contract, req.chain)
|
|
46
|
+
reqs = staking_contract.get_requirements()
|
|
47
|
+
bond_amount = reqs["required_agent_bond"]
|
|
48
|
+
min_staking_deposit = reqs["min_staking_deposit"]
|
|
49
|
+
logger.info(f"Required bond amount from contract: {bond_amount} wei")
|
|
50
|
+
logger.info(f"Required min_staking_deposit: {min_staking_deposit} wei")
|
|
51
|
+
|
|
52
|
+
# Validate upfront: total OLAS needed = bond + min_staking_deposit
|
|
53
|
+
if req.stake_on_create:
|
|
54
|
+
total_olas_needed = bond_amount + min_staking_deposit
|
|
55
|
+
logger.info(
|
|
56
|
+
f"Total OLAS needed for create + stake: {total_olas_needed / 1e18:.2f} OLAS"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Check owner balance (master is default owner for new services)
|
|
60
|
+
staking_token = reqs.get("staking_token")
|
|
61
|
+
if staking_token:
|
|
62
|
+
erc20 = ERC20Contract(staking_token, req.chain)
|
|
63
|
+
owner_balance = erc20.balance_of_wei(wallet.master_account.address)
|
|
64
|
+
logger.info(f"Owner OLAS balance: {owner_balance / 1e18:.2f} OLAS")
|
|
65
|
+
|
|
66
|
+
if owner_balance < total_olas_needed:
|
|
67
|
+
raise HTTPException(
|
|
68
|
+
status_code=400,
|
|
69
|
+
detail=f"Insufficient OLAS balance. Need {total_olas_needed / 1e18:.2f} OLAS "
|
|
70
|
+
f"(bond: {bond_amount / 1e18:.2f} + deposit: {min_staking_deposit / 1e18:.2f}), "
|
|
71
|
+
f"but owner has {owner_balance / 1e18:.2f} OLAS",
|
|
72
|
+
)
|
|
73
|
+
return bond_amount
|
|
74
|
+
|
|
32
75
|
@router.post(
|
|
33
76
|
"/create",
|
|
34
77
|
summary="Create Service",
|
|
@@ -37,28 +80,14 @@ class CreateServiceRequest(BaseModel):
|
|
|
37
80
|
def create_service(req: CreateServiceRequest, auth: bool = Depends(verify_auth)):
|
|
38
81
|
"""Create a new Olas service using spin_up for seamless deployment."""
|
|
39
82
|
try:
|
|
40
|
-
from web3 import Web3
|
|
41
83
|
|
|
42
84
|
from iwa.plugins.olas.contracts.staking import StakingContract
|
|
43
85
|
from iwa.plugins.olas.service_manager import ServiceManager
|
|
44
86
|
|
|
45
87
|
manager = ServiceManager(wallet)
|
|
46
88
|
|
|
47
|
-
# Determine bond amount
|
|
48
|
-
bond_amount =
|
|
49
|
-
|
|
50
|
-
staking_contract = None
|
|
51
|
-
if req.token_address:
|
|
52
|
-
if req.staking_contract:
|
|
53
|
-
# If a contract is specified, we MUST use its requirements
|
|
54
|
-
logger.info(f"Fetching requirements from {req.staking_contract}...")
|
|
55
|
-
staking_contract = StakingContract(req.staking_contract, req.chain)
|
|
56
|
-
reqs = staking_contract.get_requirements()
|
|
57
|
-
bond_amount = reqs["required_agent_bond"]
|
|
58
|
-
logger.info(f"Required bond amount from contract: {bond_amount} wei")
|
|
59
|
-
else:
|
|
60
|
-
# Default to 1 wei of the service token if no staking contract specified
|
|
61
|
-
bond_amount = Web3.to_wei(1, "wei")
|
|
89
|
+
# Determine bond amount
|
|
90
|
+
bond_amount = _determine_bond_amount(req)
|
|
62
91
|
|
|
63
92
|
# Step 1: Create the service (PRE_REGISTRATION state)
|
|
64
93
|
logger.info(
|
|
@@ -88,11 +117,14 @@ def create_service(req: CreateServiceRequest, auth: bool = Depends(verify_auth))
|
|
|
88
117
|
|
|
89
118
|
# Step 2: Spin up the service (activate → register → deploy → optionally stake)
|
|
90
119
|
# Only pass staking_contract if user wants to stake on create
|
|
91
|
-
|
|
120
|
+
staking_obj = None
|
|
121
|
+
if req.stake_on_create and req.staking_contract:
|
|
122
|
+
# We need to instantiate the staking contract object for spin_up
|
|
123
|
+
staking_obj = StakingContract(req.staking_contract, req.chain)
|
|
92
124
|
|
|
93
125
|
success = manager.spin_up(
|
|
94
126
|
service_id=service_id,
|
|
95
|
-
staking_contract=
|
|
127
|
+
staking_contract=staking_obj,
|
|
96
128
|
bond_amount_wei=bond_amount,
|
|
97
129
|
)
|
|
98
130
|
|
|
@@ -111,7 +143,7 @@ def create_service(req: CreateServiceRequest, auth: bool = Depends(verify_auth))
|
|
|
111
143
|
"service_key": manager.service.key if manager.service else None,
|
|
112
144
|
"multisig": str(manager.service.multisig_address) if manager.service else None,
|
|
113
145
|
"final_state": final_state,
|
|
114
|
-
"staked": req.stake_on_create and
|
|
146
|
+
"staked": req.stake_on_create and staking_obj is not None,
|
|
115
147
|
}
|
|
116
148
|
|
|
117
149
|
except HTTPException:
|