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,
};
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
];
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: