diff --git a/.changeset/shaggy-ravens-kiss.md b/.changeset/shaggy-ravens-kiss.md new file mode 100644 index 0000000000..49a29d2730 --- /dev/null +++ b/.changeset/shaggy-ravens-kiss.md @@ -0,0 +1,5 @@ +--- +"viem": minor +--- + +Added zkSync Extensions. diff --git a/site/pages/op-stack/client.md b/site/pages/op-stack/client.md index ef205f4e01..f094079e15 100644 --- a/site/pages/op-stack/client.md +++ b/site/pages/op-stack/client.md @@ -34,7 +34,7 @@ const publicClient = createPublicClient({ const l1Gas = await publicClient.estimateL1Gas({/* ... */}) ``` -## Decorators +## Extensions ### `walletActionsL1` diff --git a/site/pages/zksync.mdx b/site/pages/zksync.mdx new file mode 100644 index 0000000000..8ec2ac16a1 --- /dev/null +++ b/site/pages/zksync.mdx @@ -0,0 +1,74 @@ +--- +description: Getting started with the zkSync in Viem +--- + +# Getting started with zkSync + +Viem provides first-class support for the [zkSync](https://zksync.io) chain. + +zkSync is a Layer-2 protocol that scales Ethereum with cutting-edge ZK tech. + +## Quick Start + +### 1. Set up your Client & Transport + +Firstly, set up your [Client](/docs/clients/intro) with a desired [Transport](/docs/clients/intro) & [zkSync Chain](./zksync/chains.md) and extend it with zkSync EIP712 actions. + +```ts twoslash +import 'viem/window' +// ---cut--- +import { createWalletClient, custom } from 'viem' +import { zkSync } from 'viem/chains' +import { eip712WalletActions } from 'viem/zksync' + +const walletClient = createWalletClient({ // [!code hl] + chain: zkSync, // [!code hl] + transport: custom(window.ethereum!), // [!code hl] +}).extend(eip712WalletActions()) // [!code hl] +``` + +### 2. Use Actions + +Now that you have a Client set up, you can [send a transaction](./zksync/actions/sendTransaction.md) on zkSync using a [paymaster](https://docs.zksync.io/build/developer-reference/account-abstraction.html#paymasters)! + +```ts twoslash +import 'viem/window' +import { createWalletClient, custom } from 'viem' +import { zkSync } from 'viem/chains' +import { eip712WalletActions } from 'viem/zksync' + +const walletClient = createWalletClient({ + chain: zkSync, + transport: custom(window.ethereum!), +}).extend(eip712WalletActions()) +// ---cut--- +const hash = await walletClient.sendTransaction({ + account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n, + paymaster: '0xFD9aE5ebB0F6656f4b77a0E99dCbc5138d54b0BA', + paymasterInput: '0x123abc...' +}) +``` + +...and even write to contracts: + +```ts twoslash +import 'viem/window' +import { createWalletClient, custom, parseAbi } from 'viem' +import { zkSync } from 'viem/chains' +import { eip712WalletActions } from 'viem/zksync' + +const walletClient = createWalletClient({ + account: '0x', + chain: zkSync, + transport: custom(window.ethereum!), +}).extend(eip712WalletActions()) +// ---cut--- +const hash = await walletClient.writeContract({ + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + abi: parseAbi(['function mint(uint32 tokenId) nonpayable']), + functionName: 'mint', + args: [69420], +}) +``` diff --git a/site/pages/zksync/actions/sendTransaction.md b/site/pages/zksync/actions/sendTransaction.md new file mode 100644 index 0000000000..9d2378d1d8 --- /dev/null +++ b/site/pages/zksync/actions/sendTransaction.md @@ -0,0 +1,291 @@ +--- +description: Creates, signs, and sends a new transaction to the network, with EIP712 transaction support. +--- + +# sendTransaction + +Creates, signs, and sends a new transaction to the network, with EIP712 transaction support. + +## Usage + +:::code-group + +```ts [example.ts] +import { account, walletClient } from './config' + +const hash = await walletClient.sendTransaction({ // [!code focus:99] + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n +}) +// '0x...' +``` + +```ts [config.ts] +import { createWalletClient, custom } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { zksync } from 'viem/chains' +import { eip712Actions } from 'viem/zksync' + +export const walletClient = createWalletClient({ + chain: zksync, + transport: custom(window.ethereum) +}).extend(eip712WalletActions()) + +// JSON-RPC Account +export const [account] = await walletClient.getAddresses() +// Local Account +export const account = privateKeyToAccount(...) +``` + +::: + +### Account Hoisting + +If you do not wish to pass an `account` to every `sendTransaction`, you can also hoist the Account on the Wallet Client (see `config.ts`). + +[Learn more](/docs/clients/wallet#account). + +:::code-group + +```ts [example.ts] +import { walletClient } from './config' + +const hash = await walletClient.sendTransaction({ // [!code focus:99] + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n +}) +// '0x...' +``` + +```ts [config.ts (JSON-RPC Account)] +import { createWalletClient, custom } from 'viem' +import { eip712Actions } from 'viem/zksync' + +// Retrieve Account from an EIP-712 Provider. // [!code focus] +const [account] = await window.ethereum.request({ // [!code focus] + method: 'eth_requestAccounts' // [!code focus] +}) // [!code focus] + +export const walletClient = createWalletClient({ + account, + transport: custom(window.ethereum) // [!code focus] +}).extend(eip712WalletActions()) +``` + +```ts [config.ts (Local Account)] +import { createWalletClient, custom } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { eip712Actions } from 'viem/zksync' + +export const walletClient = createWalletClient({ + account: privateKeyToAccount('0x...'), // [!code focus] + transport: custom(window.ethereum) +}).extend(eip712WalletActions()) +``` + +::: + +## Returns + +[`Hash`](/docs/glossary/types#hash) + +The [Transaction](/docs/glossary/terms#transaction) hash. + +## Parameters + +### account + +- **Type:** `Account | Address` + +The Account to send the transaction from. + +Accepts a [JSON-RPC Account](/docs/clients/wallet#json-rpc-accounts) or [Local Account (Private Key, etc)](/docs/clients/wallet#local-accounts-private-key-mnemonic-etc). + +```ts +const hash = await walletClient.sendTransaction({ + account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', // [!code focus] + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n +}) +``` + +### to + +- **Type:** `0x${string}` + +The transaction recipient or contract address. + +```ts +const hash = await walletClient.sendTransaction({ + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', // [!code focus] + value: 1000000000000000000n, + nonce: 69 +}) +``` + +### accessList (optional) + +- **Type:** [`AccessList`](/docs/glossary/types#accesslist) + +The access list. + +```ts +const hash = await walletClient.sendTransaction({ + accessList: [ // [!code focus:6] + { + address: '0x1', + storageKeys: ['0x1'], + }, + ], + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', +}) +``` + +### chain (optional) + +- **Type:** [`Chain`](/docs/glossary/types#chain) +- **Default:** `walletClient.chain` + +The target chain. If there is a mismatch between the wallet's current chain & the target chain, an error will be thrown. + +The chain is also used to infer its request type (e.g. the Celo chain has a `gatewayFee` that you can pass through to `sendTransaction`). + +```ts +import { zksync } from 'viem/chains' // [!code focus] + +const hash = await walletClient.sendTransaction({ + chain: zksync, // [!code focus] + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n +}) +``` + +### data (optional) + +- **Type:** `0x${string}` + +A contract hashed method call with encoded args. + +```ts +const hash = await walletClient.sendTransaction({ + data: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // [!code focus] + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n +}) +``` + +### gasPrice (optional) + +- **Type:** `bigint` + +The price (in wei) to pay per gas. Only applies to [Legacy Transactions](/docs/glossary/terms#legacy-transaction). + +```ts +const hash = await walletClient.sendTransaction({ + account, + gasPrice: parseGwei('20'), // [!code focus] + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n +}) +``` + +### nonce (optional) + +- **Type:** `number` + +Unique number identifying this transaction. + +```ts +const hash = await walletClient.sendTransaction({ + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n, + nonce: 69 // [!code focus] +}) +``` + +### value (optional) + +- **Type:** `bigint` + +Value in wei sent with this transaction. + +```ts +const hash = await walletClient.sendTransaction({ + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1'), // [!code focus] + nonce: 69 +}) +``` + +### gasPerPubdata (optional) + +- **Type:** `bigint` + +The amount of gas for publishing one byte of data on Ethereum. + +```ts +const hash = await walletClient.sendTransaction({ + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + gasPerPubdata: 50000, // [!code focus] + nonce: 69, + value: 1000000000000000000n +}) +``` + +### factoryDeps (optional) + +- **Type:** `[0x${string}]` + +Contains bytecode of the deployed contract. + +```ts +const hash = await walletClient.sendTransaction({ + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + factoryDeps: ['0xcde...'], // [!code focus] + nonce: 69, + value: 1000000000000000000n +}) +``` + +### paymaster (optional) + +- **Type:** `Account | Address` + +Address of the paymaster account that will pay the fees. The `paymasterInput` field is required with this one. + +```ts +const hash = await walletClient.sendTransaction({ + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + paymaster: '0x4B5DF730c2e6b28E17013A1485E5d9BC41Efe021', // [!code focus] + paymasterInput: '0x8c5a...' // [!code focus] + nonce: 69, + value: 1000000000000000000n +}) +``` + +### paymasterInput (optional) + +- **Type:** `0x${string}` + +Input data to the paymaster. The `paymaster` field is required with this one. + +```ts +const hash = await walletClient.sendTransaction({ + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + paymaster: '0x4B5DF730c2e6b28E17013A1485E5d9BC41Efe021', // [!code focus] + paymasterInput: '0x8c5a...' // [!code focus] + nonce: 69, + value: 1000000000000000000n +}) +``` \ No newline at end of file diff --git a/site/pages/zksync/actions/signTransaction.md b/site/pages/zksync/actions/signTransaction.md new file mode 100644 index 0000000000..ed0bfd0b5a --- /dev/null +++ b/site/pages/zksync/actions/signTransaction.md @@ -0,0 +1,295 @@ +--- +description: Signs a transaction, with EIP712 transaction support. +--- + +# signTransaction + +Signs a transaction, with EIP712 transaction support. + +## Usage + +:::code-group + +```ts [example.ts] +import { account, walletClient } from './config' + +const request = await walletClient.prepareTransactionRequest({ + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n +}) + +const signature = await walletClient.signTransaction(request) // [!code focus:2] +// 0x02f850018203118080825208808080c080a04012522854168b27e5dc3d5839bab5e6b39e1a0ffd343901ce1622e3d64b48f1a04e00902ae0502c4728cbf12156290df99c3ed7de85b1dbfe20b5c36931733a33 + +const hash = await walletClient.sendRawTransaction(signature) +``` + +```ts [config.ts] +import { createWalletClient, custom } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { zkSync } from 'viem/chains' +import { eip712WalletActions } from 'viem/zksync' + +export const walletClient = createWalletClient({ + chain: zkSync, + transport: custom(window.ethereum) +}).extend(eip712WalletActions()) + +// JSON-RPC Account +export const [account] = await walletClient.getAddresses() +// Local Account +export const account = privateKeyToAccount(...) +``` + +::: + +### Account Hoisting + +If you do not wish to pass an `account` to every `prepareTransactionRequest`, you can also hoist the Account on the Wallet Client (see `config.ts`). + +[Learn more](/docs/clients/wallet#account). + +:::code-group + +```ts [example.ts] +import { walletClient } from './config' + +const request = await walletClient.prepareTransactionRequest({ + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n +}) + +const signature = await walletClient.signTransaction(request) // [!code focus:2] +// 0x02f850018203118080825208808080c080a04012522854168b27e5dc3d5839bab5e6b39e1a0ffd343901ce1622e3d64b48f1a04e00902ae0502c4728cbf12156290df99c3ed7de85b1dbfe20b5c36931733a33 + +const hash = await client.sendRawTransaction(signature) +``` + +```ts [config.ts (JSON-RPC Account)] +import { createWalletClient, custom } from 'viem' +import { eip712WalletActions } from 'viem/zksync' + +// Retrieve Account from an EIP-712 Provider. // [!code focus] +const [account] = await window.ethereum.request({ // [!code focus] + method: 'eth_requestAccounts' // [!code focus] +}) // [!code focus] + +export const walletClient = createWalletClient({ + account, // [!code focus] + transport: custom(window.ethereum) +}).extend(eip712WalletActions()) +``` + +```ts [config.ts (Local Account)] +import { createWalletClient, custom } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { eip712WalletActions } from 'viem/zksync' + +export const walletClient = createWalletClient({ + account: privateKeyToAccount('0x...'), // [!code focus] + transport: custom(window.ethereum) +}).extend(eip712WalletActions()) +``` + +::: + +## Returns + +[`Hex`](/docs/glossary/types#hex) + +The signed serialized transaction. + +## Parameters + +### account + +- **Type:** `Account | Address` + +The Account to send the transaction from. + +Accepts a [JSON-RPC Account](/docs/clients/wallet#json-rpc-accounts) or [Local Account (Private Key, etc)](/docs/clients/wallet#local-accounts-private-key-mnemonic-etc). + +```ts +const signature = await walletClient.signTransaction({ + account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', // [!code focus] + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n +}) +``` + +### to + +- **Type:** `0x${string}` + +The transaction recipient or contract address. + +```ts +const signature = await walletClient.signTransaction({ + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', // [!code focus] + value: 1000000000000000000n, + nonce: 69 +}) +``` + +### accessList (optional) + +- **Type:** [`AccessList`](/docs/glossary/types#accesslist) + +The access list. + +```ts +const signature = await walletClient.signTransaction({ + accessList: [ // [!code focus:6] + { + address: '0x1', + storageKeys: ['0x1'], + }, + ], + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', +}) +``` + +### chain (optional) + +- **Type:** [`Chain`](/docs/glossary/types#chain) +- **Default:** `walletClient.chain` + +The target chain. If there is a mismatch between the wallet's current chain & the target chain, an error will be thrown. + +The chain is also used to infer its request type (e.g. the Celo chain has a `gatewayFee` that you can pass through to `signTransaction`). + +```ts +import { zksync } from 'viem/chains' // [!code focus] + +const signature = await walletClient.signTransaction({ + chain: zksync, // [!code focus] + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n +}) +``` + +### data (optional) + +- **Type:** `0x${string}` + +A contract hashed method call with encoded args. + +```ts +const signature = await walletClient.signTransaction({ + data: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // [!code focus] + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n +}) +``` + +### gasPrice (optional) + +- **Type:** `bigint` + +The price (in wei) to pay per gas. Only applies to [Legacy Transactions](/docs/glossary/terms#legacy-transaction). + +```ts +const signature = await walletClient.signTransaction({ + account, + gasPrice: parseGwei('20'), // [!code focus] + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1') +}) +``` + +### nonce (optional) + +- **Type:** `number` + +Unique number identifying this transaction. + +```ts +const signature = await walletClient.signTransaction({ + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n, + nonce: 69 // [!code focus] +}) +``` + +### value (optional) + +- **Type:** `bigint` + +Value in wei sent with this transaction. + +```ts +const signature = await walletClient.signTransaction({ + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1'), // [!code focus] + nonce: 69 +}) +``` + +### gasPerPubdata (optional) + +- **Type:** `bigint` + +The amount of gas for publishing one byte of data on Ethereum. + +```ts +const hash = await walletClient.signTransaction({ + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + gasPerPubdata: 50000, // [!code focus] + nonce: 69 +}) +``` + +### factoryDeps (optional) + +- **Type:** `[0x${string}]` + +Contains bytecode of the deployed contract. + +```ts +const hash = await walletClient.signTransaction({ + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + factoryDeps: ['0xcde...'], // [!code focus] + nonce: 69 +}) +``` + +### paymaster (optional) + +- **Type:** `Account | Address` + +Address of the paymaster account that will pay the fees. The `paymasterInput` field is required with this one. + +```ts +const hash = await walletClient.signTransaction({ + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + paymaster: '0x4B5DF730c2e6b28E17013A1485E5d9BC41Efe021', // [!code focus] + paymasterInput: '0x8c5a...' // [!code focus] + nonce: 69 +}) +``` + +### paymasterInput (optional) + +- **Type:** `0x${string}` + +Input data to the paymaster. The `paymaster` field is required with this one. + +```ts +const hash = await walletClient.signTransaction({ + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + paymaster: '0x4B5DF730c2e6b28E17013A1485E5d9BC41Efe021', // [!code focus] + paymasterInput: '0x8c5a...' // [!code focus] + nonce: 69 +}) +``` diff --git a/site/pages/zksync/actions/writeContract.md b/site/pages/zksync/actions/writeContract.md new file mode 100644 index 0000000000..52f2a1fa39 --- /dev/null +++ b/site/pages/zksync/actions/writeContract.md @@ -0,0 +1,330 @@ +--- +description: Executes a write function on a contract, with EIP712 transaction support. +--- + +# writeContract + +Executes a write function on a contract, with EIP712 transaction support. + +## Usage + +:::code-group + +```ts [example.ts] +import { account, walletClient } from './config' + +const hash = await walletClient.writeContract({ // [!code focus:99] + account, + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + abi: wagmiAbi, + functionName: 'mint', +}) +// '0x...' +``` + +```ts [config.ts] +import { createWalletClient, custom } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { zksync } from 'viem/chains' +import { eip712WalletActions } from 'viem/zksync' + +export const walletClient = createWalletClient({ + chain: zksync, + transport: custom(window.ethereum) +}).extend(eip712WalletActions()) + +// JSON-RPC Account +export const [account] = await walletClient.getAddresses() +// Local Account +export const account = privateKeyToAccount(...) +``` + +::: + +### Account Hoisting + +If you do not wish to pass an `account` to every `sendTransaction`, you can also hoist the Account on the Wallet Client (see `config.ts`). + +[Learn more](/docs/clients/wallet#account). + +:::code-group + +```ts [example.ts] +import { walletClient } from './config' + +const hash = await walletClient.writeContract({ // [!code focus:99] + account, + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + abi: wagmiAbi, + functionName: 'mint', +}) +// '0x...' +``` + +```ts [config.ts (JSON-RPC Account)] +import { createWalletClient, custom } from 'viem' +import { eip712WalletActions } from 'viem/zksync' + +// Retrieve Account from an EIP-712 Provider. // [!code focus] +const [account] = await window.ethereum.request({ // [!code focus] + method: 'eth_requestAccounts' // [!code focus] +}) // [!code focus] + +export const walletClient = createWalletClient({ + account, + transport: custom(window.ethereum) // [!code focus] +}).extend(eip712WalletActions()) +``` + +```ts [config.ts (Local Account)] +import { createWalletClient, custom } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { eip712WalletActions } from 'viem/zksync' + +export const walletClient = createWalletClient({ + account: privateKeyToAccount('0x...'), // [!code focus] + transport: custom(window.ethereum) +}).extend(eip712WalletActions()) +``` + +::: + +## Returns + +[`Hash`](/docs/glossary/types#hash) + +The [Transaction](/docs/glossary/terms#transaction) hash. + +## Parameters + +### address + +- **Type:** [`Address`](/docs/glossary/types#address) + +The contract address. + +```ts +await walletClient.writeContract({ + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', // [!code focus] + abi: wagmiAbi, + functionName: 'mint', + args: [69420] +}) +``` + +### abi + +- **Type:** [`Abi`](/docs/glossary/types#abi) + +The contract's ABI. + +```ts +await walletClient.writeContract({ + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + abi: wagmiAbi, // [!code focus] + functionName: 'mint', + args: [69420] +}) +``` + +### functionName + +- **Type:** `string` + +A function to extract from the ABI. + +```ts +await walletClient.writeContract({ + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + abi: wagmiAbi, + functionName: 'mint', // [!code focus] + args: [69420] +}) +``` + +### account + +- **Type:** `Account | Address` + +The Account to send the transaction from. + +Accepts a [JSON-RPC Account](/docs/clients/wallet#json-rpc-accounts) or [Local Account (Private Key, etc)](/docs/clients/wallet#local-accounts-private-key-mnemonic-etc). + +```ts +await walletClient.writeContract({ + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', // [!code focus] + abi: wagmiAbi, + functionName: 'mint', + args: [69420] +}) +``` + +### accessList (optional) + +- **Type:** [`AccessList`](/docs/glossary/types#accesslist) + +The access list. + +```ts +const hash = await walletClient.writeContract({ + accessList: [ // [!code focus:6] + { + address: '0x1', + storageKeys: ['0x1'], + }, + ], + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + abi: wagmiAbi, + functionName: 'mint', + args: [69420] +}) +``` + +### chain (optional) + +- **Type:** [`Chain`](/docs/glossary/types#chain) +- **Default:** `walletClient.chain` + +The target chain. If there is a mismatch between the wallet's current chain & the target chain, an error will be thrown. + +```ts +import { zksync } from 'viem/chains' // [!code focus] + +const hash = await walletClient.writeContract({ + chain: zksync, // [!code focus] + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + abi: wagmiAbi, + functionName: 'mint', + args: [69420] +}) +``` + +### data (optional) + +- **Type:** `0x${string}` + +A contract hashed method call with encoded args. + +```ts +const hash = await walletClient.writeContract({ + data: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // [!code focus] + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + abi: wagmiAbi, + functionName: 'mint', + args: [69420] +}) +``` + +### gasPrice (optional) + +- **Type:** `bigint` + +The price (in wei) to pay per gas. Only applies to [Legacy Transactions](/docs/glossary/terms#legacy-transaction). + +```ts +const hash = await walletClient.writeContract({ + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + abi: wagmiAbi, + functionName: 'mint', + args: [69420], + gasPrice: parseGwei('20'), // [!code focus] +}) +``` + +### nonce (optional) + +- **Type:** `number` + +Unique number identifying this transaction. + +```ts +const hash = await walletClient.writeContract({ + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + abi: wagmiAbi, + functionName: 'mint', + args: [69420], + nonce: 69 // [!code focus] +}) +``` + +### value (optional) + +- **Type:** `bigint` + +Value in wei sent with this transaction. + +```ts +const hash = await walletClient.writeContract({ + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + abi: wagmiAbi, + functionName: 'mint', + args: [69420], + value: parseEther('1'), // [!code focus] +}) +``` + +### gasPerPubdata (optional) + +- **Type:** `bigint` + +The amount of gas for publishing one byte of data on Ethereum. + +```ts +const hash = await walletClient.writeContract({ + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + abi: wagmiAbi, + functionName: 'mint', + args: [69420], + gasPerPubdata: 50000, // [!code focus] +}) +``` + +### factoryDeps (optional) + +- **Type:** `[0x${string}]` + +Contains bytecode of the deployed contract. + +```ts +const hash = await walletClient.writeContract({ + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + abi: wagmiAbi, + functionName: 'mint', + args: [69420], + factoryDeps: ['0xcde...'], // [!code focus] +}) +``` + +### paymaster (optional) + +- **Type:** `Account | Address` + +Address of the paymaster account that will pay the fees. The `paymasterInput` field is required with this one. + +```ts +const hash = await walletClient.writeContract({ + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + abi: wagmiAbi, + functionName: 'mint', + args: [69420], + paymaster: '0x4B5DF730c2e6b28E17013A1485E5d9BC41Efe021', // [!code focus] + paymasterInput: '0x8c5a...' // [!code focus] +}) +``` + +### paymasterInput (optional) + +- **Type:** `0x${string}` + +Input data to the paymaster. The `paymaster` field is required with this one. + +```ts +const hash = await walletClient.writeContract({ + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + abi: wagmiAbi, + functionName: 'mint', + args: [69420], + paymaster: '0x4B5DF730c2e6b28E17013A1485E5d9BC41Efe021', // [!code focus] + paymasterInput: '0x8c5a...' // [!code focus] +}) +``` \ No newline at end of file diff --git a/site/pages/zksync/chains.md b/site/pages/zksync/chains.md new file mode 100644 index 0000000000..1c4d9e4fec --- /dev/null +++ b/site/pages/zksync/chains.md @@ -0,0 +1,26 @@ +# Chains + +The following zkSync chains are supported in Viem: + +```ts twoslash +import { + zkSync, // [!code hl] + zkSyncSepoliaTestnet, // [!code hl] +} from 'viem/chains' +``` + +## Configuration + +Viem exports zkSync's chain [formatters](/docs/chains/formatters) & [serializers](/docs/chains/serializers) via `chainConfig`. This is useful if you need to define another chain which is implemented on zkSync. + +```ts twoslash +// @noErrors +import { defineChain } from 'viem' +import { chainConfig } from 'viem/zkSync' + +export const opStackExample = defineChain({ + ...chainConfig, + name: 'zkSync Example', + // ... +}) +``` \ No newline at end of file diff --git a/site/pages/zksync/client.md b/site/pages/zksync/client.md new file mode 100644 index 0000000000..e0b950a7c1 --- /dev/null +++ b/site/pages/zksync/client.md @@ -0,0 +1,67 @@ +--- +description: Setting up your zkSync Viem Client +--- + +# Client + +To use the zkSync functionality of Viem, you must extend your existing (or new) Viem Client with zkSync Actions. + +## Usage + +```ts twoslash +import 'viem/window' +// ---cut--- +import { createPublicClient, createWalletClient, custom, http } from 'viem' +import { zkSync } from 'viem/chains' +import { eip712WalletActions } from 'viem/zksync' + +const walletClient = createWalletClient({ + chain: zkSync, + transport: custom(window.ethereum!), +}).extend(eip712WalletActions()) // [!code hl] + +const publicClient = createPublicClient({ + chain: zkSync, + transport: http() +}) +``` + +## Extensions + +### `eip712WalletActions` + +A suite of [Wallet Actions](/zksync/actions/sendTransaction) for suited for development with zkSync chains. + +```ts twoslash +import { eip712WalletActions } from 'viem/zksync' +``` + +### Sending transactions using paymaster + +[Read more](./actions/sendTransaction.md) + +```ts +const hash = await walletClient.sendTransaction({ + account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n, + paymaster: '0xFD9aE5ebB0F6656f4b77a0E99dCbc5138d54b0BA', + paymasterInput: '0x123abc...' +}) +``` + +### Calling contracts + +[Read more](../docs/contract/writeContract.md) + +```ts +import { simulateContract } from 'viem/contract' + +const { request } = await publicClient.simulateContract(walletClient, { + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + abi: parseAbi(['function mint(uint32 tokenId) nonpayable']), + functionName: 'mint', + args: [69420], +} +const hash = await walletClient.writeContract(request) +``` diff --git a/site/sidebar.ts b/site/sidebar.ts index af1edb06c4..0a92cc3e7f 100644 --- a/site/sidebar.ts +++ b/site/sidebar.ts @@ -481,7 +481,7 @@ export const sidebar = { }, { text: 'zkSync', - link: '/docs/chains/zksync', + link: '/zksync', }, ], }, @@ -1138,4 +1138,37 @@ export const sidebar = { }, ], }, + '/zksync': { + backLink: true, + items: [ + { + text: 'zkSync', + items: [ + { + text: 'Getting started', + link: '/zksync', + }, + { text: 'Client', link: '/zksync/client' }, + { text: 'Chains', link: '/zksync/chains' }, + ], + }, + { + text: 'Actions', + items: [ + { + text: 'sendTransaction', + link: '/zksync/actions/sendTransaction', + }, + { + text: 'signTransaction', + link: '/zksync/actions/signTransaction', + }, + { + text: 'writeContract', + link: '/zksync/actions/writeContract', + }, + ], + }, + ], + }, } as const satisfies Sidebar diff --git a/site/vocs.config.tsx b/site/vocs.config.tsx index 3e67f4a45f..845497413c 100644 --- a/site/vocs.config.tsx +++ b/site/vocs.config.tsx @@ -200,6 +200,10 @@ export default defineConfig({ text: 'OP Stack', link: '/op-stack', }, + { + text: 'zkSync', + link: '/zksync', + }, ], }, { diff --git a/src/actions/public/getTransactionReceipt.test.ts b/src/actions/public/getTransactionReceipt.test.ts index dc2e6ef14f..1ddc709ff5 100644 --- a/src/actions/public/getTransactionReceipt.test.ts +++ b/src/actions/public/getTransactionReceipt.test.ts @@ -11,7 +11,7 @@ import { parseGwei } from '../../utils/unit/parseGwei.js' import { mine } from '../test/mine.js' import { sendTransaction } from '../wallet/sendTransaction.js' -import type { ZkSyncTransactionReceipt } from '../../chains/zksync/types.js' +import type { ZkSyncTransactionReceipt } from '../../chains/zksync/types/transaction.js' import { wait } from '../../utils/wait.js' import { getBlock } from './getBlock.js' import { getTransaction } from './getTransaction.js' diff --git a/src/chains/zksync/actions/sendEip712Transaction.test.ts b/src/chains/zksync/actions/sendEip712Transaction.test.ts new file mode 100644 index 0000000000..acad1e76a0 --- /dev/null +++ b/src/chains/zksync/actions/sendEip712Transaction.test.ts @@ -0,0 +1,71 @@ +import { expect, test } from 'vitest' + +import { accounts } from '~test/src/constants.js' +import { zkSyncClient } from '~test/src/zksync.js' + +import { privateKeyToAccount } from '../../../accounts/privateKeyToAccount.js' +import type { EIP1193RequestFn } from '../../../index.js' +import type { ZkSyncTransactionRequestEIP712 } from '../index.js' +import { sendEip712Transaction } from './sendEip712Transaction.js' + +const sourceAccount = accounts[0] + +const client = { ...zkSyncClient } + +client.request = (async ({ method, params }) => { + if (method === 'eth_sendRawTransaction') + return '0x9afe47f3d95eccfc9210851ba5f877f76d372514a26b48bad848a07f77c33b87' + if (method == 'eth_estimateGas') return 158774n + return zkSyncClient.request({ method, params } as any) +}) as EIP1193RequestFn + +const base: ZkSyncTransactionRequestEIP712 = { + from: '0x0000000000000000000000000000000000000000', + paymaster: '0xFD9aE5ebB0F6656f4b77a0E99dCbc5138d54b0BA', + paymasterInput: + '0x8c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000', +} + +test('default', async () => { + expect( + await sendEip712Transaction(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + ...base, + }), + ).toMatchInlineSnapshot( + `"0x9afe47f3d95eccfc9210851ba5f877f76d372514a26b48bad848a07f77c33b87"`, + ) +}) + +test('errors: no account', async () => { + await expect(() => + // @ts-expect-error + sendEip712Transaction(client, base), + ).rejects.toThrowErrorMatchingInlineSnapshot(` + [AccountNotFoundError: Could not find an Account to execute with this Action. + Please provide an Account with the \`account\` argument on the Action, or by supplying an \`account\` to the WalletClient. + + Docs: https://viem.sh/docs/actions/wallet/sendTransaction#account + Version: viem@1.0.2] + `) +}) + +test('errors: invalid eip712 tx', async () => { + await expect(() => + sendEip712Transaction(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` + [TransactionExecutionError: Transaction is not an EIP712 transaction. + + Transaction must: + - include \`type: "eip712"\` + - include one of the following: \`customSignature\`, \`paymaster\`, \`paymasterInput\`, \`gasPerPubdata\`, \`factoryDeps\` + + Request Arguments: + chain: zkSync Era (id: 324) + from: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 + + Version: viem@1.0.2] + `) +}) diff --git a/src/chains/zksync/actions/sendEip712Transaction.ts b/src/chains/zksync/actions/sendEip712Transaction.ts new file mode 100644 index 0000000000..932b3dbf03 --- /dev/null +++ b/src/chains/zksync/actions/sendEip712Transaction.ts @@ -0,0 +1,129 @@ +import type { Account } from '../../../accounts/types.js' +import { parseAccount } from '../../../accounts/utils/parseAccount.js' +import { getChainId } from '../../../actions/public/getChainId.js' +import { prepareTransactionRequest } from '../../../actions/wallet/prepareTransactionRequest.js' +import { sendRawTransaction } from '../../../actions/wallet/sendRawTransaction.js' +import type { + SendTransactionErrorType, + SendTransactionParameters as SendTransactionParameters_, + SendTransactionReturnType, +} from '../../../actions/wallet/sendTransaction.js' +import type { Client } from '../../../clients/createClient.js' +import type { Transport } from '../../../clients/transports/createTransport.js' +import { AccountNotFoundError } from '../../../errors/account.js' +import { BaseError } from '../../../errors/base.js' +import type { Chain } from '../../../types/chain.js' +import { assertCurrentChain } from '../../../utils/chain/assertCurrentChain.js' +import { + type GetTransactionErrorParameters, + getTransactionError, +} from '../../../utils/errors/getTransactionError.js' +import { getAction } from '../../../utils/getAction.js' +import { type ChainEIP712 } from '../types/chain.js' +import { assertEip712Request } from '../utils/assertEip712Request.js' +import { signTransaction } from './signTransaction.js' + +export type SendEip712TransactionParameters< + TChain extends ChainEIP712 | undefined = ChainEIP712 | undefined, + TAccount extends Account | undefined = Account | undefined, + TChainOverride extends ChainEIP712 | undefined = ChainEIP712 | undefined, +> = SendTransactionParameters_ + +export type SendEip712TransactionReturnType = SendTransactionReturnType + +export type SendEip712TransactionErrorType = SendTransactionErrorType + +/** + * Creates, signs, and sends a new EIP712 transaction to the network. + * + * @param client - Client to use + * @param parameters - {@link SendEip712TransactionParameters} + * @returns The [Transaction](https://viem.sh/docs/glossary/terms#transaction) hash. {@link SendTransactionReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { zkSync } from 'viem/chains' + * import { sendEip712Transaction } from 'viem/zksync' + * + * const client = createWalletClient({ + * chain: zkSync, + * transport: custom(window.ethereum), + * }) + * const hash = await sendEip712Transaction(client, { + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * value: 1000000000000000000n, + * }) + * + * @example + * // Account Hoisting + * import { createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { zkSync } from 'viem/chains' + * import { sendEip712Transaction } from 'viem/zksync' + * + * const client = createWalletClient({ + * account: privateKeyToAccount('0x…'), + * chain: zkSync, + * transport: http(), + * }) + * + * const hash = await sendEip712Transaction(client, { + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * value: 1000000000000000000n, + * }) + */ +export async function sendEip712Transaction< + TChain extends ChainEIP712 | undefined, + TAccount extends Account | undefined, + TChainOverride extends ChainEIP712 | undefined = undefined, +>( + client: Client, + args: SendEip712TransactionParameters, +): Promise { + const { chain = client.chain } = args + + if (!args.account) + throw new AccountNotFoundError({ + docsPath: '/docs/actions/wallet/sendTransaction', + }) + const account = parseAccount(args.account) + + try { + assertEip712Request(args) + + // Prepare the request for signing (assign appropriate fees, etc.) + const request = await prepareTransactionRequest(client, { + ...args, + parameters: ['gas', 'nonce'], + } as any) + + let chainId: number | undefined + if (chain !== null) { + chainId = await getAction(client, getChainId, 'getChainId')({}) + assertCurrentChain({ + currentChainId: chainId, + chain, + }) + } + + const serializedTransaction = await signTransaction(client, { + ...request, + chainId, + } as any) + + return await getAction( + client, + sendRawTransaction, + 'sendRawTransaction', + )({ + serializedTransaction, + }) + } catch (err) { + throw getTransactionError(err as BaseError, { + ...(args as GetTransactionErrorParameters), + account, + chain: chain as Chain, + }) + } +} diff --git a/src/chains/zksync/actions/sendTransaction.test.ts b/src/chains/zksync/actions/sendTransaction.test.ts new file mode 100644 index 0000000000..4a769cbbf6 --- /dev/null +++ b/src/chains/zksync/actions/sendTransaction.test.ts @@ -0,0 +1,53 @@ +import { expect, test } from 'vitest' + +import { accounts } from '~test/src/constants.js' +import { zkSyncClient } from '~test/src/zksync.js' + +import { privateKeyToAccount } from '../../../accounts/privateKeyToAccount.js' +import type { EIP1193RequestFn, TransactionRequest } from '../../../index.js' +import type { ZkSyncTransactionRequestEIP712 } from '../index.js' +import { sendTransaction } from './sendTransaction.js' + +const sourceAccount = accounts[0] + +const client = { ...zkSyncClient } + +client.request = (async ({ method, params }) => { + if (method === 'eth_sendRawTransaction') + return '0x9afe47f3d95eccfc9210851ba5f877f76d372514a26b48bad848a07f77c33b87' + if (method == 'eth_estimateGas') return 158774n + return zkSyncClient.request({ method, params } as any) +}) as EIP1193RequestFn + +const base: TransactionRequest = { + from: '0x0000000000000000000000000000000000000000', + type: 'eip1559', +} +const eip712: ZkSyncTransactionRequestEIP712 = { + from: '0x0000000000000000000000000000000000000000', + paymaster: '0xFD9aE5ebB0F6656f4b77a0E99dCbc5138d54b0BA', + paymasterInput: + '0x8c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000', +} + +test('eip712', async () => { + expect( + await sendTransaction(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + ...eip712, + }), + ).toMatchInlineSnapshot( + `"0x9afe47f3d95eccfc9210851ba5f877f76d372514a26b48bad848a07f77c33b87"`, + ) +}) + +test('other', async () => { + expect( + await sendTransaction(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + ...base, + }), + ).toMatchInlineSnapshot( + `"0x9afe47f3d95eccfc9210851ba5f877f76d372514a26b48bad848a07f77c33b87"`, + ) +}) diff --git a/src/chains/zksync/actions/sendTransaction.ts b/src/chains/zksync/actions/sendTransaction.ts new file mode 100644 index 0000000000..3e4c89f4f5 --- /dev/null +++ b/src/chains/zksync/actions/sendTransaction.ts @@ -0,0 +1,78 @@ +import type { Account } from '../../../accounts/types.js' +import { sendTransaction as sendTransaction_ } from '../../../actions/wallet/sendTransaction.js' +import type { + SendTransactionErrorType as SendTransactionErrorType_, + SendTransactionParameters as SendTransactionParameters_, + SendTransactionReturnType as SendTransactionReturnType_, +} from '../../../actions/wallet/sendTransaction.js' +import type { Client } from '../../../clients/createClient.js' +import type { Transport } from '../../../clients/transports/createTransport.js' +import { type ChainEIP712 } from '../types/chain.js' +import { isEIP712Transaction } from '../utils/isEip712Transaction.js' +import { sendEip712Transaction } from './sendEip712Transaction.js' + +export type SendTransactionParameters< + TChain extends ChainEIP712 | undefined = ChainEIP712 | undefined, + TAccount extends Account | undefined = Account | undefined, + TChainOverride extends ChainEIP712 | undefined = ChainEIP712 | undefined, +> = SendTransactionParameters_ + +export type SendTransactionReturnType = SendTransactionReturnType_ + +export type SendTransactionErrorType = SendTransactionErrorType_ + +/** + * Creates, signs, and sends a new transaction to the network. + * + * - Docs: https://viem.sh/docs/zksync/actions/sendTransaction + * - JSON-RPC Methods: + * - JSON-RPC Accounts: [`eth_sendTransaction`](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendtransaction) + * - Local Accounts: [`eth_sendRawTransaction`](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendrawtransaction) + * + * @param client - Client to use + * @param parameters - {@link SendTransactionParameters} + * @returns The [Transaction](https://viem.sh/docs/glossary/terms#transaction) hash. {@link SendTransactionReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { zkSync } from 'viem/chains' + * import { sendTransaction } from 'viem/zksync' + * + * const client = createWalletClient({ + * chain: zkSync, + * transport: custom(window.ethereum), + * }) + * const hash = await sendTransaction(client, { + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * value: 1000000000000000000n, + * }) + * + * @example + * // Account Hoisting + * import { createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { zkSync } from 'viem/chains' + * import { sendTransaction } from 'viem/zksync' + * + * const client = createWalletClient({ + * account: privateKeyToAccount('0x…'), + * chain: zkSync, + * transport: http(), + * }) + * const hash = await sendTransaction(client, { + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * value: 1000000000000000000n, + * }) + */ +export async function sendTransaction< + TChain extends ChainEIP712 | undefined, + TAccount extends Account | undefined, + TChainOverride extends ChainEIP712 | undefined = undefined, +>( + client: Client, + args: SendTransactionParameters, +): Promise { + if (isEIP712Transaction(args)) return sendEip712Transaction(client, args) + return sendTransaction_(client, args) +} diff --git a/src/chains/zksync/actions/signEip712Transaction.test.ts b/src/chains/zksync/actions/signEip712Transaction.test.ts new file mode 100644 index 0000000000..e3f8b80da2 --- /dev/null +++ b/src/chains/zksync/actions/signEip712Transaction.test.ts @@ -0,0 +1,75 @@ +import { expect, test } from 'vitest' + +import { accounts } from '~test/src/constants.js' +import { zkSyncClient } from '~test/src/zksync.js' + +import { privateKeyToAccount } from '../../../accounts/privateKeyToAccount.js' +import type { ZkSyncTransactionRequestEIP712 } from '../index.js' +import { signTransaction } from './signTransaction.js' + +const sourceAccount = accounts[0] + +const base: ZkSyncTransactionRequestEIP712 = { + from: '0x0000000000000000000000000000000000000000', + paymaster: '0xFD9aE5ebB0F6656f4b77a0E99dCbc5138d54b0BA', + paymasterInput: + '0x8c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000', +} + +test('default', async () => { + expect( + await signTransaction(zkSyncClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + ...base, + }), + ).toMatchInlineSnapshot( + `"0x71f8c680808080808000820144808082014494000000000000000000000000000000000000000080c0b8412e940527ab264b49c70e9c1bca7636ed2b87f94b5083140c2cbdcd570d99e2c149b697d0c6a037802d0a4757fbd5d1ffd980afccb270e9d182bf7c197bc4a2661bf85b94fd9ae5ebb0f6656f4b77a0e99dcbc5138d54b0bab8448c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"`, + ) +}) + +test('errors: no eip712 domain fn', async () => { + await expect(() => + signTransaction(zkSyncClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + chain: { ...zkSyncClient.chain, custom: {} }, + ...base, + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + ` + [ViemError: \`getEip712Domain\` not found on chain. + + Version: viem@1.0.2] + `, + ) +}) + +test('errors: no serializer fn', async () => { + await expect(() => + signTransaction(zkSyncClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + chain: { ...zkSyncClient.chain, serializers: {} }, + ...base, + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + ` + [ViemError: transaction serializer not found on chain. + + Version: viem@1.0.2] + `, + ) +}) + +test('errors: no account', async () => { + await expect( + // @ts-expect-error + () => signTransaction(zkSyncClient, base), + ).rejects.toThrowErrorMatchingInlineSnapshot( + ` + [AccountNotFoundError: Could not find an Account to execute with this Action. + Please provide an Account with the \`account\` argument on the Action, or by supplying an \`account\` to the WalletClient. + + Docs: https://viem.sh/docs/actions/wallet/signTransaction#account + Version: viem@1.0.2] + `, + ) +}) diff --git a/src/chains/zksync/actions/signEip712Transaction.ts b/src/chains/zksync/actions/signEip712Transaction.ts new file mode 100644 index 0000000000..cd57ec7ef1 --- /dev/null +++ b/src/chains/zksync/actions/signEip712Transaction.ts @@ -0,0 +1,150 @@ +import type { Account } from '../../../accounts/types.js' +import { parseAccount } from '../../../accounts/utils/parseAccount.js' +import { getChainId } from '../../../actions/public/getChainId.js' +import type { + SignTransactionErrorType, + SignTransactionReturnType, +} from '../../../actions/wallet/signTransaction.js' +import { signTypedData } from '../../../actions/wallet/signTypedData.js' +import type { Client } from '../../../clients/createClient.js' +import type { Transport } from '../../../clients/transports/createTransport.js' +import { AccountNotFoundError } from '../../../errors/account.js' +import { BaseError } from '../../../errors/base.js' +import type { GetAccountParameter } from '../../../types/account.js' +import type { + ExtractChainFormatterParameters, + GetChainParameter, +} from '../../../types/chain.js' +import type { UnionOmit } from '../../../types/utils.js' +import { assertCurrentChain } from '../../../utils/chain/assertCurrentChain.js' +import { getAction } from '../../../utils/getAction.js' +import type { ChainEIP712 } from '../types/chain.js' +import type { TransactionRequestEIP712 } from '../types/transaction.js' +import { + type AssertEip712RequestParameters, + assertEip712Request, +} from '../utils/assertEip712Request.js' + +type FormattedTransactionRequest< + TChain extends ChainEIP712 | undefined = ChainEIP712 | undefined, +> = ExtractChainFormatterParameters< + TChain, + 'transactionRequest', + TransactionRequestEIP712 +> + +export type SignEip712TransactionParameters< + TChain extends ChainEIP712 | undefined = ChainEIP712 | undefined, + TAccount extends Account | undefined = Account | undefined, + TChainOverride extends ChainEIP712 | undefined = ChainEIP712 | undefined, +> = UnionOmit< + FormattedTransactionRequest< + TChainOverride extends ChainEIP712 ? TChainOverride : TChain + >, + 'from' +> & + GetAccountParameter & + GetChainParameter + +export type SignEip712TransactionReturnType = SignTransactionReturnType + +export type SignEip712TransactionErrorType = SignTransactionErrorType + +/** + * Signs an EIP712 transaction. + * + * @param args - {@link SignTransactionParameters} + * @returns The signed serialized tranasction. {@link SignTransactionReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { zkSync } from 'viem/chains' + * import { signEip712Transaction } from 'viem/zksync' + * + * const client = createWalletClient({ + * chain: zkSync, + * transport: custom(window.ethereum), + * }) + * const signature = await signEip712Transaction(client, { + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * to: '0x0000000000000000000000000000000000000000', + * value: 1n, + * }) + * + * @example + * // Account Hoisting + * import { createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { zkSync } from 'viem/chains' + * import { signEip712Transaction } from 'viem/zksync' + * + * const client = createWalletClient({ + * account: privateKeyToAccount('0x…'), + * chain: zkSync, + * transport: custom(window.ethereum), + * }) + * const signature = await signEip712Transaction(client, { + * to: '0x0000000000000000000000000000000000000000', + * value: 1n, + * }) + */ +export async function signEip712Transaction< + TChain extends ChainEIP712 | undefined, + TAccount extends Account | undefined, + TChainOverride extends ChainEIP712 | undefined, +>( + client: Client, + args: SignEip712TransactionParameters, +): Promise { + const { + account: account_ = client.account, + chain = client.chain, + ...transaction + } = args + + if (!account_) + throw new AccountNotFoundError({ + docsPath: '/docs/actions/wallet/signTransaction', + }) + const account = parseAccount(account_) + + assertEip712Request({ + account, + chain, + ...(args as AssertEip712RequestParameters), + }) + + if (!chain?.custom?.getEip712Domain) + throw new BaseError('`getEip712Domain` not found on chain.') + if (!chain?.serializers?.transaction) + throw new BaseError('transaction serializer not found on chain.') + + const chainId = await getAction(client, getChainId, 'getChainId')({}) + if (chain !== null) + assertCurrentChain({ + currentChainId: chainId, + chain: chain, + }) + + const eip712Domain = chain?.custom.getEip712Domain({ + ...transaction, + chainId, + from: account.address, + type: 'eip712', + }) + + const customSignature = await signTypedData(client, { + ...eip712Domain, + account, + }) + + return chain?.serializers?.transaction( + { + chainId, + ...transaction, + customSignature, + type: 'eip712', + }, + { r: '0x0', s: '0x0', v: 0n }, + ) +} diff --git a/src/chains/zksync/actions/signTransaction.test.ts b/src/chains/zksync/actions/signTransaction.test.ts new file mode 100644 index 0000000000..0eea0a2862 --- /dev/null +++ b/src/chains/zksync/actions/signTransaction.test.ts @@ -0,0 +1,44 @@ +import { expect, test } from 'vitest' + +import { accounts } from '~test/src/constants.js' +import { zkSyncClient } from '~test/src/zksync.js' + +import { privateKeyToAccount } from '../../../accounts/privateKeyToAccount.js' +import type { TransactionRequest } from '../../../index.js' +import type { ZkSyncTransactionRequestEIP712 } from '../index.js' +import { signTransaction } from './signTransaction.js' + +const sourceAccount = accounts[0] + +const base: TransactionRequest = { + from: '0x0000000000000000000000000000000000000000', + type: 'eip1559', +} +const eip712: ZkSyncTransactionRequestEIP712 = { + from: '0x0000000000000000000000000000000000000000', + paymaster: '0xFD9aE5ebB0F6656f4b77a0E99dCbc5138d54b0BA', + paymasterInput: + '0x8c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000', +} + +test('eip712', async () => { + expect( + await signTransaction(zkSyncClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + ...eip712, + }), + ).toMatchInlineSnapshot( + `"0x71f8c680808080808000820144808082014494000000000000000000000000000000000000000080c0b8412e940527ab264b49c70e9c1bca7636ed2b87f94b5083140c2cbdcd570d99e2c149b697d0c6a037802d0a4757fbd5d1ffd980afccb270e9d182bf7c197bc4a2661bf85b94fd9ae5ebb0f6656f4b77a0e99dcbc5138d54b0bab8448c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"`, + ) +}) + +test('other', async () => { + expect( + await signTransaction(zkSyncClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + ...base, + }), + ).toMatchInlineSnapshot( + `"0x02f84e82014480808080808080c080a0e9ff0193c0db842635d2b50dd8401c95f5fc53aa96b7170e85b5425c7ece8989a07bf8bd7e5a9ef6c8fb32cc7b5fc85f8529c71faab273eb7d00562542cc8b4dc4"`, + ) +}) diff --git a/src/chains/zksync/actions/signTransaction.ts b/src/chains/zksync/actions/signTransaction.ts new file mode 100644 index 0000000000..4baddda57d --- /dev/null +++ b/src/chains/zksync/actions/signTransaction.ts @@ -0,0 +1,95 @@ +import type { Account } from '../../../accounts/types.js' +import { signTransaction as signTransaction_ } from '../../../actions/wallet/signTransaction.js' +import type { + SignTransactionErrorType as SignTransactionErrorType_, + SignTransactionReturnType as SignTransactionReturnType_, +} from '../../../actions/wallet/signTransaction.js' +import type { Client } from '../../../clients/createClient.js' +import type { Transport } from '../../../clients/transports/createTransport.js' +import type { GetAccountParameter } from '../../../types/account.js' +import type { + ExtractChainFormatterParameters, + GetChainParameter, +} from '../../../types/chain.js' +import type { UnionOmit } from '../../../types/utils.js' +import type { ChainEIP712 } from '../types/chain.js' +import type { TransactionRequestEIP712 } from '../types/transaction.js' +import { isEIP712Transaction } from '../utils/isEip712Transaction.js' +import { signEip712Transaction } from './signEip712Transaction.js' + +type FormattedTransactionRequest< + TChain extends ChainEIP712 | undefined = ChainEIP712 | undefined, +> = ExtractChainFormatterParameters< + TChain, + 'transactionRequest', + TransactionRequestEIP712 +> + +export type SignTransactionParameters< + TChain extends ChainEIP712 | undefined = ChainEIP712 | undefined, + TAccount extends Account | undefined = Account | undefined, + TChainOverride extends ChainEIP712 | undefined = ChainEIP712 | undefined, +> = UnionOmit< + FormattedTransactionRequest< + TChainOverride extends ChainEIP712 ? TChainOverride : TChain + >, + 'from' +> & + GetAccountParameter & + GetChainParameter + +export type SignTransactionReturnType = SignTransactionReturnType_ + +export type SignTransactionErrorType = SignTransactionErrorType_ + +/** + * Signs a transaction. + * + * - Docs: https://viem.sh/docs/zksync/actions/signTransaction + * + * @param args - {@link SignTransactionParameters} + * @returns The signed serialized tranasction. {@link SignTransactionReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { zkSync } from 'viem/chains' + * import { signTransaction } from 'viem/zksync' + * + * const client = createWalletClient({ + * chain: zkSync, + * transport: custom(window.ethereum), + * }) + * const signature = await signTransaction(client, { + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * to: '0x0000000000000000000000000000000000000000', + * value: 1n, + * }) + * + * @example + * // Account Hoisting + * import { createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { zkSync } from 'viem/chains' + * import { signTransaction } from 'viem/zksync' + * + * const client = createWalletClient({ + * account: privateKeyToAccount('0x…'), + * chain: zkSync, + * transport: custom(window.ethereum), + * }) + * const signature = await signTransaction(client, { + * to: '0x0000000000000000000000000000000000000000', + * value: 1n, + * }) + */ +export async function signTransaction< + TChain extends ChainEIP712 | undefined, + TAccount extends Account | undefined, + TChainOverride extends ChainEIP712 | undefined, +>( + client: Client, + args: SignTransactionParameters, +): Promise { + if (isEIP712Transaction(args)) return signEip712Transaction(client, args) + return await signTransaction_(client, args as any) +} diff --git a/src/chains/zksync/chainConfig.ts b/src/chains/zksync/chainConfig.ts index 0935189755..1e46b0163a 100644 --- a/src/chains/zksync/chainConfig.ts +++ b/src/chains/zksync/chainConfig.ts @@ -1,7 +1,11 @@ import { formatters } from './formatters.js' import { serializers } from './serializers.js' +import { getEip712Domain } from './utils/getEip712Domain.js' export const chainConfig = { formatters, serializers, + custom: { + getEip712Domain, + }, } as const diff --git a/src/chains/zksync/chains.ts b/src/chains/zksync/chains.ts new file mode 100644 index 0000000000..71af3aeeb1 --- /dev/null +++ b/src/chains/zksync/chains.ts @@ -0,0 +1,3 @@ +export { zkSync } from '../definitions/zkSync.js' +export { zkSyncTestnet } from '../definitions/zkSyncTestnet.js' +export { zkSyncSepoliaTestnet } from '../definitions/zkSyncSepoliaTestnet.js' diff --git a/src/chains/zksync/decorators/eip712.test.ts b/src/chains/zksync/decorators/eip712.test.ts new file mode 100644 index 0000000000..e727109d88 --- /dev/null +++ b/src/chains/zksync/decorators/eip712.test.ts @@ -0,0 +1,91 @@ +import { expect, test } from 'vitest' + +import { greeterContract } from '~test/src/abis.js' +import { accounts } from '~test/src/constants.js' +import { zkSyncClient } from '~test/src/zksync.js' +import { privateKeyToAccount } from '../../../accounts/privateKeyToAccount.js' +import { simulateContract } from '../../../actions/index.js' +import type { EIP1193RequestFn } from '../../../types/eip1193.js' +import { eip712WalletActions } from './eip712.js' + +const zkSyncClient_ = zkSyncClient.extend(eip712WalletActions()) + +test('default', async () => { + expect(eip712WalletActions()(zkSyncClient)).toMatchInlineSnapshot(` + { + "sendTransaction": [Function], + "signTransaction": [Function], + "writeContract": [Function], + } + `) +}) + +test('sendTransaction', async () => { + zkSyncClient_.request = (async ({ method, params }) => { + if (method === 'eth_sendRawTransaction') + return '0x9afe47f3d95eccfc9210851ba5f877f76d372514a26b48bad848a07f77c33b87' + return zkSyncClient.request({ method, params } as any) + }) as EIP1193RequestFn + const client = zkSyncClient_.extend(eip712WalletActions()) + + const result = await client.sendTransaction({ + account: privateKeyToAccount(accounts[0].privateKey), + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + maxFeePerGas: 10000000000n, + maxPriorityFeePerGas: 10000000000n, + gas: 158774n, + value: 10000000000n, + data: '0x0', + paymaster: '0xFD9aE5ebB0F6656f4b77a0E99dCbc5138d54b0BA', + paymasterInput: + '0x8c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000', + type: 'eip712', + gasPerPubdata: 50000n, + }) + expect(result).toBeDefined() +}) + +test('signTransaction', async () => { + const signature = await zkSyncClient_.signTransaction({ + account: privateKeyToAccount(accounts[0].privateKey), + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + maxFeePerGas: 10000000000n, + maxPriorityFeePerGas: 10000000000n, + gas: 158774n, + value: 10000000000n, + data: '0x0', + paymaster: '0xFD9aE5ebB0F6656f4b77a0E99dCbc5138d54b0BA', + paymasterInput: + '0x8c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000', + type: 'eip712', + gasPerPubdata: 50000n, + }) + expect(signature).toBeDefined() +}) + +test('writeContract', async () => { + zkSyncClient_.request = (async ({ method, params }) => { + if (method === 'eth_sendRawTransaction') + return '0x9afe47f3d95eccfc9210851ba5f877f76d372514a26b48bad848a07f77c33b87' + if (method === 'eth_call') return undefined + return zkSyncClient.request({ method, params } as any) + }) as EIP1193RequestFn + const client = zkSyncClient_.extend(eip712WalletActions()) + + const { request } = await simulateContract(client, { + ...greeterContract, + account: privateKeyToAccount(accounts[0].privateKey), + functionName: 'setGreeting', + args: ['Viem ZkSync works!'], + maxFeePerGas: 250000000n, + maxPriorityFeePerGas: 0n, + gas: 158774n, + paymaster: '0xFD9aE5ebB0F6656f4b77a0E99dCbc5138d54b0BA', + paymasterInput: + '0x8c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000', + type: 'eip712', + gasPerPubdata: 50000n, + }) + const tx = await client.writeContract(request) + expect(tx).toBeDefined() +}) diff --git a/src/chains/zksync/decorators/eip712.ts b/src/chains/zksync/decorators/eip712.ts new file mode 100644 index 0000000000..3185648f8b --- /dev/null +++ b/src/chains/zksync/decorators/eip712.ts @@ -0,0 +1,186 @@ +import { writeContract } from '../../../actions/wallet/writeContract.js' +import type { Client } from '../../../clients/createClient.js' +import type { WalletActions } from '../../../clients/decorators/wallet.js' +import type { Transport } from '../../../clients/transports/createTransport.js' +import type { Account } from '../../../types/account.js' +import { + type SendTransactionParameters, + type SendTransactionReturnType, + sendTransaction, +} from '../actions/sendTransaction.js' +import { + type SignTransactionParameters, + type SignTransactionReturnType, + signTransaction, +} from '../actions/signTransaction.js' +import type { ChainEIP712 } from '../types/chain.js' + +export type Eip712WalletActions< + chain extends ChainEIP712 | undefined = ChainEIP712 | undefined, + account extends Account | undefined = Account | undefined, +> = { + /** + * Creates, signs, and sends a new transaction to the network. + * + * - Docs: https://viem.sh/docs/zksync/actions/sendTransaction + * - JSON-RPC Methods: + * - JSON-RPC Accounts: [`eth_sendTransaction`](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendtransaction) + * - Local Accounts: [`eth_sendRawTransaction`](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendrawtransaction) + * + * @param client - Client to use + * @param parameters - {@link SendTransactionParameters} + * @returns The [Transaction](https://viem.sh/docs/glossary/terms#transaction) hash. {@link SendTransactionReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { zkSync } from 'viem/chains' + * import { eip712WalletActions } from 'viem/zksync' + * + * const client = createWalletClient({ + * chain: zkSync, + * transport: custom(window.ethereum), + * }).extend(eip712WalletActions()) + * const hash = await client.sendTransaction({ + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * value: 1000000000000000000n, + * }) + * + * @example + * // Account Hoisting + * import { createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { zkSync } from 'viem/chains' + * import { eip712WalletActions } from 'viem/zksync' + * + * const client = createWalletClient({ + * account: privateKeyToAccount('0x…'), + * chain: zkSync, + * transport: http(), + * }).extend(eip712WalletActions()) + * const hash = await client.sendTransaction({ + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * value: 1000000000000000000n, + * }) + */ + sendTransaction: ( + args: SendTransactionParameters, + ) => Promise + /** + * Signs a transaction. + * + * - Docs: https://viem.sh/docs/actions/wallet/signTransaction + * - JSON-RPC Methods: + * - JSON-RPC Accounts: [`eth_signTransaction`](https://ethereum.github.io/execution-apis/api-documentation/) + * - Local Accounts: Signs locally. No JSON-RPC request. + * + * @param args - {@link SignTransactionParameters} + * @returns The signed serialized tranasction. {@link SignTransactionReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { zkSync } from 'viem/chains' + * import { eip712WalletActions } from 'viem/zksync' + * + * const client = createWalletClient({ + * chain: zkSync, + * transport: custom(window.ethereum), + * }).extend(eip712WalletActions()) + * const signature = await client.signTransaction({ + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * to: '0x0000000000000000000000000000000000000000', + * value: 1n, + * }) + * + * @example + * // Account Hoisting + * import { createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { zkSync } from 'viem/chains' + * import { eip712WalletActions } from 'viem/zksync' + * + * const client = createWalletClient({ + * account: privateKeyToAccount('0x…'), + * chain: zkSync, + * transport: custom(window.ethereum), + * }).extend(eip712WalletActions()) + * const signature = await client.signTransaction({ + * to: '0x0000000000000000000000000000000000000000', + * value: 1n, + * }) + */ + signTransaction: ( + args: SignTransactionParameters, + ) => Promise + /** + * Executes a write function on a contract. + * + * - Docs: https://viem.sh/docs/contract/writeContract + * - Examples: https://stackblitz.com/github/wevm/viem/tree/main/examples/contracts/writing-to-contracts + * + * A "write" function on a Solidity contract modifies the state of the blockchain. These types of functions require gas to be executed, and hence a [Transaction](https://viem.sh/docs/glossary/terms) is needed to be broadcast in order to change the state. + * + * Internally, uses a [Wallet Client](https://viem.sh/docs/clients/wallet) to call the [`sendTransaction` action](https://viem.sh/docs/actions/wallet/sendTransaction) with [ABI-encoded `data`](https://viem.sh/docs/contract/encodeFunctionData). + * + * __Warning: The `write` internally sends a transaction – it does not validate if the contract write will succeed (the contract may throw an error). It is highly recommended to [simulate the contract write with `contract.simulate`](https://viem.sh/docs/contract/writeContract#usage) before you execute it.__ + * + * @param client - Client to use + * @param parameters - {@link WriteContractParameters} + * @returns A [Transaction Hash](https://viem.sh/docs/glossary/terms#hash). {@link WriteContractReturnType} + * + * @example + * import { createWalletClient, custom, parseAbi } from 'viem' + * import { zkSync } from 'viem/chains' + * import { eip712WalletActions } from 'viem/zksync' + * + * const client = createWalletClient({ + * chain: zkSync, + * transport: custom(window.ethereum), + * }).extend(eip712WalletActions()) + * const hash = await client.writeContract({ + * address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + * abi: parseAbi(['function mint(uint32 tokenId) nonpayable']), + * functionName: 'mint', + * args: [69420], + * }) + * + * @example + * // With Validation + * import { createWalletClient, http, parseAbi } from 'viem' + * import { zkSync } from 'viem/chains' + * import { eip712WalletActions } from 'viem/zksync' + * + * const client = createWalletClient({ + * chain: zkSync, + * transport: http(), + * }).extend(eip712WalletActions()) + * const { request } = await client.simulateContract({ + * address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + * abi: parseAbi(['function mint(uint32 tokenId) nonpayable']), + * functionName: 'mint', + * args: [69420], + * } + * const hash = await client.writeContract(request) + */ + writeContract: WalletActions['writeContract'] +} + +export function eip712WalletActions() { + return < + transport extends Transport, + chain extends ChainEIP712 | undefined = ChainEIP712 | undefined, + account extends Account | undefined = Account | undefined, + >( + client: Client, + ): Eip712WalletActions => ({ + sendTransaction: (args) => sendTransaction(client, args), + signTransaction: (args) => signTransaction(client, args), + writeContract: (args) => + writeContract( + Object.assign(client, { + sendTransaction: (args: any) => sendTransaction(client, args), + }), + args, + ), + }) +} diff --git a/src/chains/zksync/errors/transaction.ts b/src/chains/zksync/errors/transaction.ts new file mode 100644 index 0000000000..889ccc1d02 --- /dev/null +++ b/src/chains/zksync/errors/transaction.ts @@ -0,0 +1,21 @@ +import { BaseError } from '../../../errors/base.js' + +export type InvalidEip712TransactionErrorType = + InvalidEip712TransactionError & { + name: 'InvalidEip712TransactionError' + } +export class InvalidEip712TransactionError extends BaseError { + override name = 'InvalidEip712TransactionError' + + constructor() { + super( + [ + 'Transaction is not an EIP712 transaction.', + '', + 'Transaction must:', + ' - include `type: "eip712"`', + ' - include one of the following: `customSignature`, `paymaster`, `paymasterInput`, `gasPerPubdata`, `factoryDeps`', + ].join('\n'), + ) + } +} diff --git a/src/chains/zksync/formatters.test-d.ts b/src/chains/zksync/formatters.test-d.ts index 336c12cc44..78df74c0de 100644 --- a/src/chains/zksync/formatters.test-d.ts +++ b/src/chains/zksync/formatters.test-d.ts @@ -16,16 +16,15 @@ import type { Hash } from '../../types/misc.js' import type { RpcBlock, RpcTransactionReceipt } from '../../types/rpc.js' import type { TransactionRequest } from '../../types/transaction.js' import type { Assign } from '../../types/utils.js' -import { zkSync, zkSyncSepoliaTestnet } from '../index.js' +import { zkSync } from '../index.js' import { formatters } from './formatters.js' +import type { ZkSyncEip712Meta } from './types/eip712.js' +import type { ZkSyncL2ToL1Log, ZkSyncLog } from './types/log.js' import type { - ZkSyncEip712Meta, - ZkSyncL2ToL1Log, - ZkSyncLog, ZkSyncRpcTransaction, ZkSyncRpcTransactionReceiptOverrides, ZkSyncTransactionRequest, -} from './types.js' +} from './types/transaction.js' describe('block', () => { expectTypeOf(formatters.block.format).parameter(0).toEqualTypeOf< @@ -173,7 +172,7 @@ describe('smoke', () => { test('transactionRequest (prepareTransactionRequest)', async () => { const client = createWalletClient({ account: privateKeyToAccount(accounts[0].privateKey), - chain: zkSyncSepoliaTestnet, + chain: zkSync, transport: http(), }) @@ -191,7 +190,7 @@ describe('smoke', () => { test('transactionRequest (sendTransaction)', async () => { const client = createWalletClient({ account: privateKeyToAccount(accounts[0].privateKey), - chain: zkSyncSepoliaTestnet, + chain: zkSync, transport: http(), }) diff --git a/src/chains/zksync/formatters.test.ts b/src/chains/zksync/formatters.test.ts index 9eab9fa362..c58f998ecc 100644 --- a/src/chains/zksync/formatters.test.ts +++ b/src/chains/zksync/formatters.test.ts @@ -438,6 +438,61 @@ describe('transaction', () => { "yParity": 1, } `) + + expect( + transaction.format({ + accessList: [], + blockHash: + '0xf24f67fb9f8fb300164045fe6ba409acb03904e680ec7df41ed2d331dc38f545', + blockNumber: '0x1', + chainId: '0x104', + from: '0x36615cf349d7f6344891b1e7ca7c72883f5dc049', + gas: '0x0', + gasPrice: undefined, + hash: '0xf24f67fb9f8fb300164045fe6ba409acb03904e680ec7df41ed2d331dc38f545', + input: + '0x02f87582010480840ee6b280840ee6b2808312b84b94a61464658afeaf65cccaafd3a512b69a83b77618880de0b6b3a764000080c080a08ab03d8a1aa4ab231867d9b12a1d7ebacaec3395cf9c4940674f83d79e342e4ca0475dda75d501e72fd816a9699f02af05ef7305668ee4acd0e25561d4628758a3', + l1BatchNumber: null, + maxFeePerGas: '0xee6b280', + maxPriorityFeePerGas: '0xee6b280', + nonce: '0x0', + r: '0x0', + s: '0x0', + to: '0xa61464658afeaf65cccaafd3a512b69a83b77618', + transactionIndex: '0x1', + type: '0x2', + v: '0x104', + yParity: '0x1', + value: '0xde0b6b3a7640000', + l1BatchTxIndex: null, + }), + ).toMatchInlineSnapshot(` + { + "accessList": [], + "blockHash": "0xf24f67fb9f8fb300164045fe6ba409acb03904e680ec7df41ed2d331dc38f545", + "blockNumber": 1n, + "chainId": 260, + "from": "0x36615cf349d7f6344891b1e7ca7c72883f5dc049", + "gas": 0n, + "gasPrice": undefined, + "hash": "0xf24f67fb9f8fb300164045fe6ba409acb03904e680ec7df41ed2d331dc38f545", + "input": "0x02f87582010480840ee6b280840ee6b2808312b84b94a61464658afeaf65cccaafd3a512b69a83b77618880de0b6b3a764000080c080a08ab03d8a1aa4ab231867d9b12a1d7ebacaec3395cf9c4940674f83d79e342e4ca0475dda75d501e72fd816a9699f02af05ef7305668ee4acd0e25561d4628758a3", + "l1BatchNumber": null, + "l1BatchTxIndex": null, + "maxFeePerGas": 250000000n, + "maxPriorityFeePerGas": 250000000n, + "nonce": 0, + "r": "0x0", + "s": "0x0", + "to": "0xa61464658afeaf65cccaafd3a512b69a83b77618", + "transactionIndex": 1, + "type": "eip1559", + "typeHex": "0x2", + "v": 260n, + "value": 1000000000000000000n, + "yParity": 1, + } + `) }) test('action - Priority', async () => { @@ -1633,7 +1688,7 @@ describe('transactionRequest', () => { "maxFeePerGas": "0x2", "maxPriorityFeePerGas": "0x1", "nonce": "0x4", - "type": "0xff", + "type": "0x71", "value": "0x0", } `) diff --git a/src/chains/zksync/formatters.ts b/src/chains/zksync/formatters.ts index bf4f480f9c..bed4b5270e 100644 --- a/src/chains/zksync/formatters.ts +++ b/src/chains/zksync/formatters.ts @@ -10,16 +10,17 @@ import { defineTransactionReceipt } from '../../utils/formatters/transactionRece import { defineTransactionRequest } from '../../utils/formatters/transactionRequest.js' import type { ZkSyncBlockOverrides, - ZkSyncL2ToL1Log, - ZkSyncLog, ZkSyncRpcBlockOverrides, +} from './types/block.js' +import type { ZkSyncL2ToL1Log, ZkSyncLog } from './types/log.js' +import type { ZkSyncRpcTransaction, ZkSyncRpcTransactionReceiptOverrides, ZkSyncRpcTransactionRequest, ZkSyncTransaction, ZkSyncTransactionReceipt, ZkSyncTransactionRequest, -} from './types.js' +} from './types/transaction.js' export const formatters = { block: /*#__PURE__*/ defineBlock({ @@ -32,7 +33,7 @@ export const formatters = { } { const transactions = args.transactions?.map((transaction) => { if (typeof transaction === 'string') return transaction - const formatted = formatters.transaction.format( + const formatted = formatters.transaction?.format( transaction as ZkSyncRpcTransaction, ) as ZkSyncTransaction if (formatted.typeHex === '0x71') formatted.type = 'eip712' @@ -140,7 +141,7 @@ export const formatters = { } : {}), }, - type: args.type === 'eip712' ? '0x71' : '0xff', + type: '0x71', } as ZkSyncRpcTransactionRequest return {} as ZkSyncRpcTransactionRequest }, diff --git a/src/chains/zksync/index.ts b/src/chains/zksync/index.ts index 0a2749b7d8..328640d45d 100644 --- a/src/chains/zksync/index.ts +++ b/src/chains/zksync/index.ts @@ -1,18 +1,65 @@ +export { + type SendTransactionErrorType, + type SendTransactionParameters, + type SendTransactionReturnType, + sendTransaction, +} from './actions/sendTransaction.js' +export { + type SendEip712TransactionErrorType, + type SendEip712TransactionParameters, + type SendEip712TransactionReturnType, + sendEip712Transaction, +} from './actions/sendEip712Transaction.js' +export { + type SignEip712TransactionErrorType, + type SignEip712TransactionParameters, + type SignEip712TransactionReturnType, + signEip712Transaction, +} from './actions/signEip712Transaction.js' +export { + type SignTransactionErrorType, + type SignTransactionParameters, + type SignTransactionReturnType, + signTransaction, +} from './actions/signTransaction.js' + +export { + zkSync, + zkSyncTestnet, + zkSyncSepoliaTestnet, +} from './chains.js' + export { chainConfig } from './chainConfig.js' +export { + eip712WalletActions, + type Eip712WalletActions, +} from './decorators/eip712.js' + export { serializeTransaction } from './serializers.js' export type { ZkSyncBlock, ZkSyncBlockOverrides, + ZkSyncRpcBlock, + ZkSyncRpcBlockOverrides, +} from './types/block.js' +export type { ChainEIP712 } from './types/chain.js' +export type { + EIP712Domain, + EIP712DomainFn, ZkSyncEip712Meta, - ZkSyncFeeValues, +} from './types/eip712.js' +export type { ZkSyncFeeValues } from './types/fee.js' +export type { ZkSyncL2ToL1Log, ZkSyncLog, - ZkSyncRpcBlock, - ZkSyncRpcBlockOverrides, ZkSyncRpcL2ToL1Log, ZkSyncRpcLog, +} from './types/log.js' +export type { + TransactionRequestEIP712, + ZkSyncEIP712TransactionSignable, ZkSyncRpcTransaction, ZkSyncRpcTransactionEIP712, ZkSyncRpcTransactionPriority, @@ -30,4 +77,4 @@ export type { ZkSyncTransactionSerialized, ZkSyncTransactionSerializedEIP712, ZkSyncTransactionType, -} from './types.js' +} from './types/transaction.js' diff --git a/src/chains/zksync/serializers.test.ts b/src/chains/zksync/serializers.test.ts index 8bbe759701..e3922f3ef8 100644 --- a/src/chains/zksync/serializers.test.ts +++ b/src/chains/zksync/serializers.test.ts @@ -10,13 +10,13 @@ import { parseGwei, parseTransaction, } from '../../index.js' -import { zkSyncSepoliaTestnet } from '../index.js' +import { zkSync } from '../index.js' import { serializeTransaction } from './serializers.js' -import type { ZkSyncTransactionSerializableEIP712 } from './types.js' +import type { ZkSyncTransactionSerializableEIP712 } from './types/transaction.js' const baseTransaction: TransactionSerializableEIP1559 = { to: '0x111C3E89Ce80e62EE88318C2804920D4c96f92bb', - chainId: zkSyncSepoliaTestnet.id, + chainId: zkSync.id, nonce: 7, maxFeePerGas: 250000000n, maxPriorityFeePerGas: 2n, @@ -37,150 +37,148 @@ const baseEip712: ZkSyncTransactionSerializableEIP712 = { type: 'eip712', } -describe('ZkSync - EIP712', () => { - test('should be able to serializer a ZkSync EIP712 transaction', () => { - const transaction: ZkSyncTransactionSerializableEIP712 = { - ...baseEip712, - gas: 158774n, - } +test('default', () => { + const transaction: ZkSyncTransactionSerializableEIP712 = { + ...baseEip712, + gas: 158774n, + } - expect(serializeTransaction(transaction)).toMatchInlineSnapshot( - `"0x71f901500702840ee6b28083026c3694111c3e89ce80e62ee88318c2804920d4c96f92bb880de0b6b3a7640000b864a413686200000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001790000000000000000000000000000000000000000000000000000000000000082012c808082012c94f760bdd822fccf93c44be68d94c45133002b303782c350c0b841d2312deb1e84f7733a83ae1fc55f9cc1f2334fe472e0a494781933b194e173a45d927e67b9222b92467660849efb055422f133cf67588cbcb1874901d3244ddd1cf85b944b5df730c2e6b28e17013a1485e5d9bc41efe021b8448c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"`, - ) - }) + expect(serializeTransaction(transaction)).toMatchInlineSnapshot( + `"0x71f901500702840ee6b28083026c3694111c3e89ce80e62ee88318c2804920d4c96f92bb880de0b6b3a7640000b864a4136862000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000017900000000000000000000000000000000000000000000000000000000000000820144808082014494f760bdd822fccf93c44be68d94c45133002b303782c350c0b841d2312deb1e84f7733a83ae1fc55f9cc1f2334fe472e0a494781933b194e173a45d927e67b9222b92467660849efb055422f133cf67588cbcb1874901d3244ddd1cf85b944b5df730c2e6b28e17013a1485e5d9bc41efe021b8448c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"`, + ) +}) - test('gas', () => { - const transaction: ZkSyncTransactionSerializableEIP712 = { - ...baseEip712, - gas: 69420n, - } +test('gas', () => { + const transaction: ZkSyncTransactionSerializableEIP712 = { + ...baseEip712, + gas: 69420n, + } - expect(serializeTransaction(transaction)).toMatchInlineSnapshot( - `"0x71f901500702840ee6b28083010f2c94111c3e89ce80e62ee88318c2804920d4c96f92bb880de0b6b3a7640000b864a413686200000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001790000000000000000000000000000000000000000000000000000000000000082012c808082012c94f760bdd822fccf93c44be68d94c45133002b303782c350c0b841d2312deb1e84f7733a83ae1fc55f9cc1f2334fe472e0a494781933b194e173a45d927e67b9222b92467660849efb055422f133cf67588cbcb1874901d3244ddd1cf85b944b5df730c2e6b28e17013a1485e5d9bc41efe021b8448c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"`, - ) - }) + expect(serializeTransaction(transaction)).toMatchInlineSnapshot( + `"0x71f901500702840ee6b28083010f2c94111c3e89ce80e62ee88318c2804920d4c96f92bb880de0b6b3a7640000b864a4136862000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000017900000000000000000000000000000000000000000000000000000000000000820144808082014494f760bdd822fccf93c44be68d94c45133002b303782c350c0b841d2312deb1e84f7733a83ae1fc55f9cc1f2334fe472e0a494781933b194e173a45d927e67b9222b92467660849efb055422f133cf67588cbcb1874901d3244ddd1cf85b944b5df730c2e6b28e17013a1485e5d9bc41efe021b8448c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"`, + ) +}) - test('gasPerPubdata', () => { - const transaction: ZkSyncTransactionSerializableEIP712 = { - ...baseTransaction, - from: '0xf760bdd822fccf93c44be68d94c45133002b3037', - gasPerPubdata: 50000n, - type: 'eip712', - } +test('gasPerPubdata', () => { + const transaction: ZkSyncTransactionSerializableEIP712 = { + ...baseTransaction, + from: '0xf760bdd822fccf93c44be68d94c45133002b3037', + gasPerPubdata: 50000n, + type: 'eip712', + } - expect(serializeTransaction(transaction)).toMatchInlineSnapshot( - `"0x71f8af0702840ee6b2808094111c3e89ce80e62ee88318c2804920d4c96f92bb880de0b6b3a7640000b864a413686200000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001790000000000000000000000000000000000000000000000000000000000000082012c808082012c94f760bdd822fccf93c44be68d94c45133002b303782c350c080c0"`, - ) - }) + expect(serializeTransaction(transaction)).toMatchInlineSnapshot( + `"0x71f8af0702840ee6b2808094111c3e89ce80e62ee88318c2804920d4c96f92bb880de0b6b3a7640000b864a4136862000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000017900000000000000000000000000000000000000000000000000000000000000820144808082014494f760bdd822fccf93c44be68d94c45133002b303782c350c080c0"`, + ) +}) - test('factoryDeps', () => { - const transaction: ZkSyncTransactionSerializableEIP712 = { - ...baseTransaction, - from: '0xf760bdd822fccf93c44be68d94c45133002b3037', - factoryDeps: ['0xABCDEF', '0x123456'], - type: 'eip712', - } +test('factoryDeps', () => { + const transaction: ZkSyncTransactionSerializableEIP712 = { + ...baseTransaction, + from: '0xf760bdd822fccf93c44be68d94c45133002b3037', + factoryDeps: ['0xABCDEF', '0x123456'], + type: 'eip712', + } - expect(serializeTransaction(transaction)).toMatchInlineSnapshot( - `"0x71f8b50702840ee6b2808094111c3e89ce80e62ee88318c2804920d4c96f92bb880de0b6b3a7640000b864a413686200000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001790000000000000000000000000000000000000000000000000000000000000082012c808082012c94f760bdd822fccf93c44be68d94c45133002b303780c883abcdef8312345680c0"`, - ) - }) + expect(serializeTransaction(transaction)).toMatchInlineSnapshot( + `"0x71f8b50702840ee6b2808094111c3e89ce80e62ee88318c2804920d4c96f92bb880de0b6b3a7640000b864a4136862000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000017900000000000000000000000000000000000000000000000000000000000000820144808082014494f760bdd822fccf93c44be68d94c45133002b303780c883abcdef8312345680c0"`, + ) +}) - test('customSignature', () => { - const transaction: ZkSyncTransactionSerializableEIP712 = { - ...baseTransaction, - from: '0xf760bdd822fccf93c44be68d94c45133002b3037', - customSignature: '0xABCDEF', - type: 'eip712', - } +test('customSignature', () => { + const transaction: ZkSyncTransactionSerializableEIP712 = { + ...baseTransaction, + from: '0xf760bdd822fccf93c44be68d94c45133002b3037', + customSignature: '0xABCDEF', + type: 'eip712', + } - expect(serializeTransaction(transaction)).toMatchInlineSnapshot( - `"0x71f8b00702840ee6b2808094111c3e89ce80e62ee88318c2804920d4c96f92bb880de0b6b3a7640000b864a413686200000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001790000000000000000000000000000000000000000000000000000000000000082012c808082012c94f760bdd822fccf93c44be68d94c45133002b303780c083abcdefc0"`, - ) - }) + expect(serializeTransaction(transaction)).toMatchInlineSnapshot( + `"0x71f8b00702840ee6b2808094111c3e89ce80e62ee88318c2804920d4c96f92bb880de0b6b3a7640000b864a4136862000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000017900000000000000000000000000000000000000000000000000000000000000820144808082014494f760bdd822fccf93c44be68d94c45133002b303780c083abcdefc0"`, + ) +}) - test('paymaster', () => { - const transaction: ZkSyncTransactionSerializableEIP712 = { - ...baseTransaction, - from: '0xf760bdd822fccf93c44be68d94c45133002b3037', - paymaster: '0x4B5DF730c2e6b28E17013A1485E5d9BC41Efe021', - paymasterInput: - '0x8c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000', - type: 'eip712', - } +test('paymaster', () => { + const transaction: ZkSyncTransactionSerializableEIP712 = { + ...baseTransaction, + from: '0xf760bdd822fccf93c44be68d94c45133002b3037', + paymaster: '0x4B5DF730c2e6b28E17013A1485E5d9BC41Efe021', + paymasterInput: + '0x8c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000', + type: 'eip712', + } - expect(serializeTransaction(transaction)).toMatchInlineSnapshot( - `"0x71f901090702840ee6b2808094111c3e89ce80e62ee88318c2804920d4c96f92bb880de0b6b3a7640000b864a413686200000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001790000000000000000000000000000000000000000000000000000000000000082012c808082012c94f760bdd822fccf93c44be68d94c45133002b303780c080f85b944b5df730c2e6b28e17013a1485e5d9bc41efe021b8448c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"`, - ) - }) + expect(serializeTransaction(transaction)).toMatchInlineSnapshot( + `"0x71f901090702840ee6b2808094111c3e89ce80e62ee88318c2804920d4c96f92bb880de0b6b3a7640000b864a4136862000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000017900000000000000000000000000000000000000000000000000000000000000820144808082014494f760bdd822fccf93c44be68d94c45133002b303780c080f85b944b5df730c2e6b28e17013a1485e5d9bc41efe021b8448c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"`, + ) +}) - test('without nonce', () => { - const transaction: ZkSyncTransactionSerializableEIP712 = { - ...baseEip712, - nonce: undefined, - } +test('without nonce', () => { + const transaction: ZkSyncTransactionSerializableEIP712 = { + ...baseEip712, + nonce: undefined, + } - expect(serializeTransaction(transaction)).toMatchInlineSnapshot( - `"0x71f9014d8002840ee6b2808094111c3e89ce80e62ee88318c2804920d4c96f92bb880de0b6b3a7640000b864a413686200000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001790000000000000000000000000000000000000000000000000000000000000082012c808082012c94f760bdd822fccf93c44be68d94c45133002b303782c350c0b841d2312deb1e84f7733a83ae1fc55f9cc1f2334fe472e0a494781933b194e173a45d927e67b9222b92467660849efb055422f133cf67588cbcb1874901d3244ddd1cf85b944b5df730c2e6b28e17013a1485e5d9bc41efe021b8448c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"`, - ) - }) + expect(serializeTransaction(transaction)).toMatchInlineSnapshot( + `"0x71f9014d8002840ee6b2808094111c3e89ce80e62ee88318c2804920d4c96f92bb880de0b6b3a7640000b864a4136862000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000017900000000000000000000000000000000000000000000000000000000000000820144808082014494f760bdd822fccf93c44be68d94c45133002b303782c350c0b841d2312deb1e84f7733a83ae1fc55f9cc1f2334fe472e0a494781933b194e173a45d927e67b9222b92467660849efb055422f133cf67588cbcb1874901d3244ddd1cf85b944b5df730c2e6b28e17013a1485e5d9bc41efe021b8448c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"`, + ) +}) - test('without fees', () => { - const transaction: ZkSyncTransactionSerializableEIP712 = { - ...baseEip712, - maxFeePerGas: undefined, - maxPriorityFeePerGas: undefined, - } +test('without fees', () => { + const transaction: ZkSyncTransactionSerializableEIP712 = { + ...baseEip712, + maxFeePerGas: undefined, + maxPriorityFeePerGas: undefined, + } - expect(serializeTransaction(transaction)).toMatchInlineSnapshot( - `"0x71f901490780808094111c3e89ce80e62ee88318c2804920d4c96f92bb880de0b6b3a7640000b864a413686200000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001790000000000000000000000000000000000000000000000000000000000000082012c808082012c94f760bdd822fccf93c44be68d94c45133002b303782c350c0b841d2312deb1e84f7733a83ae1fc55f9cc1f2334fe472e0a494781933b194e173a45d927e67b9222b92467660849efb055422f133cf67588cbcb1874901d3244ddd1cf85b944b5df730c2e6b28e17013a1485e5d9bc41efe021b8448c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"`, - ) - }) + expect(serializeTransaction(transaction)).toMatchInlineSnapshot( + `"0x71f901490780808094111c3e89ce80e62ee88318c2804920d4c96f92bb880de0b6b3a7640000b864a4136862000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000017900000000000000000000000000000000000000000000000000000000000000820144808082014494f760bdd822fccf93c44be68d94c45133002b303782c350c0b841d2312deb1e84f7733a83ae1fc55f9cc1f2334fe472e0a494781933b194e173a45d927e67b9222b92467660849efb055422f133cf67588cbcb1874901d3244ddd1cf85b944b5df730c2e6b28e17013a1485e5d9bc41efe021b8448c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"`, + ) +}) - test('without to', () => { - const transaction: ZkSyncTransactionSerializableEIP712 = { - ...baseEip712, - to: undefined, - } +test('without to', () => { + const transaction: ZkSyncTransactionSerializableEIP712 = { + ...baseEip712, + to: undefined, + } - expect(serializeTransaction(transaction)).toMatchInlineSnapshot( - `"0x71f901390702840ee6b2808080880de0b6b3a7640000b864a413686200000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001790000000000000000000000000000000000000000000000000000000000000082012c808082012c94f760bdd822fccf93c44be68d94c45133002b303782c350c0b841d2312deb1e84f7733a83ae1fc55f9cc1f2334fe472e0a494781933b194e173a45d927e67b9222b92467660849efb055422f133cf67588cbcb1874901d3244ddd1cf85b944b5df730c2e6b28e17013a1485e5d9bc41efe021b8448c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"`, - ) - }) + expect(serializeTransaction(transaction)).toMatchInlineSnapshot( + `"0x71f901390702840ee6b2808080880de0b6b3a7640000b864a4136862000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000017900000000000000000000000000000000000000000000000000000000000000820144808082014494f760bdd822fccf93c44be68d94c45133002b303782c350c0b841d2312deb1e84f7733a83ae1fc55f9cc1f2334fe472e0a494781933b194e173a45d927e67b9222b92467660849efb055422f133cf67588cbcb1874901d3244ddd1cf85b944b5df730c2e6b28e17013a1485e5d9bc41efe021b8448c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"`, + ) +}) - test('without value', () => { - const transaction: ZkSyncTransactionSerializableEIP712 = { - ...baseEip712, - value: undefined, - } +test('without value', () => { + const transaction: ZkSyncTransactionSerializableEIP712 = { + ...baseEip712, + value: undefined, + } - expect(serializeTransaction(transaction)).toMatchInlineSnapshot( - `"0x71f901450702840ee6b2808094111c3e89ce80e62ee88318c2804920d4c96f92bb80b864a413686200000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001790000000000000000000000000000000000000000000000000000000000000082012c808082012c94f760bdd822fccf93c44be68d94c45133002b303782c350c0b841d2312deb1e84f7733a83ae1fc55f9cc1f2334fe472e0a494781933b194e173a45d927e67b9222b92467660849efb055422f133cf67588cbcb1874901d3244ddd1cf85b944b5df730c2e6b28e17013a1485e5d9bc41efe021b8448c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"`, - ) - }) + expect(serializeTransaction(transaction)).toMatchInlineSnapshot( + `"0x71f901450702840ee6b2808094111c3e89ce80e62ee88318c2804920d4c96f92bb80b864a4136862000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000017900000000000000000000000000000000000000000000000000000000000000820144808082014494f760bdd822fccf93c44be68d94c45133002b303782c350c0b841d2312deb1e84f7733a83ae1fc55f9cc1f2334fe472e0a494781933b194e173a45d927e67b9222b92467660849efb055422f133cf67588cbcb1874901d3244ddd1cf85b944b5df730c2e6b28e17013a1485e5d9bc41efe021b8448c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"`, + ) +}) - test('without data', () => { - const transaction: ZkSyncTransactionSerializableEIP712 = { - ...baseEip712, - data: undefined, - } +test('without data', () => { + const transaction: ZkSyncTransactionSerializableEIP712 = { + ...baseEip712, + data: undefined, + } - expect(serializeTransaction(transaction)).toMatchInlineSnapshot( - `"0x71f8e80702840ee6b2808094111c3e89ce80e62ee88318c2804920d4c96f92bb880de0b6b3a76400008082012c808082012c94f760bdd822fccf93c44be68d94c45133002b303782c350c0b841d2312deb1e84f7733a83ae1fc55f9cc1f2334fe472e0a494781933b194e173a45d927e67b9222b92467660849efb055422f133cf67588cbcb1874901d3244ddd1cf85b944b5df730c2e6b28e17013a1485e5d9bc41efe021b8448c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"`, - ) - }) + expect(serializeTransaction(transaction)).toMatchInlineSnapshot( + `"0x71f8e80702840ee6b2808094111c3e89ce80e62ee88318c2804920d4c96f92bb880de0b6b3a764000000820144808082014494f760bdd822fccf93c44be68d94c45133002b303782c350c0b841d2312deb1e84f7733a83ae1fc55f9cc1f2334fe472e0a494781933b194e173a45d927e67b9222b92467660849efb055422f133cf67588cbcb1874901d3244ddd1cf85b944b5df730c2e6b28e17013a1485e5d9bc41efe021b8448c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"`, + ) +}) - test('without from', () => { - const transaction: ZkSyncTransactionSerializableEIP712 = { - ...baseEip712, - // @ts-expect-error - from: undefined, - } +test('without from', () => { + const transaction: ZkSyncTransactionSerializableEIP712 = { + ...baseEip712, + // @ts-expect-error + from: undefined, + } - expect(serializeTransaction(transaction)).toMatchInlineSnapshot( - `"0x71f901390702840ee6b2808094111c3e89ce80e62ee88318c2804920d4c96f92bb880de0b6b3a7640000b864a413686200000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001790000000000000000000000000000000000000000000000000000000000000082012c808082012c8082c350c0b841d2312deb1e84f7733a83ae1fc55f9cc1f2334fe472e0a494781933b194e173a45d927e67b9222b92467660849efb055422f133cf67588cbcb1874901d3244ddd1cf85b944b5df730c2e6b28e17013a1485e5d9bc41efe021b8448c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"`, - ) - }) + expect(serializeTransaction(transaction)).toMatchInlineSnapshot( + `"0x71f901390702840ee6b2808094111c3e89ce80e62ee88318c2804920d4c96f92bb880de0b6b3a7640000b864a413686200000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001790000000000000000000000000000000000000000000000000000000000000082014480808201448082c350c0b841d2312deb1e84f7733a83ae1fc55f9cc1f2334fe472e0a494781933b194e173a45d927e67b9222b92467660849efb055422f133cf67588cbcb1874901d3244ddd1cf85b944b5df730c2e6b28e17013a1485e5d9bc41efe021b8448c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"`, + ) }) test('signed with customSignature', async () => { @@ -191,7 +189,7 @@ test('signed with customSignature', async () => { }) expect(signed).toMatchInlineSnapshot( - `"0x71f901500702840ee6b28083026c3694111c3e89ce80e62ee88318c2804920d4c96f92bb880de0b6b3a7640000b864a413686200000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001790000000000000000000000000000000000000000000000000000000000000082012c808082012c94f760bdd822fccf93c44be68d94c45133002b303782c350c0b841d2312deb1e84f7733a83ae1fc55f9cc1f2334fe472e0a494781933b194e173a45d927e67b9222b92467660849efb055422f133cf67588cbcb1874901d3244ddd1cf85b944b5df730c2e6b28e17013a1485e5d9bc41efe021b8448c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"`, + `"0x71f901500702840ee6b28083026c3694111c3e89ce80e62ee88318c2804920d4c96f92bb880de0b6b3a7640000b864a4136862000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000017900000000000000000000000000000000000000000000000000000000000000820144808082014494f760bdd822fccf93c44be68d94c45133002b303782c350c0b841d2312deb1e84f7733a83ae1fc55f9cc1f2334fe472e0a494781933b194e173a45d927e67b9222b92467660849efb055422f133cf67588cbcb1874901d3244ddd1cf85b944b5df730c2e6b28e17013a1485e5d9bc41efe021b8448c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"`, ) }) @@ -261,7 +259,7 @@ describe('invalid params', () => { }) }) -describe('not eip712', () => { +test('other', () => { const transaction: TransactionSerializableEIP1559 = { to: accounts[0].address, chainId: 1, @@ -270,19 +268,16 @@ describe('not eip712', () => { maxPriorityFeePerGas: parseGwei('2'), value: parseEther('1'), } - - test('it calls the standard serializeTransaction', () => { - const serialized = serializeTransaction(transaction) - expect(serialized).toMatchInlineSnapshot( - '"0x02ed0180847735940084773594008094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c0"', - ) - expect(parseTransaction(serialized)).toEqual({ - to: accounts[0].address, - chainId: 1, - maxFeePerGas: parseGwei('2'), - maxPriorityFeePerGas: parseGwei('2'), - value: parseEther('1'), - type: 'eip1559', - }) + const serialized = serializeTransaction(transaction) + expect(serialized).toMatchInlineSnapshot( + '"0x02ed0180847735940084773594008094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c0"', + ) + expect(parseTransaction(serialized)).toEqual({ + to: accounts[0].address, + chainId: 1, + maxFeePerGas: parseGwei('2'), + maxPriorityFeePerGas: parseGwei('2'), + value: parseEther('1'), + type: 'eip1559', }) }) diff --git a/src/chains/zksync/serializers.ts b/src/chains/zksync/serializers.ts index c961823a84..c872c97e1a 100644 --- a/src/chains/zksync/serializers.ts +++ b/src/chains/zksync/serializers.ts @@ -1,9 +1,5 @@ -import { InvalidAddressError } from '../../errors/address.js' -import { BaseError } from '../../errors/base.js' -import { InvalidChainIdError } from '../../errors/chain.js' import type { ChainSerializers } from '../../types/chain.js' import type { TransactionSerializable } from '../../types/transaction.js' -import { isAddress } from '../../utils/address/isAddress.js' import { concatHex } from '../../utils/data/concat.js' import { toHex } from '../../utils/encoding/toHex.js' import { toRlp } from '../../utils/encoding/toRlp.js' @@ -15,12 +11,14 @@ import type { ZkSyncTransactionSerializable, ZkSyncTransactionSerializableEIP712, ZkSyncTransactionSerializedEIP712, -} from './types.js' +} from './types/transaction.js' +import { assertEip712Transaction } from './utils/assertEip712Transaction.js' +import { isEIP712Transaction } from './utils/isEip712Transaction.js' export const serializeTransaction: SerializeTransactionFn< ZkSyncTransactionSerializable > = (tx, signature) => { - if (isEIP712(tx)) + if (isEIP712Transaction(tx)) return serializeTransactionEIP712(tx as ZkSyncTransactionSerializableEIP712) return serializeTransaction_(tx as TransactionSerializable, signature) } @@ -29,9 +27,6 @@ export const serializers = { transaction: serializeTransaction, } as const satisfies ChainSerializers -////////////////////////////////////////////////////////////////////////////// -// Serializers - export type SerializeTransactionEIP712ReturnType = ZkSyncTransactionSerializedEIP712 @@ -55,7 +50,7 @@ function serializeTransactionEIP712( data, } = transaction - assertTransactionEIP712(transaction) + assertEip712Transaction(transaction) const serializedTransaction = [ nonce ? toHex(nonce) : '0x', @@ -64,7 +59,7 @@ function serializeTransactionEIP712( gas ? toHex(gas) : '0x', to ?? '0x', value ? toHex(value) : '0x', - data ?? '0x', + data ?? '0x0', toHex(chainId), toHex(''), toHex(''), @@ -81,42 +76,3 @@ function serializeTransactionEIP712( toRlp(serializedTransaction), ]) as SerializeTransactionEIP712ReturnType } - -////////////////////////////////////////////////////////////////////////////// -// Utilities - -function isEIP712(transaction: ZkSyncTransactionSerializable) { - if ( - 'customSignature' in transaction || - 'paymaster' in transaction || - 'paymasterInput' in transaction || - 'gasPerPubdata' in transaction || - 'factoryDeps' in transaction - ) - return true - return false -} - -export function assertTransactionEIP712( - transaction: ZkSyncTransactionSerializableEIP712, -) { - const { chainId, to, from, paymaster, paymasterInput } = transaction - if (chainId <= 0) throw new InvalidChainIdError({ chainId }) - - if (to && !isAddress(to)) throw new InvalidAddressError({ address: to }) - if (from && !isAddress(from)) throw new InvalidAddressError({ address: from }) - if (paymaster && !isAddress(paymaster)) - throw new InvalidAddressError({ address: paymaster }) - - if (paymaster && !paymasterInput) { - throw new BaseError( - '`paymasterInput` must be provided when `paymaster` is defined', - ) - } - - if (!paymaster && paymasterInput) { - throw new BaseError( - '`paymaster` must be provided when `paymasterInput` is defined', - ) - } -} diff --git a/src/chains/zksync/types/block.ts b/src/chains/zksync/types/block.ts new file mode 100644 index 0000000000..24d261580c --- /dev/null +++ b/src/chains/zksync/types/block.ts @@ -0,0 +1,34 @@ +import type { Block, BlockTag } from '../../../types/block.js' +import type { Hex } from '../../../types/misc.js' +import type { RpcBlock } from '../../../types/rpc.js' +import type { ZkSyncRpcTransaction, ZkSyncTransaction } from './transaction.js' + +export type ZkSyncBlockOverrides = { + l1BatchNumber: bigint | null + l1BatchTimestamp: bigint | null +} + +export type ZkSyncBlock< + TIncludeTransactions extends boolean = boolean, + TBlockTag extends BlockTag = BlockTag, +> = Block< + bigint, + TIncludeTransactions, + TBlockTag, + ZkSyncTransaction +> & + ZkSyncBlockOverrides + +export type ZkSyncRpcBlockOverrides = { + l1BatchNumber: Hex | null + l1BatchTimestamp: Hex | null +} +export type ZkSyncRpcBlock< + TBlockTag extends BlockTag = BlockTag, + TIncludeTransactions extends boolean = boolean, +> = RpcBlock< + TBlockTag, + TIncludeTransactions, + ZkSyncRpcTransaction +> & + ZkSyncRpcBlockOverrides diff --git a/src/chains/zksync/types/chain.ts b/src/chains/zksync/types/chain.ts new file mode 100644 index 0000000000..026ce208cb --- /dev/null +++ b/src/chains/zksync/types/chain.ts @@ -0,0 +1,28 @@ +import type { ChainFormatters } from '../../../types/chain.js' +import type { Chain, ChainFormatter } from '../../../types/chain.js' +import type { + TransactionSerializable, + TransactionSerializableGeneric, +} from '../../../types/transaction.js' +import type { EIP712DomainFn } from './eip712.js' + +export type ChainEIP712< + formatters extends ChainFormatters | undefined = ChainFormatters | undefined, + TransactionSignable = {}, +> = Chain< + formatters, + { + /** Return EIP712 Domain for EIP712 transaction */ + getEip712Domain?: + | EIP712DomainFn< + formatters extends ChainFormatters + ? formatters['transactionRequest'] extends ChainFormatter + ? TransactionSerializableGeneric & + Parameters[0] + : TransactionSerializable + : TransactionSerializable, + TransactionSignable + > + | undefined + } +> diff --git a/src/chains/zksync/types/eip712.ts b/src/chains/zksync/types/eip712.ts new file mode 100644 index 0000000000..9482074b95 --- /dev/null +++ b/src/chains/zksync/types/eip712.ts @@ -0,0 +1,31 @@ +import type { Address, TypedDataDomain } from 'abitype' +import type { Hex } from '../../../types/misc.js' +import type { TransactionSerializable } from '../../../types/transaction.js' + +type PaymasterParams = { + paymaster: Address + paymasterInput: number[] +} + +export type ZkSyncEip712Meta = { + gasPerPubdata?: Hex + factoryDeps?: Hex[] + customSignature?: Hex + paymasterParams?: PaymasterParams +} + +type EIP712FieldType = 'uint256' | 'bytes' | 'bytes32[]' +type EIP712Field = { name: string; type: EIP712FieldType } + +export type EIP712Domain = { + domain: TypedDataDomain + types: Record + primaryType: string + message: TransactionSignable +} + +export type EIP712DomainFn< + TTransactionSerializable extends + TransactionSerializable = TransactionSerializable, + TransactionSignable = {}, +> = (transaction: TTransactionSerializable) => EIP712Domain diff --git a/src/chains/zksync/types/fee.ts b/src/chains/zksync/types/fee.ts new file mode 100644 index 0000000000..8794df81b2 --- /dev/null +++ b/src/chains/zksync/types/fee.ts @@ -0,0 +1,5 @@ +export type ZkSyncFeeValues = { + gasPrice: TQuantity + maxFeePerGas: TQuantity + maxPriorityFeePerGas: TQuantity +} diff --git a/src/chains/zksync/types/log.ts b/src/chains/zksync/types/log.ts new file mode 100644 index 0000000000..7f73aa065c --- /dev/null +++ b/src/chains/zksync/types/log.ts @@ -0,0 +1,57 @@ +import type { Abi, AbiEvent } from 'abitype' +import type { Log as Log_ } from '../../../types/log.js' +import type { Hex } from '../../../types/misc.js' +import type { RpcLog as RpcLog_ } from '../../../types/rpc.js' + +export type ZkSyncLog< + TQuantity = bigint, + TIndex = number, + TPending extends boolean = boolean, + TAbiEvent extends AbiEvent | undefined = undefined, + TStrict extends boolean | undefined = undefined, + TAbi extends Abi | readonly unknown[] | undefined = TAbiEvent extends AbiEvent + ? [TAbiEvent] + : undefined, + TEventName extends string | undefined = TAbiEvent extends AbiEvent + ? TAbiEvent['name'] + : undefined, +> = Log_ & { + l1BatchNumber: TQuantity | null + transactionLogIndex: TIndex + logType: Hex | null +} + +export type ZkSyncRpcLog = RpcLog_ & { + l1BatchNumber: Hex | null + // These are returned but doesn't apear in Log structure neither is mentioned in https://era.zksync.io/docs/api/js/types + transactionLogIndex: Hex + logType: Hex | null +} + +export type ZkSyncL2ToL1Log = { + blockNumber: bigint + blockHash: string + l1BatchNumber: bigint + transactionIndex: bigint + shardId: bigint + isService: boolean + sender: string + key: string + value: string + transactionHash: string + logIndex: bigint +} + +export type ZkSyncRpcL2ToL1Log = { + blockNumber: Hex + blockHash: Hex + l1BatchNumber: Hex + transactionIndex: Hex + shardId: Hex + isService: boolean + sender: Hex + key: Hex + value: Hex + transactionHash: Hex + logIndex: Hex +} diff --git a/src/chains/zksync/types.ts b/src/chains/zksync/types/transaction.ts similarity index 63% rename from src/chains/zksync/types.ts rename to src/chains/zksync/types/transaction.ts index ef064b93d0..5ceb7d146b 100644 --- a/src/chains/zksync/types.ts +++ b/src/chains/zksync/types/transaction.ts @@ -1,15 +1,11 @@ -import type { Abi, AbiEvent, Address } from 'abitype' -import type { Block, BlockTag } from '../../types/block.js' -import type { FeeValuesEIP1559 } from '../../types/fee.js' -import type { Log as Log_ } from '../../types/log.js' -import type { Hex } from '../../types/misc.js' +import type { Address } from 'abitype' +import type { FeeValuesEIP1559 } from '../../../types/fee.js' +import type { Hex } from '../../../types/misc.js' import type { Index, Quantity, - RpcBlock, - RpcLog as RpcLog_, RpcTransactionRequest as RpcTransactionRequest_, -} from '../../types/rpc.js' +} from '../../../types/rpc.js' import type { Transaction as Transaction_, TransactionBase, @@ -23,121 +19,20 @@ import type { TransactionSerializableEIP1559, TransactionSerialized, TransactionType, -} from '../../types/transaction.js' -import type { UnionOmit } from '../../types/utils.js' +} from '../../../types/transaction.js' +import type { OneOf, UnionOmit } from '../../../types/utils.js' +import type { ZkSyncEip712Meta } from './eip712.js' +import type { ZkSyncFeeValues } from './fee.js' +import type { + ZkSyncL2ToL1Log, + ZkSyncLog, + ZkSyncRpcL2ToL1Log, + ZkSyncRpcLog, +} from './log.js' type EIP712Type = '0x71' type PriorityType = '0xff' -// Types -// https://era.zksync.io/docs/api/js/types - -export type ZkSyncLog< - TQuantity = bigint, - TIndex = number, - TPending extends boolean = boolean, - TAbiEvent extends AbiEvent | undefined = undefined, - TStrict extends boolean | undefined = undefined, - TAbi extends Abi | readonly unknown[] | undefined = TAbiEvent extends AbiEvent - ? [TAbiEvent] - : undefined, - TEventName extends string | undefined = TAbiEvent extends AbiEvent - ? TAbiEvent['name'] - : undefined, -> = Log_ & { - l1BatchNumber: TQuantity | null - transactionLogIndex: TIndex - logType: Hex | null -} - -export type ZkSyncRpcLog = RpcLog_ & { - l1BatchNumber: Hex | null - // These are returned but doesn't apear in Log structure neither is mentioned in https://era.zksync.io/docs/api/js/types - transactionLogIndex: Hex - logType: Hex | null -} - -type PaymasterParams = { - paymaster: Address - paymasterInput: number[] -} - -export type ZkSyncEip712Meta = { - gasPerPubdata?: Hex - factoryDeps?: Hex[] - customSignature?: Hex - paymasterParams?: PaymasterParams -} - -export type ZkSyncL2ToL1Log = { - blockNumber: bigint - blockHash: string - l1BatchNumber: bigint - transactionIndex: bigint - shardId: bigint - isService: boolean - sender: string - key: string - value: string - transactionHash: string - logIndex: bigint -} - -export type ZkSyncRpcL2ToL1Log = { - blockNumber: Hex - blockHash: Hex - l1BatchNumber: Hex - transactionIndex: Hex - shardId: Hex - isService: boolean - sender: Hex - key: Hex - value: Hex - transactionHash: Hex - logIndex: Hex -} - -export type ZkSyncFeeValues = { - gasPrice: TQuantity - maxFeePerGas: TQuantity - maxPriorityFeePerGas: TQuantity -} - -// Block -// https://era.zksync.io/docs/api/js/types#block - -export type ZkSyncBlockOverrides = { - l1BatchNumber: bigint | null - l1BatchTimestamp: bigint | null -} - -export type ZkSyncBlock< - TIncludeTransactions extends boolean = boolean, - TBlockTag extends BlockTag = BlockTag, -> = Block< - bigint, - TIncludeTransactions, - TBlockTag, - ZkSyncTransaction -> & - ZkSyncBlockOverrides - -// Block (RPC) - -export type ZkSyncRpcBlockOverrides = { - l1BatchNumber: Hex | null - l1BatchTimestamp: Hex | null -} -export type ZkSyncRpcBlock< - TBlockTag extends BlockTag = BlockTag, - TIncludeTransactions extends boolean = boolean, -> = RpcBlock< - TBlockTag, - TIncludeTransactions, - ZkSyncRpcTransaction -> & - ZkSyncRpcBlockOverrides - // Transaction // https://era.zksync.io/docs/api/js/types#transactionresponse @@ -291,9 +186,9 @@ export type ZkSyncTransactionReceipt< // Serializers -export type ZkSyncTransactionSerializable = - | TransactionSerializable - | ZkSyncTransactionSerializableEIP712 +export type ZkSyncTransactionSerializable = OneOf< + TransactionSerializable | ZkSyncTransactionSerializableEIP712 +> export type ZkSyncTransactionSerialized< TType extends TransactionType = 'eip712', @@ -315,3 +210,36 @@ export type ZkSyncTransactionSerializableEIP712< customSignature?: Hex type?: 'eip712' } + +// EIP712 Signer + +export type ZkSyncEIP712TransactionSignable = { + txType: bigint + from: bigint + to: bigint + gasLimit: bigint + gasPerPubdataByteLimit: bigint + maxFeePerGas: bigint + maxPriorityFeePerGas: bigint + paymaster: bigint + nonce: bigint + value: bigint + data: Hex + factoryDeps: Hex[] + paymasterInput: Hex +} + +export type TransactionRequestEIP712< + TQuantity = bigint, + TIndex = number, + TTransactionType = 'eip712', +> = TransactionRequestBase & + Partial> & { + accessList?: never + gasPerPubdata?: bigint + factoryDeps?: Hex[] + paymaster?: Address + paymasterInput?: Hex + customSignature?: Hex + type?: TTransactionType + } diff --git a/src/chains/zksync/utils/assertEip712Request.test.ts b/src/chains/zksync/utils/assertEip712Request.test.ts new file mode 100644 index 0000000000..49aff4ed61 --- /dev/null +++ b/src/chains/zksync/utils/assertEip712Request.test.ts @@ -0,0 +1,36 @@ +import { expect, test } from 'vitest' +import { assertEip712Request } from './assertEip712Request.js' + +test('default', () => { + assertEip712Request({ + account: '0x0000000000000000000000000000000000000000', + type: 'eip712', + }) + + expect(() => + assertEip712Request({ + account: '0x0000000000000000000000000000000000000000', + }), + ).toThrowErrorMatchingInlineSnapshot(` + [InvalidEip712TransactionError: Transaction is not an EIP712 transaction. + + Transaction must: + - include \`type: "eip712"\` + - include one of the following: \`customSignature\`, \`paymaster\`, \`paymasterInput\`, \`gasPerPubdata\`, \`factoryDeps\` + + Version: viem@1.0.2] + `) + + expect(() => + assertEip712Request({ + account: '0x0000000000000000000000000000000000000000', + maxFeePerGas: 1n, + maxPriorityFeePerGas: 2n, + type: 'eip712', + }), + ).toThrowErrorMatchingInlineSnapshot(` + [TipAboveFeeCapError: The provided tip (\`maxPriorityFeePerGas\` = 0.000000002 gwei) cannot be higher than the fee cap (\`maxFeePerGas\` = 0.000000001 gwei). + + Version: viem@1.0.2] + `) +}) diff --git a/src/chains/zksync/utils/assertEip712Request.ts b/src/chains/zksync/utils/assertEip712Request.ts new file mode 100644 index 0000000000..8c15636f57 --- /dev/null +++ b/src/chains/zksync/utils/assertEip712Request.ts @@ -0,0 +1,18 @@ +import type { ErrorType } from '../../../errors/utils.js' +import { type AssertRequestErrorType, assertRequest } from '../../../index.js' +import type { SendTransactionParameters } from '../actions/sendTransaction.js' +import type { zkSync } from '../chains.js' +import { InvalidEip712TransactionError } from '../errors/transaction.js' +import { isEIP712Transaction } from './isEip712Transaction.js' + +export type AssertEip712RequestParameters = Partial< + SendTransactionParameters +> + +export type AssertEip712RequestErrorType = AssertRequestErrorType | ErrorType + +export function assertEip712Request(args: AssertEip712RequestParameters) { + if (!isEIP712Transaction(args as any)) + throw new InvalidEip712TransactionError() + assertRequest(args as any) +} diff --git a/src/chains/zksync/utils/assertEip712Transaction.test.ts b/src/chains/zksync/utils/assertEip712Transaction.test.ts new file mode 100644 index 0000000000..71df2cf758 --- /dev/null +++ b/src/chains/zksync/utils/assertEip712Transaction.test.ts @@ -0,0 +1,100 @@ +import { expect, test } from 'vitest' +import { assertEip712Transaction } from './assertEip712Transaction.js' + +test('default', () => { + assertEip712Transaction({ + chainId: 1, + type: 'eip712', + }) + + expect(() => assertEip712Transaction({})).toThrowErrorMatchingInlineSnapshot(` + [InvalidEip712TransactionError: Transaction is not an EIP712 transaction. + + Transaction must: + - include \`type: "eip712"\` + - include one of the following: \`customSignature\`, \`paymaster\`, \`paymasterInput\`, \`gasPerPubdata\`, \`factoryDeps\` + + Version: viem@1.0.2] + `) + + expect(() => + assertEip712Transaction({ + type: 'eip712', + }), + ).toThrowErrorMatchingInlineSnapshot(` + [InvalidChainIdError: Chain ID is invalid. + + Version: viem@1.0.2] + `) + + expect(() => + assertEip712Transaction({ + chainId: -1, + type: 'eip712', + }), + ).toThrowErrorMatchingInlineSnapshot(` + [InvalidChainIdError: Chain ID "-1" is invalid. + + Version: viem@1.0.2] + `) + + expect(() => + assertEip712Transaction({ + chainId: 300, + to: '0x', + type: 'eip712', + }), + ).toThrowErrorMatchingInlineSnapshot(` + [InvalidAddressError: Address "0x" is invalid. + + Version: viem@1.0.2] + `) + + expect(() => + assertEip712Transaction({ + chainId: 300, + from: '0x', + type: 'eip712', + }), + ).toThrowErrorMatchingInlineSnapshot(` + [InvalidAddressError: Address "0x" is invalid. + + Version: viem@1.0.2] + `) + + expect(() => + assertEip712Transaction({ + chainId: 300, + paymaster: '0x', + type: 'eip712', + }), + ).toThrowErrorMatchingInlineSnapshot(` + [InvalidAddressError: Address "0x" is invalid. + + Version: viem@1.0.2] + `) + + expect(() => + assertEip712Transaction({ + chainId: 300, + paymaster: '0x0000000000000000000000000000000000000000', + type: 'eip712', + }), + ).toThrowErrorMatchingInlineSnapshot(` + [ViemError: \`paymasterInput\` must be provided when \`paymaster\` is defined + + Version: viem@1.0.2] + `) + + expect(() => + assertEip712Transaction({ + chainId: 300, + paymasterInput: '0x0000000000000000000000000000000000000000', + type: 'eip712', + }), + ).toThrowErrorMatchingInlineSnapshot(` + [ViemError: \`paymaster\` must be provided when \`paymasterInput\` is defined + + Version: viem@1.0.2] + `) +}) diff --git a/src/chains/zksync/utils/assertEip712Transaction.ts b/src/chains/zksync/utils/assertEip712Transaction.ts new file mode 100644 index 0000000000..f4999f8a73 --- /dev/null +++ b/src/chains/zksync/utils/assertEip712Transaction.ts @@ -0,0 +1,35 @@ +import { InvalidAddressError } from '../../../errors/address.js' +import { BaseError } from '../../../errors/base.js' +import { InvalidChainIdError } from '../../../errors/chain.js' +import { isAddress } from '../../../utils/address/isAddress.js' +import { InvalidEip712TransactionError } from '../errors/transaction.js' +import type { + ZkSyncTransactionSerializable, + ZkSyncTransactionSerializableEIP712, +} from '../types/transaction.js' +import { isEIP712Transaction } from './isEip712Transaction.js' + +export function assertEip712Transaction( + transaction: Partial, +) { + const { chainId, to, from, paymaster, paymasterInput } = + transaction as ZkSyncTransactionSerializableEIP712 + + if (!isEIP712Transaction(transaction)) + throw new InvalidEip712TransactionError() + if (!chainId || chainId <= 0) throw new InvalidChainIdError({ chainId }) + if (to && !isAddress(to)) throw new InvalidAddressError({ address: to }) + if (from && !isAddress(from)) throw new InvalidAddressError({ address: from }) + if (paymaster && !isAddress(paymaster)) + throw new InvalidAddressError({ address: paymaster }) + if (paymaster && !paymasterInput) { + throw new BaseError( + '`paymasterInput` must be provided when `paymaster` is defined', + ) + } + if (!paymaster && paymasterInput) { + throw new BaseError( + '`paymaster` must be provided when `paymasterInput` is defined', + ) + } +} diff --git a/src/chains/zksync/utils/getEip712Domain.test.ts b/src/chains/zksync/utils/getEip712Domain.test.ts new file mode 100644 index 0000000000..3e71dfcda4 --- /dev/null +++ b/src/chains/zksync/utils/getEip712Domain.test.ts @@ -0,0 +1,138 @@ +import { expect, test } from 'vitest' + +import { accounts } from '~test/src/constants.js' +import { signTransaction } from '../../../accounts/utils/signTransaction.js' +import { + type TransactionSerializableEIP1559, + parseEther, +} from '../../../index.js' +import { zkSync } from '../index.js' +import { serializeTransaction } from '../serializers.js' +import type { ZkSyncTransactionSerializableEIP712 } from '../types/transaction.js' +import { getEip712Domain } from './getEip712Domain.js' + +const baseTransaction: TransactionSerializableEIP1559 = { + to: '0x111C3E89Ce80e62EE88318C2804920D4c96f92bb', + chainId: zkSync.id, + nonce: 7, + maxFeePerGas: 250000000n, + maxPriorityFeePerGas: 2n, + value: parseEther('1'), + data: '0xa4136862000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000017900000000000000000000000000000000000000000000000000000000000000', +} + +const baseEip712: ZkSyncTransactionSerializableEIP712 = { + ...baseTransaction, + from: '0xf760bdd822fccf93c44be68d94c45133002b3037', + gasPerPubdata: 50000n, + factoryDeps: [], + paymaster: '0x4B5DF730c2e6b28E17013A1485E5d9BC41Efe021', + paymasterInput: + '0x8c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000', + type: 'eip712', +} + +test('default', () => { + const transaction = { + ...baseEip712, + gas: 158774n, + } + + expect(getEip712Domain(transaction)).toMatchInlineSnapshot(` + { + "domain": { + "chainId": 324, + "name": "zkSync", + "version": "2", + }, + "message": { + "data": "0xa4136862000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000017900000000000000000000000000000000000000000000000000000000000000", + "factoryDeps": [], + "from": 1412278129702086080747614868627407617204419244087n, + "gasLimit": 158774n, + "gasPerPubdataByteLimit": 50000n, + "maxFeePerGas": 250000000n, + "maxPriorityFeePerGas": 2n, + "nonce": 7n, + "paymaster": 430269810442498150241781430127684901874131001377n, + "paymasterInput": "0x8c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000", + "to": 97682711824466416464036482978912861037813273275n, + "txType": 113n, + "value": 1000000000000000000n, + }, + "primaryType": "Transaction", + "types": { + "Transaction": [ + { + "name": "txType", + "type": "uint256", + }, + { + "name": "from", + "type": "uint256", + }, + { + "name": "to", + "type": "uint256", + }, + { + "name": "gasLimit", + "type": "uint256", + }, + { + "name": "gasPerPubdataByteLimit", + "type": "uint256", + }, + { + "name": "maxFeePerGas", + "type": "uint256", + }, + { + "name": "maxPriorityFeePerGas", + "type": "uint256", + }, + { + "name": "paymaster", + "type": "uint256", + }, + { + "name": "nonce", + "type": "uint256", + }, + { + "name": "value", + "type": "uint256", + }, + { + "name": "data", + "type": "bytes", + }, + { + "name": "factoryDeps", + "type": "bytes32[]", + }, + { + "name": "paymasterInput", + "type": "bytes", + }, + ], + }, + } + `) +}) + +test('signed', async () => { + const signed = await signTransaction({ + privateKey: accounts[0].privateKey, + transaction: baseEip712, + serializer: serializeTransaction, + }) + + expect(signed).toEqual( + '0x71f9010b0702840ee6b2808094111c3e89ce80e62ee88318c2804920d4c96f92bb880de0b6b3a7640000b864a4136862000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000017900000000000000000000000000000000000000000000000000000000000000820144808082014494f760bdd822fccf93c44be68d94c45133002b303782c350c080f85b944b5df730c2e6b28e17013a1485e5d9bc41efe021b8448c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000', + ) +}) + +test('non-eip712 should throw', async () => { + expect(() => getEip712Domain(baseTransaction)).toThrowError() +}) diff --git a/src/chains/zksync/utils/getEip712Domain.ts b/src/chains/zksync/utils/getEip712Domain.ts new file mode 100644 index 0000000000..6badf79933 --- /dev/null +++ b/src/chains/zksync/utils/getEip712Domain.ts @@ -0,0 +1,83 @@ +import type { EIP712DomainFn } from '../types/eip712.js' +import type { + ZkSyncEIP712TransactionSignable, + ZkSyncTransactionSerializable, + ZkSyncTransactionSerializableEIP712, +} from '../types/transaction.js' +import { assertEip712Transaction } from './assertEip712Transaction.js' + +export const getEip712Domain: EIP712DomainFn< + ZkSyncTransactionSerializable, + ZkSyncEIP712TransactionSignable +> = (transaction) => { + assertEip712Transaction(transaction) + + const message = transactionToMessage( + transaction as ZkSyncTransactionSerializableEIP712, + ) + + return { + domain: { + name: 'zkSync', + version: '2', + chainId: transaction.chainId, + }, + types: { + Transaction: [ + { name: 'txType', type: 'uint256' }, + { name: 'from', type: 'uint256' }, + { name: 'to', type: 'uint256' }, + { name: 'gasLimit', type: 'uint256' }, + { name: 'gasPerPubdataByteLimit', type: 'uint256' }, + { name: 'maxFeePerGas', type: 'uint256' }, + { name: 'maxPriorityFeePerGas', type: 'uint256' }, + { name: 'paymaster', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'value', type: 'uint256' }, + { name: 'data', type: 'bytes' }, + { name: 'factoryDeps', type: 'bytes32[]' }, + { name: 'paymasterInput', type: 'bytes' }, + ], + }, + primaryType: 'Transaction', + message: message, + } +} + +////////////////////////////////////////////////////////////////////////////// +// Utilities + +function transactionToMessage( + transaction: ZkSyncTransactionSerializableEIP712, +): ZkSyncEIP712TransactionSignable { + const { + gas, + nonce, + to, + from, + value, + maxFeePerGas, + maxPriorityFeePerGas, + factoryDeps, + paymaster, + paymasterInput, + gasPerPubdata, + data, + } = transaction + + return { + txType: 113n, + from: BigInt(from), + to: to ? BigInt(to) : 0n, + gasLimit: gas ?? 0n, + gasPerPubdataByteLimit: gasPerPubdata ?? 0n, + maxFeePerGas: maxFeePerGas ?? 0n, + maxPriorityFeePerGas: maxPriorityFeePerGas ?? 0n, + paymaster: paymaster ? BigInt(paymaster) : 0n, + nonce: nonce ? BigInt(nonce) : 0n, + value: value ?? 0n, + data: data ? data : '0x0', + factoryDeps: factoryDeps ?? [], + paymasterInput: paymasterInput ? paymasterInput : '0x0', + } +} diff --git a/src/chains/zksync/utils/isEip712Transaction.test.ts b/src/chains/zksync/utils/isEip712Transaction.test.ts new file mode 100644 index 0000000000..8886bca965 --- /dev/null +++ b/src/chains/zksync/utils/isEip712Transaction.test.ts @@ -0,0 +1,56 @@ +import { expect, test } from 'vitest' +import { isEIP712Transaction } from './isEip712Transaction.js' + +test('true', () => { + expect( + isEIP712Transaction({ + type: 'eip712', + }), + ).toBeTruthy() + expect( + isEIP712Transaction({ + customSignature: '0x', + from: '0x0000000000000000000000000000000000000000', + }), + ).toBeTruthy() + expect( + isEIP712Transaction({ + paymaster: '0x', + from: '0x0000000000000000000000000000000000000000', + }), + ).toBeTruthy() + expect( + isEIP712Transaction({ + paymasterInput: '0x', + from: '0x0000000000000000000000000000000000000000', + }), + ).toBeTruthy() + expect( + isEIP712Transaction({ + gasPerPubdata: 1n, + from: '0x0000000000000000000000000000000000000000', + }), + ).toBeTruthy() + expect( + isEIP712Transaction({ + factoryDeps: ['0x'], + from: '0x0000000000000000000000000000000000000000', + }), + ).toBeTruthy() +}) + +test('false', () => { + expect(isEIP712Transaction({})).toBeFalsy() + expect( + isEIP712Transaction({ + type: 'eip1559', + }), + ).toBeFalsy() + expect( + isEIP712Transaction({ + from: '0x0000000000000000000000000000000000000000', + to: '0x0000000000000000000000000000000000000000', + value: 69n, + }), + ).toBeFalsy() +}) diff --git a/src/chains/zksync/utils/isEip712Transaction.ts b/src/chains/zksync/utils/isEip712Transaction.ts new file mode 100644 index 0000000000..273467013b --- /dev/null +++ b/src/chains/zksync/utils/isEip712Transaction.ts @@ -0,0 +1,17 @@ +import type { ZkSyncTransactionSerializable } from '../types/transaction.js' + +export function isEIP712Transaction( + transaction: Partial, +) { + if (transaction.type === 'eip712') return true + if ( + ('customSignature' in transaction && transaction.customSignature) || + ('paymaster' in transaction && transaction.paymaster) || + ('paymasterInput' in transaction && transaction.paymasterInput) || + ('gasPerPubdata' in transaction && + typeof transaction.gasPerPubdata === 'bigint') || + ('factoryDeps' in transaction && transaction.factoryDeps) + ) + return true + return false +} diff --git a/src/clients/createClient.ts b/src/clients/createClient.ts index 86e0e5747c..261ce90ce1 100644 --- a/src/clients/createClient.ts +++ b/src/clients/createClient.ts @@ -60,8 +60,11 @@ export type ClientConfig< // They are allowed to be extended, but must conform to their parameter & return type interfaces. // Example: an extended `call` action must accept `CallParameters` as parameters, // and conform to the `CallReturnType` return type. -type ExtendableProtectedActions = Pick< - PublicActions, +type ExtendableProtectedActions< + chain extends Chain | undefined = Chain | undefined, + account extends Account | undefined = Account | undefined, +> = Pick< + PublicActions, | 'call' | 'createContractEventFilter' | 'createEventFilter' @@ -86,7 +89,7 @@ type ExtendableProtectedActions = Pick< | 'watchBlockNumber' | 'watchContractEvent' > & - Pick + Pick, 'sendTransaction' | 'writeContract'> // TODO: Move `transport` to slot index 2 since `chain` and `account` used more frequently. // Otherwise, we end up with a lot of `Client` in actions. @@ -99,7 +102,8 @@ export type Client< > = Client_Base & (extended extends Extended ? extended : unknown) & { extend: < - const client extends Extended & Partial, + const client extends Extended & + Partial>, >( fn: ( client: Client, diff --git a/src/errors/chain.ts b/src/errors/chain.ts index c48c4579f4..209c07efbb 100644 --- a/src/errors/chain.ts +++ b/src/errors/chain.ts @@ -96,7 +96,11 @@ export type InvalidChainIdErrorType = InvalidChainIdError & { export class InvalidChainIdError extends BaseError { override name = 'InvalidChainIdError' - constructor({ chainId }: { chainId: number }) { - super(`Chain ID "${chainId}" is invalid.`) + constructor({ chainId }: { chainId?: number }) { + super( + typeof chainId === 'number' + ? `Chain ID "${chainId}" is invalid.` + : 'Chain ID is invalid.', + ) } } diff --git a/src/package.json b/src/package.json index 403742ce4f..add8dd718f 100644 --- a/src/package.json +++ b/src/package.json @@ -138,21 +138,12 @@ "license": "MIT", "homepage": "https://viem.sh", "repository": "wevm/viem", - "authors": [ - "awkweb.eth", - "jxom.eth" - ], + "authors": ["awkweb.eth", "jxom.eth"], "funding": [ { "type": "github", "url": "https://github.com/sponsors/wevm" } ], - "keywords": [ - "eth", - "ethereum", - "dapps", - "wallet", - "web3" - ] + "keywords": ["eth", "ethereum", "dapps", "wallet", "web3"] } diff --git a/src/types/chain.ts b/src/types/chain.ts index fb2e646907..344e750a3c 100644 --- a/src/types/chain.ts +++ b/src/types/chain.ts @@ -16,6 +16,9 @@ import type { SerializeTransactionFn } from '../utils/transaction/serializeTrans export type Chain< formatters extends ChainFormatters | undefined = ChainFormatters | undefined, + custom extends Record | undefined = + | Record + | undefined, > = { /** Collection of block explorers */ blockExplorers?: @@ -55,6 +58,8 @@ export type Chain< /** Flag for test networks */ testnet?: boolean | undefined + /** Custom chain data. */ + custom?: custom /** * Modifies how chain data structures (ie. Blocks, Transactions, etc) * are formatted & typed. diff --git a/test/globalSetup.ts b/test/globalSetup.ts index d6f01b4e07..ed1a3ac48c 100644 --- a/test/globalSetup.ts +++ b/test/globalSetup.ts @@ -2,6 +2,7 @@ import { startProxy } from '@viem/anvil' import { forkBlockNumber, forkUrl } from './src/constants.js' import { forkBlockNumberOptimism, forkUrlOptimism } from './src/opStack.js' +import { forkBlockNumberZkSync, forkUrlZkSync } from './src/zksync.js' export default async function () { if (process.env.SKIP_GLOBAL_SETUP) return @@ -25,22 +26,33 @@ export default async function () { // handled in `setup.ts` but may require additional resetting (e.g. via `afterAll`), in case of // any custom per-test adjustments that persist beyond `anvil_reset`. const shutdown_1 = await startProxy({ + port: Number(process.env.VITE_ANVIL_PORT || '8545'), + options: { + forkUrl, + forkBlockNumber, + noMining: true, + startTimeout: 20_000, + }, + }) + const shutdown_2 = await startProxy({ port: Number(process.env.VITE_ANVIL_PORT_OPTIMISM || '8645'), options: { forkUrl: forkUrlOptimism, forkBlockNumber: forkBlockNumberOptimism, + startTimeout: 20_000, }, }) - const shutdown_2 = await startProxy({ - port: Number(process.env.VITE_ANVIL_PORT || '8545'), + const shutdown_3 = await startProxy({ + port: Number(process.env.VITE_ANVIL_PORT_ZKSYNC || '8745'), options: { - forkUrl, - forkBlockNumber, - noMining: true, + forkUrl: forkUrlZkSync, + forkBlockNumber: forkBlockNumberZkSync, + startTimeout: 20_000, }, }) return () => { shutdown_1() shutdown_2() + shutdown_3() } } diff --git a/test/setup.ts b/test/setup.ts index 7148333c20..cd3c4750ef 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -29,7 +29,7 @@ beforeEach(async () => { if (process.env.SKIP_GLOBAL_SETUP) return await setIntervalMining(testClient, { interval: 0 }) -}) +}, 20_000) afterAll(async () => { vi.restoreAllMocks() diff --git a/test/src/abis.ts b/test/src/abis.ts index 5f138da06b..62e71f0da4 100644 --- a/test/src/abis.ts +++ b/test/src/abis.ts @@ -4482,3 +4482,46 @@ export const smartAccountConfig = { address: '0x3FCf42e10CC70Fe75A62EB3aDD6D305Aa840d145', abi: smartAccountAbi, } as const + +export const greeterContract = { + address: '0xbe9bcf56654fd81a921b6Bd07965Dd67Afbb0B69', + abi: [ + { + inputs: [ + { + internalType: 'string', + name: '_greeting', + type: 'string', + }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + inputs: [], + name: 'greet', + outputs: [ + { + internalType: 'string', + name: '', + type: 'string', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'string', + name: '_greeting', + type: 'string', + }, + ], + name: 'setGreeting', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + ], +} as const diff --git a/test/src/zksync.ts b/test/src/zksync.ts new file mode 100644 index 0000000000..d95e4d5e45 --- /dev/null +++ b/test/src/zksync.ts @@ -0,0 +1,65 @@ +import { zkSync } from '~viem/chains/index.js' +import { createClient } from '~viem/clients/createClient.js' +import { http, type Chain } from '~viem/index.js' +import { accounts, warn } from './constants.js' + +export let anvilPortZkSync: number +if (process.env.VITE_ANVIL_PORT_ZKSYNC) { + anvilPortZkSync = Number(process.env.VITE_ANVIL_PORT_ZKSYNC) +} else { + anvilPortZkSync = 8745 + warn( + `\`VITE_ANVIL_PORT_ZKSYNC\` not found. Falling back to \`${anvilPortZkSync}\`.`, + ) +} + +export let forkBlockNumberZkSync: bigint +if (process.env.VITE_ANVIL_BLOCK_NUMBER_ZKSYNC) { + forkBlockNumberZkSync = BigInt( + Number(process.env.VITE_ANVIL_BLOCK_NUMBER_ZKSYNC), + ) +} else { + forkBlockNumberZkSync = 24739066n + warn( + `\`VITE_ANVIL_BLOCK_NUMBER_ZKSYNC\` not found. Falling back to \`${forkBlockNumberZkSync}\`.`, + ) +} + +export let forkUrlZkSync: string +if (process.env.VITE_ANVIL_FORK_URL_ZKSYNC) { + forkUrlZkSync = process.env.VITE_ANVIL_FORK_URL_ZKSYNC +} else { + forkUrlZkSync = 'https://mainnet.era.zksync.io' + warn( + `\`VITE_ANVIL_FORK_URL_ZKSYNC\` not found. Falling back to \`${forkUrlZkSync}\`.`, + ) +} + +export const poolId = Number(process.env.VITEST_POOL_ID ?? 1) +export const localHttpUrlZkSync = `http://127.0.0.1:${anvilPortZkSync}/${poolId}` +export const localWsUrlZkSync = `ws://127.0.0.1:${anvilPortZkSync}/${poolId}` + +export const zksyncAnvilChain = { + ...zkSync, + rpcUrls: { + default: { + http: [localHttpUrlZkSync], + webSocket: [localWsUrlZkSync], + }, + public: { + http: [localHttpUrlZkSync], + webSocket: [localWsUrlZkSync], + }, + }, +} as const satisfies Chain + +export const zkSyncClient = createClient({ + chain: zksyncAnvilChain, + transport: http(), +}) + +export const zkSyncClientWithAccount = createClient({ + account: accounts[0].address, + chain: zksyncAnvilChain, + transport: http(), +}) diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000000..3c092d8c9a --- /dev/null +++ b/vercel.json @@ -0,0 +1,8 @@ +{ + "rewrites": [ + { + "source": "/:match*.html", + "destination": "/:match" + } + ] +}