Exemple #1
0
    def poll_outgoing_transactions():
        transactions = Transaction.objects.filter(
            kind__in=[Transaction.KIND.withdrawal, Transaction.KIND.send],
            status=Transaction.STATUS.pending_external,
        )
        try:
            complete_transactions = rri.poll_outgoing_transactions(
                transactions)
        except Exception:
            logger.exception(
                "An exception was raised by poll_pending_transfers()")
            return

        if not (isinstance(complete_transactions, list) and all(
                isinstance(t, Transaction) for t in complete_transactions)):
            logger.exception(
                "invalid return type, expected a list of Transaction objects")
            return

        ids = [t.id for t in complete_transactions]
        if ids:
            num_completed = Transaction.objects.filter(id__in=ids).update(
                status=Transaction.STATUS.completed,
                completed_at=datetime.now(timezone.utc),
            )
            logger.info(
                f"{num_completed} pending transfers have been completed")
        for t in complete_transactions:
            maybe_make_callback(t)
 def _handle_error(cls, transaction, message):
     transaction.status_message = message
     transaction.status = Transaction.STATUS.error
     transaction.pending_execution_attempt = False
     transaction.save()
     logger.error(transaction.status_message)
     maybe_make_callback(transaction)
    def execute_deposit(cls, transaction):
        try:
            _, pending_trust = PendingDeposits.get_or_create_destination_account(
                transaction
            )
        except RuntimeError as e:
            transaction.status = Transaction.STATUS.error
            transaction.status_message = str(e)
            transaction.save()
            logger.error(transaction.status_message)
            maybe_make_callback(transaction)
            return

        if pending_trust and not transaction.claimable_balance_supported:
            logger.info(
                f"destination account is pending_trust for transaction {transaction.id}"
            )
            transaction.status = Transaction.STATUS.pending_trust
            transaction.save()
            maybe_make_callback(transaction)
            return
        if MultiSigTransactions.requires_multisig(transaction):
            MultiSigTransactions.save_as_pending_signatures(transaction)
            return
        if PendingDeposits.submit(transaction):
            transaction.refresh_from_db()
            try:
                rdi.after_deposit(transaction)
            except Exception:
                logger.exception("after_deposit() threw an unexpected exception")
    def execute_deposits(cls):
        module = sys.modules[__name__]
        try:
            ready_transactions = PendingDeposits.get_ready_deposits()
        except Exception:
            logger.exception(
                "poll_pending_deposits() threw an unexpected exception")
            return
        for i, transaction in enumerate(ready_transactions):
            if module.TERMINATE:
                still_processing_transactions = ready_transactions[i:]
                Transaction.objects.filter(
                    id__in=[t.id
                            for t in still_processing_transactions]).update(
                                pending_execution_attempt=False)
                break
            cls.execute_deposit(transaction)

        with django.db.transaction.atomic():
            multisig_transactions = list(
                Transaction.objects.filter(
                    kind=Transaction.KIND.deposit,
                    status=Transaction.STATUS.pending_anchor,
                    pending_signatures=False,
                    pending_execution_attempt=False,
                ).select_for_update())
            Transaction.objects.filter(
                id__in=[t.id for t in multisig_transactions]).update(
                    pending_execution_attempt=True)

        for i, transaction in enumerate(multisig_transactions):
            if module.TERMINATE:
                still_processing_transactions = ready_transactions[i:]
                Transaction.objects.filter(
                    id__in=[t.id
                            for t in still_processing_transactions]).update(
                                pending_execution_attempt=False)
                break

            try:
                success = PendingDeposits.submit(transaction)
            except Exception as e:
                logger.exception("submit() threw an unexpected exception")
                transaction.status_message = str(e)
                transaction.status = Transaction.STATUS.error
                transaction.pending_execution_attempt = False
                transaction.save()
                maybe_make_callback(transaction)
                continue

            if success:
                transaction.refresh_from_db()
                try:
                    rdi.after_deposit(transaction)
                except Exception:
                    logger.exception(
                        "after_deposit() threw an unexpected exception")
 def save_as_pending_signatures(cls, transaction):
     channel_kp = cls.get_channel_keypair(transaction)
     try:
         channel_account, _ = get_account_obj(channel_kp)
     except RuntimeError as e:
         transaction.status = Transaction.STATUS.error
         transaction.status_message = str(e)
         logger.error(transaction.status_message)
     else:
         # Create the initial envelope XDR with the channel signature
         envelope = PendingDeposits.create_deposit_envelope(
             transaction, channel_account)
         envelope.sign(channel_kp)
         transaction.envelope_xdr = envelope.to_xdr()
         transaction.pending_signatures = True
         transaction.status = Transaction.STATUS.pending_anchor
     transaction.pending_execution_attempt = False
     transaction.save()
     maybe_make_callback(transaction)
