249 lines
8.2 KiB
JavaScript
249 lines
8.2 KiB
JavaScript
|
|
const CREATE_NFT_FEE_ATOMIC = 50000000n;
|
||
|
|
let createNftBalancesRefreshPromise = null;
|
||
|
|
|
||
|
|
function setCreateNftMessage(message, type = "") {
|
||
|
|
setScopedTransactionMessage("Create NFT", createNftMessage, message, type);
|
||
|
|
}
|
||
|
|
|
||
|
|
function createNftSeriesValue() {
|
||
|
|
return Number(createNftSeriesSelect?.value || 0) === 1 ? 1 : 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalizeCreateNftCountInput() {
|
||
|
|
const isCollection = createNftSeriesValue() === 1;
|
||
|
|
if (createNftCountInput) {
|
||
|
|
createNftCountInput.disabled = !isCollection;
|
||
|
|
createNftCountInput.min = isCollection ? "2" : "1";
|
||
|
|
if (!isCollection) {
|
||
|
|
createNftCountInput.value = "1";
|
||
|
|
} else if (Number(createNftCountInput.value || 0) < 2) {
|
||
|
|
createNftCountInput.value = "2";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateCreateNftDerivedFields({ clampCount = false } = {}) {
|
||
|
|
const isCollection = createNftSeriesValue() === 1;
|
||
|
|
if (clampCount) {
|
||
|
|
normalizeCreateNftCountInput();
|
||
|
|
} else if (createNftCountInput) {
|
||
|
|
createNftCountInput.disabled = !isCollection;
|
||
|
|
createNftCountInput.min = isCollection ? "2" : "1";
|
||
|
|
}
|
||
|
|
|
||
|
|
const rawCount = String(createNftCountInput?.value || "").trim();
|
||
|
|
const parsedCount = Number.parseInt(rawCount, 10);
|
||
|
|
const countLabel = Number.isInteger(parsedCount) && parsedCount > 0 ? String(parsedCount) : "--";
|
||
|
|
if (createNftFeeInput) {
|
||
|
|
createNftFeeInput.value = tokenAtomicToDecimal(CREATE_NFT_FEE_ATOMIC);
|
||
|
|
}
|
||
|
|
if (createNftCreator) {
|
||
|
|
createNftCreator.textContent = activeWalletAddressShort || activeWalletAddressHex || "--";
|
||
|
|
}
|
||
|
|
if (createNftTypeLabel) {
|
||
|
|
createNftTypeLabel.textContent = isCollection ? "Collection" : "Standalone NFT";
|
||
|
|
}
|
||
|
|
if (createNftCountLabel) {
|
||
|
|
createNftCountLabel.textContent = countLabel;
|
||
|
|
}
|
||
|
|
if (createNftFeeLabel) {
|
||
|
|
createNftFeeLabel.textContent =
|
||
|
|
`${tokenAtomicToDecimal(CREATE_NFT_FEE_ATOMIC)} ${activeBaseCoin()}`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function validateCreateNftForm() {
|
||
|
|
if (!walletLoaded) {
|
||
|
|
return "Load a wallet before creating an NFT.";
|
||
|
|
}
|
||
|
|
if (!registrationRegistered) {
|
||
|
|
return "Address registration is required before creating an NFT.";
|
||
|
|
}
|
||
|
|
|
||
|
|
const name = String(createNftNameInput?.value || "").trim();
|
||
|
|
if (!name) {
|
||
|
|
return "NFT name is required.";
|
||
|
|
}
|
||
|
|
if (!/^[A-Za-z0-9\s]+$/.test(name)) {
|
||
|
|
return "NFT name can only contain letters, numbers, and spaces.";
|
||
|
|
}
|
||
|
|
const canonicalName = name.replace(/\s+/g, "").toLowerCase();
|
||
|
|
if (canonicalName.length < 3 || canonicalName.length > 15) {
|
||
|
|
return "NFT name must normalize to 3 to 15 letters or numbers.";
|
||
|
|
}
|
||
|
|
if (canonicalName.toUpperCase() === activeBaseCoin()) {
|
||
|
|
return "NFT name is reserved for the base coin.";
|
||
|
|
}
|
||
|
|
|
||
|
|
const cid = String(createNftIpfsInput?.value || "").trim();
|
||
|
|
if (!cid) {
|
||
|
|
return "IPFS CID is required.";
|
||
|
|
}
|
||
|
|
if (!/^[\x20-\x7E]+$/.test(cid) || cid.length > 100) {
|
||
|
|
return "IPFS CID must be an ASCII value no longer than 100 characters.";
|
||
|
|
}
|
||
|
|
|
||
|
|
const description = String(createNftDescriptionInput?.value || "").trim();
|
||
|
|
if (description && (!/^[\x20-\x7E]+$/.test(description) || description.length > 100)) {
|
||
|
|
return "NFT description must be ASCII and no longer than 100 characters.";
|
||
|
|
}
|
||
|
|
|
||
|
|
const count = Number.parseInt(createNftCountInput?.value || "0", 10);
|
||
|
|
if (!Number.isInteger(count) || count < 1 || count > 4294967295) {
|
||
|
|
return "Enter a valid NFT item count.";
|
||
|
|
}
|
||
|
|
if (createNftSeriesValue() === 0 && count !== 1) {
|
||
|
|
return "A standalone NFT must create exactly 1 item.";
|
||
|
|
}
|
||
|
|
if (createNftSeriesValue() === 1 && count < 2) {
|
||
|
|
return "An NFT collection must contain at least 2 items.";
|
||
|
|
}
|
||
|
|
|
||
|
|
if (baseCoinBalanceAtomic() < CREATE_NFT_FEE_ATOMIC) {
|
||
|
|
return `This transaction requires ${tokenAtomicToDecimal(CREATE_NFT_FEE_ATOMIC)} ${activeBaseCoin()}.`;
|
||
|
|
}
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
function createNftPayload() {
|
||
|
|
return {
|
||
|
|
series: createNftSeriesValue(),
|
||
|
|
nftName: String(createNftNameInput?.value || "").trim(),
|
||
|
|
itemIpfs: String(createNftIpfsInput?.value || "").trim(),
|
||
|
|
count: Number.parseInt(createNftCountInput?.value || "1", 10),
|
||
|
|
description: String(createNftDescriptionInput?.value || "").trim(),
|
||
|
|
txfee: String(CREATE_NFT_FEE_ATOMIC)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function clearCreateNftForm() {
|
||
|
|
if (createNftSeriesSelect) {
|
||
|
|
createNftSeriesSelect.value = "0";
|
||
|
|
}
|
||
|
|
if (createNftNameInput) {
|
||
|
|
createNftNameInput.value = "";
|
||
|
|
}
|
||
|
|
if (createNftIpfsInput) {
|
||
|
|
createNftIpfsInput.value = "";
|
||
|
|
}
|
||
|
|
if (createNftCountInput) {
|
||
|
|
createNftCountInput.value = "1";
|
||
|
|
}
|
||
|
|
if (createNftDescriptionInput) {
|
||
|
|
createNftDescriptionInput.value = "";
|
||
|
|
}
|
||
|
|
updateCreateNftDerivedFields({ clampCount: true });
|
||
|
|
}
|
||
|
|
|
||
|
|
function setCreateNftButtonsDisabled(disabled) {
|
||
|
|
document.querySelector("[data-create-nft-save]")?.toggleAttribute("disabled", disabled);
|
||
|
|
document.querySelector("[data-create-nft-broadcast]")?.toggleAttribute("disabled", disabled);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function handleCreateNftAction(action) {
|
||
|
|
if (!activeWalletBalances.length && walletLoaded && registrationRegistered) {
|
||
|
|
const invoke = window.__TAURI__?.core?.invoke;
|
||
|
|
const broadcastNode = activeBroadcastNode();
|
||
|
|
if (invoke && broadcastNode) {
|
||
|
|
createNftBalancesRefreshPromise ||= invoke("get_active_wallet_balances", { broadcastNode })
|
||
|
|
.then((balances) => {
|
||
|
|
activeWalletBalances = Array.isArray(balances) ? balances : [];
|
||
|
|
})
|
||
|
|
.finally(() => {
|
||
|
|
createNftBalancesRefreshPromise = null;
|
||
|
|
});
|
||
|
|
try {
|
||
|
|
await createNftBalancesRefreshPromise;
|
||
|
|
} catch (error) {
|
||
|
|
setCreateNftMessage(String(error), "error");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const validationError = validateCreateNftForm();
|
||
|
|
if (validationError) {
|
||
|
|
setCreateNftMessage(validationError, "error");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const invoke = window.__TAURI__?.core?.invoke;
|
||
|
|
if (!invoke) {
|
||
|
|
setCreateNftMessage("Wallet bridge is not available.", "error");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const walletKey = await requestSigningWalletKey();
|
||
|
|
if (guiRuntimeSettings.require_key_before_signing !== false && !walletKey) {
|
||
|
|
setCreateNftMessage("Signing cancelled.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
setCreateNftButtonsDisabled(true);
|
||
|
|
try {
|
||
|
|
if (action === "save") {
|
||
|
|
setCreateNftMessage("Saving NFT transaction...");
|
||
|
|
const review = await invoke("save_create_nft_transaction", {
|
||
|
|
...createNftPayload(),
|
||
|
|
walletKey
|
||
|
|
});
|
||
|
|
clearCreateNftForm();
|
||
|
|
setCreateNftMessage(`NFT transaction saved: ${review.file_name}`, "success");
|
||
|
|
if (typeof refreshUnbroadcastTransactions === "function") {
|
||
|
|
refreshUnbroadcastTransactions();
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
const broadcastNode = activeBroadcastNode();
|
||
|
|
if (!broadcastNode) {
|
||
|
|
throw new Error("Select a broadcast node before broadcasting.");
|
||
|
|
}
|
||
|
|
setCreateNftMessage("Broadcasting NFT transaction...");
|
||
|
|
const result = await invoke("broadcast_create_nft_transaction", {
|
||
|
|
...createNftPayload(),
|
||
|
|
broadcastNode,
|
||
|
|
walletKey
|
||
|
|
});
|
||
|
|
if (typeof invalidateDashboardNftBalanceKeys === "function") {
|
||
|
|
invalidateDashboardNftBalanceKeys();
|
||
|
|
}
|
||
|
|
clearCreateNftForm();
|
||
|
|
setCreateNftMessage(result.response || "NFT transaction broadcast.", "success");
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
setCreateNftMessage(String(error), "error");
|
||
|
|
} finally {
|
||
|
|
setCreateNftButtonsDisabled(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function prepareCreateNftForm() {
|
||
|
|
updateCreateNftDerivedFields({ clampCount: true });
|
||
|
|
setCreateNftMessage("");
|
||
|
|
}
|
||
|
|
|
||
|
|
function resetCreateNftSession() {
|
||
|
|
createNftBalancesRefreshPromise = null;
|
||
|
|
clearCreateNftForm();
|
||
|
|
setCreateNftMessage("");
|
||
|
|
}
|
||
|
|
|
||
|
|
createNftSeriesSelect?.addEventListener("change", () => {
|
||
|
|
updateCreateNftDerivedFields({ clampCount: true });
|
||
|
|
setCreateNftMessage("");
|
||
|
|
});
|
||
|
|
createNftCountInput?.addEventListener("input", () => {
|
||
|
|
updateCreateNftDerivedFields();
|
||
|
|
setCreateNftMessage("");
|
||
|
|
});
|
||
|
|
createNftCountInput?.addEventListener("blur", () => {
|
||
|
|
updateCreateNftDerivedFields({ clampCount: true });
|
||
|
|
});
|
||
|
|
createNftNameInput?.addEventListener("input", () => setCreateNftMessage(""));
|
||
|
|
createNftIpfsInput?.addEventListener("input", () => setCreateNftMessage(""));
|
||
|
|
createNftDescriptionInput?.addEventListener("input", () => setCreateNftMessage(""));
|
||
|
|
document
|
||
|
|
.querySelector("[data-create-nft-save]")
|
||
|
|
?.addEventListener("click", () => handleCreateNftAction("save"));
|
||
|
|
document
|
||
|
|
.querySelector("[data-create-nft-broadcast]")
|
||
|
|
?.addEventListener("click", () => handleCreateNftAction("broadcast"));
|