How To Make A Cardano CIP-68 Fungible Token With Helios (Part 2 of 2)

Staking Rocks! [PHRCK]
13 min readSep 30, 2023

In this part, we’ll prepare the JS code that will deploy our Helios validator scripts and mint our token. If you landed directly here, you should go over that first part before proceeding below.

If you want to skip the following walk-through, you can also go straight to the finished project repo here.

Project prep

This part is intended for total beginners. You may skip this part if this is not your first JS/TypeScript project.

Before anything else, let’s create our project directory, initialize a JS project, and install our dependencies.:

mkdir cip68-fungible-token; cd cip68-fungible-token
yarn init

# fill in your project details as desired...

yarn add -D typescript @types/node ts-node tsconfig-paths && \
yarn add @hyperionbt/helios @stricahq/bip32ed25519 @blockfrost/blockfrost-js bip39 dotenv

Environment Variables

Let’s also create a .env file for the environment variables our script will need later. Place this at the root directory of the project. Let’s fill it with the following:

# change to "prod" for mainnet
ENVIRONMENT="dev"

# Blockfrost:
BLOCKFROST_API_KEY="preprodAxI..."

# Seed account phrase (can be 12, 15, or 24-word seed phrase):
SEED_ACCT_PHRASE="continue descent convulse dare medal pasture colony reign chocolate state pluck witness"

# Amount to mint:
MINT_AMT=69000000

# State thread token Minting Policy Hash (dynamically filled, only upon initialization of the state machine):
THREAD_NFT_MPH=

If you’re not yet familiar of it, Blockfrost is probably the first blockchain API provider for Cardano. Using their API lets us interact with the Cardano blockchain without having to run our own node and download a copy of the entire chain. You can get your API key at https://blockfrost.io/ for free.

For this demo project, let’s just connect to the preprod testnet of Cardano. So, get the appropriate API key from Blockfrost if you don’t have one yet.

The rest of the variables above should be self explanatory.

Typescript Stuff

We’ll be using Typescript for this, so let’s create our tsconfig.json file now:

{
"compilerOptions": {
"target": "es2020",
"lib": ["es2020", "DOM"],
"module": "nodenext",
"rootDir": "./src",
"moduleResolution": "nodenext",
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
},
"outDir": "./build",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
},
"exclude": ["node_modules"]
}

Then let’s create the followingloader.js file so that we can run our TS scripts later even without first compiling them:

import { pathToFileURL } from "url";

import { resolve as resolveTs } from "ts-node/esm";
import * as tsConfigPaths from "tsconfig-paths";

const { absoluteBaseUrl, paths } = tsConfigPaths.loadConfig();
const matchPath = tsConfigPaths.createMatchPath(absoluteBaseUrl, paths);

export function resolve(specifier, ctx, defaultResolve) {
const match = matchPath(specifier);
return match
? resolveTs(pathToFileURL(`${match}`).href, ctx, defaultResolve)
: resolveTs(specifier, ctx, defaultResolve);
}

export { load, transformSource } from "ts-node/esm";

And that’s it. We are now ready to start writing our code.

Main

Let’s create a directory called src to separate our source Typescript code from the compiled JS code which will later be contained in build/ based on our tsconfig.json settings above.

Inside our src directory, let’s create 3 more subdirectories to organize our files: cli, contracts, and lib. These are just my preferences for the purpose of this guide. You can of course, organize your project however you like.

Helios validator scripts

In the src/contracts/ directory, let’s save our Helios validator scripts from part 1. So we should now have the following files:

src/
contracts/
threadTokenPolicy.hl
supplyStateValidator.hl
mainTokenPolicy.hl

acct-details.ts

Now let’s write the first function we’ll need. And that is for deriving the signing keys and wallet address that we’ll be using for creating transactions. We’ll put it in a file named acct-details.ts inside src/lib/:

import { mnemonicToEntropy } from 'bip39';
import pkg from '@stricahq/bip32ed25519';
const { Bip32PrivateKey } = pkg;
import {
Address,
StakeAddress,
PubKeyHash
} from "@hyperionbt/helios";

