def test_deposit_check_trustlines_horizon(
        mock_delay, client, acc1_usd_deposit_transaction_factory):
    """
    Tests the `check_trustlines` function's various logical paths. Note that the Stellar
    deposit is created synchronously. This makes Horizon calls, so it is skipped by the CI.
    """
    del mock_delay
    # Initiate a transaction with a new Stellar account.
    print("Creating initial deposit.")
    deposit = acc1_usd_deposit_transaction_factory()

    keypair = Keypair.random()
    deposit.stellar_account = keypair.public_key
    response = client.get(
        f"/deposit?asset_code=USD&account={deposit.stellar_account}",
        follow=True)
    content = json.loads(response.content)
    assert response.status_code == 403
    assert content["type"] == "interactive_customer_info_needed"

    # Complete the interactive deposit. The transaction should be set
    # to pending_user_transfer_start, since wallet-side confirmation has not happened.
    print("Completing interactive deposit.")
    transaction_id = content["id"]
    url = content["url"]
    amount = 20
    response = client.post(url, {"amount": amount})
    assert response.status_code == 200
    assert (Transaction.objects.get(id=transaction_id).status ==
            Transaction.STATUS.pending_user_transfer_start)

    # As a result of this external confirmation, the transaction should
    # be `pending_trust`. This will trigger a synchronous call to
    # `create_stellar_deposit`, which will register the account on testnet.
    # Since the account will not have a trustline, the status will still
    # be `pending_trust`.
    response = client.get(
        f"/deposit/confirm_transaction?amount={amount}&transaction_id={transaction_id}",
        follow=True,
    )
    assert response.status_code == 200
    content = json.loads(response.content)
    transaction = content["transaction"]
    assert transaction
    assert transaction["status"] == Transaction.STATUS.pending_anchor
    assert float(transaction["amount_in"]) == amount

    # The Stellar account has not been registered, so
    # this should not change the status of the Transaction.
    print(
        "Check trustlines, try one. No trustline for account. Status should be pending_trust."
    )
    check_trustlines()
    assert (Transaction.objects.get(
        id=transaction_id).status == Transaction.STATUS.pending_trust)

    # Add a trustline for the transaction asset from the server
    # source account to the transaction account.
    from stellar_sdk.asset import Asset
    from stellar_sdk.transaction_builder import TransactionBuilder

    print("Create trustline.")
    asset_code = deposit.asset.code
    asset_issuer = settings.STELLAR_DISTRIBUTION_ACCOUNT_ADDRESS
    Asset(code=asset_code, issuer=asset_issuer)

    server = settings.HORIZON_SERVER
    source_account = server.load_account(keypair.public_key)
    base_fee = 100
    transaction = TransactionBuilder(
        source_account=source_account,
        network_passphrase=settings.STELLAR_NETWORK_PASSPHRASE,
        base_fee=base_fee).append_change_trust_op(asset_code,
                                                  asset_issuer).build()
    transaction.sign(keypair)
    response = server.submit_transaction(transaction)
    assert response[
        "result_xdr"] == "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAGAAAAAAAAAAA="

    print("Check trustlines, try three. Status should be completed.")
    check_trustlines()
    completed_transaction = Transaction.objects.get(id=transaction_id)
    assert completed_transaction.status == Transaction.STATUS.completed
    assert (completed_transaction.stellar_transaction_id ==
            HORIZON_SUCCESS_RESPONSE["hash"])
