Lambit for Ethereum Developers
Bitcoin and Ethereum both support programmable contracts, but the execution models differ sharply. This guide maps familiar EVM concepts to Lambit so you can reuse TypeScript skills without treating a UTXO chain like a global account state machine.
If you are already comfortable with TypeScript, continue with Basics and Stateful Contracts after this overview.
Smart contracts on Bitcoin vs Ethereum
Ethereum is a global state machine. Contract storage lives in shared world state, and transactions apply sequential transitions to that state. That design simplifies some application patterns, but it also serializes work that could otherwise proceed independently.
Bitcoin validates each transaction on its own inputs and outputs. Contract logic runs at spend time against the UTXO being consumed and the transaction the spender commits to. There is no shared mutable contract storage layer on chain — state lives in UTXO payloads and output covenants.
| Topic | Ethereum | Bitcoin with Lambit |
|---|---|---|
| Execution environment | EVM | Bitcoin Script via the BVM |
| Model | Account + storage slots | UTXO |
| Where "state" lives | Contract storage | Successor UTXO data outputs and committed output vectors |
| Transaction coupling | Sequential global ordering | Embarrassingly parallel validation per UTXO |
| Authoring paradigm | Impure methods mutating storage | Pure expression trees compiled to Script |
| Typical fees | Higher L1 fees | Lower base-layer fees; fee handling still matters in your provider flow |
| MEV surface | Protocol + builder dependent | No EVM-style global reordering of arbitrary storage writes |
Smart contract development: tool mapping
Ethereum teams usually assemble Hardhat/Foundry, Solidity, Ethers/Web3, Infura/Alchemy, and MetaMask. Lambit targets a tighter TypeScript-native stack:
| Role | Ethereum ecosystem | Lambit ecosystem |
|---|---|---|
| Language | Solidity | TypeScript functional DSL (contract(), method()) |
| Framework / CLI | Hardhat / Foundry | lambit CLI (compile, artifact, scaffold, test) |
| Contract libraries | Solidity libraries / interfaces | TypeScript helpers + library() metadata |
| Runtime SDK | Ethers.js / Web3.js | @opcat-labs/lambit runtime (deploy, call, prepareCall, providers) |
| Local testing | Hardhat network / Anvil | createMemoryProvider() + methods.<name>.verify(...) |
| Testnet access | Sepolia/Holesky RPC providers | OP_CAT testnet via createTestnetProvider() |
| IDE | Remix / VS Code + extensions | VS Code / any TypeScript IDE (contracts are plain TS modules) |
| Wallet | MetaMask | Catena |
| Explorer | Etherscan | OP_CAT testnet explorer |
| Role | Ethereum ecosystem | Lambit ecosystem |
|---|---|---|
| Language | Solidity | TypeScript functional DSL (contract(), method()) |
| Framework / CLI | Hardhat / Foundry | lambit CLI (compile, artifact, scaffold, test) |
| Contract libraries | Solidity libraries / interfaces | TypeScript helpers + [library()](/advanced/composition) metadata |
| Runtime SDK | Ethers.js / Web3.js | @opcat-labs/lambit runtime (deploy, call, prepareCall, providers) |
| Local testing | Hardhat network / Anvil | createMemoryProvider() + methods.<name>.verify(...) |
| Testnet access | Sepolia/Holesky RPC providers | OP_CAT testnet via createTestnetProvider() |
| IDE | Remix / VS Code + extensions | VS Code / any TypeScript IDE (contracts are plain TS modules) |
| Wallet | MetaMask | Catena |
| Explorer | Etherscan | OP_CAT testnet explorer |
Example code: Counter
Solidity keeps mutable storage on chain:
pragma solidity ^0.8.20;
contract Counter {
int256 private count;
constructor(int256 initialCount) {
count = initialCount;
}
function incrementCounter() public {
count += 1;
}
function getCount() public view returns (int256) {
return count;
}
}
In Lambit, the contract name and schemas are declared up front, methods return pure expressions, and state transitions describe the next UTXO state instead of mutating this:
import {
contract,
method,
TypeTag,
add,
gt,
lit,
} from '@opcat-labs/lambit';
const Counter = contract(
'Counter',
{},
{ count: TypeTag.Int },
({ state }) => ({
increase: method({}, () => {
const nextCount = add(state.count, lit(1n));
return {
next: { count: nextCount },
check: gt(nextCount, state.count),
};
}),
}),
);
const counter = Counter({});
const instance = counter.init({ count: 0n });
// Pure read of the successor state — no broadcast required
console.log(counter.methods.increase.next({ count: 0n })); // { count: 1n }
console.log(instance.scriptHashHex);
Key differences from Solidity:
- There is no implicit
msg.sender. Authorization is explicit (checkSig, hash locks, timelocks, output covenants). - Storage does not update in place. Each successful spend creates successor outputs that carry the new state.
- Reads like
getCount()become pure local helpers (methods.<name>.next(...)) or off-chain indexers reading the latest UTXO payload.