Exemple #1
0
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)
Exemple #2
0
    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
Exemple #4
0
    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()
Exemple #5
0
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)
Exemple #6
0
 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()
Exemple #8
0
    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)
Exemple #11
0
    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.")
            },
        }