From 12d6ae13b80c4b609a3011f6ac55603a0444c970 Mon Sep 17 00:00:00 2001 From: defifofum Date: Wed, 6 Dec 2023 07:36:31 -0600 Subject: [PATCH 1/6] refactor: Code to support OZ 4.9.3 and solc 0.8.19 --- contracts/Lock.sol | 2 +- contracts/LockUpgradeable.sol | 2 +- package.json | 6 +++--- scripts/deploy/DeployManager.ts | 4 +++- solhint.config.js | 4 +++- yarn.lock | 18 +++++++++--------- 6 files changed, 20 insertions(+), 16 deletions(-) diff --git a/contracts/Lock.sol b/contracts/Lock.sol index f566892..cc4b431 100644 --- a/contracts/Lock.sol +++ b/contracts/Lock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.23; +pragma solidity 0.8.19; // Import this file to use console.log // import "hardhat/console.sol"; diff --git a/contracts/LockUpgradeable.sol b/contracts/LockUpgradeable.sol index 00cf2de..413c918 100644 --- a/contracts/LockUpgradeable.sol +++ b/contracts/LockUpgradeable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.23; +pragma solidity 0.8.19; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; diff --git a/package.json b/package.json index ff427d2..a3cc3cb 100644 --- a/package.json +++ b/package.json @@ -98,8 +98,8 @@ "typescript": ">=4.5.0" }, "dependencies": { - "@openzeppelin/contracts": "^5.0.0", - "@openzeppelin/contracts-upgradeable": "^5.0.0", + "@openzeppelin/contracts": "4.9.3", + "@openzeppelin/contracts-upgradeable": "4.9.3", "@openzeppelin/hardhat-upgrades": "^1.22.1" } -} +} \ No newline at end of file diff --git a/scripts/deploy/DeployManager.ts b/scripts/deploy/DeployManager.ts index 161c844..e89aa0c 100644 --- a/scripts/deploy/DeployManager.ts +++ b/scripts/deploy/DeployManager.ts @@ -344,7 +344,9 @@ export class DeployManager { async deployProxyAdmin(adminAddress: string): Promise { logger.log(`Deploying Proxy Admin`, `🚀`) const ProxyAdminFactory = (await ethers.getContractFactory('ProxyAdmin')) as ProxyAdmin__factory - const proxyAdmin = await this.deployContractFromFactory(ProxyAdminFactory, [adminAddress], { name: 'ProxyAdmin' }) + const proxyAdmin = await this.deployContractFromFactory(ProxyAdminFactory, [], { name: 'ProxyAdmin' }) + // NOTE: in OZv5, the adminAddress is passed in as the constructor argument, but I prefer the OZv4 version because of the helper read functions + await proxyAdmin.transferOwnership(adminAddress) return proxyAdmin } diff --git a/solhint.config.js b/solhint.config.js index 4f8c9af..8c23d53 100644 --- a/solhint.config.js +++ b/solhint.config.js @@ -1,7 +1,9 @@ /** * Set the Solidity compiler versions */ -const SOLC_COMPILER_VERSIONS = ['0.8.23'] +// NOTE: Setting to '0.8.19` for now as certain chains do not support the push0 opcode. +// https://medium.com/coinmonks/push0-opcode-a-significant-update-in-the-latest-solidity-version-0-8-20-ea028668028a +const SOLC_COMPILER_VERSIONS = ['0.8.19'] /** * diff --git a/yarn.lock b/yarn.lock index cc18ca7..26e9f73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -809,15 +809,15 @@ table "^6.8.0" undici "^5.14.0" -"@openzeppelin/contracts-upgradeable@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-5.0.0.tgz#859c00c55f04b6dda85b3c88bce507d65019888f" - integrity sha512-D54RHzkOKHQ8xUssPgQe2d/U92mwaiBDY7qCCVGq6VqwQjsT3KekEQ3bonev+BLP30oZ0R1U6YC8/oLpizgC5Q== - -"@openzeppelin/contracts@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.0.0.tgz#ee0e4b4564f101a5c4ee398cd4d73c0bd92b289c" - integrity sha512-bv2sdS6LKqVVMLI5+zqnNrNU/CA+6z6CmwFXm/MzmOPBRSO5reEJN7z0Gbzvs0/bv/MZZXNklubpwy3v2+azsw== +"@openzeppelin/contracts-upgradeable@4.9.3": + version "4.9.3" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.9.3.tgz#ff17a80fb945f5102571f8efecb5ce5915cc4811" + integrity sha512-jjaHAVRMrE4UuZNfDwjlLGDxTHWIOwTJS2ldnc278a0gevfXfPr8hxKEVBGFBE96kl2G3VHDZhUimw/+G3TG2A== + +"@openzeppelin/contracts@4.9.3": + version "4.9.3" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.3.tgz#00d7a8cf35a475b160b3f0293a6403c511099364" + integrity sha512-He3LieZ1pP2TNt5JbkPA4PNT9WC3gOTOlDcFGJW4Le4QKqwmiNJCRt44APfxMxvq7OugU/cqYuPcSBzOw38DAg== "@openzeppelin/hardhat-upgrades@^1.22.1": version "1.26.0" From e53ec34ec5fa840b0b69a7d65136d0eb7082467b Mon Sep 17 00:00:00 2001 From: defifofum Date: Mon, 18 Dec 2023 16:54:01 -0600 Subject: [PATCH 2/6] feat(deployer): Add retry feature --- scripts/deploy/DeployManager.ts | 59 ++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/scripts/deploy/DeployManager.ts b/scripts/deploy/DeployManager.ts index e89aa0c..8148950 100644 --- a/scripts/deploy/DeployManager.ts +++ b/scripts/deploy/DeployManager.ts @@ -84,6 +84,7 @@ export class DeployManager { private signer?: Signer baseDir: string deployedContracts: DeployedContractDetails[] = [] + maxDeployRetries: number = 20 /** * Private constructor to initialize the DeployManager class. @@ -132,6 +133,25 @@ export class DeployManager { this.signer = signer } + /** + * More accurately manage nonces for the signer. + * @returns Next nonce for the signer + */ + private async getNextNonce(): Promise { + const signer = await this.getSigner() + // Get the nonce including pending transactions + const currentNonce = await signer.getTransactionCount('pending') + return currentNonce + } + + /** + * Sets the number of retries to attempt for deployments for errors related to nonces and gas prices. + * @param retires - The number of retries to attempt + */ + setMaxDeployRetries(retires: number) { + this.maxDeployRetries = retires + } + // ----------------------------------------------------------------------------------------------- // Deployments // ----------------------------------------------------------------------------------------------- @@ -171,7 +191,40 @@ export class DeployManager { logger.log(`Balance before deployment: ${balanceBeforeInEther} ETH`, `💰`) // Deploy contract with signer if available let encodedConstructorArgs = '' - const contractInstance = await contractFactory.connect(await this.getSigner()).deploy(...params) + let contractInstance: Awaited> | undefined = undefined + let deployAttempt = 0 + + while (deployAttempt < this.maxDeployRetries) { + try { + const nextNonce = await this.getNextNonce() + logger.log(`Attempting to deploy ${name} with nonce: ${nextNonce}`, `🚀`) + contractInstance = (await contractFactory.connect(await this.getSigner()).deploy(...params, { + nonce: nextNonce, + })) as Awaited> + await contractInstance.deployed() + logger.success(`Deployed ${name} at ${contractInstance.address}`) + break // Break out of loop if successful + } catch (error: any) { + // NOTE: Handling Nonce errors here: + if (error.code === 'NONCE_EXPIRED' || error.message.includes('already known')) { + const seconds = 1 + deployAttempt++ + logger.warn( + `${deployAttempt}/${this.maxDeployRetries}: Nonce already used, retrying with a new nonce in ${seconds} seconds...` + ) + // Optionally, wait for a short period before retrying + await new Promise((resolve) => setTimeout(resolve, seconds * 1000)) + } else { + // If the error is not related to nonce, rethrow it + throw error + } + } + } + + if (!contractInstance) { + throw new Error(`Failed to deploy ${name} after ${deployAttempt} attempts.`) + } + try { encodedConstructorArgs = contractInstance.interface.encodeDeploy(params) } catch { @@ -179,9 +232,7 @@ export class DeployManager { params.pop() encodedConstructorArgs = contractInstance.interface.encodeDeploy(params) } - await contractInstance.deployed() - logger.success(`Deployed ${name} at ${contractInstance.address}`) // Save deployment details const deployedContractDetails: DeployedContractDetails = { name: name, @@ -202,7 +253,7 @@ export class DeployManager { this.deployedContracts.push(deployedContractDetails) this.saveContractsToFile() - return contractInstance as ReturnType + return contractInstance } // ----------------------------------------------------------------------------------------------- From ce3786234cad496e9ebbaea1051fc8791c5d2c86 Mon Sep 17 00:00:00 2001 From: defifofum Date: Thu, 21 Dec 2023 14:30:46 -0600 Subject: [PATCH 3/6] feat: Add support for private key deployments or mnemonic - Add DEVELOPMENT .env to be able to log outputs from sdk when not in production --- .env.example | 16 +++++++---- hardhat.config.ts | 60 ++++++++++++++++++++--------------------- hardhat/utils/logger.ts | 31 ++++++++++++++++----- src/index.ts | 8 ++++++ 4 files changed, 73 insertions(+), 42 deletions(-) diff --git a/.env.example b/.env.example index d6f65fd..161ca48 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,9 @@ -# Seed phrase used for mainnet deployments -MAINNET_MNEMONIC= -# Seed phrase used for testnet deployments -TESTNET_MNEMONIC= +# Mainnet Accounts (Mnemonic is checked first, then Private Key) +MAINNET_MNEMONIC=crouch maple syrup lunch syrup syrup syrup syrup syrup syrup syrup syrup syrup +MAINNET_PRIVATE_KEY= +# Testnet Account (Mnemonic is checked first, then Private Key) +TESTNET_MNEMONIC=crouch maple syrup lunch syrup syrup syrup syrup syrup syrup syrup syrup syrup +TESTNET_PRIVATE_KEY= # EVM Explorer API Keys (For Verification) ETHERSCAN_API_KEY= @@ -33,4 +35,8 @@ TELOS_TESTNET_RPC_URL= # Tenderly Simulation API TENDERLY_USER= TENDERLY_PROJECT= -TENDERLY_ACCESS_KEY= \ No newline at end of file +TENDERLY_ACCESS_KEY= + +## Development and Testing +DEVELOPMENT=true # Use this setting to console log outputs from the SDK +# DEVELOPMENT=false # Omit or set to false to disable console log outputs from the SDK \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index b635f1e..e925066 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,5 +1,10 @@ import { HardhatUserConfig, task, types } from 'hardhat/config' -import { HardhatRuntimeEnvironment, HttpNetworkUserConfig, SolcUserConfig } from 'hardhat/types' +import { + HardhatRuntimeEnvironment, + HttpNetworkAccountsUserConfig, + HttpNetworkUserConfig, + SolcUserConfig, +} from 'hardhat/types' import { TASK_TEST } from 'hardhat/builtin-tasks/task-names' // Plugins import '@nomicfoundation/hardhat-toolbox' @@ -39,8 +44,15 @@ task(TASK_TEST, '🫶 Test Task') .addOptionalParam('blockNumber', 'Optional block number to fork in case of running fork tests.', undefined, types.int) .setAction(testRunner) -export const mainnetMnemonic = getEnv('MAINNET_MNEMONIC') -export const testnetMnemonic = getEnv('TESTNET_MNEMONIC') +const mainnetMnemonic = getEnv('MAINNET_MNEMONIC') +const mainnetAccounts: HttpNetworkAccountsUserConfig = mainnetMnemonic + ? { mnemonic: mainnetMnemonic } + : [getEnv('MAINNET_PRIVATE_KEY')] // Fallback to private key + +const testnetMnemonic = getEnv('TESTNET_MNEMONIC') +const testnetAccounts: HttpNetworkAccountsUserConfig = testnetMnemonic + ? { mnemonic: testnetMnemonic } + : [getEnv('TESTNET_PRIVATE_KEY')] // Fallback to private key type ExtendedNetworkOptions = { getExplorerUrl: (address: string) => string @@ -58,65 +70,49 @@ const networkConfig: ExtendedHardhatNetworkConfig = { url: getEnv('MAINNET_RPC_URL') || 'https://eth.llamarpc.com', getExplorerUrl: (address: string) => `https://etherscan.io/address/${address}`, chainId: 1, - accounts: { - mnemonic: mainnetMnemonic, - }, + accounts: mainnetAccounts, }, goerli: { url: getEnv('GOERLI_RPC_URL') || '', getExplorerUrl: (address: string) => `https://goerli.etherscan.io/address/${address}`, chainId: 5, - accounts: { - mnemonic: testnetMnemonic, - }, + accounts: testnetAccounts, }, arbitrum: { url: getEnv('ARBITRUM_RPC_URL') || 'https://endpoints.omniatech.io/v1/arbitrum/one/public ', getExplorerUrl: (address: string) => `https://arbiscan.io/address/${address}`, chainId: 42161, - accounts: { - mnemonic: mainnetMnemonic, - }, + accounts: mainnetAccounts, }, arbitrumGoerli: { url: getEnv('ARBITRUM_GOERLI_RPC_URL') || '', getExplorerUrl: (address: string) => `https://testnet.arbiscan.io/address/${address}`, chainId: 421613, - accounts: { - mnemonic: testnetMnemonic, - }, + accounts: testnetAccounts, }, bsc: { url: getEnv('BSC_RPC_URL') || 'https://bsc-dataseed1.binance.org', getExplorerUrl: (address: string) => `https://bscscan.com/address/${address}`, chainId: 56, - accounts: { - mnemonic: mainnetMnemonic, - }, + accounts: mainnetAccounts, }, bscTestnet: { url: getEnv('BSC_TESTNET_RPC_URL') || 'https://data-seed-prebsc-1-s1.binance.org:8545', getExplorerUrl: (address: string) => `https://testnet.bscscan.com/address/${address}`, chainId: 97, - accounts: { - mnemonic: testnetMnemonic, - }, + accounts: testnetAccounts, }, polygon: { - url: getEnv('POLYGON_RPC_URL') || 'https://matic-mainnet.chainstacklabs.com', + url: getEnv('POLYGON_RPC_URL') || 'https://polygon.llamarpc.com', getExplorerUrl: (address: string) => `https://polygonscan.com/address/${address}`, chainId: 137, - accounts: { - mnemonic: mainnetMnemonic, - }, + accounts: mainnetAccounts, }, polygonTestnet: { url: getEnv('POLYGON_TESTNET_RPC_URL') || 'https://rpc-mumbai.maticvigil.com/', getExplorerUrl: (address: string) => `https://mumbai.polygonscan.com/address/${address}`, chainId: 80001, - accounts: { - mnemonic: testnetMnemonic, - }, + accounts: testnetAccounts, }, // Placeholder for the configuration below. hardhat: { @@ -128,6 +124,10 @@ export function getExplorerUrlForNetwork(networkName: Networks) { return networkConfig[networkName]?.getExplorerUrl } +export function convertToExplorerUrlForNetwork(networkName: Networks, address: string) { + return getExplorerUrlForNetwork(networkName)(address) +} + /** * Configure compiler versions in ./solhint.config.js * @@ -174,9 +174,7 @@ const config: HardhatUserConfig = { typechain: { // outDir: 'src/types', // defaults to './typechain-types/' target: 'ethers-v5', - externalArtifacts: [ - // './artifacts-custom/**/*.json' - ], // optional array of glob patterns with external artifacts to process (for example external libs from node_modules) + externalArtifacts: ['./artifacts-external/**/*.json'], // optional array of glob patterns with external artifacts to process (for example external libs from node_modules) alwaysGenerateOverloads: false, // should overloads with full signatures like deposit(uint256) be generated always, even if there are no overloads? dontOverrideCompile: false, // defaults to false }, diff --git a/hardhat/utils/logger.ts b/hardhat/utils/logger.ts index 727ef9a..b23618e 100644 --- a/hardhat/utils/logger.ts +++ b/hardhat/utils/logger.ts @@ -5,18 +5,37 @@ const DEFAULTS = { silent: false, } +interface LoggerOptions { + actor?: string + color?: string + verbose?: boolean + silent?: boolean +} + export class Logger { actor: string color: string + verbose: boolean + silent: boolean - static setDefaults(silent: boolean, verbose: boolean): void { - DEFAULTS.silent = silent - DEFAULTS.verbose = verbose - } - - constructor(actor = '', color = 'white') { + constructor({ + actor = '', + color = 'white', + verbose = DEFAULTS.verbose, + silent = DEFAULTS.silent, + }: LoggerOptions = {}) { this.actor = actor this.color = color + this.verbose = verbose + this.silent = silent + } + + setVerbose(verbose: boolean): void { + this.verbose = verbose + } + + setSilent(silent: boolean): void { + this.silent = silent } info(msg: string): void { diff --git a/src/index.ts b/src/index.ts index bc17e94..31d5b0f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,3 +4,11 @@ */ const moduleState = undefined export { moduleState } + +// Logger +import { logger } from '../hardhat/utils' +if (process.env.DEVELOPMENT !== 'true') { + // If not in development, silence the logger + logger.setSilent(true) +} +export { logger } From 1ce2b7c58e2fa541e820d02f18ae900c9a03e8d4 Mon Sep 17 00:00:00 2001 From: defifofum Date: Thu, 21 Dec 2023 14:38:06 -0600 Subject: [PATCH 4/6] feat: Add support contracts for DeployManager upgradeable deployments --- contracts/external/OpenZeppelinImports.sol | 3 +- contracts/proxy/ProxyAdmin.sol | 87 ++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 contracts/proxy/ProxyAdmin.sol diff --git a/contracts/external/OpenZeppelinImports.sol b/contracts/external/OpenZeppelinImports.sol index 8279ae8..60040a5 100644 --- a/contracts/external/OpenZeppelinImports.sol +++ b/contracts/external/OpenZeppelinImports.sol @@ -3,7 +3,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +/// @dev Decided to adapt this contract with a constructor to set the initial owner. See contracts/proxy/ProxyAdmin.sol +// import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; /// @dev This contract enables hardhat to compile the builds for upgradeable deployments diff --git a/contracts/proxy/ProxyAdmin.sol b/contracts/proxy/ProxyAdmin.sol new file mode 100644 index 0000000..fb3252d --- /dev/null +++ b/contracts/proxy/ProxyAdmin.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.8.3) (proxy/transparent/ProxyAdmin.sol) + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +/** + * @dev This is an auxiliary contract meant to be assigned as the admin of a {TransparentUpgradeableProxy}. For an + * explanation of why you would want to use this see the documentation for {TransparentUpgradeableProxy}. + * @notice Added constructor to set initial owner + */ +contract ProxyAdmin is Ownable { + /// @dev ADDED by DeFiFoFum to set initial owner + constructor(address _initialOwner) Ownable() { + _transferOwnership(_initialOwner); + } + + /** + * @dev Returns the current implementation of `proxy`. + * + * Requirements: + * + * - This contract must be the admin of `proxy`. + */ + function getProxyImplementation(ITransparentUpgradeableProxy proxy) public view virtual returns (address) { + // We need to manually run the static call since the getter cannot be flagged as view + // bytes4(keccak256("implementation()")) == 0x5c60da1b + (bool success, bytes memory returndata) = address(proxy).staticcall(hex"5c60da1b"); + require(success); + return abi.decode(returndata, (address)); + } + + /** + * @dev Returns the current admin of `proxy`. + * + * Requirements: + * + * - This contract must be the admin of `proxy`. + */ + function getProxyAdmin(ITransparentUpgradeableProxy proxy) public view virtual returns (address) { + // We need to manually run the static call since the getter cannot be flagged as view + // bytes4(keccak256("admin()")) == 0xf851a440 + (bool success, bytes memory returndata) = address(proxy).staticcall(hex"f851a440"); + require(success); + return abi.decode(returndata, (address)); + } + + /** + * @dev Changes the admin of `proxy` to `newAdmin`. + * + * Requirements: + * + * - This contract must be the current admin of `proxy`. + */ + function changeProxyAdmin(ITransparentUpgradeableProxy proxy, address newAdmin) public virtual onlyOwner { + proxy.changeAdmin(newAdmin); + } + + /** + * @dev Upgrades `proxy` to `implementation`. See {TransparentUpgradeableProxy-upgradeTo}. + * + * Requirements: + * + * - This contract must be the admin of `proxy`. + */ + function upgrade(ITransparentUpgradeableProxy proxy, address implementation) public virtual onlyOwner { + proxy.upgradeTo(implementation); + } + + /** + * @dev Upgrades `proxy` to `implementation` and calls a function on the new implementation. See + * {TransparentUpgradeableProxy-upgradeToAndCall}. + * + * Requirements: + * + * - This contract must be the admin of `proxy`. + */ + function upgradeAndCall( + ITransparentUpgradeableProxy proxy, + address implementation, + bytes memory data + ) public payable virtual onlyOwner { + proxy.upgradeToAndCall{value: msg.value}(implementation, data); + } +} From 16f40741609715f3e5db86788d6d5d8d60229fe2 Mon Sep 17 00:00:00 2001 From: defifofum Date: Thu, 21 Dec 2023 14:38:42 -0600 Subject: [PATCH 5/6] 3.3.2 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a3cc3cb..0345e20 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@defifofum/hardhat-template", - "version": "3.3.1", + "version": "3.3.2", "description": "Solidity Smart Contract development template using modern Web3 frameworks/tools including Hardhat, Typechain, Foundry and more.", "main": "./dist/index.js", "files": [ @@ -102,4 +102,4 @@ "@openzeppelin/contracts-upgradeable": "4.9.3", "@openzeppelin/hardhat-upgrades": "^1.22.1" } -} \ No newline at end of file +} From 685ef5b58ede45e8f7aac003586ca1dedd6d7a6b Mon Sep 17 00:00:00 2001 From: defifofum Date: Thu, 21 Dec 2023 14:50:43 -0600 Subject: [PATCH 6/6] fix: Account management when mnemonic and PK are not provided --- hardhat.config.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index e925066..7c95ae8 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -45,14 +45,20 @@ task(TASK_TEST, '🫶 Test Task') .setAction(testRunner) const mainnetMnemonic = getEnv('MAINNET_MNEMONIC') -const mainnetAccounts: HttpNetworkAccountsUserConfig = mainnetMnemonic +const mainnetPrivateKey = getEnv('MAINNET_PRIVATE_KEY') +const mainnetAccounts: HttpNetworkAccountsUserConfig | undefined = mainnetMnemonic ? { mnemonic: mainnetMnemonic } - : [getEnv('MAINNET_PRIVATE_KEY')] // Fallback to private key + : mainnetPrivateKey + ? [mainnetPrivateKey] // Fallback to private key + : undefined const testnetMnemonic = getEnv('TESTNET_MNEMONIC') -const testnetAccounts: HttpNetworkAccountsUserConfig = testnetMnemonic +const testnetPrivateKey = getEnv('TESTNET_PRIVATE_KEY') +const testnetAccounts: HttpNetworkAccountsUserConfig | undefined = testnetMnemonic ? { mnemonic: testnetMnemonic } - : [getEnv('TESTNET_PRIVATE_KEY')] // Fallback to private key + : testnetPrivateKey + ? [testnetPrivateKey] // Fallback to private key + : undefined type ExtendedNetworkOptions = { getExplorerUrl: (address: string) => string