iwa 0.0.61__py3-none-any.whl → 0.0.64__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.
@@ -285,11 +285,15 @@ class ChainInterface:
285
285
 
286
286
  # -- ChainList enrichment ----------------------------------------------
287
287
 
288
- MAX_RPCS = 10 # Cap total RPCs per chain
288
+ MAX_RPCS = 20 # Cap total RPCs per chain
289
289
 
290
290
  def _enrich_rpcs_from_chainlist(self) -> None:
291
291
  """Add validated public RPCs from ChainList to the rotation pool."""
292
292
  if len(self.chain.rpcs) >= self.MAX_RPCS:
293
+ logger.debug(
294
+ f"{self.chain.name}: skipping ChainList enrichment "
295
+ f"(already have {len(self.chain.rpcs)} RPCs)"
296
+ )
293
297
  return
294
298
 
295
299
  try:
@@ -354,35 +358,45 @@ class ChainInterface:
354
358
 
355
359
  if should_rotate:
356
360
  failed_index = self._current_rpc_index
361
+ failed_rpc = sanitize_rpc_url(self.chain.rpcs[failed_index]) if self.chain.rpcs else "?"
357
362
 
358
363
  # Apply per-RPC backoff so smart rotation skips this RPC.
359
364
  if result["is_quota_exceeded"]:
360
- error_type = "quota exceeded"
361
- self._mark_rpc_backoff(failed_index, self.QUOTA_EXCEEDED_BACKOFF)
365
+ error_type = "QUOTA"
366
+ backoff = self.QUOTA_EXCEEDED_BACKOFF
367
+ self._mark_rpc_backoff(failed_index, backoff)
362
368
  elif result["is_rate_limit"]:
363
- error_type = "rate limit"
364
- self._mark_rpc_backoff(failed_index, self.RATE_LIMIT_BACKOFF)
369
+ error_type = "RATE_LIMIT"
370
+ backoff = self.RATE_LIMIT_BACKOFF
371
+ self._mark_rpc_backoff(failed_index, backoff)
365
372
  # Brief global backoff so other threads don't immediately flood
366
373
  # the same (now backed-off) RPC before rotation takes effect.
367
374
  self._rate_limiter.trigger_backoff(seconds=2.0)
368
375
  else:
369
- error_type = "connection"
370
- self._mark_rpc_backoff(failed_index, self.CONNECTION_ERROR_BACKOFF)
376
+ error_type = "CONNECTION"
377
+ backoff = self.CONNECTION_ERROR_BACKOFF
378
+ self._mark_rpc_backoff(failed_index, backoff)
379
+
380
+ # Count healthy RPCs for visibility
381
+ healthy_count = sum(1 for i in range(len(self.chain.rpcs)) if self._is_rpc_healthy(i))
382
+ total_rpcs = len(self.chain.rpcs) if self.chain.rpcs else 0
371
383
 
372
384
  logger.warning(
373
- f"RPC {error_type} error on {self.chain.name} "
374
- f"(RPC #{failed_index}): {error}"
385
+ f"[{self.chain.name}] RPC #{failed_index} {error_type} → "
386
+ f"backoff {int(backoff)}s ({healthy_count}/{total_rpcs} healthy) | "
387
+ f"{failed_rpc}: {str(error)[:100]}"
375
388
  )
376
389
 
377
390
  if self.rotate_rpc():
378
391
  result["rotated"] = True
379
392
  result["should_retry"] = True
380
- logger.info(f"Rotated to RPC #{self._current_rpc_index} for {self.chain.name}")
393
+ new_rpc = sanitize_rpc_url(self.chain.rpcs[self._current_rpc_index])
394
+ logger.info(f"[{self.chain.name}] Rotated to RPC #{self._current_rpc_index}: {new_rpc}")
381
395
  else:
382
396
  # Rotation skipped (cooldown or single RPC) - still allow retry
383
397
  result["should_retry"] = True
