मुख्य सामग्री पर जाएं
इस ट्यूटोरियल में, आप सीखेंगे कि bundled transactions को simulate करने के लिए Tenderly SDK का उपयोग कैसे करें। एक bundled transaction कई transactions का एक संग्रह है जिन्हें एक इकाई के रूप में simulate किया जाता है। Bundled transactions का उपयोग अक्सर DeFi protocols और अन्य एप्लिकेशन में जटिल ऑपरेशन्स करने की प्रक्रिया को simulate करने के लिए किया जाता है जिनमें कई contract इंटरैक्शन शामिल होते हैं।
एक bundle से transactions एक ही blocks पर simulate किए जाते हैं।
हम DAI की एक विशेष राशि को WETH के लिए स्वैप करने के लिए Uniswap के उपयोग को simulate करेंगे, और simulate किए गए transactions की निष्पादन स्थिति दिखाएंगे, और उपयोग की गई कुल gas की गणना करेंगे। इसे प्राप्त करने के लिए आवश्यक transactions इस प्रकार हैं:
  1. 2 DAI के minting को simulate करें ताकि स्वैपर के पास स्वैप करने के लिए कुछ संपत्ति हो। वैकल्पिक रूप से, यदि आपके पास DAI है, तो आप इसे छोड़ सकते हैं और सिर्फ transaction 2 कर सकते हैं।
  2. UniswapV3Router को DAI का उपयोग करने के लिए Approve करें
  3. स्वैप करें। स्वैप करने के लिए UniswapV2Router.exactInputSingle को कॉल करें।
पहले transaction को सफलतापूर्वक चलाने के लिए, प्रेषक को DAI stablecoin का ward होना आवश्यक है। चूंकि हम में से अधिकांश नहीं हैं, हम देखेंगे कि Bundled Simulation के संदर्भ में “ward बनने” के लिए State Overrides का उपयोग कैसे करें।

आवश्यक शर्तें

चरण 1: Project सेट अप करें

एक नया npm project बनाएं, निर्भरताएं इंस्टॉल करें, और अपने टर्मिनल में निम्नलिखित कमांड पेस्ट करके Typescript कॉन्फ़िगर करें:
example
mkdir tenderly-sdk-simulations
cd tenderly-sdk-simulations
npm init -y
yarn add @tenderly/sdk ethers
yarn add --dev typescript ts-node @types/node  dotenv
echo '{
  "compilerOptions": {
  "module": "commonjs",
  "esModuleInterop": true,
  "target": "es6",
  "moduleResolution": "node",
  "sourceMap": true,
  "outDir": "dist"
  "moduleResolution": "node16",
  },
  "lib": ["es2015"]
}' > tsconfig.json

mkdir src
touch src/swap.ts
code .

चरण 2: Simulation API को कॉल करें

नीचे दिए गए पूर्ण चलाने योग्य उदाहरण को कॉपी करें और इसे swap.ts फ़ाइल में पेस्ट करें। Transactions को functions mint2DaiTx, approveUniswapV3RouterTx, और swapSomeDaiForWethTx द्वारा निर्धारित किया जाता है:
example.ts

const fakeWardAddressEOA = '0xe2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2';
const daiOwnerEOA = '0xe58b9ee93700a616b50509c8292977fa7a0f8ce1';
const daiAddressMainnet = '0x6b175474e89094c44da98b954eedeac495271d0f';
const uniswapV3SwapRouterAddressMainnet = '0xe592427a0aece92de3edee1f18e0157c05861564';
const wethAddressMainnet = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';

