def post(account: str, request: Request, **kwargs): if kwargs: return render_error_response( _("POST requests should not specify subresources in the URI")) elif not registered_sep31_receiver_integration.valid_sending_anchor( account): return render_error_response("invalid sending account", status_code=403) try: params = validate_post_request(request) except ValueError as e: return render_error_response(str(e)) # validate fields separately since error responses need different format missing_fields = validate_post_fields(params.get("fields"), params.get("asset"), params.get("lang")) if missing_fields: return Response( { "error": "transaction_info_needed", "fields": missing_fields }, status=400, ) transaction_id = create_transaction_id() # create memo transaction_id_hex = transaction_id.hex padded_hex_memo = "0" * (64 - len(transaction_id_hex)) + transaction_id_hex transaction_memo = memo_hex_to_base64(padded_hex_memo) # create transaction object without saving to the DB transaction = Transaction( id=transaction_id, protocol=Transaction.PROTOCOL.sep31, kind=Transaction.KIND.send, status=Transaction.STATUS.pending_sender, stellar_account=account, asset=params["asset"], amount_in=params["amount"], memo=transaction_memo, memo_type=Transaction.MEMO_TYPES.hash, receiving_anchor_account=params["asset"].distribution_account, ) error_data = registered_sep31_receiver_integration.process_post_request( params, transaction) try: response_data = process_post_response(error_data, transaction) except ValueError as e: logger.error(str(e)) return render_error_response(_("unable to process the request"), status_code=500) else: transaction.save() return Response(response_data, status=400 if "error" in response_data else 200)
def _update_transaction_info(cls, transaction: Transaction, stellar_txid: str, paging_token: str, source: str): transaction.stellar_transaction_id = stellar_txid transaction.from_address = source transaction.paging_token = paging_token transaction.save()
def create_channel_account(self, transaction: Transaction): kp = Keypair.random() settings.HORIZON_SERVER._client.get( f"https://friendbot.stellar.org/?addr={kp.public_key}" ) transaction.channel_seed = kp.secret transaction.save()
def find_matching_payment_op( cls, response: Dict, horizon_tx: HorizonTransaction, transaction: Transaction) -> Optional[Operation]: """ Determines whether or not the given ``response`` represents the given ``transaction``. Polaris does this by checking the 'memo' field in the horizon response matches the `transaction.memo`, as well as ensuring the transaction includes a payment operation of the anchored asset. :param response: the JSON response from horizon for the transaction :param horizon_tx: the decoded Transaction object contained in the Horizon response :param transaction: a database model object representing the transaction """ matching_payment_op = None for operation in horizon_tx.operations: if cls._check_payment_op(operation, transaction.asset): transaction.stellar_transaction_id = response["id"] transaction.from_address = operation.source transaction.paging_token = response["paging_token"] transaction.status_eta = 0 transaction.save() matching_payment_op = operation break return matching_payment_op
def instructions_for_pending_deposit(cls, transaction: Transaction): """ This function provides a message to the user containing instructions for how to initiate a bank deposit to the anchor's account. This particular implementation generates and provides a unique memo to match an incoming deposit to the user, but there are many ways of accomplishing this. If you collect KYC information like the user's bank account number, that could be used to match the deposit and user as well. """ # Generate a unique alphanumeric memo string to identify bank deposit memo = b64encode(str(hash(transaction)).encode()).decode()[:10].upper() transaction.external_extra = memo transaction.save() return ( _( "Include this code as the memo when making the deposit: " "<strong>%s</strong>. We will use " "this memo to identify you as the sender.\n(This deposit is " "automatically confirmed for demonstration purposes. Please " "wait.)" ) % transaction.external_extra )
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 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 process_sep6_request(self, params: Dict, transaction: Transaction) -> Dict: transaction.save() return { "type": "non_interactive_customer_info_needed", "fields": ["first_name", "last_name"], }
def instructions_for_pending_deposit(cls, transaction: Transaction): """ This function provides a message to the user containing instructions for how to initiate a bank deposit to the anchor's account. This particular implementation generates and provides a unique memo to match an incoming deposit to the user, but there are many ways of accomplishing this. If you collect KYC information like the user's bank account number, that could be used to match the deposit and user as well. """ if (transaction.kind == Transaction.KIND.deposit and transaction.status == Transaction.STATUS.pending_user_transfer_start): # Generate a unique alphanumeric memo string to identify bank deposit # # If you anticipate a high rate of newly created deposits, you wouldn't # want to make a DB query for every attempt to create a unique memo. # This only suffices for the sake of providing an example. memo, memo_exists = None, True while memo_exists: memo = str(uuid4()).split("-")[0].upper() memo_exists = Transaction.objects.filter( external_extra=memo).exists() transaction.external_extra = memo transaction.save() return ( "Include this code as the memo when making the deposit: " f"<strong>{transaction.external_extra}</strong>. We will use " f"this memo to identify you as the sender.\n(This deposit is " f"automatically confirmed for demonstration purposes. Please " f"wait.)")
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 withdraw( account: str, client_domain: Optional[str], request: Request, ) -> Response: args = parse_request_args(request) if "error" in args: return args["error"] args["account"] = account transaction_id = create_transaction_id() transaction_id_hex = transaction_id.hex padded_hex_memo = "0" * (64 - len(transaction_id_hex)) + transaction_id_hex transaction_memo = memo_hex_to_base64(padded_hex_memo) transaction = Transaction( id=transaction_id, stellar_account=account, asset=args["asset"], kind=Transaction.KIND.withdrawal, status=Transaction.STATUS.pending_user_transfer_start, receiving_anchor_account=args["asset"].distribution_account, memo=transaction_memo, memo_type=Transaction.MEMO_TYPES.hash, protocol=Transaction.PROTOCOL.sep6, more_info_url=request.build_absolute_uri( f"{SEP6_MORE_INFO_PATH}?id={transaction_id}"), on_change_callback=args["on_change_callback"], client_domain=client_domain, ) # All request arguments are validated in parse_request_args() # except 'type', 'dest', and 'dest_extra'. Since Polaris doesn't know # which argument was invalid, the anchor is responsible for raising # an exception with a translated message. try: integration_response = rwi.process_sep6_request(args, transaction) except ValueError as e: return render_error_response(str(e)) try: response, status_code = validate_response(args, integration_response, transaction) except ValueError as e: logger.error(str(e)) return render_error_response(_("unable to process the request"), status_code=500) if status_code == 200: response["memo"] = transaction.memo response["memo_type"] = transaction.memo_type logger.info(f"Created withdraw transaction {transaction.id}") transaction.save() elif Transaction.objects.filter(id=transaction.id).exists(): logger.error( "Do not save transaction objects for invalid SEP-6 requests") return render_error_response(_("unable to process the request"), status_code=500) return Response(response, status=status_code)
def after_deposit(cls, transaction: Transaction): """ Deposit was successful, do any post-processing necessary. In this implementation, we remove the memo from the transaction to avoid potential collisions with still-pending deposits. """ logger.info(f"Successfully processed transaction {transaction.id}") transaction.external_extra = None transaction.save()
def deposit(account: str, client_domain: Optional[str], request: Request,) -> Response: args = parse_request_args(request) if "error" in args: return args["error"] args["account"] = account transaction_id = create_transaction_id() transaction = Transaction( id=transaction_id, stellar_account=account, asset=args["asset"], kind=Transaction.KIND.deposit, status=Transaction.STATUS.pending_user_transfer_start, memo=args["memo"], memo_type=args["memo_type"] or Transaction.MEMO_TYPES.text, to_address=account, protocol=Transaction.PROTOCOL.sep6, more_info_url=request.build_absolute_uri( f"{SEP6_MORE_INFO_PATH}?id={transaction_id}" ), claimable_balance_supported=args["claimable_balance_supported"], on_change_callback=args["on_change_callback"], client_domain=client_domain, ) try: integration_response = rdi.process_sep6_request(args, transaction) except ValueError as e: return render_error_response(str(e)) except APIException as e: return render_error_response(str(e), status_code=e.status_code) try: response, status_code = validate_response( args, integration_response, transaction ) except (ValueError, KeyError) as e: logger.error(str(e)) return render_error_response( _("unable to process the request"), status_code=500 ) if status_code == 200: logger.info(f"Created deposit transaction {transaction.id}") transaction.save() elif Transaction.objects.filter(id=transaction.id).exists(): logger.error("Do not save transaction objects for invalid SEP-6 requests") return render_error_response( _("unable to process the request"), status_code=500 ) return Response(response, status=status_code)
def match_transaction(cls, response: Dict, transaction: Transaction) -> bool: """ Determines whether or not the given ``response`` represents the given ``transaction``. Polaris does this by constructing the transaction memo from the transaction ID passed in the initial withdrawal request to ``/transactions/withdraw/interactive``. To be sure, we also check for ``transaction``'s payment operation in ``response``. :param response: a response body returned from Horizon for the transaction :param transaction: a database model object representing the transaction """ try: memo_type = response["memo_type"] response_memo = response["memo"] successful = response["successful"] stellar_transaction_id = response["id"] envelope_xdr = response["envelope_xdr"] except KeyError: logger.warning( f"Stellar response for transaction missing expected arguments" ) return False if memo_type != "hash": logger.warning( f"Transaction memo for {transaction.id} was not of type hash" ) return False # The memo on the response will be base 64 string, due to XDR, while # the memo parameter is base 16. Thus, we convert the parameter # from hex to base 64, and then to a string without trailing whitespace. if response_memo != format_memo_horizon(transaction.withdraw_memo): return False horizon_tx = TransactionEnvelope.from_xdr( response["envelope_xdr"], network_passphrase=settings.STELLAR_NETWORK_PASSPHRASE, ).transaction found_matching_payment_op = False for operation in horizon_tx.operations: if cls._check_payment_op( operation, transaction.asset.code, transaction.amount_in ): transaction.stellar_transaction_id = stellar_transaction_id transaction.from_address = horizon_tx.source.public_key transaction.save() found_matching_payment_op = True break return found_matching_payment_op
def deposit(account: str, request: Request) -> Response: args = parse_request_args(request) if "error" in args: return args["error"] args["account"] = account transaction_id = create_transaction_id() transaction = Transaction( id=transaction_id, stellar_account=account, asset=args["asset"], kind=Transaction.KIND.deposit, status=Transaction.STATUS.pending_user_transfer_start, memo=args["memo"], memo_type=args["memo_type"] or Transaction.MEMO_TYPES.text, to_address=account, protocol=Transaction.PROTOCOL.sep6, ) try: integration_response = rdi.process_sep6_request(args, transaction) except ValueError as e: return render_error_response(str(e)) except APIException as e: return render_error_response(str(e), status_code=e.status_code) try: response, status_code = validate_response( args, integration_response, transaction ) except (ValueError, KeyError): return render_error_response( _("unable to process the request"), status_code=500 ) if status_code == 200: logger.info(f"Created deposit transaction {transaction.id}") transaction.save() elif Transaction.objects.filter(id=transaction.id).exists(): logger.error("Do not save transaction objects for invalid SEP-6 requests") return render_error_response( _("unable to process the request"), status_code=500 ) return Response(response, status=status_code)
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 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 process_post_request( self, params: Dict, transaction: Transaction ) -> Optional[Dict]: _ = params.get("sender_id") # not actually used receiver_id = params.get("receiver_id") transaction_fields = params.get("fields", {}).get("transaction") for field, val in transaction_fields.items(): if not isinstance(val, str): return {"error": f"'{field}'" + _(" is not of type str")} receiving_user = PolarisUser.objects.filter(id=receiver_id).first() if not receiving_user: return {"error": "customer_info_needed", "type": "sep31-receiver"} elif not (receiving_user.bank_account_number and receiving_user.bank_number): receiving_user.bank_account_number = transaction_fields["account_number"] receiving_user.bank_number = transaction_fields["routing_number"] receiving_user.save() transaction.save() PolarisUserTransaction.objects.create( user=receiving_user, transaction_id=transaction.id )
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 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 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 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
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 process_sep6_request(self, params: Dict, transaction: Transaction) -> Dict: transaction.save() return {"how": "test", "extra_info": "not a dict"}
def execute_outgoing_transaction(self, transaction: Transaction): transaction.amount_fee = 1 transaction.status = Transaction.STATUS.completed transaction.save()
def process_sep6_request(self, params: Dict, transaction: Transaction) -> Dict: if params.get("type") == "bad type": raise ValueError() transaction.save() return {"extra_info": {"test": "test"}}
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.status_eta = 5 # Ledger close time. transaction.save() logger.info(f"Initiating Stellar deposit for {transaction.id}") maybe_make_callback(transaction) if transaction.envelope_xdr: envelope = TransactionEnvelope.from_xdr( transaction.envelope_xdr, settings.STELLAR_NETWORK_PASSPHRASE ) 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.status_eta = 0 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 interactive_withdraw(request): """ `GET /withdraw/interactive_withdraw` opens a form used to input information about the withdrawal. This creates a corresponding transaction in our database. """ transaction_id = request.GET.get("transaction_id") if not transaction_id: return render_error_response("no 'transaction_id' provided", content_type="text/html") asset_code = request.GET.get("asset_code") if not asset_code or not Asset.objects.filter(code=asset_code).exists(): return render_error_response("invalid 'asset_code'", content_type="text/html") # GET: The server needs to display the form for the user to input withdrawal information. if request.method == "GET": form = WithdrawForm() # POST: The user submitted a form with the withdrawal info. else: if Transaction.objects.filter(id=transaction_id).exists(): return render_error_response( "transaction with matching 'transaction_id' already exists", content_type="text/html") form = WithdrawForm(request.POST) asset = Asset.objects.get(code=asset_code) form.asset = asset # If the form is valid, we create a transaction pending user action # and render the success page. if form.is_valid(): amount_in = form.cleaned_data["amount"] amount_fee = calc_fee(asset, settings.OPERATION_WITHDRAWAL, amount_in) # We use the transaction ID as a memo on the Stellar transaction for the # payment in the withdrawal. This lets us identify that as uniquely # corresponding to this `Transaction` in the database. But a UUID4 is a 32 # character hex string, while the Stellar HashMemo requires a 64 character # hex-encoded (32 byte) string. So, we zero-pad the ID to create an # appropriately sized string for the `HashMemo`. transaction_id_hex = uuid.UUID(transaction_id).hex withdraw_memo = "0" * ( 64 - len(transaction_id_hex)) + transaction_id_hex transaction = Transaction( id=transaction_id, stellar_account=settings.STELLAR_DISTRIBUTION_ACCOUNT_ADDRESS, asset=asset, kind=Transaction.KIND.withdrawal, status=Transaction.STATUS.pending_user_transfer_start, amount_in=amount_in, amount_fee=amount_fee, withdraw_anchor_account=settings. STELLAR_DISTRIBUTION_ACCOUNT_ADDRESS, withdraw_memo=withdraw_memo, withdraw_memo_type=Transaction.MEMO_TYPES.hash, ) transaction.save() serializer = TransactionSerializer( transaction, context={"more_info_url": _construct_more_info_url(request)}, ) tx_json = json.dumps({"transaction": serializer.data}) return Response( { "tx_json": tx_json, "transaction": transaction, "asset_code": asset_code, }, template_name="transaction/more_info.html") return Response({"form": form}, template_name="withdraw/form.html")
def process_sep6_request(self, params: Dict, transaction: Transaction) -> Dict: if params.get("type") not in [None, "good_type"]: raise ValueError("invalid 'type'") transaction.save() return {"how": "test", "extra_info": {"test": "test"}}
def after_deposit(self, transaction: Transaction): transaction.channel_seed = None transaction.save()