Quickstart
A minimal Foundry or Hardhat project from zero to a typed, idempotent deploy.
1. Compile
forge build
# or: npx hardhat compiledeployoor auto-detects Hardhat (artifacts/) or Foundry (out/ + out/build-info).
2. Generate deployers
npx deployoor generateThis 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.tsMulti-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 deployRecords 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.