Skip to content

Commit

Permalink
Merge pull request #155 from levonyak/PAN-2344-no-transfer-retries-wi…
Browse files Browse the repository at this point in the history
…th-incorrect-destination_token

[PAN-2344] No transfer retries with incorrect destination token
  • Loading branch information
markuslevonyak authored Nov 27, 2024
2 parents e629c49 + d1be5f6 commit 140bc1d
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 2 deletions.
55 changes: 55 additions & 0 deletions pantos/servicenode/blockchains/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,61 @@ def is_valid_recipient_address(self, recipient_address: str) -> bool:
"""
pass # pragma: no cover

@dataclasses.dataclass
class ExternalTokenRecordRequest:
"""Request data for reading an external token record.
Attributes
----------
token_address : BlockchainAddress
The address of the token on the blockchain the external
token record is read from.
external_blockchain : Blockchain
The blockchain to read the external token record for.
"""
token_address: BlockchainAddress
external_blockchain: Blockchain

@dataclasses.dataclass
class ExternalTokenRecordResponse:
"""Response data from reading an external token record.
Attributes
----------
is_registration_active : bool
True if the external token registration is active.
external_token_address : BlockchainAddress
The registered external address of the token.
"""
is_registration_active: bool
external_token_address: BlockchainAddress

@abc.abstractmethod
def read_external_token_record(
self, request: ExternalTokenRecordRequest) \
-> ExternalTokenRecordResponse:
"""Read an external token record from the Pantos Hub.
Parameters
----------
request : ExternalTokenRecordRequest
The request data.
Returns
-------
ExternalTokenRecordResponse
The response data.
Raises
------
BlockchainClientError
If the external token record cannot be read.
"""
pass # pragma: no cover

@abc.abstractmethod
def read_minimum_deposit(self) -> int:
"""Read the service node's minimum deposit at the Pantos Hub on
Expand Down
23 changes: 23 additions & 0 deletions pantos/servicenode/blockchains/ethereum.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,29 @@ def is_valid_recipient_address(self, recipient_address: str) -> bool:
is_zero_address = int(recipient_address, 0) == 0
return not is_zero_address

def read_external_token_record(
self, request: BlockchainClient.ExternalTokenRecordRequest) \
-> BlockchainClient.ExternalTokenRecordResponse:
# Docstring inherited
try:
node_connections = self.__create_node_connections()
hub_contract = self._create_hub_contract(node_connections)
external_token_record = hub_contract.caller().\
getExternalTokenRecord(request.token_address,
request.external_blockchain.value).get()
assert len(external_token_record) == 2
is_registration_active = external_token_record[0]
assert isinstance(is_registration_active, bool)
external_token_address = external_token_record[1]
assert isinstance(external_token_address, str)
return BlockchainClient.ExternalTokenRecordResponse(
is_registration_active=is_registration_active,
external_token_address=BlockchainAddress(
external_token_address))
except Exception:
raise self._create_error('unable to read an external token record',
request=request)

def read_minimum_deposit(self) -> int:
# Docstring inherited
try:
Expand Down
6 changes: 6 additions & 0 deletions pantos/servicenode/blockchains/solana.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ def is_valid_recipient_address(self, recipient_address: str) -> bool:
# Docstring inherited
raise NotImplementedError # pragma: no cover

def read_external_token_record(
self, request: BlockchainClient.ExternalTokenRecordRequest) \
-> BlockchainClient.ExternalTokenRecordResponse:
# Docstring inherited
raise NotImplementedError # pragma: no cover

