Saltar al contenido principal

Verificación de pagos y seguimiento de pedidos

Esta guía explica cómo verificar los pagos no custodiales realizados con contratos inteligentes de Volet.com en Ethereum, BNB Smart Chain y TRON.

Un hash de transacción (txid) no es prueba de pago. Tu backend debe comprobar por su cuenta la ejecución, la finalidad y los eventos de pago antes de marcar un pedido como pagado. En esta integración, Volet.com no envía notificaciones a la Status URL como en Hosted Checkout.

Cómo funciona la verificación de pagos​

  1. Crea un pedido. Guarda la red, el activo, la billetera receptora (seller), el contrato procesador, el monto esperado para el comercio y, cuando corresponda, la billetera del comprador.
  2. Registra el intento de pago. Asocia el txid del pago con el pedido. Guarda aparte los hashes de las transacciones approve() de USDT.
  3. Comprueba la ejecución y la finalidad. Obtén los datos de la blockchain mediante una conexión RPC del backend o un servicio blockchain alojado.
  4. Verifica los eventos de pago. Busca el evento PaymentProcessed emitido por el procesador aprobado. En USDT, verifica además el Transfer real del token hacia el comercio.
  5. Asocia el pago con el pedido. Compara la dirección del comercio del evento, el comprador (si se requiere) y el monto neto con los datos inmutables del pedido en el backend.
  6. Completa el pedido una sola vez. Registra de forma atómica el pago verificado y lanza una entrega idempotente.

Puedes usar un proveedor alojado. No necesitas operar tu propio nodo de blockchain.

Eventos de pago y montos​

Los procesadores documentados de Volet.com emiten PaymentProcessed después de pagar al comercio:

event PaymentProcessed(
address indexed buyer,
address indexed seller,
uint256 commission,
uint256 totalPay
);

En estos despliegues, el monto neto que recibe el comercio se calcula así:

receivedAmount = totalPay - commission

Ambos valores usan las unidades mínimas del activo. El cálculo se aplica con cualquier valor de buyerPaysCommission.

ActivoEvidencia on-chain requerida
ETH, BNBEjecución exitosa y finalizada, y un log PaymentProcessed coincidente emitido por el procesador aprobado del activo nativo
USDT ERC-20, BEP-20Log PaymentProcessed coincidente del procesador y log Transfer de USDT del contrato de token aprobado: procesador → comercio por receivedAmount
TRXEjecución exitosa y solidificada, y un log PaymentProcessed coincidente emitido por el procesador aprobado de TRX
USDT TRC-20Log PaymentProcessed coincidente del procesador y log Transfer de USDT TRC-20 del contrato de token aprobado: procesador → comercio por receivedAmount

Comprueba siempre la dirección que emitió cada evento. Un log con el nombre de evento correcto, pero emitido por un contrato ajeno, no prueba el pago. Las transferencias directas de billetera a billetera quedan fuera de este flujo de pago basado en el procesador.

Según los materiales de integración de los procesadores, los pagos nativos se revierten si falla la transferencia al comercio; por eso, el evento PaymentProcessed auténtico en una transacción exitosa y finalizada sirve como evidencia del pago nativo. Las vistas de transferencias internas de los exploradores sirven para investigar, pero el método de verificación descrito aquí no las requiere.

Estas comprobaciones de eventos son fiables para las implementaciones desplegadas de los procesadores descritas aquí. Valida la ABI y las direcciones de los procesadores contra el despliegue que uses.

Requisitos de finalidad​

RedCondición para completar
EthereumTransacción en un bloque finalizado; recibo exitoso y eventos de pago coincidentes
BNB Smart ChainLa transacción cumple la política de finalidad configurada y admitida por el proveedor; recibo exitoso y eventos de pago coincidentes
TRONLa transacción está disponible en un SolidityNode, el recibo indica SUCCESS y los eventos de pago coinciden

