445 lines
14 KiB
JavaScript
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"));
|