This guide explains how to send cross-chain transactions from TON to TAC EVM using the TAC SDK. The core of hybrid dApp development is sending transactions through the TON Adapter to execute operations on EVM contracts.

The sendCrossChainTransaction method is the primary function for sending data and assets from TON to TAC EVM. It creates a transaction on the TON side that initiates a cross-chain operation.

Understanding Cross-Chain Transactions

A cross-chain transaction in TAC typically involves:

  • A call to an EVM contract on TAC
  • Optional asset transfers (tokens) between chains
  • Tracking the transaction’s progress across chains

Basic Transaction Structure

Here’s how to create and send a basic cross-chain transaction:

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

async function sendCrossChainTx() {
  // Initialize SDK
  const tacSdk = await TacSdk.create({
    network: Network.TESTNET
  });
  
  // 1. Prepare EVM message for the target contract
  const evmProxyMsg: EvmProxyMsg = {
    evmTargetAddress: "0xTargetContractAddress", // Target contract on TAC
    methodName: "myMethod(bytes,bytes)", // Method to call
    encodedParameters: "0x...", // Encoded method parameters
  };
  
  // 2. Use a sender (from wallet integration)
  const sender = /* your sender from wallet integration */;
  
  // 3. Optional: Specify assets to bridge
  const assets: AssetBridgingData[] = [
    {
      address: "EQTokenAddress...",
      amount: 10.5, // User-friendly amount
      decimals: 9 // Optional, will be detected automatically if not provided
    }
  ];
  
  // 4. Send the transaction
  const transactionLinker = await tacSdk.sendCrossChainTransaction(
    evmProxyMsg,
    sender,
    assets
  );
  
  // The transactionLinker can be used for tracking the transaction
  return transactionLinker;
}

Parameter Encoding for EVM Methods

When calling an EVM contract method, you need to encode parameters correctly. Use the ethers.js ABI encoder:

import { ethers } from 'ethers';

// Create an ABI encoder
const abi = new ethers.AbiCoder();

// Encode parameters for a simple method
const simpleParams = abi.encode(
  ['uint256', 'address'],
  [1000000, '0xRecipientAddress']
);

// Encode parameters for a complex method with structs
const complexParams = abi.encode(
  ['tuple(uint256,uint256,address[],address)'],
  [
    [
      1000000, // amount1
      500000,  // amount2
      ['0xToken1Address', '0xToken2Address'], // token addresses
      '0xRecipientAddress' // recipient
    ]
  ]
);

Always include the full method signature including parameter types in methodName (e.g., "swap(bytes,bytes)") or the transaction will fail. The method must accept two bytes parameters.

Asset Bridging

You can bridge assets (tokens) from TON to TAC as part of your transaction:

// Bridge a token
const assets: AssetBridgingData[] = [
  {
    address: "EQTokenAddress...", // TON token address
    amount: 10.5 // User-friendly amount
  }
];

// Bridge native TON
const bridgeTON: AssetBridgingData[] = [
  {
    // No address means native TON
    amount: 1.5 // TON amount
  }
];

// Bridge multiple tokens
const multipleAssets: AssetBridgingData[] = [
  {
    address: "EQToken1Address...",
    amount: 10.5
  },
  {
    address: "EQToken2Address...",
    amount: 20.25
  }
];

You can also use raw amounts if you prefer:

const assetsWithRawAmount: AssetBridgingData = {
  address: "EQTokenAddress...",
  rawAmount: BigInt(10500000000) // Raw amount with decimals included
};

Cross-Chain Transaction Examples

Example 1: Simple Contract Call (No Asset Transfer)

// Call a contract method without sending assets
const callOnly = await tacSdk.sendCrossChainTransaction(
  {
    evmTargetAddress: "0xTargetContract",
    methodName: "getData(bytes,bytes)",
    encodedParameters: "0x" // Empty parameters
  },
  sender,
  [] // No assets to bridge
);

Example 2: Token Swap on DEX

// Encode parameters for a swap
const swapParams = abi.encode(
  ['tuple(uint256,uint256,address[],address,uint256)'],
  [
    [
      ethers.parseUnits("10", 18), // amountIn
      ethers.parseUnits("9.5", 18), // amountOutMin
      [evmTokenInAddress, evmTokenOutAddress], // path
      userAddress, // to
      Math.floor(Date.now() / 1000) + 3600 // deadline (1 hour)
    ]
  ]
);

// Send transaction
const swapTx = await tacSdk.sendCrossChainTransaction(
  {
    evmTargetAddress: "0xDexProxyAddress",
    methodName: "swapExactTokensForTokens(bytes,bytes)",
    encodedParameters: swapParams
  },
  sender,
  [
    {
      address: tvmTokenAddress,
      amount: 10 // Amount to swap
    }
  ]
);

Example 3: Adding Liquidity to a Pool

// Encode parameters for adding liquidity
const addLiquidityParams = abi.encode(
  ['tuple(address,address,uint256,uint256,uint256,uint256,address,uint256)'],
  [
    [
      evmTokenAAddress,
      evmTokenBAddress,
      ethers.parseUnits("10", 18), // amountA
      ethers.parseUnits("20", 18), // amountB
      ethers.parseUnits("9.5", 18), // amountAMin
      ethers.parseUnits("19", 18), // amountBMin
      userAddress, // to
      Math.floor(Date.now() / 1000) + 3600 // deadline
    ]
  ]
);

// Send transaction with multiple assets
const addLiquidityTx = await tacSdk.sendCrossChainTransaction(
  {
    evmTargetAddress: "0xLiquidityProxyAddress",
    methodName: "addLiquidity(bytes,bytes)",
    encodedParameters: addLiquidityParams
  },
  sender,
  [
    {
      address: tvmTokenAAddress,
      amount: 10
    },
    {
      address: tvmTokenBAddress,
      amount: 20
    }
  ]
);

Transaction Simulation

Before sending a transaction, you can simulate it to estimate gas costs and check for potential issues:

// Prepare simulation request parameters
const simulationParams = {
  tacCallParams: {
    target: "0xTargetAddress",
    methodName: "myMethod(bytes,bytes)",
    arguments: encodedParameters
  },
  extraData: "0x", // Extra data
  feeAssetAddress: "", // Empty for native TON
  shardsKey: 123456, // Unique identifier
  tonAssets: [
    {
      tokenAddress: "EQTokenAddress...",
      amount: "10500000000" // Raw amount as string
    }
  ],
  tonCaller: "userTonWalletAddress"
};

// Simulate the transaction
const simulationResults = await tacSdk.simulateTACMessage(simulationParams);

// Check simulation results
if (simulationResults.simulationStatus) {
  console.log("Simulation successful!");
  console.log("Estimated gas:", simulationResults.estimatedGas.toString());
} else {
  console.error("Simulation failed:", simulationResults.simulationError);
}

Error Handling

When sending transactions, several types of errors can occur:

try {
  const tx = await tacSdk.sendCrossChainTransaction(evmProxyMsg, sender, assets);
  // Transaction sent successfully
} catch (error) {
  if (error.name === 'ContractError') {
    console.error("Contract error:", error.message);
    // Handle contract not deployed on TVM side
  } else if (error.name === 'AddressError') {
    console.error("Address error:", error.message);
    // Handle invalid token address
  } else {
    console.error("Transaction error:", error);
    // Handle other errors
  }
}