(async () => {
  const tenderly = new Tenderly({
    accessKey: 'access key',
    accountName: 'your account name',
    projectName: 'project-name-hyphenated',
    network: Network.MAINNET,
  });

  const simulatedBundle = await tenderly.simulator.simulateBundle({
    blockNumber: 0x103a957,
    transactions: [
      // TX1: Mint 2 DAI for daiOwnerEOA.
      // For minting to happen, we must do a state override so fakeWardAddress EOA is considered a ward for this simulation (see overrides)
      mint2DaiTx(),
      // TX2: daiOwnerEOA approves 1 DAI to uniswapV3SwapRouterAddressMainnet
      approveUniswapV3RouterTx(),
      // TX3: Perform a uniswap swap of 1/3 ETH
      swapSomeDaiForWethTx(),
    ],
    overrides: {
      [daiAddressMainnet]: {
        state: {
          // make DAI think that fakeWardAddress is a ward for minting
          [`wards[${fakeWardAddressEOA}]`]:
            '0x0000000000000000000000000000000000000000000000000000000000000001',
        },
      },
    },
  });
  const totalGasUsed = simulatedBundle
    .map(simulation => simulation.gasUsed)
    .reduce((total, gasUsed) => total + gasUsed);

  console.log('Total gas used:', totalGasUsed);

  simulatedBundle.forEach((simulation, idx) => {
    console.log(
      `Transaction ${idx} at block ${simulation.blockNumber}`,
      simulation.status ? 'success' : 'failed',
    );
  });

  console.log(JSON.stringify(simulatedBundle, null, 2));
})();

function mint2DaiTx(): TransactionParameters {
  return {
    from: fakeWardAddressEOA,
    to: daiAddressMainnet,
    gas: 0,
    gas_price: '0',
    value: 0,
    input: daiEthersInterface().encodeFunctionData('mint', [daiOwnerEOA, parseEther('2')]),
  };
}

function approveUniswapV3RouterTx(): TransactionParameters {
  return {
    from: daiOwnerEOA,
    to: daiAddressMainnet,
    gas: 0,
    gas_price: '0',
    value: 0,
    input: daiEthersInterface().encodeFunctionData('approve', [
      uniswapV3SwapRouterAddressMainnet,
      parseEther('1'),
    ]),
  };
}

function swapSomeDaiForWethTx(): TransactionParameters {
  return {
    from: daiOwnerEOA,
    to: uniswapV3SwapRouterAddressMainnet,
    gas: 0,
    gas_price: '0',
    value: 0,
    input: uniswapRouterV2EthersInterface().encodeFunctionData('exactInputSingle', [
      {
        tokenIn: daiAddressMainnet,
        tokenOut: wethAddressMainnet,
        fee: '10000',
        recipient: daiOwnerEOA,
        deadline: (1681109951 + 10 * 365 * 24 * 60 * 60 * 1000).toString(),
        amountIn: '33000000000000000',
        amountOutMinimum: '763124874493',
        sqrtPriceLimitX96: '0',
      },
    ]),
  };
}

function daiEthersInterface() {
  // @formatter:off
  const daiAbi=[
    {constant:false,inputs:[{internalType:'address',name:'src',type:'address',},{internalType:'address',name:'dst',type:'address',},{internalType:'uint256',name:'wad',type:'uint256',},],name:'transferFrom',outputs:[{internalType:'bool',name:'',type:'bool',},],payable:false,stateMutability:'nonpayable',type:'function',},
    {constant:false,inputs:[{internalType:'address',name:'usr',type:'address',},{internalType:'uint256',name:'wad',type:'uint256',},],name:'approve',outputs:[{internalType:'bool',name:'',type:'bool',},],payable:false,stateMutability:'nonpayable',type:'function',},
    {constant:false,inputs:[{internalType:'address',name:'usr',type:'address',},{internalType:'uint256',name:'wad',type:'uint256',},],name:'mint',outputs:[],payable:false,stateMutability:'nonpayable',type:'function',}
  ];

  return new Interface(daiAbi);
  // @formatter:on
}