384
- logger.info(
385
- f"RPC rotation skipped, retrying with current RPC #{self._current_rpc_index}"
398
+ logger.debug(
399
+ f"[{self.chain.name}] Rotation skipped (cooldown), retrying RPC #{self._current_rpc_index}"
386
400
  )
387
401
 
388
402
  elif result["is_server_error"]:
@@ -39,6 +39,10 @@ class SafeTransactionExecutor:
39
39
  MAX_GAS_MULTIPLIER = 10 # Hard cap: never exceed 10x original estimate
40
40
  DEFAULT_FALLBACK_GAS = 500_000 # Fallback when estimation fails
41
41
 
42
+ # Fee bumping for "max fee per gas less than block base fee" errors
43
+ FEE_BUMP_PERCENTAGE = 1.30 # 30% bump per retry on fee errors
44
+ MAX_FEE_BUMP_FACTOR = 3.0 # Cap: never bump more than 3x original
45
+
42
46
  def __init__(
43
47
  self,
44
48
  chain_interface: "ChainInterface",
@@ -76,6 +80,7 @@ class SafeTransactionExecutor:
76
80
  last_error = None
77
81
  current_gas = safe_tx.safe_tx_gas
78
82
  base_estimate = current_gas if current_gas > 0 else 0
83
+ fee_bump_factor = 1.0 # Multiplier for EIP-1559 fees, increases on fee errors
79
84
 
80
85
  for attempt in range(self.max_retries + 1):
81
86
  SAFE_TX_STATS["total_attempts"] += 1
@@ -89,6 +94,7 @@ class SafeTransactionExecutor:
89
94
  attempt,
90
95
  current_gas,
91
96
  base_estimate,
97
+ fee_bump_factor,
92
98
  )
93
99
 
94
100
  # Check receipt
@@ -106,7 +112,7 @@ class SafeTransactionExecutor:
106
112
  raise ValueError("Transaction reverted on-chain")
107
113
 
108
114
  except Exception as e:
109
- updated_tx, should_retry = self._handle_execution_failure(
115
+ updated_tx, should_retry, is_fee_error = self._handle_execution_failure(
110
116
  e, safe_address, safe_tx, attempt, operation_name
111
117
  )
112
118
  last_error = e
@@ -115,7 +121,12 @@ class SafeTransactionExecutor:
115
121
 
116
122
  # Update gas/nonce for next loop if needed
117
123
  safe_tx = updated_tx
118
- # If gas error, gas is recalculated in next _execute_attempt via fresh estimation
124
+
125
+ # Bump fee multiplier on fee-related errors (base fee > max fee)
126
+ if is_fee_error and fee_bump_factor < self.MAX_FEE_BUMP_FACTOR:
127
+ fee_bump_factor *= self.FEE_BUMP_PERCENTAGE
128
+ fee_bump_factor = min(fee_bump_factor, self.MAX_FEE_BUMP_FACTOR)
129
+ logger.info(f"[{operation_name}] Fee bump factor increased to {fee_bump_factor:.2f}x")
119
130
 
120
131
  delay = self.DEFAULT_RETRY_DELAY * (2**attempt)
121
132
  time.sleep(delay)
@@ -131,6 +142,7 @@ class SafeTransactionExecutor:
131
142
  attempt,
132
143
  current_gas,
133
144
  base_estimate,
145
+ fee_bump_factor: float = 1.0,
134
146
  ) -> str:
135
147
  """Prepare client, estimate gas, simulate, and execute."""
136
148
  # 1. (Re)Create Safe client
@@ -171,24 +183,11 @@ class SafeTransactionExecutor:
171
183
  signatures_backup = safe_tx.signatures
172
184
 
173
185
  try:
174
- # Always pass the first signer key as the executor (gas payer).
175
- # Note: This method does NOT re-sign the Safe hash if signatures are already present.
176
- # Use EIP-1559 'FAST' to ensure adequate priority fee (fixes Gnosis FeeTooLow)
177
- result = safe_tx.execute(signer_keys[0], eip1559_speed=TxSpeed.FAST)
178
-
179
- # Handle both tuple return (tx_hash, tx) and bytes return
180
- if isinstance(result, tuple):
181
- tx_hash_bytes = result[0]
182
- else:
183
- tx_hash_bytes = result
184
-
185
- # Handle both bytes and hex string returns
186
- if isinstance(tx_hash_bytes, bytes):
187
- return f"0x{tx_hash_bytes.hex()}"
188
- elif isinstance(tx_hash_bytes, str):
189
- return tx_hash_bytes if tx_hash_bytes.startswith("0x") else f"0x{tx_hash_bytes}"
190
- else:
191
- return str(tx_hash_bytes)
186
+ # Execute with appropriate gas pricing
187
+ result = self._execute_with_gas_pricing(
188
+ safe_tx, signer_keys[0], fee_bump_factor, operation_name
189
+ )
190
+ return self._extract_tx_hash(result)
192
191
 
193
192
  finally:
194
193
  # Restore signatures for next attempt if needed
@@ -196,6 +195,39 @@ class SafeTransactionExecutor:
196
195
  if safe_tx.signatures != signatures_backup:
197
196
  safe_tx.signatures = signatures_backup
198
197
 
198
+ def _execute_with_gas_pricing(
199
+ self, safe_tx: SafeTx, signer_key: str, fee_bump_factor: float, operation_name: str
200
+ ):
201
+ """Execute transaction with appropriate gas pricing strategy.
202
+
203
+ If fee_bump_factor > 1.0, calculates a bumped gas price to overcome
204
+ base fee volatility. Otherwise uses EIP-1559 FAST speed.
205
+ """
206
+ if fee_bump_factor > 1.0:
207
+ bumped_gas_price = self._calculate_bumped_gas_price(fee_bump_factor)
208
+ if bumped_gas_price:
209
+ logger.debug(
210
+ f"[{operation_name}] Using bumped gas price: {bumped_gas_price} wei "
211
+ f"(factor: {fee_bump_factor:.2f}x)"
212
+ )
213
+ return safe_tx.execute(signer_key, tx_gas_price=bumped_gas_price)
214
+ # Fallback to FAST if calculation fails
215
+ return safe_tx.execute(signer_key, eip1559_speed=TxSpeed.FAST)
216
+ # Default: use EIP-1559 'FAST' speed
217
+ return safe_tx.execute(signer_key, eip1559_speed=TxSpeed.FAST)
218
+
219
+ def _extract_tx_hash(self, result) -> str:
220
+ """Extract transaction hash from execute() result."""
221
+ # Handle both tuple return (tx_hash, tx) and bytes return
222
+ tx_hash_bytes = result[0] if isinstance(result, tuple) else result
223
+
224
+ # Handle both bytes and hex string returns
225
+ if isinstance(tx_hash_bytes, bytes):
226
+ return f"0x{tx_hash_bytes.hex()}"
227
+ if isinstance(tx_hash_bytes, str):
228
+ return tx_hash_bytes if tx_hash_bytes.startswith("0x") else f"0x{tx_hash_bytes}"
229
+ return str(tx_hash_bytes)
230
+
199
231
  def _check_receipt_status(self, receipt) -> bool:
200
232
  """Check if receipt has successful status."""
201
233
  status = getattr(receipt, "status", None)
@@ -210,14 +242,20 @@ class SafeTransactionExecutor:
210
242
  safe_tx: SafeTx,
211
243
  attempt: int,
212
244
  operation_name: str,
213
- ) -> Tuple[SafeTx, bool]:
214
- """Handle execution failure and determine next steps."""
245
+ ) -> Tuple[SafeTx, bool, bool]:
246
+ """Handle execution failure and determine next steps.
247
+
248
+ Returns:
249
+ Tuple of (updated_safe_tx, should_retry, is_fee_error)
250
+
251
+ """
215
252
  classification = self._classify_error(error)
