Skip to content

First Template

Introduction

CashConnect templates are JSON schemas that have a form like the following:

ts
{
  name: 'Example Template',
  description: 'This is an example template',
  actions: {
    // ... methods that can be invoked.
  },
  scripts: {
    // ... global scripts that are re-used by Actions.
    //     E.g. Lock/Unlock scripts.
  }
}

Items listed under actions can be thoguht of as the Public API of the template and, conceptually, are treated like functions in any other language: They have:

  • params - data that is passed in
  • instructions - pipeline of instructions to perform (analogous to function bodies)
  • returns - data that is returned once executed

Defining an Action

Let's create a simple action that just sends sats to the provided address ("Locking Bytecode").

INFO

We will be using some tooling from the @cashconnect-js/templates-dev package to improve legibility, but this is not required: Templates can be written in pure JSON for those who might prefer it.

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

const EXAMPLE_TEMPLATE = {
  name: 'Example Template',
  description: 'A Simple Send Example',
  actions: {
    send: {
      params: {
        'recipientLockingBytecode': Vars.address('Recipient Locking Bytecode'),
        'recipientSatsAmount': Vars.satoshis('Amount to send (Sats)'),
      },
      instructions: [
        Instructions.transaction({
          name: 'tx',
          // Use any available inputs from parent wallet.
          inputs: ['*'],
          outputs: [
            {
              // NOTE: `expr` is a CashScript-like transpiler that transpiles down to CashASM.
              lockingBytecode: expr`recipientLockingBytecode`,
              valueSatoshis: expr`recipientSatsAmount`,
            },
            // Append any change outputs to parent wallet.
            '*'
          ]
        })
      ],
      returns: {
        txHash: Vars.transactionHash('Transaction Hash')
      }
    }
  },
  scripts: {},
}

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

INFO

Click "Run Code" above to see the JSON-serialized template.

Testing the Action

Having to test each change to the template by establishing a new Wallet Session would be very painful. CashConnect ships with some tools to ease this and allow automated tests to be created.

If using CashScript (see next chapter), this tooling will also output debugging information (which CashScript line failed, etc).

Let's try executing the above action in a test environment.

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

const EXAMPLE_TEMPLATE = {
  name: 'Example Template',
  description: 'A Simple Send Example',
  actions: {
    send: {
      params: {
        'recipientLockingBytecode': Vars.address('Recipient Locking Bytecode'),
        'recipientSatsAmount': Vars.satoshis('Amount to send (Sats)'),
      },
      instructions: [
        Instructions.transaction({
          name: 'tx',
          // Use any available inputs from parent wallet.
          inputs: ['*'],
          outputs: [
            {
              // NOTE: `expr` is a CashScript-like transpiler that transpiles down to CashASM.
              lockingBytecode: expr`recipientLockingBytecode`,
              valueSatoshis: expr`recipientSatsAmount`,
            },
            // Append any change outputs to parent wallet.
            '*'
          ]
        })
      ],
      returns: {
        txHash: Vars.transactionHash('Transaction Hash')
      }
    }
  },
  scripts: {},
}

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

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

// Create Wallet B that will receive the sats.
const { wallet: walletB } = await testEnv.createWalletP2PKH();

// Execute the Send Action on our Wallet's Template Instance.
const sendResult = await walletATemplateInstance.executeAction('send', {
    recipientLockingBytecode: (await walletB.getReceivingAddress()).toLockscriptBytes(),
    recipientSatsAmount: Bytes.fromBigInt(50000n),
});

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

// Print the balances of Wallet A and Wallet B.
console.log('Wallet A balance:',  await walletA.getBalanceSats());
console.log('Wallet B balance:',  await walletB.getBalanceSats());

Trying the Action in a Wallet

Now that we've tested our action, let's try executing it in our wallet.

First, we have to add some meta information to our action. This is what will display to the user.

ts
{
  meta: {
    title: 'Send',
    description: 'Send {{ <recipientSatsAmount> | satoshis }} to {{ <recipientLockingBytecode> | address }}',
  },
  // ...
}

