Enable Hosted Deposits & Withdrawals

SEP-24 defines a standardized set of APIs that allow client applications and anchors to communicate for the purpose of depositing or withdrawing value on and off the Stellar blockchain. Practically speaking, users can use applications that implement the client side of SEP-24 to connect to businesses that will accept off-chain value, such as US dollar fiat, in exchange for on-chain value, such as USDC, and vice-versa.

SEP-24 is the hosted version of this solution, meaning the user’s application must open a webview hosted by the third-party anchor in order for the user provide the information necessary to complete the transaction. SEP-6 is an alternative to SEP-24 that supports a pure API-style solution for the same use case. See the Enable Deposts & Withdrawals via API documentation for guidance on implementing it.

Configure Settings

Activate SEP-24

Add SEP-24 as an active SEP in your .env file. SERVER_JWT_KEY is also required.

ACTIVE_SEPS=sep-1,sep-10,sep-24
HOST_URL=http://localhost:8000
LOCAL_MODE=1
ENABLE_SEP_0023=1
SIGNING_SEED=S...
SERVER_JWT_KEY=...

Add Browser Session Support

SessionMiddleware is required for all SEP-24 deployments. TimeZoneMiddleware is included in Polaris and ensures Django uses the correct timezone when rendering content to the user. If included, it must be added after SessionMiddleware.

MIDDLEWARE = [
    ...,
    'django.contrib.sessions.middleware.SessionMiddleware',
    'polaris.middleware.TimezoneMiddleware',
    ...
]

Allow Custom Templates

Add the following variable to your settings.py file.

FORM_RENDERER = "django.forms.renderers.TemplatesSetting"

This allows Polaris to override django’s default HTML form widgets to provide a great UI out of the box. They can also be replaced with your own custom HTML widgets. Working with Templates section for more information.

Secure Session Cookies

The webview, or interactive flow, supported by Polaris uses browser session cookies for maintaining state. These cookies should only be used when the client is using HTTPS to ensure the user’s session cannot be highjacked by a malicious actor.

In your anchor/anchor/settings.py file, add the following variable.

SESSION_COOKIE_SECURE = True

Polaris requires this setting to be True for SEP-24 deployments if not in LOCAL_MODE, which should only be used for local development.

Configure Static Assets

Polaris comes with a UI for displaying forms and transaction information. This UI will be rendered in a webview by the user’s application when they initiate a deposit or withdrawal.

Make sure django.contrib.staticfiles is listed in INSTALLED_APPS.

INSTALLED_APPS = [
    ...,
    "django.contrib.staticfiles",
    ...,
]

Additionally, to serve static files in production, use the middleware provided by whitenoise, which comes with your installation of Polaris. It should be near the top of the list for the best performance, but still under CorsMiddleware.

MIDDLEWARE = [
    ...,
    "corsheaders.middleware.CorsMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    ...,
]

Add the following to your settings.py as well:

STATIC_URL = "<your static url path, /static/ by default>"
STATIC_ROOT = os.path.join(BASE_DIR, "<where all static files will be collected>")
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"

Since whitenoise will now be serving your static files, use the --nostatic flag when using the runserver command locally.

Collect the static files Polaris provides into your app:

python manage.py collectstatic --no-input

Create a Stellar Asset

Businesses can choose to create or issue their own Stellar asset or they use a Stellar asset issued by another organization. This is a business decision and is out of the scope of this documentation. Polaris supports both cases, and here we’ll create our own asset for the sake of demonstration.

Add an Asset to the Database

Stellar assets are identified using a code, such as USDC, and the Stellar account that created the asset, such as GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN.

Lets drop into the python console.

python anchor/manage.py shell

Generate two public and private key pairs. These key pairs will be used for the issuing account, which creates and burns the asset, as well as a distribution account, which will hold a balance of the asset for disbursing and receiving payments.

from stellar_sdk import Keypair

issuer = Keypair.random()
distributor = Keypair.random()

with open("secretKeys.txt", "w") as f:
    f.write(f"{issuer.secret}\n{distributor.secret}")

This generated two key pairs and wrote the secret keys to the file system. Make sure to keep these secret keys secret. The issuing account’s secret key is all thats needed to create more of the asset, and the distribution account’s secret key will always hold enough of the asset to satisfy the transaction volume of the service.

Finally, create the Asset object and save it to the database. There are many columns not shown here, so check out the API Reference for a complete list.

from polaris.models import Asset

