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 @@ + + +
+ + +` 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,
});