export async function getAcctDetails(phrase:string, idx: number){
let isTestnet = process.env.ENVIRONMENT=="dev" ? true : false;

const entropy = mnemonicToEntropy(phrase);
const rootKey = await Bip32PrivateKey.fromEntropy(Buffer.from(entropy, "hex"));
const accountKey = rootKey
.derive(2147483648 + 1852) // purpose
.derive(2147483648 + 1815) // coin type
.derive(2147483648 + idx); // account index
const stakingPrivKey = accountKey
.derive(2) // chain {0: external, 1: internal, 2: staking key}
.derive(0) // staking acct idx
.toPrivateKey();
const stakingKeyHash = PubKeyHash.fromProps( stakingPrivKey.toPublicKey().hash().toString("hex") );
const stakingAddr = StakeAddress.fromHash(
isTestnet,
stakingKeyHash
);
const spendingPrivKey = accountKey
.derive(0) // chain {0: external, 1: internal, 2: staking key}
.derive(0) // spending addr idx
.toPrivateKey();
const spendingKeyHash = PubKeyHash.fromProps( spendingPrivKey.toPublicKey().hash().toString("hex") );
const spendingAddr = Address.fromHashes(
spendingKeyHash,
stakingKeyHash,
isTestnet
)

return {
stakingPrivKey,
stakingKeyHash,
stakingAddr,
spendingPrivKey,
spendingKeyHash,
spendingAddr
}
}

The function getAcctDetails() will just need to be passed a valid wallet seed phrase and an account index number, and it will return the private and public keys of the first payment and stake addresses under that account. We will especially need this spendingPrivKey later, for signing transactions.

fetch-token-utxo.ts

Next thing we’ll need is a function we can conveniently call, to get the UTXO where a certain token is currently contained in. We’ll use this later, to find the current UTXO containing our thread token, so that we can read its datum to get the current supply of our main token. Let’s put this in a file named fetch-token-utxo.ts and put inside src/lib/ directory:

import { BlockFrostAPI } from '@blockfrost/blockfrost-js';
import {
TxOutputId,
TxOutput,
TxInput,
TxId,
UplcProgram,
Address,
Datum,
UplcData,
hexToBytes,
Value,
MintingPolicyHash,
Assets
} from "@hyperionbt/helios";

export async function fetchTokenUtxo(token: string){
const apiKey: string = process.env.BLOCKFROST_API_KEY as string;
const client = new BlockFrostAPI({projectId: apiKey});
try {
const tokenTxs = await client.assetsTransactions(token, {order: "desc", count: 1});
const lastTx = tokenTxs[0]; // don't trust bfrost's .tx_index returned here! inconsistent
const txOutputs = (await client.txsUtxos(lastTx.tx_hash)).outputs;
const targetUtxo:any = txOutputs.filter(output => output.amount.some(amt => amt.unit == token))[0];
targetUtxo["tx_hash"] = lastTx.tx_hash as string;
const rebuiltTxInput = await restoreTxInput(targetUtxo);
return rebuiltTxInput;
} catch (e) {
if (404 == (e as unknown as Record<string, any>)?.status_code){
console.log(`Nothing found.`);
return false;
}
throw e;
}
}

/**
* @param {{
* address: string
* tx_hash: string
* output_index: number
* amount: {unit: string, quantity: string}[]
* inline_datum: null | string
* data_hash: null | string
* collateral: boolean
* reference_script_hash: null | string
* }} obj
* (Taken straight from Helios internal functions)
*/
async function restoreTxInput(obj:any) {
/**
* @type {null | UplcProgram}
*/
let refScript: UplcProgram | null = null;
if (obj.reference_script_hash !== null) {
const url = `https://cardano-${process.env.networkName}.blockfrost.io/api/v0/scripts/${obj.reference_script_hash}/cbor`;

const response = await fetch(url, {
method: "GET",
headers: {
"project_id": process.env.BLOCKFROST_API_KEY as string
}
});

const cbor = (await response.json()).cbor;

refScript = UplcProgram.fromCbor(cbor);
}

return new TxInput(
new TxOutputId(TxId.fromHex(obj.tx_hash), obj.output_index),
new TxOutput(
Address.fromBech32(obj.address),
parseValue(obj.amount),
obj.inline_datum ? Datum.inline(UplcData.fromCbor(hexToBytes(obj.inline_datum))) : null,
refScript
)
);
}

