/*************************************************
* Coxylib.js
* ----------------------------------------------
* Version : 1.1.1
* Author : Bernard Sibanda (Coxygen Global Pty Ltd)
* Company : Coxygen Global
* Date Started : 2024
* License : MIT License
*
* Description:
* Coxylib is a set of atomic functions simplifying use of the Helios
* smart contract library for Cardano blockchain development.
*
* Installation:
* Import this `coxylib.js` file into any project, even to static websites.
* (Dependencies: Helios and Jimba libraries) as show below:
*
*
* Advantages:
* - Provides testable atomic functions for Cardano blockchain integration
* - Simplifies and speeds up decentralized Cardano development
* - Easy deployment (even works on cPanel hosting)
* - Bundled with testing library (jimba.js) and standalone Helios.js
* - Improved console logging (toggle on/off)
* - 100% client-side dApp development
* - No need for npm, Node.js, or other bloated packages
* - Implements best practices (code reuse, functional programming, etc.)
* ----------------------------------------------
*/
import {
bytesToHex,
Cip30Wallet,
WalletHelper,
TxOutput,
Assets,
bytesToText,
hexToBytes,
AssetClass,
BlockfrostV0,
PubKeyHash,
IntData,
MapData,
StakeAddress,
Tx,
Address,
NetworkParams,
Value,
MintingPolicyHash,
Program,
ByteArrayData,
ConstrData,
NetworkEmulator,
UTxO,
TxId,
Datum,
ListData,
RootPrivateKey,
textToBytes,
dumpCostModels,
config
} from "./helios-min.js";
import { opt, j } from "./jimba.js";
//below links are option but necessary for blockchain interaction and alerts
import { generateMnemonic } from "https://esm.sh/@scure/bip39@1.6.0";
import { wordlist as english } from "https://esm.sh/@scure/bip39@1.6.0/wordlists/english";
import { mnemonicToEntropy, validateMnemonic } from "https://esm.sh/bip39@3.1.0";
/**
* An item returned by token scanners.
* @typedef {Object} TokenInfo
* @property {string} policy
* @property {string} tokenName
* @property {bigint} tokenQuantity
*/
/**
* Generic key/value (string → any).
* @typedef {Object} KV
* @property {string} key
* @property {*} value
*/
/**
* Pair of bytes used in CIP-68 map entries.
* @typedef {Object} BytePair
* @property {ByteArrayData} k
* @property {ByteArrayData} v
*/
/**
* CIP-68 "asset class" (token name bytes + quantity).
* @typedef {Object} CIP68AssetClass
* @property {Uint8Array} nameBytes
* @property {bigint} qty
*/
/**
* One output item for txCreateOutput.
* @typedef {Object} TokenOutputItem
* @property {string} tokenTN
* @property {(bigint|number)} tokenQty
* @property {string} mphHex
* @property {(bigint|number)} lovelace
*/
/****************************************************
* hlib.js
*
* Description:
* This module exports utility objects and constants
* used for handling Cardano transactions, values,
* network parameters, and related data types.
*
****************************************************/
/**
* Collection of Helios and utility bindings for blockchain interaction.
* Provides access to core transaction, address, value, and program tools.
*
* @namespace hlib
* @property {Value} Value - Represents ADA or token values.
* @property {Tx} Tx - Represents a transaction.
* @property {Assets} Assets - Asset handling (tokens, ADA, etc.).
* @property {NetworkParams} NetworkParams - Network parameter utilities.
* @property {ByteArrayData} ByteArrayData - Byte array wrapper for on-chain data.
* @property {BigInt} BigInt - BigInt for handling large numbers.
* @property {ConstrData} ConstrData - On-chain constructor data.
* @property {TxOutput} TxOutput - Transaction output representation.
* @property {Address} Address - Address utility.
* @property {Function} hexToBytes - Convert hex string to byte array.
* @property {Program} Program - Helios program representation.
* @property {MintingPolicyHash} MintingPolicyHash - Identifier for minting policies.
*/
export const hlib = {
Value: Value,
Tx: Tx,
Assets: Assets,
NetworkParams: NetworkParams,
ByteArrayData: ByteArrayData,
BigInt: BigInt,
ConstrData: ConstrData,
TxOutput: TxOutput,
Address: Address,
hexToBytes: hexToBytes,
Program: Program,
MintingPolicyHash: MintingPolicyHash
};
/**
* Default prerequisites/configuration for transactions.
* Used to set fee limits, minimum ADA requirements, and network parameters.
*
* @typedef {Object} TxPrerequisites
* @property {bigint} maxTxFee - Maximum transaction fee (in lovelace).
* @property {bigint} minChangeAmt - Minimum amount allowed for change outputs (in lovelace).
* @property {string} networkParamsUrl - Path or URL to the network parameters JSON file.
* @property {bigint} minAda - Minimum ADA required for a UTxO (in lovelace).
* @property {number} ntype - Network type: `0` = testnet, `1` = mainnet.
*/
/** @type {TxPrerequisites} */
export const txPrerequisites = {
maxTxFee: BigInt(3000000), // Maximum transaction fee (3 ADA lovelace)
minChangeAmt: BigInt(3000000), // Minimum change output (3 ADA lovelace)
networkParamsUrl: "./params/preprod.json", // URL to pre-production network parameters
minAda: BigInt(4000000), // Minimum ADA required for a UTxO (4 ADA lovelace)
ntype: 0 // Network type (0 = testnet, 1 = mainnet)
};
/**
* Mint new tokens on the Cardano blockchain.
*
* @async
* @function mint
* @param {string} ticket - Compiled smart contract source.
* @param {Object} data - Minting metadata and user input.
* @returns {Promise<void|string>}
*/
export const mintNew = async (ticket, data) => {
const minAda = txPrerequisites.minAda;
const maxTxFee = txPrerequisites.maxTxFee;
const minChangeAmt = txPrerequisites.minChangeAmt;
const baseAddress = data.address;
const ownerPKH = baseAddress.pubKeyHash;
const ownerBytes = ownerPKH.bytes;
const utxos = await getAllTxInputs(baseAddress);
const tx = new Tx();
tx.addInputs(utxos);
const utxoId = utxos[0].outputId.txId.hex;
const utxoIdx = utxos[0].outputId.utxoIdx;
const assetNameHex = utf8ToHex(data.assetName);
const script = Program.new(ticket);
script.parameters = { ownerPKH: ownerBytes, TN: assetNameHex };
const compiled = script.compile(true);
const mph = compiled.mintingPolicyHash.hex;
tx.attachScript(compiled);
const redeemer = new compiled.types.Redeemer.Mint(
hexToBytes(utxoId),
utxoIdx,
BigInt(data.quantity)
);
const redeemerData = redeemer._toUplcData ? redeemer._toUplcData() : redeemer.toData();
const tokens = [[textToBytes(assetNameHex), BigInt(data.quantity)]];
const assets = new Assets([[mph, tokens]]);
const minAdaVal = new Value(BigInt(minAda));
tx.mintTokens(mph, tokens, redeemerData);
tx.addOutput(new TxOutput(baseAddress, new Value(minAdaVal.lovelace, assets)));
tx.addSigner(ownerPKH);
const networkParams = new hlib.NetworkParams(await fetch(txPrerequisites.networkParamsUrl).then(res => res.json()));
const unsignedTx = await tx.finalize(networkParams, baseAddress, utxos);
unsignedTx.addSignature(data.recipientPayKey.sign(unsignedTx.bodyHash));
const txHash = await TEST_BLOCKFROST.submitTx(unsignedTx);
const txh = txHash.hex;
if (txh && txh.trim().length === 64) {
data.processMsg.innerHTML = "...now saving the results.";
swal({
title: "Congratulations, transaction is now on blockchain!",
text: "Here is the txHash : " + txh,
icon: "success",
buttons: [true, "Yes"],
dangerMode: true,
});
const url = `<a href='https://preprod.cexplorer.io/tx/${txh}'>${txh}</a>`;
data.processMsg.innerHTML = url;
data.btnElem.style.display = "block";
} else {
return "failed txh";
}
};
/**
* Send ADA to another address.
*
* @async
* @function sendADA
* @param {string} toAddress - Destination address in bech32.
* @param {number|string} amountToTransfer - Amount of ADA to send.
* @param {object} pk - Payment signing key.
* @returns {Promise<string>} Transaction hash or error.
*/
export const sendADANew = async (toAddress, amountToTransfer, pk) => {
try {
el("formSpanSendADA").style.display = "none";
el("processMsgSendADA").innerHTML = "...wait processing task!";
const baseAddressB32 = "<?=$_SESSION['address']?>";
const baseAddress = hlib.Address.fromBech32(baseAddressB32);
const ownerPKH = baseAddress.pubKeyHash;
const utxos = await getAllTxInputs(baseAddress);
const amountLovelace = Number(amountToTransfer) * 1_000_000;
const tx = new hlib.Tx();
tx.addInputs(utxos);
tx.addOutput(new hlib.TxOutput(hlib.Address.fromBech32(toAddress), new hlib.Value(BigInt(amountLovelace))));
tx.addSigner(ownerPKH);
const networkParams = new hlib.NetworkParams(await fetch(txPrerequisites.networkParamsUrl).then(res => res.json()));
const unsignedTx = await tx.finalize(networkParams, baseAddress, utxos);
unsignedTx.addSignature(pk.sign(unsignedTx.bodyHash));
const txHash = await TEST_BLOCKFROST.submitTx(unsignedTx);
const txh = txHash.hex;
if (txh && txh.trim().length === 64) {
swal({
title: "Congratulations, transaction was a success!",
text: `Here is the txHash : ${txh}`,
icon: "success",
buttons: [true, "Yes"],
dangerMode: true,
});
const url = `<a href='https://preprod.cexplorer.io/tx/${txh}'>${txh}</a>`;
el("processMsgSendADA").innerHTML = url;
return txh;
} else {
return "failed txh";
}
} catch (error) {
return error.message;
}
};
/**
* Send native assets to a recipient.
*
* @async
* @function sendAssets
* @param {Object} data - Transaction input/output details and metadata.
* @returns {Promise<string>} Transaction hash or failure message.
*/
export const sendAssetsNew = async (data) => {
data.clickBtnElem.style.display = "none";
const baseAddress = hlib.Address.fromBech32(data.address);
const ownerPKH = baseAddress.pubKeyHash;
const utxos = data.utxos;
const tokenNameHex = bytesToHex(textToBytes(data.assetName));
const assets = new hlib.Assets();
assets.addComponent(data.assetMPH, tokenNameHex, BigInt(data.assetQty));
const tx = new hlib.Tx();
tx.addInputs(utxos);
tx.addOutput(new hlib.TxOutput(hlib.Address.fromBech32(data.toAddress), new hlib.Value(txPrerequisites.minAda, assets)));
tx.addSigner(ownerPKH);
tx.balanceAssets();
const networkParams = new hlib.NetworkParams(await fetch(txPrerequisites.networkParamsUrl).then(res => res.json()));
const unsignedTx = await tx.finalize(networkParams, baseAddress, utxos);
unsignedTx.addSignature(data.pk.sign(unsignedTx.bodyHash));
const txHash = await TEST_BLOCKFROST.submitTx(unsignedTx);
const txh = txHash.hex;
if (txh && txh.trim().length === 64) {
swal({
title: "Congratulations, transaction was a success!",
text: `Here is the txHash : ${txh}`,
icon: "success",
buttons: [true, "Yes"],
dangerMode: true,
});
const url = `<a href='https://preprod.cexplorer.io/tx/${txh}'>${txh}</a>`;
data.displayElem.innerHTML = url;
return txh;
} else {
return "failed txh";
}
};
/**
* Retrieve payment and staking keys using passphrase and PIN.
*
* @async
* @function pkeys
* @param {string} PinCode - User PIN.
* @param {string} PrimaryEmail - User email.
* @param {string} PassPhrase - Mnemonic decryption password.
* @returns {Promise<{pk: object, pkh: object, addr: object}>}
*/
export const pkeys = async (PinCode, PrimaryEmail, PassPhrase) => {
const url = "./keys.php";
const encryptedKeysDb = await getEncryptedData(url, {
pin: PinCode,
postStatus: "get",
PrimaryEmail
});
const decryptedKeysDb = await decryptMnemonic(
PassPhrase,
encryptedKeysDb.salt,
encryptedKeysDb.iv,
encryptedKeysDb.ct
);
const cleanWords = decryptedKeysDb.split(",").flatMap(w => w.trim().split(/\s+/));
const rootKey = RootPrivateKey.fromPhrase(cleanWords);
const rootPrivateKey = rootKey.deriveSpendingRootKey(0);
const payKey = rootPrivateKey.derive(1).derive(0);
const pkh = payKey.derivePubKey().pubKeyHash;
const stakingRootKey = rootKey.deriveStakingRootKey(0);
const stakeKey = stakingRootKey.derive(0);
const stakeHash = stakeKey.derivePubKey().pubKeyHash;
const addr = Address.fromPubKeyHash(pkh, stakeHash, config.IS_TESTNET);
return { pk: payKey, pkh, addr };
};
/**
* Parse and extract asset metadata from UTXOs.
*
* @async
* @function assetFunc
* @param {Array} utxos - UTXO array.
* @returns {Promise<Array<{mph: string, tokenName: string, qty: number}>>}
*/
export const assetFuncNew = async (utxos) => {
const baseAddressB32 = "<?=$_SESSION['address']?>";
const baseAddress = hlib.Address.fromBech32(baseAddressB32);
const baseAddressPKH = baseAddress.pubKeyHash;
const index = 0;
const utxos0 = utxos[index];
if (!utxos0) return;
const assetArray = [];
const assets = utxos0.value.assets.assets;
if (!Array.isArray(assets) || assets.length === 0) return;
const [mphGroup, tokens] = assets[0];
const mph = mphGroup.hex;
tokens.map(x => {
const tokenName = hexToText(hexToText(x[0].hex));
const qty = x[1].value;
assetArray.push({ mph, tokenName, qty });
});
return assetArray;
};
/**
* Mint user property shares token.
*
* Builds, signs, and submits a transaction on-chain.
*
* @async
* @function mintPropertyUserSharesToken
* @param {string} mintPropertyUserSharesTokensScript - Helios source for minting script.
* @param {Object} info - Information object (UTxO, quantity, token metadata).
* @param {string} buySellTokenProperty - Helios program source for buy/sell.
* @returns {Promise<string>} Transaction hash or "failed txh".
*/
export const mintPropertyUserSharesToken = async (
mintPropertyUserSharesTokensScript, info, buySellTokenProperty
) => {
const wallet = await init(j); j.log({wallet});
const walletData = await walletEssentials(wallet,Cip30Wallet,WalletHelper,Value,txPrerequisites.minAda,j); j.log({walletData});
const minAda = txPrerequisites.minAda; j.log({minAda})
const maxTxFee = txPrerequisites.maxTxFee; j.log({maxTxFee})
const minChangeAmt = txPrerequisites.minChangeAmt; j.log({minChangeAmt})
const minAdaVal = new Value(BigInt(minAda)); j.log({minAdaVal})
const minUTXOVal = new Value(BigInt(minAda + maxTxFee + minChangeAmt)); j.log({minUTXOVal})
const baseAddress = await walletData.walletHelper.baseAddress; j.log({baseAddress})
const changeAddr = await walletData.walletHelper.changeAddress; j.log({changeAddr})
const ownerPKH = baseAddress.pubKeyHash; j.log({ownerPKH})
const utxos = await walletData.utxos; j.log({utxos})
const walletAPI = await walletData.walletAPI; j.log({walletAPI})
const baseAddressPKH = baseAddress.pubKeyHash; j.log({baseAddressPKH})
const baseAddressPKHHex = baseAddressPKH.hex; j.log({baseAddressPKHHex})
const baseAddressBech32 = baseAddress.toBech32(); j.log({baseAddressBech32})
const baseAddressStakingHash = baseAddress.stakingHash; j.log({baseAddressStakingHash})
const ownerBytes = baseAddressPKH.bytes; j.log({ownerBytes})
const addressFromHashAndStakingKeyHash = Address.fromHashes(baseAddressPKH,baseAddressStakingHash).toBech32();j.log({addressFromHashAndStakingKeyHash})
const buyselltokensProgram = Program.new(buySellTokenProperty);
const buyselltokensProgramCompiled = buyselltokensProgram.compile(false); j.log({buyselltokensProgramCompiled})
const scriptAddressValidatorHash = buyselltokensProgramCompiled.validatorHash; j.log({scriptAddressValidatorHash})
const scriptAddressValidatorHashHex = scriptAddressValidatorHash.hex; j.log({scriptAddressValidatorHashHex})
const scriptAddress = Address.fromHashes(buyselltokensProgramCompiled.validatorHash); j.log({scriptAddress})
const scriptAddressBech32 = scriptAddress.toBech32(); j.log({scriptAddressBech32})
const scriptAddressRecoveredFromBech32 = Address.fromBech32(scriptAddressBech32); j.log({scriptAddressRecoveredFromBech32})
swal({
title: "Minting Property Tokens",
text: "Be patient as we build and submit transaction to blockchain.",
icon: "success",
buttons: false,
timer: 1000,
dangerMode: true,
})
const tx = new Tx();
tx.addInputs(utxos[0]);
const utxoId = info.utxoId; j.log({utxoId})
const utxoIdx = info.utxoIdx; j.log({utxoIdx})
const PropertyUserSharesTokens = Program.new(mintPropertyUserSharesTokensScript);
const tokenNm = info.assetName; j.log({tokenNm})
const tokenName_ = textToBytes(tokenNm); j.log({tokenName_})
const tokenNameHex = bytesToHex(tokenName_); j.log({tokenNameHex})
PropertyUserSharesTokens.parameters = {["ownerPKH"] : baseAddress.pubKeyHash};
PropertyUserSharesTokens.parameters = {["TN"] : tokenName_};
const PropertyUserSharesTokensCompiled = PropertyUserSharesTokens.compile(false); j.log({PropertyUserSharesTokensCompiled})
const scriptReferenceTokenCompiledPolicyHashHexMPH = PropertyUserSharesTokensCompiled.mintingPolicyHash.hex; j.log({scriptReferenceTokenCompiledPolicyHashHexMPH}) ;
tx.attachScript(PropertyUserSharesTokensCompiled);
const tokens = [[tokenNameHex, BigInt(info.quantity)]];
const assets = new Assets([[scriptReferenceTokenCompiledPolicyHashHexMPH,tokens]]); j.log({assets});
info.mph = scriptReferenceTokenCompiledPolicyHashHexMPH;
const mintRedeemer = (new PropertyUserSharesTokens.types.Redeemer.Mint(hexToBytes(utxoId),BigInt(utxoIdx),BigInt(info.quantity)))._toUplcData();
tx.mintTokens(scriptReferenceTokenCompiledPolicyHashHexMPH,tokens,mintRedeemer);
tx.addOutput(new TxOutput(baseAddress,new Value(hlib.minAda, assets)));
tx.addSigner(ownerPKH);
info.policyId = scriptReferenceTokenCompiledPolicyHashHexMPH; j.log({info})
tx.addMetadata(721,generateMetadata(info));
const txh = await txEnd(tx); j.log({txh});
if(txh.toString().trim().length == 64)
{
swal({
title: "Congratulations, transaction is now on blockchain!",
text: "Here is the txHash : "+txh,
icon: "success",
buttons: [true,"Yes"],
dangerMode: true,
})
const url = "<a href='https://preprod.cexplorer.io/tx/"+txh+"'>"+txh+"</a>"
el("txhash").innerHTML = url;
const saved = await save_(tokenNameHex,scriptReferenceTokenCompiledPolicyHashHexMPH,utxoId,utxoIdx,info.quantity,
ownerPKH,txh,"open",info.tokenName, info.uid, info.pid,info.sharesTokenPrice,scriptAddressBech32,baseAddressBech32,null,info.nftId); j.log({saved})
window.location.reload();
return txh;
}
else
{
return "failed txh";
}
};
/**
* Convert a Bech32 address to its public key hash (PKH).
*
* @async
* @function convertBech32ToPkh
* @param {string} addressBech32 - Cardano address (bech32 format).
* @returns {Promise<any>} PKH object.
*/
export const convertBech32ToPkh = async (addressBech32) => {
const addressFromB32 = Address.fromBech32(addressBech32);
j.log({ addressFromB32 });
const addrPKH = addressFromB32.pubKeyHash;
j.log({ addrPKH });
return addrPKH;
};
/**
* Fetch key tokens from backend.
*
* @async
* @function getKeyTokens
* @returns {Promise<void>}
*/
export const getKeyTokens = async () => {
const xhttp = new XMLHttpRequest();
xhttp.onload = async function () {
if (this.responseText) {
const referenceToken = JSON.parse(this.responseText)[0];
j.log({ referenceToken });
save("referenceToken", JSON.stringify(referenceToken));
}
};
xhttp.open("GET", "./getKeyTokens.php", true);
xhttp.send();
};
/****************************************************
* cardanoUtils.js
*
* Description:
* Utility functions for Cardano wallet management,
* transaction building, cryptography (mnemonics,
* password strength, encryption), and vesting contracts.
* Includes network parameter initialization and helpers.
*
****************************************************/
/**
* Sum ADA from all UTXOs of an address and submit
* a transaction that consolidates them.
*
* @async
* @function sumUTXOADA
* @param {string} address - Bech32 wallet address.
* @param {TxInput} TxInput - Transaction input helper.
* @param {any} addressPKH - Address public key hash.
* @param {RootPrivateKey} addressRootPrivateKey - Root private key.
* @param {NetworkParams} TEST_NETWORK_PARAMS_PREPROD - Network parameters.
* @param {BlockfrostV0} TEST_BLOCKFROST - Blockfrost client instance.
* @returns {Promise<string>} Transaction hash in hex.
*/
export const sumUTXOADA = async (
address, TxInput, addressPKH, addressRootPrivateKey,
TEST_NETWORK_PARAMS_PREPROD, TEST_BLOCKFROST
) => {
const utxos = await getAllTxInputs(address); j.log({ utxos });
const totalValue = TxInput.sumValue(utxos); j.log({ totalValue });
const tx = Tx.new().addInputs(utxos).addSigner(addressPKH); j.log({ tx });
const unsignedTx = await tx.finalize(TEST_NETWORK_PARAMS_PREPROD, address, []);
unsignedTx.addSignature(addressRootPrivateKey.sign(unsignedTx.bodyHash)); j.log({ unsignedTx });
const txId = await TEST_BLOCKFROST.submitTx(unsignedTx); j.log({ txId });
return txId.hex;
};
/**
* Show Plutus cost model details from network parameters.
*
* @function showPlutusCostModels
* @param {NetworkParams} TEST_NETWORK_PARAMS_PREPROD - Network parameters instance.
* @returns {void}
*/
export const showPlutusCostModels = (TEST_NETWORK_PARAMS_PREPROD) => {
dumpCostModels(TEST_NETWORK_PARAMS_PREPROD);
const rawPlutusV2CostModel = TEST_NETWORK_PARAMS_PREPROD.raw.latestParams.costModels.PlutusScriptV2; j.log({ rawPlutusV2CostModel });
const sortedCostModelParams = TEST_NETWORK_PARAMS_PREPROD.sortedCostParams; j.log({ sortedCostModelParams });
const startupCost = TEST_NETWORK_PARAMS_PREPROD.plutusCoreStartupCost; j.log({ startupCost });
const variableCost = TEST_NETWORK_PARAMS_PREPROD.plutusCoreVariableCost; j.log({ variableCost });
const lambdaCost = TEST_NETWORK_PARAMS_PREPROD.plutusCoreLambdaCost; j.log({ lambdaCost });
const executionUnitPricesMemPriceCpuPrice = TEST_NETWORK_PARAMS_PREPROD.exFeeParams; j.log({ executionUnitPricesMemPriceCpuPrice });
const feeParametersFixedPerByte = TEST_NETWORK_PARAMS_PREPROD.txFeeParams; j.log({ feeParametersFixedPerByte });
const maxExecutionBudgetMemUnitsCpuSteps = TEST_NETWORK_PARAMS_PREPROD.maxTxExecutionBudget; j.log({ maxExecutionBudgetMemUnitsCpuSteps });
const maxTxSize = TEST_NETWORK_PARAMS_PREPROD.maxTxSize; j.log({ maxTxSize });
const minCollateral = TEST_NETWORK_PARAMS_PREPROD.minCollateralPct; j.log({ minCollateral });
const maxCollateralInputs = TEST_NETWORK_PARAMS_PREPROD.maxCollateralInputs; j.log({ maxCollateralInputs });
const [feeFixed, feePerByte] = TEST_NETWORK_PARAMS_PREPROD.txFeeParams; j.log({ feeFixed, feePerByte });
const lovelacePerUTXOByte = TEST_NETWORK_PARAMS_PREPROD.lovelacePerUTXOByte; j.log({ lovelacePerUTXOByte });
const changePlaceholder = new TxOutput(funderAddr, new Value(0n));
const minAdaForChange = changePlaceholder.calcMinLovelace(TEST_NETWORK_PARAMS_PREPROD);
const maxFee = BigInt(feeFixed) + BigInt(feePerByte) * BigInt(TEST_NETWORK_PARAMS_PREPROD.maxTxSize);
const totalNeeded = amount + minAdaForChange + maxFee;
const estimatedFee = txBuilder.estimateFee(TEST_NETWORK_PARAMS_PREPROD); j.log({ estimatedFee });
const totalAvailable = utxos.reduce((sum, u) => sum + u.value.lovelace, 0n); j.log({ totalAvailable });
if (totalAvailable >= totalNeeded) {
const msgNotEnoughAda = "Not enough ADA to cover all costs!";
j.log({ msgNotEnoughAda });
}
};
/**
* Create a BIP39 mnemonic.
*
* @async
* @function createMnemonic
* @param {number} [bit=256] - Bit strength.
* @returns {Promise<string[]>} Array of words.
*/
export const createMnemonic = async (bit = 256) => {
const obj = { words: null, count: 0 };
do {
obj.words = await generateMnemonic(english, bit);
obj.count++;
} while (!validateMnemonic(obj.words));
return obj.words.toString().trim().split(/\s+/);
};
/**
* Encrypt mnemonic with passphrase (Argon2id + AES-GCM).
*
* @async
* @function encryptMnemonic
* @param {string} passphrase - Passphrase.
* @param {string} mnemonic - Mnemonic string.
* @returns {Promise<Object>} { salt, iv, ct }
*/
export const encryptMnemonic = async (passphrase, mnemonic) => {
const salt = crypto.getRandomValues(new Uint8Array(16));
const argon = await argon2.hash({
pass: passphrase, salt,
time: 2, mem: 64 * 1024, parallelism: 1,
hashLen: 32, type: argon2.ArgonType.Argon2id,
});
const aesKey = await crypto.subtle.importKey("raw", argon.hash.buffer, { name: "AES-GCM", length: 256 }, false, ["encrypt"]);
const iv = crypto.getRandomValues(new Uint8Array(12));
const cipherBuf = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
aesKey,
new TextEncoder().encode(mnemonic)
);
return { salt: bufToB64(salt.buffer), iv: bufToB64(iv.buffer), ct: bufToB64(cipherBuf) };
};
/**
* Decrypt mnemonic.
*
* @async
* @function decryptMnemonic
* @param {string} passphrase - Passphrase.
* @param {string} salt - Base64 salt.
* @param {string} iv - Base64 IV.
* @param {string} ct - Base64 ciphertext.
* @returns {Promise<string>} Decrypted mnemonic or "error".
*/
export const decryptMnemonic = async (passphrase, salt, iv, ct) => {
const saltBuf = b64ToBuf(salt);
const argon = await argon2.hash({
pass: passphrase, salt: new Uint8Array(saltBuf),
time: 2, mem: 64 * 1024, parallelism: 1, hashLen: 32,
type: argon2.ArgonType.Argon2id,
});
try {
const aesKey = await crypto.subtle.importKey("raw", argon.hash.buffer, { name: "AES-GCM", length: 256 }, false, ["decrypt"]);
const plainBuf = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: new Uint8Array(b64ToBuf(iv)) },
aesKey,
b64ToBuf(ct)
);
return new TextDecoder().decode(plainBuf);
} catch {
return "error";
}
};
/**
* Create 5 derived wallet addresses from mnemonic.
*
* @function createFiveWalletAddresses
* @param {string[]} mnemonic - BIP39 mnemonic words.
* @returns {Object[]} Array of derived addresses and keys.
*/
export const createFiveWalletAddresses = (mnemonic) => {
const root = RootPrivateKey.fromPhrase(mnemonic);
const account = 0, start = 0, count = 5;
const spendingRootKey = root.deriveSpendingRootKey(account);
const stakingRootKey = root.deriveStakingRootKey(account);
const out = [];
for (let i = start; i < start + count; i++) {
const stakeHash = stakingRootKey
.derive(i)
.derivePubKey()
.pubKeyHash;
const stakeAddr = StakeAddress.fromHash(
config.IS_TESTNET,
stakeHash
);
const payKey = spendingRootKey
.derive(1)
.derive(i);
const payHash = payKey
.derivePubKey()
.pubKeyHash;
const addr = Address.fromPubKeyHash(
payHash,
stakeHash,
config.IS_TESTNET
);
const baseAddress = Address.fromPubKeyHash(
payHash,
null,
config.IS_TESTNET
);
out.push({ paymentAddressBech32: addr.toBech32(), stakingAddress: stakeAddr, privateKeys :{spendingRootKey: spendingRootKey, stakingRootKey:stakingRootKey}, baseAddresss : baseAddress.toBech32() });
}
return out;
};
/**
* Token classification mapping.
*
* @constant
* @type {Object<string,string>}
*/
export const assetClassification = {
VoteToken: "Vote",
IdentityToken: "Identity",
BusinessCard: "Business",
FaithToken: "Faith",
GovernanceToken: "Governance",
CulturalToken: "Cultural",
SportToken: "Sport",
MusicToken: "Music",
MemeToken: "Meme",
CouponTokens: "Coupon",
TicketToken: "Ticket",
LoveToken: "Love",
AttendanceToken: "Attendance",
BirthDayToken: "Birth Day",
CongratulationsToken: "Congratulations",
CondolencesToken: "Condolences",
CertificateToken: "Certificate",
MillionnaireToken: "Millionnaire",
PetToken: "Pet",
FungibleToken: "FT",
NonFungibleToken: "NFT",
RichFungibleToken: "RFT"
};
/**
* Generate keys from phrase.
*
* @function genKeys
* @param {string} [phrase] - Mnemonic phrase.
* @returns {Object} { ownerPrivateKey, ownerPublicKeyHash, ownerAddress }
*/
export const genKeys = (phrase = "...") => {
const rootKey = RootPrivateKey.fromPhrase(phrase.toString().trim().split(" ")); j.log({rootKey})
const privateKey = rootKey.deriveSpendingKey(); j.log({privateKey})
const publicKey = privateKey.derivePubKey(); j.log({publicKey})
const a = publicKey.pubKeyHash; j.log({a})
const b = Address.fromPubKeyHash(a); j.log({b})
const address = (new Address(b.bytes)).toBech32(); j.log({address})
return {ownerPrivateKey:bytesToHex(privateKey.bytes),ownerPublicKeyHash:publicKey.pubKeyHash.hex, ownerAddress:address};
};
/**
* Unlock funds from a vesting contract.
*
* @async
* @function unLock
* @param {string} vesting - Helios script source.
* @param {string} [secretMessage="secret"] - Secret parameter.
* @returns {Promise<void>}
*/
export const unLock = async (vesting, secretMessage = "secret") => {
const utxos = await walletData.utxos; j.log({utxos})
const walletAPI = await walletData.walletAPI; j.log({walletAPI})
const vestingProgram = Program.new(vesting);
vestingProgram.parameters = {["SECRET"]: secretMessage};
const vestingProgramCompiled = vestingProgram.compile(true); j.log({vestingProgramCompiled})
const addrFromValidatorHash = await Address.fromHashes(vestingProgramCompiled.validatorHash); j.log({addrFromValidatorHash})
const vestingRedeemer = (new vestingProgram.types.Redeemer.Cancel)._toUplcData();
const allAddresses = await walletData.walletHelper.allAddresses; j.log({allAddresses})
const receiveAddress = allAddresses[0];
const tx = new Tx();
tx.attachScript(vestingProgramCompiled);
tx.addInputs(utxos[0]); j.log({tx});
const txInput = await txInputFromBlockfrost(addrFromValidatorHash); j.log({txInput});
tx.addInput(txInput, vestingRedeemer); j.log({tx})
tx.addSigner(receiveAddress.pubKeyHash);
const txh = await txEnd(tx); j.log({txh});
};
/**
* Lock funds in a vesting contract.
*
* @async
* @function lock
* @param {string} vesting - Helios script source.
* @param {number} [amount=10000000] - Amount in lovelace.
* @param {string} [secretMessage="secret"] - Secret parameter.
* @returns {Promise<void>}
*/
export const lock = async (vesting, amount = 10_000_000, secretMessage = "secret") => {
const utxos = await walletData.utxos; j.log({utxos})
const walletAPI = await walletData.walletAPI; j.log({walletAPI})
const changeAddr = await walletData.walletHelper.changeAddress; j.log({changeAddr})
const tx = new Tx();
tx.addInputs(utxos[0]); j.log({tx})
const vestingProgram = Program.new(vesting);
vestingProgram.parameters = {["SECRET"]: secretMessage};
const vestingProgramCompiled = vestingProgram.compile(true); j.log({vestingProgramCompiled})
const vHash = vestingProgramCompiled.validatorHash.hex; j.log({vHash})
const scriptAddress = Address.fromHashes(vestingProgramCompiled.validatorHash); j.log({scriptAddress})
const vestingDatum = new (vestingProgram.types.Datum)(changeAddr.pubKeyHash); j.log({vestingDatum})
const adaToSend = new Value(BigInt(amount)); j.log({adaToSend})
const lovelaceToSend = adaToSend.lovelace;
const value_ = new Value(lovelaceToSend); j.log({value_});
const inLineDatum = Datum.inline(vestingDatum); j.log({inLineDatum});
const txOutput = new TxOutput(scriptAddress, value_,inLineDatum); j.log({txOutput});
tx.addOutput(txOutput); j.log({tx});
const txh = await txEnd(tx); j.log({txh});
};
const getBlockfrostKey = async (getBlockfrostKeyUrl) => {
const xhttp = new XMLHttpRequest();
return new Promise((resolve, reject) => {
xhttp.onload = function () {
if (this.responseText) {
const key = this.responseText;
resolve(key);
} else {
reject("No Blockfrost key found");
}
};
xhttp.onerror = function () {
reject("Error fetching Blockfrost key");
};
xhttp.open("GET", getBlockfrostKeyUrl, true);
xhttp.send();
});
}
/****************************************************
* utxoUtils.js
*
* Description:
* Functions for interacting with UTXOs via Blockfrost
* and Koios APIs, fetching policy-specific assets,
* handling encrypted data, and minting NFTs.
*
****************************************************/
/**
* Fetch a single UTXO input from Blockfrost for a given script address.
*
* @async
* @function txInputFromBlockfrost
* @param {string} scriptAddressPkh - Script address PKH.
* @returns {Promise<any|null>} A UTXO object or null if not found.
*/
export const txInputFromBlockfrost = async (scriptAddressPkh) => {
const bfKey = await getBlockfrostKey("./getBlockfrostKey.php?key=blockfrost"); j.log({ bfKey });
const hBlockfrostApi = new BlockfrostV0("preprod", getBlockfrostKey);
const utxosResults = await hBlockfrostApi.getUtxos(scriptAddressPkh);
if (utxosResults.length === 0) return null;
const lastElem = utxosResults.length - 1;
const txInput = utxosResults[0] || utxosResults[lastElem];
return txInput || null;
};
/**
* Get all UTXOs for a given script address.
*
* @async
* @function getAllTxInputs
* @param {string} scriptAddressPkh - Script address PKH.
* @returns {Promise<any[]>} Array of UTXOs.
*/
export const getAllTxInputs = async (scriptAddressPkh) => {
const bfKey = await getBlockfrostKey("./getBlockfrostKey.php?key=blockfrost"); j.log({ bfKey });
const hBlockfrostApi = new BlockfrostV0("preprod", getBlockfrostKey);
return await hBlockfrostApi.getUtxos(scriptAddressPkh);
};
/**
* Get UTXOs from a bech32 address via Blockfrost.
*
* @async
* @function utxoFromBech32
* @param {string} addressBech32 - Address in bech32 format.
* @returns {Promise<any[]>} Array of UTXOs.
*/
export const utxoFromBech32 = async (addressBech32) => {
const url = `https://cardano-preprod.blockfrost.io/api/v0/addresses/${addressBech32}/utxos?order=asc`;
try {
const bfKey = await getBlockfrostKey("./getBlockfrostKey.php?key=blockfrost"); j.log({ bfKey });
const hBlockfrostApi = new BlockfrostV0("preprod", getBlockfrostKey);
if (response.status === 404) return [];
let all = await response.json();
if (all?.status_code >= 300) all = [];
return await Promise.all(all.map(obj => this.restoreTxInput(obj)));
} catch (e) {
if (e.message.includes("not been found")) return [];
throw e;
}
};
/**
* Find a specific UTXO containing a token by MPH and tokenNameHex at a given bech32 script address.
*
* @async
* @function getUtxoMphTokenNameHexFromBech32Addr
* @param {string} scriptAddressBech32 - Bech32 script address.
* @param {string} mph - Minting policy hash (hex).
* @param {string} tokenNameHex - Token name (hex).
* @returns {Promise<any|null>} UTXO object or null.
*/
export const getUtxoMphTokenNameHexFromBech32Addr = async (scriptAddressBech32, mph, tokenNameHex) => {
const utxosResults = await utxoFromBech32(scriptAddressBech32);
if (!utxosResults.length) throw new Error(`No UTXOs found at ${scriptAddressBech32}`);
const found = utxosResults.find(utxo =>
utxo.origOutput.value.assets.assets.some(
([policy, [[tn]]]) => policy.hex === mph && tn.hex === tokenNameHex
)
);
return found || null;
};
/**
* Similar to getUtxoMphTokenNameHexFromBech32Addr, but queries by PKH.
*
* @async
* @function getUtxoMphTokenNameHex
* @param {string} scriptAddressPkh - Script address PKH.
* @param {string} mph - Minting policy hash.
* @param {string} tokenNameHex - Token name hex.
* @returns {Promise<any|null>} UTXO object.
*/
export const getUtxoMphTokenNameHex = async (scriptAddressPkh, mph, tokenNameHex) => {
const bfKey = await getBlockfrostKey("./getBlockfrostKey.php?key=blockfrost"); j.log({ bfKey });
const hBlockfrostApi = new BlockfrostV0("preprod", getBlockfrostKey);
const utxosResults = await hBlockfrostApi.getUtxos(scriptAddressPkh);
if (!utxosResults.length) throw new Error(`No UTXOs found at ${scriptAddressPkh}`);
return utxosResults[utxosResults.length - 1];
};
/**
* Get vesting key UTXO via PHP API.
*
* @async
* @function getKeyUtxo
* @param {string} address - Address to query.
* @returns {Promise<any>} UTXO data.
*/
export const getKeyUtxo = async (address) => {
const url = "./api-getKeyUtxo.php?address=" + address;
const resp = await fetch(url);
return await resp.json();
};
/**
* Query Koios API for an address.
*
* @async
* @function getAddr
* @param {string} address - Address to query.
* @returns {Promise<any>}
*/
export async function getAddr(address) {
return fetch('./koios-api.php?address=' + address, { method: "GET", headers: { "Content-type": "application/json;charset=UTF-8" } })
.then(r => (r.status === 200 ? r : null))
.catch(e => e);
}
/**
* Fetch transactions for a token by policyHexTokenName.
*
* @async
* @function apiListTransactionsFromPolicyHexTokenName
* @param {string} policyHexTokenName - Policy + token hex name.
* @returns {Promise<any>}
*/
export const apiListTransactionsFromPolicyHexTokenName = async (policyHexTokenName) => {
const url = "./apiListTransactionsFromPolicyHexTokenName.php?policyHexTokenName=" + policyHexTokenName;
const resp = await fetch(url);
return JSON.parse(await resp.json());
};
/**
* Fetch addresses holding a specific token policy.
*
* @async
* @function apiListAddressFromSpecificPolicy
* @param {string} policyHexTokenName - Policy identifier.
* @returns {Promise<any>}
*/
export const apiListAddressFromSpecificPolicy = async (policyHexTokenName) => {
const url = "./apiListAddressFromSpecificPolicy.php?policyHexTokenName=" + policyHexTokenName;
const resp = await fetch(url);
return JSON.parse(await resp.json());
};
/**
* Fetch token info by mphHexAssetName.
*
* @async
* @function apiTokenFromAsset
* @param {string} mphHexAssetName - Asset name with MPH.
* @returns {Promise<any>}
*/
export async function apiTokenFromAsset(mphHexAssetName) {
const url = "./apiTokenFromAsset.php?mphHexAssetName=" + mphHexAssetName;
const resp = await fetch(url);
return JSON.parse(await resp.json());
}
/**
* Post encrypted data using XMLHttpRequest.
*
* @async
* @function postEncryptedData
* @param {string} url - Target endpoint.
* @param {Object} data - Data object.
* @returns {Promise<string>} Response text.
*/
export const postEncryptedData = async (url, data) => {
const form = new FormData();
Object.entries(data).forEach(([k, v]) => form.append(k, v));
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
xhr.onload = () => xhr.status === 200 ? resolve(xhr.responseText.trim()) : reject("error");
xhr.onerror = () => reject("error");
xhr.send(form);
});
};
/**
* Get encrypted data from server.
*
* @async
* @function getEncryptedData
* @param {string} url - Target endpoint.
* @param {Object} data - Data object.
* @returns {Promise<any>} Response data.
*/
export const getEncryptedData = async (url, data) => {
const form = new FormData();
Object.entries(data).forEach(([k, v]) => form.append(k, v));
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
xhr.onload = () => {
if (xhr.status === 200) {
try {
const json = JSON.parse(xhr.responseText);
resolve(json);
} catch {
resolve(xhr.responseText);
}
} else reject(xhr.responseText);
};
xhr.onerror = () => reject(xhr.responseText);
xhr.send(form);
});
};
/**
* Mint a new NFT (internal helper).
*
* @async
* @function mintAssets
*/
const mintAssets = async (walletData, name, description, imageUrl, txPrerequisites, j, hlib, txEnd, mintAssetsScript) => {
try {
const maxTxFee = txPrerequisites.maxTxFee;
const minChangeAmt = txPrerequisites.minChangeAmt;
const minAda = txPrerequisites.minAda;
const minUTXOVal = new hlib.Value(BigInt(minAda + maxTxFee + minChangeAmt));
const [txIdHex, utxoIdx] = [walletData.utxos[0][0].txId, walletData.utxos[0][0].utxoIdx];
const mintScript = mintAssetsScript(txIdHex, utxoIdx, name).toString();
const nftCompiledProgram = hlib.Program.new(mintScript).compile(true);
const tx = new hlib.Tx().addInputs(walletData.utxos[0]);
tx.attachScript(nftCompiledProgram);
const nftTokenName = hlib.ByteArrayData.fromString(name).toHex();
const nft = [[hlib.hexToBytes(nftTokenName), BigInt(1)]];
tx.mintTokens(nftCompiledProgram.mintingPolicyHash, nft, new hlib.ConstrData(0, []));
const toAddress = (await walletData.walletHelper.baseAddress).toBech32();
tx.addOutput(new hlib.TxOutput(
hlib.Address.fromBech32(toAddress),
new hlib.Value(minUTXOVal.lovelace, new hlib.Assets([[nftCompiledProgram.mintingPolicyHash, nft]]))
));
tx.addMetadata(721, {
"map": [[nftCompiledProgram.mintingPolicyHash.hex, {
"map": [[name, {
"map": [["name", name], ["description", description], ["image", imageUrl]]
}]]
}]]
});
await txEnd(walletData, hlib, tx, j, txPrerequisites.networkParamsUrl);
} catch (error) {
j.log({ error: await error.info });
}
};
/**
* Public NFT minting wrapper.
*
* @async
* @function mint_
* @param {string} tokenName - NFT name.
* @param {string} tokenDescription - NFT description.
* @param {string} tokenImageUrl - NFT image URL.
* @returns {Promise<void>}
*/
export const mint_ = async (tokenName, tokenDescription, tokenImageUrl) => {
const wallet = await init(j);
const walletData = await walletEssentials(wallet, Cip30Wallet, WalletHelper, Value, txPrerequisites.minAda, j);
await mintAssets(walletData, tokenName, tokenDescription, tokenImageUrl, txPrerequisites, j, hlib, txEnd, mintAssetsScript);
};
/****************************************************
* walletRuntimeUtils.js
*
* Description:
* Wallet/runtime utilities for Cardano dApps:
* - Hex/text helpers
* - Wallet detection/enable flow (CIP-30)
* - Wallet data extraction & display helpers
* - Tx build/submit helpers (ADA & assets)
* - Address/balance/asset helpers
* - Misc tx composition utils
*
****************************************************/
/**
* Convert a hex string to plain text (UTF-16 code units).
*
* @function hexToTex
* @param {string|number} hexx - Hex string (without `0x`).
* @returns {string} Decoded text.
*/
export function hexToTex(hexx) {
const hex = hexx.toString();
let str = '';
for (let i = 0; i < hex.length; i += 2) {
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
}
return str;
}
/**
* Detects installed Cardano wallets (CIP-30 providers) and returns the selected id.
*
* @async
* @function init
* @param {object} j - Logger/test harness.
* @returns {Promise<"nami"|"eternl"|"lace"|"flint"|"nufi"|"yoroi"|null>}
*/
export async function init(j) {
j.s("init");
try {
const started = "...started"; j.log({ started });
const cwindow = window.cardano; j.log({ cwindow });
if (cwindow !== 'undefined') {
if (window.cardano.nami) return "nami";
else if (window.cardano.eternl) return "eternl";
else if (window.cardano.lace) return "lace";
else if (window.cardano.flint) return "flint";
else if (window.cardano.nufi) return "nufi";
else if (window.cardano.yoroi) return "yoroi";
else return null;
} else {
// checkCardano();
}
j.e("init");
} catch (error) {
console.log({ error });
return null;
}
}
/**
* Enable wallet, wrap with Cip30Wallet + WalletHelper, and pick UTXOs.
*
* @async
* @function walletEssentials
* @param {string} selectedWallet - One of the wallet ids from {@link init}.
* @param {Cip30Wallet} Cip30Wallet - CIP-30 wrapper.
* @param {WalletHelper} WalletHelper - Helper for UTXO, addresses, etc.
* @param {Value} Value - Value class (Helios).
* @param {number|bigint} utxoAmount - Minimum lovelace to cover.
* @param {object} j - Logger/test harness.
* @returns {Promise<{wallet:any,walletEnabled:boolean,walletHelper:any,walletHandler:any,walletAPI:any,utxos:any[]}|null>}
*/
export async function walletEssentials(selectedWallet, Cip30Wallet, WalletHelper, Value, utxoAmount, j) {
if (!selectedWallet) return;
j.test("showWalletData", "selectedWallet", selectedWallet).string();
j.test("showWalletData", 'jimba j', j).object();
j.test("showWalletData", "utxoAmount", utxoAmount).geq(0);
const wallet = await eval('window.cardano.' + selectedWallet); j.log({ wallet });
j.test("WalletEssentials", "wallet", wallet).object();
const walletEnabled = await wallet.isEnabled(); j.log({ walletEnabled });
j.check(selectedWallet + "wallet Enabled", walletEnabled, true);
if (!walletEnabled) {
const iwe = await wallet.enable(); j.log({ iwe });
window.location.reload();
}
if (walletEnabled) {
const walletHandler = (await wallet.enable()); j.test("walletEssentials", "walletHandler", walletHandler).object();
const walletAPI = await new Cip30Wallet(walletHandler); j.log({ walletAPI });
const walletHelper = new WalletHelper(walletAPI); j.log({ walletHelper });
const utxos = await walletHelper.pickUtxos(new Value(BigInt(utxoAmount))); j.log({ utxos });
return { wallet, walletEnabled, walletHelper, walletHandler, walletAPI, utxos };
}
return null;
}
/**
* Collect wallet summary + detected assets from first UTXO set.
*
* @async
* @function showWalletData
* @param {object} walletData - Result of {@link walletEssentials}.
* @param {number|bigint} utxoAmount - Minimum lovelace checked earlier.
* @param {AssetClass} AssetClass - Asset fingerprint helper.
* @param {object} j - Logger.
* @returns {Promise<object[]|null>} Array where the first element is wallet info, followed by asset entries.
*/
export const showWalletData = async (walletData, utxoAmount, AssetClass, j) => {
j.s("showWalletData");
try {
j.test("showWalletData", utxoAmount, utxoAmount).geq(0);
const digitalAssests = [];
const utxos = await walletData.utxos; j.log({ utxos });
const baseAddress = (await walletData.walletHelper.baseAddress); j.log({ baseAddress });
const bech32Address = baseAddress.toBech32(); j.log({ bech32Address });
const balanceLovelace = (await walletData.walletHelper.calcBalance()).lovelace.toString(); j.log({ balanceLovelace });
const collateralAda = String((await walletData.walletHelper.pickCollateral()).value.lovelace / BigInt(1_000_000)); j.log({ collateralAda });
const shortAddress = bech32Address.toString().slice(0, 10) + "..." + bech32Address.toString().substr(bech32Address.length - 5); j.log({ shortAddress });
digitalAssests.push({
baseAddress,
bech32Address,
shortAddress,
balanceLovelace,
collateralAda
});
const assets = Object.values(Object.values(utxos[0]))[0].value.assets.dump(); j.log({ assets });
const assetsArray = Object.keys(assets).map((key) => [key, assets[key]]); j.log({ assetsArray });
const assetsAsStringArray = JSON.parse(JSON.stringify(assetsArray)); j.log({ assetsAsStringArray });
assetsAsStringArray.map((x) => {
const mph = x[0];
const tokenHexName = Object.keys(x[1])[0];
const tokenName = hexToTex(Object.keys(x[1])[0]);
const tokenQuantity = Object.values(x[1])[0];
const assClass = new AssetClass(mph + '.' + tokenHexName);
const assetFingerPrint = assClass.toFingerprint();
const assetsObjects = { tokenName, assetQty: tokenQuantity, mph, assetHexName: tokenHexName, assetFingerPrint };
digitalAssests.push(assetsObjects);
});
j.log({ digitalAssests });
j.e("showWalletData");
return digitalAssests;
} catch (error) {
console.log({ error });
return null;
}
};
/**
* Finalize, sign, and submit a transaction using global walletData/hlib/txPrerequisites.
*
* @async
* @function txEnd
* @param {Tx} tx - Transaction instance.
* @returns {Promise<string>} Submitted tx hash (hex).
*
* @note Uses globals: `hlib`, `txPrerequisites`, `walletData`.
*/
export const txEnd = async (tx) => {
const networkParams = new hlib.NetworkParams(
await fetch(txPrerequisites.networkParamsUrl).then((response) => response.json())
); j.log({ networkParams });
const spareUtxo = walletData.utxos[1]; j.log({ spareUtxo });
const txBeforeFinal = tx.dump(); j.log({ txBeforeFinal });
const fnAddress = await walletData.walletHelper.changeAddress; j.log({ fnAddress });
await tx.finalize(networkParams, fnAddress, spareUtxo);
const signature = await walletData.walletAPI.signTx(tx); j.log({ signature });
tx.addSignatures(signature);
const txR = await walletData.walletAPI.submitTx(tx); j.log({ txR });
const txHash = txR.hex; j.log({ txHash });
return txHash;
};
/**
* Send a native asset/NFT to an address.
*
* @async
* @function sendAssets
* @param {string} assetMPH - Minting policy hash (hex).
* @param {string} assetName - Asset name (UTF-8).
* @param {bigint|number} assetQty - Quantity.
* @param {string} toAddress - Destination (bech32).
* @returns {Promise<void>}
*
* @note Uses globals: `j`, `Cip30Wallet`, `WalletHelper`, `Value`, `txPrerequisites`, `hlib`, `txEnd`.
*/
export const sendAssets = async (assetMPH, assetName, assetQty, toAddress) => {
try {
const wallet = await init(j); j.log({ wallet });
const walletData = await walletEssentials(wallet, Cip30Wallet, WalletHelper, Value, txPrerequisites.minAda, j); j.log({ walletData });
const utxos = await walletData.utxos; j.log({ utxos });
const tx = new hlib.Tx();
tx.addInputs(utxos[0]);
const assetsTokenOrNFTs = new hlib.Assets();
assetsTokenOrNFTs.addComponent(
hlib.MintingPolicyHash.fromHex(assetMPH),
Array.from(new TextEncoder().encode(assetName)),
assetQty
);
tx.addOutput(
new hlib.TxOutput(
hlib.Address.fromBech32(toAddress),
new hlib.Value(BigInt(0), assetsTokenOrNFTs)
)
);
const txh = txEnd(tx); // intentionally not awaited as per original code
} catch (error) {
console.log({ error });
}
};
/**
* Send ADA to a bech32 address.
*
* @async
* @function sendADA
* @param {string} toAddress - Destination (bech32).
* @param {number|string} amountToTransfer - ADA amount (not lovelace).
* @returns {Promise<void>}
*
* @note Uses globals: `hlib`, `txPrerequisites`, `init`, `walletEssentials`, `Cip30Wallet`, `WalletHelper`, `Value`, `j`, `txEnd`.
*/
export const sendADA = async (toAddress, amountToTransfer) => {
try {
const wallet = await init(j); j.log({ wallet });
const walletData = await walletEssentials(wallet, Cip30Wallet, WalletHelper, Value, txPrerequisites.minAda, j); j.log({ walletData });
const amountToTransferLovelace = Number(amountToTransfer) * 1_000_000;
const maxTxFee = txPrerequisites.maxTxFee;
const minChangeAmt = txPrerequisites.minChangeAmt;
const minUTXOVal = new hlib.Value(BigInt(amountToTransferLovelace + maxTxFee + minChangeAmt));
const utxos = await walletData.utxos;
const tx = new hlib.Tx();
tx.addInputs(utxos[0]);
tx.addOutput(
new hlib.TxOutput(
hlib.Address.fromBech32(toAddress),
new hlib.Value(BigInt(amountToTransferLovelace))
)
);
const txh = txEnd(tx); // intentionally not awaited as per original code
} catch (error) {
console.log({ error });
}
};
/**
* Get shortened base address of current wallet.
*
* @async
* @function shortAddressFunc
* @returns {Promise<string>}
*
* @note Uses global `walletData`.
*/
export const shortAddressFunc = async () => {
const baseAddress = (await walletData.walletHelper.baseAddress); j.log({ baseAddress });
const bech32Address = baseAddress.toBech32(); j.log({ bech32Address });
const shortAddress = bech32Address.toString().slice(0, 10) + "..." + bech32Address.toString().substr(bech32Address.length - 5); j.log({ shortAddress });
return shortAddress;
};
/**
* Return bech32 address from provided (or global) walletData.
*
* @async
* @function addressFunc
* @param {object} [walletData=walletData] - Wallet data.
* @returns {Promise<string>} Bech32 address.
*/
export const addressFunc = async (walletData = walletData) => {
const baseAddress = (await walletData.walletHelper.baseAddress); j.log({ baseAddress });
const bech32Address = baseAddress.toBech32(); j.log({ bech32Address });
return bech32Address;
};
/**
* Get base address PKH (hex) from global walletData.
*
* @async
* @function baseAddressPKH
* @returns {Promise<string>} PKH hex.
*/
export const baseAddressPKH = async () => {
const baseAddress = (await walletData.walletHelper.baseAddress); j.log({ baseAddress });
const pubkeyh = baseAddress.pubKeyHash.hex; j.log({ pubkeyh });
return pubkeyh;
};
/**
* Get lovelace balance as string.
*
* @async
* @function getLovelace
* @returns {Promise<string>} Lovelace balance.
*/
export const getLovelace = async () => {
const balanceLovelace = (await walletData.walletHelper.calcBalance()).lovelace.toString(); j.log({ balanceLovelace });
return balanceLovelace;
};
/**
* Get ADA balance as string.
*
* @async
* @function getAda
* @returns {Promise<string>} ADA balance.
*/
export const getAda = async () => {
const balanceLovelace = (await walletData.walletHelper.calcBalance()).lovelace;
const ada = await balanceLovelace / BigInt(1_000_000);
return ada.toString();
};
/**
* Dump first UTXO set Value object.
*
* @async
* @function displayValue
* @returns {Promise<any>} Value structure.
*/
export const displayValue = async () => {
const utxos = await walletData.utxos; j.log({ utxos });
const value = Object.values(Object.values(utxos[0]))[0].value; j.log({ value });
return value;
};
/**
* Build simple asset listing from first UTXO set.
*
* @async
* @function assetFunc
* @returns {Promise<Array<{tokenName:string,assetQty:bigint,mph:string,assetHexName:string,assetFingerPrint:string}>>}
*
* @note Uses global `AssetClass` and `walletData`.
*/
export const assetFunc = async () => {
const utxos = await walletData.utxos; j.log({ utxos });
const assets = Object.values(Object.values(utxos[0]))[0].value.assets.dump(); j.log({ assets });
const assetsArray = Object.keys(assets).map((key) => [key, assets[key]]); j.log({ assetsArray });
const assetsAsStringArray = JSON.parse(JSON.stringify(assetsArray)); j.log({ assetsAsStringArray });
const assetArray = [];
assetsAsStringArray.map((x) => {
const mph = x[0];
const tokenHexName = Object.keys(x[1])[0];
const tokenName = hexToTex(Object.keys(x[1])[0]);
const tokenQuantity = Object.values(x[1])[0];
const assClass = new AssetClass(mph + '.' + tokenHexName);
const assetFingerPrint = assClass.toFingerprint();
const assetsObjects = { tokenName, assetQty: tokenQuantity, mph, assetHexName: tokenHexName, assetFingerPrint };
assetArray.push(assetsObjects);
});
return assetArray;
};
/**
* Set transaction validity window (now → now+minutes).
*
* @async
* @function txDeadLine
* @param {Tx} tx - Transaction.
* @param {number} deadLineMinutes - Minutes to add.
* @returns {Promise<Tx>} Same tx with validity set.
*
* @warning Uses `networkParams` from outer scope. Consider passing it in.
*/
export const txDeadLine = async (tx, deadLineMinutes) => {
const slot = networkParams.liveSlot; j.log({ slot });
const time = networkParams.slotToTime(slot); j.log({ time });
const before = new Date(); j.log({ before });
const after = new Date();
after.setMinutes(after.getMinutes() + deadLineMinutes); j.log({ after });
tx.validFrom(before); j.log({ tx });
tx.validTo(after); j.log({ tx });
return tx;
};
/**
* Submit a transaction via walletAPI.
*
* @async
* @function submitTx
* @param {object} walletData - Wallet data with walletAPI.
* @param {Tx} tx - Transaction.
* @returns {Promise<string>} Tx hash hex.
*/
export const submitTx = async (walletData, tx) => {
const txHash = (await walletData.walletAPI.submitTx(tx)).toHex(); j.log({ txHash });
return txHash;
};
/**
* Add multiple outputs to a tx.
*
* @async
* @function addTxOutPuts
* @param {Tx} tx - Transaction.
* @param {Address[]} addresses - Output addresses.
* @param {any[]} datums - Corresponding Values/TxOutputs payloads.
* @param {object} j - Logger.
* @returns {Promise<Tx>} The tx.
*/
export const addTxOutPuts = async (tx, addresses, datums, j) => {
for (let i = 0; i < addresses.length; i++) {
await tx.addOutput(new TxOutput(addresses[i], datums[i]));
}
j.log({ tx });
return tx;
};
/**
* Compose standard ref input + input + script + mint + signers + finalize flow.
*
* @async
* @function txIn
* @param {Tx} tx
* @param {any} utxo - UTXO to reference/consume.
* @param {any} token - Mint/burn token tuple(s).
* @param {any} redeemer - Redeemer data.
* @param {string} mph - Policy hash.
* @param {any} script - Compiled script.
* @param {boolean} deadline - Whether to add validity window.
* @param {Address} partyAddress - Change/party address.
* @param {string[]} signersPKHList - Signers PKHs.
* @param {any[]} signaturesList - Signatures to add.
* @param {any} networkParams - Network params.
* @returns {Promise<Tx>} Finalized tx.
*
* @warning References `utxos` and `deadLineMinutes` which are not in scope here.
*/
export const txIn = async (tx, utxo, token, redeemer, mph, script, deadline, partyAddress, signersPKHList, signaturesList, networkParams) => {
await tx.addRefInput(utxo); j.log({ tx });
await tx.addInput(utxo, redeemer); j.log({ tx });
await tx.addInputs(utxos); j.log({ tx }); // @warning: utxos not defined in scope
await tx.attachScript(script); j.log({ tx });
await tx.mintTokens(mph, token, redeemer); j.log({ tx });
if (deadline) {
const slot = networkParams.liveSlot; j.log({ slot });
const time = networkParams.slotToTime(slot); j.log({ time });
const before = new Date(); j.log({ before });
const after = new Date();
after.setMinutes(after.getMinutes() + deadLineMinutes); j.log({ after }); // @warning: deadLineMinutes not defined
tx.validFrom(before); j.log({ tx });
tx.validTo(after); j.log({ tx });
}
for (let i = 0; i < signersPKHList.length; i++) {
await tx.addSigner(signersPKHList[i]);
}
await tx.finalize(networkParams, partyAddress, utxo); j.log({ tx });
for (let i = 0; i < signaturesList.length; i++) {
await tx.addSignatures(signaturesList[i]);
}
return tx;
};
/**
* Convert a string to hex.
*
* @function strToHex
* @param {string} str
* @returns {string} Hex string.
*/
export const strToHex = (str) => {
const objectR = { result: '' };
for (let i = 0; i < str.length; i++) {
objectR.result += str.charCodeAt(i).toString(16);
}
return objectR.result;
};
/**
* Fetch assets from a policy (backend PHP).
*
* @async
* @function apiGetAssetFromPolicy
* @param {string} policy - Policy id.
* @returns {Promise<any>}
*/
export const apiGetAssetFromPolicy = async (policy) => {
const url = "./apiGetAssetFromPolicy.php?policy=" + policy;
const resp = await fetch(url, { method: "GET" });
const payload = await resp.json();
const data = JSON.parse(payload);
return data;
};
/**
* Extract asset entries from a Value object.
*
* @async
* @function getAssetsFromValue
* @param {any} value - Value object with .assets, .lovelace.
* @@returns {Promise.<Array.<TokenInfo>>}
*/
export const getAssetsFromValue = async (value) => {
const mphArray = value.assets.mintingPolicies;
if (mphArray.length === 0) {
const token = {
policy: "",
tokenName: "",
tokenQuantity: value.lovelace
};
return token;
} else {
const assetsObjects = value.assets.dump();
const assets = Object.keys(assetsObjects).map((key) => [key, assetsObjects[key]]);
const tokenLists = [];
assets.map((x) => {
const thn = Object.keys(x[1])[0];
const mph = x[0];
const tokenHexName = thn;
const tokenName = hexToTex(Object.keys(x[1]));
const tokenQuantity = Object.values(Object.values(x[1]))[0];
const assClass = new AssetClass(mph + '.' + tokenHexName);
const assetFingerPrint = assClass.toFingerprint();
const assetsObjects = { tokenName, tokenQuantity, mph, tokenHexName, assetFingerPrint };
tokenLists.push(assetsObjects);
});
return tokenLists;
}
};
/****************************************************
* tokenMarketplace.js
*
* Description:
* - Token presence detection & UTXO helpers
* - Metadata generator (721-compliant)
* - NFT minting workflow
* - Marketplace actions (sell, buy, cancel, update)
* - Ticket metadata (CIP-25 & CIP-68)
* - Address helpers
*
****************************************************/
/**
* Check if a token with given MPH exists in walletData.utxos.
*
* @async
* @function tokenFound
* @param {string} tokenMph - Minting policy hash (hex).
* @returns {Promise<boolean>} True if token found.
*
* @note Uses global `walletData`.
*/
export const tokenFound = async (tokenMph) => {
if (!tokenMph) return false;
const utxos = await walletData.utxos;
const foundFlags = [];
utxos.map((utxo) => {
const vs0 = Object.values(utxo);
const value = Object.values(vs0)[0].value;
const mphArray = value.assets.mintingPolicies;
if (mphArray.length === 0) {
foundFlags.push(false);
} else {
const assetsObjects = value.assets.dump();
const assets = Object.keys(assetsObjects).map((key) => [key, assetsObjects[key]]);
assets.some((x) => foundFlags.push(x[0].trim() === tokenMph.trim()));
}
});
return foundFlags.includes(true);
};
/**
* Get UTXO from compiled program instance containing given token MPH.
*
* @async
* @function getTokenUtxoFromProgramInstance
* @param {any} programInstance - Helios program instance.
* @param {string} tokenMPH - Minting policy hash.
* @returns {Promise<any|null>} UTXO or null.
*
* @note Uses global `network` & `optimize`.
*/
const getTokenUtxoFromProgramInstance = async (programInstance, tokenMPH) => {
const compiledProgram = programInstance.compile(optimize);
const utxos = await network.getUtxos(Address.fromHashes(compiledProgram.validatorHash));
for (const utxo of utxos) {
if (utxo.value.assets.mintingPolicies.includes(tokenMPH)) return utxo;
}
return null;
};
/**
* Load network parameters JSON depending on txPrerequisites.ntype.
*
* @async
* @function getNetworkParams
* @param {number} [ntype=0] - 0=preprod, 1=preview, 2=mainnet.
* @returns {Promise<object>} Network params JSON.
*/
export const getNetworkParams = async (ntype = 0) => {
const url = { network: "" };
if (txPrerequisites.ntype == 0) url.network = "./params/preprod.json";
else if (txPrerequisites.ntype == 1) url.network = "./params/preview.json";
else url.network = "./params/mainnet.json";
try {
const response = await fetch(url.network);
if (!response.ok) throw new Error(`Response status: ${response.status}`);
return await response.json();
} catch (error) {
console.error(error.message);
}
};
/**
* Generate 721 metadata map for an NFT.
*
* @function generateMetadata
* @param {object} info - NFT info object (policyId, assetName, desc, IPFS, etc.).
* @returns {object} Metadata structure.
*/
export const generateMetadata = (info) => {
const metadata = {
"map": [
[info.policyId.trim(),
{
"map": [
[info.assetName.trim(),
{
"map": [
["name", info.assetName.trim()],
["description", info.assetDescription.trim()],
["mediaType", "image/png"],
["image", info.imageIPFS.trim()],
["mediaType", "video/mp4"],
["video", info.videoIPFS.trim()],
["qty", info.quantity.toString()],
["assetTitle", info.assetTitle.trim()],
["assetPurpose", info.assetPurpose.trim()],
["organization", info.organization.trim()],
["organizationWebsiteUrl", info.organizationWebsiteUrl.trim()],
["tickerIconIPFS", info.tickerIconIPFS.trim()],
["utxoId", info.utxoId.trim()],
["utxoIdx", info.utxoIdx.toString().trim()],
["videoIPFS", info.videoIPFS.trim()],
["dateExpires", info.dateExpires.toString().trim()],
["otherInfo", info.otherInfo.trim()],
["policyId", info.policyId.trim()],
["assetType", info.assetType.trim()],
["assetAuthor", info.assetAuthor.trim()],
["tags", info.tags.trim()],
["assetClassification", info.assetClassification.trim()],
["files", {
"map": [
["mediaType", "image/png"],
["name", info.assetName.trim()],
["src", info.imageIPFS.trim()]
]
}]
]
}
]
]
}
]
]
};
return metadata;
};
/**
* Mint new asset(s) using a provided Helios script.
*
* @async
* @function mint
* @param {object} data - Token data (assetName, title, desc, IPFS, etc.).
* @returns {Promise<void>}
*
* @note Uses global `walletData`, `txPrerequisites`, `Program`, `NetworkParams`.
*/
export const mint = async (data) => {
const minAda = txPrerequisites.minAda;
const maxTxFee = txPrerequisites.maxTxFee;
const minChangeAmt = txPrerequisites.minChangeAmt;
const minAdaVal = new Value(BigInt(minAda));
const minUTXOVal = new Value(BigInt(minAda + maxTxFee + minChangeAmt));
const utxos = await walletData.utxos;
const walletAPI = await walletData.walletAPI;
const changeAddr = await walletData.walletHelper.changeAddress;
const baseAddress = await walletData.walletHelper.baseAddress;
const tx = new Tx();
tx.addInputs(utxos[0]);
const utxoId = utxos[0][0].outputId.txId.hex;
const utxoIdx = utxos[0][0].outputId.utxoIdx;
const ticketMinting = Program.new(data.ticket);
ticketMinting.parameters = { ["TX_ID"]: utxoId };
ticketMinting.parameters = { ["TX_IDX"]: utxoIdx };
ticketMinting.parameters = { ["TN"]: data.assetName };
ticketMinting.parameters = { ["QTY"]: BigInt(data.quantity) };
const ticketCompiledMinting = ticketMinting.compile(true);
const policy = ticketCompiledMinting.mintingPolicyHash.hex;
tx.attachScript(ticketCompiledMinting);
const mintRedeemer = (new ticketMinting.types.Redeemer.Mint())._toUplcData();
const tokens = [[textToBytes(data.assetName), BigInt(data.quantity)]];
const assets = new Assets([[policy, tokens]]);
tx.mintTokens(policy, tokens, mintRedeemer);
const tokenAddress = data.address === "" ? baseAddress.toBech32() : data.address;
tx.addOutput(new TxOutput(
Address.fromBech32(tokenAddress),
new Value(minAdaVal.lovelace, assets)
));
const info = { ...data, policyId: policy, utxoId, utxoIdx };
if (!(info.assetClassification === "FT" && info.quantity > 1)) {
tx.addMetadata(721, generateMetadata(info));
}
const networkParamsJson = await getNetworkParams(txPrerequisites.ntype);
const networkParams = new NetworkParams(networkParamsJson);
await tx.finalize(networkParams, changeAddr, utxos[1]);
const signatures = await walletAPI.signTx(tx);
tx.addSignatures(signatures);
const txHash = (await walletAPI.submitTx(tx)).hex;
return txHash;
};
/**
* Convert hex string to UTF-16 text.
*
* @function hexToText
* @param {string} str1 - Hex string.
* @returns {string} Decoded text.
*/
export const hexToText = (str1) => {
let hex = str1.toString();
let str = '';
for (let n = 0; n < hex.length; n += 2) {
str += String.fromCharCode(parseInt(hex.substr(n, 2), 16));
}
return str;
};
/****************************************************
* marketplaceActions.js
*
* Description:
* Sell / Buy / Cancel / Update marketplace flows for
* script-locked assets using Helios programs.
*
* Notes:
* - Relies on globals/utilities seen elsewhere in your codebase:
* `walletData`, `txEnd`, `save`, `assetFunc`, `getUtxoMphTokenNameHex`,
* `txInputFromBlockfrost`, `txPrerequisites`, `hlib`,
* Helios classes: { Program, Address, Datum, Tx, Value, Assets, AssetClass,
* PubKeyHash, MintingPolicyHash, ByteArrayData, ConstrData, hexToBytes, textToBytes }
*
****************************************************/
/**
* Place asset for sale by locking it at the script address.
*
* Builds a datum with (sellerPKH, asset, qty, saleId, unitPrice, totalPrice)
* and sends the assets to the script address.
*
* @async
* @function sellAssets
* @param {string} buyselltokens - Helios source for the buy/sell validator.
* @param {object} data - Sale info.
* @param {bigint|number} data.nftUnitPriceLovelace - Price per token (in lovelace).
* @param {bigint|number} data.nftQuantity - Number of tokens to sell.
* @param {string|number} data.saleNo - Arbitrary sale sequence/id to build a unique saleId.
* @returns {Promise<string>} Submitted transaction hash.
*
* @example
* const txh = await sellAssets(buysellScriptSrc, {
* nftUnitPriceLovelace: 5000000n,
* nftQuantity: 3n,
* saleNo: Date.now()
* });
*/
export const sellAssets = async (buyselltokens, data) => {
// 1) Discover a token to sell from wallet (picks first by default)
const asst = await assetFunc();
if (!asst || !asst.length) throw new Error("No assets available to sell from wallet UTXOs.");
const indx = 0; // you can parameterize this if you want to choose a specific asset
const tokenNameHex = asst[indx].assetHexName;
const mph = asst[indx].mph;
// 2) Populate sale data fields
data.mph = mph;
data.tokenNameHex = tokenNameHex;
data.nft = `${mph}.${tokenNameHex}`;
data.nftId = `${mph}.${tokenNameHex}${data.saleNo}`;
data.nftTotalPriceLovelace = BigInt(data.nftUnitPriceLovelace) * BigInt(data.nftQuantity);
// 3) Build the value with assets to lock at the script
const sellerAssets = new hlib.Assets();
sellerAssets.addComponent(data.mph, data.tokenNameHex, BigInt(data.nftQuantity));
const sellerValue = new Value(0n, sellerAssets);
// 4) Gather wallet context
const utxos = await walletData.utxos;
const baseAddress = await walletData.walletHelper.baseAddress;
const baseAddressPKH = baseAddress.pubKeyHash;
// 5) Prepare script & datum
const buyselltokensProgram = Program.new(buyselltokens);
const buyselltokensProgramCompiled = buyselltokensProgram.compile(false);
const scriptAddress = Address.fromHashes(buyselltokensProgramCompiled.validatorHash);
const scriptAddressBech32 = scriptAddress.toBech32();
data.scriptAddress = scriptAddress;
data.scriptAddressBech32 = scriptAddressBech32;
// Datum(sellerPKH, AssetClass, remainingQty, saleIdBytes, unitPrice, totalPrice)
const saleIdBytes = textToBytes(data.nftId);
const datum = new (buyselltokensProgram.types.Datum)(
baseAddressPKH,
data.nft,
BigInt(data.nftQuantity),
saleIdBytes,
BigInt(data.nftUnitPriceLovelace),
BigInt(data.nftTotalPriceLovelace)
);
const inLineDatum = Datum.inline(datum);
// 6) Build TX
const tx = new Tx();
tx.addInputs(utxos[0]);
tx.addOutput(new TxOutput(scriptAddress, sellerValue, inLineDatum));
// 7) Submit
const txh = await txEnd(tx);
// 8) Persist sale state
data.txHash = txh;
save("data", JSON.stringify(data));
return txh;
};
/**
* Buy asset(s) from an active sale.
*
* Reads the datum from the script UTXO, calculates the allowed quantity,
* pays the seller, sends purchased tokens to buyer, and if tokens remain,
* re-locks them at the script with an updated datum.
*
* @async
* @function buyAssets
* @param {string} buyselltokens - Helios source for the buy/sell validator.
* @param {bigint|number} qty - Desired quantity to buy.
* @returns {Promise<string>} Submitted transaction hash.
*/
export const buyAssets = async (buyselltokens, qty) => {
const data = JSON.parse(save("data"));
if (!data) throw new Error("No sale data found. Did you call sellAssets() first?");
// Turn nft (mph.tokenHexName) into AssetClass for datum computation context
data.nft = new AssetClass(`${data.mph}.${data.tokenNameHex}`);
const utxos = await walletData.utxos;
const baseAddress = await walletData.walletHelper.baseAddress;
const baseAddressPKHBuyer = baseAddress.pubKeyHash;
// Script compilation
const buyselltokensProgram = Program.new(buyselltokens);
const buyselltokensProgramCompiled = buyselltokensProgram.compile(false);
const scriptAddress = Address.fromHashes(buyselltokensProgramCompiled.validatorHash);
// Find UTXO at script that holds this asset
const utxoMphTokenNameHex = await getUtxoMphTokenNameHex(scriptAddress, data.mph, data.tokenNameHex);
if (!utxoMphTokenNameHex) throw new Error("Sale UTXO not found at script address.");
// Decode datum fields
const datumList = utxoMphTokenNameHex.origOutput.datum.data.list;
const datumSellerPKH = datumList[0].hex; // seller PKH bytes
const datumAssetPol = datumList[1].fields[0].hex; // policy hex
const datumAssetName = datumList[1].fields[1].hex; // name hex
const datumQtyLeft = datumList[2].int; // available qty
const datumUnitPrice = datumList[4].int; // unit price
// datumList[3] is saleId bytes, datumList[5] is total price in datum
// How many can we buy?
const desired = BigInt(qty);
const permittedQty = datumQtyLeft >= desired ? desired : datumQtyLeft;
if (permittedQty <= 0n) throw new Error("No tokens left to buy.");
// Prices
const price_ = datumUnitPrice * permittedQty;
const newQtyLeft = (datumQtyLeft - permittedQty).toString();
// Recompute datum asset class for update
const datumNFT = new AssetClass(`${datumAssetPol}.${datumAssetName}`);
// Build updated datum after buy for leftover tokens (if any)
const newTotalPrice = datumUnitPrice * BigInt(newQtyLeft);
const updatedDatum = new (buyselltokensProgram.types.Datum)(
datumSellerPKH,
datumNFT,
newQtyLeft,
textToBytes(data.nftId),
datumUnitPrice,
newTotalPrice
);
const updatedInlineDatum = Datum.inline(updatedDatum);
// Redeemer for Buy(buyerPKH, saleIdBytes, qty)
const redeemer = (new buyselltokensProgram.types.Redeemer.Buy(
baseAddressPKHBuyer,
textToBytes(data.nftId),
permittedQty
))._toUplcData();
// Outputs:
// 1) Pay seller in ADA
const buyerValueToSeller = new Value(price_);
const sellerAddress = Address.fromHashes(new PubKeyHash(datumSellerPKH));
const txOutput_seller = new TxOutput(sellerAddress, buyerValueToSeller, updatedInlineDatum);
// 2) Send assets to buyer
const boughtAsset = new hlib.Assets();
boughtAsset.addComponent(data.mph, data.tokenNameHex, permittedQty);
const boughtAssetValue = new Value(0n, boughtAsset);
const txOutput_buyer = new TxOutput(baseAddress, boughtAssetValue, updatedInlineDatum);
// 3) If leftover tokens remain, re-lock them at script (same value but updated datum)
const txOutputs = [txOutput_seller, txOutput_buyer];
if (BigInt(newQtyLeft) > 0n) {
const assetsLeft = new hlib.Assets();
assetsLeft.addComponent(data.mph, data.tokenNameHex, newQtyLeft);
const assetsLeftValue = new Value(0n, assetsLeft);
const txOutput_scriptRemainder = new TxOutput(scriptAddress, assetsLeftValue, updatedInlineDatum);
txOutputs.push(txOutput_scriptRemainder);
}
// Build TX
const tx = new Tx();
tx.addInputs(utxos[0]);
tx.addInput(utxoMphTokenNameHex, redeemer);
tx.attachScript(buyselltokensProgramCompiled);
for (const out of txOutputs) tx.addOutput(out);
tx.addSigner(baseAddressPKHBuyer);
// Submit
const txh = await txEnd(tx);
data.txHash = txh;
save("data", JSON.stringify(data));
return txh;
};
/**
* Cancel an active asset sale (seller retrieves remaining tokens).
*
* Consumes the script UTXO with a `Cancel` redeemer and
* returns the remaining tokens to the seller address.
*
* @async
* @function cancelAssetsSale
* @param {string} buyselltokens - Helios source for the buy/sell validator.
* @returns {Promise<string>} Submitted transaction hash.
*/
export const cancelAssetsSale = async (buyselltokens) => {
const data = JSON.parse(save("data"));
if (!data) throw new Error("No sale data found. Did you call sellAssets() first?");
const utxos = await walletData.utxos;
const baseAddress = await walletData.walletHelper.baseAddress;
// Compile script
const buyselltokensProgram = Program.new(buyselltokens);
const buyselltokensProgramCompiled = buyselltokensProgram.compile(false);
const scriptAddress = Address.fromHashes(buyselltokensProgramCompiled.validatorHash);
// Find current sale UTXO
const utxo = await getUtxoMphTokenNameHex(scriptAddress, data.mph, data.tokenNameHex);
if (!utxo) throw new Error("Sale UTXO not found at script address.");
const datumList = utxo.origOutput.datum.data.list;
const remainingQty = datumList[2].int;
const datumNFT = new AssetClass(`${datumList[1].fields[0].hex}.${datumList[1].fields[1].hex}`);
// Prepare value to return to seller
const sellerAssets = new hlib.Assets();
sellerAssets.addComponent(datumList[1].fields[0].hex, datumList[1].fields[1].hex, remainingQty);
// Note: original code used `utxoValuePrice` from datumQtyLeft, which isn't ADA price.
// Here we return only the tokens (0 lovelace) — adjust if you intend to also return ADA.
const sellerValue = new Value(0n, sellerAssets);
const txOutputToSeller = new TxOutput(baseAddress, sellerValue);
// Redeemer Cancel(sellerPKH, saleIdBytes)
const redeemer = (new buyselltokensProgram.types.Redeemer.Cancel(
baseAddress.pubKeyHash,
textToBytes(data.nftId)
))._toUplcData();
const tx = new Tx();
tx.addInputs(utxos[0]);
tx.addInput(utxo, redeemer);
tx.attachScript(buyselltokensProgramCompiled);
tx.addOutput(txOutputToSeller);
tx.addSigner(baseAddress.pubKeyHash);
const txh = await txEnd(tx);
data.txHash = txh;
save("data", JSON.stringify(data));
return txh;
};
/**
* Update an existing sale with new unit price and/or quantity.
*
* Reads the current datum at the script, writes a new inline datum
* with updated price/qty, and re-locks the same value back at the script.
*
* @async
* @function updateAssetsSale
* @param {string} buyselltokens - Helios source for the buy/sell validator.
* @param {bigint|number} price - New unit price (lovelace).
* @param {bigint|number} qty - New quantity available.
* @returns {Promise<string>} Submitted transaction hash.
*/
export const updateAssetsSale = async (buyselltokens, price, qty) => {
const data = JSON.parse(save("data"));
if (!data) throw new Error("No sale data found. Did you call sellAssets() first?");
const utxos = await walletData.utxos;
const baseAddress = await walletData.walletHelper.baseAddress;
const baseAddressPKHOwner = baseAddress.pubKeyHash;
// Compile script
const buyselltokensProgram = Program.new(buyselltokens);
const buyselltokensProgramCompiled = buyselltokensProgram.compile(false);
const scriptAddress = Address.fromHashes(buyselltokensProgramCompiled.validatorHash);
// Target UTXO
const utxo = await getUtxoMphTokenNameHex(scriptAddress, data.mph, data.tokenNameHex);
if (!utxo) throw new Error("Sale UTXO not found at script address.");
const datumList = utxo.origOutput.datum.data.list;
const datumSellerPKH = datumList[0].hex;
const datumNFT = new AssetClass(`${datumList[1].fields[0].hex}.${datumList[1].fields[1].hex}`);
const currentQty = datumList[2].int;
// If qty not explicitly provided, keep current
const newQty = qty != null ? BigInt(qty) : currentQty;
const newPrice = BigInt(price);
const newTotal = newPrice * newQty;
// New datum
const updatedDatum = new (buyselltokensProgram.types.Datum)(
datumSellerPKH,
datumNFT,
newQty,
textToBytes(data.nftId),
newPrice,
newTotal
);
const updatedInlineDatum = Datum.inline(updatedDatum);
// Keep same value from original UTXO but update its datum
const sValue = utxo.value;
// Redeemer Update(ownerPKH, saleIdBytes, newPrice, newQty)
const redeemer = (new buyselltokensProgram.types.Redeemer.Update(
baseAddressPKHOwner,
textToBytes(data.nftId),
newPrice,
newQty
))._toUplcData();
// Build TX
const tx = new Tx();
tx.addInputs(utxos[0]);
tx.addInput(utxo, redeemer);
tx.attachScript(buyselltokensProgramCompiled);
tx.addOutput(new TxOutput(scriptAddress, sValue, updatedInlineDatum));
tx.addSigner(baseAddressPKHOwner);
// Submit
const txh = await txEnd(tx);
data.txHash = txh;
data.nftUnitPriceLovelace = newPrice.toString();
data.nftQuantity = newQty.toString();
save("data", JSON.stringify(data));
return txh;
};
/**
* Get on-chain ticket metadata (CIP-25).
*
* @async
* @function getTicketMetadataCIP25
* @param {string} asset - Asset id.
* @param {string} blockfrostapi - Blockfrost API key.
* @returns {Promise<Ticket>} Ticket info object.
*/
const getTicketMetadataCIP25 = async (asset, blockfrostapi) => {
const API = new BlockFrostAPI({ projectId: blockfrostapi });
const ticketMetadata = await API.assetsById(asset);
const { utxoId, utxoIdx, name, location, image, paymentPKH, stakePKH, qty, holdValHash, minLovelace, showtime } = ticketMetadata.onchain_metadata;
const imageSrc = Array.isArray(image) ? image.join('') : image;
return new Ticket(
utxoId, utxoIdx, Buffer.from(name).toString('hex'),
location, showtime, imageSrc, qty, paymentPKH,
stakePKH, holdValHash, minLovelace
);
};
/**
* Get on-chain ticket metadata (CIP-68).
*
* @async
* @function getTicketMetadataCIP68
* @param {string} asset - Asset id.
* @param {string} apiKey - Blockfrost API key.
* @returns {Promise<PrintTicketInfo>} Parsed ticket info.
*/
const getTicketMetadataCIP68 = async (asset, apiKey) => {
const API = new BlockFrostAPI({ projectId: apiKey });
const ticketMetadata = await API.assetsById(asset);
const name = ticketMetadata.onchain_metadata.name;
const image = ticketMetadata.onchain_metadata.image;
const extraData = ticketMetadata.onchain_metadata_extra;
const mapData = MapData.fromCbor(hexToBytes(extraData));
const mapDataJSON = JSON.parse(mapData.toSchemaJson());
const location = Buffer.from(mapDataJSON.map[0].v.bytes, 'hex').toString();
const showtime = mapDataJSON.map[1].v.int;
const pkh = mapDataJSON.map[2].v.bytes;
return new PrintTicketInfo(asset, asset.slice(0, 56), name, location, showtime, image, pkh || undefined);
};
/**
* Convert a bech32 address into PKH.
*
* @function bech32ToPkh
* @param {string} addressBech32 - Bech32 address string.
* @returns {PubKeyHash|string} PKH or error message.
*/
export const bech32ToPkh = (addressBech32) => {
const addrp = addressBech32.substr(0, 9);
const addressIsBech32 = addrp === "addr_test";
if (addressIsBech32) {
const address = Address.fromBech32(addressBech32);
return address.pubKeyHash;
} else {
return "Bech32 address is not correct";
}
};
/****************************************************
* nftUpdateAndCIP68Utils.js
*
* Description:
* - Address ⇄ hash helpers (bech32 → validator hash)
* - NFT update flow (burn-then-update metadata at script)
* - Mint & lock (CIP-68-friendly) flow
* - Script parameter helpers
* - CIP-68 token-label helpers (100/222/333/444/555/666)
* - Tx output helpers & signer utilities
* - Byte/hex helpers and Asset builders
* - POSIX/time helpers
*
* Notes:
* Many functions rely on globals from your app:
* `walletData`, `txPrerequisites`, `hlib`, `Program`, `Address`,
* `Tx`, `Value`, `Assets`, `Datum`, `AssetClass`, `MintingPolicyHash`,
* `ByteArrayData`, `IntData`, `MapData`, `ConstrData`,
* `textToBytes`, `hexToBytes`, `bytesToHex`, `el`, `save_`, `swal`,
* `getUtxoMphTokenNameHex`, `getAllTxInputs`, `generateMetadata`, `txEnd`, `j`.
*
****************************************************/
/**
* Convert a bech32 address to its validator hash.
*
* @function bech32ToValidatorHash
* @param {string} addressBech32 - Bech32 address (script address).
* @returns {any|string} Validator hash object or error message.
*
* @note Testnet script addrs typically start with "addr_test", mainnet with "addr1".
*/
export const bech32ToValidatorHash = (addressBech32) => {
const prefix = addressBech32.substr(0, 9); j.log({ prefix });
const isBech32 = prefix === "addr_test" || addressBech32.startsWith("addr1"); j.log({ isBech32 });
if (isBech32) {
const address = Address.fromBech32(addressBech32); j.log({ address });
const vhash = address.validatorHash; j.log({ vhash });
return vhash;
} else {
const msg = "Bech32 address is not correct"; j.log({ msg });
return msg;
}
};
/**
* Update an existing NFT's on-chain metadata by consuming the locked UTXO
* with a burn/unlock-style redeemer, then re-output with new metadata.
*
* @async
* @function updateNFT
* @param {string} scriptForLockingNFT - Helios source (validator) that locked the NFT.
* @param {string} scriptForMintingNFT - Helios source (minting policy) of the NFT.
* @param {object} data - Update payload with fields used in metadata + controls:
* @param {string} data.mphHex - Minting policy hash (hex) of the NFT (original).
* @param {string} data.tokenNameHex - Token name (hex) to target.
* @param {bigint|number} data.quantity - Quantity to burn/handle.
* @param {Date|number} [data.deadline] - Optional validity upper bound (Date or ms).
* @returns {Promise<string>} Submitted tx hash.
*
* @note Uses globals: walletData, txPrerequisites, Program, Address, Tx, Value, Assets, Datum, hlib, txEnd, generateMetadata, getUtxoMphTokenNameHex, j.
*/
export const updateNFT = async (scriptForLockingNFT, scriptForMintingNFT, data) => {
// 1) Wallet/fees context
const minAda = txPrerequisites.minAda;
const maxTxFee = txPrerequisites.maxTxFee;
const minChangeAmt = txPrerequisites.minChangeAmt;
const baseAddress = await walletData.walletHelper.baseAddress; j.log({ baseAddress });
const changeAddr = await walletData.walletHelper.changeAddress; j.log({ changeAddr });
const ownerPKH = baseAddress.pubKeyHash;
const utxos = await walletData.utxos; j.log({ utxos });
const baseAddressPKH = baseAddress.pubKeyHash; j.log({ baseAddressPKH });
const baseAddressPKHHex = baseAddressPKH.hex; j.log({ baseAddressPKHHex });
const baseAddressBech32 = baseAddress.toBech32(); j.log({ baseAddressBech32 });
const baseAddressStakingHash = baseAddress.stakingHash; j.log({ baseAddressStakingHash });
const ownerBytes = baseAddressPKH.bytes; j.log({ ownerBytes });
const _addrFromHashes = Address.fromHashes(baseAddressPKH, baseAddressStakingHash).toBech32();
j.log({ _addrFromHashes });
// 2) Compile locking script (validator)
const lockTokenScriptProgram = Program.new(scriptForLockingNFT);
lockTokenScriptProgram.parameters = { ["OWNER"]: ownerBytes };
const lockCompiled = lockTokenScriptProgram.compile(false);
const lockScriptAddress = Address.fromHashes(lockCompiled.validatorHash); j.log({ lockScriptAddress });
const lockValidatorHashHex = lockCompiled.validatorHash.hex; j.log({ lockValidatorHashHex });
const lockScriptBech32 = lockScriptAddress.toBech32(); j.log({ lockScriptBech32 });
// 3) (Optional) Compile minting policy if needed for context (not attached below)
const scriptReferenceToken = Program.new(scriptForMintingNFT);
scriptReferenceToken.parameters = { ["ownerPKH"]: baseAddress.pubKeyHash };
scriptReferenceToken.parameters = { ["TN"]: data.tokenNameHex };
const scriptRefCompiled = scriptReferenceToken.compile(false);
const refPolicyIdHex = scriptRefCompiled.mintingPolicyHash.hex; j.log({ refPolicyIdHex });
// 4) Find the locked UTXO for the (mph, token) at the script address
const targetUtxo = await getUtxoMphTokenNameHex(lockScriptAddress, data.mphHex, data.tokenNameHex); j.log({ targetUtxo });
if (!targetUtxo) throw new Error("Target UTXO not found at script for given mph/token name.");
const utxoTokenName = data.tokenNameHex; j.log({ utxoTokenName });
// 5) Build TX: consume target UTXO with Cancel/BurnLocked, then output updated metadata
const tx = new Tx();
tx.addInputs(utxos[0]);
const utxoId = targetUtxo.txId?.hex || targetUtxo.outputId?.txId?.hex; j.log({ utxoId });
const utxoIdx = targetUtxo.utxoIdx; j.log({ utxoIdx });
// Redeemer to unlock/burn-then-update (as defined by your validator)
const burnRedeemer = (new lockTokenScriptProgram.types.Redeemer.BurnLocked())._toUplcData(); j.log({ burnRedeemer });
// Attach validator; consume script UTXO
tx.attachScript(lockCompiled);
tx.addInput(targetUtxo, burnRedeemer);
// Recreate minimal script output (keeping min ADA; no assets here by default)
tx.addOutput(new TxOutput(lockScriptAddress, new Value(hlib.minAda)));
// Signer + metadata updates
tx.addSigner(ownerPKH);
// Fill metadata-related fields expected by generateMetadata
data.utxoId = utxoId;
data.utxoIdx = utxoIdx;
data.policyId = data.mphHex;
if (data.deadline) {
// Accept Date or number(ms)
const until = data.deadline instanceof Date ? data.deadline : new Date(data.deadline);
tx.validTo(until);
}
tx.addMetadata(721, generateMetadata(data));
const txh = await txEnd(tx);
return txh;
};
/**
* Convert UTF-8 text to hex.
*
* @function textToHex
* @param {string} text - Input text.
* @returns {string} Hex string.
*/
export const textToHex = (text) => bytesToHex(textToBytes(text));
/**
* Mint & lock a CIP-68-capable token at a script with inline datum.
* Signs and submits via provided Blockfrost client.
*
* @async
* @function mintBurnToken
* @param {string} scriptForMintingNFT - Minting policy (Helios source).
* @param {string} scriptForLockingNFT - Validator (Helios source).
* @param {object} info - Payload used for parameters & metadata:
* @param {any} info.baseAddress - Address object of the owner.
* @param {string} info.assetName - Token name (string, not hex).
* @param {bigint|number} info.quantity - Quantity to mint.
* @param {any} info.cip68InlineDatum - Datum.inline(...) for script output.
* @param {string} info.dbRecordStatus - DB marker for save_ call.
* @param {string} info.assetTitle
* @param {string} info.StudentNumber
* @param {string} info.assetNameCode
* @param {HTMLElement} info.txhash - DOM node where link is set.
* @param {string} info.phpFileName - Endpoint for save_.
* @param {any} info.recipientPayKey - Key with sign() to sign bodyHash.
* @param {any} info.TEST_BLOCKFROST - Blockfrost client with submitTx(unsignedTx).
* @returns {Promise<string>} Tx hash.
*
* @note Uses globals: txPrerequisites, Program, Address, Tx, Value, Assets, MintingPolicyHash,
* textToBytes, hexToBytes, hlib, generateMetadata, save_, el, swal, j.
*/
export const mintBurnToken = async (scriptForMintingNFT, scriptForLockingNFT, info) => {
// Fees context (unused in body but retained for completeness)
const minAda = txPrerequisites.minAda;
const maxTxFee = txPrerequisites.maxTxFee;
const minChangeAmt = txPrerequisites.minChangeAmt;
const baseAddress = info.baseAddress; j.log({ baseAddress });
const changeAddr = baseAddress; j.log({ changeAddr });
const ownerPKH = baseAddress.pubKeyHash;
// UTXOs for this base address come from chain (not from wallet API)
const utxos = await getAllTxInputs(baseAddress); j.log({ utxos });
if (!utxos || utxos.length === 0) throw new Error("No UTXOs found for provided baseAddress.");
const baseAddressPKH = baseAddress.pubKeyHash; j.log({ baseAddressPKH });
const baseAddressPKHHex = baseAddressPKH.hex; j.log({ baseAddressPKHHex });
const baseAddressBech32 = baseAddress.toBech32(); j.log({ baseAddressBech32 });
const baseAddressStakingHash = baseAddress.stakingHash; j.log({ baseAddressStakingHash });
const ownerBytes = baseAddressPKH.bytes; j.log({ ownerBytes });
// Build TX
const tx = new Tx();
tx.addInputs(utxos);
const utxoId = utxos[0].outputId.txId.hex; j.log({ utxoId });
const utxoIdx = utxos[0].outputId.utxoIdx; j.log({ utxoIdx });
// Minting policy
const scriptReferenceToken = Program.new(scriptForMintingNFT);
const tokenNameStr = info.assetName; j.log({ tokenNameStr });
scriptReferenceToken.parameters = { ["ownerPKH"]: ownerBytes };
scriptReferenceToken.parameters = { ["TN"]: tokenNameStr };
const scriptRefCompiled = scriptReferenceToken.compile(true);
const mphHex = scriptRefCompiled.mintingPolicyHash.hex; j.log({ mphHex });
tx.attachScript(scriptRefCompiled);
// Locking validator
const lockTokenScriptProgram = Program.new(scriptForLockingNFT);
lockTokenScriptProgram.parameters = { ["OWNER"]: ownerBytes };
const lockCompiled = lockTokenScriptProgram.compile(false);
const lockScriptAddress = Address.fromHashes(lockCompiled.validatorHash); j.log({ lockScriptAddress });
const lockVhashHex = lockCompiled.validatorHash.hex; j.log({ lockVhashHex });
const lockScriptBech32 = lockScriptAddress.toBech32(); j.log({ lockScriptBech32 });
// Mint + lock
const tokens = [[tokenNameStr, BigInt(info.quantity)]];
const mintedAssets = new Assets([[mphHex, tokens]]); j.log({ mintedAssets });
info.utxoId = utxoId;
info.utxoIdx = utxoIdx;
el("processMsg").innerHTML = "...now handling Cardano transactions.";
const mph = new MintingPolicyHash(hexToBytes(mphHex));
const txId = hexToBytes(utxoId);
const tokenName = hexToBytes(tokenNameStr);
// Redeemer for LockToken(mph, txId, qty, tnBytes)
const redeemer = new lockTokenScriptProgram.types.Redeemer.LockToken(
mph,
txId,
BigInt(info.quantity),
tokenName
);
const redeemerData = redeemer._toUplcData ? redeemer._toUplcData() : redeemer.toData();
console.log("RedeemerData:", redeemerData);
tx.mintTokens(mphHex, tokens, redeemerData);
// Output to script with inline datum (CIP-68 content)
tx.addOutput(new TxOutput(
lockScriptAddress,
new Value(hlib.minAda, mintedAssets),
info.cip68InlineDatum
));
tx.addSigner(ownerPKH);
info.policyId = mphHex; j.log({ info });
tx.addMetadata(721, generateMetadata(info));
// Finalize, sign (raw), and submit via Blockfrost
const networkParams = new hlib.NetworkParams(
await fetch(txPrerequisites.networkParamsUrl).then((r) => r.json())
); j.log({ networkParams });
const unsignedTx = await tx.finalize(networkParams, baseAddress, utxos);
unsignedTx.addSignature(info.recipientPayKey.sign(unsignedTx.bodyHash)); j.log({ unsignedTx });
const txHash = await info.TEST_BLOCKFROST.submitTx(unsignedTx); j.log({ txHash });
const txh = txHash.hex; j.log({ txh });
if (txh?.toString().trim().length === 64) {
el("processMsg").innerHTML = "...now saving the results.";
swal({
title: "Congratulations, transaction is now on blockchain!",
text: "Here is the txHash : " + txh,
icon: "success",
buttons: [true, "Yes"],
dangerMode: true,
});
const url = `<a href='https://preprod.cexplorer.io/tx/${txh}'>${txh}</a>`; j.log({ url });
info.txhash.innerHTML = url;
// Persist (uses your earlier save_ helper)
await save_(
tokenNameStr,
mphHex,
utxoId,
utxoIdx,
info.quantity,
ownerPKH,
txh,
info.dbRecordStatus,
info.assetTitle,
info.StudentNumber,
info.assetNameCode,
0,
lockScriptBech32,
baseAddressBech32,
null,
info.assetNameCode,
info.phpFileName
);
el("processMsg").style.display = "none";
el("cardBody").style.display = "block";
} else {
return "failed txh";
}
return txh;
};
/**
* Add (or override) parameters on a Helios Program from key/value pairs.
*
* @function scriptAddParams
* @param {any} script - Helios Program instance (not compiled).
* @param {Array.<KV>} arList - Array of [key, value] tuples.
* @returns {any} Same script instance with parameters set.
*/
export const scriptAddParams = (script, arList) => {
if (arList?.length > 0) {
for (let i = 0; i < arList.length; i++) {
const [key, value] = arList[i];
script.parameters = { [key.toString().toUpperCase()]: value };
}
}
return script;
};
/**
* Build a CIP-68 Reference Token (label=100) Assets object from MPH & tnName.
*
* @function refTokenCip68AssetFromMphTnName
* @param {string} mph - Policy hash hex.
* @param {string} tnName - Human token name (string).
* @param {bigint|number} [qty=1] - Quantity; if not 1, uses -1 (per your code).
* @returns {Assets} Helios Assets object.
*/
export const refTokenCip68AssetFromMphTnName = (mph, tnName, qty = 1) => {
const q = qty === 1 ? 1n : -1n;
const label100hex = "000643b0"; j.log({ label100hex });
const reftokenName = label100hex + bytesToHex(textToBytes(tnName)); j.log({ reftokenName });
const refTokenAssetClass = [hexToBytes(reftokenName), BigInt(q)]; j.log({ refTokenAssetClass });
const cip68Asset_ = new Assets([[mph, [refTokenAssetClass]]]); j.log({ cip68Asset_ });
return cip68Asset_;
};
/**
* Build a CIP-68 Reference Token Assets object from pre-built assetClassCIP68.
*
* @function refTokenCip68AssetFromMPHAssetClass
* @param {string} mph - Policy hash hex.
* @param {CIP68AssetClass} assetClassCIP68- [tokenNameBytes, qty].
* @returns {Assets}
*/
export const refTokenCip68AssetFromMPHAssetClass = (mph, assetClassCIP68) => {
const cip68Asset_ = new Assets([[mph, [assetClassCIP68]]]); j.log({ cip68Asset_ });
return cip68Asset_;
};
/**
* Create a CIP-68 reference token asset-class tuple from hex token name.
*
* @function refTokenAssetClassCIP68
* @param {string} createCIP68ReferenceTokenName - token name hex (already labeled).
* @param {bigint|number} [qty=1] - Quantity (or -1 if not 1).
* @returns {CIP68AssetClass} Asset-class object
*/
export const refTokenAssetClassCIP68 = (createCIP68ReferenceTokenName, qty = 1) => {
const q = qty === 1 ? 1n : -1n;
const refTokenAssetClass = [hexToBytes(createCIP68ReferenceTokenName), BigInt(q)]; j.log({ refTokenAssetClass });
return refTokenAssetClass;
};
/**
* Create a token name (hex) for known CIP-68 labels.
*
* @function createCIP68Token
* @param {string} tnNameString - Human-readable name.
* @param {number} intLabel - One of 100/222/333/444/555/666.
* @returns {string} Token name hex or error text.
*/
export const createCIP68Token = (tnNameString, intLabel) => {
if (intLabel === 666) return createCIP68CustomToken666(tnNameString);
else if (intLabel === 555) return createCIP68CustomToken555(tnNameString);
else if (intLabel === 444) return createCIP68RFT444(tnNameString);
else if (intLabel === 333) return createCIP68FT333(tnNameString);
else if (intLabel === 222) return createCIP68NFT222(tnNameString);
else if (intLabel === 100) return createCIP68RefToken100(tnNameString);
const msg = "CIP68 unknown label"; j.log({ msg });
return msg;
};
// --- CIP-68 label encoders (token name hex) ---
export const createCIP68CustomToken666 = (tokenName) => {
const label666hex = "0029ae50"; j.log({ label666hex });
return label666hex + bytesToHex(textToBytes(tokenName));
};
export const createCIP68CustomToken555 = (tokenName) => {
const label555hex = "0022bfb0"; j.log({ label555hex });
return label555hex + bytesToHex(textToBytes(tokenName.toString().trim()));
};
export const createCIP68RefToken100 = (tokenName) => {
const label100hex = "000643b0"; j.log({ label100hex });
return label100hex + bytesToHex(textToBytes(tokenName));
};
export const createCIP68NFT222 = (tokenName) => {
const label1222hex = "000de140"; j.log({ label1222hex });
return label1222hex + bytesToHex(textToBytes(tokenName));
};
export const createCIP68RFT444 = (tokenName) => {
const label444hex = "001bc280"; j.log({ label444hex });
return label444hex + bytesToHex(textToBytes(tokenName));
};
export const createCIP68FT333 = (tokenName) => {
const label333hex = "0014df10"; j.log({ label333hex });
return label333hex + bytesToHex(textToBytes(tokenName));
};
/**
* Helper to add a datumed output containing ADA + a single token.
*
* @function txOutput
* @param {Tx} tx - Transaction to mutate.
* @param {any} scriptAddressHash - Address (script) receiving.
* @param {bigint|number} amount - Lovelace amount.
* @param {any} datum - Datum to inline.
* @param {string} tokenName - Human token name (string).
* @param {bigint|number} qty - Quantity of token.
* @returns {Tx}
*/
export const txOutput = (tx, scriptAddressHash, amount, datum, tokenName, qty) => {
const adaToSend = new Value(BigInt(amount)); j.log({ adaToSend });
const lovelaceToSend = adaToSend.lovelace; j.log({ lovelaceToSend });
const qtyBInt = BigInt(qty); j.log({ qtyBInt });
const asset_ = new Asset([textToBytes(tokenName), qtyBInt]); j.log({ asset_ }); // Assumes an `Asset` class in scope
const value_ = new Value(lovelaceToSend, asset_); j.log({ value_ });
const inLineDatum = Datum.inline(datum); j.log({ inLineDatum });
const out = new TxOutput(scriptAddressHash, value_, inLineDatum); j.log({ out });
tx.addOutput(out);
return tx;
};
/**
* String → ByteArrayData wrapper.
* @function stringToByteArray
*/
export const stringToByteArray = (itemString) => new ByteArrayData(textToBytes(itemString));
/**
* Alias of stringToByteArray.
* @function strToBytes
*/
export const strToBytes = (itemString) => new ByteArrayData(textToBytes(itemString));
/**
* Hex → ByteArrayData wrapper.
* @function hexToByteArray
*/
export const hexToByteArray = (itemHex) => new ByteArrayData(hexToBytes(itemHex));
/**
* Compile a Helios script source immediately (optimize=true).
*
* @function compileScript
* @param {string} scriptName - Helios source code of the script.
* @returns {any} Compiled program.
*/
export const compileScript = (scriptName) => (Program.new(scriptName)).compile(true);
/**
* Create an AssetClass from mph + tokenNameHex.
*
* @function createAssetClass
* @param {string} mphHex
* @param {string} tokenNameHex
* @returns {AssetClass}
*/
export const createAssetClass = (mphHex, tokenNameHex) => new AssetClass(`${mphHex}.${tokenNameHex}`);
/**
* Create an Assets map with a single (mph, tokenNameHex, qty) component.
*
* @function createAsset
* @param {string} mphHex
* @param {string} tokenNameHex
* @param {bigint|number} qty
* @returns {Assets}
*/
export const createAsset = (mphHex, tokenNameHex, qty) => {
const a = new hlib.Assets();
a.addComponent(mphHex, tokenNameHex, qty);
return a;
};
/**
* Convert parallel key/value arrays to CIP-68 MapData list: [[keyBytes, valBytes], ...]
*
* @function keyValueList
* @param {string[]} arList - Keys.
* @param {string[]} arListv - Values.
* @returns {Array.<BytePair>}
*/
export const keyValueList = (arList, arListv) => {
const hexKV = [];
if (arList?.length > 0) {
for (let i = 0; i < arList.length; i++) {
hexKV.push([new ByteArrayData(textToBytes(arList[i] + "Key")), new ByteArrayData(textToBytes(arListv[i]))]);
}
}
return hexKV;
};
/**
* Set tx validity window from now-5min to +N hours (robust ordering).
*
* @function txValidTimeRange
* @param {Tx} tx
* @param {number} noHrsFromNow - Hours ahead for validTo.
* @returns {Tx}
*/
export const txValidTimeRange = (tx, noHrsFromNow) => {
const currentTime = Date.now();
const earlierTime = new Date(currentTime - 5 * 60 * 1000);
const laterTime = new Date(currentTime + noHrsFromNow * 60 * 60 * 1000);
if (earlierTime < laterTime) {
tx.validFrom(earlierTime);
tx.validTo(laterTime);
} else {
tx.validFrom(laterTime);
tx.validTo(earlierTime);
}
return tx;
};
/**
* Add multiple signer PKHs to a tx.
*
* @function txSigners
* @param {Tx} tx
* @param {any[]} pkhList - List of PKH objects.
* @returns {Tx}
*/
export const txSigners = (tx, pkhList) => {
if (pkhList?.length > 0) {
for (let i = 0; i < pkhList.length; i++) tx.addSigner(pkhList[i]);
}
return tx;
};
/**
* Create multiple outputs by summing token Values and emitting them to changeAddress.
*
* @function txCreateOutput
* @param {Tx} tx
* @param {any} changeAddress
* @param {Array.<TokenOutputItem>} dataList
* @returns {Tx}
*
* @note Original snippet referenced `dataList[i]` before `i` was declared; fixed.
*/
export const txCreateOutput = (tx, changeAddress, dataList) => {
const assetMap = new Value();
for (let i = 0; i < dataList.length; i++) {
const { tokenTN, tokenQty, mphHex, lovelace } = dataList[i];
const tokenNameBytes = textToBytes(tokenTN);
const sellerTokenMap = [[tokenNameBytes, BigInt(tokenQty)]];
const tokenAsset = new Assets([[mphBin(mphHex), sellerTokenMap]]);
const tokenValue = new Value(BigInt(lovelace), tokenAsset);
assetMap.add(tokenValue);
tx.addOutput(new TxOutput(changeAddress, assetMap));
}
return tx;
};
/**
* Create a single CIP-68 datumed output with ref assets and minimum lovelace.
*
* @function txCreateOutput_2
* @param {Tx} tx
* @param {string[]} arList1 - Keys for main MapData.
* @param {string[]} arList2 - Keys for extra MapData.
* @param {bigint|number|string} VersionNo - Version integer.
* @param {Assets} refAssets - Reference token assets (e.g., CIP-68 label=100).
* @param {any} scriptAddress - Script address object.
* @param {bigint|number} minLovelace - Min ADA for output.
* @returns {Tx}
*/
export const txCreateOutput_2 = (tx, arList1, arList2, VersionNo, refAssets, scriptAddress, minLovelace) => {
const mapData = new MapData(keyValueList(arList1, arList1.map(() => ""))); // if only keys provided
const version = new IntData(BigInt(VersionNo));
const extraData = new MapData(keyValueList(arList2, arList2.map(() => "")));
const cip068Datum = new ConstrData(0, [mapData, version, extraData]);
const cip68InlineDatum = Datum.inline(cip068Datum);
tx.addOutput(new TxOutput(scriptAddress, new Value(BigInt(minLovelace), refAssets), cip68InlineDatum));
return tx;
};
/**
* Convert mph hex string to MintingPolicyHash.
*
* @function mphBin
* @param {string} mphHex
* @returns {MintingPolicyHash}
*/
const mphBin = (mphHex) => MintingPolicyHash.fromHex(mphHex);
//-----Utility Functions-------
/**
* Generate a random alphanumeric id.
*
* @function id
* @param {number} length
* @returns {string}
*/
export const id = (length) => {
let result = '';
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < length; i++) result += chars.charAt(Math.floor(Math.random() * chars.length));
return result;
};
export const psxTime = {
/** Current time in ms (optionally shifted by tz offset minutes). */
nowMs: (tzOffsetMinutes = 0) => Date.now() + tzOffsetMinutes * 60000,
/** Rough "plus a day" in ms (with tz shift). */
nowMsPlusOneDay: (tzOffsetMinutes = 0) => {
const now = new Date();
const posixtime = (now.getTime() + tzOffsetMinutes * 60000) + 100000000; // ~+27.7h constant from original
return posixtime;
},
/** Current time in seconds (with tz shift). */
nowSec: (tzOffsetMinutes = 0) => Math.floor((Date.now() + tzOffsetMinutes * 60000) / 1000),
/** Convert seconds epoch to human string YYYY-MM-DD HH:mm:ss (with tz shift). */
secToHuman: (sec, tzOffsetMinutes = 0) => {
const date = new Date(sec * 1000 + tzOffsetMinutes * 60000);
const pad = (n, l = 2) => String(n).padStart(l, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +
`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
},
toHumanPlus1MinMs: () => Date.now() + 60_000,
toHumanPlus1HrMs: () => Date.now() + 60 * 60_000,
toHumanPlus1WkMs: () => Date.now() + 7 * 24 * 60 * 60_000,
toHumanPlus1MoMs: () => {
const d = new Date();
d.setMonth(d.getMonth() + 1);
return d.getTime();
},
toHumanPlus1YrMs: () => {
const d = new Date();
d.setFullYear(d.getFullYear() + 1);
return d.getTime();
},
toHumanMinus1MinMs: () => Date.now() - 60_000,
toHumanMinus1HrMs: () => Date.now() - 60 * 60_000,
toHumanMinus1WkMs: () => Date.now() - 7 * 24 * 60 * 60_000,
toHumanMinus1MoMs: () => {
const d = new Date();
d.setMonth(d.getMonth() - 1);
return d.getTime();
},
toHumanMinus1YrMs: () => {
const d = new Date();
d.setFullYear(d.getFullYear() - 1);
return d.getTime();
}
};
/**
* Get DOM element by ID.
* @function el
* @param {string} id - Element ID.
* @returns {HTMLElement|null}
*/
export const el = (id) => document.getElementById(id);
/**
* Convert UTF-8 string to hexadecimal.
* @function utf8ToHex
* @param {string} str - Input string.
* @returns {string} Hexadecimal representation.
*/
export const utf8ToHex = (str) =>
Array.from(new TextEncoder().encode(str))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
/**
* Show an alert using SweetAlert.
* Optional : https://cdn.jsdelivr.net/npm/sweetalert2@11
* @function jAlert
* @param {string} title - Alert title.
* @param {string} message - Alert message.
* @param {string} [icon="error"] - Alert icon type.
* @param {number} [timer=6000] - Display duration in milliseconds.
* @returns {void}
*/
export const jAlert = (title, message, icon = "error", timer = 6000) => {
swal({
title,
text: message,
icon,
buttons: false,
timer,
dangerMode: true
});
};
/**
* Get length of trimmed value of an input element.
* Optional
* @function elen
* @param {HTMLInputElement} elem - Input element.
* @returns {number} Trimmed value length.
*/
export const elen = (elem) => elem.value.toString().trim().length;
/**
* Generate random alphanumeric string.
* @function randString
* @param {number} length - Length of the string.
* @returns {string} Random string.
*/
export const randString = (length) => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
};
/**
* Create radio option DOM element for asset selection.
* @function createOptionRadio
* @param {string} name - Token name.
* @param {string|number} assetQty - Quantity.
* @param {string} mph - Minting policy hash.
* @param {number} i - Index for input name uniqueness.
* @returns {HTMLElement} DOM container with radio input and label.
*/
export const createOptionRadio = (name, assetQty, mph, i) => {
const container = document.createElement('div');
const input = document.createElement('input');
const label = document.createElement('label');
input.type = 'radio';
input.className = 'mph-option';
input.value = mph;
input.id = `${mph}-${i}`;
input.name = `${mph}-${i}`;
input.style.width = "15px";
input.style.height = "15px";
input.style.margin = "10px";
input.onchange = function () {
if (this.checked) {
el("tokenName__").value = name;
el("policy").value = `${mph}-${i}`;
el("assetQty").value = assetQty;
const namePart = name.trim().split(" ")[0];
const pid_c = namePart.substr(3, namePart.length);
el("nftId_").value = `${mph}_${pid_c}_${randString(4)}`;
} else {
el("policy").value = "";
el("assetQty").value = "";
}
};
label.htmlFor = input.id;
label.style.fontSize = "15px";
label.textContent = `${i} : ${name} (Qty: ${assetQty})`;
container.appendChild(input);
container.appendChild(label);
return container;
};
/****************************************************
* tokenUtils.js
*
* Description:
* Utility functions for saving token progress,
* interacting with property share token markets,
* minting property tokens, and working with Cardano
* addresses/keys. Includes server calls (PHP backend)
* and blockchain transaction helpers.
*
****************************************************/
/**
* Save token progress on the backend.
*
* @async
* @function save_
* @param {string} tokenHexName - Token name in hex format.
* @param {string} mph - Minting Policy Hash.
* @param {string} txId - Transaction ID.
* @param {number} txIdx - Transaction index (UTxO).
* @param {bigint} qty - Token quantity.
* @param {string} ownPKH - Owner's public key hash.
* @param {string} txh - Transaction hash.
* @param {string} status - Token status (e.g., "open").
* @param {string} tokenName - Human-readable token name.
* @param {?string} uid - User ID (optional).
* @param {?string} pid - Property ID (optional).
* @param {?number} sharesTokenPrice - Token price (optional).
* @param {?string} scriptAddressBech32 - Script address (bech32 format).
* @param {?string} ownAddressBech32 - Owner’s address (bech32 format).
* @param {?string} buyerAddressBech32 - Buyer’s address (bech32 format).
* @param {?string} nftId - NFT identifier (optional).
* @param {string} [phpFileName="./saveProgressTokens.php"] - Target PHP file for saving.
* @returns {Promise<void>}
*/
export const save_ = async (
tokenHexName, mph, txId, txIdx, qty, ownPKH, txh, status, tokenName,
uid = null, pid = null, sharesTokenPrice = null,
scriptAddressBech32 = null, ownAddressBech32 = null, buyerAddressBech32 = null,
nftId = null, phpFileName = "./saveProgressTokens.php"
) => {
const xhttp = new XMLHttpRequest();
xhttp.onload = async function () {
if (this.responseText) {
const msg = this.responseText;
j.log({ msg });
}
};
xhttp.open(
"GET",
phpFileName +
"?tokenHexName=" + tokenHexName +
"&mph=" + mph +
"&txId=" + txId +
"&txIdx=" + txIdx +
"&qty=" + qty +
"&ownPKH=" + ownPKH +
"&txh=" + txh +
"&status=" + status +
"&tokenName=" + tokenName +
"&uid=" + uid +
"&pid=" + pid +
"&sharesTokenPrice=" + sharesTokenPrice +
"&scriptAddressBech32=" + scriptAddressBech32 +
"&ownAddressBech32=" + ownAddressBech32 +
"&buyerAddressBech32=" + buyerAddressBech32 +
"&nftId=" + nftId,
true
);
xhttp.send();
};
/**
* Save token data into share token markets backend.
*
* @function save_share_token_markets
* @param {...any} args - Similar to {@link save_}, with an additional `price`.
* @returns {void}
*/
export const save_share_token_markets = (
tokenHexName, mph, txId, txIdx, qty, ownPKH, txh, status, tokenName,
uid = null, pid = null, sharesTokenPrice = null,
scriptAddressBech32 = null, ownAddressBech32 = null, buyerAddressBech32 = null,
nftId = null, price = null
) => {
const xhttp = new XMLHttpRequest();
xhttp.onload = async function () {
if (this.responseText) {
const msg = this.responseText;
j.log({ msg });
}
};
xhttp.open(
"GET",
"../saveKeyTokensShareMarkets.php?tokenHexName=" + tokenHexName +
"&mph=" + mph +
"&txId=" + txId +
"&txIdx=" + txIdx +
"&qty=" + qty +
"&ownPKH=" + ownPKH +
"&txh=" + txh +
"&status=" + status +
"&tokenName=" + tokenName +
"&uid=" + uid +
"&pid=" + pid +
"&sharesTokenPrice=" + sharesTokenPrice +
"&scriptAddressBech32=" + scriptAddressBech32 +
"&ownAddressBech32=" + ownAddressBech32 +
"&buyerAddressBech32=" + buyerAddressBech32 +
"&nftId=" + nftId +
"&price=" + price,
true
);
xhttp.send();
};
/**
* Generate a random hex string.
*
* @function random
* @param {number} [bytes=16] - Number of bytes.
* @returns {string} Random hex string.
*/
export const random = (bytes = 16) => {
const arr = new Uint8Array(bytes);
window.crypto.getRandomValues(arr);
return Array.from(arr, b => b.toString(16).padStart(2, '0')).join('');
};
/**
* Check if password is strong.
*
* @function isStrongPassword
* @param {string} password - Password string.
* @returns {boolean} True if strong, else false.
*/
export const isStrongPassword = (password) => {
if (password.length < 15) return false;
if (!/[a-z]/.test(password)) return false;
if (!/[A-Z]/.test(password)) return false;
if (!/[0-9]/.test(password)) return false;
if (!/[!@#$%^&*()_\-+=\[\]{};:'",.<>/?\\|`~]/.test(password)) return false;
return true;
};
/**
* Generate cryptographically secure random bytes.
*
* @function randomBytes
* @param {number} length - Byte length.
* @returns {Uint8Array}
*/
export const randomBytes = (length) => {
const bytes = new Uint8Array(length);
crypto.getRandomValues(bytes);
return bytes;
};
const bufToB64 = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf)));
const b64ToBuf = (b64) => {
const bin = atob(b64);
return Uint8Array.from(bin, (c) => c.charCodeAt(0)).buffer;
};
const exportKey = (key) => key.toRawBytes();
/**
* Save and retrieve values from localStorage.
*
* @function save
* @param {string} key - Storage key.
* @param {string} [value] - Optional value to save.
* @returns {string|void} Stored value when no `value` passed.
*/
export const save = (key, value) => {
if (key && value) localStorage.setItem(key, value);
else if (key && !value) return localStorage.getItem(key);
};
Helios Coxylib.js
Helios Coxylib.js Website | Helios Coxylib.js Github | Helios Coxylib.js Video Helios Library |Helios Coxylib.js Jimba.js Frontend Testing Library