npm package: @neynar/smart-contract-ts-generator
Description: Generate TypeScript contract wrappers from contract addresses on any EVM chain
Author: Neynar
Latest version: 0.7.1

# Contract TypeScript Generator

Automatically generate TypeScript wrappers for smart contracts from their addresses on various blockchain networks.

## Features

- Fetches contract ABIs from Etherscan (and variants) or Sourcify
- Generates type-safe TypeScript code using abitype
- Creates React hooks using wagmi for contract interaction
- Optional class-based client for server-side or complex use cases
- Supports multiple chains (Base, Optimism, Ethereum, and testnets)

## Installation

### NPM

```bash
npm install -g @neynar/smart-contract-ts-generator
```

### Yarn

```bash
yarn global add @neynar/smart-contract-ts-generator
```

### NPX (No Installation)

```bash
npx @neynar/smart-contract-ts-generator --address <CONTRACT_ADDRESS> --chain <CHAIN_NAME> --name <CONTRACT_NAME>
```

### Local Development (Without Publishing)

For testing locally without publishing to npm:

```bash
# Clone and set up
git clone https://github.com/neynarxyz/smart-contract-ts-generator.git
cd smart-contract-ts-generator
yarn install
yarn build

# Option 1: Link globally (recommended)
yarn link
# Now you can use: smart-contract-gen ...

# Option 2: Use yarn from within the repo
yarn generate --address 0x... --chain base --name USDC

# Option 3: Use npx from within the repo
npx . --address 0x... --chain base --name USDC
```

## Usage

### Basic Usage

```bash
smart-contract-gen \
  --address <CONTRACT_ADDRESS> \
  --chain <CHAIN_NAME> \
  --name <CONTRACT_NAME>
```

Or with npx:

```bash
npx @neynar/smart-contract-ts-generator \
  --address <CONTRACT_ADDRESS> \
  --chain <CHAIN_NAME> \
  --name <CONTRACT_NAME>
```

### Examples

#### Basic Usage - USDC on Base

```bash
smart-contract-gen \
  --address 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 \
  --chain base \
  --name USDC
```

Output: `./base/usdc/`

#### Custom Root Directory

```bash
smart-contract-gen \
  --address 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 \
  --chain base \
  --name USDC \
  --output ./contracts
```

Output: `./contracts/base/usdc/`

### CLI Options

#### Required Options

- `-a, --address <address>` - Contract address (0x-prefixed hex)
- `-c, --chain <chain>` - Chain name (see supported chains below)
- `-n, --name <name>` - Contract name for generated files (e.g., USDC, MyToken)

#### Optional Options

- `-o, --output <root>` - Output root directory (default: current directory). Files are generated at `<root>/<chain>/<name>`
- `-k, --api-key <key>` - Etherscan API key (or set `ETHERSCAN_API_KEY` env var)
- `--no-class` - Skip generating class-based client (only generate hooks)
- `-h, --help` - Show help message

### Supported Chains

| Chain            | Chain ID | Explorer API                        |
| ---------------- | -------- | ----------------------------------- |
| base             | 8453     | api.basescan.org                    |
| base-sepolia     | 84532    | api-sepolia.basescan.org            |
| optimism         | 10       | api-optimistic.etherscan.io         |
| optimism-sepolia | 11155420 | api-sepolia-optimistic.etherscan.io |
| ethereum         | 1        | api.etherscan.io                    |
| sepolia          | 11155111 | api-sepolia.etherscan.io            |

### Using Etherscan API Key

For better rate limits and reliability, provide an Etherscan API key:

```bash
# Via environment variable
export ETHERSCAN_API_KEY=your_api_key
smart-contract-gen ...

# Or via CLI argument
smart-contract-gen \
  --api-key your_api_key \
  --address ... \
  --chain ... \
  --name ...
```

Get a free API key from:

- Base: https://basescan.org/apis
- Optimism: https://optimistic.etherscan.io/apis
- Ethereum: https://etherscan.io/apis

## Generated Files

The generator creates a directory with the following structure:

```
<output-directory>/
├── abi.ts         # Raw ABI with const assertion
├── hooks.ts       # React hooks for wagmi
├── client.ts      # Class-based client (optional)
└── index.ts       # Public exports
```

### Example Generated Code

#### React Hooks (hooks.ts)

```typescript
import { useReadContract, useWriteContract } from 'wagmi';
import type { Address } from 'viem';
import { CONTRACT_ABI } from './abi.ts';

// Read function hook
export function useUsdcBalanceOf(
  contractAddress: Address,
  account: Address,
  options?: { enabled?: boolean; watch?: boolean }
) {
  return useReadContract({
    address: contractAddress,
    abi: CONTRACT_ABI,
    functionName: 'balanceOf',
    args: [account],
    query: options,
  });
}

// Write function hook
export function useUsdcTransfer(contractAddress: Address) {
  const { writeContract, ...rest } = useWriteContract();

  const write = ({ to, amount }: { to: Address; amount: bigint }) => {
    return writeContract({
      address: contractAddress,
      abi: CONTRACT_ABI,
      functionName: 'transfer',
      args: [to, amount],
    });
  };

  return { ...rest, write, writeAsync: write };
}
```

