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:

StageDescription
COLLECTED_IN_TACThe sequencer has collected events for a sharded message
INCLUDED_IN_TAC_CONSENSUSThe EVM message has been added to the Merkle tree
EXECUTED_IN_TACThe message has been executed on the EVM side
COLLECTED_IN_TONA return message event has been generated for TON execution
INCLUDED_IN_TON_CONSENSUSThe TON message has been added to the Merkle tree
EXECUTED_IN_TONThe 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

Getting Performance Data

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));