# Plugins

Plugins are lifecycle hooks. A verifier, a Slack notification, and a gas report are all the same kind of thing — small named objects subscribed to hooks in the deploy pipeline, and to `deployoor verify`.

## Built-in plugins

| Package                 | Hooks                                  | Purpose                              |
| ----------------------- | -------------------------------------- | ------------------------------------ |
| `@deployoor/etherscan`  | `onContractDeployed`, `onVerify`       | Etherscan V2 (one key, all chains)   |
| `@deployoor/sourcify`   | `onContractDeployed`, `onVerify`       | Sourcify v2 (keyless)                |
| `@deployoor/blockscout` | `onContractDeployed`, `onVerify`       | Any Blockscout instance              |
| `@deployoor/routescan`  | `onContractDeployed`, `onVerify`       | Routescan (chain id picks the index) |
| `@deployoor/slack`      | `onContractDeployed`, `onDeployFailed` | Slack webhook on deploy              |

The four verifiers are independent — run as many as you like, and see
[Verify contracts](/guides/verify) for what each needs and how `deployoor verify` replays them.

```ts
import { defineConfig } from "deployoor";
import { etherscan } from "@deployoor/etherscan";
import { sourcify } from "@deployoor/sourcify";
import { slack } from "@deployoor/slack";

export default defineConfig({
  plugins: [
    etherscan({ apiKey: process.env.ETHERSCAN_KEY }),
    sourcify(),
    slack({ webhook: process.env.SLACK_WEBHOOK }),
  ],
});
```

By default a failing plugin **warns** and the deploy still records (the transaction already happened). Set `onPluginError: "throw"` to fail the run.

## Hooks

Every hook is plain async (or sync) and receives `(ctx, deps)`, where `deps` carries `fetch`, `now` and `log` — inject them in tests instead of reaching for the globals.

| Hook                 | Called by          | Context                                                             |
| -------------------- | ------------------ | ------------------------------------------------------------------- |
| `onContractDeployed` | a deploy           | `DeployedContext` — `deployment`, `reused`, `receipt?`, `metadata?` |
| `onDeployFailed`     | a failed deploy    | `DeployFailedContext` — names, chain, `cause`                       |
| `onVerify`           | `deployoor verify` | `VerifyContext` — `deployment`, `metadata`                          |

### `onVerify`

`deployoor verify` verifies recorded deployments after the fact, and calls **only** this hook. Two consequences worth knowing:

* **A plugin that omits `onVerify` is skipped by that command.** This is how `@deployoor/slack` stays quiet on a verify run without inspecting anything. If none of your configured plugins implements it, `deployoor verify` fails rather than reporting a run that did nothing.
* **`VerifyContext` is not `DeployedContext`.** Nothing was deployed, so there is no `receipt` and no `reused` flag to misread, and `metadata` is **required** — a record whose sources were never pinned cannot be verified from committed data, so it is reported as unverifiable and never reaches a plugin. There is no `options` either: your plugin instance already closes over its own configuration, which is what `etherscan({ apiKey })` is.

```ts
export interface VerifyContext {
  readonly deployment: DeploymentRecord; // address, chain, abi, constructor args, libraries
  readonly metadata: ContractMetadata; // pinned fqn + compiler version + standard-json
}
```

A verifier should implement both hooks over one shared body, so it works at deploy time and after the fact. Both are handed exactly a record plus a `ContractMetadata`, so there is nothing to branch on:

```ts
import {
  definePlugin,
  type ContractMetadata,
  type DeploymentRecord,
  type PluginDeps,
} from "deployoor/plugin";

const submit = async (deployment: DeploymentRecord, metadata: ContractMetadata, deps: PluginDeps) => {
  // …one implementation: build the standard-json request, poll it to a conclusion, throw on failure
};

export const myVerifier = () =>
  definePlugin({
    name: "my-verifier",
    onContractDeployed: async (ctx, deps) => {
      if (ctx.metadata === undefined) return; // a deploy may have no compiler input to offer
      await submit(ctx.deployment, ctx.metadata, deps);
    },
    onVerify: (ctx, deps) => submit(ctx.deployment, ctx.metadata, deps),
  });
```

Throwing from `onVerify` marks that contract failed for that plugin; the run moves to the next contract and exits non-zero at the end.

## Author a plugin

Plugins import only from `deployoor/plugin`:

```ts
import { definePlugin } from "deployoor/plugin";

export const myPlugin = (opts: { url: string }) =>
  definePlugin({
    name: "my-plugin",
    onContractDeployed: async (ctx, { fetch }) => {
      await fetch(opts.url, {
        method: "POST",
        body: JSON.stringify({
          contract: ctx.deployment.contractName,
          address: ctx.deployment.address,
        }),
      });
    },
  });
```

Ship it as its own npm package that peer-depends on `deployoor`.

## Hardhat plugin (not a lifecycle plugin)

[`@deployoor/hardhat`](/packages) is different — it hooks `hardhat compile` to run `deployoor generate` automatically. It imports `deployoor/generate`, not `deployoor/plugin`.