253
+ is_fee_error = classification["is_fee_error"]
216
254
 
217
255
  if attempt >= self.max_retries:
218
256
  SAFE_TX_STATS["final_failures"] += 1
219
257
  logger.error(f"[{operation_name}] Failed after {attempt + 1} attempts: {error}")
220
- return safe_tx, False
258
+ return safe_tx, False, is_fee_error
221
259
 
222
260
  strategy = "retry"
223
261
  safe = self._recreate_safe_client(safe_address)
@@ -231,13 +269,16 @@ class SafeTransactionExecutor:
231
269
  SAFE_TX_STATS["rpc_rotations"] += 1
232
270
  result = self.chain_interface._handle_rpc_error(error)
233
271
  if not result["should_retry"]:
234
- return safe_tx, False
272
+ return safe_tx, False, is_fee_error
273
+ elif is_fee_error:
274
+ strategy = "fee bump"
275
+ SAFE_TX_STATS["gas_retries"] += 1
235
276
  elif classification["is_gas_error"]:
236
277
  strategy = "gas increase"
237
- # Gas increase handled in next attempt loop
278
+ SAFE_TX_STATS["gas_retries"] += 1
238
279
 
239
280
  self._log_retry(attempt + 1, error, strategy)
240
- return safe_tx, True
281
+ return safe_tx, True, is_fee_error
241
282
 
