remove evm + add xrp
This commit is contained in:
@@ -1,799 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { electrobun, type TxEntry } from '$lib/electrobun.js'
|
||||
import {
|
||||
evmWallet as wallet,
|
||||
type NetworkId,
|
||||
} from '$lib/states/evm-wallet.svelte.js'
|
||||
import { Button } from '$lib/components/ui/button/index.js'
|
||||
import { Textarea } from '$lib/components/ui/textarea/index.js'
|
||||
import { generateMnemonic } from 'viem/accounts'
|
||||
import { english } from 'viem/accounts'
|
||||
import { navigate } from 'sv-router/generated'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '$lib/components/ui/card/index.js'
|
||||
import ArrowDown from '@lucide/svelte/icons/arrow-down'
|
||||
import ArrowUp from '@lucide/svelte/icons/arrow-up'
|
||||
import ArrowLeftRight from '@lucide/svelte/icons/arrow-left-right'
|
||||
import QrCodeIcon from '@lucide/svelte/icons/qr-code'
|
||||
import SettingsIcon from '@lucide/svelte/icons/settings'
|
||||
import Fingerprint from '@lucide/svelte/icons/fingerprint'
|
||||
import Trash2 from '@lucide/svelte/icons/trash-2'
|
||||
import Plus from '@lucide/svelte/icons/plus'
|
||||
import WalletIcon from '@lucide/svelte/icons/wallet'
|
||||
import LockKeyhole from '@lucide/svelte/icons/lock-keyhole'
|
||||
import ExternalLink from '@lucide/svelte/icons/external-link'
|
||||
import * as Dialog from '$lib/components/ui/dialog/index.js'
|
||||
import QRCode from 'qrcode'
|
||||
import Loader from './loader.svelte'
|
||||
import { LineChart } from 'layerchart'
|
||||
import { scaleUtc } from 'd3-scale'
|
||||
import { curveLinear } from 'd3-shape'
|
||||
import * as Chart from '$lib/components/ui/chart/index.js'
|
||||
import { formatEther, parseEther } from 'viem'
|
||||
import { frontendLog, frontendLogError } from '$lib/electrobun.js'
|
||||
|
||||
let { networkId }: { networkId: NetworkId } = $props()
|
||||
|
||||
const CDN =
|
||||
'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains'
|
||||
|
||||
const chainDir: Record<string, string> = {
|
||||
eth: 'ethereum',
|
||||
bsc: 'smartchain',
|
||||
polygon: 'polygon',
|
||||
}
|
||||
|
||||
function nativeIcon(id: string) {
|
||||
return `${CDN}/${chainDir[id]}/info/logo.png`
|
||||
}
|
||||
|
||||
function tokenIcon(id: string, address?: string) {
|
||||
if (!address) return ''
|
||||
return `${CDN}/${chainDir[id]}/assets/${address}/logo.png`
|
||||
}
|
||||
|
||||
function txAction(tx: TxEntry, address: string): string {
|
||||
if (tx.pairedValue) return 'Swap'
|
||||
const dir =
|
||||
tx.from.toLowerCase() === address.toLowerCase()
|
||||
? 'Sent'
|
||||
: 'Received'
|
||||
if (tx.tokenSymbol) return `${dir} ${tx.tokenSymbol}`
|
||||
return dir
|
||||
}
|
||||
|
||||
const w = wallet
|
||||
let inputSeed = $state('')
|
||||
let inputPassword = $state('')
|
||||
let inputUnlockPassword = $state('')
|
||||
let inputWalletName = $state('')
|
||||
let apiKeyInput = $state('')
|
||||
let qrDataUrl = $state('')
|
||||
let qrDialogOpen = $state(false)
|
||||
let showCreateForm = $state(false)
|
||||
$effect(() => {
|
||||
if (w.address) {
|
||||
QRCode.toDataURL(w.address, { width: 256, margin: 1 }).then(
|
||||
(url) => (qrDataUrl = url),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
function navTarget(id: NetworkId): string {
|
||||
if (id === 'eth') return '/multicoin'
|
||||
return `/multicoin/${id}`
|
||||
}
|
||||
|
||||
const CHAIN_NETWORKS: Record<string, string> = {
|
||||
'1': 'eth-mainnet',
|
||||
'137': 'polygon-mainnet',
|
||||
'56': 'bnb-mainnet',
|
||||
}
|
||||
|
||||
let initStarted = false
|
||||
let nativePrice = $state<{
|
||||
value: string
|
||||
currency: string
|
||||
} | null>(null)
|
||||
let tokenPrices = $state<
|
||||
Record<string, { value: string; currency: string }>
|
||||
>({})
|
||||
let gasPrice = $state<string | null>(null)
|
||||
|
||||
$effect(() => {
|
||||
const syms: string[] = w.symbol ? [w.symbol] : []
|
||||
const addrs: { network: string; address: string }[] = []
|
||||
const network = CHAIN_NETWORKS[w.chainid]
|
||||
if (network) {
|
||||
for (const t of w.tokenBalances) {
|
||||
if (t.contractAddress) {
|
||||
addrs.push({ network, address: t.contractAddress })
|
||||
}
|
||||
}
|
||||
}
|
||||
if (syms.length > 0 || addrs.length > 0) {
|
||||
electrobun.rpc?.request
|
||||
.fetchTokenPrices({
|
||||
symbols: syms.length > 0 ? syms : undefined,
|
||||
addresses: addrs.length > 0 ? addrs : undefined,
|
||||
})
|
||||
.then((prices) => {
|
||||
const usd = prices.find(
|
||||
(p) =>
|
||||
p.currency === 'usd' &&
|
||||
p.symbol === w.symbol &&
|
||||
!p.address,
|
||||
)
|
||||
nativePrice = usd
|
||||
? { value: usd.value, currency: usd.currency }
|
||||
: null
|
||||
const map: Record<
|
||||
string,
|
||||
{ value: string; currency: string }
|
||||
> = {}
|
||||
for (const p of prices) {
|
||||
if (p.currency === 'usd' && p.address) {
|
||||
map[p.address.toLowerCase()] = {
|
||||
value: p.value,
|
||||
currency: p.currency,
|
||||
}
|
||||
}
|
||||
}
|
||||
tokenPrices = map
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (w.chainid) {
|
||||
electrobun.rpc?.request
|
||||
.fetchGasPrice({ chainid: w.chainid })
|
||||
.then((gp) => (gasPrice = gp))
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (!initStarted) {
|
||||
initStarted = true
|
||||
w.init().then(async () => {
|
||||
if (w.accountType === 'multi') {
|
||||
await w.switchNetwork(networkId)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
function isNative(tx: TxEntry): boolean {
|
||||
return !tx.contractAddress
|
||||
}
|
||||
|
||||
const balanceHistory = $derived.by(() => {
|
||||
const native = w.transactions.filter(isNative)
|
||||
const sorted = [...native].reverse()
|
||||
if (sorted.length === 0) return []
|
||||
let cum = 0n
|
||||
const addr = w.address.toLowerCase()
|
||||
const points: { date: Date; balance: number }[] = []
|
||||
for (const tx of sorted) {
|
||||
console.log(tx.value)
|
||||
const val = parseEther(tx.value)
|
||||
if (tx.from.toLowerCase() === addr) cum -= val
|
||||
else cum += val
|
||||
points.push({
|
||||
date: new Date(Number(tx.timeStamp) * 1000),
|
||||
balance: Number(formatEther(cum)),
|
||||
})
|
||||
}
|
||||
const cur = Number(w.balance)
|
||||
const offset = cur - Number(formatEther(cum))
|
||||
for (const p of points) p.balance += offset
|
||||
return points
|
||||
})
|
||||
|
||||
$inspect(balanceHistory)
|
||||
|
||||
let selectedRange = $state<'1D' | '1W' | '1M' | '1Y' | 'ALL'>('ALL')
|
||||
|
||||
const rangeCutoff = $derived(
|
||||
selectedRange === 'ALL'
|
||||
? 0
|
||||
: Date.now() -
|
||||
(
|
||||
{
|
||||
'1D': 86400000,
|
||||
'1W': 604800000,
|
||||
'1M': 2592000000,
|
||||
'1Y': 31536000000,
|
||||
} as const
|
||||
)[selectedRange],
|
||||
)
|
||||
|
||||
const chartData = $derived(
|
||||
selectedRange === 'ALL'
|
||||
? balanceHistory
|
||||
: balanceHistory.filter((p) => p.date.getTime() >= rangeCutoff),
|
||||
)
|
||||
</script>
|
||||
|
||||
{#if !w.ready}
|
||||
<div
|
||||
class="mx-auto mt-16 max-w-md text-center text-muted-foreground text-sm">
|
||||
Loading...
|
||||
</div>
|
||||
{:else if w.wallets.length === 0 || showCreateForm}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{#if showCreateForm}
|
||||
{@const cur = w.currentWallet}
|
||||
{cur ? `Add Another Wallet` : `Import / Create Wallet`}
|
||||
{:else}
|
||||
Import / Create Wallet
|
||||
{/if}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Name your wallet and enter your seed phrase to save it
|
||||
securely, or generate a new wallet
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="flex flex-col gap-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Wallet name (e.g. Main, Savings)"
|
||||
class="flex h-8 w-full rounded-md border border-input bg-transparent px-2 py-1 font-mono text-xs"
|
||||
bind:value={inputWalletName} />
|
||||
<Textarea
|
||||
placeholder="Enter your 12 or 24 word seed phrase"
|
||||
bind:value={inputSeed} />
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => {
|
||||
inputSeed = generateMnemonic(english)
|
||||
}}
|
||||
class="w-full mt-1">
|
||||
Generate Random Seed
|
||||
</Button>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Set a password (optional)"
|
||||
class="flex h-8 w-full rounded-md border border-input bg-transparent px-2 py-1 font-mono text-xs"
|
||||
bind:value={inputPassword} />
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
onclick={() =>
|
||||
w.createWallet(
|
||||
inputWalletName,
|
||||
inputSeed,
|
||||
inputPassword || undefined,
|
||||
)}
|
||||
disabled={w.loading ||
|
||||
!inputSeed.trim() ||
|
||||
!inputWalletName.trim()}>
|
||||
{w.loading ? 'Saving...' : 'Save Wallet'}
|
||||
</Button>
|
||||
{#if showCreateForm}
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => (showCreateForm = false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if w.error}
|
||||
<p class="mt-3 text-red-500">{w.error}</p>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{:else if !w.currentWalletId}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Wallets</CardTitle>
|
||||
<CardDescription>Select a wallet to use</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each w.wallets as wal}
|
||||
<button
|
||||
onclick={async () => {
|
||||
if (wal.hasPassword) {
|
||||
await w.selectWallet(wal.id)
|
||||
} else {
|
||||
await w.selectAndUnlockWallet(wal.id)
|
||||
}
|
||||
}}
|
||||
class="flex w-full cursor-pointer items-center gap-3 rounded-md border border-input p-3 text-left hover:bg-muted transition-colors">
|
||||
<WalletIcon
|
||||
size={20}
|
||||
class="shrink-0 text-muted-foreground" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-medium text-sm">{wal.name}</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{new Date(wal.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
{#if wal.hasPassword}
|
||||
<LockKeyhole
|
||||
size={14}
|
||||
class="text-muted-foreground" />
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => (showCreateForm = true)}
|
||||
class="mt-2">
|
||||
<Plus size={16} />
|
||||
Add Wallet
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{:else if w.isLocked}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{w.currentWallet?.name ?? 'Wallet'} Locked
|
||||
</CardTitle>
|
||||
<CardDescription>Unlock to access your wallet</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="flex flex-col gap-3">
|
||||
{#if w.biometricAvailable}
|
||||
<Button
|
||||
onclick={() => w.unlockWithBiometrics()}
|
||||
disabled={w.loading}>
|
||||
<Fingerprint size={16} />
|
||||
{w.loading ? 'Unlocking...' : 'Unlock with Touch ID'}
|
||||
</Button>
|
||||
{/if}
|
||||
{#if w.currentPasswordHash}
|
||||
<div class="flex gap-2 items-end">
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Enter password"
|
||||
class="flex h-8 w-full rounded-md border border-input bg-transparent px-2 py-1 font-mono text-xs"
|
||||
bind:value={inputUnlockPassword} />
|
||||
<Button
|
||||
onclick={async () => {
|
||||
const ok = await w.unlockWithPassword(
|
||||
inputUnlockPassword,
|
||||
)
|
||||
if (!ok) w.error = 'Wrong password'
|
||||
inputUnlockPassword = ''
|
||||
}}
|
||||
disabled={w.loading || !inputUnlockPassword}
|
||||
size="sm">
|
||||
{w.loading ? '...' : 'Unlock'}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
<Button variant="outline" onclick={() => w.clearSelection()}>
|
||||
Pick another wallet
|
||||
</Button>
|
||||
</div>
|
||||
{#if w.error}
|
||||
<p class="mt-3 text-red-500">{w.error}</p>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex gap-1 justify-center" role="group">
|
||||
{#each w.networks as net}
|
||||
<button
|
||||
class="cursor-pointer rounded-md px-2.5 py-1 text-xs font-medium transition-colors {w.network ===
|
||||
net.id
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted text-muted-foreground hover:bg-muted/80'}"
|
||||
onclick={() => navigate(navTarget(net.id))}>
|
||||
<img
|
||||
src={nativeIcon(net.id)}
|
||||
alt=""
|
||||
class="-ml-0.5 mr-1 inline-block size-3.5 rounded-full align-middle"
|
||||
onerror={(e) =>
|
||||
((e.target as HTMLElement).style.display = 'none')} />
|
||||
{net.name}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>
|
||||
{w.currentWallet?.name ?? 'Wallet'}
|
||||
</CardTitle>
|
||||
<CardDescription>Your wallet is unlocked</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{#if w.address}
|
||||
<div class="mb-4 space-y-1">
|
||||
<p class="font-medium text-xs">Address</p>
|
||||
<div class="flex items-start gap-2">
|
||||
<p class="font-mono text-xs break-all flex-1">
|
||||
{w.address}
|
||||
</p>
|
||||
<button
|
||||
onclick={() =>
|
||||
electrobun.rpc?.request.openExternal({
|
||||
url: w.explorerAddressUrl,
|
||||
})}
|
||||
class="shrink-0 cursor-pointer rounded-md border p-1.5 text-muted-foreground hover:bg-muted transition-colors">
|
||||
<ExternalLink size={16} />
|
||||
</button>
|
||||
{#if qrDataUrl}
|
||||
<Dialog.Root bind:open={qrDialogOpen}>
|
||||
<Dialog.Trigger>
|
||||
<button
|
||||
class="shrink-0 cursor-pointer rounded-md border p-1.5 text-muted-foreground hover:bg-muted transition-colors">
|
||||
<QrCodeIcon size={16} />
|
||||
</button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Content>
|
||||
<div
|
||||
class="flex flex-col items-center gap-3 py-4">
|
||||
<div class="relative">
|
||||
<img
|
||||
src={qrDataUrl}
|
||||
alt="QR code"
|
||||
class="size-52 rounded-md" />
|
||||
<img
|
||||
src={nativeIcon(w.network)}
|
||||
alt=""
|
||||
class="absolute left-1/2 top-1/2 size-7 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white p-0.5" />
|
||||
</div>
|
||||
<p
|
||||
class="font-mono text-xs break-all text-center max-w-64">
|
||||
{w.address}
|
||||
</p>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="mt-2 font-medium text-xs">
|
||||
{w.symbol} Balance
|
||||
</p>
|
||||
<p class="flex items-center gap-1.5 font-mono text-lg">
|
||||
<img
|
||||
src={nativeIcon(w.network)}
|
||||
alt=""
|
||||
class="size-4 rounded-full"
|
||||
onerror={(e) =>
|
||||
((e.target as HTMLElement).style.display =
|
||||
'none')} />
|
||||
{w.balance}
|
||||
{w.symbol}
|
||||
</p>
|
||||
{#if nativePrice}
|
||||
{@const balVal =
|
||||
Number(w.balance) * Number(nativePrice.value)}
|
||||
<p class="font-mono text-xs text-muted-foreground">
|
||||
${balVal.toLocaleString(undefined, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})} @ ${Number(nativePrice.value).toLocaleString(
|
||||
undefined,
|
||||
{
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
},
|
||||
)}/{w.symbol}
|
||||
</p>
|
||||
{/if}
|
||||
{#if gasPrice}
|
||||
{@const gasNative = (Number(gasPrice) * 21000) / 1e9}
|
||||
<p class="font-mono text-xs text-muted-foreground mt-1">
|
||||
Gas: {Number(gasPrice).toFixed(1)} Gwei ({gasNative.toLocaleString(
|
||||
undefined,
|
||||
{
|
||||
minimumFractionDigits: 6,
|
||||
maximumFractionDigits: 6,
|
||||
},
|
||||
)}
|
||||
{w.symbol})
|
||||
{#if nativePrice}
|
||||
${(
|
||||
gasNative * Number(nativePrice.value)
|
||||
).toLocaleString(undefined, {
|
||||
minimumFractionDigits: 4,
|
||||
maximumFractionDigits: 4,
|
||||
})}
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mb-4 space-y-1">
|
||||
<p class="font-medium text-xs">Tokens</p>
|
||||
{#each w.tokenBalances as t}
|
||||
<div
|
||||
class="flex items-center gap-1.5 font-mono text-sm">
|
||||
<img
|
||||
src={t.logo}
|
||||
alt=""
|
||||
class="size-4 rounded-full"
|
||||
onerror={(e) =>
|
||||
((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}
|
||||
</div>
|
||||
{#if t.contractAddress && tokenPrices[t.contractAddress.toLowerCase()]}
|
||||
{@const val =
|
||||
tokenPrices[t.contractAddress.toLowerCase()]}
|
||||
{@const balVal =
|
||||
Number(t.balance) * Number(val.value)}
|
||||
<p
|
||||
class="font-mono text-xs text-muted-foreground -mt-0.5 ml-6">
|
||||
${balVal.toLocaleString(undefined, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})} @ ${Number(val.value).toLocaleString(
|
||||
undefined,
|
||||
{
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
},
|
||||
)}/{t.symbol}
|
||||
</p>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if w.loadingBalance && w.address}
|
||||
<div class="flex justify-center py-4">
|
||||
<Loader />
|
||||
</div>
|
||||
{/if}
|
||||
{#if w.error}
|
||||
<p class="mb-3 text-red-500">{w.error}</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => navigate('/multicoin/send')}>
|
||||
Send
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => navigate('/settings')}>
|
||||
<SettingsIcon size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={async () => {
|
||||
await w.logout()
|
||||
navigate('/')
|
||||
}}>
|
||||
Logout
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{#if balanceHistory.length > 1}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<CardTitle>Balance</CardTitle>
|
||||
<div class="flex gap-1">
|
||||
{#each ['1D', '1W', '1M', '1Y', 'ALL'] as const as range}
|
||||
<button
|
||||
onclick={() => (selectedRange = range)}
|
||||
class="rounded px-1.5 py-0.5 text-xs font-medium transition-colors {selectedRange ===
|
||||
range
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground'}">
|
||||
{range}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{#if chartData.length > 1}
|
||||
<Chart.Container
|
||||
config={{
|
||||
balance: { label: w.symbol, color: 'var(--chart-1)' },
|
||||
}}>
|
||||
<LineChart
|
||||
data={chartData}
|
||||
x="date"
|
||||
xScale={scaleUtc()}
|
||||
axis="x"
|
||||
points
|
||||
series={[
|
||||
{
|
||||
key: 'balance',
|
||||
label: w.symbol,
|
||||
color: 'var(--chart-1)',
|
||||
},
|
||||
]}
|
||||
yDomain={[
|
||||
chartData.reduce((a, b) =>
|
||||
a.balance < b.balance ? a : b,
|
||||
).balance,
|
||||
chartData.reduce((a, b) =>
|
||||
a.balance > b.balance ? a : b,
|
||||
).balance,
|
||||
]}
|
||||
props={{
|
||||
spline: {
|
||||
curve: curveLinear,
|
||||
motion: 'tween',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
yAxis: {},
|
||||
xAxis: {
|
||||
format: (v: Date) =>
|
||||
v.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
}),
|
||||
},
|
||||
highlight: { points: { r: 4 } },
|
||||
}}>
|
||||
{#snippet tooltip()}
|
||||
<Chart.Tooltip
|
||||
labelFormatter={(value: Date) => {
|
||||
return value.toLocaleDateString()
|
||||
}}
|
||||
valueFormatter={(value: Number) => {
|
||||
return Number(value).toFixed(6)
|
||||
}} />
|
||||
{/snippet}
|
||||
</LineChart>
|
||||
</Chart.Container>
|
||||
{:else}
|
||||
<p class="text-center text-sm text-muted-foreground">
|
||||
No transactions in this time range.
|
||||
</p>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Transactions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{#if w.loadingTransactions}
|
||||
<div class="flex justify-center py-4">
|
||||
<Loader />
|
||||
</div>
|
||||
{:else if !w.apiKey}
|
||||
<div class="flex gap-2 items-center">
|
||||
<input
|
||||
class="flex h-8 w-full rounded-md border border-input bg-transparent px-2 py-1 font-mono text-xs"
|
||||
placeholder="Alchemy API key"
|
||||
bind:value={apiKeyInput} />
|
||||
<Button
|
||||
size="sm"
|
||||
onclick={() => w.saveApiKey(apiKeyInput)}
|
||||
disabled={!apiKeyInput.trim()}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
{:else if w.transactions.length === 0}
|
||||
<p class="text-muted-foreground text-xs">
|
||||
No transactions found
|
||||
</p>
|
||||
{:else}
|
||||
<div class="max-h-96 space-y-1 overflow-y-auto">
|
||||
{#each w.transactions.filter((tx) => tx.tokenDecimal !== '0') as tx}
|
||||
<button
|
||||
onclick={() =>
|
||||
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">
|
||||
<div
|
||||
class="shrink-0 mt-0.5 {txAction(tx, w.address) ===
|
||||
'Swap'
|
||||
? 'text-yellow-500'
|
||||
: tx.from.toLowerCase() ===
|
||||
w.address.toLowerCase()
|
||||
? 'text-muted-foreground'
|
||||
: 'text-green-500'}">
|
||||
{#if txAction(tx, w.address) === 'Swap'}
|
||||
<ArrowLeftRight size={16} />
|
||||
{:else if tx.from.toLowerCase() === w.address.toLowerCase()}
|
||||
<ArrowUp size={16} />
|
||||
{:else}
|
||||
<ArrowDown size={16} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 space-y-0.5">
|
||||
<p class="font-medium">
|
||||
{txAction(tx, w.address)}
|
||||
</p>
|
||||
<p
|
||||
class="flex items-center gap-1 text-muted-foreground font-mono">
|
||||
{#if tx.pairedValue}
|
||||
<img
|
||||
src={nativeIcon(w.network)}
|
||||
alt=""
|
||||
class="size-3.5 rounded-full"
|
||||
onerror={(e) =>
|
||||
((e.target as HTMLElement).style.display =
|
||||
'none')} />
|
||||
{Number(tx.value).toFixed(4)}
|
||||
{w.symbol}
|
||||
<span class="mx-0.5">→</span>
|
||||
<img
|
||||
src={tx.pairedLogo ||
|
||||
tokenIcon(w.network, tx.contractAddress)}
|
||||
alt=""
|
||||
class="size-3.5 rounded-full"
|
||||
onerror={(e) =>
|
||||
((e.target as HTMLElement).style.display =
|
||||
'none')} />
|
||||
{Number(tx.pairedValue).toFixed(4)}
|
||||
{tx.pairedSymbol ?? w.symbol}
|
||||
{:else if tx.tokenSymbol}
|
||||
<img
|
||||
src={tx.logo ||
|
||||
tokenIcon(w.network, tx.contractAddress)}
|
||||
alt=""
|
||||
class="size-3.5 rounded-full"
|
||||
onerror={(e) =>
|
||||
((e.target as HTMLElement).style.display =
|
||||
'none')} />
|
||||
{Number(tx.value).toFixed(4)}
|
||||
{tx.tokenSymbol}
|
||||
{:else}
|
||||
<img
|
||||
src={nativeIcon(w.network)}
|
||||
alt=""
|
||||
class="size-3.5 rounded-full"
|
||||
onerror={(e) =>
|
||||
((e.target as HTMLElement).style.display =
|
||||
'none')} />
|
||||
{Number(tx.value).toFixed(4)}
|
||||
{w.symbol}
|
||||
{/if}
|
||||
</p>
|
||||
<p class="text-muted-foreground">
|
||||
{new Date(
|
||||
Number(tx.timeStamp) * 1000,
|
||||
).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</p>
|
||||
<p
|
||||
class="text-muted-foreground/50 truncate font-mono">
|
||||
{tx.hash.slice(0, 10)}...
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -9,10 +9,11 @@ export type {
|
||||
MoneroAccountEntry,
|
||||
MoneroSendResult,
|
||||
MoneroTransferDetails,
|
||||
TxEntry,
|
||||
TransactionDetails,
|
||||
TokenBalanceResult,
|
||||
EvmWalletInfo,
|
||||
XrpWalletInfo,
|
||||
XrpBalance,
|
||||
XrpTxEntry,
|
||||
XrpTxDetails,
|
||||
XrpSendResult,
|
||||
} from './rpc-schema'
|
||||
|
||||
const rpc = Electroview.defineRPC<RPC>({
|
||||
|
||||
@@ -79,67 +79,50 @@ export type MoneroFeeEstimate = {
|
||||
estimatedFee: string
|
||||
}
|
||||
|
||||
export type TxEntry = {
|
||||
hash: string
|
||||
timeStamp: string
|
||||
from: string
|
||||
to: string
|
||||
value: string
|
||||
tokenSymbol?: string
|
||||
tokenDecimal?: string
|
||||
contractAddress?: string
|
||||
logo?: string
|
||||
pairedValue?: string
|
||||
pairedSymbol?: string
|
||||
pairedDecimals?: string
|
||||
pairedContractAddress?: string
|
||||
pairedLogo?: string
|
||||
}
|
||||
|
||||
export type TransactionDetails = {
|
||||
hash: string
|
||||
from: string
|
||||
to: string | null
|
||||
value: string
|
||||
blockNumber: string | null
|
||||
fee: string
|
||||
gasPrice: string
|
||||
status: 'success' | 'reverted'
|
||||
type: string
|
||||
nonce: number
|
||||
input: string
|
||||
pairedValue?: string
|
||||
pairedSymbol?: string
|
||||
pairedDecimals?: string
|
||||
pairedContractAddress?: string
|
||||
pairedLogo?: string
|
||||
}
|
||||
|
||||
export type TokenBalanceResult = {
|
||||
symbol: string
|
||||
decimals: number
|
||||
balance: string
|
||||
contractAddress: string
|
||||
logo?: string
|
||||
}
|
||||
|
||||
export type TokenPriceEntry = {
|
||||
symbol: string
|
||||
currency: string
|
||||
value: string
|
||||
lastUpdatedAt: string
|
||||
network?: string
|
||||
address?: string
|
||||
}
|
||||
|
||||
export type EvmWalletInfo = {
|
||||
export type XrpWalletInfo = {
|
||||
id: string
|
||||
name: string
|
||||
address: string
|
||||
hasPassword: boolean
|
||||
vaultKey: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export type XrpBalance = {
|
||||
address: string
|
||||
balance: string
|
||||
funded: boolean
|
||||
}
|
||||
|
||||
export type XrpTxEntry = {
|
||||
hash: string
|
||||
amount: string
|
||||
timestamp: string
|
||||
direction: 'in' | 'out'
|
||||
from: string
|
||||
to: string
|
||||
destinationTag?: number
|
||||
fee: string
|
||||
confirmed: boolean
|
||||
}
|
||||
|
||||
export type XrpTxDetails = {
|
||||
hash: string
|
||||
from: string
|
||||
to: string
|
||||
amount: string
|
||||
fee: string
|
||||
timestamp: string
|
||||
ledgerIndex: number
|
||||
destinationTag?: number
|
||||
confirmed: boolean
|
||||
}
|
||||
|
||||
export type XrpSendResult = {
|
||||
hash: string
|
||||
fee: string
|
||||
}
|
||||
|
||||
export type GeneralRpcRequests = {
|
||||
resetApp: { params: {}; response: boolean }
|
||||
biometricCanAuth: { params: {}; response: boolean }
|
||||
@@ -151,23 +134,6 @@ export type GeneralRpcRequests = {
|
||||
logToFile: { params: { message: string }; response: void }
|
||||
}
|
||||
|
||||
export type EvmRpcRequests = {
|
||||
fetchTxHistory: { params: { address: string; chainid: string }; response: TxEntry[] }
|
||||
fetchCachedTxHistory: { params: { address: string; chainid: string }; response: TxEntry[] }
|
||||
syncTxHistory: { params: { address: string; chainid: string }; response: void }
|
||||
flushTxCache: { params: {}; response: void }
|
||||
setAutoSync: { params: { address: string; chainid: string } | null; response: void }
|
||||
fetchTokenBalances: { params: { address: string; chainid: string }; response: TokenBalanceResult[] }
|
||||
fetchTransactionDetails: { params: { hash: string; chainid: string; address?: string }; response: TransactionDetails | null }
|
||||
fetchTokenPrices: { params: { symbols?: string[]; addresses?: { network: string; address: string }[] }; response: TokenPriceEntry[] }
|
||||
fetchGasPrice: { params: { chainid: string }; response: string | null }
|
||||
evmCreateWallet: { params: { name: string; phrase: string; passwordHash?: string }; response: { id: string; name: string; createdAt: string } }
|
||||
evmListWallets: { params: {}; response: EvmWalletInfo[] }
|
||||
evmGetSeed: { params: { vaultKey: string }; response: string }
|
||||
evmDeleteWallet: { params: { id: string }; response: void }
|
||||
evmSendTransfer: { params: { seed: string; to: string; amount: string; chainid: string; contractAddress?: string; tokenDecimals?: number }; response: string }
|
||||
}
|
||||
|
||||
export type MoneroRpcRequests = {
|
||||
moneroBinaryStatus: { params: {}; response: MoneroBinaryStatus }
|
||||
moneroDownloadBinary: { params: {}; response: void }
|
||||
@@ -190,15 +156,27 @@ export type MoneroRpcRequests = {
|
||||
fetchMoneroPrice: { params: {}; response: { usd: string } | null }
|
||||
}
|
||||
|
||||
export type XrpRpcRequests = {
|
||||
xrpListWallets: { params: {}; response: XrpWalletInfo[] }
|
||||
xrpCreateWallet: { params: { name: string; hasPassword?: boolean }; response: { id: string; address: string; seed: string; createdAt: string } }
|
||||
xrpImportWallet: { params: { name: string; secret: string; hasPassword?: boolean }; response: { id: string; address: string; createdAt: string } }
|
||||
xrpDeleteWallet: { params: { id: string }; response: void }
|
||||
xrpGetSeed: { params: { vaultKey: string }; response: string }
|
||||
xrpGetBalance: { params: { address: string }; response: XrpBalance }
|
||||
xrpGetTransactions: { params: { address: string }; response: XrpTxEntry[] }
|
||||
xrpGetTxDetails: { params: { hash: string }; response: XrpTxDetails | null }
|
||||
xrpSend: { params: { secret: string; to: string; amount: string; destinationTag?: number }; response: XrpSendResult }
|
||||
xrpGetFee: { params: {}; response: { fee: string } | null }
|
||||
fetchXrpPrice: { params: {}; response: { usd: string } | null }
|
||||
}
|
||||
|
||||
export type RPC = {
|
||||
bun: {
|
||||
requests: GeneralRpcRequests & EvmRpcRequests & MoneroRpcRequests
|
||||
requests: GeneralRpcRequests & MoneroRpcRequests & XrpRpcRequests
|
||||
messages: {}
|
||||
}
|
||||
webview: {
|
||||
requests: {}
|
||||
messages: {
|
||||
transfersUpdate: { count: number; chainid: string; address: string }
|
||||
}
|
||||
messages: {}
|
||||
}
|
||||
}
|
||||
|
||||
+103
-242
@@ -1,73 +1,37 @@
|
||||
import { mnemonicToAccount } from 'viem/accounts'
|
||||
import { createPublicClient, http, formatEther } from 'viem'
|
||||
import {
|
||||
electrobun,
|
||||
type TxEntry,
|
||||
type EvmWalletInfo,
|
||||
type XrpTxEntry,
|
||||
type XrpWalletInfo,
|
||||
} from '$lib/electrobun'
|
||||
import { tryCatch } from '@koins/utils'
|
||||
import { moneroWallet } from './monero-wallet.svelte.js'
|
||||
|
||||
export const networks = [
|
||||
{
|
||||
id: 'bsc',
|
||||
name: 'BNB Smart Chain',
|
||||
rpc: 'https://bsc-dataseed.binance.org',
|
||||
symbol: 'BNB',
|
||||
chainid: '56',
|
||||
explorerUrl: 'https://bscscan.com/tx/',
|
||||
},
|
||||
{
|
||||
id: 'polygon',
|
||||
name: 'Polygon',
|
||||
rpc: 'https://polygon-bor.publicnode.com',
|
||||
symbol: 'POL',
|
||||
chainid: '137',
|
||||
explorerUrl: 'https://polygonscan.com/tx/',
|
||||
},
|
||||
{
|
||||
id: 'eth',
|
||||
name: 'Ethereum',
|
||||
rpc: 'https://ethereum-rpc.publicnode.com',
|
||||
symbol: 'ETH',
|
||||
chainid: '1',
|
||||
explorerUrl: 'https://etherscan.io/tx/',
|
||||
},
|
||||
] as const
|
||||
export const xrpNetwork = {
|
||||
id: 'xrp',
|
||||
name: 'XRP Ledger',
|
||||
symbol: 'XRP',
|
||||
explorerUrl: 'https://xrpscan.com/tx/',
|
||||
explorerAddressUrl: 'https://xrpscan.com/account/',
|
||||
} as const
|
||||
|
||||
export type NetworkId = (typeof networks)[number]['id']
|
||||
|
||||
export type TokenBalance = {
|
||||
symbol: string
|
||||
decimals: number
|
||||
balance: string
|
||||
contractAddress: string
|
||||
logo?: string
|
||||
}
|
||||
|
||||
export type AccountType = 'multi' | 'monero'
|
||||
|
||||
export const EvmWallet = () => {
|
||||
let accountType = $state<AccountType | null>(null)
|
||||
export const XrpWallet = () => {
|
||||
let accountType = $state<'xrp' | null>(null)
|
||||
let ready = $state(false)
|
||||
let biometricAvailable = $state(false)
|
||||
let seed = $state('')
|
||||
let address = $state('')
|
||||
let balance = $state('0')
|
||||
let tokenBalances = $state<TokenBalance[]>([])
|
||||
let network = $state<NetworkId>('eth')
|
||||
let apiKey = $state('')
|
||||
let transactions = $state<TxEntry[]>([])
|
||||
let funded = $state(true)
|
||||
let price = $state<string | null>(null)
|
||||
let fee = $state<string | null>(null)
|
||||
let transactions = $state<XrpTxEntry[]>([])
|
||||
let loadingBalance = $state(false)
|
||||
let loadingTransactions = $state(false)
|
||||
let loading = $state(false)
|
||||
let error = $state('')
|
||||
let wallets = $state<EvmWalletInfo[]>([])
|
||||
let wallets = $state<XrpWalletInfo[]>([])
|
||||
let currentWalletId = $state<string | null>(null)
|
||||
let currentPasswordHash = $state<string | null>(null)
|
||||
|
||||
const net = () => networks.find((n) => n.id === network)!
|
||||
|
||||
async function hashPassword(
|
||||
password: string,
|
||||
salt?: string,
|
||||
@@ -111,25 +75,8 @@ export const EvmWallet = () => {
|
||||
|
||||
const init = async () => {
|
||||
if (!electrobun.rpc) return
|
||||
|
||||
electrobun.rpc.addMessageListener(
|
||||
'transfersUpdate',
|
||||
(payload: {
|
||||
chainid: string
|
||||
address: string
|
||||
count: number
|
||||
}) => {
|
||||
if (
|
||||
payload.address === address &&
|
||||
payload.chainid === net().chainid
|
||||
) {
|
||||
fetchTxHistory()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const [walletList] = await tryCatch(
|
||||
electrobun.rpc.request.evmListWallets({}),
|
||||
electrobun.rpc.request.xrpListWallets({}),
|
||||
)
|
||||
if (walletList) {
|
||||
wallets = walletList
|
||||
@@ -137,28 +84,13 @@ export const EvmWallet = () => {
|
||||
currentWalletId = walletList[0].id
|
||||
}
|
||||
}
|
||||
|
||||
const [key] = await tryCatch(
|
||||
electrobun.rpc.request.getSecret({
|
||||
service: 'koins',
|
||||
name: 'alchemy_key',
|
||||
}),
|
||||
)
|
||||
if (key) apiKey = key
|
||||
|
||||
// await moneroWallet.checkStatus()
|
||||
// if (moneroWallet.installed && !moneroWallet.running && !moneroWallet.downloading) {
|
||||
// await moneroWallet.start()
|
||||
// await moneroWallet.checkStatus()
|
||||
// }
|
||||
|
||||
await checkBiometric()
|
||||
ready = true
|
||||
}
|
||||
|
||||
const login = async () => {
|
||||
accountType = 'multi'
|
||||
if (seed) await switchNetwork(network)
|
||||
accountType = 'xrp'
|
||||
if (seed) await refresh()
|
||||
}
|
||||
|
||||
const logout = async () => {
|
||||
@@ -170,27 +102,25 @@ export const EvmWallet = () => {
|
||||
seed = ''
|
||||
address = ''
|
||||
balance = '0'
|
||||
tokenBalances = []
|
||||
funded = true
|
||||
transactions = []
|
||||
error = ''
|
||||
currentPasswordHash = null
|
||||
electrobun.rpc?.request.setAutoSync(null)
|
||||
}
|
||||
|
||||
const unlockWallet = async (walletId: string) => {
|
||||
const loadSeedForWallet = async (walletId: string) => {
|
||||
if (!electrobun.rpc) return false
|
||||
const wallet = wallets.find((w) => w.id === walletId)
|
||||
if (!wallet) return false
|
||||
currentWalletId = walletId
|
||||
const [vaultSeed] = await tryCatch(
|
||||
electrobun.rpc.request.evmGetSeed({
|
||||
electrobun.rpc.request.xrpGetSeed({
|
||||
vaultKey: wallet.vaultKey,
|
||||
}),
|
||||
)
|
||||
if (!vaultSeed) return false
|
||||
seed = vaultSeed
|
||||
address = wallet.address
|
||||
await refresh()
|
||||
await setAutoSync()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -203,8 +133,7 @@ export const EvmWallet = () => {
|
||||
)
|
||||
if (!authed) return false
|
||||
if (!currentWalletId) return false
|
||||
await loadSeedForWallet(currentWalletId)
|
||||
return !!seed
|
||||
return loadSeedForWallet(currentWalletId)
|
||||
}
|
||||
|
||||
const unlockWithPassword = async (password: string) => {
|
||||
@@ -219,22 +148,6 @@ export const EvmWallet = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadSeedForWallet = async (walletId: string) => {
|
||||
if (!electrobun.rpc) return false
|
||||
const wallet = wallets.find((w) => w.id === walletId)
|
||||
if (!wallet) return false
|
||||
const [vaultSeed] = await tryCatch(
|
||||
electrobun.rpc.request.evmGetSeed({
|
||||
vaultKey: wallet.vaultKey,
|
||||
}),
|
||||
)
|
||||
if (!vaultSeed) return false
|
||||
seed = vaultSeed
|
||||
await refresh()
|
||||
await setAutoSync()
|
||||
return true
|
||||
}
|
||||
|
||||
const selectWallet = async (walletId: string) => {
|
||||
currentWalletId = walletId
|
||||
clearWallet()
|
||||
@@ -244,7 +157,7 @@ export const EvmWallet = () => {
|
||||
const [ph] = await tryCatch(
|
||||
electrobun.rpc.request.getSecret({
|
||||
service: 'koins',
|
||||
name: `evm_auth_${walletId}`,
|
||||
name: `xrp_auth_${walletId}`,
|
||||
}),
|
||||
)
|
||||
if (ph) {
|
||||
@@ -260,61 +173,89 @@ export const EvmWallet = () => {
|
||||
if (!currentPasswordHash && biometricAvailable) {
|
||||
return unlockWithBiometrics()
|
||||
}
|
||||
if (!currentPasswordHash) {
|
||||
return loadSeedForWallet(walletId)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const createWallet = async (
|
||||
const saveWallet = async (
|
||||
kind: 'create' | 'import',
|
||||
name: string,
|
||||
phrase: string,
|
||||
secret: string | undefined,
|
||||
password?: string,
|
||||
) => {
|
||||
loading = true
|
||||
error = ''
|
||||
try {
|
||||
mnemonicToAccount(phrase.trim())
|
||||
if (!electrobun.rpc) throw new Error('RPC not available')
|
||||
let passwordHash: string | undefined
|
||||
if (password) {
|
||||
const ph = await hashPassword(password)
|
||||
passwordHash = JSON.stringify(ph)
|
||||
}
|
||||
const [result, resultError] = await tryCatch(
|
||||
electrobun.rpc!.request.evmCreateWallet({
|
||||
name,
|
||||
phrase: phrase.trim(),
|
||||
passwordHash,
|
||||
}),
|
||||
)
|
||||
if (resultError) throw resultError
|
||||
const result: {
|
||||
id: string
|
||||
address: string
|
||||
createdAt: string
|
||||
seed?: string
|
||||
} =
|
||||
kind === 'create'
|
||||
? await electrobun.rpc.request.xrpCreateWallet({
|
||||
name,
|
||||
hasPassword: !!passwordHash,
|
||||
})
|
||||
: await electrobun.rpc.request.xrpImportWallet({
|
||||
name,
|
||||
secret: secret!,
|
||||
hasPassword: !!passwordHash,
|
||||
})
|
||||
|
||||
if (passwordHash) {
|
||||
await electrobun.rpc?.request.setSecret({
|
||||
await electrobun.rpc.request.setSecret({
|
||||
service: 'koins',
|
||||
name: `evm_auth_${result.id}`,
|
||||
name: `xrp_auth_${result.id}`,
|
||||
value: passwordHash,
|
||||
})
|
||||
}
|
||||
|
||||
const newWallet: EvmWalletInfo = {
|
||||
const newWallet: XrpWalletInfo = {
|
||||
id: result.id,
|
||||
name,
|
||||
address: result.address,
|
||||
hasPassword: !!passwordHash,
|
||||
vaultKey: `evm_seed_${result.id}`,
|
||||
vaultKey: `xrp_seed_${result.id}`,
|
||||
createdAt: result.createdAt,
|
||||
}
|
||||
wallets = [...wallets, newWallet]
|
||||
currentWalletId = result.id
|
||||
if (passwordHash) currentPasswordHash = passwordHash
|
||||
seed = phrase.trim()
|
||||
seed = result.seed ?? secret!
|
||||
address = result.address
|
||||
await refresh()
|
||||
return result.seed
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Invalid seed phrase'
|
||||
error = e instanceof Error ? e.message : 'Failed to save wallet'
|
||||
throw e
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
const createWallet = async (name: string, password?: string) => {
|
||||
return saveWallet('create', name, undefined, password)
|
||||
}
|
||||
|
||||
const importWallet = async (
|
||||
name: string,
|
||||
secret: string,
|
||||
password?: string,
|
||||
) => {
|
||||
return saveWallet('import', name, secret, password)
|
||||
}
|
||||
|
||||
const deleteWallet = async (walletId: string) => {
|
||||
await electrobun.rpc?.request.evmDeleteWallet({ id: walletId })
|
||||
await electrobun.rpc?.request.xrpDeleteWallet({ id: walletId })
|
||||
wallets = wallets.filter((w) => w.id !== walletId)
|
||||
if (currentWalletId === walletId) {
|
||||
clearWallet()
|
||||
@@ -327,103 +268,49 @@ export const EvmWallet = () => {
|
||||
currentWalletId = null
|
||||
}
|
||||
|
||||
const switchNetwork = async (id: NetworkId) => {
|
||||
network = id
|
||||
if (seed) await refresh()
|
||||
}
|
||||
|
||||
const refresh = async () => {
|
||||
if (!seed || !electrobun.rpc) return
|
||||
if (!address || !electrobun.rpc) return
|
||||
const rpc = electrobun.rpc
|
||||
loadingBalance = true
|
||||
loadingTransactions = true
|
||||
balance = '0'
|
||||
error = ''
|
||||
const account = mnemonicToAccount(seed)
|
||||
address = account.address
|
||||
const client = createPublicClient({ transport: http(net().rpc) })
|
||||
|
||||
syncTxHistory()
|
||||
;(async () => {
|
||||
const [nativeBal] = await tryCatch(
|
||||
client.getBalance({ address: account.address }),
|
||||
const [bal] = await tryCatch(
|
||||
rpc.request.xrpGetBalance({ address }),
|
||||
)
|
||||
if (nativeBal !== undefined) balance = formatEther(nativeBal)
|
||||
const [bals] = await tryCatch(
|
||||
electrobun.rpc.request.fetchTokenBalances({
|
||||
address,
|
||||
chainid: net().chainid,
|
||||
}),
|
||||
)
|
||||
if (bals) tokenBalances = bals
|
||||
if (bal) {
|
||||
balance = bal.balance
|
||||
funded = bal.funded
|
||||
}
|
||||
const [p] = await tryCatch(rpc.request.fetchXrpPrice({}))
|
||||
if (p) price = p.usd
|
||||
const [f] = await tryCatch(rpc.request.xrpGetFee({}))
|
||||
if (f) fee = f.fee
|
||||
loadingBalance = false
|
||||
})()
|
||||
;(async () => {
|
||||
await fetchTxHistory()
|
||||
const [txs] = await tryCatch(
|
||||
rpc.request.xrpGetTransactions({ address }),
|
||||
)
|
||||
if (txs) transactions = txs
|
||||
loadingTransactions = false
|
||||
})()
|
||||
}
|
||||
|
||||
const syncTxHistory = () => {
|
||||
if (!address || !apiKey || !electrobun.rpc) return
|
||||
electrobun.rpc.request.syncTxHistory({
|
||||
address,
|
||||
chainid: net().chainid,
|
||||
})
|
||||
}
|
||||
|
||||
const setAutoSync = async () => {
|
||||
if (!address || !electrobun.rpc) return
|
||||
electrobun.rpc.request.setAutoSync({
|
||||
address,
|
||||
chainid: net().chainid,
|
||||
})
|
||||
}
|
||||
|
||||
const flushTxCache = async () => {
|
||||
await electrobun.rpc?.request.flushTxCache({})
|
||||
await fetchTxHistory()
|
||||
}
|
||||
|
||||
const saveApiKey = async (key: string) => {
|
||||
await electrobun.rpc?.request.setSecret({
|
||||
service: 'koins',
|
||||
name: 'alchemy_key',
|
||||
value: key,
|
||||
})
|
||||
apiKey = key
|
||||
syncTxHistory()
|
||||
if (address) await fetchTxHistory()
|
||||
}
|
||||
|
||||
const fetchTxHistory = async () => {
|
||||
if (!address) {
|
||||
transactions = []
|
||||
return
|
||||
}
|
||||
const txs = await electrobun.rpc?.request.fetchCachedTxHistory({
|
||||
address,
|
||||
chainid: net().chainid,
|
||||
})
|
||||
transactions = txs ?? []
|
||||
}
|
||||
|
||||
const send = async (
|
||||
to: string,
|
||||
amount: string,
|
||||
contractAddress?: string,
|
||||
tokenDecimals?: number,
|
||||
destinationTag?: number,
|
||||
) => {
|
||||
if (!electrobun.rpc || !seed) throw new Error('Wallet not unlocked')
|
||||
const hash = await electrobun.rpc.request.evmSendTransfer({
|
||||
seed,
|
||||
const result = await electrobun.rpc.request.xrpSend({
|
||||
secret: seed,
|
||||
to,
|
||||
amount,
|
||||
chainid: net().chainid,
|
||||
contractAddress,
|
||||
tokenDecimals,
|
||||
destinationTag,
|
||||
})
|
||||
await refresh()
|
||||
return hash
|
||||
return result
|
||||
}
|
||||
|
||||
const lock = () => {
|
||||
@@ -432,7 +319,6 @@ export const EvmWallet = () => {
|
||||
|
||||
const reset = () => {
|
||||
clearWallet()
|
||||
apiKey = ''
|
||||
wallets = []
|
||||
currentWalletId = null
|
||||
}
|
||||
@@ -464,34 +350,14 @@ export const EvmWallet = () => {
|
||||
get balance() {
|
||||
return balance
|
||||
},
|
||||
get tokenBalances() {
|
||||
return tokenBalances
|
||||
get funded() {
|
||||
return funded
|
||||
},
|
||||
get network() {
|
||||
return network
|
||||
get price() {
|
||||
return price
|
||||
},
|
||||
get chainid() {
|
||||
return net().chainid
|
||||
},
|
||||
get networkName() {
|
||||
return net().name
|
||||
},
|
||||
get symbol() {
|
||||
return net().symbol
|
||||
},
|
||||
get explorerUrl() {
|
||||
return net().explorerUrl
|
||||
},
|
||||
get explorerAddressUrl() {
|
||||
return address
|
||||
? `${net().explorerUrl.replace('/tx/', '/address/')}${address}`
|
||||
: ''
|
||||
},
|
||||
get networks() {
|
||||
return networks
|
||||
},
|
||||
get apiKey() {
|
||||
return apiKey
|
||||
get fee() {
|
||||
return fee
|
||||
},
|
||||
get transactions() {
|
||||
return transactions
|
||||
@@ -526,8 +392,8 @@ export const EvmWallet = () => {
|
||||
get currentPasswordHash() {
|
||||
return currentPasswordHash
|
||||
},
|
||||
set apiKey(v: string) {
|
||||
apiKey = v
|
||||
get explorerAddressUrl() {
|
||||
return address ? `${xrpNetwork.explorerAddressUrl}${address}` : ''
|
||||
},
|
||||
set error(v: string) {
|
||||
error = v
|
||||
@@ -537,9 +403,7 @@ export const EvmWallet = () => {
|
||||
logout,
|
||||
refresh,
|
||||
createWallet,
|
||||
switchNetwork,
|
||||
saveApiKey,
|
||||
fetchTxHistory,
|
||||
importWallet,
|
||||
lock,
|
||||
reset,
|
||||
resetApp,
|
||||
@@ -548,14 +412,11 @@ export const EvmWallet = () => {
|
||||
hashPassword,
|
||||
selectWallet,
|
||||
selectAndUnlockWallet,
|
||||
unlockWallet,
|
||||
loadSeedForWallet,
|
||||
deleteWallet,
|
||||
clearSelection,
|
||||
send,
|
||||
syncTxHistory,
|
||||
setAutoSync,
|
||||
flushTxCache,
|
||||
}
|
||||
}
|
||||
|
||||
export const evmWallet = EvmWallet()
|
||||
export const xrpWallet = XrpWallet()
|
||||
Reference in New Issue
Block a user