Connecting Off-Chain Payment Rails

One of the largest gaps in Polaris functionality is the ability to connect to an anchor’s off-chain payment rails. Businesses can connect Polaris, which connects businesses to the Stellar Network, to any other payment network, including bank, credit card, mobile money app, or other blockchain networks.

These connections are implemented through an integration class called RailsIntegration, which Polaris uses when processing SEP-6, 24, & 31 transactions.

Incoming Payments

In the context of running an anchor, incoming payments to off-chain accounts are made by users with the expectation that the anchor will make an associated payment to the users’ Stellar accounts. This flow used in SEP-6 and SEP-24 deposit transactions.

To process these deposit transactions, Polaris offers the process_pending_deposits command. This command must be run in addition to Polaris’ web server in order to complete these transactions.

The process_pending_deposits command periodically calls the poll_pending_deposits() integration function, passing all Transaction objects representing deposit transactions whose off-chain funds have not yet been received in the anchor’s off-chain account.

In this function, anchors are expected to poll their off-chain payment rails and return the Transaction objects for which off-chain funds have been received.

Polaris will update the status of these transactions and begin processing their associated on-chain payment. See the process_pending_deposits command documentation for more information.

from typing import List, Dict
from django.db.models import QuerySet
from polaris.models import Transaction
from polaris.integrations import RailsIntegration
from .rails import (
    get_reference_id,
    has_received_payment
)

class AnchorRails(RailsIntegration):
    def poll_pending_deposits(
        self,
        pending_deposits: QuerySet,
        *args: List,
        **kwargs: Dict
    ):
        received_payments = []
        for transaction in pending_deposits:
            if has_received_payment(get_reference_id(transaction)):
                received_payments.append(transaction)
        return received_payments

The code above assumes the anchor creates a reference ID for each initiated transaction and instructs the user to include the ID when making the off-chain payment. Obviously, this code abstracts away the logic needed to query a particular off-chain payment network, which varies per-anchor.

Testing Incoming Payments

On testnet, you’ll want to automatically complete transactions so you can test the transaction happy path.

Run the process_pending_deposits command in addition to the web server.

python anchor/manage.py runserver --nostatic
python anchor/manage.py process_pending_deposits --loop

Go to https://demo-wallet.stellar.org and generate a new account. Add your anchored asset, and from the action menu, select “SEP-24 Deposit” or “SEP-6 Deposit” depending on the the transaction type you’d like to test. Once you complete the flow you should land on the transaction status page or see the transaction enter the pending_user_transfer_start status.

After some time, Polaris should detect the transaction is ready to be checked for off-chain fund arrival. It will call poll_pending_deposits(), receive the returned transaction object, and begin submitting the Stellar transaction.

Polaris may create the account if it doesn’t exist yet or ask the demo wallet to add a trustline to your anchored asset, but ultimately you should see the payment complete. Your asset balance should be updated with the amount you specified minus fees.

Outgoing Payments

Outgoing payments from off-chain accounts are made by anchors after receiving an on-chain payment. This flow is used for SEP-6 & SEP-24 withdrawals, as well as SEP-31 remittances.

To process these outgoing transactions, Polaris offers the watch_transactions, execute_outgoing_transactions, and poll_outgoing_transactions commands.

watch_transactions streams payments made to your asset’s distribution accounts and queue’s the associated Transaction object for off-chain execution. It doesn’t require any integrations.

execute_outgoing_transactions periodically calls the execute_outgoing_transaction() integration function, passing the Transaction object associated with the upcoming outgoing payment. Anchors must initiate the off-chain payment in this function and update the status of transaction.

Depending on the off-chain payment networks supported, the anchor may be able to differentiate between outgoing payments that have been initiated versus outgoing payments that have been delivered. The poll_outgoing_transactions command is used in such cases. It periodically calls poll_outgoing_transaction() for all Transaction objects that was passed to execute_outgoing_transaction() but were updated to the pending_external status instead of the completed status. Polaris exects the anchor to determine whether or not each payment has been received in the user’s off-chain account and return those that have.

...
from .rails import (
    submit_payment,
    get_payment,
    PaymentStatus,
    calculate_fee,
    initiate_refund,
    is_valid_payment_amount
)

class AnchorRails(RailsIntegration):
    ...

    def execute_outgoing_transaction(
        self,
        transaction: Transaction,
        *args: List,
        **kwargs: Dict
    ):
        if transaction.amount_in != transaction.amount_expected:
            if not is_valid_payment_amount(transaction.amount_in):
                initiate_refund(transaction)
                transaction.refunded = True
                transaction.status = Transaction.STATUS.error
                transaction.status_message = "the amount received is not valid, refunding."
                transaction.save()
                return
            transaction.amount_fee = calculate_fee(transaction)
            transaction.amount_out = round(
                transaction.amount_in - transaction.amount_fee,
                transaction.asset.significant_decimals
            )
            transaction.save()
        payment = submit_payment(transaction)
        if payment.status == PaymentStatus.DELIVERED:
            transaction.status = Transaction.STATUS.completed
        elif payment.status == PaymentStatus.INITIATED:
            transaction.status = Transaction.STATUS.pending_external
        else:  # payment.status == PaymentStatus.FAILED:
            transaction.status = Transction.STATUS.error
            transaction.status_message = "payment failed, contact customer support."
        transaction.external_transaction_id = payment.id
        transaction.save()

    def poll_outgoing_transactions(
        self,
        transactions: QuerySet,
        *args: List,
        **kwargs: Dict
    ) -> List[Transaction]:
        delivered_transactions = []
        for transaction in transactions:
            payment = get_payment(transaction)
            if payment.status == PaymentStatus.INITIATED:
                continue
            if payment.status == PaymentStatus.FAILED:
                transaction.status = Transction.STATUS.error
                transaction.status_message = "payment failed, contact customer support."
                transaction.save()
                continue
            delivered_transactions.append(transaction)
        return delivered_transactions

Testing Outgoing Payments

On testnet, you’ll want to automatically complete transactions so you can test the transaction happy path.

Run three or four processes depending on whether or not you’re supporting the poll_outgoing_transactions command.

python anchor/manage.py runserver --nostatic
python anchor/manage.py watch_transactions
python anchor/manage.py execute_outgoing_transactions --loop
python anchor/manage.py poll_outgoing_transactions --loop

Go to https://demo-wallet.stellar.org and import an account already funded with your anchored Stellar asset. On the asset balance, select “SEP-24 Withdraw” or whichever transaction type you’re starting and select “Start”.

Complete the interactive flow, or if you using SEP-6 or SEP-31, complete the KYC form presented. You should then see the demo wallet submit a payment transaction from your Stellar account to your anchor’s distribution account.

Almost immediately, you should see a log message indicating that Polaris has detected the Stellar payment made to your distribution account. After some time, Polaris should detect that the transaction is ready to be submitted off-chain and call execute_outgoing_transaction(). Finally, Polaris will periodically call poll_outgoing_transactions() until the transaction is marked as completed.