Timelock
Lock and unlock BSV until specific block heights using @1sat/actions.
Actions
| Action | Description |
|--------|-------------|
| getLockData | Get summary of all locks (total, unlockable, next unlock) |
| lockBsv | Lock BSV until a specific block height |
| unlockBsv | Unlock all matured (expired) locks |
Check Lock Status
import { getLockData, createContext } from '@1sat/actions'
const ctx = createContext(wallet, { services })
const data = await getLockData.execute(ctx, {})
console.log(`Total locked: ${data.totalLocked} sats`)
console.log(`Unlockable now: ${data.unlockable} sats`)
console.log(`Next unlock at block: ${data.nextUnlock}`)
LockData Response
interface LockData {
totalLocked: number // Total satoshis in all locks
unlockable: number // Satoshis that can be unlocked now
nextUnlock: number // Block height of next maturing lock
}
Lock BSV
import { lockBsv, createContext } from '@1sat/actions'
const ctx = createContext(wallet, { services })
// Lock 10,000 sats until block 900000
const result = await lockBsv.execute(ctx, {
requests: [
{ satoshis: 10000, until: 900000 },
],
})
// Multiple locks in one transaction
const result = await lockBsv.execute(ctx, {
requests: [
{ satoshis: 5000, until: 880000 },
{ satoshis: 10000, until: 900000 },
{ satoshis: 50000, until: 950000 },
],
})
if (result.txid) {
console.log('Locked! txid:', result.txid)
}
Unlock Matured Locks
import { unlockBsv, createContext } from '@1sat/actions'
const ctx = createContext(wallet, { services })
const result = await unlockBsv.execute(ctx, {})
if (result.txid) {
console.log('Unlocked! txid:', result.txid)
} else if (result.error === 'no-matured-locks') {
console.log('No locks ready to unlock yet')
}
How Unlocking Works
- Lists all outputs in the
locksbasket - Checks each lock's
until:tag against current block height - Filters to only matured locks (until <= currentHeight)
- Calls
createActionwithsignAndProcess: falseto build the unsigned transaction - Calls
completeSignedActionwhich handles BEEF merge, signing, script verification,signAction, and abort on failure - The signing callback uses
Lock.unlockWithWalletper input
completeSignedAction Pattern
All two-phase unlock operations use the completeSignedAction helper:
import { completeSignedAction } from '@1sat/actions'
import { Lock } from '@1sat/templates'
const result = await completeSignedAction(
ctx.wallet,
createResult,
inputBEEF as number[],
async (tx) => {
const spends: Record<number, { unlockingScript: string }> = {}
for (let i = 0; i < maturedLocks.length; i++) {
const lock = maturedLocks[i]
const unlocker = Lock.unlockWithWallet(
ctx.wallet,
lock.protocolID,
lock.keyID,
'self',
)
const unlockingScript = await unlocker.sign(tx, i)
spends[i] = { unlockingScript: unlockingScript.toHex() }
}
return spends
},
)
The helper handles:
- Merging the signable transaction BEEF with
inputBEEF(fixes stripped merkle proofs) - Calling the signing callback with a fully-wired
Transaction - Verifying unlocking scripts against locking scripts before submitting
- Calling
signActionto finalize - Calling
abortActionautomatically on failure
How the Lock Script Works
The lock script combines a CLTV (CheckLockTimeVerify) check with a P2PKH signature check:
<lockPrefix> <pubKeyHash> <blockHeight> <lockSuffix>
To unlock:
- Transaction
nLockTimemust be >= the lock's block height - Input
sequenceNumbermust be 0 (enables nLockTime checking) - Valid signature from the lock key
- Sighash preimage for script verification
Lock Input Parameters
When building createAction inputs for lock outputs:
unlockingScriptLength: 1205(accounts for DER signature variability)sequenceNumber: 0(required for nLockTime)anyoneCanPaymust befalse(the default)
Lock Storage
Locks are stored in the locks basket with tags:
| Tag | Meaning |
|-----|---------|
| until:{height} | Block height when the lock matures |
Custom instructions store the protocol and key info for unlocking.
Minimum Unlock Amount
There is a minimum unlock threshold (MIN_UNLOCK_SATS) to prevent dust unlock attempts. If your matured locks total less than this threshold per lock, they won't appear as unlockable.
Current Block Height
The lock module uses services.chaintracks.currentHeight() to determine the current block height. You can also check it directly:
const res = await fetch('https://api.1sat.app/1sat/chaintracks/height')
const height = await res.json()
console.log('Current height:', height)
Requirements
bun add @1sat/actions @1sat/wallet @bsv/sdk
Unlock operations require services for current block height lookup.