SEP-6 & SEP-24

SEP-6 and SEP-24 are both protocols for transferring assets on and off the stellar network. The difference between the two is how they collect information for the transfer.

SEP-6 is non-interactive, meaning the server expects the client (usually a wallet application) to communicate with the server in a purely automated fashion via the API endpoints described in the proposal.

Comparatively, SEP-24 is interactive, meaning the server requires the user of the client application to input transfer information via a UI controlled by the anchor server instead of the client.

Using Polaris you can run one or both SEP implementations. Each have their pros and cons, so make sure you understand the proposals before choosing.

Configuration

Add the SEPs to ACTIVE_SEPS in in your settings file.

ACTIVE_SEPS = ["sep-1", "sep-6", "sep-24", ...]

Static Assets

Polaris comes with a UI for displaying forms and transaction information. While SEP-6 doesn’t use HTML forms, this is required in order to display transaction details available from the /more_info endpoint.

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_ROOT = os.path.join(BASE_DIR, "<your static root directory>")
STATIC_URL = "<your static url path>"
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"

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

The last step is to collect the static files Polaris provides into your app:

python manage.py collectstatic --no-input

Replacing Polaris UI Assets

Its also possible to replace the static assets from Polaris. 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. You can do this by adding your own polaris/base.css file to your app’s static directory.

In general, replacement asset files (.html, .css, etc.) must have the same path and name of the file its replacing. See the structure of Polaris’ static assets directory here.

See the documentation on serving static files in Django for more information.

SEP-24 Configuration

SEP-24’s interactive flow uses a short-lived JWT to authenticate users, so add the follow environment variable.

SERVER_JWT_KEY="yoursupersecretjwtkey"

Add Polaris’ PolarisSameSiteMiddleware to your settings.MIDDLEWARE. SessionMiddleware must be listed below PolarisSameSiteMiddleware.

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

Add the following to your settings.py as well:

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 HTML widgets as described in the previous section.

Integrations

Shared Integrations

Because they share a common objective, Polaris’ SEP-6 and SEP-24 implementations share many of the same integrations. We’ll go over these integrations first, then the integrations specific to each SEP.

Polaris provides a set of base classes that should be subclassed for processing transactions, DepositIntegration, WithdrawalIntegration, and RailsIntegration.

These subclasses, along with several other integration functions, should be registered with Polaris once implemented. See the Registering Integrations section for more information.

Banking Rails

One of the pieces of SEP-6 and SEP-24 Polaris cannot implement itself is the API connection to an anchor’s partner financial entity. That is why Polaris provides a set of integration functions for anchors to implement themselves.

polaris.integrations.RailsIntegration.poll_pending_deposits(self, pending_deposits: django.db.models.query.QuerySet) → List[polaris.models.Transaction]

This function should poll the appropriate financial entity for the state of all pending_deposits and return the ones that have externally completed, meaning the off-chain funds are available in the anchor’s account.

Per SEP-24, make sure to save the transaction’s from_address field with the account the funds originated from.

Also ensure the amount deposited to the anchor’s account matches each transaction’s amount_in field. Client applications may send an amount that differs from the amount originally specified in a SEP-24 API call, or in the case of SEP-6 transactions, amount_in will be None. If amount_in differs from the amount deposited, assign the amount deposited to amount_in and update amount_fee to appropriately.

For every transaction that is returned, Polaris will submit it to the Stellar network. If a transaction is completed on the network, the after_deposit integration function will be called, however implementing this function is optional.

If the Stellar network is unable to execute a transaction returned from this function, it’s status will be marked as error and its status_message attribute will be assigned a description of the problem that occurred. If the Stellar network is successful, the transaction will be marked as completed.

pending_deposits is a QuerySet of the form

Transactions.object.filter(
    kind=Transaction.KIND.deposit,
    status=Transaction.STATUS.pending_user_transfer_start
)

If you have many pending deposits, you may way want to batch the retrieval of these objects to improve query performance and memory usage.

