def handle(self, *args, **options): # setup payment_threshold_usd = 0 KYC_THRESHOLD = settings.GRANTS_PAYOUT_CLR_KYC_THRESHOLD network = 'mainnet' if not settings.DEBUG else 'rinkeby' from_address = settings.GRANTS_PAYOUT_ADDRESS from_pk = settings.GRANTS_PAYOUT_PRIVATE_KEY DECIMALS = 18 what = options['what'] clr_round = options['clr_round'] DAI_ADDRESS = '0x6b175474e89094c44da98b954eedeac495271d0f' if network=='mainnet' else '0x6a6e8b58dee0ca4b4ee147ad72d3ddd2ef1bf6f7' CLR_TOKEN_ADDRESS = '0xe4101d014443af2b7f6f9f603e904adc9faf0de5' if network=='mainnet' else '0xc19b694ebd4309d7a2adcd9970f8d7f424a1528b' # get data clr_pks = options['clr_pks'].split(',') gclrs = GrantCLR.objects.filter(pk__in=clr_pks) pks = [] for gclr in gclrs: pks += gclr.grants.values_list('pk', flat=True) scheduled_matches = CLRMatch.objects.filter(round_number=clr_round) grants = Grant.objects.filter(active=True, network='mainnet', link_to_new_grant__isnull=True, pk__in=pks) print(f"got {grants.count()} grants") # finalize rankings if what == 'finalize': total_owed_grants = 0 for grant in grants: try: for gclr in grant.clr_calculations.filter(grantclr__in=gclrs, latest=True): total_owed_grants += gclr.clr_prediction_curve[0][1] except: pass total_owed_matches = sum(sm.amount for sm in scheduled_matches) print(f"there are {grants.count()} grants to finalize worth ${round(total_owed_grants,2)}") print(f"there are {scheduled_matches.count()} Match Payments already created worth ${round(total_owed_matches,2)}") print('------------------------------') user_input = input("continue? (y/n) ") if user_input != 'y': return for grant in grants: amount = sum(ele.clr_prediction_curve[0][1] for ele in grant.clr_calculations.filter(grantclr__in=gclrs, latest=True)) has_already_kyc = grant.clr_matches.filter(has_passed_kyc=True).exists() if not amount: continue already_exists = scheduled_matches.filter(grant=grant).exists() if already_exists: continue needs_kyc = amount > KYC_THRESHOLD and not has_already_kyc comments = "" if not needs_kyc else "Needs KYC" ready_for_test_payout = not needs_kyc match = CLRMatch.objects.create( round_number=clr_round, amount=amount, grant=grant, comments=comments, ready_for_test_payout=ready_for_test_payout, ) if needs_kyc: grant_match_distribution_kyc(match) # payout rankings (round must be finalized first) if what in ['prepare_final_payout']: payout_matches = scheduled_matches.exclude(test_payout_tx='').filter(ready_for_payout=False) payout_matches_amount = sum(sm.amount for sm in payout_matches) print(f"there are {payout_matches.count()} UNPAID Match Payments already created worth ${round(payout_matches_amount,2)} {network} DAI") print('------------------------------') user_input = input("continue? (y/n) ") if user_input != 'y': return for match in payout_matches: match.ready_for_payout=True match.save() print('promoted') # payout rankings (round must be finalized first) if what in ['payout_test', 'payout_dai']: is_real_payout = what == 'payout_dai' TOKEN_ADDRESS = DAI_ADDRESS if is_real_payout else CLR_TOKEN_ADDRESS kwargs = {} token_name = f'CLR{clr_round}' if not is_real_payout else 'DAI' key = 'ready_for_test_payout' if not is_real_payout else 'ready_for_payout' kwargs[key] = False not_ready_scheduled_matches = scheduled_matches.filter(**kwargs) kwargs[key] = True kwargs2 = {} key2 = 'test_payout_tx' if not is_real_payout else 'payout_tx' kwargs2[key2] = '' unpaid_scheduled_matches = scheduled_matches.filter(**kwargs).filter(**kwargs2) paid_scheduled_matches = scheduled_matches.filter(**kwargs).exclude(**kwargs2) total_not_ready_matches = sum(sm.amount for sm in not_ready_scheduled_matches) total_owed_matches = sum(sm.amount for sm in unpaid_scheduled_matches) total_paid_matches = sum(sm.amount for sm in paid_scheduled_matches) print(f"there are {not_ready_scheduled_matches.count()} NOT READY Match Payments already created worth ${round(total_not_ready_matches,2)} {network} {token_name}") print(f"there are {unpaid_scheduled_matches.count()} UNPAID Match Payments already created worth ${round(total_owed_matches,2)} {network} {token_name}") print(f"there are {paid_scheduled_matches.count()} PAID Match Payments already created worth ${round(total_paid_matches,2)} {network} {token_name}") print('------------------------------') user_input = input("continue? (y/n) ") if user_input != 'y': return print(f"continuing with {unpaid_scheduled_matches.count()} unpaid scheduled payouts") if is_real_payout: user_input = input(F"THIS IS A REAL PAYOUT FOR {network} {token_name}. ARE YOU DOUBLE SECRET SUPER SURE? (y/n) ") if user_input != 'y': return for match in unpaid_scheduled_matches.order_by('amount'): # issue payment print(f"- issuing payout {match.pk} worth {match.amount} {token_name}") address = match.grant.admin_address amount_owed = match.amount w3 = get_web3(network) contract = w3.eth.contract(Web3.toChecksumAddress(TOKEN_ADDRESS), abi=abi) address = Web3.toChecksumAddress(address) amount = int(amount_owed * 10**DECIMALS) tx_args = { 'nonce': w3.eth.getTransactionCount(from_address), 'gas': 100000, 'gasPrice': int(float(recommend_min_gas_price_to_confirm_in_time(1)) * 10**9 * 1.4) } tx = contract.functions.transfer(address, amount).buildTransaction(tx_args) signed = w3.eth.account.signTransaction(tx, from_pk) tx_id = None success = False counter = 0 while not success: try: tx_id = w3.eth.sendRawTransaction(signed.rawTransaction).hex() success = True except Exception as e: counter +=1 if 'replacement transaction underpriced' in str(e): print(f'replacement transaction underpriced. retrying {counter}') time.sleep(WAIT_TIME_BETWEEN_PAYOUTS) elif 'nonce too low' in str(e): print(f'nonce too low. retrying {counter}') time.sleep(WAIT_TIME_BETWEEN_PAYOUTS) # rebuild txn tx_args['nonce'] = w3.eth.getTransactionCount(from_address) tx = contract.functions.transfer(address, amount).buildTransaction(tx_args) signed = w3.eth.account.signTransaction(tx, from_pk) else: raise e if not tx_id: print("cannot pay advance, did not get a txid") continue print("paid via", tx_id) # make save state to DB if is_real_payout: match.payout_tx = tx_id else: match.test_payout_tx = tx_id match.save() # wait for tx to clear while not has_tx_mined(tx_id, network): time.sleep(1) # make save state to DB if is_real_payout: match.payout_tx_date = timezone.now() grant_match_distribution_final_txn(match) else: match.test_payout_tx_date = timezone.now() grant_match_distribution_test_txn(match) match.save() # create payout obj artifacts profile = Profile.objects.get(handle__iexact='gitcoinbot') validator_comment = f"created by ingest payout_round_script" subscription = Subscription() subscription.is_postive_vote = True subscription.active = False subscription.error = True subscription.contributor_address = 'N/A' subscription.amount_per_period = match.amount subscription.real_period_seconds = 2592000 subscription.frequency = 30 subscription.frequency_unit = 'N/A' subscription.token_address = TOKEN_ADDRESS subscription.token_symbol = token_name subscription.gas_price = 0 subscription.new_approve_tx_id = '0x0' subscription.num_tx_approved = 1 subscription.network = network subscription.contributor_profile = profile subscription.grant = match.grant subscription.comments = validator_comment subscription.amount_per_period_usdt = match.amount if is_real_payout else 0 subscription.save() contrib = Contribution.objects.create( success=True, tx_cleared=True, tx_override=True, tx_id=tx_id, subscription=subscription, validator_passed=True, validator_comment=validator_comment, ) print(f"ingested {subscription.pk} / {contrib.pk}") if is_real_payout: match.payout_contribution = contrib else: match.test_payout_contribution = contrib match.save() metadata = { 'id': subscription.id, 'value_in_token': str(subscription.amount_per_period), 'value_in_usdt_now': str(round(subscription.amount_per_period_usdt,2)), 'token_name': subscription.token_symbol, 'title': subscription.grant.title, 'grant_url': subscription.grant.url, 'num_tx_approved': subscription.num_tx_approved, 'category': 'grant', } kwargs = { 'profile': profile, 'subscription': subscription, 'grant': subscription.grant, 'activity_type': 'new_grant_contribution', 'metadata': metadata, } activity = Activity.objects.create(**kwargs) if is_real_payout: comment = f"CLR Round {clr_round} Payout" comment = Comment.objects.create(profile=profile, activity=activity, comment=comment) print("SLEEPING") time.sleep(WAIT_TIME_BETWEEN_PAYOUTS) print("DONE SLEEPING")
subscription.error = True subscription.contributor_address = '/NA' subscription.amount_per_period = amount subscription.real_period_seconds = 2592000 subscription.frequency = 30 subscription.frequency_unit = 'N/A' subscription.token_address = '0x0' subscription.token_symbol = currency subscription.gas_price = 0 subscription.new_approve_tx_id = '0x0' subscription.num_tx_approved = 1 subscription.network = 'mainnet' subscription.contributor_profile = profile subscription.grant = grant subscription.comments = validator_comment subscription.amount_per_period_usdt = usd_val subscription.save() if created_on: subscription.created_on = created_on subscription.save() contrib = Contribution.objects.create( success=True, tx_cleared=True, tx_override=True, tx_id=txid, subscription=subscription, validator_passed=True, validator_comment=validator_comment, ) if created_on:
def save_data(self, profile, txid, network, created_on, symbol, value_adjusted, grant, checkout_type): """ Creates contribution and subscription and saves it to database if no matching one exists """ currency = symbol amount = value_adjusted usd_val = amount * convert_token_to_usdt(symbol) # Check that subscription with these parameters does not exist existing_subscriptions = Subscription.objects.filter( grant__pk=grant.pk, contributor_profile=profile, split_tx_id=txid, token_symbol=currency ) for existing_subscription in existing_subscriptions: tolerance = 0.01 # 1% tolerance to account for floating point amount_max = amount * (1 + tolerance) amount_min = amount * (1 - tolerance) if ( existing_subscription.amount_per_period_minus_gas_price > amount_min and existing_subscription.amount_per_period_minus_gas_price < amount_max ): # Subscription exists print("Subscription exists, exiting function\n") return # No subscription found, so create subscription and contribution try: # create objects validator_comment = f"created by ingest grant txn script" subscription = Subscription() subscription.is_postive_vote = True subscription.active = False subscription.error = True subscription.contributor_address = "N/A" subscription.amount_per_period = amount subscription.real_period_seconds = 2592000 subscription.frequency = 30 subscription.frequency_unit = "N/A" subscription.token_address = "0x0" subscription.token_symbol = currency subscription.gas_price = 0 subscription.new_approve_tx_id = "0x0" subscription.num_tx_approved = 1 subscription.network = network subscription.contributor_profile = profile subscription.grant = grant subscription.comments = validator_comment subscription.amount_per_period_usdt = usd_val subscription.created_on = created_on subscription.last_contribution_date = created_on subscription.next_contribution_date = created_on subscription.split_tx_id = txid subscription.save() # Create contribution and set the contribution as successful contrib = subscription.successful_contribution( '0x0', # subscription.new_approve_tx_id, True, # include_for_clr checkout_type=checkout_type ) contrib.success=True contrib.tx_cleared=True contrib.tx_override=True contrib.validator_comment = validator_comment contrib.created_on = created_on contrib.save() print(f"ingested {subscription.pk} / {contrib.pk}") metadata = { "id": subscription.id, "value_in_token": str(subscription.amount_per_period), "value_in_usdt_now": str(round(subscription.amount_per_period_usdt, 2)), "token_name": subscription.token_symbol, "title": subscription.grant.title, "grant_url": subscription.grant.url, "num_tx_approved": subscription.num_tx_approved, "category": "grant", } kwargs = { "profile": profile, "subscription": subscription, "grant": subscription.grant, "activity_type": "new_grant_contribution", "metadata": metadata, } Activity.objects.create(**kwargs) print("Saved!\n") except Exception as e: print(e) print("\n")
def handle(self, *args, **kwargs): import csv with open('../scripts/input/givingblock_txns.csv', newline='', encoding="utf-8") as csvfile: reader = csv.reader(csvfile, delimiter=',', quotechar='"') for row in reader: #ingest data date = row[0] grant = row[1] _ = row[2] currency = row[3] amount = row[4] txid = row[5] user = row[6] usd_val = row[7] # convert formats try: date = date.split(" ")[0] date = timezone.datetime.strptime(date, '%m/%d/%Y') grant = grant.replace('®', '') grant = Grant.objects.get(title__icontains=grant.strip()) profile = Profile.objects.get(handle__iexact=user) # create objects validator_comment = ",".join(row) validator_comment = f"created by ingest givingblock_txns script: {validator_comment}" subscription = Subscription() subscription.is_postive_vote = True subscription.active = False subscription.error = True subscription.contributor_address = '/NA' subscription.amount_per_period = amount subscription.real_period_seconds = 2592000 subscription.frequency = 30 subscription.frequency_unit = 'N/A' subscription.token_address = '0x0' subscription.token_symbol = currency subscription.gas_price = 0 subscription.new_approve_tx_id = '0x0' subscription.num_tx_approved = 1 subscription.network = 'mainnet' subscription.contributor_profile = profile subscription.grant = grant subscription.comments = validator_comment subscription.amount_per_period_usdt = usd_val subscription.save() contrib = Contribution.objects.create( success=True, tx_cleared=True, tx_override=True, tx_id=txid, subscription=subscription, validator_passed=True, validator_comment=validator_comment, ) print(f"ingested {subscription.pk} / {contrib.pk}") metadata = { 'id': subscription.id, 'value_in_token': str(subscription.amount_per_period), 'value_in_usdt_now': str(round(subscription.amount_per_period_usdt, 2)), 'token_name': subscription.token_symbol, 'title': subscription.grant.title, 'grant_url': subscription.grant.url, 'num_tx_approved': subscription.num_tx_approved, 'category': 'grant', } kwargs = { 'profile': profile, 'subscription': subscription, 'grant': subscription.grant, 'activity_type': 'new_grant_contribution', 'metadata': metadata, } Activity.objects.create(**kwargs) except Exception as e: print(e)
def handle(self, *args, **options): # Parse inputs what = options['what'] network = options['network'] process_all = options['process_all'] valid_whats = [ 'finalize', 'payout_test', 'prepare_final_payout', 'verify', 'set_payouts_test', 'set_payouts' ] if what not in valid_whats: raise Exception(f"Invalid value {what} for 'what' arg") if network not in ['rinkeby', 'mainnet']: raise Exception(f"Invalid value {network} for 'network' arg") if not options['clr_round'] or not options['clr_pks']: raise Exception('Must provide clr_round and clr_pks') # Define parameters that vary by network. The expected total DAI amount uses the value here # if one is not available in the database from_block = 11466409 if network == 'mainnet' else 7731622 # block contract was deployed at dai_address = '0x6B175474E89094C44Da98b954EedeAC495271d0F' if network == 'mainnet' else '0x2e055eEe18284513B993dB7568A592679aB13188' expected_total_dai_amount = 100_000 if network == 'mainnet' else 5000 # in dollars, not wei, e.g. 500 = 500e18 # Get contract instances PROVIDER = "wss://" + network + ".infura.io/ws/v3/" + settings.INFURA_V3_PROJECT_ID w3 = Web3(Web3.WebsocketProvider(PROVIDER)) match_payouts = w3.eth.contract(address=match_payouts_address, abi=match_payouts_abi) dai = w3.eth.contract(address=dai_address, abi=erc20_abi) # Setup clr_round = options['clr_round'] clr_pks = options['clr_pks'].split(',') KYC_THRESHOLD = settings.GRANTS_PAYOUT_CLR_KYC_THRESHOLD # Get data gclrs = GrantCLR.objects.filter(pk__in=clr_pks) pks = [] for gclr in gclrs: pks += gclr.grants.values_list('pk', flat=True) scheduled_matches = CLRMatch.objects.filter(round_number=clr_round) grants = Grant.objects.filter(active=True, network='mainnet', is_clr_eligible=True, link_to_new_grant__isnull=True, pk__in=pks) print(f"got {grants.count()} grants") # Finalize rankings ------------------------------------------------------------------------ if what == 'finalize': total_owed_grants = 0 for grant in grants: try: for gclr in grant.clr_calculations.filter( grantclr__in=gclrs, latest=True): total_owed_grants += gclr.clr_prediction_curve[0][1] except: pass total_owed_matches = sum(sm.amount for sm in scheduled_matches) print( f"there are {grants.count()} grants to finalize worth ${round(total_owed_grants,2)}" ) print( f"there are {scheduled_matches.count()} Match Payments already created worth ${round(total_owed_matches,2)}" ) print('------------------------------') user_input = input("continue? (y/n) ") if user_input != 'y': return for grant in grants: amount = sum(ele.clr_prediction_curve[0][1] for ele in grant.clr_calculations.filter( grantclr__in=gclrs, latest=True)) has_already_kyc = grant.clr_matches.filter( has_passed_kyc=True).exists() if not amount: continue already_exists = scheduled_matches.filter(grant=grant).exists() if already_exists: continue needs_kyc = amount > KYC_THRESHOLD and not has_already_kyc comments = "" if not needs_kyc else "Needs KYC" ready_for_test_payout = not needs_kyc match = CLRMatch.objects.create( round_number=clr_round, amount=amount, grant=grant, comments=comments, ready_for_test_payout=ready_for_test_payout, ) if needs_kyc: grant_match_distribution_kyc(match) # Payout rankings (round must be finalized first) ------------------------------------------ if what in ['prepare_final_payout']: payout_matches = scheduled_matches.filter(ready_for_payout=False) payout_matches_amount = sum(sm.amount for sm in payout_matches) print( f"there are {payout_matches.count()} UNPAID Match Payments already created worth ${round(payout_matches_amount,2)} {network} DAI" ) print('------------------------------') user_input = input("continue? (y/n) ") if user_input != 'y': return for match in payout_matches: match.ready_for_payout = True match.save() print('promoted') # Set payouts (round must be finalized first) ---------------------------------------------- if what in ['set_payouts_test', 'set_payouts']: is_real_payout = what == 'set_payouts' kwargs = {} token_name = 'DAI' key = 'ready_for_test_payout' if not is_real_payout else 'ready_for_payout' kwargs[key] = False not_ready_scheduled_matches = scheduled_matches.filter(**kwargs) kwargs[key] = True kwargs2 = {} key2 = 'test_payout_tx' if not is_real_payout else 'payout_tx' kwargs2[key2] = '' unpaid_scheduled_matches = scheduled_matches.filter( **kwargs).filter(**kwargs2) paid_scheduled_matches = scheduled_matches.filter( **kwargs).exclude(**kwargs2) total_not_ready_matches = sum( sm.amount for sm in not_ready_scheduled_matches) total_owed_matches = sum(sm.amount for sm in unpaid_scheduled_matches) total_paid_matches = sum(sm.amount for sm in paid_scheduled_matches) print( f"there are {not_ready_scheduled_matches.count()} NOT READY Match Payments already created worth ${round(total_not_ready_matches,2)} {network} {token_name}" ) print( f"there are {unpaid_scheduled_matches.count()} UNPAID Match Payments already created worth ${round(total_owed_matches,2)} {network} {token_name}" ) print( f"there are {paid_scheduled_matches.count()} PAID Match Payments already created worth ${round(total_paid_matches,2)} {network} {token_name}" ) print('------------------------------') target_matches = unpaid_scheduled_matches if not process_all else scheduled_matches user_input = input("continue? (y/n) ") if user_input != 'y': return print( f"continuing with {target_matches.count()} unpaid scheduled payouts" ) if is_real_payout: user_input = input( F"THIS IS A REAL PAYOUT FOR {network} {token_name}. ARE YOU DOUBLE SECRET SUPER SURE? (y/n) " ) if user_input != 'y': return # Generate dict of payout mapping that we'll use to set the contract's payout mapping full_payouts_mapping_dict = {} for match in target_matches.order_by('amount'): # Amounts to set recipient = w3.toChecksumAddress(match.grant.admin_address) amount = Decimal(match.amount) * SCALE # convert to wei # This ensures that even when multiple grants have the same receiving address, # all match funds are accounted for if recipient in full_payouts_mapping_dict.keys(): full_payouts_mapping_dict[recipient] += amount else: full_payouts_mapping_dict[recipient] = amount # Convert dict to array to use it as inputs to the contract full_payouts_mapping = [] for key, value in full_payouts_mapping_dict.items(): full_payouts_mapping.append([key, str(int(value))]) total_amount = sum(int(ele[1]) for ele in full_payouts_mapping) # In tests, it took 68,080 gas to set 2 payout values. Let's be super conservative # and say it's 50k gas per payout mapping. If we are ok using 6M gas per transaction, # that means we can set 6M / 50k = 120 payouts per transaction. So we chunk the # payout mapping into sub-arrays with max length of 120 each # KO 12/21 - edited with Matt to make 2.1x that def chunks(lst, n): """Yield successive n-sized chunks from lst. https://stackoverflow.com/a/312464""" for i in range(0, len(lst), n): yield lst[i:i + n] chunk_size = 250 if not settings.DEBUG else 120 chunked_payouts_mapping = chunks(full_payouts_mapping, chunk_size) # Set payouts from_address = settings.GRANTS_PAYOUT_ADDRESS from_pk = settings.GRANTS_PAYOUT_PRIVATE_KEY for payout_mapping in chunked_payouts_mapping: #tx = match_payouts.functions.setPayouts(payout_mapping).buildTransaction(tx_args) print( f"#TODO: Send this txn view etherscan {match_payouts_address}" ) print(json.dumps(payout_mapping)) # Pause until the next one print("SLEEPING") time.sleep(WAIT_TIME_BETWEEN_TXS) print("DONE SLEEPING") user_input = input("continue? (y/n) ") if user_input != 'y': return tx_id = input("enter a txid: ") # All payouts have been successfully set, so now we update the database for match in target_matches.order_by('amount'): # make save state to DB if is_real_payout: match.payout_tx = tx_id match.payout_tx_date = timezone.now() grant_match_distribution_final_txn(match, True) else: match.test_payout_tx = tx_id match.test_payout_tx_date = timezone.now() #grant_match_distribution_test_txn(match) match.save() # create payout obj artifacts profile = Profile.objects.get(handle__iexact='gitcoinbot') validator_comment = f"created by ingest payout_round_script" subscription = Subscription() subscription.is_postive_vote = True subscription.active = False subscription.error = True subscription.contributor_address = 'N/A' subscription.amount_per_period = match.amount subscription.real_period_seconds = 2592000 subscription.frequency = 30 subscription.frequency_unit = 'N/A' subscription.token_address = dai_address subscription.token_symbol = token_name subscription.gas_price = 0 subscription.new_approve_tx_id = '0x0' subscription.num_tx_approved = 1 subscription.network = network subscription.contributor_profile = profile subscription.grant = match.grant subscription.comments = validator_comment subscription.amount_per_period_usdt = match.amount if is_real_payout else 0 subscription.save() contrib = Contribution.objects.create( success=True, tx_cleared=True, tx_override=True, tx_id=tx_id, subscription=subscription, validator_passed=True, validator_comment=validator_comment, ) print(f"ingested {subscription.pk} / {contrib.pk}") if is_real_payout: match.payout_contribution = contrib else: match.test_payout_contribution = contrib match.save() metadata = { 'id': subscription.id, 'value_in_token': str(subscription.amount_per_period), 'value_in_usdt_now': str(round(subscription.amount_per_period_usdt, 2)), 'token_name': subscription.token_symbol, 'title': subscription.grant.title, 'grant_url': subscription.grant.url, 'num_tx_approved': subscription.num_tx_approved, 'category': 'grant', } kwargs = { 'profile': profile, 'subscription': subscription, 'grant': subscription.grant, 'activity_type': 'new_grant_contribution', 'metadata': metadata, } activity = Activity.objects.create(**kwargs) if is_real_payout: comment = f"CLR Round {clr_round} Payout" comment = Comment.objects.create(profile=profile, activity=activity, comment=comment) # Verify contract is set properly ---------------------------------------------------------- if what == 'verify': # Get expected total match amount total_owed_grants = 0 for grant in grants: try: for gclr in grant.clr_calculations.filter( grantclr__in=gclrs, latest=True): total_owed_grants += gclr.clr_prediction_curve[0][1] except: pass expected_total_dai_amount = sum(sm.amount for sm in scheduled_matches) # Get PayoutAdded events payout_added_filter = match_payouts.events.PayoutAdded.createFilter( fromBlock=from_block) payout_added_logs = payout_added_filter.get_all_entries( ) # print these if you need to inspect them # Sort payout logs by ascending block number, this way if a recipient appears in multiple blocks # we use the value from the latest block sorted_payout_added_logs = sorted( payout_added_logs, key=lambda log: log['blockNumber'], reverse=False) # Get total required DAI balance based on PayoutAdded events. Events will be sorted chronologically, # so if a recipient is duplicated we only keep the latest entry. We do this by storing our own # mapping from recipients to match amount and overwriting it as needed just like the contract would. # We keep another dict that maps the recipient's addresses to the block it was found in. If we find # two entries for the same user in the same block, we throw, since we don't know which is the # correct one payment_dict = {} user_block_dict = {} for log in sorted_payout_added_logs: # Parse parameters from logs recipient = log['args']['recipient'] amount = Decimal(log['args']['amount']) block = log['blockNumber'] # Check if recipient's payout has already been set in this block if recipient in user_block_dict and user_block_dict[ recipient] == block: raise Exception( f'Recipient {recipient} payout was set twice in block {block}, so unclear which to use' ) # Recipient not seen in this block, so save data payment_dict[recipient] = amount user_block_dict[recipient] = block # Sum up each entry to get the total required amount total_dai_required_wei = sum(payment_dict[recipient] for recipient in payment_dict.keys()) # Convert to human units total_dai_required = total_dai_required_wei / SCALE # Verify that total DAI required (from event logs) equals the expected amount if round(expected_total_dai_amount, 0) != round( total_dai_required, 0): print( '\n* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *' ) print( 'Total DAI payout amount in the contract does not equal the expected value!' ) print(' Total expected amount: ', expected_total_dai_amount) print(' Total amount from logs: ', total_dai_required) print( '* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n' ) raise Exception( 'Total payout amount in the contract does not equal the expected value!' ) print('Total payout amount in the contracts is the expected value') # Get contract DAI balance dai_balance = Decimal( dai.functions.balanceOf(match_payouts_address).call()) / SCALE # Verify that contract has sufficient DAI balance to cover all payouts print( '\n* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *' ) if dai_balance == total_dai_required: print( f'Contract balance of {dai_balance} DAI is exactly equal to the required amount' ) elif dai_balance < total_dai_required: shortage = total_dai_required - dai_balance print('Contract DAI balance is insufficient') print(' Required balance: ', total_dai_required) print(' Current balance: ', dai_balance) print(' Extra DAI needed: ', shortage) print(f'\n Contract needs another {shortage} DAI') elif dai_balance > total_dai_required: excess = dai_balance - total_dai_required print('Contract has excess DAI balance') print(' Required balance: ', total_dai_required) print(' Current balance: ', dai_balance) print(' Excess DAI amount: ', excess) print(f'\n Contract has an excess of {excess} DAI') print( '* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n' )