コード例 #1
0
 def process_sep6_request(self, params: Dict,
                          transaction: Transaction) -> Dict:
     transaction.save()
     return {
         "type": "non_interactive_customer_info_needed",
         "fields": ["first_name", "last_name"],
     }
コード例 #2
0
ファイル: integrations.py プロジェクト: teury/django-polaris
 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()
コード例 #3
0
    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)
コード例 #4
0
    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
        )
コード例 #5
0
 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()
コード例 #6
0
ファイル: integrations.py プロジェクト: vcarl/django-polaris
    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.)")
コード例 #7
0
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)
コード例 #8
0
ファイル: integrations.py プロジェクト: vcarl/django-polaris
    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()
コード例 #9
0
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()
コード例 #10
0
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)
コード例 #11
0
    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
コード例 #12
0
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)
コード例 #13
0
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)
コード例 #14
0
    def requires_trustline(cls, transaction: Transaction) -> bool:
        try:
            _, pending_trust = PendingDeposits.get_or_create_destination_account(
                transaction)
        except RuntimeError as e:
            cls.handle_error(transaction, str(e))
            return True

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

        return False
コード例 #15
0
    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()
コード例 #16
0
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)
コード例 #17
0
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)
コード例 #18
0
    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
コード例 #19
0
ファイル: integrations.py プロジェクト: teury/django-polaris
    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
        )
コード例 #20
0
 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()
コード例 #21
0
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}")
コード例 #22
0
 def execute_outgoing_transaction(self, transaction: Transaction):
     transaction.amount_fee = 1
     transaction.status = Transaction.STATUS.completed
     transaction.save()
コード例 #23
0
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
コード例 #24
0
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
コード例 #25
0
 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"}}
コード例 #26
0
 def after_deposit(self, transaction: Transaction):
     transaction.channel_seed = None
     transaction.save()
コード例 #27
0
 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"}}
コード例 #28
0
 def process_sep6_request(self, params: Dict,
                          transaction: Transaction) -> Dict:
     transaction.save()
     return {"how": "test", "extra_info": "not a dict"}
コード例 #29
0
    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
コード例 #30
0
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")