Exemple #2
0
def get_or_create_transaction_destination_account(
    transaction, ) -> Tuple[Optional[Account], bool, bool]:
    """
    Returns the stellar_sdk.account.Account for which this transaction's payment will be sent to as
    as well as whether or not the account was created as a result of calling this function.

    If the account exists, the function simply returns the account and False.

    If the account doesn't exist, Polaris must create the account using an account provided by the
    anchor. Polaris can use the distribution account of the anchored asset or a channel account if
    the asset's distribution account requires non-master signatures.

    If the transacted asset's distribution account does not require non-master signatures, Polaris
    can create the destination account using the distribution account. On successful creation,
    this function will return the account and True. On failure, a RuntimeError exception is raised.

    If the transacted asset's distribution account does require non-master signatures, the anchor
    should save a keypair of a pre-existing Stellar account to use as the channel account via
    DepositIntegration.create_channel_account().

    This channel account must only be used as the source account for transactions related to the
    ``Transaction`` object passed. It also must not be used to submit transactions by any service
    other than Polaris. If it is, the outstanding transactions will be invalidated due to bad
    sequence numbers. Finally, the channel accounts must have a master signer with a weight greater
    than or equal to the medium threshold for the account.

    After the transaction for creating the destination account has been submitted to the stellar network
    and the transaction has been created, this function will return the account and True. If the
    transaction was not submitted successfully, a RuntimeError exception will be raised.
    """
    try:
        account, json_resp = get_account_obj(
            Keypair.from_public_key(transaction.stellar_account))
        return account, False, has_trustline(transaction, json_resp)
    except RuntimeError:
        master_signer = None
        if transaction.asset.distribution_account_master_signer:
            master_signer = json.loads(
                transaction.asset.distribution_account_master_signer)
        thresholds = json.loads(
            transaction.asset.distribution_account_thresholds)
        if master_signer and master_signer["weight"] >= thresholds[
                "med_threshold"]:
            source_account_kp = Keypair.from_secret(
                transaction.asset.distribution_seed)
            source_account, _ = get_account_obj(source_account_kp)
        else:
            from polaris.integrations import registered_deposit_integration as rdi

            rdi.create_channel_account(transaction)
            source_account_kp = Keypair.from_secret(transaction.channel_seed)
            source_account, _ = get_account_obj(source_account_kp)

        base_fee = settings.HORIZON_SERVER.fetch_base_fee()
        builder = TransactionBuilder(
            source_account=source_account,
            network_passphrase=settings.STELLAR_NETWORK_PASSPHRASE,
            base_fee=base_fee,
        )
        transaction_envelope = builder.append_create_account_op(
            destination=transaction.stellar_account,
            starting_balance=settings.ACCOUNT_STARTING_BALANCE,
        ).build()
        transaction_envelope.sign(source_account_kp)

        try:
            settings.HORIZON_SERVER.submit_transaction(transaction_envelope)
        except BaseHorizonError as submit_exc:  # pragma: no cover
            raise RuntimeError(
                "Horizon error when submitting create account to horizon: "
                f"{submit_exc.message}")

        transaction.status = Transaction.STATUS.pending_trust
        transaction.save()
        logger.info(
            f"Transaction {transaction.id} is now pending_trust of destination account"
        )
        account, json_resp = get_account_obj(
            Keypair.from_public_key(transaction.stellar_account))
        return account, True, has_trustline(transaction, json_resp)
    except BaseHorizonError as e:
        raise RuntimeError(
            f"Horizon error when loading stellar account: {e.message}")
Exemple #3
0
 def create_deposit_envelope(transaction,
                             source_account) -> TransactionEnvelope:
     payment_amount = round(
         Decimal(transaction.amount_in) - Decimal(transaction.amount_fee),
         transaction.asset.significant_decimals,
     )
     builder = TransactionBuilder(
         source_account=source_account,
         network_passphrase=settings.STELLAR_NETWORK_PASSPHRASE,
         # only one operation, so base_fee will be multipled by 1
         base_fee=settings.MAX_TRANSACTION_FEE_STROOPS
         or settings.HORIZON_SERVER.fetch_base_fee(),
     )
     payment_op_kwargs = {
         "destination": transaction.stellar_account,
         "asset_code": transaction.asset.code,
         "asset_issuer": transaction.asset.issuer,
         "amount": str(payment_amount),
         "source": transaction.asset.distribution_account,
     }
     if transaction.claimable_balance_supported:
         _, json_resp = get_account_obj(
             Keypair.from_public_key(transaction.stellar_account))
         if is_pending_trust(transaction, json_resp):
             claimant = Claimant(destination=transaction.stellar_account)
             asset = Asset(code=transaction.asset.code,
                           issuer=transaction.asset.issuer)
             builder.append_create_claimable_balance_op(
                 claimants=[claimant],
                 asset=asset,
                 amount=str(payment_amount),
                 source=transaction.asset.distribution_account,
             )
         else:
             builder.append_payment_op(**payment_op_kwargs)
     else:
         builder.append_payment_op(**payment_op_kwargs)
     if transaction.memo:
         builder.add_memo(make_memo(transaction.memo,
                                    transaction.memo_type))
     return builder.build()
