Exemplo n.º 1
0
def update_transaction(response: Dict, transaction: Transaction, error_msg: str = None):
    """
    Updates the transaction depending on whether or not the transaction was
    successfully executed on the Stellar network and `process_withdrawal`
    completed without raising an exception.

    If the Horizon response indicates the response was not successful or an
    exception was raised while processing the withdrawal, we mark the status
    as `error`. If the Stellar transaction succeeded, we mark it as `completed`.

    :param error_msg: a description of the error that has occurred.
    :param response: a response body returned from Horizon for the transaction
    :param transaction: a database model object representing the transaction
    """
    if error_msg or not response["successful"]:
        transaction.status = Transaction.STATUS.error
        transaction.status_message = error_msg
    else:
        transaction.completed_at = now()
        transaction.status = Transaction.STATUS.completed
        transaction.status_eta = 0
        transaction.amount_out = transaction.amount_in - transaction.amount_fee

    transaction.stellar_transaction_id = response["id"]
    transaction.save()
Exemplo n.º 2
0
 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 submit(cls, transaction: Transaction) -> bool:
        valid_statuses = [
            Transaction.STATUS.pending_user_transfer_start,
            Transaction.STATUS.pending_external,
            Transaction.STATUS.pending_anchor,
            Transaction.STATUS.pending_trust,
        ]
        if transaction.status not in valid_statuses:
            raise ValueError(
                f"Unexpected transaction status: {transaction.status}, expecting "
                f"{' or '.join(valid_statuses)}.")

        transaction.status = Transaction.STATUS.pending_anchor
        transaction.save()
        logger.info(f"Initiating Stellar deposit for {transaction.id}")
        maybe_make_callback(transaction)

        if transaction.envelope_xdr:
            try:
                envelope = TransactionEnvelope.from_xdr(
                    transaction.envelope_xdr,
                    settings.STELLAR_NETWORK_PASSPHRASE)
            except Exception:
                cls._handle_error(transaction,
                                  "Failed to decode transaction envelope")
                return False
        else:
            distribution_acc, _ = get_account_obj(
                Keypair.from_public_key(
                    transaction.asset.distribution_account))
            envelope = cls.create_deposit_envelope(transaction,
                                                   distribution_acc)
            envelope.sign(transaction.asset.distribution_seed)

        transaction.status = Transaction.STATUS.pending_stellar
        transaction.save()
        logger.info(f"Transaction {transaction.id} now pending_stellar")
        maybe_make_callback(transaction)

        try:
            response = settings.HORIZON_SERVER.submit_transaction(envelope)
        except BaseHorizonError as e:
            cls._handle_error(transaction,
                              f"{e.__class__.__name__}: {e.message}")
            return False

        if not response.get("successful"):
            cls._handle_error(
                transaction,
                f"Stellar transaction failed when submitted to horizon: {response['result_xdr']}",
            )
            return False
        elif transaction.claimable_balance_supported:
            transaction.claimable_balance_id = cls.get_balance_id(response)

        transaction.envelope_xdr = response["envelope_xdr"]
        transaction.paging_token = response["paging_token"]
        transaction.stellar_transaction_id = response["id"]
        transaction.status = Transaction.STATUS.completed
        transaction.completed_at = datetime.datetime.now(datetime.timezone.utc)
        transaction.amount_out = round(
            Decimal(transaction.amount_in) - Decimal(transaction.amount_fee),
            transaction.asset.significant_decimals,
        )
        transaction.save()
        logger.info(f"Transaction {transaction.id} completed.")
        maybe_make_callback(transaction)
        return True
Exemplo n.º 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()

        logger.info("fetching user data for transaction")
        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

        if transaction.kind == Transaction.KIND.withdrawal:
            operation = settings.OPERATION_WITHDRAWAL
        else:
            operation = Transaction.KIND.send
        transaction.amount_fee = calculate_fee({
            "amount":
            transaction.amount_in,
            "operation":
            operation,
            "asset_code":
            transaction.asset.code,
        })
        transaction.amount_out = round(
            transaction.amount_in - transaction.amount_fee,
            transaction.asset.significant_decimals,
        )
        client = rails.BankAPIClient("fake anchor bank account number")
        response = client.send_funds(
            to_account=user.bank_account_number,
            amount=transaction.amount_in - transaction.amount_fee,
        )

        if response["success"]:
            logger.info(
                f"successfully sent mock outgoing transaction {transaction.id}"
            )
            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()
Exemplo n.º 5
0
    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
Exemplo n.º 6
0
    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.")
            },
        }