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 # 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 ## 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 - **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 - **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 - **Password fallback**: PBKDF2-salted password stored in keychain, used when biometric is unavailable
- **QR codes**: Address QR with chain logo overlay - **QR codes**: Address QR with chain logo overlay
- **Transaction history**: Cached in local SQLite via Drizzle ORM - **Transaction history**: Live XRP ledger transactions and Monero wallet transactions
- **Settings**: Reset app clears all DB tables and keychain entries - **Settings**: Reset app clears all keychain entries
## Prerequisites ## Prerequisites
@@ -35,41 +35,38 @@ apps/desktop/
│ │ │ ├── monero/ # Monero RPC management │ │ │ ├── monero/ # Monero RPC management
│ │ │ │ ├── binary.ts # Download/extract monero-wallet-rpc │ │ │ │ ├── binary.ts # Download/extract monero-wallet-rpc
│ │ │ │ └── wallet.ts # Child process + RPC client │ │ │ │ └── wallet.ts # Child process + RPC client
│ │ │ ├── alchemy/ # Alchemy API calls │ │ │ ├── xrp/ # XRP Ledger client (xrpl)
│ │ │ ├── viem/ # viem chain calls
│ │ │ ├── tokens/ # Token metadata (DB-first cache)
│ │ │ ├── transactions/ # Tx history + details (DB-first cache)
│ │ │ ├── db/ # Drizzle ORM + SQLite schema
│ │ │ └── biometric.ts # Biometric auth (Swift subprocess) │ │ │ └── biometric.ts # Biometric auth (Swift subprocess)
│ │ ├── rpc/
│ │ │ ├── monero/ # Monero RPC handlers
│ │ │ └── xrp/ # XRP RPC handlers
│ │ └── native/ │ │ └── native/
│ │ └── biometric-helper.swift # Touch ID prompt binary │ │ └── biometric-helper.swift # Touch ID prompt binary
│ ├── lib/ │ ├── lib/
│ │ ├── states/ │ │ ├── states/
│ │ │ └── wallet.svelte.ts # Wallet state (singleton) │ │ │ ├── monero-wallet.svelte.ts # Monero wallet state (singleton)
│ │ ├── components/ │ │ │ └── xrp-wallet.svelte.ts # XRP wallet state (singleton)
│ │ │ └── EvmDashboard.svelte # Shared EVM dashboard
│ │ └── electrobun.ts # RPC type definitions │ │ └── electrobun.ts # RPC type definitions
│ └── routes/ │ └── routes/
│ ├── index.svelte # Login/welcome page │ ├── index.svelte # Login/welcome page
│ ├── multicoin/ # ETH / BSC / Polygon routes │ ├── monero/ # Monero routes
│ ├── monero/ # Monero dashboard │ ├── xrp/ # XRP routes
│ ├── settings.svelte # App settings │ └── settings.svelte # App settings
│ └── tx/ # Transaction details
└── package.json └── package.json
``` ```
## Key Decisions ## 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. - **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). - **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 ## 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 - [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 - [Svelte 5](https://svelte.dev) — UI framework
- [Electrobun](https://electrobun.dev) — Desktop app shell - [Electrobun](https://electrobun.dev) — Desktop app shell
- [sv-router](https://github.com/TZGyn/sv-router) — File-based routing - [sv-router](https://github.com/TZGyn/sv-router) — File-based routing
@@ -80,5 +77,4 @@ apps/desktop/
```bash ```bash
bun run dev # Start development server 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/index.html': 'views/mainview/index.html',
'dist/assets': 'views/mainview/assets', 'dist/assets': 'views/mainview/assets',
'dist/icons': 'views/mainview/icons', 'dist/icons': 'views/mainview/icons',
'drizzle': 'drizzle',
'src/bun/native/biometric-helper': 'native/biometric-helper', 'src/bun/native/biometric-helper': 'native/biometric-helper',
}, },
// Ignore Vite output in watch mode — HMR handles view rebuilds separately // 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:canary": "bun run build:native && vite build && electrobun build --env=canary",
"build:stable": "bun run build:native && vite build && electrobun build --env=stable", "build:stable": "bun run build:native && vite build && electrobun build --env=stable",
"build:win": "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" "build:native": "swiftc -O -o src/bun/native/biometric-helper src/bun/native/biometric-helper.swift"
}, },
"dependencies": { "dependencies": {
"@alchemy/common": "^5.0.0",
"@alchemy/wallet-apis": "^5.0.0",
"@koins/utils": "workspace:*", "@koins/utils": "workspace:*",
"d3-scale": "^4.0.2", "d3-scale": "^4.0.2",
"d3-shape": "^3.2.0", "d3-shape": "^3.2.0",
"drizzle-orm": "^1.0.0-beta.22",
"electrobun": "^1.18.1", "electrobun": "^1.18.1",
"env-paths": "^4.0.0",
"mode-watcher": "^1.1.0", "mode-watcher": "^1.1.0",
"monero-ts": "^0.11.8", "monero-ts": "^0.11.8",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"sv-router": "latest", "sv-router": "latest",
"viem": "^2.48.11" "xrpl": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@fontsource-variable/geist-mono": "^5.2.7", "@fontsource-variable/geist-mono": "^5.2.7",
@@ -45,7 +41,6 @@
"bun-types": "^1.3.14", "bun-types": "^1.3.14",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"drizzle-kit": "^1.0.0-beta.22",
"embla-carousel-svelte": "^8.6.0", "embla-carousel-svelte": "^8.6.0",
"formsnap": "^2.0.1", "formsnap": "^2.0.1",
"layerchart": "2.0.0-next.48", "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, Updater,
Utils, Utils,
} from 'electrobun/bun' } from 'electrobun/bun'
import { migrate } from 'drizzle-orm/bun-sqlite/migrator'
import { db } from './lib/db'
import { join } from 'path' import { join } from 'path'
import { existsSync } from 'fs' import { existsSync } from 'fs'
import { getENV } from './lib/get-env' import { getENV } from './lib/get-env'
import { createEvmHandlers } from './rpc/evm'
import { createMoneroHandlers } from './rpc/monero' import { createMoneroHandlers } from './rpc/monero'
import { createXrpHandlers } from './rpc/xrp'
import type { MoneroWalletState } from './lib/monero' import type { MoneroWalletState } from './lib/monero'
import { canPromptTouchID, promptTouchID } from './lib/biometric' import { canPromptTouchID, promptTouchID } from './lib/biometric'
import { sqlite } from './lib/sqlite' import { getXrpWalletList } from './lib/xrp'
import { evmWallets } from './lib/db/schema'
import type { RPC } from '../lib/rpc-schema' import type { RPC } from '../lib/rpc-schema'
import { log, logError } from './lib/logger' import { log } from './lib/logger'
const env = getENV() const env = getENV()
log(`env: ${JSON.stringify(env)}`) 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_PORT = 5173
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}` const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`
@@ -92,10 +77,6 @@ const moneroState = {
manager: null as MoneroWalletState | null, manager: null as MoneroWalletState | null,
downloading: false, downloading: false,
} }
const syncState = {
target: null as { address: string; chainid: string } | null,
timer: null as ReturnType<typeof setInterval> | null,
}
const rpc = BrowserView.defineRPC<RPC>({ const rpc = BrowserView.defineRPC<RPC>({
maxRequestTime: 120000, maxRequestTime: 120000,
@@ -108,11 +89,11 @@ const rpc = BrowserView.defineRPC<RPC>({
rpc.setRequestHandler({ rpc.setRequestHandler({
resetApp: async () => { resetApp: async () => {
try { try {
const wallets = db.select().from(evmWallets).all() const wallets = await getXrpWalletList()
await Promise.all([ await Promise.all([
Bun.secrets.delete({ Bun.secrets.delete({
service: 'koins', service: 'koins',
name: 'alchemy_key', name: 'xrp_wallets',
}), }),
...wallets.flatMap((w) => [ ...wallets.flatMap((w) => [
Bun.secrets.delete({ Bun.secrets.delete({
@@ -121,15 +102,10 @@ rpc.setRequestHandler({
}), }),
Bun.secrets.delete({ Bun.secrets.delete({
service: 'koins', 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') console.log('[rpc] resetApp complete')
return true return true
} catch (e) { } catch (e) {
@@ -174,8 +150,8 @@ rpc.setRequestHandler({
logToFile: async ({ message }) => { logToFile: async ({ message }) => {
log(message) log(message)
}, },
...createEvmHandlers(rpc, syncState),
...createMoneroHandlers(moneroState), ...createMoneroHandlers(moneroState),
...createXrpHandlers(),
}) })
log('creating BrowserWindow...') 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, MoneroAccountEntry,
MoneroSendResult, MoneroSendResult,
MoneroTransferDetails, MoneroTransferDetails,
TxEntry, XrpWalletInfo,
TransactionDetails, XrpBalance,
TokenBalanceResult, XrpTxEntry,
EvmWalletInfo, XrpTxDetails,
XrpSendResult,
} from './rpc-schema' } from './rpc-schema'
const rpc = Electroview.defineRPC<RPC>({ const rpc = Electroview.defineRPC<RPC>({
+53 -75
View File
@@ -79,67 +79,50 @@ export type MoneroFeeEstimate = {
estimatedFee: string estimatedFee: string
} }
export type TxEntry = { export type XrpWalletInfo = {
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 = {
id: string id: string
name: string name: string
address: string
hasPassword: boolean hasPassword: boolean
vaultKey: string vaultKey: string
createdAt: 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 = { export type GeneralRpcRequests = {
resetApp: { params: {}; response: boolean } resetApp: { params: {}; response: boolean }
biometricCanAuth: { params: {}; response: boolean } biometricCanAuth: { params: {}; response: boolean }
@@ -151,23 +134,6 @@ export type GeneralRpcRequests = {
logToFile: { params: { message: string }; response: void } 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 = { export type MoneroRpcRequests = {
moneroBinaryStatus: { params: {}; response: MoneroBinaryStatus } moneroBinaryStatus: { params: {}; response: MoneroBinaryStatus }
moneroDownloadBinary: { params: {}; response: void } moneroDownloadBinary: { params: {}; response: void }
@@ -190,15 +156,27 @@ export type MoneroRpcRequests = {
fetchMoneroPrice: { params: {}; response: { usd: string } | null } 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 = { export type RPC = {
bun: { bun: {
requests: GeneralRpcRequests & EvmRpcRequests & MoneroRpcRequests requests: GeneralRpcRequests & MoneroRpcRequests & XrpRpcRequests
messages: {} messages: {}
} }
webview: { webview: {
requests: {} requests: {}
messages: { messages: {}
transfersUpdate: { count: number; chainid: string; address: string }
}
} }
} }
@@ -1,73 +1,37 @@
import { mnemonicToAccount } from 'viem/accounts'
import { createPublicClient, http, formatEther } from 'viem'
import { import {
electrobun, electrobun,
type TxEntry, type XrpTxEntry,
type EvmWalletInfo, type XrpWalletInfo,
} from '$lib/electrobun' } from '$lib/electrobun'
import { tryCatch } from '@koins/utils' import { tryCatch } from '@koins/utils'
import { moneroWallet } from './monero-wallet.svelte.js'
export const networks = [ export const xrpNetwork = {
{ id: 'xrp',
id: 'bsc', name: 'XRP Ledger',
name: 'BNB Smart Chain', symbol: 'XRP',
rpc: 'https://bsc-dataseed.binance.org', explorerUrl: 'https://xrpscan.com/tx/',
symbol: 'BNB', explorerAddressUrl: 'https://xrpscan.com/account/',
chainid: '56', } as const
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 type NetworkId = (typeof networks)[number]['id'] export const XrpWallet = () => {
let accountType = $state<'xrp' | null>(null)
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)
let ready = $state(false) let ready = $state(false)
let biometricAvailable = $state(false) let biometricAvailable = $state(false)
let seed = $state('') let seed = $state('')
let address = $state('') let address = $state('')
let balance = $state('0') let balance = $state('0')
let tokenBalances = $state<TokenBalance[]>([]) let funded = $state(true)
let network = $state<NetworkId>('eth') let price = $state<string | null>(null)
let apiKey = $state('') let fee = $state<string | null>(null)
let transactions = $state<TxEntry[]>([]) let transactions = $state<XrpTxEntry[]>([])
let loadingBalance = $state(false) let loadingBalance = $state(false)
let loadingTransactions = $state(false) let loadingTransactions = $state(false)
let loading = $state(false) let loading = $state(false)
let error = $state('') let error = $state('')
let wallets = $state<EvmWalletInfo[]>([]) let wallets = $state<XrpWalletInfo[]>([])
let currentWalletId = $state<string | null>(null) let currentWalletId = $state<string | null>(null)
let currentPasswordHash = $state<string | null>(null) let currentPasswordHash = $state<string | null>(null)
const net = () => networks.find((n) => n.id === network)!
async function hashPassword( async function hashPassword(
password: string, password: string,
salt?: string, salt?: string,
@@ -111,25 +75,8 @@ export const EvmWallet = () => {
const init = async () => { const init = async () => {
if (!electrobun.rpc) return 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( const [walletList] = await tryCatch(
electrobun.rpc.request.evmListWallets({}), electrobun.rpc.request.xrpListWallets({}),
) )
if (walletList) { if (walletList) {
wallets = walletList wallets = walletList
@@ -137,28 +84,13 @@ export const EvmWallet = () => {
currentWalletId = walletList[0].id 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() await checkBiometric()
ready = true ready = true
} }
const login = async () => { const login = async () => {
accountType = 'multi' accountType = 'xrp'
if (seed) await switchNetwork(network) if (seed) await refresh()
} }
const logout = async () => { const logout = async () => {
@@ -170,27 +102,25 @@ export const EvmWallet = () => {
seed = '' seed = ''
address = '' address = ''
balance = '0' balance = '0'
tokenBalances = [] funded = true
transactions = [] transactions = []
error = '' error = ''
currentPasswordHash = null currentPasswordHash = null
electrobun.rpc?.request.setAutoSync(null)
} }
const unlockWallet = async (walletId: string) => { const loadSeedForWallet = async (walletId: string) => {
if (!electrobun.rpc) return false if (!electrobun.rpc) return false
const wallet = wallets.find((w) => w.id === walletId) const wallet = wallets.find((w) => w.id === walletId)
if (!wallet) return false if (!wallet) return false
currentWalletId = walletId
const [vaultSeed] = await tryCatch( const [vaultSeed] = await tryCatch(
electrobun.rpc.request.evmGetSeed({ electrobun.rpc.request.xrpGetSeed({
vaultKey: wallet.vaultKey, vaultKey: wallet.vaultKey,
}), }),
) )
if (!vaultSeed) return false if (!vaultSeed) return false
seed = vaultSeed seed = vaultSeed
address = wallet.address
await refresh() await refresh()
await setAutoSync()
return true return true
} }
@@ -203,8 +133,7 @@ export const EvmWallet = () => {
) )
if (!authed) return false if (!authed) return false
if (!currentWalletId) return false if (!currentWalletId) return false
await loadSeedForWallet(currentWalletId) return loadSeedForWallet(currentWalletId)
return !!seed
} }
const unlockWithPassword = async (password: string) => { 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) => { const selectWallet = async (walletId: string) => {
currentWalletId = walletId currentWalletId = walletId
clearWallet() clearWallet()
@@ -244,7 +157,7 @@ export const EvmWallet = () => {
const [ph] = await tryCatch( const [ph] = await tryCatch(
electrobun.rpc.request.getSecret({ electrobun.rpc.request.getSecret({
service: 'koins', service: 'koins',
name: `evm_auth_${walletId}`, name: `xrp_auth_${walletId}`,
}), }),
) )
if (ph) { if (ph) {
@@ -260,61 +173,89 @@ export const EvmWallet = () => {
if (!currentPasswordHash && biometricAvailable) { if (!currentPasswordHash && biometricAvailable) {
return unlockWithBiometrics() return unlockWithBiometrics()
} }
if (!currentPasswordHash) {
return loadSeedForWallet(walletId)
}
return false return false
} }
const createWallet = async ( const saveWallet = async (
kind: 'create' | 'import',
name: string, name: string,
phrase: string, secret: string | undefined,
password?: string, password?: string,
) => { ) => {
loading = true loading = true
error = '' error = ''
try { try {
mnemonicToAccount(phrase.trim()) if (!electrobun.rpc) throw new Error('RPC not available')
let passwordHash: string | undefined let passwordHash: string | undefined
if (password) { if (password) {
const ph = await hashPassword(password) const ph = await hashPassword(password)
passwordHash = JSON.stringify(ph) passwordHash = JSON.stringify(ph)
} }
const [result, resultError] = await tryCatch( const result: {
electrobun.rpc!.request.evmCreateWallet({ id: string
name, address: string
phrase: phrase.trim(), createdAt: string
passwordHash, seed?: string
}), } =
) kind === 'create'
if (resultError) throw resultError ? await electrobun.rpc.request.xrpCreateWallet({
name,
hasPassword: !!passwordHash,
})
: await electrobun.rpc.request.xrpImportWallet({
name,
secret: secret!,
hasPassword: !!passwordHash,
})
if (passwordHash) { if (passwordHash) {
await electrobun.rpc?.request.setSecret({ await electrobun.rpc.request.setSecret({
service: 'koins', service: 'koins',
name: `evm_auth_${result.id}`, name: `xrp_auth_${result.id}`,
value: passwordHash, value: passwordHash,
}) })
} }
const newWallet: EvmWalletInfo = { const newWallet: XrpWalletInfo = {
id: result.id, id: result.id,
name, name,
address: result.address,
hasPassword: !!passwordHash, hasPassword: !!passwordHash,
vaultKey: `evm_seed_${result.id}`, vaultKey: `xrp_seed_${result.id}`,
createdAt: result.createdAt, createdAt: result.createdAt,
} }
wallets = [...wallets, newWallet] wallets = [...wallets, newWallet]
currentWalletId = result.id currentWalletId = result.id
if (passwordHash) currentPasswordHash = passwordHash if (passwordHash) currentPasswordHash = passwordHash
seed = phrase.trim() seed = result.seed ?? secret!
address = result.address
await refresh() await refresh()
return result.seed
} catch (e) { } catch (e) {
error = e instanceof Error ? e.message : 'Invalid seed phrase' error = e instanceof Error ? e.message : 'Failed to save wallet'
throw e
} finally { } finally {
loading = false 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) => { 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) wallets = wallets.filter((w) => w.id !== walletId)
if (currentWalletId === walletId) { if (currentWalletId === walletId) {
clearWallet() clearWallet()
@@ -327,103 +268,49 @@ export const EvmWallet = () => {
currentWalletId = null currentWalletId = null
} }
const switchNetwork = async (id: NetworkId) => {
network = id
if (seed) await refresh()
}
const refresh = async () => { const refresh = async () => {
if (!seed || !electrobun.rpc) return if (!address || !electrobun.rpc) return
const rpc = electrobun.rpc
loadingBalance = true loadingBalance = true
loadingTransactions = true loadingTransactions = true
balance = '0'
error = '' error = ''
const account = mnemonicToAccount(seed)
address = account.address
const client = createPublicClient({ transport: http(net().rpc) })
syncTxHistory()
;(async () => { ;(async () => {
const [nativeBal] = await tryCatch( const [bal] = await tryCatch(
client.getBalance({ address: account.address }), rpc.request.xrpGetBalance({ address }),
) )
if (nativeBal !== undefined) balance = formatEther(nativeBal) if (bal) {
const [bals] = await tryCatch( balance = bal.balance
electrobun.rpc.request.fetchTokenBalances({ funded = bal.funded
address, }
chainid: net().chainid, const [p] = await tryCatch(rpc.request.fetchXrpPrice({}))
}), if (p) price = p.usd
) const [f] = await tryCatch(rpc.request.xrpGetFee({}))
if (bals) tokenBalances = bals if (f) fee = f.fee
loadingBalance = false loadingBalance = false
})() })()
;(async () => { ;(async () => {
await fetchTxHistory() const [txs] = await tryCatch(
rpc.request.xrpGetTransactions({ address }),
)
if (txs) transactions = txs
loadingTransactions = false 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 ( const send = async (
to: string, to: string,
amount: string, amount: string,
contractAddress?: string, destinationTag?: number,
tokenDecimals?: number,
) => { ) => {
if (!electrobun.rpc || !seed) throw new Error('Wallet not unlocked') if (!electrobun.rpc || !seed) throw new Error('Wallet not unlocked')
const hash = await electrobun.rpc.request.evmSendTransfer({ const result = await electrobun.rpc.request.xrpSend({
seed, secret: seed,
to, to,
amount, amount,
chainid: net().chainid, destinationTag,
contractAddress,
tokenDecimals,
}) })
await refresh() await refresh()
return hash return result
} }
const lock = () => { const lock = () => {
@@ -432,7 +319,6 @@ export const EvmWallet = () => {
const reset = () => { const reset = () => {
clearWallet() clearWallet()
apiKey = ''
wallets = [] wallets = []
currentWalletId = null currentWalletId = null
} }
@@ -464,34 +350,14 @@ export const EvmWallet = () => {
get balance() { get balance() {
return balance return balance
}, },
get tokenBalances() { get funded() {
return tokenBalances return funded
}, },
get network() { get price() {
return network return price
}, },
get chainid() { get fee() {
return net().chainid return fee
},
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 transactions() { get transactions() {
return transactions return transactions
@@ -526,8 +392,8 @@ export const EvmWallet = () => {
get currentPasswordHash() { get currentPasswordHash() {
return currentPasswordHash return currentPasswordHash
}, },
set apiKey(v: string) { get explorerAddressUrl() {
apiKey = v return address ? `${xrpNetwork.explorerAddressUrl}${address}` : ''
}, },
set error(v: string) { set error(v: string) {
error = v error = v
@@ -537,9 +403,7 @@ export const EvmWallet = () => {
logout, logout,
refresh, refresh,
createWallet, createWallet,
switchNetwork, importWallet,
saveApiKey,
fetchTxHistory,
lock, lock,
reset, reset,
resetApp, resetApp,
@@ -548,14 +412,11 @@ export const EvmWallet = () => {
hashPassword, hashPassword,
selectWallet, selectWallet,
selectAndUnlockWallet, selectAndUnlockWallet,
unlockWallet, loadSeedForWallet,
deleteWallet, deleteWallet,
clearSelection, clearSelection,
send, send,
syncTxHistory,
setAutoSync,
flushTxCache,
} }
} }
export const evmWallet = EvmWallet() export const xrpWallet = XrpWallet()
+48 -61
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { evmWallet as wallet } from '$lib/states/evm-wallet.svelte.js'
import { moneroWallet } from '$lib/states/monero-wallet.svelte.js' import { moneroWallet } from '$lib/states/monero-wallet.svelte.js'
import { xrpWallet } from '$lib/states/xrp-wallet.svelte.js'
import { Button } from '$lib/components/ui/button/index.js' import { Button } from '$lib/components/ui/button/index.js'
import { import {
Card, Card,
@@ -13,14 +13,20 @@
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { frontendLog, frontendLogError } from '$lib/electrobun.js' import { frontendLog, frontendLogError } from '$lib/electrobun.js'
const w = wallet
let feError = $state<string | null>(null) let feError = $state<string | null>(null)
onMount(() => { onMount(() => {
frontendLog('index.svelte mounted') frontendLog('index.svelte mounted')
w.init().catch((e) => frontendLogError('w.init() failed', e))
moneroWallet.init().catch((e) => frontendLogError('moneroWallet.init() failed', e)) moneroWallet.init().catch((e) => frontendLogError('moneroWallet.init() failed', e))
xrpWallet.init().catch((e) => frontendLogError('xrpWallet.init() failed', e))
})
$effect(() => {
if (moneroWallet.accountType === 'monero') {
navigate('/monero')
} else if (xrpWallet.accountType === 'xrp') {
navigate('/xrp')
}
}) })
$effect(() => { $effect(() => {
@@ -56,61 +62,42 @@
{/if} {/if}
<div class="mx-auto mt-24 max-w-sm"> <div class="mx-auto mt-24 max-w-sm">
{#if !w.ready} <Card>
<p class="text-center text-muted-foreground text-sm"> <CardHeader class="text-center">
Loading... <CardTitle>Welcome</CardTitle>
</p> <CardDescription>
{:else if w.accountType} Choose an account type to get started
{#if w.accountType === 'multi'} </CardDescription>
{navigate('/multicoin')} </CardHeader>
{:else} <CardContent>
{navigate('/monero')} <div class="flex flex-col gap-3">
{/if} <Button
{:else} onclick={async () => {
<Card> await moneroWallet.login()
<CardHeader class="text-center"> navigate('/monero')
<CardTitle>Welcome</CardTitle> }}
<CardDescription> variant="outline"
Choose an account type to get started class="w-full">
</CardDescription> <img
</CardHeader> src="/icons/monero.png"
<CardContent> alt=""
<div class="flex flex-col gap-3"> class="-ml-1 mr-1.5 inline-block size-5 rounded-full align-middle" />
<Button Monero
onclick={async () => { </Button>
await w.login() <Button
navigate('/multicoin') onclick={async () => {
}} await xrpWallet.login()
class="w-full"> navigate('/xrp')
<span class="-ml-1 mr-2 flex items-center"> }}
<img variant="outline"
src="/icons/ethereum.png" class="w-full">
alt="" <img
class="relative z-30 size-5 rounded-full ring-2 ring-accent" /> src="/icons/xrp.png"
<img alt=""
src="/icons/binance.png" class="-ml-1 mr-1.5 inline-block size-5 rounded-full align-middle" />
alt="" XRP
class="-ml-1.5 relative z-20 size-5 rounded-full ring-2 ring-accent" /> </Button>
<img </div>
src="/icons/polygon.png" </CardContent>
alt="" </Card>
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}
</div> </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"> <script lang="ts">
import { evmWallet as wallet } from '$lib/states/evm-wallet.svelte.js' import { xrpWallet } from '$lib/states/xrp-wallet.svelte.js'
import { electrobun } from '$lib/electrobun.js'
import { Button } from '$lib/components/ui/button/index.js' import { Button } from '$lib/components/ui/button/index.js'
import { Input } from '$lib/components/ui/input/index.js'
import { import {
Card, Card,
CardContent, CardContent,
@@ -12,19 +10,13 @@
} from '$lib/components/ui/card/index.js' } from '$lib/components/ui/card/index.js'
import { navigate } from 'sv-router/generated' import { navigate } from 'sv-router/generated'
import ArrowLeft from '@lucide/svelte/icons/arrow-left' 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 Trash2 from '@lucide/svelte/icons/trash-2'
import WalletIcon from '@lucide/svelte/icons/wallet' import WalletIcon from '@lucide/svelte/icons/wallet'
const w = wallet const w = xrpWallet
let confirm = $state(false) let confirm = $state(false)
let resetting = $state(false) let resetting = $state(false)
let deletingWalletId = $state<string | null>(null) let deletingWalletId = $state<string | null>(null)
let flushing = $state(false)
let seedRevealed = $state(false)
let seedPassword = $state('')
let seedError = $state('')
async function handleReset() { async function handleReset() {
resetting = true resetting = true
@@ -37,20 +29,22 @@
async function handleDeleteWallet(id: string) { async function handleDeleteWallet(id: string) {
await w.deleteWallet(id) await w.deleteWallet(id)
deletingWalletId = null deletingWalletId = null
if (w.wallets.length === 0) navigate('/multicoin') if (w.wallets.length === 0) navigate('/xrp')
}
async function handleFlushCache() {
flushing = true
await w.flushTxCache()
flushing = false
} }
</script> </script>
<div class="mx-auto mt-16 max-w-md"> <div class="mx-auto mt-16 max-w-md">
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="flex items-center gap-2"> <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} /> <ArrowLeft size={16} />
Back Back
</Button> </Button>
@@ -59,7 +53,7 @@
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Wallets</CardTitle> <CardTitle>Wallets</CardTitle>
<CardDescription>Manage your EVM wallets</CardDescription> <CardDescription>Manage your XRP wallets</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@@ -68,8 +62,8 @@
<WalletIcon size={18} class="shrink-0 text-muted-foreground" /> <WalletIcon size={18} class="shrink-0 text-muted-foreground" />
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<p class="font-medium text-sm">{wal.name}</p> <p class="font-medium text-sm">{wal.name}</p>
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground truncate font-mono">
Created {new Date(wal.createdAt).toLocaleDateString()} {wal.address}
</p> </p>
</div> </div>
{#if deletingWalletId === wal.id} {#if deletingWalletId === wal.id}
@@ -92,83 +86,17 @@
</CardContent> </CardContent>
</Card> </Card>
{/if} {/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> <Card>
<CardHeader> <CardHeader>
<CardTitle>Settings</CardTitle> <CardTitle>Settings</CardTitle>
<CardDescription>App preferences and data management</CardDescription> <CardDescription>App preferences and data management</CardDescription>
</CardHeader> </CardHeader>
<CardContent class="space-y-4"> <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> <div>
<p class="font-medium text-xs">Reset App</p> <p class="font-medium text-xs">Reset App</p>
<p class="text-muted-foreground text-xs mt-1"> <p class="text-muted-foreground text-xs mt-1">
Removes your seed, API key, and all cached data from the keychain Removes all XRP wallets and seeds from the keychain. You
and database. You can then set up the wallet fresh. can then set up the wallet fresh.
</p> </p>
<div class="mt-3"> <div class="mt-3">
{#if confirm} {#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": { "apps/desktop": {
"name": "koins", "name": "koins",
"dependencies": { "dependencies": {
"@alchemy/common": "^5.0.0",
"@alchemy/wallet-apis": "^5.0.0",
"@koins/utils": "workspace:*", "@koins/utils": "workspace:*",
"d3-scale": "^4.0.2", "d3-scale": "^4.0.2",
"d3-shape": "^3.2.0", "d3-shape": "^3.2.0",
"drizzle-orm": "^1.0.0-beta.22",
"electrobun": "^1.18.1", "electrobun": "^1.18.1",
"env-paths": "^4.0.0",
"mode-watcher": "^1.1.0", "mode-watcher": "^1.1.0",
"monero-ts": "^0.11.8", "monero-ts": "^0.11.8",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"sv-router": "latest", "sv-router": "latest",
"viem": "^2.48.11", "xrpl": "^5.0.0",
}, },
"devDependencies": { "devDependencies": {
"@fontsource-variable/geist-mono": "^5.2.7", "@fontsource-variable/geist-mono": "^5.2.7",
@@ -42,7 +40,6 @@
"bun-types": "^1.3.14", "bun-types": "^1.3.14",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"drizzle-kit": "^1.0.0-beta.22",
"embla-carousel-svelte": "^8.6.0", "embla-carousel-svelte": "^8.6.0",
"formsnap": "^2.0.1", "formsnap": "^2.0.1",
"layerchart": "2.0.0-next.48", "layerchart": "2.0.0-next.48",
@@ -75,14 +72,6 @@
}, },
}, },
"packages": { "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/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=="], "@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=="], "@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/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/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=="], "@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=="], "@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=="], "@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"], "@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=="], "@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=="], "@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@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="],
"@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"@oxc-project/types": ["@oxc-project/types@0.95.0", "", {}, "sha512-vACy7vhpMPhjEJhULNxrdR0D943TkA/MigMpJCHmBHvMXxRStRi/dPtTlfQ3uDwWSzRpT8z+7ImjZVf8JWBocQ=="], "@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=="], "@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=="], "@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=="], "@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=="], "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=="], "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-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=="], "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=="], "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=="], "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=="], "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
"basic-ftp": ["basic-ftp@5.3.1", "", {}, "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw=="], "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=="], "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=="], "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=="], "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-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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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-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-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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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-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=="], "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=="], "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-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-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=="], "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=="], "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=="], "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
"ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], "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-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-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-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-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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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-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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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-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=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], "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-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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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": ["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-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=="], "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=="], "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=="], "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/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=="], "@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=="], "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/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=="], "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=="], "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=="], "@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=="], "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=="], "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/@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=="], "vite/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.18", "", { "os": "android", "cpu": "arm64" }, "sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ=="],