Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "21"
node-version: "22"
registry-url: "https://registry.npmjs.org"

- name: Install Foundry
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "21"
node-version: "22"
registry-url: "https://registry.npmjs.org"

- name: Install Foundry
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-npm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "21"
node-version: "22"
registry-url: "https://registry.npmjs.org"

- name: Install Foundry
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "21"
node-version: "22"
registry-url: "https://registry.npmjs.org"

- name: Install Foundry
Expand Down
6 changes: 3 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ Configuration options including signer and optional gateway address

###### signer

`AbstractSigner`\<`null` \| `Provider`\> = `...`
`AbstractSigner`\<`Provider` \| `null`\> = `...`

###### txOptions?

Expand Down Expand Up @@ -208,7 +208,7 @@ Configuration options including signer and optional gateway address

###### signer

`AbstractSigner`\<`null` \| `Provider`\> = `...`
`AbstractSigner`\<`Provider` \| `null`\> = `...`

###### txOptions?

Expand Down Expand Up @@ -305,7 +305,7 @@ Configuration options including signer and optional gateway address

###### signer

`AbstractSigner`\<`null` \| `Provider`\> = `...`
`AbstractSigner`\<`Provider` \| `null`\> = `...`

###### txOptions?

Expand Down
10 changes: 10 additions & 0 deletions packages/commands/src/query/contracts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Command } from "commander";

import { listCommand } from "./list";
import { showCommand } from "./show";

export const contractsCommand = new Command("contracts")
.description("Contract registry commands")
.addCommand(listCommand)
.addCommand(showCommand)
.helpCommand(false);
143 changes: 143 additions & 0 deletions packages/commands/src/query/contracts/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import RegistryABI from "@zetachain/protocol-contracts/abi/Registry.sol/Registry.json";
import chalk from "chalk";
import { Command, Option } from "commander";
import { ethers } from "ethers";
import ora from "ora";
import { getBorderCharacters, table } from "table";
import { z } from "zod";

import { CONTRACT_REGISTRY_ADDRESS } from "../../../../../src/constants/addresses";
import { DEFAULT_EVM_RPC_URL } from "../../../../../src/constants/api";
import { contractsListOptionsSchema } from "../../../../../src/schemas/commands/contracts";
import { formatAddressForChain } from "../../../../../utils/addressResolver";

type ContractsListOptions = z.infer<typeof contractsListOptionsSchema>;

interface ContractData {
addressBytes: string;
chainId: ethers.BigNumberish;
contractType: string;
}

const normalizeHex = (hex: string): string =>
(hex || "").toLowerCase().startsWith("0x")
? (hex || "").toLowerCase()
: `0x${(hex || "").toLowerCase()}`;

const deduplicateContracts = (contracts: ContractData[]): ContractData[] => {
const seen = new Set<string>();
const unique: ContractData[] = [];
for (const c of contracts) {
const key = `${c.chainId.toString()}::${c.contractType}::${normalizeHex(
c.addressBytes
)}`;
if (seen.has(key)) continue;
seen.add(key);
unique.push(c);
}
return unique;
};

export const fetchContracts = async (
rpcUrl: string
): Promise<ContractData[]> => {
const provider = new ethers.JsonRpcProvider(rpcUrl);
const contractRegistry = new ethers.Contract(
CONTRACT_REGISTRY_ADDRESS,
RegistryABI.abi,
provider
);

const contracts =
(await contractRegistry.getAllContracts()) as ContractData[];
return contracts;
};

const formatContractsTable = (
contracts: ContractData[],
columns: ("type" | "address")[]
): string[][] => {
const headers = ["Chain ID"];

if (columns.includes("type")) headers.push("Type");
if (columns.includes("address")) headers.push("Address");

const rows = contracts.map((contract) => {
const baseRow = [contract.chainId.toString()];

if (columns.includes("type")) baseRow.push(contract.contractType);
if (columns.includes("address"))
baseRow.push(
formatAddressForChain(contract.addressBytes, contract.chainId)
);

return baseRow;
});

return [headers, ...rows];
};

const main = async (options: ContractsListOptions) => {
const spinner = options.json
? null
: ora("Fetching contracts from registry...").start();

try {
const contracts = await fetchContracts(options.rpc);
const uniqueContracts = deduplicateContracts(contracts);
if (!options.json) {
spinner?.succeed(
`Successfully fetched ${uniqueContracts.length} contracts`
);
}

const sortedContracts = [...uniqueContracts].sort(
(a, b) => parseInt(a.chainId.toString()) - parseInt(b.chainId.toString())
);

if (options.json) {
const jsonOutput = sortedContracts.map((c: ContractData) => ({
address: formatAddressForChain(c.addressBytes, c.chainId),
chainId: c.chainId.toString(),
type: c.contractType,
}));
console.log(JSON.stringify(jsonOutput, null, 2));
return;
}

if (uniqueContracts.length === 0) {
console.log(chalk.yellow("No contracts found in the registry"));
return;
}

const tableData = formatContractsTable(sortedContracts, options.columns);
const tableOutput = table(tableData, {
border: getBorderCharacters("norc"),
});

console.log(tableOutput);
} catch (error) {
if (!options.json) {
spinner?.fail("Failed to fetch contracts");
}
console.error(chalk.red("Error details:"), error);
process.exit(1);
}
};

