Skip to content

โš™๏ธ Compile Time Constants โ€‹

Injecting values into ErgoScript contracts at compile time

Compile time constants allow you to inject values into contracts when compiling, creating customized contract instances.

What are Compile Time Constants? โ€‹

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    COMPILE TIME CONSTANTS                       โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                 โ”‚
โ”‚   ErgoScript Template          Compiled Contract                โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”             โ”‚
โ”‚   โ”‚ {               โ”‚          โ”‚ {               โ”‚             โ”‚
โ”‚   โ”‚   val pk = $PK  โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ  โ”‚   val pk = ...  โ”‚             โ”‚
โ”‚   โ”‚   val amt = $AMTโ”‚  inject  โ”‚   val amt = 100 โ”‚             โ”‚
โ”‚   โ”‚   ...           โ”‚  values  โ”‚   ...           โ”‚             โ”‚
โ”‚   โ”‚ }               โ”‚          โ”‚ }               โ”‚             โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜             โ”‚
โ”‚                                                                 โ”‚
โ”‚   Template (reusable)          Instance (specific)              โ”‚
โ”‚                                                                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Why Use Compile Time Constants? โ€‹

Use CaseDescription
User-specific contractsInject user's public key
Configurable amountsSet minimum values at compile
Time-locked contractsInject unlock timestamps
Token IDsSpecify which tokens contract accepts
Custom logicCreate variants of same contract

Basic Example โ€‹

typescript
import { compile } from "@fleet-sdk/compiler";
import { Network } from "@fleet-sdk/core";

// Contract template with placeholder $ownerPk
const contractTemplate = `
{
  // Only owner can spend this box
  proveDlog($ownerPk)
}
`;

// Compile with injected value
const compiledContract = compile(contractTemplate, {
  map: {
    ownerPk: "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
  },
  network: Network.Mainnet
});

console.log("ErgoTree:", compiledContract.toHex());

Types of Constants โ€‹

1. Public Keys (GroupElement) โ€‹

typescript
const contract = `
{
  // Spend requires signature from owner
  proveDlog($ownerPk)
}
`;

const compiled = compile(contract, {
  map: {
    ownerPk: userPublicKey  // Hex-encoded group element
  }
});

2. Numbers (Long/Int) โ€‹

typescript
const contract = `
{
  // Box must have at least minimum value
  OUTPUTS(0).value >= $minValue
}
`;

const compiled = compile(contract, {
  map: {
    minValue: 1000000000n  // 1 ERG in nanoERG
  }
});

3. Byte Arrays (Coll[Byte]) โ€‹

typescript
const contract = `
{
  // Only accept specific token
  OUTPUTS(0).tokens(0)._1 == $requiredTokenId
}
`;

const compiled = compile(contract, {
  map: {
    requiredTokenId: Buffer.from(tokenId, "hex")
  }
});

4. Timestamps โ€‹

typescript
const contract = `
{
  // Time-locked: can only spend after unlock time
  CONTEXT.preHeader.timestamp >= $unlockTime
}
`;

const unlockDate = new Date("2025-06-01").getTime();

const compiled = compile(contract, {
  map: {
    unlockTime: BigInt(unlockDate)
  }
});

Real-World Example: Time-Lock Contract โ€‹

typescript
import { compile } from "@fleet-sdk/compiler";
import { 
  TransactionBuilder, 
  OutputBuilder,
  Network 
} from "@fleet-sdk/core";

/**
 * Create a time-locked box that can only be spent after a certain date
 */
function createTimeLockContract(
  ownerPk: string,
  unlockTimestamp: bigint
): string {
  const contract = `
  {
    // Condition 1: Must be after unlock time
    val timeCondition = CONTEXT.preHeader.timestamp >= $unlockTime
    
    // Condition 2: Must be signed by owner
    val ownerCondition = proveDlog($ownerPk)
    
    // Both conditions must be met
    sigmaProp(timeCondition) && ownerCondition
  }
  `;

  const compiled = compile(contract, {
    map: {
      unlockTime: unlockTimestamp,
      ownerPk: ownerPk
    },
    network: Network.Mainnet
  });

  return compiled.toHex();
}

// Usage
const ownerPublicKey = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
const unlockDate = BigInt(new Date("2025-12-31").getTime());

const ergoTree = createTimeLockContract(ownerPublicKey, unlockDate);
console.log("Time-locked contract:", ergoTree);

Multi-Signature Contract โ€‹

typescript
const multiSigContract = `
{
  // 2-of-3 multisig
  val pk1 = $pubKey1
  val pk2 = $pubKey2
  val pk3 = $pubKey3
  
  atLeast(2, Coll(
    proveDlog(pk1),
    proveDlog(pk2),
    proveDlog(pk3)
  ))
}
`;

const compiled = compile(multiSigContract, {
  map: {
    pubKey1: alice.publicKey,
    pubKey2: bob.publicKey,
    pubKey3: carol.publicKey
  }
});