def read_minimum_deposit(self) -> int:
# Docstring inherited
raise NotImplementedError # pragma: no cover
Expand Down
22 changes: 20 additions & 2 deletions pantos/servicenode/business/transfers.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@ def __single_chain_transfer(self,

def __cross_chain_transfer(self,
request: ExecuteTransferRequest) -> uuid.UUID:
source_blockchain_client = get_blockchain_client(
request.source_blockchain)
self.__validate_destination_token(source_blockchain_client, request)
transfer_from_request = \
BlockchainClient.TransferFromSubmissionStartRequest(
request.internal_transfer_id,
Expand All @@ -299,8 +302,6 @@ def __cross_chain_transfer(self,
request.destination_token_address,
request.amount, request.fee, request.sender_nonce,
request.valid_until, request.signature)
source_blockchain_client = get_blockchain_client(
request.source_blockchain)
try:
return source_blockchain_client.start_transfer_from_submission(
transfer_from_request)
Expand All @@ -316,6 +317,23 @@ def __cross_chain_transfer(self,
request.internal_transfer_id, TransferStatus.ACCEPTED)
raise

def __validate_destination_token(
self, source_blockchain_client: BlockchainClient,
request: ExecuteTransferRequest) -> None:
external_token_request = BlockchainClient.ExternalTokenRecordRequest(
token_address=request.source_token_address,
external_blockchain=request.destination_blockchain)
external_token_response = source_blockchain_client.\
read_external_token_record(external_token_request)
if (not external_token_response.is_registration_active
or request.destination_token_address
!= external_token_response.external_token_address):
database_access.update_transfer_status(
request.internal_transfer_id, TransferStatus.FAILED)
raise TransferInteractorUnrecoverableError(
'invalid destination token', request=request,
**dataclasses.asdict(external_token_response))

def __check_valid_until(self, source_blockchain: Blockchain,
valid_until: int, bid_execution_time: int,
time_received: float) -> None:
Expand Down
47 changes: 47 additions & 0 deletions tests/blockchains/test_ethereum.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@

_TRANSFER_DESTIONATION_TOKEN_ADDRESS = 'destination_token'

_TOKEN_ADDRESS = '0xa6fd6EB118BBdf6c4B31866542972d3D589b24C6'

_EXTERNAL_TOKEN_ADDRESS = '0x4E4d4470d72CA0CE478d6f87a1ae3a868F0e8Bb9'


@pytest.fixture(scope='module')
def web3_account():
Expand Down Expand Up @@ -333,6 +337,49 @@ def test_is_valid_recipient_address_0_address_false(mock_is_valid_address,
assert is_recipient_address_correct is False


@pytest.mark.parametrize('is_registration_active', [True, False])
@pytest.mark.parametrize('external_blockchain', [
blockchain
for blockchain in Blockchain if blockchain is not Blockchain.ETHEREUM
])
def test_read_external_token_record_correct(
external_blockchain, is_registration_active, ethereum_client,
mock_get_blockchain_config, provider_timeout, hub_contract_address,
mock_get_blockchain_utilities):
mock_get_blockchain_config.return_value = {
'provider_timeout': provider_timeout,
'hub': hub_contract_address
}
mock_get_blockchain_utilities().create_contract().caller().\
getExternalTokenRecord().get.return_value = (is_registration_active,
_EXTERNAL_TOKEN_ADDRESS)

request = BlockchainClient.ExternalTokenRecordRequest(
token_address=_TOKEN_ADDRESS, external_blockchain=external_blockchain)
response = ethereum_client.read_external_token_record(request)

assert response.is_registration_active is is_registration_active
assert response.external_token_address == _EXTERNAL_TOKEN_ADDRESS


def test_read_external_token_record_error(ethereum_client,
mock_get_blockchain_config,
provider_timeout,
hub_contract_address,
mock_get_blockchain_utilities):
mock_get_blockchain_config.return_value = {
'provider_timeout': provider_timeout,
'hub': hub_contract_address
}
mock_get_blockchain_utilities().create_contract().caller.side_effect = \
EthereumUtilitiesError

request = BlockchainClient.ExternalTokenRecordRequest(
token_address=_TOKEN_ADDRESS, external_blockchain=Blockchain.AVALANCHE)
with pytest.raises(EthereumClientError):
ethereum_client.read_external_token_record(request)


def test_read_minimum_deposit_correct(ethereum_client,
mock_get_blockchain_config,
provider_timeout, hub_contract_address,
Expand Down
54 changes: 54 additions & 0 deletions tests/business/test_transfers.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,11 @@ def test_execute_transfer_cross_chain_correct(mocked_database_access,
mocked_time,
execute_transfer_request):
mocked_time.time.return_value = execute_transfer_request.valid_until - 1
mocked_get_blockchain_client().read_external_token_record.return_value = \
BlockchainClient.ExternalTokenRecordResponse(
is_registration_active=True,
external_token_address=execute_transfer_request.
destination_token_address)
expected_transfer_from_request = \
BlockchainClient.TransferFromSubmissionStartRequest(
execute_transfer_request.internal_transfer_id,
Expand All @@ -344,6 +349,50 @@ def test_execute_transfer_cross_chain_correct(mocked_database_access,
start_transfer_from_submission(expected_transfer_from_request))


@unittest.mock.patch('pantos.servicenode.business.transfers.time')
@unittest.mock.patch(
'pantos.servicenode.business.transfers.get_blockchain_client')
@unittest.mock.patch('pantos.servicenode.business.transfers.database_access')
def test_execute_transfer_cross_chain_destination_token_inactive_error(
mocked_database_access, mocked_get_blockchain_client, mocked_time,
execute_transfer_request):
mocked_time.time.return_value = execute_transfer_request.valid_until - 1
mocked_get_blockchain_client().read_external_token_record.return_value = \
BlockchainClient.ExternalTokenRecordResponse(
is_registration_active=False,
external_token_address=execute_transfer_request.
destination_token_address)

with pytest.raises(TransferInteractorUnrecoverableError):
TransferInteractor().execute_transfer(execute_transfer_request)

mocked_database_access.update_transfer_status.assert_called_once_with(
execute_transfer_request.internal_transfer_id, TransferStatus.FAILED)


@unittest.mock.patch('pantos.servicenode.business.transfers.time')
@unittest.mock.patch(
'pantos.servicenode.business.transfers.get_blockchain_client')
@unittest.mock.patch('pantos.servicenode.business.transfers.database_access')
def test_execute_transfer_cross_chain_destination_token_address_invalid_error(
mocked_database_access, mocked_get_blockchain_client, mocked_time,
execute_transfer_request):
external_token_address = '0x3b25C4449EF3aB8c383774a2194C573D1cC6e4c5'
assert (external_token_address
!= execute_transfer_request.destination_token_address)
mocked_time.time.return_value = execute_transfer_request.valid_until - 1
mocked_get_blockchain_client().read_external_token_record.return_value = \
BlockchainClient.ExternalTokenRecordResponse(
is_registration_active=True,
external_token_address=external_token_address)

with pytest.raises(TransferInteractorUnrecoverableError):
TransferInteractor().execute_transfer(execute_transfer_request)

mocked_database_access.update_transfer_status.assert_called_once_with(
execute_transfer_request.internal_transfer_id, TransferStatus.FAILED)


@unittest.mock.patch('pantos.servicenode.business.transfers.'
'time')
@unittest.mock.patch('pantos.servicenode.business.transfers.'
Expand All @@ -354,6 +403,11 @@ def test_execute_transfer_cross_chain_unrecoverable_error(
mocked_database_access, mocked_get_blockchain_client, mocked_time,
execute_transfer_request):
mocked_time.time.return_value = execute_transfer_request.valid_until - 1
mocked_get_blockchain_client().read_external_token_record.return_value = \
BlockchainClient.ExternalTokenRecordResponse(
is_registration_active=True,
external_token_address=execute_transfer_request.
destination_token_address)
mocked_get_blockchain_client().start_transfer_from_submission.\
side_effect = InvalidSignatureError

Expand Down

0 comments on commit 140bc1d

Please sign in to comment.