@latchprotocol/cli
v0.1.0
Published
The latch command: launch tokens on locked pools, create hosted Launchpad and DEX sites, airdrops, splits and locks, check a chain with latch doctor, and scaffold hooks and front ends. MIT; signs only with an encrypted keystore, never a raw key.
Downloads
157
Maintainers
Readme
@latchprotocol/cli — the latch command
Build on Latch from a terminal: launch a token on a locked pool, open a hosted
Launchpad or DEX site, run airdrops, multisends, Team splits and token locks, check a chain with
latch doctor, and scaffold a front end or a hook. MIT.
npm install -g @latchprotocol/cli
latch doctor --chain robinhood # RPCs, contract clock, address book, USD source
latch launch --chain sepolia # step-by-step launch, priced by its opening market cap
latch site create --chain sepolia # a hosted Launchpad, DEX, or both
latch init my-dex --chain sepolia # Latch app template (runs @latchprotocol/create-app)| command | what it does |
| ------- | ------------ |
| latch init [dir] | scaffold the Latch app template: DEX, Launchpad or both (wraps @latchprotocol/create-app) |
| latch launch | a Kit v2 launch: opening market cap, protection, creator tax, listing, review, sign |
| latch site create \| edit \| show | a hosted Launchpad or DEX site: terms, earnings, brand |
| latch doctor | per-endpoint RPC health and contract clock, address book, launch-kit wiring, a wallet, terms drift |
| latch airdrop build \| create \| verify \| claim | CSV to a Merkle drop; verify asks the drop contract itself to judge the proofs |
| latch multisend | CSV to batched sends |
| latch split | a Team split with fixed shares |
| latch lock | a time lock or cliff-and-linear vesting |
| latch quote, latch swap | coming soon |
| latch new \| devnet \| bitmap | hook tooling (below) |
The stepper
Run latch launch or latch site create in a terminal and it asks one step at a time:
Step 4 of 7 · Opening market cap. Type < in any text step, or pick Back in a list, to return
to the previous step; what you entered stays as the default. Every value is checked as you go with the
SDK's own validators, and nothing is offered for signing until the review screen has shown:
- the snapped opening price per pool (the price the pool is actually born at) and the market cap it implies;
- each fee on its own line — protocol launch fee, Launchpad launch fee, the value sent, creator-tax rates, expiry and split, locked-LP fee split;
- what is permanent;
and the exact transaction has succeeded as an eth_call. Irreversible actions ask you to type the
token symbol or site name.
Market cap first. The opening price comes from the market cap you choose. A dollar default ($3,500) is offered only when a live native/USD price was read (the source, block and age are shown under it). Where there is none, you choose the cap in the quote currency and no dollar figure appears.
Signing
latch never accepts a raw private key or a mnemonic — --private-key, --mnemonic and friends
are refused, and so is any argument shaped like one.
--print(default): the unsigned transaction (to,data,value, chain) and an equivalentcast sendline. Add--from <address>: it decides predicted addresses. Nothing is signed.--keystore <file>: an encrypted keystore (cast wallet new/cast wallet import, geth, ethers). The password is asked with a hidden prompt, or read from--password-filefor scripts.
Scripts and bots
Every prompt has a flag. --yes never prompts (and replaces typed confirmation); --json prints
exactly one JSON document on stdout, with progress on stderr:
{ "schema": "latch.cli/1", "command": "launch", "ok": true, "chainId": 11155111, "mode": "print",
"tx": { "chainId": 11155111, "from": "0x…", "to": "0x…", "data": "0x…", "value": "1500000000000000" },
"simulation": { "ok": true },
"result": { "token": "0x…", "openingPrice": { "snapped": "…" }, "fees": { … }, "hash": null } }ok means the command ran; a command's own verdict lives in result (e.g. result.healthy for
doctor), and the exit code is non-zero when that verdict is negative. On an error:
{ "schema": "latch.cli/1", "command": "…", "ok": false, "error": { "message": "…", "hint": "…" } }.
Every amount is a decimal string of base units. Fields are only ever added within latch.cli/1.
Chains, RPCs and your own addresses
Addresses come from @latchprotocol/sdk's address book. A chain Latch has not reached yet says
"Coming soon on ".
--rpc <url>orLATCH_RPC_<chainId>: your node. When given, it is used alone — never mixed with public endpoints, so a failing local node can never hand a signed transaction to another network.--deployment <file.json>orLATCH_DEPLOYMENT_OVERRIDE_<chainId>: a partial address book merged over the SDK's (same shape as the web app's override;deployedAtBlockis a string). Every screen says when it is in use.
Hook tooling
npx @latchprotocol/create-hook <name> (or latch new) generates a hook project; latch devnet runs a local
node with the protocol on it; latch bitmap explains permission bitmaps.
Why this exists
On Uniswap v4 a hook's permissions are encoded in the address of the hook contract, so shipping one means grinding a CREATE2 salt until the deployed address happens to carry the right low bits - and changing your mind about a callback means grinding again.
Latch keeps permissions in the pool key instead: the low 16 bits of
poolKey.parameters hold a registration bitmap, and CLPoolManager.initialize
cross-checks it against the hook's own getHooksRegistrationBitmap(). A hook
works from any address. There is no salt to mine.
What replaces salt mining as the first hurdle is keeping those two values in
step. They live in different files, in different languages, and when they drift
initialize reverts with HookConfigValidationError - the single most common
way a first hook fails. This CLI generates both from one permission set, so
there is no second source for them to drift from, and the generated test fails
the build if they ever stop agreeing.
Quickstart
You need Foundry (forge, anvil) and Node 20+.
# 1. install the CLI (from a checkout, until it is published)
cd packages/cli
npm install
npm run build
npm link # puts `latch` and `create-latch-hook` on PATH# 2. scaffold a hook
create-latch-hook my-hook
cd my-hook
forge test -vvThat is the loop. The first forge test compiles the protocol and takes about a
minute; after that it is seconds.
# 3. run a local node with the protocol on it, in another terminal
latch devnet# 4. deploy the hook you just generated onto it
# (bash/zsh - the devnet prints the PowerShell form too)
# NOTE: this is anvil's well-known deterministic account #0. It is public, holds no
# real funds, and exists only for local development. NEVER export a real
# private key this way - shell history and terminal transcripts persist it.
export PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
export CL_POOL_MANAGER=<address printed by latch devnet>
forge script script/DeployMyHook.s.sol:DeployMyHook --rpc-url http://127.0.0.1:8545 --broadcastThe deploy script reads the bitmap back off the deployed contract and prints the
exact parameters word your pool key needs, so the value you copy can never be
stale.
Without installing
node packages/cli/dist/bin/create-latch-hook.js my-hook
node packages/cli/dist/bin/latch.js devnetFinding the contracts
Until Latch is published, generated projects and the devnet compile against a checkout on disk. It is resolved in this order:
--core <path-to-packages/core>LATCH_CORE_PATHin the environment- a walk up from the output directory (and then the current directory) looking
for
packages/core
Generated foundry.toml files get a relative path when the project sits near
the checkout, so the project can be committed and moved with it, and an absolute
one only when no sensible relative path exists.
@latchprotocol/create-hook / latch new
npx @latchprotocol/create-hook <name> [options] # or: npm create @latchprotocol/hook <name> -- [options]Interactive by default; --yes makes it non-interactive, and every prompt has a
flag, so it runs in CI unattended.
| flag | meaning |
| ---- | ------- |
| -t, --template <id> | hooks: noop, dynamic-fee, swap-counter; apps: airdrop, locks (see below) |
| -p, --permissions <list> | comma-separated callbacks, or all / none |
| --tick-spacing <n> | tick spacing packed into parameters (default 60) |
| --fee <n\|dynamic> | static LP fee in hundredths of a bip, or a dynamic-fee pool |
| --contract <Name> | Solidity contract name (default: derived from <name>) |
| -d, --dir <path> | output directory (default ./<name>) |
| --core <path> | where packages/core lives |
| -y, --yes | accept defaults, never prompt |
| --force | write into a non-empty directory |
| --build | run forge build on the result before reporting success |
Templates
| id | what you get |
| -- | ------------ |
| noop | the smallest hook that builds, deploys and backs a live pool - one beforeSwap that does nothing |
| dynamic-fee | beforeSwap returns a per-swap LP fee, higher above a size threshold, on a dynamic-fee pool |
| swap-counter | afterSwap keeps an on-chain swap count per PoolId |
Templates are starting points, not permission sets. Pick any callbacks you like
with --permissions: the generator emits an override for each one, using the
template's body where it has one and a pass-through otherwise. Registering a
callback you have not implemented is not a silent no-op on Latch -
BaseCLHook's default override points revert HookNotImplemented() - so the
generator never emits a bitmap wider than the code behind it.
A *ReturnsDelta permission cannot stand alone; the base callback it needs is
added for you, with a warning, rather than generating a project that reverts at
pool initialization.
App templates: airdrop and locks
Not every Latch integration is a hook. Two templates generate a Node project
on the MIT @latchprotocol/sdk instead of a Foundry one:
latch new my-drop --template airdrop
latch new my-locks --template locks| id | scripts | contracts |
| -- | ------- | --------- |
| airdrop | multisend (CSV -> atomic batches of up to 1,000), drop (CSV -> Merkle tree -> claims.json -> a funded drop), holders (a token's holders at a snapshot block -> a checked recipients CSV, pro-rata / equal / fixed), check-claims (verify a claims file against a drop's stored root) | LatchMultisend, LatchDropFactory |
| locks | lock-position (time-lock a CL position NFT), vest (token time-lock or cliff + linear vesting), status (locked share of a pool's active liquidity, or of a token's supply) | LatchPositionLock, LatchTokenLock |
Every generated app follows the same rules, and test/app.test.ts checks them:
- MIT dependencies only:
@latchprotocol/sdk,viem, andtsx/@types/nodefor development. Nothing GPL, so the project is yours to license. - Dry run by default. Without
--executea script reads, validates, prints the plan and simulates every transaction it can (asPRIVATE_KEY's address, orFROM). A step that needs an earlier one mined first (a transfer after its approval) is reported as such, not faked. - Simulate, then send. With
--executeeach transaction's exact bytes are simulated, sent, and its receipt checked before the next; the first failure stops the plan, and the revert is named from the contract's ABI. - Keys only from the environment.
PRIVATE_KEY(or.env); a key passed as a flag is refused. - Exact approvals, never unlimited, skipped when the allowance already covers it.
- Addresses from the SDK's address book;
nullthere means not deployed, and the script stops and says so.LATCH_MULTISEND,LATCH_DROP_FACTORY,LATCH_POSITION_LOCKandLATCH_TOKEN_LOCKoverride them for a local devnet.
Every action pays the utility's flat native fee, charged by the contract
however it is called; the scripts read it live. The hook-only flags
(--permissions, --tick-spacing, --fee, --contract, --core, --build)
are refused for an app template rather than silently ignored.
Inside a Latch checkout the SDK is linked with file: (it is not on npm yet):
build it once with npm install && npm run build in packages/sdk, then
npm install in the generated project. The script sources live as real files
under assets/apps/ so the test suite compiles them against the SDK source.
What gets generated
my-hook/
src/MyHook.sol extends BaseCLHook; getHooksRegistrationBitmap() is generated
test/MyHook.t.sol Vault + CLPoolManager + router + two mock ERC20s, end to end
script/DeployMyHook.s.sol deploys, then prints the parameters word for your pool key
foundry.toml remappings and both build profiles
latch.json machine-readable record of the permission set
README.md quickstart, the invariant, and a security checklist
.gitignoreThe generated test suite covers, for every template:
test_registrationBitmapMatchesPoolKey- the hook's bitmap, the pool key's bitmap and the generated constant are one number.test_mismatchedBitmapRevertsAtInitialize- deliberately reproducesHookConfigValidationError, so the error you are most likely to hit is one you have already seen pass by.test_hookWorksFromAnArbitraryAddress-vm.etches the hook at0xDEAD, initializes a pool with it and swaps. This is the part v4 cannot do.- liquidity and swaps in both directions, plus whatever the template adds.
latch devnet
latch devnet [options] [-- <extra anvil args>]Starts anvil and deploys:
- Vault and CLPoolManager / BinPoolManager, both registered as Vault apps
- LatchProtocolFeeController from
packages/fees, wired into both managers - a Create3Factory, so addresses are deterministic across restarts
- two freely mintable mock ERC20s (
mintis public - fund yourself) - CLPoolManagerRouter, the core test router, so pools are usable without writing a lock callback by hand
- one initialized, seeded, hookless CL pool
Addresses are printed as a table and also written to
<workdir>/deployments/devnet.json.
| flag | meaning |
| ---- | ------- |
| -p, --port <n> | anvil port (default 8545) |
| --host <addr> | bind address (default 127.0.0.1) |
| -f, --fork <rpc-url> | fork a live chain (anvil's --fork-url) |
| --fork-block-number <n> | pin the fork height |
| --rpc-url <url> | deploy to a node that is already running instead of starting anvil |
| --deployer-key <hex> | deploying key (default: anvil account 0) |
| --workdir <path> | where the devnet project and its build cache live (default ~/.latch/devnet) |
| -q, --quiet | do not stream anvil's log |
| --json | deploy, print the addresses as JSON, shut down |
Anything after -- goes to anvil untouched: latch devnet -- --block-time 2.
The deployment is a Foundry script that reuses packages/core/script's CREATE3
and BackendGuard machinery rather than a second, divergent deployment path. The
first run compiles the protocol with via_ir and takes a couple of minutes; the
build cache lives in the working directory, so later runs start in seconds.
anvil runs a Cancun-capable EVM whatever it forks, so the devnet always uses the
default (EIP-1153 / transient storage) build. The legacy SSTORE backend is
for pre-Cancun chains only.
latch bitmap
The permission maths on its own - useful when a pool refuses to initialize.
$ latch bitmap beforeSwap,beforeSwapReturnsDelta
bitmap 0x0440
decimal 1088
binary 0000010001000000
callbacks beforeSwap, beforeSwapReturnsDelta
tick spacing 60
pool key parameters 0x00000000000000000000000000000000000000000000000000000000003c0440
In the hook:
function getHooksRegistrationBitmap() public pure override returns (uint16) {
return BEFORE_SWAP | BEFORE_SWAP_RETURNS_DELTA;
}latch bitmap --decode 0x0080 # which callbacks is that?
latch bitmap --parameters 0x...003c0080 # unpack a whole parameters word
latch bitmap --list # every permission and its bitBuild profiles
Every generated project carries the same two profiles as the rest of the monorepo, differing only in which transient-storage backend the settlement layer compiles against:
forge test # default: EIP-1153 (tstore/tload), evm_version = cancun
FOUNDRY_PROFILE=legacy forge test # legacy: SSTORE/SLOAD, evm_version = shanghaiThe hp-transient/ remapping is pinned per profile in foundry.toml, and
generated projects deliberately have no remappings.txt: that file silently
overrides per-profile remappings and would build the wrong backend under
--profile legacy.
How this package stays honest
The permission tables here are a copy of numbers that really live in Solidity.
test/parity.test.ts reads the Solidity and fails if they drift:
- bit offsets against
packages/core/src/pool-cl/interfaces/ICLHooks.sol - constant names and shifts against
packages/hooks/src/base/BaseCLHook.sol - bitmap and tick-spacing offsets against
ParametersHelper.solandCLPoolParametersHelper.sol
Encoding, decoding and validation themselves are reused from
@latchprotocol/sdk rather than re-derived.
npm run typecheck # tsc --noEmit, strict
npm test # vitest
npm run build # tsc + copy the Foundry assets into dist/Known limitations
- Templates target concentrated-liquidity (
BaseCLHook) pools. Bin-pool scaffolding is not generated yet, thoughlatch devnetdoes deployBinPoolManager. - Latch is not on npm yet, so generated projects reference a checkout on disk
rather than a
forge installdependency. latch devnetassumes anvil's deterministic accounts. If you pass a custom--mnemonicthrough--, pass the matching--deployer-keytoo.- The devnet script constructs
LatchProtocolFeeControllerdirectly, so a change to that contract's constructor inpackages/feesbreakslatch devnetuntilassets/devnet/script/LatchDevnet.s.solis updated to match. The failure is a compile error naming the constructor, not a silent misdeployment. - The devnet refuses to start when something is already serving JSON-RPC on the
chosen port, rather than quietly deploying onto a stranger's node. Use
--port, or--rpc-urlif attaching was the intent.
Other Latch packages
@latchprotocol/sdk— TypeScript SDK: pool keys and ids, the hook permission bitmap, typed events, the address book, market reads and the Launchpad builders@latchprotocol/connect— the wallet layer: a Latch-themed wrapper over RainbowKit and wagmi, with the Latch chain list@latchprotocol/widgets— embeddable swap, liquidity and launch widgets with integrator fee attribution@latchprotocol/create-app— the Latch app template: a DEX, a Launchpad or both, on the shared core@latchprotocol/create-hook— the hook starter:npx @latchprotocol/create-hook my-latch --template dynamic-fee
Website: latches.fun · X: @Latchesdotfun
Licence
MIT. Generated hooks are yours; they import BaseCLHook (GPL-2.0-or-later) from
packages/hooks, and building against the MIT-licensed SDK carries no obligation
from the core contracts.
