|
|
|
|
@@ -8,7 +8,7 @@
|
|
|
|
|
|
|
|
|
|
<!-- Quick Actions Container -->
|
|
|
|
|
<div class="glass-card p-6 mb-6">
|
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-4 stagger-grid">
|
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 stagger-grid">
|
|
|
|
|
<!-- Networking Profits -->
|
|
|
|
|
<div data-controller-container tabindex="0" class="card-stagger flex flex-col gap-3 p-4 bg-white/5 rounded-lg min-w-0" style="--stagger-index: 0">
|
|
|
|
|
<div class="flex items-center gap-3 min-w-0">
|
|
|
|
|
@@ -39,13 +39,20 @@
|
|
|
|
|
<p v-else class="text-xs text-white/60 capitalize">{{ didStatus }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
v-if="userDid"
|
|
|
|
|
@click="copyDid"
|
|
|
|
|
class="w-fit px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors"
|
|
|
|
|
>
|
|
|
|
|
{{ didCopied ? 'Copied!' : 'Copy DID' }}
|
|
|
|
|
</button>
|
|
|
|
|
<div v-if="userDid" class="flex gap-2">
|
|
|
|
|
<button
|
|
|
|
|
@click="copyDid"
|
|
|
|
|
class="px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors"
|
|
|
|
|
>
|
|
|
|
|
{{ didCopied ? 'Copied!' : 'Copy DID' }}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
@click="showDidDocument"
|
|
|
|
|
class="px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors"
|
|
|
|
|
>
|
|
|
|
|
View DID Document
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
v-else
|
|
|
|
|
@click="createDID"
|
|
|
|
|
@@ -119,6 +126,55 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Hardware Wallet Detected Banner -->
|
|
|
|
|
<div v-if="detectedHwWallets.length > 0" class="mb-6 p-4 bg-orange-500/10 border border-orange-500/20 rounded-xl flex items-center gap-3">
|
|
|
|
|
<div class="w-8 h-8 rounded-lg bg-orange-500/20 flex items-center justify-center flex-shrink-0">
|
|
|
|
|
<svg class="w-5 h-5 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex-1 min-w-0">
|
|
|
|
|
<p class="text-sm font-medium text-orange-400">Hardware Wallet Detected</p>
|
|
|
|
|
<p class="text-xs text-white/60">
|
|
|
|
|
{{ detectedHwWallets.map(d => `${d.type}${d.product ? ' (' + d.product + ')' : ''}`).join(', ') }}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- DID Document Modal -->
|
|
|
|
|
<Teleport to="body">
|
|
|
|
|
<div v-if="showDidDocModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" @click.self="showDidDocModal = false" @keydown.escape="showDidDocModal = false">
|
|
|
|
|
<div class="glass-card p-6 max-w-lg w-full max-h-[90vh] overflow-y-auto" role="dialog" aria-modal="true" aria-labelledby="did-doc-title">
|
|
|
|
|
<div class="flex items-center justify-between mb-4">
|
|
|
|
|
<h3 id="did-doc-title" class="text-lg font-semibold text-white">DID Document</h3>
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<span v-if="didDocVerified === true" class="text-xs text-green-400 flex items-center gap-1">
|
|
|
|
|
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>
|
|
|
|
|
Verified
|
|
|
|
|
</span>
|
|
|
|
|
<span v-else-if="didDocVerified === false" class="text-xs text-red-400">Invalid</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="loadingDidDoc" class="text-white/60 text-sm">Loading DID Document...</div>
|
|
|
|
|
<pre v-else class="text-xs text-white/80 font-mono bg-black/30 rounded-lg p-4 overflow-x-auto whitespace-pre-wrap">{{ didDocumentFormatted }}</pre>
|
|
|
|
|
<div class="flex gap-3 mt-4">
|
|
|
|
|
<button
|
|
|
|
|
@click="copyDidDocument"
|
|
|
|
|
class="flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors"
|
|
|
|
|
>
|
|
|
|
|
{{ didDocCopied ? 'Copied!' : 'Copy JSON' }}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
@click="showDidDocModal = false"
|
|
|
|
|
class="px-4 py-2 rounded-lg bg-white/10 text-white font-medium hover:bg-white/20 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
Close
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Teleport>
|
|
|
|
|
|
|
|
|
|
<!-- Send Message Modal -->
|
|
|
|
|
<Teleport to="body">
|
|
|
|
|
<div v-if="showSendMessageModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" @click.self="closeSendMessageModal()">
|
|
|
|
|
@@ -865,8 +921,8 @@
|
|
|
|
|
|
|
|
|
|
<!-- Content Streaming Player -->
|
|
|
|
|
<Teleport to="body">
|
|
|
|
|
<div v-if="streamingItem" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm" @click.self="closePlayer">
|
|
|
|
|
<div class="glass-card p-0 w-full max-w-2xl overflow-hidden">
|
|
|
|
|
<div v-if="streamingItem" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm" @click.self="closePlayer" @keydown.escape="closePlayer">
|
|
|
|
|
<div class="glass-card p-0 w-full max-w-2xl overflow-hidden" role="dialog" aria-modal="true">
|
|
|
|
|
<!-- Player Header -->
|
|
|
|
|
<div class="flex items-center justify-between px-4 py-3 border-b border-white/10">
|
|
|
|
|
<div class="min-w-0 flex-1">
|
|
|
|
|
@@ -937,9 +993,9 @@
|
|
|
|
|
|
|
|
|
|
<!-- Add Content Modal -->
|
|
|
|
|
<Teleport to="body">
|
|
|
|
|
<div v-if="showAddContentModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showAddContentModal = false">
|
|
|
|
|
<div class="glass-card p-6 w-full max-w-md mx-4">
|
|
|
|
|
<h2 class="text-lg font-bold text-white mb-4">Add Content</h2>
|
|
|
|
|
<div v-if="showAddContentModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showAddContentModal = false" @keydown.escape="showAddContentModal = false">
|
|
|
|
|
<div class="glass-card p-6 w-full max-w-md mx-4" role="dialog" aria-modal="true" aria-labelledby="add-content-title">
|
|
|
|
|
<h2 id="add-content-title" class="text-lg font-bold text-white mb-4">Add Content</h2>
|
|
|
|
|
<div class="space-y-4">
|
|
|
|
|
<div>
|
|
|
|
|
<label class="text-white/60 text-sm block mb-1">Filename</label>
|
|
|
|
|
@@ -1086,9 +1142,9 @@
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Create Identity Modal -->
|
|
|
|
|
<div v-if="showCreateIdentityModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showCreateIdentityModal = false">
|
|
|
|
|
<div class="glass-card p-6 w-full max-w-md mx-4">
|
|
|
|
|
<h2 class="text-lg font-bold text-white mb-4">Create Identity</h2>
|
|
|
|
|
<div v-if="showCreateIdentityModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showCreateIdentityModal = false" @keydown.escape="showCreateIdentityModal = false">
|
|
|
|
|
<div class="glass-card p-6 w-full max-w-md mx-4" role="dialog" aria-modal="true" aria-labelledby="create-identity-title">
|
|
|
|
|
<h2 id="create-identity-title" class="text-lg font-bold text-white mb-4">Create Identity</h2>
|
|
|
|
|
<div class="space-y-4">
|
|
|
|
|
<div>
|
|
|
|
|
<label class="text-white/60 text-sm block mb-1">Name</label>
|
|
|
|
|
@@ -1120,9 +1176,9 @@
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Delete Confirmation Modal -->
|
|
|
|
|
<div v-if="deleteIdentityTarget" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="deleteIdentityTarget = null">
|
|
|
|
|
<div class="glass-card p-6 w-full max-w-sm mx-4">
|
|
|
|
|
<h2 class="text-lg font-bold text-white mb-2">Delete Identity?</h2>
|
|
|
|
|
<div v-if="deleteIdentityTarget" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="deleteIdentityTarget = null" @keydown.escape="deleteIdentityTarget = null">
|
|
|
|
|
<div class="glass-card p-6 w-full max-w-sm mx-4" role="dialog" aria-modal="true" aria-labelledby="delete-identity-title">
|
|
|
|
|
<h2 id="delete-identity-title" class="text-lg font-bold text-white mb-2">Delete Identity?</h2>
|
|
|
|
|
<p class="text-white/60 text-sm mb-4">This will permanently delete "{{ deleteIdentityTarget.name }}" and its keypair.</p>
|
|
|
|
|
<div class="flex gap-3">
|
|
|
|
|
<button @click="deleteIdentityTarget = null" class="flex-1 glass-button px-4 py-2 rounded-lg text-sm">Cancel</button>
|
|
|
|
|
@@ -1133,9 +1189,9 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- Unified Send Modal -->
|
|
|
|
|
<div v-if="showUnifiedSendModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="closeUnifiedSendModal">
|
|
|
|
|
<div class="glass-card p-6 w-full max-w-md mx-4">
|
|
|
|
|
<h2 class="text-lg font-bold text-white mb-4">Send Bitcoin</h2>
|
|
|
|
|
<div v-if="showUnifiedSendModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="closeUnifiedSendModal" @keydown.escape="closeUnifiedSendModal">
|
|
|
|
|
<div class="glass-card p-6 w-full max-w-md mx-4" role="dialog" aria-modal="true" aria-labelledby="send-bitcoin-title">
|
|
|
|
|
<h2 id="send-bitcoin-title" class="text-lg font-bold text-white mb-4">Send Bitcoin</h2>
|
|
|
|
|
|
|
|
|
|
<!-- Method tabs -->
|
|
|
|
|
<div class="flex gap-1 mb-4 p-1 bg-white/5 rounded-lg">
|
|
|
|
|
@@ -1174,6 +1230,40 @@
|
|
|
|
|
<button @click="copyEcashToken(ecashSendToken)" class="mt-2 text-xs text-orange-400 hover:text-orange-300">Copy</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Hardware Wallet toggle (on-chain only) -->
|
|
|
|
|
<div v-if="effectiveSendMethod === 'onchain'" class="mb-3 flex items-center gap-3 p-3 bg-white/5 rounded-lg">
|
|
|
|
|
<label class="relative inline-flex items-center cursor-pointer">
|
|
|
|
|
<input type="checkbox" v-model="useHardwareWallet" class="sr-only peer" />
|
|
|
|
|
<div class="w-9 h-5 bg-white/10 peer-focus:outline-none rounded-full peer peer-checked:bg-orange-500/40 transition-colors after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:after:translate-x-full"></div>
|
|
|
|
|
</label>
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-sm text-white">Sign with Hardware Wallet</p>
|
|
|
|
|
<p class="text-xs text-white/40">Creates a PSBT for external signing</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- PSBT display (hardware wallet flow) -->
|
|
|
|
|
<div v-if="psbtStep === 'created' && psbtData" class="mb-3 space-y-2">
|
|
|
|
|
<div class="p-3 bg-white/5 rounded-lg">
|
|
|
|
|
<p class="text-xs text-white/50 mb-1">Unsigned PSBT (copy or download):</p>
|
|
|
|
|
<textarea readonly :value="psbtData" rows="3" class="w-full bg-black/20 border border-white/10 rounded px-2 py-1 text-xs font-mono text-white/80 focus:outline-none"></textarea>
|
|
|
|
|
<div class="flex gap-2 mt-2">
|
|
|
|
|
<button @click="copyPsbt" class="text-xs text-orange-400 hover:text-orange-300">Copy PSBT</button>
|
|
|
|
|
<button @click="downloadPsbt" class="text-xs text-orange-400 hover:text-orange-300">Download .psbt</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="p-3 bg-white/5 rounded-lg">
|
|
|
|
|
<p class="text-xs text-white/50 mb-1">Paste signed PSBT or upload file:</p>
|
|
|
|
|
<textarea v-model="signedPsbtInput" rows="3" placeholder="Paste signed PSBT base64 here..." class="w-full bg-black/20 border border-white/10 rounded px-2 py-1 text-xs font-mono text-white/80 focus:outline-none focus:border-white/30"></textarea>
|
|
|
|
|
<div class="flex gap-2 mt-2">
|
|
|
|
|
<label class="text-xs text-orange-400 hover:text-orange-300 cursor-pointer">
|
|
|
|
|
Upload .psbt
|
|
|
|
|
<input type="file" accept=".psbt,.txt" class="hidden" @change="handlePsbtFileUpload" />
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- On-chain txid result -->
|
|
|
|
|
<div v-if="sendResultTxid" class="mb-3 p-2 bg-green-500/10 border border-green-500/20 rounded-lg">
|
|
|
|
|
<p class="text-green-400 text-xs">Sent! TX: {{ sendResultTxid }}</p>
|
|
|
|
|
@@ -1188,17 +1278,20 @@
|
|
|
|
|
|
|
|
|
|
<div class="flex gap-3">
|
|
|
|
|
<button @click="closeUnifiedSendModal" class="flex-1 glass-button px-4 py-2 rounded-lg text-sm">Close</button>
|
|
|
|
|
<button @click="unifiedSend" :disabled="unifiedSendProcessing || !unifiedSendAmount" class="flex-1 glass-button px-4 py-2 rounded-lg text-sm font-medium bg-orange-500/20 border-orange-500/30 disabled:opacity-50">
|
|
|
|
|
{{ unifiedSendProcessing ? 'Sending...' : 'Send' }}
|
|
|
|
|
<button v-if="psbtStep === 'created'" @click="finalizePsbt" :disabled="unifiedSendProcessing || !signedPsbtInput.trim()" class="flex-1 glass-button px-4 py-2 rounded-lg text-sm font-medium bg-orange-500/20 border-orange-500/30 disabled:opacity-50">
|
|
|
|
|
{{ unifiedSendProcessing ? 'Broadcasting...' : 'Broadcast' }}
|
|
|
|
|
</button>
|
|
|
|
|
<button v-else @click="unifiedSend" :disabled="unifiedSendProcessing || !unifiedSendAmount" class="flex-1 glass-button px-4 py-2 rounded-lg text-sm font-medium bg-orange-500/20 border-orange-500/30 disabled:opacity-50">
|
|
|
|
|
{{ unifiedSendProcessing ? 'Sending...' : (useHardwareWallet && effectiveSendMethod === 'onchain' ? 'Create PSBT' : 'Send') }}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Unified Receive Modal -->
|
|
|
|
|
<div v-if="showUnifiedReceiveModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="closeUnifiedReceiveModal">
|
|
|
|
|
<div class="glass-card p-6 w-full max-w-md mx-4">
|
|
|
|
|
<h2 class="text-lg font-bold text-white mb-4">Receive Bitcoin</h2>
|
|
|
|
|
<div v-if="showUnifiedReceiveModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="closeUnifiedReceiveModal" @keydown.escape="closeUnifiedReceiveModal">
|
|
|
|
|
<div class="glass-card p-6 w-full max-w-md mx-4" role="dialog" aria-modal="true" aria-labelledby="receive-bitcoin-title">
|
|
|
|
|
<h2 id="receive-bitcoin-title" class="text-lg font-bold text-white mb-4">Receive Bitcoin</h2>
|
|
|
|
|
|
|
|
|
|
<!-- Method tabs -->
|
|
|
|
|
<div class="flex gap-1 mb-4 p-1 bg-white/5 rounded-lg">
|
|
|
|
|
@@ -1306,18 +1399,44 @@
|
|
|
|
|
</div>
|
|
|
|
|
<div class="bg-white/5 rounded-lg p-3">
|
|
|
|
|
<div class="text-xs text-white/50 mb-1">Messages</div>
|
|
|
|
|
<span class="text-sm text-white font-medium">{{ dwnStatus?.messages_synced ?? 0 }}</span>
|
|
|
|
|
<span class="text-sm text-white font-medium">{{ dwnStatus?.message_count ?? 0 }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Protocols -->
|
|
|
|
|
<div v-if="dwnStatus?.registered_protocols?.length" class="mb-4">
|
|
|
|
|
<div class="text-xs text-white/50 mb-2">Registered Protocols</div>
|
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
|
|
|
<span v-for="proto in dwnStatus.registered_protocols" :key="proto" class="px-2 py-1 rounded-md bg-blue-500/15 border border-blue-500/20 text-xs text-blue-300">
|
|
|
|
|
{{ proto }}
|
|
|
|
|
</span>
|
|
|
|
|
<div class="mb-4">
|
|
|
|
|
<div class="flex items-center justify-between mb-2">
|
|
|
|
|
<div class="text-xs text-white/50">Registered Protocols ({{ dwnProtocols.length }})</div>
|
|
|
|
|
<button @click="showRegisterProtocol = !showRegisterProtocol" class="text-xs text-blue-400 hover:text-blue-300 transition-colors">
|
|
|
|
|
{{ showRegisterProtocol ? 'Cancel' : '+ Register' }}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- Register Protocol Form -->
|
|
|
|
|
<div v-if="showRegisterProtocol" class="bg-white/5 rounded-lg p-3 mb-3">
|
|
|
|
|
<div class="flex gap-2 items-end">
|
|
|
|
|
<div class="flex-1">
|
|
|
|
|
<label class="text-xs text-white/50 block mb-1">Protocol URI</label>
|
|
|
|
|
<input v-model="newProtocolUri" type="text" placeholder="https://example.com/protocol" class="w-full bg-black/30 border border-white/10 rounded-lg px-3 py-1.5 text-sm text-white placeholder-white/30 focus:outline-none focus:border-blue-500/50" />
|
|
|
|
|
</div>
|
|
|
|
|
<label class="flex items-center gap-1.5 text-xs text-white/60 cursor-pointer whitespace-nowrap pb-1.5">
|
|
|
|
|
<input v-model="newProtocolPublished" type="checkbox" class="rounded bg-black/30 border-white/20" />
|
|
|
|
|
Published
|
|
|
|
|
</label>
|
|
|
|
|
<button @click="registerDwnProtocol" :disabled="registeringProtocol || !newProtocolUri.trim()" class="glass-button glass-button-sm px-3 rounded-lg text-xs font-medium disabled:opacity-50 whitespace-nowrap">
|
|
|
|
|
{{ registeringProtocol ? 'Registering...' : 'Register' }}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="dwnProtocols.length" class="flex flex-wrap gap-2">
|
|
|
|
|
<div v-for="proto in dwnProtocols" :key="proto.protocol" class="flex items-center gap-1.5 px-2 py-1 rounded-md bg-blue-500/15 border border-blue-500/20 text-xs text-blue-300 group">
|
|
|
|
|
<span>{{ proto.protocol }}</span>
|
|
|
|
|
<span v-if="proto.published" class="text-green-400/60" title="Published">•</span>
|
|
|
|
|
<button @click="removeDwnProtocol(proto.protocol)" :disabled="removingProtocol === proto.protocol" class="opacity-0 group-hover:opacity-100 text-red-400/60 hover:text-red-400 transition-all ml-1" title="Remove">
|
|
|
|
|
×
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-else class="text-xs text-white/30 italic">No protocols registered</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Sync Targets -->
|
|
|
|
|
@@ -1331,6 +1450,34 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Messages Browser -->
|
|
|
|
|
<div class="mb-4">
|
|
|
|
|
<div class="flex items-center justify-between mb-2">
|
|
|
|
|
<div class="text-xs text-white/50">Messages</div>
|
|
|
|
|
<button @click="toggleDwnMessages" class="text-xs text-blue-400 hover:text-blue-300 transition-colors">
|
|
|
|
|
{{ showDwnMessages ? 'Hide' : 'Browse' }}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="showDwnMessages">
|
|
|
|
|
<div v-if="loadingDwnMessages" class="text-xs text-white/40 py-4 text-center">Loading messages...</div>
|
|
|
|
|
<div v-else-if="dwnMessages.length === 0" class="text-xs text-white/30 italic py-2">No messages stored</div>
|
|
|
|
|
<div v-else class="space-y-2 max-h-64 overflow-y-auto">
|
|
|
|
|
<div v-for="msg in dwnMessages" :key="msg.record_id" class="bg-white/5 rounded-lg p-3">
|
|
|
|
|
<div class="flex items-center justify-between mb-1">
|
|
|
|
|
<span class="text-xs font-mono text-white/50 truncate max-w-[200px]" :title="msg.record_id">{{ msg.record_id.slice(0, 8) }}...</span>
|
|
|
|
|
<span class="text-xs text-white/40">{{ new Date(msg.date_created).toLocaleString() }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex flex-wrap gap-2 text-xs">
|
|
|
|
|
<span class="text-white/70">{{ msg.author }}</span>
|
|
|
|
|
<span v-if="msg.descriptor.protocol" class="text-blue-300/80">{{ msg.descriptor.protocol }}</span>
|
|
|
|
|
<span v-if="msg.descriptor.schema" class="text-purple-300/80">{{ msg.descriptor.schema }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="msg.data" class="mt-1 text-xs text-white/40 font-mono truncate">{{ JSON.stringify(msg.data).slice(0, 120) }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Last Sync & Actions -->
|
|
|
|
|
<div class="flex items-center justify-between pt-3 border-t border-white/10">
|
|
|
|
|
<div class="text-xs text-white/40">
|
|
|
|
|
@@ -1359,12 +1506,9 @@
|
|
|
|
|
<p class="text-xs text-white/60">Issue and manage W3C Verifiable Credentials</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<button @click="showIssueCredentialModal = true" class="glass-button glass-button-sm px-3 rounded-lg text-sm font-medium flex items-center gap-2">
|
|
|
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
|
|
|
|
</svg>
|
|
|
|
|
Issue
|
|
|
|
|
</button>
|
|
|
|
|
<router-link to="/dashboard/web5/credentials" class="glass-button glass-button-sm px-3 rounded-lg text-sm font-medium flex items-center gap-2">
|
|
|
|
|
Manage →
|
|
|
|
|
</router-link>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Stats -->
|
|
|
|
|
@@ -1383,76 +1527,33 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Credentials List -->
|
|
|
|
|
<!-- Credentials List (summary) -->
|
|
|
|
|
<div v-if="vcCredentials.length" class="space-y-2">
|
|
|
|
|
<div v-for="vc in vcCredentials" :key="vc.id" class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
|
|
|
<div v-for="vc in vcCredentials.slice(0, 3)" :key="vc.id" class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
|
|
|
<div class="min-w-0 flex-1">
|
|
|
|
|
<div class="text-sm text-white font-medium">{{ vc.type }}</div>
|
|
|
|
|
<div class="text-xs text-white/50 truncate">To: {{ vc.subject.slice(0, 30) }}...</div>
|
|
|
|
|
<div class="text-xs text-white/40">{{ new Date(vc.issued_at).toLocaleDateString() }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center gap-2 flex-shrink-0">
|
|
|
|
|
<span :class="{
|
|
|
|
|
'text-green-400': vc.status === 'active',
|
|
|
|
|
'text-red-400': vc.status === 'revoked',
|
|
|
|
|
'text-yellow-400': vc.status === 'expired'
|
|
|
|
|
}" class="text-xs font-medium capitalize">{{ vc.status }}</span>
|
|
|
|
|
<button v-if="vc.status === 'active'" @click="revokeCredential(vc.id)" class="text-white/30 hover:text-red-400 transition-colors p-1" title="Revoke">
|
|
|
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" /></svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<span :class="{
|
|
|
|
|
'text-green-400': vc.status === 'active',
|
|
|
|
|
'text-red-400': vc.status === 'revoked',
|
|
|
|
|
'text-yellow-400': vc.status === 'expired'
|
|
|
|
|
}" class="text-xs font-medium capitalize">{{ vc.status }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<router-link v-if="vcCredentials.length > 3" to="/dashboard/web5/credentials" class="block text-center text-xs text-white/50 hover:text-white/70 py-2 transition-colors">
|
|
|
|
|
View all {{ vcCredentials.length }} credentials →
|
|
|
|
|
</router-link>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-else class="text-center text-white/40 text-sm py-4">
|
|
|
|
|
No credentials issued yet
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Issue Credential Modal -->
|
|
|
|
|
<div v-if="showIssueCredentialModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showIssueCredentialModal = false">
|
|
|
|
|
<div class="glass-card p-6 w-full max-w-md mx-4">
|
|
|
|
|
<div class="flex items-center justify-between mb-4">
|
|
|
|
|
<h2 class="text-lg font-bold text-white">Issue Credential</h2>
|
|
|
|
|
<button @click="showIssueCredentialModal = false" class="text-white/40 hover:text-white/80 transition-colors">
|
|
|
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="space-y-3">
|
|
|
|
|
<div>
|
|
|
|
|
<label class="text-white/60 text-xs block mb-1">Issuer Identity</label>
|
|
|
|
|
<select v-model="vcIssuerIdentityId" class="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-white/30">
|
|
|
|
|
<option value="" disabled>Select issuer...</option>
|
|
|
|
|
<option v-for="id in managedIdentities" :key="id.id" :value="id.id">{{ id.name }} ({{ id.did.slice(0, 24) }}...)</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label class="text-white/60 text-xs block mb-1">Subject DID</label>
|
|
|
|
|
<input v-model="vcSubjectDid" type="text" placeholder="did:key:z6Mk..." class="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-white/30" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label class="text-white/60 text-xs block mb-1">Credential Type</label>
|
|
|
|
|
<input v-model="vcType" type="text" placeholder="MembershipCredential" class="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-white/30" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label class="text-white/60 text-xs block mb-1">Claims (JSON)</label>
|
|
|
|
|
<textarea v-model="vcClaimsJson" rows="3" placeholder='{"role": "member", "level": "gold"}' class="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white text-sm font-mono focus:outline-none focus:border-white/30"></textarea>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="vcError" class="text-xs text-red-400 mt-2">{{ vcError }}</div>
|
|
|
|
|
<div class="flex gap-3 mt-4">
|
|
|
|
|
<button @click="showIssueCredentialModal = false" class="flex-1 glass-button px-4 py-2 rounded-lg text-sm">Cancel</button>
|
|
|
|
|
<button @click="issueNewCredential" :disabled="vcIssuing || !vcIssuerIdentityId || !vcSubjectDid.trim() || !vcType.trim()" class="flex-1 glass-button px-4 py-2 rounded-lg text-sm font-medium bg-blue-500/20 border-blue-500/30 disabled:opacity-50">
|
|
|
|
|
{{ vcIssuing ? 'Issuing...' : 'Issue' }}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Domains Management Modal -->
|
|
|
|
|
<div v-if="showDomainsModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showDomainsModal = false">
|
|
|
|
|
<div class="glass-card p-6 w-full max-w-lg mx-4 max-h-[80vh] overflow-y-auto">
|
|
|
|
|
<div v-if="showDomainsModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showDomainsModal = false" @keydown.escape="showDomainsModal = false">
|
|
|
|
|
<div class="glass-card p-6 w-full max-w-lg mx-4 max-h-[80vh] overflow-y-auto" role="dialog" aria-modal="true" aria-labelledby="domains-title">
|
|
|
|
|
<div class="flex items-center justify-between mb-4">
|
|
|
|
|
<h2 class="text-lg font-bold text-white">Domain Names</h2>
|
|
|
|
|
<h2 id="domains-title" class="text-lg font-bold text-white">Domain Names</h2>
|
|
|
|
|
<button @click="showDomainsModal = false" class="text-white/40 hover:text-white/80 transition-colors">
|
|
|
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
|
|
|
|
</button>
|
|
|
|
|
@@ -1526,10 +1627,10 @@
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Relay Management Modal -->
|
|
|
|
|
<div v-if="showRelaysModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showRelaysModal = false">
|
|
|
|
|
<div class="glass-card p-6 w-full max-w-lg mx-4 max-h-[80vh] overflow-y-auto">
|
|
|
|
|
<div v-if="showRelaysModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showRelaysModal = false" @keydown.escape="showRelaysModal = false">
|
|
|
|
|
<div class="glass-card p-6 w-full max-w-lg mx-4 max-h-[80vh] overflow-y-auto" role="dialog" aria-modal="true" aria-labelledby="relays-title">
|
|
|
|
|
<div class="flex items-center justify-between mb-4">
|
|
|
|
|
<h2 class="text-lg font-bold text-white">Nostr Relays</h2>
|
|
|
|
|
<h2 id="relays-title" class="text-lg font-bold text-white">Nostr Relays</h2>
|
|
|
|
|
<button @click="showRelaysModal = false" class="text-white/40 hover:text-white/80 transition-colors">
|
|
|
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
|
|
|
|
</button>
|
|
|
|
|
@@ -1718,13 +1819,6 @@ interface VCData {
|
|
|
|
|
status: string
|
|
|
|
|
}
|
|
|
|
|
const vcCredentials = ref<VCData[]>([])
|
|
|
|
|
const showIssueCredentialModal = ref(false)
|
|
|
|
|
const vcIssuerIdentityId = ref('')
|
|
|
|
|
const vcSubjectDid = ref('')
|
|
|
|
|
const vcType = ref('VerifiableCredential')
|
|
|
|
|
const vcClaimsJson = ref('{}')
|
|
|
|
|
const vcIssuing = ref(false)
|
|
|
|
|
const vcError = ref('')
|
|
|
|
|
|
|
|
|
|
async function loadCredentials() {
|
|
|
|
|
try {
|
|
|
|
|
@@ -1735,40 +1829,6 @@ async function loadCredentials() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function issueNewCredential() {
|
|
|
|
|
if (!vcIssuerIdentityId.value || !vcSubjectDid.value.trim() || !vcType.value.trim()) return
|
|
|
|
|
vcIssuing.value = true
|
|
|
|
|
vcError.value = ''
|
|
|
|
|
try {
|
|
|
|
|
let claims: Record<string, unknown> = {}
|
|
|
|
|
try { claims = JSON.parse(vcClaimsJson.value) } catch { claims = {} }
|
|
|
|
|
await rpcClient.call({ method: 'identity.issue-credential', params: {
|
|
|
|
|
issuer_id: vcIssuerIdentityId.value,
|
|
|
|
|
subject_did: vcSubjectDid.value.trim(),
|
|
|
|
|
type: vcType.value.trim(),
|
|
|
|
|
claims,
|
|
|
|
|
}})
|
|
|
|
|
showIssueCredentialModal.value = false
|
|
|
|
|
vcSubjectDid.value = ''
|
|
|
|
|
vcType.value = 'VerifiableCredential'
|
|
|
|
|
vcClaimsJson.value = '{}'
|
|
|
|
|
await loadCredentials()
|
|
|
|
|
} catch (e: unknown) {
|
|
|
|
|
vcError.value = e instanceof Error ? e.message : 'Failed to issue credential'
|
|
|
|
|
} finally {
|
|
|
|
|
vcIssuing.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function revokeCredential(id: string) {
|
|
|
|
|
try {
|
|
|
|
|
await rpcClient.call({ method: 'identity.revoke-credential', params: { id } })
|
|
|
|
|
await loadCredentials()
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (import.meta.env.DEV) console.warn('Silent fail for revocation', e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Nostr Relay Functions ---
|
|
|
|
|
async function loadNostrRelays() {
|
|
|
|
|
try {
|
|
|
|
|
@@ -1861,6 +1921,45 @@ async function copyDid() {
|
|
|
|
|
setTimeout(() => { didCopied.value = false }, 2000)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DID Document modal
|
|
|
|
|
const showDidDocModal = ref(false)
|
|
|
|
|
const loadingDidDoc = ref(false)
|
|
|
|
|
const didDocumentData = ref<Record<string, unknown> | null>(null)
|
|
|
|
|
const didDocVerified = ref<boolean | null>(null)
|
|
|
|
|
const didDocCopied = ref(false)
|
|
|
|
|
|
|
|
|
|
const didDocumentFormatted = computed(() =>
|
|
|
|
|
didDocumentData.value ? JSON.stringify(didDocumentData.value, null, 2) : ''
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async function showDidDocument() {
|
|
|
|
|
showDidDocModal.value = true
|
|
|
|
|
loadingDidDoc.value = true
|
|
|
|
|
didDocVerified.value = null
|
|
|
|
|
try {
|
|
|
|
|
const doc = await rpcClient.resolveDid()
|
|
|
|
|
didDocumentData.value = doc
|
|
|
|
|
// Verify the document
|
|
|
|
|
const verification = await rpcClient.call({
|
|
|
|
|
method: 'identity.verify-did-document',
|
|
|
|
|
params: { document: doc },
|
|
|
|
|
}) as { valid: boolean }
|
|
|
|
|
didDocVerified.value = verification.valid
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (import.meta.env.DEV) console.error('Failed to load DID Document:', err)
|
|
|
|
|
didDocumentData.value = null
|
|
|
|
|
} finally {
|
|
|
|
|
loadingDidDoc.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function copyDidDocument() {
|
|
|
|
|
if (!didDocumentFormatted.value) return
|
|
|
|
|
await navigator.clipboard.writeText(didDocumentFormatted.value)
|
|
|
|
|
didDocCopied.value = true
|
|
|
|
|
setTimeout(() => { didDocCopied.value = false }, 2000)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DWN Status & Sync
|
|
|
|
|
interface DwnStatusData {
|
|
|
|
|
running: boolean
|
|
|
|
|
@@ -1869,12 +1968,43 @@ interface DwnStatusData {
|
|
|
|
|
last_sync: string | null
|
|
|
|
|
messages_synced: number
|
|
|
|
|
storage_bytes: number
|
|
|
|
|
message_count: number
|
|
|
|
|
protocol_count: number
|
|
|
|
|
registered_protocols: string[]
|
|
|
|
|
peer_sync_targets: string[]
|
|
|
|
|
}
|
|
|
|
|
interface DwnProtocol {
|
|
|
|
|
protocol: string
|
|
|
|
|
published: boolean
|
|
|
|
|
types: Record<string, unknown>
|
|
|
|
|
structure: Record<string, unknown>
|
|
|
|
|
dateRegistered: string
|
|
|
|
|
}
|
|
|
|
|
interface DwnMessageEntry {
|
|
|
|
|
record_id: string
|
|
|
|
|
author: string
|
|
|
|
|
date_created: string
|
|
|
|
|
descriptor: {
|
|
|
|
|
interface: string
|
|
|
|
|
method: string
|
|
|
|
|
protocol?: string
|
|
|
|
|
schema?: string
|
|
|
|
|
dataFormat?: string
|
|
|
|
|
}
|
|
|
|
|
data?: unknown
|
|
|
|
|
}
|
|
|
|
|
const dwnStatus = ref<DwnStatusData | null>(null)
|
|
|
|
|
const dwnSyncStatus = ref<'synced' | 'syncing' | 'error' | 'idle'>('idle')
|
|
|
|
|
const syncingDWNs = ref(false)
|
|
|
|
|
const dwnProtocols = ref<DwnProtocol[]>([])
|
|
|
|
|
const dwnMessages = ref<DwnMessageEntry[]>([])
|
|
|
|
|
const showDwnMessages = ref(false)
|
|
|
|
|
const loadingDwnMessages = ref(false)
|
|
|
|
|
const showRegisterProtocol = ref(false)
|
|
|
|
|
const newProtocolUri = ref('')
|
|
|
|
|
const newProtocolPublished = ref(false)
|
|
|
|
|
const registeringProtocol = ref(false)
|
|
|
|
|
const removingProtocol = ref<string | null>(null)
|
|
|
|
|
|
|
|
|
|
const formatDwnStorage = computed(() => {
|
|
|
|
|
if (!dwnStatus.value) return '0 B'
|
|
|
|
|
@@ -1927,6 +2057,25 @@ const peerReachableLocal = ref<Record<string, boolean>>({})
|
|
|
|
|
const peerReachable = computed(() => ({ ...appStore.peerHealth, ...peerReachableLocal.value }))
|
|
|
|
|
const connectedNodesCount = computed(() => peers.value.length)
|
|
|
|
|
|
|
|
|
|
// Hardware wallet detection
|
|
|
|
|
interface HwWalletDevice {
|
|
|
|
|
type: string
|
|
|
|
|
vendor_id: string
|
|
|
|
|
product_id: string
|
|
|
|
|
manufacturer: string
|
|
|
|
|
product: string
|
|
|
|
|
}
|
|
|
|
|
const detectedHwWallets = ref<HwWalletDevice[]>([])
|
|
|
|
|
|
|
|
|
|
async function detectHardwareWallets() {
|
|
|
|
|
try {
|
|
|
|
|
const res = await rpcClient.detectUsbDevices()
|
|
|
|
|
detectedHwWallets.value = res.devices || []
|
|
|
|
|
} catch {
|
|
|
|
|
detectedHwWallets.value = []
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Send Message modal
|
|
|
|
|
const showSendMessageModal = ref(false)
|
|
|
|
|
const sendMessageModalRef = ref<HTMLElement | null>(null)
|
|
|
|
|
@@ -1981,7 +2130,7 @@ async function loadPeers() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Failed to load peers:', e)
|
|
|
|
|
if (import.meta.env.DEV) console.error('Failed to load peers:', e)
|
|
|
|
|
} finally {
|
|
|
|
|
loadingPeers.value = false
|
|
|
|
|
}
|
|
|
|
|
@@ -2023,7 +2172,7 @@ async function discoverAndAddPeers() {
|
|
|
|
|
}
|
|
|
|
|
await loadPeers()
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Discover failed:', e)
|
|
|
|
|
if (import.meta.env.DEV) console.error('Discover failed:', e)
|
|
|
|
|
} finally {
|
|
|
|
|
discovering.value = false
|
|
|
|
|
}
|
|
|
|
|
@@ -2046,6 +2195,10 @@ const unifiedSendProcessing = ref(false)
|
|
|
|
|
const unifiedSendError = ref('')
|
|
|
|
|
const sendResultTxid = ref('')
|
|
|
|
|
const sendResultHash = ref('')
|
|
|
|
|
const useHardwareWallet = ref(false)
|
|
|
|
|
const psbtData = ref('')
|
|
|
|
|
const psbtStep = ref<'idle' | 'created' | 'finalizing'>('idle')
|
|
|
|
|
const signedPsbtInput = ref('')
|
|
|
|
|
|
|
|
|
|
// Unified Receive
|
|
|
|
|
const showUnifiedReceiveModal = ref(false)
|
|
|
|
|
@@ -2089,6 +2242,9 @@ function closeUnifiedSendModal() {
|
|
|
|
|
unifiedSendError.value = ''
|
|
|
|
|
sendResultTxid.value = ''
|
|
|
|
|
sendResultHash.value = ''
|
|
|
|
|
psbtData.value = ''
|
|
|
|
|
psbtStep.value = 'idle'
|
|
|
|
|
signedPsbtInput.value = ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeUnifiedReceiveModal() {
|
|
|
|
|
@@ -2131,6 +2287,17 @@ async function unifiedSend() {
|
|
|
|
|
unifiedSendError.value = 'Enter a Bitcoin address'
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (useHardwareWallet.value) {
|
|
|
|
|
// Hardware wallet flow: create unsigned PSBT
|
|
|
|
|
const res = await rpcClient.createPsbt({
|
|
|
|
|
outputs: [{ address: unifiedSendDest.value.trim(), amount_sats: unifiedSendAmount.value }],
|
|
|
|
|
})
|
|
|
|
|
psbtData.value = res.psbt_base64
|
|
|
|
|
psbtStep.value = 'created'
|
|
|
|
|
signedPsbtInput.value = ''
|
|
|
|
|
unifiedSendProcessing.value = false
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const res = await rpcClient.call<{ txid: string }>({
|
|
|
|
|
method: 'lnd.sendcoins',
|
|
|
|
|
params: { addr: unifiedSendDest.value.trim(), amount: unifiedSendAmount.value },
|
|
|
|
|
@@ -2146,6 +2313,53 @@ async function unifiedSend() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function finalizePsbt() {
|
|
|
|
|
if (!signedPsbtInput.value.trim() || unifiedSendProcessing.value) return
|
|
|
|
|
unifiedSendProcessing.value = true
|
|
|
|
|
unifiedSendError.value = ''
|
|
|
|
|
try {
|
|
|
|
|
await rpcClient.finalizePsbt(signedPsbtInput.value.trim())
|
|
|
|
|
psbtStep.value = 'idle'
|
|
|
|
|
psbtData.value = ''
|
|
|
|
|
signedPsbtInput.value = ''
|
|
|
|
|
sendResultTxid.value = 'Broadcast via hardware wallet'
|
|
|
|
|
await loadLndBalances()
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
unifiedSendError.value = err instanceof Error ? err.message : 'Broadcast failed'
|
|
|
|
|
} finally {
|
|
|
|
|
unifiedSendProcessing.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function copyPsbt() {
|
|
|
|
|
if (!psbtData.value) return
|
|
|
|
|
window.navigator.clipboard.writeText(psbtData.value)
|
|
|
|
|
unifiedSendError.value = 'PSBT copied!'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function downloadPsbt() {
|
|
|
|
|
if (!psbtData.value) return
|
|
|
|
|
const blob = new Blob([psbtData.value], { type: 'text/plain' })
|
|
|
|
|
const url = URL.createObjectURL(blob)
|
|
|
|
|
const a = document.createElement('a')
|
|
|
|
|
a.href = url
|
|
|
|
|
a.download = 'transaction.psbt'
|
|
|
|
|
a.click()
|
|
|
|
|
URL.revokeObjectURL(url)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handlePsbtFileUpload(event: Event) {
|
|
|
|
|
const input = event.target as HTMLInputElement
|
|
|
|
|
const file = input.files?.[0]
|
|
|
|
|
if (!file) return
|
|
|
|
|
const reader = new FileReader()
|
|
|
|
|
reader.onload = (e) => {
|
|
|
|
|
signedPsbtInput.value = (e.target?.result as string) || ''
|
|
|
|
|
}
|
|
|
|
|
reader.readAsText(file)
|
|
|
|
|
input.value = ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function unifiedReceive() {
|
|
|
|
|
if (unifiedReceiveProcessing.value) return
|
|
|
|
|
unifiedReceiveProcessing.value = true
|
|
|
|
|
@@ -2708,10 +2922,12 @@ onMounted(() => {
|
|
|
|
|
loadContentItems()
|
|
|
|
|
loadNetworkingProfits()
|
|
|
|
|
loadDwnStatus()
|
|
|
|
|
loadDwnProtocols()
|
|
|
|
|
loadDomainNames()
|
|
|
|
|
loadNostrRelays()
|
|
|
|
|
loadCredentials()
|
|
|
|
|
loadLndBalances()
|
|
|
|
|
detectHardwareWallets()
|
|
|
|
|
// Open Messages tab when navigated via toast (e.g. ?tab=messages)
|
|
|
|
|
if (route.query.tab === 'messages') {
|
|
|
|
|
nodesContainerTab.value = 'messages'
|
|
|
|
|
@@ -2747,6 +2963,64 @@ async function syncDWNs() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadDwnProtocols() {
|
|
|
|
|
try {
|
|
|
|
|
const res = await rpcClient.call<{ protocols: DwnProtocol[] }>({ method: 'dwn.list-protocols' })
|
|
|
|
|
dwnProtocols.value = res.protocols || []
|
|
|
|
|
} catch {
|
|
|
|
|
dwnProtocols.value = []
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function registerDwnProtocol() {
|
|
|
|
|
if (registeringProtocol.value || !newProtocolUri.value.trim()) return
|
|
|
|
|
registeringProtocol.value = true
|
|
|
|
|
try {
|
|
|
|
|
await rpcClient.call({ method: 'dwn.register-protocol', params: { protocol: newProtocolUri.value.trim(), published: newProtocolPublished.value } })
|
|
|
|
|
newProtocolUri.value = ''
|
|
|
|
|
newProtocolPublished.value = false
|
|
|
|
|
showRegisterProtocol.value = false
|
|
|
|
|
await loadDwnProtocols()
|
|
|
|
|
await loadDwnStatus()
|
|
|
|
|
} catch {
|
|
|
|
|
if (import.meta.env.DEV) console.error('Failed to register protocol')
|
|
|
|
|
} finally {
|
|
|
|
|
registeringProtocol.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function removeDwnProtocol(protocol: string) {
|
|
|
|
|
removingProtocol.value = protocol
|
|
|
|
|
try {
|
|
|
|
|
await rpcClient.call({ method: 'dwn.remove-protocol', params: { protocol } })
|
|
|
|
|
await loadDwnProtocols()
|
|
|
|
|
await loadDwnStatus()
|
|
|
|
|
} catch {
|
|
|
|
|
if (import.meta.env.DEV) console.error('Failed to remove protocol')
|
|
|
|
|
} finally {
|
|
|
|
|
removingProtocol.value = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function toggleDwnMessages() {
|
|
|
|
|
showDwnMessages.value = !showDwnMessages.value
|
|
|
|
|
if (showDwnMessages.value) {
|
|
|
|
|
await loadDwnMessages()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadDwnMessages() {
|
|
|
|
|
loadingDwnMessages.value = true
|
|
|
|
|
try {
|
|
|
|
|
const res = await rpcClient.call<{ messages: DwnMessageEntry[]; count: number }>({ method: 'dwn.query-messages', params: { limit: 50 } })
|
|
|
|
|
dwnMessages.value = res.messages || []
|
|
|
|
|
} catch {
|
|
|
|
|
dwnMessages.value = []
|
|
|
|
|
} finally {
|
|
|
|
|
loadingDwnMessages.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadLndBalances() {
|
|
|
|
|
try {
|
|
|
|
|
const res = await rpcClient.call<{
|
|
|
|
|
@@ -2776,7 +3050,7 @@ async function connectWallet() {
|
|
|
|
|
|
|
|
|
|
function manageRelays() {
|
|
|
|
|
// TODO: Navigate to relay management or open modal
|
|
|
|
|
console.log('Managing Nostr relays...')
|
|
|
|
|
if (import.meta.env.DEV) console.log('Managing Nostr relays...')
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|