๐ Time-Lock Contract Example โ
Lock ERG or tokens until a specific date/time
This example demonstrates a single-interaction transaction pattern where funds are locked until a specific timestamp.
Overview โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TIME-LOCK FLOW โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ DEPOSIT PHASE WITHDRAWAL PHASE โ
โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โ
โ โ User โ โ User โ โ
โ โ Wallet โ โ Wallet โ โ
โ โโโโโโโโฌโโโโโโโ โโโโโโโโฒโโโโโโโ โ
โ โ โ โ
โ โ Lock funds โ Unlock after date โ
โ โผ โ โ
โ โโโโโโโโโโโโโโโ โโโโโโโโดโโโโโโโ โ
โ โ TimeLock โ TIME PASSES โ TimeLock โ โ
โ โ Box โ โโโโโโโโโโโโโถ โ Box โ โ
โ โ ๐ LOCKED โ โ ๐ UNLOCKED โ โ
โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โ
โ โ
โ Before: unlockTime After: unlockTime โ
โ โ Cannot withdraw โ
Can withdraw โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโThe Smart Contract โ
scala
{
// Time-Lock Contract
// Funds can only be spent after unlockTime by the owner
// Constants (injected at compile time)
val unlockTime: Long = $unlockTime
val ownerPk: GroupElement = $ownerPk
// Conditions
val timeCondition = CONTEXT.preHeader.timestamp >= unlockTime
val ownerCondition = proveDlog(ownerPk)
// Both must be satisfied
sigmaProp(timeCondition) && ownerCondition
}Complete Implementation โ
typescript
import {
TransactionBuilder,
OutputBuilder,
ErgoAddress,
SAFE_MIN_BOX_VALUE,
type Box
} from "@fleet-sdk/core";
import { compile } from "@fleet-sdk/compiler";
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๐ TIME-LOCK CONTRACT
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const TIME_LOCK_CONTRACT = `
{
// Time-Lock Contract v1.0
// Funds locked until specific timestamp
val unlockTime = $unlockTime
val ownerPk = $ownerPk
// Can only spend after unlock time
val isUnlocked = CONTEXT.preHeader.timestamp >= unlockTime
// Must be signed by owner
val isOwner = proveDlog(ownerPk)
sigmaProp(isUnlocked) && isOwner
}
`;
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๐ง HELPER FUNCTIONS
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
interface TimeLockParams {
ownerAddress: string;
lockAmount: string;
unlockDate: Date;
}
/**
* Compile time-lock contract with specific parameters
*/
function compileTimeLockContract(
ownerPk: string,
unlockTimestamp: bigint
): string {
const compiled = compile(TIME_LOCK_CONTRACT, {
map: {
unlockTime: unlockTimestamp,
ownerPk: ownerPk
}
});
return compiled.toHex();
}
/**
* Get public key from Ergo address
*/
function getPublicKeyFromAddress(address: string): string {
const ergoAddress = ErgoAddress.fromBase58(address);
const publicKeys = ergoAddress.getPublicKeys();
if (publicKeys.length === 0) {
throw new Error("Address has no public key (might be P2S address)");
}
return publicKeys[0];
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๐ LOCK FUNDS
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/**
* Create a time-locked box
*
* @param inputs - User's input boxes
* @param params - Lock parameters
* @param currentHeight - Current blockchain height
* @returns Unsigned transaction
*/
async function createTimeLockBox(
inputs: Box<string>[],
params: TimeLockParams,
currentHeight: number
) {
const { ownerAddress, lockAmount, unlockDate } = params;
// 1. Get owner's public key
const ownerPk = getPublicKeyFromAddress(ownerAddress);
// 2. Convert date to timestamp
const unlockTimestamp = BigInt(unlockDate.getTime());
// 3. Compile contract
const ergoTree = compileTimeLockContract(ownerPk, unlockTimestamp);
// 4. Get contract address
const contractAddress = ErgoAddress.fromErgoTree(ergoTree).toString();
console.log("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ");
console.log("๐ Creating Time-Lock Box");
console.log("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ");
console.log(`Owner: ${ownerAddress}`);
console.log(`Amount: ${BigInt(lockAmount) / 1000000000n} ERG`);
console.log(`Unlock Date: ${unlockDate.toISOString()}`);
console.log(`Contract: ${contractAddress}`);
console.log("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ");
// 5. Build transaction
const unsignedTx = new TransactionBuilder(currentHeight)
.from(inputs)
.to(
new OutputBuilder(lockAmount, contractAddress)
.setAdditionalRegisters({
// Store unlock time in R4 for easy reading
R4: unlockTimestamp.toString(),
// Store owner address in R5 for reference
R5: ownerAddress
})
)
.sendChangeTo(ownerAddress)
.payFee("1100000")
.build();
return {
transaction: unsignedTx,
contractAddress,
unlockTimestamp
};
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๐ UNLOCK FUNDS
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/**
* Withdraw from time-lock box (after unlock time)
*/
async function withdrawFromTimeLock(
timeLockBox: Box<string>,
ownerAddress: string,
currentHeight: number,
currentTimestamp: number
) {
// 1. Check if unlocked
const unlockTime = BigInt(timeLockBox.additionalRegisters.R4 || "0");
if (BigInt(currentTimestamp) < unlockTime) {
const unlockDate = new Date(Number(unlockTime));
throw new Error(
`Box is still locked! Unlocks at: ${unlockDate.toISOString()}`
);
}
console.log("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ");
console.log("๐ Withdrawing from Time-Lock");
console.log("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ");
console.log(`Box ID: ${timeLockBox.boxId}`);
console.log(`Amount: ${BigInt(timeLockBox.value) / 1000000000n} ERG`);
console.log(`To: ${ownerAddress}`);
console.log("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ");
// 2. Build withdrawal transaction
const withdrawAmount = BigInt(timeLockBox.value) - BigInt("1100000");
const unsignedTx = new TransactionBuilder(currentHeight)
.from([timeLockBox])
.to(
new OutputBuilder(withdrawAmount.toString(), ownerAddress)
)
.payFee("1100000")
.build();
return unsignedTx;
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๐ USAGE EXAMPLE
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async function main() {
// User's address
const ownerAddress = "9f4QF8AD1nQ3nJahQVkMj8hFSVVzVom77b52JU7EW71Zexg6N8y";
// Mock input box (in real app, fetch from blockchain)
const userInputBox: Box<string> = {
boxId: "abc123def456...",
value: "10000000000", // 10 ERG
ergoTree: "0008cd...",
creationHeight: 1000000,
assets: [],
additionalRegisters: {},
transactionId: "tx123...",
index: 0
};
// Lock until New Year 2026
const unlockDate = new Date("2026-01-01T00:00:00Z");
// Create time-lock box
const { transaction, contractAddress } = await createTimeLockBox(
[userInputBox],
{
ownerAddress,
lockAmount: "5000000000", // Lock 5 ERG
unlockDate
},
1100000
);
console.log("\n๐ฆ Transaction created!");
console.log(`Outputs: ${transaction.outputs.length}`);
console.log(`Contract address: ${contractAddress}`);
// After signing and submitting...
// Later, when time passes:
// const timeLockBox = await fetchBox(contractAddress);
// const withdrawTx = await withdrawFromTimeLock(
// timeLockBox,
// ownerAddress,
// currentHeight,
// Date.now()
// );
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๐งช TESTS
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
import { describe, it, expect } from "vitest";
describe("Time-Lock Contract", () => {
const mockAddress = "9f4QF8AD1nQ3nJahQVkMj8hFSVVzVom77b52JU7EW71Zexg6N8y";
it("should compile time-lock contract", () => {
const mockPk = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
const unlockTime = BigInt(Date.now() + 86400000); // 1 day from now
const ergoTree = compileTimeLockContract(mockPk, unlockTime);
expect(ergoTree).toBeTruthy();
expect(ergoTree.length).toBeGreaterThan(0);
});
it("should reject withdrawal before unlock time", async () => {
const timeLockBox: Box<string> = {
boxId: "test123",
value: "1000000000",
ergoTree: "0008cd...",
creationHeight: 100000,
assets: [],
additionalRegisters: {
R4: (Date.now() + 86400000).toString() // Locked for 1 more day
},
transactionId: "tx123",
index: 0
};
await expect(
withdrawFromTimeLock(timeLockBox, mockAddress, 110000, Date.now())
).rejects.toThrow("Box is still locked");
});
it("should allow withdrawal after unlock time", async () => {
const pastTime = Date.now() - 86400000; // 1 day ago
const timeLockBox: Box<string> = {
boxId: "test123",
value: "1000000000",
ergoTree: "0008cd...",
creationHeight: 100000,
assets: [],
additionalRegisters: {
R4: pastTime.toString() // Already unlocked
},
transactionId: "tx123",
index: 0
};
const tx = await withdrawFromTimeLock(
timeLockBox,
mockAddress,
110000,
Date.now()
);
expect(tx).toBeTruthy();
expect(tx.outputs.length).toBe(1);
});
});
export {
createTimeLockBox,
withdrawFromTimeLock,
compileTimeLockContract,
TIME_LOCK_CONTRACT
};Use Cases โ
| Use Case | Description |
|---|---|
| Vesting | Release employee tokens over time |
| Savings | Lock funds to prevent impulse spending |
| Escrow | Release funds after delivery date |
| ICO | Lock team tokens for specified period |
| Inheritance | Release to heirs after certain date |
Try It Yourself โ
Next Steps โ
- More Examples - Browse all code examples
- Data Inputs - Reference external boxes
- ErgoPay Integration - Mobile signing