def execute_deposit(transaction: Transaction) -> bool: """ The external deposit has been completed, so the transaction status must now be updated to *pending_anchor*. Executes the transaction by calling :func:`create_stellar_deposit`. :param transaction: the transaction to be executed :returns a boolean of whether or not the transaction was completed successfully on the Stellar network. """ if transaction.kind != transaction.KIND.deposit: raise ValueError("Transaction not a deposit") elif transaction.status != transaction.STATUS.pending_user_transfer_start: raise ValueError( f"Unexpected transaction status: {transaction.status}, expecting " f"{transaction.STATUS.pending_user_transfer_start}") elif transaction.amount_fee is None: if registered_fee_func == calculate_fee: transaction.amount_fee = calculate_fee({ "amount": transaction.amount_in, "operation": settings.OPERATION_DEPOSIT, "asset_code": transaction.asset.code, }) else: transaction.amount_fee = Decimal(0) transaction.status = Transaction.STATUS.pending_anchor transaction.status_eta = 5 # Ledger close time. transaction.save() logger.info( f"Transaction {transaction.id} now pending_anchor, initiating deposit") # launch the deposit Stellar transaction. return create_stellar_deposit(transaction.id)
def poll_pending_deposits(self, pending_deposits: QuerySet) -> List[Transaction]: """ Anchors should implement their banking rails here, as described in the :class:`.RailsIntegration` docstrings. This implementation interfaces with a fake banking rails client for demonstration purposes. """ # interface with mock banking rails ready_deposits = [] mock_bank_account_id = "XXXXXXXXXXXXX" client = rails.BankAPIClient(mock_bank_account_id) for deposit in pending_deposits: bank_deposit = client.get_deposit(deposit=deposit) if bank_deposit and bank_deposit.status == "complete": if not deposit.amount_in: deposit.amount_in = Decimal(103) deposit.amount_fee = calculate_fee({ "amount": deposit.amount_in, "operation": settings.OPERATION_DEPOSIT, "asset_code": deposit.asset.code, }) deposit.save() ready_deposits.append(deposit) return ready_deposits
def get_ready_deposits() -> List[Transaction]: pending_deposits = Transaction.objects.filter( status__in=[ Transaction.STATUS.pending_user_transfer_start, Transaction.STATUS.pending_external, ], kind=Transaction.KIND.deposit, ) ready_transactions = rri.poll_pending_deposits(pending_deposits) for transaction in ready_transactions: if transaction.kind != Transaction.KIND.deposit: raise ValueError( "A non-deposit Transaction was returned from poll_pending_deposits()" ) elif transaction.amount_in is None: raise ValueError( "poll_pending_deposits() did not assign a value to the " "amount_in field of a Transaction object returned" ) elif transaction.amount_fee is None: if registered_fee_func is calculate_fee: transaction.amount_fee = calculate_fee( { "amount": transaction.amount_in, "operation": settings.OPERATION_DEPOSIT, "asset_code": transaction.asset.code, } ) else: transaction.amount_fee = Decimal(0) transaction.save() return ready_transactions
def execute_outgoing_transaction(self, transaction: Transaction): def error(): transaction.status = Transaction.STATUS.error transaction.status_message = ( f"Unable to find user info for transaction {transaction.id}") transaction.save() user_transaction = PolarisUserTransaction.objects.filter( transaction_id=transaction.id).first() if not user_transaction: # something is wrong with our user tracking code error() return # SEP31 users don't have stellar accounts, so check the user column on the transaction. # Since that is a new column, it may be None. If so, use the account's user column if user_transaction.user: user = user_transaction.user else: user = getattr(user_transaction.account, "user", None) if not user: # something is wrong with our user tracking code error() return client = rails.BankAPIClient("fake anchor bank account number") transaction.amount_fee = calculate_fee({ "amount": transaction.amount_in, "operation": settings.OPERATION_DEPOSIT, "asset_code": transaction.asset.code, }) response = client.send_funds( to_account=user.bank_account_number, amount=transaction.amount_in - transaction.amount_fee, ) if response["success"]: transaction.status = Transaction.STATUS.pending_external else: # Parse a mock bank API response to demonstrate how an anchor would # report back to the sending anchor which fields needed updating. error_fields = response.error.fields info_fields = MySEP31ReceiverIntegration().info(transaction.asset) required_info_update = defaultdict(dict) for field in error_fields: if "name" in field: required_info_update["receiver"][field] = info_fields[ "receiver"][field] elif "account" in field: required_info_update["transaction"][field] = info_fields[ "receiver"][field] transaction.required_info_update = json.dumps(required_info_update) transaction.required_info_message = response.error.message transaction.status = Transaction.STATUS.pending_transaction_info_update transaction.save()
def fee_integration(fee_params: Dict) -> Decimal: """ This function replaces the default registered_fee_func for demonstration purposes. However, since we don't have any custom logic to implement, it simply calls the default that has been replaced. """ return calculate_fee(fee_params)
def get_ready_deposits(cls) -> List[Transaction]: pending_deposits = Transaction.objects.filter( status__in=[ Transaction.STATUS.pending_user_transfer_start, Transaction.STATUS.pending_external, ], kind=Transaction.KIND.deposit, pending_execution_attempt=False, ).select_for_update() with django.db.transaction.atomic(): ready_transactions = rri.poll_pending_deposits(pending_deposits) Transaction.objects.filter( id__in=[t.id for t in ready_transactions]).update( pending_execution_attempt=True) verified_ready_transactions = [] for transaction in ready_transactions: # refresh from DB to pull pending_execution_attempt value and to ensure invalid # values were not assigned to the transaction in rri.poll_pending_deposits() transaction.refresh_from_db() if transaction.kind != transaction.KIND.deposit: cls.handle_error( transaction, "poll_pending_deposits() returned a non-deposit transaction", ) continue if transaction.amount_in is None: cls.handle_error( transaction, "poll_pending_deposits() did not assign a value to the " "amount_in field of a Transaction object returned", ) continue elif transaction.amount_fee is None: if registered_fee_func is calculate_fee: try: transaction.amount_fee = calculate_fee({ "amount": transaction.amount_in, "operation": settings.OPERATION_DEPOSIT, "asset_code": transaction.asset.code, }) except ValueError as e: cls.handle_error(transaction, str(e)) continue else: transaction.amount_fee = Decimal(0) transaction.save() verified_ready_transactions.append(transaction) return verified_ready_transactions
def after_form_validation(self, form: forms.Form, transaction: Transaction): try: SEP24KYC.track_user_activity(form, transaction) except RuntimeError: # Since no polaris account exists for this transaction, KYCForm # will be returned from the next form_for_transaction() call logger.exception( f"KYCForm was not served first for unknown account, id: " f"{transaction.stellar_account}") if isinstance(form, TransactionForm): transaction.amount_fee = calculate_fee({ "amount": form.cleaned_data["amount"], "operation": "withdraw", "asset_code": transaction.asset.code, }) transaction.amount_out = round( form.cleaned_data["amount"] - transaction.amount_fee, transaction.asset.significant_decimals, ) transaction.save()
def execute_outgoing_transactions(): """ Execute pending withdrawals. """ module = sys.modules[__name__] sep31_qparams = Q( protocol=Transaction.PROTOCOL.sep31, status=Transaction.STATUS.pending_receiver, kind=Transaction.KIND.send, ) sep6_24_qparams = Q( protocol__in=[ Transaction.PROTOCOL.sep24, Transaction.PROTOCOL.sep6 ], status=Transaction.STATUS.pending_anchor, kind=Transaction.KIND.withdrawal, ) with django.db.transaction.atomic(): transactions = list( Transaction.objects.filter( sep6_24_qparams | sep31_qparams, pending_execution_attempt=False).select_for_update()) ids = [] for t in transactions: t.pending_execution_attempt = True ids.append(t.id) Transaction.objects.filter(id__in=ids).update( pending_execution_attempt=True) if transactions: logger.info(f"Executing {len(transactions)} outgoing transactions") num_completed = 0 for i, transaction in enumerate(transactions): if module.TERMINATE: still_processing_transactions = transactions[i:] Transaction.objects.filter( id__in=[t.id for t in still_processing_transactions]).update( pending_execution_attempt=False) break logger.info( f"Calling execute_outgoing_transaction() for {transaction.id}") try: rri.execute_outgoing_transaction(transaction) except Exception: transaction.pending_execution_attempt = False transaction.save() logger.exception( "execute_outgoing_transactions() threw an unexpected exception" ) continue transaction.refresh_from_db() if (transaction.protocol == Transaction.PROTOCOL.sep31 and transaction.status == Transaction.STATUS.pending_receiver ) or (transaction.protocol in [ Transaction.PROTOCOL.sep24, Transaction.PROTOCOL.sep6 ] and transaction.status == transaction.STATUS.pending_anchor): transaction.pending_execution_attempt = False transaction.save() logger.error( f"Transaction {transaction.id} status must be " f"updated after call to execute_outgoing_transaction()") continue elif transaction.status in [ Transaction.STATUS.pending_external, Transaction.STATUS.completed, ]: if transaction.amount_fee is None: if registered_fee_func is calculate_fee: op = { Transaction.KIND.withdrawal: settings.OPERATION_WITHDRAWAL, Transaction.KIND.send: Transaction.KIND.send, }[transaction.kind] try: transaction.amount_fee = calculate_fee({ "amount": transaction.amount_in, "operation": op, "asset_code": transaction.asset.code, }) except ValueError: transaction.pending_execution_attempt = False transaction.save() logger.exception("Unable to calculate fee") continue else: transaction.amount_fee = Decimal(0) transaction.amount_out = transaction.amount_in - transaction.amount_fee # Anchors can mark transactions as pending_external if the transfer # cannot be completed immediately due to external processing. # poll_outgoing_transactions will check on these transfers and mark them # as complete when the funds have been received by the user. if transaction.status == Transaction.STATUS.completed: num_completed += 1 transaction.completed_at = datetime.now(timezone.utc) elif transaction.status not in [ Transaction.STATUS.error, Transaction.STATUS.pending_transaction_info_update, Transaction.STATUS.pending_customer_info_update, ]: transaction.pending_execution_attempt = False transaction.save() logger.error( f"Transaction {transaction.id} was moved to invalid status" f" {transaction.status}") continue transaction.pending_execution_attempt = False transaction.save() maybe_make_callback(transaction) if num_completed: logger.info(f"{num_completed} transfers have been completed")
def execute_deposits(cls): module = sys.modules[__name__] pending_deposits = Transaction.objects.filter( status__in=[ Transaction.STATUS.pending_user_transfer_start, Transaction.STATUS.pending_external, ], kind=Transaction.KIND.deposit, ) try: ready_transactions = rri.poll_pending_deposits(pending_deposits) except Exception: # pragma: no cover logger.exception("poll_pending_deposits() threw an unexpected exception") return if ready_transactions is None: raise CommandError( "poll_pending_deposits() returned None. " "Ensure is returns a list of transaction objects." ) for transaction in ready_transactions: if module.TERMINATE: break elif transaction.kind != Transaction.KIND.deposit: raise CommandError( "A non-deposit Transaction was returned from poll_pending_deposits()" ) elif transaction.amount_in is None: raise CommandError( "poll_pending_deposits() did not assign a value to the " "amount_in field of a Transaction object returned" ) elif transaction.amount_fee is None: if registered_fee_func is calculate_fee: transaction.amount_fee = calculate_fee( { "amount": transaction.amount_in, "operation": settings.OPERATION_DEPOSIT, "asset_code": transaction.asset.code, } ) else: transaction.amount_fee = Decimal(0) logger.info("calling get_or_create_transaction_destination_account()") try: ( _, created, pending_trust, ) = get_or_create_transaction_destination_account(transaction) except RuntimeError as e: transaction.status = Transaction.STATUS.error transaction.status_message = str(e) transaction.save() logger.error(transaction.status_message) continue # Transaction.status == pending_trust, wait for client # to add trustline for asset to send if created or pending_trust: logger.info( f"destination account is pending_trust for transaction {transaction.id}" ) if ( pending_trust and transaction.status != Transaction.STATUS.pending_trust ): transaction.status = Transaction.STATUS.pending_trust transaction.save() continue if check_for_multisig(transaction): # Now Polaris waits for signatures to be collected by the anchor continue cls.execute_deposit(transaction) ready_multisig_transactions = Transaction.objects.filter( kind=Transaction.KIND.deposit, status=Transaction.STATUS.pending_anchor, pending_signatures=False, envelope_xdr__isnull=False, ) for t in ready_multisig_transactions: cls.execute_deposit(t)
def execute_deposits(cls): module = sys.modules[__name__] pending_deposits = Transaction.objects.filter( status__in=[ Transaction.STATUS.pending_user_transfer_start, Transaction.STATUS.pending_external, ], kind=Transaction.KIND.deposit, ) try: ready_transactions = rri.poll_pending_deposits(pending_deposits) except Exception: # pragma: no cover logger.exception("poll_pending_deposits() threw an unexpected exception") return if ready_transactions is None: raise CommandError( "poll_pending_deposits() returned None. " "Ensure is returns a list of transaction objects." ) for transaction in ready_transactions: if module.TERMINATE: break elif transaction.kind != Transaction.KIND.deposit: raise CommandError( "A non-deposit Transaction was returned from poll_pending_deposits()" ) elif transaction.amount_in is None: raise CommandError( "poll_pending_deposits() did not assign a value to the " "amount_in field of a Transaction object returned" ) elif transaction.amount_fee is None: if registered_fee_func is calculate_fee: transaction.amount_fee = calculate_fee( { "amount": transaction.amount_in, "operation": settings.OPERATION_DEPOSIT, "asset_code": transaction.asset.code, } ) else: transaction.amount_fee = Decimal(0) try: ( _, created, pending_trust, ) = get_or_create_transaction_destination_account(transaction) except RuntimeError as e: transaction.status = Transaction.STATUS.error transaction.status_message = str(e) transaction.save() logger.error(transaction.status_message) continue if ( created or pending_trust ) and not transaction.claimable_balance_supported: # after checking/creating the account, we discovered the transaction # doesn't have a trustline. # And the transaction is does not support claimable balances # Transaction.status is definitely not # pending_trust yet because only the transactions with these statuses # are queried: # - Transaction.STATUS.pending_user_transfer_start # - Transaction.STATUS.pending_external logger.info( f"destination account is pending_trust for transaction {transaction.id}" ) transaction.status = Transaction.STATUS.pending_trust transaction.save() continue elif check_for_multisig(transaction): # We still have to check if the transaction requires additional # signatures. # If so we want to skip current transaction's execute_deposit call continue cls.execute_deposit(transaction) ready_multisig_transactions = Transaction.objects.filter( kind=Transaction.KIND.deposit, status=Transaction.STATUS.pending_anchor, pending_signatures=False, envelope_xdr__isnull=False, ) for t in ready_multisig_transactions: cls.execute_deposit(t)
def execute_outgoing_transactions(): """ Execute pending withdrawals. """ module = sys.modules[__name__] sep31_qparams = Q( protocol=Transaction.PROTOCOL.sep31, status=Transaction.STATUS.pending_receiver, kind=Transaction.KIND.send, ) sep6_24_qparams = Q( protocol__in=[ Transaction.PROTOCOL.sep24, Transaction.PROTOCOL.sep6 ], status=Transaction.STATUS.pending_anchor, kind=Transaction.KIND.withdrawal, ) transactions = Transaction.objects.filter(sep6_24_qparams | sep31_qparams) num_completed = 0 for transaction in transactions: if module.TERMINATE: break try: rri.execute_outgoing_transaction(transaction) except Exception: logger.exception( "An exception was raised by execute_outgoing_transaction()" ) continue transaction.refresh_from_db() if transaction.status == Transaction.STATUS.pending_receiver: logger.error( f"Transaction {transaction.id} status must be " f"updated after call to execute_outgoing_transaction()") continue elif transaction.status in [ Transaction.STATUS.pending_external, Transaction.STATUS.completed, ]: if transaction.amount_fee is None: if registered_fee_func == calculate_fee: op = { Transaction.KIND.withdrawal: settings.OPERATION_WITHDRAWAL, Transaction.KIND.send: Transaction.KIND.send, }[transaction.kind] transaction.amount_fee = calculate_fee({ "amount": transaction.amount_in, "operation": op, "asset_code": transaction.asset.code, }) else: transaction.amount_fee = Decimal(0) transaction.amount_out = transaction.amount_in - transaction.amount_fee # Anchors can mark transactions as pending_external if the transfer # cannot be completed immediately due to external processing. # poll_pending_transfers will check on these transfers and mark them # as complete when the funds have been received by the user. if transaction.status == Transaction.STATUS.completed: num_completed += 1 transaction.completed_at = datetime.now(timezone.utc) elif transaction.status not in [ Transaction.STATUS.error, Transaction.STATUS.pending_transaction_info_update, Transaction.STATUS.pending_customer_info_update, ]: logger.error( f"Transaction {transaction.id} was moved to invalid status" f" {transaction.status}") continue transaction.save() if num_completed: logger.info(f"{num_completed} transfers have been completed")
def process_sep6_request(self, params: Dict, transaction: Transaction) -> Dict: account = (PolarisStellarAccount.objects.filter( account=params["account"], memo=params["memo"], memo_type=params["memo_type"], ).select_related("user").first()) info_needed_resp = { "type": "non_interactive_customer_info_needed", "fields": [ "first_name", "last_name", "email_address", "bank_number", "bank_account_number", ], } if not account: return info_needed_resp elif not (account.user.bank_account_number and account.user.bank_number): return info_needed_resp elif params["type"] != "bank_account": raise ValueError(_("'type' must be 'bank_account'")) elif not params["dest"]: raise ValueError(_("'dest' is required")) elif not params["dest_extra"]: raise ValueError(_("'dest_extra' is required")) elif params["dest"] != account.user.bank_account_number: raise ValueError( _("'dest' must match bank account number for account")) elif params["dest_extra"] != account.user.bank_number: raise ValueError( _("'dest_extra' must match bank routing number for account")) elif not account.confirmed: # Here is where you would normally return something like this: # { # "type": "customer_info_status", # "status": "pending" # } # However, we're not going to block the client from completing # the flow since this is a reference server. pass asset = params["asset"] min_amount = round(asset.withdrawal_min_amount, asset.significant_decimals) max_amount = round(asset.withdrawal_max_amount, asset.significant_decimals) if params["amount"]: if not (min_amount <= params["amount"] <= max_amount): raise ValueError(_("invalid 'amount'")) transaction.amount_in = params["amount"] transaction.amount_fee = calculate_fee({ "amount": params["amount"], "operation": "withdraw", "asset_code": asset.code, }) transaction.amount_out = round( transaction.amount_in - transaction.amount_fee, asset.significant_decimals, ) transaction.save() response = { "account_id": asset.distribution_account, "min_amount": min_amount, "max_amount": max_amount, "fee_fixed": round(asset.withdrawal_fee_fixed, asset.significant_decimals), "fee_percent": asset.withdrawal_fee_percent, } if params["memo_type"] and params["memo"]: response["memo_type"] = params["memo_type"] response["memo"] = params["memo"] PolarisUserTransaction.objects.create(transaction_id=transaction.id, user=account.user, account=account) return response
def process_sep6_request(self, params: Dict, transaction: Transaction) -> Dict: account = (PolarisStellarAccount.objects.filter( account=params["account"], memo=None).select_related("user").first()) info_needed_resp = { "type": "non_interactive_customer_info_needed", "fields": [ "first_name", "last_name", "email_address", "bank_number", "bank_account_number", ], } if not account: return info_needed_resp elif not (account.user.bank_account_number and account.user.bank_number): return info_needed_resp elif params["type"] != "bank_account": raise ValueError(_("'type' must be 'bank_account'")) elif not account.confirmed: # Here is where you would normally return something like this: # { # "type": "customer_info_status", # "status": "pending" # } # However, we're not going to block the client from completing # the flow since this is a reference server. pass asset = params["asset"] min_amount = round(asset.deposit_min_amount, asset.significant_decimals) max_amount = round(asset.deposit_max_amount, asset.significant_decimals) if params["amount"]: if not (min_amount <= params["amount"] <= max_amount): raise ValueError(_("invalid 'amount'")) transaction.amount_in = params["amount"] transaction.amount_fee = calculate_fee({ "amount": params["amount"], "operation": "deposit", "asset_code": asset.code, }) transaction.amount_out = round( transaction.amount_in - transaction.amount_fee, asset.significant_decimals, ) transaction.save() # request is valid, return success data and add transaction to user model PolarisUserTransaction.objects.create(transaction_id=transaction.id, user=account.user, account=account) return { "how": "fake bank account number", "extra_info": { "message": ("'how' would normally contain a terse explanation for how " "to deposit the asset with the anchor, and 'extra_info' " "would provide any additional information.") }, }