added features and fixed cache reset buggs
This commit is contained in:
parent
ad64c14a04
commit
18bedf64b0
|
|
@ -202,44 +202,6 @@ dependencies = [
|
||||||
"objc2",
|
"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]]
|
[[package]]
|
||||||
name = "brotli"
|
name = "brotli"
|
||||||
version = "8.0.3"
|
version = "8.0.3"
|
||||||
|
|
@ -558,13 +520,51 @@ version = "0.4.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3"
|
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]]
|
[[package]]
|
||||||
name = "contractless-wallet"
|
name = "contractless-wallet"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"blockchain",
|
|
||||||
"clipboard-win",
|
"clipboard-win",
|
||||||
|
"contractless",
|
||||||
"native-dialog",
|
"native-dialog",
|
||||||
"reqwest 0.12.28",
|
"reqwest 0.12.28",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -1196,9 +1196,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encrypted_images"
|
name = "encrypted_images"
|
||||||
version = "1.4.0"
|
version = "1.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "761fe0fc7059c1e9cb4217f49263b146d269322354aeefb8fc519a60b2e8cef1"
|
checksum = "dda784ca648e472fc1bb0e408cb60b22e6b1647511d3c8f7144dadcf7133f605"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"encoding",
|
"encoding",
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ build = "build.rs"
|
||||||
tauri-build = { version = "2", features = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
blockchain = { path = ".." }
|
contractless = { path = ".." }
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
native-dialog = "0.6"
|
native-dialog = "0.6"
|
||||||
reqwest = { version = "0.12", features = ["rustls-tls"] }
|
reqwest = { version = "0.12", features = ["rustls-tls"] }
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,10 @@ Contractless/
|
||||||
The wallet depends on the blockchain crate with:
|
The wallet depends on the blockchain crate with:
|
||||||
|
|
||||||
```toml
|
```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
|
## Layout
|
||||||
|
|
||||||
|
|
@ -35,7 +35,7 @@ Because of that, `gui wallet/` must be in the same folder that contains the Cont
|
||||||
|
|
||||||
## Build Check
|
## Build Check
|
||||||
|
|
||||||
From `gui wallet/`:
|
From `gui_wallet/`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo check
|
cargo check
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,10 @@
|
||||||
<span class="nav-icon">%</span>
|
<span class="nav-icon">%</span>
|
||||||
<span>Marketing</span>
|
<span>Marketing</span>
|
||||||
</button>
|
</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">
|
<button class="nav-item" data-view="addressbook" type="button">
|
||||||
<span class="nav-icon">@</span>
|
<span class="nav-icon">@</span>
|
||||||
<span>Address Book</span>
|
<span>Address Book</span>
|
||||||
|
|
@ -782,6 +786,58 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="startup-message hidden" data-marketing-message></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>
|
<div class="form-grid compact hidden" data-vanity-form>
|
||||||
<label>
|
<label>
|
||||||
<span>Vanity Name</span>
|
<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="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="Burn">Burn</button>
|
||||||
<button class="transaction-type" type="button" data-transaction-type="Marketing">Marketing</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>
|
<button class="transaction-type" type="button" data-transaction-type="Vanity Address">Vanity Address</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
@ -1106,7 +1163,7 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="marketing-view hidden" id="marketing-view">
|
<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 class="panel-header">
|
||||||
<div>
|
<div>
|
||||||
<h2>Marketing Campaigns</h2>
|
<h2>Marketing Campaigns</h2>
|
||||||
|
|
@ -1215,6 +1272,27 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<section class="settings-view hidden" id="settings-view">
|
||||||
<div class="settings-workspace panel">
|
<div class="settings-workspace panel">
|
||||||
<div class="panel-header settings-header">
|
<div class="panel-header settings-header">
|
||||||
|
|
@ -1540,6 +1618,7 @@
|
||||||
<script src="js/dashboard_network.js"></script>
|
<script src="js/dashboard_network.js"></script>
|
||||||
<script src="js/address_book.js"></script>
|
<script src="js/address_book.js"></script>
|
||||||
<script src="js/transaction_history.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/history_page.js"></script>
|
||||||
<script src="js/assets_page.js"></script>
|
<script src="js/assets_page.js"></script>
|
||||||
<script src="js/nfts_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/collateral_claim_form.js"></script>
|
||||||
<script src="js/burn_form.js"></script>
|
<script src="js/burn_form.js"></script>
|
||||||
<script src="js/marketing_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/vanity_address_form.js"></script>
|
||||||
<script src="js/unbroadcast_transactions.js"></script>
|
<script src="js/unbroadcast_transactions.js"></script>
|
||||||
<script src="js/wallet_session.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."],
|
nfts: ["NFTs", "View and manage non-fungible token holdings."],
|
||||||
loans: ["Loans", "Create loans, payments, and collateral claims."],
|
loans: ["Loans", "Create loans, payments, and collateral claims."],
|
||||||
marketing: ["Marketing", "Review campaign activity from agency wallets."],
|
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."],
|
addressbook: ["Address Book", "Manage known addresses and labels."],
|
||||||
settings: ["Settings", "Manage wallets, nodes, security, and app preferences."]
|
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 nftsView = document.querySelector("#nfts-view");
|
||||||
const loansView = document.querySelector("#loans-view");
|
const loansView = document.querySelector("#loans-view");
|
||||||
const marketingView = document.querySelector("#marketing-view");
|
const marketingView = document.querySelector("#marketing-view");
|
||||||
|
const addressDataView = document.querySelector("#address-data-view");
|
||||||
const addressbookView = document.querySelector("#addressbook-view");
|
const addressbookView = document.querySelector("#addressbook-view");
|
||||||
const settingsView = document.querySelector("#settings-view");
|
const settingsView = document.querySelector("#settings-view");
|
||||||
const viewTitle = document.querySelector("#view-title");
|
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 marketingAdvertiser = document.querySelector("[data-marketing-advertiser]");
|
||||||
const marketingFeeLabel = document.querySelector("[data-marketing-fee-label]");
|
const marketingFeeLabel = document.querySelector("[data-marketing-fee-label]");
|
||||||
const marketingMessage = document.querySelector("[data-marketing-message]");
|
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 vanityForm = document.querySelector("[data-vanity-form]");
|
||||||
const vanityActions = document.querySelector("[data-vanity-actions]");
|
const vanityActions = document.querySelector("[data-vanity-actions]");
|
||||||
const vanityNameInput = document.querySelector("[data-vanity-name]");
|
const vanityNameInput = document.querySelector("[data-vanity-name]");
|
||||||
|
|
@ -368,6 +391,7 @@ let latestDashboardTransactionsTimer = null;
|
||||||
let latestDashboardTxids = [];
|
let latestDashboardTxids = [];
|
||||||
let latestDashboardTransactionRecords = new Map();
|
let latestDashboardTransactionRecords = new Map();
|
||||||
let backgroundTransactionSyncId = 0;
|
let backgroundTransactionSyncId = 0;
|
||||||
|
let backgroundTransactionSyncPromise = Promise.resolve();
|
||||||
let historyRefreshTimer = null;
|
let historyRefreshTimer = null;
|
||||||
let historyRefreshReadId = 0;
|
let historyRefreshReadId = 0;
|
||||||
let historyRefreshInFlight = false;
|
let historyRefreshInFlight = false;
|
||||||
|
|
@ -676,6 +700,7 @@ async function refreshDashboardNftBalanceKeys() {
|
||||||
return knownNftBalanceLoad;
|
return knownNftBalanceLoad;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const wasReady = dashboardNftBalanceKeysReady();
|
||||||
knownNftBalanceLoad = invoke("nft_list", { broadcastNode })
|
knownNftBalanceLoad = invoke("nft_list", { broadcastNode })
|
||||||
.then((rows) => {
|
.then((rows) => {
|
||||||
const nextKeys = new Set();
|
const nextKeys = new Set();
|
||||||
|
|
@ -692,7 +717,7 @@ async function refreshDashboardNftBalanceKeys() {
|
||||||
knownNftBalanceKeys = nextKeys;
|
knownNftBalanceKeys = nextKeys;
|
||||||
knownNftBalanceNode = broadcastNode;
|
knownNftBalanceNode = broadcastNode;
|
||||||
knownNftBalanceLoadedAt = Date.now();
|
knownNftBalanceLoadedAt = Date.now();
|
||||||
return previous !== next;
|
return !wasReady || previous !== next;
|
||||||
})
|
})
|
||||||
.catch(() => false)
|
.catch(() => false)
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|
@ -832,6 +857,7 @@ function resetTransactionForms() {
|
||||||
"resetCollateralClaimSession",
|
"resetCollateralClaimSession",
|
||||||
"resetBurnSession",
|
"resetBurnSession",
|
||||||
"resetMarketingSession",
|
"resetMarketingSession",
|
||||||
|
"resetDataStorageSession",
|
||||||
"resetVanityAddressSession"
|
"resetVanityAddressSession"
|
||||||
].forEach((name) => {
|
].forEach((name) => {
|
||||||
if (typeof window[name] === "function") {
|
if (typeof window[name] === "function") {
|
||||||
|
|
@ -850,6 +876,7 @@ function resetTransactionForms() {
|
||||||
"clearCollateralClaimForm",
|
"clearCollateralClaimForm",
|
||||||
"clearBurnForm",
|
"clearBurnForm",
|
||||||
"clearMarketingForm",
|
"clearMarketingForm",
|
||||||
|
"clearDataStorageForm",
|
||||||
"clearVanityForm"
|
"clearVanityForm"
|
||||||
].forEach((name) => {
|
].forEach((name) => {
|
||||||
if (typeof window[name] === "function") {
|
if (typeof window[name] === "function") {
|
||||||
|
|
@ -868,6 +895,7 @@ function resetTransactionForms() {
|
||||||
collateralClaimMessage,
|
collateralClaimMessage,
|
||||||
burnMessage,
|
burnMessage,
|
||||||
marketingMessage,
|
marketingMessage,
|
||||||
|
dataStorageMessage,
|
||||||
vanityMessage
|
vanityMessage
|
||||||
].forEach((element) => {
|
].forEach((element) => {
|
||||||
if (!element) {
|
if (!element) {
|
||||||
|
|
|
||||||
|
|
@ -56,12 +56,11 @@ async function loadCollateralClaimContracts(force = false) {
|
||||||
if (collateralClaimLoadPromise) {
|
if (collateralClaimLoadPromise) {
|
||||||
return collateralClaimLoadPromise;
|
return collateralClaimLoadPromise;
|
||||||
}
|
}
|
||||||
const invoke = window.__TAURI__?.core?.invoke;
|
if (!walletLoaded || !registrationRegistered) {
|
||||||
const broadcastNode = activeBroadcastNode();
|
|
||||||
if (!invoke || !broadcastNode || !walletLoaded || !registrationRegistered) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const key = `${activeWalletAddressShort || activeWalletAddressHex}|${broadcastNode}`;
|
const walletToken = activeWalletCacheToken();
|
||||||
|
const key = `${walletToken}|local-claimable-collateral`;
|
||||||
if (!force && collateralClaimContractsKey === key) {
|
if (!force && collateralClaimContractsKey === key) {
|
||||||
populateCollateralClaimContracts();
|
populateCollateralClaimContracts();
|
||||||
return;
|
return;
|
||||||
|
|
@ -69,7 +68,9 @@ async function loadCollateralClaimContracts(force = false) {
|
||||||
|
|
||||||
collateralClaimLoadPromise = (async () => {
|
collateralClaimLoadPromise = (async () => {
|
||||||
setCollateralClaimMessage("Loading claimable collateral...");
|
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 : [];
|
collateralClaimContracts = Array.isArray(contracts) ? contracts : [];
|
||||||
collateralClaimContractsKey = key;
|
collateralClaimContractsKey = key;
|
||||||
populateCollateralClaimContracts();
|
populateCollateralClaimContracts();
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,7 @@ function signedAmount(sign, value) {
|
||||||
|
|
||||||
function signedBaseFee(sign, value) {
|
function signedBaseFee(sign, value) {
|
||||||
const amount = signedAmount(sign, value);
|
const amount = signedAmount(sign, value);
|
||||||
return amount && amount !== "0" ? `${amount} ${activeBaseCoin()}` : "--";
|
return amount && amount !== "0" ? amount : "--";
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusForBlock(blockHeight, currentHeight = latestKnownBlockHeight) {
|
function statusForBlock(blockHeight, currentHeight = latestKnownBlockHeight) {
|
||||||
|
|
@ -248,7 +248,21 @@ function txTypeName(type) {
|
||||||
9: "Collateral Claim",
|
9: "Collateral Claim",
|
||||||
10: "Burn",
|
10: "Burn",
|
||||||
11: "Issue Token",
|
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";
|
}[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?.timestamp,
|
||||||
transaction?.unsigned_issue_token?.time,
|
transaction?.unsigned_issue_token?.time,
|
||||||
transaction?.unsigned_vanity_address?.timestamp,
|
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);
|
const timestamp = candidates.map(Number).find((value) => Number.isFinite(value) && value > 0);
|
||||||
return timestamp || 0;
|
return timestamp || 0;
|
||||||
|
|
|
||||||
|
|
@ -55,12 +55,11 @@ async function loadLoanPaymentContracts(force = false) {
|
||||||
if (loanPaymentLoadPromise) {
|
if (loanPaymentLoadPromise) {
|
||||||
return loanPaymentLoadPromise;
|
return loanPaymentLoadPromise;
|
||||||
}
|
}
|
||||||
const invoke = window.__TAURI__?.core?.invoke;
|
if (!walletLoaded || !registrationRegistered) {
|
||||||
const broadcastNode = activeBroadcastNode();
|
|
||||||
if (!invoke || !broadcastNode || !walletLoaded || !registrationRegistered) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const key = `${activeWalletAddressShort || activeWalletAddressHex}|${broadcastNode}`;
|
const walletToken = activeWalletCacheToken();
|
||||||
|
const key = `${walletToken}|local-active-loans`;
|
||||||
if (!force && loanPaymentContractsKey === key) {
|
if (!force && loanPaymentContractsKey === key) {
|
||||||
populateLoanPaymentContracts();
|
populateLoanPaymentContracts();
|
||||||
return;
|
return;
|
||||||
|
|
@ -68,7 +67,9 @@ async function loadLoanPaymentContracts(force = false) {
|
||||||
|
|
||||||
loanPaymentLoadPromise = (async () => {
|
loanPaymentLoadPromise = (async () => {
|
||||||
setLoanPaymentMessage("Loading active loans...");
|
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 : [];
|
loanPaymentContracts = Array.isArray(contracts) ? contracts : [];
|
||||||
loanPaymentContractsKey = key;
|
loanPaymentContractsKey = key;
|
||||||
populateLoanPaymentContracts();
|
populateLoanPaymentContracts();
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,20 @@ function loanPaymentCount(contract) {
|
||||||
return Array.isArray(contract.payments) ? contract.payments.length : 0;
|
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) {
|
function loanAmountLabel(value, asset) {
|
||||||
return `${loanFormatAtomic(value)} ${String(asset || "").trim()}`.trim();
|
return `${loanFormatAtomic(value)} ${String(asset || "").trim()}`.trim();
|
||||||
}
|
}
|
||||||
|
|
@ -146,6 +160,257 @@ function nextLoanPaymentDueLabel(contract) {
|
||||||
return formatLoanDueDate(loanDueDateForPayment(contract, completedPayments + 1));
|
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() {
|
function sortedLoans() {
|
||||||
const query = String(loansSearch?.value || "").trim().toLowerCase();
|
const query = String(loansSearch?.value || "").trim().toLowerCase();
|
||||||
const roleFilter = loansRoleFilter?.value || "all";
|
const roleFilter = loansRoleFilter?.value || "all";
|
||||||
|
|
@ -327,8 +592,6 @@ function renderLoansPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadLoansPage({ force = false } = {}) {
|
async function loadLoansPage({ force = false } = {}) {
|
||||||
const invoke = window.__TAURI__?.core?.invoke;
|
|
||||||
const broadcastNode = activeBroadcastNode();
|
|
||||||
if (!walletLoaded) {
|
if (!walletLoaded) {
|
||||||
loansContracts = [];
|
loansContracts = [];
|
||||||
selectedLoanContractHash = "";
|
selectedLoanContractHash = "";
|
||||||
|
|
@ -336,21 +599,8 @@ async function loadLoansPage({ force = false } = {}) {
|
||||||
renderLoansPage();
|
renderLoansPage();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!invoke) {
|
const walletToken = activeWalletCacheToken();
|
||||||
loansContracts = [];
|
const key = `${walletToken}|local-loans`;
|
||||||
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}`;
|
|
||||||
if (!force && loansLoadedForKey === key) {
|
if (!force && loansLoadedForKey === key) {
|
||||||
renderLoansPage();
|
renderLoansPage();
|
||||||
return;
|
return;
|
||||||
|
|
@ -363,21 +613,17 @@ async function loadLoansPage({ force = false } = {}) {
|
||||||
loansStateMessage = "Loading loans...";
|
loansStateMessage = "Loading loans...";
|
||||||
renderLoansPage();
|
renderLoansPage();
|
||||||
try {
|
try {
|
||||||
const contracts = await invokeWalletRpcWithBackoff(
|
const contracts = await loadLocalLoanContractsFromCache(walletToken);
|
||||||
"loan contracts",
|
loansContracts = await applyRemoteCollateralStatuses(Array.isArray(contracts) ? contracts : []);
|
||||||
() => invoke("loan_contracts", { broadcastNode }),
|
|
||||||
() => currentView === "loans" && activeBroadcastNode() === broadcastNode
|
|
||||||
);
|
|
||||||
loansContracts = Array.isArray(contracts) ? contracts : [];
|
|
||||||
loansLoadedForKey = key;
|
loansLoadedForKey = key;
|
||||||
selectedLoanContractHash = loansContracts[0]?.contract || "";
|
selectedLoanContractHash = loansContracts[0]?.contract || "";
|
||||||
loansStateMessage = loansContracts.length
|
loansStateMessage = loansContracts.length
|
||||||
? ""
|
? ""
|
||||||
: "Loan contract command returned 0 contracts for this wallet.";
|
: "No locally synced loan contracts found for this wallet.";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
loansContracts = [];
|
loansContracts = [];
|
||||||
selectedLoanContractHash = "";
|
selectedLoanContractHash = "";
|
||||||
loansStateMessage = `Loan contract lookup failed: ${String(error)}`;
|
loansStateMessage = `Local loan cache lookup failed: ${String(error)}`;
|
||||||
} finally {
|
} finally {
|
||||||
loansLoadInFlight = false;
|
loansLoadInFlight = false;
|
||||||
renderLoansPage();
|
renderLoansPage();
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@
|
||||||
if (previousView === "nfts" && view !== "nfts" && typeof resetNftsPage === "function") {
|
if (previousView === "nfts" && view !== "nfts" && typeof resetNftsPage === "function") {
|
||||||
resetNftsPage();
|
resetNftsPage();
|
||||||
}
|
}
|
||||||
|
if (previousView === "addressdata" && view !== "addressdata" && typeof resetAddressDataSelection === "function") {
|
||||||
|
resetAddressDataSelection();
|
||||||
|
}
|
||||||
|
|
||||||
const [title, subtitle] = titles[view] || titles.dashboard;
|
const [title, subtitle] = titles[view] || titles.dashboard;
|
||||||
currentView = view;
|
currentView = view;
|
||||||
|
|
@ -42,6 +45,7 @@
|
||||||
nftsView.classList.add("hidden");
|
nftsView.classList.add("hidden");
|
||||||
loansView.classList.add("hidden");
|
loansView.classList.add("hidden");
|
||||||
marketingView.classList.add("hidden");
|
marketingView.classList.add("hidden");
|
||||||
|
addressDataView.classList.add("hidden");
|
||||||
addressbookView.classList.add("hidden");
|
addressbookView.classList.add("hidden");
|
||||||
settingsView.classList.add("hidden");
|
settingsView.classList.add("hidden");
|
||||||
updateNetworkInfoRefreshState();
|
updateNetworkInfoRefreshState();
|
||||||
|
|
@ -67,6 +71,7 @@
|
||||||
nftsView.classList.add("hidden");
|
nftsView.classList.add("hidden");
|
||||||
loansView.classList.add("hidden");
|
loansView.classList.add("hidden");
|
||||||
marketingView.classList.add("hidden");
|
marketingView.classList.add("hidden");
|
||||||
|
addressDataView.classList.add("hidden");
|
||||||
addressbookView.classList.add("hidden");
|
addressbookView.classList.add("hidden");
|
||||||
settingsView.classList.add("hidden");
|
settingsView.classList.add("hidden");
|
||||||
|
|
||||||
|
|
@ -106,6 +111,12 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (view === "addressdata") {
|
||||||
|
addressDataView.classList.remove("hidden");
|
||||||
|
prepareAddressDataPage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (view === "addressbook") {
|
if (view === "addressbook") {
|
||||||
addressbookView.classList.remove("hidden");
|
addressbookView.classList.remove("hidden");
|
||||||
prepareAddressBookPage();
|
prepareAddressBookPage();
|
||||||
|
|
@ -193,6 +204,8 @@ function refreshActiveTransactionFormAfterWalletData() {
|
||||||
prepareCollateralClaimForm();
|
prepareCollateralClaimForm();
|
||||||
} else if (currentTransactionType === "Burn" && typeof prepareBurnForm === "function") {
|
} else if (currentTransactionType === "Burn" && typeof prepareBurnForm === "function") {
|
||||||
prepareBurnForm();
|
prepareBurnForm();
|
||||||
|
} else if (currentTransactionType === "Data Storage" && typeof prepareDataStorageForm === "function") {
|
||||||
|
prepareDataStorageForm();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,6 +225,7 @@ function clearInactiveTransactionMessages(activeType) {
|
||||||
"Collateral Claim": collateralClaimMessage,
|
"Collateral Claim": collateralClaimMessage,
|
||||||
"Burn": burnMessage,
|
"Burn": burnMessage,
|
||||||
"Marketing": marketingMessage,
|
"Marketing": marketingMessage,
|
||||||
|
"Data Storage": dataStorageMessage,
|
||||||
"Vanity Address": vanityMessage
|
"Vanity Address": vanityMessage
|
||||||
};
|
};
|
||||||
Object.entries(messageByType).forEach(([type, element]) => {
|
Object.entries(messageByType).forEach(([type, element]) => {
|
||||||
|
|
@ -244,6 +258,7 @@ function showTransactionForm(type) {
|
||||||
const isCollateralClaim = type === "Collateral Claim";
|
const isCollateralClaim = type === "Collateral Claim";
|
||||||
const isBurn = type === "Burn";
|
const isBurn = type === "Burn";
|
||||||
const isMarketing = type === "Marketing";
|
const isMarketing = type === "Marketing";
|
||||||
|
const isDataStorage = type === "Data Storage";
|
||||||
const isVanity = type === "Vanity Address";
|
const isVanity = type === "Vanity Address";
|
||||||
document.querySelector("[data-transfer-form]")?.classList.toggle("hidden", !isTransfer);
|
document.querySelector("[data-transfer-form]")?.classList.toggle("hidden", !isTransfer);
|
||||||
transferActions?.classList.toggle("hidden", !isTransfer);
|
transferActions?.classList.toggle("hidden", !isTransfer);
|
||||||
|
|
@ -276,6 +291,9 @@ function showTransactionForm(type) {
|
||||||
marketingForm?.classList.toggle("hidden", !isMarketing);
|
marketingForm?.classList.toggle("hidden", !isMarketing);
|
||||||
marketingActions?.classList.toggle("hidden", !isMarketing);
|
marketingActions?.classList.toggle("hidden", !isMarketing);
|
||||||
marketingMessage?.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);
|
vanityForm?.classList.toggle("hidden", !isVanity);
|
||||||
vanityActions?.classList.toggle("hidden", !isVanity);
|
vanityActions?.classList.toggle("hidden", !isVanity);
|
||||||
vanityMessage?.classList.toggle("hidden", !isVanity);
|
vanityMessage?.classList.toggle("hidden", !isVanity);
|
||||||
|
|
@ -300,6 +318,8 @@ function showTransactionForm(type) {
|
||||||
prepareBurnForm();
|
prepareBurnForm();
|
||||||
} else if (isMarketing && typeof prepareMarketingForm === "function") {
|
} else if (isMarketing && typeof prepareMarketingForm === "function") {
|
||||||
prepareMarketingForm();
|
prepareMarketingForm();
|
||||||
|
} else if (isDataStorage && typeof prepareDataStorageForm === "function") {
|
||||||
|
prepareDataStorageForm();
|
||||||
} else if (isVanity && typeof prepareVanityAddressForm === "function") {
|
} else if (isVanity && typeof prepareVanityAddressForm === "function") {
|
||||||
prepareVanityAddressForm();
|
prepareVanityAddressForm();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,16 +80,20 @@ function scheduleClipboardClear(copiedText) {
|
||||||
}
|
}
|
||||||
lastClipboardText = copiedText || "";
|
lastClipboardText = copiedText || "";
|
||||||
const timeoutMs = durationSettingMs(guiRuntimeSettings.clipboard_clear_timeout);
|
const timeoutMs = durationSettingMs(guiRuntimeSettings.clipboard_clear_timeout);
|
||||||
if (!timeoutMs || !lastClipboardText || !navigator.clipboard) {
|
if (!timeoutMs || !lastClipboardText) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
clipboardClearTimer = setTimeout(async () => {
|
clipboardClearTimer = setTimeout(async () => {
|
||||||
lastClipboardText = "";
|
lastClipboardText = "";
|
||||||
|
const invoke = window.__TAURI__?.core?.invoke;
|
||||||
|
if (!invoke) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText("");
|
await invoke("clear_system_clipboard");
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Clipboard writes can fail outside a user gesture; never prompt or block.
|
// Background clipboard clearing must never open a browser permission prompt.
|
||||||
}
|
}
|
||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
}
|
}
|
||||||
|
|
@ -102,20 +106,11 @@ async function clearWalletClipboard() {
|
||||||
lastClipboardText = "";
|
lastClipboardText = "";
|
||||||
|
|
||||||
const invoke = window.__TAURI__?.core?.invoke;
|
const invoke = window.__TAURI__?.core?.invoke;
|
||||||
if (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) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText("");
|
await invoke("clear_system_clipboard");
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -311,11 +306,15 @@ async function clearSelectedWalletCache(kind, button) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clearingHistory = kind === "history";
|
||||||
button && (button.disabled = true);
|
button && (button.disabled = true);
|
||||||
setCacheClearMessage("Clearing cache...");
|
setCacheClearMessage("Clearing cache...");
|
||||||
try {
|
try {
|
||||||
|
if (clearingHistory) {
|
||||||
|
await stopBackgroundTransactionSync();
|
||||||
|
}
|
||||||
await invoke("clear_wallet_cache", { cacheKind: kind });
|
await invoke("clear_wallet_cache", { cacheKind: kind });
|
||||||
if (kind === "history") {
|
if (clearingHistory) {
|
||||||
clearHistoryRows();
|
clearHistoryRows();
|
||||||
resetLatestDashboardTransactionsMemory();
|
resetLatestDashboardTransactionsMemory();
|
||||||
if (typeof resetHistoryPage === "function") {
|
if (typeof resetHistoryPage === "function") {
|
||||||
|
|
@ -340,6 +339,9 @@ async function clearSelectedWalletCache(kind, button) {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setCacheClearMessage(String(error), "error");
|
setCacheClearMessage(String(error), "error");
|
||||||
} finally {
|
} finally {
|
||||||
|
if (clearingHistory) {
|
||||||
|
startBackgroundTransactionSync();
|
||||||
|
}
|
||||||
button && (button.disabled = false);
|
button && (button.disabled = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,20 @@
|
||||||
transaction.unsigned_burn?.txtype ??
|
transaction.unsigned_burn?.txtype ??
|
||||||
transaction.unsigned_issue_token?.txtype ??
|
transaction.unsigned_issue_token?.txtype ??
|
||||||
transaction.unsigned_vanity_address?.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
|
0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -33,7 +47,21 @@ function txTypeKey(type) {
|
||||||
9: "collateral_claim",
|
9: "collateral_claim",
|
||||||
10: "burn",
|
10: "burn",
|
||||||
11: "token_issue",
|
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";
|
}[Number(type)] || "transaction";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,6 +102,24 @@ function loanSummaryFromLinkedTransactions(linkedTransactions = {}, contractHash
|
||||||
return null;
|
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(
|
function parseDecodedDashboardTransactionRows(
|
||||||
txid,
|
txid,
|
||||||
blockHeight,
|
blockHeight,
|
||||||
|
|
@ -224,6 +270,18 @@ function parseDecodedDashboardTransactionRows(
|
||||||
}
|
}
|
||||||
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(tx.address), signedAmount("+", tx.txfee || tx.fee || 0))];
|
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(tx.address), signedAmount("+", tx.txfee || tx.fee || 0))];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (_) {
|
} catch (_) {
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
@ -440,6 +498,16 @@ function parseDashboardTransactionRows(
|
||||||
}
|
}
|
||||||
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(address), signedAmount("+", fee))];
|
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(address), signedAmount("+", fee))];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type >= 100 && type <= 113) {
|
||||||
|
return parseDecodedDashboardTransactionRows(
|
||||||
|
txid,
|
||||||
|
blockHeight,
|
||||||
|
decodeLightweightTransaction(bytes),
|
||||||
|
linkedTransactions,
|
||||||
|
minerEarnings
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return fallback;
|
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 } };
|
return { unknown: { txtype: type } };
|
||||||
}
|
}
|
||||||
|
|
@ -1146,10 +1314,12 @@ async function writeTransactionRecordFile(fileName, record, walletToken = active
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await invoke("write_local_transaction_record", {
|
const recordJson = JSON.stringify(record);
|
||||||
fileName,
|
if (!fileName || typeof recordJson !== "string") {
|
||||||
recordJson: JSON.stringify(record)
|
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()) {
|
async function mutateTransactionCache(mutator, walletToken = activeWalletCacheToken()) {
|
||||||
|
|
@ -1421,8 +1591,10 @@ function shouldContinueBackgroundTransactionSync(syncId, walletToken = activeWal
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopBackgroundTransactionSync() {
|
async function stopBackgroundTransactionSync() {
|
||||||
|
const runningSync = backgroundTransactionSyncPromise;
|
||||||
backgroundTransactionSyncId += 1;
|
backgroundTransactionSyncId += 1;
|
||||||
|
await runningSync;
|
||||||
}
|
}
|
||||||
|
|
||||||
function startBackgroundTransactionSync() {
|
function startBackgroundTransactionSync() {
|
||||||
|
|
@ -1432,7 +1604,11 @@ function startBackgroundTransactionSync() {
|
||||||
|
|
||||||
const walletToken = activeWalletCacheToken();
|
const walletToken = activeWalletCacheToken();
|
||||||
const syncId = ++backgroundTransactionSyncId;
|
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()) {
|
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);
|
const backoff = Math.min(BACKGROUND_SYNC_IDLE_MS * 2 ** failureCount, 60000);
|
||||||
await sleepMs(backoff);
|
await sleepMs(backoff);
|
||||||
if (shouldContinueBackgroundTransactionSync(syncId, walletToken)) {
|
if (shouldContinueBackgroundTransactionSync(syncId, walletToken)) {
|
||||||
runBackgroundTransactionSync(syncId, failureCount + 1, walletToken);
|
return runBackgroundTransactionSync(syncId, failureCount + 1, walletToken);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,9 @@ function updateActiveWallet(wallet) {
|
||||||
if (typeof resetAddressBookPage === "function") {
|
if (typeof resetAddressBookPage === "function") {
|
||||||
resetAddressBookPage();
|
resetAddressBookPage();
|
||||||
}
|
}
|
||||||
|
if (typeof resetAddressDataPage === "function") {
|
||||||
|
resetAddressDataPage();
|
||||||
|
}
|
||||||
if (typeof resetAssetsPage === "function") {
|
if (typeof resetAssetsPage === "function") {
|
||||||
resetAssetsPage();
|
resetAssetsPage();
|
||||||
}
|
}
|
||||||
|
|
@ -181,6 +184,9 @@ function clearActiveWallet() {
|
||||||
if (typeof resetMarketingSession === "function") {
|
if (typeof resetMarketingSession === "function") {
|
||||||
resetMarketingSession();
|
resetMarketingSession();
|
||||||
}
|
}
|
||||||
|
if (typeof resetDataStorageSession === "function") {
|
||||||
|
resetDataStorageSession();
|
||||||
|
}
|
||||||
if (typeof resetVanityAddressSession === "function") {
|
if (typeof resetVanityAddressSession === "function") {
|
||||||
resetVanityAddressSession();
|
resetVanityAddressSession();
|
||||||
}
|
}
|
||||||
|
|
@ -190,6 +196,9 @@ function clearActiveWallet() {
|
||||||
if (typeof resetAddressBookPage === "function") {
|
if (typeof resetAddressBookPage === "function") {
|
||||||
resetAddressBookPage();
|
resetAddressBookPage();
|
||||||
}
|
}
|
||||||
|
if (typeof resetAddressDataPage === "function") {
|
||||||
|
resetAddressDataPage();
|
||||||
|
}
|
||||||
if (typeof resetAssetsPage === "function") {
|
if (typeof resetAssetsPage === "function") {
|
||||||
resetAssetsPage();
|
resetAssetsPage();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,7 @@ body.wallet-open .startup-screen {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 22px;
|
gap: 22px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand {
|
.brand {
|
||||||
|
|
@ -1546,7 +1547,7 @@ th {
|
||||||
min-height: 552px;
|
min-height: 552px;
|
||||||
height: calc(100vh - 142px);
|
height: calc(100vh - 142px);
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto minmax(0, 1fr) auto auto;
|
grid-template-rows: auto minmax(0, 1fr) auto auto auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1595,7 +1596,7 @@ th {
|
||||||
}
|
}
|
||||||
|
|
||||||
.marketing-workspace {
|
.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 {
|
.marketing-summary-grid {
|
||||||
|
|
@ -1605,18 +1606,9 @@ th {
|
||||||
margin: 14px 0 12px;
|
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 {
|
.marketing-table-wrap {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.marketing-summary-grid > div {
|
.marketing-summary-grid > div {
|
||||||
|
|
@ -2606,6 +2598,174 @@ th {
|
||||||
text-align: right;
|
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) {
|
.addressbook-table td:nth-child(3) {
|
||||||
max-width: 360px;
|
max-width: 360px;
|
||||||
}
|
}
|
||||||
|
|
@ -3040,6 +3200,7 @@ tr:hover td {
|
||||||
min-height: auto;
|
min-height: auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
|
grid-template-rows: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.marketing-campaign-controls {
|
.marketing-campaign-controls {
|
||||||
|
|
@ -3072,6 +3233,21 @@ tr:hover td {
|
||||||
max-height: none;
|
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 {
|
.settings-workspace {
|
||||||
min-height: auto;
|
min-height: auto;
|
||||||
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::general_purpose::STANDARD;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use blockchain::blocks::burn::{BurnTransaction, UnsignedBurnTransaction};
|
use contractless::blocks::burn::{BurnTransaction, UnsignedBurnTransaction};
|
||||||
use blockchain::blocks::collateral::{
|
use contractless::blocks::collateral::{
|
||||||
CollateralClaimTransaction, UnsignedCollateralClaimTransaction,
|
CollateralClaimTransaction, UnsignedCollateralClaimTransaction,
|
||||||
};
|
};
|
||||||
use blockchain::blocks::genesis::GenesisTransaction;
|
use contractless::blocks::delete_key::{DeleteKey, UnsignedDeleteKey};
|
||||||
use blockchain::blocks::issue_token::{IssueTokenTransaction, UnsignedIssueTokenTransaction};
|
use contractless::blocks::genesis::GenesisTransaction;
|
||||||
use blockchain::blocks::loan_payment::{
|
use contractless::blocks::issue_token::{IssueTokenTransaction, UnsignedIssueTokenTransaction};
|
||||||
|
use contractless::blocks::loan_payment::{
|
||||||
ContractPaymentTransaction, UnsignedContractPaymentTransaction,
|
ContractPaymentTransaction, UnsignedContractPaymentTransaction,
|
||||||
};
|
};
|
||||||
use blockchain::blocks::loans::{LoanContractTransaction, UnsignedLoanContractTransaction};
|
use contractless::blocks::loans::{LoanContractTransaction, UnsignedLoanContractTransaction};
|
||||||
use blockchain::blocks::marketing::{MarketingTransaction, UnsignedMarketingTransaction};
|
use contractless::blocks::marketing::{MarketingTransaction, UnsignedMarketingTransaction};
|
||||||
use blockchain::blocks::nft::{CreateNftTransaction, UnsignedCreateNftTransaction};
|
use contractless::blocks::nft::{CreateNftTransaction, UnsignedCreateNftTransaction};
|
||||||
use blockchain::blocks::rewards::RewardsTransaction;
|
use contractless::blocks::rewards::RewardsTransaction;
|
||||||
use blockchain::blocks::swap::{SwapTransaction, UnsignedSwapTransaction};
|
use contractless::blocks::storage_key::{StorageKey, UnsignedStorageKey};
|
||||||
use blockchain::blocks::token::{CreateTokenTransaction, UnsignedCreateTokenTransaction};
|
use contractless::blocks::storage_values::{
|
||||||
use blockchain::blocks::transfer::{TransferTransaction, UnsignedTransferTransaction};
|
StorageBool, StorageI128, StorageI16, StorageI32, StorageI64, StorageI8, StorageString,
|
||||||
use blockchain::blocks::vanity::{UnsignedVanityAddressTransaction, VanityAddressTransaction};
|
StorageU128, StorageU16, StorageU32, StorageU64, StorageU8, UnsignedStorageBool,
|
||||||
use blockchain::common::skein::skein_256_hash_bytes;
|
UnsignedStorageI128, UnsignedStorageI16, UnsignedStorageI32, UnsignedStorageI64,
|
||||||
use blockchain::common::types::{
|
UnsignedStorageI8, UnsignedStorageString, UnsignedStorageU128, UnsignedStorageU16,
|
||||||
minimum_transfer_fee, BORROWER_FEE, BURN_FEE, COLLATERAL_FEE, CREATE_NFT_FEE, CREATE_TOKEN_FEE,
|
UnsignedStorageU32, UnsignedStorageU64, UnsignedStorageU8, STORAGE_KEY_HASH_BYTES,
|
||||||
ISSUE_TOKEN_FEE, LENDER_FEE, MARKETING_FEE, SWAP_FEE, VANITY_ADDRESS_FEE,
|
STORAGE_STRING_VALUE_BYTES, STORAGE_VALUE_KEY_BYTES,
|
||||||
};
|
};
|
||||||
use blockchain::records::memory::mempool::BASECOIN;
|
use contractless::blocks::swap::{SwapTransaction, UnsignedSwapTransaction};
|
||||||
use blockchain::records::memory::response_channels::generate_uid;
|
use contractless::blocks::token::{CreateTokenTransaction, UnsignedCreateTokenTransaction};
|
||||||
use blockchain::rpc::command_maps::{
|
use contractless::blocks::transfer::{TransferTransaction, UnsignedTransferTransaction};
|
||||||
RPC_ADDRESS_HISTORY, RPC_ADDRESS_TOTAL_BALANCE, RPC_BLOCK_HEIGHT, RPC_CONTRACT_BY_ADDRESS,
|
use contractless::blocks::vanity::{UnsignedVanityAddressTransaction, VanityAddressTransaction};
|
||||||
RPC_LATEST_ADDRESS_TRANSACTIONS, RPC_LOAN_CONTRACT, RPC_MARKETING_CAMPAIGN_HISTORY,
|
use contractless::common::skein::skein_256_hash_bytes;
|
||||||
RPC_NETWORK_INFO, RPC_NFT_DETAILS, RPC_NFT_LIST, RPC_REGISTER_WALLET, RPC_TIME,
|
use contractless::common::types::{
|
||||||
RPC_TOKEN_CATALOG, RPC_TOKEN_DETAILS, RPC_TOKEN_LIST, RPC_TRANSACTION_BY_TXID,
|
minimum_transfer_fee, BORROWER_FEE, BURN_FEE, COLLATERAL_FEE, CREATE_NFT_FEE, CREATE_TOKEN_FEE,
|
||||||
RPC_VALIDATE_MESSAGE, RPC_VANITY_LOOKUP, RPC_VANITY_OWNER_LOOKUP,
|
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,
|
RPC_WALLET_REGISTRATION_STATUS,
|
||||||
};
|
};
|
||||||
use blockchain::standalone_tools::connections::handshake::{self, HandshakeWallet};
|
use contractless::standalone_tools::connections::handshake::{self, HandshakeWallet};
|
||||||
use blockchain::tokio::fs::{
|
use contractless::tokio::fs::{
|
||||||
create_dir_all, metadata, read, read_dir, read_to_string, remove_dir_all, remove_file,
|
create_dir_all, metadata, read, read_dir, read_to_string, remove_dir_all, remove_file,
|
||||||
};
|
};
|
||||||
use blockchain::tokio::sync::{oneshot, Mutex as AsyncMutex};
|
use contractless::tokio::sync::{oneshot, Mutex as AsyncMutex};
|
||||||
use blockchain::tokio::time::sleep;
|
use contractless::tokio::time::sleep;
|
||||||
use blockchain::wallets::structures::{SavedWallet, Wallet};
|
use contractless::wallets::structures::{SavedWallet, Wallet};
|
||||||
use blockchain::{
|
use contractless::{
|
||||||
create_img, decode_image_and_extract_text, decrypts, encode, encrypts, to_string_pretty,
|
create_img, decode_image_and_extract_text, decrypts, encode, encrypts, to_string_pretty,
|
||||||
};
|
};
|
||||||
use native_dialog::FileDialog;
|
use native_dialog::FileDialog;
|
||||||
|
|
@ -183,6 +194,8 @@ fn main() {
|
||||||
clear_wallet_cache,
|
clear_wallet_cache,
|
||||||
read_address_book,
|
read_address_book,
|
||||||
write_address_book,
|
write_address_book,
|
||||||
|
read_storage_labels,
|
||||||
|
write_storage_labels,
|
||||||
lookup_address_vanity,
|
lookup_address_vanity,
|
||||||
check_vanity_address_availability,
|
check_vanity_address_availability,
|
||||||
save_history_export,
|
save_history_export,
|
||||||
|
|
@ -199,12 +212,16 @@ fn main() {
|
||||||
broadcast_burn_transaction,
|
broadcast_burn_transaction,
|
||||||
save_marketing_transaction,
|
save_marketing_transaction,
|
||||||
broadcast_marketing_transaction,
|
broadcast_marketing_transaction,
|
||||||
|
save_data_storage_transaction,
|
||||||
|
broadcast_data_storage_transaction,
|
||||||
|
broadcast_delete_storage_transaction,
|
||||||
save_vanity_address_transaction,
|
save_vanity_address_transaction,
|
||||||
broadcast_vanity_address_transaction,
|
broadcast_vanity_address_transaction,
|
||||||
save_swap_offer,
|
save_swap_offer,
|
||||||
save_loan_offer,
|
save_loan_offer,
|
||||||
active_loan_contracts,
|
active_loan_contracts,
|
||||||
loan_contracts,
|
loan_contracts,
|
||||||
|
collateral_status,
|
||||||
save_loan_payment_transaction,
|
save_loan_payment_transaction,
|
||||||
broadcast_loan_payment_transaction,
|
broadcast_loan_payment_transaction,
|
||||||
claimable_collateral_contracts,
|
claimable_collateral_contracts,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn current_block_height(
|
async fn current_block_height(
|
||||||
state: State<'_, WalletState>,
|
state: State<'_, WalletState>,
|
||||||
broadcast_node: String,
|
broadcast_node: String,
|
||||||
|
|
@ -109,7 +109,7 @@ async fn write_local_transaction_cache(
|
||||||
let normalized = serde_json::to_string_pretty(&parsed)
|
let normalized = serde_json::to_string_pretty(&parsed)
|
||||||
.map_err(|err| format!("Unable to serialize transaction cache: {err}"))?;
|
.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
|
.await
|
||||||
.map_err(|err| format!("Unable to write local transaction cache: {err}"))
|
.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)
|
let normalized = serde_json::to_string_pretty(&parsed)
|
||||||
.map_err(|err| format!("Unable to serialize transaction index: {err}"))?;
|
.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
|
.await
|
||||||
.map_err(|err| format!("Unable to write local transaction index: {err}"))
|
.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)
|
let normalized = serde_json::to_string_pretty(&parsed)
|
||||||
.map_err(|err| format!("Unable to serialize transaction record: {err}"))?;
|
.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
|
.await
|
||||||
.map_err(|err| format!("Unable to write local transaction record: {err}"))
|
.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()));
|
.and_then(|header| header.split_once(';').map(|(mime, _)| mime.to_string()));
|
||||||
if let Ok(bytes) = base64::Engine::decode(&STANDARD, encoded) {
|
if let Ok(bytes) = base64::Engine::decode(&STANDARD, encoded) {
|
||||||
if bytes.len() <= 10_000_000 {
|
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
|
.await
|
||||||
.map_err(|err| format!("Unable to write NFT cached image: {err}"))?;
|
.map_err(|err| format!("Unable to write NFT cached image: {err}"))?;
|
||||||
image_cached = true;
|
image_cached = true;
|
||||||
|
|
@ -322,7 +322,7 @@ async fn write_nft_cache(
|
||||||
.await
|
.await
|
||||||
.map_err(|err| format!("Failed to read NFT image response: {err}"))?;
|
.map_err(|err| format!("Failed to read NFT image response: {err}"))?;
|
||||||
if bytes.len() <= 10_000_000 {
|
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
|
.await
|
||||||
.map_err(|err| format!("Unable to write NFT cached image: {err}"))?;
|
.map_err(|err| format!("Unable to write NFT cached image: {err}"))?;
|
||||||
image_cached = true;
|
image_cached = true;
|
||||||
|
|
@ -338,7 +338,7 @@ async fn write_nft_cache(
|
||||||
});
|
});
|
||||||
let normalized = serde_json::to_string_pretty(&record)
|
let normalized = serde_json::to_string_pretty(&record)
|
||||||
.map_err(|err| format!("Unable to serialize NFT cache record: {err}"))?;
|
.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
|
.await
|
||||||
.map_err(|err| format!("Unable to write NFT cache record: {err}"))?;
|
.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();
|
let key = entry.file_name().to_string_lossy().to_ascii_lowercase();
|
||||||
if !keep.contains(&key) {
|
if !keep.contains(&key) {
|
||||||
blockchain::tokio::fs::remove_dir_all(entry.path())
|
contractless::tokio::fs::remove_dir_all(entry.path())
|
||||||
.await
|
.await
|
||||||
.map_err(|err| format!("Unable to remove stale NFT cache entry: {err}"))?;
|
.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)
|
let normalized = serde_json::to_string_pretty(&parsed)
|
||||||
.map_err(|err| format!("Unable to serialize address book: {err}"))?;
|
.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
|
.await
|
||||||
.map_err(|err| format!("Unable to write address book: {err}"))
|
.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]
|
#[tauri::command]
|
||||||
async fn lookup_address_vanity(
|
async fn lookup_address_vanity(
|
||||||
state: State<'_, WalletState>,
|
state: State<'_, WalletState>,
|
||||||
|
|
@ -595,7 +624,7 @@ async fn save_history_export(
|
||||||
path.set_extension(extension);
|
path.set_extension(extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
blockchain::tokio::fs::write(&path, contents)
|
contractless::tokio::fs::write(&path, contents)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| format!("Unable to save history export: {err}"))?;
|
.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 {
|
fn get_app_status() -> AppStatus {
|
||||||
AppStatus {
|
AppStatus {
|
||||||
app_name: "Contractless Wallet",
|
app_name: "Contractless Wallet",
|
||||||
|
|
@ -342,7 +342,7 @@ async fn backup_active_wallet_image(state: State<'_, WalletState>) -> Result<Opt
|
||||||
path.set_extension("png");
|
path.set_extension("png");
|
||||||
}
|
}
|
||||||
|
|
||||||
blockchain::tokio::fs::write(&path, image_bytes)
|
contractless::tokio::fs::write(&path, image_bytes)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| format!("Unable to save wallet image backup: {err}"))?;
|
.map_err(|err| format!("Unable to save wallet image backup: {err}"))?;
|
||||||
Ok(Some(path.to_string_lossy().into_owned()))
|
Ok(Some(path.to_string_lossy().into_owned()))
|
||||||
|
|
@ -377,7 +377,7 @@ async fn backup_active_wallet_private_key(
|
||||||
path.set_extension("txt");
|
path.set_extension("txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
blockchain::tokio::fs::write(&path, contents)
|
contractless::tokio::fs::write(&path, contents)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| format!("Unable to save private key backup: {err}"))?;
|
.map_err(|err| format!("Unable to save private key backup: {err}"))?;
|
||||||
Ok(Some(path.to_string_lossy().into_owned()))
|
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, "Piggyback", &piggyback_entries);
|
||||||
let updated = replace_ini_section(&updated, "GUI", &gui_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
|
.await
|
||||||
.map_err(|err| format!("Unable to write GUI settings.ini: {err}"))
|
.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"))
|
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> {
|
fn validate_transaction_record_file(file_name: &str) -> Result<String, String> {
|
||||||
let trimmed = file_name.trim();
|
let trimmed = file_name.trim();
|
||||||
if trimmed.len() != 13 || !trimmed.ends_with(".json") {
|
if trimmed.len() != 13 || !trimmed.ends_with(".json") {
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,100 @@
|
||||||
.await
|
.await
|
||||||
.map_err(|err| format!("Failed to decode vanity transaction: {err}"))?,
|
.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}")),
|
_ => return Err(format!("Unknown transaction type: {txtype}")),
|
||||||
}
|
}
|
||||||
.map_err(|err| format!("Failed to serialize decoded transaction: {err}"))
|
.map_err(|err| format!("Failed to serialize decoded transaction: {err}"))
|
||||||
|
|
|
||||||
|
|
@ -299,6 +299,11 @@ struct CollateralClaimContractView {
|
||||||
fee: String,
|
fee: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct CollateralStatusView {
|
||||||
|
claimed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct BroadcastUnbroadcastResult {
|
struct BroadcastUnbroadcastResult {
|
||||||
response: String,
|
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 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() {
|
if saved_wallet.private_key.trim().is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -103,7 +103,7 @@ async fn import_private_key_file(
|
||||||
};
|
};
|
||||||
let output = to_string_pretty(&saved_wallet)
|
let output = to_string_pretty(&saved_wallet)
|
||||||
.map_err(|err| format!("Failed to serialize imported wallet: {err}"))?;
|
.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
|
.await
|
||||||
.map_err(|err| format!("Failed to write imported wallet file: {err}"))?;
|
.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> {
|
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
|
.await
|
||||||
.map_err(|err| format!("Failed to read private key image: {err}"))?;
|
.map_err(|err| format!("Failed to read private key image: {err}"))?;
|
||||||
let image_base64 = STANDARD.encode(image_bytes);
|
let image_base64 = STANDARD.encode(image_bytes);
|
||||||
|
|
@ -250,7 +250,7 @@ dbname = contractless_gui
|
||||||
public_ip = public_ip
|
public_ip = public_ip
|
||||||
);
|
);
|
||||||
|
|
||||||
blockchain::tokio::fs::write(&settings_path, settings)
|
contractless::tokio::fs::write(&settings_path, settings)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| format!("Unable to write GUI settings.ini: {err}"))?;
|
.map_err(|err| format!("Unable to write GUI settings.ini: {err}"))?;
|
||||||
env::set_var("SETTINGS_PATH", &settings_path);
|
env::set_var("SETTINGS_PATH", &settings_path);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue