Skip to content

Vision to Reality: P2PKH Fungible Token Request

In this article, we are going to go over how a templating engine might function in the context of using a P2PKH template to request fungible tokens. We will cover both the expected flow for a user interaction, as well as explaining how the engine gets all data it requires to function.



The Templating System

Engine

At the center of the templating system is an engine that together with templates is responsible for the full lifecycle of lockingscripts, outputs, relevant state and private keys.

Templates

The templates are immutable and only contain detailed descriptions and instructions on how to create and manage blockchain and use-case related data structures, such as transactions and lockscripts.

Templates are organized around actions which defines when and what data to create.

Invitations

When a template action is requested, the engine follows a consistent and re-usable invitation process.

First an invitation is created that defines what template and action has been requested. This invitation is then accepted by entities wanting to participate in that action, including which of the available roles they want take on.

Then, the invitation is extended with any missing information by the participants until it reaches a state where the action defined in the invitation can be executed.

This process is the same regardless of the complexity of the template and allows for both single-user and multi-user participation.

Identity

When we are working with templates and interactions using invitations, it is important that each entity that is providing data or is taking part in an action has a cryptographic identity. This helps provide a clear log and history, allows us to assign blame and to recover when something has gone wrong.

The engine will deterministically create an identity based on the root entropy provided during initialization. If no entropy is provided it will generate a temporary root entropy and will not have a stable identity.

More information on identities will be covered in a future article.

Dynamic content

The templating system described here uses CashASM to provide dynamic descriptions, properties and other values.

This article will not go into depth on how CashASM works, but here's what you need to know:

  • $(expression) evaluates the expression using the BCH VM opcodes, then return the top stack item.
  • <something> push something to the stack.

Here's an example evaluating to BitcoinCash, for reference:

$(<'Bitcoin'> <'Cash'> OP_CAT)

Requesting fungible tokens

We will now go over a very detailed example of how the engine, a template and invitations all go together to make a request for 100.00 MUSD fungible tokens in a way that creates great user experience by keeping the technical complexities confined in the templating system.

Starting the engine

Before the engine can be used, it needs to be initialized.

This process provides the engine an initial root entropy, storage facilities and ways to monitor the bitcoin cash blockchain:

ts
// Create seed.
// NOTE: this should be randomized the first time, then reused when the engine is setup.
const seed = '<some random stuff>';

// Set up a way to monitor block chain status
const blockchainMonitor =
  '<some code to set up a blockchain monitoring solution>';

// Set up ways to store state and history
const browserIDBState = '<some code to store state in your browsers indexeddb>';
const privateServerState =
  '<some code to store state with a private state service>';

// Initialize the engine using the seed, blockchain and state providers.
const xoEngine = new Engine(seed, blockchainMonitor, [
  browserIDBState,
  privateServerState,
]);

Importing template

Before we can use the P2PKH template, we need to import it into the engine:

ts
const p2pkhTemplateIdentifier = await xoEngine.importTemplate(TemplateP2PKH);

Requesting Fungible Tokens

In the template there is list of starting actions that are available at any time:

ts
start:
{
	owner: [ 'receive', 'requestSatoshis', 'requestFungibleTokens', 'requestNonfungibleTokens' ],
},

Note that these actions are grouped by role, in this case the wallet owner can receive and request funds. Other templates might have different starting actions for different roles.

Since we are looking to specifically request 100.00 MUSD fungible tokens, we will need to take a look at the requestFungibleTokens action:

ts
requestFungibleTokens:
{
	name: 'Request Fungible Tokens',
	description: 'Requests a specific amount of a fungible tokens from one or more senders.',
	icon: 'request',

	variables: [ 'requestedTokenCategory', 'requestedTokenAmount' ],
	transactions: [ 'requestFungibleTokensTransaction' ],
},

The name and description here describes the action itself and is intended to help user find the right action for what they want to do.

The action will in the end produce a requestFungibleTokensTransaction transaction, but to do so it needs some variables like the requestedTokenCategory and the requestedTokenAmount.

Here's the description of these variables in the template:

ts
requestedTokenCategory:
{
	name: 'Requested Token Category',
	description: 'The token category requested',
	type: 'bytes',
	hint: 'token_category',
},
requestedTokenAmount:
{
	name: 'Requested Token Amount',
	description: 'The fungible token amount requested',
	type: 'integer',
	hint: 'token_amount',
},

Note that there is a technical type, the bytes, and a type hint.

The type hint is there so that apps and wallets can provide better user experiences, and lets the engine do some helpful validation.

