Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Cannon is a DevOps tool for EVM chains designed for testing, deploying, and publ
- `cannon build --keep-alive` - Build and keep node running for interaction
- `cannon run <package:version>` - Run a built package locally
- `cannon test` - Build cannonfile and run forge tests with deployment context
- `cannon fetch <ipfs-hash> [package:version]` - Fetch package data from IPFS (auto-detects package name)
- `cannon publish <package:version> --chain-id <id>` - Publish package to registry
- `cannon verify <package:version>` - Verify contracts on Etherscan
- `cannon inspect <package:version>` - Inspect package deployment data
Expand Down Expand Up @@ -158,6 +159,7 @@ examples/ # Sample projects and usage demos
4. **Test**: `cannon test` runs forge tests with deployment context using cannon-std
5. **Deploy**: `cannon build --rpc-url <url> --private-key <key>` for live networks
6. **Publish**: `cannon publish <name:version> --chain-id <id>` to share with others
7. **Fetch**: `cannon fetch <ipfs-hash>` to download and use published packages

### Cannonfile Structure
```toml
Expand Down Expand Up @@ -185,6 +187,26 @@ args = []
- Chain actions using template interpolation for dependencies
- Add `privateSourceCode = true` to exclude source from published packages

### Fetching Packages
The `cannon fetch` command downloads package data from IPFS and stores it locally:

```bash
# Auto-detect package name from IPFS data (recommended)
cannon fetch ipfs://QmTK6qhaBAxwRTmFVejHyKyVeAzibxeWdJ1j3LXVj98eej --chain-id 1

# Or specify package name explicitly for validation
cannon fetch ipfs://QmTK6qhaBAxwRTmFVejHyKyVeAzibxeWdJ1j3LXVj98eej synthetix-omnibus:3.10.1 --chain-id 1

