Saltar al contenido principal

Ejemplo

Integrarse con Anchor Platform implica tres áreas clave:

  • Crear una experiencia de usuario basada en web que pueda abrirse en una vista web móvil
  • Proporcionar actualizaciones del estado de las transacciones a Anchor Platform
  • Obtener actualizaciones del estado de las transacciones desde Anchor Platform

Crear una experiencia de usuario basada en web

Anchor Platform no ofrece una interfaz de usuario marca blanca que tu negocio pueda utilizar; en cambio, espera que el negocio construya su propia interfaz y sistema backend. No construiremos toda una experiencia de usuario de entrada y salida en esta guía, pero cubriremos las maneras en que tu producto existente debe actualizarse para ser compatible con Anchor Platform.

Autenticación

Si tu negocio ya tiene un producto de entrada y salida, probablemente ya cuentes con un sistema para la autenticación de usuarios. Sin embargo, dado que Anchor Platform autentica al usuario antes de proporcionar la URL del negocio, requerir al usuario que pase por otra forma de autenticación resulta innecesario. De esta forma, se puede pensar que Anchor Platform proporciona una forma alternativa de autenticación.

El negocio es libre de seguir exigiendo que los usuarios se autentiquen usando su sistema existente, pero la experiencia ideal sería saltar este paso y crear una sesión autenticada si ya se autenticaron usando su cuenta Stellar.

Anchor Platform añade un parámetro de consulta JWT token a la URL del negocio que se proporciona a la aplicación wallet. Este token está firmado con el valor SECRET_SEP24_INTERACTIVE_URL_JWT_SECRET configurado previamente, e incluye la información necesaria para identificar al usuario. El proceso debería verse algo así:

  1. Pasa el token añadido a la URL de tu sistema backend
  2. Verifica la firma del token y revisa su caducidad
  3. Crea una sesión autenticada para el usuario identificado por token.sub

El contenido decodificado del token se verá algo así:

{
"jti": "e26cf292-814f-4918-9b40-b4f76a300f98",
"sub": "GB244654NC6YPEFU3AY7L25COGES445P3Q63W6Q76JHR3UBJMLT2XBOB:1234567",
"exp": 1516239022,
"data": {
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]"
}
}

Nota que el valor sub identifica al usuario usando una cuenta Stellar y un entero. Este será el valor cuando aplicaciones de custodia que usan una cuenta global se autentiquen con tu servicio. Cuando carteras no custodias se autentiquen, el token puede verse un poco diferente.

{
"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]"
}
}

El valor sub aquí solo contiene una clave pública para identificar al usuario, y el campo data.client_domain identifica la aplicación wallet usada para autenticar.

En ambos casos, toda la información dentro del objeto data es opcional y solo estará presente si la wallet la proporciona.

Vamos a añadir un servidor backend a nuestro archivo compose que se usará para verificar el token y crear sesiones web autenticadas para usuarios que inicien transacciones.

# docker-compose.yaml
---
business-server:
build: .
ports:
- "8081:8081"
env_file:
- ./dev.env
depends_on:
- platform-server

Vamos a crear un contenedor Docker sencillo para nuestra aplicación.

FROM node:19

WORKDIR /home
COPY . .
RUN npm install

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

Ahora vamos a crear una aplicación NodeJS mínima.

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

A continuación, un ejemplo de un servidor backend autenticando un usuario usando 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}`);
});

Ejecuta esto junto con el servidor de la plataforma y la base de datos, e inicia una nueva transacción con la demo wallet. Luego, enviaremos el token a nuestro servidor.

curl \
-X POST \
-H 'Content-Type: application/json' \
-d '{"platformToken": "<paste the token from the URL here>"}' \
http://localhost:8081/session | jq

Proporcionar actualizaciones a la plataforma

Vamos a crear un endpoint para nuestro servidor de negocio que acepte la información recogida en nuestra 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"
},
"fee_details": {
"total": req.body.fee_details.total,
"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();
}

Esto actualizará la base de datos de Anchor Platform con la información proporcionada y permitirá que las aplicaciones wallet obtengan esta información actualizada para transmitírsela al usuario. Ya deberías haber informado al usuario sobre los montos de la transacción y que tu negocio está esperando que llegue el pago en cadena, pero proporcionando estas actualizaciones permites a los usuarios ver el estado de sus transacciones desde su aplicación móvil sin necesidad de abrir nuevamente la UI del negocio.

nota

Actualmente, Anchor Platform no envía notificaciones a la aplicación wallet cuando cambian los estados de las transacciones; sin embargo, está en nuestra hoja de ruta agregar estas notificaciones o "callback requests" para que las wallets no tengan que hacer polling a Anchor Platform en busca de actualizaciones.

Obtener actualizaciones desde la plataforma

Si solo usas Anchor Platform para exponer las APIs SEP a aplicaciones wallet, entonces no tendrás una razón fuerte para obtener actualizaciones del estado de las transacciones desde Anchor Platform, principalmente porque no actualizará el estado de la transacción hasta que hagas solicitudes JSON-RPC API.

Sin embargo, si usas Anchor Platform para monitorear la red Stellar en busca de pagos entrantes (asociados con transacciones de retirada de fondos), Anchor Platform actualizará los estados de las transacciones cuando se reciban esos pagos.

Hay dos formas de obtener actualizaciones desde Anchor Platform,

  • Haciendo polling al endpoint GET /transactions/:id del API de la plataforma para las transacciones por las que esperas un pago
  • Recibiendo eventos de cambio de estado de transacciones mediante streaming desde un clúster Kafka

Aunque hacer streaming de cambios de estado de transacciones desde un clúster Kafka puede ser un enfoque más robusto y escalable, usaremos el método de polling en esta guía. Configurar y usar un clúster Kafka será tema de una sección distinta de la documentación.

Primero, configuremos Anchor Platform para observar la red Stellar en busca de pagos entrantes.

# docker-compose.yml
---
stellar-observer:
image: stellar/anchor-platform:latest
command: --stellar-observer
env_file:
- ./dev.env
volumes:
- ./config:/home
depends_on:
- db

El comando --stellar-observer inicia un proceso que monitorea las cuentas de distribución configuradas en tu archivo config.yaml en busca de pagos de retirada de fondos.

Si se envía un pago a una de estas cuentas y el memo adjunto a la transacción coincide con un valor memo proporcionado o generado por Anchor Platform, Anchor Platform considerará que la transacción con ese memo se ha recibido y actualizará el estado de la transacción a pending_anchor. Esto se hace realizando una solicitud JSON-RPC API, por lo que necesitamos configurar la URL que debe usar.

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

Hagamos algunas adiciones al archivo server.js para poder hacer polling a Anchor Platform para los pagos que esperamos.

// 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}`);
}
}
}
})()

Implementación completa de ejemplo

Stellar proporciona una implementación de servidor de negocio de ejemplo para SEP-24. Está dividida en dos partes: 1) una UI web, accesible para el usuario final; y 2) una implementación backend, usada para obtener y enviar actualizaciones desde/hacia Anchor Platform.

El código para la UI web puede encontrarse aquí

El código para el backend forma parte de Anchor Platform, y está disponible como un submódulo.