Parameters:pending_deposits – a django Queryset for pending Transactions
Returns:a list of Transaction database objects which correspond to successful user deposits to the anchor’s account.
polaris.integrations.RailsIntegration.execute_outgoing_transaction(self, transaction: polaris.models.Transaction)

Send the amount of the off-chain asset specified by transaction minus fees to the user associated with transaction. This function is used for SEP-6 & SEP-24 withdraws as well as SEP-31 payments.

When this function is called, transaction.amount_in is the amount sent to the anchor, not the amount specified in a SEP-24 or SEP-31 API call. This matters because the amount actually sent to the anchor may differ from the amount specified in an API call. That is why you should always validate transaction.amount_in and calculate transaction.amount_fee here.

If the amount is invalid in some way, the anchor must choose how to handle it. If you choose to refund the payment in its entirety, change transaction.status to “error”, assign an appropriate message to transaction.status_message, and update transaction.refunded to True.

You could also refund a portion of the amount and continue processing the remaining amount. In this case, the transaction.status column should be assigned one of the expected statuses for this function, mentioned below, and the amount_in field should be reassigned the value the anchor is accepted.

If the funds transferred to the user become available in the user’s off-chain account immediately, update Transaction.status to Transaction.STATUS.completed. If the transfer was simply initiated and is pending external systems, update the status to Transaction.STATUS.pending_external.

If an exception is raised, the transaction will be left in its current status and may be used again as a parameter to this function. To ensure the exception isn’t repeatedly re-raised, change the problematic transaction’s status to Transaction.STATUS.error.

If transaction.protocol == Transaction.PROTOCOL.sep31 and more information is required from the sending anchor or user to complete the transaction, update the status to Transaction.STATUS.pending_info_update and save a JSON-serialized string of the missing fields to the Transaction.required_info_update column. The JSON string should be in the format returned from SendIntegration.info(). You can also optionally save a human-readable message to Transaction.required_info_message. Both fields will included in the /transaction response requested by the sending anchor.

If the SEP-31 transaction is waiting for an update, the sending anchor will eventually make a request to the /update endpoint with the information specified in Transaction.required_info_update. Once updated, this function will be called again with the updated transaction.

Parameters:transaction – the Transaction object associated with the payment this function should make

Fee Integration

polaris.integrations.calculate_fee(fee_params: Dict[KT, VT]) → decimal.Decimal[source]

Calculate the fee to be charged for the transaction described by fee_params.

Replace this function with another by passing it to register_integrations() as described in Registering Integrations if the fees charged for transactions is not calculated using the asset’s fee_fixed and fee_percent attributes.

If replaced, /info responses will no longer contain the fee_fixed and fee_percent attributes per-asset. This is because Polaris can no longer assume fees are determined using those attributes alone.

fee_params will always contain the following key-value pairs:

  • amount: Decimal
  • asset_code: str
  • operation: str
  • type: str

Each of these key-value pairs correspond to the associated parameter for the /fee endpoint. The Decimal returned will be used as the fee value in the response.

Deposit Instructions

polaris.integrations.DepositIntegration.instructions_for_pending_deposit(self, transaction: polaris.models.Transaction)

For pending deposits, its common to show instructions to the user for how to initiate the external transfer. Use this function to return text or HTML instructions to be rendered in response to /transaction/more_info.

Parameters:transaction – the transaction database object to be serialized and rendered in the response.
Returns:the text or HTML to render in the instructions template section

Deposit Post-Processing

polaris.integrations.DepositIntegration.after_deposit(self, transaction: polaris.models.Transaction)

Use this function to perform any post-processing of transaction after its been executed on the Stellar network. This could include actions such as updating other django models in your project or emailing users about completed deposits. Overriding this function is not required.

Parameters:transaction – a Transaction that was executed on the Stellar network

SEP-6 Integrations

The previous integrations are available for both SEP-6 and SEP-24 transactions, but the functions described below are only for SEP-6.

