Skip to main content

Basics

Lambit contracts are pure TypeScript functions that build expression trees (ExprNode). Use contract() to define a named contract, then call that definition to bind props and reach the runtime bridge.

contract()

This example uses the byte-minimal P2PKH form: compose the address check and signature check directly, and let pubKey2Addr(pubkey) document the address derivation intent.

import {
contract,
method,
TypeTag,
and,
eq,
pubKey2Addr,
checkSig,
} from '@opcat-labs/lambit';

const P2PKH = contract(
'P2PKH',
{ addr: TypeTag.Ripemd160 },
({ props }) => ({
unlock: method(
{ sig: TypeTag.Sig, pubkey: TypeTag.PubKey },
(sig, pubkey) => and(eq(pubKey2Addr(pubkey), props.addr), checkSig(sig, pubkey)),
),
}),
);

const p2pkh = P2PKH({ addr: '00112233445566778899aabbccddeeff00112233' });
console.log(P2PKH.name); // P2PKH
console.log(p2pkh.artifact.hex); // 76a9<addr>88ac

Wrap the same predicate in assert(...) when you want assertion metadata. That appends an explicit verify boundary (OP_VERIFY OP_1) after the P2PKH predicate.

import {
contract,
method,
TypeTag,
and,
assert,
eq,
pubKey2Addr,
checkSig,
} from '@opcat-labs/lambit';

const AssertP2PKH = contract(
'AssertP2PKH',
{ addr: TypeTag.Ripemd160 },
({ props }) => ({
unlock: method(
{ sig: TypeTag.Sig, pubkey: TypeTag.PubKey },
(sig, pubkey) => assert(and(eq(pubKey2Addr(pubkey), props.addr), checkSig(sig, pubkey))),
),
}),
);

const assertP2pkh = AssertP2PKH({ addr: '00112233445566778899aabbccddeeff00112233' });
console.log(assertP2pkh.artifact.hex); // 76a9<addr>88ac6951

contract() parameters

ParameterTypeDescription
namestringRequired contract name. Must be a valid TypeScript identifier.
propsRecord&lt;string, TypeTag | ReturnType&lt;typeof alias&gt; | ReturnType&lt;typeof struct&gt;&gt;Constructor schema. These are the arguments you pass when binding the definition, for example P2PKH({ addr }). Each field becomes a prop expression node.
body / state, body({ props, state? }) => Record&lt;string, MethodDescriptor&gt;Closure that receives the ambient contract env and returns method(schema, fn) or method.named(schema, (args) => ...) declarations.

Binding props

Calling the definition binds constructor props without a separate helper import.

import {
contract,
method,
TypeTag,
and,
eq,
hash160,
checkSig,
} from '@opcat-labs/lambit';

const P2PKH = contract(
'P2PKH',
{ addr: TypeTag.Ripemd160 },
({ props }) => ({
unlock: method(
{ sig: TypeTag.Sig, pubkey: TypeTag.PubKey },
(sig, pubkey) => and(eq(hash160(pubkey), props.addr), checkSig(sig, pubkey)),
),
}),
);

const p2pkh = P2PKH({ addr: '00112233445566778899aabbccddeeff00112233' });
console.log(p2pkh.artifact.contract); // P2PKH
console.log(Object.keys(p2pkh.methods)); // ['unlock']

Reusable helpers

Lambit exports standard helpers so you do not have to rebuild common patterns each time.

import {
OpCode,
P2PK,
P2PKH,
contract,
method,
TypeTag,
eq,
fill,
lit,
min,
opcode,
opcodes,
within,
} from '@opcat-labs/lambit';

const p2pkInstance = P2PK({ pubkey: '02'.padEnd(66, '1') });
const p2pkhInstance = P2PKH({ addr: '00112233445566778899aabbccddeeff00112233' });

const OpcodePolicy = contract(
'OpcodePolicy',
{},
() => ({
matchesDup: method(
{ actual: TypeTag.OpCodeType },
(actual) => eq(actual, opcode('OP_DUP')),
),
matchesP2pkhPrefix: method(
{ prefix: TypeTag.ByteString },
(prefix) => eq(prefix, opcodes('OP_DUP', 'OP_HASH160')),
),
}),
);

const opcodeBytes = opcodes('OP_DUP', 'OP_HASH160');
const repeatedChecks = fill(eq(OpCode.OP_DUP, opcode('OP_DUP')), 2);
const bounded = within(min(lit(5n), lit(7n)), lit(0n), lit(10n));

console.log(p2pkInstance.artifact.hex); // <pubkey>ac
console.log(p2pkhInstance.artifact.hex); // 76a9<addr>88ac
console.log(opcodeBytes); // { tag: 'literal', value: '76a9' }
console.log(repeatedChecks.length); // 2
console.log(bounded); // { tag: 'call', op: 'within', ... }
console.log(OpcodePolicy.name); // OpcodePolicy

within(value, lower, upper) uses the Bitcoin OP_WITHIN half-open range rule: lower <= value < upper (the lower bound is included, the upper bound is not).

Props and method params

props are read from the ambient contract closure. Method parameters are declared by method(schema, fn) for positional args, or method.named(schema, (args) => ...) when named helper composition reads better. That same schema is emitted into artifact.abi, preserving each param name and type. The output is an ABI array with one function entry per method plus a trailing constructor entry.

