This guide provides a deeper dive into cross-chain operations with the TAC SDK, covering advanced scenarios and best practices for building robust hybrid dApps.

Cross-chain operations are the core of TAC’s functionality, allowing developers to create seamless interactions between TON and EVM environments. Understanding how these operations work “under the hood” will help you build more efficient and reliable applications.

Understanding the Cross-Chain Flow

When a cross-chain operation is initiated, it follows this general flow:

  • Initiation on TON: Transaction begins on TON, with data and/or assets
  • Sequencer Processing: TON Adapter’s sequencer network validates and processes the message
  • Consensus Formation: Sequencers form Merkle trees and reach consensus
  • EVM Execution: The validated message is executed on the TAC EVM side
  • Return Path (Optional): Results or assets may be returned to TON

Sharded Messages

Due to TON’s architecture, it’s not possible to send multiple tokens in a single transaction. TAC solves this with a sharded messaging system.

How Sharded Messages Work

When you need to send multiple tokens (like when adding liquidity to a DEX), each token is sent as a separate transaction on TON. The TAC SDK and sequencer network handle linking these transactions together:

// Example: Adding liquidity with two tokens
const evmProxyMsg = {
  evmTargetAddress: "0xPoolContract",
  methodName: "addLiquidity(bytes,bytes)",
  encodedParameters: liquidityParams
};

// Each token will be sent as a separate transaction on TON
const assets = [
  {
    address: "EQTokenAAddress...",
    amount: 10
  },
  {
    address: "EQTokenBAddress...",
    amount: 20
  }
];

// The SDK handles creating separate TON transactions that are linked together
const transactionLinker = await tacSdk.sendCrossChainTransaction(
  evmProxyMsg,
  sender,
  assets
);

Transaction Linking

These separate transactions are linked using a triplet of identifiers:

  • caller: The address initiating the transaction
  • shardsKey: A unique identifier for the set of sharded messages
  • shardCount: The total number of shards in the transaction

The TransactionLinker object returned by sendCrossChainTransaction contains this information and is used for tracking the status of the entire operation.

TAC Headers and Proxy Contracts

Every cross-chain message includes a TAC header, which contains essential metadata:

// TAC header structure (sent automatically by the protocol)
type TacHeaderV1 = {
  shardsKey: uint64;        // ID for linking sharded messages
  timestamp: uint256;       // Block timestamp from TON
  operationId: bytes32;     // Unique ID for the operation
  tvmCaller: string;        // TON user's wallet address
  extraData: bytes;         // Additional data (if any)
};

Proxy Contract Interface

The target contract on the EVM side must implement a specific interface to receive cross-chain messages:

// Example proxy contract function
function myProxyFunction(bytes calldata tacHeader, bytes calldata arguments) 
    external 
    onlyTacCCL 
{
    // Decode the TAC header
    TacHeaderV1 memory header = _decodeTacHeader(tacHeader);
    
    // Decode your custom arguments
    MyArgs memory args = abi.decode(arguments, (MyArgs));
    
    // Implement your logic
    // ...
}

The TAC SDK handles the encoding of your parameters into the arguments parameter. You only need to encode the data specific to your contract method.

Advanced Message Types

One-Way Messages (TON to TAC)

Simple messages that only go from TON to TAC without expecting a return:

// One-way message with token transfer
const oneWayMsg = {
  evmTargetAddress: "0xTargetContract",
  methodName: "deposit(bytes,bytes)",
  encodedParameters: depositParams
};

await tacSdk.sendCrossChainTransaction(oneWayMsg, sender, assets);

Round-Trip Messages (TON to TAC to TON)

Messages that initiate on TON, execute on TAC, and then return results to TON:

// Round-trip message (like a swap that returns tokens)
const roundTripMsg = {
  evmTargetAddress: "0xSwapContract",
  methodName: "swap(bytes,bytes)",
  encodedParameters: swapParams
};

await tacSdk.sendCrossChainTransaction(roundTripMsg, sender, assets);

The proxy contract on the EVM side determines whether a message is one-way or round-trip based on whether it creates a return message to TON.

Message Types by Operation

Different operations use different message patterns

Operation TypeFlowDescription
TON_TACOne-waySimple transfer from TON to TAC
TON_TAC_TONRound-tripComplete cycle: TON → TAC → TON
ROLLBACKFailedTransaction failed and assets rolled back
TAC_TONReturnAssets returned from TAC to TON

You can check the operation type using the tracker:

const type = await tracker.getOperationType(operationId);
console.log("Operation type:", type);

Gas Management

Cross-chain operations involve gas costs on both chains:

  • TON Gas: Paid for the TON-side transaction in TON tokens
  • TAC Gas: Paid for the EVM-side execution in TAC tokens

The SDK can automatically estimate the gas required for EVM execution:

// With automatic gas estimation (recommended)
const txWithAutoGas = await tacSdk.sendCrossChainTransaction(
  {
    evmTargetAddress: "0xTarget",
    methodName: "method(bytes,bytes)",
    encodedParameters: params
    // No gasLimit - will be estimated automatically
  },
  sender,
  assets
);

