Skip to content

Creating custom Casts

Libs edited this page May 16, 2022 · 3 revisions

TxForge ships with a collection of built in casts that are great for most common transaction use cases. However, as an adventurous and ambitious apprentice in the art of crafting transactions, you'll be pleased to hear you have access to all the tools you need to start building any kind of transaction template you can imagine.

Cast API

Creating a Cast is a case of defining a class that extends from Cast, and defining the locking and unlocking scripts using the Tape API.

import { Cast } from 'txforge'

class MyPuzzle extends Cast {
  init(params, opts) {
    // a useful hook to validate and/or coerce parameters
  }

  lockingScript(params, opts) {
    // define the locking script
  }

  unlockingScript(params, opts) {
    // define the unlocking script
  }
}

init(params, opts)

The init() function is called after a Cast is instantiated. It provides a hook where parameters can be validated and values coerced.

init(params) {
  if (typeof params.address === 'string') {
    this.params.address = Address.fromString(params.address)
  }
  if (this.mode === 'lock' && !this.params.address) {
    throw new Error('address param is required')
  }
}

lockingScript(params, opts)

The lockingScript() function is used to build the locking script on this.script using the Tape API.

lockingScript({ address }) {
  this.script
    .push(OP_DUP)
    .push(OP_HASH160)
    .push(address.pubkeyhash)
    .push(OP_EQUALVERIFY)
    .push(OP_CHECKSIG)
}

unlockingScript(params, opts)

The unlockingScript() function is used to build the unlocking script on this.script using the Tape API.

unlockingScript({ privkey }) {
  this.script
    .apply(signTx, [privkey])
    .push(privkey.toPublicKey())
}

Examples

Math puzzle

This example locks satoshis to a number (the answer). The satoshis can then be unlocked by specifying two numbers (a and b) that when added together equal the answer.

Putting aside the obvious flaw that coins locked to a puzzle like this can be spent by anyone who can add two numbers together, it's a nice example to demonstrate the basic anatomy of a Cast.

import { Cast, helpers } from 'txforge'
import nimble from '@runonbitcoin/nimble'

const { num } = helpers
const { OP_ADD, OP_EQUAL } = nimble.constants.opcodes

class MathPuzzle extends Cast {
  lockingScript({ answer }) {
    this.script
      .push(OP_ADD)
      .push(num(answer))
      .push(OP_EQUAL)
  }

  unlockingScript({ a, b }) {
    this.script
      .push(num(a))
      .push(num(b))
  }
}

The following shows satoshis being locked and then spent using the MathPuzzle Cast.

import { forgeTx, getUTXO } from 'txforge'

const tx1 = forgeTx({
  inputs: [...],
  outputs: [
    MathPuzzle.lock(10000, { answer: 21 })
  ]
})

const utxo = getUTXO(tx1, 0)

const tx2 = forgeTx({
  inputs: [
    MathPuzzle.unlock(utxo, { a: 9, b: 12 })
  ],
  outputs: [...]
})

Hash puzzle

A hash puzzle involves locking satoshis to the hash of a secret. The coins can be unlocked by proving that you know that secret.

import { Cast } from 'txforge'
import nimble from '@runonbitcoin/nimble'

const { OP_EQUAL, OP_SHA256 } = nimble.constants.opcodes
const { sha256, sha256d } = nimble.functions

class HashPuzzle extends Cast {
  lockingScript({ secret }) {
    this.script
      .push(OP_SHA256)
      .push(sha256d(secret))
      .push(OP_EQUAL)
  }

  unlockingScript({ secret }) {
    this.script
      .push(sha256(secret))
  }
}

The following demonstrates how the HashPuzzle Cast is used to lock and unlock coins.

import { forgeTx, getUTXO } from 'txforge'

const secret = 'My Secret Password!!!1'

const tx1 = forgeTx({
  inputs: [...],
  outputs: [
    HashPuzzle.lock(10000, { secret })
  ]
})

const utxo = getUTXO(tx1, 0)

const tx2 = forgeTx({
  inputs: [
    HashPuzzle.unlock(utxo, { secret })
  ],
  outputs: [...]
})