Contractless-GUI-Wallet/frontend/js/data_storage_form.js

445 lines
14 KiB
JavaScript

const DATA_STORAGE_KEY_FEE_ATOMIC = 50000000000n;
const DATA_STORAGE_VALUE_FEE_ATOMIC = 100000000n;
const DATA_STORAGE_U64_MAX = 18446744073709551615n;
const DATA_STORAGE_U128_MAX = 340282366920938463463374607431768211455n;
const DATA_STORAGE_I64_MIN = -9223372036854775808n;
const DATA_STORAGE_I64_MAX = 9223372036854775807n;
const DATA_STORAGE_I128_MIN = -170141183460469231731687303715884105728n;
const DATA_STORAGE_I128_MAX = 170141183460469231731687303715884105727n;
const DATA_STORAGE_NEW_KEY_OPTION = "__new__";
function setDataStorageMessage(message, type = "") {
setScopedTransactionMessage("Data Storage", dataStorageMessage, message, type);
}
function isDataStorageKeyMode() {
return dataStorageModeSelect?.value !== "write_value";
}
function dataStorageFeeAtomic() {
return isDataStorageKeyMode() ? DATA_STORAGE_KEY_FEE_ATOMIC : DATA_STORAGE_VALUE_FEE_ATOMIC;
}
function dataStorageValueTypeName() {
return inferDataStorageValueType(dataStorageValueInput?.value);
}
function inferDataStorageValueType(value) {
const text = String(value ?? "").trim();
if (!text) {
return "";
}
const normalized = text.toLowerCase();
if (["true", "false", "yes", "no"].includes(normalized)) {
return "bool";
}
if (/^\+?\d+$/.test(text)) {
const number = BigInt(text);
if (number <= 255n) return "u8";
if (number <= 65535n) return "u16";
if (number <= 4294967295n) return "u32";
if (number <= DATA_STORAGE_U64_MAX) return "u64";
if (number <= DATA_STORAGE_U128_MAX) return "u128";
return "string";
}
if (/^-\d+$/.test(text)) {
const number = BigInt(text);
if (number >= -128n) return "i8";
if (number >= -32768n) return "i16";
if (number >= -2147483648n) return "i32";
if (number >= DATA_STORAGE_I64_MIN) return "i64";
if (number >= DATA_STORAGE_I128_MIN) return "i128";
return "string";
}
return "string";
}
function dataStorageFieldIsAscii(value) {
return /^[\x20-\x7E]+$/.test(value);
}
function dataStorageHashIsValid(value) {
return /^[0-9a-fA-F]{64}$/.test(String(value || "").trim());
}
function dataStoragePreviousHashValue() {
const text = String(dataStoragePreviousInput?.value || "").trim();
return text === "" ? "0" : text;
}
function dataStorageUsesNewKey() {
return dataStorageKeySelect?.value === DATA_STORAGE_NEW_KEY_OPTION;
}
function selectedDataStorageKey() {
if (dataStorageUsesNewKey()) {
return String(dataStorageKeyInput?.value || "").trim();
}
return String(dataStorageKeySelect?.value || "").trim();
}
async function refreshDataStorageKeyOptions() {
if (!dataStorageKeySelect || isDataStorageKeyMode()) {
return;
}
const previousSelection = dataStorageKeySelect.value;
dataStorageKeySelect.disabled = true;
dataStorageKeySelect.replaceChildren(new Option("Loading storage keys...", ""));
let knownKeys = [];
try {
if (typeof knownAddressDataStorageKeys === "function") {
knownKeys = await knownAddressDataStorageKeys();
}
} catch (_) {
knownKeys = [];
}
knownKeys.sort((left, right) => {
const leftName = left.label || left.storageId;
const rightName = right.label || right.storageId;
return leftName.localeCompare(rightName);
});
const options = [new Option("Select storage key", "")];
knownKeys.forEach(({ storageId, label }) => {
options.push(new Option(label || storageId, storageId));
});
options.push(new Option("Enter new storage key", DATA_STORAGE_NEW_KEY_OPTION));
dataStorageKeySelect.replaceChildren(...options);
dataStorageKeySelect.disabled = false;
const selectionStillExists = Array.from(dataStorageKeySelect.options)
.some((option) => option.value === previousSelection);
dataStorageKeySelect.value = selectionStillExists ? previousSelection : "";
updateDataStorageDerivedFields();
}
function dataStorageParseBigInt(value, max, label) {
const text = String(value || "").trim();
if (!/^\+?\d+$/.test(text)) {
return { error: `${label} must be a whole number.` };
}
const number = BigInt(text);
if (number > max) {
return { error: `${label} is too large for ${dataStorageValueTypeName().toUpperCase()}.` };
}
return { value: number.toString() };
}
function dataStorageParseSignedBigInt(value, min, max, label) {
const text = String(value || "").trim();
if (!/^-?\d+$/.test(text)) {
return { error: `${label} must be a whole number.` };
}
const number = BigInt(text);
if (number < min || number > max) {
return { error: `${label} is outside the ${dataStorageValueTypeName().toUpperCase()} range.` };
}
return { value: number.toString() };
}
function dataStorageNormalizedBool(value) {
const text = String(value || "").trim().toLowerCase();
if (["true", "1", "yes"].includes(text)) {
return { value: "true" };
}
if (["false", "0", "no"].includes(text)) {
return { value: "false" };
}
return { error: "Bool value must be true or false." };
}
function updateDataStorageDerivedFields() {
const keyMode = isDataStorageKeyMode();
const inferredType = dataStorageValueTypeName();
const stringMode = !keyMode && inferredType === "string";
const fee = dataStorageFeeAtomic();
dataStorageTypeRow?.classList.toggle("hidden", keyMode);
dataStorageKeyChoiceRow?.classList.toggle("hidden", keyMode);
dataStorageKeyRow?.classList.toggle("hidden", keyMode || !dataStorageUsesNewKey());
dataStorageFieldRow?.classList.toggle("hidden", keyMode);
dataStorageValueRow?.classList.toggle("hidden", keyMode);
dataStoragePreviousRow?.classList.toggle("hidden", !stringMode);
if (dataStorageTypeSelect) {
dataStorageTypeSelect.value = keyMode ? "" : inferredType;
}
if (dataStorageFeeInput) {
dataStorageFeeInput.value = atomicToDecimal(fee);
}
if (dataStorageWriter) {
dataStorageWriter.textContent = activeWalletAddressShort || activeWalletAddressHex || "--";
}
if (dataStorageActionLabel) {
dataStorageActionLabel.textContent = keyMode ? "Create storage key" : "Write storage value";
}
if (dataStorageTypeLabel) {
dataStorageTypeLabel.textContent = keyMode || !inferredType ? "--" : inferredType.toUpperCase();
}
if (dataStorageFeeLabel) {
dataStorageFeeLabel.textContent = `${atomicToDecimal(fee)} ${activeBaseCoin()}`;
}
}
function validateDataStorageForm() {
if (!walletLoaded) {
return "Load a wallet before creating a data storage transaction.";
}
if (!registrationRegistered) {
return "Address registration is required before creating a data storage transaction.";
}
if (baseCoinBalanceAtomic() < dataStorageFeeAtomic()) {
return `Data storage requires ${atomicToDecimal(dataStorageFeeAtomic())} ${activeBaseCoin()} for the fee.`;
}
if (isDataStorageKeyMode()) {
return "";
}
const storageKey = selectedDataStorageKey();
if (!storageKey) {
return "Select a storage key or choose Enter new storage key.";
}
if (!dataStorageHashIsValid(storageKey)) {
return "Storage key hash must be exactly 64 hexadecimal characters.";
}
const fieldKey = String(dataStorageFieldKeyInput?.value || "").trim();
if (!fieldKey) {
return "Data key is required.";
}
if (fieldKey.length > 50 || !dataStorageFieldIsAscii(fieldKey)) {
return "Data key must be ASCII and no longer than 50 characters.";
}
const value = String(dataStorageValueInput?.value || "");
const type = dataStorageValueTypeName();
if (!type) {
return "Value is required.";
}
if (type === "bool") {
return dataStorageNormalizedBool(value).error || "";
}
if (type === "u8") {
return dataStorageParseBigInt(value, 255n, "U8 value").error || "";
}
if (type === "u16") {
return dataStorageParseBigInt(value, 65535n, "U16 value").error || "";
}
if (type === "u32") {
return dataStorageParseBigInt(value, 4294967295n, "U32 value").error || "";
}
if (type === "u64") {
return dataStorageParseBigInt(value, DATA_STORAGE_U64_MAX, "U64 value").error || "";
}
if (type === "u128") {
return dataStorageParseBigInt(value, DATA_STORAGE_U128_MAX, "U128 value").error || "";
}
if (type === "i8") {
return dataStorageParseSignedBigInt(value, -128n, 127n, "I8 value").error || "";
}
if (type === "i16") {
return dataStorageParseSignedBigInt(value, -32768n, 32767n, "I16 value").error || "";
}
if (type === "i32") {
return dataStorageParseSignedBigInt(value, -2147483648n, 2147483647n, "I32 value").error || "";
}
if (type === "i64") {
return dataStorageParseSignedBigInt(value, DATA_STORAGE_I64_MIN, DATA_STORAGE_I64_MAX, "I64 value").error || "";
}
if (type === "i128") {
return dataStorageParseSignedBigInt(value, DATA_STORAGE_I128_MIN, DATA_STORAGE_I128_MAX, "I128 value").error || "";
}
if (type === "string") {
if (!dataStorageFieldIsAscii(value) || value.length > 180) {
return "String value must be ASCII and no longer than 180 characters.";
}
const previous = dataStoragePreviousHashValue();
if (previous !== "0" && !dataStorageHashIsValid(previous)) {
return "Previous string transaction must be 0 or a 64 character hash.";
}
return "";
}
return "Select a valid storage value type.";
}
function normalizedDataStorageValue() {
const value = String(dataStorageValueInput?.value || "").trim();
const type = dataStorageValueTypeName();
if (type === "bool") {
return dataStorageNormalizedBool(value).value;
}
if (type === "u8") {
return dataStorageParseBigInt(value, 255n, "U8 value").value;
}
if (type === "u16") {
return dataStorageParseBigInt(value, 65535n, "U16 value").value;
}
if (type === "u32") {
return dataStorageParseBigInt(value, 4294967295n, "U32 value").value;
}
if (type === "u64") {
return dataStorageParseBigInt(value, DATA_STORAGE_U64_MAX, "U64 value").value;
}
if (type === "u128") {
return dataStorageParseBigInt(value, DATA_STORAGE_U128_MAX, "U128 value").value;
}
if (type === "i8") {
return dataStorageParseSignedBigInt(value, -128n, 127n, "I8 value").value;
}
if (type === "i16") {
return dataStorageParseSignedBigInt(value, -32768n, 32767n, "I16 value").value;
}
if (type === "i32") {
return dataStorageParseSignedBigInt(value, -2147483648n, 2147483647n, "I32 value").value;
}
if (type === "i64") {
return dataStorageParseSignedBigInt(value, DATA_STORAGE_I64_MIN, DATA_STORAGE_I64_MAX, "I64 value").value;
}
if (type === "i128") {
return dataStorageParseSignedBigInt(value, DATA_STORAGE_I128_MIN, DATA_STORAGE_I128_MAX, "I128 value").value;
}
return String(dataStorageValueInput?.value || "").trim();
}
function dataStoragePayload() {
return {
mode: dataStorageModeSelect.value,
valueType: dataStorageValueTypeName(),
storageKey: selectedDataStorageKey(),
fieldKey: String(dataStorageFieldKeyInput?.value || "").trim(),
value: isDataStorageKeyMode() ? "" : normalizedDataStorageValue(),
previousHash: isDataStorageKeyMode() ? "0" : dataStoragePreviousHashValue(),
txfee: String(dataStorageFeeAtomic())
};
}
function clearDataStorageForm() {
if (dataStorageModeSelect) {
dataStorageModeSelect.value = "create_key";
}
if (dataStorageTypeSelect) {
dataStorageTypeSelect.value = "";
}
if (dataStorageKeySelect) {
dataStorageKeySelect.value = "";
}
if (dataStorageKeyInput) {
dataStorageKeyInput.value = "";
}
if (dataStorageFieldKeyInput) {
dataStorageFieldKeyInput.value = "";
}
if (dataStorageValueInput) {
dataStorageValueInput.value = "";
}
if (dataStoragePreviousInput) {
dataStoragePreviousInput.value = "";
}
updateDataStorageDerivedFields();
}
function setDataStorageButtonsDisabled(disabled) {
document.querySelector("[data-data-storage-save]")?.toggleAttribute("disabled", disabled);
document.querySelector("[data-data-storage-broadcast]")?.toggleAttribute("disabled", disabled);
}
async function handleDataStorageAction(action) {
const validationError = validateDataStorageForm();
if (validationError) {
setDataStorageMessage(validationError, "error");
return;
}
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke) {
setDataStorageMessage("Wallet bridge is not available.", "error");
return;
}
const walletKey = await requestSigningWalletKey();
if (guiRuntimeSettings.require_key_before_signing !== false && !walletKey) {
setDataStorageMessage("Signing cancelled.");
return;
}
setDataStorageButtonsDisabled(true);
try {
if (action === "save") {
setDataStorageMessage("Signing and saving data storage transaction...");
const review = await invoke("save_data_storage_transaction", {
...dataStoragePayload(),
walletKey
});
clearDataStorageForm();
setDataStorageMessage(`Data storage 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.");
}
setDataStorageMessage("Signing and broadcasting data storage transaction...");
const result = await invoke("broadcast_data_storage_transaction", {
...dataStoragePayload(),
broadcastNode,
walletKey
});
clearDataStorageForm();
setDataStorageMessage(result.response || "Data storage transaction broadcast.", "success");
}
} catch (error) {
setDataStorageMessage(String(error), "error");
} finally {
setDataStorageButtonsDisabled(false);
}
}
function prepareDataStorageForm() {
updateDataStorageDerivedFields();
refreshDataStorageKeyOptions();
setDataStorageMessage("");
}
function resetDataStorageSession() {
clearDataStorageForm();
setDataStorageMessage("");
}
[
dataStorageModeSelect,
dataStorageTypeSelect,
dataStorageKeySelect,
dataStorageKeyInput,
dataStorageFieldKeyInput,
dataStorageValueInput,
dataStoragePreviousInput
].forEach((element) => {
element?.addEventListener("input", () => {
updateDataStorageDerivedFields();
setDataStorageMessage("");
});
element?.addEventListener("change", () => {
updateDataStorageDerivedFields();
setDataStorageMessage("");
});
});
dataStorageModeSelect?.addEventListener("change", () => {
refreshDataStorageKeyOptions();
});
document
.querySelector("[data-data-storage-save]")
?.addEventListener("click", () => handleDataStorageAction("save"));
document
.querySelector("[data-data-storage-broadcast]")
?.addEventListener("click", () => handleDataStorageAction("broadcast"));