function uniswapRouterV2EthersInterface() {
  // @formatter:off
  const swapRouterAbi = [
    {inputs: [ {internalType: 'address', name: '_factory', type: 'address',}, {internalType: 'address', name: '_WETH9', type: 'address',}, ], stateMutability: 'nonpayable', type: 'constructor',},
    {inputs: [], name: 'WETH9', outputs: [ {internalType: 'address', name: '', type: 'address',}, ], stateMutability: 'view', type: 'function',},
    {inputs: [ {components: [ {internalType: 'bytes', name: 'path', type: 'bytes',}, {internalType: 'address', name: 'recipient', type: 'address',}, {internalType: 'uint256', name: 'deadline', type: 'uint256',}, {internalType: 'uint256', name: 'amountIn', type: 'uint256',}, {internalType: 'uint256', name: 'amountOutMinimum', type: 'uint256',}, ], internalType: 'struct ISwapRouter.ExactInputParams', name: 'params', type: 'tuple',}, ], name: 'exactInput', outputs: [ {internalType: 'uint256', name: 'amountOut', type: 'uint256',}, ], stateMutability: 'payable', type: 'function',},
    {inputs: [ {components: [ {internalType: 'address', name: 'tokenIn', type: 'address',}, {internalType: 'address', name: 'tokenOut', type: 'address',}, {internalType: 'uint24', name: 'fee', type: 'uint24',}, {internalType: 'address', name: 'recipient', type: 'address',}, {internalType: 'uint256', name: 'deadline', type: 'uint256',}, {internalType: 'uint256', name: 'amountIn', type: 'uint256',}, {internalType: 'uint256', name: 'amountOutMinimum', type: 'uint256',}, {internalType: 'uint160', name: 'sqrtPriceLimitX96', type: 'uint160',}, ], internalType: 'struct ISwapRouter.ExactInputSingleParams', name: 'params', type: 'tuple',}, ], name: 'exactInputSingle', outputs: [ {internalType: 'uint256', name: 'amountOut', type: 'uint256',}, ], stateMutability: 'payable', type: 'function',},
    {inputs: [ {components: [ {internalType: 'bytes', name: 'path', type: 'bytes',}, {internalType: 'address', name: 'recipient', type: 'address',}, {internalType: 'uint256', name: 'deadline', type: 'uint256',}, {internalType: 'uint256', name: 'amountOut', type: 'uint256',}, {internalType: 'uint256', name: 'amountInMaximum', type: 'uint256',}, ], internalType: 'struct ISwapRouter.ExactOutputParams', name: 'params', type: 'tuple',}, ], name: 'exactOutput', outputs: [ {internalType: 'uint256', name: 'amountIn', type: 'uint256',}, ], stateMutability: 'payable', type: 'function',},
    {inputs: [ {components: [ {internalType: 'address', name: 'tokenIn', type: 'address',}, {internalType: 'address', name: 'tokenOut', type: 'address',}, {internalType: 'uint24', name: 'fee', type: 'uint24',}, {internalType: 'address', name: 'recipient', type: 'address',}, {internalType: 'uint256', name: 'deadline', type: 'uint256',}, {internalType: 'uint256', name: 'amountOut', type: 'uint256',}, {internalType: 'uint256', name: 'amountInMaximum', type: 'uint256',}, {internalType: 'uint160', name: 'sqrtPriceLimitX96', type: 'uint160',}, ], internalType: 'struct ISwapRouter.ExactOutputSingleParams', name: 'params', type: 'tuple',}, ], name: 'exactOutputSingle', outputs: [ {internalType: 'uint256', name: 'amountIn', type: 'uint256',}, ], stateMutability: 'payable', type: 'function',},
    {inputs: [], name: 'factory', outputs: [ {internalType: 'address', name: '', type: 'address',}, ], stateMutability: 'view', type: 'function',},
    {inputs: [ {internalType: 'bytes[]', name: 'data', type: 'bytes[]',}, ], name: 'multicall', outputs: [ {internalType: 'bytes[]', name: 'results', type: 'bytes[]',}, ], stateMutability: 'payable', type: 'function',},
    {inputs: [], name: 'refundETH', outputs: [], stateMutability: 'payable', type: 'function',},
    {inputs: [ {internalType: 'address', name: 'token', type: 'address',}, {internalType: 'uint256', name: 'value', type: 'uint256',}, {internalType: 'uint256', name: 'deadline', type: 'uint256',}, {internalType: 'uint8', name: 'v', type: 'uint8',}, {internalType: 'bytes32', name: 'r', type: 'bytes32',}, {internalType: 'bytes32', name: 's', type: 'bytes32',}, ], name: 'selfPermit', outputs: [], stateMutability: 'payable', type: 'function',},
    {inputs: [ {internalType: 'address', name: 'token', type: 'address',}, {internalType: 'uint256', name: 'nonce', type: 'uint256',}, {internalType: 'uint256', name: 'expiry', type: 'uint256',}, {internalType: 'uint8', name: 'v', type: 'uint8',}, {internalType: 'bytes32', name: 'r', type: 'bytes32',}, {internalType: 'bytes32', name: 's', type: 'bytes32',}, ], name: 'selfPermitAllowed', outputs: [], stateMutability: 'payable', type: 'function',},
    {inputs: [ {internalType: 'address', name: 'token', type: 'address',}, {internalType: 'uint256', name: 'nonce', type: 'uint256',}, {internalType: 'uint256', name: 'expiry', type: 'uint256',}, {internalType: 'uint8', name: 'v', type: 'uint8',}, {internalType: 'bytes32', name: 'r', type: 'bytes32',}, {internalType: 'bytes32', name: 's', type: 'bytes32',}, ], name: 'selfPermitAllowedIfNecessary', outputs: [], stateMutability: 'payable', type: 'function',},
    {inputs: [ {internalType: 'address', name: 'token', type: 'address',}, {internalType: 'uint256', name: 'value', type: 'uint256',}, {internalType: 'uint256', name: 'deadline', type: 'uint256',}, {internalType: 'uint8', name: 'v', type: 'uint8',}, {internalType: 'bytes32', name: 'r', type: 'bytes32',}, {internalType: 'bytes32', name: 's', type: 'bytes32',}, ], name: 'selfPermitIfNecessary', outputs: [], stateMutability: 'payable', type: 'function',},
    {inputs: [ {internalType: 'address', name: 'token', type: 'address',}, {internalType: 'uint256', name: 'amountMinimum', type: 'uint256',}, {internalType: 'address', name: 'recipient', type: 'address',}, ], name: 'sweepToken', outputs: [], stateMutability: 'payable', type: 'function',},
    {inputs: [ {internalType: 'address', name: 'token', type: 'address',}, {internalType: 'uint256', name: 'amountMinimum', type: 'uint256',}, {internalType: 'address', name: 'recipient', type: 'address',}, {internalType: 'uint256', name: 'feeBips', type: 'uint256',}, {internalType: 'address', name: 'feeRecipient', type: 'address',}, ], name: 'sweepTokenWithFee', outputs: [], stateMutability: 'payable', type: 'function',},
    {inputs: [ {internalType: 'int256', name: 'amount0Delta', type: 'int256',}, {internalType: 'int256', name: 'amount1Delta', type: 'int256',}, {internalType: 'bytes', name: '_data', type: 'bytes',}, ], name: 'uniswapV3SwapCallback', outputs: [], stateMutability: 'nonpayable', type: 'function',}, {inputs: [ {internalType: 'uint256', name: 'amountMinimum', type: 'uint256',}, {internalType: 'address', name: 'recipient', type: 'address',}, ], name: 'unwrapWETH9', outputs: [], stateMutability: 'payable', type: 'function',}, {inputs: [ {internalType: 'uint256', name: 'amountMinimum', type: 'uint256',}, {internalType: 'address', name: 'recipient', type: 'address',}, {internalType: 'uint256', name: 'feeBips', type: 'uint256',}, {internalType: 'address', name: 'feeRecipient', type: 'address',}, ], name: 'unwrapWETH9WithFee', outputs: [], stateMutability: 'payable', type: 'function',}, {stateMutability: 'payable', type: 'receive',}
  ];

  return new Interface(swapRouterAbi);
  // @formatter:on
}
ट्यूटोरियल में आगे, आप इस बात के विस्तृत स्पष्टीकरण पाएंगे कि कोड क्या कर रहा है।

