From f12cde43284b3a735007ff8c4f944502a53fb223 Mon Sep 17 00:00:00 2001 From: Chibuotu Amadi Date: Wed, 22 Sep 2021 12:09:04 +0100 Subject: [PATCH] GITC-394: polygon tx validation (#9475) * update admin + checkout type * update get_web3 * handle polygon tx validation * fix travis * check fix * get_tx_status fix import * bug fix get_web3 * pass polygon boolean * set infura id env for polygon * check fix * try maticvigil * revert to infura * use chain over is_polygon * refac --- app/dashboard/utils.py | 19 ++++++++++++------- app/economy/tx.py | 36 +++++++++++++++++++++++------------- app/grants/models.py | 31 +++++++++++++++++-------------- 3 files changed, 52 insertions(+), 34 deletions(-) diff --git a/app/dashboard/utils.py b/app/dashboard/utils.py index ee435439ffa..1e8b5d70c7a 100644 --- a/app/dashboard/utils.py +++ b/app/dashboard/utils.py @@ -284,7 +284,7 @@ def ipfs_cat_requests(key): return None, 500 -def get_web3(network, sockets=False): +def get_web3(network, sockets=False, chain='std'): """Get a Web3 session for the provided network. Attributes: @@ -298,8 +298,13 @@ def get_web3(network, sockets=False): web3.main.Web3: A web3 instance for the provided network. """ - if network in ['mainnet', 'rinkeby', 'ropsten']: - if sockets: + if network in ['mainnet', 'rinkeby', 'ropsten', 'testnet']: + if network == 'mainnet' and chain == 'polygon': + network = 'polygon-mainnet' + elif network == 'testnet': + network = 'polygon-mumbai' + + if sockets and chain != 'polygon': # polygon doesn't yet have socket support in infura if settings.INFURA_USE_V3: provider = WebsocketProvider(f'wss://{network}.infura.io/ws/v3/{settings.INFURA_V3_PROJECT_ID}') else: @@ -925,12 +930,12 @@ def is_valid_eth_address(eth_address): return (bool(re.match(r"^0x[a-zA-Z0-9]{40}$", eth_address)) or eth_address == "0x0") -def get_tx_status(txid, network, created_on): - status, timestamp, tx = get_tx_status_and_details(txid, network, created_on) +def get_tx_status(txid, network, created_on, chain='std'): + status, timestamp, tx = get_tx_status_and_details(txid, network, created_on, chain=chain) return status, timestamp -def get_tx_status_and_details(txid, network, created_on): +def get_tx_status_and_details(txid, network, created_on, chain='std'): from django.utils import timezone import pytz @@ -944,7 +949,7 @@ def get_tx_status_and_details(txid, network, created_on): if txid == 'override': return 'success', None #overridden by admin try: - web3 = get_web3(network) + web3 = get_web3(network, chain=chain) tx = web3.eth.getTransactionReceipt(txid) if not tx: drop_dead_date = created_on + timezone.timedelta(days=DROPPED_DAYS) diff --git a/app/economy/tx.py b/app/economy/tx.py index c899875fbe2..a884bdd1517 100644 --- a/app/economy/tx.py +++ b/app/economy/tx.py @@ -3,9 +3,7 @@ from django.conf import settings from django.utils import timezone -import requests from dashboard.abi import erc20_abi -from dashboard.utils import get_tx_status, get_web3 from economy.models import Token from web3 import Web3 from web3.exceptions import BadFunctionCallOutput @@ -107,39 +105,51 @@ def parse_token_amount(token_symbol, amount, network): parsed_amount = int(amount * 10 ** decimals) return parsed_amount -def check_for_replaced_tx(tx_hash, network): +def check_for_replaced_tx(tx_hash, network, datetime=None, chain='std'): """ Get status of the provided transaction hash, and look for a replacement transaction hash. If a replacement exists, return the status and hash of the new transaction """ - status, timestamp = get_tx_status(tx_hash, network, timezone.now()) + from dashboard.utils import get_tx_status + + if not datetime: + datetime = timezone.now() + + status, timestamp = get_tx_status(tx_hash, network, datetime, chain=chain) if status in ['pending', 'dropped', 'unknown', '']: new_tx = getReplacedTX(tx_hash) if new_tx: tx_hash = new_tx - status, timestamp = get_tx_status(tx_hash, network, timezone.now()) + status, timestamp = get_tx_status(tx_hash, network, datetime) return tx_hash, status, timestamp -def grants_transaction_validator(contribution, w3): +def grants_transaction_validator(contribution, w3, chain='std'): """ - This function is used to validate contributions sent on L1 through the BulkCheckout contract. + This function is used to validate contributions sent on L1 & Polygon L2 through the BulkCheckout contract. This contract can be found here: - On GitHub: https://github.com/gitcoinco/BulkTransactions/blob/master/contracts/BulkCheckout.sol - On mainnet: https://etherscan.io/address/0x7d655c57f71464b6f83811c55d84009cd9f5221c + - On Polygon mainnet: https://polygonscan.com/address/0xb99080b9407436eBb2b8Fe56D45fFA47E9bb8877 + - On Polygon testnet: https://mumbai.polygonscan.com/address/0x3E2849E2A489C8fE47F52847c42aF2E8A82B9973 - To facilitate testing on Rinkeby, we pass in a web3 instance instead of using the mainnet + To facilitate testing on Rinkeby and Mumbai, we pass in a web3 instance instead of using the mainnet instance defined at the top of this file """ - # Get bulk checkout contract instance - bulk_checkout_contract = w3.eth.contract(address=bulk_checkout_address, abi=bulk_checkout_abi) - # Get specific info about this contribution that we use later tx_hash = contribution.split_tx_id network = contribution.subscription.network + if network == 'mainnet' and chain == 'polygon': + bulk_checkout_address = '0xb99080b9407436eBb2b8Fe56D45fFA47E9bb8877' + elif network == 'testnet': + bulk_checkout_address = '0x3E2849E2A489C8fE47F52847c42aF2E8A82B9973' + + # Get bulk checkout contract instance + bulk_checkout_contract = w3.eth.contract(address=bulk_checkout_address, abi=bulk_checkout_abi) + # Response that calling function uses to set fields on Contribution. Set the defaults here response = { # We set `passed` to `True` if matching transfer is found for this contribution. The @@ -168,7 +178,7 @@ def grants_transaction_validator(contribution, w3): return response # Check for dropped and replaced txn - tx_hash, status, timestamp = check_for_replaced_tx(tx_hash, network) + tx_hash, status, _ = check_for_replaced_tx(tx_hash, network, chain=chain) # If transaction was successful, continue to validate it if status == 'success': @@ -220,7 +230,7 @@ def grants_transaction_validator(contribution, w3): is_correct_token = event['args']['token'].lower() == expected_token transfer_amount = event['args']['amount'] - is_correct_amount = transfer_amount > expected_amount_min and transfer_amount < expected_amount_max + is_correct_amount = transfer_amount >= expected_amount_min and transfer_amount <= expected_amount_max if is_correct_recipient and is_correct_token and is_correct_amount: # We found the event log corresponding to the contribution parameters diff --git a/app/grants/models.py b/app/grants/models.py index b2ad1236a38..47fcf205d91 100644 --- a/app/grants/models.py +++ b/app/grants/models.py @@ -46,6 +46,7 @@ import requests from django_extensions.db.fields import AutoSlugField from economy.models import SuperModel +from economy.tx import check_for_replaced_tx from economy.utils import ConversionRateNotFoundError, convert_amount from gas.utils import eth_usd_conv_rate, recommend_min_gas_price_to_confirm_in_time from grants.utils import generate_collection_thumbnail, get_upload_filename, is_grant_team_member @@ -1681,6 +1682,7 @@ class Contribution(SuperModel): CHECKOUT_TYPES = [ ('eth_std', 'eth_std'), ('eth_zksync', 'eth_zksync'), + ('eth_polygon', 'eth_polygon'), ('zcash_std', 'zcash_std'), ('celo_std', 'celo_std'), ('zil_std', 'zil_std'), @@ -1767,6 +1769,9 @@ def blockexplorer_url_txid(self): def blockexplorer_url_helper(self, tx_id): if self.checkout_type == 'eth_zksync': return f'https://zkscan.io/explorer/transactions/{tx_id.replace("sync-tx:", "")}' + if self.checkout_type == 'eth_polygon': + network_sub = f"mumbai." if self.subscription and self.subscription.network != 'mainnet' else '' + return f'https://{network_sub}polygonscan.com/tx/{tx_id}' if self.checkout_type == 'eth_std': network_sub = f"{self.subscription.network}." if self.subscription and self.subscription.network != 'mainnet' else '' return f'https://{network_sub}etherscan.io/tx/{tx_id}' @@ -1808,7 +1813,7 @@ def leave_gitcoinbot_comment_for_status(self, status): "comment":comment, "is_edited":True, } - ); + ) except Exception as e: print(e) @@ -1816,9 +1821,8 @@ def leave_gitcoinbot_comment_for_status(self, status): def update_tx_status(self): """Updates tx status for Ethereum contributions.""" try: + from dashboard.utils import get_web3 from economy.tx import grants_transaction_validator - from dashboard.utils import get_tx_status - from economy.tx import getReplacedTX # If `tx_override` is True, we don't run the validator for this contribution if self.tx_override: @@ -1849,20 +1853,19 @@ def update_tx_status(self): self.tx_cleared = True self.validator_comment = "zkSync checkout. Success" if self.success else f"zkSync Checkout. {tx_data['fail_reason']}" - elif self.checkout_type == 'eth_std': - # Standard L1 checkout using the BulkCheckout contract + elif self.checkout_type == 'eth_std' or self.checkout_type == 'eth_polygon': + # Standard L1 and sidechain L2 checkout using the BulkCheckout contract + # get active chain std/polygon + chain = self.checkout_type.split('_')[-1] + # Prepare web3 provider - PROVIDER = "wss://" + network + ".infura.io/ws/v3/" + settings.INFURA_V3_PROJECT_ID - w3 = Web3(Web3.WebsocketProvider(PROVIDER)) + w3 = get_web3(network, chain=chain) # Handle dropped/replaced transactions - split_tx_status, _ = get_tx_status(self.split_tx_id, self.subscription.network, self.created_on) - if split_tx_status in ['pending', 'dropped', 'unknown', '']: - new_tx = getReplacedTX(self.split_tx_id) - if new_tx: - self.split_tx_id = new_tx - split_tx_status, _ = get_tx_status(self.split_tx_id, self.subscription.network, self.created_on) + _, split_tx_status, _ = check_for_replaced_tx( + self.split_tx_id, network, self.created_on, chain=chain + ) # Handle pending txns if split_tx_status in ['pending']: @@ -1888,7 +1891,7 @@ def update_tx_status(self): return # Validate that the token transfers occurred - response = grants_transaction_validator(self, w3) + response = grants_transaction_validator(self, w3, chain=chain) if len(response['originator']): self.originated_address = response['originator'][0] self.validator_passed = response['validation']['passed']