Skip to main content

Example

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 white-label 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's 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's 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:

  1. Pass the token added to the URL of your backend system
  2. Verify the signature on the token and check its expiration
  3. Create an authenticated session for the user identified by token.sub

The decoded contents of the token will look something like this:

{
"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.

{
"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.

# 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.

FROM node:19

WORKDIR /home
COPY . .
RUN npm install

CMD ["node", "server.js"]

Now let's create a minimal NodeJS application.

yarn init -y
yarn add express jsonwebtoken
touch server.js

Below is an example of a backend server authenticating a user using NodeJS.

# 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.

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.

# 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 rpcRequestBody = [
{
"id": 1,
"jsonrpc": "2.0",
"method": "request_onchain_funds",
"params": {
"transaction_id": req.body.transaction.id,,
"message": "waiting for the user to provide off-chain funds.",
"amount_in": {
"amount": req.body.amount_in.amount,
"asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
},
"amount_out": {
"amount": req.body.amount_out.amount,
"asset": "iso4217:USD"
},
"amount_fee": {
"amount": req.body.amount_fee.amount,
"asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
},
"destination_account": "GD...G",
"memo": transactionMemos[req.body.transaction.id],
"memo_type": "id"
}
}
];
let platformResponse;
try {
platformResponse = await updatePlatformTransaction(rpcRequestBody);
} 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}`,
{
method: "POST",
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 informed the user of the transaction's amounts and that your business's is waiting for the 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.

note

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 JSON-RPC API requests.

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.

# docker-compose.yml
---
stellar-observer:
image: stellar/anchor-platform:latest
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. It does this by making a JSON-RPC API request, so we need to configure the URL it should use.

# dev.env
PLATFORM_API_BASE_URL=http://platform-server:8085

Let's make some additions to the server.js file so we can poll the Anchor Platform for our expected payments.

// 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 two parts: 1) a web UI, accessible for the end user; and 2) a back-end implementation, used to get and push updates from/to the Anchor Platform.

The code for web UI can be found here

The code for the backend is a part of the Anchor Platform, and is available as a submodule.