diff --git a/src/arbOwnerPrepareTransactionRequest.ts b/src/arbOwnerPrepareTransactionRequest.ts index b48005c8..9787780e 100644 --- a/src/arbOwnerPrepareTransactionRequest.ts +++ b/src/arbOwnerPrepareTransactionRequest.ts @@ -1,48 +1,35 @@ import { - PublicClient, + Client, encodeFunctionData, EncodeFunctionDataParameters, Address, Chain, Transport, + GetFunctionArgs, } from 'viem'; -import { arbOwner } from './contracts'; +import { arbOwner, ArbOSVersions, ArbOwnerABIs } from './contracts'; import { upgradeExecutorEncodeFunctionData } from './upgradeExecutor'; -import { GetFunctionName } from './types/utils'; +import { ArbOwnerPublicAbi, ArbOwnerPublicFunctionName } from './arbOwnerReadContract'; -type ArbOwnerAbi = typeof arbOwner.abi; -export type ArbOwnerPrepareTransactionRequestFunctionName = GetFunctionName; export type ArbOwnerEncodeFunctionDataParameters< - TFunctionName extends ArbOwnerPrepareTransactionRequestFunctionName, -> = EncodeFunctionDataParameters; - -function arbOwnerEncodeFunctionData< - TFunctionName extends ArbOwnerPrepareTransactionRequestFunctionName, ->({ functionName, abi, args }: ArbOwnerEncodeFunctionDataParameters) { - return encodeFunctionData({ - abi, - functionName, - args, - }); -} - -type ArbOwnerPrepareFunctionDataParameters< - TFunctionName extends ArbOwnerPrepareTransactionRequestFunctionName, -> = ArbOwnerEncodeFunctionDataParameters & { + TArbOsVersion extends ArbOSVersions, + TFunctionName extends ArbOwnerPublicFunctionName, +> = EncodeFunctionDataParameters, TFunctionName> & { upgradeExecutor: Address | false; - abi: ArbOwnerAbi; }; + function arbOwnerPrepareFunctionData< - TFunctionName extends ArbOwnerPrepareTransactionRequestFunctionName, ->(params: ArbOwnerPrepareFunctionDataParameters) { + TArbOsVersion extends ArbOSVersions, + TFunctionName extends ArbOwnerPublicFunctionName, +>(params: ArbOwnerEncodeFunctionDataParameters) { const { upgradeExecutor } = params; if (!upgradeExecutor) { return { to: arbOwner.address, - data: arbOwnerEncodeFunctionData( - params as ArbOwnerEncodeFunctionDataParameters, + data: encodeFunctionData( + params as EncodeFunctionDataParameters, TFunctionName>, ), value: BigInt(0), }; @@ -54,7 +41,9 @@ function arbOwnerPrepareFunctionData< functionName: 'executeCall', args: [ arbOwner.address, // target - arbOwnerEncodeFunctionData(params as ArbOwnerEncodeFunctionDataParameters), // targetCallData + encodeFunctionData( + params as EncodeFunctionDataParameters, TFunctionName>, + ), // targetCallData ], }), value: BigInt(0), @@ -62,27 +51,32 @@ function arbOwnerPrepareFunctionData< } export type ArbOwnerPrepareTransactionRequestParameters< - TFunctionName extends ArbOwnerPrepareTransactionRequestFunctionName, -> = Omit, 'abi'> & { + TArbOsVersion extends ArbOSVersions, + TFunctionName extends ArbOwnerPublicFunctionName, +> = { + functionName: TFunctionName; account: Address; -}; + upgradeExecutor: Address | false; +} & GetFunctionArgs, TFunctionName>; export async function arbOwnerPrepareTransactionRequest< - TFunctionName extends ArbOwnerPrepareTransactionRequestFunctionName, + TArbOsVersion extends ArbOSVersions, TChain extends Chain | undefined, + TFunctionName extends ArbOwnerPublicFunctionName, >( - client: PublicClient, - params: ArbOwnerPrepareTransactionRequestParameters, + client: Client, + params: ArbOwnerPrepareTransactionRequestParameters & { + arbOsVersion: TArbOsVersion; + }, ) { if (typeof client.chain === 'undefined') { throw new Error('[arbOwnerPrepareTransactionRequest] client.chain is undefined'); } - // params is extending ArbOwnerPrepareFunctionDataParameters, it's safe to cast const { to, data, value } = arbOwnerPrepareFunctionData({ ...params, - abi: arbOwner.abi, - } as unknown as ArbOwnerPrepareFunctionDataParameters); + abi: ArbOwnerABIs[params.arbOsVersion], + } as unknown as ArbOwnerEncodeFunctionDataParameters); // @ts-ignore (todo: fix viem type issue) const request = await client.prepareTransactionRequest({ diff --git a/src/arbOwnerReadContract.ts b/src/arbOwnerReadContract.ts index 62a5ff15..8eb4892f 100644 --- a/src/arbOwnerReadContract.ts +++ b/src/arbOwnerReadContract.ts @@ -1,29 +1,41 @@ -import { Chain, GetFunctionArgs, PublicClient, ReadContractReturnType, Transport } from 'viem'; +import { Chain, GetFunctionArgs, Client, ReadContractReturnType, Transport } from 'viem'; -import { arbOwnerPublic } from './contracts'; +import { ArbOSVersions, ArbOwnerABIs, arbOwnerPublic } from './contracts'; import { GetFunctionName } from './types/utils'; -export type ArbOwnerPublicAbi = typeof arbOwnerPublic.abi; -export type ArbOwnerPublicFunctionName = GetFunctionName; +export type ArbOwnerPublicAbi = + (typeof ArbOwnerABIs)[TArbOsVersion]; -export type ArbOwnerReadContractParameters = { +export type ArbOwnerPublicFunctionName = GetFunctionName< + ArbOwnerPublicAbi +>; + +export type ArbOwnerReadContractParameters< + TArbOsVersion extends ArbOSVersions, + TFunctionName extends ArbOwnerPublicFunctionName, +> = { functionName: TFunctionName; -} & GetFunctionArgs; +} & GetFunctionArgs, TFunctionName>; -export type ArbOwnerReadContractReturnType = - ReadContractReturnType; +export type ArbOwnerReadContractReturnType< + TArbOsVersion extends ArbOSVersions, + TFunctionName extends ArbOwnerPublicFunctionName, +> = ReadContractReturnType, TFunctionName>; export function arbOwnerReadContract< + TArbOsVersion extends ArbOSVersions, TChain extends Chain | undefined, - TFunctionName extends ArbOwnerPublicFunctionName, + TFunctionName extends ArbOwnerPublicFunctionName, >( - client: PublicClient, - params: ArbOwnerReadContractParameters, -): Promise> { + client: Client, + params: ArbOwnerReadContractParameters & { + arbOsVersion: TArbOsVersion; + }, +): Promise> { // @ts-ignore (todo: fix viem type issue) return client.readContract({ address: arbOwnerPublic.address, - abi: arbOwnerPublic.abi, + abi: ArbOwnerABIs[params.arbOsVersion], functionName: params.functionName, args: params.args, }); diff --git a/src/contracts.ts b/src/contracts.ts index 53997932..1637ef5f 100644 --- a/src/contracts.ts +++ b/src/contracts.ts @@ -198,3 +198,54 @@ export const tokenBridgeCreator = { export const sequencerInbox = { abi: sequencerInboxABI, }; +export type ArbOSVersions = 10 | 11 | 20; + +export const ArbOwnerABIs = { + 10: [ + { + stateMutability: 'view', + type: 'function', + inputs: [], + name: 'getAllChainOwners', + outputs: [{ name: '', internalType: 'address', type: 'address' }], + }, + { + stateMutability: 'view', + type: 'function', + inputs: [], + name: 'onlyOnArbOS10', + outputs: [{ name: '', internalType: 'address[]', type: 'address[]' }], + }, + { + stateMutability: 'nonpayable', + type: 'function', + inputs: [{ name: 'recipient', internalType: 'address[]', type: 'address[]' }], + name: 'setL1PricingRewardRecipient', + outputs: [], + }, + ], + 11: [ + { + stateMutability: 'view', + type: 'function', + inputs: [], + name: 'getAllChainOwners', + outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], + }, + { + stateMutability: 'view', + type: 'function', + inputs: [], + name: 'onlyOnArbOS11', + outputs: [{ name: '', internalType: 'address[]', type: 'address[]' }], + }, + { + stateMutability: 'nonpayable', + type: 'function', + inputs: [{ name: 'recipient', internalType: 'uint64', type: 'uint64' }], + name: 'setL1PricingRewardRecipient', + outputs: [], + }, + ], + 20: arbOwnerConfig.abi, +} as const; diff --git a/src/decorators/arbOwnerPrepareTransactionRequest.unit.test.ts b/src/decorators/arbOwnerPrepareTransactionRequest.unit.test.ts index 5c3639f7..6535d013 100644 --- a/src/decorators/arbOwnerPrepareTransactionRequest.unit.test.ts +++ b/src/decorators/arbOwnerPrepareTransactionRequest.unit.test.ts @@ -14,7 +14,7 @@ import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; const client = createPublicClient({ chain: nitroTestnodeL2, transport: http(), -}).extend(arbOwnerPublicActions); +}).extend(arbOwnerPublicActions({ arbOsVersion: 20 })); const randomAccount = privateKeyToAccount(generatePrivateKey()); it('Infer parameters based on function name', async () => { diff --git a/src/decorators/arbOwnerPublicActions.integration.test.ts b/src/decorators/arbOwnerPublicActions.integration.test.ts index 890094c3..c354aa5f 100644 --- a/src/decorators/arbOwnerPublicActions.integration.test.ts +++ b/src/decorators/arbOwnerPublicActions.integration.test.ts @@ -15,7 +15,7 @@ const randomAccount = privateKeyToAccount(generatePrivateKey()); const client = createPublicClient({ chain: nitroTestnodeL2, transport: http(), -}).extend(arbOwnerPublicActions); +}).extend(arbOwnerPublicActions({ arbOsVersion: 20 })); it('successfully fetches network fee receiver', async () => { const result = await client.arbOwnerReadContract({ diff --git a/src/decorators/arbOwnerPublicActions.ts b/src/decorators/arbOwnerPublicActions.ts index 0776fb17..614bbdac 100644 --- a/src/decorators/arbOwnerPublicActions.ts +++ b/src/decorators/arbOwnerPublicActions.ts @@ -1,4 +1,4 @@ -import { Transport, Chain, PrepareTransactionRequestReturnType, PublicClient } from 'viem'; +import { Transport, Chain, PrepareTransactionRequestReturnType, Client, Account } from 'viem'; import { arbOwnerReadContract, @@ -8,29 +8,104 @@ import { } from '../arbOwnerReadContract'; import { arbOwnerPrepareTransactionRequest, - ArbOwnerPrepareTransactionRequestFunctionName, ArbOwnerPrepareTransactionRequestParameters, } from '../arbOwnerPrepareTransactionRequest'; +import { ArbOSVersions } from '../contracts'; -export type ArbOwnerPublicActions = { - arbOwnerReadContract: ( - args: ArbOwnerReadContractParameters, - ) => Promise>; +export type ArbOwnerPublicActions< + TArbOsVersion extends ArbOSVersions = 20, + TChain extends Chain | undefined = Chain | undefined, +> = { + arbOwnerReadContract: >( + args: ArbOwnerReadContractParameters, + ) => Promise>; arbOwnerPrepareTransactionRequest: < - TFunctionName extends ArbOwnerPrepareTransactionRequestFunctionName, + TFunctionName extends ArbOwnerPublicFunctionName, >( - args: ArbOwnerPrepareTransactionRequestParameters, + args: ArbOwnerPrepareTransactionRequestParameters, ) => Promise & { chainId: number }>; }; +const defaultArbOsVersion = 20; + +// Client is passed explicitly +// `arbOwnerPublicActions(client)`, `arbOwnerPublicActions(client, { arbOsVersion: 20 })` +export function arbOwnerPublicActions< + TArbOsVersion extends ArbOSVersions, + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TAccount extends Account | undefined = Account | undefined, +>( + client: Client, + { arbOsVersion }: { arbOsVersion: TArbOsVersion }, +): ArbOwnerPublicActions; +// arbOsVersion is passed as a parameter +// `client.extend(arbOwnerPublicActions({ arbOsVersion: 10 }))` +export function arbOwnerPublicActions< + TArbOsVersion extends ArbOSVersions, + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TAccount extends Account | undefined = Account | undefined, +>(param: { + arbOsVersion: TArbOsVersion; +}): (client: Client) => ArbOwnerPublicActions; +// No parameter are passed +// `client.extend(arbOwnerPublicActions)` +export function arbOwnerPublicActions< + TArbOsVersion extends ArbOSVersions, + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TAccount extends Account | undefined = Account | undefined, +>( + param: Client, +): ArbOwnerPublicActions; export function arbOwnerPublicActions< + TArbOsVersion extends ArbOSVersions, TTransport extends Transport = Transport, TChain extends Chain | undefined = Chain | undefined, ->(client: PublicClient): ArbOwnerPublicActions { - return { - arbOwnerReadContract: (args) => arbOwnerReadContract(client, args), + TAccount extends Account | undefined = Account | undefined, +>( + paramOrClient: { arbOsVersion: TArbOsVersion } | Client, + options?: { arbOsVersion: TArbOsVersion }, +) { + // arbOsVersion is passed as a parameter, return actions with curried arbOsVersion + if ('arbOsVersion' in paramOrClient) { + const result: ( + client: Client, + ) => ArbOwnerPublicActions = (client) => ({ + arbOwnerReadContract: (args) => + arbOwnerReadContract(client, { ...args, arbOsVersion: paramOrClient.arbOsVersion }), + arbOwnerPrepareTransactionRequest: (args) => + // @ts-ignore (todo: fix viem type issue) + arbOwnerPrepareTransactionRequest(client, { + ...args, + arbOsVersion: paramOrClient.arbOsVersion, + }), + }); + + return result; + } - arbOwnerPrepareTransactionRequest: (args) => arbOwnerPrepareTransactionRequest(client, args), + /** + * Parameter is a client, we either have: + * - client.extend(arbOwnerPublicActions) + * - arbOwnerPublicActions(client) + * - arbOwnerPublicActions(client, { arbOsVersion: X }) + * + * If we don't have arbOsVersion (the 2 first cases), default the version to defaultArbOsVersion + */ + const version = options?.arbOsVersion ?? defaultArbOsVersion; + const result: ArbOwnerPublicActions = { + arbOwnerReadContract: (args) => + // @ts-ignore (todo: fix viem type issue) + arbOwnerReadContract(paramOrClient, { ...args, arbOsVersion: version }), + arbOwnerPrepareTransactionRequest: (args) => + // @ts-ignore (todo: fix viem type issue) + arbOwnerPrepareTransactionRequest(paramOrClient, { + ...args, + arbOsVersion: version, + }), }; + return result; } diff --git a/src/decorators/arbOwnerPublicActions.unit.test.ts b/src/decorators/arbOwnerPublicActions.unit.test.ts new file mode 100644 index 00000000..ab335df6 --- /dev/null +++ b/src/decorators/arbOwnerPublicActions.unit.test.ts @@ -0,0 +1,215 @@ +import { describe, it, expect, expectTypeOf } from 'vitest'; + +import { AbiFunctionNotFoundError, createPublicClient, http } from 'viem'; +import { nitroTestnodeL2 } from '../chains'; +import { arbOwnerPublicActions } from './arbOwnerPublicActions'; +import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; + +const clientWithoutParam = createPublicClient({ + chain: nitroTestnodeL2, + transport: http(), +}).extend(arbOwnerPublicActions); +const client10 = createPublicClient({ + chain: nitroTestnodeL2, + transport: http(), +}).extend(arbOwnerPublicActions({ arbOsVersion: 10 })); +const client11 = createPublicClient({ + chain: nitroTestnodeL2, + transport: http(), +}).extend(arbOwnerPublicActions({ arbOsVersion: 11 })); +const client20 = createPublicClient({ + chain: nitroTestnodeL2, + transport: http(), +}).extend(arbOwnerPublicActions({ arbOsVersion: 20 })); +const randomAccount = privateKeyToAccount(generatePrivateKey()); +const upgradeExecutorAddress = '0x24198F8A339cd3C47AEa3A764A20d2dDaB4D1b5b'; + +const client = createPublicClient({ + chain: nitroTestnodeL2, + transport: http(), +}); +const actionsWithVersion = arbOwnerPublicActions(client, { arbOsVersion: 11 }); +const actionsWithDefaultVersion = arbOwnerPublicActions(client); + +describe('Accept function name based on arbOSVersion', async () => { + it('Client with actions (version 10)', () => { + expectTypeOf>().toBeCallableWith({ + functionName: 'onlyOnArbOS10', + }); + + expectTypeOf< + typeof client10.arbOwnerPrepareTransactionRequest<'setL1PricingRewardRecipient'> + >().toBeCallableWith({ + functionName: 'setL1PricingRewardRecipient', + account: randomAccount.address, + upgradeExecutor: upgradeExecutorAddress, + args: [[randomAccount.address, randomAccount.address]], + }); + + expect( + client10.arbOwnerReadContract({ + // @ts-expect-error Not available for version 10 + functionName: 'onlyOnArbOS20', + }), + ).rejects.toThrowError(AbiFunctionNotFoundError); + }); + it('Client with actions (version 11)', () => { + expectTypeOf>().toBeCallableWith({ + functionName: 'onlyOnArbOS11', + }); + + expectTypeOf< + typeof client11.arbOwnerPrepareTransactionRequest<'setL1PricingRewardRecipient'> + >().toBeCallableWith({ + functionName: 'setL1PricingRewardRecipient', + account: randomAccount.address, + upgradeExecutor: upgradeExecutorAddress, + args: [100n], + }); + + expect( + client11.arbOwnerReadContract({ + // @ts-expect-error Not available for version 11 + functionName: 'onlyOnArbOS20', + }), + ).rejects.toThrowError(AbiFunctionNotFoundError); + }); + it('Client with actions (version 20)', () => { + expectTypeOf>().toBeCallableWith({ + functionName: 'getInfraFeeAccount', + }); + + expectTypeOf< + typeof client20.arbOwnerPrepareTransactionRequest<'setL1PricingRewardRecipient'> + >().toBeCallableWith({ + functionName: 'setL1PricingRewardRecipient', + account: randomAccount.address, + upgradeExecutor: upgradeExecutorAddress, + args: [randomAccount.address], + }); + + expect( + client20.arbOwnerReadContract({ + // @ts-expect-error Not available for version 20 + functionName: 'onlyOnArbOS10', + }), + ).rejects.toThrowError(AbiFunctionNotFoundError); + }); + it('Client with actions (default version)', () => { + // arbOwnerPublicActions without params is defaulted to arbOsVersion 20 + expectTypeOf< + typeof clientWithoutParam.arbOwnerReadContract<'getInfraFeeAccount'> + >().toBeCallableWith({ + functionName: 'getInfraFeeAccount', + }); + + expectTypeOf< + typeof clientWithoutParam.arbOwnerPrepareTransactionRequest<'setL1PricingRewardRecipient'> + >().toBeCallableWith({ + functionName: 'setL1PricingRewardRecipient', + account: randomAccount.address, + upgradeExecutor: upgradeExecutorAddress, + args: [randomAccount.address], + }); + + expect( + clientWithoutParam.arbOwnerReadContract({ + // @ts-expect-error Not available for version 20 + functionName: 'onlyOnArbOS10', + }), + ).rejects.toThrowError(AbiFunctionNotFoundError); + }); + it('Standalone actions (version 11) ', () => { + expectTypeOf< + typeof actionsWithVersion.arbOwnerReadContract<'onlyOnArbOS11'> + >().toBeCallableWith({ + functionName: 'onlyOnArbOS11', + }); + + expectTypeOf< + typeof actionsWithVersion.arbOwnerPrepareTransactionRequest<'setL1PricingRewardRecipient'> + >().toBeCallableWith({ + functionName: 'setL1PricingRewardRecipient', + account: randomAccount.address, + upgradeExecutor: upgradeExecutorAddress, + args: [100n], + }); + + expect( + actionsWithVersion.arbOwnerReadContract({ + // @ts-expect-error Not available for version 11 + functionName: 'onlyOnArbOS20', + }), + ).rejects.toThrowError(AbiFunctionNotFoundError); + }); + it('Standalone actions (default version) ', () => { + expectTypeOf< + typeof actionsWithDefaultVersion.arbOwnerReadContract<'getInfraFeeAccount'> + >().toBeCallableWith({ + functionName: 'getInfraFeeAccount', + }); + + expectTypeOf< + typeof actionsWithDefaultVersion.arbOwnerPrepareTransactionRequest<'setL1PricingRewardRecipient'> + >().toBeCallableWith({ + functionName: 'setL1PricingRewardRecipient', + account: randomAccount.address, + upgradeExecutor: upgradeExecutorAddress, + args: [randomAccount.address], + }); + + expect( + actionsWithDefaultVersion.arbOwnerReadContract({ + // @ts-expect-error Not available for version 20 + functionName: 'onlyOnArbOS10', + }), + ).rejects.toThrowError(AbiFunctionNotFoundError); + }); +}); + +// Those tests won't fail if the return type is wrong +// But they will display an error in the IDE +describe('Type return values for function in multiple versions', () => { + it('Client with actions (Version 10)', () => { + expectTypeOf( + client10.arbOwnerReadContract({ + functionName: 'getAllChainOwners', + }), + ).resolves.toEqualTypeOf<`0x${string}`>(); + }); + it('Client with actions (Version 11)', () => { + expectTypeOf( + client11.arbOwnerReadContract({ + functionName: 'getAllChainOwners', + }), + ).resolves.toEqualTypeOf(); + }); + it('Client with actions (Version 20)', () => { + expectTypeOf( + client20.arbOwnerReadContract({ + functionName: 'getAllChainOwners', + }), + ).resolves.toEqualTypeOf(); + }); + it('Client with actions (default version)', () => { + expectTypeOf( + clientWithoutParam.arbOwnerReadContract({ + functionName: 'getAllChainOwners', + }), + ).resolves.toEqualTypeOf(); + }); + it('Standalone actions (version 11)', () => { + expectTypeOf( + actionsWithVersion.arbOwnerReadContract({ + functionName: 'getAllChainOwners', + }), + ).resolves.toEqualTypeOf(); + }); + it('Standalone actions (default version)', () => { + expectTypeOf( + actionsWithDefaultVersion.arbOwnerReadContract({ + functionName: 'getAllChainOwners', + }), + ).resolves.toEqualTypeOf(); + }); +}); diff --git a/src/decorators/arbOwnerPublicActionsUpgradeExecutor.integration.test.ts b/src/decorators/arbOwnerPublicActionsUpgradeExecutor.integration.test.ts index 8c8e7904..431ccc8d 100644 --- a/src/decorators/arbOwnerPublicActionsUpgradeExecutor.integration.test.ts +++ b/src/decorators/arbOwnerPublicActionsUpgradeExecutor.integration.test.ts @@ -20,7 +20,7 @@ const client = createPublicClient({ chain: nitroTestnodeL3, transport: http(), }) - .extend(arbOwnerPublicActions) + .extend(arbOwnerPublicActions({ arbOsVersion: 20 })) .extend(arbGasInfoPublicActions); it('succesfully adds chain owner using upgrade executor', async () => {