# Use the fetched package
cannon run synthetix-omnibus:3.10.1 --chain-id 1
```

**When to use `cannon fetch`:**
- Download published packages from IPFS to use locally
- Share package deployments across teams using IPFS hashes
- Access packages that aren't published to the on-chain registry
- Work with packages in development or testing environments

## Development Notes

- Use `pnpm` exclusively (enforced by preinstall hook)
Expand Down
12 changes: 7 additions & 5 deletions packages/cli/src/commands/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,16 +274,18 @@ export const commandsConfig: CommandsConfig = {
],
},
fetch: {
description: 'Fetch cannon package data from an IPFS hash and store it in the local registry.',
description:
'Fetch cannon package data from an IPFS hash and store it in the local registry. Package name will be auto-detected from IPFS data if not specified.',
arguments: [
{
flags: '<packageRef>',
description: 'Name, version and preset of the Cannon package to fetch from (name:version@preset)',
},
{
flags: '<ipfsHash>',
description: 'IPFS hash to fetch deployment data from',
},
{
flags: '[packageRef]',
description:
'Optional: Name, version and preset of the Cannon package (name:version@preset). Will be auto-detected if not provided.',
},
],
options: [
{
Expand Down
83 changes: 66 additions & 17 deletions packages/cli/src/commands/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import fs from 'node:fs';
import path from 'path';
import util from 'util';

import { logSpinner } from '../util/console';
import { log, logSpinner, warnSpinner } from '../util/console';
import { LocalRegistry } from '../registry';
import { resolveCliSettings } from '../settings';

import { yellow, bold } from 'chalk';

const debug = Debug('cannon:cli:fetch');

const mkdir = util.promisify(fs.mkdir);
Expand All @@ -38,7 +40,7 @@ async function storeDeployReference(filePath: string, content: string) {
}
}

export async function fetch(fullPackageRef: string, chainId: number, _ipfsUrl: string, _metaIpfsUrl?: string) {
export async function fetch(fullPackageRef: string | null, chainId: number | null, _ipfsUrl: string, _metaIpfsUrl?: string) {
const ipfsUrl = getIpfsUrl(_ipfsUrl);
const metaIpfsUrl = getIpfsUrl(_metaIpfsUrl);

Expand All @@ -50,8 +52,6 @@ export async function fetch(fullPackageRef: string, chainId: number, _ipfsUrl: s

const cliSettings = resolveCliSettings();

const { name, version, preset } = new PackageReference(fullPackageRef);

const localRegistry = new LocalRegistry(cliSettings.cannonDirectory);

const storage = new CannonStorage(localRegistry, {
Expand All @@ -66,34 +66,83 @@ export async function fetch(fullPackageRef: string, chainId: number, _ipfsUrl: s
// Fetching deployment info
const deployInfo: DeploymentInfo = await storage.readBlob(ipfsUrl);

const def = new ChainDefinition(deployInfo.def);

const preCtx = await createInitialContext(def, deployInfo.meta, deployInfo.chainId || chainId, deployInfo.options);

const pkgName = `${name}:${def.getVersion(preCtx) || version}@${preset}`;

if (!deployInfo || Object.keys(deployInfo).length === 0) {
throw new Error(`could not find package data on IPFS using the hash: ${ipfsUrl}`);
throw new Error(
`Could not find package data on IPFS using the hash: ${ipfsUrl}\n` +
'Please verify that:\n' +
' - The IPFS hash is correct\n' +
' - The IPFS gateway is accessible\n' +
' - The hash contains valid Cannon package data'
);
}

if (name !== deployInfo.def.name) {
throw new Error(`deployment data at ${ipfsUrl} does not match the specified package "${pkgName}"`);
const def = new ChainDefinition(deployInfo.def);
const preCtx = await createInitialContext(
def,
deployInfo.meta,
deployInfo.chainId || chainId || 13370,
deployInfo.options
);

let packageRef = '';
if (fullPackageRef) {
// Package reference was provided, validate it matches the IPFS data
const ref = new PackageReference(fullPackageRef);

if (ref.name !== deployInfo.def.name) {
warnSpinner(
yellow(
'The IPFS package you downloaded is being saved to a different name than is recorded in the package data. Please double check to make sure this is correct.'
)
);
warnSpinner(yellow(bold(`Package Name (IPFS Data): ${deployInfo.def.name}`)));
warnSpinner(yellow(bold(`Provided Name: ${ref.name}`)));
}
if (ref.version !== deployInfo.def.version) {
warnSpinner(
yellow(
'The IPFS package you downloaded is being saved to a different version than is recorded in the package data. Please double check to make sure that this is correct.'
)
);
warnSpinner(yellow(bold(`Package Version (IPFS Data): ${deployInfo.def.version}`)));
warnSpinner(yellow(bold(`Provided Version: ${ref.version}`)));
}
if (deployInfo.chainId !== null && chainId !== deployInfo.chainId) {
warnSpinner(
yellow(
'The IPFS package you downloaded is being saved to a different chain ID than is recorded in the package data. Please double check to make sure that this is correct.'
)
);
warnSpinner(yellow(bold(`Chain ID (IPFS Data): ${deployInfo.chainId}`)));
warnSpinner(yellow(bold(`Chain ID (User Input): ${chainId}`)));
}

packageRef = fullPackageRef;
} else {
// Auto-detect package information from IPFS data
packageRef = new PackageReference(
`${deployInfo.def.name}:${def.getVersion(preCtx) || 'latest'}@${def.getPreset(preCtx)}`
).fullPackageRef;

log(`\nDetected package: ${packageRef}`);
}

debug('storing deploy info');

const deployPath = localRegistry.getTagReferenceStorage(pkgName, deployInfo.chainId || chainId);
const resolvedChainId = chainId || deployInfo.chainId || 13370;

const deployPath = localRegistry.getTagReferenceStorage(packageRef, resolvedChainId);

await storeDeployReference(deployPath, ipfsUrl);

if (metaIpfsUrl) {
debug('reading metadata from ipfs');
const deployMetadataPath = localRegistry.getMetaTagReferenceStorage(pkgName, chainId);
const deployMetadataPath = localRegistry.getMetaTagReferenceStorage(packageRef, resolvedChainId);
await storeDeployReference(deployMetadataPath, metaIpfsUrl);
}

logSpinner(`\n\nSuccessfully fetched and saved deployment data for the following package: ${pkgName}`);
logSpinner(`\n\nSuccessfully fetched and saved deployment data for the following package: ${packageRef}`);
logSpinner(
`run 'cannon publish ${pkgName} --chain-id <CHAIN_ID> --private-key <PRIVATE_KEY>' to publish the package to the registry`
`run 'cannon publish ${packageRef} --chain-id <CHAIN_ID> --private-key <PRIVATE_KEY>' to publish the package to the registry`
);
}
10 changes: 8 additions & 2 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,14 +370,20 @@ applyCommandsConfig(program.command('alter'), commandsConfig.alter).action(async
});

applyCommandsConfig(program.command('fetch'), commandsConfig.fetch).action(async function (
packageRef,
givenIpfsUrl,
packageRef,
options
) {
try {
const { fetch } = await import('./commands/fetch');

const { fullPackageRef, chainId } = await getPackageReference(packageRef, options.chainId);
let fullPackageRef = null;
let chainId = null;
if (packageRef) {
const refInfo = await getPackageReference(packageRef, options.chainId);
fullPackageRef = refInfo.fullPackageRef;
chainId = refInfo.chainId;
}
const ipfsUrl = getIpfsUrl(givenIpfsUrl);
const metaIpfsUrl = getIpfsUrl(options.metaHash) || undefined;

Expand Down
6 changes: 5 additions & 1 deletion packages/cli/test/e2e/scripts/non-interactive/fetch.sh
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
$CANNON fetch synthetix:3.3.4@main QmUtELWtepNn1ByaFUF8YAgLMtdvy6GGe2P9ex48EVit4H --chain-id 1
# Test with auto-detection (new preferred usage)
$CANNON fetch QmUtELWtepNn1ByaFUF8YAgLMtdvy6GGe2P9ex48EVit4H --chain-id 1

# Test with explicit package name (backwards compatibility)
$CANNON fetch QmUtELWtepNn1ByaFUF8YAgLMtdvy6GGe2P9ex48EVit4H synthetix:3.3.4@main --chain-id 1
Loading
Loading