SEP-31

SEP-31 is a bilateral payment standard for one anchor’s user to make payments to another anchor’s user. Where SEP-6 and SEP-24 allow users to deposit and withdraw their funds on and off the Stellar network, users of SEP-31 anchors may not even know they are using Stellar. A user can simply send fiat or crypto to the sending anchor and have them send the same amount (minus fees) to another anchor who can deposit the off-chain funds into the receiving user’s account.

An anchor can use the integrations outlined below to implement a fully functional SEP-31 anchor.

Configuration

Add the SEP to ACTIVE_SEPS in in your settings file.

ACTIVE_SEPS = ["sep-1", "sep-31", ...]

Integrations

Where SEP-6 and SEP-24 use DepositIntegration and WithdrawalIntegration, SEP-31 uses SendIntegration and RailsIntegration. Note that in future releases, some SEP-6 and SEP-24 functions related to payment rails may be moved from DepositIntegration or WithdrawalIntegration to RailsIntegration.

SEP-31 Endpoints

polaris.integrations.SendIntegration.info(self, asset: polaris.models.Asset, lang: str = None) → Dict[KT, VT]

Return a dictionary containing the “fields” object as described in the info response for the given asset. If your anchor requires KYC information about the sender or receiver, return the “receiver_sep12_type” or “sender_sep12_type” key-value pairs as well. Polaris will provide the rest of the fields documented in the info response.

Descriptions should be in the lang passed if supported.

return {
    "receiver_sep12_type": "sep31-receiver",
    "sender_sep12_type": "sep31-sender",
    "fields": {
        "transaction":{
           "routing_number":{
              "description": "routing number of the destination bank account"
           },
           "account_number":{
              "description": "bank account number of the destination"
           },
           "type":{
              "description": "type of deposit to make",
              "choices":[
                 "SEPA",
                 "SWIFT"
              ]
           }
        }
    }
}
Parameters:
  • asset – the Asset object for the field values returned
  • lang – the ISO 639-1 language code of the user
polaris.integrations.SendIntegration.process_send_request(self, params: Dict[KT, VT], transaction_id: str) → Optional[Dict[KT, VT]]

Use the params passed in the request to do any processing of the user and requested transaction necessary to facilitate the payment to the receiving user.

Polaris validates that the request includes all the required fields returned by SendIntegration.info() but cannot validate the values. Return None if the params passed are valid, otherwise return one of the error dictionaries outlined below.

If the sender_id or receiver_id values are invalid or the information collected for these users is not sufficient to process this request, return a dictionary matching the customer-info-needed response schema.

return {
    "error": "customer_info_needed",
    "type": "sep31-large-amount-sender"
}

For example, the above response could be used if the anchor requires additional information on the sender when the amount is large. The type key specifies the appropriate type value the client should use for the sender’s SEP-12 GET /customer request, and is optional.

If some optional fields from info() are missing but needed for this transaction, return a dictionary matching the schema described in the transaction-info-needed response.

return {
    "error": "transaction_info_needed",
    "fields": {
        "transaction": {
            "sender_bank_account": {
                "description": (
                    "The bank account number used by the sender. "
                    "Only required for large transactions."
                ),
                "optional": True
            }
        }
    }
}

If some parameters passed are simply not acceptable, return a dictionary containing a single "error" key-value pair.

return {
    "error": "invalid 'sender_bank_account' format"
}

Note that the Transaction object specified by transaction_id does not exist when this function is called. A transaction with the passed ID will only be created if a non-error response is returned.

Parameters:
  • params – The parameters included in the /send request
  • transaction_id – The UUID string that will be used as the primary key for the Transaction object.
polaris.integrations.SendIntegration.process_update_request(self, params: Dict[KT, VT], transaction: polaris.models.Transaction)

Use the params passed in the request to update transaction or any related data.

Polaris validates that every field listed in Transaction.required_info_update is present in params but cannot validate the field values. If a ValueError 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_receiver as well as clear the required_info_update and required_info_message fields.

Once the transaction enters the pending_receiver status, the execute_outgoing_transactions process will attempt to send the payment to the receiving user. See the RailsIntegration.execute_outgoing_transaction function for more information on the lifecycle of a transaction.

Parameters:
  • params – the parameters sent in the /update request
  • transaction – the Transaction object that should be updated
polaris.integrations.SendIntegration.valid_sending_anchor(self, public_key: str) → bool

Return True if public_key is a known anchor’s stellar account address, and False otherwise. This function ensures that only registered sending anchors can make /send requests.

Parameters:public_key – the public key of the sending anchor’s stellar account

Payment Rails

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. Don’t forget to populate the Transaction.amount_fee attribute if non-zero.

If the user receives the funds before returning from this function, 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 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 necessary fields to the Transaction.required_info_update field in the same 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 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.

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.

Currently, only SEP31 payment transactions are passed to this function, but SEP24 and SEP6 withdrawal transactions will be passed in future releases instead of using WithdrawalIntegration.process_withdrawal().

Parameters:transaction – the Transaction object associated with the payment this function should make
polaris.integrations.RailsIntegration.poll_outgoing_transactions(self, transactions: django.db.models.query.QuerySet) → List[polaris.models.Transaction]

Check the transactions that are still in a pending_external status and return the ones that are completed, meaning the user has received the funds.

Polaris will update the transactions returned to Transaction.STATUS.completed.

transactions is passed as a Django QuerySet in case there are many pending transactions. You may want to query batches of Transaction objects to avoid consuming large amounts of memory.

Parameters:transactions – a QuerySet of Transaction objects

Running the Service

In addition to the web server, SEP-31 requires three additional processes to be run in order to work.

Watch Transactions

Polaris’ watch_transactions command line tool streams transactions from every anchored asset’s distribution account and attempts to match every incoming stellar payment with a Transaction object created by the sending anchor’s /send request.

If it finds a match, it will update the transaction’s status to pending_receiver and update the Transaction.amount_in field with the amount actually sent in the stellar transaction.

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