remove evm + add xrp

This commit is contained in:
2026-08-01 00:58:22 +08:00
parent f46e61ed5e
commit 0d3acef182
55 changed files with 1457 additions and 4148 deletions
+17 -21
View File
@@ -1,16 +1,16 @@
# Koins
Multi-chain desktop wallet for **Ethereum**, **BSC**, **Polygon**, and **Monero**. Built with [Bun](https://bun.sh) + [Electrobun](https://electrobun.dev) + Svelte.
Desktop wallet for **Monero** and **XRP**. Built with [Bun](https://bun.sh) + [Electrobun](https://electrobun.dev) + Svelte.
## Features
- **Multi-coin mode**: ETH, BSC, Polygon wallet with seed phrase vault stored in system keychain
- **Monero mode**: Self-contained Monero wallet — auto-downloads `monero-wallet-rpc` binary on first launch, no manual binary install
- **XRP mode**: XRP Ledger wallet — generate or import XRP seeds, send payments with optional destination tags
- **Touch ID unlock**: Biometric auth via Swift helper, no node-gyp or FFI dylib
- **Password fallback**: PBKDF2-salted password stored in keychain, used when biometric is unavailable
- **QR codes**: Address QR with chain logo overlay
- **Transaction history**: Cached in local SQLite via Drizzle ORM
- **Settings**: Reset app clears all DB tables and keychain entries
- **Transaction history**: Live XRP ledger transactions and Monero wallet transactions
- **Settings**: Reset app clears all keychain entries
## Prerequisites
@@ -35,41 +35,38 @@ apps/desktop/
│ │ │ ├── monero/ # Monero RPC management
│ │ │ │ ├── binary.ts # Download/extract monero-wallet-rpc
│ │ │ │ └── wallet.ts # Child process + RPC client
│ │ │ ├── alchemy/ # Alchemy API calls
│ │ │ ├── viem/ # viem chain calls
│ │ │ ├── tokens/ # Token metadata (DB-first cache)
│ │ │ ├── transactions/ # Tx history + details (DB-first cache)
│ │ │ ├── db/ # Drizzle ORM + SQLite schema
│ │ │ ├── xrp/ # XRP Ledger client (xrpl)
│ │ │ └── biometric.ts # Biometric auth (Swift subprocess)
│ │ ├── rpc/
│ │ │ ├── monero/ # Monero RPC handlers
│ │ │ └── xrp/ # XRP RPC handlers
│ │ └── native/
│ │ └── biometric-helper.swift # Touch ID prompt binary
│ ├── lib/
│ │ ├── states/
│ │ │ └── wallet.svelte.ts # Wallet state (singleton)
│ │ ├── components/
│ │ │ └── EvmDashboard.svelte # Shared EVM dashboard
│ │ │ ├── monero-wallet.svelte.ts # Monero wallet state (singleton)
│ │ │ └── xrp-wallet.svelte.ts # XRP wallet state (singleton)
│ │ └── electrobun.ts # RPC type definitions
│ └── routes/
│ ├── index.svelte # Login/welcome page
│ ├── multicoin/ # ETH / BSC / Polygon routes
│ ├── monero/ # Monero dashboard
│ ├── settings.svelte # App settings
│ └── tx/ # Transaction details
│ ├── monero/ # Monero routes
│ ├── xrp/ # XRP routes
│ └── settings.svelte # App settings
└── package.json
```
## Key Decisions
- **Monero via RPC, not WASM**: `monero-ts`'s WASM wallet (`MoneroWalletFull`) doesn't work in Bun because the C++ HTTP client fails at runtime. Instead, `monero-wallet-rpc` runs as a child process.
- **SQLite cache**: All chain data (tx history, receipts, token metadata) cached in SQLite via Drizzle, not localStorage.
- **XRP via xrpl**: Uses the official `xrpl` library over the public `wss://s1.ripple.com` WebSocket server. XRP seeds are stored in the system keychain.
- **Keychain storage**: Wallet seeds and metadata are stored in the macOS keychain via `Bun.secrets`, not a local database.
- **Biometric auth**: Uses a Swift binary spawned via `Bun.spawnSync()` (not `dlopen` — Bun crashes on ObjC runtime; not `node-mac-auth` — fragile node-gyp chain).
- **Separate account types**: EVM and Monero are separate account types with their own seed formats and auth flows.
- **Separate account types**: Monero and XRP are separate account types with their own seed formats and auth flows.
## Dependencies
- [Viem](https://viem.sh) — EVM chain RPC calls + wallet operations
- [xrpl](https://github.com/XRPLF/xrpl.js) — XRP Ledger client
- [monero-ts](https://github.com/woodser/monero-ts) — Monero RPC client
- [Drizzle ORM](https://orm.drizzle.team) — SQLite schema + queries
- [Svelte 5](https://svelte.dev) — UI framework
- [Electrobun](https://electrobun.dev) — Desktop app shell
- [sv-router](https://github.com/TZGyn/sv-router) — File-based routing
@@ -80,5 +77,4 @@ apps/desktop/
```bash
bun run dev # Start development server
bun run db:push # Push Drizzle schema to SQLite
```
-19
View File
@@ -1,19 +0,0 @@
import { defineConfig } from 'drizzle-kit'
import envPaths from 'env-paths'
import { join } from 'path'
import { mkdirSync } from 'fs'
import { getENV } from './src/bun/lib/get-env'
const env = getENV()
const { data } = envPaths(env.appName, { suffix: '' })
mkdirSync(data, { recursive: true })
export default defineConfig({
out: './drizzle',
schema: './src/bun/lib/db/schema.ts',
dialect: 'sqlite',
dbCredentials: {
url: join(data, env.dbFilename),
},
})
@@ -1,10 +0,0 @@
CREATE TABLE `token_metadata` (
`name` text,
`symbol` text NOT NULL,
`decimals` integer,
`logo` text,
`contractAddress` text NOT NULL,
`chainId` text NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `contract_unique` ON `token_metadata` (`chainId`,`contractAddress`);
@@ -1,93 +0,0 @@
{
"version": "7",
"dialect": "sqlite",
"id": "2845d968-33cd-48b6-8944-03e0e46a4a90",
"prevIds": [
"00000000-0000-0000-0000-000000000000"
],
"ddl": [
{
"name": "token_metadata",
"entityType": "tables"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "name",
"entityType": "columns",
"table": "token_metadata"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "symbol",
"entityType": "columns",
"table": "token_metadata"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "decimals",
"entityType": "columns",
"table": "token_metadata"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "logo",
"entityType": "columns",
"table": "token_metadata"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "contractAddress",
"entityType": "columns",
"table": "token_metadata"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "chainId",
"entityType": "columns",
"table": "token_metadata"
},
{
"columns": [
{
"value": "chainId",
"isExpression": false
},
{
"value": "contractAddress",
"isExpression": false
}
],
"isUnique": true,
"where": null,
"origin": "manual",
"name": "contract_unique",
"entityType": "indexes",
"table": "token_metadata"
}
],
"renames": []
}
@@ -1,41 +0,0 @@
CREATE TABLE `transaction_receipts` (
`transactionHash` text NOT NULL,
`chainId` text NOT NULL,
`blockHash` text NOT NULL,
`blockNumber` text NOT NULL,
`from` text NOT NULL,
`to` text,
`cumulativeGasUsed` text NOT NULL,
`gasUsed` text NOT NULL,
`status` text NOT NULL,
`effectiveGasPrice` text,
`contractAddress` text,
`type` text NOT NULL,
`transactionIndex` integer NOT NULL,
`logsBloom` text
);
--> statement-breakpoint
CREATE TABLE `transactions` (
`hash` text NOT NULL,
`chainId` text NOT NULL,
`from` text NOT NULL,
`to` text,
`value` text NOT NULL,
`blockNumber` text,
`blockHash` text,
`gas` text NOT NULL,
`gasPrice` text,
`maxFeePerGas` text,
`maxPriorityFeePerGas` text,
`input` text NOT NULL,
`nonce` integer NOT NULL,
`transactionIndex` integer,
`type` text,
`v` text,
`r` text,
`s` text,
`yParity` integer
);
--> statement-breakpoint
CREATE UNIQUE INDEX `receipt_unique` ON `transaction_receipts` (`chainId`,`transactionHash`);--> statement-breakpoint
CREATE UNIQUE INDEX `tx_unique` ON `transactions` (`chainId`,`hash`);
@@ -1,467 +0,0 @@
{
"version": "7",
"dialect": "sqlite",
"id": "3271a353-c528-4d23-a2ff-9be22f763b9d",
"prevIds": [
"2845d968-33cd-48b6-8944-03e0e46a4a90"
],
"ddl": [
{
"name": "token_metadata",
"entityType": "tables"
},
{
"name": "transaction_receipts",
"entityType": "tables"
},
{
"name": "transactions",
"entityType": "tables"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "name",
"entityType": "columns",
"table": "token_metadata"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "symbol",
"entityType": "columns",
"table": "token_metadata"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "decimals",
"entityType": "columns",
"table": "token_metadata"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "logo",
"entityType": "columns",
"table": "token_metadata"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "contractAddress",
"entityType": "columns",
"table": "token_metadata"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "chainId",
"entityType": "columns",
"table": "token_metadata"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "transactionHash",
"entityType": "columns",
"table": "transaction_receipts"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "chainId",
"entityType": "columns",
"table": "transaction_receipts"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "blockHash",
"entityType": "columns",
"table": "transaction_receipts"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "blockNumber",
"entityType": "columns",
"table": "transaction_receipts"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "from",
"entityType": "columns",
"table": "transaction_receipts"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "to",
"entityType": "columns",
"table": "transaction_receipts"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "cumulativeGasUsed",
"entityType": "columns",
"table": "transaction_receipts"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "gasUsed",
"entityType": "columns",
"table": "transaction_receipts"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "status",
"entityType": "columns",
"table": "transaction_receipts"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "effectiveGasPrice",
"entityType": "columns",
"table": "transaction_receipts"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "contractAddress",
"entityType": "columns",
"table": "transaction_receipts"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "type",
"entityType": "columns",
"table": "transaction_receipts"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "transactionIndex",
"entityType": "columns",
"table": "transaction_receipts"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "logsBloom",
"entityType": "columns",
"table": "transaction_receipts"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "hash",
"entityType": "columns",
"table": "transactions"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "chainId",
"entityType": "columns",
"table": "transactions"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "from",
"entityType": "columns",
"table": "transactions"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "to",
"entityType": "columns",
"table": "transactions"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "value",
"entityType": "columns",
"table": "transactions"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "blockNumber",
"entityType": "columns",
"table": "transactions"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "blockHash",
"entityType": "columns",
"table": "transactions"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "gas",
"entityType": "columns",
"table": "transactions"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "gasPrice",
"entityType": "columns",
"table": "transactions"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "maxFeePerGas",
"entityType": "columns",
"table": "transactions"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "maxPriorityFeePerGas",
"entityType": "columns",
"table": "transactions"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "input",
"entityType": "columns",
"table": "transactions"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "nonce",
"entityType": "columns",
"table": "transactions"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "transactionIndex",
"entityType": "columns",
"table": "transactions"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "type",
"entityType": "columns",
"table": "transactions"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "v",
"entityType": "columns",
"table": "transactions"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "r",
"entityType": "columns",
"table": "transactions"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "s",
"entityType": "columns",
"table": "transactions"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "yParity",
"entityType": "columns",
"table": "transactions"
},
{
"columns": [
{
"value": "chainId",
"isExpression": false
},
{
"value": "contractAddress",
"isExpression": false
}
],
"isUnique": true,
"where": null,
"origin": "manual",
"name": "contract_unique",
"entityType": "indexes",
"table": "token_metadata"
},
{
"columns": [
{
"value": "chainId",
"isExpression": false
},
{
"value": "transactionHash",
"isExpression": false
}
],
"isUnique": true,
"where": null,
"origin": "manual",
"name": "receipt_unique",
"entityType": "indexes",
"table": "transaction_receipts"
},
{
"columns": [
{
"value": "chainId",
"isExpression": false
},
{
"value": "hash",
"isExpression": false
}
],
"isUnique": true,
"where": null,
"origin": "manual",
"name": "tx_unique",
"entityType": "indexes",
"table": "transactions"
}
],
"renames": []
}
-1
View File
@@ -12,7 +12,6 @@ export default {
'dist/index.html': 'views/mainview/index.html',
'dist/assets': 'views/mainview/assets',
'dist/icons': 'views/mainview/icons',
'drizzle': 'drizzle',
'src/bun/native/biometric-helper': 'native/biometric-helper',
},
// Ignore Vite output in watch mode — HMR handles view rebuilds separately
+2 -7
View File
@@ -11,23 +11,19 @@
"build:canary": "bun run build:native && vite build && electrobun build --env=canary",
"build:stable": "bun run build:native && vite build && electrobun build --env=stable",
"build:win": "vite build && electrobun build --env=stable",
"db:push": "bun x --bun drizzle-kit push",
"db:generate": "drizzle-kit generate",
"build:native": "swiftc -O -o src/bun/native/biometric-helper src/bun/native/biometric-helper.swift"
},
"dependencies": {
"@alchemy/common": "^5.0.0",
"@alchemy/wallet-apis": "^5.0.0",
"@koins/utils": "workspace:*",
"d3-scale": "^4.0.2",
"d3-shape": "^3.2.0",
"drizzle-orm": "^1.0.0-beta.22",
"electrobun": "^1.18.1",
"env-paths": "^4.0.0",
"mode-watcher": "^1.1.0",
"monero-ts": "^0.11.8",
"qrcode": "^1.5.4",
"sv-router": "latest",
"viem": "^2.48.11"
"xrpl": "^5.0.0"
},
"devDependencies": {
"@fontsource-variable/geist-mono": "^5.2.7",
@@ -45,7 +41,6 @@
"bun-types": "^1.3.14",
"clsx": "^2.1.1",
"concurrently": "^9.2.1",
"drizzle-kit": "^1.0.0-beta.22",
"embla-carousel-svelte": "^8.6.0",
"formsnap": "^2.0.1",
"layerchart": "2.0.0-next.48",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

+7 -31
View File
@@ -6,36 +6,21 @@ import {
Updater,
Utils,
} from 'electrobun/bun'
import { migrate } from 'drizzle-orm/bun-sqlite/migrator'
import { db } from './lib/db'
import { join } from 'path'
import { existsSync } from 'fs'
import { getENV } from './lib/get-env'
import { createEvmHandlers } from './rpc/evm'
import { createMoneroHandlers } from './rpc/monero'
import { createXrpHandlers } from './rpc/xrp'
import type { MoneroWalletState } from './lib/monero'
import { canPromptTouchID, promptTouchID } from './lib/biometric'
import { sqlite } from './lib/sqlite'
import { evmWallets } from './lib/db/schema'
import { getXrpWalletList } from './lib/xrp'
import type { RPC } from '../lib/rpc-schema'
import { log, logError } from './lib/logger'
import { log } from './lib/logger'
const env = getENV()
log(`env: ${JSON.stringify(env)}`)
if (env.env === 'stable') {
try {
log('running migrations...')
migrate(db, {
migrationsFolder: join(PATHS.RESOURCES_FOLDER, 'app', 'drizzle'),
})
log('migrations complete')
} catch (e) {
logError('migration failed', e)
}
}
const DEV_SERVER_PORT = 5173
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`
@@ -92,10 +77,6 @@ const moneroState = {
manager: null as MoneroWalletState | null,
downloading: false,
}
const syncState = {
target: null as { address: string; chainid: string } | null,
timer: null as ReturnType<typeof setInterval> | null,
}
const rpc = BrowserView.defineRPC<RPC>({
maxRequestTime: 120000,
@@ -108,11 +89,11 @@ const rpc = BrowserView.defineRPC<RPC>({
rpc.setRequestHandler({
resetApp: async () => {
try {
const wallets = db.select().from(evmWallets).all()
const wallets = await getXrpWalletList()
await Promise.all([
Bun.secrets.delete({
service: 'koins',
name: 'alchemy_key',
name: 'xrp_wallets',
}),
...wallets.flatMap((w) => [
Bun.secrets.delete({
@@ -121,15 +102,10 @@ rpc.setRequestHandler({
}),
Bun.secrets.delete({
service: 'koins',
name: `evm_auth_${w.id}`,
name: `xrp_auth_${w.id}`,
}),
]),
])
sqlite.run('DELETE FROM token_metadata')
sqlite.run('DELETE FROM transactions')
sqlite.run('DELETE FROM transaction_receipts')
sqlite.run('DELETE FROM tx_history')
sqlite.run('DELETE FROM evm_wallets')
console.log('[rpc] resetApp complete')
return true
} catch (e) {
@@ -174,8 +150,8 @@ rpc.setRequestHandler({
logToFile: async ({ message }) => {
log(message)
},
...createEvmHandlers(rpc, syncState),
...createMoneroHandlers(moneroState),
...createXrpHandlers(),
})
log('creating BrowserWindow...')
-31
View File
@@ -1,31 +0,0 @@
import { tryCatch } from '@koins/utils'
const TIMEOUT = 15_000
export const post = async <T>(
url: string,
body: any,
): Promise<{ jsonrpc: '2.0'; id: number; result: T }> => {
const [response, error] = await tryCatch(
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal: AbortSignal.timeout(TIMEOUT),
}),
)
if (error) {
console.log(error)
throw error
}
const [data, dataError] = await tryCatch(response.json())
if (dataError) {
console.log(dataError)
throw dataError
}
return data
}
@@ -1,32 +0,0 @@
import { post } from './call'
import { getChainID, getURL } from './network'
import { inspect } from 'util'
export const getTokensBalances = async (
key: string,
chainid: string,
address: string,
) => {
const chainId = getChainID(chainid)
if (!chainId) return
const url = getURL({
chainId,
key,
})
const balRes = await post<{
address: `0x${string}`
tokenBalances: {
contractAddress: `0x${string}`
tokenBalance: string
}[]
}>(url, {
jsonrpc: '2.0',
id: 0,
method: 'alchemy_getTokenBalances',
params: [address, 'erc20'],
})
return balRes
}
@@ -1,25 +0,0 @@
import { post } from './call'
import { getChainID, getURL } from './network'
export const getTokenMetadata = async (
key: string,
chainid: string,
tokenAddress: string,
) => {
const chainId = getChainID(chainid)
if (!chainId) return
const url = getURL({ chainId, key })
return await post<{
name: string | null
symbol: string | null
decimals: number | null
logo: string | null
}>(url, {
jsonrpc: '2.0',
id: 0,
method: 'alchemy_getTokenMetadata',
params: [tokenAddress],
})
}
@@ -1,104 +0,0 @@
import { post } from './call'
import { getChainID, getURL } from './network'
import type { Transaction } from './types'
import { numberToHex } from 'viem'
type PageResult = {
transfers: Transaction[]
pageKey?: string
}
async function fetchPage(
url: string,
fromAddr: string | undefined,
toAddr: string | undefined,
fromBlock: string | undefined,
chainId: string,
): Promise<{ result?: PageResult }> {
const category = ['external', 'erc20']
if (chainId !== '56') {
category.push('internal')
}
const body = {
jsonrpc: '2.0',
id: 0,
method: 'alchemy_getAssetTransfers',
params: [
{
...(fromAddr ? { fromAddress: fromAddr } : {}),
...(toAddr ? { toAddress: toAddr } : {}),
...(fromBlock ? { fromBlock } : {}),
category,
withMetadata: true,
maxCount: numberToHex(1000),
order: 'asc' as const,
},
],
}
return post<PageResult>(url, body)
}
export const getTransactionHistory = async (
key: string,
chainid: string,
address: string,
opts: { maxCount: number; fromBlock?: string },
) => {
const chainId = getChainID(chainid)
if (!chainId) return
const url = getURL({ chainId, key })
const seen = new Set<string>()
const all: Transaction[] = []
for (const direction of ['out', 'in'] as const) {
const fromAddr = direction === 'out' ? address : undefined
const toAddr = direction === 'in' ? address : undefined
let cursorBlock = opts.fromBlock
for (let page = 0; page < 100; page++) {
const res = await fetchPage(url, fromAddr, toAddr, cursorBlock, chainId)
const transfers = res.result?.transfers ?? []
if (transfers.length === 0) break
let pageMaxBlock = 0
let firstTimestamp = ''
let lastTimestamp = ''
for (const t of transfers) {
if (!t.hash) continue
const dedupKey = `${t.hash}_${t.category}_${t.rawContract?.address ?? ''}`
if (seen.has(dedupKey)) continue
seen.add(dedupKey)
all.push(t)
if (t.blockNum) {
const blockNum = parseInt(t.blockNum, 16)
if (blockNum > pageMaxBlock) {
pageMaxBlock = blockNum
}
}
if (t.metadata?.blockTimestamp) {
if (!firstTimestamp) firstTimestamp = t.metadata.blockTimestamp
lastTimestamp = t.metadata.blockTimestamp
}
}
console.log(
`[alchemy] ${direction} page ${page + 1}: ${transfers.length} transfers, ` +
`block ${pageMaxBlock}, ` +
`${firstTimestamp ? new Date(firstTimestamp).toISOString() : '?'} → ${lastTimestamp ? new Date(lastTimestamp).toISOString() : '?'}`,
)
if (pageMaxBlock === 0) break
cursorBlock = numberToHex(pageMaxBlock + 1)
}
}
console.log(
`[alchemy] total: ${all.length} transfers for ${chainid}/${address.slice(0, 10)}... ` +
`from ${opts.fromBlock ?? 'genesis'}`,
)
return all
}
@@ -1,3 +0,0 @@
export * from './get-token-balances'
export * from './get-token-metadata'
export * from './types'
@@ -1,31 +0,0 @@
export const ALCHEMY_NETWORKS = {
'1': 'eth-mainnet',
'137': 'polygon-mainnet',
'56': 'bnb-mainnet',
} as const
export type ChainID = keyof typeof ALCHEMY_NETWORKS
export const CHAIN_ID = Object.keys(
ALCHEMY_NETWORKS,
) as Array<ChainID>
export const getChainID = (id: string) => {
if (CHAIN_ID.includes(id as ChainID)) {
return id as ChainID
}
return null
}
export const getURL = ({
key,
chainId,
}: {
chainId: ChainID
key: string
}) => {
const network = ALCHEMY_NETWORKS[chainId]
const url = `https://${network}.g.alchemy.com/v2/${key}`
return url
}
-35
View File
@@ -1,35 +0,0 @@
export type Token = {}
export type TokenBalance = {}
export type Transaction = {
category?:
| 'external'
| 'internal'
| 'token'
| 'erc20'
| 'erc721'
| 'erc1155'
| 'specialnft'
blockNum?: string
from?: string
to?: string
value?: number
erc721TokenId?: string
erc1155Metadata?: {
tokenId: string
value: string
}[]
tokenId?: string
asset?: string
uniqueId?: string
hash?: string
rawContract?: {
value?: string
address?: string
decimal?: string
}
metadata?: {
blockTimestamp?: string
}
}
-5
View File
@@ -1,5 +0,0 @@
import { drizzle } from 'drizzle-orm/bun-sqlite'
import { sqlite } from '../sqlite'
import { relations } from './relations'
export const db = drizzle({ client: sqlite, relations })
-4
View File
@@ -1,4 +0,0 @@
import { defineRelations } from 'drizzle-orm'
import * as schema from './schema'
export const relations = defineRelations(schema, () => ({}))
-115
View File
@@ -1,115 +0,0 @@
import { int, sqliteTable, text, uniqueIndex, index } from 'drizzle-orm/sqlite-core'
export const tokenMetadata = sqliteTable(
'token_metadata',
{
name: text(),
symbol: text().notNull(),
decimals: int(),
logo: text(),
contractAddress: text().notNull(),
chainId: text().notNull(),
},
(table) => ({
contractUnique: uniqueIndex('contract_unique').on(
table.chainId,
table.contractAddress,
),
}),
)
export const transactions = sqliteTable(
'transactions',
{
hash: text().notNull(),
chainId: text().notNull(),
from: text().notNull(),
to: text(),
value: text().notNull(),
blockNumber: text(),
blockHash: text(),
gas: text().notNull(),
gasPrice: text(),
maxFeePerGas: text(),
maxPriorityFeePerGas: text(),
input: text().notNull(),
nonce: int().notNull(),
transactionIndex: int(),
type: text(),
v: text(),
r: text(),
s: text(),
yParity: int(),
},
(table) => ({
txUnique: uniqueIndex('tx_unique').on(table.chainId, table.hash),
}),
)
export const transactionReceipts = sqliteTable(
'transaction_receipts',
{
transactionHash: text().notNull(),
chainId: text().notNull(),
blockHash: text().notNull(),
blockNumber: text().notNull(),
from: text().notNull(),
to: text(),
cumulativeGasUsed: text().notNull(),
gasUsed: text().notNull(),
status: text().notNull(),
effectiveGasPrice: text(),
contractAddress: text(),
type: text().notNull(),
transactionIndex: int().notNull(),
logsBloom: text(),
},
(table) => ({
receiptUnique: uniqueIndex('receipt_unique').on(
table.chainId,
table.transactionHash,
),
}),
)
export const evmWallets = sqliteTable('evm_wallets', {
id: text('id').primaryKey(),
name: text('name').notNull(),
passwordHash: text('password_hash'),
vaultKey: text('vault_key').notNull(),
createdAt: text('created_at').notNull(),
})
export const txSyncStatus = sqliteTable('tx_sync_status', {
chainId: text('chain_id').notNull(),
address: text('address').notNull(),
lastSyncBlock: text('last_sync_block').notNull(),
syncedAt: text('synced_at').notNull(),
}, (table) => ({
syncUnique: uniqueIndex('sync_unique').on(table.chainId, table.address),
}))
export const txHistory = sqliteTable(
'tx_history',
{
chainId: text().notNull(),
hash: text().notNull(),
blockNum: text(),
from: text(),
to: text(),
value: text(),
asset: text(),
category: text(),
rawContractAddress: text(),
rawContractDecimal: text(),
metadataBlockTimestamp: text(),
raw: text().notNull(),
syncedAt: text().notNull(),
},
(table) => ({
historyUnique: uniqueIndex('tx_history_unique').on(table.chainId, table.hash, table.category, table.rawContractAddress),
blockIdx: index('tx_history_block_idx').on(table.chainId, table.blockNum),
fromIdx: index('tx_history_from_idx').on(table.from),
toIdx: index('tx_history_to_idx').on(table.to),
}),
)
-16
View File
@@ -1,16 +0,0 @@
import { Database } from 'bun:sqlite'
import envPaths from 'env-paths'
import { join } from 'path'
import { mkdirSync } from 'fs'
import { getENV } from '../get-env'
const env = getENV()
const { data } = envPaths(env.appName, { suffix: '' })
mkdirSync(data, { recursive: true })
console.log(data)
export const sqlite = new Database(join(data, env.dbFilename), {
create: true,
})
-1
View File
@@ -1 +0,0 @@
export { syncTransactionHistory } from './sync-tx-history'
@@ -1,76 +0,0 @@
import { getClient, getTransactionDetails } from '../viem'
import { db } from '../db'
import { transactions, transactionReceipts } from '../db/schema'
import { getChainID } from '../alchemy/network'
export const syncTxDetails = async (
chainId: string,
apiKey: string,
hash: string,
) => {
const client = getClient(chainId, apiKey)
const id = getChainID(chainId)
if (!id) return null
const raw = await getTransactionDetails(client, hash)
if (!raw) return null
const hashLower = hash.toLowerCase()
try {
await db
.insert(transactions)
.values({
hash: hashLower,
chainId: id,
from: raw.tx.from.toLowerCase(),
to: raw.tx.to?.toLowerCase() ?? null,
value: raw.tx.value.toString(),
blockNumber: raw.tx.blockNumber?.toString() ?? null,
blockHash: raw.tx.blockHash?.toLowerCase() ?? null,
gas: raw.tx.gas.toString(),
gasPrice: raw.tx.gasPrice?.toString() ?? null,
maxFeePerGas: raw.tx.maxFeePerGas?.toString() ?? null,
maxPriorityFeePerGas:
raw.tx.maxPriorityFeePerGas?.toString() ?? null,
input: raw.tx.input,
nonce: raw.tx.nonce,
transactionIndex: raw.tx.transactionIndex ?? null,
type: raw.tx.type ?? null,
v: raw.tx.v?.toString() ?? null,
r: raw.tx.r?.toLowerCase() ?? null,
s: raw.tx.s?.toLowerCase() ?? null,
yParity: raw.tx.yParity ?? null,
})
.onConflictDoNothing()
} catch (e) {
console.log('Failed to cache transaction', e)
}
try {
await db
.insert(transactionReceipts)
.values({
transactionHash: hashLower,
chainId: id,
blockHash: raw.receipt.blockHash.toLowerCase(),
blockNumber: raw.receipt.blockNumber.toString(),
from: raw.receipt.from.toLowerCase(),
to: raw.receipt.to?.toLowerCase() ?? null,
cumulativeGasUsed: raw.receipt.cumulativeGasUsed.toString(),
gasUsed: raw.receipt.gasUsed.toString(),
status: raw.receipt.status,
effectiveGasPrice:
raw.receipt.effectiveGasPrice?.toString() ?? null,
contractAddress:
raw.receipt.contractAddress?.toLowerCase() ?? null,
type: raw.receipt.type,
transactionIndex: raw.receipt.transactionIndex,
logsBloom: raw.receipt.logsBloom?.toLowerCase() ?? null,
})
.onConflictDoNothing()
} catch (e) {
console.log('Failed to cache transaction receipt', e)
}
}
@@ -1,149 +0,0 @@
import { eq, and } from 'drizzle-orm'
import { numberToHex } from 'viem'
import { db } from '../db'
import { txHistory, txSyncStatus } from '../db/schema'
import { getTransactionHistory } from '../alchemy/get-transaction-history'
import { syncTxDetails } from './sync-tx-details'
const PAGE_SIZE = 76
export const syncTransactionHistory = async (
key: string,
chainid: string,
address: string,
): Promise<number> => {
const addrLower = address.toLowerCase()
const existing = db
.select()
.from(txSyncStatus)
.where(
and(
eq(txSyncStatus.chainId, chainid),
eq(txSyncStatus.address, addrLower),
),
)
.get()
const fromBlock = existing?.lastSyncBlock
? numberToHex(Number(existing.lastSyncBlock) + 1)
: undefined
const transfers = await getTransactionHistory(
key,
chainid,
address,
{
maxCount: 1000,
fromBlock,
},
)
if (!transfers || transfers.length === 0) return 0
const prepared: Array<typeof txHistory.$inferInsert> = []
let latestBlock = Number(existing?.lastSyncBlock ?? '0')
for (const t of transfers) {
if (!t.hash) continue
if (t.blockNum) {
const blockNum = parseInt(t.blockNum, 16)
if (blockNum > latestBlock) latestBlock = blockNum
}
prepared.push({
chainId: chainid,
hash: t.hash,
blockNum: t.blockNum ? String(parseInt(t.blockNum, 16)) : null,
from: t.from?.toLowerCase() ?? null,
to: t.to?.toLowerCase() ?? null,
value: t.value?.toString() ?? null,
asset: t.asset ?? null,
category: t.category ?? null,
rawContractAddress:
t.rawContract?.address?.toLowerCase() ?? null,
rawContractDecimal: t.rawContract?.decimal ?? null,
metadataBlockTimestamp: t.metadata?.blockTimestamp ?? null,
raw: JSON.stringify(t),
syncedAt: new Date().toISOString(),
})
await syncTxDetails(chainid, key, t.hash)
}
flush(prepared)
db.insert(txSyncStatus)
.values({
chainId: chainid,
address: addrLower,
lastSyncBlock: String(latestBlock),
syncedAt: new Date().toISOString(),
})
.onConflictDoUpdate({
target: [txSyncStatus.chainId, txSyncStatus.address],
set: {
lastSyncBlock: String(latestBlock),
syncedAt: new Date().toISOString(),
},
})
.run()
console.log(
`[sync] ${chainid}/${addrLower}: synced ${prepared.length} transfers up to block ${latestBlock}`,
)
return prepared.length
}
function flush(prepared: Array<typeof txHistory.$inferInsert>) {
while (prepared.length > 0) {
const batch = prepared.splice(0, PAGE_SIZE)
const cols = [
'chainId',
'hash',
'blockNum',
'"from"',
'"to"',
'value',
'asset',
'category',
'rawContractAddress',
'rawContractDecimal',
'metadataBlockTimestamp',
'raw',
'syncedAt',
]
const colKeys = [
'chainId',
'hash',
'blockNum',
'from',
'to',
'value',
'asset',
'category',
'rawContractAddress',
'rawContractDecimal',
'metadataBlockTimestamp',
'raw',
'syncedAt',
]
const placeholders = batch
.map(() => `(${colKeys.map(() => '?').join(',')})`)
.join(',')
const flat = batch.flatMap((r) =>
colKeys.map((c) => (r as any)[c] ?? null),
)
const sql = `INSERT OR IGNORE INTO tx_history (${cols.join(',')}) VALUES ${placeholders}`
const dbClient = db.$client
dbClient.exec('BEGIN')
try {
dbClient.run(sql, ...flat)
dbClient.exec('COMMIT')
} catch (e) {
dbClient.exec('ROLLBACK')
console.error('[sync] flush error:', e)
}
}
}
@@ -1,64 +0,0 @@
import { eq, and } from 'drizzle-orm'
import { db } from '../db'
import { tokenMetadata } from '../db/schema'
import { getChainID } from '../alchemy/network'
import { getTokenMetadata as getTokenMetadataFromAlchemy } from '../alchemy/get-token-metadata'
export const getTokenMetadata = async (
key: string,
chainid: string,
tokenAddress: string,
) => {
const chainId = getChainID(chainid)
if (!chainId) return
const cached = db
.select()
.from(tokenMetadata)
.where(
and(
eq(tokenMetadata.chainId, chainId),
eq(tokenMetadata.contractAddress, tokenAddress.toLowerCase()),
),
)
.get()
if (cached) {
return {
jsonrpc: '2.0' as const,
id: 0,
result: {
name: cached.name ?? null,
symbol: cached.symbol,
decimals: cached.decimals ?? null,
logo: cached.logo ?? null,
},
}
}
const result = await getTokenMetadataFromAlchemy(
key,
chainid,
tokenAddress,
)
if (result?.result?.symbol) {
try {
await db
.insert(tokenMetadata)
.values({
name: result.result.name,
symbol: result.result.symbol,
decimals: result.result.decimals,
logo: result.result.logo,
contractAddress: tokenAddress.toLowerCase(),
chainId,
})
.onConflictDoNothing()
} catch (e) {
console.log('Failed to cache token metadata', e)
}
}
return result
}
-1
View File
@@ -1 +0,0 @@
export { getTokenMetadata } from './get-metadata'
@@ -1,66 +0,0 @@
import { eq, and } from 'drizzle-orm'
import { formatEther, formatGwei, type PublicClient } from 'viem'
import { db } from '../db'
import { transactions, transactionReceipts } from '../db/schema'
import { getChainID } from '../alchemy/network'
export const getTransactionDetails = async (
hash: string,
chainId: string,
) => {
if (!hash.startsWith('0x')) return null
const id = getChainID(chainId)
if (!id) return null
const hashLower = hash.toLowerCase()
const cachedTx = db
.select()
.from(transactions)
.where(
and(
eq(transactions.hash, hashLower),
eq(transactions.chainId, id),
),
)
.get()
const cachedReceipt = db
.select()
.from(transactionReceipts)
.where(
and(
eq(transactionReceipts.transactionHash, hashLower),
eq(transactionReceipts.chainId, id),
),
)
.get()
if (cachedTx && cachedReceipt) {
const value = BigInt(cachedTx.value)
const gasUsed = BigInt(cachedReceipt.gasUsed)
const gasPrice = cachedTx.gasPrice
? BigInt(cachedTx.gasPrice)
: BigInt(0)
return {
hash: cachedTx.hash,
from: cachedTx.from,
to: cachedTx.to,
value: formatEther(value),
blockNumber: cachedReceipt.blockNumber,
fee: (
parseFloat(formatGwei(gasUsed)) *
parseFloat(formatGwei(gasPrice))
).toString(),
gasPrice: formatGwei(gasPrice),
status: cachedReceipt.status as 'success' | 'reverted',
type: cachedTx.type ?? 'legacy',
nonce: cachedTx.nonce,
input: cachedTx.input,
}
}
return null
}
@@ -1,63 +0,0 @@
import { eq, and, or, desc } from 'drizzle-orm'
import { db } from '../db'
import { txHistory } from '../db/schema'
import type { Transaction } from '../alchemy/types'
export const getTransactionHistory = async (
key: string,
chainid: string,
address: string,
opts: { maxCount: number },
) => {
return getCachedTransactionHistory(chainid, address)
}
export const getCachedTransactionHistory = (
chainid: string,
address: string,
) => {
const addrLower = address.toLowerCase()
const rows = db
.select()
.from(txHistory)
.where(
and(
eq(txHistory.chainId, chainid),
or(
eq(txHistory.from, addrLower),
eq(txHistory.to, addrLower),
),
),
)
.orderBy(desc(txHistory.blockNum))
.all()
return rows.map(toTransfer)
}
function toTransfer(row: typeof txHistory.$inferSelect): Transaction {
if (row.raw) {
return JSON.parse(row.raw) as Transaction
}
return {
hash: row.hash,
blockNum: row.blockNum
? `0x${parseInt(row.blockNum).toString(16)}`
: undefined,
from: row.from ?? undefined,
to: row.to ?? undefined,
value: row.value ? Number(row.value) : undefined,
asset: row.asset ?? undefined,
category: row.category as Transaction['category'],
rawContract:
row.rawContractAddress || row.rawContractDecimal
? {
address: row.rawContractAddress ?? undefined,
decimal: row.rawContractDecimal ?? undefined,
}
: undefined,
metadata: row.metadataBlockTimestamp
? { blockTimestamp: row.metadataBlockTimestamp }
: undefined,
}
}
@@ -1,5 +0,0 @@
export {
getTransactionHistory,
getCachedTransactionHistory,
} from './get-history'
export { getTransactionDetails } from './get-details'
@@ -1,50 +0,0 @@
import { isHex, type PublicClient } from 'viem'
export const getTransactionDetails = async (
client: PublicClient,
hash: string,
) => {
if (!isHex(hash)) return null
const [tx, receipt] = await Promise.all([
client.getTransaction({ hash }),
client.getTransactionReceipt({ hash }),
])
return {
tx: {
hash: tx.hash,
from: tx.from,
to: tx.to ?? null,
value: tx.value,
blockNumber: tx.blockNumber ?? null,
gasPrice: tx.gasPrice ?? null,
gas: tx.gas,
input: tx.input,
nonce: tx.nonce,
type: tx.type ?? 'legacy',
blockHash: tx.blockHash ?? null,
maxFeePerGas: tx.maxFeePerGas ?? null,
maxPriorityFeePerGas: tx.maxPriorityFeePerGas ?? null,
transactionIndex: tx.transactionIndex ?? null,
v: tx.v ?? null,
r: tx.r?.toString() ?? null,
s: tx.s?.toString() ?? null,
yParity: tx.yParity ?? null,
},
receipt: {
blockHash: receipt.blockHash,
blockNumber: receipt.blockNumber,
from: receipt.from,
to: receipt.to ?? null,
cumulativeGasUsed: receipt.cumulativeGasUsed,
gasUsed: receipt.gasUsed,
status: receipt.status,
effectiveGasPrice: receipt.effectiveGasPrice ?? null,
contractAddress: receipt.contractAddress ?? null,
type: receipt.type,
transactionIndex: receipt.transactionIndex,
logsBloom: receipt.logsBloom?.toString() ?? null,
},
}
}
-12
View File
@@ -1,12 +0,0 @@
import { bsc, mainnet, polygon, type Chain } from 'viem/chains'
import type { ChainID } from '../alchemy/network'
const chains: Record<ChainID, Chain> = {
'1': mainnet,
'56': bsc,
'137': polygon,
}
export const getChain = (id: ChainID) => {
return chains[id]
}
@@ -1,19 +0,0 @@
import { createPublicClient, http } from 'viem'
import { getChainID, getURL } from '../alchemy/network'
import { getChain } from './getChain'
export const getClient = (chainId: string, apiKey: string) => {
const id = getChainID(chainId)
if (!id) {
throw new Error()
}
const url = getURL({ chainId: id, key: apiKey })
const publicClient = createPublicClient({
chain: getChain(id),
transport: http(url),
})
return publicClient
}
-2
View File
@@ -1,2 +0,0 @@
export * from './getClient'
export { getTransactionDetails } from './get-transaction-details'
+188
View File
@@ -0,0 +1,188 @@
import {
Client,
Wallet,
xrpToDrops,
dropsToXrp,
isValidClassicAddress,
} from 'xrpl'
import type {
XrpBalance,
XrpTxDetails,
XrpTxEntry,
XrpWalletInfo,
} from '../../../lib/rpc-schema'
const SERVER = 'wss://s1.ripple.com'
const RIPPLE_EPOCH = 946684800
const WALLET_LIST_KEY = 'xrp_wallets'
const SERVICE = 'koins'
let client: Client | null = null
export async function getClient(): Promise<Client> {
if (client?.isConnected()) return client
client = new Client(SERVER)
await client.connect()
return client
}
export function isValidAddress(address: string): boolean {
return isValidClassicAddress(address)
}
export function generateWallet(): { address: string; seed: string } {
const wallet = Wallet.generate()
return { address: wallet.classicAddress, seed: wallet.seed! }
}
export function walletFromSecret(secret: string): { address: string } {
const s = secret.trim()
try {
return { address: Wallet.fromSeed(s).classicAddress }
} catch {
return { address: Wallet.fromMnemonic(s).classicAddress }
}
}
export function walletForSigning(secret: string): Wallet {
const s = secret.trim()
try {
return Wallet.fromSeed(s)
} catch {
return Wallet.fromMnemonic(s)
}
}
export async function getXrpWalletList(): Promise<XrpWalletInfo[]> {
const raw = await Bun.secrets.get({ service: SERVICE, name: WALLET_LIST_KEY })
if (!raw) return []
try {
return JSON.parse(raw)
} catch {
return []
}
}
export async function saveXrpWalletList(
wallets: XrpWalletInfo[],
): Promise<void> {
await Bun.secrets.set({
service: SERVICE,
name: WALLET_LIST_KEY,
value: JSON.stringify(wallets),
})
}
export async function getBalance(address: string): Promise<XrpBalance> {
const c = await getClient()
try {
const balance = await c.getXrpBalance(address)
return { address, balance: String(balance), funded: true }
} catch (e) {
if (e instanceof Error && e.message.includes('Account not found')) {
return { address, balance: '0', funded: false }
}
throw e
}
}
function dropsAmount(value: unknown): string {
if (typeof value !== 'string') return '0'
return String(dropsToXrp(value))
}
export async function getTransactions(address: string): Promise<XrpTxEntry[]> {
const c = await getClient()
const res = await c.request({
command: 'account_tx',
account: address,
ledger_index_min: -1,
ledger_index_max: -1,
limit: 25,
})
const entries: XrpTxEntry[] = []
for (const entry of res.result.transactions as any[]) {
const tx = entry.tx_json ?? entry.tx
if (!tx || tx.TransactionType !== 'Payment') continue
const meta = entry.meta ?? entry.metaData ?? {}
if (meta.TransactionResult && meta.TransactionResult !== 'tesSUCCESS')
continue
const delivered = meta.delivered_amount ?? meta.DeliveredAmount ?? tx.Amount
entries.push({
hash: entry.hash ?? tx.hash,
amount: dropsAmount(delivered),
timestamp: tx.date ? String(tx.date + RIPPLE_EPOCH) : '0',
direction: tx.Destination === address ? 'in' : 'out',
from: tx.Account,
to: tx.Destination,
destinationTag: tx.DestinationTag,
fee: dropsAmount(tx.Fee),
confirmed: entry.validated === true,
})
}
return entries
}
export async function getTxDetails(
hash: string,
): Promise<XrpTxDetails | null> {
const c = await getClient()
try {
const res = await c.request({ command: 'tx', transaction: hash })
const d = res.result as any
const tx = d.tx_json ?? d
if (tx.TransactionType !== 'Payment') return null
const meta = d.meta ?? {}
const delivered = meta.delivered_amount ?? meta.DeliveredAmount ?? tx.Amount
return {
hash: d.hash ?? hash,
from: tx.Account,
to: tx.Destination,
amount: dropsAmount(delivered),
fee: dropsAmount(tx.Fee),
timestamp: tx.date ? String(tx.date + RIPPLE_EPOCH) : '0',
ledgerIndex: d.ledger_index ?? 0,
destinationTag: tx.DestinationTag,
confirmed: d.validated === true,
}
} catch {
return null
}
}
export async function sendPayment(
secret: string,
to: string,
amount: string,
destinationTag?: number,
): Promise<{ hash: string; fee: string }> {
if (!isValidClassicAddress(to)) throw new Error('Invalid XRP address')
const wallet = walletForSigning(secret)
const c = await getClient()
const prepared = await c.autofill({
TransactionType: 'Payment',
Account: wallet.classicAddress,
Amount: xrpToDrops(amount),
Destination: to,
...(destinationTag !== undefined ? { DestinationTag: destinationTag } : {}),
})
const signed = wallet.sign(prepared)
const result = await c.submit(signed.tx_blob)
const engineResult = (result.result as any).engine_result
if (engineResult !== 'tesSUCCESS') {
const msg =
(result.result as any).engine_result_message ?? 'Transaction failed'
throw new Error(`${engineResult}: ${msg}`)
}
return { hash: signed.hash, fee: String(dropsToXrp((prepared as any).Fee ?? '0')) }
}
export async function getFee(): Promise<{ fee: string } | null> {
try {
const c = await getClient()
const res = await c.request({ command: 'fee' })
return { fee: String(dropsToXrp(res.result.drops.open_ledger_fee)) }
} catch {
return null
}
}
-641
View File
@@ -1,641 +0,0 @@
import { getTokenMetadata } from '../../lib/tokens'
import { getTokensBalances } from '../../lib/alchemy'
import { tryCatch } from '@koins/utils'
import {
getTransactionHistory,
getTransactionDetails,
getCachedTransactionHistory,
} from '../../lib/transactions'
import { syncTransactionHistory } from '../../lib/sync'
import { eq, and } from 'drizzle-orm'
import { db } from '../../lib/db'
import { getClient } from '../../lib/viem'
import {
formatGwei,
parseEther,
parseUnits,
encodeFunctionData,
createWalletClient,
} from 'viem'
import { mnemonicToAccount } from 'viem/accounts'
import { getChain } from '../../lib/viem/getChain'
import { getChainID } from '../../lib/alchemy/network'
import { alchemyTransport } from '@alchemy/common'
import { sqlite } from '../../lib/sqlite'
import { txHistory, evmWallets } from '../../lib/db/schema'
import type {
TokenBalanceResult,
TokenPriceEntry,
TxEntry,
} from '../../../lib/rpc-schema'
export function createEvmHandlers(
rpc: {
send: {
transfersUpdate: (p: {
count: number
chainid: string
address: string
}) => void
}
},
syncState: {
target: { address: string; chainid: string } | null
timer: ReturnType<typeof setInterval> | null
},
) {
const ALCHEMY_NETWORKS: Record<string, string> = {
'1': 'eth-mainnet',
'137': 'polygon-mainnet',
'56': 'bnb-mainnet',
}
const NATIVE_SYMBOLS: Record<string, string> = {
'1': 'ETH',
'137': 'POL',
'56': 'BNB',
}
async function fetchAlchemyTransfers(
key: string,
chainid: string,
address: string,
) {
const transactions = await getTransactionHistory(
key,
chainid,
address,
{ maxCount: 10 },
)
if (!transactions) return []
return transactions
}
function formatUnits(balance: string, decimals: number): string {
const bal = BigInt(balance)
if (bal === 0n) return '0'
const divisor = 10n ** BigInt(decimals)
const intPart = bal / divisor
const fracPart = (bal % divisor)
.toString()
.padStart(decimals, '0')
.replace(/0+$/, '')
return fracPart ? `${intPart}.${fracPart}` : `${intPart}`
}
async function fetchAlchemyTokenBalances(
key: string,
chainid: string,
address: string,
): Promise<TokenBalanceResult[]> {
const [balRes, balResError] = await tryCatch(
getTokensBalances(key, chainid, address),
)
if (balResError) {
console.log('Get Tokens Balance Error', balResError)
return []
}
if (!balRes) return []
const tokens = balRes.result?.tokenBalances ?? []
const nonZero = tokens.filter(
(t: any) => t.tokenBalance !== '0x0',
)
const metas = await Promise.allSettled(
nonZero.map((t: any) =>
getTokenMetadata(key, chainid, t.contractAddress),
),
)
if (!metas) return []
return nonZero
.map((t: any, i: number) => {
const meta =
metas[i].status === 'fulfilled' && metas[i].value
? metas[i].value.result
: null
return {
symbol: meta?.symbol ?? 'Unknown',
decimals: meta?.decimals ?? 0,
balance: formatUnits(t.tokenBalance, meta?.decimals ?? 18),
contractAddress: t.contractAddress,
logo: meta?.logo ?? undefined,
}
})
.filter((t) => t.logo && t.decimals !== 0)
}
function mapTransfer(t: any): TxEntry {
const isExternal = t.category === 'external'
return {
hash: t.hash,
timeStamp: String(
Math.floor(
new Date(t.metadata?.blockTimestamp).getTime() / 1000,
),
),
from: t.from,
to: t.to,
value: String(t.value),
...(isExternal
? {}
: {
tokenSymbol: t.asset,
tokenDecimal: t.rawContract?.decimal
? String(parseInt(t.rawContract.decimal, 16))
: undefined,
contractAddress: t.rawContract?.address ?? undefined,
}),
}
}
async function pairTransfers(
transfers: any[],
address: string,
key?: string,
chainid?: string,
): Promise<TxEntry[]> {
const addrLower = address.toLowerCase()
const byHash = new Map<string, any[]>()
for (const t of transfers) {
if (!t.hash) continue
const h = t.hash
if (!byHash.has(h)) byHash.set(h, [])
byHash.get(h)!.push(t)
}
const combined: TxEntry[] = []
for (const [, group] of byHash) {
const native = group.filter(
(t: any) =>
t.category === 'external' || t.category === 'internal',
)
const tokens = group.filter((t: any) => t.category === 'erc20')
const extOut = native.filter(
(t: any) => t.from?.toLowerCase() === addrLower,
)
const extIn = native.filter(
(t: any) => t.to?.toLowerCase() === addrLower,
)
const tokIn = tokens.filter(
(t: any) => t.to?.toLowerCase() === addrLower,
)
const tokOut = tokens.filter(
(t: any) => t.from?.toLowerCase() === addrLower,
)
if (extOut.length > 0 && tokIn.length > 0) {
combined.push({
...mapTransfer(extOut[0]),
tokenSymbol: tokIn[0].asset,
tokenDecimal: tokIn[0].rawContract?.decimal
? String(parseInt(tokIn[0].rawContract.decimal, 16))
: undefined,
contractAddress: tokIn[0].rawContract?.address ?? undefined,
pairedValue: String(tokIn[0].value),
pairedSymbol: tokIn[0].asset,
pairedDecimals: tokIn[0].rawContract?.decimal
? String(parseInt(tokIn[0].rawContract.decimal, 16))
: undefined,
pairedContractAddress:
tokIn[0].rawContract?.address ?? undefined,
})
} else if (extOut.length > 0) {
combined.push(mapTransfer(extOut[0]))
} else if (extIn.length > 0) {
combined.push(mapTransfer(extIn[0]))
} else if (tokOut.length > 0 && tokIn.length > 0) {
combined.push({
...mapTransfer(tokOut[0]),
pairedValue: String(tokIn[0].value),
pairedSymbol: tokIn[0].asset,
pairedDecimals: tokIn[0].rawContract?.decimal
? String(parseInt(tokIn[0].rawContract.decimal, 16))
: undefined,
pairedContractAddress:
tokIn[0].rawContract?.address ?? undefined,
})
} else if (tokOut.length > 0 && extIn.length > 0) {
combined.push({
...mapTransfer(tokOut[0]),
pairedValue: String(extIn[0].value),
pairedSymbol: chainid ? NATIVE_SYMBOLS[chainid] : undefined,
})
} else if (tokOut.length > 0) {
combined.push(mapTransfer(tokOut[0]))
} else if (tokIn.length > 0) {
combined.push(mapTransfer(tokIn[0]))
}
}
if (key && chainid) {
const addrSet = new Set<string>()
for (const entry of combined) {
if (entry.contractAddress)
addrSet.add(entry.contractAddress.toLowerCase())
if (entry.pairedContractAddress)
addrSet.add(entry.pairedContractAddress.toLowerCase())
}
const logoMap = new Map<string, string>()
await Promise.all(
Array.from(addrSet).map(async (addr) => {
const meta = await getTokenMetadata(key, chainid, addr)
if (meta?.result?.logo) logoMap.set(addr, meta.result.logo)
}),
)
for (const entry of combined) {
if (entry.contractAddress)
entry.logo = logoMap.get(
entry.contractAddress.toLowerCase(),
)
if (entry.pairedContractAddress)
entry.pairedLogo = logoMap.get(
entry.pairedContractAddress.toLowerCase(),
)
}
}
combined.sort((a, b) => Number(b.timeStamp) - Number(a.timeStamp))
return combined
}
return {
fetchTxHistory: async ({
address,
chainid,
}: {
address: string
chainid: string
}) => {
const key = await Bun.secrets.get({
service: 'koins',
name: 'alchemy_key',
})
if (!key) return []
const [all, allError] = await tryCatch(
fetchAlchemyTransfers(key, chainid, address),
)
if (allError) {
console.log(allError)
return []
}
return pairTransfers(all, address, key, chainid)
},
fetchCachedTxHistory: async ({
address,
chainid,
}: {
address: string
chainid: string
}) => {
const transfers = await getCachedTransactionHistory(
chainid,
address,
)
return pairTransfers(transfers, address)
},
syncTxHistory: async ({
address,
chainid,
}: {
address: string
chainid: string
}) => {
const key = await Bun.secrets.get({
service: 'koins',
name: 'alchemy_key',
})
if (!key) return
try {
const count = await syncTransactionHistory(
key,
chainid,
address,
)
rpc.send.transfersUpdate({ count, chainid, address })
} catch (e) {
console.error('[sync] error:', e)
}
},
setAutoSync: async (
target: { address: string; chainid: string } | null,
) => {
if (syncState.timer) clearInterval(syncState.timer)
syncState.timer = null
syncState.target = null
if (!target) return
syncState.target = target
syncState.timer = setInterval(async () => {
if (!syncState.target) return
const key = await Bun.secrets.get({
service: 'koins',
name: 'alchemy_key',
})
if (!key) return
try {
const count = await syncTransactionHistory(
key,
syncState.target.chainid,
syncState.target.address,
)
rpc.send.transfersUpdate({
count,
chainid: syncState.target.chainid,
address: syncState.target.address,
})
} catch (e) {
console.error('[auto-sync] error:', e)
}
}, 30000)
},
flushTxCache: async () => {
sqlite.run('DELETE FROM tx_history')
sqlite.run('DELETE FROM tx_sync_status')
console.log('[rpc] flushTxCache complete')
},
fetchTokenBalances: async ({
address,
chainid,
}: {
address: string
chainid: string
}) => {
const key = await Bun.secrets.get({
service: 'koins',
name: 'alchemy_key',
})
if (!key) return []
try {
return await fetchAlchemyTokenBalances(key, chainid, address)
} catch (error) {
console.log(error)
return []
}
},
fetchTokenPrices: async ({
symbols,
addresses,
}: {
symbols?: string[]
addresses?: { network: string; address: string }[]
}) => {
const key = await Bun.secrets.get({
service: 'koins',
name: 'alchemy_key',
})
if (!key) return []
try {
const results: TokenPriceEntry[] = []
if (symbols && symbols.length > 0) {
const url = `https://api.g.alchemy.com/prices/v1/${key}/tokens/by-symbol?${symbols.map((s) => `symbols=${encodeURIComponent(s)}`).join('&')}`
const res = await fetch(url, {
signal: AbortSignal.timeout(10000),
})
if (res.ok) {
const body = await res.json()
for (const d of body.data ?? []) {
for (const p of d.prices ?? []) {
results.push({
symbol: d.symbol,
currency: p.currency,
value: p.value,
lastUpdatedAt: p.lastUpdatedAt,
})
}
}
}
}
if (addresses && addresses.length > 0) {
const url = `https://api.g.alchemy.com/prices/v1/${key}/tokens/by-address`
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ addresses }),
signal: AbortSignal.timeout(10000),
})
if (res.ok) {
const body = await res.json()
for (const d of body.data ?? []) {
for (const p of d.prices ?? []) {
results.push({
symbol: d.symbol,
currency: p.currency,
value: p.value,
lastUpdatedAt: p.lastUpdatedAt,
network: d.network,
address: d.address,
})
}
}
}
}
console.log('[prices] results:', results.length)
return results
} catch (error) {
console.log('[prices] error:', error)
return []
}
},
fetchGasPrice: async ({ chainid }: { chainid: string }) => {
const key = await Bun.secrets.get({
service: 'koins',
name: 'alchemy_key',
})
if (!key) {
console.log('[gas] no key')
return null
}
try {
const client = getClient(chainid, key)
const gasPrice = await client.getGasPrice()
console.log(
'[gas] price for chain',
chainid,
':',
gasPrice.toString(),
)
return formatGwei(gasPrice)
} catch (error) {
console.log('[gas] error:', error)
return null
}
},
fetchTransactionDetails: async ({
hash,
chainid,
address,
}: {
hash: string
chainid: string
address?: string
}) => {
const key = await Bun.secrets.get({
service: 'koins',
name: 'alchemy_key',
})
if (!key) return null
try {
const details = await getTransactionDetails(hash, chainid)
if (!details) return null
if (address) {
const transfers = db
.select()
.from(txHistory)
.where(
and(
eq(txHistory.chainId, chainid),
eq(txHistory.hash, hash.toLowerCase()),
),
)
.all()
if (transfers.length > 0) {
const paired = await pairTransfers(transfers, address)
if (paired.length > 0 && paired[0].pairedValue) {
details.pairedValue = paired[0].pairedValue
details.pairedSymbol = paired[0].pairedSymbol
details.pairedDecimals = paired[0].pairedDecimals
details.pairedContractAddress =
paired[0].pairedContractAddress
details.pairedLogo = paired[0].pairedLogo
}
}
}
return details
} catch (error) {
console.log(error)
return null
}
},
evmCreateWallet: async ({
name,
phrase,
passwordHash,
}: {
name: string
phrase: string
passwordHash?: string
}) => {
console.log('[rpc] evmCreateWallet:', name)
const id = crypto.randomUUID()
const vaultKey = `evm_seed_${id}`
await Bun.secrets.set({
service: 'koins',
name: vaultKey,
value: phrase,
})
await db.insert(evmWallets).values({
id,
name,
passwordHash: passwordHash ?? null,
vaultKey,
createdAt: new Date().toISOString(),
})
console.log('[rpc] evmCreateWallet complete:', id)
return { id, name, createdAt: new Date().toISOString() }
},
evmListWallets: async () => {
console.log('[rpc] evmListWallets')
const wallets = db.select().from(evmWallets).all()
return wallets.map((w: any) => ({
id: w.id,
name: w.name,
hasPassword: !!w.passwordHash,
vaultKey: w.vaultKey,
createdAt: w.createdAt,
}))
},
evmGetSeed: async ({ vaultKey }: { vaultKey: string }) => {
console.log('[rpc] evmGetSeed:', vaultKey)
const seed = await Bun.secrets.get({
service: 'koins',
name: vaultKey,
})
if (!seed) throw new Error('Seed not found in keychain')
return seed
},
evmDeleteWallet: async ({ id }: { id: string }) => {
console.log('[rpc] evmDeleteWallet:', id)
const wallet = db
.select()
.from(evmWallets)
.where(eq(evmWallets.id, id))
.get()
if (!wallet) throw new Error('Wallet not found')
await Promise.all([
Bun.secrets.delete({
service: 'koins',
name: wallet.vaultKey,
}),
Bun.secrets.delete({
service: 'koins',
name: `evm_auth_${id}`,
}),
])
db.delete(evmWallets).where(eq(evmWallets.id, id)).run()
console.log('[rpc] evmDeleteWallet complete')
},
evmSendTransfer: async ({
seed,
to,
amount,
chainid,
contractAddress,
tokenDecimals,
}: {
seed: string
to: string
amount: string
chainid: string
contractAddress?: string
tokenDecimals?: number
}) => {
const key = await Bun.secrets.get({
service: 'koins',
name: 'alchemy_key',
})
if (!key) throw new Error('API key not set')
const _amount = String(amount)
const account = mnemonicToAccount(seed)
const id = getChainID(chainid)
if (!id) throw new Error('Unsupported chain')
const chain = getChain(id)
const walletClient = createWalletClient({
account,
chain,
transport: alchemyTransport({ apiKey: key }),
})
if (contractAddress && tokenDecimals !== undefined) {
const erc20Abi = [
{
name: 'transfer',
type: 'function',
inputs: [
{ name: 'to', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
outputs: [{ name: '', type: 'bool' }],
},
] as const
const data = encodeFunctionData({
abi: erc20Abi,
functionName: 'transfer',
args: [
to as `0x${string}`,
parseUnits(_amount, tokenDecimals),
],
})
const hash = await walletClient.sendTransaction({
to: contractAddress as `0x${string}`,
data,
value: 0n,
})
return hash
}
const hash = await walletClient.sendTransaction({
to: to as `0x${string}`,
value: parseEther(_amount),
})
return hash
},
}
}
+153
View File
@@ -0,0 +1,153 @@
import {
generateWallet,
walletFromSecret,
getXrpWalletList,
saveXrpWalletList,
getBalance,
getTransactions,
getTxDetails,
sendPayment,
getFee,
} from '../../lib/xrp'
import type { XrpWalletInfo } from '../../../lib/rpc-schema'
const SERVICE = 'koins'
export function createXrpHandlers() {
return {
xrpListWallets: async () => {
console.log('[rpc] xrpListWallets')
return getXrpWalletList()
},
xrpCreateWallet: async ({
name,
hasPassword,
}: {
name: string
hasPassword?: boolean
}) => {
console.log('[rpc] xrpCreateWallet:', name)
const { address, seed } = generateWallet()
const id = crypto.randomUUID()
const vaultKey = `xrp_seed_${id}`
await Bun.secrets.set({ service: SERVICE, name: vaultKey, value: seed })
const wallets = await getXrpWalletList()
const createdAt = new Date().toISOString()
const info: XrpWalletInfo = {
id,
name,
address,
hasPassword: hasPassword ?? false,
vaultKey,
createdAt,
}
await saveXrpWalletList([...wallets, info])
console.log('[rpc] xrpCreateWallet complete:', id)
return { id, address, seed, createdAt }
},
xrpImportWallet: async ({
name,
secret,
hasPassword,
}: {
name: string
secret: string
hasPassword?: boolean
}) => {
console.log('[rpc] xrpImportWallet:', name)
const { address } = walletFromSecret(secret)
const id = crypto.randomUUID()
const vaultKey = `xrp_seed_${id}`
await Bun.secrets.set({
service: SERVICE,
name: vaultKey,
value: secret.trim(),
})
const wallets = await getXrpWalletList()
const createdAt = new Date().toISOString()
const info: XrpWalletInfo = {
id,
name,
address,
hasPassword: hasPassword ?? false,
vaultKey,
createdAt,
}
await saveXrpWalletList([...wallets, info])
console.log('[rpc] xrpImportWallet complete:', id)
return { id, address, createdAt }
},
xrpDeleteWallet: async ({ id }: { id: string }) => {
console.log('[rpc] xrpDeleteWallet:', id)
const wallets = await getXrpWalletList()
const wallet = wallets.find((w) => w.id === id)
if (!wallet) throw new Error('Wallet not found')
await Promise.all([
Bun.secrets.delete({ service: SERVICE, name: wallet.vaultKey }),
Bun.secrets.delete({ service: SERVICE, name: `xrp_auth_${id}` }),
])
await saveXrpWalletList(wallets.filter((w) => w.id !== id))
console.log('[rpc] xrpDeleteWallet complete')
},
xrpGetSeed: async ({ vaultKey }: { vaultKey: string }) => {
console.log('[rpc] xrpGetSeed:', vaultKey)
const seed = await Bun.secrets.get({ service: SERVICE, name: vaultKey })
if (!seed) throw new Error('Seed not found in keychain')
return seed
},
xrpGetBalance: async ({ address }: { address: string }) => {
console.log('[rpc] xrpGetBalance:', address)
return getBalance(address)
},
xrpGetTransactions: async ({ address }: { address: string }) => {
console.log('[rpc] xrpGetTransactions:', address)
try {
return await getTransactions(address)
} catch (e) {
if (e instanceof Error && e.message.includes('Account not found')) {
return []
}
throw e
}
},
xrpGetTxDetails: async ({ hash }: { hash: string }) => {
console.log('[rpc] xrpGetTxDetails:', hash)
return getTxDetails(hash)
},
xrpSend: async ({
secret,
to,
amount,
destinationTag,
}: {
secret: string
to: string
amount: string
destinationTag?: number
}) => {
console.log('[rpc] xrpSend:', { to, amount, destinationTag })
const result = await sendPayment(secret, to, amount, destinationTag)
console.log('[rpc] xrpSend complete:', result.hash)
return result
},
xrpGetFee: async () => {
console.log('[rpc] xrpGetFee')
return getFee()
},
fetchXrpPrice: async () => {
try {
const res = await fetch(
'https://api.coingecko.com/api/v3/simple/price?ids=ripple&vs_currencies=usd',
{ signal: AbortSignal.timeout(10000) },
)
if (!res.ok) return null
const data = await res.json()
if (!data?.ripple?.usd) return null
return { usd: String(data.ripple.usd) }
} catch (error) {
console.log('[xrp][price] error:', error)
return null
}
},
}
}
@@ -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}
+5 -4
View File
@@ -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>({
+53 -75
View File
@@ -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: {}
}
}
@@ -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()
+48 -61
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { evmWallet as wallet } from '$lib/states/evm-wallet.svelte.js'
import { moneroWallet } from '$lib/states/monero-wallet.svelte.js'
import { xrpWallet } from '$lib/states/xrp-wallet.svelte.js'
import { Button } from '$lib/components/ui/button/index.js'
import {
Card,
@@ -13,14 +13,20 @@
import { onMount } from 'svelte'
import { frontendLog, frontendLogError } from '$lib/electrobun.js'
const w = wallet
let feError = $state<string | null>(null)
onMount(() => {
frontendLog('index.svelte mounted')
w.init().catch((e) => frontendLogError('w.init() failed', e))
moneroWallet.init().catch((e) => frontendLogError('moneroWallet.init() failed', e))
xrpWallet.init().catch((e) => frontendLogError('xrpWallet.init() failed', e))
})
$effect(() => {
if (moneroWallet.accountType === 'monero') {
navigate('/monero')
} else if (xrpWallet.accountType === 'xrp') {
navigate('/xrp')
}
})
$effect(() => {
@@ -56,61 +62,42 @@
{/if}
<div class="mx-auto mt-24 max-w-sm">
{#if !w.ready}
<p class="text-center text-muted-foreground text-sm">
Loading...
</p>
{:else if w.accountType}
{#if w.accountType === 'multi'}
{navigate('/multicoin')}
{:else}
{navigate('/monero')}
{/if}
{:else}
<Card>
<CardHeader class="text-center">
<CardTitle>Welcome</CardTitle>
<CardDescription>
Choose an account type to get started
</CardDescription>
</CardHeader>
<CardContent>
<div class="flex flex-col gap-3">
<Button
onclick={async () => {
await w.login()
navigate('/multicoin')
}}
class="w-full">
<span class="-ml-1 mr-2 flex items-center">
<img
src="/icons/ethereum.png"
alt=""
class="relative z-30 size-5 rounded-full ring-2 ring-accent" />
<img
src="/icons/binance.png"
alt=""
class="-ml-1.5 relative z-20 size-5 rounded-full ring-2 ring-accent" />
<img
src="/icons/polygon.png"
alt=""
class="-ml-1.5 relative z-10 size-5 rounded-full ring-2 ring-accent" />
</span>
</Button>
<Button
onclick={async () => {
await moneroWallet.login()
navigate('/monero')
}}
variant="outline"
class="w-full">
<img
src="/icons/monero.png"
alt=""
class="-ml-1 mr-1.5 inline-block size-5 rounded-full align-middle" />
</Button>
</div>
</CardContent>
</Card>
{/if}
<Card>
<CardHeader class="text-center">
<CardTitle>Welcome</CardTitle>
<CardDescription>
Choose an account type to get started
</CardDescription>
</CardHeader>
<CardContent>
<div class="flex flex-col gap-3">
<Button
onclick={async () => {
await moneroWallet.login()
navigate('/monero')
}}
variant="outline"
class="w-full">
<img
src="/icons/monero.png"
alt=""
class="-ml-1 mr-1.5 inline-block size-5 rounded-full align-middle" />
Monero
</Button>
<Button
onclick={async () => {
await xrpWallet.login()
navigate('/xrp')
}}
variant="outline"
class="w-full">
<img
src="/icons/xrp.png"
alt=""
class="-ml-1 mr-1.5 inline-block size-5 rounded-full align-middle" />
XRP
</Button>
</div>
</CardContent>
</Card>
</div>
@@ -1,7 +0,0 @@
<script lang="ts">
import EvmDashboard from '$lib/components/EvmDashboard.svelte'
</script>
<div class="mx-auto mt-16 max-w-md">
<EvmDashboard networkId="bsc" />
</div>
@@ -1,7 +0,0 @@
<script lang="ts">
import EvmDashboard from '$lib/components/EvmDashboard.svelte'
</script>
<div class="mx-auto mt-16 max-w-md">
<EvmDashboard networkId="eth" />
</div>
@@ -1,7 +0,0 @@
<script lang="ts">
import EvmDashboard from '$lib/components/EvmDashboard.svelte'
</script>
<div class="mx-auto mt-16 max-w-md">
<EvmDashboard networkId="polygon" />
</div>
@@ -1,220 +0,0 @@
<script lang="ts">
import { evmWallet as wallet } from '$lib/states/evm-wallet.svelte.js'
import { electrobun } from '$lib/electrobun.js'
import { Button } from '$lib/components/ui/button/index.js'
import { Input } from '$lib/components/ui/input/index.js'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from '$lib/components/ui/select/index.js'
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
} from '$lib/components/ui/card/index.js'
import { navigate } from 'sv-router/generated'
import { parseEther, formatEther, parseGwei } from 'viem'
const w = wallet
type TokenOption = {
symbol: string
balance: string
contractAddress: string
decimals: number
logo?: string
}
const tokens = $derived<TokenOption[]>([
{
symbol: w.symbol,
balance: w.balance,
contractAddress: '',
decimals: 18,
},
...w.tokenBalances.map((t) => ({
symbol: t.symbol,
balance: t.balance,
contractAddress: t.contractAddress,
decimals: t.decimals,
logo: t.logo,
})),
])
let selectedSymbol = $state(w.symbol)
let recipient = $state('')
let amount = $state('')
let sending = $state(false)
let sentResult = $state<{ txHash: string } | null>(null)
let sendError = $state('')
let sendAll = $state(false)
const selectedToken = $derived(
tokens.find((t) => t.symbol === selectedSymbol) ?? tokens[0],
)
const exceedsBalance = $derived(
amount !== '' &&
selectedToken.balance !== '0' &&
parseFloat(amount) > parseFloat(selectedToken.balance),
)
const canSend = $derived(
recipient.length === 42 &&
recipient.startsWith('0x') &&
!sending &&
(sendAll || (amount !== '' && parseFloat(amount) > 0)),
)
const handleSend = async () => {
sending = true
sendError = ''
sentResult = null
try {
const isNative = !selectedToken.contractAddress
let finalAmount = String(amount)
if (sendAll && isNative) {
const gasPriceGwei = await electrobun.rpc?.request.fetchGasPrice({ chainid: w.chainid })
if (!gasPriceGwei) throw new Error('Could not fetch gas price')
const gasWei = parseGwei(gasPriceGwei) * 21000n
const balanceWei = parseEther(selectedToken.balance)
const sendWei = balanceWei - gasWei
if (sendWei <= 0n) throw new Error('Balance too low to cover gas')
finalAmount = formatEther(sendWei)
} else if (sendAll) {
finalAmount = String(selectedToken.balance)
}
const txHash = await w.send(
recipient,
finalAmount,
selectedToken.contractAddress || undefined,
selectedToken.decimals,
)
sentResult = { txHash }
recipient = ''
amount = ''
sendAll = false
} catch (e) {
sendError = e instanceof Error ? e.message : 'Send failed'
} finally {
sending = false
}
}
</script>
<div class="mx-auto mt-16 max-w-md">
<Button
variant="outline"
size="sm"
onclick={() => navigate(w.network === 'eth' ? '/multicoin' : `/multicoin/${w.network}`)}
class="mb-4">
&larr; Back
</Button>
<Card>
<CardHeader>
<CardTitle>Send</CardTitle>
<CardDescription>
Balance: {selectedToken.balance} {selectedToken.symbol}
</CardDescription>
</CardHeader>
<CardContent>
<form
onsubmit={(e) => {
e.preventDefault()
handleSend()
}}
class="space-y-4">
<div class="space-y-1.5">
<label class="text-xs font-medium">Asset</label>
<Select type="single" bind:value={selectedSymbol}>
<SelectTrigger>
{selectedToken.symbol}
</SelectTrigger>
<SelectContent>
{#each tokens as t}
<SelectItem value={t.symbol}>
{t.symbol} ({t.balance})
</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
<div class="space-y-1.5">
<label class="text-xs font-medium">Recipient Address</label>
<Input
placeholder="0x..."
bind:value={recipient} />
</div>
<div class="space-y-1.5">
<label class="text-xs font-medium">Amount ({selectedToken.symbol})</label>
<div class="flex gap-2 items-start">
<div class="flex-1">
<Input
type="number"
step="0.000000000000000001"
min="0"
placeholder="0.0"
bind:value={amount} />
</div>
<Button
variant="outline"
onclick={() => { amount = selectedToken.balance }}
class="mt-0 shrink-0">
All
</Button>
</div>
{#if exceedsBalance}
<p class="text-xs text-red-500">
Amount exceeds balance
</p>
{/if}
</div>
{#if !selectedToken.contractAddress}
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
bind:checked={sendAll}
class="h-3.5 w-3.5" />
<span class="text-xs text-muted-foreground">
Send all (deducts estimated gas fee)
</span>
</label>
{/if}
<Button type="submit" disabled={!canSend} class="w-full">
{sending ? 'Sending...' : 'Send'}
</Button>
</form>
{#if sendError}
<p class="mt-3 text-xs text-red-500">{sendError}</p>
{/if}
{#if sentResult}
<div
class="mt-4 space-y-2 rounded-md border border-green-500/30 bg-green-500/5 p-3">
<p class="text-xs font-medium text-green-600">
Transaction Sent
</p>
<p class="font-mono text-xs break-all">
<a
href="{w.explorerUrl}{sentResult.txHash}"
target="_blank"
rel="noopener noreferrer"
class="underline hover:text-green-500">
{sentResult.txHash}
</a>
</p>
</div>
{/if}
</CardContent>
</Card>
</div>
+17 -89
View File
@@ -1,8 +1,6 @@
<script lang="ts">
import { evmWallet as wallet } from '$lib/states/evm-wallet.svelte.js'
import { electrobun } from '$lib/electrobun.js'
import { xrpWallet } from '$lib/states/xrp-wallet.svelte.js'
import { Button } from '$lib/components/ui/button/index.js'
import { Input } from '$lib/components/ui/input/index.js'
import {
Card,
CardContent,
@@ -12,19 +10,13 @@
} from '$lib/components/ui/card/index.js'
import { navigate } from 'sv-router/generated'
import ArrowLeft from '@lucide/svelte/icons/arrow-left'
import Eye from '@lucide/svelte/icons/eye'
import Fingerprint from '@lucide/svelte/icons/fingerprint'
import Trash2 from '@lucide/svelte/icons/trash-2'
import WalletIcon from '@lucide/svelte/icons/wallet'
const w = wallet
const w = xrpWallet
let confirm = $state(false)
let resetting = $state(false)
let deletingWalletId = $state<string | null>(null)
let flushing = $state(false)
let seedRevealed = $state(false)
let seedPassword = $state('')
let seedError = $state('')
async function handleReset() {
resetting = true
@@ -37,20 +29,22 @@
async function handleDeleteWallet(id: string) {
await w.deleteWallet(id)
deletingWalletId = null
if (w.wallets.length === 0) navigate('/multicoin')
}
async function handleFlushCache() {
flushing = true
await w.flushTxCache()
flushing = false
if (w.wallets.length === 0) navigate('/xrp')
}
</script>
<div class="mx-auto mt-16 max-w-md">
<div class="flex flex-col gap-4">
<div class="flex items-center gap-2">
<Button variant="outline" size="sm" onclick={() => navigate(w.network === 'eth' ? '/multicoin' : `/multicoin/${w.network}`)}>
<Button
variant="outline"
size="sm"
onclick={() =>
w.currentWalletId
? navigate('/xrp/wallet/:id', {
params: { id: w.currentWalletId },
})
: navigate('/xrp')}>
<ArrowLeft size={16} />
Back
</Button>
@@ -59,7 +53,7 @@
<Card>
<CardHeader>
<CardTitle>Wallets</CardTitle>
<CardDescription>Manage your EVM wallets</CardDescription>
<CardDescription>Manage your XRP wallets</CardDescription>
</CardHeader>
<CardContent>
<div class="flex flex-col gap-2">
@@ -68,8 +62,8 @@
<WalletIcon size={18} 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">
Created {new Date(wal.createdAt).toLocaleDateString()}
<p class="text-xs text-muted-foreground truncate font-mono">
{wal.address}
</p>
</div>
{#if deletingWalletId === wal.id}
@@ -92,83 +86,17 @@
</CardContent>
</Card>
{/if}
{#if w.seed}
<Card>
<CardHeader>
<CardTitle>Seed Phrase</CardTitle>
<CardDescription>Reveal your wallet recovery phrase</CardDescription>
</CardHeader>
<CardContent>
{#if seedRevealed}
<div class="rounded-md border border-destructive/50 bg-destructive/5 p-3">
<p class="text-xs text-destructive font-medium mb-2">
Never share your seed phrase. Anyone with it can access your funds.
</p>
<p class="font-mono text-xs break-all">{w.seed}</p>
</div>
<Button variant="outline" size="sm" class="mt-3" onclick={() => { seedRevealed = false; seedPassword = ''; seedError = '' }}>
<Eye size={14} /> Hide
</Button>
{:else}
<div class="space-y-2">
{#if w.currentPasswordHash}
<div class="flex gap-2 items-end">
<Input type="password" placeholder="Enter password" bind:value={seedPassword} />
<Button onclick={async () => {
seedError = ''
const { salt, hash } = JSON.parse(w.currentPasswordHash!)
const { hash: check } = await w.hashPassword(seedPassword, salt)
if (check === hash) {
seedRevealed = true
} else {
seedError = 'Incorrect password'
}
seedPassword = ''
}}>Reveal</Button>
</div>
{/if}
{#if w.biometricAvailable}
<Button variant="outline" class="w-full" onclick={async () => {
seedError = ''
const ok = await electrobun.rpc?.request.biometricAuth({ reason: 'Reveal seed phrase' })
if (ok) seedRevealed = true
}}>
<Fingerprint size={14} /> Reveal with Touch ID
</Button>
{/if}
{#if !w.currentPasswordHash && !w.biometricAvailable}
<Button onclick={() => (seedRevealed = true)}>Reveal Seed Phrase</Button>
{/if}
{#if seedError}
<p class="text-xs text-destructive">{seedError}</p>
{/if}
</div>
{/if}
</CardContent>
</Card>
{/if}
<Card>
<CardHeader>
<CardTitle>Settings</CardTitle>
<CardDescription>App preferences and data management</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<div>
<p class="font-medium text-xs">Transaction Cache</p>
<p class="text-muted-foreground text-xs mt-1">
Clears all cached transaction history and sync status. Data will be re-fetched from Alchemy on next refresh.
</p>
<div class="mt-3">
<Button variant="outline" onclick={handleFlushCache} disabled={flushing}>
{flushing ? 'Flushing...' : 'Flush Transaction Cache'}
</Button>
</div>
</div>
<div>
<p class="font-medium text-xs">Reset App</p>
<p class="text-muted-foreground text-xs mt-1">
Removes your seed, API key, and all cached data from the keychain
and database. You can then set up the wallet fresh.
Removes all XRP wallets and seeds from the keychain. You
can then set up the wallet fresh.
</p>
<div class="mt-3">
{#if confirm}
@@ -1,175 +0,0 @@
<script lang="ts">
import {
electrobun,
type TransactionDetails,
} from '$lib/electrobun.js'
import { evmWallet } from '$lib/states/evm-wallet.svelte.js'
import { networks } from '$lib/states/evm-wallet.svelte.js'
import { navigate, route } from 'sv-router/generated'
import { Button } from '$lib/components/ui/button/index.js'
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '$lib/components/ui/card/index.js'
let details = $state<TransactionDetails | null>(null)
let loading = $state(true)
let error = $state('')
const chainid = $derived(route.params.chainid)
const hash = $derived(route.params.hash)
const network = $derived(
networks.find((n) => n.chainid === chainid),
)
$effect(() => {
if (!hash || !chainid) return
loading = true
error = ''
electrobun.rpc?.request
.fetchTransactionDetails({
hash,
chainid,
address: evmWallet.address || undefined,
})
.then((res) => {
details = res
loading = false
})
.catch((e) => {
error =
e instanceof Error ? e.message : 'Failed to fetch details'
loading = false
})
})
function explorerUrl() {
if (!network) return ''
return network.explorerUrl + hash
}
</script>
<div class="mx-auto mt-16 max-w-md">
<Button
variant="outline"
size="sm"
onclick={() => navigate('/multicoin')}
class="mb-4">
&larr; Back
</Button>
<Card>
<CardHeader>
<div class="flex items-center justify-between">
<CardTitle>Transaction Details</CardTitle>
{#if network}
<span
class="rounded-md bg-muted px-2 py-0.5 text-xs font-medium">
{network.name}
</span>
{/if}
</div>
</CardHeader>
<CardContent>
{#if loading}
<p class="text-muted-foreground text-xs">Loading...</p>
{:else if error}
<p class="text-red-500 text-xs">{error}</p>
{:else if details}
<div class="space-y-2 font-mono text-xs">
<div class="rounded-md bg-muted p-2">
<p class="text-muted-foreground mb-0.5">Hash</p>
<p class="break-all">{details.hash}</p>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="rounded-md bg-muted p-2">
<p class="text-muted-foreground mb-0.5">Status</p>
<p
class={details.status === 'success'
? 'text-green-500'
: 'text-red-500'}>
{details.status}
</p>
</div>
<div class="rounded-md bg-muted p-2">
<p class="text-muted-foreground mb-0.5">Block</p>
<p>{details.blockNumber ?? 'N/A'}</p>
</div>
</div>
<div class="rounded-md bg-muted p-2">
<p class="text-muted-foreground mb-0.5">From</p>
<p class="break-all">{details.from}</p>
</div>
<div class="rounded-md bg-muted p-2">
<p class="text-muted-foreground mb-0.5">To</p>
<p class="break-all">{details.to ?? 'N/A'}</p>
</div>
<div class="rounded-md bg-muted p-2">
<p class="text-muted-foreground mb-0.5">
Value ({network?.symbol ?? 'ETH'})
</p>
<p>{details.value}</p>
</div>
{#if details.pairedValue}
<div
class="rounded-md border border-yellow-500/30 bg-yellow-500/5 p-2">
<p class="mb-0.5 flex items-center gap-1 text-xs font-medium text-yellow-600">
Swap
</p>
<p class="font-mono text-xs">
{Number(details.value).toFixed(4)}
{network?.symbol ?? 'ETH'}
<span class="mx-1">→</span>
{Number(details.pairedValue).toFixed(4)}
{details.pairedSymbol ?? network?.symbol ?? 'ETH'}
</p>
</div>
{/if}
<div class="grid grid-cols-2 gap-2">
<div class="rounded-md bg-muted p-2">
<p class="text-muted-foreground mb-0.5">
Tx Fee ({network?.symbol ?? 'ETH'})
</p>
<p>{details.fee}</p>
</div>
<div class="rounded-md bg-muted p-2">
<p class="text-muted-foreground mb-0.5">
Gas Price (Gwei)
</p>
<p>{details.gasPrice}</p>
</div>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="rounded-md bg-muted p-2">
<p class="text-muted-foreground mb-0.5">Nonce</p>
<p>{details.nonce}</p>
</div>
<div class="rounded-md bg-muted p-2">
<p class="text-muted-foreground mb-0.5">Type</p>
<p>{details.type}</p>
</div>
</div>
</div>
<div class="mt-4">
<Button
variant="outline"
size="sm"
class="w-full"
onclick={() =>
electrobun.rpc?.request.openExternal({
url: explorerUrl(),
})}>
View on Explorer
</Button>
</div>
{:else}
<p class="text-muted-foreground text-xs">
No details available
</p>
{/if}
</CardContent>
</Card>
</div>
+273
View File
@@ -0,0 +1,273 @@
<script lang="ts">
import { xrpWallet } from '$lib/states/xrp-wallet.svelte.js'
import { Button } from '$lib/components/ui/button/index.js'
import { Input } from '$lib/components/ui/input/index.js'
import { Textarea } from '$lib/components/ui/textarea/index.js'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '$lib/components/ui/card/index.js'
import Fingerprint from '@lucide/svelte/icons/fingerprint'
import LockKeyhole from '@lucide/svelte/icons/lock-keyhole'
import Plus from '@lucide/svelte/icons/plus'
import WalletIcon from '@lucide/svelte/icons/wallet'
import Loader from '$lib/components/loader.svelte'
import { navigate } from 'sv-router/generated'
const w = xrpWallet
let inputWalletName = $state('')
let inputSeed = $state('')
let inputPassword = $state('')
let inputUnlockPassword = $state('')
let showCreateForm = $state(false)
let newSeed = $state<string | null>(null)
let initStarted = false
$effect(() => {
if (!initStarted) {
initStarted = true
w.init().then(async () => {
if (w.accountType === 'xrp') return
await w.login()
})
}
})
$effect(() => {
if (w.accountType === 'xrp' && w.currentWalletId && w.seed && !newSeed) {
navigate('/xrp/wallet/:id', {
params: { id: w.currentWalletId },
})
}
})
const handleCreate = async () => {
const seed = await w.createWallet(
inputWalletName,
inputPassword || undefined,
)
newSeed = seed ?? null
}
const handleImport = async () => {
await w.importWallet(
inputWalletName,
inputSeed.trim(),
inputPassword || undefined,
)
}
</script>
<div class="mx-auto mt-16 max-w-md">
<div class="flex flex-col gap-4">
{#if !w.ready}
<p class="text-center text-muted-foreground text-sm mt-8">
Loading...
</p>
{:else if !w.accountType}
<p class="text-center text-muted-foreground text-sm mt-8">
<a href="/" class="underline">Go to welcome page</a>
to get started
</p>
{:else}
<a
href="/"
class="text-xs text-muted-foreground underline mb-1 inline-block">
← Back to home
</a>
{#if newSeed}
<Card>
<CardHeader>
<CardTitle>Wallet Created</CardTitle>
<CardDescription>
Save this seed phrase — it is the only way to recover
your wallet
</CardDescription>
</CardHeader>
<CardContent>
<div class="rounded-md border border-destructive/50 bg-destructive/5 p-3 mb-3">
<p class="text-xs text-destructive font-medium mb-2">
Never share your seed. Anyone with it can access your
funds.
</p>
<p class="font-mono text-xs break-all">{newSeed}</p>
</div>
<p class="text-xs text-muted-foreground mb-3">
Your XRP address becomes active after it receives at
least 10 XRP (the base reserve).
</p>
<Button
class="w-full"
onclick={() => {
newSeed = null
inputWalletName = ''
inputPassword = ''
}}>
I saved my seed
</Button>
</CardContent>
</Card>
{:else if w.wallets.length === 0 || showCreateForm}
<Card>
<CardHeader>
<CardTitle>
{showCreateForm ? 'Add Another Wallet' : 'Create / Import Wallet'}
</CardTitle>
<CardDescription>
Generate a new XRP wallet or import an existing seed
</CardDescription>
</CardHeader>
<CardContent>
<div class="flex flex-col gap-3">
<Input
placeholder="Wallet name (e.g. Main, Savings)"
bind:value={inputWalletName} />
<Button
onclick={handleCreate}
disabled={w.loading || !inputWalletName.trim()}>
{w.loading ? 'Creating...' : 'Generate New Wallet'}
</Button>
<hr class="border-muted" />
<p class="text-xs text-muted-foreground">
Or import an existing wallet:
</p>
<Textarea
placeholder="XRP seed (s...) or 12/24-word phrase"
bind:value={inputSeed} />
<Input
type="password"
placeholder="Set a password (optional)"
bind:value={inputPassword} />
<div class="flex gap-2">
<Button
variant="outline"
onclick={handleImport}
disabled={w.loading ||
!inputSeed.trim() ||
!inputWalletName.trim()}>
{w.loading ? 'Importing...' : 'Import Wallet'}
</Button>
{#if showCreateForm}
<Button
variant="outline"
onclick={() => (showCreateForm = false)}>
Cancel
</Button>
{/if}
</div>
</div>
{#if w.error}
<p class="mt-3 text-xs 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 truncate font-mono">
{wal.address}
</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"
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">
{#if w.loading}
<Loader />
{:else}
Unlock
{/if}
</Button>
</div>
{/if}
<Button
variant="outline"
onclick={() => w.clearSelection()}>
Pick another wallet
</Button>
</div>
{#if w.error}
<p class="mt-3 text-xs text-red-500">{w.error}</p>
{/if}
</CardContent>
</Card>
{/if}
{/if}
</div>
</div>
+165
View File
@@ -0,0 +1,165 @@
<script lang="ts">
import { xrpWallet } from '$lib/states/xrp-wallet.svelte.js'
import { Button } from '$lib/components/ui/button/index.js'
import { Input } from '$lib/components/ui/input/index.js'
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
} from '$lib/components/ui/card/index.js'
import { navigate } from 'sv-router/generated'
const w = xrpWallet
let recipient = $state('')
let destinationTag = $state('')
let amountXrp = $state('')
let sending = $state(false)
let sentResult = $state<{ hash: string; fee: string } | null>(null)
let sendError = $state('')
const isValidAddress = $derived(/^r[1-9A-HJ-NP-Za-km-z]{24,34}$/.test(recipient))
const amount = $derived(parseFloat(amountXrp))
const amountValid = $derived(!isNaN(amount) && amount > 0)
const exceedsBalance = $derived(amountValid && amount > parseFloat(w.balance))
const tagValid = $derived(
destinationTag === '' ||
(/^\d+$/.test(destinationTag) && Number(destinationTag) <= 4294967295),
)
const canSend = $derived(
isValidAddress &&
amountValid &&
!exceedsBalance &&
tagValid &&
!sending,
)
const handleSend = async () => {
sending = true
sendError = ''
sentResult = null
try {
const result = await w.send(
recipient,
amount.toString(),
destinationTag === '' ? undefined : Number(destinationTag),
)
sentResult = result
recipient = ''
destinationTag = ''
amountXrp = ''
} catch (e) {
sendError = e instanceof Error ? e.message : 'Send failed'
} finally {
sending = false
}
}
</script>
<div class="mx-auto mt-16 max-w-md">
<Button
variant="outline"
size="sm"
onclick={() =>
navigate('/xrp/wallet/:id', {
params: { id: w.currentWalletId ?? '' },
})}
class="mb-4">
&larr; Back
</Button>
<Card>
<CardHeader>
<CardTitle>Send XRP</CardTitle>
<CardDescription>
Balance: {w.balance} XRP
{#if w.fee}
(fee: ~{w.fee} XRP)
{/if}
</CardDescription>
</CardHeader>
<CardContent>
<form
onsubmit={(e) => {
e.preventDefault()
handleSend()
}}
class="space-y-4">
<div class="space-y-1.5">
<label class="text-xs font-medium">Recipient Address</label>
<Input
placeholder="XRP address starting with r..."
bind:value={recipient} />
{#if recipient && !isValidAddress}
<p class="text-xs text-red-500">Invalid XRP address</p>
{/if}
</div>
<div class="space-y-1.5">
<label class="text-xs font-medium">
Destination Tag (optional)
</label>
<Input
type="number"
min="0"
step="1"
placeholder="Required by some exchanges"
bind:value={destinationTag} />
{#if !tagValid}
<p class="text-xs text-red-500">
Destination tag must be a number between 0 and
4294967295
</p>
{/if}
</div>
<div class="space-y-1.5">
<label class="text-xs font-medium">Amount (XRP)</label>
<div class="relative">
<Input
type="number"
step="0.000001"
min="0"
placeholder="0.0"
bind:value={amountXrp} />
<span
class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-muted-foreground">
XRP
</span>
</div>
{#if exceedsBalance}
<p class="text-xs text-red-500">
Amount exceeds balance
</p>
{/if}
</div>
<Button type="submit" disabled={!canSend} class="w-full">
{sending ? 'Sending...' : 'Send'}
</Button>
</form>
{#if sendError}
<p class="mt-3 text-xs text-red-500">{sendError}</p>
{/if}
{#if sentResult}
<div
class="mt-4 space-y-2 rounded-md border border-green-500/30 bg-green-500/5 p-3">
<p class="text-xs font-medium text-green-600">
Transaction Sent
</p>
<p class="font-mono text-xs break-all">
Hash: {sentResult.hash}
</p>
<p class="font-mono text-xs">
Fee: {sentResult.fee} XRP
</p>
</div>
{/if}
</CardContent>
</Card>
</div>
@@ -0,0 +1,142 @@
<script lang="ts">
import { electrobun, type XrpTxDetails } from '$lib/electrobun.js'
import { xrpWallet, xrpNetwork } from '$lib/states/xrp-wallet.svelte.js'
import { navigate, route } from 'sv-router/generated'
import { Button } from '$lib/components/ui/button/index.js'
import {
Card,
CardContent,
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'
let details = $state<XrpTxDetails | null>(null)
let loading = $state(true)
let error = $state('')
const hash = $derived(route.params.hash)
$effect(() => {
if (!hash) return
loading = true
error = ''
electrobun.rpc?.request
.xrpGetTxDetails({ hash })
.then((res) => {
details = res
loading = false
})
.catch((e) => {
error = e instanceof Error ? e.message : 'Failed to fetch details'
loading = false
})
})
</script>
<div class="mx-auto mt-16 max-w-md">
<Button
variant="outline"
size="sm"
onclick={() =>
navigate('/xrp/wallet/:id', {
params: { id: xrpWallet.currentWalletId ?? '' },
})}
class="mb-4">
&larr; Back
</Button>
<Card>
<CardHeader>
<CardTitle>Transaction Details</CardTitle>
</CardHeader>
<CardContent>
{#if loading}
<p class="text-muted-foreground text-xs">Loading...</p>
{:else if error}
<p class="text-red-500 text-xs">{error}</p>
{:else if details}
<div class="space-y-2 font-mono text-xs">
<div class="flex items-center gap-2 rounded-md bg-muted p-2">
<div class="shrink-0 {details.to === xrpWallet.address ? 'text-green-500' : 'text-muted-foreground'}">
{#if details.to === xrpWallet.address}
<ArrowDown size={16} />
{:else}
<ArrowUp size={16} />
{/if}
</div>
<p class="font-medium">
{details.to === xrpWallet.address ? 'Received' : 'Sent'}
</p>
</div>
<div class="rounded-md bg-muted p-2">
<p class="text-muted-foreground mb-0.5">Transaction Hash</p>
<p class="break-all">{details.hash}</p>
</div>
<div class="rounded-md bg-muted p-2">
<p class="text-muted-foreground mb-0.5">From</p>
<p class="break-all">{details.from}</p>
</div>
<div class="rounded-md bg-muted p-2">
<p class="text-muted-foreground mb-0.5">To</p>
<p class="break-all">{details.to}</p>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="rounded-md bg-muted p-2">
<p class="text-muted-foreground mb-0.5">Amount</p>
<p>{details.amount} XRP</p>
</div>
<div class="rounded-md bg-muted p-2">
<p class="text-muted-foreground mb-0.5">Fee</p>
<p>{details.fee} XRP</p>
</div>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="rounded-md bg-muted p-2">
<p class="text-muted-foreground mb-0.5">Ledger</p>
<p>{details.ledgerIndex.toLocaleString()}</p>
</div>
<div class="rounded-md bg-muted p-2">
<p class="text-muted-foreground mb-0.5">Validated</p>
<p>{details.confirmed ? 'Yes' : 'No'}</p>
</div>
</div>
{#if details.destinationTag !== undefined}
<div class="rounded-md bg-muted p-2">
<p class="text-muted-foreground mb-0.5">Destination Tag</p>
<p>{details.destinationTag}</p>
</div>
{/if}
{#if details.timestamp && details.timestamp !== '0'}
<div class="rounded-md bg-muted p-2">
<p class="text-muted-foreground mb-0.5">Date</p>
<p>{new Date(Number(details.timestamp) * 1000).toLocaleString()}</p>
</div>
{/if}
</div>
<div class="mt-4">
<Button
variant="outline"
size="sm"
class="w-full"
onclick={() =>
electrobun.rpc?.request.openExternal({
url: `${xrpNetwork.explorerUrl}${details?.hash}`,
})}>
View on Explorer
</Button>
</div>
{:else}
<p class="text-muted-foreground text-xs">No details available</p>
{/if}
</CardContent>
</Card>
</div>
@@ -0,0 +1,235 @@
<script lang="ts">
import { xrpWallet, xrpNetwork } from '$lib/states/xrp-wallet.svelte.js'
import { electrobun } from '$lib/electrobun.js'
import { Button } from '$lib/components/ui/button/index.js'
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 SettingsIcon from '@lucide/svelte/icons/settings'
import QrCodeIcon from '@lucide/svelte/icons/qr-code'
import ExternalLink from '@lucide/svelte/icons/external-link'
import Loader from '$lib/components/loader.svelte'
import QrCode from '$lib/components/qr.svelte'
import * as Dialog from '$lib/components/ui/dialog/index.js'
import { CopyButton } from '$lib/components/ui/copy-button'
import { navigate, route } from 'sv-router/generated'
import { onMount } from 'svelte'
const w = xrpWallet
const walletId = $derived(route.params.id)
let qrDialogOpen = $state(false)
let autoRefreshTimer: ReturnType<typeof setInterval> | undefined =
$state()
$effect(() => {
if (w.seed && !autoRefreshTimer) {
autoRefreshTimer = setInterval(() => {
w.refresh()
}, 30000)
} else if (!w.seed && autoRefreshTimer) {
clearInterval(autoRefreshTimer)
autoRefreshTimer = undefined
}
return () => {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer)
autoRefreshTimer = undefined
}
}
})
onMount(() => {
if (!w.seed || w.currentWalletId !== walletId) {
navigate('/xrp')
return
}
w.refresh()
})
</script>
<div class="mx-auto mt-16 max-w-md">
<div class="flex flex-col gap-4">
<Card>
<CardHeader>
<div class="flex items-center justify-between">
<CardTitle>
{w.currentWallet?.name ?? 'XRP Wallet'}
</CardTitle>
<img src="/icons/xrp.png" alt="" class="size-6 rounded-full" />
</div>
<CardDescription>XRP Ledger</CardDescription>
</CardHeader>
<CardContent>
<div class="mb-4 space-y-1">
<div class="flex items-start gap-3">
<div class="min-w-0 flex-1 space-y-1">
<p class="font-medium text-xs">Address</p>
<p class="font-mono text-xs break-all">{w.address}</p>
<div class="flex gap-1.5 mt-1">
<Dialog.Root bind:open={qrDialogOpen}>
<Dialog.Trigger>
<Button
variant="outline"
size="icon-sm"
class="text-muted-foreground"
aria-label="Show QR code">
<QrCodeIcon size={16} />
</Button>
</Dialog.Trigger>
<Dialog.Content>
<div
class="flex flex-col items-center gap-3 py-4">
<QrCode text={w.address} size={256} />
<p
class="font-mono text-xs break-all text-center max-w-64">
{w.address}
</p>
</div>
</Dialog.Content>
</Dialog.Root>
<CopyButton
text={w.address}
variant="outline"
size="icon-sm"
class="text-muted-foreground">
</CopyButton>
<Button
variant="outline"
size="icon-sm"
class="text-muted-foreground"
aria-label="View on explorer"
onclick={() =>
electrobun.rpc?.request.openExternal({
url: w.explorerAddressUrl,
})}>
<ExternalLink size={16} />
</Button>
</div>
</div>
</div>
<p class="mt-2 font-medium text-xs">Balance</p>
{#if w.loadingBalance}
<div class="py-1"><Loader /></div>
{:else}
<div class="flex items-baseline gap-2">
<p class="font-mono text-lg">{w.balance} XRP</p>
{#if w.price}
<p class="font-mono text-sm text-muted-foreground">
≈ ${(
parseFloat(w.balance) * parseFloat(w.price)
).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</p>
{/if}
</div>
{#if !w.funded}
<p class="text-xs text-muted-foreground">
Account not activated — it needs at least 10 XRP to
meet the base reserve.
</p>
{/if}
{#if w.fee}
<p class="text-xs text-muted-foreground mt-1">
Network fee: ~{w.fee} XRP
</p>
{/if}
{/if}
</div>
<div class="flex gap-2">
<Button
variant="outline"
onclick={() => navigate('/xrp/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>
<Card>
<CardHeader>
<CardTitle>Transactions</CardTitle>
</CardHeader>
<CardContent>
{#if w.loadingTransactions}
<div class="flex justify-center py-4">
<Loader />
</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 as tx}
<button
onclick={() =>
navigate('/xrp/tx/:hash', {
params: { hash: tx.hash },
})}
class="flex w-full items-start gap-2 rounded-md bg-muted px-3 py-2 text-xs text-left cursor-pointer hover:bg-muted/80 transition-colors">
<div
class="shrink-0 mt-0.5 {tx.direction === 'in'
? 'text-green-500'
: 'text-muted-foreground'}">
{#if tx.direction === 'in'}
<ArrowDown size={16} />
{:else}
<ArrowUp size={16} />
{/if}
</div>
<div class="min-w-0 flex-1 space-y-0.5">
<p class="font-medium">
{tx.direction === 'in' ? 'Received' : 'Sent'}
{#if !tx.confirmed}
<span class="text-muted-foreground">
(unconfirmed)
</span>
{/if}
</p>
<p class="font-mono text-muted-foreground">
{tx.amount} XRP
</p>
{#if tx.timestamp && tx.timestamp !== '0'}
<p class="text-muted-foreground">
{new Date(
Number(tx.timestamp) * 1000,
).toLocaleString()}
</p>
{/if}
<p
class="text-muted-foreground/50 truncate font-mono">
{tx.hash.slice(0, 12)}...
</p>
</div>
</button>
{/each}
</div>
{/if}
</CardContent>
</Card>
</div>
</div>
+49 -222
View File
@@ -13,18 +13,16 @@
"apps/desktop": {
"name": "koins",
"dependencies": {
"@alchemy/common": "^5.0.0",
"@alchemy/wallet-apis": "^5.0.0",
"@koins/utils": "workspace:*",
"d3-scale": "^4.0.2",
"d3-shape": "^3.2.0",
"drizzle-orm": "^1.0.0-beta.22",
"electrobun": "^1.18.1",
"env-paths": "^4.0.0",
"mode-watcher": "^1.1.0",
"monero-ts": "^0.11.8",
"qrcode": "^1.5.4",
"sv-router": "latest",
"viem": "^2.48.11",
"xrpl": "^5.0.0",
},
"devDependencies": {
"@fontsource-variable/geist-mono": "^5.2.7",
@@ -42,7 +40,6 @@
"bun-types": "^1.3.14",
"clsx": "^2.1.1",
"concurrently": "^9.2.1",
"drizzle-kit": "^1.0.0-beta.22",
"embla-carousel-svelte": "^8.6.0",
"formsnap": "^2.0.1",
"layerchart": "2.0.0-next.48",
@@ -75,14 +72,6 @@
},
},
"packages": {
"@adraffy/ens-normalize": ["@adraffy/ens-normalize@1.11.1", "", {}, "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ=="],
"@alchemy/common": ["@alchemy/common@5.0.2", "", { "dependencies": { "zod": "^3.23.0" }, "peerDependencies": { "viem": "^2.45.0" } }, "sha512-ORFjRhAJ07BUjhNqjgx2b4khWu0sNNEnnTXj3DT7UKFxZvK79cpyoE/6E0D17OWiz40y+uk6eRtkWog5bNWVpg=="],
"@alchemy/wallet-api-types": ["@alchemy/wallet-api-types@0.2.0-alpha.3", "", { "dependencies": { "@alchemy/common": "5.0.0-beta.32", "deep-equal": "^2.2.3", "ox": "^0.6.12", "viem": "^2.45.0", "zod": "^4.4.1" }, "peerDependencies": { "typescript": "^5.8.2" } }, "sha512-VaNj8gzGfPveo8i6l3aX4YovQezu4oDuAqyAy2k1LqsLf18pn2+iw3cV/WiFTwMrdQXLtnDYzS9D6NdkgEokzw=="],
"@alchemy/wallet-apis": ["@alchemy/wallet-apis@5.0.2", "", { "dependencies": { "@alchemy/common": "5.0.2", "@alchemy/wallet-api-types": "0.2.0-alpha.3", "deep-equal": "^2.2.3", "ox": "^0.11.1", "zod": "^4.4.1" }, "peerDependencies": { "@solana/kit": ">=6.0.0", "@solana/web3.js": "^1.95.0", "viem": "^2.45.0" }, "optionalPeers": ["@solana/kit", "@solana/web3.js"] }, "sha512-HmwOAvpe248LOQaJ3Der2owT82l0OwxBfVbwiXd7WXlJ+k04L1ooFKo5AjiWXMaoJQzpIkh37n66SUmyVVd4PA=="],
"@ark/schema": ["@ark/schema@0.56.0", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA=="],
"@ark/util": ["@ark/util@0.56.0", "", {}, "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA=="],
@@ -105,65 +94,63 @@
"@dagrejs/graphlib": ["@dagrejs/graphlib@3.0.4", "", {}, "sha512-HxZ7fCvAwTLCWCO0WjDkzAFQze8LdC6iOpKbetDKHIuDfIgMlIzYzqZ4nxwLlclQX+3ZVeZ1K2OuaOE2WWcyOg=="],
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="],
"@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.4", "", { "os": "android", "cpu": "arm" }, "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.4", "", { "os": "android", "cpu": "arm64" }, "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.4", "", { "os": "android", "cpu": "x64" }, "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.4", "", { "os": "linux", "cpu": "arm" }, "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.4", "", { "os": "linux", "cpu": "x64" }, "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.4", "", { "os": "none", "cpu": "x64" }, "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg=="],
"@exodus/schemasafe": ["@exodus/schemasafe@1.3.0", "", {}, "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw=="],
@@ -191,8 +178,6 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="],
"@koins/utils": ["@koins/utils@workspace:packages/utils"],
"@layerstack/svelte-actions": ["@layerstack/svelte-actions@1.0.1-next.18", "", { "dependencies": { "@floating-ui/dom": "^1.7.0", "@layerstack/utils": "2.0.0-next.18", "d3-scale": "^4.0.2" } }, "sha512-gxPzCnJ1c9LTfWtRqLUzefCx+k59ZpxDUQ2XB+LokveZQPe7IDSOwHaBOEMlaGoGrtwc3Ft8dSZq+2WT2o9u/g=="],
@@ -209,11 +194,9 @@
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
"@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="],
"@noble/curves": ["@noble/curves@2.2.0", "", { "dependencies": { "@noble/hashes": "2.2.0" } }, "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ=="],
"@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="],
"@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="],
"@oxc-project/types": ["@oxc-project/types@0.95.0", "", {}, "sha512-vACy7vhpMPhjEJhULNxrdR0D943TkA/MigMpJCHmBHvMXxRStRi/dPtTlfQ3uDwWSzRpT8z+7ImjZVf8JWBocQ=="],
@@ -257,11 +240,11 @@
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.45", "", {}, "sha512-Le9ulGCrD8ggInzWw/k2J8QcbPz7eGIOWqfJ2L+1R0Opm7n6J37s2hiDWlh6LJN0Lk9L5sUzMvRHKW7UxBZsQA=="],
"@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="],
"@scure/base": ["@scure/base@2.2.0", "", {}, "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg=="],
"@scure/bip32": ["@scure/bip32@1.7.0", "", { "dependencies": { "@noble/curves": "~1.9.0", "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw=="],
"@scure/bip32": ["@scure/bip32@2.2.0", "", { "dependencies": { "@noble/curves": "2.2.0", "@noble/hashes": "2.2.0", "@scure/base": "2.2.0" } }, "sha512-zFr7t2F+a9+5tB7QbarF2HQNYrgjCNaoLAupZdKkrFMYMozJf5zqH2WJCQibMzm1qQ0QogrxVGO3qXfQDYMaQg=="],
"@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="],
"@scure/bip39": ["@scure/bip39@2.2.0", "", { "dependencies": { "@noble/hashes": "2.2.0", "@scure/base": "2.2.0" } }, "sha512-T/Bj/YvYMNkIPq6EENO6/rcs2e7qTNuyoUXf0KBFDmp0ZDu0H2X4Lq6yC3i0c8PcWkov5EbW+yQZZbdMmk154A=="],
"@sideway/address": ["@sideway/address@4.1.5", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q=="],
@@ -369,7 +352,9 @@
"@vinejs/vine": ["@vinejs/vine@3.0.1", "", { "dependencies": { "@poppinss/macroable": "^1.0.4", "@types/validator": "^13.12.2", "@vinejs/compiler": "^3.0.0", "camelcase": "^8.0.0", "dayjs": "^1.11.13", "dlv": "^1.1.3", "normalize-url": "^8.0.1", "validator": "^13.12.0" } }, "sha512-ZtvYkYpZOYdvbws3uaOAvTFuvFXoQGAtmzeiXu+XSMGxi5GVsODpoI9Xu9TplEMuD/5fmAtBbKb9cQHkWkLXDQ=="],
"abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="],
"@xrplf/isomorphic": ["@xrplf/isomorphic@1.0.2", "", { "dependencies": { "@noble/hashes": "^2.0.1", "eventemitter3": "5.0.1", "ws": "^8.20.0" } }, "sha512-ncZUdMXr6VlSXtdoiDi0jTH+gBrgGxwVeEidhoegII3PmyErbQsyj6e+j7acmR4LW/lvBkPkzb9QzRfJH0n3rA=="],
"@xrplf/secret-numbers": ["@xrplf/secret-numbers@3.0.0", "", { "dependencies": { "@xrplf/isomorphic": "^1.0.2", "ripple-keypairs": "^3.0.0" } }, "sha512-qpGhAZXv5noMDjCtfzq5NK0y5rrdwTVjKhhPcAYSE+a/gogBOgqdpCKyieprVVPCnmVmJnGeRoZKBAqpCGegsA=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
@@ -389,8 +374,6 @@
"arktype": ["arktype@2.2.0", "", { "dependencies": { "@ark/schema": "0.56.0", "@ark/util": "0.56.0", "arkregex": "0.0.5" } }, "sha512-t54MZ7ti5BhOEvzEkgKnWvqj+UbDfWig+DHr5I34xatymPusKLS0lQpNJd8M6DzmIto2QGszHfNKoFIT8tMCZQ=="],
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
"ast-kit": ["ast-kit@2.2.0", "", { "dependencies": { "@babel/parser": "^7.28.5", "pathe": "^2.0.3" } }, "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw=="],
"ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="],
@@ -399,14 +382,14 @@
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
"axios": ["axios@1.16.1", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
"basic-ftp": ["basic-ftp@5.3.1", "", {}, "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw=="],
"bignumber.js": ["bignumber.js@10.0.2", "", {}, "sha512-E8Wp9O06QA6lneJ4aRUXKYf/1GIomqUEmUMwtIOMtDxf1U52ffJY+y7JBk/8wRafA8qOIqLnXQGqonYXZdBnFQ=="],
"birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
"bits-ui": ["bits-ui@2.18.1", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.35.1", "svelte-toolbelt": "^0.10.6", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-KkemzKFH4T3gt3H+P86JcnAWExjByv/6vlwjm/BoCwTPHu03yiCdxbghdJLvFReQTe0acCAiRcKfmixxD6XvlA=="],
@@ -415,12 +398,8 @@
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@@ -513,14 +492,8 @@
"decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
"deep-equal": ["deep-equal@2.2.3", "", { "dependencies": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.5", "es-get-iterator": "^1.1.3", "get-intrinsic": "^1.2.2", "is-arguments": "^1.1.1", "is-array-buffer": "^3.0.2", "is-date-object": "^1.0.5", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "isarray": "^2.0.5", "object-is": "^1.1.5", "object-keys": "^1.1.1", "object.assign": "^4.1.4", "regexp.prototype.flags": "^1.5.1", "side-channel": "^1.0.4", "which-boxed-primitive": "^1.0.2", "which-collection": "^1.0.1", "which-typed-array": "^1.1.13" } }, "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA=="],
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
"defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="],
"degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="],
@@ -541,10 +514,6 @@
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
"drizzle-kit": ["drizzle-kit@1.0.0-beta.22", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-9HTZuQRljQKTgCx4UhiGn8KYYfHGk4+B/bRR1714W67kz0qgJvdrG527i8rQD8uUyET9UTGR1u8syySJD4znGw=="],
"drizzle-orm": ["drizzle-orm@1.0.0-beta.22", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/mssql", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mssql", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-F+DZyVIvH0oVKa/w08Cle1xfoH+pc+htIXHG/frnMLG72aby9NYYr9oc+9XvghnoO4umxFItduz0OMmQJMnenw=="],
"dts-resolver": ["dts-resolver@2.1.3", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
@@ -571,13 +540,11 @@
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-get-iterator": ["es-get-iterator@1.1.3", "", { "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", "has-symbols": "^1.0.3", "is-arguments": "^1.1.1", "is-map": "^2.0.2", "is-set": "^2.0.2", "is-string": "^1.0.7", "isarray": "^2.0.5", "stop-iteration-iterator": "^1.0.0" } }, "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
"esbuild": ["esbuild@0.27.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
@@ -607,8 +574,6 @@
"follow-redirects": ["follow-redirects@1.16.0", "", { "peerDependencies": { "debug": "*" }, "optionalPeers": ["debug"] }, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="],
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
"formsnap": ["formsnap@2.0.1", "", { "dependencies": { "svelte-toolbelt": "^0.5.0" }, "peerDependencies": { "svelte": "^5.0.0", "sveltekit-superforms": "^2.19.0" } }, "sha512-iJSe4YKd/W6WhLwKDVJU9FQeaJRpEFuolhju7ZXlRpUVyDdqFdMP8AUBICgnVvQPyP41IPAlBa/v0Eo35iE6wQ=="],
@@ -617,8 +582,6 @@
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
@@ -633,12 +596,8 @@
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
@@ -655,64 +614,26 @@
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
"ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="],
"is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="],
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
"is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="],
"is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="],
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
"is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="],
"is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
"is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
"is-safe-filename": ["is-safe-filename@0.1.1", "", {}, "sha512-4SrR7AdnY11LHfDKTZY1u6Ga3RuxZdl3YKWWShO5iyuG5h8QS4GD2tOb04peBJ5I7pXbR+CGBNEhTcwK+FzN3g=="],
"is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
"is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="],
"is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="],
"is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="],
"is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="],
"is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="],
"is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
"isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"joi": ["joi@17.13.3", "", { "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } }, "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA=="],
"jsbi": ["jsbi@4.3.2", "", {}, "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
@@ -797,18 +718,8 @@
"normalize-url": ["normalize-url@8.1.1", "", {}, "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"object-is": ["object-is@1.1.6", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1" } }, "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q=="],
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
"object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="],
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
"ox": ["ox@0.11.3", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-1bWYGk/xZel3xro3l8WGg6eq4YEKlaqvyMtVhfMFpbJzK2F6rj4EDRtqDCWVEJMkzcmEi9uW2QxsqELokOlarw=="],
"p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
@@ -835,8 +746,6 @@
"pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="],
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
@@ -867,8 +776,6 @@
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
"remixicon-svelte": ["remixicon-svelte@0.0.5", "", { "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-a7tnkBeA6P/ItRc2Q/jOgF+Xoh6Fj7hKrr/E6ZUCmOM4XBFi+mBDR95hqdRHWVf2s8sK85ycGXg+AAFZv87s+Q=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
@@ -877,6 +784,12 @@
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"ripple-address-codec": ["ripple-address-codec@5.0.1", "", { "dependencies": { "@scure/base": "^2.0.0", "@xrplf/isomorphic": "^1.0.2" } }, "sha512-JQHLKuVJV8lv9Qobmn4aUM2Dpv9WRRLKnNWfM8tN02fAbUtG8mUPsu9q9UYX8P76G4qzytEc5ZKMp/3JggNYmw=="],
"ripple-binary-codec": ["ripple-binary-codec@2.8.0", "", { "dependencies": { "@xrplf/isomorphic": "^1.0.2", "bignumber.js": "^10.0.2", "ripple-address-codec": "^5.0.1" } }, "sha512-+NKnOi3hdzjm5dDpoZLUEaYon1jahPlSGnp3YrDoNMSR09ICEqgupN5wpEkPuqJvV75PF/g+W1QUwIXVzbEe7w=="],
"ripple-keypairs": ["ripple-keypairs@3.0.0", "", { "dependencies": { "@noble/curves": "^2.0.1", "@xrplf/isomorphic": "^1.0.2", "ripple-address-codec": "^5.0.1" } }, "sha512-lE69pD0E8hFNCqZoVXRyY45Yi8Ku+Qw7Rf1qRwPj4nOi34vp9NAuwzfiJH1IwXGWNCfEkwVfctG99CPTEoUf+g=="],
"robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
"rolldown": ["rolldown@1.0.0-beta.45", "", { "dependencies": { "@oxc-project/types": "=0.95.0", "@rolldown/pluginutils": "1.0.0-beta.45" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.45", "@rolldown/binding-darwin-arm64": "1.0.0-beta.45", "@rolldown/binding-darwin-x64": "1.0.0-beta.45", "@rolldown/binding-freebsd-x64": "1.0.0-beta.45", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.45", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.45", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.45", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.45", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.45", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.45", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.45", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.45", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.45", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.45" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-iMmuD72XXLf26Tqrv1cryNYLX6NNPLhZ3AmNkSf8+xda0H+yijjGJ+wVT9UdBUHOpKzq9RjKtQKRCWoEKQQBZQ=="],
@@ -893,8 +806,6 @@
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
@@ -905,10 +816,6 @@
"set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="],
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
"shadcn-svelte": ["shadcn-svelte@1.2.7", "", { "dependencies": { "commander": "^14.0.0", "node-fetch-native": "^1.6.4", "postcss": "^8.5.5", "tailwind-merge": "^3.0.0" }, "peerDependencies": { "svelte": "^5.0.0" }, "bin": { "shadcn-svelte": "dist/index.mjs" } }, "sha512-mWuQk4H4gtV+J2wJQ7nEPKNnB/v86AALFryZU0SSM7ChHmJJMZ1kH+qIuxYKrXm9vOOOcSWHRsWzPDB71DnjYA=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
@@ -917,14 +824,6 @@
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
@@ -937,8 +836,6 @@
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@@ -949,7 +846,7 @@
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"sv-router": ["sv-router@0.16.3", "", { "dependencies": { "esm-env": "^1.2.2" }, "peerDependencies": { "svelte": "^5" }, "bin": { "sv-router": "./src/cli/index.js" } }, "sha512-nne8keA2GKZRdRE8r7HFx7wcR2ruqVPf/RZ9De5webNgaDbfKHEeh5qpAe0Hg0cczXnZh/qcgRsSNDphbVd0Gw=="],
"sv-router": ["sv-router@0.18.0", "", { "dependencies": { "esm-env": "^1.2.2" }, "peerDependencies": { "svelte": "^5" }, "bin": { "sv-router": "./src/cli/index.js" } }, "sha512-gLky7FBGj8P6MAna1wxYniHJxMrXgaeTNnkZ7WH387ddRyUohfDtvEgWQFfB4oLL2XxWlw8Lfmk03TtBdK/JTA=="],
"svelte": ["svelte@5.55.5", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.4", "esm-env": "^1.2.1", "esrap": "^2.2.4", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw=="],
@@ -1025,8 +922,6 @@
"vaul-svelte": ["vaul-svelte@1.0.0-next.7", "", { "dependencies": { "runed": "^0.23.2", "svelte-toolbelt": "^0.7.1" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-7zN7Bi3dFQixvvbUJY9uGDe7Ws/dGZeBQR2pXdXmzQiakjrxBvWo0QrmsX3HK+VH+SZOltz378cmgmCS9f9rSg=="],
"viem": ["viem@2.48.11", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.14.20", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-+WZ5E0dBS6GtKb+1wEk5DeYRRRW42+pFnXCo67Ydodf42sBwO+hu3wnQy66lc4MKmHz+llPVdbyehYr9oTE2iw=="],
"vite": ["vite@8.0.11", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.14", "rolldown": "1.0.0-rc.18", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow=="],
"vitefu": ["vitefu@1.1.2", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw=="],
@@ -1035,17 +930,13 @@
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
"which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="],
"which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="],
"which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="],
"xrpl": ["xrpl@5.0.0", "", { "dependencies": { "@scure/bip32": "^2.0.1", "@scure/bip39": "^2.0.1", "@xrplf/isomorphic": "^1.0.2", "@xrplf/secret-numbers": "^3.0.0", "bignumber.js": "^10.0.2", "eventemitter3": "^5.0.1", "fast-json-stable-stringify": "^2.1.0", "ripple-address-codec": "^5.0.1", "ripple-binary-codec": "^2.8.0", "ripple-keypairs": "^3.0.0" } }, "sha512-YqaTFJUnhOu0mI4bsuHbKGj6w9ATcH8EIgw+gOLnh1rrlLTo5oImLQzhKJixCAPqqWOKnsY7J3jsN+l+zeEWgA=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
@@ -1061,12 +952,6 @@
"zod-v3-to-json-schema": ["zod-v3-to-json-schema@4.0.0", "", { "peerDependencies": { "zod": "^3.25 || ^4.0.14" } }, "sha512-KixLrhX/uPmRFnDgsZrzrk4x5SSJA+PmaE5adbfID9+3KPJcdxqRobaHU397EfWBqfQircrjKqvEqZ/mW5QH6w=="],
"@alchemy/common/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@alchemy/wallet-api-types/@alchemy/common": ["@alchemy/common@5.0.0-beta.32", "", { "dependencies": { "zod": "^3.23.0" }, "peerDependencies": { "viem": "^2.45.0" } }, "sha512-gVUrMhnhdD8UUipd70XUbmxm3PX3OhHK1TSi3ODBGOH3Jd9SoH0+mFagSn62pwi5XFPCOg3UGGXj9bmLKFlGhA=="],
"@alchemy/wallet-api-types/ox": ["ox@0.6.12", "", { "dependencies": { "@adraffy/ens-normalize": "^1.10.1", "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", "@scure/bip32": "^1.5.0", "@scure/bip39": "^1.4.0", "abitype": "^1.0.6", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-78hziRGLj0qTDa0UW4+ynv9tW2Cp1vmCfGokL8D7kiSDh6Y0LAfHL+HaDN4l2a9jcrOG3fexTDtLNtDNkEwLtg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
@@ -1119,18 +1004,12 @@
"svelte-sonner/runed": ["runed@0.28.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ=="],
"tsx/esbuild": ["esbuild@0.27.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="],
"vaul-svelte/runed": ["runed@0.23.4", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA=="],
"vaul-svelte/svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="],
"viem/ox": ["ox@0.14.20", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-rby38C3nDn8eQkf29Zgw4hkCZJ64Qqi0zRPWL8ENUQ7JVuoITqrVtwWQgM/He19SCMUEc7hS/Sjw0jIOSLJhOw=="],
"vite/rolldown": ["rolldown@1.0.0-rc.18", "", { "dependencies": { "@oxc-project/types": "=0.128.0", "@rolldown/pluginutils": "1.0.0-rc.18" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.18", "@rolldown/binding-darwin-arm64": "1.0.0-rc.18", "@rolldown/binding-darwin-x64": "1.0.0-rc.18", "@rolldown/binding-freebsd-x64": "1.0.0-rc.18", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.18", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.18", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.18", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.18", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.18", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.18", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.18" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg=="],
"@alchemy/wallet-api-types/@alchemy/common/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@types/bun/bun-types/@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="],
"axios/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
@@ -1153,58 +1032,6 @@
"qrcode/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q=="],
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.4", "", { "os": "android", "cpu": "arm" }, "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ=="],
"tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.4", "", { "os": "android", "cpu": "arm64" }, "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw=="],
"tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.4", "", { "os": "android", "cpu": "x64" }, "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw=="],
"tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ=="],
"tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw=="],
"tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw=="],
"tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ=="],
"tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.4", "", { "os": "linux", "cpu": "arm" }, "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg=="],
"tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA=="],
"tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA=="],
"tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA=="],
"tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw=="],
"tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA=="],
"tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw=="],
"tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA=="],
"tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.4", "", { "os": "linux", "cpu": "x64" }, "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA=="],
"tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q=="],
"tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.4", "", { "os": "none", "cpu": "x64" }, "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg=="],
"tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow=="],
"tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ=="],
"tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg=="],
"tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g=="],
"tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg=="],
"tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw=="],
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg=="],
"vite/rolldown/@oxc-project/types": ["@oxc-project/types@0.128.0", "", {}, "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ=="],
"vite/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.18", "", { "os": "android", "cpu": "arm64" }, "sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ=="],