/**
* @param {{unit: string, quantity: string}[]} obj
* @returns {Value}
* (Taken straight from Helios internal functions)
*/
function parseValue(obj:any) {
let value = new Value();

for (let item of obj) {
let qty = BigInt(item.quantity);

if (item.unit == "lovelace") {
value = value.add(new Value(qty));
} else {
let policyID = item.unit.substring(0, 56);
let mph = MintingPolicyHash.fromHex(policyID);

let token = hexToBytes(item.unit.substring(56));

value = value.add(new Value(0n, new Assets([
[mph, [
[token, qty]
]]
])));
}
}

return value;
}

get-addr-outputs.ts

We will also need a function to get all the UTXOs in a given address. We’ll use this later to get the UTXOs we can use as transaction inputs. Let’s put this in a file named get-addr-outputs.ts and also put it inside src/lib/:

import {
Tx,
Address,
TxInput,
TxOutputId,
TxOutputIdProps
} from "@hyperionbt/helios";

export function getAddressOutputs(tx: Tx, returnAddr: Address): TxInput[]{
const txId = tx.id();
const outputs = tx.body.outputs;
const returnOutputs: TxInput[] = [];
for (let [idx, output] of Object.entries(outputs) ){
if (output.address.eq(returnAddr)) {
returnOutputs.push(
new TxInput(
new TxOutputId(`${txId}#${idx}` as TxOutputIdProps),
output
)
);
}
}
return returnOutputs;
}

get-witness.ts

For signing transactions, let’s create the following function and put it in a file named get-witness.ts inside src/lib/:

import { Tx, Signature } from "@hyperionbt/helios";
import pkg from '@stricahq/bip32ed25519';
import { blake2b } from "blakejs";

type PrivateKey = pkg.PrivateKey;

function hash32(data: any) {
const hash = blake2b(data, undefined, 32);
return Buffer.from(hash);
};
function sign(privKey:PrivateKey, txHash: Buffer){
const pubkey = privKey.toPublicKey().toBytes();
const signature = privKey.sign(txHash);
return new Signature([...pubkey], [...signature]);
}

export function getWitness(tx: Tx, privKey:PrivateKey, vars:any): Signature | undefined {
const txBodyCbor = tx.body.toCborHex();
const txBody = Buffer.from(txBodyCbor, 'hex');
const txHash = hash32(txBody);
return sign(privKey, txHash);
}

submit.ts

And of course, for submitting transactions to the blockchain (via Blockfrost), we need the following function which we will put in a file named submit.ts still inside src/lib/:

import { BlockFrostAPI } from '@blockfrost/blockfrost-js';
import { Tx } from "@hyperionbt/helios";

export async function submitTx(tx: Tx): Promise<string> {
const payload = new Uint8Array(tx.toCbor());
const apiKey: string = process.env.BLOCKFROST_API_KEY as string;
try {
const client = new BlockFrostAPI({
projectId: apiKey,
});
const txHash = await client.txSubmit(payload);
return txHash;
}
catch (err) {
console.error("signSubmitTx API Failed: ", err);
throw err;
}
}

init-vars.ts

Before we can build a transaction with the Helios library, especially one involving validator scripts, there’s quite a few variables we need beforehand. To reduce clutter in our main minting script later, we will put most of these variable preparations into its own function, which in turn we’ll put in a file named init-vars.ts still inside src/lib/:

import dotenv from "dotenv";
dotenv.config();
import fs from "fs";
import path from "path";
import { fetchTokenUtxo } from "./fetch-token-utxo.js";
import { getAcctDetails } from "./acct-details.js";
import {
Address,
TxInput,
NetworkParams,
Value,
RemoteWallet,
WalletHelper,
Program,
textToBytes,
bytesToHex,
hexToBytes,
BlockfrostV0,
AssetClass,
ConstrData,
MintingPolicyHash
} from "@hyperionbt/helios";

