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 Type | Flow | Description |
---|
TON_TAC | One-way | Simple transfer from TON to TAC |
TON_TAC_TON | Round-trip | Complete cycle: TON → TAC → TON |
ROLLBACK | Failed | Transaction failed and assets rolled back |
TAC_TON | Return | Assets 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
);
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
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);
}