Los ejemplos para desarrolladores usan 15 confirmaciones en las redes EVM. Es una política de número de confirmaciones, no la finalidad de Ethereum. Los ejemplos de abajo mantienen una comprobación de bloque finalizado; usa un proveedor que admita la semántica de finalidad que requiere tu red. Si no puede proporcionar esa información, deja el pago en revisión en lugar de considerarlo final.

Si un proveedor no está disponible o aún no aparece el recibo, vuelve a intentarlo. Una respuesta vacía no prueba que el pago haya fallado.

Ciclo de vida del pedido​

EstadoSignificado¿Entregar?
createdPedido guardado, sin pago enviadoNo
approval_pendingTransacción de aprobación de USDT enviadaNo
pendingtxid del pago guardado; a la espera de inclusión o finalidadNo
verifyingSe están comprobando la ejecución, la finalidad y los eventosNo
paidEl pago verificado coincide con el pedidoSí, una vez
failedFallo de ejecución confirmado o validación fallida de forma concluyenteNo
expiredVenció el plazo de pago; los pagos enviados aún requieren conciliaciónNo, hasta conciliar
reviewDatos de verificación incompletos, ambiguos o contradictoriosNo

Recargar la página, que venza el plazo de pago o cerrar la pestaña del navegador no debe detener el proceso de verificación del backend.

Guarda el pago esperado​

Crea el registro del pago antes de pedir al comprador que envíe la transacción. Ejemplo:

function createPendingPaymentRecord(order) {
return {
orderId: order.orderId,
chain: order.chain, // e.g. 'ethereum', 'bsc', 'tron'
chainId: order.chainId ?? null,
asset: order.asset,
tokenAddress: order.tokenAddress ?? null,
processorAddress: order.processorAddress,
sellerAddress: order.sellerAddress,
buyerAddress: order.buyerAddress ?? null,
amountBaseUnits: order.amountBaseUnits.toString(),
expectedSellerAmount: order.expectedSellerAmount.toString(),
buyerPaysCommission: order.buyerPaysCommission,
approvalTxids: [],
paymentAttempts: [],
status: 'created'
};
}

amountBaseUnits es el monto solicitado del pedido. expectedSellerAmount es el monto neto que debe recibir el comercio, en unidades mínimas. Determínalo y guárdalo a partir del pedido validado en tu servidor y de la configuración de comisiones aprobada, usando los resultados de previewPayment() del procesador. No lo obtengas de datos arbitrarios del navegador ni de un porcentaje de comisión fijo en el código.

En USDT, usa 6 decimales en Ethereum y TRON y 18 decimales en BNB Smart Chain. ETH y BNB usan 18 decimales; TRX usa 6 (SUN). Nunca uses aritmética de punto flotante para calcular montos de tokens.

Guarda las direcciones aprobadas de redes, tokens y procesadores en una lista de permitidos del backend. La función transfer() no tiene el parámetro orderId, así que tu aplicación debe vincular los intentos de pago con los pedidos.

Usa registros únicos de intentos de pago y asegúrate de que un evento de pago on-chain no pueda asignarse a más de un pedido. Un evento se identifica por (chain, txid, logIndex). Haz también que la entrega de pedidos sea idempotente. Un reintento del cliente debe crear un intento nuevo en lugar de sobrescribir un hash pendiente anterior.

Verificación en Ethereum y BNB Smart Chain​

Usa ethers v6 en el backend. El siguiente ejemplo verifica la ejecución y la finalidad y luego comprueba un log PaymentProcessed auténtico. En USDT, además comprueba la transferencia real del token.

El argumento order debe contener valores de confianza del backend. Pasa el provider de la misma red que order.chainId.

import { ethers } from 'ethers';

const events = new ethers.Interface([
'event PaymentProcessed(address indexed buyer, address indexed seller, uint256 commission, uint256 totalPay)',
'event Transfer(address indexed from, address indexed to, uint256 value)'
]);

