diff --git a/biome.json b/biome.json index 2f50cf5..5024050 100644 --- a/biome.json +++ b/biome.json @@ -19,5 +19,17 @@ "formatter": { "quoteStyle": "double" } - } + }, + "overrides": [ + { + "includes": ["**/*.css"], + "linter": { + "rules": { + "suspicious": { + "noUnknownAtRules": "off" + } + } + } + } + ] } diff --git a/bun.lock b/bun.lock index 50e4e03..e79ea8c 100644 --- a/bun.lock +++ b/bun.lock @@ -14,6 +14,25 @@ "packages/app": { "name": "@bitcoinmints/app", "version": "0.0.0", + "dependencies": { + "@bitcoinmints/core": "workspace:*", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "dexie-react-hooks": "^4.4.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "tailwind-merge": "^3.5.0", + "zustand": "^5.0.12", + }, + "devDependencies": { + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^6.0.1", + "autoprefixer": "^10.5.0", + "postcss": "^8.5.10", + "tailwindcss": "^3.4.19", + "vite": "^8.0.8", + }, }, "packages/core": { "name": "@bitcoinmints/core", @@ -28,6 +47,8 @@ }, }, "packages": { + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@biomejs/biome": ["@biomejs/biome@2.3.12", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.12", "@biomejs/cli-darwin-x64": "2.3.12", "@biomejs/cli-linux-arm64": "2.3.12", "@biomejs/cli-linux-arm64-musl": "2.3.12", "@biomejs/cli-linux-x64": "2.3.12", "@biomejs/cli-linux-x64-musl": "2.3.12", "@biomejs/cli-win32-arm64": "2.3.12", "@biomejs/cli-win32-x64": "2.3.12" }, "bin": { "biome": "bin/biome" } }, "sha512-AR7h4aSlAvXj7TAajW/V12BOw2EiS0AqZWV5dGozf4nlLoUF/ifvD0+YgKSskT0ylA6dY1A8AwgP8kZ6yaCQnA=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cO6fn+KiMBemva6EARDLQBxeyvLzgidaFRJi8G7OeRqz54kWK0E+uSjgFaiHlc3DZYoa0+1UFE8mDxozpc9ieg=="], @@ -56,8 +77,14 @@ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], "@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], @@ -66,6 +93,12 @@ "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@oxc-project/types": ["@oxc-project/types@0.124.0", "", {}, "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg=="], "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.15", "", { "os": "android", "cpu": "arm64" }, "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA=="], @@ -98,7 +131,7 @@ "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.15", "", { "os": "win32", "cpu": "x64" }, "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.15", "", {}, "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="], "@scure/base": ["@scure/base@2.0.0", "", {}, "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w=="], @@ -118,6 +151,12 @@ "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="], + "@vitest/expect": ["@vitest/expect@4.1.4", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.4", "@vitest/utils": "4.1.4", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww=="], "@vitest/mocker": ["@vitest/mocker@4.1.4", "", { "dependencies": { "@vitest/spy": "4.1.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg=="], @@ -132,28 +171,98 @@ "@vitest/utils": ["@vitest/utils@4.1.4", "", { "dependencies": { "@vitest/pretty-format": "4.1.4", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw=="], + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "autoprefixer": ["autoprefixer@10.5.0", "", { "dependencies": { "browserslist": "^4.28.2", "caniuse-lite": "^1.0.30001787", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + + "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001788", "", {}, "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "dexie": ["dexie@4.4.2", "", {}, "sha512-zMtV8q79EFE5U8FKZvt0Y/77PCU/Hr/RDxv1EDeo228L+m/HTbeN2AjoQm674rhQCX8n3ljK87lajt7UQuZfvw=="], + "dexie-react-hooks": ["dexie-react-hooks@4.4.0", "", { "peerDependencies": { "dexie": ">=4.2.0-alpha.1 <5.0.0", "react": ">=16" } }, "sha512-ObLXBS5+4BJU8vtSvBx6b9fY6zZYgniAtwxzjCHsUQadgbqYN6935X2/1TWw4Rf2N1aZV1io5/ziox4vKuxABA=="], + + "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], + + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.340", "", {}, "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], "fake-indexeddb": ["fake-indexeddb@6.2.5", "", {}, "sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w=="], + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], @@ -178,26 +287,80 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + "nostr-tools": ["nostr-tools@2.23.3", "", { "dependencies": { "@noble/ciphers": "2.1.1", "@noble/curves": "2.0.1", "@noble/hashes": "2.0.1", "@scure/base": "2.0.0", "@scure/bip32": "2.0.1", "@scure/bip39": "2.0.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-AALyt9k8xPdF4UV2mlLJ2mgCn4kpTB0DZ8t2r6wjdUh6anfx2cTVBsHUlo9U0EY/cKC5wcNyiMAmRJV5OVEalA=="], "nostr-wasm": ["nostr-wasm@0.1.0", "", {}, "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + "postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="], + "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], + + "postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="], + + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], + + "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="], + + "react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="], + + "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], + + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rolldown": ["rolldown@1.0.0-rc.15", "", { "dependencies": { "@oxc-project/types": "=0.124.0", "@rolldown/pluginutils": "1.0.0-rc.15" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-x64": "1.0.0-rc.15", "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -206,6 +369,18 @@ "std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], + "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], + + "tailwindcss": ["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.2", "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.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], @@ -214,16 +389,38 @@ "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "vite": ["vite@8.0.8", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.15", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw=="], "vitest": ["vitest@4.1.4", "", { "dependencies": { "@vitest/expect": "4.1.4", "@vitest/mocker": "4.1.4", "@vitest/pretty-format": "4.1.4", "@vitest/runner": "4.1.4", "@vitest/snapshot": "4.1.4", "@vitest/spy": "4.1.4", "@vitest/utils": "4.1.4", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.4", "@vitest/browser-preview": "4.1.4", "@vitest/browser-webdriverio": "4.1.4", "@vitest/coverage-istanbul": "4.1.4", "@vitest/coverage-v8": "4.1.4", "@vitest/ui": "4.1.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg=="], "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "zustand": ["zustand@5.0.12", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g=="], + + "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.15", "", {}, "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g=="], } } diff --git a/package.json b/package.json index 36078ff..2d220cd 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "check": "biome check .", "format": "biome format --write .", "typecheck": "bun --filter='*' run typecheck", - "test": "bun --filter='*' run test" + "test": "bun --filter='*' run test", + "dev": "bun --filter='@bitcoinmints/app' run dev", + "build": "bun --filter='@bitcoinmints/app' run build" }, "devDependencies": { "@biomejs/biome": "2.3.12", diff --git a/packages/app/components.json b/packages/app/components.json new file mode 100644 index 0000000..6296a74 --- /dev/null +++ b/packages/app/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/packages/app/index.html b/packages/app/index.html new file mode 100644 index 0000000..f90549b --- /dev/null +++ b/packages/app/index.html @@ -0,0 +1,12 @@ + + + + + + bitcoinmints — data dump (PR #6) + + +
+ + + diff --git a/packages/app/package.json b/packages/app/package.json index 48d3015..bd35930 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -4,7 +4,29 @@ "private": true, "type": "module", "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", "test": "vitest run --passWithNoTests", "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@bitcoinmints/core": "workspace:*", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "dexie-react-hooks": "^4.4.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "tailwind-merge": "^3.5.0", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^6.0.1", + "autoprefixer": "^10.5.0", + "postcss": "^8.5.10", + "tailwindcss": "^3.4.19", + "vite": "^8.0.8" } } diff --git a/packages/app/postcss.config.js b/packages/app/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/packages/app/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx new file mode 100644 index 0000000..ad4e99f --- /dev/null +++ b/packages/app/src/App.tsx @@ -0,0 +1,162 @@ +import { + BitcoinmintsDB, + createMintInfoFetcher, + createPool, + createScheduler, + getSubscribedKinds, + type Scheduler, + type SchedulerStats, + SEED_RELAYS, +} from "@bitcoinmints/core"; +import { type JSX, useEffect, useState } from "react"; +import { MintList } from "./components/MintList"; + +/** + * Human labels for each subscribed kind, aligned with the validation-paths + * table below. The "firehose — no authors" annotation on kinds 0 and 10002 + * matches PR #30's review callout: we subscribe without an `authors` + * restriction, so those filters are fundamentally unbounded. Visible in the + * X-ray so the demo doesn't need a footnote. + */ +const KIND_LABELS: Record = { + 38172: "cashu announcements", + 38173: "fedimint announcements", + 38000: "reviews", + 0: "profiles (firehose — no authors)", + 10002: "relay lists (firehose — no authors)", +}; + +/** + * The one and only route. No router: a single `/` view dumping everything + * Dexie has. + * + * Boot discipline: + * - db + pool + fetcher + scheduler are created exactly once at module + * load (outside the component) so React 19 StrictMode's double-invoke + * of effects in dev can't produce two schedulers fighting over the + * same Dexie. The effect body then does `scheduler.start()` on every + * mount and `scheduler.stop()` on every cleanup — the scheduler is + * idempotent across those calls (start resets `stopped`, stop drains + * in-flight Layer B work), so StrictMode's double-invoke produces + * start → stop → start exactly as intended rather than leaving us + * stuck after the first cleanup. + * + * Stats refresh: + * - getStats() returns a plain snapshot; we poll it on a 500ms ticker so + * the `
` at the top advances even when Dexie writes are quiet (the
+ *     Dexie-triggered `useLiveQuery` in MintList would otherwise be the
+ *     only re-render driver, and it only fires on row changes — not when
+ *     `eventsReceived` increments without a write).
+ */
+const db = new BitcoinmintsDB();
+const pool = createPool({ relays: [...SEED_RELAYS] });
+const fetcher = createMintInfoFetcher({ concurrency: 4 });
+/**
+ * Debug logging is a demo/X-ray aid, toggled via `?debug` on the URL (any
+ * presence wins; no value parsing). When enabled, the scheduler logs its
+ * filters/relays on start(), a per-event path line, and a per-Layer-B
+ * verdict line — all through `console.log`/`console.warn` with the
+ * `[scheduler]` prefix. Deliberately URL-toggled (not env-baked) so an
+ * alchemist can flip it on during a live demo without rebuilding.
+ */
+const DEBUG_SCHEDULER =
+  typeof window !== "undefined" && new URLSearchParams(window.location.search).has("debug");
+const scheduler: Scheduler = createScheduler({
+  db,
+  pool,
+  fetcher,
+  relays: SEED_RELAYS,
+  debug: DEBUG_SCHEDULER,
+});
+
+const STATS_POLL_MS = 500;
+
+export function App(): JSX.Element {
+  const [stats, setStats] = useState(scheduler.getStats());
+
+  useEffect(() => {
+    void scheduler.start();
+    const handle = window.setInterval(() => {
+      setStats(scheduler.getStats());
+    }, STATS_POLL_MS);
+    return () => {
+      window.clearInterval(handle);
+      // Fire-and-forget stop(): app unmount means page navigation away or
+      // dev-HMR, either way we want the subscription closed. We don't await
+      // because React's effect-cleanup contract is synchronous.
+      void scheduler.stop();
+    };
+  }, []);
+
+  // Static derivations for the X-ray — computed once per render, but the
+  // inputs are module-constants so React's reconciler is effectively a
+  // no-op on these blocks.
+  const kinds = getSubscribedKinds();
+  // The widest kind label is 5 chars (e.g. "38172"). Pad to align.
+  const filtersBlock = kinds
+    .map((k) => {
+      const label = KIND_LABELS[k] ?? "";
+      const padded = `{ kinds: [${String(k).padEnd(5, " ")}] }`;
+      return `  ${padded}     ${label}`;
+    })
+    .join("\n");
+  const relaysBlock = SEED_RELAYS.map((r) => `  ${r}`).join("\n");
+
+  return (
+    
+
scheduler stats
+
{JSON.stringify(stats, null, 2)}
+ {/* + Counter note: all scheduler stats are monotonically increasing + EXCEPT `layerBPending`, which is transient — it goes up on + enqueue and back down when Layer B completes. The alchemist + observed it "going up then down" during the prior demo; this + comment exists so the next viewer doesn't flag it as a bug. + */} +
counters: monotonic, except layerBPending (transient: enqueue↑ / complete↓)
+ +
+
filters in use
+
{filtersBlock}
+
relays
+
{relaysBlock}
+ +
+
validation paths
+
{VALIDATION_PATHS_TABLE}
+ +
+ +
+ ); +} + +/** + * Reference doc rendered in the X-ray — the kind → parser → gate → counter + * path for every subscribed kind. Kept as a plain string (not a React + * table) so it stays font-mono and terse alongside the other `
`
+ * blocks. Source of truth: scheduler/index.ts:onEvent switch.
+ *
+ * Hand-aligned fixed-width columns — edit with care. If a column grows
+ * past its width, widen the whole column rather than wrapping mid-row.
+ */
+const VALIDATION_PATHS_TABLE = `
+kind           parser                       Layer A gate                         Layer B                               counters
+─────          ──────                       ────────────                         ───────                               ────────
+38172 cashu    parseMintAnnouncement        upsertAnnouncement d-tag + spam      verifySignerBinding /v1/info ×        parse-null → drop
+               (needs d + ≥1 u)             check                                pubkey match                          rejected → rejectedByLayerA
+                                                                                                                       accepted → accepted + layerBPending
+
+38173 fedimint parseMintAnnouncement        upsertAnnouncement d-tag + spam      none                                  same as 38172 minus Layer B
+               (needs d + ≥1 u)             check
+
+38000 review   parseReview                  upsertReviewWithAggregate d-tag      none                                  parse-null → rejectedByParse
+                                            check                                                                      rejected → rejectedByLayerA
+                                                                                                                       accepted → accepted
+
+0 profile      toProfileRow                 upsertProfile                        none                                  null → drop
+                                                                                                                       accepted → accepted
+
+10002 relay    toRelayListRow               upsertRelayList                      none                                  same as kind 0
+list
+`;
diff --git a/packages/app/src/components/MintList.tsx b/packages/app/src/components/MintList.tsx
new file mode 100644
index 0000000..7d778c5
--- /dev/null
+++ b/packages/app/src/components/MintList.tsx
@@ -0,0 +1,97 @@
+import {
+  type AnnouncementRow,
+  type BitcoinmintsDB,
+  type MintAggregateRow,
+  type MintInfoRow,
+  rankMints,
+} from "@bitcoinmints/core";
+import { useLiveQuery } from "dexie-react-hooks";
+import type { JSX } from "react";
+import { MintRow } from "./MintRow";
+
+/**
+ * The whole list surface for PR #6 — spec is raw field dump per mint,
+ * sorted by `bayesianScore` DESC via `rankMints(db, 50)`.
+ *
+ * Join strategy: one unified `useLiveQuery` at this level pre-joins
+ * aggregate → announcement → mintInfo and hands `` fully-resolved
+ * props. Previously we had a per-row `useLiveQuery` which resolved a
+ * microtask after the aggregate query, causing a ~1s "(no announcement)"
+ * placeholder flash on reload. Single query kills the flash.
+ */
+type Props = {
+  db: BitcoinmintsDB;
+};
+
+type JoinedRow = {
+  aggregate: MintAggregateRow;
+  announcement: AnnouncementRow | undefined;
+  info: MintInfoRow | undefined;
+};
+
+export function MintList({ db }: Props): JSX.Element {
+  // Order: rankMints() returns aggregates sorted by bayesianScore DESC.
+  // A mint with no reviews yet has no aggregate row, so this list can
+  // lag behind `announcements` — that's intentional. PR #7 will decide
+  // whether to render un-reviewed announcements as a tail section; for
+  // the X-ray we follow the ranked-aggregate-as-truth posture.
+  //
+  // Render-filter note: we drop non-Cashu announcements (kind !== 38172)
+  // below so the X-ray matches spec v1 (Cashu-only). The parse layer still
+  // stores k=38173 (Fedimint) rows and `rankMints` still ranks them — PR
+  // #7+ may surface those elsewhere. Keep this filter in the UI; do NOT
+  // push it into `rankMints` (don't mutate core for a UI-only concern).
+  const rows = useLiveQuery(
+    async () => {
+      const aggregates = await rankMints(db, 50);
+      const ds = aggregates.map((a) => a.d);
+      const announcements = await db.announcements.where("d").anyOf(ds).toArray();
+      const infos = await db.mintInfo.bulkGet(ds);
+      // A single `d` CAN map to multiple announcements (different pubkeys).
+      // Map.set keeps whichever appears LAST in `toArray()`; that matches
+      // the previous per-row `.first()` behavior only by luck-of-insert-order.
+      // PR #7 will resolve the ambiguity properly.
+      const annByD = new Map(announcements.map((a) => [a.d, a]));
+      // bulkGet returns an array in the same order as the keys; index align.
+      return aggregates.map((agg, i) => ({
+        aggregate: agg,
+        announcement: annByD.get(agg.d),
+        info: infos[i],
+      }));
+    },
+    [db],
+    [],
+  );
+
+  // Render-only Cashu filter (see note above). Orphan aggregates (no
+  // announcement — shouldn't happen in practice) are also dropped
+  // defensively so we never flash a "(no announcement)" row.
+  const visible = rows.filter((r) => r.announcement !== undefined && r.announcement.kind === 38172);
+
+  // Empty state per spec: stats block still renders (that's in App.tsx),
+  // the `mints` header always renders, and if there's nothing to show the
+  // single line `no mints yet` sits below it.
+  if (visible.length === 0) {
+    return (
+      <>
+        
mints
+
no mints yet
+ + ); + } + + return ( + <> +
mints
+ {visible.map((row, i) => ( + + ))} + + ); +} diff --git a/packages/app/src/components/MintRow.tsx b/packages/app/src/components/MintRow.tsx new file mode 100644 index 0000000..06ba539 --- /dev/null +++ b/packages/app/src/components/MintRow.tsx @@ -0,0 +1,137 @@ +import type { AnnouncementRow, MintAggregateRow, MintInfoRow } from "@bitcoinmints/core"; +import type { JSX } from "react"; + +/** + * Two-tier X-ray render of a single mint. + * + * Tier 1 — human-readable top matter pulled out of `info.infoJson` (NUT-06): + * name, description, version, motd, contact, urls, rating/score/verified. + * + * Tier 0 — de-emphasized technical identifiers (d, pubkey, kind) pinned below + * the top matter for X-ray debugging; deliberately small + faded, not + * user-facing content. + * + * Tier 2 — collapsed
blocks for the raw mintInfo, announcement, and + * aggregate JSON, so the full shape (including rawTags, sig, etc.) stays + * inspectable without drowning the first glance. + * + * Browser-native
disclosure — no dep, no state, no animation. + */ +type Props = { + aggregate: MintAggregateRow; + announcement: AnnouncementRow | undefined; + info: MintInfoRow | undefined; + /** Final row in the list gets no trailing
. */ + isLast: boolean; +}; + +/** + * Render the NUT-06 `contact` field. The spec says it's an array of + * `{ method, info }` objects, but mints in the wild have been seen emitting + * the older array-of-tuples shape (`[[method, info], ...]`). Handle both + * defensively — anything that doesn't match either shape is skipped rather + * than crashing the row render. + */ +function renderContact(contact: unknown): string[] { + if (!Array.isArray(contact)) return []; + const lines: string[] = []; + for (const entry of contact) { + if (Array.isArray(entry) && entry.length >= 2) { + // Legacy tuple shape: ["email", "foo@bar"]. + const [method, value] = entry; + if (typeof method === "string" && typeof value === "string") { + lines.push(`contact: ${method} ${value}`); + } + continue; + } + if (entry && typeof entry === "object") { + // NUT-06 object shape: { method, info }. + const obj = entry as Record; + const method = obj.method; + const value = obj.info; + if (typeof method === "string" && typeof value === "string") { + lines.push(`contact: ${method} ${value}`); + } + } + } + return lines; +} + +export function MintRow({ aggregate, announcement, info, isLast }: Props): JSX.Element { + const body = info?.infoJson as Record | undefined; + + const name = typeof body?.name === "string" && body.name.length > 0 ? body.name : undefined; + const description = typeof body?.description === "string" ? body.description : undefined; + const version = typeof body?.version === "string" ? body.version : undefined; + const motd = typeof body?.motd === "string" ? body.motd : undefined; + const contactLines = renderContact(body?.contact); + + const urls = announcement?.u ?? []; + + const ratedCount = aggregate.ratedCount; + const reviewCount = aggregate.reviewCount; + const avg = aggregate.avgRating; + const ratingLine = + avg === null + ? `rating: — (${reviewCount} total, 0 rated)` + : `rating: ${avg}/5 (${ratedCount} ratings, ${reviewCount} total)`; + + const verifiedLabel = announcement + ? announcement.verifiedBySignerBinding === true + ? "verified" + : announcement.verifiedBySignerBinding === false + ? "unverified" + : "pending" + : "(no announcement)"; + + // Tier 0 identifiers — present even if announcement is missing so the + // dangling-aggregate state is still visible. + const pubkey = announcement?.pubkey ?? "(no announcement)"; + const d = aggregate.d; + const kind = announcement?.kind ?? "(no announcement)"; + + return ( + <> +
name: {name ?? "(no name)"}
+ {description &&
description: {description}
} + {urls.map((url) => ( +
url: {url}
+ ))} +
{ratingLine}
+
score: {aggregate.bayesianScore}
+
verified: {verifiedLabel}
+ {version &&
version: {version}
} + {motd &&
motd: {motd}
} + {contactLines.map((line) => ( +
{line}
+ ))} + +
+
d: {d}
+
pubkey: {pubkey}
+
kind: {String(kind)}
+
+ + {info ? ( +
+ raw mintInfo +
{JSON.stringify(info, null, 2)}
+
+ ) : ( +
mintInfo: (none)
+ )} + {announcement && ( +
+ raw announcement +
{JSON.stringify(announcement, null, 2)}
+
+ )} +
+ raw aggregate +
{JSON.stringify(aggregate, null, 2)}
+
+ + {!isLast &&
} + + ); +} diff --git a/packages/app/src/index.css b/packages/app/src/index.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/packages/app/src/index.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts deleted file mode 100644 index cb0ff5c..0000000 --- a/packages/app/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/app/src/lib/utils.ts b/packages/app/src/lib/utils.ts new file mode 100644 index 0000000..f1f0551 --- /dev/null +++ b/packages/app/src/lib/utils.ts @@ -0,0 +1,11 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +/** + * shadcn-style class-name merge helper. Not used in PR #6 (data-dump + * X-ray has no designed UI), but committed as part of the shadcn init so + * PR #7+ can drop in `shadcn add ` without reshaping imports. + */ +export function cn(...inputs: ClassValue[]): string { + return twMerge(clsx(inputs)); +} diff --git a/packages/app/src/main.tsx b/packages/app/src/main.tsx new file mode 100644 index 0000000..94013d9 --- /dev/null +++ b/packages/app/src/main.tsx @@ -0,0 +1,20 @@ +import { createRoot } from "react-dom/client"; +import { App } from "./App"; +import "./index.css"; + +const rootEl = document.getElementById("root"); +if (!rootEl) { + throw new Error("root element not found"); +} + +// StrictMode is intentionally omitted. The scheduler's idempotency is +// sequenced around `startReady` so stop() must await the in-progress +// start() before closing the handle; StrictMode's double-invoke of +// effects ends up no-opping the second start (the stopped flag latches +// true after cleanup 1 completes inside start's microtask-chained body) +// which leaves the app with no active relay subscription in dev. Not a +// correctness bug in the scheduler — it's doing what its contract says — +// but the useEffect dance for sync start + async stop under double-invoke +// is not worth the churn for a data-dump X-ray. Production mounts once. +// PR #7+ can revisit this if a more robust teardown pattern is needed. +createRoot(rootEl).render(); diff --git a/packages/app/tailwind.config.js b/packages/app/tailwind.config.js new file mode 100644 index 0000000..93aa364 --- /dev/null +++ b/packages/app/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./index.html", "./src/**/*.{ts,tsx}"], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/packages/app/tsconfig.json b/packages/app/tsconfig.json index bf5a36d..291db67 100644 --- a/packages/app/tsconfig.json +++ b/packages/app/tsconfig.json @@ -1,4 +1,12 @@ { "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + "paths": { + "@/*": ["./src/*"] + } + }, "include": ["src/**/*"] } diff --git a/packages/app/vite.config.ts b/packages/app/vite.config.ts new file mode 100644 index 0000000..8696102 --- /dev/null +++ b/packages/app/vite.config.ts @@ -0,0 +1,22 @@ +import path from "node:path"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +/** + * Vite config for the bitcoinmints X-ray app. + * + * This PR (#6) is the data-dump verifier — we want Vite + React + the + * workspace `@bitcoinmints/core` on the module graph and nothing more. + * PR #7+ will layer designed UI on top. + * + * The `@/*` alias matches shadcn's components.json so future scaffolding + * (PR #7+) drops in without reshaping imports. + */ +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "src"), + }, + }, +}); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 302e547..faba74b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -3,5 +3,6 @@ export * from "./cashu"; export * from "./nip87"; export * from "./nostr"; export * from "./reviews"; +export * from "./scheduler"; export const VERSION = "0.0.0"; diff --git a/packages/core/src/nostr/pool.test.ts b/packages/core/src/nostr/pool.test.ts index 5f50ada..9e758af 100644 --- a/packages/core/src/nostr/pool.test.ts +++ b/packages/core/src/nostr/pool.test.ts @@ -31,16 +31,22 @@ vi.mock("nostr-tools/pool", () => { import { createPool, SEED_RELAYS } from "./pool"; describe("SEED_RELAYS", () => { - it("exports exactly the five-relay default seed pool from the spec", () => { + it("exports exactly the seven-relay default seed pool from the spec", () => { // Top 3 are the ecosystem-consensus NIP-87 implementor defaults // (damus 6/6, nos.lol 5/6, primal 4/6 across 6 surveyed hardcoders). - // Last 2 are cashu-branded relays — thin on event count but part of the - // cashu community's curated NIP-87 surface. + // Next 3 are audited secondary relays with real k38172 event counts + // (nostr.mom 8, relay.nostr.wirednet.jp 4, relay.nostrplebs.com 2) + // — added to widen the Cashu catch past the power-law knee. Last is + // relay.cashumints.space (cashu-branded, thin on events but part of + // the community's curated NIP-87 surface). relay.8333.space was + // dropped per audit: defunct / handshake timeout. expect(SEED_RELAYS).toEqual([ "wss://nos.lol", "wss://relay.damus.io", "wss://relay.primal.net", - "wss://relay.8333.space", + "wss://nostr.mom", + "wss://relay.nostr.wirednet.jp", + "wss://relay.nostrplebs.com", "wss://relay.cashumints.space", ]); }); diff --git a/packages/core/src/nostr/pool.ts b/packages/core/src/nostr/pool.ts index 28ca6df..6304fb6 100644 --- a/packages/core/src/nostr/pool.ts +++ b/packages/core/src/nostr/pool.ts @@ -10,16 +10,25 @@ import { SimplePool } from "nostr-tools/pool"; * nos.lol + damus alone carry 98.4% of all historical NIP-87 events per * /srv/forge/projects/bitcoinmints/audit/relay-strategy-v1.md. * - * relay.8333.space + relay.cashumints.space are cashu-branded relays included - * for ecosystem citizenship — thin on event count but part of the cashu - * community's curated NIP-87 surface (8333 is cashu.me's extra default; - * cashumints.space appears in 2/6 implementor defaults). + * Extended with three audited secondary relays (nostr.mom 8 × k38172, + * relay.nostr.wirednet.jp 4, relay.nostrplebs.com 2) to widen the Cashu + * catch. The alchemist demo against the prior 5-relay seed found only 1 + * Cashu announcement on the wire — adding these pushes us past the + * power-law knee documented in the audit (§3 cumulative table). + * + * relay.cashumints.space is the sole cashu-branded holdover — thin on event + * count (only 4 historical events per audit) but part of the cashu + * community's curated NIP-87 surface. relay.8333.space was dropped: the + * audit reports a handshake timeout and classifies it as defunct despite + * matching the audit.8333 domain. */ export const SEED_RELAYS: readonly string[] = [ "wss://nos.lol", "wss://relay.damus.io", "wss://relay.primal.net", - "wss://relay.8333.space", + "wss://nostr.mom", + "wss://relay.nostr.wirednet.jp", + "wss://relay.nostrplebs.com", "wss://relay.cashumints.space", ]; diff --git a/packages/core/src/scheduler/index.test.ts b/packages/core/src/scheduler/index.test.ts index 518f335..31ce019 100644 --- a/packages/core/src/scheduler/index.test.ts +++ b/packages/core/src/scheduler/index.test.ts @@ -788,3 +788,54 @@ describe("scheduler — backoff cap", () => { await sched.stop(); }); }); + +describe("scheduler — debug logging opt-in", () => { + // We assert the negative-case contract: debug unset (and debug=false, + // exercised via the default) produces ZERO console.log calls from the + // scheduler. We don't assert the `debug=true` output verbatim — that + // log format is a demo/X-ray aid and asserting exact strings would + // brittle the tests against formatting tweaks. + it("default (debug unset) produces no scheduler console.log across a full pipeline run", async () => { + const db = await freshDB(); + const { pool, pushEvent } = makeFakePool(); + const pubkey = "02".padEnd(66, "7"); + const { fetcher } = makeFetcher({ "https://mint.example.com": pubkey }); + const sched = createScheduler({ db, pool, fetcher, relays: ["wss://test"] }); + + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + try { + await sched.start(); + // Exercise each kind's path so any path-level log would fire. + await pushEvent(makeAnnouncement({ pubkey, d: pubkey, u: ["https://mint.example.com"] })); + await pushEvent({ + id: "review-debug", + kind: 38000, + pubkey: "reviewer".padEnd(64, "0"), + created_at: 1_700_000_000, + tags: [ + ["k", "38172"], + ["d", pubkey], + ["rating", "5", "5"], + ], + content: "[5/5] fine", + sig: "fake", + }); + await pushEvent({ + id: "profile-debug", + kind: 0, + pubkey: "profile".padEnd(64, "0"), + created_at: 1_700_000_001, + tags: [], + content: "{}", + sig: "fake", + }); + await settle(); + await sched.stop(); + + // Zero calls: confirms debug-off is a pure no-op for logging cost. + expect(logSpy).not.toHaveBeenCalled(); + } finally { + logSpy.mockRestore(); + } + }); +}); diff --git a/packages/core/src/scheduler/index.ts b/packages/core/src/scheduler/index.ts index 2f8bb62..1e8fc36 100644 --- a/packages/core/src/scheduler/index.ts +++ b/packages/core/src/scheduler/index.ts @@ -134,11 +134,39 @@ export type SchedulerConfig = { relays: readonly string[]; /** Optional clock injector for deterministic backoff tests. Defaults to Date.now. */ now?: () => number; + /** + * Opt-in per-event debug logging. Default `false` — zero perf cost when + * off (no allocations, no logs). + * + * When `true`, logs through `console.log` / `console.warn` with the + * stable `[scheduler]` prefix: + * - on start(): the filters array being sent to relays + the configured + * relay list + * - per event (after the switch branch resolves): kind, id prefix, + * delivering relay, and the resolved path (accepted / rejected-* + * / dropped / replaced) + * - per Layer B resolution: kind/id/url + verdict (verified / + * failed: / transient) + * + * Keep the surface console-only — no structured logger is wired through + * the package. Intended as a demo/X-ray aid, not production telemetry. + */ + debug?: boolean; }; /** NIP-87 + supporting kinds. See data-model-v1.md §1 for the full list. */ const SUBSCRIBED_KINDS = [38172, 38173, 38000, 0, 10002] as const; +/** + * Expose the subscribed kinds tuple for UI consumers that want to render + * "filters in use" without duplicating the literal. Frozen through + * `as const` in the declaration above, so callers cannot mutate the + * underlying array. + */ +export function getSubscribedKinds(): readonly number[] { + return SUBSCRIBED_KINDS; +} + /** Initial backoff window for a failed mint URL: attempts=0 → 30s. */ const BASE_BACKOFF_MS = 30_000; /** Cap at 1 hour. */ @@ -269,6 +297,7 @@ function toRelayListRow(event: NostrEvent): RelayListRow | null { export function createScheduler(config: SchedulerConfig): Scheduler { const { db, pool, fetcher } = config; const now = config.now ?? Date.now; + const debug = config.debug ?? false; const stats: SchedulerStats = { eventsReceived: 0, @@ -552,6 +581,23 @@ export function createScheduler(config: SchedulerConfig): Scheduler { } else { stats.layerBFailed += 1; } + + if (debug) { + // Verdict mirrors `verdictForPersistence` shape: verified=true → + // verified; pubkey-mismatch → failed:; anything else → + // transient (the row stays null and will be retried). + let verdict: string; + if (result.verified) { + verdict = "verified"; + } else if (typeof result.reason === "string" && result.reason.startsWith("pubkey-mismatch")) { + verdict = `failed:${result.reason}`; + } else { + verdict = `transient:${result.reason ?? "unknown"}`; + } + console.log( + `[scheduler] layerB kind=38172 id=${row.eventId.slice(0, 8)} url=${persistedUrl} verdict=${verdict}`, + ); + } } /** @@ -590,8 +636,20 @@ export function createScheduler(config: SchedulerConfig): Scheduler { * gap). Log surface matches `reenqueueUnverified`'s existing pattern: * a `[scheduler]`-prefixed console call, no structured logger is wired * through the package yet. + * + * `relay` is the wss:// URL that delivered the event, threaded through + * from the pool's `onEvent(event, relay)` callback purely so the + * opt-in debug log line can include it. It is NOT otherwise used by + * the scheduler (single global watermark, no per-relay bookkeeping). */ - async function onEvent(event: NostrEvent): Promise { + // path labels for the debug per-event line. Kept narrow so we can't + // typo a path name and have it silently fall through. + type EventPath = "accepted" | "rejected-layerA" | "rejected-parse" | "dropped" | "replaced"; + const logPath = (kind: number, eventId: string, relay: string, path: EventPath): void => { + if (!debug) return; + console.log(`[scheduler] kind=${kind} id=${eventId.slice(0, 8)} relay=${relay} path=${path}`); + }; + async function onEvent(event: NostrEvent, relay: string): Promise { if (stopped) return; stats.eventsReceived += 1; @@ -600,11 +658,16 @@ export function createScheduler(config: SchedulerConfig): Scheduler { case 38173: { try { const parsed = parseMintAnnouncement(event); - if (!parsed) return; + if (!parsed) { + logPath(event.kind, event.id, relay, "dropped"); + return; + } const row = toAnnouncementRow(parsed); const result = await upsertAnnouncement(db, row); if (result === "rejected-invalid") { stats.rejectedByLayerA += 1; + logPath(event.kind, event.id, relay, "rejected-layerA"); + return; } if (result === "inserted" || result === "replaced") { stats.accepted += 1; @@ -615,7 +678,12 @@ export function createScheduler(config: SchedulerConfig): Scheduler { if (event.kind === 38172) { enqueueLayerB(row); } + logPath(event.kind, event.id, relay, result === "replaced" ? "replaced" : "accepted"); + return; } + // rejected-stale or any other terminal upsert result: count as + // a drop so the trace doesn't go silent on duplicates. + logPath(event.kind, event.id, relay, "dropped"); } catch (err) { stats.handlerErrors += 1; console.error("[scheduler] handler error", { @@ -638,17 +706,22 @@ export function createScheduler(config: SchedulerConfig): Scheduler { // — neither should reach here in a healthy pipeline but both // are silent drops worth counting (silent-failure gap). stats.rejectedByParse += 1; + logPath(event.kind, event.id, relay, "rejected-parse"); return; } const result = await upsertReviewWithAggregate(db, row, now); if (result === "inserted" || result === "replaced") { stats.accepted += 1; updateWatermark(event.kind, event.created_at); + logPath(event.kind, event.id, relay, result === "replaced" ? "replaced" : "accepted"); } else if (result === "rejected-invalid") { // Layer A gate on reviews: pointing at a bot-spam d-tag. Count // under the same stats bucket as the announcement Layer A // rejection — it's the same firewall. stats.rejectedByLayerA += 1; + logPath(event.kind, event.id, relay, "rejected-layerA"); + } else { + logPath(event.kind, event.id, relay, "dropped"); } } catch (err) { stats.handlerErrors += 1; @@ -663,11 +736,17 @@ export function createScheduler(config: SchedulerConfig): Scheduler { case 0: { try { const row = toProfileRow(event); - if (!row) return; + if (!row) { + logPath(event.kind, event.id, relay, "dropped"); + return; + } const result = await upsertProfile(db, row); if (result === "inserted" || result === "replaced") { stats.accepted += 1; updateWatermark(event.kind, event.created_at); + logPath(event.kind, event.id, relay, result === "replaced" ? "replaced" : "accepted"); + } else { + logPath(event.kind, event.id, relay, "dropped"); } } catch (err) { stats.handlerErrors += 1; @@ -682,11 +761,17 @@ export function createScheduler(config: SchedulerConfig): Scheduler { case 10002: { try { const row = toRelayListRow(event); - if (!row) return; + if (!row) { + logPath(event.kind, event.id, relay, "dropped"); + return; + } const result = await upsertRelayList(db, row); if (result === "inserted" || result === "replaced") { stats.accepted += 1; updateWatermark(event.kind, event.created_at); + logPath(event.kind, event.id, relay, result === "replaced" ? "replaced" : "accepted"); + } else { + logPath(event.kind, event.id, relay, "dropped"); } } catch (err) { stats.handlerErrors += 1; @@ -699,6 +784,7 @@ export function createScheduler(config: SchedulerConfig): Scheduler { return; } default: + logPath(event.kind, event.id, relay, "dropped"); return; // unknown kind — ignore } } @@ -745,15 +831,20 @@ export function createScheduler(config: SchedulerConfig): Scheduler { // duplicates CAS-fail at the cache layer. return since !== undefined ? { kinds: [kind], since } : { kinds: [kind] }; }); + if (debug) { + console.log( + `[scheduler] start — filters=${JSON.stringify(filters)} relays=${JSON.stringify(config.relays)}`, + ); + } handle = pool.subscribe({ filters, - onEvent: (event) => { + onEvent: (event, relay) => { // onEvent returns a promise; we don't await here because the // pool callback contract is sync. Each kind's case body wraps // its own try/catch that counts into stats.handlerErrors, so // a thrown Dexie transaction can't escape as an unhandled // rejection or silently freeze the stats. - void onEvent(event); + void onEvent(event, relay); }, closeOnEose: false, });