Asset.objects.create(
    code="TEST",
    issuer=issuer.public_key,
    distribution_seed=distributor.secret,
    sep24_enabled=True,
    deposit_enabled=True,
    withdrawal_enabled=True,
    symbol="$"
)

The distribution_seed column is encrypted at the database layer using Fernet symmetric encryption, and only decrypted when held in memory within an Asset object. It uses your Django project’s SECRET_KEY setting to generate the encryption key, so make sure its value is unguessable and kept a secret.

Issue the Asset on Stellar

You can finally create the asset on the Stellar blockchain. This documentation assumes you’re issuing on testnet. If you’re working on mainnet, you may want to hold off from issuing your asset until you’re ready to go live.

Polaris has a built-in command for issuing assets on testnet.

python anchor/manage.py testnet issue --asset TEST --issuer-seed <...> --distribution-seed <...>

Polaris will ask you to specify a home domain for the asset. This must be the domain that hosts your stellar.toml file.

Your asset should not exist on Stellar’s testnet. You can use a block explorer to take a look at your issuing and distribution accounts.

You can optionally specify the amount to be issued. See the CLI Commands documentation for more information.

Integrations

There are several pieces of functionality required to run a anchor that are custom to each business. Polaris implements everything but these pieces, and calls functions that have been passed to register_integrations() in order to invoke custom functionality implemented by the business.

Communicating Fee Structure

Client applications may want to communicate the fee a user would have to pay before initiating a transaction. By default, Polaris uses the fee values assigned to each Asset object for displaying fee information in GET /info requests and calculating fees in GET /fee requests.

However, the values assigned to the Asset are fixed. If the fee you want to charge is variable depending on factors external to Polaris, you must implement your custom fee calculation logic in a function and replace calculate_fee() by passing your function to register_integrations(). See the function definitions for more information.

Defining Django Forms

SEP-24 anchors must implement a user web-based interface that collects KYC and transaction information from the user of the client application. Because the information necessary to complete transactions differs for each business, Polaris expects the anchor to provide Django form objects that can be rendered as HTML to the user.

Define a set of forms that collect all of the information needed to facilitate a transaction in a anchor/anchor/sep24/forms.py file.

from django import forms
from us import states  # https://pypi.org/project/us/

state_list = sorted(
    status.mapping("abbr", "name").items(),
    key=lambda x: x[1]
)

class ContactForm(forms.Form):
    first_name = forms.CharField()
    last_name = forms.CharField()
    email = forms.EmailField()

class AddressForm(forms.Form):
    address_1 = forms.CharField()
    address_2 = forms.CharField()
    city = forms.CharField()
    state = forms.ChoiceField(choices=state_list)
    zip_code = forms.CharField()

class BankAccount(forms.Form):
    account_number = forms.CharField()
    routing_number = forms.CharField()

Django’s form capabilities are comprehensive, so check out the documentation if you want to customize error messages, add validations to specific fields, and more.

Notice how some transaction information is not collected, such as the amount. Because every anchor needs to collect the transaction amount, Polaris defines a TransactionForm class that includes proper validations. It is highly recommended to use this form or a subclass of it.

Processing Form Data

When a user initiates a transaction, Polaris will return a URL that the wallet will open in a webview. Once opened, Polaris does the following:

  1. Polaris calls form_for_transaction()

  2. Polaris calls content_for_template()

  3. Polaris renders the template with the form and content returned from the methods called

  4. The user enters the information requested by the form and submits

  5. Polaris calls form_for_transaction() again. The form returned must be the same form returned previously, because Polaris calls Form.is_valid() to ensure the data provided is valid. If it isn’t, the form is re-rendered with the appropriate error message.

  6. When Form.is_valid() is True, Polaris calls after_form_validation(). This method should change the state of the user’s flow so the next call to form_for_transaction() returns the next form.

  7. Repeat

When form_for_transaction() and content_for_template() both return None, Polaris assumes the anchor is done collecting and processing information and redirects the webview to a transaction information page, called the “more info page” by SEP-24.

All of the methods used to process form data are defined on the DepositIntegration and WithdrawIntegration classes. Create subclasses of both and implement the methods used to process the forms.

from decimal import Decimal
from django import forms
from rest_framework.request import Request
from polaris.models import Transaction
from polaris.templates import Template
from polaris.integrations import (
    DepositIntegration,
    WithdrawIntegration,
    TransactionForm
)
from .users import user_for_account, create_user

