Skip to content

Commit

Permalink
modifies fund-devnet to be fund-contracts and enables non-Anvil networks
Browse files Browse the repository at this point in the history
  • Loading branch information
dghelm committed Aug 21, 2024
1 parent aafc638 commit 32facc0
Show file tree
Hide file tree
Showing 2 changed files with 304 additions and 70 deletions.
304 changes: 304 additions & 0 deletions src/commands/helper/fund-contracts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
import { Command, Flags } from '@oclif/core'
import { ethers } from 'ethers'
import path from 'node:path'
import chalk from 'chalk'
import { select, confirm } from '@inquirer/prompts'
import { toString as qrCodeToString } from 'qrcode'

import { parseTomlConfig } from '../../utils/config-parser.js'
import { addressLink, txLink } from '../../utils/onchain/index.js'

enum Layer {
L1 = 'l1',
L2 = 'l2',
}

const FUNDING_AMOUNT = 0.004

export default class HelperFundContracts extends Command {
static description = 'Fund L1 and L2 accounts for contracts'

static flags = {
account: Flags.string({
char: 'a',
description: 'Additional account to fund',
}),
config: Flags.string({
char: 'c',
default: './config.toml',
description: 'Path to config.toml file',
}),
l1rpc: Flags.string({
char: 'o',
description: 'L1 RPC URL',
}),
l2rpc: Flags.string({
char: 't',
description: 'L2 RPC URL',
}),
dev: Flags.boolean({
char: 'd',
description: 'Use Anvil devnet funding logic',
default: false,
}),
pod: Flags.boolean({
char: 'p',
default: false,
description: 'Run inside Kubernetes pod',
}),
manual: Flags.boolean({
char: 'm',
description: 'Manually fund the accounts',
}),
'private-key': Flags.string({
char: 'k',
description: 'Private key for funder wallet',
}),
'config-contracts': Flags.string({
char: 'n',
default: './config-contracts.toml',
description: 'Path to config-contracts.toml file',
}),
'gateway-address': Flags.string({
char: 'g',
description: 'L1 Gateway Router or L1 ETH Gateway contract address',
}),
}

private l1Provider!: ethers.JsonRpcProvider
private l2Provider!: ethers.JsonRpcProvider
private l1Rpc!: string
private l2Rpc!: string
private l1FundingWallet!: ethers.Wallet
private l2FundingWallet!: ethers.Wallet
private l1ETHGateway!: string
private blockExplorers: Record<Layer, { blockExplorerURI: string }> = {
[Layer.L1]: { blockExplorerURI: '' },
[Layer.L2]: { blockExplorerURI: '' },
}

public async run(): Promise<void> {
const { flags } = await this.parse(HelperFundContracts)

const configPath = path.resolve(flags.config)
const config = parseTomlConfig(configPath)

let l1RpcUrl: string
let l2RpcUrl: string

if (flags.pod) {
l1RpcUrl = config?.general?.L1_RPC_ENDPOINT
l2RpcUrl = config?.general?.L2_RPC_ENDPOINT
} else {
l1RpcUrl = flags.l1rpc ?? config.frontend.EXTERNAL_RPC_URI_L1
l2RpcUrl = flags.l2rpc ?? config.frontend.EXTERNAL_RPC_URI_L2
}

if (!l1RpcUrl || !l2RpcUrl) {
this.error(
`Missing RPC URL(s) in ${configPath}. Please ensure L1_RPC_ENDPOINT and L2_RPC_ENDPOINT (for pod mode) or EXTERNAL_RPC_URI_L1 and EXTERNAL_RPC_URI_L2 (for non-pod mode) are defined or use the '-o' and '-t' flags.`,
)
}

this.l1Rpc = l1RpcUrl
this.l2Rpc = l2RpcUrl
this.l1Provider = new ethers.JsonRpcProvider(l1RpcUrl)
this.l2Provider = new ethers.JsonRpcProvider(l2RpcUrl)

this.blockExplorers.l1.blockExplorerURI = config?.frontend?.EXTERNAL_EXPLORER_URI_L1
this.blockExplorers.l2.blockExplorerURI = config?.frontend?.EXTERNAL_EXPLORER_URI_L2

this.l1ETHGateway = config?.contracts?.L1_ETH_GATEWAY_PROXY_ADDR

if (flags['private-key']) {
this.l1FundingWallet = new ethers.Wallet(flags['private-key'], this.l1Provider)
this.l2FundingWallet = new ethers.Wallet(flags['private-key'], this.l2Provider)
} else if (!flags.manual && !flags.dev) {
this.l1FundingWallet = new ethers.Wallet(config.accounts.DEPLOYER_PRIVATE_KEY, this.l1Provider)
this.l2FundingWallet = new ethers.Wallet(config.accounts.DEPLOYER_PRIVATE_KEY, this.l2Provider)
}

if (flags['gateway-address']) {
this.l1ETHGateway = flags['gateway-address']
} else if (flags['config-contracts']) {
const contractsConfigPath = path.resolve(flags['config-contracts'])
try {
const contractsConfig = parseTomlConfig(contractsConfigPath)
this.l1ETHGateway = contractsConfig?.L1_ETH_GATEWAY_PROXY_ADDR
} catch (error) {
this.log("Parsing config-contracts.toml failed. Bridging will be disabled.")
}
}

const l1Addresses = [
config.accounts.L1_COMMIT_SENDER_ADDR,
config.accounts.L1_FINALIZE_SENDER_ADDR,
config.accounts.L1_GAS_ORACLE_SENDER_ADDR,
]

const l2Addresses = [
config.accounts.L2_GAS_ORACLE_SENDER_ADDR,
]

if (flags.account) {
l1Addresses.push(flags.account)
l2Addresses.push(flags.account)
}

await this.fundL1Addresses(l1Addresses, flags)
await this.fundL2Addresses(l2Addresses, flags)

this.log(chalk.green('Funding complete'))
}

private async fundL1Addresses(addresses: string[], flags: any): Promise<void> {
this.log(chalk.cyan('\nFunding L1 Addresses:'))
for (const address of addresses) {
if (!address) {
this.warn(`Address not found in config for one of the L1 accounts`)
continue
}

if (flags.dev) {
await this.fundAddressAnvil(this.l1Provider, address, FUNDING_AMOUNT, Layer.L1)
} else if (flags.manual) {
await this.promptManualFunding(address, FUNDING_AMOUNT, Layer.L1)
} else {
await this.fundAddressNetwork(this.l1Provider, address, FUNDING_AMOUNT, Layer.L1)
}
}
}

private async fundL2Addresses(addresses: string[], flags: any): Promise<void> {
this.log(chalk.cyan('\nFunding L2 Addresses:'))
for (const address of addresses) {
if (!address) {
this.warn(`Address not found in config for one of the L2 accounts`)
continue
}

this.log(this.l1ETHGateway)

if (flags.manual) {
await this.promptManualFunding(address, FUNDING_AMOUNT, Layer.L2)
} else {
const fundingMethod = await this.promptUserForL2Funding()

if (fundingMethod === 'bridge') {
await this.bridgeFundsL1ToL2(address, FUNDING_AMOUNT)
} else if (fundingMethod === 'direct') {
await this.fundAddressNetwork(this.l2Provider, address, FUNDING_AMOUNT, Layer.L2)
} else {
await this.promptManualFunding(address, FUNDING_AMOUNT, Layer.L2)
}
}
}
}

private async fundAddressAnvil(provider: ethers.JsonRpcProvider, address: string, amount: number, layer: Layer) {
try {
const result = await provider.send('anvil_setBalance', [address, ethers.parseEther(amount.toString()).toString()])
await this.logAddress(address, `Successfully funded with ${amount} ETH`, layer)
return result
} catch (error) {
this.error(`Failed to fund ${address} (${layer} devnet): ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}

private async fundAddressNetwork(provider: ethers.JsonRpcProvider, address: string, amount: number, layer: Layer) {
const fundingWallet = layer === Layer.L1 ? this.l1FundingWallet : this.l2FundingWallet
try {
const tx = await fundingWallet.sendTransaction({
to: address,
value: ethers.parseEther(amount.toString()),
})
await tx.wait()
await this.logTx(tx.hash, `Funded ${address} with ${amount} ETH`, layer)
} catch (error) {
this.error(`Failed to fund ${address} (${layer}): ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}

private async promptManualFunding(address: string, amount: number, layer: Layer) {
const chainId = layer === Layer.L1 ?
(await this.l1Provider.getNetwork()).chainId :
(await this.l2Provider.getNetwork()).chainId

let qrString = `ethereum:${address}@${chainId}&value=${amount}`

await this.logAddress(address, `Please fund the following address with ${chalk.yellow(amount)} ETH`, layer)
this.log('\n')
this.log(`ChainID: ${chalk.cyan(Number(chainId))}`)
this.log(`Chain RPC: ${chalk.cyan(layer === Layer.L1 ? this.l1Rpc : this.l2Rpc)}`)
this.log('\n')
this.log('Scan this QR code to fund the address:')

this.log(await qrCodeToString(qrString, { small: true, type: 'terminal' }))

let funded = false
while (!funded) {
await confirm({ message: 'Press Enter when ready...' })
this.log(`Checking...`)
const balance = await (layer === Layer.L1 ? this.l1Provider : this.l2Provider).getBalance(address)
const formattedBalance = ethers.formatEther(balance)

if (Number(formattedBalance) >= amount) {
this.log(chalk.green(`Wallet Balance: ${formattedBalance}`))
funded = true
} else {
this.log(chalk.yellow(`Balance is only ${formattedBalance}. Please fund the wallet.`))
}
}
}

private async promptUserForL2Funding(): Promise<string> {
const answer = await select({
message: 'How would you like to fund the L2 address?',
choices: [
{ name: 'Bridge funds from L1', value: 'bridge', disabled: !(this.l1ETHGateway) },
{ name: 'Directly fund L2 wallet using Deployer / private key', value: 'direct' },
{ name: 'Manual funding', value: 'manual' },
],
})
return answer
}

private async bridgeFundsL1ToL2(recipient: string, amount: number): Promise<void> {
try {
this.log(chalk.cyan(`Bridging funds from L1 to L2 for recipient: ${recipient}`))

const gasLimit = BigInt(170_000)
const value = ethers.parseEther((amount + 0.001).toString())

this.log(this.l1ETHGateway)

const l1ETHGateway = new ethers.Contract(
this.l1ETHGateway,
['function depositETH(address _to, uint256 _amount, uint256 _gasLimit) payable'],
this.l1FundingWallet
)

await this.logAddress(this.l1ETHGateway, `Depositing ${amount} ETH by sending ${ethers.formatEther(value)} to`, Layer.L1)

const tx = await l1ETHGateway.depositETH(recipient, ethers.parseEther(amount.toString()), gasLimit, { value })
await this.logTx(tx.hash, 'Bridge transaction sent', Layer.L1)

const receipt = await tx.wait()
this.log(chalk.green(`Transaction mined in block: ${receipt.blockNumber}`))

this.log(chalk.yellow(`Funds are being bridged to ${recipient}. Please wait for the transaction to be processed on L2.`))
} catch (error) {
this.error(`Error bridging funds from L1 to L2: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}

private async logAddress(address: string, description: string, layer: Layer): Promise<void> {
const link = await addressLink(address, this.blockExplorers[layer])
this.log(`${description}: ${chalk.cyan(link)}`)
}

private async logTx(txHash: string, description: string, layer: Layer): Promise<void> {
const link = await txLink(txHash, this.blockExplorers[layer])
this.log(`${description}: ${chalk.cyan(link)}`)
}
}
70 changes: 0 additions & 70 deletions src/commands/helper/fund-devnet.ts

This file was deleted.

0 comments on commit 32facc0

Please sign in to comment.