Tracking the status of cross-chain transactions is essential for providing a good user experience in hybrid dApps. The TAC SDK provides robust tools for monitoring the progress of transactions across both TON and TAC EVM chains.
Cross-chain transactions go through multiple stages across different blockchains. The TAC SDK’s tracking tools help you monitor this process and provide appropriate feedback to users.
Understanding Transaction Flow
When a cross-chain transaction is initiated, it passes through several stages:
- TON Side: Transaction is sent from the user’s TON wallet
- Sequencer Network: Transaction is collected, validated, and included in Merkle trees
- TAC EVM Side: Transaction is executed on the target EVM contract
- Return Path: Results and/or assets may be sent back to TON
Creating a Transaction Tracker
To track transactions, you’ll use the OperationTracker class:
import { OperationTracker, Network } from '@tonappchain/sdk';
// Create a tracker instance
const tracker = new OperationTracker(
Network.TESTNET,
// Optional: Provide custom sequencer endpoints
// { customLiteSequencerEndpoints: ["https://custom-endpoint.com"] }
);
Getting the Operation ID
The first step in tracking a transaction is to get its operation ID using the TransactionLinker object that was returned when sending the transaction:
async function getOperationId(transactionLinker) {
const operationId = await tracker.getOperationId(transactionLinker);
if (!operationId) {
console.log("Transaction not yet picked up by sequencers, retrying...");
// Implement retry logic here
return null;
}
console.log("Operation ID:", operationId);
return operationId;
}
An empty response when calling getOperationId
means the sequencers haven’t yet received or processed your transaction. You should implement a retry mechanism with appropriate delay.
Checking Transaction Status
Once you have the operation ID, you can check the transaction’s status:
async function checkTransactionStatus(operationId) {
const status = await tracker.getOperationStatus(operationId);
console.log("Current stage:", status.stage);
console.log("Success:", status.success);
console.log("Timestamp:", new Date(status.timestamp * 1000).toISOString());
if (status.transactions) {
console.log("Transactions:", status.transactions);
}
if (status.note) {
console.log("Notes:", status.note);
}
return status;
}
Understanding Status Stages
The transaction can be in one of these stages, represented by the StageName enum:
Stage | Description |
---|
COLLECTED_IN_TAC | The sequencer has collected events for a sharded message |
INCLUDED_IN_TAC_CONSENSUS | The EVM message has been added to the Merkle tree |
EXECUTED_IN_TAC | The message has been executed on the EVM side |
COLLECTED_IN_TON | A return message event has been generated for TON execution |
INCLUDED_IN_TON_CONSENSUS | The TON message has been added to the Merkle tree |
EXECUTED_IN_TON | The message has been executed on the TON side (terminal state) |
Getting Simplified Status
For a simpler status representation, use the getSimplifiedOperationStatus
method:
async function getSimplifiedStatus(transactionLinker) {
const status = await tracker.getSimplifiedOperationStatus(transactionLinker);
switch (status) {
case 'PENDING':
console.log("Transaction is in progress");
break;
case 'SUCCESSFUL':
console.log("Transaction completed successfully");
break;
case 'FAILED':
console.log("Transaction failed");
break;
case 'OPERATION_ID_NOT_FOUND':
console.log("Operation ID not found");
break;
}
return status;
}
Getting Operation Type
You can also get the type of operation, which provides more context about the transaction flow:
async function getOperationType(operationId) {
const type = await tracker.getOperationType(operationId);
switch (type) {
case 'PENDING':
console.log("Transaction is still in progress");
break;
case 'TON_TAC_TON':
console.log("Full round-trip: TON → TAC → TON");
break;
case 'ROLLBACK':
console.log("Transaction failed, assets rolled back");
break;
case 'TON_TAC':
console.log("One-way: TON → TAC");
break;
case 'TAC_TON':
console.log("One-way: TAC → TON");
break;
case 'UNKNOWN':
console.log("Unknown operation type");
break;
}
return type;
}
Automatic Transaction Tracking
For a more convenient approach, you can use the startTracking
function to automatically track a transaction until completion:
import { startTracking, Network } from '@tonappchain/sdk';
async function trackTransaction(transactionLinker) {
try {
// Track until completion with default options
await startTracking(transactionLinker, Network.TESTNET);
// Or with custom options
/*
await startTracking(transactionLinker, Network.TESTNET, {
customLiteSequencerEndpoints: ["https://your-endpoint.com"],
delay: 5, // Check every 5 seconds
maxIterationCount: 60, // Try up to 60 times
returnValue: true, // Return execution stages data
tableView: true // Display data in table format
});
*/
console.log("Transaction tracking completed");
} catch (error) {
console.error("Tracking error:", error);
}
}
Advanced Tracking Features
You can get performance data for a transaction to understand how long each stage took:
async function getPerformanceData(operationId) {
const stages = await tracker.getStageProfiling(operationId);
console.log("Operation type:", stages.operationType);
// Check each stage
for (const stageName of Object.keys(stages)) {
if (stageName === 'operationType') continue;
const stage = stages[stageName];
if (stage.exists) {
console.log(`Stage ${stageName}:`);
console.log(` Success: ${stage.stageData.success}`);
console.log(` Time: ${new Date(stage.stageData.timestamp * 1000).toISOString()}`);
} else {
console.log(`Stage ${stageName}: Not reached`);
}
}
return stages;
}
Multiple Operations Tracking
If you need to track multiple operations at once:
async function trackMultipleOperations(operationIds) {
const statuses = await tracker.getOperationStatuses(operationIds);
for (const [id, status] of Object.entries(statuses)) {
console.log(`Operation ${id}: Stage ${status.stage}, Success: ${status.success}`);
}
return statuses;
}
Implementing a Complete Tracking System
Here’s a complete example of a tracking system with retries:
import { OperationTracker, Network } from '@tonappchain/sdk';
class TransactionMonitor {
private tracker: OperationTracker;
private pollingInterval: number;
private maxAttempts: number;
constructor(network: Network, pollingInterval = 5000, maxAttempts = 60) {
this.tracker = new OperationTracker(network);
this.pollingInterval = pollingInterval;
this.maxAttempts = maxAttempts;
}
async monitorTransaction(transactionLinker) {
// Step 1: Get operation ID with retries
const operationId = await this.getOperationIdWithRetry(transactionLinker);
if (!operationId) {
throw new Error("Failed to get operation ID after maximum attempts");
}
// Step 2: Poll for status until completion
return this.pollForCompletion(operationId);
}
private async getOperationIdWithRetry(transactionLinker, attempts = 0) {
if (attempts >= this.maxAttempts) {
return null;
}
const operationId = await this.tracker.getOperationId(transactionLinker);
if (operationId) {
return operationId;
}
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, this.pollingInterval));
return this.getOperationIdWithRetry(transactionLinker, attempts + 1);
}
private async pollForCompletion(operationId, attempts = 0) {
if (attempts >= this.maxAttempts) {
throw new Error("Transaction did not complete within the maximum number of attempts");
}
const status = await this.tracker.getOperationStatus(operationId);
// Check if transaction is complete
if (status.stage === 'EXECUTED_IN_TON') {
return {
success: status.success,
operationId,
completedAt: new Date(status.timestamp * 1000)
};
}
// Check for failure
if (!status.success) {
return {
success: false,
operationId,
failedAt: new Date(status.timestamp * 1000),
stage: status.stage,
note: status.note
};
}
// Wait before checking again
await new Promise(resolve => setTimeout(resolve, this.pollingInterval));
return this.pollForCompletion(operationId, attempts + 1);
}
}
// Usage
const monitor = new TransactionMonitor(Network.TESTNET);
monitor.monitorTransaction(transactionLinker)
.then(result => console.log("Transaction completed:", result))
.catch(error => console.error("Monitoring failed:", error));