Skip to content

Using CashScript with CashConnect

Introduction

The previous chapter only demonstrated sending using CashConnect. But, for msot real-world use-cases, you'll also need to unlock those UTXOs that you create.

The below will demonstrate how to do this using Cashscript.

Defining the contract

We're going to use a very simple (and relatively useless) example: A password protected Smart Contract that must be unlocked using the Password "pass":

ts
pragma cashscript ~0.12.0;

contract Password() {
    function unlock(bytes password) {
        require(password == bytes("pass"), "Password must be pass");
    }
}

The @cashconnect-js/templates-dev package then has a utility class to help convert this into template format.

import { CashScript } from '@cashconnect-js/templates-dev';
import { encodeExtendedJson } from '@cashconnect-js/core/primitives';

const CASHSCRIPT_CONTRACT = `pragma cashscript ~0.12.0;

contract Password() {
    function unlock(bytes password) {
        require(password == bytes("pass"), "Password must be pass");
    }
}`;

// Compile to Template Format.
const scripts = {
  ...CashScript.fromSource(CASHSCRIPT_CONTRACT).toScripts({ debug: true })
};

// Pretty print the scripts in Template format.
console.log(encodeExtendedJson(scripts, 2));

INFO

Note that the resulting script format is very similar to LibAuth Template format but, if debug is set, CashScript sourcemaps will also be included (The BlockchainTest util has partial support for these).

Building the template

Let's build a template using the above code and CashScript utils:

import { CashScript, Instructions, TestEnv, Vars, expr } from '@cashconnect-js/templates-dev';
import { encodeExtendedJson } from '@cashconnect-js/core/primitives';

const CASHSCRIPT_CONTRACT = `pragma cashscript ~0.12.0;

contract Password() {
    function unlock(bytes password) {
        require(password == bytes("pass"), "Password must be pass");
    }
}`;

const PASSWORD_TEMPLATE = {
  name: 'Password Example',
  description: 'Password Example',
  actions: {
    lock: {
      params: {},
      instructions: [
        Instructions.transaction({
          name: 'lockTx',
          inputs: ['*'],
          outputs: [
            {
              lockingBytecode: '#Password.lock',
              valueSatoshis: 10_000n,
            },
            '*',
          ],
        }),
      ],
      returns: {
        lockTxHash: Vars.transactionHash('Lock Tx Hash'),
      },
    },
    unlock: {
      params: {
        lockTxHash: Vars.transactionHash('Lock Tx Hash'),
        password: Vars.string('Password'),
      },
      instructions: [
        Instructions.transaction({
          name: 'unlockTx',
          inputs: [
            {
              outpointTransactionHash: expr`lockTxHash`,
              outpointIndex: 0,
              sequenceNumber: 0,
              unlockingBytecode: '#Password.unlock.unlock',
            },
          ],
          outputs: ['*'],
        }),
      ],
      returns: {
        unlockTxHash: Vars.transactionHash('Unlock Tx Hash'),
      },
    },
  },
  scripts: {
    ...CashScript.fromSource(CASHSCRIPT_CONTRACT).toScripts({ debug: true }),
  },
}

// Let's pretty print the template JSON.
console.log(encodeExtendedJson(PASSWORD_TEMPLATE, 2));

There are a few things to note in the above:

  1. We now have two actions: one for locking the contract and one for unlocking the contract.
  2. We use a Util from the @cashconnect-js/templates-dev package to add the Locking/Unlocking scripts to our global list of scripts in the template.
  3. We set a debug: true property so that we can get CashScript Source Maps when using with out BlockchainTest instance later

DANGER

Some of the above will be refactored when an outpoint variable type is supported. See Roadmap.

Testing the Template

As with the previous chapter, let's now create a TestEnv to test our template.

import { Bytes } from '@cashconnect-js/core/primitives';
import { CashScript, Instructions, TestEnv, Vars, expr, } from '@cashconnect-js/templates-dev';
import { encodeExtendedJson } from '@cashconnect-js/core/primitives';

const CASHSCRIPT_CONTRACT = `pragma cashscript ~0.12.0;

contract Password() {
    function unlock(bytes password) {
        require(password == bytes("pass"), "Password must be pass");
    }
}`;

const PASSWORD_TEMPLATE = {
  name: 'Password Example',
  description: 'Password Example',
  actions: {
    lock: {
      params: {},
      instructions: [
        Instructions.transaction({
          name: 'lockTx',
          inputs: ['*'],
          outputs: [
            {
              lockingBytecode: '#Password.lock',
              valueSatoshis: 10_000n,
            },
            '*',
          ],
        }),
      ],
      returns: {
        lockTxHash: Vars.transactionHash('Lock Tx Hash'),
      },
    },
    unlock: {
      params: {
        lockTxHash: Vars.transactionHash('Lock Tx Hash'),
        password: Vars.string('Password'),
      },
      instructions: [
        Instructions.transaction({
          name: 'unlockTx',
          inputs: [
            {
              outpointTransactionHash: expr`lockTxHash`,
              outpointIndex: 0,
              sequenceNumber: 0,
              unlockingBytecode: '#Password.unlock.unlock',
            },
          ],
          outputs: ['*'],
        }),
      ],
      returns: {
        unlockTxHash: Vars.transactionHash('Unlock Tx Hash'),
      },
    },
  },
  scripts: {
    ...CashScript.fromSource(CASHSCRIPT_CONTRACT).toScripts({ debug: true }),
  },
}

// Instantiate a Test Environment.
const testEnv = await TestEnv.from(PASSWORD_TEMPLATE);

// Create Wallet A that will execute this action.
const { wallet, templateInstance } = await testEnv.createWalletP2PKH({
    // Load the Wallet with 1 BCH.
    satsBalance: 100_000_000n
});

// Execute the Lock Action on our Wallet's Template Instance.
const lockResult = await templateInstance.executeAction('lock', {});

// Broadcast the transaction(s) using our Mock Blockchain.
await testEnv.blockchain.broadcastTransactions(lockResult.transactions);

// Print the data returned by the Lock Action.
console.log('Lock Data', encodeExtendedJson(lockResult.data, 2));

// Execute the unlock action.
const unlockResult = await templateInstance.executeAction('unlock', {
  // PAY ATTENTION TO THIS LINE.
  ...lockResult.data,
  password: Bytes.fromUtf8('pass'),
});

// Broadcast the unlock action.
await testEnv.blockchain.broadcastTransactions(unlockResult.transactions);

// Print the data returned by the Unlock Action.
console.log('Unlock Data', encodeExtendedJson(unlockResult.data, 2));

In the above, we are first executing an action to create the Password Protected UTXO and then following up with an action to Unlock/Spend it.

Note that in the unlock, we spread the returned data of the Lock Action into the parameters of the Unlock Action. This is a pattern that will likely be repeated often in CashConnect as it the recommended way to model and handle Contract Systems. We will be diving into this in the next chapter on "Modeling Contract Systems".