Skip to content
LogoLogo

Quickstart

A minimal Foundry or Hardhat project from zero to a typed, idempotent deploy.

1. Compile

forge build
# or: npx hardhat compile

deployoor auto-detects Hardhat (artifacts/) or Foundry (out/ + out/build-info).

2. Generate deployers

npx deployoor generate

This writes ./deployers/ — one typed getOrDeploy<Name> per deployable contract.

// deployers/Counter.ts — generated; do not edit by hand
import { defineDeployer } from "deployoor";
import config from "../deployoor.config";
import { counterArtifact } from "./types/Counter";
 
export const getOrDeployCounter = defineDeployer(counterArtifact, config);

Call it from a script or test — you get back a typed viem contract object:

const { contract, freshDeploy } = await getOrDeployCounter({
  walletClient,
  publicClient,
  args: [0n],
});
 
await contract.write.increment();

3. Write a deploy script

// scripts/deploy.ts
import { createWalletClient, createPublicClient, http } from "viem";
import { sepolia } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import { getOrDeployToken } from "../deployers";
 
const account = privateKeyToAccount(process.env.PK as `0x${string}`);
const transport = http(process.env.RPC_URL);
const clients = {
  walletClient: createWalletClient({ account, chain: sepolia, transport }),
  publicClient: createPublicClient({ chain: sepolia, transport }),
};
 
const { contract: token, freshDeploy } = await getOrDeployToken({
  ...clients,
  args: [account.address],
});
 
if (freshDeploy) {
  console.log("Deployed fresh — run one-time setup here");
}
 
await token.write.transfer([recipient, amount]);
tsx --env-file=.env scripts/deploy.ts

Multi-chain deploy (LayerZero Ping / Pong)

One script can target multiple networks — pass a different viem client pair per chain. deployoor records each deploy under its own deployments/<chainId>-<network>/ folder.

This example deploys Ping on Sepolia and Pong on Base Sepolia, then links them as LayerZero peers:

// scripts/deploy.ts — full version in examples/multi-chain/
import { createPublicClient, createWalletClient, http, pad, type Address } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { baseSepolia, sepolia } from "viem/chains";
import { getOrDeployPing, getOrDeployPong } from "../deployers";
 
const LZ_ENDPOINT = "0x6EDCE65408990e3A38e31dE08b74da7D5258d898";
const LZ_EID = { sepolia: 40_161, baseSepolia: 40_245 } as const;
 
function clientsFor(chain: typeof sepolia, rpcUrl: string, privateKey: `0x${string}`) {
  const account = privateKeyToAccount(privateKey);
  const transport = http(rpcUrl);
  return {
    walletClient: createWalletClient({ account, chain, transport }),
    publicClient: createPublicClient({ chain, transport }),
  };
}
 
const key = process.env.PRIVATE_KEY as `0x${string}`;
const sepoliaClients = clientsFor(sepolia, process.env.SEPOLIA_RPC_URL!, key);
const baseClients = clientsFor(baseSepolia, process.env.BASE_SEPOLIA_RPC_URL!, key);
 
const { contract: ping } = await getOrDeployPing({ ...sepoliaClients, args: [LZ_ENDPOINT] });
const { contract: pong } = await getOrDeployPong({ ...baseClients, args: [LZ_ENDPOINT] });
 
const peer = (addr: Address) => pad(addr, { size: 32 });
await ping.write.setPeer([LZ_EID.baseSepolia, peer(pong.address)]);
await pong.write.setPeer([LZ_EID.sepolia, peer(ping.address)]);
# .env: PRIVATE_KEY, SEPOLIA_RPC_URL, BASE_SEPOLIA_RPC_URL
pnpm --filter @example/multi-chain generate
pnpm --filter @example/multi-chain deploy

Records land in:

deployments/
├─ 11155111-sepolia/
│  └─ Ping.json
└─ 84532-base-sepolia/
   └─ Pong.json

Runnable contracts + script: examples/multi-chain.

4. Check the record

Every deploy writes a committed JSON file:

deployments/
└─ 11155111-sepolia/
   └─ Token.json

First run deploys and records. Later runs return the same contract with no transaction.

What you get back

getOrDeploy resolves to:

{
  contract,      // typed viem contract — read/write immediately
  freshDeploy,   // true only when this call broadcast a deploy tx
  receipt,       // deploy receipt (only on fresh deploy)
  deployment,    // full DeploymentRecord
}

Use freshDeploy to gate one-time setup (e.g. initialize()) only when the contract was actually deployed this run.