Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/get-chain-contract-versions/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
INBOX_ADDRESS=0xaE21fDA3de92dE2FDAF606233b2863782Ba046F9
NETWORK=arb1
ORBIT_ACTIONS_IMAGE=offchainlabs/chain-actions
PARENT_CHAIN_RPC=
33 changes: 33 additions & 0 deletions examples/get-chain-contract-versions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Get Orbit chain contract versions

This example uses the SDK's `getOrbitChainContractVersions` helper to inspect the deployed Nitro contract versions for an Orbit chain on Arbitrum.

It runs against the current repository checkout, so `pnpm dev` builds the local SDK before executing the example.

It requires:

- Docker installed and available on `PATH`
- an inbox address for the chain you want to inspect
- a parent chain RPC URL

## Setup

1. Install dependencies

```bash
pnpm install
```

2. Create `.env`

```bash
cp .env.example .env
```

3. Run the example

```bash
pnpm dev
```

The script prints the discovered contract versions and any upgrade recommendation as JSON.
37 changes: 37 additions & 0 deletions examples/get-chain-contract-versions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { config } from 'dotenv';
import { getOrbitChainContractVersions } from '@arbitrum/chain-sdk';
import { isAddress, Address } from 'viem';

config();

const orbitActionsImage = process.env.ORBIT_ACTIONS_IMAGE ?? 'offchainlabs/chain-actions';
const network = process.env.NETWORK ?? 'arb1';
const inboxAddress = process.env.INBOX_ADDRESS;
const parentChainRpc = process.env.PARENT_CHAIN_RPC;

if (!inboxAddress || !isAddress(inboxAddress)) {
throw new Error('Please provide the "INBOX_ADDRESS" environment variable');
}

if (!parentChainRpc || parentChainRpc === '') {
throw new Error('Please provide the "PARENT_CHAIN_RPC" environment variable');
}

async function main() {
console.log('Getting Orbit chain contract versions...');
const result = await getOrbitChainContractVersions({
image: orbitActionsImage,
inboxAddress: inboxAddress as Address,
network,
env: {
PARENT_CHAIN_RPC: parentChainRpc,
},
});

console.log(result);
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
17 changes: 17 additions & 0 deletions examples/get-chain-contract-versions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "get-chain-contract-versions",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "tsc --outDir dist && node ./dist/index.js",
"predev": "pnpm --dir ../.. build"
},
"devDependencies": {
"@types/node": "^20.9.0",
"typescript": "^5.2.2"
},
"dependencies": {
"@arbitrum/chain-sdk": "workspace:*"
}
}
4 changes: 4 additions & 0 deletions examples/get-chain-contract-versions/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../tsconfig.base.json",
"include": ["./**/*"]
}
13 changes: 13 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export const createRollupDefaultRetryablesFees = parseEther('0.125');
* Approximate value necessary to pay for retryables fees for `createTokenBridge`.
*/
export const createTokenBridgeDefaultRetryablesFees = parseEther('0.02');

export const DEFAULT_ORBIT_ACTIONS_IMAGE = 'offchainlabs/chain-actions:150d84f832ea';
53 changes: 53 additions & 0 deletions src/getOrbitChainContractVersions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { Address } from 'viem';

import { runDockerCommand } from './runDockerCommand';
import { DEFAULT_ORBIT_ACTIONS_IMAGE } from './constants';

export type GetOrbitChainContractVersionsParameters = {
image?: string;
inboxAddress: Address;
network: string;
env?: Record<string, string | undefined>;
};

export type GetOrbitChainContractVersionsResult = {
versions: Record<string, string | null>;
upgradeRecommendation: unknown;
};

export async function getOrbitChainContractVersions({
image = DEFAULT_ORBIT_ACTIONS_IMAGE,
inboxAddress,
network,
env,
}: GetOrbitChainContractVersionsParameters): Promise<GetOrbitChainContractVersionsResult> {
const { stdout } = await runDockerCommand({
image,
entrypoint: 'yarn',
command: ['--silent', 'orbit:contracts:version', '--network', network, '--no-compile'],
env: {
...env,
INBOX_ADDRESS: inboxAddress,
JSON_OUTPUT: 'true',
},
});

let parsed: unknown;

try {
parsed = JSON.parse(stdout) as unknown;
} catch {
throw new Error('Failed to parse Orbit chain contract versions');
}

if (
typeof parsed !== 'object' ||
parsed === null ||
!('versions' in parsed) ||
!('upgradeRecommendation' in parsed)
) {
throw new Error('Failed to parse Orbit chain contract versions');
}

return parsed as GetOrbitChainContractVersionsResult;
}
158 changes: 158 additions & 0 deletions src/getOrbitChainContractVersions.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { getOrbitChainContractVersions } from './getOrbitChainContractVersions';
import { runDockerCommand } from './runDockerCommand';

vi.mock('./runDockerCommand', () => ({
runDockerCommand: vi.fn(),
}));

