added features and fixed cache reset buggs

This commit is contained in:
viraladmin 2026-07-16 08:46:52 -06:00
parent ad64c14a04
commit 18bedf64b0
25 changed files with 3191 additions and 224 deletions

82
Cargo.lock generated
View File

@ -202,44 +202,6 @@ dependencies = [
"objc2",
]
[[package]]
name = "blockchain"
version = "0.1.0"
dependencies = [
"anyhow",
"base64 0.22.1",
"chrono",
"cid",
"colored",
"encrypted_images",
"falcon-rs",
"flexi_logger",
"fn-dsa",
"hex",
"ipnetwork",
"lazy_static",
"log",
"nix 0.27.1",
"once_cell",
"rand 0.8.6",
"rayon",
"ripemd",
"rpassword",
"rust-ini",
"rustyline",
"rustyline-derive",
"serde",
"serde_json",
"shellexpand",
"skein",
"sled",
"tokio",
"tokio-postgres",
"windows-service",
"windows-sys 0.61.2",
"winreg 0.52.0",
]
[[package]]
name = "brotli"
version = "8.0.3"
@ -558,13 +520,51 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3"
[[package]]
name = "contractless"
version = "0.1.0"
dependencies = [
"anyhow",
"base64 0.22.1",
"chrono",
"cid",
"colored",
"encrypted_images",
"falcon-rs",
"flexi_logger",
"fn-dsa",
"hex",
"ipnetwork",
"lazy_static",
"log",
"nix 0.27.1",
"once_cell",
"rand 0.8.6",
"rayon",
"ripemd",
"rpassword",
"rust-ini",
"rustyline",
"rustyline-derive",
"serde",
"serde_json",
"shellexpand",
"skein",
"sled",
"tokio",
"tokio-postgres",
"windows-service",
"windows-sys 0.61.2",
"winreg 0.52.0",
]
[[package]]
name = "contractless-wallet"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"blockchain",
"clipboard-win",
"contractless",
"native-dialog",
"reqwest 0.12.28",
"serde",
@ -1196,9 +1196,9 @@ dependencies = [
[[package]]
name = "encrypted_images"
version = "1.4.0"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "761fe0fc7059c1e9cb4217f49263b146d269322354aeefb8fc519a60b2e8cef1"
checksum = "dda784ca648e472fc1bb0e408cb60b22e6b1647511d3c8f7144dadcf7133f605"
dependencies = [
"base64 0.21.7",
"encoding",

View File

@ -8,7 +8,7 @@ build = "build.rs"
tauri-build = { version = "2", features = [] }
[dependencies]
blockchain = { path = ".." }
contractless = { path = ".." }
base64 = "0.22"
native-dialog = "0.6"
reqwest = { version = "0.12", features = ["rustls-tls"] }

View File

@ -22,10 +22,10 @@ Contractless/
The wallet depends on the blockchain crate with:
```toml
blockchain = { path = ".." }
contractless = { path = ".." }
```
Because of that, `gui wallet/` must be in the same folder that contains the Contractless `src/` folder and root `Cargo.toml`. If the wallet folder is moved somewhere else, the parent `..` path will not point at the blockchain crate and the wallet will not build.
Because of that, `gui_wallet/` must be in the same folder that contains the Contractless `src/` folder and root `Cargo.toml`. If the wallet folder is moved somewhere else, the parent `..` path will not point at the blockchain crate and the wallet will not build.
## Layout
@ -35,7 +35,7 @@ Because of that, `gui wallet/` must be in the same folder that contains the Cont
## Build Check
From `gui wallet/`:
From `gui_wallet/`:
```bash
cargo check

View File

@ -177,6 +177,10 @@
<span class="nav-icon">%</span>
<span>Marketing</span>
</button>
<button class="nav-item desktop-only-nav" data-view="addressdata" type="button">
<span class="nav-icon">{}</span>
<span>Address Data</span>
</button>
<button class="nav-item" data-view="addressbook" type="button">
<span class="nav-icon">@</span>
<span>Address Book</span>
@ -782,6 +786,58 @@
</div>
<div class="startup-message hidden" data-marketing-message></div>
<div class="form-grid compact hidden" data-data-storage-form>
<label>
<span>Storage Action</span>
<select data-data-storage-mode>
<option value="create_key">Create storage key</option>
<option value="write_value">Write storage value</option>
</select>
</label>
<label data-data-storage-key-choice-row>
<span>Storage Key</span>
<select data-data-storage-key-select>
<option value="">Select storage key</option>
<option value="__new__">Enter new storage key</option>
</select>
</label>
<label class="hidden" data-data-storage-key-row>
<span>New Storage Key Hash</span>
<input data-data-storage-key maxlength="64" placeholder="64 character storage key hash">
</label>
<label data-data-storage-field-row>
<span>Data Key</span>
<input data-data-storage-field-key maxlength="50" placeholder="Example: username">
</label>
<label data-data-storage-value-row>
<span>Value</span>
<input data-data-storage-value placeholder="Value to store">
</label>
<label data-data-storage-type-row>
<span>Value Type</span>
<input data-data-storage-value-type readonly>
</label>
<label>
<span>Transaction Fee</span>
<input data-data-storage-fee value="500.00000000" readonly>
</label>
<label data-data-storage-previous-row>
<span>Previous String Tx</span>
<input data-data-storage-previous maxlength="64" placeholder="0 for first string chunk">
</label>
<div class="review-grid wide transfer-details">
<div><span>Writer</span><b data-data-storage-writer>--</b></div>
<div><span>Action</span><b data-data-storage-action-label>--</b></div>
<div><span>Value Type</span><b data-data-storage-type-label>--</b></div>
<div><span>Fee</span><b data-data-storage-fee-label>500.00000000</b></div>
</div>
</div>
<div class="form-actions hidden" data-data-storage-actions>
<button class="secondary-button" type="button" data-data-storage-save>Save Transaction</button>
<button class="primary-button" type="button" data-data-storage-broadcast>Broadcast Transaction</button>
</div>
<div class="startup-message hidden" data-data-storage-message></div>
<div class="form-grid compact hidden" data-vanity-form>
<label>
<span>Vanity Name</span>
@ -837,6 +893,7 @@
<button class="transaction-type" type="button" data-transaction-type="Collateral Claim">Collateral Claim</button>
<button class="transaction-type" type="button" data-transaction-type="Burn">Burn</button>
<button class="transaction-type" type="button" data-transaction-type="Marketing">Marketing</button>
<button class="transaction-type" type="button" data-transaction-type="Data Storage">Data Storage</button>
<button class="transaction-type" type="button" data-transaction-type="Vanity Address">Vanity Address</button>
</div>
</aside>
@ -1106,7 +1163,7 @@
</section>
<section class="marketing-view hidden" id="marketing-view">
<div class="history-workspace panel">
<div class="history-workspace marketing-workspace panel">
<div class="panel-header">
<div>
<h2>Marketing Campaigns</h2>
@ -1215,6 +1272,27 @@
</div>
</section>
<section class="address-data-view hidden" id="address-data-view">
<div class="address-data-workspace panel">
<div class="panel-header address-data-header">
<div>
<h2>Address Data</h2>
<p>On-chain application data stored by the active address.</p>
</div>
<div class="address-data-actions">
<select data-address-data-select aria-label="Storage ID" disabled>
<option value="">Select storage key</option>
</select>
<button class="secondary-button" type="button" data-address-data-refresh>Refresh</button>
</div>
</div>
<div class="address-data-selection hidden" data-address-data-selection></div>
<div class="address-data-groups" data-address-data-groups></div>
<div class="assets-state" data-address-data-state>Load a wallet to view stored address data.</div>
</div>
</section>
<section class="settings-view hidden" id="settings-view">
<div class="settings-workspace panel">
<div class="panel-header settings-header">
@ -1540,6 +1618,7 @@
<script src="js/dashboard_network.js"></script>
<script src="js/address_book.js"></script>
<script src="js/transaction_history.js"></script>
<script src="js/address_data_page.js"></script>
<script src="js/history_page.js"></script>
<script src="js/assets_page.js"></script>
<script src="js/nfts_page.js"></script>
@ -1557,6 +1636,7 @@
<script src="js/collateral_claim_form.js"></script>
<script src="js/burn_form.js"></script>
<script src="js/marketing_form.js"></script>
<script src="js/data_storage_form.js"></script>
<script src="js/vanity_address_form.js"></script>
<script src="js/unbroadcast_transactions.js"></script>
<script src="js/wallet_session.js"></script>

View File

@ -0,0 +1,593 @@
const STORAGE_TRANSACTION_TYPES = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113];
const STORAGE_ZERO_HASH = "0".repeat(64);
const ADDRESS_DATA_DELETE_FEE_ATOMIC = 100000000n;
const addressDataGroups = document.querySelector("[data-address-data-groups]");
const addressDataState = document.querySelector("[data-address-data-state]");
const addressDataSelect = document.querySelector("[data-address-data-select]");
const addressDataRefresh = document.querySelector("[data-address-data-refresh]");
const addressDataSelection = document.querySelector("[data-address-data-selection]");
let addressDataRecords = [];
let addressDataLoading = false;
let addressDataWalletToken = "";
let addressDataError = "";
let addressDataLabels = { labels: {} };
let addressDataLabelsLoaded = false;
let addressDataLabelSavePromise = Promise.resolve();
function escapeAddressDataText(value) {
return String(value ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function trimStoredText(value) {
return String(value ?? "").replace(/[\0 ]+$/g, "");
}
function normalizeStorageAddress(value) {
return String(value || "").trim().toLowerCase().replace(/\.cltc$/, "");
}
function isActiveStorageAddress(value) {
const active = normalizeStorageAddress(activeWalletAddressHex || activeWalletAddressShort);
return Boolean(active && normalizeStorageAddress(value) === active);
}
function storageUnsignedRecord(transaction) {
if (!transaction || typeof transaction !== "object" || Array.isArray(transaction)) {
return null;
}
return transaction.unsigned_storagekey
|| transaction.unsigned_storagebool
|| transaction.unsigned_storageu8
|| transaction.unsigned_storageu16
|| transaction.unsigned_storageu32
|| transaction.unsigned_storageu64
|| transaction.unsigned_storageu128
|| transaction.unsigned_storage
|| transaction.unsigned_storagei8
|| transaction.unsigned_storagei16
|| transaction.unsigned_storagei32
|| transaction.unsigned_storagei64
|| transaction.unsigned_storagei128
|| transaction.unsigned_deletekey
|| null;
}
function storageTypeLabel(type) {
return {
101: "Bool",
102: "U8",
103: "U16",
104: "U32",
105: "U64",
106: "U128",
107: "String",
108: "I8",
109: "I16",
110: "I32",
111: "I64",
112: "I128"
}[Number(type)] || "Value";
}
function storageDisplayValue(type, value) {
if (Number(type) === 101) {
return value === true || value === 1 || value === "1" ? "true" : "false";
}
return trimStoredText(value);
}
function storageRecordTime(record, unsigned) {
return Number(unsigned?.time || unsigned?.timestamp || 0);
}
function storageRecordOrder(left, right) {
const blockDifference = Number(left.record?.block || 0) - Number(right.record?.block || 0);
if (blockDifference) {
return blockDifference;
}
const timeDifference = storageRecordTime(left.record, left.unsigned)
- storageRecordTime(right.record, right.unsigned);
if (timeDifference) {
return timeDifference;
}
return left.txid.localeCompare(right.txid);
}
function storageTransactionIds(index) {
const seen = new Set();
const txids = [];
for (const type of STORAGE_TRANSACTION_TYPES) {
const values = index?.type_to_txids?.[String(type)];
for (const txid of Array.isArray(values) ? values : []) {
if (txid && !seen.has(txid)) {
seen.add(txid);
txids.push(txid);
}
}
}
return txids;
}
function storageRecordNeedsDecode(record) {
const type = Number(record?.txtype || record?.transaction_type || 0);
return STORAGE_TRANSACTION_TYPES.includes(type) && Boolean(record?.transaction?.unknown);
}
async function repairLegacyStorageRecords(cache, index, walletToken) {
const invoke = window.__TAURI__?.core?.invoke;
const broadcastNode = activeBroadcastNode();
if (!invoke || !broadcastNode) {
return cache;
}
const stale = Object.entries(cache.transactions || {})
.filter(([, record]) => storageRecordNeedsDecode(record));
if (!stale.length) {
return cache;
}
setAddressDataState("Updating cached storage records...", "loading");
for (const [txid] of stale) {
if (!isActiveWalletCacheToken(walletToken)) {
return cache;
}
try {
const tx = await invoke("transaction_by_txid", { broadcastNode, txid });
const record = repairCachedTransactionRecord(txid, {
txid,
block: tx.block_height || null,
transaction: tx.transaction,
transaction_type: tx.transaction_type,
linked_transactions: {},
miner_earnings: []
});
cache.transactions[txid] = record;
const fileName = index.txid_to_file?.[txid];
if (fileName) {
await writeTransactionRecordFile(fileName, record, walletToken);
}
} catch (_) {
// Keep the remaining locally decoded records visible if one old lookup fails.
}
}
return cache;
}
function buildAddressDataGroups(cache) {
const records = Object.entries(cache.transactions || {})
.map(([txid, record]) => ({
txid,
record,
unsigned: storageUnsignedRecord(record?.transaction),
type: Number(record?.txtype || decodedTransactionType(record?.transaction) || 0)
}))
.filter((item) => item.unsigned && isActiveStorageAddress(item.unsigned.address))
.sort(storageRecordOrder);
const groups = new Map();
const stringsByTxid = new Map(records.filter((item) => item.type === 107).map((item) => [item.txid, item]));
const stringDepthCache = new Map();
const stringDepth = (item, visited = new Set()) => {
if (stringDepthCache.has(item.txid)) {
return stringDepthCache.get(item.txid);
}
const previous = String(item.unsigned.previous || "").toLowerCase();
if (!previous || previous === STORAGE_ZERO_HASH) {
stringDepthCache.set(item.txid, 0);
return 0;
}
if (visited.has(item.txid)) {
return null;
}
const parent = stringsByTxid.get(previous);
if (!parent) {
return null;
}
visited.add(item.txid);
const parentDepth = stringDepth(parent, visited);
const depth = parentDepth === null ? null : parentDepth + 1;
stringDepthCache.set(item.txid, depth);
return depth;
};
const ensureGroup = (storageId) => {
const normalized = String(storageId || "").toLowerCase();
if (!groups.has(normalized)) {
groups.set(normalized, {
storageId: normalized,
created: 0,
updated: 0,
scalarFields: new Map(),
stringFields: new Map()
});
}
return groups.get(normalized);
};
for (const item of records) {
const timestamp = storageRecordTime(item.record, item.unsigned);
if (item.type === 100) {
const group = ensureGroup(item.txid);
group.created = timestamp;
group.updated = Math.max(group.updated, timestamp);
continue;
}
const storageId = String(item.unsigned.storagekey || "").toLowerCase();
if (!storageId) {
continue;
}
const group = ensureGroup(storageId);
const key = trimStoredText(item.unsigned.key);
group.updated = Math.max(group.updated, timestamp);
if (item.type === 113) {
const chunkMatch = key.match(/^(.*)_(\d{2})$/);
if (chunkMatch && Number(chunkMatch[2]) < 20) {
group.stringFields.get(chunkMatch[1])?.delete(Number(chunkMatch[2]));
} else {
group.scalarFields.delete(key);
group.stringFields.delete(key);
}
continue;
}
if (item.type === 107) {
const depth = stringDepth(item);
if (depth === null || depth < 0 || depth > 19) {
continue;
}
if (!group.stringFields.has(key)) {
group.stringFields.set(key, new Map());
}
group.stringFields.get(key).set(depth, {
value: trimStoredText(item.unsigned.value),
timestamp,
txid: item.txid
});
continue;
}
group.scalarFields.set(key, {
key,
value: storageDisplayValue(item.type, item.unsigned.value),
type: storageTypeLabel(item.type),
timestamp,
txid: item.txid
});
}
return Array.from(groups.values()).map((group) => {
const fields = Array.from(group.scalarFields.values());
for (const [key, chunks] of group.stringFields.entries()) {
const orderedChunks = Array.from(chunks.entries()).sort(([left], [right]) => left - right);
if (!orderedChunks.length) {
continue;
}
fields.push({
key,
value: orderedChunks.map(([, chunk]) => chunk.value).join(""),
type: "String",
timestamp: Math.max(...orderedChunks.map(([, chunk]) => chunk.timestamp)),
txid: orderedChunks.at(-1)?.[1]?.txid || ""
});
}
fields.sort((left, right) => left.key.localeCompare(right.key));
return { ...group, fields };
}).sort((left, right) => (right.updated || right.created) - (left.updated || left.created));
}
function setAddressDataState(message, mode = "") {
if (!addressDataState) {
return;
}
addressDataState.textContent = message || "";
addressDataState.classList.toggle("loading", mode === "loading");
addressDataState.classList.toggle("error", mode === "error");
}
function normalizeAddressDataLabels(source) {
const labels = source?.labels && typeof source.labels === "object" && !Array.isArray(source.labels)
? source.labels
: {};
const normalized = {};
Object.entries(labels).forEach(([storageId, label]) => {
const id = String(storageId || "").trim().toLowerCase();
const text = String(label || "").trim().slice(0, 80);
if (/^[a-f0-9]{64}$/.test(id) && text) {
normalized[id] = text;
}
});
return { labels: normalized };
}
async function loadAddressDataLabels() {
if (!walletLoaded || addressDataLabelsLoaded) {
return;
}
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke) {
return;
}
try {
const raw = await invoke("read_storage_labels");
addressDataLabels = normalizeAddressDataLabels(JSON.parse(raw || "{}"));
} catch (_) {
addressDataLabels = { labels: {} };
} finally {
addressDataLabelsLoaded = true;
}
}
async function saveAddressDataLabel(storageId, label) {
const id = String(storageId || "").trim().toLowerCase();
const text = String(label || "").trim().slice(0, 80);
const walletToken = activeWalletCacheToken();
if (!/^[a-f0-9]{64}$/.test(id)) {
return;
}
if (text) {
addressDataLabels.labels[id] = text;
} else {
delete addressDataLabels.labels[id];
}
syncAddressDataOptions();
const status = addressDataSelection?.querySelector("[data-address-data-label-status]");
if (status) {
status.textContent = "Saving...";
}
addressDataLabelSavePromise = addressDataLabelSavePromise
.catch(() => {})
.then(() => {
if (!isActiveWalletCacheToken(walletToken)) {
return;
}
return window.__TAURI__?.core?.invoke?.("write_storage_labels", {
storageLabelsJson: JSON.stringify(normalizeAddressDataLabels(addressDataLabels))
});
});
try {
await addressDataLabelSavePromise;
if (status?.isConnected) {
status.textContent = "Saved";
}
} catch (_) {
if (status?.isConnected) {
status.textContent = "Unable to save label";
status.classList.add("error");
}
}
}
function syncAddressDataOptions() {
if (!addressDataSelect) {
return;
}
const currentSelection = addressDataSelect.value;
if (!addressDataRecords.length) {
addressDataSelect.innerHTML = '<option value="">Select storage key</option>';
addressDataSelect.disabled = true;
return;
}
addressDataSelect.innerHTML = '<option value="">Select storage key</option>' + addressDataRecords.map((group) => {
const optionLabel = addressDataLabels.labels[group.storageId] || group.storageId;
return `<option value="${escapeAddressDataText(group.storageId)}">${escapeAddressDataText(optionLabel)}</option>`;
}).join("");
addressDataSelect.disabled = false;
addressDataSelect.value = addressDataRecords.some((group) => group.storageId === currentSelection)
? currentSelection
: "";
}
async function knownAddressDataStorageKeys() {
await loadAddressDataLabels();
await loadAddressDataPage();
return addressDataRecords.map((group) => ({
storageId: group.storageId,
label: addressDataLabels.labels[group.storageId] || ""
}));
}
function renderAddressDataPage() {
if (!addressDataGroups) {
return;
}
syncAddressDataOptions();
const selectedStorageId = addressDataSelect?.value || "";
const selectedGroup = addressDataRecords.find((group) => group.storageId === selectedStorageId);
if (addressDataSelection) {
addressDataSelection.classList.toggle("hidden", !selectedGroup);
addressDataSelection.innerHTML = selectedGroup ? `
<label class="address-data-label-field">
<span>Application Label</span>
<input type="text" maxlength="80" value="${escapeAddressDataText(addressDataLabels.labels[selectedGroup.storageId] || "")}" placeholder="Add a label" data-address-data-label="${escapeAddressDataText(selectedGroup.storageId)}">
<small data-address-data-label-status></small>
</label>
<div class="address-data-selected-id">
<span class="mini-label">Storage Key</span>
<code>${escapeAddressDataText(selectedGroup.storageId)}</code>
</div>
` : "";
}
addressDataGroups.innerHTML = selectedGroup ? `
<section class="address-data-group">
<div class="address-data-fields">
<div class="address-data-field address-data-field-header" aria-hidden="true">
<strong>Key</strong>
<strong>Value</strong>
<strong>Type</strong>
<strong>Action</strong>
</div>
${selectedGroup.fields.length ? selectedGroup.fields.map((field) => `
<div class="address-data-field">
<div class="address-data-field-key">
<strong>${escapeAddressDataText(field.key || "<empty>")}</strong>
</div>
<div class="address-data-field-value">${escapeAddressDataText(field.value)}</div>
<div class="address-data-field-type">${escapeAddressDataText(field.type)}</div>
<div class="address-data-field-action">
<button class="address-data-delete-button" type="button" data-address-data-delete="${escapeAddressDataText(field.key)}">Delete</button>
</div>
</div>
`).join("") : '<div class="subtle-state">No values have been stored under this storage ID.</div>'}
</div>
</section>
` : "";
if (!walletLoaded) {
setAddressDataState("Load a wallet to view stored address data.");
} else if (addressDataError) {
setAddressDataState(addressDataError, "error");
} else if (addressDataLoading) {
setAddressDataState("Loading stored address data...", "loading");
} else if (!addressDataRecords.length) {
setAddressDataState("No stored address data found in the local transaction cache.");
} else if (!selectedGroup) {
setAddressDataState("Select a storage ID to view its keys.");
} else {
const keyCount = selectedGroup.fields.length;
setAddressDataState(`${keyCount} key${keyCount === 1 ? "" : "s"} shown`);
}
}
async function loadAddressDataPage({ force = false } = {}) {
const walletToken = activeWalletCacheToken();
if (!walletLoaded || !walletToken || addressDataLoading) {
renderAddressDataPage();
return;
}
await loadAddressDataLabels();
if (!force && addressDataWalletToken === walletToken && addressDataRecords.length) {
renderAddressDataPage();
return;
}
addressDataLoading = true;
addressDataError = "";
renderAddressDataPage();
try {
const index = await readTransactionIndexObject(walletToken);
const txids = storageTransactionIds(index);
let cache = await readTransactionCacheRecords(index, txids, walletToken);
cache = await repairLegacyStorageRecords(cache, index, walletToken);
if (!isActiveWalletCacheToken(walletToken)) {
return;
}
addressDataRecords = buildAddressDataGroups(cache);
addressDataWalletToken = walletToken;
} catch (error) {
addressDataRecords = [];
addressDataError = String(error);
} finally {
addressDataLoading = false;
renderAddressDataPage();
}
}
function prepareAddressDataPage() {
renderAddressDataPage();
loadAddressDataPage();
}
function resetAddressDataSelection() {
if (addressDataSelect) {
addressDataSelect.value = "";
}
renderAddressDataPage();
}
function resetAddressDataPage() {
addressDataRecords = [];
addressDataLoading = false;
addressDataWalletToken = "";
addressDataError = "";
addressDataLabels = { labels: {} };
addressDataLabelsLoaded = false;
addressDataLabelSavePromise = Promise.resolve();
if (addressDataSelect) {
addressDataSelect.innerHTML = '<option value="">Select storage key</option>';
addressDataSelect.disabled = true;
}
if (addressDataSelection) {
addressDataSelection.innerHTML = "";
addressDataSelection.classList.add("hidden");
}
if (addressDataGroups) {
addressDataGroups.innerHTML = "";
}
renderAddressDataPage();
}
addressDataSelect?.addEventListener("change", renderAddressDataPage);
addressDataRefresh?.addEventListener("click", () => loadAddressDataPage({ force: true }));
addressDataGroups?.addEventListener("click", async (event) => {
const button = event.target.closest("[data-address-data-delete]");
if (!button || button.disabled) {
return;
}
const storageKey = addressDataSelect?.value || "";
const fieldKey = button.dataset.addressDataDelete || "";
const invoke = window.__TAURI__?.core?.invoke;
const broadcastNode = activeBroadcastNode();
if (!invoke || !storageKey || !fieldKey || !broadcastNode) {
setAddressDataState("Unable to prepare the storage delete transaction.", "error");
return;
}
const fee = formatAtomicBalance(ADDRESS_DATA_DELETE_FEE_ATOMIC);
const walletKey = guiRuntimeSettings.require_key_before_signing === false
? null
: await requestWalletBackupKey(
`Deleting this key requires a transaction fee of ${fee} ${activeBaseCoin()}. Enter the wallet encryption key to sign and broadcast the transaction.`,
"Delete Stored Data"
);
if (guiRuntimeSettings.require_key_before_signing !== false && !walletKey) {
return;
}
button.disabled = true;
setAddressDataState("Signing and broadcasting storage deletion...", "loading");
try {
await invoke("broadcast_delete_storage_transaction", {
walletKey,
broadcastNode,
storageKey,
fieldKey,
txfee: ADDRESS_DATA_DELETE_FEE_ATOMIC.toString()
});
const group = addressDataRecords.find((entry) => entry.storageId === storageKey);
if (group) {
group.fields = group.fields.filter((field) => field.key !== fieldKey);
}
renderAddressDataPage();
setAddressDataState(`Delete transaction broadcast. ${group?.fields.length || 0} key${group?.fields.length === 1 ? "" : "s"} shown`);
} catch (error) {
button.disabled = false;
setAddressDataState(String(error), "error");
}
});
addressDataSelection?.addEventListener("change", (event) => {
const input = event.target.closest("[data-address-data-label]");
if (input) {
saveAddressDataLabel(input.dataset.addressDataLabel, input.value);
}
});
addressDataSelection?.addEventListener("keydown", (event) => {
if (event.key === "Enter" && event.target.matches("[data-address-data-label]")) {
event.preventDefault();
event.target.blur();
}
});

View File

@ -8,6 +8,7 @@
nfts: ["NFTs", "View and manage non-fungible token holdings."],
loans: ["Loans", "Create loans, payments, and collateral claims."],
marketing: ["Marketing", "Review campaign activity from agency wallets."],
addressdata: ["Address Data", "Review on-chain application data stored by this address."],
addressbook: ["Address Book", "Manage known addresses and labels."],
settings: ["Settings", "Manage wallets, nodes, security, and app preferences."]
};
@ -36,6 +37,7 @@ const assetsView = document.querySelector("#assets-view");
const nftsView = document.querySelector("#nfts-view");
const loansView = document.querySelector("#loans-view");
const marketingView = document.querySelector("#marketing-view");
const addressDataView = document.querySelector("#address-data-view");
const addressbookView = document.querySelector("#addressbook-view");
const settingsView = document.querySelector("#settings-view");
const viewTitle = document.querySelector("#view-title");
@ -202,6 +204,27 @@ const marketingFeeInput = document.querySelector("[data-marketing-fee]");
const marketingAdvertiser = document.querySelector("[data-marketing-advertiser]");
const marketingFeeLabel = document.querySelector("[data-marketing-fee-label]");
const marketingMessage = document.querySelector("[data-marketing-message]");
const dataStorageForm = document.querySelector("[data-data-storage-form]");
const dataStorageActions = document.querySelector("[data-data-storage-actions]");
const dataStorageModeSelect = document.querySelector("[data-data-storage-mode]");
const dataStorageTypeSelect = document.querySelector("[data-data-storage-value-type]");
const dataStorageTypeRow = document.querySelector("[data-data-storage-type-row]");
const dataStorageKeyChoiceRow = document.querySelector("[data-data-storage-key-choice-row]");
const dataStorageKeySelect = document.querySelector("[data-data-storage-key-select]");
const dataStorageKeyRow = document.querySelector("[data-data-storage-key-row]");
const dataStorageFieldRow = document.querySelector("[data-data-storage-field-row]");
const dataStorageValueRow = document.querySelector("[data-data-storage-value-row]");
const dataStoragePreviousRow = document.querySelector("[data-data-storage-previous-row]");
const dataStorageKeyInput = document.querySelector("[data-data-storage-key]");
const dataStorageFieldKeyInput = document.querySelector("[data-data-storage-field-key]");
const dataStorageValueInput = document.querySelector("[data-data-storage-value]");
const dataStoragePreviousInput = document.querySelector("[data-data-storage-previous]");
const dataStorageFeeInput = document.querySelector("[data-data-storage-fee]");
const dataStorageWriter = document.querySelector("[data-data-storage-writer]");
const dataStorageActionLabel = document.querySelector("[data-data-storage-action-label]");
const dataStorageTypeLabel = document.querySelector("[data-data-storage-type-label]");
const dataStorageFeeLabel = document.querySelector("[data-data-storage-fee-label]");
const dataStorageMessage = document.querySelector("[data-data-storage-message]");
const vanityForm = document.querySelector("[data-vanity-form]");
const vanityActions = document.querySelector("[data-vanity-actions]");
const vanityNameInput = document.querySelector("[data-vanity-name]");
@ -368,6 +391,7 @@ let latestDashboardTransactionsTimer = null;
let latestDashboardTxids = [];
let latestDashboardTransactionRecords = new Map();
let backgroundTransactionSyncId = 0;
let backgroundTransactionSyncPromise = Promise.resolve();
let historyRefreshTimer = null;
let historyRefreshReadId = 0;
let historyRefreshInFlight = false;
@ -676,6 +700,7 @@ async function refreshDashboardNftBalanceKeys() {
return knownNftBalanceLoad;
}
const wasReady = dashboardNftBalanceKeysReady();
knownNftBalanceLoad = invoke("nft_list", { broadcastNode })
.then((rows) => {
const nextKeys = new Set();
@ -692,7 +717,7 @@ async function refreshDashboardNftBalanceKeys() {
knownNftBalanceKeys = nextKeys;
knownNftBalanceNode = broadcastNode;
knownNftBalanceLoadedAt = Date.now();
return previous !== next;
return !wasReady || previous !== next;
})
.catch(() => false)
.finally(() => {
@ -832,6 +857,7 @@ function resetTransactionForms() {
"resetCollateralClaimSession",
"resetBurnSession",
"resetMarketingSession",
"resetDataStorageSession",
"resetVanityAddressSession"
].forEach((name) => {
if (typeof window[name] === "function") {
@ -850,6 +876,7 @@ function resetTransactionForms() {
"clearCollateralClaimForm",
"clearBurnForm",
"clearMarketingForm",
"clearDataStorageForm",
"clearVanityForm"
].forEach((name) => {
if (typeof window[name] === "function") {
@ -868,6 +895,7 @@ function resetTransactionForms() {
collateralClaimMessage,
burnMessage,
marketingMessage,
dataStorageMessage,
vanityMessage
].forEach((element) => {
if (!element) {

View File

@ -56,12 +56,11 @@ async function loadCollateralClaimContracts(force = false) {
if (collateralClaimLoadPromise) {
return collateralClaimLoadPromise;
}
const invoke = window.__TAURI__?.core?.invoke;
const broadcastNode = activeBroadcastNode();
if (!invoke || !broadcastNode || !walletLoaded || !registrationRegistered) {
if (!walletLoaded || !registrationRegistered) {
return;
}
const key = `${activeWalletAddressShort || activeWalletAddressHex}|${broadcastNode}`;
const walletToken = activeWalletCacheToken();
const key = `${walletToken}|local-claimable-collateral`;
if (!force && collateralClaimContractsKey === key) {
populateCollateralClaimContracts();
return;
@ -69,7 +68,9 @@ async function loadCollateralClaimContracts(force = false) {
collateralClaimLoadPromise = (async () => {
setCollateralClaimMessage("Loading claimable collateral...");
const contracts = await invoke("claimable_collateral_contracts", { broadcastNode });
const contracts = typeof loadLocalClaimableCollateralContracts === "function"
? await loadLocalClaimableCollateralContracts(walletToken)
: [];
collateralClaimContracts = Array.isArray(contracts) ? contracts : [];
collateralClaimContractsKey = key;
populateCollateralClaimContracts();

View File

@ -199,7 +199,7 @@ function signedAmount(sign, value) {
function signedBaseFee(sign, value) {
const amount = signedAmount(sign, value);
return amount && amount !== "0" ? `${amount} ${activeBaseCoin()}` : "--";
return amount && amount !== "0" ? amount : "--";
}
function statusForBlock(blockHeight, currentHeight = latestKnownBlockHeight) {
@ -248,7 +248,21 @@ function txTypeName(type) {
9: "Collateral Claim",
10: "Burn",
11: "Issue Token",
12: "Vanity Address"
12: "Vanity Address",
100: "Data Storage Key",
101: "Data Storage Bool",
102: "Data Storage U8",
103: "Data Storage U16",
104: "Data Storage U32",
105: "Data Storage U64",
106: "Data Storage U128",
107: "Data Storage String",
108: "Data Storage I8",
109: "Data Storage I16",
110: "Data Storage I32",
111: "Data Storage I64",
112: "Data Storage I128",
113: "Delete Stored Data"
}[type] || "Transaction";
}

View File

@ -0,0 +1,444 @@
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("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("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"));

View File

@ -282,7 +282,35 @@ function historyTransactionTimestamp(record) {
transaction?.unsigned_issue_token?.timestamp,
transaction?.unsigned_issue_token?.time,
transaction?.unsigned_vanity_address?.timestamp,
transaction?.unsigned_vanity_address?.time
transaction?.unsigned_vanity_address?.time,
transaction?.unsigned_storagekey?.timestamp,
transaction?.unsigned_storagekey?.time,
transaction?.unsigned_storagebool?.timestamp,
transaction?.unsigned_storagebool?.time,
transaction?.unsigned_storageu8?.timestamp,
transaction?.unsigned_storageu8?.time,
transaction?.unsigned_storageu16?.timestamp,
transaction?.unsigned_storageu16?.time,
transaction?.unsigned_storageu32?.timestamp,
transaction?.unsigned_storageu32?.time,
transaction?.unsigned_storageu64?.timestamp,
transaction?.unsigned_storageu64?.time,
transaction?.unsigned_storageu128?.timestamp,
transaction?.unsigned_storageu128?.time,
transaction?.unsigned_storage?.timestamp,
transaction?.unsigned_storage?.time,
transaction?.unsigned_storagei8?.timestamp,
transaction?.unsigned_storagei8?.time,
transaction?.unsigned_storagei16?.timestamp,
transaction?.unsigned_storagei16?.time,
transaction?.unsigned_storagei32?.timestamp,
transaction?.unsigned_storagei32?.time,
transaction?.unsigned_storagei64?.timestamp,
transaction?.unsigned_storagei64?.time,
transaction?.unsigned_storagei128?.timestamp,
transaction?.unsigned_storagei128?.time,
transaction?.unsigned_deletekey?.timestamp,
transaction?.unsigned_deletekey?.time
];
const timestamp = candidates.map(Number).find((value) => Number.isFinite(value) && value > 0);
return timestamp || 0;

View File

@ -55,12 +55,11 @@ async function loadLoanPaymentContracts(force = false) {
if (loanPaymentLoadPromise) {
return loanPaymentLoadPromise;
}
const invoke = window.__TAURI__?.core?.invoke;
const broadcastNode = activeBroadcastNode();
if (!invoke || !broadcastNode || !walletLoaded || !registrationRegistered) {
if (!walletLoaded || !registrationRegistered) {
return;
}
const key = `${activeWalletAddressShort || activeWalletAddressHex}|${broadcastNode}`;
const walletToken = activeWalletCacheToken();
const key = `${walletToken}|local-active-loans`;
if (!force && loanPaymentContractsKey === key) {
populateLoanPaymentContracts();
return;
@ -68,7 +67,9 @@ async function loadLoanPaymentContracts(force = false) {
loanPaymentLoadPromise = (async () => {
setLoanPaymentMessage("Loading active loans...");
const contracts = await invoke("active_loan_contracts", { broadcastNode });
const contracts = typeof loadLocalActiveBorrowerLoanContracts === "function"
? await loadLocalActiveBorrowerLoanContracts(walletToken)
: [];
loanPaymentContracts = Array.isArray(contracts) ? contracts : [];
loanPaymentContractsKey = key;
populateLoanPaymentContracts();

View File

@ -74,6 +74,20 @@ function loanPaymentCount(contract) {
return Array.isArray(contract.payments) ? contract.payments.length : 0;
}
function loanPeriodLabel(period) {
const normalized = String(period || "").trim().toLowerCase();
if (normalized === "d" || normalized === "daily") {
return "daily";
}
if (normalized === "w" || normalized === "weekly") {
return "weekly";
}
if (normalized === "m" || normalized === "monthly") {
return "monthly";
}
return normalized || "unknown";
}
function loanAmountLabel(value, asset) {
return `${loanFormatAtomic(value)} ${String(asset || "").trim()}`.trim();
}
@ -146,6 +160,257 @@ function nextLoanPaymentDueLabel(contract) {
return formatLoanDueDate(loanDueDateForPayment(contract, completedPayments + 1));
}
function loanTransactionTime(transaction) {
return Number(
transaction?.unsigned_loan_contract?.time ??
transaction?.unsigned_loan_contract?.timestamp ??
transaction?.unsigned_contract_payment?.time ??
transaction?.unsigned_contract_payment?.timestamp ??
transaction?.unsigned_collateral_claim?.time ??
transaction?.unsigned_collateral_claim?.timestamp ??
0
) || 0;
}
function loanContractRecordFromCache(txid, record) {
const tx = record?.transaction?.unsigned_loan_contract;
if (!tx) {
return null;
}
const contractHash = txid || record?.txid || "";
const totalDue = BigInt(tx.payment_amount || 0) * BigInt(tx.payment_number || 0);
return {
contract: contractHash,
status: "active",
start_timestamp: Number(tx.time ?? tx.timestamp ?? 0) || 0,
lender: tx.lender || "",
borrower: tx.borrower || "",
loan_asset: tx.loan_coin || activeBaseCoin(),
loan_amount: String(tx.loan_amount || 0),
collateral: tx.collateral || activeBaseCoin(),
collateral_amount: String(tx.collateral_amount || 0),
payment_period: loanPeriodLabel(tx.payment_period),
payment_number: Number(tx.payment_number || 0),
payment_amount: String(tx.payment_amount || 0),
grace_period: Number(tx.grace_period || 0),
max_late_value: String(tx.max_late_value || 0),
confirmed_paid: "0",
pending_paid: "0",
remaining_balance: String(totalDue),
remaining_after_pending: String(totalDue),
payments: [],
collateral_claimed_by: ""
};
}
function loanPaymentsDueCount(contract, nowSeconds = Math.floor(Date.now() / 1000)) {
const startTimestamp = Number(contract?.start_timestamp || 0);
const paymentNumber = Number(contract?.payment_number || 0);
if (!startTimestamp || !paymentNumber || nowSeconds <= startTimestamp) {
return 0;
}
const period = String(contract.payment_period || "").trim().toLowerCase();
let due = 0;
if (period === "daily") {
due = Math.floor((nowSeconds - startTimestamp) / 86_400);
} else if (period === "weekly") {
due = Math.floor((nowSeconds - startTimestamp) / (7 * 86_400));
} else if (period === "monthly") {
const start = new Date(startTimestamp * 1000);
const now = new Date(nowSeconds * 1000);
due = (now.getUTCFullYear() - start.getUTCFullYear()) * 12
+ (now.getUTCMonth() - start.getUTCMonth());
const dueDate = addUtcMonths(startTimestamp, due);
if (dueDate && now.getTime() < dueDate.getTime()) {
due -= 1;
}
}
return Math.max(0, Math.min(paymentNumber, due));
}
function loanFinalGracePeriodPassed(contract, nowSeconds = Math.floor(Date.now() / 1000)) {
const paymentNumber = Number(contract?.payment_number || 0);
const gracePeriod = Number(contract?.grace_period || 0);
if (!paymentNumber) {
return false;
}
const dueDate = loanDueDateForPayment(contract, paymentNumber + gracePeriod);
if (!dueDate) {
return false;
}
return nowSeconds >= Math.floor(dueDate.getTime() / 1000) + 86_400;
}
function finalizeLocalLoanContract(contract) {
const totalPaid = (contract.payments || []).reduce(
(sum, payment) => sum + BigInt(payment.amount_atomic || payment.amount || 0),
0n
);
const paymentAmount = BigInt(contract.payment_amount || 0);
const totalDue = paymentAmount * BigInt(contract.payment_number || 0);
const remaining = totalDue > totalPaid ? totalDue - totalPaid : 0n;
const shouldHavePaid = BigInt(loanPaymentsDueCount(contract)) * paymentAmount;
const claimed = Boolean(contract.collateral_claimed_by);
contract.payments.sort((left, right) => Number(left.timestamp || 0) - Number(right.timestamp || 0));
contract.confirmed_paid = String(totalPaid);
contract.pending_paid = "0";
contract.remaining_balance = String(remaining);
contract.remaining_after_pending = String(remaining);
contract.status = claimed ? "inactive" : (shouldHavePaid > totalPaid ? "delinquent" : "active");
return contract;
}
async function applyRemoteCollateralStatuses(contracts) {
const invoke = window.__TAURI__?.core?.invoke;
const broadcastNode = typeof activeBroadcastNode === "function" ? activeBroadcastNode() : "";
if (!invoke || !broadcastNode || !Array.isArray(contracts) || !contracts.length) {
return contracts;
}
await Promise.all(contracts.map(async (contract) => {
if (!contract?.contract || contract.collateral_claimed_by) {
return;
}
try {
const status = await invoke("collateral_status", {
broadcastNode,
contractHash: contract.contract
});
if (status?.claimed) {
contract.collateral_claimed_by = "remote";
contract.status = "inactive";
}
} catch (_) {
// Keep the locally synced view usable if the lightweight status check is unavailable.
}
}));
return contracts;
}
async function loadLocalLoanContractsFromCache(walletToken = activeWalletCacheToken()) {
if (typeof readTransactionIndexObject !== "function" || typeof readTransactionCacheRecords !== "function") {
return [];
}
const index = await readTransactionIndexObject(walletToken);
let typeTxids = [7, 8, 9].flatMap((type) => Array.isArray(index.type_to_txids?.[String(type)])
? index.type_to_txids[String(type)]
: []);
if (!typeTxids.length) {
typeTxids = Array.isArray(index.ordered_txids) ? index.ordered_txids : [];
}
const uniqueTxids = Array.from(new Set(typeTxids.filter(Boolean)));
if (!uniqueTxids.length) {
return [];
}
const cache = await readTransactionCacheRecords(index, uniqueTxids, walletToken);
const contracts = new Map();
Object.entries(cache.transactions || {}).forEach(([txid, record]) => {
const contract = loanContractRecordFromCache(txid, record);
if (contract && (isActiveWalletAddress(contract.lender) || isActiveWalletAddress(contract.borrower))) {
contracts.set(contract.contract, contract);
}
});
Object.entries(cache.transactions || {}).forEach(([txid, record]) => {
const payment = record?.transaction?.unsigned_contract_payment;
if (payment?.contract_hash && contracts.has(payment.contract_hash)) {
const contract = contracts.get(payment.contract_hash);
contract.payments.push({
txid,
amount: String(payment.payback_amount || 0),
amount_atomic: String(payment.payback_amount || 0),
payer: payment.address || "",
timestamp: Number(payment.time ?? payment.timestamp ?? 0) || 0,
date: formatHistoryDate(loanTransactionTime(record.transaction))
});
}
const claim = record?.transaction?.unsigned_collateral_claim;
if (claim?.contract_hash && contracts.has(claim.contract_hash)) {
contracts.get(claim.contract_hash).collateral_claimed_by = claim.address || "";
}
});
return Array.from(contracts.values()).map(finalizeLocalLoanContract);
}
async function loadLocalActiveBorrowerLoanContracts(walletToken = activeWalletCacheToken()) {
const activeAddress = String(activeWalletAddressShort || "").trim().toLowerCase();
const contracts = await applyRemoteCollateralStatuses(await loadLocalLoanContractsFromCache(walletToken));
return contracts.filter((contract) => (
String(contract.borrower || "").trim().toLowerCase() === activeAddress &&
String(contract.status || "").toLowerCase() !== "inactive" &&
BigInt(contract.remaining_after_pending || 0) > 0n
));
}
function localCollateralClaimReason(contract) {
if (!contract || String(contract.status || "").toLowerCase() === "inactive") {
return "";
}
const role = activeLoanRole(contract);
const remainingBalance = BigInt(contract.remaining_balance || "0");
if (role === "borrower") {
return remainingBalance === 0n
? "Loan is fully repaid, so the borrower can reclaim collateral."
: "";
}
if (role !== "lender") {
return "";
}
if (remainingBalance > 0n && loanFinalGracePeriodPassed(contract)) {
return "The full loan term plus allowed grace has passed and an unpaid balance remains.";
}
const paymentsDue = loanPaymentsDueCount(contract);
const gracePeriod = Number(contract.grace_period || 0);
if (paymentsDue <= gracePeriod) {
return "";
}
const confirmedPaid = BigInt(contract.confirmed_paid || "0");
const paymentAmount = BigInt(contract.payment_amount || "0");
const maxLateValue = BigInt(contract.max_late_value || "0");
const shouldHavePaid = BigInt(paymentsDue) * paymentAmount;
const overdueValue = shouldHavePaid > confirmedPaid ? shouldHavePaid - confirmedPaid : 0n;
return overdueValue > maxLateValue
? `Borrower is late by ${overdueValue - maxLateValue} atomic units beyond the allowed threshold.`
: "";
}
async function loadLocalClaimableCollateralContracts(walletToken = activeWalletCacheToken()) {
const contracts = await applyRemoteCollateralStatuses(await loadLocalLoanContractsFromCache(walletToken));
return contracts
.map((contract) => {
const reason = localCollateralClaimReason(contract);
if (!reason) {
return null;
}
const role = activeLoanRole(contract);
return {
contract: contract.contract,
role,
reason,
lender: contract.lender,
borrower: contract.borrower,
loan_asset: contract.loan_asset,
loan_amount: contract.loan_amount,
collateral: contract.collateral,
collateral_amount: contract.collateral_amount,
confirmed_paid: contract.confirmed_paid,
pending_paid: contract.pending_paid,
remaining_balance: contract.remaining_balance,
remaining_after_pending: contract.remaining_after_pending,
fee: String(COLLATERAL_CLAIM_FEE_ATOMIC || 300000000n)
};
})
.filter(Boolean)
.sort((left, right) => left.contract.localeCompare(right.contract));
}
function sortedLoans() {
const query = String(loansSearch?.value || "").trim().toLowerCase();
const roleFilter = loansRoleFilter?.value || "all";
@ -327,8 +592,6 @@ function renderLoansPage() {
}
async function loadLoansPage({ force = false } = {}) {
const invoke = window.__TAURI__?.core?.invoke;
const broadcastNode = activeBroadcastNode();
if (!walletLoaded) {
loansContracts = [];
selectedLoanContractHash = "";
@ -336,21 +599,8 @@ async function loadLoansPage({ force = false } = {}) {
renderLoansPage();
return;
}
if (!invoke) {
loansContracts = [];
selectedLoanContractHash = "";
loansStateMessage = "Loan lookup skipped: Tauri invoke is unavailable.";
renderLoansPage();
return;
}
if (!broadcastNode) {
loansContracts = [];
selectedLoanContractHash = "";
loansStateMessage = "Loan lookup skipped: no broadcast node is selected.";
renderLoansPage();
return;
}
const key = `${activeWalletAddressShort || activeWalletAddressHex}|${broadcastNode}`;
const walletToken = activeWalletCacheToken();
const key = `${walletToken}|local-loans`;
if (!force && loansLoadedForKey === key) {
renderLoansPage();
return;
@ -363,21 +613,17 @@ async function loadLoansPage({ force = false } = {}) {
loansStateMessage = "Loading loans...";
renderLoansPage();
try {
const contracts = await invokeWalletRpcWithBackoff(
"loan contracts",
() => invoke("loan_contracts", { broadcastNode }),
() => currentView === "loans" && activeBroadcastNode() === broadcastNode
);
loansContracts = Array.isArray(contracts) ? contracts : [];
const contracts = await loadLocalLoanContractsFromCache(walletToken);
loansContracts = await applyRemoteCollateralStatuses(Array.isArray(contracts) ? contracts : []);
loansLoadedForKey = key;
selectedLoanContractHash = loansContracts[0]?.contract || "";
loansStateMessage = loansContracts.length
? ""
: "Loan contract command returned 0 contracts for this wallet.";
: "No locally synced loan contracts found for this wallet.";
} catch (error) {
loansContracts = [];
selectedLoanContractHash = "";
loansStateMessage = `Loan contract lookup failed: ${String(error)}`;
loansStateMessage = `Local loan cache lookup failed: ${String(error)}`;
} finally {
loansLoadInFlight = false;
renderLoansPage();

View File

@ -13,6 +13,9 @@
if (previousView === "nfts" && view !== "nfts" && typeof resetNftsPage === "function") {
resetNftsPage();
}
if (previousView === "addressdata" && view !== "addressdata" && typeof resetAddressDataSelection === "function") {
resetAddressDataSelection();
}
const [title, subtitle] = titles[view] || titles.dashboard;
currentView = view;
@ -42,6 +45,7 @@
nftsView.classList.add("hidden");
loansView.classList.add("hidden");
marketingView.classList.add("hidden");
addressDataView.classList.add("hidden");
addressbookView.classList.add("hidden");
settingsView.classList.add("hidden");
updateNetworkInfoRefreshState();
@ -67,6 +71,7 @@
nftsView.classList.add("hidden");
loansView.classList.add("hidden");
marketingView.classList.add("hidden");
addressDataView.classList.add("hidden");
addressbookView.classList.add("hidden");
settingsView.classList.add("hidden");
@ -105,6 +110,12 @@
prepareMarketingCampaignPage();
return;
}
if (view === "addressdata") {
addressDataView.classList.remove("hidden");
prepareAddressDataPage();
return;
}
if (view === "addressbook") {
addressbookView.classList.remove("hidden");
@ -193,6 +204,8 @@ function refreshActiveTransactionFormAfterWalletData() {
prepareCollateralClaimForm();
} else if (currentTransactionType === "Burn" && typeof prepareBurnForm === "function") {
prepareBurnForm();
} else if (currentTransactionType === "Data Storage" && typeof prepareDataStorageForm === "function") {
prepareDataStorageForm();
}
}
@ -212,6 +225,7 @@ function clearInactiveTransactionMessages(activeType) {
"Collateral Claim": collateralClaimMessage,
"Burn": burnMessage,
"Marketing": marketingMessage,
"Data Storage": dataStorageMessage,
"Vanity Address": vanityMessage
};
Object.entries(messageByType).forEach(([type, element]) => {
@ -244,6 +258,7 @@ function showTransactionForm(type) {
const isCollateralClaim = type === "Collateral Claim";
const isBurn = type === "Burn";
const isMarketing = type === "Marketing";
const isDataStorage = type === "Data Storage";
const isVanity = type === "Vanity Address";
document.querySelector("[data-transfer-form]")?.classList.toggle("hidden", !isTransfer);
transferActions?.classList.toggle("hidden", !isTransfer);
@ -276,6 +291,9 @@ function showTransactionForm(type) {
marketingForm?.classList.toggle("hidden", !isMarketing);
marketingActions?.classList.toggle("hidden", !isMarketing);
marketingMessage?.classList.toggle("hidden", !isMarketing);
dataStorageForm?.classList.toggle("hidden", !isDataStorage);
dataStorageActions?.classList.toggle("hidden", !isDataStorage);
dataStorageMessage?.classList.toggle("hidden", !isDataStorage);
vanityForm?.classList.toggle("hidden", !isVanity);
vanityActions?.classList.toggle("hidden", !isVanity);
vanityMessage?.classList.toggle("hidden", !isVanity);
@ -300,6 +318,8 @@ function showTransactionForm(type) {
prepareBurnForm();
} else if (isMarketing && typeof prepareMarketingForm === "function") {
prepareMarketingForm();
} else if (isDataStorage && typeof prepareDataStorageForm === "function") {
prepareDataStorageForm();
} else if (isVanity && typeof prepareVanityAddressForm === "function") {
prepareVanityAddressForm();
}

View File

@ -80,16 +80,20 @@ function scheduleClipboardClear(copiedText) {
}
lastClipboardText = copiedText || "";
const timeoutMs = durationSettingMs(guiRuntimeSettings.clipboard_clear_timeout);
if (!timeoutMs || !lastClipboardText || !navigator.clipboard) {
if (!timeoutMs || !lastClipboardText) {
return;
}
clipboardClearTimer = setTimeout(async () => {
lastClipboardText = "";
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke) {
return;
}
try {
await navigator.clipboard.writeText("");
await invoke("clear_system_clipboard");
} catch (_) {
// Clipboard writes can fail outside a user gesture; never prompt or block.
// Background clipboard clearing must never open a browser permission prompt.
}
}, timeoutMs);
}
@ -102,20 +106,11 @@ async function clearWalletClipboard() {
lastClipboardText = "";
const invoke = window.__TAURI__?.core?.invoke;
if (invoke) {
try {
await invoke("clear_system_clipboard");
return;
} catch (_) {
// Fall back to the Web Clipboard API when the native bridge is unavailable.
}
}
if (!navigator.clipboard) {
if (!invoke) {
return;
}
try {
await navigator.clipboard.writeText("");
await invoke("clear_system_clipboard");
} catch (_) {}
}
@ -311,11 +306,15 @@ async function clearSelectedWalletCache(kind, button) {
return;
}
const clearingHistory = kind === "history";
button && (button.disabled = true);
setCacheClearMessage("Clearing cache...");
try {
if (clearingHistory) {
await stopBackgroundTransactionSync();
}
await invoke("clear_wallet_cache", { cacheKind: kind });
if (kind === "history") {
if (clearingHistory) {
clearHistoryRows();
resetLatestDashboardTransactionsMemory();
if (typeof resetHistoryPage === "function") {
@ -340,6 +339,9 @@ async function clearSelectedWalletCache(kind, button) {
} catch (error) {
setCacheClearMessage(String(error), "error");
} finally {
if (clearingHistory) {
startBackgroundTransactionSync();
}
button && (button.disabled = false);
}
}

View File

@ -13,11 +13,25 @@
transaction.unsigned_contract_payment?.txtype ??
transaction.unsigned_collateral_claim?.txtype ??
transaction.unsigned_burn?.txtype ??
transaction.unsigned_issue_token?.txtype ??
transaction.unsigned_vanity_address?.txtype ??
0
);
}
transaction.unsigned_issue_token?.txtype ??
transaction.unsigned_vanity_address?.txtype ??
transaction.unsigned_storagekey?.txtype ??
transaction.unsigned_storagebool?.txtype ??
transaction.unsigned_storageu8?.txtype ??
transaction.unsigned_storageu16?.txtype ??
transaction.unsigned_storageu32?.txtype ??
transaction.unsigned_storageu64?.txtype ??
transaction.unsigned_storageu128?.txtype ??
transaction.unsigned_storage?.txtype ??
transaction.unsigned_storagei8?.txtype ??
transaction.unsigned_storagei16?.txtype ??
transaction.unsigned_storagei32?.txtype ??
transaction.unsigned_storagei64?.txtype ??
transaction.unsigned_storagei128?.txtype ??
transaction.unsigned_deletekey?.txtype ??
0
);
}
function txTypeKey(type) {
return {
@ -33,7 +47,21 @@ function txTypeKey(type) {
9: "collateral_claim",
10: "burn",
11: "token_issue",
12: "vanity"
12: "vanity",
100: "storage_key",
101: "storage_bool",
102: "storage_u8",
103: "storage_u16",
104: "storage_u32",
105: "storage_u64",
106: "storage_u128",
107: "storage_string",
108: "storage_i8",
109: "storage_i16",
110: "storage_i32",
111: "storage_i64",
112: "storage_i128",
113: "storage_delete"
}[Number(type)] || "transaction";
}
@ -74,6 +102,24 @@ function loanSummaryFromLinkedTransactions(linkedTransactions = {}, contractHash
return null;
}
function decodedStorageUnsigned(transaction) {
return transaction?.unsigned_storagekey
|| transaction?.unsigned_storagebool
|| transaction?.unsigned_storageu8
|| transaction?.unsigned_storageu16
|| transaction?.unsigned_storageu32
|| transaction?.unsigned_storageu64
|| transaction?.unsigned_storageu128
|| transaction?.unsigned_storage
|| transaction?.unsigned_storagei8
|| transaction?.unsigned_storagei16
|| transaction?.unsigned_storagei32
|| transaction?.unsigned_storagei64
|| transaction?.unsigned_storagei128
|| transaction?.unsigned_deletekey
|| null;
}
function parseDecodedDashboardTransactionRows(
txid,
blockHeight,
@ -224,7 +270,19 @@ function parseDecodedDashboardTransactionRows(
}
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(tx.address), signedAmount("+", tx.txfee || tx.fee || 0))];
}
} catch (_) {
if (type >= 100 && type <= 113) {
const tx = decodedStorageUnsigned(transaction);
if (!tx) {
return fallback;
}
if (isActiveWalletAddress(tx.address)) {
const counterparty = type === 100 ? "Self" : shortenAddress(tx.storagekey || "Storage");
return [dashboardRow(base, txTypeName(type), "Data Storage", counterparty, "", signedBaseFee("-", tx.txfee || 0))];
}
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(tx.address), signedAmount("+", tx.txfee || 0))];
}
} catch (_) {
return fallback;
}
@ -432,15 +490,25 @@ function parseDashboardTransactionRows(
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(creator), signedAmount("+", fee))];
}
if (type === 12) {
if (type === 12) {
const address = bytesToAddressHex(bytes.slice(5, 27));
const fee = readU64LE(bytes, 49);
if (isActiveAddress(address)) {
return [dashboardRow(base, "Vanity Registered", "Vanity", "Self", "", signedBaseFee("-", fee))];
}
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(address), signedAmount("+", fee))];
}
} catch (_) {
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(address), signedAmount("+", fee))];
}
if (type >= 100 && type <= 113) {
return parseDecodedDashboardTransactionRows(
txid,
blockHeight,
decodeLightweightTransaction(bytes),
linkedTransactions,
minerEarnings
);
}
} catch (_) {
return fallback;
}
@ -608,6 +676,106 @@ function decodeLightweightTransaction(bytesLike) {
}
};
}
if (type === 100) {
return {
unsigned_storagekey: {
txtype: type,
time: time(),
address: bytesToAddressHex(bytes.slice(5, 27)),
txfee: Number(readU64LE(bytes, 27))
}
};
}
const storageValue = (wrapper, value, valueEnd) => ({
[wrapper]: {
txtype: type,
time: time(),
storagekey: bytesToHex(bytes.slice(5, 37)),
key: bytesToText(bytes.slice(37, 87)),
value,
address: bytesToAddressHex(bytes.slice(valueEnd, valueEnd + 22)),
txfee: Number(readU64LE(bytes, valueEnd + 22))
}
});
const signedStorageValue = (wrapper, byteLength) => {
let value = 0n;
for (let index = byteLength - 1; index >= 0; index -= 1) {
value = (value << 8n) + BigInt(Number(bytes[87 + index] || 0));
}
const bitLength = BigInt(byteLength * 8);
const signBit = 1n << (bitLength - 1n);
if ((value & signBit) !== 0n) {
value -= 1n << bitLength;
}
return storageValue(wrapper, value.toString(), 87 + byteLength);
};
if (type === 101) {
return storageValue("unsigned_storagebool", Boolean(bytes[87]), 88);
}
if (type === 102) {
return storageValue("unsigned_storageu8", Number(bytes[87] || 0), 88);
}
if (type === 103) {
const value = Number(bytes[87] || 0) | (Number(bytes[88] || 0) << 8);
return storageValue("unsigned_storageu16", value, 89);
}
if (type === 104) {
return storageValue("unsigned_storageu32", readU32LE(bytes, 87), 91);
}
if (type === 105) {
return storageValue("unsigned_storageu64", readU64LE(bytes, 87).toString(), 95);
}
if (type === 106) {
let value = 0n;
for (let index = 15; index >= 0; index -= 1) {
value = (value << 8n) + BigInt(Number(bytes[87 + index] || 0));
}
return storageValue("unsigned_storageu128", value.toString(), 103);
}
if (type === 107) {
return {
unsigned_storage: {
txtype: type,
time: time(),
storagekey: bytesToHex(bytes.slice(5, 37)),
key: bytesToText(bytes.slice(37, 87)),
value: bytesToText(bytes.slice(87, 267)),
address: bytesToAddressHex(bytes.slice(267, 289)),
previous: bytesToHex(bytes.slice(289, 321)),
txfee: Number(readU64LE(bytes, 321))
}
};
}
if (type === 108) {
return signedStorageValue("unsigned_storagei8", 1);
}
if (type === 109) {
return signedStorageValue("unsigned_storagei16", 2);
}
if (type === 110) {
return signedStorageValue("unsigned_storagei32", 4);
}
if (type === 111) {
return signedStorageValue("unsigned_storagei64", 8);
}
if (type === 112) {
return signedStorageValue("unsigned_storagei128", 16);
}
if (type === 113) {
return {
unsigned_deletekey: {
txtype: type,
time: time(),
storagekey: bytesToHex(bytes.slice(5, 37)),
key: bytesToText(bytes.slice(37, 87)),
address: bytesToAddressHex(bytes.slice(87, 109)),
txfee: Number(readU64LE(bytes, 109))
}
};
}
return { unknown: { txtype: type } };
}
@ -1146,10 +1314,12 @@ async function writeTransactionRecordFile(fileName, record, walletToken = active
return;
}
await invoke("write_local_transaction_record", {
fileName,
recordJson: JSON.stringify(record)
});
const recordJson = JSON.stringify(record);
if (!fileName || typeof recordJson !== "string") {
throw new Error("Unable to write an empty local transaction record.");
}
await invoke("write_local_transaction_record", { fileName, recordJson });
}
async function mutateTransactionCache(mutator, walletToken = activeWalletCacheToken()) {
@ -1421,8 +1591,10 @@ function shouldContinueBackgroundTransactionSync(syncId, walletToken = activeWal
);
}
function stopBackgroundTransactionSync() {
backgroundTransactionSyncId += 1;
async function stopBackgroundTransactionSync() {
const runningSync = backgroundTransactionSyncPromise;
backgroundTransactionSyncId += 1;
await runningSync;
}
function startBackgroundTransactionSync() {
@ -1432,7 +1604,11 @@ function startBackgroundTransactionSync() {
const walletToken = activeWalletCacheToken();
const syncId = ++backgroundTransactionSyncId;
runBackgroundTransactionSync(syncId, 0, walletToken);
const currentSync = runBackgroundTransactionSync(syncId, 0, walletToken);
backgroundTransactionSyncPromise = Promise.allSettled([
backgroundTransactionSyncPromise,
currentSync
]).then(() => undefined);
}
async function runBackgroundTransactionSync(syncId, failureCount = 0, walletToken = activeWalletCacheToken()) {
@ -1519,8 +1695,7 @@ async function runBackgroundTransactionSync(syncId, failureCount = 0, walletToke
const backoff = Math.min(BACKGROUND_SYNC_IDLE_MS * 2 ** failureCount, 60000);
await sleepMs(backoff);
if (shouldContinueBackgroundTransactionSync(syncId, walletToken)) {
runBackgroundTransactionSync(syncId, failureCount + 1, walletToken);
return;
return runBackgroundTransactionSync(syncId, failureCount + 1, walletToken);
}
}
}

View File

@ -71,6 +71,9 @@ function updateActiveWallet(wallet) {
if (typeof resetAddressBookPage === "function") {
resetAddressBookPage();
}
if (typeof resetAddressDataPage === "function") {
resetAddressDataPage();
}
if (typeof resetAssetsPage === "function") {
resetAssetsPage();
}
@ -181,6 +184,9 @@ function clearActiveWallet() {
if (typeof resetMarketingSession === "function") {
resetMarketingSession();
}
if (typeof resetDataStorageSession === "function") {
resetDataStorageSession();
}
if (typeof resetVanityAddressSession === "function") {
resetVanityAddressSession();
}
@ -190,6 +196,9 @@ function clearActiveWallet() {
if (typeof resetAddressBookPage === "function") {
resetAddressBookPage();
}
if (typeof resetAddressDataPage === "function") {
resetAddressDataPage();
}
if (typeof resetAssetsPage === "function") {
resetAssetsPage();
}

View File

@ -226,6 +226,7 @@ body.wallet-open .startup-screen {
display: flex;
flex-direction: column;
gap: 22px;
overflow-y: auto;
}
.brand {
@ -1546,7 +1547,7 @@ th {
min-height: 552px;
height: calc(100vh - 142px);
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto auto;
grid-template-rows: auto minmax(0, 1fr) auto auto auto;
overflow: hidden;
}
@ -1595,7 +1596,7 @@ th {
}
.marketing-workspace {
grid-template-rows: auto auto minmax(0, 1fr) auto auto;
grid-template-rows: auto auto minmax(0, 1fr) auto auto auto;
}
.marketing-summary-grid {
@ -1605,18 +1606,9 @@ th {
margin: 14px 0 12px;
}
.history-workspace.panel {
display: grid;
grid-template-rows:
auto /* header */
auto /* summary */
minmax(0,1fr) /* table */
auto; /* footer */
}
.marketing-table-wrap {
min-height: 0;
overflow: auto;
min-height: 0;
overflow: auto;
}
.marketing-summary-grid > div {
@ -2606,6 +2598,174 @@ th {
text-align: right;
}
.address-data-workspace {
min-height: calc(100vh - 100px);
}
.address-data-header {
align-items: end;
}
.address-data-actions {
display: grid;
grid-template-columns: minmax(280px, 420px) auto;
gap: 10px;
}
.address-data-actions select {
width: 100%;
min-height: 42px;
padding: 9px 11px;
border: 1px solid var(--line);
border-radius: 8px;
background: #121b24;
color: var(--text);
}
.address-data-groups {
display: grid;
margin-top: 14px;
}
.address-data-selection {
display: grid;
grid-template-columns: minmax(280px, 420px) minmax(0, 1fr);
gap: 20px;
align-items: start;
margin-top: 18px;
padding: 14px 0 12px;
border-top: 1px solid var(--line);
}
.address-data-selected-id {
min-width: 0;
display: grid;
gap: 7px;
}
.address-data-selected-id code {
overflow-wrap: anywhere;
color: var(--accent-soft);
font-size: 13px;
line-height: 1.45;
}
.address-data-label-field {
display: grid;
grid-template-columns: 1fr auto;
gap: 7px 10px;
color: var(--muted);
font-size: 12px;
font-weight: 700;
}
.address-data-label-field input {
grid-column: 1 / -1;
min-height: 40px;
padding: 9px 11px;
border: 1px solid var(--line);
border-radius: 8px;
background: #121b24;
color: var(--text);
}
.address-data-label-field small {
min-height: 15px;
color: var(--success);
font-weight: 600;
text-align: right;
}
.address-data-label-field small.error {
color: var(--danger);
}
.address-data-group {
display: grid;
padding: 0;
border-top: 1px solid var(--line);
}
.address-data-fields {
display: grid;
}
.address-data-field {
display: grid;
grid-template-columns: minmax(180px, 0.8fr) minmax(240px, 2fr) minmax(90px, 0.45fr) 72px;
gap: 18px;
align-items: start;
padding: 13px 0;
border-top: 1px solid color-mix(in srgb, var(--line) 65%, transparent);
}
.address-data-field-key {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.address-data-field-header {
background: var(--bg);
color: var(--muted);
font-size: 12px;
text-transform: uppercase;
}
.address-data-field-key strong,
.address-data-field-value,
.address-data-field-type {
overflow-wrap: anywhere;
}
.address-data-field-value {
white-space: pre-wrap;
line-height: 1.45;
}
.address-data-field-action {
display: flex;
justify-content: flex-start;
}
.address-data-delete-button {
min-height: 30px;
padding: 4px 10px;
border: 1px solid rgba(239, 111, 111, 0.55);
border-radius: 6px;
background: rgba(130, 28, 28, 0.22);
color: #ff9a9a;
font: inherit;
font-weight: 700;
cursor: pointer;
}
.address-data-delete-button:hover,
.address-data-delete-button:focus-visible {
background: rgba(164, 38, 38, 0.34);
border-color: #ff9a9a;
}
.address-data-delete-button:disabled {
cursor: wait;
opacity: 0.55;
}
@media (max-width: 1180px) and (min-width: 981px) {
.address-data-actions {
grid-template-columns: minmax(210px, 1fr) auto;
}
.address-data-field {
grid-template-columns: minmax(130px, 0.7fr) minmax(180px, 1.5fr) minmax(76px, 0.4fr) 72px;
}
.address-data-selection {
grid-template-columns: minmax(220px, 320px) minmax(0, 1fr);
}
}
.addressbook-table td:nth-child(3) {
max-width: 360px;
}
@ -3040,6 +3200,7 @@ tr:hover td {
min-height: auto;
height: auto;
max-height: none;
grid-template-rows: none;
}
.marketing-campaign-controls {
@ -3072,6 +3233,21 @@ tr:hover td {
max-height: none;
}
.address-data-header,
.address-data-actions {
display: grid;
grid-template-columns: minmax(0, 1fr);
width: 100%;
}
.address-data-actions .secondary-button {
width: 100%;
}
.address-data-selection {
grid-template-columns: minmax(0, 1fr);
}
.settings-workspace {
min-height: auto;
height: auto;

View File

@ -2,46 +2,57 @@
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use blockchain::blocks::burn::{BurnTransaction, UnsignedBurnTransaction};
use blockchain::blocks::collateral::{
use contractless::blocks::burn::{BurnTransaction, UnsignedBurnTransaction};
use contractless::blocks::collateral::{
CollateralClaimTransaction, UnsignedCollateralClaimTransaction,
};
use blockchain::blocks::genesis::GenesisTransaction;
use blockchain::blocks::issue_token::{IssueTokenTransaction, UnsignedIssueTokenTransaction};
use blockchain::blocks::loan_payment::{
use contractless::blocks::delete_key::{DeleteKey, UnsignedDeleteKey};
use contractless::blocks::genesis::GenesisTransaction;
use contractless::blocks::issue_token::{IssueTokenTransaction, UnsignedIssueTokenTransaction};
use contractless::blocks::loan_payment::{
ContractPaymentTransaction, UnsignedContractPaymentTransaction,
};
use blockchain::blocks::loans::{LoanContractTransaction, UnsignedLoanContractTransaction};
use blockchain::blocks::marketing::{MarketingTransaction, UnsignedMarketingTransaction};
use blockchain::blocks::nft::{CreateNftTransaction, UnsignedCreateNftTransaction};
use blockchain::blocks::rewards::RewardsTransaction;
use blockchain::blocks::swap::{SwapTransaction, UnsignedSwapTransaction};
use blockchain::blocks::token::{CreateTokenTransaction, UnsignedCreateTokenTransaction};
use blockchain::blocks::transfer::{TransferTransaction, UnsignedTransferTransaction};
use blockchain::blocks::vanity::{UnsignedVanityAddressTransaction, VanityAddressTransaction};
use blockchain::common::skein::skein_256_hash_bytes;
use blockchain::common::types::{
minimum_transfer_fee, BORROWER_FEE, BURN_FEE, COLLATERAL_FEE, CREATE_NFT_FEE, CREATE_TOKEN_FEE,
ISSUE_TOKEN_FEE, LENDER_FEE, MARKETING_FEE, SWAP_FEE, VANITY_ADDRESS_FEE,
use contractless::blocks::loans::{LoanContractTransaction, UnsignedLoanContractTransaction};
use contractless::blocks::marketing::{MarketingTransaction, UnsignedMarketingTransaction};
use contractless::blocks::nft::{CreateNftTransaction, UnsignedCreateNftTransaction};
use contractless::blocks::rewards::RewardsTransaction;
use contractless::blocks::storage_key::{StorageKey, UnsignedStorageKey};
use contractless::blocks::storage_values::{
StorageBool, StorageI128, StorageI16, StorageI32, StorageI64, StorageI8, StorageString,
StorageU128, StorageU16, StorageU32, StorageU64, StorageU8, UnsignedStorageBool,
UnsignedStorageI128, UnsignedStorageI16, UnsignedStorageI32, UnsignedStorageI64,
UnsignedStorageI8, UnsignedStorageString, UnsignedStorageU128, UnsignedStorageU16,
UnsignedStorageU32, UnsignedStorageU64, UnsignedStorageU8, STORAGE_KEY_HASH_BYTES,
STORAGE_STRING_VALUE_BYTES, STORAGE_VALUE_KEY_BYTES,
};
use blockchain::records::memory::mempool::BASECOIN;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::rpc::command_maps::{
RPC_ADDRESS_HISTORY, RPC_ADDRESS_TOTAL_BALANCE, RPC_BLOCK_HEIGHT, RPC_CONTRACT_BY_ADDRESS,
RPC_LATEST_ADDRESS_TRANSACTIONS, RPC_LOAN_CONTRACT, RPC_MARKETING_CAMPAIGN_HISTORY,
RPC_NETWORK_INFO, RPC_NFT_DETAILS, RPC_NFT_LIST, RPC_REGISTER_WALLET, RPC_TIME,
RPC_TOKEN_CATALOG, RPC_TOKEN_DETAILS, RPC_TOKEN_LIST, RPC_TRANSACTION_BY_TXID,
RPC_VALIDATE_MESSAGE, RPC_VANITY_LOOKUP, RPC_VANITY_OWNER_LOOKUP,
use contractless::blocks::swap::{SwapTransaction, UnsignedSwapTransaction};
use contractless::blocks::token::{CreateTokenTransaction, UnsignedCreateTokenTransaction};
use contractless::blocks::transfer::{TransferTransaction, UnsignedTransferTransaction};
use contractless::blocks::vanity::{UnsignedVanityAddressTransaction, VanityAddressTransaction};
use contractless::common::skein::skein_256_hash_bytes;
use contractless::common::types::{
minimum_transfer_fee, BORROWER_FEE, BURN_FEE, COLLATERAL_FEE, CREATE_NFT_FEE, CREATE_TOKEN_FEE,
DELETE_KEY_TYPE, ISSUE_TOKEN_FEE, LENDER_FEE, MARKETING_FEE, STORAGE_KEY_FEE,
STORAGE_VALUE_FEE, SWAP_FEE, VANITY_ADDRESS_FEE,
};
use contractless::records::memory::mempool::BASECOIN;
use contractless::records::memory::response_channels::generate_uid;
use contractless::rpc::command_maps::{
RPC_ADDRESS_HISTORY, RPC_ADDRESS_TOTAL_BALANCE, RPC_BLOCK_HEIGHT, RPC_COLLATERAL_STATUS,
RPC_CONTRACT_BY_ADDRESS, RPC_LATEST_ADDRESS_TRANSACTIONS, RPC_LOAN_CONTRACT,
RPC_MARKETING_CAMPAIGN_HISTORY, RPC_NETWORK_INFO, RPC_NFT_DETAILS, RPC_NFT_LIST,
RPC_REGISTER_WALLET, RPC_TIME, RPC_TOKEN_CATALOG, RPC_TOKEN_DETAILS, RPC_TOKEN_LIST,
RPC_TRANSACTION_BY_TXID, RPC_VALIDATE_MESSAGE, RPC_VANITY_LOOKUP, RPC_VANITY_OWNER_LOOKUP,
RPC_WALLET_REGISTRATION_STATUS,
};
use blockchain::standalone_tools::connections::handshake::{self, HandshakeWallet};
use blockchain::tokio::fs::{
use contractless::standalone_tools::connections::handshake::{self, HandshakeWallet};
use contractless::tokio::fs::{
create_dir_all, metadata, read, read_dir, read_to_string, remove_dir_all, remove_file,
};
use blockchain::tokio::sync::{oneshot, Mutex as AsyncMutex};
use blockchain::tokio::time::sleep;
use blockchain::wallets::structures::{SavedWallet, Wallet};
use blockchain::{
use contractless::tokio::sync::{oneshot, Mutex as AsyncMutex};
use contractless::tokio::time::sleep;
use contractless::wallets::structures::{SavedWallet, Wallet};
use contractless::{
create_img, decode_image_and_extract_text, decrypts, encode, encrypts, to_string_pretty,
};
use native_dialog::FileDialog;
@ -183,6 +194,8 @@ fn main() {
clear_wallet_cache,
read_address_book,
write_address_book,
read_storage_labels,
write_storage_labels,
lookup_address_vanity,
check_vanity_address_availability,
save_history_export,
@ -199,12 +212,16 @@ fn main() {
broadcast_burn_transaction,
save_marketing_transaction,
broadcast_marketing_transaction,
save_data_storage_transaction,
broadcast_data_storage_transaction,
broadcast_delete_storage_transaction,
save_vanity_address_transaction,
broadcast_vanity_address_transaction,
save_swap_offer,
save_loan_offer,
active_loan_contracts,
loan_contracts,
collateral_status,
save_loan_payment_transaction,
broadcast_loan_payment_transaction,
claimable_collateral_contracts,

View File

@ -1,4 +1,4 @@
#[tauri::command]
#[tauri::command]
async fn current_block_height(
state: State<'_, WalletState>,
broadcast_node: String,
@ -109,7 +109,7 @@ async fn write_local_transaction_cache(
let normalized = serde_json::to_string_pretty(&parsed)
.map_err(|err| format!("Unable to serialize transaction cache: {err}"))?;
blockchain::tokio::fs::write(&cache_path, normalized)
contractless::tokio::fs::write(&cache_path, normalized)
.await
.map_err(|err| format!("Unable to write local transaction cache: {err}"))
}
@ -140,7 +140,7 @@ async fn write_local_transaction_index(
let normalized = serde_json::to_string_pretty(&parsed)
.map_err(|err| format!("Unable to serialize transaction index: {err}"))?;
blockchain::tokio::fs::write(history_dir.join("index.json"), normalized)
contractless::tokio::fs::write(history_dir.join("index.json"), normalized)
.await
.map_err(|err| format!("Unable to write local transaction index: {err}"))
}
@ -176,7 +176,7 @@ async fn write_local_transaction_record(
let normalized = serde_json::to_string_pretty(&parsed)
.map_err(|err| format!("Unable to serialize transaction record: {err}"))?;
blockchain::tokio::fs::write(history_dir.join(file_name), normalized)
contractless::tokio::fs::write(history_dir.join(file_name), normalized)
.await
.map_err(|err| format!("Unable to write local transaction record: {err}"))
}
@ -293,7 +293,7 @@ async fn write_nft_cache(
.and_then(|header| header.split_once(';').map(|(mime, _)| mime.to_string()));
if let Ok(bytes) = base64::Engine::decode(&STANDARD, encoded) {
if bytes.len() <= 10_000_000 {
blockchain::tokio::fs::write(record_dir.join("image.bin"), bytes)
contractless::tokio::fs::write(record_dir.join("image.bin"), bytes)
.await
.map_err(|err| format!("Unable to write NFT cached image: {err}"))?;
image_cached = true;
@ -322,7 +322,7 @@ async fn write_nft_cache(
.await
.map_err(|err| format!("Failed to read NFT image response: {err}"))?;
if bytes.len() <= 10_000_000 {
blockchain::tokio::fs::write(record_dir.join("image.bin"), &bytes)
contractless::tokio::fs::write(record_dir.join("image.bin"), &bytes)
.await
.map_err(|err| format!("Unable to write NFT cached image: {err}"))?;
image_cached = true;
@ -338,7 +338,7 @@ async fn write_nft_cache(
});
let normalized = serde_json::to_string_pretty(&record)
.map_err(|err| format!("Unable to serialize NFT cache record: {err}"))?;
blockchain::tokio::fs::write(record_dir.join("record.json"), normalized)
contractless::tokio::fs::write(record_dir.join("record.json"), normalized)
.await
.map_err(|err| format!("Unable to write NFT cache record: {err}"))?;
@ -373,7 +373,7 @@ async fn prune_nft_cache(
}
let key = entry.file_name().to_string_lossy().to_ascii_lowercase();
if !keep.contains(&key) {
blockchain::tokio::fs::remove_dir_all(entry.path())
contractless::tokio::fs::remove_dir_all(entry.path())
.await
.map_err(|err| format!("Unable to remove stale NFT cache entry: {err}"))?;
}
@ -460,11 +460,40 @@ async fn write_address_book(
let normalized = serde_json::to_string_pretty(&parsed)
.map_err(|err| format!("Unable to serialize address book: {err}"))?;
blockchain::tokio::fs::write(&address_book_path, normalized)
contractless::tokio::fs::write(&address_book_path, normalized)
.await
.map_err(|err| format!("Unable to write address book: {err}"))
}
#[tauri::command]
async fn read_storage_labels(state: State<'_, WalletState>) -> Result<String, String> {
let labels_path = active_wallet_storage_labels_path(&state)?;
match read(&labels_path).await {
Ok(bytes) => String::from_utf8(bytes)
.map_err(|_| "Storage labels file is not valid UTF-8.".to_string()),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
Ok("{\"labels\":{}}".to_string())
}
Err(err) => Err(format!("Unable to read storage labels: {err}")),
}
}
#[tauri::command]
async fn write_storage_labels(
state: State<'_, WalletState>,
storage_labels_json: String,
) -> Result<(), String> {
let labels_path = active_wallet_storage_labels_path(&state)?;
let parsed: JsonValue = serde_json::from_str(&storage_labels_json)
.map_err(|err| format!("Storage labels must be valid JSON: {err}"))?;
let normalized = serde_json::to_string_pretty(&parsed)
.map_err(|err| format!("Unable to serialize storage labels: {err}"))?;
contractless::tokio::fs::write(&labels_path, normalized)
.await
.map_err(|err| format!("Unable to write storage labels: {err}"))
}
#[tauri::command]
async fn lookup_address_vanity(
state: State<'_, WalletState>,
@ -595,7 +624,7 @@ async fn save_history_export(
path.set_extension(extension);
}
blockchain::tokio::fs::write(&path, contents)
contractless::tokio::fs::write(&path, contents)
.await
.map_err(|err| format!("Unable to save history export: {err}"))?;

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
#[tauri::command]
#[tauri::command]
fn get_app_status() -> AppStatus {
AppStatus {
app_name: "Contractless Wallet",
@ -342,7 +342,7 @@ async fn backup_active_wallet_image(state: State<'_, WalletState>) -> Result<Opt
path.set_extension("png");
}
blockchain::tokio::fs::write(&path, image_bytes)
contractless::tokio::fs::write(&path, image_bytes)
.await
.map_err(|err| format!("Unable to save wallet image backup: {err}"))?;
Ok(Some(path.to_string_lossy().into_owned()))
@ -377,7 +377,7 @@ async fn backup_active_wallet_private_key(
path.set_extension("txt");
}
blockchain::tokio::fs::write(&path, contents)
contractless::tokio::fs::write(&path, contents)
.await
.map_err(|err| format!("Unable to save private key backup: {err}"))?;
Ok(Some(path.to_string_lossy().into_owned()))
@ -693,7 +693,7 @@ async fn save_gui_settings(settings: GuiSettingsView) -> Result<(), String> {
let updated = replace_ini_section(&updated, "Piggyback", &piggyback_entries);
let updated = replace_ini_section(&updated, "GUI", &gui_entries);
blockchain::tokio::fs::write(&settings_path, updated)
contractless::tokio::fs::write(&settings_path, updated)
.await
.map_err(|err| format!("Unable to write GUI settings.ini: {err}"))
}

View File

@ -153,6 +153,23 @@ fn active_wallet_address_book_path(state: &State<'_, WalletState>) -> Result<Pat
Ok(wallet_path.with_file_name("address_book.json"))
}
fn active_wallet_storage_labels_path(state: &State<'_, WalletState>) -> Result<PathBuf, String> {
let guard = state
.active_wallet
.lock()
.map_err(|_| "Wallet session lock was poisoned.".to_string())?;
let active_wallet = guard
.as_ref()
.ok_or_else(|| "No wallet is loaded.".to_string())?;
let wallet_path = PathBuf::from(&active_wallet.wallet_path);
let short_address_bytes =
Wallet::short_address_to_bytes(&active_wallet.wallet.saved.short_address)
.ok_or_else(|| "Active wallet short address is invalid.".to_string())?;
let file_name = format!("{}.storage_labels.json", encode(short_address_bytes));
Ok(wallet_path.with_file_name(file_name))
}
fn validate_transaction_record_file(file_name: &str) -> Result<String, String> {
let trimmed = file_name.trim();
if trimmed.len() != 13 || !trimmed.ends_with(".json") {

View File

@ -64,13 +64,107 @@
.await
.map_err(|err| format!("Failed to decode token issue transaction: {err}"))?,
),
12 => serde_json::to_value(
VanityAddressTransaction::from_bytes(txtype, body)
.await
.map_err(|err| format!("Failed to decode vanity transaction: {err}"))?,
),
_ => return Err(format!("Unknown transaction type: {txtype}")),
}
12 => serde_json::to_value(
VanityAddressTransaction::from_bytes(txtype, body)
.await
.map_err(|err| format!("Failed to decode vanity transaction: {err}"))?,
),
100 => serde_json::to_value(
StorageKey::from_bytes(txtype, body)
.await
.map_err(|err| format!("Failed to decode storage key transaction: {err}"))?,
),
101 => serde_json::to_value(
StorageBool::from_bytes(txtype, body)
.await
.map_err(|err| format!("Failed to decode storage bool transaction: {err}"))?,
),
102 => serde_json::to_value(
StorageU8::from_bytes(txtype, body)
.await
.map_err(|err| format!("Failed to decode storage u8 transaction: {err}"))?,
),
103 => serde_json::to_value(
StorageU16::from_bytes(txtype, body)
.await
.map_err(|err| format!("Failed to decode storage u16 transaction: {err}"))?,
),
104 => serde_json::to_value(
StorageU32::from_bytes(txtype, body)
.await
.map_err(|err| format!("Failed to decode storage u32 transaction: {err}"))?,
),
105 => serde_json::to_value(
StorageU64::from_bytes(txtype, body)
.await
.map_err(|err| format!("Failed to decode storage u64 transaction: {err}"))?,
),
106 => {
let tx = StorageU128::from_bytes(txtype, body)
.await
.map_err(|err| format!("Failed to decode storage u128 transaction: {err}"))?;
Ok(serde_json::json!({
"unsigned_storageu128": {
"txtype": tx.unsigned_storageu128.txtype,
"time": tx.unsigned_storageu128.time,
"storagekey": tx.unsigned_storageu128.storagekey,
"key": tx.unsigned_storageu128.key,
"value": tx.unsigned_storageu128.value.to_string(),
"address": tx.unsigned_storageu128.address,
"txfee": tx.unsigned_storageu128.txfee,
},
"signature": tx.signature,
}))
}
107 => serde_json::to_value(
StorageString::from_bytes(txtype, body)
.await
.map_err(|err| format!("Failed to decode storage string transaction: {err}"))?,
),
108 => serde_json::to_value(
StorageI8::from_bytes(txtype, body)
.await
.map_err(|err| format!("Failed to decode storage i8 transaction: {err}"))?,
),
109 => serde_json::to_value(
StorageI16::from_bytes(txtype, body)
.await
.map_err(|err| format!("Failed to decode storage i16 transaction: {err}"))?,
),
110 => serde_json::to_value(
StorageI32::from_bytes(txtype, body)
.await
.map_err(|err| format!("Failed to decode storage i32 transaction: {err}"))?,
),
111 => serde_json::to_value(
StorageI64::from_bytes(txtype, body)
.await
.map_err(|err| format!("Failed to decode storage i64 transaction: {err}"))?,
),
112 => {
let tx = StorageI128::from_bytes(txtype, body)
.await
.map_err(|err| format!("Failed to decode storage i128 transaction: {err}"))?;
Ok(serde_json::json!({
"unsigned_storagei128": {
"txtype": tx.unsigned_storagei128.txtype,
"time": tx.unsigned_storagei128.time,
"storagekey": tx.unsigned_storagei128.storagekey,
"key": tx.unsigned_storagei128.key,
"value": tx.unsigned_storagei128.value.to_string(),
"address": tx.unsigned_storagei128.address,
"txfee": tx.unsigned_storagei128.txfee,
},
"signature": tx.signature,
}))
}
113 => serde_json::to_value(
DeleteKey::from_bytes(txtype, body)
.await
.map_err(|err| format!("Failed to decode storage delete transaction: {err}"))?,
),
_ => return Err(format!("Unknown transaction type: {txtype}")),
}
.map_err(|err| format!("Failed to serialize decoded transaction: {err}"))
}

View File

@ -299,6 +299,11 @@ struct CollateralClaimContractView {
fee: String,
}
#[derive(Serialize)]
struct CollateralStatusView {
claimed: bool,
}
#[derive(Serialize)]
struct BroadcastUnbroadcastResult {
response: String,

View File

@ -1,6 +1,6 @@
async fn saved_private_key_image(wallet_path: &str) -> Option<String> {
async fn saved_private_key_image(wallet_path: &str) -> Option<String> {
let bytes = read(wallet_path).await.ok()?;
let saved_wallet: SavedWallet = blockchain::from_slice(&bytes).ok()?;
let saved_wallet: SavedWallet = contractless::from_slice(&bytes).ok()?;
if saved_wallet.private_key.trim().is_empty() {
None
} else {
@ -103,7 +103,7 @@ async fn import_private_key_file(
};
let output = to_string_pretty(&saved_wallet)
.map_err(|err| format!("Failed to serialize imported wallet: {err}"))?;
blockchain::tokio::fs::write(wallet_path, output)
contractless::tokio::fs::write(wallet_path, output)
.await
.map_err(|err| format!("Failed to write imported wallet file: {err}"))?;
@ -111,7 +111,7 @@ async fn import_private_key_file(
}
async fn private_key_from_png_image(image_path: &Path, wallet_key: &str) -> Result<String, String> {
let image_bytes = blockchain::tokio::fs::read(image_path)
let image_bytes = contractless::tokio::fs::read(image_path)
.await
.map_err(|err| format!("Failed to read private key image: {err}"))?;
let image_base64 = STANDARD.encode(image_bytes);
@ -250,7 +250,7 @@ dbname = contractless_gui
public_ip = public_ip
);
blockchain::tokio::fs::write(&settings_path, settings)
contractless::tokio::fs::write(&settings_path, settings)
.await
.map_err(|err| format!("Unable to write GUI settings.ini: {err}"))?;
env::set_var("SETTINGS_PATH", &settings_path);