polaris.integrations.DepositIntegration.process_sep6_request(self, params: Dict[KT, VT], transaction: polaris.models.Transaction) → Dict[KT, VT]

Process the request arguments passed to the deposit endpoint and return one of the following responses outlined below as a dictionary. Save transaction to the DB if you plan to return a success response. If transaction is saved to the DB but a failure response is returned, Polaris will return a 500 error to the user.

Deposit no additional information needed

The success response. Polaris creates most of the attributes described in this response. Simply return the ‘how’ and optionally ‘extra_info’ attributes. For example:

return {
    "how": "<your bank account address>",
    "extra_info": {
        "message": "Deposit the funds to the bank account specified in 'how'"
    }
}

Customer information needed

A failure response. Return the response as described in SEP.

return {
  "type": "non_interactive_customer_info_needed",
  "fields" : ["family_name", "given_name", "address", "tax_id"]
}

Customer Information Status

A failure response. Return the ‘type’ and ‘status’ attributes. If CustomerIntegration.more_info_url() is implemented, Polaris will include the ‘more_info_url’ attribute in the response as well.

return {
  "type": "customer_info_status",
  "status": "denied",
}
Parameters:
  • params – the request parameters as described in /deposit
  • transaction – an unsaved Transaction object representing the transaction to be processed
polaris.integrations.WithdrawalIntegration.process_sep6_request(self, params: Dict[KT, VT], transaction: polaris.models.Transaction) → Dict[KT, VT]

Process the request arguments passed to the /withdraw endpoint and return one of the following responses outlined below as a dictionary. Save transaction to the DB if you plan to return a success response. If transaction is saved to the DB but a failure response is returned, Polaris will return a 500 error to the user.

Withdraw no additional information needed

A success response. Polaris populates most of the attributes for this response. Simply return an ‘extra_info’ attribute if applicable:

{
    "extra_info": {
        "message": "Send the funds to the following stellar account including 'memo'"
    }
}

You may also return the Customer information needed and Customer Information Status responses as described in DepositIntegration.process_sep6_request.

polaris.integrations.default_info_func(asset: polaris.models.Asset, lang: Optional[str]) → Dict[KT, VT][source]

Replace this function with another by passing it to register_integrations() as described in Registering Integrations.

Return a dictionary containing the fields and types key-value pairs described in the SEP-6 /info response for the asset passed. Raise a ValueError() if lang is not supported. For example,

if asset.code == "USD":
    return {
        "fields": {
            "email_address" : {
                "description": "your email address for transaction status updates",
                "optional": True
            },
            "amount" : {
                "description": "amount in USD that you plan to deposit"
            },
            "type" : {
                "description": "type of deposit to make",
                "choices": ["SEPA", "SWIFT", "cash"]
            }
        },
        "types": {
            "bank_account": {
                "fields": {
                    "dest": {"description": "your bank account number" },
                    "dest_extra": { "description": "your routing number" },
                    "bank_branch": { "description": "address of your bank branch" },
                    "phone_number": { "description": "your phone number in case there's an issue" }
                }
            },
            "cash": {
                "fields": {
                    "dest": {
                        "description": "your email address. Your cashout PIN will be sent here.",
                        "optional": True
                    }
                }
            }
        }
    }
Parameters:
  • assetAsset object for which to return the fields and types key-value pairs
  • lang – the language code the client requested for the description values in the response

SEP-24 Integrations

Form Integrations

Polaris provides a set of integration functions that allow you to collect, validate, and process the information you need to collect from users with Django Forms.

Of course, you’ll need to collect the amount the user would like to deposit or withdraw. Polaris provides a TransactionForm that can be subclassed to add additional fields for this purpose. One TransactionForm should be rendered for every transaction processed. See the Forms documentation for more information.

The functions below facilitate the process of collecting the information needed.

polaris.integrations.DepositIntegration.form_for_transaction(self, transaction: polaris.models.Transaction, post_data: Optional[django.http.request.QueryDict] = None, amount: Optional[decimal.Decimal] = None) → Optional[django.forms.forms.Form]

