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

View File

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

View File

@ -105,6 +105,7 @@ const issueTokenMessage = document.querySelector("[data-issue-token-message]");
const createNftForm = document.querySelector("[data-create-nft-form]");
const createNftActions = document.querySelector("[data-create-nft-actions]");
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 createNftIpfsInput = document.querySelector("[data-create-nft-ipfs]");
const createNftCountInput = document.querySelector("[data-create-nft-count]");

View File

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

View File

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

View File

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

View File

@ -118,7 +118,7 @@ async function handleCreateTokenAction(action) {
setCreateTokenButtonsDisabled(true);
try {
if (action === "save") {
setCreateTokenMessage("Saving token transaction...");
setCreateTokenMessage("Signing and saving token transaction...");
const review = await invoke("save_create_token_transaction", {
...createTokenPayload(),
walletKey
@ -133,7 +133,7 @@ async function handleCreateTokenAction(action) {
if (!broadcastNode) {
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", {
...createTokenPayload(),
broadcastNode,

View File

@ -371,7 +371,7 @@ async function handleDataStorageAction(action) {
setDataStorageButtonsDisabled(true);
try {
if (action === "save") {
setDataStorageMessage("Saving data storage transaction...");
setDataStorageMessage("Signing and saving data storage transaction...");
const review = await invoke("save_data_storage_transaction", {
...dataStoragePayload(),
walletKey
@ -386,7 +386,7 @@ async function handleDataStorageAction(action) {
if (!broadcastNode) {
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", {
...dataStoragePayload(),
broadcastNode,

View File

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

View File

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

View File

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

View File

@ -126,7 +126,7 @@ async function handleMarketingAction(action) {
setMarketingButtonsDisabled(true);
try {
if (action === "save") {
setMarketingMessage("Saving marketing transaction...");
setMarketingMessage("Signing and saving marketing transaction...");
const review = await invoke("save_marketing_transaction", {
...marketingPayload(),
walletKey
@ -141,7 +141,7 @@ async function handleMarketingAction(action) {
if (!broadcastNode) {
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", {
...marketingPayload(),
broadcastNode,

View File

@ -165,7 +165,9 @@ function ownedNftRowsFromBalances(balances, registry) {
nft_name: nftName,
series,
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)
@ -197,12 +199,29 @@ function metadataDescription(metadata) {
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) {
const key = nftKey(nft);
const name = nftDisplayName(nft);
const image = nftImageUri(nft);
const type = nftTypeLabel(nft);
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
? `<img src="${escapeNftText(ipfsToGatewayUrl(image, nft.gateway))}" alt="">`
: `<div class="asset-mark art">${escapeNftText(assetInitial(name))}</div>`;
@ -214,6 +233,7 @@ function renderNftCard(nft) {
<div>
<strong>${escapeNftText(name)}</strong>
<span>${escapeNftText(type)} - ${escapeNftText(nft.asset_name || `${nft.nft_name}_${nft.series}`)}</span>
<span>${escapeNftText(ownershipLabel)}</span>
</div>
</div>
<div class="nft-link-row">

View File

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

View File

@ -357,12 +357,12 @@ function parseDashboardTransactionRows(
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(creator), signedAmount("+", fee))];
}
if (type === 4) {
const creator = bytesToAddressHex(bytes.slice(5, 27));
const series = Number(bytes[27] || 0);
const nftName = bytesToText(bytes.slice(28, 43)) || "NFT";
const count = readU32LE(bytes, 143);
const fee = readU64LE(bytes, 247);
if (type === 4) {
const creator = bytesToAddressHex(bytes.slice(5, 27));
const series = Number(bytes[27] || 0);
const nftName = bytesToText(bytes.slice(29, 44)) || "NFT";
const count = readU32LE(bytes, 144);
const fee = readU64LE(bytes, 248);
if (isActiveAddress(creator)) {
return [dashboardRow(base, "NFT Created", assetWithSeries(nftName, series), "Self", `+${count}`, signedBaseFee("-", fee))];
}
@ -556,11 +556,12 @@ function decodeLightweightTransaction(bytesLike) {
time: time(),
creator: bytesToAddressHex(bytes.slice(5, 27)),
series: Number(bytes[27] || 0),
nft_name: bytesToText(bytes.slice(28, 43)),
item_ipfs: bytesToText(bytes.slice(43, 143)),
count: readU32LE(bytes, 143),
description: bytesToText(bytes.slice(147, 247)),
txfee: Number(readU64LE(bytes, 247))
ownership_type: Number(bytes[28] || 0),
nft_name: bytesToText(bytes.slice(29, 44)),
item_ipfs: bytesToText(bytes.slice(44, 144)),
count: readU32LE(bytes, 144),
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 NON_BASE_TRANSFER_MIN_FEE_ATOMIC = 100000000n;
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 = "") {
setScopedTransactionMessage("Transfer", transferMessage, message, type);
@ -79,6 +113,8 @@ function selectedTransferAsset() {
return {
asset: option.dataset.asset || "",
nftSeries: Number(option.dataset.nftSeries || 0),
ownershipType: Number(option.dataset.ownershipType || -1),
isNft: option.dataset.isNft === "true",
balance: BigInt(option.dataset.balance || "0")
};
}
@ -122,7 +158,7 @@ function updateTransferDerivedFields() {
}
if (transferAmountInput) {
if (selected.nftSeries > 0) {
if (selected.isNft && selected.ownershipType === 0) {
transferAmountInput.value = "1";
transferAmountInput.disabled = true;
} else {
@ -144,7 +180,7 @@ function updateTransferFee() {
return;
}
const amount = selected.nftSeries > 0 ? 1n : decimalToAtomic(transferAmountInput?.value);
const amount = decimalToAtomic(transferAmountInput?.value);
if (amount === null || amount <= 0n) {
transferFeeInput.value = "";
return;
@ -188,6 +224,9 @@ function refreshTransferAssetOptions() {
option.dataset.asset = balance.asset || "";
option.dataset.nftSeries = String(Number(balance.nft_series || 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);
});
@ -199,7 +238,7 @@ function refreshTransferAssetOptions() {
}
async function refreshTransferBalancesIfNeeded() {
if (activeWalletBalances.length || !walletLoaded || !registrationRegistered) {
if (!walletLoaded || !registrationRegistered) {
refreshTransferAssetOptions();
return;
}
@ -218,7 +257,12 @@ async function refreshTransferBalancesIfNeeded() {
transferBalancesRefreshPromise = (async () => {
setTransferMessage("Loading spendable assets...");
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 : [];
refreshTransferAssetOptions();
setTransferMessage("");
@ -326,11 +370,15 @@ function validateTransferForm() {
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) {
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.";
}
if (amount > selected.balance) {
@ -360,7 +408,7 @@ function validateTransferForm() {
function transferPayload() {
const selected = selectedTransferAsset();
const value = selected?.nftSeries > 0 ? 1n : decimalToAtomic(transferAmountInput?.value);
const value = decimalToAtomic(transferAmountInput?.value);
const fee = decimalToAtomic(transferFeeInput?.value || "0");
return {
@ -420,7 +468,7 @@ async function handleTransferAction(action) {
if (!broadcastNode) {
throw new Error("Select a broadcast node before saving.");
}
setTransferMessage("Saving transfer...");
setTransferMessage("Signing and saving transfer...");
const review = await invoke("save_transfer_transaction", {
...transferPayload(),
broadcastNode,
@ -436,7 +484,7 @@ async function handleTransferAction(action) {
if (!broadcastNode) {
throw new Error("Select a broadcast node before broadcasting.");
}
setTransferMessage("Broadcasting transfer...");
setTransferMessage("Signing and broadcasting transfer...");
const result = await invoke("broadcast_transfer_transaction", {
...transferPayload(),
broadcastNode,

View File

@ -181,7 +181,7 @@ async function handleVanityAction(action) {
setVanityButtonsDisabled(true);
try {
if (action === "save") {
setVanityMessage("Saving vanity address transaction...");
setVanityMessage("Signing and saving vanity address transaction...");
const review = await invoke("save_vanity_address_transaction", {
...vanityPayload(),
walletKey
@ -196,7 +196,7 @@ async function handleVanityAction(action) {
if (!broadcastNode) {
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", {
...vanityPayload(),
broadcastNode,

View File

@ -474,7 +474,7 @@ async fn nft_list(
if let Some(error) = parse_error_response(&response) {
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 {
return Err("NFT list response had an unexpected byte length.".to_string());
}
@ -488,6 +488,12 @@ async fn nft_list(
.try_into()
.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() {
continue;
}
@ -495,6 +501,8 @@ async fn nft_list(
nft_name,
series,
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 mut value = parse_atomic_u64(&value, "Transfer value")?;
let value = parse_atomic_u64(&value, "Transfer value")?;
let txfee = parse_atomic_u64(&txfee, "Transfer fee")?;
if value == 0 {
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 minimum_fee = minimum_transfer_fee(value, is_base_coin);
if txfee < minimum_fee {
@ -1247,6 +1251,7 @@ async fn build_create_nft_json(
state: &State<'_, WalletState>,
wallet_key: Option<String>,
series: u8,
ownership_type: u8,
nft_name: String,
item_ipfs: String,
count: u32,
@ -1263,6 +1268,12 @@ async fn build_create_nft_json(
if series == 1 && count < 2 {
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)
.map_err(|_| "NFT name must normalize to 3-15 alphanumeric characters.".to_string())?;
@ -1290,6 +1301,7 @@ async fn build_create_nft_json(
timestamp,
&creator,
series,
ownership_type,
&nft_name,
&item_ipfs,
count,
@ -1307,6 +1319,7 @@ async fn build_create_nft_json(
"timestamp": timestamp,
"creator": creator,
"series": series,
"ownership_type": ownership_type,
"nft_name": nft_name,
"item_ipfs": item_ipfs,
"count": count,
@ -2732,6 +2745,7 @@ async fn rebuild_create_nft(
json_u32(transaction, "timestamp", "time")?,
&json_str(transaction, "creator")?,
json_u8(transaction, "series")?,
json_u8(transaction, "ownership_type")?,
&json_str(transaction, "nft_name")?,
&json_str(transaction, "item_ipfs")?,
json_u64(transaction, "count")? as u32,
@ -3990,6 +4004,7 @@ async fn save_create_nft_transaction(
state: State<'_, WalletState>,
wallet_key: Option<String>,
series: u8,
ownership_type: u8,
nft_name: String,
item_ipfs: String,
count: u32,
@ -4001,6 +4016,7 @@ async fn save_create_nft_transaction(
&state,
wallet_key,
series,
ownership_type,
nft_name,
item_ipfs,
count,
@ -4017,6 +4033,7 @@ async fn broadcast_create_nft_transaction(
wallet_key: Option<String>,
broadcast_node: String,
series: u8,
ownership_type: u8,
nft_name: String,
item_ipfs: String,
count: u32,
@ -4027,6 +4044,7 @@ async fn broadcast_create_nft_transaction(
&state,
wallet_key,
series,
ownership_type,
nft_name,
item_ipfs,
count,

View File

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