describe('getOrbitChainContractVersions', () => {
beforeEach(() => {
vi.mocked(runDockerCommand).mockReset();
});

it('uses the default Orbit Actions image through the internal docker runner', async () => {
vi.mocked(runDockerCommand).mockResolvedValueOnce({
argv: ['docker', 'run'],
stdout:
'{"versions":{"Inbox":"v1.1.1","RollupProxy":"v1.1.1"},"upgradeRecommendation":{"message":"No upgrade path found"}}',
stderr: '',
exitCode: 0,
});

const result = await getOrbitChainContractVersions({
inboxAddress: '0xaE21fDA3de92dE2FDAF606233b2863782Ba046F9',
network: 'arb1',
env: {
PARENT_CHAIN_RPC: 'https://rpc.example',
},
});

expect(runDockerCommand).toHaveBeenCalledWith({
image: 'offchainlabs/chain-actions:150d84f832ea',
entrypoint: 'yarn',
command: ['--silent', 'orbit:contracts:version', '--network', 'arb1', '--no-compile'],
env: {
PARENT_CHAIN_RPC: 'https://rpc.example',
INBOX_ADDRESS: '0xaE21fDA3de92dE2FDAF606233b2863782Ba046F9',
JSON_OUTPUT: 'true',
},
});

expect(result).toEqual({
versions: {
Inbox: 'v1.1.1',
RollupProxy: 'v1.1.1',
},
upgradeRecommendation: {
message: 'No upgrade path found',
},
});
});

it('uses a custom Orbit Actions image when one is provided', async () => {
vi.mocked(runDockerCommand).mockResolvedValueOnce({
argv: ['docker', 'run'],
stdout:
'{"versions":{"Inbox":"v1.1.1","Bridge":"v1.1.2","RollupProxy":null},"upgradeRecommendation":null}',
stderr: '',
exitCode: 0,
});

const result = await getOrbitChainContractVersions({
image: 'offchainlabs/chain-actions:custom',
inboxAddress: '0xaE21fDA3de92dE2FDAF606233b2863782Ba046F9',
network: 'arb-sepolia',
});

expect(runDockerCommand).toHaveBeenCalledWith({
image: 'offchainlabs/chain-actions:custom',
entrypoint: 'yarn',
command: ['--silent', 'orbit:contracts:version', '--network', 'arb-sepolia', '--no-compile'],
env: {
INBOX_ADDRESS: '0xaE21fDA3de92dE2FDAF606233b2863782Ba046F9',
JSON_OUTPUT: 'true',
},
});

expect(result).toEqual({
versions: {
Inbox: 'v1.1.1',
Bridge: 'v1.1.2',
RollupProxy: null,
},
upgradeRecommendation: null,
});
});

it('throws when the docker output does not contain a valid JSON payload', async () => {
vi.mocked(runDockerCommand).mockResolvedValueOnce({
argv: ['docker', 'run'],
stdout: 'not json',
stderr: '',
exitCode: 0,
});

await expect(
getOrbitChainContractVersions({
inboxAddress: '0xaE21fDA3de92dE2FDAF606233b2863782Ba046F9',
network: 'arb1',
}),
).rejects.toThrow('Failed to parse Orbit chain contract versions');
});

it.each(['null', '"unexpected"', '42', 'true', '[]'])(
'throws when the parsed JSON payload is not an object: %s',
async (stdout) => {
vi.mocked(runDockerCommand).mockResolvedValueOnce({
argv: ['docker', 'run'],
stdout,
stderr: '',
exitCode: 0,
});

await expect(
getOrbitChainContractVersions({
inboxAddress: '0xaE21fDA3de92dE2FDAF606233b2863782Ba046F9',
network: 'arb1',
}),
).rejects.toThrow('Failed to parse Orbit chain contract versions');
},
);

it('throws when the JSON payload is missing top level fields', async () => {
vi.mocked(runDockerCommand).mockResolvedValueOnce({
argv: ['docker', 'run'],
stdout: '{"versions":{"Inbox":"v1.1.1"}}',
stderr: '',
exitCode: 0,
});

await expect(
getOrbitChainContractVersions({
inboxAddress: '0xaE21fDA3de92dE2FDAF606233b2863782Ba046F9',
network: 'arb1',
}),
).rejects.toThrow('Failed to parse Orbit chain contract versions');
});

it('accepts any JSON payload that includes versions and upgradeRecommendation', async () => {
vi.mocked(runDockerCommand).mockResolvedValueOnce({
argv: ['docker', 'run'],
stdout: '{"versions":"not-validated","upgradeRecommendation":null}',
stderr: '',
exitCode: 0,
});

await expect(
getOrbitChainContractVersions({
inboxAddress: '0xaE21fDA3de92dE2FDAF606233b2863782Ba046F9',
network: 'arb1',
}),
).resolves.toEqual({
versions: 'not-validated',
upgradeRecommendation: null,
});
});
});
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ import {
FetchDecimalsProps,
} from './utils/erc20';
import { prepareArbitrumNetwork } from './utils/registerNewNetwork';
import { getOrbitChainContractVersions } from './getOrbitChainContractVersions';

export {
arbOwnerPublicActions,
Expand Down Expand Up @@ -354,4 +355,6 @@ export {
FetchDecimalsProps,
//
prepareArbitrumNetwork,
//
getOrbitChainContractVersions,
};
Loading
Loading