export async function getInitVars(returnAddr: null | Address = null) {
const version = "1.0";
const optimize = false;

// set network
const network = process.env.ENVIRONMENT=="dev" ? "preprod" : "mainnet";
process.env.networkName = network;

// blockfrost API
const bfrost = new BlockfrostV0(network, process.env.BLOCKFROST_API_KEY as string);

// network params
const networkParams: NetworkParams = await bfrost.getParameters();

// wallet stuff
const seedAcctPhrase = process.env.SEED_ACCT_PHRASE as string;
const seedAcct = await getAcctDetails(seedAcctPhrase, 0);
const changeAddr = returnAddr ? returnAddr : seedAcct.spendingAddr;
const txInputs: TxInput[] = await bfrost.getUtxos(changeAddr);
const remoteWallet = new RemoteWallet(network=="mainnet", [changeAddr], [], txInputs);
const walletHelper = new WalletHelper(remoteWallet);
const initialUtxoVal = new Value(BigInt(1000000));
const sortedUtxos = await walletHelper.pickUtxos(initialUtxoVal); // pick utxos totalling at least initialUtxoVal

// ------------------------
// Helios validators stuff
// ------------------------
// threadTokenPolicy script (MPH)
const threadTokenNameStr = "tokenSupplyState";
const seedUtxo = sortedUtxos[0][0];
const rawScriptThreadToken = fs.readFileSync(path.resolve('src/contracts/threadTokenPolicy.hl')); // buffer
const threadTokenProgram = Program.new(rawScriptThreadToken.toString());
threadTokenProgram.parameters.seedTxId = seedUtxo.outputId.txId.hex;
threadTokenProgram.parameters.seedTxIx = seedUtxo.outputId.utxoIdx;
const threadTokenCompiledProgram = threadTokenProgram.compile(optimize);
let threadTokenPolicyHash: MintingPolicyHash;
const existingThreadNftMPH = process.env.THREAD_NFT_MPH;
if (existingThreadNftMPH){
threadTokenPolicyHash = MintingPolicyHash.fromHex(existingThreadNftMPH);
} else {
threadTokenPolicyHash = threadTokenCompiledProgram.mintingPolicyHash;
}

// supplyStateValidator script
const rawScriptStateValidator = fs.readFileSync(path.resolve('src/contracts/supplyStateValidator.hl')); // buffer
const supplyStateProgram = Program.new(rawScriptStateValidator.toString());
supplyStateProgram.parameters.adminPkh = seedAcct.spendingKeyHash.hex;
supplyStateProgram.parameters.threadTokenMPH = threadTokenPolicyHash.hex;
const supplyStateCompiledProgram = supplyStateProgram.compile(optimize);
const supplyStateValidatorHash = supplyStateCompiledProgram.validatorHash;
const supplyStateValidatorAddr = Address.fromHashes(supplyStateValidatorHash, seedAcct.stakingKeyHash);

// mainTokenPolicy script (MPH)
const mainTokenNameHex = bytesToHex(textToBytes("DigAss"));
const mainTokenCip68Ref_hexStr = "000643b0" + mainTokenNameHex
const mainTokenCip68Usr_hexStr = "0014df10" + mainTokenNameHex;
const rawScriptMainToken = fs.readFileSync(path.resolve('src/contracts/mainTokenPolicy.hl')); // buffer
const mainTokenProgram = Program.new(rawScriptMainToken.toString());
mainTokenProgram.parameters.supplyStateValidatorHash = supplyStateValidatorHash.hex;
mainTokenProgram.parameters.adminPkh = seedAcct.spendingKeyHash.hex;
mainTokenProgram.parameters.refTokenName = mainTokenCip68Ref_hexStr;
mainTokenProgram.parameters.userTokenName = mainTokenCip68Usr_hexStr;
mainTokenProgram.parameters.threadTokenMPH = threadTokenPolicyHash.hex;
const mainTokenCompiledProgram = mainTokenProgram.compile(optimize);
const mainTokenPolicyHash = mainTokenCompiledProgram.mintingPolicyHash;

// -----------------------------
// TokenNames and asset classes
// -----------------------------
const TN_mainRefTokenCip68 = hexToBytes(mainTokenCip68Ref_hexStr); // 000643b0446967417373
const TN_mainUsrTokenCip68 = hexToBytes(mainTokenCip68Usr_hexStr); // 0014df10446967417373
const TN_threadToken = textToBytes(threadTokenNameStr);

const assetClass_cip68Ref = AssetClass.fromProps({"mph":mainTokenPolicyHash, "tokenName": TN_mainRefTokenCip68});
const assetClass_cip68Usr = AssetClass.fromProps({"mph":mainTokenPolicyHash, "tokenName": TN_mainUsrTokenCip68});
const assetClass_supplyState = AssetClass.fromProps({"mph":threadTokenPolicyHash, "tokenName": TN_threadToken});


// fetch thread and ref tokens utxos:
// ----------------------------------
// supplyState
const supplyStateTokenStr = `${threadTokenPolicyHash.hex}${bytesToHex(textToBytes(threadTokenNameStr))}`;
const supplyStateUtxo = await fetchTokenUtxo(supplyStateTokenStr);
// ref token
const cip68RefTokenStr = `${mainTokenPolicyHash.hex}${mainTokenCip68Ref_hexStr}`;
const cip68RefTokenUtxo = await fetchTokenUtxo(cip68RefTokenStr);

// get current supply:
const mainTokenCurrentSupply = !supplyStateUtxo ? 0 : (supplyStateUtxo as TxInput).dump().output.datum.inlineSchema.list[3].int as number;

// dummy datum/redeemer data:
const dummyPlutusData = new ConstrData(0, []);

return {
version,
optimize,
network,
bfrost,
networkParams,
txInputs,
seedAcct,
changeAddr,
remoteWallet,
walletHelper,
initialUtxoVal,
sortedUtxos,
existingThreadNftMPH,
threadTokenProgram,
threadTokenCompiledProgram,
threadTokenPolicyHash,
mainTokenProgram,
mainTokenCompiledProgram,
mainTokenPolicyHash,
supplyStateProgram,
supplyStateCompiledProgram,
supplyStateValidatorHash,
supplyStateValidatorAddr,
supplyStateUtxo,
cip68RefTokenUtxo,
mainTokenCurrentSupply,
TN_mainRefTokenCip68,
TN_mainUsrTokenCip68,
TN_threadToken,
mainTokenCip68Ref_hexStr,
mainTokenCip68Usr_hexStr,
assetClass_cip68Ref,
assetClass_cip68Usr,
assetClass_supplyState,
dummyPlutusData
};
}

