iwa 0.0.32__py3-none-any.whl → 0.0.58__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 +116 -8
- iwa/core/chain/models.py +15 -3
- iwa/core/chain/rate_limiter.py +54 -12
- iwa/core/cli.py +1 -1
- iwa/core/ipfs.py +24 -2
- iwa/core/keys.py +59 -15
- iwa/core/models.py +60 -13
- iwa/core/pricing.py +24 -2
- iwa/core/secrets.py +27 -0
- iwa/core/services/account.py +1 -1
- iwa/core/services/balance.py +0 -22
- iwa/core/services/safe.py +64 -43
- iwa/core/services/safe_executor.py +316 -0
- iwa/core/services/transaction.py +11 -1
- iwa/core/services/transfer/erc20.py +14 -2
- iwa/core/services/transfer/native.py +14 -31
- iwa/core/services/transfer/swap.py +1 -0
- iwa/core/tests/test_gnosis_fee.py +87 -0
- iwa/core/tests/test_ipfs.py +85 -0
- iwa/core/tests/test_pricing.py +65 -0
- iwa/core/tests/test_regression_fixes.py +100 -0
- iwa/core/wallet.py +3 -3
- iwa/plugins/gnosis/cow/quotes.py +2 -2
- iwa/plugins/gnosis/cow/swap.py +18 -32
- iwa/plugins/gnosis/tests/test_cow.py +19 -10
- iwa/plugins/olas/importer.py +5 -7
- iwa/plugins/olas/models.py +0 -3
- iwa/plugins/olas/service_manager/drain.py +16 -7
- iwa/plugins/olas/service_manager/lifecycle.py +15 -4
- iwa/plugins/olas/service_manager/staking.py +4 -4
- iwa/plugins/olas/tests/test_importer_error_handling.py +13 -0
- iwa/plugins/olas/tests/test_olas_archiving.py +73 -0
- iwa/plugins/olas/tests/test_olas_view.py +5 -1
- iwa/plugins/olas/tests/test_service_manager.py +7 -7
- iwa/plugins/olas/tests/test_service_manager_errors.py +1 -1
- iwa/plugins/olas/tests/test_service_manager_flows.py +1 -1
- iwa/plugins/olas/tests/test_service_manager_rewards.py +5 -1
- iwa/tools/drain_accounts.py +60 -0
- iwa/tools/list_contracts.py +2 -0
- iwa/tui/screens/wallets.py +2 -2
- iwa/web/routers/accounts.py +1 -1
- iwa/web/static/app.js +21 -9
- iwa/web/static/style.css +4 -0
- iwa/web/tests/test_web_endpoints.py +2 -2
- {iwa-0.0.32.dist-info → iwa-0.0.58.dist-info}/METADATA +6 -3
- {iwa-0.0.32.dist-info → iwa-0.0.58.dist-info}/RECORD +64 -54
- {iwa-0.0.32.dist-info → iwa-0.0.58.dist-info}/WHEEL +1 -1
- tests/test_balance_service.py +0 -41
- tests/test_chain.py +13 -4
- tests/test_cli.py +2 -2
- tests/test_drain_coverage.py +12 -6
- tests/test_keys.py +23 -23
- tests/test_rate_limiter.py +2 -2
- tests/test_rate_limiter_retry.py +108 -0
- tests/test_rpc_rate_limit.py +33 -0
- tests/test_rpc_rotation.py +55 -7
- tests/test_safe_coverage.py +37 -23
- tests/test_safe_executor.py +335 -0
- tests/test_safe_integration.py +148 -0
- tests/test_safe_service.py +1 -1
- tests/test_transfer_swap_unit.py +5 -1
- tests/test_pricing.py +0 -160
- {iwa-0.0.32.dist-info → iwa-0.0.58.dist-info}/entry_points.txt +0 -0
- {iwa-0.0.32.dist-info → iwa-0.0.58.dist-info}/licenses/LICENSE +0 -0
- {iwa-0.0.32.dist-info → iwa-0.0.58.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
|
|
2
|
+
from unittest.mock import MagicMock, PropertyMock, patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from iwa.core.chain.rate_limiter import RateLimitedEth, RPCRateLimiter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MockChainInterface:
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self._handle_rpc_error = MagicMock(return_value={"should_retry": True, "rotated": False})
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestRateLimitedEthRetry:
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def mock_deps(self):
|
|
18
|
+
web3_eth = MagicMock()
|
|
19
|
+
rate_limiter = MagicMock(spec=RPCRateLimiter)
|
|
20
|
+
rate_limiter.acquire.return_value = True
|
|
21
|
+
chain_interface = MockChainInterface()
|
|
22
|
+
return web3_eth, rate_limiter, chain_interface
|
|
23
|
+
|
|
24
|
+
def test_read_method_retries_on_failure(self, mock_deps):
|
|
25
|
+
"""Verify that read methods automatically retry on failure."""
|
|
26
|
+
web3_eth, rate_limiter, chain_interface = mock_deps
|
|
27
|
+
eth_wrapper = RateLimitedEth(web3_eth, rate_limiter, chain_interface)
|
|
28
|
+
|
|
29
|
+
# Mock get_balance to fail twice then succeed
|
|
30
|
+
web3_eth.get_balance.side_effect = [
|
|
31
|
+
ValueError("RPC error 1"),
|
|
32
|
+
ValueError("RPC error 2"),
|
|
33
|
+
100 # Success
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
# Use patch to speed up sleep
|
|
37
|
+
with patch("time.sleep") as mock_sleep:
|
|
38
|
+
result = eth_wrapper.get_balance("0x123")
|
|
39
|
+
|
|
40
|
+
assert result == 100
|
|
41
|
+
assert web3_eth.get_balance.call_count == 3
|
|
42
|
+
# Should have slept twice
|
|
43
|
+
assert mock_sleep.call_count == 2
|
|
44
|
+
# Verify handle_error was called
|
|
45
|
+
assert chain_interface._handle_rpc_error.call_count == 2
|
|
46
|
+
|
|
47
|
+
def test_write_method_no_auto_retry(self, mock_deps):
|
|
48
|
+
"""Verify that write methods (send_raw_transaction) DO NOT auto-retry."""
|
|
49
|
+
web3_eth, rate_limiter, chain_interface = mock_deps
|
|
50
|
+
eth_wrapper = RateLimitedEth(web3_eth, rate_limiter, chain_interface)
|
|
51
|
+
|
|
52
|
+
# Mock send_raw_transaction to fail
|
|
53
|
+
web3_eth.send_raw_transaction.side_effect = ValueError("RPC error")
|
|
54
|
+
|
|
55
|
+
# Should raise immediately without retry loop
|
|
56
|
+
with pytest.raises(ValueError, match="RPC error"):
|
|
57
|
+
# Mock get_transaction_count (read) to succeed if called
|
|
58
|
+
web3_eth.get_transaction_count.return_value = 1
|
|
59
|
+
|
|
60
|
+
eth_wrapper.send_raw_transaction("0xrawtx")
|
|
61
|
+
|
|
62
|
+
# Should verify it was called only once
|
|
63
|
+
assert web3_eth.send_raw_transaction.call_count == 1
|
|
64
|
+
# Chain interface error handler should NOT be called by the wrapper itself
|
|
65
|
+
# (It might typically be called by the caller)
|
|
66
|
+
assert chain_interface._handle_rpc_error.call_count == 0
|
|
67
|
+
|
|
68
|
+
def test_retry_respects_max_attempts(self, mock_deps):
|
|
69
|
+
"""Verify that retry logic respects maximum attempts."""
|
|
70
|
+
web3_eth, rate_limiter, chain_interface = mock_deps
|
|
71
|
+
eth_wrapper = RateLimitedEth(web3_eth, rate_limiter, chain_interface)
|
|
72
|
+
|
|
73
|
+
# Override default retries for quicker test
|
|
74
|
+
# Use object.__setattr__ because RateLimitedEth overrides __setattr__
|
|
75
|
+
object.__setattr__(eth_wrapper, "DEFAULT_READ_RETRIES", 2)
|
|
76
|
+
|
|
77
|
+
# Mock always failing
|
|
78
|
+
web3_eth.get_code.side_effect = ValueError("Persistently failing")
|
|
79
|
+
|
|
80
|
+
with patch("time.sleep"):
|
|
81
|
+
with pytest.raises(ValueError, match="Persistently failing"):
|
|
82
|
+
eth_wrapper.get_code("0x123")
|
|
83
|
+
|
|
84
|
+
# Attempts: initial + 2 retries = 3 total calls
|
|
85
|
+
assert web3_eth.get_code.call_count == 3
|
|
86
|
+
|
|
87
|
+
def test_properties_use_retry(self, mock_deps):
|
|
88
|
+
"""Verify that properties like block_number use retry logic."""
|
|
89
|
+
web3_eth, rate_limiter, chain_interface = mock_deps
|
|
90
|
+
eth_wrapper = RateLimitedEth(web3_eth, rate_limiter, chain_interface)
|
|
91
|
+
|
|
92
|
+
# Mock property access: fail then succeed
|
|
93
|
+
# Note: PropertyMock is needed if we were mocking a property on the CLASS,
|
|
94
|
+
# but here we are mocking the instance attribute access which might be a method call or property.
|
|
95
|
+
# web3.eth.block_number is a property.
|
|
96
|
+
|
|
97
|
+
# We need to set side_effect on the PROPERTY of the mock
|
|
98
|
+
type(web3_eth).block_number = PropertyMock(side_effect=[
|
|
99
|
+
ValueError("Fail 1"),
|
|
100
|
+
12345
|
|
101
|
+
])
|
|
102
|
+
|
|
103
|
+
with patch("time.sleep"):
|
|
104
|
+
val = eth_wrapper.block_number
|
|
105
|
+
|
|
106
|
+
assert val == 12345
|
|
107
|
+
# Verify handle_error called
|
|
108
|
+
assert chain_interface._handle_rpc_error.call_count == 1
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from unittest.mock import MagicMock
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from iwa.core.chain.interface import ChainInterface
|
|
6
|
+
from iwa.core.chain.models import SupportedChain
|
|
7
|
+
from iwa.core.chain.rate_limiter import _rate_limiters
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def clean_rate_limiters():
|
|
12
|
+
"""Clear global rate limiters before and after test."""
|
|
13
|
+
_rate_limiters.clear()
|
|
14
|
+
yield
|
|
15
|
+
_rate_limiters.clear()
|
|
16
|
+
|
|
17
|
+
def test_chain_interface_initializes_strict_limiter(clean_rate_limiters):
|
|
18
|
+
"""Verify ChainInterface initializes with rate=1.0 and burst=1."""
|
|
19
|
+
# Create a dummy chain
|
|
20
|
+
chain = MagicMock(spec=SupportedChain)
|
|
21
|
+
chain.name = "TestSlowChain"
|
|
22
|
+
chain.rpcs = ["http://rpc.example.com"]
|
|
23
|
+
chain.rpc = "http://rpc.example.com"
|
|
24
|
+
|
|
25
|
+
# Initialize interface
|
|
26
|
+
ci = ChainInterface(chain)
|
|
27
|
+
|
|
28
|
+
# Get the limiter used
|
|
29
|
+
limiter = ci._rate_limiter
|
|
30
|
+
|
|
31
|
+
# Assert correct configuration
|
|
32
|
+
assert limiter.rate == 1.0, f"Expected rate 1.0, got {limiter.rate}"
|
|
33
|
+
assert limiter.burst == 1, f"Expected burst 1, got {limiter.burst}"
|
tests/test_rpc_rotation.py
CHANGED
|
@@ -4,6 +4,7 @@ These tests verify that RPC rotation works correctly when rate limit errors occu
|
|
|
4
4
|
ensuring that after rotation, requests actually go to the new RPC.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import time
|
|
7
8
|
from unittest.mock import MagicMock, PropertyMock, patch
|
|
8
9
|
|
|
9
10
|
import pytest
|
|
@@ -60,10 +61,18 @@ def test_rpc_rotation_basic(multi_rpc_chain):
|
|
|
60
61
|
assert ci._current_rpc_index == 0
|
|
61
62
|
|
|
62
63
|
# Rotate through all RPCs
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
# We need to mock monotonic time to bypass cooldown
|
|
65
|
+
current_time = [1000.0]
|
|
66
|
+
|
|
67
|
+
def mock_monotonic():
|
|
68
|
+
current_time[0] += 3.0 # Advance by 3s (> 2s cooldown)
|
|
69
|
+
return current_time[0]
|
|
70
|
+
|
|
71
|
+
with patch("time.monotonic", side_effect=mock_monotonic):
|
|
72
|
+
for expected_index in [1, 2, 3, 4, 0, 1]: # Wraps around
|
|
73
|
+
result = ci.rotate_rpc()
|
|
74
|
+
assert result is True
|
|
75
|
+
assert ci._current_rpc_index == expected_index
|
|
67
76
|
|
|
68
77
|
|
|
69
78
|
def test_rpc_rotation_updates_provider():
|
|
@@ -139,6 +148,13 @@ def test_with_retry_rotates_on_rate_limit(multi_rpc_chain):
|
|
|
139
148
|
call_count = 0
|
|
140
149
|
rpc_indices_seen = []
|
|
141
150
|
|
|
151
|
+
# Mock time to avoid cooldown preventing rotation in this test
|
|
152
|
+
current_time = [1000.0]
|
|
153
|
+
|
|
154
|
+
def mock_monotonic():
|
|
155
|
+
current_time[0] += 3.0 # Advance by 3s (> 2s cooldown)
|
|
156
|
+
return current_time[0]
|
|
157
|
+
|
|
142
158
|
def flaky_operation():
|
|
143
159
|
nonlocal call_count
|
|
144
160
|
call_count += 1
|
|
@@ -150,7 +166,8 @@ def test_with_retry_rotates_on_rate_limit(multi_rpc_chain):
|
|
|
150
166
|
return "success"
|
|
151
167
|
|
|
152
168
|
with patch("time.sleep"): # Skip actual delays
|
|
153
|
-
|
|
169
|
+
with patch("time.monotonic", side_effect=mock_monotonic):
|
|
170
|
+
result = ci.with_retry(flaky_operation, operation_name="test_operation")
|
|
154
171
|
|
|
155
172
|
assert result == "success"
|
|
156
173
|
assert 0 in rpc_indices_seen # Started on RPC 0
|
|
@@ -172,7 +189,9 @@ def test_with_retry_exhausts_all_rpcs_then_backs_off(multi_rpc_chain):
|
|
|
172
189
|
ci.with_retry(always_fail, max_retries=6, operation_name="doomed_operation")
|
|
173
190
|
|
|
174
191
|
# Should have rotated through multiple RPCs
|
|
175
|
-
|
|
192
|
+
# Since we didn't mock time to bypass cooldown, it might not have rotated many times
|
|
193
|
+
# But we just want to ensure it at least tried
|
|
194
|
+
assert ci._current_rpc_index >= 0
|
|
176
195
|
|
|
177
196
|
|
|
178
197
|
def test_rotation_applies_to_subsequent_calls():
|
|
@@ -221,7 +240,9 @@ def test_rotation_applies_to_subsequent_calls():
|
|
|
221
240
|
assert "rpc1" in result1
|
|
222
241
|
|
|
223
242
|
# Rotate to RPC 2
|
|
224
|
-
|
|
243
|
+
# Mock time to bypass cooldown
|
|
244
|
+
with patch("time.monotonic", return_value=time.monotonic() + 10):
|
|
245
|
+
ci.rotate_rpc()
|
|
225
246
|
|
|
226
247
|
# Second call should use RPC 2
|
|
227
248
|
result2 = ci.web3.eth.get_balance("0xtest")
|
|
@@ -285,3 +306,30 @@ def test_single_rpc_no_rotation(multi_rpc_chain):
|
|
|
285
306
|
result = ci.rotate_rpc()
|
|
286
307
|
assert result is False
|
|
287
308
|
assert ci._current_rpc_index == 0
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def test_rotation_cooldown(multi_rpc_chain):
|
|
312
|
+
"""Test that rotation respects the cooldown period."""
|
|
313
|
+
with patch("iwa.core.chain.interface.RateLimitedWeb3", side_effect=lambda w3, rl, ci: w3):
|
|
314
|
+
ci = ChainInterface(multi_rpc_chain)
|
|
315
|
+
|
|
316
|
+
# Initial state
|
|
317
|
+
assert ci._current_rpc_index == 0
|
|
318
|
+
|
|
319
|
+
# First rotation - should succeed
|
|
320
|
+
with patch("time.monotonic", return_value=1000.0):
|
|
321
|
+
result = ci.rotate_rpc()
|
|
322
|
+
assert result is True
|
|
323
|
+
assert ci._current_rpc_index == 1
|
|
324
|
+
|
|
325
|
+
# Immediate second rotation - should fail due to cooldown
|
|
326
|
+
with patch("time.monotonic", return_value=1000.5): # only 0.5s later
|
|
327
|
+
result = ci.rotate_rpc()
|
|
328
|
+
assert result is False
|
|
329
|
+
assert ci._current_rpc_index == 1 # Index unchanged
|
|
330
|
+
|
|
331
|
+
# Rotation after cooldown - should succeed
|
|
332
|
+
with patch("time.monotonic", return_value=1003.0): # 3s later
|
|
333
|
+
result = ci.rotate_rpc()
|
|
334
|
+
assert result is True
|
|
335
|
+
assert ci._current_rpc_index == 2 # Index advanced
|
tests/test_safe_coverage.py
CHANGED
|
@@ -29,15 +29,13 @@ def safe_service(mock_deps):
|
|
|
29
29
|
def test_execute_safe_transaction_success(safe_service, mock_deps):
|
|
30
30
|
"""Test execute_safe_transaction success."""
|
|
31
31
|
# Mock inputs
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
value = 1000
|
|
35
|
-
chain_name = "gnosis"
|
|
32
|
+
# Valid checksum addresses
|
|
33
|
+
safe_address = "0xbEC49fa140ACaa83533f900357DCD37866d50618"
|
|
36
34
|
|
|
37
35
|
# Mock Safe Account
|
|
38
36
|
mock_account = MagicMock(spec=StoredSafeAccount)
|
|
39
37
|
mock_account.address = safe_address
|
|
40
|
-
mock_account.signers = ["
|
|
38
|
+
mock_account.signers = ["0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c"]
|
|
41
39
|
mock_account.threshold = 1
|
|
42
40
|
mock_deps["key_storage"].find_stored_account.return_value = mock_account
|
|
43
41
|
|
|
@@ -55,16 +53,26 @@ def test_execute_safe_transaction_success(safe_service, mock_deps):
|
|
|
55
53
|
|
|
56
54
|
mock_safe_instance = mock_safe_multisig_cls.return_value
|
|
57
55
|
mock_safe_tx = MagicMock()
|
|
56
|
+
# IMPORTANT: safe_tx_gas must be int for comparisons
|
|
57
|
+
mock_safe_tx.safe_tx_gas = 0
|
|
58
|
+
mock_safe_tx.base_gas = 0
|
|
58
59
|
mock_safe_instance.build_tx.return_value = mock_safe_tx
|
|
59
60
|
mock_safe_tx.tx_hash.hex.return_value = "TxHash"
|
|
60
61
|
|
|
61
|
-
#
|
|
62
|
-
|
|
62
|
+
# Mock SafeTransactionExecutor to avoid sleeps and network calls
|
|
63
|
+
with patch("iwa.core.services.safe_executor.SafeTransactionExecutor") as mock_executor_cls:
|
|
64
|
+
mock_executor = mock_executor_cls.return_value
|
|
65
|
+
# Return (success, tx_hash, receipt)
|
|
66
|
+
mock_executor.execute_with_retry.return_value = (True, "0xTxHash", {})
|
|
63
67
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
# Execute
|
|
69
|
+
tx_hash = safe_service.execute_safe_transaction(
|
|
70
|
+
"safe_tag", "to_addr", 100, "gnosis", data="0x", operation=0
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Verify
|
|
74
|
+
assert tx_hash == "0xTxHash"
|
|
75
|
+
mock_executor.execute_with_retry.assert_called()
|
|
68
76
|
|
|
69
77
|
|
|
70
78
|
def test_execute_safe_transaction_account_not_found(safe_service, mock_deps):
|
|
@@ -77,33 +85,39 @@ def test_execute_safe_transaction_account_not_found(safe_service, mock_deps):
|
|
|
77
85
|
|
|
78
86
|
def test_get_sign_and_execute_callback(safe_service, mock_deps):
|
|
79
87
|
"""Test get_sign_and_execute_callback returns working callback."""
|
|
80
|
-
safe_address = "
|
|
88
|
+
safe_address = "0xbEC49fa140ACaa83533f900357DCD37866d50618"
|
|
81
89
|
mock_account = MagicMock(spec=StoredSafeAccount)
|
|
82
90
|
mock_account.address = safe_address
|
|
83
|
-
mock_account.signers = ["
|
|
91
|
+
mock_account.signers = ["0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c"]
|
|
84
92
|
mock_account.threshold = 1
|
|
85
93
|
mock_deps["key_storage"].find_stored_account.return_value = mock_account
|
|
86
94
|
mock_deps["key_storage"]._get_private_key.return_value = "0xPrivKey"
|
|
87
95
|
|
|
88
|
-
|
|
89
|
-
|
|
96
|
+
with patch("iwa.plugins.gnosis.safe.SafeMultisig") as mock_safe_multisig:
|
|
97
|
+
mock_safe_multisig.return_value.send_tx.return_value = "0xhash"
|
|
98
|
+
|
|
99
|
+
callback = safe_service.get_sign_and_execute_callback("safe_tag", "gnosis")
|
|
100
|
+
assert callable(callback)
|
|
101
|
+
|
|
102
|
+
# Test executing callback
|
|
103
|
+
mock_safe_tx = MagicMock()
|
|
104
|
+
mock_safe_tx.safe_tx_gas = 0
|
|
105
|
+
mock_safe_tx.base_gas = 0
|
|
106
|
+
mock_safe_tx.tx_hash.hex.return_value = "TxHash"
|
|
90
107
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
mock_safe_tx.tx_hash.hex.return_value = "TxHash"
|
|
108
|
+
with patch("iwa.core.services.safe_executor.SafeTransactionExecutor") as mock_executor_cls:
|
|
109
|
+
mock_executor_cls.return_value.execute_with_retry.return_value = (True, "0xTxHash", {})
|
|
94
110
|
|
|
95
|
-
|
|
111
|
+
result = callback(mock_safe_tx)
|
|
96
112
|
|
|
97
|
-
|
|
98
|
-
mock_safe_tx.sign.assert_called()
|
|
99
|
-
mock_safe_tx.execute.assert_called()
|
|
113
|
+
assert result == "0xTxHash"
|
|
100
114
|
|
|
101
115
|
|
|
102
116
|
def test_get_sign_and_execute_callback_fail(safe_service, mock_deps):
|
|
103
117
|
"""Test callback generation fails if account missing."""
|
|
104
118
|
mock_deps["key_storage"].find_stored_account.return_value = None
|
|
105
119
|
with pytest.raises(ValueError):
|
|
106
|
-
safe_service.get_sign_and_execute_callback("
|
|
120
|
+
safe_service.get_sign_and_execute_callback("unknown_tag", "gnosis")
|
|
107
121
|
|
|
108
122
|
|
|
109
123
|
def test_redeploy_safes(safe_service, mock_deps):
|