Compare commits
No commits in common. "master" and "migrate/slint" have entirely different histories.
master
...
migrate/sl
127 changed files with 15172 additions and 21593 deletions
20
.changelogrc
20
.changelogrc
|
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"types": {
|
|
||||||
"feat": { "title": "🚀 Enhancements", "semver": "minor" },
|
|
||||||
"fix": { "title": "🐛 Bug Fixes", "semver": "patch" },
|
|
||||||
"docs": { "title": "📖 Documentation", "semver": "patch" },
|
|
||||||
"style": { "title": "💄 Styles", "semver": "patch" },
|
|
||||||
"refactor": { "title": "♻️ Refactors", "semver": "patch" },
|
|
||||||
"perf": { "title": "⚡ Performance", "semver": "patch" },
|
|
||||||
"test": { "title": "✅ Tests", "semver": "patch" },
|
|
||||||
"build": { "title": "🏗️ Build System", "semver": "patch" },
|
|
||||||
"ci": { "title": "🤖 CI/CD", "semver": "patch" },
|
|
||||||
"chore": { "title": "🧹 Chores", "semver": "patch" },
|
|
||||||
"revert": { "title": "⏪ Reverts", "semver": "patch" }
|
|
||||||
},
|
|
||||||
"excludeAuthors": ["dependabot[bot]", "renovate[bot]"],
|
|
||||||
"github": {
|
|
||||||
"repo": "rizilab/bismillahdao",
|
|
||||||
"token": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
import { MakerDeb } from '@electron-forge/maker-deb'
|
|
||||||
import { MakerDMG } from '@electron-forge/maker-dmg'
|
|
||||||
import { MakerSquirrel } from '@electron-forge/maker-squirrel'
|
|
||||||
import { MakerZIP } from '@electron-forge/maker-zip'
|
|
||||||
import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives'
|
|
||||||
import { FusesPlugin } from '@electron-forge/plugin-fuses'
|
|
||||||
import { VitePlugin } from '@electron-forge/plugin-vite'
|
|
||||||
import { PublisherGithub } from '@electron-forge/publisher-github'
|
|
||||||
import type { ForgeConfig } from '@electron-forge/shared-types'
|
|
||||||
import { FuseV1Options, FuseVersion } from '@electron/fuses'
|
|
||||||
import setLanguages from 'electron-packager-languages'
|
|
||||||
import packageJSON from '../package.json'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
packagerConfig: {
|
|
||||||
name: packageJSON.name,
|
|
||||||
appBundleId: 'com.bismillahdao.ziya',
|
|
||||||
appCategoryType: 'public.app-category.utilities',
|
|
||||||
appCopyright: `Copyright (C) ${new Date().getFullYear()} ${packageJSON.author.name}`,
|
|
||||||
icon: 'public/favicon',
|
|
||||||
asar: {
|
|
||||||
unpack: '**/node_modules/{sharp,@img}/**/*',
|
|
||||||
},
|
|
||||||
osxSign: {},
|
|
||||||
ignore: [
|
|
||||||
/^\/(?!node_modules|package\.json|.vite)/,
|
|
||||||
],
|
|
||||||
afterCopy: [setLanguages(['en', 'en-US', 'en-GB'])],
|
|
||||||
},
|
|
||||||
rebuildConfig: {
|
|
||||||
onlyModules: ['sharp'],
|
|
||||||
force: true,
|
|
||||||
},
|
|
||||||
makers: [
|
|
||||||
new MakerZIP({}),
|
|
||||||
// Windows
|
|
||||||
new MakerSquirrel({
|
|
||||||
usePackageJson: true,
|
|
||||||
iconUrl: 'https://raw.githubusercontent.com/rizilab/ziya/main/public/favicon.ico',
|
|
||||||
setupIcon: 'public/favicon.ico',
|
|
||||||
}),
|
|
||||||
// macOS
|
|
||||||
new MakerDMG({
|
|
||||||
overwrite: true,
|
|
||||||
format: 'ULFO',
|
|
||||||
icon: 'public/favicon.icns',
|
|
||||||
}),
|
|
||||||
// Linux
|
|
||||||
new MakerDeb({
|
|
||||||
options: {
|
|
||||||
categories: ['Utility'],
|
|
||||||
icon: 'public/favicon.png',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
new VitePlugin({
|
|
||||||
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
|
|
||||||
// If you are familiar with Vite configuration, it will look really familiar.
|
|
||||||
build: [
|
|
||||||
{
|
|
||||||
entry: 'electron/main.ts',
|
|
||||||
config: '.config/vite.forge.ts',
|
|
||||||
target: 'main',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
entry: 'electron/preload.ts',
|
|
||||||
config: '.config/vite.forge.ts',
|
|
||||||
target: 'preload',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
renderer: [], // Nuxt app is generated no need to specify renderer
|
|
||||||
}),
|
|
||||||
// Fuses are used to enable/disable various Electron functionality
|
|
||||||
// at package time, before code signing the application
|
|
||||||
new FusesPlugin({
|
|
||||||
version: FuseVersion.V1,
|
|
||||||
[FuseV1Options.RunAsNode]: false,
|
|
||||||
[FuseV1Options.EnableCookieEncryption]: true,
|
|
||||||
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
|
|
||||||
[FuseV1Options.EnableNodeCliInspectArguments]: false,
|
|
||||||
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
|
|
||||||
[FuseV1Options.OnlyLoadAppFromAsar]: true,
|
|
||||||
}),
|
|
||||||
new AutoUnpackNativesPlugin({}),
|
|
||||||
],
|
|
||||||
publishers: [
|
|
||||||
new PublisherGithub({
|
|
||||||
repository: {
|
|
||||||
owner: 'Rizary',
|
|
||||||
name: packageJSON.name,
|
|
||||||
},
|
|
||||||
prerelease: true,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
} satisfies ForgeConfig
|
|
||||||
108
.config/nuxt.ts
108
.config/nuxt.ts
|
|
@ -1,108 +0,0 @@
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
|
||||||
import { defineNuxtConfig } from 'nuxt/config'
|
|
||||||
import { getConfig } from '../app.config'
|
|
||||||
|
|
||||||
const config = getConfig()
|
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
|
||||||
modules: [
|
|
||||||
'@nuxt/eslint',
|
|
||||||
'@pinia/nuxt',
|
|
||||||
'@nuxt/icon',
|
|
||||||
],
|
|
||||||
ssr: false,
|
|
||||||
devtools: { enabled: true },
|
|
||||||
|
|
||||||
devServer: {
|
|
||||||
port: config.development.nuxt.port,
|
|
||||||
host: config.development.nuxt.host,
|
|
||||||
},
|
|
||||||
|
|
||||||
runtimeConfig: {
|
|
||||||
// Private keys (only available on server-side)
|
|
||||||
redis: {
|
|
||||||
host: config.redis.host,
|
|
||||||
port: config.redis.port,
|
|
||||||
db: config.redis.db,
|
|
||||||
keyPrefix: config.redis.keyPrefix,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Public keys (exposed to client-side)
|
|
||||||
public: {
|
|
||||||
app: {
|
|
||||||
name: config.app.name,
|
|
||||||
version: config.app.version,
|
|
||||||
description: config.app.description,
|
|
||||||
author: config.app.author,
|
|
||||||
},
|
|
||||||
window: config.window,
|
|
||||||
theme: config.theme,
|
|
||||||
isDevelopment: process.env.NODE_ENV === 'development',
|
|
||||||
isElectron: process.env.IS_ELECTRON === 'true',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
app: {
|
|
||||||
baseURL: './',
|
|
||||||
cdnURL: './',
|
|
||||||
head: {
|
|
||||||
title: config.app.name,
|
|
||||||
meta: [
|
|
||||||
{ name: 'description', content: config.app.description },
|
|
||||||
{
|
|
||||||
'http-equiv': 'content-security-policy',
|
|
||||||
'content': `script-src ${config.security.csp.scriptSrc.join(' ')}; style-src ${config.security.csp.styleSrc.join(' ')}; img-src ${config.security.csp.imgSrc.join(' ')}`
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
css: [
|
|
||||||
'~/assets/css/main.css',
|
|
||||||
],
|
|
||||||
|
|
||||||
vite: {
|
|
||||||
plugins: [
|
|
||||||
tailwindcss(),
|
|
||||||
],
|
|
||||||
server: {
|
|
||||||
watch: {
|
|
||||||
ignored: ['./docker-data/*'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
postcss: {
|
|
||||||
plugins: {
|
|
||||||
'@tailwindcss/postcss': {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
router: {
|
|
||||||
options: {
|
|
||||||
hashMode: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
typescript: {
|
|
||||||
typeCheck: false,
|
|
||||||
includeWorkspace: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
imports: {
|
|
||||||
dirs: [
|
|
||||||
'composables/**',
|
|
||||||
'stores/**'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
future: { compatibilityVersion: 4 },
|
|
||||||
features: {
|
|
||||||
inlineStyles: false,
|
|
||||||
},
|
|
||||||
experimental: {
|
|
||||||
typedPages: true,
|
|
||||||
payloadExtraction: false,
|
|
||||||
renderJsonPayloads: false,
|
|
||||||
},
|
|
||||||
compatibilityDate: '2025-05-26',
|
|
||||||
})
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "stylelint-config-standard-scss",
|
|
||||||
"rules": {
|
|
||||||
"length-zero-no-unit": true,
|
|
||||||
"rule-empty-line-before": ["always-multi-line", { "except": ["first-nested"] }],
|
|
||||||
"color-function-notation": ["modern", { "ignore": ["with-var-inside"] }],
|
|
||||||
"scss/double-slash-comment-empty-line-before": "never"
|
|
||||||
},
|
|
||||||
"ignoreFiles": [
|
|
||||||
"../node_modules/**/*",
|
|
||||||
"../.nuxt/**/*",
|
|
||||||
"../dist/**/*",
|
|
||||||
"../.output/**/*",
|
|
||||||
"../public/**/*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
content: [
|
|
||||||
'./app/**/*.{js,ts,jsx,tsx,vue}',
|
|
||||||
'./components/**/*.{js,ts,jsx,tsx,vue}',
|
|
||||||
'./layouts/**/*.vue',
|
|
||||||
'./pages/**/*.vue',
|
|
||||||
'./plugins/**/*.{js,ts}',
|
|
||||||
'./nuxt.config.{js,ts}',
|
|
||||||
'./app.vue',
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
// Let daisyUI handle the color variables
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
// daisyUI is now configured in the CSS file using the new @plugin syntax
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
import { cp, mkdir } from 'node:fs/promises'
|
|
||||||
import { fileURLToPath } from 'node:url'
|
|
||||||
import { type Plugin, defineConfig } from 'vite'
|
|
||||||
|
|
||||||
const copyNuxtOutput: Plugin = {
|
|
||||||
name: 'copy-nuxt-output',
|
|
||||||
async closeBundle() {
|
|
||||||
const outputDir = fileURLToPath(new URL('../.output/public', import.meta.url))
|
|
||||||
const targetDir = fileURLToPath(new URL('../.vite/renderer', import.meta.url))
|
|
||||||
|
|
||||||
await mkdir(targetDir, { recursive: true })
|
|
||||||
await cp(outputDir, targetDir, { recursive: true, force: true })
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
publicDir: false,
|
|
||||||
plugins: [copyNuxtOutput],
|
|
||||||
build: {
|
|
||||||
emptyOutDir: false,
|
|
||||||
lib: {
|
|
||||||
entry: 'electron/main.ts',
|
|
||||||
formats: ['cjs'],
|
|
||||||
},
|
|
||||||
rollupOptions: {
|
|
||||||
output: {
|
|
||||||
entryFileNames: '[name].cjs',
|
|
||||||
},
|
|
||||||
external: [
|
|
||||||
'electron',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
end_of_line = lf
|
|
||||||
charset = utf-8
|
|
||||||
max_line_length = 200
|
|
||||||
|
|
||||||
// NOTE: has auto-save impact! always.
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
insert_final_newline = true
|
|
||||||
|
|
||||||
[*.{js,ts,vue,css,scss,sass,html}]
|
|
||||||
insert_final_newline = true
|
|
||||||
|
|
||||||
[*.{txt, md}]
|
|
||||||
max_line_length = off
|
|
||||||
trim_trailing_whitespace = false
|
|
||||||
insert_final_newline = false
|
|
||||||
|
|
||||||
[*.{yml, json},.prettierrc]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
1
.gitattributes
vendored
1
.gitattributes
vendored
|
|
@ -1 +0,0 @@
|
||||||
* text=auto eol=lf
|
|
||||||
89
.github/COMMIT_CONVENTION.md
vendored
89
.github/COMMIT_CONVENTION.md
vendored
|
|
@ -1,89 +0,0 @@
|
||||||
# Commit Convention Guide
|
|
||||||
|
|
||||||
This project uses [Conventional Commits](https://www.conventionalcommits.org/) with [changelogen](https://github.com/unjs/changelogen) for automatic changelog generation.
|
|
||||||
|
|
||||||
## Commit Message Format
|
|
||||||
|
|
||||||
```
|
|
||||||
<type>[optional scope]: <description>
|
|
||||||
|
|
||||||
[optional body]
|
|
||||||
|
|
||||||
[optional footer(s)]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Types
|
|
||||||
|
|
||||||
| Type | Emoji | Description | Version Bump |
|
|
||||||
|------|-------|-------------|--------------|
|
|
||||||
| `feat` | 🚀 | New features | minor |
|
|
||||||
| `fix` | 🐛 | Bug fixes | patch |
|
|
||||||
| `docs` | 📖 | Documentation changes | patch |
|
|
||||||
| `style` | 💄 | Code style changes | patch |
|
|
||||||
| `refactor` | ♻️ | Code refactoring | patch |
|
|
||||||
| `perf` | ⚡ | Performance improvements | patch |
|
|
||||||
| `test` | ✅ | Adding tests | patch |
|
|
||||||
| `build` | 🏗️ | Build system changes | patch |
|
|
||||||
| `ci` | 🤖 | CI/CD changes | patch |
|
|
||||||
| `chore` | 🧹 | Maintenance tasks | patch |
|
|
||||||
| `revert` | ⏪ | Reverting changes | patch |
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
### Feature
|
|
||||||
```bash
|
|
||||||
git commit -m "feat: add user authentication system"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Bug Fix
|
|
||||||
```bash
|
|
||||||
git commit -m "fix: resolve login validation error"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Breaking Change
|
|
||||||
```bash
|
|
||||||
git commit -m "feat: redesign API structure
|
|
||||||
|
|
||||||
BREAKING CHANGE: API endpoints have changed from /api/v1 to /api/v2"
|
|
||||||
```
|
|
||||||
|
|
||||||
### With Scope
|
|
||||||
```bash
|
|
||||||
git commit -m "feat(theme): add dark mode support"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Changelog Scripts
|
|
||||||
|
|
||||||
### Generate Changelog
|
|
||||||
```bash
|
|
||||||
pnpm run changelog
|
|
||||||
```
|
|
||||||
|
|
||||||
### Release with Changelog
|
|
||||||
```bash
|
|
||||||
pnpm run changelog:release
|
|
||||||
```
|
|
||||||
|
|
||||||
### Full Release Workflow
|
|
||||||
```bash
|
|
||||||
pnpm run release
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. **Use present tense**: "add feature" not "added feature"
|
|
||||||
2. **Use imperative mood**: "fix bug" not "fixes bug"
|
|
||||||
3. **Keep first line under 72 characters**
|
|
||||||
4. **Reference issues**: "fix: resolve login issue (#123)"
|
|
||||||
5. **Include breaking changes**: Always document breaking changes in footer
|
|
||||||
6. **Be descriptive**: Explain what and why, not how
|
|
||||||
|
|
||||||
## Scopes (Optional)
|
|
||||||
|
|
||||||
Common scopes for this project:
|
|
||||||
- `theme` - Theme system changes
|
|
||||||
- `eslint` - ESLint configuration
|
|
||||||
- `ui` - User interface components
|
|
||||||
- `auth` - Authentication system
|
|
||||||
- `electron` - Electron-specific changes
|
|
||||||
- `build` - Build system changes
|
|
||||||
113
.gitignore
vendored
113
.gitignore
vendored
|
|
@ -1,99 +1,24 @@
|
||||||
# Logs
|
target/
|
||||||
logs
|
**/target/
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
||||||
lib-cov
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage
|
|
||||||
*.lcov
|
|
||||||
|
|
||||||
# nyc test coverage
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
|
||||||
build/Release
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
node_modules/
|
|
||||||
jspm_packages/
|
|
||||||
|
|
||||||
# TypeScript v1 declaration files
|
|
||||||
typings/
|
|
||||||
|
|
||||||
# TypeScript cache
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional eslint cache
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Optional REPL history
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
# Output of 'npm pack'
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
# Yarn Integrity file
|
|
||||||
.yarn-integrity
|
|
||||||
|
|
||||||
# dotenv environment variables file
|
|
||||||
.env
|
|
||||||
.env.test
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
|
||||||
.cache
|
|
||||||
|
|
||||||
# next.js build output
|
|
||||||
.next
|
|
||||||
|
|
||||||
# nuxt.js build output
|
|
||||||
.nuxt
|
|
||||||
|
|
||||||
# vuepress build output
|
|
||||||
.vuepress/dist
|
|
||||||
|
|
||||||
# Serverless directories
|
|
||||||
.serverless/
|
|
||||||
|
|
||||||
# FuseBox cache
|
|
||||||
.fusebox/
|
|
||||||
|
|
||||||
# DynamoDB Local files
|
|
||||||
.dynamodb/
|
|
||||||
|
|
||||||
# Webpack
|
|
||||||
.webpack/
|
|
||||||
|
|
||||||
# Vite
|
|
||||||
.vite/
|
|
||||||
|
|
||||||
# Electron-Forge
|
|
||||||
out/
|
|
||||||
dist/
|
dist/
|
||||||
.output/
|
node_modules/
|
||||||
|
diff
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.prod
|
||||||
|
.logs/
|
||||||
|
.logs/**
|
||||||
|
docker.dev/**/.env
|
||||||
|
**/.env
|
||||||
|
.env.*local
|
||||||
|
|
||||||
|
*address.json
|
||||||
|
cex_database*
|
||||||
|
|
||||||
|
Config.toml
|
||||||
.cursor/
|
.cursor/
|
||||||
|
|
||||||
palettes/
|
*.crt
|
||||||
|
*.key
|
||||||
|
*.p12
|
||||||
3
.npmrc
3
.npmrc
|
|
@ -1,3 +0,0 @@
|
||||||
shamefully-hoist=true
|
|
||||||
strict-peer-dependencies=false
|
|
||||||
node-linker=hoisted
|
|
||||||
16
.prettierrc
16
.prettierrc
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"semi": false,
|
|
||||||
"useTabs": false,
|
|
||||||
"singleQuote": true,
|
|
||||||
"plugins": [
|
|
||||||
"prettier-plugin-vue"
|
|
||||||
],
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": "*.vue",
|
|
||||||
"options": {
|
|
||||||
"parser": "vue"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
8
.vscode/extensions.json
vendored
8
.vscode/extensions.json
vendored
|
|
@ -1,9 +1,7 @@
|
||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"syler.sass-indented",
|
"rust-lang.rust-analyzer",
|
||||||
"vue.volar",
|
"vadimcn.vscode-lldb",
|
||||||
"dbaeumer.vscode-eslint",
|
"Slint.slint"
|
||||||
"editorconfig.editorconfig",
|
|
||||||
"vitest.explorer"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
65
.vscode/settings.json
vendored
65
.vscode/settings.json
vendored
|
|
@ -1,66 +1,3 @@
|
||||||
{
|
{
|
||||||
"eslint.validate": [
|
"files.autoSave": "off"
|
||||||
"javascript",
|
|
||||||
"typescript",
|
|
||||||
"vue"
|
|
||||||
],
|
|
||||||
"eslint.useFlatConfig": true,
|
|
||||||
"eslint.workingDirectories": [
|
|
||||||
"."
|
|
||||||
],
|
|
||||||
"eslint.probe": [
|
|
||||||
"javascript",
|
|
||||||
"typescript",
|
|
||||||
"html",
|
|
||||||
"vue"
|
|
||||||
],
|
|
||||||
"search.exclude": {
|
|
||||||
"**/node_modules": true,
|
|
||||||
"**/.vite": true,
|
|
||||||
"**/dist": true,
|
|
||||||
"**/build": true
|
|
||||||
},
|
|
||||||
"[javascript]": {
|
|
||||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.fixAll.eslint": "explicit"
|
|
||||||
},
|
|
||||||
"javascript.preferences.quoteStyle": "single"
|
|
||||||
},
|
|
||||||
"[typescript]": {
|
|
||||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.fixAll.eslint": "explicit"
|
|
||||||
},
|
|
||||||
"typescript.preferences.quoteStyle": "single",
|
|
||||||
"typescript.preferences.organizeImports": "off"
|
|
||||||
},
|
|
||||||
"[vue]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.fixAll.eslint": "explicit"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"files.associations": {
|
|
||||||
"*.css": "tailwindcss"
|
|
||||||
},
|
|
||||||
"[json]": {
|
|
||||||
"editor.defaultFormatter": "vscode.json-language-features",
|
|
||||||
"editor.insertSpaces": true,
|
|
||||||
"editor.tabSize": 2
|
|
||||||
},
|
|
||||||
"[jsonc]": {
|
|
||||||
"editor.defaultFormatter": "vscode.json-language-features",
|
|
||||||
"editor.insertSpaces": true,
|
|
||||||
"editor.tabSize": 2
|
|
||||||
},
|
|
||||||
"eslint.format.enable": true,
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"typescript.preferences.includePackageJsonAutoImports": "auto",
|
|
||||||
"typescript.suggest.autoImports": true,
|
|
||||||
"typescript.updateImportsOnFileMove.enabled": "always",
|
|
||||||
"typescript.workspaceSymbols.scope": "allOpenProjects",
|
|
||||||
}
|
}
|
||||||
99
CHANGELOG.md
99
CHANGELOG.md
|
|
@ -1,99 +0,0 @@
|
||||||
# Changelog
|
|
||||||
|
|
||||||
## v0.2.0
|
|
||||||
|
|
||||||
[compare changes](https://ssh.rizilab.com/rizary/Ziya/compare/v0.1.2...v0.2.0)
|
|
||||||
|
|
||||||
### 🚀 Enhancements
|
|
||||||
|
|
||||||
- ⚠️ Implement CEX analysis cards and real-time token monitoring (67fb3a2)
|
|
||||||
|
|
||||||
### 📖 Documentation
|
|
||||||
|
|
||||||
- Adding important notes on versioning (3fdcccf)
|
|
||||||
|
|
||||||
#### ⚠️ Breaking Changes
|
|
||||||
|
|
||||||
- ⚠️ Implement CEX analysis cards and real-time token monitoring (67fb3a2)
|
|
||||||
|
|
||||||
### ❤️ Contributors
|
|
||||||
|
|
||||||
- Rizary <rizary@rizilab.com>
|
|
||||||
|
|
||||||
## v0.1.2
|
|
||||||
|
|
||||||
[compare changes](https://ssh.rizilab.com/rizary/Ziya/compare/v0.1.0...v0.1.2)
|
|
||||||
|
|
||||||
### 📖 Documentation
|
|
||||||
|
|
||||||
- Update package description and changelog (e7f74d9)
|
|
||||||
|
|
||||||
### 🏡 Chore
|
|
||||||
|
|
||||||
- Update versioning to start from 0.1.0 (451a8b6)
|
|
||||||
|
|
||||||
### ❤️ Contributors
|
|
||||||
|
|
||||||
- Rizary <rizary@rizilab.com>
|
|
||||||
|
|
||||||
## v0.1.1
|
|
||||||
|
|
||||||
[compare changes](https://ssh.rizilab.com/rizary/Ziya/compare/v0.1.0...v0.1.1)
|
|
||||||
|
|
||||||
### 📖 Documentation
|
|
||||||
|
|
||||||
- Update package description and changelog (e7f74d9)
|
|
||||||
|
|
||||||
### 🏡 Chore
|
|
||||||
|
|
||||||
- Update versioning to start from 0.1.0 (451a8b6)
|
|
||||||
|
|
||||||
### ❤️ Contributors
|
|
||||||
|
|
||||||
- Rizary <rizary@rizilab.com>
|
|
||||||
|
|
||||||
## v0.1.0...fix/electron-vue-ui-state
|
|
||||||
|
|
||||||
[compare changes](https://ssh.rizilab.com/rizary/Ziya/compare/v0.1.0...fix/electron-vue-ui-state)
|
|
||||||
|
|
||||||
### 🏡 Chore
|
|
||||||
|
|
||||||
- Update versioning to start from 0.1.0 (451a8b6)
|
|
||||||
|
|
||||||
### ❤️ Contributors
|
|
||||||
|
|
||||||
- Rizary <rizary@rizilab.com>
|
|
||||||
|
|
||||||
## v0.1.0 (2025-01-26)
|
|
||||||
|
|
||||||
### 🚀 Enhancements
|
|
||||||
|
|
||||||
- ⚠️ Complete ESLint configuration overhaul and theme system improvements (6efcf43)
|
|
||||||
- Migrate from legacy .eslintrc.json to modern flat config system
|
|
||||||
- Remove conflicting ESLint configuration files
|
|
||||||
- Fix auto-generation of eslint.config.mjs by Nuxt
|
|
||||||
- Update ESLint rules to use single quotes and proper formatting
|
|
||||||
- Add comprehensive theme switching system with 24 palettes
|
|
||||||
- Implement proper daisyUI theme integration
|
|
||||||
- Add theme store with persistence and dark/light mode support
|
|
||||||
- Create ThemeSwitcher component with enhanced UI
|
|
||||||
- Fix package.json scripts to work with new ESLint flat config
|
|
||||||
- Update VS Code settings for proper ESLint integration
|
|
||||||
|
|
||||||
### 📖 Documentation
|
|
||||||
|
|
||||||
- Clean up and format changelog (f6347f1)
|
|
||||||
- Add comprehensive commit convention guide (d415a7c)
|
|
||||||
- Finalize changelog format and remove duplicates (a21e60c)
|
|
||||||
|
|
||||||
### 🧹 Chore
|
|
||||||
|
|
||||||
- Add changelogen configuration and scripts (e6b817b)
|
|
||||||
|
|
||||||
#### ⚠️ Breaking Changes
|
|
||||||
|
|
||||||
- **ESLint configuration migrated to flat config system**
|
|
||||||
|
|
||||||
### ❤️ Contributors
|
|
||||||
|
|
||||||
- Rizary <rizary@rizilab.com>
|
|
||||||
453
CONTRIBUTING.md
Normal file
453
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,453 @@
|
||||||
|
# Contributing to Ziya-Slint
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to Ziya-Slint! This guide outlines our development approach, coding standards, and best practices for the desktop trading application.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Ziya-Slint is a desktop trading application built with Rust and Slint UI framework, designed for cryptocurrency trading with real-time market data and portfolio management.
|
||||||
|
|
||||||
|
## Development Philosophy
|
||||||
|
|
||||||
|
We follow these core principles:
|
||||||
|
|
||||||
|
1. **KISS Principle** - Keep code simple and straightforward
|
||||||
|
2. **Explicit Error Handling** - Never use dummy types or unfinished code
|
||||||
|
3. **Real-world Ready** - All code should be production-ready
|
||||||
|
4. **Async-first** - Prefer tokio over std for async operations
|
||||||
|
5. **Type Safety** - Leverage Rust's type system for correctness
|
||||||
|
6. **Desktop UX** - Native desktop app behavior and performance
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Rust 2024 Edition (see `rust-toolchain.toml`)
|
||||||
|
- Just command runner (for development tasks)
|
||||||
|
- Redis (for data caching)
|
||||||
|
- Git and Git-cliff (for development workflow)
|
||||||
|
|
||||||
|
### Initial Setup
|
||||||
|
|
||||||
|
1. Clone the repository
|
||||||
|
2. Copy configuration files:
|
||||||
|
```bash
|
||||||
|
cp Config.example.toml Config.toml
|
||||||
|
```
|
||||||
|
3. Install development dependencies:
|
||||||
|
```bash
|
||||||
|
just install-deps
|
||||||
|
```
|
||||||
|
4. Verify setup:
|
||||||
|
```bash
|
||||||
|
just test
|
||||||
|
just clippy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Workflow
|
||||||
|
|
||||||
|
We use `just` for common development tasks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all available commands
|
||||||
|
just
|
||||||
|
|
||||||
|
# Development with hot reloading
|
||||||
|
just dev
|
||||||
|
|
||||||
|
# Building
|
||||||
|
just build # Build with dev features
|
||||||
|
just build-prod # Build with prod features
|
||||||
|
|
||||||
|
# Quality assurance
|
||||||
|
just test # Run tests
|
||||||
|
just clippy # Run linter
|
||||||
|
just fmt # Format code
|
||||||
|
just check # Check code for errors
|
||||||
|
just clean # Clean build artifacts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coding Standards
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
We use strict error handling patterns. **Never** use:
|
||||||
|
- Dummy types or results
|
||||||
|
- Placeholder implementations
|
||||||
|
- TODO comments in production code
|
||||||
|
|
||||||
|
**Always** use our error handling pattern:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use crate::err_with_loc;
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
|
// Import the Result type
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
|
// Use map_err before ? operator
|
||||||
|
let result = some_operation()
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("operation_failed: {}", e);
|
||||||
|
err_with_loc!(AppError::OperationFailed(format!("operation_failed: {}", e)))
|
||||||
|
})?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Async Programming
|
||||||
|
|
||||||
|
- **Use tokio** instead of std for async operations
|
||||||
|
- Follow the actors pattern from [this guide](https://ryhl.io/blog/actors-with-tokio/)
|
||||||
|
- **Never** use `tokio::spawn` with UI components directly
|
||||||
|
- **Always** use `slint::invoke_from_event_loop` for UI updates from background tasks
|
||||||
|
|
||||||
|
### Code Formatting
|
||||||
|
|
||||||
|
We use `rustfmt` with specific configuration (see `rustfmt.toml`):
|
||||||
|
|
||||||
|
- Max width: 120 characters
|
||||||
|
- Use field init shorthand
|
||||||
|
- Reorder imports and impl items
|
||||||
|
- Group imports by StdExternalCrate
|
||||||
|
- Prefer same line braces
|
||||||
|
|
||||||
|
Run formatting:
|
||||||
|
```bash
|
||||||
|
just fmt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture: Feature-Sliced Design (FSD)
|
||||||
|
|
||||||
|
We follow **Feature-Sliced Design (FSD)** methodology for frontend architecture:
|
||||||
|
|
||||||
|
### FSD Layer Structure
|
||||||
|
```
|
||||||
|
ui/
|
||||||
|
├── app/ # Application layer - root, global setup
|
||||||
|
├── pages/ # Page layer - complete screens
|
||||||
|
├── widgets/ # Widget layer - composite UI blocks
|
||||||
|
├── entities/ # Entity layer - business entities
|
||||||
|
├── features/ # Feature layer - user interactions
|
||||||
|
└── shared/ # Shared layer - reusable resources
|
||||||
|
├── ui/ # UI components
|
||||||
|
├── types/ # Type definitions
|
||||||
|
└── design-system/ # Theme, tokens, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
### FSD Rules & Guidelines
|
||||||
|
|
||||||
|
1. **Import Rule**: Higher layers can only import from lower layers
|
||||||
|
- ✅ `app/` → `pages/` → `widgets/` → `entities/` → `features/` → `shared/`
|
||||||
|
- ❌ Never import from higher layers
|
||||||
|
|
||||||
|
2. **Layer Responsibilities**:
|
||||||
|
- **App**: Global providers, routing, application setup
|
||||||
|
- **Pages**: Complete screens, page-level logic
|
||||||
|
- **Widgets**: Composite UI blocks (navigation, forms, cards)
|
||||||
|
- **Entities**: Business entities (token, user, market data)
|
||||||
|
- **Features**: User interactions (login, trading, portfolio management)
|
||||||
|
- **Shared**: Reusable resources (UI kit, utilities, constants)
|
||||||
|
|
||||||
|
3. **Slicing by Features**: Each feature should be self-contained
|
||||||
|
```
|
||||||
|
features/
|
||||||
|
├── authentication/
|
||||||
|
│ ├── ui/ # Login components
|
||||||
|
│ ├── model/ # Auth state
|
||||||
|
│ └── api/ # Auth API calls
|
||||||
|
└── trading/
|
||||||
|
├── ui/ # Trading components
|
||||||
|
├── model/ # Trading state
|
||||||
|
└── api/ # Trading API calls
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Component Composition**: Build from bottom-up
|
||||||
|
```slint
|
||||||
|
// shared/ui/button/
|
||||||
|
export component Button { /* base button */ }
|
||||||
|
|
||||||
|
// widgets/navigation/
|
||||||
|
import { Button } from "../../shared/ui/button/";
|
||||||
|
export component NavigationWidget {
|
||||||
|
Button { /* composed navigation */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// pages/dashboard/
|
||||||
|
import { NavigationWidget } from "../../widgets/navigation/";
|
||||||
|
export component Dashboard {
|
||||||
|
NavigationWidget { /* page composition */ }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **State Management**: Follow unidirectional data flow
|
||||||
|
- Global state in `app/` layer
|
||||||
|
- Feature state in respective `features/` slices
|
||||||
|
- Pass data down, emit events up
|
||||||
|
|
||||||
|
6. **Design System**: Centralized in `shared/design-system/`
|
||||||
|
- Theme tokens and variables
|
||||||
|
- Consistent spacing, colors, typography
|
||||||
|
- Reusable UI components
|
||||||
|
|
||||||
|
## Slint UI Development
|
||||||
|
|
||||||
|
### Threading & State Management
|
||||||
|
|
||||||
|
**Critical Pattern for Slint + Tokio:**
|
||||||
|
```rust
|
||||||
|
// Background work in Rust
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let result = do_background_work().await;
|
||||||
|
|
||||||
|
// Update UI from main thread following FSD data flow
|
||||||
|
let _ = slint::invoke_from_event_loop(move || {
|
||||||
|
if let Some(ui) = ui_weak.upgrade() {
|
||||||
|
// Update flows from app → pages → widgets → shared
|
||||||
|
ui.update_global_state(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Component Development Guidelines
|
||||||
|
|
||||||
|
1. **Start with Shared Layer**: Build reusable components first
|
||||||
|
```slint
|
||||||
|
// shared/ui/loading/loading.slint
|
||||||
|
export component LoadingSpinner {
|
||||||
|
// Base loading component
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Compose in Widgets**: Create business-specific blocks
|
||||||
|
```slint
|
||||||
|
// widgets/token-card/index.slint
|
||||||
|
import { LoadingSpinner } from "../../shared/ui/loading/";
|
||||||
|
export component TokenCard {
|
||||||
|
LoadingSpinner { /* token-specific loading */ }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Integrate in Pages**: Assemble complete screens
|
||||||
|
```slint
|
||||||
|
// pages/dashboard/index.slint
|
||||||
|
import { TokenCard } from "../../widgets/token-card/";
|
||||||
|
export component Dashboard {
|
||||||
|
TokenCard { /* dashboard context */ }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Wire in App**: Handle global state and routing
|
||||||
|
```slint
|
||||||
|
// app/index.slint
|
||||||
|
import { Dashboard } from "../pages/dashboard/";
|
||||||
|
export component App {
|
||||||
|
if current-page == "dashboard": Dashboard { }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Naming Conventions
|
||||||
|
|
||||||
|
- Use `index.slint` for main component exports
|
||||||
|
- Use kebab-case for file names: `token-card.slint`
|
||||||
|
- Use PascalCase for component names: `TokenCard`
|
||||||
|
|
||||||
|
### Property & Callback Flow
|
||||||
|
|
||||||
|
- Properties flow down: `app → pages → widgets → shared`
|
||||||
|
- Callbacks flow up: `shared → widgets → pages → app`
|
||||||
|
- Keep callback interfaces simple and focused
|
||||||
|
|
||||||
|
## FSD Best Practices
|
||||||
|
|
||||||
|
### ✅ Do:
|
||||||
|
- Keep components focused on single responsibility
|
||||||
|
- Use semantic component names that reflect business domain
|
||||||
|
- Extract common patterns to `shared/` layer
|
||||||
|
- Implement loading and error states consistently
|
||||||
|
- Use TypeScript-like typing through Slint properties
|
||||||
|
- Follow consistent import paths relative to FSD structure
|
||||||
|
|
||||||
|
### ❌ Don't:
|
||||||
|
- Import from higher layers (breaks FSD hierarchy)
|
||||||
|
- Put business logic directly in UI components
|
||||||
|
- Create circular dependencies between features
|
||||||
|
- Hardcode values that should be in design system
|
||||||
|
- Mix concerns (UI logic with business logic)
|
||||||
|
- Create deep nesting beyond FSD layers
|
||||||
|
|
||||||
|
### Common Anti-patterns to Avoid:
|
||||||
|
```slint
|
||||||
|
// ❌ BAD: Widget importing from pages
|
||||||
|
// widgets/header/index.slint
|
||||||
|
import { Dashboard } from "../../pages/dashboard/"; // Wrong!
|
||||||
|
|
||||||
|
// ❌ BAD: Shared component with business logic
|
||||||
|
// shared/ui/button/index.slint
|
||||||
|
export component Button {
|
||||||
|
// Don't put trading logic here!
|
||||||
|
clicked => { execute_trade(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ GOOD: Proper callback delegation
|
||||||
|
// shared/ui/button/index.slint
|
||||||
|
export component Button {
|
||||||
|
callback clicked();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ GOOD: Business logic in appropriate layer
|
||||||
|
// features/trading/ui/trade-button.slint
|
||||||
|
import { Button } from "../../../shared/ui/button/";
|
||||||
|
export component TradeButton {
|
||||||
|
Button {
|
||||||
|
clicked => { handle_trade_click(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
|
||||||
|
- Test UI components with mock data
|
||||||
|
- Test service layer integration
|
||||||
|
- Use feature flags for test environments
|
||||||
|
|
||||||
|
### Feature Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run with specific features
|
||||||
|
cargo test --features dev
|
||||||
|
cargo test --features prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## UI/UX Principles
|
||||||
|
|
||||||
|
We follow comprehensive usability guidelines:
|
||||||
|
|
||||||
|
1. **10 Usability Heuristics** - Nielsen's principles
|
||||||
|
2. **Gestalt Principles** - Visual hierarchy and grouping
|
||||||
|
3. **Accessibility** - Support for different user needs
|
||||||
|
4. **Simplicity** - Minimize cognitive load
|
||||||
|
5. **Desktop Patterns** - Native desktop app behavior
|
||||||
|
|
||||||
|
### Key Requirements:
|
||||||
|
- Responsive design within window constraints (1080x800 - 1920x1080)
|
||||||
|
- Proper error messaging in plain language
|
||||||
|
- Loading states with progress indication
|
||||||
|
- Keyboard navigation support
|
||||||
|
- Native desktop interactions
|
||||||
|
|
||||||
|
## Submission Guidelines
|
||||||
|
|
||||||
|
### Branch Naming
|
||||||
|
|
||||||
|
Use descriptive names:
|
||||||
|
- `feat/shared/button-component` - New shared component
|
||||||
|
- `feat/pages/dashboard-redesign` - Page-level changes
|
||||||
|
- `feat/features/trading-flow` - Feature implementation
|
||||||
|
- `fix/widgets/token-card-loading` - Bug fixes
|
||||||
|
- `refactor/shared/design-system` - Code improvements
|
||||||
|
|
||||||
|
### Commit Messages
|
||||||
|
|
||||||
|
Be descriptive:
|
||||||
|
```
|
||||||
|
feat(shared): add loading spinner component
|
||||||
|
|
||||||
|
- Implement reusable loading spinner in shared/ui
|
||||||
|
- Add animation and theme support
|
||||||
|
- Export from shared layer for use in widgets
|
||||||
|
|
||||||
|
fix(pages): resolve dashboard layout overflow
|
||||||
|
|
||||||
|
- Fix token card grid overflow in dashboard
|
||||||
|
- Improve responsive behavior for small windows
|
||||||
|
- Add proper scrolling for token list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Review Checklist
|
||||||
|
|
||||||
|
**General:**
|
||||||
|
- [ ] Follows error handling patterns
|
||||||
|
- [ ] Uses tokio for async operations
|
||||||
|
- [ ] Includes proper logging
|
||||||
|
- [ ] Formatted with rustfmt
|
||||||
|
- [ ] Passes all tests
|
||||||
|
- [ ] Updates documentation if needed
|
||||||
|
|
||||||
|
**FSD-Specific:**
|
||||||
|
- [ ] Components placed in correct FSD layer
|
||||||
|
- [ ] Import dependencies only from lower layers
|
||||||
|
- [ ] Properties flow down, callbacks flow up
|
||||||
|
- [ ] No direct business logic in UI components
|
||||||
|
- [ ] Reusable components in `shared/` layer
|
||||||
|
- [ ] Feature-specific logic in `features/` layer
|
||||||
|
|
||||||
|
**UI/UX:**
|
||||||
|
- [ ] Follows design system patterns
|
||||||
|
- [ ] Implements proper loading states
|
||||||
|
- [ ] Handles error states gracefully
|
||||||
|
- [ ] Supports keyboard navigation
|
||||||
|
- [ ] Responsive within window constraints
|
||||||
|
|
||||||
|
## Development Environment
|
||||||
|
|
||||||
|
### Recommended Tools
|
||||||
|
|
||||||
|
- **IDE**: VS Code with rust-analyzer and Slint extension
|
||||||
|
- **Debugging**: Use tracing for structured logging
|
||||||
|
- **File Watching**: `watchexec` for auto-reload during development
|
||||||
|
- **Hot Reloading**: `just dev` for development workflow
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### Slint Compilation Errors
|
||||||
|
|
||||||
|
If you encounter field offset errors with watchexec:
|
||||||
|
```bash
|
||||||
|
cargo clean
|
||||||
|
# Then rebuild
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Threading Issues
|
||||||
|
|
||||||
|
Remember: Slint UI components are not `Send`. Always use `slint::invoke_from_event_loop` for UI updates from background tasks.
|
||||||
|
|
||||||
|
#### Hot Reloading
|
||||||
|
|
||||||
|
For the best development experience:
|
||||||
|
```bash
|
||||||
|
just dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This watches both `src/` and `ui/` directories and automatically rebuilds.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- **Component APIs**: Document with clear examples
|
||||||
|
- **Architecture**: Update this guide when patterns change
|
||||||
|
- **Configuration**: Document all config options
|
||||||
|
- **UI Components**: Maintain design system documentation
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
1. Check existing documentation
|
||||||
|
2. Search closed issues on GitHub
|
||||||
|
3. Ask in project discussions
|
||||||
|
4. Check Slint documentation for UI issues
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
- Be respectful and inclusive
|
||||||
|
- Focus on constructive feedback
|
||||||
|
- Help others learn and grow
|
||||||
|
- Maintain professional communication
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
By following these guidelines, you help maintain code quality and ensure smooth collaboration on the Ziya-Slint desktop application. Thank you for contributing! 🚀
|
||||||
414
CONTRIBUTORS.md
414
CONTRIBUTORS.md
|
|
@ -1,414 +0,0 @@
|
||||||
# Contributors Guide
|
|
||||||
|
|
||||||
Welcome to the Ziya Token Monitor development team! This guide will help you get up and running quickly with the project.
|
|
||||||
|
|
||||||
## 🚀 Quick Start for New Developers
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
Make sure you have these installed:
|
|
||||||
- **Node.js** >= 18.0.0
|
|
||||||
- **pnpm** >= 8.0.0 (package manager)
|
|
||||||
- **Redis** (for local development)
|
|
||||||
- **Git** for version control
|
|
||||||
|
|
||||||
### Installation Steps
|
|
||||||
|
|
||||||
1. **Clone the repository**:
|
|
||||||
```bash
|
|
||||||
git clone <repository-url>
|
|
||||||
cd muhafidh/ziya
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Install dependencies**:
|
|
||||||
```bash
|
|
||||||
pnpm install
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Set up Redis** (choose one):
|
|
||||||
|
|
||||||
**Option A: Docker (Recommended)**
|
|
||||||
```bash
|
|
||||||
# Run Redis in Docker container
|
|
||||||
docker run -d --name bismillahdao-redis -p 6379:6379 redis:alpine
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option B: Local Installation**
|
|
||||||
```bash
|
|
||||||
# Install Redis locally (varies by OS)
|
|
||||||
# macOS: brew install redis
|
|
||||||
# Ubuntu: sudo apt install redis-server
|
|
||||||
# Windows: Use WSL or Redis for Windows
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Start development**:
|
|
||||||
```bash
|
|
||||||
pnpm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
The application will start with:
|
|
||||||
- Nuxt dev server at `http://localhost:3000`
|
|
||||||
- Electron desktop app will launch automatically
|
|
||||||
- Hot reload enabled for both frontend and Electron
|
|
||||||
|
|
||||||
## 🏗️ Development Architecture
|
|
||||||
|
|
||||||
### Tech Stack Overview
|
|
||||||
- **Frontend**: Vue 3 (Vapor Mode), Nuxt 3, TypeScript
|
|
||||||
- **Desktop**: Electron with secure IPC communication
|
|
||||||
- **Styling**: TailwindCSS + DaisyUI
|
|
||||||
- **State Management**: Pinia
|
|
||||||
- **Backend Integration**: Redis (ioredis) for real-time events
|
|
||||||
- **Build Tools**: Vite, Electron Forge
|
|
||||||
|
|
||||||
### Project Structure Deep Dive
|
|
||||||
```
|
|
||||||
ziya/
|
|
||||||
├── app/ # Nuxt 3 application
|
|
||||||
│ ├── components/ # Vue components
|
|
||||||
│ │ ├── TokenCard.vue # Individual token display cards
|
|
||||||
│ │ ├── CexAnalysisCard.vue # CEX analysis results
|
|
||||||
│ │ └── ...
|
|
||||||
│ ├── pages/ # Nuxt pages/routes
|
|
||||||
│ │ └── hunting-ground.vue # Main dashboard
|
|
||||||
│ ├── stores/ # Pinia state management
|
|
||||||
│ ├── utils/ # Utility functions
|
|
||||||
│ │ ├── address.ts # Solana address handling
|
|
||||||
│ │ ├── format.ts # Data formatting
|
|
||||||
│ │ └── ...
|
|
||||||
│ └── types/ # TypeScript definitions
|
|
||||||
├── electron/ # Electron main process
|
|
||||||
│ ├── main.ts # Electron entry point
|
|
||||||
│ ├── config/ # Configuration files
|
|
||||||
│ │ ├── environment.ts # Environment settings
|
|
||||||
│ │ └── redis.ts # Redis configuration
|
|
||||||
│ ├── handlers/ # Event handlers
|
|
||||||
│ ├── utils/ # Electron utilities
|
|
||||||
│ │ └── redis.ts # Redis connection logic
|
|
||||||
│ └── preload.ts # Preload script for IPC
|
|
||||||
├── types/ # Shared TypeScript types
|
|
||||||
│ └── redis-events.ts # Redis event definitions
|
|
||||||
└── .config/ # Configuration files
|
|
||||||
└── nuxt.ts # Nuxt configuration
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Development Workflow
|
|
||||||
|
|
||||||
### Available Scripts
|
|
||||||
```bash
|
|
||||||
# Development
|
|
||||||
pnpm run dev # Start development with hot reload
|
|
||||||
pnpm run dev:nuxt # Start only Nuxt dev server
|
|
||||||
pnpm run dev:electron # Start only Electron (requires built Nuxt)
|
|
||||||
|
|
||||||
# Building
|
|
||||||
pnpm run build # Production build
|
|
||||||
pnpm run build:dev # Development build
|
|
||||||
pnpm run build:prod # Production build (explicit)
|
|
||||||
|
|
||||||
# Utilities
|
|
||||||
pnpm run lint # Run ESLint
|
|
||||||
pnpm run type-check # TypeScript type checking
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment Configuration
|
|
||||||
|
|
||||||
The application automatically detects the environment and configures Redis accordingly:
|
|
||||||
|
|
||||||
**Development Mode** (`NODE_ENV=development`):
|
|
||||||
- Redis: `localhost:6379` or `bismillahdao-redis:6379` (Docker)
|
|
||||||
- Hot reload enabled
|
|
||||||
- Debug logging active
|
|
||||||
|
|
||||||
**Production Mode** (`NODE_ENV=production`):
|
|
||||||
- Redis: `154.38.185.112:6379` (production server)
|
|
||||||
- Optimized builds
|
|
||||||
- Minimal logging
|
|
||||||
|
|
||||||
### Key Configuration Files
|
|
||||||
- `electron/config/environment.ts` - Environment-specific settings
|
|
||||||
- `electron/config/redis.ts` - Redis connection configuration
|
|
||||||
- `.config/nuxt.ts` - Nuxt configuration
|
|
||||||
- `package.json` - Build scripts and dependencies
|
|
||||||
|
|
||||||
## 🎯 Core Features & Components
|
|
||||||
|
|
||||||
### Real-time Token Dashboard
|
|
||||||
**Location**: `app/pages/hunting-ground.vue`
|
|
||||||
- Displays three columns of token events
|
|
||||||
- Real-time updates via Redis subscriptions
|
|
||||||
- Individual and bulk card management
|
|
||||||
|
|
||||||
### Token Cards System
|
|
||||||
**Components**:
|
|
||||||
- `TokenCard.vue` - New token creation events
|
|
||||||
- `CexAnalysisCard.vue` - CEX analysis and max depth events
|
|
||||||
|
|
||||||
**Features**:
|
|
||||||
- Duration calculation from timestamps
|
|
||||||
- Creator information display
|
|
||||||
- Graph visualization with hover tooltips
|
|
||||||
- Click-to-open in browser functionality
|
|
||||||
- Individual close buttons and "Clear All" actions
|
|
||||||
|
|
||||||
### Redis Integration
|
|
||||||
**Location**: `electron/utils/redis.ts`
|
|
||||||
- Subscribes to channels: `new_token_created`, `token_cex_updated`, `max_depth_reached`
|
|
||||||
- Handles connection management and error recovery
|
|
||||||
- Forwards events to renderer process via IPC
|
|
||||||
|
|
||||||
## 🐛 Common Development Issues & Solutions
|
|
||||||
|
|
||||||
### Redis Connection Issues
|
|
||||||
**Problem**: `ECONNREFUSED` when connecting to Redis
|
|
||||||
**Solutions**:
|
|
||||||
1. Ensure Redis is running: `redis-cli ping`
|
|
||||||
2. Check Docker container: `docker ps | grep redis`
|
|
||||||
3. Verify port 6379 is not blocked
|
|
||||||
|
|
||||||
### Build Errors
|
|
||||||
**Problem**: TypeScript compilation errors
|
|
||||||
**Solutions**:
|
|
||||||
1. Run type check: `pnpm run type-check`
|
|
||||||
2. Clear node_modules: `rm -rf node_modules && pnpm install`
|
|
||||||
3. Check for missing dependencies
|
|
||||||
|
|
||||||
### Hot Reload Not Working
|
|
||||||
**Problem**: Changes not reflecting in development
|
|
||||||
**Solutions**:
|
|
||||||
1. Restart dev server: `Ctrl+C` then `pnpm run dev`
|
|
||||||
2. Clear Nuxt cache: `rm -rf .nuxt`
|
|
||||||
3. Check if both Nuxt and Electron processes are running
|
|
||||||
|
|
||||||
## 📝 Code Style & Best Practices
|
|
||||||
|
|
||||||
### TypeScript Guidelines
|
|
||||||
- Use strict type definitions for all Redis events
|
|
||||||
- Prefer interfaces over types for object shapes
|
|
||||||
- Use proper error handling with try-catch blocks
|
|
||||||
|
|
||||||
### Vue Component Guidelines
|
|
||||||
- Use Composition API with `<script setup>`
|
|
||||||
- Keep components focused and single-responsibility
|
|
||||||
- Use proper TypeScript props definitions
|
|
||||||
|
|
||||||
### Electron Security
|
|
||||||
- Never expose Node.js APIs directly to renderer
|
|
||||||
- Use contextIsolation and sandboxed renderers
|
|
||||||
- Validate all IPC messages
|
|
||||||
|
|
||||||
## 🔍 Debugging Tips
|
|
||||||
|
|
||||||
### Electron DevTools
|
|
||||||
- Main process: Use VS Code debugger or console logs
|
|
||||||
- Renderer process: Open DevTools in Electron app (`Ctrl+Shift+I`)
|
|
||||||
|
|
||||||
### Redis Debugging
|
|
||||||
- Monitor Redis: `redis-cli monitor`
|
|
||||||
- Check subscriptions: `redis-cli pubsub channels`
|
|
||||||
- Test publishing: `redis-cli publish channel_name "test message"`
|
|
||||||
|
|
||||||
### Common Debug Commands
|
|
||||||
```bash
|
|
||||||
# Check Redis connection
|
|
||||||
redis-cli ping
|
|
||||||
|
|
||||||
# Monitor Redis events
|
|
||||||
redis-cli monitor
|
|
||||||
|
|
||||||
# Check running processes
|
|
||||||
ps aux | grep electron
|
|
||||||
ps aux | grep node
|
|
||||||
|
|
||||||
# Check ports
|
|
||||||
netstat -tulpn | grep :6379
|
|
||||||
netstat -tulpn | grep :3000
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 Deployment & Production
|
|
||||||
|
|
||||||
### Production Build Process
|
|
||||||
1. **Environment**: Automatically uses production Redis server (`154.38.185.112:6379`)
|
|
||||||
2. **Build**: `pnpm run build`
|
|
||||||
3. **Output**: Electron distributables in `out/` directory
|
|
||||||
|
|
||||||
### Production Checklist
|
|
||||||
- [ ] Redis server is accessible at `154.38.185.112:6379`
|
|
||||||
- [ ] All dependencies are production-ready
|
|
||||||
- [ ] Environment variables are set correctly
|
|
||||||
- [ ] Build passes without warnings
|
|
||||||
- [ ] Application connects to production Redis successfully
|
|
||||||
|
|
||||||
## 🤝 Contributing Guidelines
|
|
||||||
|
|
||||||
### Before Starting Development
|
|
||||||
1. Pull latest changes: `git pull origin master`
|
|
||||||
2. Create feature branch: `git checkout -b feature/your-feature-name`
|
|
||||||
3. Install dependencies: `pnpm install`
|
|
||||||
4. Start development server: `pnpm run dev`
|
|
||||||
|
|
||||||
### Code Review Process
|
|
||||||
1. Ensure all TypeScript types are properly defined
|
|
||||||
2. Test Redis connectivity in both dev and prod modes
|
|
||||||
3. Verify Electron security best practices
|
|
||||||
4. Check for memory leaks in long-running processes
|
|
||||||
5. Test hot reload functionality
|
|
||||||
|
|
||||||
### Git Workflow & Release Process
|
|
||||||
|
|
||||||
This project uses [Conventional Commits](https://www.conventionalcommits.org/) with automated changelog generation and semantic versioning.
|
|
||||||
|
|
||||||
#### Branch Management
|
|
||||||
```bash
|
|
||||||
# Create feature branch from master
|
|
||||||
git checkout master
|
|
||||||
git pull origin master
|
|
||||||
git checkout -b feat/your-feature-name
|
|
||||||
# or
|
|
||||||
git checkout -b fix/bug-description
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Commit Convention
|
|
||||||
Follow the conventional commit format:
|
|
||||||
```
|
|
||||||
<type>[optional scope]: <description>
|
|
||||||
|
|
||||||
[optional body]
|
|
||||||
|
|
||||||
[optional footer(s)]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Available Types:**
|
|
||||||
| Type | Emoji | Description | Version Bump |
|
|
||||||
|------|-------|-------------|--------------|
|
|
||||||
| `feat` | 🚀 | New features | minor |
|
|
||||||
| `fix` | 🐛 | Bug fixes | patch |
|
|
||||||
| `docs` | 📖 | Documentation changes | patch |
|
|
||||||
| `style` | 💄 | Code style changes | patch |
|
|
||||||
| `refactor` | ♻️ | Code refactoring | patch |
|
|
||||||
| `perf` | ⚡ | Performance improvements | patch |
|
|
||||||
| `test` | ✅ | Adding tests | patch |
|
|
||||||
| `build` | 🏗️ | Build system changes | patch |
|
|
||||||
| `ci` | 🤖 | CI/CD changes | patch |
|
|
||||||
| `chore` | 🧹 | Maintenance tasks | patch |
|
|
||||||
| `revert` | ⏪ | Reverting changes | patch |
|
|
||||||
|
|
||||||
**Example Commits:**
|
|
||||||
```bash
|
|
||||||
git commit -m "feat: add CEX analysis card component"
|
|
||||||
git commit -m "fix: resolve timestamp type inconsistency"
|
|
||||||
git commit -m "docs: update contributors guide"
|
|
||||||
git commit -m "feat(redis): add production environment configuration"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Breaking Changes:**
|
|
||||||
```bash
|
|
||||||
git commit -m "feat: redesign token card structure
|
|
||||||
|
|
||||||
BREAKING CHANGE: TokenCard props have changed from 'data' to 'token'"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Development Workflow
|
|
||||||
```bash
|
|
||||||
# 1. Create and switch to feature branch
|
|
||||||
git checkout -b feat/new-feature
|
|
||||||
|
|
||||||
# 2. Make changes and commit with conventional format
|
|
||||||
git add .
|
|
||||||
git commit -m "feat: add new feature description"
|
|
||||||
|
|
||||||
# 3. Push branch and create PR
|
|
||||||
git push origin feat/new-feature
|
|
||||||
|
|
||||||
# 4. After review approval, merge to master
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Release Process (Maintainers Only)
|
|
||||||
|
|
||||||
**Option 1: Full Automated Release** (Recommended)
|
|
||||||
```bash
|
|
||||||
# This will:
|
|
||||||
# - Generate changelog
|
|
||||||
# - Update version in package.json
|
|
||||||
# - Create git tag
|
|
||||||
# - Commit changes
|
|
||||||
# - Push to remote
|
|
||||||
pnpm run release
|
|
||||||
|
|
||||||
# Dry run to preview changes
|
|
||||||
pnpm run release:dry
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option 2: Manual Step-by-Step Release**
|
|
||||||
```bash
|
|
||||||
# Step 1: Generate changelog and update version
|
|
||||||
pnpm run changelog:release
|
|
||||||
|
|
||||||
# Step 2: Review the generated CHANGELOG.md
|
|
||||||
# Step 3: Create and push tag manually
|
|
||||||
git tag v0.2.0
|
|
||||||
git push origin v0.2.0
|
|
||||||
git push origin master
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option 3: Changelog Only** (No version bump)
|
|
||||||
```bash
|
|
||||||
# Generate changelog without releasing
|
|
||||||
pnpm run changelog
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Version Bumping Rules
|
|
||||||
- **Major** (1.0.0): Breaking changes (`BREAKING CHANGE:` in commit footer)
|
|
||||||
- **Minor** (0.1.0): New features (`feat:` commits)
|
|
||||||
- **Patch** (0.0.1): Bug fixes, docs, style, refactor, etc.
|
|
||||||
|
|
||||||
**Important Note for 0.x.x versions:**
|
|
||||||
- Before 1.0.0, breaking changes typically bump the **minor** version
|
|
||||||
- Example: `0.1.2` with `BREAKING CHANGE:` → `0.2.0` (not 1.0.0)
|
|
||||||
- Major version 1.0.0 is reserved for the first stable, production-ready release
|
|
||||||
|
|
||||||
#### Tagging Convention
|
|
||||||
- Tags follow semantic versioning: `v0.1.2`, `v1.0.0`
|
|
||||||
- Tags are automatically created during release process
|
|
||||||
- Each tag corresponds to a changelog entry
|
|
||||||
- Tags trigger automated builds and deployment
|
|
||||||
|
|
||||||
#### Best Practices
|
|
||||||
- **Use present tense**: "add feature" not "added feature"
|
|
||||||
- **Use imperative mood**: "fix bug" not "fixes bug"
|
|
||||||
- **Keep first line under 72 characters**
|
|
||||||
- **Reference issues**: "fix: resolve login issue (#123)"
|
|
||||||
- **Always document breaking changes** in commit footer
|
|
||||||
- **Be descriptive**: Explain what and why, not how
|
|
||||||
|
|
||||||
#### Common Scopes
|
|
||||||
Use these optional scopes for better organization:
|
|
||||||
- `redis` - Redis-related changes
|
|
||||||
- `ui` - User interface components
|
|
||||||
- `electron` - Electron-specific changes
|
|
||||||
- `build` - Build system changes
|
|
||||||
- `docs` - Documentation updates
|
|
||||||
|
|
||||||
## 📚 Additional Resources
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
- [Electron Documentation](https://www.electronjs.org/docs)
|
|
||||||
- [Nuxt 3 Documentation](https://nuxt.com)
|
|
||||||
- [Vue 3 Documentation](https://vuejs.org)
|
|
||||||
- [Redis Documentation](https://redis.io/docs)
|
|
||||||
|
|
||||||
### Tools & Extensions
|
|
||||||
- **VS Code Extensions**: Vue Language Features, TypeScript Vue Plugin
|
|
||||||
- **Redis GUI**: RedisInsight, Redis Desktop Manager
|
|
||||||
- **Debugging**: Vue DevTools, Electron DevTools
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🆘 Need Help?
|
|
||||||
|
|
||||||
If you encounter issues not covered in this guide:
|
|
||||||
1. Check the existing issues in the repository
|
|
||||||
2. Review the error logs carefully
|
|
||||||
3. Test with a fresh installation
|
|
||||||
4. Ask for help from the team
|
|
||||||
|
|
||||||
Welcome to the team! 🎉
|
|
||||||
8674
Cargo.lock
generated
Normal file
8674
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
189
Cargo.toml
Normal file
189
Cargo.toml
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
[package]
|
||||||
|
name = "ziya"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "One stop shop for your trading habit - Slint version"
|
||||||
|
authors = ["rizary"]
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "ziya"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
slint = "1.8.0"
|
||||||
|
i-slint-backend-winit = "1.12.0"
|
||||||
|
winit = "0.30"
|
||||||
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
|
tokio-util = "0.7.13"
|
||||||
|
async-compat = "0.2.4"
|
||||||
|
anyhow = "1.0"
|
||||||
|
log = "0.4"
|
||||||
|
env_logger = "0.11"
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
tracing-appender = "0.2"
|
||||||
|
chrono = { version = "0.4.39", features = ["serde"] }
|
||||||
|
chrono-tz = "0.10.3"
|
||||||
|
redis = { version = "0.32.2", features = ["aio", "tokio-comp"] }
|
||||||
|
bb8 = "0.9"
|
||||||
|
bb8-redis = "0.24.0"
|
||||||
|
solana-sdk = "2.3.0"
|
||||||
|
solana-pubkey = "2.3.0"
|
||||||
|
thiserror = "2.0.12"
|
||||||
|
futures-util = "0.3"
|
||||||
|
tokio-stream = "0.1"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
toml = "0.8"
|
||||||
|
bs58 = "0.5"
|
||||||
|
petgraph = { version = "0.8.1", features = ["serde-1"] }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
slint-build = "1.12.0"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["prod"]
|
||||||
|
dev = []
|
||||||
|
prod = []
|
||||||
|
deep-trace = []
|
||||||
|
|
||||||
|
[profile.release-with-debug]
|
||||||
|
inherits = "release"
|
||||||
|
debug = true
|
||||||
|
opt-level = 3
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
|
||||||
|
# Config for 'git cliff'
|
||||||
|
# Run with `GITHUB_TOKEN=$(gh auth token) git cliff --bump -up CHANGELOG.md`
|
||||||
|
# https://git-cliff.org/docs/configuration
|
||||||
|
[workspace.metadata.git-cliff.bump]
|
||||||
|
features_always_bump_minor = false
|
||||||
|
breaking_always_bump_major = false
|
||||||
|
|
||||||
|
[workspace.metadata.git-cliff.remote.github]
|
||||||
|
owner = "rizilab"
|
||||||
|
repo = "ziya"
|
||||||
|
|
||||||
|
[workspace.metadata.git-cliff.changelog]
|
||||||
|
# changelog header
|
||||||
|
header = """
|
||||||
|
# Changelog\n
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n
|
||||||
|
"""
|
||||||
|
# template for the changelog body
|
||||||
|
# https://keats.github.io/tera/docs/#introduction
|
||||||
|
body = """
|
||||||
|
{%- macro remote_url() -%}
|
||||||
|
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
|
||||||
|
{%- endmacro -%}
|
||||||
|
|
||||||
|
{% if version -%}
|
||||||
|
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||||
|
{% else -%}
|
||||||
|
## [Unreleased]
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
{% for group, commits in commits | group_by(attribute="group") %}
|
||||||
|
### {{ group | striptags | trim | upper_first }}
|
||||||
|
{%- for commit in commits %}
|
||||||
|
- {% if commit.breaking %}**BREAKING** {% endif -%}
|
||||||
|
{% if commit.scope %}*({{ commit.scope }})* {% endif -%}
|
||||||
|
{{ commit.message | trim | upper_first }}\
|
||||||
|
{% if commit.github.username and commit.github.username != "rizary" %} by \
|
||||||
|
[@{{ commit.github.username }}](https://github.com/{{ commit.github.username }})\
|
||||||
|
{%- endif -%}
|
||||||
|
{% if commit.github.pr_number %} in \
|
||||||
|
[#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }})\
|
||||||
|
{%- endif -%}.
|
||||||
|
{%- set fixes = commit.footers | filter(attribute="token", value="Fixes") -%}
|
||||||
|
{%- set closes = commit.footers | filter(attribute="token", value="Closes") -%}
|
||||||
|
{% for footer in fixes | concat(with=closes) -%}
|
||||||
|
{%- set issue_number = footer.value | trim_start_matches(pat="#") %} \
|
||||||
|
([{{ footer.value }}]({{ self::remote_url() }}/issues/{{ issue_number }}))\
|
||||||
|
{%- endfor -%}
|
||||||
|
{% if commit.body %}
|
||||||
|
{%- for section in commit.body | trim | split(pat="\n\n") %}
|
||||||
|
{% raw %} {% endraw %}- {{ section | replace(from="\n", to=" ") }}
|
||||||
|
{%- endfor -%}
|
||||||
|
{%- endif -%}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{%- if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
|
||||||
|
### New Contributors
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
|
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
|
||||||
|
- @{{ contributor.username }} made their first contribution
|
||||||
|
{%- if contributor.pr_number %} in \
|
||||||
|
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
|
||||||
|
{%- endif %}
|
||||||
|
{%- endfor %}\n
|
||||||
|
"""
|
||||||
|
# template for the changelog footer
|
||||||
|
footer = """
|
||||||
|
{%- macro remote_url() -%}
|
||||||
|
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
|
||||||
|
{%- endmacro -%}
|
||||||
|
|
||||||
|
{% for release in releases -%}
|
||||||
|
{% if release.version -%}
|
||||||
|
{% if release.previous.version -%}
|
||||||
|
[{{ release.version | trim_start_matches(pat="v") }}]: \
|
||||||
|
{{ self::remote_url() }}/compare/{{ release.previous.version }}...{{ release.version }}
|
||||||
|
{% else -%}
|
||||||
|
{#- compare against the initial commit for the first version -#}
|
||||||
|
[{{ release.version | trim_start_matches(pat="v") }}]: \
|
||||||
|
{{ self::remote_url() }}/compare/{{ release.commit_id }}...{{ release.version }}
|
||||||
|
{% endif -%}
|
||||||
|
{% else -%}
|
||||||
|
[Unreleased]: {{ self::remote_url() }}/compare/{{ release.previous.version }}...HEAD
|
||||||
|
{% endif -%}
|
||||||
|
{%- endfor -%}
|
||||||
|
"""
|
||||||
|
# remove the leading and trailing whitespace from the templates
|
||||||
|
trim = true
|
||||||
|
# postprocessors
|
||||||
|
postprocessors = []
|
||||||
|
|
||||||
|
[workspace.metadata.git-cliff.git]
|
||||||
|
# parse the commits based on https://www.conventionalcommits.org
|
||||||
|
conventional_commits = true
|
||||||
|
# filter out the commits that are not conventional
|
||||||
|
filter_unconventional = true
|
||||||
|
# process each line of a commit as an individual commit
|
||||||
|
split_commits = false
|
||||||
|
# regex for preprocessing the commit messages
|
||||||
|
commit_preprocessors = []
|
||||||
|
# regex for parsing and grouping commits
|
||||||
|
commit_parsers = [
|
||||||
|
{ message = "^feat", group = "<!-- 0 -->Features" },
|
||||||
|
{ body = ".*security", group = "<!-- 1 -->Security" },
|
||||||
|
{ message = "^fix", group = "<!-- 2 -->Bug Fixes" },
|
||||||
|
{ message = "^perf", group = "<!-- 3 -->Performance" },
|
||||||
|
{ message = "^doc", group = "<!-- 4 -->Documentation" },
|
||||||
|
{ message = "^test", group = "<!-- 5 -->Tests" },
|
||||||
|
{ message = "^refactor", group = "<!-- 6 -->Refactor" },
|
||||||
|
{ message = "^style", group = "<!-- 7 -->Style" },
|
||||||
|
{ message = "^chore", group = "<!-- 8 -->Miscellaneous" },
|
||||||
|
{ message = "^ci", default_scope = "ci", group = "<!-- 8 -->Miscellaneous" },
|
||||||
|
{ message = "^release", skip = true },
|
||||||
|
]
|
||||||
|
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
||||||
|
protect_breaking_commits = false
|
||||||
|
# filter out the commits that are not matched by commit parsers
|
||||||
|
filter_commits = false
|
||||||
|
# regex for matching git tags
|
||||||
|
tag_pattern = "v[0-9].*"
|
||||||
|
# regex for skipping tags
|
||||||
|
skip_tags = ""
|
||||||
|
# regex for ignoring tags
|
||||||
|
ignore_tags = ""
|
||||||
|
# sort the tags topologically
|
||||||
|
topo_order = false
|
||||||
|
# sort the commits inside sections by oldest/newest order
|
||||||
|
sort_commits = "oldest"
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) <year> <copyright holders>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
220
README.md
220
README.md
|
|
@ -1,158 +1,124 @@
|
||||||
# Ziya Token Monitor
|
# Ziya - Slint Edition
|
||||||
|
|
||||||
A modern Electron-based desktop application for monitoring Solana token creation, CEX analysis, and developer balance source graphs. Built with Vue 3, Nuxt 3, and TypeScript in an Electron wrapper.
|
**One stop shop for your trading habit** - Now powered by Slint and Rust!
|
||||||
|
|
||||||
## 🏗️ Architecture
|
> **/dˤiˈjaːʔ/**, "zee‑yah" — *Proper noun, meaning "light"*
|
||||||
|
> A bismillahDAO creation
|
||||||
|
|
||||||
This project follows a hybrid architecture combining the power of Nuxt 3 for the frontend with Electron for desktop capabilities:
|
## Overview
|
||||||
|
|
||||||
### Tech Stack
|
Ziya is a modern, high-performance trading platform built with [Slint](https://slint.dev/) and Rust. Designed for cryptocurrency traders who demand speed, reliability, and a beautiful user interface.
|
||||||
- **Frontend**: Vue 3 (Vapor Mode), Nuxt 3, TypeScript, Pinia, TailwindCSS + DaisyUI
|
|
||||||
- **Desktop**: Electron with secure IPC communication
|
|
||||||
- **Backend Integration**: Redis (ioredis) for real-time event streaming
|
|
||||||
- **Development**: pnpm workspaces, ESLint, hot reload
|
|
||||||
|
|
||||||
### Project Structure
|
## Features
|
||||||
```
|
|
||||||
ziya/
|
|
||||||
├── app/ # Nuxt 3 application
|
|
||||||
│ ├── components/ # Vue components
|
|
||||||
│ │ ├── TokenCard.vue # Individual token display cards
|
|
||||||
│ │ ├── CexAnalysisCard.vue # CEX analysis results
|
|
||||||
│ │ └── ...
|
|
||||||
│ ├── pages/ # Nuxt pages/routes
|
|
||||||
│ │ └── hunting-ground.vue # Main dashboard
|
|
||||||
│ ├── stores/ # Pinia state management
|
|
||||||
│ ├── utils/ # Utility functions
|
|
||||||
│ └── types/ # TypeScript definitions
|
|
||||||
├── electron/ # Electron main process
|
|
||||||
│ ├── main.ts # Electron entry point
|
|
||||||
│ ├── config/ # Configuration files
|
|
||||||
│ │ ├── environment.ts # Environment settings
|
|
||||||
│ │ └── redis.ts # Redis configuration
|
|
||||||
│ ├── handlers/ # Event handlers
|
|
||||||
│ ├── utils/ # Electron utilities
|
|
||||||
│ └── preload.ts # Preload script for IPC
|
|
||||||
├── types/ # Shared TypeScript types
|
|
||||||
│ └── redis-events.ts # Redis event definitions
|
|
||||||
└── .config/ # Configuration files
|
|
||||||
└── nuxt.ts # Nuxt configuration
|
|
||||||
```
|
|
||||||
|
|
||||||
## ✨ Current Features
|
- **⚡ Lightning Fast**: Native Rust performance with instant startup
|
||||||
|
- **🎨 Beautiful UI**: Modern interface built with Slint's declarative language
|
||||||
|
- **📊 Real-time Data**: Live token prices and market analysis
|
||||||
|
- **🔒 Secure**: Type-safe Rust backend with robust error handling
|
||||||
|
- **📱 Native Feel**: True desktop application, not a web wrapper
|
||||||
|
- **🚀 Lightweight**: ~5MB standalone binary vs ~100MB Electron apps
|
||||||
|
|
||||||
### Real-time Token Dashboard
|
## Installation
|
||||||
- **Three-column layout** displaying different token event types
|
|
||||||
- **Live updates** via Redis pub/sub integration
|
|
||||||
- **Individual card management** with close buttons
|
|
||||||
- **Bulk operations** with "Clear All" functionality
|
|
||||||
|
|
||||||
### Token Event Types
|
### Download Pre-built Binaries
|
||||||
1. **New Token Created** - Recently minted tokens with creator information
|
|
||||||
2. **CEX Analysis** - Tokens analyzed for centralized exchange connections
|
|
||||||
3. **Max Depth Analysis** - Tokens that reached maximum analysis depth
|
|
||||||
|
|
||||||
### Interactive Features
|
Visit our [Releases](https://github.com/rizilab/ziya/releases) page to download the latest version for your platform:
|
||||||
- **Graph Visualization** - Hover tooltips showing node/edge relationships
|
|
||||||
- **Duration Display** - Time elapsed since token creation/analysis
|
|
||||||
- **Browser Integration** - Click to open token details in browser
|
|
||||||
- **Creator Information** - Display developer names and addresses
|
|
||||||
- **CEX Integration** - Show exchange connections and analysis results
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
- **Windows**: `ziya-x86_64-pc-windows-msvc.zip`
|
||||||
|
- **macOS**: `ziya-x86_64-apple-darwin.tar.xz` (Intel) or `ziya-aarch64-apple-darwin.tar.xz` (Apple Silicon)
|
||||||
|
- **Linux**: `ziya-x86_64-unknown-linux-gnu.tar.xz`
|
||||||
|
|
||||||
### Prerequisites
|
### Build from Source
|
||||||
- Node.js >= 18.0.0
|
|
||||||
- pnpm >= 8.0.0
|
|
||||||
- Redis server (local or Docker)
|
|
||||||
|
|
||||||
### Installation
|
If you have Rust installed:
|
||||||
```bash
|
|
||||||
# Clone and install
|
|
||||||
git clone <repository-url>
|
|
||||||
cd muhafidh/ziya
|
|
||||||
pnpm install
|
|
||||||
|
|
||||||
# Start Redis (Docker)
|
|
||||||
docker run -d --name bismillahdao-redis -p 6379:6379 redis:alpine
|
|
||||||
|
|
||||||
# Start development
|
|
||||||
pnpm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Environment Configuration
|
|
||||||
|
|
||||||
The application automatically configures Redis connection based on environment:
|
|
||||||
|
|
||||||
### Development
|
|
||||||
- **Redis**: `localhost:6379` (local) or `bismillahdao-redis:6379` (Docker)
|
|
||||||
- **Build**: `pnpm run dev`
|
|
||||||
- **Features**: Hot reload, debug logging
|
|
||||||
|
|
||||||
### Production
|
|
||||||
- **Redis**: `154.38.185.112:6379` (production server)
|
|
||||||
- **Build**: `pnpm run build:prod`
|
|
||||||
- **Features**: Optimized builds, minimal logging
|
|
||||||
|
|
||||||
## 📋 Available Scripts
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Development
|
git clone https://github.com/rizilab/ziya.git
|
||||||
pnpm run dev # Start development with hot reload
|
cd ziya-slint
|
||||||
pnpm run dev:nuxt # Start only Nuxt dev server
|
cargo install --path .
|
||||||
pnpm run dev:electron # Start only Electron
|
|
||||||
|
|
||||||
# Building
|
|
||||||
pnpm run build # Production build
|
|
||||||
pnpm run build:dev # Development build
|
|
||||||
pnpm run build:prod # Production build (explicit)
|
|
||||||
|
|
||||||
# Utilities
|
|
||||||
pnpm run lint # Run ESLint
|
|
||||||
pnpm run type-check # TypeScript type checking
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎯 Key Components
|
For detailed build instructions, see [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||||
|
|
||||||
### TokenCard.vue
|
## Quick Start
|
||||||
- Displays new token creation events
|
|
||||||
- Shows creator information and timestamps
|
|
||||||
- Handles browser integration for token details
|
|
||||||
|
|
||||||
### CexAnalysisCard.vue
|
1. **Launch Ziya**: Run the application
|
||||||
- Shows CEX analysis and max depth results
|
2. **Login**: Enter your credentials or create a new account
|
||||||
- Displays graph data with interactive tooltips
|
3. **Dashboard**: Browse and search tokens
|
||||||
- Includes duration calculation and CEX information
|
4. **Trade**: Access advanced tools in the Hunting Ground
|
||||||
|
|
||||||
### hunting-ground.vue
|
## Application Features
|
||||||
- Main dashboard page with three-column layout
|
|
||||||
- Manages real-time Redis event subscriptions
|
|
||||||
- Handles card state management and user interactions
|
|
||||||
|
|
||||||
## 🔌 Redis Integration
|
### Dashboard
|
||||||
|
- **Token Search**: Find tokens by name, symbol, or address
|
||||||
|
- **Market Overview**: Real-time price data and trends
|
||||||
|
- **Portfolio**: Track your holdings and performance
|
||||||
|
|
||||||
The application subscribes to three Redis channels:
|
### Hunting Ground
|
||||||
- `new_token_created` - New token creation events
|
- **Advanced Analysis**: Deep market insights and analytics
|
||||||
- `token_cex_updated` - CEX analysis completion
|
- **Trading Tools**: Professional-grade trading interface
|
||||||
- `max_depth_reached` - Maximum analysis depth events
|
- **Risk Management**: Built-in tools to manage trading risks
|
||||||
|
|
||||||
Events are automatically forwarded from Electron main process to renderer via secure IPC.
|
### Profile Management
|
||||||
|
- **Wallet Integration**: Connect and manage multiple wallets
|
||||||
|
- **Settings**: Customize your trading experience
|
||||||
|
- **Security**: Secure credential management
|
||||||
|
|
||||||
## 🔒 Security Features
|
## Why Slint + Rust?
|
||||||
|
|
||||||
- **Context Isolation**: Enabled for all renderer processes
|
| Aspect | Traditional (Electron) | Ziya (Slint + Rust) |
|
||||||
- **Sandboxing**: Renderer processes run in sandbox mode
|
|--------|----------------------|-------------------|
|
||||||
- **Secure IPC**: All communication through preload scripts
|
| **Memory Usage** | ~50MB+ | ~10MB |
|
||||||
- **No Node.js Exposure**: APIs not directly accessible to renderer
|
| **Startup Time** | 2-5 seconds | Instant |
|
||||||
|
| **Bundle Size** | ~100MB | ~5MB |
|
||||||
|
| **Performance** | JavaScript V8 | Native machine code |
|
||||||
|
| **Security** | Web vulnerabilities | Memory-safe Rust |
|
||||||
|
| **Updates** | Large downloads | Efficient delta updates |
|
||||||
|
|
||||||
## 🤝 Contributing
|
## Support
|
||||||
|
|
||||||
For detailed development setup, code style guidelines, and contribution workflow, please see [CONTRIBUTORS.md](./CONTRIBUTORS.md).
|
- **Documentation**: [Full documentation](https://docs.ziya.trading)
|
||||||
|
- **Issues**: [GitHub Issues](https://github.com/rizilab/ziya/issues)
|
||||||
|
- **Discussions**: [GitHub Discussions](https://github.com/rizilab/ziya/discussions)
|
||||||
|
- **Community**: [Discord Server](https://discord.gg/bismillahdao)
|
||||||
|
|
||||||
## 📄 License
|
## Roadmap
|
||||||
|
|
||||||
MIT License - see LICENSE file for details.
|
### Current Version (v0.2.0)
|
||||||
|
- ✅ Core trading dashboard
|
||||||
|
- ✅ Token search and filtering
|
||||||
|
- ✅ User authentication
|
||||||
|
- ✅ Basic portfolio tracking
|
||||||
|
|
||||||
|
### Next Release (v0.3.0)
|
||||||
|
- 🚧 Real-time WebSocket data feeds
|
||||||
|
- 🚧 Advanced charting with TradingView
|
||||||
|
- 🚧 Multi-exchange support
|
||||||
|
- 🚧 Trading automation tools
|
||||||
|
|
||||||
|
### Future Releases
|
||||||
|
- 📋 Mobile companion app
|
||||||
|
- 📋 DeFi protocol integration
|
||||||
|
- 📋 Social trading features
|
||||||
|
- 📋 Advanced risk analytics
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for:
|
||||||
|
|
||||||
|
- Development setup
|
||||||
|
- Code style guidelines
|
||||||
|
- Testing procedures
|
||||||
|
- Release process
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - see [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## About bismillahDAO
|
||||||
|
|
||||||
|
Ziya is proudly developed by [bismillahDAO](https://bismillahdao.org), building ethical and innovative tools for the cryptocurrency community.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Ready to monitor Solana tokens in real-time! 🚀**
|
**⭐ Star this repository if you find Ziya useful!**
|
||||||
|
|
|
||||||
169
app.config.ts
169
app.config.ts
|
|
@ -1,169 +0,0 @@
|
||||||
/**
|
|
||||||
* Ziya Application Configuration
|
|
||||||
* Centralized configuration for all app settings
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface AppConfig {
|
|
||||||
app: {
|
|
||||||
name: string;
|
|
||||||
version: string;
|
|
||||||
description: string;
|
|
||||||
author: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
development: {
|
|
||||||
nuxt: {
|
|
||||||
host: string;
|
|
||||||
port: number;
|
|
||||||
https: boolean;
|
|
||||||
};
|
|
||||||
electron: {
|
|
||||||
devTools: boolean;
|
|
||||||
reloadOnChange: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
production: {
|
|
||||||
electron: {
|
|
||||||
devTools: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
window: {
|
|
||||||
minHeight: number;
|
|
||||||
minWidth: number;
|
|
||||||
maxHeight: number;
|
|
||||||
maxWidth: number;
|
|
||||||
defaultHeight: number;
|
|
||||||
defaultWidth: number;
|
|
||||||
titleBarStyle: 'default' | 'hidden' | 'hiddenInset' | 'customButtonsOnHover';
|
|
||||||
};
|
|
||||||
|
|
||||||
theme: {
|
|
||||||
defaultPalette: number;
|
|
||||||
defaultDarkMode: boolean;
|
|
||||||
availablePalettes: number[];
|
|
||||||
};
|
|
||||||
|
|
||||||
redis: {
|
|
||||||
host: string;
|
|
||||||
port: number;
|
|
||||||
db: number;
|
|
||||||
keyPrefix: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
security: {
|
|
||||||
csp: {
|
|
||||||
scriptSrc: string[];
|
|
||||||
styleSrc: string[];
|
|
||||||
imgSrc: string[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default configuration
|
|
||||||
*/
|
|
||||||
export const appConfig: AppConfig = {
|
|
||||||
app: {
|
|
||||||
name: 'Ziya',
|
|
||||||
version: '1.0.0',
|
|
||||||
description: 'One stop shop trading solution',
|
|
||||||
author: 'bismillahDAO',
|
|
||||||
},
|
|
||||||
|
|
||||||
development: {
|
|
||||||
nuxt: {
|
|
||||||
host: 'localhost',
|
|
||||||
port: 3000,
|
|
||||||
https: false,
|
|
||||||
},
|
|
||||||
electron: {
|
|
||||||
devTools: true,
|
|
||||||
reloadOnChange: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
production: {
|
|
||||||
electron: {
|
|
||||||
devTools: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
window: {
|
|
||||||
minHeight: 800,
|
|
||||||
minWidth: 1080,
|
|
||||||
maxHeight: 1080,
|
|
||||||
maxWidth: 1920,
|
|
||||||
defaultHeight: 1024,
|
|
||||||
defaultWidth: 1280,
|
|
||||||
titleBarStyle: 'hidden',
|
|
||||||
},
|
|
||||||
|
|
||||||
theme: {
|
|
||||||
defaultPalette: 1,
|
|
||||||
defaultDarkMode: false,
|
|
||||||
availablePalettes: Array.from({ length: 24 }, (_, i) => i + 1),
|
|
||||||
},
|
|
||||||
|
|
||||||
redis: {
|
|
||||||
host: 'localhost',
|
|
||||||
port: 6379,
|
|
||||||
db: 0,
|
|
||||||
keyPrefix: 'ziya:',
|
|
||||||
},
|
|
||||||
|
|
||||||
security: {
|
|
||||||
csp: {
|
|
||||||
scriptSrc: ["'self'", "'unsafe-inline'"],
|
|
||||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
||||||
imgSrc: ["'self'", 'data:', 'https:'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get configuration value with environment override support
|
|
||||||
*/
|
|
||||||
export function getConfig(): AppConfig {
|
|
||||||
// Allow environment variables to override config
|
|
||||||
const config = { ...appConfig };
|
|
||||||
|
|
||||||
// Override with environment variables if they exist
|
|
||||||
if (process.env.NUXT_DEV_PORT) {
|
|
||||||
config.development.nuxt.port = parseInt(process.env.NUXT_DEV_PORT, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.NUXT_DEV_HOST) {
|
|
||||||
config.development.nuxt.host = process.env.NUXT_DEV_HOST;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.REDIS_HOST) {
|
|
||||||
config.redis.host = process.env.REDIS_HOST;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.REDIS_PORT) {
|
|
||||||
config.redis.port = parseInt(process.env.REDIS_PORT, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the development server URL
|
|
||||||
*/
|
|
||||||
export function getDevServerUrl(): string {
|
|
||||||
const config = getConfig();
|
|
||||||
const { host, port, https } = config.development.nuxt;
|
|
||||||
const protocol = https ? 'https' : 'http';
|
|
||||||
return `${protocol}://${host}:${port}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Environment-specific configuration helpers
|
|
||||||
*/
|
|
||||||
export const isDevelopment = process.env.NODE_ENV === 'development';
|
|
||||||
export const isProduction = process.env.NODE_ENV === 'production';
|
|
||||||
export const isElectron = process.env.IS_ELECTRON === 'true';
|
|
||||||
|
|
||||||
export default appConfig;
|
|
||||||
26
app.vue
26
app.vue
|
|
@ -1,26 +0,0 @@
|
||||||
/// <reference types="../types/electron" />
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<NuxtLayout>
|
|
||||||
<NuxtPage />
|
|
||||||
</NuxtLayout>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
/**
|
|
||||||
* Main App Component
|
|
||||||
*
|
|
||||||
* This is the root component that handles global layout rendering.
|
|
||||||
* It provides the foundation for the entire Ziya application.
|
|
||||||
*
|
|
||||||
* The component is intentionally minimal to avoid SSR issues with
|
|
||||||
* Pinia stores and to ensure proper initialization flow.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Component metadata
|
|
||||||
defineOptions({
|
|
||||||
name: 'ZiyaApp',
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
23
app/app.vue
23
app/app.vue
|
|
@ -1,23 +0,0 @@
|
||||||
/// <reference types="../types/electron" />
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<NuxtLayout>
|
|
||||||
<NuxtPage />
|
|
||||||
</NuxtLayout>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
// Main app component - handles global layout rendering
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.app-container {
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,478 +0,0 @@
|
||||||
@import "tailwindcss";
|
|
||||||
|
|
||||||
@plugin "daisyui" {
|
|
||||||
themes:
|
|
||||||
light --default,
|
|
||||||
dark --prefersdark,
|
|
||||||
palette-01-light, palette-01-dark,
|
|
||||||
palette-02-light, palette-02-dark,
|
|
||||||
palette-03-light, palette-03-dark,
|
|
||||||
palette-04-light, palette-04-dark,
|
|
||||||
palette-05-light, palette-05-dark,
|
|
||||||
palette-06-light, palette-06-dark,
|
|
||||||
palette-07-light, palette-07-dark,
|
|
||||||
palette-08-light, palette-08-dark,
|
|
||||||
palette-09-light, palette-09-dark,
|
|
||||||
palette-10-light, palette-10-dark,
|
|
||||||
palette-11-light, palette-11-dark,
|
|
||||||
palette-12-light, palette-12-dark,
|
|
||||||
palette-13-light, palette-13-dark,
|
|
||||||
palette-14-light, palette-14-dark,
|
|
||||||
palette-15-light, palette-15-dark,
|
|
||||||
palette-16-light, palette-16-dark,
|
|
||||||
palette-17-light, palette-17-dark,
|
|
||||||
palette-18-light, palette-18-dark,
|
|
||||||
palette-19-light, palette-19-dark,
|
|
||||||
palette-20-light, palette-20-dark,
|
|
||||||
palette-21-light, palette-21-dark,
|
|
||||||
palette-22-light, palette-22-dark,
|
|
||||||
palette-23-light, palette-23-dark,
|
|
||||||
palette-24-light, palette-24-dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom theme definitions */
|
|
||||||
|
|
||||||
/* Palette 01 - Cyan Ocean */
|
|
||||||
@plugin "daisyui/theme" {
|
|
||||||
name: "palette-01-light";
|
|
||||||
color-scheme: light;
|
|
||||||
--color-primary: oklch(65% 0.15 195);
|
|
||||||
--color-primary-content: oklch(98% 0.01 195);
|
|
||||||
--color-secondary: oklch(60% 0.15 250);
|
|
||||||
--color-secondary-content: oklch(98% 0.01 250);
|
|
||||||
--color-accent: oklch(65% 0.25 330);
|
|
||||||
--color-accent-content: oklch(98% 0.01 330);
|
|
||||||
--color-neutral: oklch(60% 0.05 220);
|
|
||||||
--color-neutral-content: oklch(98% 0.01 220);
|
|
||||||
--color-base-100: oklch(98% 0.01 220);
|
|
||||||
--color-base-200: oklch(95% 0.02 220);
|
|
||||||
--color-base-300: oklch(90% 0.03 220);
|
|
||||||
--color-base-content: oklch(25% 0.05 220);
|
|
||||||
}
|
|
||||||
|
|
||||||
@plugin "daisyui/theme" {
|
|
||||||
name: "palette-01-dark";
|
|
||||||
color-scheme: dark;
|
|
||||||
--color-primary: oklch(70% 0.18 195);
|
|
||||||
--color-primary-content: oklch(25% 0.05 220);
|
|
||||||
--color-secondary: oklch(65% 0.18 250);
|
|
||||||
--color-secondary-content: oklch(25% 0.05 220);
|
|
||||||
--color-accent: oklch(70% 0.28 330);
|
|
||||||
--color-accent-content: oklch(25% 0.05 220);
|
|
||||||
--color-neutral: oklch(65% 0.08 220);
|
|
||||||
--color-neutral-content: oklch(25% 0.05 220);
|
|
||||||
--color-base-100: oklch(25% 0.05 220);
|
|
||||||
--color-base-200: oklch(30% 0.06 220);
|
|
||||||
--color-base-300: oklch(35% 0.07 220);
|
|
||||||
--color-base-content: oklch(95% 0.02 220);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Palette 02 - Royal Blue */
|
|
||||||
@plugin "daisyui/theme" {
|
|
||||||
name: "palette-02-light";
|
|
||||||
color-scheme: light;
|
|
||||||
--color-primary: oklch(60% 0.25 260);
|
|
||||||
--color-primary-content: oklch(98% 0.01 260);
|
|
||||||
--color-secondary: oklch(65% 0.22 270);
|
|
||||||
--color-secondary-content: oklch(98% 0.01 270);
|
|
||||||
--color-accent: oklch(70% 0.25 350);
|
|
||||||
--color-accent-content: oklch(98% 0.01 350);
|
|
||||||
--color-neutral: oklch(60% 0.05 240);
|
|
||||||
--color-neutral-content: oklch(98% 0.01 240);
|
|
||||||
--color-base-100: oklch(98% 0.01 240);
|
|
||||||
--color-base-200: oklch(96% 0.02 240);
|
|
||||||
--color-base-300: oklch(92% 0.03 240);
|
|
||||||
--color-base-content: oklch(20% 0.05 240);
|
|
||||||
}
|
|
||||||
|
|
||||||
@plugin "daisyui/theme" {
|
|
||||||
name: "palette-02-dark";
|
|
||||||
color-scheme: dark;
|
|
||||||
--color-primary: oklch(65% 0.28 260);
|
|
||||||
--color-primary-content: oklch(20% 0.05 240);
|
|
||||||
--color-secondary: oklch(70% 0.25 270);
|
|
||||||
--color-secondary-content: oklch(20% 0.05 240);
|
|
||||||
--color-accent: oklch(75% 0.28 350);
|
|
||||||
--color-accent-content: oklch(20% 0.05 240);
|
|
||||||
--color-neutral: oklch(65% 0.08 240);
|
|
||||||
--color-neutral-content: oklch(20% 0.05 240);
|
|
||||||
--color-base-100: oklch(20% 0.05 240);
|
|
||||||
--color-base-200: oklch(25% 0.06 240);
|
|
||||||
--color-base-300: oklch(30% 0.07 240);
|
|
||||||
--color-base-content: oklch(96% 0.02 240);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Palette 03 - Purple Dream */
|
|
||||||
@plugin "daisyui/theme" {
|
|
||||||
name: "palette-03-light";
|
|
||||||
color-scheme: light;
|
|
||||||
--color-primary: oklch(60% 0.28 280);
|
|
||||||
--color-primary-content: oklch(98% 0.01 280);
|
|
||||||
--color-secondary: oklch(65% 0.20 160);
|
|
||||||
--color-secondary-content: oklch(98% 0.01 160);
|
|
||||||
--color-accent: oklch(70% 0.22 200);
|
|
||||||
--color-accent-content: oklch(98% 0.01 200);
|
|
||||||
--color-neutral: oklch(60% 0.05 220);
|
|
||||||
--color-neutral-content: oklch(98% 0.01 220);
|
|
||||||
--color-base-100: oklch(98% 0.01 220);
|
|
||||||
--color-base-200: oklch(95% 0.02 220);
|
|
||||||
--color-base-300: oklch(90% 0.03 220);
|
|
||||||
--color-base-content: oklch(25% 0.05 220);
|
|
||||||
}
|
|
||||||
|
|
||||||
@plugin "daisyui/theme" {
|
|
||||||
name: "palette-03-dark";
|
|
||||||
color-scheme: dark;
|
|
||||||
--color-primary: oklch(65% 0.31 280);
|
|
||||||
--color-primary-content: oklch(25% 0.05 220);
|
|
||||||
--color-secondary: oklch(70% 0.23 160);
|
|
||||||
--color-secondary-content: oklch(25% 0.05 220);
|
|
||||||
--color-accent: oklch(75% 0.25 200);
|
|
||||||
--color-accent-content: oklch(25% 0.05 220);
|
|
||||||
--color-neutral: oklch(65% 0.08 220);
|
|
||||||
--color-neutral-content: oklch(25% 0.05 220);
|
|
||||||
--color-base-100: oklch(25% 0.05 220);
|
|
||||||
--color-base-200: oklch(30% 0.06 220);
|
|
||||||
--color-base-300: oklch(35% 0.07 220);
|
|
||||||
--color-base-content: oklch(95% 0.02 220);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* For remaining palettes (04-24), we'll use a systematic approach */
|
|
||||||
/* Each palette will have mathematically distributed hues for consistency */
|
|
||||||
|
|
||||||
/* Palette 04 - Teal Fresh */
|
|
||||||
@plugin "daisyui/theme" {
|
|
||||||
name: "palette-04-light";
|
|
||||||
color-scheme: light;
|
|
||||||
--color-primary: oklch(65% 0.20 180);
|
|
||||||
--color-primary-content: oklch(98% 0.01 180);
|
|
||||||
--color-secondary: oklch(60% 0.25 300);
|
|
||||||
--color-secondary-content: oklch(98% 0.01 300);
|
|
||||||
--color-accent: oklch(70% 0.30 45);
|
|
||||||
--color-accent-content: oklch(98% 0.01 45);
|
|
||||||
--color-neutral: oklch(60% 0.05 200);
|
|
||||||
--color-neutral-content: oklch(98% 0.01 200);
|
|
||||||
--color-base-100: oklch(98% 0.01 200);
|
|
||||||
--color-base-200: oklch(95% 0.02 200);
|
|
||||||
--color-base-300: oklch(90% 0.03 200);
|
|
||||||
--color-base-content: oklch(25% 0.05 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
@plugin "daisyui/theme" {
|
|
||||||
name: "palette-04-dark";
|
|
||||||
color-scheme: dark;
|
|
||||||
--color-primary: oklch(70% 0.23 180);
|
|
||||||
--color-primary-content: oklch(25% 0.05 200);
|
|
||||||
--color-secondary: oklch(65% 0.28 300);
|
|
||||||
--color-secondary-content: oklch(25% 0.05 200);
|
|
||||||
--color-accent: oklch(75% 0.33 45);
|
|
||||||
--color-accent-content: oklch(25% 0.05 200);
|
|
||||||
--color-neutral: oklch(65% 0.08 200);
|
|
||||||
--color-neutral-content: oklch(25% 0.05 200);
|
|
||||||
--color-base-100: oklch(25% 0.05 200);
|
|
||||||
--color-base-200: oklch(30% 0.06 200);
|
|
||||||
--color-base-300: oklch(35% 0.07 200);
|
|
||||||
--color-base-content: oklch(95% 0.02 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* I'll create a more efficient approach for the remaining palettes using CSS loops would be ideal,
|
|
||||||
but since CSS doesn't support loops, I'll create a few more key palettes and use a pattern */
|
|
||||||
|
|
||||||
/* Palette 05 - Slate Modern */
|
|
||||||
@plugin "daisyui/theme" {
|
|
||||||
name: "palette-05-light";
|
|
||||||
color-scheme: light;
|
|
||||||
--color-primary: oklch(55% 0.15 240);
|
|
||||||
--color-primary-content: oklch(98% 0.01 240);
|
|
||||||
--color-secondary: oklch(65% 0.25 280);
|
|
||||||
--color-secondary-content: oklch(98% 0.01 280);
|
|
||||||
--color-accent: oklch(70% 0.30 320);
|
|
||||||
--color-accent-content: oklch(98% 0.01 320);
|
|
||||||
--color-neutral: oklch(55% 0.05 240);
|
|
||||||
--color-neutral-content: oklch(98% 0.01 240);
|
|
||||||
--color-base-100: oklch(98% 0.01 240);
|
|
||||||
--color-base-200: oklch(96% 0.02 240);
|
|
||||||
--color-base-300: oklch(92% 0.03 240);
|
|
||||||
--color-base-content: oklch(20% 0.05 240);
|
|
||||||
}
|
|
||||||
|
|
||||||
@plugin "daisyui/theme" {
|
|
||||||
name: "palette-05-dark";
|
|
||||||
color-scheme: dark;
|
|
||||||
--color-primary: oklch(65% 0.18 240);
|
|
||||||
--color-primary-content: oklch(20% 0.05 240);
|
|
||||||
--color-secondary: oklch(70% 0.28 280);
|
|
||||||
--color-secondary-content: oklch(20% 0.05 240);
|
|
||||||
--color-accent: oklch(75% 0.33 320);
|
|
||||||
--color-accent-content: oklch(20% 0.05 240);
|
|
||||||
--color-neutral: oklch(65% 0.08 240);
|
|
||||||
--color-neutral-content: oklch(20% 0.05 240);
|
|
||||||
--color-base-100: oklch(20% 0.05 240);
|
|
||||||
--color-base-200: oklch(25% 0.06 240);
|
|
||||||
--color-base-300: oklch(30% 0.07 240);
|
|
||||||
--color-base-content: oklch(96% 0.02 240);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* For brevity, I'll create a pattern-based system for palettes 06-24 */
|
|
||||||
/* Each will follow the mathematical distribution but I'll define key ones */
|
|
||||||
|
|
||||||
/* Palette 06 - Ruby Fire */
|
|
||||||
@plugin "daisyui/theme" {
|
|
||||||
name: "palette-06-light";
|
|
||||||
color-scheme: light;
|
|
||||||
--color-primary: oklch(55% 0.25 15);
|
|
||||||
--color-primary-content: oklch(98% 0.01 15);
|
|
||||||
--color-secondary: oklch(65% 0.20 195);
|
|
||||||
--color-secondary-content: oklch(98% 0.01 195);
|
|
||||||
--color-accent: oklch(60% 0.30 120);
|
|
||||||
--color-accent-content: oklch(98% 0.01 120);
|
|
||||||
--color-neutral: oklch(60% 0.05 200);
|
|
||||||
--color-neutral-content: oklch(98% 0.01 200);
|
|
||||||
--color-base-100: oklch(98% 0.01 200);
|
|
||||||
--color-base-200: oklch(95% 0.02 200);
|
|
||||||
--color-base-300: oklch(90% 0.03 200);
|
|
||||||
--color-base-content: oklch(25% 0.05 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
@plugin "daisyui/theme" {
|
|
||||||
name: "palette-06-dark";
|
|
||||||
color-scheme: dark;
|
|
||||||
--color-primary: oklch(65% 0.28 15);
|
|
||||||
--color-primary-content: oklch(25% 0.05 200);
|
|
||||||
--color-secondary: oklch(70% 0.23 195);
|
|
||||||
--color-secondary-content: oklch(25% 0.05 200);
|
|
||||||
--color-accent: oklch(70% 0.33 120);
|
|
||||||
--color-accent-content: oklch(25% 0.05 200);
|
|
||||||
--color-neutral: oklch(65% 0.08 200);
|
|
||||||
--color-neutral-content: oklch(25% 0.05 200);
|
|
||||||
--color-base-100: oklch(25% 0.05 200);
|
|
||||||
--color-base-200: oklch(30% 0.06 200);
|
|
||||||
--color-base-300: oklch(35% 0.07 200);
|
|
||||||
--color-base-content: oklch(95% 0.02 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Palette 07 - Cyan Steel */
|
|
||||||
@plugin "daisyui/theme" {
|
|
||||||
name: "palette-07-light";
|
|
||||||
color-scheme: light;
|
|
||||||
--color-primary: oklch(60% 0.20 200);
|
|
||||||
--color-primary-content: oklch(98% 0.01 200);
|
|
||||||
--color-secondary: oklch(55% 0.25 25);
|
|
||||||
--color-secondary-content: oklch(98% 0.01 25);
|
|
||||||
--color-accent: oklch(65% 0.30 320);
|
|
||||||
--color-accent-content: oklch(98% 0.01 320);
|
|
||||||
--color-neutral: oklch(50% 0.05 220);
|
|
||||||
--color-neutral-content: oklch(98% 0.01 220);
|
|
||||||
--color-base-100: oklch(98% 0.01 220);
|
|
||||||
--color-base-200: oklch(96% 0.02 220);
|
|
||||||
--color-base-300: oklch(92% 0.03 220);
|
|
||||||
--color-base-content: oklch(20% 0.05 220);
|
|
||||||
}
|
|
||||||
|
|
||||||
@plugin "daisyui/theme" {
|
|
||||||
name: "palette-07-dark";
|
|
||||||
color-scheme: dark;
|
|
||||||
--color-primary: oklch(70% 0.23 200);
|
|
||||||
--color-primary-content: oklch(20% 0.05 220);
|
|
||||||
--color-secondary: oklch(65% 0.28 25);
|
|
||||||
--color-secondary-content: oklch(20% 0.05 220);
|
|
||||||
--color-accent: oklch(75% 0.33 320);
|
|
||||||
--color-accent-content: oklch(20% 0.05 220);
|
|
||||||
--color-neutral: oklch(60% 0.08 220);
|
|
||||||
--color-neutral-content: oklch(20% 0.05 220);
|
|
||||||
--color-base-100: oklch(20% 0.05 220);
|
|
||||||
--color-base-200: oklch(25% 0.06 220);
|
|
||||||
--color-base-300: oklch(30% 0.07 220);
|
|
||||||
--color-base-content: oklch(96% 0.02 220);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Palette 12 - Forest Green */
|
|
||||||
@plugin "daisyui/theme" {
|
|
||||||
name: "palette-12-light";
|
|
||||||
color-scheme: light;
|
|
||||||
--color-primary: oklch(60% 0.25 140);
|
|
||||||
--color-primary-content: oklch(98% 0.01 140);
|
|
||||||
--color-secondary: oklch(65% 0.20 200);
|
|
||||||
--color-secondary-content: oklch(98% 0.01 200);
|
|
||||||
--color-accent: oklch(70% 0.30 60);
|
|
||||||
--color-accent-content: oklch(98% 0.01 60);
|
|
||||||
--color-neutral: oklch(60% 0.05 160);
|
|
||||||
--color-neutral-content: oklch(98% 0.01 160);
|
|
||||||
--color-base-100: oklch(98% 0.01 160);
|
|
||||||
--color-base-200: oklch(95% 0.02 160);
|
|
||||||
--color-base-300: oklch(90% 0.03 160);
|
|
||||||
--color-base-content: oklch(25% 0.05 160);
|
|
||||||
}
|
|
||||||
|
|
||||||
@plugin "daisyui/theme" {
|
|
||||||
name: "palette-12-dark";
|
|
||||||
color-scheme: dark;
|
|
||||||
--color-primary: oklch(70% 0.28 140);
|
|
||||||
--color-primary-content: oklch(25% 0.05 160);
|
|
||||||
--color-secondary: oklch(70% 0.23 200);
|
|
||||||
--color-secondary-content: oklch(25% 0.05 160);
|
|
||||||
--color-accent: oklch(75% 0.33 60);
|
|
||||||
--color-accent-content: oklch(25% 0.05 160);
|
|
||||||
--color-neutral: oklch(65% 0.08 160);
|
|
||||||
--color-neutral-content: oklch(25% 0.05 160);
|
|
||||||
--color-base-100: oklch(25% 0.05 160);
|
|
||||||
--color-base-200: oklch(30% 0.06 160);
|
|
||||||
--color-base-300: oklch(35% 0.07 160);
|
|
||||||
--color-base-content: oklch(95% 0.02 160);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Note: For a production app, you would want to define all 48 themes (24 palettes × 2 modes)
|
|
||||||
For now, I'm providing the pattern and key examples. The remaining themes will fall back
|
|
||||||
to the default light/dark themes when not explicitly defined. */
|
|
||||||
|
|
||||||
/* Desktop app specific styles */
|
|
||||||
body {
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure proper theme transitions */
|
|
||||||
* {
|
|
||||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 150ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Base styles for the desktop app */
|
|
||||||
html, body {
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
/* Prevent dragging by default - only title bar should be draggable */
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
}
|
|
||||||
|
|
||||||
#__nuxt {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom scrollbar styles using theme colors */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background-color: oklch(var(--b2));
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background-color: oklch(var(--b3));
|
|
||||||
border-radius: 9999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background-color: oklch(var(--n));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading animation */
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Login page layout - not covered by DaisyUI */
|
|
||||||
.login-container {
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Desktop app styling */
|
|
||||||
.desktop-container {
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar styling for desktop app */
|
|
||||||
.scrollbar-thin {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(156, 163, 175, 0.3) transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollbar-thin::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollbar-thin::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
|
||||||
background-color: rgba(156, 163, 175, 0.3);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
|
||||||
background-color: rgba(156, 163, 175, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Window drag regions - important for Electron */
|
|
||||||
/* By default, everything is no-drag. Only the title bar has drag enabled. */
|
|
||||||
/* This prevents forms, buttons, and other interactive elements from being draggable */
|
|
||||||
|
|
||||||
/* Remove web-like behaviors */
|
|
||||||
button:focus,
|
|
||||||
input:focus,
|
|
||||||
textarea:focus {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Desktop-style buttons */
|
|
||||||
.btn-desktop {
|
|
||||||
border-radius: 6px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-desktop:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Desktop-style cards */
|
|
||||||
.card-desktop {
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-card {
|
|
||||||
width: 400px;
|
|
||||||
max-width: 90vw;
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 2rem;
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Base styles for the desktop app */
|
|
||||||
.drag-region {
|
|
||||||
-webkit-app-region: drag;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-drag {
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
}
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="navbar bg-base-300 px-4">
|
|
||||||
<div class="navbar-start">
|
|
||||||
<div class="text-xl font-bold">{{ title }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="navbar-end">
|
|
||||||
<div class="dropdown dropdown-end">
|
|
||||||
<div
|
|
||||||
tabindex="0"
|
|
||||||
role="button"
|
|
||||||
class="btn btn-ghost btn-circle avatar"
|
|
||||||
>
|
|
||||||
<div class="w-10 rounded-full bg-primary flex items-center justify-center">
|
|
||||||
<span class="text-primary-content font-bold text-sm">
|
|
||||||
{{ userInitials }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
tabindex="0"
|
|
||||||
class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-64"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<a @click="navigateToProfile">Profile</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<details>
|
|
||||||
<summary>Theme Settings</summary>
|
|
||||||
<div class="p-4">
|
|
||||||
<ThemeSwitcher />
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a @click="handleLogout">Logout</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { useNavigation } from '../composables/navigation';
|
|
||||||
import { useAppStore } from '../stores/app';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
title: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
// Validate title prop
|
|
||||||
const validateTitle = (title: string): boolean => {
|
|
||||||
return typeof title === 'string' && title.length > 0 && title.length <= 50;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Validate props
|
|
||||||
if (!validateTitle(props.title)) {
|
|
||||||
console.warn('AppNavbar: title prop should be a non-empty string with max 50 characters');
|
|
||||||
}
|
|
||||||
|
|
||||||
const appStore = useAppStore();
|
|
||||||
const { navigateToProfile, handleLogout } = useNavigation();
|
|
||||||
|
|
||||||
const userInitials = computed(() => appStore.userInitials);
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="w-64 bg-base-200 p-4">
|
|
||||||
<ul class="menu">
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
:class="{ active: currentRoute === 'dashboard' }"
|
|
||||||
@click="navigateToDashboard"
|
|
||||||
>
|
|
||||||
Dashboard
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
:class="{ active: currentRoute === 'profile' }"
|
|
||||||
@click="navigateToProfile"
|
|
||||||
>
|
|
||||||
Profile
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li><a>Trading</a></li>
|
|
||||||
<li><a>Portfolio</a></li>
|
|
||||||
<li><a>Markets</a></li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
:class="{ active: currentRoute === 'hunting-ground' }"
|
|
||||||
@click="navigateToHuntingGround"
|
|
||||||
>
|
|
||||||
Hunting Ground
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li><a>Analytics</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useNavigation } from '../composables/navigation';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
currentRoute: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
defineProps<Props>();
|
|
||||||
|
|
||||||
const { navigateToDashboard, navigateToProfile, navigateToHuntingGround } = useNavigation();
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,538 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="cex-analysis-card relative bg-base-100 hover:bg-base-200/50 transition-all duration-200 cursor-pointer border-b border-base-300 last:border-b-0"
|
|
||||||
:class="cardClass"
|
|
||||||
@click="$emit('click', token)"
|
|
||||||
>
|
|
||||||
<!-- Quick actions (visible on hover) -->
|
|
||||||
<div class="absolute top-2 right-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex gap-1">
|
|
||||||
<button
|
|
||||||
class="w-6 h-6 bg-base-300/90 hover:bg-error rounded text-base-content/60 hover:text-error-content transition-colors flex items-center justify-center text-xs"
|
|
||||||
title="Close token"
|
|
||||||
@click.stop="$emit('close', token)"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:x-mark" class="w-3 h-3" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="w-6 h-6 bg-base-300/90 hover:bg-base-300 rounded text-base-content/60 hover:text-primary transition-colors flex items-center justify-center text-xs"
|
|
||||||
title="Hide token"
|
|
||||||
@click.stop="$emit('hide', token)"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:eye-slash" class="w-3 h-3" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="w-6 h-6 bg-base-300/90 hover:bg-base-300 rounded text-base-content/60 hover:text-warning transition-colors flex items-center justify-center text-xs"
|
|
||||||
title="Watch token"
|
|
||||||
@click.stop="$emit('watch', token)"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:bookmark" class="w-3 h-3" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Quick buy button (bottom right) -->
|
|
||||||
<div class="absolute bottom-2 right-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
|
||||||
<button
|
|
||||||
class="bg-primary hover:bg-primary/80 text-primary-content px-2 py-1 rounded text-xs font-medium flex items-center gap-1 shadow-sm"
|
|
||||||
@click.stop="$emit('quick-buy', token)"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:bolt" class="w-3 h-3" />
|
|
||||||
Quick Buy
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main content -->
|
|
||||||
<div class="p-3 group">
|
|
||||||
<div class="flex items-start gap-3">
|
|
||||||
<!-- Token image/avatar -->
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<div v-if="metadata?.image && !imageError" class="w-10 h-10 rounded-lg overflow-hidden bg-base-300 relative">
|
|
||||||
<img
|
|
||||||
:src="metadata.image"
|
|
||||||
:alt="token.name"
|
|
||||||
class="w-full h-full object-cover"
|
|
||||||
@error="handleImageError"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="_metadataLoading" class="w-10 h-10 bg-base-300 rounded-lg flex items-center justify-center">
|
|
||||||
<div class="loading loading-spinner loading-xs text-primary" />
|
|
||||||
</div>
|
|
||||||
<div v-else-if="_metadataError" class="w-10 h-10 bg-error/20 rounded-lg flex items-center justify-center" :title="_metadataError">
|
|
||||||
<Icon name="heroicons:exclamation-triangle" class="w-4 h-4 text-error" />
|
|
||||||
</div>
|
|
||||||
<div v-else class="w-10 h-10 bg-gradient-to-br from-primary to-secondary rounded-lg flex items-center justify-center">
|
|
||||||
<span class="text-primary-content font-bold text-sm">{{ token.name?.charAt(0) || '?' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Token info -->
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<!-- Header with name and CEX badge -->
|
|
||||||
<div class="flex items-center gap-2 mb-1">
|
|
||||||
<h3 class="font-semibold text-sm text-base-content truncate">{{ token.name }}</h3>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<!-- CEX badge with icon -->
|
|
||||||
<div
|
|
||||||
class="badge badge-xs flex items-center gap-1 px-2 py-1"
|
|
||||||
:class="cexBadgeClass"
|
|
||||||
>
|
|
||||||
<Icon :name="cexIcon" class="w-3 h-3" />
|
|
||||||
{{ cexDisplayName }}
|
|
||||||
</div>
|
|
||||||
<!-- CEX wallet type (if not main exchange name) -->
|
|
||||||
<span v-if="cexWalletType" class="text-xs text-base-content/50 font-mono">
|
|
||||||
{{ cexWalletType }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Address -->
|
|
||||||
<div class="flex items-center gap-2 mb-2">
|
|
||||||
<span class="text-xs text-base-content/60 font-mono">{{ truncateAddress(mintAddress) }}</span>
|
|
||||||
<button
|
|
||||||
class="text-base-content/40 hover:text-primary transition-colors"
|
|
||||||
title="Copy address"
|
|
||||||
@click.stop="copyToClipboard(mintAddress)"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:clipboard-document" class="w-3 h-3" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Analysis info and creator -->
|
|
||||||
<div class="flex items-center justify-between mb-2">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<!-- Analysis type badge -->
|
|
||||||
<span class="badge badge-info badge-xs">
|
|
||||||
CEX ANALYSIS
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- Dev info (if not unknown_dev) -->
|
|
||||||
<div v-if="showDevInfo" class="flex items-center gap-1">
|
|
||||||
<Icon name="heroicons:user-circle" class="w-3 h-3 text-warning" />
|
|
||||||
<span class="text-xs text-warning font-medium">{{ token.dev_name }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Creator with graph tooltip -->
|
|
||||||
<div
|
|
||||||
class="relative"
|
|
||||||
@mouseenter="showGraphTooltip = true"
|
|
||||||
@mouseleave="showGraphTooltip = false"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
:href="`https://solscan.io/account/${token.creator}`"
|
|
||||||
target="_blank"
|
|
||||||
class="flex items-center gap-1 text-xs text-base-content/50 hover:text-primary transition-colors"
|
|
||||||
title="View creator on Solscan"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:user" class="w-3 h-3" />
|
|
||||||
{{ truncateAddress(token.creator) }}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<!-- Graph tooltip -->
|
|
||||||
<div
|
|
||||||
v-if="showGraphTooltip && graphNodes.length > 0"
|
|
||||||
class="absolute bottom-full left-0 mb-2 z-50 bg-base-200 rounded-lg shadow-lg border border-base-300 p-3 min-w-[200px]"
|
|
||||||
>
|
|
||||||
<div class="text-xs font-medium text-base-content mb-2">Connection Graph</div>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<div v-for="node in graphNodes.slice(0, 5)" :key="node.id" class="flex items-center gap-2 text-xs">
|
|
||||||
<div class="w-2 h-2 rounded-full bg-primary" />
|
|
||||||
<span class="font-mono text-base-content/70">{{ truncateAddress(node.id) }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="graphNodes.length > 5" class="text-xs text-base-content/50 text-center pt-1">
|
|
||||||
+{{ graphNodes.length - 5 }} more nodes
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 pt-2 border-t border-base-300 text-xs text-base-content/60">
|
|
||||||
{{ token.node_count }} nodes, {{ token.edge_count }} edges
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Analysis duration -->
|
|
||||||
<span class="text-xs text-base-content/50">
|
|
||||||
{{ analysisDuration }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Social links (if available) -->
|
|
||||||
<div v-if="metadata && hasSocialLinks" class="flex items-center gap-1 mt-2">
|
|
||||||
<a
|
|
||||||
v-if="metadata.twitter"
|
|
||||||
:href="metadata.twitter"
|
|
||||||
target="_blank"
|
|
||||||
class="w-5 h-5 text-base-content/40 hover:text-info transition-colors"
|
|
||||||
title="Twitter"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<Icon name="simple-icons:x" class="w-3 h-3" />
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
v-if="metadata.telegram"
|
|
||||||
:href="metadata.telegram"
|
|
||||||
target="_blank"
|
|
||||||
class="w-5 h-5 text-base-content/40 hover:text-info transition-colors"
|
|
||||||
title="Telegram"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<Icon name="simple-icons:telegram" class="w-3 h-3" />
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
v-if="metadata.website"
|
|
||||||
:href="metadata.website"
|
|
||||||
target="_blank"
|
|
||||||
class="w-5 h-5 text-base-content/40 hover:text-info transition-colors"
|
|
||||||
title="Website"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:globe-alt" class="w-3 h-3" />
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
v-if="metadata.discord"
|
|
||||||
:href="metadata.discord"
|
|
||||||
target="_blank"
|
|
||||||
class="w-5 h-5 text-base-content/40 hover:text-info transition-colors"
|
|
||||||
title="Discord"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<Icon name="simple-icons:discord" class="w-3 h-3" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, onMounted, ref } from 'vue';
|
|
||||||
import { truncateAddress as truncateAddr } from '~/utils/address';
|
|
||||||
import type { MaxDepthReachedData, TokenCexUpdatedData, TokenMetadata } from '../../types/redis-events';
|
|
||||||
import { fetchTokenMetadata } from '../utils/ipfs';
|
|
||||||
|
|
||||||
// Props
|
|
||||||
interface Props {
|
|
||||||
token: TokenCexUpdatedData | MaxDepthReachedData;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
|
||||||
|
|
||||||
// Emits
|
|
||||||
interface Emits {
|
|
||||||
(e: 'click' | 'hide' | 'watch' | 'quick-buy' | 'close', token: TokenCexUpdatedData | MaxDepthReachedData): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
defineEmits<Emits>();
|
|
||||||
|
|
||||||
// Reactive data
|
|
||||||
const imageError = ref(false);
|
|
||||||
const mintAddress = ref<string>('');
|
|
||||||
const showGraphTooltip = ref(false);
|
|
||||||
|
|
||||||
// Simple metadata state management
|
|
||||||
const metadata = ref<TokenMetadata | null>(null);
|
|
||||||
const _metadataLoading = ref(false);
|
|
||||||
const _metadataError = ref<string | null>(null);
|
|
||||||
|
|
||||||
// CEX mapping utilities
|
|
||||||
const getCexInfo = (cexName: string) => {
|
|
||||||
const name = cexName.toLowerCase();
|
|
||||||
|
|
||||||
// Extract base exchange name and wallet type
|
|
||||||
if (name.includes('coinbase')) {
|
|
||||||
const type = name.replace('coinbase_', '').replace('coinbase', '');
|
|
||||||
return {
|
|
||||||
baseName: 'Coinbase',
|
|
||||||
walletType: type ? type.toUpperCase() : '',
|
|
||||||
color: 'badge-info',
|
|
||||||
icon: 'simple-icons:coinbase'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.includes('binance')) {
|
|
||||||
const type = name.replace('binance_', '').replace('binance', '');
|
|
||||||
return {
|
|
||||||
baseName: 'Binance',
|
|
||||||
walletType: type ? type.toUpperCase() : '',
|
|
||||||
color: 'badge-warning',
|
|
||||||
icon: 'simple-icons:binance'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.includes('okx')) {
|
|
||||||
const type = name.replace('okx_', '').replace('okx', '');
|
|
||||||
return {
|
|
||||||
baseName: 'OKX',
|
|
||||||
walletType: type ? type.toUpperCase() : '',
|
|
||||||
color: 'badge-primary',
|
|
||||||
icon: 'heroicons:building-office'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.includes('kraken')) {
|
|
||||||
const type = name.replace('kraken_', '').replace('kraken', '');
|
|
||||||
return {
|
|
||||||
baseName: 'Kraken',
|
|
||||||
walletType: type ? type.toUpperCase() : '',
|
|
||||||
color: 'badge-secondary',
|
|
||||||
icon: 'heroicons:building-office-2'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.includes('mexc')) {
|
|
||||||
const type = name.replace('mexc_', '').replace('mexc', '');
|
|
||||||
return {
|
|
||||||
baseName: 'MEXC',
|
|
||||||
walletType: type ? type.toUpperCase() : '',
|
|
||||||
color: 'badge-accent',
|
|
||||||
icon: 'heroicons:building-office'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.includes('bitget')) {
|
|
||||||
const type = name.replace('bitget_', '').replace('bitget', '');
|
|
||||||
return {
|
|
||||||
baseName: 'Bitget',
|
|
||||||
walletType: type ? type.toUpperCase() : '',
|
|
||||||
color: 'badge-info',
|
|
||||||
icon: 'heroicons:building-office'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.includes('gateio') || name.includes('gate.io')) {
|
|
||||||
const type = name.replace('gateio_', '').replace('gateio', '');
|
|
||||||
return {
|
|
||||||
baseName: 'Gate.io',
|
|
||||||
walletType: type ? type.toUpperCase() : '',
|
|
||||||
color: 'badge-primary',
|
|
||||||
icon: 'heroicons:building-office'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.includes('bybit')) {
|
|
||||||
const type = name.replace('bybit_', '').replace('bybit', '');
|
|
||||||
return {
|
|
||||||
baseName: 'Bybit',
|
|
||||||
walletType: type ? type.toUpperCase() : '',
|
|
||||||
color: 'badge-warning',
|
|
||||||
icon: 'heroicons:building-office'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.includes('bitfinex')) {
|
|
||||||
const type = name.replace('bitfinex_', '').replace('bitfinex', '');
|
|
||||||
return {
|
|
||||||
baseName: 'Bitfinex',
|
|
||||||
walletType: type ? type.toUpperCase() : '',
|
|
||||||
color: 'badge-success',
|
|
||||||
icon: 'heroicons:building-office'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.includes('kucoin')) {
|
|
||||||
const type = name.replace('kucoin_', '').replace('kucoin', '');
|
|
||||||
return {
|
|
||||||
baseName: 'KuCoin',
|
|
||||||
walletType: type ? type.toUpperCase() : '',
|
|
||||||
color: 'badge-accent',
|
|
||||||
icon: 'heroicons:building-office'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.includes('poloniex')) {
|
|
||||||
const type = name.replace('poloniex_', '').replace('poloniex', '');
|
|
||||||
return {
|
|
||||||
baseName: 'Poloniex',
|
|
||||||
walletType: type ? type.toUpperCase() : '',
|
|
||||||
color: 'badge-neutral',
|
|
||||||
icon: 'heroicons:building-office'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.includes('lbank')) {
|
|
||||||
return {
|
|
||||||
baseName: 'LBank',
|
|
||||||
walletType: '',
|
|
||||||
color: 'badge-neutral',
|
|
||||||
icon: 'heroicons:building-office'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.includes('debridge')) {
|
|
||||||
return {
|
|
||||||
baseName: 'DeBridge',
|
|
||||||
walletType: 'VAULT',
|
|
||||||
color: 'badge-secondary',
|
|
||||||
icon: 'heroicons:shield-check'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.includes('revolut')) {
|
|
||||||
return {
|
|
||||||
baseName: 'Revolut',
|
|
||||||
walletType: 'HOT',
|
|
||||||
color: 'badge-info',
|
|
||||||
icon: 'heroicons:credit-card'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.includes('bitstamp')) {
|
|
||||||
return {
|
|
||||||
baseName: 'BitStamp',
|
|
||||||
walletType: 'HOT',
|
|
||||||
color: 'badge-success',
|
|
||||||
icon: 'heroicons:building-office'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.includes('stakecom')) {
|
|
||||||
return {
|
|
||||||
baseName: 'Stake.com',
|
|
||||||
walletType: 'HOT',
|
|
||||||
color: 'badge-warning',
|
|
||||||
icon: 'heroicons:fire'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default for unknown exchanges
|
|
||||||
return {
|
|
||||||
baseName: cexName.replace(/_/g, ' ').toUpperCase(),
|
|
||||||
walletType: '',
|
|
||||||
color: 'badge-neutral',
|
|
||||||
icon: 'heroicons:building-office'
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Computed properties
|
|
||||||
const cexInfo = computed(() => getCexInfo(props.token.cex_name));
|
|
||||||
|
|
||||||
const cardClass = computed(() => {
|
|
||||||
const baseClass = 'h-[140px] min-h-[140px]';
|
|
||||||
// Use CEX-specific border color
|
|
||||||
if (cexInfo.value.color.includes('info')) {
|
|
||||||
return `${baseClass} border-l-2 border-l-info`;
|
|
||||||
} else if (cexInfo.value.color.includes('warning')) {
|
|
||||||
return `${baseClass} border-l-2 border-l-warning`;
|
|
||||||
} else if (cexInfo.value.color.includes('success')) {
|
|
||||||
return `${baseClass} border-l-2 border-l-success`;
|
|
||||||
} else if (cexInfo.value.color.includes('primary')) {
|
|
||||||
return `${baseClass} border-l-2 border-l-primary`;
|
|
||||||
} else if (cexInfo.value.color.includes('secondary')) {
|
|
||||||
return `${baseClass} border-l-2 border-l-secondary`;
|
|
||||||
} else if (cexInfo.value.color.includes('accent')) {
|
|
||||||
return `${baseClass} border-l-2 border-l-accent`;
|
|
||||||
}
|
|
||||||
return `${baseClass} border-l-2 border-l-neutral`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const cexBadgeClass = computed(() => cexInfo.value.color);
|
|
||||||
const cexDisplayName = computed(() => cexInfo.value.baseName);
|
|
||||||
const cexWalletType = computed(() => cexInfo.value.walletType);
|
|
||||||
const cexIcon = computed(() => cexInfo.value.icon);
|
|
||||||
|
|
||||||
const showDevInfo = computed(() => {
|
|
||||||
return props.token.dev_name && props.token.dev_name !== 'unknown_dev';
|
|
||||||
});
|
|
||||||
|
|
||||||
const analysisDuration = computed(() => {
|
|
||||||
const createdAt = typeof props.token.created_at === 'string'
|
|
||||||
? parseInt(props.token.created_at)
|
|
||||||
: props.token.created_at;
|
|
||||||
const updatedAt = typeof props.token.updated_at === 'string'
|
|
||||||
? parseInt(props.token.updated_at)
|
|
||||||
: props.token.updated_at;
|
|
||||||
|
|
||||||
const durationSeconds = updatedAt - createdAt;
|
|
||||||
|
|
||||||
if (durationSeconds < 60) {
|
|
||||||
return `${durationSeconds}s analysis`;
|
|
||||||
} else if (durationSeconds < 3600) {
|
|
||||||
const minutes = Math.floor(durationSeconds / 60);
|
|
||||||
return `${minutes}m analysis`;
|
|
||||||
} else {
|
|
||||||
const hours = Math.floor(durationSeconds / 3600);
|
|
||||||
const minutes = Math.floor((durationSeconds % 3600) / 60);
|
|
||||||
return `${hours}h ${minutes}m analysis`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const graphNodes = computed(() => {
|
|
||||||
try {
|
|
||||||
if (props.token.graph && typeof props.token.graph === 'object') {
|
|
||||||
const graph = props.token.graph as { graph?: { nodes?: Array<{ id: string }> } };
|
|
||||||
if (graph.graph && graph.graph.nodes) {
|
|
||||||
return graph.graph.nodes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error parsing graph data:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasSocialLinks = computed(() => {
|
|
||||||
return metadata.value && (
|
|
||||||
metadata.value.twitter ||
|
|
||||||
metadata.value.telegram ||
|
|
||||||
metadata.value.website ||
|
|
||||||
metadata.value.discord
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
const truncateAddress = (address: string): string => {
|
|
||||||
return truncateAddr(address);
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyToClipboard = async (text: string): Promise<void> => {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(text);
|
|
||||||
// You could add a toast notification here
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to copy to clipboard:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImageError = (): void => {
|
|
||||||
imageError.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load metadata on mount
|
|
||||||
onMounted(async () => {
|
|
||||||
// Set mint address (now it's already a string)
|
|
||||||
mintAddress.value = props.token.mint;
|
|
||||||
|
|
||||||
// Load metadata if URI exists
|
|
||||||
if (props.token.uri) {
|
|
||||||
_metadataLoading.value = true;
|
|
||||||
_metadataError.value = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await fetchTokenMetadata(props.token.uri);
|
|
||||||
metadata.value = result;
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch metadata';
|
|
||||||
_metadataError.value = errorMessage;
|
|
||||||
} finally {
|
|
||||||
_metadataLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.cex-analysis-card {
|
|
||||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cex-analysis-card:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.group:hover .opacity-0 {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,205 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<!-- Dark Mode Toggle -->
|
|
||||||
<label class="swap swap-rotate">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
class="theme-controller"
|
|
||||||
:checked="themeStore.isDark"
|
|
||||||
@change="themeStore.toggleDarkMode()"
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- Sun icon -->
|
|
||||||
<svg
|
|
||||||
class="swap-off fill-current w-6 h-6"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<!-- Moon icon -->
|
|
||||||
<svg
|
|
||||||
class="swap-on fill-current w-6 h-6"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
|
|
||||||
</svg>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<!-- Palette Dropdown -->
|
|
||||||
<div class="dropdown dropdown-end">
|
|
||||||
<div
|
|
||||||
tabindex="0"
|
|
||||||
role="button"
|
|
||||||
class="btn btn-sm btn-outline flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
class="w-4 h-4 rounded-full border border-base-content/20"
|
|
||||||
:style="{ backgroundColor: getPalettePreviewColor(themeStore.currentPalette, 'primary') }"
|
|
||||||
/>
|
|
||||||
<span class="hidden sm:inline">{{ themeStore.currentPaletteName }}</span>
|
|
||||||
<span class="sm:hidden">P{{ themeStore.currentPalette }}</span>
|
|
||||||
</div>
|
|
||||||
<svg
|
|
||||||
class="w-4 h-4"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M19 9l-7 7-7-7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
tabindex="0"
|
|
||||||
class="dropdown-content z-[1] card card-compact w-80 p-4 shadow-lg bg-base-100 border border-base-300"
|
|
||||||
>
|
|
||||||
<div class="card-body p-0">
|
|
||||||
<h3 class="font-semibold text-sm text-base-content/70 mb-3">Choose Color Palette</h3>
|
|
||||||
|
|
||||||
<div class="max-h-64 overflow-y-auto">
|
|
||||||
<div class="space-y-1">
|
|
||||||
<button
|
|
||||||
v-for="paletteId in themeStore.availablePalettes"
|
|
||||||
:key="`palette-${paletteId}`"
|
|
||||||
:class="{
|
|
||||||
'bg-primary/10 border-primary': themeStore.currentPalette === paletteId,
|
|
||||||
'hover:bg-base-200': themeStore.currentPalette !== paletteId,
|
|
||||||
}"
|
|
||||||
class="w-full flex items-center justify-between p-3 rounded-lg border border-transparent transition-all duration-200"
|
|
||||||
@click="themeStore.setPalette(paletteId)"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<span class="text-xs font-mono text-base-content/50 w-6">
|
|
||||||
{{ paletteId.toString().padStart(2, '0') }}
|
|
||||||
</span>
|
|
||||||
<span class="text-sm font-medium">
|
|
||||||
{{ themeStore.paletteNames[paletteId] || `Palette ${paletteId}` }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<!-- Color preview circles with better spacing -->
|
|
||||||
<div
|
|
||||||
class="w-4 h-4 rounded-full border border-base-content/20"
|
|
||||||
:style="{ backgroundColor: getPalettePreviewColor(paletteId, 'primary') }"
|
|
||||||
title="Primary"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="w-4 h-4 rounded-full border border-base-content/20"
|
|
||||||
:style="{ backgroundColor: getPalettePreviewColor(paletteId, 'secondary') }"
|
|
||||||
title="Secondary"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="w-4 h-4 rounded-full border border-base-content/20"
|
|
||||||
:style="{ backgroundColor: getPalettePreviewColor(paletteId, 'accent') }"
|
|
||||||
title="Accent"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Checkmark for active palette -->
|
|
||||||
<div class="w-4 h-4 flex items-center justify-center ml-2">
|
|
||||||
<svg
|
|
||||||
v-if="themeStore.currentPalette === paletteId"
|
|
||||||
class="w-3 h-3 text-primary"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Action buttons -->
|
|
||||||
<div class="flex gap-2 pt-3 mt-3 border-t border-base-300">
|
|
||||||
<button
|
|
||||||
class="btn btn-ghost btn-sm flex-1 text-xs"
|
|
||||||
@click="themeStore.resetToDefault()"
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-ghost btn-sm flex-1 text-xs"
|
|
||||||
@click="themeStore.setRandomPalette()"
|
|
||||||
>
|
|
||||||
Random
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { onMounted } from 'vue';
|
|
||||||
import { useThemeStore } from '../stores/theme';
|
|
||||||
|
|
||||||
const themeStore = useThemeStore();
|
|
||||||
|
|
||||||
// Simple color preview function - you can enhance this based on your palette definitions
|
|
||||||
function getPalettePreviewColor(paletteId: number, colorType: 'primary' | 'secondary' | 'accent'): string {
|
|
||||||
// This is a simplified preview - in a real implementation, you might want to
|
|
||||||
// extract actual colors from your theme definitions
|
|
||||||
const hueBase = (paletteId - 1) * 15; // Distribute hues across the color wheel
|
|
||||||
|
|
||||||
const hues = {
|
|
||||||
primary: hueBase,
|
|
||||||
secondary: (hueBase + 60) % 360,
|
|
||||||
accent: (hueBase + 120) % 360,
|
|
||||||
};
|
|
||||||
|
|
||||||
return `hsl(${hues[colorType]}, 70%, 50%)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize theme when component mounts
|
|
||||||
onMounted(() => {
|
|
||||||
themeStore.initializeTheme();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* Custom dropdown styles for better desktop app feel */
|
|
||||||
.dropdown-content {
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Smooth transitions for theme changes */
|
|
||||||
.btn, .swap {
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom scrollbar for the palette list */
|
|
||||||
.max-h-64::-webkit-scrollbar {
|
|
||||||
width: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.max-h-64::-webkit-scrollbar-track {
|
|
||||||
background: oklch(var(--color-base-200));
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.max-h-64::-webkit-scrollbar-thumb {
|
|
||||||
background: oklch(var(--color-base-content) / 0.3);
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.max-h-64::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: oklch(var(--color-base-content) / 0.5);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="h-8 bg-base-300 border-b border-base-content/10 flex items-center justify-between px-4 select-none" style="-webkit-app-region: drag;">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="text-primary">
|
|
||||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<span class="text-base-content text-sm font-medium">Ziya</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-1" style="-webkit-app-region: no-drag;">
|
|
||||||
<!-- Theme Switcher -->
|
|
||||||
<ThemeSwitcher />
|
|
||||||
|
|
||||||
<!-- Window Controls -->
|
|
||||||
<button
|
|
||||||
class="w-8 h-8 flex items-center justify-center text-base-content/60 hover:text-base-content hover:bg-base-200 transition-colors duration-150 rounded"
|
|
||||||
title="Minimize"
|
|
||||||
@click="minimizeWindow"
|
|
||||||
>
|
|
||||||
<svg class="w-3 h-3" viewBox="0 0 12 12" fill="none">
|
|
||||||
<rect x="2" y="5.5" width="8" height="1" fill="currentColor" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="w-8 h-8 flex items-center justify-center text-base-content/60 hover:text-base-content hover:bg-base-200 transition-colors duration-150 rounded"
|
|
||||||
:title="isMaximized ? 'Restore' : 'Maximize'"
|
|
||||||
@click="maximizeWindow"
|
|
||||||
>
|
|
||||||
<svg v-if="isMaximized" class="w-3 h-3" viewBox="0 0 12 12" fill="none">
|
|
||||||
<rect x="2" y="2" width="6" height="6" stroke="currentColor" stroke-width="1" fill="none" />
|
|
||||||
<path d="M4 4V1h7v7h-3" stroke="currentColor" stroke-width="1" fill="none" />
|
|
||||||
</svg>
|
|
||||||
<svg v-else class="w-3 h-3" viewBox="0 0 12 12" fill="none">
|
|
||||||
<rect x="2" y="2" width="8" height="8" stroke="currentColor" stroke-width="1" fill="none" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="w-8 h-8 flex items-center justify-center text-base-content/60 hover:text-error hover:bg-error/10 transition-colors duration-150 rounded"
|
|
||||||
title="Close"
|
|
||||||
@click="closeWindow"
|
|
||||||
>
|
|
||||||
<svg class="w-3 h-3" viewBox="0 0 12 12" fill="none">
|
|
||||||
<path d="M9 3L3 9M3 3l6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { onMounted, onUnmounted, ref } from 'vue';
|
|
||||||
import ThemeSwitcher from './ThemeSwitcher.vue';
|
|
||||||
|
|
||||||
const isMaximized = ref(false);
|
|
||||||
|
|
||||||
// Window control methods
|
|
||||||
const minimizeWindow = async () => {
|
|
||||||
if (window.electronAPI) {
|
|
||||||
await window.electronAPI.minimizeWindow();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const maximizeWindow = async () => {
|
|
||||||
if (window.electronAPI) {
|
|
||||||
await window.electronAPI.maximizeWindow();
|
|
||||||
// Update maximized state
|
|
||||||
isMaximized.value = await window.electronAPI.isMaximized();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeWindow = async () => {
|
|
||||||
if (window.electronAPI) {
|
|
||||||
await window.electronAPI.closeWindow();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Listen for maximize state changes
|
|
||||||
const handleMaximizeChange = (_event: unknown, maximized: boolean) => {
|
|
||||||
isMaximized.value = maximized;
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
if (window.electronAPI) {
|
|
||||||
// Get initial maximized state
|
|
||||||
isMaximized.value = await window.electronAPI.isMaximized();
|
|
||||||
|
|
||||||
// Listen for maximize state changes
|
|
||||||
window.electronAPI.onMaximizeChange(handleMaximizeChange);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (window.electronAPI) {
|
|
||||||
window.electronAPI.removeMaximizeListener(handleMaximizeChange);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
@ -1,395 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="token-card relative bg-base-100 hover:bg-base-200/50 transition-all duration-200 cursor-pointer border-b border-base-300 last:border-b-0"
|
|
||||||
:class="cardClass"
|
|
||||||
@click="$emit('click', token)"
|
|
||||||
>
|
|
||||||
<!-- Quick actions (visible on hover) -->
|
|
||||||
<div class="absolute top-2 right-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex gap-1">
|
|
||||||
<button
|
|
||||||
class="w-6 h-6 bg-base-300/90 hover:bg-error rounded text-base-content/60 hover:text-error-content transition-colors flex items-center justify-center text-xs"
|
|
||||||
title="Close token"
|
|
||||||
@click.stop="$emit('close', token)"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:x-mark" class="w-3 h-3" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="w-6 h-6 bg-base-300/90 hover:bg-base-300 rounded text-base-content/60 hover:text-primary transition-colors flex items-center justify-center text-xs"
|
|
||||||
title="Hide token"
|
|
||||||
@click.stop="$emit('hide', token)"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:eye-slash" class="w-3 h-3" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="w-6 h-6 bg-base-300/90 hover:bg-base-300 rounded text-base-content/60 hover:text-warning transition-colors flex items-center justify-center text-xs"
|
|
||||||
title="Watch token"
|
|
||||||
@click.stop="$emit('watch', token)"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:bookmark" class="w-3 h-3" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Quick buy button (bottom right) -->
|
|
||||||
<div class="absolute bottom-2 right-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
|
||||||
<button
|
|
||||||
class="bg-primary hover:bg-primary/80 text-primary-content px-2 py-1 rounded text-xs font-medium flex items-center gap-1 shadow-sm"
|
|
||||||
@click.stop="$emit('quick-buy', token)"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:bolt" class="w-3 h-3" />
|
|
||||||
Quick Buy
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main content -->
|
|
||||||
<div class="p-3 group">
|
|
||||||
<div class="flex items-start gap-3">
|
|
||||||
<!-- Token image/avatar -->
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<div v-if="metadata?.image && !imageError" class="w-10 h-10 rounded-lg overflow-hidden bg-base-300 relative">
|
|
||||||
<img
|
|
||||||
:src="metadata.image"
|
|
||||||
:alt="token.name"
|
|
||||||
class="w-full h-full object-cover"
|
|
||||||
@error="handleImageError"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="_metadataLoading" class="w-10 h-10 bg-base-300 rounded-lg flex items-center justify-center">
|
|
||||||
<div class="loading loading-spinner loading-xs text-primary" />
|
|
||||||
</div>
|
|
||||||
<div v-else-if="_metadataError" class="w-10 h-10 bg-error/20 rounded-lg flex items-center justify-center" :title="_metadataError">
|
|
||||||
<Icon name="heroicons:exclamation-triangle" class="w-4 h-4 text-error" />
|
|
||||||
</div>
|
|
||||||
<div v-else class="w-10 h-10 bg-gradient-to-br from-primary to-secondary rounded-lg flex items-center justify-center">
|
|
||||||
<span class="text-primary-content font-bold text-sm">{{ getTokenSymbol(token)?.charAt(0) || '?' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Token info -->
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<!-- Header with name and symbol -->
|
|
||||||
<div class="flex items-center gap-2 mb-1">
|
|
||||||
<h3 class="font-semibold text-sm text-base-content truncate">{{ token.name }}</h3>
|
|
||||||
<span v-if="getTokenSymbol(token)" class="badge badge-primary badge-xs">{{ getTokenSymbol(token) }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Address -->
|
|
||||||
<div class="flex items-center gap-2 mb-2">
|
|
||||||
<span class="text-xs text-base-content/60 font-mono">{{ truncateAddress(getMintAddress()) }}</span>
|
|
||||||
<button
|
|
||||||
class="text-base-content/40 hover:text-primary transition-colors"
|
|
||||||
title="Copy address"
|
|
||||||
@click.stop="copyToClipboard(getMintAddress())"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:clipboard-document" class="w-3 h-3" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Type-specific info -->
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<!-- Type indicator -->
|
|
||||||
<span
|
|
||||||
class="badge badge-xs"
|
|
||||||
:class="typeClass"
|
|
||||||
>
|
|
||||||
{{ typeLabel }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- Creator info for new tokens -->
|
|
||||||
<a
|
|
||||||
v-if="type === 'new' && devAddress"
|
|
||||||
:href="`https://solscan.io/account/${devAddress}`"
|
|
||||||
target="_blank"
|
|
||||||
class="flex items-center gap-1 text-xs text-base-content/50 hover:text-primary transition-colors"
|
|
||||||
title="View creator on Solscan"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:user" class="w-3 h-3" />
|
|
||||||
{{ truncateAddress(devAddress) }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Timestamp -->
|
|
||||||
<span class="text-xs text-base-content/50">
|
|
||||||
{{ formatTimeAgoUtil(getDisplayTimestamp(token), currentTime) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Social links (if available) -->
|
|
||||||
<div v-if="metadata && hasSocialLinks" class="flex items-center gap-1 mt-2">
|
|
||||||
<a
|
|
||||||
v-if="metadata.twitter"
|
|
||||||
:href="metadata.twitter"
|
|
||||||
target="_blank"
|
|
||||||
class="w-5 h-5 text-base-content/40 hover:text-info transition-colors"
|
|
||||||
title="Twitter"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<Icon name="simple-icons:x" class="w-3 h-3" />
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
v-if="metadata.telegram"
|
|
||||||
:href="metadata.telegram"
|
|
||||||
target="_blank"
|
|
||||||
class="w-5 h-5 text-base-content/40 hover:text-info transition-colors"
|
|
||||||
title="Telegram"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<Icon name="simple-icons:telegram" class="w-3 h-3" />
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
v-if="metadata.website"
|
|
||||||
:href="metadata.website"
|
|
||||||
target="_blank"
|
|
||||||
class="w-5 h-5 text-base-content/40 hover:text-info transition-colors"
|
|
||||||
title="Website"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:globe-alt" class="w-3 h-3" />
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
v-if="metadata.discord"
|
|
||||||
:href="metadata.discord"
|
|
||||||
target="_blank"
|
|
||||||
class="w-5 h-5 text-base-content/40 hover:text-info transition-colors"
|
|
||||||
title="Discord"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<Icon name="simple-icons:discord" class="w-3 h-3" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Additional info for analysis type -->
|
|
||||||
<div v-if="(type === 'analysis' || type === 'dev') && 'node_count' in token" class="mt-2 text-xs text-base-content/60">
|
|
||||||
<span>{{ token.node_count }} nodes, {{ token.edge_count }} edges</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { isAddress } from '@solana/kit';
|
|
||||||
import { computed, onMounted, ref } from 'vue';
|
|
||||||
import { byteArrayToAddress, toSolanaAddress, truncateAddress as truncateAddr } from '~/utils/address';
|
|
||||||
import type {
|
|
||||||
MaxDepthReachedData,
|
|
||||||
NewTokenCreatedData,
|
|
||||||
TokenCexUpdatedData,
|
|
||||||
TokenMetadata
|
|
||||||
} from '../../types/redis-events';
|
|
||||||
import { formatTimeAgo as formatTimeAgoUtil, useRealTimeUpdate } from '../composables/useRealTimeUpdate';
|
|
||||||
import { fetchTokenMetadata } from '../utils/ipfs';
|
|
||||||
|
|
||||||
// Props
|
|
||||||
interface Props {
|
|
||||||
token: NewTokenCreatedData | TokenCexUpdatedData | MaxDepthReachedData;
|
|
||||||
type: 'new' | 'cex' | 'analysis' | 'dev';
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
|
||||||
|
|
||||||
// Emits
|
|
||||||
interface Emits {
|
|
||||||
(e: 'click' | 'hide' | 'watch' | 'quick-buy' | 'close', token: NewTokenCreatedData | TokenCexUpdatedData | MaxDepthReachedData): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
defineEmits<Emits>();
|
|
||||||
|
|
||||||
// Reactive data
|
|
||||||
const imageError = ref(false);
|
|
||||||
const mintAddress = ref<string>('');
|
|
||||||
const devAddress = ref<string>('');
|
|
||||||
const bondingCurveAddress = ref<string>('');
|
|
||||||
|
|
||||||
// Simple metadata state management
|
|
||||||
const metadata = ref<TokenMetadata | null>(null);
|
|
||||||
const _metadataLoading = ref(false);
|
|
||||||
const _metadataError = ref<string | null>(null);
|
|
||||||
|
|
||||||
// Real-time updates
|
|
||||||
const { currentTime } = useRealTimeUpdate();
|
|
||||||
|
|
||||||
// Computed properties
|
|
||||||
const cardClass = computed(() => {
|
|
||||||
const baseClass = 'h-[120px] min-h-[120px]';
|
|
||||||
switch (props.type) {
|
|
||||||
case 'new':
|
|
||||||
return `${baseClass} border-l-2 border-l-success`;
|
|
||||||
case 'cex':
|
|
||||||
return `${baseClass} border-l-2 border-l-info`;
|
|
||||||
case 'analysis':
|
|
||||||
case 'dev':
|
|
||||||
return `${baseClass} border-l-2 border-l-warning`;
|
|
||||||
default:
|
|
||||||
return baseClass;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const typeClass = computed(() => {
|
|
||||||
switch (props.type) {
|
|
||||||
case 'new':
|
|
||||||
return 'badge-success';
|
|
||||||
case 'cex':
|
|
||||||
return 'badge-info';
|
|
||||||
case 'analysis':
|
|
||||||
case 'dev':
|
|
||||||
return 'badge-warning';
|
|
||||||
default:
|
|
||||||
return 'badge-neutral';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const typeLabel = computed(() => {
|
|
||||||
switch (props.type) {
|
|
||||||
case 'new':
|
|
||||||
return 'NEW';
|
|
||||||
case 'cex':
|
|
||||||
return 'CEX';
|
|
||||||
case 'analysis':
|
|
||||||
return 'ANALYSIS';
|
|
||||||
case 'dev':
|
|
||||||
return 'DEV';
|
|
||||||
default:
|
|
||||||
return 'UNKNOWN';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasSocialLinks = computed(() => {
|
|
||||||
return metadata.value && (
|
|
||||||
metadata.value.twitter ||
|
|
||||||
metadata.value.telegram ||
|
|
||||||
metadata.value.website ||
|
|
||||||
metadata.value.discord
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
const truncateAddress = (address: string): string => {
|
|
||||||
return truncateAddr(address);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTokenSymbol = (token: NewTokenCreatedData | TokenCexUpdatedData | MaxDepthReachedData): string | undefined => {
|
|
||||||
if ('symbol' in token) {
|
|
||||||
return token.symbol;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMintAddress = (): string => {
|
|
||||||
return mintAddress.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDisplayTimestamp = (token: NewTokenCreatedData | TokenCexUpdatedData | MaxDepthReachedData): number => {
|
|
||||||
// For CEX and analysis types, prefer updated_at if available
|
|
||||||
if ((props.type === 'cex' || props.type === 'analysis') && 'updated_at' in token) {
|
|
||||||
return token.updated_at;
|
|
||||||
}
|
|
||||||
// For new tokens or fallback, use created_at
|
|
||||||
return token.created_at;
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyToClipboard = async (text: string): Promise<void> => {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(text);
|
|
||||||
// You could add a toast notification here
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to copy to clipboard:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImageError = (): void => {
|
|
||||||
imageError.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
function convertAddresses() {
|
|
||||||
try {
|
|
||||||
// Convert mint address
|
|
||||||
if (Array.isArray(props.token.mint)) {
|
|
||||||
mintAddress.value = byteArrayToAddress(props.token.mint);
|
|
||||||
} else if (typeof props.token.mint === 'string') {
|
|
||||||
if (isAddress(props.token.mint)) {
|
|
||||||
mintAddress.value = props.token.mint;
|
|
||||||
} else {
|
|
||||||
mintAddress.value = toSolanaAddress(props.token.mint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert creator address (only for NewTokenCreatedData)
|
|
||||||
if (props.type === 'new' && 'creator' in props.token) {
|
|
||||||
const token = props.token as NewTokenCreatedData;
|
|
||||||
if (Array.isArray(token.creator)) {
|
|
||||||
devAddress.value = byteArrayToAddress(token.creator);
|
|
||||||
} else if (typeof token.creator === 'string') {
|
|
||||||
if (isAddress(token.creator)) {
|
|
||||||
devAddress.value = token.creator;
|
|
||||||
} else {
|
|
||||||
devAddress.value = toSolanaAddress(token.creator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert bonding curve address (NewTokenCreatedData and MaxDepthReachedData)
|
|
||||||
if ('bonding_curve' in props.token && props.token.bonding_curve) {
|
|
||||||
const token = props.token as NewTokenCreatedData | MaxDepthReachedData;
|
|
||||||
if (Array.isArray(token.bonding_curve)) {
|
|
||||||
bondingCurveAddress.value = byteArrayToAddress(token.bonding_curve);
|
|
||||||
} else if (typeof token.bonding_curve === 'string') {
|
|
||||||
if (isAddress(token.bonding_curve)) {
|
|
||||||
bondingCurveAddress.value = token.bonding_curve;
|
|
||||||
} else {
|
|
||||||
bondingCurveAddress.value = toSolanaAddress(token.bonding_curve);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error converting addresses:', error);
|
|
||||||
// Fallback to string representation
|
|
||||||
mintAddress.value = String(props.token.mint);
|
|
||||||
if (props.type === 'new' && 'creator' in props.token) {
|
|
||||||
devAddress.value = String((props.token as NewTokenCreatedData).creator);
|
|
||||||
}
|
|
||||||
if ('bonding_curve' in props.token && props.token.bonding_curve) {
|
|
||||||
bondingCurveAddress.value = String((props.token as NewTokenCreatedData | MaxDepthReachedData).bonding_curve);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load metadata and convert addresses on mount
|
|
||||||
onMounted(async () => {
|
|
||||||
// Convert addresses first
|
|
||||||
convertAddresses();
|
|
||||||
|
|
||||||
// Then load metadata if URI exists
|
|
||||||
if (props.token.uri) {
|
|
||||||
_metadataLoading.value = true;
|
|
||||||
_metadataError.value = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await fetchTokenMetadata(props.token.uri);
|
|
||||||
metadata.value = result;
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch metadata';
|
|
||||||
_metadataError.value = errorMessage;
|
|
||||||
} finally {
|
|
||||||
_metadataLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.token-card {
|
|
||||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-card:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.group:hover .opacity-0 {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { useAppStore } from '../stores/app';
|
|
||||||
|
|
||||||
export const useNavigation = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
const appStore = useAppStore();
|
|
||||||
|
|
||||||
const navigateToDashboard = () => {
|
|
||||||
router.push('/dashboard');
|
|
||||||
};
|
|
||||||
|
|
||||||
const navigateToProfile = () => {
|
|
||||||
router.push('/profile');
|
|
||||||
};
|
|
||||||
|
|
||||||
const navigateToHuntingGround = () => {
|
|
||||||
router.push('/hunting-ground');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
await appStore.logout();
|
|
||||||
router.push('/login');
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
navigateToDashboard,
|
|
||||||
navigateToProfile,
|
|
||||||
navigateToHuntingGround,
|
|
||||||
handleLogout,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
import { onMounted, onUnmounted, ref } from 'vue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable for real-time timestamp updates
|
|
||||||
* Updates every second to show live "time ago" timestamps
|
|
||||||
*/
|
|
||||||
export function useRealTimeUpdate() {
|
|
||||||
const currentTime = ref(Date.now());
|
|
||||||
let intervalId: NodeJS.Timeout | null = null;
|
|
||||||
|
|
||||||
const updateTime = () => {
|
|
||||||
currentTime.value = Date.now();
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// Update every second for real-time display
|
|
||||||
intervalId = setInterval(updateTime, 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (intervalId) {
|
|
||||||
clearInterval(intervalId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
currentTime
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format timestamp to "time ago" string
|
|
||||||
* @param timestamp - Unix timestamp in seconds
|
|
||||||
* @param currentTime - Current time for real-time updates
|
|
||||||
*/
|
|
||||||
export function formatTimeAgo(timestamp: number, currentTime: number): string {
|
|
||||||
const now = Math.floor(currentTime / 1000);
|
|
||||||
const then = Math.floor(timestamp);
|
|
||||||
const diffSeconds = Math.max(0, now - then); // Prevent negative values
|
|
||||||
|
|
||||||
if (diffSeconds < 60) {
|
|
||||||
return `${diffSeconds}s ago`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const diffMinutes = Math.floor(diffSeconds / 60);
|
|
||||||
if (diffMinutes < 60) {
|
|
||||||
return `${diffMinutes}m ago`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const diffHours = Math.floor(diffMinutes / 60);
|
|
||||||
if (diffHours < 24) {
|
|
||||||
return `${diffHours}h ago`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const diffDays = Math.floor(diffHours / 24);
|
|
||||||
return `${diffDays}d ago`;
|
|
||||||
}
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
/**
|
|
||||||
* Ziya App Configuration Composable
|
|
||||||
* Provides centralized configuration values for the entire application
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* const { config, getDevServerUrl, isDevelopment } = useZiyaConfig()
|
|
||||||
* console.log(config.app.name) // 'Ziya'
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface ZiyaAppConfig {
|
|
||||||
name: string;
|
|
||||||
version: string;
|
|
||||||
description: string;
|
|
||||||
author: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ZiyaDevelopmentConfig {
|
|
||||||
nuxtPort: number;
|
|
||||||
nuxtHost: string;
|
|
||||||
electronDevTools: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ZiyaWindowConfig {
|
|
||||||
minHeight: number;
|
|
||||||
minWidth: number;
|
|
||||||
maxHeight: number;
|
|
||||||
maxWidth: number;
|
|
||||||
defaultHeight: number;
|
|
||||||
defaultWidth: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ZiyaThemeConfig {
|
|
||||||
defaultPalette: number;
|
|
||||||
defaultDarkMode: boolean;
|
|
||||||
availablePalettes: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ZiyaConfigValues {
|
|
||||||
app: ZiyaAppConfig;
|
|
||||||
development: ZiyaDevelopmentConfig;
|
|
||||||
window: ZiyaWindowConfig;
|
|
||||||
theme: ZiyaThemeConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ZIYA_CONFIG: ZiyaConfigValues = {
|
|
||||||
app: {
|
|
||||||
name: 'Ziya',
|
|
||||||
version: '1.0.0',
|
|
||||||
description: 'One stop shop trading solution',
|
|
||||||
author: 'bismillahDAO',
|
|
||||||
},
|
|
||||||
|
|
||||||
development: {
|
|
||||||
nuxtPort: 3000,
|
|
||||||
nuxtHost: 'localhost',
|
|
||||||
electronDevTools: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
window: {
|
|
||||||
minHeight: 800,
|
|
||||||
minWidth: 1080,
|
|
||||||
maxHeight: 1080,
|
|
||||||
maxWidth: 1920,
|
|
||||||
defaultHeight: 1024,
|
|
||||||
defaultWidth: 1280,
|
|
||||||
},
|
|
||||||
|
|
||||||
theme: {
|
|
||||||
defaultPalette: 1,
|
|
||||||
defaultDarkMode: false,
|
|
||||||
availablePalettes: Array.from({ length: 24 }, (_, i) => i + 1),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get development server URL based on configuration
|
|
||||||
*/
|
|
||||||
const getDevServerUrl = (): string => {
|
|
||||||
const { nuxtHost, nuxtPort } = ZIYA_CONFIG.development;
|
|
||||||
return `http://${nuxtHost}:${nuxtPort}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Environment detection utilities
|
|
||||||
*/
|
|
||||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
|
||||||
const isClient = import.meta.client;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main composable function that provides Ziya app configuration
|
|
||||||
*
|
|
||||||
* @returns Object containing configuration values and helper functions
|
|
||||||
*/
|
|
||||||
export const useZiyaConfig = () => {
|
|
||||||
return {
|
|
||||||
config: ZIYA_CONFIG,
|
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
getDevServerUrl,
|
|
||||||
|
|
||||||
// Environment flags
|
|
||||||
isDevelopment,
|
|
||||||
isProduction,
|
|
||||||
isClient,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Default export for convenience
|
|
||||||
export default useZiyaConfig;
|
|
||||||
|
|
||||||
// Export types for external use
|
|
||||||
export type {
|
|
||||||
ZiyaAppConfig, ZiyaConfigValues, ZiyaDevelopmentConfig, ZiyaThemeConfig, ZiyaWindowConfig
|
|
||||||
};
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="min-h-screen bg-base-100 flex flex-col">
|
|
||||||
<!-- Custom Title Bar for window dragging and controls -->
|
|
||||||
<TitleBar />
|
|
||||||
|
|
||||||
<!-- Main Content Area with no-drag to prevent dragging from form elements -->
|
|
||||||
<main class="flex-1 overflow-hidden" style="-webkit-app-region: no-drag;">
|
|
||||||
<slot />
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
// Auth layout - includes title bar but no additional navigation
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Ensure proper window behavior */
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#__nuxt {
|
|
||||||
height: 100vh;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="min-h-screen bg-base-100 flex flex-col">
|
|
||||||
<!-- Custom Title Bar for window dragging and controls -->
|
|
||||||
<TitleBar />
|
|
||||||
|
|
||||||
<!-- Main Content Area with no-drag to prevent dragging from content -->
|
|
||||||
<main class="flex-1 overflow-hidden" style="-webkit-app-region: no-drag;">
|
|
||||||
<slot />
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
// No additional setup needed for this layout
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Global styles */
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#__nuxt {
|
|
||||||
height: 100vh;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,166 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="desktop-container">
|
|
||||||
<!-- Top bar with user info -->
|
|
||||||
<div class="navbar bg-base-300 px-4">
|
|
||||||
<div class="navbar-start">
|
|
||||||
<div class="text-xl font-bold">Ziya Dashboard</div>
|
|
||||||
</div>
|
|
||||||
<div class="navbar-end">
|
|
||||||
<div class="dropdown dropdown-end">
|
|
||||||
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar">
|
|
||||||
<div class="w-10 rounded-full bg-primary flex items-center justify-center">
|
|
||||||
<span class="text-primary-content font-bold text-sm">
|
|
||||||
{{ appStore.userInitials }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
|
|
||||||
<li><a @click="navigateToProfile">Profile</a></li>
|
|
||||||
<li><a>Settings</a></li>
|
|
||||||
<li><a @click="handleLogout">Logout</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main content area -->
|
|
||||||
<div class="flex-1 flex overflow-hidden">
|
|
||||||
<!-- Sidebar -->
|
|
||||||
<div class="w-64 bg-base-200 p-4">
|
|
||||||
<ul class="menu">
|
|
||||||
<li><a class="active">Dashboard</a></li>
|
|
||||||
<li><a @click="navigateToProfile">Profile</a></li>
|
|
||||||
<li><a>Trading</a></li>
|
|
||||||
<li><a>Portfolio</a></li>
|
|
||||||
<li><a>Markets</a></li>
|
|
||||||
<li><a @click="navigateToHuntingGround">Hunting Ground</a></li>
|
|
||||||
<li><a>Analytics</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main content -->
|
|
||||||
<div class="flex-1 p-6 overflow-y-auto">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
|
|
||||||
<!-- Stats cards -->
|
|
||||||
<div class="stats shadow">
|
|
||||||
<div class="stat">
|
|
||||||
<div class="stat-title">Total Balance</div>
|
|
||||||
<div class="stat-value text-primary">$25,600</div>
|
|
||||||
<div class="stat-desc">↗︎ 12% (30d)</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stats shadow">
|
|
||||||
<div class="stat">
|
|
||||||
<div class="stat-title">Active Positions</div>
|
|
||||||
<div class="stat-value text-secondary">8</div>
|
|
||||||
<div class="stat-desc">↗︎ 2 new today</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stats shadow">
|
|
||||||
<div class="stat">
|
|
||||||
<div class="stat-title">P&L Today</div>
|
|
||||||
<div class="stat-value text-accent">+$450</div>
|
|
||||||
<div class="stat-desc">↗︎ +2.1%</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Trading interface -->
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<div class="card bg-base-100 shadow-xl">
|
|
||||||
<div class="card-body">
|
|
||||||
<h2 class="card-title">Quick Trade</h2>
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text">Asset</span>
|
|
||||||
</label>
|
|
||||||
<select class="select select-bordered">
|
|
||||||
<option>BTC/USD</option>
|
|
||||||
<option>ETH/USD</option>
|
|
||||||
<option>SOL/USD</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text">Amount</span>
|
|
||||||
</label>
|
|
||||||
<input type="number" class="input input-bordered" placeholder="0.00">
|
|
||||||
</div>
|
|
||||||
<div class="card-actions justify-end">
|
|
||||||
<button class="btn btn-success">Buy</button>
|
|
||||||
<button class="btn btn-error">Sell</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card bg-base-100 shadow-xl">
|
|
||||||
<div class="card-body">
|
|
||||||
<h2 class="card-title">Recent Trades</h2>
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="table table-sm">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Asset</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Amount</th>
|
|
||||||
<th>Price</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>BTC</td>
|
|
||||||
<td><span class="badge badge-success">Buy</span></td>
|
|
||||||
<td>0.025</td>
|
|
||||||
<td>$42,500</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>ETH</td>
|
|
||||||
<td><span class="badge badge-error">Sell</span></td>
|
|
||||||
<td>2.5</td>
|
|
||||||
<td>$2,650</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { onMounted } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { useAppStore } from '../stores/app';
|
|
||||||
|
|
||||||
const appStore = useAppStore();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// Redirect if not authenticated
|
|
||||||
onMounted(() => {
|
|
||||||
if (!appStore.isAuthenticated) {
|
|
||||||
router.push('/login');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
await appStore.logout();
|
|
||||||
router.push('/login');
|
|
||||||
};
|
|
||||||
|
|
||||||
const navigateToProfile = () => {
|
|
||||||
router.push('/profile');
|
|
||||||
};
|
|
||||||
|
|
||||||
const navigateToHuntingGround = () => {
|
|
||||||
router.push('/hunting-ground');
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* Page-specific styles if needed */
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,476 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="hunting-ground h-screen flex bg-base-100 overflow-hidden">
|
|
||||||
<!-- Professional Sidebar -->
|
|
||||||
<aside class="w-64 bg-base-200 border-r border-base-300 flex flex-col shadow-lg">
|
|
||||||
<!-- Sidebar Header -->
|
|
||||||
<div class="p-4 border-b border-base-300">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
|
|
||||||
<Icon name="heroicons:chart-bar-square" class="w-5 h-5 text-primary-content" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 class="font-semibold text-base text-base-content">Hunting Ground</h1>
|
|
||||||
<p class="text-xs text-base-content/60">Real-time discovery</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Navigation Menu -->
|
|
||||||
<nav class="flex-1 p-3">
|
|
||||||
<div class="space-y-1">
|
|
||||||
<button
|
|
||||||
class="w-full flex items-center gap-3 px-3 py-2.5 text-sm font-medium rounded-lg bg-primary text-primary-content"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:magnifying-glass" class="w-4 h-4" />
|
|
||||||
Token Discovery
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="w-full flex items-center gap-3 px-3 py-2.5 text-sm font-medium rounded-lg text-base-content/70 hover:bg-base-300 transition-colors"
|
|
||||||
@click="navigateToDashboard"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:squares-2x2" class="w-4 h-4" />
|
|
||||||
Dashboard
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="w-full flex items-center gap-3 px-3 py-2.5 text-sm font-medium rounded-lg text-base-content/70 hover:bg-base-300 transition-colors"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:chart-pie" class="w-4 h-4" />
|
|
||||||
Analytics
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="w-full flex items-center gap-3 px-3 py-2.5 text-sm font-medium rounded-lg text-base-content/70 hover:bg-base-300 transition-colors"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:bookmark" class="w-4 h-4" />
|
|
||||||
Watchlist
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Stats Section -->
|
|
||||||
<div class="mt-6 p-3 bg-base-300/50 rounded-lg">
|
|
||||||
<h3 class="text-xs font-semibold text-base-content/70 uppercase tracking-wider mb-2">Live Stats</h3>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="flex justify-between text-sm">
|
|
||||||
<span class="text-base-content/60">New Tokens</span>
|
|
||||||
<span class="font-medium text-success">{{ newTokens.length }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between text-sm">
|
|
||||||
<span class="text-base-content/60">CEX Updates</span>
|
|
||||||
<span class="font-medium text-info">{{ cexTokens.length }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between text-sm">
|
|
||||||
<span class="text-base-content/60">Analysis Done</span>
|
|
||||||
<span class="font-medium text-warning">{{ maxDepthTokens.length }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- User Section -->
|
|
||||||
<div class="p-3 border-t border-base-300">
|
|
||||||
<div class="flex items-center gap-3 p-2 rounded-lg hover:bg-base-300 transition-colors cursor-pointer">
|
|
||||||
<div class="w-8 h-8 bg-primary rounded-full flex items-center justify-center">
|
|
||||||
<span class="text-xs font-bold text-primary-content">U</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<p class="text-sm font-medium text-base-content truncate">User</p>
|
|
||||||
<p class="text-xs text-base-content/60">Connected</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="p-1 rounded hover:bg-base-200 transition-colors"
|
|
||||||
title="Logout"
|
|
||||||
@click="handleLogout"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:arrow-right-on-rectangle" class="w-4 h-4 text-base-content/60" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<main class="flex-1 flex flex-col overflow-hidden">
|
|
||||||
<!-- Top Bar -->
|
|
||||||
<header class="bg-base-100 border-b border-base-300 px-6 py-3 flex items-center justify-between flex-shrink-0">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<h2 class="text-lg font-semibold text-base-content">Token Streams</h2>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="w-2 h-2 bg-success rounded-full animate-pulse" />
|
|
||||||
<span class="text-sm text-base-content/60">Live</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<button class="btn btn-ghost btn-sm">
|
|
||||||
<Icon name="heroicons:funnel" class="w-4 h-4" />
|
|
||||||
Filters
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-ghost btn-sm">
|
|
||||||
<Icon name="heroicons:cog-6-tooth" class="w-4 h-4" />
|
|
||||||
Settings
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Content Grid -->
|
|
||||||
<div class="flex-1 grid grid-cols-3 gap-0 overflow-hidden">
|
|
||||||
<!-- New Tokens Column -->
|
|
||||||
<section class="flex flex-col border-r border-base-300">
|
|
||||||
<header class="bg-base-200/50 px-4 py-3 border-b border-base-300 flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="w-3 h-3 bg-success rounded-full" />
|
|
||||||
<h3 class="font-semibold text-sm text-base-content">New Tokens</h3>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
v-if="newTokens.length > 0"
|
|
||||||
class="text-xs text-base-content/60 hover:text-error transition-colors underline"
|
|
||||||
title="Clear all new tokens"
|
|
||||||
@click="clearAllNewTokens"
|
|
||||||
>
|
|
||||||
Clear All
|
|
||||||
</button>
|
|
||||||
<span class="badge badge-success badge-sm">{{ newTokens.length }}</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div class="flex-1 overflow-y-auto">
|
|
||||||
<div class="divide-y divide-base-300">
|
|
||||||
<TokenCard
|
|
||||||
v-for="token in newTokens"
|
|
||||||
:key="byteArrayToAddress(token.mint)"
|
|
||||||
:token="token"
|
|
||||||
type="new"
|
|
||||||
@click="openToken"
|
|
||||||
@hide="hideToken"
|
|
||||||
@watch="watchToken"
|
|
||||||
@quick-buy="quickBuyToken"
|
|
||||||
@close="removeNewToken"
|
|
||||||
/>
|
|
||||||
<div v-if="newTokens.length === 0" class="p-8 text-center text-base-content/60">
|
|
||||||
<Icon name="heroicons:plus-circle" class="w-12 h-12 mx-auto mb-3 opacity-50" />
|
|
||||||
<p class="font-medium">Waiting for new tokens</p>
|
|
||||||
<p class="text-sm mt-1">New tokens will appear here</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- CEX Tokens Column -->
|
|
||||||
<section class="flex flex-col border-r border-base-300">
|
|
||||||
<header class="bg-base-200/50 px-4 py-3 border-b border-base-300 flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="w-3 h-3 bg-info rounded-full" />
|
|
||||||
<h3 class="font-semibold text-sm text-base-content">CEX Updates</h3>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
v-if="cexTokens.length > 0"
|
|
||||||
class="text-xs text-base-content/60 hover:text-error transition-colors underline"
|
|
||||||
title="Clear all CEX tokens"
|
|
||||||
@click="clearAllCexTokens"
|
|
||||||
>
|
|
||||||
Clear All
|
|
||||||
</button>
|
|
||||||
<span class="badge badge-info badge-sm">{{ cexTokens.length }}</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div class="flex-1 overflow-y-auto">
|
|
||||||
<div class="divide-y divide-base-300">
|
|
||||||
<CexAnalysisCard
|
|
||||||
v-for="token in cexTokens"
|
|
||||||
:key="token.mint"
|
|
||||||
:token="token.data"
|
|
||||||
@click="openToken"
|
|
||||||
@hide="hideToken"
|
|
||||||
@watch="watchToken"
|
|
||||||
@quick-buy="quickBuyToken"
|
|
||||||
@close="removeCexToken"
|
|
||||||
/>
|
|
||||||
<div v-if="cexTokens.length === 0" class="p-8 text-center text-base-content/60">
|
|
||||||
<Icon name="heroicons:building-office" class="w-12 h-12 mx-auto mb-3 opacity-50" />
|
|
||||||
<p class="font-medium">No CEX updates yet</p>
|
|
||||||
<p class="text-sm mt-1">CEX listings will appear here</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Analysis Complete Column -->
|
|
||||||
<section class="flex flex-col">
|
|
||||||
<header class="bg-base-200/50 px-4 py-3 border-b border-base-300 flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="w-3 h-3 bg-warning rounded-full" />
|
|
||||||
<h3 class="font-semibold text-sm text-base-content">Analysis Complete</h3>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
v-if="maxDepthTokens.length > 0"
|
|
||||||
class="text-xs text-base-content/60 hover:text-error transition-colors underline"
|
|
||||||
title="Clear all analysis complete tokens"
|
|
||||||
@click="clearAllMaxDepthTokens"
|
|
||||||
>
|
|
||||||
Clear All
|
|
||||||
</button>
|
|
||||||
<span class="badge badge-warning badge-sm">{{ maxDepthTokens.length }}</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div class="flex-1 overflow-y-auto">
|
|
||||||
<div class="divide-y divide-base-300">
|
|
||||||
<CexAnalysisCard
|
|
||||||
v-for="token in maxDepthTokens"
|
|
||||||
:key="token.mint"
|
|
||||||
:token="token.data"
|
|
||||||
@click="openToken"
|
|
||||||
@hide="hideToken"
|
|
||||||
@watch="watchToken"
|
|
||||||
@quick-buy="quickBuyToken"
|
|
||||||
@close="removeMaxDepthToken"
|
|
||||||
/>
|
|
||||||
<div v-if="maxDepthTokens.length === 0" class="p-8 text-center text-base-content/60">
|
|
||||||
<Icon name="heroicons:cpu-chip" class="w-12 h-12 mx-auto mb-3 opacity-50" />
|
|
||||||
<p class="font-medium">No analysis complete yet</p>
|
|
||||||
<p class="text-sm mt-1">Completed analyses will appear here</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { isAddress } from '@solana/kit';
|
|
||||||
import { onMounted, onUnmounted, ref } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { byteArrayToAddress, toSolanaAddress } from '~/utils/address';
|
|
||||||
import type {
|
|
||||||
MaxDepthReachedData,
|
|
||||||
NewTokenCreatedData,
|
|
||||||
RedisMessage,
|
|
||||||
TokenCexUpdatedData
|
|
||||||
} from '../../types/redis-events';
|
|
||||||
import CexAnalysisCard from '../components/CexAnalysisCard.vue';
|
|
||||||
import TokenCard from '../components/TokenCard.vue';
|
|
||||||
import { useAppStore } from '../stores/app';
|
|
||||||
|
|
||||||
const appStore = useAppStore();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// Reactive data for three columns
|
|
||||||
const newTokens = ref<NewTokenCreatedData[]>([]);
|
|
||||||
const cexTokens = ref<Array<{ mint: string; data: TokenCexUpdatedData }>>([]);
|
|
||||||
const maxDepthTokens = ref<Array<{ mint: string; data: MaxDepthReachedData }>>([]);
|
|
||||||
|
|
||||||
// Redirect if not authenticated
|
|
||||||
onMounted(() => {
|
|
||||||
if (!appStore.isAuthenticated) {
|
|
||||||
router.push('/login');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up Redis pubsub listener
|
|
||||||
if (window.electronAPI) {
|
|
||||||
window.electronAPI.onRedisData(handleRedisMessage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
// Clean up Redis listener
|
|
||||||
if (window.electronAPI) {
|
|
||||||
window.electronAPI.removeRedisDataListener();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleRedisMessage = (message: RedisMessage) => {
|
|
||||||
switch (message.channel) {
|
|
||||||
case 'new_token_created':
|
|
||||||
addNewToken(message.data as NewTokenCreatedData);
|
|
||||||
break;
|
|
||||||
case 'token_cex_updated':
|
|
||||||
addCexToken(message.data as TokenCexUpdatedData);
|
|
||||||
break;
|
|
||||||
case 'max_depth_reached':
|
|
||||||
addMaxDepthToken(message.data as MaxDepthReachedData);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addNewToken = (data: NewTokenCreatedData) => {
|
|
||||||
// Add to front of array (latest first) - data is already properly typed
|
|
||||||
newTokens.value.unshift(data);
|
|
||||||
|
|
||||||
// Keep only latest 100 items for performance
|
|
||||||
if (newTokens.value.length > 100) {
|
|
||||||
newTokens.value = newTokens.value.slice(0, 100);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addCexToken = (data: TokenCexUpdatedData) => {
|
|
||||||
// Add to front of array (latest first)
|
|
||||||
cexTokens.value.unshift({
|
|
||||||
mint: data.mint, // mint is already a string
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Keep only latest 100 items for performance
|
|
||||||
if (cexTokens.value.length > 100) {
|
|
||||||
cexTokens.value = cexTokens.value.slice(0, 100);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addMaxDepthToken = (data: MaxDepthReachedData) => {
|
|
||||||
// Add to front of array (latest first)
|
|
||||||
maxDepthTokens.value.unshift({
|
|
||||||
mint: data.mint, // mint is already a string
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Keep only latest 100 items for performance
|
|
||||||
if (maxDepthTokens.value.length > 100) {
|
|
||||||
maxDepthTokens.value = maxDepthTokens.value.slice(0, 100);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Event handlers for TokenCard
|
|
||||||
const openToken = (token: NewTokenCreatedData | TokenCexUpdatedData | MaxDepthReachedData) => {
|
|
||||||
try {
|
|
||||||
let bondingCurveAddress = '';
|
|
||||||
|
|
||||||
// Always prioritize bonding curve address
|
|
||||||
if ('bonding_curve' in token && token.bonding_curve) {
|
|
||||||
if (Array.isArray(token.bonding_curve)) {
|
|
||||||
bondingCurveAddress = byteArrayToAddress(token.bonding_curve);
|
|
||||||
} else if (typeof token.bonding_curve === 'string') {
|
|
||||||
if (isAddress(token.bonding_curve)) {
|
|
||||||
bondingCurveAddress = token.bonding_curve;
|
|
||||||
} else {
|
|
||||||
bondingCurveAddress = toSolanaAddress(token.bonding_curve);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bondingCurveAddress) {
|
|
||||||
const axiomUrl = `https://axiom.trade/meme/${bondingCurveAddress}`;
|
|
||||||
|
|
||||||
// Use Electron API to open external URL
|
|
||||||
if (window.electronAPI) {
|
|
||||||
window.electronAPI.openExternal(axiomUrl);
|
|
||||||
} else {
|
|
||||||
// Fallback for development or non-Electron environment
|
|
||||||
window.open(axiomUrl, '_blank');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('No bonding curve address found for token:', token);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error opening token in Axiom:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const hideToken = (_token: NewTokenCreatedData | TokenCexUpdatedData | MaxDepthReachedData) => {
|
|
||||||
// TODO: Implement hide functionality
|
|
||||||
};
|
|
||||||
|
|
||||||
const watchToken = (_token: NewTokenCreatedData | TokenCexUpdatedData | MaxDepthReachedData) => {
|
|
||||||
// TODO: Implement watch functionality
|
|
||||||
};
|
|
||||||
|
|
||||||
const quickBuyToken = (_token: NewTokenCreatedData | TokenCexUpdatedData | MaxDepthReachedData) => {
|
|
||||||
// TODO: Implement quick buy functionality
|
|
||||||
};
|
|
||||||
|
|
||||||
// Navigation handlers
|
|
||||||
const handleLogout = async () => {
|
|
||||||
await appStore.logout();
|
|
||||||
router.push('/login');
|
|
||||||
};
|
|
||||||
|
|
||||||
const navigateToDashboard = () => {
|
|
||||||
router.push('/dashboard');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Clear all functions (performance optimized)
|
|
||||||
const clearAllNewTokens = () => {
|
|
||||||
newTokens.value.length = 0; // Fastest way to clear array
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearAllCexTokens = () => {
|
|
||||||
cexTokens.value.length = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearAllMaxDepthTokens = () => {
|
|
||||||
maxDepthTokens.value.length = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Individual remove functions (using findIndex for performance)
|
|
||||||
const removeNewToken = (token: NewTokenCreatedData | TokenCexUpdatedData | MaxDepthReachedData) => {
|
|
||||||
// Type guard to ensure it's a NewTokenCreatedData
|
|
||||||
if ('symbol' in token && Array.isArray(token.mint)) {
|
|
||||||
const mintAddr = byteArrayToAddress(token.mint);
|
|
||||||
const index = newTokens.value.findIndex(t => byteArrayToAddress(t.mint) === mintAddr);
|
|
||||||
if (index > -1) {
|
|
||||||
newTokens.value.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeCexToken = (token: TokenCexUpdatedData | MaxDepthReachedData) => {
|
|
||||||
// Type guard to ensure it's a TokenCexUpdatedData (doesn't have bonding_curve)
|
|
||||||
if (!('bonding_curve' in token)) {
|
|
||||||
const index = cexTokens.value.findIndex(t => t.mint === token.mint);
|
|
||||||
if (index > -1) {
|
|
||||||
cexTokens.value.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeMaxDepthToken = (token: TokenCexUpdatedData | MaxDepthReachedData) => {
|
|
||||||
// Type guard to ensure it's a MaxDepthReachedData (has bonding_curve)
|
|
||||||
if ('bonding_curve' in token) {
|
|
||||||
const index = maxDepthTokens.value.findIndex(t => t.mint === token.mint);
|
|
||||||
if (index > -1) {
|
|
||||||
maxDepthTokens.value.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* Professional scrollbar styling */
|
|
||||||
.overflow-y-auto {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overflow-y-auto::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overflow-y-auto::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overflow-y-auto::-webkit-scrollbar-thumb {
|
|
||||||
background-color: rgba(0, 0, 0, 0.15);
|
|
||||||
border-radius: 3px;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overflow-y-auto::-webkit-scrollbar-thumb:hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure proper grid layout */
|
|
||||||
.grid-cols-3 > section {
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Smooth transitions */
|
|
||||||
* {
|
|
||||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 150ms;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
@ -1,159 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="min-h-screen bg-base-100 flex items-center justify-center">
|
|
||||||
<div
|
|
||||||
v-if="isLoading"
|
|
||||||
key="loading-state"
|
|
||||||
class="text-center"
|
|
||||||
>
|
|
||||||
<div class="loading loading-spinner loading-lg text-primary mb-4" />
|
|
||||||
<h2 class="text-xl font-semibold text-base-content mb-2">Loading Ziya</h2>
|
|
||||||
<p class="text-base-content/70">Initializing your trading environment...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-else-if="error"
|
|
||||||
key="error-state"
|
|
||||||
class="text-center max-w-md"
|
|
||||||
>
|
|
||||||
<div class="alert alert-error mb-4">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
<span>{{ error }}</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="btn btn-primary"
|
|
||||||
@click="retryInitialization"
|
|
||||||
>
|
|
||||||
Try Again
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
key="main-content"
|
|
||||||
class="text-center max-w-6xl px-4"
|
|
||||||
>
|
|
||||||
<!-- Hero Section -->
|
|
||||||
<div class="mb-12">
|
|
||||||
<div class="w-24 h-24 mx-auto mb-6 bg-primary/10 rounded-full flex items-center justify-center">
|
|
||||||
<svg class="w-12 h-12 text-primary" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main Title with Pronunciation -->
|
|
||||||
<div class="mb-4">
|
|
||||||
<h1 class="text-5xl md:text-6xl font-bold text-base-content mb-2">
|
|
||||||
Ziya
|
|
||||||
</h1>
|
|
||||||
<p class="text-lg text-base-content/60 mb-6">
|
|
||||||
<span class="font-medium">/dˤiˈjaːʔ/</span>, "zee‑yah" — <em>Proper noun, meaning "light"</em>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tagline -->
|
|
||||||
<p class="text-2xl md:text-3xl text-base-content/80 mb-4 font-light">
|
|
||||||
One stop shop trading solution
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Brand Attribution -->
|
|
||||||
<div class="mb-8">
|
|
||||||
<p class="text-base text-base-content/70 font-medium">
|
|
||||||
A <span class="text-primary font-semibold">bismillahDAO</span> creation
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Prominent CTA Section -->
|
|
||||||
<div class="bg-gradient-to-r from-primary/5 to-secondary/5 rounded-2xl p-8 mb-8">
|
|
||||||
<!-- Primary CTA Button - Highly Visible -->
|
|
||||||
<button
|
|
||||||
class="btn btn-primary btn-lg px-12 py-4 text-lg font-semibold shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200"
|
|
||||||
@click="navigateToLogin"
|
|
||||||
>
|
|
||||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
|
||||||
</svg>
|
|
||||||
Get Started
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Tutorial Text -->
|
|
||||||
<div class="mt-4">
|
|
||||||
<p class="text-base-content/70 text-sm">
|
|
||||||
Read the tutorial
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Secondary Action -->
|
|
||||||
<div class="mt-4">
|
|
||||||
<button class="btn btn-ghost btn-sm text-base-content/70 hover:text-base-content">
|
|
||||||
Learn more about our features
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- App Version -->
|
|
||||||
<div class="text-center">
|
|
||||||
<p class="text-xs text-base-content/50">
|
|
||||||
Version {{ appVersion }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { onMounted, ref } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
// Use auth layout to prevent navbar from showing
|
|
||||||
definePageMeta({
|
|
||||||
layout: 'auth',
|
|
||||||
});
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// Local reactive state instead of accessing store immediately
|
|
||||||
const isLoading = ref(true);
|
|
||||||
const error = ref(null);
|
|
||||||
const appVersion = ref('1.0.0');
|
|
||||||
|
|
||||||
const navigateToLogin = () => {
|
|
||||||
router.push('/login');
|
|
||||||
};
|
|
||||||
|
|
||||||
const retryInitialization = async () => {
|
|
||||||
isLoading.value = true;
|
|
||||||
error.value = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Only import and use store after mount
|
|
||||||
const { useAppStore } = await import('../stores/app');
|
|
||||||
const appStore = useAppStore();
|
|
||||||
await appStore.initialize();
|
|
||||||
appVersion.value = appStore.appVersion;
|
|
||||||
isLoading.value = false;
|
|
||||||
} catch (err) {
|
|
||||||
error.value = err instanceof Error ? err.message : 'Failed to initialize app';
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
// Only access store after component is mounted (client-side only)
|
|
||||||
if (import.meta.client) {
|
|
||||||
try {
|
|
||||||
const { useAppStore } = await import('../stores/app');
|
|
||||||
const appStore = useAppStore();
|
|
||||||
await appStore.initialize();
|
|
||||||
appVersion.value = appStore.appVersion;
|
|
||||||
isLoading.value = false;
|
|
||||||
} catch (err) {
|
|
||||||
error.value = err instanceof Error ? err.message : 'Failed to initialize app';
|
|
||||||
isLoading.value = false;
|
|
||||||
console.error('App initialization failed:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,179 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="login-content">
|
|
||||||
<div class="w-96 space-y-4">
|
|
||||||
<!-- Login Card -->
|
|
||||||
<div class="card bg-base-100 shadow-xl">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="text-center mb-6">
|
|
||||||
<h1 class="text-3xl font-bold">Welcome to Ziya</h1>
|
|
||||||
<p class="text-base-content/70">Sign in to your trading platform</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form
|
|
||||||
class="space-y-4"
|
|
||||||
@submit.prevent="handleLogin"
|
|
||||||
>
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text">Email Address</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model="email"
|
|
||||||
type="email"
|
|
||||||
required
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
placeholder="Enter your email"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text">Password</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model="password"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
placeholder="Enter your password"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label cursor-pointer justify-start">
|
|
||||||
<input type="checkbox" class="checkbox checkbox-sm mr-2">
|
|
||||||
<span class="label-text">Remember me</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-control mt-6">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn btn-primary w-full"
|
|
||||||
:class="{ loading: isLoading }"
|
|
||||||
:disabled="isLoading"
|
|
||||||
>
|
|
||||||
{{ isLoading ? 'Signing in...' : 'Sign In' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="divider">OR</div>
|
|
||||||
|
|
||||||
<div class="text-center">
|
|
||||||
<p class="text-sm text-base-content/70">
|
|
||||||
Don't have an account?
|
|
||||||
<a href="#" class="link link-primary">Sign up</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Back Button -->
|
|
||||||
<div class="text-center">
|
|
||||||
<button
|
|
||||||
class="btn btn-ghost btn-sm text-base-content/70 hover:text-base-content"
|
|
||||||
title="Go back to home"
|
|
||||||
@click="goBack"
|
|
||||||
>
|
|
||||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
|
||||||
</svg>
|
|
||||||
Back to Home
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- App Version -->
|
|
||||||
<div class="text-center">
|
|
||||||
<p class="text-xs opacity-50">
|
|
||||||
Version {{ appVersion }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { onMounted, ref } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
definePageMeta({
|
|
||||||
layout: 'auth',
|
|
||||||
});
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// Local reactive state instead of accessing store immediately
|
|
||||||
const email = ref('');
|
|
||||||
const password = ref('');
|
|
||||||
const isLoading = ref(false);
|
|
||||||
const appVersion = ref('1.0.0');
|
|
||||||
const isAuthenticated = ref(false);
|
|
||||||
|
|
||||||
// Redirect if already authenticated - but only after mount
|
|
||||||
onMounted(async () => {
|
|
||||||
if (import.meta.client) {
|
|
||||||
try {
|
|
||||||
const { useAppStore } = await import('../stores/app');
|
|
||||||
const appStore = useAppStore();
|
|
||||||
|
|
||||||
// Initialize app if not already done
|
|
||||||
if (!appStore.isInitialized) {
|
|
||||||
await appStore.initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
appVersion.value = appStore.appVersion;
|
|
||||||
isAuthenticated.value = appStore.isAuthenticated;
|
|
||||||
|
|
||||||
// Redirect if already authenticated
|
|
||||||
if (appStore.isAuthenticated) {
|
|
||||||
router.push('/dashboard');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to initialize app store:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleLogin = async () => {
|
|
||||||
if (!email.value || !password.value) {
|
|
||||||
// Simple client-side validation without store
|
|
||||||
console.warn('Please fill in all fields');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Dynamic import to avoid SSR issues
|
|
||||||
const { useAppStore } = await import('../stores/app');
|
|
||||||
const appStore = useAppStore();
|
|
||||||
|
|
||||||
const success = await appStore.login(email.value, password.value);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
router.push('/dashboard');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Login failed:', error);
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const goBack = () => {
|
|
||||||
router.push('/');
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.login-content {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: linear-gradient(135deg, hsl(var(--b3)) 0%, hsl(var(--b2)) 100%);
|
|
||||||
/* Ensure this area cannot be used for dragging */
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,257 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="desktop-container">
|
|
||||||
<!-- Top bar with user info -->
|
|
||||||
<div class="navbar bg-base-300 px-4">
|
|
||||||
<div class="navbar-start">
|
|
||||||
<div class="text-xl font-bold">Profile</div>
|
|
||||||
</div>
|
|
||||||
<div class="navbar-end">
|
|
||||||
<div class="dropdown dropdown-end">
|
|
||||||
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar">
|
|
||||||
<div class="w-10 rounded-full bg-primary flex items-center justify-center">
|
|
||||||
<span class="text-primary-content font-bold text-sm">
|
|
||||||
{{ appStore.userInitials }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
|
|
||||||
<li><a @click="navigateToProfile">Profile</a></li>
|
|
||||||
<li><a>Settings</a></li>
|
|
||||||
<li><a @click="handleLogout">Logout</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main content area -->
|
|
||||||
<div class="flex-1 flex overflow-hidden">
|
|
||||||
<!-- Sidebar -->
|
|
||||||
<div class="w-64 bg-base-200 p-4">
|
|
||||||
<ul class="menu">
|
|
||||||
<li><a @click="navigateToDashboard">Dashboard</a></li>
|
|
||||||
<li><a class="active">Profile</a></li>
|
|
||||||
<li><a>Trading</a></li>
|
|
||||||
<li><a>Portfolio</a></li>
|
|
||||||
<li><a>Markets</a></li>
|
|
||||||
<li><a @click="navigateToHuntingGround">Hunting Ground</a></li>
|
|
||||||
<li><a>Analytics</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main content -->
|
|
||||||
<div class="flex-1 p-6 overflow-y-auto">
|
|
||||||
<!-- User Info Section -->
|
|
||||||
<div class="card bg-base-100 shadow-xl mb-6">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="flex items-center gap-6">
|
|
||||||
<div class="w-20 h-20 rounded-full bg-primary flex items-center justify-center">
|
|
||||||
<span class="text-primary-content font-bold text-2xl">
|
|
||||||
{{ appStore.userInitials }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 class="card-title text-2xl">{{ appStore.currentUser?.name || 'John Trader' }}</h2>
|
|
||||||
<p class="text-base-content/70">{{ appStore.currentUser?.email || 'john@example.com' }}</p>
|
|
||||||
<div class="badge badge-success mt-2">Pro Trader</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Account Overview -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
|
||||||
<div class="card bg-base-100 shadow-lg">
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title text-lg">Total Portfolio Value</h3>
|
|
||||||
<div class="stat-value text-primary text-3xl">$125,340</div>
|
|
||||||
<div class="text-success text-sm">↗ +8.2% today</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card bg-base-100 shadow-lg">
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title text-lg">Available Balance</h3>
|
|
||||||
<div class="stat-value text-secondary text-3xl">$25,680</div>
|
|
||||||
<div class="text-base-content/70 text-sm">Ready to trade</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card bg-base-100 shadow-lg">
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title text-lg">Total Profit/Loss</h3>
|
|
||||||
<div class="stat-value text-accent text-3xl">+$12,450</div>
|
|
||||||
<div class="text-success text-sm">↗ +15.6% all time</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Current Positions -->
|
|
||||||
<div class="card bg-base-100 shadow-xl mb-6">
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title">Current Positions</h3>
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Token</th>
|
|
||||||
<th>Amount</th>
|
|
||||||
<th>Entry Price</th>
|
|
||||||
<th>Current Price</th>
|
|
||||||
<th>P&L</th>
|
|
||||||
<th>Change</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="w-8 h-8 rounded-full bg-orange-500 flex items-center justify-center text-white font-bold text-xs">
|
|
||||||
BTC
|
|
||||||
</div>
|
|
||||||
<span class="font-semibold">Bitcoin</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>0.25 BTC</td>
|
|
||||||
<td>$42,000</td>
|
|
||||||
<td>$45,200</td>
|
|
||||||
<td class="text-success font-semibold">+$800</td>
|
|
||||||
<td><span class="badge badge-success">+7.6%</span></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white font-bold text-xs">
|
|
||||||
ETH
|
|
||||||
</div>
|
|
||||||
<span class="font-semibold">Ethereum</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>5.5 ETH</td>
|
|
||||||
<td>$2,800</td>
|
|
||||||
<td>$2,650</td>
|
|
||||||
<td class="text-error font-semibold">-$825</td>
|
|
||||||
<td><span class="badge badge-error">-5.4%</span></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="w-8 h-8 rounded-full bg-purple-500 flex items-center justify-center text-white font-bold text-xs">
|
|
||||||
SOL
|
|
||||||
</div>
|
|
||||||
<span class="font-semibold">Solana</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>120 SOL</td>
|
|
||||||
<td>$98</td>
|
|
||||||
<td>$105</td>
|
|
||||||
<td class="text-success font-semibold">+$840</td>
|
|
||||||
<td><span class="badge badge-success">+7.1%</span></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Recent Transactions -->
|
|
||||||
<div class="card bg-base-100 shadow-xl">
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title">Recent Transactions</h3>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="flex items-center justify-between p-4 border border-base-200 rounded-lg">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="w-10 h-10 rounded-full bg-success flex items-center justify-center">
|
|
||||||
<svg class="w-5 h-5 text-success-content" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="font-semibold">Bought BTC</div>
|
|
||||||
<div class="text-sm text-base-content/70">Today, 2:30 PM</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-right">
|
|
||||||
<div class="font-semibold">0.1 BTC</div>
|
|
||||||
<div class="text-sm text-base-content/70">$4,520</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between p-4 border border-base-200 rounded-lg">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="w-10 h-10 rounded-full bg-error flex items-center justify-center">
|
|
||||||
<svg class="w-5 h-5 text-error-content" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="font-semibold">Sold ETH</div>
|
|
||||||
<div class="text-sm text-base-content/70">Yesterday, 11:45 AM</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-right">
|
|
||||||
<div class="font-semibold">2.5 ETH</div>
|
|
||||||
<div class="text-sm text-base-content/70">$6,625</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between p-4 border border-base-200 rounded-lg">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="w-10 h-10 rounded-full bg-success flex items-center justify-center">
|
|
||||||
<svg class="w-5 h-5 text-success-content" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="font-semibold">Bought SOL</div>
|
|
||||||
<div class="text-sm text-base-content/70">2 days ago, 4:15 PM</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-right">
|
|
||||||
<div class="font-semibold">50 SOL</div>
|
|
||||||
<div class="text-sm text-base-content/70">$4,900</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { onMounted } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { useAppStore } from '../stores/app';
|
|
||||||
|
|
||||||
const appStore = useAppStore();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// Redirect if not authenticated
|
|
||||||
onMounted(() => {
|
|
||||||
if (!appStore.isAuthenticated) {
|
|
||||||
router.push('/login');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
await appStore.logout();
|
|
||||||
router.push('/login');
|
|
||||||
};
|
|
||||||
|
|
||||||
const navigateToDashboard = () => {
|
|
||||||
router.push('/dashboard');
|
|
||||||
};
|
|
||||||
|
|
||||||
const navigateToProfile = () => {
|
|
||||||
router.push('/profile');
|
|
||||||
};
|
|
||||||
|
|
||||||
const navigateToHuntingGround = () => {
|
|
||||||
router.push('/hunting-ground');
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* Page-specific styles if needed */
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,162 +0,0 @@
|
||||||
import { defineStore } from 'pinia';
|
|
||||||
import { useZiyaConfig } from '../composables/useZiyaConfig';
|
|
||||||
import { useThemeStore } from './theme';
|
|
||||||
|
|
||||||
interface AppState {
|
|
||||||
isInitialized: boolean;
|
|
||||||
isLoading: boolean;
|
|
||||||
error: string | null;
|
|
||||||
currentUser: { name: string; email: string } | null;
|
|
||||||
appVersion: string;
|
|
||||||
toastMessage: string;
|
|
||||||
toastType: 'success' | 'error' | 'info';
|
|
||||||
showToast: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useAppStore = defineStore('app', {
|
|
||||||
state: (): AppState => {
|
|
||||||
// Get config from composable if available (client-side)
|
|
||||||
const { config } = import.meta.client ? useZiyaConfig() : { config: { app: { version: '1.0.0' } } };
|
|
||||||
|
|
||||||
return {
|
|
||||||
isInitialized: false,
|
|
||||||
isLoading: false,
|
|
||||||
error: null,
|
|
||||||
currentUser: null,
|
|
||||||
appVersion: config.app.version,
|
|
||||||
toastMessage: '',
|
|
||||||
toastType: 'info',
|
|
||||||
showToast: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getters: {
|
|
||||||
isAuthenticated: state => state.currentUser !== null,
|
|
||||||
userInitials: (state) => {
|
|
||||||
if (!state.currentUser) return '??';
|
|
||||||
return state.currentUser.name
|
|
||||||
.split(' ')
|
|
||||||
.map(n => n[0])
|
|
||||||
.join('')
|
|
||||||
.toUpperCase();
|
|
||||||
},
|
|
||||||
|
|
||||||
appInfo: (state) => {
|
|
||||||
// Get config for additional app info
|
|
||||||
const { config } = import.meta.client ? useZiyaConfig() : { config: { app: { name: 'Ziya', version: '1.0.0', description: 'Trading Platform', author: 'bismillahDAO' } } };
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: config.app.name,
|
|
||||||
version: state.appVersion,
|
|
||||||
description: config.app.description,
|
|
||||||
author: config.app.author,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
async initialize() {
|
|
||||||
if (this.isInitialized) return;
|
|
||||||
|
|
||||||
this.isLoading = true;
|
|
||||||
this.error = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Initialize theme system
|
|
||||||
const themeStore = useThemeStore();
|
|
||||||
await themeStore.initializeTheme();
|
|
||||||
|
|
||||||
// Mark as initialized
|
|
||||||
this.isInitialized = true;
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
this.error = error instanceof Error ? error.message : 'Failed to initialize app';
|
|
||||||
console.error('App initialization failed:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setLoading(loading: boolean) {
|
|
||||||
this.isLoading = loading;
|
|
||||||
},
|
|
||||||
|
|
||||||
showToastMessage(message: string, type: 'success' | 'error' | 'info' = 'info') {
|
|
||||||
this.toastMessage = message;
|
|
||||||
this.toastType = type;
|
|
||||||
this.showToast = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
this.showToast = false;
|
|
||||||
}, 3000);
|
|
||||||
},
|
|
||||||
|
|
||||||
async login(email: string, _password: string) {
|
|
||||||
this.setLoading(true);
|
|
||||||
try {
|
|
||||||
// Simulate API call
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
// Mock user data
|
|
||||||
this.currentUser = {
|
|
||||||
name: 'John Trader',
|
|
||||||
email: email,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.showToastMessage('Welcome back!', 'success');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
this.showToastMessage('Login failed. Please try again.', 'error');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
this.setLoading(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async logout() {
|
|
||||||
this.setLoading(true);
|
|
||||||
try {
|
|
||||||
// Simulate API call
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
this.currentUser = null;
|
|
||||||
|
|
||||||
this.showToastMessage('You have been logged out', 'info');
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
this.setLoading(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Persist user data to localStorage
|
|
||||||
async $afterStateRestored() {
|
|
||||||
if (this.currentUser) {
|
|
||||||
localStorage.setItem('ziya-user', JSON.stringify(this.currentUser));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
localStorage.removeItem('ziya-user');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Initialize from localStorage
|
|
||||||
async initializeFromStorage() {
|
|
||||||
if (import.meta.client) {
|
|
||||||
const storedUser = localStorage.getItem('ziya-user');
|
|
||||||
if (storedUser) {
|
|
||||||
try {
|
|
||||||
this.currentUser = JSON.parse(storedUser);
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('Failed to parse stored user data:', error);
|
|
||||||
localStorage.removeItem('ziya-user');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.initialize();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
import { defineStore } from 'pinia';
|
|
||||||
import { useZiyaConfig } from '../composables/useZiyaConfig';
|
|
||||||
|
|
||||||
export const useThemeStore = defineStore('theme', {
|
|
||||||
state: () => {
|
|
||||||
// Get config from composable if available (client-side)
|
|
||||||
const { config } = import.meta.client ? useZiyaConfig() : { config: { theme: { defaultDarkMode: false, defaultPalette: 1, availablePalettes: Array.from({ length: 24 }, (_, i) => i + 1) } } };
|
|
||||||
|
|
||||||
return {
|
|
||||||
isDark: config.theme.defaultDarkMode,
|
|
||||||
currentPalette: config.theme.defaultPalette,
|
|
||||||
availablePalettes: config.theme.availablePalettes,
|
|
||||||
|
|
||||||
// Theme names for display
|
|
||||||
paletteNames: {
|
|
||||||
1: 'Cyan Ocean',
|
|
||||||
2: 'Royal Blue',
|
|
||||||
3: 'Purple Dream',
|
|
||||||
4: 'Teal Fresh',
|
|
||||||
5: 'Slate Modern',
|
|
||||||
6: 'Ruby Fire',
|
|
||||||
7: 'Cyan Steel',
|
|
||||||
8: 'Navy Deep',
|
|
||||||
9: 'Sky Bright',
|
|
||||||
10: 'Indigo Classic',
|
|
||||||
11: 'Pink Vivid',
|
|
||||||
12: 'Forest Green',
|
|
||||||
13: 'Golden Sun',
|
|
||||||
14: 'Orange Burst',
|
|
||||||
15: 'Blue Electric',
|
|
||||||
16: 'Purple Royal',
|
|
||||||
17: 'Magenta Bold',
|
|
||||||
18: 'Purple Deep',
|
|
||||||
19: 'Indigo Night',
|
|
||||||
20: 'Ocean Blue',
|
|
||||||
21: 'Orange Fire',
|
|
||||||
22: 'Indigo Bright',
|
|
||||||
23: 'Teal Vivid',
|
|
||||||
24: 'Sunshine',
|
|
||||||
} as Record<number, string>,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getters: {
|
|
||||||
currentTheme(): string {
|
|
||||||
// Use daisyUI theme naming convention with hyphens
|
|
||||||
const suffix = this.isDark ? 'dark' : 'light';
|
|
||||||
const paletteId = this.currentPalette.toString().padStart(2, '0');
|
|
||||||
return `palette-${paletteId}-${suffix}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
currentPaletteName(): string {
|
|
||||||
return this.paletteNames[this.currentPalette] || `Palette ${this.currentPalette}`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
toggleDarkMode() {
|
|
||||||
this.isDark = !this.isDark;
|
|
||||||
this.applyTheme();
|
|
||||||
this.saveToStorage();
|
|
||||||
},
|
|
||||||
|
|
||||||
async setPalette(paletteNumber: number) {
|
|
||||||
if (this.availablePalettes.includes(paletteNumber)) {
|
|
||||||
this.currentPalette = paletteNumber;
|
|
||||||
this.applyTheme();
|
|
||||||
this.saveToStorage();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
applyTheme() {
|
|
||||||
if (import.meta.client) {
|
|
||||||
try {
|
|
||||||
const html = document.documentElement;
|
|
||||||
const theme = this.currentTheme;
|
|
||||||
|
|
||||||
// Set the data-theme attribute for daisyUI
|
|
||||||
html.setAttribute('data-theme', theme);
|
|
||||||
|
|
||||||
// Also set it on body for additional styling if needed
|
|
||||||
document.body.setAttribute('data-theme', theme);
|
|
||||||
|
|
||||||
// Add a class for easier CSS targeting
|
|
||||||
html.className = html.className.replace(/theme-[\w-]+/g, '');
|
|
||||||
html.classList.add(`theme-${theme}`);
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('Error applying theme:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
initializeTheme() {
|
|
||||||
if (import.meta.client) {
|
|
||||||
try {
|
|
||||||
// Load from localStorage
|
|
||||||
const savedDark = localStorage.getItem('theme-dark');
|
|
||||||
const savedPalette = localStorage.getItem('theme-palette');
|
|
||||||
|
|
||||||
if (savedDark !== null) {
|
|
||||||
this.isDark = savedDark === 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (savedPalette) {
|
|
||||||
const paletteNumber = parseInt(savedPalette);
|
|
||||||
if (this.availablePalettes.includes(paletteNumber)) {
|
|
||||||
this.currentPalette = paletteNumber;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply the theme
|
|
||||||
this.applyTheme();
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('Error initializing theme:', error);
|
|
||||||
// Fallback to defaults from config
|
|
||||||
const { config } = useZiyaConfig();
|
|
||||||
this.isDark = config.theme.defaultDarkMode;
|
|
||||||
this.currentPalette = config.theme.defaultPalette;
|
|
||||||
this.applyTheme();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
saveToStorage() {
|
|
||||||
if (import.meta.client) {
|
|
||||||
try {
|
|
||||||
localStorage.setItem('theme-dark', this.isDark.toString());
|
|
||||||
localStorage.setItem('theme-palette', this.currentPalette.toString());
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('Error saving theme to storage:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
resetToDefault() {
|
|
||||||
// Get defaults from config
|
|
||||||
const { config } = useZiyaConfig();
|
|
||||||
|
|
||||||
this.isDark = config.theme.defaultDarkMode;
|
|
||||||
this.currentPalette = config.theme.defaultPalette;
|
|
||||||
this.applyTheme();
|
|
||||||
this.saveToStorage();
|
|
||||||
},
|
|
||||||
|
|
||||||
setRandomPalette() {
|
|
||||||
const randomIndex = Math.floor(Math.random() * this.availablePalettes.length);
|
|
||||||
const randomPalette = this.availablePalettes[randomIndex];
|
|
||||||
if (randomPalette) {
|
|
||||||
this.setPalette(randomPalette);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
import { type Address, address, getAddressDecoder, isAddress } from '@solana/kit';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a 32-byte Uint8Array to a Solana address using proper Solana utilities
|
|
||||||
*/
|
|
||||||
export function bytesToAddress(bytes: Uint8Array): Address {
|
|
||||||
if (bytes.length !== 32) {
|
|
||||||
throw new Error(`Expected 32 bytes, got ${bytes.length}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const decoder = getAddressDecoder();
|
|
||||||
return decoder.decode(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a comma-separated byte string to a Solana address
|
|
||||||
* Example: "207,240,50,185,127,150,26,145..." -> "B9Lf9z5BfNPT4d5KMeaBFx8x1G4CULZYR1jA2kmxRDka"
|
|
||||||
*/
|
|
||||||
export function byteStringToAddress(byteString: string): Address {
|
|
||||||
const bytes = byteString.split(',').map(b => parseInt(b.trim(), 10));
|
|
||||||
|
|
||||||
if (bytes.length !== 32) {
|
|
||||||
throw new Error(`Expected 32 bytes, got ${bytes.length}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const uint8Array = new Uint8Array(bytes);
|
|
||||||
return bytesToAddress(uint8Array);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts an array of numbers to a Solana address
|
|
||||||
* Example: [207, 240, 50, 185, ...] -> "B9Lf9z5BfNPT4d5KMeaBFx8x1G4CULZYR1jA2kmxRDka"
|
|
||||||
*/
|
|
||||||
export function byteArrayToAddress(byteArray: number[]): Address {
|
|
||||||
if (byteArray.length !== 32) {
|
|
||||||
throw new Error(`Expected 32 bytes, got ${byteArray.length}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const uint8Array = new Uint8Array(byteArray);
|
|
||||||
return bytesToAddress(uint8Array);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts various input formats to a valid Solana address
|
|
||||||
*/
|
|
||||||
export function toSolanaAddress(input: string | Uint8Array | number[]): Address {
|
|
||||||
if (typeof input === 'string') {
|
|
||||||
// Check if it's already a valid address
|
|
||||||
if (isAddress(input)) {
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's a comma-separated byte string
|
|
||||||
if (input.includes(',')) {
|
|
||||||
return byteStringToAddress(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to parse as address
|
|
||||||
return address(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input instanceof Uint8Array) {
|
|
||||||
return bytesToAddress(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(input)) {
|
|
||||||
return byteArrayToAddress(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Invalid input format for address conversion');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Truncates an address for display purposes
|
|
||||||
*/
|
|
||||||
export function truncateAddress(addr: string | Address, startLength = 4, endLength = 4): string {
|
|
||||||
if (addr.length <= startLength + endLength) {
|
|
||||||
return addr;
|
|
||||||
}
|
|
||||||
return `${addr.slice(0, startLength)}...${addr.slice(-endLength)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates if a string is a valid Solana address
|
|
||||||
*/
|
|
||||||
export function isValidSolanaAddress(input: string): boolean {
|
|
||||||
return isAddress(input);
|
|
||||||
}
|
|
||||||
|
|
@ -1,235 +0,0 @@
|
||||||
// Simple IPFS metadata fetcher with direct gateway access
|
|
||||||
export interface TokenMetadata {
|
|
||||||
name?: string;
|
|
||||||
symbol?: string;
|
|
||||||
description?: string;
|
|
||||||
image?: string;
|
|
||||||
showName?: boolean;
|
|
||||||
createdOn?: string;
|
|
||||||
twitter?: string;
|
|
||||||
website?: string;
|
|
||||||
telegram?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache for metadata to avoid duplicate requests
|
|
||||||
const metadataCache = new Map<string, TokenMetadata>();
|
|
||||||
|
|
||||||
// IPFS gateways that support CORS
|
|
||||||
const IPFS_GATEWAYS = [
|
|
||||||
'https://dweb.link/ipfs/',
|
|
||||||
'https://nftstorage.link/ipfs/',
|
|
||||||
'https://cloudflare-ipfs.com/ipfs/',
|
|
||||||
'https://gateway.pinata.cloud/ipfs/',
|
|
||||||
'https://ipfs.io/ipfs/'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Extract IPFS hash from various URI formats
|
|
||||||
function extractIpfsHash(uri: string): string | null {
|
|
||||||
if (!uri) return null;
|
|
||||||
|
|
||||||
// Handle different IPFS URI formats:
|
|
||||||
// - ipfs://bafkreixxx
|
|
||||||
// - https://ipfs.io/ipfs/bafkreixxx
|
|
||||||
// - bafkreixxx (direct hash)
|
|
||||||
|
|
||||||
if (uri.startsWith('ipfs://')) {
|
|
||||||
return uri.replace('ipfs://', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uri.includes('/ipfs/')) {
|
|
||||||
const parts = uri.split('/ipfs/');
|
|
||||||
return parts[1]?.split('/')[0] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assume it's a direct hash if it looks like one
|
|
||||||
if (uri.match(/^[a-zA-Z0-9]{46,}$/)) {
|
|
||||||
return uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchTokenMetadata(uri: string): Promise<TokenMetadata | null> {
|
|
||||||
if (!uri || typeof uri !== 'string') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check cache first
|
|
||||||
if (metadataCache.has(uri)) {
|
|
||||||
return metadataCache.get(uri)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Extract IPFS hash from URI
|
|
||||||
const hash = extractIpfsHash(uri);
|
|
||||||
if (!hash) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try each gateway until one works
|
|
||||||
for (const gateway of IPFS_GATEWAYS) {
|
|
||||||
try {
|
|
||||||
const url = `${gateway}${hash}`;
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
mode: 'cors',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
},
|
|
||||||
signal: AbortSignal.timeout(8000) // 8 second timeout
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
continue; // Try next gateway
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if response is JSON
|
|
||||||
const contentType = response.headers.get('content-type');
|
|
||||||
if (!contentType || !contentType.includes('application/json')) {
|
|
||||||
continue; // Try next gateway
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadata: TokenMetadata = await response.json();
|
|
||||||
|
|
||||||
// Cache the result
|
|
||||||
metadataCache.set(uri, metadata);
|
|
||||||
return metadata;
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
// Continue to next gateway
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If all gateways fail, return null
|
|
||||||
return null;
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to get token image URL
|
|
||||||
export function getTokenImage(metadata: TokenMetadata): string | null {
|
|
||||||
if (!metadata.image) return null;
|
|
||||||
|
|
||||||
// If the image is already a full URL, return it
|
|
||||||
if (metadata.image.startsWith('http')) {
|
|
||||||
return metadata.image;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's an IPFS URI, extract the hash and use a reliable gateway
|
|
||||||
const hash = extractIpfsHash(metadata.image);
|
|
||||||
if (hash) {
|
|
||||||
return `https://dweb.link/ipfs/${hash}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return metadata.image;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to check if a URL is a social media link
|
|
||||||
export function getSocialIcon(url: string): string | null {
|
|
||||||
if (!url) return null;
|
|
||||||
|
|
||||||
if (url.includes('twitter.com') || url.includes('x.com')) {
|
|
||||||
return 'twitter';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.includes('telegram.org') || url.includes('t.me')) {
|
|
||||||
return 'telegram';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.includes('discord.gg') || url.includes('discord.com')) {
|
|
||||||
return 'discord';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'website';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to check if metadata has social links
|
|
||||||
export function getSocialLinks(metadata: TokenMetadata) {
|
|
||||||
return {
|
|
||||||
twitter: metadata.twitter || null,
|
|
||||||
website: metadata.website || null,
|
|
||||||
telegram: metadata.telegram || null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to validate and clean social URLs
|
|
||||||
export function cleanSocialUrl(url: string): string | null {
|
|
||||||
if (!url || typeof url !== 'string') return null;
|
|
||||||
|
|
||||||
// Basic URL validation
|
|
||||||
try {
|
|
||||||
const urlObj = new URL(url);
|
|
||||||
return urlObj.href;
|
|
||||||
} catch {
|
|
||||||
// If not a valid URL, try to make it one
|
|
||||||
if (!url.startsWith('http')) {
|
|
||||||
try {
|
|
||||||
const urlObj = new URL(`https://${url}`);
|
|
||||||
return urlObj.href;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to extract social links from metadata
|
|
||||||
export function extractSocialLinks(metadata: TokenMetadata) {
|
|
||||||
const links: Array<{ type: string; url: string; icon: string }> = [];
|
|
||||||
|
|
||||||
if (metadata.twitter) {
|
|
||||||
const cleanUrl = cleanSocialUrl(metadata.twitter);
|
|
||||||
const icon = getSocialIcon('twitter');
|
|
||||||
if (cleanUrl && icon) {
|
|
||||||
links.push({
|
|
||||||
type: 'twitter',
|
|
||||||
url: cleanUrl,
|
|
||||||
icon
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata.website) {
|
|
||||||
const cleanUrl = cleanSocialUrl(metadata.website);
|
|
||||||
const icon = getSocialIcon('website');
|
|
||||||
if (cleanUrl && icon) {
|
|
||||||
links.push({
|
|
||||||
type: 'website',
|
|
||||||
url: cleanUrl,
|
|
||||||
icon
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata.telegram) {
|
|
||||||
const cleanUrl = cleanSocialUrl(metadata.telegram);
|
|
||||||
const icon = getSocialIcon('telegram');
|
|
||||||
if (cleanUrl && icon) {
|
|
||||||
links.push({
|
|
||||||
type: 'telegram',
|
|
||||||
url: cleanUrl,
|
|
||||||
icon
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return links;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear cache utility
|
|
||||||
export function clearMetadataCache(): void {
|
|
||||||
metadataCache.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get cache statistics
|
|
||||||
export function getCacheStats() {
|
|
||||||
return {
|
|
||||||
size: metadataCache.size,
|
|
||||||
keys: Array.from(metadataCache.keys())
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,283 +0,0 @@
|
||||||
# IPFS Retry Implementation & Error Handling
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
This document outlines the comprehensive retry mechanism and error handling improvements implemented for IPFS metadata fetching to resolve `AbortError: signal is aborted without reason` issues and CORS-related problems.
|
|
||||||
|
|
||||||
## Key Features Implemented
|
|
||||||
|
|
||||||
### 1. Enhanced IPFS Utility (`app/utils/ipfs.ts`)
|
|
||||||
|
|
||||||
#### Retry Configuration
|
|
||||||
- **Max Retries**: 5 attempts per gateway (automatic fallback)
|
|
||||||
- **Timeout**: 8 seconds per individual request
|
|
||||||
- **Gateway Rotation**: Automatic fallback to next gateway on failure
|
|
||||||
|
|
||||||
#### CORS-Friendly Gateway Strategy
|
|
||||||
```typescript
|
|
||||||
const IPFS_GATEWAYS = [
|
|
||||||
'https://dweb.link/ipfs/',
|
|
||||||
'https://nftstorage.link/ipfs/',
|
|
||||||
'https://cloudflare-ipfs.com/ipfs/',
|
|
||||||
'https://gateway.pinata.cloud/ipfs/',
|
|
||||||
'https://ipfs.io/ipfs/'
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
**Gateway Selection Strategy:**
|
|
||||||
- Prioritizes CORS-friendly gateways (`dweb.link`, `nftstorage.link`)
|
|
||||||
- Falls back to other reliable gateways
|
|
||||||
- Automatic rotation on failure
|
|
||||||
|
|
||||||
#### Smart Error Handling
|
|
||||||
- **Content-Type Validation**: Ensures response is JSON before parsing
|
|
||||||
- **Network Error Detection**: Distinguishes between network and parsing errors
|
|
||||||
- **Graceful Degradation**: Returns `null` on failure instead of throwing errors
|
|
||||||
- **Gateway Isolation**: Individual gateway failures don't affect others
|
|
||||||
|
|
||||||
#### Caching Mechanism
|
|
||||||
- **Simple Map-based Cache**: Prevents duplicate requests for same URIs
|
|
||||||
- **Memory Management**: Configurable cache with statistics
|
|
||||||
- **Cache Utilities**: `clearMetadataCache()` and `getCacheStats()` functions
|
|
||||||
|
|
||||||
### 2. Enhanced TokenCard Component (`app/components/TokenCard.vue`)
|
|
||||||
|
|
||||||
#### Direct Metadata Integration
|
|
||||||
- **Non-blocking Loading**: Metadata loads after component mounts
|
|
||||||
- **Individual Error Handling**: Card failures don't affect others
|
|
||||||
- **Visual Feedback**: Loading spinners and error indicators
|
|
||||||
|
|
||||||
#### Social Media Integration
|
|
||||||
```typescript
|
|
||||||
// Automatic detection of social links
|
|
||||||
const socialLinks = extractSocialLinks(metadata);
|
|
||||||
// Returns: { type: 'twitter', url: string, icon: string }[]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Supported Platforms:**
|
|
||||||
- Twitter/X (twitter.com, x.com)
|
|
||||||
- Telegram (t.me, telegram.org)
|
|
||||||
- Discord (discord.gg, discord.com)
|
|
||||||
- Generic Website (fallback)
|
|
||||||
|
|
||||||
#### Image Handling
|
|
||||||
- **IPFS Image Support**: Automatic IPFS hash extraction and gateway routing
|
|
||||||
- **Fallback Avatars**: Gradient avatars with token symbol when images fail
|
|
||||||
- **Error Recovery**: Graceful handling of image load failures
|
|
||||||
|
|
||||||
## Core Functions
|
|
||||||
|
|
||||||
### Primary Functions
|
|
||||||
|
|
||||||
#### `fetchTokenMetadata(uri: string): Promise<TokenMetadata | null>`
|
|
||||||
- Fetches metadata from IPFS URI
|
|
||||||
- Handles multiple URI formats (ipfs://, https://ipfs.io/ipfs/, direct hash)
|
|
||||||
- Returns `null` on failure (no exceptions thrown)
|
|
||||||
- Automatic caching to prevent duplicate requests
|
|
||||||
|
|
||||||
#### `getTokenImage(metadata: TokenMetadata): string | null`
|
|
||||||
- Extracts and formats token image URL
|
|
||||||
- Handles IPFS URIs and direct URLs
|
|
||||||
- Uses reliable gateways for IPFS images
|
|
||||||
|
|
||||||
#### `extractSocialLinks(metadata: TokenMetadata)`
|
|
||||||
- Extracts social media links from metadata
|
|
||||||
- Returns array of social link objects with icons
|
|
||||||
- Validates and cleans URLs
|
|
||||||
|
|
||||||
### Utility Functions
|
|
||||||
|
|
||||||
#### `extractIpfsHash(uri: string): string | null`
|
|
||||||
- Extracts IPFS hash from various URI formats
|
|
||||||
- Supports ipfs://, gateway URLs, and direct hashes
|
|
||||||
|
|
||||||
#### `getSocialIcon(url: string): string | null`
|
|
||||||
- Determines appropriate icon for social media URL
|
|
||||||
- Returns icon identifier for UI rendering
|
|
||||||
|
|
||||||
#### `cleanSocialUrl(url: string): string | null`
|
|
||||||
- Validates and normalizes social media URLs
|
|
||||||
- Adds https:// prefix when missing
|
|
||||||
|
|
||||||
## CORS Resolution
|
|
||||||
|
|
||||||
### Problem Identified
|
|
||||||
Initial implementation encountered CORS errors:
|
|
||||||
```
|
|
||||||
Access to fetch at 'https://ipfs.io/ipfs/...' from origin 'http://localhost:3000'
|
|
||||||
has been blocked by CORS policy: Request header field cache-control is not
|
|
||||||
allowed by Access-Control-Allow-Headers in preflight response.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Solutions Attempted
|
|
||||||
|
|
||||||
#### 1. Server API Route Approach (Abandoned)
|
|
||||||
- Created Nuxt server API route to proxy IPFS requests
|
|
||||||
- Issues: Returned HTML instead of JSON in Electron environment
|
|
||||||
- Not compatible with Electron + Nuxt setup
|
|
||||||
|
|
||||||
#### 2. CORS-Friendly Gateway Strategy (Final Solution)
|
|
||||||
- Prioritized gateways with proper CORS headers
|
|
||||||
- `dweb.link` and `nftstorage.link` as primary gateways
|
|
||||||
- Removed problematic headers from requests
|
|
||||||
- Simplified request configuration
|
|
||||||
|
|
||||||
### Final Working Configuration
|
|
||||||
```typescript
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
mode: 'cors',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
},
|
|
||||||
signal: AbortSignal.timeout(8000)
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture Decisions
|
|
||||||
|
|
||||||
### Why Direct Implementation Over Composables
|
|
||||||
1. **Simplicity**: Direct metadata fetching in components is easier to debug
|
|
||||||
2. **Performance**: Eliminates unnecessary abstraction layers
|
|
||||||
3. **Maintenance**: Fewer files to maintain and update
|
|
||||||
4. **Debugging**: Clearer error tracking and logging
|
|
||||||
|
|
||||||
### Why Multiple Gateways
|
|
||||||
1. **Reliability**: Fallback ensures higher success rate
|
|
||||||
2. **Performance**: Different gateways have varying response times
|
|
||||||
3. **CORS Compatibility**: Not all gateways support CORS properly
|
|
||||||
4. **Geographic Distribution**: Better global accessibility
|
|
||||||
|
|
||||||
### Why Simple Caching
|
|
||||||
1. **Memory Efficiency**: Map-based cache with minimal overhead
|
|
||||||
2. **Request Deduplication**: Prevents multiple requests for same URI
|
|
||||||
3. **No Persistence**: Cache clears on app restart (prevents stale data)
|
|
||||||
4. **Statistics**: Built-in cache monitoring
|
|
||||||
|
|
||||||
## Error Handling Strategy
|
|
||||||
|
|
||||||
### Non-Blocking Operations
|
|
||||||
- Individual token failures don't affect others
|
|
||||||
- UI remains responsive during metadata fetching
|
|
||||||
- Graceful degradation with fallback content
|
|
||||||
|
|
||||||
### User Experience
|
|
||||||
- **Loading States**: Clear visual feedback during fetching
|
|
||||||
- **Error Indicators**: Subtle error icons with tooltips
|
|
||||||
- **Fallback Content**: Token symbol avatars when images fail
|
|
||||||
- **Retry Capability**: Users can refresh individual tokens
|
|
||||||
|
|
||||||
### Developer Experience
|
|
||||||
- **No Console Spam**: Removed all debugging output
|
|
||||||
- **Clear Error Types**: Distinguishable error conditions
|
|
||||||
- **Cache Management**: Tools for cache inspection and clearing
|
|
||||||
|
|
||||||
## Performance Optimizations
|
|
||||||
|
|
||||||
### Request Optimization
|
|
||||||
- 8-second timeout prevents hanging requests
|
|
||||||
- Automatic gateway rotation minimizes wait time
|
|
||||||
- Content-type validation prevents unnecessary parsing
|
|
||||||
- Simple caching reduces duplicate requests
|
|
||||||
|
|
||||||
### UI Optimization
|
|
||||||
- Non-blocking metadata loading
|
|
||||||
- Individual component error isolation
|
|
||||||
- Efficient social link extraction
|
|
||||||
- Optimized image loading with fallbacks
|
|
||||||
|
|
||||||
## Implementation Status
|
|
||||||
|
|
||||||
### ✅ Completed Features
|
|
||||||
- [x] Multiple CORS-friendly IPFS gateways
|
|
||||||
- [x] Automatic retry with gateway fallback
|
|
||||||
- [x] Simple metadata caching
|
|
||||||
- [x] Social media link extraction and icons
|
|
||||||
- [x] Image handling with IPFS support
|
|
||||||
- [x] Error handling without console spam
|
|
||||||
- [x] Non-blocking UI operations
|
|
||||||
- [x] Clean TypeScript implementation
|
|
||||||
|
|
||||||
### 🚫 Removed Features
|
|
||||||
- [x] Complex composable abstractions
|
|
||||||
- [x] Batch processing utilities
|
|
||||||
- [x] Server API proxy routes
|
|
||||||
- [x] Debugging console output
|
|
||||||
- [x] Exponential backoff (replaced with gateway rotation)
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
### Basic Metadata Fetching
|
|
||||||
```typescript
|
|
||||||
import { fetchTokenMetadata } from '../utils/ipfs';
|
|
||||||
|
|
||||||
const metadata = await fetchTokenMetadata('https://ipfs.io/ipfs/bafkreixxx');
|
|
||||||
if (metadata) {
|
|
||||||
console.log(metadata.name, metadata.symbol);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Image Handling
|
|
||||||
```typescript
|
|
||||||
import { getTokenImage } from '../utils/ipfs';
|
|
||||||
|
|
||||||
const imageUrl = getTokenImage(metadata);
|
|
||||||
if (imageUrl) {
|
|
||||||
// Use imageUrl for img src
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Social Links
|
|
||||||
```typescript
|
|
||||||
import { extractSocialLinks } from '../utils/ipfs';
|
|
||||||
|
|
||||||
const socialLinks = extractSocialLinks(metadata);
|
|
||||||
socialLinks.forEach(link => {
|
|
||||||
console.log(link.type, link.url, link.icon);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing and Validation
|
|
||||||
|
|
||||||
### Manual Testing Performed
|
|
||||||
- [x] IPFS metadata fetching from various URIs
|
|
||||||
- [x] Gateway fallback functionality
|
|
||||||
- [x] CORS compatibility across gateways
|
|
||||||
- [x] Image loading and fallback behavior
|
|
||||||
- [x] Social media link detection
|
|
||||||
- [x] Error handling and recovery
|
|
||||||
- [x] Cache functionality and statistics
|
|
||||||
|
|
||||||
### Known Working URIs
|
|
||||||
```
|
|
||||||
https://ipfs.io/ipfs/bafkreigr67ogup7ijve5mq7vh22nyydsvksfqtctxu3bdtsgs47uihlaka
|
|
||||||
https://ipfs.io/ipfs/bafkreido7xq6dx2m7nxlnoeoz562uapvpfs4yup2eyckerzvggylgttcoa
|
|
||||||
```
|
|
||||||
|
|
||||||
## Maintenance Notes
|
|
||||||
|
|
||||||
### Cache Management
|
|
||||||
```typescript
|
|
||||||
import { clearMetadataCache, getCacheStats } from '../utils/ipfs';
|
|
||||||
|
|
||||||
// Clear cache when needed
|
|
||||||
clearMetadataCache();
|
|
||||||
|
|
||||||
// Monitor cache usage
|
|
||||||
const stats = getCacheStats();
|
|
||||||
console.log(`Cache size: ${stats.size}, hits: ${stats.hits}`);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Gateway Management
|
|
||||||
- Monitor gateway performance and update priority as needed
|
|
||||||
- Add new CORS-friendly gateways when available
|
|
||||||
- Remove unreliable gateways from the list
|
|
||||||
|
|
||||||
### Error Monitoring
|
|
||||||
- Monitor for new types of IPFS errors
|
|
||||||
- Update error handling as needed
|
|
||||||
- Track gateway success rates for optimization
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated**: December 22, 2024
|
|
||||||
**Status**: Production Ready ✅
|
|
||||||
6
build.rs
Normal file
6
build.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
fn main() {
|
||||||
|
// Use fluent-light as default, but we'll support dynamic switching
|
||||||
|
let config = slint_build::CompilerConfiguration::new().with_style("fluent".to_string());
|
||||||
|
|
||||||
|
slint_build::compile_with_config("ui/index.slint", config).expect("Slint build failed");
|
||||||
|
}
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
/**
|
|
||||||
* Environment Configuration
|
|
||||||
* This handles different configurations for development and production builds
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface EnvironmentConfig {
|
|
||||||
redis: {
|
|
||||||
host: string;
|
|
||||||
port: number;
|
|
||||||
};
|
|
||||||
app: {
|
|
||||||
name: string;
|
|
||||||
version: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Development Configuration
|
|
||||||
*/
|
|
||||||
const developmentConfig: EnvironmentConfig = {
|
|
||||||
redis: {
|
|
||||||
host: 'localhost', // or 'bismillahdao-redis' if using Docker
|
|
||||||
port: 6379,
|
|
||||||
},
|
|
||||||
app: {
|
|
||||||
name: 'Ziya Token Monitor (Dev)',
|
|
||||||
version: '1.0.0-dev',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Production Configuration
|
|
||||||
*/
|
|
||||||
const productionConfig: EnvironmentConfig = {
|
|
||||||
redis: {
|
|
||||||
host: '154.38.185.112', // Your production Redis server
|
|
||||||
port: 6379,
|
|
||||||
},
|
|
||||||
app: {
|
|
||||||
name: 'Ziya Token Monitor',
|
|
||||||
version: '1.0.0',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get configuration based on NODE_ENV
|
|
||||||
*/
|
|
||||||
export const getEnvironmentConfig = (): EnvironmentConfig => {
|
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
|
||||||
|
|
||||||
if (isProduction) {
|
|
||||||
console.info('[CONFIG] Using production configuration');
|
|
||||||
return productionConfig;
|
|
||||||
} else {
|
|
||||||
console.info('[CONFIG] Using development configuration');
|
|
||||||
return developmentConfig;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current environment configuration
|
|
||||||
*/
|
|
||||||
export const ENV_CONFIG = getEnvironmentConfig();
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
/**
|
|
||||||
* Redis Configuration for different environments
|
|
||||||
*/
|
|
||||||
import { ENV_CONFIG } from './environment';
|
|
||||||
|
|
||||||
export interface RedisConfig {
|
|
||||||
host: string;
|
|
||||||
port: number;
|
|
||||||
lazyConnect: boolean;
|
|
||||||
retryDelayOnFailover: number;
|
|
||||||
maxRetriesPerRequest: number;
|
|
||||||
connectTimeout: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Environment-based Redis configuration
|
|
||||||
*/
|
|
||||||
const getRedisConfig = (): RedisConfig => {
|
|
||||||
return {
|
|
||||||
host: ENV_CONFIG.redis.host,
|
|
||||||
port: ENV_CONFIG.redis.port,
|
|
||||||
lazyConnect: true,
|
|
||||||
retryDelayOnFailover: 100,
|
|
||||||
maxRetriesPerRequest: 3,
|
|
||||||
connectTimeout: 10000,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Channels to subscribe to
|
|
||||||
*/
|
|
||||||
export const REDIS_CHANNELS = [
|
|
||||||
'new_token_created',
|
|
||||||
'token_cex_updated',
|
|
||||||
'max_depth_reached',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current Redis configuration
|
|
||||||
*/
|
|
||||||
export const REDIS_CONFIG = getRedisConfig();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log current configuration (without sensitive data)
|
|
||||||
*/
|
|
||||||
export const logRedisConfig = (): void => {
|
|
||||||
const env = process.env.NODE_ENV || 'development';
|
|
||||||
console.info(`[REDIS] Environment: ${env}`);
|
|
||||||
console.info(`[REDIS] Connecting to: ${REDIS_CONFIG.host}:${REDIS_CONFIG.port}`);
|
|
||||||
};
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export * from './ipc-handlers';
|
|
||||||
export * from './redis-handlers';
|
|
||||||
export { registerWindowHandlers } from './window-handlers';
|
|
||||||
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import { ipcMain } from 'electron';
|
|
||||||
|
|
||||||
type IpcHandler<T = unknown, R = unknown> = (event: Electron.IpcMainInvokeEvent, ...args: T[]) => Promise<R> | R;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility function to define IPC handlers with better type safety and error handling
|
|
||||||
*/
|
|
||||||
export function defineIpcHandler<T = unknown, R = unknown>(
|
|
||||||
channel: string,
|
|
||||||
handler: IpcHandler<T, R>,
|
|
||||||
): void {
|
|
||||||
ipcMain.handle(channel, async (event, ...args) => {
|
|
||||||
try {
|
|
||||||
return await handler(event, ...args);
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error(`Error in IPC handler '${channel}':`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove an IPC handler
|
|
||||||
*/
|
|
||||||
export function removeIpcHandler(channel: string): void {
|
|
||||||
ipcMain.removeHandler(channel);
|
|
||||||
}
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
import type { BrowserWindow } from 'electron';
|
|
||||||
|
|
||||||
interface RedisMessageData {
|
|
||||||
channel: string;
|
|
||||||
data: unknown;
|
|
||||||
timestamp: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle new token created events
|
|
||||||
*/
|
|
||||||
export function handleNewTokenCreated(mainWindow: BrowserWindow, data: unknown): void {
|
|
||||||
const messageData: RedisMessageData = {
|
|
||||||
channel: 'new_token_created',
|
|
||||||
data,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mainWindow.webContents.send('redis-data', messageData);
|
|
||||||
// console.info('Handled new token created:', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle token CEX updated events
|
|
||||||
*/
|
|
||||||
export function handleTokenCexUpdated(mainWindow: BrowserWindow, data: unknown): void {
|
|
||||||
const messageData: RedisMessageData = {
|
|
||||||
channel: 'token_cex_updated',
|
|
||||||
data,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mainWindow.webContents.send('redis-data', messageData);
|
|
||||||
// console.info('Handled token CEX updated:', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle max depth reached events
|
|
||||||
*/
|
|
||||||
export function handleMaxDepthReached(mainWindow: BrowserWindow, data: unknown): void {
|
|
||||||
const messageData: RedisMessageData = {
|
|
||||||
channel: 'max_depth_reached',
|
|
||||||
data,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mainWindow.webContents.send('redis-data', messageData);
|
|
||||||
// console.info('Handled max depth reached:', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the appropriate handler for a Redis channel
|
|
||||||
*/
|
|
||||||
export function getRedisChannelHandler(channel: string): ((mainWindow: BrowserWindow, data: unknown) => void) | null {
|
|
||||||
switch (channel) {
|
|
||||||
case 'new_token_created':
|
|
||||||
return handleNewTokenCreated;
|
|
||||||
case 'token_cex_updated':
|
|
||||||
return handleTokenCexUpdated;
|
|
||||||
case 'max_depth_reached':
|
|
||||||
return handleMaxDepthReached;
|
|
||||||
default:
|
|
||||||
console.warn(`No handler found for Redis channel: ${channel}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { BrowserWindow, shell } from 'electron';
|
|
||||||
import { defineIpcHandler } from './ipc-handlers';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register all window-related IPC handlers
|
|
||||||
*/
|
|
||||||
export function registerWindowHandlers(): void {
|
|
||||||
defineIpcHandler('window-minimize', () => {
|
|
||||||
const window = BrowserWindow.getFocusedWindow();
|
|
||||||
if (window) window.minimize();
|
|
||||||
});
|
|
||||||
|
|
||||||
defineIpcHandler('window-maximize', () => {
|
|
||||||
const window = BrowserWindow.getFocusedWindow();
|
|
||||||
if (window) {
|
|
||||||
if (window.isMaximized()) {
|
|
||||||
window.unmaximize();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
window.maximize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
defineIpcHandler('window-close', () => {
|
|
||||||
const window = BrowserWindow.getFocusedWindow();
|
|
||||||
if (window) window.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
defineIpcHandler('window-is-maximized', (): boolean => {
|
|
||||||
const window = BrowserWindow.getFocusedWindow();
|
|
||||||
return window ? window.isMaximized() : false;
|
|
||||||
});
|
|
||||||
|
|
||||||
defineIpcHandler('open-external', (_event, url: string) => {
|
|
||||||
shell.openExternal(url);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
import { BrowserWindow, app } from 'electron';
|
|
||||||
import started from 'electron-squirrel-startup';
|
|
||||||
import { registerWindowHandlers } from './handlers';
|
|
||||||
import { connectRedis, createMainWindow, disconnectRedis } from './utils';
|
|
||||||
|
|
||||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
|
||||||
if (started) {
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the application
|
|
||||||
*/
|
|
||||||
function initializeApp(): void {
|
|
||||||
// Connect to Redis
|
|
||||||
connectRedis();
|
|
||||||
|
|
||||||
// Register all IPC handlers
|
|
||||||
registerWindowHandlers();
|
|
||||||
|
|
||||||
// Create the main window
|
|
||||||
createMainWindow();
|
|
||||||
}
|
|
||||||
|
|
||||||
// This method will be called when Electron has finished
|
|
||||||
// initialization and is ready to create browser windows.
|
|
||||||
// Some APIs can only be used after this event occurs.
|
|
||||||
app.on('ready', initializeApp);
|
|
||||||
|
|
||||||
// Quit when all windows are closed, except on macOS. There, it's common
|
|
||||||
// for applications and their menu bar to stay active until the user quits
|
|
||||||
// explicitly with Cmd + Q.
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('activate', () => {
|
|
||||||
// On OS X it's common to re-create a window in the app when the
|
|
||||||
// dock icon is clicked and there are no other windows open.
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
|
||||||
createMainWindow();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clean up Redis connection on app quit
|
|
||||||
app.on('before-quit', () => {
|
|
||||||
disconnectRedis();
|
|
||||||
});
|
|
||||||
|
|
||||||
// In this file you can include the rest of your app's specific main process
|
|
||||||
// code. You can also put them in separate files and import them here.
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { contextBridge, ipcRenderer } from 'electron';
|
|
||||||
|
|
||||||
interface RedisData {
|
|
||||||
channel: string;
|
|
||||||
data: unknown;
|
|
||||||
timestamp: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expose protected methods that allow the renderer process to use
|
|
||||||
// the ipcRenderer without exposing the entire object
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
|
||||||
// Window controls
|
|
||||||
minimizeWindow: () => ipcRenderer.invoke('window-minimize'),
|
|
||||||
maximizeWindow: () => ipcRenderer.invoke('window-maximize'),
|
|
||||||
closeWindow: () => ipcRenderer.invoke('window-close'),
|
|
||||||
isMaximized: () => ipcRenderer.invoke('window-is-maximized'),
|
|
||||||
|
|
||||||
// Window state listeners
|
|
||||||
onMaximizeChange: (callback: (event: unknown, maximized: boolean) => void) => {
|
|
||||||
ipcRenderer.on('window-maximize-changed', callback);
|
|
||||||
},
|
|
||||||
removeMaximizeListener: (callback: (event: unknown, maximized: boolean) => void) => {
|
|
||||||
ipcRenderer.removeListener('window-maximize-changed', callback);
|
|
||||||
},
|
|
||||||
|
|
||||||
// External links
|
|
||||||
openExternal: (url: string) => ipcRenderer.invoke('open-external', url),
|
|
||||||
|
|
||||||
// Redis data subscription
|
|
||||||
onRedisData: (callback: (data: RedisData) => void) => {
|
|
||||||
ipcRenderer.on('redis-data', (_event, data) => callback(data));
|
|
||||||
},
|
|
||||||
|
|
||||||
// Remove listener
|
|
||||||
removeRedisDataListener: () => {
|
|
||||||
ipcRenderer.removeAllListeners('redis-data');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2022",
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": ".",
|
|
||||||
"baseUrl": ".",
|
|
||||||
"allowImportingTsExtensions": false,
|
|
||||||
"noEmit": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"verbatimModuleSyntax": false,
|
|
||||||
"paths": {
|
|
||||||
"./handlers/*": [
|
|
||||||
"./handlers/*"
|
|
||||||
],
|
|
||||||
"./utils/*": [
|
|
||||||
"./utils/*"
|
|
||||||
],
|
|
||||||
"./*": [
|
|
||||||
"./*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"types": [
|
|
||||||
"node",
|
|
||||||
"electron"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"**/*.ts",
|
|
||||||
"**/*.js"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules",
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"ts-node": {
|
|
||||||
"esm": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export * from './redis';
|
|
||||||
export * from './window';
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
import type { BrowserWindow } from 'electron';
|
|
||||||
import { Redis } from 'ioredis';
|
|
||||||
import { REDIS_CHANNELS, REDIS_CONFIG, logRedisConfig } from '../config/redis';
|
|
||||||
import { getRedisChannelHandler } from '../handlers/redis-handlers';
|
|
||||||
|
|
||||||
let redisSubscriber: Redis | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize Redis connection
|
|
||||||
*/
|
|
||||||
export function connectRedis(): void {
|
|
||||||
try {
|
|
||||||
// Log configuration info
|
|
||||||
logRedisConfig();
|
|
||||||
|
|
||||||
redisSubscriber = new Redis(REDIS_CONFIG);
|
|
||||||
|
|
||||||
redisSubscriber.on('error', (error) => {
|
|
||||||
console.error('[REDIS] Connection error:', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.info('[REDIS] Initialized');
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('[REDIS] Init failed:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up Redis pub/sub with message handlers
|
|
||||||
*/
|
|
||||||
export function setupRedisPubSub(mainWindow: BrowserWindow): void {
|
|
||||||
if (!redisSubscriber) {
|
|
||||||
console.error('[REDIS] Not initialized');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
redisSubscriber.subscribe(...REDIS_CHANNELS);
|
|
||||||
|
|
||||||
// Handle incoming messages
|
|
||||||
redisSubscriber.on('message', (channel: string, message: string) => {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(message);
|
|
||||||
const handler = getRedisChannelHandler(channel);
|
|
||||||
|
|
||||||
if (handler) {
|
|
||||||
handler(mainWindow, data);
|
|
||||||
} else {
|
|
||||||
console.warn(`[REDIS] No handler for '${channel}'`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error(`[REDIS] Parse error on '${channel}':`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.info('[REDIS] PubSub ready');
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('[REDIS] Setup error:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disconnect Redis
|
|
||||||
*/
|
|
||||||
export function disconnectRedis(): void {
|
|
||||||
if (redisSubscriber) {
|
|
||||||
redisSubscriber.disconnect();
|
|
||||||
redisSubscriber = null;
|
|
||||||
console.info('[REDIS] Disconnected');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Redis connection status
|
|
||||||
*/
|
|
||||||
export function getRedisStatus(): 'connected' | 'disconnected' | 'not_initialized' {
|
|
||||||
if (!redisSubscriber) return 'not_initialized';
|
|
||||||
return redisSubscriber.status === 'ready' ? 'connected' : 'disconnected';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test Redis connection by trying to connect
|
|
||||||
*/
|
|
||||||
export async function testRedisConnection(): Promise<boolean> {
|
|
||||||
if (!redisSubscriber) {
|
|
||||||
console.error('[REDIS] Not initialized');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await redisSubscriber.connect();
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[REDIS] Test failed:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,124 +0,0 @@
|
||||||
import { BrowserWindow, shell } from 'electron';
|
|
||||||
import path from 'node:path';
|
|
||||||
import { getRedisStatus, setupRedisPubSub, testRedisConnection } from './redis';
|
|
||||||
/**
|
|
||||||
* Window configuration - centralized values
|
|
||||||
*/
|
|
||||||
const WINDOW_CONFIG = {
|
|
||||||
minHeight: 800,
|
|
||||||
minWidth: 1080,
|
|
||||||
maxHeight: 1080,
|
|
||||||
maxWidth: 1920,
|
|
||||||
height: 1024,
|
|
||||||
width: 1280,
|
|
||||||
titleBarStyle: 'hidden' as const,
|
|
||||||
webPreferences: {
|
|
||||||
nodeIntegration: false,
|
|
||||||
contextIsolation: true,
|
|
||||||
preload: path.join(__dirname, 'preload.cjs'),
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create and configure the main application window
|
|
||||||
*/
|
|
||||||
export function createMainWindow(): BrowserWindow {
|
|
||||||
const mainWindow = new BrowserWindow(WINDOW_CONFIG);
|
|
||||||
|
|
||||||
// Hide menu bar
|
|
||||||
mainWindow.setMenuBarVisibility(false);
|
|
||||||
|
|
||||||
// Set up window event listeners
|
|
||||||
setupWindowEventListeners(mainWindow);
|
|
||||||
|
|
||||||
// Set up external link handling
|
|
||||||
setupExternalLinkHandling(mainWindow);
|
|
||||||
|
|
||||||
// Load the appropriate content
|
|
||||||
loadWindowContent(mainWindow);
|
|
||||||
|
|
||||||
// Set up Redis pub/sub when window is ready
|
|
||||||
mainWindow.webContents.once('dom-ready', async () => {
|
|
||||||
console.info('[WINDOW] DOM ready, setting up Redis pub/sub...');
|
|
||||||
|
|
||||||
// Check Redis status
|
|
||||||
const status = getRedisStatus();
|
|
||||||
console.info('[WINDOW] Redis status:', status);
|
|
||||||
|
|
||||||
// Test connection if needed
|
|
||||||
if (status !== 'connected') {
|
|
||||||
console.info('[WINDOW] Testing Redis connection...');
|
|
||||||
const connected = await testRedisConnection();
|
|
||||||
console.info('[WINDOW] Redis connection test result:', connected);
|
|
||||||
}
|
|
||||||
|
|
||||||
setupRedisPubSub(mainWindow);
|
|
||||||
console.info('[WINDOW] Redis pub/sub setup completed');
|
|
||||||
});
|
|
||||||
|
|
||||||
return mainWindow;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up window event listeners for maximize/unmaximize
|
|
||||||
*/
|
|
||||||
function setupWindowEventListeners(mainWindow: BrowserWindow): void {
|
|
||||||
mainWindow.on('maximize', () => {
|
|
||||||
mainWindow.webContents.send('window-maximize-changed', true);
|
|
||||||
});
|
|
||||||
|
|
||||||
mainWindow.on('unmaximize', () => {
|
|
||||||
mainWindow.webContents.send('window-maximize-changed', false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up external link handling to open in default browser
|
|
||||||
*/
|
|
||||||
function setupExternalLinkHandling(mainWindow: BrowserWindow): void {
|
|
||||||
mainWindow.webContents.on('will-navigate', (event, reqUrl) => {
|
|
||||||
const requestedHost = new URL(reqUrl).host;
|
|
||||||
const currentHost = new URL(mainWindow.webContents.getURL()).host;
|
|
||||||
|
|
||||||
if (requestedHost && requestedHost !== currentHost) {
|
|
||||||
event.preventDefault();
|
|
||||||
shell.openExternal(reqUrl);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load window content based on environment
|
|
||||||
*/
|
|
||||||
function loadWindowContent(mainWindow: BrowserWindow): void {
|
|
||||||
const isDev = process.env.NODE_ENV === 'development';
|
|
||||||
|
|
||||||
if (isDev) {
|
|
||||||
mainWindow.setIcon(path.resolve(__dirname, '../../public/favicon.ico'));
|
|
||||||
// Try different ports to find the Nuxt dev server
|
|
||||||
const possiblePorts = [3000, 3001, 3002];
|
|
||||||
tryLoadDevServer(mainWindow, possiblePorts);
|
|
||||||
mainWindow.webContents.openDevTools();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to load the dev server from different ports
|
|
||||||
*/
|
|
||||||
function tryLoadDevServer(mainWindow: BrowserWindow, ports: number[], index = 0): void {
|
|
||||||
if (index >= ports.length) {
|
|
||||||
console.error('Could not find Nuxt dev server on any port');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const port = ports[index];
|
|
||||||
const url = `http://localhost:${port}`;
|
|
||||||
|
|
||||||
mainWindow.loadURL(url).catch(() => {
|
|
||||||
// If this port fails, try the next one
|
|
||||||
setTimeout(() => tryLoadDevServer(mainWindow, ports, index + 1), 100);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
// @ts-check
|
|
||||||
import stylistic from '@stylistic/eslint-plugin';
|
|
||||||
import withNuxt from './.nuxt/eslint.config.mjs';
|
|
||||||
|
|
||||||
export default withNuxt(
|
|
||||||
// Disable legacy stylistic rules
|
|
||||||
stylistic.configs['disable-legacy'],
|
|
||||||
{
|
|
||||||
files: ['**/*.vue', '**/*.js', '**/*.ts', '**/*.mjs'],
|
|
||||||
ignores: [
|
|
||||||
'node_modules/**',
|
|
||||||
'dist/**',
|
|
||||||
'.nuxt/**',
|
|
||||||
'.output/**',
|
|
||||||
'.vite/**',
|
|
||||||
'.*/**',
|
|
||||||
],
|
|
||||||
plugins: {
|
|
||||||
'@stylistic': stylistic,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
// Semicolon rules - require semicolons (Rust-style)
|
|
||||||
'@stylistic/semi': ['error', 'always'],
|
|
||||||
|
|
||||||
// Interface and type rules - require semicolons in interfaces
|
|
||||||
'@stylistic/member-delimiter-style': ['error', {
|
|
||||||
multiline: { delimiter: 'semi', requireLast: true },
|
|
||||||
singleline: { delimiter: 'semi', requireLast: false },
|
|
||||||
}],
|
|
||||||
|
|
||||||
// Code quality rules
|
|
||||||
'camelcase': ['error', { properties: 'never', ignoreDestructuring: true }],
|
|
||||||
'no-console': ['error', { allow: ['info', 'warn', 'error'] }],
|
|
||||||
'sort-imports': ['error', { ignoreDeclarationSort: true }],
|
|
||||||
|
|
||||||
// Nuxt specific rules
|
|
||||||
'nuxt/prefer-import-meta': 'error',
|
|
||||||
|
|
||||||
// Vue specific rules
|
|
||||||
'vue/first-attribute-linebreak': ['error', { singleline: 'ignore', multiline: 'ignore' }],
|
|
||||||
'vue/no-unused-vars': ['error', {
|
|
||||||
ignorePattern: '^_',
|
|
||||||
}],
|
|
||||||
'vue/max-attributes-per-line': ['error', { singleline: 100 }],
|
|
||||||
'vue/singleline-html-element-content-newline': ['off'],
|
|
||||||
'vue/no-multiple-template-root': ['off'],
|
|
||||||
'vue/html-closing-bracket-spacing': ['error', { selfClosingTag: 'always' }],
|
|
||||||
'vue/html-indent': ['error', 2],
|
|
||||||
'vue/multiline-html-element-content-newline': ['error', { ignores: [] }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
59
justfile
Normal file
59
justfile
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
# Default recipe to display available commands
|
||||||
|
default:
|
||||||
|
@just --list
|
||||||
|
|
||||||
|
# Run the application with dev features
|
||||||
|
run:
|
||||||
|
cargo run --features dev
|
||||||
|
|
||||||
|
# Run the application with prod features
|
||||||
|
run-prod:
|
||||||
|
cargo run --features prod
|
||||||
|
|
||||||
|
# Build the application with dev features
|
||||||
|
build:
|
||||||
|
cargo build --features dev
|
||||||
|
|
||||||
|
# Build the application with prod features
|
||||||
|
build-prod:
|
||||||
|
cargo build --features prod
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
test:
|
||||||
|
cargo test
|
||||||
|
|
||||||
|
# Run clippy linter
|
||||||
|
clippy:
|
||||||
|
cargo clippy --all-targets --all-features -- -D warnings
|
||||||
|
|
||||||
|
# Format code
|
||||||
|
fmt:
|
||||||
|
cargo fmt
|
||||||
|
|
||||||
|
# Check formatting
|
||||||
|
fmt-check:
|
||||||
|
cargo fmt --check
|
||||||
|
|
||||||
|
# Run development server with hot reloading using watchexec
|
||||||
|
dev:
|
||||||
|
watchexec --clear --stop-timeout=3s -i "./**/target" -w "src" -w "ui" -e "rs,toml,slint" --project-origin "." -r "just run"
|
||||||
|
|
||||||
|
# Generate changelog using git-cliff
|
||||||
|
changelog:
|
||||||
|
git-cliff -o CHANGELOG.md
|
||||||
|
|
||||||
|
# Generate unreleased changelog
|
||||||
|
changelog-unreleased:
|
||||||
|
git-cliff --unreleased --tag unreleased -o CHANGELOG.md
|
||||||
|
|
||||||
|
# Clean build artifacts
|
||||||
|
clean:
|
||||||
|
cargo clean
|
||||||
|
|
||||||
|
# Check for security vulnerabilities
|
||||||
|
audit:
|
||||||
|
cargo audit
|
||||||
|
|
||||||
|
# Install development dependencies
|
||||||
|
install-deps:
|
||||||
|
cargo install watchexec-cli git-cliff cargo-audit
|
||||||
85
package.json
85
package.json
|
|
@ -1,85 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Ziya",
|
|
||||||
"productName": "Ziya",
|
|
||||||
"version": "0.2.0",
|
|
||||||
"description": "One stop shop for your trading habit",
|
|
||||||
"type": "module",
|
|
||||||
"main": ".vite/build/main.cjs",
|
|
||||||
"scripts": {
|
|
||||||
"start": "electron-forge start",
|
|
||||||
"dev": "concurrently \"pnpm run dev:nuxt\" \"pnpm run dev:electron\"",
|
|
||||||
"dev:nuxt": "nuxt dev --config-file .config/nuxt.ts",
|
|
||||||
"dev:electron": "cross-env NODE_ENV=development electron-forge start",
|
|
||||||
"build": "cross-env NODE_ENV=production nuxt generate --config-file .config/nuxt.ts && cross-env NODE_ENV=production electron-forge make",
|
|
||||||
"build:dev": "cross-env NODE_ENV=development nuxt generate --config-file .config/nuxt.ts && cross-env NODE_ENV=development electron-forge make",
|
|
||||||
"package": "cross-env NODE_ENV=production electron-forge package",
|
|
||||||
"package:dev": "cross-env NODE_ENV=development electron-forge package",
|
|
||||||
"make": "electron-forge make",
|
|
||||||
"publish": "electron-forge publish",
|
|
||||||
"lint": "eslint .",
|
|
||||||
"lint:eslint:inspect": "pnpm dlx @eslint/config-inspector",
|
|
||||||
"format": "prettier --write .",
|
|
||||||
"changelog": "changelogen --output CHANGELOG.md",
|
|
||||||
"changelog:release": "changelogen --release --output CHANGELOG.md",
|
|
||||||
"release": "changelogen --release --push",
|
|
||||||
"release:dry": "changelogen --release --no-commit --no-tag"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "rizary",
|
|
||||||
"license": "MIT",
|
|
||||||
"devDependencies": {
|
|
||||||
"@electron-forge/cli": "^7.8.1",
|
|
||||||
"@electron-forge/maker-deb": "^7.8.1",
|
|
||||||
"@electron-forge/maker-dmg": "^7.8.1",
|
|
||||||
"@electron-forge/maker-rpm": "^7.8.1",
|
|
||||||
"@electron-forge/maker-squirrel": "^7.8.1",
|
|
||||||
"@electron-forge/maker-zip": "^7.8.1",
|
|
||||||
"@electron-forge/plugin-auto-unpack-natives": "^7.8.1",
|
|
||||||
"@electron-forge/plugin-fuses": "^7.8.1",
|
|
||||||
"@electron-forge/plugin-vite": "^7.8.1",
|
|
||||||
"@electron-forge/publisher-github": "^7.8.1",
|
|
||||||
"@electron/fuses": "^1.8.0",
|
|
||||||
"@nuxt/eslint": "^1.4.1",
|
|
||||||
"@pinia/nuxt": "^0.11.1",
|
|
||||||
"@stylistic/eslint-plugin": "^4.4.1",
|
|
||||||
"@tailwindcss/cli": "^4.1.10",
|
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
|
||||||
"@tailwindcss/vite": "^4.1.10",
|
|
||||||
"@types/electron-squirrel-startup": "^1.0.2",
|
|
||||||
"@types/node": "^24.0.3",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^8.34.1",
|
|
||||||
"@typescript-eslint/parser": "^8.34.1",
|
|
||||||
"changelogen": "^0.6.1",
|
|
||||||
"concurrently": "^9.1.2",
|
|
||||||
"cross-env": "^7.0.3",
|
|
||||||
"daisyui": "^5.0.43",
|
|
||||||
"electron": "36.5.0",
|
|
||||||
"electron-packager-languages": "^0.6.0",
|
|
||||||
"eslint": "^9.29.0",
|
|
||||||
"eslint-config-prettier": "^10.1.5",
|
|
||||||
"eslint-plugin-import": "^2.32.0",
|
|
||||||
"nuxt": "^3.17.5",
|
|
||||||
"pinia": "^3.0.3",
|
|
||||||
"prettier": "3.5.3",
|
|
||||||
"stylelint": "^16.21.0",
|
|
||||||
"tailwindcss": "^4.1.10",
|
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"typescript": "^5.8.3",
|
|
||||||
"vite": "^6.3.5",
|
|
||||||
"vite-plugin-electron": "^0.29.0",
|
|
||||||
"vite-plugin-electron-renderer": "^0.14.6",
|
|
||||||
"vite-plugin-eslint2": "^5.0.3",
|
|
||||||
"vitest": "^3.2.4",
|
|
||||||
"vue-tsc": "^2.2.10"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@nuxt/icon": "^1.14.0",
|
|
||||||
"@solana/kit": "^2.1.1",
|
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
|
||||||
"ioredis": "^5.6.1"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"forge": ".config/forge.ts"
|
|
||||||
},
|
|
||||||
"packageManager": "pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac"
|
|
||||||
}
|
|
||||||
14987
pnpm-lock.yaml
generated
14987
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,14 +0,0 @@
|
||||||
packages:
|
|
||||||
- .
|
|
||||||
|
|
||||||
ignoredBuiltDependencies:
|
|
||||||
- '@parcel/watcher'
|
|
||||||
- '@tailwindcss/oxide'
|
|
||||||
- esbuild
|
|
||||||
- unrs-resolver
|
|
||||||
|
|
||||||
nodeLinker: hoisted
|
|
||||||
|
|
||||||
onlyBuiltDependencies:
|
|
||||||
- electron
|
|
||||||
- electron-winstaller
|
|
||||||
4
public/icons/dark-mode/chart-network-dark-mode.svg
Normal file
4
public/icons/dark-mode/chart-network-dark-mode.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24" style="filter: brightness(0) invert(1);">
|
||||||
|
<path fill="currentColor" d="m20.5,13c-1.204,0-2.268.612-2.898,1.54l-1.014-.563c.263-.607.411-1.275.411-1.977,0-2.757-2.243-5-5-5-.822,0-1.586.218-2.27.571l-.937-1.448c.734-.642,1.207-1.574,1.207-2.623,0-1.93-1.57-3.5-3.5-3.5s-3.5,1.57-3.5,3.5,1.57,3.5,3.5,3.5c.521,0,1.012-.122,1.457-.328l.934,1.443c-1.143.917-1.891,2.308-1.891,3.884,0,1.198.441,2.284,1.146,3.146l-2.56,2.56c-.584-.438-1.302-.707-2.086-.707-1.93,0-3.5,1.57-3.5,3.5s1.57,3.5,3.5,3.5,3.5-1.57,3.5-3.5c0-.785-.269-1.502-.707-2.086l2.56-2.56c.862.705,1.948,1.146,3.146,1.146,1.697,0,3.195-.854,4.099-2.151l1.08.6c-.106.334-.179.682-.179,1.051,0,1.93,1.57,3.5,3.5,3.5s3.5-1.57,3.5-3.5-1.57-3.5-3.5-3.5ZM4,3.5c0-1.378,1.121-2.5,2.5-2.5s2.5,1.122,2.5,2.5-1.121,2.5-2.5,2.5-2.5-1.122-2.5-2.5Zm-.5,19.5c-1.379,0-2.5-1.122-2.5-2.5s1.121-2.5,2.5-2.5,2.5,1.122,2.5,2.5-1.121,2.5-2.5,2.5Zm8.5-7c-2.206,0-4-1.794-4-4s1.794-4,4-4,4,1.794,4,4-1.794,4-4,4Zm8.5,3c-1.379,0-2.5-1.122-2.5-2.5s1.121-2.5,2.5-2.5,2.5,1.122,2.5,2.5-1.121,2.5-2.5,2.5Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
2
public/icons/light-mode/chart-network-light-mode.svg
Normal file
2
public/icons/light-mode/chart-network-light-mode.svg
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24" width="512" height="512"><path d="M20,12a3.994,3.994,0,0,0-3.172,1.566l-.07-.03a5,5,0,0,0-6.009-6.377l-.091-.172A3.995,3.995,0,1,0,8.879,7.9l.073.137a4.992,4.992,0,0,0-1.134,6.7L5.933,16.5a4,4,0,1,0,1.455,1.377l1.838-1.718a4.993,4.993,0,0,0,6.539-.871l.279.119A4,4,0,1,0,20,12ZM6,4A2,2,0,1,1,8,6,2,2,0,0,1,6,4ZM4,22a2,2,0,1,1,2-2A2,2,0,0,1,4,22Zm8-7a3,3,0,0,1-1.6-5.534l.407-.217A3,3,0,1,1,12,15Zm8,3a2,2,0,1,1,2-2A2,2,0,0,1,20,18Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 573 B |
37
src/config.rs
Normal file
37
src/config.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
use crate::err_with_loc;
|
||||||
|
use crate::error::app::AppError;
|
||||||
|
use crate::error::Result;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct StorageRedisConfig {
|
||||||
|
pub host: String,
|
||||||
|
pub port: u16,
|
||||||
|
pub pool_size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LoggingConfig {
|
||||||
|
pub directory: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
pub storage_redis: StorageRedisConfig,
|
||||||
|
pub logging: LoggingConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load_config(path: impl AsRef<Path>) -> Result<Config> {
|
||||||
|
let config_str = std::fs::read_to_string(path).map_err(|e| {
|
||||||
|
err_with_loc!(AppError::Config(format!(
|
||||||
|
"failed_to_read_config_file: {}",
|
||||||
|
e
|
||||||
|
)))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let config: Config = toml::from_str(&config_str)
|
||||||
|
.map_err(|e| err_with_loc!(AppError::Config(format!("failed_to_parse_config: {}", e))))?;
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
22
src/error/app.rs
Normal file
22
src/error/app.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum AppError {
|
||||||
|
#[error("Redis error: {0}")]
|
||||||
|
Redis(String),
|
||||||
|
|
||||||
|
#[error("Handler error: {0}")]
|
||||||
|
Handler(String),
|
||||||
|
|
||||||
|
#[error("Slint error: {0}")]
|
||||||
|
Slint(String),
|
||||||
|
|
||||||
|
#[error("Serialization error: {0}")]
|
||||||
|
Serialization(String),
|
||||||
|
|
||||||
|
#[error("Configuration error: {0}")]
|
||||||
|
Config(String),
|
||||||
|
|
||||||
|
#[error("UI error: {0}")]
|
||||||
|
UiError(String),
|
||||||
|
}
|
||||||
13
src/error/mod.rs
Normal file
13
src/error/mod.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
pub mod app;
|
||||||
|
|
||||||
|
pub use anyhow::anyhow;
|
||||||
|
pub use anyhow::Context;
|
||||||
|
pub use anyhow::Error;
|
||||||
|
pub use anyhow::Result;
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! err_with_loc {
|
||||||
|
($err:expr) => {
|
||||||
|
anyhow::anyhow!($err).context(format!("at {}:{}", file!(), line!()))
|
||||||
|
};
|
||||||
|
}
|
||||||
19
src/handler/mod.rs
Normal file
19
src/handler/mod.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
pub mod token;
|
||||||
|
pub mod ui;
|
||||||
|
|
||||||
|
use crate::model::token::{MaxDepthReachedData, NewTokenCreatedData, TokenCexUpdatedData};
|
||||||
|
|
||||||
|
// Messages that can be sent to the hunting ground handler
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum TokenHandler {
|
||||||
|
NewToken { data: NewTokenCreatedData },
|
||||||
|
CexUpdated { data: TokenCexUpdatedData },
|
||||||
|
MaxDepthReached { data: MaxDepthReachedData },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum SlintHandler {
|
||||||
|
ClearNewTokens {},
|
||||||
|
ClearCexTokens {},
|
||||||
|
ClearAnalysisTokens {},
|
||||||
|
}
|
||||||
284
src/handler/token.rs
Normal file
284
src/handler/token.rs
Normal file
|
|
@ -0,0 +1,284 @@
|
||||||
|
use chrono::TimeZone;
|
||||||
|
use chrono::Utc;
|
||||||
|
use chrono_tz::Asia::Jakarta;
|
||||||
|
use std::{collections::VecDeque, sync::Arc};
|
||||||
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
|
use super::SlintHandler;
|
||||||
|
use super::TokenHandler;
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::model::token::{MaxDepthReachedData, NewTokenCreatedData, TokenCexUpdatedData};
|
||||||
|
use crate::slint_ui::{CexUpdatedUiData, MainWindow, MaxDepthReachedUiData, NewTokenUiData};
|
||||||
|
use crate::storage::StorageEngine;
|
||||||
|
use crate::task::shutdown::ShutdownSignal;
|
||||||
|
use slint::{Model, SharedString, Weak};
|
||||||
|
|
||||||
|
// Internal state for the hunting ground handler
|
||||||
|
pub struct TokenMetadataHandler {
|
||||||
|
pub receiver: mpsc::Receiver<TokenHandler>,
|
||||||
|
pub slint_tx: mpsc::Sender<SlintHandler>,
|
||||||
|
pub ui_weak: Weak<MainWindow>,
|
||||||
|
pub shutdown: ShutdownSignal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TokenMetadataHandler {
|
||||||
|
pub fn new(
|
||||||
|
receiver: mpsc::Receiver<TokenHandler>,
|
||||||
|
slint_tx: mpsc::Sender<SlintHandler>,
|
||||||
|
ui_weak: Weak<MainWindow>,
|
||||||
|
shutdown: ShutdownSignal,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
receiver,
|
||||||
|
slint_tx,
|
||||||
|
ui_weak,
|
||||||
|
shutdown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn process_new_token_to_slint(&self, token: NewTokenCreatedData) {
|
||||||
|
info!("processing_new_token_to_slint: mint={}", token.mint.clone());
|
||||||
|
let utc_timestamp = Utc.timestamp_millis_opt(token.created_at as i64).unwrap();
|
||||||
|
let jakarta_timestamp = utc_timestamp.with_timezone(&chrono_tz::Asia::Jakarta);
|
||||||
|
let created_at = jakarta_timestamp.format("%Y-%m-%d %H:%M:%S");
|
||||||
|
|
||||||
|
let new_token_ui_data = NewTokenUiData {
|
||||||
|
mint: SharedString::from(token.mint.to_string()),
|
||||||
|
bonding_curve: token
|
||||||
|
.bonding_curve
|
||||||
|
.map(|bc| SharedString::from(bc.to_string()))
|
||||||
|
.unwrap_or(SharedString::from("")),
|
||||||
|
name: SharedString::from(token.name),
|
||||||
|
symbol: SharedString::from(token.symbol),
|
||||||
|
uri: SharedString::from(token.uri),
|
||||||
|
creator: SharedString::from(token.creator.to_string()),
|
||||||
|
created_at: SharedString::from(created_at.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ Update UI using proper Slint threading model
|
||||||
|
let ui_weak_clone = self.ui_weak.clone();
|
||||||
|
slint::invoke_from_event_loop(move || {
|
||||||
|
if let Some(ui) = ui_weak_clone.upgrade() {
|
||||||
|
// Convert ModelRc to Vec, add new item, and set back
|
||||||
|
let current_tokens = ui.get_new_tokens();
|
||||||
|
let mut tokens_vec: Vec<NewTokenUiData> = current_tokens.iter().collect();
|
||||||
|
tokens_vec.insert(0, new_token_ui_data);
|
||||||
|
ui.set_new_tokens(tokens_vec.as_slice().into());
|
||||||
|
info!(
|
||||||
|
"process_new_token_to_slint::ui_updated::total_tokens={}",
|
||||||
|
tokens_vec.len()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
warn!("process_new_token_to_slint::ui_weak_reference_expired");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
error!(
|
||||||
|
"process_new_token_to_slint::failed_to_invoke_from_event_loop: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn process_cex_updated_to_slint(&self, token: TokenCexUpdatedData) {
|
||||||
|
info!("processing_cex_updated_to_slint: mint={}", token.mint);
|
||||||
|
|
||||||
|
let utc_created_at = Utc.timestamp_millis_opt(token.created_at as i64).unwrap();
|
||||||
|
let jakarta_created_at = utc_created_at.with_timezone(&chrono_tz::Asia::Jakarta);
|
||||||
|
let created_at = jakarta_created_at.format("%Y-%m-%d %H:%M:%S");
|
||||||
|
|
||||||
|
let utc_updated_at = Utc.timestamp_millis_opt(token.updated_at as i64).unwrap();
|
||||||
|
let jakarta_updated_at = utc_updated_at.with_timezone(&chrono_tz::Asia::Jakarta);
|
||||||
|
let updated_at = jakarta_updated_at.format("%Y-%m-%d %H:%M:%S");
|
||||||
|
|
||||||
|
let cex_updated_ui_data = CexUpdatedUiData {
|
||||||
|
mint: SharedString::from(token.mint),
|
||||||
|
name: SharedString::from(token.name),
|
||||||
|
uri: SharedString::from(token.uri),
|
||||||
|
dev_name: SharedString::from(token.dev_name),
|
||||||
|
creator: SharedString::from(token.creator),
|
||||||
|
cex_name: SharedString::from(token.cex_name),
|
||||||
|
cex_address: SharedString::from(token.cex_address),
|
||||||
|
bonding_curve: SharedString::from(token.bonding_curve),
|
||||||
|
created_at: SharedString::from(created_at.to_string()),
|
||||||
|
updated_at: SharedString::from(updated_at.to_string()),
|
||||||
|
node_count: token.node_count as i32,
|
||||||
|
edge_count: token.edge_count as i32,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ Update UI using proper Slint threading model
|
||||||
|
let ui_weak_clone = self.ui_weak.clone();
|
||||||
|
slint::invoke_from_event_loop(move || {
|
||||||
|
if let Some(ui) = ui_weak_clone.upgrade() {
|
||||||
|
// Convert ModelRc to Vec, add new item, and set back
|
||||||
|
let current_tokens = ui.get_cex_tokens();
|
||||||
|
let mut tokens_vec: Vec<CexUpdatedUiData> = current_tokens.iter().collect();
|
||||||
|
tokens_vec.insert(0, cex_updated_ui_data);
|
||||||
|
ui.set_cex_tokens(tokens_vec.as_slice().into());
|
||||||
|
info!(
|
||||||
|
"process_cex_updated_to_slint::ui_updated::total_tokens={}",
|
||||||
|
tokens_vec.len()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
warn!("process_cex_updated_to_slint::ui_weak_reference_expired");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
error!(
|
||||||
|
"process_cex_updated_to_slint::failed_to_invoke_from_event_loop: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn process_max_depth_reached_to_slint(&self, token: MaxDepthReachedData) {
|
||||||
|
info!("processing_max_depth_reached_to_slint: mint={}", token.mint);
|
||||||
|
|
||||||
|
let utc_created_at = Utc.timestamp_millis_opt(token.created_at as i64).unwrap();
|
||||||
|
let jakarta_created_at = utc_created_at.with_timezone(&chrono_tz::Asia::Jakarta);
|
||||||
|
let created_at = jakarta_created_at.format("%Y-%m-%d %H:%M:%S");
|
||||||
|
|
||||||
|
let utc_updated_at = Utc.timestamp_millis_opt(token.updated_at as i64).unwrap();
|
||||||
|
let jakarta_updated_at = utc_updated_at.with_timezone(&chrono_tz::Asia::Jakarta);
|
||||||
|
let updated_at = jakarta_updated_at.format("%Y-%m-%d %H:%M:%S");
|
||||||
|
|
||||||
|
let max_depth_ui_data = MaxDepthReachedUiData {
|
||||||
|
mint: SharedString::from(token.mint),
|
||||||
|
name: SharedString::from(token.name),
|
||||||
|
uri: SharedString::from(token.uri),
|
||||||
|
dev_name: SharedString::from(token.dev_name),
|
||||||
|
creator: SharedString::from(token.creator),
|
||||||
|
bonding_curve: SharedString::from(token.bonding_curve),
|
||||||
|
created_at: SharedString::from(created_at.to_string()),
|
||||||
|
updated_at: SharedString::from(updated_at.to_string()),
|
||||||
|
node_count: token.node_count as i32,
|
||||||
|
edge_count: token.edge_count as i32,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ Update UI using proper Slint threading model
|
||||||
|
let ui_weak_clone = self.ui_weak.clone();
|
||||||
|
slint::invoke_from_event_loop(move || {
|
||||||
|
if let Some(ui) = ui_weak_clone.upgrade() {
|
||||||
|
// Convert ModelRc to Vec, add new item, and set back
|
||||||
|
let current_tokens = ui.get_analysis_tokens();
|
||||||
|
let mut tokens_vec: Vec<MaxDepthReachedUiData> = current_tokens.iter().collect();
|
||||||
|
tokens_vec.insert(0, max_depth_ui_data);
|
||||||
|
ui.set_analysis_tokens(tokens_vec.as_slice().into());
|
||||||
|
info!(
|
||||||
|
"process_max_depth_reached_to_slint::ui_updated::total_tokens={}",
|
||||||
|
tokens_vec.len()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
warn!("process_max_depth_reached_to_slint::ui_weak_reference_expired");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
error!(
|
||||||
|
"process_max_depth_reached_to_slint::failed_to_invoke_from_event_loop: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_token_handler(mut token_metadata_handler: TokenMetadataHandler) {
|
||||||
|
debug!("token_handler::started");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
Some(msg) = token_metadata_handler.receiver.recv() => {
|
||||||
|
match msg {
|
||||||
|
TokenHandler::NewToken { data } => {
|
||||||
|
info!("received_new_token: mint={}", data.mint);
|
||||||
|
token_metadata_handler.process_new_token_to_slint(data).await;
|
||||||
|
},
|
||||||
|
TokenHandler::CexUpdated { data } => {
|
||||||
|
info!("received_cex_updated_token: mint={}", data.mint);
|
||||||
|
token_metadata_handler.process_cex_updated_to_slint(data).await;
|
||||||
|
},
|
||||||
|
TokenHandler::MaxDepthReached { data } => {
|
||||||
|
info!("received_max_depth_reached_token: mint={}", data.mint);
|
||||||
|
token_metadata_handler.process_max_depth_reached_to_slint(data).await;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
// Channel closed, exit gracefully
|
||||||
|
debug!("hunting_ground_handler_token::channel_closed::exiting");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main hunting ground handler following the muhafidh actor pattern
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TokenMetadataHandlerOperator {
|
||||||
|
pub db: Arc<StorageEngine>,
|
||||||
|
pub sender: mpsc::Sender<TokenHandler>,
|
||||||
|
pub shutdown: ShutdownSignal,
|
||||||
|
pub ui_weak: Weak<MainWindow>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TokenMetadataHandlerOperator {
|
||||||
|
pub fn new(
|
||||||
|
db: Arc<StorageEngine>,
|
||||||
|
shutdown: ShutdownSignal,
|
||||||
|
receiver: mpsc::Receiver<TokenHandler>,
|
||||||
|
sender: mpsc::Sender<TokenHandler>,
|
||||||
|
slint_tx: mpsc::Sender<SlintHandler>,
|
||||||
|
ui_weak: Weak<MainWindow>,
|
||||||
|
) -> Self {
|
||||||
|
let token_metadata_handler =
|
||||||
|
TokenMetadataHandler::new(receiver, slint_tx, ui_weak.clone(), shutdown.clone());
|
||||||
|
|
||||||
|
// Spawn the actor
|
||||||
|
tokio::spawn(run_token_handler(token_metadata_handler));
|
||||||
|
Self {
|
||||||
|
db,
|
||||||
|
sender,
|
||||||
|
shutdown,
|
||||||
|
ui_weak,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn process_new_token(&self, token: NewTokenCreatedData) -> Result<()> {
|
||||||
|
info!("processing_new_token: mint={}", token.mint.clone());
|
||||||
|
if let Err(e) = self.sender.try_send(TokenHandler::NewToken {
|
||||||
|
data: token.clone(),
|
||||||
|
}) {
|
||||||
|
error!(
|
||||||
|
"failed_to_send_token_to_token_handler::mint::{}::error::{}",
|
||||||
|
token.mint, e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn process_cex_updated(&self, token: TokenCexUpdatedData) -> Result<()> {
|
||||||
|
info!("processing_cex_updated: mint={}", token.mint);
|
||||||
|
if let Err(e) = self.sender.try_send(TokenHandler::CexUpdated {
|
||||||
|
data: token.clone(),
|
||||||
|
}) {
|
||||||
|
error!(
|
||||||
|
"failed_to_send_cex_updated_to_token_handler::mint::{}::error::{}",
|
||||||
|
token.mint, e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn process_max_depth_reached(&self, token: MaxDepthReachedData) -> Result<()> {
|
||||||
|
info!("processing_max_depth_reached: mint={}", token.mint);
|
||||||
|
if let Err(e) = self.sender.try_send(TokenHandler::MaxDepthReached {
|
||||||
|
data: token.clone(),
|
||||||
|
}) {
|
||||||
|
error!(
|
||||||
|
"failed_to_send_max_depth_reached_to_token_handler::mint::{}::error::{}",
|
||||||
|
token.mint, e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/handler/ui.rs
Normal file
99
src/handler/ui.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
|
use super::SlintHandler;
|
||||||
|
use super::TokenHandler;
|
||||||
|
use crate::storage::StorageEngine;
|
||||||
|
use crate::task::shutdown::ShutdownSignal;
|
||||||
|
|
||||||
|
// Internal state for the hunting ground handler
|
||||||
|
pub struct SlintHandlerUi {
|
||||||
|
pub receiver: mpsc::Receiver<SlintHandler>,
|
||||||
|
pub token_tx: mpsc::Sender<TokenHandler>,
|
||||||
|
pub shutdown: ShutdownSignal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SlintHandlerUi {
|
||||||
|
pub fn new(
|
||||||
|
receiver: mpsc::Receiver<SlintHandler>,
|
||||||
|
token_tx: mpsc::Sender<TokenHandler>,
|
||||||
|
shutdown: ShutdownSignal,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
receiver,
|
||||||
|
token_tx,
|
||||||
|
shutdown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_slint_handler_ui(mut slint_handler_ui: SlintHandlerUi) {
|
||||||
|
debug!("slint_handler_ui::started");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
Some(msg) = slint_handler_ui.receiver.recv() => {
|
||||||
|
match msg {
|
||||||
|
SlintHandler::ClearNewTokens { } => {
|
||||||
|
info!("received_clear_new_tokens");
|
||||||
|
},
|
||||||
|
SlintHandler::ClearCexTokens { } => {
|
||||||
|
info!("received_clear_cex_tokens");
|
||||||
|
},
|
||||||
|
SlintHandler::ClearAnalysisTokens { } => {
|
||||||
|
info!("received_clear_analysis_tokens");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
// Channel closed, exit gracefully
|
||||||
|
debug!("hunting_ground_handler_token::channel_closed::exiting");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Main hunting ground handler following the muhafidh actor pattern
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SlintHandlerUiOperator {
|
||||||
|
pub db: Arc<StorageEngine>,
|
||||||
|
pub sender: mpsc::Sender<SlintHandler>,
|
||||||
|
pub shutdown: ShutdownSignal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SlintHandlerUiOperator {
|
||||||
|
pub fn new(
|
||||||
|
db: Arc<StorageEngine>,
|
||||||
|
shutdown: ShutdownSignal,
|
||||||
|
receiver: mpsc::Receiver<SlintHandler>,
|
||||||
|
sender: mpsc::Sender<SlintHandler>,
|
||||||
|
token_tx: mpsc::Sender<TokenHandler>,
|
||||||
|
) -> Self {
|
||||||
|
let slint_handler_ui = SlintHandlerUi::new(receiver, token_tx, shutdown.clone());
|
||||||
|
|
||||||
|
// Spawn the actor
|
||||||
|
tokio::spawn(run_slint_handler_ui(slint_handler_ui));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
db,
|
||||||
|
sender,
|
||||||
|
shutdown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn clear_new_tokens(&self) {
|
||||||
|
if let Err(e) = self.sender.try_send(SlintHandler::ClearNewTokens {}) {
|
||||||
|
error!("failed_to_send_clear_new_tokens::error::{}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn clear_cex_tokens(&self) {
|
||||||
|
if let Err(e) = self.sender.try_send(SlintHandler::ClearCexTokens {}) {
|
||||||
|
error!("failed_to_send_clear_cex_tokens::error::{}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn clear_analysis_tokens(&self) {
|
||||||
|
if let Err(e) = self.sender.try_send(SlintHandler::ClearAnalysisTokens {}) {
|
||||||
|
error!("failed_to_send_clear_analysis_tokens::error::{}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
363
src/lib.rs
Normal file
363
src/lib.rs
Normal file
|
|
@ -0,0 +1,363 @@
|
||||||
|
// This lib.rs is currently not used since we're building a binary application
|
||||||
|
// If needed in the future, add modules here
|
||||||
|
|
||||||
|
pub mod config;
|
||||||
|
pub mod error;
|
||||||
|
pub mod handler;
|
||||||
|
pub mod model;
|
||||||
|
pub mod storage;
|
||||||
|
pub mod task;
|
||||||
|
pub mod tracing;
|
||||||
|
|
||||||
|
pub mod slint_ui {
|
||||||
|
slint::include_modules!();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-export commonly used types
|
||||||
|
pub use config::Config;
|
||||||
|
pub use error::app::AppError;
|
||||||
|
pub use error::Result;
|
||||||
|
pub use slint::*;
|
||||||
|
pub use slint_ui::*;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
use handler::token::TokenMetadataHandlerOperator;
|
||||||
|
use handler::ui::SlintHandlerUiOperator;
|
||||||
|
use handler::SlintHandler;
|
||||||
|
use handler::TokenHandler;
|
||||||
|
use storage::StorageEngine;
|
||||||
|
use task::shutdown::ShutdownSignal;
|
||||||
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
|
// Application states
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum AppState {
|
||||||
|
Loading,
|
||||||
|
Login,
|
||||||
|
Authenticated,
|
||||||
|
}
|
||||||
|
|
||||||
|
// User session
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UserSession {
|
||||||
|
pub email: String,
|
||||||
|
pub authenticated_at: SystemTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_unix_timestamp() -> i32 {
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs() as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple email validation
|
||||||
|
pub fn is_valid_email(email: &str) -> bool {
|
||||||
|
email.contains('@') && email.contains('.') && email.len() > 5
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health check function
|
||||||
|
pub async fn check_backend_health(db: &StorageEngine) -> bool {
|
||||||
|
// Check Redis connection
|
||||||
|
match db.redis.pool.get().await {
|
||||||
|
Ok(mut conn) => {
|
||||||
|
match redis::cmd("PING").query_async::<String>(&mut *conn).await {
|
||||||
|
Ok(_) => {
|
||||||
|
info!("Redis health check passed");
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Redis ping failed: {}", e);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Redis health check failed: {}", e);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ZiyaApp {
|
||||||
|
pub db: Arc<StorageEngine>,
|
||||||
|
pub ui_weak: Weak<MainWindow>,
|
||||||
|
pub shutdown: ShutdownSignal,
|
||||||
|
pub cancellation_token: CancellationToken,
|
||||||
|
pub slint_handler: Arc<SlintHandlerUiOperator>,
|
||||||
|
pub token_handler: Arc<TokenMetadataHandlerOperator>,
|
||||||
|
pub app_state: AppState,
|
||||||
|
pub user_session: Option<UserSession>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZiyaApp {
|
||||||
|
pub async fn new(config: &Config, ui_weak: Weak<MainWindow>) -> Result<Self> {
|
||||||
|
info!("Creating storage engine");
|
||||||
|
let db = StorageEngine::new(config.clone()).await?;
|
||||||
|
let db = Arc::new(db);
|
||||||
|
|
||||||
|
let shutdown_signal = ShutdownSignal::new();
|
||||||
|
let cancellation_token = CancellationToken::new();
|
||||||
|
|
||||||
|
let (slint_tx, slint_rx) = mpsc::channel::<SlintHandler>(1000);
|
||||||
|
let (token_tx, token_rx) = mpsc::channel::<TokenHandler>(1000);
|
||||||
|
|
||||||
|
let slint_handler = Arc::new(SlintHandlerUiOperator::new(
|
||||||
|
db.clone(),
|
||||||
|
shutdown_signal.clone(),
|
||||||
|
slint_rx,
|
||||||
|
slint_tx.clone(),
|
||||||
|
token_tx.clone(),
|
||||||
|
));
|
||||||
|
|
||||||
|
let token_handler = Arc::new(TokenMetadataHandlerOperator::new(
|
||||||
|
db.clone(),
|
||||||
|
shutdown_signal.clone(),
|
||||||
|
token_rx,
|
||||||
|
token_tx,
|
||||||
|
slint_tx,
|
||||||
|
ui_weak.clone(),
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
db,
|
||||||
|
ui_weak,
|
||||||
|
shutdown: shutdown_signal,
|
||||||
|
cancellation_token,
|
||||||
|
slint_handler,
|
||||||
|
token_handler,
|
||||||
|
app_state: AppState::Loading,
|
||||||
|
user_session: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the UI with health check
|
||||||
|
pub fn init_ui(&self) -> Result<()> {
|
||||||
|
if let Some(ui) = self.ui_weak.upgrade() {
|
||||||
|
// Set initial state
|
||||||
|
ui.set_app_state("loading".into());
|
||||||
|
ui.set_is_loading(true);
|
||||||
|
ui.set_has_connection_error(false);
|
||||||
|
ui.set_loading_status("Initializing your trading environment...".into());
|
||||||
|
|
||||||
|
// Set up health check callback
|
||||||
|
let db = self.db.clone();
|
||||||
|
let ui_weak = self.ui_weak.clone();
|
||||||
|
|
||||||
|
ui.on_retry_health_check(move || {
|
||||||
|
let db = db.clone();
|
||||||
|
let ui_weak_for_spawn = ui_weak.clone();
|
||||||
|
let ui_weak_for_immediate = ui_weak.clone();
|
||||||
|
|
||||||
|
// Immediately update UI for loading state
|
||||||
|
if let Some(ui) = ui_weak_for_immediate.upgrade() {
|
||||||
|
ui.set_is_loading(true);
|
||||||
|
ui.set_has_connection_error(false);
|
||||||
|
ui.set_loading_status("Checking backend connection...".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn background task for health check
|
||||||
|
tokio::spawn(async move {
|
||||||
|
// Simulate loading time
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||||
|
|
||||||
|
let health_result = check_backend_health(&db).await;
|
||||||
|
|
||||||
|
// Update UI from event loop thread
|
||||||
|
let _ = slint::invoke_from_event_loop(move || {
|
||||||
|
if let Some(ui) = ui_weak_for_spawn.upgrade() {
|
||||||
|
if health_result {
|
||||||
|
info!("Health check passed, transitioning to login");
|
||||||
|
ui.set_app_state("login".into());
|
||||||
|
} else {
|
||||||
|
warn!("Health check failed");
|
||||||
|
ui.set_has_connection_error(true);
|
||||||
|
ui.set_is_loading(false);
|
||||||
|
ui.set_loading_status("Connection failed".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up login callback
|
||||||
|
let ui_weak = self.ui_weak.clone();
|
||||||
|
ui.on_login_attempt(move |email, _password| {
|
||||||
|
let ui_weak = ui_weak.clone();
|
||||||
|
let email_str = email.to_string();
|
||||||
|
|
||||||
|
info!("Login attempt for email: {}", email_str);
|
||||||
|
|
||||||
|
if let Some(ui) = ui_weak.upgrade() {
|
||||||
|
if is_valid_email(&email_str) {
|
||||||
|
ui.set_user_email(email.into());
|
||||||
|
ui.set_app_state("authenticated".into());
|
||||||
|
ui.set_is_authenticated(true);
|
||||||
|
info!("User authenticated successfully");
|
||||||
|
} else {
|
||||||
|
warn!("Invalid email provided: {}", email_str);
|
||||||
|
// Handle login error in the UI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up logout callback
|
||||||
|
let ui_weak = self.ui_weak.clone();
|
||||||
|
ui.on_logout_requested(move || {
|
||||||
|
let ui_weak = ui_weak.clone();
|
||||||
|
|
||||||
|
if let Some(ui) = ui_weak.upgrade() {
|
||||||
|
info!("User logout requested");
|
||||||
|
ui.set_user_email("".into());
|
||||||
|
ui.set_app_state("login".into());
|
||||||
|
ui.set_is_authenticated(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up authenticate user callback (for demo mode)
|
||||||
|
let ui_weak = self.ui_weak.clone();
|
||||||
|
ui.on_authenticate_user(move |email| {
|
||||||
|
let ui_weak = ui_weak.clone();
|
||||||
|
let email_str = email.to_string();
|
||||||
|
|
||||||
|
if let Some(ui) = ui_weak.upgrade() {
|
||||||
|
info!("Demo authentication for: {}", email_str);
|
||||||
|
ui.set_user_email(email.into());
|
||||||
|
ui.set_app_state("authenticated".into());
|
||||||
|
ui.set_is_authenticated(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up sidebar toggle
|
||||||
|
let ui_weak = self.ui_weak.clone();
|
||||||
|
ui.on_toggle_sidebar(move || {
|
||||||
|
let ui_weak = ui_weak.clone();
|
||||||
|
|
||||||
|
if let Some(ui) = ui_weak.upgrade() {
|
||||||
|
let current_state = ui.get_sidebar_state();
|
||||||
|
if current_state.as_str() == "full" {
|
||||||
|
ui.set_sidebar_state("icon-only".into());
|
||||||
|
} else {
|
||||||
|
ui.set_sidebar_state("full".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start initial health check
|
||||||
|
let db = self.db.clone();
|
||||||
|
let ui_weak = self.ui_weak.clone();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
// Initial delay to show loading screen
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||||
|
|
||||||
|
let health_result = check_backend_health(&db).await;
|
||||||
|
|
||||||
|
// Update UI from event loop thread
|
||||||
|
let _ = slint::invoke_from_event_loop(move || {
|
||||||
|
if let Some(ui) = ui_weak.upgrade() {
|
||||||
|
ui.set_loading_status("Checking backend connection...".into());
|
||||||
|
|
||||||
|
if health_result {
|
||||||
|
info!("Initial health check passed");
|
||||||
|
ui.set_app_state("login".into());
|
||||||
|
} else {
|
||||||
|
warn!("Initial health check failed");
|
||||||
|
ui.set_has_connection_error(true);
|
||||||
|
ui.set_is_loading(false);
|
||||||
|
ui.set_loading_status("Connection failed".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(AppError::UiError("Failed to upgrade UI weak reference".into()).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(self) -> Result<()> {
|
||||||
|
info!("Starting Ziya Slint Application");
|
||||||
|
|
||||||
|
// Initialize UI callbacks
|
||||||
|
self.init_ui()?;
|
||||||
|
|
||||||
|
let slint_handler = self.slint_handler.clone();
|
||||||
|
let db = self.db.clone();
|
||||||
|
|
||||||
|
let (shutdown_tx, mut shutdown_rx) = mpsc::channel(1);
|
||||||
|
|
||||||
|
let ui_task = task::ui::spawn_ui_task(
|
||||||
|
slint_handler.clone(),
|
||||||
|
self.ui_weak.clone(),
|
||||||
|
self.cancellation_token.clone(),
|
||||||
|
shutdown_tx.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ✅ Spawn all background subscriber tasks
|
||||||
|
let new_token_subscriber = task::subscriber::spawn_new_token_subscriber(
|
||||||
|
self.token_handler.clone(),
|
||||||
|
db.clone(),
|
||||||
|
self.cancellation_token.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let cex_updated_subscriber = task::subscriber::spawn_token_cex_updated_subscriber(
|
||||||
|
self.token_handler.clone(),
|
||||||
|
db.clone(),
|
||||||
|
self.cancellation_token.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let max_depth_subscriber = task::subscriber::spawn_max_depth_reached_subscriber(
|
||||||
|
self.token_handler.clone(),
|
||||||
|
db.clone(),
|
||||||
|
self.cancellation_token.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ✅ Use the original tokio::select! pattern
|
||||||
|
tokio::select! {
|
||||||
|
result = ui_task => {
|
||||||
|
match result {
|
||||||
|
Ok(Ok(())) => info!("ui_task_completed_successfully"),
|
||||||
|
Ok(Err(e)) => error!("ui_task_error: {}", e),
|
||||||
|
Err(e) => error!("ui_task_join_error: {}", e),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
result = new_token_subscriber => {
|
||||||
|
match result {
|
||||||
|
Ok(Ok(())) => info!("new_token_subscriber_completed_successfully"),
|
||||||
|
Ok(Err(e)) => error!("new_token_subscriber_error: {}", e),
|
||||||
|
Err(e) => error!("new_token_subscriber_join_error: {}", e),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
result = cex_updated_subscriber => {
|
||||||
|
match result {
|
||||||
|
Ok(Ok(())) => info!("cex_updated_subscriber_completed_successfully"),
|
||||||
|
Ok(Err(e)) => error!("cex_updated_subscriber_error: {}", e),
|
||||||
|
Err(e) => error!("cex_updated_subscriber_join_error: {}", e),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
result = max_depth_subscriber => {
|
||||||
|
match result {
|
||||||
|
Ok(Ok(())) => info!("max_depth_subscriber_completed_successfully"),
|
||||||
|
Ok(Err(e)) => error!("max_depth_subscriber_error: {}", e),
|
||||||
|
Err(e) => error!("max_depth_subscriber_join_error: {}", e),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ = tokio::signal::ctrl_c() => {
|
||||||
|
info!("ctrl_c_received::initiating_shutdown");
|
||||||
|
let _ = shutdown_tx.send(()).await;
|
||||||
|
},
|
||||||
|
_ = shutdown_rx.recv() => {
|
||||||
|
info!("shutdown_signal_received");
|
||||||
|
self.cancellation_token.cancel();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("application_shutdown_complete");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/main.rs
Normal file
36
src/main.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
// Prevent console window in addition to Slint window in Windows release builds when, e.g., starting the app via file manager. Ignored on other platforms.
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
use ziya::config::load_config;
|
||||||
|
use ziya::error::Result;
|
||||||
|
use ziya::slint_ui::*;
|
||||||
|
use ziya::tracing::setup_tracing;
|
||||||
|
use ziya::ZiyaApp;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
// ✅ Create and show UI on main thread using Slint's built-in pattern
|
||||||
|
let ui = MainWindow::new()
|
||||||
|
.map_err(|e| ziya::error::app::AppError::Slint(format!("failed_to_create_ui: {}", e)))?;
|
||||||
|
|
||||||
|
// ✅ Set up async tasks using spawn_local with async-compat for Tokio futures
|
||||||
|
let ui_weak = ui.as_weak();
|
||||||
|
// Load configuration
|
||||||
|
let config = load_config("Config.toml").await.unwrap();
|
||||||
|
|
||||||
|
// Initialize tracing with config
|
||||||
|
setup_tracing(config.clone(), "ziya-slint").await.unwrap();
|
||||||
|
|
||||||
|
// Create and run the app
|
||||||
|
let app = ZiyaApp::new(&config, ui_weak).await.unwrap();
|
||||||
|
slint::spawn_local(async_compat::Compat::new(async {
|
||||||
|
app.run().await.unwrap();
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
ui.run().map_err(|e| {
|
||||||
|
ziya::error::app::AppError::Slint(format!("failed_to_run_event_loop: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
612
src/model/cex.rs
Normal file
612
src/model/cex.rs
Normal file
|
|
@ -0,0 +1,612 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Cex {
|
||||||
|
pub name: CexName,
|
||||||
|
pub address: solana_pubkey::Pubkey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cex {
|
||||||
|
pub fn new(name: CexName, address: solana_pubkey::Pubkey) -> Self {
|
||||||
|
Self { name, address }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_exchange_name(address: solana_pubkey::Pubkey) -> Option<CexName> {
|
||||||
|
match address.to_string().as_str() {
|
||||||
|
"FpwQQhQQoEaVu3WU2qZMfF1hx48YyfwsLoRgXG83E99Q" => Some(CexName::CoinbaseHW1),
|
||||||
|
"GJRs4FwHtemZ5ZE9x3FNvJ8TMwitKTh21yxdRPqn7npE" => Some(CexName::CoinbaseHW2),
|
||||||
|
"D89hHJT5Aqyx1trP6EnGY9jJUB3whgnq3aUvvCqedvzf" => Some(CexName::CoinbaseHW3),
|
||||||
|
"DPqsobysNf5iA9w7zrQM8HLzCKZEDMkZsWbiidsAt1xo" => Some(CexName::CoinbaseHW4),
|
||||||
|
"H8sMJSCQxfKiFTCfDR3DUMLPwcRbM61LGFJ8N4dK3WjS" => Some(CexName::Coinbase1),
|
||||||
|
"2AQdpHJ2JpcEgPiATUXjQxA8QmafFegfQwSLWSprPicm" => Some(CexName::Coinbase2),
|
||||||
|
"59L2oxymiQQ9Hvhh92nt8Y7nDYjsauFkdb3SybdnsG6h" => Some(CexName::Coinbase4),
|
||||||
|
"9obNtb5GyUegcs3a1CbBkLuc5hEWynWfJC6gjz5uWQkE" => Some(CexName::Coinbase5),
|
||||||
|
"3vxheE5C46XzK4XftziRhwAf8QAfipD7HXXWj25mgkom" => Some(CexName::CoinbasePrime),
|
||||||
|
"CKy3KzEMSL1PQV6Wppggoqi2nGA7teE4L7JipEK89yqj" => Some(CexName::CoinbaseCW1),
|
||||||
|
"G6zmnfSdG6QJaDWYwbGQ4dpCSUC4gvjfZxYQ4ZharV7C" => Some(CexName::CoinbaseCW2),
|
||||||
|
"VTvk7sG6QQ28iK3NEKRRD9fvPzk5pKpJL2iwgVqMFcL" => Some(CexName::CoinbaseCW3),
|
||||||
|
"85cPov8nuRCkJ88VNMcHaHZ26Ux85PbSrHW4jg7izW4h" => Some(CexName::CoinbaseCW4),
|
||||||
|
"D6gCBB3CZEMNbX1PDr3GtZAMhnebEumcgJ2yv8Etv5hF" => Some(CexName::CoinbaseCW5),
|
||||||
|
"3qP77PzrHxSrW1S8dH4Ss1dmpJDHpC6ATVgwy5FmXDEf" => Some(CexName::CoinbaseCW6),
|
||||||
|
"146yGthSmnTPuCo6Zfbmr56YbAyWZ3rzAhRcT7tTF5ha" => Some(CexName::CoinbaseCW7),
|
||||||
|
"GXTrXayxMJUujsRTxYjAbkdbNvs6u2KN89UpG8f6eMAg" => Some(CexName::CoinbaseCW8),
|
||||||
|
"AzAvbCQsXurd2PbGLYcB61tyvE8kLDaZShE1S5Bp3WeS" => Some(CexName::CoinbaseCW9),
|
||||||
|
"4pHKEisSmAr5CSump4dJnTJgG6eugmtieXcUxDBcQcG5" => Some(CexName::CoinbaseCW10),
|
||||||
|
"BmGyWBMEcjJD7JQD1jRJ5vEt7XX2LyVvtxwtTGV4N1bp" => Some(CexName::CoinbaseCW11),
|
||||||
|
"py5jDEUAynTufQHM7P6Tu9M8NUd8JYux7aMcLXcC51q" => Some(CexName::CoinbaseCW12),
|
||||||
|
"is6MTRHEgyFLNTfYcuV4QBWLjrZBfmhVNYR6ccgr8KV" => Some(CexName::OKXHW1),
|
||||||
|
"C68a6RCGLiPskbPYtAcsCjhG8tfTWYcoB4JjCrXFdqyo" => Some(CexName::OKXHW2),
|
||||||
|
"5VCwKtCXgCJ6kit5FybXjvriW3xELsFDhYrPSqtJNmcD" => Some(CexName::OKX),
|
||||||
|
"9un5wqE3q4oCjyrDkwsdD48KteCJitQX5978Vh7KKxHo" => Some(CexName::OKX2),
|
||||||
|
"ASTyfSima4LLAdDgoFGkgqoKowG1LZFDr9fAQrg7iaJZ" => Some(CexName::MEXC1),
|
||||||
|
"5PAhQiYdLBd6SVdjzBQDxUAEFyDdF5ExNPQfcscnPRj5" => Some(CexName::MEXC2),
|
||||||
|
"FWznbcNXWQuHTawe9RxvQ2LdCENssh12dsznf4RiouN5" => Some(CexName::Kraken),
|
||||||
|
"9cNE6KBg2Xmf34FPMMvzDF8yUHMrgLRzBV3vD7b1JnUS" => Some(CexName::KrakenCW),
|
||||||
|
"F7RkX6Y1qTfBqoX5oHoZEgrG1Dpy55UZ3GfWwPbM58nQ" => Some(CexName::KrakenCW2),
|
||||||
|
"3yFwqXBfZY4jBVUafQ1YEXw189y2dN3V5KQq9uzBDy1E" => Some(CexName::Binance8),
|
||||||
|
"2ojv9BAiHUrvsm9gxDe7fJSzbNZSJcxZvf8dqmWGHG8S" => Some(CexName::Binance1),
|
||||||
|
"5tzFkiKscXHK5ZXCGbXZxdw7gTjjD1mBwuoFbhUvuAi9" => Some(CexName::Binance2),
|
||||||
|
"9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM" => Some(CexName::Binance3),
|
||||||
|
"53unSgGWqEWANcPYRF35B2Bgf8BkszUtcccKiXwGGLyr" => Some(CexName::BinanceUSHW),
|
||||||
|
"3gd3dqgtJ4jWfBfLYTX67DALFetjc5iS72sCgRhCkW2u" => Some(CexName::Binance10),
|
||||||
|
"6QJzieMYfp7yr3EdrePaQoG3Ghxs2wM98xSLRu8Xh56U" => Some(CexName::Binance11),
|
||||||
|
"GBrURzmtWujJRTA3Bkvo7ZgWuZYLMMwPCwre7BejJXnK" => Some(CexName::BinanceCW),
|
||||||
|
"4S8C1yrRZmJYPzCqzEVjZYf6qCYWFoF7hWLRzssTCotX" => Some(CexName::BitgetCW),
|
||||||
|
"A77HErqtfN1hLLpvZ9pCtu66FEtM8BveoaKbbMoZ4RiR" => Some(CexName::BitgetExchange),
|
||||||
|
"u6PJ8DtQuPFnfmwHbGFULQ4u4EgjDiyYKjVEsynXq2w" => Some(CexName::Gateio1),
|
||||||
|
"HiRpdAZifEsZGdzQ5Xo5wcnaH3D2Jj9SoNsUzcYNK78J" => Some(CexName::Gateio2),
|
||||||
|
"AC5RDfQFmDS1deWZos921JfqscXdByf8BKHs5ACWjtW2" => Some(CexName::BybitHW),
|
||||||
|
"42brAgAVNzMBP7aaktPvAmBSPEkehnFQejiZc53EpJFd" => Some(CexName::BybitCW),
|
||||||
|
"FxteHmLwG9nk1eL4pjNve3Eub2goGkkz6g6TbvdmW46a" => Some(CexName::BitfinexHW),
|
||||||
|
"FyJBKcfcEBzGN74uNxZ95GxnCxeuJJujQCELpPv14ZfN" => Some(CexName::BitfinexCW),
|
||||||
|
"57vSaRTqN9iXaemgh4AoDsZ63mcaoshfMK8NP3Z5QNbs" => Some(CexName::KuCoin1),
|
||||||
|
"BmFdpraQhkiDQE6SnfG5omcA1VwzqfXrwtNYBwWTymy6" => Some(CexName::KuCoin2),
|
||||||
|
"HVh6wHNBAsG3pq1Bj5oCzRjoWKVogEDHwUHkRz3ekFgt" => Some(CexName::KuCoin3),
|
||||||
|
"DBmae92YTQKLsNzXcPscxiwPqMcz9stQr2prB5ZCAHPd" => Some(CexName::KuCoinCW),
|
||||||
|
"7Ci23i82UMa8RpfVbdMjTytiDi2VoZS8uLyHhZBV2Qy7" => Some(CexName::PoloniexHW),
|
||||||
|
"8s9j5qUtuE9PGA5s7QeAXEh5oc2UGr71pmJXgyiZMHkt" => Some(CexName::LBank),
|
||||||
|
"G9X7F4JzLzbSGMCndiBdWNi5YzZZakmtkdwq7xS3Q3FE" => Some(CexName::StakecomHotWallet),
|
||||||
|
"2snHHreXbpJ7UwZxPe37gnUNf7Wx7wv6UKDSR2JckKuS" => Some(CexName::DeBridgeVault),
|
||||||
|
"Biw4eeaiYYYq6xSqEd7GzdwsrrndxA8mqdxfAtG3PTUU" => Some(CexName::RevolutHotWallet),
|
||||||
|
"HBxZShcE86UMmF93KUM8eWJKqeEXi5cqWCLYLMMhqMYm" => Some(CexName::BitStampHotWallet),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_exchange_address(name: CexName) -> Option<solana_pubkey::Pubkey> {
|
||||||
|
match name {
|
||||||
|
CexName::CoinbaseHW1 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("FpwQQhQQoEaVu3WU2qZMfF1hx48YyfwsLoRgXG83E99Q")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::CoinbaseHW2 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("GJRs4FwHtemZ5ZE9x3FNvJ8TMwitKTh21yxdRPqn7npE")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::CoinbaseHW3 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("D89hHJT5Aqyx1trP6EnGY9jJUB3whgnq3aUvvCqedvzf")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::CoinbaseHW4 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("DPqsobysNf5iA9w7zrQM8HLzCKZEDMkZsWbiidsAt1xo")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::Coinbase1 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("H8sMJSCQxfKiFTCfDR3DUMLPwcRbM61LGFJ8N4dK3WjS")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::Coinbase2 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("2AQdpHJ2JpcEgPiATUXjQxA8QmafFegfQwSLWSprPicm")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::Coinbase4 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("59L2oxymiQQ9Hvhh92nt8Y7nDYjsauFkdb3SybdnsG6h")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::Coinbase5 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("9obNtb5GyUegcs3a1CbBkLuc5hEWynWfJC6gjz5uWQkE")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::CoinbasePrime => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("3vxheE5C46XzK4XftziRhwAf8QAfipD7HXXWj25mgkom")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::CoinbaseCW1 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("CKy3KzEMSL1PQV6Wppggoqi2nGA7teE4L7JipEK89yqj")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::CoinbaseCW2 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("G6zmnfSdG6QJaDWYwbGQ4dpCSUC4gvjfZxYQ4ZharV7C")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::CoinbaseCW3 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("VTvk7sG6QQ28iK3NEKRRD9fvPzk5pKpJL2iwgVqMFcL")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::CoinbaseCW4 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("85cPov8nuRCkJ88VNMcHaHZ26Ux85PbSrHW4jg7izW4h")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::CoinbaseCW5 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("D6gCBB3CZEMNbX1PDr3GtZAMhnebEumcgJ2yv8Etv5hF")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::CoinbaseCW6 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("3qP77PzrHxSrW1S8dH4Ss1dmpJDHpC6ATVgwy5FmXDEf")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::CoinbaseCW7 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("146yGthSmnTPuCo6Zfbmr56YbAyWZ3rzAhRcT7tTF5ha")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::CoinbaseCW8 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("GXTrXayxMJUujsRTxYjAbkdbNvs6u2KN89UpG8f6eMAg")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::CoinbaseCW9 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("AzAvbCQsXurd2PbGLYcB61tyvE8kLDaZShE1S5Bp3WeS")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::CoinbaseCW10 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("4pHKEisSmAr5CSump4dJnTJgG6eugmtieXcUxDBcQcG5")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::CoinbaseCW11 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("BmGyWBMEcjJD7JQD1jRJ5vEt7XX2LyVvtxwtTGV4N1bp")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::CoinbaseCW12 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("py5jDEUAynTufQHM7P6Tu9M8NUd8JYux7aMcLXcC51q")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::OKXHW1 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("is6MTRHEgyFLNTfYcuV4QBWLjrZBfmhVNYR6ccgr8KV")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::OKXHW2 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("C68a6RCGLiPskbPYtAcsCjhG8tfTWYcoB4JjCrXFdqyo")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::OKX => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("5VCwKtCXgCJ6kit5FybXjvriW3xELsFDhYrPSqtJNmcD")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::OKX2 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("9un5wqE3q4oCjyrDkwsdD48KteCJitQX5978Vh7KKxHo")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::MEXC1 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("ASTyfSima4LLAdDgoFGkgqoKowG1LZFDr9fAQrg7iaJZ")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::MEXC2 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("5PAhQiYdLBd6SVdjzBQDxUAEFyDdF5ExNPQfcscnPRj5")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::Kraken => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("FWznbcNXWQuHTawe9RxvQ2LdCENssh12dsznf4RiouN5")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::KrakenCW => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("9cNE6KBg2Xmf34FPMMvzDF8yUHMrgLRzBV3vD7b1JnUS")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::KrakenCW2 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("F7RkX6Y1qTfBqoX5oHoZEgrG1Dpy55UZ3GfWwPbM58nQ")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::Binance8 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("3yFwqXBfZY4jBVUafQ1YEXw189y2dN3V5KQq9uzBDy1E")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::Binance1 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("2ojv9BAiHUrvsm9gxDe7fJSzbNZSJcxZvf8dqmWGHG8S")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::Binance2 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("5tzFkiKscXHK5ZXCGbXZxdw7gTjjD1mBwuoFbhUvuAi9")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::Binance3 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::BinanceUSHW => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("53unSgGWqEWANcPYRF35B2Bgf8BkszUtcccKiXwGGLyr")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::Binance10 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("3gd3dqgtJ4jWfBfLYTX67DALFetjc5iS72sCgRhCkW2u")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::Binance11 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("6QJzieMYfp7yr3EdrePaQoG3Ghxs2wM98xSLRu8Xh56U")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::BinanceCW => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("GBrURzmtWujJRTA3Bkvo7ZgWuZYLMMwPCwre7BejJXnK")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::BitgetCW => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("4S8C1yrRZmJYPzCqzEVjZYf6qCYWFoF7hWLRzssTCotX")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::BitgetExchange => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("A77HErqtfN1hLLpvZ9pCtu66FEtM8BveoaKbbMoZ4RiR")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::Gateio1 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("u6PJ8DtQuPFnfmwHbGFULQ4u4EgjDiyYKjVEsynXq2w")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::Gateio2 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("HiRpdAZifEsZGdzQ5Xo5wcnaH3D2Jj9SoNsUzcYNK78J")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::BybitHW => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("AC5RDfQFmDS1deWZos921JfqscXdByf8BKHs5ACWjtW2")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::BybitCW => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("42brAgAVNzMBP7aaktPvAmBSPEkehnFQejiZc53EpJFd")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::BitfinexHW => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("FxteHmLwG9nk1eL4pjNve3Eub2goGkkz6g6TbvdmW46a")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::BitfinexCW => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("FyJBKcfcEBzGN74uNxZ95GxnCxeuJJujQCELpPv14ZfN")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::KuCoin1 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("57vSaRTqN9iXaemgh4AoDsZ63mcaoshfMK8NP3Z5QNbs")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::KuCoin2 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("BmFdpraQhkiDQE6SnfG5omcA1VwzqfXrwtNYBwWTymy6")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::KuCoin3 => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("HVh6wHNBAsG3pq1Bj5oCzRjoWKVogEDHwUHkRz3ekFgt")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::KuCoinCW => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("DBmae92YTQKLsNzXcPscxiwPqMcz9stQr2prB5ZCAHPd")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::PoloniexHW => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("7Ci23i82UMa8RpfVbdMjTytiDi2VoZS8uLyHhZBV2Qy7")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::LBank => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("8s9j5qUtuE9PGA5s7QeAXEh5oc2UGr71pmJXgyiZMHkt")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::StakecomHotWallet => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("G9X7F4JzLzbSGMCndiBdWNi5YzZZakmtkdwq7xS3Q3FE")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::DeBridgeVault => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("2snHHreXbpJ7UwZxPe37gnUNf7Wx7wv6UKDSR2JckKuS")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::RevolutHotWallet => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("Biw4eeaiYYYq6xSqEd7GzdwsrrndxA8mqdxfAtG3PTUU")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
CexName::BitStampHotWallet => Some(
|
||||||
|
solana_pubkey::Pubkey::from_str("HBxZShcE86UMmF93KUM8eWJKqeEXi5cqWCLYLMMhqMYm")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum CexName {
|
||||||
|
#[serde(rename = "coinbase_hw1")]
|
||||||
|
CoinbaseHW1,
|
||||||
|
#[serde(rename = "coinbase_hw2")]
|
||||||
|
CoinbaseHW2,
|
||||||
|
#[serde(rename = "coinbase_hw3")]
|
||||||
|
CoinbaseHW3,
|
||||||
|
#[serde(rename = "coinbase_hw4")]
|
||||||
|
CoinbaseHW4,
|
||||||
|
#[serde(rename = "coinbase_1")]
|
||||||
|
Coinbase1,
|
||||||
|
#[serde(rename = "coinbase_2")]
|
||||||
|
Coinbase2,
|
||||||
|
#[serde(rename = "coinbase_4")]
|
||||||
|
Coinbase4,
|
||||||
|
#[serde(rename = "coinbase_5")]
|
||||||
|
Coinbase5,
|
||||||
|
#[serde(rename = "coinbase_prime")]
|
||||||
|
CoinbasePrime,
|
||||||
|
#[serde(rename = "coinbase_cw1")]
|
||||||
|
CoinbaseCW1,
|
||||||
|
#[serde(rename = "coinbase_cw2")]
|
||||||
|
CoinbaseCW2,
|
||||||
|
#[serde(rename = "coinbase_cw3")]
|
||||||
|
CoinbaseCW3,
|
||||||
|
#[serde(rename = "coinbase_cw4")]
|
||||||
|
CoinbaseCW4,
|
||||||
|
#[serde(rename = "coinbase_cw5")]
|
||||||
|
CoinbaseCW5,
|
||||||
|
#[serde(rename = "coinbase_cw6")]
|
||||||
|
CoinbaseCW6,
|
||||||
|
#[serde(rename = "coinbase_cw7")]
|
||||||
|
CoinbaseCW7,
|
||||||
|
#[serde(rename = "coinbase_cw8")]
|
||||||
|
CoinbaseCW8,
|
||||||
|
#[serde(rename = "coinbase_cw9")]
|
||||||
|
CoinbaseCW9,
|
||||||
|
#[serde(rename = "coinbase_cw10")]
|
||||||
|
CoinbaseCW10,
|
||||||
|
#[serde(rename = "coinbase_cw11")]
|
||||||
|
CoinbaseCW11,
|
||||||
|
#[serde(rename = "coinbase_cw12")]
|
||||||
|
CoinbaseCW12,
|
||||||
|
#[serde(rename = "okx_hw1")]
|
||||||
|
OKXHW1,
|
||||||
|
#[serde(rename = "okx_hw2")]
|
||||||
|
OKXHW2,
|
||||||
|
#[serde(rename = "okx")]
|
||||||
|
OKX,
|
||||||
|
#[serde(rename = "okx_2")]
|
||||||
|
OKX2,
|
||||||
|
#[serde(rename = "mexc_1")]
|
||||||
|
MEXC1,
|
||||||
|
#[serde(rename = "mexc_2")]
|
||||||
|
MEXC2,
|
||||||
|
#[serde(rename = "kraken")]
|
||||||
|
Kraken,
|
||||||
|
#[serde(rename = "kraken_cw")]
|
||||||
|
KrakenCW,
|
||||||
|
#[serde(rename = "kraken_cw2")]
|
||||||
|
KrakenCW2,
|
||||||
|
#[serde(rename = "binance_8")]
|
||||||
|
Binance8,
|
||||||
|
#[serde(rename = "binance_1")]
|
||||||
|
Binance1,
|
||||||
|
#[serde(rename = "binance_2")]
|
||||||
|
Binance2,
|
||||||
|
#[serde(rename = "binance_3")]
|
||||||
|
Binance3,
|
||||||
|
#[serde(rename = "binance_us_hw")]
|
||||||
|
BinanceUSHW,
|
||||||
|
#[serde(rename = "binance_10")]
|
||||||
|
Binance10,
|
||||||
|
#[serde(rename = "binance_11")]
|
||||||
|
Binance11,
|
||||||
|
#[serde(rename = "binance_cw")]
|
||||||
|
BinanceCW,
|
||||||
|
#[serde(rename = "bitget_cw")]
|
||||||
|
BitgetCW,
|
||||||
|
#[serde(rename = "bitget_exchange")]
|
||||||
|
BitgetExchange,
|
||||||
|
#[serde(rename = "gateio_1")]
|
||||||
|
Gateio1,
|
||||||
|
#[serde(rename = "gateio_2")]
|
||||||
|
Gateio2,
|
||||||
|
#[serde(rename = "bybit_hw")]
|
||||||
|
BybitHW,
|
||||||
|
#[serde(rename = "bybit_cw")]
|
||||||
|
BybitCW,
|
||||||
|
#[serde(rename = "bitfinex_hw")]
|
||||||
|
BitfinexHW,
|
||||||
|
#[serde(rename = "bitfinex_cw")]
|
||||||
|
BitfinexCW,
|
||||||
|
#[serde(rename = "kucoin_1")]
|
||||||
|
KuCoin1,
|
||||||
|
#[serde(rename = "kucoin_2")]
|
||||||
|
KuCoin2,
|
||||||
|
#[serde(rename = "kucoin_3")]
|
||||||
|
KuCoin3,
|
||||||
|
#[serde(rename = "kucoin_cw")]
|
||||||
|
KuCoinCW,
|
||||||
|
#[serde(rename = "poloniex_hw")]
|
||||||
|
PoloniexHW,
|
||||||
|
#[serde(rename = "lbank")]
|
||||||
|
LBank,
|
||||||
|
#[serde(rename = "stakecom_hot_wallet")]
|
||||||
|
StakecomHotWallet,
|
||||||
|
#[serde(rename = "debridge_vault")]
|
||||||
|
DeBridgeVault,
|
||||||
|
#[serde(rename = "revolut_hot_wallet")]
|
||||||
|
RevolutHotWallet,
|
||||||
|
#[serde(rename = "bitstamp_hot_wallet")]
|
||||||
|
BitStampHotWallet,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for CexName {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
CexName::CoinbaseHW1 => write!(f, "coinbase_hw1"),
|
||||||
|
CexName::CoinbaseHW2 => write!(f, "coinbase_hw2"),
|
||||||
|
CexName::CoinbaseHW3 => write!(f, "coinbase_hw3"),
|
||||||
|
CexName::CoinbaseHW4 => write!(f, "coinbase_hw4"),
|
||||||
|
CexName::Coinbase1 => write!(f, "coinbase_1"),
|
||||||
|
CexName::Coinbase2 => write!(f, "coinbase_2"),
|
||||||
|
CexName::Coinbase4 => write!(f, "coinbase_4"),
|
||||||
|
CexName::Coinbase5 => write!(f, "coinbase_5"),
|
||||||
|
CexName::CoinbasePrime => write!(f, "coinbase_prime"),
|
||||||
|
CexName::CoinbaseCW1 => write!(f, "coinbase_cw1"),
|
||||||
|
CexName::CoinbaseCW2 => write!(f, "coinbase_cw2"),
|
||||||
|
CexName::CoinbaseCW3 => write!(f, "coinbase_cw3"),
|
||||||
|
CexName::CoinbaseCW4 => write!(f, "coinbase_cw4"),
|
||||||
|
CexName::CoinbaseCW5 => write!(f, "coinbase_cw5"),
|
||||||
|
CexName::CoinbaseCW6 => write!(f, "coinbase_cw6"),
|
||||||
|
CexName::CoinbaseCW7 => write!(f, "coinbase_cw7"),
|
||||||
|
CexName::CoinbaseCW8 => write!(f, "coinbase_cw8"),
|
||||||
|
CexName::CoinbaseCW9 => write!(f, "coinbase_cw9"),
|
||||||
|
CexName::CoinbaseCW10 => write!(f, "coinbase_cw10"),
|
||||||
|
CexName::CoinbaseCW11 => write!(f, "coinbase_cw11"),
|
||||||
|
CexName::CoinbaseCW12 => write!(f, "coinbase_cw12"),
|
||||||
|
CexName::OKXHW1 => write!(f, "okx_hw1"),
|
||||||
|
CexName::OKXHW2 => write!(f, "okx_hw2"),
|
||||||
|
CexName::OKX => write!(f, "okx"),
|
||||||
|
CexName::OKX2 => write!(f, "okx_2"),
|
||||||
|
CexName::MEXC1 => write!(f, "mexc_1"),
|
||||||
|
CexName::MEXC2 => write!(f, "mexc_2"),
|
||||||
|
CexName::Kraken => write!(f, "kraken"),
|
||||||
|
CexName::KrakenCW => write!(f, "kraken_cw"),
|
||||||
|
CexName::KrakenCW2 => write!(f, "kraken_cw2"),
|
||||||
|
CexName::Binance8 => write!(f, "binance_8"),
|
||||||
|
CexName::Binance1 => write!(f, "binance_1"),
|
||||||
|
CexName::Binance2 => write!(f, "binance_2"),
|
||||||
|
CexName::Binance3 => write!(f, "binance_3"),
|
||||||
|
CexName::BinanceUSHW => write!(f, "binance_us_hw"),
|
||||||
|
CexName::Binance10 => write!(f, "binance_10"),
|
||||||
|
CexName::Binance11 => write!(f, "binance_11"),
|
||||||
|
CexName::BinanceCW => write!(f, "binance_cw"),
|
||||||
|
CexName::BitgetCW => write!(f, "bitget_cw"),
|
||||||
|
CexName::BitgetExchange => write!(f, "bitget_exchange"),
|
||||||
|
CexName::Gateio1 => write!(f, "gateio_1"),
|
||||||
|
CexName::Gateio2 => write!(f, "gateio_2"),
|
||||||
|
CexName::BybitHW => write!(f, "bybit_hw"),
|
||||||
|
CexName::BybitCW => write!(f, "bybit_cw"),
|
||||||
|
CexName::BitfinexHW => write!(f, "bitfinex_hw"),
|
||||||
|
CexName::BitfinexCW => write!(f, "bitfinex_cw"),
|
||||||
|
CexName::KuCoin1 => write!(f, "kucoin_1"),
|
||||||
|
CexName::KuCoin2 => write!(f, "kucoin_2"),
|
||||||
|
CexName::KuCoin3 => write!(f, "kucoin_3"),
|
||||||
|
CexName::KuCoinCW => write!(f, "kucoin_cw"),
|
||||||
|
CexName::PoloniexHW => write!(f, "poloniex_hw"),
|
||||||
|
CexName::LBank => write!(f, "lbank"),
|
||||||
|
CexName::StakecomHotWallet => write!(f, "stakecom_hot_wallet"),
|
||||||
|
CexName::DeBridgeVault => write!(f, "debridge_vault"),
|
||||||
|
CexName::RevolutHotWallet => write!(f, "revolut_hot_wallet"),
|
||||||
|
CexName::BitStampHotWallet => write!(f, "bitstamp_hot_wallet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CexName> for String {
|
||||||
|
fn from(cex: CexName) -> Self {
|
||||||
|
match cex {
|
||||||
|
CexName::CoinbaseHW1 => "coinbase_hw1".to_string(),
|
||||||
|
CexName::CoinbaseHW2 => "coinbase_hw2".to_string(),
|
||||||
|
CexName::CoinbaseHW3 => "coinbase_hw3".to_string(),
|
||||||
|
CexName::CoinbaseHW4 => "coinbase_hw4".to_string(),
|
||||||
|
CexName::Coinbase1 => "coinbase_1".to_string(),
|
||||||
|
CexName::Coinbase2 => "coinbase_2".to_string(),
|
||||||
|
CexName::Coinbase4 => "coinbase_4".to_string(),
|
||||||
|
CexName::Coinbase5 => "coinbase_5".to_string(),
|
||||||
|
CexName::CoinbasePrime => "coinbase_prime".to_string(),
|
||||||
|
CexName::CoinbaseCW1 => "coinbase_cw1".to_string(),
|
||||||
|
CexName::CoinbaseCW2 => "coinbase_cw2".to_string(),
|
||||||
|
CexName::CoinbaseCW3 => "coinbase_cw3".to_string(),
|
||||||
|
CexName::CoinbaseCW4 => "coinbase_cw4".to_string(),
|
||||||
|
CexName::CoinbaseCW5 => "coinbase_cw5".to_string(),
|
||||||
|
CexName::CoinbaseCW6 => "coinbase_cw6".to_string(),
|
||||||
|
CexName::CoinbaseCW7 => "coinbase_cw7".to_string(),
|
||||||
|
CexName::CoinbaseCW8 => "coinbase_cw8".to_string(),
|
||||||
|
CexName::CoinbaseCW9 => "coinbase_cw9".to_string(),
|
||||||
|
CexName::CoinbaseCW10 => "coinbase_cw10".to_string(),
|
||||||
|
CexName::CoinbaseCW11 => "coinbase_cw11".to_string(),
|
||||||
|
CexName::CoinbaseCW12 => "coinbase_cw12".to_string(),
|
||||||
|
CexName::OKXHW1 => "okx_hw1".to_string(),
|
||||||
|
CexName::OKXHW2 => "okx_hw2".to_string(),
|
||||||
|
CexName::OKX => "okx".to_string(),
|
||||||
|
CexName::OKX2 => "okx_2".to_string(),
|
||||||
|
CexName::MEXC1 => "mexc_1".to_string(),
|
||||||
|
CexName::MEXC2 => "mexc_2".to_string(),
|
||||||
|
CexName::Kraken => "kraken".to_string(),
|
||||||
|
CexName::KrakenCW => "kraken_cw".to_string(),
|
||||||
|
CexName::KrakenCW2 => "kraken_cw2".to_string(),
|
||||||
|
CexName::Binance8 => "binance_8".to_string(),
|
||||||
|
CexName::Binance1 => "binance_1".to_string(),
|
||||||
|
CexName::Binance2 => "binance_2".to_string(),
|
||||||
|
CexName::Binance3 => "binance_3".to_string(),
|
||||||
|
CexName::BinanceUSHW => "binance_us_hw".to_string(),
|
||||||
|
CexName::Binance10 => "binance_10".to_string(),
|
||||||
|
CexName::Binance11 => "binance_11".to_string(),
|
||||||
|
CexName::BinanceCW => "binance_cw".to_string(),
|
||||||
|
CexName::BitgetCW => "bitget_cw".to_string(),
|
||||||
|
CexName::BitgetExchange => "bitget_exchange".to_string(),
|
||||||
|
CexName::Gateio1 => "gateio_1".to_string(),
|
||||||
|
CexName::Gateio2 => "gateio_2".to_string(),
|
||||||
|
CexName::BybitHW => "bybit_hw".to_string(),
|
||||||
|
CexName::BybitCW => "bybit_cw".to_string(),
|
||||||
|
CexName::BitfinexHW => "bitfinex_hw".to_string(),
|
||||||
|
CexName::BitfinexCW => "bitfinex_cw".to_string(),
|
||||||
|
CexName::KuCoin1 => "kucoin_1".to_string(),
|
||||||
|
CexName::KuCoin2 => "kucoin_2".to_string(),
|
||||||
|
CexName::KuCoin3 => "kucoin_3".to_string(),
|
||||||
|
CexName::KuCoinCW => "kucoin_cw".to_string(),
|
||||||
|
CexName::PoloniexHW => "poloniex_hw".to_string(),
|
||||||
|
CexName::LBank => "lbank".to_string(),
|
||||||
|
CexName::StakecomHotWallet => "stakecom_hot_wallet".to_string(),
|
||||||
|
CexName::DeBridgeVault => "debridge_vault".to_string(),
|
||||||
|
CexName::RevolutHotWallet => "revolut_hot_wallet".to_string(),
|
||||||
|
CexName::BitStampHotWallet => "bitstamp_hot_wallet".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CexName {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
CexName::CoinbaseHW1 => "coinbase_hw1",
|
||||||
|
CexName::CoinbaseHW2 => "coinbase_hw2",
|
||||||
|
CexName::CoinbaseHW3 => "coinbase_hw3",
|
||||||
|
CexName::CoinbaseHW4 => "coinbase_hw4",
|
||||||
|
CexName::Coinbase1 => "coinbase_1",
|
||||||
|
CexName::Coinbase2 => "coinbase_2",
|
||||||
|
CexName::Coinbase4 => "coinbase_4",
|
||||||
|
CexName::Coinbase5 => "coinbase_5",
|
||||||
|
CexName::CoinbasePrime => "coinbase_prime",
|
||||||
|
CexName::CoinbaseCW1 => "coinbase_cw1",
|
||||||
|
CexName::CoinbaseCW2 => "coinbase_cw2",
|
||||||
|
CexName::CoinbaseCW3 => "coinbase_cw3",
|
||||||
|
CexName::CoinbaseCW4 => "coinbase_cw4",
|
||||||
|
CexName::CoinbaseCW5 => "coinbase_cw5",
|
||||||
|
CexName::CoinbaseCW6 => "coinbase_cw6",
|
||||||
|
CexName::CoinbaseCW7 => "coinbase_cw7",
|
||||||
|
CexName::CoinbaseCW8 => "coinbase_cw8",
|
||||||
|
CexName::CoinbaseCW9 => "coinbase_cw9",
|
||||||
|
CexName::CoinbaseCW10 => "coinbase_cw10",
|
||||||
|
CexName::CoinbaseCW11 => "coinbase_cw11",
|
||||||
|
CexName::CoinbaseCW12 => "coinbase_cw12",
|
||||||
|
CexName::OKXHW1 => "okx_hw1",
|
||||||
|
CexName::OKXHW2 => "okx_hw2",
|
||||||
|
CexName::OKX => "okx",
|
||||||
|
CexName::OKX2 => "okx_2",
|
||||||
|
CexName::MEXC1 => "mexc_1",
|
||||||
|
CexName::MEXC2 => "mexc_2",
|
||||||
|
CexName::Kraken => "kraken",
|
||||||
|
CexName::KrakenCW => "kraken_cw",
|
||||||
|
CexName::KrakenCW2 => "kraken_cw2",
|
||||||
|
CexName::Binance8 => "binance_8",
|
||||||
|
CexName::Binance1 => "binance_1",
|
||||||
|
CexName::Binance2 => "binance_2",
|
||||||
|
CexName::Binance3 => "binance_3",
|
||||||
|
CexName::BinanceUSHW => "binance_us_hw",
|
||||||
|
CexName::Binance10 => "binance_10",
|
||||||
|
CexName::Binance11 => "binance_11",
|
||||||
|
CexName::BinanceCW => "binance_cw",
|
||||||
|
CexName::BitgetCW => "bitget_cw",
|
||||||
|
CexName::BitgetExchange => "bitget_exchange",
|
||||||
|
CexName::Gateio1 => "gateio_1",
|
||||||
|
CexName::Gateio2 => "gateio_2",
|
||||||
|
CexName::BybitHW => "bybit_hw",
|
||||||
|
CexName::BybitCW => "bybit_cw",
|
||||||
|
CexName::BitfinexHW => "bitfinex_hw",
|
||||||
|
CexName::BitfinexCW => "bitfinex_cw",
|
||||||
|
CexName::KuCoin1 => "kucoin_1",
|
||||||
|
CexName::KuCoin2 => "kucoin_2",
|
||||||
|
CexName::KuCoin3 => "kucoin_3",
|
||||||
|
CexName::KuCoinCW => "kucoin_cw",
|
||||||
|
CexName::PoloniexHW => "poloniex_hw",
|
||||||
|
CexName::LBank => "lbank",
|
||||||
|
CexName::StakecomHotWallet => "stakecom_hot_wallet",
|
||||||
|
CexName::DeBridgeVault => "debridge_vault",
|
||||||
|
CexName::RevolutHotWallet => "revolut_hot_wallet",
|
||||||
|
CexName::BitStampHotWallet => "bitstamp_hot_wallet",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
121
src/model/graph.rs
Normal file
121
src/model/graph.rs
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use petgraph::prelude::*;
|
||||||
|
use petgraph::Graph;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use solana_pubkey::Pubkey;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
use crate::model::cex::Cex;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AddressNode {
|
||||||
|
pub address: solana_pubkey::Pubkey,
|
||||||
|
pub total_received: f64,
|
||||||
|
pub total_balance: f64,
|
||||||
|
pub is_cex: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TransactionEdge {
|
||||||
|
pub from: solana_pubkey::Pubkey,
|
||||||
|
pub to: solana_pubkey::Pubkey,
|
||||||
|
pub amount: f64,
|
||||||
|
pub timestamp: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
pub struct CreatorConnectionGraph {
|
||||||
|
graph: Graph<AddressNode, TransactionEdge>,
|
||||||
|
#[serde(skip)]
|
||||||
|
node_indices: HashMap<Pubkey, NodeIndex>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CreatorConnectionGraph {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
graph: Graph::new(),
|
||||||
|
node_indices: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild the node_indices HashMap from the graph (useful after deserialization)
|
||||||
|
pub fn rebuild_indices(&mut self) {
|
||||||
|
self.node_indices.clear();
|
||||||
|
for node_index in self.graph.node_indices() {
|
||||||
|
if let Some(node) = self.graph.node_weight(node_index) {
|
||||||
|
self.node_indices.insert(node.address, node_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure indices are available (rebuild if empty and graph has nodes)
|
||||||
|
fn ensure_indices(&mut self) {
|
||||||
|
if self.node_indices.is_empty() && self.graph.node_count() > 0 {
|
||||||
|
self.rebuild_indices();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_node_count(&self) -> usize {
|
||||||
|
self.graph.node_count()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_edge_count(&self) -> usize {
|
||||||
|
self.graph.edge_count()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all nodes in the graph
|
||||||
|
pub fn get_nodes(&self) -> Vec<AddressNode> {
|
||||||
|
self.graph.node_weights().map(|node| node.clone()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all edges in the graph
|
||||||
|
pub fn get_edges(&self) -> Vec<TransactionEdge> {
|
||||||
|
self.graph.edge_weights().map(|edge| edge.clone()).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread-safe wrapper for the graph
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SharedCreatorConnectionGraph {
|
||||||
|
#[serde(skip)]
|
||||||
|
inner: Arc<RwLock<CreatorConnectionGraph>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SharedCreatorConnectionGraph {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Arc::new(RwLock::new(CreatorConnectionGraph::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_node_count(&self) -> usize {
|
||||||
|
self.inner.read().await.get_node_count()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_edge_count(&self) -> usize {
|
||||||
|
self.inner.read().await.get_edge_count()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clone_graph(&self) -> CreatorConnectionGraph {
|
||||||
|
let mut graph = self.inner.read().await.clone();
|
||||||
|
// Ensure indices are rebuilt after cloning (since they're skipped in serialization)
|
||||||
|
graph.rebuild_indices();
|
||||||
|
graph
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to ensure indices are available (useful after deserialization)
|
||||||
|
pub async fn ensure_indices(&self) {
|
||||||
|
self.inner.write().await.ensure_indices();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CreatorConnectionGraph> for SharedCreatorConnectionGraph {
|
||||||
|
fn from(graph: CreatorConnectionGraph) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Arc::new(RwLock::new(graph)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/model/mod.rs
Normal file
3
src/model/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod cex;
|
||||||
|
pub mod graph;
|
||||||
|
pub mod token;
|
||||||
48
src/model/token.rs
Normal file
48
src/model/token.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
use super::graph::SharedCreatorConnectionGraph;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
// Type for new token created event (matches NewTokenCache from muhafidh)
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct NewTokenCreatedData {
|
||||||
|
pub mint: solana_pubkey::Pubkey,
|
||||||
|
pub bonding_curve: Option<solana_pubkey::Pubkey>,
|
||||||
|
pub name: String,
|
||||||
|
pub symbol: String,
|
||||||
|
pub uri: String,
|
||||||
|
pub creator: solana_pubkey::Pubkey,
|
||||||
|
pub created_at: u64, // Unix timestamp in seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type for token CEX updated event (matches TokenAnalyzedCache from muhafidh)
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TokenCexUpdatedData {
|
||||||
|
pub mint: String, // Mint address as string
|
||||||
|
pub name: String,
|
||||||
|
pub uri: String,
|
||||||
|
pub dev_name: String, // DevName from muhafidh
|
||||||
|
pub creator: String, // Creator address as string
|
||||||
|
pub cex_name: String,
|
||||||
|
pub cex_address: String,
|
||||||
|
pub bonding_curve: String, // Bonding curve address as string
|
||||||
|
pub created_at: u64, // Unix timestamp in seconds
|
||||||
|
pub updated_at: u64, // Unix timestamp in seconds
|
||||||
|
pub node_count: usize,
|
||||||
|
pub edge_count: usize,
|
||||||
|
pub graph: SharedCreatorConnectionGraph,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type for max depth reached event (same structure as TokenAnalyzedCache)
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct MaxDepthReachedData {
|
||||||
|
pub mint: String, // Mint address as string
|
||||||
|
pub name: String,
|
||||||
|
pub uri: String,
|
||||||
|
pub dev_name: String, // DevName from muhafidh
|
||||||
|
pub creator: String, // Creator address as string
|
||||||
|
pub bonding_curve: String, // Bonding curve address as string
|
||||||
|
pub created_at: u64, // Unix timestamp in seconds
|
||||||
|
pub updated_at: u64, // Unix timestamp in seconds
|
||||||
|
pub node_count: usize,
|
||||||
|
pub edge_count: usize,
|
||||||
|
pub graph: SharedCreatorConnectionGraph,
|
||||||
|
}
|
||||||
26
src/storage/mod.rs
Normal file
26
src/storage/mod.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub mod redis;
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::storage::redis::RedisStorage;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct StorageEngine {
|
||||||
|
pub redis: Arc<RedisStorage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StorageEngine {
|
||||||
|
pub async fn new(config: Config) -> Result<Self> {
|
||||||
|
let redis_storage = Arc::new(RedisStorage::new(&config.storage_redis).await.map_err(
|
||||||
|
|e| {
|
||||||
|
error!("failed_to_create_redis_storage: {}", e);
|
||||||
|
e
|
||||||
|
},
|
||||||
|
)?);
|
||||||
|
Ok(Self {
|
||||||
|
redis: redis_storage,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/storage/redis.rs
Normal file
73
src/storage/redis.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
use bb8::Pool;
|
||||||
|
use bb8_redis::RedisConnectionManager;
|
||||||
|
use redis::aio::PubSub;
|
||||||
|
use tracing::{error, info, instrument};
|
||||||
|
|
||||||
|
use crate::config::StorageRedisConfig;
|
||||||
|
use crate::err_with_loc;
|
||||||
|
use crate::error::app::AppError;
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
|
pub type RedisPool = Pool<RedisConnectionManager>;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RedisStorage {
|
||||||
|
pub pool: RedisPool,
|
||||||
|
redis_url: String, // Store the URL to create new connections
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RedisStorage {
|
||||||
|
#[instrument(level = "debug")]
|
||||||
|
pub async fn new(config: &StorageRedisConfig) -> Result<Self> {
|
||||||
|
let redis_url = format!("redis://{}:{}/?protocol=resp3", config.host, config.port);
|
||||||
|
|
||||||
|
let manager = RedisConnectionManager::new(redis_url.clone()).map_err(|e| {
|
||||||
|
error!("failed_to_create_redis_manager: {}", e);
|
||||||
|
err_with_loc!(AppError::Redis(format!(
|
||||||
|
"failed_to_create_redis_manager: {}",
|
||||||
|
e
|
||||||
|
)))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let pool = bb8::Pool::builder()
|
||||||
|
.max_size(config.pool_size)
|
||||||
|
.build(manager)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("failed_to_create_redis_pool: {}", e);
|
||||||
|
err_with_loc!(AppError::Redis(format!(
|
||||||
|
"failed_to_create_redis_pool: {}",
|
||||||
|
e
|
||||||
|
)))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
info!("redis::connection_established");
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
pool,
|
||||||
|
redis_url,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new independent PubSub connection for subscribers
|
||||||
|
/// Each subscriber should use its own connection to avoid blocking
|
||||||
|
pub async fn create_pubsub_connection(&self) -> Result<PubSub> {
|
||||||
|
let client = redis::Client::open(self.redis_url.clone()).map_err(|e| {
|
||||||
|
error!("failed_to_create_redis_client_for_pubsub: {}", e);
|
||||||
|
err_with_loc!(AppError::Redis(format!(
|
||||||
|
"failed_to_create_redis_client_for_pubsub: {}",
|
||||||
|
e
|
||||||
|
)))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let pubsub = client.get_async_pubsub().await.map_err(|e| {
|
||||||
|
error!("failed_to_get_new_pubsub_connection: {}", e);
|
||||||
|
err_with_loc!(AppError::Redis(format!(
|
||||||
|
"failed_to_get_new_pubsub_connection: {}",
|
||||||
|
e
|
||||||
|
)))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(pubsub)
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/task/mod.rs
Normal file
3
src/task/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod shutdown;
|
||||||
|
pub mod subscriber;
|
||||||
|
pub mod ui;
|
||||||
33
src/task/shutdown.rs
Normal file
33
src/task/shutdown.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use tokio::sync::Notify;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ShutdownSignal {
|
||||||
|
pub signal: Arc<Notify>,
|
||||||
|
shutdown_triggered: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShutdownSignal {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
signal: Arc::new(Notify::new()),
|
||||||
|
shutdown_triggered: Arc::new(AtomicBool::new(false)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shutdown(&self) {
|
||||||
|
self.shutdown_triggered.store(true, Ordering::SeqCst);
|
||||||
|
self.signal.notify_waiters();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_shutdown(&self) -> bool {
|
||||||
|
self.shutdown_triggered.load(Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wait_for_shutdown(&self) {
|
||||||
|
self.signal.notified().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
186
src/task/subscriber.rs
Normal file
186
src/task/subscriber.rs
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
use crate::err_with_loc;
|
||||||
|
use crate::error::app::AppError;
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::handler::token::TokenMetadataHandlerOperator;
|
||||||
|
use crate::model::token::{MaxDepthReachedData, NewTokenCreatedData, TokenCexUpdatedData};
|
||||||
|
use crate::slint_ui::{MainWindow, NewTokenUiData};
|
||||||
|
use crate::storage::StorageEngine;
|
||||||
|
use futures_util::StreamExt;
|
||||||
|
use slint::Weak;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
|
// ✅ Simplified subscriber that creates its own PubSub connection
|
||||||
|
pub fn spawn_new_token_subscriber(
|
||||||
|
token_handler: Arc<TokenMetadataHandlerOperator>,
|
||||||
|
db: Arc<StorageEngine>,
|
||||||
|
cancellation_token: CancellationToken,
|
||||||
|
) -> JoinHandle<Result<()>> {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
debug!("new_token_subscriber::starting");
|
||||||
|
|
||||||
|
// ✅ Create dedicated PubSub connection for this subscriber
|
||||||
|
let mut pubsub = db.redis.create_pubsub_connection().await?;
|
||||||
|
|
||||||
|
if let Err(e) = pubsub.subscribe("new_token_created").await {
|
||||||
|
error!("failed_to_subscribe_to_new_token_created: {}", e);
|
||||||
|
return Err(err_with_loc!(AppError::Redis(format!(
|
||||||
|
"failed_to_subscribe_to_new_token_created: {}",
|
||||||
|
e
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a channel for buffering messages - with good capacity for performance
|
||||||
|
let (buffer_tx, mut buffer_rx) = mpsc::channel::<NewTokenCreatedData>(1000);
|
||||||
|
|
||||||
|
info!("new_token_subscriber::subscribed_successfully");
|
||||||
|
let mut msg_stream = pubsub.on_message();
|
||||||
|
let cancellation_token = cancellation_token.clone();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
Some(token) = buffer_rx.recv() => {
|
||||||
|
// ✅ Just call the existing handler - no duplication!
|
||||||
|
if let Err(e) = token_handler.process_new_token(token.clone()).await {
|
||||||
|
error!("failed_to_send_token_to_token_handler::mint::{}::error::{}", token.mint.clone(), e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Some(message) = msg_stream.next() => {
|
||||||
|
if let Ok(msg) = message.get_payload::<String>() {
|
||||||
|
if let Ok(token) = serde_json::from_str::<NewTokenCreatedData>(&msg) {
|
||||||
|
debug!("new_token_received::mint::{}::name::{}::creator::{}",
|
||||||
|
token.mint, token.name, token.creator);
|
||||||
|
if let Err(e) = buffer_tx.try_send(token.clone()) {
|
||||||
|
error!("failed_to_send_token_to_buffer::mint::{}::error::{}", token.mint, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ = cancellation_token.cancelled() => {
|
||||||
|
warn!("new_token_subscriber::shutdown_signal_received");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("new_token_subscriber::ended");
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn_token_cex_updated_subscriber(
|
||||||
|
token_handler: Arc<TokenMetadataHandlerOperator>,
|
||||||
|
db: Arc<StorageEngine>,
|
||||||
|
cancellation_token: CancellationToken,
|
||||||
|
) -> JoinHandle<Result<()>> {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
debug!("token_cex_updated_subscriber::starting");
|
||||||
|
|
||||||
|
// ✅ Create dedicated PubSub connection for this subscriber
|
||||||
|
let mut pubsub = db.redis.create_pubsub_connection().await?;
|
||||||
|
|
||||||
|
if let Err(e) = pubsub.subscribe("token_cex_updated").await {
|
||||||
|
error!("failed_to_subscribe_to_token_cex_updated: {}", e);
|
||||||
|
return Err(err_with_loc!(AppError::Redis(format!(
|
||||||
|
"failed_to_subscribe_to_token_cex_updated: {}",
|
||||||
|
e
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a channel for buffering messages - with good capacity for performance
|
||||||
|
let (buffer_tx, mut buffer_rx) = mpsc::channel::<TokenCexUpdatedData>(1000);
|
||||||
|
|
||||||
|
info!("token_cex_updated_subscriber::subscribed_successfully");
|
||||||
|
let mut msg_stream = pubsub.on_message();
|
||||||
|
let cancellation_token = cancellation_token.clone();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
Some(token) = buffer_rx.recv() => {
|
||||||
|
// ✅ Just call the existing handler - no duplication!
|
||||||
|
if let Err(e) = token_handler.process_cex_updated(token.clone()).await {
|
||||||
|
error!("failed_to_send_cex_updated_to_token_handler::mint::{}::error::{}", token.mint, e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Some(message) = msg_stream.next() => {
|
||||||
|
if let Ok(msg) = message.get_payload::<String>() {
|
||||||
|
if let Ok(token) = serde_json::from_str::<TokenCexUpdatedData>(&msg) {
|
||||||
|
debug!("token_cex_updated_received::mint::{}::name::{}::cex::{}",
|
||||||
|
token.mint, token.name, token.cex_name);
|
||||||
|
if let Err(e) = buffer_tx.try_send(token.clone()) {
|
||||||
|
error!("failed_to_send_cex_updated_to_buffer::mint::{}::error::{}", token.mint, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ = cancellation_token.cancelled() => {
|
||||||
|
warn!("token_cex_updated_subscriber::shutdown_signal_received");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("token_cex_updated_subscriber::ended");
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn_max_depth_reached_subscriber(
|
||||||
|
token_handler: Arc<TokenMetadataHandlerOperator>,
|
||||||
|
db: Arc<StorageEngine>,
|
||||||
|
cancellation_token: CancellationToken,
|
||||||
|
) -> JoinHandle<Result<()>> {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
debug!("max_depth_reached_subscriber::starting");
|
||||||
|
|
||||||
|
// ✅ Create dedicated PubSub connection for this subscriber
|
||||||
|
let mut pubsub = db.redis.create_pubsub_connection().await?;
|
||||||
|
|
||||||
|
if let Err(e) = pubsub.subscribe("max_depth_reached").await {
|
||||||
|
error!("failed_to_subscribe_to_max_depth_reached: {}", e);
|
||||||
|
return Err(err_with_loc!(AppError::Redis(format!(
|
||||||
|
"failed_to_subscribe_to_max_depth_reached: {}",
|
||||||
|
e
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a channel for buffering messages - with good capacity for performance
|
||||||
|
let (buffer_tx, mut buffer_rx) = mpsc::channel::<MaxDepthReachedData>(1000);
|
||||||
|
|
||||||
|
info!("max_depth_reached_subscriber::subscribed_successfully");
|
||||||
|
let mut msg_stream = pubsub.on_message();
|
||||||
|
let cancellation_token = cancellation_token.clone();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
Some(token) = buffer_rx.recv() => {
|
||||||
|
// ✅ Just call the existing handler - no duplication!
|
||||||
|
if let Err(e) = token_handler.process_max_depth_reached(token.clone()).await {
|
||||||
|
error!("failed_to_send_max_depth_reached_to_token_handler::mint::{}::error::{}", token.mint, e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Some(message) = msg_stream.next() => {
|
||||||
|
if let Ok(msg) = message.get_payload::<String>() {
|
||||||
|
if let Ok(token) = serde_json::from_str::<MaxDepthReachedData>(&msg) {
|
||||||
|
debug!("max_depth_reached_received::mint::{}::name::{}::nodes::{}::edges::{}",
|
||||||
|
token.mint, token.name, token.node_count, token.edge_count);
|
||||||
|
if let Err(e) = buffer_tx.try_send(token.clone()) {
|
||||||
|
error!("failed_to_send_max_depth_reached_to_buffer::mint::{}::error::{}", token.mint, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ = cancellation_token.cancelled() => {
|
||||||
|
warn!("max_depth_reached_subscriber::shutdown_signal_received");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("max_depth_reached_subscriber::ended");
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
147
src/task/ui.rs
Normal file
147
src/task/ui.rs
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
use i_slint_backend_winit::WinitWindowAccessor;
|
||||||
|
use slint::Weak;
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::get_current_unix_timestamp;
|
||||||
|
use crate::handler::ui::SlintHandlerUiOperator;
|
||||||
|
use crate::slint_ui::*;
|
||||||
|
use crate::{err_with_loc, error::app::AppError};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tracing::{error, info};
|
||||||
|
|
||||||
|
pub fn spawn_ui_task(
|
||||||
|
slint_handler: Arc<SlintHandlerUiOperator>,
|
||||||
|
ui_weak: Weak<MainWindow>,
|
||||||
|
cancellation_token: CancellationToken,
|
||||||
|
shutdown_tx: tokio::sync::mpsc::Sender<()>,
|
||||||
|
) -> JoinHandle<Result<()>> {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
ui_weak
|
||||||
|
.clone()
|
||||||
|
.upgrade_in_event_loop(move |ui| {
|
||||||
|
// Window dragging
|
||||||
|
ui.on_start_drag_window({
|
||||||
|
let ui_weak = ui.as_weak();
|
||||||
|
move || {
|
||||||
|
let _ = ui_weak.upgrade_in_event_loop(|ui| {
|
||||||
|
let _ = ui.window().with_winit_window(
|
||||||
|
|winit_window: &winit::window::Window| {
|
||||||
|
let _ = winit_window.drag_window();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Window minimize
|
||||||
|
ui.on_minimize_window({
|
||||||
|
let ui_weak = ui_weak.clone();
|
||||||
|
move || {
|
||||||
|
let _ = ui_weak.upgrade_in_event_loop(|ui| {
|
||||||
|
let _ = ui.window().with_winit_window(
|
||||||
|
|winit_window: &winit::window::Window| {
|
||||||
|
winit_window.set_minimized(true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Window maximize/restore
|
||||||
|
ui.on_maximize_window({
|
||||||
|
let ui_weak = ui_weak.clone();
|
||||||
|
move || {
|
||||||
|
let _ = ui_weak.upgrade_in_event_loop(|ui| {
|
||||||
|
let _ = ui.window().with_winit_window(
|
||||||
|
|winit_window: &winit::window::Window| {
|
||||||
|
let is_maximized = winit_window.is_maximized();
|
||||||
|
winit_window.set_maximized(!is_maximized);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Theme toggle
|
||||||
|
ui.on_theme_toggle_clicked({
|
||||||
|
let ui_weak = ui_weak.clone();
|
||||||
|
move || {
|
||||||
|
let _ = ui_weak.upgrade_in_event_loop(|ui| {
|
||||||
|
ui.invoke_toggle_theme();
|
||||||
|
info!("Theme toggled");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Navigation callback
|
||||||
|
ui.on_navigation_changed({
|
||||||
|
move |page| {
|
||||||
|
info!("Navigation changed to: {}", page);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let new_tokens_handler = slint_handler.clone();
|
||||||
|
ui.on_clear_new_tokens(move || {
|
||||||
|
let handler = new_tokens_handler.clone();
|
||||||
|
handler.clear_new_tokens();
|
||||||
|
});
|
||||||
|
|
||||||
|
let cex_tokens_handler = slint_handler.clone();
|
||||||
|
ui.on_clear_cex_tokens(move || {
|
||||||
|
let handler = cex_tokens_handler.clone();
|
||||||
|
handler.clear_cex_tokens();
|
||||||
|
});
|
||||||
|
|
||||||
|
let analysis_tokens_handler = slint_handler.clone();
|
||||||
|
ui.on_clear_analysis_tokens(move || {
|
||||||
|
let handler = analysis_tokens_handler.clone();
|
||||||
|
handler.clear_analysis_tokens();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Other callbacks (placeholder implementations)
|
||||||
|
ui.on_logout_clicked({
|
||||||
|
move || {
|
||||||
|
info!("Logout clicked");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Window close - with proper task cleanup
|
||||||
|
ui.on_close_window({
|
||||||
|
let ui_weak = ui_weak.clone();
|
||||||
|
let shutdown_tx = shutdown_tx.clone();
|
||||||
|
let cancellation_token = cancellation_token.clone();
|
||||||
|
move || {
|
||||||
|
let _ = ui_weak.upgrade_in_event_loop({
|
||||||
|
let shutdown_tx = shutdown_tx.clone();
|
||||||
|
let cancellation_token = cancellation_token.clone();
|
||||||
|
move |ui| {
|
||||||
|
info!("close_window::shutting_down_all_tasks");
|
||||||
|
|
||||||
|
// Signal shutdown to all subscriber tasks
|
||||||
|
cancellation_token.cancel();
|
||||||
|
|
||||||
|
// Hide window immediately for responsive UI
|
||||||
|
let _ = ui.window().hide();
|
||||||
|
|
||||||
|
// Send shutdown signal
|
||||||
|
let _ = shutdown_tx.try_send(());
|
||||||
|
|
||||||
|
info!("close_window::all_tasks_cleaned_up");
|
||||||
|
|
||||||
|
// Quit the event loop after cleanup
|
||||||
|
let _ = slint::quit_event_loop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("failed_to_setup_ui_callbacks: {}", e);
|
||||||
|
err_with_loc!(AppError::Slint(format!(
|
||||||
|
"failed_to_setup_ui_callbacks: {}",
|
||||||
|
e
|
||||||
|
)))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
73
src/tracing/filter.rs
Normal file
73
src/tracing/filter.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
use tracing::Level;
|
||||||
|
use tracing::Metadata;
|
||||||
|
use tracing_subscriber::layer::Context;
|
||||||
|
use tracing_subscriber::layer::Filter;
|
||||||
|
use tracing_subscriber::registry::LookupSpan;
|
||||||
|
|
||||||
|
// Custom filter for exact debug level matching
|
||||||
|
pub struct DebugOnlyFilter;
|
||||||
|
|
||||||
|
impl<S> Filter<S> for DebugOnlyFilter
|
||||||
|
where
|
||||||
|
S: tracing::Subscriber + for<'lookup> LookupSpan<'lookup>,
|
||||||
|
{
|
||||||
|
fn enabled(&self, meta: &Metadata<'_>, _ctx: &Context<'_, S>) -> bool {
|
||||||
|
let target = meta.target();
|
||||||
|
meta.level() == &Level::DEBUG && target.starts_with("ziya")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom filter for error and warn levels
|
||||||
|
pub struct ErrorWarnFilter;
|
||||||
|
|
||||||
|
impl<S> Filter<S> for ErrorWarnFilter
|
||||||
|
where
|
||||||
|
S: tracing::Subscriber + for<'lookup> LookupSpan<'lookup>,
|
||||||
|
{
|
||||||
|
fn enabled(&self, meta: &Metadata<'_>, _ctx: &Context<'_, S>) -> bool {
|
||||||
|
let target = meta.target();
|
||||||
|
(meta.level() == &Level::ERROR || meta.level() == &Level::WARN)
|
||||||
|
&& target.starts_with("ziya")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom filter for info levels
|
||||||
|
#[cfg(feature = "dev")]
|
||||||
|
pub struct InfoOnlyFilter;
|
||||||
|
|
||||||
|
#[cfg(feature = "dev")]
|
||||||
|
impl<S> Filter<S> for InfoOnlyFilter
|
||||||
|
where
|
||||||
|
S: tracing::Subscriber + for<'lookup> LookupSpan<'lookup>,
|
||||||
|
{
|
||||||
|
fn enabled(&self, meta: &Metadata<'_>, _ctx: &Context<'_, S>) -> bool {
|
||||||
|
let target = meta.target();
|
||||||
|
meta.level() == &Level::INFO && target.starts_with("ziya")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom filter for error levels
|
||||||
|
pub struct ErrorOnlyFilter;
|
||||||
|
|
||||||
|
impl<S> Filter<S> for ErrorOnlyFilter
|
||||||
|
where
|
||||||
|
S: tracing::Subscriber + for<'lookup> LookupSpan<'lookup>,
|
||||||
|
{
|
||||||
|
fn enabled(&self, meta: &Metadata<'_>, _ctx: &Context<'_, S>) -> bool {
|
||||||
|
let target = meta.target();
|
||||||
|
meta.level() == &Level::ERROR && target.starts_with("ziya")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom filter for warn levels
|
||||||
|
pub struct WarnOnlyFilter;
|
||||||
|
|
||||||
|
impl<S> Filter<S> for WarnOnlyFilter
|
||||||
|
where
|
||||||
|
S: tracing::Subscriber + for<'lookup> LookupSpan<'lookup>,
|
||||||
|
{
|
||||||
|
fn enabled(&self, meta: &Metadata<'_>, _ctx: &Context<'_, S>) -> bool {
|
||||||
|
let target = meta.target();
|
||||||
|
meta.level() == &Level::WARN && target.starts_with("ziya")
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/tracing/format.rs
Normal file
60
src/tracing/format.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
use tracing::Event;
|
||||||
|
use tracing_subscriber::fmt::format::Writer;
|
||||||
|
use tracing_subscriber::fmt::FmtContext;
|
||||||
|
use tracing_subscriber::fmt::FormatEvent;
|
||||||
|
use tracing_subscriber::fmt::FormatFields;
|
||||||
|
use tracing_subscriber::registry::LookupSpan;
|
||||||
|
|
||||||
|
pub struct ZiyaFormat {
|
||||||
|
pub app_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement Clone for ZiyaFormat
|
||||||
|
impl Clone for ZiyaFormat {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
app_name: self.app_name.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, N> FormatEvent<S, N> for ZiyaFormat
|
||||||
|
where
|
||||||
|
S: tracing::Subscriber + for<'lookup> LookupSpan<'lookup>,
|
||||||
|
N: for<'writer> FormatFields<'writer> + 'static,
|
||||||
|
{
|
||||||
|
fn format_event(
|
||||||
|
&self,
|
||||||
|
ctx: &FmtContext<'_, S, N>,
|
||||||
|
mut writer: Writer<'_>,
|
||||||
|
event: &Event<'_>,
|
||||||
|
) -> std::fmt::Result {
|
||||||
|
// To get the message, we need to format the fields with a special visitor
|
||||||
|
let metadata = event.metadata();
|
||||||
|
let file = metadata.file().unwrap_or("unknown");
|
||||||
|
let line = metadata.line().unwrap_or(0);
|
||||||
|
|
||||||
|
if file == "unknown" && !cfg!(feature = "deep-trace") {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let utc_timestamp = chrono::Utc::now();
|
||||||
|
let jakarta_timestamp = utc_timestamp.with_timezone(&chrono_tz::Asia::Jakarta);
|
||||||
|
let timestamp = jakarta_timestamp.format("%Y-%m-%d %H:%M:%S");
|
||||||
|
|
||||||
|
write!(
|
||||||
|
writer,
|
||||||
|
"{} {}::{}::{}::{}::",
|
||||||
|
metadata.level(),
|
||||||
|
timestamp,
|
||||||
|
self.app_name,
|
||||||
|
file,
|
||||||
|
line
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Format the actual message
|
||||||
|
ctx.field_format().format_fields(writer.by_ref(), event)?;
|
||||||
|
|
||||||
|
writeln!(writer)
|
||||||
|
}
|
||||||
|
}
|
||||||
187
src/tracing/mod.rs
Normal file
187
src/tracing/mod.rs
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
pub mod filter;
|
||||||
|
pub mod format;
|
||||||
|
|
||||||
|
pub use tracing::*;
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use self::filter::DebugOnlyFilter;
|
||||||
|
use self::filter::ErrorOnlyFilter;
|
||||||
|
use self::filter::ErrorWarnFilter;
|
||||||
|
#[cfg(feature = "dev")]
|
||||||
|
use self::filter::InfoOnlyFilter;
|
||||||
|
use self::format::ZiyaFormat;
|
||||||
|
|
||||||
|
use tracing_appender::rolling::RollingFileAppender;
|
||||||
|
use tracing_appender::rolling::Rotation;
|
||||||
|
use tracing_subscriber::prelude::*;
|
||||||
|
use tracing_subscriber::Layer;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::err_with_loc;
|
||||||
|
use crate::error::app::AppError;
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
|
pub async fn setup_tracing(config: Config, app_name: &str) -> Result<()> {
|
||||||
|
// Get logging config
|
||||||
|
let logging_config = config.logging;
|
||||||
|
|
||||||
|
// Base logs directory
|
||||||
|
let base_logs_dir = Path::new(&logging_config.directory);
|
||||||
|
|
||||||
|
// Create logs directories if they don't exist
|
||||||
|
let logs_dirs = [
|
||||||
|
base_logs_dir,
|
||||||
|
&base_logs_dir.join("debug"),
|
||||||
|
&base_logs_dir.join("error"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for dir in &logs_dirs {
|
||||||
|
if !dir.exists() {
|
||||||
|
std::fs::create_dir_all(dir).map_err(|e| {
|
||||||
|
err_with_loc!(AppError::Config(format!(
|
||||||
|
"Failed to create logs directory {}: {}",
|
||||||
|
dir.display(),
|
||||||
|
e
|
||||||
|
)))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create file appenders for each log level
|
||||||
|
#[cfg(feature = "dev")]
|
||||||
|
let info_appender =
|
||||||
|
RollingFileAppender::new(Rotation::DAILY, base_logs_dir, format!("{}.log", app_name));
|
||||||
|
|
||||||
|
let debug_appender = RollingFileAppender::new(
|
||||||
|
Rotation::DAILY,
|
||||||
|
base_logs_dir.join("debug"),
|
||||||
|
format!("{}.log", app_name),
|
||||||
|
);
|
||||||
|
|
||||||
|
let error_appender = RollingFileAppender::new(
|
||||||
|
Rotation::DAILY,
|
||||||
|
base_logs_dir.join("error"),
|
||||||
|
format!("{}.log", app_name),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create non-blocking writers
|
||||||
|
#[cfg(feature = "dev")]
|
||||||
|
let (non_blocking_info, info_guard) = tracing_appender::non_blocking(info_appender);
|
||||||
|
let (non_blocking_debug, debug_guard) = tracing_appender::non_blocking(debug_appender);
|
||||||
|
let (non_blocking_error, error_guard) = tracing_appender::non_blocking(error_appender);
|
||||||
|
|
||||||
|
// Store the guards in statics to keep them alive
|
||||||
|
#[cfg(feature = "dev")]
|
||||||
|
static mut INFO_GUARD: Option<tracing_appender::non_blocking::WorkerGuard> = None;
|
||||||
|
static mut DEBUG_GUARD: Option<tracing_appender::non_blocking::WorkerGuard> = None;
|
||||||
|
static mut ERROR_GUARD: Option<tracing_appender::non_blocking::WorkerGuard> = None;
|
||||||
|
|
||||||
|
// Create the custom format for all outputs
|
||||||
|
let format = ZiyaFormat {
|
||||||
|
app_name: app_name.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set up the registry with all outputs
|
||||||
|
let subscriber = tracing_subscriber::registry()
|
||||||
|
// DEBUG log file - debug only using custom filter
|
||||||
|
.with(
|
||||||
|
tracing_subscriber::fmt::Layer::default()
|
||||||
|
.with_ansi(false)
|
||||||
|
.with_file(true)
|
||||||
|
.with_line_number(true)
|
||||||
|
.with_target(false)
|
||||||
|
.event_format(format.clone())
|
||||||
|
.with_writer(non_blocking_debug)
|
||||||
|
.with_filter(DebugOnlyFilter),
|
||||||
|
)
|
||||||
|
// ERROR log file - warn and error only
|
||||||
|
.with(
|
||||||
|
tracing_subscriber::fmt::Layer::default()
|
||||||
|
.with_ansi(false)
|
||||||
|
.with_file(true)
|
||||||
|
.with_line_number(true)
|
||||||
|
.with_target(false)
|
||||||
|
.event_format(format.clone())
|
||||||
|
.with_writer(non_blocking_error)
|
||||||
|
.with_filter(ErrorWarnFilter),
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(feature = "prod")]
|
||||||
|
let subscriber = subscriber
|
||||||
|
// Terminal output with custom ZiyaFormat - Error only in production
|
||||||
|
.with(
|
||||||
|
tracing_subscriber::fmt::Layer::default()
|
||||||
|
.with_ansi(true)
|
||||||
|
.with_file(true)
|
||||||
|
.with_line_number(true)
|
||||||
|
.with_target(false)
|
||||||
|
.event_format(format.clone())
|
||||||
|
.with_filter(ErrorOnlyFilter),
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(feature = "dev")]
|
||||||
|
let subscriber = subscriber
|
||||||
|
// Terminal output with custom ZiyaFormat - INFO and above in development
|
||||||
|
.with(
|
||||||
|
tracing_subscriber::fmt::Layer::default()
|
||||||
|
.with_ansi(true)
|
||||||
|
.with_file(true)
|
||||||
|
.with_line_number(true)
|
||||||
|
.with_target(false)
|
||||||
|
.event_format(format.clone())
|
||||||
|
.with_filter(InfoOnlyFilter),
|
||||||
|
)
|
||||||
|
// INFO log file - info and above
|
||||||
|
.with(
|
||||||
|
tracing_subscriber::fmt::Layer::default()
|
||||||
|
.with_ansi(false)
|
||||||
|
.with_file(true)
|
||||||
|
.with_line_number(true)
|
||||||
|
.with_target(false)
|
||||||
|
.event_format(format.clone())
|
||||||
|
.with_writer(non_blocking_info)
|
||||||
|
.with_filter(InfoOnlyFilter),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set the subscriber as the global default
|
||||||
|
match tracing::subscriber::set_global_default(subscriber) {
|
||||||
|
Ok(_) => {
|
||||||
|
// Store the guards to keep the loggers alive
|
||||||
|
#[cfg(feature = "dev")]
|
||||||
|
unsafe {
|
||||||
|
INFO_GUARD = Some(info_guard);
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
DEBUG_GUARD = Some(debug_guard);
|
||||||
|
ERROR_GUARD = Some(error_guard);
|
||||||
|
}
|
||||||
|
tracing::info!(
|
||||||
|
"{}_logging_started::info_logs::{}\\{}.log",
|
||||||
|
app_name,
|
||||||
|
base_logs_dir.display(),
|
||||||
|
app_name
|
||||||
|
);
|
||||||
|
tracing::info!(
|
||||||
|
"{}_logging_started::debug_logs::{}\\debug\\{}.log",
|
||||||
|
app_name,
|
||||||
|
base_logs_dir.display(),
|
||||||
|
app_name
|
||||||
|
);
|
||||||
|
tracing::info!(
|
||||||
|
"{}_logging_started::error_logs::{}\\error\\{}.log",
|
||||||
|
app_name,
|
||||||
|
base_logs_dir.display(),
|
||||||
|
app_name
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("failed_to_setup_tracing: {}", e);
|
||||||
|
Err(err_with_loc!(AppError::Config(format!(
|
||||||
|
"Failed to setup tracing: {}",
|
||||||
|
e
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
test-ipfs.js
18
test-ipfs.js
|
|
@ -1,18 +0,0 @@
|
||||||
// Simple test to verify IPFS functionality
|
|
||||||
import { fetchTokenMetadata } from './app/utils/ipfs.js';
|
|
||||||
|
|
||||||
async function testIpfs() {
|
|
||||||
console.info('Testing IPFS fetch...');
|
|
||||||
|
|
||||||
// Test with a common IPFS URI format
|
|
||||||
const testUri = 'https://ipfs.io/ipfs/QmPFELY2WMF7KRcpegQxjLqiFGD5AL6bGA9cYB6bE7WVd9';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await fetchTokenMetadata(testUri);
|
|
||||||
console.info('Result:', result);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testIpfs();
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "./.nuxt/tsconfig.json",
|
|
||||||
"include": [
|
|
||||||
"app/**/*",
|
|
||||||
"electron/**/*",
|
|
||||||
"types/**/*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
32
types/electron.d.ts
vendored
32
types/electron.d.ts
vendored
|
|
@ -1,32 +0,0 @@
|
||||||
export interface RedisMessage {
|
|
||||||
channel: string;
|
|
||||||
data: unknown;
|
|
||||||
timestamp: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IElectronAPI {
|
|
||||||
// Window controls
|
|
||||||
minimizeWindow: () => Promise<void>;
|
|
||||||
maximizeWindow: () => Promise<void>;
|
|
||||||
closeWindow: () => Promise<void>;
|
|
||||||
isMaximized: () => Promise<boolean>;
|
|
||||||
|
|
||||||
// Window state listeners
|
|
||||||
onMaximizeChange: (callback: (event: unknown, maximized: boolean) => void) => void;
|
|
||||||
removeMaximizeListener: (callback: (event: unknown, maximized: boolean) => void) => void;
|
|
||||||
|
|
||||||
// External links
|
|
||||||
openExternal: (url: string) => Promise<void>;
|
|
||||||
|
|
||||||
// Redis data subscription
|
|
||||||
onRedisData: (callback: (data: RedisMessage) => void) => void;
|
|
||||||
removeRedisDataListener: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
electronAPI: IElectronAPI;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { };
|
|
||||||
1
types/forge.d.ts
vendored
1
types/forge.d.ts
vendored
|
|
@ -1 +0,0 @@
|
||||||
/// <reference types="@electron-forge/plugin-vite/forge-vite-env" />
|
|
||||||
3
types/nuxt.d.ts
vendored
3
types/nuxt.d.ts
vendored
|
|
@ -1,3 +0,0 @@
|
||||||
/// <reference types="nuxt" />
|
|
||||||
|
|
||||||
export { };
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
// Type for new token created event (from NewTokenCache in muhafidh/src/storage/redis/model.rs)
|
|
||||||
export interface NewTokenCreatedData {
|
|
||||||
mint: number[]; // 32-byte array from Rust Pubkey
|
|
||||||
bonding_curve?: number[]; // 32-byte array from Rust Pubkey
|
|
||||||
name: string;
|
|
||||||
symbol: string;
|
|
||||||
uri: string;
|
|
||||||
creator: number[]; // 32-byte array from Rust Pubkey
|
|
||||||
created_at: number; // Unix timestamp in seconds
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type for token CEX updated event (from creator.rs line 133-144)
|
|
||||||
export interface TokenCexUpdatedData {
|
|
||||||
mint: string; // Mint address as string
|
|
||||||
name: string;
|
|
||||||
uri: string;
|
|
||||||
dev_name: string;
|
|
||||||
cex_name: string;
|
|
||||||
cex_address: string;
|
|
||||||
creator: string; // Creator address as string
|
|
||||||
created_at: number; // Unix timestamp in seconds
|
|
||||||
updated_at: number; // Unix timestamp in seconds
|
|
||||||
node_count: number;
|
|
||||||
edge_count: number;
|
|
||||||
graph: unknown; // Connection graph data
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type for max depth reached event (from creator.rs line 133-144)
|
|
||||||
export interface MaxDepthReachedData {
|
|
||||||
mint: string; // Mint address as string
|
|
||||||
name: string;
|
|
||||||
uri: string;
|
|
||||||
dev_name: string;
|
|
||||||
cex_name: string;
|
|
||||||
cex_address: string;
|
|
||||||
creator: string; // Creator address as string
|
|
||||||
bonding_curve: string; // Bonding curve address as string
|
|
||||||
created_at: number; // Unix timestamp in seconds (now consistent with backend fix)
|
|
||||||
updated_at: number; // Unix timestamp in seconds (now consistent with backend fix)
|
|
||||||
node_count: number;
|
|
||||||
edge_count: number;
|
|
||||||
graph: unknown; // Connection graph data
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redis message wrapper
|
|
||||||
export interface RedisMessage<T = unknown> {
|
|
||||||
channel: string;
|
|
||||||
data: T;
|
|
||||||
timestamp: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// IPFS metadata structure for token URIs
|
|
||||||
export interface TokenMetadata {
|
|
||||||
name?: string;
|
|
||||||
symbol?: string;
|
|
||||||
description?: string;
|
|
||||||
image?: string;
|
|
||||||
external_url?: string;
|
|
||||||
attributes?: Array<{
|
|
||||||
trait_type: string;
|
|
||||||
value: string | number;
|
|
||||||
}>;
|
|
||||||
properties?: {
|
|
||||||
files?: Array<{
|
|
||||||
uri: string;
|
|
||||||
type: string;
|
|
||||||
}>;
|
|
||||||
category?: string;
|
|
||||||
};
|
|
||||||
// Social links that might be in the metadata
|
|
||||||
twitter?: string;
|
|
||||||
telegram?: string;
|
|
||||||
website?: string;
|
|
||||||
discord?: string;
|
|
||||||
}
|
|
||||||
268
ui/app/index.slint
Normal file
268
ui/app/index.slint
Normal file
|
|
@ -0,0 +1,268 @@
|
||||||
|
// App Layer - Main Application Entry Point
|
||||||
|
// This is the root of the application following Feature-Sliced Design
|
||||||
|
|
||||||
|
// Import std-widgets for Palette
|
||||||
|
import { Palette } from "std-widgets.slint";
|
||||||
|
import { NewTokenUiData, CexUpdatedUiData, MaxDepthReachedUiData } from "../shared/types/token.slint";
|
||||||
|
|
||||||
|
// Import widgets (standalone UI blocks)
|
||||||
|
import { NavigationWidget } from "../widgets/navigation/index.slint";
|
||||||
|
import { TitleBar } from "../widgets/window-controls/ui/title-bar.slint";
|
||||||
|
|
||||||
|
// Import pages
|
||||||
|
import { Dashboard } from "../pages/dashboard/index.slint";
|
||||||
|
import { HuntingGroundPage } from "../pages/hunting-ground/index.slint";
|
||||||
|
import { TradingPage } from "../pages/trading/index.slint";
|
||||||
|
import { PortfolioPage } from "../pages/portfolio/index.slint";
|
||||||
|
import { MarketsPage } from "../pages/markets/index.slint";
|
||||||
|
import { LoginView } from "../pages/auth/index.slint";
|
||||||
|
import { LoadingView } from "../shared/ui/index.slint";
|
||||||
|
|
||||||
|
export component App inherits Window {
|
||||||
|
title: "Ziya - One Stop Shop for Your Trading Habit";
|
||||||
|
min-width: 1280px;
|
||||||
|
min-height: 720px;
|
||||||
|
|
||||||
|
// Disable default background
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
// Theme state
|
||||||
|
in-out property <bool> is-dark-mode: false;
|
||||||
|
|
||||||
|
// Application state management
|
||||||
|
in-out property <string> app-state: "loading"; // "loading", "login", "authenticated"
|
||||||
|
in-out property <bool> is-authenticated: false;
|
||||||
|
in-out property <string> user-email: "";
|
||||||
|
in-out property <string> sidebar-state: "full"; // "full", "icon-only", or "hidden"
|
||||||
|
|
||||||
|
// Loading state
|
||||||
|
in-out property <bool> is-loading: true;
|
||||||
|
in-out property <bool> has-connection-error: false;
|
||||||
|
in-out property <string> loading-status: "Initializing your trading environment...";
|
||||||
|
|
||||||
|
// Navigation state
|
||||||
|
in-out property <string> current-page: "Dashboard";
|
||||||
|
in-out property <string> user-initials: "JD";
|
||||||
|
|
||||||
|
// Token data
|
||||||
|
in-out property <[NewTokenUiData]> new-tokens: [];
|
||||||
|
in-out property <[CexUpdatedUiData]> cex-tokens: [];
|
||||||
|
in-out property <[MaxDepthReachedUiData]> analysis-tokens: [];
|
||||||
|
in-out property <int> current-time: 0;
|
||||||
|
|
||||||
|
// Callbacks for application state
|
||||||
|
callback health-check-completed(bool); // true if healthy, false if error
|
||||||
|
callback retry-health-check();
|
||||||
|
callback login-attempt(string, string);
|
||||||
|
callback logout-requested();
|
||||||
|
callback authenticate-user(string);
|
||||||
|
|
||||||
|
// Callbacks for window controls
|
||||||
|
callback start-drag-window();
|
||||||
|
callback minimize-window();
|
||||||
|
callback maximize-window();
|
||||||
|
callback close-window();
|
||||||
|
callback theme-toggle-clicked();
|
||||||
|
|
||||||
|
// Callbacks for navigation
|
||||||
|
callback navigation-changed(string);
|
||||||
|
callback toggle-sidebar();
|
||||||
|
|
||||||
|
// Callbacks for hunting ground
|
||||||
|
callback refresh-hunting-ground();
|
||||||
|
callback clear-column(string);
|
||||||
|
|
||||||
|
// Other callbacks
|
||||||
|
callback logout-clicked();
|
||||||
|
callback buy-clicked();
|
||||||
|
callback sell-clicked();
|
||||||
|
callback clear-new-tokens();
|
||||||
|
callback clear-cex-tokens();
|
||||||
|
callback clear-analysis-tokens();
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
background: Palette.background;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
// Apply overflow hidden to prevent scrolling
|
||||||
|
clip: true;
|
||||||
|
|
||||||
|
// Loading Screen
|
||||||
|
if app-state == "loading": LoadingView {
|
||||||
|
is-loading: root.is-loading;
|
||||||
|
has-error: root.has-connection-error;
|
||||||
|
status-text: root.loading-status;
|
||||||
|
|
||||||
|
retry-connection => {
|
||||||
|
root.retry-health-check();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login Screen
|
||||||
|
if app-state == "login": VerticalLayout {
|
||||||
|
spacing: 0px;
|
||||||
|
|
||||||
|
// Title Bar (fixed height)
|
||||||
|
TitleBar {
|
||||||
|
height: 40px;
|
||||||
|
is-dark-theme: root.is-dark-mode;
|
||||||
|
minimize-window => {
|
||||||
|
root.minimize-window();
|
||||||
|
}
|
||||||
|
maximize-window => {
|
||||||
|
root.maximize-window();
|
||||||
|
}
|
||||||
|
close-window => {
|
||||||
|
root.close-window();
|
||||||
|
}
|
||||||
|
toggle-theme => {
|
||||||
|
root.is-dark-mode = !root.is-dark-mode;
|
||||||
|
if (root.is-dark-mode) {
|
||||||
|
Palette.color-scheme = ColorScheme.dark;
|
||||||
|
} else {
|
||||||
|
Palette.color-scheme = ColorScheme.light;
|
||||||
|
}
|
||||||
|
root.theme-toggle-clicked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LoginView {
|
||||||
|
login-clicked(email, password) => {
|
||||||
|
root.login-attempt(email, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate-to-dashboard => {
|
||||||
|
// Demo mode - skip login
|
||||||
|
root.authenticate-user("demo@ziya.trading");
|
||||||
|
}
|
||||||
|
|
||||||
|
back-to-loading => {
|
||||||
|
root.app-state = "loading";
|
||||||
|
root.retry-health-check();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main Application (Authenticated State)
|
||||||
|
if app-state == "authenticated": VerticalLayout {
|
||||||
|
spacing: 0px;
|
||||||
|
// Title Bar (fixed height)
|
||||||
|
TitleBar {
|
||||||
|
height: 40px;
|
||||||
|
is-dark-theme: root.is-dark-mode;
|
||||||
|
minimize-window => {
|
||||||
|
root.minimize-window();
|
||||||
|
}
|
||||||
|
maximize-window => {
|
||||||
|
root.maximize-window();
|
||||||
|
}
|
||||||
|
close-window => {
|
||||||
|
root.close-window();
|
||||||
|
}
|
||||||
|
toggle-theme => {
|
||||||
|
root.is-dark-mode = !root.is-dark-mode;
|
||||||
|
if (root.is-dark-mode) {
|
||||||
|
Palette.color-scheme = ColorScheme.dark;
|
||||||
|
} else {
|
||||||
|
Palette.color-scheme = ColorScheme.light;
|
||||||
|
}
|
||||||
|
root.theme-toggle-clicked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main Content Area (stretches to fill remaining space)
|
||||||
|
HorizontalLayout {
|
||||||
|
spacing: 0px;
|
||||||
|
|
||||||
|
// Navigation Sidebar (different states)
|
||||||
|
if sidebar-state == "full": NavigationWidget {
|
||||||
|
width: 280px;
|
||||||
|
current-page: root.current-page;
|
||||||
|
user-initials: root.user-initials;
|
||||||
|
sidebar-state: root.sidebar-state;
|
||||||
|
navigation-changed(page) => {
|
||||||
|
root.current-page = page;
|
||||||
|
root.navigation-changed(page);
|
||||||
|
}
|
||||||
|
logout-clicked => {
|
||||||
|
root.logout-requested();
|
||||||
|
}
|
||||||
|
toggle-sidebar => {
|
||||||
|
root.sidebar-state = "icon-only";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sidebar-state == "icon-only": NavigationWidget {
|
||||||
|
width: 80px;
|
||||||
|
current-page: root.current-page;
|
||||||
|
user-initials: root.user-initials;
|
||||||
|
sidebar-state: root.sidebar-state;
|
||||||
|
navigation-changed(page) => {
|
||||||
|
root.current-page = page;
|
||||||
|
root.navigation-changed(page);
|
||||||
|
}
|
||||||
|
logout-clicked => {
|
||||||
|
root.logout-requested();
|
||||||
|
}
|
||||||
|
toggle-sidebar => {
|
||||||
|
root.sidebar-state = "full";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page Content (stretches to fill remaining space)
|
||||||
|
Rectangle {
|
||||||
|
background: Palette.background;
|
||||||
|
|
||||||
|
// Content container with proper centering
|
||||||
|
VerticalLayout {
|
||||||
|
alignment: stretch;
|
||||||
|
|
||||||
|
// Route to different pages
|
||||||
|
if current-page == "Dashboard": Dashboard {
|
||||||
|
user-initials: root.user-initials;
|
||||||
|
logout => {
|
||||||
|
root.logout-requested();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if current-page == "Hunting Ground": HuntingGroundPage {
|
||||||
|
new-tokens: root.new-tokens;
|
||||||
|
cex-tokens: root.cex-tokens;
|
||||||
|
analysis-tokens: root.analysis-tokens;
|
||||||
|
current-time: root.current-time;
|
||||||
|
|
||||||
|
clear-new-tokens => {
|
||||||
|
root.clear-new-tokens();
|
||||||
|
}
|
||||||
|
|
||||||
|
clear-cex-tokens => {
|
||||||
|
root.clear-cex-tokens();
|
||||||
|
}
|
||||||
|
|
||||||
|
clear-analysis-tokens => {
|
||||||
|
root.clear-analysis-tokens();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if current-page == "Trading": TradingPage {
|
||||||
|
buy-clicked => {
|
||||||
|
root.buy-clicked();
|
||||||
|
}
|
||||||
|
sell-clicked => {
|
||||||
|
root.sell-clicked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if current-page == "Portfolio": PortfolioPage {
|
||||||
|
}
|
||||||
|
|
||||||
|
if current-page == "Markets": MarketsPage {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
2
ui/entities/token/index.slint
Normal file
2
ui/entities/token/index.slint
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
// Token Entity Public API
|
||||||
|
export { TokenListView } from "ui/token-list.slint";
|
||||||
181
ui/entities/token/ui/token-list.slint
Normal file
181
ui/entities/token/ui/token-list.slint
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
import { Button, VerticalBox, HorizontalBox, LineEdit, ScrollView } from "std-widgets.slint";
|
||||||
|
|
||||||
|
// Token data structure
|
||||||
|
export struct TokenData {
|
||||||
|
name: string,
|
||||||
|
symbol: string,
|
||||||
|
price: string,
|
||||||
|
market-cap: string,
|
||||||
|
volume: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token list component
|
||||||
|
export component TokenListView {
|
||||||
|
in-out property <color> primary-color: #2563eb;
|
||||||
|
in-out property <color> background-color: #f8fafc;
|
||||||
|
in-out property <color> card-background: #ffffff;
|
||||||
|
in-out property <color> text-color: #1e293b;
|
||||||
|
in-out property <color> border-color: #e2e8f0;
|
||||||
|
in-out property <string> search-query: "";
|
||||||
|
in-out property <[TokenData]> tokens: [];
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
callback search-changed(string);
|
||||||
|
callback token-selected(TokenData);
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
background: background-color;
|
||||||
|
|
||||||
|
VerticalBox {
|
||||||
|
spacing: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
// Header section
|
||||||
|
VerticalBox {
|
||||||
|
spacing: 15px;
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Token Explorer";
|
||||||
|
font-size: 24px;
|
||||||
|
color: text-color;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search bar
|
||||||
|
LineEdit {
|
||||||
|
placeholder-text: "Search tokens...";
|
||||||
|
text <=> search-query;
|
||||||
|
height: 45px;
|
||||||
|
edited => {
|
||||||
|
search-changed(search-query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token list
|
||||||
|
ScrollView {
|
||||||
|
viewport-height: 400px;
|
||||||
|
|
||||||
|
VerticalBox {
|
||||||
|
spacing: 10px;
|
||||||
|
|
||||||
|
for token in tokens: Rectangle {
|
||||||
|
height: 80px;
|
||||||
|
background: card-background;
|
||||||
|
border-radius: 12px;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: border-color;
|
||||||
|
drop-shadow-blur: 5px;
|
||||||
|
drop-shadow-color: #00000008;
|
||||||
|
|
||||||
|
HorizontalBox {
|
||||||
|
alignment: stretch;
|
||||||
|
spacing: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
|
||||||
|
// Token icon placeholder
|
||||||
|
Rectangle {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 25px;
|
||||||
|
background: primary-color.with-alpha(0.1);
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: token.symbol.to-uppercase();
|
||||||
|
color: primary-color;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token info
|
||||||
|
VerticalBox {
|
||||||
|
alignment: start;
|
||||||
|
spacing: 5px;
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: token.name;
|
||||||
|
font-size: 16px;
|
||||||
|
color: text-color;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: token.symbol.to-uppercase();
|
||||||
|
font-size: 12px;
|
||||||
|
color: text-color;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Price info
|
||||||
|
VerticalBox {
|
||||||
|
alignment: end;
|
||||||
|
spacing: 5px;
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "$" + token.price;
|
||||||
|
font-size: 16px;
|
||||||
|
color: text-color;
|
||||||
|
font-weight: 600;
|
||||||
|
horizontal-alignment: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalBox {
|
||||||
|
alignment: end;
|
||||||
|
spacing: 10px;
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "MC: $" + token.market-cap;
|
||||||
|
font-size: 10px;
|
||||||
|
color: text-color;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Vol: $" + token.volume;
|
||||||
|
font-size: 10px;
|
||||||
|
color: text-color;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TouchArea {
|
||||||
|
clicked => {
|
||||||
|
token-selected(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading state or empty state
|
||||||
|
if tokens.length == 0: Rectangle {
|
||||||
|
height: 200px;
|
||||||
|
background: card-background;
|
||||||
|
border-radius: 12px;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: border-color;
|
||||||
|
|
||||||
|
VerticalBox {
|
||||||
|
alignment: center;
|
||||||
|
spacing: 10px;
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: search-query == "" ? "🔍" : "😔";
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: search-query == "" ? "Loading tokens..." : "No tokens found";
|
||||||
|
color: text-color;
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
187
ui/index.slint
Normal file
187
ui/index.slint
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
// Main Entry Point - Following Feature-Sliced Design
|
||||||
|
// This is the root file that main.rs imports
|
||||||
|
// Following the pattern from moonlogs: index -> app -> pages/widgets/entities/shared
|
||||||
|
|
||||||
|
import { App } from "app/index.slint";
|
||||||
|
import { Palette } from "std-widgets.slint";
|
||||||
|
import { NewTokenUiData, CexUpdatedUiData, MaxDepthReachedUiData } from "shared/types/token.slint";
|
||||||
|
|
||||||
|
export component MainWindow inherits Window {
|
||||||
|
// Window properties
|
||||||
|
preferred-width: 1280px;
|
||||||
|
preferred-height: 1024px;
|
||||||
|
min-width: 1080px;
|
||||||
|
min-height: 800px;
|
||||||
|
no-frame: true;
|
||||||
|
background: Palette.background;
|
||||||
|
|
||||||
|
// Theme state
|
||||||
|
in-out property <bool> is-dark-mode: true;
|
||||||
|
|
||||||
|
// Application state management
|
||||||
|
in-out property <string> app-state: "loading"; // "loading", "login", "authenticated"
|
||||||
|
in-out property <bool> is-authenticated: false;
|
||||||
|
in-out property <string> user-email: "";
|
||||||
|
in-out property <string> sidebar-state: "full"; // "full", "icon-only", or "hidden"
|
||||||
|
|
||||||
|
// Loading state
|
||||||
|
in-out property <bool> is-loading: true;
|
||||||
|
in-out property <bool> has-connection-error: false;
|
||||||
|
in-out property <string> loading-status: "Initializing your trading environment...";
|
||||||
|
|
||||||
|
// Navigation state
|
||||||
|
in-out property <string> current-page: "Dashboard";
|
||||||
|
in-out property <string> user-initials: "JD";
|
||||||
|
|
||||||
|
// Hunting ground properties - using correct types
|
||||||
|
in-out property <[NewTokenUiData]> new-tokens: [];
|
||||||
|
in-out property <[CexUpdatedUiData]> cex-tokens: [];
|
||||||
|
in-out property <[MaxDepthReachedUiData]> analysis-tokens: [];
|
||||||
|
in-out property <int> current-time: 0;
|
||||||
|
|
||||||
|
// Callbacks for application state
|
||||||
|
callback health-check-completed(bool); // true if healthy, false if error
|
||||||
|
callback retry-health-check();
|
||||||
|
callback login-attempt(string, string);
|
||||||
|
callback logout-requested();
|
||||||
|
callback authenticate-user(string);
|
||||||
|
|
||||||
|
// Callbacks for window controls
|
||||||
|
callback start-drag-window();
|
||||||
|
callback minimize-window();
|
||||||
|
callback maximize-window();
|
||||||
|
callback close-window();
|
||||||
|
callback theme-toggle-clicked();
|
||||||
|
|
||||||
|
// Callbacks for navigation
|
||||||
|
callback navigation-changed(string);
|
||||||
|
callback toggle-sidebar();
|
||||||
|
|
||||||
|
// Callbacks for hunting ground
|
||||||
|
callback refresh-hunting-ground();
|
||||||
|
callback clear-column(string);
|
||||||
|
|
||||||
|
// Other callbacks
|
||||||
|
callback logout-clicked();
|
||||||
|
callback buy-clicked();
|
||||||
|
callback sell-clicked();
|
||||||
|
callback clear-new-tokens();
|
||||||
|
callback clear-cex-tokens();
|
||||||
|
callback clear-analysis-tokens();
|
||||||
|
|
||||||
|
// Public function that can be called from Rust to toggle theme
|
||||||
|
public function toggle_theme() {
|
||||||
|
root.is-dark-mode = !root.is-dark-mode;
|
||||||
|
if (root.is-dark-mode) {
|
||||||
|
Palette.color-scheme = ColorScheme.dark;
|
||||||
|
} else {
|
||||||
|
Palette.color-scheme = ColorScheme.light;
|
||||||
|
}
|
||||||
|
debug("Theme toggled from Rust. New state: " + (root.is-dark-mode ? "dark" : "light"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TouchArea {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
moved => {
|
||||||
|
if (self.pressed) {
|
||||||
|
start-drag-window();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// App component handles all the UI following FSD layers
|
||||||
|
App {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
// Theme state
|
||||||
|
is-dark-mode: root.is-dark-mode;
|
||||||
|
|
||||||
|
// Application state management
|
||||||
|
app-state: root.app-state;
|
||||||
|
is-authenticated: root.is-authenticated;
|
||||||
|
user-email: root.user-email;
|
||||||
|
sidebar-state: root.sidebar-state;
|
||||||
|
|
||||||
|
// Loading state
|
||||||
|
is-loading: root.is-loading;
|
||||||
|
has-connection-error: root.has-connection-error;
|
||||||
|
loading-status: root.loading-status;
|
||||||
|
|
||||||
|
// Navigation state
|
||||||
|
current-page: root.current-page;
|
||||||
|
user-initials: root.user-initials;
|
||||||
|
|
||||||
|
// Hunting ground properties
|
||||||
|
new-tokens: root.new-tokens;
|
||||||
|
cex-tokens: root.cex-tokens;
|
||||||
|
analysis-tokens: root.analysis-tokens;
|
||||||
|
current-time: root.current-time;
|
||||||
|
|
||||||
|
// Forward all callbacks to main.rs
|
||||||
|
health-check-completed(healthy) => {
|
||||||
|
root.health-check-completed(healthy);
|
||||||
|
}
|
||||||
|
retry-health-check => {
|
||||||
|
root.retry-health-check();
|
||||||
|
}
|
||||||
|
login-attempt(email, password) => {
|
||||||
|
root.login-attempt(email, password);
|
||||||
|
}
|
||||||
|
logout-requested => {
|
||||||
|
root.logout-requested();
|
||||||
|
}
|
||||||
|
authenticate-user(email) => {
|
||||||
|
root.authenticate-user(email);
|
||||||
|
}
|
||||||
|
navigation-changed(page) => {
|
||||||
|
root.current-page = page;
|
||||||
|
root.navigation-changed(page);
|
||||||
|
}
|
||||||
|
toggle-sidebar => {
|
||||||
|
if (root.sidebar-state == "full") {
|
||||||
|
root.sidebar-state = "icon-only";
|
||||||
|
} else {
|
||||||
|
root.sidebar-state = "full";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
theme-toggle-clicked => {
|
||||||
|
root.toggle_theme();
|
||||||
|
}
|
||||||
|
logout-clicked => {
|
||||||
|
root.logout-clicked();
|
||||||
|
}
|
||||||
|
buy-clicked => {
|
||||||
|
root.buy-clicked();
|
||||||
|
}
|
||||||
|
sell-clicked => {
|
||||||
|
root.sell-clicked();
|
||||||
|
}
|
||||||
|
start-drag-window => {
|
||||||
|
root.start-drag-window();
|
||||||
|
}
|
||||||
|
minimize-window => {
|
||||||
|
root.minimize-window();
|
||||||
|
}
|
||||||
|
maximize-window => {
|
||||||
|
root.maximize-window();
|
||||||
|
}
|
||||||
|
close-window => {
|
||||||
|
root.close-window();
|
||||||
|
}
|
||||||
|
refresh-hunting-ground => {
|
||||||
|
root.refresh-hunting-ground();
|
||||||
|
}
|
||||||
|
clear-new-tokens => {
|
||||||
|
root.clear-new-tokens();
|
||||||
|
}
|
||||||
|
clear-cex-tokens => {
|
||||||
|
root.clear-cex-tokens();
|
||||||
|
}
|
||||||
|
clear-analysis-tokens => {
|
||||||
|
root.clear-analysis-tokens();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
2
ui/pages/auth/index.slint
Normal file
2
ui/pages/auth/index.slint
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
// Auth Page Public API
|
||||||
|
export { LoginView } from "ui/login-page.slint";
|
||||||
137
ui/pages/auth/ui/login-page.slint
Normal file
137
ui/pages/auth/ui/login-page.slint
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
import { Palette, Button, VerticalBox, HorizontalBox, LineEdit } from "std-widgets.slint";
|
||||||
|
import { TitleBar } from "../../../widgets/window-controls/ui/title-bar.slint";
|
||||||
|
|
||||||
|
// Login component with branded interface
|
||||||
|
export component LoginView {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
in-out property <bool> is-dark-mode: true;
|
||||||
|
|
||||||
|
// Define the input properties for passing data to parent
|
||||||
|
callback login-clicked(string, string);
|
||||||
|
callback navigate-to-dashboard();
|
||||||
|
callback back-to-loading();
|
||||||
|
callback minimize-window();
|
||||||
|
callback maximize-window();
|
||||||
|
callback close-window();
|
||||||
|
callback theme-toggle-clicked();
|
||||||
|
|
||||||
|
VerticalLayout {
|
||||||
|
alignment: center;
|
||||||
|
spacing: 32px;
|
||||||
|
padding: 40px;
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: Palette.background;
|
||||||
|
|
||||||
|
// Center container
|
||||||
|
Rectangle {
|
||||||
|
width: 400px;
|
||||||
|
height: 500px;
|
||||||
|
|
||||||
|
background: Palette.alternate-background;
|
||||||
|
border-radius: 16px;
|
||||||
|
drop-shadow-blur: 24px;
|
||||||
|
drop-shadow-color: #000000.with-alpha(0.1);
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: Palette.border;
|
||||||
|
|
||||||
|
VerticalLayout {
|
||||||
|
alignment: center;
|
||||||
|
spacing: 24px;
|
||||||
|
padding: 32px;
|
||||||
|
|
||||||
|
// Logo/Icon
|
||||||
|
Rectangle {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 40px;
|
||||||
|
background: Palette.accent-background;
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "🚀";
|
||||||
|
font-size: 40px;
|
||||||
|
horizontal-alignment: center;
|
||||||
|
vertical-alignment: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title
|
||||||
|
Text {
|
||||||
|
text: "Ziya";
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: Palette.alternate-foreground;
|
||||||
|
horizontal-alignment: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subtitle
|
||||||
|
Text {
|
||||||
|
text: "Your one-stop trading platform";
|
||||||
|
font-size: 16px;
|
||||||
|
color: Palette.alternate-foreground;
|
||||||
|
horizontal-alignment: center;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form container
|
||||||
|
VerticalLayout {
|
||||||
|
spacing: 16px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
// Email field
|
||||||
|
LineEdit {
|
||||||
|
placeholder-text: "Enter your email";
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password field
|
||||||
|
LineEdit {
|
||||||
|
placeholder-text: "Enter your password";
|
||||||
|
input-type: password;
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login button
|
||||||
|
Button {
|
||||||
|
text: "Sign In";
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
clicked => {
|
||||||
|
root.login-clicked("demo@ziya.trading", "password123");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Demo button for development
|
||||||
|
Button {
|
||||||
|
text: "Continue to Demo";
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
clicked => {
|
||||||
|
root.navigate-to-dashboard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back to connection check
|
||||||
|
Text {
|
||||||
|
text: "← Back to Connection Check";
|
||||||
|
font-size: 14px;
|
||||||
|
color: Palette.accent-background;
|
||||||
|
horizontal-alignment: center;
|
||||||
|
|
||||||
|
TouchArea {
|
||||||
|
clicked => {
|
||||||
|
root.back-to-loading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
ui/pages/dashboard/index.slint
Normal file
2
ui/pages/dashboard/index.slint
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
// Dashboard Page Public API
|
||||||
|
export { Dashboard } from "ui/dashboard-page.slint";
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue