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

refactor: add use fallback decorator #85

Merged
merged 4 commits into from
Jan 15, 2024
Merged
Changes from 3 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
40 changes: 40 additions & 0 deletions mm-bot/src/services/decorators/use_fallback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from functools import wraps


def use_fallback(rpc_nodes, logger, error_message="Failed"):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
exceptions = []
for rpc_node in rpc_nodes:
try:
return func(*args, **kwargs, rpc_node=rpc_node)
except Exception as exception:
logger.warning(f"[-] {error_message}: {exception}")
exceptions.append(exception)
logger.error(f"[-] {error_message} from all nodes")
raise Exception(f"{error_message} from all nodes: [{', '.join(str(e) for e in exceptions)}]")

return wrapper

return decorator


def use_async_fallback(rpc_nodes, logger, error_message="Failed"):
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
exceptions = []
for rpc_node in rpc_nodes:
try:
return await func(*args, **kwargs, rpc_node=rpc_node)
except Exception as exception:
logger.warning(f"[-] {error_message}: {exception}")
exceptions.append(exception)
logger.error(f"[-] {error_message} from all nodes")
raise Exception(f"{error_message} from all nodes: [{', '.join(str(e) for e in exceptions)}]")

return wrapper

return decorator

173 changes: 65 additions & 108 deletions mm-bot/src/services/ethereum.py
Original file line number Diff line number Diff line change
@@ -4,65 +4,50 @@
from web3 import Web3

from config import constants
from services.decorators.use_fallback import use_fallback

ETH_CHAIN_ID = int(constants.ETH_CHAIN_ID)
# get only the abi not the entire file
abi_file = json.load(open(os.getcwd() + '/abi/YABTransfer.json'))['abi']

main_w3 = Web3(Web3.HTTPProvider(constants.ETH_RPC_URL))
fallback_w3 = Web3(Web3.HTTPProvider(constants.ETH_FALLBACK_RPC_URL))
w3_clients = [main_w3, fallback_w3]

"""
accounts are instances from the same account but from different nodes
So if a node is down we can use the other one
"""
main_account_rpc = main_w3.eth.account.from_key(constants.ETH_PRIVATE_KEY)
fallback_account_rpc = fallback_w3.eth.account.from_key(constants.ETH_PRIVATE_KEY)
accounts_rpc = [main_account_rpc, fallback_account_rpc]
class EthereumRpcNode:
def __init__(self, rpc_url, private_key, contract_address, abi):
self.w3 = Web3(Web3.HTTPProvider(rpc_url))
self.account = self.w3.eth.account.from_key(private_key)
self.contract = self.w3.eth.contract(address=contract_address, abi=abi)

# get only the abi not the entire file
abi = json.load(open(os.getcwd() + '/abi/YABTransfer.json'))['abi']
"""
contracts_rpc are instances from the same contract but from different nodes
So if a node is down we can use the other one
"""
main_contract_rpc = main_w3.eth.contract(address=constants.ETH_CONTRACT_ADDR, abi=abi)
fallback_contract_rpc = fallback_w3.eth.contract(address=constants.ETH_CONTRACT_ADDR, abi=abi)
contracts_rpc = [main_contract_rpc, fallback_contract_rpc]

main_rpc_node = EthereumRpcNode(constants.ETH_RPC_URL,
constants.ETH_PRIVATE_KEY,
constants.ETH_CONTRACT_ADDR,
abi_file)
fallback_rpc_node = EthereumRpcNode(constants.ETH_FALLBACK_RPC_URL,
constants.ETH_PRIVATE_KEY,
constants.ETH_CONTRACT_ADDR,
abi_file)
rpc_nodes = [main_rpc_node, fallback_rpc_node]

logger = logging.getLogger(__name__)


def get_latest_block() -> int:
for w3 in w3_clients:
try:
return w3.eth.block_number
except Exception as exception:
logger.warning(f"[-] Failed to get block number from node: {exception}")
logger.error(f"[-] Failed to get block number from all nodes")
@use_fallback(rpc_nodes, logger, "Failed to get latest block")
def get_latest_block(rpc_node=main_rpc_node) -> int:
return rpc_node.w3.eth.block_number


def get_is_used_order(order_id, recipient_address, amount) -> bool:
@use_fallback(rpc_nodes, logger, "Failed to get order status")
def get_is_used_order(order_id, recipient_address, amount, rpc_node=main_rpc_node) -> bool:
is_used_index = 2
order_data = Web3.solidity_keccak(['uint256', 'uint256', 'uint256'],
[order_id, int(recipient_address, 0), amount])
for contract_rpc in contracts_rpc:
try:
res = contract_rpc.functions.transfers(order_data).call()
return res[is_used_index]
except Exception as exception:
logger.warning(f"[-] Failed to get is used order from node: {exception}")
logger.error(f"[-] Failed to get is used order from all nodes")
raise Exception("Failed to get is used order from all nodes")


def get_balance() -> int:
for index, w3 in enumerate(w3_clients):
try:
return w3.eth.get_balance(accounts_rpc[index].address)
except Exception as exception:
logger.warning(f"[-] Failed to get balance from node: {exception}")
logger.error(f"[-] Failed to get balance from all nodes")
raise Exception("Failed to get balance from all nodes")
res = rpc_node.contract.functions.transfers(order_data).call()
return res[is_used_index]


@use_fallback(rpc_nodes, logger, "Failed to get balance")
def get_balance(rpc_node=main_rpc_node) -> int:
return rpc_node.w3.eth.get_balance(rpc_node.account.address)


def has_funds(amount: int) -> bool:
@@ -85,21 +70,16 @@ def transfer(deposit_id, dst_addr, amount):


# we need amount so the transaction is valid with the transfer that will be transferred
def create_transfer(deposit_id, dst_addr_bytes, amount):
for index, w3 in enumerate(w3_clients):
try:
unsent_tx = contracts_rpc[index].functions.transfer(deposit_id, dst_addr_bytes, amount).build_transaction({
"chainId": ETH_CHAIN_ID,
"from": accounts_rpc[index].address,
"nonce": get_nonce(w3, accounts_rpc[index].address),
"value": amount,
})
signed_tx = w3.eth.account.sign_transaction(unsent_tx, private_key=accounts_rpc[index].key)
return unsent_tx, signed_tx
except Exception as exception:
logger.warning(f"[-] Failed to create transfer eth on node: {exception}")
logger.error(f"[-] Failed to create transfer eth on all nodes")
raise Exception("Failed to create transfer eth on all nodes")
@use_fallback(rpc_nodes, logger, "Failed to create ethereum transfer")
def create_transfer(deposit_id, dst_addr_bytes, amount, rpc_node=main_rpc_node):
unsent_tx = rpc_node.contract.functions.transfer(deposit_id, dst_addr_bytes, amount).build_transaction({
"chainId": ETH_CHAIN_ID,
"from": rpc_node.account.address,
"nonce": get_nonce(rpc_node.w3, rpc_node.account.address),
"value": amount,
})
signed_tx = rpc_node.w3.eth.account.sign_transaction(unsent_tx, private_key=rpc_node.account.key)
return unsent_tx, signed_tx


def withdraw(deposit_id, dst_addr, amount, value):
@@ -117,40 +97,28 @@ def withdraw(deposit_id, dst_addr, amount, value):
return tx_hash


def create_withdraw(deposit_id, dst_addr_bytes, amount, value):
exceptions = []
for index, w3 in enumerate(w3_clients):
try:
unsent_tx = contracts_rpc[index].functions.withdraw(deposit_id, dst_addr_bytes, amount).build_transaction({
"chainId": ETH_CHAIN_ID,
"from": accounts_rpc[index].address,
"nonce": get_nonce(w3, accounts_rpc[index].address),
"value": value,
})
signed_tx = w3.eth.account.sign_transaction(unsent_tx, private_key=accounts_rpc[index].key)
return unsent_tx, signed_tx
except Exception as exception:
logger.warning(f"[-] Failed to create withdraw eth on node: {exception}")
exceptions.append(exception)
logger.error(f"[-] Failed to create withdraw eth on all nodes")
raise Exception(f"Failed to create withdraw eth on all nodes: [{', '.join(str(e) for e in exceptions)}]")
@use_fallback(rpc_nodes, logger, "Failed to create withdraw eth")
def create_withdraw(deposit_id, dst_addr_bytes, amount, value, rpc_node=main_rpc_node):
unsent_tx = rpc_node.contract.functions.withdraw(deposit_id, dst_addr_bytes, amount).build_transaction({
"chainId": ETH_CHAIN_ID,
"from": rpc_node.account.address,
"nonce": get_nonce(rpc_node.w3, rpc_node.account.address),
"value": value,
})
signed_tx = rpc_node.w3.eth.account.sign_transaction(unsent_tx, private_key=rpc_node.account.key)
return unsent_tx, signed_tx


def get_nonce(w3: Web3, address):
return w3.eth.get_transaction_count(address)


def estimate_gas_fee(transaction):
for w3 in w3_clients:
try:
gas_limit = w3.eth.estimate_gas(transaction)
fee = w3.eth.gas_price
gas_fee = fee * gas_limit
return gas_fee
except Exception as exception:
logger.warning(f"[-] Failed to estimate fee on node: {exception}")
logger.error(f"[-] Failed to estimate fee on all nodes")
raise Exception("Failed to estimate fee on all nodes")
@use_fallback(rpc_nodes, logger, "Failed to estimate gas fee")
def estimate_gas_fee(transaction, rpc_node=main_rpc_node):
rcatalan98 marked this conversation as resolved.
Show resolved Hide resolved
gas_limit = rpc_node.w3.eth.estimate_gas(transaction)
fee = rpc_node.w3.eth.gas_price
gas_fee = fee * gas_limit
return gas_fee


def is_transaction_viable(amount: int, percentage: float, gas_fee: int) -> bool:
@@ -161,23 +129,12 @@ def has_enough_funds(amount: int = 0, gas_fee: int = 0) -> bool:
return get_balance() >= amount + gas_fee


def send_raw_transaction(signed_tx):
for w3 in w3_clients:
try:
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
return tx_hash
except Exception as exception:
logger.warning(f"[-] Failed to send raw transaction on node: {exception}")
logger.error(f"[-] Failed to send raw transaction on all nodes")
raise Exception("Failed to send raw transaction on all nodes")


def wait_for_transaction_receipt(tx_hash):
for w3 in w3_clients:
try:
w3.eth.wait_for_transaction_receipt(tx_hash)
return True
except Exception as exception:
logger.warning(f"[-] Failed to wait for transaction receipt on node: {exception}")
logger.error(f"[-] Failed to wait for transaction receipt on all nodes")
raise Exception("Failed to wait for transaction receipt on all nodes")
@use_fallback(rpc_nodes, logger, "Failed to send raw transaction")
def send_raw_transaction(signed_tx, rpc_node=main_rpc_node):
tx_hash = rpc_node.w3.eth.send_raw_transaction(signed_tx.rawTransaction)
return tx_hash


@use_fallback(rpc_nodes, logger, "Failed to wait for transaction receipt")
def wait_for_transaction_receipt(tx_hash, rpc_node=main_rpc_node):
rpc_node.w3.eth.wait_for_transaction_receipt(tx_hash)
Loading