Verificação de pagamentos e acompanhamento de pedidos
Este guia explica como verificar pagamentos não custodiais feitos com contratos inteligentes da Volet.com no Ethereum, na BNB Smart Chain e na TRON.
Um hash de transação (txid) não é prova de pagamento. Seu backend deve verificar de forma independente a execução, a finalidade e os eventos de pagamento antes de marcar um pedido como pago. Nesta integração, a Volet.com não envia notificações para a Status URL como no Hosted Checkout.
Como funciona a verificação de pagamentos
- Crie um pedido. Armazene a rede, o ativo, a carteira de recebimento (
seller), o contrato processador, o valor esperado para o lojista e, quando aplicável, a carteira do comprador. - Registre a tentativa de pagamento. Associe o
txiddo pagamento ao pedido. Armazene separadamente os hashes das transaçõesapprove()de USDT. - Verifique a execução e a finalidade. Obtenha os dados da blockchain por uma conexão RPC do backend ou por um serviço de blockchain hospedado.
- Verifique os eventos de pagamento. Encontre o evento
PaymentProcessedemitido pelo processador aprovado. Em USDT, verifique também oTransferreal do token para o lojista. - Associe ao pedido. Compare o endereço do lojista no evento, o comprador (se exigido) e o valor líquido com os dados imutáveis do pedido no backend.
- Conclua o pedido uma única vez. Registre de forma atômica o pagamento verificado e acione uma entrega idempotente.
Você pode usar um provedor hospedado. Não é preciso operar um nó de blockchain próprio.
Eventos de pagamento e valores
Os processadores documentados da Volet.com emitem PaymentProcessed depois de pagar o lojista:
event PaymentProcessed(
address indexed buyer,
address indexed seller,
uint256 commission,
uint256 totalPay
);
Nessas implantações, o valor líquido recebido pelo lojista é calculado assim:
receivedAmount = totalPay - commission
Os dois valores usam as menores unidades do ativo. O cálculo vale para qualquer valor de buyerPaysCommission.
| Ativo | Evidência on-chain exigida |
|---|---|
| ETH, BNB | Execução bem-sucedida e finalizada e um log PaymentProcessed correspondente emitido pelo processador aprovado do ativo nativo |
| USDT ERC-20, BEP-20 | Log PaymentProcessed correspondente do processador e log Transfer de USDT do contrato de token aprovado: processador → lojista no valor de receivedAmount |
| TRX | Execução bem-sucedida e solidificada e um log PaymentProcessed correspondente emitido pelo processador aprovado de TRX |
| USDT TRC-20 | Log PaymentProcessed correspondente do processador e log Transfer de USDT TRC-20 do contrato de token aprovado: processador → lojista no valor de receivedAmount |
Sempre verifique o endereço que emitiu cada evento. Um log com o nome de evento certo, mas emitido por um contrato não relacionado, não é evidência de pagamento. Transferências diretas de carteira para carteira ficam fora deste fluxo de pagamento baseado no processador.
Segundo os materiais de integração dos processadores, os pagamentos nativos são revertidos quando a transferência para o lojista falha; por isso, o evento PaymentProcessed autêntico em uma transação bem-sucedida e finalizada serve como evidência do pagamento nativo. As visualizações de transferências internas nos exploradores podem ajudar na investigação, mas não são exigidas pelo método de verificação descrito aqui.
Essas verificações de eventos são confiáveis para as implementações implantadas dos processadores descritas aqui. Valide a ABI e os endereços dos processadores com a implantação que você usa.
Requisitos de finalidade
| Rede | Condição para concluir |
|---|---|
| Ethereum | Transação em um bloco finalizado; recibo bem-sucedido e eventos de pagamento correspondentes |
| BNB Smart Chain | A transação atende à política de finalidade configurada e suportada pelo provedor; recibo bem-sucedido e eventos de pagamento correspondentes |
| TRON | A transação está disponível em um SolidityNode, o recibo informa SUCCESS e os eventos de pagamento correspondem |
Os exemplos para desenvolvedores usam 15 confirmações nas redes EVM. Essa é uma política de número de confirmações, não a finalidade do Ethereum. Os exemplos abaixo mantêm uma verificação de bloco finalizado; use um provedor que suporte a semântica de finalidade exigida pela sua rede. Se ele não puder fornecer essa informação, mantenha o pagamento em análise em vez de tratá-lo como final.
Se um provedor estiver indisponível ou o recibo ainda não tiver aparecido, tente de novo. Uma resposta vazia não prova que o pagamento falhou.
Ciclo de vida do pedido
| Estado | Significado | Entregar? |
|---|---|---|
created | Pedido salvo, nenhum pagamento enviado | Não |
approval_pending | Transação de aprovação de USDT enviada | Não |
pending | txid do pagamento salvo; aguardando inclusão ou finalidade | Não |
verifying | Execução, finalidade e eventos em verificação | Não |
paid | O pagamento verificado corresponde ao pedido | Sim, uma vez |
failed | Falha de execução confirmada ou falha conclusiva na validação | Não |
expired | O prazo de pagamento expirou; os pagamentos enviados ainda precisam de conciliação | Não, até a conciliação |
review | Dados de verificação incompletos, ambíguos ou conflitantes | Não |
Atualizar a página, o prazo de pagamento expirar ou fechar a aba do navegador não deve interromper o processo de verificação do backend.
Armazene o pagamento esperado
Crie o registro do pagamento antes de pedir ao comprador que envie a transação. Exemplo:
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 é o valor solicitado do pedido. expectedSellerAmount é o valor líquido que o lojista deve receber, nas menores unidades. Defina e salve esse valor com base no pedido validado no servidor e na configuração de taxas aprovada, usando os resultados de previewPayment() do processador. Não o derive de dados arbitrários do navegador nem de um percentual de taxa fixo no código.
Em USDT, use 6 decimais no Ethereum e na TRON e 18 decimais na BNB Smart Chain. ETH e BNB usam 18 decimais; TRX usa 6 (SUN). Nunca use aritmética de ponto flutuante para calcular valores de tokens.
Armazene os endereços aprovados de redes, tokens e processadores em uma lista de permissões no backend. A função transfer() não tem o parâmetro orderId, então o seu aplicativo deve vincular as tentativas de pagamento aos pedidos.
Use registros únicos de tentativas de pagamento e garanta que um evento de pagamento on-chain não possa ser atribuído a mais de um pedido. Um evento é identificado por (chain, txid, logIndex). Torne também a entrega dos pedidos idempotente. Uma nova tentativa do cliente deve criar um registro novo em vez de sobrescrever um hash pendente anterior.
Verificação no Ethereum e na BNB Smart Chain
Use ethers v6 no backend. O exemplo a seguir verifica a execução e a finalidade e depois confere um log PaymentProcessed autêntico. Em USDT, ele também confere a transferência real do token.
O argumento order deve conter valores confiáveis do backend. Passe o provider da mesma rede de 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' };
}
Como usar este resultado: matched significa que a evidência do pagamento passou nas verificações desta função. Antes de mudar o pedido para paid, registre o eventId dele de forma única no banco de dados e conclua o pedido por meio de uma transação idempotente no servidor.
O código associa intencionalmente os logs autenticados do recibo em vez de depender do calldata da transação. Uma transação direta para o lojista ou um evento Transfer não relacionado não contam.
Em pagamentos com ETH e BNB nativos, o evento PaymentProcessed autenticado do processador é a evidência documentada do pagamento. Em USDT, o Transfer adicional deve ser emitido pelo contrato de token aprovado e transferir os tokens do processador para o vendedor.
Em produção, configure seus próprios endpoints RPC confiáveis e trate os erros do provedor e as novas tentativas em torno desta função.
Verificação na TRON
Na TRON, consulte um recibo de transação solidificado pelo endpoint do SolidityNode:
POST https://api.trongrid.io/walletsolidity/gettransactioninfobyid
O recibo inclui os logs de eventos brutos. A implementação a seguir lê esses logs diretamente, sem depender de um indexador ter a lista de eventos nem do formato de endereços que ele usa. Ela usa ethers v6 para decodificar os dados de eventos compatíveis com EVM e tronweb para normalizar os endereços da 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' };
}
A função verifica o recibo de execução solidificado, o evento PaymentProcessed autêntico e, em USDT, o evento Transfer do token correspondente. A falta do recibo solidificado significa que o pagamento está pendente, não que falhou. Em caso de erros ou dados de eventos incompletos, mantenha o pagamento em análise e tente de novo.
Use os endereços aprovados dos processadores e tokens na mainnet da TRON indicados na Referência de contratos inteligentes. Não aceite outra rede nem outro endereço de processador enviados pelo navegador.
Veja TRON confirmation semantics para detalhes sobre a finalidade no SolidityNode.
Associe pagamentos a pedidos e evite repetições
A função transfer() não inclui um orderId. Manter uma associação confiável entre pedidos e pagamentos é responsabilidade do seu backend.
- Quando a página de pagamento envia um pagamento: salve o
txiddele no pedido existente e registre uma tentativa de pagamento imutável. - Ao processar um evento de pagamento: verifique se o vendedor, o comprador opcional e o valor líquido do evento correspondem ao pedido esperado.
- Para idempotência: use uma chave única
(chain, txid, logIndex)para cada evento de pagamento. Impeça que o mesmo evento conclua vários pedidos. Não entregue um pedido duas vezes, mesmo que o processo de verificação tente de novo. - Para novas tentativas e pagamentos atrasados: mantenha as tentativas de pagamento anteriores e concilie as transações finalizadas depois que o prazo de pagamento expira.
Vários pagamentos podem ter o mesmo vendedor e valor. Não dependa apenas do vendedor, do valor ou de uma janela de tempo, nem altere as últimas casas decimais para criar valores de fatura únicos. Mantenha uma associação direta entre transação e pedido em todos os pontos em que a sua página de pagamento envia a transação.
Novas tentativas e erros operacionais
| Situação | Ação |
|---|---|
| A transação ainda não foi minerada ou solidificada | Mantenha a tentativa pendente e tente de novo |
| Tempo esgotado no RPC, 429 ou erro de rede temporário | Tente de novo com espera progressiva; não conclua que o pagamento falhou |
| A transação de aprovação foi bem-sucedida | Registre a aprovação e aguarde a transação de pagamento separada |
| A transação finalizada foi revertida | Marque a tentativa como falha; não entregue o pedido |
| Recibo bem-sucedido, mas evento sem correspondência | Mantenha em análise; confira o processador, o token, o vendedor e o valor esperado |
| O navegador é fechado ou o prazo de pagamento expira | Continue a conciliação no backend |
| O evento de pagamento já foi registrado para um pedido | Não o atribua a outro pedido |
Um contador de confirmações no navegador, o selo de status de um explorador de blocos ou uma captura de tela do cliente não bastam para concluir um pedido.
Verificação manual
Use o Etherscan, o BscScan ou o TronScan ao investigar um pagamento. Confira a transação na rede certa: execução bem-sucedida, evento PaymentProcessed do processador aprovado, endereço do lojista e valor líquido. Em USDT, confira também o Transfer autêntico do token do processador para o lojista.
A verificação manual ajuda a equipe de suporte a conciliar pagamentos, mas uma página de pagamento em produção deve implementar uma verificação automática no servidor.
Checklist para o lançamento
- Endereços aprovados dos processadores e do token USDT na mainnet configurados no servidor.
- A ABI do evento
PaymentProcessedcorresponde aos contratos processadores implantados. - Precisão correta dos ativos: ETH/BNB 18, USDT no Ethereum/TRON 6, USDT na BSC 18, TRX 6.
- Pedidos e valores líquidos esperados para o vendedor registrados antes do envio do pagamento.
- Hashes de aprovação de tokens mantidos separados dos hashes de pagamento.
- Status do recibo e finalidade específica de cada rede verificados.
-
PaymentProcessedverificado em relação ao endereço do processador emissor, ao vendedor e ao valor líquido esperado. -
Transferde USDT verificado em relação ao endereço do token emissor, ao processador, ao vendedor e ao valor. - Carteira fixa do comprador conferida quando a página de pagamento exigir.
- Cada evento registrado de forma única; a entrega é idempotente.
- Testados pagamentos com falha, duplicados, abaixo do valor e para o destinatário errado.
- Tratadas indisponibilidades de RPC, logs ausentes, confirmações atrasadas e prazos de pagamento expirados.
- Verificação testada com transações bem-sucedidas e com falha conhecidas para cada ativo suportado.
Relacionados
- Integração com contratos inteligentes — visão geral da integração de pagamentos não custodiais.
- Integração com Ethereum e BNB Smart Chain — envie pagamentos em ETH, BNB e USDT.
- Integração com TRON — envie pagamentos em TRX e USDT TRC-20.
- Referência de contratos inteligentes — endereços dos processadores, decimais dos tokens e interface de eventos.
- Solução de problemas — erros comuns de carteiras e pagamentos.