Skip to content
Open
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
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,45 @@ This command creates a directory containing a new project template, fully set up
also included. If you push your project to GitHub, GitHub Actions run your tests (named as `*.test.js`) automatically whenever you push a commit or open a pull request.
- Code style consistency (using Prettier) and linting (using ES Lint) is automatically enforced using Git pre-commit hooks. This requires no configuration and occurs automatically when you commit a change, for example, `git commit -m 'feat: add awesome feature'`.

## Deploy to Zeko L2

The zkApp CLI supports deploying to [Zeko L2](https://zeko.io), a high-performance layer 2 solution for Mina Protocol that provides faster finality and higher throughput while maintaining full o1js compatibility.

Zeko networks are integrated as standard network options in the zkApp CLI. Projects created with `zk project` are network-agnostic and can be deployed to any network (Mina L1 or Zeko L2) by configuring the appropriate deploy alias.

### Configure for Zeko deployment

To deploy to Zeko, create a new deploy alias using `zk config`:

```sh
zk config
```

When prompted to **"Choose the target network"**, select **Zeko Devnet** for development and testing.

The Zeko GraphQL endpoint URL is automatically populated based on your network selection. You don't need to manually enter it.

**Note:** Zeko mainnet will be added when it becomes available.

After configuration, deploy using the standard deploy command:

```sh
zk deploy <your-alias-name>
```

### Network-specific features

- **Auto-populated endpoints**: GraphQL URLs are automatically configured for Zeko Devnet
- **Network-specific faucets**: The CLI provides the appropriate faucet URL for requesting test MINA
- **Full o1js compatibility**: The same zkApp code works on both Mina L1 and Zeko L2
- **Multi-network support**: Configure multiple deploy aliases to deploy the same project to different networks

### Request test MINA

After configuring a Zeko deploy alias, request test MINA from the Zeko faucet:

- **Zeko Faucet**: https://zeko.io/faucet/

## Create an example project

```sh
Expand Down
38 changes: 34 additions & 4 deletions src/lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,28 @@ export {
getCachedFeepayerAddress,
getCachedFeepayerAliases,
getExplorerName,
getNetworkUrl,
printDeployAliasesConfig,
printInteractiveDeployAliasConfigSuccessMessage,
printLightnetDeployAliasConfigSuccessMessage,
recoverKeyPairStep,
savedKeyPairStep,
};

/**
* Get the GraphQL URL for a given network ID
* @param {string} networkId - The network ID (testnet, mainnet, zeko-devnet)
* @returns {string|null} The GraphQL URL or null if not a known network
*/
function getNetworkUrl(networkId) {
const urlMap = {
testnet: 'https://api.minascan.io/node/devnet/v1/graphql',
mainnet: 'https://api.minascan.io/node/mainnet/v1/graphql',
'zeko-devnet': 'https://devnet.zeko.io/graphql',
};
return urlMap[networkId] || null;
}

/**
* Show existing deploy aliases in `config.json` and allow user to add a new
* deploy alias.
Expand Down Expand Up @@ -197,6 +212,11 @@ async function createDeployAlias(projectRoot, deployAliasesConfig) {
alternateCachedFeepayerAlias,
} = promptResponse;

// Auto-populate URL
if (!url) {
url = getNetworkUrl(networkId);
}

if (!deployAliasName || !url || !fee) process.exit(1);

let feepayerKeyPair;
Expand Down Expand Up @@ -426,16 +446,26 @@ function printInteractiveDeployAliasConfigSuccessMessage(
deployAliasName,
feepayerKeyPair
) {
const networkId =
deployAliasesConfig.deployAliases[deployAliasName]?.networkId;
const explorerName = getExplorerName(
deployAliasesConfig.deployAliases[deployAliasName]?.url
);

let faucetMessage = '';
if (networkId === 'testnet') {
faucetMessage = `\n - If this is the testnet, request tMINA at:\n https://faucet.minaprotocol.com/?address=${encodeURIComponent(
feepayerKeyPair.publicKey
)}${explorerName ? `&explorer=${explorerName}` : ''}`;
} else if (networkId === 'zeko-devnet') {
faucetMessage = `\n - Request test MINA at:\n https://zeko.io/faucet/`;
faucetMessage += `\n (Use address: ${feepayerKeyPair.publicKey})`;
}