Exemple #4
0
    def test_to_txrep_full_tx(self):
        keypair = Keypair.from_secret(
            "SAHGKA7QJB6SRFDZSPZDEEIOEHUHTQS4XVN4IMR5YCKBPEN5A6YNKKUO")
        account = Account(keypair.public_key, 46489056724385792)
        transaction_builder = TransactionBuilder(
            source_account=account,
            network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE,
            base_fee=200,
        )
        transaction_builder.add_text_memo("Enjoy this transaction")
        transaction_builder.add_time_bounds(1535756672, 1567292672)
        transaction_builder.append_create_account_op(
            destination=
            "GAZFEVBSEGJJ63WPVVIWXLZLWN2JYZECECGT6GUNP4FJDVZVNXWQWMYI",
            starting_balance="10",
            source=keypair.public_key,
        )
        transaction_builder.append_payment_op(
            destination=
            "GBAF6NXN3DHSF357QBZLTBNWUTABKUODJXJYYE32ZDKA2QBM2H33IK6O",
            asset_code="USD",
            asset_issuer=
            "GAZFEVBSEGJJ63WPVVIWXLZLWN2JYZECECGT6GUNP4FJDVZVNXWQWMYI",
            amount="40.0004",
            source=keypair.public_key,
        ),
        transaction_builder.append_path_payment_strict_receive_op(
            destination=
            "GBAF6NXN3DHSF357QBZLTBNWUTABKUODJXJYYE32ZDKA2QBM2H33IK6O",
            send_code="USD",
            send_issuer=
            "GAZFEVBSEGJJ63WPVVIWXLZLWN2JYZECECGT6GUNP4FJDVZVNXWQWMYI",
            send_max="10",
            dest_code="XCN",
            dest_issuer=
            "GAYE5SDEM5JIEMGQ7LBMQVRQRVJB6A5E7AZVLJYFL3CNHLZX24DFD35F",
            dest_amount="5.125",
            path=[
                Asset(
                    "Hello",
                    "GD3RXMK2GHSXXEHPBZY5IL7VW5BXQEDJMCD4KVMXOH2GRFKDGZXR5PFO"
                ),
                Asset.native(),
                Asset(
                    "MOEW",
                    "GBR765FQTCAJLLJGZVYLXCFAOZI6ORTHPDPOOHJOHFRZ5GHNVYGK4IFM"
                ),
            ],
            source=keypair.public_key,
        )
        transaction_builder.append_manage_sell_offer_op(
            selling_code="XCN",
            selling_issuer=
            "GAYE5SDEM5JIEMGQ7LBMQVRQRVJB6A5E7AZVLJYFL3CNHLZX24DFD35F",
            buying_code="USD",
            buying_issuer=
            "GAZFEVBSEGJJ63WPVVIWXLZLWN2JYZECECGT6GUNP4FJDVZVNXWQWMYI",
            amount="100.123",
            price=Price(n=7, d=1),
            offer_id=12345,
            source=keypair.public_key,
        )
        transaction_builder.append_create_passive_sell_offer_op(
            selling_code="XCN",
            selling_issuer=
            "GAYE5SDEM5JIEMGQ7LBMQVRQRVJB6A5E7AZVLJYFL3CNHLZX24DFD35F",
            buying_code="USD",
            buying_issuer=
            "GAZFEVBSEGJJ63WPVVIWXLZLWN2JYZECECGT6GUNP4FJDVZVNXWQWMYI",
            amount="100.123",
            price="7.1",
            source=keypair.public_key,
        )
        transaction_builder.append_set_options_op(
            inflation_dest=
            "GCVAZXCGX3HLHZ6P5WKEPE3U2YJMTLMPTZJFGY67MTNPSOA4COKVJ6AF",
            clear_flags=3,
            set_flags=3,
            master_weight=255,
            low_threshold=1,
            med_threshold=2,
            high_threshold=3,
            home_domain="stellar.org",
            signer=Signer.ed25519_public_key(
                "GAO3YIWNOBP4DN3ORDXYXTUMLF5S54OK4PKAFCWE23TTOML4COGQOIYA",
                255),
            source=keypair.public_key,
        )
        transaction_builder.append_set_options_op(home_domain="stellarcn.org")
        transaction_builder.append_pre_auth_tx_signer(
            "0ab0c76b1c661db0e829abfdd9e32e6ce3c11f756bdf71aa23846582106c1783",
            5)
        transaction_builder.append_hashx_signer(
            "0d64fac556c1545616b3c915a4ec215239500bce287007cff038b6020950af46",
            10)
        transaction_builder.append_change_trust_op(
            asset_code="XCN",
            asset_issuer=
            "GAYE5SDEM5JIEMGQ7LBMQVRQRVJB6A5E7AZVLJYFL3CNHLZX24DFD35F",
            limit="1000",
            source=keypair.public_key,
        )
        transaction_builder.append_allow_trust_op(
            trustor="GDU64QWPRTO3LW5VGZPTLRR6QROFWV36XG5QT4C6FZBPHQBBFYRCWZTZ",
            asset_code="CAT",
            authorize=True,
            source=keypair.public_key,
        )
        transaction_builder.append_account_merge_op(
            destination=
            "GDU64QWPRTO3LW5VGZPTLRR6QROFWV36XG5QT4C6FZBPHQBBFYRCWZTZ",
            source=keypair.public_key,
        )
        transaction_builder.append_manage_data_op("Hello",
                                                  "Stellar",
                                                  source=keypair.public_key)
        transaction_builder.append_manage_data_op("World",
                                                  None,
                                                  source=keypair.public_key)
        transaction_builder.append_bump_sequence_op(bump_to=46489056724385800,
                                                    source=keypair.public_key)
        transaction_builder.append_manage_buy_offer_op(
            selling_code="XCN",
            selling_issuer=
            "GAYE5SDEM5JIEMGQ7LBMQVRQRVJB6A5E7AZVLJYFL3CNHLZX24DFD35F",
            buying_code="USD",
            buying_issuer=
            "GAZFEVBSEGJJ63WPVVIWXLZLWN2JYZECECGT6GUNP4FJDVZVNXWQWMYI",
            amount="100.123",
            price="7.1",
            source=keypair.public_key,
        )
        transaction_builder.append_path_payment_strict_send_op(
            destination=
            "GBAF6NXN3DHSF357QBZLTBNWUTABKUODJXJYYE32ZDKA2QBM2H33IK6O",
            send_code="USD",
            send_issuer=
            "GAZFEVBSEGJJ63WPVVIWXLZLWN2JYZECECGT6GUNP4FJDVZVNXWQWMYI",
            send_amount="10",
            dest_code="XCN",
            dest_issuer=
            "GAYE5SDEM5JIEMGQ7LBMQVRQRVJB6A5E7AZVLJYFL3CNHLZX24DFD35F",
            dest_min="5.125",
            path=[
                Asset(
                    "Hello",
                    "GD3RXMK2GHSXXEHPBZY5IL7VW5BXQEDJMCD4KVMXOH2GRFKDGZXR5PFO"
                ),
                Asset.native(),
                Asset(
                    "MOEW",
                    "GBR765FQTCAJLLJGZVYLXCFAOZI6ORTHPDPOOHJOHFRZ5GHNVYGK4IFM"
                ),
            ],
            source=keypair.public_key,
        )
        transaction_builder.append_inflation_op()

        te = transaction_builder.build()
        te.sign(keypair)
        te.sign_hashx(
            bytes.fromhex(
                "8b73f9e12fcc8cd9580a2a26aec14d6175aa1ff45e76b816618635d03f3256b8"
            ))
        txrep = to_txrep(te)
        assert txrep == get_txrep_file("test_to_txrep_full_tx.txt")
        assert (from_txrep(
            txrep, Network.TESTNET_NETWORK_PASSPHRASE).to_xdr() == te.to_xdr())
