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

/*************************************************
 * 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);
};