export const listCommand = new Command("list")
.alias("l")
.summary("List protocol contracts on all connected chains")
.addOption(
new Option("--rpc <url>", "Custom RPC URL").default(DEFAULT_EVM_RPC_URL)
)
.option("--json", "Output contracts as JSON")
.addOption(
new Option("--columns <values...>", "Additional columns to show")
.choices(["type", "address"])
.default(["type", "address"])
)
.action(async (options: ContractsListOptions) => {
const validatedOptions = contractsListOptionsSchema.parse(options);
await main(validatedOptions);
});
85 changes: 85 additions & 0 deletions packages/commands/src/query/contracts/show.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import chalk from "chalk";
import { Command, Option } from "commander";
import { ethers } from "ethers";
import { z } from "zod";

import { DEFAULT_EVM_RPC_URL } from "../../../../../src/constants/api";
import { contractsShowOptionsSchema } from "../../../../../src/schemas/commands/contracts";
import { formatAddressForChain } from "../../../../../utils/addressResolver";
import { fetchContracts } from "./list";

type ContractsShowOptions = z.infer<typeof contractsShowOptionsSchema>;

interface ContractData {
addressBytes: string;
chainId: ethers.BigNumberish;
contractType: string;
}

const findContractByChainId = (
contracts: ContractData[],
chainId: string,
type: string
): ContractData | null => {
const matchingContracts = contracts.filter(
(contract) => contract.chainId.toString() === chainId
);

return (
matchingContracts.find(
(contract) => contract.contractType.toLowerCase() === type.toLowerCase()
) || null
);
};

const main = async (options: ContractsShowOptions) => {
try {
const contracts = await fetchContracts(options.rpc);

const contract = findContractByChainId(
contracts,
options.chainId,
options.type
);

if (!contract) {
console.error(
chalk.red(
`Contract on chain '${options.chainId}' with type '${options.type}' not found`
)
);
console.log(chalk.yellow("Available contracts:"));
const availableContracts = contracts
.map((c) => `${c.chainId.toString()}:${c.contractType}`)
.sort();
console.log(availableContracts.join(", "));
process.exit(1);
}

const address = formatAddressForChain(
contract.addressBytes,
contract.chainId
);
console.log(address);
} catch (error) {
console.error(chalk.red("Error details:"), error);
process.exit(1);
}
};

export const showCommand = new Command("show")
.alias("s")
.summary("Show a protocol contract address on a specific chain")
.addOption(
new Option("--rpc <url>", "Custom RPC URL").default(DEFAULT_EVM_RPC_URL)
)
.addOption(
new Option("-c, --chain-id <chainId>", "Chain ID").makeOptionMandatory()
)
.addOption(
new Option("-t, --type <type>", "Contract type").makeOptionMandatory()
)
.action(async (options: ContractsShowOptions) => {
const validatedOptions = contractsShowOptionsSchema.parse(options);
await main(validatedOptions);
});
2 changes: 2 additions & 0 deletions packages/commands/src/query/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Command } from "commander";
import { balancesCommand } from "./balances";
import { cctxCommand } from "./cctx";
import { chainsCommand } from "./chains";
import { contractsCommand } from "./contracts";
import { feesCommand } from "./fees";
import { tokensCommand } from "./tokens";

Expand All @@ -16,6 +17,7 @@ You can retrieve balances, token information, supported chain details, cross-cha
)
.addCommand(balancesCommand)
.addCommand(cctxCommand)
.addCommand(contractsCommand)
.addCommand(feesCommand)
.addCommand(tokensCommand)
.addCommand(chainsCommand)
Expand Down
2 changes: 2 additions & 0 deletions src/constants/addresses.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export const MULTICALL_ADDRESS = "0xca11bde05977b3631167028862be2a173976ca11";
export const CONTRACT_REGISTRY_ADDRESS =
"0x7cce3eb018bf23e1fe2a32692f2c77592d110394";
15 changes: 15 additions & 0 deletions src/schemas/commands/contracts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { z } from "zod";

import { DEFAULT_EVM_RPC_URL } from "../../constants/api";

export const contractsListOptionsSchema = z.object({
columns: z.array(z.enum(["type", "address"])).default(["type", "address"]),
json: z.boolean().default(false),
rpc: z.string().default(DEFAULT_EVM_RPC_URL),
});

export const contractsShowOptionsSchema = z.object({
chainId: z.string(),
rpc: z.string().default(DEFAULT_EVM_RPC_URL),
type: z.string(),
});
6 changes: 5 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
"outDir": "./dist",
"skipLibCheck": true,
"sourceMap": false,
"strict": true
"strict": true,
"types": [
"node",
"jest"
]
},
"files": [
"./hardhat.config.ts"
Expand Down
Loading