class AnchorDeposit(DepositIntegration):
    def form_for_transaction(
        self,
        request: Request,
        transaction: Transaction,
        post_data: dict = None,
        amount: Decimal = None,
        *args,
        **kwargs
    ):
        # if we haven't collected amount, collect it
        if not transaction.amount_in:
            if post_data:
                return TransactionForm(transaction, post_data)
            else:
                return TransactionForm(transaction, initial={"amount": amount})

        # if a user doesn't exist for this Stellar account,
        # collect their contact info
        user = user_for_account(
            transaction.muxed_account or transaction.stellar_account
        )
        if not user:
            if post_data:
                return ContactForm(post_data)
            else:
                return ContactForm()
        # if we haven't gotten the user's full address, colelct it
        elif not user.full_address:
            if post_data:
                return AddressForm(post_data)
            else:
                return AddressForm()
        # we don't have anything more to collect
        else:
            return None

    def after_form_validation(
        self,
        request: Request,
        form: forms.Form,
        transaction: Transaction,
        *args,
        **kwargs,
    ):
        if isinstance(form, TransactionForm):
            # Polaris automatically assigns amount to Transaction.amount_in
            return
        if isinstance(form, ContactForm):
            # creates the user to be returned from user_for_account()
            create_user(form)
        elif isinstance(form, AddressForm):
            # assigns user.full_address
            update_user_address(form)

    def content_for_template(
        self,
        request: Request,
        template: Template,
        form: Optional[forms.Form] = None,
        transaction: Optional[Transaction] = None,
        *args,
        **kwargs,
    ):
        if form is not None or template == Template.MORE_INFO:
            # provides a label for the image displayed at the top of each page
            return {"icon_label": "Anchor Inc."}
        else:
            # we're done
            return None

Similar logic should be implemented for WithdrawIntegration. For more detailed information on any of the classes or functions used about, see the API Reference.

Register Integrations

Once you’ve implemented the integration functions, you need to register them via register_integrations(). Open your anchor/anchor/apps.py file.

from django.apps import AppConfig

class AnchorConfig(AppConfig):
    name = 'anchor'

    def ready(self):
        from polaris.integrations import register_integrations
        from .sep1 import return_toml_contents
        from .deposit import AnchorDeposit
        from .withdraw import AnchorWithdraw

        register_integrations(
            toml=return_toml_contents,
            deposit=AnchorDeposit(),
            withdraw=AnchorWithdraw()
        )

Working with Templates

Polaris uses Django’s template system for defining the UI content rendered to users. If you’re interested in customizing Polaris’ UI, read Django’s template documentation before continuing.

Polaris’ templates have the following inheritance structure:

  • templates/polaris/base.html
    • templates/polaris/deposit.html

    • templates/polaris/withdraw.html

    • templates/polaris/more_info.html

base.html defines the top-level HTML tags like html, body, and head, while each of the four other templates extend base.html by overriding its content block, among others. deposit.html and withdraw.html are very similar and are used for pages that display forms. more_info.html simply displays transaction details.

Polaris will render its own deposit, withdraw, and more info templates by default, but anchors have the option to extend or replace Polaris templates, or use a completely different set of templates.

Extending or Replacing Polaris Templates

In order to or extend a Polaris Template, anchors must create a file with the same path and name of the file its replacing. Once created, the anchor can override any block tag defined in the template (or it’s parent templates).

Create a templates directory.

mkdir anchor/anchor/templates
mkdir anchor/anchor/templates/polaris

Create a file for the template you’d like to extend or replace. In this guide we’ll extend base.html.

touch anchor/anchor/templates/polaris/base.html

Polaris provides two block tags that are intentionally left empty for anchors to extend: extra_head and extra_body. These blocks should be used if you’ll looking to add additional CSS or JavaScript files to any of your templates.

In case you want to override Polaris’s JavaScript <script> tags, there’s also a body_scripts block.

You are also allowed to extend any of the blocks actually implemented by Polaris, such as header, content, and footer. Note that header contains extra_header, and body contains extra_body and body_scripts.

Lets add Google Analytics to our base template.

{% extends "polaris/base.html" %}

{% block extra_head %}

    <!-- Global site tag (gtag.js) - Google Analytics -->
    <script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
    <script>
      window.dataLayer = window.dataLayer || [];
      function gtag(){window.dataLayer.push(arguments);}
      gtag('js', new Date());

      gtag('config', 'GA_MEASUREMENT_ID');
    </script>

{% endblock %}

We can make our HTML code cleaner by putting this JavaScript code in its own JS file.

mkdir anchor/anchor/static/js
touch anchor/anchor/static/js/googleAnalytics.js

Paste the JS code into googleAnalytics.js. Then, link the JS file in your base template extension.

