Skip to main content

Working with Tokens

Nexa has native tokens, called group tokens. The node itself keeps track of them, so a token transfer is an ordinary transaction and no contract is needed to hold or move tokens. Contracts come in when you want to put rules on tokens: who may receive them, how many may be minted, or where an authority must go. This guide covers how tokens work on Nexa, how to read and constrain them in NexScript, and how to send them with the SDK.

Nexa's token model​

Groups​

Every token belongs to a group. A group is identified by a 32 byte group ID, which is created in a genesis transaction: the ID is the double SHA256 of the transaction's first input outpoint, its first OP_RETURN output (usually the token description) and the authority field of its authority output, which includes a nonce. The wallet searches nonces until the last two bytes of the hash match the group flags it wants. Those two bytes are the group's flags:

FlagValueMeaning
COVENANT1Output script template must match the input's
HOLDS_NEX2The group carries NEX instead of tokens: inputs and outputs must balance NEX, and the token quantity must be 0

The other bits are reserved and must be zero. Most groups have no flags.

Token outputs​

Tokens are carried by outputs. A grouped output's locking bytecode starts with two extra pushes, the group ID and a quantity, followed by the usual script template fields. The quantity is an unsigned little endian number of 2, 4 or 8 bytes. An output can carry tokens of only one group, and it still holds some NEX (at least the dust amount of 546 satoshis, which wallets usually use).

A grouped output must carry at least 1 token: the node rejects a grouped output with a quantity of 0. The exception is a HOLDS_NEX group, where the quantity must be 0 and the output's NEX value is what is balanced.

Subgroups and NFTs​

A group ID longer than 32 bytes is a subgroup: the first 32 bytes are the parent group ID, and the rest is the subgroup data. Subgroups are commonly used as NFTs, with the subgroup data as the NFT's commitment and an amount of 1. A subgroup is a group of its own for balancing, so tokens of a subgroup do not count as tokens of the parent.

Authorities​

An authority is a grouped output whose quantity field has its top bit (bit 63) set. Read as a signed 64 bit number it is negative, and the bits below it are permission flags:

FlagBitPermission
AUTHORITY63Marks the output as an authority
MINT62Mint tokens
MELT61Melt (destroy) tokens
BATON60Create authority outputs, including passing itself on
RESCRIPT59Change the redeem script
SUBGROUP58Create subgroups

The low bits of a genesis authority hold the nonce used to find the group ID.

What the node enforces​

For each group in a transaction, the node checks that:

  • the token outputs add up to the token inputs, unless an input authority has MINT (more out) or MELT (less out). Authorities themselves do not count towards these amounts.
  • outside of genesis, every authority output only has flags that the group's input authorities with BATON have. An authority without BATON cannot be passed on: it is used up when spent.
  • a subgroup output gets its permissions from a parent group authority with SUBGROUP in the inputs. The subgroup gets all of the parent authority's flags except SUBGROUP, so creating NFTs needs MINT as well.

Authorities can always be destroyed, by spending them without creating a new one.

Reading tokens in a contract​

Every input and output has token fields. Inputs read the UTXO that the input spends.

FieldTypeValue
tokenGroupIdbytesThe full group ID, including any subgroup data. 0x if ungrouped
tokenParentGroupIdbytesThe first 32 bytes of the group ID. 0x if ungrouped
subgroupDatabytesThe bytes after the parent group ID. 0x if not a subgroup
tokenAmountintThe token quantity. 0 if ungrouped or an authority
tokenAuthorityintThe authority field as a signed number, so negative. 0 if not an authority
// output 0 must carry at least 100 tokens of groupId
require(tx.outputs[0].tokenGroupId == groupId);
require(tx.outputs[0].tokenAmount >= 100);

// output 1 must be an NFT of the groupId collection with the commitment 0xabcd
require(tx.outputs[1].tokenParentGroupId == groupId);
require(tx.outputs[1].subgroupData == 0xabcd);