This way, we can conveniently reuse this preparatory function later.

init-state.ts

When we later get to the point where we submit transactions to the blockchain, the first tx we need to submit is the one to initialize our state machine. That is, a tx creating a UTXO that will be locked at our supplyStateValidator address. It will contain our initial supply amount — which is zero — in the datum attached to it.. along with a couple other necessary info.

Since we don’t yet have our official thread token at this point, this will also be the tx that will mint that thread NFT.

Let’s put this function in a file named init-state.ts likewise inside src/lib/ directory:

import fs from "fs";
import path from "path";
import { getInitVars } from './init-vars.js';
import { getWitness } from './get-witness.js';
import { submitTx } from './submit.js';
import { getAddressOutputs } from './get-addr-outputs.js';
import {
Datum,
Tx,
TxOutput,
Value,
Signature,
Assets
} from "@hyperionbt/helios";

type Vars = Awaited<ReturnType<typeof getInitVars>>;
const envPath = path.join(process.cwd(), ".env");

export async function initializeSuppyState(vars: Vars){
// build tx
const tx = new Tx();

// create empty redeemer for threadToken because we must always send a Redeemer in
// a plutus script transaction even if we don't actually use it.
const threadTokenMintRedeemer = vars.dummyPlutusData; // new ConstrData(0, []);

// add mint of the thread token:
tx.mintTokens(
vars.threadTokenPolicyHash,
[
[vars.TN_threadToken, BigInt(1)]
],
threadTokenMintRedeemer
)

// add thread token minting script:
tx.attachScript(vars.threadTokenCompiledProgram);

// create initial supplyState Datum:
const initSupplyStateDatum = new vars.supplyStateProgram.types.Datum(
vars.assetClass_cip68Ref,
vars.assetClass_cip68Usr,
BigInt(0),
BigInt(0)
)._toUplcData();

// add output (supply state thread token):
const supplyStateAssets = new Assets([[vars.assetClass_supplyState, BigInt(1)]]);
const supplyStateOutput = new TxOutput(
vars.supplyStateValidatorAddr,
new Value(undefined, supplyStateAssets),
Datum.inline(initSupplyStateDatum)
);
tx.addOutput(supplyStateOutput);

// add inputs:
tx.addInputs(vars.sortedUtxos[0]);

// add admin signer
tx.addSigner(vars.seedAcct.spendingKeyHash);

// finalize:
await tx.finalize(vars.networkParams, vars.changeAddr, vars.sortedUtxos[1]);

// -------------------------------------
// sign tx:
// -------------------------------------
const adminWitness = getWitness(tx, vars.seedAcct.spendingPrivKey, vars) as Signature;
tx.addSignature(adminWitness);

// -------------------------------------
// get outputs for use in the next tx:
// -------------------------------------
const returnOutputs = getAddressOutputs(tx, vars.seedAcct.spendingAddr);
const scriptOutputs = getAddressOutputs(tx, vars.supplyStateValidatorAddr);

// -------------------------------------
// submit tx:
// -------------------------------------
const txId = await submitTx(tx);
console.log("init state tx submitted!");
console.log("txId: ", txId);

// Update env vars in file:
const env = fs.readFileSync(envPath).toString().split(/\r?\n/g);
for(let lineNo in env) {
if(env[lineNo].startsWith("THREAD_NFT_MPH=")) {
env[lineNo] = `THREAD_NFT_MPH=${vars.threadTokenPolicyHash.hex}`;
}
}
const newEnv = env.join("\n");
fs.writeFileSync(envPath, newEnv);
// also update the env vars in the current running instance:
process.env.THREAD_NFT_MPH = vars.threadTokenPolicyHash.hex;

return {
returnOutputs,
scriptOutputs
}
}