242
283
  def _estimate_safe_tx_gas(self, safe: Safe, safe_tx: SafeTx, base_estimate: int = 0) -> int:
243
284
  """Estimate gas for a Safe transaction with buffer and hard cap."""
@@ -320,14 +361,57 @@ class SafeTransactionExecutor:
320
361
  error
321
362
  ) or self.chain_interface._is_connection_error(error)
322
363
 
364
+ # Fee-specific errors: base fee jumped above our max fee
365
+ fee_error_signals = [
366
+ "max fee per gas less than block base fee",
367
+ "maxfeepergas",
368
+ "fee too low",
369
+ "underpriced",
370
+ ]
371
+ is_fee_error = any(signal in err_text for signal in fee_error_signals)
372
+
323
373
  return {
324
374
  "is_gas_error": any(x in err_text for x in ["gas", "out of gas", "intrinsic"]),
375
+ "is_fee_error": is_fee_error,
325
376
  "is_nonce_error": self._is_nonce_error(error),
326
377
  "is_rpc_error": is_rpc,
327
378
  "is_revert": "revert" in err_text or "execution reverted" in err_text,
328
379
  "is_signature_error": self._is_signature_error(error),
329
380
  }
330
381
 
382
+ def _calculate_bumped_gas_price(self, bump_factor: float) -> Optional[int]:
383
+ """Calculate a bumped gas price based on current base fee.
384
+
385
+ Uses legacy gas price (not EIP-1559) for compatibility with safe-eth-py's
386
+ tx_gas_price parameter. The bumped price ensures we're above the current
387
+ base fee even if it's volatile.
388
+
389
+ Args:
390
+ bump_factor: Multiplier to apply to the base fee (e.g., 1.3 = 30% bump)
391
+
392
+ Returns:
393
+ Gas price in wei, or None if calculation fails
394
+
395
+ """
396
+ try:
397
+ web3 = self.chain_interface.web3
398
+ latest_block = web3.eth.get_block("latest")
399
+ base_fee = latest_block.get("baseFeePerGas")
400
+
401
+ if base_fee is not None:
402
+ # EIP-1559 chain: calculate bumped max fee
403
+ # base_fee * bump_factor * 1.5 (extra buffer) + priority fee
404
+ priority_fee = max(int(web3.eth.max_priority_fee), 1)
405
+ bumped_fee = int(base_fee * bump_factor * 1.5) + priority_fee
406
+ return bumped_fee
407
+ else:
408
+ # Legacy chain: bump the gas price directly
409
+ gas_price = web3.eth.gas_price
410
+ return int(gas_price * bump_factor)
411
+ except Exception as e:
412
+ logger.debug(f"Failed to calculate bumped gas price: {e}")
413
+ return None
414
+
331
415
  def _decode_revert_reason(self, error: Exception) -> Optional[str]:
332
416
  """Attempt to decode the revert reason."""
333
417
  import re
@@ -2,7 +2,7 @@
2
2
 
3
3
  from typing import Dict, List, Optional
4
4
 
5
- from pydantic import BaseModel, Field, root_validator
5
+ from pydantic import BaseModel, Field
6
6
 
7
7
  from iwa.core.models import EthereumAddress
8
8
 
@@ -15,37 +15,20 @@ class Service(BaseModel):
15
15
  service_id: int # Unique per chain
16
16
  agent_ids: List[int] = Field(default_factory=list) # List of agent type IDs
17
17
 
18
- # New explicit owner fields
18
+ # Owner fields:
19
+ # - service_owner_eoa_address: EOA that owns the service (or signs for the multisig)
20
+ # - service_owner_multisig_address: Safe multisig owner (optional, if service is owned by a Safe)
19
21
  service_owner_eoa_address: Optional[EthereumAddress] = None
20
22
  service_owner_multisig_address: Optional[EthereumAddress] = None
21
23
 