function sameEvmAddress(a, b) {
return ethers.getAddress(a) === ethers.getAddress(b);
}

async function verifyEvmPayment(provider, txid, order) {
const chainId = (await provider.getNetwork()).chainId;
if (chainId !== BigInt(order.chainId)) {
return { state: 'review', reason: 'Unexpected network' };
}

const receipt = await provider.getTransactionReceipt(txid);
if (!receipt) return { state: 'pending' };
if (receipt.status !== 1) return { state: 'failed', reason: 'Execution reverted' };

let finalized;
try {
finalized = await provider.getBlock('finalized');
} catch {
return { state: 'review', reason: 'Finality data unavailable' };
}
if (!finalized || receipt.blockNumber > finalized.number) {
return { state: 'pending' };
}

// Make sure the receipt still belongs to the canonical block.
const block = await provider.getBlock(receipt.blockNumber);
if (!block || block.hash !== receipt.blockHash) {
return { state: 'review', reason: 'Block mismatch' };
}

const processor = ethers.getAddress(order.processorAddress);
const seller = ethers.getAddress(order.sellerAddress);
const token = order.tokenAddress
? ethers.getAddress(order.tokenAddress)
: null;
const expectedNet = BigInt(order.expectedSellerAmount);

for (const log of receipt.logs) {
if (!sameEvmAddress(log.address, processor)) continue;

let event;
try { event = events.parseLog(log); } catch { continue; }
if (event?.name !== 'PaymentProcessed') continue;
if (!sameEvmAddress(event.args.seller, seller)) continue;
if (order.buyerAddress &&
!sameEvmAddress(event.args.buyer, order.buyerAddress)) continue;

const net = event.args.totalPay - event.args.commission;
if (net < 0n || net !== expectedNet) continue;

if (token) {
const tokenPaid = receipt.logs.some((otherLog) => {
if (!sameEvmAddress(otherLog.address, token)) return false;
let transfer;
try { transfer = events.parseLog(otherLog); } catch { return false; }
return transfer?.name === 'Transfer' &&
sameEvmAddress(transfer.args.from, processor) &&
sameEvmAddress(transfer.args.to, seller) &&
transfer.args.value === net;
});
if (!tokenPaid) continue;
}

return {
state: 'matched',
buyer: event.args.buyer,
sellerReceived: net.toString(),
eventId: `${order.chain}:${txid.toLowerCase()}:${log.index}`
};
}

return { state: 'review', reason: 'No matching payment event' };
}

Cómo usar este resultado: matched significa que la evidencia del pago pasó las comprobaciones de esta función. Antes de cambiar el pedido a paid, reclama su eventId de forma única en tu base de datos y completa el pedido mediante una transacción idempotente en el servidor.

El código asocia intencionalmente los logs autenticados del recibo en lugar de basarse en el calldata de la transacción. Una transacción directa al comercio o un evento Transfer ajeno no cuentan.

En los pagos con ETH y BNB nativos, el evento PaymentProcessed autenticado del procesador es la evidencia documentada del pago. En USDT, el Transfer adicional debe emitirlo el contrato de token aprobado y debe transferir los tokens del procesador al vendedor.

En producción, configura tus propios endpoints RPC fiables y gestiona los errores del proveedor y los reintentos alrededor de esta función.

Verificación en TRON​

En TRON, consulta un recibo de transacción solidificado mediante el endpoint del SolidityNode:

POST https://api.trongrid.io/walletsolidity/gettransactioninfobyid

El recibo incluye los logs de eventos sin procesar. La siguiente implementación los lee directamente, sin depender de que un indexador tenga la lista de eventos ni de su formato de direcciones. Usa ethers v6 para decodificar los datos de eventos compatibles con EVM y tronweb para normalizar las direcciones de TRON.

import { ethers } from 'ethers';
import { TronWeb } from 'tronweb';

