add biometric support for monero wallet open/create/restore, add monero settings card, fix monero logout to stop rpc
- Added moneroGetStoredPassword / moneroStorePassword keychain helpers - moneroCreateWallet and moneroRestoreWallet accept storePw flag to save password to keychain - moneroOpenExistingWallet accepts optional password (falls back to keychain lookup) - Monero UI: 'Save password with Touch ID' checkbox hides password field, auto-generates random password, saves to keychain - Monero UI: 'Open with Touch ID' button uses biometric auth + retrieves stored password - Monero Settings card: enter wallet password to save with Touch ID for existing wallets - logout() now async, calls moneroStop() to kill RPC process so re-login requires explicit wallet open
This commit is contained in:
@@ -261,7 +261,7 @@
|
|||||||
<Button variant="outline" onclick={() => navigate('/settings')}>
|
<Button variant="outline" onclick={() => navigate('/settings')}>
|
||||||
<SettingsIcon size={16} />
|
<SettingsIcon size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" onclick={() => { w.logout(); navigate('/') }}>
|
<Button variant="outline" onclick={async () => { await w.logout(); navigate('/') }}>
|
||||||
Logout
|
Logout
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -142,20 +142,27 @@ export const Wallet = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const logout = () => {
|
const logout = async () => {
|
||||||
|
await moneroStop()
|
||||||
accountType = null
|
accountType = null
|
||||||
seed = ''
|
seed = ''
|
||||||
address = ''
|
address = ''
|
||||||
balance = '0'
|
balance = '0'
|
||||||
tokenBalances = []
|
tokenBalances = []
|
||||||
transactions = []
|
transactions = []
|
||||||
moneroWalletOpen = false
|
}
|
||||||
moneroBalAtomic = '0'
|
|
||||||
moneroUnlockedAtomic = '0'
|
const moneroGetStoredPassword = async (name: string): Promise<string | null> => {
|
||||||
moneroAddress = ''
|
if (!electrobun.rpc) return null
|
||||||
moneroTxs = []
|
const [raw] = await tryCatch(
|
||||||
moneroWalletName = ''
|
electrobun.rpc.request.getSecret({ service: 'koins', name: `monero_pw_${name}` }),
|
||||||
moneroAccounts = []
|
)
|
||||||
|
return raw ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
const moneroStorePassword = async (name: string, password: string) => {
|
||||||
|
if (!electrobun.rpc) return
|
||||||
|
await electrobun.rpc.request.setSecret({ service: 'koins', name: `monero_pw_${name}`, value: password })
|
||||||
}
|
}
|
||||||
|
|
||||||
const switchNetwork = async (id: NetworkId) => {
|
const switchNetwork = async (id: NetworkId) => {
|
||||||
@@ -334,9 +341,11 @@ export const Wallet = () => {
|
|||||||
moneroWallets = wallets ?? []
|
moneroWallets = wallets ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
const moneroOpenExistingWallet = async (name: string, password: string) => {
|
const moneroOpenExistingWallet = async (name: string, password?: string) => {
|
||||||
if (!electrobun.rpc) throw new Error('RPC not available')
|
if (!electrobun.rpc) throw new Error('RPC not available')
|
||||||
await electrobun.rpc.request.moneroOpenWallet({ name, password })
|
const pw = password ?? await moneroGetStoredPassword(name)
|
||||||
|
if (!pw) throw new Error('Password required')
|
||||||
|
await electrobun.rpc.request.moneroOpenWallet({ name, password: pw })
|
||||||
moneroWalletName = name
|
moneroWalletName = name
|
||||||
moneroWalletOpen = true
|
moneroWalletOpen = true
|
||||||
await moneroRefresh()
|
await moneroRefresh()
|
||||||
@@ -415,17 +424,18 @@ export const Wallet = () => {
|
|||||||
moneroWallets = []
|
moneroWallets = []
|
||||||
}
|
}
|
||||||
|
|
||||||
const moneroCreateWallet = async (name: string, password: string) => {
|
const moneroCreateWallet = async (name: string, password: string, storePw?: boolean) => {
|
||||||
if (!electrobun.rpc) throw new Error('RPC not available')
|
if (!electrobun.rpc) throw new Error('RPC not available')
|
||||||
const result = await electrobun.rpc.request.moneroCreateWallet({ name, password })
|
const result = await electrobun.rpc.request.moneroCreateWallet({ name, password })
|
||||||
moneroWalletName = name
|
moneroWalletName = name
|
||||||
moneroWalletOpen = true
|
moneroWalletOpen = true
|
||||||
moneroAddress = result.address
|
moneroAddress = result.address
|
||||||
|
if (storePw) await moneroStorePassword(name, password)
|
||||||
await moneroRefresh()
|
await moneroRefresh()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
const moneroRestoreWallet = async (name: string, password: string, mnemonic: string, restoreHeight?: number) => {
|
const moneroRestoreWallet = async (name: string, password: string, mnemonic: string, restoreHeight?: number, storePw?: boolean) => {
|
||||||
if (!electrobun.rpc) throw new Error('RPC not available')
|
if (!electrobun.rpc) throw new Error('RPC not available')
|
||||||
const result = await electrobun.rpc.request.moneroRestoreWallet({
|
const result = await electrobun.rpc.request.moneroRestoreWallet({
|
||||||
name, password, mnemonic, restoreHeight,
|
name, password, mnemonic, restoreHeight,
|
||||||
@@ -433,6 +443,7 @@ export const Wallet = () => {
|
|||||||
moneroWalletName = name
|
moneroWalletName = name
|
||||||
moneroWalletOpen = true
|
moneroWalletOpen = true
|
||||||
moneroAddress = result.address
|
moneroAddress = result.address
|
||||||
|
if (storePw) await moneroStorePassword(name, password)
|
||||||
await moneroRefresh()
|
await moneroRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,6 +547,8 @@ export const Wallet = () => {
|
|||||||
moneroFetchAccounts,
|
moneroFetchAccounts,
|
||||||
moneroListWallets,
|
moneroListWallets,
|
||||||
moneroOpenExistingWallet,
|
moneroOpenExistingWallet,
|
||||||
|
moneroGetStoredPassword,
|
||||||
|
moneroStorePassword,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
} from '$lib/components/ui/card/index.js'
|
} from '$lib/components/ui/card/index.js'
|
||||||
import ArrowDown from '@lucide/svelte/icons/arrow-down'
|
import ArrowDown from '@lucide/svelte/icons/arrow-down'
|
||||||
import ArrowUp from '@lucide/svelte/icons/arrow-up'
|
import ArrowUp from '@lucide/svelte/icons/arrow-up'
|
||||||
|
import Fingerprint from '@lucide/svelte/icons/fingerprint'
|
||||||
import { navigate } from 'sv-router/generated'
|
import { navigate } from 'sv-router/generated'
|
||||||
|
|
||||||
const w = wallet
|
const w = wallet
|
||||||
@@ -22,6 +23,8 @@
|
|||||||
let moneroRestoreHeight = $state<number | undefined>(undefined)
|
let moneroRestoreHeight = $state<number | undefined>(undefined)
|
||||||
let moneroSelectedWallet = $state('')
|
let moneroSelectedWallet = $state('')
|
||||||
let moneroSelectedWalletPass = $state('')
|
let moneroSelectedWalletPass = $state('')
|
||||||
|
let moneroUseBiometric = $state(false)
|
||||||
|
let moneroSettingsPw = $state('')
|
||||||
|
|
||||||
let initStarted = false
|
let initStarted = false
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -122,32 +125,57 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{#if moneroSelectedWallet}
|
{#if moneroSelectedWallet}
|
||||||
<div class="flex gap-2 items-end">
|
<div class="flex flex-col gap-2">
|
||||||
<Input
|
{#if w.biometricAvailable}
|
||||||
type="password"
|
<Button
|
||||||
placeholder="Enter password"
|
onclick={async () => {
|
||||||
bind:value={moneroSelectedWalletPass}
|
const ok = await w.unlockWithBiometrics()
|
||||||
/>
|
if (ok) {
|
||||||
<Button
|
await w.moneroOpenExistingWallet(moneroSelectedWallet)
|
||||||
onclick={() => w.moneroOpenExistingWallet(moneroSelectedWallet, moneroSelectedWalletPass)}
|
if (!w.moneroWalletOpen) w.error = 'No password saved for this wallet'
|
||||||
disabled={w.loading || !moneroSelectedWalletPass}
|
}
|
||||||
>
|
}}
|
||||||
Open
|
disabled={w.loading}>
|
||||||
</Button>
|
<Fingerprint size={14} />
|
||||||
|
Open with Touch ID
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
<div class="flex gap-2 items-end">
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter password"
|
||||||
|
bind:value={moneroSelectedWalletPass}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onclick={() => w.moneroOpenExistingWallet(moneroSelectedWallet, moneroSelectedWalletPass)}
|
||||||
|
disabled={w.loading || !moneroSelectedWalletPass}
|
||||||
|
>
|
||||||
|
Open
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<hr class="border-muted" />
|
<hr class="border-muted" />
|
||||||
{/if}
|
{/if}
|
||||||
<p class="text-xs text-muted-foreground">Or create a new wallet:</p>
|
<p class="text-xs text-muted-foreground">Or create a new wallet:</p>
|
||||||
<Input placeholder="Wallet name" bind:value={moneroWalletName} />
|
<Input placeholder="Wallet name" bind:value={moneroWalletName} />
|
||||||
<Input type="password" placeholder="Password" bind:value={moneroWalletPass} />
|
{#if w.biometricAvailable}
|
||||||
|
<label class="flex items-center gap-2 text-xs cursor-pointer">
|
||||||
|
<input type="checkbox" bind:checked={moneroUseBiometric} />
|
||||||
|
Save password with Touch ID
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
|
{#if !moneroUseBiometric}
|
||||||
|
<Input type="password" placeholder="Password" bind:value={moneroWalletPass} />
|
||||||
|
{/if}
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
onclick={async () => {
|
onclick={async () => {
|
||||||
const result = await w.moneroCreateWallet(moneroWalletName, moneroWalletPass)
|
const pw = moneroUseBiometric ? crypto.randomUUID() : moneroWalletPass
|
||||||
|
const result = await w.moneroCreateWallet(moneroWalletName, pw, moneroUseBiometric)
|
||||||
moneroMnemonic = result.mnemonic
|
moneroMnemonic = result.mnemonic
|
||||||
}}
|
}}
|
||||||
disabled={w.loading || !moneroWalletName || !moneroWalletPass}>
|
disabled={w.loading || !moneroWalletName || (!moneroUseBiometric && !moneroWalletPass)}>
|
||||||
Create Wallet
|
Create Wallet
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -155,9 +183,12 @@
|
|||||||
<p class="text-xs text-muted-foreground">Restore from seed</p>
|
<p class="text-xs text-muted-foreground">Restore from seed</p>
|
||||||
<Textarea placeholder="Enter your Monero seed phrase (16 or 25 words)" bind:value={moneroMnemonic} />
|
<Textarea placeholder="Enter your Monero seed phrase (16 or 25 words)" bind:value={moneroMnemonic} />
|
||||||
<Input type="number" placeholder="Restore height (optional)" bind:value={moneroRestoreHeight} />
|
<Input type="number" placeholder="Restore height (optional)" bind:value={moneroRestoreHeight} />
|
||||||
|
{#if !moneroUseBiometric}
|
||||||
|
<Input type="password" placeholder="Password" bind:value={moneroWalletPass} />
|
||||||
|
{/if}
|
||||||
<Button
|
<Button
|
||||||
onclick={() => w.moneroRestoreWallet(moneroWalletName, moneroWalletPass, moneroMnemonic, moneroRestoreHeight)}
|
onclick={() => w.moneroRestoreWallet(moneroWalletName, moneroUseBiometric ? crypto.randomUUID() : moneroWalletPass, moneroMnemonic, moneroRestoreHeight, moneroUseBiometric)}
|
||||||
disabled={w.loading || !moneroWalletName || !moneroWalletPass || !moneroMnemonic.trim()}>
|
disabled={w.loading || !moneroWalletName || (!moneroUseBiometric && !moneroWalletPass) || !moneroMnemonic.trim()}>
|
||||||
Restore Wallet
|
Restore Wallet
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -209,13 +240,36 @@
|
|||||||
<Button variant="outline" onclick={() => w.moneroStop()}>
|
<Button variant="outline" onclick={() => w.moneroStop()}>
|
||||||
Stop
|
Stop
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" onclick={() => { w.logout(); navigate('/') }}>
|
<Button variant="outline" onclick={async () => { await w.logout(); window.location.href = '/' }}>
|
||||||
Logout
|
Logout
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<!-- Monero settings -->
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Settings</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{#if w.biometricAvailable}
|
||||||
|
<p class="text-xs font-medium mb-2">Save wallet password with Touch ID</p>
|
||||||
|
<p class="text-xs text-muted-foreground mb-3">Enter your wallet password to enable biometric unlock</p>
|
||||||
|
<div class="flex gap-2 items-end">
|
||||||
|
<Input type="password" placeholder="Wallet password" bind:value={moneroSettingsPw} />
|
||||||
|
<Button size="sm" onclick={async () => {
|
||||||
|
await w.moneroStorePassword(w.moneroWalletName, moneroSettingsPw)
|
||||||
|
moneroSettingsPw = ''
|
||||||
|
w.error = ''
|
||||||
|
}} disabled={!moneroSettingsPw || w.loading}>Save</Button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="text-xs text-muted-foreground">Biometric authentication not available on this device</p>
|
||||||
|
{/if}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<!-- Monero transactions -->
|
<!-- Monero transactions -->
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
Reference in New Issue
Block a user