This function should return the next form to render for the user given the state of the interactive flow.

For example, this function could return an instance of a TransactionForm subclass. Once the form is submitted, Polaris will detect the form used is a TransactionForm subclass and update transaction.amount_in with the amount specified in form.

If post_data is passed, it must be used to initialize the form returned so Polaris can validate the data submitted. If amount is passed, it can be used to pre-populate a TransactionForm amount field to improve the user experience.

def form_for_transaction(self, transaction, post_data = None, amount = None):
    if not transaction.amount_in:
        return
    elif post_data:
        return TransactionForm(transaction, post_data)
    else:
        return TransactionForm(transaction, initial={"amount": amount})

After a form is submitted and validated, Polaris will call DepositIntegration.after_form_validation with the populated form and transaction. This is where developers should update their own state-tracking constructs or do any processing with the data submitted in the form.

Finally, Polaris will call this function again to check if there is another form that needs to be rendered to the user. If you are collecting KYC data, you can return a forms.Form with the fields you need.

This loop of submitting a form, validating & processing it, and checking for the next form will continue until this function returns None.

When that happens, Polaris will check if content_for_template() also returns None. If that is the case, Polaris assumes the anchor is finished collecting information and will update the Transaction status to pending_user_transfer_start.

If content_for_template() returns a dictionary, Polaris will serve a page without a form. Anchors should do this when the user needs to take some action in order to continue, such as confirming their email address. Once the user is confirmed, form_for_transaction() should return the next form.

Parameters:
  • transaction – the Transaction database object
  • post_data – A django request.POST object
  • amount – a Decimal object the wallet may pass in the GET request. Use it to pre-populate your TransactionForm along with any SEP-9 parameters.
Returns:

a dictionary containing various pieces of information to use when rendering the next page.

polaris.integrations.DepositIntegration.content_for_template(self, template: polaris.templates.Template, form: Optional[django.forms.forms.Form] = None, transaction: Optional[polaris.models.Transaction] = None) → Optional[Dict[KT, VT]]

Return a dictionary containing page content to be used in the template passed for the given form and transaction.

Polaris will pass one of the following polaris.templates.Template values:

  • Template.DEPOSIT

    The template used for deposit flows

  • Template.MORE_INFO

    The template used to show transaction details

The form parameter will always be None when template is Template.MORE_INFO since that page does not display form content.

If form is None and template_name is not Template.MORE_INFO, returning None will signal to Polaris that the anchor is done collecting information for the transaction. Returning content will signal to Polaris that the user needs to take some action before receiving the next form, such as confirming their email. In this case, make sure to return an appropriate guidance message.

Using this function, anchors can change the page title, replace the company icon shown on each page and its label, and give guidance to the user.

def content_for_template(template_name, form=None, transaction=None):
    ...
    return {
        "title": "Deposit Transaction Form",
        "guidance": "Please enter the amount you would like to deposit.",
        "icon_label": "Stellar Development Foundation",
        "icon_path": "images/company-icon.png"
    }

title is the browser tab’s title, and guidance is shown as plain text on the page. icon_label is the label for the icon specified by icon_path.

icon_path should be the relative file path to the image you would like to use as the company icon in the UI. The file path should be relative to the value of your STATIC_ROOT setting. If icon_path is not present, Polaris will use the image specified by your TOML integration function’s ORG_LOGO key.

Finally, if neither are present, Polaris will default to its default image. All images will be rendered in a 100 x 150px sized box.

Parameters:
  • template – a polaris.templates.Template enum value for the template to be rendered in the response
  • form – the form to be rendered in the template
  • transaction – the transaction being processed
polaris.integrations.DepositIntegration.after_form_validation(self, form: django.forms.forms.Form, transaction: polaris.models.Transaction)

Use this function to process the data collected with form and to update the state of the interactive flow so that the next call to DepositIntegration.form_for_transaction returns the next form to render to the user, or returns None.

Keep in mind that if a TransactionForm is submitted, Polaris will update the amount_in and amount_fee with the information collected. There is no need to implement that yourself.

