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()
def create_stellar_deposit(transaction: Transaction) -> bool: """ Performs final status and signature checks before calling submit_stellar_deposit(). Returns true on successful submission, false otherwise. `transaction` will be placed in the error status if submission fails or if it is a multisig transaction and is not signed by the channel account. """ 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) # if the distribution account's master signer's weight is great or equal to the its # medium threshold, verify the transaction is signed by it's channel account master_signer = None if transaction.asset.distribution_account_master_signer: master_signer = transaction.asset.distribution_account_master_signer thresholds = transaction.asset.distribution_account_thresholds if not master_signer or master_signer["weight"] < thresholds[ "med_threshold"]: envelope = TransactionEnvelope.from_xdr( transaction.envelope_xdr, settings.STELLAR_NETWORK_PASSPHRASE) try: _verify_te_signed_by_account_id(envelope, transaction.channel_account) except InvalidSep10ChallengeError: transaction.status = Transaction.STATUS.error transaction.status_message = gettext( "Multisig transaction's envelope was not signed by channel account" ) transaction.save() return False # otherwise, create the envelope and sign it with the distribution account's secret else: distribution_acc, _ = get_account_obj( Keypair.from_public_key(transaction.asset.distribution_account)) envelope = create_transaction_envelope(transaction, distribution_acc) envelope.sign(transaction.asset.distribution_seed) try: submit_stellar_deposit(transaction, envelope) except (RuntimeError, BaseHorizonError) as e: transaction.status_message = f"{e.__class__.__name__}: {e.message}" transaction.status = Transaction.STATUS.error transaction.save() logger.error(transaction.status_message) return False else: return True
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()
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)
def execute_deposit(transaction: Transaction) -> bool: valid_statuses = [ Transaction.STATUS.pending_user_transfer_start, Transaction.STATUS.pending_anchor, ] if transaction.kind != transaction.KIND.deposit: raise ValueError("Transaction not a deposit") elif transaction.status not in valid_statuses: raise ValueError( f"Unexpected transaction status: {transaction.status}, expecting " f"{' or '.join(valid_statuses)}.") if transaction.status != Transaction.STATUS.pending_anchor: transaction.status = Transaction.STATUS.pending_anchor transaction.status_eta = 5 # Ledger close time. transaction.save() logger.info(f"Initiating Stellar deposit for {transaction.id}") # launch the deposit Stellar transaction. return create_stellar_deposit(transaction)
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 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}") transaction.status = Transaction.STATUS.pending_anchor transaction.status_eta = 5 # Ledger close time. transaction.save() # launch the deposit Stellar transaction. return create_stellar_deposit(transaction.id)
def create_stellar_deposit(transaction: Transaction, destination_exists: bool = False) -> bool: """ Create and submit the Stellar transaction for the deposit. 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) # if we don't know if the destination account exists if not destination_exists: 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) return False if created or pending_trust: # the account is pending_trust for the asset to be received if pending_trust and transaction.status != Transaction.STATUS.pending_trust: transaction.status = Transaction.STATUS.pending_trust transaction.save() return False # if the distribution account's master signer's weight is great or equal to the its # medium threshold, verify the transaction is signed by it's channel account master_signer = None if transaction.asset.distribution_account_master_signer: master_signer = transaction.asset.distribution_account_master_signer thresholds = transaction.asset.distribution_account_thresholds if not (master_signer and master_signer["weight"] >= thresholds["med_threshold"]): multisig = True envelope = TransactionEnvelope.from_xdr( transaction.envelope_xdr, settings.STELLAR_NETWORK_PASSPHRASE) try: _verify_te_signed_by_account_id(envelope, transaction.channel_account) except InvalidSep10ChallengeError: transaction.status = Transaction.STATUS.error transaction.status_message = gettext( "Multisig transaction's envelope was not signed by channel account" ) transaction.save() return False # otherwise, create the envelope and sign it with the distribution account's secret else: multisig = False distribution_acc, _ = get_account_obj( Keypair.from_public_key(transaction.asset.distribution_account)) envelope = create_transaction_envelope(transaction, distribution_acc) envelope.sign(transaction.asset.distribution_seed) transaction.envelope_xdr = envelope.to_xdr() try: return submit_stellar_deposit(transaction, multisig=multisig) except RuntimeError as e: transaction.status_message = str(e) transaction.status = Transaction.STATUS.error transaction.save() logger.error(transaction.status_message) return False
def get_or_create_transaction_destination_account( transaction: Transaction, ) -> Tuple[Optional[Account], bool, bool]: """ Returns: Tuple[Optional[Account]: The account(s) found or created for the Transaction bool: boolean, True if created, False otherwise. bool: boolean, 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 not require non-master signatures, Polaris can create the destination account using the distribution account. 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(). 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, False, is_pending_trust(transaction, json_resp) except RuntimeError: master_signer = None if transaction.asset.distribution_account_master_signer: master_signer = transaction.asset.distribution_account_master_signer thresholds = 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) 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 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, _ = get_account_obj( Keypair.from_public_key(transaction.stellar_account) ) return account, True, True except BaseHorizonError as e: raise RuntimeError(f"Horizon error when loading stellar account: {e.message}")
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
def execute_outgoing_transaction(self, transaction: Transaction): transaction.amount_fee = 1 transaction.status = Transaction.STATUS.completed transaction.save()