An authority has a tokenAmount of 0, so do not use tokenAmount to tell an authority from an ungrouped output: check whether tokenGroupId is empty (tokenGroupId.length == 0), and use tokenAuthority for authorities.

Checking authority flags​

hasAuthority(flags, required) checks that flags is an authority with every flag in required. required is a sum of the constants AUTHORITY_MINT, AUTHORITY_MELT, AUTHORITY_BATON, AUTHORITY_RESCRIPT and AUTHORITY_SUBGROUP, or 0 to only check that it is an authority.

// input 0 must be an authority that can mint and pass itself on
require(hasAuthority(tx.inputs[0].tokenAuthority, AUTHORITY_MINT + AUTHORITY_BATON));

Use hasAuthority() rather than testing bits with &: script numbers are sign-magnitude, so the bits of a negative tokenAuthority are not the bits of the authority field.

Group totals​

The transaction state functions look at a whole group across the transaction:

FunctionValue
tx.groupAmountIn(groupId)Sum of the group's token inputs (authorities not counted)
tx.groupAmountOut(groupId)Sum of the group's token outputs (authorities not counted)
tx.groupCountIn(groupId)Number of inputs of the group, authorities included
tx.groupCountOut(groupId)Number of outputs of the group, authorities included
tx.groupNthInput(n, groupId)Index of the group's nth input (from 0). Fails if there is none
tx.groupNthOutput(n, groupId)Index of the group's nth output (from 0). Fails if there is none

The amount and count functions return 0 for a group that is not in the transaction. The group is matched exactly, so a parent group ID does not include its subgroups. tx.groupAmountOut(groupId) - tx.groupAmountIn(groupId) is the amount minted in the transaction.

groupId must be a bytes32. The token fields are bytes, so cast them first:

bytes32 inputGroup = bytes32(tx.inputs[this.activeInputIndex].tokenGroupId);
require(tx.groupAmountOut(inputGroup) == tx.groupAmountIn(inputGroup));

The cast does not check the length at run time. If the input is ungrouped, the group function fails the script.

Building token outputs in a covenant​

To require that an output carries tokens to a contract, compare its locking bytecode with the five argument form of LockingBytecodeP2ST:

new LockingBytecodeP2ST(bytes20 templateHash, bytes constraintHash, bytes visibleParameters, bytes groupId, int amount)
  • groupId is a 32 byte group ID or a longer subgroup ID.
  • amount >= 0 is a token quantity. The compiler encodes it in the smallest of 2, 4 or 8 bytes, the same minimal encoding libnexa-ts uses.
  • amount < 0 is an authority: pass the signed authority field, as read from tokenAuthority.
  • An empty groupId builds an ungrouped output, and amount is ignored.

Because an empty group ID builds an ungrouped output, a covenant can forward an input whether or not it carries tokens. This contract requires every one of its inputs to go back to itself, in the output with the same index, with the same tokens:

Forward.nex
pragma nexscript ^1.2.0;

contract Forward() {
function spend() {
int i = this.activeInputIndex;
bytes20 templateHash = hash160(this.activeBytecode);
require(tx.outputs[i].lockingBytecode == new LockingBytecodeP2ST(
templateHash, 0x, 0x, tx.inputs[i].tokenGroupId, tx.inputs[i].tokenAmount));
}
}

Forward has no constructor parameters, so its constraint hash and visible parameters are empty. It cannot forward an authority, since an authority's tokenAmount is 0 (see the pitfalls below). For a contract with parameters, rebuild them as shown in the covenants guide.

When the tokens go back unchanged, you do not need to rebuild anything: the input's own locking bytecode already includes its group and amount, so tx.outputs[i].lockingBytecode == tx.inputs[i].lockingBytecode does the same job, and also works for authorities.