22
- # Deprecated fields (kept for migration, removed from physical model via aliasing/validation)
23
- # Actually, we keep it optional but not used, or use migration logic.
24
- # Let's remove it from fields and rely on before validator to map it to eoa.
25
-
26
24
  agent_address: Optional[EthereumAddress] = None
27
25
  multisig_address: Optional[EthereumAddress] = None
28
26
  staking_contract_address: Optional[EthereumAddress] = None
29
27
  token_address: Optional[EthereumAddress] = None
30
28
 
31
- @root_validator(pre=True)
32
- def migrate_owner_fields(cls, values): # noqa: N805
33
- """Migrate legacy service_owner_address to service_owner_eoa_address."""
34
- # Check for legacy 'service_owner_address'
35
- if "service_owner_address" in values and values["service_owner_address"]:
36
- legacy_addr = values["service_owner_address"]
37
-
38
- # If service_owner_eoa_address is missing, use legacy
39
- if "service_owner_eoa_address" not in values or not values["service_owner_eoa_address"]:
40
- values["service_owner_eoa_address"] = legacy_addr
41
-
42
- # Remove legacy field from values so it doesn't cause extra field errors if we removed it from model
43
- # Or if strict.
44
- return values
45
-
46
29
  @property
47
30
  def service_owner_address(self) -> Optional[EthereumAddress]:
48
- """Backward compatibility property: Returns effective owner (Safe if present, else EOA)."""
31
+ """Returns effective owner address (Safe multisig if present, else EOA)."""
49
32
  return self.service_owner_multisig_address or self.service_owner_eoa_address
50
33
 
51
34
  @property
@@ -12,7 +12,9 @@ from iwa.plugins.olas.contracts.staking import StakingContract, StakingState
12
12
  class DrainManagerMixin:
13
13
  """Mixin for draining and service token management."""
14
14
 
15
- def claim_rewards(self, staking_contract: Optional[StakingContract] = None) -> Tuple[bool, int]:
15
+ def claim_rewards( # noqa: C901
16
+ self, staking_contract: Optional[StakingContract] = None
17
+ ) -> Tuple[bool, int]:
16
18
  """Claim staking rewards for the active service.
17
19
 
18
20
  The claimed OLAS tokens will be sent to the service's multisig (Safe).
@@ -51,13 +53,19 @@ class DrainManagerMixin:
51
53
  logger.info("Service not staked, skipping claim")
52
54
  return False, 0
53
55
 
54
- # Check accrued rewards
55
- accrued_rewards = staking_contract.get_accrued_rewards(service_id)
56
- if accrued_rewards == 0:
57
- logger.info("No accrued rewards to claim")
56
+ # Check claimable rewards using calculate_staking_reward for accurate value
57
+ # (get_accrued_rewards returns stored value which may be outdated)
58
+ try:
59
+ claimable_rewards = staking_contract.calculate_staking_reward(service_id)
60
+ except Exception:
61
+ # Fallback to stored value if calculation fails
62
+ claimable_rewards = staking_contract.get_accrued_rewards(service_id)
63
+
64
+ if claimable_rewards == 0:
65
+ logger.info("No rewards to claim")
58
66
  return False, 0
59
67
 
60
- logger.info(f"Claiming {accrued_rewards / 1e18:.4f} OLAS rewards for service {service_id}")
68
+ logger.info(f"Claiming ~{claimable_rewards / 1e18:.4f} OLAS rewards for service {service_id}")
61
69
 
62
70
  # Use service owner which holds the reward rights (not necessarily master)
63
71
  owner_address = self.service.service_owner_address or self.wallet.master_account.address
@@ -83,11 +91,19 @@ class DrainManagerMixin:
83
91
  return False, 0
84
92
 
85
93
  events = staking_contract.extract_events(receipt)
86
- if "RewardClaimed" not in [event["name"] for event in events]:
87
- logger.warning("RewardClaimed event not found, but transaction succeeded")
88
94
 
89
- logger.info(f"Successfully claimed {accrued_rewards / 1e18:.4f} OLAS rewards")
90
- return True, accrued_rewards
95
+ # Extract actual claimed amount from RewardClaimed event
96
+ claimed_amount = claimable_rewards # Default to estimated
97
+ for event in events:
98
+ if event["name"] == "RewardClaimed":
99
+ # RewardClaimed event has 'amount' or 'reward' field
100
+ claimed_amount = event["args"].get("amount", event["args"].get("reward", claimed_amount))
101
+ break
102
+ else:
103
+ logger.warning("RewardClaimed event not found, using estimated amount")
104
+
105
+ logger.info(f"Successfully claimed {claimed_amount / 1e18:.4f} OLAS rewards")
106
+ return True, claimed_amount
91
107
 
92
108
  def withdraw_rewards(self) -> Tuple[bool, float]:
93
109
  """Withdraw OLAS from the service Safe to the configured withdrawal address.
