Script Context
Lambit exposes transaction context through TxContext. The usual authoring
shape is a trailing ctx parameter: if a positional method(schema, fn) callback
declares one argument more than the schema fields, that final argument is a
TxContextExpr proxy. The compiler appends only the ctx fields your method
actually reads.
import { TypeTag, contract, eq, method } from '@opcat-labs/lambit';
const ContextGuard = contract(
'ContextGuard',
{ expectedHashOutputs: TypeTag.Hash256 },
({ props }) => ({
unlock: method(
{},
(ctx) => eq(ctx.hashOutputs, props.expectedHashOutputs),
),
}),
);
const contextGuard = ContextGuard({ expectedHashOutputs: '00'.repeat(32) });
console.log(contextGuard.artifact.contract); // ContextGuard
Context Fields
TxContext is the canonical OP_CAT sighash-preimage surface used by the compiler
and runtime. It is exported both as a schema value and as a TxContextExpr type
for helper functions that accept the trailing proxy.
| Field | Type | Typical use |
|---|---|---|
nVersion | ByteString | Version-byte checks. |
hashPrevouts | Hash256 | Full input-set commitments. |
inputIndex | Int | Selected input checks. |
outpoint | ByteString | Current outpoint checks. |
spentScriptHash | Sha256 | Native state output construction. |
spentDataHash | Sha256 | Current serialized state checks. |
value | Int | Spending amount for terminal outputs. |
nSequence | ByteString | Sequence-byte checks. |
hashSpentAmounts | Hash256 | Taproot-style spent amount commitments. |
hashSpentScriptHashes | Hash256 | Spent script commitments. |
hashSpentDataHashes | Hash256 | Spent data commitments. |
hashSequences | Hash256 | Sequence-vector commitments. |
hashOutputs | Hash256 | Output-vector or same-index output commitment. |
nLockTime | Int | Locktime and height checks. |
sigHashType | SigHashType | Selected sighash-path checks. |
Byte-oriented preimage fields such as nVersion, outpoint, and nSequence
stay exposed as raw bytes so contracts can inspect the exact serialized
preimage form. nLockTime is decoded as an Int, so author code can use
bigint-native comparisons while runtime timelock checks still enforce the
underlying UInt32 bounds.
Runtime auto-fill is based on the canonical TxContext field names and types.
Use the trailing ctx parameter when a method only needs a few fields. Use
method.named(TxContext, (ctx) => ...) when you intentionally want the full
context object in the ABI.
Native stateful compilation authenticates the pushed preimage by appending the
selected sighash type byte to the DER signature in the commitment prologue.
Successor-state outputs and single-output terminal paths use 0x03
(SIGHASH_SINGLE), while selected multi-output terminal paths use 0x01
(SIGHASH_ALL). Wallet-funded inputs are signed separately by the provider.
The assertOutputs(outputs) helper is the stateless full-output-vector
covenant. Native stateful contracts use SIGHASH_SINGLE for state transitions
and single-output terminal paths, so those hashOutputs commitments cover only
the same-index output. Multi-output terminal paths switch to SIGHASH_ALL,
which commits the full output vector.
Stateful methods may therefore:
- return
{ next, check? }for successor-state outputs - return
{ outputs, check? }for terminal outputs - use
branches(...)to choose between those two shapes on mutually exclusive runtime paths
Stateful compilation rejects user-authored hashOutputs dependencies in normal
expressions; the compiler generates the same-index hashOutputs check needed
for next transitions and selected terminal { outputs, check? } paths.
Funded provider paths also reject source-backed contracts that contain any
hashOutputs-dependent method, and artifact-only templates containing
<hashOutputs>, because wallet funding can add change after the prepared
context is fixed. Pass the provider to
deployed.methods.<name>.prepareCall(args?, { provider }) when you want this
funded-provider check to run before handing the prepared payload to an offline
signer; deployed method .call(...) requires the provider and performs the same
check before broadcast. Selected multi-output stateful terminal paths are also
rejected in funded mode, because provider-added change would invalidate the
SIGHASH_ALL output-vector commitment.
Inspecting Output Digests
import {
TypeTag,
buildArtifact,
buildDataOutput,
contract,
eq,
hash256,
method,
} from '@opcat-labs/lambit';
const OutputDigestGuard = contract(
'OutputDigestGuard',
{},
() => ({
unlock: method(
{ nextDataHash: TypeTag.Sha256 },
(nextDataHash, ctx) =>
eq(
ctx.hashOutputs,
hash256(buildDataOutput(ctx.spentScriptHash, ctx.value, nextDataHash)),
),
),
}),
);
const artifact = buildArtifact(OutputDigestGuard);
const unlockAbi = artifact.abi.find(
(entry) => entry.type === 'function' && entry.name === 'unlock',
);
console.log(unlockAbi?.params.map((param) => param.name));
// ['nextDataHash', 'spentScriptHash', 'value', 'hashOutputs']
console.log(artifact.hex.length > 0); // true
Full Context Checks
Use method.named(TxContext, (ctx) => ...) when a method really wants the full
context object. The example below uses nVersion as a byte-exact version guard
and nLockTime as a numeric timelock floor.
import {
TxContext,
TypeTag,
and,
buildArtifact,
buildDataOutput,
contract,
eq,
gte,
hash256,
method,
} from '@opcat-labs/lambit';
const PreimageGuard = contract(
'PreimageGuard',
{ expectedVersionBytes: TypeTag.ByteString, minLockTime: TypeTag.Int },
({ props }) => ({
unlock: method.named(
TxContext,
(ctx) => and(
eq(ctx.nVersion, props.expectedVersionBytes),
and(
gte(ctx.nLockTime, props.minLockTime),
eq(
ctx.hashOutputs,
hash256(buildDataOutput(ctx.spentScriptHash, ctx.value, ctx.spentDataHash)),
),
),
),
),
}),
);
const artifact = buildArtifact(PreimageGuard);
console.log(Object.keys(TxContext).length); // 15
console.log(artifact.abi[0]?.params.at(-1)?.name); // sigHashType
Helper Functions
Import TxContextExpr when a reusable helper needs to accept the trailing
context proxy.
import {
type TxContextExpr,
contract,
eq,
hash256,
buildDataOutput,
method,
TypeTag,
} from '@opcat-labs/lambit';
function commitsNextData(ctx: TxContextExpr, nextDataHash: string) {
return eq(
ctx.hashOutputs,
hash256(buildDataOutput(ctx.spentScriptHash, ctx.value, nextDataHash)),
);
}
const HelperBackedGuard = contract(
'HelperBackedGuard',
{},
() => ({
unlock: method(
{ nextDataHash: TypeTag.Sha256 },
(nextDataHash, ctx) => commitsNextData(ctx, nextDataHash),
),
}),
);
console.log(HelperBackedGuard({}).artifact.contract); // HelperBackedGuard
Stateful Output Verification Lifecycle
import { contract, method, TypeTag, add, gt, buildArtifact, 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 artifact = buildArtifact(Counter);
// Native stateful compilation replaces every context marker with
// preimage-backed opcode sequences, so no UTF-8 placeholders survive in the
// deployed hex.
console.log(artifact.hex.includes('<hashOutputs>')); // false
console.log(artifact.hex.includes('<spentScriptHash>')); // false
console.log(artifact.hex.includes('<value>')); // false
buildDataOutput() and stateHash() are exported compiler helpers when you need explicit output-hash expressions.
TxContext covers the runtime-injected spend/output placeholders available
today. Keep examples consensus-meaningful; avoid placeholder self-equality
checks such as eq(x, x) in production contracts.
Funded provider calls perform extra rejection checks before broadcast because wallet-added change can alter the output vector after a contract context was prepared.1