Examples
This page collects reference contract definitions that compile with the current Lambit prototype.
Use these as starting points and adapt field names, checks, and methods for your own policy.
Included contracts
| Contract | Kind | Main idea |
|---|---|---|
P2PK | Stateless | Standard single-pubkey ownership check. |
P2PKH | Stateless | Signature + pubkey-hash ownership check. |
Vault | Stateful | Bound-contract workflow with pure next() transitions and init(). |
Counter | Stateful | Monotonic integer state transition. |
AtomicSwap | Stateless | Hashlock redeem path + timelocked refund path. |
Auction | Stateful | Bid updates + testing-only seller-authorized same-state transition. |
MultiSig | Stateless | checkMultiSig threshold-style authorization. |
TicTacToe | Stateful | FixedArray board state with winner/draw terminal payouts. |
Token | Stateful | Supply/balance-root transitions with minter auth. |
Vault walkthrough
import {
contract,
method,
TypeTag,
add,
and,
checkLocktime,
checkSig,
gte,
sub,
type TxContextExpr,
} from '@opcat-labs/lambit';
const Vault = contract(
'Vault',
{ owner: TypeTag.PubKey },
{ balance: TypeTag.Int },
({ props, state }) => ({
withdraw: method(
{ amount: TypeTag.Int, sig: TypeTag.Sig },
(amount, sig) => ({
next: { balance: sub(state.balance, amount) },
check: and(checkSig(sig, props.owner), gte(state.balance, amount)),
}),
),
deposit: method(
{ amount: TypeTag.Int },
(amount) => ({
// Pedagogical example: anyone can deposit. Add auth for production flows.
next: { balance: add(state.balance, amount) },
}),
),
}),
);
const vault = Vault({ owner: '02'.padEnd(66, '1') });
const afterWithdraw = vault.methods.withdraw.next({ balance: 100n }, { amount: 30n });
const afterDeposit = vault.methods.deposit.next(afterWithdraw, { amount: 50n });
const instance = vault.init({ balance: 100n });
console.log(vault.artifact.contract); // Vault
console.log(afterWithdraw); // { balance: 70n }
console.log(afterDeposit); // { balance: 120n }
console.log(instance.state); // { balance: 100n }
Complete example set
import {
P2PK,
P2PKH,
branches,
buildArtifact,
contract,
method,
TypeTag,
ContractDef,
FixedArray,
add,
and,
assertOutputs,
checkLocktime,
checkMultiSig,
checkSig,
cond,
div,
eq,
ExprNode,
gt,
gte,
hash160,
len,
lit as literal,
lt,
neq,
not,
p2pkhOutput,
sha256,
some,
sub,
} from '@opcat-labs/lambit';
// 1) P2PK
const P2PKExample = P2PK({ pubkey: '02'.padEnd(66, '2') });
// 2) P2PKH
const P2PKHExample = P2PKH({ addr: '00112233445566778899aabbccddeeff00112233' });
// 3) Vault
const Vault = contract(
'Vault',
{ owner: TypeTag.PubKey },
{ balance: TypeTag.Int },
({ props, state }) => ({
withdraw: method(
{ amount: TypeTag.Int, sig: TypeTag.Sig },
(amount, sig) => ({
next: { balance: sub(state.balance, amount) },
check: and(checkSig(sig, props.owner), gte(state.balance, amount)),
}),
),
deposit: method(
{ amount: TypeTag.Int },
(amount) => ({
// Pedagogical example: anyone can deposit. Add auth for production flows.
next: { balance: add(state.balance, amount) },
}),
),
}),
);
// 4) Counter
const Counter = contract(
'Counter',
{},
{ count: TypeTag.Int },
({ state }) => ({
increase: method({}, () => {
const nextCount = add(state.count, literal(1n));
return {
next: { count: nextCount },
check: gt(nextCount, state.count),
};
}),
}),
);
// 5) AtomicSwap
// Stateless methods that call assertOutputs use the trailing ctx proxy so the
// compiler and runtime can bind the spent value and hashOutputs preimage field.
const AtomicSwap = contract(
'AtomicSwap',
{
recipient: TypeTag.Ripemd160,
refundPkh: TypeTag.Ripemd160,
hashlock: TypeTag.Sha256,
// Timelock operands use Int in the DSL but still must fit the protocol's UInt32 range.
timeout: TypeTag.Int,
},
({ props }) => ({
redeem: method(
{
sig: TypeTag.Sig,
pubkey: TypeTag.PubKey,
preimage: TypeTag.ByteString,
},
(sig, pubkey, preimage, ctx) =>
and(
eq(hash160(pubkey), props.recipient),
eq(sha256(preimage), props.hashlock),
checkSig(sig, pubkey),
assertOutputs([p2pkhOutput(ctx.value, props.recipient)]),
),
),
refund: method(
{ sig: TypeTag.Sig, pubkey: TypeTag.PubKey },
(sig, pubkey, ctx) =>
and(
// CLTV must bind to transaction nLockTime; do not replace this with
// a caller-supplied locktime argument and numeric comparison.
// Keeping it as a direct `and()` operand lets the compiler fuse its
// self-aborting check without adding a redundant truth sentinel.
checkLocktime(props.timeout),
eq(hash160(pubkey), props.refundPkh),
checkSig(sig, pubkey),
assertOutputs([p2pkhOutput(ctx.value, props.refundPkh)]),
),
),
}),
);
// 6) Auction
// Simplified example: bid updates state and close authenticates the seller.
const Auction = contract(
'Auction',
{
seller: TypeTag.Ripemd160,
minBidIncrement: TypeTag.Int,
},
{ highestBid: TypeTag.Int, highestBidder: TypeTag.Ripemd160 },
({ props, state }) => ({
bid: method(
{ newBid: TypeTag.Int, newBidder: TypeTag.Ripemd160 },
(newBid, newBidder) => ({
next: {
highestBid: newBid,
highestBidder: newBidder,
},
check: and(
// Keep equality invalid even when minBidIncrement is configured as 0n.
gt(newBid, state.highestBid),
gte(sub(newBid, state.highestBid), props.minBidIncrement),
),
}),
),
close: method(
{ sig: TypeTag.Sig, pubkey: TypeTag.PubKey },
(sig, pubkey) =>
and(
eq(hash160(pubkey), props.seller),
checkSig(sig, pubkey),
),
),
}),
);
// 7) MultiSig (2 signatures checked against 3 keys)
const MultiSig = contract('MultiSig', {}, () => ({
unlock: method(
{
sigA: TypeTag.Sig,
sigB: TypeTag.Sig,
pk1: TypeTag.PubKey,
pk2: TypeTag.PubKey,
pk3: TypeTag.PubKey,
},
(sigA, sigB, pk1, pk2, pk3) => checkMultiSig([sigA, sigB], [pk1, pk2, pk3]),
),
}));
const TICTACTOE_WIN_LINES: ReadonlyArray<readonly [number, number, number]> = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
const ticTacToeBoard = FixedArray(TypeTag.Int, 9);
function ticTacToeBoolEq(left: ExprNode, right: ExprNode): ExprNode {
return cond(eq(left, right), literal(true), literal(false));
}
function ticTacToeMarkAfterMove(
board: { at(index: ExprNode): ExprNode },
moveIndex: ExprNode,
targetIndex: number,
mark: ExprNode,
): ExprNode {
return cond(
eq(moveIndex, literal(BigInt(targetIndex))),
literal(true),
ticTacToeBoolEq(board.at(literal(BigInt(targetIndex))), mark),
);
}
function ticTacToeWon(
board: { at(index: ExprNode): ExprNode },
moveIndex: ExprNode,
mark: ExprNode,
): ExprNode {
return some(TICTACTOE_WIN_LINES, ([a, b, c]) => and(
ticTacToeMarkAfterMove(board, moveIndex, a, mark),
and(
ticTacToeMarkAfterMove(board, moveIndex, b, mark),
ticTacToeMarkAfterMove(board, moveIndex, c, mark),
),
));
}
function ticTacToeDef(): ContractDef {
return contract(
'TicTacToe',
{ alice: TypeTag.PubKey, bob: TypeTag.PubKey },
{
board: ticTacToeBoard,
isAliceTurn: TypeTag.Bool,
},
({ props, state }) => ({
move: method(
{ playerSig: TypeTag.Sig, n: TypeTag.Int },
(playerSig, n, ctx: TxContextExpr) => {
const currentPlayer = cond(state.isAliceTurn, props.alice, props.bob);
const currentMark = cond(state.isAliceTurn, literal(1n), literal(2n));
const isBoundedMove = and(gte(n, literal(0n)), lt(n, literal(9n)));
const safeIndex = cond(isBoundedMove, n, literal(0n));
const nextBoard = state.board.set(safeIndex, currentMark);
const precondition = and(
isBoundedMove,
and(
eq(state.board.at(safeIndex), literal(0n)),
checkSig(playerSig, currentPlayer),
),
);
const won = ticTacToeWon(state.board, safeIndex, currentMark);
const full = state.board.every((cell, index) => cond(
eq(index, safeIndex),
literal(true),
neq(cell, literal(0n)),
));
return branches(
{
when: won,
do: {
outputs: [p2pkhOutput(ctx.value, hash160(currentPlayer))],
check: precondition,
},
},
{
when: full,
do: {
outputs: [
// Odd satoshi draws round the remainder toward Alice so the
// split stays deterministic across offline/live tests.
p2pkhOutput(sub(ctx.value, div(ctx.value, literal(2n))), hash160(props.alice)),
p2pkhOutput(div(ctx.value, literal(2n)), hash160(props.bob)),
],
check: precondition,
},
},
{
default: {
next: {
board: nextBoard,
isAliceTurn: not(state.isAliceTurn),
},
check: precondition,
},
},
);
},
),
}),
);
}
// 8) TicTacToe
const TicTacToe = ticTacToeDef();
// 9) Token
const Token = contract(
'Token',
{ minter: TypeTag.Ripemd160 },
{
totalSupply: TypeTag.Int,
balanceRoot: TypeTag.ByteString,
nonce: TypeTag.Int,
},
({ props, state }) => ({
transfer: method({ nextBalanceRoot: TypeTag.ByteString }, (nextBalanceRoot) => ({
next: {
totalSupply: state.totalSupply,
balanceRoot: nextBalanceRoot,
nonce: add(state.nonce, literal(1n)),
},
check: gt(len(nextBalanceRoot), literal(0n)),
})),
mint: method(
{
amount: TypeTag.Int,
nextBalanceRoot: TypeTag.ByteString,
sig: TypeTag.Sig,
pubkey: TypeTag.PubKey,
},
(amount, nextBalanceRoot, sig, pubkey) => ({
next: {
totalSupply: add(state.totalSupply, amount),
balanceRoot: nextBalanceRoot,
nonce: add(state.nonce, literal(1n)),
},
check: and(
eq(hash160(pubkey), props.minter),
and(checkSig(sig, pubkey), gt(amount, literal(0n))),
),
}),
),
}),
);
const vault = Vault({ owner: '02'.padEnd(66, '1') });
const transitions = {
afterWithdraw: vault.methods.withdraw.next({ balance: 100n }, { amount: 30n }),
afterDeposit: vault.methods.deposit.next({ balance: 70n }, { amount: 50n }),
};
const instance = vault.init({ balance: 100n });
const artifacts = [
P2PKExample.artifact,
P2PKHExample.artifact,
buildArtifact(Vault),
buildArtifact(Counter),
buildArtifact(AtomicSwap),
buildArtifact(Auction),
buildArtifact(MultiSig),
buildArtifact(TicTacToe),
buildArtifact(Token),
];
console.log(artifacts.map((artifact) => artifact.contract));
console.log(transitions, instance.state);
note
These examples focus on compiler-level contract definitions, bound-contract transitions, and artifact output.
Auction note:
closeis a testing-only terminal auth path. Production auctions still need escrow, bidder authentication, payout/refund handling, and fee policy.Caution: Production deployments must add transaction builders, signer/provider integration, and full security review. If you are migrating older code, keep names in
contract('Name', ...)instead of assigningdef.namelater.
Auction bid transition walkthrough
import {
contract,
method,
TypeTag,
and,
gt,
gte,
sub,
} from '@opcat-labs/lambit';
const Auction = contract(
'Auction',
{
seller: TypeTag.Ripemd160,
minBidIncrement: TypeTag.Int,
},
{ highestBid: TypeTag.Int, highestBidder: TypeTag.Ripemd160 },
({ props, state }) => ({
bid: method(
{ newBid: TypeTag.Int, newBidder: TypeTag.Ripemd160 },
(newBid, newBidder) => ({
next: {
highestBid: newBid,
highestBidder: newBidder,
},
check: and(
gt(newBid, state.highestBid),
gte(sub(newBid, state.highestBid), props.minBidIncrement),
),
}),
),
}),
);
const auction = Auction({
seller: '11'.repeat(20),
minBidIncrement: 10n,
});
const nextBidState = auction.methods.bid.next(
{
highestBid: 100n,
highestBidder: '22'.repeat(20),
},
{
newBid: 115n,
newBidder: '33'.repeat(20),
},
);
console.log(nextBidState); // { highestBid: 115n, highestBidder: '3333...' }