@@ -132,8 +148,14 @@ class DrainManagerMixin:
132
148
  return False, 0
133
149
 
134
150
  olas_amount = olas_balance / 1e18
135
- withdrawal_tag = self.wallet.get_tag_by_address(withdrawal_address) or withdrawal_address
136
- multisig_tag = self.wallet.get_tag_by_address(multisig_address) or multisig_address
151
+ withdrawal_tag = (
152
+ self.wallet.account_service.get_tag_by_address(withdrawal_address)
153
+ or withdrawal_address
154
+ )
155
+ multisig_tag = (
156
+ self.wallet.account_service.get_tag_by_address(multisig_address)
157
+ or multisig_address
158
+ )
137
159
 
138
160
  logger.info(f"Withdrawing {olas_amount:.4f} OLAS from {multisig_tag} to {withdrawal_tag}")
139
161
 
@@ -183,7 +183,7 @@ class LifecycleManagerMixin:
183
183
  service_name=service_name,
184
184
  chain_name=chain_name,
185
185
  agent_id_values=agent_id_values,
186
- service_owner_address=service_owner_account.address,
186
+ service_owner_eoa_address=service_owner_account.address,
187
187
  token_address=token_address,
188
188
  )
189
189
 
@@ -263,7 +263,7 @@ class LifecycleManagerMixin:
263
263
  service_name: Optional[str],
264
264
  chain_name: str,
265
265
  agent_id_values: List[int],
266
- service_owner_address: str,
266
+ service_owner_eoa_address: str,
267
267
  token_address: Optional[str],
268
268
  ) -> None:
269
269
  """Create and save the new Service model."""
@@ -272,7 +272,7 @@ class LifecycleManagerMixin:
272
272
  chain_name=chain_name,
273
273
  service_id=service_id,
274
274
  agent_ids=agent_id_values,
275
- service_owner_address=service_owner_address,
275
+ service_owner_eoa_address=service_owner_eoa_address,
276
276
  token_address=token_address,
277
277
  )
278
278
 
@@ -14,7 +14,7 @@ class TestOlasConfig:
14
14
  chain_name="gnosis",
15
15
  service_id=456,
16
16
  agent_ids=[25],
17
- service_owner_address="0x1234567890123456789012345678901234567890",
17
+ service_owner_eoa_address="0x1234567890123456789012345678901234567890",
18
18
  )
19
19
 
20
20
  config.add_service(service)
@@ -30,7 +30,7 @@ class TestOlasConfig:
30
30
  chain_name="gnosis",
31
31
  service_id=789,
32
32
  agent_ids=[25],
33
- service_owner_address="0x1234567890123456789012345678901234567890",
33
+ service_owner_eoa_address="0x1234567890123456789012345678901234567890",
34
34
  )
35
35
  config.services["gnosis:789"] = service
36
36
 
@@ -53,7 +53,7 @@ class TestOlasConfig:
53
53
  chain_name="ethereum",
54
54
  service_id=200,
55
55
  agent_ids=[25],
56
- service_owner_address="0x1234567890123456789012345678901234567890",
56
+ service_owner_eoa_address="0x1234567890123456789012345678901234567890",
57
57
  )
58
58
  config.services["ethereum:200"] = service
59
59
 
@@ -116,7 +116,7 @@ class TestService:
116
116
  chain_name="gnosis",
117
117
  service_id=123,
118
118
  agent_ids=[25],
119
- service_owner_address="0x1234567890123456789012345678901234567890",
119
+ service_owner_eoa_address="0x1234567890123456789012345678901234567890",
120
120
  )
121
121
 
122
122
  assert service.key == "gnosis:123"
