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(), ) _, json_resp = get_account_obj( Keypair.from_public_key(transaction.stellar_account)) if transaction.claimable_balance_supported and is_pending_trust( transaction, json_resp): logger.debug( f"Crafting claimable balance operation for Transaction {transaction.id}" ) 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( 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.memo: builder.add_memo(make_memo(transaction.memo, transaction.memo_type)) return builder.build()
def create_transaction_envelope(transaction, source_account) -> TransactionEnvelope: payment_amount = round( Decimal(transaction.amount_in) - Decimal(transaction.amount_fee), transaction.asset.significant_decimals, ) memo = make_memo(transaction.memo, transaction.memo_type) base_fee = settings.HORIZON_SERVER.fetch_base_fee() builder = TransactionBuilder( source_account=source_account, network_passphrase=settings.STELLAR_NETWORK_PASSPHRASE, base_fee=base_fee, ).append_payment_op( destination=transaction.stellar_account, asset_code=transaction.asset.code, asset_issuer=transaction.asset.issuer, amount=str(payment_amount), source=transaction.asset.distribution_account, ) if memo: builder.add_memo(memo) return builder.build()
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", ) elif transaction.amount_in is None or transaction.amount_fee is None: transaction.status = Transaction.STATUS.error transaction.status_message = ( "`amount_in` and `amount_fee` must be populated, skipping transaction" ) transaction.save() raise ValueError(transaction.status_message) transaction.status = Transaction.STATUS.pending_stellar transaction.save() logger.info(f"Transaction {transaction_id} now pending_stellar") # 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, transaction.asset.significant_decimals, ) asset = transaction.asset memo = make_memo(transaction.memo, transaction.memo_type) # 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(asset.distribution_account) 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: # pragma: no cover msg = ( "Horizon error when loading stellar account: " f"{address_exc.message}" ) logger.error(msg) transaction.status_message = msg transaction.status = Transaction.STATUS.error transaction.save() return False logger.info(f"Stellar account {stellar_account} does not exist. Creating.") transaction_envelope = builder.append_create_account_op( destination=stellar_account, starting_balance=starting_balance, source=asset.distribution_account, ).build() transaction_envelope.sign(asset.distribution_seed) try: server.submit_transaction(transaction_envelope) except BaseHorizonError as submit_exc: # pragma: no cover msg = ( "Horizon error when submitting create account to horizon: " f"{submit_exc.message}" ) logger.error(msg) transaction.status_message = msg transaction.status = Transaction.STATUS.error transaction.save() return False transaction.status = Transaction.STATUS.pending_trust transaction.save() logger.info(f"Transaction for account {stellar_account} now pending_trust.") 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. builder.append_payment_op( destination=stellar_account, asset_code=asset.code, asset_issuer=asset.issuer, amount=str(payment_amount), ) if memo: builder.add_memo(memo) transaction_envelope = builder.build() transaction_envelope.sign(asset.distribution_seed) try: response = server.submit_transaction(transaction_envelope) # Functional errors at this stage are Horizon errors. except BaseHorizonError as exception: tx_result = TransactionResult.from_xdr(exception.result_xdr) op_result = tx_result.result.results[0] if op_result.tr.paymentResult.code != const.PAYMENT_NO_TRUST: msg = ( "Unable to submit payment to horizon, " f"non-trustline failure: {exception.message}" ) logger.error(msg) transaction.status_message = msg transaction.status = Transaction.STATUS.error transaction.save() return False msg = "trustline error when submitting transaction to horizon" logger.error(msg) transaction.status_message = msg transaction.status = Transaction.STATUS.pending_trust transaction.save() return False if not response.get("successful"): transaction_result = TransactionResult.from_xdr(response["result_xdr"]) msg = ( "Stellar transaction failed when submitted to horizon: " f"{transaction_result.result.results}" ) logger.error(msg) transaction.status_message = msg transaction.status = Transaction.STATUS.error transaction.save() return False 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.status_eta = 0 # No more status change. transaction.amount_out = payment_amount transaction.save() logger.info(f"Transaction {transaction.id} completed.") return True