Exemple #6
0
    def requires_trustline(cls, transaction: Transaction) -> bool:
        try:
            _, pending_trust = PendingDeposits.get_or_create_destination_account(
                transaction)
        except RuntimeError as e:
            cls.handle_error(transaction, str(e))
            return True

        if pending_trust and not transaction.claimable_balance_supported:
            logger.info(
                f"destination account is pending_trust for transaction {transaction.id}"
            )
            transaction.status = Transaction.STATUS.pending_trust
            transaction.pending_execution_attempt = False
            transaction.save()
            maybe_make_callback(transaction)
            return True

        return False
    def process_response(cls, response, account):
        # We should not match valid pending transactions with ones that were
        # unsuccessful on the stellar network. If they were unsuccessful, the
        # client is also aware of the failure and will likely attempt to
        # resubmit it, in which case we should match the resubmitted transaction
        if not response.get("successful"):
            return

        try:
            _ = response["id"]
            envelope_xdr = response["envelope_xdr"]
            memo = response["memo"]
            result_xdr = response["result_xdr"]
        except KeyError:
            return

        # Query filters for SEP6 and 24
        withdraw_filters = Q(
            status=Transaction.STATUS.pending_user_transfer_start,
            kind=Transaction.KIND.withdrawal,
        )
        # Query filters for SEP31
        send_filters = Q(
            status=Transaction.STATUS.pending_sender,
            kind=Transaction.KIND.send,
        )
        transactions = Transaction.objects.filter(
            withdraw_filters | send_filters,
            memo=memo,
            receiving_anchor_account=account).all()
        if not transactions:
            logger.info(
                f"No match found for stellar transaction {response['id']}")
            return
        elif len(transactions) == 1:
            transaction = transactions[0]
        else:
            # in the prior implementation of watch_transactions, the first transaction
            # to have the same memo is matched, so we'll do the same in the refactored
            # version.
            logger.error(
                f"multiple Transaction objects returned for memo: {memo}")
            transaction = transactions[0]

        op_results = (Xdr.StellarXDRUnpacker(
            b64decode(result_xdr)).unpack_TransactionResult().result.results)
        horion_tx = TransactionEnvelope.from_xdr(
            envelope_xdr,
            network_passphrase=settings.STELLAR_NETWORK_PASSPHRASE,
        ).transaction

        payment_data = cls._find_matching_payment_data(response, horion_tx,
                                                       op_results, transaction)
        if not payment_data:
            logger.warning(
                f"Transaction matching memo {memo} has no payment operation")
            return

        # Transaction.amount_in is overwritten with the actual amount sent in the stellar
        # transaction. This allows anchors to validate the actual amount sent in
        # execute_outgoing_transactions() and handle invalid amounts appropriately.
        transaction.amount_in = round(
            Decimal(payment_data["amount"]),
            transaction.asset.significant_decimals,
        )

        # The stellar transaction has been matched with an existing record in the DB.
        # Now the anchor needs to initiate the off-chain transfer of the asset.
        if transaction.protocol == Transaction.PROTOCOL.sep31:
            # SEP-31 uses 'pending_receiver' status
            transaction.status = Transaction.STATUS.pending_receiver
            transaction.save()
        else:
            # SEP-6 and 24 uses 'pending_anchor' status
            transaction.status = Transaction.STATUS.pending_anchor
            transaction.save()
        maybe_make_callback(transaction)
        return
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 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
Exemple #10
0
    def check_trustlines():
        """
        Create Stellar transaction for deposit transactions marked as pending
        trust, if a trustline has been created.
        """
        module = sys.modules[__name__]
        with django.db.transaction.atomic():
            transactions = list(
                Transaction.objects.filter(
                    kind=Transaction.KIND.deposit,
                    status=Transaction.STATUS.pending_trust,
                    pending_execution_attempt=False,
                ).select_for_update()
            )
            Transaction.objects.filter(id__in=[t.id for t in transactions]).update(
                pending_execution_attempt=True
            )
        server = settings.HORIZON_SERVER
        accounts = {}
        for i, transaction in enumerate(transactions):
            if module.TERMINATE:
                still_process_transactions = transactions[i:]
                Transaction.objects.filter(
                    id__in=[t.id for t in still_process_transactions]
                ).update(pending_execution_attempt=False)
                break
            if accounts.get(transaction.stellar_account):
                account = accounts[transaction.stellar_account]
            else:
                try:
                    account = (
                        server.accounts().account_id(transaction.stellar_account).call()
                    )
                    accounts[transaction.stellar_account] = account
                except BaseRequestError:
                    logger.exception(
                        f"Failed to load account {transaction.stellar_account}"
                    )
                    continue
            for balance in account["balances"]:
                if balance.get("asset_type") == "native":
                    continue
                if (
                    balance["asset_code"] == transaction.asset.code
                    and balance["asset_issuer"] == transaction.asset.issuer
                ):
                    logger.info(
                        f"Account {account['id']} has established a trustline for "
                        f"{balance['asset_code']}:{balance['asset_issuer']}"
                    )
                    if MultiSigTransactions.requires_multisig(transaction):
                        MultiSigTransactions.save_as_pending_signatures(transaction)
                        continue

                    try:
                        success = PendingDeposits.submit(transaction)
                    except Exception as e:
                        logger.exception("submit() threw an unexpected exception")
                        transaction.status_message = str(e)
                        transaction.status = Transaction.STATUS.error
                        transaction.pending_execution_attempt = False
                        transaction.save()
                        maybe_make_callback(transaction)
                        return

                    if success:
                        transaction.refresh_from_db()
                        try:
                            rdi.after_deposit(transaction)
                        except Exception:
                            logger.exception(
                                "after_deposit() threw an unexpected exception"
                            )
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.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
            ):
                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]
                        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()
            maybe_make_callback(transaction)

        if num_completed:
            logger.info(f"{num_completed} transfers have been completed")
 def _handle_error(cls, transaction, message):
     transaction.status_message = message
     transaction.status = Transaction.STATUS.error
     transaction.save()
     logger.error(transaction.status_message)
     maybe_make_callback(transaction)
Exemple #13
0
def test_maybe_make_callback_postmessage(mock_log_error, mock_make_callback):
    mock_transaction = Mock(on_change_callback="postMessage")
    utils.maybe_make_callback(mock_transaction)
    mock_make_callback.assert_not_called()
    mock_log_error.assert_not_called()
Exemple #14
0
def test_maybe_make_callback_raises(mock_log_error, mock_make_callback):
    mock_make_callback.side_effect = RequestException()
    mock_transaction = Mock()
    utils.maybe_make_callback(mock_transaction)
    mock_make_callback.assert_called_once_with(mock_transaction, timeout=None)
    mock_log_error.assert_called_once()
Exemple #15
0
def test_maybe_make_callback_not_ok(mock_log_error, mock_make_callback):
    mock_make_callback.return_value = Mock(ok=False)
    mock_transaction = Mock()
    utils.maybe_make_callback(mock_transaction)
    mock_make_callback.assert_called_once_with(mock_transaction, timeout=None)
    mock_log_error.assert_called_once()