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?