Deploy contracts

deploy, from @rocketh/deploy, deploys a contract from its artifact, records it, and returns the record. Run it again and it compares what is recorded with what you ask for, and does nothing when they match. Almost every project starts with it, and @rocketh/proxy, @rocketh/diamond and @rocketh/router are built on it.

It is an extension: spread it into extensions in rocketh/config.ts, as in Get started.

Deploying a contract

import {deployScript, artifacts} from '../rocketh/deploy.js';

export default deployScript(
	async ({deploy, execute, namedAccounts}) => {
		const {deployer, admin} = namedAccounts;

		const registry = await deploy('GreetingsRegistry', {
			account: deployer,
			artifact: artifacts.GreetingsRegistry,
			args: [''],
		});

		// only on the run that actually deployed it
		if (registry.newlyDeployed) {
			await execute(registry, {account: deployer, functionName: 'setMessage', args: ['hello']});
		}
	},
	{tags: ['GreetingsRegistry', 'GreetingsRegistry_deploy']},
);

deploy(name, construction, options?):

  • name is the deployment's name: the record is deployments/<environment>/<name>.json, and get(name) reads it back. It does not have to be the contract's name, which is how one artifact is deployed several times.
  • construction says what to deploy and from whom: account (a named account, or an address), artifact (the ABI and bytecode, typed, so args are checked against the constructor), args, and value (wei for a payable constructor). Any other field of viem's deployContract parameters can be given too (gas, nonce, the fee fields).
  • options are deterministic (see Deterministic addresses), libraries (below), linkedData (below), and the re-deployment controls (below).

It returns the record with one extra field, newlyDeployed, which is true only on the run that sent the deployment transaction. Branch on it for a step that must follow a first deployment.

When does a re-run redeploy?

A deploy script is meant to be re-runnable, so deploy first asks whether the contract it is about to deploy is already deployed. Three options control that question, and the type lets you set only one of them:

optioneffect
skipIfAlreadyDeployedif a deployment with this name exists, return it and look no further: the code is never compared
alwaysOverrideredeploy unconditionally, comparing nothing (combined with skipIfAlreadyDeployed, it throws)
strictBytecodeMatchhow to compare, when comparing happens (see below)

With neither skipIfAlreadyDeployed nor alwaysOverride, rocketh compares the saved deployment's code and constructor arguments against what you are deploying now, and redeploys only if they differ.

By default that comparison ignores the contract metadata. Solidity appends a CBOR metadata blob to a contract's runtime bytecode, and that blob changes when things that do not affect behaviour change: a comment, an absolute source path, a compiler patch version. Comparing raw bytes would therefore redeploy (or, worse, UPGRADE A PROXY) because someone reformatted a file. So rocketh strips the metadata from both sides before comparing (ADR 0004). For the proxy contract itself, @rocketh/proxy forces this off for exactly that reason and does not let you change it; a strictBytecodeMatch you pass to deployViaProxy still applies to the implementation.

Set strictBytecodeMatch: true to compare the bytes verbatim instead, metadata included:

await deploy(
	'GreetingsRegistry',
	{account: deployer, artifact: artifacts.GreetingsRegistry, args: ['']},
	{strictBytecodeMatch: true},
);

Use it when you need the deployed contract to correspond to one exact compilation, typically because a verification or attestation flow pins the metadata hash, so a metadata-only difference genuinely IS a different artifact to you. The cost is that recompiling on a different machine, or with a patch-level compiler bump, will redeploy.

Linking libraries

A contract that calls an external library is deployed with the library's address linked into its bytecode. Deploy the library first, then pass libraries, a map from each library's name to its address (several can be given):

import {deployScript, artifacts} from '../rocketh/deploy.js';

export default deployScript(
	async ({deploy, namedAccounts}) => {
		const {deployer} = namedAccounts;

		// Deploy the library first
		const exampleLibrary = await deploy('ExampleLibrary', {
			account: deployer,
			artifact: artifacts.ExampleLibrary,
		});

		// Deploy a contract that uses the library
		await deploy(
			'Example',
			{
				account: deployer,
				artifact: artifacts.Example,
				args: ['example string argument'],
			},
			{
				libraries: {
					ExampleLibrary: exampleLibrary.address,
				},
			},
		);
	},
	{tags: ['Example', 'Example_deploy']},
);

Attaching data to a deployment: linkedData

linkedData stores JSON next to the record, for values a frontend or a later script needs and cannot cheaply read from the chain (a prefix, an admin, a start block):

await deploy(
	'GreetingsRegistry',
	{account: deployer, artifact: artifacts.GreetingsRegistry, args: [prefix]},
	{linkedData: {prefix, admin}},
);

It is copied into every export, so it is public: never put a secret in it. bigint values are stored as strings.

Calling your contracts: read, execute, tx

The work after a deployment (wiring contracts together, transferring ownership, seeding state) is done with @rocketh/read-execute, an extension like @rocketh/deploy:

export default deployScript(
	async ({deploy, read, execute, namedAccounts}) => {
		const {deployer, admin} = namedAccounts;
		const token = await deploy('Token', {account: deployer, artifact: artifacts.Token, args: ['Token', 'TKN']});

		// a view call, typed from the ABI
		const owner = await read(token, {functionName: 'owner'});

		// a transaction, only when it would change something
		if (owner.toLowerCase() !== admin.toLowerCase()) {
			await execute(token, {account: deployer, functionName: 'transferOwnership', args: [admin]});
		}
	},
	{tags: ['Token']},
);
  • read(deployment, {functionName, args?, account?}) makes a call and returns the decoded result; account is the msg.sender of the call. The deployment can be a record or anything with an address and an abi.
  • execute(deployment, {account, functionName, args?, value?, message?, guard?}) sends a transaction and resolves to its receipt. account is required; message is how rocketh describes the transaction to a human; the other fields of viem's writeContract (gas, the fee fields) can be given too. A guard states the on-chain condition under which the call is still needed, and skips it otherwise.
  • tx({account, to, data?, value?}, {message?}) sends a transaction with no ABI involved: a plain transfer, a call built by hand.
  • readByName and executeByName take a deployment name instead of a record, for a contract another script deployed (a typo then fails at run time, not at compile time).
  • evaluateGuard(guard, target?) evaluates a guard without sending anything, to know which privileged steps are still pending.

Everything they send goes through rocketh's broadcast path, so a transaction from an account rocketh cannot sign for (a Safe) is handed to you rather than failing, and an interrupted run resumes it rather than sending it twice.

Beyond a plain contract

Search
Search the documentation