चरण 3: स्क्रिप्ट चलाएं

example
npx ts-node src/swap.ts

कोड को समझना

पहले, हम Tenderly SDK का एक नया इंस्टेंस बना रहे हैं:
example.ts
const tenderly = new Tenderly({
  accessKey: 'access key',
  accountName: 'your account name',
  projectName: 'project-name-hyphenated',
  network: Network.MAINNET,
});
Simulation API को कॉल करने और एक bundled simulation करने के लिए, हम Tenderly.simulator.simulateBundle() का उपयोग कर रहे हैं और निम्नलिखित फ़ील्ड्स वाला एक object पास कर रहे हैं:
  • blockNumber - वह block number जिस पर आप simulation को निष्पादित करना चाहते हैं।
  • transactions - तीन transactions रखने वाली एक array: 2 DAI का minting, Uniswap Router की approving, और WETH के लिए DAI की swapping।
  • overrides - एक object जो simulation के भीतर fakeWardAddress को DAI का ward बनाता है।
example.ts
const simulatedBundle = await tenderly.simulator.simulateBundle({
  blockNumber: 0x103a957,
  transactions: [
    // TX1: Mint 2 DAI for daiOwnerEOA.
    // For minting to happen, we must do a state override so fakeWardAddressEOA is considered a ward for this simulation (see overrides below)
    mint2DaiTx(),
    // TX2: daiOwnerEOA approves 1 DAI to uniswapV3SwapRouterAddressMainnet
    approveUniswapV3RouterTx(),
    // TX3: Perform a uniswap swap of 1/3 ETH
    swapSomeDaiForWethTx(),
  ],
  overrides: {
    [daiAddressMainnet]: {
      state: {
        // make DAI think that fakeWardAddress is a ward for minting
        [`wards[${fakeWardAddressEOA}]`]:
          '0x0000000000000000000000000000000000000000000000000000000000000001',
      },
    },
  },
});