For example, rather than have the user provide the MUSD token category like a hex string: b38a33f750f84c5c169a6f23cb873e6e79605021585d4f3408789689ed87f366, the wallet might show a dropdown with known tokens the user can choose from with the option of manual input as needed.

In our example we will simply hard-code 100.00 worth of MUSD:

ts
const moriaTokenCategory =
  'b38a33f750f84c5c169a6f23cb873e6e79605021585d4f3408789689ed87f366';
const moriaDecimals = 2;

const requestVariables = {
  requestedTokenCategory: hexToBin(moriaTokenCategory),
  requestedTokenAmount: BigInt(100.0 * 10 ** moriaDecimals),
};

const requestInvitation = await xoEngine.createStartingInvitation(
  p2pkhTemplateIdentifier,
  'requestFungibleTokens',
  { variables: requestVariables },
);

This code will return an invitation that contains information such as the template identifier, what action is in progress and any variable values for that action. Additionally every value in the invitation is covered by a signature from the entity that provided it.

In this example, we will use this invitation to coordinate the details of the request between the sender and the receiver.

Under the hood

As part of creating the starting invitation, the engine also needs to provide all information that should come from the current user. Since the starting action here was grouped under the owner role, it also automatically accepts the invitation in order to take on that role.

The next steps are going to drill down through the template in order to demonstrate how the engine finds the requirements of the action and how it generates the necessary data to satisfy those requirements without any further user interaction.

We start with the requestFungibleTokensTransaction that the action needs to produce:

ts
requestFungibleTokensTransaction:
{
	name: 'Request',
	description: 'Request for $(<requestedTokenAmount> <requestedTokenCategory.decimalsFactor> OP_DIV).$(<requestedTokenAmount> <requestedTokenCategory.decimalsFactor> OP_MOD) $(<requestedTokenCategory.symbol>).',
	icon: 'request',

	// Standard transaction without a locktime.
	version: 2,
	locktime: 0,

	// Inputs and outputs that must exist in the transaction.
	inputs: [ ],
	outputs: [ 'requestFungibleTokensOutput' ],

	// ...
	composable: true,
},

As you can see, it's a simple transaction construction, with a requirement set for a single requestFungibleTokensOutput.

The transaction also has a description that might not look very human readable in here, but once you take the description provided and apply the CashASM evaluations in it, it transforms to a much more human friendly version:

Request for 100.00 MUSD.

The engine now needs to determine how to create the requestFungibleTokensOutput:

ts
requestFungibleTokensOutput:
{
	name: 'Requested',
	description: '$(<requestedTokenAmount> <requestedTokenCategory.decimalsFactor> OP_DIV).$(<requestedTokenAmount> <requestedTokenCategory.decimalsFactor> OP_MOD) requested $(<requestedTokenCategory.symbol>) tokens.',
	icon: 'request',

	// Defines how the requested funds should be locked.
	lockscript: 'receivingLockingScript',

	// Require a flat 1000 satoshis to ensure the fungible tokens remains transferrable.
	valueSatoshis: 1000n,

	// Require only the specified amount and type of fungible tokens.
	// NOTE: This can be composed with a request for a non-fungible token, but will always result in two separate outputs.
	token:
	{
		category: '$(<requestedTokenCategory>)',
		amount: '$(<requestedTokenAmount>)',
		nft: null,
	},
},

Like the transaction, the output has a human readable name and description, but also requires a flat 1000 satoshis as well as the requested amount and category, and is managed by the receivingLockingScript.

If we follow that lockscript we will get to a quite long section, but for now the part that we care about is determining how the engine will resolve this to get an actual lockscript it can use in this output.

Here's a shortened version showing a deeper reference to the lockP2PKH script:

ts
receivingLockingScript:
{
	// Defines how spending future received funds should be authorized.
	lockingType: 'standard',
	lockingScript: 'lockP2PKH',
}

Following that reference shows the lockP2PKH script itself:

ts
lockP2PKH: 'OP_DUP OP_HASH160 <$(<ownerKey.public_key> OP_HASH160)> OP_EQUALVERIFY OP_CHECKSIG',

As some of you might've noticed, this is where the actual P2PKH part comes in, and the lockscript as it will be used on-chain is constructed.

However, we're not quite done yet as the lockP2PKH is defined with a dynamic CashASM expression: <$(<ownerKey.public_key> OP_HASH160)>.

This part takes the ownerKey, produces the corresponding public key and pushes it to the stack, then runs OP_HASH160 and return the hash.

In order to resolve this it needs to know how to get to the ownerKey:

ts
// Describe the secret private key.
ownerKey:
{
	name: 'Owners Private Key',
	description: 'The private key used to authorize spending of received funds.',
	type: 'bytes',
	hint: 'private_key',
},

This key is not provided by the action, but since the engine knows what a private key is and no key was provided it will create a new key on-demand every time such a key is needed, avoiding unnecessary address re-use.

The engine can now evaluate the CashASM part of the lockP2PKH script to create the receivingLockingScript that completes the requestFungibleTokensOutput, which is added to the invitation.

The ownerKey is not added to the invitation though, and we will go into more details when discussing state and secrets later in the article.

At this point, we have gone through all the requirements for setting up a request to receive 100.00 MUSD and we have created an invitation that can be shared to complete that request.

Handling the request

Let's assume that we have received the request invitation above via an XO compatible transport, and that we have an XO compatible wallet with sufficient funds in it.

In order to handle the request, there is a number of questions that will need to be answered:

  • what part do I take in this action?
  • what information will I need to provide?
  • which of my funds will be used for this?
  • do I actually even want to do this?

Ideally, we also want to interact with the wallet user as few times as possible, so the first thing we would need to do is figure out what is required of the user.

Roles

We start by checking if there is any open roles the user can take:

ts
const availableRoles = await xoEngine.listAvailableRoles(requestInvitation);

This step is needed because taking on a role defines which of the variables and resources needs to be provided later.

In this case, the only role that for the action was the owner, and that is already taken, so the only thing for us to do here is to accept the request without taking on a specific role in it:

ts
const acceptedRequestInvitiation = await xoEngine.acceptInvitation(
  requestInvitation,
  [],
);

In a more complex setup, this would require showing the user a list of available roles that they can choose from, before accepting the invitation

Variables

Next step is to check what variables are missing to complete the request:

ts
const { requiredVariables } =
  await xoEngine.listRequirements(requestInvitation);

This step is needed because the required variables could be used to define the resources needed to be provided later.

The action in this invitation only required two variables, the requestedTokenAmount and requestedTokenCategory. Both of these were already set by the owner, so there's nothing for us to do at this point.

In a more complex setup, this could be handled automaticelly by an app or require asking the user to manually input values for their required variables before moving on.

Inputs and outputs

The last step is to determine what resources are required in terms of blockchain assets.

For satoshis, we look at the the requestFungibleTokensOutput to see 1000:

valueSatoshis: 1000n,

For tokens we also look at the requestFungibleTokensOutput and here we can see that it has a fungible token amount that was evaluated to 10000n and a token category of b38a33f750f84c5c169a6f23cb873e6e79605021585d4f3408789689ed87f366:

ts
token:
{
	category: '$(<requestedTokenCategory>)',
	amount: '$(<requestedTokenAmount>)',
	nft: null,
},

Now that the engine knows what resources must exist in the output of the transaction, it can infer the required inputs. In this case to cover the required output and mining fee.

The next step would be to select some UTXOs to add as inputs, create any needed change outputs and then update the invitation with this information, which can be done automatically by the engine:

ts
const requirements = {
  requiredSatoshis,
  requiredFungibleTokens,
  requiredNonFungibleToken,
};

const preparedRequestInvitation = await xoEngine.reserveResources(
  acceptedRequestInvitiation,
  requirements,
);

In a later example we will show how to do this manually, to achieve coin control, choice in coin selection strategy and using different lockscripts for different change outputs to maintain privacy.

Authorization

Now that we know all roles, variables and resources the user would provide, we can show this information to the user and ask for a confirmation.

Assuming the user then agrees to take part in the action we go ahead and sign our inputs, marking our participation in this invitation complete:

ts
const signedRequestInvitation = await xoEngine.signInvitation(
  preparedRequestInvitiation,
);

The invitation can then be sent back, or if signed by all relevant parties, we can execute the action in the template directly:

ts
const requestTransaction = await xoEngine.executeAction(
  signedRequestInvitation,
  BROADCAST_ON_EXECUTION,
);

The request for 100.00 MUSD fungible tokens is now complete and a transaction exist on-chain.


While this process may seem more complex than necessary in comparison to ordinary BIP21 requests and using BIP44 to manage keys and lockscripts, it comes with many advantages that improves the user experience and safety of user interactions, while also being capable of working directly with a large number of smart contract systems.

In part 2 of this series, we will cover the remaining parts of the P2PKH template, go over how keys and state is managed, how to estimate ownership in shared outputs and go into more depth on what makes this system valuable.