iwa 0.0.0__py3-none-any.whl → 0.0.1a2__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.
- conftest.py +22 -0
- iwa/__init__.py +1 -0
- iwa/__main__.py +6 -0
- iwa/core/__init__.py +1 -0
- iwa/core/chain/__init__.py +68 -0
- iwa/core/chain/errors.py +47 -0
- iwa/core/chain/interface.py +514 -0
- iwa/core/chain/manager.py +38 -0
- iwa/core/chain/models.py +128 -0
- iwa/core/chain/rate_limiter.py +193 -0
- iwa/core/cli.py +210 -0
- iwa/core/constants.py +28 -0
- iwa/core/contracts/__init__.py +1 -0
- iwa/core/contracts/contract.py +297 -0
- iwa/core/contracts/erc20.py +79 -0
- iwa/core/contracts/multisend.py +71 -0
- iwa/core/db.py +317 -0
- iwa/core/keys.py +361 -0
- iwa/core/mnemonic.py +385 -0
- iwa/core/models.py +344 -0
- iwa/core/monitor.py +209 -0
- iwa/core/plugins.py +45 -0
- iwa/core/pricing.py +91 -0
- iwa/core/services/__init__.py +17 -0
- iwa/core/services/account.py +57 -0
- iwa/core/services/balance.py +113 -0
- iwa/core/services/plugin.py +88 -0
- iwa/core/services/safe.py +392 -0
- iwa/core/services/transaction.py +172 -0
- iwa/core/services/transfer/__init__.py +166 -0
- iwa/core/services/transfer/base.py +260 -0
- iwa/core/services/transfer/erc20.py +247 -0
- iwa/core/services/transfer/multisend.py +386 -0
- iwa/core/services/transfer/native.py +262 -0
- iwa/core/services/transfer/swap.py +326 -0
- iwa/core/settings.py +95 -0
- iwa/core/tables.py +60 -0
- iwa/core/test.py +27 -0
- iwa/core/tests/test_wallet.py +255 -0
- iwa/core/types.py +59 -0
- iwa/core/ui.py +99 -0
- iwa/core/utils.py +59 -0
- iwa/core/wallet.py +380 -0
- iwa/plugins/__init__.py +1 -0
- iwa/plugins/gnosis/__init__.py +5 -0
- iwa/plugins/gnosis/cow/__init__.py +6 -0
- iwa/plugins/gnosis/cow/quotes.py +148 -0
- iwa/plugins/gnosis/cow/swap.py +403 -0
- iwa/plugins/gnosis/cow/types.py +20 -0
- iwa/plugins/gnosis/cow_utils.py +44 -0
- iwa/plugins/gnosis/plugin.py +68 -0
- iwa/plugins/gnosis/safe.py +157 -0
- iwa/plugins/gnosis/tests/test_cow.py +227 -0
- iwa/plugins/gnosis/tests/test_safe.py +100 -0
- iwa/plugins/olas/__init__.py +5 -0
- iwa/plugins/olas/constants.py +106 -0
- iwa/plugins/olas/contracts/activity_checker.py +93 -0
- iwa/plugins/olas/contracts/base.py +10 -0
- iwa/plugins/olas/contracts/mech.py +49 -0
- iwa/plugins/olas/contracts/mech_marketplace.py +43 -0
- iwa/plugins/olas/contracts/service.py +215 -0
- iwa/plugins/olas/contracts/staking.py +403 -0
- iwa/plugins/olas/importer.py +736 -0
- iwa/plugins/olas/mech_reference.py +135 -0
- iwa/plugins/olas/models.py +110 -0
- iwa/plugins/olas/plugin.py +243 -0
- iwa/plugins/olas/scripts/test_full_mech_flow.py +259 -0
- iwa/plugins/olas/scripts/test_simple_lifecycle.py +74 -0
- iwa/plugins/olas/service_manager/__init__.py +60 -0
- iwa/plugins/olas/service_manager/base.py +113 -0
- iwa/plugins/olas/service_manager/drain.py +336 -0
- iwa/plugins/olas/service_manager/lifecycle.py +839 -0
- iwa/plugins/olas/service_manager/mech.py +322 -0
- iwa/plugins/olas/service_manager/staking.py +530 -0
- iwa/plugins/olas/tests/conftest.py +30 -0
- iwa/plugins/olas/tests/test_importer.py +128 -0
- iwa/plugins/olas/tests/test_importer_error_handling.py +349 -0
- iwa/plugins/olas/tests/test_mech_contracts.py +85 -0
- iwa/plugins/olas/tests/test_olas_contracts.py +249 -0
- iwa/plugins/olas/tests/test_olas_integration.py +561 -0
- iwa/plugins/olas/tests/test_olas_models.py +144 -0
- iwa/plugins/olas/tests/test_olas_view.py +258 -0
- iwa/plugins/olas/tests/test_olas_view_actions.py +137 -0
- iwa/plugins/olas/tests/test_olas_view_modals.py +120 -0
- iwa/plugins/olas/tests/test_plugin.py +70 -0
- iwa/plugins/olas/tests/test_plugin_full.py +212 -0
- iwa/plugins/olas/tests/test_service_lifecycle.py +150 -0
- iwa/plugins/olas/tests/test_service_manager.py +1065 -0
- iwa/plugins/olas/tests/test_service_manager_errors.py +208 -0
- iwa/plugins/olas/tests/test_service_manager_flows.py +497 -0
- iwa/plugins/olas/tests/test_service_manager_mech.py +135 -0
- iwa/plugins/olas/tests/test_service_manager_rewards.py +360 -0
- iwa/plugins/olas/tests/test_service_manager_validation.py +145 -0
- iwa/plugins/olas/tests/test_service_staking.py +342 -0
- iwa/plugins/olas/tests/test_staking_integration.py +269 -0
- iwa/plugins/olas/tests/test_staking_validation.py +109 -0
- iwa/plugins/olas/tui/__init__.py +1 -0
- iwa/plugins/olas/tui/olas_view.py +952 -0
- iwa/tools/check_profile.py +67 -0
- iwa/tools/release.py +111 -0
- iwa/tools/reset_env.py +111 -0
- iwa/tools/reset_tenderly.py +362 -0
- iwa/tools/restore_backup.py +82 -0
- iwa/tui/__init__.py +1 -0
- iwa/tui/app.py +174 -0
- iwa/tui/modals/__init__.py +5 -0
- iwa/tui/modals/base.py +406 -0
- iwa/tui/rpc.py +63 -0
- iwa/tui/screens/__init__.py +1 -0
- iwa/tui/screens/wallets.py +749 -0
- iwa/tui/tests/test_app.py +125 -0
- iwa/tui/tests/test_rpc.py +139 -0
- iwa/tui/tests/test_wallets_refactor.py +30 -0
- iwa/tui/tests/test_widgets.py +123 -0
- iwa/tui/widgets/__init__.py +5 -0
- iwa/tui/widgets/base.py +100 -0
- iwa/tui/workers.py +42 -0
- iwa/web/dependencies.py +76 -0
- iwa/web/models.py +76 -0
- iwa/web/routers/accounts.py +115 -0
- iwa/web/routers/olas/__init__.py +24 -0
- iwa/web/routers/olas/admin.py +169 -0
- iwa/web/routers/olas/funding.py +135 -0
- iwa/web/routers/olas/general.py +29 -0
- iwa/web/routers/olas/services.py +378 -0
- iwa/web/routers/olas/staking.py +341 -0
- iwa/web/routers/state.py +65 -0
- iwa/web/routers/swap.py +617 -0
- iwa/web/routers/transactions.py +153 -0
- iwa/web/server.py +155 -0
- iwa/web/tests/test_web_endpoints.py +713 -0
- iwa/web/tests/test_web_olas.py +430 -0
- iwa/web/tests/test_web_swap.py +103 -0
- iwa-0.0.1a2.dist-info/METADATA +234 -0
- iwa-0.0.1a2.dist-info/RECORD +186 -0
- iwa-0.0.1a2.dist-info/entry_points.txt +2 -0
- iwa-0.0.1a2.dist-info/licenses/LICENSE +21 -0
- iwa-0.0.1a2.dist-info/top_level.txt +4 -0
- tests/legacy_cow.py +248 -0
- tests/legacy_safe.py +93 -0
- tests/legacy_transaction_retry_logic.py +51 -0
- tests/legacy_tui.py +440 -0
- tests/legacy_wallets_screen.py +554 -0
- tests/legacy_web.py +243 -0
- tests/test_account_service.py +120 -0
- tests/test_balance_service.py +186 -0
- tests/test_chain.py +490 -0
- tests/test_chain_interface.py +210 -0
- tests/test_cli.py +139 -0
- tests/test_contract.py +195 -0
- tests/test_db.py +180 -0
- tests/test_drain_coverage.py +174 -0
- tests/test_erc20.py +95 -0
- tests/test_gnosis_plugin.py +111 -0
- tests/test_keys.py +449 -0
- tests/test_legacy_wallet.py +1285 -0
- tests/test_main.py +13 -0
- tests/test_mnemonic.py +217 -0
- tests/test_modals.py +109 -0
- tests/test_models.py +213 -0
- tests/test_monitor.py +202 -0
- tests/test_multisend.py +84 -0
- tests/test_plugin_service.py +119 -0
- tests/test_pricing.py +143 -0
- tests/test_rate_limiter.py +199 -0
- tests/test_reset_tenderly.py +202 -0
- tests/test_rpc_view.py +73 -0
- tests/test_safe_coverage.py +139 -0
- tests/test_safe_service.py +168 -0
- tests/test_service_manager_integration.py +61 -0
- tests/test_service_manager_structure.py +31 -0
- tests/test_service_transaction.py +176 -0
- tests/test_staking_router.py +71 -0
- tests/test_staking_simple.py +31 -0
- tests/test_tables.py +76 -0
- tests/test_transaction_service.py +161 -0
- tests/test_transfer_multisend.py +179 -0
- tests/test_transfer_native.py +220 -0
- tests/test_transfer_security.py +93 -0
- tests/test_transfer_structure.py +37 -0
- tests/test_transfer_swap_unit.py +155 -0
- tests/test_ui_coverage.py +66 -0
- tests/test_utils.py +53 -0
- tests/test_workers.py +91 -0
- tools/verify_drain.py +183 -0
- __init__.py +0 -2
- hello.py +0 -6
- iwa-0.0.0.dist-info/METADATA +0 -10
- iwa-0.0.0.dist-info/RECORD +0 -6
- iwa-0.0.0.dist-info/top_level.txt +0 -2
- {iwa-0.0.0.dist-info → iwa-0.0.1a2.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Tests for TransactionService."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from web3 import exceptions as web3_exceptions
|
|
7
|
+
|
|
8
|
+
from iwa.core.keys import EncryptedAccount, KeyStorage
|
|
9
|
+
from iwa.core.services.transaction import TransactionService
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def mock_key_storage():
|
|
14
|
+
"""Mock key storage."""
|
|
15
|
+
mock = MagicMock(spec=KeyStorage)
|
|
16
|
+
|
|
17
|
+
# Mock sign_transaction
|
|
18
|
+
mock_signed_tx = MagicMock()
|
|
19
|
+
mock_signed_tx.raw_transaction = b"raw_tx_bytes"
|
|
20
|
+
mock.sign_transaction.return_value = mock_signed_tx
|
|
21
|
+
|
|
22
|
+
return mock
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def mock_account_service():
|
|
27
|
+
"""Mock account service."""
|
|
28
|
+
mock = MagicMock()
|
|
29
|
+
|
|
30
|
+
mock_account = MagicMock(spec=EncryptedAccount)
|
|
31
|
+
mock_account.address = "0xSigner"
|
|
32
|
+
mock_account.tag = "signer_tag"
|
|
33
|
+
|
|
34
|
+
mock.resolve_account.return_value = mock_account
|
|
35
|
+
return mock
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.fixture
|
|
39
|
+
def mock_chain_interfaces():
|
|
40
|
+
"""Mock chain interfaces."""
|
|
41
|
+
with patch("iwa.core.services.transaction.ChainInterfaces") as mock:
|
|
42
|
+
instance = mock.return_value
|
|
43
|
+
gnosis_interface = MagicMock()
|
|
44
|
+
gnosis_interface.chain.chain_id = 100
|
|
45
|
+
|
|
46
|
+
# Web3 mocks
|
|
47
|
+
gnosis_interface.web3.eth.get_transaction_count.return_value = 5
|
|
48
|
+
gnosis_interface.web3.eth.send_raw_transaction.return_value = b"tx_hash_bytes"
|
|
49
|
+
|
|
50
|
+
# Receipt valid
|
|
51
|
+
mock_receipt = MagicMock()
|
|
52
|
+
mock_receipt.status = 1
|
|
53
|
+
mock_receipt.gasUsed = 21000
|
|
54
|
+
mock_receipt.effectiveGasPrice = 10
|
|
55
|
+
gnosis_interface.web3.eth.wait_for_transaction_receipt.return_value = mock_receipt
|
|
56
|
+
|
|
57
|
+
instance.get.return_value = gnosis_interface
|
|
58
|
+
yield instance
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.fixture
|
|
62
|
+
def mock_external_deps():
|
|
63
|
+
"""Mock logger, db, pricing."""
|
|
64
|
+
with (
|
|
65
|
+
patch("iwa.core.services.transaction.log_transaction") as mock_log,
|
|
66
|
+
patch("iwa.core.pricing.PriceService") as mock_price,
|
|
67
|
+
patch("iwa.core.services.transaction.time.sleep") as _, # speed up tests
|
|
68
|
+
):
|
|
69
|
+
mock_price.return_value.get_token_price.return_value = 1.0 # 1 EUR per Token
|
|
70
|
+
yield {
|
|
71
|
+
"log": mock_log,
|
|
72
|
+
"price": mock_price,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_sign_and_send_success(
|
|
77
|
+
mock_key_storage, mock_account_service, mock_chain_interfaces, mock_external_deps
|
|
78
|
+
):
|
|
79
|
+
"""Test successful sign and send flow."""
|
|
80
|
+
service = TransactionService(mock_key_storage, mock_account_service)
|
|
81
|
+
|
|
82
|
+
tx = {"to": "0xDest", "value": 100}
|
|
83
|
+
|
|
84
|
+
success, receipt = service.sign_and_send(tx, "signer")
|
|
85
|
+
|
|
86
|
+
assert success is True
|
|
87
|
+
assert receipt.status == 1
|
|
88
|
+
|
|
89
|
+
# Check flow
|
|
90
|
+
mock_account_service.resolve_account.assert_called_with("signer")
|
|
91
|
+
mock_chain_interfaces.get.assert_called_with("gnosis")
|
|
92
|
+
|
|
93
|
+
# Check nonce filling
|
|
94
|
+
mock_chain_interfaces.get.return_value.web3.eth.get_transaction_count.assert_called()
|
|
95
|
+
|
|
96
|
+
# Check signing
|
|
97
|
+
mock_key_storage.sign_transaction.assert_called()
|
|
98
|
+
|
|
99
|
+
# Check sending
|
|
100
|
+
mock_chain_interfaces.get.return_value.web3.eth.send_raw_transaction.assert_called_with(
|
|
101
|
+
b"raw_tx_bytes"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Check logging
|
|
105
|
+
mock_external_deps["log"].assert_called_once()
|
|
106
|
+
call_args = mock_external_deps["log"].call_args[1]
|
|
107
|
+
assert call_args["tx_hash"] == "74785f686173685f6279746573" # hex of b'tx_hash_bytes'
|
|
108
|
+
assert call_args["tags"] is None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_sign_and_send_low_gas_retry(
|
|
112
|
+
mock_key_storage, mock_account_service, mock_chain_interfaces, mock_external_deps
|
|
113
|
+
):
|
|
114
|
+
"""Test retry logic on low gas error."""
|
|
115
|
+
service = TransactionService(mock_key_storage, mock_account_service)
|
|
116
|
+
|
|
117
|
+
web3_mock = mock_chain_interfaces.get.return_value.web3.eth
|
|
118
|
+
|
|
119
|
+
# First attempt fails with "intrinsic gas too low", second succeeds
|
|
120
|
+
web3_mock.send_raw_transaction.side_effect = [
|
|
121
|
+
web3_exceptions.Web3RPCError("intrinsic gas too low"),
|
|
122
|
+
b"tx_hash_bytes_success",
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
tx = {"to": "0xDest", "value": 100, "gas": 20000}
|
|
126
|
+
|
|
127
|
+
success, receipt = service.sign_and_send(tx, "signer")
|
|
128
|
+
|
|
129
|
+
assert success is True
|
|
130
|
+
|
|
131
|
+
# Check retries
|
|
132
|
+
assert web3_mock.send_raw_transaction.call_count == 2
|
|
133
|
+
|
|
134
|
+
# Verify gas increase
|
|
135
|
+
# Since 'tx' is mutated in place, both mock calls point to the same dict object which now has 30000
|
|
136
|
+
# We can verify that sign_transaction was called twice, and the final gas is 30000
|
|
137
|
+
assert mock_key_storage.sign_transaction.call_count == 2
|
|
138
|
+
final_tx_arg = mock_key_storage.sign_transaction.call_args[0][0]
|
|
139
|
+
assert final_tx_arg["gas"] == 30000
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_sign_and_send_rpc_rotation(
|
|
143
|
+
mock_key_storage, mock_account_service, mock_chain_interfaces, mock_external_deps
|
|
144
|
+
):
|
|
145
|
+
"""Test RPC rotation on generic error."""
|
|
146
|
+
service = TransactionService(mock_key_storage, mock_account_service)
|
|
147
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
148
|
+
|
|
149
|
+
# Side effect: 1. Exception, 2. Success
|
|
150
|
+
chain_interface.web3.eth.send_raw_transaction.side_effect = [
|
|
151
|
+
Exception("Connection reset"),
|
|
152
|
+
b"tx_hash_bytes",
|
|
153
|
+
]
|
|
154
|
+
chain_interface.rotate_rpc.return_value = True
|
|
155
|
+
|
|
156
|
+
tx = {"to": "0xDest", "value": 100}
|
|
157
|
+
|
|
158
|
+
success, receipt = service.sign_and_send(tx, "signer")
|
|
159
|
+
|
|
160
|
+
assert success is True
|
|
161
|
+
chain_interface.rotate_rpc.assert_called_once()
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Tests for TransferService.multi_send."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from iwa.core.constants import NATIVE_CURRENCY_ADDRESS
|
|
8
|
+
from iwa.core.models import StoredSafeAccount
|
|
9
|
+
from iwa.core.services.transfer import TransferService
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def mock_deps():
|
|
14
|
+
"""Mock dependencies for TransferService."""
|
|
15
|
+
with (
|
|
16
|
+
patch("iwa.core.services.transfer.base.ChainInterfaces") as mock_chain,
|
|
17
|
+
patch("iwa.core.services.transfer.multisend.ChainInterfaces", new=mock_chain),
|
|
18
|
+
patch("iwa.core.services.transfer.swap.ChainInterfaces", new=mock_chain),
|
|
19
|
+
patch("iwa.core.services.transfer.multisend.MultiSendContract") as mock_ms,
|
|
20
|
+
patch("iwa.core.services.transfer.multisend.MultiSendCallOnlyContract") as mock_ms_co,
|
|
21
|
+
patch("iwa.core.services.transfer.multisend.ERC20Contract") as mock_erc20,
|
|
22
|
+
patch("iwa.core.services.transfer.swap.ERC20Contract", new=mock_erc20),
|
|
23
|
+
patch("iwa.core.services.transfer.erc20.ERC20Contract", new=mock_erc20),
|
|
24
|
+
):
|
|
25
|
+
mock_account_service = MagicMock()
|
|
26
|
+
mock_key_storage = MagicMock()
|
|
27
|
+
mock_balance_service = MagicMock()
|
|
28
|
+
mock_safe_service = MagicMock()
|
|
29
|
+
mock_txn_service = MagicMock()
|
|
30
|
+
# Set default return for sign_and_send
|
|
31
|
+
mock_txn_service.sign_and_send.return_value = (True, {})
|
|
32
|
+
|
|
33
|
+
# Setup Chain Interface
|
|
34
|
+
mock_w3 = MagicMock()
|
|
35
|
+
# Ensure to_checksum_address returns input if string
|
|
36
|
+
mock_w3.to_checksum_address.side_effect = lambda x: x
|
|
37
|
+
mock_w3.to_wei.return_value = 1000 # 1000 wei default
|
|
38
|
+
mock_chain.return_value.get.return_value.web3 = mock_w3
|
|
39
|
+
mock_chain.return_value.get.return_value.chain.name = "gnosis"
|
|
40
|
+
mock_chain.return_value.get.return_value.chain.tokens = {"TEST": "0xToken"}
|
|
41
|
+
mock_erc20.return_value.allowance_wei.return_value = 0
|
|
42
|
+
|
|
43
|
+
deps = {
|
|
44
|
+
"account_service": mock_account_service,
|
|
45
|
+
"key_storage": mock_key_storage,
|
|
46
|
+
"balance_service": mock_balance_service,
|
|
47
|
+
"safe_service": mock_safe_service,
|
|
48
|
+
"transaction_service": mock_txn_service,
|
|
49
|
+
"contracts": {"ms": mock_ms, "ms_co": mock_ms_co, "erc20": mock_erc20},
|
|
50
|
+
}
|
|
51
|
+
yield deps
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_multi_send_eoa_native(mock_deps):
|
|
55
|
+
"""Test multi_send with EOA and native transfers."""
|
|
56
|
+
service = TransferService(
|
|
57
|
+
mock_deps["account_service"],
|
|
58
|
+
mock_deps["key_storage"],
|
|
59
|
+
mock_deps["balance_service"],
|
|
60
|
+
mock_deps["safe_service"],
|
|
61
|
+
mock_deps["transaction_service"],
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Mock From Account (EOA)
|
|
65
|
+
mock_from = MagicMock()
|
|
66
|
+
mock_from.address = "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D" # Valid checksum address
|
|
67
|
+
mock_from.tag = "from_tag"
|
|
68
|
+
|
|
69
|
+
def resolve_side_effect(arg):
|
|
70
|
+
if arg == "from_tag":
|
|
71
|
+
return mock_from
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
service.account_service.resolve_account.side_effect = resolve_side_effect
|
|
75
|
+
|
|
76
|
+
# Mock dependencies
|
|
77
|
+
mock_ms_co = mock_deps["contracts"]["ms_co"].return_value
|
|
78
|
+
mock_ms_co.prepare_tx.return_value = {"value": 0, "data": b"encoded"}
|
|
79
|
+
mock_ms_co.address = "0xMultiSendCallOnly"
|
|
80
|
+
|
|
81
|
+
transactions = [
|
|
82
|
+
{
|
|
83
|
+
"to": "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB",
|
|
84
|
+
"amount": 1.0,
|
|
85
|
+
"token": NATIVE_CURRENCY_ADDRESS,
|
|
86
|
+
},
|
|
87
|
+
{"to": "0xTo2", "amount_wei": 500, "token": NATIVE_CURRENCY_ADDRESS},
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
service.multi_send("from_tag", transactions)
|
|
91
|
+
|
|
92
|
+
# Verify Account Resolution
|
|
93
|
+
# mock_deps["account_service"].resolve_account.assert_called() # Side effect makes called_with tricky
|
|
94
|
+
|
|
95
|
+
# Verify Contract Interaction
|
|
96
|
+
mock_deps["contracts"]["ms_co"].assert_called()
|
|
97
|
+
mock_ms_co.prepare_tx.assert_called()
|
|
98
|
+
|
|
99
|
+
# Verify Transaction Service called
|
|
100
|
+
mock_deps["transaction_service"].sign_and_send.assert_called()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_multi_send_safe_erc20(mock_deps):
|
|
104
|
+
"""Test multi_send with Safe and ERC20 transfers."""
|
|
105
|
+
service = TransferService(
|
|
106
|
+
mock_deps["account_service"],
|
|
107
|
+
mock_deps["key_storage"],
|
|
108
|
+
mock_deps["balance_service"],
|
|
109
|
+
mock_deps["safe_service"],
|
|
110
|
+
mock_deps["transaction_service"],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Mock From Account (Safe)
|
|
114
|
+
mock_safe_account = MagicMock(spec=StoredSafeAccount)
|
|
115
|
+
mock_safe_account.address = "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB"
|
|
116
|
+
|
|
117
|
+
service.account_service.resolve_account.side_effect = (
|
|
118
|
+
lambda x: mock_safe_account if x == "safe_tag" else None
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Mock ERC20
|
|
122
|
+
mock_erc20 = mock_deps["contracts"]["erc20"].return_value
|
|
123
|
+
mock_erc20.decimals = 18
|
|
124
|
+
mock_erc20.prepare_transfer_tx.return_value = {"data": b"transfer_data"}
|
|
125
|
+
mock_erc20.address = "0xToken"
|
|
126
|
+
|
|
127
|
+
mock_deps["account_service"].get_token_address.return_value = "0xToken"
|
|
128
|
+
|
|
129
|
+
# Mock MultiSend Normal (for Safe)
|
|
130
|
+
mock_ms = mock_deps["contracts"]["ms"].return_value
|
|
131
|
+
mock_ms.prepare_tx.return_value = {"value": 0, "data": b"multisend_data"}
|
|
132
|
+
mock_ms.address = "0xMultiSend"
|
|
133
|
+
|
|
134
|
+
transactions = [{"to": "0xRecipient", "amount": 10.0, "token": "TEST"}]
|
|
135
|
+
|
|
136
|
+
service.multi_send("safe_tag", transactions)
|
|
137
|
+
|
|
138
|
+
# Verify ERC20 prep (Safe uses transfer, not transferFrom)
|
|
139
|
+
mock_erc20.prepare_transfer_tx.assert_called()
|
|
140
|
+
|
|
141
|
+
# Verify Safe Service execution
|
|
142
|
+
mock_deps["safe_service"].execute_safe_transaction.assert_called()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_multi_send_eoa_erc20_approval(mock_deps):
|
|
146
|
+
"""Test multi_send with EOA checks for allowances."""
|
|
147
|
+
service = TransferService(
|
|
148
|
+
mock_deps["account_service"],
|
|
149
|
+
mock_deps["key_storage"],
|
|
150
|
+
mock_deps["balance_service"],
|
|
151
|
+
mock_deps["safe_service"],
|
|
152
|
+
mock_deps["transaction_service"],
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Stub approve_erc20 to verify it's called
|
|
156
|
+
service.approve_erc20 = MagicMock()
|
|
157
|
+
|
|
158
|
+
# Mock From Account (EOA)
|
|
159
|
+
mock_from = MagicMock()
|
|
160
|
+
mock_from.address = "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D"
|
|
161
|
+
del mock_from.threshold # EOA has no threshold
|
|
162
|
+
service.account_service.resolve_account.side_effect = (
|
|
163
|
+
lambda x: mock_from if x == "from_tag" else None
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
mock_deps["account_service"].get_token_address.return_value = "0xToken"
|
|
167
|
+
mock_erc20 = mock_deps["contracts"]["erc20"].return_value
|
|
168
|
+
mock_erc20.decimals = 18
|
|
169
|
+
mock_erc20.prepare_transfer_from_tx.return_value = {"data": b"transferFrom"}
|
|
170
|
+
|
|
171
|
+
mock_ms_co = mock_deps["contracts"]["ms_co"].return_value
|
|
172
|
+
mock_ms_co.prepare_tx.return_value = {"value": 0, "data": b"encoded"}
|
|
173
|
+
|
|
174
|
+
transactions = [{"to": "0xRecipient", "amount": 10.0, "token": "TEST"}]
|
|
175
|
+
|
|
176
|
+
service.multi_send("from_tag", transactions)
|
|
177
|
+
|
|
178
|
+
# Verify Approval logic was triggered
|
|
179
|
+
service.approve_erc20.assert_called_once()
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Tests for NativeTransferMixin (wrap/unwrap)."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from iwa.core.services.transfer import TransferService
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def mock_deps():
|
|
12
|
+
"""Mock dependencies for TransferService."""
|
|
13
|
+
with (
|
|
14
|
+
patch("iwa.core.services.transfer.base.ChainInterfaces") as mock_chain,
|
|
15
|
+
patch("iwa.core.services.transfer.native.ChainInterfaces", new=mock_chain),
|
|
16
|
+
):
|
|
17
|
+
mock_account_service = MagicMock()
|
|
18
|
+
mock_key_storage = MagicMock()
|
|
19
|
+
mock_balance_service = MagicMock()
|
|
20
|
+
mock_safe_service = MagicMock()
|
|
21
|
+
mock_txn_service = MagicMock()
|
|
22
|
+
|
|
23
|
+
# Setup Chain Interface
|
|
24
|
+
mock_w3 = MagicMock()
|
|
25
|
+
# Mock eth.contract
|
|
26
|
+
mock_contract = MagicMock()
|
|
27
|
+
mock_w3._web3.eth.contract.return_value = mock_contract
|
|
28
|
+
mock_w3._web3.eth.gas_price = 1000000000
|
|
29
|
+
mock_w3._web3.eth.get_transaction_count.return_value = 5
|
|
30
|
+
|
|
31
|
+
# Mock chain info
|
|
32
|
+
mock_chain_instance = mock_chain.return_value.get.return_value
|
|
33
|
+
mock_chain_instance.web3 = mock_w3
|
|
34
|
+
mock_chain_instance.chain.tokens = {"WXDAI": "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d"}
|
|
35
|
+
|
|
36
|
+
deps = {
|
|
37
|
+
"account_service": mock_account_service,
|
|
38
|
+
"key_storage": mock_key_storage,
|
|
39
|
+
"balance_service": mock_balance_service,
|
|
40
|
+
"safe_service": mock_safe_service,
|
|
41
|
+
"transaction_service": mock_txn_service,
|
|
42
|
+
"chain_interface": mock_chain_instance,
|
|
43
|
+
"contract": mock_contract,
|
|
44
|
+
}
|
|
45
|
+
yield deps
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_wrap_native_success(mock_deps):
|
|
49
|
+
"""Test successful wrap of native currency."""
|
|
50
|
+
service = TransferService(
|
|
51
|
+
mock_deps["account_service"],
|
|
52
|
+
mock_deps["key_storage"],
|
|
53
|
+
mock_deps["balance_service"],
|
|
54
|
+
mock_deps["safe_service"],
|
|
55
|
+
mock_deps["transaction_service"],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Mock Account
|
|
59
|
+
mock_account = MagicMock(name="mock_account")
|
|
60
|
+
mock_account.address = "0xUser"
|
|
61
|
+
service.account_service.resolve_account.return_value = mock_account
|
|
62
|
+
|
|
63
|
+
# Mock Contract Function build_transaction
|
|
64
|
+
mock_function = mock_deps["contract"].functions.deposit.return_value
|
|
65
|
+
mock_function.build_transaction.return_value = {
|
|
66
|
+
"to": "0xWXDAI",
|
|
67
|
+
"value": 1000,
|
|
68
|
+
"data": "0x",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Mock Sign and Send
|
|
72
|
+
mock_signed_tx = MagicMock()
|
|
73
|
+
mock_signed_tx.raw_transaction = b"signed_tx"
|
|
74
|
+
service.key_storage.sign_transaction.return_value = mock_signed_tx
|
|
75
|
+
mock_deps["chain_interface"].web3._web3.eth.send_raw_transaction.return_value = b"tx_hash"
|
|
76
|
+
|
|
77
|
+
# Mock Receipt
|
|
78
|
+
mock_receipt = MagicMock()
|
|
79
|
+
mock_receipt.status = 1
|
|
80
|
+
mock_deps[
|
|
81
|
+
"chain_interface"
|
|
82
|
+
].web3._web3.eth.wait_for_transaction_receipt.return_value = mock_receipt
|
|
83
|
+
|
|
84
|
+
result = service.wrap_native("user", 1000)
|
|
85
|
+
|
|
86
|
+
assert result == "74785f68617368" # "tx_hash".hex()
|
|
87
|
+
mock_deps["contract"].functions.deposit.assert_called()
|
|
88
|
+
service.key_storage.sign_transaction.assert_called()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_wrap_native_account_not_found(mock_deps):
|
|
92
|
+
"""Test wrap fails when account not found."""
|
|
93
|
+
service = TransferService(
|
|
94
|
+
mock_deps["account_service"],
|
|
95
|
+
mock_deps["key_storage"],
|
|
96
|
+
mock_deps["balance_service"],
|
|
97
|
+
mock_deps["safe_service"],
|
|
98
|
+
mock_deps["transaction_service"],
|
|
99
|
+
)
|
|
100
|
+
service.account_service.resolve_account.return_value = None
|
|
101
|
+
|
|
102
|
+
result = service.wrap_native("invalid", 1000)
|
|
103
|
+
assert result is None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_wrap_native_token_not_found(mock_deps):
|
|
107
|
+
"""Test wrap fails when WXDAI token not configured."""
|
|
108
|
+
service = TransferService(
|
|
109
|
+
mock_deps["account_service"],
|
|
110
|
+
mock_deps["key_storage"],
|
|
111
|
+
mock_deps["balance_service"],
|
|
112
|
+
mock_deps["safe_service"],
|
|
113
|
+
mock_deps["transaction_service"],
|
|
114
|
+
)
|
|
115
|
+
service.account_service.resolve_account.return_value = MagicMock()
|
|
116
|
+
mock_deps["chain_interface"].chain.tokens = {} # Empty tokens
|
|
117
|
+
|
|
118
|
+
result = service.wrap_native("user", 1000)
|
|
119
|
+
assert result is None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_unwrap_native_success(mock_deps):
|
|
123
|
+
"""Test successful unwrap of wrapped token."""
|
|
124
|
+
service = TransferService(
|
|
125
|
+
mock_deps["account_service"],
|
|
126
|
+
mock_deps["key_storage"],
|
|
127
|
+
mock_deps["balance_service"],
|
|
128
|
+
mock_deps["safe_service"],
|
|
129
|
+
mock_deps["transaction_service"],
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Mock Account
|
|
133
|
+
mock_account = MagicMock(name="mock_account")
|
|
134
|
+
mock_account.address = "0xUser"
|
|
135
|
+
service.account_service.resolve_account.return_value = mock_account
|
|
136
|
+
|
|
137
|
+
# Mock Contract Function build_transaction
|
|
138
|
+
mock_function = mock_deps["contract"].functions.withdraw.return_value
|
|
139
|
+
mock_function.build_transaction.return_value = {
|
|
140
|
+
"to": "0xWXDAI",
|
|
141
|
+
"data": "0x",
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# Mock Sign and Send
|
|
145
|
+
mock_signed_tx = MagicMock()
|
|
146
|
+
mock_signed_tx.raw_transaction = b"signed_tx"
|
|
147
|
+
service.key_storage.sign_transaction.return_value = mock_signed_tx
|
|
148
|
+
mock_deps["chain_interface"].web3._web3.eth.send_raw_transaction.return_value = b"tx_hash"
|
|
149
|
+
|
|
150
|
+
# Mock Receipt
|
|
151
|
+
mock_receipt = MagicMock()
|
|
152
|
+
mock_receipt.status = 1
|
|
153
|
+
mock_deps[
|
|
154
|
+
"chain_interface"
|
|
155
|
+
].web3._web3.eth.wait_for_transaction_receipt.return_value = mock_receipt
|
|
156
|
+
|
|
157
|
+
result = service.unwrap_native("user", 1000)
|
|
158
|
+
|
|
159
|
+
assert result == "74785f68617368"
|
|
160
|
+
mock_deps["contract"].functions.withdraw.assert_called_with(1000)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_unwrap_native_auto_balance(mock_deps):
|
|
164
|
+
"""Test unwrap with auto-balance detection."""
|
|
165
|
+
service = TransferService(
|
|
166
|
+
mock_deps["account_service"],
|
|
167
|
+
mock_deps["key_storage"],
|
|
168
|
+
mock_deps["balance_service"],
|
|
169
|
+
mock_deps["safe_service"],
|
|
170
|
+
mock_deps["transaction_service"],
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
mock_account = MagicMock(name="mock_account")
|
|
174
|
+
mock_account.address = "0xUser"
|
|
175
|
+
service.account_service.resolve_account.return_value = mock_account
|
|
176
|
+
|
|
177
|
+
# Mock Balance
|
|
178
|
+
mock_deps["balance_service"].get_erc20_balance_wei.return_value = 500
|
|
179
|
+
|
|
180
|
+
# Mock Contract Function
|
|
181
|
+
mock_function = mock_deps["contract"].functions.withdraw.return_value
|
|
182
|
+
mock_function.build_transaction.return_value = {}
|
|
183
|
+
|
|
184
|
+
# Mock Sign/Send/Receipt
|
|
185
|
+
mock_signed_tx = MagicMock()
|
|
186
|
+
mock_signed_tx.raw_transaction = b"signed_tx"
|
|
187
|
+
service.key_storage.sign_transaction.return_value = mock_signed_tx
|
|
188
|
+
mock_deps["chain_interface"].web3._web3.eth.send_raw_transaction.return_value = b"tx_hash"
|
|
189
|
+
mock_receipt = MagicMock()
|
|
190
|
+
mock_receipt.status = 1
|
|
191
|
+
mock_deps[
|
|
192
|
+
"chain_interface"
|
|
193
|
+
].web3._web3.eth.wait_for_transaction_receipt.return_value = mock_receipt
|
|
194
|
+
|
|
195
|
+
# Call without amount
|
|
196
|
+
result = service.unwrap_native("user")
|
|
197
|
+
|
|
198
|
+
assert result == "74785f68617368"
|
|
199
|
+
mock_deps["balance_service"].get_erc20_balance_wei.assert_called_with(
|
|
200
|
+
"0xUser", "WXDAI", "gnosis"
|
|
201
|
+
)
|
|
202
|
+
mock_deps["contract"].functions.withdraw.assert_called_with(500)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_unwrap_native_no_balance(mock_deps):
|
|
206
|
+
"""Test unwrap fails when no balance available."""
|
|
207
|
+
service = TransferService(
|
|
208
|
+
mock_deps["account_service"],
|
|
209
|
+
mock_deps["key_storage"],
|
|
210
|
+
mock_deps["balance_service"],
|
|
211
|
+
mock_deps["safe_service"],
|
|
212
|
+
mock_deps["transaction_service"],
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
mock_account = MagicMock()
|
|
216
|
+
service.account_service.resolve_account.return_value = mock_account
|
|
217
|
+
mock_deps["balance_service"].get_erc20_balance_wei.return_value = 0
|
|
218
|
+
|
|
219
|
+
result = service.unwrap_native("user")
|
|
220
|
+
assert result is None
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Security tests for TransferService."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from eth_account import Account
|
|
7
|
+
|
|
8
|
+
from iwa.core.models import EthereumAddress
|
|
9
|
+
from iwa.core.services.transfer import TransferService
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def transfer_service():
|
|
14
|
+
"""Create a TransferService instance with mocked dependencies."""
|
|
15
|
+
return TransferService(
|
|
16
|
+
key_storage=MagicMock(),
|
|
17
|
+
account_service=MagicMock(),
|
|
18
|
+
balance_service=MagicMock(),
|
|
19
|
+
safe_service=MagicMock(),
|
|
20
|
+
transaction_service=MagicMock(),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_is_whitelisted_destination_fail_closed(transfer_service):
|
|
25
|
+
"""Verify that destination whitelist fails closed (returns False) by default.
|
|
26
|
+
|
|
27
|
+
This addresses TRANS-S1 (Whitelist Bypass).
|
|
28
|
+
"""
|
|
29
|
+
# 1. Not an internal account
|
|
30
|
+
transfer_service.account_service.resolve_account.return_value = None
|
|
31
|
+
|
|
32
|
+
# 2. No config whitelist (simulating config.core is None or whitelist empty)
|
|
33
|
+
with patch("iwa.core.services.transfer.base.Config") as mock_config_cls:
|
|
34
|
+
mock_config = MagicMock()
|
|
35
|
+
mock_config.core = None # SIMULATE MISSING CONFIG
|
|
36
|
+
mock_config_cls.return_value = mock_config
|
|
37
|
+
|
|
38
|
+
# Should return False (Blocked)
|
|
39
|
+
random_addr = Account.create().address
|
|
40
|
+
assert transfer_service._is_whitelisted_destination(random_addr) is False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_is_whitelisted_destination_explicit_allow(transfer_service):
|
|
44
|
+
"""Verify that destination whitelist allows explicitly listed addresses."""
|
|
45
|
+
# 1. Not an internal account
|
|
46
|
+
transfer_service.account_service.resolve_account.return_value = None
|
|
47
|
+
|
|
48
|
+
allowed_addr = Account.create().address
|
|
49
|
+
|
|
50
|
+
# 2. In config whitelist
|
|
51
|
+
with patch("iwa.core.services.transfer.base.Config") as mock_config_cls:
|
|
52
|
+
mock_config = MagicMock()
|
|
53
|
+
mock_config.core.whitelist.values.return_value = [EthereumAddress(allowed_addr)]
|
|
54
|
+
mock_config_cls.return_value = mock_config
|
|
55
|
+
|
|
56
|
+
# Should return True (Allowed)
|
|
57
|
+
assert transfer_service._is_whitelisted_destination(allowed_addr) is True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_is_supported_token_strict_validation(transfer_service):
|
|
61
|
+
"""Verify that token validation is strict and rejects arbitrary addresses.
|
|
62
|
+
|
|
63
|
+
This addresses TRANS-S2 (Explicit Token Whitelist).
|
|
64
|
+
"""
|
|
65
|
+
chain_name = "Gnosis"
|
|
66
|
+
valid_token_addr = Account.create().address
|
|
67
|
+
invalid_token_addr = Account.create().address
|
|
68
|
+
|
|
69
|
+
# Mock chain interface
|
|
70
|
+
mock_chain_interface = MagicMock()
|
|
71
|
+
mock_chain_interface.tokens = {"OLAS": EthereumAddress(valid_token_addr)}
|
|
72
|
+
|
|
73
|
+
with patch("iwa.core.services.transfer.base.ChainInterfaces") as mock_ci_cls:
|
|
74
|
+
mock_ci_cls.return_value.get.return_value = mock_chain_interface
|
|
75
|
+
|
|
76
|
+
# 1. Native currency -> Allowed
|
|
77
|
+
from iwa.core.constants import NATIVE_CURRENCY_ADDRESS
|
|
78
|
+
|
|
79
|
+
assert transfer_service._is_supported_token(NATIVE_CURRENCY_ADDRESS, chain_name) is True
|
|
80
|
+
assert transfer_service._is_supported_token("native", chain_name) is True
|
|
81
|
+
|
|
82
|
+
# 2. Explicitly supported token (ByName) -> Allowed
|
|
83
|
+
assert transfer_service._is_supported_token("OLAS", chain_name) is True
|
|
84
|
+
|
|
85
|
+
# 3. Explicitly supported token (ByAddress) -> Allowed
|
|
86
|
+
assert transfer_service._is_supported_token(valid_token_addr, chain_name) is True
|
|
87
|
+
|
|
88
|
+
# 4. Arbitrary address -> BLOCKED
|
|
89
|
+
# Even if it's a valid ETH address, if it's not in the map, it must be False
|
|
90
|
+
assert transfer_service._is_supported_token(invalid_token_addr, chain_name) is False
|
|
91
|
+
|
|
92
|
+
# 5. Garbage input -> BLOCKED
|
|
93
|
+
assert transfer_service._is_supported_token("NOT_A_TOKEN", chain_name) is False
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Test TransferService structure after refactoring."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
|
|
5
|
+
from iwa.core.services.transfer import TransferService
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_transfer_service_structure():
|
|
9
|
+
"""Verify TransferService has all expected methods from mixins."""
|
|
10
|
+
service_methods = dict(inspect.getmembers(TransferService, predicate=inspect.isfunction))
|
|
11
|
+
|
|
12
|
+
# Check Base methods
|
|
13
|
+
assert "_resolve_destination" in service_methods
|
|
14
|
+
assert "_calculate_gas_info" in service_methods
|
|
15
|
+
|
|
16
|
+
# Check Native methods
|
|
17
|
+
assert "_send_native_via_safe" in service_methods
|
|
18
|
+
assert "wrap_native" in service_methods
|
|
19
|
+
assert "unwrap_native" in service_methods
|
|
20
|
+
|
|
21
|
+
# Check ERC20 methods
|
|
22
|
+
assert "_send_erc20_via_safe" in service_methods
|
|
23
|
+
assert "get_erc20_allowance" in service_methods
|
|
24
|
+
assert "approve_erc20" in service_methods
|
|
25
|
+
assert "transfer_from_erc20" in service_methods
|
|
26
|
+
|
|
27
|
+
# Check MultiSend/Drain methods
|
|
28
|
+
assert "multi_send" in service_methods
|
|
29
|
+
assert "drain" in service_methods
|
|
30
|
+
|
|
31
|
+
# Check Swap methods
|
|
32
|
+
assert "swap" in service_methods
|
|
33
|
+
|
|
34
|
+
# Check Main methods
|
|
35
|
+
assert "send" in service_methods
|
|
36
|
+
|
|
37
|
+
print("TransferService structure verification passed!")
|