many fixes

This commit is contained in:
viraladmin 2026-07-25 17:52:48 -06:00
parent 7286741c41
commit a4d04a60ae
19 changed files with 272 additions and 101 deletions

21
Cargo.lock generated
View File

@ -551,6 +551,7 @@ dependencies = [
"shellexpand", "shellexpand",
"skein", "skein",
"sled", "sled",
"socket2 0.5.10",
"tokio", "tokio",
"tokio-postgres", "tokio-postgres",
"windows-service", "windows-service",
@ -2077,7 +2078,7 @@ dependencies = [
"libc", "libc",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2 0.6.4",
"system-configuration", "system-configuration",
"tokio", "tokio",
"tower-service", "tower-service",
@ -3528,7 +3529,7 @@ dependencies = [
"quinn-udp", "quinn-udp",
"rustc-hash", "rustc-hash",
"rustls", "rustls",
"socket2", "socket2 0.6.4",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"tracing", "tracing",
@ -3565,7 +3566,7 @@ dependencies = [
"cfg_aliases 0.2.1", "cfg_aliases 0.2.1",
"libc", "libc",
"once_cell", "once_cell",
"socket2", "socket2 0.6.4",
"tracing", "tracing",
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
@ -4456,6 +4457,16 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.6.4" version = "0.6.4"
@ -5080,7 +5091,7 @@ dependencies = [
"parking_lot 0.12.5", "parking_lot 0.12.5",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"socket2", "socket2 0.6.4",
"tokio-macros", "tokio-macros",
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
@ -5126,7 +5137,7 @@ dependencies = [
"postgres-protocol", "postgres-protocol",
"postgres-types", "postgres-types",
"rand 0.10.1", "rand 0.10.1",
"socket2", "socket2 0.6.4",
"tokio", "tokio",
"tokio-util", "tokio-util",
"whoami", "whoami",

View File

@ -391,8 +391,8 @@
</div> </div>
</div> </div>
<div class="form-actions" data-transfer-actions> <div class="form-actions" data-transfer-actions>
<button class="secondary-button" type="button" data-transfer-save>Save Transaction</button> <button class="secondary-button" type="button" data-transfer-save>Sign and Save</button>
<button class="primary-button" type="button" data-transfer-broadcast>Broadcast Transaction</button> <button class="primary-button" type="button" data-transfer-broadcast>Sign and Broadcast</button>
</div> </div>
<div class="startup-message" data-transfer-message></div> <div class="startup-message" data-transfer-message></div>
@ -420,8 +420,8 @@
</div> </div>
</div> </div>
<div class="form-actions hidden" data-create-token-actions> <div class="form-actions hidden" data-create-token-actions>
<button class="secondary-button" type="button" data-create-token-save>Save Transaction</button> <button class="secondary-button" type="button" data-create-token-save>Sign and Save</button>
<button class="primary-button" type="button" data-create-token-broadcast>Broadcast Transaction</button> <button class="primary-button" type="button" data-create-token-broadcast>Sign and Broadcast</button>
</div> </div>
<div class="startup-message hidden" data-create-token-message></div> <div class="startup-message hidden" data-create-token-message></div>
@ -447,8 +447,8 @@
</div> </div>
</div> </div>
<div class="form-actions hidden" data-issue-token-actions> <div class="form-actions hidden" data-issue-token-actions>
<button class="secondary-button" type="button" data-issue-token-save>Save Transaction</button> <button class="secondary-button" type="button" data-issue-token-save>Sign and Save</button>
<button class="primary-button" type="button" data-issue-token-broadcast>Broadcast Transaction</button> <button class="primary-button" type="button" data-issue-token-broadcast>Sign and Broadcast</button>
</div> </div>
<div class="startup-message hidden" data-issue-token-message></div> <div class="startup-message hidden" data-issue-token-message></div>
@ -467,6 +467,13 @@
<span>NFT Name</span> <span>NFT Name</span>
<input data-create-nft-name maxlength="15" placeholder="3 to 15 characters"> <input data-create-nft-name maxlength="15" placeholder="3 to 15 characters">
</label> </label>
<label>
<span>Ownership</span>
<select data-create-nft-ownership>
<option value="0">Indivisible</option>
<option value="1">Fractional</option>
</select>
</label>
<label> <label>
<span>IPFS CID</span> <span>IPFS CID</span>
<input data-create-nft-ipfs maxlength="100" placeholder="CIDv0 or CIDv1"> <input data-create-nft-ipfs maxlength="100" placeholder="CIDv0 or CIDv1">
@ -491,8 +498,8 @@
</div> </div>
</div> </div>
<div class="form-actions hidden" data-create-nft-actions> <div class="form-actions hidden" data-create-nft-actions>
<button class="secondary-button" type="button" data-create-nft-save>Save Transaction</button> <button class="secondary-button" type="button" data-create-nft-save>Sign and Save</button>
<button class="primary-button" type="button" data-create-nft-broadcast>Broadcast Transaction</button> <button class="primary-button" type="button" data-create-nft-broadcast>Sign and Broadcast</button>
</div> </div>
<div class="startup-message hidden" data-create-nft-message></div> <div class="startup-message hidden" data-create-nft-message></div>
@ -675,8 +682,8 @@
</div> </div>
</div> </div>
<div class="form-actions hidden" data-loan-payment-actions> <div class="form-actions hidden" data-loan-payment-actions>
<button class="secondary-button" type="button" data-loan-payment-save>Save Transaction</button> <button class="secondary-button" type="button" data-loan-payment-save>Sign and Save</button>
<button class="primary-button" type="button" data-loan-payment-broadcast>Broadcast Transaction</button> <button class="primary-button" type="button" data-loan-payment-broadcast>Sign and Broadcast</button>
</div> </div>
<div class="startup-message hidden" data-loan-payment-message></div> <div class="startup-message hidden" data-loan-payment-message></div>
@ -703,8 +710,8 @@
</div> </div>
</div> </div>
<div class="form-actions hidden" data-collateral-claim-actions> <div class="form-actions hidden" data-collateral-claim-actions>
<button class="secondary-button" type="button" data-collateral-claim-save>Save Transaction</button> <button class="secondary-button" type="button" data-collateral-claim-save>Sign and Save</button>
<button class="primary-button" type="button" data-collateral-claim-broadcast>Broadcast Transaction</button> <button class="primary-button" type="button" data-collateral-claim-broadcast>Sign and Broadcast</button>
</div> </div>
<div class="startup-message hidden" data-collateral-claim-message></div> <div class="startup-message hidden" data-collateral-claim-message></div>
@ -729,8 +736,8 @@
</div> </div>
</div> </div>
<div class="form-actions hidden" data-burn-actions> <div class="form-actions hidden" data-burn-actions>
<button class="secondary-button" type="button" data-burn-save>Save Transaction</button> <button class="secondary-button" type="button" data-burn-save>Sign and Save</button>
<button class="primary-button" type="button" data-burn-broadcast>Broadcast Transaction</button> <button class="primary-button" type="button" data-burn-broadcast>Sign and Broadcast</button>
</div> </div>
<div class="startup-message hidden" data-burn-message></div> <div class="startup-message hidden" data-burn-message></div>
@ -781,8 +788,8 @@
</div> </div>
</div> </div>
<div class="form-actions hidden" data-marketing-actions> <div class="form-actions hidden" data-marketing-actions>
<button class="secondary-button" type="button" data-marketing-save>Save Transaction</button> <button class="secondary-button" type="button" data-marketing-save>Sign and Save</button>
<button class="primary-button" type="button" data-marketing-broadcast>Broadcast Transaction</button> <button class="primary-button" type="button" data-marketing-broadcast>Sign and Broadcast</button>
</div> </div>
<div class="startup-message hidden" data-marketing-message></div> <div class="startup-message hidden" data-marketing-message></div>
@ -833,8 +840,8 @@
</div> </div>
</div> </div>
<div class="form-actions hidden" data-data-storage-actions> <div class="form-actions hidden" data-data-storage-actions>
<button class="secondary-button" type="button" data-data-storage-save>Save Transaction</button> <button class="secondary-button" type="button" data-data-storage-save>Sign and Save</button>
<button class="primary-button" type="button" data-data-storage-broadcast>Broadcast Transaction</button> <button class="primary-button" type="button" data-data-storage-broadcast>Sign and Broadcast</button>
</div> </div>
<div class="startup-message hidden" data-data-storage-message></div> <div class="startup-message hidden" data-data-storage-message></div>
@ -857,8 +864,8 @@
</div> </div>
</div> </div>
<div class="form-actions hidden" data-vanity-actions> <div class="form-actions hidden" data-vanity-actions>
<button class="secondary-button" type="button" data-vanity-save>Save Transaction</button> <button class="secondary-button" type="button" data-vanity-save>Sign and Save</button>
<button class="primary-button" type="button" data-vanity-broadcast>Broadcast Transaction</button> <button class="primary-button" type="button" data-vanity-broadcast>Sign and Broadcast</button>
</div> </div>
<div class="startup-message hidden" data-vanity-message></div> <div class="startup-message hidden" data-vanity-message></div>
</form> </form>

View File

@ -105,6 +105,7 @@ const issueTokenMessage = document.querySelector("[data-issue-token-message]");
const createNftForm = document.querySelector("[data-create-nft-form]"); const createNftForm = document.querySelector("[data-create-nft-form]");
const createNftActions = document.querySelector("[data-create-nft-actions]"); const createNftActions = document.querySelector("[data-create-nft-actions]");
const createNftSeriesSelect = document.querySelector("[data-create-nft-series]"); const createNftSeriesSelect = document.querySelector("[data-create-nft-series]");
const createNftOwnershipSelect = document.querySelector("[data-create-nft-ownership]");
const createNftNameInput = document.querySelector("[data-create-nft-name]"); const createNftNameInput = document.querySelector("[data-create-nft-name]");
const createNftIpfsInput = document.querySelector("[data-create-nft-ipfs]"); const createNftIpfsInput = document.querySelector("[data-create-nft-ipfs]");
const createNftCountInput = document.querySelector("[data-create-nft-count]"); const createNftCountInput = document.querySelector("[data-create-nft-count]");

View File

@ -32,6 +32,8 @@ function selectedBurnAsset() {
return { return {
asset: option.dataset.asset || "", asset: option.dataset.asset || "",
nftSeries: Number(option.dataset.nftSeries || 0), nftSeries: Number(option.dataset.nftSeries || 0),
ownershipType: Number(option.dataset.ownershipType || -1),
isNft: option.dataset.isNft === "true",
balance: BigInt(option.dataset.balance || "0") balance: BigInt(option.dataset.balance || "0")
}; };
} }
@ -55,6 +57,9 @@ function populateBurnAssets() {
option.dataset.asset = balance.asset || ""; option.dataset.asset = balance.asset || "";
option.dataset.nftSeries = String(balance.nft_series || 0); option.dataset.nftSeries = String(balance.nft_series || 0);
option.dataset.balance = String(balance.balance || "0"); option.dataset.balance = String(balance.balance || "0");
const metadata = nftOwnershipMetadata(balance.asset, balance.nft_series);
option.dataset.isNft = String(Boolean(metadata));
option.dataset.ownershipType = String(metadata?.ownershipType ?? -1);
option.textContent = burnAssetOptionLabel(balance); option.textContent = burnAssetOptionLabel(balance);
burnAssetSelect.appendChild(option); burnAssetSelect.appendChild(option);
}); });
@ -70,7 +75,7 @@ function populateBurnAssets() {
} }
async function ensureBurnBalances() { async function ensureBurnBalances() {
if (activeWalletBalances.length || !walletLoaded || !registrationRegistered) { if (!walletLoaded || !registrationRegistered) {
return; return;
} }
const invoke = window.__TAURI__?.core?.invoke; const invoke = window.__TAURI__?.core?.invoke;
@ -78,8 +83,13 @@ async function ensureBurnBalances() {
if (!invoke || !broadcastNode) { if (!invoke || !broadcastNode) {
return; return;
} }
burnBalancesRefreshPromise ||= invoke("get_active_wallet_balances", { broadcastNode }) burnBalancesRefreshPromise ||= Promise.all([
.then((balances) => { activeWalletBalances.length
? Promise.resolve(activeWalletBalances)
: invoke("get_active_wallet_balances", { broadcastNode }),
ensureNftOwnershipRegistry()
])
.then(([balances]) => {
activeWalletBalances = Array.isArray(balances) ? balances : []; activeWalletBalances = Array.isArray(balances) ? balances : [];
}) })
.finally(() => { .finally(() => {
@ -113,16 +123,19 @@ function updateBurnDerivedFields() {
return; return;
} }
const isNft = selected.nftSeries > 0; const isNft = selected.isNft;
const isIndivisible = isNft && selected.ownershipType === 0;
if (burnAvailable) { if (burnAvailable) {
burnAvailable.textContent = `${atomicToDecimal(selected.balance)} ${selected.asset}`; burnAvailable.textContent = `${atomicToDecimal(selected.balance)} ${selected.asset}`;
} }
if (burnType) { if (burnType) {
burnType.textContent = isNft ? `NFT #${selected.nftSeries}` : "Token"; burnType.textContent = isNft
? (selected.nftSeries > 0 ? `NFT #${selected.nftSeries}` : "NFT / RWA")
: "Token";
} }
if (burnAmountInput) { if (burnAmountInput) {
burnAmountInput.disabled = isNft; burnAmountInput.disabled = isIndivisible;
if (isNft) { if (isIndivisible) {
burnAmountInput.value = "1.00000000"; burnAmountInput.value = "1.00000000";
} }
} }
@ -143,7 +156,7 @@ function validateBurnForm() {
if (amount === null || amount <= 0n) { if (amount === null || amount <= 0n) {
return "Enter a valid burn amount."; return "Enter a valid burn amount.";
} }
if (selected.nftSeries > 0 && amount !== ATOMIC_UNITS_PER_COIN) { if (selected.isNft && selected.ownershipType === 0 && amount !== ATOMIC_UNITS_PER_COIN) {
return "NFT burns must destroy exactly 1.00000000 units."; return "NFT burns must destroy exactly 1.00000000 units.";
} }
if (amount > selected.balance) { if (amount > selected.balance) {
@ -209,7 +222,7 @@ async function handleBurnAction(action) {
setBurnButtonsDisabled(true); setBurnButtonsDisabled(true);
try { try {
if (action === "save") { if (action === "save") {
setBurnMessage("Saving burn transaction..."); setBurnMessage("Signing and saving burn transaction...");
const review = await invoke("save_burn_transaction", { const review = await invoke("save_burn_transaction", {
...burnPayload(), ...burnPayload(),
walletKey walletKey
@ -224,7 +237,7 @@ async function handleBurnAction(action) {
if (!broadcastNode) { if (!broadcastNode) {
throw new Error("Select a broadcast node before broadcasting."); throw new Error("Select a broadcast node before broadcasting.");
} }
setBurnMessage("Broadcasting burn transaction..."); setBurnMessage("Signing and broadcasting burn transaction...");
const result = await invoke("broadcast_burn_transaction", { const result = await invoke("broadcast_burn_transaction", {
...burnPayload(), ...burnPayload(),
broadcastNode, broadcastNode,
@ -233,6 +246,12 @@ async function handleBurnAction(action) {
if (typeof invalidateAssetsCatalog === "function") { if (typeof invalidateAssetsCatalog === "function") {
invalidateAssetsCatalog(); invalidateAssetsCatalog();
} }
if (typeof invalidateDashboardNftBalanceKeys === "function") {
invalidateDashboardNftBalanceKeys();
}
if (typeof resetNftsPage === "function") {
resetNftsPage();
}
activeWalletBalances = []; activeWalletBalances = [];
clearBurnForm(); clearBurnForm();
setBurnMessage(result.response || "Burn transaction broadcast.", "success"); setBurnMessage(result.response || "Burn transaction broadcast.", "success");

View File

@ -167,7 +167,7 @@ async function handleCollateralClaimAction(action) {
setCollateralClaimButtonsDisabled(true); setCollateralClaimButtonsDisabled(true);
try { try {
if (action === "save") { if (action === "save") {
setCollateralClaimMessage("Saving collateral claim transaction..."); setCollateralClaimMessage("Signing and saving collateral claim transaction...");
const review = await invoke("save_collateral_claim_transaction", { const review = await invoke("save_collateral_claim_transaction", {
...collateralClaimPayload(), ...collateralClaimPayload(),
walletKey walletKey
@ -178,7 +178,7 @@ async function handleCollateralClaimAction(action) {
refreshUnbroadcastTransactions(); refreshUnbroadcastTransactions();
} }
} else { } else {
setCollateralClaimMessage("Broadcasting collateral claim..."); setCollateralClaimMessage("Signing and broadcasting collateral claim...");
const result = await invoke("broadcast_collateral_claim_transaction", { const result = await invoke("broadcast_collateral_claim_transaction", {
...collateralClaimPayload(), ...collateralClaimPayload(),
walletKey walletKey

View File

@ -9,6 +9,10 @@ function createNftSeriesValue() {
return Number(createNftSeriesSelect?.value || 0) === 1 ? 1 : 0; return Number(createNftSeriesSelect?.value || 0) === 1 ? 1 : 0;
} }
function createNftOwnershipValue() {
return Number(createNftOwnershipSelect?.value || 0) === 1 ? 1 : 0;
}
function normalizeCreateNftCountInput() { function normalizeCreateNftCountInput() {
const isCollection = createNftSeriesValue() === 1; const isCollection = createNftSeriesValue() === 1;
if (createNftCountInput) { if (createNftCountInput) {
@ -20,6 +24,12 @@ function normalizeCreateNftCountInput() {
createNftCountInput.value = "2"; createNftCountInput.value = "2";
} }
} }
if (createNftOwnershipSelect) {
createNftOwnershipSelect.disabled = isCollection;
if (isCollection) {
createNftOwnershipSelect.value = "0";
}
}
} }
function updateCreateNftDerivedFields({ clampCount = false } = {}) { function updateCreateNftDerivedFields({ clampCount = false } = {}) {
@ -108,6 +118,7 @@ function validateCreateNftForm() {
function createNftPayload() { function createNftPayload() {
return { return {
series: createNftSeriesValue(), series: createNftSeriesValue(),
ownershipType: createNftOwnershipValue(),
nftName: String(createNftNameInput?.value || "").trim(), nftName: String(createNftNameInput?.value || "").trim(),
itemIpfs: String(createNftIpfsInput?.value || "").trim(), itemIpfs: String(createNftIpfsInput?.value || "").trim(),
count: Number.parseInt(createNftCountInput?.value || "1", 10), count: Number.parseInt(createNftCountInput?.value || "1", 10),
@ -120,6 +131,9 @@ function clearCreateNftForm() {
if (createNftSeriesSelect) { if (createNftSeriesSelect) {
createNftSeriesSelect.value = "0"; createNftSeriesSelect.value = "0";
} }
if (createNftOwnershipSelect) {
createNftOwnershipSelect.value = "0";
}
if (createNftNameInput) { if (createNftNameInput) {
createNftNameInput.value = ""; createNftNameInput.value = "";
} }
@ -181,7 +195,7 @@ async function handleCreateNftAction(action) {
setCreateNftButtonsDisabled(true); setCreateNftButtonsDisabled(true);
try { try {
if (action === "save") { if (action === "save") {
setCreateNftMessage("Saving NFT transaction..."); setCreateNftMessage("Signing and saving NFT transaction...");
const review = await invoke("save_create_nft_transaction", { const review = await invoke("save_create_nft_transaction", {
...createNftPayload(), ...createNftPayload(),
walletKey walletKey
@ -196,7 +210,7 @@ async function handleCreateNftAction(action) {
if (!broadcastNode) { if (!broadcastNode) {
throw new Error("Select a broadcast node before broadcasting."); throw new Error("Select a broadcast node before broadcasting.");
} }
setCreateNftMessage("Broadcasting NFT transaction..."); setCreateNftMessage("Signing and broadcasting NFT transaction...");
const result = await invoke("broadcast_create_nft_transaction", { const result = await invoke("broadcast_create_nft_transaction", {
...createNftPayload(), ...createNftPayload(),
broadcastNode, broadcastNode,
@ -230,6 +244,7 @@ createNftSeriesSelect?.addEventListener("change", () => {
updateCreateNftDerivedFields({ clampCount: true }); updateCreateNftDerivedFields({ clampCount: true });
setCreateNftMessage(""); setCreateNftMessage("");
}); });
createNftOwnershipSelect?.addEventListener("change", () => setCreateNftMessage(""));
createNftCountInput?.addEventListener("input", () => { createNftCountInput?.addEventListener("input", () => {
updateCreateNftDerivedFields(); updateCreateNftDerivedFields();
setCreateNftMessage(""); setCreateNftMessage("");

View File

@ -118,7 +118,7 @@ async function handleCreateTokenAction(action) {
setCreateTokenButtonsDisabled(true); setCreateTokenButtonsDisabled(true);
try { try {
if (action === "save") { if (action === "save") {
setCreateTokenMessage("Saving token transaction..."); setCreateTokenMessage("Signing and saving token transaction...");
const review = await invoke("save_create_token_transaction", { const review = await invoke("save_create_token_transaction", {
...createTokenPayload(), ...createTokenPayload(),
walletKey walletKey
@ -133,7 +133,7 @@ async function handleCreateTokenAction(action) {
if (!broadcastNode) { if (!broadcastNode) {
throw new Error("Select a broadcast node before broadcasting."); throw new Error("Select a broadcast node before broadcasting.");
} }
setCreateTokenMessage("Broadcasting token transaction..."); setCreateTokenMessage("Signing and broadcasting token transaction...");
const result = await invoke("broadcast_create_token_transaction", { const result = await invoke("broadcast_create_token_transaction", {
...createTokenPayload(), ...createTokenPayload(),
broadcastNode, broadcastNode,

View File

@ -371,7 +371,7 @@ async function handleDataStorageAction(action) {
setDataStorageButtonsDisabled(true); setDataStorageButtonsDisabled(true);
try { try {
if (action === "save") { if (action === "save") {
setDataStorageMessage("Saving data storage transaction..."); setDataStorageMessage("Signing and saving data storage transaction...");
const review = await invoke("save_data_storage_transaction", { const review = await invoke("save_data_storage_transaction", {
...dataStoragePayload(), ...dataStoragePayload(),
walletKey walletKey
@ -386,7 +386,7 @@ async function handleDataStorageAction(action) {
if (!broadcastNode) { if (!broadcastNode) {
throw new Error("Select a broadcast node before broadcasting."); throw new Error("Select a broadcast node before broadcasting.");
} }
setDataStorageMessage("Broadcasting data storage transaction..."); setDataStorageMessage("Signing and broadcasting data storage transaction...");
const result = await invoke("broadcast_data_storage_transaction", { const result = await invoke("broadcast_data_storage_transaction", {
...dataStoragePayload(), ...dataStoragePayload(),
broadcastNode, broadcastNode,

View File

@ -222,7 +222,7 @@ async function handleIssueTokenAction(action) {
setIssueTokenButtonsDisabled(true); setIssueTokenButtonsDisabled(true);
try { try {
if (action === "save") { if (action === "save") {
setIssueTokenMessage("Saving issue-token transaction..."); setIssueTokenMessage("Signing and saving issue-token transaction...");
const review = await invoke("save_issue_token_transaction", { const review = await invoke("save_issue_token_transaction", {
...issueTokenPayload(), ...issueTokenPayload(),
walletKey walletKey
@ -233,7 +233,7 @@ async function handleIssueTokenAction(action) {
refreshUnbroadcastTransactions(); refreshUnbroadcastTransactions();
} }
} else { } else {
setIssueTokenMessage("Broadcasting issue-token transaction..."); setIssueTokenMessage("Signing and broadcasting issue-token transaction...");
const result = await invoke("broadcast_issue_token_transaction", { const result = await invoke("broadcast_issue_token_transaction", {
...issueTokenPayload(), ...issueTokenPayload(),
walletKey walletKey

View File

@ -32,6 +32,7 @@ function selectedLoanCollateral() {
asset: option.dataset.asset || "", asset: option.dataset.asset || "",
kind: option.dataset.kind || "token", kind: option.dataset.kind || "token",
series: Number(option.dataset.series || 0), series: Number(option.dataset.series || 0),
ownershipType: Number(option.dataset.ownershipType || -1),
wireName: option.dataset.wireName || option.dataset.asset || "" wireName: option.dataset.wireName || option.dataset.asset || ""
}; };
} }
@ -120,6 +121,7 @@ function populateLoanCollateralSelect() {
option.dataset.asset = asset.asset; option.dataset.asset = asset.asset;
option.dataset.kind = asset.kind; option.dataset.kind = asset.kind;
option.dataset.series = String(asset.series || 0); option.dataset.series = String(asset.series || 0);
option.dataset.ownershipType = String(asset.ownershipType ?? -1);
option.dataset.wireName = asset.wireName; option.dataset.wireName = asset.wireName;
loanCollateralSelect.appendChild(option); loanCollateralSelect.appendChild(option);
}); });
@ -167,6 +169,7 @@ async function loadLoanOfferAssets() {
const tokenRows = Array.isArray(tokens) ? tokens : []; const tokenRows = Array.isArray(tokens) ? tokens : [];
const nftRows = Array.isArray(nfts) ? nfts : []; const nftRows = Array.isArray(nfts) ? nfts : [];
loanNftKeys = new Set(nftRows.map((nft) => loanNftKey(nft.nft_name, nft.series))); loanNftKeys = new Set(nftRows.map((nft) => loanNftKey(nft.nft_name, nft.series)));
updateNftOwnershipRegistry(nftRows, key);
loanCollateralAssets = [ loanCollateralAssets = [
{ {
@ -193,7 +196,10 @@ async function loadLoanOfferAssets() {
wireName: series > 0 ? `${asset}_${series}` : asset, wireName: series > 0 ? `${asset}_${series}` : asset,
series, series,
kind: "nft", kind: "nft",
label: series > 0 ? `${asset} #${series} (NFT)` : `${asset} (NFT)` ownershipType: Number(nft.ownership_type),
label: series > 0
? `${asset} #${series} (NFT)`
: `${asset} (${Number(nft.ownership_type) === 1 ? "Fractional NFT / RWA" : "NFT"})`
}; };
}) })
].filter((asset) => { ].filter((asset) => {
@ -229,7 +235,7 @@ function updateLoanOfferDerivedFields() {
if (loanFeeInput) { if (loanFeeInput) {
loanFeeInput.value = atomicToDecimal(LOAN_FEE_ATOMIC); loanFeeInput.value = atomicToDecimal(LOAN_FEE_ATOMIC);
} }
if (collateral?.kind === "nft") { if (collateral?.kind === "nft" && collateral.ownershipType === 0) {
loanCollateralAmountInput.value = "1"; loanCollateralAmountInput.value = "1";
loanCollateralAmountInput.disabled = true; loanCollateralAmountInput.disabled = true;
} else if (loanCollateralAmountInput) { } else if (loanCollateralAmountInput) {
@ -273,9 +279,7 @@ function validateLoanOfferForm() {
} }
const loanAmount = decimalToAtomic(loanAmountInput?.value); const loanAmount = decimalToAtomic(loanAmountInput?.value);
const collateralAmount = collateral.kind === "nft" const collateralAmount = decimalToAtomic(loanCollateralAmountInput?.value);
? ATOMIC_UNITS_PER_COIN
: decimalToAtomic(loanCollateralAmountInput?.value);
const paymentAmount = decimalToAtomic(loanPaymentAmountInput?.value); const paymentAmount = decimalToAtomic(loanPaymentAmountInput?.value);
const maxLateValue = decimalToAtomic(loanMaxLateValueInput?.value); const maxLateValue = decimalToAtomic(loanMaxLateValueInput?.value);
if (loanAmount === null || loanAmount <= 0n) { if (loanAmount === null || loanAmount <= 0n) {
@ -284,6 +288,13 @@ function validateLoanOfferForm() {
if (collateralAmount === null || collateralAmount <= 0n) { if (collateralAmount === null || collateralAmount <= 0n) {
return "Enter a valid collateral amount."; return "Enter a valid collateral amount.";
} }
if (
collateral.kind === "nft"
&& collateral.ownershipType === 0
&& collateralAmount !== ATOMIC_UNITS_PER_COIN
) {
return "Indivisible NFT collateral must be exactly 1.";
}
if (paymentAmount === null || paymentAmount <= 0n) { if (paymentAmount === null || paymentAmount <= 0n) {
return "Enter a valid payment amount."; return "Enter a valid payment amount.";
} }
@ -330,9 +341,7 @@ function validateLoanOfferForm() {
function loanOfferPayload() { function loanOfferPayload() {
const loanAsset = selectedLoanAsset(); const loanAsset = selectedLoanAsset();
const collateral = selectedLoanCollateral(); const collateral = selectedLoanCollateral();
const collateralAmount = collateral.kind === "nft" const collateralAmount = decimalToAtomic(loanCollateralAmountInput.value);
? ATOMIC_UNITS_PER_COIN
: decimalToAtomic(loanCollateralAmountInput.value);
return { return {
broadcastNode: activeBroadcastNode(), broadcastNode: activeBroadcastNode(),
loanCoin: loanAsset.asset, loanCoin: loanAsset.asset,

View File

@ -227,7 +227,7 @@ async function handleLoanPaymentAction(action) {
setLoanPaymentButtonsDisabled(true); setLoanPaymentButtonsDisabled(true);
try { try {
if (action === "save") { if (action === "save") {
setLoanPaymentMessage("Saving loan payment transaction..."); setLoanPaymentMessage("Signing and saving loan payment transaction...");
const review = await invoke("save_loan_payment_transaction", { const review = await invoke("save_loan_payment_transaction", {
...loanPaymentPayload(), ...loanPaymentPayload(),
walletKey walletKey
@ -238,7 +238,7 @@ async function handleLoanPaymentAction(action) {
refreshUnbroadcastTransactions(); refreshUnbroadcastTransactions();
} }
} else { } else {
setLoanPaymentMessage("Broadcasting loan payment..."); setLoanPaymentMessage("Signing and broadcasting loan payment...");
const result = await invoke("broadcast_loan_payment_transaction", { const result = await invoke("broadcast_loan_payment_transaction", {
...loanPaymentPayload(), ...loanPaymentPayload(),
walletKey walletKey

View File

@ -126,7 +126,7 @@ async function handleMarketingAction(action) {
setMarketingButtonsDisabled(true); setMarketingButtonsDisabled(true);
try { try {
if (action === "save") { if (action === "save") {
setMarketingMessage("Saving marketing transaction..."); setMarketingMessage("Signing and saving marketing transaction...");
const review = await invoke("save_marketing_transaction", { const review = await invoke("save_marketing_transaction", {
...marketingPayload(), ...marketingPayload(),
walletKey walletKey
@ -141,7 +141,7 @@ async function handleMarketingAction(action) {
if (!broadcastNode) { if (!broadcastNode) {
throw new Error("Select a broadcast node before broadcasting."); throw new Error("Select a broadcast node before broadcasting.");
} }
setMarketingMessage("Broadcasting marketing transaction..."); setMarketingMessage("Signing and broadcasting marketing transaction...");
const result = await invoke("broadcast_marketing_transaction", { const result = await invoke("broadcast_marketing_transaction", {
...marketingPayload(), ...marketingPayload(),
broadcastNode, broadcastNode,

View File

@ -165,7 +165,9 @@ function ownedNftRowsFromBalances(balances, registry) {
nft_name: nftName, nft_name: nftName,
series, series,
balance: String(balance.balance || "0"), balance: String(balance.balance || "0"),
origin_txid: nft.origin_txid || "" origin_txid: nft.origin_txid || "",
ownership_type: Number(nft.ownership_type || 0),
remaining_supply: String(nft.remaining_supply || "0")
}; };
}) })
.filter(Boolean) .filter(Boolean)
@ -197,12 +199,29 @@ function metadataDescription(metadata) {
return String(metadata?.description || metadata?.summary || "").trim(); return String(metadata?.description || metadata?.summary || "").trim();
} }
function nftOwnershipPercent(balance, remainingSupply) {
const atomic = BigInt(String(balance || "0"));
const supply = BigInt(String(remainingSupply || "0"));
if (supply <= 0n) {
return "0%";
}
const scaledPercent = atomic * 100000000n / supply;
const whole = scaledPercent / 1000000n;
const fraction = String(scaledPercent % 1000000n).padStart(6, "0").replace(/0+$/, "");
return fraction ? `${whole}.${fraction}%` : `${whole}%`;
}
function renderNftCard(nft) { function renderNftCard(nft) {
const key = nftKey(nft); const key = nftKey(nft);
const name = nftDisplayName(nft); const name = nftDisplayName(nft);
const image = nftImageUri(nft); const image = nftImageUri(nft);
const type = nftTypeLabel(nft); const type = nftTypeLabel(nft);
const active = key === selectedNftKey ? " active" : ""; const active = key === selectedNftKey ? " active" : "";
const fractional = Number(nft.ownership_type || 0) === 1;
const ownershipAtomic = BigInt(String(nft.balance || "0"));
const ownershipLabel = fractional
? `${nftOwnershipPercent(ownershipAtomic, nft.remaining_supply)} ownership`
: "Complete ownership";
const preview = image && nft.gateway const preview = image && nft.gateway
? `<img src="${escapeNftText(ipfsToGatewayUrl(image, nft.gateway))}" alt="">` ? `<img src="${escapeNftText(ipfsToGatewayUrl(image, nft.gateway))}" alt="">`
: `<div class="asset-mark art">${escapeNftText(assetInitial(name))}</div>`; : `<div class="asset-mark art">${escapeNftText(assetInitial(name))}</div>`;
@ -214,6 +233,7 @@ function renderNftCard(nft) {
<div> <div>
<strong>${escapeNftText(name)}</strong> <strong>${escapeNftText(name)}</strong>
<span>${escapeNftText(type)} - ${escapeNftText(nft.asset_name || `${nft.nft_name}_${nft.series}`)}</span> <span>${escapeNftText(type)} - ${escapeNftText(nft.asset_name || `${nft.nft_name}_${nft.series}`)}</span>
<span>${escapeNftText(ownershipLabel)}</span>
</div> </div>
</div> </div>
<div class="nft-link-row"> <div class="nft-link-row">

View File

@ -1,7 +1,7 @@
const SWAP_FEE_ATOMIC = 100000000n; const SWAP_FEE_ATOMIC = 100000000n;
let swapOfferLoadPromise = null; let swapOfferLoadPromise = null;
let swapNetworkAssets = []; let swapNetworkAssets = [];
let swapNftKeys = new Set(); let swapNftMetadata = new Map();
let swapOfferAssetsKey = ""; let swapOfferAssetsKey = "";
function setSwapOfferMessage(message, type = "") { function setSwapOfferMessage(message, type = "") {
@ -18,10 +18,14 @@ function swapAssetLabel(asset, includeBalance = false) {
? `${asset.asset} #${series}` ? `${asset.asset} #${series}`
: asset.asset; : asset.asset;
if (!includeBalance) { if (!includeBalance) {
return asset.kind === "nft" ? `${name} (NFT)` : name; return asset.kind === "nft"
? `${name} (${asset.ownershipType === 1 ? "Fractional NFT / RWA" : "NFT"})`
: name;
} }
if (asset.kind === "nft") { if (asset.kind === "nft") {
return `${name} (1 item)`; return asset.ownershipType === 1
? `${name} (${formatAtomicBalance(String(asset.balance || "0"))})`
: `${name} (1 item)`;
} }
return `${name} (${formatAtomicBalance(String(asset.balance || "0"))})`; return `${name} (${formatAtomicBalance(String(asset.balance || "0"))})`;
} }
@ -35,6 +39,7 @@ function selectedSwapAsset(select) {
asset: option.dataset.asset || "", asset: option.dataset.asset || "",
nftSeries: Number(option.dataset.nftSeries || 0), nftSeries: Number(option.dataset.nftSeries || 0),
kind: option.dataset.kind || "token", kind: option.dataset.kind || "token",
ownershipType: Number(option.dataset.ownershipType || -1),
balance: BigInt(option.dataset.balance || "0") balance: BigInt(option.dataset.balance || "0")
}; };
} }
@ -59,6 +64,7 @@ function populateSwapAssetSelect(select, assets, includeBalance) {
option.dataset.asset = asset.asset; option.dataset.asset = asset.asset;
option.dataset.nftSeries = String(Number(asset.nftSeries || 0)); option.dataset.nftSeries = String(Number(asset.nftSeries || 0));
option.dataset.kind = asset.kind; option.dataset.kind = asset.kind;
option.dataset.ownershipType = String(asset.ownershipType ?? -1);
option.dataset.balance = String(asset.balance || "0"); option.dataset.balance = String(asset.balance || "0");
select.appendChild(option); select.appendChild(option);
}); });
@ -73,9 +79,10 @@ function populateSwapAssetSelect(select, assets, includeBalance) {
} }
function swapRegistryNftKeys(nfts) { function swapRegistryNftKeys(nfts) {
return new Set(nfts.map((nft) => { return new Map(nfts.map((nft) => [
return `${String(nft.nft_name || "").trim().toLowerCase()}|${Number(nft.series || 0)}`; nftRegistryKey(nft.nft_name, nft.series),
})); Number(nft.ownership_type)
]));
} }
function refreshSwapOfferSelects() { function refreshSwapOfferSelects() {
@ -84,11 +91,13 @@ function refreshSwapOfferSelects() {
.map((balance) => { .map((balance) => {
const asset = String(balance.asset || "").trim(); const asset = String(balance.asset || "").trim();
const nftSeries = Number(balance.nft_series || 0); const nftSeries = Number(balance.nft_series || 0);
const isNft = swapNftKeys.has(`${asset.toLowerCase()}|${nftSeries}`); const ownershipType = swapNftMetadata.get(nftRegistryKey(asset, nftSeries));
const isNft = ownershipType !== undefined;
return { return {
asset, asset,
nftSeries, nftSeries,
kind: isNft ? "nft" : (isBaseTransferAsset(asset) ? "coin" : "token"), kind: isNft ? "nft" : (isBaseTransferAsset(asset) ? "coin" : "token"),
ownershipType: isNft ? ownershipType : -1,
balance: String(balance.balance || "0") balance: String(balance.balance || "0")
}; };
}); });
@ -125,7 +134,8 @@ async function loadSwapOfferAssets() {
activeWalletBalances = Array.isArray(balances) ? balances : []; activeWalletBalances = Array.isArray(balances) ? balances : [];
const tokenRows = Array.isArray(tokens) ? tokens : []; const tokenRows = Array.isArray(tokens) ? tokens : [];
const nftRows = Array.isArray(nfts) ? nfts : []; const nftRows = Array.isArray(nfts) ? nfts : [];
swapNftKeys = swapRegistryNftKeys(nftRows); swapNftMetadata = swapRegistryNftKeys(nftRows);
updateNftOwnershipRegistry(nftRows, assetsKey);
swapNetworkAssets = [ swapNetworkAssets = [
{ asset: activeBaseCoin(), nftSeries: 0, kind: "coin", balance: "0" }, { asset: activeBaseCoin(), nftSeries: 0, kind: "coin", balance: "0" },
@ -133,12 +143,14 @@ async function loadSwapOfferAssets() {
asset: String(token.ticker || "").trim(), asset: String(token.ticker || "").trim(),
nftSeries: 0, nftSeries: 0,
kind: "token", kind: "token",
ownershipType: -1,
balance: "0" balance: "0"
})), })),
...nftRows.map((nft) => ({ ...nftRows.map((nft) => ({
asset: String(nft.nft_name || "").trim(), asset: String(nft.nft_name || "").trim(),
nftSeries: Number(nft.series || 0), nftSeries: Number(nft.series || 0),
kind: "nft", kind: "nft",
ownershipType: Number(nft.ownership_type),
balance: "0" balance: "0"
})) }))
]; ];
@ -164,7 +176,7 @@ function updateSwapSide(asset, amountInput, tipInput) {
tipInput.value = ""; tipInput.value = "";
return; return;
} }
if (asset.kind === "nft") { if (asset.kind === "nft" && asset.ownershipType === 0) {
amountInput.value = "1"; amountInput.value = "1";
amountInput.disabled = true; amountInput.disabled = true;
tipInput.value = "0"; tipInput.value = "0";
@ -172,7 +184,10 @@ function updateSwapSide(asset, amountInput, tipInput) {
return; return;
} }
amountInput.disabled = false; amountInput.disabled = false;
tipInput.disabled = false; tipInput.disabled = asset.kind === "nft";
if (asset.kind === "nft") {
tipInput.value = "0";
}
const amount = decimalToAtomic(amountInput.value); const amount = decimalToAtomic(amountInput.value);
if (!amount || amount <= 0n) { if (!amount || amount <= 0n) {
if (!tipInput.value) { if (!tipInput.value) {
@ -198,7 +213,9 @@ function updateSwapOfferDerivedFields() {
if (swapAvailable) { if (swapAvailable) {
swapAvailable.textContent = offered swapAvailable.textContent = offered
? (offered.kind === "nft" ? (offered.kind === "nft"
? "1 NFT" ? (offered.ownershipType === 1
? `${formatAtomicBalance(String(offered.balance))} available`
: "1 NFT")
: `${formatAtomicBalance(String(offered.balance))} ${offered.asset}`) : `${formatAtomicBalance(String(offered.balance))} ${offered.asset}`)
: "--"; : "--";
} }
@ -211,10 +228,13 @@ function validateSwapSide(asset, amountInput, tipInput, label) {
if (!asset) { if (!asset) {
return `Select the asset ${label}.`; return `Select the asset ${label}.`;
} }
const amount = asset.kind === "nft" ? 1n : decimalToAtomic(amountInput.value); const amount = decimalToAtomic(amountInput.value);
if (amount === null || amount <= 0n) { if (amount === null || amount <= 0n) {
return `Enter a valid amount ${label}.`; return `Enter a valid amount ${label}.`;
} }
if (asset.kind === "nft" && asset.ownershipType === 0 && amount !== ATOMIC_UNITS_PER_COIN) {
return `Indivisible NFT amounts must be exactly 1 ${label}.`;
}
const tip = decimalToAtomic(tipInput.value || "0"); const tip = decimalToAtomic(tipInput.value || "0");
if (tip === null) { if (tip === null) {
return `Enter a valid miner tip ${label}.`; return `Enter a valid miner tip ${label}.`;
@ -260,7 +280,7 @@ function validateSwapOfferForm() {
return "Offer expiration must be between 1 and 720 hours."; return "Offer expiration must be between 1 and 720 hours.";
} }
const amount = offered.kind === "nft" ? 1n : decimalToAtomic(swapOfferAmountInput.value); const amount = decimalToAtomic(swapOfferAmountInput.value);
const tip = offered.kind === "nft" ? 0n : decimalToAtomic(swapOfferTipInput.value); const tip = offered.kind === "nft" ? 0n : decimalToAtomic(swapOfferTipInput.value);
if (amount + tip > offered.balance) { if (amount + tip > offered.balance) {
return "Offered amount plus miner tip exceeds the available asset balance."; return "Offered amount plus miner tip exceeds the available asset balance.";
@ -279,8 +299,8 @@ function validateSwapOfferForm() {
function swapOfferPayload() { function swapOfferPayload() {
const offered = selectedSwapAsset(swapOfferAssetSelect); const offered = selectedSwapAsset(swapOfferAssetSelect);
const requested = selectedSwapAsset(swapRequestAssetSelect); const requested = selectedSwapAsset(swapRequestAssetSelect);
const value1 = offered.kind === "nft" ? 1n : decimalToAtomic(swapOfferAmountInput.value); const value1 = decimalToAtomic(swapOfferAmountInput.value);
const value2 = requested.kind === "nft" ? 1n : decimalToAtomic(swapRequestAmountInput.value); const value2 = decimalToAtomic(swapRequestAmountInput.value);
const tip1 = offered.kind === "nft" ? 0n : decimalToAtomic(swapOfferTipInput.value); const tip1 = offered.kind === "nft" ? 0n : decimalToAtomic(swapOfferTipInput.value);
const tip2 = requested.kind === "nft" ? 0n : decimalToAtomic(swapRequestTipInput.value); const tip2 = requested.kind === "nft" ? 0n : decimalToAtomic(swapRequestTipInput.value);
return { return {

View File

@ -360,9 +360,9 @@ function parseDashboardTransactionRows(
if (type === 4) { if (type === 4) {
const creator = bytesToAddressHex(bytes.slice(5, 27)); const creator = bytesToAddressHex(bytes.slice(5, 27));
const series = Number(bytes[27] || 0); const series = Number(bytes[27] || 0);
const nftName = bytesToText(bytes.slice(28, 43)) || "NFT"; const nftName = bytesToText(bytes.slice(29, 44)) || "NFT";
const count = readU32LE(bytes, 143); const count = readU32LE(bytes, 144);
const fee = readU64LE(bytes, 247); const fee = readU64LE(bytes, 248);
if (isActiveAddress(creator)) { if (isActiveAddress(creator)) {
return [dashboardRow(base, "NFT Created", assetWithSeries(nftName, series), "Self", `+${count}`, signedBaseFee("-", fee))]; return [dashboardRow(base, "NFT Created", assetWithSeries(nftName, series), "Self", `+${count}`, signedBaseFee("-", fee))];
} }
@ -556,11 +556,12 @@ function decodeLightweightTransaction(bytesLike) {
time: time(), time: time(),
creator: bytesToAddressHex(bytes.slice(5, 27)), creator: bytesToAddressHex(bytes.slice(5, 27)),
series: Number(bytes[27] || 0), series: Number(bytes[27] || 0),
nft_name: bytesToText(bytes.slice(28, 43)), ownership_type: Number(bytes[28] || 0),
item_ipfs: bytesToText(bytes.slice(43, 143)), nft_name: bytesToText(bytes.slice(29, 44)),
count: readU32LE(bytes, 143), item_ipfs: bytesToText(bytes.slice(44, 144)),
description: bytesToText(bytes.slice(147, 247)), count: readU32LE(bytes, 144),
txfee: Number(readU64LE(bytes, 247)) description: bytesToText(bytes.slice(148, 248)),
txfee: Number(readU64LE(bytes, 248))
} }
}; };
} }

View File

@ -3,6 +3,40 @@ const transferBroadcastButton = document.querySelector("[data-transfer-broadcast
const ATOMIC_UNITS_PER_COIN = 100000000n; const ATOMIC_UNITS_PER_COIN = 100000000n;
const NON_BASE_TRANSFER_MIN_FEE_ATOMIC = 100000000n; const NON_BASE_TRANSFER_MIN_FEE_ATOMIC = 100000000n;
let transferBalancesRefreshPromise = null; let transferBalancesRefreshPromise = null;
let nftOwnershipRegistry = new Map();
let nftOwnershipRegistryKey = "";
function nftRegistryKey(asset, series) {
return `${String(asset || "").trim().toLowerCase()}|${Number(series || 0)}`;
}
function updateNftOwnershipRegistry(nfts, registryKey = "") {
nftOwnershipRegistry = new Map(
(Array.isArray(nfts) ? nfts : []).map((nft) => [
nftRegistryKey(nft.nft_name, nft.series),
{
ownershipType: Number(nft.ownership_type),
remainingSupply: BigInt(nft.remaining_supply || "0")
}
])
);
nftOwnershipRegistryKey = registryKey;
}
function nftOwnershipMetadata(asset, series) {
return nftOwnershipRegistry.get(nftRegistryKey(asset, series)) || null;
}
async function ensureNftOwnershipRegistry() {
const invoke = window.__TAURI__?.core?.invoke;
const broadcastNode = activeBroadcastNode();
const registryKey = `${activeWalletAddressShort || activeWalletAddressHex}|${broadcastNode}`;
if (!invoke || !broadcastNode || nftOwnershipRegistryKey === registryKey) {
return;
}
const nfts = await invoke("nft_list", { broadcastNode });
updateNftOwnershipRegistry(nfts, registryKey);
}
function setTransferMessage(message, type = "") { function setTransferMessage(message, type = "") {
setScopedTransactionMessage("Transfer", transferMessage, message, type); setScopedTransactionMessage("Transfer", transferMessage, message, type);
@ -79,6 +113,8 @@ function selectedTransferAsset() {
return { return {
asset: option.dataset.asset || "", asset: option.dataset.asset || "",
nftSeries: Number(option.dataset.nftSeries || 0), nftSeries: Number(option.dataset.nftSeries || 0),
ownershipType: Number(option.dataset.ownershipType || -1),
isNft: option.dataset.isNft === "true",
balance: BigInt(option.dataset.balance || "0") balance: BigInt(option.dataset.balance || "0")
}; };
} }
@ -122,7 +158,7 @@ function updateTransferDerivedFields() {
} }
if (transferAmountInput) { if (transferAmountInput) {
if (selected.nftSeries > 0) { if (selected.isNft && selected.ownershipType === 0) {
transferAmountInput.value = "1"; transferAmountInput.value = "1";
transferAmountInput.disabled = true; transferAmountInput.disabled = true;
} else { } else {
@ -144,7 +180,7 @@ function updateTransferFee() {
return; return;
} }
const amount = selected.nftSeries > 0 ? 1n : decimalToAtomic(transferAmountInput?.value); const amount = decimalToAtomic(transferAmountInput?.value);
if (amount === null || amount <= 0n) { if (amount === null || amount <= 0n) {
transferFeeInput.value = ""; transferFeeInput.value = "";
return; return;
@ -188,6 +224,9 @@ function refreshTransferAssetOptions() {
option.dataset.asset = balance.asset || ""; option.dataset.asset = balance.asset || "";
option.dataset.nftSeries = String(Number(balance.nft_series || 0)); option.dataset.nftSeries = String(Number(balance.nft_series || 0));
option.dataset.balance = String(balance.balance || "0"); option.dataset.balance = String(balance.balance || "0");
const metadata = nftOwnershipMetadata(balance.asset, balance.nft_series);
option.dataset.isNft = String(Boolean(metadata));
option.dataset.ownershipType = String(metadata?.ownershipType ?? -1);
transferAssetSelect.appendChild(option); transferAssetSelect.appendChild(option);
}); });
@ -199,7 +238,7 @@ function refreshTransferAssetOptions() {
} }
async function refreshTransferBalancesIfNeeded() { async function refreshTransferBalancesIfNeeded() {
if (activeWalletBalances.length || !walletLoaded || !registrationRegistered) { if (!walletLoaded || !registrationRegistered) {
refreshTransferAssetOptions(); refreshTransferAssetOptions();
return; return;
} }
@ -218,7 +257,12 @@ async function refreshTransferBalancesIfNeeded() {
transferBalancesRefreshPromise = (async () => { transferBalancesRefreshPromise = (async () => {
setTransferMessage("Loading spendable assets..."); setTransferMessage("Loading spendable assets...");
try { try {
const balances = await invoke("get_active_wallet_balances", { broadcastNode }); const [balances] = await Promise.all([
activeWalletBalances.length
? Promise.resolve(activeWalletBalances)
: invoke("get_active_wallet_balances", { broadcastNode }),
ensureNftOwnershipRegistry()
]);
activeWalletBalances = Array.isArray(balances) ? balances : []; activeWalletBalances = Array.isArray(balances) ? balances : [];
refreshTransferAssetOptions(); refreshTransferAssetOptions();
setTransferMessage(""); setTransferMessage("");
@ -326,11 +370,15 @@ function validateTransferForm() {
return "Receiver cannot be the active wallet address."; return "Receiver cannot be the active wallet address.";
} }
const amount = selected.nftSeries > 0 ? 1n : decimalToAtomic(transferAmountInput?.value); const amount = decimalToAtomic(transferAmountInput?.value);
if (amount === null || amount <= 0n) { if (amount === null || amount <= 0n) {
return "Enter a valid amount with up to 8 decimal places."; return "Enter a valid amount with up to 8 decimal places.";
} }
if (selected.nftSeries > 0 && String(transferAmountInput?.value || "").trim() !== "1") { if (
selected.isNft
&& selected.ownershipType === 0
&& amount !== ATOMIC_UNITS_PER_COIN
) {
return "NFT transfers must send exactly 1 item."; return "NFT transfers must send exactly 1 item.";
} }
if (amount > selected.balance) { if (amount > selected.balance) {
@ -360,7 +408,7 @@ function validateTransferForm() {
function transferPayload() { function transferPayload() {
const selected = selectedTransferAsset(); const selected = selectedTransferAsset();
const value = selected?.nftSeries > 0 ? 1n : decimalToAtomic(transferAmountInput?.value); const value = decimalToAtomic(transferAmountInput?.value);
const fee = decimalToAtomic(transferFeeInput?.value || "0"); const fee = decimalToAtomic(transferFeeInput?.value || "0");
return { return {
@ -420,7 +468,7 @@ async function handleTransferAction(action) {
if (!broadcastNode) { if (!broadcastNode) {
throw new Error("Select a broadcast node before saving."); throw new Error("Select a broadcast node before saving.");
} }
setTransferMessage("Saving transfer..."); setTransferMessage("Signing and saving transfer...");
const review = await invoke("save_transfer_transaction", { const review = await invoke("save_transfer_transaction", {
...transferPayload(), ...transferPayload(),
broadcastNode, broadcastNode,
@ -436,7 +484,7 @@ async function handleTransferAction(action) {
if (!broadcastNode) { if (!broadcastNode) {
throw new Error("Select a broadcast node before broadcasting."); throw new Error("Select a broadcast node before broadcasting.");
} }
setTransferMessage("Broadcasting transfer..."); setTransferMessage("Signing and broadcasting transfer...");
const result = await invoke("broadcast_transfer_transaction", { const result = await invoke("broadcast_transfer_transaction", {
...transferPayload(), ...transferPayload(),
broadcastNode, broadcastNode,

View File

@ -181,7 +181,7 @@ async function handleVanityAction(action) {
setVanityButtonsDisabled(true); setVanityButtonsDisabled(true);
try { try {
if (action === "save") { if (action === "save") {
setVanityMessage("Saving vanity address transaction..."); setVanityMessage("Signing and saving vanity address transaction...");
const review = await invoke("save_vanity_address_transaction", { const review = await invoke("save_vanity_address_transaction", {
...vanityPayload(), ...vanityPayload(),
walletKey walletKey
@ -196,7 +196,7 @@ async function handleVanityAction(action) {
if (!broadcastNode) { if (!broadcastNode) {
throw new Error("Select a broadcast node before broadcasting."); throw new Error("Select a broadcast node before broadcasting.");
} }
setVanityMessage("Broadcasting vanity address transaction..."); setVanityMessage("Signing and broadcasting vanity address transaction...");
const result = await invoke("broadcast_vanity_address_transaction", { const result = await invoke("broadcast_vanity_address_transaction", {
...vanityPayload(), ...vanityPayload(),
broadcastNode, broadcastNode,

View File

@ -474,7 +474,7 @@ async fn nft_list(
if let Some(error) = parse_error_response(&response) { if let Some(error) = parse_error_response(&response) {
return Err(error); return Err(error);
} }
const NFT_LIST_ROW_BYTES: usize = 32 + 15 + 4; const NFT_LIST_ROW_BYTES: usize = 32 + 15 + 4 + 1 + 8;
if response.len() % NFT_LIST_ROW_BYTES != 0 { if response.len() % NFT_LIST_ROW_BYTES != 0 {
return Err("NFT list response had an unexpected byte length.".to_string()); return Err("NFT list response had an unexpected byte length.".to_string());
} }
@ -488,6 +488,12 @@ async fn nft_list(
.try_into() .try_into()
.map_err(|_| "Failed to parse NFT series.".to_string())?, .map_err(|_| "Failed to parse NFT series.".to_string())?,
); );
let ownership_type = chunk[51];
let remaining_supply = u64::from_le_bytes(
chunk[52..60]
.try_into()
.map_err(|_| "Failed to parse NFT remaining supply.".to_string())?,
);
if nft_name.is_empty() { if nft_name.is_empty() {
continue; continue;
} }
@ -495,6 +501,8 @@ async fn nft_list(
nft_name, nft_name,
series, series,
origin_txid, origin_txid,
ownership_type,
remaining_supply: remaining_supply.to_string(),
}); });
} }
@ -1152,15 +1160,11 @@ async fn build_transfer_json(
} }
let coin = padded_coin_field(&coin)?; let coin = padded_coin_field(&coin)?;
let mut value = parse_atomic_u64(&value, "Transfer value")?; let value = parse_atomic_u64(&value, "Transfer value")?;
let txfee = parse_atomic_u64(&txfee, "Transfer fee")?; let txfee = parse_atomic_u64(&txfee, "Transfer fee")?;
if value == 0 { if value == 0 {
return Err("Transfer value must be greater than zero.".to_string()); return Err("Transfer value must be greater than zero.".to_string());
} }
if nft_series > 0 {
value = 1;
}
let is_base_coin = coin.trim().eq_ignore_ascii_case(BASECOIN.trim()); let is_base_coin = coin.trim().eq_ignore_ascii_case(BASECOIN.trim());
let minimum_fee = minimum_transfer_fee(value, is_base_coin); let minimum_fee = minimum_transfer_fee(value, is_base_coin);
if txfee < minimum_fee { if txfee < minimum_fee {
@ -1247,6 +1251,7 @@ async fn build_create_nft_json(
state: &State<'_, WalletState>, state: &State<'_, WalletState>,
wallet_key: Option<String>, wallet_key: Option<String>,
series: u8, series: u8,
ownership_type: u8,
nft_name: String, nft_name: String,
item_ipfs: String, item_ipfs: String,
count: u32, count: u32,
@ -1263,6 +1268,12 @@ async fn build_create_nft_json(
if series == 1 && count < 2 { if series == 1 && count < 2 {
return Err("An NFT collection must contain at least 2 items.".to_string()); return Err("An NFT collection must contain at least 2 items.".to_string());
} }
if ownership_type > 1 {
return Err("Ownership type must be indivisible or fractional.".to_string());
}
if series == 1 && ownership_type != 0 {
return Err("NFT/RWA collections must use indivisible ownership.".to_string());
}
let nft_name = padded_token_ticker(&nft_name) let nft_name = padded_token_ticker(&nft_name)
.map_err(|_| "NFT name must normalize to 3-15 alphanumeric characters.".to_string())?; .map_err(|_| "NFT name must normalize to 3-15 alphanumeric characters.".to_string())?;
@ -1290,6 +1301,7 @@ async fn build_create_nft_json(
timestamp, timestamp,
&creator, &creator,
series, series,
ownership_type,
&nft_name, &nft_name,
&item_ipfs, &item_ipfs,
count, count,
@ -1307,6 +1319,7 @@ async fn build_create_nft_json(
"timestamp": timestamp, "timestamp": timestamp,
"creator": creator, "creator": creator,
"series": series, "series": series,
"ownership_type": ownership_type,
"nft_name": nft_name, "nft_name": nft_name,
"item_ipfs": item_ipfs, "item_ipfs": item_ipfs,
"count": count, "count": count,
@ -2732,6 +2745,7 @@ async fn rebuild_create_nft(
json_u32(transaction, "timestamp", "time")?, json_u32(transaction, "timestamp", "time")?,
&json_str(transaction, "creator")?, &json_str(transaction, "creator")?,
json_u8(transaction, "series")?, json_u8(transaction, "series")?,
json_u8(transaction, "ownership_type")?,
&json_str(transaction, "nft_name")?, &json_str(transaction, "nft_name")?,
&json_str(transaction, "item_ipfs")?, &json_str(transaction, "item_ipfs")?,
json_u64(transaction, "count")? as u32, json_u64(transaction, "count")? as u32,
@ -3990,6 +4004,7 @@ async fn save_create_nft_transaction(
state: State<'_, WalletState>, state: State<'_, WalletState>,
wallet_key: Option<String>, wallet_key: Option<String>,
series: u8, series: u8,
ownership_type: u8,
nft_name: String, nft_name: String,
item_ipfs: String, item_ipfs: String,
count: u32, count: u32,
@ -4001,6 +4016,7 @@ async fn save_create_nft_transaction(
&state, &state,
wallet_key, wallet_key,
series, series,
ownership_type,
nft_name, nft_name,
item_ipfs, item_ipfs,
count, count,
@ -4017,6 +4033,7 @@ async fn broadcast_create_nft_transaction(
wallet_key: Option<String>, wallet_key: Option<String>,
broadcast_node: String, broadcast_node: String,
series: u8, series: u8,
ownership_type: u8,
nft_name: String, nft_name: String,
item_ipfs: String, item_ipfs: String,
count: u32, count: u32,
@ -4027,6 +4044,7 @@ async fn broadcast_create_nft_transaction(
&state, &state,
wallet_key, wallet_key,
series, series,
ownership_type,
nft_name, nft_name,
item_ipfs, item_ipfs,
count, count,

View File

@ -122,6 +122,8 @@ struct NftListItemView {
nft_name: String, nft_name: String,
series: u32, series: u32,
origin_txid: String, origin_txid: String,
ownership_type: u8,
remaining_supply: String,
} }
#[derive(Serialize)] #[derive(Serialize)]