diff --git a/.changeset/clean-ladybugs-cheer.md b/.changeset/clean-ladybugs-cheer.md new file mode 100644 index 0000000000..5322154836 --- /dev/null +++ b/.changeset/clean-ladybugs-cheer.md @@ -0,0 +1,5 @@ +--- +"viem": minor +--- + +**zkSync Extension:** Added L1 Public Actions. diff --git a/bun.lockb b/bun.lockb index 2fc3f891aa..5acd4178cb 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/site/pages/zksync/actions/getL1Allowance.md b/site/pages/zksync/actions/getL1Allowance.md new file mode 100644 index 0000000000..5ad37e7daf --- /dev/null +++ b/site/pages/zksync/actions/getL1Allowance.md @@ -0,0 +1,110 @@ +--- +description: Determines the amount of approved tokens for a specific L1 bridge. +--- + +# getL1Allowance + +Determines the amount of approved tokens for a specific L1 bridge. + +## Usage + +:::code-group + +```ts [example.ts] +import { account, publicClient } from './config' + +const allowance = await publicClient.getL1Allowance({ + account + token: '0x5C221E77624690fff6dd741493D735a17716c26B', + bridgeAddress: '0x84DbCC0B82124bee38e3Ce9a92CdE2f943bab60D', +}) +``` + +```ts [config.ts] +import { createPublicClient, custom } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { mainnet } from 'viem/chains' +import { publicActionsL1 } from 'viem/zksync' + +export const publicClient = createPublicClient({ + chain: mainnet, + transport: custom(window.ethereum) +}).extend(publicActionsL1()) + +// JSON-RPC Account +export const account = '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266' +// Local Account +export const account = privateKeyToAccount(...) +``` + +::: + +## Returns + +`bigint` + +Returns the amount of approved tokens. + +## Parameters + +### account + +- **Type:** `Account | Address` + +The Account used for check. + +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 allowance = await publicClient.getL1Allowance({ + account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266' // [!code focus] + blockTag: 'latest', + bridgeAddress: '0x84DbCC0B82124bee38e3Ce9a92CdE2f943bab60D', + token: '0x5C221E77624690fff6dd741493D735a17716c26B', +}) +``` + +### blockTag (optional) + +- **Type:** `BlockTag | undefined` + +In which block an allowance should be checked on. The latest processed one is the default option. + +```ts +const allowance = await publicClient.getL1Allowance({ + account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266' + blockTag: 'latest', // [!code focus] + bridgeAddress: '0x84DbCC0B82124bee38e3Ce9a92CdE2f943bab60D', + token: '0x5C221E77624690fff6dd741493D735a17716c26B', +}) +``` + +### bridgeAddress + +- **Type:** `Address` + +The address of the bridge contract to be used. + +```ts +const allowance = await publicClient.getL1Allowance({ + account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + blockTag: 'latest', + bridgeAddress: '0x84DbCC0B82124bee38e3Ce9a92CdE2f943bab60D', // [!code focus] + token: '0x5C221E77624690fff6dd741493D735a17716c26B', +}) +``` + +### token + +- **Type:** `Address` + +The Ethereum address of the token. + +```ts +const allowance = await publicClient.getL1Allowance({ + account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + blockTag: 'latest', + bridgeAddress: '0x84DbCC0B82124bee38e3Ce9a92CdE2f943bab60D', + token: '0x5C221E77624690fff6dd741493D735a17716c26B', // [!code focus] +}) +``` \ No newline at end of file diff --git a/site/pages/zksync/actions/getL1Balance.md b/site/pages/zksync/actions/getL1Balance.md new file mode 100644 index 0000000000..8e23bcfb7d --- /dev/null +++ b/site/pages/zksync/actions/getL1Balance.md @@ -0,0 +1,99 @@ +--- +description: Returns the amount of the token held by the account on the L1 network. +--- + +# getL1Balance + +Returns the amount of the token held by the account on the L1 network. + +## Usage + +:::code-group + +```ts [example.ts (token balance)] +import { account, publicClient } from './config' + +const balance = await publicClient.getL1Balance({ + account + token: '0x5C221E77624690fff6dd741493D735a17716c26B', +}) +``` + +```ts [example.ts (ETH balance)] +import { account, publicClient } from './config' + +const balance = await publicClient.getL1Balance({ + account +}) +``` + +```ts [config.ts] +import { createPublicClient, custom } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { mainnet } from 'viem/chains' +import { publicActionsL1 } from 'viem/zksync' + +export const publicClient = createPublicClient({ + chain: mainnet, + transport: custom(window.ethereum) +}).extend(publicActionsL1()) + +// JSON-RPC Account +export const account = '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266' +// Local Account +export const account = privateKeyToAccount(...) +``` + +::: + +## Returns + +`bigint` + +Returns the amount of the tokens. + +## Parameters + +### account (optional) + +- **Type:** `Account | Address` + +The Account used for check. + +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 balance = await publicClient.getL1Balance({ + account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266' // [!code focus] + blockTag: 'latest', + token: '0x5C221E77624690fff6dd741493D735a17716c26B', +}) +``` + +### blockTag (optional) + +- **Type:** `BlockTag | undefined` + +In which block an balance should be checked on. The latest processed one is the default option. + +```ts +const balance = await publicClient.getL1Balance({ + account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266' + blockTag: 'latest', // [!code focus] + token: '0x5C221E77624690fff6dd741493D735a17716c26B', +}) +``` + +### token (optional) + +- **Type:** `Address` + +The address of the token. Defaults to ETH if not provided. + +```ts +const balance = await publicClient.getL1Balance({ + account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + blockTag: 'latest', + token: '0x5C221E77624690fff6dd741493D735a17716c26B', // [!code focus] +}) +``` \ No newline at end of file diff --git a/site/pages/zksync/actions/getL1TokenBalance.md b/site/pages/zksync/actions/getL1TokenBalance.md new file mode 100644 index 0000000000..4db8ba217c --- /dev/null +++ b/site/pages/zksync/actions/getL1TokenBalance.md @@ -0,0 +1,91 @@ +--- +description: Retrieve the token balance held by the contract on L1. +--- + +# getL1TokenBalance + +Retrieve the token balance held by the contract on L1. + +## Usage + +:::code-group + +```ts [example.ts] +import { account, publicClient } from './config' + +const balance = await publicClient.getL1TokenBalance({ + account + token: '0x5C221E77624690fff6dd741493D735a17716c26B', +}) +``` + +```ts [config.ts] +import { createPublicClient, custom } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { mainnet } from 'viem/chains' +import { publicActionsL1 } from 'viem/zksync' + +export const publicClient = createPublicClient({ + chain: mainnet, + transport: custom(window.ethereum) +}).extend(publicActionsL1()) + +// JSON-RPC Account +export const account = '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266' +// Local Account +export const account = privateKeyToAccount(...) +``` + +::: + +## Returns + +`bigint` + +Returns the amount of the tokens. + +## Parameters + +### account + +- **Type:** `Account | Address` + +The Account used for check. + +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 balance = await publicClient.getL1TokenBalance({ + account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266' // [!code focus] + blockTag: 'latest', + token: '0x5C221E77624690fff6dd741493D735a17716c26B', +}) +``` + +### blockTag (optional) + +- **Type:** `BlockTag | undefined` + +In which block an balance should be checked on. The latest processed one is the default option. + +```ts +const balance = await publicClient.getL1TokenBalance({ + account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266' + blockTag: 'latest', // [!code focus] + token: '0x5C221E77624690fff6dd741493D735a17716c26B', +}) +``` + +### token + +- **Type:** `Address` + +The address of the token. + +```ts +const balance = await publicClient.getL1TokenBalance({ + account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + blockTag: 'latest', + token: '0x5C221E77624690fff6dd741493D735a17716c26B', // [!code focus] +}) +``` \ No newline at end of file diff --git a/site/pages/zksync/client.md b/site/pages/zksync/client.md index e0b950a7c1..162125f562 100644 --- a/site/pages/zksync/client.md +++ b/site/pages/zksync/client.md @@ -36,7 +36,7 @@ A suite of [Wallet Actions](/zksync/actions/sendTransaction) for suited for deve import { eip712WalletActions } from 'viem/zksync' ``` -### Sending transactions using paymaster +#### Sending transactions using paymaster [Read more](./actions/sendTransaction.md) @@ -50,7 +50,7 @@ const hash = await walletClient.sendTransaction({ }) ``` -### Calling contracts +#### Calling contracts [Read more](../docs/contract/writeContract.md) @@ -62,6 +62,14 @@ const { request } = await publicClient.simulateContract(walletClient, { abi: parseAbi(['function mint(uint32 tokenId) nonpayable']), functionName: 'mint', args: [69420], -} +}); const hash = await walletClient.writeContract(request) ``` + +### `publicActionsL1` + +A suite of [Public Actions](/zksync/actions/getL1Allowance) suited for development with **Layer 1** chains. These actions provide functionalities specific to public clients operating at the Layer 1 level, enabling them to interact seamlessly with Layer 2 protocols. + +```ts +import { publicActionsL1 } from 'viem/zksync' +``` diff --git a/site/sidebar.ts b/site/sidebar.ts index 84ad9123bc..0aed8371b2 100644 --- a/site/sidebar.ts +++ b/site/sidebar.ts @@ -1350,97 +1350,109 @@ export const sidebar = { ], }, { - text: 'Actions', + text: 'EIP-712 Actions', items: [ { - text: 'EIP-712 Actions', - items: [ - { - text: 'deployContract', - link: '/zksync/actions/deployContract', - }, - { - text: 'sendTransaction', - link: '/zksync/actions/sendTransaction', - }, - { - text: 'signTransaction', - link: '/zksync/actions/signTransaction', - }, - { - text: 'writeContract', - link: '/zksync/actions/writeContract', - }, - ], + text: 'deployContract', + link: '/zksync/actions/deployContract', }, { - text: 'L2 Public Actions', - items: [ - { - text: 'estimateFee', - link: '/zksync/actions/estimateFee', - }, - { - text: 'estimateGasL1ToL2', - link: '/zksync/actions/estimateGasL1ToL2', - }, - { - text: 'getAllBalances', - link: '/zksync/actions/getAllBalances', - }, - { - text: 'getBaseTokenL1Address', - link: '/zksync/actions/getBaseTokenL1Address', - }, - { - text: 'getBlockDetails', - link: '/zksync/actions/getBlockDetails', - }, - { - text: 'getBridgehubContractAddress', - link: '/zksync/actions/getBridgehubContractAddress', - }, - { - text: 'getDefaultBridgeAddress', - link: '/zksync/actions/getDefaultBridgeAddress', - }, - { - text: 'getL1BatchDetails', - link: '/zksync/actions/getL1BatchDetails', - }, - { - text: 'getL1BatchBlockRange', - link: '/zksync/actions/getL1BatchBlockRange', - }, - { - text: 'getL1BatchNumber', - link: '/zksync/actions/getL1BatchNumber', - }, - { - text: 'getL1ChainId', - link: '/zksync/actions/getL1ChainId', - }, - { - text: 'getLogProof', - link: '/zksync/actions/getLogProof', - }, - { - text: 'getMainContractAddress', - link: '/zksync/actions/getMainContractAddress', - }, - { - text: 'getRawBlockTransaction', - link: '/zksync/actions/getRawBlockTransactions', - }, - { - text: 'getTestnetPaymasterAddress', - link: '/zksync/actions/getTestnetPaymasterAddress', - }, - { - text: 'getTransactionDetails', - link: '/zksync/actions/getTransactionDetails', - }, - ], + text: 'sendTransaction', + link: '/zksync/actions/sendTransaction', + }, + { + text: 'signTransaction', + link: '/zksync/actions/signTransaction', + }, + { + text: 'writeContract', + link: '/zksync/actions/writeContract', + }, + ], + }, + { + text: 'L2 Public Actions', + items: [ + { + text: 'estimateFee', + link: '/zksync/actions/estimateFee', + }, + { + text: 'estimateGasL1ToL2', + link: '/zksync/actions/estimateGasL1ToL2', + }, + { + text: 'getAllBalances', + link: '/zksync/actions/getAllBalances', + }, + { + text: 'getBaseTokenL1Address', + link: '/zksync/actions/getBaseTokenL1Address', + }, + { + text: 'getBlockDetails', + link: '/zksync/actions/getBlockDetails', + }, + { + text: 'getBridgehubContractAddress', + link: '/zksync/actions/getBridgehubContractAddress', + }, + { + text: 'getDefaultBridgeAddress', + link: '/zksync/actions/getDefaultBridgeAddress', + }, + { + text: 'getL1BatchDetails', + link: '/zksync/actions/getL1BatchDetails', + }, + { + text: 'getL1BatchBlockRange', + link: '/zksync/actions/getL1BatchBlockRange', + }, + { + text: 'getL1BatchNumber', + link: '/zksync/actions/getL1BatchNumber', + }, + { + text: 'getL1ChainId', + link: '/zksync/actions/getL1ChainId', + }, + { + text: 'getLogProof', + link: '/zksync/actions/getLogProof', + }, + { + text: 'getMainContractAddress', + link: '/zksync/actions/getMainContractAddress', + }, + { + text: 'getRawBlockTransaction', + link: '/zksync/actions/getRawBlockTransactions', + }, + { + text: 'getTestnetPaymasterAddress', + link: '/zksync/actions/getTestnetPaymasterAddress', + }, + { + text: 'getTransactionDetails', + link: '/zksync/actions/getTransactionDetails', + }, + ], + }, + { + text: 'L1 Public Actions', + items: [ + { + text: 'getL1Allowance', + link: '/zksync/actions/getL1Allowance', + }, + { + text: 'getL1Balance', + link: '/zksync/actions/getL1Balance', + }, + { + text: 'getL1TokenBalance', + link: '/zksync/actions/getL1TokenBalance', }, ], }, diff --git a/src/zksync/actions/getL1Allowance.test.ts b/src/zksync/actions/getL1Allowance.test.ts new file mode 100644 index 0000000000..c2f020e35f --- /dev/null +++ b/src/zksync/actions/getL1Allowance.test.ts @@ -0,0 +1,131 @@ +import { afterAll, expect, test, vi } from 'vitest' + +import { accounts } from '~test/src/constants.js' +import { privateKeyToAccount } from '../../accounts/privateKeyToAccount.js' + +import { sepolia } from '~viem/chains/index.js' +import { erc20Abi } from '~viem/constants/abis.js' +import * as readContract from '../../actions/public/readContract.js' +import { + http, + createClient, + createPublicClient, + createWalletClient, +} from '../../index.js' +import { publicActionsL1 } from '../decorators/publicL1.js' +import { getL1Allowance } from './getL1Allowance.js' + +const sourceAccount = accounts[0] +const tokenL1 = '0x5C221E77624690fff6dd741493D735a17716c26B' +const account = privateKeyToAccount(sourceAccount.privateKey) +const spy = vi.spyOn(readContract, 'readContract').mockResolvedValue(170n) + +afterAll(() => { + spy.mockRestore() +}) + +test('default with account hoisting', async () => { + const client = createWalletClient({ + chain: sepolia, + transport: http(), + account, + }).extend(publicActionsL1()) + + expect( + await getL1Allowance(client, { + token: tokenL1, + bridgeAddress: '0x84DbCC0B82124bee38e3Ce9a92CdE2f943bab60D', + }), + ).toBe(170n) + + expect(spy).toHaveBeenCalledWith(client, { + abi: erc20Abi, + address: tokenL1, + functionName: 'allowance', + args: [ + '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + '0x84DbCC0B82124bee38e3Ce9a92CdE2f943bab60D', + ], + blockTag: undefined, + }) +}) + +test('args: blockTag with account hoisting', async () => { + const client = createClient({ + chain: sepolia, + transport: http(), + account, + }).extend(publicActionsL1()) + + expect( + await getL1Allowance(client, { + token: tokenL1, + bridgeAddress: '0x84DbCC0B82124bee38e3Ce9a92CdE2f943bab60D', + blockTag: 'finalized', + }), + ).toBe(170n) + + expect(spy).toHaveBeenCalledWith(client, { + abi: erc20Abi, + address: tokenL1, + functionName: 'allowance', + args: [ + '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + '0x84DbCC0B82124bee38e3Ce9a92CdE2f943bab60D', + ], + blockTag: 'finalized', + }) +}) + +test('default with account provided to the method', async () => { + const client = createPublicClient({ + chain: sepolia, + transport: http(), + }).extend(publicActionsL1()) + + expect( + await getL1Allowance(client, { + token: tokenL1, + bridgeAddress: '0x84DbCC0B82124bee38e3Ce9a92CdE2f943bab60D', + account, + }), + ).toBe(170n) + + expect(spy).toHaveBeenCalledWith(client, { + abi: erc20Abi, + address: tokenL1, + functionName: 'allowance', + args: [ + '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + '0x84DbCC0B82124bee38e3Ce9a92CdE2f943bab60D', + ], + blockTag: undefined, + }) +}) + +test('args: blockTag with account provided to the method', async () => { + const client = createPublicClient({ + chain: sepolia, + transport: http(), + }).extend(publicActionsL1()) + + expect( + await getL1Allowance(client, { + token: tokenL1, + bridgeAddress: '0x84DbCC0B82124bee38e3Ce9a92CdE2f943bab60D', + account, + blockTag: 'finalized', + }), + ).toBe(170n) + + expect(spy).toHaveBeenCalledWith(client, { + abi: erc20Abi, + address: tokenL1, + functionName: 'allowance', + args: [ + '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + '0x84DbCC0B82124bee38e3Ce9a92CdE2f943bab60D', + ], + blockTag: 'finalized', + }) +}) diff --git a/src/zksync/actions/getL1Allowance.ts b/src/zksync/actions/getL1Allowance.ts new file mode 100644 index 0000000000..198ddce804 --- /dev/null +++ b/src/zksync/actions/getL1Allowance.ts @@ -0,0 +1,43 @@ +import type { Address } from 'abitype' +import { readContract } from '../../actions/public/readContract.js' +import type { Client } from '../../clients/createClient.js' +import type { Transport } from '../../clients/transports/createTransport.js' +import { erc20Abi } from '../../constants/abis.js' +import type { AccountNotFoundError } from '../../errors/account.js' +import type { BaseError } from '../../errors/base.js' +import type { Account, GetAccountParameter } from '../../types/account.js' +import type { BlockTag } from '../../types/block.js' +import type { Chain } from '../../types/chain.js' +import { parseAccount } from '../../utils/accounts.js' + +export type GetL1AllowanceParameters< + TAccount extends Account | undefined = Account | undefined, +> = GetAccountParameter & { + bridgeAddress: Address + blockTag?: BlockTag + token: Address +} + +export type GetL1AllowanceReturnType = bigint + +export type GetL1AllowanceErrorType = AccountNotFoundError | BaseError + +export async function getL1Allowance< + TChain extends Chain | undefined, + TAccount extends Account | undefined, +>( + client: Client, + parameters: GetL1AllowanceParameters, +): Promise { + const { token, bridgeAddress, blockTag, account: account_ } = parameters + + const account = account_ ? parseAccount(account_) : client.account + + return await readContract(client, { + abi: erc20Abi, + address: token, + functionName: 'allowance', + args: [account!.address, bridgeAddress], + blockTag: blockTag, + }) +} diff --git a/src/zksync/actions/getL1Balance.test.ts b/src/zksync/actions/getL1Balance.test.ts new file mode 100644 index 0000000000..a881f7ab8f --- /dev/null +++ b/src/zksync/actions/getL1Balance.test.ts @@ -0,0 +1,160 @@ +import { afterAll, expect, test, vi } from 'vitest' + +import { accounts } from '~test/src/constants.js' +import { privateKeyToAccount } from '../../accounts/privateKeyToAccount.js' + +import * as readContract from '../../actions/public/readContract.js' +import { sepolia } from '../../chains/index.js' +import { erc20Abi } from '../../constants/abis.js' +import { http, createClient, createPublicClient } from '../../index.js' +import { getL1Balance } from './getL1Balance.js' + +const sourceAccount = accounts[0] +const tokenL1 = '0x5C221E77624690fff6dd741493D735a17716c26B' +const account = privateKeyToAccount(sourceAccount.privateKey) +const spy = vi.spyOn(readContract, 'readContract').mockResolvedValue(170n) + +afterAll(() => { + spy.mockRestore() +}) + +test('default with account hoisting and token', async () => { + const client = createClient({ + chain: sepolia, + transport: http(), + account, + }) + + expect( + await getL1Balance(client, { + token: tokenL1, + }), + ).toBeDefined() + + expect(spy).toHaveBeenCalledWith(client, { + abi: erc20Abi, + address: tokenL1, + functionName: 'balanceOf', + args: ['0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'], + blockTag: undefined, + }) +}) + +test('args: blockTag with account hoisting and token', async () => { + const client = createClient({ + chain: sepolia, + transport: http(), + account, + }) + + expect( + await getL1Balance(client, { + token: tokenL1, + blockTag: 'finalized', + }), + ).toBe(170n) + + expect(spy).toHaveBeenCalledWith(client, { + abi: erc20Abi, + address: tokenL1, + functionName: 'balanceOf', + args: ['0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'], + blockTag: 'finalized', + }) +}) + +test('default with account provided to the method and token', async () => { + const client = createPublicClient({ + chain: sepolia, + transport: http(), + }) + + expect( + await getL1Balance(client, { + token: tokenL1, + account, + }), + ).toBe(170n) + + expect(spy).toHaveBeenCalledWith(client, { + abi: erc20Abi, + address: tokenL1, + functionName: 'balanceOf', + args: ['0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'], + blockTag: undefined, + }) +}) + +test('args: blockTag with account provided to the method and token', async () => { + const client = createPublicClient({ + chain: sepolia, + transport: http(), + }) + + expect( + await getL1Balance(client, { + token: tokenL1, + account, + blockTag: 'finalized', + }), + ).toBe(170n) + + expect(spy).toHaveBeenCalledWith(client, { + abi: erc20Abi, + address: tokenL1, + functionName: 'balanceOf', + args: ['0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'], + blockTag: 'finalized', + }) +}) + +test('default with account hoisting', async () => { + const client = createClient({ + chain: sepolia, + transport: http(), + account, + }) + + expect(await getL1Balance(client)).toBeDefined() +}) + +test('args: blockTag with account hoisting', async () => { + const client = createClient({ + chain: sepolia, + transport: http(), + account, + }) + + expect( + await getL1Balance(client, { + blockTag: 'finalized', + }), + ).toBeDefined() +}) + +test('default with account provided to the method', async () => { + const client = createPublicClient({ + chain: sepolia, + transport: http(), + }) + + expect( + await getL1Balance(client, { + account, + }), + ).toBeDefined() +}) + +test('args: blockTag with account provided to the method', async () => { + const client = createPublicClient({ + chain: sepolia, + transport: http(), + }) + + expect( + await getL1Balance(client, { + account, + blockTag: 'finalized', + }), + ).toBeDefined() +}) diff --git a/src/zksync/actions/getL1Balance.ts b/src/zksync/actions/getL1Balance.ts new file mode 100644 index 0000000000..097354db2f --- /dev/null +++ b/src/zksync/actions/getL1Balance.ts @@ -0,0 +1,68 @@ +import type { Address } from 'abitype' +import { + type GetBalanceParameters, + getBalance, +} from '../../actions/public/getBalance.js' +import type { Client } from '../../clients/createClient.js' +import type { Transport } from '../../clients/transports/createTransport.js' +import type { AccountNotFoundError } from '../../errors/account.js' +import type { BaseError } from '../../errors/base.js' +import type { Account, GetAccountParameter } from '../../types/account.js' +import type { BlockTag } from '../../types/block.js' +import type { Chain } from '../../types/chain.js' +import { parseAccount } from '../../utils/accounts.js' +import { legacyEthAddress } from '../constants/address.js' +import { isEth } from '../utils/isEth.js' +import { + type GetL1TokenBalanceParameters, + getL1TokenBalance, +} from './getL1TokenBalance.js' + +export type GetL1BalanceParameters< + TAccount extends Account | undefined = Account | undefined, +> = GetAccountParameter & { token?: Address | undefined } & ( + | { + /** The balance of the account at a block number. */ + blockNumber?: bigint | undefined + blockTag?: never | undefined + } + | { + blockNumber?: never | undefined + /** The balance of the account at a block tag. */ + blockTag?: BlockTag | undefined + } + ) + +export type GetL1BalanceReturnType = bigint + +export type GetL1BalanceErrorType = AccountNotFoundError | BaseError + +export async function getL1Balance< + TChain extends Chain | undefined, + TAccount extends Account | undefined, +>( + client: Client, + ...[parameters = {}]: TAccount extends undefined + ? [GetL1BalanceParameters] + : [GetL1BalanceParameters] | [] +): Promise { + const { + account: account_ = client.account, + blockNumber, + blockTag, + token = legacyEthAddress, + } = parameters + + const account = account_ ? parseAccount(account_) : undefined + + if (isEth(token)) + return await getBalance(client, { + address: account!.address, + blockNumber, + blockTag, + } as GetBalanceParameters) + + return await getL1TokenBalance(client, { + ...(parameters as GetL1TokenBalanceParameters), + }) +} diff --git a/src/zksync/actions/getL1TokenBalance.test.ts b/src/zksync/actions/getL1TokenBalance.test.ts new file mode 100644 index 0000000000..c065a340a2 --- /dev/null +++ b/src/zksync/actions/getL1TokenBalance.test.ts @@ -0,0 +1,169 @@ +import { afterAll, expect, test, vi } from 'vitest' + +import { accounts } from '~test/src/constants.js' +import { privateKeyToAccount } from '../../accounts/privateKeyToAccount.js' + +import * as readContract from '../../actions/public/readContract.js' +import { sepolia } from '../../chains/index.js' +import { erc20Abi } from '../../constants/abis.js' +import { http, createClient, createPublicClient } from '../../index.js' +import { + ethAddressInContracts, + l2BaseTokenAddress, + legacyEthAddress, +} from '../constants/address.js' +import { getL1TokenBalance } from './getL1TokenBalance.js' + +const sourceAccount = accounts[0] +const tokenL1 = '0x5C221E77624690fff6dd741493D735a17716c26B' +const account = privateKeyToAccount(sourceAccount.privateKey) +const spy = vi.spyOn(readContract, 'readContract').mockResolvedValue(170n) + +afterAll(() => { + spy.mockRestore() +}) + +test('default with account hoisting', async () => { + const client = createClient({ + chain: sepolia, + transport: http(), + account, + }) + + expect( + await getL1TokenBalance(client, { + token: tokenL1, + }), + ).toBe(170n) + + expect(spy).toHaveBeenCalledWith(client, { + abi: erc20Abi, + address: tokenL1, + functionName: 'balanceOf', + args: ['0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'], + blockTag: undefined, + }) +}) + +test('args: blockTag with account hoisting', async () => { + const client = createClient({ + chain: sepolia, + transport: http(), + account, + }) + + expect( + await getL1TokenBalance(client, { + token: tokenL1, + blockTag: 'finalized', + }), + ).toBe(170n) + + expect(spy).toHaveBeenCalledWith(client, { + abi: erc20Abi, + address: tokenL1, + functionName: 'balanceOf', + args: ['0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'], + blockTag: 'finalized', + }) +}) + +test('default with account provided to the method', async () => { + const client = createPublicClient({ + chain: sepolia, + transport: http(), + }) + + expect( + await getL1TokenBalance(client, { + token: tokenL1, + account, + }), + ).toBe(170n) + + expect(spy).toHaveBeenCalledWith(client, { + abi: erc20Abi, + address: tokenL1, + functionName: 'balanceOf', + args: ['0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'], + blockTag: undefined, + }) +}) + +test('args: blockTag with account provided to the method', async () => { + const client = createPublicClient({ + chain: sepolia, + transport: http(), + }) + + expect( + await getL1TokenBalance(client, { + token: tokenL1, + account, + blockTag: 'finalized', + }), + ).toBe(170n) + + expect(spy).toHaveBeenCalledWith(client, { + abi: erc20Abi, + address: tokenL1, + functionName: 'balanceOf', + args: ['0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'], + blockTag: 'finalized', + }) +}) + +test('provided token is ETH', async () => { + const client = createPublicClient({ + chain: sepolia, + transport: http(), + }) + + await expect(() => + getL1TokenBalance(client, { + token: ethAddressInContracts, + account, + blockTag: 'finalized', + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + ` + [TokenIsEthError: Token is an ETH token. + + ETH token cannot be retrived. + + Version: viem@1.0.2] + `, + ) + + await expect(() => + getL1TokenBalance(client, { + token: legacyEthAddress, + account, + blockTag: 'finalized', + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + ` + [TokenIsEthError: Token is an ETH token. + + ETH token cannot be retrived. + + Version: viem@1.0.2] + `, + ) + + await expect(() => + getL1TokenBalance(client, { + token: l2BaseTokenAddress, + account, + blockTag: 'finalized', + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + ` + [TokenIsEthError: Token is an ETH token. + + ETH token cannot be retrived. + + Version: viem@1.0.2] + `, + ) +}) diff --git a/src/zksync/actions/getL1TokenBalance.ts b/src/zksync/actions/getL1TokenBalance.ts new file mode 100644 index 0000000000..ec534f0bc9 --- /dev/null +++ b/src/zksync/actions/getL1TokenBalance.ts @@ -0,0 +1,63 @@ +import type { Address } from '../../accounts/index.js' +import { readContract } from '../../actions/index.js' +import type { Client } from '../../clients/createClient.js' +import type { Transport } from '../../clients/transports/createTransport.js' +import { erc20Abi } from '../../constants/abis.js' +import type { AccountNotFoundError } from '../../errors/account.js' +import type { BaseError } from '../../errors/base.js' +import type { Account, GetAccountParameter } from '../../types/account.js' +import type { BlockTag } from '../../types/block.js' +import type { Chain } from '../../types/chain.js' +import { parseAccount } from '../../utils/accounts.js' +import { TokenIsEthError } from '../errors/token-is-eth.js' +import { isEth } from '../utils/isEth.js' + +export type GetL1TokenBalanceParameters< + TAccount extends Account | undefined = Account | undefined, +> = GetAccountParameter & { token: Address } & ( + | { + /** The balance of the account at a block number. */ + blockNumber?: bigint | undefined + blockTag?: never | undefined + } + | { + blockNumber?: never | undefined + /** The balance of the account at a block tag. */ + blockTag?: BlockTag | undefined + } + ) + +export type GetL1TokenBalanceReturnType = bigint + +export type GetL1TokenBalanceErrorType = + | AccountNotFoundError + | BaseError + | TokenIsEthError + +export async function getL1TokenBalance< + TChain extends Chain | undefined, + TAccount extends Account | undefined, +>( + client: Client, + parameters: GetL1TokenBalanceParameters, +): Promise { + const { + account: account_ = client.account, + blockTag, + blockNumber, + token, + } = parameters + + if (isEth(token!)) throw new TokenIsEthError() + + const account = account_ ? parseAccount(account_) : client.account + + return await readContract(client, { + abi: erc20Abi, + address: token!, + functionName: 'balanceOf', + args: [account!.address], + blockNumber: blockNumber, + blockTag: blockTag, + }) +} diff --git a/src/zksync/actions/getRawBlockTransaction.test.ts b/src/zksync/actions/getRawBlockTransactions.test.ts similarity index 99% rename from src/zksync/actions/getRawBlockTransaction.test.ts rename to src/zksync/actions/getRawBlockTransactions.test.ts index 925e5f3977..35bfd370d7 100644 --- a/src/zksync/actions/getRawBlockTransaction.test.ts +++ b/src/zksync/actions/getRawBlockTransactions.test.ts @@ -3,7 +3,7 @@ import { mockClientPublicActionsL2, zkSyncClientLocalNode, } from '../../../test/src/zksync.js' -import { getRawBlockTransactions } from './getRawBlockTransaction.js' +import { getRawBlockTransactions } from './getRawBlockTransactions.js' const client = { ...zkSyncClientLocalNode } diff --git a/src/zksync/actions/getRawBlockTransaction.ts b/src/zksync/actions/getRawBlockTransactions.ts similarity index 72% rename from src/zksync/actions/getRawBlockTransaction.ts rename to src/zksync/actions/getRawBlockTransactions.ts index 94287e2c50..5d2c6130e7 100644 --- a/src/zksync/actions/getRawBlockTransaction.ts +++ b/src/zksync/actions/getRawBlockTransactions.ts @@ -7,20 +7,20 @@ import type { PublicZkSyncRpcSchema } from '../types/eip1193.js' import type { ZkSyncRawBlockTransactions } from '../types/transaction.js' import { camelCaseKeys } from '../utils/camelCaseKeys.js' -export type GetRawBlockTransactionParameters = ZkSyncNumberParameter +export type GetRawBlockTransactionsParameters = ZkSyncNumberParameter -export type GetRawBlockTransactionReturnType = ZkSyncRawBlockTransactions +export type GetRawBlockTransactionsReturnType = ZkSyncRawBlockTransactions export async function getRawBlockTransactions< TChain extends Chain | undefined, TAccount extends Account | undefined, >( client: Client, - parameters: GetRawBlockTransactionParameters, -): Promise { + parameters: GetRawBlockTransactionsParameters, +): Promise { const result = await client.request({ method: 'zks_getRawBlockTransactions', params: [parameters.number], }) - return camelCaseKeys(result) as GetRawBlockTransactionReturnType + return camelCaseKeys(result) as GetRawBlockTransactionsReturnType } diff --git a/src/zksync/constants/address.ts b/src/zksync/constants/address.ts index 9366eb728f..dd8e4835aa 100644 --- a/src/zksync/constants/address.ts +++ b/src/zksync/constants/address.ts @@ -1,2 +1,11 @@ export const contractDeployerAddress = '0x0000000000000000000000000000000000008006' as const + +export const legacyEthAddress = + '0x0000000000000000000000000000000000000000' as const + +export const ethAddressInContracts = + '0x0000000000000000000000000000000000000001' as const + +export const l2BaseTokenAddress = + '0x000000000000000000000000000000000000800a' as const diff --git a/src/zksync/decorators/publicL1.test.ts b/src/zksync/decorators/publicL1.test.ts new file mode 100644 index 0000000000..f18382eaed --- /dev/null +++ b/src/zksync/decorators/publicL1.test.ts @@ -0,0 +1,80 @@ +import { afterAll, expect, test, vi } from 'vitest' + +import { accounts } from '~test/src/constants.js' +import { sepolia } from '~viem/chains/index.js' +import { createPublicClient } from '~viem/clients/createPublicClient.js' +import { createWalletClient } from '~viem/clients/createWalletClient.js' +import { http } from '~viem/clients/transports/http.js' +import { privateKeyToAccount } from '../../accounts/privateKeyToAccount.js' +import * as readContract from '../../actions/public/readContract.js' +import { publicActionsL1 } from './publicL1.js' + +const client = createPublicClient({ + chain: sepolia, + transport: http(), +}).extend(publicActionsL1()) + +const clientWithAccount = createWalletClient({ + chain: sepolia, + transport: http(), + account: privateKeyToAccount(accounts[0].privateKey), +}).extend(publicActionsL1()) + +const spy = vi.spyOn(readContract, 'readContract').mockResolvedValue(170n) + +afterAll(() => { + spy.mockRestore() +}) + +test('default', async () => { + expect(publicActionsL1()(client)).toMatchInlineSnapshot(` + { + "getL1Allowance": [Function], + "getL1Balance": [Function], + "getL1TokenBalance": [Function], + } + `) +}) + +test('getL1Allowance', async () => { + expect( + await client.getL1Allowance({ + bridgeAddress: '0x84DbCC0B82124bee38e3Ce9a92CdE2f943bab60D', + token: '0x5C221E77624690fff6dd741493D735a17716c26B', + account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + }), + ).toBe(170n) + + expect( + await clientWithAccount.getL1Allowance({ + bridgeAddress: '0x84DbCC0B82124bee38e3Ce9a92CdE2f943bab60D', + token: '0x5C221E77624690fff6dd741493D735a17716c26B', + }), + ).toBe(170n) +}) + +test('getL1TokenBalance', async () => { + expect( + await client.getL1TokenBalance({ + token: '0x5C221E77624690fff6dd741493D735a17716c26B', + account: accounts[0].privateKey, + }), + ).toBe(170n) + + expect( + await clientWithAccount.getL1TokenBalance({ + token: '0x5C221E77624690fff6dd741493D735a17716c26B', + }), + ).toBe(170n) +}) + +test('getL1Balance', async () => { + expect( + await client.getL1Balance({ + token: '0x5C221E77624690fff6dd741493D735a17716c26B', + account: accounts[0].address, + }), + ).toBe(170n) + + expect(await clientWithAccount.getL1Balance()).toBeDefined() +}) diff --git a/src/zksync/decorators/publicL1.ts b/src/zksync/decorators/publicL1.ts new file mode 100644 index 0000000000..6175eac8ca --- /dev/null +++ b/src/zksync/decorators/publicL1.ts @@ -0,0 +1,180 @@ +import type { Chain } from '../../chains/index.js' +import type { Client } from '../../clients/createClient.js' +import type { Transport } from '../../clients/transports/createTransport.js' +import type { Account } from '../../types/account.js' +import { + type GetL1AllowanceParameters, + type GetL1AllowanceReturnType, + getL1Allowance, +} from '../actions/getL1Allowance.js' +import { + type GetL1BalanceParameters, + type GetL1BalanceReturnType, + getL1Balance, +} from '../actions/getL1Balance.js' +import { + type GetL1TokenBalanceParameters, + type GetL1TokenBalanceReturnType, + getL1TokenBalance, +} from '../actions/getL1TokenBalance.js' + +export type PublicActionsL1< + TAccount extends Account | undefined = Account | undefined, +> = { + /** + * Returns the amount of approved tokens for a specific L1 bridge. + * + * - Docs: https://viem.sh/zksync/actions/getL1Allowance + * + * @param client - Client to use + * @param parameters - {@link AllowanceL1Parameters} + * @returns The amount of approved tokens for a specific L1 bridge. {@link GetL1AllowanceReturnType} + * + * @example + * import { createPublicClient, custom, parseEther } from 'viem' + * import { base, mainnet } from 'viem/chains' + * import { publicActionsL1 } from 'viem/zksync' + * + * const client = createPublicClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }).extend(publicActionsL1()) + * + * const data = await client.getL1Allowance({ + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * token: '0x5C221E77624690fff6dd741493D735a17716c26B' + * bridgeAddress: '0x84DbCC0B82124bee38e3Ce9a92CdE2f943bab60D', + * }) + * + * @example + * // Account Hoisting + * import { createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { base, mainnet } from 'viem/chains' + * import { publicActionsL1 } from 'viem/zksync' + * + * const client = createWalletClient({ + * account: privateKeyToAccount('0x…'), + * chain: mainnet, + * transport: http(), + * }).extend(publicActionsL1()) + * + * const data = await client.getL1Allowance({ + * token: '0x5C221E77624690fff6dd741493D735a17716c26B' + * bridgeAddress: '0x84DbCC0B82124bee38e3Ce9a92CdE2f943bab60D', + * }) + */ + getL1Allowance: ( + parameters: GetL1AllowanceParameters, + ) => Promise + /** + * Returns the amount of the ERC20 token the client has on specific address. + * + * - Docs: https://viem.sh/zksync/actions/getL1TokenBalance + * + * @param client - Client to use + * @param parameters - {@link GetL1TokenBalanceParameters} + * @returns The amount of the ERC20 token the client has on specific addresse. {@link GetL1TokenBalanceReturnType} + * + * @example + * import { createPublicClient, custom, parseEther } from 'viem' + * import { base, mainnet } from 'viem/chains' + * import { publicActionsL1 } from 'viem/zksync' + * + * const client = createPublicClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }).extend(publicActionsL1()) + * + * const data = await client.getL1TokenBalance({ + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * token: '0x5C221E77624690fff6dd741493D735a17716c26B' + * }) + * + * @example + * // Account Hoisting + * import { createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { base, mainnet } from 'viem/chains' + * import { publicActionsL1 } from 'viem/zksync' + * + * const client = createWalletClient({ + * account: privateKeyToAccount('0x…'), + * chain: mainnet, + * transport: http(), + * }).extend(publicActionsL1()) + * + * const data = await client.getL1TokenBalance({ + * token: '0x5C221E77624690fff6dd741493D735a17716c26B' + * }) + */ + getL1TokenBalance: ( + parameters: GetL1TokenBalanceParameters, + ) => Promise + /** + * Returns the amount of the token held by the account on the L1 network. + * + * - Docs: https://viem.sh/zksync/actions/getL1TokenBalance + * + * @param client - Client to use + * @param parameters - {@link BalanceL1Parameters} + * @returns Returns the amount of the token held by the account on the L1 network. {@link GetL1BalanceReturnType} + * + * @example + * import { createPublicClient, custom, parseEther } from 'viem' + * import { base, mainnet } from 'viem/chains' + * import { publicActionsL1 } from 'viem/zksync' + * + * const client = createPublicClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }).extend(publicActionsL1()) + * + * const data = await client.getL1Balance({ + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e' + * }) + * + * const data = await client.getL1Balance({ + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * token: '0x5C221E77624690fff6dd741493D735a17716c26B' + * }) + * + * @example + * // Account Hoisting + * import { createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { base, mainnet } from 'viem/chains' + * import { publicActionsL1 } from 'viem/zksync' + * + * const client = createWalletClient({ + * account: privateKeyToAccount('0x…'), + * chain: mainnet, + * transport: http(), + * }).extend(publicActionsL1()) + * + * const data = await client.getL1Balance({}) + * + * const data = await client.getL1Balance({ + * token: '0x5C221E77624690fff6dd741493D735a17716c26B' + * }) + */ + getL1Balance: ( + ...parameters: TAccount extends undefined + ? [GetL1BalanceParameters] + : [GetL1BalanceParameters] | [] + ) => Promise +} + +export function publicActionsL1() { + return < + TChain extends Chain | undefined = Chain | undefined, + TAccount extends Account | undefined = Account | undefined, + >( + client: Client, + ): PublicActionsL1 => ({ + getL1Allowance: (args) => getL1Allowance(client, args), + getL1TokenBalance: (args) => getL1TokenBalance(client, args), + // @ts-expect-error + getL1Balance: (args) => getL1Balance(client, args), + }) +} diff --git a/src/zksync/decorators/publicL2.ts b/src/zksync/decorators/publicL2.ts index 3ba9fd446f..9b76494daa 100644 --- a/src/zksync/decorators/publicL2.ts +++ b/src/zksync/decorators/publicL2.ts @@ -2,7 +2,6 @@ import type { Address } from 'abitype' import type { Client } from '../../clients/createClient.js' import type { Transport } from '../../clients/transports/createTransport.js' import type { Account } from '../../types/account.js' -import type { Hex } from '../../types/misc.js' import { estimateFee } from '../actions/estimateFee.js' import type { EstimateFeeParameters, @@ -10,6 +9,7 @@ import type { } from '../actions/estimateFee.js' import { type EstimateGasL1ToL2Parameters, + type EstimateGasL1ToL2ReturnType, estimateGasL1ToL2, } from '../actions/estimateGasL1ToL2.js' import { @@ -17,13 +17,19 @@ import { type GetAllBalancesReturnType, getAllBalances, } from '../actions/getAllBalances.js' -import { getBaseTokenL1Address } from '../actions/getBaseTokenL1Address.js' +import { + type GetBaseTokenL1AddressReturnType, + getBaseTokenL1Address, +} from '../actions/getBaseTokenL1Address.js' import { type GetBlockDetailsParameters, type GetBlockDetailsReturnType, getBlockDetails, } from '../actions/getBlockDetails.js' -import { getBridgehubContractAddress } from '../actions/getBridgehubContractAddress.js' +import { + type GetBridgehubContractAddressReturnType, + getBridgehubContractAddress, +} from '../actions/getBridgehubContractAddress.js' import { type GetDefaultBridgeAddressesReturnType, getDefaultBridgeAddresses, @@ -38,20 +44,32 @@ import { type GetL1BatchDetailsReturnType, getL1BatchDetails, } from '../actions/getL1BatchDetails.js' -import { getL1BatchNumber } from '../actions/getL1BatchNumber.js' -import { getL1ChainId } from '../actions/getL1ChainId.js' +import { + type GetL1BatchNumberReturnType, + getL1BatchNumber, +} from '../actions/getL1BatchNumber.js' +import { + type GetL1ChainIdReturnType, + getL1ChainId, +} from '../actions/getL1ChainId.js' import { type GetLogProofParameters, type GetLogProofReturnType, getLogProof, } from '../actions/getLogProof.js' -import { getMainContractAddress } from '../actions/getMainContractAddress.js' import { - type GetRawBlockTransactionParameters, - type GetRawBlockTransactionReturnType, + type GetMainContractAddressReturnType, + getMainContractAddress, +} from '../actions/getMainContractAddress.js' +import { + type GetRawBlockTransactionsParameters, + type GetRawBlockTransactionsReturnType, getRawBlockTransactions, -} from '../actions/getRawBlockTransaction.js' -import { getTestnetPaymasterAddress } from '../actions/getTestnetPaymasterAddress.js' +} from '../actions/getRawBlockTransactions.js' +import { + type GetTestnetPaymasterAddressReturnType, + getTestnetPaymasterAddress, +} from '../actions/getTestnetPaymasterAddress.js' import { type GetTransactionDetailsParameters, type GetTransactionDetailsReturnType, @@ -99,7 +117,7 @@ export type PublicActionsL2< * * const address = await client.getTestnetPaymasterAddress(); */ - getTestnetPaymasterAddress: () => Promise
+ getTestnetPaymasterAddress: () => Promise /** * Returns the Chain Id of underlying L1 network. @@ -119,7 +137,7 @@ export type PublicActionsL2< * const chainId = await client.getL1ChainId(); */ - getL1ChainId: () => Promise + getL1ChainId: () => Promise /** * Returns the address of a Main zkSync Contract. @@ -138,7 +156,7 @@ export type PublicActionsL2< * * const address = await client.getMainContractAddress(); */ - getMainContractAddress: () => Promise
+ getMainContractAddress: () => Promise /** * Returns all known balances for a given account. @@ -166,7 +184,7 @@ export type PublicActionsL2< * Returns data of transactions in a block. * * @returns data of transactions {@link RawBlockTransactions} - * @param args - {@link GetRawBlockTransactionParameters} + * @param args - {@link GetRawBlockTransactionsParameters} * * @example * import { createPublicClient, http } from 'viem' @@ -181,8 +199,8 @@ export type PublicActionsL2< * const rawTx = await client.getRawBlockTransaction({number: 1}); */ getRawBlockTransaction: ( - args: GetRawBlockTransactionParameters, - ) => Promise + args: GetRawBlockTransactionsParameters, + ) => Promise /** * Returns additional zkSync-specific information about the L2 block. @@ -267,7 +285,7 @@ export type PublicActionsL2< * * const latestNumber = await client.getL1BatchNumber({number: 1}); */ - getL1BatchNumber: () => Promise + getL1BatchNumber: () => Promise /** * Given a transaction hash, and an index of the L2 to L1 log produced within the transaction, it returns the proof for the corresponding L2 to L1 log. @@ -352,7 +370,7 @@ export type PublicActionsL2< */ estimateGasL1ToL2: ( args: EstimateGasL1ToL2Parameters, - ) => Promise + ) => Promise /** * Returns the Bridgehub smart contract address. @@ -372,7 +390,7 @@ export type PublicActionsL2< * * const address = await client.getBridgehubContractAddress(); */ - getBridgehubContractAddress: () => Promise
+ getBridgehubContractAddress: () => Promise /** * Returns the address of the base L1 token. @@ -392,7 +410,7 @@ export type PublicActionsL2< * * const address = await client.getBaseTokenL1Address(); */ - getBaseTokenL1Address: () => Promise
+ getBaseTokenL1Address: () => Promise } export function publicActionsL2() { diff --git a/src/zksync/errors/token-is-eth.test.ts b/src/zksync/errors/token-is-eth.test.ts new file mode 100644 index 0000000000..9c802ccb8d --- /dev/null +++ b/src/zksync/errors/token-is-eth.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from 'vitest' +import { TokenIsEthError } from './token-is-eth.js' + +test('TokenIsEthError', () => { + expect(new TokenIsEthError()).toMatchInlineSnapshot(` + [TokenIsEthError: Token is an ETH token. + + ETH token cannot be retrived. + + Version: viem@1.0.2] + `) +}) diff --git a/src/zksync/errors/token-is-eth.ts b/src/zksync/errors/token-is-eth.ts new file mode 100644 index 0000000000..89f81d2d32 --- /dev/null +++ b/src/zksync/errors/token-is-eth.ts @@ -0,0 +1,16 @@ +import { BaseError } from '../../errors/base.js' + +export type TokenIsEthErrorType = TokenIsEthError & { + name: 'TokenIsEthError' +} +export class TokenIsEthError extends BaseError { + override name = 'TokenIsEthError' + + constructor() { + super( + ['Token is an ETH token.', '', 'ETH token cannot be retrived.'].join( + '\n', + ), + ) + } +} diff --git a/src/zksync/index.ts b/src/zksync/index.ts index 01e4f07af6..d838de38a0 100644 --- a/src/zksync/index.ts +++ b/src/zksync/index.ts @@ -24,6 +24,18 @@ export { getDefaultBridgeAddresses, } from './actions/getDefaultBridgeAddresses.js' export { getBridgehubContractAddress } from './actions/getBridgehubContractAddress.js' +export { + type GetL1AllowanceErrorType, + type GetL1AllowanceParameters, + type GetL1AllowanceReturnType, + getL1Allowance, +} from './actions/getL1Allowance.js' +export { + type GetL1BalanceErrorType, + type GetL1BalanceParameters, + type GetL1BalanceReturnType, + getL1Balance, +} from './actions/getL1Balance.js' export { type GetL1BatchBlockRangeParameters, type GetL1BatchBlockRangeReturnParameters, @@ -36,6 +48,12 @@ export { } from './actions/getL1BatchDetails.js' export { getL1BatchNumber } from './actions/getL1BatchNumber.js' export { getL1ChainId } from './actions/getL1ChainId.js' +export { + type GetL1TokenBalanceErrorType, + type GetL1TokenBalanceParameters, + type GetL1TokenBalanceReturnType, + getL1TokenBalance, +} from './actions/getL1TokenBalance.js' export { type GetLogProofReturnType, type GetLogProofParameters, @@ -43,10 +61,10 @@ export { } from './actions/getLogProof.js' export { getMainContractAddress } from './actions/getMainContractAddress.js' export { - type GetRawBlockTransactionParameters, - type GetRawBlockTransactionReturnType, + type GetRawBlockTransactionsParameters, + type GetRawBlockTransactionsReturnType, getRawBlockTransactions, -} from './actions/getRawBlockTransaction.js' +} from './actions/getRawBlockTransactions.js' export { getTestnetPaymasterAddress } from './actions/getTestnetPaymasterAddress.js' export { type GetTransactionDetailsParameters, @@ -91,13 +109,18 @@ export { type Eip712WalletActions, } from './decorators/eip712.js' -export { serializeTransaction } from './serializers.js' +export { + publicActionsL1, + type PublicActionsL1, +} from './decorators/publicL1.js' export { publicActionsL2, type PublicActionsL2, } from './decorators/publicL2.js' +export { serializeTransaction } from './serializers.js' + export type { ZkSyncBlock, ZkSyncBlockOverrides, diff --git a/src/zksync/utils/isEth.test.ts b/src/zksync/utils/isEth.test.ts new file mode 100644 index 0000000000..3e9e5496fa --- /dev/null +++ b/src/zksync/utils/isEth.test.ts @@ -0,0 +1,13 @@ +import { expect, test } from 'vitest' +import { isEth } from './isEth.js' + +test('true', () => { + expect(isEth('0x0000000000000000000000000000000000000000')).toBeTruthy() + expect(isEth('0x000000000000000000000000000000000000800a')).toBeTruthy() + expect(isEth('0x0000000000000000000000000000000000000001')).toBeTruthy() +}) + +test('false', () => { + expect(isEth('0x0000000000000000000000000000000000000002')).toBeFalsy() + expect(isEth('0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f')).toBeFalsy() +}) diff --git a/src/zksync/utils/isEth.ts b/src/zksync/utils/isEth.ts new file mode 100644 index 0000000000..144232bd0a --- /dev/null +++ b/src/zksync/utils/isEth.ts @@ -0,0 +1,20 @@ +import type { Address } from '../../accounts/index.js' +import { + ethAddressInContracts, + l2BaseTokenAddress, + legacyEthAddress, +} from '../constants/address.js' + +export function isEth(token: Address) { + return ( + token.localeCompare(legacyEthAddress, undefined, { + sensitivity: 'accent', + }) === 0 || + token.localeCompare(l2BaseTokenAddress, undefined, { + sensitivity: 'accent', + }) === 0 || + token.localeCompare(ethAddressInContracts, undefined, { + sensitivity: 'accent', + }) === 0 + ) +}