diff --git a/src/__snapshots__/createRollupPrepareDeploymentParamsConfig.unit.test.ts.snap b/src/__snapshots__/createRollupPrepareDeploymentParamsConfig.unit.test.ts.snap index a9496ed0..75e231b6 100644 --- a/src/__snapshots__/createRollupPrepareDeploymentParamsConfig.unit.test.ts.snap +++ b/src/__snapshots__/createRollupPrepareDeploymentParamsConfig.unit.test.ts.snap @@ -1,5 +1,26 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`creates a config for a chain on top of a custom parent chain 1`] = ` +{ + "baseStake": 100000000000000000n, + "chainConfig": "{\\"homesteadBlock\\":0,\\"daoForkBlock\\":null,\\"daoForkSupport\\":true,\\"eip150Block\\":0,\\"eip150Hash\\":\\"0x0000000000000000000000000000000000000000000000000000000000000000\\",\\"eip155Block\\":0,\\"eip158Block\\":0,\\"byzantiumBlock\\":0,\\"constantinopleBlock\\":0,\\"petersburgBlock\\":0,\\"istanbulBlock\\":0,\\"muirGlacierBlock\\":0,\\"berlinBlock\\":0,\\"londonBlock\\":0,\\"clique\\":{\\"period\\":0,\\"epoch\\":0},\\"arbitrum\\":{\\"EnableArbOS\\":true,\\"AllowDebugPrecompiles\\":false,\\"DataAvailabilityCommittee\\":false,\\"InitialArbOSVersion\\":32,\\"GenesisBlockNum\\":0,\\"MaxCodeSize\\":24576,\\"MaxInitCodeSize\\":49152,\\"InitialChainOwner\\":\\"0xd8da6bf26964af9d7eed9e03e53415d37aa96045\\"},\\"chainId\\":123}", + "chainId": 123n, + "confirmPeriodBlocks": 1n, + "extraChallengeTimeBlocks": 0n, + "genesisBlockNum": 0n, + "loserStakeEscrow": "0x0000000000000000000000000000000000000000", + "owner": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", + "sequencerInboxMaxTimeVariation": { + "delayBlocks": 2n, + "delaySeconds": 4n, + "futureBlocks": 3n, + "futureSeconds": 5n, + }, + "stakeToken": "0x0000000000000000000000000000000000000000", + "wasmModuleRoot": "0x184884e1eb9fefdc158f6c8ac912bb183bf3cf83f0090317e0bc4ac5860baa39", +} +`; + exports[`creates config for a chain on top of arbitrum one with defaults 1`] = ` { "baseStake": 100000000000000000n, diff --git a/src/chains.ts b/src/chains.ts index b2525fe4..04dc167c 100644 --- a/src/chains.ts +++ b/src/chains.ts @@ -1,4 +1,4 @@ -import { defineChain } from 'viem'; +import { defineChain, Chain, ChainContract, isAddress, zeroAddress } from 'viem'; import { mainnet, arbitrum as arbitrumOne, @@ -59,6 +59,72 @@ const nitroTestnodeL3 = defineChain({ testnet: true, }); +const customParentChains: Record = {}; + +export function getCustomParentChains(): Chain[] { + return Object.values(customParentChains); +} + +/** + * Registers a custom parent chain. + * + * @param {Chain} chain Regular `Chain` object with mandatory `contracts.rollupCreator` and `contracts.tokenBridgeCreator` fields. + * + * @example + * registerCustomParentChain({ + * id: 123_456, + * name: `My Chain`, + * network: `my-chain`, + * nativeCurrency: { + * name: 'Ether', + * symbol: 'ETH', + * decimals: 18, + * }, + * rpcUrls: { + * public: { + * http: ['http://localhost:8080'], + * }, + * default: { + * http: ['http://localhost:8080'], + * }, + * }, + * // these are mandatory + * contracts: { + * rollupCreator: { + * address: '0x0000000000000000000000000000000000000001', + * }, + * tokenBridgeCreator: { + * address: '0x0000000000000000000000000000000000000002', + * }, + * }, + * }); + */ +export function registerCustomParentChain( + chain: Chain & { + contracts: { + rollupCreator: ChainContract; + tokenBridgeCreator: ChainContract; + }; + }, +) { + const rollupCreator = chain.contracts.rollupCreator.address; + const tokenBridgeCreator = chain.contracts.tokenBridgeCreator.address; + + if (!isAddress(rollupCreator) || rollupCreator === zeroAddress) { + throw new Error( + `"contracts.rollupCreator.address" is invalid for custom parent chain with id ${chain.id}`, + ); + } + + if (!isAddress(tokenBridgeCreator) || tokenBridgeCreator === zeroAddress) { + throw new Error( + `"contracts.tokenBridgeCreator.address" is invalid for custom parent chain with id ${chain.id}`, + ); + } + + customParentChains[chain.id] = chain; +} + export const chains = [ // mainnet L1 mainnet, diff --git a/src/chains.unit.test.ts b/src/chains.unit.test.ts new file mode 100644 index 00000000..ec5d7443 --- /dev/null +++ b/src/chains.unit.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from 'vitest'; + +import { getCustomParentChains, registerCustomParentChain } from './chains'; +import { testHelper_createCustomParentChain } from './testHelpers'; + +describe('registerCustomParentChain', () => { + it(`throws if "contracts.rollupCreator.address" is invalid`, () => { + // omit contracts from the chain + const { contracts, ...chain } = testHelper_createCustomParentChain(); + + expect(() => + registerCustomParentChain({ + ...chain, + contracts: { + rollupCreator: { + address: '0x123', + }, + tokenBridgeCreator: { + address: '0x123', + }, + }, + }), + ).toThrowError( + `"contracts.rollupCreator.address" is invalid for custom parent chain with id ${chain.id}`, + ); + }); + + it(`throws if "contracts.tokenBridgeCreator.address" is invalid`, () => { + // omit contracts from the chain + const { contracts, ...chain } = testHelper_createCustomParentChain(); + + expect(() => + registerCustomParentChain({ + ...chain, + contracts: { + rollupCreator: { + // use a correct address for the RollupCreator + address: contracts.rollupCreator.address, + }, + tokenBridgeCreator: { + address: '0x0', + }, + }, + }), + ).toThrowError( + `"contracts.tokenBridgeCreator.address" is invalid for custom parent chain with id ${chain.id}`, + ); + }); + + it('successfully registers a custom parent chain', () => { + const chain = testHelper_createCustomParentChain(); + + // assert before + expect(getCustomParentChains().map((c) => c.id)).not.includes(chain.id); + + // register + registerCustomParentChain(chain); + + // assert after + expect(getCustomParentChains().map((c) => c.id)).includes(chain.id); + }); +}); diff --git a/src/createRollupFetchTransactionHash.ts b/src/createRollupFetchTransactionHash.ts index c1c2e8b9..a61866eb 100644 --- a/src/createRollupFetchTransactionHash.ts +++ b/src/createRollupFetchTransactionHash.ts @@ -1,7 +1,7 @@ import { Address, PublicClient, Transport, Chain } from 'viem'; import { AbiEvent } from 'abitype'; -import { validateParentChain } from './types/ParentChain'; +import { ParentChainId, validateParentChain } from './types/ParentChain'; import { mainnet, arbitrumOne, @@ -40,7 +40,9 @@ const RollupInitializedEventAbi: AbiEvent = { type: 'event', }; -const earliestRollupCreatorDeploymentBlockNumber = { +const earliestRollupCreatorDeploymentBlockNumber: { + [Key in ParentChainId]: bigint; +} = { // mainnet L1 [mainnet.id]: 18736164n, // mainnet L2 @@ -62,12 +64,12 @@ export async function createRollupFetchTransactionHash) { - const { chainId } = validateParentChain(publicClient); + const { chainId: parentChainId, isCustom: parentChainIsCustom } = + validateParentChain(publicClient); - const fromBlock = - chainId in earliestRollupCreatorDeploymentBlockNumber - ? earliestRollupCreatorDeploymentBlockNumber[chainId] - : 'earliest'; + const fromBlock = parentChainIsCustom + ? 'earliest' + : earliestRollupCreatorDeploymentBlockNumber[parentChainId]; // Find the RollupInitialized event from that Rollup contract const rollupInitializedEvents = await publicClient.getLogs({ diff --git a/src/createRollupPrepareDeploymentParamsConfig.ts b/src/createRollupPrepareDeploymentParamsConfig.ts index 08173204..79af5162 100644 --- a/src/createRollupPrepareDeploymentParamsConfig.ts +++ b/src/createRollupPrepareDeploymentParamsConfig.ts @@ -10,7 +10,10 @@ import { prepareChainConfig } from './prepareChainConfig'; import { defaults } from './createRollupPrepareDeploymentParamsConfigDefaults'; import { getDefaultConfirmPeriodBlocks } from './getDefaultConfirmPeriodBlocks'; -import { getDefaultSequencerInboxMaxTimeVariation } from './getDefaultSequencerInboxMaxTimeVariation'; +import { + SequencerInboxMaxTimeVariation, + getDefaultSequencerInboxMaxTimeVariation, +} from './getDefaultSequencerInboxMaxTimeVariation'; export type CreateRollupPrepareDeploymentParamsConfigResult = CreateRollupFunctionInputs[0]['config']; @@ -65,18 +68,50 @@ export type CreateRollupPrepareDeploymentParamsConfigParams = Prettify< */ export function createRollupPrepareDeploymentParamsConfig( client: Client, - { chainConfig, ...params }: CreateRollupPrepareDeploymentParamsConfigParams, + { + chainConfig, + confirmPeriodBlocks, + sequencerInboxMaxTimeVariation, + ...params + }: CreateRollupPrepareDeploymentParamsConfigParams, ): CreateRollupPrepareDeploymentParamsConfigResult { - const { chainId: parentChainId } = validateParentChain(client); + const { chainId: parentChainId, isCustom: parentChainIsCustom } = validateParentChain(client); - const defaultsBasedOnParentChain = { - confirmPeriodBlocks: getDefaultConfirmPeriodBlocks(parentChainId), - sequencerInboxMaxTimeVariation: getDefaultSequencerInboxMaxTimeVariation(parentChainId), + let paramsByParentBlockTime: { + confirmPeriodBlocks: bigint; + sequencerInboxMaxTimeVariation: SequencerInboxMaxTimeVariation; }; + if (parentChainIsCustom) { + if (typeof confirmPeriodBlocks === 'undefined') { + throw new Error( + `"params.confirmPeriodBlocks" must be provided when using a custom parent chain.`, + ); + } + + if (typeof sequencerInboxMaxTimeVariation === 'undefined') { + throw new Error( + `"params.sequencerInboxMaxTimeVariation" must be provided when using a custom parent chain.`, + ); + } + + paramsByParentBlockTime = { + confirmPeriodBlocks, + sequencerInboxMaxTimeVariation, + }; + } else { + const defaultConfirmPeriodBlocks = getDefaultConfirmPeriodBlocks(parentChainId); + const defaultSequencerInboxMTV = getDefaultSequencerInboxMaxTimeVariation(parentChainId); + + paramsByParentBlockTime = { + confirmPeriodBlocks: confirmPeriodBlocks ?? defaultConfirmPeriodBlocks, + sequencerInboxMaxTimeVariation: sequencerInboxMaxTimeVariation ?? defaultSequencerInboxMTV, + }; + } + return { ...defaults, - ...defaultsBasedOnParentChain, + ...paramsByParentBlockTime, ...params, chainConfig: JSON.stringify( chainConfig ?? diff --git a/src/createRollupPrepareDeploymentParamsConfig.unit.test.ts b/src/createRollupPrepareDeploymentParamsConfig.unit.test.ts index f50b1167..10ccca06 100644 --- a/src/createRollupPrepareDeploymentParamsConfig.unit.test.ts +++ b/src/createRollupPrepareDeploymentParamsConfig.unit.test.ts @@ -1,10 +1,18 @@ import { it, expect } from 'vitest'; import { Address, createPublicClient, http } from 'viem'; -import { arbitrumOne, arbitrumSepolia, base, baseSepolia } from './chains'; +import { + arbitrumOne, + arbitrumSepolia, + base, + baseSepolia, + registerCustomParentChain, +} from './chains'; import { prepareChainConfig } from './prepareChainConfig'; import { createRollupPrepareDeploymentParamsConfig } from './createRollupPrepareDeploymentParamsConfig'; +import { testHelper_createCustomParentChain } from './testHelpers'; + const chainId = 69_420n; const vitalik: `0x${string}` = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045'; @@ -117,3 +125,70 @@ it('creates config for a chain on top of base sepolia with defaults', () => { }), ).toMatchSnapshot(); }); + +it('fails to create a config for a chain on top of a custom parent chain if "confirmPeriodBlocks" is not provided', () => { + const chain = testHelper_createCustomParentChain(); + + const publicClient = createPublicClient({ + chain, + transport: http(), + }); + + registerCustomParentChain(chain); + + expect(() => + createRollupPrepareDeploymentParamsConfig(publicClient, { + owner: vitalik, + chainId: BigInt(chain.id), + }), + ).toThrowError('"params.confirmPeriodBlocks" must be provided when using a custom parent chain'); +}); + +it('fails to create a config for a chain on top of a custom parent chain if "sequencerInboxMaxTimeVariation" is not provided', () => { + const chain = testHelper_createCustomParentChain(); + + const publicClient = createPublicClient({ + chain, + transport: http(), + }); + + registerCustomParentChain(chain); + + expect(() => + createRollupPrepareDeploymentParamsConfig(publicClient, { + owner: vitalik, + chainId: BigInt(chain.id), + confirmPeriodBlocks: 1n, + }), + ).toThrowError( + '"params.sequencerInboxMaxTimeVariation" must be provided when using a custom parent chain.', + ); +}); + +it('creates a config for a chain on top of a custom parent chain', () => { + const chain = testHelper_createCustomParentChain({ + // using a specific chain id here as it's a snapshot test + id: 123, + }); + + const publicClient = createPublicClient({ + chain, + transport: http(), + }); + + registerCustomParentChain(chain); + + expect( + createRollupPrepareDeploymentParamsConfig(publicClient, { + owner: vitalik, + chainId: BigInt(chain.id), + confirmPeriodBlocks: 1n, + sequencerInboxMaxTimeVariation: { + delayBlocks: 2n, + futureBlocks: 3n, + delaySeconds: 4n, + futureSeconds: 5n, + }, + }), + ).toMatchSnapshot(); +}); diff --git a/src/createRollupPrepareTransactionRequest.ts b/src/createRollupPrepareTransactionRequest.ts index 7a01f371..e86846d1 100644 --- a/src/createRollupPrepareTransactionRequest.ts +++ b/src/createRollupPrepareTransactionRequest.ts @@ -47,7 +47,8 @@ export async function createRollupPrepareTransactionRequest) { - const { chainId } = validateParentChain(publicClient); + const { chainId: parentChainId, isCustom: parentChainIsCustom } = + validateParentChain(publicClient); if (params.batchPosters.length === 0 || params.batchPosters.includes(zeroAddress)) { throw new Error(`"params.batchPosters" can't be empty or contain the zero address.`); @@ -75,6 +76,18 @@ export async function createRollupPrepareTransactionRequest { + // generate a random chain id + const chainId = generateChainId(); + + // create the chain config + const chainConfig = prepareChainConfig({ + chainId, + arbitrum: { InitialChainOwner: deployer.address, DataAvailabilityCommittee: true }, + }); + + const chain = testHelper_createCustomParentChain({ + id: chainId, + }); + + const publicClient = createPublicClient({ + chain, + transport: http(), + }); + + registerCustomParentChain(chain); + + // prepare the transaction for deploying the core contracts + await expect( + createRollupPrepareTransactionRequest({ + params: { + config: createRollupPrepareDeploymentParamsConfig(publicClient, { + chainId: BigInt(chainId), + owner: deployer.address, + chainConfig, + confirmPeriodBlocks: 1n, + sequencerInboxMaxTimeVariation: { + delayBlocks: 2n, + futureBlocks: 3n, + delaySeconds: 4n, + futureSeconds: 5n, + }, + }), + batchPosters: [deployer.address], + validators: [deployer.address], + }, + account: deployer.address, + publicClient, + }), + ).rejects.toThrowError(`"params.maxDataSize" must be provided when using a custom parent chain.`); +}); + it(`successfully prepares a transaction request with the default rollup creator and a gas limit override`, async () => { // generate a random chain id const chainId = generateChainId(); @@ -356,3 +406,55 @@ it(`successfully prepares a transaction request with a custom rollup creator and expect(txRequest.chainId).toEqual(arbitrumSepolia.id); expect(txRequest.gas).toEqual(1_200n); }); + +it(`successfully prepares a transaction request with a custom parent chain`, async () => { + // generate a random chain id + const chainId = generateChainId(); + + // create the chain config + const chainConfig = prepareChainConfig({ + chainId, + arbitrum: { InitialChainOwner: deployer.address, DataAvailabilityCommittee: true }, + }); + + const chain = testHelper_createCustomParentChain({ + id: chainId, + }); + + const publicClient = createPublicClient({ + chain, + transport: http(), + }); + + registerCustomParentChain(chain); + + const txRequest = await createRollupPrepareTransactionRequest({ + params: { + config: createRollupPrepareDeploymentParamsConfig(publicClient, { + chainId: BigInt(chainId), + owner: deployer.address, + chainConfig, + confirmPeriodBlocks: 1n, + sequencerInboxMaxTimeVariation: { + delayBlocks: 2n, + futureBlocks: 3n, + delaySeconds: 4n, + futureSeconds: 5n, + }, + }), + batchPosters: [deployer.address], + validators: [deployer.address], + maxDataSize: 123_456n, + }, + account: deployer.address, + value: createRollupDefaultRetryablesFees, + publicClient, + gasOverrides: { gasLimit: { base: 1_000n } }, + }); + + expect(txRequest.account).toEqual(deployer.address); + expect(txRequest.from).toEqual(deployer.address); + expect(txRequest.to).toEqual(chain.contracts.rollupCreator.address); + expect(txRequest.chainId).toEqual(chainId); + expect(txRequest.gas).toEqual(1_000n); +}); diff --git a/src/getDefaultConfirmPeriodBlocks.ts b/src/getDefaultConfirmPeriodBlocks.ts index b3bd33eb..6a10b104 100644 --- a/src/getDefaultConfirmPeriodBlocks.ts +++ b/src/getDefaultConfirmPeriodBlocks.ts @@ -7,7 +7,14 @@ import { base, baseSepolia } from './chains'; export function getDefaultConfirmPeriodBlocks( parentChainIdOrClient: ParentChainId | Client, ): bigint { - const { chainId: parentChainId } = validateParentChain(parentChainIdOrClient); + const { chainId: parentChainId, isCustom: parentChainIsCustom } = + validateParentChain(parentChainIdOrClient); + + if (parentChainIsCustom) { + throw new Error( + `[getDefaultConfirmPeriodBlocks] can't provide defaults for custom parent chain with id ${parentChainId}`, + ); + } const isMainnet = parentChainIsMainnet(parentChainId); const confirmPeriodBlocks = isMainnet ? 45_818n : 150n; diff --git a/src/index.ts b/src/index.ts index 5f95a0c0..ae67d37b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -144,6 +144,7 @@ import { getConsensusReleaseByWasmModuleRoot, GetConsensusReleaseByWasmModuleRoot, } from './wasmModuleRoot'; +import { registerCustomParentChain } from './chains'; export * from './actions'; export { @@ -264,4 +265,6 @@ export { isKnownWasmModuleRoot, getConsensusReleaseByWasmModuleRoot, GetConsensusReleaseByWasmModuleRoot, + // + registerCustomParentChain, }; diff --git a/src/prepareNodeConfig.ts b/src/prepareNodeConfig.ts index a5481e83..d43ae50e 100644 --- a/src/prepareNodeConfig.ts +++ b/src/prepareNodeConfig.ts @@ -31,6 +31,7 @@ export type PrepareNodeConfigParams = { batchPosterPrivateKey: string; validatorPrivateKey: string; parentChainId: ParentChainId; + parentChainIsArbitrum?: boolean; parentChainRpcUrl: string; parentChainBeaconRpcUrl?: string; dasServerUrl?: string; @@ -51,6 +52,7 @@ export function prepareNodeConfig({ batchPosterPrivateKey, validatorPrivateKey, parentChainId, + parentChainIsArbitrum: parentChainIsArbitrumParam, parentChainRpcUrl, parentChainBeaconRpcUrl, dasServerUrl, @@ -60,7 +62,14 @@ export function prepareNodeConfig({ throw new Error(`"parentChainBeaconRpcUrl" is required for L2 Orbit chains.`); } - const { chainId: parentChainIdValidated } = validateParentChain(parentChainId); + const { chainId: parentChainIdValidated, isCustom: parentChainIsCustom } = + validateParentChain(parentChainId); + + if (parentChainIsCustom && typeof parentChainIsArbitrumParam === 'undefined') { + throw new Error( + `"params.parentChainIsArbitrum" must be provided when using a custom parent chain.`, + ); + } const config: NodeConfig = { 'chain': { @@ -68,7 +77,9 @@ export function prepareNodeConfig({ { 'chain-id': chainConfig.chainId, 'parent-chain-id': parentChainId, - 'parent-chain-is-arbitrum': parentChainIsArbitrum(parentChainIdValidated), + 'parent-chain-is-arbitrum': parentChainIsCustom + ? parentChainIsArbitrumParam! + : parentChainIsArbitrum(parentChainIdValidated), 'chain-name': chainName, 'chain-config': chainConfig, 'rollup': { diff --git a/src/testHelpers.ts b/src/testHelpers.ts index eb8c3893..597bd5c9 100644 --- a/src/testHelpers.ts +++ b/src/testHelpers.ts @@ -1,5 +1,5 @@ -import { Address, Client, PublicClient, zeroAddress } from 'viem'; -import { privateKeyToAccount, PrivateKeyAccount } from 'viem/accounts'; +import { Address, Chain, PublicClient, zeroAddress } from 'viem'; +import { PrivateKeyAccount, privateKeyToAccount, generatePrivateKey } from 'viem/accounts'; import { config } from 'dotenv'; import { execSync } from 'node:child_process'; @@ -179,3 +179,34 @@ export async function createRollupHelper({ createRollupInformation, }; } + +export function testHelper_createCustomParentChain(params?: { id?: number }) { + const chainId = params?.id ?? generateChainId(); + const rollupCreator = privateKeyToAccount(generatePrivateKey()).address; + const tokenBridgeCreator = privateKeyToAccount(generatePrivateKey()).address; + + return { + id: chainId, + name: `Custom Parent Chain (${chainId})`, + network: `custom-parent-chain-${chainId}`, + nativeCurrency: { + name: 'Ether', + symbol: 'ETH', + decimals: 18, + }, + rpcUrls: { + public: { + // have to put a valid rpc here so using arbitrum sepolia + http: ['https://sepolia-rollup.arbitrum.io/rpc'], + }, + default: { + // have to put a valid rpc here so using arbitrum sepolia + http: ['https://sepolia-rollup.arbitrum.io/rpc'], + }, + }, + contracts: { + rollupCreator: { address: rollupCreator }, + tokenBridgeCreator: { address: tokenBridgeCreator }, + }, + } satisfies Chain; +} diff --git a/src/types/ParentChain.ts b/src/types/ParentChain.ts index 715cb4c1..bc50a1c8 100644 --- a/src/types/ParentChain.ts +++ b/src/types/ParentChain.ts @@ -1,13 +1,18 @@ import { Client, Transport, Chain } from 'viem'; -import { chains, nitroTestnodeL3 } from '../chains'; +import { chains, getCustomParentChains, nitroTestnodeL3 } from '../chains'; // exclude nitro-testnode L3 from the list of parent chains export type ParentChain = Exclude<(typeof chains)[number], { id: typeof nitroTestnodeL3.id }>; export type ParentChainId = ParentChain['id']; -function isValidParentChainId(parentChainId: number | undefined): parentChainId is ParentChainId { - const ids = chains +function isCustomParentChain(chainId: number): boolean { + const ids = getCustomParentChains().map((chain) => chain.id); + return ids.includes(chainId); +} + +function isValidParentChainId(parentChainId: number | undefined): parentChainId is number { + const ids = [...chains, ...getCustomParentChains()] // exclude nitro-testnode L3 from the list of parent chains .filter((chain) => chain.id !== nitroTestnodeL3.id) .map((chain) => chain.id) as Number[]; @@ -16,12 +21,16 @@ function isValidParentChainId(parentChainId: number | undefined): parentChainId export function validateParentChain( chainIdOrClient: number | Client, -): { chainId: ParentChainId } { +): { chainId: number; isCustom: true } | { chainId: ParentChainId; isCustom: false } { const chainId = typeof chainIdOrClient === 'number' ? chainIdOrClient : chainIdOrClient.chain?.id; if (!isValidParentChainId(chainId)) { throw new Error(`Parent chain not supported: ${chainId}`); } - return { chainId }; + if (isCustomParentChain(chainId)) { + return { chainId, isCustom: true }; + } + + return { chainId: chainId as ParentChainId, isCustom: false }; } diff --git a/src/utils/getParentChainFromId.ts b/src/utils/getParentChainFromId.ts index 409d34fd..f575f65f 100644 --- a/src/utils/getParentChainFromId.ts +++ b/src/utils/getParentChainFromId.ts @@ -1,13 +1,13 @@ -import { extractChain } from 'viem'; +import { Chain, extractChain } from 'viem'; -import { chains } from '../chains'; -import { ParentChain, validateParentChain } from '../types/ParentChain'; +import { validateParentChain } from '../types/ParentChain'; +import { chains, getCustomParentChains } from '../chains'; -export function getParentChainFromId(chainId: number): ParentChain { +export function getParentChainFromId(chainId: number): Chain { const { chainId: parentChainId } = validateParentChain(chainId); return extractChain({ - chains, + chains: [...chains, ...getCustomParentChains()], id: parentChainId, - }) as ParentChain; + }); } diff --git a/src/utils/getRollupCreatorAddress.ts b/src/utils/getRollupCreatorAddress.ts index e6fd5b5f..97edac91 100644 --- a/src/utils/getRollupCreatorAddress.ts +++ b/src/utils/getRollupCreatorAddress.ts @@ -1,16 +1,25 @@ -import { Client, Transport, Chain } from 'viem'; +import { Client, Transport, Chain, ChainContract, Address } from 'viem'; import { rollupCreatorAddress } from '../contracts/RollupCreator'; import { validateParentChain } from '../types/ParentChain'; export function getRollupCreatorAddress( client: Client, -) { - const { chainId } = validateParentChain(client); +): Address { + const { chainId: parentChainId, isCustom: parentChainIsCustom } = validateParentChain(client); - if (!rollupCreatorAddress[chainId]) { - throw new Error(`Parent chain not supported: ${chainId}`); + if (parentChainIsCustom) { + const contract = client.chain?.contracts?.rollupCreator as ChainContract | undefined; + const address = contract?.address; + + if (typeof address === 'undefined') { + throw new Error( + `Address for RollupCreator is missing on custom parent chain with id ${parentChainId}`, + ); + } + + return address; } - return rollupCreatorAddress[chainId]; + return rollupCreatorAddress[parentChainId]; } diff --git a/src/utils/getRollupCreatorAddress.unit.test.ts b/src/utils/getRollupCreatorAddress.unit.test.ts new file mode 100644 index 00000000..1874c8b1 --- /dev/null +++ b/src/utils/getRollupCreatorAddress.unit.test.ts @@ -0,0 +1,43 @@ +import { expect, it } from 'vitest'; +import { createPublicClient, http } from 'viem'; +import { sepolia } from 'viem/chains'; + +import { getRollupCreatorAddress } from './getRollupCreatorAddress'; +import { registerCustomParentChain } from '../chains'; + +import { testHelper_createCustomParentChain } from '../testHelpers'; + +it(`successfully returns address for Sepolia`, () => { + const client = createPublicClient({ + chain: sepolia, + transport: http(), + }); + + expect(getRollupCreatorAddress(client)).toEqual('0xfb774eA8A92ae528A596c8D90CBCF1bdBC4Cee79'); +}); + +it(`fails to return address for an unrecognized parent chain`, () => { + const chain = testHelper_createCustomParentChain(); + + const client = createPublicClient({ + chain, + transport: http(), + }); + + expect(() => getRollupCreatorAddress(client)).toThrowError( + `Parent chain not supported: ${chain.id}`, + ); +}); + +it(`successfully returns address for a registered custom parent chain`, () => { + const chain = testHelper_createCustomParentChain(); + + registerCustomParentChain(chain); + + const client = createPublicClient({ + chain, + transport: http(), + }); + + expect(getRollupCreatorAddress(client)).toEqual(chain.contracts.rollupCreator.address); +}); diff --git a/src/utils/getTokenBridgeCreatorAddress.ts b/src/utils/getTokenBridgeCreatorAddress.ts index d66d55ef..aa7c9485 100644 --- a/src/utils/getTokenBridgeCreatorAddress.ts +++ b/src/utils/getTokenBridgeCreatorAddress.ts @@ -1,4 +1,4 @@ -import { Client, Transport, Chain } from 'viem'; +import { Client, Transport, Chain, ChainContract } from 'viem'; import { tokenBridgeCreatorAddress } from '../contracts/TokenBridgeCreator'; import { validateParentChain } from '../types/ParentChain'; @@ -6,11 +6,20 @@ import { validateParentChain } from '../types/ParentChain'; export function getTokenBridgeCreatorAddress( client: Client, ) { - const { chainId } = validateParentChain(client); + const { chainId: parentChainId, isCustom: parentChainIsCustom } = validateParentChain(client); - if (!tokenBridgeCreatorAddress[chainId]) { - throw new Error(`Parent chain not supported: ${chainId}`); + if (parentChainIsCustom) { + const contract = client.chain?.contracts?.tokenBridgeCreator as ChainContract | undefined; + const address = contract?.address; + + if (typeof address === 'undefined') { + throw new Error( + `Address for TokenBridgeCreator is missing on custom parent chain with id ${parentChainId}`, + ); + } + + return address; } - return tokenBridgeCreatorAddress[chainId]; + return tokenBridgeCreatorAddress[parentChainId]; } diff --git a/src/utils/getTokenBridgeCreatorAddress.unit.test.ts b/src/utils/getTokenBridgeCreatorAddress.unit.test.ts new file mode 100644 index 00000000..82eb11ce --- /dev/null +++ b/src/utils/getTokenBridgeCreatorAddress.unit.test.ts @@ -0,0 +1,45 @@ +import { expect, it } from 'vitest'; +import { createPublicClient, http } from 'viem'; +import { sepolia } from 'viem/chains'; + +import { getTokenBridgeCreatorAddress } from './getTokenBridgeCreatorAddress'; +import { registerCustomParentChain } from '../chains'; + +import { testHelper_createCustomParentChain } from '../testHelpers'; + +it(`successfully returns address for Sepolia`, () => { + const client = createPublicClient({ + chain: sepolia, + transport: http(), + }); + + expect(getTokenBridgeCreatorAddress(client)).toEqual( + '0x7edb2dfBeEf9417e0454A80c51EE0C034e45a570', + ); +}); + +it(`fails to return address for an unrecognized parent chain`, () => { + const chain = testHelper_createCustomParentChain(); + + const client = createPublicClient({ + chain, + transport: http(), + }); + + expect(() => getTokenBridgeCreatorAddress(client)).toThrowError( + `Parent chain not supported: ${chain.id}`, + ); +}); + +it(`successfully returns address for a registered custom parent chain`, () => { + const chain = testHelper_createCustomParentChain(); + + registerCustomParentChain(chain); + + const client = createPublicClient({ + chain, + transport: http(), + }); + + expect(getTokenBridgeCreatorAddress(client)).toEqual(chain.contracts.tokenBridgeCreator.address); +}); diff --git a/src/validateParentChain.unit.test.ts b/src/validateParentChain.unit.test.ts new file mode 100644 index 00000000..a579e936 --- /dev/null +++ b/src/validateParentChain.unit.test.ts @@ -0,0 +1,31 @@ +import { it, expect } from 'vitest'; + +import { validateParentChain } from './types/ParentChain'; +import { arbitrumOne, registerCustomParentChain } from './chains'; +import { generateChainId } from './utils'; + +import { testHelper_createCustomParentChain } from './testHelpers'; + +it(`sucessfully validates arbitrum one`, () => { + const result = validateParentChain(arbitrumOne.id); + + expect(result.chainId).toEqual(arbitrumOne.id); + expect(result.isCustom).toEqual(false); +}); + +it(`throws for an unregistered custom parent chain`, () => { + const id = generateChainId(); + + expect(() => validateParentChain(id)).toThrowError(`Parent chain not supported: ${id}`); +}); + +it(`sucessfully validates a registered custom parent chain`, () => { + const chain = testHelper_createCustomParentChain(); + + registerCustomParentChain(chain); + + const result = validateParentChain(chain.id); + + expect(result.chainId).toEqual(chain.id); + expect(result.isCustom).toEqual(true); +});