Zahlungsprüfung und Bestellverfolgung
Diese Anleitung erklärt, wie Sie Non-Custodial-Zahlungen über Smart Contracts von Volet.com auf Ethereum, BNB Smart Chain und TRON prüfen.
Ein Transaktions-Hash (txid) ist kein Zahlungsnachweis. Ihr Backend muss Ausführung, Finalität und Zahlungs-Events unabhängig prüfen, bevor es eine Bestellung als bezahlt markiert. Bei dieser Integration sendet Volet.com keine Benachrichtigungen an eine Status URL wie bei Hosted Checkout.
So funktioniert die Zahlungsprüfung
- Bestellung erstellen. Speichern Sie erwartetes Netzwerk, Asset, Empfangs-Wallet (
seller), Prozessor-Contract, den erwarteten Betrag für den Händler und gegebenenfalls das Wallet des Käufers. - Zahlungsversuch erfassen. Ordnen Sie die
txidder Zahlung der Bestellung zu. Speichern Sie Hashes vonapprove()-Transaktionen für USDT separat. - Ausführung und Finalität prüfen. Rufen Sie Blockchain-Daten über eine RPC-Verbindung im Backend oder einen gehosteten Blockchain-Dienst ab.
- Zahlungs-Events prüfen. Suchen Sie das Event
PaymentProcesseddes freigegebenen Prozessors. Prüfen Sie bei USDT zusätzlich den tatsächlichen Token-Transferan den Händler. - Mit der Bestellung abgleichen. Vergleichen Sie Händleradresse, Käufer (falls erforderlich) und Nettobetrag aus dem Event mit den unveränderlichen Bestelldaten im Backend.
- Bestellung einmal abschließen. Speichern Sie die geprüfte Zahlung atomar und lösen Sie eine idempotente Ausführung aus.
Sie können einen gehosteten Anbieter nutzen. Ein eigener Blockchain-Node ist nicht erforderlich.
Zahlungs-Events und Beträge
Die dokumentierten Prozessoren von Volet.com lösen PaymentProcessed aus, nachdem die Auszahlung an den Händler erfolgt ist:
event PaymentProcessed(
address indexed buyer,
address indexed seller,
uint256 commission,
uint256 totalPay
);
Für diese Deployments berechnen Sie den Nettobetrag, den der Händler erhält, so:
receivedAmount = totalPay - commission
Beide Werte sind in den kleinsten Einheiten des Assets angegeben. Die Berechnung gilt für beide Einstellungen von buyerPaysCommission.
| Asset | Erforderliche On-Chain-Nachweise |
|---|---|
| ETH, BNB | Erfolgreiche, finalisierte Ausführung und ein übereinstimmender PaymentProcessed-Log des freigegebenen Prozessors für das native Asset |
| USDT ERC-20, BEP-20 | Übereinstimmender PaymentProcessed-Log des Prozessors und USDT-Transfer-Log des freigegebenen Token-Contracts: Prozessor → Händler über receivedAmount |
| TRX | Erfolgreiche, solidified Ausführung und ein übereinstimmender PaymentProcessed-Log des freigegebenen TRX-Prozessors |
| USDT TRC-20 | Übereinstimmender PaymentProcessed-Log des Prozessors und USDT-TRC-20-Transfer-Log des freigegebenen Token-Contracts: Prozessor → Händler über receivedAmount |
Prüfen Sie immer die Adresse, die ein Event ausgelöst hat. Ein Log mit dem richtigen Event-Namen, aber von einem fremden Contract ist kein Zahlungsnachweis. Direkte Überweisungen von Wallet zu Wallet gehören nicht zu diesem prozessorbasierten Zahlungsablauf.
Laut den Integrationsunterlagen der Prozessoren werden native Auszahlungen rückgängig gemacht, wenn die Überweisung an den Händler fehlschlägt. Daher dient das echte PaymentProcessed-Event in einer erfolgreichen, finalisierten Transaktion als Nachweis der nativen Auszahlung. Ansichten interner Überweisungen in Block-Explorern helfen bei der Untersuchung, werden für die hier beschriebene Prüfmethode aber nicht benötigt.
Diese Event-Prüfungen sind für die hier beschriebenen bereitgestellten Prozessor-Implementierungen maßgeblich. Gleichen Sie ABI und Prozessor-Adressen mit dem Deployment ab, das Sie verwenden.
Anforderungen an die Finalität
| Netzwerk | Abschlussbedingung |
|---|---|
| Ethereum | Transaktion in einem finalisierten Block; erfolgreiches Receipt und übereinstimmende Zahlungs-Events |
| BNB Smart Chain | Transaktion erfüllt die konfigurierte, vom Anbieter unterstützte Finalitätsrichtlinie; erfolgreiches Receipt und übereinstimmende Zahlungs-Events |
| TRON | Transaktion ist über einen SolidityNode verfügbar, das Receipt meldet SUCCESS und die Zahlungs-Events stimmen überein |
Die Entwicklerbeispiele verwenden in EVM-Netzwerken 15 Bestätigungen. Das ist eine Richtlinie nach Anzahl der Bestätigungen, keine Ethereum-Finalität. Die Beispiele unten behalten eine Prüfung auf finalisierte Blöcke bei; verwenden Sie einen Anbieter, der die für Ihr Netzwerk erforderliche Finalitätssemantik unterstützt. Kann er diese Information nicht liefern, lassen Sie die Zahlung in Prüfung, statt sie als final zu behandeln.
Ist ein Anbieter nicht erreichbar oder liegt noch kein Receipt vor, versuchen Sie es erneut. Eine leere Antwort beweist nicht, dass eine Zahlung fehlgeschlagen ist.
Lebenszyklus einer Bestellung
| Status | Bedeutung | Ausführen? |
|---|---|---|
created | Bestellung gespeichert, keine Zahlung gesendet | Nein |
approval_pending | USDT-Freigabetransaktion gesendet | Nein |
pending | txid der Zahlung gespeichert; wartet auf Aufnahme in einen Block oder Finalität | Nein |
verifying | Ausführung, Finalität und Events werden geprüft | Nein |
paid | Geprüfte Zahlung passt zur Bestellung | Ja, einmal |
failed | Bestätigter Ausführungsfehler oder eindeutig fehlgeschlagene Validierung | Nein |
expired | Checkout-Zeitfenster abgelaufen; gesendete Zahlungen müssen trotzdem abgeglichen werden | Nein, bis zum Abgleich |
review | Unvollständige, mehrdeutige oder widersprüchliche Prüfdaten | Nein |
Ein Neuladen der Seite, ein abgelaufener Checkout oder ein geschlossener Browser-Tab darf den Prüfprozess im Backend nicht anhalten.
Erwartete Zahlung speichern
Legen Sie den Zahlungsdatensatz an, bevor Sie den Käufer bitten, die Transaktion zu senden. Beispiel:
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 ist der angeforderte Bestellbetrag. expectedSellerAmount ist der Nettobetrag, den der Händler erhalten muss, in kleinsten Einheiten. Ermitteln und speichern Sie ihn anhand der validierten serverseitigen Bestellung und der freigegebenen Gebührenkonfiguration mithilfe der Ergebnisse von previewPayment() des Prozessors. Leiten Sie ihn nicht aus beliebigen Browser-Eingaben oder einem fest codierten Gebührensatz ab.
Verwenden Sie für USDT 6 Decimals auf Ethereum und TRON und 18 Decimals auf BNB Smart Chain. ETH und BNB verwenden 18 Decimals, TRX verwendet 6 (SUN). Berechnen Sie Token-Beträge niemals mit Gleitkommaarithmetik.
Speichern Sie freigegebene Netzwerk-, Token- und Prozessor-Adressen in einer Allowlist im Backend. Die Funktion transfer() hat keinen Parameter orderId, daher muss Ihre Anwendung Zahlungsversuche mit Bestellungen verknüpfen.
Verwenden Sie eindeutige Datensätze für Zahlungsversuche und stellen Sie sicher, dass ein On-Chain-Zahlungs-Event nicht mehr als einer Bestellung zugeordnet werden kann. Ein Event wird durch (chain, txid, logIndex) identifiziert. Machen Sie außerdem die Ausführung von Bestellungen idempotent. Ein erneuter Versuch des Kunden muss einen neuen Versuch anlegen, statt einen früheren ausstehenden Hash zu überschreiben.
Prüfung auf Ethereum und BNB Smart Chain
Verwenden Sie im Backend ethers v6. Das folgende Beispiel prüft Ausführung und Finalität und anschließend einen echten PaymentProcessed-Log. Bei USDT prüft es zusätzlich die tatsächliche Token-Übertragung.
Das Argument order muss vertrauenswürdige Werte aus dem Backend enthalten. Übergeben Sie den provider für dasselbe Netzwerk wie 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' };
}
So verwenden Sie das Ergebnis: matched bedeutet, dass der Zahlungsnachweis die Prüfungen dieser Funktion bestanden hat. Bevor Sie die Bestellung auf paid setzen, reservieren Sie ihre eventId eindeutig in Ihrer Datenbank und schließen die Bestellung über eine idempotente serverseitige Transaktion ab.
Der Code gleicht bewusst authentifizierte Receipt-Logs ab, statt sich auf die Calldata der Transaktion zu verlassen. Eine direkte Transaktion an den Händler oder ein fremdes Transfer-Event zählen nicht.
Bei Zahlungen in nativem ETH und BNB ist das authentifizierte PaymentProcessed-Event des Prozessors der dokumentierte Auszahlungsnachweis. Bei USDT muss der zusätzliche Transfer vom freigegebenen Token-Contract stammen und Token vom Prozessor an den Verkäufer übertragen.
Konfigurieren Sie in der Produktion eigene zuverlässige RPC-Endpunkte und behandeln Sie Anbieterfehler und Wiederholungen rund um diese Funktion.
Prüfung auf TRON
Fragen Sie auf TRON ein solidified Transaktions-Receipt über den SolidityNode-Endpunkt ab:
POST https://api.trongrid.io/walletsolidity/gettransactioninfobyid
Das Receipt enthält die rohen Event-Logs. Die folgende Implementierung liest sie direkt und hängt damit weder von der Verfügbarkeit der Event-Liste eines Indexers noch von dessen Adressformat ab. Sie verwendet ethers v6, um die EVM-kompatiblen Event-Daten zu dekodieren, und tronweb, um TRON-Adressen zu normalisieren.
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' };
}
Die Funktion prüft das solidified Ausführungs-Receipt, das echte PaymentProcessed-Event und bei USDT das übereinstimmende Transfer-Event des Tokens. Fehlt das solidified Receipt, ist die Zahlung ausstehend, nicht fehlgeschlagen. Bei Fehlern oder unvollständigen Event-Daten lassen Sie die Zahlung in Prüfung und versuchen es erneut.
Verwenden Sie die freigegebenen Prozessor- und Token-Adressen im TRON-Mainnet aus der Smart-Contract-Referenz. Akzeptieren Sie kein anderes Netzwerk und keine andere Prozessor-Adresse, die vom Browser übermittelt werden.
Details zur SolidityNode-Finalität finden Sie unter TRON confirmation semantics.
Zahlungen Bestellungen zuordnen und Replays verhindern
Die Funktion transfer() enthält keine orderId. Für eine zuverlässige Zuordnung von Bestellungen und Zahlungen ist Ihr Backend verantwortlich.
- Wenn der Checkout eine Zahlung sendet: Speichern Sie ihre
txidbei der bestehenden Bestellung und erfassen Sie einen unveränderlichen Zahlungsversuch. - Bei der Verarbeitung eines Zahlungs-Events: Prüfen Sie, ob Verkäufer, optionaler Käufer und Nettobetrag aus dem Event zur erwarteten Bestellung passen.
- Für Idempotenz: Verwenden Sie für jedes Zahlungs-Event einen eindeutigen Schlüssel
(chain, txid, logIndex). Verhindern Sie, dass dasselbe Event mehrere Bestellungen abschließt. Führen Sie eine Bestellung nicht zweimal aus, auch wenn der Prüfprozess es erneut versucht. - Für Wiederholungen und verspätete Zahlungen: Bewahren Sie frühere Zahlungsversuche auf und gleichen Sie Transaktionen ab, die nach Ablauf des Checkouts finalisiert werden.
Mehrere Zahlungen können denselben Verkäufer und Betrag haben. Verlassen Sie sich nicht nur auf Verkäufer, Betrag oder ein Zeitfenster, und ändern Sie keine letzten Nachkommastellen, um eindeutige Rechnungsbeträge zu erzeugen. Halten Sie überall dort, wo Ihr Checkout die Transaktion sendet, eine direkte Zuordnung von Transaktion und Bestellung fest.
Wiederholungen und Betriebsfehler
| Situation | Maßnahme |
|---|---|
| Transaktion noch nicht gemined oder solidified | Versuch ausstehend lassen und erneut prüfen |
| RPC-Timeout, 429 oder vorübergehender Netzwerkfehler | Mit Backoff wiederholen; nicht auf eine fehlgeschlagene Zahlung schließen |
| Freigabetransaktion erfolgreich | Freigabe erfassen und auf die separate Zahlungstransaktion warten |
| Finalisierte Transaktion rückgängig gemacht | Versuch als fehlgeschlagen markieren; nicht ausführen |
| Erfolgreiches Receipt, aber kein passendes Event | In Prüfung lassen; Prozessor, Token, Verkäufer und erwarteten Betrag kontrollieren |
| Browser geschlossen oder Checkout abgelaufen | Abgleich im Backend fortsetzen |
| Zahlungs-Event bereits von einer Bestellung reserviert | Keiner anderen Bestellung zuordnen |
Ein Bestätigungszähler im Browser, ein Status-Badge im Block-Explorer oder ein Screenshot des Kunden reichen nicht aus, um eine Bestellung abzuschließen.
Manuelle Prüfung
Verwenden Sie Etherscan, BscScan oder TronScan, um eine Zahlung zu untersuchen. Prüfen Sie die Transaktion im richtigen Netzwerk: erfolgreiche Ausführung, PaymentProcessed-Event des freigegebenen Prozessors, Händleradresse und Nettobetrag. Prüfen Sie bei USDT zusätzlich den echten Token-Transfer vom Prozessor an den Händler.
Eine manuelle Prüfung hilft dem Support beim Abgleich von Zahlungen, ein produktiver Checkout muss aber eine automatisierte serverseitige Prüfung umsetzen.
Checkliste für den Go-live
- Freigegebene Prozessor- und USDT-Token-Adressen im Mainnet serverseitig konfiguriert.
- ABI des Events
PaymentProcessedstimmt mit den bereitgestellten Prozessor-Contracts überein. - Korrekte Asset-Genauigkeit: ETH/BNB 18, USDT auf Ethereum/TRON 6, USDT auf BSC 18, TRX 6.
- Bestellungen und erwartete Nettobeträge für den Verkäufer vor dem Senden der Zahlung erfasst.
- Hashes von Token-Freigaben getrennt von Zahlungs-Hashes gespeichert.
- Receipt-Status und netzwerkspezifische Finalität geprüft.
-
PaymentProcessedgegen auslösende Prozessor-Adresse, Verkäufer und erwarteten Nettobetrag geprüft. - USDT-
Transfergegen auslösende Token-Adresse, Prozessor, Verkäufer und Betrag geprüft. - Optionales festes Käufer-Wallet abgeglichen, wenn der Checkout es verlangt.
- Jedes Event eindeutig reserviert; Ausführung ist idempotent.
- Fehlgeschlagene, doppelte, zu niedrige Zahlungen und Zahlungen an falsche Empfänger getestet.
- RPC-Ausfälle, fehlende Logs, verspätete Bestätigungen und abgelaufene Checkouts behandelt.
- Prüfung mit bekannten erfolgreichen und fehlgeschlagenen Transaktionen für jedes unterstützte Asset getestet.
Siehe auch
- Smart-Contract-Integration – Überblick über die Non-Custodial-Zahlungsintegration.
- Ethereum- und BNB-Smart-Chain-Integration – Zahlungen in ETH, BNB und USDT senden.
- TRON-Integration – Zahlungen in TRX und USDT TRC-20 senden.
- Smart-Contract-Referenz – Prozessor-Adressen, Token-Decimals und Event-Schnittstelle.
- Fehlerbehebung – häufige Wallet- und Zahlungsfehler.