# Quickstart

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

## 1. Compile

```bash
forge build
# or: npx hardhat compile
```

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

## 2. Generate deployers

```bash
npx deployoor generate
```

This writes `./deployers/`, one typed `getOrDeploy<Name>` per deployable contract. [Commit them](/concepts/version-control) — they hold a name and an abi, and read everything else from your artifacts at deploy time, so they are small and a fresh clone typechecks without running `generate` first.

```ts
// 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, and you get back a typed viem contract object:

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

await contract.write.increment();
```

## 3. Set up your wallet, once

Two small modules that every script, test, and ops task will import. You write them one time.

```ts
// accounts.ts
import type { Hex } from "viem";
import { privateKeyToAccount } from "viem/accounts";

export const deployer = privateKeyToAccount(process.env.PK as Hex);
```

```ts
// clients.ts
import { createPublicClient, createWalletClient, http, type Chain } from "viem";
import { baseSepolia, sepolia } from "viem/chains";
import { deployer } from "./accounts";

const rpcUrls: Record<number, string | undefined> = {
  [sepolia.id]: process.env.SEPOLIA_RPC_URL,
  [baseSepolia.id]: process.env.BASE_SEPOLIA_RPC_URL,
};

export const clientsFor = (chain: Chain) => {
  const rpcUrl = rpcUrls[chain.id];
  // http() with no url silently falls back to the chain's public RPC, which is not
  // something a deploy should do by accident.
  if (rpcUrl === undefined) throw new Error(`No RPC URL for chain ${chain.id} (${chain.name})`);

  const transport = http(rpcUrl);
  return {
    walletClient: createWalletClient({ account: deployer, chain, transport }),
    publicClient: createPublicClient({ chain, transport }),
  };
};
```

`privateKeyToAccount` is one option. Swap in an encrypted keystore, AWS or GCP KMS, [Turnkey](/recipes/turnkey), [Privy](/recipes/privy), or a Ledger, and `accounts.ts` is the only file that changes, because deployoor reaches your signer only through viem. It never sees a key, so it has no opinion about where yours lives.

## 4. Write a deploy script

```ts
// scripts/deploy.ts
import { sepolia } from "viem/chains";
import { deployer } from "../accounts";
import { clientsFor } from "../clients";
import { getOrDeployToken } from "../deployers";

const { contract: token, freshDeploy } = await getOrDeployToken({
  ...clientsFor(sepolia),
  args: [deployer.address],
});

if (freshDeploy) {
  console.log("Deployed fresh, run one-time setup here");
}

await token.write.transfer([recipient, amount]);
```

```bash
tsx --env-file=.env scripts/deploy.ts
```

No deployoor CLI in this step, and no framework to boot: it is a Node program.

Two things about that script are deliberate, and they are why the rest of these docs look the way they do:

* **You built the account**, in `accounts.ts`, and every caller imports it rather than rebuilding it.
* **You chose the chain.** `chain` and `transport` are arguments, not config entries, so a second chain is another `clientsFor` call and testing against a fork means changing the transport.

Both are what make the next two sections possible: another call to `clientsFor` gives you a multi-chain deploy, and in-memory clients turn the identical `getOrDeployToken` call into a test.

## Multi-chain deploy (LayerZero Ping / Pong)

One script can target **multiple networks**, one `clientsFor` call 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:

```ts
// scripts/deploy.ts, full version in examples/multi-chain/
import { pad, type Address } from "viem";
import { baseSepolia, sepolia } from "viem/chains";
import { clientsFor } from "../clients";
import { getOrDeployPing, getOrDeployPong } from "../deployers";

const LZ_ENDPOINT = "0x6EDCE65408990e3A38e31dE08b74da7D5258d898";
const LZ_EID = { sepolia: 40_161, baseSepolia: 40_245 } as const;

const sepoliaClients = clientsFor(sepolia);
const baseClients = clientsFor(baseSepolia);

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)]);
```

```bash
# .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`](https://github.com/raycashxyz/deployoor/tree/main/examples/multi-chain).

## 5. 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:

```ts
{
  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.