Pitfalls​

  • Authorities forwarded with tokenAmount. tokenAmount is 0 for an authority, and a grouped output with a quantity of 0 is invalid. To forward an authority, pass tx.inputs[i].tokenAuthority as the amount, or compare the whole locking bytecode.
  • Passing on an authority needs BATON. A covenant that requires an authority to come back can only be spent if the authority has BATON. Check it with hasAuthority(..., AUTHORITY_BATON), or the contract may be unspendable.
  • Zero amounts. If a covenant sends remaining tokens back, handle remaining == 0 separately: there is no valid output with 0 tokens.
  • Byte exact comparison. The node accepts non-minimal quantity encodings (for example 5 in 8 bytes), which do not match the bytecode the compiler builds. Wallets and the SDK use the minimal encoding.
  • Every contract input runs the contract. If the contract also holds plain NEX for fees, those inputs run the same function. Branch on tx.inputs[this.activeInputIndex].tokenGroupId so each input only checks what applies to it.

Examples​

Token vault​

A hot key may spend the vault's NEX, but its tokens must stay. Only the cold key can move tokens or use an authority held by the vault.

TokenVault.nex
pragma nexscript ^1.2.0;

contract TokenVault(pubkey hot, pubkey cold) {
// The hot key can spend plain NEX, but any input that carries tokens
// must go back to the vault unchanged, in the same position
function spend(sig s) {
require(checkSig(s, hot));

int i = this.activeInputIndex;
if (tx.inputs[i].tokenGroupId.length > 0) {
// An authority in the inputs would let the transaction mint tokens
// or create new authorities, even if it goes back to the vault
require(tx.inputs[i].tokenAuthority == 0);
require(tx.outputs[i].lockingBytecode == tx.inputs[i].lockingBytecode);
}
}

// The cold key can move everything
function sweep(sig s) {
require(checkSig(s, cold));
}
}

Token drip​

The beneficiary can claim up to maxClaim tokens once every period blocks. The rest goes back to a contract with the same parameters, so the next claim has to wait another period blocks. All parameters are visible, so the contract can rebuild its own locking bytecode without a constraint hash.

TokenDrip.nex
pragma nexscript ^1.2.0;

contract TokenDrip(
pubkey visible beneficiary,
pubkey visible funder,
bytes32 visible groupId,
int visible maxClaim,
int visible period
) {
function claim(sig s, int amount) {
require(checkSig(s, beneficiary));
require(tx.age >= period);
require(amount > 0 && amount <= maxClaim);

// Only one input of this group, so a claim cannot combine UTXOs
require(tx.groupCountIn(groupId) == 1);

int i = this.activeInputIndex;
if (tx.inputs[i].tokenGroupId == groupId) {
// An authority's tokenAmount is 0, which would skip the output
// check below and let the beneficiary take it
require(tx.inputs[i].tokenAuthority == 0);
int remaining = tx.inputs[i].tokenAmount - amount;
// A grouped output must hold at least 1 token, so a claim that
// empties the contract has no output to check
if (remaining > 0) {
bytes20 templateHash = hash160(this.activeBytecode);
bytes visibleParams = encodeData(beneficiary) + encodeData(funder)
+ encodeData(groupId) + encodeNumber(maxClaim) + encodeNumber(period);
require(tx.outputs[i].lockingBytecode == new LockingBytecodeP2ST(
templateHash, 0x, visibleParams, groupId, remaining));
}
}
}

function reclaim(sig s) {
require(checkSig(s, funder));
}
}

Token vendor​

The vendor holds a mint authority for groupId. Anyone can mint new tokens by paying price satoshis per token to the treasury, as long as the authority goes back to the vendor.

Whoever spends an authority can use all of its flags in that transaction, so the vendor has to limit more than the amount minted. With BATON a buyer could add new authority outputs for themselves, and with SUBGROUP they could create subgroups. The vendor therefore allows only two outputs of the group (the returned authority and the minted tokens) and refuses an authority with SUBGROUP. Give it an authority with only MINT and BATON, and keep a single authority at the vendor address: with two identical ones, anyone could spend both and return only one, destroying the other.

TokenVendor.nex
pragma nexscript ^1.2.0;

