native ui + touch id unlock + portfolio dashboard
This commit is contained in:
+93
-61
@@ -6,77 +6,78 @@
|
|||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: oklch(1 0 0);
|
--background: oklch(0.99 0 0);
|
||||||
--foreground: oklch(0.145 0 0);
|
--foreground: oklch(0.16 0 0);
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0);
|
||||||
--card-foreground: oklch(0.145 0 0);
|
--card-foreground: oklch(0.16 0 0);
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
--popover-foreground: oklch(0.16 0 0);
|
||||||
--primary: oklch(0.553 0.195 38.402);
|
--primary: oklch(0.4 0 0);
|
||||||
--primary-foreground: oklch(0.98 0.016 73.684);
|
--primary-foreground: oklch(0.99 0 0);
|
||||||
--secondary: oklch(0.967 0.001 286.375);
|
--secondary: oklch(0.96 0 0);
|
||||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
--secondary-foreground: oklch(0.2 0 0);
|
||||||
--muted: oklch(0.97 0 0);
|
--muted: oklch(0.965 0 0);
|
||||||
--muted-foreground: oklch(0.556 0 0);
|
--muted-foreground: oklch(0.52 0 0);
|
||||||
--accent: oklch(0.553 0.195 38.402);
|
--accent: oklch(0.94 0 0);
|
||||||
--accent-foreground: oklch(0.98 0.016 73.684);
|
--accent-foreground: oklch(0.2 0 0);
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--border: oklch(0.922 0 0);
|
--border: oklch(0.905 0 0);
|
||||||
--input: oklch(0.922 0 0);
|
--input: oklch(0.905 0 0);
|
||||||
--ring: oklch(0.708 0 0);
|
--ring: oklch(0.6 0 0);
|
||||||
--chart-1: oklch(0.837 0.128 66.29);
|
--chart-1: oklch(0.4 0 0);
|
||||||
--chart-2: oklch(0.705 0.213 47.604);
|
--chart-2: oklch(0.55 0 0);
|
||||||
--chart-3: oklch(0.646 0.222 41.116);
|
--chart-3: oklch(0.68 0 0);
|
||||||
--chart-4: oklch(0.553 0.195 38.402);
|
--chart-4: oklch(0.78 0 0);
|
||||||
--chart-5: oklch(0.47 0.157 37.304);
|
--chart-5: oklch(0.88 0 0);
|
||||||
--radius: 0rem;
|
--radius: 0.625rem;
|
||||||
--sidebar: oklch(0.985 0 0);
|
--sidebar: oklch(0.97 0 0);
|
||||||
--sidebar-foreground: oklch(0.145 0 0);
|
--sidebar-foreground: oklch(0.2 0 0);
|
||||||
--sidebar-primary: oklch(0.646 0.222 41.116);
|
--sidebar-primary: oklch(0.35 0 0);
|
||||||
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
|
--sidebar-primary-foreground: oklch(0.99 0 0);
|
||||||
--sidebar-accent: oklch(0.97 0 0);
|
--sidebar-accent: oklch(0.92 0 0);
|
||||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
--sidebar-accent-foreground: oklch(0.2 0 0);
|
||||||
--sidebar-border: oklch(0.922 0 0);
|
--sidebar-border: oklch(0.905 0 0);
|
||||||
--sidebar-ring: oklch(0.708 0 0);
|
--sidebar-ring: oklch(0.6 0 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.145 0 0);
|
--background: oklch(0.21 0 0);
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.96 0 0);
|
||||||
--card: oklch(0.205 0 0);
|
--card: oklch(0.235 0 0);
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--card-foreground: oklch(0.96 0 0);
|
||||||
--popover: oklch(0.205 0 0);
|
--popover: oklch(0.25 0 0);
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
--popover-foreground: oklch(0.96 0 0);
|
||||||
--primary: oklch(0.47 0.157 37.304);
|
--primary: oklch(0.5 0 0);
|
||||||
--primary-foreground: oklch(0.98 0.016 73.684);
|
--primary-foreground: oklch(0.99 0 0);
|
||||||
--secondary: oklch(0.274 0.006 286.033);
|
--secondary: oklch(0.29 0 0);
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--secondary-foreground: oklch(0.96 0 0);
|
||||||
--muted: oklch(0.269 0 0);
|
--muted: oklch(0.27 0 0);
|
||||||
--muted-foreground: oklch(0.708 0 0);
|
--muted-foreground: oklch(0.66 0 0);
|
||||||
--accent: oklch(0.47 0.157 37.304);
|
--accent: oklch(0.3 0 0);
|
||||||
--accent-foreground: oklch(0.98 0.016 73.684);
|
--accent-foreground: oklch(0.96 0 0);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
--border: oklch(1 0 0 / 10%);
|
--border: oklch(1 0 0 / 12%);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: oklch(1 0 0 / 16%);
|
||||||
--ring: oklch(0.556 0 0);
|
--ring: oklch(0.55 0 0);
|
||||||
--chart-1: oklch(0.837 0.128 66.29);
|
--chart-1: oklch(0.7 0 0);
|
||||||
--chart-2: oklch(0.705 0.213 47.604);
|
--chart-2: oklch(0.6 0 0);
|
||||||
--chart-3: oklch(0.646 0.222 41.116);
|
--chart-3: oklch(0.5 0 0);
|
||||||
--chart-4: oklch(0.553 0.195 38.402);
|
--chart-4: oklch(0.4 0 0);
|
||||||
--chart-5: oklch(0.47 0.157 37.304);
|
--chart-5: oklch(0.3 0 0);
|
||||||
--sidebar: oklch(0.205 0 0);
|
--sidebar: oklch(0.205 0 0);
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
--sidebar-foreground: oklch(0.92 0 0);
|
||||||
--sidebar-primary: oklch(0.705 0.213 47.604);
|
--sidebar-primary: oklch(0.7 0 0);
|
||||||
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
|
--sidebar-primary-foreground: oklch(0.1 0 0);
|
||||||
--sidebar-accent: oklch(0.269 0 0);
|
--sidebar-accent: oklch(0.28 0 0);
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
--sidebar-accent-foreground: oklch(0.96 0 0);
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
--sidebar-ring: oklch(0.556 0 0);
|
--sidebar-ring: oklch(0.55 0 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--font-mono: 'Geist Mono Variable', monospace;
|
--font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Segoe UI", system-ui, sans-serif;
|
||||||
--font-heading: 'Geist Mono Variable', monospace;
|
--font-mono: 'Geist Mono Variable', ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
--font-heading: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", system-ui, sans-serif;
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
@@ -121,10 +122,41 @@
|
|||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
|
html {
|
||||||
|
@apply font-sans;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
font-feature-settings: "ss01", "cv01";
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
html {
|
::selection {
|
||||||
@apply font-mono;
|
background: oklch(0.5 0 0 / 25%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: oklch(0.5 0 0 / 25%) transparent;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: oklch(0.5 0 0 / 22%);
|
||||||
|
border-radius: 9999px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
background-clip: content-box;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: oklch(0.5 0 0 / 38%);
|
||||||
|
background-clip: content-box;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,16 +353,44 @@ export async function getTransferDetails(
|
|||||||
txid: string,
|
txid: string,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
if (!state.wallet) throw new Error('Wallet RPC not started')
|
if (!state.wallet) throw new Error('Wallet RPC not started')
|
||||||
|
let t: any = null
|
||||||
|
let dests: any[] = []
|
||||||
|
try {
|
||||||
const result = await rawRpc(state, 'get_transfer_by_txid', { txid })
|
const result = await rawRpc(state, 'get_transfer_by_txid', { txid })
|
||||||
if (!result?.transfer) return null
|
if (result?.transfer) {
|
||||||
const t = result.transfer
|
t = result.transfer
|
||||||
const isIn = t.type === 'in'
|
dests =
|
||||||
const isOut = t.type === 'out'
|
|
||||||
const dests =
|
|
||||||
t.destinations?.map((d: any) => ({
|
t.destinations?.map((d: any) => ({
|
||||||
address: d.address,
|
address: d.address,
|
||||||
amount: d.amount?.toString() ?? '0',
|
amount: d.amount?.toString() ?? '0',
|
||||||
})) ?? []
|
})) ?? []
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// get_transfer_by_txid can fail for various reasons; fall back
|
||||||
|
}
|
||||||
|
if (!t) {
|
||||||
|
const params: Record<string, any> = {
|
||||||
|
in: true,
|
||||||
|
out: true,
|
||||||
|
pending: true,
|
||||||
|
pool: true,
|
||||||
|
}
|
||||||
|
const result = await rawRpc(state, 'get_transfers', params)
|
||||||
|
let found: any = null
|
||||||
|
for (const dir of ['in', 'out', 'pending', 'pool'] as const) {
|
||||||
|
const transfers = result[dir] ?? []
|
||||||
|
for (const tr of transfers) {
|
||||||
|
if (tr.txid === txid) {
|
||||||
|
found = { ...tr, type: dir === 'in' ? 'in' : 'out' }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found) break
|
||||||
|
}
|
||||||
|
if (!found) return null
|
||||||
|
t = found
|
||||||
|
}
|
||||||
|
const isIn = t.type === 'in'
|
||||||
const subaddrIndices = isIn
|
const subaddrIndices = isIn
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
|
|||||||
Binary file not shown.
@@ -246,6 +246,22 @@ export const MoneroWallet = () => {
|
|||||||
await refresh()
|
await refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const autoUnlock = async (name?: string) => {
|
||||||
|
if (!electrobun.rpc) return false
|
||||||
|
const target = name ?? wallets[0]
|
||||||
|
if (!target) return false
|
||||||
|
const pw = await moneroGetStoredPassword(target)
|
||||||
|
if (!pw) return false
|
||||||
|
await electrobun.rpc.request.moneroOpenWallet({
|
||||||
|
name: target,
|
||||||
|
password: pw,
|
||||||
|
})
|
||||||
|
walletName = target
|
||||||
|
walletOpen = true
|
||||||
|
await refresh()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
if (!electrobun.rpc) return
|
if (!electrobun.rpc) return
|
||||||
loading = true
|
loading = true
|
||||||
@@ -430,6 +446,7 @@ export const MoneroWallet = () => {
|
|||||||
createWallet,
|
createWallet,
|
||||||
restoreWallet,
|
restoreWallet,
|
||||||
openWallet,
|
openWallet,
|
||||||
|
autoUnlock,
|
||||||
refresh,
|
refresh,
|
||||||
send,
|
send,
|
||||||
sendAll,
|
sendAll,
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { electrobun } from '$lib/electrobun'
|
||||||
|
|
||||||
|
let unlocked = $state(false)
|
||||||
|
let biometricAvailable = $state(false)
|
||||||
|
let unlocking = $state(false)
|
||||||
|
let error = $state('')
|
||||||
|
|
||||||
|
const checkBiometric = async () => {
|
||||||
|
if (!electrobun.rpc) return
|
||||||
|
try {
|
||||||
|
biometricAvailable = await electrobun.rpc.request.biometricCanAuth({})
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
const unlock = async (): Promise<boolean> => {
|
||||||
|
if (!electrobun.rpc) return false
|
||||||
|
unlocking = true
|
||||||
|
error = ''
|
||||||
|
try {
|
||||||
|
const ok = await electrobun.rpc.request.biometricAuth({
|
||||||
|
reason: 'Unlock Koins',
|
||||||
|
})
|
||||||
|
if (ok) {
|
||||||
|
unlocked = true
|
||||||
|
} else {
|
||||||
|
error = 'Authentication failed'
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
} catch (e) {
|
||||||
|
error = e instanceof Error ? e.message : 'Unlock failed'
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
unlocking = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lock = () => {
|
||||||
|
unlocked = false
|
||||||
|
error = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
checkBiometric()
|
||||||
|
|
||||||
|
export const session = {
|
||||||
|
get unlocked() {
|
||||||
|
return unlocked
|
||||||
|
},
|
||||||
|
get biometricAvailable() {
|
||||||
|
return biometricAvailable
|
||||||
|
},
|
||||||
|
get unlocking() {
|
||||||
|
return unlocking
|
||||||
|
},
|
||||||
|
get error() {
|
||||||
|
return error
|
||||||
|
},
|
||||||
|
unlock,
|
||||||
|
lock,
|
||||||
|
}
|
||||||
@@ -179,6 +179,28 @@ export const XrpWallet = () => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const autoUnlock = async (walletId?: string) => {
|
||||||
|
const id = walletId ?? currentWalletId ?? wallets[0]?.id
|
||||||
|
if (!id) return false
|
||||||
|
currentWalletId = id
|
||||||
|
const wallet = wallets.find((w) => w.id === id)
|
||||||
|
if (!wallet) return false
|
||||||
|
if (electrobun.rpc && wallet.hasPassword) {
|
||||||
|
const [ph] = await tryCatch(
|
||||||
|
electrobun.rpc.request.getSecret({
|
||||||
|
service: 'koins',
|
||||||
|
name: `xrp_auth_${id}`,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if (ph) currentPasswordHash = ph
|
||||||
|
} else {
|
||||||
|
currentPasswordHash = null
|
||||||
|
}
|
||||||
|
const ok = await loadSeedForWallet(id)
|
||||||
|
if (ok) accountType = 'xrp'
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
const saveWallet = async (
|
const saveWallet = async (
|
||||||
kind: 'create' | 'import',
|
kind: 'create' | 'import',
|
||||||
name: string,
|
name: string,
|
||||||
@@ -412,6 +434,7 @@ export const XrpWallet = () => {
|
|||||||
hashPassword,
|
hashPassword,
|
||||||
selectWallet,
|
selectWallet,
|
||||||
selectAndUnlockWallet,
|
selectAndUnlockWallet,
|
||||||
|
autoUnlock,
|
||||||
loadSeedForWallet,
|
loadSeedForWallet,
|
||||||
deleteWallet,
|
deleteWallet,
|
||||||
clearSelection,
|
clearSelection,
|
||||||
|
|||||||
@@ -1,31 +1,125 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { moneroWallet } from '$lib/states/monero-wallet.svelte.js'
|
import { moneroWallet } from '$lib/states/monero-wallet.svelte.js'
|
||||||
import { xrpWallet } from '$lib/states/xrp-wallet.svelte.js'
|
import { xrpWallet } from '$lib/states/xrp-wallet.svelte.js'
|
||||||
import { Button } from '$lib/components/ui/button/index.js'
|
import { electrobun } from '$lib/electrobun.js'
|
||||||
|
import { navigate } from 'sv-router/generated'
|
||||||
|
import { onMount } from 'svelte'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
} from '$lib/components/ui/card/index.js'
|
} from '$lib/components/ui/card/index.js'
|
||||||
import { navigate } from 'sv-router/generated'
|
import { Button } from '$lib/components/ui/button/index.js'
|
||||||
import { onMount } from 'svelte'
|
import Plus from '@lucide/svelte/icons/plus'
|
||||||
|
import WalletIcon from '@lucide/svelte/icons/wallet'
|
||||||
|
import Loader from '$lib/components/loader.svelte'
|
||||||
import { frontendLog, frontendLogError } from '$lib/electrobun.js'
|
import { frontendLog, frontendLogError } from '$lib/electrobun.js'
|
||||||
|
|
||||||
|
let xrpPrice = $state<string | null>(null)
|
||||||
|
let xmrPrice = $state<string | null>(null)
|
||||||
|
let xrpBalances = $state<Record<string, { balance: number; funded: boolean }>>({})
|
||||||
|
let loadingXrp = $state(true)
|
||||||
|
let loadingXmr = $state(true)
|
||||||
|
|
||||||
let feError = $state<string | null>(null)
|
let feError = $state<string | null>(null)
|
||||||
|
|
||||||
|
const usd = (amount: string, price: string | null) =>
|
||||||
|
price ? parseFloat(amount) * parseFloat(price) : 0
|
||||||
|
|
||||||
|
const xrpTotalUsd = $derived.by(() => {
|
||||||
|
const price = xrpPrice
|
||||||
|
return xrpWallet.wallets.reduce((sum, w) => {
|
||||||
|
const b = xrpBalances[w.address]?.balance ?? 0
|
||||||
|
return sum + (price ? b * parseFloat(price) : 0)
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const xrpTotalBalance = $derived.by(() =>
|
||||||
|
xrpWallet.wallets.reduce((sum, w) => {
|
||||||
|
return sum + (xrpBalances[w.address]?.balance ?? 0)
|
||||||
|
}, 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
const xmrBalance = $derived(
|
||||||
|
moneroWallet.walletOpen ? parseFloat(moneroWallet.balance || '0') : 0,
|
||||||
|
)
|
||||||
|
const xmrTotalUsd = $derived(
|
||||||
|
moneroWallet.walletOpen && xmrPrice
|
||||||
|
? xmrBalance * parseFloat(xmrPrice)
|
||||||
|
: 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
const portfolioUsd = $derived(xrpTotalUsd + xmrTotalUsd)
|
||||||
|
|
||||||
|
const hasXrp = $derived(xrpWallet.wallets.length > 0)
|
||||||
|
const hasXmr = $derived(moneroWallet.wallets.length > 0)
|
||||||
|
const noWallets = $derived(!hasXrp && !hasXmr)
|
||||||
|
|
||||||
|
const fmtUsd = (v: number) =>
|
||||||
|
v.toLocaleString(undefined, {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
const fmtBal = (v: number, dp = 2) =>
|
||||||
|
v.toLocaleString(undefined, {
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
maximumFractionDigits: dp,
|
||||||
|
})
|
||||||
|
|
||||||
|
async function fetchXrp() {
|
||||||
|
loadingXrp = true
|
||||||
|
const rpc = electrobun.rpc
|
||||||
|
if (!rpc) {
|
||||||
|
loadingXrp = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const p = await rpc.request.fetchXrpPrice({})
|
||||||
|
if (p) xrpPrice = p.usd
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
await Promise.allSettled(
|
||||||
|
xrpWallet.wallets.map(async (w) => {
|
||||||
|
const res = await rpc.request.xrpGetBalance({
|
||||||
|
address: w.address,
|
||||||
|
})
|
||||||
|
xrpBalances[w.address] = {
|
||||||
|
balance: parseFloat(res.balance),
|
||||||
|
funded: res.funded,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
loadingXrp = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchXmr() {
|
||||||
|
loadingXmr = true
|
||||||
|
const rpc = electrobun.rpc
|
||||||
|
if (!rpc) {
|
||||||
|
loadingXmr = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const p = await rpc.request.fetchMoneroPrice({})
|
||||||
|
xmrPrice = p?.usd ?? null
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
loadingXmr = false
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
frontendLog('index.svelte mounted')
|
frontendLog('dashboard mounted')
|
||||||
moneroWallet.init().catch((e) => frontendLogError('moneroWallet.init() failed', e))
|
moneroWallet.init().catch((e) => frontendLogError('moneroWallet.init() failed', e))
|
||||||
xrpWallet.init().catch((e) => frontendLogError('xrpWallet.init() failed', e))
|
xrpWallet.init().then(async () => {
|
||||||
|
await fetchXrp()
|
||||||
|
})
|
||||||
|
fetchXmr()
|
||||||
})
|
})
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (moneroWallet.accountType === 'monero') {
|
if (moneroWallet.walletOpen) {
|
||||||
navigate('/monero')
|
fetchXmr()
|
||||||
} else if (xrpWallet.accountType === 'xrp') {
|
|
||||||
navigate('/xrp')
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -61,43 +155,168 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="mx-auto mt-24 max-w-sm">
|
<div class="space-y-6">
|
||||||
<Card>
|
{#if noWallets}
|
||||||
<CardHeader class="text-center">
|
<div class="flex flex-col items-center justify-center py-16">
|
||||||
<CardTitle>Welcome</CardTitle>
|
<WalletIcon size={48} class="text-muted-foreground/50" />
|
||||||
<CardDescription>
|
<p class="mt-4 text-lg font-medium">Welcome to Koins</p>
|
||||||
Choose an account type to get started
|
<p class="mb-6 text-sm text-muted-foreground">Choose an asset to get started</p>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<Button
|
||||||
|
onclick={() => navigate('/xrp')}
|
||||||
|
variant="outline"
|
||||||
|
class="gap-2">
|
||||||
|
<img src="/icons/xrp.png" alt="" class="size-4 rounded-full" />
|
||||||
|
Set up XRP
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onclick={() => navigate('/monero')}
|
||||||
|
variant="outline"
|
||||||
|
class="gap-2">
|
||||||
|
<img src="/icons/monero.png" alt="" class="size-4 rounded-full" />
|
||||||
|
Set up Monero
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col items-center pt-4 pb-2">
|
||||||
|
<p class="text-xs text-muted-foreground">Total Portfolio Value</p>
|
||||||
|
{#if loadingXrp || loadingXmr}
|
||||||
|
<div class="mt-2 h-9 w-44 rounded-md bg-muted animate-pulse"></div>
|
||||||
|
{:else}
|
||||||
|
<p class="mt-1 text-3xl font-semibold tabular-nums">
|
||||||
|
${fmtUsd(portfolioUsd)}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<button
|
||||||
|
onclick={() => navigate('/xrp')}
|
||||||
|
class="text-left transition-transform active:scale-[0.98]">
|
||||||
|
<Card class="hover:border-foreground/20 transition-colors">
|
||||||
|
<CardHeader class="pb-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<img src="/icons/xrp.png" alt="" class="size-5 rounded-full" />
|
||||||
|
<CardTitle class="text-base">XRP</CardTitle>
|
||||||
|
</div>
|
||||||
|
<CardDescription class="text-xs">
|
||||||
|
{xrpWallet.wallets.length} wallet{xrpWallet.wallets.length !== 1 ? 's' : ''}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div class="flex flex-col gap-3">
|
{#if loadingXrp}
|
||||||
|
<div class="h-14 flex items-center"><Loader /></div>
|
||||||
|
{:else}
|
||||||
|
<p class="font-mono text-xl tabular-nums">{fmtBal(xrpTotalBalance)}</p>
|
||||||
|
{#if xrpPrice}
|
||||||
|
<p class="mt-0.5 text-sm text-muted-foreground tabular-nums">
|
||||||
|
${fmtUsd(xrpTotalUsd)}
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-xs text-muted-foreground">
|
||||||
|
${fmtUsd(parseFloat(xrpPrice) )} / XRP
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onclick={() => navigate('/monero')}
|
||||||
|
class="text-left transition-transform active:scale-[0.98]">
|
||||||
|
<Card class="hover:border-foreground/20 transition-colors">
|
||||||
|
<CardHeader class="pb-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<img src="/icons/monero.png" alt="" class="size-5 rounded-full" />
|
||||||
|
<CardTitle class="text-base">Monero</CardTitle>
|
||||||
|
</div>
|
||||||
|
<CardDescription class="text-xs">
|
||||||
|
{moneroWallet.walletOpen
|
||||||
|
? moneroWallet.walletName || 'Wallet open'
|
||||||
|
: moneroWallet.wallets.length > 0
|
||||||
|
? `${moneroWallet.wallets.length} wallet${moneroWallet.wallets.length !== 1 ? 's' : ''}`
|
||||||
|
: 'Not set up'}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{#if loadingXmr && !moneroWallet.walletOpen}
|
||||||
|
<div class="h-14 flex items-center"><Loader /></div>
|
||||||
|
{:else if moneroWallet.walletOpen}
|
||||||
|
<p class="font-mono text-xl tabular-nums">{fmtBal(xmrBalance, 4)}</p>
|
||||||
|
{#if xmrPrice}
|
||||||
|
<p class="mt-0.5 text-sm text-muted-foreground tabular-nums">
|
||||||
|
${fmtUsd(xmrTotalUsd)}
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-xs text-muted-foreground">
|
||||||
|
${fmtUsd(parseFloat(xmrPrice) )} / XMR
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
{:else if moneroWallet.wallets.length > 0}
|
||||||
|
<p class="text-sm text-muted-foreground py-3">Open wallet to view balance</p>
|
||||||
|
{:else}
|
||||||
<Button
|
<Button
|
||||||
onclick={async () => {
|
onclick={() => navigate('/monero')}
|
||||||
await moneroWallet.login()
|
|
||||||
navigate('/monero')
|
|
||||||
}}
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="w-full">
|
size="sm"
|
||||||
<img
|
class="gap-1.5">
|
||||||
src="/icons/monero.png"
|
<Plus size={14} />
|
||||||
alt=""
|
Set up
|
||||||
class="-ml-1 mr-1.5 inline-block size-5 rounded-full align-middle" />
|
|
||||||
Monero
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{/if}
|
||||||
onclick={async () => {
|
</CardContent>
|
||||||
await xrpWallet.login()
|
</Card>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if xrpWallet.wallets.length > 1}
|
||||||
|
<Card>
|
||||||
|
<CardHeader class="pb-3">
|
||||||
|
<CardTitle class="text-sm">XRP Wallets</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div class="space-y-1">
|
||||||
|
{#each xrpWallet.wallets as wal}
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
xrpWallet.selectAndUnlockWallet(wal.id)
|
||||||
navigate('/xrp')
|
navigate('/xrp')
|
||||||
}}
|
}}
|
||||||
variant="outline"
|
class="flex w-full items-center gap-3 rounded-lg p-2 text-left hover:bg-muted transition-colors">
|
||||||
class="w-full">
|
<WalletIcon size={16} class="shrink-0 text-muted-foreground" />
|
||||||
<img
|
<div class="flex-1 min-w-0">
|
||||||
src="/icons/xrp.png"
|
<p class="text-sm font-medium">{wal.name}</p>
|
||||||
alt=""
|
<p class="text-xs text-muted-foreground truncate font-mono">{wal.address}</p>
|
||||||
class="-ml-1 mr-1.5 inline-block size-5 rounded-full align-middle" />
|
</div>
|
||||||
XRP
|
{#if xrpBalances[wal.address]?.funded === false}
|
||||||
</Button>
|
<span class="text-xs text-muted-foreground">unfunded</span>
|
||||||
|
{:else if xrpBalances[wal.address]}
|
||||||
|
<span class="font-mono text-sm tabular-nums">
|
||||||
|
{fmtBal(xrpBalances[wal.address].balance)}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<Button
|
||||||
|
onclick={() => navigate('/xrp')}
|
||||||
|
variant="outline"
|
||||||
|
class="flex-1 gap-2">
|
||||||
|
<img src="/icons/xrp.png" alt="" class="size-4 rounded-full" />
|
||||||
|
XRP Wallet
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onclick={() => navigate('/monero')}
|
||||||
|
variant="outline"
|
||||||
|
class="flex-1 gap-2">
|
||||||
|
<img src="/icons/monero.png" alt="" class="size-4 rounded-full" />
|
||||||
|
Monero Wallet
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { route } from 'sv-router/generated'
|
||||||
|
import { session } from '$lib/states/session.svelte.js'
|
||||||
|
import { xrpWallet } from '$lib/states/xrp-wallet.svelte.js'
|
||||||
|
import { moneroWallet } from '$lib/states/monero-wallet.svelte.js'
|
||||||
|
import { Button } from '$lib/components/ui/button/index.js'
|
||||||
|
import House from '@lucide/svelte/icons/house'
|
||||||
|
import SettingsIcon from '@lucide/svelte/icons/settings'
|
||||||
|
import Fingerprint from '@lucide/svelte/icons/fingerprint'
|
||||||
|
import LockIcon from '@lucide/svelte/icons/lock'
|
||||||
|
|
||||||
|
let { children } = $props()
|
||||||
|
|
||||||
|
const title = $derived.by(() => {
|
||||||
|
const p = route.pathname
|
||||||
|
if (p === '/') return 'Koins'
|
||||||
|
if (p === '/settings') return 'Settings'
|
||||||
|
if (p.startsWith('/xrp/tx/')) return 'Transaction'
|
||||||
|
if (p === '/xrp/send') return 'Send'
|
||||||
|
if (p.startsWith('/xrp')) return 'XRP'
|
||||||
|
if (p.startsWith('/monero/tx/')) return 'Transaction'
|
||||||
|
if (p === '/monero/send') return 'Send'
|
||||||
|
if (p === '/monero/settings') return 'Monero Settings'
|
||||||
|
if (p.startsWith('/monero')) return 'Monero'
|
||||||
|
return 'Koins'
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleUnlock = async () => {
|
||||||
|
const ok = await session.unlock()
|
||||||
|
if (!ok) return
|
||||||
|
if (!xrpWallet.ready) await xrpWallet.init()
|
||||||
|
if (!moneroWallet.ready) await moneroWallet.init()
|
||||||
|
await Promise.allSettled([
|
||||||
|
xrpWallet.autoUnlock(),
|
||||||
|
moneroWallet.autoUnlock(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemClass = (active: boolean) =>
|
||||||
|
`flex items-center gap-2.5 rounded-lg px-2.5 py-1.5 text-sm transition-colors ${
|
||||||
|
active
|
||||||
|
? 'bg-sidebar-accent font-medium text-sidebar-accent-foreground'
|
||||||
|
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent/60 hover:text-sidebar-foreground'
|
||||||
|
}`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !session.unlocked}
|
||||||
|
<div
|
||||||
|
class="flex h-screen w-screen flex-col items-center justify-center bg-background gap-6">
|
||||||
|
<div class="flex flex-col items-center gap-4">
|
||||||
|
<div
|
||||||
|
class="flex size-16 items-center justify-center rounded-full bg-muted">
|
||||||
|
<Fingerprint size={32} class="text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-lg font-semibold">Koins</p>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
Use Touch ID to unlock
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if session.biometricAvailable}
|
||||||
|
<Button
|
||||||
|
onclick={handleUnlock}
|
||||||
|
size="lg"
|
||||||
|
class="gap-2"
|
||||||
|
disabled={session.unlocking}>
|
||||||
|
<Fingerprint size={18} />
|
||||||
|
{session.unlocking ? 'Unlocking...' : 'Unlock'}
|
||||||
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<p class="text-sm text-muted-foreground max-w-xs text-center">
|
||||||
|
Biometric authentication isn't available on this device.
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if session.error}
|
||||||
|
<p class="text-sm text-destructive">{session.error}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex h-screen w-screen overflow-hidden">
|
||||||
|
<aside class="flex w-60 shrink-0 flex-col bg-sidebar text-sidebar-foreground">
|
||||||
|
<div class="px-3 pt-9">
|
||||||
|
<p class="px-2 pb-4 text-sm font-semibold">Koins</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-y-auto px-3">
|
||||||
|
<nav class="space-y-0.5">
|
||||||
|
<a href="/" class={itemClass(route.pathname === '/')}>
|
||||||
|
<House size={16} class="shrink-0" />
|
||||||
|
<span>Home</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<p
|
||||||
|
class="mb-1 mt-4 px-2 text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70">
|
||||||
|
Wallets
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<nav class="space-y-0.5">
|
||||||
|
<a href="/xrp" class={itemClass(route.pathname.startsWith('/xrp'))}>
|
||||||
|
<img
|
||||||
|
src="/icons/xrp.png"
|
||||||
|
alt=""
|
||||||
|
class="size-4 shrink-0 rounded-full" />
|
||||||
|
<span>XRP</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/monero"
|
||||||
|
class={itemClass(route.pathname.startsWith('/monero'))}>
|
||||||
|
<img
|
||||||
|
src="/icons/monero.png"
|
||||||
|
alt=""
|
||||||
|
class="size-4 shrink-0 rounded-full" />
|
||||||
|
<span>Monero</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-sidebar-border px-3 pb-4 pt-2 space-y-0.5">
|
||||||
|
<a
|
||||||
|
href="/settings"
|
||||||
|
class={itemClass(route.pathname === '/settings')}>
|
||||||
|
<SettingsIcon size={16} class="shrink-0" />
|
||||||
|
<span>Settings</span>
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
onclick={() => session.lock()}
|
||||||
|
class="flex w-full items-center gap-2.5 rounded-lg px-2.5 py-1.5 text-sm text-sidebar-foreground/80 hover:bg-sidebar-accent/60 hover:text-sidebar-foreground transition-colors">
|
||||||
|
<LockIcon size={16} class="shrink-0" />
|
||||||
|
<span>Lock</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="flex min-w-0 flex-1 flex-col">
|
||||||
|
<header
|
||||||
|
class="relative flex h-12 shrink-0 items-center border-b border-border px-3">
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||||
|
<span class="text-sm font-medium">{title}</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="min-w-0 flex-1 overflow-y-auto">
|
||||||
|
<div class="mx-auto w-full max-w-2xl px-8 py-8">
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { moneroWallet } from '$lib/states/monero-wallet.svelte.js'
|
import { moneroWallet } from '$lib/states/monero-wallet.svelte.js'
|
||||||
|
import { session } from '$lib/states/session.svelte.js'
|
||||||
import { Button } from '$lib/components/ui/button/index.js'
|
import { Button } from '$lib/components/ui/button/index.js'
|
||||||
import { Textarea } from '$lib/components/ui/textarea/index.js'
|
import { Textarea } from '$lib/components/ui/textarea/index.js'
|
||||||
import { Input } from '$lib/components/ui/input/index.js'
|
import { Input } from '$lib/components/ui/input/index.js'
|
||||||
@@ -47,7 +48,7 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto mt-16 max-w-md">
|
<div class="mx-auto max-w-xl">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
{#if !w.ready}
|
{#if !w.ready}
|
||||||
<p class="text-center text-muted-foreground text-sm mt-8">
|
<p class="text-center text-muted-foreground text-sm mt-8">
|
||||||
@@ -59,9 +60,6 @@
|
|||||||
to get started
|
to get started
|
||||||
</p>
|
</p>
|
||||||
{:else if w.accountType === 'monero'}
|
{:else if w.accountType === 'monero'}
|
||||||
<a href="/" class="text-xs text-muted-foreground underline mb-1 inline-block">
|
|
||||||
← Back to home
|
|
||||||
</a>
|
|
||||||
{#if w.downloading}
|
{#if w.downloading}
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -134,7 +132,19 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if moneroSelectedWallet}
|
{#if moneroSelectedWallet}
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
{#if w.biometricAvailable}
|
{#if session.unlocked}
|
||||||
|
<Button
|
||||||
|
onclick={() =>
|
||||||
|
w.autoUnlock(
|
||||||
|
moneroSelectedWallet,
|
||||||
|
)}
|
||||||
|
disabled={w.loading}>
|
||||||
|
{#if w.loading}
|
||||||
|
<Loader />
|
||||||
|
{/if}
|
||||||
|
Open
|
||||||
|
</Button>
|
||||||
|
{:else if w.biometricAvailable}
|
||||||
<Button
|
<Button
|
||||||
onclick={async () => {
|
onclick={async () => {
|
||||||
const ok = await w.biometricAuth()
|
const ok = await w.biometricAuth()
|
||||||
|
|||||||
@@ -92,15 +92,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto mt-16 max-w-md">
|
<div class="mx-auto max-w-xl">
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onclick={() => navigate('/monero')}
|
|
||||||
class="mb-4">
|
|
||||||
← Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Send XMR</CardTitle>
|
<CardTitle>Send XMR</CardTitle>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto mt-16 max-w-md">
|
<div class="mx-auto max-w-xl">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -82,14 +82,7 @@
|
|||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="pt-2 border-t border-border">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onclick={() => navigate('/monero')}>
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -35,15 +35,7 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto mt-16 max-w-md">
|
<div class="mx-auto max-w-xl">
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onclick={() => navigate('/monero')}
|
|
||||||
class="mb-4">
|
|
||||||
← Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Transaction Details</CardTitle>
|
<CardTitle>Transaction Details</CardTitle>
|
||||||
|
|||||||
@@ -146,7 +146,7 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto mt-16 max-w-md">
|
<div class="mx-auto max-w-xl">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<Card>
|
<Card>
|
||||||
{@const a = chunkAddress(w.address)}
|
{@const a = chunkAddress(w.address)}
|
||||||
|
|||||||
@@ -33,23 +33,9 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto mt-16 max-w-md">
|
<div class="mx-auto max-w-xl">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="flex items-center gap-2">
|
{#if w.wallets.length > 0}
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onclick={() =>
|
|
||||||
w.currentWalletId
|
|
||||||
? navigate('/xrp/wallet/:id', {
|
|
||||||
params: { id: w.currentWalletId },
|
|
||||||
})
|
|
||||||
: navigate('/xrp')}>
|
|
||||||
<ArrowLeft size={16} />
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{#if w.wallets.length > 0}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Wallets</CardTitle>
|
<CardTitle>Wallets</CardTitle>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { xrpWallet } from '$lib/states/xrp-wallet.svelte.js'
|
import { xrpWallet } from '$lib/states/xrp-wallet.svelte.js'
|
||||||
|
import { session } from '$lib/states/session.svelte.js'
|
||||||
import { Button } from '$lib/components/ui/button/index.js'
|
import { Button } from '$lib/components/ui/button/index.js'
|
||||||
import { Input } from '$lib/components/ui/input/index.js'
|
import { Input } from '$lib/components/ui/input/index.js'
|
||||||
import { Textarea } from '$lib/components/ui/textarea/index.js'
|
import { Textarea } from '$lib/components/ui/textarea/index.js'
|
||||||
@@ -63,7 +64,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto mt-16 max-w-md">
|
<div class="mx-auto max-w-xl">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
{#if !w.ready}
|
{#if !w.ready}
|
||||||
<p class="text-center text-muted-foreground text-sm mt-8">
|
<p class="text-center text-muted-foreground text-sm mt-8">
|
||||||
@@ -75,11 +76,6 @@
|
|||||||
to get started
|
to get started
|
||||||
</p>
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
<a
|
|
||||||
href="/"
|
|
||||||
class="text-xs text-muted-foreground underline mb-1 inline-block">
|
|
||||||
← Back to home
|
|
||||||
</a>
|
|
||||||
{#if newSeed}
|
{#if newSeed}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -177,7 +173,9 @@
|
|||||||
{#each w.wallets as wal}
|
{#each w.wallets as wal}
|
||||||
<button
|
<button
|
||||||
onclick={async () => {
|
onclick={async () => {
|
||||||
if (wal.hasPassword) {
|
if (session.unlocked) {
|
||||||
|
await w.autoUnlock(wal.id)
|
||||||
|
} else if (wal.hasPassword) {
|
||||||
await w.selectWallet(wal.id)
|
await w.selectWallet(wal.id)
|
||||||
} else {
|
} else {
|
||||||
await w.selectAndUnlockWallet(wal.id)
|
await w.selectAndUnlockWallet(wal.id)
|
||||||
|
|||||||
@@ -59,18 +59,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto mt-16 max-w-md">
|
<div class="mx-auto max-w-xl">
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onclick={() =>
|
|
||||||
navigate('/xrp/wallet/:id', {
|
|
||||||
params: { id: w.currentWalletId ?? '' },
|
|
||||||
})}
|
|
||||||
class="mb-4">
|
|
||||||
← Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Send XRP</CardTitle>
|
<CardTitle>Send XRP</CardTitle>
|
||||||
|
|||||||
@@ -35,18 +35,7 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto mt-16 max-w-md">
|
<div class="mx-auto max-w-xl">
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onclick={() =>
|
|
||||||
navigate('/xrp/wallet/:id', {
|
|
||||||
params: { id: xrpWallet.currentWalletId ?? '' },
|
|
||||||
})}
|
|
||||||
class="mb-4">
|
|
||||||
← Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Transaction Details</CardTitle>
|
<CardTitle>Transaction Details</CardTitle>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto mt-16 max-w-md">
|
<div class="mx-auto max-w-xl">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
Reference in New Issue
Block a user