const str =
`\nSuccess!\n` +
`\nNext steps:` +
`\n - If this is the testnet, request tMINA at:\n https://faucet.minaprotocol.com/?address=${encodeURIComponent(
feepayerKeyPair.publicKey
)}` +
(explorerName ? `&explorer=${explorerName}` : '') +
faucetMessage +
`\n - To deploy zkApp, run: \`zk deploy ${deployAliasName}\``;
console.log(chalk.green(str));
}
Expand Down
9 changes: 7 additions & 2 deletions src/lib/config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ describe('config.js', () => {
fs.readdirSync.mockReturnValue([]);
enquirer.prompt.mockImplementation(async () => ({
deployAliasName: 'testAlias1',
networkId: 'testnet',
networkId: 'unknown-network',
fee: '0.01',
feepayer: 'create',
feepayerAlias: 'feePayerTestAlias1',
Expand Down Expand Up @@ -677,7 +677,12 @@ describe('config.js', () => {

printInteractiveDeployAliasConfigSuccessMessage(
{
deployAliases: { testAlias1: { url: 'https://minascan.test.url' } },
deployAliases: {
testAlias1: {
networkId: 'testnet',
url: 'https://minascan.test.url',
},
},
},
'testAlias1',
{ publicKey: 'publicKey' }
Expand Down
9 changes: 6 additions & 3 deletions src/lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@ import path from 'node:path';
/**
* @typedef {'next' | 'svelte' | 'nuxt' | 'empty' | 'none'} UiType
* @typedef {'sudoku' | 'tictactoe'} ExampleType
* @typedef {'testnet' | 'mainnet'} NetworkId
* @typedef {'testnet' | 'mainnet' | 'zeko-devnet'} NetworkId
* @typedef {'single-node' | 'multi-node'} LightnetMode
* @typedef {'fast' | 'real'} LightnetType
* @typedef {'none' | 'full'} LightnetProofLevel
* @typedef {'master' | 'compatible' | 'develop'} LightnetMinaBranch
* @typedef {'Spam' | 'Trace' | 'Debug' | 'Info' | 'Warn' | 'Error' | 'Fatal'} LightnetMinaLogLevel
*
* @type {{ uiTypes: UiType[], exampleTypes: ExampleType[], feePayerCacheDir: string, networkIds: NetworkId[], lightnetWorkDir: string, lightnetModes: LightnetMode[], lightnetTypes: LightnetType[], lightnetProofLevels: LightnetProofLevel[], lightnetMinaBranches: LightnetMinaBranch[], lightnetProcessToLogFileMapping: Map<string, string>, lightnetMinaProcessesLogLevels: LightnetMinaLogLevel[], lightnetMinaDaemonGraphQlEndpoint: string, lightnetAccountManagerEndpoint: string, lightnetArchiveNodeApiEndpoint: string }}
* @type {{ uiTypes: UiType[], exampleTypes: ExampleType[], feePayerCacheDir: string, networkIds: NetworkId[], lightnetWorkDir: string, lightnetModes: LightnetMode[], lightnetTypes: LightnetType[], lightnetProofLevels: LightnetProofLevel[], lightnetMinaBranches: LightnetMinaBranch[], lightnetProcessToLogFileMapping: Map<string, string>, lightnetMinaProcessesLogLevels: LightnetMinaLogLevel[], lightnetMinaDaemonGraphQlEndpoint: string, lightnetAccountManagerEndpoint: string, lightnetArchiveNodeApiEndpoint: string, zekoDevnetEndpoint: string, zekoDevnetArchiveEndpoint: string }}
*/
const Constants = Object.freeze({
uiTypes: ['next', 'svelte', 'nuxt', 'empty', 'none'],
exampleTypes: ['sudoku', 'tictactoe'],
feePayerCacheDir: `${homedir()}/.cache/zkapp-cli/keys`,
networkIds: ['testnet', 'mainnet'],
networkIds: ['testnet', 'mainnet', 'zeko-devnet'],
lightnetWorkDir: path.resolve(`${homedir()}/.cache/zkapp-cli/lightnet`),
lightnetModes: ['single-node', 'multi-node'],
lightnetTypes: ['fast', 'real'],
Expand Down Expand Up @@ -47,6 +47,9 @@ const Constants = Object.freeze({
lightnetMinaDaemonGraphQlEndpoint: 'http://127.0.0.1:8080/graphql',
lightnetAccountManagerEndpoint: 'http://127.0.0.1:8181',
lightnetArchiveNodeApiEndpoint: 'http://127.0.0.1:8282',
zekoDevnetEndpoint: 'https://devnet.zeko.io/graphql',
zekoDevnetArchiveEndpoint: 'https://devnet.zeko.io/graphql',
// Zeko mainnet to be added when it becomes available
});

// Module external API
Expand Down
1 change: 1 addition & 0 deletions src/lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ async function setupProject(destination, lang = 'ts') {
path.resolve(destDir, '_.npmignore'),
path.resolve(destDir, '.npmignore')
);

spin.succeed(chalk.green(step));

return true;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ async function project({ name, ui }) {
shell.mkdir('contracts');
shell.cd('contracts');
}
if (!(await setupProject(shell.pwd().toString()))) {
if (!(await setupProject(shell.pwd().toString(), 'ts'))) {
shell.exit(1);
}

Expand Down
21 changes: 16 additions & 5 deletions src/lib/prompts.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,18 @@ const prompts = {
name: 'networkId',
initial: 0, // 0 = testnet, 1 = mainnet, change it to 1 after the HF
choices: Constants.networkIds
.map((networkId) => ({
name: capitalize(networkId),
value: networkId,
}))
.map((networkId) => {
let displayName;
if (networkId === 'zeko-devnet') {
displayName = 'Zeko Devnet';
} else {
displayName = capitalize(networkId);
}
return {
name: displayName,
value: networkId,
};
})
.concat({
name: 'Custom network',
value: 'selectCustom',
Expand Down Expand Up @@ -74,7 +82,10 @@ const prompts = {
},
},
{
type: 'input',
type() {
const knownNetworks = ['testnet', 'mainnet', 'zeko-devnet'];
return !knownNetworks.includes(this.answers.networkId) ? 'input' : null;
},
name: 'url',
message: (state) => {
const style =
Expand Down
55 changes: 55 additions & 0 deletions tests/cli/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,5 +443,60 @@ test.describe('zkApp-CLI', () => {
}
});

test(`should create Zeko deployment alias with Zeko Devnet network, @serial @smoke @config @zeko`, async () => {
const projectName = crypto.randomUUID();
const networkId = 'zeko-devnet';
const deploymentAlias = crypto.randomUUID();
const feePayerAlias = crypto.randomUUID();
const feePayerAccount = await acquireAvailableAccount();
const feePayerMgmtType = 'recover';
const transactionFee = '0.1';
const { spawn, cleanup, path } = await prepareEnvironment();
console.info(`[Test Execution] Path: ${path}`);

try {
await test.step('Project generation', async () => {
await zkProject(projectName, 'none', true, spawn);
});
await test.step('Zeko Devnet deployment alias creation and validation', async () => {
const { exitCode, stdOut } = await zkConfig({
processHandler: spawn,
workDir: `${path}/${projectName}`,
networkId,
deploymentAlias,
feePayerAlias,
feePayerAccount,
feePayerMgmtType,
minaGraphQlEndpoint: null, // Will be auto-populated
transactionFee,
interruptProcess: false,
runFrom: `./${projectName}`,
waitForCompletion: true,
});

// Verify exit code
expect(exitCode).toBe(0);

// Verify config.json contains Zeko devnet alias with auto-populated URL
const configPath = `${path}/${projectName}/config.json`;
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
expect(config.deployAliases[deploymentAlias]).toBeDefined();
expect(config.deployAliases[deploymentAlias].url).toBe(
'https://devnet.zeko.io/graphql'
);
expect(config.deployAliases[deploymentAlias].networkId).toBe(
'zeko-devnet'
);

// Verify faucet message mentions Zeko
expect(stdOut.join('\n')).toContain('https://zeko.io/faucet/');
});
} finally {
cleanupFeePayerCacheByAlias(feePayerAlias);
await releaseAcquiredAccount(feePayerAccount);
await cleanup();
}
});

// TODO: https://github.com/o1-labs/zkapp-cli/issues/582
});
2 changes: 1 addition & 1 deletion tests/mocks/mocked-endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import http, {
} from 'node:http';
import { TestConstants, generateRandomInt } from '../utils/common-utils.js';

const applicationName = '⚡️[Mocked Endpoints Service]';
const applicationName = '[Mocked Endpoints Service]';
const host = '127.0.0.1';
const port = Number(
process.env.MOCKED_ENDPOINTS_SERVICE_PORT ??
Expand Down
8 changes: 4 additions & 4 deletions tests/models/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,12 @@ export type CommandOptions = {
export type ConfigOptions = {
processHandler: CLITestEnvironment['spawn'];
workDir: string;
networkId: NetworkId | undefined;
networkId: NetworkId | 'zeko-devnet' | undefined; // Network id comes from mina-signer hence missing zeko-devnet
deploymentAlias: string;
feePayerAlias: string;
feePayerAccount: Account;
feePayerMgmtType: string;
minaGraphQlEndpoint: string;
minaGraphQlEndpoint: string | null;
transactionFee: string;
interruptProcess: boolean;
runFrom: string | undefined;
Expand All @@ -161,12 +161,12 @@ export type CommandResults = {

export type ZkConfigCommandResults = {
workDir: string;
networkId: NetworkId | undefined;
networkId: NetworkId | 'zeko-devnet' | undefined;
deploymentAlias: string;
feePayerAlias: string;
feePayerAccount: Account;
feePayerMgmtType: string;
minaGraphQlEndpoint: string;
minaGraphQlEndpoint: string | null;
transactionFee: string;
stdOut: string[];
exitCode: ExitCode | null;
Expand Down
43 changes: 41 additions & 2 deletions tests/utils/config-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,24 @@ export async function zkConfig(
}
}

// Known networks (testnet, mainnet, zeko-devnet) auto-populate URLs
const knownNetworks = ['testnet', 'mainnet', 'zeko-devnet'];
const isKnownNetwork =
networkId && knownNetworks.includes(networkId as string);

// Default URLs for known networks
const defaultUrlMap: Record<string, string> = {
testnet: 'https://api.minascan.io/node/devnet/v1/graphql',
mainnet: 'https://api.minascan.io/node/mainnet/v1/graphql',
'zeko-devnet': 'https://devnet.zeko.io/graphql',
};

// Only skip URL prompt if it's a known network AND using default URL (or no URL provided)
const shouldSkipUrlPrompt =
isKnownNetwork &&
(!minaGraphQlEndpoint ||
minaGraphQlEndpoint === defaultUrlMap[networkId as string]);

const interactiveDialog = {
'Create a name (can be anything)': [deploymentAlias, 'enter'],
'Choose the target network': networkId
Expand All @@ -107,7 +125,15 @@ export async function zkConfig(
Constants.networkIds
)
: ['enter'],
'Set the Mina GraphQL API URL to deploy to': [minaGraphQlEndpoint, 'enter'],
// Only prompt for URL if it's a custom network or using a custom URL with a known network
...(shouldSkipUrlPrompt
? {}
: {
'Set the Mina GraphQL API URL to deploy to': [
minaGraphQlEndpoint,
'enter',
],
}),
'Set transaction fee to use when deploying (in MINA)': [
transactionFee,
'enter',
Expand Down Expand Up @@ -209,10 +235,23 @@ export function checkZkConfig(options: ZkConfigCommandResults): void {
}

cachedFeePayerAccountPath = `${Constants.feePayerCacheDir}/${sanitizedFeePayerAlias}.json`;

// Auto-populate URL for known networks if not provided
const defaultUrlMap: Record<string, string> = {
testnet: 'https://api.minascan.io/node/devnet/v1/graphql',
mainnet: 'https://api.minascan.io/node/mainnet/v1/graphql',
'zeko-devnet': 'https://devnet.zeko.io/graphql',
};

let expectedUrl = minaGraphQlEndpoint;
if (!expectedUrl) {
expectedUrl = defaultUrlMap[networkId as string] || minaGraphQlEndpoint;
}

expect(JSON.stringify(config)).toEqual(
JSON.stringify({
networkId,
url: minaGraphQlEndpoint,
url: expectedUrl,
keyPath: `keys/${sanitizedDeploymentAlias}.json`,
feepayerKeyPath: cachedFeePayerAccountPath,
feepayerAlias: sanitizedFeePayerAlias,
Expand Down
Loading