contract TokenVendor(bytes32 groupId, bytes20 treasury, int price, bytes20 owner) {
// Anyone can mint tokens by paying `price` satoshis per token to the treasury
function buy() {
int i = this.activeInputIndex;

// This input must be the vendor's mint authority, and it must be able
// to pass itself on, but not create subgroups
require(tx.inputs[i].tokenGroupId == groupId);
int authority = tx.inputs[i].tokenAuthority;
require(hasAuthority(authority, AUTHORITY_MINT + AUTHORITY_BATON));
require(!hasAuthority(authority, AUTHORITY_SUBGROUP));

// Output 0 returns the same authority to the vendor, and the only other
// output of the group holds the minted tokens: no new authorities
require(tx.outputs[0].lockingBytecode == tx.inputs[i].lockingBytecode);
require(tx.groupCountOut(groupId) == 2);

// Output 1 pays for the newly minted tokens
int minted = tx.groupAmountOut(groupId) - tx.groupAmountIn(groupId);
require(minted > 0);
require(tx.outputs[1].lockingBytecode == new LockingBytecodeP2PKT(treasury));
require(tx.outputs[1].value >= minted * price);
}

// The owner can take the authority and any NEX the vendor holds
function withdraw(pubkey pk, sig s) {
require(hash160(pk) == owner);
require(checkSig(s, pk));
}
}

buy() requires its input to be the authority, so the buyer pays with their own P2PKT input rather than the vendor's NEX (see below).

Tokens in the SDK​

In the SDK a group ID is a hex string: 64 characters for a group, longer for a subgroup (the parent group ID followed by the subgroup data). UTXOs with tokens have a token field:

interface TokenDetails {
amount: bigint; // token quantity, or the signed authority field (negative)
groupId: string; // hex
}

const utxos = await vault.getUtxos();
const tokens = utxos.filter((u) => u.token?.groupId === groupId && u.token.amount > 0n);
const authority = utxos.find((u) => u.token?.groupId === groupId && u.token.amount < 0n);

Sending tokens to a contract​

A token output is a normal output to the contract's address with token details. From a contract spend, use to():

// `other` is another contract that holds tokens of groupId
await other.functions.spend(new SignatureTemplate(alicePriv))
.to(vault.address, 546n, { groupId, amount: 1000n })
.send();

From a wallet, use the wallet's own token send with contract.address as the destination. contract.tokenAddress(groupId, amount) returns an address whose output script carries amount tokens of groupId to the contract.

caution

tokenAddress() currently always builds a mainnet address, whatever network the contract's provider uses.

Sending tokens from a contract​

Add a token to an output. The SDK selects the contract's token UTXOs for you and sends any token change back to the contract:

const tx = await vault.functions.sweep(new SignatureTemplate(bobPriv))
.to([{ to: aliceAddress, amount: 546n, token: { groupId, amount: 400n } }])
.send();

send() returns the transaction as libnexa-ts ITransaction. A token output holds the amount you pass, at least 546 satoshis.

The SDK also tries not to burn anything by accident:

  • Token change goes back to the contract address.
  • An authority input without a matching output gets one, back to the contract address. The node only accepts this for an authority with BATON.
  • An NFT input that you selected with from() and do not send anywhere is sent back to the contract.

withoutTokenChange() turns all of this off. Melting tokens this way still needs a MELT authority in the transaction, or the node rejects it.

These change outputs go to the contract's own address and are appended after your outputs. If your contract is a covenant that checks outputs by index, put the outputs it checks first, or select inputs with from() and send everything explicitly.

Selecting inputs for a covenant​

A covenant that pairs input i with output i needs the inputs in a known order, so select them with from(). For the vault's hot spend, the token UTXO goes first and comes back as output 0:

const tokenUtxo = utxos.find((u) => u.token?.groupId === groupId && u.token.amount > 0n)!;
const feeUtxo = utxos.find((u) => !u.token)!;

await vault.functions.spend(new SignatureTemplate(alicePriv))
.from([tokenUtxo, feeUtxo])
.to([
{ to: vault.address, amount: 546n, token: tokenUtxo.token },
{ to: aliceAddress, amount: 10000n },
])
.send();