// With manual gas limit specification
const txWithManualGas = await tacSdk.sendCrossChainTransaction(
  {
    evmTargetAddress: "0xTarget",
    methodName: "method(bytes,bytes)",
    encodedParameters: params,
    gasLimit: BigInt(300000) // Manual gas limit
  },
  sender,
  assets
);

Performance Considerations

Cross-chain operations involve multiple blockchains and validation steps, which affects performance:

  • Latency: Cross-chain operations take longer than single-chain transactions
  • Message Size: There are limits to how much data can be sent in a single message
  • State Dependencies: Applications requiring rapid state updates across chains need careful design

Optimizing Performance

To optimize performance:

  • Minimize Round Trips: Design operations to require minimal back-and-forth between chains
  • Batch Operations: Group related operations when possible
  • Asynchronous UX: Design your UI to handle the asynchronous nature of cross-chain operations
  • Status Tracking: Implement robust status tracking to keep users informed

Security Considerations

Cross-chain operations introduce unique security considerations:

  • Asset Security: Ensure proper handling of assets during bridging
  • Error Recovery: Implement robust error handling and recovery mechanisms
  • Input Validation: Validate all inputs on both chains
  • Transaction Timing: Be aware of the potential for timing-related issues

Handling Failed Transactions

If a transaction fails on the EVM side, the TON Adapter automatically handles the rollback of assets to the TON side. However, your application should still provide appropriate feedback to users:

// Track for failures
const status = await tracker.getOperationStatus(operationId);

if (!status.success) {
  console.error("Transaction failed at stage:", status.stage);
  if (status.note) {
    console.error("Error details:", status.note);
  }
  
  // Inform the user and suggest recovery steps
  showUserError("Transaction failed", status);
}

Real-World Complex Example: DEX Aggregator

Here’s a more complex example showing how to create a DEX aggregator that finds the best price across multiple DEXes:

import { TacSdk, Network, EvmProxyMsg, AssetBridgingData } from '@tonappchain/sdk';
import { ethers } from 'ethers';

async function executeBestSwap() {
  const tacSdk = await TacSdk.create({ network: Network.TESTNET });
  const sender = /* your sender */;
  
  // Token addresses
  const sourceTONToken = "EQSourceToken...";
  const sourceEVMToken = await tacSdk.getEVMTokenAddress(sourceTONToken);
  const destEVMToken = "0xDestToken...";
  
  // Amount to swap
  const swapAmount = 10;
  
  // First simulate swaps on different DEXes to find best price
  const dexes = [
    { name: "DEX1", address: "0xDex1Address", proxyAddress: "0xDex1Proxy" },
    { name: "DEX2", address: "0xDex2Address", proxyAddress: "0xDex2Proxy" },
    { name: "DEX3", address: "0xDex3Address", proxyAddress: "0xDex3Proxy" }
  ];
  
  // Simulate each DEX to find best price
  const simulationResults = await Promise.all(dexes.map(async dex => {
    // Create simulation parameters for this DEX
    const simulationParams = {
      tacCallParams: {
        target: dex.proxyAddress,
        methodName: "getAmountOut(bytes,bytes)",
        arguments: abi.encode(
          ['tuple(address,address,uint256)'],
          [[sourceEVMToken, destEVMToken, ethers.parseUnits(swapAmount.toString(), 18)]]
        )
      },
      tonAssets: [],
      tonCaller: sender.address,
      feeAssetAddress: "",
      shardsKey: Date.now(),
      extraData: "0x"
    };
    
    try {
      const result = await tacSdk.simulateTACMessage(simulationParams);
      // Extract amount out from simulation result
      return {
        dex,
        amountOut: extractAmountOut(result),
        success: result.simulationStatus
      };
    } catch (error) {
      return { dex, success: false, error };
    }
  }));
  
  // Find best DEX
  const validResults = simulationResults.filter(r => r.success);
  if (validResults.length === 0) {
    throw new Error("No valid swap routes found");
  }
  
  const bestRoute = validResults.reduce((best, current) => 
    current.amountOut > best.amountOut ? current : best
  );
  
  console.log(`Best route: ${bestRoute.dex.name} with output ${bestRoute.amountOut}`);
  
  // Execute swap on best DEX
  const swapParams = abi.encode(
    ['tuple(address,address,uint256,uint256,address)'],
    [
      [
        sourceEVMToken,
        destEVMToken,
        ethers.parseUnits(swapAmount.toString(), 18),
        ethers.parseUnits((bestRoute.amountOut * 0.99).toString(), 18), // 1% slippage
        sender.address
      ]
    ]
  );
  
  const evmProxyMsg = {
    evmTargetAddress: bestRoute.dex.proxyAddress,
    methodName: "swap(bytes,bytes)",
    encodedParameters: swapParams
  };
  
  const assets = [{
    address: sourceTONToken,
    amount: swapAmount
  }];
  
  // Execute the transaction
  const transactionLinker = await tacSdk.sendCrossChainTransaction(
    evmProxyMsg,
    sender,
    assets
  );
  
  return { transactionLinker, bestRoute };
}

// Helper to extract amount out from simulation result
function extractAmountOut(result) {
  // Implementation depends on the specific DEX proxy contract's return format
  // This is a placeholder
  return parseFloat(result.outMessages[0].payload);
}