diff --git a/apps/desktop-wallet/pnpm-lock.yaml b/apps/desktop-wallet/pnpm-lock.yaml new file mode 100644 index 0000000..10d034b --- /dev/null +++ b/apps/desktop-wallet/pnpm-lock.yaml @@ -0,0 +1,1919 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tauri-apps/api': + specifier: ^2.0.0 + version: 2.9.1 + '@tauri-apps/plugin-clipboard-manager': + specifier: ^2.0.0 + version: 2.3.2 + '@tauri-apps/plugin-dialog': + specifier: ^2.0.0 + version: 2.6.0 + '@tauri-apps/plugin-fs': + specifier: ^2.0.0 + version: 2.4.5 + '@tauri-apps/plugin-notification': + specifier: ^2.0.0 + version: 2.3.3 + '@tauri-apps/plugin-process': + specifier: ^2.0.0 + version: 2.3.1 + '@tauri-apps/plugin-shell': + specifier: ^2.0.0 + version: 2.3.4 + '@tauri-apps/plugin-store': + specifier: ^2.0.0 + version: 2.4.2 + '@tauri-apps/plugin-updater': + specifier: ^2.0.0 + version: 2.9.0 + clsx: + specifier: ^2.1.0 + version: 2.1.1 + lucide-react: + specifier: ^0.303.0 + version: 0.303.0(react@18.3.1) + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + react-router-dom: + specifier: ^6.21.0 + version: 6.30.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tailwind-merge: + specifier: ^2.2.0 + version: 2.6.1 + zustand: + specifier: ^4.4.7 + version: 4.5.7(@types/react@18.3.27)(react@18.3.1) + devDependencies: + '@tauri-apps/cli': + specifier: ^2.0.0 + version: 2.9.6 + '@types/react': + specifier: ^18.2.45 + version: 18.3.27 + '@types/react-dom': + specifier: ^18.2.18 + version: 18.3.7(@types/react@18.3.27) + '@vitejs/plugin-react': + specifier: ^4.2.1 + version: 4.7.0(vite@5.4.21) + autoprefixer: + specifier: ^10.4.16 + version: 10.4.24(postcss@8.5.6) + postcss: + specifier: ^8.4.32 + version: 8.5.6 + tailwindcss: + specifier: ^3.4.0 + version: 3.4.19 + typescript: + specifier: ^5.3.3 + version: 5.9.3 + vite: + specifier: ^5.0.10 + version: 5.4.21 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.0': + resolution: {integrity: sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@remix-run/router@1.23.2': + resolution: {integrity: sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==} + engines: {node: '>=14.0.0'} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.57.1': + resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.57.1': + resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.57.1': + resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.57.1': + resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.57.1': + resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.57.1': + resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.57.1': + resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.57.1': + resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.57.1': + resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.57.1': + resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.57.1': + resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.57.1': + resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.57.1': + resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.57.1': + resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==} + cpu: [x64] + os: [win32] + + '@tauri-apps/api@2.9.1': + resolution: {integrity: sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==} + + '@tauri-apps/cli-darwin-arm64@2.9.6': + resolution: {integrity: sha512-gf5no6N9FCk1qMrti4lfwP77JHP5haASZgVbBgpZG7BUepB3fhiLCXGUK8LvuOjP36HivXewjg72LTnPDScnQQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tauri-apps/cli-darwin-x64@2.9.6': + resolution: {integrity: sha512-oWh74WmqbERwwrwcueJyY6HYhgCksUc6NT7WKeXyrlY/FPmNgdyQAgcLuTSkhRFuQ6zh4Np1HZpOqCTpeZBDcw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tauri-apps/cli-linux-arm-gnueabihf@2.9.6': + resolution: {integrity: sha512-/zde3bFroFsNXOHN204DC2qUxAcAanUjVXXSdEGmhwMUZeAQalNj5cz2Qli2elsRjKN/hVbZOJj0gQ5zaYUjSg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tauri-apps/cli-linux-arm64-gnu@2.9.6': + resolution: {integrity: sha512-pvbljdhp9VOo4RnID5ywSxgBs7qiylTPlK56cTk7InR3kYSTJKYMqv/4Q/4rGo/mG8cVppesKIeBMH42fw6wjg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tauri-apps/cli-linux-arm64-musl@2.9.6': + resolution: {integrity: sha512-02TKUndpodXBCR0oP//6dZWGYcc22Upf2eP27NvC6z0DIqvkBBFziQUcvi2n6SrwTRL0yGgQjkm9K5NIn8s6jw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tauri-apps/cli-linux-riscv64-gnu@2.9.6': + resolution: {integrity: sha512-fmp1hnulbqzl1GkXl4aTX9fV+ubHw2LqlLH1PE3BxZ11EQk+l/TmiEongjnxF0ie4kV8DQfDNJ1KGiIdWe1GvQ==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + + '@tauri-apps/cli-linux-x64-gnu@2.9.6': + resolution: {integrity: sha512-vY0le8ad2KaV1PJr+jCd8fUF9VOjwwQP/uBuTJvhvKTloEwxYA/kAjKK9OpIslGA9m/zcnSo74czI6bBrm2sYA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tauri-apps/cli-linux-x64-musl@2.9.6': + resolution: {integrity: sha512-TOEuB8YCFZTWVDzsO2yW0+zGcoMiPPwcUgdnW1ODnmgfwccpnihDRoks+ABT1e3fHb1ol8QQWsHSCovb3o2ENQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tauri-apps/cli-win32-arm64-msvc@2.9.6': + resolution: {integrity: sha512-ujmDGMRc4qRLAnj8nNG26Rlz9klJ0I0jmZs2BPpmNNf0gM/rcVHhqbEkAaHPTBVIrtUdf7bGvQAD2pyIiUrBHQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tauri-apps/cli-win32-ia32-msvc@2.9.6': + resolution: {integrity: sha512-S4pT0yAJgFX8QRCyKA1iKjZ9Q/oPjCZf66A/VlG5Yw54Nnr88J1uBpmenINbXxzyhduWrIXBaUbEY1K80ZbpMg==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@tauri-apps/cli-win32-x64-msvc@2.9.6': + resolution: {integrity: sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tauri-apps/cli@2.9.6': + resolution: {integrity: sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw==} + engines: {node: '>= 10'} + hasBin: true + + '@tauri-apps/plugin-clipboard-manager@2.3.2': + resolution: {integrity: sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==} + + '@tauri-apps/plugin-dialog@2.6.0': + resolution: {integrity: sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==} + + '@tauri-apps/plugin-fs@2.4.5': + resolution: {integrity: sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA==} + + '@tauri-apps/plugin-notification@2.3.3': + resolution: {integrity: sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==} + + '@tauri-apps/plugin-process@2.3.1': + resolution: {integrity: sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==} + + '@tauri-apps/plugin-shell@2.3.4': + resolution: {integrity: sha512-ktsRWf8wHLD17aZEyqE8c5x98eNAuTizR1FSX475zQ4TxaiJnhwksLygQz+AGwckJL5bfEP13nWrlTNQJUpKpA==} + + '@tauri-apps/plugin-store@2.4.2': + resolution: {integrity: sha512-0ClHS50Oq9HEvLPhNzTNFxbWVOqoAp3dRvtewQBeqfIQ0z5m3JRnOISIn2ZVPCrQC0MyGyhTS9DWhHjpigQE7A==} + + '@tauri-apps/plugin-updater@2.9.0': + resolution: {integrity: sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.27': + resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + autoprefixer@10.4.24: + resolution: {integrity: sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + baseline-browser-mapping@2.9.19: + resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} + hasBin: true + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001766: + resolution: {integrity: sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + electron-to-chromium@1.5.283: + resolution: {integrity: sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@0.303.0: + resolution: {integrity: sha512-B0B9T3dLEFBYPCUlnUS1mvAhW1craSbF9HO+JfBjAtpFUJ7gMIqmEwNSclikY3RiN2OnCkj/V1ReAQpaHae8Bg==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-router-dom@6.30.3: + resolution: {integrity: sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + react-router@6.30.3: + resolution: {integrity: sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.57.1: + resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwind-merge@2.6.1: + resolution: {integrity: sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==} + + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.0': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.0 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@remix-run/router@1.23.2': {} + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.57.1': + optional: true + + '@rollup/rollup-android-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-x64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.57.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.57.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.57.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.57.1': + optional: true + + '@tauri-apps/api@2.9.1': {} + + '@tauri-apps/cli-darwin-arm64@2.9.6': + optional: true + + '@tauri-apps/cli-darwin-x64@2.9.6': + optional: true + + '@tauri-apps/cli-linux-arm-gnueabihf@2.9.6': + optional: true + + '@tauri-apps/cli-linux-arm64-gnu@2.9.6': + optional: true + + '@tauri-apps/cli-linux-arm64-musl@2.9.6': + optional: true + + '@tauri-apps/cli-linux-riscv64-gnu@2.9.6': + optional: true + + '@tauri-apps/cli-linux-x64-gnu@2.9.6': + optional: true + + '@tauri-apps/cli-linux-x64-musl@2.9.6': + optional: true + + '@tauri-apps/cli-win32-arm64-msvc@2.9.6': + optional: true + + '@tauri-apps/cli-win32-ia32-msvc@2.9.6': + optional: true + + '@tauri-apps/cli-win32-x64-msvc@2.9.6': + optional: true + + '@tauri-apps/cli@2.9.6': + optionalDependencies: + '@tauri-apps/cli-darwin-arm64': 2.9.6 + '@tauri-apps/cli-darwin-x64': 2.9.6 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.9.6 + '@tauri-apps/cli-linux-arm64-gnu': 2.9.6 + '@tauri-apps/cli-linux-arm64-musl': 2.9.6 + '@tauri-apps/cli-linux-riscv64-gnu': 2.9.6 + '@tauri-apps/cli-linux-x64-gnu': 2.9.6 + '@tauri-apps/cli-linux-x64-musl': 2.9.6 + '@tauri-apps/cli-win32-arm64-msvc': 2.9.6 + '@tauri-apps/cli-win32-ia32-msvc': 2.9.6 + '@tauri-apps/cli-win32-x64-msvc': 2.9.6 + + '@tauri-apps/plugin-clipboard-manager@2.3.2': + dependencies: + '@tauri-apps/api': 2.9.1 + + '@tauri-apps/plugin-dialog@2.6.0': + dependencies: + '@tauri-apps/api': 2.9.1 + + '@tauri-apps/plugin-fs@2.4.5': + dependencies: + '@tauri-apps/api': 2.9.1 + + '@tauri-apps/plugin-notification@2.3.3': + dependencies: + '@tauri-apps/api': 2.9.1 + + '@tauri-apps/plugin-process@2.3.1': + dependencies: + '@tauri-apps/api': 2.9.1 + + '@tauri-apps/plugin-shell@2.3.4': + dependencies: + '@tauri-apps/api': 2.9.1 + + '@tauri-apps/plugin-store@2.4.2': + dependencies: + '@tauri-apps/api': 2.9.1 + + '@tauri-apps/plugin-updater@2.9.0': + dependencies: + '@tauri-apps/api': 2.9.1 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/estree@1.0.8': {} + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.27)': + dependencies: + '@types/react': 18.3.27 + + '@types/react@18.3.27': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@vitejs/plugin-react@4.7.0(vite@5.4.21)': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 5.4.21 + transitivePeerDependencies: + - supports-color + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@5.0.2: {} + + autoprefixer@10.4.24(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + caniuse-lite: 1.0.30001766 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + baseline-browser-mapping@2.9.19: {} + + binary-extensions@2.3.0: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001766 + electron-to-chromium: 1.5.283 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001766: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + clsx@2.1.1: {} + + commander@4.1.1: {} + + convert-source-map@2.0.0: {} + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + didyoumean@1.2.2: {} + + dlv@1.1.3: {} + + electron-to-chromium@1.5.283: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + fraction.js@5.3.4: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + jiti@1.21.7: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@0.303.0(react@18.3.1): + dependencies: + react: 18.3.1 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + node-releases@2.0.27: {} + + normalize-path@3.0.0: {} + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + path-parse@1.0.7: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pify@2.3.0: {} + + pirates@4.0.7: {} + + postcss-import@15.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.11 + + postcss-js@4.1.0(postcss@8.5.6): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.6 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.6 + + postcss-nested@6.2.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + queue-microtask@1.2.3: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-refresh@0.17.0: {} + + react-router-dom@6.30.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 6.30.3(react@18.3.1) + + react-router@6.30.3(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.2 + react: 18.3.1 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rollup@4.57.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.57.1 + '@rollup/rollup-android-arm64': 4.57.1 + '@rollup/rollup-darwin-arm64': 4.57.1 + '@rollup/rollup-darwin-x64': 4.57.1 + '@rollup/rollup-freebsd-arm64': 4.57.1 + '@rollup/rollup-freebsd-x64': 4.57.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.57.1 + '@rollup/rollup-linux-arm-musleabihf': 4.57.1 + '@rollup/rollup-linux-arm64-gnu': 4.57.1 + '@rollup/rollup-linux-arm64-musl': 4.57.1 + '@rollup/rollup-linux-loong64-gnu': 4.57.1 + '@rollup/rollup-linux-loong64-musl': 4.57.1 + '@rollup/rollup-linux-ppc64-gnu': 4.57.1 + '@rollup/rollup-linux-ppc64-musl': 4.57.1 + '@rollup/rollup-linux-riscv64-gnu': 4.57.1 + '@rollup/rollup-linux-riscv64-musl': 4.57.1 + '@rollup/rollup-linux-s390x-gnu': 4.57.1 + '@rollup/rollup-linux-x64-gnu': 4.57.1 + '@rollup/rollup-linux-x64-musl': 4.57.1 + '@rollup/rollup-openbsd-x64': 4.57.1 + '@rollup/rollup-openharmony-arm64': 4.57.1 + '@rollup/rollup-win32-arm64-msvc': 4.57.1 + '@rollup/rollup-win32-ia32-msvc': 4.57.1 + '@rollup/rollup-win32-x64-gnu': 4.57.1 + '@rollup/rollup-win32-x64-msvc': 4.57.1 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@6.3.1: {} + + source-map-js@1.2.1: {} + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwind-merge@2.6.1: {} + + tailwindcss@3.4.19: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.1.0(postcss@8.5.6) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6) + postcss-nested: 6.2.0(postcss@8.5.6) + postcss-selector-parser: 6.1.2 + resolve: 1.22.11 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-interface-checker@0.1.13: {} + + typescript@5.9.3: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + + util-deprecate@1.0.2: {} + + vite@5.4.21: + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.57.1 + optionalDependencies: + fsevents: 2.3.3 + + yallist@3.1.1: {} + + zustand@4.5.7(@types/react@18.3.27)(react@18.3.1): + dependencies: + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + react: 18.3.1 diff --git a/apps/desktop-wallet/src-tauri/Cargo.toml b/apps/desktop-wallet/src-tauri/Cargo.toml index 0294f76..d6f863f 100644 --- a/apps/desktop-wallet/src-tauri/Cargo.toml +++ b/apps/desktop-wallet/src-tauri/Cargo.toml @@ -42,14 +42,32 @@ bech32 = "0.11" # OS Keychain integration (macOS Keychain, Windows Credential Manager, Linux Secret Service) keyring = "3" -# Local crates from the monorepo (optional - for direct integration with core) -synor-crypto = { path = "../../../crates/synor-crypto", optional = true } -synor-types = { path = "../../../crates/synor-types", optional = true } -synor-rpc = { path = "../../../crates/synor-rpc", optional = true } +# HTTP client for RPC calls +reqwest = { version = "0.12", features = ["json"] } + +# WebSocket client for real-time events +tokio-tungstenite = { version = "0.24", features = ["native-tls"] } +futures-util = "0.3" + +# Logging +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# Local crates from the monorepo (required for wallet functionality) +synor-crypto = { path = "../../../crates/synor-crypto" } +synor-types = { path = "../../../crates/synor-types" } +synor-rpc = { path = "../../../crates/synor-rpc" } + +# Optional: Embedded node support (enables running a full node inside the wallet) +synord = { path = "../../../apps/synord", optional = true } +synor-mining = { path = "../../../crates/synor-mining", optional = true } +synor-network = { path = "../../../crates/synor-network", optional = true } [features] default = ["custom-protocol"] custom-protocol = ["tauri/custom-protocol"] +# Enable embedded node support - compiles full node into wallet binary +embedded-node = ["dep:synord", "dep:synor-mining", "dep:synor-network"] [profile.release] lto = true diff --git a/apps/desktop-wallet/src-tauri/src/commands.rs b/apps/desktop-wallet/src-tauri/src/commands.rs index 69f232d..335b7ea 100644 --- a/apps/desktop-wallet/src-tauri/src/commands.rs +++ b/apps/desktop-wallet/src-tauri/src/commands.rs @@ -373,3 +373,404 @@ pub async fn get_network_status(state: State<'_, WalletState>) -> Result, + pub rpc_client: Arc, +} + +/// Connect to external RPC node +#[tauri::command] +pub async fn node_connect_external( + state: State<'_, AppState>, + http_url: String, + ws_url: Option, +) -> Result { + state + .node_manager + .connect_external(http_url, ws_url) + .await?; + + Ok(state.node_manager.status().await) +} + +/// Start embedded node (requires embedded-node feature) +#[tauri::command] +pub async fn node_start_embedded( + state: State<'_, AppState>, + network: String, + data_dir: Option, + mining_enabled: bool, + coinbase_address: Option, + mining_threads: usize, +) -> Result { + let data_dir = data_dir.map(PathBuf::from); + + state + .node_manager + .start_embedded_node( + &network, + data_dir, + mining_enabled, + coinbase_address, + mining_threads, + ) + .await?; + + Ok(state.node_manager.status().await) +} + +/// Stop the current node connection +#[tauri::command] +pub async fn node_stop(state: State<'_, AppState>) -> Result<()> { + state.node_manager.disconnect().await +} + +/// Get current node status +#[tauri::command] +pub async fn node_get_status(state: State<'_, AppState>) -> Result { + state.node_manager.refresh_status().await +} + +/// Get current connection mode +#[tauri::command] +pub async fn node_get_connection_mode(state: State<'_, AppState>) -> Result { + Ok(state.node_manager.connection_mode().await) +} + +/// Get connected peers +#[tauri::command] +pub async fn node_get_peers(state: State<'_, AppState>) -> Result> { + state.rpc_client.get_peers().await +} + +/// Get sync progress +#[tauri::command] +pub async fn node_get_sync_progress(state: State<'_, AppState>) -> Result { + let status = state.node_manager.status().await; + + Ok(SyncProgress { + current_height: status.block_height, + target_height: status.block_height, // TODO: Get from peers + progress: status.sync_progress, + eta_seconds: None, + status: if status.is_syncing { + "Syncing...".to_string() + } else { + "Synced".to_string() + }, + }) +} + +// ============================================================================ +// Mining Commands +// ============================================================================ + +/// Mining status +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MiningStatus { + /// Is mining active + pub is_mining: bool, + /// Is mining paused + pub is_paused: bool, + /// Current hashrate (H/s) + pub hashrate: f64, + /// Blocks found in this session + pub blocks_found: u64, + /// Total shares submitted + pub shares_submitted: u64, + /// Number of mining threads + pub threads: usize, + /// Coinbase address for rewards + pub coinbase_address: Option, +} + +/// Mining stats (more detailed) +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MiningStats { + /// Current hashrate (H/s) + pub hashrate: f64, + /// Average hashrate (H/s) + pub avg_hashrate: f64, + /// Peak hashrate (H/s) + pub peak_hashrate: f64, + /// Blocks found + pub blocks_found: u64, + /// Rejected blocks + pub blocks_rejected: u64, + /// Estimated daily coins + pub estimated_daily_coins: f64, + /// Mining uptime in seconds + pub uptime_seconds: u64, + /// Per-thread hashrates + pub thread_hashrates: Vec, +} + +/// Global mining state +pub struct MiningState { + pub is_mining: std::sync::atomic::AtomicBool, + pub is_paused: std::sync::atomic::AtomicBool, + pub threads: std::sync::atomic::AtomicUsize, + pub coinbase_address: tokio::sync::RwLock>, + pub blocks_found: std::sync::atomic::AtomicU64, + pub hashrate: tokio::sync::RwLock, +} + +impl MiningState { + pub fn new() -> Self { + MiningState { + is_mining: std::sync::atomic::AtomicBool::new(false), + is_paused: std::sync::atomic::AtomicBool::new(false), + threads: std::sync::atomic::AtomicUsize::new(0), + coinbase_address: tokio::sync::RwLock::new(None), + blocks_found: std::sync::atomic::AtomicU64::new(0), + hashrate: tokio::sync::RwLock::new(0.0), + } + } +} + +impl Default for MiningState { + fn default() -> Self { + Self::new() + } +} + +/// Start mining +#[tauri::command] +pub async fn mining_start( + app_state: State<'_, AppState>, + mining_state: State<'_, MiningState>, + coinbase_address: String, + threads: usize, +) -> Result { + use std::sync::atomic::Ordering; + + // Verify we're connected to a node + let mode = app_state.node_manager.connection_mode().await; + if matches!(mode, ConnectionMode::Disconnected) { + return Err(Error::NotConnected); + } + + // Store mining configuration + *mining_state.coinbase_address.write().await = Some(coinbase_address.clone()); + mining_state.threads.store(threads, Ordering::SeqCst); + mining_state.is_mining.store(true, Ordering::SeqCst); + mining_state.is_paused.store(false, Ordering::SeqCst); + + // TODO: Actually start mining via embedded node or external RPC + // For embedded node with mining feature: + // if let Some(node) = app_state.node_manager.embedded_node().await { + // node.miner().start().await?; + // } + + Ok(MiningStatus { + is_mining: true, + is_paused: false, + hashrate: 0.0, + blocks_found: 0, + shares_submitted: 0, + threads, + coinbase_address: Some(coinbase_address), + }) +} + +/// Stop mining +#[tauri::command] +pub async fn mining_stop( + mining_state: State<'_, MiningState>, +) -> Result<()> { + use std::sync::atomic::Ordering; + + mining_state.is_mining.store(false, Ordering::SeqCst); + mining_state.is_paused.store(false, Ordering::SeqCst); + *mining_state.hashrate.write().await = 0.0; + + // TODO: Actually stop mining + + Ok(()) +} + +/// Pause mining +#[tauri::command] +pub async fn mining_pause( + mining_state: State<'_, MiningState>, +) -> Result<()> { + use std::sync::atomic::Ordering; + + if !mining_state.is_mining.load(Ordering::SeqCst) { + return Err(Error::MiningError("Mining is not active".to_string())); + } + + mining_state.is_paused.store(true, Ordering::SeqCst); + + Ok(()) +} + +/// Resume mining +#[tauri::command] +pub async fn mining_resume( + mining_state: State<'_, MiningState>, +) -> Result<()> { + use std::sync::atomic::Ordering; + + if !mining_state.is_mining.load(Ordering::SeqCst) { + return Err(Error::MiningError("Mining is not active".to_string())); + } + + mining_state.is_paused.store(false, Ordering::SeqCst); + + Ok(()) +} + +/// Get mining status +#[tauri::command] +pub async fn mining_get_status( + mining_state: State<'_, MiningState>, +) -> Result { + use std::sync::atomic::Ordering; + + Ok(MiningStatus { + is_mining: mining_state.is_mining.load(Ordering::SeqCst), + is_paused: mining_state.is_paused.load(Ordering::SeqCst), + hashrate: *mining_state.hashrate.read().await, + blocks_found: mining_state.blocks_found.load(Ordering::SeqCst), + shares_submitted: 0, + threads: mining_state.threads.load(Ordering::SeqCst), + coinbase_address: mining_state.coinbase_address.read().await.clone(), + }) +} + +/// Get detailed mining stats +#[tauri::command] +pub async fn mining_get_stats( + mining_state: State<'_, MiningState>, +) -> Result { + use std::sync::atomic::Ordering; + + let hashrate = *mining_state.hashrate.read().await; + let threads = mining_state.threads.load(Ordering::SeqCst); + + Ok(MiningStats { + hashrate, + avg_hashrate: hashrate, + peak_hashrate: hashrate, + blocks_found: mining_state.blocks_found.load(Ordering::SeqCst), + blocks_rejected: 0, + estimated_daily_coins: 0.0, // TODO: Calculate based on network difficulty + uptime_seconds: 0, + thread_hashrates: vec![hashrate / threads.max(1) as f64; threads], + }) +} + +/// Set mining threads +#[tauri::command] +pub async fn mining_set_threads( + mining_state: State<'_, MiningState>, + threads: usize, +) -> Result<()> { + use std::sync::atomic::Ordering; + + if threads == 0 { + return Err(Error::MiningError("Threads must be greater than 0".to_string())); + } + + mining_state.threads.store(threads, Ordering::SeqCst); + + // TODO: Actually adjust mining threads + + Ok(()) +} + +// ============================================================================ +// Enhanced Wallet Commands (using RPC client) +// ============================================================================ + +/// Get balance using RPC client +#[tauri::command] +pub async fn wallet_get_balance( + wallet_state: State<'_, WalletState>, + app_state: State<'_, AppState>, +) -> Result { + let addresses = wallet_state.addresses.read().await; + if addresses.is_empty() { + return Ok(BalanceResponse { + balance: 0, + balance_human: "0 SYN".to_string(), + pending: 0, + }); + } + + let mut total_balance: u64 = 0; + + for addr in addresses.iter() { + match app_state.rpc_client.get_balance(&addr.address).await { + Ok(balance) => { + total_balance += balance.balance; + } + Err(e) => { + tracing::warn!("Failed to get balance for {}: {}", addr.address, e); + } + } + } + + // Convert sompi to SYN (1 SYN = 100_000_000 sompi) + let syn = total_balance as f64 / 100_000_000.0; + let balance_human = format!("{:.8} SYN", syn); + + Ok(BalanceResponse { + balance: total_balance, + balance_human, + pending: 0, // TODO: Track pending transactions + }) +} + +/// Get UTXOs using RPC client +#[tauri::command] +pub async fn wallet_get_utxos( + wallet_state: State<'_, WalletState>, + app_state: State<'_, AppState>, +) -> Result> { + let addresses = wallet_state.addresses.read().await; + let mut all_utxos = Vec::new(); + + for addr in addresses.iter() { + match app_state.rpc_client.get_utxos(&addr.address).await { + Ok(utxos) => { + all_utxos.extend(utxos); + } + Err(e) => { + tracing::warn!("Failed to get UTXOs for {}: {}", addr.address, e); + } + } + } + + Ok(all_utxos) +} + +/// Get network info using RPC client +#[tauri::command] +pub async fn wallet_get_network_info( + app_state: State<'_, AppState>, +) -> Result { + app_state.rpc_client.get_network_info().await +} + +/// Get fee estimate using RPC client +#[tauri::command] +pub async fn wallet_get_fee_estimate( + app_state: State<'_, AppState>, +) -> Result { + app_state.rpc_client.get_fee_estimate().await +} diff --git a/apps/desktop-wallet/src-tauri/src/error.rs b/apps/desktop-wallet/src-tauri/src/error.rs index cafe031..5220ea2 100644 --- a/apps/desktop-wallet/src-tauri/src/error.rs +++ b/apps/desktop-wallet/src-tauri/src/error.rs @@ -44,6 +44,24 @@ pub enum Error { #[error("Keychain error: {0}")] Keychain(String), + #[error("Node error: {0}")] + NodeError(String), + + #[error("Node is already running")] + NodeAlreadyRunning, + + #[error("Node is not running")] + NodeNotRunning, + + #[error("Not connected to any node")] + NotConnected, + + #[error("Feature not enabled: {0}")] + FeatureNotEnabled(String), + + #[error("Mining error: {0}")] + MiningError(String), + #[error("Internal error: {0}")] Internal(String), } diff --git a/apps/desktop-wallet/src-tauri/src/lib.rs b/apps/desktop-wallet/src-tauri/src/lib.rs index de2cfcc..48c52db 100644 --- a/apps/desktop-wallet/src-tauri/src/lib.rs +++ b/apps/desktop-wallet/src-tauri/src/lib.rs @@ -12,6 +12,8 @@ mod commands; mod crypto; mod error; mod keychain; +mod node; +mod rpc_client; mod wallet; use tauri::{ @@ -116,6 +118,27 @@ pub fn run() { let wallet_state = wallet::WalletState::new(); app.manage(wallet_state); + // Initialize node manager with app handle for events + let node_manager = std::sync::Arc::new( + node::NodeManager::with_app_handle(app.handle().clone()) + ); + + // Initialize RPC client + let rpc_client = std::sync::Arc::new( + rpc_client::RpcClient::new(node_manager.clone()) + ); + + // Initialize app state (node + RPC) + let app_state = commands::AppState { + node_manager, + rpc_client, + }; + app.manage(app_state); + + // Initialize mining state + let mining_state = commands::MiningState::new(); + app.manage(mining_state); + // Build and set up system tray let menu = build_tray_menu(app.handle())?; let _tray = TrayIconBuilder::new() @@ -165,10 +188,31 @@ pub fn run() { commands::sign_transaction, commands::broadcast_transaction, commands::get_transaction_history, - // Network + // Network (legacy) commands::connect_node, commands::disconnect_node, commands::get_network_status, + // Node management (new) + commands::node_connect_external, + commands::node_start_embedded, + commands::node_stop, + commands::node_get_status, + commands::node_get_connection_mode, + commands::node_get_peers, + commands::node_get_sync_progress, + // Mining + commands::mining_start, + commands::mining_stop, + commands::mining_pause, + commands::mining_resume, + commands::mining_get_status, + commands::mining_get_stats, + commands::mining_set_threads, + // Enhanced wallet (using RPC client) + commands::wallet_get_balance, + commands::wallet_get_utxos, + commands::wallet_get_network_info, + commands::wallet_get_fee_estimate, // Updates check_update, install_update, diff --git a/apps/desktop-wallet/src-tauri/src/node.rs b/apps/desktop-wallet/src-tauri/src/node.rs new file mode 100644 index 0000000..3275344 --- /dev/null +++ b/apps/desktop-wallet/src-tauri/src/node.rs @@ -0,0 +1,480 @@ +//! Embedded Node Module +//! +//! Provides optional embedded Synor node functionality for the desktop wallet. +//! When enabled with the `embedded-node` feature, users can run a full node +//! directly inside the wallet application. + +use std::path::PathBuf; +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; +use tokio::sync::RwLock; +use tracing::{error, info, warn}; + +/// Node connection mode - embedded node or external RPC +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum ConnectionMode { + /// No connection configured + Disconnected, + /// Connect to an external node via RPC + External { + http_url: String, + ws_url: Option, + }, + /// Run embedded node (requires `embedded-node` feature) + #[cfg(feature = "embedded-node")] + Embedded { + network: String, + data_dir: Option, + }, +} + +impl Default for ConnectionMode { + fn default() -> Self { + ConnectionMode::Disconnected + } +} + +/// Node status information +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct NodeStatus { + /// Connection mode + pub mode: ConnectionMode, + /// Whether the node is connected/running + pub is_connected: bool, + /// Current block height + pub block_height: u64, + /// Current blue score (DAG metric) + pub blue_score: u64, + /// Number of connected peers + pub peer_count: usize, + /// Whether the node is syncing + pub is_syncing: bool, + /// Sync progress (0.0 - 1.0) + pub sync_progress: f64, + /// Network name + pub network: String, + /// Chain ID + pub chain_id: u64, +} + +impl Default for NodeStatus { + fn default() -> Self { + NodeStatus { + mode: ConnectionMode::Disconnected, + is_connected: false, + block_height: 0, + blue_score: 0, + peer_count: 0, + is_syncing: false, + sync_progress: 0.0, + network: String::new(), + chain_id: 0, + } + } +} + +/// Sync progress information +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SyncProgress { + /// Current block height + pub current_height: u64, + /// Target block height (highest known) + pub target_height: u64, + /// Progress percentage (0.0 - 100.0) + pub progress: f64, + /// Estimated time remaining in seconds + pub eta_seconds: Option, + /// Sync status message + pub status: String, +} + +/// Peer information +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PeerInfo { + /// Peer ID + pub peer_id: String, + /// Peer address + pub address: String, + /// Connection direction (inbound/outbound) + pub direction: String, + /// Latency in milliseconds + pub latency_ms: Option, + /// Peer's block height + pub block_height: u64, + /// Connection status + pub status: String, +} + +/// Manages node connection state and lifecycle +pub struct NodeManager { + /// Current connection mode + mode: RwLock, + /// Cached status + status: RwLock, + /// Embedded node instance (when feature enabled) + #[cfg(feature = "embedded-node")] + embedded_node: RwLock>>, + /// Tauri app handle for emitting events + app_handle: Option, +} + +impl NodeManager { + /// Creates a new node manager + pub fn new() -> Self { + NodeManager { + mode: RwLock::new(ConnectionMode::Disconnected), + status: RwLock::new(NodeStatus::default()), + #[cfg(feature = "embedded-node")] + embedded_node: RwLock::new(None), + app_handle: None, + } + } + + /// Creates a new node manager with Tauri app handle for events + pub fn with_app_handle(app_handle: tauri::AppHandle) -> Self { + NodeManager { + mode: RwLock::new(ConnectionMode::Disconnected), + status: RwLock::new(NodeStatus::default()), + #[cfg(feature = "embedded-node")] + embedded_node: RwLock::new(None), + app_handle: Some(app_handle), + } + } + + /// Gets the current connection mode + pub async fn connection_mode(&self) -> ConnectionMode { + self.mode.read().await.clone() + } + + /// Gets the current node status + pub async fn status(&self) -> NodeStatus { + self.status.read().await.clone() + } + + /// Connects to an external node via RPC + pub async fn connect_external( + &self, + http_url: String, + ws_url: Option, + ) -> crate::Result<()> { + info!(http_url = %http_url, "Connecting to external node"); + + // Update mode + *self.mode.write().await = ConnectionMode::External { + http_url: http_url.clone(), + ws_url: ws_url.clone(), + }; + + // Update status + let mut status = self.status.write().await; + status.mode = ConnectionMode::External { + http_url, + ws_url, + }; + status.is_connected = true; + + self.emit_status_changed(&status); + Ok(()) + } + + /// Disconnects from the current node + pub async fn disconnect(&self) -> crate::Result<()> { + let current_mode = self.mode.read().await.clone(); + + match current_mode { + ConnectionMode::Disconnected => { + // Already disconnected + Ok(()) + } + ConnectionMode::External { .. } => { + info!("Disconnecting from external node"); + *self.mode.write().await = ConnectionMode::Disconnected; + + let mut status = self.status.write().await; + *status = NodeStatus::default(); + self.emit_status_changed(&status); + Ok(()) + } + #[cfg(feature = "embedded-node")] + ConnectionMode::Embedded { .. } => { + self.stop_embedded_node().await + } + } + } + + /// Emits a status changed event to the frontend + fn emit_status_changed(&self, status: &NodeStatus) { + if let Some(ref app) = self.app_handle { + use tauri::Emitter; + if let Err(e) = app.emit("node:status-changed", status) { + warn!("Failed to emit node status: {}", e); + } + } + } + + /// Emits a sync progress event to the frontend + fn emit_sync_progress(&self, progress: &SyncProgress) { + if let Some(ref app) = self.app_handle { + use tauri::Emitter; + if let Err(e) = app.emit("node:sync-progress", progress) { + warn!("Failed to emit sync progress: {}", e); + } + } + } +} + +// ============================================================================ +// Embedded Node Support (feature-gated) +// ============================================================================ + +#[cfg(feature = "embedded-node")] +impl NodeManager { + /// Starts the embedded node + pub async fn start_embedded_node( + &self, + network: &str, + data_dir: Option, + mining_enabled: bool, + coinbase_address: Option, + mining_threads: usize, + ) -> crate::Result<()> { + // Check if already running + if self.embedded_node.read().await.is_some() { + return Err(crate::Error::NodeAlreadyRunning); + } + + info!(network = %network, "Starting embedded node"); + + // Create node configuration + let mut config = synord::NodeConfig::for_network(network) + .map_err(|e| crate::Error::NodeError(e.to_string()))?; + + // Override data directory if specified + if let Some(dir) = data_dir.clone() { + config.data_dir = dir; + } + + // Configure mining + if mining_enabled { + config.mining.enabled = true; + config.mining.coinbase_address = coinbase_address; + if mining_threads > 0 { + config.mining.threads = mining_threads; + } + } + + // Configure RPC to use wallet-specific ports + config.rpc.http_addr = "127.0.0.1:19423".to_string(); + config.rpc.ws_addr = "127.0.0.1:19424".to_string(); + + // Configure P2P to use wallet-specific port + config.p2p.listen_addr = format!("/ip4/0.0.0.0/tcp/{}", match network { + "mainnet" => 19422, + "testnet" => 19522, + "devnet" => 19622, + _ => 19422, + }); + + // Create and start the node + let node = synord::SynorNode::new(config) + .await + .map_err(|e| crate::Error::NodeError(format!("Failed to create node: {}", e)))?; + + node.start() + .await + .map_err(|e| crate::Error::NodeError(format!("Failed to start node: {}", e)))?; + + let node = Arc::new(node); + + // Store the node + *self.embedded_node.write().await = Some(node.clone()); + + // Update mode and status + *self.mode.write().await = ConnectionMode::Embedded { + network: network.to_string(), + data_dir, + }; + + let node_info = node.info().await; + let mut status = self.status.write().await; + status.mode = self.mode.read().await.clone(); + status.is_connected = true; + status.network = node_info.network; + status.chain_id = node_info.chain_id; + status.block_height = node_info.block_height; + status.blue_score = node_info.blue_score; + status.peer_count = node_info.peer_count; + status.is_syncing = node_info.is_syncing; + + self.emit_status_changed(&status); + + info!("Embedded node started successfully"); + Ok(()) + } + + /// Stops the embedded node + pub async fn stop_embedded_node(&self) -> crate::Result<()> { + let node = self.embedded_node.write().await.take(); + + if let Some(node) = node { + info!("Stopping embedded node"); + node.stop() + .await + .map_err(|e| crate::Error::NodeError(format!("Failed to stop node: {}", e)))?; + } + + *self.mode.write().await = ConnectionMode::Disconnected; + + let mut status = self.status.write().await; + *status = NodeStatus::default(); + self.emit_status_changed(&status); + + info!("Embedded node stopped"); + Ok(()) + } + + /// Gets the embedded node if running + pub async fn embedded_node(&self) -> Option> { + self.embedded_node.read().await.clone() + } + + /// Refreshes the node status from the embedded node + pub async fn refresh_status(&self) -> crate::Result { + if let Some(node) = self.embedded_node.read().await.as_ref() { + let node_info = node.info().await; + let state = node.state().await; + + let mut status = self.status.write().await; + status.block_height = node_info.block_height; + status.blue_score = node_info.blue_score; + status.peer_count = node_info.peer_count; + status.is_syncing = node_info.is_syncing; + + // Calculate sync progress if syncing + if node_info.is_syncing { + // In a real implementation, we'd get the target height from peers + status.sync_progress = 0.0; // Placeholder + } else { + status.sync_progress = 100.0; + } + + self.emit_status_changed(&status); + Ok(status.clone()) + } else { + Ok(self.status.read().await.clone()) + } + } + + /// Gets connected peers from the embedded node + pub async fn get_peers(&self) -> crate::Result> { + if let Some(node) = self.embedded_node.read().await.as_ref() { + let peer_count = node.network().peer_count().await; + // In a full implementation, we'd get detailed peer info from the network service + // For now, return a placeholder + Ok(vec![]) + } else { + Ok(vec![]) + } + } +} + +// Stub implementations when embedded-node feature is disabled +#[cfg(not(feature = "embedded-node"))] +impl NodeManager { + /// Stub: Embedded node not available + pub async fn start_embedded_node( + &self, + _network: &str, + _data_dir: Option, + _mining_enabled: bool, + _coinbase_address: Option, + _mining_threads: usize, + ) -> crate::Result<()> { + Err(crate::Error::FeatureNotEnabled("embedded-node".to_string())) + } + + /// Stub: Embedded node not available + pub async fn stop_embedded_node(&self) -> crate::Result<()> { + Err(crate::Error::FeatureNotEnabled("embedded-node".to_string())) + } + + /// Stub: No embedded node + pub async fn refresh_status(&self) -> crate::Result { + Ok(self.status.read().await.clone()) + } + + /// Stub: No peers without embedded node + pub async fn get_peers(&self) -> crate::Result> { + Ok(vec![]) + } +} + +impl Default for NodeManager { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_connection_mode_default() { + let mode = ConnectionMode::default(); + assert_eq!(mode, ConnectionMode::Disconnected); + } + + #[test] + fn test_node_status_default() { + let status = NodeStatus::default(); + assert!(!status.is_connected); + assert_eq!(status.block_height, 0); + assert_eq!(status.peer_count, 0); + } + + #[tokio::test] + async fn test_node_manager_creation() { + let manager = NodeManager::new(); + let mode = manager.connection_mode().await; + assert_eq!(mode, ConnectionMode::Disconnected); + } + + #[tokio::test] + async fn test_connect_external() { + let manager = NodeManager::new(); + let result = manager + .connect_external( + "http://localhost:16110".to_string(), + Some("ws://localhost:16111".to_string()), + ) + .await; + assert!(result.is_ok()); + + let mode = manager.connection_mode().await; + match mode { + ConnectionMode::External { http_url, ws_url } => { + assert_eq!(http_url, "http://localhost:16110"); + assert_eq!(ws_url, Some("ws://localhost:16111".to_string())); + } + _ => panic!("Expected External mode"), + } + } + + #[tokio::test] + async fn test_disconnect() { + let manager = NodeManager::new(); + manager + .connect_external("http://localhost:16110".to_string(), None) + .await + .unwrap(); + + let result = manager.disconnect().await; + assert!(result.is_ok()); + + let mode = manager.connection_mode().await; + assert_eq!(mode, ConnectionMode::Disconnected); + } +} diff --git a/apps/desktop-wallet/src-tauri/src/rpc_client.rs b/apps/desktop-wallet/src-tauri/src/rpc_client.rs new file mode 100644 index 0000000..27b6724 --- /dev/null +++ b/apps/desktop-wallet/src-tauri/src/rpc_client.rs @@ -0,0 +1,563 @@ +//! RPC Client Abstraction +//! +//! Provides a unified interface for communicating with Synor nodes, +//! whether embedded or external. This allows the wallet to seamlessly +//! switch between connection modes. + +use std::sync::Arc; + +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use tokio::sync::RwLock; +use tracing::{debug, error, info}; + +use crate::node::{ConnectionMode, NodeManager, NodeStatus}; + +/// JSON-RPC 2.0 request +#[derive(Debug, Serialize)] +struct JsonRpcRequest { + jsonrpc: &'static str, + method: String, + params: T, + id: u64, +} + +/// JSON-RPC 2.0 response +#[derive(Debug, Deserialize)] +struct JsonRpcResponse { + #[allow(dead_code)] + jsonrpc: String, + result: Option, + error: Option, + #[allow(dead_code)] + id: u64, +} + +/// JSON-RPC 2.0 error +#[derive(Debug, Deserialize)] +struct JsonRpcError { + code: i32, + message: String, + #[allow(dead_code)] + data: Option, +} + +/// Balance information +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Balance { + /// Address + pub address: String, + /// Balance in sompi (smallest unit) + pub balance: u64, +} + +/// UTXO (Unspent Transaction Output) +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Utxo { + /// Transaction ID + pub transaction_id: String, + /// Output index + pub index: u32, + /// Amount in sompi + pub amount: u64, + /// Script public key (hex) + pub script_public_key: String, + /// Block DAA score when created + pub block_daa_score: u64, + /// Whether this is a coinbase output + pub is_coinbase: bool, +} + +/// Transaction status +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionStatus { + /// Transaction ID + pub transaction_id: String, + /// Is confirmed + pub is_confirmed: bool, + /// Confirmations count + pub confirmations: u64, + /// Block hash (if confirmed) + pub block_hash: Option, + /// Block time (if confirmed) + pub block_time: Option, +} + +/// Transaction for broadcasting +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RawTransaction { + /// Transaction version + pub version: u16, + /// Inputs + pub inputs: Vec, + /// Outputs + pub outputs: Vec, + /// Lock time + pub lock_time: u64, + /// Subnetwork ID + pub subnetwork_id: String, + /// Gas + pub gas: u64, + /// Payload (hex) + pub payload: String, +} + +/// Transaction input +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionInput { + /// Previous transaction ID + pub previous_transaction_id: String, + /// Previous output index + pub previous_index: u32, + /// Signature script (hex) + pub signature_script: String, + /// Sequence number + pub sequence: u64, +} + +/// Transaction output +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionOutput { + /// Amount in sompi + pub amount: u64, + /// Script public key (hex) + pub script_public_key: String, +} + +/// Network information +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NetworkInfo { + /// Network name + pub network: String, + /// Is synced + pub is_synced: bool, + /// Current block height + pub block_height: u64, + /// Blue score + pub blue_score: u64, + /// Difficulty + pub difficulty: f64, + /// Peer count + pub peer_count: usize, + /// Mempool size + pub mempool_size: u64, +} + +/// Fee estimate +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FeeEstimate { + /// Priority fee rate (sompi per gram) + pub priority: f64, + /// Normal fee rate + pub normal: f64, + /// Low fee rate + pub low: f64, +} + +/// Unified RPC client for communicating with Synor nodes +pub struct RpcClient { + /// Node manager for connection handling + node_manager: Arc, + /// HTTP client for external RPC + http_client: reqwest::Client, + /// Request ID counter + request_id: RwLock, +} + +impl RpcClient { + /// Creates a new RPC client + pub fn new(node_manager: Arc) -> Self { + RpcClient { + node_manager, + http_client: reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + .expect("Failed to build HTTP client"), + request_id: RwLock::new(0), + } + } + + /// Gets the next request ID + async fn next_id(&self) -> u64 { + let mut id = self.request_id.write().await; + *id += 1; + *id + } + + /// Gets the current connection mode + pub async fn connection_mode(&self) -> ConnectionMode { + self.node_manager.connection_mode().await + } + + /// Gets the current node status + pub async fn node_status(&self) -> NodeStatus { + self.node_manager.status().await + } + + /// Makes an RPC call to an external node + async fn call_external(&self, http_url: &str, method: &str, params: P) -> crate::Result + where + P: Serialize, + R: DeserializeOwned, + { + let id = self.next_id().await; + let request = JsonRpcRequest { + jsonrpc: "2.0", + method: method.to_string(), + params, + id, + }; + + debug!(method = %method, id = %id, "Making external RPC call"); + + let response = self + .http_client + .post(http_url) + .json(&request) + .send() + .await + .map_err(|e| crate::Error::Rpc(format!("HTTP request failed: {}", e)))?; + + let status = response.status(); + if !status.is_success() { + return Err(crate::Error::Rpc(format!("HTTP error: {}", status))); + } + + let rpc_response: JsonRpcResponse = response + .json() + .await + .map_err(|e| crate::Error::Rpc(format!("Failed to parse response: {}", e)))?; + + if let Some(error) = rpc_response.error { + return Err(crate::Error::Rpc(format!( + "RPC error {}: {}", + error.code, error.message + ))); + } + + rpc_response + .result + .ok_or_else(|| crate::Error::Rpc("Empty response".to_string())) + } + + // ============================================================================ + // Public API Methods + // ============================================================================ + + /// Gets the balance for an address + pub async fn get_balance(&self, address: &str) -> crate::Result { + let mode = self.node_manager.connection_mode().await; + + match mode { + ConnectionMode::Disconnected => Err(crate::Error::NotConnected), + ConnectionMode::External { http_url, .. } => { + self.call_external(&http_url, "synor_getBalanceByAddress", (address,)) + .await + } + #[cfg(feature = "embedded-node")] + ConnectionMode::Embedded { .. } => { + // Direct call to embedded node + if let Some(node) = self.node_manager.embedded_node().await { + // TODO: Implement direct UTXO query from embedded node + // For now, use the internal RPC + let rpc_addr = format!( + "http://{}", + node.config().rpc.http_addr + ); + self.call_external(&rpc_addr, "synor_getBalanceByAddress", (address,)) + .await + } else { + Err(crate::Error::NodeNotRunning) + } + } + } + } + + /// Gets UTXOs for an address + pub async fn get_utxos(&self, address: &str) -> crate::Result> { + let mode = self.node_manager.connection_mode().await; + + match mode { + ConnectionMode::Disconnected => Err(crate::Error::NotConnected), + ConnectionMode::External { http_url, .. } => { + self.call_external(&http_url, "synor_getUtxosByAddress", (address,)) + .await + } + #[cfg(feature = "embedded-node")] + ConnectionMode::Embedded { .. } => { + if let Some(node) = self.node_manager.embedded_node().await { + let rpc_addr = format!("http://{}", node.config().rpc.http_addr); + self.call_external(&rpc_addr, "synor_getUtxosByAddress", (address,)) + .await + } else { + Err(crate::Error::NodeNotRunning) + } + } + } + } + + /// Broadcasts a transaction to the network + pub async fn broadcast_transaction(&self, tx: &RawTransaction) -> crate::Result { + let mode = self.node_manager.connection_mode().await; + + match mode { + ConnectionMode::Disconnected => Err(crate::Error::NotConnected), + ConnectionMode::External { http_url, .. } => { + self.call_external(&http_url, "synor_submitTransaction", (tx,)) + .await + } + #[cfg(feature = "embedded-node")] + ConnectionMode::Embedded { .. } => { + if let Some(node) = self.node_manager.embedded_node().await { + let rpc_addr = format!("http://{}", node.config().rpc.http_addr); + self.call_external(&rpc_addr, "synor_submitTransaction", (tx,)) + .await + } else { + Err(crate::Error::NodeNotRunning) + } + } + } + } + + /// Gets transaction status + pub async fn get_transaction_status(&self, txid: &str) -> crate::Result { + let mode = self.node_manager.connection_mode().await; + + match mode { + ConnectionMode::Disconnected => Err(crate::Error::NotConnected), + ConnectionMode::External { http_url, .. } => { + self.call_external(&http_url, "synor_getTransaction", (txid,)) + .await + } + #[cfg(feature = "embedded-node")] + ConnectionMode::Embedded { .. } => { + if let Some(node) = self.node_manager.embedded_node().await { + let rpc_addr = format!("http://{}", node.config().rpc.http_addr); + self.call_external(&rpc_addr, "synor_getTransaction", (txid,)) + .await + } else { + Err(crate::Error::NodeNotRunning) + } + } + } + } + + /// Gets network information + pub async fn get_network_info(&self) -> crate::Result { + let mode = self.node_manager.connection_mode().await; + + match mode { + ConnectionMode::Disconnected => Err(crate::Error::NotConnected), + ConnectionMode::External { http_url, .. } => { + self.call_external(&http_url, "synor_getInfo", ()) + .await + } + #[cfg(feature = "embedded-node")] + ConnectionMode::Embedded { .. } => { + if let Some(node) = self.node_manager.embedded_node().await { + // Build network info from embedded node directly + let info = node.info().await; + Ok(NetworkInfo { + network: info.network, + is_synced: !info.is_syncing, + block_height: info.block_height, + blue_score: info.blue_score, + difficulty: 0.0, // TODO: Get from consensus + peer_count: info.peer_count, + mempool_size: 0, // TODO: Get from mempool + }) + } else { + Err(crate::Error::NodeNotRunning) + } + } + } + } + + /// Gets fee estimate + pub async fn get_fee_estimate(&self) -> crate::Result { + let mode = self.node_manager.connection_mode().await; + + match mode { + ConnectionMode::Disconnected => Err(crate::Error::NotConnected), + ConnectionMode::External { http_url, .. } => { + self.call_external(&http_url, "synor_estimateFee", ()) + .await + } + #[cfg(feature = "embedded-node")] + ConnectionMode::Embedded { .. } => { + if let Some(node) = self.node_manager.embedded_node().await { + let rpc_addr = format!("http://{}", node.config().rpc.http_addr); + self.call_external(&rpc_addr, "synor_estimateFee", ()) + .await + } else { + Err(crate::Error::NodeNotRunning) + } + } + } + } + + /// Gets block by hash + pub async fn get_block(&self, hash: &str) -> crate::Result { + let mode = self.node_manager.connection_mode().await; + + match mode { + ConnectionMode::Disconnected => Err(crate::Error::NotConnected), + ConnectionMode::External { http_url, .. } => { + self.call_external(&http_url, "synor_getBlock", (hash, true)) + .await + } + #[cfg(feature = "embedded-node")] + ConnectionMode::Embedded { .. } => { + if let Some(node) = self.node_manager.embedded_node().await { + let rpc_addr = format!("http://{}", node.config().rpc.http_addr); + self.call_external(&rpc_addr, "synor_getBlock", (hash, true)) + .await + } else { + Err(crate::Error::NodeNotRunning) + } + } + } + } + + /// Gets current block template for mining + pub async fn get_block_template(&self, coinbase_address: &str) -> crate::Result { + let mode = self.node_manager.connection_mode().await; + + match mode { + ConnectionMode::Disconnected => Err(crate::Error::NotConnected), + ConnectionMode::External { http_url, .. } => { + self.call_external(&http_url, "synor_getBlockTemplate", (coinbase_address,)) + .await + } + #[cfg(feature = "embedded-node")] + ConnectionMode::Embedded { .. } => { + if let Some(node) = self.node_manager.embedded_node().await { + let rpc_addr = format!("http://{}", node.config().rpc.http_addr); + self.call_external(&rpc_addr, "synor_getBlockTemplate", (coinbase_address,)) + .await + } else { + Err(crate::Error::NodeNotRunning) + } + } + } + } + + /// Submits a mined block + pub async fn submit_block(&self, block: &serde_json::Value) -> crate::Result { + let mode = self.node_manager.connection_mode().await; + + match mode { + ConnectionMode::Disconnected => Err(crate::Error::NotConnected), + ConnectionMode::External { http_url, .. } => { + self.call_external(&http_url, "synor_submitBlock", (block,)) + .await + } + #[cfg(feature = "embedded-node")] + ConnectionMode::Embedded { .. } => { + if let Some(node) = self.node_manager.embedded_node().await { + let rpc_addr = format!("http://{}", node.config().rpc.http_addr); + self.call_external(&rpc_addr, "synor_submitBlock", (block,)) + .await + } else { + Err(crate::Error::NodeNotRunning) + } + } + } + } + + /// Gets connected peers + pub async fn get_peers(&self) -> crate::Result> { + let mode = self.node_manager.connection_mode().await; + + match mode { + ConnectionMode::Disconnected => Err(crate::Error::NotConnected), + ConnectionMode::External { http_url, .. } => { + // External: call RPC + let peers: Vec = self + .call_external(&http_url, "net_getPeerInfo", ()) + .await?; + + // Convert to our PeerInfo type + let peer_infos = peers + .into_iter() + .map(|p| crate::node::PeerInfo { + peer_id: p["id"].as_str().unwrap_or("").to_string(), + address: p["address"].as_str().unwrap_or("").to_string(), + direction: if p["isOutbound"].as_bool().unwrap_or(false) { + "outbound" + } else { + "inbound" + } + .to_string(), + latency_ms: p["lastPingDuration"].as_u64(), + block_height: p["syncedHeaders"].as_u64().unwrap_or(0), + status: "connected".to_string(), + }) + .collect(); + + Ok(peer_infos) + } + #[cfg(feature = "embedded-node")] + ConnectionMode::Embedded { .. } => { + self.node_manager.get_peers().await + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_balance_serialization() { + let balance = Balance { + address: "synor:qztest123".to_string(), + balance: 1_000_000_000, + }; + + let json = serde_json::to_string(&balance).unwrap(); + assert!(json.contains("synor:qztest123")); + assert!(json.contains("1000000000")); + } + + #[test] + fn test_utxo_serialization() { + let utxo = Utxo { + transaction_id: "abc123".to_string(), + index: 0, + amount: 5_000_000_000, + script_public_key: "76a914...88ac".to_string(), + block_daa_score: 100, + is_coinbase: false, + }; + + let json = serde_json::to_string(&utxo).unwrap(); + assert!(json.contains("transactionId")); + assert!(json.contains("blockDaaScore")); + } + + #[test] + fn test_network_info_serialization() { + let info = NetworkInfo { + network: "testnet".to_string(), + is_synced: true, + block_height: 10000, + blue_score: 5000, + difficulty: 12345.67, + peer_count: 25, + mempool_size: 50, + }; + + let json = serde_json::to_string(&info).unwrap(); + assert!(json.contains("isSynced")); + assert!(json.contains("blockHeight")); + } +} diff --git a/apps/desktop-wallet/src/App.tsx b/apps/desktop-wallet/src/App.tsx index 40343da..a1f9282 100644 --- a/apps/desktop-wallet/src/App.tsx +++ b/apps/desktop-wallet/src/App.tsx @@ -8,6 +8,8 @@ import UpdateBanner from './components/UpdateBanner'; // Hooks import { useTrayEvents } from './hooks/useTrayEvents'; +import { useNodeEvents } from './hooks/useNodeEvents'; +import { useMiningEvents } from './hooks/useMiningEvents'; // Pages import Welcome from './pages/Welcome'; @@ -19,6 +21,8 @@ import Send from './pages/Send'; import Receive from './pages/Receive'; import History from './pages/History'; import Settings from './pages/Settings'; +import NodeDashboard from './pages/Node/NodeDashboard'; +import MiningDashboard from './pages/Mining/MiningDashboard'; function App() { const { isInitialized, isUnlocked } = useWalletStore(); @@ -26,6 +30,10 @@ function App() { // Listen for system tray events useTrayEvents(); + // Setup node and mining event listeners + useNodeEvents(); + useMiningEvents(); + return (
{/* Update notification banner */} @@ -80,6 +88,18 @@ function App() { isUnlocked ? : } /> + : + } + /> + : + } + /> state.status); + const miningStatus = useMiningStore((state) => state.status); const handleLock = async () => { await lockWallet(); @@ -63,27 +74,71 @@ export default function Layout() { {label} ))} + + {/* Separator */} +
+

+ Advanced +

+
+ + {/* Advanced nav items with status indicators */} + {advancedNavItems.map(({ to, label, icon: Icon }) => ( + + `flex items-center justify-between px-4 py-3 rounded-lg transition-colors ${ + isActive + ? 'bg-synor-600 text-white' + : 'text-gray-400 hover:text-white hover:bg-gray-800' + }` + } + > +
+ + {label} +
+ {/* Status indicators */} + {to === '/node' && nodeStatus.isConnected && ( + + )} + {to === '/mining' && miningStatus.isMining && ( + + {formatHashrate(miningStatus.hashrate)} + + )} +
+ ))} {/* Footer */}
- {/* Network status */} + {/* Node status */}
- {networkStatus.connected ? ( + {nodeStatus.isConnected ? ( <> - {networkStatus.network || 'Connected'} + {nodeStatus.network || 'Connected'} + {nodeStatus.isSyncing && ' (Syncing...)'} ) : ( <> - Disconnected + Not Connected )}
+ {/* Block height */} + {nodeStatus.isConnected && nodeStatus.blockHeight > 0 && ( +
+ Block #{nodeStatus.blockHeight.toLocaleString()} +
+ )} + {/* Lock button */} + + + ) : ( + + )} +
+
+ + {/* Error display */} + {error && ( +
+ {error} +
+ )} + + {/* Warning if not connected */} + {!nodeStatus.isConnected && ( +
+ Please connect to a node before starting mining +
+ )} + + {/* Stats Grid */} +
+ } + label="Hashrate" + value={formatHashrate(status.hashrate)} + highlight={status.isMining && !status.isPaused} + /> + } + label="Blocks Found" + value={status.blocksFound.toString()} + /> + } + label="Threads" + value={`${status.threads || threads}/${maxThreads}`} + /> + } + label="Status" + value={ + status.isMining + ? status.isPaused + ? 'Paused' + : 'Mining' + : 'Idle' + } + highlight={status.isMining && !status.isPaused} + /> +
+ + {/* Configuration (when not mining) */} + {!status.isMining && ( +
+

+ Mining Configuration +

+
+ {/* Coinbase address */} +
+ + +

+ Mining rewards will be sent to this address +

+
+ + {/* Thread slider */} +
+ + handleThreadsChange(parseInt(e.target.value))} + className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-synor-500" + /> +
+ 1 (Low Power) + {maxThreads} (Max Performance) +
+
+
+
+ )} + + {/* Hashrate Chart (simplified) */} + {status.isMining && hashrateHistory.length > 0 && ( +
+

+ + Hashrate History +

+
+ {hashrateHistory.slice(-60).map((point, i) => { + const maxHash = Math.max(...hashrateHistory.map((h) => h.hashrate)); + const height = maxHash > 0 ? (point.hashrate / maxHash) * 100 : 0; + return ( +
+ ); + })} +
+
+ 60s ago + Now +
+
+ )} + + {/* Recent Blocks */} + {recentBlocks.length > 0 && ( +
+

+ + Blocks Found +

+
+ {recentBlocks.slice(0, 5).map((block, i) => ( +
+
+

+ Block #{block.height.toLocaleString()} +

+

+ {block.hash.slice(0, 24)}... +

+
+
+

+ +{(block.reward / 100_000_000).toFixed(2)} SYN +

+

+ {new Date(block.timestamp).toLocaleTimeString()} +

+
+
+ ))} +
+
+ )} + + {/* Mining Tips */} +
+

Mining Tips

+
    +
  • • Use fewer threads to keep your computer responsive
  • +
  • • Mining profitability depends on network difficulty
  • +
  • • Ensure adequate cooling for sustained mining
  • +
  • • For best results, use an embedded node for lower latency
  • +
+
+
+ ); +} + +// Stat card component +function StatCard({ + icon, + label, + value, + highlight = false, +}: { + icon: React.ReactNode; + label: string; + value: string; + highlight?: boolean; +}) { + return ( +
+
+ {icon} + {label} +
+