{% extends "polaris/base.html" %} {% load static %}

{% block extra_head %}

    <script src="{% static 'sep24_scripts/google_analytics.js' %}"></script>

{% endblock %}

If you’re unfamiliar with the syntax of Django’s templates, check out the template syntax documentation and particularly the block documentation.

If you wish to replace a template completely, create a file with the same relative path from the templates directory but do not use the extend keyword. Instead, simply write a Django template that does not extend one defined by Polaris.

Using Custom Templates

Anchors don’t have to use any of Polaris’ templates, and can instead explicitly provide templates for Polaris to render per-request.

Simply include a template_name key in the dictionary returned by content_for_template(), which is called every time a template is about to be rendered. The value of template_name key must be the file path of template you’d like Polaris to render relative to your applications’s templates directory.

Providing Context to Templates

Whenever a template is rendered and displayed to the user, its rendered using a context, which is a Python dictionary containing key-value pairs that can be used to alter the content rendered.

Polaris has an integration function that allows anchors to add key-value pairs to the context used whenever a template is about to be rendered, content_for_template(). See the documentation on this function for more detailed information.

Warning

Any content returned from content_for_template() that originates from user input should be validiated and sanitized.

Replacing Static Assets

Similar to Polaris’ templates, Polaris’ static assets can also be replaced by creating a file with a matching path relative to it’s app’s static directory. This allows anchors to customize the UI’s appearance. For example, you can replace Polaris’ base.css file to give the interactive flow pages a different look using your own polaris/base.css file.

Note that if you would like to add CSS styling in addition to what Polaris provides, you should extend the Polaris template and define an extra_head block containing the associated link tags.

Using an External Interactive Flow

If you do not want to use Django’s built-in forms and template system, as well as Polaris’ integration functions such as form_for_transaction(), Polaris enables anchors to use an external interactive flow served by a completely independent application.

Anchors can return a URL for an external applicaiton from interactive_url(). When wallets initiate a transaction, Polaris will first provide its own URL, however, when the wallet opens this URL in a webview, Polaris will call interactive_url() and redirect to the URL returned. This allows the wallet to create a web session with the Polaris web server before being redirected to the external application, which is necessary for the next step used in this approach.

def interactive_url(...) -> Optional[str]:
    return f"http://app.anchor.com?id={transaction.id}&callback={callback}&amount={amount}"

Once the user has completed the external application’s interactive flow, Polaris’ Transaction record, and maybe other models used by the anchor, will need to be updated with the information collected by the external application, such as the transaction’s amounts and fees.

Polaris provides an endpoint, GET /sep24/transactions/<deposit or withdraw>/interactive/complete, that should be used to send this information back to the Polaris web server. Polaris will ensure a web session was created for the wallet and then call after_interactive_flow() with the request and relevant Transaction object. See the function for documentation on how it should be implemented.

def after_interactive_flow(self, request: Request, transaction: Transaction):
    transaction.amount_in = Decimal(request.query_params.get("amount_in"))
    transaction.amount_fee = Decimal(request.query_params.get("amount_fee"))
    transaction.amount_out = Decimal(request.query_params.get("amount_out"))
    transaction.status = Transaction.STATUS.pending_user_transfer_start
    transaction.save()

The endpoint requires a transaction_id parameter, and optionally accepts the callback parameter, which may have originally been appended to the interactive flow’s URL by the wallet.

If the request is valid and successfully processed, the endpoint will return a redirect response for Polaris’ more_info.html page. This page is important because it will automatically make the callback requested by the wallet, if specified. Anchors can choose not to follow this redirect, but must make the requested SEP-24 callback themselves.

Testing with the Demo Wallet

Start up the web server.

python anchor/manage.py runuserver --nostatic

Go to https://demo-wallet.stellar.org. Generate a new Keypair and select the “Add Asset” button. Enter the code and localhost:8000 for the anchor home domain. Entering the issuing address is not necessary.

You should see a 0 balance of the asset you’ve issued. Select the drop down on the right labeled “Select action”, and select “SEP-24 Deposit”. Select “Start”.

If you’ve configured your application and implemented the integrations properly, you should see the demo wallet hit your SEP-1, 10, and 24 APIs, and it will eventually open up a popup containing your webview or interactive flow.

Enter the information each form requests. Once you’ve entered all the information, you should be redirected to a transaction status page. At this point, Polaris is waiting for the user (you) to send funds to the business’s off-chain account.

We’ll explore how to connect off-chain payment rails to your Polaris application for detecting these payments in Connecting Off-Chain Payment Rails.