Exemple #5
0
    def get_or_create_destination_account(
        cls,
        transaction: Transaction,
    ) -> Tuple[Account, bool]:
        """
        Returns:
            Account: The account found or created for the Transaction
            bool: True if trustline doesn't exist, False otherwise.

        If the account doesn't exist, Polaris must create the account using an account provided by the
        anchor. Polaris can use the distribution account of the anchored asset or a channel account if
        the asset's distribution account requires non-master signatures.

        If the transacted asset's distribution account does require non-master signatures,
        DepositIntegration.create_channel_account() will be called. See the function docstring for more
        info.

        On failure to create the destination account, a RuntimeError exception is raised.
        """
        try:
            account, json_resp = get_account_obj(
                Keypair.from_public_key(transaction.stellar_account))
            return account, is_pending_trust(transaction, json_resp)
        except RuntimeError:  # account does not exist
            try:
                requires_multisig = PendingDeposits.requires_multisig(
                    transaction)
            except NotFoundError:
                logger.error(
                    f"{transaction.asset.code} distribution account "
                    f"{transaction.asset.distribution_account} does not exist")
                raise RuntimeError("the distribution account does not exist")
            if requires_multisig:
                source_account_kp = cls.get_channel_keypair(transaction)
                source_account, _ = get_account_obj(source_account_kp)
            else:
                source_account_kp = Keypair.from_secret(
                    transaction.asset.distribution_seed)
                source_account, _ = get_account_obj(source_account_kp)

            builder = TransactionBuilder(
                source_account=source_account,
                network_passphrase=settings.STELLAR_NETWORK_PASSPHRASE,
                # this transaction contains one operation so base_fee will be multiplied by 1
                base_fee=settings.MAX_TRANSACTION_FEE_STROOPS
                or settings.HORIZON_SERVER.fetch_base_fee(),
            )
            transaction_envelope = builder.append_create_account_op(
                destination=transaction.stellar_account,
                starting_balance=settings.ACCOUNT_STARTING_BALANCE,
            ).build()
            transaction_envelope.sign(source_account_kp)

            try:
                settings.HORIZON_SERVER.submit_transaction(
                    transaction_envelope)
            except BaseHorizonError as e:  # pragma: no cover
                raise RuntimeError(
                    "Horizon error when submitting create account "
                    f"to horizon: {e.message}")

            account, _ = get_account_obj(
                Keypair.from_public_key(transaction.stellar_account))
            return account, True
        except BaseHorizonError as e:
            raise RuntimeError(
                f"Horizon error when loading stellar account: {e.message}")
        except ConnectionError:
            raise RuntimeError("Failed to connect to Horizon")