#### Class-Based Client (client.ts)

```typescript
import type { PublicClient, WalletClient, Address, Hex } from 'viem';
import { CONTRACT_ABI } from './abi.ts';

export class UsdcContract {
  private publicClient: PublicClient;
  private walletClient?: WalletClient;
  private address: Address;

  constructor(address: Address, publicClient: PublicClient, walletClient?: WalletClient) {
    this.address = address;
    this.publicClient = publicClient;
    this.walletClient = walletClient;
  }

  // Read method
  async balanceOf(account: Address): Promise<bigint> {
    return await this.publicClient.readContract({
      address: this.address,
      abi: CONTRACT_ABI,
      functionName: 'balanceOf',
      args: [account],
    });
  }

  // Write method
  async transfer(to: Address, amount: bigint): Promise<Hex> {
    if (!this.walletClient) {
      throw new Error('Wallet client required for write operations');
    }

    const { request } = await this.publicClient.simulateContract({
      address: this.address,
      abi: CONTRACT_ABI,
      functionName: 'transfer',
      args: [to, amount],
      account: this.walletClient.account,
    });

    return await this.walletClient.writeContract(request);
  }
}
```

## Using Generated Code

### In React Components (Hooks)

```typescript
import { useUsdcBalanceOf, useUsdcTransfer } from './contracts/usdc';

function MyComponent() {
  const contractAddress = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
  const userAddress = '0x...';

  // Read balance
  const { data: balance, isLoading } = useUsdcBalanceOf(contractAddress, userAddress);

  // Write transfer
  const { write: transfer, isPending } = useUsdcTransfer(contractAddress);

  const handleTransfer = () => {
    transfer({
      to: '0xRecipient...',
      amount: 1000000n, // 1 USDC (6 decimals)
    });
  };

  return (
    <div>
      <p>Balance: {balance?.toString()}</p>
      <button onClick={handleTransfer} disabled={isPending}>
        Transfer
      </button>
    </div>
  );
}
```

### In Server-Side Code (Class)

```typescript
import { createPublicClient, createWalletClient, http } from 'viem';
import { base } from 'viem/chains';
import { UsdcContract } from './contracts/usdc';

const publicClient = createPublicClient({
  chain: base,
  transport: http(),
});

const walletClient = createWalletClient({
  chain: base,
  transport: http(),
  account: yourAccount,
});

const usdc = new UsdcContract(
  '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
  publicClient,
  walletClient
);

// Read balance
const balance = await usdc.balanceOf('0x...');

// Transfer tokens
const txHash = await usdc.transfer('0xRecipient...', 1000000n);
```

## How It Works

1. **Fetch ABI**: Retrieves the contract ABI from Etherscan API (primary) or Sourcify (fallback)
2. **Parse ABI**: Validates and categorizes functions (read vs write), events, and constructor
3. **Generate Code**: Creates TypeScript files with:
   - ABI constant with type assertion
   - React hooks for each function
   - Optional class-based client
   - Type-safe function arguments and return types
4. **Write Files**: Outputs formatted TypeScript files to the specified directory

## Type Safety

All generated code is fully type-safe using:

- **abitype**: Provides strict TypeScript types derived from the ABI
- **viem**: Ensures type-safe contract interactions
- **wagmi**: Type-safe React hooks for Ethereum

The generator automatically converts Solidity types to TypeScript:

- `uint256` → `bigint`
- `address` → `Address` (from viem)
- `bytes` → `Hex` (from viem)
- `bool` → `boolean`
- `string` → `string`
- Arrays → `readonly T[]`

## Troubleshooting

### Contract Not Found

Make sure the contract is verified on Etherscan or Sourcify. Unverified contracts cannot have their ABIs fetched.

### Rate Limiting

If you encounter rate limiting errors:

1. Get a free Etherscan API key (see links above)
2. Use the `--api-key` option or `ETHERSCAN_API_KEY` environment variable
3. The tool automatically falls back to Sourcify if Etherscan fails

### Invalid ABI

If the generated code has issues, check that:

1. The contract address is correct
2. The contract is on the specified chain
3. The contract is verified on the block explorer

## Contributing

To add support for new chains:

1. Add the chain configuration to `CHAIN_CONFIGS` in [fetch-abi.ts](./fetch-abi.ts)
2. Include the chain ID, name, Etherscan API URL, and Sourcify support flag

## License

Part of the Neynar monorepo.