Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PAN-1990] Read transfer data from destination blockchain #21

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions client-library.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ blockchains:
fallback_providers:
- !ENV ${AVALANCHE_FALLBACK_PROVIDER:https://rpc.ankr.com/avalanche_fuji}
average_block_time: !ENV tag:yaml.org,2002:int ${AVALANCHE_AVERAGE_BLOCK_TIME:3}
blocks_per_query: !ENV tag:yaml.org,2002:int ${AVALANCHE_BLOCKS_PER_QUERY:2000}
chain_id: !ENV tag:yaml.org,2002:int ${AVALANCHE_CHAIN_ID:43113}
confirmations: !ENV tag:yaml.org,2002:int ${AVALANCHE_CONFIRMATIONS:20}
hub: !ENV ${AVALANCHE_HUB:0xbafFb84601BeC1FCb4B842f8917E3eA850781BE7}
Expand All @@ -32,6 +33,7 @@ blockchains:
fallback_providers:
- !ENV ${BNB_FALLBACK_PROVIDER:https://data-seed-prebsc-1-s1.binance.org:8545/}
average_block_time: !ENV tag:yaml.org,2002:int ${BNB_AVERAGE_BLOCK_TIME:3}
blocks_per_query: !ENV tag:yaml.org,2002:int ${BNB_BLOCKS_PER_QUERY:2000}
chain_id: !ENV tag:yaml.org,2002:int ${BNB_CHAIN_ID:97}
confirmations: !ENV tag:yaml.org,2002:int ${BNB_CONFIRMATIONS:3}
hub: !ENV ${BNB_HUB:0xFB37499DC5401Dc39a0734df1fC7924d769721d5}
Expand All @@ -53,6 +55,7 @@ blockchains:
fallback_providers:
- !ENV ${CELO_FALLBACK_PROVIDER:https://alfajores-forno.celo-testnet.org}
average_block_time: !ENV tag:yaml.org,2002:int ${CELO_AVERAGE_BLOCK_TIME:5}
blocks_per_query: !ENV tag:yaml.org,2002:int ${CELO_BLOCKS_PER_QUERY:2000}
chain_id: !ENV tag:yaml.org,2002:int ${CELO_CHAIN_ID:44787}
confirmations: !ENV tag:yaml.org,2002:int ${CELO_CONFIRMATIONS:3}
hub: !ENV ${CELO_HUB:0x8389B9A7608dbf52a699b998f309883257923C0E}
Expand All @@ -74,6 +77,7 @@ blockchains:
fallback_providers:
- !ENV ${CRONOS_FALLBACK_PROVIDER:https://evm-t3.cronos.org}
average_block_time: !ENV tag:yaml.org,2002:int ${CRONOS_AVERAGE_BLOCK_TIME:5}
blocks_per_query: !ENV tag:yaml.org,2002:int ${CRONOS_BLOCKS_PER_QUERY:2000}
chain_id: !ENV tag:yaml.org,2002:int ${CRONOS_CHAIN_ID:338}
confirmations: !ENV tag:yaml.org,2002:int ${CRONOS_CONFIRMATIONS:3}
hub: !ENV ${CRONOS_HUB:0x0Cfb3c7C11A33BEf124A9D86073e73932b9AbF90}
Expand All @@ -95,6 +99,7 @@ blockchains:
fallback_providers:
- !ENV ${ETHEREUM_FALLBACK_PROVIDER:https://ethereum-holesky.publicnode.com}
average_block_time: !ENV tag:yaml.org,2002:int ${ETHEREUM_AVERAGE_BLOCK_TIME:14}
blocks_per_query: !ENV tag:yaml.org,2002:int ${ETHEREUM_BLOCKS_PER_QUERY:2000}
chain_id: !ENV tag:yaml.org,2002:int ${ETHEREUM_CHAIN_ID:17000}
confirmations: !ENV tag:yaml.org,2002:int ${ETHEREUM_CONFIRMATIONS:20}
hub: !ENV ${ETHEREUM_HUB:0x5e447968d4a177fE7bFB8877cA12aE20Bd60dD85}
Expand All @@ -116,6 +121,7 @@ blockchains:
fallback_providers:
- !ENV ${FANTOM_FALLBACK_PROVIDER:https://rpc.ankr.com/fantom_testnet}
average_block_time: !ENV tag:yaml.org,2002:int ${FANTOM_AVERAGE_BLOCK_TIME:1}
blocks_per_query: !ENV tag:yaml.org,2002:int ${FANTOM_BLOCKS_PER_QUERY:2000}
chain_id: !ENV tag:yaml.org,2002:int ${FANTOM_CHAIN_ID:4002}
confirmations: !ENV tag:yaml.org,2002:int ${FANTOM_CONFIRMATIONS:3}
hub: !ENV ${FANTOM_HUB:0x4BC6A71D4C3D6170d0Db849fE19b8DbA18f1a7F5}
Expand All @@ -137,6 +143,7 @@ blockchains:
fallback_providers:
- !ENV ${POLYGON_FALLBACK_PROVIDER:https://rpc.ankr.com/polygon_mumbai}
average_block_time: !ENV tag:yaml.org,2002:int ${POLYGON_AVERAGE_BLOCK_TIME:3}
blocks_per_query: !ENV tag:yaml.org,2002:int ${POLYGON_BLOCKS_PER_QUERY:2000}
chain_id: !ENV tag:yaml.org,2002:int ${POLYGON_CHAIN_ID:80001}
confirmations: !ENV tag:yaml.org,2002:int ${POLYGON_CONFIRMATIONS:20}
hub: !ENV ${POLYGON_HUB:0x5C4B92cd0A956dedc14AF31fD474931540D8277B}
Expand All @@ -158,6 +165,7 @@ blockchains:
fallback_providers:
- !ENV ${SOLANA_FALLBACK_PROVIDER}
average_block_time: !ENV tag:yaml.org,2002:int ${SOLANA_AVERAGE_BLOCK_TIME:1}
blocks_per_query: !ENV tag:yaml.org,2002:int ${SOLANA_BLOCKS_PER_QUERY:2000}
chain_id: !ENV tag:yaml.org,2002:int ${SOLANA_CHAIN_ID:-1}
confirmations: !ENV tag:yaml.org,2002:int ${SOLANA_CONFIRMATIONS:1}
hub: !ENV ${SOLANA_HUB}
Expand Down
126 changes: 121 additions & 5 deletions pantos/client/library/blockchains/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ class BlockchainClientError(ClientLibraryError):
pass


class UnknownTransferError(BlockchainClientError):
"""Exception raised for unknown token transfers.

"""
def __init__(self, **kwargs: typing.Any):
# Docstring inherited
super().__init__('unknown transfer', **kwargs)


class BlockchainClient(BlockchainHandler, ErrorCreator[BlockchainClientError]):
"""Base class for all blockchain clients.

Expand Down Expand Up @@ -125,6 +134,82 @@ class ComputeTransferSignatureResponse:
sender_nonce: int
signature: str

@dataclasses.dataclass
class DestinationTransferRequest:
"""Request data for a token transfer on the destination
blockchain.

Attributes
----------
source_blockchain : Blockchain
The token transfer's source blockchain.
source_transaction_id : str
The transaction ID of the token transfer on the
source blockchain.
blocks_to_search : int | None
The blocks to search for the token transfer on the
destination blockchain.

"""
source_blockchain: Blockchain
source_transaction_id: str
blocks_to_search: int | None = None

@dataclasses.dataclass
class DestinationTransferResponse:
"""Response data for a token transfer on the destination
blockchain.

Attributes
----------
latest_block_number : int
The latest block number on the destination blockchain.
transaction_block_number : int
The block number of the token transfer transaction.
destination_transaction_id : str
The transaction ID of the token transfer.
source_transfer_id : int
The unique identifier of the token transfer on the source
blockchain.
destination_transfer_id : int
The unique identifier of the token transfer on the
destination blockchain.
sender_address : BlockchainAddress
The address of the sender's account.
recipient_address : BlockchainAddress
The address of the recipient's account.
source_token_address : BlockchainAddress
The transferred token's address on the source blockchain.
destination_token_address : BlockchainAddress
The transferred token's address on the destination
blockchain.
amount : int
The transferred token amount.
validator_nonce : int
The unique nonce of the validator for the token transfer on
the destination blockchain.
signer_addresses : list of BlockchainAddress
The addresses of the validators which signed the token
transfer on the destination blockchain.
signatures : list of str
The signatures of the validators which signed the token
transfer on the destination blockchain.

"""
latest_block_number: int
transaction_block_number: int
destination_transaction_id: str
source_transfer_id: int
destination_transfer_id: int
sender_address: BlockchainAddress
recipient_address: BlockchainAddress
source_token_address: BlockchainAddress
destination_token_address: BlockchainAddress
amount: int
validator_nonce: int
signer_addresses: list[BlockchainAddress]
signatures: list[str]

@abc.abstractmethod
def compute_transfer_signature(
self, request: ComputeTransferSignatureRequest) \
Expand Down Expand Up @@ -289,9 +374,9 @@ def decrypt_private_key(self, keystore: str, password: str) -> PrivateKey:

@abc.abstractmethod
def read_external_token_address(
self, token_address: BlockchainAddress,
destination_blockchain: Blockchain
) -> BlockchainAddress: # pragma: no cover
self, token_address: BlockchainAddress,
destination_blockchain: Blockchain) \
-> BlockchainAddress: # pragma: no cover
"""Read an external token address that is registered at the
Pantos Hub on the blockchain.

Expand Down Expand Up @@ -338,8 +423,8 @@ def read_service_node_addresses(

@abc.abstractmethod
def read_service_node_url(
self, service_node_address: BlockchainAddress
) -> str: # pragma: no cover
self, service_node_address: BlockchainAddress) \
-> str: # pragma: no cover
"""Read a service node's URL that is registered at the Pantos
Hub on the blockchain.

Expand All @@ -362,6 +447,32 @@ def read_service_node_url(
"""
pass

@abc.abstractmethod
def read_destination_transfer(
self, request: DestinationTransferRequest) \
-> DestinationTransferResponse: # pragma: no cover
"""Read a token transfer on the destination blockchain.

Parameters
----------
request : DestinationTransferRequest
The request data for reading the token transfer.

Returns
-------
DestinationTransferResponse
The response data with the token transfer information.

Raises
------
UnknownTransferError
If the token transfer is unkown.
BlockchainClientError
If the token transfer cannot be read.

"""
pass

def read_token_balance(self, token_address: BlockchainAddress,
account_id: AccountId) -> int:
"""Read a blockchain account's balance of a Pantos-compatible
Expand Down Expand Up @@ -418,6 +529,11 @@ def read_token_decimals(
"""
pass

def _create_unknown_transfer_error(
self, **kwargs: typing.Any) -> BlockchainClientError:
return self._create_error(specialized_error_class=UnknownTransferError,
**kwargs)

def _account_id_to_account_address(
self, account_id: AccountId) -> BlockchainAddress:
if isinstance(account_id, BlockchainAddress):
Expand Down
66 changes: 66 additions & 0 deletions pantos/client/library/blockchains/ethereum.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import hexbytes
import web3
import web3.contract
import web3.types
from pantos.common.blockchains.base import Blockchain
from pantos.common.blockchains.base import NodeConnections
from pantos.common.blockchains.enums import ContractAbi
Expand All @@ -18,6 +19,7 @@
from pantos.client.library.blockchains.base import VERSIONED_CONTRACT_ABIS
from pantos.client.library.blockchains.base import BlockchainClient
from pantos.client.library.blockchains.base import BlockchainClientError
from pantos.client.library.blockchains.base import UnknownTransferError
from pantos.client.library.constants import TOKEN_SYMBOL_PAN

Web3Contract: typing.TypeAlias = NodeConnections.Wrapper[
Expand Down Expand Up @@ -192,6 +194,40 @@ def read_service_node_url(self,
raise self._create_error('unable to read a service node URL',
service_node_address=service_node_address)

def read_destination_transfer(
self, request: BlockchainClient.DestinationTransferRequest) \
-> BlockchainClient.DestinationTransferResponse:
# Docstring inherited
try:
node_connections = self._get_utilities().create_node_connections()
to_block_number = \
node_connections.eth.get_block_number().get_minimum_result()
from_block_number = (to_block_number - request.blocks_to_search +
1 if request.blocks_to_search else 0)
blocks_per_query = self._get_config()['blocks_per_query']
hub_contract = self._create_hub_contract(node_connections)
transfer_event = typing.cast(
NodeConnections.Wrapper[web3.contract.contract.ContractEvent],
hub_contract.events.TransferTo())
for to_block_number_ in range(to_block_number + 1,
from_block_number,
-blocks_per_query):
from_block_number_ = max(to_block_number_ - blocks_per_query,
from_block_number)
transfer_event_logs = self._get_utilities().get_logs(
transfer_event, from_block_number_, to_block_number_ - 1)
transfer_response = self.__find_destination_transfer(
transfer_event_logs, request.source_transaction_id,
request.source_blockchain, to_block_number)
if transfer_response:
return transfer_response
raise self._create_unknown_transfer_error(request=request)
except UnknownTransferError:
raise
except Exception:
raise self._create_error('unable to read a destination transfer',
request=request)

def read_token_decimals(self, token_address: BlockchainAddress) -> int:
# Docstring inherited
try:
Expand Down Expand Up @@ -246,3 +282,33 @@ def __sign_message(self, private_key: PrivateKey,
signed_message = web3.Account.sign_message(message,
private_key=private_key)
return signed_message.signature.to_0x_hex()

def __find_destination_transfer(
self, transfer_event_logs: list[web3.types.EventData],
source_transaction_id: str, source_blockchain_id: int,
to_block_number: int) \
-> BlockchainClient.DestinationTransferResponse | None:
for transfer_event_log in transfer_event_logs:
transfer_event_args = transfer_event_log['args']
if (transfer_event_args['sourceTransactionId']
== source_transaction_id
and transfer_event_args['sourceBlockchainId']
== source_blockchain_id):
return BlockchainClient.DestinationTransferResponse(
to_block_number, transfer_event_log['blockNumber'],
transfer_event_log['transactionHash'].hex(),
transfer_event_args['sourceTransferId'],
transfer_event_args['destinationTransferId'],
BlockchainAddress(transfer_event_args['sender']),
BlockchainAddress(transfer_event_args['recipient']),
BlockchainAddress(transfer_event_args['sourceToken']),
BlockchainAddress(transfer_event_args['destinationToken']),
transfer_event_args['amount'],
transfer_event_args['nonce'], [
BlockchainAddress(signer_address) for signer_address in
transfer_event_args['signerAddresses']
], [
signature.hex()
for signature in transfer_event_args['signatures']
])
return None
6 changes: 6 additions & 0 deletions pantos/client/library/blockchains/solana.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ def read_service_node_url(self,
# Docstring inherited
raise NotImplementedError

def read_destination_transfer(
self, request: BlockchainClient.DestinationTransferRequest) \
-> BlockchainClient.DestinationTransferResponse:
# Docstring inherited
raise NotImplementedError

def read_token_decimals(self, token_address: BlockchainAddress) -> int:
# Docstring inherited
raise NotImplementedError
5 changes: 5 additions & 0 deletions pantos/client/library/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
'type': 'integer',
'required': True
},
'blocks_per_query': {
'type': 'integer',
'required': True,
'min': 1
},
'chain_id': {
'type': 'integer',
'required': True
Expand Down
Loading