├── CODEOWNERS ├── lib ├── executors │ ├── index.ts │ ├── Simulator.types.ts │ └── Simulator.ts ├── constants.ts ├── sdkVersion.ts ├── simulator.ts ├── wallets.ts ├── contracts.ts ├── repositories │ ├── index.ts │ ├── Repository.ts │ ├── wallets │ │ ├── wallets.types.ts │ │ └── wallets.repository.ts │ └── contracts │ │ ├── contracts.types.ts │ │ └── contracts.repository.ts ├── core │ ├── index.ts │ ├── ApiClientProvider.ts │ ├── ApiClient.ts │ └── Tenderly.ts ├── errors │ ├── NotFoundError.ts │ ├── EncodingError.ts │ ├── InvalidArgumentsError.ts │ ├── InvalidResponseError.ts │ ├── UnexpectedVerificationError.ts │ ├── Error.handlerRegistry.ts │ ├── CompilationError.ts │ ├── BytecodeMismatchError.ts │ ├── ApiError.ts │ ├── Error.types.ts │ ├── GeneralError.ts │ └── index.ts ├── helpers.ts ├── index.ts └── types.ts ├── .husky ├── pre-commit └── commit-msg ├── commitlint.config.js ├── assets └── Tenderly Logo-Purple.png ├── jest.config.json ├── .editorconfig ├── .gitignore ├── .prettierrc ├── test ├── setup.js ├── simulator.test.ts ├── wallets.repository.test.ts └── contracts.repository.test.ts ├── .changeset ├── config.json └── README.md ├── prebuild.js ├── examples ├── contractVerification │ ├── withDependencies │ │ ├── contracts │ │ │ ├── MyToken.sol │ │ │ ├── IERC20Metadata.sol │ │ │ ├── Context.sol │ │ │ ├── IERC20.sol │ │ │ └── ERC20.sol │ │ └── index.ts │ ├── withLibrary │ │ ├── contracts │ │ │ ├── Library.sol │ │ │ └── LibraryToken.sol │ │ └── index.ts │ ├── README.md │ └── simpleCounter │ │ ├── contracts │ │ └── Counter.sol │ │ └── index.ts ├── addWallets │ ├── README.md │ └── index.ts ├── addContracts │ ├── README.md │ └── index.ts ├── simulateTransaction │ ├── README.md │ ├── index.ts │ └── counterContract.ts ├── simulateBundle │ ├── README.md │ └── index.ts └── allowAndTransfer │ ├── README.md │ ├── index.ts │ └── myTokenAbi.ts ├── .vscode └── launch.json ├── .env.example ├── tsconfig.json ├── .github └── workflows │ ├── build-and-test.yml │ └── npm-publish.yml ├── LICENSE.md ├── .eslintrc.json ├── package.json ├── README.md └── CHANGELOG.md /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @Tenderly/devx 2 | -------------------------------------------------------------------------------- /lib/executors/index.ts: -------------------------------------------------------------------------------- 1 | export { Simulator } from './Simulator'; 2 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const TENDERLY_API_BASE_URL = 'https://api.tenderly.co'; 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /lib/sdkVersion.ts: -------------------------------------------------------------------------------- 1 | // autogenerated by prebuild.js 2 | export const TENDERLY_SDK_VERSION = '0.1.14'; 3 | -------------------------------------------------------------------------------- /lib/simulator.ts: -------------------------------------------------------------------------------- 1 | // namespace file for simulator 2 | export * from './executors/Simulator.types'; 3 | -------------------------------------------------------------------------------- /lib/wallets.ts: -------------------------------------------------------------------------------- 1 | // namespace file for wallets 2 | export * from './repositories/wallets/wallets.types'; 3 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm commitlint --edit 5 | -------------------------------------------------------------------------------- /lib/contracts.ts: -------------------------------------------------------------------------------- 1 | // namespace file for contracts 2 | export * from './repositories/contracts/contracts.types'; 3 | -------------------------------------------------------------------------------- /assets/Tenderly Logo-Purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tenderly/tenderly-sdk/HEAD/assets/Tenderly Logo-Purple.png -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ts-jest", 3 | "testEnvironment": "node", 4 | "globalSetup": "./test/setup.js" 5 | } 6 | -------------------------------------------------------------------------------- /lib/repositories/index.ts: -------------------------------------------------------------------------------- 1 | export { ContractRepository } from './contracts/contracts.repository'; 2 | export { WalletRepository } from './wallets/wallets.repository'; 3 | -------------------------------------------------------------------------------- /lib/core/index.ts: -------------------------------------------------------------------------------- 1 | export { Tenderly } from './Tenderly'; 2 | export * from '../repositories/contracts/contracts.types'; 3 | export * from '../repositories/wallets/wallets.types'; 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_size = 2 6 | indent_style = space 7 | insert_final_newline = true 8 | max_line_length = 120 9 | tab_width = 2 10 | trim_trailing_whitespace = true 11 | 12 | [*.scss] 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /lib/errors/NotFoundError.ts: -------------------------------------------------------------------------------- 1 | import { GeneralError } from './GeneralError'; 2 | 3 | export class NotFoundError extends GeneralError { 4 | constructor(message: string) { 5 | super({ message, id: 'local_error', slug: 'resource_not_found' }); 6 | this.name = 'NotFoundError'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | 4 | coverage 5 | 6 | .vscode 7 | .env 8 | .DS_Store 9 | .idea 10 | 11 | *.output.json 12 | *.output.txt 13 | 14 | .pnp.* 15 | .yarn/* 16 | !.yarn/patches 17 | !.yarn/plugins 18 | !.yarn/releases 19 | !.yarn/sdks 20 | !.yarn/versions 21 | 22 | *.tgz 23 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "singleQuote": true, 5 | "printWidth": 100, 6 | "bracketSpacing": true, 7 | "trailingComma": "all", 8 | "arrowParens": "avoid", 9 | "quoteProps": "as-needed", 10 | "parser": "typescript", 11 | "editorconfig": true 12 | } 13 | -------------------------------------------------------------------------------- /lib/errors/EncodingError.ts: -------------------------------------------------------------------------------- 1 | import { GeneralError } from './GeneralError'; 2 | import { TenderlyError } from './Error.types'; 3 | 4 | export class EncodingError extends GeneralError { 5 | constructor(error: TenderlyError) { 6 | super(error); 7 | this.name = 'EncodingError'; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/errors/InvalidArgumentsError.ts: -------------------------------------------------------------------------------- 1 | import { GeneralError } from './GeneralError'; 2 | 3 | export class InvalidArgumentsError extends GeneralError { 4 | constructor(message: string) { 5 | super({ id: 'local_error', message, slug: 'invalid_arguments' }); 6 | this.name = 'InvalidArgumentsError'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/errors/InvalidResponseError.ts: -------------------------------------------------------------------------------- 1 | import { GeneralError } from './GeneralError'; 2 | 3 | export class InvalidResponseError extends GeneralError { 4 | constructor(message: string) { 5 | super({ id: 'local_error', message, slug: 'invalid_response' }); 6 | this.name = 'InvalidResponseError'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/helpers.ts: -------------------------------------------------------------------------------- 1 | type TenderlyEnvVariables = { 2 | TENDERLY_ACCESS_KEY: string; 3 | TENDERLY_ACCOUNT_NAME: string; 4 | TENDERLY_PROJECT_NAME: string; 5 | TENDERLY_GET_BY_PROJECT_NAME: string; 6 | }; 7 | 8 | export function getEnvironmentVariables() { 9 | return process.env as TenderlyEnvVariables; 10 | } 11 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const dotenv = require('dotenv'); 3 | 4 | module.exports = () => { 5 | try { 6 | dotenv.config(); 7 | 8 | console.log('Jest setup successful!'); 9 | } catch (error) { 10 | console.error('Jest setup failed!', error); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core'; 2 | export * from './types'; 3 | export * from './errors'; 4 | export * from './helpers'; 5 | 6 | // export helper types 7 | export * from './executors/Simulator.types'; 8 | export * from './repositories/contracts/contracts.types'; 9 | export * from './repositories/wallets/wallets.types'; 10 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "master", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /lib/errors/UnexpectedVerificationError.ts: -------------------------------------------------------------------------------- 1 | import { GeneralError } from "./GeneralError"; 2 | 3 | export class UnexpectedVerificationError extends GeneralError { 4 | constructor(message: string) { 5 | super({ message, id: 'local_error', slug: 'unexpected_verification_error'}); 6 | this.name = 'UnexpectedVerificationError'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /prebuild.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const { version } = require('./package.json'); 5 | 6 | fs.writeFileSync( 7 | path.resolve(__dirname, 'lib/sdkVersion.ts'), 8 | `// autogenerated by prebuild.js 9 | export const TENDERLY_SDK_VERSION = '${version}'; 10 | `, 11 | ); 12 | -------------------------------------------------------------------------------- /examples/contractVerification/withDependencies/contracts/MyToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract MyToken is ERC20 { 7 | constructor(string memory name, string memory symbol) ERC20(name, symbol) { 8 | _mint(msg.sender, 10000); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/errors/Error.handlerRegistry.ts: -------------------------------------------------------------------------------- 1 | import { ApiError } from './ApiError'; 2 | import { ErrorHandler } from './Error.types'; 3 | import { GeneralError } from './GeneralError'; 4 | 5 | export const errorHandlers: ErrorHandler[] = [ApiError, GeneralError]; 6 | 7 | export function errorHandler(handlerClass: ErrorHandler) { 8 | errorHandlers.push(handlerClass); 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Jest Tests", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeArgs": ["--inspect-brk", "${workspaceRoot}/node_modules/.bin/jest"], 9 | "console": "integratedTerminal", 10 | "internalConsoleOptions": "neverOpen" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # 1. remove .example from the file name 2 | # 2. generate your access key from https://dashboard.tenderly.co/account/authorization 3 | # 3. copy and paste the key instead of demo 4 | # 4. fill in account and project names 5 | 6 | TENDERLY_ACCESS_KEY="demo" 7 | TENDERLY_ACCOUNT_NAME="REPLACE_ME" 8 | TENDERLY_PROJECT_NAME="REPLACE_ME" 9 | TENDERLY_GET_BY_PROJECT_NAME="REPLACE_ME" 10 | -------------------------------------------------------------------------------- /examples/contractVerification/withLibrary/contracts/Library.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: UNLICENSED 2 | 3 | // Solidity files have to start with this pragma. 4 | // It will be used by the Solidity compiler to validate its version. 5 | pragma solidity 0.8.17; 6 | 7 | library Library { 8 | function add(uint a, uint b) public pure returns (uint) { 9 | return a + b; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/repositories/Repository.ts: -------------------------------------------------------------------------------- 1 | export interface Repository { 2 | get: (uniqueId: string) => Promise; 3 | add: (uniqueId: string, data: Partial) => Promise; 4 | remove: (uniqueId: string) => Promise; 5 | update: (uniqueId: string, data: never) => Promise; 6 | getBy: (queryObject: never) => Promise; 7 | getAll: () => Promise; 8 | } 9 | -------------------------------------------------------------------------------- /lib/errors/CompilationError.ts: -------------------------------------------------------------------------------- 1 | import { GeneralError } from "./GeneralError"; 2 | import { CompilationErrorResponse } from '../repositories/contracts/contracts.types'; 3 | 4 | export class CompilationError extends GeneralError { 5 | constructor(message: string, data?: CompilationErrorResponse[]) { 6 | super({ message, id: 'local_error', slug: 'compilation_error', data: data }); 7 | this.name = 'CompilationError'; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["lib/**/*", "test/**/*", "examples/**/*"], 3 | "exclude": ["node_modules", "coverage", "dist"], 4 | "compilerOptions": { 5 | "esModuleInterop": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "noUncheckedIndexedAccess": true, 9 | "noEmit": true, 10 | "target": "ES2016", 11 | "module": "commonjs", 12 | "moduleResolution": "node16", 13 | "strict": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/errors/BytecodeMismatchError.ts: -------------------------------------------------------------------------------- 1 | import { GeneralError } from "./GeneralError"; 2 | import { BytecodeMismatchErrorResponse } from '../repositories/contracts/contracts.types'; 3 | 4 | export class BytecodeMismatchError extends GeneralError { 5 | constructor(message: string, data?: BytecodeMismatchErrorResponse) { 6 | super({ message, id: 'local_error', slug: 'bytecode_mismatch_error', data: data }); 7 | this.name = 'BytecodeMismatchError'; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/addWallets/README.md: -------------------------------------------------------------------------------- 1 | ### **Adding a wallet** 2 | 3 | To add a new wallet, you can use the **`add`** method of the **`wallets`** namespace: 4 | 5 | ```jsx 6 | try { 7 | const walletAddress = '0x742d35Cc6634C0532925a3b844Bc454e4438f44e'; 8 | const addedWallet = await tenderly.wallets.add(walletAddress, { 9 | displayName: "My Wallet", 10 | }); 11 | 12 | console.log("Added wallet:", addedWallet); 13 | 14 | } catch(error) { 15 | console.error("Error adding wallet:", error); 16 | } 17 | 18 | ``` -------------------------------------------------------------------------------- /examples/addContracts/README.md: -------------------------------------------------------------------------------- 1 | ### **Adding a contract** 2 | 3 | To add a new contract, you can use the `add` method of the **`contracts`** namespace: 4 | 5 | ```jsx 6 | try { 7 | const contractAddress '0x742d35Cc6634C0532925a3b844Bc454e4438f44e'; 8 | const contract = await tenderly.contracts.add(contractAddress, { 9 | displayName: "MyContract" 10 | }); 11 | 12 | console.log("Added contract:", addedContract); 13 | 14 | } catch(error) { 15 | console.error("Error adding contract:", error); 16 | } 17 | ``` -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /examples/contractVerification/withLibrary/contracts/LibraryToken.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: UNLICENSED 2 | 3 | // Solidity files have to start with this pragma. 4 | // It will be used by the Solidity compiler to validate its version. 5 | pragma solidity 0.8.17; 6 | 7 | import "./Library.sol"; 8 | 9 | contract LibraryToken { 10 | uint public dummyToken = 1; 11 | 12 | constructor() { 13 | dummyToken = 2; 14 | } 15 | 16 | function add(uint a, uint b) public pure returns (uint) { 17 | return Library.add(a, b); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/errors/ApiError.ts: -------------------------------------------------------------------------------- 1 | import { GeneralError } from './GeneralError'; 2 | import { isTenderlyAxiosError, TenderlyError } from './Error.types'; 3 | 4 | export class ApiError extends GeneralError { 5 | public readonly status: number; 6 | 7 | constructor({ status, ...error }: { status: number } & TenderlyError) { 8 | super(error); 9 | this.status = status; 10 | this.name = 'ApiError'; 11 | } 12 | 13 | static handle(error: Error | unknown): void { 14 | if (isTenderlyAxiosError(error)) { 15 | throw new ApiError({ status: error.response.status, ...error.response.data.error }); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/core/ApiClientProvider.ts: -------------------------------------------------------------------------------- 1 | import { ApiClient, ApiVersion } from './ApiClient'; 2 | import { EmptyObject } from '../types'; 3 | 4 | export class ApiClientProvider { 5 | static instance: ApiClientProvider; 6 | private readonly apiKey: string; 7 | private readonly apiClients: Record | EmptyObject = {}; 8 | 9 | constructor({ apiKey }: { apiKey: string }) { 10 | this.apiKey = apiKey; 11 | } 12 | 13 | getApiClient({ version }: { version: ApiVersion }): ApiClient { 14 | if (!this.apiClients[version]) { 15 | this.apiClients[version] = new ApiClient({ version, apiKey: this.apiKey }); 16 | } 17 | return this.apiClients[version]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/errors/Error.types.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError, isAxiosError } from 'axios'; 2 | import { WithRequired } from '../types'; 3 | 4 | export interface TenderlyError { 5 | readonly id?: string; 6 | readonly message: string; 7 | readonly slug: string; 8 | } 9 | 10 | export interface ErrorHandler { 11 | handle: (error: Error | unknown) => void; 12 | } 13 | 14 | type TenderlyAxiosError = WithRequired, 'response'>; 15 | 16 | export const isTenderlyAxiosError = (error: unknown): error is TenderlyAxiosError => { 17 | return ( 18 | isAxiosError<{ error: TenderlyError }>(error) && 19 | !!(error?.response?.data?.error && error?.response?.status) 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /examples/contractVerification/README.md: -------------------------------------------------------------------------------- 1 | ### **Verify Contract** 2 | 3 | To verify a contract, you can use **`verify`** method of the **`contracts`** namespace. 4 | 5 | There are three examples that show the usage of `tenderly.contracts.verify` method: 6 | - First example shows the verification of a simple `Counter` contract with no dependencies. 7 | - Second example shows the verification of a `MyToken` contract that has multiple dependencies which are other contracts. 8 | - Third example shows the verification of a `LibraryToken` contract that has a library dependency. 9 | 10 | You can start these examples with: 11 | - `pnpm start:example:contractVerification:simpleCounter` 12 | - `pnpm start:example:contractVerification:withDependencies` 13 | - `pnpm start:example:contractVerification:withLibrary` 14 | -------------------------------------------------------------------------------- /lib/errors/GeneralError.ts: -------------------------------------------------------------------------------- 1 | import { TenderlyError } from './Error.types'; 2 | 3 | export abstract class GeneralError extends Error implements TenderlyError { 4 | public readonly id?: string; 5 | public readonly message: string; 6 | public readonly slug: string; 7 | public readonly data?: T; 8 | 9 | constructor({ id, message, slug, data }: TenderlyError & { data?: T }) { 10 | super(message); 11 | this.id = id; 12 | this.message = message; 13 | this.slug = slug; 14 | this.data = data; 15 | } 16 | 17 | static handle(error: Error | unknown) { 18 | if (error instanceof Error) throw error; 19 | 20 | // In case we do not know the error type we will convert to string and throw it anyway 21 | throw new Error(JSON.stringify(error)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/errors/index.ts: -------------------------------------------------------------------------------- 1 | export { ApiError } from './ApiError'; 2 | export { BytecodeMismatchError } from './BytecodeMismatchError'; 3 | export { CompilationError } from './CompilationError'; 4 | export { EncodingError } from './EncodingError'; 5 | import { errorHandlers } from './Error.handlerRegistry'; 6 | export type { TenderlyError } from './Error.types'; 7 | export { GeneralError } from './GeneralError'; 8 | export { InvalidArgumentsError } from './InvalidArgumentsError'; 9 | export { InvalidResponseError } from './InvalidResponseError'; 10 | export { NotFoundError } from './NotFoundError'; 11 | export { UnexpectedVerificationError } from './UnexpectedVerificationError'; 12 | 13 | export function handleError(error: Error | unknown) { 14 | errorHandlers.forEach(handler => handler.handle(error)); 15 | throw error; 16 | } 17 | -------------------------------------------------------------------------------- /examples/contractVerification/simpleCounter/contracts/Counter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | contract CounterWithLogs { 5 | uint public count; 6 | 7 | event CounterChanged( 8 | string method, 9 | uint256 oldNumber, 10 | uint256 newNumber, 11 | address caller 12 | ); 13 | 14 | // Function to get the current count 15 | function get() public view returns (uint) { 16 | return count; 17 | } 18 | 19 | // Function to increment count by 1 20 | function inc() public { 21 | emit CounterChanged("Increment", count, count + 1, msg.sender); 22 | count += 1; 23 | } 24 | 25 | // Function to decrement count by 1 26 | function dec() public { 27 | emit CounterChanged("Decrement", count, count - 1, msg.sender); 28 | 29 | count -= 1; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/contractVerification/withDependencies/contracts/IERC20Metadata.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | import "../IERC20.sol"; 7 | 8 | /** 9 | * @dev Interface for the optional metadata functions from the ERC20 standard. 10 | * 11 | * _Available since v4.1._ 12 | */ 13 | interface IERC20Metadata is IERC20 { 14 | /** 15 | * @dev Returns the name of the token. 16 | */ 17 | function name() external view returns (string memory); 18 | 19 | /** 20 | * @dev Returns the symbol of the token. 21 | */ 22 | function symbol() external view returns (string memory); 23 | 24 | /** 25 | * @dev Returns the decimals places of the token. 26 | */ 27 | function decimals() external view returns (uint8); 28 | } 29 | -------------------------------------------------------------------------------- /examples/simulateTransaction/README.md: -------------------------------------------------------------------------------- 1 | ### **Simulate Transaction** 2 | 3 | To simulate a transaction, you can use **`simulateTransaction`** method of the **`simulator`** namespace: 4 | 5 | ```jsx 6 | 7 | const callerAddress = '0xDBcB6Db1FFEaA10cd157F985a8543261250eFA46'; 8 | const counterContract = '0x93Cc0A80DE37EC4A4F97240B9807CDdfB4a19fB1'; 9 | 10 | const counterContractAbiInterface = new Interface(counterContractAbi); 11 | try { 12 | const transaction = await tenderly.simulator.simulateTransaction({ 13 | transaction: { 14 | from: callerAddress, 15 | to: counterContract, 16 | gas: 20000000, 17 | gas_price: '19419609232', 18 | value: 0, 19 | input: counterContractAbiInterface.encodeFunctionData('inc', []), 20 | }, 21 | blockNumber: 3237677, 22 | }); 23 | 24 | console.log('Simulated transaction:', transaction); 25 | } catch (error) { 26 | console.error('Error. Failed to simulate transaction: ', error); 27 | } 28 | ``` -------------------------------------------------------------------------------- /examples/contractVerification/withDependencies/contracts/Context.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts v4.4.1 (utils/Context.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | /** 7 | * @dev Provides information about the current execution context, including the 8 | * sender of the transaction and its data. While these are generally available 9 | * via msg.sender and msg.data, they should not be accessed in such a direct 10 | * manner, since when dealing with meta-transactions the account sending and 11 | * paying for execution may not be the actual sender (as far as an application 12 | * is concerned). 13 | * 14 | * This contract is only required for intermediate, library-like contracts. 15 | */ 16 | abstract contract Context { 17 | function _msgSender() internal view virtual returns (address) { 18 | return msg.sender; 19 | } 20 | 21 | function _msgData() internal view virtual returns (bytes calldata) { 22 | return msg.data; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/addWallets/index.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { Tenderly, Network, InvalidArgumentsError, getEnvironmentVariables } from '../../lib'; 3 | 4 | dotenv.config(); 5 | 6 | (async () => { 7 | try { 8 | const tenderly = new Tenderly({ 9 | accessKey: getEnvironmentVariables().TENDERLY_ACCESS_KEY, 10 | accountName: getEnvironmentVariables().TENDERLY_ACCOUNT_NAME, 11 | projectName: getEnvironmentVariables().TENDERLY_PROJECT_NAME, 12 | network: Network.SEPOLIA, 13 | }); 14 | 15 | const sepoliaWalletAddress = '0xDBcB6Db1FFEaA10cd157F985a8543261250eFA46'.toLowerCase(); 16 | 17 | const wallet = await tenderly.wallets.add(sepoliaWalletAddress, { 18 | displayName: 'Sepolia Wallet', 19 | }); 20 | 21 | console.log(wallet); 22 | } catch (error) { 23 | console.error(error); 24 | 25 | if (error instanceof InvalidArgumentsError) { 26 | console.error( 27 | 'Please provide a valid access key, account name and project name, by populating .env file.', 28 | ); 29 | } 30 | } 31 | })(); 32 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | 3 | on: 4 | pull_request_target: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build-and-test: 10 | runs-on: ubuntu-latest 11 | environment: CI 12 | env: 13 | TENDERLY_ACCESS_KEY: ${{secrets.TENDERLY_ACCESS_KEY}} 14 | TENDERLY_ACCOUNT_NAME: ${{secrets.TENDERLY_ACCOUNT_NAME}} 15 | TENDERLY_PROJECT_NAME: ${{secrets.TENDERLY_PROJECT_NAME}} 16 | TENDERLY_GET_BY_PROJECT_NAME: ${{secrets.TENDERLY_GET_BY_PROJECT_NAME}} 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | ref: ${{ github.event.pull_request.head.ref }} 22 | repository: ${{ github.event.pull_request.head.repo.full_name }} 23 | - uses: pnpm/action-setup@v3.0.0 24 | with: 25 | version: 8.15.7 26 | - uses: actions/setup-node@v4 27 | with: 28 | node-version: lts/* 29 | cache: "pnpm" 30 | - run: pnpm install --frozen-lockfile 31 | - run: pnpm lint 32 | - run: pnpm build 33 | - run: pnpm test 34 | -------------------------------------------------------------------------------- /examples/simulateBundle/README.md: -------------------------------------------------------------------------------- 1 | ### **Simulate bundle: mint DAI, approve and perform a Uniswap Swap** 2 | 3 | This example shows simulation of doing a Uniswap swap. We'll show the simulation outcome: the execution status of simulated transactions, and calculate the total gas used. 4 | 5 | Here are the transactions needed to achieve this: 6 | - Simulate minting of 2 DAI so the swapper has some assets to swap. Alternatively, if you have DAI, you can skip this and just do transaction 2. 7 | - Approve UniswapV3Router to use DAI. 8 | - Do the swap. Call UniswapV2Router.exactInputSingle to perform the swap. 9 | 10 | The 1st simulation (minting 2 DAI) is there to avoid working with actual assets. For it to run successfully, the sender needs to be a ward of DAI stablecoin. Since most of us aren't, you'll use the State Overrides to "become a ward" within the context of the Bundled Simulation, which is achieved by specifying `overrides` to the SDK. 11 | 12 | You can find a more detailed explanation in [Tenderly docs](https://docs.tenderly.co/tenderly-sdk/tutorials-and-quickstarts/how-to-perform-simulation-bundles-with-tenderly-sdk). 13 | 14 | -------------------------------------------------------------------------------- /lib/repositories/wallets/wallets.types.ts: -------------------------------------------------------------------------------- 1 | import { Network } from '../../types'; 2 | 3 | export interface Wallet { 4 | address: string; 5 | displayName?: string; 6 | tags?: string[]; 7 | network: Network; 8 | } 9 | 10 | export type TenderlyWallet = Wallet; 11 | 12 | export type WalletRequest = { 13 | address: string; 14 | display_name: string; 15 | network_ids: string[]; 16 | }; 17 | 18 | export type WalletResponse = { 19 | id: string; 20 | account_type: 'wallet'; 21 | display_name: string; 22 | account: { 23 | id: string; 24 | contract_id: string; 25 | balance: string; 26 | network_id: string; 27 | address: string; 28 | }; 29 | contract?: { 30 | id: string; 31 | contract_id: string; 32 | balance: string; 33 | network_id: string; 34 | address: string; 35 | }; 36 | wallet?: { 37 | id: string; 38 | contract_id: string; 39 | balance: string; 40 | network_id: string; 41 | address: string; 42 | }; 43 | tags?: { tag: string }[]; 44 | }; 45 | 46 | export type UpdateWalletRequest = { 47 | displayName?: string; 48 | appendTags?: string[]; 49 | }; 50 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Tenderly 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaVersion": "latest", 10 | "sourceType": "module", 11 | "project": "./tsconfig.json" 12 | }, 13 | "plugins": ["@typescript-eslint", "unused-imports"], 14 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"], 15 | "rules": { 16 | "max-len": [ 17 | "warn", 18 | { 19 | "code": 120 20 | } 21 | ], 22 | "no-multiple-empty-lines": [ 23 | "warn", 24 | { 25 | "max": 1, 26 | "maxEOF": 1, 27 | "maxBOF": 0 28 | } 29 | ], 30 | "semi": ["warn", "always"], 31 | "@typescript-eslint/no-unused-vars": "off", 32 | "unused-imports/no-unused-imports": "error", 33 | "unused-imports/no-unused-vars": [ 34 | "error", 35 | { "vars": "all", "varsIgnorePattern": "^_", "args": "after-used", "argsIgnorePattern": "^_" } 36 | ] 37 | }, 38 | "ignorePatterns": ["dist", "node_modules", "commitlint.config.js", "coverage", "prebuild.js"] 39 | } 40 | -------------------------------------------------------------------------------- /examples/addContracts/index.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { Tenderly, Network, InvalidArgumentsError, getEnvironmentVariables } from '../../lib'; 3 | 4 | dotenv.config(); 5 | 6 | try { 7 | const tenderly = new Tenderly({ 8 | accessKey: getEnvironmentVariables().TENDERLY_ACCESS_KEY, 9 | accountName: getEnvironmentVariables().TENDERLY_ACCOUNT_NAME, 10 | projectName: getEnvironmentVariables().TENDERLY_PROJECT_NAME, 11 | network: Network.MAINNET, 12 | }); 13 | 14 | const unverifiedContractAddress = '0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe'.toLowerCase(); 15 | const daiContract = '0x6b175474e89094c44da98b954eedeac495271d0f'.toLowerCase(); 16 | 17 | (async () => { 18 | const unverifiedContract = await tenderly.contracts.add(unverifiedContractAddress, { 19 | displayName: 'Unverified Contract', 20 | }); 21 | const verifiedContract = await tenderly.contracts.add(daiContract); 22 | 23 | console.log(unverifiedContract); 24 | console.log(verifiedContract); 25 | })(); 26 | } catch (error) { 27 | console.error(error); 28 | 29 | if (error instanceof InvalidArgumentsError) { 30 | console.error(error.message); 31 | console.log( 32 | 'Please provide a valid access key, account name and project name, by populating .env file.', 33 | ); 34 | } 35 | 36 | process.exit(1); 37 | } 38 | -------------------------------------------------------------------------------- /examples/simulateTransaction/index.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { Interface } from 'ethers'; 3 | import { counterContractAbi } from './counterContract'; 4 | import { Tenderly, Network, getEnvironmentVariables } from '../../lib'; 5 | 6 | dotenv.config(); 7 | 8 | const callerAddress = '0xDBcB6Db1FFEaA10cd157F985a8543261250eFA46'; 9 | const counterContract = '0x93Cc0A80DE37EC4A4F97240B9807CDdfB4a19fB1'; 10 | 11 | const counterContractAbiInterface = new Interface(counterContractAbi); 12 | 13 | (async () => { 14 | try { 15 | const tenderly = new Tenderly({ 16 | accessKey: getEnvironmentVariables().TENDERLY_ACCESS_KEY, 17 | accountName: getEnvironmentVariables().TENDERLY_ACCOUNT_NAME, 18 | projectName: getEnvironmentVariables().TENDERLY_PROJECT_NAME, 19 | network: Network.SEPOLIA, 20 | }); 21 | 22 | const transaction = await tenderly.simulator.simulateTransaction({ 23 | transaction: { 24 | from: callerAddress, 25 | to: counterContract, 26 | gas: 20000000, 27 | gas_price: '19419609232', 28 | value: '0', 29 | input: counterContractAbiInterface.encodeFunctionData('inc', []), 30 | }, 31 | blockNumber: 3237677, 32 | }); 33 | 34 | console.log('Simulated transaction:', transaction); 35 | } catch (error) { 36 | console.error('Error. Failed to simulate transaction: ', error); 37 | } 38 | })(); 39 | -------------------------------------------------------------------------------- /examples/contractVerification/simpleCounter/index.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { readFileSync } from 'fs'; 3 | import { Tenderly, Network, Web3Address, getEnvironmentVariables } from '../../../lib'; 4 | 5 | dotenv.config(); 6 | 7 | const myTokenAddress = '0x8aaf9071e6c3129653b2dc39044c3b79c0bfcfbf'.toLowerCase() as Web3Address; 8 | 9 | (async () => { 10 | try { 11 | const tenderly = new Tenderly({ 12 | accessKey: getEnvironmentVariables().TENDERLY_ACCESS_KEY, 13 | accountName: getEnvironmentVariables().TENDERLY_ACCOUNT_NAME, 14 | projectName: getEnvironmentVariables().TENDERLY_PROJECT_NAME, 15 | network: Network.SEPOLIA, 16 | }); 17 | 18 | const result = await tenderly.contracts.verify(myTokenAddress, { 19 | config: { 20 | mode: 'public', 21 | }, 22 | contractToVerify: 'Counter.sol:CounterWithLogs', 23 | solc: { 24 | version: 'v0.8.18', 25 | sources: { 26 | 'Counter.sol': { 27 | content: readFileSync( 28 | 'examples/contractVerification/simpleCounter/contracts/Counter.sol', 29 | 'utf8', 30 | ), 31 | }, 32 | }, 33 | settings: { 34 | libraries: {}, 35 | optimizer: { 36 | enabled: false, 37 | runs: 200, 38 | }, 39 | }, 40 | }, 41 | }); 42 | 43 | console.log('Result:', result); 44 | } catch (error) { 45 | console.error(error); 46 | } 47 | })(); 48 | -------------------------------------------------------------------------------- /examples/simulateTransaction/counterContract.ts: -------------------------------------------------------------------------------- 1 | export const counterContractAbi = [ 2 | { 3 | anonymous: false, 4 | inputs: [ 5 | { 6 | indexed: false, 7 | internalType: 'string', 8 | name: 'method', 9 | type: 'string', 10 | }, 11 | { 12 | indexed: false, 13 | internalType: 'uint256', 14 | name: 'oldNumber', 15 | type: 'uint256', 16 | }, 17 | { 18 | indexed: false, 19 | internalType: 'uint256', 20 | name: 'newNumber', 21 | type: 'uint256', 22 | }, 23 | { 24 | indexed: false, 25 | internalType: 'address', 26 | name: 'caller', 27 | type: 'address', 28 | }, 29 | ], 30 | name: 'CounterChanged', 31 | type: 'event', 32 | }, 33 | { 34 | inputs: [], 35 | name: 'dec', 36 | outputs: [], 37 | stateMutability: 'nonpayable', 38 | type: 'function', 39 | }, 40 | { 41 | inputs: [], 42 | name: 'inc', 43 | outputs: [], 44 | stateMutability: 'nonpayable', 45 | type: 'function', 46 | }, 47 | { 48 | inputs: [], 49 | name: 'count', 50 | outputs: [ 51 | { 52 | internalType: 'uint256', 53 | name: '', 54 | type: 'uint256', 55 | }, 56 | ], 57 | stateMutability: 'view', 58 | type: 'function', 59 | }, 60 | { 61 | inputs: [], 62 | name: 'get', 63 | outputs: [ 64 | { 65 | internalType: 'uint256', 66 | name: '', 67 | type: 'uint256', 68 | }, 69 | ], 70 | stateMutability: 'view', 71 | type: 'function', 72 | }, 73 | ]; 74 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish NPM Package 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | environment: CI 12 | env: 13 | TENDERLY_ACCESS_KEY: ${{secrets.TENDERLY_ACCESS_KEY}} 14 | TENDERLY_ACCOUNT_NAME: ${{secrets.TENDERLY_ACCOUNT_NAME}} 15 | TENDERLY_PROJECT_NAME: ${{secrets.TENDERLY_PROJECT_NAME}} 16 | TENDERLY_GET_BY_PROJECT_NAME: ${{secrets.TENDERLY_GET_BY_PROJECT_NAME}} 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: pnpm/action-setup@v3.0.0 21 | with: 22 | version: 8.15.7 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: lts/* 26 | cache: "pnpm" 27 | - run: pnpm install --frozen-lockfile 28 | - run: pnpm lint 29 | - run: pnpm build 30 | - run: pnpm test 31 | 32 | publish-npm: 33 | needs: build 34 | runs-on: ubuntu-latest 35 | environment: CI 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: pnpm/action-setup@v3.0.0 39 | with: 40 | version: 8.15.7 41 | - uses: actions/setup-node@v4 42 | with: 43 | node-version: lts/* 44 | cache: "pnpm" 45 | - run: pnpm install --frozen-lockfile 46 | - run: pnpm build 47 | - name: Create Release Pull Request or Publish 48 | id: changesets 49 | uses: changesets/action@v1 50 | with: 51 | publish: pnpm run publish 52 | commit: "chore(release): npm release" 53 | title: "Release" 54 | env: 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 57 | -------------------------------------------------------------------------------- /examples/contractVerification/withLibrary/index.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { readFileSync } from 'fs'; 3 | import { Tenderly, Network, Web3Address, getEnvironmentVariables } from '../../../lib'; 4 | 5 | dotenv.config(); 6 | 7 | const libraryTokenContract = 8 | '0xbeba0016bd2fff7c81c5877cc0fcc509760785b5'.toLowerCase() as Web3Address; 9 | const libraryContract = '0xcA00A6512792aa89e347c713F443b015A1006f1d'.toLowerCase(); 10 | 11 | (async () => { 12 | try { 13 | const tenderly = new Tenderly({ 14 | accessKey: getEnvironmentVariables().TENDERLY_ACCESS_KEY, 15 | accountName: getEnvironmentVariables().TENDERLY_ACCOUNT_NAME, 16 | projectName: getEnvironmentVariables().TENDERLY_PROJECT_NAME, 17 | network: Network.SEPOLIA, 18 | }); 19 | 20 | const result = await tenderly.contracts.verify(libraryTokenContract, { 21 | config: { 22 | mode: 'public', 23 | }, 24 | contractToVerify: 'LibraryToken.sol:LibraryToken', 25 | solc: { 26 | version: 'v0.8.17', 27 | sources: { 28 | 'LibraryToken.sol': { 29 | content: readFileSync( 30 | 'examples/contractVerification/withLibrary/contracts/LibraryToken.sol', 31 | 'utf8', 32 | ), 33 | }, 34 | 'Library.sol': { 35 | content: readFileSync( 36 | 'examples/contractVerification/withLibrary/contracts/Library.sol', 37 | 'utf8', 38 | ), 39 | }, 40 | }, 41 | settings: { 42 | libraries: { 43 | 'Library.sol': { 44 | Library: libraryContract, 45 | }, 46 | }, 47 | optimizer: { 48 | enabled: true, 49 | runs: 200, 50 | }, 51 | }, 52 | }, 53 | }); 54 | 55 | console.log('Result:', result); 56 | } catch (error) { 57 | console.error(error); 58 | } 59 | })(); 60 | -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | export type Path = string; 2 | export type Web3Address = string; 3 | 4 | export enum Network { 5 | MAINNET = 1, 6 | OPTIMISTIC = 10, 7 | RSK = 30, 8 | RSK_TESTNET = 31, 9 | BINANCE = 56, 10 | RIALTO_BINANCE = 97, 11 | GNOSIS_CHAIN = 100, 12 | BOB_SEPOLIA = 111, 13 | POLYGON = 137, 14 | FRAXTAL = 252, 15 | BOBA_ETHEREUM = 288, 16 | WORLDCHAIN = 480, 17 | MODE_SEPOLIA = 919, 18 | LISK = 1135, 19 | MOONBEAM = 1284, 20 | MOONRIVER = 1285, 21 | MOONBASE_ALPHA = 1287, 22 | SEI_ATLANTIC_2 = 1328, 23 | SEI_PACIFIC_1 = 1329, 24 | FRAXTAL_HOLESKY = 2522, 25 | MORPH_HOLESKY = 2810, 26 | LISK_SEPOLIA = 4202, 27 | GOLD = 4653, 28 | WORLDCHAIN_SEPOLIA = 4801, 29 | MANTLE = 5000, 30 | MANTLE_SEPOLIA = 5003, 31 | ZETACHAIN = 7000, 32 | ZETACHAIN_TESTNET = 7001, 33 | KINTO = 7887, 34 | POLYNOMIAL = 8008, 35 | BASE = 8453, 36 | BOBA_BINANCE_RIALTO = 9728, 37 | INTERVAL_TESTNET = 11069, 38 | IMMUTABLE = 13371, 39 | IMMUTABLE_TESTNET = 13473, 40 | HOLESKY = 17000, 41 | UNREAL = 18233, 42 | CONCRETE_TESTNET = 18291, 43 | BOBA_SEPOLIA = 28882, 44 | APECHAIN_CURTIS = 33111, 45 | APECHAIN = 33139, 46 | MODE = 34443, 47 | ARBITRUM_ONE = 42161, 48 | ARBITRUM_NOVA = 42170, 49 | AVALANCHE_FUJI = 43113, 50 | AVALANCHE = 43114, 51 | BOBA_BINANCE = 56288, 52 | LINEA_SEPOLIA = 59141, 53 | LINEA = 59144, 54 | BOB = 60808, 55 | POLYGON_AMOY = 80002, 56 | POLYNOMIAL_SEPOLIA = 80008, 57 | BLAST = 81457, 58 | BASE_SEPOLIA = 84532, 59 | REAL = 111188, 60 | TAIKO = 167000, 61 | TAIKO_HEKLA = 167009, 62 | ARBITRUM_SEPOLIA = 421614, 63 | ZORA = 7777777, 64 | SEPOLIA = 11155111, 65 | OPTIMISTIC_SEPOLIA = 11155420, 66 | ZORA_SEPOLIA = 999999999, 67 | } 68 | 69 | export type TenderlyConfiguration = { 70 | accountName: string; 71 | projectName: string; 72 | accessKey: string; 73 | network: Network | number; 74 | }; 75 | 76 | // helper types 77 | export type WithRequired = T & { [P in K]-?: T[P] }; 78 | export type EmptyObject = Record; 79 | -------------------------------------------------------------------------------- /examples/contractVerification/withDependencies/index.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { readFileSync } from 'fs'; 3 | import { Tenderly, Network, Web3Address, getEnvironmentVariables } from '../../../lib'; 4 | 5 | dotenv.config(); 6 | 7 | const myTokenAddress = `0x1a273A64C89CC45aBa798B1bC31B416A199Be3b3`.toLowerCase() as Web3Address; 8 | const ROOT_FOLDER = `examples/contractVerification/withDependencies/contracts`; 9 | 10 | (async () => { 11 | try { 12 | const tenderly = new Tenderly({ 13 | accessKey: getEnvironmentVariables().TENDERLY_ACCESS_KEY, 14 | accountName: getEnvironmentVariables().TENDERLY_ACCOUNT_NAME, 15 | projectName: getEnvironmentVariables().TENDERLY_PROJECT_NAME, 16 | network: Network.SEPOLIA, 17 | }); 18 | 19 | const result = await tenderly.contracts.verify(myTokenAddress, { 20 | config: { 21 | mode: `public`, 22 | }, 23 | contractToVerify: `${ROOT_FOLDER}/MyToken.sol:MyToken`, 24 | solc: { 25 | version: `v0.8.19`, 26 | sources: { 27 | [`${ROOT_FOLDER}/MyToken.sol`]: { 28 | content: readFileSync(`${ROOT_FOLDER}/MyToken.sol`, `utf8`), 29 | }, 30 | [`@openzeppelin/contracts/token/ERC20/ERC20.sol`]: { 31 | content: readFileSync(`${ROOT_FOLDER}/ERC20.sol`, `utf8`), 32 | }, 33 | [`@openzeppelin/contracts/token/ERC20/IERC20.sol`]: { 34 | content: readFileSync(`${ROOT_FOLDER}/IERC20.sol`, `utf8`), 35 | }, 36 | [`@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol`]: { 37 | content: readFileSync(`${ROOT_FOLDER}/IERC20Metadata.sol`, `utf8`), 38 | }, 39 | [`@openzeppelin/contracts/utils/Context.sol`]: { 40 | content: readFileSync(`${ROOT_FOLDER}/Context.sol`, `utf8`), 41 | }, 42 | }, 43 | settings: { 44 | optimizer: { 45 | enabled: true, 46 | runs: 200, 47 | }, 48 | }, 49 | }, 50 | }); 51 | 52 | console.log(`Result:`, result); 53 | } catch (error) { 54 | console.error(error); 55 | } 56 | })(); 57 | -------------------------------------------------------------------------------- /examples/allowAndTransfer/README.md: -------------------------------------------------------------------------------- 1 | ### **Allow and Transfer on ERC20** 2 | 3 | In this example we simulate a common transaction sequence in which a user first approves another address (the spender), by calling **approve** method, to spend their tokens, which is followed by the spender calling **transferFrom** method on the contract to transfer the actual tokens. 4 | 5 | 6 | ```typescript 7 | 8 | import { Interface } from 'ethers'; 9 | import { myTokenAbi } from './myTokenAbi'; // ABI of the ERC20 token contract 10 | 11 | ... 12 | 13 | const myTokenAddress = '0x912043e00a14a6b79f5e500c825b1439e812d7ce'; 14 | const fromWalletAddress = '0x8d1d4e2b8b9b1b4b9e0d0d6d7c7c4e4e8d9d00e0'; 15 | const toWalletAddress = '0xDBcB6Db1FFEaA10cd157F985a8543261250eFA46'; 16 | 17 | const myTokenAbiInterface = new Interface(myTokenAbi); 18 | 19 | const simulations = await tenderly.simulator.simulateBundle({ 20 | transactions: [ 21 | { 22 | from: fromWalletAddress, 23 | to: myTokenAddress, 24 | gas: 0, 25 | gas_price: '0', 26 | value: 0, 27 | input: myTokenAbiInterface.encodeFunctionData('approve', [toWalletAddress, 1234567890]), 28 | }, 29 | { 30 | from: toWalletAddress, 31 | to: myTokenAddress, 32 | gas: 0, 33 | gas_price: '0', 34 | value: 0, 35 | input: myTokenAbiInterface.encodeFunctionData('transferFrom', [ 36 | fromWalletAddress, 37 | toWalletAddress, 38 | 1234567890, 39 | ]), 40 | }, 41 | ], 42 | blockNumber: 3262454, 43 | overrides: { 44 | [myTokenAddress]: { 45 | state: { 46 | [`_balances[${fromWalletAddress}]`]: '1234567891', 47 | }, 48 | }, 49 | }, 50 | }); 51 | 52 | ``` 53 | 54 | Notice the overrides argument in for this transaction bundle: 55 | ```js 56 | overrides: { 57 | [myTokenAddress]: { 58 | state: { 59 | [`_balances[${fromWalletAddress}]`]: '1234567891', 60 | }, 61 | }, 62 | }, 63 | ``` 64 | 65 | This is used to override the state of the contract, in this case the balance of the sender. This is useful when you want to simulate a transaction bundle that is not possible in the current state of the contract. 66 | -------------------------------------------------------------------------------- /lib/core/ApiClient.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance, AxiosResponse } from 'axios'; 2 | import { TENDERLY_API_BASE_URL } from '../constants'; 3 | import { TENDERLY_SDK_VERSION } from '../sdkVersion'; 4 | 5 | export type ApiVersion = 'v1' | 'v2'; 6 | 7 | export class ApiClient { 8 | private readonly api: AxiosInstance; 9 | 10 | /** 11 | * @param apiKey API key to be used for the requests. 12 | * Can be generated in the Tenderly dashboard: https://dashboard.tenderly.co/account/authorization 13 | * @param version API version to be used for the requests. Defaults to 'v1' 14 | */ 15 | constructor({ apiKey, version = 'v1' }: { apiKey: string; version?: ApiVersion }) { 16 | this.api = axios.create({ 17 | baseURL: `${TENDERLY_API_BASE_URL}/api/${version}`, 18 | headers: { 19 | 'Content-Type': 'application/json', 20 | 'X-Access-Key': apiKey, 21 | 'X-User-Agent': `@tenderly/sdk-js/${TENDERLY_SDK_VERSION}`, 22 | }, 23 | }); 24 | } 25 | 26 | /** 27 | * 28 | * @param path url path to the resource 29 | * @param params url query params 30 | * @returns Promise with AxiosResponse that 31 | * contains the response data model with a type of a given generic parameter 32 | */ 33 | public get(path: string, params?: Record) { 34 | return this.api.get(path.replace(/\s/g, ''), { params }); 35 | } 36 | 37 | /** 38 | * 39 | * @param path url path to the resource 40 | * @param data data to be sent to the server. Type of data expected can be specified with a second generic parameter 41 | * @returns Promise with AxiosResponse that 42 | * contains the response data model with a type of a second generic parameter 43 | */ 44 | public post(path: string, data?: RequestModel) { 45 | return this.api.post(path.replace(/\s/g, ''), data); 46 | } 47 | 48 | /** 49 | * 50 | * @param path url path to the resource 51 | * @param data data to be sent to the server in order to update the model. 52 | * Type of data expected can be specified with a second generic parameter 53 | * @param params url query params 54 | * @returns Promise with AxiosResponse that contains the response data model with a type of a second generic parameter 55 | */ 56 | public async put( 57 | path: string, 58 | data?: RequestModel, 59 | params?: Record, 60 | ) { 61 | return this.api.put(path.replace(/\s/g, ''), data, { params }); 62 | } 63 | 64 | /** 65 | * @param path url path to the resource 66 | * @param data data to be sent to the server in order to remove the model. 67 | * @returns AxiosResponse 68 | */ 69 | public async delete(path: string, data?: Record): Promise { 70 | return this.api.delete(path.replace(/\s/g, ''), { data }); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /examples/allowAndTransfer/index.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { Interface } from 'ethers'; 3 | import { myTokenAbi } from './myTokenAbi'; 4 | import { 5 | Tenderly, 6 | Network, 7 | InvalidArgumentsError, 8 | Web3Address, 9 | RawEvent, 10 | getEnvironmentVariables, 11 | } from '../../lib'; 12 | 13 | dotenv.config(); 14 | 15 | const myTokenAddress = '0x912043e00a14a6b79f5e500c825b1439e812d7ce'.toLowerCase() as Web3Address; 16 | 17 | const fromWalletAddress = '0x8d1d4e2b8b9b1b4b9e0d0d6d7c7c4e4e8d9d00e0'.toLowerCase() as Web3Address; 18 | 19 | const toWalletAddress = '0xDBcB6Db1FFEaA10cd157F985a8543261250eFA46'.toLowerCase() as Web3Address; 20 | 21 | const myTokenAbiInterface = new Interface(myTokenAbi); 22 | 23 | (async () => { 24 | try { 25 | const tenderly = new Tenderly({ 26 | accessKey: getEnvironmentVariables().TENDERLY_ACCESS_KEY, 27 | accountName: getEnvironmentVariables().TENDERLY_ACCOUNT_NAME, 28 | projectName: getEnvironmentVariables().TENDERLY_PROJECT_NAME, 29 | network: Network.SEPOLIA, 30 | }); 31 | 32 | const simulations = await tenderly.simulator.simulateBundle({ 33 | transactions: [ 34 | { 35 | from: fromWalletAddress, 36 | to: myTokenAddress, 37 | gas: 0, 38 | gas_price: '0', 39 | value: '0', 40 | input: myTokenAbiInterface.encodeFunctionData('approve', [toWalletAddress, 1234567890]), 41 | }, 42 | { 43 | from: toWalletAddress, 44 | to: myTokenAddress, 45 | gas: 0, 46 | gas_price: '0', 47 | value: '0', 48 | input: myTokenAbiInterface.encodeFunctionData('transferFrom', [ 49 | fromWalletAddress, 50 | toWalletAddress, 51 | 1234567890, 52 | ]), 53 | }, 54 | ], 55 | blockNumber: 3262454, 56 | overrides: { 57 | [myTokenAddress]: { 58 | state: { 59 | [`_balances[${fromWalletAddress}]`]: '1234567891', 60 | }, 61 | }, 62 | }, 63 | }); 64 | 65 | if (!simulations || simulations.length !== 2) { 66 | throw new Error('Simulation bundle is invalid'); 67 | } 68 | 69 | const allLogs = simulations 70 | .map(simulation => simulation.logs) 71 | .reduce((acc, logs) => (logs && acc ? [...acc, ...logs] : acc), []); 72 | 73 | if (!allLogs || allLogs.length !== 3) { 74 | throw new Error('Simulation bundle failed to return all logs'); 75 | } 76 | 77 | // parse raw logs 78 | const [firstApprovalLog, secondApprovalLog, transferLog] = allLogs.map(log => 79 | myTokenAbiInterface?.parseLog(log.raw as RawEvent), 80 | ); 81 | 82 | console.log('Approval logs:', [firstApprovalLog, secondApprovalLog]); 83 | 84 | console.log('Transfer log:', transferLog); 85 | } catch (e) { 86 | if (e instanceof InvalidArgumentsError) { 87 | console.log('Please populate your .env file with the correct values'); 88 | process.exit(1); 89 | } 90 | 91 | console.log(e); 92 | } 93 | })(); 94 | -------------------------------------------------------------------------------- /examples/contractVerification/withDependencies/contracts/IERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | /** 7 | * @dev Interface of the ERC20 standard as defined in the EIP. 8 | */ 9 | interface IERC20 { 10 | /** 11 | * @dev Emitted when `value` tokens are moved from one account (`from`) to 12 | * another (`to`). 13 | * 14 | * Note that `value` may be zero. 15 | */ 16 | event Transfer(address indexed from, address indexed to, uint256 value); 17 | 18 | /** 19 | * @dev Emitted when the allowance of a `spender` for an `owner` is set by 20 | * a call to {approve}. `value` is the new allowance. 21 | */ 22 | event Approval(address indexed owner, address indexed spender, uint256 value); 23 | 24 | /** 25 | * @dev Returns the amount of tokens in existence. 26 | */ 27 | function totalSupply() external view returns (uint256); 28 | 29 | /** 30 | * @dev Returns the amount of tokens owned by `account`. 31 | */ 32 | function balanceOf(address account) external view returns (uint256); 33 | 34 | /** 35 | * @dev Moves `amount` tokens from the caller's account to `to`. 36 | * 37 | * Returns a boolean value indicating whether the operation succeeded. 38 | * 39 | * Emits a {Transfer} event. 40 | */ 41 | function transfer(address to, uint256 amount) external returns (bool); 42 | 43 | /** 44 | * @dev Returns the remaining number of tokens that `spender` will be 45 | * allowed to spend on behalf of `owner` through {transferFrom}. This is 46 | * zero by default. 47 | * 48 | * This value changes when {approve} or {transferFrom} are called. 49 | */ 50 | function allowance(address owner, address spender) external view returns (uint256); 51 | 52 | /** 53 | * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. 54 | * 55 | * Returns a boolean value indicating whether the operation succeeded. 56 | * 57 | * IMPORTANT: Beware that changing an allowance with this method brings the risk 58 | * that someone may use both the old and the new allowance by unfortunate 59 | * transaction ordering. One possible solution to mitigate this race 60 | * condition is to first reduce the spender's allowance to 0 and set the 61 | * desired value afterwards: 62 | * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 63 | * 64 | * Emits an {Approval} event. 65 | */ 66 | function approve(address spender, uint256 amount) external returns (bool); 67 | 68 | /** 69 | * @dev Moves `amount` tokens from `from` to `to` using the 70 | * allowance mechanism. `amount` is then deducted from the caller's 71 | * allowance. 72 | * 73 | * Returns a boolean value indicating whether the operation succeeded. 74 | * 75 | * Emits a {Transfer} event. 76 | */ 77 | function transferFrom( 78 | address from, 79 | address to, 80 | uint256 amount 81 | ) external returns (bool); 82 | } 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tenderly/sdk", 3 | "version": "0.3.1", 4 | "description": "Tenderly SDK", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "module": "dist/index.mjs", 8 | "homepage:": "https://github.com/Tenderly/tenderly-sdk#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Tenderly/tenderly-sdk.git" 12 | }, 13 | "files": [ 14 | "dist" 15 | ], 16 | "scripts": { 17 | "start:example:addContracts": "ts-node examples/addContracts/index.ts", 18 | "start:example:addWallets": "ts-node examples/addWallets/index.ts", 19 | "start:example:allowAndTransfer": "ts-node examples/allowAndTransfer/index.ts", 20 | "start:example:contractVerification:withDependencies": "ts-node examples/contractVerification/withDependencies/index.ts", 21 | "start:example:contractVerification:simpleCounter": "ts-node examples/contractVerification/simpleCounter/index.ts", 22 | "start:example:contractVerification:withLibrary": "ts-node examples/contractVerification/withLibrary/index.ts", 23 | "start:example:simulateBundle": "ts-node examples/simulateBundle/index.ts", 24 | "start:example:simulateTransaction": "ts-node examples/simulateTransaction/index.ts", 25 | "test": "jest", 26 | "test:watch": "jest --watch", 27 | "build": "node prebuild.js && tsup lib/index.ts --dts --format cjs,esm", 28 | "lint": "tsc && eslint . --ext .ts,.js", 29 | "format": "prettier --ignore-path .gitignore --write \"**/*.+(js|ts|json)\"", 30 | "prepare": "husky install", 31 | "publish": "changeset publish", 32 | "version": "pnpm changeset version" 33 | }, 34 | "author": "Tenderly Dev Team", 35 | "license": "MIT", 36 | "devDependencies": { 37 | "@changesets/cli": "^2.26.1", 38 | "@commitlint/cli": "^17.4.4", 39 | "@commitlint/config-conventional": "^17.4.4", 40 | "@safe-global/safe-deployments": "^1.21.1", 41 | "@swc/core": "^1.3.46", 42 | "@types/jest": "^29.4.0", 43 | "@types/node": "^20.4.8", 44 | "@typescript-eslint/eslint-plugin": "^5.53.0", 45 | "@typescript-eslint/parser": "^5.53.0", 46 | "dotenv": "^16.0.3", 47 | "eslint": "^8.34.0", 48 | "eslint-config-prettier": "^8.6.0", 49 | "eslint-plugin-prettier": "^4.2.1", 50 | "eslint-plugin-unused-imports": "^3.0.0", 51 | "ethers": "^6.2.0", 52 | "husky": "^8.0.3", 53 | "jest": "^29.4.3", 54 | "lint-staged": "^13.2.0", 55 | "prettier": "^2.8.4", 56 | "ts-jest": "^29.0.5", 57 | "ts-node": "^10.9.1", 58 | "tsup": "^6.7.0", 59 | "typescript": "^4.9.5" 60 | }, 61 | "dependencies": { 62 | "axios": "^1.3.4" 63 | }, 64 | "lint-staged": { 65 | "*.ts": [ 66 | "pnpm eslint --fix --max-warnings 0", 67 | "pnpm prettier --write" 68 | ] 69 | }, 70 | "bugs": { 71 | "url": "https://github.com/Tenderly/tenderly-sdk/issues" 72 | }, 73 | "homepage": "https://github.com/Tenderly/tenderly-sdk#readme", 74 | "directories": { 75 | "example": "examples", 76 | "lib": "lib", 77 | "test": "test" 78 | }, 79 | "keywords": [ 80 | "sdk", 81 | "javascript", 82 | "web3", 83 | "simulations", 84 | "ethereum" 85 | ], 86 | "packageManager": "pnpm@8.15.7" 87 | } 88 | -------------------------------------------------------------------------------- /lib/core/Tenderly.ts: -------------------------------------------------------------------------------- 1 | import { TenderlyConfiguration } from '../types'; 2 | import { WalletRepository, ContractRepository } from '../repositories'; 3 | import { Simulator } from '../executors'; 4 | import { VerificationRequest } from '../repositories/contracts/contracts.types'; 5 | import { ApiClientProvider } from './ApiClientProvider'; 6 | import { InvalidArgumentsError } from '../errors'; 7 | 8 | /** 9 | * The main class of the Tenderly SDK 10 | * Instantiate this class with your config, and you're ready to go 11 | * @example 12 | * const tenderly = new Tenderly({ 13 | * accountName: 'my-account', 14 | * projectName: 'my-project', 15 | * accessKey: 'my-access-key', 16 | * network: Network.Mainnet, 17 | * }) 18 | */ 19 | export class Tenderly { 20 | public readonly configuration: TenderlyConfiguration; 21 | // public readonly api: ApiClient; 22 | 23 | /** 24 | * Contract repository - used for managing contracts on your project 25 | */ 26 | public readonly contracts: ContractRepository & { 27 | verify: (address: string, verificationRequest: VerificationRequest) => Promise; 28 | }; 29 | 30 | /** 31 | * Wallet repository - used for managing wallets on your project 32 | */ 33 | public readonly wallets: WalletRepository; 34 | 35 | /** 36 | * Simulator - used for simulating transactions 37 | */ 38 | public readonly simulator: Simulator; 39 | 40 | private readonly apiClientProvider: ApiClientProvider; 41 | 42 | /** 43 | * The main class of the Tenderly SDK 44 | * Instantiate this class with your config, and you're ready to go 45 | * @example 46 | * const tenderly = new Tenderly({ 47 | * accountName: 'my-account', 48 | * projectName: 'my-project', 49 | * accessKey: 'my-access-key', 50 | * network: Network.Mainnet, 51 | * }) 52 | */ 53 | constructor(configuration: TenderlyConfiguration) { 54 | this.checkConfiguration(configuration); 55 | 56 | this.configuration = configuration; 57 | this.apiClientProvider = new ApiClientProvider({ apiKey: configuration.accessKey }); 58 | 59 | this.simulator = new Simulator({ apiProvider: this.apiClientProvider, configuration }); 60 | this.contracts = new ContractRepository({ apiProvider: this.apiClientProvider, configuration }); 61 | this.wallets = new WalletRepository({ apiProvider: this.apiClientProvider, configuration }); 62 | } 63 | 64 | /** 65 | * Create a new Tenderly instance with the provided configuration override 66 | * @param configurationOverride - The configuration override 67 | * @returns The new Tenderly instance 68 | * @example 69 | * const tenderly = new Tenderly({ 70 | * accountName: 'my-account', 71 | * projectName: 'my-project', 72 | * ); 73 | */ 74 | public with(configurationOverride: Partial) { 75 | return new Tenderly({ ...this.configuration, ...configurationOverride }); 76 | } 77 | 78 | checkConfiguration(configuration: TenderlyConfiguration) { 79 | if (!configuration.accessKey) { 80 | throw new InvalidArgumentsError('Missing access key.'); 81 | } 82 | if (!configuration.accountName) { 83 | throw new InvalidArgumentsError('Missing account name.'); 84 | } 85 | if (!configuration.projectName) { 86 | throw new InvalidArgumentsError('Missing project name.'); 87 | } 88 | if (!configuration.network) { 89 | throw new InvalidArgumentsError('Missing network.'); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tenderly SDK 2 | 3 |
4 | 5 | tenderly-logo 6 | 7 |
8 |
9 | 10 | SDK for working with your favorite Web3 development platform 11 | 12 |
13 | 14 |
15 | 16 | 17 | [![License](https://img.shields.io/github/license/Tenderly/tenderly-sdk)](./LICENSE) 18 | [![npm](https://img.shields.io/npm/v/@tenderly%2Fsdk.svg)](https://www.npmjs.org/package/@tenderly/sdk) 19 | [![Twitter](https://img.shields.io/twitter/follow/TenderlyApp?style=social)](https://twitter.com/intent/follow?screen_name=TenderlyApp) 20 | [![Github](https://img.shields.io/github/stars/Tenderly/tenderly-sdk?style=social)](https://github.com/Tenderly/tenderly-sdk) 21 | 22 |
23 | 24 | ## Table of contents 25 | 26 | 37 | 38 | ## Introduction 39 | 40 | The Tenderly SDK provides an easy-to-use interface for interacting with the Tenderly platform. 41 | 42 | It allows you to simulate transactions, simulate transaction bundles, manage contracts and wallets, and verify smart contracts from your code. The SDK is particularly useful for blockchain developers who want to integrate Tenderly's powerful tools into their dapp or development workflow. 43 | 44 | List of supported networks can be found here 45 | 46 | ## Documentation 47 | 48 | Full documentation with example snippets here:
Tenderly SDK docs 49 | 50 | ## Quick start 51 | 52 | ### Installation 53 | 54 | Available on npm as tenderly-sdk 55 | npm 56 | 57 | ```sh 58 | npm i @tenderly/sdk 59 | ``` 60 | 61 | yarn 62 | 63 | ```sh 64 | yarn add @tenderly/sdk 65 | ``` 66 | 67 | pnpm 68 | 69 | ```sh 70 | pnpm add @tenderly/sdk 71 | ``` 72 | 73 | ### Quick start 74 | 75 | Instantiate a new tenderly instance with your project details. _We highly recommend using environment variables for sensitive data such as access keys during your local development!_ 76 | 77 | ```ts 78 | import { Tenderly, Network } from '@tenderly/sdk'; 79 | 80 | const tenderlyInstance = new Tenderly({ 81 | accessKey: process.env.TENDERLY_ACCESS_KEY, 82 | accountName: process.env.TENDERLY_ACCOUNT_NAME, 83 | projectName: process.env.TENDERLY_PROJECT_NAME, 84 | network: Network.MAINNET, 85 | }); 86 | ``` 87 | 88 | Fetch project contracts 89 | 90 | ```ts 91 | const contracts = await tenderlyInstance.contracts.getAll(); 92 | 93 | console.log(contracts.map(contract => contract.address).join(', ')); 94 | // 0x63456...5689, 0x54j2...23890, 0x211e...289n 95 | ``` 96 | 97 | ## Examples 98 | 99 | - Add contracts 100 | - Add wallets 101 | - Allow and Transfer 102 | - Contract verification 103 | - Simulate transaction 104 | - Simulate bundle 105 | 106 | ## Contributors 107 | 108 | 109 | 110 | 111 | 112 | ## License 113 | 114 | [MIT](LICENSE) 115 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @tenderly/sdk 2 | 3 | ## 0.3.1 4 | 5 | ### Patch Changes 6 | 7 | - 79f564f: Deprecate fantom 8 | 9 | ## 0.3.0 10 | 11 | ### Minor Changes 12 | 13 | - d9951fa: Add missing networks 14 | 15 | ## 0.2.8 16 | 17 | ### Patch Changes 18 | 19 | - 6076dbc: Add Polygon Amoy 20 | 21 | ## 0.2.7 22 | 23 | ### Patch Changes 24 | 25 | - e358968: Deprecate Mumbai network 26 | 27 | ## 0.2.6 28 | 29 | ### Patch Changes 30 | 31 | - 064ed94: add mode and moonbase alpha networks support 32 | - 10d3cf6: lisk and immutable networks 33 | 34 | ## 0.2.5 35 | 36 | ### Patch Changes 37 | 38 | - f212198: deprecate Goerli 39 | 40 | ## 0.2.4 41 | 42 | ### Patch Changes 43 | 44 | - a80ca93: add linea sepolia support 45 | 46 | ## 0.2.3 47 | 48 | ### Patch Changes 49 | 50 | - 3ad9f00: add missing networks support 51 | 52 | ## 0.2.2 53 | 54 | ### Patch Changes 55 | 56 | - b6829cc: mantle sepolia and boba sepolia networks 57 | 58 | ## 0.2.1 59 | 60 | ### Patch Changes 61 | 62 | - 7cdf08f: Added Base and Base Goerli networks to `Network` enum 63 | 64 | ## 0.2.0 65 | 66 | ### Minor Changes 67 | 68 | - 6e93f3b: enabled typescript strict mode 69 | 70 | ## 0.1.15 71 | 72 | ### Patch Changes 73 | 74 | - 4ca0753: Update value type in Simulator types to be string or number 75 | 76 | ## 0.1.14 77 | 78 | ### Patch Changes 79 | 80 | - a6f4fb5: Repack libraries without `TenderlySolcConfig` 81 | - 7ce6a2b: Make TenderlySolcConfig dependent on SolcConfig 82 | 83 | ## 0.1.13 84 | 85 | ### Patch Changes 86 | 87 | - 23d79f0: Renamed user-agent to x-user-agent 88 | 89 | ## 0.1.12 90 | 91 | ### Patch Changes 92 | 93 | - 9f24eb7: Added @deprecated to error_messages 94 | Populate error_reason 95 | 96 | ## 0.1.11 97 | 98 | ### Patch Changes 99 | 100 | - b1ad7e8: TIC-498 `getAll` method should target v2 API 101 | 102 | ## 0.1.10 103 | 104 | ### Patch Changes 105 | 106 | - 595093b: Fixed error while performing simulated transaction with state overrides 107 | - 64e9578: Enabling external contributors to run tests in target repository context 108 | 109 | ## 0.1.9 110 | 111 | ### Patch Changes 112 | 113 | - a54bcec: Added more examples that elaborate contract verification. 114 | 115 | ## 0.1.8 116 | 117 | ### Patch Changes 118 | 119 | - 54fe4bd: [docs] update readme intro 120 | 121 | ## 0.1.7 122 | 123 | ### Patch Changes 124 | 125 | - 6b11f1b: Fixed readme example 126 | 127 | ## 0.1.6 128 | 129 | ### Patch Changes 130 | 131 | - 7f5602f: Changed npm shield for readme.md 132 | - 67b0785: Adding JSDOC for verify and simulate methods 133 | 134 | ## 0.1.5 135 | 136 | ### Patch Changes 137 | 138 | - 56c21fb: [fix] remove deprecated network enums 139 | 140 | ## 0.1.4 141 | 142 | ### Patch Changes 143 | 144 | - c09fc9a: Making helper types available from root import 145 | 146 | ## 0.1.3 147 | 148 | ### Patch Changes 149 | 150 | - 8a56132: Adding all supported networks 151 | 152 | ## 0.1.2 153 | 154 | ### Patch Changes 155 | 156 | - 5b54588: Shrinkng npm bundle by whitelisting lib and dist folders 157 | 158 | ## 0.1.1 159 | 160 | ### Patch Changes 161 | 162 | - 3cee5aa: Adding examples for simulating transactions 163 | 164 | ## 0.1.0 165 | 166 | ### Minor Changes 167 | 168 | - e1c5595: Fixed bumping version automatically 169 | 170 | ## 0.0.7 171 | 172 | ### Patch Changes 173 | 174 | - b795d39: Implemented verification and added tests. 175 | 176 | ## 0.0.6 177 | 178 | ### Patch Changes 179 | 180 | - 827ab8f: Accepting plain numbers as input for tenderly network configuration 181 | 182 | ## 0.0.5 183 | 184 | ### Patch Changes 185 | 186 | - 4481283: Adding env variables correctly 187 | - 66a8e1a: adding changeset file to release 188 | - 7fef38e: Restoring .env variables 189 | 190 | ## 0.0.2 191 | 192 | ### Patch Changes 193 | 194 | - 2b2195f: Added initial code to the public repo 195 | - 2272fc1: cleaning not used files 196 | - 0a04a8b: Switching from yarn to pnpm 197 | - dc5c8d5: Removing old yarn calls from package.json 198 | - 8555cdb: Fixing branch name inside the github workflow 199 | -------------------------------------------------------------------------------- /examples/allowAndTransfer/myTokenAbi.ts: -------------------------------------------------------------------------------- 1 | export const myTokenAbi = [ 2 | { 3 | inputs: [ 4 | { internalType: 'string', name: 'name', type: 'string' }, 5 | { internalType: 'string', name: 'symbol', type: 'string' }, 6 | ], 7 | stateMutability: 'nonpayable', 8 | type: 'constructor', 9 | }, 10 | { 11 | anonymous: false, 12 | inputs: [ 13 | { indexed: true, internalType: 'address', name: 'owner', type: 'address' }, 14 | { indexed: true, internalType: 'address', name: 'spender', type: 'address' }, 15 | { indexed: false, internalType: 'uint256', name: 'value', type: 'uint256' }, 16 | ], 17 | name: 'Approval', 18 | type: 'event', 19 | }, 20 | { 21 | anonymous: false, 22 | inputs: [ 23 | { indexed: true, internalType: 'address', name: 'from', type: 'address' }, 24 | { indexed: true, internalType: 'address', name: 'to', type: 'address' }, 25 | { indexed: false, internalType: 'uint256', name: 'value', type: 'uint256' }, 26 | ], 27 | name: 'Transfer', 28 | type: 'event', 29 | }, 30 | { 31 | inputs: [ 32 | { internalType: 'address', name: 'owner', type: 'address' }, 33 | { internalType: 'address', name: 'spender', type: 'address' }, 34 | ], 35 | name: 'allowance', 36 | outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], 37 | stateMutability: 'view', 38 | type: 'function', 39 | }, 40 | { 41 | inputs: [ 42 | { internalType: 'address', name: 'spender', type: 'address' }, 43 | { internalType: 'uint256', name: 'amount', type: 'uint256' }, 44 | ], 45 | name: 'approve', 46 | outputs: [{ internalType: 'bool', name: '', type: 'bool' }], 47 | stateMutability: 'nonpayable', 48 | type: 'function', 49 | }, 50 | { 51 | inputs: [{ internalType: 'address', name: 'account', type: 'address' }], 52 | name: 'balanceOf', 53 | outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], 54 | stateMutability: 'view', 55 | type: 'function', 56 | }, 57 | { 58 | inputs: [], 59 | name: 'decimals', 60 | outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }], 61 | stateMutability: 'view', 62 | type: 'function', 63 | }, 64 | { 65 | inputs: [ 66 | { internalType: 'address', name: 'spender', type: 'address' }, 67 | { internalType: 'uint256', name: 'subtractedValue', type: 'uint256' }, 68 | ], 69 | name: 'decreaseAllowance', 70 | outputs: [{ internalType: 'bool', name: '', type: 'bool' }], 71 | stateMutability: 'nonpayable', 72 | type: 'function', 73 | }, 74 | { 75 | inputs: [ 76 | { internalType: 'address', name: 'spender', type: 'address' }, 77 | { internalType: 'uint256', name: 'addedValue', type: 'uint256' }, 78 | ], 79 | name: 'increaseAllowance', 80 | outputs: [{ internalType: 'bool', name: '', type: 'bool' }], 81 | stateMutability: 'nonpayable', 82 | type: 'function', 83 | }, 84 | { 85 | inputs: [], 86 | name: 'name', 87 | outputs: [{ internalType: 'string', name: '', type: 'string' }], 88 | stateMutability: 'view', 89 | type: 'function', 90 | }, 91 | { 92 | inputs: [], 93 | name: 'symbol', 94 | outputs: [{ internalType: 'string', name: '', type: 'string' }], 95 | stateMutability: 'view', 96 | type: 'function', 97 | }, 98 | { 99 | inputs: [], 100 | name: 'totalSupply', 101 | outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], 102 | stateMutability: 'view', 103 | type: 'function', 104 | }, 105 | { 106 | inputs: [ 107 | { internalType: 'address', name: 'to', type: 'address' }, 108 | { internalType: 'uint256', name: 'amount', type: 'uint256' }, 109 | ], 110 | name: 'transfer', 111 | outputs: [{ internalType: 'bool', name: '', type: 'bool' }], 112 | stateMutability: 'nonpayable', 113 | type: 'function', 114 | }, 115 | { 116 | inputs: [ 117 | { internalType: 'address', name: 'from', type: 'address' }, 118 | { internalType: 'address', name: 'to', type: 'address' }, 119 | { internalType: 'uint256', name: 'amount', type: 'uint256' }, 120 | ], 121 | name: 'transferFrom', 122 | outputs: [{ internalType: 'bool', name: '', type: 'bool' }], 123 | stateMutability: 'nonpayable', 124 | type: 'function', 125 | }, 126 | ]; 127 | -------------------------------------------------------------------------------- /lib/repositories/contracts/contracts.types.ts: -------------------------------------------------------------------------------- 1 | import { Network, Path } from '../../types'; 2 | 3 | export interface Contract { 4 | address: string; 5 | network: Network; 6 | displayName?: string; 7 | tags?: string[]; 8 | } 9 | 10 | export type TenderlyContract = Contract; 11 | 12 | export interface ContractRequest { 13 | address: string; 14 | network_id: string; 15 | display_name?: string; 16 | } 17 | 18 | export type GetByParams = { 19 | tags?: string[]; 20 | displayNames?: string[]; 21 | }; 22 | 23 | export type ContractResponse = { 24 | id: string; 25 | account_type: 'contract'; 26 | contract: InternalContract; 27 | display_name: string; 28 | tags?: { 29 | tag: string; 30 | }[]; 31 | }; 32 | 33 | interface InternalContract { 34 | id: string; 35 | contract_id: string; 36 | balance: string; 37 | network_id: string; 38 | public: boolean; 39 | export: boolean; 40 | verified_by: string; 41 | verification_date: string | null; 42 | address: string; 43 | contract_name: string; 44 | ens_domain: string | null; 45 | type: string; 46 | standard: string; 47 | standards: string[]; 48 | token_data: { 49 | symbol: string; 50 | name: string; 51 | main: boolean; 52 | }; 53 | evm_version: string; 54 | compiler_version: string; 55 | optimizations_used: boolean; 56 | optimization_runs: number; 57 | libraries: null; 58 | data: { 59 | main_contract: number; 60 | contract_info: { 61 | id: number; 62 | path: string; 63 | name: string; 64 | source: string; 65 | 66 | abi: unknown[]; 67 | raw_abi: unknown[]; 68 | states: unknown[]; 69 | } | null; 70 | creation_block: number; 71 | creation_tx: string; 72 | creator_address: string; 73 | created_at: string; 74 | number_of_watches: null; 75 | language: string; 76 | in_project: boolean; 77 | number_of_files: number; 78 | }; 79 | account: { 80 | id: string; 81 | contract_id: string; 82 | balance: string; 83 | network_id: string; 84 | public: boolean; 85 | export: boolean; 86 | verified_by: string; 87 | verification_date: string | null; 88 | address: string; 89 | contract_name: string; 90 | ens_domain: string | null; 91 | type: string; 92 | standard: string; 93 | standards: string[]; 94 | evm_version: string; 95 | compiler_version: string; 96 | optimizations_used: boolean; 97 | optimization_runs: number; 98 | libraries: null; 99 | data: null; 100 | creation_block: number; 101 | creation_tx: string; 102 | creator_address: string; 103 | created_at: string; 104 | number_of_watches: null; 105 | language: string; 106 | in_project: boolean; 107 | number_of_files: number; 108 | }; 109 | project_id: string; 110 | added_by_id: string; 111 | previous_versions: unknown[]; 112 | details_visible: boolean; 113 | include_in_transaction_listing: boolean; 114 | display_name: string; 115 | account_type: string; 116 | verification_type: string; 117 | added_at: string; 118 | } 119 | 120 | export type UpdateContractRequest = { 121 | displayName?: string; 122 | appendTags?: string[]; 123 | }; 124 | 125 | export type SolidityCompilerVersions = `v${number}.${number}.${number}`; 126 | 127 | export type SolcConfig = { 128 | version: SolidityCompilerVersions; 129 | sources: Record; 130 | settings: unknown; 131 | }; 132 | 133 | export type TenderlySolcConfigLibraries = Record }>; 134 | 135 | export type VerificationRequest = { 136 | contractToVerify: string; 137 | solc: SolcConfig; 138 | config: { 139 | mode: 'private' | 'public'; 140 | }; 141 | }; 142 | 143 | export type VerificationResponse = { 144 | compilation_errors: CompilationErrorResponse[]; 145 | results: VerificationResult[]; 146 | }; 147 | 148 | export type CompilationErrorResponse = { 149 | source_location: SourceLocation; 150 | error_ype: string; 151 | component: string; 152 | message: string; 153 | formatted_message: string; 154 | }; 155 | 156 | interface SourceLocation { 157 | file: string; 158 | start: number; 159 | end: number; 160 | } 161 | 162 | interface VerificationResult { 163 | bytecode_mismatch_error: BytecodeMismatchErrorResponse; 164 | verified_contract: InternalContract; 165 | } 166 | 167 | export type BytecodeMismatchErrorResponse = { 168 | contract_id: string; 169 | expected: string; 170 | got: string; 171 | similarity: number; 172 | assumed_reason: string; 173 | }; 174 | -------------------------------------------------------------------------------- /lib/executors/Simulator.types.ts: -------------------------------------------------------------------------------- 1 | import { Web3Address } from '../types'; 2 | 3 | export type TransactionParameters = { 4 | from: Web3Address; 5 | to: Web3Address; 6 | input: string; 7 | gas: number; 8 | gas_price: string; 9 | max_fee_per_gas?: number; 10 | max_priority_fee_per_gas?: number; 11 | value: string | number; 12 | access_list?: AccessList; 13 | }; 14 | 15 | type AccessList = { 16 | value_address: string; 17 | value_storage_keys: string[]; 18 | }[]; 19 | 20 | export type SimulationRequestOverrides = { 21 | [contractAddress: Web3Address]: SimulationRequestOverride; 22 | }; 23 | 24 | export type SimulationRequestOverride = { 25 | nonce?: number; 26 | code?: string; 27 | balance?: string; 28 | state_diff?: { 29 | [storageKey: string]: string | unknown; 30 | }; 31 | }; 32 | 33 | export type SimulationRequest = { 34 | // FIXME: this should be a number, but the API expects a string 35 | network_id: string; 36 | call_args: SimulationCallArguments; 37 | block_number_or_hash: { 38 | blockNumber: number; 39 | }; 40 | overrides?: SimulationRequestOverrides | null; 41 | }; 42 | 43 | export type SimulationBundleRequest = { 44 | network_id: string; 45 | call_args: SimulationCallArguments[]; 46 | block_number_or_hash: { 47 | blockNumber: number; 48 | }; 49 | overrides?: SimulationRequestOverrides | null; 50 | }; 51 | 52 | export type SimulationCallArguments = { 53 | from: string; 54 | to: string; 55 | gas: number; 56 | gas_price?: string; 57 | max_fee_per_gas?: number; 58 | max_priority_fee_per_gas?: number; 59 | value: string | number; 60 | data: string; 61 | access_list?: AccessList; 62 | }; 63 | 64 | export type SimulationBundleDetails = { 65 | transactions: TransactionParameters[]; 66 | blockNumber: number; 67 | overrides?: SimulationParametersOverrides | null; 68 | }; 69 | 70 | export type SimulationParametersOverrides = { 71 | [contractAddress: Web3Address]: SimulationParametersOverride; 72 | }; 73 | 74 | export type SimulationParametersOverride = { 75 | nonce?: number; 76 | code?: string; 77 | balance?: string; 78 | state?: { 79 | [property: string]: unknown; 80 | }; 81 | }; 82 | 83 | export type SimulationParameters = { 84 | transaction: TransactionParameters; 85 | blockNumber: number; 86 | overrides?: SimulationParametersOverrides | null; 87 | }; 88 | 89 | export type SimulationOutput = { 90 | status?: boolean; 91 | gasUsed?: number; 92 | cumulativeGasUsed?: number; 93 | blockNumber?: number; 94 | type?: number; 95 | logsBloom?: Uint8Array; 96 | logs?: SimulateSimpleResponse_DecodedLog[]; 97 | trace?: SimulateSimpleResponse_TraceResponse[]; 98 | }; 99 | 100 | export interface SimulateBundleResponse { 101 | simulations: SimulateSimpleResponse[]; 102 | } 103 | 104 | export interface SimulateSimpleResponse { 105 | status?: boolean; 106 | gas_used?: number; 107 | cumulative_gas_used?: number; 108 | block_number?: number; 109 | type?: number; 110 | logs_bloom?: Uint8Array; 111 | logs?: SimulateSimpleResponse_DecodedLog[]; 112 | trace?: SimulateSimpleResponse_TraceResponse[]; 113 | } 114 | 115 | export type StateOverride = Record }>; 116 | 117 | export type EncodeStateRequest = { 118 | networkID: string; 119 | stateOverrides: StateOverride; 120 | }; 121 | 122 | export type EncodedStateOverride = Record>; 123 | 124 | interface SimulateSimpleResponse_DecodedLog { 125 | name?: string; 126 | anonymous?: boolean; 127 | inputs?: SimulateSimpleResponse_DecodedArgument[]; 128 | raw?: RawEvent; 129 | } 130 | 131 | interface SimulateSimpleResponse_DecodedArgument { 132 | value?: unknown; 133 | type?: string; 134 | name?: string; 135 | } 136 | 137 | export interface RawEvent { 138 | address?: string; 139 | topics: string[]; 140 | data: string; 141 | } 142 | 143 | export interface SimulateSimpleResponse_TraceResponse { 144 | type?: string; 145 | from?: string; 146 | to?: string; 147 | gas?: number; 148 | gas_used?: number; 149 | address?: string | null; 150 | balance?: number | null; 151 | refund_address?: string | null; 152 | value?: number | null; 153 | error?: string | null; 154 | /** 155 | * @deprecated Use {@link error_reason} instead 156 | */ 157 | error_messages?: string | null; 158 | error_reason?: string | null; 159 | input?: string | null; 160 | decoded_input?: SimulateSimpleResponse_DecodedArgument[]; 161 | method?: string | null; 162 | output?: string | null; 163 | decoded_output?: SimulateSimpleResponse_DecodedArgument[]; 164 | subtraces?: number; 165 | trace_address?: number[]; 166 | } 167 | -------------------------------------------------------------------------------- /test/simulator.test.ts: -------------------------------------------------------------------------------- 1 | import { Interface } from 'ethers'; 2 | import { 3 | ApiError, 4 | RawEvent, 5 | SimulationOutput, 6 | Network, 7 | Tenderly, 8 | Web3Address, 9 | getEnvironmentVariables, 10 | } from '../lib'; 11 | 12 | jest.setTimeout(60000); 13 | 14 | const tenderly = new Tenderly({ 15 | accessKey: getEnvironmentVariables().TENDERLY_ACCESS_KEY, 16 | accountName: getEnvironmentVariables().TENDERLY_ACCOUNT_NAME, 17 | projectName: getEnvironmentVariables().TENDERLY_PROJECT_NAME, 18 | network: Network.SEPOLIA, 19 | }); 20 | 21 | const counterContract = '0x93Cc0A80DE37EC4A4F97240B9807CDdfB4a19fB1'.toLowerCase() as Web3Address; 22 | const callerAddress = '0xDBcB6Db1FFEaA10cd157F985a8543261250eFA46'.toLowerCase() as Web3Address; 23 | 24 | const counterContractAbi = [ 25 | { 26 | anonymous: false, 27 | inputs: [ 28 | { 29 | indexed: false, 30 | internalType: 'string', 31 | name: 'method', 32 | type: 'string', 33 | }, 34 | { 35 | indexed: false, 36 | internalType: 'uint256', 37 | name: 'oldNumber', 38 | type: 'uint256', 39 | }, 40 | { 41 | indexed: false, 42 | internalType: 'uint256', 43 | name: 'newNumber', 44 | type: 'uint256', 45 | }, 46 | { 47 | indexed: false, 48 | internalType: 'address', 49 | name: 'caller', 50 | type: 'address', 51 | }, 52 | ], 53 | name: 'CounterChanged', 54 | type: 'event', 55 | }, 56 | { 57 | inputs: [], 58 | name: 'dec', 59 | outputs: [], 60 | stateMutability: 'nonpayable', 61 | type: 'function', 62 | }, 63 | { 64 | inputs: [], 65 | name: 'inc', 66 | outputs: [], 67 | stateMutability: 'nonpayable', 68 | type: 'function', 69 | }, 70 | { 71 | inputs: [], 72 | name: 'count', 73 | outputs: [ 74 | { 75 | internalType: 'uint256', 76 | name: '', 77 | type: 'uint256', 78 | }, 79 | ], 80 | stateMutability: 'view', 81 | type: 'function', 82 | }, 83 | { 84 | inputs: [], 85 | name: 'get', 86 | outputs: [ 87 | { 88 | internalType: 'uint256', 89 | name: '', 90 | type: 'uint256', 91 | }, 92 | ], 93 | stateMutability: 'view', 94 | type: 'function', 95 | }, 96 | ]; 97 | 98 | const counterContractAbiInterface = new Interface(counterContractAbi); 99 | 100 | beforeAll(async () => { 101 | await tenderly.contracts.add(counterContract); 102 | }); 103 | 104 | test('simulateTransaction works', async () => { 105 | const transaction = await tenderly.simulator.simulateTransaction({ 106 | transaction: { 107 | from: callerAddress, 108 | to: counterContract, 109 | gas: 20000000, 110 | gas_price: '19419609232', 111 | value: '0', 112 | input: counterContractAbiInterface.encodeFunctionData('inc', []), 113 | }, 114 | blockNumber: 3237677, 115 | }); 116 | 117 | if (!transaction?.logs || !transaction.logs[0]) { 118 | throw new Error('No logs found in transaction'); 119 | } 120 | 121 | const [eventName, previousValue, newValue, caller] = 122 | counterContractAbiInterface?.parseLog(transaction.logs[0].raw as RawEvent)?.args?.toArray() || 123 | []; 124 | 125 | expect(eventName).toBe('Increment'); 126 | expect(previousValue).toBe(BigInt(0)); 127 | expect(newValue).toBe(BigInt(1)); 128 | expect(caller.toLowerCase()).toBe(callerAddress); 129 | }); 130 | 131 | test('simulateTransaction works with overrides', async () => { 132 | const transaction = await tenderly.simulator.simulateTransaction({ 133 | transaction: { 134 | from: callerAddress, 135 | to: counterContract, 136 | gas: 20000000, 137 | gas_price: '19419609232', 138 | value: '0', 139 | input: counterContractAbiInterface.encodeFunctionData('inc', []), 140 | }, 141 | blockNumber: 3237677, 142 | overrides: { 143 | [counterContract]: { 144 | state: { 145 | count: '66', 146 | }, 147 | }, 148 | }, 149 | }); 150 | 151 | if (!transaction?.logs || !transaction.logs[0]) { 152 | throw new Error('No logs found in transaction'); 153 | } 154 | 155 | const [eventName, previousValue, newValue, caller] = 156 | counterContractAbiInterface?.parseLog(transaction.logs[0].raw as RawEvent)?.args?.toArray() || 157 | []; 158 | 159 | expect(eventName).toBe('Increment'); 160 | expect(previousValue).toBe(BigInt(66)); 161 | expect(newValue).toBe(BigInt(67)); 162 | expect(caller.toLowerCase()).toBe(callerAddress); 163 | }); 164 | 165 | test('simulateTransaction throws when block number is set to high', async () => { 166 | try { 167 | await tenderly.simulator.simulateTransaction({ 168 | transaction: { 169 | from: callerAddress, 170 | to: counterContract, 171 | gas: 20000000, 172 | gas_price: '19419609232', 173 | value: '0', 174 | input: counterContractAbiInterface.encodeFunctionData('inc', []), 175 | }, 176 | blockNumber: 999999999999, 177 | }); 178 | } catch (error) { 179 | expect(error instanceof ApiError).toBeTruthy(); 180 | // FIXME: this should be fixed on new route 181 | // expect(error.slug).toBe('invalid_transaction_simulation'); 182 | // expect(error.message).toBe('Unknown block number'); 183 | } 184 | }); 185 | 186 | test('simulateBundle works', async () => { 187 | const simulationBundle = await tenderly.simulator.simulateBundle({ 188 | transactions: [ 189 | { 190 | from: callerAddress, 191 | to: counterContract, 192 | gas: 20000000, 193 | gas_price: '19419609232', 194 | value: '0', 195 | input: counterContractAbiInterface.encodeFunctionData('inc', []), 196 | }, 197 | { 198 | from: callerAddress, 199 | to: counterContract, 200 | gas: 20000000, 201 | gas_price: '19419609232', 202 | value: '0', 203 | input: counterContractAbiInterface.encodeFunctionData('inc', []), 204 | }, 205 | ], 206 | blockNumber: 3237677, 207 | }); 208 | 209 | if (!simulationBundle || simulationBundle.length !== 2) { 210 | throw new Error('Simulation bundle is invalid'); 211 | } 212 | 213 | const firstSimulation = simulationBundle[0] as SimulationOutput; 214 | 215 | if (!firstSimulation?.logs || !firstSimulation.logs[0]) { 216 | throw new Error('No logs found in first simulation'); 217 | } 218 | 219 | const firstLog = 220 | counterContractAbiInterface 221 | ?.parseLog(firstSimulation?.logs[0].raw as RawEvent) 222 | ?.args?.toArray() || []; 223 | 224 | expect(firstLog[0]).toBe('Increment'); // method 225 | expect(firstLog[1]).toBe(BigInt(0)); // oldNumber 226 | expect(firstLog[2]).toBe(BigInt(1)); // newNumber 227 | expect(firstLog[3].toLowerCase()).toBe(callerAddress); 228 | 229 | const secondSimulation = simulationBundle[1] as SimulationOutput; 230 | 231 | if (!secondSimulation?.logs || !secondSimulation.logs[0]) { 232 | throw new Error('No logs found in second simulation'); 233 | } 234 | 235 | const secondLog = 236 | counterContractAbiInterface 237 | ?.parseLog(secondSimulation.logs[0].raw as RawEvent) 238 | ?.args.toArray() || []; 239 | 240 | expect(secondLog[0]).toBe('Increment'); // method 241 | expect(secondLog[1]).toBe(BigInt(1)); // oldNumber 242 | expect(secondLog[2]).toBe(BigInt(2)); // newNumber 243 | expect(secondLog[3].toLowerCase()).toBe(callerAddress); 244 | }); 245 | 246 | afterAll(async () => { 247 | await tenderly.contracts.remove(counterContract); 248 | }); 249 | -------------------------------------------------------------------------------- /lib/repositories/wallets/wallets.repository.ts: -------------------------------------------------------------------------------- 1 | import { Network, TenderlyConfiguration } from '../../types'; 2 | import { Repository } from '../Repository'; 3 | import { ApiClient } from '../../core/ApiClient'; 4 | import { 5 | TenderlyWallet, 6 | WalletResponse, 7 | UpdateWalletRequest, 8 | WalletRequest, 9 | Wallet, 10 | } from './wallets.types'; 11 | import { handleError, InvalidResponseError, NotFoundError } from '../../errors'; 12 | import { GetByParams } from '../contracts/contracts.types'; 13 | import { ApiClientProvider } from '../../core/ApiClientProvider'; 14 | 15 | function getContractFromResponse(contractResponse: WalletResponse): Wallet { 16 | const walletDetails = contractResponse.account || contractResponse.contract; 17 | 18 | return { 19 | address: walletDetails.address, 20 | network: Number.parseInt(walletDetails.network_id) as unknown as Network, 21 | }; 22 | } 23 | 24 | function mapWalletResponseToWalletModel(walletResponse: WalletResponse) { 25 | const retVal: TenderlyWallet = getContractFromResponse(walletResponse); 26 | 27 | if (walletResponse.tags) { 28 | retVal.tags = walletResponse.tags.map(({ tag }) => tag); 29 | } 30 | 31 | if (walletResponse.display_name) { 32 | retVal.displayName = walletResponse.display_name; 33 | } 34 | 35 | return retVal; 36 | } 37 | 38 | function mapWalletModelToWalletRequest(wallet: TenderlyWallet): WalletRequest { 39 | return { 40 | address: wallet.address, 41 | display_name: wallet.displayName || '', 42 | network_ids: [`${wallet.network}`], 43 | }; 44 | } 45 | 46 | export class WalletRepository implements Repository { 47 | private readonly apiV1: ApiClient; 48 | private readonly apiV2: ApiClient; 49 | private readonly configuration: TenderlyConfiguration; 50 | 51 | constructor({ 52 | apiProvider, 53 | configuration, 54 | }: { 55 | apiProvider: ApiClientProvider; 56 | configuration: TenderlyConfiguration; 57 | }) { 58 | this.apiV1 = apiProvider.getApiClient({ version: 'v1' }); 59 | this.apiV2 = apiProvider.getApiClient({ version: 'v2' }); 60 | this.configuration = configuration; 61 | } 62 | 63 | /** 64 | * Get a contract by address if it exists in the Tenderly's instances' project 65 | * @param address - The address of the contract 66 | * @returns The contract object in a plain format 67 | * @example 68 | * const contract = await tenderly.contracts.get('0x1234567890'); 69 | */ 70 | async get(address: string) { 71 | try { 72 | const { data } = await this.apiV2.get<{ accounts: WalletResponse[] }>( 73 | ` 74 | /accounts/${this.configuration.accountName} 75 | /projects/${this.configuration.projectName} 76 | /accounts 77 | `, 78 | { 79 | 'addresses[]': [address.toLowerCase()], 80 | 'networkIDs[]': [`${this.configuration.network}`], 81 | 'types[]': ['wallet'], 82 | }, 83 | ); 84 | 85 | if (!data?.accounts || !data.accounts[0]) { 86 | throw new NotFoundError(`Wallet with address ${address} not found`); 87 | } 88 | 89 | return mapWalletResponseToWalletModel(data.accounts[0]); 90 | } catch (error) { 91 | handleError(error); 92 | } 93 | } 94 | 95 | /** 96 | * Add a wallet to the project. 97 | * @param address - The address of the wallet 98 | * @param walletData - Values to populate the displayName 99 | * @returns The wallet object in a plain format 100 | * @example 101 | * const wallet = await tenderly.contracts.add('0x1234567890', { displayName: 'My Wallet' }); 102 | */ 103 | async add(address: string, walletData?: { displayName?: string }) { 104 | try { 105 | const { data } = await this.apiV1.post< 106 | WalletRequest & { return_existing: boolean }, 107 | WalletResponse[] 108 | >( 109 | ` 110 | /account/${this.configuration.accountName} 111 | /project/${this.configuration.projectName} 112 | /wallet 113 | `, 114 | { 115 | return_existing: true, 116 | ...mapWalletModelToWalletRequest({ 117 | address: address.toLowerCase(), 118 | network: this.configuration.network, 119 | ...walletData, 120 | }), 121 | }, 122 | ); 123 | 124 | if (!data[0]) { 125 | throw new InvalidResponseError( 126 | `Invalid response received while trying to create a wallet ${address}`, 127 | ); 128 | } 129 | 130 | return mapWalletResponseToWalletModel(data[0]); 131 | } catch (error) { 132 | handleError(error); 133 | } 134 | } 135 | 136 | /** 137 | * Remove a wallet from the Tenderly instances' project. 138 | * @param address - The address of the wallet 139 | * @returns {Promise} 140 | * @example 141 | * await tenderly.contracts.remove('0x1234567890'); 142 | */ 143 | async remove(address: string) { 144 | try { 145 | await this.apiV1.delete( 146 | ` 147 | /account/${this.configuration.accountName} 148 | /project/${this.configuration.projectName} 149 | /contracts 150 | `, 151 | { account_ids: [`eth:${this.configuration.network}:${address}`] }, 152 | ); 153 | } catch (error) { 154 | handleError(error); 155 | } 156 | } 157 | 158 | /** 159 | * Update a wallet's displayName and/or tags. 160 | * @param address - The address of the wallet 161 | * @param payload - The values to update the wallet with 162 | * @returns The wallet object in a plain format 163 | * @example 164 | * const wallet = await tenderly.contracts.update('0x1234567890', { 165 | * displayName: 'My Wallet', 166 | * appendTags: ['my-tag'] 167 | * }); 168 | * const wallet = await tenderly.contracts.update('0x1234567890', { displayName: 'My Wallet' }); 169 | * const wallet = await tenderly.contracts.update('0x1234567890', { appendTags: ['my-tag'] }); 170 | */ 171 | async update(address: string, payload: UpdateWalletRequest) { 172 | try { 173 | let promiseArray = payload.appendTags?.map(tag => 174 | this.apiV1.post( 175 | ` 176 | /account/${this.configuration.accountName} 177 | /project/${this.configuration.projectName} 178 | /tag 179 | `, 180 | { 181 | contract_ids: [`eth:${this.configuration.network}:${address}`], 182 | tag, 183 | }, 184 | ), 185 | ); 186 | 187 | promiseArray ||= []; 188 | 189 | if (payload.displayName) { 190 | promiseArray.push( 191 | this.apiV1.post( 192 | ` 193 | /account/${this.configuration.accountName} 194 | /project/${this.configuration.projectName} 195 | /contract/${this.configuration.network}/${address} 196 | /rename 197 | `, 198 | { display_name: payload.displayName }, 199 | ), 200 | ); 201 | } 202 | 203 | await Promise.all(promiseArray); 204 | 205 | return this.get(address); 206 | } catch (error) { 207 | handleError(error); 208 | } 209 | } 210 | 211 | /** 212 | * Get all wallets in the Tenderly instances' project. 213 | * 214 | */ 215 | async getAll(): Promise { 216 | try { 217 | const wallets = await this.apiV2.get<{ accounts: WalletResponse[] }>( 218 | ` 219 | /accounts/${this.configuration.accountName} 220 | /projects/${this.configuration.projectName} 221 | /accounts 222 | `, 223 | { 'types[]': 'wallet' }, 224 | ); 225 | 226 | if (wallets?.data?.accounts?.length) { 227 | return wallets.data.accounts.map(mapWalletResponseToWalletModel); 228 | } else { 229 | return []; 230 | } 231 | } catch (error) { 232 | handleError(error); 233 | } 234 | } 235 | 236 | /** 237 | * Get all wallets in the Tenderly instances' project. 238 | * @param queryObject - The query object to filter the wallets with 239 | * @returns An array of wallets in a plain format 240 | * @example 241 | * const wallets = await tenderly.contracts.getBy(); 242 | * const wallets = await tenderly.contracts.getBy({ 243 | * displayName: 'My Wallet', 244 | * tags: ['my-tag'] 245 | * }); 246 | */ 247 | async getBy(queryObject: GetByParams = {}): Promise { 248 | try { 249 | const queryParams = this.buildQueryParams(queryObject); 250 | const wallets = await this.apiV2.get<{ accounts: WalletResponse[] }>( 251 | ` 252 | /accounts/${this.configuration.accountName} 253 | /projects/${this.configuration.projectName} 254 | /accounts 255 | `, 256 | { ...queryParams, 'types[]': 'wallet' }, 257 | ); 258 | 259 | if (wallets?.data?.accounts?.length) { 260 | return wallets.data.accounts.map(mapWalletResponseToWalletModel); 261 | } else { 262 | return []; 263 | } 264 | } catch (error) { 265 | handleError(error); 266 | } 267 | } 268 | 269 | private buildQueryParams(queryObject: GetByParams = {}) { 270 | const queryParams: { [key: string]: string | string[] } = { 271 | 'networkIDs[]': `${this.configuration.network}`, 272 | }; 273 | 274 | if (queryObject.displayNames && queryObject.displayNames.filter(x => !!x).length > 0) { 275 | queryParams['display_names[]'] = queryObject.displayNames; 276 | } 277 | 278 | if (queryObject.tags && queryObject.tags.filter(x => !!x).length > 0) { 279 | queryParams['tags[]'] = queryObject.tags; 280 | } 281 | 282 | return queryParams; 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /lib/executors/Simulator.ts: -------------------------------------------------------------------------------- 1 | import { ApiClient } from '../core/ApiClient'; 2 | import { Web3Address } from '../types'; 3 | import { 4 | SimulationParameters, 5 | SimulationRequest, 6 | SimulateSimpleResponse, 7 | SimulationOutput, 8 | SimulationBundleDetails, 9 | SimulationBundleRequest, 10 | EncodeStateRequest, 11 | StateOverride, 12 | EncodedStateOverride, 13 | SimulationParametersOverrides, 14 | TransactionParameters, 15 | SimulationRequestOverrides, 16 | SimulateBundleResponse, 17 | SimulationRequestOverride, 18 | } from './Simulator.types'; 19 | import { TenderlyConfiguration } from '../types'; 20 | import { handleError, EncodingError } from '../errors'; 21 | import { ApiClientProvider } from '../core/ApiClientProvider'; 22 | import { isTenderlyAxiosError } from '../errors/Error.types'; 23 | 24 | function mapToSimulationResult(simpleSimulationResponse: SimulateSimpleResponse): SimulationOutput { 25 | return { 26 | status: simpleSimulationResponse.status, 27 | gasUsed: simpleSimulationResponse.gas_used, 28 | cumulativeGasUsed: simpleSimulationResponse.cumulative_gas_used, 29 | blockNumber: simpleSimulationResponse.block_number, 30 | type: simpleSimulationResponse.type, 31 | logsBloom: simpleSimulationResponse.logs_bloom, 32 | logs: simpleSimulationResponse.logs, 33 | trace: simpleSimulationResponse.trace?.map(trace => ({ 34 | ...trace, 35 | error_messages: trace.error_reason, 36 | })), 37 | }; 38 | } 39 | 40 | export class Simulator { 41 | private readonly apiV1: ApiClient; 42 | private readonly configuration: TenderlyConfiguration; 43 | private readonly apiV2: ApiClient; 44 | 45 | constructor({ 46 | apiProvider, 47 | configuration, 48 | }: { 49 | apiProvider: ApiClientProvider; 50 | configuration: TenderlyConfiguration; 51 | }) { 52 | this.apiV1 = apiProvider.getApiClient({ version: 'v1' }); 53 | this.apiV2 = apiProvider.getApiClient({ version: 'v2' }); 54 | this.configuration = configuration; 55 | } 56 | 57 | private mapStateOverridesToEncodeStateRequest( 58 | overrides: SimulationParametersOverrides, 59 | ): EncodeStateRequest { 60 | return { 61 | networkID: `${this.configuration.network}`, 62 | stateOverrides: Object.keys(overrides) 63 | .map(contractAddress => { 64 | const cAddress = contractAddress.toLowerCase(); 65 | return { 66 | [cAddress]: overrides[contractAddress as Web3Address]?.state, 67 | }; 68 | }) 69 | .map(addresses => { 70 | const mappedOverrides: StateOverride = {}; 71 | Object.keys(addresses).forEach(address => { 72 | mappedOverrides[address] = { value: addresses[address] as Record }; 73 | }); 74 | return mappedOverrides; 75 | }) 76 | .reduce((acc, curr) => ({ ...acc, ...curr })), 77 | }; 78 | } 79 | 80 | private mapToEncodedOverrides(stateOverrides: StateOverride): EncodedStateOverride { 81 | return Object.keys(stateOverrides) 82 | .map(address => address.toLowerCase()) 83 | .reduce((acc, curr) => { 84 | acc[curr] = stateOverrides[curr]?.value as Record; 85 | return acc; 86 | }, {} as EncodedStateOverride); 87 | } 88 | 89 | private replaceJSONOverridesWithEncodedOverrides( 90 | overrides: SimulationParameters['overrides'] | null, 91 | encodedStateOverrides: EncodedStateOverride | null, 92 | ): SimulationRequest['overrides'] | null { 93 | if (!overrides) { 94 | return null; 95 | } 96 | 97 | return Object.keys(overrides) 98 | .map(address => address.toLowerCase()) 99 | .reduce((acc, curr: Web3Address) => { 100 | const currentOverride: SimulationRequestOverride = {}; 101 | 102 | if (encodedStateOverrides && encodedStateOverrides[curr]) { 103 | currentOverride.state_diff = encodedStateOverrides[curr]; 104 | } 105 | if (overrides[curr]?.nonce) { 106 | currentOverride.nonce = overrides[curr]?.nonce; 107 | } 108 | 109 | if (overrides[curr]?.code) { 110 | currentOverride.code = overrides[curr]?.code; 111 | } 112 | 113 | if (overrides[curr]?.balance) { 114 | currentOverride.balance = overrides[curr]?.balance; 115 | } 116 | 117 | return { ...acc, [curr]: currentOverride }; 118 | }, {} as SimulationRequestOverrides); 119 | } 120 | 121 | private buildSimulationBundleRequest( 122 | transactions: TransactionParameters[], 123 | blockNumber: number, 124 | encodedOverrides?: SimulationRequestOverrides | null, 125 | ): SimulationBundleRequest { 126 | return { 127 | network_id: `${this.configuration.network}`, 128 | call_args: transactions.map(transaction => ({ 129 | from: transaction.from, 130 | to: transaction.to, 131 | gas: transaction.gas, 132 | gas_price: transaction.gas_price, 133 | max_fee_per_gas: transaction.max_fee_per_gas, 134 | max_priority_fee_per_gas: transaction.max_priority_fee_per_gas, 135 | value: transaction.value, 136 | data: transaction.input, 137 | access_list: transaction.access_list, 138 | })), 139 | block_number_or_hash: { 140 | blockNumber: blockNumber, 141 | }, 142 | overrides: encodedOverrides, 143 | }; 144 | } 145 | 146 | private buildSimpleSimulationRequest( 147 | transaction: TransactionParameters, 148 | blockNumber: number, 149 | encodedOverrides?: SimulationRequestOverrides | null, 150 | ): SimulationRequest { 151 | return { 152 | network_id: `${this.configuration.network}`, 153 | call_args: { 154 | from: transaction.from, 155 | to: transaction.to, 156 | gas: transaction.gas, 157 | gas_price: transaction.gas_price, 158 | max_fee_per_gas: transaction.max_fee_per_gas, 159 | max_priority_fee_per_gas: transaction.max_priority_fee_per_gas, 160 | value: transaction.value, 161 | data: transaction.input, 162 | access_list: transaction.access_list, 163 | }, 164 | block_number_or_hash: { 165 | blockNumber: blockNumber, 166 | }, 167 | overrides: encodedOverrides, 168 | }; 169 | } 170 | 171 | private async encodeOverrideRequest(overrides?: SimulationParametersOverrides | null) { 172 | const encodedStateOverrides = await this.encodeStateOverrides(overrides); 173 | return this.replaceJSONOverridesWithEncodedOverrides(overrides, encodedStateOverrides); 174 | } 175 | 176 | private async encodeStateOverrides(overrides?: SimulationParametersOverrides | null) { 177 | if (!overrides) { 178 | return null; 179 | } 180 | const encodingRequest = this.mapStateOverridesToEncodeStateRequest(overrides); 181 | try { 182 | const { data: encodedStates } = await this.apiV1.post< 183 | EncodeStateRequest, 184 | { stateOverrides: StateOverride } 185 | >( 186 | `/account/${this.configuration.accountName} 187 | /project/${this.configuration.projectName} 188 | /contracts/encode-states 189 | `, 190 | encodingRequest, 191 | ); 192 | return this.mapToEncodedOverrides(encodedStates.stateOverrides); 193 | } catch (error) { 194 | if (isTenderlyAxiosError(error)) { 195 | throw new EncodingError(error.response.data.error); 196 | } 197 | 198 | throw error; 199 | } 200 | } 201 | 202 | private async executeSimpleSimulationRequest( 203 | simulationRequest: SimulationRequest, 204 | ): Promise { 205 | const { data } = await this.apiV2.post( 206 | ` 207 | /account/${this.configuration.accountName} 208 | /project/${this.configuration.projectName} 209 | /simulations/simulate 210 | `, 211 | simulationRequest, 212 | ); 213 | 214 | return data; 215 | } 216 | 217 | private async executeSimulationBundleRequest( 218 | simulationRequest: SimulationBundleRequest, 219 | ): Promise { 220 | const { data } = await this.apiV2.post( 221 | ` 222 | /account/${this.configuration.accountName} 223 | /project/${this.configuration.projectName} 224 | /simulations/simulate/bundle 225 | `, 226 | simulationRequest, 227 | ); 228 | 229 | return data; 230 | } 231 | 232 | /** 233 | * Simulates a transaction by encoding overrides, building a request body, and executing a simulation request. 234 | * @async 235 | * @function 236 | * @param {SimulationParameters} simulationParams - Parameters for the transaction simulation. 237 | * @param {object} simulationParams.transaction - The transaction object to be simulated. 238 | * @param {number} simulationParams.blockNumber - The block number for the simulation. 239 | * @param {object} simulationParams.overrides - Overrides for the transaction simulation. 240 | * @returns {Promise} - A Promise that resolves to a simulation output. 241 | */ 242 | async simulateTransaction({ 243 | transaction, 244 | blockNumber, 245 | overrides, 246 | }: SimulationParameters): Promise { 247 | try { 248 | // Encode overrides if present 249 | const encodedOverrides = await this.encodeOverrideRequest(overrides); 250 | 251 | // Repackage the request body for the POST request for executing simulation 252 | const simulationRequest = this.buildSimpleSimulationRequest( 253 | transaction, 254 | blockNumber, 255 | encodedOverrides, 256 | ); 257 | 258 | // Execute the simulation 259 | const simpleSimulationResponse = await this.executeSimpleSimulationRequest(simulationRequest); 260 | 261 | // Map simulation result into a more user friendly format 262 | return mapToSimulationResult(simpleSimulationResponse); 263 | } catch (error) { 264 | handleError(error); 265 | } 266 | } 267 | 268 | /** 269 | * Simulates a bundle of transactions by encoding overrides, building a request body, 270 | * and executing a simulation bundle request. 271 | * @async 272 | * @function 273 | * @param {SimulationBundleDetails} params - Details of the transaction bundle simulation. 274 | * @param {object[]} params.transactions - An array of transaction objects to be simulated. 275 | * @param {object} params.overrides - Overrides for the transaction bundle simulation. 276 | * @param {number} params.blockNumber - The block number for the simulation bundle. 277 | * @returns {Promise} - A Promise that resolves to an array of simulation result objects. 278 | */ 279 | 280 | async simulateBundle({ transactions, overrides, blockNumber }: SimulationBundleDetails) { 281 | try { 282 | // Encode overrides if present 283 | const encodedOverrides = await this.encodeOverrideRequest(overrides); 284 | 285 | // Repackage the request body for the POST request for executing simulation bundle 286 | const simulationBundleRequest = this.buildSimulationBundleRequest( 287 | transactions, 288 | blockNumber, 289 | encodedOverrides, 290 | ); 291 | 292 | // Execute the simulation 293 | const simulationBundleResponse = await this.executeSimulationBundleRequest( 294 | simulationBundleRequest, 295 | ); 296 | 297 | // Map simulation result into a more user friendly format 298 | return simulationBundleResponse.simulations.map(simulation => 299 | mapToSimulationResult(simulation), 300 | ); 301 | } catch (error) { 302 | handleError(error); 303 | } 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /examples/simulateBundle/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import dotenv from 'dotenv'; 3 | import { Interface, parseEther } from 'ethers'; 4 | import { Network, Tenderly, TransactionParameters, getEnvironmentVariables } from '../../lib'; 5 | 6 | const fakeWardAddressEOA = '0xe2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2'; 7 | const daiOwnerEOA = '0xe58b9ee93700a616b50509c8292977fa7a0f8ce1'; 8 | const daiAddressMainnet = '0x6b175474e89094c44da98b954eedeac495271d0f'; 9 | const uniswapV3SwapRouterAddressMainnet = '0xe592427a0aece92de3edee1f18e0157c05861564'; 10 | const wethAddressMainnet = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; 11 | 12 | dotenv.config(); 13 | 14 | (async () => { 15 | const tenderly = new Tenderly({ 16 | accessKey: getEnvironmentVariables().TENDERLY_ACCESS_KEY, 17 | accountName: getEnvironmentVariables().TENDERLY_ACCOUNT_NAME, 18 | projectName: getEnvironmentVariables().TENDERLY_PROJECT_NAME, 19 | network: Network.MAINNET, 20 | }); 21 | 22 | const simulatedBundle = await tenderly.simulator.simulateBundle({ 23 | blockNumber: 0x103a957, 24 | transactions: [ 25 | // TX1: Mint 2 DAI for daiOwnerEOA. 26 | // For minting to happen, we must do a state override so fakeWardAddress EOA is considered a ward for this simulation (see overrides) 27 | mint2DaiTx(), 28 | // TX2: daiOwnerEOA approves 1 DAI to uniswapV3SwapRouterAddressMainnet 29 | approveUniswapV2RouterTx(), 30 | // TX3: Perform a uniswap swap of 1/3 ETH 31 | swapSomeDaiForWethTx(), 32 | ], 33 | overrides: { 34 | [daiAddressMainnet]: { 35 | state: { 36 | // make DAI think that fakeWardAddress is a ward for minting 37 | [`wards[${fakeWardAddressEOA}]`]: 38 | '0x0000000000000000000000000000000000000000000000000000000000000001', 39 | }, 40 | }, 41 | }, 42 | }); 43 | 44 | if (simulatedBundle === undefined) { 45 | throw new Error('Simulation returned no data'); 46 | } 47 | 48 | const totalGasUsed = simulatedBundle 49 | .map(simulation => simulation.gasUsed || 0) 50 | .reduce((total, gasUsed) => total + gasUsed); 51 | 52 | console.log('Total gas used:', totalGasUsed); 53 | 54 | simulatedBundle.forEach((simulation, idx) => { 55 | console.log( 56 | `Transaction ${idx} at block ${simulation.blockNumber}`, 57 | simulation.status ? 'success' : 'failed', 58 | ); 59 | }); 60 | })(); 61 | 62 | function mint2DaiTx(): TransactionParameters { 63 | return { 64 | from: fakeWardAddressEOA, 65 | to: daiAddressMainnet, 66 | gas: 0, 67 | gas_price: '0', 68 | value: '0', 69 | //'0x40c10f19000000000000000000000000e58b9ee93700a616b50509c8292977fa7a0f8ce10000000000000000000000000000000000000000000000001bc16d674ec80000', 70 | input: daiEthersInterface().encodeFunctionData('mint', [daiOwnerEOA, parseEther('2')]), 71 | }; 72 | } 73 | 74 | function approveUniswapV2RouterTx(): TransactionParameters { 75 | return { 76 | from: daiOwnerEOA, 77 | to: daiAddressMainnet, 78 | gas: 0, 79 | gas_price: '0', 80 | value: '0', 81 | input: daiEthersInterface().encodeFunctionData('approve', [ 82 | uniswapV3SwapRouterAddressMainnet, 83 | parseEther('1'), 84 | ]), 85 | }; 86 | } 87 | 88 | function swapSomeDaiForWethTx(): TransactionParameters { 89 | return { 90 | from: daiOwnerEOA, 91 | to: uniswapV3SwapRouterAddressMainnet, 92 | gas: 0, 93 | gas_price: '0', 94 | value: '0', 95 | input: uniswapRouterV2EthersInterface().encodeFunctionData('exactInputSingle', [ 96 | { 97 | tokenIn: daiAddressMainnet, 98 | tokenOut: wethAddressMainnet, 99 | fee: '10000', 100 | recipient: daiOwnerEOA, 101 | deadline: (1681109951 + 10 * 365 * 24 * 60 * 60 * 1000).toString(), 102 | amountIn: '33000000000000000', 103 | amountOutMinimum: '763124874493', 104 | sqrtPriceLimitX96: '0', 105 | }, 106 | ]), 107 | }; 108 | } 109 | 110 | function daiEthersInterface() { 111 | // prettier-ignore 112 | const daiAbi=[ 113 | {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',}, 114 | {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',}, 115 | {constant:false,inputs:[{internalType:'address',name:'usr',type:'address',},{internalType:'uint256',name:'wad',type:'uint256',},],name:'mint',outputs:[],payable:false,stateMutability:'nonpayable',type:'function',} 116 | ]; 117 | 118 | return new Interface(daiAbi); 119 | } 120 | 121 | function uniswapRouterV2EthersInterface() { 122 | //prettier-ignore 123 | const swapRouterAbi = [ 124 | { inputs: [ { internalType: 'address', name: '_factory', type: 'address', }, { internalType: 'address', name: '_WETH9', type: 'address', }, ], stateMutability: 'nonpayable', type: 'constructor', }, 125 | { inputs: [], name: 'WETH9', outputs: [ { internalType: 'address', name: '', type: 'address', }, ], stateMutability: 'view', type: 'function', }, 126 | { 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', }, 127 | { 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', }, 128 | { 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', }, 129 | { 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', }, 130 | { inputs: [], name: 'factory', outputs: [ { internalType: 'address', name: '', type: 'address', }, ], stateMutability: 'view', type: 'function', }, 131 | { inputs: [ { internalType: 'bytes[]', name: 'data', type: 'bytes[]', }, ], name: 'multicall', outputs: [ { internalType: 'bytes[]', name: 'results', type: 'bytes[]', }, ], stateMutability: 'payable', type: 'function', }, 132 | { inputs: [], name: 'refundETH', outputs: [], stateMutability: 'payable', type: 'function', }, 133 | { 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', }, 134 | { 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', }, 135 | { 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', }, 136 | { 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', }, 137 | { 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', }, 138 | { 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', }, 139 | { 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', } 140 | ]; 141 | 142 | return new Interface(swapRouterAbi); 143 | } 144 | -------------------------------------------------------------------------------- /examples/contractVerification/withDependencies/contracts/ERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/ERC20.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | import "./IERC20.sol"; 7 | import "./extensions/IERC20Metadata.sol"; 8 | import "../../utils/Context.sol"; 9 | 10 | /** 11 | * @dev Implementation of the {IERC20} interface. 12 | * 13 | * This implementation is agnostic to the way tokens are created. This means 14 | * that a supply mechanism has to be added in a derived contract using {_mint}. 15 | * For a generic mechanism see {ERC20PresetMinterPauser}. 16 | * 17 | * TIP: For a detailed writeup see our guide 18 | * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How 19 | * to implement supply mechanisms]. 20 | * 21 | * We have followed general OpenZeppelin Contracts guidelines: functions revert 22 | * instead returning `false` on failure. This behavior is nonetheless 23 | * conventional and does not conflict with the expectations of ERC20 24 | * applications. 25 | * 26 | * Additionally, an {Approval} event is emitted on calls to {transferFrom}. 27 | * This allows applications to reconstruct the allowance for all accounts just 28 | * by listening to said events. Other implementations of the EIP may not emit 29 | * these events, as it isn't required by the specification. 30 | * 31 | * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} 32 | * functions have been added to mitigate the well-known issues around setting 33 | * allowances. See {IERC20-approve}. 34 | */ 35 | contract ERC20 is Context, IERC20, IERC20Metadata { 36 | mapping(address => uint256) private _balances; 37 | 38 | mapping(address => mapping(address => uint256)) private _allowances; 39 | 40 | uint256 private _totalSupply; 41 | 42 | string private _name; 43 | string private _symbol; 44 | 45 | /** 46 | * @dev Sets the values for {name} and {symbol}. 47 | * 48 | * The default value of {decimals} is 18. To select a different value for 49 | * {decimals} you should overload it. 50 | * 51 | * All two of these values are immutable: they can only be set once during 52 | * construction. 53 | */ 54 | constructor(string memory name_, string memory symbol_) { 55 | _name = name_; 56 | _symbol = symbol_; 57 | } 58 | 59 | /** 60 | * @dev Returns the name of the token. 61 | */ 62 | function name() public view virtual override returns (string memory) { 63 | return _name; 64 | } 65 | 66 | /** 67 | * @dev Returns the symbol of the token, usually a shorter version of the 68 | * name. 69 | */ 70 | function symbol() public view virtual override returns (string memory) { 71 | return _symbol; 72 | } 73 | 74 | /** 75 | * @dev Returns the number of decimals used to get its user representation. 76 | * For example, if `decimals` equals `2`, a balance of `505` tokens should 77 | * be displayed to a user as `5.05` (`505 / 10 ** 2`). 78 | * 79 | * Tokens usually opt for a value of 18, imitating the relationship between 80 | * Ether and Wei. This is the value {ERC20} uses, unless this function is 81 | * overridden; 82 | * 83 | * NOTE: This information is only used for _display_ purposes: it in 84 | * no way affects any of the arithmetic of the contract, including 85 | * {IERC20-balanceOf} and {IERC20-transfer}. 86 | */ 87 | function decimals() public view virtual override returns (uint8) { 88 | return 18; 89 | } 90 | 91 | /** 92 | * @dev See {IERC20-totalSupply}. 93 | */ 94 | function totalSupply() public view virtual override returns (uint256) { 95 | return _totalSupply; 96 | } 97 | 98 | /** 99 | * @dev See {IERC20-balanceOf}. 100 | */ 101 | function balanceOf(address account) public view virtual override returns (uint256) { 102 | return _balances[account]; 103 | } 104 | 105 | /** 106 | * @dev See {IERC20-transfer}. 107 | * 108 | * Requirements: 109 | * 110 | * - `to` cannot be the zero address. 111 | * - the caller must have a balance of at least `amount`. 112 | */ 113 | function transfer(address to, uint256 amount) public virtual override returns (bool) { 114 | address owner = _msgSender(); 115 | _transfer(owner, to, amount); 116 | return true; 117 | } 118 | 119 | /** 120 | * @dev See {IERC20-allowance}. 121 | */ 122 | function allowance(address owner, address spender) public view virtual override returns (uint256) { 123 | return _allowances[owner][spender]; 124 | } 125 | 126 | /** 127 | * @dev See {IERC20-approve}. 128 | * 129 | * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on 130 | * `transferFrom`. This is semantically equivalent to an infinite approval. 131 | * 132 | * Requirements: 133 | * 134 | * - `spender` cannot be the zero address. 135 | */ 136 | function approve(address spender, uint256 amount) public virtual override returns (bool) { 137 | address owner = _msgSender(); 138 | _approve(owner, spender, amount); 139 | return true; 140 | } 141 | 142 | /** 143 | * @dev See {IERC20-transferFrom}. 144 | * 145 | * Emits an {Approval} event indicating the updated allowance. This is not 146 | * required by the EIP. See the note at the beginning of {ERC20}. 147 | * 148 | * NOTE: Does not update the allowance if the current allowance 149 | * is the maximum `uint256`. 150 | * 151 | * Requirements: 152 | * 153 | * - `from` and `to` cannot be the zero address. 154 | * - `from` must have a balance of at least `amount`. 155 | * - the caller must have allowance for ``from``'s tokens of at least 156 | * `amount`. 157 | */ 158 | function transferFrom( 159 | address from, 160 | address to, 161 | uint256 amount 162 | ) public virtual override returns (bool) { 163 | address spender = _msgSender(); 164 | _spendAllowance(from, spender, amount); 165 | _transfer(from, to, amount); 166 | return true; 167 | } 168 | 169 | /** 170 | * @dev Atomically increases the allowance granted to `spender` by the caller. 171 | * 172 | * This is an alternative to {approve} that can be used as a mitigation for 173 | * problems described in {IERC20-approve}. 174 | * 175 | * Emits an {Approval} event indicating the updated allowance. 176 | * 177 | * Requirements: 178 | * 179 | * - `spender` cannot be the zero address. 180 | */ 181 | function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { 182 | address owner = _msgSender(); 183 | _approve(owner, spender, allowance(owner, spender) + addedValue); 184 | return true; 185 | } 186 | 187 | /** 188 | * @dev Atomically decreases the allowance granted to `spender` by the caller. 189 | * 190 | * This is an alternative to {approve} that can be used as a mitigation for 191 | * problems described in {IERC20-approve}. 192 | * 193 | * Emits an {Approval} event indicating the updated allowance. 194 | * 195 | * Requirements: 196 | * 197 | * - `spender` cannot be the zero address. 198 | * - `spender` must have allowance for the caller of at least 199 | * `subtractedValue`. 200 | */ 201 | function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { 202 | address owner = _msgSender(); 203 | uint256 currentAllowance = allowance(owner, spender); 204 | require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero"); 205 | unchecked { 206 | _approve(owner, spender, currentAllowance - subtractedValue); 207 | } 208 | 209 | return true; 210 | } 211 | 212 | /** 213 | * @dev Moves `amount` of tokens from `from` to `to`. 214 | * 215 | * This internal function is equivalent to {transfer}, and can be used to 216 | * e.g. implement automatic token fees, slashing mechanisms, etc. 217 | * 218 | * Emits a {Transfer} event. 219 | * 220 | * Requirements: 221 | * 222 | * - `from` cannot be the zero address. 223 | * - `to` cannot be the zero address. 224 | * - `from` must have a balance of at least `amount`. 225 | */ 226 | function _transfer( 227 | address from, 228 | address to, 229 | uint256 amount 230 | ) internal virtual { 231 | require(from != address(0), "ERC20: transfer from the zero address"); 232 | require(to != address(0), "ERC20: transfer to the zero address"); 233 | 234 | _beforeTokenTransfer(from, to, amount); 235 | 236 | uint256 fromBalance = _balances[from]; 237 | require(fromBalance >= amount, "ERC20: transfer amount exceeds balance"); 238 | unchecked { 239 | _balances[from] = fromBalance - amount; 240 | // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by 241 | // decrementing then incrementing. 242 | _balances[to] += amount; 243 | } 244 | 245 | emit Transfer(from, to, amount); 246 | 247 | _afterTokenTransfer(from, to, amount); 248 | } 249 | 250 | /** @dev Creates `amount` tokens and assigns them to `account`, increasing 251 | * the total supply. 252 | * 253 | * Emits a {Transfer} event with `from` set to the zero address. 254 | * 255 | * Requirements: 256 | * 257 | * - `account` cannot be the zero address. 258 | */ 259 | function _mint(address account, uint256 amount) internal virtual { 260 | require(account != address(0), "ERC20: mint to the zero address"); 261 | 262 | _beforeTokenTransfer(address(0), account, amount); 263 | 264 | _totalSupply += amount; 265 | unchecked { 266 | // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above. 267 | _balances[account] += amount; 268 | } 269 | emit Transfer(address(0), account, amount); 270 | 271 | _afterTokenTransfer(address(0), account, amount); 272 | } 273 | 274 | /** 275 | * @dev Destroys `amount` tokens from `account`, reducing the 276 | * total supply. 277 | * 278 | * Emits a {Transfer} event with `to` set to the zero address. 279 | * 280 | * Requirements: 281 | * 282 | * - `account` cannot be the zero address. 283 | * - `account` must have at least `amount` tokens. 284 | */ 285 | function _burn(address account, uint256 amount) internal virtual { 286 | require(account != address(0), "ERC20: burn from the zero address"); 287 | 288 | _beforeTokenTransfer(account, address(0), amount); 289 | 290 | uint256 accountBalance = _balances[account]; 291 | require(accountBalance >= amount, "ERC20: burn amount exceeds balance"); 292 | unchecked { 293 | _balances[account] = accountBalance - amount; 294 | // Overflow not possible: amount <= accountBalance <= totalSupply. 295 | _totalSupply -= amount; 296 | } 297 | 298 | emit Transfer(account, address(0), amount); 299 | 300 | _afterTokenTransfer(account, address(0), amount); 301 | } 302 | 303 | /** 304 | * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. 305 | * 306 | * This internal function is equivalent to `approve`, and can be used to 307 | * e.g. set automatic allowances for certain subsystems, etc. 308 | * 309 | * Emits an {Approval} event. 310 | * 311 | * Requirements: 312 | * 313 | * - `owner` cannot be the zero address. 314 | * - `spender` cannot be the zero address. 315 | */ 316 | function _approve( 317 | address owner, 318 | address spender, 319 | uint256 amount 320 | ) internal virtual { 321 | require(owner != address(0), "ERC20: approve from the zero address"); 322 | require(spender != address(0), "ERC20: approve to the zero address"); 323 | 324 | _allowances[owner][spender] = amount; 325 | emit Approval(owner, spender, amount); 326 | } 327 | 328 | /** 329 | * @dev Updates `owner` s allowance for `spender` based on spent `amount`. 330 | * 331 | * Does not update the allowance amount in case of infinite allowance. 332 | * Revert if not enough allowance is available. 333 | * 334 | * Might emit an {Approval} event. 335 | */ 336 | function _spendAllowance( 337 | address owner, 338 | address spender, 339 | uint256 amount 340 | ) internal virtual { 341 | uint256 currentAllowance = allowance(owner, spender); 342 | if (currentAllowance != type(uint256).max) { 343 | require(currentAllowance >= amount, "ERC20: insufficient allowance"); 344 | unchecked { 345 | _approve(owner, spender, currentAllowance - amount); 346 | } 347 | } 348 | } 349 | 350 | /** 351 | * @dev Hook that is called before any transfer of tokens. This includes 352 | * minting and burning. 353 | * 354 | * Calling conditions: 355 | * 356 | * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens 357 | * will be transferred to `to`. 358 | * - when `from` is zero, `amount` tokens will be minted for `to`. 359 | * - when `to` is zero, `amount` of ``from``'s tokens will be burned. 360 | * - `from` and `to` are never both zero. 361 | * 362 | * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. 363 | */ 364 | function _beforeTokenTransfer( 365 | address from, 366 | address to, 367 | uint256 amount 368 | ) internal virtual {} 369 | 370 | /** 371 | * @dev Hook that is called after any transfer of tokens. This includes 372 | * minting and burning. 373 | * 374 | * Calling conditions: 375 | * 376 | * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens 377 | * has been transferred to `to`. 378 | * - when `from` is zero, `amount` tokens have been minted for `to`. 379 | * - when `to` is zero, `amount` of ``from``'s tokens have been burned. 380 | * - `from` and `to` are never both zero. 381 | * 382 | * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. 383 | */ 384 | function _afterTokenTransfer( 385 | address from, 386 | address to, 387 | uint256 amount 388 | ) internal virtual {} 389 | } 390 | -------------------------------------------------------------------------------- /lib/repositories/contracts/contracts.repository.ts: -------------------------------------------------------------------------------- 1 | import { Network, Path, TenderlyConfiguration, Web3Address } from '../../types'; 2 | import { Repository } from '../Repository'; 3 | import { ApiClient } from '../../core/ApiClient'; 4 | import { 5 | Contract, 6 | ContractRequest, 7 | ContractResponse, 8 | GetByParams, 9 | SolcConfig, 10 | TenderlyContract, 11 | TenderlySolcConfigLibraries, 12 | UpdateContractRequest, 13 | VerificationRequest, 14 | VerificationResponse, 15 | } from './contracts.types'; 16 | import { ApiClientProvider } from '../../core/ApiClientProvider'; 17 | import { 18 | handleError, 19 | NotFoundError, 20 | CompilationError, 21 | BytecodeMismatchError, 22 | UnexpectedVerificationError, 23 | } from '../../errors'; 24 | 25 | function mapContractResponseToContractModel(contractResponse: ContractResponse): TenderlyContract { 26 | const retVal: TenderlyContract = { 27 | address: contractResponse.contract.address, 28 | network: Number.parseInt(contractResponse.contract.network_id) as unknown as Network, 29 | }; 30 | 31 | if (contractResponse.display_name) { 32 | retVal.displayName = contractResponse.display_name; 33 | } 34 | 35 | if (contractResponse.tags) { 36 | retVal.tags = contractResponse.tags.map(({ tag }) => tag); 37 | } 38 | 39 | return retVal; 40 | } 41 | 42 | function mapContractModelToContractRequest(contract: TenderlyContract): ContractRequest { 43 | return { 44 | address: contract.address, 45 | network_id: `${contract.network}`, 46 | display_name: contract.displayName, 47 | }; 48 | } 49 | 50 | export class ContractRepository implements Repository { 51 | private readonly apiV1: ApiClient; 52 | private readonly apiV2: ApiClient; 53 | 54 | private readonly configuration: TenderlyConfiguration; 55 | 56 | constructor({ 57 | apiProvider, 58 | configuration, 59 | }: { 60 | apiProvider: ApiClientProvider; 61 | configuration: TenderlyConfiguration; 62 | }) { 63 | this.apiV1 = apiProvider.getApiClient({ version: 'v1' }); 64 | this.apiV2 = apiProvider.getApiClient({ version: 'v2' }); 65 | this.configuration = configuration; 66 | } 67 | 68 | /** 69 | * Get a contract by address if it exists in the Tenderly's instances' project 70 | * @param address - The address of the contract 71 | * @returns The contract object in a plain format 72 | * @example 73 | * const contract = await tenderly.contracts.get('0x1234567890'); 74 | */ 75 | async get(address: string) { 76 | try { 77 | const result = await this.apiV2.get<{ accounts: ContractResponse[] }>( 78 | ` 79 | /accounts/${this.configuration.accountName} 80 | /projects/${this.configuration.projectName} 81 | /accounts 82 | `, 83 | { 84 | 'addresses[]': [address], 85 | 'networkIDs[]': [`${this.configuration.network}`], 86 | 'types[]': ['contract', 'unverified_contract'], 87 | }, 88 | ); 89 | 90 | if (!result.data?.accounts || !result.data.accounts[0]) { 91 | throw new NotFoundError(`Contract with address ${address} not found`); 92 | } 93 | 94 | return mapContractResponseToContractModel(result.data.accounts[0]); 95 | } catch (error) { 96 | handleError(error); 97 | } 98 | } 99 | 100 | /** 101 | * Add a contract to the Tenderly's instances' project 102 | * @param address - The address of the contract 103 | * @param contractData - The data of the contract 104 | * @returns The contract object in a plain format 105 | * @example 106 | * const contract = await tenderly.contracts.add('0x1234567890'); 107 | * // or 108 | * const contract = await tenderly.contracts.add('0x1234567890', { displayName: 'MyContract' }); 109 | */ 110 | async add(address: string, contractData: { displayName?: string } = {}) { 111 | try { 112 | await this.apiV1.post( 113 | ` 114 | /account/${this.configuration.accountName} 115 | /project/${this.configuration.projectName} 116 | /address 117 | `, 118 | mapContractModelToContractRequest({ 119 | address, 120 | network: this.configuration.network, 121 | ...contractData, 122 | }), 123 | ); 124 | 125 | return this.get(address); 126 | } catch (error) { 127 | handleError(error); 128 | } 129 | } 130 | 131 | /** 132 | * Remove a contract from the Tenderly's instances' project 133 | * @param address - The address of the contract 134 | * @returns The contract object in a plain format 135 | * @example 136 | * await tenderly.contracts.remove('0x1234567890'); 137 | */ 138 | async remove(address: string) { 139 | try { 140 | await this.apiV1.delete( 141 | ` 142 | /account/${this.configuration.accountName} 143 | /project/${this.configuration.projectName} 144 | /contract/${this.configuration.network}/${address} 145 | `, 146 | ); 147 | } catch (error) { 148 | handleError(error); 149 | } 150 | } 151 | 152 | /** 153 | * Update a contract in the Tenderly's instances' project 154 | * @param address - The address of the contract 155 | * @param payload - The data of the contract 156 | * @returns The contract object in a plain format 157 | * @example 158 | * const contract = await tenderly.contracts.update('0x1234567890', { displayName: 'MyContract' }); 159 | * // or 160 | * const contract = await tenderly.contracts.update('0x1234567890', { tags: ['my-tag'] }); 161 | * // or 162 | * const contract = await tenderly.contracts.update('0x1234567890', { 163 | * displayName: 'MyContract', 164 | * appendTags: ['my-tag'] 165 | * }); 166 | * // or 167 | * const contract = await tenderly.contracts.update('0x1234567890', { appendTags: ['my-tag'] }); 168 | */ 169 | async update(address: string, payload: UpdateContractRequest) { 170 | try { 171 | let promiseArray = payload.appendTags?.map(tag => 172 | this.apiV1.post( 173 | ` 174 | /account/${this.configuration.accountName} 175 | /project/${this.configuration.projectName} 176 | /tag 177 | `, 178 | { 179 | contract_ids: [`eth:${this.configuration.network}:${address}`], 180 | tag, 181 | }, 182 | ), 183 | ); 184 | 185 | promiseArray ||= []; 186 | 187 | if (payload.displayName) { 188 | promiseArray.push( 189 | this.apiV1.post( 190 | ` 191 | /account/${this.configuration.accountName} 192 | /project/${this.configuration.projectName} 193 | /contract/${this.configuration.network}/${address} 194 | /rename 195 | `, 196 | { display_name: payload.displayName }, 197 | ), 198 | ); 199 | } 200 | 201 | await Promise.all(promiseArray); 202 | 203 | return await this.get(address); 204 | } catch (error) { 205 | handleError(error); 206 | } 207 | } 208 | 209 | async getAll(): Promise { 210 | try { 211 | const wallets = await this.apiV2.get<{ accounts: ContractResponse[] }>( 212 | ` 213 | /accounts/${this.configuration.accountName} 214 | /projects/${this.configuration.projectName} 215 | /accounts 216 | `, 217 | { 'types[]': 'contract' }, 218 | ); 219 | 220 | return wallets.data.accounts.map(mapContractResponseToContractModel); 221 | } catch (error) { 222 | handleError(error); 223 | } 224 | } 225 | 226 | /** 227 | * Get all contracts in the Tenderly's instances' project 228 | * @param queryObject - The query object 229 | * @returns The contract objects in a plain format 230 | * @example 231 | * const contracts = await tenderly.contracts.getBy(); 232 | * const contracts = await tenderly.contracts.getBy({ 233 | * tags: ['my-tag'], 234 | * displayName: ['MyContract'] 235 | * }); 236 | */ 237 | async getBy(queryObject: GetByParams = {}): Promise { 238 | try { 239 | const queryParams = this.buildQueryParams(queryObject); 240 | const contracts = await this.apiV2.get<{ accounts: ContractResponse[] }>( 241 | ` 242 | /accounts/${this.configuration.accountName} 243 | /projects/${this.configuration.projectName} 244 | /accounts 245 | `, 246 | { ...queryParams, 'types[]': 'contract' }, 247 | ); 248 | 249 | if (contracts?.data?.accounts?.length) { 250 | return contracts.data.accounts.map(mapContractResponseToContractModel); 251 | } else { 252 | return []; 253 | } 254 | } catch (error) { 255 | handleError(error); 256 | } 257 | } 258 | 259 | private buildQueryParams(queryObject: GetByParams = {}) { 260 | const queryParams: { [key: string]: string | string[] } = { 261 | 'networkIDs[]': `${this.configuration.network}`, 262 | }; 263 | 264 | if (queryObject.displayNames && queryObject.displayNames.filter(x => !!x).length > 0) { 265 | queryParams['display_names[]'] = queryObject.displayNames; 266 | } 267 | 268 | if (queryObject.tags && queryObject.tags.filter(x => !!x).length > 0) { 269 | queryParams['tags[]'] = queryObject.tags; 270 | } 271 | 272 | return queryParams; 273 | } 274 | 275 | /**Verifies a contract on Tenderly by submitting a verification request with 276 | * the provided address and verification details. 277 | * @param {string} address - The address of the contract to be verified. 278 | * @param {VerificationRequest} verificationRequest - Details of the verification request. 279 | * @returns {Promise} - A Promise that resolves to a TenderlyContract 280 | * object representing the verified contract. 281 | */ 282 | async verify( 283 | address: string, 284 | verificationRequest: VerificationRequest, 285 | ): Promise { 286 | if (!this._isFullyQualifiedContractName(verificationRequest.contractToVerify)) { 287 | throw new Error( 288 | // eslint-disable-next-line max-len 289 | `The contract name '${verificationRequest.contractToVerify}' is not a fully qualified name. Please use the fully qualified name (e.g. path/to/file.sol:ContractName)`, 290 | ); 291 | } 292 | try { 293 | const payload = { 294 | contracts: [ 295 | { 296 | compiler: this._repackLibraries(verificationRequest.solc), 297 | sources: this._mapSolcSourcesToTenderlySources(verificationRequest.solc.sources), 298 | networks: { 299 | [this.configuration.network]: { address }, 300 | }, 301 | contractToVerify: verificationRequest.contractToVerify, 302 | }, 303 | ], 304 | }; 305 | 306 | const response = await this.apiV1.post( 307 | verificationRequest.config.mode === 'private' 308 | ? // eslint-disable-next-line max-len 309 | `/accounts/${this.configuration.accountName}/projects/${this.configuration.projectName}/contracts/verify` 310 | : '/public/contracts/verify', 311 | payload, 312 | ); 313 | 314 | const verificationResp = response.data as VerificationResponse; 315 | 316 | if (verificationResp.compilation_errors) { 317 | throw new CompilationError( 318 | 'There has been a compilation error while trying to verify contracts.', 319 | verificationResp.compilation_errors, 320 | ); 321 | } 322 | if (!verificationResp.results || !verificationResp.results[0]) { 323 | throw new UnexpectedVerificationError( 324 | // eslint-disable-next-line max-len 325 | "There has been an unexpected verification error during the verification process. Please check your contract's source code and try again.", 326 | ); 327 | } 328 | if (verificationResp.results[0].bytecode_mismatch_error) { 329 | throw new BytecodeMismatchError( 330 | 'There has been a bytecode mismatch error while trying to verify contracts.', 331 | verificationResp.results[0].bytecode_mismatch_error, 332 | ); 333 | } 334 | 335 | return this.add(address); 336 | } catch (error) { 337 | handleError(error); 338 | } 339 | } 340 | 341 | _mapSolcSourcesToTenderlySources(sources: Record) { 342 | const tenderlySources: Record = {}; 343 | 344 | Object.entries(sources).forEach(([path, source]) => { 345 | tenderlySources[path] = { code: source.content }; 346 | }); 347 | 348 | return tenderlySources; 349 | } 350 | 351 | _repackLibraries(solcConfig: SolcConfig) { 352 | const tenderlySolcConfig = this._copySolcConfigToTenderlySolcConfig(solcConfig); 353 | 354 | const solcConfigSettings = solcConfig.settings as { 355 | libraries?: Record>; 356 | }; 357 | if (!solcConfigSettings.libraries) { 358 | return tenderlySolcConfig; 359 | } 360 | const libraries: TenderlySolcConfigLibraries = {}; 361 | for (const [fileName, libVal] of Object.entries(solcConfigSettings.libraries)) { 362 | for (const [libName, libAddress] of Object.entries(libVal)) { 363 | libraries[fileName] = { 364 | addresses: { 365 | ...libraries?.[fileName]?.addresses, 366 | [libName]: libAddress, 367 | }, 368 | }; 369 | } 370 | } 371 | (tenderlySolcConfig.settings as { libraries: TenderlySolcConfigLibraries }).libraries = 372 | libraries; 373 | 374 | return tenderlySolcConfig; 375 | } 376 | 377 | _isFullyQualifiedContractName(contractName: string): boolean { 378 | // Regex pattern for fully qualified contract name 379 | // matches `path/to/file.sol:ContractName` 380 | const pattern = /^(.+)\.sol:([a-zA-Z_][a-zA-Z_0-9]*)$/; 381 | 382 | // Test if the contractName string matches the pattern 383 | return pattern.test(contractName); 384 | } 385 | 386 | _copySolcConfigToTenderlySolcConfig(solcConfig: SolcConfig): Omit { 387 | // remove libraries from settings since the backend accepts a different format of libraries 388 | const { libraries: _, ...settings } = solcConfig.settings as { 389 | libraries?: unknown; 390 | }; 391 | 392 | return { 393 | version: solcConfig.version, 394 | settings: settings, 395 | }; 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /test/wallets.repository.test.ts: -------------------------------------------------------------------------------- 1 | import { Tenderly, Network, NotFoundError, getEnvironmentVariables } from '../lib'; 2 | 3 | jest.setTimeout(60000); 4 | 5 | const walletAddress = '0xDBcB6Db1FFEaA10cd157F985a8543261250eFA46'.toLowerCase(); 6 | 7 | const liquidityActivePoolWallet = '0xDf9Eb223bAFBE5c5271415C75aeCD68C21fE3D7F'.toLowerCase(); 8 | const canonicalTransactionChainWalletAddress = 9 | '0x5E4e65926BA27467555EB562121fac00D24E9dD2'.toLowerCase(); 10 | const polygonEtherBridgeWalletAddress = '0x8484Ef722627bf18ca5Ae6BcF031c23E6e922B30'.toLowerCase(); 11 | const geminiContract1WalletAddress = '0x07Ee55aA48Bb72DcC6E9D78256648910De513eca'.toLowerCase(); 12 | const someOtherWalletAddress = '0xe1f8EbeC8A5b270902C4B0fA261490698dfD33eb'.toLowerCase(); 13 | const someThirdWallet = '0xAc34758802995Da3279f038D8465E87EC9aDb24B'.toLowerCase(); 14 | const binance7WalletAddress = '0xBE0eB53F46cd790Cd13851d5EFf43D12404d33E8'.toLowerCase(); 15 | const binance8WalletAddress = '0xF977814e90dA44bFA03b6295A0616a897441aceC'.toLowerCase(); 16 | 17 | const tenderly = new Tenderly({ 18 | accessKey: getEnvironmentVariables().TENDERLY_ACCESS_KEY, 19 | accountName: getEnvironmentVariables().TENDERLY_ACCOUNT_NAME, 20 | projectName: getEnvironmentVariables().TENDERLY_PROJECT_NAME, 21 | network: Network.MAINNET, 22 | }); 23 | const getByTenderly = tenderly.with({ 24 | projectName: getEnvironmentVariables().TENDERLY_GET_BY_PROJECT_NAME, 25 | }); 26 | 27 | beforeAll(async () => { 28 | try { 29 | await Promise.all([ 30 | tenderly.wallets.add(liquidityActivePoolWallet), 31 | tenderly.wallets.add(canonicalTransactionChainWalletAddress), 32 | tenderly.wallets.add(someOtherWalletAddress), 33 | getByTenderly.wallets.add(binance7WalletAddress), 34 | getByTenderly.wallets.add(binance8WalletAddress), 35 | ]); 36 | } catch (error) { 37 | console.warn('Wallets already present!'); 38 | } 39 | }); 40 | 41 | afterAll(async () => { 42 | await Promise.all([ 43 | tenderly.wallets.add(walletAddress), 44 | tenderly.wallets.remove(liquidityActivePoolWallet), 45 | tenderly.wallets.remove(canonicalTransactionChainWalletAddress), 46 | tenderly.wallets.remove(polygonEtherBridgeWalletAddress), 47 | tenderly.wallets.remove(someOtherWalletAddress), 48 | getByTenderly.wallets.remove(binance7WalletAddress), 49 | getByTenderly.wallets.remove(binance8WalletAddress), 50 | ]); 51 | }); 52 | 53 | test('Tenderly has wallets namespace', () => { 54 | expect(tenderly.wallets).toBeDefined(); 55 | }); 56 | 57 | describe('wallets.add', () => { 58 | beforeEach(async () => { 59 | await tenderly.wallets.remove(walletAddress); 60 | }); 61 | 62 | test('successfully adds wallet', async () => { 63 | const wallet = await tenderly.wallets.add(walletAddress); 64 | 65 | expect(wallet?.address).toEqual(walletAddress); 66 | }); 67 | 68 | test('adding wallet data will successfully add with specified data', async () => { 69 | const wallet = await tenderly.wallets.add(walletAddress, { 70 | displayName: 'VB3', 71 | }); 72 | 73 | expect(wallet?.address).toEqual(walletAddress); 74 | expect(wallet?.displayName).toEqual('VB3'); 75 | // tags don't work yet 76 | // expect(wallet.tags.sort()).toEqual(['tag1', 'tag2']); 77 | }); 78 | 79 | // FIXME: We don't want to throw here, but currently that is what the API does 80 | test(`doesn't throw when adding existing wallet, and returns wallet model`, async () => { 81 | await tenderly.wallets.add(walletAddress); 82 | const existingWallet = await tenderly.wallets.add(walletAddress); 83 | expect(existingWallet?.address).toEqual(walletAddress); 84 | }); 85 | }); 86 | 87 | describe('wallets.remove', () => { 88 | test(`Doesn't throw if wallet does not exist`, async () => { 89 | await tenderly.wallets.remove(geminiContract1WalletAddress); 90 | }); 91 | 92 | // FIXME: This should not throw, but currently that is what the API does 93 | test(`doesn't throw when removing non existing wallet`, async () => { 94 | tenderly.wallets.remove('0xfake_wallet_address'); 95 | }); 96 | }); 97 | 98 | describe('wallets.get', () => { 99 | test('returns wallet if it exists', async () => { 100 | const walletResponse = await tenderly.wallets.get(someOtherWalletAddress); 101 | 102 | expect(walletResponse?.address).toEqual(someOtherWalletAddress); 103 | }); 104 | 105 | test("returns undefined value if wallet doesn't exist", async () => { 106 | try { 107 | await tenderly.wallets.get('0xfake_wallet_address'); 108 | throw new Error('Should not be here'); 109 | } catch (error) { 110 | expect(error instanceof NotFoundError).toBeTruthy(); 111 | expect((error as NotFoundError).slug).toEqual('resource_not_found'); 112 | } 113 | }); 114 | }); 115 | 116 | describe('wallets.update', () => { 117 | const tag1 = 'Tag1'; 118 | const tag2 = 'Tag2'; 119 | const displayName = 'DisplayName'; 120 | 121 | beforeEach(async () => { 122 | await tenderly.wallets.add(someThirdWallet); 123 | }); 124 | 125 | afterEach(async () => { 126 | await tenderly.wallets.remove(someThirdWallet); 127 | }); 128 | 129 | test('updates tags and display name if both are passed', async () => { 130 | const wallet = await tenderly.wallets.update(someThirdWallet, { 131 | displayName, 132 | appendTags: [tag1, tag2], 133 | }); 134 | 135 | expect(wallet?.address).toEqual(someThirdWallet); 136 | expect(wallet?.displayName).toEqual(displayName); 137 | expect(wallet?.tags?.sort()).toEqual([tag1, tag2]); 138 | }); 139 | 140 | test('updates only displayName', async () => { 141 | const wallet = await tenderly.wallets.update(someThirdWallet, { 142 | displayName, 143 | }); 144 | 145 | expect(wallet?.address).toEqual(someThirdWallet); 146 | expect(wallet?.displayName).toEqual(displayName); 147 | expect(wallet?.tags).toBeUndefined(); 148 | }); 149 | 150 | test('updates only tags', async () => { 151 | const wallet = await tenderly.wallets.update(someThirdWallet, { 152 | appendTags: [tag1, tag2], 153 | }); 154 | 155 | expect(wallet?.address).toEqual(someThirdWallet); 156 | expect(wallet?.displayName).toBeUndefined(); 157 | expect(wallet?.tags?.sort()).toEqual(expect.arrayContaining([tag1, tag2])); 158 | }); 159 | }); 160 | 161 | describe('wallets.getBy', () => { 162 | const binance7WalletDisplayName = 'Binance7'; 163 | const binance8WalletDisplayName = 'Binance8'; 164 | const tag1 = 'Tag1'; 165 | const tag2 = 'Tag2'; 166 | const tag3 = 'Tag3'; 167 | const binance7WalletTags = [tag1, tag2]; 168 | const binance8WalletTags = [tag2, tag3]; 169 | 170 | beforeAll(async () => { 171 | await Promise.all([ 172 | getByTenderly.wallets.update(binance7WalletAddress, { 173 | displayName: binance7WalletDisplayName, 174 | appendTags: binance7WalletTags, 175 | }), 176 | getByTenderly.wallets.update(binance8WalletAddress, { 177 | displayName: binance8WalletDisplayName, 178 | appendTags: binance8WalletTags, 179 | }), 180 | ]); 181 | }); 182 | 183 | describe('tags', () => { 184 | test('returns 1 wallet, when 1 tag matches (passed as 1 string, not an array)', async () => { 185 | const wallets = await getByTenderly.wallets.getBy({ tags: [tag1] }); 186 | 187 | if (!wallets) { 188 | throw new Error('Wallets are not defined'); 189 | } 190 | 191 | expect(wallets).toHaveLength(1); 192 | expect(wallets?.[0]?.address).toEqual(binance7WalletAddress); 193 | expect(wallets?.[0]?.displayName).toEqual(binance7WalletDisplayName); 194 | expect(wallets?.[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); 195 | }); 196 | 197 | test('returns 0 wallets, when no tags match', async () => { 198 | const wallets = await getByTenderly.wallets.getBy({ tags: ['Tag4'] }); 199 | 200 | expect(wallets).toHaveLength(0); 201 | }); 202 | 203 | test('returns 1 wallet, when `tag1` matches', async () => { 204 | const wallets = await getByTenderly.wallets.getBy({ tags: [tag1] }); 205 | 206 | if (!wallets) { 207 | throw new Error('Wallets are not defined'); 208 | } 209 | 210 | expect(wallets).toHaveLength(1); 211 | expect(wallets?.[0]?.address).toEqual(binance7WalletAddress); 212 | expect(wallets?.[0]?.displayName).toEqual(binance7WalletDisplayName); 213 | expect(wallets?.[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); 214 | }); 215 | 216 | test('returns 2 wallets, when `tag2` matches', async () => { 217 | const wallets = (await getByTenderly.wallets.getBy({ tags: [tag2] }))?.sort((a, b) => 218 | a.address > b.address ? 1 : -1, 219 | ); 220 | 221 | if (!wallets) { 222 | throw new Error('Wallets are not defined'); 223 | } 224 | 225 | expect(wallets).toHaveLength(2); 226 | expect(wallets?.[0]?.address).toEqual(binance7WalletAddress); 227 | expect(wallets?.[0]?.displayName).toEqual(binance7WalletDisplayName); 228 | expect(wallets?.[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); 229 | expect(wallets?.[1]?.address).toEqual(binance8WalletAddress); 230 | expect(wallets?.[1]?.displayName).toEqual(binance8WalletDisplayName); 231 | expect(wallets?.[1]?.tags?.sort()).toEqual(binance8WalletTags.sort()); 232 | }); 233 | 234 | test('returns 1 wallet, when `tag3` matches', async () => { 235 | const wallets = await getByTenderly.wallets.getBy({ tags: [tag3] }); 236 | if (!wallets) { 237 | throw new Error('Wallets are not defined'); 238 | } 239 | expect(wallets).toHaveLength(1); 240 | expect(wallets[0]?.address).toEqual(binance8WalletAddress); 241 | expect(wallets[0]?.displayName).toEqual(binance8WalletDisplayName); 242 | expect(wallets[0]?.tags?.sort()).toEqual(binance8WalletTags.sort()); 243 | }); 244 | 245 | test('returns 2 wallets, when any of 3 tags match', async () => { 246 | const wallets = (await getByTenderly.wallets.getBy({ tags: [tag1, tag2, tag3] }))?.sort( 247 | (a, b) => (a.address > b.address ? 1 : -1), 248 | ); 249 | 250 | if (!wallets) { 251 | throw new Error('Wallets are not defined'); 252 | } 253 | 254 | expect(wallets).toHaveLength(2); 255 | expect(wallets[0]?.address).toEqual(binance7WalletAddress); 256 | expect(wallets[0]?.displayName).toEqual(binance7WalletDisplayName); 257 | expect(wallets[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); 258 | expect(wallets[1]?.address).toEqual(binance8WalletAddress); 259 | expect(wallets[1]?.displayName).toEqual(binance8WalletDisplayName); 260 | expect(wallets[1]?.tags?.sort()).toEqual(binance8WalletTags.sort()); 261 | }); 262 | 263 | test("returns 2 wallets, when both tags that don't overlap are passed", async () => { 264 | const wallets = (await getByTenderly.wallets.getBy({ tags: [tag1, tag3] }))?.sort((a, b) => 265 | a.address > b.address ? 1 : -1, 266 | ); 267 | 268 | if (!wallets) { 269 | throw new Error('Wallets are not defined'); 270 | } 271 | 272 | expect(wallets).toHaveLength(2); 273 | expect(wallets[0]?.address).toEqual(binance7WalletAddress); 274 | expect(wallets[0]?.displayName).toEqual(binance7WalletDisplayName); 275 | expect(wallets[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); 276 | expect(wallets[1]?.address).toEqual(binance8WalletAddress); 277 | expect(wallets[1]?.displayName).toEqual(binance8WalletDisplayName); 278 | expect(wallets[1]?.tags?.sort()).toEqual(binance8WalletTags.sort()); 279 | }); 280 | 281 | test('returns 2 wallets, when no tags are passed', async () => { 282 | const wallets = (await getByTenderly.wallets.getBy())?.sort((a, b) => 283 | a.address > b.address ? 1 : -1, 284 | ); 285 | 286 | if (!wallets) { 287 | throw new Error('Wallets are not defined'); 288 | } 289 | 290 | expect(wallets).toHaveLength(2); 291 | expect(wallets[0]?.address).toEqual(binance7WalletAddress); 292 | expect(wallets[0]?.displayName).toEqual(binance7WalletDisplayName); 293 | expect(wallets[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); 294 | expect(wallets[1]?.address).toEqual(binance8WalletAddress); 295 | expect(wallets[1]?.displayName).toEqual(binance8WalletDisplayName); 296 | expect(wallets[1]?.tags?.sort()).toEqual(binance8WalletTags.sort()); 297 | }); 298 | 299 | test('returns 2 wallets, when empty array is passed', async () => { 300 | const wallets = (await getByTenderly.wallets.getBy({ tags: [] }))?.sort((a, b) => 301 | a.address > b.address ? 1 : -1, 302 | ); 303 | 304 | if (!wallets) { 305 | throw new Error('Wallets are not defined'); 306 | } 307 | 308 | expect(wallets).toHaveLength(2); 309 | expect(wallets[0]?.address).toEqual(binance7WalletAddress); 310 | expect(wallets[0]?.displayName).toEqual(binance7WalletDisplayName); 311 | expect(wallets[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); 312 | expect(wallets[1]?.address).toEqual(binance8WalletAddress); 313 | expect(wallets[1]?.displayName).toEqual(binance8WalletDisplayName); 314 | expect(wallets[1]?.tags?.sort()).toEqual(binance8WalletTags.sort()); 315 | }); 316 | }); 317 | 318 | describe('displayName', () => { 319 | test('returns 1 wallet, when displayName matches', async () => { 320 | const wallets = await getByTenderly.wallets.getBy({ 321 | displayNames: [binance7WalletDisplayName], 322 | }); 323 | 324 | if (!wallets) { 325 | throw new Error('Wallets are not defined'); 326 | } 327 | expect(wallets).toHaveLength(1); 328 | expect(wallets[0]?.address).toEqual(binance7WalletAddress); 329 | expect(wallets[0]?.displayName).toEqual(binance7WalletDisplayName); 330 | expect(wallets[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); 331 | }); 332 | 333 | test('returns 0 wallets, when displayName does not match', async () => { 334 | const wallets = await getByTenderly.wallets.getBy({ 335 | displayNames: ['non existing display name'], 336 | }); 337 | 338 | expect(wallets).toHaveLength(0); 339 | }); 340 | 341 | test('returns 2 contracts, when displayName is not passed', async () => { 342 | const wallets = (await getByTenderly.wallets.getBy())?.sort((a, b) => 343 | a.address > b.address ? 1 : -1, 344 | ); 345 | 346 | if (!wallets) { 347 | throw new Error('Wallets are not defined'); 348 | } 349 | 350 | expect(wallets).toHaveLength(2); 351 | expect(wallets[0]?.address).toEqual(binance7WalletAddress); 352 | expect(wallets[0]?.displayName).toEqual(binance7WalletDisplayName); 353 | expect(wallets[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); 354 | expect(wallets[1]?.address).toEqual(binance8WalletAddress); 355 | expect(wallets[1]?.displayName).toEqual(binance8WalletDisplayName); 356 | expect(wallets[1]?.tags?.sort()).toEqual(binance8WalletTags.sort()); 357 | }); 358 | 359 | test('returns 2 contracts, when both displayNames match', async () => { 360 | const wallets = ( 361 | await getByTenderly.wallets.getBy({ 362 | displayNames: [binance7WalletDisplayName, binance8WalletDisplayName], 363 | }) 364 | )?.sort((a, b) => (a.address > b.address ? 1 : -1)); 365 | 366 | if (!wallets) { 367 | throw new Error('Wallets are not defined'); 368 | } 369 | 370 | expect(wallets).toHaveLength(2); 371 | expect(wallets[0]?.address).toEqual(binance7WalletAddress); 372 | expect(wallets[0]?.displayName).toEqual(binance7WalletDisplayName); 373 | expect(wallets[0]?.tags?.sort()).toEqual(binance7WalletTags.sort()); 374 | expect(wallets[1]?.address).toEqual(binance8WalletAddress); 375 | expect(wallets[1]?.displayName).toEqual(binance8WalletDisplayName); 376 | expect(wallets[1]?.tags?.sort()).toEqual(binance8WalletTags.sort()); 377 | }); 378 | }); 379 | }); 380 | -------------------------------------------------------------------------------- /test/contracts.repository.test.ts: -------------------------------------------------------------------------------- 1 | import { Network, Tenderly, NotFoundError, getEnvironmentVariables } from '../lib'; 2 | 3 | const counterContractSource = ` 4 | // SPDX-License-Identifier: MIT 5 | pragma solidity ^0.8.18; 6 | 7 | contract CounterWithLogs { 8 | uint public count; 9 | 10 | event CounterChanged( 11 | string method, 12 | uint256 oldNumber, 13 | uint256 newNumber, 14 | address caller 15 | ); 16 | 17 | // Function to get the current count 18 | function get() public view returns (uint) { 19 | return count; 20 | } 21 | 22 | // Function to increment count by 1 23 | function inc() public { 24 | emit CounterChanged("Increment", count, count + 1, msg.sender); 25 | count += 1; 26 | } 27 | 28 | // Function to decrement count by 1 29 | function dec() public { 30 | emit CounterChanged("Decrement", count, count - 1, msg.sender); 31 | 32 | count -= 1; 33 | } 34 | } 35 | `; 36 | 37 | // const bytecodeMismatchCounterContractSource = ` 38 | // // SPDX-License-Identifier: MIT 39 | // pragma solidity ^0.8.18; 40 | // 41 | // contract CounterWithLogs { 42 | // uint public count; 43 | // 44 | // event CounterChanged( 45 | // string method, 46 | // uint256 oldNumber, 47 | // uint256 newNumber, 48 | // address caller 49 | // ); 50 | // 51 | // // Function to get the current count 52 | // function get() public view returns (uint) { 53 | // return count; 54 | // } 55 | // 56 | // // Function to increment count by 1 57 | // function inc() public { 58 | // emit CounterChanged("Increment", count, count + 1, msg.sender); 59 | // count += 1; 60 | // } 61 | // 62 | // // Function to decrement count by 1 63 | // function dec() public { 64 | // emit CounterChanged("Decrement", count, count - 1, msg.sender); 65 | // 66 | // count -= 1; 67 | // } 68 | // 69 | // // Has an additional 'inc2' function that is not in the original contract 70 | // function inc2() public { 71 | // emit CounterChanged("Increment", count, count + 2, msg.sender); 72 | // count += 2; 73 | // } 74 | // } 75 | // `; 76 | 77 | const libraryTokenContractSource = ` 78 | //SPDX-License-Identifier: UNLICENSED 79 | 80 | // Solidity files have to start with this pragma. 81 | // It will be used by the Solidity compiler to validate its version. 82 | pragma solidity 0.8.17; 83 | 84 | import "./Library.sol"; 85 | 86 | contract LibraryToken { 87 | uint public dummyToken = 1; 88 | 89 | constructor() { 90 | dummyToken = 2; 91 | } 92 | 93 | function add(uint a, uint b) public pure returns (uint) { 94 | return Library.add(a, b); 95 | } 96 | } 97 | `; 98 | 99 | const libraryContractSource = ` 100 | //SPDX-License-Identifier: UNLICENSED 101 | 102 | // Solidity files have to start with this pragma. 103 | // It will be used by the Solidity compiler to validate its version. 104 | pragma solidity 0.8.17; 105 | 106 | library Library { 107 | function add(uint a, uint b) public pure returns (uint) { 108 | return a + b; 109 | } 110 | } 111 | `; 112 | 113 | let tenderly: Tenderly; 114 | let sepoliaTenderly: Tenderly; 115 | let getByTenderly: Tenderly; 116 | 117 | jest.setTimeout(60000); 118 | 119 | const lidoContract = '0xDC24316b9AE028F1497c275EB9192a3Ea0f67022'.toLowerCase(); 120 | const counterContract = '0x8AAF9071E6C3129653B2dC39044C3B79c0bFCfBF'.toLowerCase(); 121 | const libraryTokenContract = '0xbeba0016bd2fff7c81c5877cc0fcc509760785b5'.toLowerCase(); 122 | const libraryContract = '0xcA00A6512792aa89e347c713F443b015A1006f1d'.toLowerCase(); 123 | const kittyCoreContract = '0x06012c8cf97BEaD5deAe237070F9587f8E7A266d'.toLowerCase(); 124 | const wrappedEtherContract = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'.toLowerCase(); 125 | const beaconDepositContract = '0x00000000219ab540356cBB839Cbe05303d7705Fa'.toLowerCase(); 126 | const bitDAOTreasuryContract = '0x78605Df79524164911C144801f41e9811B7DB73D'.toLowerCase(); 127 | const arbitrumBridgeContract = '0x8315177aB297bA92A06054cE80a67Ed4DBd7ed3a'.toLowerCase(); 128 | const unverifiedContract = '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae'.toLowerCase(); 129 | 130 | beforeAll(async () => { 131 | tenderly = new Tenderly({ 132 | accessKey: getEnvironmentVariables().TENDERLY_ACCESS_KEY, 133 | accountName: getEnvironmentVariables().TENDERLY_ACCOUNT_NAME, 134 | projectName: getEnvironmentVariables().TENDERLY_PROJECT_NAME, 135 | network: Network.MAINNET, 136 | }); 137 | 138 | sepoliaTenderly = tenderly.with({ network: Network.SEPOLIA }); 139 | 140 | getByTenderly = tenderly.with({ 141 | projectName: getEnvironmentVariables().TENDERLY_GET_BY_PROJECT_NAME, 142 | }); 143 | 144 | await Promise.all([ 145 | tenderly.contracts.add(kittyCoreContract), 146 | tenderly.contracts.add(unverifiedContract, { displayName: 'Unverified Contract' }), 147 | tenderly.contracts.add(wrappedEtherContract), 148 | tenderly.contracts.add(arbitrumBridgeContract), 149 | getByTenderly.contracts.add(beaconDepositContract), 150 | getByTenderly.contracts.add(bitDAOTreasuryContract), 151 | ]); 152 | }); 153 | 154 | afterAll(async () => { 155 | await Promise.all([ 156 | tenderly.contracts.remove(lidoContract), 157 | tenderly.contracts.remove(kittyCoreContract), 158 | tenderly.contracts.remove(wrappedEtherContract), 159 | tenderly.contracts.remove(arbitrumBridgeContract), 160 | getByTenderly.contracts.remove(beaconDepositContract), 161 | getByTenderly.contracts.remove(bitDAOTreasuryContract), 162 | ]); 163 | }); 164 | 165 | test('Tenderly has contracts namespace', () => { 166 | expect(tenderly.contracts).toBeDefined(); 167 | }); 168 | 169 | describe('contracts.add', () => { 170 | beforeEach(async () => { 171 | await tenderly.contracts.remove(lidoContract); 172 | }); 173 | 174 | test('successfully adds contract', async () => { 175 | const contract = await tenderly.contracts.add(lidoContract); 176 | 177 | expect(contract?.address).toEqual(lidoContract); 178 | }); 179 | 180 | test(`adding contract twice doesn't throw an error`, async () => { 181 | await tenderly.contracts.add(lidoContract); 182 | const contract = await tenderly.contracts.add(lidoContract); 183 | expect(contract?.address).toEqual(lidoContract); 184 | }); 185 | 186 | test('adding contract data will successfully add with specified data', async () => { 187 | const lidoContractResponse = await tenderly.contracts.add(lidoContract, { 188 | displayName: 'Lido', 189 | }); 190 | 191 | expect(lidoContractResponse?.address).toEqual(lidoContract); 192 | expect(lidoContractResponse?.displayName).toEqual('Lido'); 193 | // tags don't work yet 194 | // expect(lidoContractResponse.tags.sort()).toEqual(['eth2', 'staking']); 195 | }); 196 | 197 | test('returns contract, if it already exists', async () => { 198 | await tenderly.contracts.add(lidoContract); 199 | const contract = await tenderly.contracts.add(lidoContract); 200 | 201 | expect(contract?.address).toEqual(lidoContract); 202 | }); 203 | 204 | // TODO: decide whether we want to update contract if it already exists 205 | // test("doesn't update contract if it already exists", async () => { 206 | // await tenderly.contracts.add(lidoContract, { 207 | // displayName: 'NewDisplayName1', 208 | // tags: ['NewTag1'], 209 | // }); 210 | // const contract = await tenderly.contracts.add(lidoContract, { 211 | // displayName: 'NewDisplayName2', 212 | // tags: ['NewTag2'], 213 | // }); 214 | 215 | // expect(contract.address).toEqual(lidoContract); 216 | // expect(contract.displayName).toEqual('NewDisplayName1'); 217 | // // tags don't work yet 218 | // // expect(contract.tags.sort()).toEqual(['eth2', 'staking']); 219 | // }); 220 | }); 221 | 222 | describe('contracts.remove', () => { 223 | test('removes contract', async () => { 224 | try { 225 | await tenderly.contracts.remove(arbitrumBridgeContract); 226 | await tenderly.contracts.get(arbitrumBridgeContract); 227 | } catch (error) { 228 | expect(error instanceof NotFoundError).toBeTruthy(); 229 | expect((error as NotFoundError).slug).toEqual('resource_not_found'); 230 | } 231 | }); 232 | 233 | // test("returns falsy value if contract doesn't exist", async () => { 234 | // await tenderly.contracts.remove('0xfake_contract_address'); 235 | 236 | // expect(removeContractResponse).toBeFalsy(); 237 | // }); 238 | }); 239 | 240 | describe('contracts.get', () => { 241 | test('returns contract if it exists', async () => { 242 | const contract = await tenderly.contracts.get(kittyCoreContract); 243 | 244 | expect(contract?.address).toEqual(kittyCoreContract); 245 | }); 246 | 247 | test('returns unverified contract if it exists', async () => { 248 | const contract = await tenderly.contracts.get(unverifiedContract); 249 | expect(contract?.address).toEqual(unverifiedContract); 250 | }); 251 | 252 | test("throws 400 error with non_existing_contract slug if contract doesn't exist on project", async () => { 253 | try { 254 | await tenderly.contracts.get('0xfake_contract_address'); 255 | throw new Error('Should not be here'); 256 | } catch (error) { 257 | expect(error instanceof NotFoundError).toBeTruthy(); 258 | expect((error as NotFoundError).slug).toEqual('resource_not_found'); 259 | } 260 | }); 261 | }); 262 | 263 | describe('contracts.update', () => { 264 | beforeEach(async () => { 265 | await tenderly.contracts.add(wrappedEtherContract); 266 | }); 267 | 268 | afterEach(async () => { 269 | await tenderly.contracts.remove(wrappedEtherContract); 270 | }); 271 | 272 | test('updates tags and display name if both are passed', async () => { 273 | const contract = await tenderly.contracts.update(wrappedEtherContract, { 274 | displayName: 'NewDisplayName', 275 | appendTags: ['NewTag', 'NewTag2'], 276 | }); 277 | 278 | expect(contract?.address).toEqual(wrappedEtherContract); 279 | expect(contract?.displayName).toEqual('NewDisplayName'); 280 | expect(contract?.tags?.sort()).toEqual(['NewTag', 'NewTag2']); 281 | }); 282 | 283 | test('updates only displayName', async () => { 284 | const contractResponse = await tenderly.contracts.update(wrappedEtherContract, { 285 | displayName: 'NewDisplayName', 286 | }); 287 | 288 | expect(contractResponse?.address).toEqual(wrappedEtherContract); 289 | expect(contractResponse?.displayName).toEqual('NewDisplayName'); 290 | }); 291 | 292 | test('updates only tags', async () => { 293 | const contract = await tenderly.contracts.update(wrappedEtherContract, { 294 | appendTags: ['NewTag', 'NewTag2'], 295 | }); 296 | 297 | expect(contract?.address).toEqual(wrappedEtherContract); 298 | expect(contract?.tags?.sort()).toEqual(['NewTag', 'NewTag2']); 299 | expect(contract?.displayName).toBeUndefined(); 300 | }); 301 | }); 302 | 303 | describe('contracts.verify', () => { 304 | beforeAll(async () => { 305 | await sepoliaTenderly.contracts.add(counterContract); 306 | }); 307 | 308 | afterAll(async () => { 309 | await sepoliaTenderly.contracts.remove(counterContract); 310 | }); 311 | 312 | test('contracts.verify works for correct config', async () => { 313 | const result = await sepoliaTenderly.contracts.verify(counterContract, { 314 | config: { 315 | mode: 'public', // 'private' is also possible 316 | }, 317 | contractToVerify: 'Counter.sol:CounterWithLogs', 318 | solc: { 319 | version: 'v0.8.18', 320 | sources: { 321 | 'Counter.sol': { 322 | content: counterContractSource, 323 | }, 324 | }, 325 | settings: { 326 | libraries: {}, 327 | optimizer: { 328 | enabled: false, 329 | }, 330 | }, 331 | }, 332 | }); 333 | 334 | expect(result?.address).toEqual(counterContract); 335 | }); 336 | 337 | test('contracts.verify works for contract with libraries', async () => { 338 | const verifiedContract = await sepoliaTenderly.contracts.verify(libraryTokenContract, { 339 | config: { 340 | mode: 'public', // 'private' is also possible 341 | }, 342 | contractToVerify: 'LibraryToken.sol:LibraryToken', 343 | solc: { 344 | version: 'v0.8.17', 345 | sources: { 346 | 'LibraryToken.sol': { 347 | content: libraryTokenContractSource, 348 | }, 349 | 'Library.sol': { 350 | content: libraryContractSource, 351 | }, 352 | }, 353 | settings: { 354 | libraries: { 355 | 'Library.sol': { 356 | Library: libraryContract, 357 | }, 358 | }, 359 | optimizer: { 360 | enabled: true, 361 | runs: 200, 362 | }, 363 | }, 364 | }, 365 | }); 366 | 367 | expect(verifiedContract?.address).toEqual(libraryTokenContract); 368 | }); 369 | 370 | // TODO: @krunicn investigate why it fails on thrown error 371 | // test('contracts.verify fails for wrong compiler version', async () => { 372 | // try { 373 | // await sepoliaTenderly.contracts.verify(counterContract, { 374 | // config: { 375 | // mode: 'public', 376 | // }, 377 | // contractToVerify: 'Counter.sol:CounterWithLogs', 378 | // solc: { 379 | // version: 'v0.8.4', 380 | // sources: { 381 | // 'Counter.sol': { 382 | // content: counterContractSource, 383 | // }, 384 | // }, 385 | // settings: { 386 | // libraries: {}, 387 | // optimizer: { 388 | // enabled: false, 389 | // }, 390 | // }, 391 | // }, 392 | // }); 393 | // } catch (error) { 394 | // expect(error instanceof CompilationError).toBeTruthy(); 395 | // expect((error as CompilationError).slug).toEqual('compilation_error'); 396 | // } 397 | // }); 398 | // test('contracts.verify fails for bytecode mismatch error', async () => { 399 | // try { 400 | // await sepoliaTenderly.contracts.verify(counterContract, { 401 | // config: { 402 | // mode: 'public', 403 | // }, 404 | // contractToVerify: 'Counter.sol:CounterWithLogs', 405 | // solc: { 406 | // version: 'v0.8.18', 407 | // sources: { 408 | // 'Counter.sol': { 409 | // content: bytecodeMismatchCounterContractSource, 410 | // }, 411 | // }, 412 | // settings: { 413 | // libraries: {}, 414 | // optimizer: { 415 | // enabled: false, 416 | // }, 417 | // }, 418 | // }, 419 | // }); 420 | // } catch (error) { 421 | // expect(error instanceof BytecodeMismatchError).toBeTruthy(); 422 | // expect((error as BytecodeMismatchError).slug).toEqual('bytecode_mismatch_error'); 423 | // } 424 | // }); 425 | }); 426 | 427 | describe('contract.getBy', () => { 428 | const beaconDepositContractDisplayName = 'Contract1'; 429 | const bitDAOTreasuryContractDisplayName = 'Contract2'; 430 | const tag1 = 'Tag1'; 431 | const tag2 = 'Tag2'; 432 | const tag3 = 'Tag3'; 433 | const beaconDepositContractTags = [tag1, tag2]; 434 | const bitDAOTreasuryContractTags = [tag2, tag3]; 435 | 436 | beforeAll(async () => { 437 | await Promise.all([ 438 | getByTenderly.contracts.update(beaconDepositContract, { 439 | displayName: beaconDepositContractDisplayName, 440 | appendTags: beaconDepositContractTags, 441 | }), 442 | getByTenderly.contracts.update(bitDAOTreasuryContract, { 443 | displayName: bitDAOTreasuryContractDisplayName, 444 | appendTags: bitDAOTreasuryContractTags, 445 | }), 446 | ]); 447 | }); 448 | 449 | describe('tags', () => { 450 | test('returns 1 contract, when 1 tag matches (passed as 1 string, not an array)', async () => { 451 | const contracts = await getByTenderly.contracts.getBy({ tags: [tag1] }); 452 | 453 | expect(contracts).toHaveLength(1); 454 | expect(contracts?.[0]?.address).toEqual(beaconDepositContract); 455 | expect(contracts?.[0]?.displayName).toEqual(beaconDepositContractDisplayName); 456 | expect(contracts?.[0]?.tags?.sort()).toEqual(beaconDepositContractTags.sort()); 457 | }); 458 | 459 | test('returns 0 contracts, when no tags match', async () => { 460 | const contracts = await getByTenderly.contracts.getBy({ tags: ['non existing tag'] }); 461 | 462 | expect(contracts).toHaveLength(0); 463 | }); 464 | 465 | test('returns 1 contract, when `tag1` matches', async () => { 466 | const contracts = await getByTenderly.contracts.getBy({ 467 | tags: [tag1], 468 | }); 469 | 470 | expect(contracts).toHaveLength(1); 471 | expect(contracts?.[0]?.address).toEqual(beaconDepositContract); 472 | expect(contracts?.[0]?.displayName).toEqual(beaconDepositContractDisplayName); 473 | expect(contracts?.[0]?.tags?.sort()).toEqual( 474 | expect.arrayContaining(beaconDepositContractTags), 475 | ); 476 | }); 477 | 478 | test('returns 2 contracts, when `tag2` matches', async () => { 479 | const contracts = (await getByTenderly.contracts.getBy({ tags: [tag2] }))?.sort((a, b) => 480 | a.address > b.address ? 1 : -1, 481 | ); 482 | 483 | expect(contracts).toHaveLength(2); 484 | expect(contracts?.[0]?.address).toEqual(beaconDepositContract); 485 | expect(contracts?.[0]?.displayName).toEqual(beaconDepositContractDisplayName); 486 | expect(contracts?.[0]?.tags?.sort()).toEqual( 487 | expect.arrayContaining(beaconDepositContractTags), 488 | ); 489 | expect(contracts?.[1]?.address).toEqual(bitDAOTreasuryContract); 490 | expect(contracts?.[1]?.displayName).toEqual(bitDAOTreasuryContractDisplayName); 491 | expect(contracts?.[1]?.tags?.sort()).toEqual( 492 | expect.arrayContaining(bitDAOTreasuryContractTags), 493 | ); 494 | }); 495 | 496 | test('returns 1 contract, when `tag3` matches', async () => { 497 | const contracts = await getByTenderly.contracts.getBy({ tags: [tag3] }); 498 | 499 | expect(contracts).toHaveLength(1); 500 | expect(contracts?.[0]?.address).toEqual(bitDAOTreasuryContract); 501 | expect(contracts?.[0]?.displayName).toEqual(bitDAOTreasuryContractDisplayName); 502 | expect(contracts?.[0]?.tags?.sort()).toEqual( 503 | expect.arrayContaining(bitDAOTreasuryContractTags), 504 | ); 505 | }); 506 | 507 | test('returns 2 contracts, when any of 3 tags match', async () => { 508 | const contracts = (await getByTenderly.contracts.getBy({ tags: [tag1, tag2, tag3] }))?.sort( 509 | (a, b) => (a.address > b.address ? 1 : -1), 510 | ); 511 | 512 | expect(contracts).toHaveLength(2); 513 | expect(contracts?.[0]?.address).toEqual(beaconDepositContract); 514 | expect(contracts?.[0]?.displayName).toEqual(beaconDepositContractDisplayName); 515 | expect(contracts?.[0]?.tags?.sort()).toEqual( 516 | expect.arrayContaining(beaconDepositContractTags), 517 | ); 518 | expect(contracts?.[1]?.address).toEqual(bitDAOTreasuryContract); 519 | expect(contracts?.[1]?.displayName).toEqual(bitDAOTreasuryContractDisplayName); 520 | expect(contracts?.[1]?.tags?.sort()).toEqual( 521 | expect.arrayContaining(bitDAOTreasuryContractTags), 522 | ); 523 | }); 524 | 525 | test("returns 2 contracts, when both tags that don't overlap are passed", async () => { 526 | const contracts = (await getByTenderly.contracts.getBy({ tags: [tag1, tag3] }))?.sort( 527 | (a, b) => (a.address > b.address ? 1 : -1), 528 | ); 529 | 530 | expect(contracts).toHaveLength(2); 531 | expect(contracts?.[0]?.address).toEqual(beaconDepositContract); 532 | expect(contracts?.[0]?.displayName).toEqual(beaconDepositContractDisplayName); 533 | expect(contracts?.[0]?.tags?.sort()).toEqual( 534 | expect.arrayContaining(beaconDepositContractTags), 535 | ); 536 | expect(contracts?.[1]?.address).toEqual(bitDAOTreasuryContract); 537 | expect(contracts?.[1]?.displayName).toEqual(bitDAOTreasuryContractDisplayName); 538 | expect(contracts?.[1]?.tags?.sort()).toEqual( 539 | expect.arrayContaining(bitDAOTreasuryContractTags), 540 | ); 541 | }); 542 | 543 | test('returns 2 contracts, when no tags are passed', async () => { 544 | const contracts = (await getByTenderly.contracts.getBy())?.sort((a, b) => 545 | a.address > b.address ? 1 : -1, 546 | ); 547 | 548 | expect(contracts).toHaveLength(2); 549 | expect(contracts?.[0]?.address).toEqual(beaconDepositContract); 550 | expect(contracts?.[0]?.displayName).toEqual(beaconDepositContractDisplayName); 551 | expect(contracts?.[0]?.tags?.sort()).toEqual( 552 | expect.arrayContaining(beaconDepositContractTags), 553 | ); 554 | expect(contracts?.[1]?.address).toEqual(bitDAOTreasuryContract); 555 | expect(contracts?.[1]?.displayName).toEqual(bitDAOTreasuryContractDisplayName); 556 | expect(contracts?.[1]?.tags?.sort()).toEqual( 557 | expect.arrayContaining(bitDAOTreasuryContractTags), 558 | ); 559 | }); 560 | 561 | test('returns 2 contracts, when empty tags array is passed', async () => { 562 | const contracts = (await getByTenderly.contracts.getBy({ tags: [] }))?.sort((a, b) => 563 | a.address > b.address ? 1 : -1, 564 | ); 565 | 566 | expect(contracts).toHaveLength(2); 567 | expect(contracts?.[0]?.address).toEqual(beaconDepositContract); 568 | expect(contracts?.[0]?.displayName).toEqual(beaconDepositContractDisplayName); 569 | expect(contracts?.[0]?.tags?.sort()).toEqual( 570 | expect.arrayContaining(beaconDepositContractTags), 571 | ); 572 | expect(contracts?.[1]?.address).toEqual(bitDAOTreasuryContract); 573 | expect(contracts?.[1]?.displayName).toEqual(bitDAOTreasuryContractDisplayName); 574 | expect(contracts?.[1]?.tags?.sort()).toEqual( 575 | expect.arrayContaining(bitDAOTreasuryContractTags), 576 | ); 577 | }); 578 | }); 579 | 580 | describe('displayName', () => { 581 | test('returns 1 contract, when displayName matches', async () => { 582 | const contractsResponse = await getByTenderly.contracts.getBy({ 583 | displayNames: [beaconDepositContractDisplayName], 584 | }); 585 | 586 | expect(contractsResponse).toHaveLength(1); 587 | expect(contractsResponse?.[0]?.address).toEqual(beaconDepositContract); 588 | expect(contractsResponse?.[0]?.displayName).toEqual(beaconDepositContractDisplayName); 589 | expect(contractsResponse?.[0]?.tags?.sort()).toEqual(beaconDepositContractTags.sort()); 590 | }); 591 | 592 | test('returns 0 contracts, when displayName does not match', async () => { 593 | const contracts = await getByTenderly.contracts.getBy({ 594 | displayNames: ['non existing display name'], 595 | }); 596 | 597 | expect(contracts).toHaveLength(0); 598 | }); 599 | 600 | test('returns 2 contracts, when displayName is not passed', async () => { 601 | const contractsResponse = (await getByTenderly.contracts.getBy())?.sort((a, b) => 602 | a.address > b.address ? 1 : -1, 603 | ); 604 | 605 | expect(contractsResponse).toHaveLength(2); 606 | expect(contractsResponse?.[0]?.address).toEqual(beaconDepositContract); 607 | expect(contractsResponse?.[0]?.displayName).toEqual(beaconDepositContractDisplayName); 608 | expect(contractsResponse?.[0]?.tags?.sort()).toEqual(beaconDepositContractTags.sort()); 609 | expect(contractsResponse?.[1]?.address).toEqual(bitDAOTreasuryContract); 610 | expect(contractsResponse?.[1]?.displayName).toEqual(bitDAOTreasuryContractDisplayName); 611 | expect(contractsResponse?.[1]?.tags?.sort()).toEqual(bitDAOTreasuryContractTags.sort()); 612 | }); 613 | 614 | test('returns 2 contracts, when both displayNames match', async () => { 615 | const contractsResponse = ( 616 | await getByTenderly.contracts.getBy({ 617 | displayNames: [beaconDepositContractDisplayName, bitDAOTreasuryContractDisplayName], 618 | }) 619 | )?.sort((a, b) => (a.address > b.address ? 1 : -1)); 620 | 621 | expect(contractsResponse).toHaveLength(2); 622 | expect(contractsResponse?.[0]?.address).toEqual(beaconDepositContract); 623 | expect(contractsResponse?.[0]?.displayName).toEqual(beaconDepositContractDisplayName); 624 | expect(contractsResponse?.[0]?.tags?.sort()).toEqual(beaconDepositContractTags.sort()); 625 | expect(contractsResponse?.[1]?.address).toEqual(bitDAOTreasuryContract); 626 | expect(contractsResponse?.[1]?.displayName).toEqual(bitDAOTreasuryContractDisplayName); 627 | expect(contractsResponse?.[1]?.tags?.sort()).toEqual(bitDAOTreasuryContractTags.sort()); 628 | }); 629 | }); 630 | }); 631 | 632 | test('Tenderly.with() overide works', () => { 633 | const newHandle = tenderly.with({ accountName: 'newAccountName' }); 634 | expect(newHandle.configuration.accountName).toEqual('newAccountName'); 635 | }); 636 | --------------------------------------------------------------------------------