Creating Boxes with Custom Contracts โ€‹

typescript
import { 
  TransactionBuilder, 
  OutputBuilder,
  ErgoAddress 
} from "@fleet-sdk/core";
import { compile } from "@fleet-sdk/compiler";

async function createTimeLockBox(
  inputs: Box<string>[],
  ownerAddress: string,
  lockAmount: string,
  unlockTimestamp: bigint
) {
  // Get owner's public key from address
  const ownerPk = ErgoAddress.fromBase58(ownerAddress).getPublicKeys()[0];
  
  // Compile time-lock contract
  const contract = compile(`
    {
      val unlocked = CONTEXT.preHeader.timestamp >= $unlockTime
      sigmaProp(unlocked) && proveDlog($ownerPk)
    }
  `, {
    map: {
      unlockTime: unlockTimestamp,
      ownerPk: ownerPk
    }
  });

  const currentHeight = 1100000;

  // Create box with custom contract
  const tx = new TransactionBuilder(currentHeight)
    .from(inputs)
    .to(
      new OutputBuilder(
        lockAmount,
        contract.toAddress()  // Address from compiled contract
      )
    )
    .sendChangeTo(ownerAddress)
    .payFee("1100000")
    .build();

  return tx;
}

Constants vs Registers โ€‹

FeatureCompile Time ConstantsRegisters
When setAt contract compileAt box creation
ModifiableNo (baked into ErgoTree)No (but can create new box)
SizePart of contractPart of box
Use caseContract configurationBox-specific data
AccessDirect in contractSELF.R4, etc.
typescript
// Compile time constant - same contract for all users
const contractWithConstant = `
{
  OUTPUTS(0).value >= $minAmount  // minAmount is fixed
}
`;

// Register - different value per box
const contractWithRegister = `
{
  val minAmount = SELF.R4[Long].get  // minAmount from box register
  OUTPUTS(0).value >= minAmount
}
`;

Factory Pattern โ€‹

Create multiple contract instances from one template:

typescript
class ContractFactory {
  private template: string;

  constructor() {
    this.template = `
    {
      val owner = $ownerPk
      val minValue = $minValue
      
      val correctOwner = proveDlog(owner)
      val sufficientValue = OUTPUTS(0).value >= minValue
      
      correctOwner && sigmaProp(sufficientValue)
    }
    `;
  }

  createForUser(userPk: string, minValue: bigint): string {
    const compiled = compile(this.template, {
      map: {
        ownerPk: userPk,
        minValue: minValue
      }
    });
    return compiled.toHex();
  }
}

// Usage
const factory = new ContractFactory();

const aliceContract = factory.createForUser(alicePk, 1000000000n);
const bobContract = factory.createForUser(bobPk, 2000000000n);

// Different ErgoTrees, same logic!
console.log("Alice:", aliceContract);
console.log("Bob:", bobContract);

Best Practices โ€‹

1. Validate Constants Before Compiling โ€‹

typescript
function createContract(ownerPk: string, amount: bigint) {
  // Validate inputs
  if (!ownerPk || ownerPk.length !== 66) {
    throw new Error("Invalid public key");
  }
  if (amount <= 0n) {
    throw new Error("Amount must be positive");
  }

  return compile(template, {
    map: { ownerPk, amount }
  });
}

2. Use Descriptive Names โ€‹

typescript
// โœ… Good: Clear intent
const contract = `{ proveDlog($recipientPublicKey) }`;

// โŒ Bad: Unclear
const contract = `{ proveDlog($pk) }`;

3. Document Constants โ€‹

typescript
/**
 * Vesting Contract
 * 
 * Constants:
 * - $beneficiaryPk: Public key of token recipient
 * - $vestingStart: Timestamp when vesting begins
 * - $vestingPeriod: Duration in milliseconds
 * - $totalAmount: Total tokens to vest
 */
const vestingContract = `{
  // ... contract logic
}`;

Test Example โ€‹

typescript
import { describe, it, expect } from "vitest";
import { compile } from "@fleet-sdk/compiler";
import { Network } from "@fleet-sdk/core";

describe("Compile Time Constants", () => {
  it("should inject public key into contract", () => {
    const contract = `{ proveDlog($ownerPk) }`;
    const publicKey = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
    
    const compiled = compile(contract, {
      map: { ownerPk: publicKey },
      network: Network.Mainnet
    });

    expect(compiled.toHex()).toBeTruthy();
    expect(compiled.toHex().length).toBeGreaterThan(0);
  });

  it("should inject numeric values", () => {
    const contract = `{ sigmaProp(OUTPUTS(0).value >= $minValue) }`;
    
    const compiled = compile(contract, {
      map: { minValue: 1000000000n },
      network: Network.Mainnet
    });

    expect(compiled.toHex()).toBeTruthy();
  });
});

Next Steps โ€‹

Released under the MIT License.