+ {value} +

+
+ ); +} diff --git a/apps/desktop-wallet/src/pages/Node/NodeDashboard.tsx b/apps/desktop-wallet/src/pages/Node/NodeDashboard.tsx new file mode 100644 index 0000000..5588135 --- /dev/null +++ b/apps/desktop-wallet/src/pages/Node/NodeDashboard.tsx @@ -0,0 +1,343 @@ +import { useEffect, useState } from 'react'; +import { + Server, + Wifi, + WifiOff, + Users, + Blocks, + RefreshCw, + Power, + PowerOff, + Globe, + HardDrive, +} from 'lucide-react'; +import { useNodeStore, ConnectionMode } from '../../store/node'; + +export default function NodeDashboard() { + const { + status, + syncProgress, + peers, + preferredMode, + lastExternalUrl, + lastNetwork, + connectExternal, + startEmbeddedNode, + disconnect, + refreshStatus, + refreshPeers, + setupEventListeners, + cleanupEventListeners, + } = useNodeStore(); + + const [isConnecting, setIsConnecting] = useState(false); + const [externalUrl, setExternalUrl] = useState(lastExternalUrl); + const [embeddedNetwork, setEmbeddedNetwork] = useState(lastNetwork); + const [error, setError] = useState(null); + + // Setup event listeners on mount + useEffect(() => { + setupEventListeners(); + return () => cleanupEventListeners(); + }, [setupEventListeners, cleanupEventListeners]); + + // Refresh status periodically when connected + useEffect(() => { + if (!status.isConnected) return; + + const interval = setInterval(() => { + refreshStatus(); + refreshPeers(); + }, 5000); + + return () => clearInterval(interval); + }, [status.isConnected, refreshStatus, refreshPeers]); + + const handleConnectExternal = async () => { + setIsConnecting(true); + setError(null); + try { + await connectExternal(externalUrl); + } catch (err) { + setError(err instanceof Error ? err.message : 'Connection failed'); + } finally { + setIsConnecting(false); + } + }; + + const handleStartEmbedded = async () => { + setIsConnecting(true); + setError(null); + try { + await startEmbeddedNode({ network: embeddedNetwork }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to start node'); + } finally { + setIsConnecting(false); + } + }; + + const handleDisconnect = async () => { + setError(null); + try { + await disconnect(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Disconnect failed'); + } + }; + + const getModeLabel = (mode: ConnectionMode): string => { + switch (mode.type) { + case 'disconnected': + return 'Disconnected'; + case 'external': + return `External: ${mode.http_url}`; + case 'embedded': + return `Embedded: ${mode.network}`; + } + }; + + return ( +
+ {/* Header */} +
+
+

+ + Node +

+

+ Manage your connection to the Synor network +

+
+ + {status.isConnected && ( + + )} +
+ + {/* Error display */} + {error && ( +
+ {error} +
+ )} + + {/* Connection Status Card */} +
+
+ {status.isConnected ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+

+ {status.isConnected ? 'Connected' : 'Not Connected'} +

+

{getModeLabel(status.mode)}

+
+
+ + {/* Sync progress bar */} + {status.isConnected && status.isSyncing && syncProgress && ( +
+
+ Syncing... + {syncProgress.progress.toFixed(1)}% +
+
+
+
+

{syncProgress.status}

+
+ )} +
+ + {/* Stats Grid (when connected) */} + {status.isConnected && ( +
+ } + label="Block Height" + value={status.blockHeight.toLocaleString()} + /> + } + label="Blue Score" + value={status.blueScore.toLocaleString()} + /> + } + label="Peers" + value={status.peerCount.toString()} + /> + } + label="Network" + value={status.network || 'Unknown'} + /> +
+ )} + + {/* Connection Options (when disconnected) */} + {!status.isConnected && ( +
+ {/* External Node */} +
+
+ +

External Node

+
+

+ Connect to a remote Synor node via RPC. No local resources required. +

+
+ setExternalUrl(e.target.value)} + placeholder="http://localhost:16110" + className="w-full px-4 py-3 rounded-lg bg-gray-800 border border-gray-700 text-white placeholder-gray-500 focus:outline-none focus:border-synor-500" + /> + +
+
+ + {/* Embedded Node */} +
+
+ +

Embedded Node

+
+

+ Run a full node inside the wallet. Requires more resources but provides maximum security. +

+
+ + +

+ Requires embedded-node feature enabled +

+
+
+
+ )} + + {/* Peers List (when connected) */} + {status.isConnected && peers.length > 0 && ( +
+
+

+ + Connected Peers +

+ +
+
+ {peers.slice(0, 10).map((peer) => ( +
+
+
+
+

+ {peer.peerId.slice(0, 16)}... +

+

{peer.address}

+
+
+
+

{peer.direction}

+ {peer.latencyMs !== undefined && ( +

{peer.latencyMs}ms

+ )} +
+
+ ))} +
+ {peers.length > 10 && ( +

+ And {peers.length - 10} more peers... +

+ )} +
+ )} +
+ ); +} + +// Stat card component +function StatCard({ + icon, + label, + value, +}: { + icon: React.ReactNode; + label: string; + value: string; +}) { + return ( +
+
+ {icon} + {label} +
+