DANGER

This is currently being refactored to support control flows (e.g. looping over a list of recipients). The above is for demonstration only.

Let's then use the CashConnectDappConsole UI to establish a session with our wallet and execute the action.

import { BlockchainElectrum, Instructions, Vars, expr } from '@cashconnect-js/templates-dev';
import { Address, Bytes } from '@cashconnect-js/core/primitives';
import { CashConnectDappConsole } from '@cashconnect-js/dapp';

const EXAMPLE_TEMPLATE = {
  name: 'Example Template',
  description: 'A Simple Send Example',
  actions: {
    send: {
      meta: {
        title: 'Send',
        description: 'Send {{ <recipientSatsAmount> | satoshis }} to {{ <recipientLockingBytecode> | address }}',
      },
      params: {
        'recipientLockingBytecode': Vars.address('Recipient Locking Bytecode'),
        'recipientSatsAmount': Vars.satoshis('Amount to send (Sats)'),
      },
      instructions: [
        Instructions.transaction({
          name: 'tx',
          // Use any available inputs from parent wallet.
          inputs: ['*'],
          outputs: [
            {
              // NOTE: `expr` is a CashScript-like transpiler that transpiles down to CashASM.
              lockingBytecode: expr`recipientLockingBytecode`,
              valueSatoshis: expr`recipientSatsAmount`,
            },
            // Append any change outputs to parent wallet.
            '*'
          ]
        })
      ],
      returns: {
        txHash: Vars.transactionHash('Transaction Hash')
      }
    }
  },
  scripts: {},
}

// Instantiate a Blockchain Connection using a Fulcrum Chipnet Server.
const blockchain = await BlockchainElectrum.from({
    servers: ['chipnet.imaginary.cash'],
    // We are executing in an IFrame that is never visible.
    disableBrowserVisibilityHandling: true,
});

// Instantiate our CashConnect Dapp (using Chipnet).
const dapp = await CashConnectDappConsole.from(EXAMPLE_TEMPLATE, 'bch:bchtest');

// Request a session with the wallet.
// Copy+Paste the URL in the console into your wallet.
await dapp.newSession();

// Define the address we should send to.
const recipientAddress = Address.from('bitcoincash:qpkgeeesjf72sazuwmhe69hxk40dlupkpyn28w07f2');

// Execute the Send Action on our Wallet's Template Instance.
const sendResult = await dapp.executeAction('send', {
    recipientLockingBytecode: recipientAddress.toLockscriptBytes(),
    recipientSatsAmount: Bytes.fromBigInt(50000n),
});

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

// Print a link to the transaction.
console.log(`View data here: https://chipnet.chaingraph.cash/tx/${Bytes.from(sendResult.data.txHash).toHex()}`);

What's happening under the hood?

Template Instances are stand-alone. Although some Transports may have additional methods (e.g. cancelRequest for WalletConnect) and may also inject specific context-level variables (e.g. __domain), for the most part, they just serve as proxies for executeAction on the templates.

A quick sequence diagram is given below to illustrate a typical flow.

sequenceDiagram
    Dapp (Frontend)->>+Wallet: Send Template/Session Parameters
    Note over Dapp (Frontend),Wallet: Assuming WC-like Transport here where there's a session.<br/>But HTTP-like transport would be similar...<br/>it would just include template/executeAction in single call instead.
    Wallet->>+Wallet: User Views/Approves
    Wallet->>Dapp (Frontend): Return Session Details
    Dapp (Frontend)->>+Wallet: executeAction(someAction, params)
    Note over Dapp (Frontend),Wallet: executeAction on the Dapp-end behaves like a proxy<br/>It simply forwards the action/params to the Wallet to execute.
    Wallet->>+Wallet: executeAction is executed internally with the given Parameters
    Wallet-->>+Wallet: In future, some wallets might store params, outputs, etc.<br/> (But this is not compulsory)
    Wallet->>Dapp (Frontend): Result of action (data) execution is sent back to Dapp.

Next chapter we'll look at how to use CashScript Contracts with CashConnect.