diff --git a/package.json b/package.json index 5a1b73f..f65244a 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "cli-progress": "^3.12.0", "ethers": "^6.13.2", "js-yaml": "^4.1.0", + "ora": "^8.0.1", "qrcode": "^1.5.4", "terminal-link": "^3.0.0", "toml": "^3.0.0" diff --git a/src/commands/test/e2e.ts b/src/commands/test/e2e.ts index 65f3f63..0236bd9 100644 --- a/src/commands/test/e2e.ts +++ b/src/commands/test/e2e.ts @@ -4,9 +4,29 @@ import { confirm, select, Separator } from '@inquirer/prompts' import QRCode from 'qrcode' import { parseTomlConfig } from '../../utils/config-parser.js' import path from 'path' -import { getFinalizedBlockHeight, l1ETHGatewayABI, l2ETHGatewayABI, getCrossDomainMessageFromTx, getPendingQueueIndex, getGasOracleL2BaseFee, awaitTx, txLink } from '../../utils/onchain/index.js' +import { + addressLink, + awaitTx, + blockLink, + erc20ABI, + erc20Bytecode, + getCrossDomainMessageFromTx, + getFinalizedBlockHeight, + getGasOracleL2BaseFee, + getL2TokenFromL1Address, + getPendingQueueIndex, + getWithdrawals, + l1ETHGatewayABI, + l2ETHGatewayABI, + l1GatewayRouterABI, + l2GatewayRouterWithdrawERC20ABI, + l1MessengerRelayMessageWithProofABI, + scrollERC20ABI, + txLink, +} from '../../utils/onchain/index.js' import { Wallet } from 'ethers' import chalk from 'chalk'; +import ora from 'ora' interface ContractsConfig { [key: string]: string @@ -89,18 +109,43 @@ export default class TestE2e extends Command { private l2Rpc!: string private l1ETHGateway!: string private l2ETHGateway!: string + private l1GatewayRouter!: string + private l2GatewayRouter!: string + private l1Messenger!: string private l1MessegeQueueProxyAddress!: string private bridgeApiUrl!: string + private mockFinalizeEnabled!: boolean + private mockFinalizeTimeout!: number private results: { bridgeFundsL1ToL2: { - L1DepositETHTx?: string; - L2ETHBridgeTx?: string; + l1DepositTx?: string; + l2MessengerTx?: string; + queueIndex?: number; complete: boolean; }; bridgeFundsL2ToL1: { - L2DepositETHTx?: string; - complete: false + l2WithdrawTx?: string; + complete: boolean + }; + bridgeERC20L1ToL2: { + l1DepositTx?: string; + l2MessengerTx?: string; + queueIndex?: number; + l2TokenAddress?: string; + complete: boolean; + }; + bridgeERC20L2ToL1: { + l2WithdrawTx?: string; + complete: boolean; + }; + claimERC20OnL1: { + complete: boolean; + l1ClaimTx?: string; + }; + claimETHOnL1: { + complete: boolean; + l1ClaimTx?: string; }; deployERC20OnL1: { address?: string; @@ -112,14 +157,6 @@ export default class TestE2e extends Command { txHash?: string; complete: boolean; }; - bridgeERC20L1ToL2: { - L2TxHash?: string; - complete: boolean; - }; - bridgeERC20L2ToL1: { - L2TxHash?: string; - complete: boolean; - }; fundWalletOnL1: { complete: boolean; }; @@ -129,10 +166,12 @@ export default class TestE2e extends Command { } = { bridgeFundsL1ToL2: { complete: false }, bridgeFundsL2ToL1: { complete: false }, - deployERC20OnL1: { complete: false }, - deployERC20OnL2: { complete: false }, bridgeERC20L1ToL2: { complete: false }, bridgeERC20L2ToL1: { complete: false }, + claimETHOnL1: { complete: false }, + claimERC20OnL1: { complete: false }, + deployERC20OnL1: { complete: false }, + deployERC20OnL2: { complete: false }, fundWalletOnL1: { complete: false }, fundWalletOnL2: { complete: false }, }; @@ -166,8 +205,14 @@ export default class TestE2e extends Command { this.log('\n' + chalk.bgCyan.black(` ${sectionName} `) + '\n'); } - private logTx(txHash: string, description: string): void { - this.logResult(`${description}: ${chalk.cyan(txHash)}`, 'info'); + private async logTx(txHash: string, description: string, layer: Layer): Promise { + const link = await txLink(txHash, { rpc: layer === Layer.L1 ? this.l1Provider : this.l2Provider }); + this.logResult(`${description}: ${chalk.cyan(link)}`, 'info'); + } + + private async logAddress(address: string, description: string, layer: Layer): Promise { + const link = await addressLink(address, { rpc: layer === Layer.L1 ? this.l1Provider : this.l2Provider }); + this.logResult(`${description}: ${chalk.cyan(link)}`, 'info'); } public async run(): Promise { @@ -202,11 +247,16 @@ export default class TestE2e extends Command { this.l1Rpc = l1RpcUrl this.l2Rpc = l2RpcUrl + this.skipWalletGen = flags.skip_wallet_generation this.l1ETHGateway = contractsConfig.L1_ETH_GATEWAY_PROXY_ADDR this.l2ETHGateway = contractsConfig.L2_ETH_GATEWAY_PROXY_ADDR + this.l1GatewayRouter = contractsConfig.L1_GATEWAY_ROUTER_PROXY_ADDR + this.l2GatewayRouter = contractsConfig.L2_GATEWAY_ROUTER_PROXY_ADDR this.l1MessegeQueueProxyAddress = contractsConfig.L1_MESSAGE_QUEUE_PROXY_ADDR + this.l1Messenger = contractsConfig.L1_SCROLL_MESSENGER_PROXY_ADDR + this.mockFinalizeEnabled = config?.general.TEST_ENV_MOCK_FINALIZE_ENABLED === "true" ? true : false + this.mockFinalizeTimeout = config?.general.TEST_ENV_MOCK_FINALIZE_TIMEOUT_SEC ? parseInt(contractsConfig.TEST_ENV_MOCK_FINALIZE_TIMEOUT_SEC) : 0 this.bridgeApiUrl = config?.frontend.BRIDGE_API_URI - this.skipWalletGen = flags.skip_wallet_generation this.l1Provider = new ethers.JsonRpcProvider(l1RpcUrl) this.l2Provider = new ethers.JsonRpcProvider(l2RpcUrl) @@ -247,17 +297,66 @@ export default class TestE2e extends Command { try { this.logSection('Running E2E Test'); + this.logSection('Setup L1'); // Setup L1 if (!this.skipWalletGen) { + this.logSection('Generate and Fund Wallets'); await this.generateNewWallet(); await this.fundWalletOnL1(); } - // Run L1 and L2 groups in parallel - await Promise.all([ - this.runL1Groups(), - this.runL2Groups(), - ]); + this.logSection('Initiate ETH Deposit on L1'); + await this.bridgeFundsL1ToL2(); + await this.shortPause() + + this.logSection('Deploying ERC20 on L1'); + await this.deployERC20OnL1(); + await this.shortPause() + + + this.logSection('Initiate ERC20 Deposit on L1'); + await this.bridgeERC20L1ToL2(); + await this.shortPause() + + // Setup L2 + this.logSection('Setup L2'); + await this.fundWalletOnL2(); + await this.shortPause() + + if (!this.results.fundWalletOnL2.complete) { + this.logSection('Waiting for L1 ETH Deposit'); + await this.completeL1ETHDeposit(); + await this.shortPause() + } + + this.logSection('Initiate ETH Withdrawal on L2'); + await this.bridgeFundsL2ToL1(); + await this.shortPause() + + this.logSection('Deploying an ERC20 on L2'); + await this.deployERC20OnL2() + await this.shortPause() + + this.logSection('Waiting for L1 ERC20 Deposit'); + await this.completeL1ERC20Deposit(); + await this.shortPause() + await this.shortPause() + await this.shortPause() + await this.shortPause() + await this.shortPause() + await this.shortPause() + await this.shortPause() + + this.logSection('Bridging ERC20 Back to L1'); + await this.bridgeERC20L2ToL1(); + await this.shortPause() + + + this.logSection('Claiming ETH and ERC20 on L1'); + await this.claimFundsOnL1(); + await this.shortPause() + await this.claimERC20OnL1(); + await this.shortPause() this.logResult('E2E Test completed successfully', 'success'); } catch (error) { @@ -265,47 +364,68 @@ export default class TestE2e extends Command { } } - private async runL1Groups(): Promise { + private async shortPause() { + // Sleep for 0.5 second + await new Promise(resolve => setTimeout(resolve, 500)); + } + + private async completeL1ETHDeposit(): Promise { try { - this.logSection('Running L1 Groups'); + this.logResult('Waiting for L1 ETH deposit to complete on L2...', 'info'); - // Sequential L1 - Group 1 (ETH) - this.logResult('L1 Group 1 (ETH)', 'info'); - await this.bridgeFundsL1ToL2(); + if (!this.results.bridgeFundsL1ToL2.l2MessengerTx) { + throw new BridgingError('L2 destination transaction hash is missing.'); + } - // Sequential L1 - Group 2 (ERC20) - this.logResult('L1 Group 2 (ERC20)', 'info'); - await this.deployERC20OnL1(); - await this.bridgeERC20L1ToL2(); + const spinner = ora('Waiting for L2 transaction to be mined...').start(); + + try { + // Wait for the L2 transaction to be mined + const l2Receipt = await this.l2Provider.waitForTransaction(this.results.bridgeFundsL1ToL2.l2MessengerTx); - this.logResult('L1 Groups completed', 'success'); + if (l2Receipt && l2Receipt.status === 1) { + spinner.succeed('L1 ETH deposit successfully completed on L2'); + this.results.bridgeFundsL1ToL2.complete = true; + } else { + spinner.fail('L2 transaction failed or was reverted.'); + throw new BridgingError('L2 transaction failed or was reverted.'); + } + } catch (error) { + spinner.fail('Failed to complete L1 ETH deposit'); + throw error; + } } catch (error) { - this.handleGroupError('L1 Groups', error); + throw new BridgingError(`Failed to complete L1 ETH deposit: ${error instanceof Error ? error.message : 'Unknown error'}`); } } - private async runL2Groups(): Promise { + private async completeL1ERC20Deposit(): Promise { try { - this.logSection('Running L2 Groups'); + this.logResult('Waiting for L1 ERC20 deposit to complete on L2...', 'info'); - // Setup L2 - this.logResult('Setup L2', 'info'); - await this.fundWalletOnL2(); + if (!this.results.bridgeERC20L1ToL2.l2MessengerTx) { + throw new BridgingError('L2 destination transaction hash for ERC20 deposit is missing.'); + } - // Sequential L2 - Group 1 (ETH) - this.logResult('L2 Group 1 (ETH)', 'info'); - await this.bridgeFundsL2ToL1(); - await this.claimFundsOnL1(); + const spinner = ora('Waiting for L2 transaction to be mined...').start(); - // Sequential L2 - Group 2 (ERC20) - this.logResult('L2 Group 2 (ERC20)', 'info'); - await this.deployERC20OnL2(); - await this.bridgeERC20L2ToL1(); - await this.claimERC20OnL1(); + try { + // Wait for the L2 transaction to be mined + const l2Receipt = await this.l2Provider.waitForTransaction(this.results.bridgeERC20L1ToL2.l2MessengerTx); - this.logResult('L2 Groups completed', 'success'); + if (l2Receipt && l2Receipt.status === 1) { + spinner.succeed('L1 ERC20 deposit successfully completed on L2'); + this.results.bridgeERC20L1ToL2.complete = true; + } else { + spinner.fail('L2 ERC20 deposit transaction failed or was reverted.'); + throw new BridgingError('L2 ERC20 deposit transaction failed or was reverted.'); + } + } catch (error) { + spinner.fail('Failed to complete L1 ERC20 deposit'); + throw error; + } } catch (error) { - this.handleGroupError('L2 Groups', error); + throw new BridgingError(`Failed to complete L1 ERC20 deposit: ${error instanceof Error ? error.message : 'Unknown error'}`); } } @@ -342,7 +462,7 @@ export default class TestE2e extends Command { private async generateNewWallet(): Promise { const randomWallet = ethers.Wallet.createRandom() this.wallet = new ethers.Wallet(randomWallet.privateKey, this.l1Provider) - this.logResult(`Generated new wallet: ${chalk.cyan(this.wallet.address)}`, 'success') + await this.logAddress(this.wallet.address, 'Generated new wallet', Layer.L1); this.logResult(`Private Key: ${chalk.yellow(this.wallet.privateKey)}`, 'warning') } @@ -371,7 +491,7 @@ export default class TestE2e extends Command { value: ethers.parseEther(amount.toString()) }) await tx.wait() - this.logResult(`Funded wallet with ${amount} ETH: ${tx.hash}`) + await this.logTx(tx.hash, `Funded wallet with ${amount} ETH`, layer); } private async bridgeFundsL1ToL2(): Promise { @@ -384,57 +504,62 @@ export default class TestE2e extends Command { // TODO: what's the best way to determine the gasLimit? const l2BaseFee = await getGasOracleL2BaseFee(this.l1Rpc, this.l1MessegeQueueProxyAddress) - const value = ethers.parseEther((FUNDING_AMOUNT / 2 + 0.00002).toString()); + const value = ethers.parseEther((FUNDING_AMOUNT / 2 + 0.001).toString()); // Create the contract instance const l1ETHGateway = new ethers.Contract(this.l1ETHGateway, l1ETHGatewayABI, this.wallet.connect(this.l1Provider)); // const value = amount + ethers.parseEther(`${gasLimit*l2BaseFee} wei`); - this.logResult(`Depositing ${amount} by sending ${value} to ${await l1ETHGateway.getAddress()}`) + await this.logAddress(await l1ETHGateway.getAddress(), `Depositing ${amount} by sending ${value} to`, Layer.L1); const tx = await l1ETHGateway.depositETH(amount, gasLimit, { value }); - this.results.bridgeFundsL1ToL2.L1DepositETHTx = tx.hash - this.logTx(tx.hash, 'Transaction sent'); + await this.logTx(tx.hash, 'Transaction sent', Layer.L1); const receipt = await tx.wait(); const blockNumber = receipt?.blockNumber; this.logResult(`Transaction mined in block: ${chalk.cyan(blockNumber)}`, 'success'); const { queueIndex, l2TxHash } = await getCrossDomainMessageFromTx(tx.hash, this.l1Rpc, this.l1MessegeQueueProxyAddress); - this.results.bridgeFundsL1ToL2.L2ETHBridgeTx = l2TxHash - this.logResult(`Waiting for the following tx on L2: ${chalk.cyan(l2TxHash)}`, 'info'); + this.results.bridgeFundsL1ToL2 = { + l1DepositTx: tx.hash, + complete: false, + l2MessengerTx: l2TxHash, + queueIndex + }; - let isFinalized = false; - while (!isFinalized) { + // await this.logTx(l2TxHash, 'Waiting for the following tx on L2', Layer.L2); - const finalizedBlockNumber = await getFinalizedBlockHeight(this.l1Rpc); + // let isFinalized = false; + // while (!isFinalized) { - if (blockNumber >= finalizedBlockNumber) { - isFinalized = true; - this.logResult(`Block ${blockNumber} is finalized. Bridging should be completed soon.`, 'success'); - } else { - // TODO: This doesn't work on Sepolia? Look into it. - this.logResult(`Waiting for block ${blockNumber} to be finalized, current height is ${finalizedBlockNumber}`, 'info'); - } + // const finalizedBlockNumber = await getFinalizedBlockHeight(this.l1Rpc); - const queueHeight = await getPendingQueueIndex(this.l1Rpc, this.l1MessegeQueueProxyAddress); + // if (blockNumber >= finalizedBlockNumber) { + // isFinalized = true; + // this.logResult(`Block ${blockNumber} is finalized. Bridging should be completed soon.`, 'success'); + // } else { + // // TODO: This doesn't work on Sepolia? Look into it. + // this.logResult(`Waiting for block ${blockNumber} to be finalized, current height is ${finalizedBlockNumber}`, 'info'); + // } - this.logResult(`Current bridge queue position is ${queueHeight}, pending tx is position ${queueIndex}`, 'info') + // const queueHeight = await getPendingQueueIndex(this.l1Rpc, this.l1MessegeQueueProxyAddress); - // await new Promise(resolve => setTimeout(resolve, 20000)); // Wait for 10 seconds -- todo, communicate this better and detect better - } + // this.logResult(`Current bridge queue position is ${queueHeight}, pending tx is position ${queueIndex}`, 'info') + + // await new Promise(resolve => setTimeout(resolve, 20000)); // Wait for 10 seconds -- todo, communicate this better and detect better + // } - // Now that the block is finalized, check L2 for the l2TxHash every 20 seconds. - const l2TxLink = txLink(l2TxHash, { rpc: this.l2Provider }) + // // Now that the block is finalized, check L2 for the l2TxHash every 20 seconds. + // const l2TxLink = await txLink(l2TxHash, { rpc: this.l2Provider }) - this.logResult(`Waiting for ${chalk.cyan(l2TxLink)}...`, 'info') - const l2TxReceipt = await awaitTx(l2TxHash, this.l2Provider) + // this.logResult(`Waiting for ${chalk.cyan(l2TxLink)}...`, 'info') + // const l2TxReceipt = await awaitTx(l2TxHash, this.l2Provider) - this.log(`${chalk.gray(JSON.stringify(l2TxReceipt, null, 2))}`) + // this.log(`${chalk.gray(JSON.stringify(l2TxReceipt, null, 2))}`) - this.logResult('Bridging funds from L1 to L2 completed', 'success'); + // this.logResult('Bridging funds from L1 to L2 completed', 'success'); } catch (error) { throw new BridgingError(`Error bridging funds from L1 to L2: ${error instanceof Error ? error.message : 'Unknown error'}`); } @@ -444,6 +569,12 @@ export default class TestE2e extends Command { try { // Implement deploying ERC20 on L1 this.logResult('Deploying ERC20 on L1', 'info') + // Deploy new TKN ERC20 token and mint 1000 to admin wallet + const tokenContract = await this.deployERC20(Layer.L1) + + this.logAddress(tokenContract, "Token successfully deployed", Layer.L1) + + this.results.deployERC20OnL1.address = tokenContract this.results.deployERC20OnL1.complete = true; } catch (error) { throw new DeploymentError(`Failed to deploy ERC20 on L1: ${error instanceof Error ? error.message : 'Unknown error'}`); @@ -454,7 +585,97 @@ export default class TestE2e extends Command { try { // Implement bridging ERC20 from L1 to L2 this.logResult('Bridging ERC20 from L1 to L2', 'info') - this.results.bridgeERC20L1ToL2.complete = true; + // Wait for token balance to exist in wallet before proceeding + const erc20Address = this.results.deployERC20OnL1.address; + if (!erc20Address) { + throw new Error("ERC20 address not found. Make sure deployERC20OnL1 was successful."); + } + + const erc20Contract = new ethers.Contract(erc20Address, erc20ABI, this.wallet.connect(this.l1Provider)); + + let balance = BigInt(0); + let attempts = 0; + const delay = 15000; // 15 seconds + + while (balance === BigInt(0)) { + balance = await erc20Contract.balanceOf(this.wallet.address); + if (balance > BigInt(0)) { + this.logResult(`Token balance found: ${balance.toString()}`, 'success'); + break; + } + attempts++; + this.logResult(`Waiting for token balance...`, 'info'); + await new Promise(resolve => setTimeout(resolve, delay)); + } + + const halfBalance = balance / 2n; + + // Set allowance for l1GatewayRouter + const approvalTx = await erc20Contract.approve(this.l1GatewayRouter, halfBalance); + await approvalTx.wait(); + + this.logResult(`Approved ${halfBalance} tokens for L1GatewayRouter`, 'success'); + + // Create L1GatewayRouter contract instance + const l1GatewayRouter = new ethers.Contract(this.l1GatewayRouter, l1GatewayRouterABI, this.wallet.connect(this.l1Provider)); + + // Call depositERC20 + const depositTx = await l1GatewayRouter.depositERC20(erc20Address, halfBalance, 200000, { value: ethers.parseEther("0.0005") }); + // TODO: figure out value here + await depositTx.wait(); + + // const blockNumber = receipt?.blockNumber; + + // Get L2TokenAddress from L1 Contract Address + const l2TokenAddress = await getL2TokenFromL1Address(erc20Address, this.l1Rpc, this.l1GatewayRouter); + const { queueIndex, l2TxHash } = await getCrossDomainMessageFromTx(depositTx.hash, this.l1Rpc, this.l1MessegeQueueProxyAddress) + + this.logTx(depositTx.hash, `Deposit transaction sent`, Layer.L1); + this.logAddress(l2TokenAddress, `L2 Token Address`, Layer.L2); + this.logTx(l2TxHash, `L2 Messenger Tx`, Layer.L2); + + this.results.bridgeERC20L1ToL2 = { + l1DepositTx: depositTx.hash, + complete: false, + l2MessengerTx: l2TxHash, + l2TokenAddress, + queueIndex + }; + + // let isFinalized = false; + // while (!isFinalized) { + + // const finalizedBlockNumber = await getFinalizedBlockHeight(this.l1Rpc); + + // if (blockNumber >= finalizedBlockNumber) { + // isFinalized = true; + // this.logResult(`Block ${blockNumber} is finalized. Bridging should be completed soon.`, 'success'); + // } else { + // // TODO: This doesn't work on Sepolia? Look into it. + // this.logResult(`Waiting for block ${blockNumber} to be finalized, current height is ${finalizedBlockNumber}`, 'info'); + // } + + // const queueHeight = await getPendingQueueIndex(this.l1Rpc, this.l1MessegeQueueProxyAddress); + + // this.logResult(`Current bridge queue position is ${queueHeight}, pending tx is position ${queueIndex}`, 'info') + + // await new Promise(resolve => setTimeout(resolve, 20000)); // Wait for 10 seconds -- todo, communicate this better and detect better + // } + + + // // Now that the block is finalized, check L2 for the l2TxHash every 20 seconds. + // const l2TxLink = await txLink(l2TxHash, { rpc: this.l2Provider }) + + // this.logResult(`Waiting for ${chalk.cyan(l2TxLink)}...`, 'info') + // const l2TxReceipt = await awaitTx(l2TxHash, this.l2Provider) + + // this.log(`${chalk.gray(JSON.stringify(l2TxReceipt, null, 2))}`) + + // this.logResult('Bridging funds from L1 to L2 completed', 'success'); + + + // this.results.bridgeERC20L1ToL2.complete = true; + } catch (error) { throw new BridgingError(`Error bridging ERC20 from L1 to L2: ${error instanceof Error ? error.message : 'Unknown error'}`); } @@ -501,9 +722,9 @@ export default class TestE2e extends Command { // TODO: handle some async stuff in parallel this.logResult(`Waiting for L1 -> L2 bridge to complete...`, 'info') - // Wait for this.bridgeFundsL1ToL2 to complete -- signaled by this.results.bridgeFundsL1ToL2.complete becoming true + // will check this later in the main flow... - this.results.fundWalletOnL2.complete = true; + this.results.fundWalletOnL2.complete = false; return } @@ -526,6 +747,16 @@ export default class TestE2e extends Command { try { // Implement deploying ERC20 on L2 this.logResult('Deploying ERC20 on L2', 'info') + + // Deploy new TKN ERC20 token and mint 1000 to admin wallet + const tokenContract = await this.deployERC20(Layer.L2) + + this.logAddress(tokenContract, "Token successfully deployed", Layer.L2) + + this.results.deployERC20OnL2.address = tokenContract + this.results.deployERC20OnL2.complete = true + + } catch (error) { throw new DeploymentError(`Failed to deploy ERC20 on L2: ${error instanceof Error ? error.message : 'Unknown error'}`); } @@ -536,74 +767,109 @@ export default class TestE2e extends Command { this.logResult('Bridging funds from L2 to L1', 'info') const amount = ethers.parseEther((FUNDING_AMOUNT / 4).toString()); - - const value = ethers.parseEther((FUNDING_AMOUNT / 2 + 0.00002).toString()); + const value = amount; + // const value = ethers.parseEther((FUNDING_AMOUNT / 4 + 0.001).toString()); + // TODO: sort out how to set value here // Create the contract instance const l2ETHGateway = new ethers.Contract(this.l2ETHGateway, l2ETHGatewayABI, this.wallet.connect(this.l2Provider)); // const value = amount + ethers.parseEther(`${gasLimit*l2BaseFee} wei`); - this.logResult(`Withdrawing ${amount} by sending ${value} to ${await l2ETHGateway.getAddress()}`, 'info') + await this.logAddress(await l2ETHGateway.getAddress(), `Withdrawing ${amount} by sending ${value} to`, Layer.L2); const tx = await l2ETHGateway.withdrawETH(amount, 0, { value }); - this.results.bridgeFundsL2ToL1.L2DepositETHTx = tx.hash + this.results.bridgeFundsL2ToL1.l2WithdrawTx = tx.hash - this.logTx(tx.hash, 'Transaction sent'); + await this.logTx(tx.hash, 'Transaction sent', Layer.L2); const receipt = await tx.wait(); const blockNumber = receipt?.blockNumber; this.logResult(`Transaction mined in block: ${chalk.cyan(blockNumber)}`, 'success'); - const { l2TxHash, queueIndex } = await getCrossDomainMessageFromTx(tx.hash, this.l1Rpc, this.l1MessegeQueueProxyAddress); - this.results.bridgeFundsL1ToL2.L2ETHBridgeTx = l2TxHash + this.results.bridgeFundsL2ToL1.complete = true; - this.logResult(`Waiting for the following tx on L2: ${chalk.cyan(l2TxHash)}`, 'info'); + } catch (error) { + throw new BridgingError(`Error bridging funds from L2 to L1: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } - let isFinalized = false; - while (!isFinalized) { + private async bridgeERC20L2ToL1(): Promise { + try { + // Implement bridging ERC20 from L2 to L1 + this.logResult('Bridging L1-originated ERC20 from L2 to L1', 'info') - const finalizedBlockNumber = await getFinalizedBlockHeight(this.l1Rpc); + // Wait for token balance to exist in wallet before proceeding + const erc20Address = this.results.bridgeERC20L1ToL2.l2TokenAddress; + if (!erc20Address) { + throw new Error("ERC20 address not found. Make sure deployERC20OnL1 was successful."); + } - if (blockNumber >= finalizedBlockNumber) { - isFinalized = true; - this.logResult(`Block ${blockNumber} is finalized. Bridging should be completed soon.`, 'success'); - } else { - // TODO: This doesn't work on Sepolia? Look into it. - this.logResult(`Waiting for block ${blockNumber} to be finalized, current height is ${finalizedBlockNumber}`, 'info'); + const erc20Contract = new ethers.Contract(erc20Address, scrollERC20ABI, this.wallet.connect(this.l2Provider)); + + let balance = BigInt(0); + let attempts = 0; + const delay = 15000; // 15 seconds + + while (balance === BigInt(0)) { + this.logResult(`Getting token balance...`, 'info'); + balance = await erc20Contract.balanceOf(this.wallet.address); + if (balance > BigInt(0)) { + this.logResult(`Token balance found: ${balance.toString()}`, 'success'); + break; } + attempts++; + this.logResult(`Waiting for token balance...`, 'info'); + await new Promise(resolve => setTimeout(resolve, delay)); + } - const queueHeight = await getPendingQueueIndex(this.l1Rpc, this.l1MessegeQueueProxyAddress); + const halfBalance = balance / 2n; - this.logResult(`Current bridge queue position is ${queueHeight}, pending tx is position ${queueIndex}`, 'info') + // Set allowance for l2GatewayRouter + const approvalTx = await erc20Contract.approve(this.l2GatewayRouter, halfBalance); + await approvalTx.wait(); - // await new Promise(resolve => setTimeout(resolve, 20000)); // Wait for 10 seconds -- todo, communicate this better and detect better - } + this.logResult(`Approved ${halfBalance} tokens for L2GatewayRouter`, 'success'); - // Now that the block is finalized, check L2 for the l2TxHash every 20 seconds. - const l2TxLink = txLink(l2TxHash, { rpc: this.l2Provider }) + // Create L2GatewayRouter contract instance + const l2GatewayRouter = new ethers.Contract(this.l2GatewayRouter, l2GatewayRouterWithdrawERC20ABI, this.wallet.connect(this.l2Provider)); - this.logResult(`Waiting for ${chalk.cyan(l2TxLink)}...`, 'info') - const l2TxReceipt = await awaitTx(l2TxHash, this.l2Provider) + // Call withdrawERC20 + const withdrawTx = await l2GatewayRouter.withdrawERC20(erc20Address, halfBalance, 0, { value: 0 }); + const receipt = await withdrawTx.wait(); - this.log(`${chalk.gray(JSON.stringify(l2TxReceipt, null, 2))}`) - } catch (error) { - throw new BridgingError(`Error bridging funds from L2 to L1: ${error instanceof Error ? error.message : 'Unknown error'}`); - } - } + this.logResult(`Withdrawal transaction sent: ${withdrawTx.hash}`, 'success'); + this.results.bridgeERC20L2ToL1 = { + l2WithdrawTx: withdrawTx.hash, + complete: true + }; - private async bridgeERC20L2ToL1(): Promise { - try { - // Implement bridging ERC20 from L2 to L1 - this.logResult('Bridging ERC20 from L2 to L1', 'info') } catch (error) { throw new BridgingError(`Error bridging ERC20 from L2 to L1: ${error instanceof Error ? error.message : 'Unknown error'}`); } } private async claimFundsOnL1(): Promise { + try { // Implement claiming funds on L1 this.logResult('Claiming funds on L1', 'info') + + if (this.mockFinalizeEnabled) { + this.logResult(`Config shows finalization timeout enabled at ${this.mockFinalizeTimeout} seconds. May need to wait...`) + } else { + this.logResult(`Proof generation can take up to 1h. Please wait...`) + } + + if (this.results.bridgeFundsL2ToL1.l2WithdrawTx === undefined) { + throw new BridgingError('L2 deposit ETH transaction hash is undefined. Cannot claim funds on L1.'); + } + + const txHash = await this.findAndExecuteWithdrawal(this.results.bridgeFundsL2ToL1.l2WithdrawTx) + + this.results.claimETHOnL1.complete = true + this.results.claimETHOnL1.l1ClaimTx = txHash + + } catch (error) { throw new Error(`Error claiming funds on L1: ${error instanceof Error ? error.message : 'Unknown error'}`); } @@ -613,11 +879,101 @@ export default class TestE2e extends Command { try { // Implement claiming ERC20 on L1 this.logResult('Claiming ERC20 on L1', 'info') + + if (this.mockFinalizeEnabled) { + this.logResult(`Config shows finalization timeout enabled at ${this.mockFinalizeTimeout} seconds. May need to wait...`) + } else { + this.logResult(`Proof generation can take up to 1h. Please wait...`) + } + + if (this.results.bridgeERC20L2ToL1.l2WithdrawTx === undefined) { + throw new BridgingError('L2 deposit ETH transaction hash is undefined. Cannot claim funds on L1.'); + } + + const txHash = await this.findAndExecuteWithdrawal(this.results.bridgeERC20L2ToL1.l2WithdrawTx) + + this.results.claimERC20OnL1.complete = true + this.results.claimERC20OnL1.l1ClaimTx = txHash + } catch (error) { throw new Error(`Error claiming ERC20 on L1: ${error instanceof Error ? error.message : 'Unknown error'}`); } } + private async findAndExecuteWithdrawal(txHash: string) { + + try { + + let unclaimedWithdrawal; + + while (!unclaimedWithdrawal?.claim_info) { + let withdrawals = await getWithdrawals(this.wallet.address, this.bridgeApiUrl); + + // Check to see if the bridged tx is among unclaimed withdrawals if so, set withdrawalFound to true. + for (const withdrawal of withdrawals) { + this.log(withdrawal.hash) + if (withdrawal.hash === txHash) { + unclaimedWithdrawal = withdrawal; + this.logResult(`Found matching withdrawal for transaction: ${txHash}`, 'success'); + break; + } + } + + let l1TxHash = unclaimedWithdrawal?.counterpart_chain_tx.hash; + if (l1TxHash) { + this.logTx(l1TxHash, "This withdrawal has already been claimed", Layer.L1) + return + } + + if (!unclaimedWithdrawal) { + this.logResult(`Withdrawal not found yet. Waiting...`, 'info'); + await new Promise(resolve => setTimeout(resolve, 20000)); // Wait for 20 seconds before checking again + } else if (!unclaimedWithdrawal?.claim_info) { + this.logResult(`Withdrawal seen, but waiting for finalization. Waiting...`, 'info'); + await new Promise(resolve => setTimeout(resolve, 20000)); // Wait for 20 seconds before checking again + } + + + } + + + if (!unclaimedWithdrawal.claim_info.claimable) { + throw new Error(`Claim found, but marked as "unclaimable".`) + } + + if (!unclaimedWithdrawal?.claim_info) { + throw new Error(`No claim info in claim withdrawal.`) + } + + // + + // Now build and make the withdrawal claim + + // Create the contract instance + const l1Messenger = new ethers.Contract(this.l1Messenger, l1MessengerRelayMessageWithProofABI, this.wallet.connect(this.l1Provider)); + + // const value = amount + ethers.parseEther(`${gasLimit*l2BaseFee} wei`); + await this.logAddress(await l1Messenger.getAddress(), `Calling relayMessageWithProof on`, Layer.L1); + + + const { from, to, value, nonce, message, proof } = unclaimedWithdrawal.claim_info; + + const tx = await l1Messenger.relayMessageWithProof(from, to, value, nonce, message, { batchIndex: proof.batch_index, merkleProof: proof.merkle_proof }); + + await this.logTx(tx.hash, 'Transaction sent', Layer.L1); + const receipt = await tx.wait(); + const blockNumber = receipt?.blockNumber; + + this.logResult(`Transaction mined in block: ${chalk.cyan(blockNumber)}`, 'success'); + + return receipt.hash; + + + } catch (error) { + throw new Error(`Error finding and executing withdrawal on L1: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } + private async promptManualFunding(address: string, amount: number, layer: Layer) { let chainId = layer === Layer.L1 ? (await this.l1Provider.getNetwork()).chainId : (await this.l2Provider.getNetwork()).chainId @@ -629,8 +985,7 @@ export default class TestE2e extends Command { qrString += "&value=" qrString += amount / 2 - this.logResult(`Please fund the following address with ${chalk.yellow(amount)} ETH:`, 'warning'); - this.logResult(chalk.cyan(address), 'info'); + await this.logAddress(address, `Please fund the following address with ${chalk.yellow(amount)} ETH`, layer); this.log('\n'); this.logResult(`ChainID: ${chalk.cyan(Number(chainId))}`, 'info'); this.logResult(`Chain RPC: ${chalk.cyan(layer === Layer.L1 ? this.l1Rpc : this.l2Rpc)}`, 'info'); @@ -643,7 +998,7 @@ export default class TestE2e extends Command { while (!funded) { - const answer = await confirm({ message: 'Done?' }); + await confirm({ message: 'Press Enter when ready...' }); this.logResult(`Checking...`, 'info') // Check if wallet is actually funded -- if not, we'll loop. @@ -660,4 +1015,34 @@ export default class TestE2e extends Command { } } + + private async deployERC20(layer: Layer) { + try { + // Choose the correct provider based on the layer + const provider = layer === Layer.L1 ? this.l1Provider : this.l2Provider; + + // Connect the wallet to the correct provider + const connectedWallet = this.wallet.connect(provider); + + // Create the contract factory with the connected wallet + const tokenFactory = new ethers.ContractFactory(erc20ABI, erc20Bytecode, connectedWallet); + + // Deploy the contract + const tokenContract = await tokenFactory.deploy(); + + // Wait for the deployment transaction to be mined + await tokenContract.waitForDeployment(); + + // Get the deployed contract address + const contractAddress = await tokenContract.getAddress(); + + return contractAddress; + } catch (error) { + if (error instanceof Error) { + throw new DeploymentError(`Failed to deploy ERC20 on ${layer === Layer.L1 ? 'L1' : 'L2'}: ${error.message}`); + } else { + throw new DeploymentError(`Failed to deploy ERC20 on ${layer === Layer.L1 ? 'L1' : 'L2'}: Unknown error`); + } + } + } } \ No newline at end of file diff --git a/src/tester.ts b/src/tester.ts index acf5584..c09f941 100644 --- a/src/tester.ts +++ b/src/tester.ts @@ -1,4 +1,13 @@ -import { getFinalizedBlockHeight, getCrossDomainMessageFromTx, getPendingQueueIndex, getGasOracleL2BaseFee, awaitTx, txLink, getUnclaimedWithdrawals } from './utils/onchain/index.js'; +import { + getFinalizedBlockHeight, + getCrossDomainMessageFromTx, + getPendingQueueIndex, + getGasOracleL2BaseFee, + awaitTx, + txLink, + getWithdrawals +} from './utils/onchain/index.js'; +import { getScrollERC20Balance } from './utils/onchain/getScrollERC20Balance.js'; const EXTERNAL_RPC_URI_L1 = "https://alien-flashy-arm.ethereum-sepolia.quiknode.pro/2aeb75414e5ee0e930b64c2e7feff59efb537f30" const EXTERNAL_RPC_URI_L2 = "https://sepolia-rpc.scroll.io/" @@ -70,12 +79,22 @@ async function testTxLink() { } } -async function testGetUnclaimedWithdrawals() { +async function testGetWithdrawals() { try { - const results = await getUnclaimedWithdrawals("0x98110937b5D6C5FCB0BA99480e585D2364e9809C", BRIDGE_API_URI) + const results = await getWithdrawals("0x98110937b5D6C5FCB0BA99480e585D2364e9809C", BRIDGE_API_URI) console.log(results); } catch (error) { - console.error('Error in testGetUnclaimedWithdrawals:', error); + console.error('Error in testGetWithdrawals:', error); + } + +} + +async function testGetScrollERC20Balance() { + try { + const results = await getScrollERC20Balance("0x98110937b5D6C5FCB0BA99480e585D2364e9809C", "0x92e717f0564811A79A8d3E8F3cF1D65Ca06d2FA0", EXTERNAL_RPC_URI_L2) + console.log(results); + } catch (error) { + console.error('Error in testGetWithdrawals:', error); } } @@ -90,7 +109,8 @@ async function main() { await testGetGasOracleL2BaseFee(); await testAwaitTx(); await testTxLink(); - await testGetUnclaimedWithdrawals(); + await testGetWithdrawals(); + await testGetScrollERC20Balance() console.log('Test completed.'); } diff --git a/src/utils/onchain/blockLink.ts b/src/utils/onchain/blockLink.ts new file mode 100644 index 0000000..d8d89e1 --- /dev/null +++ b/src/utils/onchain/blockLink.ts @@ -0,0 +1,14 @@ +import { BlockExplorerParams, LookupType, constructBlockExplorerUrl } from "./index.js"; +import terminalLink from "terminal-link"; + +/** + * Creates a terminal-friendly link to an address on a block explorer. + * + * @param address - The Ethereum address. + * @param params - Optional parameters for constructing the block explorer URL. + * @returns A promise that resolves to a string containing the terminal-friendly link. + */ +export async function blockLink(block: number, params: BlockExplorerParams = {}): Promise { + const explorerUrl = await constructBlockExplorerUrl(`${block}`, LookupType.BLOCK, params); + return terminalLink(`${block}`, explorerUrl); +} \ No newline at end of file diff --git a/src/utils/onchain/constructBlockExplorerUrl.ts b/src/utils/onchain/constructBlockExplorerUrl.ts index 25ffb4b..a694b16 100644 --- a/src/utils/onchain/constructBlockExplorerUrl.ts +++ b/src/utils/onchain/constructBlockExplorerUrl.ts @@ -6,7 +6,8 @@ import { generateProvider } from './generateProvider.js'; */ export enum LookupType { TX = "tx", - ADDRESS = "address" + ADDRESS = "address", + BLOCK = "block" } /** diff --git a/src/utils/onchain/contractABIs.ts b/src/utils/onchain/contractABIs.ts new file mode 100644 index 0000000..47a1365 --- /dev/null +++ b/src/utils/onchain/contractABIs.ts @@ -0,0 +1,219 @@ +/** + * ABI for the L1 ETH Gateway contract, specifically for depositETH. + */ +export const l1ETHGatewayABI = [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasLimit", + "type": "uint256" + } + ], + "name": "depositETH", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +]; + +/** + * ABI for the L2 ETH Gateway contract. Specifically for withdrawETH. + */ +export const l2ETHGatewayABI = [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasLimit", + "type": "uint256" + } + ], + "name": "withdrawETH", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] + +export const l1GatewayRouterABI = [ + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_gasLimit", + "type": "uint256" + } + ], + "name": "depositERC20", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_l1Token", + "type": "address" + } + ], + "name": "getL2ERC20Address", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +]; + +export const l2GatewayRouterWithdrawERC20ABI = [{ + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasLimit", + "type": "uint256" + } + ], + "name": "withdrawERC20", + "outputs": [], + "stateMutability": "payable", + "type": "function" +}] + + +/** + * ABI for the relayMessageWithProof function. + */ +export const l1MessengerRelayMessageWithProofABI = [ + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "message", + "type": "bytes" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "batchIndex", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "merkleProof", + "type": "bytes" + } + ], + "internalType": "struct L2MessageProof", + "name": "proof", + "type": "tuple" + } + ], + "name": "relayMessageWithProof", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +]; + +// ABI for ERC20s created by the bridge +export const scrollERC20ABI = [ + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +]; \ No newline at end of file diff --git a/src/utils/onchain/erc20Contract.ts b/src/utils/onchain/erc20Contract.ts new file mode 100644 index 0000000..c64db4e --- /dev/null +++ b/src/utils/onchain/erc20Contract.ts @@ -0,0 +1,317 @@ +export const erc20Bytecode = "608060405234801561000f575f80fd5b506040518060400160405280600581526020017f546f6b656e0000000000000000000000000000000000000000000000000000008152506040518060400160405280600381526020017f544b4e0000000000000000000000000000000000000000000000000000000000815250816003908161008b91906105bd565b50806004908161009b91906105bd565b5050506100d8336100b06100dd60201b60201c565b60ff16600a6100bf91906107e8565b620f42406100cd9190610832565b6100e560201b60201c565b61095b565b5f6012905090565b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610155575f6040517fec442f0500000000000000000000000000000000000000000000000000000000815260040161014c91906108b2565b60405180910390fd5b6101665f838361016a60201b60201c565b5050565b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036101ba578060025f8282546101ae91906108cb565b92505081905550610288565b5f805f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2054905081811015610243578381836040517fe450d38c00000000000000000000000000000000000000000000000000000000815260040161023a9392919061090d565b60405180910390fd5b8181035f808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2081905550505b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036102cf578060025f8282540392505081905550610319565b805f808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040516103769190610942565b60405180910390a3505050565b5f81519050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f60028204905060018216806103fe57607f821691505b602082108103610411576104106103ba565b5b50919050565b5f819050815f5260205f209050919050565b5f6020601f8301049050919050565b5f82821b905092915050565b5f600883026104737fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82610438565b61047d8683610438565b95508019841693508086168417925050509392505050565b5f819050919050565b5f819050919050565b5f6104c16104bc6104b784610495565b61049e565b610495565b9050919050565b5f819050919050565b6104da836104a7565b6104ee6104e6826104c8565b848454610444565b825550505050565b5f90565b6105026104f6565b61050d8184846104d1565b505050565b5b81811015610530576105255f826104fa565b600181019050610513565b5050565b601f8211156105755761054681610417565b61054f84610429565b8101602085101561055e578190505b61057261056a85610429565b830182610512565b50505b505050565b5f82821c905092915050565b5f6105955f198460080261057a565b1980831691505092915050565b5f6105ad8383610586565b9150826002028217905092915050565b6105c682610383565b67ffffffffffffffff8111156105df576105de61038d565b5b6105e982546103e7565b6105f4828285610534565b5f60209050601f831160018114610625575f8415610613578287015190505b61061d85826105a2565b865550610684565b601f19841661063386610417565b5f5b8281101561065a57848901518255600182019150602085019450602081019050610635565b868310156106775784890151610673601f891682610586565b8355505b6001600288020188555050505b505050505050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f8160011c9050919050565b5f808291508390505b600185111561070e578086048111156106ea576106e961068c565b5b60018516156106f95780820291505b8081029050610707856106b9565b94506106ce565b94509492505050565b5f8261072657600190506107e1565b81610733575f90506107e1565b8160018114610749576002811461075357610782565b60019150506107e1565b60ff8411156107655761076461068c565b5b8360020a91508482111561077c5761077b61068c565b5b506107e1565b5060208310610133831016604e8410600b84101617156107b75782820a9050838111156107b2576107b161068c565b5b6107e1565b6107c484848460016106c5565b925090508184048111156107db576107da61068c565b5b81810290505b9392505050565b5f6107f282610495565b91506107fd83610495565b925061082a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8484610717565b905092915050565b5f61083c82610495565b915061084783610495565b925082820261085581610495565b9150828204841483151761086c5761086b61068c565b5b5092915050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f61089c82610873565b9050919050565b6108ac81610892565b82525050565b5f6020820190506108c55f8301846108a3565b92915050565b5f6108d582610495565b91506108e083610495565b92508282019050808211156108f8576108f761068c565b5b92915050565b61090781610495565b82525050565b5f6060820190506109205f8301866108a3565b61092d60208301856108fe565b61093a60408301846108fe565b949350505050565b5f6020820190506109555f8301846108fe565b92915050565b610de1806109685f395ff3fe608060405234801561000f575f80fd5b5060043610610091575f3560e01c8063313ce56711610064578063313ce5671461013157806370a082311461014f57806395d89b411461017f578063a9059cbb1461019d578063dd62ed3e146101cd57610091565b806306fdde0314610095578063095ea7b3146100b357806318160ddd146100e357806323b872dd14610101575b5f80fd5b61009d6101fd565b6040516100aa9190610a5a565b60405180910390f35b6100cd60048036038101906100c89190610b0b565b61028d565b6040516100da9190610b63565b60405180910390f35b6100eb6102af565b6040516100f89190610b8b565b60405180910390f35b61011b60048036038101906101169190610ba4565b6102b8565b6040516101289190610b63565b60405180910390f35b6101396102e6565b6040516101469190610c0f565b60405180910390f35b61016960048036038101906101649190610c28565b6102ee565b6040516101769190610b8b565b60405180910390f35b610187610333565b6040516101949190610a5a565b60405180910390f35b6101b760048036038101906101b29190610b0b565b6103c3565b6040516101c49190610b63565b60405180910390f35b6101e760048036038101906101e29190610c53565b6103e5565b6040516101f49190610b8b565b60405180910390f35b60606003805461020c90610cbe565b80601f016020809104026020016040519081016040528092919081815260200182805461023890610cbe565b80156102835780601f1061025a57610100808354040283529160200191610283565b820191905f5260205f20905b81548152906001019060200180831161026657829003601f168201915b5050505050905090565b5f80610297610467565b90506102a481858561046e565b600191505092915050565b5f600254905090565b5f806102c2610467565b90506102cf858285610480565b6102da858585610512565b60019150509392505050565b5f6012905090565b5f805f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20549050919050565b60606004805461034290610cbe565b80601f016020809104026020016040519081016040528092919081815260200182805461036e90610cbe565b80156103b95780601f10610390576101008083540402835291602001916103b9565b820191905f5260205f20905b81548152906001019060200180831161039c57829003601f168201915b5050505050905090565b5f806103cd610467565b90506103da818585610512565b600191505092915050565b5f60015f8473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2054905092915050565b5f33905090565b61047b8383836001610602565b505050565b5f61048b84846103e5565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff811461050c57818110156104fd578281836040517ffb8f41b20000000000000000000000000000000000000000000000000000000081526004016104f493929190610cfd565b60405180910390fd5b61050b84848484035f610602565b5b50505050565b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610582575f6040517f96c6fd1e0000000000000000000000000000000000000000000000000000000081526004016105799190610d32565b60405180910390fd5b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036105f2575f6040517fec442f050000000000000000000000000000000000000000000000000000000081526004016105e99190610d32565b60405180910390fd5b6105fd8383836107d1565b505050565b5f73ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff1603610672575f6040517fe602df050000000000000000000000000000000000000000000000000000000081526004016106699190610d32565b60405180910390fd5b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036106e2575f6040517f94280d620000000000000000000000000000000000000000000000000000000081526004016106d99190610d32565b60405180910390fd5b8160015f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f208190555080156107cb578273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040516107c29190610b8b565b60405180910390a35b50505050565b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610821578060025f8282546108159190610d78565b925050819055506108ef565b5f805f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20549050818110156108aa578381836040517fe450d38c0000000000000000000000000000000000000000000000000000000081526004016108a193929190610cfd565b60405180910390fd5b8181035f808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2081905550505b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610936578060025f8282540392505081905550610980565b805f808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040516109dd9190610b8b565b60405180910390a3505050565b5f81519050919050565b5f82825260208201905092915050565b8281835e5f83830152505050565b5f601f19601f8301169050919050565b5f610a2c826109ea565b610a3681856109f4565b9350610a46818560208601610a04565b610a4f81610a12565b840191505092915050565b5f6020820190508181035f830152610a728184610a22565b905092915050565b5f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610aa782610a7e565b9050919050565b610ab781610a9d565b8114610ac1575f80fd5b50565b5f81359050610ad281610aae565b92915050565b5f819050919050565b610aea81610ad8565b8114610af4575f80fd5b50565b5f81359050610b0581610ae1565b92915050565b5f8060408385031215610b2157610b20610a7a565b5b5f610b2e85828601610ac4565b9250506020610b3f85828601610af7565b9150509250929050565b5f8115159050919050565b610b5d81610b49565b82525050565b5f602082019050610b765f830184610b54565b92915050565b610b8581610ad8565b82525050565b5f602082019050610b9e5f830184610b7c565b92915050565b5f805f60608486031215610bbb57610bba610a7a565b5b5f610bc886828701610ac4565b9350506020610bd986828701610ac4565b9250506040610bea86828701610af7565b9150509250925092565b5f60ff82169050919050565b610c0981610bf4565b82525050565b5f602082019050610c225f830184610c00565b92915050565b5f60208284031215610c3d57610c3c610a7a565b5b5f610c4a84828501610ac4565b91505092915050565b5f8060408385031215610c6957610c68610a7a565b5b5f610c7685828601610ac4565b9250506020610c8785828601610ac4565b9150509250929050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f6002820490506001821680610cd557607f821691505b602082108103610ce857610ce7610c91565b5b50919050565b610cf781610a9d565b82525050565b5f606082019050610d105f830186610cee565b610d1d6020830185610b7c565b610d2a6040830184610b7c565b949350505050565b5f602082019050610d455f830184610cee565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f610d8282610ad8565b9150610d8d83610ad8565b9250828201905080821115610da557610da4610d4b565b5b9291505056fea26469706673582212201a2f1ea5861f197549d859f8421a1a8d8eef68aa77acfa336c8c7b08bd60d53e64736f6c634300081a0033" + +export const erc20ABI = [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "allowance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientAllowance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "approver", + "type": "address" + } + ], + "name": "ERC20InvalidApprover", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "ERC20InvalidReceiver", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "ERC20InvalidSender", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "ERC20InvalidSpender", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/src/utils/onchain/getL2TokenFromL1Address.ts b/src/utils/onchain/getL2TokenFromL1Address.ts new file mode 100644 index 0000000..4e90596 --- /dev/null +++ b/src/utils/onchain/getL2TokenFromL1Address.ts @@ -0,0 +1,26 @@ +import { Contract, ethers } from 'ethers'; +import { generateProvider, l1GatewayRouterABI, RpcSource } from './index.js'; + +/** + * Retrieves the L2 token address corresponding to an L1 token address. + * + * @param l1TokenAddress - The address of the token on L1. + * @param rpc - The RPC source for connecting to the network. + * @param l1GatewayRouterAddress - The address of the L1 Gateway Router contract. + * @returns A Promise that resolves to the address of the corresponding L2 token. + * @throws Will throw an error if the L2 token address cannot be retrieved. + */ + +export async function getL2TokenFromL1Address( + l1TokenAddress: string, + rpc: RpcSource, + l1GatewayRouterAddress: string +): Promise { + const provider = generateProvider(rpc) + + const l1GatewayRouter = new Contract(l1GatewayRouterAddress, l1GatewayRouterABI, provider); + + const l2TokenAddress = await l1GatewayRouter.getL2ERC20Address(l1TokenAddress); + + return l2TokenAddress; +} \ No newline at end of file diff --git a/src/utils/onchain/getScrollERC20Balance.ts b/src/utils/onchain/getScrollERC20Balance.ts new file mode 100644 index 0000000..cb2b301 --- /dev/null +++ b/src/utils/onchain/getScrollERC20Balance.ts @@ -0,0 +1,33 @@ +import { ethers } from 'ethers'; +import { RpcSource, scrollERC20ABI, generateProvider } from './index.js'; + +export async function getScrollERC20Balance( + walletAddress: string, + erc20Address: string, + rpc: RpcSource +): Promise { + try { + const provider = generateProvider(rpc) + const erc20Contract = new ethers.Contract(erc20Address, scrollERC20ABI, provider); + + let balance = BigInt(0); + let attempts = 0; + const maxAttempts = 5; + const delay = 15000; // 15 seconds + + while (balance === BigInt(0) && attempts < maxAttempts) { + balance = await erc20Contract.balanceOf(walletAddress); + if (balance > BigInt(0)) { + return balance.toString(); + } + attempts++; + console.log(`Attempt ${attempts}: Waiting for token balance...`); + await new Promise(resolve => setTimeout(resolve, delay)); + } + + return balance.toString(); + } catch (error) { + console.error('Error in getScrollERC20Balance:', error); + throw error; + } +} \ No newline at end of file diff --git a/src/utils/onchain/getUnclaimedWithdrawals.ts b/src/utils/onchain/getUnclaimedWithdrawals.ts deleted file mode 100644 index 0b41429..0000000 --- a/src/utils/onchain/getUnclaimedWithdrawals.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Represents an unclaimed withdrawal. - */ -export type UnclaimedWithdrawal = { - hash: string; - messageHash: string; - tokenType: number; - tokenAmounts: string[]; - l1TokenAddress: string; - l2TokenAddress: string; - blockNumber: number; - claimable: boolean; - from: string; - to: string; - value: string; -}; - -/** - * Retrieves unclaimed withdrawals for a given address. - * - * @param address - The address to check for unclaimed withdrawals. - * @param apiUri - The URI of the API to query for unclaimed withdrawals. - * @returns A promise that resolves to an array of UnclaimedWithdrawal objects. - * @throws An error if the API request fails or returns an error. - */ -export async function getUnclaimedWithdrawals(address: string, apiUri: string): Promise { - let url = `${apiUri}/l2/unclaimed/withdrawals?address=${address}&page=1&page_size=100`; - - try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const data = await response.json(); - - if (data.errcode !== 0) { - throw new Error(`API error: ${data.errmsg}`); - } - - const unclaimedWithdrawals: UnclaimedWithdrawal[] = data.data.results.map((result: any) => ({ - hash: result.hash, - messageHash: result.message_hash, - tokenType: result.token_type, - tokenAmounts: result.token_amounts, - l1TokenAddress: result.l1_token_address, - l2TokenAddress: result.l2_token_address, - blockNumber: result.block_number, - claimable: result.claim_info.claimable, - from: result.claim_info.from, - to: result.claim_info.to, - value: result.claim_info.value, - })); - - return unclaimedWithdrawals; - } catch (error) { - console.error('Error fetching unclaimed withdrawals:', error); - throw error; - } -} \ No newline at end of file diff --git a/src/utils/onchain/getWithdrawals.ts b/src/utils/onchain/getWithdrawals.ts new file mode 100644 index 0000000..683b05c --- /dev/null +++ b/src/utils/onchain/getWithdrawals.ts @@ -0,0 +1,98 @@ +/** + * Represents an unclaimed withdrawal. + */ +export type Withdrawal = { + hash: string; + replay_tx_hash: string; + refund_tx_hash: string; + message_hash: string; + token_type: number; + token_ids: any[]; + token_amounts: string[]; + message_type: number; + l1_token_address: string; + l2_token_address: string; + block_number: number; + tx_status: number; + counterpart_chain_tx: CounterpartChainTx; + claim_info: ClaimInfo | null; + block_timestamp: number; + batch_deposit_fee: string; +}; + +export interface ClaimInfo { + from: string; + to: string; + value: string; + nonce: string; + message: string; + proof: Proof; + claimable: boolean; +} + +export interface Proof { + batch_index: string; + merkle_proof: string; +} + +export interface CounterpartChainTx { + hash: string; + block_number: number; +} + +/** + * Retrieves unclaimed withdrawals for a given address. + * + * @param address - The address to check for unclaimed withdrawals. + * @param apiUri - The URI of the API to query for unclaimed withdrawals. + * @returns A promise that resolves to an array of UnclaimedWithdrawal objects. + * @throws An error if the API request fails or returns an error. + */ +export async function getWithdrawals(address: string, apiUri: string): Promise { + let url = `${apiUri}/l2/withdrawals?address=${address}&page=1&page_size=100`; + + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + + if (data.errcode !== 0) { + throw new Error(`API error: ${data.errmsg}`); + } + + const withdrawals: Withdrawal[] = data.data.results.map((result: any) => ({ + hash: result.hash, + from: result.from, + to: result.to, + value: result.value, + nonce: result.nonce, + block_number: result.block_number, + tx_status: result.tx_status, + counterpart_chain_tx: { + hash: result.counterpart_chain_tx.hash, + block_number: result.counterpart_chain_tx.block_number + }, + claim_info: result.claim_info ? { + from: result.claim_info.from, + to: result.claim_info.to, + value: result.claim_info.value, + nonce: result.claim_info.nonce, + message: result.claim_info.message, + proof: { + batch_index: result.claim_info.proof.batch_index, + merkle_proof: result.claim_info.proof.merkle_proof + }, + claimable: result.claim_info.claimable + } : null, + block_timestamp: result.block_timestamp, + batch_deposit_fee: result.batch_deposit_fee + })); + + return withdrawals; + } catch (error) { + console.error('Error fetching unclaimed withdrawals:', error); + throw error; + } +} \ No newline at end of file diff --git a/src/utils/onchain/index.ts b/src/utils/onchain/index.ts index d630d18..d41eeab 100644 --- a/src/utils/onchain/index.ts +++ b/src/utils/onchain/index.ts @@ -1,65 +1,22 @@ import { JsonRpcProvider, Wallet } from 'ethers'; -export { getFinalizedBlockHeight } from './getFinalizedBlockHeight.js'; -export { getCrossDomainMessageFromTx } from './getCrossDomainMessageFromTx.js'; -export { getPendingQueueIndex } from './getPendingQueueIndex.js'; -export { getGasOracleL2BaseFee } from './getGasOracleL2BaseFee.js'; +export { addressLink } from './addressLink.js' export { awaitTx } from './awaitTx.js'; +export { blockLink } from './blockLink.js' export { constructBlockExplorerUrl, LookupType } from './constructBlockExplorerUrl.js'; export type { BlockExplorerParams } from './constructBlockExplorerUrl.js'; -export { txLink } from './txLink.js' -export { addressLink } from './addressLink.js' +export * from './contractABIs.js' +export { erc20ABI, erc20Bytecode } from './erc20Contract.js' export { generateProvider } from './generateProvider.js' -export { getUnclaimedWithdrawals } from './getUnclaimedWithdrawals.js' +export { getCrossDomainMessageFromTx } from './getCrossDomainMessageFromTx.js'; +export { getFinalizedBlockHeight } from './getFinalizedBlockHeight.js'; +export { getGasOracleL2BaseFee } from './getGasOracleL2BaseFee.js'; +export { getL2TokenFromL1Address } from './getL2TokenFromL1Address.js'; +export { getPendingQueueIndex } from './getPendingQueueIndex.js'; +export { getWithdrawals } from './getWithdrawals.js' +export { txLink } from './txLink.js' /** * Represents a source for an RPC provider, which can be a JsonRpcProvider, a Wallet, or a string URL. */ export type RpcSource = JsonRpcProvider | Wallet | string; -/** - * ABI for the L1 ETH Gateway contract. - */ -export const l1ETHGatewayABI = [ - { - "inputs": [ - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "gasLimit", - "type": "uint256" - } - ], - "name": "depositETH", - "outputs": [], - "stateMutability": "payable", - "type": "function" - } -]; - -/** - * ABI for the L2 ETH Gateway contract. - */ -export const l2ETHGatewayABI = [ - { - "inputs": [ - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "gasLimit", - "type": "uint256" - } - ], - "name": "withdrawETH", - "outputs": [], - "stateMutability": "payable", - "type": "function" - } -] \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 3eae75f..83162fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2648,6 +2648,13 @@ cli-columns@^4.0.0: string-width "^4.2.3" strip-ansi "^6.0.1" +cli-cursor@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" + integrity sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg== + dependencies: + restore-cursor "^4.0.0" + cli-progress@^3.12.0: version "3.12.0" resolved "https://registry.yarnpkg.com/cli-progress/-/cli-progress-3.12.0.tgz#807ee14b66bcc086258e444ad0f19e7d42577942" @@ -2951,6 +2958,11 @@ ejs@^3.1.10: dependencies: jake "^10.8.5" +emoji-regex@^10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.3.0.tgz#76998b9268409eb3dae3de989254d456e70cfe23" + integrity sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -3632,6 +3644,11 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-east-asian-width@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz#5e6ebd9baee6fb8b7b6bd505221065f0cd91f64e" + integrity sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA== + get-func-name@^2.0.1, get-func-name@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" @@ -4150,6 +4167,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-interactive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-2.0.0.tgz#40c57614593826da1100ade6059778d597f16e90" + integrity sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ== + is-lambda@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" @@ -4243,6 +4265,16 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +is-unicode-supported@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz#d824984b616c292a2e198207d4a609983842f714" + integrity sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ== + +is-unicode-supported@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-2.0.0.tgz#fdf32df9ae98ff6ab2cedc155a5a6e895701c451" + integrity sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q== + is-weakref@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" @@ -4605,6 +4637,14 @@ log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" +log-symbols@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-6.0.0.tgz#bb95e5f05322651cac30c0feb6404f9f2a8a9439" + integrity sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw== + dependencies: + chalk "^5.3.0" + is-unicode-supported "^1.3.0" + loupe@^2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" @@ -4684,6 +4724,11 @@ mime-types@^2.1.12, mime-types@~2.1.19: dependencies: mime-db "1.52.0" +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + mimic-response@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" @@ -5205,6 +5250,13 @@ once@^1.3.0: dependencies: wrappy "1" +onetime@^5.1.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + openid-client@^5.3.0: version "5.6.5" resolved "https://registry.yarnpkg.com/openid-client/-/openid-client-5.6.5.tgz#c149ad07b9c399476dc347097e297bbe288b8b00" @@ -5227,6 +5279,21 @@ optionator@^0.9.3: type-check "^0.4.0" word-wrap "^1.2.5" +ora@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-8.0.1.tgz#6dcb9250a629642cbe0d2df3a6331ad6f7a2af3e" + integrity sha512-ANIvzobt1rls2BDny5fWZ3ZVKyD6nscLvfFRpQgfWsythlcsVUC9kL0zq6j2Z5z9wwp1kd7wpsD/T9qNPVLCaQ== + dependencies: + chalk "^5.3.0" + cli-cursor "^4.0.0" + cli-spinners "^2.9.2" + is-interactive "^2.0.0" + is-unicode-supported "^2.0.0" + log-symbols "^6.0.0" + stdin-discarder "^0.2.1" + string-width "^7.0.0" + strip-ansi "^7.1.0" + os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -5692,6 +5759,14 @@ responselike@^3.0.0: dependencies: lowercase-keys "^3.0.0" +restore-cursor@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-4.0.0.tgz#519560a4318975096def6e609d44100edaa4ccb9" + integrity sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + retry@0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" @@ -5859,6 +5934,11 @@ side-channel@^1.0.4: get-intrinsic "^1.2.4" object-inspect "^1.13.1" +signal-exit@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + signal-exit@^4.0.1, signal-exit@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" @@ -6008,6 +6088,11 @@ ssri@^10.0.0, ssri@^10.0.6: dependencies: minipass "^7.0.3" +stdin-discarder@^0.2.1: + version "0.2.2" + resolved "https://registry.yarnpkg.com/stdin-discarder/-/stdin-discarder-0.2.2.tgz#390037f44c4ae1a1ae535c5fe38dc3aba8d997be" + integrity sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ== + stream-buffers@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-3.0.3.tgz#9fc6ae267d9c4df1190a781e011634cac58af3cd" @@ -6031,6 +6116,15 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string-width@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.2.0.tgz#b5bb8e2165ce275d4d43476dd2700ad9091db6dc" + integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ== + dependencies: + emoji-regex "^10.3.0" + get-east-asian-width "^1.0.0" + strip-ansi "^7.1.0" + string.prototype.trim@^1.2.9: version "1.2.9" resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" @@ -6066,7 +6160,7 @@ string.prototype.trimstart@^1.0.8: dependencies: ansi-regex "^5.0.1" -strip-ansi@^7.0.1: +strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==