@five-vm/cli
Version:
High-performance CLI for Five VM development with WebAssembly integration
476 lines (445 loc) • 20.3 kB
JavaScript
/**
* Five CLI Template Command
*
* Generate starter Five DSL (.v) templates for common patterns
* like vaults, escrows, AMMs, fungible tokens, and NFTs.
*/
import { mkdir, writeFile, access, readFile } from 'fs/promises';
import { join, resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
import chalk from 'chalk';
import ora from 'ora';
export const templateCommand = {
name: 'template',
description: 'Generate Five DSL templates (vault, escrow, amm, token, nft, nft-globals). Note: nft-globals assumes metadata is set globally and only handles transfers.',
aliases: ['tmpl', 'scaffold'],
options: [
{
flags: '-t, --type <name>',
description: 'Template type',
choices: ['vault', 'escrow', 'amm', 'token', 'nft', 'nft-globals', 'multisig', 'vesting', 'auction-english', 'staking', 'airdrop-merkle', 'system-lamports', 'interface', 'spl-token'],
required: false,
},
{
flags: '--all',
description: 'Generate all templates',
defaultValue: false,
},
{
flags: '-o, --out-dir <dir>',
description: 'Output directory',
defaultValue: '.',
},
{
flags: '-f, --force',
description: 'Overwrite existing files',
defaultValue: false,
},
],
arguments: [
{
name: 'name',
description: 'Optional base filename (without extension) when generating single template',
required: false,
},
],
examples: [
{
command: 'five template --type vault',
description: 'Generate a vault.v template in current directory',
},
{
command: 'five template --type escrow -o examples',
description: 'Generate escrow.v under examples/',
},
{
command: 'five template --all -o templates',
description: 'Generate all templates into templates/',
},
{
command: 'five template --type token my_token',
description: 'Generate my_token.v for a single template',
},
{
command: 'five template --type nft-globals',
description: 'Generate nft-globals.v (transfer-only; metadata assumed pre-set)',
},
// Quickstart flows (Token)
{
command: '# Token: generate → compile → run locally',
description: '—',
},
{
command: 'five template --type token -o templates',
description: 'Create token.v in ./templates',
},
{
command: 'five compile templates/token.v -o build/token.bin',
description: 'Compile to bytecode (.bin)',
},
{
command: 'five execute build/token.bin --local',
description: 'Local WASM execution (use -f to pick a function index)',
},
// Quickstart flows (AMM)
{
command: '# AMM: generate → compile → run locally',
description: '—',
},
{
command: 'five template --type amm -o templates',
description: 'Create amm.v in ./templates',
},
{
command: 'five compile templates/amm.v -o build/amm.bin',
description: 'Compile AMM template',
},
{
command: 'five execute build/amm.bin --local',
description: 'Local WASM execution for AMM',
},
// Quickstart flows (NFT)
{
command: '# NFT: generate → compile → run locally',
description: '—',
},
{
command: 'five template --type nft -o templates',
description: 'Create nft.v in ./templates',
},
{
command: 'five compile templates/nft.v -o build/nft.bin',
description: 'Compile NFT template',
},
{
command: 'five execute build/nft.bin --local',
description: 'Local WASM execution for NFT',
},
// Deploy + on-chain execution (mainnet)
{
command: '# Deploy + execute on-chain (generic)',
description: '—',
},
{
command: 'five deploy build/token.bin --target mainnet',
description: 'Deploy compiled bytecode to mainnet',
},
{
command: 'five execute --script-account <ACCOUNT_ID> -f 0 --target mainnet',
description: 'Execute function 0 of deployed script (replace <ACCOUNT_ID>)',
},
],
handler: async (args, options, context) => {
const { logger } = context;
const outDir = resolve(options.outDir || options['out-dir'] || '.');
const baseNameArg = args[0];
const force = !!options.force;
const all = !!options.all;
const type = options.type;
if (!all && !type) {
logger.error('Please specify --type <vault|escrow|amm|token|nft|nft-globals> or use --all');
throw new Error('Template type not specified');
}
// Determine which templates to generate
const templates = all
? ['vault', 'escrow', 'amm', 'token', 'nft', 'nft-globals', 'multisig', 'vesting', 'auction-english', 'staking', 'airdrop-merkle', 'system-lamports', 'interface', 'spl-token']
: [type];
const spinner = ora(`Generating ${all ? 'all templates' : `${type} template`}...`).start();
try {
await mkdir(outDir, { recursive: true });
const results = [];
for (const t of templates) {
const filename = buildFilename(t, baseNameArg);
const filepath = join(outDir, filename);
const content = await getTemplateContent(t);
const created = await writeFileSafe(filepath, content, force);
results.push({ file: filepath, created });
}
spinner.succeed('Template generation complete');
for (const r of results) {
if (r.created) {
console.log(chalk.green(`✓ Created`), chalk.cyan(r.file));
}
else {
console.log(chalk.yellow(`• Skipped (exists)`), chalk.cyan(r.file));
}
}
console.log('\nNext steps:');
console.log(`- Edit generated .v files to fit your use case`);
console.log(`- Compile:`, chalk.cyan('five compile <file>.v'));
console.log(`- Execute locally:`, chalk.cyan('five execute <file>.five --local'));
}
catch (err) {
spinner.fail('Failed to generate templates');
logger.error(err.message);
throw err;
}
},
};
function buildFilename(type, base) {
if (base && base.trim().length > 0) {
return `${base.trim()}.v`;
}
return `${type}.v`;
}
async function writeFileSafe(path, content, force) {
try {
if (!force) {
await access(path);
// Exists and not forcing
return false;
}
}
catch {
// Does not exist, proceed
}
await writeFile(path, content);
return true;
}
async function getTemplateContent(name) {
// Prefer external template files for easier debugging and iteration
try {
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); // .../dist/commands at runtime
const candidatePaths = [
// When running from dist
resolve(__dirname, '../templates', `${name}.v`),
// When running from src via ts-node or tests
resolve(__dirname, '../../src/templates', `${name}.v`),
// When executed from repository root
resolve(process.cwd(), 'templates', `${name}.v`),
];
for (const p of candidatePaths) {
try {
const data = await readFile(p, 'utf8');
if (data && data.trim().length > 0)
return data;
}
catch {
// try next
}
}
}
catch {
// ignore and fall back to inline
}
// Fallback to inline templates if files not found
switch (name) {
case 'vault':
return TEMPLATE_VAULT;
case 'escrow':
return TEMPLATE_ESCROW;
case 'amm':
return TEMPLATE_AMM;
case 'token':
return TEMPLATE_TOKEN;
case 'nft':
return TEMPLATE_NFT;
case 'nft-globals':
// Minimal inline fallback; prefer file template
return `// NFT (globals) inline fallback\nmut collection_symbol: string;\nmut base_uri: string;\naccount NFT { token_id: pubkey; owner_key: pubkey; uri: string; }\nconfigure(symbol: string, base: string) { collection_symbol = symbol; base_uri = base; }\nmint_from_globals(state: NFT @mut, owner: pubkey) { state.token_id = owner; state.owner_key = owner; state.uri = base_uri; }\n`;
case 'multisig':
return `account MultisigState { threshold: u8; approvals: u64; last_proposal_id: u64; proposal_hash: u64; executed: bool; }\ninit_multisig(state: MultisigState , t: u8) { state.threshold = t; state.approvals = 0; state.last_proposal_id = 0; state.proposal_hash = 0; state.executed = false; }\nopen_proposal(state: MultisigState , h: u64) { state.last_proposal_id = state.last_proposal_id + 1; state.proposal_hash = h; state.approvals = 0; state.executed = false; }\napprove(state: MultisigState ) { state.approvals = state.approvals + 1; }\nexecute(state: MultisigState ) { require(!state.executed); require(state.approvals >= state.threshold); state.executed = true; }\n`;
case 'vesting':
return `account VestingState { beneficiary: pubkey; start_time: u64; cliff_seconds: u64; duration_seconds: u64; total_amount: u64; released_amount: u64; }\ninit_vesting(state: VestingState , b: pubkey, s: u64, c: u64, d: u64, t: u64) { state.beneficiary = b; state.start_time = s; state.cliff_seconds = c; state.duration_seconds = d; state.total_amount = t; state.released_amount = 0; }\nrelease(state: VestingState , amount: u64) -> u64 { require(amount > 0); state.released_amount = state.released_amount + amount; return amount; }\n`;
case 'auction-english':
return `account AuctionState { seller: pubkey; end_time: u64; min_increment: u64; highest_bid: u64; highest_bidder: pubkey; settled: bool; }\ninit_auction(state: AuctionState , s: pubkey, e: u64, m: u64, r: u64) { state.seller = s; state.end_time = e; state.min_increment = m; state.highest_bid = r; state.highest_bidder = s; state.settled = false; }\nbid(state: AuctionState , b: pubkey, a: u64) { let now = get_clock(); require(now < state.end_time); require(a >= state.highest_bid + state.min_increment); state.highest_bid = a; state.highest_bidder = b; }\nsettle(state: AuctionState ) { let now = get_clock(); require(now >= state.end_time); require(!state.settled); state.settled = true; }\n`;
case 'staking':
return `account Pool { reward_rate_per_slot: u64; last_update_slot: u64; acc_reward_per_share: u64; scale: u64; }\naccount StakeAccount { owner_key: pubkey; amount: u64; reward_debt: u64; }\ninit_pool(state: Pool , r: u64, sc: u64) { state.reward_rate_per_slot = r; state.last_update_slot = get_clock(); state.acc_reward_per_share = 0; state.scale = sc; }\naccrue(state: Pool , slots: u64) { state.acc_reward_per_share = state.acc_reward_per_share + (state.reward_rate_per_slot * slots); state.last_update_slot = state.last_update_slot + slots; }\ninit_staker(state: StakeAccount , o: pubkey) { state.owner_key = o; state.amount = 0; state.reward_debt = 0; }\nstake(state: StakeAccount , o: pubkey, a: u64, acc: u64) { require(state.owner_key == o); state.reward_debt = state.reward_debt + (a * acc); state.amount = state.amount + a; }\nunstake(state: StakeAccount , o: pubkey, a: u64, acc: u64) { require(state.owner_key == o); require(state.amount >= a); state.amount = state.amount - a; state.reward_debt = state.reward_debt - (a * acc); }\nclaimable(state: StakeAccount, acc: u64) -> u64 { let accrued = state.amount * acc; if (accrued <= state.reward_debt) { return 0; } return accrued - state.reward_debt; }\nrecord_claim(state: StakeAccount , c: u64) { state.reward_debt = state.reward_debt + c; }\n`;
case 'airdrop-merkle':
return `account AirdropConfig { merkle_root: u64; total_claimed: u64; }\naccount ClaimRecord { claimer: pubkey; amount: u64; claimed: bool; }\ninit_airdrop(state: AirdropConfig , r: u64) { state.merkle_root = r; state.total_claimed = 0; }\nclaim(state: ClaimRecord , c: pubkey, a: u64, expected: u64, cfg_root: u64) { require(expected == cfg_root); require(!state.claimed); state.claimer = c; state.amount = a; state.claimed = true; }\n`;
case 'system-lamports':
return `quote_transfer(from: account, to: account, amount: u64) -> (u64, u64) { require(amount > 0); require(from.lamports >= amount); let nf = from.lamports - amount; let nt = to.lamports + amount; return (nf, nt); }\ncheck_min_balance(acc: account, min: u64) -> bool { return acc.lamports >= min; }\ntopup_needed(acc: account, min: u64) -> u64 { if (acc.lamports >= min) { return 0; } return min - acc.lamports; }\n`;
case 'interface':
return `interface ExampleProgram { do_thing (arg: u64); }\ncall_example(target: account , value: u64) { ExampleProgram.do_thing(value); }\n`;
case 'spl-token':
return `interface SPLToken { initialize_mint (mint: pubkey, decimals: u8, authority: pubkey, freeze_authority: pubkey); mint_to (mint: pubkey, to: pubkey, authority: pubkey, amount: u64); }\ncreate_mint(payer: account , mint: account , decimals: u8) -> pubkey { SPLToken.initialize_mint(mint, decimals, payer, payer); return mint; }\nmint_tokens(mint: account , dest: account , amount: u64) { SPLToken.mint_to(mint, dest, mint, amount); }\n`;
}
}
// --- Templates ---
const COMMON_HEADER = `// Generated by five template
// Starter template in Five DSL. Adjust constraints and accounts
// to match your application and follow state layout best practices.
`;
const TEMPLATE_VAULT = `${COMMON_HEADER}
// Vault template: simple balance store gated by an authority signer
account VaultState {
balance: u64;
authorized_user: pubkey;
}
// Initialize vault state
init_vault(state: VaultState , authority: account ) {
state.balance = 0;
state.authorized_user = authority.key;
}
// Deposit increases the stored balance
deposit(state: VaultState , amount: u64) {
require(amount > 0);
state.balance = state.balance + amount;
}
// Withdraw requires signer to match authorized user
withdraw(state: VaultState , authority: account , amount: u64) {
require(state.authorized_user == authority.key);
require(state.balance >= amount);
state.balance = state.balance - amount;
}
`;
const TEMPLATE_ESCROW = `${COMMON_HEADER}
// Escrow template: maker locks funds for a designated taker
account EscrowState {
maker: pubkey;
taker: pubkey;
amount: u64;
is_funded: bool;
is_settled: bool;
}
init_escrow(state: EscrowState , maker: account , taker: pubkey, amount: u64) {
state.maker = maker.key;
state.taker = taker;
state.amount = amount;
state.is_funded = false;
state.is_settled = false;
}
fund_escrow(state: EscrowState , maker: account , amount: u64) {
require(state.maker == maker.key);
require(amount == state.amount);
state.is_funded = true;
}
claim_escrow(state: EscrowState , taker: account ) {
require(state.is_funded);
require(!state.is_settled);
require(state.taker == taker.key);
state.is_settled = true;
}
cancel_escrow(state: EscrowState , maker: account ) {
require(!state.is_settled);
require(state.maker == maker.key);
state.is_funded = false;
}
`;
const TEMPLATE_AMM = `${COMMON_HEADER}
// Constant-product AMM template (x*y=k) with simple fee
account Pool {
token_a: u64;
token_b: u64;
total_shares: u64;
fee_bps: u64;
}
init_pool(state: Pool , fee_bps: u64) {
state.token_a = 0;
state.token_b = 0;
state.total_shares = 0;
state.fee_bps = fee_bps;
}
add_liquidity(state: Pool , amount_a: u64, amount_b: u64) -> u64 {
// Simplified share calc for template
let shares = amount_a;
state.token_a = state.token_a + amount_a;
state.token_b = state.token_b + amount_b;
state.total_shares = state.total_shares + shares;
return shares;
}
swap(state: Pool , amount_in: u64, a_for_b: bool) -> u64 {
// Skeleton implementation for validator compatibility
let fee = (amount_in * state.fee_bps) / 10000;
let net_in = amount_in - fee;
// No state changes to avoid multi-account/multi-branch rules in validator
return net_in;
}
// Quote liquidity shares without mutating state
quote_add_liquidity(state: Pool, amount_a: u64, amount_b: u64) -> u64 {
if (amount_b < amount_a) {
return amount_b;
}
return amount_a;
}
// Remove liquidity (simplified)
remove_liquidity(state: Pool , share: u64) -> u64 {
require(state.total_shares >= share);
state.total_shares = state.total_shares - share;
return share;
}
`;
const TEMPLATE_TOKEN = `${COMMON_HEADER}
// Fungible token template with simple mint and transfer
account Mint {
authority: pubkey;
supply: u64;
decimals: u8;
}
account TokenAccount {
owner_key: pubkey;
bal: u64;
}
// Initialize mint state
init_mint(state: Mint , authority: pubkey, decimals: u8) {
state.authority = authority;
state.supply = 0;
state.decimals = decimals;
}
// Initialize a token account
init_account(state: TokenAccount , owner: pubkey) {
state.owner_key = owner;
state.bal = 0;
}
// Split flows to satisfy current validator constraints
mint_increase_supply(state: Mint , authority: pubkey, amount: u64) {
require(state.authority == authority);
state.supply = state.supply + amount;
}
credit_account(state: TokenAccount , amount: u64) {
state.bal = state.bal + amount;
}
debit_account(state: TokenAccount , signer: pubkey, amount: u64) {
require(state.owner_key == signer);
require(state.bal >= amount);
state.bal = state.bal - amount;
}
credit_after_debit(state: TokenAccount , amount: u64) {
state.bal = state.bal + amount;
}
// Burn reduces supply
burn_supply(state: Mint , authority: pubkey, amount: u64) {
require(state.authority == authority);
require(state.supply >= amount);
state.supply = state.supply - amount;
}
// Change mint authority
set_mint_authority(state: Mint , current: pubkey, new_auth: pubkey) {
require(state.authority == current);
state.authority = new_auth;
}
// Read-only helpers
get_supply(state: Mint) -> u64 { return state.supply; }
get_balance(state: TokenAccount) -> u64 { return state.bal; }
`;
const TEMPLATE_NFT = `${COMMON_HEADER}
// NFT template with simple mint and transfer
account NFT {
token_id: pubkey;
owner_key: pubkey;
uri: string;
}
// Initialize NFT fields
mint_nft(state: NFT , owner: pubkey, uri: string) {
// For template simplicity, assign token_id deterministically
state.token_id = owner;
state.owner_key = owner;
state.uri = uri;
}
// Transfer ownership
transfer_nft(state: NFT , from: pubkey, to: pubkey) {
require(state.owner_key == from);
state.owner_key = to;
}
// Update token metadata URI
set_uri(state: NFT , owner: pubkey, new_uri: string) {
require(state.owner_key == owner);
state.uri = new_uri;
}
// Read-only helpers
get_uri(state: NFT) -> string { return state.uri; }
get_owner(state: NFT) -> pubkey { return state.owner_key; }
`;
export default templateCommand;
//# sourceMappingURL=template.js.map