DO NOT update transaction.status here or in any other function for that matter. This column is managed by Polaris and is expected to have particular values at different points in the flow.

If you need to store some data to determine which form to return next when DepositIntegration.form_for_transaction is called, store this data in a model not used by Polaris.

Parameters:
  • form – the completed forms.Form submitted by the user
  • transaction – the Transaction database object
polaris.integrations.WithdrawalIntegration.form_for_transaction(self, transaction: polaris.models.Transaction, post_data: Optional[django.http.request.QueryDict] = None, amount: Optional[decimal.Decimal] = None) → Optional[django.forms.forms.Form]

Same as DepositIntegration.form_for_transaction

Parameters:
  • transaction – the Transaction database object
  • post_data – A django request.POST object
  • amount – a Decimal object the wallet may pass in the GET request. Use it to pre-populate your TransactionForm along with any SEP-9 parameters.
polaris.integrations.WithdrawalIntegration.content_for_template(self, template: polaris.templates.Template, form: Optional[django.forms.forms.Form] = None, transaction: Optional[polaris.models.Transaction] = None)

Same as DepositIntegration.content_for_template, except the Template values passed will be one of:

  • Template.WITHDRAW

    The template used for withdraw flows

  • Template.MORE_INFO

    The template used to show transaction details

Parameters:
  • template – a polaris.templates.Template enum value for the template to be rendered in the response
  • form – the form to be rendered in the template
  • transaction – the transaction being processed
polaris.integrations.WithdrawalIntegration.after_form_validation(self, form: polaris.integrations.forms.TransactionForm, transaction: polaris.models.Transaction)

Same as DepositIntegration.after_form_validation, except transaction.to_address should be saved here when present in form.

Parameters:
  • form – the completed forms.Form submitted by the user
  • transaction – the Transaction database object

Some wallets may pass fields documented in SEP-9 in the initial POST request for the anchor to use to pre-populate the forms presented to the user. Polaris provides an integration function to save and validate the fields passed.

polaris.integrations.DepositIntegration.save_sep9_fields(self, stellar_account: str, fields: Dict[KT, VT], language_code: str)

Save the fields passed for stellar_account to pre-populate the forms returned from form_for_transaction(). Note that this function is called before the transaction is created.

For example, you could save the user’s contact information with the model used for KYC information.

# Assuming you have a similar method and model
user = user_for_account(stellar_account)
user.phone_number = fields.get('mobile_number')
user.email = fields.get('email_address')
user.save()

Then when returning a form to collect KYC information, also return the values saved in this method relevant to that form.

# In your form_for_transaction() implementation
user = user_for_account(transaction.stellar_account)
form_args = {
    'phone_number': format_number(user.phone_number),
    'email': user.email_address
}
return KYCForm(initial=form_args),

If you’d like to validate the values passed in fields, you can perform any necessary checks and raise a ValueError in this function. Polaris will return the message of the exception in the response along with 400 HTTP status. The error message should be in the language specified by language_code if possible.

polaris.integrations.WithdrawalIntegration.save_sep9_fields(self, stellar_account: str, fields: Dict[KT, VT], language_code: str)

Same as DepositIntegration.save_sep9_fields

Using an External Application for the Interactive Flow

Polaris provides Form Integrations for collecting and processing information about a deposit or withdraw. However if you would rather use another application to collect that information, override this function to return the URL that should be requested to begin that process.

Note that if you choose to use another application or set of endpoints, you must redirect to the /transactions/<deposit or withdraw>/interactive/complete?transaction_id= endpoint for the relevant transaction when finished. This signals to the wallet that the anchor is done processing the transaction and may resume control. A callback parameter can also be included in the URL.

polaris.integrations.DepositIntegration.interactive_url(self, request: rest_framework.request.Request, transaction: polaris.models.Transaction, asset: polaris.models.Asset, amount: Optional[decimal.Decimal], callback: Optional[str]) → Optional[str]