After minting our official thread token, the above function will save the policy ID (minting policy hash) of our thread token into the .env file. We need to make sure we note down this policy ID because going forward, this is the token we will always look for, in order to get the official datum containing our current supply. Any succeeding mints and burns will require querying the blockchain for this policy ID.

mint-tokens.ts

Finally, we get to our main script to mint our CIP-68 token. Let’s put this in a file named mint-tokens.ts and this time, let’s put this in the src/cli/ directory.

import { getInitVars } from '../lib/init-vars.js';
import { getWitness } from '../lib/get-witness.js';
import { initializeSuppyState } from '../lib/init-state.js';
import { submitTx } from '../lib/submit.js';
import {
Datum,
Tx,
TxInput,
TxOutput,
Value,
textToBytes,
ConstrData,
ByteArrayProps,
ByteArrayData,
HIntProps,
IntData,
MapData,
Signature,
Assets
} from "@hyperionbt/helios";

type Vars = Awaited<ReturnType<typeof getInitVars>>;
const vars:Vars = await getInitVars();

// Initialize state machine if not yet started
if (!vars.existingThreadNftMPH){
const results = await initializeSuppyState(vars);
vars.txInputs = results.returnOutputs;
vars.supplyStateUtxo = results.scriptOutputs[0];
}

// build tx
const tx = new Tx();

// mints
const mintAmt = process.env.MINT_AMT as unknown as number;
const tokensToMint:[ByteArrayProps, HIntProps][] = [ [vars.TN_mainUsrTokenCip68, BigInt(mintAmt)] ];
if (!vars.cip68RefTokenUtxo){
tokensToMint.push([vars.TN_mainRefTokenCip68, BigInt(1)]);
}
tx.mintTokens(vars.mainTokenPolicyHash, tokensToMint, vars.dummyPlutusData);

// build updated supplyState Datum:
const updatedSupply = Number(mintAmt) + vars.mainTokenCurrentSupply;
const updatedSupplyStateDatum = new vars.supplyStateProgram.types.Datum(
vars.assetClass_cip68Ref,
vars.assetClass_cip68Usr,
BigInt(1),
BigInt(updatedSupply)
);

// build supplyState Redeemer:
const stateSupplyRedeemer = new vars.supplyStateProgram.types.Redeemer.Mint();

// inputs
for (let input of vars.txInputs){
// don't spend the UTXO containing the ref token datum
if (! input.value.assets.has(vars.mainTokenPolicyHash, vars.TN_mainRefTokenCip68)) tx.addInput(input);
}
if (vars.supplyStateUtxo) tx.addInput(vars.supplyStateUtxo as TxInput, stateSupplyRedeemer._toUplcData());

/* --------------------------------------------------------------
* outputs
* -------------------------------------------------------------- */
// supplyStateOutput
const supplyStateAsset = new Assets([ [vars.assetClass_supplyState, BigInt(1)] ]);
tx.addOutput(new TxOutput(
vars.supplyStateValidatorAddr,
new Value(undefined, supplyStateAsset),
Datum.inline(updatedSupplyStateDatum._toUplcData())
));

