Cómo registrar y escuchar un efecto de estado de intent
En esta guía, aprenderás cómo registrar un Efecto en Kamin Ledger que escuche cambios en los estados de un Intent y envíe una notificación webhook cuando estos cambios ocurran.
Requisitos previos
Antes de comenzar:
- Tienes una Billetera y un Dominio creados en Kamin Ledger.
- Tienes un par de llaves válido para firmar solicitudes.
- Entiendes que:
- Las solicitudes
GETusan un token Bearer. - Las mutaciones (como la creación de Efectos) deben ser firmadas digitalmente.
- Las solicitudes
- Tienes un servidor local corriendo para recibir el webhook (por ejemplo, usando
Express).
Objetivo
Configurar un Efecto para escuchar eventos "intent-updated" y activar un webhook cuando tu billetera esté involucrada como fuente o destino.
Paso 1: Registrar un Efecto de Webhook
Crearemos un Efecto activado por la señal intent-updated, la cual ocurre cuando cambia el estado de un intent.
Cuando se genera este evento, el Efecto ejecutará una acción webhook que envía una solicitud HTTP POST al endpoint definido en la acción.
Asumimos que un servidor local está corriendo en:
http://localhost:3000/v2/effectsEjemplo de la petición:
import {
createHash,
createSignatureDigest,
generateProofResult,
} from './common';
import axios from 'axios';
const keyPair = {
public: '',
secret: '',
scheme: 'ed25519-raw',
};
const ledgerHandle = '<ledger-handle>';
const ledgerUrl = 'https://<ledger-url>/api/v2';
async function createIntentUpdatedEffect() {
try {
const effectBody = {
data: {
handle: 'business-intent-updated@<your-domain>',
signal: 'intent-updated',
action: {
schema: 'webhook',
endpoint: 'https://webhook.com.co/intents',
},
access: [
{
action: 'any',
signer: {
public: keyPair.public,
},
},
],
},
};
const dataHash = createHash(effectBody.data);
const proofCustom = {
moment: new Date().toISOString(),
domain: 'business-domain', // el dominio debe existir, de lo contrario se devolverá un error 403
};
const digest = createSignatureDigest(dataHash, proofCustom);
axios
.post(
`${ledgerUrl}/effects`,
{
...effectBody,
hash: dataHash,
meta: {
proofs: [
{
method: 'ed25519-v2',
public: keyPair.public,
digest: digest,
result: generateProofResult(
digest,
keyPair.secret,
),
custom: proofCustom,
},
],
},
},
{
headers: {
'x-ledger': ledgerHandle,
},
},
)
.then((response) => {
console.log('Respuesta:', JSON.stringify(response.data));
})
.catch((error) => {
console.error(
'Error:',
error.response ? error.response.data : error.message,
);
});
} catch (error) {
console.error(
'Error:',
error.response ? error.response.data : error.message,
);
}
}
void createIntentUpdatedEffect();Efecto creado exitosamente:
- Handle:
business-intent-updated@<tu-dominio> - Señal:
intent-updated - Acción:
webhook→https://webhook.com.co/intents
Asegúrate de incluir tu dominio en el handle. Esto garantiza que el Efecto se dispare solo para intents que involucren tu billetera. De lo contrario, no podrás registrar el efecto.
Filtrar estados
Es posible filtrar las notificaciones que se quieran recibir y no recibir notificaciones para estados transitorios sino solo para los estados finales completed o rejected
y solo para intents con el schema transfer.
para esto se puede agregar un filtro en la creación del webhook el cual filtrara las señales por los estados que se quieran.
Esto se hace agregandole el objeto filter dentro de data, en este caso queremos ser notificados solo por
intents completed y rejected, esto nos dejaria con el objeto data asi:
"data": {
"handle": "business-intent-updated@<your-domain>",
"signal": "intent-updated",
"filter": {
"intent.meta.status": {
"$in": [
"completed",
"rejected"
]
},
"intent.data.schema": "transfer"
},
"action": {
"schema": "webhook",
"endpoint": "https://webhook.com.co/intents"
},
"access": [
{
"action": "any",
"signer": {
"public": "tP0Si9tPTdEJM+UK0iXeuWO57nsSv9A5cF13NUYqnxQ="
}
}
]
}Paso 2: Manejar llamadas Webhook (Actualizaciones de estado de Intent)
Ahora implementaremos un servidor HTTP simple en Node.js usando Express para manejar la llamada del webhook.
Cuando el estado de un intent cambia, se emite la señal intent-updated. Este evento es enviado a nuestro endpoint webhook por el Efecto registrado. El cuerpo del evento incluye:
handle: identificador único del evento (usado para idempotencia)signal: señal del evento (intent-updated)parent: objectodatadel intent originalintent: el registro completo del intent
El campo que nos interesa es intent.meta.status, para ver todos los
posibles estados consulta Ciclo de vida del
Intent
Aquí un ejemplo simplificado:
{
"data": {
"handle": "<handle-del-evento>",
"signal": "intent-updated",
"parent": {...},
"intent": {
"hash": "<hash-del-intent>",
"data": {
"handle": "<handle-del-intent>",
"claims": [
{
"action": "transfer",
"amount": 20000,
"source": {
"handle": "source.wallet"
},
"symbol": {
"handle": "cop"
},
"target": {
"handle": "target.wallet"
}
}
],
"schema": "transfer",
"custom": {...},
"access": [...],
"config": {...}
},
"luid": "...",
"meta": {
"proofs": [...],
"routed": true,
"status": "completed",
"thread": "...",
"moment": "...",
"owners": [...]
}
}
},
"hash": "<hash-del-evento>",
"meta": {...}
}Verificaremos la carga del evento y ejecutaremos un efecto secundario en un sistema externo. Este efecto secundario puede, por ejemplo, notificar al usuario una vez completada la transferencia o activar un proceso interno para conciliar la transacción.
Para asegurar que el mismo evento no sea procesado múltiples veces, usaremos el handle del evento como identificador único para garantizar la idempotencia.
Esto es particularmente importante porque el Ledger puede volver a intentar la entrega del evento si no recibe una respuesta exitosa, como en el caso de un error de red.
import express from 'express';
const app = express();
const port = 3000;
const SIGNAL_INTENT_UPDATED = 'intent-updated';
const sdk = new LedgerSdk({
server: '<URL del ledger>',
signer: {
format: 'ed25519-raw',
public: '<llave pública del ledger>',
},
});
/**
* LedgerSdk.proofs devuelve una nueva instancia de un
* cliente de verificación que puede usarse para verificar
* pruebas provenientes del ledger. Por defecto verifica
* que los registros tengan al menos 1 prueba, al
* llamar al método ledger() también verificamos que la
* respuesta esté firmada por la llave esperada del ledger.
*/
const verificationClient = sdk.proofs.ledger();
app.post('/intent-updated', async (req, res) => {
// 1. Reconocer el evento enviando HTTP 202 (ACCEPTED) como respuesta
console.log(`Evento recibido, reconociendo...`);
res.status(202).send();
// 2. Verificar que el evento esté firmado por la llave esperada del ledger
console.log(`Verificando evento...`);
const event = req.body;
await verificationClient.verify(event);
// 3. Chequeo de idempotencia, verificar que el evento no se haya procesado ya
if (isEventProcessed(event?.data?.handle)) {
// Evento ya procesado
return;
}
// 4. Verificar que la señal sea correcta
const signal = event?.data?.signal;
if (signal !== SIGNAL_INTENT_UPDATED) {
// Señal incorrecta enviada
console.error(
`Señal incorrecta '${signal}', se esperaba '${SIGNAL_INTENT_UPDATED}'`,
);
return;
}
// 5. Enviar notificación del nuevo estado
await processStatusChange(event?.data?.intent?.meta?.status);
});
app.listen(port, () => {
console.log(`Servidor corriendo en el puerto ${port}`);
});Asegúrate de manejar el evento rápidamente. Devuelve un código de estado HTTP 2XX inmediatamente para confirmar la entrega. Cualquier respuesta que no sea 2XX (excepto 501) o un error de red causará que el Ledger reintente con un backoff exponencial, comenzando en 1 segundo e incrementando 20% cada vez, hasta un máximo de 1 hora.
La carga del evento debe verificarse usando la llave pública del Ledger. Esto asegura la autenticidad y protege contra manipulaciones durante el tránsito.
const sdk = new LedgerSdk({
server: '<URL del ledger>',
signer: {
format: 'ed25519-raw',
public: '<llave pública del ledger>',
},
});
const verificationClient = sdk.proofs.ledger();
await verificationClient.verify(event);¿Qué sigue?
Con tu Efecto configurado, puedes:
- Automatizar procesos con base en cambios de estado (por ejemplo,
completed,rejected) - Integrarte con servicios internos o de terceros
- Crear trazabilidad de auditoría registrando todas las transiciones de estado en un sistema externo de monitoreo
Historial de cambios
- Corregido• Se corrigieron enlaces que llevaban a la versión en español
- Agregado• Se agrego filtro por schema al registro del effect
- Agregado• Se agrego filtros por estado al registro del effect
- Agregado• Versión inicial