A drip claim works the same way, with the remaining tokens going back to the drip's address (its parameters do not change, so neither does its address):

await drip.functions.claim(new SignatureTemplate(alicePriv), 100n)
.from([dripTokens, dripFee])
.withAge(144)
.to([
{ to: drip.address, amount: 546n, token: { groupId, amount: dripTokens.token!.amount - 100n } },
{ to: aliceAddress, amount: 546n, token: { groupId, amount: 100n } },
])
.send();

Minting from a contract​

If the outputs need more tokens than the contract's token UTXOs hold, the SDK adds one of the contract's authorities with MINT to the inputs and the difference is minted. When you select inputs with from(), include the authority yourself. Unless you send the authority somewhere with an output that has the same token as the authority UTXO, the SDK sends it back to the contract.

For the vendor, the buyer funds the transaction with their own input. The SDK sends NEX change to the contract address, so here the buyer adds their own change output and sets the fee:

const auth = (await vendor.getUtxos()).find((u) => u.token?.groupId === groupId && u.token.amount < 0n)!;
const count = 50n;
const cost = count * price;
const fee = 1000n;
const change = auth.satoshis + buyerUtxo.satoshis - 546n - cost - 546n - fee;

await vendor.functions.buy()
.from(auth)
.fromP2PKT(buyerUtxo, new SignatureTemplate(buyerPriv))
.to([
{ to: vendor.address, amount: 546n, token: auth.token }, // output 0: the authority
{ to: treasuryAddress, amount: cost }, // output 1: the payment
{ to: buyerAddress, amount: 546n, token: { groupId, amount: count } },
{ to: buyerAddress, amount: change },
])
.withHardcodedFee(fee)
.send();

Creating NFTs​

An output whose groupId is longer than 64 hex characters creates or sends a subgroup token. If the contract holds no tokens of that subgroup, the SDK adds a parent group authority with SUBGROUP (the node also needs MINT, see above):

await vault.functions.sweep(new SignatureTemplate(bobPriv))
.to([
{ to: vault.address, amount: 546n, token: authority!.token },
{ to: bobAddress, amount: 546n, token: { groupId: `${groupId}abcd`, amount: 1n } },
])
.send();

Creating NFTs needs the SDK's automatic input selection: with from(), every NFT output must match an NFT input.

Creating a group​

The SDK has no helper for genesis, and a contract spend cannot create a group. Build the genesis transaction with libnexa-ts, as the NexScript tests do:

import {
Address, AddressType, GroupToken, ScriptFactory, Transaction, TransactionBuilder,
} from 'libnexa-ts';
import { binToHex } from '@nexscript/utils';

// The group ID commits to the first input's outpoint and the OP_RETURN output
const opReturn = ScriptFactory.buildTokenDescriptionLegacy('TKN', 'My token');
const flags = GroupToken.authFlags.ACTIVE_FLAG_BITS;
const { hashBuffer, nonce } = GroupToken.findGroupId(utxo.txid, opReturn, flags);
const groupId = binToHex(hashBuffer);
const groupAddress = new Address(hashBuffer, provider.network, AddressType.GroupIdAddress).toString();

// Send the new group's authority (every flag, plus the nonce) to the contract
const txBuilder = new TransactionBuilder()
.from({ outpoint: utxo.txid, satoshis: Number(utxo.satoshis), address: utxo.address })
.addData(opReturn, true)
.to(contract.address, Transaction.DUST_AMOUNT, groupAddress, flags | nonce)
.change(walletAddress)
.sign(privateKey);
await provider.sendRawTransaction(txBuilder.build().serialize());

Limitations​

  • A contract spend can pass on an authority, but not split it: every authority output must match an authority input exactly (same group and same authority field). To give a contract an authority with fewer flags, such as the vendor's MINT and BATON, split it in a wallet transaction before sending it to the contract, for example with libnexa-ts as the NexScript tests do.
  • No genesis from a contract spend, and no group genesis helper (see above).
  • tokenAddress() always builds a mainnet address.