State Overrides को समझना

हम DAI stablecoin के mint function को कॉल कर रहे हैं। simulation के दौरान, DAI contract को यह विश्वास करना होगा कि आप (प्रेषक) wards में से एक हैं। अन्यथा, यह mint करने से इंकार कर देगा। fakeWardAddressEOA द्वारा शुरू किए गए एक सफल minting के लिए DAI contract के state के लिए हमें निम्नलिखित शर्त को बनाए रखना होगा:
example.ts
daiAddressMainnet.state.wards[fakeWardAddressEOA] == 0x0000...0001
इसे सक्षम करने के लिए, हम SDK को overrides पास कर रहे हैं, जहां हम DAI wards storage variable को override कर रहे हैं, ताकि simulation के दौरान fakeDaiWardAddress एक ward बन जाए।
example.ts
overrides: {
  [daiAddressMainnet]: {
    state: {
        // make DAI think that fakeWardAddress is a ward for minting
        [`wards[${fakeWardAddressEOA}]`]:
        '0x0000000000000000000000000000000000000000000000000000000000000001',
    },
  },
},
ध्यान दें कि overrides पूरे bundle पर लागू होते हैं और bundle के भीतर सभी simulations के लिए दृश्यमान होते हैं।
अब, आपको जटिल simulation परिदृश्यों को करने और आवश्यक परिणामों को प्रदर्शित करने के लिए Tenderly SDK का उपयोग करने की बेहतर समझ है।