Skip to main content

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

ContractKindMain idea
P2PKStatelessStandard single-pubkey ownership check.
P2PKHStatelessSignature + pubkey-hash ownership check.
VaultStatefulBound-contract workflow with pure next() transitions and init().
CounterStatefulMonotonic integer state transition.
AtomicSwapStatelessHashlock redeem path + timelocked refund path.
AuctionStatefulBid updates + testing-only seller-authorized same-state transition.
MultiSigStatelesscheckMultiSig threshold-style authorization.
TicTacToeStatefulFixedArray board state with winner/draw terminal payouts.
TokenStatefulSupply/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: close is 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 assigning def.name later.

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...' }

What's Next