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 POLARIS_ACTIVE_SEPS
in in your settings file.
POLARIS_ACTIVE_SEPS = ["sep-1", "sep-6", "sep-24", ...]
If you’re running SEP-24, add the following::
SESSION_COOKIE_SECURE = True
Polaris requires this setting to be True
for SEP-24 deployments if not in
LOCAL_MODE
.
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
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"
Some wallets may open the interactive URL’s returned by Polaris in iframes instead of popups.
This affects the way in which HTTPS cookie headers must be configured. Add Polaris’ PolarisSameSiteMiddleware
to your settings.MIDDLEWARE
if you want to support wallets using iframes.
SessionMiddleware
is required for all SEP-24 deployments, and 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¶
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.
If you’d like the user to send
Transaction.amount_in
plus the fee amount, add the amount charged as a fee toTransaction.amount_in
andTransaction.amount_expected
. here. While not required per SEP-6, it is encouraged to also populateTransaction.amount_fee
andTransaction.amount_out
here as well.Note that the amount sent over the Stellar Network could differ from the amount specified in this API call, so fees and the amount delievered may have to be recalculated in
RailsIntegration.execute_outgoing_transaction()
.Polaris responds to requests with the standard status code according the SEP. However, if you would like to return a custom error code in the range of 400-599 you may raise an
rest_framework.exceptions.APIException
. For example, you could return a 503 status code by raising anAPIException("service unavailable", status_code=503)
.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'" } }
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"] }
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]¶ Same as
DepositIntegration.process_sep6_request
except for the case below. Specifically, thehow
attribute should not be included.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'" } }
In addition to this response, 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: - asset –
Asset
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
- asset –
-
polaris.integrations.DepositIntegration.
patch_transaction
(self, params: Dict[KT, VT], transaction: polaris.models.Transaction)¶ Currently only used for SEP-6 transactions.
The GET /info response contains a fields object that describes the custom fields an anchor requires in requests to the GET /deposit endpoint. If one or more of these fields were originally accepted but later discovered to be invalid in some way, an anchor can place the transaction in the
pending_transaction_info_update
status, save a JSON-serialized object describing the fields that need updating toTransaction.required_info_updates
, and save a human-readable message toTransaction.required_info_message
describing the reason the fields need to be updated. A client can make a request to GET /transactions to detect that updated information is needed.This function is called when SEP-6 PATCH /transactions requests are made by the client to provide the updated values described in the
Transaction.required_info_updates
. Use the params passed in the request to update transaction or any related data.Polaris validates that every field listed in
Transaction.required_info_updates
is present in params but cannot validate the values. If aValueError
is raised, Polaris will return a 400 response containing the exception message in the body.If no exception is raised, Polaris assumes the update was successful and will update the transaction’s status back to
pending_anchor
as well as clear therequired_info_updates
andrequired_info_message
fields.Parameters: - params – The request parameters as described in the PATCH /transactions endpoint.
- transaction – A
Transaction
object for which updated was provided.
-
polaris.integrations.WithdrawalIntegration.
patch_transaction
(self, params: Dict[KT, VT], transaction: polaris.models.Transaction)¶ Same as
DepositIntegration.patch_transaction
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 aTransactionForm
subclass and updatetransaction.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 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 returnsNone
. If that is the case, Polaris assumes the anchor is finished collecting information and will update the Transaction status topending_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 – the data sent in the POST request as a dictionary
- 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.
- transaction – the
-
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 flowsTemplate.WITHDRAW
The template used for withdraw flowsTemplate.MORE_INFO
The template used to show transaction detailsThe form parameter will always be
None
when template isTemplate.MORE_INFO
since that page does not display form content.If form is
None
and template_name is notTemplate.MORE_INFO
, returningNone
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 pass key-value pairs to the template being rendered. Some of these key-value pairs are used by Polaris, but anchors are allowed and encouraged to extend Polaris’ templates and pass custom key-value pairs.
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", # custom field passed by the anchor "username": "John.Doe" }
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’sORG_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
- template – a
-
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.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.Keep in mind that if a
TransactionForm
is submitted, Polaris will update theTransaction.amount_in
,Transaction.amount_fee
, andTransaction.amount_out
fields with the information collected. There is no need to implement that yourself here. However, note that if the amount ultimately delivered to the anchor does not match the amount specified in the form, these attributes must be updated appropriately.If form is the last form to be served to the user, Polaris will update the transaction status to
pending_user_transfer_start
, indicating that the anchor is waiting for the user to deliver off-chain funds to the anchor. If the KYC information collected is still being verified, update theTransaction.status
column topending_anchor
here. Make sure to save this change to the database before returning. In this case Polaris will detect the status change and will not update the status again. Polaris will wait until the anchor changes the transaction’s status topending_user_transfer_start
before including the transaction in calls toDepositIntegration.poll_pending_deposits()
.Parameters: - form – the completed
forms.Form
submitted by the user - transaction – the
Transaction
database object
- form – the completed
-
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 – the data included in the POST request body as a dictionary
- 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.
- transaction – the
-
polaris.integrations.WithdrawalIntegration.
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]]¶ Same as
DepositIntegration.content_for_template
, except theTemplate
values passed will be one of:Template.WITHDRAW
The template used for withdraw flowsTemplate.MORE_INFO
The template used to show transaction detailsParameters: - 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
- template – a
-
polaris.integrations.WithdrawalIntegration.
after_form_validation
(self, form: django.forms.forms.Form, 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
- form – the completed
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, account_memo: Optional[str] = None, account_memo_type: Optional[str] = None)¶ Save the fields passed for the user identified by 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, account_memo: Optional[str] = None, account_memo_type: Optional[str] = None)¶ 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
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. See the CLI Commands for more information on all Polaris commands.
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 to pending_anchor
,
signaling to Polaris that it needs to submit the transaction to the external rails
used by the anchor.
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_anchor
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