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")
def process_grant_contribution(self, grant_id, grant_slug, profile_id, package, retry: bool = True) -> None: """ :param self: :param grant_id: :param grant_slug: :param profile_id: :param package: :return: """ grant = Grant.objects.get(pk=grant_id) profile = Profile.objects.get(pk=profile_id) if 'contributor_address' in package: subscription = Subscription() if grant.negative_voting_enabled: #is_postive_vote = True if package.get('is_postive_vote', 1) else False is_postive_vote = package.get('match_direction', '+') == '+' else: is_postive_vote = True subscription.is_postive_vote = is_postive_vote fee_pct = float(package.get('gitcoin-grant-input-amount', 0)) subscription.active = False subscription.contributor_address = package.get('contributor_address', '') subscription.amount_per_period = package.get('amount_per_period', 0) subscription.real_period_seconds = package.get('real_period_seconds', 2592000) subscription.frequency = package.get('frequency', 30) subscription.frequency_unit = package.get('frequency_unit', 'days') subscription.token_address = package.get('token_address', '') subscription.token_symbol = package.get('token_symbol', '') subscription.gas_price = (float(subscription.amount_per_period) * (fee_pct / 100)) subscription.new_approve_tx_id = package.get('sub_new_approve_tx_id', '0x0') subscription.split_tx_id = package.get('split_tx_id', '0x0') subscription.num_tx_approved = package.get('num_tx_approved', 1) subscription.network = package.get('network', '') subscription.contributor_profile = profile subscription.grant = grant subscription.comments = package.get('comment', '') subscription.save() # one time payments activity = None if int(subscription.num_tx_approved) == 1: subscription.successful_contribution( subscription.new_approve_tx_id) subscription.error = True #cancel subs so it doesnt try to bill again subscription.subminer_comments = "skipping subminer bc this is a 1 and done subscription, and tokens were alredy sent" subscription.save() activity = record_subscription_activity_helper( 'new_grant_contribution', subscription, profile) else: activity = record_subscription_activity_helper( 'new_grant_subscription', subscription, profile) if 'comment' in package: _profile = profile comment = package.get('comment') if comment and activity: if subscription and subscription.negative: _profile = Profile.objects.filter( handle='gitcoinbot').first() comment = f"Comment from contributor: {comment}" comment = Comment.objects.create(profile=_profile, activity=activity, comment=comment) if 'hide_wallet_address' in package: profile.hide_wallet_address = bool( package.get('hide_wallet_address', False)) profile.save() new_supporter(grant, subscription) thank_you_for_supporting(grant, subscription) update_grant_metadata.delay(grant_id)
to = to_address grant = Grant.objects.filter(admin_address__iexact=to).order_by( '-positive_round_contributor_count').first() #ingest data currency = symbol amount = value usd_val = amount * convert_token_to_usdt(symbol) # convert formats date = timezone.now() # 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 = '/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
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 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, **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' )
def process_grant_contribution(self, grant_id, grant_slug, profile_id, package, send_supporter_mail: bool = True, retry: bool = True): """ :param self: :param grant_id: :param grant_slug: :param profile_id: :param package: :param send_supporter_mail: :return: """ from grants.views import record_subscription_activity_helper grant = Grant.objects.get(pk=grant_id) profile = Profile.objects.get(pk=profile_id) if 'contributor_address' in package: subscription = Subscription() if grant.negative_voting_enabled: #is_postive_vote = True if package.get('is_postive_vote', 1) else False is_postive_vote = package.get('match_direction', '+') == '+' else: is_postive_vote = True subscription.is_postive_vote = is_postive_vote fee_pct = float(package.get('gitcoin-grant-input-amount', 0)) subscription.active = False subscription.contributor_address = package.get('contributor_address', '') subscription.amount_per_period = package.get('amount_per_period', 0) subscription.real_period_seconds = package.get('real_period_seconds', 2592000) subscription.frequency = package.get('frequency', 30) subscription.frequency_unit = package.get('frequency_unit', 'days') subscription.token_address = package.get('token_address', '') subscription.token_symbol = package.get('token_symbol', '') subscription.gas_price = (float(subscription.amount_per_period) * (fee_pct / 100)) subscription.new_approve_tx_id = package.get('sub_new_approve_tx_id', '0x0') subscription.split_tx_id = package.get('split_tx_id', '0x0') subscription.num_tx_approved = package.get('num_tx_approved', 1) subscription.network = package.get('network', '') subscription.visitorId = package.get('visitorId', '') if subscription.network == 'undefined': # we unfortunately cannot trust the frontend to give us a valid network name # so this handles that case. more details are available at # https://gitcoincore.slack.com/archives/C01FQV4FX4J/p1607980714026400 if not settings.DEBUG: subscription.network = 'mainnet' subscription.contributor_profile = profile subscription.grant = grant subscription.comments = package.get('comment', '') subscription.save() value_usdt = subscription.get_converted_amount(True) include_for_clr = package.get('include_for_clr') if value_usdt < 1 or subscription.contributor_profile.shadowbanned: include_for_clr = False subscription.successful_contribution(subscription.new_approve_tx_id, include_for_clr, checkout_type=package.get( 'checkout_type', None)) # one time payments activity = None subscription.error = True #cancel subs so it doesnt try to bill again subscription.subminer_comments = "skipping subminer bc this is a 1 and done subscription, and tokens were alredy sent" subscription.save() if 'hide_wallet_address' in package: profile.hide_wallet_address = bool( package.get('hide_wallet_address', False)) profile.save() if 'anonymize_gitcoin_grants_contributions' in package: profile.anonymize_gitcoin_grants_contributions = package.get( 'anonymize_gitcoin_grants_contributions') profile.save() activity_profile = profile if not profile.anonymize_gitcoin_grants_contributions else Profile.objects.get( handle='gitcoinbot') activity = record_subscription_activity_helper( 'new_grant_contribution', subscription, activity_profile) if 'comment' in package: _profile = profile comment = package.get('comment') if value_usdt >= 1 and comment and activity: if profile.anonymize_gitcoin_grants_contributions: _profile = Profile.objects.filter( handle='gitcoinbot').first() comment = f"Comment from contributor: {comment}" comment = Comment.objects.create(profile=_profile, activity=activity, comment=comment) # emails to contributor if value_usdt >= 1 and send_supporter_mail: grants_with_subscription = [{ 'grant': grant, 'subscription': subscription }] try: thank_you_for_supporting(grants_with_subscription) except Exception as e: logger.exception(e) update_grant_metadata.delay(grant_id) return grant, subscription
def grant_fund(request, grant_id, grant_slug): """Handle grant funding.""" try: grant = Grant.objects.get(pk=grant_id, slug=grant_slug) except Grant.DoesNotExist: raise Http404 profile = get_profile(request) if not grant.active: params = { 'active': 'grant_error', 'title': _('Fund - Grant Ended'), 'grant': grant, 'text': _('This Grant has ended.'), 'subtext': _('Contributions can no longer be made this grant') } return TemplateResponse(request, 'grants/shared/error.html', params) if is_grant_team_member(grant, profile): params = { 'active': 'grant_error', 'title': _('Fund - Grant funding blocked'), 'grant': grant, 'text': _('This Grant cannot be funded'), 'subtext': _('Grant team members cannot contribute to their own grant.') } return TemplateResponse(request, 'grants/shared/error.html', params) if grant.link_to_new_grant: params = { 'active': 'grant_error', 'title': _('Fund - Grant Migrated'), 'grant': grant.link_to_new_grant, 'text': f'This Grant has ended', 'subtext': 'Contributions can no longer be made to this grant. <br> Visit the new grant to contribute.', 'button_txt': 'View New Grant' } return TemplateResponse(request, 'grants/shared/error.html', params) active_subscription = Subscription.objects.select_related('grant').filter( grant=grant_id, active=True, error=False, contributor_profile=request.user.profile, is_postive_vote=True ) if active_subscription: params = { 'active': 'grant_error', 'title': _('Subscription Exists'), 'grant': grant, 'text': _('You already have an active subscription for this grant.') } return TemplateResponse(request, 'grants/shared/error.html', params) if grant.contract_address == '0x0': messages.info( request, _('This grant is not configured to accept funding at this time. Please contact [email protected] if you believe this message is in error!') ) logger.error(f"Grant {grant.pk} is not properly configured for funding. Please set grant.contract_address on this grant") return redirect(reverse('grants:details', args=(grant.pk, grant.slug))) if request.method == 'POST': if 'contributor_address' in request.POST: subscription = Subscription() if grant.negative_voting_enabled: #is_postive_vote = True if request.POST.get('is_postive_vote', 1) else False is_postive_vote = request.POST.get('match_direction', '+') == '+' else: is_postive_vote = True subscription.is_postive_vote = is_postive_vote subscription.active = False subscription.contributor_address = request.POST.get('contributor_address', '') subscription.amount_per_period = request.POST.get('amount_per_period', 0) subscription.real_period_seconds = request.POST.get('real_period_seconds', 2592000) subscription.frequency = request.POST.get('frequency', 30) subscription.frequency_unit = request.POST.get('frequency_unit', 'days') subscription.token_address = request.POST.get('token_address', '') subscription.token_symbol = request.POST.get('token_symbol', '') subscription.gas_price = request.POST.get('gas_price', 0) subscription.new_approve_tx_id = request.POST.get('sub_new_approve_tx_id', '0x0') subscription.num_tx_approved = request.POST.get('num_tx_approved', 1) subscription.network = request.POST.get('network', '') subscription.contributor_profile = profile subscription.grant = grant subscription.comments = request.POST.get('comment', '') subscription.save() # one time payments activity = None if int(subscription.num_tx_approved) == 1: subscription.successful_contribution(subscription.new_approve_tx_id); subscription.error = True #cancel subs so it doesnt try to bill again subscription.subminer_comments = "skipping subminer bc this is a 1 and done subscription, and tokens were alredy sent" subscription.save() activity = record_subscription_activity_helper('new_grant_contribution', subscription, profile) else: activity = record_subscription_activity_helper('new_grant_subscription', subscription, profile) if 'comment' in request.POST: comment = request.POST.get('comment') if comment and activity: profile = request.user.profile if subscription and subscription.negative: profile = Profile.objects.filter(handle='gitcoinbot').first() comment = f"Comment from contributor: {comment}" comment = Comment.objects.create( profile=profile, activity=activity, comment=comment) message = 'Your contribution has succeeded. Thank you for supporting Public Goods on Gitcoin !' if request.session.get('send_notification'): msg_html = request.session.get('msg_html') cta_text = request.session.get('cta_text') cta_url = request.session.get('cta_url') to_user = request.user send_notification_to_user_from_gitcoinbot(to_user, cta_url, cta_text, msg_html) if int(subscription.num_tx_approved) > 1: message = 'Your subscription has been created. It will bill within the next 5 minutes or so. Thank you for supporting Public Goods on Gitcoin !' messages.info( request, message ) return JsonResponse({ 'success': True, }) if 'hide_wallet_address' in request.POST: profile.hide_wallet_address = bool(request.POST.get('hide_wallet_address', False)) profile.save() if 'signature' in request.POST: sub_new_approve_tx_id = request.POST.get('sub_new_approve_tx_id', '') subscription = Subscription.objects.filter(new_approve_tx_id=sub_new_approve_tx_id).first() subscription.active = True subscription.subscription_hash = request.POST.get('subscription_hash', '') subscription.contributor_signature = request.POST.get('signature', '') if 'split_tx_id' in request.POST: subscription.split_tx_id = request.POST.get('split_tx_id', '') subscription.save_split_tx_to_contribution() if 'split_tx_confirmed' in request.POST: subscription.split_tx_confirmed = bool(request.POST.get('split_tx_confirmed', False)) subscription.save_split_tx_to_contribution() subscription.save() value_usdt = subscription.get_converted_amount() if value_usdt: grant.monthly_amount_subscribed += subscription.get_converted_monthly_amount() grant.save() if not subscription.negative: new_supporter(grant, subscription) thank_you_for_supporting(grant, subscription) return JsonResponse({ 'success': True, 'url': reverse('grants:details', args=(grant.pk, grant.slug)) }) # handle phantom funding active_tab = 'normal' fund_reward = None round_number = clr_round can_phantom_fund = request.user.is_authenticated and request.user.groups.filter(name='phantom_funders_round_5').exists() and clr_active phantom_funds = PhantomFunding.objects.filter(profile=request.user.profile, round_number=round_number).order_by('created_on').nocache() if request.user.is_authenticated else PhantomFunding.objects.none() is_phantom_funding_this_grant = can_phantom_fund and phantom_funds.filter(grant=grant).exists() show_tweet_modal = False fund_reward = get_fund_reward(request, grant) if can_phantom_fund and request.POST.get('toggle_phantom_fund'): if is_phantom_funding_this_grant: msg = "You are no longer signaling for this grant." phantom_funds.filter(grant=grant).delete() else: msg = "You are now signaling for this grant." show_tweet_modal = True pt = PhantomFunding.objects.create(grant=grant, profile=request.user.profile, round_number=round_number) record_grant_activity_helper('new_grant_contribution', grant, request.user.profile, amount=pt.value, token='DAI') messages.info( request, msg ) is_phantom_funding_this_grant = not is_phantom_funding_this_grant images = [ 'new.svg', 'torchbearer.svg', 'robots.png', 'profile/fund.svg', ] img = random.choice(images) params = { 'profile': profile, 'active': 'fund_grant', 'title': matching_live + grant.title + " | Fund Now", 'card_desc': grant.description, 'avatar_url': grant.logo.url if grant.logo else None, 'subscription': {}, 'show_tweet_modal': show_tweet_modal, 'direction': request.GET.get('direction', '+'), 'grant_has_no_token': True if grant.token_address == '0x0000000000000000000000000000000000000000' else False, 'grant': grant, 'img': img, 'clr_prediction_curve': [c[1] for c in grant.clr_prediction_curve] if grant.clr_prediction_curve and len(grant.clr_prediction_curve[0]) > 1 else [0, 0, 0, 0, 0, 0], 'keywords': get_keywords(), 'recommend_gas_price': recommend_min_gas_price_to_confirm_in_time(4), 'recommend_gas_price_slow': recommend_min_gas_price_to_confirm_in_time(120), 'recommend_gas_price_avg': recommend_min_gas_price_to_confirm_in_time(15), 'recommend_gas_price_fast': recommend_min_gas_price_to_confirm_in_time(1), 'eth_usd_conv_rate': eth_usd_conv_rate(), 'conf_time_spread': conf_time_spread(), 'gas_advisories': gas_advisories(), 'splitter_contract_address': settings.SPLITTER_CONTRACT_ADDRESS, 'gitcoin_donation_address': settings.GITCOIN_DONATION_ADDRESS, 'can_phantom_fund': can_phantom_fund, 'is_phantom_funding_this_grant': is_phantom_funding_this_grant, 'active_tab': active_tab, 'fund_reward': fund_reward, 'phantom_funds': phantom_funds, 'clr_round': clr_round, 'clr_active': clr_active, 'total_clr_pot': total_clr_pot, } return TemplateResponse(request, 'grants/fund.html', params)