diff --git a/src/index.ts b/src/index.ts index e05d0fe4..db681b69 100644 --- a/src/index.ts +++ b/src/index.ts @@ -355,3 +355,7 @@ export { // prepareArbitrumNetwork, }; + +export * from './scripting/scriptUtils'; +export * from './scripting/viemTransforms'; +export * from './scripting/schemas'; diff --git a/src/scripting/cli.ts b/src/scripting/cli.ts new file mode 100644 index 00000000..01b0010a --- /dev/null +++ b/src/scripting/cli.ts @@ -0,0 +1,278 @@ +import { runCli, cmd } from './scriptUtils'; + +import { + getValidatorsSchema, + getValidatorsTransform, + getBatchPostersSchema, + getBatchPostersTransform, + getKeysetsSchema, + getKeysetsTransform, + isAnyTrustSchema, + isAnyTrustTransform, + createRollupFetchTransactionHashSchema, + createRollupFetchTransactionHashTransform, + createRollupFetchCoreContractsSchema, + createRollupFetchCoreContractsTransform, + isTokenBridgeDeployedSchema, + isTokenBridgeDeployedTransform, + getBridgeUiConfigSchema, + getBridgeUiConfigTransform, + upgradeExecutorFetchPrivilegedAccountsSchema, + upgradeExecutorFetchPrivilegedAccountsTransform, + setAnyTrustFastConfirmerSchema, + setAnyTrustFastConfirmerTransform, + setValidKeysetSchema, + setValidKeysetTransform, + createRollupSchema, + createRollupTransform, + createTokenBridgeSchema, + createTokenBridgeTransform, + createTokenBridgePrepareTransactionRequestSchema, + createTokenBridgePrepareTransactionRequestTransform, + createTokenBridgePrepareSetWethGatewayTransactionRequestSchema, + createTokenBridgePrepareSetWethGatewayTransactionRequestTransform, + setValidKeysetPrepareTransactionRequestSchema, + setValidKeysetPrepareTransactionRequestTransform, + createRollupPrepareTransactionRequestSchema, + createRollupPrepareTransactionRequestTransform, + createSafePrepareTransactionRequestSchema, + createSafePrepareTransactionRequestTransform, + upgradeExecutorPrepareTransactionRequestSchema, + upgradeExecutorPrepareTransactionRequestTransform, + createRollupEnoughCustomFeeTokenAllowanceSchema, + createRollupEnoughCustomFeeTokenAllowanceTransform, + createRollupPrepareCustomFeeTokenApprovalTransactionRequestSchema, + createRollupPrepareCustomFeeTokenApprovalTransactionRequestTransform, + createTokenBridgeEnoughCustomFeeTokenAllowanceSchema, + createTokenBridgeEnoughCustomFeeTokenAllowanceTransform, + createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequestSchema, + createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequestTransform, + feeRouterDeployRewardDistributorSchema, + feeRouterDeployRewardDistributorTransform, + feeRouterDeployChildToParentRewardRouterSchema, + feeRouterDeployChildToParentRewardRouterTransform, + prepareChainConfigParamsSchema, + prepareChainConfigTransform, + prepareNodeConfigSchema, + prepareNodeConfigTransform, + prepareKeysetSchema, + prepareKeysetTransform, + prepareKeysetHashSchema, + prepareKeysetHashTransform, + prepareDeploymentParamsConfigV21Schema, + prepareDeploymentParamsConfigV21Transform, + prepareDeploymentParamsConfigV32Schema, + prepareDeploymentParamsConfigV32Transform, + getDefaultsSchema, + getDefaultsTransform, + createRollupGetRetryablesFeesSchema, + createRollupGetRetryablesFeesTransform, + fetchAllowanceSchema, + fetchAllowanceTransform, + fetchDecimalsSchema, + fetchDecimalsTransform, +} from './schemas'; + +import { getValidators } from '../getValidators'; +import { getBatchPosters } from '../getBatchPosters'; +import { getKeysets } from '../getKeysets'; +import { isAnyTrust } from '../isAnyTrust'; +import { createRollupFetchTransactionHash } from '../createRollupFetchTransactionHash'; +import { createRollupFetchCoreContracts } from '../createRollupFetchCoreContracts'; +import { isTokenBridgeDeployed } from '../isTokenBridgeDeployed'; +import { getBridgeUiConfig } from '../getBridgeUiConfig'; +import { upgradeExecutorFetchPrivilegedAccounts } from '../upgradeExecutorFetchPrivilegedAccounts'; +import { setAnyTrustFastConfirmerPrepareTransactionRequest } from '../setAnyTrustFastConfirmerPrepareTransactionRequest'; +import { setValidKeyset } from '../setValidKeyset'; +import { createRollup } from '../createRollup'; +import { createTokenBridge } from '../createTokenBridge'; +import { createTokenBridgePrepareTransactionRequest } from '../createTokenBridgePrepareTransactionRequest'; +import { createTokenBridgePrepareSetWethGatewayTransactionRequest } from '../createTokenBridgePrepareSetWethGatewayTransactionRequest'; +import { setValidKeysetPrepareTransactionRequest } from '../setValidKeysetPrepareTransactionRequest'; +import { createRollupPrepareTransactionRequest } from '../createRollupPrepareTransactionRequest'; +import { createSafePrepareTransactionRequest } from '../createSafePrepareTransactionRequest'; +import { upgradeExecutorPrepareAddExecutorTransactionRequest } from '../upgradeExecutorPrepareAddExecutorTransactionRequest'; +import { upgradeExecutorPrepareRemoveExecutorTransactionRequest } from '../upgradeExecutorPrepareRemoveExecutorTransactionRequest'; +import { createRollupEnoughCustomFeeTokenAllowance } from '../createRollupEnoughCustomFeeTokenAllowance'; +import { createRollupPrepareCustomFeeTokenApprovalTransactionRequest } from '../createRollupPrepareCustomFeeTokenApprovalTransactionRequest'; +import { createTokenBridgeEnoughCustomFeeTokenAllowance } from '../createTokenBridgeEnoughCustomFeeTokenAllowance'; +import { createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequest } from '../createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequest'; +import { feeRouterDeployRewardDistributor } from '../feeRouterDeployRewardDistributor'; +import { feeRouterDeployChildToParentRewardRouter } from '../feeRouterDeployChildToParentRewardRouter'; +import { prepareChainConfig } from '../prepareChainConfig'; +import { prepareNodeConfig } from '../prepareNodeConfig'; +import { prepareKeyset } from '../prepareKeyset'; +import { prepareKeysetHash } from '../prepareKeysetHash'; +import { createRollupPrepareDeploymentParamsConfig } from '../createRollupPrepareDeploymentParamsConfig'; +import { createRollupGetRetryablesFees } from '../createRollupGetRetryablesFees'; +import { getDefaultConfirmPeriodBlocks } from '../getDefaultConfirmPeriodBlocks'; +import { getDefaultChallengeGracePeriodBlocks } from '../getDefaultChallengeGracePeriodBlocks'; +import { getDefaultMinimumAssertionPeriod } from '../getDefaultMinimumAssertionPeriod'; +import { getDefaultValidatorAfkBlocks } from '../getDefaultValidatorAfkBlocks'; +import { getDefaultSequencerInboxMaxTimeVariation } from '../getDefaultSequencerInboxMaxTimeVariation'; +import { fetchAllowance, fetchDecimals } from '../utils/erc20'; + +runCli('chain-sdk', { + getValidators: cmd(getValidatorsSchema.transform(getValidatorsTransform), getValidators), + getBatchPosters: cmd(getBatchPostersSchema.transform(getBatchPostersTransform), getBatchPosters), + getKeysets: cmd(getKeysetsSchema.transform(getKeysetsTransform), getKeysets), + isAnyTrust: cmd(isAnyTrustSchema.transform(isAnyTrustTransform), isAnyTrust), + createRollupFetchTransactionHash: cmd( + createRollupFetchTransactionHashSchema.transform(createRollupFetchTransactionHashTransform), + createRollupFetchTransactionHash, + ), + createRollupFetchCoreContracts: cmd( + createRollupFetchCoreContractsSchema.transform(createRollupFetchCoreContractsTransform), + createRollupFetchCoreContracts, + ), + isTokenBridgeDeployed: cmd( + isTokenBridgeDeployedSchema.transform(isTokenBridgeDeployedTransform), + isTokenBridgeDeployed, + ), + getBridgeUiConfig: cmd( + getBridgeUiConfigSchema.transform(getBridgeUiConfigTransform), + getBridgeUiConfig, + ), + upgradeExecutorFetchPrivilegedAccounts: cmd( + upgradeExecutorFetchPrivilegedAccountsSchema.transform( + upgradeExecutorFetchPrivilegedAccountsTransform, + ), + upgradeExecutorFetchPrivilegedAccounts, + ), + fetchAllowance: cmd(fetchAllowanceSchema.transform(fetchAllowanceTransform), fetchAllowance), + fetchDecimals: cmd(fetchDecimalsSchema.transform(fetchDecimalsTransform), fetchDecimals), + + setAnyTrustFastConfirmer: cmd( + setAnyTrustFastConfirmerSchema.transform(setAnyTrustFastConfirmerTransform), + setAnyTrustFastConfirmerPrepareTransactionRequest, + ), + setValidKeyset: cmd(setValidKeysetSchema.transform(setValidKeysetTransform), setValidKeyset), + createRollup: cmd(createRollupSchema.transform(createRollupTransform), createRollup), + createTokenBridge: cmd( + createTokenBridgeSchema.transform(createTokenBridgeTransform), + createTokenBridge, + ), + createTokenBridgePrepareTransactionRequest: cmd( + createTokenBridgePrepareTransactionRequestSchema.transform( + createTokenBridgePrepareTransactionRequestTransform, + ), + createTokenBridgePrepareTransactionRequest, + ), + createTokenBridgePrepareSetWethGatewayTransactionRequest: cmd( + createTokenBridgePrepareSetWethGatewayTransactionRequestSchema.transform( + createTokenBridgePrepareSetWethGatewayTransactionRequestTransform, + ), + createTokenBridgePrepareSetWethGatewayTransactionRequest, + ), + setValidKeysetPrepareTransactionRequest: cmd( + setValidKeysetPrepareTransactionRequestSchema.transform( + setValidKeysetPrepareTransactionRequestTransform, + ), + setValidKeysetPrepareTransactionRequest, + ), + createRollupPrepareTransactionRequest: cmd( + createRollupPrepareTransactionRequestSchema.transform( + createRollupPrepareTransactionRequestTransform, + ), + createRollupPrepareTransactionRequest, + ), + createSafePrepareTransactionRequest: cmd( + createSafePrepareTransactionRequestSchema.transform( + createSafePrepareTransactionRequestTransform, + ), + createSafePrepareTransactionRequest, + ), + upgradeExecutorPrepareAddExecutor: cmd( + upgradeExecutorPrepareTransactionRequestSchema.transform( + upgradeExecutorPrepareTransactionRequestTransform, + ), + upgradeExecutorPrepareAddExecutorTransactionRequest, + ), + upgradeExecutorPrepareRemoveExecutor: cmd( + upgradeExecutorPrepareTransactionRequestSchema.transform( + upgradeExecutorPrepareTransactionRequestTransform, + ), + upgradeExecutorPrepareRemoveExecutorTransactionRequest, + ), + createRollupEnoughCustomFeeTokenAllowance: cmd( + createRollupEnoughCustomFeeTokenAllowanceSchema.transform( + createRollupEnoughCustomFeeTokenAllowanceTransform, + ), + createRollupEnoughCustomFeeTokenAllowance, + ), + createRollupPrepareCustomFeeTokenApprovalTransactionRequest: cmd( + createRollupPrepareCustomFeeTokenApprovalTransactionRequestSchema.transform( + createRollupPrepareCustomFeeTokenApprovalTransactionRequestTransform, + ), + createRollupPrepareCustomFeeTokenApprovalTransactionRequest, + ), + createTokenBridgeEnoughCustomFeeTokenAllowance: cmd( + createTokenBridgeEnoughCustomFeeTokenAllowanceSchema.transform( + createTokenBridgeEnoughCustomFeeTokenAllowanceTransform, + ), + createTokenBridgeEnoughCustomFeeTokenAllowance, + ), + createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequest: cmd( + createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequestSchema.transform( + createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequestTransform, + ), + createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequest, + ), + feeRouterDeployRewardDistributor: cmd( + feeRouterDeployRewardDistributorSchema.transform(feeRouterDeployRewardDistributorTransform), + feeRouterDeployRewardDistributor, + ), + feeRouterDeployChildToParentRewardRouter: cmd( + feeRouterDeployChildToParentRewardRouterSchema.transform( + feeRouterDeployChildToParentRewardRouterTransform, + ), + feeRouterDeployChildToParentRewardRouter, + ), + + prepareChainConfig: cmd( + prepareChainConfigParamsSchema.transform(prepareChainConfigTransform), + prepareChainConfig, + ), + prepareNodeConfig: cmd( + prepareNodeConfigSchema.transform(prepareNodeConfigTransform), + prepareNodeConfig, + ), + prepareKeyset: cmd(prepareKeysetSchema.transform(prepareKeysetTransform), prepareKeyset), + prepareKeysetHash: cmd( + prepareKeysetHashSchema.transform(prepareKeysetHashTransform), + prepareKeysetHash, + ), + prepareDeploymentParamsConfigV21: cmd( + prepareDeploymentParamsConfigV21Schema.transform(prepareDeploymentParamsConfigV21Transform), + createRollupPrepareDeploymentParamsConfig, + ), + prepareDeploymentParamsConfigV32: cmd( + prepareDeploymentParamsConfigV32Schema.transform(prepareDeploymentParamsConfigV32Transform), + createRollupPrepareDeploymentParamsConfig, + ), + createRollupGetRetryablesFees: cmd( + createRollupGetRetryablesFeesSchema.transform(createRollupGetRetryablesFeesTransform), + createRollupGetRetryablesFees, + ), + + getDefaultConfirmPeriodBlocks: cmd( + getDefaultsSchema.transform(getDefaultsTransform), + getDefaultConfirmPeriodBlocks, + ), + getDefaultChallengeGracePeriodBlocks: cmd( + getDefaultsSchema.transform(getDefaultsTransform), + getDefaultChallengeGracePeriodBlocks, + ), + getDefaultMinimumAssertionPeriod: cmd( + getDefaultsSchema.transform(getDefaultsTransform), + getDefaultMinimumAssertionPeriod, + ), + getDefaultValidatorAfkBlocks: cmd( + getDefaultsSchema.transform(getDefaultsTransform), + getDefaultValidatorAfkBlocks, + ), + getDefaultSequencerInboxMaxTimeVariation: cmd( + getDefaultsSchema.transform(getDefaultsTransform), + getDefaultSequencerInboxMaxTimeVariation, + ), +}); diff --git a/src/scripting/examples/createRollup.ts b/src/scripting/examples/createRollup.ts new file mode 100644 index 00000000..22047ba9 --- /dev/null +++ b/src/scripting/examples/createRollup.ts @@ -0,0 +1,39 @@ +import { runScript } from '../scriptUtils'; +import { createRollupDefaultSchema } from '../schemas/createRollup'; +import { paramsV3Dot2Schema } from '../schemas/createRollupPrepareDeploymentParamsConfig'; +import { prepareChainConfigParamsSchema } from '../schemas/prepareChainConfig'; +import { toPublicClient, toAccount } from '../viemTransforms'; +import { createRollupPrepareDeploymentParamsConfig } from '../../createRollupPrepareDeploymentParamsConfig'; +import { prepareChainConfig } from '../../prepareChainConfig'; +import { createRollup } from '../../createRollup'; + +const schema = createRollupDefaultSchema + .extend({ + params: createRollupDefaultSchema.shape.params.extend({ + config: paramsV3Dot2Schema.extend({ + chainConfig: prepareChainConfigParamsSchema.optional(), + }), + }), + }) + .transform((input) => { + const parentChainPublicClient = toPublicClient(input.parentChainRpcUrl); + const { + config: { chainConfig: chainConfigParams, ...restConfig }, + ...params + } = input.params; + const chainConfig = chainConfigParams ? prepareChainConfig(chainConfigParams) : undefined; + const config = createRollupPrepareDeploymentParamsConfig(parentChainPublicClient, { + ...restConfig, + chainConfig, + }); + return { + params: { config, ...params }, + account: toAccount(input.privateKey), + parentChainPublicClient, + }; + }); + +runScript(schema, async (input) => { + const result = await createRollup(input); + return result.coreContracts; +}); diff --git a/src/scripting/examples/deployNewChain.ts b/src/scripting/examples/deployNewChain.ts new file mode 100644 index 00000000..12f36350 --- /dev/null +++ b/src/scripting/examples/deployNewChain.ts @@ -0,0 +1,77 @@ +import { runScript } from '../scriptUtils'; +import { createRollupDefaultSchema } from '../schemas/createRollup'; +import { hexSchema, bigintSchema, addressSchema } from '../schemas/common'; +import { paramsV3Dot2Schema } from '../schemas/createRollupPrepareDeploymentParamsConfig'; +import { prepareChainConfigParamsSchema } from '../schemas/prepareChainConfig'; +import { toPublicClient, toAccount, toWalletClient } from '../viemTransforms'; +import { createRollupPrepareDeploymentParamsConfig } from '../../createRollupPrepareDeploymentParamsConfig'; +import { prepareChainConfig } from '../../prepareChainConfig'; +import { createRollup } from '../../createRollup'; +import { zeroAddress } from 'viem'; +import { setValidKeyset } from '../../setValidKeyset'; +import { generateChainId } from '../../utils/generateChainId'; + +const schema = createRollupDefaultSchema + .extend({ + params: createRollupDefaultSchema.shape.params.extend({ + config: paramsV3Dot2Schema.extend({ + chainId: bigintSchema.prefault(() => String(generateChainId())), + chainConfig: prepareChainConfigParamsSchema.optional(), + }), + nativeToken: addressSchema.default(zeroAddress), + keyset: hexSchema.optional(), + }), + }) + .superRefine((data, ctx) => { + const isAnytrust = data.params.config.chainConfig?.arbitrum?.DataAvailabilityCommittee === true; + if (data.params.keyset && !isAnytrust) { + ctx.addIssue({ + code: 'custom', + path: ['params', 'keyset'], + message: + 'keyset provided but chain is not AnyTrust (DataAvailabilityCommittee is not true)', + }); + } + }) + .transform((input) => { + const parentChainPublicClient = toPublicClient(input.parentChainRpcUrl); + const { + config: { chainConfig: chainConfigParams, ...restConfig }, + keyset, + ...params + } = input.params; + const chainConfig = chainConfigParams ? prepareChainConfig(chainConfigParams) : undefined; + const isAnytrust = chainConfigParams?.arbitrum?.DataAvailabilityCommittee === true; + const config = createRollupPrepareDeploymentParamsConfig(parentChainPublicClient, { + ...restConfig, + chainConfig, + }); + + const DEFAULT_KEYSET: `0x${string}` = + '0x00000000000000010000000000000001012160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'; + + return { + params: { config, ...params }, + account: toAccount(input.privateKey), + parentChainPublicClient, + walletClient: toWalletClient(input.parentChainRpcUrl, input.privateKey), + keyset: isAnytrust ? keyset ?? DEFAULT_KEYSET : undefined, + }; + }); + +runScript(schema, async (input) => { + const { keyset, walletClient, ...createRollupArgs } = input; + const result = await createRollup(createRollupArgs); + const coreContracts = result.coreContracts; + + if (keyset) { + await setValidKeyset({ + keyset, + publicClient: createRollupArgs.parentChainPublicClient, + walletClient, + coreContracts, + }); + } + + return coreContracts; +}); diff --git a/src/scripting/examples/getValidators.ts b/src/scripting/examples/getValidators.ts new file mode 100644 index 00000000..df3f28b6 --- /dev/null +++ b/src/scripting/examples/getValidators.ts @@ -0,0 +1,7 @@ +import { runScript } from '../scriptUtils'; +import { getValidatorsSchema, getValidatorsTransform } from '../schemas'; +import { getValidators } from '../../getValidators'; + +const schema = getValidatorsSchema.transform(getValidatorsTransform); + +runScript(schema, async (args) => getValidators(...args)); diff --git a/src/scripting/examples/setAnyTrustFastConfirmer.ts b/src/scripting/examples/setAnyTrustFastConfirmer.ts new file mode 100644 index 00000000..e6da57d5 --- /dev/null +++ b/src/scripting/examples/setAnyTrustFastConfirmer.ts @@ -0,0 +1,7 @@ +import { runScript } from '../scriptUtils'; +import { setAnyTrustFastConfirmerSchema, setAnyTrustFastConfirmerTransform } from '../schemas'; +import { setAnyTrustFastConfirmerPrepareTransactionRequest } from '../../setAnyTrustFastConfirmerPrepareTransactionRequest'; + +const schema = setAnyTrustFastConfirmerSchema.transform(setAnyTrustFastConfirmerTransform); + +runScript(schema, async (args) => setAnyTrustFastConfirmerPrepareTransactionRequest(...args)); diff --git a/src/scripting/examples/transferOwnership.ts b/src/scripting/examples/transferOwnership.ts new file mode 100644 index 00000000..9bc40736 --- /dev/null +++ b/src/scripting/examples/transferOwnership.ts @@ -0,0 +1,280 @@ +import { z } from 'zod'; +import { + concatHex, + createWalletClient, + custom, + defineChain, + encodeFunctionData, + parseAbi, + toHex, + zeroAddress, +} from 'viem'; +import { runScript } from '../scriptUtils'; +import { addressSchema, bigintSchema, privateKeySchema } from '../schemas/common'; +import { toPublicClient, toAccount, toWalletClient, findChain } from '../viemTransforms'; +import { upgradeExecutorPrepareAddExecutorTransactionRequest } from '../../upgradeExecutorPrepareAddExecutorTransactionRequest'; +import { upgradeExecutorPrepareRemoveExecutorTransactionRequest } from '../../upgradeExecutorPrepareRemoveExecutorTransactionRequest'; +import { + UPGRADE_EXECUTOR_ROLE_EXECUTOR, + upgradeExecutorEncodeFunctionData, +} from '../../upgradeExecutorEncodeFunctionData'; +import { upgradeExecutorABI } from '../../contracts/UpgradeExecutor'; +import { arbOwnerABI, arbOwnerAddress } from '../../contracts/ArbOwner'; + +const createRetryableTicketEthABI = parseAbi([ + 'function createRetryableTicket(address to, uint256 l2CallValue, uint256 maxSubmissionCost, address excessFeeRefundAddress, address callValueRefundAddress, uint256 gasLimit, uint256 maxFeePerGas, bytes data) payable returns (uint256)', +]); +const createRetryableTicketErc20ABI = parseAbi([ + 'function createRetryableTicket(address to, uint256 l2CallValue, uint256 maxSubmissionCost, address excessFeeRefundAddress, address callValueRefundAddress, uint256 gasLimit, uint256 maxFeePerGas, uint256 tokenTotalFeeAmount, bytes data) returns (uint256)', +]); +const sendL2MessageABI = parseAbi(['function sendL2Message(bytes messageData) returns (uint256)']); +const inboxSubmissionFeeABI = parseAbi([ + 'function calculateRetryableSubmissionFee(uint256 dataLength, uint256 baseFee) view returns (uint256)', +]); + +const DEFAULT_GAS_LIMIT = 100_000n; +const BUFFER_PERCENT = 50n; // 50% buffer + +function applyBuffer(value: bigint): bigint { + return value + (value * BUFFER_PERCENT) / 100n; +} + +type Input = z.output; + +async function calculateMaxSubmissionCost( + { publicClient, inboxAddress }: Input, + data: `0x${string}`, +) { + const gasPrice = await publicClient.getGasPrice(); + return publicClient.readContract({ + address: inboxAddress, + abi: inboxSubmissionFeeABI, + functionName: 'calculateRetryableSubmissionFee', + args: [BigInt(Math.ceil((data.length - 2) / 2)), applyBuffer(gasPrice)], + }); +} + +async function sendRetryableViaUpgradeExecutor( + input: Input, + to: `0x${string}`, + data: `0x${string}`, +) { + const { + publicClient, + walletClient, + account, + upgradeExecutorAddress, + inboxAddress, + refundAddress, + maxGasPrice, + nativeToken, + } = input; + const isErc20 = nativeToken !== zeroAddress; + const createRetryableTicketAbi = isErc20 + ? createRetryableTicketErc20ABI + : createRetryableTicketEthABI; + + const maxSubmissionCost = applyBuffer(await calculateMaxSubmissionCost(input, data)); + const gasLimit = applyBuffer(DEFAULT_GAS_LIMIT); + const deposit = maxSubmissionCost + gasLimit * maxGasPrice; + + const retryableArgs: unknown[] = [ + to, + 0n, + maxSubmissionCost, + refundAddress, + refundAddress, + gasLimit, + maxGasPrice, + ]; + if (isErc20) retryableArgs.push(deposit); + retryableArgs.push(data); + + const createRetryableTicketData = encodeFunctionData({ + abi: createRetryableTicketAbi, + functionName: 'createRetryableTicket', + args: retryableArgs as never, + }); + + const { request } = await publicClient.simulateContract({ + account: account.address, + address: upgradeExecutorAddress, + abi: upgradeExecutorABI, + functionName: 'executeCall', + args: [inboxAddress, createRetryableTicketData], + // executeCall ABI is missing payable modifier but the contract accepts value + ...(!isErc20 && { value: deposit }), + } as Parameters[0]); + + const txHash = await walletClient.writeContract(request); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + return txHash; +} + +async function sendL2Message( + { publicClient, walletClient, account, inboxAddress, childChainId, maxGasPrice }: Input, + to: `0x${string}`, + data: `0x${string}`, + nonce: number, +) { + const childChain = defineChain({ + id: childChainId, + name: 'Child Chain', + network: 'child-chain', + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, + rpcUrls: { + default: { http: ['http://localhost'] }, + public: { http: ['http://localhost'] }, + }, + }); + + const mockedWalletClient = createWalletClient({ + account, + chain: childChain, + transport: custom({ + async request({ method }) { + if (method === 'eth_chainId') return toHex(childChainId); + throw new Error(`Unexpected RPC call: ${method}`); + }, + }), + }); + + const signedTx = await mockedWalletClient.signTransaction({ + to, + data, + nonce, + gas: DEFAULT_GAS_LIMIT, + maxFeePerGas: maxGasPrice, + maxPriorityFeePerGas: 0n, + }); + + // InboxMessageKind.L2MessageType_signedTx = 4 + const message = concatHex([toHex(4, { size: 1 }), signedTx]); + + const { request } = await publicClient.simulateContract({ + account: account.address, + address: inboxAddress, + abi: sendL2MessageABI, + functionName: 'sendL2Message', + args: [message], + }); + + const txHash = await walletClient.writeContract(request); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + return txHash; +} + +const schema = z + .object({ + rpcUrl: z.url(), + chainId: z.number(), + privateKey: privateKeySchema, + upgradeExecutorAddress: addressSchema, + newOwnerAddress: addressSchema, + inboxAddress: addressSchema, + childUpgradeExecutorAddress: addressSchema, + childChainId: z.number(), + nativeToken: addressSchema.default(zeroAddress), + maxGasPrice: bigintSchema, + refundAddress: addressSchema.optional(), + }) + .transform(({ rpcUrl, chainId, privateKey, refundAddress, newOwnerAddress, ...rest }) => ({ + ...rest, + publicClient: toPublicClient(rpcUrl, findChain(chainId)), + account: toAccount(privateKey), + walletClient: toWalletClient(rpcUrl, privateKey, findChain(chainId)), + refundAddress: refundAddress ?? newOwnerAddress, + newOwnerAddress, + })); + +runScript(schema, async (input) => { + const { + publicClient, + account, + upgradeExecutorAddress, + newOwnerAddress, + childUpgradeExecutorAddress, + } = input; + + // Step 1: Add new owner as executor on parent-chain UpgradeExecutor + const addParentExecutorTxRequest = await upgradeExecutorPrepareAddExecutorTransactionRequest({ + account: newOwnerAddress, + upgradeExecutorAddress, + executorAccountAddress: account.address, + publicClient, + }); + const step1TxHash = await publicClient.sendRawTransaction({ + serializedTransaction: await account.signTransaction(addParentExecutorTxRequest), + }); + await publicClient.waitForTransactionReceipt({ hash: step1TxHash }); + + // Step 2: Grant executor role on child-chain UpgradeExecutor (via retryable) + const grantRoleCalldata = encodeFunctionData({ + abi: upgradeExecutorABI, + functionName: 'grantRole', + args: [UPGRADE_EXECUTOR_ROLE_EXECUTOR, newOwnerAddress], + }); + const addChildExecutorData = upgradeExecutorEncodeFunctionData({ + functionName: 'executeCall', + args: [childUpgradeExecutorAddress, grantRoleCalldata], + }); + const step2TxHash = await sendRetryableViaUpgradeExecutor( + input, + childUpgradeExecutorAddress, + addChildExecutorData, + ); + + // Step 3: Add child-chain UpgradeExecutor as chain owner (via sendL2Message) + const addChainOwnerCalldata = encodeFunctionData({ + abi: arbOwnerABI, + functionName: 'addChainOwner', + args: [childUpgradeExecutorAddress], + }); + const step3TxHash = await sendL2Message(input, arbOwnerAddress, addChainOwnerCalldata, 0); + + // Step 4: Remove deployer as chain owner (via sendL2Message) + const removeChainOwnerCalldata = encodeFunctionData({ + abi: arbOwnerABI, + functionName: 'removeChainOwner', + args: [account.address], + }); + const step4TxHash = await sendL2Message(input, arbOwnerAddress, removeChainOwnerCalldata, 1); + + // Step 5: Revoke deployer's executor role on child-chain UpgradeExecutor (via retryable) + const revokeRoleCalldata = encodeFunctionData({ + abi: upgradeExecutorABI, + functionName: 'revokeRole', + args: [UPGRADE_EXECUTOR_ROLE_EXECUTOR, account.address], + }); + const removeChildExecutorData = upgradeExecutorEncodeFunctionData({ + functionName: 'executeCall', + args: [childUpgradeExecutorAddress, revokeRoleCalldata], + }); + const step5TxHash = await sendRetryableViaUpgradeExecutor( + input, + childUpgradeExecutorAddress, + removeChildExecutorData, + ); + + // Step 6: Remove deployer as executor on parent-chain UpgradeExecutor + const removeParentExecutorTxRequest = + await upgradeExecutorPrepareRemoveExecutorTransactionRequest({ + account: account.address, + upgradeExecutorAddress, + executorAccountAddress: account.address, + publicClient, + }); + const step6TxHash = await publicClient.sendRawTransaction({ + serializedTransaction: await account.signTransaction(removeParentExecutorTxRequest), + }); + await publicClient.waitForTransactionReceipt({ hash: step6TxHash }); + + return { + step1TxHash, + step2TxHash, + step3TxHash, + step4TxHash, + step5TxHash, + step6TxHash, + }; +}); diff --git a/src/scripting/index.ts b/src/scripting/index.ts new file mode 100644 index 00000000..33f44876 --- /dev/null +++ b/src/scripting/index.ts @@ -0,0 +1,3 @@ +export { runScript } from './scriptUtils'; +export { toPublicClient, toAccount, toWalletClient } from './viemTransforms'; +export * from './schemas'; diff --git a/src/scripting/scriptUtils.ts b/src/scripting/scriptUtils.ts new file mode 100644 index 00000000..d936cc21 --- /dev/null +++ b/src/scripting/scriptUtils.ts @@ -0,0 +1,90 @@ +import { z, ZodError, ZodType } from 'zod'; + +function formatError(error: unknown): string { + if (error instanceof Error) { + const stack = error.stack ?? error.message; + if (error instanceof ZodError) { + return `Input validation failed:\n${error.message}`; + } + return stack; + } + return `Non-Error value thrown: ${JSON.stringify(error)}`; +} + +const replacer = (_k: string, v: unknown) => (typeof v === 'bigint' ? v.toString() : v); + +function handleError(error: unknown): never { + process.stderr.write(formatError(error) + '\n'); + process.exit(1); +} + +export function runScript( + schema: TSchema, + run: (input: z.output) => unknown, +): void { + const jsonString = process.argv[2]; + + if (!jsonString) { + process.stderr.write('JSON string expected as the first argument.\n'); + process.exit(1); + } + + let rawInput: unknown; + try { + rawInput = JSON.parse(jsonString); + } catch (error) { + handleError(error); + } + + (async () => { + const parsed = schema.parse(rawInput); + const result = await run(parsed); + const output = typeof result === 'string' ? result : JSON.stringify(result, replacer, 2); + process.stdout.write(output + '\n'); + })().catch(handleError); +} + +export function cmd>( + input: TSchema, + run: (...args: z.output) => unknown, +) { + return { + input, + run: (parsed: unknown) => run(...(parsed as z.output)), + }; +} + +export function runCli( + cliName: string, + commands: Record unknown }>, +): void { + const name = process.argv[2]; + const command = name ? commands[name] : undefined; + + if (!command) { + const available = Object.keys(commands).join(', '); + process.stderr.write(`Usage: ${cliName} ''\nCommands: ${available}\n`); + process.exit(1); + } + + const jsonString = process.argv[3]; + + if (!jsonString) { + process.stderr.write(`Usage: ${cliName} ${name} ''\n`); + process.exit(1); + } + + let rawInput: unknown; + try { + rawInput = JSON.parse(jsonString); + } catch (error) { + handleError(error); + } + + (async () => { + const parsed = command.input.parse(rawInput); + const result = await command.run(parsed); + const output = typeof result === 'string' ? result : JSON.stringify(result, replacer, 2); + process.stdout.write(output + '\n'); + })().catch(handleError); +} diff --git a/src/scripting/scriptUtils.unit.test.ts b/src/scripting/scriptUtils.unit.test.ts new file mode 100644 index 00000000..c12ee063 --- /dev/null +++ b/src/scripting/scriptUtils.unit.test.ts @@ -0,0 +1,183 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { z } from 'zod'; +import { runScript, runCli, cmd } from './scriptUtils'; + +let stdoutData: string; +let stderrData: string; + +beforeEach(() => { + stdoutData = ''; + stderrData = ''; + + vi.spyOn(process.stdout, 'write').mockImplementation((data: string | Uint8Array) => { + stdoutData += data.toString(); + return true; + }); + + vi.spyOn(process.stderr, 'write').mockImplementation((data: string | Uint8Array) => { + stderrData += data.toString(); + return true; + }); + + vi.spyOn(process, 'exit').mockImplementation((() => { + // intentionally empty -- just prevents the real exit + }) as never); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +function getExitCode(): number | undefined { + const calls = (process.exit as unknown as { mock?: { calls: number[][] } }).mock?.calls; + if (calls && calls.length > 0) { + return calls[calls.length - 1][0]; + } + return undefined; +} + +it('parses JSON from argv and writes result to stdout', async () => { + process.argv[2] = '{"x": 5}'; + runScript(z.object({ x: z.number() }), async (input) => ({ doubled: input.x * 2 })); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(JSON.parse(stdoutData)).toEqual({ doubled: 10 }); + expect(getExitCode()).toBeUndefined(); +}); + +it('exits with code 1 when no JSON argument provided', () => { + process.argv[2] = undefined as unknown as string; + runScript(z.object({}), async () => ({})); + + expect(getExitCode()).toBe(1); + expect(stderrData).toContain('Usage'); +}); + +it('exits with code 1 for invalid JSON', () => { + process.argv[2] = 'not json'; + runScript(z.object({}), async () => ({})); + + expect(getExitCode()).toBe(1); + expect(stderrData).toContain('Unexpected token'); +}); + +it('prints validation errors to stderr on schema failure', async () => { + process.argv[2] = '{"name": 123}'; + runScript(z.object({ name: z.string() }), async (input) => input); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(getExitCode()).toBe(1); + expect(stderrData).toContain('validation failed'); +}); + +it('serializes bigint values as strings', async () => { + process.argv[2] = '{}'; + runScript(z.object({}), async () => ({ value: BigInt('123456789012345678901234567890') })); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + const parsed = JSON.parse(stdoutData); + expect(parsed.value).toBe('123456789012345678901234567890'); +}); + +it('prints run errors to stderr', async () => { + process.argv[2] = '{}'; + runScript(z.object({}), async () => { + throw new Error('something broke'); + }); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(getExitCode()).toBe(1); + expect(stderrData).toContain('something broke'); + expect(stderrData).toContain('at'); +}); + +it('outputs raw string without JSON quotes', async () => { + process.argv[2] = '{}'; + runScript(z.object({}), async () => 'hello world'); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(stdoutData).toBe('hello world\n'); + expect(getExitCode()).toBeUndefined(); +}); + +describe('runCli', () => { + const testSchema = z.object({ value: z.string() }); + const testCommands = { + echo: cmd( + testSchema.transform((input) => [input]), + (input: { value: string }) => input, + ), + }; + + it('prints usage and exits 1 for unknown command', () => { + process.argv[2] = 'nope'; + process.argv[3] = '{}'; + runCli('test-cli', testCommands); + + expect(getExitCode()).toBe(1); + expect(stderrData).toContain('Usage'); + expect(stderrData).toContain('echo'); + }); + + it('prints usage and exits 1 when JSON arg is missing', () => { + process.argv[2] = 'echo'; + process.argv[3] = undefined as unknown as string; + runCli('test-cli', testCommands); + + expect(getExitCode()).toBe(1); + expect(stderrData).toContain('Usage'); + }); + + it('prints parse error and exits 1 for invalid JSON', () => { + process.argv[2] = 'echo'; + process.argv[3] = 'not json'; + runCli('test-cli', testCommands); + + expect(getExitCode()).toBe(1); + expect(stderrData).toContain('Unexpected token'); + }); + + it('prints validation error and exits 1 on schema failure', async () => { + process.argv[2] = 'echo'; + process.argv[3] = '{"value": 123}'; + runCli('test-cli', testCommands); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(getExitCode()).toBe(1); + expect(stderrData).toContain('validation failed'); + }); + + it('calls the command and writes result to stdout', async () => { + process.argv[2] = 'echo'; + process.argv[3] = '{"value": "hi"}'; + runCli('test-cli', testCommands); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(JSON.parse(stdoutData)).toEqual({ value: 'hi' }); + expect(getExitCode()).toBeUndefined(); + }); + + it('outputs raw string without JSON quotes', async () => { + const stringCommands = { + greet: cmd( + z.object({}).transform((input) => [input]), + () => 'hello world', + ), + }; + process.argv[2] = 'greet'; + process.argv[3] = '{}'; + runCli('test-cli', stringCommands); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(stdoutData).toBe('hello world\n'); + expect(getExitCode()).toBeUndefined(); + }); +});