Schemas may also use alias(name, type) and struct(name, fields) when you need artifact/runtime metadata for shaped values. Current limit: these custom types are preserved through the IR, artifact, and runtime encoding layers, but the DSL still treats them as opaque values rather than exposing field access inside contract expressions. The optional struct(..., genericTypes) list is emitted as metadata for artifact consumers; runtime encoding validates the declared field list directly and does not specialize generic parameters.

import { contract, method, TypeTag, add, eq, buildArtifact } from '@opcat-labs/lambit';

const SumPolicy = contract(
'SumPolicy',
{ target: TypeTag.Int },
({ props }) => ({
unlock: method(
{ a: TypeTag.Int, b: TypeTag.Int },
(a, b) => eq(add(a, b), props.target),
),
namedSum: method.named(
{ left: TypeTag.Int, right: TypeTag.Int },
({ left, right }) => eq(add(left, right), props.target),
),
}),
);

const artifact = buildArtifact(SumPolicy);
console.log(artifact.abi);

Expressions and combinators

Use DSL primitives to build boolean predicates.

CategoryHelpers
Logicand, or, not, cond
Comparisoneq, neq, gt, gte, lt, lte
Arithmeticadd, sub, mul, div, mod, abs, min, max, within
Cryptosha256(...), hash160(...), hash256(...), pubKey2Addr(...), checkSig(...), checkSigVerify(...), checkMultiSig(...), checkMultiSigVerify(...), checkDataSig(...), checkDataSigVerify(...)
Bytescat, slice, len, intToBytes, bytesToInt, buildP2pkhOutput

assert(expr) and assert(expr, message) both create explicit verify boundaries. pubKey2Addr(pubkey) is the named P2PKH address helper. It preserves authored address intent in IR while lowering to the same OP_HASH160 script surface as hash160(pubkey). within(value, lower, upper) uses a half-open interval, so within(x, 0n, 10n) accepts 0n through 9n and rejects 10n. buildP2pkhOutput(pkh, satoshis) builds the serialized 34-byte P2PKH transaction output <8-byte amount><1-byte script length 0x19><25-byte P2PKH scriptPubKey> used by output-covenant commitments. pkh values must be exactly 20 bytes, and satoshis values must fit the current OP_NUM2BIN 8-byte encoding path: 0 <= satoshis <= 2^63 - 1. The compiled guard uses OP_NUM2BIN 8 for the upper bound and a byte-level sign-bit check for non-negativity.

PatternUse when
and(checkSig(...), otherCheck) or and(checkDataSig(...), otherCheck)You are composing boolean predicates; the compiler can fuse eligible checks to VERIFY opcodes.
assert(checkSig(...), message) or assert(checkDataSig(...), message)You want an explicit verify boundary plus assertion metadata in the artifact.
checkSigVerify(...), checkMultiSigVerify(...), or checkDataSigVerify(...)The VERIFY opcode is the whole method body, or you want assertion metadata exactly at that VERIFY site.
import {
contract,
method,
TypeTag,
assert,
checkDataSig,
} from '@opcat-labs/lambit';

const DataSig = contract(
'DataSig',
{ pubKey: TypeTag.PubKey },
({ props }) => ({
unlock: method(
{ sig: TypeTag.Sig, message: TypeTag.ByteString },
(sig, message) => assert(checkDataSig(sig, message, props.pubKey)),
),
}),
);

const dataSig = DataSig({ pubKey: '02'.padEnd(66, '1') });
console.log(dataSig.artifact.hex.includes('ba69')); // true

Build-time helpers

fold, map, every, and some run at build time and return new ExprNode trees. range, mapRange, foldRange, everyRange, and someRange do the same for explicit numeric loops.

import { contract, method, fold, lit, add, eq, TypeTag } from '@opcat-labs/lambit';

const SumThree = contract(
'SumThree',
{ expected: TypeTag.Int },
({ props }) => ({
unlock: method(
{ x: TypeTag.Int, y: TypeTag.Int, z: TypeTag.Int },
(x, y, z) => {
const total = fold([x, y, z], lit(0n), (acc, elem) => add(acc, elem));
return eq(total, props.expected);
},
),
}),
);
import {
contract,
method,
TypeTag,
lit,
add,
eq,
foldRange,
} from '@opcat-labs/lambit';

const BiasedSum = contract(
'BiasedSum',
{ expected: TypeTag.Int },
({ props }) => ({
unlock: method(
{ x: TypeTag.Int, y: TypeTag.Int, z: TypeTag.Int },
(x, y, z) => {
const values = [x, y, z];
// Produces 1-based weights 1..values.length; values.length + 1 is the exclusive end.
// i remains the zero-based JavaScript subscript; weightExpr is the 1-based literal.
const total = foldRange(1, values.length + 1, lit(0n), (acc, weightExpr, i) =>
add(acc, add(values[i]!, weightExpr)),
);
return eq(total, props.expected);
},
),
}),
);

Loop helpers: Range callbacks receive both the literal range value and the zero-based JavaScript iteration counter. In the count form (foldRange(values.length, ...)), that literal is 0..count-1; in the bounded form (foldRange(1, values.length + 1, ...)), it is start..end-1. Use the literal expression inside returned IR trees and the iteration number for local array subscripts.

Caution: and(...) and or(...) require at least one argument and throw on empty input. Compile-time range helpers also reject non-integer bounds, reject inverted bounds (start > end), accept signed bounds such as range(-2, 1), and cap unrolling at 128 iterations per helper call. If you are migrating from older code, keep the contract name in contract('Name', ...) instead of assigning def.name later.

For the full supported model, including cond(...), branches(...), dispatch structure, and loop-sizing guidance, see Built-in Functions.

What's Next