Override this function to provide the wallet a non-Polaris endpoint to begin the interactive flow. If the amount or callback arguments are not None, make sure you include them in the URL returned.

Returns:a URL to be used as the entry point for the interactive deposit flow
polaris.integrations.WithdrawalIntegration.interactive_url(self, request: rest_framework.request.Request, transaction: polaris.models.Transaction, asset: polaris.models.Asset, amount: Optional[decimal.Decimal], callback: Optional[str]) → Optional[str]

Same as DepositIntegration.interactive_url

Returns:a URL to be used as the entry point for the interactive withdraw flow

Javascript Integration

polaris.integrations.scripts(page_content: Optional[Dict[KT, VT]]) → List[str][source]

Replace this function with another by passing it to register_integrations() as described in Registering Integrations.

Return a list of strings containing script tags that will be added to the bottom of the HTML body served for the current request. The scripts will be rendered like so:

{% for script in scripts %}
    {{ script|safe }}
{% endfor %}

page_content will be the return value from content_for_template(). page_content will also contain a "form" key-value pair if a form will be rendered in the UI.

This gives anchors a great deal of freedom on the client side. The example reference server uses this functionality to inject Google Analytics into our deployment of Polaris, and to refresh the Confirm Email page every time the window is brought back into focus.

Note that the scripts will be executed in the order in which they are returned.

Running the Service

In addition to the web server, SEP-6 and SEP-24 require five additional processes to be run in order to work.

Polling Pending Deposits

When a user initiates a deposit transaction, the anchor must wait for the user to send the deposit amount to the anchor’s bank account. When this happens, the anchor should notice and deposit the same amount of the tokenized asset into the user’s stellar account.

Polaris provides the poll_pending_deposits() integration function for this purpose, which will be run periodically via the poll_pending_deposits command-line tool:

python manage.py poll_pending_deposits --loop --interval 10

This process will continue indefinitely, calling the associated integration function, sleeping for 10 seconds, and then calling it again.

Watching for Withdrawals

When a user initiates a withdrawal transaction, the anchor must wait for the user to send the tokenized amount to the anchor’s stellar account. Polaris’ watch_transactions command line tool streams transactions from every anchored asset’s distribution account and attempts to match every incoming deposit with a pending withdrawal.

If it finds a match, it will update the transaction’s status and call the process_withdrawal() integration function. Use this function to connect to your banking rails and send the transaction amount to the user’s bank account.

Run the process like so:

python manage.py watch_transactions

Executing Outgoing Transactions

The execute_outgoing_transactions CLI tool polls the database for transactions in the pending_receiver status and passes them to the RailsIntegration.execute_outgoing_transaction() function for the anchor to initiate the payment to the receiving user. See the integration function’s documentation for more information about this step.

You can run the service like so:

python manage.py execute_outgoing_transactions --loop --interval 10

This process will continue indefinitely, calling the associated integration function, sleeping for 10 seconds, and then calling it again.

Poll Outgoing Transactions

And finally, once a payment to the user has been initiated by the anchor, this CLI tool periodically calls RailsIntegration.poll_outgoing_transactions() so the anchor can return the transactions that have have completed, meaning the user has received the funds.

If your banking or payment rails do not provide the necessary information to check if the user has received funds, do not run this process and simply mark each transaction as Transaction.STATUS.completed after initiating the payment in RailsIntegration.execute_outgoing_transaction().

Run the process like so:

python manage.py poll_outgoing_transactions --loop --interval 60

Checking Trustlines

Sometimes, a user will initiate a deposit to an account that does not exist yet, or the user’s account won’t have a trustline to the asset’s issuer account. In these cases, the transaction database object gets assigned the pending_trust status.

check_trustlines is a command line tool that periodically checks if the transactions with this status now have a trustline to the relevant asset. If one does, Polaris will submit the transaction to the stellar network and call the after_deposit integration function once its completed.

check_trustlines has the same arguments as poll_pending_deposits:

python manage.py check_trustlines --loop --interval 60