Get Started
Smart Contracts
TAC SDK (Frontend)
Partner Integrations
- Oracles
- RPC
- Indexers
Simulation & Testing
The TAC SDK provides powerful tools for simulating and testing cross-chain transactions before sending them to the network. This helps developers verify behavior, estimate gas costs, and identify potential issues without spending real tokens.
Why Simulate Transactions?
Simulating transactions before execution offers several benefits:
- Gas Estimation: Determine how much gas your transaction will require on the EVM side
- Error Detection: Identify issues with contract interactions before spending real tokens
- Return Value Prediction: See what values a contract method would return
- Parameter Validation: Verify that your method parameters are correctly encoded
Using the Simulation API
The TAC SDK provides a simulateTACMessage method for simulating cross-chain operations:
import { TacSdk, Network } from '@tonappchain/sdk';
async function simulateTransaction() {
const tacSdk = await TacSdk.create({
network: Network.TESTNET
});
// Define the simulation parameters
const simulationParams = {
tacCallParams: {
target: "0xTargetContractAddress",
methodName: "myMethod(bytes,bytes)",
arguments: "0x..." // Encoded method parameters
},
extraData: "0x", // Extra data (usually empty)
feeAssetAddress: "", // Empty for native TON
shardsKey: 123456, // Unique identifier
tonAssets: [
{
tokenAddress: "EQTokenAddress...",
amount: "10500000000" // Raw amount as string
}
],
tonCaller: "userTonWalletAddress"
};
// Run the simulation
const simulationResults = await tacSdk.simulateTACMessage(simulationParams);
// Process the results
if (simulationResults.simulationStatus) {
console.log("Simulation successful!");
console.log("Estimated gas:", simulationResults.estimatedGas.toString());
console.log("Fee parameters:", simulationResults.feeParams);
// Check for output messages
if (simulationResults.outMessages && simulationResults.outMessages.length > 0) {
console.log("Output messages:", simulationResults.outMessages);
}
} else {
console.error("Simulation failed:", simulationResults.simulationError);
}
return simulationResults;
}
Simulation Results Structure
The simulateTACMessage
method returns a comprehensive object with details about the simulated transaction:
type TACSimulationResults = {
// Gas and fee information
estimatedGas: bigint;
estimatedJettonFeeAmount: string;
feeParams: {
currentBaseFee: string;
isEip1559: boolean;
suggestedGasPrice: string;
suggestedGasTip: string;
};
// Message details
message: string;
outMessages: {
callerAddress: string;
operationId: string;
payload: string;
queryId: number;
targetAddress: string;
tokensBurned: {
amount: string;
tokenAddress: string;
}[];
tokensLocked: {
amount: string;
tokenAddress: string;
}[];
}[] | null;
// Simulation status
simulationError: string;
simulationStatus: boolean;
// Debug information
debugInfo: {
from: string;
to: string;
callData: string;
blockNumber: number;
};
};
Practical Simulation Examples
Simulating a Token Swap
import { TacSdk, Network } from '@tonappchain/sdk';
import { ethers } from 'ethers';
async function simulateSwap() {
const tacSdk = await TacSdk.create({
network: Network.TESTNET
});
// TON token to swap
const tonTokenAddress = "EQTokenAddress...";
// Get corresponding EVM address
const evmTokenAddress = await tacSdk.getEVMTokenAddress(tonTokenAddress);
// Destination token on EVM
const destTokenAddress = "0xDestTokenAddress...";
// Encode swap parameters
const abi = new ethers.AbiCoder();
const swapParams = abi.encode(
['tuple(uint256,uint256,address[],address,uint256)'],
[
[
ethers.parseUnits("10", 18), // amountIn
ethers.parseUnits("9.5", 18), // amountOutMin
[evmTokenAddress, destTokenAddress], // path
"0xUserEVMAddress", // recipient
Math.floor(Date.now() / 1000) + 3600 // deadline
]
]
);
// Prepare simulation request
const simulationParams = {
tacCallParams: {
target: "0xDexProxyAddress",
methodName: "swapExactTokensForTokens(bytes,bytes)",
arguments: swapParams
},
extraData: "0x",
feeAssetAddress: "",
shardsKey: Date.now(),
tonAssets: [
{
tokenAddress: tonTokenAddress,
amount: "10000000000" // 10 tokens with 9 decimals
}
],
tonCaller: "userTonWalletAddress"
};
// Run simulation
const results = await tacSdk.simulateTACMessage(simulationParams);
// Process results
if (results.simulationStatus) {
console.log("Swap simulation successful!");
console.log("Estimated gas:", results.estimatedGas.toString());
// Extract expected output amount (depends on specific DEX implementation)
if (results.outMessages && results.outMessages.length > 0) {
const expectedOutput = extractOutputAmount(results.outMessages[0]);
console.log("Expected output amount:", expectedOutput);
}
return {
success: true,
estimatedGas: results.estimatedGas,
outMessages: results.outMessages
};
} else {
console.error("Swap simulation failed:", results.simulationError);
return {
success: false,
error: results.simulationError
};
}
}
// Helper function to extract output amount from DEX response
function extractOutputAmount(outMessage) {
// Implementation depends on the specific DEX proxy contract's return format
// This is a placeholder
return "Placeholder: Depends on DEX implementation";
}
Simulating Adding Liquidity
async function simulateAddLiquidity() {
const tacSdk = await TacSdk.create({
network: Network.TESTNET
});
// Token addresses
const tonTokenAAddress = "EQTokenAAddress...";
const tonTokenBAddress = "EQTokenBAddress...";
// Get corresponding EVM addresses
const evmTokenAAddress = await tacSdk.getEVMTokenAddress(tonTokenAAddress);
const evmTokenBAddress = await tacSdk.getEVMTokenAddress(tonTokenBAddress);
// Encode parameters
const abi = new ethers.AbiCoder();
const liquidityParams = 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
"0xUserEVMAddress", // to
Math.floor(Date.now() / 1000) + 3600 // deadline
]
]
);
// Simulate for each token separately (multiple tokens require multiple simulations)
const simulationA = {
tacCallParams: {
target: "0xLiquidityProxyAddress",
methodName: "addLiquidity(bytes,bytes)",
arguments: liquidityParams
},
extraData: "0x",
feeAssetAddress: "",
shardsKey: Date.now(),
tonAssets: [
{
tokenAddress: tonTokenAAddress,
amount: "10000000000" // 10 tokens with 9 decimals
}
],
tonCaller: "userTonWalletAddress"
};
const simulationB = {
// Same parameters but different token
...simulationA,
tonAssets: [
{
tokenAddress: tonTokenBAddress,
amount: "20000000000" // 20 tokens with 9 decimals
}
]
};
// Run both simulations
const [resultsA, resultsB] = await Promise.all([
tacSdk.simulateTACMessage(simulationA),
tacSdk.simulateTACMessage(simulationB)
]);
// Process results
return {
tokenA: {
success: resultsA.simulationStatus,
estimatedGas: resultsA.estimatedGas,
error: resultsA.simulationStatus ? null : resultsA.simulationError
},
tokenB: {
success: resultsB.simulationStatus,
estimatedGas: resultsB.estimatedGas,
error: resultsB.simulationStatus ? null : resultsB.simulationError
},
totalEstimatedGas: resultsA.simulationStatus && resultsB.simulationStatus
? resultsA.estimatedGas + resultsB.estimatedGas
: null
};
}
Testing Strategies
Beyond simulation, implementing comprehensive testing for your hybrid dApp is crucial. Here are some testing strategies:
Unit Testing
Test individual functions and components in isolation:
// Example unit test for a token swap function
describe('swapTokens', () => {
it('should correctly encode swap parameters', () => {
const params = encodeSwapParams('0xToken1', '0xToken2', 10, 9.5);
expect(params).to.not.be.null;
// More specific assertions about the encoding
});
it('should reject invalid token addresses', () => {
expect(() => encodeSwapParams('invalid', '0xToken2', 10, 9.5))
.to.throw('Invalid token address');
});
it('should handle zero amounts correctly', () => {
expect(() => encodeSwapParams('0xToken1', '0xToken2', 0, 0))
.to.throw('Amount must be greater than zero');
});
});
Integration Testing
Test how different components work together:
describe('Token swap integration', () => {
// Mock SDK and components
let mockTacSdk;
let mockSender;
beforeEach(() => {
// Set up mocks
mockTacSdk = {
create: sinon.stub().resolves({
sendCrossChainTransaction: sinon.stub().resolves({
caller: 'mockCaller',
shardCount: 1,
shardsKey: 'mockKey',
timestamp: Date.now()
}),
simulateTACMessage: sinon.stub().resolves({
simulationStatus: true,
estimatedGas: BigInt(200000)
})
}),
getEVMTokenAddress: sinon.stub().resolves('0xMappedToken')
};
mockSender = {
address: 'mockUserAddress'
};
});
it('should create and send a swap transaction', async () => {
const result = await performSwap(
mockTacSdk,
mockSender,
'EQToken1',
'0xToken2',
10
);
expect(result).to.have.property('transactionLinker');
expect(mockTacSdk.sendCrossChainTransaction.called).to.be.true;
});
it('should handle simulation before sending transaction', async () => {
const result = await performSwapWithSimulation(
mockTacSdk,
mockSender,
'EQToken1',
'0xToken2',
10
);
expect(mockTacSdk.simulateTACMessage.called).to.be.true;
expect(result).to.have.property('transactionLinker');
});
});
End-to-End Testing
Test the complete flow from user interaction to transaction completion:
describe('End-to-end swap flow', () => {
// This would typically be done in a testing environment
// with actual testnet connections
it('should complete a token swap on testnet', async function() {
// Set longer timeout for blockchain interactions
this.timeout(60000);
// Initialize real SDK with testnet
const tacSdk = await TacSdk.create({
network: Network.TESTNET
});
// Use a test wallet with known mnemonic
const sender = await SenderFactory.getSender({
version: 'v4',
mnemonic: TEST_MNEMONIC,
network: Network.TESTNET
});
// Perform the actual swap
const transactionLinker = await performTokenSwap(
tacSdk,
sender,
TEST_TOKEN_ADDRESS,
TEST_DESTINATION_TOKEN,
0.1 // Small test amount
);
// Track the transaction
const tracker = new OperationTracker(Network.TESTNET);
// Poll for operation ID
const operationId = await pollForOperationId(tracker, transactionLinker);
expect(operationId).to.not.be.null;
// Poll for completion
const finalStatus = await pollForCompletion(tracker, operationId);
expect(finalStatus.success).to.be.true;
expect(finalStatus.stage).to.equal('EXECUTED_IN_TON');
});
});
// Helper functions
async function pollForOperationId(tracker, transactionLinker, maxAttempts = 20) {
for (let i = 0; i < maxAttempts; i++) {
const operationId = await tracker.getOperationId(transactionLinker);
if (operationId) return operationId;
await new Promise(resolve => setTimeout(resolve, 3000));
}
return null;
}
async function pollForCompletion(tracker, operationId, maxAttempts = 60) {
for (let i = 0; i < maxAttempts; i++) {
const status = await tracker.getOperationStatus(operationId);
if (status.stage === 'EXECUTED_IN_TON' || !status.success) {
return status;
}
await new Promise(resolve => setTimeout(resolve, 3000));
}
throw new Error('Transaction did not complete in time');
}
Using Test Networks
Always test your hybrid dApps on test networks before deploying to production:
- TAC Testnet: Use the Turin testnet for TAC EVM interactions
- TON Testnet: Use the TON testnet for TON interactions
Configure your SDK for testnet:
const tacSdk = await TacSdk.create({
network: Network.TESTNET
});
Was this page helpful?