├── .nvmrc ├── .npmrc ├── site ├── docs │ ├── utilities │ │ ├── deposits │ │ │ ├── getSourceHash.md │ │ │ ├── getTransactionDepositedEvents.md │ │ │ ├── rlpEncodeDepositTransaction.md │ │ │ ├── getL2HashFromL1DepositInfo.md │ │ │ └── getDepositTransaction.md │ │ ├── withdrawals │ │ │ ├── getWithdrawlMessageStorageSlot.md │ │ │ └── getProof.md │ │ └── transactionHelpers │ │ │ ├── getArgsFromTransactionDepositedOpaqueData.md │ │ │ └── transactionSerializer.md │ ├── actions │ │ ├── public │ │ │ ├── L2 │ │ │ │ ├── getWithdrawalMessages.md │ │ │ │ ├── simulateWithdrawERC20.md │ │ │ │ ├── getProveWithdrawalTransactionArgs.md │ │ │ │ ├── estimateL1GasUsed.md │ │ │ │ ├── estimateL1Fee.md │ │ │ │ ├── estimateFees.md │ │ │ │ └── simulateWithdrawETH.md │ │ │ └── L1 │ │ │ │ ├── getL2HashesForDepositTx.md │ │ │ │ ├── getSecondsToNextL2Output.md │ │ │ │ ├── readFinalizedWithdrawals.md │ │ │ │ ├── getOutputForL2Block.md │ │ │ │ ├── simulateDepositETH.md │ │ │ │ ├── readProvenWithdrawals.md │ │ │ │ ├── getSecondsToFinalizable.md │ │ │ │ ├── simulateFinalizeWithdrawalTransaction.md │ │ │ │ └── simulateDepositERC20.md │ │ └── wallet │ │ │ ├── L2 │ │ │ └── writeWithdrawERC20.md │ │ │ └── L1 │ │ │ ├── writeFinalizeWithdrawalTransaction.md │ │ │ ├── writeDepositETH.md │ │ │ ├── writeDepositERC20.md │ │ │ └── writeContractDeposit.md │ └── introduction │ │ └── quickstart.md ├── package.json ├── .vitepress │ └── theme │ │ └── index.ts └── index.md ├── pnpm-workspace.yaml ├── .github ├── actions │ ├── README.md │ └── setup │ │ └── action.yml └── workflows │ ├── release.yml │ └── verify.yml ├── src ├── types │ ├── withdrawal.ts │ ├── depositETH.ts │ ├── depositERC20.ts │ ├── withdrawTo.ts │ ├── addresses.ts │ ├── l1Actions.ts │ ├── l2Actions.ts │ ├── depositTransaction.ts │ ├── opStackContracts.ts │ └── gasPriceOracle.ts ├── chains │ ├── index.ts │ ├── baseGoerli.ts │ ├── optimismGoerli.ts │ ├── base.ts │ ├── zora.ts │ ├── optimism.ts │ └── zoraGoerli.ts ├── actions │ ├── public │ │ ├── L1 │ │ │ ├── getLatestProposedL2BlockNumber.test.ts │ │ │ ├── getOutputForL2Block.test.ts │ │ │ ├── simulateDepositETH.test.ts │ │ │ ├── readProvenWithdrawals.bench.ts │ │ │ ├── readFinalizedWithdrawals.test.ts │ │ │ ├── getSecondsToNextL2Output.test.ts │ │ │ ├── readProvenWithdrawals.test.ts │ │ │ ├── simulateFinalizeWithdrawalTransaction.test.ts │ │ │ ├── getL2HashesForDepositTx.ts │ │ │ ├── getLatestProposedL2BlockNumber.ts │ │ │ ├── getL2HashesForDepositTx.bench.ts │ │ │ ├── readFinalizedWithdrawals.ts │ │ │ ├── getSecondsToFinalizable.test.ts │ │ │ ├── getOutputForL2Block.ts │ │ │ ├── simulateDepositERC20.test.ts │ │ │ ├── simulateDepositETH.ts │ │ │ ├── getL2HashesForDepositTx.test.ts │ │ │ ├── simulateDepositERC20.ts │ │ │ ├── readProvenWithdrawals.ts │ │ │ ├── getSecondsToFinalizable.ts │ │ │ ├── simulateDepositTransaction.ts │ │ │ ├── simulateFinalizeWithdrawalTransaction.ts │ │ │ ├── getSecondsToNextL2Output.ts │ │ │ ├── simulateProveWithdrawalTransaction.ts │ │ │ └── simulateDepositTransaction.test.ts │ │ ├── getProof.test.ts │ │ ├── L2 │ │ │ ├── simulateWithdrawETH.test.ts │ │ │ ├── simulateWithdrawERC20.test.ts │ │ │ ├── estimateL1GasUsed.ts │ │ │ ├── getWithdrawalMessages.test.ts │ │ │ ├── estimateL1Fee.ts │ │ │ ├── getProveWithdrawalTransactionArgs.test.ts │ │ │ ├── getWithdrawalMessages.ts │ │ │ ├── simulateWithdrawETH.ts │ │ │ ├── simulateWithdrawERC20.ts │ │ │ ├── estimateFees.ts │ │ │ ├── getProveWithdrawalTransactionArgs.bench.ts │ │ │ ├── getProveWithdrawalTransactionArgs.ts │ │ │ └── estimateL1GasUsed.test.ts │ │ └── getProof.ts │ └── wallet │ │ ├── L1 │ │ ├── writeFinalizeWithdrawalTransaction.test.ts │ │ ├── writeDepositERC20.test.ts │ │ ├── writeDepositETH.ts │ │ ├── writeSendMessage.test.ts │ │ ├── writeDepositERC20.ts │ │ ├── writeDepositETH.test.ts │ │ ├── writeSendMessage.ts │ │ ├── writeFinalizeWithdrawalTransaction.ts │ │ ├── writeProveWithdrawalTransaction.ts │ │ └── writeDepositTransaction.ts │ │ └── L2 │ │ ├── writeWithdrawETH.test.ts │ │ ├── writeWithdrawETH.ts │ │ ├── writeWithdrawERC20.ts │ │ └── writeWithdrawERC20.test.ts ├── utils │ ├── getWithdrawalMessageStorageSlot.test.ts │ ├── getWithdrawalMessageStorageSlot.bench.ts │ ├── getL2HashFromL1DepositInfo.ts │ ├── index.ts │ ├── getWithdrawalMessageStorageSlot.ts │ ├── transactionSerializer.ts │ ├── getSourceHash.ts │ ├── rlpEncodeDepositTransaction.ts │ ├── getSourceHash.test.ts │ ├── getDepositTransaction.ts │ ├── getL2HashFromL1DepositInfo.bench.ts │ ├── getArgsFromTransactionDepositedOpaqueData.ts │ ├── getL2HashFromL1DepositInfo.test.ts │ ├── getTransactionDepositedEvents.ts │ ├── getTransactionDepositedEvents.test.ts │ ├── rlpEncodeDepositTransaction.test.ts │ └── getDepositTransaction.test.ts ├── _test │ ├── bench.ts │ ├── globalSetup.ts │ └── live.test.ts ├── decorators │ ├── walletL2OpStackActions.ts │ └── publicL2OpStackActions.ts └── index.ts ├── .changeset ├── config.json └── README.md ├── .vscode └── settings.json ├── tsconfig.node.json ├── tsconfig.build.json ├── tsconfig.json ├── dprint.json ├── .gitignore ├── biome.json ├── LICENSE.md ├── vitest.config.ts ├── README.md └── tsconfig.base.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.12.1 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | 3 | -------------------------------------------------------------------------------- /site/docs/utilities/deposits/getSourceHash.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/docs/actions/public/L2/getWithdrawalMessages.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/docs/actions/public/L2/simulateWithdrawERC20.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/docs/utilities/deposits/getTransactionDepositedEvents.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/docs/utilities/deposits/rlpEncodeDepositTransaction.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/docs/actions/public/L2/getProveWithdrawalTransactionArgs.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/docs/introduction/quickstart.md: -------------------------------------------------------------------------------- 1 | This is a quickstart page 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | prefer-workspace-packages: true 2 | packages: 3 | - '.' 4 | - 'examples/**/*' 5 | - 'playgrounds/*' 6 | - 'site' 7 | -------------------------------------------------------------------------------- /.github/actions/README.md: -------------------------------------------------------------------------------- 1 | # .github/actions 2 | 3 | Reusable actions that are used in other actions are here 4 | 5 | ## setup 6 | 7 | Setups forge pnpm node and caches node modules 8 | -------------------------------------------------------------------------------- /src/types/withdrawal.ts: -------------------------------------------------------------------------------- 1 | import { type Address, type Hex } from 'viem' 2 | 3 | export type MessagePassedEvent = { 4 | nonce: bigint 5 | sender: Address 6 | target: Address 7 | value: bigint 8 | gasLimit: bigint 9 | data: Hex 10 | withdrawalHash: Hex 11 | } 12 | -------------------------------------------------------------------------------- /site/docs/actions/wallet/L2/writeWithdrawERC20.md: -------------------------------------------------------------------------------- 1 | # writeWithdrawERC20 2 | 3 | ```ts 4 | const USDbC = '0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA' 5 | 6 | await opStackL2WalletClient.writeWithdrawERC20({ 7 | args: { l2Token: USDbC, to, amount, minGasLimit }, 8 | account, 9 | }) 10 | ``` 11 | -------------------------------------------------------------------------------- /src/chains/index.ts: -------------------------------------------------------------------------------- 1 | export { baseAddresses } from './base.js' 2 | export { baseGoerliAddresses } from './baseGoerli.js' 3 | export { optimismAddresses } from './optimism.js' 4 | export { optimismGoerliAddresses } from './optimismGoerli.js' 5 | export { zoraAddresses } from './zora.js' 6 | export { zoraGoerliAddresses } from './zoraGoerli.js' 7 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@op-viem/site", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vitepress dev", 7 | "build": "vitepress build", 8 | "serve": "vitepress serve" 9 | }, 10 | "devDependencies": { 11 | "vitepress": "1.0.0-beta.4", 12 | "vue": "^3.2.45" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/types/depositETH.ts: -------------------------------------------------------------------------------- 1 | import { optimismPortalABI } from '@eth-optimism/contracts-ts' 2 | import type { Address, Hex } from 'viem' 3 | 4 | export const ABI = optimismPortalABI 5 | export const FUNCTION = 'depositTransaction' 6 | 7 | export type DepositETHParameters = { 8 | to: Address 9 | gasLimit: number 10 | data?: Hex 11 | amount: bigint 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "dprint.dprint", 3 | "editor.formatOnSave": true, 4 | "typescript.tsdk": "node_modules/typescript/lib", 5 | "typescript.enablePromptUseWorkspaceTsdk": true, 6 | "editor.codeActionsOnSave": { 7 | "source.organizeImports.biome": "explicit" 8 | }, 9 | "[typescript]": { 10 | "editor.defaultFormatter": "dprint.dprint" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | // This configuration is used for local development and type checking of configuration and script files that are not part of the build. 3 | "include": ["vite.config.ts", "wagmi.config.ts", "scripts"], 4 | "compilerOptions": { 5 | "strict": true, 6 | "composite": true, 7 | "module": "ESNext", 8 | "moduleResolution": "Node", 9 | "allowSyntheticDefaultImports": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is used to compile the for cjs and esm (see package.json build scripts). It should exclude all test files. 3 | "extends": "./tsconfig.base.json", 4 | "include": ["src"], 5 | "exclude": [ 6 | "src/**/*.test.ts", 7 | "src/**/*.test-d.ts", 8 | "src/**/*.bench.ts", 9 | "src/_test" 10 | ], 11 | "compilerOptions": { 12 | "sourceMap": true, 13 | "rootDir": "./src" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This configuration is used for local development and type checking. 3 | "extends": "./tsconfig.base.json", 4 | "include": ["src"], 5 | "exclude": [], 6 | "references": [{ "path": "./tsconfig.node.json" }], 7 | "compilerOptions": { 8 | // Json import is used in `src/_test/utils.ts`. 9 | "resolveJsonModule": true // TODO: Try to avoid this. Maybe generate .ts files for bytecode too with wagmi? 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /site/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import './style.css' 2 | import Theme from 'vitepress/theme' 3 | // https://vitepress.dev/guide/custom-theme 4 | import { h } from 'vue' 5 | 6 | export default { 7 | extends: Theme, 8 | Layout: () => { 9 | return h(Theme.Layout, null, { 10 | // https://vitepress.dev/guide/extending-default-theme#layout-slots 11 | }) 12 | }, 13 | // enhanceApp({ app, router, siteData }) { 14 | // // ... 15 | // }, 16 | } 17 | -------------------------------------------------------------------------------- /dprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript": { 3 | "semiColons": "asi", 4 | "quoteStyle": "preferSingle" 5 | }, 6 | "json": { 7 | }, 8 | "markdown": { 9 | }, 10 | "excludes": [ 11 | "**/node_modules", 12 | "**/*-lock.json", 13 | "CHANGELOG.md" 14 | ], 15 | "plugins": [ 16 | "https://plugins.dprint.dev/typescript-0.87.1.wasm", 17 | "https://plugins.dprint.dev/json-0.17.4.wasm", 18 | "https://plugins.dprint.dev/markdown-0.16.0.wasm" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/types/depositERC20.ts: -------------------------------------------------------------------------------- 1 | import { l1StandardBridgeABI } from '@eth-optimism/contracts-ts' 2 | import type { Address, Hex } from 'viem' 3 | 4 | // TODO(Wilson): Consider moving these to actions/wallet/L1/types 5 | export const ABI = l1StandardBridgeABI 6 | export const FUNCTION = 'depositERC20To' 7 | 8 | export type DepositERC20Parameters = { 9 | l1Token: Address 10 | l2Token: Address 11 | to: Address 12 | amount: bigint 13 | minGasLimit: number 14 | extraData?: Hex 15 | } 16 | -------------------------------------------------------------------------------- /src/actions/public/L1/getLatestProposedL2BlockNumber.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | import { publicClient } from '../../../_test/utils.js' 3 | import { baseAddresses } from '../../../chains/index.js' 4 | import { getLatestProposedL2BlockNumber } from './getLatestProposedL2BlockNumber.js' 5 | 6 | test('retrieves correctly', async () => { 7 | const result = await getLatestProposedL2BlockNumber(publicClient, { 8 | ...baseAddresses, 9 | }) 10 | expect(result.l2BlockNumber).toBeDefined() 11 | }) 12 | -------------------------------------------------------------------------------- /src/utils/getWithdrawalMessageStorageSlot.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | import { getWithdrawalMessageStorageSlot } from './getWithdrawalMessageStorageSlot.js' 3 | 4 | test('returns correct source hash', async () => { 5 | const hash = '0xB1C3824DEF40047847145E069BF467AA67E906611B9F5EF31515338DB0AABFA2' 6 | // checked result of same method in OP SDK 7 | expect(getWithdrawalMessageStorageSlot(hash)).toEqual( 8 | '0x4a932049252365b3eedbc5190e18949f2ec11f39d3bef2d259764799a1b27d99', 9 | ) 10 | }) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | bench 25 | dist 26 | cache 27 | node_modules 28 | tsconfig*.tsbuildinfo 29 | *.tgz 30 | vitest.config.ts.timestamp* 31 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /src/types/withdrawTo.ts: -------------------------------------------------------------------------------- 1 | import { l2StandardBridgeABI } from '@eth-optimism/contracts-ts' 2 | import type { Address, Hex } from 'viem' 3 | 4 | export const OVM_ETH = '0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000' 5 | 6 | export const ABI = l2StandardBridgeABI 7 | export const FUNCTION = 'withdrawTo' 8 | 9 | export type WithdrawToParameters = { 10 | l2Token: Address 11 | to: Address 12 | amount: bigint 13 | minGasLimit: number 14 | extraData?: Hex 15 | } 16 | 17 | export type WithdrawETHParameters = Omit 18 | -------------------------------------------------------------------------------- /src/actions/public/L1/getOutputForL2Block.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | import { publicClient } from '../../../_test/utils.js' 3 | import { baseAddresses } from '../../../chains/index.js' 4 | import { getOutputForL2Block } from './getOutputForL2Block.js' 5 | 6 | test('retrieves correctly', async () => { 7 | const result = await getOutputForL2Block(publicClient, { 8 | l2BlockNumber: 2725977n, 9 | ...baseAddresses, 10 | }) 11 | expect(result.proposal).toBeDefined() 12 | expect(result.outputIndex).toBeDefined() 13 | }) 14 | -------------------------------------------------------------------------------- /site/docs/utilities/withdrawals/getWithdrawlMessageStorageSlot.md: -------------------------------------------------------------------------------- 1 | # getWithdrawalMessageStorageSlot 2 | 3 | Utility for hashing a message hash. This computes the storage slot where the message hash will be stored in state. 0 is used because the first mapping in the contract is used. 4 | 5 | ## Usage 6 | 7 | ```ts 8 | import { getWithdrawalMessageStorageSlot } from 'op-viem' 9 | 10 | const hash = 11 | '0xB1C3824DEF40047847145E069BF467AA67E906611B9F5EF31515338DB0AABFA2' 12 | 13 | getWithdrawalMessageStorageSlot(hash) 14 | ``` 15 | 16 | ## Returns 17 | 18 | `Hex` 19 | -------------------------------------------------------------------------------- /site/docs/actions/public/L1/getL2HashesForDepositTx.md: -------------------------------------------------------------------------------- 1 | # getL2HashesForDepositTx 2 | 3 | Get the L2 transaction hashes for a given L1 deposit transaction. 4 | 5 | ```ts [example.ts] 6 | import { publicL1Actions } from 'op-viem' 7 | import { createPublicClient } from 'viem' 8 | 9 | const publicClient = createPublicClient({ 10 | account, 11 | chain: mainnet, 12 | transport: http(), 13 | }).extend(publicL1Actions) 14 | 15 | const L2Hashes = await publicClient.getL2HashesForDepositTx({ 16 | l1TxHash: 17 | '0xe94031c3174788c3fee7216465c50bb2b72e7a1963f5af807b3768da10827f5c', 18 | }) 19 | ``` 20 | -------------------------------------------------------------------------------- /src/_test/bench.ts: -------------------------------------------------------------------------------- 1 | import { providers } from 'ethers' 2 | 3 | import { CrossChainMessenger } from '@eth-optimism/sdk' 4 | import { base, mainnet } from 'viem/chains' 5 | import { localHttpUrl, localRollupHttpUrl } from './constants.js' 6 | 7 | export const ethersProvider = new providers.JsonRpcProvider(localHttpUrl) 8 | export const ethersRollupProvider = new providers.JsonRpcProvider( 9 | localRollupHttpUrl, 10 | ) 11 | export const opSDKMessenger = new CrossChainMessenger({ 12 | l1ChainId: mainnet.id, 13 | l2ChainId: base.id, 14 | l1SignerOrProvider: ethersProvider, 15 | l2SignerOrProvider: ethersRollupProvider, 16 | }) 17 | -------------------------------------------------------------------------------- /src/utils/getWithdrawalMessageStorageSlot.bench.ts: -------------------------------------------------------------------------------- 1 | import { hashMessageHash } from '@eth-optimism/sdk' 2 | import { type Hex } from 'viem' 3 | import { bench, describe } from 'vitest' 4 | import { getWithdrawalMessageStorageSlot } from './getWithdrawalMessageStorageSlot.js' 5 | 6 | describe('Hashes message hash', () => { 7 | const hash: Hex = '0xB1C3824DEF40047847145E069BF467AA67E906611B9F5EF31515338DB0AABFA2' 8 | bench('op-viem: `getWithdrawalMessageStorageSlot`', async () => { 9 | getWithdrawalMessageStorageSlot(hash) 10 | }) 11 | 12 | bench('@eth-optimism/sdk: `hashMessageHash`', async () => { 13 | hashMessageHash(hash) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /src/chains/baseGoerli.ts: -------------------------------------------------------------------------------- 1 | import type { Addresses } from '../types/addresses.js' 2 | 3 | export const baseGoerliAddresses: Addresses<5> = { 4 | portal: { 5 | address: '0xe93c8cD0D409341205A592f8c4Ac1A5fe5585cfA', 6 | chainId: 5, 7 | }, 8 | l2OutputOracle: { 9 | address: '0x2A35891ff30313CcFa6CE88dcf3858bb075A2298', 10 | chainId: 5, 11 | }, 12 | l1StandardBridge: { 13 | address: '0xfA6D8Ee5BE770F84FC001D098C4bD604Fe01284a', 14 | chainId: 5, 15 | }, 16 | l1CrossDomainMessenger: { 17 | address: '0x8e5693140eA606bcEB98761d9beB1BC87383706D', 18 | chainId: 5, 19 | }, 20 | l1Erc721Bridge: { 21 | address: '0x5E0c967457347D5175bF82E8CCCC6480FCD7e568', 22 | chainId: 5, 23 | }, 24 | } as const 25 | -------------------------------------------------------------------------------- /src/chains/optimismGoerli.ts: -------------------------------------------------------------------------------- 1 | import type { Addresses } from '../types/addresses.js' 2 | 3 | export const optimismGoerliAddresses: Addresses<5> = { 4 | portal: { 5 | address: '0x5b47E1A08Ea6d985D6649300584e6722Ec4B1383', 6 | chainId: 5, 7 | }, 8 | l2OutputOracle: { 9 | address: '0xE6Dfba0953616Bacab0c9A8ecb3a9BBa77FC15c0', 10 | chainId: 5, 11 | }, 12 | l1StandardBridge: { 13 | address: '0x636Af16bf2f682dD3109e60102b8E1A089FedAa8', 14 | chainId: 5, 15 | }, 16 | l1CrossDomainMessenger: { 17 | address: '0x5086d1eEF304eb5284A0f6720f79403b4e9bE294', 18 | chainId: 5, 19 | }, 20 | l1Erc721Bridge: { 21 | address: '0x0F9C590b958002E8B10a7431979c1aF882772E88', 22 | chainId: 5, 23 | }, 24 | } as const 25 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | runs: 3 | using: composite 4 | steps: 5 | - uses: pnpm/action-setup@v2 6 | with: 7 | version: 8.6.0 8 | 9 | - uses: actions/setup-node@v3 10 | with: 11 | node-version-file: ".nvmrc" 12 | registry-url: https://registry.npmjs.org 13 | cache: pnpm 14 | 15 | - name: Install Foundry 16 | uses: foundry-rs/foundry-toolchain@v1 17 | with: 18 | version: nightly 19 | 20 | # Workaround for parallel building with forge: 21 | # https://github.com/foundry-rs/foundry/issues/4736 22 | - name: Setup forge hack 23 | shell: bash 24 | run: forge build 25 | 26 | - name: Install node modules 27 | run: pnpm install --frozen-lockfile 28 | shell: bash 29 | -------------------------------------------------------------------------------- /src/actions/public/L1/simulateDepositETH.test.ts: -------------------------------------------------------------------------------- 1 | import { writeContract } from 'viem/actions' 2 | import { expect, test } from 'vitest' 3 | import { accounts } from '../../../_test/constants.js' 4 | import { publicClient, walletClient } from '../../../_test/utils.js' 5 | import { baseAddresses } from '../../../chains/index.js' 6 | import { simulateDepositETH } from './simulateDepositETH.js' 7 | 8 | test('default', async () => { 9 | const { request } = await simulateDepositETH(publicClient, { 10 | args: { 11 | to: accounts[0].address, 12 | gasLimit: 100000, 13 | amount: 1n, 14 | }, 15 | account: accounts[0].address, 16 | ...baseAddresses, 17 | }) 18 | expect(request).toBeDefined() 19 | expect(await writeContract(walletClient, request)).toBeDefined() 20 | }) 21 | -------------------------------------------------------------------------------- /src/utils/getL2HashFromL1DepositInfo.ts: -------------------------------------------------------------------------------- 1 | import { type Hash, keccak256 } from 'viem' 2 | import { type TransactionDepositedEvent } from '../types/depositTransaction.js' 3 | import { getDepositTransaction } from './getDepositTransaction.js' 4 | import { rlpEncodeDepositTransaction } from './rlpEncodeDepositTransaction.js' 5 | 6 | type GetL2HashFromDepositInfoParams = { 7 | event: TransactionDepositedEvent 8 | logIndex: number 9 | blockHash: Hash 10 | } 11 | 12 | export function getL2HashFromL1DepositInfo({ 13 | event, 14 | logIndex, 15 | blockHash, 16 | }: GetL2HashFromDepositInfoParams) { 17 | const depositTx = getDepositTransaction({ 18 | event, 19 | logIndex, 20 | l1BlockHash: blockHash, 21 | }) 22 | 23 | return keccak256(rlpEncodeDepositTransaction(depositTx)) 24 | } 25 | -------------------------------------------------------------------------------- /src/actions/public/getProof.test.ts: -------------------------------------------------------------------------------- 1 | import { createPublicClient, http, toHex } from 'viem' 2 | import { base } from 'viem/chains' 3 | import { expect, test } from 'vitest' 4 | import { getProof } from './getProof.js' 5 | 6 | test('correctly retrieves proof', async () => { 7 | // cannot currently use anvil rollupPublicClient for this as eth_getProof isn't working 8 | const client = createPublicClient({ 9 | chain: base, 10 | transport: http(), 11 | }) 12 | 13 | const result = await getProof(client, { 14 | address: '0x4200000000000000000000000000000000000016', 15 | storageKeys: [ 16 | '0x4a932049252365b3eedbc5190e18949f2ec11f39d3bef2d259764799a1b27d99', 17 | ], 18 | block: toHex(3155269n), 19 | }) 20 | 21 | expect(result.storageProof[0].value).toEqual('0x1') 22 | }) 23 | -------------------------------------------------------------------------------- /src/types/addresses.ts: -------------------------------------------------------------------------------- 1 | import { type Address } from 'viem' 2 | 3 | export type ContractAddress = { address: Address; chainId: chainId; blockCreated?: number } 4 | export type Addresses = { 5 | portal: ContractAddress 6 | l2OutputOracle: ContractAddress 7 | l1StandardBridge: ContractAddress 8 | l1CrossDomainMessenger: ContractAddress 9 | l1Erc721Bridge: ContractAddress 10 | } 11 | 12 | export type RawOrContractAddress = Address | ContractAddress 13 | 14 | export function resolveAddress(address: RawOrContractAddress): `0x${string}` { 15 | if (typeof address !== 'string' && !address?.address) throw new Error('Invalid address') 16 | return typeof address === 'string' ? address : address.address 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { parseOpaqueData } from './getArgsFromTransactionDepositedOpaqueData.js' 2 | export type { GetDepositTransactionParams } from './getDepositTransaction.js' 3 | export { getDepositTransaction } from './getDepositTransaction.js' 4 | export { getL2HashFromL1DepositInfo } from './getL2HashFromL1DepositInfo.js' 5 | export { getSourceHash } from './getSourceHash.js' 6 | export type { 7 | GetTransactionDepositedEventsParams, 8 | GetTransactionDepositedEventsReturnType, 9 | TransactionDepositedEventDetails, 10 | } from './getTransactionDepositedEvents.js' 11 | export { getTransactionDepositedEvents } from './getTransactionDepositedEvents.js' 12 | export { getWithdrawalMessageStorageSlot } from './getWithdrawalMessageStorageSlot.js' 13 | export { rlpEncodeDepositTransaction } from './rlpEncodeDepositTransaction.js' 14 | -------------------------------------------------------------------------------- /src/chains/base.ts: -------------------------------------------------------------------------------- 1 | import { type Addresses } from '../types/addresses.js' 2 | 3 | export const baseAddresses: Addresses<1> = { 4 | portal: { 5 | address: '0x49048044D57e1C92A77f79988d21Fa8fAF74E97e', 6 | chainId: 1, 7 | blockCreated: 17482143, 8 | }, 9 | l2OutputOracle: { 10 | address: '0x56315b90c40730925ec5485cf004d835058518A0', 11 | chainId: 1, 12 | blockCreated: 17482143, 13 | }, 14 | l1StandardBridge: { 15 | address: '0x3154Cf16ccdb4C6d922629664174b904d80F2C35', 16 | chainId: 1, 17 | blockCreated: 17482143, 18 | }, 19 | l1CrossDomainMessenger: { 20 | address: '0x866E82a600A1414e583f7F13623F1aC5d58b0Afa', 21 | chainId: 1, 22 | blockCreated: 17482143, 23 | }, 24 | l1Erc721Bridge: { 25 | address: '0x608d94945A64503E642E6370Ec598e519a2C1E53', 26 | chainId: 1, 27 | blockCreated: 17482143, 28 | }, 29 | } as const 30 | -------------------------------------------------------------------------------- /src/chains/zora.ts: -------------------------------------------------------------------------------- 1 | import type { Addresses } from '../types/addresses.js' 2 | 3 | export const zoraAddresses: Addresses<1> = { 4 | portal: { 5 | address: '0x1a0ad011913A150f69f6A19DF447A0CfD9551054', 6 | chainId: 1, 7 | blockCreated: 17473938, 8 | }, 9 | l2OutputOracle: { 10 | address: '0x9E6204F750cD866b299594e2aC9eA824E2e5f95c', 11 | chainId: 1, 12 | blockCreated: 17473936, 13 | }, 14 | l1StandardBridge: { 15 | address: '0xbF6acaF315477b15D638bf4d91eA48FA79b58335', 16 | chainId: 1, 17 | blockCreated: 17473944, 18 | }, 19 | l1CrossDomainMessenger: { 20 | address: '0x363B4B1ADa52E50353f746999bd9E94395190d2C', 21 | chainId: 1, 22 | blockCreated: 17473943, 23 | }, 24 | l1Erc721Bridge: { 25 | address: '0x83A4521A3573Ca87f3a971B169C5A0E1d34481c3', 26 | chainId: 1, 27 | blockCreated: 17473940, 28 | }, 29 | } as const 30 | -------------------------------------------------------------------------------- /src/types/l1Actions.ts: -------------------------------------------------------------------------------- 1 | import type { Abi, Account, Chain, SendTransactionParameters, SimulateContractParameters } from 'viem' 2 | 3 | export type L1WriteActionBaseType< 4 | TChain extends Chain | undefined = Chain, 5 | TAccount extends Account | undefined = Account | undefined, 6 | TChainOverride extends Chain | undefined = Chain | undefined, 7 | > = Omit< 8 | SendTransactionParameters< 9 | TChain, 10 | TAccount, 11 | TChainOverride 12 | >, 13 | 'chain' 14 | > 15 | 16 | export type L1SimulateActionBaseType< 17 | TChain extends Chain | undefined = Chain, 18 | TChainOverride extends Chain | undefined = Chain | undefined, 19 | TAbi extends Abi | readonly unknown[] = Abi, 20 | TFunctioName extends string = string, 21 | > = Omit< 22 | SimulateContractParameters, 23 | 'abi' | 'functionName' | 'args' | 'address' 24 | > 25 | -------------------------------------------------------------------------------- /src/chains/optimism.ts: -------------------------------------------------------------------------------- 1 | import type { Addresses } from '../types/addresses.js' 2 | 3 | export const optimismAddresses: Addresses<1> = { 4 | portal: { 5 | address: '0xbEb5Fc579115071764c7423A4f12eDde41f106Ed', 6 | chainId: 1, 7 | blockCreated: 17365802, 8 | }, 9 | l2OutputOracle: { 10 | address: '0xdfe97868233d1aa22e815a266982f2cf17685a27', 11 | chainId: 1, 12 | blockCreated: 17365801, 13 | }, 14 | l1StandardBridge: { 15 | address: '0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1', 16 | chainId: 1, 17 | blockCreated: 12686786, 18 | }, 19 | l1CrossDomainMessenger: { 20 | address: '0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1', 21 | chainId: 1, 22 | blockCreated: 12686757, 23 | }, 24 | l1Erc721Bridge: { 25 | address: '0x5a7749f83b81B301cAb5f48EB8516B986DAef23D', 26 | chainId: 1, 27 | blockCreated: 15677422, 28 | }, 29 | } as const 30 | -------------------------------------------------------------------------------- /src/chains/zoraGoerli.ts: -------------------------------------------------------------------------------- 1 | import type { Addresses } from '../types/addresses.js' 2 | 3 | export const zoraGoerliAddresses: Addresses<5> = { 4 | portal: { 5 | address: '0xDb9F51790365e7dc196e7D072728df39Be958ACe', 6 | chainId: 5, 7 | blockCreated: 8942392, 8 | }, 9 | l2OutputOracle: { 10 | address: '0xdD292C9eEd00f6A32Ff5245d0BCd7f2a15f24e00', 11 | chainId: 5, 12 | blockCreated: 8942390, 13 | }, 14 | l1StandardBridge: { 15 | address: '0x39CCDe9769d52d61189AB799d91665A11b5f3464', 16 | chainId: 5, 17 | blockCreated: 8942398, 18 | }, 19 | l1CrossDomainMessenger: { 20 | address: '0x9779A9D2f3B66A4F4d27cB99Ab6cC1266b3Ca9af', 21 | chainId: 5, 22 | blockCreated: 8942397, 23 | }, 24 | l1Erc721Bridge: { 25 | address: '0x57C1C6b596ce90C0e010c358DD4Aa052404bB70F', 26 | chainId: 5, 27 | blockCreated: 8942394, 28 | }, 29 | } as const 30 | -------------------------------------------------------------------------------- /src/utils/getWithdrawalMessageStorageSlot.ts: -------------------------------------------------------------------------------- 1 | import { encodeAbiParameters, type Hex, keccak256, parseAbiParameters } from 'viem' 2 | 3 | // from https://github.com/ethereum-optimism/optimism/blob/develop/packages/sdk/src/utils/message-utils.ts#L42 4 | // adapted to viem 5 | 6 | /** 7 | * Utility for hashing a message hash. This computes the storage slot 8 | * where the message hash will be stored in state. 0 is used 9 | * because the first mapping in the contract is used. 10 | * 11 | * @param messageHash sent message hash, i.e. keccak256(abi.encode({...WithdrawalTransaction})). 12 | * @returns The storage slot in L2ToL1MessagePasser of the sent message 13 | */ 14 | export const getWithdrawalMessageStorageSlot = (messageHash: Hex): Hex => { 15 | const data = encodeAbiParameters(parseAbiParameters(['bytes32, uint256']), [ 16 | messageHash, 17 | 0n, 18 | ]) 19 | return keccak256(data) 20 | } 21 | -------------------------------------------------------------------------------- /src/actions/public/L1/readProvenWithdrawals.bench.ts: -------------------------------------------------------------------------------- 1 | import { bench, describe } from 'vitest' 2 | import { opSDKMessenger } from '../../../_test/bench.js' 3 | import { publicClient } from '../../../_test/utils.js' 4 | import { baseAddresses } from '../../../chains/index.js' 5 | import { readProvenWithdrawals } from './readProvenWithdrawals.js' 6 | 7 | describe('reads proven withdrawal', () => { 8 | const withdrawalHash = '0xEC0AD491512F4EDC603C2DD7B9371A0B18D4889A23E74692101BA4C6DC9B5709' 9 | bench( 10 | 'op-viem: `readProvenWithdrawals`', 11 | async () => { 12 | await readProvenWithdrawals(publicClient, { 13 | ...baseAddresses, 14 | withdrawalHash: withdrawalHash, 15 | }) 16 | }, 17 | ) 18 | 19 | bench( 20 | '@eth-optimism/sdk: `getProvenWithdrawal`', 21 | async () => { 22 | await opSDKMessenger.getProvenWithdrawal(withdrawalHash) 23 | }, 24 | ) 25 | }) 26 | -------------------------------------------------------------------------------- /src/utils/transactionSerializer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Abi, 3 | encodeFunctionData, 4 | type EncodeFunctionDataParameters, 5 | serializeTransaction, 6 | type TransactionSerializableEIP1559, 7 | type TransactionSerializedEIP1559, 8 | } from 'viem' 9 | 10 | /** 11 | * Serializes a transaction with EIP-1559 12 | */ 13 | export function serializeEip1559Transaction< 14 | TAbi extends Abi | readonly unknown[], 15 | TFunctionName extends string | undefined = undefined, 16 | >( 17 | options: 18 | & EncodeFunctionDataParameters 19 | & Omit, 20 | ): TransactionSerializedEIP1559 { 21 | const encodedFunctionData = encodeFunctionData(options) 22 | const serializedTransaction = serializeTransaction({ 23 | ...options, 24 | data: encodedFunctionData, 25 | type: 'eip1559', 26 | }) 27 | return serializedTransaction as TransactionSerializedEIP1559 28 | } 29 | -------------------------------------------------------------------------------- /src/actions/public/L1/readFinalizedWithdrawals.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | import { publicClient } from '../../../_test/utils.js' 3 | import { baseAddresses } from '../../../chains/base.js' 4 | import { readFinalizedWithdrawals } from './readFinalizedWithdrawals.js' 5 | 6 | test('read finalized withdrawals', async () => { 7 | const finalizedWithdrawal = await readFinalizedWithdrawals(publicClient, { 8 | ...baseAddresses, 9 | withdrawalHash: '0xEC0AD491512F4EDC603C2DD7B9371A0B18D4889A23E74692101BA4C6DC9B5709', 10 | }) 11 | 12 | expect(finalizedWithdrawal).toEqual(true) 13 | }) 14 | 15 | test('returns false if not finalized', async () => { 16 | const finalizedWithdrawal = await readFinalizedWithdrawals(publicClient, { 17 | ...baseAddresses, 18 | withdrawalHash: '0xEC0AD491512F4EDC603C2DD7B9371A0C18D4889A23E74692101BA4C6DC9B5709', 19 | }) 20 | 21 | expect(finalizedWithdrawal).toEqual(false) 22 | }) 23 | -------------------------------------------------------------------------------- /src/utils/getSourceHash.ts: -------------------------------------------------------------------------------- 1 | import { concat, type Hex, keccak256, pad, toHex } from 'viem' 2 | import { SourceHashDomain } from '../types/depositTransaction.js' 3 | 4 | type GetSourceHashParams = { 5 | domain: SourceHashDomain 6 | logIndex: number 7 | l1BlockHash: Hex 8 | } 9 | 10 | /// from https://github.com/ethereum-optimism/optimism/blob/develop/packages/core-utils/src/optimism/deposit-transaction.ts#L117 11 | /// with adaptions for viem 12 | /// NOTE currently only supports user deposit txs, not L1InfoDeposit 13 | export function getSourceHash({ 14 | domain, 15 | logIndex, 16 | l1BlockHash, 17 | }: GetSourceHashParams) { 18 | const marker = toHex(logIndex) 19 | const input = concat([l1BlockHash, pad(marker, { size: 32 })]) 20 | const depositIDHash = keccak256(input) 21 | const domainHex = toHex(domain) 22 | const domainInput = concat([pad(domainHex, { size: 32 }), depositIDHash]) 23 | return keccak256(domainInput) 24 | } 25 | -------------------------------------------------------------------------------- /src/actions/public/L2/simulateWithdrawETH.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | import { accounts } from '../../../_test/constants.js' 3 | import { rollupPublicClient } from '../../../_test/utils.js' 4 | import { opStackL2ChainContracts } from '../../../index.js' 5 | import { OVM_ETH } from '../../../types/withdrawTo.js' 6 | import { simulateWithdrawETH } from './simulateWithdrawETH.js' 7 | 8 | test('correctly simulatwes transaction', async () => { 9 | const amount = 100n 10 | const extraData = '0x1234' 11 | const minGasLimit = 10000 12 | const to = accounts[0].address 13 | const { request } = await simulateWithdrawETH(rollupPublicClient, { 14 | args: { to, amount, minGasLimit, extraData }, 15 | }) 16 | expect(request.args).toEqual([OVM_ETH, to, amount, minGasLimit, extraData]) 17 | expect(request.address).toEqual(opStackL2ChainContracts.l2StandardBridge.address) 18 | expect(request.functionName).toEqual('withdrawTo') 19 | }) 20 | -------------------------------------------------------------------------------- /src/actions/public/L1/getSecondsToNextL2Output.test.ts: -------------------------------------------------------------------------------- 1 | import { createPublicClient, http } from 'viem' 2 | import { base } from 'viem/chains' 3 | import { expect, test } from 'vitest' 4 | import { publicClient } from '../../../_test/utils.js' 5 | import { baseAddresses } from '../../../chains/base.js' 6 | import { getSecondsToNextL2Output } from './getSecondsToNextL2Output.js' 7 | 8 | test('get seconds to next L2 output', async () => { 9 | const l2Client = createPublicClient({ 10 | chain: base, 11 | transport: http(), 12 | }) 13 | const latestL2BlockNumber = await l2Client.getBlockNumber() 14 | 15 | const time = await getSecondsToNextL2Output(publicClient, { latestL2BlockNumber, ...baseAddresses }) 16 | expect(time).toBeDefined() 17 | // this is too noisy to node issues, 18 | // but I do think we should revert if latestL2BlockNumber 19 | // passed is less than latestBlock from the oracle 20 | // expect(time).toBeLessThan(1800n * 2n) 21 | }) 22 | -------------------------------------------------------------------------------- /src/utils/rlpEncodeDepositTransaction.ts: -------------------------------------------------------------------------------- 1 | import { concat, type Hex, toRlp, trim } from 'viem' 2 | import { DEPOSIT_TX_PREFIX, type DepositTransaction } from '../types/depositTransaction.js' 3 | 4 | export function rlpEncodeDepositTransaction( 5 | depositTx: DepositTransaction, 6 | ): Hex { 7 | const trimmedMint = trim(depositTx.mint) 8 | const trimmedValue = trim(depositTx.value) 9 | const trimmedGas = trim(depositTx.gas) 10 | const rlp = toRlp([ 11 | depositTx.sourceHash, 12 | depositTx.from, 13 | depositTx.to, 14 | // NOTE(Wilson): I am not sure who is *correct* here but OP encodes 15 | // '0x00' as '0x' and viem trim does not have the same behavior 16 | trimmedMint === '0x00' ? '0x' : trimmedMint, 17 | trimmedValue === '0x00' ? '0x' : trimmedValue, 18 | trimmedGas === '0x00' ? '0x' : trimmedGas, 19 | depositTx.isSystemTransaction ? '0x1' : '0x', 20 | depositTx.data, 21 | ]) 22 | return concat([DEPOSIT_TX_PREFIX, rlp]) 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/getSourceHash.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | import { SourceHashDomain } from '../types/depositTransaction.js' 3 | import { getSourceHash } from './getSourceHash.js' 4 | 5 | /* 6 | Check values 7 | L2 curl to get source hash: curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_getTransactionByHash","params":["0xe67200042bf79eef76850dd3986bdd544e7aceeb7bbf8449158088bdc582168a"],"id":1}' https://developer-access-mainnet.base.org/ 8 | L1 tx: https://etherscan.io/tx/0xe94031c3174788c3fee7216465c50bb2b72e7a1963f5af807b3768da10827f5c 9 | */ 10 | test('returns correct source hash', () => { 11 | const sourceHash = getSourceHash({ 12 | domain: SourceHashDomain.UserDeposit, 13 | logIndex: 196, 14 | l1BlockHash: '0x9ba3933dc6ce43c145349770a39c30f9b647f17668f004bd2e05c80a2e7262f7', 15 | }) 16 | expect(sourceHash).toEqual( 17 | '0xd0868c8764d81f1749edb7dec4a550966963540d9fe50aefce8cdb38ea7b2213', 18 | ) 19 | }) 20 | -------------------------------------------------------------------------------- /src/types/l2Actions.ts: -------------------------------------------------------------------------------- 1 | import type { Abi, Account, Chain, SimulateContractParameters, WriteContractParameters } from 'viem' 2 | 3 | export type L2WriteContractParameters< 4 | TAbi extends Abi | readonly unknown[] = Abi, 5 | TFunctioName extends string = string, 6 | TChain extends Chain | undefined = Chain, 7 | TAccount extends Account | undefined = Account | undefined, 8 | TChainOverride extends Chain | undefined = Chain | undefined, 9 | > = Omit< 10 | WriteContractParameters, 11 | 'abi' | 'functionName' | 'args' | 'address' 12 | > 13 | 14 | export type L2SimulateContractParameters< 15 | TAbi extends Abi | readonly unknown[] = Abi, 16 | TFunctioName extends string = string, 17 | TChain extends Chain | undefined = Chain, 18 | TChainOverride extends Chain | undefined = Chain | undefined, 19 | > = Omit< 20 | SimulateContractParameters, 21 | 'abi' | 'functionName' | 'args' | 'address' 22 | > 23 | -------------------------------------------------------------------------------- /src/actions/public/getProof.ts: -------------------------------------------------------------------------------- 1 | import type { Address, BlockTag, Chain, Hash, Hex, PublicClient, Transport } from 'viem' 2 | 3 | export type GetProofParameters = { 4 | address: Address 5 | storageKeys: Hex[] 6 | block: Hex | BlockTag | Hash 7 | } 8 | 9 | export type AccountProof = { 10 | address: Address 11 | accountProof: Hex[] 12 | balance: Hex 13 | nonce: Hex 14 | storageHash: Hex 15 | storageProof: StorageProof[] 16 | } 17 | 18 | export type StorageProof = { 19 | key: Hex 20 | value: Hex 21 | proof: Hex[] 22 | } 23 | 24 | // NOTE(wilson): This should be suported in viem but isn't currently 25 | export async function getProof( 26 | client: PublicClient, 27 | { address, storageKeys, block }: GetProofParameters, 28 | ): Promise { 29 | return await client.request<{ 30 | Parameters: [Address, Hex[], Hex | BlockTag | Hash] 31 | ReturnType: AccountProof 32 | }>({ method: 'eth_getProof', params: [address, storageKeys, block] }) 33 | } 34 | -------------------------------------------------------------------------------- /src/actions/public/L2/simulateWithdrawERC20.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | import { accounts } from '../../../_test/constants.js' 3 | import { rollupPublicClient } from '../../../_test/utils.js' 4 | import { opStackL2ChainContracts } from '../../../index.js' 5 | import { simulateWithdrawERC20 } from './simulateWithdrawERC20.js' 6 | 7 | test('correctly simulatwes transaction', async () => { 8 | const l2Token = '0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA' 9 | const amount = 100n 10 | const extraData = '0x1234' 11 | const minGasLimit = 10000 12 | const to = accounts[0].address 13 | const { request } = await simulateWithdrawERC20(rollupPublicClient, { 14 | args: { l2Token, to, amount, minGasLimit, extraData }, 15 | account: '0xbc3ed6b537f2980e66f396fe14210a56ba3f72c4', 16 | }) 17 | expect(request.args).toEqual([l2Token, to, amount, minGasLimit, extraData]) 18 | expect(request.address).toEqual(opStackL2ChainContracts.l2StandardBridge.address) 19 | expect(request.functionName).toEqual('withdrawTo') 20 | }) 21 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.0.0/schema.json", 3 | "organizeImports": { 4 | "enabled": false 5 | }, 6 | "files": { 7 | "ignore": [ 8 | "**/node_modules", 9 | "CHANGELOG.md", 10 | "cache", 11 | "coverage", 12 | "dist", 13 | "tsconfig.json", 14 | "tsconfig.*.json", 15 | "generated.ts", 16 | "pnpm-lock.yaml", 17 | "bench" 18 | ] 19 | }, 20 | "linter": { 21 | "enabled": true, 22 | "rules": { 23 | "recommended": true, 24 | "a11y": { 25 | "useButtonType": "off" 26 | }, 27 | "correctness": { 28 | "noUnusedVariables": "error" 29 | }, 30 | "performance": { 31 | "noDelete": "off" 32 | }, 33 | "style": { 34 | "noNonNullAssertion": "off", 35 | "useShorthandArrayType": "error" 36 | }, 37 | "suspicious": { 38 | "noArrayIndexKey": "off", 39 | "noAssignInExpressions": "off", 40 | "noExplicitAny": "off" 41 | } 42 | } 43 | }, 44 | "formatter": { 45 | "enabled": false 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/types/depositTransaction.ts: -------------------------------------------------------------------------------- 1 | import type { Address, Hex } from 'viem' 2 | 3 | // https://github.com/ethereum-optimism/op-geth/blob/optimism/core/types/deposit_tx.go#L25 4 | export const DEPOSIT_TX_PREFIX = '0x7E' 5 | 6 | // https://github.com/ethereum-optimism/optimism/blob/develop/op-node/rollup/derive/deposit_source.go#L15-L18 7 | export enum SourceHashDomain { 8 | UserDeposit = 0, 9 | // TODO(wilson): consider supporting system transactions 10 | // L1InfoDeposit = 1, 11 | } 12 | 13 | // https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L1/OptimismPortal.sol#L73C1-L73C1 14 | export type TransactionDepositedEvent = { 15 | eventName: 'TransactionDeposited' 16 | args: { 17 | from: Address 18 | to: Address 19 | version: bigint 20 | opaqueData: Hex 21 | } 22 | } 23 | 24 | // https://github.com/ethereum-optimism/op-geth/blob/optimism/core/types/deposit_tx.go#L27-L44 25 | export type DepositTransaction = { 26 | sourceHash: Hex 27 | from: Address 28 | to: Address 29 | mint: Hex 30 | value: Hex 31 | gas: Hex 32 | isSystemTransaction: boolean 33 | data: Hex 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present Coinbase 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/actions/public/L1/readProvenWithdrawals.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | import { publicClient } from '../../../_test/utils.js' 3 | import { baseAddresses } from '../../../chains/base.js' 4 | import { readProvenWithdrawals } from './readProvenWithdrawals.js' 5 | 6 | test('read proven withdrawals', async () => { 7 | const expected = { 8 | outputRoot: '0xe83cb1b39b2d1059fafec9cf9b9338b9944d2683c421856ead33b1eb02036a56', 9 | timestamp: 1692521795n, 10 | l2OutputIndex: 1490n, 11 | } 12 | 13 | const provenWithdrawal = await readProvenWithdrawals(publicClient, { 14 | ...baseAddresses, 15 | withdrawalHash: '0xEC0AD491512F4EDC603C2DD7B9371A0B18D4889A23E74692101BA4C6DC9B5709', 16 | }) 17 | 18 | expect(provenWithdrawal).toEqual(expected) 19 | }) 20 | 21 | test('raises error if not proven', async () => { 22 | expect(() => 23 | readProvenWithdrawals(publicClient, { 24 | ...baseAddresses, 25 | withdrawalHash: '0xEC0AD491512F4EDC603C2DD7B9371A0C18D4889A23E74692101BA4C6DC9B5709', 26 | }) 27 | ).rejects.toThrowError( 28 | 'Withdrawal with hash 0xEC0AD491512F4EDC603C2DD7B9371A0C18D4889A23E74692101BA4C6DC9B5709 is not proven', 29 | ) 30 | }) 31 | -------------------------------------------------------------------------------- /src/types/opStackContracts.ts: -------------------------------------------------------------------------------- 1 | import type { ChainContract } from 'viem' 2 | 3 | export enum OpStackL2Contract { 4 | L2CrossDomainMessenger = 'l2CrossDomainMessenger', 5 | L2StandardBridge = 'l2StandardBridge', 6 | GasPriceOracle = 'gasPriceOracle', 7 | L1Block = 'l1Block', 8 | L2ToL1MessagePasser = 'l2ToL1MessagePasser', 9 | L2Erc721Bridge = 'l2Erc721Bridge', 10 | OptimismMintableErc721Factory = 'optimismMintableErc721Factory', 11 | } 12 | 13 | export type OpStackL2ChainContracts = { [key in OpStackL2Contract]: ChainContract } 14 | 15 | export const opStackL2ChainContracts: OpStackL2ChainContracts = { 16 | l2CrossDomainMessenger: { address: '0x4200000000000000000000000000000000000007' }, 17 | l2StandardBridge: { address: '0x4200000000000000000000000000000000000010' }, 18 | gasPriceOracle: { address: '0x420000000000000000000000000000000000000F' }, 19 | l1Block: { address: '0x4200000000000000000000000000000000000015' }, 20 | l2ToL1MessagePasser: { address: '0x4200000000000000000000000000000000000016' }, 21 | l2Erc721Bridge: { address: '0x4200000000000000000000000000000000000014' }, 22 | optimismMintableErc721Factory: { address: '0x4200000000000000000000000000000000000017' }, 23 | } 24 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | export const poolId = Number(process.env.VITEST_POOL_ID ?? 1) 2 | export const localHttpUrl = `http://127.0.0.1:8545/${poolId}` 3 | export const localWsUrl = `ws://127.0.0.1:8545/${poolId}` 4 | export const localRollupHttpUrl = `http://127.0.0.1:8555/${poolId}` 5 | export const localRollupWsUrl = `ws://127.0.0.1:8555/${poolId}` 6 | import { defineConfig } from 'vitest/config' 7 | 8 | export default defineConfig({ 9 | test: { 10 | benchmark: { 11 | outputFile: './bench/report.json', 12 | reporters: process.env.CI ? ['json'] : ['verbose'], 13 | }, 14 | // if you are using the default rpc you will need these to not get rate limited 15 | // maxConcurrency: 1, 16 | // maxThreads: 1, 17 | // minThreads: 1, 18 | coverage: { 19 | lines: 95, 20 | statements: 95, 21 | functions: 90, 22 | branches: 90, 23 | thresholdAutoUpdate: true, 24 | reporter: ['text', 'json-summary', 'json'], 25 | exclude: [ 26 | '**/errors/utils.ts', 27 | '**/dist/**', 28 | '**/*.test.ts', 29 | '**/_test/**', 30 | ], 31 | }, 32 | environment: 'node', 33 | globalSetup: ['./src/_test/globalSetup.ts'], 34 | testTimeout: 100_000, 35 | }, 36 | }) 37 | -------------------------------------------------------------------------------- /site/docs/actions/public/L1/getSecondsToNextL2Output.md: -------------------------------------------------------------------------------- 1 | # getSecondsToNextL2Output 2 | 3 | Returns how long until the next L2 output, for a given chain, is posted on L1. This is useful when waiting to prove a withdrawal. 4 | 5 | ```ts [example.ts] 6 | const l2Client = createPublicClient({ 7 | chain: base, 8 | transport: http(), 9 | }) 10 | const latestL2BlockNumber = await l2Client.getBlockNumber() 11 | 12 | const l1Client = createPublicClient({ 13 | chain: mainnet, 14 | transport: http(), 15 | }).extend(publicL1OpStackActions) 16 | 17 | const time = await l1Client.getSecondsToNextL2Output(, { 18 | latestL2BlockNumber, 19 | l2OutputOracle: baseAddresses.l2OutputOracle, 20 | }) 21 | // Or 22 | const time = await l1Client.getSecondsToNextL2Output(, { 23 | latestL2BlockNumber, 24 | ...baseAddresses, 25 | }) 26 | ``` 27 | 28 | ## Return Value 29 | 30 | `bigint` 31 | 32 | Seconds until the next L2 output should be posted. 33 | 34 | ## Parameters 35 | 36 | ### latestL2BlockNumber 37 | 38 | - **Type:** `bigint` 39 | 40 | The latest L2 block number. 41 | 42 | ### l2OutputOracle 43 | 44 | - **Type:** [`RawOrContractAddress`](https://opviem.sh/docs/glossary/types.html#raworcontractaddress) 45 | 46 | The address of the L2OutputOracle contract. 47 | -------------------------------------------------------------------------------- /site/docs/actions/wallet/L1/writeFinalizeWithdrawalTransaction.md: -------------------------------------------------------------------------------- 1 | # writeFinalizeWithdrawalTransaction 2 | 3 | ```ts 4 | const withdrawal: FinalizeWithdrawalTransactionParameters = { 5 | nonce: 176684 7064778384329583297500742918515827483896875618958121606201292641795n, 6 | sender: '0x02f086dBC384d69b3041BC738F0a8af5e49dA181', 7 | target: '0x02f086dBC384d69b3041BC738F0a8af5e49dA181', 8 | value: 335000000000000000000n, 9 | gasLimit: 100000n, 10 | data: '0x01', 11 | } 12 | 13 | const hash = await opStackL1WalletClient.writeFinalizeWithdrawalTranasction({ 14 | portal: baseAddresses.portal, 15 | args: { withdrawal }, 16 | account, 17 | }) 18 | ``` 19 | 20 | ## Return Value 21 | 22 | [`Hash`](https://viem.sh/docs/glossary/types#hash) 23 | 24 | A [Transaction Hash](https://viem.sh/docs/glossary/terms#hash). 25 | 26 | ## Parameters 27 | 28 | ### withdrawal 29 | 30 | ```ts 31 | type FinalizeWithdrawalTransactionParameters = { 32 | nonce: bigint 33 | sender: `0x${string}` 34 | target: `0x${string}` 35 | value: bigint 36 | gasLimit: bigint 37 | data: `0x${string}` 38 | } 39 | ``` 40 | 41 | ### portal 42 | 43 | - **Type:** [`RawOrContractAddress`](https://opviem.sh/docs/glossary/types.html#raworcontractaddress) 44 | 45 | The `OptimismPortal` contract. 46 | -------------------------------------------------------------------------------- /site/docs/actions/public/L1/readFinalizedWithdrawals.md: -------------------------------------------------------------------------------- 1 | # readFinalizedWithdrawals 2 | 3 | Returns a boolean for whether the withdrawal of a given withdrawl hash has been finalized. 4 | 5 | ```ts [example.ts] 6 | import { publicL1Actions } from 'op-viem' 7 | import { createPublicClient } from 'viem' 8 | 9 | const publicClient = createPublicClient({ 10 | account, 11 | chain: mainnet, 12 | transport: http(), 13 | }).extend(publicL1Actions) 14 | 15 | const finalizedWithdrawal = await readFinalizedWithdrawals(publicClient, { 16 | portal: baseAddresses.portal, 17 | withdrawalHash: 18 | '0xEC0AD491512F4EDC603C2DD7B9371A0B18D4889A23E74692101BA4C6DC9B5709', 19 | }) 20 | // Or 21 | const finalizedWithdrawal = await readFinalizedWithdrawals(publicClient, { 22 | ...baseAddresses, 23 | withdrawalHash: 24 | '0xEC0AD491512F4EDC603C2DD7B9371A0B18D4889A23E74692101BA4C6DC9B5709', 25 | }) 26 | ``` 27 | 28 | ## Return Value 29 | 30 | Returns a `boolean` for whether the withdrawal has been finalized. 31 | 32 | ## Parameters 33 | 34 | ### portalAddress 35 | 36 | - **Type:** [`RawOrContractAddress`](https://opviem.sh/docs/glossary/types.html#raworcontractaddress) 37 | 38 | The `OptimismPortal` contract where the sendMessage call should be made. 39 | 40 | ### withdrawalHash 41 | 42 | - **Type:** `Hex` 43 | 44 | The hash of the withdrawal 45 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Version 🔖 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | version: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | environment: release 15 | permissions: 16 | contents: write 17 | pull-requests: write 18 | id-token: write 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | with: 23 | submodules: recursive 24 | 25 | - name: "Setup" 26 | uses: ./.github/actions/setup 27 | 28 | - name: Build 29 | shell: bash 30 | run: pnpm build 31 | 32 | - name: Set deployment token 33 | run: npm config set '//registry.npmjs.org/:_authToken' "${{ secrets.NPM_TOKEN }}" 34 | 35 | # https://github.com/changesets/action#with-publishing 36 | - name: Handle Release Pull Request or Publish to npm 37 | id: changesets 38 | uses: changesets/action@v1 39 | with: 40 | title: "chore: version packages 🔖" 41 | commit: "chore: version packages 🔖" 42 | publish: pnpm release:publish 43 | version: pnpm release:version 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 47 | 48 | -------------------------------------------------------------------------------- /src/actions/public/L1/simulateFinalizeWithdrawalTransaction.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | import { accounts } from '../../../_test/constants.js' 3 | import { publicClient } from '../../../_test/utils.js' 4 | import { baseAddresses } from '../../../chains/base.js' 5 | import { type FinalizeWithdrawalTransactionParameters } from '../../wallet/L1/writeFinalizeWithdrawalTransaction.js' 6 | import { simulateFinalizeWithdrawalTransaction } from './simulateFinalizeWithdrawalTransaction.js' 7 | 8 | // From https://etherscan.io/tx/0xcb571be93844895a45a4cf70cc4424fcc6ccf55dd4c14759da1efd57fa593ac5 9 | test('succesfully submits finalizeWithdrawalTransaction', async () => { 10 | const withdrawal: FinalizeWithdrawalTransactionParameters = { 11 | nonce: 1766847064778384329583297500742918515827483896875618958121606201292642114n, 12 | sender: '0x54392fc895e6e44538975272E0dD7335fCcC9045', 13 | target: '0x54392fc895e6e44538975272E0dD7335fCcC9045', 14 | value: 20000000000000000n, 15 | gasLimit: 100000n, 16 | data: '0x01', 17 | } 18 | 19 | const { request } = await simulateFinalizeWithdrawalTransaction(publicClient, { 20 | ...baseAddresses, 21 | withdrawal, 22 | account: accounts[0].address, 23 | }) 24 | 25 | expect(request.address).toEqual(baseAddresses.portal.address) 26 | expect(request.args[0]).toEqual(withdrawal) 27 | }) 28 | -------------------------------------------------------------------------------- /src/actions/wallet/L1/writeFinalizeWithdrawalTransaction.test.ts: -------------------------------------------------------------------------------- 1 | import { mine } from 'viem/actions' 2 | import { expect, test } from 'vitest' 3 | import { accounts } from '../../../_test/constants.js' 4 | import { publicClient, testClient, walletClient } from '../../../_test/utils.js' 5 | import { baseAddresses } from '../../../chains/base.js' 6 | import { 7 | type FinalizeWithdrawalTransactionParameters, 8 | writeFinalizeWithdrawalTranasction, 9 | } from './writeFinalizeWithdrawalTransaction.js' 10 | 11 | // From https://etherscan.io/tx/0x33b540f3ae33049ecb19c83a62fe15ad41dc38ccce4cf0eaf92c55431031f1b5 12 | test('succesfully submits finalizeWithdrawalTransaction', async () => { 13 | const withdrawal: FinalizeWithdrawalTransactionParameters = { 14 | nonce: 1766847064778384329583297500742918515827483896875618958121606201292641795n, 15 | sender: '0x02f086dBC384d69b3041BC738F0a8af5e49dA181', 16 | target: '0x02f086dBC384d69b3041BC738F0a8af5e49dA181', 17 | value: 335000000000000000000n, 18 | gasLimit: 100000n, 19 | data: '0x01', 20 | } 21 | 22 | const hash = await writeFinalizeWithdrawalTranasction(walletClient, { 23 | ...baseAddresses, 24 | args: { withdrawal }, 25 | account: accounts[0].address, 26 | }) 27 | await mine(testClient, { blocks: 1 }) 28 | 29 | const r = await publicClient.getTransactionReceipt({ hash }) 30 | expect(r.status).toEqual('success') 31 | }) 32 | -------------------------------------------------------------------------------- /site/docs/actions/wallet/L1/writeDepositETH.md: -------------------------------------------------------------------------------- 1 | # writeDepositETH 2 | 3 | Writes a deposit of ETH from L1 to L2. 4 | 5 | ```ts [example.ts] 6 | import { walletL1OpStackActions } from 'op-viem' 7 | import { baseAddresses } from 'op-viem/chains' 8 | import { createWalletClient } from 'viem' 9 | 10 | const walletClient = createWalletClient({ 11 | chain: mainnet, 12 | transport: http(), 13 | }).extend(walletL1OpStackActions) 14 | 15 | const hash = await walletClient.writeDepositETH({ 16 | args: { 17 | to: '0xFd4F24676eD4588928213F37B126B53c07186F45', 18 | gasLimit: 100000, 19 | amount: 1n, 20 | }, 21 | ...baseAddresses, 22 | account: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045', 23 | }) 24 | ``` 25 | 26 | ## Return Value 27 | 28 | Returns a transaction hash of the deposit transaction. 29 | 30 | ## Parameters 31 | 32 | ### args 33 | 34 | - #### to 35 | - **Type:** `Address` 36 | - The address to deposit the tokens to. 37 | 38 | - #### gasLimit 39 | - **Type:** `number` 40 | - The minimum gas limit to use for the deposit transaction. 41 | 42 | - #### amount 43 | - **Type:** `bigint` 44 | - The amount of ETH to deposit. 45 | 46 | ### portal 47 | 48 | - **Type:** [`RawOrContractAddress`](https://opviem.sh/docs/glossary/types.html#raworcontractaddress) 49 | 50 | The `OptimismPortal` contract. 51 | 52 | ### account 53 | 54 | - **Type:** `Address` 55 | 56 | The address of the account to deposit from. 57 | -------------------------------------------------------------------------------- /src/types/gasPriceOracle.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Abi, 3 | BlockTag, 4 | EncodeFunctionDataParameters, 5 | PublicClient, 6 | TransactionSerializableEIP1559, 7 | Transport, 8 | } from 'viem' 9 | import type { Chain } from 'viem/chains' 10 | 11 | /** 12 | * Options to query a specific block 13 | */ 14 | export type BlockOptions = { 15 | /** 16 | * Block number to query from 17 | */ 18 | blockNumber?: bigint 19 | /** 20 | * Block tag to query from 21 | */ 22 | blockTag?: BlockTag 23 | } 24 | 25 | /** 26 | * Options for all GasPriceOracle methods 27 | */ 28 | export type GasPriceOracleParameters = BlockOptions 29 | 30 | /** 31 | * Options for specifying the transaction being estimated 32 | */ 33 | export type OracleTransactionParameters< 34 | TAbi extends Abi | readonly unknown[], 35 | TFunctionName extends string | undefined = undefined, 36 | > = 37 | & EncodeFunctionDataParameters 38 | & Omit 39 | & GasPriceOracleParameters 40 | /** 41 | * Options for specifying the transaction being estimated 42 | */ 43 | export type GasPriceOracleEstimator = < 44 | TChain extends Chain | undefined, 45 | TAbi extends Abi | readonly unknown[], 46 | TFunctionName extends string | undefined = undefined, 47 | >( 48 | client: PublicClient, 49 | options: OracleTransactionParameters, 50 | ) => Promise 51 | -------------------------------------------------------------------------------- /site/docs/actions/public/L2/estimateL1GasUsed.md: -------------------------------------------------------------------------------- 1 | # estimateL1GasUsed 2 | 3 | Returns the L1 gas used for the specified transaction. 4 | 5 | ```ts 6 | const L1GasUsedValue = await estimateL1GasUsed(data, { 7 | abi, 8 | functionName: balanceOf, 9 | args: [address], 10 | }) 11 | ``` 12 | 13 | The [Gas Price Oracle](https://docs.optimism.io/builders/tools/oracles#gas-oracle) is called to calculate the gas that will be necessary to publish the given transaction to L1. 14 | 15 | ## Return Value 16 | 17 | `bigint` 18 | 19 | The estimated gas used. 20 | 21 | ## Parameters 22 | 23 | ### client 24 | 25 | - **Type:** `PublicClient` 26 | 27 | A client for the desired OP Stack chain. 28 | 29 | ### options 30 | 31 | - **abi:** `Abi` 32 | 33 | The ABI for the contract containing the function being estimated. 34 | 35 | - **functionName:** `string` 36 | 37 | The name of the function being estimated. 38 | 39 | - **args:** `any[]` 40 | 41 | The arguments to the function being estimated. 42 | 43 | - **to (optional):** `Address` 44 | 45 | Transaction recipient. 46 | 47 | - **value (optional):** `bigint` 48 | 49 | Value (in wei) sent with this transaction. 50 | 51 | - **blockNumber (optional)**: `number` 52 | 53 | The block number to perform the gas estimate against. 54 | 55 | - **blockTag (optional):** `'latest' | 'earliest' | 'pending' | 'safe' | 'finalized'` 56 | 57 | **Default:** `'latest'` 58 | 59 | The block tag to perform the gas estimate against. 60 | -------------------------------------------------------------------------------- /site/docs/actions/public/L1/getOutputForL2Block.md: -------------------------------------------------------------------------------- 1 | # getOutputForL2Block 2 | 3 | Calls to the L2OutputOracle contract on L1 to get the output for a given L2 block. 4 | 5 | ```ts [example.ts] 6 | import { publicL1Actions } from 'op-viem' 7 | import { baseAddresses } from 'op-viem/chains' 8 | import { createPublicClient } from 'viem' 9 | 10 | const publicClient = createPublicClient({ 11 | account, 12 | chain: mainnet, 13 | transport: http(), 14 | }).extend(publicL1Actions) 15 | 16 | await getOutputForL2Block(publicClient, { 17 | blockNumber: 2725977n, 18 | l2OutputOracle: baseAddresses.l2OutputOracle, 19 | }) 20 | 21 | // more simply 22 | await getOutputForL2Block(publicClient, { 23 | blockNumber: 2725977n, 24 | ...baseAddresses, 25 | }) 26 | ``` 27 | 28 | ## Return Value 29 | 30 | Returns `GetOutputForL2BlockReturnType`. 31 | 32 | ```ts [example.ts] 33 | export type Proposal = { 34 | outputRoot: Hex 35 | timestamp: bigint 36 | l2BlockNumber: bigint 37 | } 38 | 39 | export type GetOutputForL2BlockReturnType = { 40 | proposal: Proposal 41 | outputIndex: bigint 42 | } 43 | ``` 44 | 45 | ## Parameters 46 | 47 | ### blockNumber 48 | 49 | - **Type:** `bigint` 50 | 51 | The block number of the L2 block for which to get the output. 52 | 53 | ### l2OutputOracle 54 | 55 | - **Type:** [`RawOrContractAddress`](https://opviem.sh/docs/glossary/types.html#raworcontractaddress) 56 | 57 | The address of the L2OutputOracle contract where the `getOutputForL2Block` call will be made. 58 | -------------------------------------------------------------------------------- /src/actions/public/L1/getL2HashesForDepositTx.ts: -------------------------------------------------------------------------------- 1 | import type { Chain, Hash, PublicClient, TransactionReceipt, Transport } from 'viem' 2 | import { getTransactionReceipt } from 'viem/actions' 3 | import { getL2HashFromL1DepositInfo } from '../../../utils/getL2HashFromL1DepositInfo.js' 4 | import { getTransactionDepositedEvents } from '../../../utils/getTransactionDepositedEvents.js' 5 | 6 | export type GetL2HashesForDepositTxParameters = { 7 | l1TxHash: Hash 8 | l1TxReceipt?: never 9 | } | { l1TxHash?: never; l1TxReceipt: TransactionReceipt } 10 | 11 | export type GetL2HashesForDepositTxReturnType = Hash[] 12 | 13 | /** 14 | * Gets the L2 transaction hashes for a given L1 deposit transaction 15 | * 16 | * @param {Hash} l1TxHash the L1 transaction hash of the deposit 17 | * @returns {GetL2HashesForDepositTxReturnType} the L2 transaction hashes for the deposit 18 | */ 19 | export async function getL2HashesForDepositTx( 20 | client: PublicClient, 21 | { l1TxHash, l1TxReceipt }: GetL2HashesForDepositTxParameters, 22 | ): Promise { 23 | const txReceipt = l1TxReceipt ?? await getTransactionReceipt(client, { hash: l1TxHash }) 24 | const depositEvents = getTransactionDepositedEvents({ txReceipt }) 25 | 26 | return depositEvents.map(({ event, logIndex }) => 27 | getL2HashFromL1DepositInfo({ 28 | event, 29 | logIndex, 30 | blockHash: txReceipt.blockHash, 31 | }) 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/actions/public/L1/getLatestProposedL2BlockNumber.ts: -------------------------------------------------------------------------------- 1 | import { l2OutputOracleABI } from '@eth-optimism/contracts-ts' 2 | import type { Chain, PublicClient, Transport } from 'viem' 3 | import { readContract } from 'viem/actions' 4 | import { type RawOrContractAddress, resolveAddress } from '../../../types/addresses.js' 5 | 6 | export type GetLatestProposedL2BlockNumberParameters< 7 | TChain extends Chain | undefined = Chain | undefined, 8 | _chainId = TChain extends Chain ? TChain['id'] : number, 9 | > = { l2OutputOracle: RawOrContractAddress<_chainId> } 10 | 11 | export type GetLatestProposedL2BlockNumberReturnType = { 12 | l2BlockNumber: bigint 13 | } 14 | 15 | /** 16 | * Gets the latest proposed L2 block number from the L2 Output Oracle. 17 | * 18 | * @param {RawOrContractAddress} l2OutputOracle the address of the L2 Output Oracle 19 | * @returns {GetLatestProposedL2BlockNumberReturnType} the latest proposed L2 block number 20 | */ 21 | export async function getLatestProposedL2BlockNumber( 22 | client: PublicClient, 23 | { 24 | l2OutputOracle, 25 | }: GetLatestProposedL2BlockNumberParameters, 26 | ): Promise { 27 | const resolvedAddress = resolveAddress(l2OutputOracle) 28 | 29 | const l2BlockNumber = await readContract(client, { 30 | address: resolvedAddress, 31 | abi: l2OutputOracleABI, 32 | functionName: 'latestBlockNumber', 33 | }) 34 | 35 | return { l2BlockNumber } 36 | } 37 | -------------------------------------------------------------------------------- /site/docs/actions/public/L1/simulateDepositETH.md: -------------------------------------------------------------------------------- 1 | # simulateDepositETH 2 | 3 | Simulates a deposit of ETH from L1 to L2. 4 | 5 | ```ts [example.ts] 6 | import { publicL1Actions } from 'op-viem' 7 | import { baseAddresses } from 'op-viem/chains' 8 | import { createPublicClient } from 'viem' 9 | 10 | const publicClient = createPublicClient({ 11 | account, 12 | chain: mainnet, 13 | transport: http(), 14 | }).extend(publicL1Actions) 15 | 16 | const { request } = await publicClient.simulateDepositETH({ 17 | args: { 18 | to: '0xFd4F24676eD4588928213F37B126B53c07186F45', 19 | gasLimit: 100000, 20 | amount: 1n, 21 | }, 22 | portal: baseAddresses.portal, 23 | account: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045', 24 | }) 25 | ``` 26 | 27 | ## Return Value 28 | 29 | Returns a `request` that can be passed to Viem's `writeContract` and a `result` indicating whether the simulation succeeded. 30 | 31 | ## Parameters 32 | 33 | ### args 34 | 35 | - #### to 36 | - **Type:** `Address` 37 | - The address to deposit the tokens to. 38 | 39 | - #### gasLimit 40 | - **Type:** `number` 41 | - The minimum gas limit to use for the deposit transaction. 42 | 43 | - #### amount 44 | - **Type:** `bigint` 45 | - The amount of ETH to deposit. 46 | 47 | ### portal 48 | 49 | - **Type:** [`RawOrContractAddress`](https://opviem.sh/docs/glossary/types.html#raworcontractaddress) 50 | 51 | The `OptimismPortal` contract. 52 | 53 | ### account 54 | 55 | - **Type:** `Address` 56 | 57 | The address of the account to deposit from. 58 | -------------------------------------------------------------------------------- /src/decorators/walletL2OpStackActions.ts: -------------------------------------------------------------------------------- 1 | import type { Account, Chain, Transport, WriteContractReturnType } from 'viem' 2 | import type { WalletClient } from 'viem' 3 | import { writeWithdrawERC20, type WriteWithdrawERC20Parameters } from '../actions/wallet/L2/writeWithdrawERC20.js' 4 | import { writeWithdrawETH, type WriteWithdrawETHParameters } from '../actions/wallet/L2/writeWithdrawETH.js' 5 | 6 | export type WalletL2OpStackActions< 7 | // TODO(Wilson): Consider making this OpStackChain 8 | TChain extends Chain | undefined = Chain | undefined, 9 | TAccount extends Account | undefined = Account | undefined, 10 | > = { 11 | writeWithdrawETH: < 12 | TChainOverride extends Chain | undefined = Chain | undefined, 13 | >( 14 | args: WriteWithdrawETHParameters, 15 | ) => Promise 16 | writeWithdrawERC20: < 17 | TChainOverride extends Chain | undefined = Chain | undefined, 18 | >( 19 | args: WriteWithdrawERC20Parameters, 20 | ) => Promise 21 | } 22 | 23 | export function walletL2OpStackActions< 24 | TTransport extends Transport = Transport, 25 | TChain extends Chain = Chain, 26 | TAccount extends Account = Account, 27 | >( 28 | client: WalletClient, 29 | ): WalletL2OpStackActions { 30 | return { 31 | writeWithdrawERC20: (args) => writeWithdrawERC20(client, args), 32 | writeWithdrawETH: (args) => writeWithdrawETH(client, args), 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/actions/wallet/L1/writeDepositERC20.test.ts: -------------------------------------------------------------------------------- 1 | import { mine, writeContract } from 'viem/actions' 2 | import { expect, test } from 'vitest' 3 | import { erc20ABI } from 'wagmi' 4 | import { publicClient, testClient, walletClient } from '../../../_test/utils.js' 5 | import { baseAddresses } from '../../../chains/index.js' 6 | import { writeDepositERC20 } from './writeDepositERC20.js' 7 | 8 | const USDCL1 = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' 9 | const USDCL2 = '0x2e668bb88287675e34c8df82686dfd0b7f0c0383' 10 | const zenaddress = '0xFd4F24676eD4588928213F37B126B53c07186F45' 11 | 12 | test('default', async () => { 13 | await testClient.impersonateAccount({ 14 | address: zenaddress, 15 | }) 16 | await testClient.setBalance({ 17 | address: zenaddress, 18 | value: 10n ** 22n, 19 | }) 20 | await writeContract(testClient, { 21 | address: USDCL1, 22 | abi: erc20ABI, 23 | functionName: 'approve', 24 | args: [baseAddresses.l1StandardBridge.address, 10000n], 25 | account: zenaddress, 26 | }) 27 | await mine(testClient, { blocks: 1 }) 28 | const hash = await writeDepositERC20(walletClient, { 29 | args: { 30 | l1Token: USDCL1, 31 | l2Token: USDCL2, 32 | to: zenaddress, 33 | amount: 1n, 34 | minGasLimit: 21000, 35 | extraData: '0x', 36 | }, 37 | ...baseAddresses, 38 | account: zenaddress, 39 | }) 40 | await mine(testClient, { blocks: 1 }) 41 | expect((await publicClient.getTransactionReceipt({ hash })).status).toEqual( 42 | 'success', 43 | ) 44 | }) 45 | -------------------------------------------------------------------------------- /site/docs/utilities/deposits/getL2HashFromL1DepositInfo.md: -------------------------------------------------------------------------------- 1 | --- 2 | head: 3 | - - meta 4 | - property: og:title 5 | content: getL2HashFromL1DepositInfo 6 | - - meta 7 | - name: description 8 | content: Get the L2 transaction hash for a given L1 deposit transaction. 9 | --- 10 | 11 | # getL2HashFromL1DepositInfo 12 | 13 | Get the L2 transaction hash for a given L1 deposit transaction. 14 | 15 | ```ts [example.ts] 16 | import { getL2HashFromL1DepositInfo, TransactionDepositedEvent } from 'op-viem' 17 | 18 | const event: TransactionDepositedEvent = { 19 | eventName: 'TransactionDeposited', 20 | args: { 21 | from: '0xbc3ed6B537f2980e66f396Fe14210A56ba3f72C4', 22 | to: '0xbc3ed6B537f2980e66f396Fe14210A56ba3f72C4', 23 | version: 0n, 24 | opaqueData: 25 | '0x0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000001000000000000526c0000', 26 | }, 27 | } 28 | 29 | const logIndex = 196 30 | const blockHash = 31 | '0x9ba3933dc6ce43c145349770a39c30f9b647f17668f004bd2e05c80a2e7262f7' 32 | 33 | const hash = getL2HashFromL1DepositInfo({ 34 | event: event, 35 | logIndex: logIndex, 36 | l1BlockHash: blockHash, 37 | }) 38 | ``` 39 | 40 | ## Returns 41 | 42 | [`L2Hash`](/docs/glossary/types#l2hash) 43 | 44 | ## Parameters 45 | 46 | ### event 47 | 48 | - **Type:** [`TransactionDepositedEvent`](/docs/glossary/types#transactiondepositedevent) 49 | 50 | ### logIndex 51 | 52 | - **Type:** `number` 53 | 54 | ### l1BlockHash 55 | 56 | - **Type:** `Hash` 57 | -------------------------------------------------------------------------------- /src/actions/public/L1/getL2HashesForDepositTx.bench.ts: -------------------------------------------------------------------------------- 1 | import { optimismPortalABI } from '@eth-optimism/contracts-ts' 2 | import { DepositTx } from '@eth-optimism/core-utils' 3 | import { ethers } from 'ethers' 4 | import { bench, describe } from 'vitest' 5 | import { ethersProvider } from '../../../_test/bench.js' 6 | import { publicClient } from '../../../_test/utils.js' 7 | import { baseAddresses } from '../../../chains/base.js' 8 | import { getL2HashesForDepositTx } from './getL2HashesForDepositTx.js' 9 | 10 | describe('Computes L2 hash for L1 event', () => { 11 | bench('op-viem: `getL2HashesForDepositTx`', async () => { 12 | await getL2HashesForDepositTx(publicClient, { 13 | l1TxHash: '0xe94031c3174788c3fee7216465c50bb2b72e7a1963f5af807b3768da10827f5c', 14 | }) 15 | }) 16 | 17 | bench('@eth-optimism/core-utils: `DepositTx`', async () => { 18 | // Note(Wilson): I could not find a more efficient way to get the event needed from ethers. 19 | // I am not sure how to produce an event from a transaction receipt. 20 | // Happy to update this if there is a better comparison 21 | const contract = new ethers.Contract( 22 | baseAddresses.portal.address, 23 | optimismPortalABI, 24 | ethersProvider, 25 | ) 26 | const filter = contract.filters.TransactionDeposited( 27 | '0xbc3ed6B537f2980e66f396Fe14210A56ba3f72C4', 28 | '0xbc3ed6B537f2980e66f396Fe14210A56ba3f72C4', 29 | ) 30 | const events = await contract.queryFilter(filter, 17809754, 17809754) 31 | DepositTx.fromL1Event(events[0]).hash() 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /site/docs/utilities/transactionHelpers/getArgsFromTransactionDepositedOpaqueData.md: -------------------------------------------------------------------------------- 1 | --- 2 | head: 3 | - - meta 4 | - property: og:title 5 | content: getArgsFromTransactionDepositedOpaqueData 6 | - - meta 7 | - name: description 8 | content: Parses the opaque data from the `TransactionDepositedEvent` event args, extracting and structuring key transaction data. 9 | --- 10 | 11 | # getArgsFromTransactionDepositedOpaqueData 12 | 13 | Parses the opaque data from the `TransactionDepositedEvent` event args, returning structured transaction data. 14 | 15 | This function is a key component in the `getDepositTransaction` process, where it extracts and formats fields like `mint`, `value`, `gas`, `isCreation`, and `data` from the opaque data. These fields are then used to construct a `DepositTransaction` object. 16 | 17 | ## Import 18 | 19 | ```ts 20 | import { parseOpaqueData } from './utils/getArgsFromTransactionDepositedOpaqueData.js' 21 | ``` 22 | 23 | ## Usage 24 | 25 | ```ts 26 | import { parseOpaqueData } from './utils/getArgsFromTransactionDepositedOpaqueData.js' 27 | 28 | // ... within getDepositTransaction function 29 | const parsedOpaqueData = parseOpaqueData(event.args.opaqueData) 30 | // Use parsedOpaqueData to construct DepositTransaction 31 | ``` 32 | 33 | ## Returns 34 | 35 | `ParsedTransactionDepositedOpaqueData` 36 | 37 | Returns an object containing structured transaction data with fields such as mint, value, gas, isCreation, and data. 38 | 39 | ## Parameters 40 | 41 | `opaqueData` 42 | 43 | **Type:** Hex 44 | **Description:** The opaque data from the TransactionDepositedEvent event args. 45 | -------------------------------------------------------------------------------- /src/utils/getDepositTransaction.ts: -------------------------------------------------------------------------------- 1 | import { type Hex } from 'viem' 2 | import { 3 | type DepositTransaction, 4 | SourceHashDomain, 5 | type TransactionDepositedEvent, 6 | } from '../types/depositTransaction.js' 7 | import { parseOpaqueData } from './getArgsFromTransactionDepositedOpaqueData.js' 8 | import { getSourceHash } from './getSourceHash.js' 9 | 10 | export type GetDepositTransactionParams = 11 | & { event: TransactionDepositedEvent } 12 | & ({ 13 | sourceHash: Hex 14 | logIndex?: never 15 | l1BlockHash?: never 16 | domain?: never 17 | } | { 18 | sourceHash?: never 19 | logIndex: number 20 | l1BlockHash: Hex 21 | domain?: SourceHashDomain 22 | }) 23 | 24 | export function getDepositTransaction({ 25 | event, 26 | sourceHash, 27 | logIndex, 28 | l1BlockHash, 29 | domain = SourceHashDomain.UserDeposit, 30 | }: GetDepositTransactionParams): DepositTransaction { 31 | sourceHash = sourceHash ?? getSourceHash({ domain, logIndex, l1BlockHash }) 32 | /// code from https://github.com/ethereum-optimism/optimism/blob/develop/packages/core-utils/src/optimism/deposit-transaction.ts#L198 33 | /// with adaptions for viem 34 | const parsedOpaqueData = parseOpaqueData(event.args.opaqueData) 35 | const isSystemTransaction = false 36 | const to = parsedOpaqueData.isCreation === true ? '0x' : event.args.to 37 | 38 | return { 39 | sourceHash, 40 | from: event.args.from, 41 | to, 42 | mint: parsedOpaqueData.mint, 43 | value: parsedOpaqueData.value, 44 | gas: parsedOpaqueData.gas, 45 | isSystemTransaction, 46 | data: parsedOpaqueData.data, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /site/docs/actions/public/L1/readProvenWithdrawals.md: -------------------------------------------------------------------------------- 1 | # readProvenWithdrawals 2 | 3 | Returns a `ProvenWithdrawal` struct containing the `outputRoot`, `timestamp`, and `l2OutputIndex` for a given withdrawal hash. Returns error if withdrawal has not been proven. 4 | 5 | ```ts [example.ts] 6 | import { publicL1Actions } from 'op-viem' 7 | import { baseAddresses } from 'op-viem/chains' 8 | import { createPublicClient } from 'viem' 9 | 10 | const publicClient = createPublicClient({ 11 | account, 12 | chain: mainnet, 13 | transport: http(), 14 | }).extend(publicL1Actions) 15 | 16 | const provenWithdrawal = await readProvenWithdrawals(publicClient, { 17 | portal: baseAddresses.portal, 18 | withdrawalHash: 19 | '0xEC0AD491512F4EDC603C2DD7B9371A0B18D4889A23E74692101BA4C6DC9B5709', 20 | }) 21 | // or 22 | const provenWithdrawal = await readProvenWithdrawals(publicClient, { 23 | ...baseAddresses, 24 | withdrawalHash: 25 | '0xEC0AD491512F4EDC603C2DD7B9371A0B18D4889A23E74692101BA4C6DC9B5709', 26 | }) 27 | ``` 28 | 29 | ## Return Value 30 | 31 | Returns an object that represents a `ProvenWithdrawl` struct that contains the `outputRoot`, `timestamp`, and `l2OutputIndex` 32 | 33 | ```ts 34 | type ProvenWithdrawal = { 35 | outputRoot: Hex 36 | timestamp: bigint 37 | l2OutputIndex: bigint 38 | } 39 | ``` 40 | 41 | ## Parameters 42 | 43 | ### portal 44 | 45 | - **Type:** [`RawOrContractAddress`](https://opviem.sh/docs/glossary/types.html#raworcontractaddress) 46 | 47 | The `OptimismPortal` contract where the sendMessage call should be made. 48 | 49 | ### withdrawalHash 50 | 51 | - **Type:** `Hex` 52 | 53 | The hash of the withdrawal 54 | -------------------------------------------------------------------------------- /src/actions/public/L1/readFinalizedWithdrawals.ts: -------------------------------------------------------------------------------- 1 | import { optimismPortalABI } from '@eth-optimism/contracts-ts' 2 | import type { Chain, PublicClient, ReadContractParameters, Transport } from 'viem' 3 | import { readContract } from 'viem/actions' 4 | import type { MessagePassedEvent } from '../../../index.js' 5 | import { type RawOrContractAddress, resolveAddress } from '../../../types/addresses.js' 6 | 7 | const ABI = optimismPortalABI 8 | const FUNCTION_NAME = 'finalizedWithdrawals' 9 | 10 | export type ReadFinalizedWithdrawalsParameters< 11 | TChain extends Chain | undefined = Chain | undefined, 12 | _chainId = TChain extends Chain ? TChain['id'] : number, 13 | > = { withdrawalHash: MessagePassedEvent['withdrawalHash']; portal: RawOrContractAddress<_chainId> } 14 | 15 | /** 16 | * Reads whether a withdrawal has been finalized from the Optimism Portal. 17 | * 18 | * @param {Hash} withdrawalHash the hash of the withdrawal 19 | * @param {RawOrContractAddress} portal the address of the portal 20 | * 21 | * @returns {Promise} whether the withdrawal is finalized 22 | */ 23 | export async function readFinalizedWithdrawals( 24 | client: PublicClient, 25 | { 26 | withdrawalHash, 27 | portal, 28 | }: ReadFinalizedWithdrawalsParameters, 29 | ): Promise { 30 | const finalizedWithdrawal = await readContract(client, { 31 | abi: ABI, 32 | functionName: FUNCTION_NAME, 33 | address: resolveAddress(portal), 34 | args: [withdrawalHash], 35 | chain: client.chain, 36 | } as ReadContractParameters) 37 | 38 | return finalizedWithdrawal 39 | } 40 | -------------------------------------------------------------------------------- /site/docs/utilities/transactionHelpers/transactionSerializer.md: -------------------------------------------------------------------------------- 1 | --- 2 | head: 3 | - - meta 4 | - property: og:title 5 | content: transactionSerializer 6 | - - meta 7 | - name: description 8 | content: Add here. 9 | --- 10 | 11 | # transactionSerializer 12 | 13 | Serializes a transaction compliant with Ethereum Improvement Proposal (EIP) 1559. 14 | 15 | This utility function takes transaction parameters and serializes them into a format compatible with EIP-1559, which introduced a new transaction type to Ethereum's fee market. It is essential for applications interacting with Ethereum's Layer 1 and Layer 2 solutions, particularly in scenarios where precise gas fee calculations and transaction structuring are required. 16 | 17 | ## Import 18 | 19 | ```ts 20 | import { serializeEip1559Transaction } from './utils/transactionSerializer.js' 21 | ``` 22 | 23 | ## Usage 24 | 25 | ```ts 26 | // Example usage in an Ethereum transaction 27 | import { serializeEip1559Transaction } from './utils/transactionSerializer.js' 28 | // Additional imports... 29 | 30 | const serializedTx = serializeEip1559Transaction({ 31 | // Transaction parameters... 32 | }) 33 | // Use serializedTx for further processing... 34 | ``` 35 | 36 | ## Parameters 37 | 38 | `options`: Combination of `EncodeFunctionDataParameters` and `TransactionSerializableEIP1559` (excluding `data`). 39 | 40 | **Type:** Object 41 | **Details:** Contains all the necessary parameters to encode function data and serialize the transaction. 42 | 43 | ## Returns 44 | 45 | `TransactionSerializedEIP1559` 46 | 47 | **Type:** Object 48 | **Description:** The serialized transaction data, formatted according to EIP-1559 standards. 49 | -------------------------------------------------------------------------------- /src/utils/getL2HashFromL1DepositInfo.bench.ts: -------------------------------------------------------------------------------- 1 | import { optimismPortalABI } from '@eth-optimism/contracts-ts' 2 | import { DepositTx } from '@eth-optimism/core-utils' 3 | import { ethers } from 'ethers' 4 | import { getTransactionReceipt } from 'viem/actions' 5 | import { bench, describe } from 'vitest' 6 | import { ethersProvider } from '../_test/bench.js' 7 | import { publicClient } from '../_test/utils.js' 8 | import { baseAddresses } from '../chains/index.js' 9 | import { getL2HashFromL1DepositInfo } from './getL2HashFromL1DepositInfo.js' 10 | import { getTransactionDepositedEvents } from './getTransactionDepositedEvents.js' 11 | 12 | describe('Obtain L2 hash from already fetched event', async () => { 13 | const txReceipt = await getTransactionReceipt(publicClient, { 14 | hash: '0xe94031c3174788c3fee7216465c50bb2b72e7a1963f5af807b3768da10827f5c', 15 | }) 16 | const events = getTransactionDepositedEvents({ txReceipt }) 17 | bench('op-viem: `getL2HashFromL1DepositInfo`', async () => { 18 | getL2HashFromL1DepositInfo({ 19 | event: events[0].event, 20 | logIndex: events[0].logIndex, 21 | blockHash: txReceipt.blockHash, 22 | }) 23 | }) 24 | 25 | const contract = new ethers.Contract( 26 | baseAddresses.portal.address, 27 | optimismPortalABI, 28 | ethersProvider, 29 | ) 30 | const filter = contract.filters.TransactionDeposited( 31 | '0xbc3ed6B537f2980e66f396Fe14210A56ba3f72C4', 32 | '0xbc3ed6B537f2980e66f396Fe14210A56ba3f72C4', 33 | ) 34 | const ethersEvents = await contract.queryFilter(filter, 17809754, 17809754) 35 | bench('@eth-optimism/core-utils: `DepositTx.fromL1Event`', async () => { 36 | DepositTx.fromL1Event(ethersEvents[0]).hash() 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/actions/public/L1/getSecondsToFinalizable.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test, vi } from 'vitest' 2 | import { publicClient } from '../../../_test/utils.js' 3 | import { baseAddresses } from '../../../chains/base.js' 4 | import { getSecondsToFinalizable } from './getSecondsToFinalizable.js' 5 | 6 | test('returns 0 if seconds would be negative', async () => { 7 | const seconds = await getSecondsToFinalizable(publicClient, { 8 | ...baseAddresses, 9 | withdrawalHash: '0xEC0AD491512F4EDC603C2DD7B9371A0B18D4889A23E74692101BA4C6DC9B5709', 10 | }) 11 | 12 | expect(seconds).toEqual(0n) 13 | }) 14 | 15 | test('returns correctly seconds', async () => { 16 | // transaction hash of the prove withdrawal tranasction 17 | const t = await publicClient.getTransaction({ 18 | hash: '0x3e1870a473f62448ea26eafd700052697f13f278a490cc11512b82fd70937f6d', 19 | }) 20 | const block = await publicClient.getBlock({ blockNumber: t.blockNumber }) 21 | const date = new Date(parseInt(block.timestamp.toString()) * 1000) 22 | vi.setSystemTime(date) 23 | const seconds = await getSecondsToFinalizable(publicClient, { 24 | ...baseAddresses, 25 | withdrawalHash: '0xEC0AD491512F4EDC603C2DD7B9371A0B18D4889A23E74692101BA4C6DC9B5709', 26 | }) 27 | 28 | // seven days 29 | expect(seconds).toEqual(604800n) 30 | }) 31 | 32 | test('raises error if cannot find', async () => { 33 | expect(() => 34 | getSecondsToFinalizable(publicClient, { 35 | ...baseAddresses, 36 | withdrawalHash: '0xEC0AD491512F5EDC603C2DD7B9371A0B18D4889A23E74692101BA4C6DC9B5709', 37 | }) 38 | ).rejects.toThrowError( 39 | 'Withdrawal with hash 0xEC0AD491512F5EDC603C2DD7B9371A0B18D4889A23E74692101BA4C6DC9B5709 is not proven', 40 | ) 41 | }) 42 | -------------------------------------------------------------------------------- /site/docs/actions/public/L2/estimateL1Fee.md: -------------------------------------------------------------------------------- 1 | # estimateL1Fee 2 | 3 | Computes the L1 portion of the fee based on the size of the RLP-encoded input transaction, the current L1 base fee, and the various dynamic parameters. 4 | 5 | ```ts 6 | const L1FeeValue = await estimateL1Fee(publicClient, { 7 | abi, 8 | functionName: balanceOf, 9 | args: [address], 10 | }) 11 | ``` 12 | 13 | The L1 portion of the fee depends primarily on the _length_ of the transaction data and the current gas price on L1. The [Gas Price Oracle](https://docs.optimism.io/builders/tools/oracles#gas-oracle) is called to provide the L1 gas price and calculate the total L1 fee. 14 | 15 | See also: [Transaction Fees on OP Mainnet](https://docs.optimism.io/stack/transactions/transaction-fees) 16 | 17 | ## Return Value 18 | 19 | `bigint` 20 | 21 | The fee in units of wei. 22 | 23 | ## Parameters 24 | 25 | ### client 26 | 27 | - **Type:** `PublicClient` 28 | 29 | A client for the desired OP Stack chain. 30 | 31 | ### options 32 | 33 | - **abi:** `Abi` 34 | 35 | The ABI for the contract containing the function being estimated. 36 | 37 | - **functionName:** `string` 38 | 39 | The name of the function being estimated. 40 | 41 | - **args:** `any[]` 42 | 43 | The arguments to the function being estimated. 44 | 45 | - **to (optional):** `Address` 46 | 47 | Transaction recipient. 48 | 49 | - **value (optional):** `bigint` 50 | 51 | Value (in wei) sent with this transaction. 52 | 53 | - **blockNumber (optional)**: `number` 54 | 55 | The block number to perform the gas estimate against. 56 | 57 | - **blockTag (optional):** `'latest' | 'earliest' | 'pending' | 'safe' | 'finalized'` 58 | 59 | **Default:** `'latest'` 60 | 61 | The block tag to perform the gas estimate against. 62 | -------------------------------------------------------------------------------- /src/actions/wallet/L2/writeWithdrawETH.test.ts: -------------------------------------------------------------------------------- 1 | import { l2StandardBridgeABI } from '@eth-optimism/contracts-ts' 2 | import { decodeEventLog } from 'viem' 3 | import { mine } from 'viem/actions' 4 | import { expect, test } from 'vitest' 5 | import { accounts } from '../../../_test/constants.js' 6 | import { rollupPublicClient, rollupTestClient, rollupWalletClient } from '../../../_test/utils.js' 7 | import { opStackL2ChainContracts } from '../../../types/opStackContracts.js' 8 | import { OVM_ETH } from '../../../types/withdrawTo.js' 9 | import { writeWithdrawETH } from './writeWithdrawETH.js' 10 | 11 | test('successfuly submits transaction', async () => { 12 | const amount = 100n 13 | const account = accounts[0].address 14 | const hash = await writeWithdrawETH(rollupWalletClient, { 15 | args: { to: account, amount, minGasLimit: 100000 }, 16 | account: account, 17 | }) 18 | await mine(rollupTestClient, { blocks: 1 }) 19 | const receipt = await rollupPublicClient.getTransactionReceipt({ hash }) 20 | expect(receipt.status).toEqual('success') 21 | expect(receipt.to).toEqual(opStackL2ChainContracts.l2StandardBridge.address.toLowerCase()) 22 | const withdawalLogs: any[] = [] 23 | for (const l of receipt.logs) { 24 | try { 25 | const event = decodeEventLog({ abi: l2StandardBridgeABI, data: l.data, topics: l.topics }) 26 | if (event.eventName === 'WithdrawalInitiated') { 27 | withdawalLogs.push(event) 28 | } 29 | } catch {} 30 | } 31 | 32 | expect(withdawalLogs[0].args.l2Token).toEqual(OVM_ETH) 33 | expect(withdawalLogs[0].args.to.toLowerCase()).toEqual(account) 34 | expect(withdawalLogs[0].args.from.toLowerCase()).toEqual(account) 35 | expect(withdawalLogs[0].args.amount).toEqual(amount) 36 | }) 37 | -------------------------------------------------------------------------------- /site/docs/actions/public/L1/getSecondsToFinalizable.md: -------------------------------------------------------------------------------- 1 | # getSecondsToFinalizable 2 | 3 | Returns the number of seconds until a withdrawal is finalized for a given withdrawal hash. Will return 0 if seconds would be negative. 4 | 5 | ```ts [example.ts] 6 | import { publicL1Actions } from 'op-viem' 7 | import { baseAddresses } from 'op-viem/chains' 8 | import { createPublicClient } from 'viem' 9 | 10 | const publicClient = createPublicClient({ 11 | account, 12 | chain: mainnet, 13 | transport: http(), 14 | }).extend(publicL1Actions) 15 | 16 | const seconds = await getSecondsToFinalizable(publicClient, { 17 | portal: baseAddresses.portal, 18 | l2OutputOracle: baseAddresses.l2OutputOracle, 19 | withdrawalHash: 20 | '0xEC0AD491512F4EDC603C2DD7B9371A0B18D4889A23E74692101BA4C6DC9B5709', 21 | }) 22 | 23 | // or more simply 24 | const seconds = await getSecondsToFinalizable(publicClient, { 25 | ...baseAddresses, 26 | withdrawalHash: 27 | '0xEC0AD491512F4EDC603C2DD7B9371A0B18D4889A23E74692101BA4C6DC9B5709', 28 | }) 29 | ``` 30 | 31 | ## Return Value 32 | 33 | Returns a `number` representative of the seconds until withdrawal finalization. 34 | 35 | ## Parameters 36 | 37 | ### portal 38 | 39 | - **Type:** [`RawOrContractAddress`](https://opviem.sh/docs/glossary/types.html#raworcontractaddress) 40 | 41 | The address of the `OptimismPortal` contract where the `readProvenWithdrawals` call will be made. 42 | 43 | ### l2OutputOracle 44 | 45 | - **Type:** [`RawOrContractAddress`](https://opviem.sh/docs/glossary/types.html#raworcontractaddress) 46 | 47 | The address of the L2OutputOracle contract where the `FINALIZATION_PERIOD_SECONDS` call will be made. 48 | 49 | ### withdrawalHash 50 | 51 | - **Type:** `Hex` 52 | 53 | The hash of the withdrawal 54 | -------------------------------------------------------------------------------- /src/actions/public/L2/estimateL1GasUsed.ts: -------------------------------------------------------------------------------- 1 | import { gasPriceOracleABI } from '@eth-optimism/contracts-ts' 2 | import { type Abi, type PublicClient, type Transport } from 'viem' 3 | import { readContract } from 'viem/actions' 4 | import { type Chain } from 'viem/chains' 5 | import { opStackL2ChainContracts } from '../../../index.js' 6 | import type { OracleTransactionParameters } from '../../../types/gasPriceOracle.js' 7 | import { serializeEip1559Transaction } from '../../../utils/transactionSerializer.js' 8 | 9 | export type EstimateL1GasUsedParameters< 10 | TAbi extends Abi, 11 | TFunctionName extends string | undefined, 12 | > = OracleTransactionParameters 13 | 14 | /** 15 | * Options for specifying the transaction being estimated 16 | */ 17 | export type GasPriceOracleEstimator = < 18 | TChain extends Chain | undefined, 19 | TAbi extends Abi | readonly unknown[], 20 | TFunctionName extends string | undefined = undefined, 21 | >( 22 | client: PublicClient, 23 | options: OracleTransactionParameters, 24 | ) => Promise 25 | 26 | /** 27 | * Returns the L1 gas used 28 | * @example 29 | * const L1GasUsedValue = await estimateL1GasUsed(data, { 30 | * abi, 31 | * functionName: balanceOf, 32 | * args: [address], 33 | * }); 34 | */ 35 | export const estimateL1GasUsed: GasPriceOracleEstimator = async ( 36 | client, 37 | options, 38 | ) => { 39 | const data = serializeEip1559Transaction(options) 40 | return readContract(client, { 41 | address: opStackL2ChainContracts.gasPriceOracle.address, 42 | abi: gasPriceOracleABI, 43 | blockNumber: options.blockNumber, 44 | blockTag: options.blockTag, 45 | functionName: 'getL1GasUsed', 46 | args: [data], 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /src/actions/public/L2/getWithdrawalMessages.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | import { rollupPublicClient } from '../../../_test/utils.js' 3 | import { getWithdrawalMessages } from './getWithdrawalMessages.js' 4 | 5 | test('correctly retrieves messages from hash', async () => { 6 | const messages = await getWithdrawalMessages(rollupPublicClient, { 7 | hash: '0x999bab960dbdf600c51371ae819957063337a50cec2eb8032412739defadabe7', 8 | }) 9 | expect(messages.blockNumber).toEqual(2725977n) 10 | expect(messages.messages.length).toEqual(1) 11 | expect(messages.messages[0].nonce).toBeDefined() 12 | expect(messages.messages[0].gasLimit).toBeDefined() 13 | expect(messages.messages[0].data).toBeDefined() 14 | expect(messages.messages[0].value).toBeDefined() 15 | expect(messages.messages[0].sender).toBeDefined() 16 | expect(messages.messages[0].target).toBeDefined() 17 | expect(messages.messages[0].withdrawalHash).toBeDefined() 18 | }) 19 | 20 | test('correctly retrieves messages from receipt', async () => { 21 | const txReceipt = await rollupPublicClient.getTransactionReceipt({ 22 | hash: '0x999bab960dbdf600c51371ae819957063337a50cec2eb8032412739defadabe7', 23 | }) 24 | const messages = await getWithdrawalMessages(rollupPublicClient, { 25 | txReceipt, 26 | }) 27 | expect(messages.blockNumber).toEqual(2725977n) 28 | expect(messages.messages.length).toEqual(1) 29 | expect(messages.messages[0].nonce).toBeDefined() 30 | expect(messages.messages[0].gasLimit).toBeDefined() 31 | expect(messages.messages[0].data).toBeDefined() 32 | expect(messages.messages[0].value).toBeDefined() 33 | expect(messages.messages[0].sender).toBeDefined() 34 | expect(messages.messages[0].target).toBeDefined() 35 | expect(messages.messages[0].withdrawalHash).toBeDefined() 36 | }) 37 | -------------------------------------------------------------------------------- /site/docs/actions/public/L1/simulateFinalizeWithdrawalTransaction.md: -------------------------------------------------------------------------------- 1 | # simulateFinalizeWithdrawalTransaction 2 | 3 | Simulates a call to finalizeWithdrawalTranasction on the OptimismPortal contract. 4 | 5 | ```ts [example.ts] 6 | import { publicL1Actions } from 'op-viem' 7 | import { baseAddresses } from 'op-viem/chains' 8 | import { createPublicClient } from 'viem' 9 | 10 | const publicClient = createPublicClient({ 11 | account, 12 | chain: mainnet, 13 | transport: http(), 14 | }).extend(publicL1Actions) 15 | 16 | const withdrawal: FinalizeWithdrawalTransactionParameters = { 17 | nonce: 18 | 1766847064778384329583297500742918515827483896875618958121606201292641795n, 19 | sender: '0x02f086dBC384d69b3041BC738F0a8af5e49dA181', 20 | target: '0x02f086dBC384d69b3041BC738F0a8af5e49dA181', 21 | value: 335000000000000000000n, 22 | gasLimit: 100000n, 23 | data: '0x01', 24 | } 25 | 26 | const { request } = await publicClient.simulateFinalizeWithdrawalTransaction({ 27 | ...baseAddresses, 28 | withdrawal, 29 | account: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045', 30 | }) 31 | ``` 32 | 33 | ## Return Value 34 | 35 | Returns a `request` that can be passed to Viem's `writeContract` and a `result` indicating whether the simulation succeeded. 36 | 37 | ## Parameters 38 | 39 | ### withdrawal 40 | 41 | ```ts 42 | type FinalizeWithdrawalTransactionParameters = { 43 | nonce: bigint 44 | sender: `0x${string}` 45 | target: `0x${string}` 46 | value: bigint 47 | gasLimit: bigint 48 | data: `0x${string}` 49 | } 50 | ``` 51 | 52 | ### portal 53 | 54 | - **Type:** [`RawOrContractAddress`](https://opviem.sh/docs/glossary/types.html#raworcontractaddress) 55 | 56 | The `OptimismPortal` contract. 57 | 58 | ### account 59 | 60 | - **Type:** `Address` 61 | 62 | The address of the account to use. 63 | -------------------------------------------------------------------------------- /site/index.md: -------------------------------------------------------------------------------- 1 | # OP Viem 2 | 3 | ## Overview 4 | 5 | op-viem is a TypeScript extension for [Viem](https://viem.sh) that provides actions for working with [OP Stack](https://stack.optimism.io/) L2 chains such as [Optimism](https://community.optimism.io/docs/useful-tools/networks/) and [Base](https://docs.base.org/). 6 | 7 | ## Features 8 | 9 | - Simplifies cross L1 & L2 interactions 10 | - Seamless extension to [Viem](https://github.com/wagmi-dev/viem) 11 | - TypeScript ready 12 | - Test suite running against [forked](https://ethereum.org/en/glossary/#fork) Ethereum network 13 | 14 | ## Installation 15 | 16 | ::: code-group 17 | 18 | ```bash [npm] 19 | npm i op-viem 20 | ``` 21 | 22 | ```bash [pnpm] 23 | pnpm i op-viem 24 | ``` 25 | 26 | ```bash [bun] 27 | bun i op-viem 28 | ``` 29 | 30 | ::: 31 | 32 | ## Example 33 | 34 | ```ts 35 | // import modules 36 | import { createWalletClient, createPublicClient, custom, http } from 'viem' 37 | import { privateKeyToAccount } from 'viem/accounts' 38 | import { mainnet, base } from 'viem/chains' 39 | import { baseAddresses } from 'op-viem/chains' 40 | import { walletL1OpStackActions, publicL1OpStackActions, publicL2OpStackActions } from 'op-viem' 41 | 42 | // create clients 43 | export const opStackL1WalletClient = createWalletClient({ 44 | chain: mainnet, 45 | transport: custom(window.ethereum) 46 | }).extend(walletL1OpStackActions) 47 | 48 | export const opStackL1PublicClient = createPublicClient({ 49 | chain: mainnet, 50 | transport: http() 51 | }).extend(publicL1OpStackActions) 52 | 53 | export const opStackL2PublicClient = createPublicClient({ 54 | chain: base, 55 | transport: http() 56 | }).extend(publicL2OpStackActions) 57 | 58 | // perform an action 59 | opStackL1PublicClient.getOutputForL2Block(blockNumber: 2725977n, ...baseAddresses) 60 | ``` 61 | -------------------------------------------------------------------------------- /src/utils/getArgsFromTransactionDepositedOpaqueData.ts: -------------------------------------------------------------------------------- 1 | import { type Hex, size, slice } from 'viem' 2 | import type { TransactionDepositedEvent } from '../index.js' 3 | 4 | export type ParsedTransactionDepositedOpaqueData = { 5 | // TODO(Wilson): consider using mint in writeDepositContract is it is more 6 | // clear that this is the value that will be minted on L2 7 | // this type could then maybe be called DepositTransactionArgs, though it would have this 8 | // additional mint field, when compared to the contract 9 | mint: Hex 10 | value: Hex 11 | gas: Hex 12 | isCreation: boolean 13 | data: Hex 14 | } 15 | 16 | /** 17 | * @description Returns the TransactionDeposited event and log index, if found, 18 | * from the transaction receipt 19 | * 20 | * @param {opaqueData} opaqueData from the TransactionDepositedEvent event args 21 | * @returns {ParsedTransactionDepositedOpaqueData} The data parsed into five fields 22 | */ 23 | export function parseOpaqueData( 24 | opaqueData: TransactionDepositedEvent['args']['opaqueData'], 25 | ): ParsedTransactionDepositedOpaqueData { 26 | let offset = 0 27 | const mint = slice(opaqueData, offset, offset + 32) 28 | offset += 32 29 | const value = slice(opaqueData, offset, offset + 32) 30 | offset += 32 31 | const gas = slice(opaqueData, offset, offset + 8) 32 | offset += 8 33 | const isCreation = BigInt(opaqueData[offset]) === 1n 34 | offset += 1 35 | const data = 36 | // NOTE(Wilson): this is to deal with kind of odd behvior in slice 37 | // https://github.com/wagmi-dev/viem/blob/main/src/utils/data/slice.ts#L34 38 | offset > size(opaqueData) - 1 39 | ? '0x' 40 | : slice(opaqueData, offset, opaqueData.length) 41 | return { 42 | mint, 43 | value, 44 | gas, 45 | isCreation, 46 | data, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/actions/public/L2/estimateL1Fee.ts: -------------------------------------------------------------------------------- 1 | import { gasPriceOracleABI, gasPriceOracleAddress } from '@eth-optimism/contracts-ts' 2 | import { type Abi, type PublicClient, type Transport } from 'viem' 3 | import { readContract } from 'viem/actions' 4 | import { type Chain } from 'viem/chains' 5 | import type { OracleTransactionParameters } from '../../../types/gasPriceOracle.js' 6 | import { serializeEip1559Transaction } from '../../../utils/transactionSerializer.js' 7 | 8 | export type EstimateL1FeeParameters< 9 | TAbi extends Abi, 10 | TFunctionName extends string | undefined, 11 | > = OracleTransactionParameters 12 | 13 | /** 14 | * Options for specifying the transaction being estimated 15 | */ 16 | export type GasPriceOracleEstimator = < 17 | TChain extends Chain | undefined, 18 | TAbi extends Abi | readonly unknown[], 19 | TFunctionName extends string | undefined = undefined, 20 | >( 21 | client: PublicClient, 22 | options: OracleTransactionParameters, 23 | ) => Promise 24 | 25 | /** 26 | * Computes the L1 portion of the fee based on the size of the rlp encoded input 27 | * transaction, the current L1 base fee, and the various dynamic parameters. 28 | * @example 29 | * const L1FeeValue = await estimateL1Fee(publicClient, { 30 | * abi, 31 | * functionName: balanceOf, 32 | * args: [address], 33 | * }); 34 | */ 35 | export const estimateL1Fee: GasPriceOracleEstimator = async ( 36 | client, 37 | options, 38 | ) => { 39 | const data = serializeEip1559Transaction(options) 40 | return readContract(client, { 41 | address: gasPriceOracleAddress['420'], 42 | abi: gasPriceOracleABI, 43 | blockNumber: options.blockNumber, 44 | blockTag: options.blockTag, 45 | args: [data], 46 | functionName: 'getL1Fee', 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /src/actions/public/L2/getProveWithdrawalTransactionArgs.test.ts: -------------------------------------------------------------------------------- 1 | import { createPublicClient, http } from 'viem' 2 | import { base } from 'viem/chains' 3 | import { expect, test } from 'vitest' 4 | import { accounts } from '../../../_test/constants.js' 5 | import { publicClient, walletClient } from '../../../_test/utils.js' 6 | import { baseAddresses } from '../../../chains/index.js' 7 | import { writeProveWithdrawalTransaction } from '../../index.js' 8 | import { getLatestProposedL2BlockNumber } from '../L1/getLatestProposedL2BlockNumber.js' 9 | import { getOutputForL2Block } from '../L1/getOutputForL2Block.js' 10 | import { getProveWithdrawalTransactionArgs } from './getProveWithdrawalTransactionArgs.js' 11 | import { getWithdrawalMessages } from './getWithdrawalMessages.js' 12 | 13 | test('correctly generates args', async () => { 14 | // cannot currently use anvil rollupPublicClient for this as eth_getProof isn't working 15 | const client = createPublicClient({ 16 | chain: base, 17 | transport: http(), 18 | }) 19 | 20 | const withdrawalMessages = await getWithdrawalMessages(client, { 21 | hash: '0xd0eb2a59f3cc4c61b01c350e71e1804ad6bd776dc9abc1bdb5e2e40695ab2628', 22 | }) 23 | 24 | const { l2BlockNumber } = await getLatestProposedL2BlockNumber(publicClient, { 25 | ...baseAddresses, 26 | }) 27 | 28 | const output = await getOutputForL2Block(publicClient, { 29 | l2BlockNumber, 30 | ...baseAddresses, 31 | }) 32 | 33 | // TODO(wilson): We should simplify these test to not require so much setup ^ 34 | const args = await getProveWithdrawalTransactionArgs(client, { 35 | message: withdrawalMessages.messages[0], 36 | output: output, 37 | }) 38 | 39 | const hash = await writeProveWithdrawalTransaction(walletClient, { 40 | args, 41 | ...baseAddresses, 42 | account: accounts[0].address, 43 | }) 44 | expect(hash).toBeDefined() 45 | }) 46 | -------------------------------------------------------------------------------- /src/actions/public/L1/getOutputForL2Block.ts: -------------------------------------------------------------------------------- 1 | import { l2OutputOracleABI } from '@eth-optimism/contracts-ts' 2 | import type { Chain, Hex, PublicClient, Transport } from 'viem' 3 | import { readContract } from 'viem/actions' 4 | import { type RawOrContractAddress, resolveAddress } from '../../../types/addresses.js' 5 | 6 | export type Proposal = { 7 | outputRoot: Hex 8 | timestamp: bigint 9 | l2BlockNumber: bigint 10 | } 11 | 12 | export type GetOutputForL2BlockParameters< 13 | TChain extends Chain | undefined = Chain | undefined, 14 | _chainId = TChain extends Chain ? TChain['id'] : number, 15 | > = { l2BlockNumber: bigint; l2OutputOracle: RawOrContractAddress<_chainId> } 16 | 17 | export type GetOutputForL2BlockReturnType = { 18 | proposal: Proposal 19 | outputIndex: bigint 20 | } 21 | 22 | /** 23 | * Calls to the L2OutputOracle contract on L1 to get the output for a given L2 block 24 | * 25 | * @param {bigint} blockNumber the L2 block number to get the output for 26 | * @param {OpChainL2} rollup the L2 chain 27 | * @returns {GetOutputForL2BlockReturnType} Output proposal and index for the L2 block 28 | */ 29 | export async function getOutputForL2Block( 30 | client: PublicClient, 31 | { 32 | l2BlockNumber, 33 | l2OutputOracle, 34 | }: GetOutputForL2BlockParameters, 35 | ): Promise { 36 | const resolvedAddress = resolveAddress(l2OutputOracle) 37 | const outputIndex = await readContract(client, { 38 | address: resolvedAddress, 39 | abi: l2OutputOracleABI, 40 | functionName: 'getL2OutputIndexAfter', 41 | args: [l2BlockNumber], 42 | }) 43 | 44 | const proposal = await readContract(client, { 45 | address: resolvedAddress, 46 | abi: l2OutputOracleABI, 47 | functionName: 'getL2Output', 48 | args: [outputIndex], 49 | }) 50 | 51 | return { proposal, outputIndex } 52 | } 53 | -------------------------------------------------------------------------------- /src/utils/getL2HashFromL1DepositInfo.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | import { type TransactionDepositedEvent } from '../types/depositTransaction.js' 3 | import { getL2HashFromL1DepositInfo } from './getL2HashFromL1DepositInfo.js' 4 | 5 | const event: TransactionDepositedEvent = { 6 | eventName: 'TransactionDeposited', 7 | args: { 8 | from: '0xbc3ed6B537f2980e66f396Fe14210A56ba3f72C4', 9 | to: '0xbc3ed6B537f2980e66f396Fe14210A56ba3f72C4', 10 | version: 0n, 11 | opaqueData: 12 | '0x0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000001000000000000526c0000', 13 | }, 14 | } 15 | 16 | const eventWithZeroData: TransactionDepositedEvent = { 17 | eventName: 'TransactionDeposited', 18 | args: { 19 | from: '0x80B01fDEd19145FFB893123eC38eBba31b4043Ee', 20 | to: '0x80B01fDEd19145FFB893123eC38eBba31b4043Ee', 21 | version: 0n, 22 | opaqueData: 23 | '0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000520800', 24 | }, 25 | } 26 | 27 | test('returns correct hash', () => { 28 | const logIndex = 196 29 | const blockHash = '0x9ba3933dc6ce43c145349770a39c30f9b647f17668f004bd2e05c80a2e7262f7' 30 | const hash = getL2HashFromL1DepositInfo({ event, logIndex, blockHash }) 31 | 32 | expect(hash).toEqual( 33 | '0xe67200042bf79eef76850dd3986bdd544e7aceeb7bbf8449158088bdc582168a', 34 | ) 35 | }) 36 | 37 | test('returns correct hash with zero data', () => { 38 | const event = eventWithZeroData 39 | const logIndex = 36 40 | const blockHash = '0x9375ba075993fcc3cd3f66ef1fc45687aeccc04edfc06da2bc7cdb8984046ed7' 41 | const hash = getL2HashFromL1DepositInfo({ event, logIndex, blockHash }) 42 | 43 | expect(hash).toEqual( 44 | '0xb81d4b3fe43986c51d29bf29a8c68c9a301c074531d585298bc1e03df68c8459', 45 | ) 46 | }) 47 | -------------------------------------------------------------------------------- /site/docs/actions/wallet/L1/writeDepositERC20.md: -------------------------------------------------------------------------------- 1 | # writeDepositERC20 2 | 3 | Writes a deposit of ERC20 tokens from L1 to L2. 4 | 5 | ```ts [example.ts] 6 | import { walletL1OpStackActions } from 'op-viem' 7 | import { baseAddresses } from 'op-viem/chains' 8 | import { createWalletClient } from 'viem' 9 | 10 | const USDCL1 = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' 11 | const USDCL2 = '0x2e668bb88287675e34c8df82686dfd0b7f0c0383' 12 | 13 | const walletClient = createWalletClient({ 14 | chain: mainnet, 15 | transport: http(), 16 | }).extend(walletL1OpStackActions) 17 | 18 | const { request } = await walletClient.writeDepositERC20({ 19 | args: { 20 | l1Token: USDCL1, 21 | l2Token: USDCL2, 22 | to: '0xFd4F24676eD4588928213F37B126B53c07186F45', 23 | amount: 1n, 24 | minGasLimit: 100000, 25 | }, 26 | ...baseAddresses, 27 | account: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045', 28 | }) 29 | ``` 30 | 31 | ## Return Value 32 | 33 | Returns a transaction hash of the deposit transaction. 34 | 35 | ## Parameters 36 | 37 | ### args 38 | 39 | #### l1Token 40 | 41 | - **Type:** `Address` 42 | 43 | The L1 token contract address. 44 | 45 | #### l2Token 46 | 47 | - **Type:** `Address` 48 | 49 | The L2 token contract address. 50 | 51 | #### to 52 | 53 | - **Type:** `Address` 54 | 55 | The address to deposit the tokens to. 56 | 57 | #### amount 58 | 59 | - **Type:** `bigint` 60 | 61 | The amount of tokens to deposit. 62 | 63 | #### gasLimit 64 | 65 | - **Type:** `bigint` 66 | 67 | The gas limit for the transaction. 68 | 69 | #### extraData (optional) 70 | 71 | - **Type:** `data` 72 | 73 | Extra data to include in the transaction. 74 | 75 | ### l1StandardBridge 76 | 77 | - **Type:** [`RawOrContractAddress`](https://opviem.sh/docs/glossary/types.html#raworcontractaddress) 78 | 79 | The `L1StandardBridge` contract. 80 | 81 | ### account 82 | 83 | - **Type:** `Address` 84 | 85 | The address to deposit the tokens from. 86 | -------------------------------------------------------------------------------- /src/utils/getTransactionDepositedEvents.ts: -------------------------------------------------------------------------------- 1 | import { optimismPortalABI } from '@eth-optimism/contracts-ts' 2 | import { decodeEventLog, type TransactionReceipt } from 'viem' 3 | import { type TransactionDepositedEvent } from '../types/depositTransaction.js' 4 | 5 | export type TransactionDepositedEventDetails = { 6 | event: TransactionDepositedEvent 7 | logIndex: number 8 | } 9 | 10 | export type GetTransactionDepositedEventsParams = { 11 | txReceipt: TransactionReceipt 12 | } 13 | 14 | export type GetTransactionDepositedEventsReturnType = TransactionDepositedEventDetails[] 15 | 16 | /** 17 | * @description Returns the TransactionDeposited event and log index, if found, 18 | * from the transaction receipt 19 | * 20 | * @param {TransactionReceipt} receipt the receipt of the transaction supposedly containing the TransactionDeposited event 21 | * @returns {GetDepositEventInfoFromTxReceiptParams} An array of L2 transaction hashes, corresponding to all TransactionDeposited events found in the transaction 22 | */ 23 | export function getTransactionDepositedEvents({ 24 | txReceipt, 25 | }: GetTransactionDepositedEventsParams): GetTransactionDepositedEventsReturnType { 26 | const depositEvents: { 27 | event: TransactionDepositedEvent 28 | logIndex: number 29 | }[] = [] 30 | for (const l of txReceipt.logs) { 31 | try { 32 | const event = decodeEventLog({ 33 | abi: optimismPortalABI, 34 | data: l.data, 35 | topics: l.topics, 36 | }) 37 | if (event.eventName === 'TransactionDeposited') { 38 | if (!l.logIndex) { 39 | throw new Error('Found TransactionDeposited but logIndex undefined') 40 | } 41 | depositEvents.push({ event, logIndex: l.logIndex }) 42 | } 43 | // The transaction may have events from many contracts 44 | // we can ignore errors decoding transactions we do not care about. 45 | } catch {} 46 | } 47 | return depositEvents 48 | } 49 | -------------------------------------------------------------------------------- /src/actions/public/L2/getWithdrawalMessages.ts: -------------------------------------------------------------------------------- 1 | import { l2ToL1MessagePasserABI } from '@eth-optimism/contracts-ts' 2 | import { type Chain, decodeEventLog, type Hash, type PublicClient, type TransactionReceipt, type Transport } from 'viem' 3 | import { getTransactionReceipt } from 'viem/actions' 4 | import type { MessagePassedEvent } from '../../../types/withdrawal.js' 5 | 6 | export type GetWithdrawalMessagesParameters = { 7 | hash: Hash 8 | txReceipt?: never 9 | } | { hash?: never; txReceipt: TransactionReceipt } 10 | 11 | export type GetWithdrawalMessagesReturnType = { 12 | messages: MessagePassedEvent[] 13 | blockNumber: bigint 14 | } 15 | 16 | /** 17 | * Retrieves all MessagePassed events from a withdrawal transaction 18 | * 19 | * @param client - Public client to use 20 | * @param {GetWithdrawalMessagesParameters} parameters - {@link GetWithdrawalMessagesParameters} 21 | * @returns {GetWithdrawalMessagesReturnType} An array of all MessagePassed events emitted in this transaction. {@link GetWithdrawalMessagesReturnType} 22 | */ 23 | export async function getWithdrawalMessages( 24 | client: PublicClient, 25 | { hash, txReceipt }: GetWithdrawalMessagesParameters, 26 | ): Promise { 27 | const receipt = txReceipt ?? await getTransactionReceipt(client, { hash }) 28 | const messages: MessagePassedEvent[] = [] 29 | for (const log of receipt.logs) { 30 | /// These transactions will contain events from several contracts 31 | /// this decode will revert for events not from l2ToL1MessagePasserABI 32 | /// we are OK ignoring these events 33 | try { 34 | const event = decodeEventLog({ 35 | abi: l2ToL1MessagePasserABI, 36 | data: log.data, 37 | topics: log.topics, 38 | }) 39 | if (event.eventName === 'MessagePassed') { 40 | messages.push(event.args) 41 | } 42 | } catch {} 43 | } 44 | return { messages, blockNumber: receipt.blockNumber } 45 | } 46 | -------------------------------------------------------------------------------- /src/actions/public/L1/simulateDepositERC20.test.ts: -------------------------------------------------------------------------------- 1 | import { readContract, writeContract } from 'viem/actions' 2 | import { expect, test } from 'vitest' 3 | import { erc20ABI } from 'wagmi' 4 | import { publicClient, testClient } from '../../../_test/utils.js' 5 | import { baseAddresses } from '../../../chains/index.js' 6 | import { simulateDepositERC20 } from './simulateDepositERC20.js' 7 | 8 | const USDCL1 = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' 9 | const USDCL2 = '0x2e668bb88287675e34c8df82686dfd0b7f0c0383' 10 | const zenaddress = '0xFd4F24676eD4588928213F37B126B53c07186F45' 11 | 12 | test('default', async () => { 13 | await testClient.impersonateAccount({ 14 | address: zenaddress, 15 | }) 16 | await testClient.setBalance({ 17 | address: zenaddress, 18 | value: 10n ** 22n, 19 | }) 20 | 21 | await writeContract(testClient, { 22 | address: USDCL1, 23 | abi: erc20ABI, 24 | functionName: 'approve', 25 | args: [baseAddresses.l1StandardBridge.address, 10000n], 26 | account: zenaddress, 27 | }) 28 | const balanceBefore = await readContract(testClient, { 29 | address: USDCL1, 30 | abi: erc20ABI, 31 | functionName: 'balanceOf', 32 | args: [zenaddress], 33 | }) 34 | await testClient.mine({ blocks: 1 }) 35 | const { request } = await simulateDepositERC20(publicClient, { 36 | args: { 37 | l1Token: USDCL1, 38 | l2Token: USDCL2, 39 | to: zenaddress, 40 | amount: 1n, 41 | minGasLimit: 100000, 42 | }, 43 | ...baseAddresses, 44 | account: zenaddress, 45 | }) 46 | 47 | expect(request).toBeDefined() 48 | 49 | expect(request.args[0]).toEqual(USDCL1) 50 | expect(await writeContract(testClient, request)).toBeDefined() 51 | 52 | await testClient.mine({ blocks: 1 }) 53 | const balanceAfter = await readContract(testClient, { 54 | address: USDCL1, 55 | abi: erc20ABI, 56 | functionName: 'balanceOf', 57 | args: [zenaddress], 58 | }) 59 | 60 | expect(balanceAfter).toEqual(balanceBefore - 1n) 61 | }) 62 | -------------------------------------------------------------------------------- /site/docs/actions/public/L1/simulateDepositERC20.md: -------------------------------------------------------------------------------- 1 | # simulateDepositERC20 2 | 3 | Simulates a deposit of ERC20 tokens to L2. 4 | 5 | ```ts [example.ts] 6 | import { base, publicL1Actions } from 'op-viem' 7 | import { baseAddresses } from 'op-viem/chains' 8 | import { createPublicClient } from 'viem' 9 | 10 | const USDCL1 = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' 11 | const USDCL2 = '0x2e668bb88287675e34c8df82686dfd0b7f0c0383' 12 | 13 | const publicClient = createPublicClient({ 14 | account, 15 | chain: mainnet, 16 | transport: http(), 17 | }).extend(publicL1Actions) 18 | 19 | const { request } = await publicClient.simulateDepositERC20({ 20 | args: { 21 | l1Token: USDCL1, 22 | l2Token: USDCL2, 23 | to: '0xFd4F24676eD4588928213F37B126B53c07186F45', 24 | amount: 1n, 25 | minGasLimit: 100000, 26 | }, 27 | l1StandardBridge: baseAddresses.l1StandardBridge, 28 | account: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045', 29 | }) 30 | ``` 31 | 32 | ## Return Value 33 | 34 | Returns a `request` that can be passed to Viem's `writeContract` and a `result` indicating whether the simulation succeeded. 35 | 36 | ## Parameters 37 | 38 | ### args 39 | 40 | - #### l1Token 41 | - **Type:** `Address` 42 | - The L1 token contract address. 43 | 44 | - #### l2Token 45 | - **Type:** `Address` 46 | - The L2 token contract address. 47 | 48 | - #### to 49 | - **Type:** `Address` 50 | - The address to deposit the tokens to. 51 | 52 | - #### amount 53 | - **Type:** `bigint` 54 | - The amount of tokens to deposit. 55 | 56 | - #### minGasLimit 57 | - **Type:** `number` 58 | - The gas limit for the transaction. 59 | 60 | - #### extraData (optional) 61 | - **Type:** `Hex` 62 | - Extra data to include in the transaction. 63 | 64 | ### l1StandardBridge 65 | 66 | - **Type:** [`RawOrContractAddress`](https://opviem.sh/docs/glossary/types.html#raworcontractaddress) 67 | 68 | The `L1StandardBridge` contract. 69 | 70 | ### account 71 | 72 | - **Type:** `Address` 73 | 74 | The address to deposit the tokens from. 75 | -------------------------------------------------------------------------------- /src/_test/globalSetup.ts: -------------------------------------------------------------------------------- 1 | // from https://github.com/wagmi-dev/viem/blob/main/src/_test/globalSetup.ts 2 | import { startProxy } from '@viem/anvil' 3 | 4 | import { 5 | blockTime, 6 | forkBlockNumber, 7 | forkUrl, 8 | rollupBlockTime, 9 | rollupForkBlockNumber, 10 | rollupForkUrl, 11 | } from './constants.js' 12 | 13 | export default async function() { 14 | if (process.env.SKIP_GLOBAL_SETUP) { 15 | return 16 | } 17 | 18 | // Using this proxy, we can parallelize our test suite by spawning multiple "on demand" anvil 19 | // instances and proxying requests to them. Especially for local development, this is much faster 20 | // than running the tests serially. 21 | // 22 | // In vitest, each thread is assigned a unique, numerical id (`process.env.VITEST_POOL_ID`). We 23 | // append this id to the local rpc url (e.g. `http://127.0.0.1:8545/`). 24 | // 25 | // Whenever a request hits the proxy server at this url, it spawns (or reuses) an anvil instance 26 | // at a randomly assigned port and proxies the request to it. The anvil instance is added to a 27 | // [id:port] mapping for future request and is kept alive until the test suite finishes. 28 | // 29 | // Since each thread processes one test file after the other, we don't have to worry about 30 | // non-deterministic behavior caused by multiple tests hitting the same anvil instance concurrently 31 | // as long as we avoid `test.concurrent()`. 32 | // 33 | // We still need to remember to reset the anvil instance between test files. This is generally 34 | // handled in `setup.ts` but may require additional resetting (e.g. via `afterAll`), in case of 35 | // any custom per-test adjustments that persist beyond `anvil_reset`. 36 | await startProxy({ 37 | port: 8555, 38 | options: { 39 | forkUrl: rollupForkUrl, 40 | forkBlockNumber: rollupForkBlockNumber, 41 | blockTime: rollupBlockTime, 42 | }, 43 | }) 44 | return await startProxy({ 45 | options: { 46 | forkUrl, 47 | forkBlockNumber, 48 | blockTime, 49 | }, 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /src/actions/wallet/L1/writeDepositETH.ts: -------------------------------------------------------------------------------- 1 | import type { Account, Chain, Transport, WalletClient, WriteContractReturnType } from 'viem' 2 | import { type RawOrContractAddress, resolveAddress } from '../../../types/addresses.js' 3 | import { type DepositETHParameters } from '../../../types/depositETH.js' 4 | import type { L1WriteActionBaseType } from '../../../types/l1Actions.js' 5 | import { writeDepositTransaction, type WriteDepositTransactionParameters } from './writeDepositTransaction.js' 6 | 7 | export type WriteDepositETHParameters< 8 | TChain extends Chain | undefined = Chain, 9 | TAccount extends Account | undefined = Account | undefined, 10 | TChainOverride extends Chain | undefined = Chain | undefined, 11 | _chainId = TChain extends Chain ? TChain['id'] : number, 12 | > = 13 | & { args: DepositETHParameters; portal: RawOrContractAddress<_chainId> } 14 | & Omit< 15 | L1WriteActionBaseType< 16 | TChain, 17 | TAccount, 18 | TChainOverride 19 | >, 20 | 'value' 21 | > 22 | 23 | /** 24 | * Deposits ETH to L2 using the OptimismPortal contract 25 | * @param parameters - {@link WriteDepositETHParameters} 26 | * @returns A [Transaction Hash](https://viem.sh/docs/glossary/terms.html#hash). {@link WriteContractReturnType} 27 | */ 28 | export async function writeDepositETH< 29 | TChain extends Chain | undefined, 30 | TAccount extends Account | undefined, 31 | TChainOverride extends Chain | undefined = undefined, 32 | >( 33 | client: WalletClient, 34 | { 35 | args: { to, gasLimit, data, amount }, 36 | portal, 37 | ...rest 38 | }: WriteDepositETHParameters< 39 | TChain, 40 | TAccount, 41 | TChainOverride 42 | >, 43 | ): Promise { 44 | return writeDepositTransaction(client, { 45 | args: { to, value: amount, gasLimit: BigInt(gasLimit), data }, 46 | portal: resolveAddress(portal), 47 | value: amount, 48 | ...rest, 49 | } as unknown as WriteDepositTransactionParameters< 50 | TChain, 51 | TAccount, 52 | TChainOverride 53 | >) 54 | } 55 | -------------------------------------------------------------------------------- /src/actions/public/L1/simulateDepositETH.ts: -------------------------------------------------------------------------------- 1 | import type { Chain, PublicClient, SimulateContractParameters, SimulateContractReturnType, Transport } from 'viem' 2 | import { simulateContract } from 'viem/actions' 3 | import { type RawOrContractAddress, resolveAddress } from '../../../types/addresses.js' 4 | import { ABI, type DepositETHParameters, FUNCTION } from '../../../types/depositETH.js' 5 | import type { L1SimulateActionBaseType } from '../../../types/l1Actions.js' 6 | 7 | export type SimulateDepositETHParameters< 8 | TChain extends Chain | undefined = Chain, 9 | TChainOverride extends Chain | undefined = Chain | undefined, 10 | _chainId = TChain extends Chain ? TChain['id'] : number, 11 | > = 12 | & { args: DepositETHParameters; portal: RawOrContractAddress<_chainId> } 13 | & Omit, 'value'> 14 | 15 | export type SimulateDepositETHReturnType< 16 | TChain extends Chain | undefined, 17 | TChainOverride extends Chain | undefined = undefined, 18 | > = SimulateContractReturnType 19 | 20 | /** 21 | * Simulates a deposit of ETH to L2 22 | * @param parameters - {@link SimulateDepositETHParameters} 23 | * @returns A [Transaction Hash](https://viem.sh/docs/glossary/terms.html#hash). {@link WriteContractReturnType} 24 | */ 25 | export async function simulateDepositETH< 26 | TChain extends Chain | undefined, 27 | TChainOverride extends Chain | undefined = undefined, 28 | >( 29 | client: PublicClient, 30 | { 31 | args: { to, gasLimit, data = '0x', amount }, 32 | portal, 33 | ...rest 34 | }: SimulateDepositETHParameters, 35 | ): Promise> { 36 | return simulateContract(client, { 37 | address: resolveAddress(portal), 38 | abi: ABI, 39 | functionName: FUNCTION, 40 | args: [to, amount, gasLimit, false, data], 41 | value: amount, 42 | ...rest, 43 | } as unknown as SimulateContractParameters) 44 | } 45 | -------------------------------------------------------------------------------- /src/actions/wallet/L2/writeWithdrawETH.ts: -------------------------------------------------------------------------------- 1 | import type { Account, Chain, Transport, WalletClient, WriteContractReturnType } from 'viem' 2 | import type { L2WriteContractParameters } from '../../../types/l2Actions.js' 3 | import { type ABI, type FUNCTION, OVM_ETH, type WithdrawETHParameters } from '../../../types/withdrawTo.js' 4 | import { writeWithdrawERC20, type WriteWithdrawERC20Parameters } from './writeWithdrawERC20.js' 5 | 6 | export type WriteWithdrawETHParameters< 7 | TChain extends Chain | undefined = Chain, 8 | TAccount extends Account | undefined = Account | undefined, 9 | TChainOverride extends Chain | undefined = Chain | undefined, 10 | > = 11 | & { args: WithdrawETHParameters } 12 | & L2WriteContractParameters 13 | 14 | /** 15 | * Withdraws ETH to an L1 address. 16 | * 17 | * @param {Address} to the address to withdraw to on L1 18 | * @param {Bigint} amount the amount of ETH to withdraw 19 | * @param {Bigint} minGasLimit the minimum gas limit for the withdrawal 20 | * @param {Hex} [extraData] the extra data for the withdrawal 21 | * 22 | * @returns {Promise} the hash of the transaction 23 | */ 24 | export async function writeWithdrawETH< 25 | TChain extends Chain | undefined = Chain, 26 | TAccount extends Account | undefined = Account | undefined, 27 | TChainOverride extends Chain | undefined = Chain | undefined, 28 | >(client: WalletClient, { 29 | args: { to, amount, minGasLimit, extraData = '0x' }, 30 | ...rest 31 | }: WriteWithdrawETHParameters< 32 | TChain, 33 | TAccount, 34 | TChainOverride 35 | >): Promise { 36 | return writeWithdrawERC20(client, { 37 | args: { l2Token: OVM_ETH, to, amount, minGasLimit, extraData }, 38 | // NOTE: msg.value must = amount or transaction will revert 39 | // https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/universal/StandardBridge.sol#L306 40 | value: amount, 41 | ...rest, 42 | } as unknown as WriteWithdrawERC20Parameters) 43 | } 44 | -------------------------------------------------------------------------------- /src/actions/wallet/L1/writeSendMessage.test.ts: -------------------------------------------------------------------------------- 1 | import { l1CrossDomainMessengerABI } from '@eth-optimism/contracts-ts' 2 | import { decodeEventLog, encodeFunctionData, type Hex } from 'viem' 3 | import { mine } from 'viem/actions' 4 | import { expect, test } from 'vitest' 5 | import { accounts } from '../../../_test/constants.js' 6 | import { publicClient, testClient, walletClient } from '../../../_test/utils.js' 7 | import { baseAddresses } from '../../../chains/index.js' 8 | import { writeSendMessage } from './writeSendMessage.js' 9 | 10 | test('can successfully call sendsMessage', async () => { 11 | const args = { 12 | target: accounts[0].address, 13 | minGasLimit: 25000, 14 | } 15 | const hash = await writeSendMessage(walletClient, { 16 | args, 17 | value: 0n, 18 | ...baseAddresses, 19 | account: accounts[0].address, 20 | }) 21 | 22 | await mine(testClient, { blocks: 1 }) 23 | 24 | const r = await publicClient.getTransactionReceipt({ hash: hash }) 25 | let messageEvent 26 | for (const l of r.logs) { 27 | try { 28 | const event = decodeEventLog({ 29 | abi: l1CrossDomainMessengerABI, 30 | data: l.data, 31 | topics: l.topics, 32 | }) 33 | if (event.eventName === 'SentMessage') { 34 | messageEvent = event 35 | } 36 | } catch {} 37 | } 38 | 39 | expect(messageEvent).toBeDefined() 40 | }) 41 | 42 | test('passes correct calldata to sendMessage', async () => { 43 | const args = { 44 | target: accounts[0].address, 45 | minGasLimit: 25000, 46 | message: '0x1234' as Hex, 47 | } 48 | const hash = await writeSendMessage(walletClient, { 49 | args, 50 | value: 0n, 51 | ...baseAddresses, 52 | account: accounts[0].address, 53 | }) 54 | 55 | await mine(testClient, { blocks: 1 }) 56 | 57 | const t = await publicClient.getTransaction({ hash: hash }) 58 | const expectedCalldata = encodeFunctionData({ 59 | abi: l1CrossDomainMessengerABI, 60 | functionName: 'sendMessage', 61 | args: [args.target, args.message, args.minGasLimit], 62 | }) 63 | expect(t.input).toEqual(expectedCalldata) 64 | }) 65 | -------------------------------------------------------------------------------- /src/actions/wallet/L1/writeDepositERC20.ts: -------------------------------------------------------------------------------- 1 | import type { Account, Chain, Transport, WalletClient, WriteContractParameters, WriteContractReturnType } from 'viem' 2 | import { writeContract } from 'viem/actions' 3 | import { type RawOrContractAddress, resolveAddress } from '../../../types/addresses.js' 4 | import { ABI, type DepositERC20Parameters, FUNCTION } from '../../../types/depositERC20.js' 5 | import type { L1WriteActionBaseType } from '../../../types/l1Actions.js' 6 | 7 | export type WriteDepositERC20Parameters< 8 | TChain extends Chain | undefined = Chain, 9 | TAccount extends Account | undefined = Account | undefined, 10 | TChainOverride extends Chain | undefined = Chain | undefined, 11 | _chainId = TChain extends Chain ? TChain['id'] : number, 12 | > = 13 | & { args: DepositERC20Parameters; l1StandardBridge: RawOrContractAddress<_chainId> } 14 | & L1WriteActionBaseType< 15 | TChain, 16 | TAccount, 17 | TChainOverride 18 | > 19 | 20 | /** 21 | * Deposits ERC20 tokens to L2 using the standard bridge 22 | * @param parameters - {@link WriteDepositERC20Parameters} 23 | * @returns A [Transaction Hash](https://viem.sh/docs/glossary/terms.html#hash). {@link WriteContractReturnType} 24 | */ 25 | export async function writeDepositERC20< 26 | TChain extends Chain | undefined, 27 | TAccount extends Account | undefined, 28 | TChainOverride extends Chain | undefined = undefined, 29 | >( 30 | client: WalletClient, 31 | { 32 | args: { l1Token, l2Token, to, amount, minGasLimit, extraData = '0x' }, 33 | l1StandardBridge, 34 | ...rest 35 | }: WriteDepositERC20Parameters< 36 | TChain, 37 | TAccount, 38 | TChainOverride 39 | >, 40 | ): Promise { 41 | return writeContract(client, { 42 | address: resolveAddress(l1StandardBridge), 43 | abi: ABI, 44 | functionName: FUNCTION, 45 | args: [l1Token, l2Token, to, amount, minGasLimit, extraData], 46 | ...rest, 47 | } as unknown as WriteContractParameters< 48 | typeof ABI, 49 | typeof FUNCTION, 50 | TChain, 51 | TAccount, 52 | TChainOverride 53 | >) 54 | } 55 | -------------------------------------------------------------------------------- /src/actions/public/L1/getL2HashesForDepositTx.test.ts: -------------------------------------------------------------------------------- 1 | import { optimismPortalABI } from '@eth-optimism/contracts-ts' 2 | import { DepositTx } from '@eth-optimism/core-utils' 3 | import { ethers } from 'ethers' 4 | import { expect, test } from 'vitest' 5 | import { ethersProvider } from '../../../_test/bench.js' 6 | import { publicClient } from '../../../_test/utils.js' 7 | import { optimismAddresses } from '../../../chains/optimism.js' 8 | import { getL2HashesForDepositTx } from './getL2HashesForDepositTx.js' 9 | 10 | test('correctly retrieves L2 hash', async () => { 11 | const hashes = await getL2HashesForDepositTx(publicClient, { 12 | l1TxHash: '0x33faeeee9c6d5e19edcdfc003f329c6652f05502ffbf3218d9093b92589a42c4', 13 | }) 14 | 15 | expect(hashes.length).toEqual(1) 16 | 17 | expect(hashes[0]).toEqual( 18 | '0xed88afbd3f126180bd5488c2212cd033c51a6f9b1765249bdb738dcac1d0cb41', 19 | ) 20 | }) 21 | 22 | test('correctly retrieves L2 hash when given receipt', async () => { 23 | const l1TxReceipt = await publicClient.getTransactionReceipt({ 24 | hash: '0x33faeeee9c6d5e19edcdfc003f329c6652f05502ffbf3218d9093b92589a42c4', 25 | }) 26 | const hashes = await getL2HashesForDepositTx(publicClient, { 27 | l1TxReceipt, 28 | }) 29 | 30 | expect(hashes.length).toEqual(1) 31 | 32 | expect(hashes[0]).toEqual( 33 | '0xed88afbd3f126180bd5488c2212cd033c51a6f9b1765249bdb738dcac1d0cb41', 34 | ) 35 | }) 36 | 37 | test('matches @eth-optimism/core-utils', async () => { 38 | const hashes = await getL2HashesForDepositTx(publicClient, { 39 | l1TxHash: '0x33faeeee9c6d5e19edcdfc003f329c6652f05502ffbf3218d9093b92589a42c4', 40 | }) 41 | 42 | const contract = new ethers.Contract( 43 | optimismAddresses.portal.address, 44 | optimismPortalABI, 45 | ethersProvider, 46 | ) 47 | const filter = contract.filters.TransactionDeposited( 48 | '0x36BDE71C97B33Cc4729cf772aE268934f7AB70B2', 49 | '0x4200000000000000000000000000000000000007', 50 | ) 51 | const events = await contract.queryFilter(filter, 18033412, 18033413) 52 | const depositTx = DepositTx.fromL1Event(events[0]) 53 | expect(depositTx.hash()).toEqual(hashes[0]) 54 | }) 55 | -------------------------------------------------------------------------------- /src/actions/wallet/L1/writeDepositETH.test.ts: -------------------------------------------------------------------------------- 1 | import { optimismPortalABI } from '@eth-optimism/contracts-ts' 2 | import { decodeEventLog, encodePacked } from 'viem' 3 | import { mine } from 'viem/actions' 4 | import { expect, test } from 'vitest' 5 | import { accounts } from '../../../_test/constants.js' 6 | import { publicClient, testClient, walletClient } from '../../../_test/utils.js' 7 | import { baseAddresses } from '../../../chains/index.js' 8 | import type { DepositETHParameters, TransactionDepositedEvent } from '../../../index.js' 9 | import { writeDepositETH } from './writeDepositETH.js' 10 | 11 | test('default', async () => { 12 | expect( 13 | await writeDepositETH(walletClient, { 14 | args: { 15 | to: accounts[0].address, 16 | gasLimit: 21000, 17 | data: '0x', 18 | amount: 1n, 19 | }, 20 | ...baseAddresses, 21 | account: accounts[0].address, 22 | }), 23 | ).toBeDefined() 24 | }) 25 | 26 | test('correctly deposits ETH', async () => { 27 | const amount = 1n 28 | const args: DepositETHParameters = { 29 | to: '0x0c54fccd2e384b4bb6f2e405bf5cbc15a017aafb', 30 | gasLimit: 25000, 31 | data: '0x', 32 | amount, 33 | } 34 | const hash = await writeDepositETH(walletClient, { 35 | args, 36 | ...baseAddresses, 37 | account: accounts[0].address, 38 | }) 39 | 40 | await mine(testClient, { blocks: 5 }) 41 | 42 | const r = await publicClient.getTransactionReceipt({ hash }) 43 | expect(r.logs.length).toEqual(1) 44 | const depositEvent = decodeEventLog({ 45 | abi: optimismPortalABI, 46 | data: r.logs[0].data, 47 | topics: r.logs[0].topics, 48 | }) 49 | expect(depositEvent.eventName).toEqual('TransactionDeposited') 50 | const deposit = depositEvent as TransactionDepositedEvent 51 | expect(deposit.args.from.toLowerCase()).toEqual(accounts[0].address) 52 | expect(deposit.args.to.toLowerCase()).toEqual(args.to) 53 | const expectOpaqueData = encodePacked( 54 | ['uint', 'uint', 'uint64', 'bool', 'bytes'], 55 | [amount, amount, BigInt(args.gasLimit), false, '0x'], 56 | ) 57 | expect(deposit.args.opaqueData).toEqual(expectOpaqueData) 58 | }) 59 | -------------------------------------------------------------------------------- /site/docs/actions/public/L2/estimateFees.md: -------------------------------------------------------------------------------- 1 | # estimateFees 2 | 3 | Estimates gas for an L2 transaction including the L1 fee. 4 | 5 | ```ts 6 | const feeValue = await estimateFees(publicClient, { 7 | abi, 8 | functionName: balanceOf, 9 | args: [address], 10 | }) 11 | ``` 12 | 13 | On non-OP chains, fees are usually by `GasUsed * GasPrice`, so the fee depends on the amount of computation required to execute the transaction. On OP chains this is `GasUsed * GasPrice + L1Fee`. 14 | 15 | The L1 portion of the fee depends primarily on the _length_ of the transaction data and the current gas price on L1. The [Gas Price Oracle](https://docs.optimism.io/builders/tools/oracles#gas-oracle) is called to provide the L1 gas price and calculate the total L1 fee. 16 | 17 | See also: 18 | 19 | - [Transaction Fees on OP Mainnet](https://docs.optimism.io/stack/transactions/transaction-fees) 20 | - [`estimateGas` from `viem`](https://viem.sh/docs/actions/public/estimateGas.html) 21 | 22 | ## Return Value 23 | 24 | `bigint` 25 | 26 | The fee in units of wei. 27 | 28 | ## Parameters 29 | 30 | ### client 31 | 32 | - **Type:** `PublicClient` 33 | 34 | A client for the desired OP Stack chain. 35 | 36 | ### options 37 | 38 | - **abi:** `Abi` 39 | 40 | The ABI for the contract containing the function being estimated. 41 | 42 | - **functionName:** `string` 43 | 44 | The name of the function being estimated. 45 | 46 | - **args:** `any[]` 47 | 48 | The arguments to the function being estimated. 49 | 50 | - **account:** `Account | Address` 51 | 52 | The Account to estimate gas from. 53 | 54 | - **to (optional):** `Address` 55 | 56 | Transaction recipient. 57 | 58 | - **value (optional):** `bigint` 59 | 60 | Value (in wei) sent with this transaction. 61 | 62 | - **blockNumber (optional)**: `number` 63 | 64 | The block number to perform the gas estimate against. 65 | 66 | - **blockTag (optional):** `'latest' | 'earliest' | 'pending' | 'safe' | 'finalized'` 67 | 68 | **Default:** `'latest'` 69 | 70 | The block tag to perform the gas estimate against. 71 | 72 | ## JSON-RPC Methods 73 | 74 | [`eth_estimateGas`](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_estimategas) 75 | -------------------------------------------------------------------------------- /src/actions/public/L2/simulateWithdrawETH.ts: -------------------------------------------------------------------------------- 1 | import type { Chain, PublicClient, Transport } from 'viem' 2 | import type { L2SimulateContractParameters } from '../../../types/l2Actions.js' 3 | import { type ABI, type FUNCTION, OVM_ETH, type WithdrawETHParameters } from '../../../types/withdrawTo.js' 4 | import { 5 | simulateWithdrawERC20, 6 | type SimulateWithdrawERC20Parameters, 7 | type SimulateWithdrawERC20ReturnType, 8 | } from './simulateWithdrawERC20.js' 9 | 10 | export type SimulateWithdrawETHParameters< 11 | TChain extends Chain | undefined = Chain, 12 | TChainOverride extends Chain | undefined = Chain | undefined, 13 | > = 14 | & { args: WithdrawETHParameters } 15 | & L2SimulateContractParameters 16 | 17 | export type SimulateWithdrawETHReturnType< 18 | TChain extends Chain | undefined, 19 | TChainOverride extends Chain | undefined = undefined, 20 | > = SimulateWithdrawERC20ReturnType 21 | 22 | /** 23 | * Simulates a withdrawal of ETH to an L1 address. 24 | * 25 | * @param {Address} to the address to withdraw to on L1 26 | * @param {Bigint} amount the amount of ETH to withdraw 27 | * @param {Bigint} minGasLimit the minimum gas limit for the withdrawal 28 | * @param {Hex} [extraData] the extra data for the withdrawal 29 | */ 30 | export async function simulateWithdrawETH< 31 | TChain extends Chain | undefined = Chain, 32 | TChainOverride extends Chain | undefined = Chain | undefined, 33 | >(client: PublicClient, { 34 | args: { to, amount, minGasLimit, extraData = '0x' }, 35 | ...rest 36 | }: SimulateWithdrawETHParameters< 37 | TChain, 38 | TChainOverride 39 | >): Promise> { 40 | return simulateWithdrawERC20(client, { 41 | args: { l2Token: OVM_ETH, to, amount, minGasLimit, extraData }, 42 | // NOTE: msg.value must = amount or transaction will revert 43 | // https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/universal/StandardBridge.sol#L306 44 | value: amount, 45 | ...rest, 46 | } as unknown as SimulateWithdrawERC20Parameters) 47 | } 48 | -------------------------------------------------------------------------------- /src/actions/public/L1/simulateDepositERC20.ts: -------------------------------------------------------------------------------- 1 | import type { Chain, PublicClient, SimulateContractParameters, SimulateContractReturnType, Transport } from 'viem' 2 | import { simulateContract } from 'viem/actions' 3 | import { type RawOrContractAddress, resolveAddress } from '../../../types/addresses.js' 4 | import { ABI, type DepositERC20Parameters, FUNCTION } from '../../../types/depositERC20.js' 5 | import type { L1SimulateActionBaseType } from '../../../types/l1Actions.js' 6 | 7 | export type SimulateDepositERC20Parameters< 8 | TChain extends Chain | undefined = Chain, 9 | TChainOverride extends Chain | undefined = Chain | undefined, 10 | _chainId = TChain extends Chain ? TChain['id'] : number, 11 | > = 12 | & { args: DepositERC20Parameters; l1StandardBridge: RawOrContractAddress<_chainId> } 13 | & L1SimulateActionBaseType 14 | 15 | export type SimulateDepositERC20ReturnType< 16 | TChain extends Chain | undefined, 17 | TChainOverride extends Chain | undefined = undefined, 18 | > = SimulateContractReturnType 19 | 20 | /** 21 | * Simulates a deposit of ERC20 tokens to L2 22 | * @param parameters - {@link SimulateDepositERC20Parameters} 23 | * @returns A [Transaction Hash](https://viem.sh/docs/glossary/terms.html#hash). {@link WriteContractReturnType} 24 | */ 25 | export async function simulateDepositERC20< 26 | TChain extends Chain | undefined, 27 | TChainOverride extends Chain | undefined = undefined, 28 | >( 29 | client: PublicClient, 30 | { 31 | args: { l1Token, l2Token, to, amount, minGasLimit, extraData = '0x' }, 32 | l1StandardBridge, 33 | ...rest 34 | }: SimulateDepositERC20Parameters, 35 | ): Promise> { 36 | return simulateContract(client, { 37 | address: resolveAddress(l1StandardBridge), 38 | abi: ABI, 39 | functionName: FUNCTION, 40 | args: [l1Token, l2Token, to, amount, minGasLimit, extraData], 41 | ...rest, 42 | } as unknown as SimulateContractParameters) 43 | } 44 | -------------------------------------------------------------------------------- /src/_test/live.test.ts: -------------------------------------------------------------------------------- 1 | import { createPublicClient, createWalletClient, type Hex, http } from 'viem' 2 | import { privateKeyToAccount } from 'viem/accounts' 3 | import { estimateGas } from 'viem/actions' 4 | import { goerli } from 'viem/chains' 5 | import { test } from 'vitest' 6 | import type { DepositTransactionParameters } from '../actions/wallet/L1/writeDepositTransaction.js' 7 | import { baseGoerliAddresses } from '../chains/baseGoerli.js' 8 | import { publicL1OpStackActions } from '../decorators/publicL1OpStackActions.js' 9 | import { walletL1OpStackActions } from '../decorators/walletL1OpStackActions.js' 10 | 11 | test('correctly retrieves L2 hash', async () => { 12 | return 13 | // rome-ignore lint: ok code is unreachable 14 | const pk = process.env.VITE_PRIVATE_KEY 15 | if (!pk) { 16 | console.log('no private key') 17 | return 18 | } 19 | const account = privateKeyToAccount(pk as Hex) 20 | const walletClient = createWalletClient({ 21 | account, 22 | chain: goerli, 23 | transport: http(), 24 | }).extend(walletL1OpStackActions) 25 | 26 | const baseGoerliPublicClient = createPublicClient({ 27 | chain: goerli, 28 | transport: http(), 29 | }).extend(publicL1OpStackActions) 30 | 31 | const args: DepositTransactionParameters = { 32 | to: account.address, 33 | mint: 1n, 34 | data: '0x', 35 | gasLimit: 0n, 36 | isCreation: false, 37 | } 38 | 39 | const gas = await estimateGas(baseGoerliPublicClient, { 40 | account: account.address, 41 | to: args.to, 42 | value: args.value, 43 | data: args.data, 44 | }) 45 | 46 | args.gasLimit = gas 47 | 48 | const depositHash = await walletClient.writeDepositTransaction({ 49 | ...baseGoerliAddresses, 50 | args, 51 | }) 52 | 53 | console.log('depositHash', depositHash) 54 | 55 | const mainnetPublicClient = createPublicClient({ 56 | chain: goerli, 57 | transport: http(), 58 | }).extend(publicL1OpStackActions) 59 | 60 | await mainnetPublicClient.waitForTransactionReceipt({ hash: depositHash }) 61 | 62 | const l2Hash = await mainnetPublicClient.getL2HashesForDepositTx({ 63 | l1TxHash: depositHash, 64 | }) 65 | 66 | console.log('l2Hash', l2Hash) 67 | }) 68 | -------------------------------------------------------------------------------- /src/actions/public/L1/readProvenWithdrawals.ts: -------------------------------------------------------------------------------- 1 | import { optimismPortalABI } from '@eth-optimism/contracts-ts' 2 | import type { Chain, Hex, PublicClient, ReadContractParameters, Transport } from 'viem' 3 | import { readContract } from 'viem/actions' 4 | import type { MessagePassedEvent } from '../../../index.js' 5 | import { type RawOrContractAddress, resolveAddress } from '../../../types/addresses.js' 6 | 7 | const ABI = optimismPortalABI 8 | const FUNCTION_NAME = 'provenWithdrawals' 9 | 10 | export type ReadProvenWithdrawalsParameters< 11 | TChain extends Chain | undefined = Chain | undefined, 12 | _chainId = TChain extends Chain ? TChain['id'] : number, 13 | > = { withdrawalHash: MessagePassedEvent['withdrawalHash']; portal: RawOrContractAddress<_chainId> } 14 | 15 | export type ProvenWithdrawal = { 16 | outputRoot: Hex 17 | timestamp: bigint 18 | l2OutputIndex: bigint 19 | } 20 | 21 | export type ReadProvenWithdrawalsReturnType = ProvenWithdrawal 22 | 23 | // Convention: use `read` if this is just 1:1 with some contract function 24 | /** 25 | * Reads a proven withdrawal from the Optimism Portal. 26 | * 27 | * @param {Hash} withdrawalHash the hash of the withdrawal 28 | * @param {RawOrContractAddress} portal the address of the portal 29 | * 30 | * @returns {Promise} the proven withdrawal 31 | */ 32 | export async function readProvenWithdrawals( 33 | client: PublicClient, 34 | { 35 | withdrawalHash, 36 | portal, 37 | }: ReadProvenWithdrawalsParameters, 38 | ): Promise { 39 | const values = await readContract(client, { 40 | abi: ABI, 41 | functionName: FUNCTION_NAME, 42 | address: resolveAddress(portal), 43 | args: [withdrawalHash], 44 | chain: client.chain, 45 | } as ReadContractParameters) 46 | 47 | const provenWithdrawal = { 48 | outputRoot: values[0], 49 | timestamp: values[1], 50 | l2OutputIndex: values[2], 51 | } 52 | 53 | if (provenWithdrawal.timestamp === 0n) { 54 | throw new Error(`Withdrawal with hash ${withdrawalHash} is not proven`) 55 | } 56 | 57 | return provenWithdrawal 58 | } 59 | -------------------------------------------------------------------------------- /src/actions/wallet/L2/writeWithdrawERC20.ts: -------------------------------------------------------------------------------- 1 | import type { Account, Chain, Transport, WalletClient, WriteContractParameters, WriteContractReturnType } from 'viem' 2 | import { writeContract } from 'viem/actions' 3 | import type { L2WriteContractParameters } from '../../../types/l2Actions.js' 4 | import { opStackL2ChainContracts, OpStackL2Contract } from '../../../types/opStackContracts.js' 5 | import { ABI, FUNCTION, type WithdrawToParameters } from '../../../types/withdrawTo.js' 6 | 7 | export type WriteWithdrawERC20Parameters< 8 | TChain extends Chain | undefined = Chain, 9 | TAccount extends Account | undefined = Account | undefined, 10 | TChainOverride extends Chain | undefined = Chain | undefined, 11 | > = 12 | & { args: WithdrawToParameters } 13 | & L2WriteContractParameters 14 | 15 | /** 16 | * Withdraws ERC20 tokens to an L1 address. 17 | * 18 | * @param {Address} l2Token the address of the ERC20 token on L2 19 | * @param {Address} to the address to withdraw to on L1 20 | * @param {Bigint} amount the amount of tokens to withdraw 21 | * @param {Bigint} minGasLimit the minimum gas limit for the withdrawal 22 | * @param {Hex} [extraData] the extra data for the withdrawal 23 | * 24 | * @returns {Promise} the hash of the transaction 25 | */ 26 | export async function writeWithdrawERC20< 27 | TChain extends Chain | undefined = Chain, 28 | TAccount extends Account | undefined = Account | undefined, 29 | TChainOverride extends Chain | undefined = Chain | undefined, 30 | >(client: WalletClient, { 31 | args: { l2Token, to, amount, minGasLimit, extraData = '0x' }, 32 | ...rest 33 | }: WriteWithdrawERC20Parameters< 34 | TChain, 35 | TAccount, 36 | TChainOverride 37 | >): Promise { 38 | return writeContract(client, { 39 | abi: ABI, 40 | functionName: FUNCTION, 41 | args: [l2Token, to, amount, minGasLimit, extraData], 42 | address: opStackL2ChainContracts[OpStackL2Contract.L2StandardBridge].address, 43 | ...rest, 44 | } as unknown as WriteContractParameters< 45 | typeof ABI, 46 | typeof FUNCTION, 47 | TChain, 48 | TAccount, 49 | TChainOverride 50 | >) 51 | } 52 | -------------------------------------------------------------------------------- /src/actions/public/L2/simulateWithdrawERC20.ts: -------------------------------------------------------------------------------- 1 | import type { Chain, PublicClient, SimulateContractParameters, SimulateContractReturnType, Transport } from 'viem' 2 | import { simulateContract } from 'viem/actions' 3 | import type { L2SimulateContractParameters } from '../../../types/l2Actions.js' 4 | import { opStackL2ChainContracts } from '../../../types/opStackContracts.js' 5 | import { ABI, FUNCTION, type WithdrawToParameters } from '../../../types/withdrawTo.js' 6 | 7 | export type SimulateWithdrawERC20Parameters< 8 | TChain extends Chain | undefined = Chain, 9 | TChainOverride extends Chain | undefined = Chain | undefined, 10 | > = 11 | & { args: WithdrawToParameters } 12 | & L2SimulateContractParameters 13 | 14 | export type SimulateWithdrawERC20ReturnType< 15 | TChain extends Chain | undefined, 16 | TChainOverride extends Chain | undefined = undefined, 17 | > = SimulateContractReturnType 18 | 19 | /** 20 | * Simulates a withdrawal of ERC20 tokens to an L1 address. 21 | * 22 | * @param {Address} l2Token the address of the ERC20 token on L2 23 | * @param {Address} to the address to withdraw to on L1 24 | * @param {Bigint} amount the amount of tokens to withdraw 25 | * @param {Bigint} minGasLimit the minimum gas limit for the withdrawal 26 | * @param {Hex} [extraData] the extra data for the withdrawal 27 | */ 28 | export async function simulateWithdrawERC20< 29 | TChain extends Chain | undefined = Chain, 30 | TChainOverride extends Chain | undefined = Chain | undefined, 31 | >(client: PublicClient, { 32 | args: { l2Token, to, amount, minGasLimit, extraData = '0x' }, 33 | ...rest 34 | }: SimulateWithdrawERC20Parameters< 35 | TChain, 36 | TChainOverride 37 | >): Promise> { 38 | return simulateContract(client, { 39 | abi: ABI, 40 | functionName: FUNCTION, 41 | args: [l2Token, to, amount, minGasLimit, extraData], 42 | address: opStackL2ChainContracts.l2StandardBridge.address, 43 | ...rest, 44 | } as unknown as SimulateContractParameters< 45 | typeof ABI, 46 | typeof FUNCTION, 47 | TChain, 48 | TChainOverride 49 | >) 50 | } 51 | -------------------------------------------------------------------------------- /src/actions/wallet/L2/writeWithdrawERC20.test.ts: -------------------------------------------------------------------------------- 1 | import { l2StandardBridgeABI } from '@eth-optimism/contracts-ts' 2 | import { decodeEventLog } from 'viem' 3 | import { mine, writeContract } from 'viem/actions' 4 | import { expect, test } from 'vitest' 5 | import { erc20ABI } from 'wagmi' 6 | import { rollupPublicClient, rollupTestClient, rollupWalletClient } from '../../../_test/utils.js' 7 | import { opStackL2ChainContracts } from '../../../types/opStackContracts.js' 8 | import { writeWithdrawERC20 } from './writeWithdrawERC20.js' 9 | 10 | const USDbC = '0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA' 11 | const address = '0xbc3ed6b537f2980e66f396fe14210a56ba3f72c4' 12 | 13 | test('successfuly submits transaction', async () => { 14 | const amount = 100n 15 | await rollupTestClient.impersonateAccount({ 16 | address, 17 | }) 18 | await rollupTestClient.setBalance({ 19 | address, 20 | value: 10n ** 22n, 21 | }) 22 | await writeContract(rollupTestClient, { 23 | address: USDbC, 24 | abi: erc20ABI, 25 | functionName: 'approve', 26 | args: [opStackL2ChainContracts.l2StandardBridge.address, amount], 27 | account: address, 28 | }) 29 | 30 | await mine(rollupTestClient, { blocks: 1 }) 31 | const hash = await writeWithdrawERC20(rollupWalletClient, { 32 | args: { l2Token: USDbC, to: address, amount, minGasLimit: 100000 }, 33 | account: address, 34 | }) 35 | await mine(rollupTestClient, { blocks: 1 }) 36 | const receipt = await rollupPublicClient.getTransactionReceipt({ hash }) 37 | expect(receipt.status).toEqual('success') 38 | expect(receipt.to).toEqual(opStackL2ChainContracts.l2StandardBridge.address.toLowerCase()) 39 | const withdawalLogs: any[] = [] 40 | for (const l of receipt.logs) { 41 | try { 42 | const event = decodeEventLog({ abi: l2StandardBridgeABI, data: l.data, topics: l.topics }) 43 | if (event.eventName === 'WithdrawalInitiated') { 44 | withdawalLogs.push(event) 45 | } 46 | } catch {} 47 | } 48 | 49 | expect(withdawalLogs[0].args.l2Token).toEqual(USDbC) 50 | expect(withdawalLogs[0].args.to.toLowerCase()).toEqual(address) 51 | expect(withdawalLogs[0].args.from.toLowerCase()).toEqual(address) 52 | expect(withdawalLogs[0].args.amount).toEqual(amount) 53 | }) 54 | -------------------------------------------------------------------------------- /src/actions/public/L1/getSecondsToFinalizable.ts: -------------------------------------------------------------------------------- 1 | import { l2OutputOracleABI } from '@eth-optimism/contracts-ts' 2 | import type { Chain, PublicClient, ReadContractParameters, Transport } from 'viem' 3 | import { readContract } from 'viem/actions' 4 | import type { MessagePassedEvent } from '../../../index.js' 5 | import { type RawOrContractAddress, resolveAddress } from '../../../types/addresses.js' 6 | import { readProvenWithdrawals } from './readProvenWithdrawals.js' 7 | 8 | const ABI = l2OutputOracleABI 9 | 10 | export type GetSecondsToFinalizableParameters< 11 | TChain extends Chain | undefined = Chain | undefined, 12 | _chainId = TChain extends Chain ? TChain['id'] : number, 13 | > = { 14 | withdrawalHash: MessagePassedEvent['withdrawalHash'] 15 | portal: RawOrContractAddress<_chainId> 16 | l2OutputOracle: RawOrContractAddress<_chainId> 17 | } 18 | 19 | /** 20 | * Gets the number of seconds until a withdrawal is finalizable. 21 | * 22 | * @param {Hash} withdrawalHash the hash of the withdrawal 23 | * @param {RawOrContractAddress} portal the address of the portal 24 | * @param {RawOrContractAddress} l2OutputOracle the address of the L2 Output Oracle 25 | * 26 | * @returns {Promise} the number of seconds until the withdrawal is finalizable 27 | */ 28 | export async function getSecondsToFinalizable( 29 | client: PublicClient, 30 | { 31 | withdrawalHash, 32 | l2OutputOracle, 33 | portal, 34 | }: GetSecondsToFinalizableParameters, 35 | ): Promise { 36 | const provenWithdrawal = await readProvenWithdrawals(client, { 37 | portal: resolveAddress(portal), 38 | withdrawalHash, 39 | }) 40 | 41 | const finalizationPeriod = await readContract(client, { 42 | abi: l2OutputOracleABI, 43 | functionName: 'FINALIZATION_PERIOD_SECONDS', 44 | address: resolveAddress(l2OutputOracle), 45 | } as ReadContractParameters) 46 | 47 | const timeSinceProven = BigInt(Date.now()) / 1000n - provenWithdrawal.timestamp 48 | 49 | const finalizable = finalizationPeriod - timeSinceProven 50 | 51 | // NOTE(Wilson): No negative numbers, does not make sense to have negative seconds until finalizable 52 | return finalizable < 0n ? 0n : finalizable 53 | } 54 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { type ProvenWithdrawal } from './actions/public/L1/readProvenWithdrawals.js' 2 | export { type ProveWithdrawalTransactionParameters } from './actions/wallet/L1/writeProveWithdrawalTransaction.js' 3 | export { type PublicL1OpStackActions, publicL1OpStackActions } from './decorators/publicL1OpStackActions.js' 4 | export { type PublicL2OpStackActions, publicL2OpStackActions } from './decorators/publicL2OpStackActions.js' 5 | export { type WalletL1OpStackActions, walletL1OpStackActions } from './decorators/walletL1OpStackActions.js' 6 | export { type WalletL2OpStackActions, walletL2OpStackActions } from './decorators/walletL2OpStackActions.js' 7 | export type { Addresses, ContractAddress, RawOrContractAddress } from './types/addresses.js' 8 | export type { DepositERC20Parameters } from './types/depositERC20.js' 9 | export type { DepositETHParameters } from './types/depositETH.js' 10 | export type { DepositTransaction, TransactionDepositedEvent } from './types/depositTransaction.js' 11 | export { DEPOSIT_TX_PREFIX, SourceHashDomain } from './types/depositTransaction.js' 12 | export type { 13 | BlockOptions, 14 | GasPriceOracleEstimator, 15 | GasPriceOracleParameters, 16 | OracleTransactionParameters, 17 | } from './types/gasPriceOracle.js' 18 | export { type OpStackL2ChainContracts, opStackL2ChainContracts, OpStackL2Contract } from './types/opStackContracts.js' 19 | export type { MessagePassedEvent } from './types/withdrawal.js' 20 | export type { WithdrawETHParameters, WithdrawToParameters } from './types/withdrawTo.js' 21 | export type { GetDepositTransactionParams } from './utils/getDepositTransaction.js' 22 | export { getDepositTransaction } from './utils/getDepositTransaction.js' 23 | export { getL2HashFromL1DepositInfo } from './utils/getL2HashFromL1DepositInfo.js' 24 | export { getSourceHash } from './utils/getSourceHash.js' 25 | export type { 26 | GetTransactionDepositedEventsParams, 27 | GetTransactionDepositedEventsReturnType, 28 | TransactionDepositedEventDetails, 29 | } from './utils/getTransactionDepositedEvents.js' 30 | export { getTransactionDepositedEvents } from './utils/getTransactionDepositedEvents.js' 31 | export { getWithdrawalMessageStorageSlot } from './utils/getWithdrawalMessageStorageSlot.js' 32 | export { rlpEncodeDepositTransaction } from './utils/rlpEncodeDepositTransaction.js' 33 | -------------------------------------------------------------------------------- /src/actions/public/L2/estimateFees.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Abi, 3 | encodeFunctionData, 4 | type EncodeFunctionDataParameters, 5 | type EstimateGasParameters, 6 | type PublicClient, 7 | type Transport, 8 | } from 'viem' 9 | import { type Chain } from 'viem/chains' 10 | import type { GasPriceOracleParameters, OracleTransactionParameters } from '../../../types/gasPriceOracle.js' 11 | import { estimateL1Fee } from './estimateL1Fee.js' 12 | 13 | export type EstimateFeesParameters< 14 | TAbi extends Abi | readonly unknown[], 15 | TFunctionName extends string | undefined = undefined, 16 | > = 17 | & OracleTransactionParameters 18 | & GasPriceOracleParameters 19 | & Omit 20 | 21 | export type EstimateFees = < 22 | TChain extends Chain | undefined, 23 | TAbi extends Abi | readonly unknown[], 24 | TFunctionName extends string | undefined = undefined, 25 | >( 26 | client: PublicClient, 27 | options: EstimateFeesParameters, 28 | ) => Promise 29 | 30 | /** 31 | * Estimates gas for an L2 transaction including the l1 fee 32 | * on non OP chains this is usually GasUsed * GasPrice 33 | * on OP chains this is GasUsed * GasPrice + L1Fee 34 | * @example 35 | * const feeValue = await estimateFees(publicClient, { 36 | * abi, 37 | * functionName: balanceOf, 38 | * args: [address], 39 | * }); 40 | */ 41 | export const estimateFees: EstimateFees = async (client, options) => { 42 | const encodedFunctionData = encodeFunctionData({ 43 | abi: options.abi, 44 | args: options.args, 45 | functionName: options.functionName, 46 | } as EncodeFunctionDataParameters) 47 | const [l1Fee, l2Gas, l2GasPrice] = await Promise.all([ 48 | estimateL1Fee(client, { 49 | ...options, 50 | // account must be undefined or else viem will return undefined 51 | account: undefined as any, 52 | }), 53 | client.estimateGas({ 54 | to: options.to, 55 | account: options.account, 56 | accessList: options.accessList, 57 | blockNumber: options.blockNumber, 58 | blockTag: options.blockTag, 59 | data: encodedFunctionData, 60 | value: options.value, 61 | } as EstimateGasParameters), 62 | client.getGasPrice(), 63 | ]) 64 | return l1Fee + l2Gas * l2GasPrice 65 | } 66 | -------------------------------------------------------------------------------- /src/actions/public/L2/getProveWithdrawalTransactionArgs.bench.ts: -------------------------------------------------------------------------------- 1 | import { CrossChainMessenger } from '@eth-optimism/sdk' 2 | import { providers } from 'ethers' 3 | import { createPublicClient, http } from 'viem' 4 | import { base, mainnet } from 'viem/chains' 5 | import { bench, describe } from 'vitest' 6 | import { forkUrl, rollupForkUrl } from '../../../_test/constants.js' 7 | import { baseAddresses } from '../../../chains/index.js' 8 | import { getOutputForL2Block } from '../L1/getOutputForL2Block.js' 9 | import { getProveWithdrawalTransactionArgs } from './getProveWithdrawalTransactionArgs.js' 10 | import { getWithdrawalMessages } from './getWithdrawalMessages.js' 11 | 12 | describe('Computes L1 prove args from L2 tx hash', () => { 13 | const hash = '0xd0eb2a59f3cc4c61b01c350e71e1804ad6bd776dc9abc1bdb5e2e40695ab2628' 14 | bench( 15 | 'op-viem: `getWithdrawalMessages, getOutputForL2Block, getProveArgsForWithdrawal`', 16 | async () => { 17 | // cannot currently use anvil rollupPublicClient for this as eth_getProof isn't working 18 | const client = createPublicClient({ 19 | chain: base, 20 | transport: http(), 21 | }) 22 | 23 | const withdrawalMessages = await getWithdrawalMessages(client, { 24 | hash, 25 | }) 26 | 27 | const l1Client = createPublicClient({ 28 | chain: mainnet, 29 | transport: http(), 30 | }) 31 | 32 | const output = await getOutputForL2Block(l1Client, { 33 | l2BlockNumber: withdrawalMessages.blockNumber, 34 | ...baseAddresses, 35 | }) 36 | 37 | await getProveWithdrawalTransactionArgs(client, { 38 | message: withdrawalMessages.messages[0], 39 | output: output, 40 | }) 41 | }, 42 | ) 43 | 44 | bench( 45 | '@eth-optimism/sdk: `getBedrockMessageProof`', 46 | async () => { 47 | const ethersProvider = new providers.JsonRpcProvider( 48 | forkUrl, 49 | ) 50 | const ethersRollupProvider = new providers.JsonRpcProvider( 51 | rollupForkUrl, 52 | ) 53 | const messenger = new CrossChainMessenger({ 54 | l1ChainId: mainnet.id, 55 | l2ChainId: base.id, 56 | l1SignerOrProvider: ethersProvider, 57 | l2SignerOrProvider: ethersRollupProvider, 58 | }) 59 | await messenger.getBedrockMessageProof(hash) 60 | }, 61 | ) 62 | }) 63 | -------------------------------------------------------------------------------- /src/actions/public/L1/simulateDepositTransaction.ts: -------------------------------------------------------------------------------- 1 | import type { Chain, PublicClient, SimulateContractParameters, SimulateContractReturnType, Transport } from 'viem' 2 | import { simulateContract } from 'viem/actions' 3 | import { type RawOrContractAddress, resolveAddress } from '../../../types/addresses.js' 4 | import { type L1SimulateActionBaseType } from '../../../types/l1Actions.js' 5 | import { ABI, type DepositTransactionParameters, FUNCTION } from '../../wallet/L1/writeDepositTransaction.js' 6 | 7 | export type SimulateDepositTransactionParameters< 8 | TChain extends Chain | undefined = Chain, 9 | TChainOverride extends Chain | undefined = Chain | undefined, 10 | _chainId = TChain extends Chain ? TChain['id'] : number, 11 | > = 12 | & { args: DepositTransactionParameters; portal: RawOrContractAddress<_chainId> } 13 | & L1SimulateActionBaseType< 14 | TChain, 15 | TChainOverride, 16 | typeof ABI, 17 | typeof FUNCTION 18 | > 19 | 20 | export type SimulateDepositTransactionReturnType< 21 | TChain extends Chain | undefined, 22 | TChainOverride extends Chain | undefined = undefined, 23 | > = SimulateContractReturnType< 24 | typeof ABI, 25 | typeof FUNCTION, 26 | TChain, 27 | TChainOverride 28 | > 29 | 30 | /** 31 | * Simulates a call to DepositTranasction on the OptimismPortal contract. 32 | * 33 | * @param parameters - {@link SimulateDepositTransactionParameters} 34 | * @returns A [Transaction Hash](https://viem.sh/docs/glossary/terms.html#hash). {@link WriteContractReturnType} 35 | */ 36 | export async function simulateDepositTransaction< 37 | TChain extends Chain | undefined, 38 | TChainOverride extends Chain | undefined = undefined, 39 | >( 40 | client: PublicClient, 41 | { 42 | args: { to, value = 0n, gasLimit, isCreation = false, data = '0x' }, 43 | portal, 44 | ...rest 45 | }: SimulateDepositTransactionParameters< 46 | TChain, 47 | TChainOverride 48 | >, 49 | ): Promise> { 50 | return simulateContract(client, { 51 | address: resolveAddress(portal), 52 | abi: ABI, 53 | functionName: FUNCTION, 54 | args: [to, value, gasLimit, isCreation, data], 55 | ...rest, 56 | } as unknown as SimulateContractParameters< 57 | typeof ABI, 58 | typeof FUNCTION, 59 | TChain, 60 | TChainOverride 61 | >) 62 | } 63 | -------------------------------------------------------------------------------- /src/actions/public/L1/simulateFinalizeWithdrawalTransaction.ts: -------------------------------------------------------------------------------- 1 | import type { Chain, PublicClient, SimulateContractParameters, SimulateContractReturnType, Transport } from 'viem' 2 | import { simulateContract } from 'viem/actions' 3 | import { type RawOrContractAddress, resolveAddress } from '../../../types/addresses.js' 4 | import { type L1SimulateActionBaseType } from '../../../types/l1Actions.js' 5 | import { 6 | ABI, 7 | type FinalizeWithdrawalTransactionParameters, 8 | FUNCTION, 9 | } from '../../wallet/L1/writeFinalizeWithdrawalTransaction.js' 10 | 11 | export type SimulateFinalizeWithdrawalTransactionParameters< 12 | TChain extends Chain | undefined = Chain, 13 | TChainOverride extends Chain | undefined = Chain | undefined, 14 | _chainId = TChain extends Chain ? TChain['id'] : number, 15 | > = 16 | & { withdrawal: FinalizeWithdrawalTransactionParameters; portal: RawOrContractAddress<_chainId> } 17 | & L1SimulateActionBaseType< 18 | TChain, 19 | TChainOverride, 20 | typeof ABI, 21 | typeof FUNCTION 22 | > 23 | 24 | export type SimulateFinalizeWithdrawalTransactionReturnType< 25 | TChain extends Chain | undefined, 26 | TChainOverride extends Chain | undefined = undefined, 27 | > = SimulateContractReturnType< 28 | typeof ABI, 29 | typeof FUNCTION, 30 | TChain, 31 | TChainOverride 32 | > 33 | 34 | /** 35 | * Simulates a call to finalizeWithdrawalTranasction on the OptimismPortal contract. 36 | * 37 | * @param parameters - {@link SimulateFinalizeWithdrawalTransactionParameters} 38 | * @returns A [Transaction Hash](https://viem.sh/docs/glossary/terms.html#hash). {@link WriteContractReturnType} 39 | */ 40 | export async function simulateFinalizeWithdrawalTransaction< 41 | TChain extends Chain | undefined, 42 | TChainOverride extends Chain | undefined = undefined, 43 | >( 44 | client: PublicClient, 45 | { 46 | withdrawal, 47 | portal, 48 | ...rest 49 | }: SimulateFinalizeWithdrawalTransactionParameters< 50 | TChain, 51 | TChainOverride 52 | >, 53 | ): Promise> { 54 | return simulateContract(client, { 55 | address: resolveAddress(portal), 56 | abi: ABI, 57 | functionName: FUNCTION, 58 | args: [withdrawal], 59 | ...rest, 60 | } as unknown as SimulateContractParameters< 61 | typeof ABI, 62 | typeof FUNCTION, 63 | TChain, 64 | TChainOverride 65 | >) 66 | } 67 | -------------------------------------------------------------------------------- /src/actions/wallet/L1/writeSendMessage.ts: -------------------------------------------------------------------------------- 1 | import { l1CrossDomainMessengerABI } from '@eth-optimism/contracts-ts' 2 | import type { 3 | Account, 4 | Address, 5 | Chain, 6 | Hex, 7 | Transport, 8 | WalletClient, 9 | WriteContractParameters, 10 | WriteContractReturnType, 11 | } from 'viem' 12 | import { writeContract } from 'viem/actions' 13 | import { type RawOrContractAddress, resolveAddress } from '../../../types/addresses.js' 14 | import type { L1WriteActionBaseType } from '../../../types/l1Actions.js' 15 | 16 | const ABI = l1CrossDomainMessengerABI 17 | const FUNCTION = 'sendMessage' 18 | 19 | export type SendMessageParameters = { 20 | target: Address 21 | minGasLimit: number 22 | message?: Hex 23 | } 24 | 25 | export type WriteSendMessageParameters< 26 | TChain extends Chain | undefined = Chain, 27 | TAccount extends Account | undefined = Account | undefined, 28 | TChainOverride extends Chain | undefined = Chain | undefined, 29 | _chainId = TChain extends Chain ? TChain['id'] : number, 30 | > = 31 | & { args: SendMessageParameters; l1CrossDomainMessenger: RawOrContractAddress<_chainId> } 32 | & L1WriteActionBaseType< 33 | TChain, 34 | TAccount, 35 | TChainOverride 36 | > 37 | 38 | /** 39 | * A generic, low-level way to make a L1 -> L2 call with replayability. 40 | * Calls sendMessage on the L1CrossDomainMessenger contract. 41 | * 42 | * @param parameters - {@link WriteSendMessageParameters} 43 | * @returns A [Transaction Hash](https://viem.sh/docs/glossary/terms.html#hash). {@link WriteContractReturnType} 44 | */ 45 | export async function writeSendMessage< 46 | TChain extends Chain | undefined, 47 | TAccount extends Account | undefined, 48 | TChainOverride extends Chain | undefined = undefined, 49 | >( 50 | client: WalletClient, 51 | { 52 | args: { target, minGasLimit, message = '0x' }, 53 | l1CrossDomainMessenger, 54 | ...rest 55 | }: WriteSendMessageParameters< 56 | TChain, 57 | TAccount, 58 | TChainOverride 59 | >, 60 | ): Promise { 61 | return writeContract(client, { 62 | address: resolveAddress(l1CrossDomainMessenger), 63 | abi: ABI, 64 | functionName: FUNCTION, 65 | args: [target, message, minGasLimit], 66 | ...rest, 67 | } as unknown as WriteContractParameters< 68 | typeof ABI, 69 | typeof FUNCTION, 70 | TChain, 71 | TAccount, 72 | TChainOverride 73 | >) 74 | } 75 | -------------------------------------------------------------------------------- /src/actions/wallet/L1/writeFinalizeWithdrawalTransaction.ts: -------------------------------------------------------------------------------- 1 | import { optimismPortalABI } from '@eth-optimism/contracts-ts' 2 | import type { Account, Chain, Transport, WalletClient, WriteContractParameters, WriteContractReturnType } from 'viem' 3 | import { writeContract } from 'viem/actions' 4 | import type { MessagePassedEvent } from '../../../index.js' 5 | import { type RawOrContractAddress, resolveAddress } from '../../../types/addresses.js' 6 | import type { L1WriteActionBaseType } from '../../../types/l1Actions.js' 7 | 8 | export const ABI = optimismPortalABI 9 | export const FUNCTION = 'finalizeWithdrawalTransaction' 10 | 11 | export type FinalizeWithdrawalTransactionParameters = Omit 12 | 13 | export type WriteFinalizeWithdrawalTransactionParameters< 14 | TChain extends Chain | undefined = Chain, 15 | TAccount extends Account | undefined = Account | undefined, 16 | TChainOverride extends Chain | undefined = Chain | undefined, 17 | _chainId = TChain extends Chain ? TChain['id'] : number, 18 | > = 19 | & { args: { withdrawal: FinalizeWithdrawalTransactionParameters }; portal: RawOrContractAddress<_chainId> } 20 | & L1WriteActionBaseType< 21 | TChain, 22 | TAccount, 23 | TChainOverride 24 | > 25 | 26 | /** 27 | * Calls writeFinalizeWithdrawalTranasction on the OptimismPortal contract. 28 | * Is the second and final L1 step of a withdrawal. 29 | * 30 | * @param parameters - {@link WriteFinalizeWithdrawalTransactionParameters} 31 | * @returns A [Transaction Hash](https://viem.sh/docs/glossary/terms.html#hash). {@link WriteContractReturnType} 32 | */ 33 | export async function writeFinalizeWithdrawalTranasction< 34 | TChain extends Chain | undefined, 35 | TAccount extends Account | undefined, 36 | TChainOverride extends Chain | undefined = undefined, 37 | >( 38 | client: WalletClient, 39 | { 40 | args: { withdrawal }, 41 | portal, 42 | ...rest 43 | }: WriteFinalizeWithdrawalTransactionParameters< 44 | TChain, 45 | TAccount, 46 | TChainOverride 47 | >, 48 | ): Promise { 49 | return writeContract(client, { 50 | address: resolveAddress(portal), 51 | abi: ABI, 52 | functionName: FUNCTION, 53 | args: [withdrawal], 54 | ...rest, 55 | } as unknown as WriteContractParameters< 56 | typeof ABI, 57 | typeof FUNCTION, 58 | TChain, 59 | TAccount, 60 | TChainOverride 61 | >) 62 | } 63 | -------------------------------------------------------------------------------- /src/actions/public/L1/getSecondsToNextL2Output.ts: -------------------------------------------------------------------------------- 1 | import { l2OutputOracleABI } from '@eth-optimism/contracts-ts' 2 | import type { Chain, PublicClient, ReadContractParameters, Transport } from 'viem' 3 | import { readContract } from 'viem/actions' 4 | import { type RawOrContractAddress, resolveAddress } from '../../../types/addresses.js' 5 | 6 | const ABI = l2OutputOracleABI 7 | 8 | export type GetSecondsToNextL2OutputParameters< 9 | TChain extends Chain | undefined = Chain | undefined, 10 | _chainId = TChain extends Chain ? TChain['id'] : number, 11 | > = { latestL2BlockNumber: bigint; l2OutputOracle: RawOrContractAddress<_chainId> } 12 | 13 | /** 14 | * Gets the number of seconds until the next L2 output is posted. 15 | * 16 | * @param {bigint} latestL2BlockNumber the latest L2 block number 17 | * @param {RawOrContractAddress} l2OutputOracle the address of the L2 Output Oracle 18 | * 19 | * @returns {Promise} the number of seconds until the next L2 output is posted 20 | */ 21 | export async function getSecondsToNextL2Output( 22 | client: PublicClient, 23 | { 24 | latestL2BlockNumber, 25 | l2OutputOracle, 26 | }: GetSecondsToNextL2OutputParameters, 27 | ): Promise { 28 | const address = resolveAddress(l2OutputOracle) 29 | const latestBlockNumber = await readContract(client, { 30 | abi: ABI, 31 | functionName: 'latestBlockNumber', 32 | address, 33 | } as ReadContractParameters) 34 | 35 | const interval = await readContract(client, { 36 | abi: ABI, 37 | functionName: 'SUBMISSION_INTERVAL', 38 | address, 39 | } as ReadContractParameters) 40 | 41 | const blockTime = await readContract(client, { 42 | abi: ABI, 43 | functionName: 'L2_BLOCK_TIME', 44 | address, 45 | } as ReadContractParameters) 46 | 47 | if (latestL2BlockNumber < latestBlockNumber) { 48 | throw new Error(`latestBlock ${latestBlockNumber} is great than latestL2BlockNumber ${latestL2BlockNumber}!`) 49 | } 50 | 51 | const blocksTillUpdate = interval - (latestL2BlockNumber - latestBlockNumber) 52 | // NOTE(Wilson): incase there is some problem 53 | // e.g. output posting has stalled or the wrong latestL2BlockNumber is passed 54 | // we do not return a negative number, as negative seconds to next output 55 | // does not make sense 56 | return blocksTillUpdate < 0n ? 0n : blocksTillUpdate * blockTime 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | 5 |

OP Viem

6 | 7 |

8 | 9 |

10 | Viem Extension for OP Stack Chains 11 |

12 | 13 |
14 | 15 | ## 🚨 DEPRECATION WARNING 🚨 16 | 17 | With the upstreaming of most op-viem features into [Viem](https://viem.sh/op-stack) consider this library deprecated. We recommend using [Viem's native OP Stack support](https://viem.sh/op-stack) instead. 18 | 19 | ## Features 20 | 21 | - Simplifies cross L1 & L2 interactions 22 | - Seamless extension to [Viem](https://github.com/wagmi-dev/viem) 23 | - TypeScript ready 24 | - Test suite running against [forked](https://ethereum.org/en/glossary/#fork) Ethereum network 25 | 26 | ## Overview 27 | 28 | ```ts 29 | // import modules 30 | import { createWalletClient, createPublicClient, custom, http } from 'viem' 31 | import { privateKeyToAccount } from 'viem/accounts' 32 | import { mainnet, base } from 'viem/chains' 33 | import { baseAddresses } from 'op-viem/chains' 34 | import { walletL1OpStackActions, publicL1OpStackActions, publicL2OpStackActions } from 'op-viem' 35 | 36 | // create clients 37 | export const opStackL1WalletClient = createWalletClient({ 38 | chain: mainnet, 39 | transport: custom(window.ethereum) 40 | }).extend(walletL1OpStackActions) 41 | 42 | export const opStackL1PublicClient = createPublicClient({ 43 | chain: mainnet, 44 | transport: http() 45 | }).extend(publicL1OpStackActions) 46 | 47 | export const opStackL2PublicClient = createPublicClient({ 48 | chain: base, 49 | transport: http() 50 | }).extend(publicL2OpStackActions) 51 | 52 | // perform an action 53 | opStackL1PublicClient.getOutputForL2Block(blockNumber: 2725977n, ...baseAddresses) 54 | ``` 55 | 56 | ## Community 57 | 58 | Check out the following places for more viem-related content: 59 | 60 | - Follow [@wilsoncusack](https://twitter.com/wilsoncusack) Twitter for project updates 61 | 62 | ## Contributing 63 | 64 | If you're interested in contributing, please read the [contributing docs](CONTRIBUTING.md) **before submitting a pull request**. 65 | 66 | ## Authors 67 | 68 | - [@wilsoncusack](https://github.com/wilsoncusack) (wilsoncusack.eth [Twitter](https://twitter.com/wilsoncusack)) 69 | - [@zencephalon](https://github.com/zencephalon) (zencephalon.eth, [Twitter](https://twitter.com/zencephalon)) 70 | - [@roninjin10](https://github.com/roninjin10) (fucory.eth, [Twitter](https://twitter.com/FUCORY)) 71 | 72 | ## License 73 | 74 | [MIT](LICENSE.md) License 75 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | // This tsconfig file contains the shared config for the build (tsconfig.build.json) and type checking (tsconfig.json) config. 3 | "include": [], 4 | "compilerOptions": { 5 | // Incremental builds 6 | // NOTE: Enabling incremental builds speeds up `tsc`. Keep in mind though that it does not reliably bust the cache when the `tsconfig.json` file changes. 7 | "incremental": true, 8 | 9 | // Type checking 10 | "strict": true, 11 | "useDefineForClassFields": true, // Not enabled by default in `strict` mode unless we bump `target` to ES2022. 12 | "noFallthroughCasesInSwitch": true, // Not enabled by default in `strict` mode. 13 | "noImplicitReturns": true, // Not enabled by default in `strict` mode. 14 | "useUnknownInCatchVariables": true, // TODO: This would normally be enabled in `strict` mode but would require some adjustments to the codebase. 15 | "noImplicitOverride": true, // Not enabled by default in `strict` mode. 16 | "noUnusedLocals": true, // Not enabled by default in `strict` mode. 17 | "noUnusedParameters": true, // Not enabled by default in `strict` mode. 18 | // TODO: The following options are also not enabled by default in `strict` mode and would be nice to have but would require some adjustments to the codebase. 19 | // "exactOptionalPropertyTypes": true, 20 | // "noUncheckedIndexedAccess": true, 21 | 22 | // JavaScript support 23 | "allowJs": false, 24 | "checkJs": false, 25 | 26 | // Interop constraints 27 | "esModuleInterop": false, 28 | "allowSyntheticDefaultImports": false, 29 | "forceConsistentCasingInFileNames": true, 30 | "verbatimModuleSyntax": true, 31 | "importHelpers": true, // This is only used for build validation. Since we do not have `tslib` installed, this will fail if we accidentally make use of anything that'd require injection of helpers. 32 | 33 | // Language and environment 34 | "moduleResolution": "NodeNext", 35 | "module": "NodeNext", 36 | "target": "ES2021", // Setting this to `ES2021` enables native support for `Node v16+`: https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping. 37 | "lib": [ 38 | "ES2022", // By using ES2022 we get access to the `.cause` property on `Error` instances. 39 | "DOM" // We are adding `DOM` here to get the `fetch`, etc. types. This should be removed once these types are available via DefinitelyTyped. 40 | ], 41 | 42 | // Skip type checking for node modules 43 | "skipLibCheck": true 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: Verify 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | lint: 12 | name: Lint 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 5 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v3 19 | 20 | - name: Set up pnpm 21 | uses: pnpm/action-setup@v2 22 | with: 23 | version: 8 24 | 25 | - name: Set up node 26 | uses: actions/setup-node@v3 27 | with: 28 | cache: pnpm 29 | node-version: 18 30 | 31 | - name: Install dependencies 32 | run: pnpm install 33 | 34 | - name: Lint code 35 | run: pnpm format:check && pnpm lint 36 | 37 | types: 38 | name: Types 39 | runs-on: ubuntu-latest 40 | timeout-minutes: 5 41 | 42 | steps: 43 | - name: Checkout code 44 | uses: actions/checkout@v3 45 | 46 | - name: Set up pnpm 47 | uses: pnpm/action-setup@v2 48 | with: 49 | version: 8 50 | 51 | - name: Set up node 52 | uses: actions/setup-node@v3 53 | with: 54 | cache: pnpm 55 | node-version: 18 56 | 57 | - name: Install dependencies 58 | run: pnpm install 59 | 60 | - name: Check types 61 | run: pnpm typecheck 62 | 63 | test: 64 | name: Test 65 | runs-on: ubuntu-latest 66 | environment: verify 67 | 68 | steps: 69 | - name: Set up foundry 70 | uses: foundry-rs/foundry-toolchain@v1 71 | 72 | - name: Checkout code 73 | uses: actions/checkout@v3 74 | 75 | - name: Set up pnpm 76 | uses: pnpm/action-setup@v2 77 | with: 78 | version: 8 79 | 80 | - name: Set up node 81 | uses: actions/setup-node@v3 82 | with: 83 | cache: pnpm 84 | node-version: 18 85 | 86 | - name: Install dependencies 87 | run: pnpm install 88 | 89 | - name: Run tests 90 | run: pnpm test:ci 91 | env: 92 | VITE_ANVIL_FORK_URL: ${{ secrets.VITE_ANVIL_FORK_URL }} 93 | 94 | - name: 'Report Coverage' 95 | if: always() # Also generate the report if tests are failing 96 | uses: davelosert/vitest-coverage-report-action@v2 97 | with: 98 | name: "op-viem" 99 | working-directory: "." 100 | -------------------------------------------------------------------------------- /src/actions/wallet/L1/writeProveWithdrawalTransaction.ts: -------------------------------------------------------------------------------- 1 | import { optimismPortalABI } from '@eth-optimism/contracts-ts' 2 | import type { Account, Chain, Transport, WalletClient, WriteContractParameters, WriteContractReturnType } from 'viem' 3 | import { writeContract } from 'viem/actions' 4 | import { type RawOrContractAddress, resolveAddress } from '../../../types/addresses.js' 5 | import type { L1WriteActionBaseType } from '../../../types/l1Actions.js' 6 | import type { GetProveWithdrawalTransactionArgsReturnType } from '../../index.js' 7 | 8 | export const ABI = optimismPortalABI 9 | export const FUNCTION = 'proveWithdrawalTransaction' 10 | 11 | export type ProveWithdrawalTransactionParameters = GetProveWithdrawalTransactionArgsReturnType 12 | 13 | export type WriteProveWithdrawalTransactionParameters< 14 | TChain extends Chain | undefined = Chain, 15 | TAccount extends Account | undefined = Account | undefined, 16 | TChainOverride extends Chain | undefined = Chain | undefined, 17 | _chainId = TChain extends Chain ? TChain['id'] : number, 18 | > = 19 | & { args: ProveWithdrawalTransactionParameters; portal: RawOrContractAddress<_chainId> } 20 | & L1WriteActionBaseType< 21 | TChain, 22 | TAccount, 23 | TChainOverride 24 | > 25 | 26 | /** 27 | * Calls proveWithdrawalTransaction on the OptimismPortal contract. 28 | * Is the first L1 step of a withdrawal. 29 | * 30 | * @param parameters - {@link WriteProveWithdrawalTransactionParameters} 31 | * @returns A [Transaction Hash](https://viem.sh/docs/glossary/terms.html#hash). {@link WriteContractReturnType} 32 | */ 33 | export async function writeProveWithdrawalTransaction< 34 | TChain extends Chain | undefined, 35 | TAccount extends Account | undefined, 36 | TChainOverride extends Chain | undefined = undefined, 37 | >( 38 | client: WalletClient, 39 | { 40 | args: { withdrawalTransaction, outputRootProof, withdrawalProof, L2OutputIndex }, 41 | portal, 42 | ...rest 43 | }: WriteProveWithdrawalTransactionParameters< 44 | TChain, 45 | TAccount, 46 | TChainOverride 47 | >, 48 | ): Promise { 49 | return writeContract(client, { 50 | address: resolveAddress(portal), 51 | abi: ABI, 52 | functionName: FUNCTION, 53 | args: [withdrawalTransaction, L2OutputIndex, outputRootProof, withdrawalProof], 54 | ...rest, 55 | } as unknown as WriteContractParameters< 56 | typeof ABI, 57 | typeof FUNCTION, 58 | TChain, 59 | TAccount, 60 | TChainOverride 61 | >) 62 | } 63 | -------------------------------------------------------------------------------- /site/docs/actions/public/L2/simulateWithdrawETH.md: -------------------------------------------------------------------------------- 1 | # simulateWithdawETH 2 | 3 | Simulates a [writeWithdrawETH](/docs/actions/wallet/L2/writeWithdrawETH) transaction. 4 | 5 | ::: code-group 6 | 7 | ```ts [example.ts] 8 | import { WithdrawETHParameters } from 'op-viem' 9 | import { account, opStackL2PublicClient } from './config' 10 | 11 | const args: WithdrawETHParameters = { 12 | to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 13 | amount: 100n, 14 | minGasLimit: 85000, 15 | extraData: '0x123', 16 | } 17 | 18 | const hash = await opStackL2PublicClient.simulateWithdrawETH({ 19 | args, 20 | account, 21 | }) 22 | ``` 23 | 24 | ```ts [config.ts] 25 | import { publicL2OpStackActions } from 'op-viem' 26 | import { base } from 'op-viem/chains' 27 | import { createPublicClient, http } from 'viem' 28 | import { privateKeyToAccount } from 'viem/accounts' 29 | 30 | export const opStackL2PublicClient = createPublicClient({ 31 | chain: base, 32 | transport: http(), 33 | }).extend(publicL2OpStackActions) 34 | 35 | // JSON-RPC Account 36 | export const [account] = '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266' 37 | // Local Account 38 | export const account = privateKeyToAccount(...) 39 | ``` 40 | 41 | ::: 42 | 43 | ## Return Value 44 | 45 | The simulation result and write request. Type is inferred. 46 | 47 | ## Parameters 48 | 49 | ### args 50 | 51 | - #### to 52 | - **Type:** [`Address`](https://viem.sh/docs/glossary/types#address) 53 | - The `to` address of the L1 transaction. The L1 address to withdraw the ETH to. 54 | 55 | - #### amount 56 | - **Type:** `bigint` 57 | - Value in wei to withdraw. This is the amount of ETH, specified in wei, that will leave the user's address on L2. 58 | 59 | - #### minGasLimit 60 | - **Type:** `number` 61 | - The minimum gas limit for the L1 transaction. 62 | 63 | - #### extraData (optional) 64 | - **Type:** `Hex` 65 | - **Default:** `0x` 66 | - The data of the L1 transaction 67 | 68 | ```ts 69 | await opStackL2PublicClient.simulateWithdrawETH({ 70 | args: { // [!code focus:6] 71 | to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 72 | amount: 100n, 73 | minGasLimit: 85000, 74 | extraData: '0x123', 75 | }, 76 | }) 77 | ``` 78 | 79 | ::: tip 80 | `account`, `accessList`, `dataSuffix`, `gas`, `gasPrice`, `maxFeePerGas`, `maxPriorityFeePerGas`, `nonce`, `blockNumber`, and `blockTag` can all also be passed and behave as with any viem simulateContract call. See [their documentation](https://viem.sh/docs/contract/simulateContract.html#simulatecontract) for more details. 81 | ::: 82 | -------------------------------------------------------------------------------- /src/actions/public/L1/simulateProveWithdrawalTransaction.ts: -------------------------------------------------------------------------------- 1 | import type { Chain, PublicClient, SimulateContractParameters, SimulateContractReturnType, Transport } from 'viem' 2 | import { simulateContract } from 'viem/actions' 3 | import { type RawOrContractAddress, resolveAddress } from '../../../types/addresses.js' 4 | import { type L1SimulateActionBaseType } from '../../../types/l1Actions.js' 5 | import { 6 | ABI, 7 | FUNCTION, 8 | type ProveWithdrawalTransactionParameters, 9 | } from '../../wallet/L1/writeProveWithdrawalTransaction.js' 10 | 11 | export type SimulateProveWithdrawalTransactionParameters< 12 | TChain extends Chain | undefined = Chain, 13 | TChainOverride extends Chain | undefined = Chain | undefined, 14 | _chainId = TChain extends Chain ? TChain['id'] : number, 15 | > = 16 | & { args: ProveWithdrawalTransactionParameters; portal: RawOrContractAddress<_chainId> } 17 | & L1SimulateActionBaseType< 18 | TChain, 19 | TChainOverride, 20 | typeof ABI, 21 | typeof FUNCTION 22 | > 23 | 24 | export type SimulateProveWithdrawalTransactionReturnType< 25 | TChain extends Chain | undefined, 26 | TChainOverride extends Chain | undefined = undefined, 27 | > = SimulateContractReturnType< 28 | typeof ABI, 29 | typeof FUNCTION, 30 | TChain, 31 | TChainOverride 32 | > 33 | 34 | /** 35 | * Simulates a call to proveWithdrawalTransaction on the OptimismPortal contract. 36 | * Is the first L1 step of a withdrawal. 37 | * 38 | * @param parameters - {@link SimulateProveWithdrawalTransactionParameters} 39 | * @returns A [Transaction Hash](https://viem.sh/docs/glossary/terms.html#hash). {@link WriteContractReturnType} 40 | */ 41 | export async function simulateProveWithdrawalTransaction< 42 | TChain extends Chain | undefined, 43 | TChainOverride extends Chain | undefined = undefined, 44 | >( 45 | client: PublicClient, 46 | { 47 | args: { withdrawalTransaction, outputRootProof, withdrawalProof, L2OutputIndex }, 48 | portal, 49 | ...rest 50 | }: SimulateProveWithdrawalTransactionParameters< 51 | TChain, 52 | TChainOverride 53 | >, 54 | ): Promise> { 55 | return simulateContract(client, { 56 | address: resolveAddress(portal), 57 | abi: ABI, 58 | functionName: FUNCTION, 59 | args: [withdrawalTransaction, L2OutputIndex, outputRootProof, withdrawalProof], 60 | ...rest, 61 | } as unknown as SimulateContractParameters< 62 | typeof ABI, 63 | typeof FUNCTION, 64 | TChain, 65 | TChainOverride 66 | >) 67 | } 68 | -------------------------------------------------------------------------------- /site/docs/utilities/withdrawals/getProof.md: -------------------------------------------------------------------------------- 1 | --- 2 | head: 3 | - - meta 4 | - property: og:title 5 | content: getProof 6 | - - meta 7 | - name: description 8 | content: Generates a proof of account state and storage for a specified Ethereum address at a given block. 9 | 10 | --- 11 | 12 | # getProof 13 | 14 | Generates a proof of account state and storage for a specified Ethereum address at a given block. 15 | 16 | This function is crucial for verifying the state of an Ethereum account, particularly in applications dealing with cross-chain operations, such as withdrawals from Layer 2 to Layer 1 networks. It fetches proofs for given storage keys of an account at a specific block. 17 | 18 | ## Import 19 | 20 | ```ts 21 | import { getProof } from './getProof.js' 22 | ``` 23 | 24 | ## Usage 25 | 26 | This example, adapted from `getProof.test.js`, demonstrates fetching an account's state and storage proof for a specific Ethereum address at a given block. It illustrates a practical application of the `getProof` function. 27 | 28 | ```ts 29 | import { createPublicClient, http, toHex } from 'viem' 30 | import { base } from 'viem/chains' 31 | import { getProof } from './getProof.js' 32 | 33 | // Setting up the client with base chain and HTTP transport 34 | const client = createPublicClient({ 35 | chain: base, 36 | transport: http(), 37 | }) 38 | 39 | // Example usage of getProof to fetch account state and storage proof 40 | const result = await getProof(client, { 41 | address: '0x4200000000000000000000000000000000000016', // Ethereum address 42 | storageKeys: [ 43 | '0x4a932049252365b3eedbc5190e18949f2ec11f39d3bef2d259764799a1b27d99', // Storage key 44 | ], 45 | block: toHex(3155269n), // Block number in hexadecimal 46 | }) 47 | 48 | // The result contains the storage proof for the specified address and block 49 | ``` 50 | 51 | This approach not only provides a clear, practical example of how to use `getProof` but also shows the function in action in a scenario similar to what users might encounter in their own applications. 52 | 53 | ## Parameters 54 | 55 | - `client`: An instance of PublicClient. Responsible for making the request to the Ethereum node. 56 | - `GetProofParameters`: An object containing: 57 | - `address`: The Ethereum address for which to get the proof. 58 | - `storageKeys`: An array of storage keys (Hex) to fetch the storage proof. 59 | - `block`: The block number (Hex or BigInt), tag ('latest', 'earliest', 'pending'), or hash at which to fetch the proof. 60 | 61 | ## Returns 62 | 63 | `AccountProof`: An object containing: 64 | 65 | - `address`: The Ethereum address. 66 | - `accountProof`: Array of hex strings forming the Merkle-Patricia proof of the account's existence and state. 67 | - `balance`: Account's balance at the specified block. 68 | - `nonce`: Account's nonce at the specified block. 69 | - `storageHash`: Hash of the storage root. 70 | - `storageProof`: Array of `StorageProof` objects for each requested storage key. 71 | -------------------------------------------------------------------------------- /src/actions/public/L2/getProveWithdrawalTransactionArgs.ts: -------------------------------------------------------------------------------- 1 | import { type Chain, type Hex, type PublicClient, toHex, type Transport } from 'viem' 2 | import { getBlock } from 'viem/actions' 3 | import { opStackL2ChainContracts } from '../../../index.js' 4 | import type { MessagePassedEvent } from '../../../types/withdrawal.js' 5 | import { getWithdrawalMessageStorageSlot } from '../../../utils/getWithdrawalMessageStorageSlot.js' 6 | import { getProof } from '../getProof.js' 7 | import type { GetOutputForL2BlockReturnType } from '../L1/getOutputForL2Block.js' 8 | 9 | export type OutputRootProof = { 10 | version: Hex 11 | stateRoot: Hex 12 | messagePasserStorageRoot: Hex 13 | latestBlockhash: Hex 14 | } 15 | const OUTPUT_ROOT_PROOF_VERSION = '0x0000000000000000000000000000000000000000000000000000000000000000' 16 | 17 | export type GetProveWithdrawalTransactionArgsParams = { 18 | message: MessagePassedEvent 19 | output: GetOutputForL2BlockReturnType 20 | } 21 | 22 | export type GetProveWithdrawalTransactionArgsReturnType = { 23 | withdrawalTransaction: Omit 24 | L2OutputIndex: bigint 25 | outputRootProof: OutputRootProof 26 | withdrawalProof: Hex[] 27 | } 28 | 29 | /** 30 | * For a given L2 message and output proposal, generates the args needed to call proveWithdrawalTransaction 31 | * on the OptimismPortal contract 32 | * 33 | * @param {MessagePassedEvent} message the MessagePassed event emitted from the withdrawal transaction 34 | * @param {GetOutputForL2BlockReturnType} output the output proposal and index for the L2 block that contained the withdrawal transaction 35 | * @returns {getProveWithdrawalTransactionArgsReturnType} The arguments required by proveWithdrawalTransaction 36 | */ 37 | export async function getProveWithdrawalTransactionArgs< 38 | TChain extends Chain | undefined, 39 | >( 40 | client: PublicClient, 41 | { message, output }: GetProveWithdrawalTransactionArgsParams, 42 | ): Promise { 43 | const slot = getWithdrawalMessageStorageSlot(message.withdrawalHash) 44 | const block = await getBlock(client, { 45 | blockNumber: output.proposal.l2BlockNumber, 46 | }) 47 | if (!block.hash) { 48 | throw new Error( 49 | `Block not found for block number ${output.proposal.l2BlockNumber}`, 50 | ) 51 | } 52 | const proof = await getProof(client, { 53 | address: opStackL2ChainContracts.l2ToL1MessagePasser.address, 54 | storageKeys: [slot], 55 | block: toHex(block.number), 56 | }) 57 | // rome-ignore lint: ok unused variable 58 | const { withdrawalHash, ...withdrawalTransaction } = message 59 | return { 60 | withdrawalTransaction, 61 | outputRootProof: { 62 | version: OUTPUT_ROOT_PROOF_VERSION, 63 | stateRoot: block.stateRoot, 64 | messagePasserStorageRoot: proof.storageHash, 65 | latestBlockhash: block.hash, 66 | }, 67 | withdrawalProof: proof.storageProof[0].proof, 68 | L2OutputIndex: output.outputIndex, 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/actions/wallet/L1/writeDepositTransaction.ts: -------------------------------------------------------------------------------- 1 | import { optimismPortalABI } from '@eth-optimism/contracts-ts' 2 | import type { 3 | Account, 4 | Address, 5 | Chain, 6 | Hex, 7 | Transport, 8 | WalletClient, 9 | WriteContractParameters, 10 | WriteContractReturnType, 11 | } from 'viem' 12 | import { writeContract } from 'viem/actions' 13 | import { type RawOrContractAddress, resolveAddress } from '../../../types/addresses.js' 14 | import type { L1WriteActionBaseType } from '../../../types/l1Actions.js' 15 | 16 | export const ABI = optimismPortalABI 17 | export const FUNCTION = 'depositTransaction' 18 | 19 | export type DepositTransactionParameters = { 20 | to: Address 21 | gasLimit: bigint 22 | value?: bigint 23 | isCreation?: boolean 24 | data?: Hex 25 | mint?: bigint 26 | } 27 | 28 | export type WriteDepositTransactionParameters< 29 | TChain extends Chain | undefined = Chain, 30 | TAccount extends Account | undefined = Account | undefined, 31 | TChainOverride extends Chain | undefined = Chain | undefined, 32 | _chainId = TChain extends Chain ? TChain['id'] : number, 33 | > = 34 | & { args: DepositTransactionParameters; portal: RawOrContractAddress<_chainId> } 35 | & Omit< 36 | L1WriteActionBaseType< 37 | TChain, 38 | TAccount, 39 | TChainOverride 40 | >, 41 | 'value' 42 | > 43 | 44 | /** 45 | * Calls depositTransaction on the OptimismPortal contract. 46 | * 47 | * Unlike writeSendMessage, does not offer replayability on L2 incase the L2 tx fails. 48 | * But has the advantage that, if the caller is an EOA, msg.sender of the L2 tx 49 | * will be the caller address. Allowing users to fully tranasact on L2 from L1, which 50 | * is a critical security property. 51 | * 52 | * If the caller is not an EOA, e.g. if the caller is a smart contract wallet, 53 | * msg.sender on L2 will be alias of the caller address 54 | * https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L1/OptimismPortal.sol#L407 55 | * 56 | * @param parameters - {@link WriteDepositTransactionParameters} 57 | * @returns A [Transaction Hash](https://viem.sh/docs/glossary/terms.html#hash). {@link WriteContractReturnType} 58 | */ 59 | export async function writeDepositTransaction< 60 | TChain extends Chain | undefined, 61 | TAccount extends Account | undefined, 62 | TChainOverride extends Chain | undefined = undefined, 63 | >( 64 | client: WalletClient, 65 | { 66 | args: { to, value = 0n, gasLimit, isCreation = false, data = '0x', mint = 0n }, 67 | portal, 68 | ...rest 69 | }: WriteDepositTransactionParameters< 70 | TChain, 71 | TAccount, 72 | TChainOverride 73 | >, 74 | ): Promise { 75 | return writeContract(client, { 76 | address: resolveAddress(portal), 77 | abi: ABI, 78 | functionName: FUNCTION, 79 | args: [to, value, gasLimit, isCreation, data], 80 | value: mint, 81 | ...rest, 82 | } as unknown as WriteContractParameters< 83 | typeof ABI, 84 | typeof FUNCTION, 85 | TChain, 86 | TAccount, 87 | TChainOverride 88 | >) 89 | } 90 | -------------------------------------------------------------------------------- /src/utils/getTransactionDepositedEvents.test.ts: -------------------------------------------------------------------------------- 1 | import type { TransactionReceipt } from 'viem' 2 | import { expect, test } from 'vitest' 3 | import { getTransactionDepositedEvents } from './getTransactionDepositedEvents.js' 4 | 5 | const txReceipt: TransactionReceipt = { 6 | type: 'eip1559', 7 | transactionHash: '0xe94031c3174788c3fee7216465c50bb2b72e7a1963f5af807b3768da10827f5c', 8 | transactionIndex: 78, 9 | blockHash: '0x9ba3933dc6ce43c145349770a39c30f9b647f17668f004bd2e05c80a2e7262f7', 10 | blockNumber: 17809754n, 11 | from: '0xbc3ed6b537f2980e66f396fe14210a56ba3f72c4', 12 | to: '0x49048044d57e1c92a77f79988d21fa8faf74e97e', 13 | cumulativeGasUsed: 10142442n, 14 | gasUsed: 50764n, 15 | contractAddress: null, 16 | logs: [ 17 | { 18 | removed: false, 19 | logIndex: 196, 20 | transactionIndex: 78, 21 | transactionHash: '0xe94031c3174788c3fee7216465c50bb2b72e7a1963f5af807b3768da10827f5c', 22 | blockHash: '0x9ba3933dc6ce43c145349770a39c30f9b647f17668f004bd2e05c80a2e7262f7', 23 | blockNumber: 17809754n, 24 | address: '0x49048044d57e1c92a77f79988d21fa8faf74e97e', 25 | data: 26 | '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004a0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000001000000000000526c000000000000000000000000000000000000000000000000', 27 | topics: [ 28 | '0xb3813568d9991fc951961fcb4c784893574240a28925604d09fc577c55bb7c32', 29 | '0x000000000000000000000000bc3ed6b537f2980e66f396fe14210a56ba3f72c4', 30 | '0x000000000000000000000000bc3ed6b537f2980e66f396fe14210a56ba3f72c4', 31 | '0x0000000000000000000000000000000000000000000000000000000000000000', 32 | ], 33 | }, 34 | ], 35 | logsBloom: 36 | '0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000800000000000000200000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000800000000040000000000000000000200000000000000000000000000020000000000000000a0000000000000000000000', 37 | status: 'success', 38 | effectiveGasPrice: 15949156605n, 39 | } 40 | 41 | test('returns correct event info', () => { 42 | const events = getTransactionDepositedEvents({ 43 | txReceipt, 44 | }) 45 | 46 | expect(events.length).toEqual(1) 47 | expect(events[0].logIndex).toEqual(196) 48 | expect(events[0].event).toEqual({ 49 | eventName: 'TransactionDeposited', 50 | args: { 51 | from: '0xbc3ed6B537f2980e66f396Fe14210A56ba3f72C4', 52 | to: '0xbc3ed6B537f2980e66f396Fe14210A56ba3f72C4', 53 | version: 0n, 54 | opaqueData: 55 | '0x0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000001000000000000526c0000', 56 | }, 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /src/utils/rlpEncodeDepositTransaction.test.ts: -------------------------------------------------------------------------------- 1 | import { DepositTx } from '@eth-optimism/core-utils' 2 | import { expect, test } from 'vitest' 3 | import type { DepositTransaction } from '../types/depositTransaction.js' 4 | import { rlpEncodeDepositTransaction } from './rlpEncodeDepositTransaction.js' 5 | 6 | const ZERO = '0x0000000000000000000000000000000000000000000000000000000000000000' 7 | const DEPOSIT_TX: DepositTransaction = { 8 | sourceHash: '0x9d7b6db5fcbc23b017d4f179574dfe34b792f60d89bc077ca567af7cc65e8b3e', 9 | from: '0x36BDE71C97B33Cc4729cf772aE268934f7AB70B2', 10 | to: '0x4200000000000000000000000000000000000007', 11 | mint: '0x0000000000000000000000000000000000000000000000000000000000000001', 12 | value: '0x0000000000000000000000000000000000000000000000000000000000000001', 13 | gas: '0x000000000006b8c4', 14 | isSystemTransaction: false, 15 | data: 16 | '0xd764ad0b000100000000000000000000000000000000000000000000000000000000af0a0000000000000000000000006587a6164b091a058acba2e91f971454ec172940000000000000000000000000a81d244a1814468c734e5b4101f7b9c0c577a8fc000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000249f000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000c4cc29a306000000000000000000000000f282ed5de6f51854b4f07f5c1dbc8f178ab8a89b000000000000000000000000000000000000000000000000000000011af9f9f000000000000000000000000000000000000000000000000000000001181336b40000000000000000000000000000000000000000000000000000000064f98c01000000000000000000000000a6a688f107851131f0e1dce493ebbebfaf99203e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', 17 | } 18 | 19 | // adding these tests after we saw some unexpected behavior between ethers and viem methods 20 | // when encoding 21 | test('produces correct encoding', () => { 22 | const coreUtilsDeposit = new DepositTx({ 23 | ...DEPOSIT_TX, 24 | }) 25 | expect(coreUtilsDeposit.encode()).toEqual( 26 | rlpEncodeDepositTransaction(DEPOSIT_TX).toLocaleLowerCase(), 27 | ) 28 | }) 29 | 30 | test('produces correct encoding when mint is 0', () => { 31 | const coreUtilsDeposit = new DepositTx({ 32 | ...DEPOSIT_TX, 33 | mint: ZERO, 34 | }) 35 | expect(coreUtilsDeposit.encode()).toEqual( 36 | rlpEncodeDepositTransaction({ 37 | ...DEPOSIT_TX, 38 | mint: ZERO, 39 | }).toLocaleLowerCase(), 40 | ) 41 | }) 42 | 43 | test('produces correct encoding when value is 0', () => { 44 | const coreUtilsDeposit = new DepositTx({ 45 | ...DEPOSIT_TX, 46 | value: ZERO, 47 | }) 48 | expect(coreUtilsDeposit.encode()).toEqual( 49 | rlpEncodeDepositTransaction({ 50 | ...DEPOSIT_TX, 51 | value: ZERO, 52 | }).toLocaleLowerCase(), 53 | ) 54 | }) 55 | 56 | test('produces correct encoding when gas is 0', () => { 57 | const coreUtilsDeposit = new DepositTx({ 58 | ...DEPOSIT_TX, 59 | gas: ZERO, 60 | }) 61 | expect(coreUtilsDeposit.encode()).toEqual( 62 | rlpEncodeDepositTransaction({ 63 | ...DEPOSIT_TX, 64 | gas: ZERO, 65 | }).toLocaleLowerCase(), 66 | ) 67 | }) 68 | -------------------------------------------------------------------------------- /site/docs/utilities/deposits/getDepositTransaction.md: -------------------------------------------------------------------------------- 1 | --- 2 | head: 3 | - - meta 4 | - property: og:title 5 | content: getDepositTransaction 6 | - - meta 7 | - name: description 8 | content: Returns a deposit transaction from a TransactionDeposited event and sourcehash or logIndex and l1BlockHash. 9 | --- 10 | 11 | # getDepositTransaction 12 | 13 | Returns a [DepositTransaction](/docs/glossary/types#deposittransaction) from a [TransactionDepositedEvent](/docs/glossary/types#transactiondepositedevent) and [SourceHash](/docs/glossary/types#sourcehash) or logIndex, L1 block hash and [SourceHashDomain](/docs/glossary/types#sourcehashdomain). 14 | 15 | Internally, if [`sourceHash`](#sourcehash-optional) not provided, will call [`getSourceHash({ domain, logIndex, l1BlockHash })`](docs/utilities/deposits/getSourceHash). 16 | 17 | ## Import 18 | 19 | ```ts 20 | import { getDepositTransaction } from 'op-viem' 21 | ``` 22 | 23 | ## Usage 24 | 25 | ```ts 26 | import { getDepositTransaction } from 'viem' 27 | 28 | const event: TransactionDepositedEvent = { 29 | eventName: 'TransactionDeposited', 30 | args: { 31 | from: '0xbc3ed6B537f2980e66f396Fe14210A56ba3f72C4', 32 | to: '0xbc3ed6B537f2980e66f396Fe14210A56ba3f72C4', 33 | version: 0n, 34 | opaqueData: 35 | '0x0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000001000000000000526c0000', 36 | }, 37 | } 38 | const logIndex = 196 39 | const blockHash = 40 | '0x9ba3933dc6ce43c145349770a39c30f9b647f17668f004bd2e05c80a2e7262f7' 41 | 42 | // without known source hash 43 | const deposit = getDepositTransaction({ 44 | event: event, 45 | logIndex: logIndex, 46 | l1BlockHash: blockHash, 47 | }) 48 | 49 | // with known source hash 50 | const deposit = getDepositTransaction({ 51 | event: event, 52 | sourceHash: 53 | '0xd0868c8764d81f1749edb7dec4a550966963540d9fe50aefce8cdb38ea7b2213', 54 | }) 55 | ``` 56 | 57 | ## Returns 58 | 59 | [`DepositTransaction`](/docs/glossary/types#deposittransaction) 60 | 61 | The L2 deposit transaction generated by the L1 transaction. Can be rlp encoded and hashed to get the L2 transaction hash. 62 | 63 | ## Parameters 64 | 65 | ### event 66 | 67 | - **Type:** [`TransactionDepositedEvent`](/docs/glossary/types#transactiondepositedevent) 68 | 69 | ### sourceHash (optional) 70 | 71 | - **Type:** [sourceHash](/docs/glossary/types#sourcehash) 72 | 73 | Can be provided instead of [l1BlockHash](#l1blockhash-optional) and [logIndex](#logindex-optional). 74 | 75 | ### logIndex (optional) 76 | 77 | - **Type:** `number` 78 | 79 | The index of the given [`TransactionDepositedEvent`](/docs/glossary/types#transactiondepositedevent) log among all events emitted in the same L1 block. MUST be provided with [l1BlockHash](#l1BlockHash-optional) if sourceHash not passed. 80 | 81 | ### l1BlockHash (optional) 82 | 83 | - **Type:** `Hex` 84 | 85 | The blockhash of the L1 block in which the passed [event](#event) was emitted. MUST be provided with [logIndex](#logIndex-optional) if sourceHash not passed. 86 | 87 | ### domain (optional) 88 | 89 | - **Type:** [SourceHashDomain](/docs/glossary/types#SourceHashDomain) 90 | - **Default** `SourceHashDomain.UserDeposit` 91 | 92 | Note: only `SourceHashDomain.UserDeposit` currently supported. 93 | -------------------------------------------------------------------------------- /src/actions/public/L1/simulateDepositTransaction.test.ts: -------------------------------------------------------------------------------- 1 | import { type Address } from 'viem' 2 | import { estimateGas, mine } from 'viem/actions' 3 | import { expect, test } from 'vitest' 4 | import { accounts } from '../../../_test/constants.js' 5 | import { publicClient, rollupPublicClient, testClient } from '../../../_test/utils.js' 6 | import { baseAddresses } from '../../../chains/index.js' 7 | import { type DepositTransactionParameters } from '../../index.js' 8 | import { simulateDepositTransaction } from './simulateDepositTransaction.js' 9 | 10 | test('default', async () => { 11 | expect( 12 | await simulateDepositTransaction(publicClient, { 13 | args: { 14 | to: '0x0c54fccd2e384b4bb6f2e405bf5cbc15a017aafb', 15 | value: 1n, 16 | gasLimit: 25000n, 17 | data: '0x', 18 | isCreation: false, 19 | }, 20 | value: 0n, 21 | ...baseAddresses, 22 | account: accounts[0].address, 23 | }), 24 | ).toBeDefined() 25 | }) 26 | 27 | test('sends transaction to correct infered address', async () => { 28 | const args: DepositTransactionParameters = { 29 | to: '0x0c54fccd2e384b4bb6f2e405bf5cbc15a017aafb', 30 | value: 1n, 31 | gasLimit: 0n, 32 | data: '0x', 33 | isCreation: false, 34 | } 35 | 36 | const gas = await estimateGas(rollupPublicClient, { 37 | account: accounts[0].address, 38 | to: args.to, 39 | value: args.value, 40 | data: args.data, 41 | }) 42 | 43 | args.gasLimit = gas 44 | 45 | const { request } = await simulateDepositTransaction(publicClient, { 46 | args, 47 | value: 1n, 48 | ...baseAddresses, 49 | account: accounts[0].address, 50 | }) 51 | 52 | expect(request.address).toEqual( 53 | baseAddresses.portal.address, 54 | ) 55 | }) 56 | 57 | test('sends transaction to correct explicit address', async () => { 58 | const portal: Address = '0xbEb5Fc579115071764c7423A4f12eDde41f106Ed' 59 | const { request } = await simulateDepositTransaction(publicClient, { 60 | args: { 61 | to: '0x0c54fccd2e384b4bb6f2e405bf5cbc15a017aafb', 62 | value: 1n, 63 | gasLimit: 25000n, 64 | }, 65 | value: 1n, 66 | portal: portal, 67 | account: accounts[0].address, 68 | }) 69 | 70 | expect(request.address).toEqual(portal) 71 | }) 72 | 73 | test('correctly passes arugments', async () => { 74 | const args: DepositTransactionParameters = { 75 | to: '0x0c54fccd2e384b4bb6f2e405bf5cbc15a017aafb', 76 | value: 1n, 77 | gasLimit: 25000n, 78 | data: '0x', 79 | isCreation: false, 80 | } 81 | 82 | const { request } = await simulateDepositTransaction(publicClient, { 83 | args, 84 | ...baseAddresses, 85 | account: accounts[0].address, 86 | value: 2n, 87 | }) 88 | 89 | await mine(testClient, { blocks: 1 }) 90 | 91 | expect(request.value).toEqual(2n) 92 | }) 93 | 94 | test('errors if portal not passed', async () => { 95 | expect(() => 96 | // @ts-expect-error 97 | simulateDepositTransaction(publicClient, { 98 | args: { 99 | to: '0x0c54fccd2e384b4bb6f2e405bf5cbc15a017aafb', 100 | gasLimit: 25000n, 101 | }, 102 | value: 0n, 103 | account: accounts[0].address, 104 | }) 105 | ).rejects.toThrowError('Invalid address') 106 | }) 107 | -------------------------------------------------------------------------------- /site/docs/actions/wallet/L1/writeContractDeposit.md: -------------------------------------------------------------------------------- 1 | # writeContractDeposit 2 | 3 | Creates an L1 to L2 transaction by depositing into a contract on L2. This function serves as a specialized version of Viem's `writeContract`, adapted for L1 -> L2 transactions. 4 | 5 | ```ts [example.ts] 6 | import { walletL1OpStackActions } from 'op-viem' 7 | import { baseAddresses } from 'op-viem/chains' 8 | import { erc721ABI } from 'wagmi' 9 | 10 | import { createWalletClient } from 'viem' 11 | 12 | const walletClient = createWalletClient({ 13 | chain: mainnet, 14 | transport: http(), 15 | }).extend(walletL1OpStackActions) 16 | 17 | const txHash = await writeContractDeposit(walletClient, { 18 | abi: erc721ABI, 19 | address: '0x6171f829e107f70b58d67594c6b62a7d3eb7f23b', 20 | functionName: 'approve', 21 | args: ['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 2048n], 22 | l2GasLimit: 100000n, 23 | ...baseAddresses, 24 | }) 25 | ``` 26 | 27 | ## Return Value 28 | 29 | Returns a transaction hash for the L2 transaction being initiated. The transaction hash conforms to Viem's [`WriteContractReturnType`](https://viem.sh/docs/glossary/types#writecontractreturntype). 30 | 31 | ## Parameters 32 | 33 | ### abi 34 | 35 | - **Type:** `Abi | readonly unknown[]` 36 | 37 | The ABI (Application Binary Interface) related to the contract. 38 | 39 | ### address 40 | 41 | - **Type:** `Address` 42 | 43 | The contract address on L2 that you are interacting with. 44 | 45 | ### functionName 46 | 47 | - **Type:** `string` 48 | 49 | The contract function name to call on L2. 50 | 51 | ### args 52 | 53 | - **Type:** `Array` 54 | 55 | The arguments to pass to the function. Must match the function signature. 56 | 57 | ### account (Optional) 58 | 59 | - **Type:** `Account | Address` 60 | 61 | Account address initiating the L2 transaction. If not supplied, defaults to `client.account`. 62 | 63 | ### l2GasLimit 64 | 65 | - **Type:** `bigint` 66 | 67 | Gas limit for the L2 transaction. 68 | 69 | ### l2MsgValue (Optional) 70 | 71 | - **Type:** `bigint` 72 | 73 | The Ether value sent along with the L2 transaction. Defaults to `0n`. 74 | 75 | ### strict (Optional) 76 | 77 | - **Type:** `boolean` 78 | 79 | If set to `true`, throws an error when called from a smart contract account. Defaults to `true`. 80 | 81 | ### portal 82 | 83 | - **Type:** [`RawOrContractAddress`](https://opviem.sh/docs/glossary/types.html#raworcontractaddress) 84 | 85 | The `OptimismPortal` contract, or equivalent, facilitating the L1 to L2 transition. 86 | 87 | ## Notes 88 | 89 | - The function will throw an error if `strict` is `true` and the `account` is a smart contract. This is to mitigate unexpected behavior due to [address aliasing](https://github.com/ethereum-optimism/optimism/blob/develop/specs/deposits.md#address-aliasing). 90 | 91 | - The function uses `encodeFunctionData` to create calldata for the L2 transaction, thus requiring ABI, function name, and args. 92 | 93 | - This function wraps around `writeDepositTransaction` and adds additional logic to cater to L2 transactions from L1. 94 | 95 | ## Errors 96 | 97 | - Throws "No account found" if no `account` is supplied and none is set in the `client`. 98 | 99 | - Throws a strict mode error when called from a smart contract account with `strict` set to `true`. 100 | -------------------------------------------------------------------------------- /src/utils/getDepositTransaction.test.ts: -------------------------------------------------------------------------------- 1 | import { optimismPortalABI } from '@eth-optimism/contracts-ts' 2 | import { DepositTx } from '@eth-optimism/core-utils' 3 | import { BigNumber, ethers } from 'ethers' 4 | import { getTransactionReceipt } from 'viem/actions' 5 | import { expect, test } from 'vitest' 6 | import { ethersProvider } from '../_test/bench.js' 7 | import { publicClient } from '../_test/utils.js' 8 | import { optimismAddresses } from '../chains/optimism.js' 9 | import { SourceHashDomain } from '../types/depositTransaction.js' 10 | import { getDepositTransaction } from './getDepositTransaction.js' 11 | import { getSourceHash } from './getSourceHash.js' 12 | import { getTransactionDepositedEvents } from './getTransactionDepositedEvents.js' 13 | 14 | // Simply testing against another implementation is not the best practice 15 | // but I added these after debugging a difference. They will be useful to have 16 | // if debugging again in the future. 17 | test('derives same values as op-ethereum/core-utils', async () => { 18 | const contract = new ethers.Contract( 19 | optimismAddresses.portal.address, 20 | optimismPortalABI, 21 | ethersProvider, 22 | ) 23 | const filter = contract.filters.TransactionDeposited( 24 | '0x36BDE71C97B33Cc4729cf772aE268934f7AB70B2', 25 | '0x4200000000000000000000000000000000000007', 26 | ) 27 | const events = await contract.queryFilter(filter, 18033412, 18033413) 28 | const depositTx = DepositTx.fromL1Event(events[0]) 29 | 30 | const txReceipt = await getTransactionReceipt(publicClient, { 31 | hash: '0x33faeeee9c6d5e19edcdfc003f329c6652f05502ffbf3218d9093b92589a42c4', 32 | }) 33 | const depositEvents = getTransactionDepositedEvents({ txReceipt }) 34 | const opViemTx = getDepositTransaction({ 35 | event: depositEvents[0].event, 36 | sourceHash: getSourceHash({ 37 | domain: SourceHashDomain.UserDeposit, 38 | logIndex: depositEvents[0].logIndex, 39 | l1BlockHash: txReceipt.blockHash, 40 | }), 41 | }) 42 | expect(depositTx.sourceHash()).toEqual(opViemTx.sourceHash) 43 | expect(depositTx.from).toEqual(opViemTx.from) 44 | expect(depositTx.to).toEqual(opViemTx.to) 45 | expect(depositTx.mint).toEqual(BigNumber.from(opViemTx.mint)) 46 | expect(depositTx.gas).toEqual(BigNumber.from(opViemTx.gas)) 47 | expect(depositTx.value).toEqual(BigNumber.from(opViemTx.value)) 48 | expect(depositTx.data).toEqual(opViemTx.data) 49 | expect(depositTx.isSystemTransaction).toEqual(opViemTx.isSystemTransaction) 50 | }) 51 | 52 | test('works with or without sourceHash passed', async () => { 53 | const txReceipt = await getTransactionReceipt(publicClient, { 54 | hash: '0x33faeeee9c6d5e19edcdfc003f329c6652f05502ffbf3218d9093b92589a42c4', 55 | }) 56 | const depositEvents = getTransactionDepositedEvents({ txReceipt }) 57 | 58 | const first = getDepositTransaction({ 59 | event: depositEvents[0].event, 60 | domain: SourceHashDomain.UserDeposit, 61 | logIndex: depositEvents[0].logIndex, 62 | l1BlockHash: txReceipt.blockHash, 63 | }) 64 | 65 | const second = getDepositTransaction({ 66 | event: depositEvents[0].event, 67 | sourceHash: getSourceHash({ 68 | domain: SourceHashDomain.UserDeposit, 69 | logIndex: depositEvents[0].logIndex, 70 | l1BlockHash: txReceipt.blockHash, 71 | }), 72 | }) 73 | 74 | expect(first).toEqual(second) 75 | }) 76 | -------------------------------------------------------------------------------- /src/actions/public/L2/estimateL1GasUsed.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The first 2 test cases are good documentation of how to use this library 3 | */ 4 | import { l2StandardBridgeABI, l2StandardBridgeAddress, optimistABI, optimistAddress } from '@eth-optimism/contracts-ts' 5 | import { createPublicClient, http, parseEther, parseGwei } from 'viem' 6 | import { optimism } from 'viem/chains' 7 | import { beforeEach, expect, test, vi } from 'vitest' 8 | import { estimateL1GasUsed } from './estimateL1GasUsed.js' 9 | 10 | vi.mock('viem', async () => { 11 | const _viem = (await vi.importActual('viem')) as any 12 | return { 13 | ..._viem, 14 | // no way to get historical gas price 15 | createPublicClient: (...args: [any]) => { 16 | const client = _viem.createPublicClient(...args) 17 | client.getGasPrice = async () => parseGwei('0.00000042') 18 | return client 19 | }, 20 | } 21 | }) 22 | 23 | // using this optimist https://optimistic.etherscan.io/tx/0xaa291efba7ea40b0742e5ff84a1e7831a2eb6c2fc35001fa03ba80fd3b609dc9 24 | const blockNumber = BigInt(107028270) 25 | const optimistOwnerAddress = '0x77194aa25a06f932c10c0f25090f3046af2c85a6' as const 26 | const functionDataBurn = { 27 | functionName: 'burn', 28 | // this is an erc721 abi 29 | abi: optimistABI, 30 | args: [BigInt(optimistOwnerAddress)], 31 | account: optimistOwnerAddress, 32 | to: optimistAddress[10], 33 | chainId: 10, 34 | } as const 35 | const functionDataBurnWithPriorityFees = { 36 | ...functionDataBurn, 37 | maxFeePerGas: parseGwei('2'), 38 | maxPriorityFeePerGas: parseGwei('2'), 39 | } as const 40 | // This tx 41 | // https://optimistic.etherscan.io/tx/0xe6f3719be7327a991b9cb562ebf8d979cbca72bbdb2775f55a18274f4d0c9bbf 42 | const functionDataWithdraw = { 43 | abi: l2StandardBridgeABI, 44 | functionName: 'withdraw', 45 | value: BigInt(parseEther('0.00000001')), 46 | account: '0x6387a88a199120aD52Dd9742C7430847d3cB2CD4', 47 | // currently a bug is making chain id 10 not exist 48 | to: l2StandardBridgeAddress[420], 49 | chainId: 10, 50 | args: [ 51 | // l2 token address 52 | '0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000', 53 | // amount 54 | BigInt(parseEther('0.00000001')), 55 | // l1 gas 56 | 0, 57 | // extra data 58 | '0x0', 59 | ], 60 | maxFeePerGas: parseGwei('.2'), 61 | maxPriorityFeePerGas: parseGwei('.1'), 62 | } as const 63 | 64 | const viemClient = createPublicClient({ 65 | chain: optimism, 66 | transport: http(process.env.VITE_L2_RPC_URL ?? 'https://mainnet.optimism.io'), 67 | }) 68 | 69 | const paramsOptimist = { 70 | blockNumber, 71 | } as const 72 | const paramsWithdraw = { 73 | blockNumber: BigInt(107046472), 74 | } as const 75 | 76 | beforeEach(() => { 77 | vi.resetAllMocks() 78 | }) 79 | 80 | test('getL1GasUsed should return the correct result', async () => { 81 | // burn 82 | expect( 83 | await estimateL1GasUsed(viemClient, { ...paramsOptimist, ...functionDataBurn }), 84 | ).toMatchInlineSnapshot('2220n') 85 | expect( 86 | await estimateL1GasUsed(viemClient, { ...paramsOptimist, ...functionDataBurn }), 87 | ).toMatchInlineSnapshot('2220n') 88 | expect( 89 | await estimateL1GasUsed(viemClient, { 90 | ...paramsOptimist, 91 | ...functionDataBurnWithPriorityFees, 92 | }), 93 | ).toMatchInlineSnapshot('2324n') 94 | // withdraw 95 | expect( 96 | await estimateL1GasUsed(viemClient, { ...paramsWithdraw, ...functionDataWithdraw }), 97 | ).toMatchInlineSnapshot('2868n') 98 | }) 99 | -------------------------------------------------------------------------------- /src/decorators/publicL2OpStackActions.ts: -------------------------------------------------------------------------------- 1 | import type { Abi, Chain, PublicClient, Transport } from 'viem' 2 | import { estimateFees, type EstimateFeesParameters } from '../actions/public/L2/estimateFees.js' 3 | import { estimateL1Fee } from '../actions/public/L2/estimateL1Fee.js' 4 | import { estimateL1GasUsed } from '../actions/public/L2/estimateL1GasUsed.js' 5 | import { 6 | getProveWithdrawalTransactionArgs, 7 | type GetProveWithdrawalTransactionArgsParams, 8 | type GetProveWithdrawalTransactionArgsReturnType, 9 | } from '../actions/public/L2/getProveWithdrawalTransactionArgs.js' 10 | import { 11 | getWithdrawalMessages, 12 | type GetWithdrawalMessagesParameters, 13 | type GetWithdrawalMessagesReturnType, 14 | } from '../actions/public/L2/getWithdrawalMessages.js' 15 | import { 16 | simulateWithdrawERC20, 17 | type SimulateWithdrawERC20Parameters, 18 | type SimulateWithdrawERC20ReturnType, 19 | } from '../actions/public/L2/simulateWithdrawERC20.js' 20 | import { 21 | simulateWithdrawETH, 22 | type SimulateWithdrawETHParameters, 23 | type SimulateWithdrawETHReturnType, 24 | } from '../actions/public/L2/simulateWithdrawETH.js' 25 | 26 | import { type OracleTransactionParameters } from '../types/gasPriceOracle.js' 27 | 28 | export type PublicL2OpStackActions = { 29 | /** 30 | * Estimate the l1 gas price portion for a transaction 31 | * @example 32 | * const price = await getL1GasPrice(publicClient, { 33 | * blockNumber, 34 | * blockTag, 35 | * }); 36 | */ 37 | estimateFees: ( 38 | args: EstimateFeesParameters, 39 | ) => Promise 40 | estimateL1Fee: ( 41 | args: OracleTransactionParameters, 42 | ) => Promise 43 | estimateL1GasUsed: ( 44 | args: OracleTransactionParameters, 45 | ) => Promise 46 | 47 | getProveWithdrawalTransactionArgs: ( 48 | args: GetProveWithdrawalTransactionArgsParams, 49 | ) => Promise 50 | getWithdrawalMessages: ( 51 | args: GetWithdrawalMessagesParameters, 52 | ) => Promise 53 | 54 | simulateWithdrawERC20: < 55 | TChainOverride extends Chain | undefined = undefined, 56 | >( 57 | args: SimulateWithdrawERC20Parameters, 58 | ) => Promise< 59 | SimulateWithdrawERC20ReturnType< 60 | TChain, 61 | TChainOverride 62 | > 63 | > 64 | 65 | simulateWithdrawETH: < 66 | TChainOverride extends Chain | undefined = undefined, 67 | >( 68 | args: SimulateWithdrawETHParameters, 69 | ) => Promise< 70 | SimulateWithdrawETHReturnType< 71 | TChain, 72 | TChainOverride 73 | > 74 | > 75 | } 76 | 77 | export function publicL2OpStackActions< 78 | TTransport extends Transport = Transport, 79 | TChain extends Chain | undefined = Chain | undefined, 80 | >( 81 | client: PublicClient, 82 | ): PublicL2OpStackActions { 83 | return { 84 | estimateFees: (args) => estimateFees(client, args), 85 | estimateL1Fee: (args) => estimateL1Fee(client, args), 86 | estimateL1GasUsed: (args) => estimateL1GasUsed(client, args), 87 | 88 | getProveWithdrawalTransactionArgs: (args) => getProveWithdrawalTransactionArgs(client, args), 89 | getWithdrawalMessages: (args) => getWithdrawalMessages(client, args), 90 | 91 | simulateWithdrawERC20: (args) => simulateWithdrawERC20(client, args), 92 | simulateWithdrawETH: (args) => simulateWithdrawETH(client, args), 93 | } 94 | } 95 | --------------------------------------------------------------------------------