A Hardhat-like development framework for Movement L1 and Aptos Move smart contracts
Write your tests and deployment scripts in TypeScript while building Move smart contracts.
- Local Node Testing - Run a real Movement blockchain locally for testing (just like Hardhat!)
- Dual Testing Modes - Choose between
local-node(full blockchain) orfork(read-only snapshot) - Auto-Deploy in Tests - Contracts deploy automatically when tests run - zero manual setup
- setupTestFixture Helper - High-level API for test setup with type-safe contracts (no
!operator needed) - Zero Setup Testing - Auto-generated test accounts funded from local faucet
- Auto-detection of Named Addresses - Automatically detects and configures addresses from your Move code (like Hardhat)
- Native Fork System - Create local snapshots of Movement L1 with actual network state (JSON-based, no BCS issues)
- TypeScript-first - Write tests and deployment scripts in TypeScript
- Hardhat-style accounts - Single
PRIVATE_KEYworks across all networks - Multi-network support - Configure multiple networks (testnet, mainnet, local)
- Hardhat-like workflow - Familiar commands and project structure
- Movehat Runtime Environment - Global context object similar to Hardhat's HRE
- Movement CLI integration - Wraps Movement CLI for compilation and publishing
- Deployment tracking - Automatic per-network deployment tracking (like hardhat-deploy)
- Security-focused - Built-in protection against path traversal, command injection, and YAML injection
Before installing Movehat, make sure you have:
-
Node.js (v18 or later) - Download
-
Movement CLI - REQUIRED for compiling and deploying Move contracts
Install Movement CLI by following the official guide: Movement CLI Installation Guide
Verify installation:
movement --version
IMPORTANT: Without Movement CLI installed, compilation will fail with:
Compilation failed: Command failed: movement move build /bin/sh: movement: command not found
npm install -g movehat
# or
pnpm install -g movehatGet started with Movehat in 4 commands:
# 1. Create project
npx movehat init my-project && cd my-project
# 2. Install dependencies
npm install
# 3. Compile contracts (auto-detects addresses)
npx movehat compile
# 4. Run tests (runs on real local blockchain)
npm testThat's it! No manual setup needed - local blockchain starts automatically.
What just happened?
- Tests run on a real local Movement node (full blockchain, just like Hardhat!)
- Automatically starts blockchain, funds accounts, deploys contracts
- Zero manual setup - perfect for learning and rapid development
Ready to deploy? Set PRIVATE_KEY in .env and run deployment scripts. See detailed guide below.
Want more details? Check out the complete Quick Start guide.
mkdir my-move-project
cd my-move-project
movehat initThis creates the following structure:
my-move-project/
├── move/ # Move smart contracts
│ ├── Move.toml
│ └── sources/
│ └── Counter.move
├── scripts/ # Deployment scripts (TypeScript)
│ └── deploy-counter.ts
├── tests/ # Test files (TypeScript)
│ └── Counter.test.ts
├── movehat.config.ts # Movehat configuration
├── .env.example
├── package.json
└── tsconfig.json
For testing only? Skip this step! Tests use simulation with auto-generated accounts.
For real deployment? Configure your private key:
cp .env.example .envEdit .env:
# Your private key (works on all networks - Hardhat-style)
PRIVATE_KEY=0x1234567890abcdef...
# Optional: Override RPC URL
MOVEMENT_RPC_URL=https://custom-testnet.movementnetwork.xyz/v1Note: Like Hardhat, Movehat uses a single PRIVATE_KEY that works across all networks (testnet, mainnet, local). This simplifies configuration and matches real-world usage.
Security: The PRIVATE_KEY is only required for:
- Real deployments to testnet/mainnet
- Running deployment scripts
- Tests automatically use safe, auto-generated test accounts (no configuration needed)
npm install
# or
pnpm installmovehat compileHow it works:
- Movehat automatically detects named addresses from your Move files (e.g.,
module counter::counter→ detectscounter) - No manual address configuration needed for development
- Uses temporary dev addresses (
0xcafe) for compilation - Just like Hardhat - add any new contract and it compiles automatically
# Deploy to testnet (default)
movehat run scripts/deploy-counter.ts
# Deploy to specific network
movehat run scripts/deploy-counter.ts --network mainnet
movehat run scripts/deploy-counter.ts --network localnpm test
# or
pnpm testHow it works:
- When you run
npm test(ormovehat test), you'll see an interactive menu:
? What tests do you want to run?
❯ Move unit tests (fast, no node required)
TypeScript integration tests (starts local node)
All tests (Move + TypeScript)
- Move unit tests: Fast tests written in Move with
#[test]annotations - TypeScript integration tests: Tests that run on a real local Movement blockchain (like Hardhat)
- Use flags for CI/scripts:
--move,--ts, or--all
MoveHat includes a native fork system for creating local snapshots of Movement L1 network state. This allows you to test against real network data without deploying to testnet.
Create a fork:
movehat fork create --network testnet --name my-forkThis creates a snapshot at the current ledger version with real network state (Chain ID: 250, actual balances, deployed contracts, etc.)
View resources from the fork:
movehat fork view-resource \
--fork .movehat/forks/my-fork \
--account 0x1 \
--resource "0x1::coin::CoinInfo<0x1::aptos_coin::AptosCoin>"Fund accounts for testing:
movehat fork fund \
--fork .movehat/forks/my-fork \
--account 0x123 \
--amount 5000000000List all available forks:
movehat fork listServe fork via RPC:
movehat fork serve --fork .movehat/forks/my-fork --port 8080This starts a local RPC server that emulates a Movement L1 node using your fork's data. You can connect the Aptos/Movement SDK to http://localhost:8080/v1 to interact with the fork state.
Use in tests (programmatic API):
import { ForkManager } from 'movehat';
describe('Token Tests', () => {
let fork: ForkManager;
before(async () => {
fork = new ForkManager('.movehat/forks/test');
await fork.initialize('https://testnet.movementnetwork.xyz/v1', 'testnet');
});
it('should read real network data', async () => {
const coinInfo = await fork.getResource(
'0x1',
'0x1::coin::CoinInfo<0x1::aptos_coin::AptosCoin>'
);
expect(coinInfo.name).to.equal('Move Coin');
});
it('should modify fork state for testing', async () => {
await fork.fundAccount('0x123', 5_000_000_000);
const coinStore = await fork.getResource(
'0x123',
'0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>'
);
expect(coinStore.coin.value).to.equal('5000000000');
});
});Why use forks?
- Test against real network state with actual balances and deployed contracts
- No need to deploy contracts to testnet for every test
- Modify state locally without affecting the network
- Lazy loading - only fetches resources you actually access
- JSON-based storage - human-readable and easy to inspect
- Works natively with Movement L1 (no BCS compatibility issues)
See FORK_GUIDE.md for complete fork system documentation including architecture details and advanced usage.
Edit movehat.config.ts to configure your networks:
import dotenv from "dotenv";
dotenv.config();
export default {
// Default network to use when no --network flag is provided
defaultNetwork: "testnet",
// Network configurations
networks: {
testnet: {
url: process.env.MOVEMENT_RPC_URL || "https://testnet.movementnetwork.xyz/v1",
chainId: "testnet",
},
mainnet: {
url: "https://mainnet.movementnetwork.xyz/v1",
chainId: "mainnet",
},
local: {
url: "http://localhost:8080/v1",
chainId: "local",
},
},
// Global accounts configuration (Hardhat-style)
// Uses PRIVATE_KEY from .env by default
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
// Move source directory
moveDir: "./move",
// Named addresses (optional - auto-detected from Move files)
// Only specify if you need specific addresses for production deployment
namedAddresses: {
// Example: counter: "0x1234...",
// If not specified, Movehat auto-detects from your .move files
},
};Key differences from other frameworks:
- One account for all networks - Just like Hardhat, your
PRIVATE_KEYworks across testnet, mainnet, and local - Simpler configuration - Networks only need to define their RPC URL
- Flexible - You can still specify different accounts per network if needed
Deployment scripts use the Movehat Runtime Environment (MRE):
// scripts/deploy-counter.ts
import { getMovehat } from "movehat";
async function main() {
const mh = await getMovehat();
console.log("Deploying from:", mh.account.accountAddress.toString());
console.log("Network:", mh.config.network);
// Deploy (publish) the module
// Movehat automatically checks if already deployed
const deployment = await mh.deployContract("counter");
console.log("Module deployed at:", deployment.address);
console.log("Transaction:", deployment.txHash);
// Get contract instance
const contract = mh.getContract(deployment.address, "counter");
// Initialize the counter
await contract.call(mh.account, "init", []);
console.log("Counter initialized!");
}
main().catch((error) => {
console.error(error);
process.exit(1);
});Movehat automatically tracks deployments per network, similar to hardhat-deploy:
my-project/
└── deployments/
├── testnet/
│ ├── counter.json
│ └── token.json
├── mainnet/
│ └── counter.json
└── local/
└── counter.json
Each deployment file contains:
{
"address": "0x662a2aa90fdf2b8e400640a49fc922b713fe4baaec8c37b088ecef315561e4d9",
"moduleName": "counter",
"network": "testnet",
"deployer": "0x662a2aa90fdf2b8e400640a49fc922b713fe4baaec8c37b088ecef315561e4d9",
"timestamp": 1704985623564,
"txHash": "0x59cb0c2df832064174b50fc69909af5819c6e273cc644f9a2123102b20bb0ef2"
}When you run a deployment script, Movehat automatically checks if the module is already deployed:
First time:
movehat run scripts/deploy-counter.ts --network testnet
# Deploys successfullySecond time (already deployed):
movehat run scripts/deploy-counter.ts --network testnet
# Error: Module "counter" is already deployed on testnet
# Address: 0x662a...
# Deployed at: 12/5/2025, 11:38:14 PM
# Transaction: 0x59cb0c2df832...
#
# To redeploy, run with the --redeploy flag:
# movehat run <script> --network testnet --redeployForce redeploy:
movehat run scripts/deploy-counter.ts --network testnet --redeploy
# Redeploys and updates deployment infoconst mh = await getMovehat();
// Core
mh.config // Resolved configuration
mh.network // Network info (name, chainId, rpc)
mh.aptos // Aptos SDK client
mh.account // Primary account
mh.accounts // All configured accounts
// Contract helpers
mh.getContract // Get contract helper
// Deployment functions
mh.deployContract // Deploy and track module
mh.getDeployment // Get deployment info for a module
mh.getDeployments // Get all deployments for current network
mh.getDeploymentAddress // Get deployed address for a module
// Network management
mh.switchNetwork // Switch to different network// movehat.config.ts - Configure multiple accounts
export default {
accounts: [
process.env.PRIVATE_KEY, // Primary (mh.account)
process.env.SECONDARY_KEY, // mh.accounts[1]
].filter(Boolean),
};
// In your script - Access accounts
const mh = await getMovehat();
const primaryAccount = mh.account; // accounts[0]
const secondaryAccount = mh.getAccountByIndex(1); // accounts[1]Movehat supports two types of tests for comprehensive coverage:
Write tests directly in your Move files using #[test] annotations. Perfect for testing internal logic and business rules.
Example from move/sources/Counter.move:
#[test(account = @0x1)]
public fun test_increment(account: &signer) acquires Counter {
let addr = signer::address_of(account);
aptos_framework::account::create_account_for_test(addr);
init(account);
assert!(get(addr) == 0, 0);
increment(account);
assert!(get(addr) == 1, 1);
increment(account);
assert!(get(addr) == 2, 2);
}When to use Move tests:
- Testing internal logic and calculations
- Validating business rules and invariants
- Testing edge cases in pure functions
- TDD during Move development (ultra-fast feedback)
Run Move tests:
movehat test --move
# or
movehat test:moveWrite tests in TypeScript that run on a real local Movement blockchain (just like Hardhat!). Perfect for end-to-end testing with real transactions:
// tests/Counter.test.ts
import { describe, it, before, after } from "mocha";
import { expect } from "chai";
import { setupTestFixture, teardownTestFixture, type TestFixture } from "movehat/helpers";
describe("Counter Contract", () => {
let fixture: TestFixture<'counter'>;
before(async function () {
this.timeout(60000); // Allow time for local node startup + deployment
// Setup local testing environment with auto-deployment
// This will:
// 1. Start a local Movement blockchain node
// 2. Generate and fund test accounts from local faucet
// 3. Auto-deploy the counter module
// 4. Return everything ready to use
//
// By default uses 'local-node' mode (full blockchain)
// For faster tests on existing state, pass { mode: 'fork' }
fixture = await setupTestFixture(['counter'] as const, ['alice', 'bob']);
console.log(`\n✅ Testing on local blockchain`);
console.log(` Deployer: ${fixture.accounts.deployer.accountAddress.toString()}`);
console.log(` Alice: ${fixture.accounts.alice.accountAddress.toString()}`);
console.log(` Bob: ${fixture.accounts.bob.accountAddress.toString()}\n`);
});
it("should initialize with value 0", async () => {
const counter = fixture.contracts.counter; // Type-safe, no `!` needed
const deployer = fixture.accounts.deployer;
// Read counter value (returns string from view function)
const value = await counter.view<string>("get", [
deployer.accountAddress.toString()
]);
console.log(` Counter value: ${value}`);
// Assert the counter is 0 (note: values from view are strings)
expect(parseInt(value)).to.equal(0);
});
it("should increment counter", async () => {
const counter = fixture.contracts.counter;
const deployer = fixture.accounts.deployer;
// Increment the counter (real transaction on local blockchain!)
const tx = await counter.call(deployer, "increment", []);
console.log(` Transaction: ${tx.hash}`);
// Read new value
const value = await counter.view<string>("get", [
deployer.accountAddress.toString()
]);
console.log(` New counter value: ${value}`);
// Should be 1 now
expect(parseInt(value)).to.equal(1);
});
it("alice can also increment counter", async () => {
const counter = fixture.contracts.counter;
const alice = fixture.accounts.alice;
// Alice increments her own counter
const tx = await counter.call(alice, "increment", []);
console.log(` Alice's transaction: ${tx.hash}`);
// Read counter value for Alice (each user has their own counter)
const aliceValue = await counter.view<string>("get", [
alice.accountAddress.toString()
]);
console.log(` Alice's counter value: ${aliceValue}`);
expect(parseInt(aliceValue)).to.equal(1);
// Deployer's counter should still be 1 (unchanged)
const deployerValue = await counter.view<string>("get", [
fixture.accounts.deployer.accountAddress.toString()
]);
console.log(` Deployer's counter value: ${deployerValue}`);
expect(parseInt(deployerValue)).to.equal(1);
});
after(async () => {
// Cleanup: Stop local node and clear account pool
await teardownTestFixture();
});
});When to use TypeScript tests:
- Testing real transaction flows with actual state changes
- Validating multi-account interactions
- Testing contract upgrades and migrations
- End-to-end integration testing
- CI/CD pipelines
Run TypeScript tests:
movehat test --ts
# or
movehat test:ts
# Watch mode for development
movehat test --watchRun both Move and TypeScript tests together:
movehat test --all
# or
movehat test # Then select "All tests" from the menuOutput:
Running all tests...
============================================================
1. Move Unit Tests
------------------------------------------------------------
Running Move unit tests for package Counter
[ PASS ] 0xcafe::counter::test_increment
Test result: OK. Total tests: 1; passed: 1; failed: 0
✓ Move tests passed
============================================================
2. TypeScript Integration Tests
------------------------------------------------------------
Counter Contract
Counter functionality
[TESTNET] Using auto-generated test account (safe for testing only)
[TESTNET] For mainnet, set PRIVATE_KEY in .env
Testing on testnet
Account: 0x1234...
✓ should initialize counter using simulation
✓ should increment counter using simulation
2 passing (3s)
✓ TypeScript tests passed
============================================================
✓ All tests passed!
Benefits of dual testing:
- Move tests catch logic errors early (milliseconds)
- TypeScript tests verify integration works (seconds)
- Comprehensive coverage from both perspectives
- Fast feedback loop for development
Initialize a new Movehat project.
movehat init my-project
movehat init # Uses current directoryCompile Move smart contracts using Movement CLI.
Execute a TypeScript/JavaScript script with the Movehat Runtime.
movehat run scripts/deploy-counter.ts --network testnet
movehat run scripts/deploy-counter.ts --network testnet --redeploy # Force redeployRun tests with an interactive menu or flags. If no flags are provided, shows an interactive menu to choose test type.
movehat test # Interactive menu to choose test type
movehat test --move # Run only Move unit tests (fast, no node required)
movehat test --ts # Run only TypeScript integration tests (starts local node)
movehat test --all # Run all tests (Move + TypeScript)
movehat test --watch # Run TypeScript tests in watch mode (implies --ts)
movehat test --filter pattern # Filter Move tests by patternInteractive Menu:
? What tests do you want to run?
❯ Move unit tests (fast, no node required)
TypeScript integration tests (starts local node)
All tests (Move + TypeScript)
Run only Move unit tests.
movehat test:move # Run all Move tests
movehat test:move --filter test_inc # Run only tests matching pattern
movehat test:move --ignore-warnings # Ignore compilation warningsRun only TypeScript integration tests.
movehat test:ts # Run TypeScript tests
movehat test:ts --watch # Run in watch modeCheck for updates and upgrade to the latest version.
movehat updateThis command will:
- Check npm registry for the latest published version
- Compare with your current version
- Automatically upgrade if a new version is available
- Uses yarn by default (respects your package manager preference)
Automatic update notifications:
- Movehat automatically checks for updates when you run any command
- Notifications are cached for 24 hours to avoid excessive network requests
- The first time you run movehat, it checks npm in the background
- Subsequent runs show notifications immediately using the cache
- Update checks are skipped when running
--helporupdatecommand
Manage local forks of Movement/Aptos networks. See FORK_GUIDE.md for complete documentation.
Available commands:
fork create- Create a new fork from a networkfork list- List all available forksfork view-resource- View a resource from the forkfork fund- Fund an account in the forkfork serve- Start a local RPC server serving the fork
# Create a fork
movehat fork create --network testnet --name my-fork
# Start fork server
movehat fork serve --fork .movehat/forks/my-fork --port 8080
# List all forks
movehat fork list# Required: Your wallet private key (works on all networks - Hardhat-style)
PRIVATE_KEY=0x1234567890abcdef...
# Optional: Override RPC URL or default network
MOVEMENT_RPC_URL=https://custom-testnet.movementnetwork.xyz/v1
MH_DEFAULT_NETWORK=mainnetAccount resolution: Movehat looks for accounts in this order:
- Network-specific
accountsin config - Global
accountsin config PRIVATE_KEYenv variable
import { getMovehat } from "movehat";
async function main() {
const mh = await getMovehat();
// 1. Deploy (publish) the module
// Automatically checks if already deployed
const deployment = await mh.deployContract("counter");
console.log("Module deployed at:", deployment.address);
console.log("Transaction:", deployment.txHash);
// 2. Initialize
const contract = mh.getContract(deployment.address, "counter");
await contract.call(mh.account, "init", []);
// 3. Verify
const value = await contract.view("getValue", []);
console.log("Initial value:", value);
}
main().catch(console.error);import { getMovehat } from "movehat";
async function main() {
const mh = await getMovehat();
if (mh.config.network === "mainnet") {
console.log("WARNING: Deploying to MAINNET");
// Add confirmation logic
}
// Deploy module - automatically tracked per network
const deployment = await mh.deployContract("counter");
console.log(`Deployed on ${mh.config.network}:`, deployment.address);
}
main().catch(console.error);| Error | Solution |
|---|---|
| "Configuration file not found" | Create movehat.config.ts in your project root |
| "Network 'X' not found" | Add the network to networks in config |
| "No accounts configured" | Set PRIVATE_KEY in .env |
| "Module not found" | Run movehat compile first |
| "movement: command not found" | Install Movement CLI - REQUIRED |
| "Compilation failed: ENOENT: no such file or directory, uv_cwd" | Your current directory was deleted. Run cd ~ then navigate to your project |
| "Cannot find package 'dotenv'" | Run npm install or pnpm install in your project directory |
Required:
- Node.js v18+
- Movement CLI (install from Movement docs)
- npm or pnpm
What happens if Movement CLI is not installed:
movehat compilewill fail with "command not found" error- You won't be able to build or deploy contracts
- Solution: Install Movement CLI before using Movehat
Recommended:
- Git (for version control)
- VS Code with Move syntax highlighting extension
See CONTRIBUTING.md for development setup and guidelines.
MIT
- Quick Start Guide - Get started in 30 seconds
- Fork System Guide - Complete fork system documentation
- GitHub Repository
- NPM Package
- Movement Documentation
- Aptos SDK
Gilberts Ahumada