added features and fixed cache reset buggs
This commit is contained in:
parent
ad64c14a04
commit
18bedf64b0
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
79
src/main.rs
79
src/main.rs
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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}"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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}"))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -299,6 +299,11 @@ struct CollateralClaimContractView {
|
|||
fee: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CollateralStatusView {
|
||||
claimed: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct BroadcastUnbroadcastResult {
|
||||
response: String,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue