Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add getUpgradeExecutor function #140

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
28 changes: 28 additions & 0 deletions src/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,31 @@ export {
nitroTestnodeL2,
nitroTestnodeL3,
};

export const xai = defineChain({
chrstph-dvx marked this conversation as resolved.
Show resolved Hide resolved
id: 660279,
network: 'Xai Mainnet',
name: 'Xai Mainnet',
nativeCurrency: { name: 'Xai', symbol: 'XAI', decimals: 18 },
rpcUrls: {
default: {
http: ['https://xai-chain.net/rpc'],
},
public: {
http: ['https://xai-chain.net/rpc'],
},
},
blockExplorers: {
default: {
name: 'Blockscout',
url: 'https://explorer.xai-chain.net',
},
},
contracts: {
multicall3: {
address: '0xca11bde05977b3631167028862be2a173976ca11',
blockCreated: 222549,
},
},
fionnachan marked this conversation as resolved.
Show resolved Hide resolved
testnet: false,
});
32 changes: 32 additions & 0 deletions src/getUpgradeExecutor.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, it, expect } from 'vitest';
import { createPublicClient, http } from 'viem';

import { nitroTestnodeL2, nitroTestnodeL3 } from './chains';
import { getInformationFromTestnode } from './testHelpers';
import { getUpgradeExecutor } from './getUpgradeExecutor';

const { l3UpgradeExecutor, l3Rollup } = getInformationFromTestnode();

// Tests can be enabled once we run one node per integration test
describe('successfully get upgrade executor', () => {
it('from parent chain', async () => {
const parentChainClient = createPublicClient({
chain: nitroTestnodeL2,
transport: http(),
});

const upgradeExecutor = await getUpgradeExecutor(parentChainClient, {
rollup: l3Rollup,
});
expect(upgradeExecutor?.toLowerCase()).toEqual(l3UpgradeExecutor);
});

it('from child chain', async () => {
const childChainClient = createPublicClient({
chain: nitroTestnodeL3,
transport: http(),
});
const upgradeExecutor = await getUpgradeExecutor(childChainClient);
expect(upgradeExecutor).toEqual(upgradeExecutor);
fionnachan marked this conversation as resolved.
Show resolved Hide resolved
});
});
91 changes: 91 additions & 0 deletions src/getUpgradeExecutor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Address, Chain, PublicClient, Transport, getAbiItem } from 'viem';
import { rollupAdminLogicABI } from './abi';
import { createRollupFetchTransactionHash } from './createRollupFetchTransactionHash';
import { isValidParentChainId } from './types/ParentChain';
import { arbOwnerPublic, upgradeExecutor } from './contracts';
import { UPGRADE_EXECUTOR_ROLE_ADMIN } from './upgradeExecutorEncodeFunctionData';

const AdminChangedAbi = getAbiItem({ abi: rollupAdminLogicABI, name: 'AdminChanged' });

export type GetUpgradeExecutorParams = {
/** Address of the rollup we're getting logs from */
rollup: Address;
} | void;
chrstph-dvx marked this conversation as resolved.
Show resolved Hide resolved
/**
* Address of the current upgrade executor
*/
export type GetUpgradeExecutorReturnType = Address | undefined;

/**
*
* @param {PublicClient} publicClient - The chain Viem Public Client
* @param {GetUpgradeExecutorParams} GetUpgradeExecutorParams {@link GetUpgradeExecutorParams}
*
* @returns Promise<{@link GetUpgradeExecutorReturnType}>
*
* @example
* const upgradeExecutor = getUpgradeExecutor(client, {
* rollup: '0xc47dacfbaa80bd9d8112f4e8069482c2a3221336'
* });
*
*/
export async function getUpgradeExecutor<TChain extends Chain | undefined>(
fionnachan marked this conversation as resolved.
Show resolved Hide resolved
publicClient: PublicClient<Transport, TChain>,
params: GetUpgradeExecutorParams,
): Promise<GetUpgradeExecutorReturnType> {
const isParentChain = isValidParentChainId(publicClient.chain?.id);
if (isParentChain && !params) {
throw new Error('[getUpgradeExecutor] requires a rollup address');
}

// Parent chain, get the newOwner args from the last event
if (isParentChain && params) {
let blockNumber: bigint | 'earliest';
let createRollupTransactionHash: Address | null = null;
fionnachan marked this conversation as resolved.
Show resolved Hide resolved

try {
createRollupTransactionHash = await createRollupFetchTransactionHash({
rollup: params.rollup,
publicClient,
});
const receipt = await publicClient.waitForTransactionReceipt({
hash: createRollupTransactionHash,
});
blockNumber = receipt.blockNumber;
} catch (e) {
console.warn(`[getUpgradeExecutor] ${(e as any).message}`);
blockNumber = 'earliest';
}

const events = await publicClient.getLogs({
address: params.rollup,
events: [AdminChangedAbi],
fromBlock: blockNumber,
toBlock: 'latest',
});

return events[events.length - 1].args.newAdmin;
}

// Child chain, check for all chainOwners
const chainOwners = await publicClient.readContract({
abi: arbOwnerPublic.abi,
functionName: 'getAllChainOwners',
address: arbOwnerPublic.address,
});

const results = await Promise.allSettled(
chainOwners.map((chainOwner) =>
publicClient.readContract({
address: chainOwner,
abi: upgradeExecutor.abi,
functionName: 'hasRole',
args: [UPGRADE_EXECUTOR_ROLE_ADMIN, chainOwner],
}),
),
);
const upgradeExecutorIndex = results.findIndex(
(p) => p.status === 'fulfilled' && p.value === true,
);
return chainOwners[upgradeExecutorIndex];
}
105 changes: 105 additions & 0 deletions src/getUpgradeExecutor.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Address, EIP1193RequestFn, createPublicClient, createTransport, http, padHex } from 'viem';
import { arbitrum, arbitrumSepolia } from 'viem/chains';
import { it, vi, describe } from 'vitest';
import { getUpgradeExecutor } from './getUpgradeExecutor';
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
import { xai } from './chains';

const rollupAddress = '0xe0875cbd144fe66c015a95e5b2d2c15c3b612179';

function mockAdminChangedEvent(previousAdmin: Address, newAdmin: Address) {
return {
address: '0xa58f38102579dae7c584850780dda55744f67df1',
blockNumber: 183097536n,
transactionHash: '0x13baa9be2bf267fde01e730855d34526f339a21f1877af175f0958e5dc546e6d',
transactionIndex: 1,
blockHash: '0x31d403a11112e6a8be0e24423df83341790a8c1cc1728a2c2deff1b683961635',
logIndex: 0,
data: '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000013300000000000000010000000000000001012160184f37a4eaea75e8252d38d5b3f0298703794d58f38b3551104ce0c2472aea78f53ccd07fdb7b1c5b08444d2be9025d519ccc21d12fd9f8b67c50615694b626aaec898b9c1e613b0c17aac28539ee667a98e08d734193de9b2e612b4b082439506aa6ff965bfff2e8d3e6ade9e038412d767778850c717b388fb17e40c359c8ef3b99b4e7aee94b88f7d96c09e8d522a0f24d90efa7db34f42cefa18ae1ab1e08f780e613e0baf8e28c322a0d52b915fcff3e143a9daa7c2ba525029066f8230120e9803fd21d332015b3ec22ae180cbd1f3cf89561a0c5bd914dc5f746d692cefcb4762a012af0fe55c1148f138221a196fbec9942400b7772ce371c8ccc8ed0cd3926398cd62f1b900758b82591174295eb7ac00555d40051ad280ceb2cfa700000000000000000000000000',
args: {
previousAdmin,
newAdmin,
},
eventName: 'AdminChanged',
fionnachan marked this conversation as resolved.
Show resolved Hide resolved
};
}

