-
Address
-
-
{w.address}
- {#if qrDataUrl}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {w.address}
-
-
-
-
- {/if}
+
+ w.login('multi')}
+ class="w-full">
+ Multi Coins (ETH / BSC / Polygon)
+
+ navigate('/monero')}
+ variant="outline"
+ class="w-full">
+ Monero
+
+
+
+
+
+{:else if w.accountType === 'multi'}
+ {#if !w.vaultExists}
+
+
+ Import Wallet
+
+ Enter your seed phrase to save it securely in your system keychain
+
+
+
+
+
+
+ w.saveVault(inputSeed, inputPassword || undefined)}
+ disabled={w.loading || !inputSeed.trim()}>
+ {w.loading ? 'Saving...' : 'Save to Keychain'}
+
+
+ {#if w.error}
+ {w.error}
+ {/if}
+
+
+ {:else if w.isLocked}
+
+
+ Wallet Locked
+ Your seed is stored in the system keychain
+
+
+
+ {#if w.biometricAvailable}
+
w.unlockWithBiometrics()}
+ disabled={w.loading}>
+
+ {w.loading ? 'Unlocking...' : 'Unlock with Touch ID'}
+
+ {/if}
+ {#if w.passwordSet}
+
+
+ {
+ const ok = await w.unlockWithPassword(inputUnlockPassword)
+ if (!ok) w.error = 'Wrong password'
+ inputUnlockPassword = ''
+ }}
+ disabled={w.loading || !inputUnlockPassword}
+ size="sm">
+ {w.loading ? '...' : 'Unlock'}
+
+
+ {/if}
+
+ {#if w.error}
+ {w.error}
+ {/if}
+
+
+ {:else}
+
+
+ {#each w.evmNetworks as net}
+ navigate(navTarget(net.id))}>
+ {net.name}
+
+ {/each}
+ w.logout()}>
+ Logout
+
+
+
+
+
+
+
+ Wallet
+
+ Your wallet is unlocked
+
-
- {w.symbol} Balance
-
-
-
- ((e.target as HTMLElement).style.display =
- 'none')} />
- {w.balance}
- {w.symbol}
-
-
-
Tokens
- {#each w.tokenBalances as t}
-
+
+
+ {#if w.address}
+
+
Address
+
+
{w.address}
+ {#if qrDataUrl}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {w.address}
+
+
+
+
+ {/if}
+
+
+ {w.symbol} Balance
+
+
((e.target as HTMLElement).style.display =
'none')} />
- {Number(t.balance) < 0.0001 && Number(t.balance) > 0
- ? '<0.0001'
- : Number(t.balance).toFixed(4)}
- {t.symbol}
+ {w.balance}
+ {w.symbol}
- {/each}
-
- {/if}
+
+
+
Tokens
+ {#each w.tokenBalances as t}
+
+
+ ((e.target as HTMLElement).style.display =
+ 'none')} />
+ {Number(t.balance) < 0.0001 && Number(t.balance) > 0
+ ? '<0.0001'
+ : Number(t.balance).toFixed(4)}
+ {t.symbol}
+
+ {/each}
+
+ {/if}
- {#if w.error}
- {w.error}
- {/if}
+ {#if w.error}
+ {w.error}
+ {/if}
-
- w.refresh()} disabled={w.loading}>
- {w.loading ? 'Refreshing...' : 'Refresh'}
-
- navigate('/settings')}>
-
-
- w.lock()}>
- Lock
-
-
-
-
-
-
-
- Transactions
-
-
- {#if !w.apiKey}
-
-
-
w.saveApiKey(apiKeyInput)}
- disabled={!apiKeyInput.trim()}>
- Save
+
+ w.refresh()} disabled={w.loading}>
+ {w.loading ? 'Refreshing...' : 'Refresh'}
+
+ navigate('/settings')}>
+
+
+ w.lock()}>
+ Lock
- {:else if w.transactions.length === 0}
-
- No transactions found
-
- {:else}
-
- {#each w.transactions as tx}
-
- navigate('/tx/:chainid/:hash', {
- params: {
- chainid: w.chainid,
- hash: tx.hash,
- },
- })}
- class="flex w-full cursor-pointer items-start gap-2 rounded-md bg-muted px-3 py-2 text-xs text-left hover:bg-muted/80 transition-colors">
-
- {#if tx.from.toLowerCase() === w.address.toLowerCase()}
-
- {:else}
-
- {/if}
-
-
-
- {txAction(tx, w.address)}
-
-
- {#if tx.pairedValue}
-
- ((e.target as HTMLElement).style.display =
- 'none')} />
- {Number(tx.value).toFixed(4)}
- {w.symbol}
- →
-
- ((e.target as HTMLElement).style.display =
- 'none')} />
- {Number(tx.pairedValue).toFixed(4)}
- {tx.pairedSymbol}
- {:else if tx.tokenSymbol}
-
- ((e.target as HTMLElement).style.display =
- 'none')} />
- {Number(tx.value).toFixed(4)}
- {tx.tokenSymbol}
- {:else}
-
- ((e.target as HTMLElement).style.display =
- 'none')} />
- {Number(tx.value).toFixed(4)}
- {w.symbol}
- {/if}
-
-
- {new Date(
- Number(tx.timeStamp) * 1000,
- ).toLocaleDateString('en-US', {
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- hour: '2-digit',
- minute: '2-digit',
+
+
+
+
+
+ Transactions
+
+
+ {#if !w.apiKey}
+
+
+ w.saveApiKey(apiKeyInput)}
+ disabled={!apiKeyInput.trim()}>
+ Save
+
+
+ {:else if w.transactions.length === 0}
+
+ No transactions found
+
+ {:else}
+
+ {#each w.transactions as tx}
+
+ navigate('/tx/:chainid/:hash', {
+ params: {
+ chainid: w.chainid,
+ hash: tx.hash,
+ },
})}
-
-
- {tx.hash.slice(0, 10)}...
-
-
-
- {/each}
-
- {/if}
-
-
-
+ class="flex w-full cursor-pointer items-start gap-2 rounded-md bg-muted px-3 py-2 text-xs text-left hover:bg-muted/80 transition-colors">
+
+ {#if tx.from.toLowerCase() === w.address.toLowerCase()}
+
+ {:else}
+
+ {/if}
+
+
+
+ {txAction(tx, w.address)}
+
+
+ {#if tx.pairedValue}
+
+ ((e.target as HTMLElement).style.display =
+ 'none')} />
+ {Number(tx.value).toFixed(4)}
+ {w.symbol}
+ →
+
+ ((e.target as HTMLElement).style.display =
+ 'none')} />
+ {Number(tx.pairedValue).toFixed(4)}
+ {tx.pairedSymbol}
+ {:else if tx.tokenSymbol}
+
+ ((e.target as HTMLElement).style.display =
+ 'none')} />
+ {Number(tx.value).toFixed(4)}
+ {tx.tokenSymbol}
+ {:else}
+
+ ((e.target as HTMLElement).style.display =
+ 'none')} />
+ {Number(tx.value).toFixed(4)}
+ {w.symbol}
+ {/if}
+
+
+ {new Date(
+ Number(tx.timeStamp) * 1000,
+ ).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ })}
+
+
+ {tx.hash.slice(0, 10)}...
+
+
+
+ {/each}
+
+ {/if}
+
+
+
+ {/if}
{/if}
diff --git a/apps/desktop/src/lib/states/wallet.svelte.ts b/apps/desktop/src/lib/states/wallet.svelte.ts
index 224427e..e021025 100644
--- a/apps/desktop/src/lib/states/wallet.svelte.ts
+++ b/apps/desktop/src/lib/states/wallet.svelte.ts
@@ -27,16 +27,17 @@ export const networks = [
chainid: '1',
explorerUrl: 'https://etherscan.io/tx/',
},
- {
- id: 'monero',
- name: 'Monero',
- rpc: '',
- symbol: 'XMR',
- chainid: '',
- explorerUrl: 'https://moneroblocks.info/tx/',
- },
] as const
+const moneroNetwork = {
+ id: 'monero',
+ name: 'Monero',
+ rpc: '',
+ symbol: 'XMR',
+ chainid: '',
+ explorerUrl: 'https://moneroblocks.info/tx/',
+} as const
+
export type NetworkId = (typeof networks)[number]['id']
export type TokenBalance = {
@@ -53,7 +54,10 @@ export const atomicToXmr = (atomic: string): string => {
return frac ? `${whole}.${frac}` : `${whole}`
}
+export type AccountType = 'multi' | 'monero'
+
export const Wallet = () => {
+ let accountType = $state
(null)
let seed = $state('')
let address = $state('')
let balance = $state('0')
@@ -94,8 +98,6 @@ export const Wallet = () => {
return { salt: s, hash }
}
- const isMonero = () => network === 'monero'
-
const init = async () => {
if (!electrobun.rpc) return
const [raw, rawError] = await tryCatch(
@@ -122,20 +124,46 @@ export const Wallet = () => {
)
if (ph) passwordHash = ph
+ await checkMoneroStatus()
await checkBiometric()
ready = true
}
- const switchNetwork = async (id: NetworkId) => {
- network = id
- if (id === 'monero') {
- await checkMoneroStatus()
+ const login = async (type: AccountType) => {
+ accountType = type
+ if (type === 'multi') {
+ if (!seed && vaultExists) {
+ await loadSeed()
+ }
+ if (seed) await switchNetwork(network)
+ } else if (type === 'monero') {
if (moneroInstalled && !moneroRunning) {
await moneroStart()
await checkMoneroStatus()
}
await moneroListWallets()
- } else if (seed) {
+ }
+ }
+
+ const logout = () => {
+ accountType = null
+ seed = ''
+ address = ''
+ balance = '0'
+ tokenBalances = []
+ transactions = []
+ moneroWalletOpen = false
+ moneroBalAtomic = '0'
+ moneroUnlockedAtomic = '0'
+ moneroAddress = ''
+ moneroTxs = []
+ moneroWalletName = ''
+ moneroAccounts = []
+ }
+
+ const switchNetwork = async (id: NetworkId) => {
+ network = id
+ if (seed) {
await refresh()
}
}
@@ -448,8 +476,9 @@ export const Wallet = () => {
return {
get isLocked() {
- return vaultExists && !seed && network !== 'monero'
+ return accountType === 'multi' && vaultExists && !seed
},
+ get accountType() { return accountType },
get seed() { return seed },
get address() { return address },
get balance() { return balance },
@@ -464,9 +493,10 @@ export const Wallet = () => {
get vaultExists() { return vaultExists },
get ready() { return ready },
get networks() { return networks },
+ get moneroNetwork() { return moneroNetwork },
+ get evmNetworks() { return networks },
get apiKey() { return apiKey },
get transactions() { return transactions },
- isMonero,
get moneroRunning() { return moneroRunning },
get moneroWalletOpen() { return moneroWalletOpen },
get moneroConnected() { return moneroConnected },
@@ -487,6 +517,8 @@ export const Wallet = () => {
get passwordSet() { return !!passwordHash },
refresh,
init,
+ login,
+ logout,
lock,
unlockWithBiometrics,
unlockWithPassword,
diff --git a/apps/desktop/src/routes/about.svelte b/apps/desktop/src/routes/about.svelte
deleted file mode 100644
index 3f14839..0000000
--- a/apps/desktop/src/routes/about.svelte
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-About
-
-This is another page
diff --git a/apps/desktop/src/routes/index.svelte b/apps/desktop/src/routes/index.svelte
index 62ab2f4..a2bae17 100644
--- a/apps/desktop/src/routes/index.svelte
+++ b/apps/desktop/src/routes/index.svelte
@@ -1,7 +1,50 @@
-
-
+
+ {#if !w.ready}
+
Loading...
+ {:else if w.accountType}
+ {#if w.accountType === 'multi'}
+ {navigate('/multicoin')}
+ {:else}
+ {navigate('/monero')}
+ {/if}
+ {:else}
+
+
+ Welcome
+ Choose an account type to get started
+
+
+
+ navigate('/multicoin')}
+ class="w-full">
+ Multi Coins (ETH / BSC / Polygon)
+
+ navigate('/monero')}
+ variant="outline"
+ class="w-full">
+ Monero
+
+
+
+
+ {/if}
diff --git a/apps/desktop/src/routes/monero/index.svelte b/apps/desktop/src/routes/monero/index.svelte
index 63586f2..db0bbfe 100644
--- a/apps/desktop/src/routes/monero/index.svelte
+++ b/apps/desktop/src/routes/monero/index.svelte
@@ -1,5 +1,5 @@
-
- {#each w.networks as net}
+ {#if !w.ready}
+
Loading...
+ {:else if !w.accountType}
+
+
+ Welcome
+ Choose an account type to get started
+
+
+
+ navigate('/multicoin')}
+ variant="outline"
+ class="w-full">
+ Multi Coins (ETH / BSC / Polygon)
+
+ w.login('monero')}
+ class="w-full">
+ Monero
+
+
+
+
+ {:else if w.accountType === 'monero'}
+
navigate(navTarget(net.id))}>
- {net.name}
+ class="cursor-pointer rounded-md px-2.5 py-1 text-xs font-medium transition-colors bg-primary text-primary-foreground">
+ Monero
- {/each}
-
+
w.logout()}>
+ Logout
+
+
-
- {#if w.moneroDownloading}
+ {#if w.moneroDownloading}
Downloading Monero binary (70MB)...
@@ -276,5 +297,6 @@
{/if}
+ {/if}
diff --git a/apps/desktop/src/routes/bsc/index.svelte b/apps/desktop/src/routes/multicoin/bsc/index.svelte
similarity index 100%
rename from apps/desktop/src/routes/bsc/index.svelte
rename to apps/desktop/src/routes/multicoin/bsc/index.svelte
diff --git a/apps/desktop/src/routes/multicoin/index.svelte b/apps/desktop/src/routes/multicoin/index.svelte
new file mode 100644
index 0000000..62ab2f4
--- /dev/null
+++ b/apps/desktop/src/routes/multicoin/index.svelte
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/apps/desktop/src/routes/polygon/index.svelte b/apps/desktop/src/routes/multicoin/polygon/index.svelte
similarity index 100%
rename from apps/desktop/src/routes/polygon/index.svelte
rename to apps/desktop/src/routes/multicoin/polygon/index.svelte
diff --git a/apps/desktop/src/routes/tx/[chainid]/[hash].svelte b/apps/desktop/src/routes/tx/[chainid]/[hash].svelte
index a122227..aca8a5f 100644
--- a/apps/desktop/src/routes/tx/[chainid]/[hash].svelte
+++ b/apps/desktop/src/routes/tx/[chainid]/[hash].svelte
@@ -51,7 +51,7 @@
navigate('/')}
+ onclick={() => navigate('/multicoin')}
class="mb-4">
← Back