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.
Files changed (58) hide show
  1. iwa/core/chain/interface.py +51 -30
  2. iwa/core/chain/models.py +9 -15
  3. iwa/core/contracts/contract.py +8 -2
  4. iwa/core/pricing.py +10 -8
  5. iwa/core/services/safe.py +13 -8
  6. iwa/core/services/transaction.py +211 -7
  7. iwa/core/utils.py +22 -0
  8. iwa/core/wallet.py +2 -1
  9. iwa/plugins/gnosis/safe.py +4 -3
  10. iwa/plugins/gnosis/tests/test_safe.py +9 -7
  11. iwa/plugins/olas/contracts/abis/service_registry_token_utility.json +926 -0
  12. iwa/plugins/olas/contracts/service.py +54 -4
  13. iwa/plugins/olas/contracts/staking.py +2 -3
  14. iwa/plugins/olas/plugin.py +14 -7
  15. iwa/plugins/olas/service_manager/lifecycle.py +382 -85
  16. iwa/plugins/olas/service_manager/mech.py +1 -1
  17. iwa/plugins/olas/service_manager/staking.py +229 -82
  18. iwa/plugins/olas/tests/test_olas_contracts.py +6 -2
  19. iwa/plugins/olas/tests/test_plugin.py +6 -1
  20. iwa/plugins/olas/tests/test_plugin_full.py +12 -7
  21. iwa/plugins/olas/tests/test_service_lifecycle.py +1 -4
  22. iwa/plugins/olas/tests/test_service_manager.py +59 -89
  23. iwa/plugins/olas/tests/test_service_manager_errors.py +1 -2
  24. iwa/plugins/olas/tests/test_service_manager_flows.py +5 -15
  25. iwa/plugins/olas/tests/test_service_manager_validation.py +16 -15
  26. iwa/tools/list_contracts.py +2 -2
  27. iwa/web/dependencies.py +1 -3
  28. iwa/web/routers/accounts.py +1 -2
  29. iwa/web/routers/olas/admin.py +1 -3
  30. iwa/web/routers/olas/funding.py +1 -3
  31. iwa/web/routers/olas/general.py +1 -3
  32. iwa/web/routers/olas/services.py +53 -21
  33. iwa/web/routers/olas/staking.py +27 -24
  34. iwa/web/routers/swap.py +1 -2
  35. iwa/web/routers/transactions.py +0 -2
  36. iwa/web/server.py +8 -6
  37. iwa/web/static/app.js +22 -0
  38. iwa/web/tests/test_web_endpoints.py +1 -1
  39. iwa/web/tests/test_web_olas.py +1 -1
  40. {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/METADATA +1 -1
  41. {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/RECORD +58 -56
  42. tests/test_chain.py +12 -7
  43. tests/test_chain_interface_coverage.py +3 -2
  44. tests/test_contract.py +165 -0
  45. tests/test_keys.py +2 -1
  46. tests/test_legacy_wallet.py +11 -0
  47. tests/test_pricing.py +32 -15
  48. tests/test_safe_coverage.py +3 -3
  49. tests/test_safe_service.py +3 -6
  50. tests/test_service_transaction.py +8 -3
  51. tests/test_staking_router.py +6 -3
  52. tests/test_transaction_service.py +4 -0
  53. tools/create_and_stake_service.py +103 -0
  54. tools/verify_drain.py +1 -4
  55. {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/WHEEL +0 -0
  56. {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/entry_points.txt +0 -0
  57. {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/licenses/LICENSE +0 -0
  58. {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
- mock_erc20_contract,
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
- with patch("iwa.plugins.olas.service_manager.staking.ERC20Contract"): # Mock ERC20
178
- res = service_manager.create(
179
- token_address_or_tag="0x1111111111111111111111111111111111111111"
180
- )
181
- # create() finds no ID, logs error, returns None for service_id.
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
- with patch("iwa.plugins.olas.service_manager.staking.ERC20Contract") as mock_erc20:
291
- mock_erc20.return_value.balance_of_wei.return_value = 100000000000000000000 # 100 OLAS
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
- # We need to make sure prepare_approve_tx is mocked ON THE REGISTRY INSTANCE
298
- service_manager.registry.prepare_approve_tx.return_value = {"to": "0xApprove"}
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
- assert service_manager.stake(staking_contract) is True
301
- assert service_manager.service.staking_contract_address == TEST_STAKING_ADDR
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
- # Mock state transitions - need to match actual calls in spin_up
374
- # The state after activate_registration should be ACTIVE_REGISTRATION
375
- state_sequence = [
376
- {
377
- "state": ServiceState.PRE_REGISTRATION,
378
- "security_deposit": 50000000000000000000,
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
- service_manager.registry.get_service.side_effect = state_sequence
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
- with patch("iwa.plugins.olas.service_manager.staking.ERC20Contract") as mock_erc20:
561
- mock_erc20.return_value.balance_of_wei.return_value = 100000000000000000000 # 100 OLAS
562
- mock_wallet.sign_and_send_transaction.return_value = (True, {})
563
- staking_contract.extract_events.return_value = [{"name": "ServiceStaked"}]
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
- assert service_manager.spin_up(staking_contract=staking_contract) is True
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
- with patch("iwa.plugins.olas.service_manager.staking.ERC20Contract"):
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
- mock_erc20, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
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
- mock_erc20, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
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
- mock_erc20, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
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
- @patch("iwa.plugins.olas.service_manager.staking.ERC20Contract") # For checking balance
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
- # 3. Not enough funds
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.object(sm, "claim_rewards", return_value=(True, 10**18)):
49
- # Wallet.drain is called for Safe and Agent
50
- def mock_drain(from_address_or_tag=None, to_address_or_tag=None, chain_name=None):
51
- if from_address_or_tag == VALID_ADDR_2: # Safe
52
- raise Exception("Safe drain failed")
53
- return {"native": 0.5}
54
-
55
- mock_wallet.drain.side_effect = mock_drain
56
-
57
- result = sm.drain_service()
58
-
59
- assert "safe" not in result
60
- assert "agent" in result
61
- assert result["agent"]["native"] == 0.5
62
- # Verify it continued after Safe failure
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):
@@ -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 to avoid noise during execution
15
- logger = configure_logger()
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
 
@@ -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
@@ -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
 
@@ -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
 
@@ -1,12 +1,10 @@
1
1
  """Olas General Router."""
2
2
 
3
- import logging
4
-
5
3
  from fastapi import APIRouter, Depends
4
+ from loguru import logger
6
5
 
7
6
  from iwa.web.dependencies import verify_auth
8
7
 
9
- logger = logging.getLogger(__name__)
10
8
  router = APIRouter(tags=["olas"])
11
9
 
12
10
 
@@ -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 based on staking contract
48
- bond_amount = 1 # Default for native token (1 wei)
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
- spin_up_staking = staking_contract if req.stake_on_create else None
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=spin_up_staking,
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 spin_up_staking is not None,
146
+ "staked": req.stake_on_create and staking_obj is not None,
115
147
  }
116
148
 
117
149
  except HTTPException: