Skip to main content
The TAC SDK’s core functionality revolves around sendCrossChainTransaction(), which enables seamless execution of EVM contract calls directly from TON wallets. This guide covers everything from basic transactions to advanced batch operations and error handling.
Always test transactions on testnet first. Cross-chain transactions are irreversible once confirmed, and incorrect parameters can result in loss of funds.

Basic Cross-Chain Transaction

The fundamental pattern for sending cross-chain transactions involves three components: an EVM target message, a sender, and optional assets to bridge.
import { TacSdk, SenderFactory, Network, AssetType } from "@tonappchain/sdk";

// Initialize SDK and sender
const tacSdk = await TacSdk.create({ network: Network.TESTNET });
const sender = await SenderFactory.getSender({ tonConnect: tonConnectUI });

// Define EVM operation
const evmProxyMsg = {
  evmTargetAddress: "0x742d35Cc647332...", // Target contract address
  methodName: "transfer(bytes,bytes)", // Contract method signature
  encodedParameters: "0x000000000000000000...", // ABI-encoded parameters
};

// Define assets to bridge (optional)
const assets = [
  {
    amount: 1.0,
  }, // Bridge 1 TON
];

// Execute cross-chain transaction
const transactionLinker = await tacSdk.sendCrossChainTransaction(
  evmProxyMsg,
  sender,
  assets
);

console.log("Transaction ID:", transactionLinker.operationId);

const waitOptions: WaitOptions = {
  timeout: 600000, // 10 minutes timeout
  maxAttempts: 60, // Up to 60 polling attempts
  delay: 10000, // 10 seconds between status checks
  successCheck: (result) => result.success === true,
  onSuccess: (result) =>
    console.log("Transaction completed successfully!", result),
};

// Execute cross-chain transaction with automatic waiting for completion
const transactionLinkerWithWait = await tacSdk.sendCrossChainTransaction(
  evmProxyMsg,
  sender,
  assets,
  waitOptions
);

console.log("Transaction completed:", transactionLinkerWithWait.operationId);

EVM Proxy Message Structure

The EvmProxyMsg object defines what contract call to execute on the TAC EVM side:

Required Fields

const evmProxyMsg = {
  evmTargetAddress: "0x742d35Cc647332...",
  methodName: "methodName(bytes,bytes)",
  encodedParameters: "0x00000000000000...",
  gasLimit: 500000n,
};

Method Name Formatting

The SDK accepts flexible method name formats:
  • Full Signature
  • Simple Name
  • No Method Name
const evmProxyMsg = {
  evmTargetAddress: "0x742d35Cc647332...",
  methodName: "swapExactTokensForTokens(bytes,bytes)",
  encodedParameters: encodedSwapParams
};

Parameter Encoding

Use ethers.js or similar libraries to encode contract parameters:
Make sure the encoded parameters are valid tuples.
abi.encode(["tuple(uint256,....)"], [1, ...]);
import { ethers } from "ethers";

// For ERC20 transfer
const abi = new ethers.AbiCoder();
const transferParams = abi.encode(
  ["address", "uint256"],
  ["0xRecipientAddress...", ethers.parseEther("100")]
);

// For Uniswap V2 swap
const swapParams = abi.encode(
  ["uint256", "uint256", "address[]", "address", "uint256"],
  [
    ethers.parseEther("1"), // amountIn
    ethers.parseEther("0.95"), // amountOutMin
    [tokenA, tokenB], // path
    recipientAddress, // to
    Math.floor(Date.now() / 1000) + 1200, // deadline
  ]
);

const evmProxyMsg = {
  evmTargetAddress: uniswapRouterAddress,
  methodName: "swapExactTokensForTokens",
  encodedParameters: swapParams,
};

Asset Bridging

Assets are automatically bridged from TON to TAC EVM as part of the cross-chain transaction. The SDK supports multiple asset types and formats.

Asset Types

  • Native TON
  • Jetton Tokens
  • NFT Items
  • Multiple Assets
const assets = [
  { 
    amount: 1.5 
  } // Bridge 1.5 TON
];

Raw Asset Format

For precise control, use raw asset format:
const rawAssets = [
  {
    // Fungible Token
    address: "EQJettonMasterAddress...",
    rawAmount: 1000000000n, // Raw amount (with decimals)
  },
  {
    address: "EQNFTItemAddress...", // Direct NFT item address
  },
];

Transaction Options

Customize transaction behavior with advanced options:
const options = {
  forceSend: false, // Force send even if simulation fails
  isRoundTrip: false, // Whether to return assets to TON
  protocolFee: 10000000n, // Custom protocol fee (nanotons)
  evmValidExecutors: [
    // Trusted EVM executors
    "0xExecutor1Address...",
    "0xExecutor2Address...",
  ],
  evmExecutorFee: 50000000n, // EVM executor fee (wei)
  tvmValidExecutors: [
    // Trusted TON executors
    "EQExecutor1Address...",
    "EQExecutor2Address...",
  ],
  tvmExecutorFee: 100000000n, // TON executor fee (nanotons)
};

