SEP-24 User Guide
This guide will walk you through installing, configuring, and integrating with the Anchor Platform for the purpose of building an on & off-ramp service compatible with SEP-24, the ecosystem's standardized protocol for hosted deposits and withdrawals.
By leveraging the Anchor Platform's support for SEP-24, businesses make their on & off-ramp service available as an in-app experience through Stellar-based applications such as wallets and exchanges, extending their reach and connecting with users through the applications they already use.
As we improve the documentation, parts of this guide that are relevant to other use cases may be moved into their own sections.
Installation
The easiest way to install the Anchor Platform is to pull the docker image. SEP-24 support is not yet officially released, but is available using the 2.0.0-preview-1
tag.
- bash
docker pull stellar/anchor-platform:2.0.0-preview-1
Configuration
In this guide we'll use docker compose for simplicity, but you can run the Anchor Platform using other tools that support docker as well, such as minikube or a full-blown kubernetes cluster.
Let's create a minimal compose file to get started.
- YAML
# docker-compose.yml
version: "3.8"
services:
platform-server:
image: stellar/anchor-platform:2.0.0-preview-1
command: --sep-server
ports:
- "8080:8080"
env_file:
- ./config/dev.env
volumes:
- ./config:/home
The --sep-server
option tells the Anchor Platform to make the API endpoints defined by the SEPs you've enabled via configuration available on port 8080. This option also makes the Platform API available, which is the backend API your service(s) will use to communicate with the Anchor Platform.
A future release of the Anchor Platform will add an additional command line option for the Platform API, and update the --sep-server
option to only make the SEP APIs available.
The Anchor Platform supports two approaches for configuration: using environment variables or a YAML configuration file. For example, enabling SEP-24 in the Anchor Platform using an environment variable would look like the following:
- bash
# dev.env
SEP24_ENABLED=true
Alternatively, enabling SEP-24 using a yaml configuration file would look like the following:
- bash
# dev.env
ANCHOR_CONFIG_FILE=/home/dev.config.yaml
- YAML
# dev.config.yaml
sep24:
enabled: true
Businesses are able to use one approach exclusively or a combination of both approaches. Nested variables in the YAML file are expressed using underscores or dots (_
, .
) when using environment variables. We'll use environment variables for our configuration in this guide.
See the full set of configuration options in the Anchor Platform's default values file.
Specify Your Service's Assets
Let's create a file for our environment variables, and a directory for containing files referenced in our environment.
- bash
touch dev.env
mkdir config
touch config/dev.assets.yaml
The first thing we'll need to do is specify which assets our Anchor Platform deployment supports.
- bash
# dev.env
ASSETS_TYPE=file
ASSETS_VALUE=/home/dev.assets.yaml
Specify the following in your dev.assets.yaml
file, and change the values depending on your preferences.
- YAML
# dev.assets.yaml
assets:
- schema: stellar
code: USDC
issuer: GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5
distribution_account: GBLSAHONJRODSFTLOV225NZR4LHICH63RIFQTQN37L5CRTR2IMQ5UEK7
significant_decimals: 2
deposit:
enabled: true
min_amount: 1
max_amount: 1000000
withdraw:
enabled: true
min_amount: 1
max_amount: 1000000
sep24_enabled: true
Update the above values based on the assets you'll be issuing. Make sure to specify the testnet issuer
in your development file, and create your own distribution_account
using a tool like Stellar Lab.
Note that the distribution_account
will be used as the destination of your user's payments on Stellar when facilitating withdrawals.
The information provided for the assets
value closely maps to the information that will be expsosed to the wallet application using the GET /info
SEP-24 endpoint. The Anchor Platform also uses this information to validate requests made to your service.
Publish a Stellar Info File
Next, let's enable applications to learn more about your service by hosting a stellar.toml
file at a standardized URL path. This file allows applications to find information about your business, the assets your services utilize, as well as the root URL paths for these services. We can host this file using the Anchor Platform.
Let's create a file called dev.stellar.toml
file using the contents below as a starting point. For the full set of attributes, see the SEP-1 specification.
- TOML
# dev.stellar.toml
ACCOUNTS = ["add your public keys for your distribution accounts here"]
SIGNING_KEY = "add your signing key here"
NETWORK_PASSPHRASE = "Test SDF Network ; September 2015"
TRANSFER_SERVER_SEP0024 = "http://localhost:8080/sep24"
WEB_AUTH_ENDPOINT = "http://localhost:8080/auth"
[[CURRENCIES]]
code = "USDC"
issuer = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
status = "test"
is_asset_anchored = false
desc = "USD Coin issued by Circle"
[DOCUMENTATION]
ORG_NAME = "Your organization"
ORG_URL = "Your website"
ORG_DESCRIPTION = "A description of your organization"
Note that you'll need to create another file for your production deployment that uses the public network's passphrase, your production service URLs, your Mainnet distribution accounts and signing key, as well as the Mainnet issuing accounts of the assets your service utilizes.
In your dev.env
file, specify the following.
- bash
# dev.env
SEP1_ENABLED=true
SEP1_TYPE=file
SEP1_VALUE=/home/dev.stellar.toml
This will tell the Anchor Platform that it should host the file specified by SEP1_VALUE
at ./well-known/stellar.toml
.
Alternatively, your stellar.toml
file could be hosted using a proper static file server like nginx. As long as your info file includes the appropriate URLs pointing to the Anchor Platform, this will work just fine.
Enable Stellar Authentication
Stellar-based wallet applications create authenticated sessions with Stellar anchors by proving they, or their users, have sufficient control over a Stellar account. Once authenticated, the wallet application uses a session token provided by the anchor in subsequent requests to the anchor's standardized services.
The Anchor Platform supports this form of authentication with minimal configuration from the business.
- bash
# dev.env
SEP10_ENABLED=true
SECRET_SEP10_SIGNING_SEED="a Stellar private key"
SECRET_SEP10_JWT_SECRET="a secret encryption key"
SECRET_SEP10_SIGNING_SEED
is the private key to the public key you've specified as the SIGNING_KEY
in your stellar.toml
file. It will be used to sign authentication challenges presented to wallet applications, providing that you are in possession of the SIGNING_KEY
. Wallets will check for this signature before signing and sending back the authentication challenge.
SECRET_SEP10_JWT_SECRET
is the encryption key that will be used to sign and verify the authentication tokens you issue to wallet applications after they or their users have proven control of their Stellar account.
By default, the Anchor Platform allows anyone with a Stellar account to authenticate with your services. If you'd like to only allow users of a particular wallet application to authenticate, or want to disallow specific users from authenticating, use the following environment variables. This is an optional feature and should only be added if it is a business requirement.
- bash
# dev.env
SEP10_CLIENT_ATTRIBUTION_REQUIRED=true
SEP10_CLIENT_ATTRIBUTION_ALLOWLIST=lobstr.co,api.vibrantapp.com,decaf.so,api.beansapp.com
SEP10_REQUIRE_KNOWN_OMNIBUS_ACCOUNT=true
SEP10_OMNIBUS_ACCOUNT_LIST=GBIBMZNXMD3P7HXVQCYIWWT5NG43NEIIY7VYBQ5SADV6UULUKCAJTGPG
SEP10_CLIENT_ATTRIBUTION_REQUIRED
informs the Anchor Platform whether or not it should allow users of non-custodial wallets to authenticate without the wallet also identifying itself, and SEP10_CLIENT_ATTTRIBUTION_ALLOWLIST
is the list of non-custodial wallets that can create authenticated sessions with your services.
SEP10_REQUIRE_KNOWN_OMNIBUS_ACCOUNT
informs the Anchor Platform whether or not it should allow users of custodial wallets to authenticate without the custodial wallet's public key being included in the SEP10_OMNIBUS_ACCOUNT_LIST
.
Enable Hosted Deposits & Withdrawals
Now you're ready to enable hosted deposits and withdrawals via the SEP-24 API. Add the following variables to your environment file.
- bash
# dev.env
SEP24_ENABLED=true
SEP24_INTERACTIVE_URL_BASE_URL=http://localhost:8081
SECRET_SEP24_INTERACTIVE_URL_JWT_SECRET="your encryption key shared with your business server"
SECRET_SEP24_MORE_INFO_URL_JWT_SECRET="your encryption key shared with your business server"
SEP24_INTERACTIVE_URL_BASE_URL
is the URL that the Anchor Platform will provide to wallet applications when they initiate transactions. Wallet applications will open this URL in a web view inside their app, handing over control of the user experience from the wallet to your business. We'll dive further into this experience in subsequent sections.
SECRET_SEP24_INTERACTIVE_URL_JWT_SECRET
and SECRET_SEP24_MORE_INFO_URL_JWT_SECRET
are encryption keys that the Anchor Platform will use to generate short-lived tokens it will add to the URLs provided to the wallet. Your business server must also have these keys in its environment so it can verify the token's signature.
Test With the Demo Wallet
Wallets should now be able to discover, authenticate, and initiate transactions with your service! Your project and source files should now look something like this.
- Example
├── dev.env
├── docker-compose.yaml
├── config
│ ├── dev.assets.yaml
│ ├── dev.stellar.toml
Your environment should now look like the following.
- bash
# dev.env
ASSETS_TYPE=file
ASSETS_VALUE=/home/dev.assets.yaml
SEP1_ENABLED=true
SEP1_TYPE=file
SEP1_VALUE=/home/dev.stellar.toml
SEP10_ENABLED=true
SECRET_SEP10_SIGNING_SEED="a Stellar private key"
SECRET_SEP10_JWT_SECRET="a secret encryption key"
SEP24_ENABLED=true
SEP24_INTERACTIVE_URL_BASE_URL=http://localhost:8081
SECRET_SEP24_INTERACTIVE_URL_JWT_SECRET="your encryption key shared with your business server"
SECRET_SEP24_MORE_INFO_URL_JWT_SECRET="your encryption key shared with your business server"
To test this out, go to the Stellar Demo Wallet.
Initiate a transaction by doing the following:
- Create a new keypair
- Click the "Add Asset" button and enter
- the code of the Stellar asset on your
stellar.toml
file - your home domain,
localhost:8080
- the code of the Stellar asset on your
- Select the dropdown and click "SEP-24 Deposit", then click "Start"
The demo wallet should be able to find your stellar.toml
file, authenticate using the Stellar keypair you just created, and initiate a transaction. However, when the demo wallet attempts to open the URL provided by the Anchor Platform, you'll get a not found page.
Add Data Persistence
The Anchor Platform supports PostgreSQL and Aurora PostgreSQL for use in production, but also supports H2 or SQLite for use in development. For managing migrations, the Anchor Platform uses Flyway.
Before we move forward, let's add a database to our development environment so the transactions we initiate persist after stopping the service.
- YAML
# docker-compose.yml
version: "3.8"
services:
platform-server:
image: stellar/anchor-platform:2.0.0-preview-1
command: --sep-server
env_file:
- ./dev.env
volumes:
- ./config:/home
ports:
- "8080:8080"
depends_on:
- db
db:
image: postgres:latest
ports:
- "5432:5432"
env_file:
- ./dev.env
Now let's update our environment so the platform server can connect to the database server.
- bash
# dev.env
DATA_TYPE=postgres
DATA_SERVER=db
DATA_DATABASE=platform
SECRET_DATA_USERNAME=postgres
SECRET_DATA_PASSWORD=password
DATA_FLYWAY_ENABLED=true
POSTGRES_USER=postgres
POSTGRES_PASSWORD=password
We have to create the platform
database before the platform server can connect to it, so let's run the database container and create our database.
- bash
docker compose up db
Copy the name of the container from the command's output. If you're in a directory called sep24-anchor
, then your container will be sep24-anchor-db-1
. Now let's create the database inside the container.
- bash
docker exec -it sep24-anchor-db-1 psql -U postgres
Now that you're in the PostgreSQL console, create the database.
- SQL
CREATE DATABASE platform;
Exit the container, and try to run the platform server in addition to the database.
- bash
docker compose up
You should see the logs reporting a successful connection to the postgres database.
Integration
Providing Updates To the Anchor Platform
One of the main points of interaction with the Anchor Platform is notifying the Platform about events related to the transaction.
In general, you'll want to provide updates for the following events:
- Your business is processing the KYC information provided by the user
- Your business is ready to receive funds from the user
- Your business has received funds from the user
- Your business has sent funds to the user
- Your business has processed a refund for the user's transaction
- Your business experienced an unexpected error
This is done by making a request to the Platform API's PATCH /transactions
endpoint. This endpoint allows you to update the data returned from the Platform API's GET /transaction/:id
endpoint as well as the data provided to wallet applications through the applicable SEP endpoints.
Anchor Platform PATCH /transactions
endpoint is designed to notify platform about changes of the transaction state. Given that, it will be called every time user or you (the anchor) take some action that progresses the transaction state in the flow.
You can find more about transaction flow and statuses in the SEP-24 protocol document
In the next major version release of the Anchor Platform, we'll be replacing the PATCH /transactions
Platform API endpoint with a JSON RPC endpoint that will simplify the request schema, provide better validation of data, and make the API easier to understand in general.
Securing Platform API
By default, the Platform API's endpoints such as PATCH /transactions
and GET /transaction/:id
are not protected, and accessible by anyone who has access to the server, including wallet applications.
Using API Key
To enable API key authentication, modify your dev.env
file:
- bash
# dev.env
PLATFORM_API_AUTH_TYPE=api_key
# Will be used as API key
SECRET_PLATFORM_API_AUTH_SECRET="your API key that business server will use"
After it's enabled, all requests must contain valid X-Api-Key
header, set to the configured API key.
Using JWT
To enable JWT authentication, modify your dev.env
file:
- bash
# dev.env
PLATFORM_API_AUTH_TYPE=jwt
SECRET_PLATFORM_API_AUTH_SECRET="your encryption key shared with your business server"
After it's enabled, all requests must contain valid Authorization
header. The JWT provided must have the jti
and exp
fields representing a valid transaction and token expiration time, respectively.
Making Patch Requests
Before diving into making PATCH requests, let's create a template for making a request to the Anchor Platform.
- bash
# call-patch.sh
#!/usr/bin/env bash
transaction=`cat $1`
curl localhost:8080/transactions \
-X PATCH \
-H 'Content-Type: application/json' \
-d '{"records":['"$transaction"']}'
This small script will make a PATCH /transactions
request to the Anchor Platform hosted on the default port (8080). Json transaction data stored in provided file will be used as body (wrapped into records singleton array).
It's possible to provide multiple updates in a single PATCH /transactions
call (by placing multiple transaction objects in the records array). When update is done in such manner, all updates will be done sequentially.
Importantly, this operation is NOT atomic. If one of updates fail, all previous updates would be applied, all subsequent updates would be discarded, and an error would be raised.
We will also be making a reference to the $transaction_id
variable. It's an identification of transaction, that's being return from Anchor Platform on interactive withdrawal or deposit start request. You can obtain transaction id by connecting test wallet to your local Anchor Platform instance. Set this variable in your terminal to be able to execute scripts below.
Updating Deposit Transaction Via PATCH
When the user has provided you the information necessary to facilitate the transaction, you need to update the status of the transaction and provide some of the information you collected to the Anchor Platform.
First step of the deposit flow after starting the deposit itself is collecting KYC. It's usually done in the web-app, but can also be optionally provided by wallet application, using SEP-9. After necessary KYC is collected, PATCH /transactions
request should be made.
When KYC process is long (for example, ID verification), it's advised to first set transaction status to pending_anchor
, indicating user that KYC is being processed.
Processing KYC Information
Update for this action is simply changing transaction status. It's a good idea to modify message too, reflecting details of the process:
- JSON
// kyc-in-process.json
{
"transaction": {
"id": "<transaction_id>",
"status": "pending_anchor",
"message": "KYC is being verified"
}
}
To execute this update, simply run
./call-path.sh kyc-in-process.json
Ready to Receive Funds
After KYC is collected, you should be ready to receive the off-chain deposit. While you're waiting for the user to deliver funds, you'll want to provide an updated transaction status as well as basic transaction information.
- JSON
{
"transaction": {
"id": "<transaction_id>",
"status": "pending_user_transfer_start",
"message": "waiting on the user to transfer funds",
"amount_in": {
"asset": "iso4217:USD",
"amount": "10"
},
"amount_out": {
"asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
"amount": "9.0"
},
"amount_fee": {
"asset": "iso4217:USD",
"amount": "1.0"
}
}
}
Amount should be collected in the interactive flow. However, it can also be specified by wallet as part of the request (see amount_expected
of GET /transaction/:id
). Please note, that you must specify amount fields, as wallet do not provide this information.
amount_in
is amount user requested. Ifamount_expected
is not null,amount_in
should be equal toamount_expected
. In that case, amount should not be collected in the interactive flow.amount_out
is amount user will receiveamount_fee
is total fees collected by the application.asset
part of amount fields is an SEP-38 formatted asset of the amount. In this example, it's set to USD, assuming user made a bank transfer to the system using USD.
Asset field is optional. When not set, it defaults to the asset user requested.
Sending Stellar Transaction
The next step would be sending Stellar transaction. First, let's change status to reflect the latest transaction state:
- JSON
{
"transaction": {
"id": "<transaction_id>",
"status": "pending_anchor",
"message": "Transaction is ready to be fullfilled by the anchor"
}
}
Next, send a transaction in the Stellar network to fulfill user request. Currently, Anchor Platform doesn't offer such functionality. You may use a custodian service, or manage distribution account yourself. In latter case, you may want to use the Stellar SDK on your server side to create, sign and submit transaction.
In the future, the Anchor Platform will also support making payments associated with deposit transactions to users on behalf of the business. This is another reason why you may want to use the Anchor Platform's event system.
User account may not be ready to receive funds. You should check that account has established the trustline. Otherwise, set transaction to status pending_trust
to indicate that anchor is waiting for the user establishing the trustline.
(Optional) You may set transaction status to pending_stellar
, when it has been submitted to the network, but not yet included in the ledger. Due to the nature of Stellar network, transaction confirmation is almost instant. Some SDKs (Java) have blocking transaction submission method. After receiving success result from the submission method, transaction will already be completed.
Completing Transaction
Finally, after transaction has been sent and successfully submitted to the network, it's time to complete transaction:
- JSON
{
"id": "<transaction_id>",
"status": "completed",
"message": "completed",
"stellar_transactions": [
{
"id": "<stellar_transaction_id>",
"payments": [
{
"id": "<payment_operation_id>",
"amount": {
"amount": "10",
"asset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
}
}
]
}
]
}
It's possible to send multiple transactions to fulfill user request, but due to current protocol restrictions, only the latest one will be visible for user via stellar_transaction_id
field.
Handling Errors
If you encounter an unrecoverable error when processing the transaction, it's required to set transaction status to error
. Message field can be used to describe error details.
- JSON
{
"id": "<transaction_d>",
"status": "error",
"message": "Example error"
}
This status should not be used after user has made a transfer (if there are some external factors that prevent completing transaction). Instead, refund should be initiated. It can be done either by user's request (in your web view), or automatically.
Updating Withdrawal Transaction Via PATCH
Once the deposit flow is finished, implementing withdrawal becomes quite easy. Some parts of the flow are similar, and can be reused.
The starting point for withdrawal and deposit is the same, i.e., collecting and verifying KYC.
Ready to Receive Funds
Similar to deposit, the next step would be to notify the user that the Anchor is ready to receive funds. However, as your service will be receiving transactions over the Stellar network, the update would look a bit different.
- JSON
{
"transaction": {
"id": "<transaction_id>",
"status": "pending_user_transfer_start",
"message": "waiting on the user to transfer funds",
"amount_in": {
"asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
"amount": "10"
},
"amount_out": {
"asset": "iso4217:USD",
"amount": "9.0"
},
"amount_fee": {
"asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
"amount": "1.0"
},
"memo": "466857256912784999",
"memo_type": "id"
}
}
Setting memo
and memo_type
is optional if you don't use a custodian service.
If your business uses a third-party custodian, you would need to generate a memo with your custodian that can be used to receive the user's payment. These memos should be generated per-transaction. You should provide memo
and memo_type
to the request as shown above. Note that the memo must be unique, this is what helps to associate stellar transactions with SEP transaction.
If your business manages the assets, the anchor platform can generate memos for you. When status is changed to pending_user_transfer_start
, Anchor Platform sets memo
and memo_type
automatically (only if it's not included in the request).
The Stellar account that will be used to receive funds should be configured in assets
Tracking Stellar Transaction
Using Payment Observer allows delegating this step to the Anchor Platform. To enable Payment Observer use --stellar-observer
flag in the command section of the compose file
Payment Observer will track all transactions sent to distribution account. When transaction with correct memo is detected in the network, status will automatically change to pending_anchor
and event will be emitted (if Kafka is used).
The Payment Observer won't validate amounts, and it's your responsibility to verify that the amount sent by the user is correct.
If you already have system that monitors payments, make sure that the logic of the system matches the description below:
First, wait for the transaction to be included in the ledger (using SDK). This transaction must have correct memo and destination address (distribution account). Once this transaction has been detected and verified, notify the user that the funds have been received:
- JSON
{
"id": "<transaction_id>",
"status": "pending_anchor",
"message": "Stellar transaction has been received",
"stellar_transactions": [
{
"id": "<stellar_transaction_id>",
"payments": [
{
"id": "<payment_operation_id>",
"amount": {
"amount": "10",
"asset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
}
}
]
}
]
}
Sending External Transfer
The next step is to send an external transfer, such as a bank transfer, to the user. When done, change status to notify user transfer has been sent, but has not been confirmed yet.
If your off-chain rails don't provide enough information on whether the payment is pending instead of delivered, you should skip this step and mark payments as completed
after initiating the payment.
- JSON
{
"transaction": {
"id": "<transaction_id>",
"status": "pending_external",
"externaltransactionid": "<id_of_external_transaction>",
"message": "Transfer is being verified"
}
}
Completing Transaction
Finally, after the transaction has been successfully sent and submitted to the network, it's time to complete the transaction:
- JSON
{
"id": "<transaction_id>",
"status": "completed",
"message": "completed"
}
Example Using Javascript
Integrating with the Anchor Platform involves three key areas.
- Building a web-based user experience that can be opened in a mobile web view
- Providing transaction status updates to the Anchor Platform
- Fetching transaction status updates from the Anchor Platform
Building a Web-Based User Experience
The Anchor Platform does not offer a whitelabel UI that your business can utilize, and instead expects the business to build their own UI and backend system. We won't build an entire on & off-ramp user experience in this guide, but will cover the ways in which your existing product should be updated to be compatible with the Anchor Platform.
Authentication
If your business has an existing on & off-ramp product, you likely have an existing system for user authentication. However, because the Anchor Platform authenticates the user prior to providing the business' URL, requiring the user to go through another form of authentication is actually unnecessary. In this way, the Anchor Platform can be thought of as providing an alternative form of authentication.
The business is free to continue requiring users to authenticate using their existing system, but the ideal user experience would skip this step and create an authenticated session for the user if they have already authenticated using their Stellar account.
The Anchor Platform adds a JWT token
query parameter to the business' URL given to the wallet application. This token is signed by the previously-configured SECRET_SEP24_INTERACTIVE_URL_JWT_SECRET
value, and includes the information you need to identify the user. The process should look something like this:
- Pass the
token
added to the URL of your backend system - Verify the signature on the
token
and check its expiration - Create an authenticated session for the user identified by
token.sub
The decoded contents of the token
will look something like this:
- JSON
{
"jti": "e26cf292-814f-4918-9b40-b4f76a300f98",
"sub": "GB244654NC6YPEFU3AY7L25COGES445P3Q63W6Q76JHR3UBJMLT2XBOB:1234567",
"exp": 1516239022,
"data": {
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]"
}
}
Note that the sub
value identifies the user using a Stellar account and integer. This is what the value will be when custodial applications that use an omnibus account authenticate with your service. When non-custodial wallets authenticate, the token may look slightly different.
- JSON
{
"jti": "e26cf292-814f-4918-9b40-b4f76a300f98",
"sub": "GB244654NC6YPEFU3AY7L25COGES445P3Q63W6Q76JHR3UBJMLT2XBOB",
"exp": 1516239022,
"data": {
"client_domain": "api.vibrantapp.com",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]"
}
}
The sub
value here only contains a public key to identify the user, and the data.client_domain
field identifies the wallet application used to authenticate.
In both cases, all information in the data
object is optional, and will only be present if the wallet provides that information.
Let's add a backend server to our compose file that will be used to verify the token and create authenticated web sessions for users initiating transactions.
- YAML
# docker-compose.yaml
---
business-server:
build: .
ports:
- "8081:8081"
env_file:
- ./dev.env
depends_on:
- platform-server
Let's create a simple Docker container for our application.
- Dockerfile
FROM node:19
WORKDIR /home
COPY . .
RUN npm install
CMD ["node", "server.js"]
Now let's create a minimal NodeJS application.
- bash
yarn init -y
yarn add express jsonwebtoken
touch server.js
Below is an example of a backend server authenticating a user using NodeJS.
- JavaScript
# server.js
const express = require("express");
const jwt = require("jsonwebtoken");
const app = express();
const port = process.env.BUSINESS_SERVER_PORT;
app.use(express.json());
/*
* We'll store user session data in memory, but production systems
* should store this data somewhere more persistent.
*/
const sessions = {};
/*
* Create an authenticated session for the user.
*
* Return a session token to be used in future requests as well as the
* user data. Note that you may not have a user for the stellar account
* provided, in which case the user should go through your onboarding
* process.
*/
app.post("/session", async (req, res) => {
let decodedPlatformToken;
try {
decodedPlatformToken = validatePlatformToken(req.body.platformToken);
} catch (err) {
res.status = 400;
res.send({ "error": err });
return;
}
let user = getUser(decodedPlatformToken.sub);
let sessionToken = jwt.sign(
{ "jti": decodedPlatformToken.jti },
process.env.SESSION_JWT_SECRET
);
sessions[sessionToken] = user;
res.send({
"token": sessionToken,
"user": user
});
});
/*
* Validate the signature and contents of the platform's token
*/
function validatePlatformToken(token) {
if (!token) {
throw "missing 'platformToken'";
}
let decodedToken;
try {
decodedToken = jwt.verify(token, process.env.SECRET_SEP10_JWT_SECRET);
} catch {
throw "invalid 'platformToken'";
}
if (!decodedToken.jti) {
throw "invalid 'platformToken': missing 'jti'";
}
return decodedToken;
}
/*
* Query your own database for the user based on account:memo string parameter
*/
function getUser(sub) {
return null;
}
app.listen(port, () => {
console.log(`business server listening on port ${port}`);
});
Run this with the platform server and database and initiate a new transaction with the demo wallet. Then, we'll send the token to our server.
- bash
curl \
-X POST \
-H 'Content-Type: application/json' \
-d '{"platformToken": "<paste the token from the URL here>"}' \
http://localhost:8081/session | jq
Providing Updates to the Platform
Let's create an endpoint for our business server that accepts the information collected in our UI.
- JavaScript
# server.js
// Production systems should either let the Anchor Platform generate its own memos
// or have your custodial service generate a memo for each transaction.
const transactionMemos = {};
app.post("/transaction", async (req, res) => {
let sessionToken;
try {
sessionToken = validateSessionToken(req.headers.get("authorization"));
} catch (err) {
res.status = 400;
res.send({ "error": err })
return;
}
// assuming this is a withdrawal transaction, we'll provide a memo, which is
// required by our third-party custodian to credit us the payment. When the
// payment is made with this memo, we can match the on-chain payment with the
// transaction in the Anchor Platform's database.
transactionMemos[req.body.transaction.id] = parseInt(Math.random() * 100000);
let patchTransactionBody = {
"id": req.body.transaction.id,
"status": "pending_user_transfer_start",
"memo": transactionMemos[req.body.transaction.id],
"amount_in": {
"asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
"amount": req.body.amount_in.amount,
},
"amount_fee": {
"asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
"amount": req.body.amount_fee.amount,
},
"amount_out": {
"asset": "iso4217:USD",
"amount": req.body.amount_out.amount,
},
"message": "waiting for the user to provide off-chain funds."
};
let platformResponse;
try {
platformResponse = await updatePlatformTransaction(patchTransactionBody);
} catch (err) {
res.status = 500;
res.send({ "error": err })
return;
}
res.send({
"transaction": platformResponse.records[0]
});
});
function validateSessionToken(authorizationHeader) {
let parts = authorizationHeader.split(" ");
if (parts.length != 2 || parts[0] != "Bearer") {
throw "invalid authorization header format";
}
let sessionToken = parts[1];
try {
jwt.verify(sessionToken, process.env.SESSION_JWT_SECRET);
} catch {
throw "invalid session token";
}
if (!sessions[sessionToken]) {
throw "expired session";
}
return sessionToken;
}
async function updatePlatformTransaction(requestBody) {
let response = await fetch(
`${process.env.PLATFORM_SERVER}/transactions`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(requestBody)
}
);
if (response.status != 200) {
throw `unexpected status code: ${response.status}`;
}
return await response.json();
}
This will update the Anchor Platform's database with the information provided and enable wallet applications to fetch this updated information so it can relay it back to the user. You should have already informted the user of the transaction's amounts and that your business is waiting for on-chain payment to arrive, but providing these updates allows users to view their transactions' statuses through their mobile application without opening the business' UI again.
At this time, the Anchor Platform does not send notifications to the wallet application when transaction statuses change, however, it is on our roadmap to add these notifications or "callback requests" so that wallet applications do not have to poll the Anchor Platform for updates.
Fetching Updates from the Platform
If you only use the Anchor Platform to expose the SEP APIs to wallet applications, then you won't have a strong reason for fetching transaction status updates from the Anchor Platform, mostly because it won't update the transaction status until you make requests to PATCH /transactions
.
However, if you use the Anchor Platform to monitor the Stellar network for incoming payments (associated with withdrawal transactions), the Anchor Platform will update transaction statuses when payments are received.
There are two ways to fetch updates from the Anchor Platform,
- Polling the Platform API's
GET /transactions/:id
endpoint for the transactions you're expecting a payment for - Streaming transaction status change events from a Kafka cluster
While streaming transaction status changes from a Kafka cluster may be a more robust and scalable approach, we're going to use the polling method in this guide. Setting up and using a Kafka cluster will be the subject of a different section of the docs.
First, let's configure the Anchor Platform to observe the Stellar network for incoming payments.
- YAML
# docker-compose.yml
---
stellar-observer:
image: stellar/anchor-platform:2.0.0-preview-1
command: --stellar-observer
env_file:
- ./dev.env
volumes:
- ./config:/home
depends_on:
- db
The --stellar-observer
command starts a process that monitors the distribution accounts configured in your config.yaml
file for withdrawal payments.
If a payment is sent to one of these accounts and the memo attached to the transaction matches a memo
value provided or generated by the Anchor Platform, the Anchor Platform will consider the transaction that memo is associated with as received and update the transaction's status to pending_anchor
.
Let's make some additions to the server.js
file so we can poll the Anchor Platform for our expected payments.
- JavaScript
// server.js
...
/*
* Fetch the transaction data from the Platform API
*
* Production systems should have proper retry mechanisms.
*/
async function getPlatformTransaction(transactionId) {
let response = await fetch(`${process.env.PLATFORM_SERVER}/transactions/${transactionId}`)
if (response.status != 200) {
throw `unexpected status code: ${response.status}`;
}
return await response.json();
}
(async () => {
while (true) {
await new Promise(r => setTimeout(r, 2000));
let requestPromises;
for (const transactionId in transactionMemos) {
requestPromises.push(getPlatformTransaction(transactionId))
}
let transactions = await new Promise.all(requestPromises);
for (const transaction in transactions) {
// assuming all requests were successful
if (transaction.status == "pending_anchor") {
// initiate off-chain delivery of funds
console.log(`received payment for transaction ${transaction.id}`);
}
}
}
})()
Full Example Implementation
Stellar provides an example business server implementation for SEP-24. It's split into 2 parts: web UI, accessible for the end user and back-end implementation, used to get and push updates from/to the Anchor Platform.
Code for web UI can be found here
Code for backend part is a part of the Anchor platform, and is available as a submodule