// cip68 ref token output (only needed once)
// note: it is good practice to lock this token into its own script address
// in order to avoid spending this UTXO and losing the ref datum info.
if (!vars.cip68RefTokenUtxo){
// cip68 ref token datum:
const nameKey = new ByteArrayData(textToBytes("name"));
const nameValue = new ByteArrayData(textToBytes("Digital Asset"));
const descKey = new ByteArrayData(textToBytes("description"));
const descValue = new ByteArrayData(textToBytes("A very valuable Cardano native token"));
const tickerKey = new ByteArrayData(textToBytes("ticker"));
const tickerValue = new ByteArrayData(textToBytes("DigAss"));
const urlKey = new ByteArrayData(textToBytes("url"));
const urlValue = new ByteArrayData(textToBytes("https://staking.rocks"));
const decimlsKey = new ByteArrayData(textToBytes("decimals"));
const decimlsValue = new IntData(BigInt(6));
const logoKey = new ByteArrayData(textToBytes("logo"));
const logoValue = new ByteArrayData(textToBytes("ipfs://QmVb3oW3Ctsp6FmQdUpqPKJ6D1CJKsV1AgE1s8s8geuK94/DigAss.svg"));
const mapData = new MapData([
[nameKey, nameValue],
[descKey, descValue],
[tickerKey, tickerValue],
[urlKey, urlValue],
[decimlsKey, decimlsValue],
[logoKey, logoValue]
]);
const version = new IntData(BigInt(1));
const refTokenDatum = new ConstrData(0, [mapData, version]);
const cip68RefAsset = new Assets([ [vars.assetClass_cip68Ref, BigInt(1)] ]);
tx.addOutput(new TxOutput(
vars.seedAcct.spendingAddr,
new Value(undefined, cip68RefAsset),
Datum.inline(refTokenDatum)
));
}

// cip68 user token output
const cip68UsrAsset = new Assets([ [vars.assetClass_cip68Usr, BigInt(mintAmt)] ]);
tx.addOutput(new TxOutput(
vars.seedAcct.spendingAddr,
new Value(undefined, cip68UsrAsset)
));

/* --------------------------------------------------------------
* minting and state validator scripts
* -------------------------------------------------------------- */
// main token policy script
tx.attachScript(vars.mainTokenCompiledProgram);

// supply state validator script
tx.attachScript(vars.supplyStateCompiledProgram);

/* --------------------------------------------------------------
* finalize and submit
* -------------------------------------------------------------- */
// add admin signer (not really necessary if the tx maker acct is the same as the admin acct)
tx.addSigner(vars.seedAcct.spendingKeyHash);

// finalize:
await tx.finalize(vars.networkParams, vars.changeAddr);

// sign
const txMakerWitness = getWitness(tx, vars.seedAcct.spendingPrivKey, vars) as Signature;
tx.addSignature(txMakerWitness);

const txId = await submitTx(tx);
console.log("mint tx successfully submitted!");
console.log("txId: ", txId);

What we do here is we first check if we already have an existing thread token. If we don’t, that means it’s the first time we ran the script and we have to initialize our state machine. So we proceed to minting our official thread token, and initialize the supply info to zero in the output datum of that tx. That output is locked at our supplyStateValidator address.

Immediately right after that, we chain the transaction that actually mints our main CIP-68 token. In the same tx, we also update the current supply contained in our datum state machine.

The first time we mint our main token, we mint 2 assets — the CIP-68 reference token, and the main user token which is the one that will be held by users.

In succeeding runs, this script will not mint any more reference tokens since CIP-68 specifies there should only be 1 ref token for each asset.

Run

First, make sure that the seed account provided in the .env file already has some ADA in it. To run our minting script, we can just issue the following CLI command from the project’s root directory:

node --no-warnings --loader=./loader.js src/cli/mint-tokens.ts

It will mint the amount specified in the MINT_AMT variable in the .env file.

Found this useful?

Check out Staking Rocks! [PHRCK] stake pool and consider staking with us.

--

--

Staking Rocks! [PHRCK]

Highly reliable stake pool and validator node operator for Cardano, Umbrella Oracle and World Mobile. https://staking.rocks | https://twitter.com/PHRCKpool