diff --git a/packages/currency/src/chains/ChainsAbstract.ts b/packages/currency/src/chains/ChainsAbstract.ts index 523ca141c1..ecc96017ef 100644 --- a/packages/currency/src/chains/ChainsAbstract.ts +++ b/packages/currency/src/chains/ChainsAbstract.ts @@ -59,4 +59,11 @@ export abstract class ChainsAbstract< public getChainName(chainId: CHAIN_ID): CHAIN_NAME | undefined { return this.chainNames.find((chainName) => this.chains[chainName].chainId === chainId); } + + /** + * Returns true is the chain is a testnet chain + */ + public isTestnet(chainName: CHAIN_NAME): boolean { + return Boolean(this.chains[chainName].testnet); + } } diff --git a/packages/currency/src/chains/btc/data/testnet.ts b/packages/currency/src/chains/btc/data/testnet.ts index 61ad4860df..db05a6d60d 100644 --- a/packages/currency/src/chains/btc/data/testnet.ts +++ b/packages/currency/src/chains/btc/data/testnet.ts @@ -1 +1,2 @@ export const chainId = '000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943'; +export const testnet = true; diff --git a/packages/currency/src/chains/evm/data/arbitrum-rinkeby.ts b/packages/currency/src/chains/evm/data/arbitrum-rinkeby.ts index dd24630b18..19da716ff9 100644 --- a/packages/currency/src/chains/evm/data/arbitrum-rinkeby.ts +++ b/packages/currency/src/chains/evm/data/arbitrum-rinkeby.ts @@ -1 +1,2 @@ export const chainId = 421611; +export const testnet = true; diff --git a/packages/currency/src/chains/evm/data/goerli.ts b/packages/currency/src/chains/evm/data/goerli.ts index 2668b32207..2f700bcfbd 100644 --- a/packages/currency/src/chains/evm/data/goerli.ts +++ b/packages/currency/src/chains/evm/data/goerli.ts @@ -2,6 +2,7 @@ import { TokenMap } from '../../../types'; import { supportedGoerliERC20 } from '../../../erc20/chains/goerli'; export const chainId = 5; +export const testnet = true; export const currencies: TokenMap = { ...supportedGoerliERC20, }; diff --git a/packages/currency/src/chains/evm/data/mumbai.ts b/packages/currency/src/chains/evm/data/mumbai.ts index 4e2cf0f738..a5b0bd6e95 100644 --- a/packages/currency/src/chains/evm/data/mumbai.ts +++ b/packages/currency/src/chains/evm/data/mumbai.ts @@ -1 +1,2 @@ export const chainId = 80001; +export const testnet = true; diff --git a/packages/currency/src/chains/evm/data/rinkeby.ts b/packages/currency/src/chains/evm/data/rinkeby.ts index 1d3610f911..aa9da936d3 100644 --- a/packages/currency/src/chains/evm/data/rinkeby.ts +++ b/packages/currency/src/chains/evm/data/rinkeby.ts @@ -3,6 +3,7 @@ import { supportedRinkebyERC20 } from '../../../erc20/chains/rinkeby'; import { supportedRinkebyERC777 } from '../../../erc777/chains/rinkeby'; export const chainId = 4; +export const testnet = true; export const currencies: TokenMap = { ...supportedRinkebyERC20, ...supportedRinkebyERC777, diff --git a/packages/currency/src/chains/near/data/near-testnet.ts b/packages/currency/src/chains/near/data/near-testnet.ts index aeffcb72cd..ebfb20c494 100644 --- a/packages/currency/src/chains/near/data/near-testnet.ts +++ b/packages/currency/src/chains/near/data/near-testnet.ts @@ -1 +1,2 @@ export const chainId = 'testnet'; +export const testnet = true; diff --git a/packages/currency/src/types.ts b/packages/currency/src/types.ts index ec6f3c3c08..af72b2fa5e 100644 --- a/packages/currency/src/types.ts +++ b/packages/currency/src/types.ts @@ -12,6 +12,7 @@ export type TokenMap = Record; */ export type Chain = { chainId: number | string; + testnet?: boolean; currencies?: TokenMap; }; diff --git a/packages/smart-contracts/README.md b/packages/smart-contracts/README.md index 5747ec767e..ef7ca47ebb 100644 --- a/packages/smart-contracts/README.md +++ b/packages/smart-contracts/README.md @@ -108,7 +108,7 @@ The deployer contract should be deployed at `0xE99Ab70a5FAE59551544FA326fA048f7B Be sure to run `yarn build:sol` before deploying the deployer or a contract. -The contracts implemented are listed in the array `create2ContractDeploymentList` in [Utils](./scripts-create2/utils.ts). +The contracts implemented are listed in the array `create2ContractDeploymentList` in [Utils](scripts-create2/utils.ts). ### Deploy the request deployer (once per chain) @@ -166,6 +166,16 @@ See `hardhat.config.ts`. yarn hardhat verify-contract-from-deployer --network ``` +### Add the contracts to Tenderly + +Once the contract has been added to the artifacts (`./src/lib/artifacts`), run the following command to synchronize +contracts with the Tenderly account. +Environment variables needed: `TENDERLY_...` (see `hardhat.config.ts`). + +```bash +yarn hardhat tenderly-monitor-contracts +``` + #### Verify the contracts manually With Hardhat (legacy) A more generic way to verify any contract by setting constructor argments manually: diff --git a/packages/smart-contracts/hardhat.config.ts b/packages/smart-contracts/hardhat.config.ts index 6f5786b301..873cc227b4 100644 --- a/packages/smart-contracts/hardhat.config.ts +++ b/packages/smart-contracts/hardhat.config.ts @@ -14,6 +14,7 @@ import { VerifyCreate2FromList } from './scripts-create2/verify'; import { deployWithCreate2FromList } from './scripts-create2/deploy'; import { NUMBER_ERRORS } from './scripts/utils'; import { networkRpcs } from '@requestnetwork/utils'; +import { tenderlyImportAll } from './scripts-create2/tenderly'; config(); @@ -185,6 +186,11 @@ export default { }, ], }, + tenderly: { + project: process.env.TENDERLY_PROJECT, + username: process.env.TENDERLY_USERNAME, + accessKey: process.env.TENDERLY_ACCESS_KEY, + }, typechain: { outDir: 'src/types', target: 'ethers-v5', @@ -272,6 +278,12 @@ task( await VerifyCreate2FromList(hre as HardhatRuntimeEnvironmentExtended); }); +task('tenderly-monitor-contracts', 'Import all contracts to a Tenderly account').setAction( + async (_args, hre) => { + await tenderlyImportAll(hre as HardhatRuntimeEnvironmentExtended); + }, +); + subtask(DEPLOYER_KEY_GUARD, 'prevent usage of the deployer master key').setAction(async () => { if (accounts && accounts[0] === process.env.DEPLOYER_MASTER_KEY) { throw new Error('The deployer master key should not be used for this action'); diff --git a/packages/smart-contracts/package.json b/packages/smart-contracts/package.json index 9d386adeea..da70c8d50a 100644 --- a/packages/smart-contracts/package.json +++ b/packages/smart-contracts/package.json @@ -69,6 +69,7 @@ "@types/chai": "4.2.21", "@types/mocha": "8.2.3", "@types/node": "16.11.7", + "axios": "0.27.2", "chai": "4.3.4", "dotenv": "10.0.0", "ethereum-waffle": "3.4.0", diff --git a/packages/smart-contracts/scripts-create2/tenderly.ts b/packages/smart-contracts/scripts-create2/tenderly.ts new file mode 100644 index 0000000000..f34a14eabd --- /dev/null +++ b/packages/smart-contracts/scripts-create2/tenderly.ts @@ -0,0 +1,109 @@ +import { HardhatRuntimeEnvironmentExtended } from './types'; +import * as artifacts from '../src/lib/artifacts'; +import { ContractArtifact } from '../src/lib'; +import { Contract } from 'ethers'; +import * as console from 'console'; +import axios from 'axios'; +import { EvmChains } from '@requestnetwork/currency'; +import { CurrencyTypes } from '@requestnetwork/types'; + +const getTenderlyAxiosInstance = (hre: HardhatRuntimeEnvironmentExtended) => { + return axios.create({ + baseURL: 'https://api.tenderly.co', + headers: { + 'X-Access-Key': hre.config.tenderly.accessKey, + }, + }); +}; + +const capitalizeFirstLetter = (string: string) => string.charAt(0).toUpperCase() + string.slice(1); + +/** + * Chains supported by Tenderly. + * Supported testnet chains are commented out. + */ +const supportedTenderlyChains: CurrencyTypes.EvmChainName[] = [ + 'arbitrum-one', + 'arbitrum-rinkeby', + 'avalanche', + 'bsc', + 'fantom', + 'goerli', + 'mainnet', + 'matic', + 'moonbeam', + 'mumbai', + 'optimism', + 'rinkeby', + 'xdai', +]; + +type TenderlyContract = { address: string; chainId: number }; + +const getTenderlyContractId = (c: TenderlyContract) => + `eth:${c.chainId}:${c.address.toLowerCase()}`; + +export const tenderlyImportAll = async (hre: HardhatRuntimeEnvironmentExtended): Promise => { + try { + const { username, project } = hre.config.tenderly; + const contracts: Record = {}; + const mainnetContracts: Set = new Set(); + const testnetContracts: Set = new Set(); + const versions: Record> = {}; + for (const artifactName in artifacts) { + const artifact = (artifacts as any)[artifactName] as ContractArtifact; + const deployments = artifact.getAllAddressesFromAllNetworks(); + for (const deployment of deployments) { + const { networkName, address, version } = deployment; + if (!supportedTenderlyChains.includes(networkName)) continue; + const chainId = EvmChains.getChainId(networkName); + const contract: TenderlyContract = { + address, + chainId, + }; + const contractId = getTenderlyContractId(contract); + contracts[contractId] = { + name: capitalizeFirstLetter(artifactName.replace(/Artifact/i, '')), + ...contract, + }; + versions[version] ??= new Set(); + versions[version].add(contractId); + (EvmChains.isTestnet(networkName) ? testnetContracts : mainnetContracts).add(contractId); + } + } + console.log(`> Retrieved ${Object.keys(contracts).length} contracts from protocol artifacts`); + + console.log(`> Syncing contracts with Tenderly...`); + const axiosInstance = getTenderlyAxiosInstance(hre); + await axiosInstance.post(`/api/v2/accounts/${username}/projects/${project}/contracts`, { + contracts: Object.values(contracts).map((contract) => ({ + address: contract.address, + display_name: contract.name, + network_id: contract.chainId.toString(), + })), + }); + console.log(' ✔ done'); + + console.log(`> Adding version tags to contracts...`); + for (const version in versions) { + await axiosInstance.post(`/api/v1/account/${username}/project/${project}/tag`, { + contract_ids: Array.from(versions[version]), + tag: `v${version}`, + }); + } + console.log(' ✔ done'); + + console.log(`> Adding mainnet/testnet tags to contracts...`); + await axiosInstance.post(`/api/v1/account/${username}/project/${project}/tag`, { + contract_ids: Array.from(mainnetContracts), + tag: 'mainnet', + }); + await axiosInstance.post(`/api/v1/account/${username}/project/${project}/tag`, { + contract_ids: Array.from(testnetContracts), + tag: 'testnet', + }); + console.log(' ✔ done'); + } catch (err) { + console.error('Error while adding contract(s) to Tenderly', err.response?.data || err); + } +}; diff --git a/packages/smart-contracts/scripts-create2/types.ts b/packages/smart-contracts/scripts-create2/types.ts index be4c33bf72..a08f64894b 100644 --- a/packages/smart-contracts/scripts-create2/types.ts +++ b/packages/smart-contracts/scripts-create2/types.ts @@ -14,6 +14,11 @@ export type HardhatRuntimeEnvironmentExtended = HardhatRuntimeEnvironment & { deployerAddress: string; gasLimit?: number; }; + tenderly: { + project: string; + username: string; + accessKey: string; + }; }; }; diff --git a/packages/smart-contracts/scripts-create2/verify.ts b/packages/smart-contracts/scripts-create2/verify.ts index a02707df12..d5f1a976ef 100644 --- a/packages/smart-contracts/scripts-create2/verify.ts +++ b/packages/smart-contracts/scripts-create2/verify.ts @@ -57,7 +57,7 @@ export async function VerifyCreate2FromList(hre: HardhatRuntimeEnvironmentExtend // Other cases to add when necessary default: throw new Error( - `The contrat ${contract} is not to be deployed using the CREATE2 scheme`, + `The contract ${contract} is not to be deployed using the CREATE2 scheme`, ); } } catch (err) { diff --git a/packages/smart-contracts/src/lib/ContractArtifact.ts b/packages/smart-contracts/src/lib/ContractArtifact.ts index 54360c4a7f..5413510acb 100644 --- a/packages/smart-contracts/src/lib/ContractArtifact.ts +++ b/packages/smart-contracts/src/lib/ContractArtifact.ts @@ -99,6 +99,31 @@ export class ContractArtifact { })); } + /** + * Retrieve all addresses for all versions for all networks + * @returns the addresses of the deployed contract and the associated network and version. + */ + getAllAddressesFromAllNetworks(): { + version: string; + address: string; + networkName: CurrencyTypes.EvmChainName; + }[] { + const deployments = []; + for (const version in this.info) { + let networkName: CurrencyTypes.EvmChainName; + for (networkName in this.info[version].deployment) { + const address = this.info[version].deployment[networkName]?.address; + if (!address) continue; + deployments.push({ + version, + address, + networkName, + }); + } + } + return deployments; + } + /** * Retrieve the block creation number from the artifact of the used version * deployed into the specified network