diff --git a/README.md b/README.md index fb7fc53..6c09098 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,17 @@ From root directory: yarn install ``` +## Environment Setup + +To simplify providing environment variables across all tutorials, a helper script automates creating `.env` files from each existing `.env-sample`. + +```bash +yarn setup-envs + +# To update existing .env files as well +yarn setup-envs --update +``` + ## Testing 1. Start the nitro-testnode (you can find instructions [here](https://docs.arbitrum.io/run-arbitrum-node/run-local-full-chain-simulation)) with the following parameters: @@ -26,6 +37,10 @@ yarn install yarn run testAll ``` +### Notes on RPCs and Finality + +Using public testnet RPCs can be slow because many tutorials wait for transaction finality or multiple confirmations. Some tests may take 10–15 minutes to complete on testnets. For faster and more reliable execution, prefer running a local node and pointing your environment variables (`CHAIN_RPC`, `PARENT_CHAIN_RPC`, and `L1_RPC` when applicable) to local RPC endpoints. + ## What's included? #### :white_check_mark: Basics diff --git a/customNetwork.json b/customNetwork.json index ae17823..4c5e6c5 100644 --- a/customNetwork.json +++ b/customNetwork.json @@ -3,11 +3,11 @@ "chainID": 412346, "confirmPeriodBlocks": 20, "ethBridge": { - "bridge": "0x5eCF728ffC5C5E802091875f96281B5aeECf6C49", - "inbox": "0x9f8c1c641336A371031499e3c362e40d58d0f254", - "outbox": "0x50143333b44Ea46255BEb67255C9Afd35551072F", - "rollup": "0xe5Ab92C74CD297F0a1F2914cE37204FC5Bc4e82D", - "sequencerInbox": "0x18d19C5d3E685f5be5b9C86E097f0E439285D216" + "bridge": "0xd21d0cA137A64165E3C3702a37C5D42E6a5CdACC", + "inbox": "0xF52c6E85A9E6287942fD913DfCc269EB571816a6", + "outbox": "0xbf2265Cd6Fd551e5EAA862Fd81BA317D585eE693", + "rollup": "0x1b172F52EC0EFFeeC7946903F20410B5d6571AA0", + "sequencerInbox": "0x7F6958d308593dC0BdEa73c4bcD742965E1C79af" }, "explorerUrl": "", "isArbitrum": true, @@ -19,17 +19,17 @@ "nitroGenesisL1Block": 0, "depositTimeout": 900000, "tokenBridge": { - "parentGatewayRouter": "0x093AAa96CD4387A68FC0e24C60140938Dc812549", + "parentGatewayRouter": "0x145899e2EF2A3f460Cfb2F7897615F5758000F66", "childGatewayRouter": "0x32656396981868E925280FB772b3f806892cf4bF", - "parentErc20Gateway": "0x00D9fE1a2B67B8151aEdE8855c95E58D73FB4245", + "parentErc20Gateway": "0x4D9Aa26aCee73293ba84fB1FcA8100006239860E", "childErc20Gateway": "0x7424e3DAAAAcd867c85ceB75c1E00119F2ee5eb7", - "parentCustomGateway": "0x8407E6180dC009D20D26D4BABB4790C1d4E6D2aA", + "parentCustomGateway": "0xC9ba2864E0E5dC3B78a539fA5e50b4508595A236", "childCustomGateway": "0x0B35cfE62314C3852A0942b5830c728353BD654F", - "parentWethGateway": "0xB8F48Ba39fCfB44d70F6008fe1bf4F3E744044AF", + "parentWethGateway": "0xCb24A87c3Dfc5b6DC8f1Da8D62D731fA1109A32C", "childWethGateway": "0x67aE8014BD1A0c1Ed747715d22b3b3a188aC324B", "parentWeth": "0x7E32b54800705876d3b5cFbc7d9c226a211F7C1a", "childWeth": "0xA1abD387192e3bb4e84D3109181F9f005aBaF5CA", - "parentProxyAdmin": "0x2A1f38c9097e7883570e0b02BFBE6869Cc25d8a3", + "parentProxyAdmin": "0x275FC51309e5928Cb085b463ADEF5cbD45c76b62", "childProxyAdmin": "0x9F95547ABB0FfC92b4E37b3124d1e8613d5aB74A", "parentMultiCall": "0x49117fC32930E324F2E9A7BeA588FFb26008b8eC", "childMultiCall": "0x6B1E93aE298B64e8f5b9f43B65Dd8F1eaA6DD4c3" @@ -41,11 +41,11 @@ "chainID": 333333, "confirmPeriodBlocks": 20, "ethBridge": { - "bridge": "0xA584795e24628D9c067A6480b033C9E96281fcA3", - "inbox": "0xDcA690902d3154886Ec259308258D10EA5450996", - "outbox": "0xda243bD61B011024FC923164db75Dde198AC6175", - "rollup": "0x47b238E195b638b8972Cb3649e5d6775c279245d", - "sequencerInbox": "0x16c54EE2015CD824415c2077F4103f444E00A8cb" + "bridge": "0xFbd865f29BadF3fdcf14b7e742F78c8DdF7fa4C9", + "inbox": "0x7CB5a1892d31936a13CeE3a4A8183Ed40300e87c", + "outbox": "0x3dAEcE87269310A2f055920ef5C84679ca50F218", + "rollup": "0x9129f7A7f4D5A87F971Fe15C554AD1CF22ceb4EF", + "sequencerInbox": "0x99c807B58c00DE2b068e521AAC227A1e8ADe1fa9" }, "explorerUrl": "", "isArbitrum": true, @@ -57,17 +57,17 @@ "nitroGenesisL1Block": 0, "depositTimeout": 900000, "tokenBridge": { - "parentGatewayRouter": "0xfE03DBdf7A126994dBd749631D7fbaB58C618c58", + "parentGatewayRouter": "0x9a6E8E638451781B47b8632a11E56020270A335B", "childGatewayRouter": "0x8B6BC759226f8Fe687c8aD8Cc0DbF85E095e9297", - "parentErc20Gateway": "0x6B0805Fc6e275ef66a0901D0CE68805631E271e5", + "parentErc20Gateway": "0x7A6b9Ca6e4f87E6a641c1AbD7671aD42bF538977", "childErc20Gateway": "0xaa7d51aFFEeB32d99b1CB2fd6d81D7adA4a896e8", - "parentCustomGateway": "0xA191D519260A06b32f8D04c84b9F457B8Caa0514", + "parentCustomGateway": "0xDCa39C8Fbd686cb8838106Ed94C0B9e9802697d6", "childCustomGateway": "0xD4816AeF8f85A3C1E01Cd071a81daD4fa941625f", - "parentWethGateway": "0x77603b0ea6a797C74Fa9ef11b5BdE04A4E03D550", + "parentWethGateway": "0x3bF927fc03d13bD2199b02a029ABb490D13706b3", "childWethGateway": "0xA6AB233B3c7bfd0399834897b5073974A3D467e2", "parentWeth": "0xA1abD387192e3bb4e84D3109181F9f005aBaF5CA", "childWeth": "0x582a8dBc77f665dF2c49Ce0a138978e9267dd968", - "parentProxyAdmin": "0x1A61102c26ad3f64bA715B444C93388491fd8E68", + "parentProxyAdmin": "0x74513d47D265527f0647eD5B90072578c609B378", "childProxyAdmin": "0x36C56eC2CF3a3f53db9F01d0A5Ae84b36fb0A1e2", "parentMultiCall": "0x20a3627Dcc53756E38aE3F92717DE9B23617b422", "childMultiCall": "0x052B15c8Ff0544287AE689C4F2FC53A3905d7Db3" diff --git a/package.json b/package.json index 81cb9ab..6b3ab87 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "lint": "eslint .", "prettier:format": "prettier './**/*.{js,json,md,sol,ts,yml}' --write && yarn run lint --fix", "prettier:check": "prettier './**/*.{js,json,md,sol,ts,yml}' --check && yarn run lint", - "testAll": "tests/runAll.sh" + "testAll": "tests/runAll.sh", + "setup-envs": "node scripts/setup-envs.js" }, "devDependencies": { "@offchainlabs/eslint-config-typescript": "^0.2.1", diff --git a/packages/arb-shared-dependencies/index.js b/packages/arb-shared-dependencies/index.js index 9e33ae3..ee9dba9 100644 --- a/packages/arb-shared-dependencies/index.js +++ b/packages/arb-shared-dependencies/index.js @@ -53,18 +53,21 @@ const requireEnvVariables = (envVars) => { console.log('Environmental variables properly set 👍'); }; -const addCustomNetworkFromFile = () => { +const addCustomNetworkFromFile = (registerFn) => { const pathToCustomNetworkFile = path.join(__dirname, '..', '..', 'customNetwork.json'); if (!fs.existsSync(pathToCustomNetworkFile)) { return; } + // Use the provided register function, or fall back to the local one + const register = registerFn || registerCustomArbitrumNetwork; + const customNetworkFileContents = fs.readFileSync(pathToCustomNetworkFile, 'utf8'); const customNetworkInformation = JSON.parse(customNetworkFileContents); if (customNetworkInformation instanceof Array) { - customNetworkInformation.map((customNetwork) => registerCustomArbitrumNetwork(customNetwork)); + customNetworkInformation.map((customNetwork) => register(customNetwork)); } else { - registerCustomArbitrumNetwork(customNetworkInformation); + register(customNetworkInformation); } }; diff --git a/packages/block-verification-in-parent-chain-assertion/README.md b/packages/block-verification-in-parent-chain-assertion/README.md index b8d6831..482ce0a 100644 --- a/packages/block-verification-in-parent-chain-assertion/README.md +++ b/packages/block-verification-in-parent-chain-assertion/README.md @@ -1,12 +1,12 @@ # Block verification in an assertion posted on the parent chain -This tutorial shows how to verify whether a block of a chain has been processed as part of an RBlock assertion on its parent chain. +This tutorial shows how to verify whether a block of a chain has been processed as part of an assertion on its parent chain. -It uses the `Rollup` contract to find the latest confirmed (or created if configured in the script) RBlock/node, find the event that created it, and get the latest processed block hash of the child chain that's part of the assertion of that RBlock/node. +It uses the `Rollup` contract to find the latest confirmed assertion hash, find the `AssertionCreated` event that created it, and get the latest processed block hash of the child chain that's part of that assertion's `afterState`. Then it checks whether the block number passed as argument was created before the latest block hash of the child chain processed. -See [./exec.js](./scripts/exec.js) for inline explanations. +See [./exec.ts](./scripts/exec.ts) for inline explanations. ## Set environment variables diff --git a/packages/block-verification-in-parent-chain-assertion/scripts/exec.ts b/packages/block-verification-in-parent-chain-assertion/scripts/exec.ts index 8ba9734..110ca6b 100644 --- a/packages/block-verification-in-parent-chain-assertion/scripts/exec.ts +++ b/packages/block-verification-in-parent-chain-assertion/scripts/exec.ts @@ -1,6 +1,6 @@ import { providers, Contract } from 'ethers'; -import { getArbitrumNetwork } from '@arbitrum/sdk'; -import { RollupCore__factory } from '@arbitrum/sdk/dist/lib/abi/factories/RollupCore__factory'; +import { getArbitrumNetwork, registerCustomArbitrumNetwork } from '@arbitrum/sdk'; +import { BoldRollupUserLogic__factory } from '@arbitrum/sdk/dist/lib/abi-bold/factories/BoldRollupUserLogic__factory'; import { arbLog, requireEnvVariables, addCustomNetworkFromFile } from 'arb-shared-dependencies'; require('dotenv').config(); requireEnvVariables(['CHAIN_RPC', 'PARENT_CHAIN_RPC']); @@ -11,93 +11,64 @@ requireEnvVariables(['CHAIN_RPC', 'PARENT_CHAIN_RPC']); const parentChainProvider = new providers.JsonRpcProvider(process.env.PARENT_CHAIN_RPC); const childChainProvider = new providers.JsonRpcProvider(process.env.CHAIN_RPC); -/** - * Use the latest node created instead of the last confirmed one - */ -const useCreatedNodeInsteadOfConfirmed = false; - const main = async (childChainBlockNumberToVerify: number) => { await arbLog( 'Find whether a block of the child chain has been processed as part of an RBlock on the parent chain', ); - /** - * Add the custom network configuration to the SDK if present - */ - addCustomNetworkFromFile(); + addCustomNetworkFromFile(registerCustomArbitrumNetwork); - /** - * Use childChainNetwork to find the Rollup contract's address and instantiate a contract handler - */ const childChainNetwork = await getArbitrumNetwork(childChainProvider); const rollupAddress = childChainNetwork.ethBridge.rollup; - const rollup = new Contract(rollupAddress, RollupCore__factory.abi, parentChainProvider); + const rollup = new Contract(rollupAddress, BoldRollupUserLogic__factory.abi, parentChainProvider); console.log(`Rollup contract found at address ${rollup.address}`); - /** - * Get the latest node created or confirmed - */ - const nodeId = useCreatedNodeInsteadOfConfirmed - ? await rollup.latestNodeCreated() - : await rollup.latestConfirmed(); - console.log( - `Latest ${useCreatedNodeInsteadOfConfirmed ? 'created' : 'confirmed'} Rblock/node: ${nodeId}`, - ); + const assertionHash: string = await rollup.latestConfirmed(); + console.log(`Latest confirmed assertion hash: ${assertionHash}`); - /** - * Find the NodeCreated event - */ - const nodeCreatedEventFilter = rollup.filters.NodeCreated(nodeId); - const nodeCreatedEvents = await rollup.queryFilter(nodeCreatedEventFilter); - if (!nodeCreatedEvents) { - throw new Error(`INTERNAL ERROR: NodeCreated events not found for Rblock/node: ${nodeId}`); + const assertionCreatedEventFilter = (rollup as any).filters.AssertionCreated(assertionHash); + const assertionCreatedEvents = await rollup.queryFilter(assertionCreatedEventFilter); + if (!assertionCreatedEvents || assertionCreatedEvents.length === 0) { + throw new Error( + `INTERNAL ERROR: AssertionCreated events not found for assertion: ${assertionHash}`, + ); } - const nodeCreatedEvent = nodeCreatedEvents[0]; - console.log(`NodeCreated event found in transaction ${nodeCreatedEvent.transactionHash}`); + const assertionCreatedEvent = assertionCreatedEvents[0]; + console.log( + `AssertionCreated event found in transaction ${assertionCreatedEvent.transactionHash}`, + ); - /** - * Finding the assertion within the NodeCreated event, and getting the afterState - */ - if (!nodeCreatedEvent.args) { + if (!assertionCreatedEvent.args) { throw new Error( - `INTERNAL ERROR: NodeCreated event does not have an assertion for Rblock/node: ${nodeId}`, + `INTERNAL ERROR: AssertionCreated event does not have an assertion for hash: ${assertionHash}`, ); } - const assertion = nodeCreatedEvent.args.assertion; + const assertion = (assertionCreatedEvent as any).args.assertion; const afterState = assertion.afterState; - /** - * Latest child chain's block hash processed is in the first element of the bytes32Vals property in the globalState - */ + // Latest child chain's block hash processed is in the first element of the bytes32Vals property in the globalState const lastChildChainBlockHash = afterState.globalState.bytes32Vals[0]; console.log( `Last block hash of the child chain processed in this Rblock/node: ${lastChildChainBlockHash}`, ); - /** - * Getting the block number from that block hash - */ + // Getting the block number from that block hash + const lastChildChainBlock = await childChainProvider.getBlock(lastChildChainBlockHash); const lastChildChainBlockNumber = lastChildChainBlock.number; console.log( `Last block number of the child chain processed in this Rblock/node: ${lastChildChainBlockNumber}`, ); - /** - * Final verification - */ + // Final verification console.log(`************`); if (lastChildChainBlockNumber > childChainBlockNumberToVerify) { console.log( - `${childChainBlockNumberToVerify} has been processed as part of the latest ${ - useCreatedNodeInsteadOfConfirmed ? 'created' : 'confirmed' - } RBlock/node`, + `${childChainBlockNumberToVerify} has been processed as part of the latest confirmed assertion`, ); } else { console.log( - `${childChainBlockNumberToVerify} has NOT been processed as part of the latest ${ - useCreatedNodeInsteadOfConfirmed ? 'created' : 'confirmed' - } RBlock/node`, + `${childChainBlockNumberToVerify} has NOT been processed as part of the latest confirmed assertion`, ); } console.log(`************`); @@ -106,9 +77,7 @@ const main = async (childChainBlockNumberToVerify: number) => { // Getting the transaction hash from the command arguments if (process.argv.length < 3) { console.log( - `Missing block number of the child chain to verify whether it has been processed in the latest ${ - useCreatedNodeInsteadOfConfirmed ? 'created' : 'confirmed' - } RBlock/node`, + 'Missing block number of the child chain to verify whether it has been processed in the latest confirmed assertion', ); console.log(`Usage: yarn run exec `); process.exit(1); diff --git a/scripts/setup-envs.js b/scripts/setup-envs.js new file mode 100644 index 0000000..35bd0ff --- /dev/null +++ b/scripts/setup-envs.js @@ -0,0 +1,173 @@ +#!/usr/bin/env node + +/* + * Environment setup script for Arbitrum Tutorials. + * Usage: + * yarn setup-envs # prompts for values (L1_RPC optional), does NOT overwrite existing .env + * yarn setup-envs --update # prompts and updates existing .env files as well + */ + +/* eslint-disable no-await-in-loop */ + +const fs = require('fs'); +const path = require('path'); +const readline = require('readline'); + +const VARS = ['PRIVATE_KEY', 'CHAIN_RPC', 'PARENT_CHAIN_RPC', 'L1_RPC', 'TransferTo']; +const ARGS = new Set(process.argv.slice(2)); +const UPDATE_EXISTING = ARGS.has('--update'); + +function log(msg) { + console.log(msg); +} +function warn(msg) { + console.warn(msg); +} +function error(msg) { + console.error(msg); +} + +async function promptForValues() { + const values = {}; + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + const ask = (q) => new Promise((res) => rl.question(q, (ans) => res(ans.trim()))); + for (const v of VARS) { + const optional = v === 'L1_RPC' || v === 'TransferTo'; + const existing = process.env[v] ? ` [default: ${process.env[v]}]` : ''; + const prompt = optional ? `${v} (optional)${existing}: ` : `${v}${existing}: `; + const ans = await ask(prompt); + if (ans) { + values[v] = ans; + } else if (process.env[v]) { + values[v] = process.env[v]; + } else if (!optional) { + error(`Required value missing: ${v}`); + process.exit(1); + } + } + rl.close(); + return values; +} + +function replaceOrAppend(contentLines, key, value) { + const prefix = key + '='; + let replaced = false; + for (let i = 0; i < contentLines.length; i++) { + const line = contentLines[i]; + if (line.startsWith(prefix)) { + contentLines[i] = `${key}="${value}"`; + replaced = true; + break; + } + } + if (!replaced) contentLines.push(`${key}="${value}"`); +} + +function processSampleFile(samplePath, envPath, values) { + const lines = fs.readFileSync(samplePath, 'utf8').split(/\r?\n/); + while (lines.length && lines[lines.length - 1].trim() === '') lines.pop(); + for (const v of VARS) { + if (values[v]) replaceOrAppend(lines, v, values[v]); + } + + const newContent = lines.join('\n') + '\n'; + fs.writeFileSync(envPath, newContent, 'utf8'); +} + +function processDirectory(dir, values, summary) { + const samplePath = path.join(dir, '.env-sample'); + const envPath = path.join(dir, '.env'); + const hasSample = fs.existsSync(samplePath); + const hasEnv = fs.existsSync(envPath); + + if (!hasSample && !hasEnv) return; + try { + if (hasEnv) { + if (UPDATE_EXISTING) { + processSampleFile(envPath, envPath, values); + summary.updated.push(dir); + } else { + summary.skipped.push(dir); + } + } else if (hasSample) { + processSampleFile(samplePath, envPath, values); + summary.updated.push(dir); + } + } catch (e) { + summary.errors.push({ dir, error: e.message }); + } +} + +function validate(values) { + const pk = values.PRIVATE_KEY && String(values.PRIVATE_KEY).trim(); + if (!/^(?:0x)?[a-fA-F0-9]{64}$/.test(pk)) { + throw new Error('PRIVATE_KEY must be 64 hex characters, with or without a leading 0x.'); + } + // Normalize: ensure stored value has 0x prefix + if (!pk.startsWith('0x')) { + values.PRIVATE_KEY = `0x${pk}`; + } else { + values.PRIVATE_KEY = pk; + } + ['CHAIN_RPC', 'PARENT_CHAIN_RPC'].forEach((k) => { + if (!/^https?:\/\/\S+$/i.test(String(values[k] || ''))) { + throw new Error(`${k} must be an http(s) URL.`); + } + }); + if (values.L1_RPC && !/^https?:\/\/\S+$/i.test(String(values.L1_RPC))) { + throw new Error('L1_RPC must be an http(s) URL if provided.'); + } + if (values.TransferTo) { + const addr = String(values.TransferTo).trim(); + if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) { + throw new Error('TransferTo must be a valid 0x-prefixed Ethereum address.'); + } + values.TransferTo = addr; + } +} + +async function main() { + log('Arbitrum Tutorials environment setup starting...'); + const values = await promptForValues(); + + if (!values.PRIVATE_KEY || !values.CHAIN_RPC || !values.PARENT_CHAIN_RPC) { + error('PRIVATE_KEY, CHAIN_RPC, and PARENT_CHAIN_RPC are required.'); + process.exit(1); + } + + validate(values); + + const rootDir = path.resolve(__dirname, '..'); + const packagesDir = path.join(rootDir, 'packages'); + + const summary = { updated: [], skipped: [], errors: [] }; + + processDirectory(rootDir, values, summary); + + if (fs.existsSync(packagesDir)) { + const entries = fs.readdirSync(packagesDir); + for (const entry of entries) { + const fullPath = path.join(packagesDir, entry); + if (fs.statSync(fullPath).isDirectory()) { + processDirectory(fullPath, values, summary); + } + } + } + + log('Environment setup complete.'); + log(`Updated: ${summary.updated.length}`); + if (summary.skipped.length) + log(`Skipped (existing .env, no --update): ${summary.skipped.length}`); + if (summary.errors.length) { + warn('Errors encountered:'); + for (const e of summary.errors) warn(` - ${e.dir}: ${e.error}`); + } + + log('\nExample: run a tutorial script after env creation:'); + log(' cd packages/greeter && npx hardhat run scripts/sendParentMessage.ts'); +} + +main().catch((e) => { + error(e.stack || e.message); + process.exit(1); +}); diff --git a/yarn.lock b/yarn.lock index f861af2..2039192 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,6 +34,17 @@ async-mutex "^0.4.0" ethers "^5.1.0" +"@arbitrum/sdk@^v4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@arbitrum/sdk/-/sdk-4.0.4.tgz#32f3cfa75d2b3f9ab0be01eb807112e0001f428d" + integrity sha512-GscwlkHYmPzRKs9huDHntbqx1xMRhTraTUvTC9exu+prjndKxHe9ZORuIcqmtEqwLwma/l8nqxI+k+pEEdIO6Q== + dependencies: + "@ethersproject/address" "^5.0.8" + "@ethersproject/bignumber" "^5.1.1" + "@ethersproject/bytes" "^5.0.8" + async-mutex "^0.4.0" + ethers "^5.1.0" + "@arbitrum/token-bridge-contracts@^1.2.3": version "1.2.3" resolved "https://registry.yarnpkg.com/@arbitrum/token-bridge-contracts/-/token-bridge-contracts-1.2.3.tgz#b08b22b5dcac255149a10bd8b66b0b55be7f3329"