@@ -133,7 +133,7 @@ class TestService:
133
133
  chain_name="gnosis",
134
134
  service_id=456,
135
135
  agent_ids=[25],
136
- service_owner_address="0x1234567890123456789012345678901234567890",
136
+ service_owner_eoa_address="0x1234567890123456789012345678901234567890",
137
137
  staking_contract_address=staking_addr,
138
138
  token_address=token_addr,
139
139
  )
@@ -35,7 +35,7 @@ def mock_olas_config():
35
35
  chain_name="gnosis",
36
36
  agent_address=VALID_ADDR_1,
37
37
  multisig_address=VALID_ADDR_2,
38
- service_owner_address=VALID_ADDR_3,
38
+ service_owner_eoa_address=VALID_ADDR_3,
39
39
  staking_contract_address=VALID_ADDR_1,
40
40
  )
41
41
  config = OlasConfig(services={"gnosis:1": service})
@@ -108,6 +108,7 @@ def test_claim_rewards_no_accrued_rewards(mock_wallet):
108
108
 
109
109
  mock_staking = MagicMock()
110
110
  mock_staking.get_staking_state.return_value = StakingState.STAKED
111
+ mock_staking.calculate_staking_reward.return_value = 0
111
112
  mock_staking.get_accrued_rewards.return_value = 0
112
113
 
113
114
  success, amount = manager.claim_rewards(staking_contract=mock_staking)
@@ -129,9 +130,12 @@ def test_claim_rewards_success(mock_wallet):
129
130
 
130
131
  mock_staking = MagicMock()
131
132
  mock_staking.get_staking_state.return_value = StakingState.STAKED
133
+ mock_staking.calculate_staking_reward.return_value = 10 * 10**18 # 10 OLAS
132
134
  mock_staking.get_accrued_rewards.return_value = 10 * 10**18 # 10 OLAS
133
135
  mock_staking.prepare_claim_tx.return_value = {"data": "0x"}
134
- mock_staking.extract_events.return_value = [{"name": "RewardClaimed"}]
136
+ mock_staking.extract_events.return_value = [
137
+ {"name": "RewardClaimed", "args": {"amount": 10 * 10**18}}
138
+ ]
135
139
 
136
140
  success, amount = manager.claim_rewards(staking_contract=mock_staking)
137
141
 
@@ -152,6 +156,7 @@ def test_claim_rewards_tx_fails(mock_wallet):
152
156
 
153
157
  mock_staking = MagicMock()
154
158
  mock_staking.get_staking_state.return_value = StakingState.STAKED
159
+ mock_staking.calculate_staking_reward.return_value = 10 * 10**18
155
160
  mock_staking.get_accrued_rewards.return_value = 10 * 10**18
156
161
  mock_staking.prepare_claim_tx.return_value = {"data": "0x"}
157
162
  mock_wallet.sign_and_send_transaction.return_value = (False, {})
@@ -201,6 +201,7 @@ def test_sm_claim_rewards_tx_fails(mock_wallet):
201
201
  mock_staking = MagicMock()
202
202
  mock_staking.prepare_claim_tx.return_value = {"to": VALID_ADDR}
203
203
  mock_staking.get_staking_state.return_value = StakingState.STAKED
204
+ mock_staking.calculate_staking_reward.return_value = 100
204
205
  mock_staking.get_accrued_rewards.return_value = 100
205
206
 
206
207
  def get_contract_side_effect(cls, *args, **kwargs):
@@ -43,7 +43,7 @@ def mock_olas_config():
43
43
  chain_name="gnosis",
44
44
  agent_address="0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB",
45
45
  multisig_address="0x40A2aCCbd92BCA938b02010E17A5b8929b49130D",
46
- service_owner_address="0x1111111111111111111111111111111111111111",
46
+ service_owner_eoa_address="0x1111111111111111111111111111111111111111",
47
47
  staking_contract_address="0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB",
48
48
  )
49
49
  return OlasConfig(services={"gnosis:1": service})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iwa
3
- Version: 0.0.61
3
+ Version: 0.0.64
4
4
  Summary: A secure, modular, and plugin-based framework for crypto agents and ops
5
5
  Requires-Python: <4.0,>=3.12
6
6
  Description-Content-Type: text/markdown