{value}

+
+ ); +} diff --git a/apps/desktop-wallet/src/store/index.ts b/apps/desktop-wallet/src/store/index.ts new file mode 100644 index 0000000..de10f99 --- /dev/null +++ b/apps/desktop-wallet/src/store/index.ts @@ -0,0 +1,15 @@ +// Store exports +export { useWalletStore } from './wallet'; +export type { WalletAddress, Balance, NetworkStatus } from './wallet'; + +export { useNodeStore, useIsConnected, useBlockHeight, useIsSyncing } from './node'; +export type { ConnectionMode, NodeStatus, SyncProgress, PeerInfo } from './node'; + +export { + useMiningStore, + useIsMiningActive, + useHashrate, + useBlocksFound, + formatHashrate, +} from './mining'; +export type { MiningStatus, MiningStats, BlockFoundEvent } from './mining'; diff --git a/apps/desktop-wallet/src/store/mining.ts b/apps/desktop-wallet/src/store/mining.ts new file mode 100644 index 0000000..0f200a2 --- /dev/null +++ b/apps/desktop-wallet/src/store/mining.ts @@ -0,0 +1,310 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; +import { invoke } from '@tauri-apps/api/core'; +import { listen, UnlistenFn } from '@tauri-apps/api/event'; + +/** + * Sanitized error logging + */ +function logError(context: string, error: unknown): void { + if (import.meta.env.PROD) { + const errorType = error instanceof Error ? error.name : 'Unknown'; + console.error(`[Mining] ${context}: ${errorType}`); + } else { + console.error(`[Mining] ${context}:`, error); + } +} + +// ============================================================================ +// Types +// ============================================================================ + +export interface MiningStatus { + isMining: boolean; + isPaused: boolean; + hashrate: number; + blocksFound: number; + sharesSubmitted: number; + threads: number; + coinbaseAddress?: string; +} + +export interface MiningStats { + hashrate: number; + avgHashrate: number; + peakHashrate: number; + blocksFound: number; + blocksRejected: number; + estimatedDailyCoins: number; + uptimeSeconds: number; + threadHashrates: number[]; +} + +export interface BlockFoundEvent { + height: number; + hash: string; + reward: number; + timestamp: number; +} + +// ============================================================================ +// Store +// ============================================================================ + +interface MiningState { + // Status + status: MiningStatus; + stats: MiningStats | null; + recentBlocks: BlockFoundEvent[]; + hashrateHistory: { timestamp: number; hashrate: number }[]; + + // Settings (persisted) + defaultThreads: number; + defaultCoinbaseAddress: string; + autoStartMining: boolean; + + // Event listener cleanup + _unlisteners: UnlistenFn[]; + + // Actions + setStatus: (status: MiningStatus) => void; + setStats: (stats: MiningStats | null) => void; + addBlockFound: (block: BlockFoundEvent) => void; + addHashratePoint: (hashrate: number) => void; + setDefaultThreads: (threads: number) => void; + setDefaultCoinbaseAddress: (address: string) => void; + setAutoStartMining: (autoStart: boolean) => void; + + // Async actions + startMining: (coinbaseAddress: string, threads: number) => Promise; + stopMining: () => Promise; + pauseMining: () => Promise; + resumeMining: () => Promise; + setThreads: (threads: number) => Promise; + refreshStatus: () => Promise; + refreshStats: () => Promise; + + // Event listeners + setupEventListeners: () => Promise; + cleanupEventListeners: () => void; +} + +const initialStatus: MiningStatus = { + isMining: false, + isPaused: false, + hashrate: 0, + blocksFound: 0, + sharesSubmitted: 0, + threads: 0, + coinbaseAddress: undefined, +}; + +export const useMiningStore = create()( + persist( + (set, get) => ({ + // Initial state + status: initialStatus, + stats: null, + recentBlocks: [], + hashrateHistory: [], + defaultThreads: navigator.hardwareConcurrency || 4, + defaultCoinbaseAddress: '', + autoStartMining: false, + _unlisteners: [], + + // Sync setters + setStatus: (status) => set({ status }), + setStats: (stats) => set({ stats }), + + addBlockFound: (block) => + set((state) => ({ + recentBlocks: [block, ...state.recentBlocks].slice(0, 50), // Keep last 50 + })), + + addHashratePoint: (hashrate) => + set((state) => ({ + hashrateHistory: [ + ...state.hashrateHistory, + { timestamp: Date.now(), hashrate }, + ].slice(-300), // Keep last 5 minutes (assuming 1s intervals) + })), + + setDefaultThreads: (threads) => set({ defaultThreads: threads }), + setDefaultCoinbaseAddress: (address) => + set({ defaultCoinbaseAddress: address }), + setAutoStartMining: (autoStart) => set({ autoStartMining: autoStart }), + + // Async actions + startMining: async (coinbaseAddress: string, threads: number) => { + try { + const status = await invoke('mining_start', { + coinbaseAddress, + threads, + }); + set({ + status, + defaultCoinbaseAddress: coinbaseAddress, + defaultThreads: threads, + }); + } catch (error) { + logError('startMining', error); + throw error; + } + }, + + stopMining: async () => { + try { + await invoke('mining_stop'); + set({ + status: initialStatus, + stats: null, + }); + } catch (error) { + logError('stopMining', error); + throw error; + } + }, + + pauseMining: async () => { + try { + await invoke('mining_pause'); + set((state) => ({ + status: { ...state.status, isPaused: true }, + })); + } catch (error) { + logError('pauseMining', error); + throw error; + } + }, + + resumeMining: async () => { + try { + await invoke('mining_resume'); + set((state) => ({ + status: { ...state.status, isPaused: false }, + })); + } catch (error) { + logError('resumeMining', error); + throw error; + } + }, + + setThreads: async (threads: number) => { + try { + await invoke('mining_set_threads', { threads }); + set((state) => ({ + status: { ...state.status, threads }, + defaultThreads: threads, + })); + } catch (error) { + logError('setThreads', error); + throw error; + } + }, + + refreshStatus: async () => { + try { + const status = await invoke('mining_get_status'); + set({ status }); + } catch (error) { + logError('refreshStatus', error); + } + }, + + refreshStats: async () => { + try { + const stats = await invoke('mining_get_stats'); + set({ stats }); + // Also add to hashrate history + get().addHashratePoint(stats.hashrate); + } catch (error) { + logError('refreshStats', error); + } + }, + + // Event listeners for real-time updates + setupEventListeners: async () => { + const unlisteners: UnlistenFn[] = []; + + // Listen for mining stats updates + const unlistenStats = await listen( + 'mining:stats-update', + (event) => { + set({ stats: event.payload }); + get().addHashratePoint(event.payload.hashrate); + } + ); + unlisteners.push(unlistenStats); + + // Listen for block found events + const unlistenBlock = await listen( + 'mining:block-found', + (event) => { + get().addBlockFound(event.payload); + // Refresh status to update block count + get().refreshStatus(); + } + ); + unlisteners.push(unlistenBlock); + + set({ _unlisteners: unlisteners }); + }, + + cleanupEventListeners: () => { + const { _unlisteners } = get(); + for (const unlisten of _unlisteners) { + unlisten(); + } + set({ _unlisteners: [] }); + }, + }), + { + name: 'synor-mining-storage', + partialize: (state) => ({ + defaultThreads: state.defaultThreads, + defaultCoinbaseAddress: state.defaultCoinbaseAddress, + autoStartMining: state.autoStartMining, + }), + } + ) +); + +// ============================================================================ +// Helper Hooks +// ============================================================================ + +/** + * Returns true if currently mining (and not paused) + */ +export function useIsMiningActive(): boolean { + return useMiningStore((state) => state.status.isMining && !state.status.isPaused); +} + +/** + * Returns the current hashrate + */ +export function useHashrate(): number { + return useMiningStore((state) => state.status.hashrate); +} + +/** + * Returns total blocks found + */ +export function useBlocksFound(): number { + return useMiningStore((state) => state.status.blocksFound); +} + +/** + * Formats hashrate for display + */ +export function formatHashrate(hashrate: number): string { + if (hashrate < 1000) { + return `${hashrate.toFixed(2)} H/s`; + } else if (hashrate < 1_000_000) { + return `${(hashrate / 1000).toFixed(2)} KH/s`; + } else if (hashrate < 1_000_000_000) { + return `${(hashrate / 1_000_000).toFixed(2)} MH/s`; + } else { + return `${(hashrate / 1_000_000_000).toFixed(2)} GH/s`; + } +} diff --git a/apps/desktop-wallet/src/store/node.ts b/apps/desktop-wallet/src/store/node.ts new file mode 100644 index 0000000..780a48c --- /dev/null +++ b/apps/desktop-wallet/src/store/node.ts @@ -0,0 +1,289 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; +import { invoke } from '@tauri-apps/api/core'; +import { listen, UnlistenFn } from '@tauri-apps/api/event'; + +/** + * Sanitized error logging + */ +function logError(context: string, error: unknown): void { + if (import.meta.env.PROD) { + const errorType = error instanceof Error ? error.name : 'Unknown'; + console.error(`[Node] ${context}: ${errorType}`); + } else { + console.error(`[Node] ${context}:`, error); + } +} + +// ============================================================================ +// Types +// ============================================================================ + +export type ConnectionMode = + | { type: 'disconnected' } + | { type: 'external'; http_url: string; ws_url?: string } + | { type: 'embedded'; network: string; data_dir?: string }; + +export interface NodeStatus { + mode: ConnectionMode; + isConnected: boolean; + blockHeight: number; + blueScore: number; + peerCount: number; + isSyncing: boolean; + syncProgress: number; + network: string; + chainId: number; +} + +export interface SyncProgress { + currentHeight: number; + targetHeight: number; + progress: number; + etaSeconds?: number; + status: string; +} + +export interface PeerInfo { + peerId: string; + address: string; + direction: 'inbound' | 'outbound'; + latencyMs?: number; + blockHeight: number; + status: string; +} + +// ============================================================================ +// Store +// ============================================================================ + +interface NodeState { + // Status + status: NodeStatus; + syncProgress: SyncProgress | null; + peers: PeerInfo[]; + + // Connection preferences (persisted) + preferredMode: 'external' | 'embedded'; + lastExternalUrl: string; + lastNetwork: string; + + // Event listener cleanup + _unlisteners: UnlistenFn[]; + + // Actions + setStatus: (status: NodeStatus) => void; + setSyncProgress: (progress: SyncProgress | null) => void; + setPeers: (peers: PeerInfo[]) => void; + setPreferredMode: (mode: 'external' | 'embedded') => void; + + // Async actions + connectExternal: (httpUrl: string, wsUrl?: string) => Promise; + startEmbeddedNode: (options: { + network: string; + dataDir?: string; + miningEnabled?: boolean; + coinbaseAddress?: string; + miningThreads?: number; + }) => Promise; + disconnect: () => Promise; + refreshStatus: () => Promise; + refreshPeers: () => Promise; + + // Event listeners + setupEventListeners: () => Promise; + cleanupEventListeners: () => void; +} + +const initialStatus: NodeStatus = { + mode: { type: 'disconnected' }, + isConnected: false, + blockHeight: 0, + blueScore: 0, + peerCount: 0, + isSyncing: false, + syncProgress: 0, + network: '', + chainId: 0, +}; + +export const useNodeStore = create()( + persist( + (set, get) => ({ + // Initial state + status: initialStatus, + syncProgress: null, + peers: [], + preferredMode: 'external', + lastExternalUrl: 'http://localhost:16110', + lastNetwork: 'testnet', + _unlisteners: [], + + // Sync setters + setStatus: (status) => set({ status }), + setSyncProgress: (progress) => set({ syncProgress: progress }), + setPeers: (peers) => set({ peers }), + setPreferredMode: (mode) => set({ preferredMode: mode }), + + // Async actions + connectExternal: async (httpUrl: string, wsUrl?: string) => { + try { + const status = await invoke('node_connect_external', { + httpUrl, + wsUrl, + }); + set({ + status: { ...status, isConnected: true }, + lastExternalUrl: httpUrl, + }); + } catch (error) { + logError('connectExternal', error); + throw error; + } + }, + + startEmbeddedNode: async (options) => { + try { + const status = await invoke('node_start_embedded', { + network: options.network, + dataDir: options.dataDir, + miningEnabled: options.miningEnabled ?? false, + coinbaseAddress: options.coinbaseAddress, + miningThreads: options.miningThreads ?? 0, + }); + set({ + status: { ...status, isConnected: true }, + lastNetwork: options.network, + preferredMode: 'embedded', + }); + } catch (error) { + logError('startEmbeddedNode', error); + throw error; + } + }, + + disconnect: async () => { + try { + await invoke('node_stop'); + set({ + status: initialStatus, + syncProgress: null, + peers: [], + }); + } catch (error) { + logError('disconnect', error); + throw error; + } + }, + + refreshStatus: async () => { + try { + const status = await invoke('node_get_status'); + set({ status }); + } catch (error) { + logError('refreshStatus', error); + } + }, + + refreshPeers: async () => { + try { + const peers = await invoke('node_get_peers'); + set({ peers }); + } catch (error) { + logError('refreshPeers', error); + } + }, + + // Event listeners for real-time updates + setupEventListeners: async () => { + const unlisteners: UnlistenFn[] = []; + + // Listen for status changes + const unlistenStatus = await listen( + 'node:status-changed', + (event) => { + set({ status: event.payload }); + } + ); + unlisteners.push(unlistenStatus); + + // Listen for sync progress + const unlistenSync = await listen( + 'node:sync-progress', + (event) => { + set({ syncProgress: event.payload }); + } + ); + unlisteners.push(unlistenSync); + + // Listen for new blocks + const unlistenBlock = await listen<{ height: number; hash: string }>( + 'node:new-block', + (event) => { + const current = get().status; + set({ + status: { + ...current, + blockHeight: event.payload.height, + }, + }); + } + ); + unlisteners.push(unlistenBlock); + + // Listen for peer connections + const unlistenPeer = await listen( + 'node:peer-connected', + () => { + // Refresh peer list when new peer connects + get().refreshPeers(); + } + ); + unlisteners.push(unlistenPeer); + + set({ _unlisteners: unlisteners }); + }, + + cleanupEventListeners: () => { + const { _unlisteners } = get(); + for (const unlisten of _unlisteners) { + unlisten(); + } + set({ _unlisteners: [] }); + }, + }), + { + name: 'synor-node-storage', + partialize: (state) => ({ + preferredMode: state.preferredMode, + lastExternalUrl: state.lastExternalUrl, + lastNetwork: state.lastNetwork, + }), + } + ) +); + +// ============================================================================ +// Helper Hooks +// ============================================================================ + +/** + * Returns true if connected to any node (embedded or external) + */ +export function useIsConnected(): boolean { + return useNodeStore((state) => state.status.isConnected); +} + +/** + * Returns the current block height + */ +export function useBlockHeight(): number { + return useNodeStore((state) => state.status.blockHeight); +} + +/** + * Returns the current sync status + */ +export function useIsSyncing(): boolean { + return useNodeStore((state) => state.status.isSyncing); +}