describe.concurrent('getUpgradeExecutor', () => {
it('should return upgrade executor on arbitrum one for xai', async ({ expect }) => {
const arbitrumOneClient = createPublicClient({
chain: arbitrum,
transport: http(),
});
const upgradeExecutor = await getUpgradeExecutor(arbitrumOneClient, {
rollup: '0xc47dacfbaa80bd9d8112f4e8069482c2a3221336',
});
expect(upgradeExecutor).toEqual('0x0EE7AD3Cc291343C9952fFd8844e86d294fa513F');
});

it('should return upgrade executor on xai for xai', async ({ expect }) => {
const xaiClient = createPublicClient({
chain: xai,
transport: http(),
});
const upgradeExecutor = await getUpgradeExecutor(xaiClient);
expect(upgradeExecutor).toEqual('0xB30f0939c072255C9a8019B5a52Df9a364861f84');
});

it('should return upgrade executor on parent chain with mocked data', async ({ expect }) => {
const randomAddress = privateKeyToAccount(generatePrivateKey()).address;
const randomAddress2 = privateKeyToAccount(generatePrivateKey()).address;
const randomAddress3 = privateKeyToAccount(generatePrivateKey()).address;

const mockTransport = () =>
createTransport({
key: 'mock',
name: 'Mock Transport',
request: vi.fn(({ method, params }) => {
return [
mockAdminChangedEvent(randomAddress3, randomAddress),
mockAdminChangedEvent(randomAddress, randomAddress3),
mockAdminChangedEvent(randomAddress3, randomAddress),
mockAdminChangedEvent(randomAddress, randomAddress2),
];
}) as unknown as EIP1193RequestFn,
type: 'mock',
});

const mockClient = createPublicClient({
transport: mockTransport,
chain: arbitrumSepolia,
});

const upgradeExecutor = await getUpgradeExecutor(mockClient, {
rollup: rollupAddress,
});

expect(upgradeExecutor).toEqual(randomAddress2);
});

it('should return upgrade executor on child chain with mocked data', async ({ expect }) => {
const randomAddress = privateKeyToAccount(generatePrivateKey()).address;
const randomAddress2 = privateKeyToAccount(generatePrivateKey()).address;

const mockClient = createPublicClient({
transport: http(),
chain: xai,
});

// Mock initial getChainOwners
const readContractSpy = vi.spyOn(mockClient, 'readContract');
readContractSpy
.mockImplementationOnce(async () => [randomAddress]) // getChainOwners
.mockImplementationOnce(async () => true); // hasRole

const upgradeExecutor = await getUpgradeExecutor(mockClient);
expect(upgradeExecutor).toEqual(randomAddress);

readContractSpy
.mockImplementationOnce(async () => [randomAddress, randomAddress2]) // second getChainOwners
.mockImplementationOnce(async () => false)
.mockImplementationOnce(async () => true);
const upgradeExecutor2 = await getUpgradeExecutor(mockClient);
expect(upgradeExecutor2).toEqual(randomAddress2);
});
});
2 changes: 2 additions & 0 deletions src/testHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ type TestnodeInformation = {
rollup: Address;
sequencerInbox: Address;
l3SequencerInbox: Address;
upgradeExecutor: Address;
l3Bridge: Address;
batchPoster: Address;
l3BatchPoster: Address;
Expand Down Expand Up @@ -120,6 +121,7 @@ export function getInformationFromTestnode(): TestnodeInformation {
rollup: deploymentJson['rollup'],
sequencerInbox: deploymentJson['sequencer-inbox'],
batchPoster: sequencerConfig.node['batch-poster']['parent-chain-wallet'].account,
upgradeExecutor: deploymentJson['upgrade-executor'],
l3Bridge: l3DeploymentJson['bridge'],
l3Rollup: l3DeploymentJson['rollup'],
l3SequencerInbox: l3DeploymentJson['sequencer-inbox'],
Expand Down
4 changes: 3 additions & 1 deletion src/types/ParentChain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ export type ParentChainPublicClient<TChain extends Chain | undefined> = Prettify
PublicClient<Transport, TChain> & { chain: { id: ParentChainId } }
>;

function isValidParentChainId(parentChainId: number | undefined): parentChainId is ParentChainId {
export function isValidParentChainId(
parentChainId: number | undefined,
): parentChainId is ParentChainId {
const ids = chains
// exclude nitro-testnode L3 from the list of parent chains
.filter((chain) => chain.id !== nitroTestnodeL3.id)
Expand Down
Loading