const tronEvents = new ethers.Interface([
'event PaymentProcessed(address indexed buyer, address indexed seller, uint256 commission, uint256 totalPay)',
'event Transfer(address indexed from, address indexed to, uint256 value)'
]);

function tronAddress20(address) {
// Base58 addresses use 41 + 20-byte account; EVM event addresses use 20 bytes.
if (typeof address !== 'string') throw new Error('Invalid address');
if (TronWeb.isAddress(address)) {
return TronWeb.address.toHex(address).slice(-40).toLowerCase();
}
const hex = address.replace(/^0x/, '').toLowerCase();
if (/^[0-9a-f]{40}$/.test(hex)) return hex;
if (/^41[0-9a-f]{40}$/.test(hex)) return hex.slice(2);
throw new Error('Invalid TRON address format');
}

function decodeTronLog(log) {
return tronEvents.parseLog({
topics: (log.topics || []).map((topic) => `0x${topic.replace(/^0x/, '')}`),
data: `0x${(log.data || '').replace(/^0x/, '')}`
});
}

async function verifyTronPayment(txid, order, tronGridApiKey) {
if (!/^[0-9a-fA-F]{64}$/.test(txid)) {
throw new Error('Invalid TRON transaction ID');
}

const response = await fetch(
'https://api.trongrid.io/walletsolidity/gettransactioninfobyid',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'TRON-PRO-API-KEY': tronGridApiKey
},
body: JSON.stringify({ value: txid })
}
);
if (!response.ok) throw new Error(`TronGrid HTTP ${response.status}`);

const info = await response.json();
if (info.Error) throw new Error(`TronGrid: ${info.Error}`);
if (!info.id) return { state: 'pending' };
if (info.id.toLowerCase() !== txid.toLowerCase()) {
return { state: 'review', reason: 'Transaction ID mismatch' };
}
if (!info.receipt?.result) {
return { state: 'review', reason: 'Receipt status unavailable' };
}
if (info.receipt.result !== 'SUCCESS') {
return { state: 'failed', reason: info.receipt.result };
}

const processor = tronAddress20(order.processorAddress);
const seller = tronAddress20(order.sellerAddress);
const token = order.tokenAddress ? tronAddress20(order.tokenAddress) : null;
const expectedNet = BigInt(order.expectedSellerAmount);
const logs = info.log || [];

for (const [index, log] of logs.entries()) {
if (tronAddress20(log.address) !== processor) continue;
let event;
try { event = decodeTronLog(log); } catch { continue; }
if (event?.name !== 'PaymentProcessed') continue;
if (tronAddress20(event.args.seller) !== seller) continue;
if (order.buyerAddress &&
tronAddress20(event.args.buyer) !== tronAddress20(order.buyerAddress)) continue;

const net = event.args.totalPay - event.args.commission;
if (net < 0n || net !== expectedNet) continue;

if (token) {
const tokenPaid = logs.some((other) => {
if (tronAddress20(other.address) !== token) return false;
let transfer;
try { transfer = decodeTronLog(other); } catch { return false; }
return transfer?.name === 'Transfer' &&
tronAddress20(transfer.args.from) === processor &&
tronAddress20(transfer.args.to) === seller &&
transfer.args.value === net;
});
if (!tokenPaid) continue;
}

return {
state: 'matched',
buyer: TronWeb.address.fromHex(`41${tronAddress20(event.args.buyer)}`),
sellerReceived: net.toString(),
eventId: `tron:${txid.toLowerCase()}:${index}`
};
}

return { state: 'review', reason: 'No matching payment event' };
}

La función verifica el recibo de ejecución solidificado, el evento PaymentProcessed auténtico y, en USDT, el evento Transfer del token coincidente. Si falta el recibo solidificado, el pago está pendiente, no fallido. Ante errores o información de eventos incompleta, deja el pago en revisión y vuelve a intentarlo.

Usa las direcciones aprobadas de los procesadores y tokens en la mainnet de TRON que figuran en la Referencia de contratos inteligentes. No aceptes otra red ni otra dirección de procesador enviadas desde el navegador.

