Skip to content

Vision to Reality 6: Writing Templates

Templates provide the instructions necessary for an application to make transactions and interact with the Bitcoin Cash blockchain. They are used in virtually all usecases in the XO ecosystem and are relevant for all participants, ranging from developers to end users.

In this article, we will dig deep into the art of template writing. We will cover all nooks and crannies of a template, make recommendations for best practices and give new template authors a good place to start their XO journey.

This article builds on the previous p2pkh request article in the Vision to Reality series, as well as the state and ownership and concepts articles.


mermaid
mindmap
  root )Responsibilities(

    [INVITATIONS]
      (Connects)
        📱 Apps
        👛 Wallets
        👥 Users
      (Coordinates)
        ⚡ Actions
        🧩 Variables
        📥 Inputs
        📤 Outputs

    [TEMPLATES]
      (Describes)
        🧩 Variables
        💸 Transactions
        📥 Inputs
        📤 Outputs
        🏷️ Lockscripts
      (Defines)
          ⚡ Actions
          💸 Transactions
          💾 Data

    [ENGINE]
      (Generates)
        🔑 Keys
        🪪 Identities
        ✉️ Invitations
        💸 Transactions
      (Manages)
          🤫 Secrets
          🧩 Variables
          📤 Outputs
          🏷️ Lockscripts

Where to start

Top to bottom

One way to write templates is to start at the top level and describe the user experience by defining the actions first, then provide one level deeper at a time until you reach the bottom of the stack and fill in the scripts section.

This is a good way to learn the template structures and both intuitive and convenient when you work with a fixed well-known locking script, such as if you already have a cashscript compiled contract you want to encapsulate in an XO template.

Bottom to top

Another way to write templates is to start with the underlying locking scripts first, then build up one layer at a time.

This is better suited for AI/LLM generation when you do not have a fixed locking script, as changing the locking script later can introduce a large number of inconsistencies in already generated variables, names, descriptions and other fields.

Best practices

Know your users first

When writing templates, you are practically combining the underlying technical operation of your usecase with user-facing components.

The reason for this unification is that XO is fundamentally about resolving trust issues, and so we cannot afford to allow any foreign or third parties to be responsible for the source of truth relating to XO activity. This means that even though many other fields might consider it a bad practice to embed end-user interface material in the underlying technical components, for XO it is a technical requirement to do so.

When your template is in the late stage and you are considering finalizing it, be sure to consider what your potential users might see when using the template. Go through the template from their point of view, making sure the names, descriptions, available actions and follow-up actions all line up in a way that is intuitive and helpful for those users.

You can find lots of documentation on this elsewhere by looking for design guidelines or design best practices.

As an example, here are some personas we use during the development of the AnyHedge template:

# Hedge

Alice is a contractor hedging future tax obligations.

- Wants a **pure 1× hedge** (no extra leverage).
- Primary fields she cares about: 'nominalUnits' (amount she needs to protect), 'maturityTimestamp' (tax deadline), and payout address.
- Should see only user-friendly text: "Create hedge position", "Your hedge position in AnyHedge contract #X7FR1", "If price falls below this, your hedge position will settle early".

# Short

Bob is a short-term BCH trader expecting a price drop.

- Wants **leveraged short exposure**.
- Primary fields he cares about: 'shortInputSats' (amount to speculate with), 'shortLeverage' (risk/reward amplification), 'maturityTimestamp' (how long to place the bet).
- He also cares about 'highLiquidationPrice' (risk for when he loses the bet early).
- Sees: "Create short position", "Opened short position at … USD/BCH".

# Long

Carol is a long-term BCH believer with sufficient liquidity to make long-duration bets.

- Wants **long exposure**.
- Primary fields she cares about: 'longInputSats' (amount to speculate with), 'longLeverage' (risk/reward amplification).
- She also cares about 'lowLiquidationPrice' (risk for when she loses the bet early).
- Sees: "Create long position", "Your long position in AnyHedge contract #X7FR1", "If price rises above this, your long position is liquidated".

Names and descriptions

Names and descriptions in templates help users make sense of their wallet history and are also used to generate user interfaces for the usecases the templates support. Templete authors get a good intuition for how to write names and descriptions as they get more experience, but for someone just starting out there are some best practices that we can recommend.

Write for the end-user

Write in a positive and human-readable way. Keep both names and descriptions short and avoid technical identifiers and jargon when possible. This makes your template usable by a larger number of people, while application developers and other technically skilled users can refer to the full content of the template for details.

When applicable, write both generic/role-less content, as well as role-specific content under the respective roles section.

When writing names and descriptions that apply to specific roles, write from the expected user's perpective. For role-less or generic content, write in a neutral way that does not imply ownership, participation or make other assumptions about the reader.

Use variables and CashASM dynamic evaluations as needed to make descriptions concise, human-readable, accurate and useful.

Sometimes it is helpful to define a constant in starting or follow-up actions that you can then use to provide accurate descriptions. For example, in AnyHedge you could set a positionType to hedge, short or long and use that in dynamic CashASM evaluation. This is particularly useful when you need dynamic evaluation before any user-supplied variables are available.

Technical explanations are best kept in code comments or in bundled documentation, rather than in names and descriptions.

When using a service provider

If your usecase requires a service provider or a 3rd party for additional coordination, you might want to write a template that does not have any start actions. This allows you to use the templating system and gives users the additional safety and clarity that comes with the templates, but requires the users to use a service provider to initiate actions.

If possible, write your template so that you minimize any trust relationships with service providers and make it possible for alternative service providers. This makes your template more portable and allows third parties to integrate with your services, rather than directly compete for your users.

In order to avoid user confusion and tracking of irrelevant history or resources you will often want to mark some outputs and locking scripts with relevant set to false, balance set to 0 and selectable set to false. These can be set on a per-role basis, such that each of your service and your user only sees the parts they specifically care about

For additional clarity, you can explicitly list your service provider as a party in actions with the participants requirement.

Example template sections

Metadata

In each template's metadata there is a link to a template schema intended to be used for validation. This field is currently unused as the template structures are being refined and worked out. Validation can still be done with XO utilities and we will cover those in a future article. When looking at templates you might sometimes find it pointing to libauth's wallet-template schema, as that is what XO originally used as the starting point and inspiration. Once the template structures in XO have matured, we will publish an XO schema such that validation can be done with standardized tools.

ts
$schema: 'https://libauth.org/schemas/wallet-template-v0.schema.json',

Templates have names, descriptions and icons that give users a good idea of what the template is for and help provide a user friendly identity in places like template lists, during template import and when explaining the context of actions, transactions and assets.

ts
name: 'Wrapped BCH',
description: 'Convert between BCH and wBCH tokens.',
icon: 'wrap',

In the future we expect to add versioning support so that users can migrate from one version of a template to an upgraded template. We also expect localization support to target specific versions.

No version related functionality is currently implemented, but we recommend setting a version number and simply incrementing it so that you are ready when new version related features get implemented.

ts
version: '1',

Each template must list at least one CashVM version that can safely execute the template instructions. It is perfectly safe to just list the current version here, but for best compatibility with XO wallets and applications that might not otherwise be up-to-date, it's a good idea to list all versions that your usecase is designed to support. See libauths documentation more detail and the BchVmVersions in the XO types repository for a list of supported versions.

ts
supported: ['BCH_2023_05', 'BCH_2024_05', 'BCH_2025_05', 'BCH_2026_05'],

As an optional feature you can also specificy a set of resources that this template relates to. For example, you can add links to websites that provide documentation, support or more information on the templated usecase.

ts
resources: [
	{
		name: 'Official Website',
		description: 'Official homepage for the wBCH token.',

		url: 'https://wrapped.cash/',
	}
],

Roles

All usecases a template can support involve at least one participant. You can describe each participant of your usecase as a role which can help users better understand what value or responsibilities they can expect from their participation. Sometimes you might also want to define roles for services or 3rd party data providers necessary for your usecase in order to make their participation clear to other users.

Roles are likely to be displayed in many contexts. They are sometimes shown in future context, for example when listing possible roles for an upcoming action to take, but they are also sometimes shown in past context, such as when looking at your role in an output or transaction.

It is best to keep the role names and descriptions short and neutral.

ts
name: 'Wrapper',
description: 'The person wrapping BCH into wBCH.',
icon: 'user',

Actions

The main way to access the functionality in a template is through actions. You can describe each action separately, with a name, description and icon.

Action names can appear as buttons when inspecting an output/asset or when viewing template-level starting actions, so should be kept short in order to keep the size of buttons manageable.

Descriptions should be more explanatory and can be longer, but should fit comfortably in a tooltip or in a single-line explanation. Both name and description should be verb-oriented in future or present tense, written from the user's perspective.

If there is a need to use dynamic action names and description, remember that variables provided by users happen after they have seen this description, so you should ensure that such actions are strictly follow-up actions and that the required state has been explicitly propagated where that follow-up action is made available.

ts
name: 'Wrap BCH',
description: 'Convert BCH into wBCH tokens.',
icon: 'wrap',

In XO templates you need to list all required variables and secrets directly in each action. If any data is missing when the engine tries to complete an action, it will throw an error. Such failures will not update any state nor will they free any previously reserved resources.

In most cases, you will want to define the requirements on a per-role basis, but it is also possible to define generic requirements that the initiator of an action, or the party wishing to finalize the action, can volunteer to provide.

It is currently not possible to validate that all required parameters are present since actions can be invoked from many different places and not all parameters are necessary in all possible invocations.

ts
roles: {
  service: {
    requirements: {
      variables: ['direction', 'poolSatoshis', 'poolTokens'],
    },
  },
  wrapper: {
    requirements: {
      variables: ['amountToWrap', 'recipientLockingScript'],
    },
  },
},

In addition to variable and secret requirements, you should also specify how many participants are required in the action. If you set minimum to 0, that role becomes optional. If you set a maximum value over 1, then the action has multiple role slots and can support multiple participants taking on the same role, where each participant needs to satisfy their own variable and secrets requirements. In some case, you can omit the maximum value, in which case the template will support an unlimited number of participants with the specified role. For example, when building a fundraising platform, you might not know how many participants there will be.

ts
requirements: {
  participants: [
    { role: 'service', slots: { min: 1, max: 1 } },
    { role: 'wrapper', slots: { min: 1, max: 1 } }
  ],
},

Finally, when an action is completed it results in some technical data or state change. For now, there are only two possible results in XO: data and transaction.

In almost all cases you will want a transaction to happen on-chain, and you do this by referencing a transaction to create in your action.

Note that transaction outputs are the natural state-holders in the XO ecosystem, and so if you make an action that returns some data instead, such as a signature, then that is currently considered ephemeral and no persistent state is created. Usage of data results is currently a work in progress, and might change signficantly in the future.

ts
transaction: 'wrapTransaction',

Transactions

Transactions at their core are just a list of inputs to spend and outputs to make.

The ordering of inputs and outputs in a transaction is done by specifying their respective indexes. In most cases, you will want to set fixed indexes so that you have a predictable and reliable transaction shape.

If your usecase has many different transactions, it is recommended to use consistent ordering of your outputs to make manual audits easier, and to reduce the need for refactoring in future updates.

If your locking scripts are built to work without fixed transaction shapes, you can omit the input or output index to make the transaction less rigid and allow for broader composition in the future.

In most cases you also don't need to specify inputs that exist solely to cover a cost or requirement in a transaction. This can instead be handled by coin-selection, either automatically by the engine, or by the requesting application. Sometimes though, there are strict requirements for the inputs that can not be automatically satisfied by coin selection, and in those cases you do need to specify the details of the input specifically (and either have a 3rd party service select the appropriate UTXOs, or list them explicitly in follow-up actions).

You also do not need to define change outputs under most conditions. When an input is selected or provided, the engine determines if this will result in excess value, and will automatically create a change output if possible. The main case where you would want to define the change outputs in a template is when multiple participants exist and some should receive change, while others should intentionally not.

Transactions can be shown in history entries, transaction detail views and action confirmation dialogs. As such, transaction names and descriptions are best written in past tense and are expected to use various transaction details to provide dynamic userfriendly context.

ts
name: 'Wrapped BCH',
description: 'Wrapped $(<amountToWrap> <satoshisPerBCH> OP_DIV).$(<amountToWrap> <satoshisPerBCH> OP_MOD) BCH into wBCH tokens.',
icon: 'wrap',

inputs: [
	{ input: 'covenantInput', inputIndex: 0 },
],
outputs: [
	{ output: 'covenantOutput', outputIndex: 0 },
	{ output: 'wrappedTokensOutput', outputIndex: undefined },
],

In addition to inputs and outputs, transactions can also have a specified locktime. Unless your usecases requires a locktime, you can safely omit this field. If your usecase does require a locktime, see the Bitcoin Cash protocol documentation for how to use this field correctly, and be aware that there are many edge-cases and broken intuitions around the locktime field and how it interacts with the blockchain, real-world time and the relevant introspection opcodes.

ts
locktime: 0,

There is also a version field in transactions which you can omit, and it will be set to 2 automatically.

ts
version: 2,

Inputs and Outputs

Inputs and outputs can be displayed in asset details, transaction detail views and as resources to use in invitations. Be sure to make good use of the output state and CashASM to provide clear, descriptive noun phrases.

If possible, include units and amounts directly in the description, for example "15 FURU tokens".

The main reasons to define an input in a template, is to provide a specific unlocking bytecode to use and to provide better user documentation. When inputs are not defined, and instead provided as part of coin selection, they instead use their defaultUnlockingBytecode from their lockingScript.

In addition to the unlockingBytecode and the name, description and icon, inputs can also set their intended satoshis and token values. This might seem odd since the blockchain does not validate these values, but when specified here the engine will do additional validation of these values before accepting any UTXO to satisfy this input.

ts
name: 'wBCH Covenant',
description: 'The covenant being updated.',
icon: 'contract',

valueSatoshis: '$(poolSatoshis)',
token: {
	category: '$(<wbchTokenCategory>)',
	amount: '$(poolTokens)',
	nft: null,
},

unlockingBytecode: 'wrapBCHUnlockingBytecode',

Outputs define how much satoshis and tokens to lock with a specific locking script in the transaction.

Note that in both outputs and inputs, all satoshi and token values can be dynamically evaluated with CashASM if needed.

ts
name: 'Wrapped wBCH',
description: 'Wrapped $(<amountToWrap> <satoshisPerBCH> OP_DIV).$(<amountToWrap> <satoshisPerBCH> OP_MOD) wBCH tokens.',
icon: 'receive',

valueSatoshis: '<tokenDust>',
token: {
	category: '$(<wbchTokenCategory>)',
	amount: '$(<amountToWrap>)',
	nft: null,
},

lockingScript: '$(<recipientLockingScript>)',

Outputs can also encode an estimated ownership value on a per-role basis. This is used to inform users how much of an output is expected to belong to them at any given time. When there are multiple role slots, dynamic evaluation uses the variables provided by the relevant slot. If no estimated ownership values are provided the engine assumes that none of the value on the output is owned by a user.

With this information, it becomes possible to create good asset overviews that properly account for contracted output values. Unfortunately, there are some contract constructions that cannot provide meaningful estimations, for example contracts that requires data that does not yet exist, such as oracle information for future events.

The current name of this field, balance, is ambigious and is expected to change in a future update

ts
balance: {
    satoshis: true,
    fungibleTokens: true,
    nonfungibleTokens: true,
},

Some usecases create outputs that are meant for general spending. When this is the case, you should set the selectable property to true to allow it to be used in automatic coin selection, and provide a defaultUnlockingScript reference that defines how to unlock the output. If there is more than one spending path, you can still set a default here, and provide explicit input-defined unlocking conditions in follow-up actions.

It is also possible to set a defaultUnlockingBytecode directly if you don't want to set up and provide a description for the unlocking script.

ts
selectable: true,
defaultUnlockingScript: 'unlockP2PKH'

Locking and Unlocking Scripts

Locking scripts are usually displayed as addresses and can be shown in technical sections such as lists of generated locking scripts, but also in configuration options for default change or receiving addresses, and in output detail views. They can be relevant in both past and future tense, so it is best to keep names and descriptions in short neutral language.

Locking scripts have a lockingType and lockingBytecode. If the lockingType is set to p2pkh or p2sh (pay-to-public-key-hash or pay-to-script-hash), the engine will automatically wrap the locking script in its respective construction, while p2s (pay-to-script) will run the locking bytecode exactly as defined.

Sometimes a transaction is discovered that has outputs to a locking script defined in a template, but the transaction was not coordinated through the XO system. Such unknown outputs can be described and handled via the defaultOutput property. If your template requires specific state or secrets, it can be that such outputs remain unspendable until such time that state or secrets are provided. If no such state or secrets are provided they could remain unspendable forever.

If unknown outputs handled via the defaultOutput property are spendable in your usecase, then it is recommended that you provide the defaultUnlockingBytecode or a list of follow-up actions on the defaultOutput.

Note that the current implementation of the XO engine does not enforce uniqueness with regards to locking bytecodes across different lockingScripts or even different templates. Reuse of locking bytecodes is idempotent, but handling of uncoordinated outputs will require manual intervention in order to link the output to a desired template and to provide any necessary state.

ts
name: 'wBCH Covenant',
description: 'Holds BCH and wBCH tokens that can be freely converted.',
icon: 'contract',

lockingType: 'p2sh',
lockingBytecode: 'wrapBCHLockingBytecode',

// All outputs on the covenent are covenant outputs.
defaultOutput: 'covenantOutput',

// Indicate that users should not index this locking script.
roles: {
	wrapper: { relevant: false },
	unwrapper: { relevant: false },
},

Unlocking scripts are simpler, with a field for unlocking bytecode and a name and description explaining how an input was unlocked.

ts
name: 'wBCH Covenant Usage',
description: 'Unlocks the wBCH covenent locking script so that BCH and wBCH tokens that can be freely converted.',
icon: 'contract',

unlockingBytecode: 'wrapBCHUnlockingBytecode',

Bytecode Scripts

At the heart of the Bitcoin Cash scripting language is the CashVM OP_CODE set. In XO templates, they are used for dynamic evaluation in many contexts, but the scripts section is where you will see their full power.

Practically, scripts is simply a list of CashASM evaluations referenced from other parts of the template, but practically they are used to create the necessary locking and unlocking bytecodes that makes it possible to build transactions.

These scripts are not shown in any user-facing place. Template authors must provide them for a template to be meaningful, and otherwise only auditors are expected to review them.

To ease both audits and debugging, it is recommended that you write these scripts as modular, descriptively named fragments. Each such fragment should use precise, verb-phrase names indicating a singular responsibility and any limitations that cannot be enforced on-chain should be documented clearly.

This is also a major factor in the technical safety of a template, so remember to properly enforce all rules, using introspection if needed.

ts
// Utility to check if we are doing a wrap or unwrap.
checkIfWrapping: '<direction> <"wrap"> OP_EQUAL',

// Utilities to calculate the covenent change satoshis.
wrappedChangeSatoshis: '<poolSatoshis> <amountToWrap> OP_ADD',
unwrappedChangeSatoshis: '<poolSatoshis> <amountToUnwrap> OP_SUB',
covenantChangeSatoshis: 'checkIfWrapping OP_IF wrappedChangeSatoshis OP_ELSE unwrappedChangeSatoshis OP_ENDIF',

// Utilities to calculate the covenent change tokens.
wrappedChangeTokens: '<poolTokens> <amountToWrap> OP_SUB',
unwrappedChangeTokens: '<poolTokens> <amountToUnwrap> OP_ADD',
covenantChangeTokens: 'checkIfWrapping OP_IF wrappedChangeTokens OP_ELSE unwrappedChangeTokens OP_ENDIF',

// NOTE: This is the wrapped.cash covenant and so this bytecode cannot be updated.
// NOTE: This covenant only ensure the security of its own funds, leaving user protection to be done in user space.
enforceCovenantPersists: 'OP_INPUTINDEX OP_OUTPUTBYTECODE OP_INPUTINDEX OP_UTXOBYTECODE OP_EQUALVERIFY',
enforceTokenCategoryPreserved: 'OP_INPUTINDEX OP_OUTPUTTOKENCATEGORY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_EQUALVERIFY',
enforceValueTokenSumConserved: 'OP_INPUTINDEX OP_UTXOTOKENAMOUNT OP_INPUTINDEX OP_UTXOVALUE OP_ADD OP_INPUTINDEX OP_OUTPUTTOKENAMOUNT OP_INPUTINDEX OP_OUTPUTVALUE OP_ADD OP_NUMEQUAL',

// The final lock and unlocking bytecodes.
wrapBCHLockingBytecode: 'enforceCovenantPersists enforceTokenCategoryPreserved enforceValueTokenSumConserved',
wrapBCHUnlockingBytecode: '<wrapBCHLockingBytecode>',

There is one major exception where these best practices do not apply, and that is when you are compiling your bytecode with an external tool such as CashScript. In that case, the expected structure of the scripts section is based on the compiled output, plus any hand-made additional utility scripts.

ts
parameters: '<maturityTimestamp> <startTimestamp> <highLiquidationPrice> <lowLiquidationPrice> <payoutSats> <satsForNominalUnitsAtHighLiquidation> <nominalUnitsXSatsPerBch> <oraclePublicKey> <longLockingScript> <shortLockingScript> <enableMutualRedemption> <longMutualRedeemPublicKey> <shortMutualRedeemPublicKey>',

bytecode: 'OP_13 OP_PICK OP_0 OP_NUMEQUAL OP_IF OP_ROT OP_VERIFY OP_13 OP_ROLL OP_SWAP OP_CHECKSIGVERIFY OP_12 OP_ROLL OP_SWAP OP_CHECKSIGVERIFY OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_DROP OP_1 OP_ELSE OP_13 OP_ROLL OP_1 OP_NUMEQUALVERIFY OP_TXINPUTCOUNT OP_1 OP_NUMEQUALVERIFY OP_16 OP_ROLL OP_16 OP_PICK OP_7 OP_PICK OP_CHECKDATASIGVERIFY OP_14 OP_ROLL OP_14 OP_PICK OP_7 OP_ROLL OP_CHECKDATASIGVERIFY OP_13 OP_PICK OP_8 OP_SPLIT OP_NIP OP_4 OP_SPLIT OP_DROP OP_BIN2NUM OP_DUP OP_0 OP_GREATERTHAN OP_VERIFY OP_13 OP_PICK OP_8 OP_SPLIT OP_NIP OP_4 OP_SPLIT OP_DROP OP_BIN2NUM OP_1SUB OP_NUMEQUALVERIFY OP_13 OP_ROLL OP_4 OP_SPLIT OP_DROP OP_BIN2NUM OP_12 OP_PICK OP_LESSTHAN OP_VERIFY OP_12 OP_PICK OP_12 OP_SPLIT OP_NIP OP_BIN2NUM OP_DUP OP_0 OP_GREATERTHAN OP_VERIFY OP_10 OP_PICK OP_MIN OP_9 OP_PICK OP_MAX OP_13 OP_ROLL OP_4 OP_SPLIT OP_DROP OP_BIN2NUM OP_DUP OP_13 OP_ROLL OP_GREATERTHANOREQUAL OP_VERIFY OP_12 OP_ROLL OP_GREATERTHANOREQUAL OP_OVER OP_11 OP_ROLL OP_1ADD OP_12 OP_ROLL OP_WITHIN OP_NOT OP_BOOLOR OP_VERIFY <0x3405> OP_DUP OP_8 OP_ROLL OP_3 OP_ROLL OP_DIV OP_8 OP_ROLL OP_SUB OP_MAX OP_SWAP OP_7 OP_ROLL OP_2 OP_PICK OP_SUB OP_MAX OP_0 OP_OUTPUTVALUE OP_ROT OP_NUMEQUALVERIFY OP_0 OP_OUTPUTBYTECODE OP_5 OP_ROLL OP_EQUALVERIFY OP_1 OP_OUTPUTVALUE OP_NUMEQUALVERIFY OP_1 OP_OUTPUTBYTECODE OP_4 OP_ROLL OP_EQUAL OP_NIP OP_NIP OP_NIP OP_ENDIF',

Variables and Constants

Constants and variables are mostly shown during the invitation process when there are missing data/resources required from users. Depending on their type field, they will have different presentation to the user, but for the most part expect them to be used in text-input fields. Try to keep names short enough to fit in input labels, while descriptions should fit in one-line descriptions or tooltips.

ts
name: 'wBCH Token Category',
description: 'The official token category for Wrapped BCH.',
type: 'bytes',
value: 'ff4d6e4b90aa8158d39c5dc874fd9411af1ac3b5ed6f354755e8362a0d02c6b3',
ts
name: 'Amount to Wrap',
description: 'How much BCH to convert to wBCH (in satoshis).',
type: 'integer',
hint: 'satoshis',

After writing a template

When you have a template written, the next step is to validate the template and test it to ensure it works as you expect.

Our next article will cover these steps, and show you what tools exist today and how to use them to take your template from a draft to a valid, testable template that can be used to make on-chain transactions.