Skip to main content

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.

TopicEthereumBitcoin with Lambit
Execution environmentEVMBitcoin Script via the BVM
ModelAccount + storage slotsUTXO
Where "state" livesContract storageSuccessor UTXO data outputs and committed output vectors
Transaction couplingSequential global orderingEmbarrassingly parallel validation per UTXO
Authoring paradigmImpure methods mutating storagePure expression trees compiled to Script
Typical feesHigher L1 feesLower base-layer fees; fee handling still matters in your provider flow
MEV surfaceProtocol + builder dependentNo 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:

RoleEthereum ecosystemLambit ecosystem
LanguageSolidityTypeScript functional DSL (contract(), method())
Framework / CLIHardhat / Foundrylambit CLI (compile, artifact, scaffold, test)
Contract librariesSolidity libraries / interfacesTypeScript helpers + library() metadata
Runtime SDKEthers.js / Web3.js@opcat-labs/lambit runtime (deploy, call, prepareCall, providers)
Local testingHardhat network / AnvilcreateMemoryProvider() + methods.<name>.verify(...)
Testnet accessSepolia/Holesky RPC providersOP_CAT testnet via createTestnetProvider()
IDERemix / VS Code + extensionsVS Code / any TypeScript IDE (contracts are plain TS modules)
WalletMetaMaskCatena
ExplorerEtherscanOP_CAT testnet explorer
RoleEthereum ecosystemLambit ecosystem
LanguageSolidityTypeScript functional DSL (contract(), method())
Framework / CLIHardhat / Foundrylambit CLI (compile, artifact, scaffold, test)
Contract librariesSolidity libraries / interfacesTypeScript helpers + [library()](/advanced/composition) metadata
Runtime SDKEthers.js / Web3.js@opcat-labs/lambit runtime (deploy, call, prepareCall, providers)
Local testingHardhat network / AnvilcreateMemoryProvider() + methods.<name>.verify(...)
Testnet accessSepolia/Holesky RPC providersOP_CAT testnet via createTestnetProvider()
IDERemix / VS Code + extensionsVS Code / any TypeScript IDE (contracts are plain TS modules)
WalletMetaMaskCatena
ExplorerEtherscanOP_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.