Consulta TRON confirmation semantics para conocer los detalles de la finalidad en SolidityNode.

Asocia los pagos con los pedidos y evita repeticiones​

La función transfer() no incluye un orderId. Mantener una asociación fiable entre pedidos y pagos es responsabilidad de tu backend.

  • Cuando la página de pago envía un pago: guarda su txid en el pedido existente y registra un intento de pago inmutable.
  • Al procesar un evento de pago: comprueba que el vendedor, el comprador opcional y el monto neto del evento coincidan con el pedido esperado.
  • Para la idempotencia: usa una clave única (chain, txid, logIndex) para cada evento de pago. Evita que el mismo evento complete varios pedidos. No entregues un pedido dos veces aunque el proceso de verificación lo reintente.
  • Para reintentos y pagos tardíos: conserva los intentos de pago anteriores y concilia las transacciones que se finalizan después de que vence el plazo de pago.

Varios pagos pueden tener el mismo vendedor y monto. No te bases solo en el vendedor, el monto o una ventana de tiempo, ni modifiques los últimos decimales para fabricar montos de factura únicos. Mantén una asociación directa entre transacción y pedido en todos los puntos donde tu página de pago envía la transacción.

Reintentos y errores operativos​

SituaciónAcción
La transacción aún no se minó o no está solidificadaMantén el intento pendiente y vuelve a intentarlo
Tiempo de espera de RPC, 429 o error de red transitorioReintenta con espera progresiva; no concluyas que el pago falló
La transacción de aprobación fue exitosaRegistra la aprobación y espera la transacción de pago aparte
La transacción finalizada se revirtióMarca el intento como fallido; no entregues el pedido
Recibo exitoso, pero evento sin coincidenciaDéjalo en revisión; revisa el procesador, el token, el vendedor y el monto esperado
Se cierra el navegador o vence el plazo de pagoContinúa la conciliación en el backend
El evento de pago ya está reclamado por un pedidoNo lo asignes a otro pedido

Un contador de confirmaciones en el navegador, la insignia de estado de un explorador de bloques o una captura de pantalla del cliente no bastan para completar un pedido.

Verificación manual​

Usa Etherscan, BscScan o TronScan para investigar un pago. Revisa la transacción en la red correcta: ejecución exitosa, evento PaymentProcessed del procesador aprobado, dirección del comercio y monto neto. En USDT, revisa también el Transfer auténtico del token del procesador al comercio.

La revisión manual puede ayudar al equipo de soporte a conciliar pagos, pero una página de pago en producción debe implementar una verificación automática en el servidor.

Lista de verificación para el lanzamiento​

  • Direcciones aprobadas de los procesadores y del token USDT en mainnet configuradas en el servidor.
  • La ABI del evento PaymentProcessed coincide con los contratos procesadores desplegados.
  • Precisión correcta de los activos: ETH/BNB 18, USDT en Ethereum/TRON 6, USDT en BSC 18, TRX 6.
  • Pedidos y montos netos esperados para el vendedor registrados antes de enviar el pago.
  • Hashes de aprobación de tokens guardados aparte de los hashes de pago.
  • Estado del recibo y finalidad específica de cada red comprobados.
  • PaymentProcessed verificado contra la dirección del procesador emisor, el vendedor y el monto neto esperado.
  • Transfer de USDT verificado contra la dirección del token emisor, el procesador, el vendedor y el monto.
  • Billetera fija del comprador comprobada cuando la página de pago lo requiere.
  • Cada evento reclamado de forma única; la entrega es idempotente.
  • Probados los pagos fallidos, duplicados, insuficientes y con destinatario incorrecto.
  • Gestionadas las caídas de RPC, los logs faltantes, las confirmaciones tardías y los plazos de pago vencidos.
  • Verificación probada con transacciones exitosas y fallidas conocidas para cada activo compatible.