Exemple #6
0
def transaction_builder():
    account = server().load_account(CONF['public_key'])
    return TransactionBuilder(source_account=account,
                              network_passphrase=network_passphrase(),
                              base_fee=fetch_base_fee())
builder.add_text_memo('For beers') # string length <= 28 bytes
builder.sign()
builder.submit()'''

base_fee = server.fetch_base_fee()
source_account = server.load_account(source_acc_id)
# Start building the transaction.
transaction = (
    TransactionBuilder(
        source_account=source_account,
        network_passphrase='Test SDF Network ; September 2015',
        base_fee=base_fee,
    )
    # Because Stellar allows transaction in many currencies, you must specify the asset type.
    # Here we are sending Lumens.
    .append_payment_op(destination=destination_acc_id,
                       amount="220",
                       asset_code="XLM")
    # A memo allows you to add your own metadata to a transaction. It's
    # optional and does not affect how Stellar treats the transaction.
    .add_text_memo("Test Transaction")
    # Wait a maximum of three minutes for the transaction
    .set_timeout(10).build())

# Sign the transaction to prove you are actually the person sending it.
transaction.sign(source_acc_key)

try:
    # And finally, send it off to Stellar!
    response = server.submit_transaction(transaction)
    print(f"Response: {response}")
def create_stellar_deposit(transaction_id):
    """Create and submit the Stellar transaction for the deposit."""
    transaction = Transaction.objects.get(id=transaction_id)

    # We check the Transaction status to avoid double submission of a Stellar
    # transaction. The Transaction can be either `pending_anchor` if the task
    # is called from `GET deposit/confirm_transaction` or `pending_trust` if called
    # from the `check_trustlines()`.
    if transaction.status not in [
            Transaction.STATUS.pending_anchor,
            Transaction.STATUS.pending_trust,
    ]:
        logger.debug(
            "unexpected transaction status %s at top of create_stellar_deposit",
            transaction.status,
        )
        return
    transaction.status = Transaction.STATUS.pending_stellar
    transaction.save()

    # We can assume transaction has valid stellar_account, amount_in, and asset
    # because this task is only called after those parameters are validated.
    stellar_account = transaction.stellar_account
    payment_amount = round(transaction.amount_in - transaction.amount_fee, 7)
    asset = transaction.asset.code

    # If the given Stellar account does not exist, create
    # the account with at least enough XLM for the minimum
    # reserve and a trust line (recommended 2.01 XLM), update
    # the transaction in our internal database, and return.

    server = settings.HORIZON_SERVER
    starting_balance = settings.ACCOUNT_STARTING_BALANCE
    server_account = server.load_account(
        settings.STELLAR_DISTRIBUTION_ACCOUNT_ADDRESS)
    base_fee = server.fetch_base_fee()
    builder = TransactionBuilder(
        source_account=server_account,
        network_passphrase=settings.STELLAR_NETWORK_PASSPHRASE,
        base_fee=base_fee)
    try:
        server.load_account(stellar_account)
    except BaseHorizonError as address_exc:
        # 404 code corresponds to Resource Missing.
        if address_exc.status != 404:
            logger.debug(
                "error with message %s when loading stellar account",
                address_exc.message,
            )
            return
        transaction_envelope = builder \
            .append_create_account_op(destination=stellar_account,
                                      starting_balance=starting_balance,
                                      source=settings.STELLAR_DISTRIBUTION_ACCOUNT_ADDRESS) \
            .build()
        transaction_envelope.sign(settings.STELLAR_DISTRIBUTION_ACCOUNT_SEED)
        try:
            server.submit_transaction(transaction_envelope)
        except BaseHorizonError as submit_exc:
            logger.debug(
                f"error with message {submit_exc.message} when submitting create account to horizon"
            )
            return
        transaction.status = Transaction.STATUS.pending_trust
        transaction.save()
        return

    # If the account does exist, deposit the desired amount of the given
    # asset via a Stellar payment. If that payment succeeds, we update the
    # transaction to completed at the current time. If it fails due to a
    # trustline error, we update the database accordingly. Else, we do not update.

    transaction_envelope = builder \
        .append_payment_op(destination=stellar_account,
                           asset_code=asset,
                           asset_issuer=settings.STELLAR_ISSUER_ACCOUNT_ADDRESS,
                           amount=str(payment_amount)) \
        .build()
    transaction_envelope.sign(settings.STELLAR_DISTRIBUTION_ACCOUNT_SEED)
    try:
        response = server.submit_transaction(transaction_envelope)
    # Functional errors at this stage are Horizon errors.
    except BaseHorizonError as exception:
        if TRUSTLINE_FAILURE_XDR not in exception.result_xdr:
            logger.debug(
                "error with message %s when submitting payment to horizon, non-trustline failure",
                exception.message,
            )
            return
        logger.debug("trustline error when submitting transaction to horizon")
        transaction.status = Transaction.STATUS.pending_trust
        transaction.save()
        return

    # If this condition is met, the Stellar payment succeeded, so we
    # can mark the transaction as completed.
    if response["result_xdr"] != SUCCESS_XDR:
        logger.debug(
            "payment stellar transaction failed when submitted to horizon")
        return

    transaction.stellar_transaction_id = response["hash"]
    transaction.status = Transaction.STATUS.completed
    transaction.completed_at = now()
    transaction.status_eta = 0  # No more status change.
    transaction.amount_out = payment_amount
    transaction.save()
Exemple #9
0
def create_stellar_deposit(transaction_id: str) -> bool:
    """
    Create and submit the Stellar transaction for the deposit.

    :returns a boolean indicating whether or not the deposit was successfully
        completed. One reason a transaction may not be completed is if a
        trustline must be established. The transaction's status will be set as
        ``pending_stellar`` and the status_message will be populated
        with a description of the problem encountered.
    :raises ValueError: the transaction has an unexpected status
    """
    transaction = Transaction.objects.get(id=transaction_id)

    # We check the Transaction status to avoid double submission of a Stellar
    # transaction. The Transaction can be either `pending_anchor` if the task
    # is called from `poll_pending_deposits()` or `pending_trust` if called
    # from the `check_trustlines()`.
    if transaction.status not in [
            Transaction.STATUS.pending_anchor,
            Transaction.STATUS.pending_trust,
    ]:
        raise ValueError(
            f"unexpected transaction status {transaction.status} for "
            "create_stellar_deposit", )
    transaction.status = Transaction.STATUS.pending_stellar
    transaction.save()

    # We can assume transaction has valid stellar_account, amount_in, and asset
    # because this task is only called after those parameters are validated.
    stellar_account = transaction.stellar_account
    payment_amount = round(transaction.amount_in - transaction.amount_fee, 7)
    asset = transaction.asset.code

    # If the given Stellar account does not exist, create
    # the account with at least enough XLM for the minimum
    # reserve and a trust line (recommended 2.01 XLM), update
    # the transaction in our internal database, and return.

    server = settings.HORIZON_SERVER
    starting_balance = settings.ACCOUNT_STARTING_BALANCE
    server_account = server.load_account(
        settings.STELLAR_DISTRIBUTION_ACCOUNT_ADDRESS)
    base_fee = server.fetch_base_fee()
    builder = TransactionBuilder(
        source_account=server_account,
        network_passphrase=settings.STELLAR_NETWORK_PASSPHRASE,
        base_fee=base_fee,
    )
    try:
        server.load_account(stellar_account)
    except BaseHorizonError as address_exc:
        # 404 code corresponds to Resource Missing.
        if address_exc.status != 404:
            transaction.status = Transaction.STATUS.error
            transaction.status_message = (
                "Horizon error when loading stellar account: "
                f"{address_exc.message}")
            transaction.save()
            return False

        transaction_envelope = builder.append_create_account_op(
            destination=stellar_account,
            starting_balance=starting_balance,
            source=settings.STELLAR_DISTRIBUTION_ACCOUNT_ADDRESS,
        ).build()
        transaction_envelope.sign(settings.STELLAR_DISTRIBUTION_ACCOUNT_SEED)
        try:
            server.submit_transaction(transaction_envelope)
        except BaseHorizonError as submit_exc:
            transaction.status = Transaction.STATUS.error
            transaction.status_message = (
                "Horizon error when submitting create account to horizon: "
                f"{submit_exc.message}")
            transaction.save()
            return False

        transaction.status = Transaction.STATUS.pending_trust
        transaction.save()
        return False

    # If the account does exist, deposit the desired amount of the given
    # asset via a Stellar payment. If that payment succeeds, we update the
    # transaction to completed at the current time. If it fails due to a
    # trustline error, we update the database accordingly. Else, we do not update.

    transaction_envelope = builder.append_payment_op(
        destination=stellar_account,
        asset_code=asset,
        asset_issuer=settings.STELLAR_ISSUER_ACCOUNT_ADDRESS,
        amount=str(payment_amount),
    ).build()
    transaction_envelope.sign(settings.STELLAR_DISTRIBUTION_ACCOUNT_SEED)
    try:
        response = server.submit_transaction(transaction_envelope)
    # Functional errors at this stage are Horizon errors.
    except BaseHorizonError as exception:
        if TRUSTLINE_FAILURE_XDR not in exception.result_xdr:
            transaction.status = Transaction.STATUS.error
            transaction.status_message = (
                "Unable to submit payment to horizon, "
                f"non-trustline failure: {exception.message}")
            transaction.save()
            return False
        transaction.status_message = (
            "trustline error when submitting transaction to horizon")
        transaction.status = Transaction.STATUS.pending_trust
        transaction.save()
        return False

    if response["result_xdr"] != SUCCESS_XDR:
        transaction_result = TransactionResult.from_xdr(response["result_xdr"])
        transaction.status_message = (
            "Stellar transaction failed when submitted to horizon: "
            f"{transaction_result.result}")
        transaction.save()
        return False

    transaction.stellar_transaction_id = response["hash"]
    transaction.status = Transaction.STATUS.completed
    transaction.completed_at = now()
    transaction.status_eta = 0  # No more status change.
    transaction.amount_out = payment_amount
    transaction.save()
    return True