const transactionLinker = await tacSdk.sendCrossChainTransaction(
  evmProxyMsg,
  sender,
  assets,
  options
);
Custom executor fees and validator lists are advanced features. Use default values unless you have specific requirements for execution control.

Wait Options for Automatic Completion

Both sendCrossChainTransaction and sendCrossChainTransactions support optional waitOptions parameter that automatically waits for transaction completion instead of requiring manual status polling:
interface WaitOptions<T = unknown, TContext = unknown> {
  timeout?: number; // Timeout in milliseconds (default: 300000)
  maxAttempts?: number; // Maximum number of attempts (default: 30)
  delay?: number; // Delay between attempts in milliseconds (default: 10000)
  logger?: ILogger; // Logger instance
  context?: TContext; // Optional context object for additional parameters
  successCheck?: (result: T, context?: TContext) => boolean; // Custom success validation
  onSuccess?: (result: T, context?: TContext) => Promise<void> | void; // Success callback
}

Common Wait Option Patterns

// Basic waiting with defaults
const defaultWaitOptions = {};
const result = await tacSdk.sendCrossChainTransaction(
  evmProxyMsg,
  sender,
  assets,
  undefined, // transaction options
  defaultWaitOptions // use default wait options
);

// Custom timeout and polling interval
const customTimeoutWaitOptions = {
  timeout: 600000, // 10 minutes
  delay: 5000, // Check every 5 seconds
};
const resultWithCustomTiming = await tacSdk.sendCrossChainTransaction(
  evmProxyMsg,
  sender,
  assets,
  undefined,
  customTimeoutWaitOptions
);

// With custom success validation and callback
const validatedWaitOptions = {
  successCheck: (result) => result.success && result.confirmations >= 2,
  onSuccess: (result) => {
    console.log("Transaction confirmed with sufficient confirmations");
    // Send notification, update UI, etc.
  },
};
const resultWithValidation = await tacSdk.sendCrossChainTransaction(
  evmProxyMsg,
  sender,
  assets,
  undefined,
  validatedWaitOptions
);

// With context for additional data
const contextualWaitOptions = {
  context: { userId: "user123", transactionType: "swap" },
  onSuccess: (result, context) => {
    console.log(
      `${context.transactionType} completed for user ${context.userId}`
    );
  },
};
const resultWithContext = await tacSdk.sendCrossChainTransaction(
  evmProxyMsg,
  sender,
  assets,
  undefined,
  contextualWaitOptions
);

// Advanced example: Automatic profiling data retrieval with context
const profilingWaitOptions = {
  context: {
    operationTracker: tacSdk.operationTracker,
    enableProfiling: true,
  },
  onSuccess: async (result, context) => {
    const operationId = result;
    if (context?.enableProfiling && context.operationTracker) {
      console.log("📊 Retrieving profiling data...");

      // Get detailed profiling information
      const profilingData = await context.operationTracker.getStageProfiling(
        operationId
      );

      console.log("📈 OPERATION PROFILING COMPLETE");
      console.log(`🔹 Operation ID: ${operationId}`);
      console.log(`🔹 Profiling stages: ${Object.keys(profilingData).length}`);

      // Show stage completion summary
      for (const [stageName, stageInfo] of Object.entries(profilingData)) {
        if (stageName !== "operationType" && stageName !== "metaInfo") {
          console.log(
            `   • ${stageName}: ${
              stageInfo.exists ? "✅ Completed" : "⏸️ Not executed"
            }`
          );
        }
      }

      return profilingData;
    }
  },
};
const resultWithProfiling = await tacSdk.sendCrossChainTransaction(
  evmProxyMsg,
  sender,
  assets,
  undefined,
  profilingWaitOptions
);
Using waitOptions eliminates the need for manual polling but will block the calling thread until completion or timeout. For applications that need non-blocking behavior, use the standard approach without waitOptions and implement manual tracking.
Advanced Usage: You can access the SDK’s internal OperationTracker via tacSdk.operationTracker and pass it through the context parameter to avoid creating new tracker instances in your callbacks. This is especially useful for profiling and advanced monitoring scenarios.

Batch Transactions

Send multiple cross-chain transactions simultaneously:
const batchTransactions = async () => {
  // Define multiple transactions
  const transactions = [
    {
      evmProxyMsg: {
        evmTargetAddress: "0xContract1...",
        methodName: "method1(bytes,bytes)",
        encodedParameters: "0x...",
      },
      assets: [{ amount: 1.0 }],
    },
    {
      evmProxyMsg: {
        evmTargetAddress: "0xContract2...",
        methodName: "method2(bytes,bytes)",
        encodedParameters: "0x...",
      },
      assets: [{ address: "EQJetton...", amount: 100 }],
    },
  ];

  // Convert to batch format
  const crosschainTxs = transactions.map((tx) => ({
    evmProxyMsg: tx.evmProxyMsg,
    assets: tx.assets || [],
  }));

  // Send batch
  const transactionLinkers = await tacSdk.sendCrossChainTransactions(
    sender,
    crosschainTxs
  );

  // Send batch with wait options for automatic completion waiting
  const transactionLinkersWithWait = await tacSdk.sendCrossChainTransactions(
    sender,
    crosschainTxs,
    {
      timeout: 900000, // 15 minutes for batch operations
      maxAttempts: 90, // More attempts for batch
      delay: 10000, // 10 seconds between checks
      successCheck: (results) => results.every((r) => r.success === true),
      onSuccess: (results) =>
        console.log(`Batch of ${results.length} transactions completed`),
    }
  );

  return transactionLinkers.map((linker) => linker.operationId);
};

Transaction Simulation

Simulate transactions before execution to estimate fees and validate parameters:
const simulateTransaction = async () => {
  try {
    const simulation = await tacSdk.getSimulationInfo(
      evmProxyMsg,
      sender,
      assets
    );

    console.log("Simulation Results:");
    console.log("Success:", simulation.success);
    console.log("Estimated Gas:", simulation.estimatedGas);
    console.log("Protocol Fee:", simulation.protocolFee);
    console.log("EVM Executor Fee:", simulation.evmExecutorFee);
    console.log("TON Executor Fee:", simulation.tvmExecutorFee);

    if (simulation.success) {
      // Proceed with actual transaction
      const result = await tacSdk.sendCrossChainTransaction(
        evmProxyMsg,
        sender,
        assets
      );
      return result;
    } else {
      console.error("Simulation failed:", simulation.error);
      return null;
    }
  } catch (error) {
    console.error("Simulation error:", error);
    return null;
  }
};

Simulator Component

The SDK uses an internal Simulator component for transaction simulation and fee estimation. The simulator is automatically created when you initialize the SDK:
// Simulator is created automatically with SDK
const sdk = await TacSdk.create({ network: Network.TESTNET });

// SDK simulation methods (recommended)
const simulationResult = await sdk.getSimulationInfo(
  evmProxyMsg,
  sender,
  assets,
  options
);

// Batch simulation
const transactions = [
  { evmProxyMsg: msg1, assets: [asset1] },
  { evmProxyMsg: msg2, assets: [asset2] },
];
const batchResults = await sdk.simulateTransactions(sender, transactions);
The Simulator performs TAC-side simulation to estimate gas costs, validate transaction logic, and calculate required fees. It’s automatically configured when you create the SDK instance.

Error Handling

Implement comprehensive error handling for transaction failures:
const sendTransactionWithErrorHandling = async (
  evmProxyMsg,
  sender,
  assets
) => {
  try {
    // Pre-flight validation
    if (!evmProxyMsg.evmTargetAddress) {
      throw new Error("EVM target address is required");
    }

    if (!assets || assets.length === 0) {
      console.warn("No assets specified for bridging");
    }

    // Simulate first (optional but recommended)
    const simulation = await tacSdk.getSimulationInfo(
      evmProxyMsg,
      sender,
      assets
    );

    if (!simulation.success) {
      throw new Error(`Transaction simulation failed: ${simulation.error}`);
    }

    // Execute transaction
    const result = await tacSdk.sendCrossChainTransaction(
      evmProxyMsg,
      sender,
      assets
    );

    return {
      success: true,
      operationId: result.operationId,
      transactionLinker: result,
    };
  } catch (error) {
    console.error("Transaction failed:", error);

    // Handle specific error types
    if (error.message.includes("insufficient balance")) {
      return {
        success: false,
        error: "Insufficient balance for transaction and fees",
      };
    }

    if (error.message.includes("invalid address")) {
      return {
        success: false,
        error: "Invalid contract address specified",
      };
    }

    if (error.message.includes("simulation failed")) {
      return {
        success: false,
        error: "Transaction would fail on execution",
      };
    }

    return {
      success: false,
      error: "Transaction failed: " + error.message,
    };
  }
};

// Usage
const result = await sendTransactionWithErrorHandling(
  evmProxyMsg,
  sender,
  assets
);

if (result.success) {
  console.log("Transaction sent:", result.operationId);
} else {
  console.error("Transaction failed:", result.error);
}

Transaction Lifecycle

Understanding the transaction lifecycle helps with proper error handling and user experience:

Parameter Validation

The SDK validates all parameters including addresses, amounts, and method signatures before proceeding.

Simulation (Optional)

If requested, the SDK simulates the transaction to estimate gas costs and validate execution.

Asset Preparation

Assets are prepared for bridging, including balance checks and approval transactions if needed.

Message Construction

The cross-chain message is constructed with proper encoding and sequencer routing information.

TON Transaction

Transaction is signed and submitted to the TON network via the connected wallet.

Cross-Chain Routing

The sequencer network processes the message and routes it to the TAC EVM layer.

EVM Execution

The target contract method is executed on TAC EVM with bridged assets available.

Completion

Transaction completes successfully, or rollback mechanisms are triggered on failure.

What’s Next?

With transactions executing successfully, learn to monitor their progress:
I