commit a1a576d088c8f83ed32d48753a7611f70a964574 Author: k Date: Sun Jun 23 17:38:53 2024 +0200 git: reset diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..b45d778d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.DS_Store + +/datasets +/datasets2 +/datasets_* + +TODO.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..a47930bb3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,39 @@ +# Changelog + +## v. 0.1.1 - WIP + +### Parser + +- Fixed overflow in `Price` struct which caused many Realized Caps and Realized Prices to have completely bogus data +- Fixed Realized Cap computation which was using rounded prices instead normal ones + +### Server + +- Added the chunk, date and time in the terminal logs + +### App + +- Chart + - Added double click option on a legend to toggle the visibility of all other series + - Added highlight effect to a legend by darkening the color of all the other series on the chart while hovering it with the mouse + - Added an API link in the legend for each dataset where applicable (when isn't generated locally) + - Save fullscreen preference in local storage and url + - Fixed time range shifting not being the one in url params or saved in local storage + - Fixed time range shifting on series toggling via the legend + - Fixed time range shifting on fullscreen + - Fixed time range shifting on resize of the sidebar + - Set default view at first load to last 6 months + - Added some padding around the datasets (year 1970 to 2100) +- History + - Changed background for the sticky dates from blur to a solid color as it didn't appear properly in Firefox +- Build + - Added lazy loads to have split chunks after build + - Removed many libraries and did some things manually instead to improve build size +- Strip + - Temporarily removed the Home button on the strip bar on desktop as there is no landing page yet +- Misc + - Removed tracker even though it was a very privacy friendly as it appeared to not be working properly + +### Price + +- Deleted old price datasets and their backups diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000..e0de8eb22 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Satonomics + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 000000000..36a02c03a --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# SATONOMICS + +## Description + +TLDR: FOSS [glassnode](https://glassnode.com). + +Satonomics is an open-source suite of tools that computes, distributes, and displays on-chain data, making it freely available for anyone to use. + +The generated datasets are incredibly diverse and can be used for a wide range of purposes. Whether you're looking to conduct a health check on the network, gain insights into its current or past state, or leverage the data for trading purposes, these tools offer various charts, dashboards (Soon TM), and an extensive API to help you achieve your goals. + +To promote transparency and trust in the network, this project is committed to making on-chain data accessible and verifiable to all, without discrimination and is a great complimentary tool to [mempool.space](https://mempool.space). + +## Instances + +Web App: +- [app.satonomics.xyz](https://app.satonomics.xyz) + +API: +- [api.satonomics.xyz](https://api.satonomics.xyz) + +## Structure + +- `parser`: The backbone of the project, it does most of the work by parsing and then computing datasets from the timechain. +- `server`: A small server which automatically creates routes to access through an API all created datasets. +- `app`: A web app which displays the generated datasets in various charts. + +## Git + +- [Repository](https://codeberg.org/satonomics/satonomics) +- [Issues](https://gitworkshop.dev/r/naddr1qq99xct5dahx7mtfvdesz9thwden5te0wp6hyurvv4ex2mrp0yhxxmmdqgsfw5dacngjlahye34krvgz7u0yghhjgk7gxzl5ptm9v6n2y3sn03srqsqqqaueek2h03/issues) +- [Proposals](https://gitworkshop.dev/r/naddr1qq99xct5dahx7mtfvdesz9thwden5te0wp6hyurvv4ex2mrp0yhxxmmdqgsfw5dacngjlahye34krvgz7u0yghhjgk7gxzl5ptm9v6n2y3sn03srqsqqqaueek2h03/proposals) + +## Goals + +- Be the absolute best on-chain data source and app +- Have as many datasets and charts as possible +- Be self-hostable on cheap computers + - Be runnable on a machine with 8 GB RAM (16 GB RAM is already possible right now) +- Still being runnable 10 years from now + - By not relying on any third-party dependencies besides price APIs (which are and should be very common and easy to update) + - By **NOT** doing address labelling/tagging (which means **NO** exchange or any other individual address tracking), for that please use [mempool.space](https://mempool.space) or any other tool + +## Proof of Work + +Aka: Previous iterations + +The initial idea was totally different yet morphed over time into what it is today: a fully FOSS self-hostable on-chain data generator + +- https://github.com/drgarlic/satonomics +- https://github.com/drgarlic/satonomics-parser +- https://github.com/drgarlic/satonomics-explorer +- https://github.com/drgarlic/satonomics-server +- https://github.com/drgarlic/satonomics-app +- https://github.com/drgarlic/bitalisys +- https://github.com/drgarlic/bitesque-app +- https://github.com/drgarlic/bitesque-back +- https://github.com/drgarlic/bitesque-front +- https://github.com/drgarlic/bitesque-assets +- https://github.com/drgarlic/syf diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 000000000..71ed004fc --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,9 @@ +node_modules +charts +dist +dev-dist +.DS_Store +visualizer +# Local Netlify folder +.netlify +.wrangler \ No newline at end of file diff --git a/app/README.md b/app/README.md new file mode 100644 index 000000000..8a34e4ba8 --- /dev/null +++ b/app/README.md @@ -0,0 +1,10 @@ +# Satonomics - App + +## Description + +A web app to view the generated datasets in various charts. + +## Requirements + +- `node` +- `pnpm` diff --git a/app/_redirects b/app/_redirects new file mode 100644 index 000000000..293658da4 --- /dev/null +++ b/app/_redirects @@ -0,0 +1 @@ +/* /index.html \ No newline at end of file diff --git a/app/index.html b/app/index.html new file mode 100644 index 000000000..7a1d67275 --- /dev/null +++ b/app/index.html @@ -0,0 +1,372 @@ + + + + + Satonomics + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/app/package.json b/app/package.json new file mode 100644 index 000000000..147cdb518 --- /dev/null +++ b/app/package.json @@ -0,0 +1,48 @@ +{ + "name": "satonomics", + "description": "Satoshi Economics", + "version": "0.1.0", + "license": "MIT", + "type": "module", + "scripts": { + "dev": "($npm_execpath outdated || read -p \"Press enter to ignore...\") && vite --host", + "build": "vite build", + "check": "tsc --noEmit --skipLibCheck --pretty", + "check-watch": "$npm_execpath check --watch", + "format": "prettier --write './src'", + "prod": "$npm_execpath run build && vite preview --host", + "pages-prod": "pnpm build && pnpm wrangler pages deploy ./dist", + "pages-dev": "pnpm build && pnpm wrangler pages deploy --branch dev ./dist", + "assets": "pnpm pwa-asset-generator ./public/logo/white.svg ./public/assets --index ./index.html --manifest ./public/manifest.webmanifest --icon-only --favicon --background \"linear-gradient(to right bottom, rgb(249, 115, 22), rgb(154, 52, 18))\" --padding \"min(15vh, 15vw)\" --path-override \"/assets\" && pnpm pwa-asset-generator ./public/logo/white.svg ./public/assets --index ./index.html --splash-only --background \"linear-gradient(to right bottom, rgb(249, 115, 22), rgb(154, 52, 18))\" --padding \"min(33vh, 33vw)\" --path-override \"/assets\" && pnpm pwa-asset-generator ./public/logo/white.svg ./public/assets --index ./index.html --splash-only --dark-mode --background \"#0c0a09\" --padding \"min(33vh, 33vw)\" --path-override \"/assets\"" + }, + "dependencies": { + "@leeoniya/ufuzzy": "^1.0.14", + "@solid-primitives/event-listener": "^2.3.3", + "@solid-primitives/intersection-observer": "^2.1.6", + "@solid-primitives/memo": "^1.3.8", + "@solid-primitives/resize-observer": "^2.0.25", + "lean-qr": "^2.3.4", + "lightweight-charts": "^4.1.6", + "solid-js": "^1.8.17" + }, + "devDependencies": { + "@ianvs/prettier-plugin-sort-imports": "^4.2.1", + "@iconify-json/tabler": "^1.1.114", + "@tailwindcss/container-queries": "^0.1.1", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.38", + "prettier": "^3.3.2", + "prettier-plugin-tailwindcss": "^0.6.5", + "pwa-asset-generator": "^6.3.1", + "rollup-plugin-visualizer": "^5.12.0", + "tailwindcss": "^3.4.4", + "typescript": "^5.5.2", + "unplugin-auto-import": "^0.17.6", + "unplugin-icons": "^0.19.0", + "vite": "^5.3.1", + "vite-plugin-pwa": "^0.20.0", + "vite-plugin-solid": "^2.10.2", + "workbox-window": "^7.1.0", + "wrangler": "^3.61.0" + } +} diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml new file mode 100644 index 000000000..fbc6ff1ff --- /dev/null +++ b/app/pnpm-lock.yaml @@ -0,0 +1,6333 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@leeoniya/ufuzzy': + specifier: ^1.0.14 + version: 1.0.14 + '@solid-primitives/event-listener': + specifier: ^2.3.3 + version: 2.3.3(solid-js@1.8.17) + '@solid-primitives/intersection-observer': + specifier: ^2.1.6 + version: 2.1.6(solid-js@1.8.17) + '@solid-primitives/memo': + specifier: ^1.3.8 + version: 1.3.8(solid-js@1.8.17) + '@solid-primitives/resize-observer': + specifier: ^2.0.25 + version: 2.0.25(solid-js@1.8.17) + lean-qr: + specifier: ^2.3.4 + version: 2.3.4 + lightweight-charts: + specifier: ^4.1.6 + version: 4.1.6 + solid-js: + specifier: ^1.8.17 + version: 1.8.17 + +devDependencies: + '@ianvs/prettier-plugin-sort-imports': + specifier: ^4.2.1 + version: 4.2.1(prettier@3.3.2) + '@iconify-json/tabler': + specifier: ^1.1.114 + version: 1.1.114 + '@tailwindcss/container-queries': + specifier: ^0.1.1 + version: 0.1.1(tailwindcss@3.4.4) + autoprefixer: + specifier: ^10.4.19 + version: 10.4.19(postcss@8.4.38) + postcss: + specifier: ^8.4.38 + version: 8.4.38 + prettier: + specifier: ^3.3.2 + version: 3.3.2 + prettier-plugin-tailwindcss: + specifier: ^0.6.5 + version: 0.6.5(@ianvs/prettier-plugin-sort-imports@4.2.1)(prettier@3.3.2) + pwa-asset-generator: + specifier: ^6.3.1 + version: 6.3.1 + rollup-plugin-visualizer: + specifier: ^5.12.0 + version: 5.12.0(rollup@2.79.1) + tailwindcss: + specifier: ^3.4.4 + version: 3.4.4 + typescript: + specifier: ^5.5.2 + version: 5.5.2 + unplugin-auto-import: + specifier: ^0.17.6 + version: 0.17.6(rollup@2.79.1) + unplugin-icons: + specifier: ^0.19.0 + version: 0.19.0 + vite: + specifier: ^5.3.1 + version: 5.3.1 + vite-plugin-pwa: + specifier: ^0.20.0 + version: 0.20.0(vite@5.3.1)(workbox-build@7.1.1)(workbox-window@7.1.0) + vite-plugin-solid: + specifier: ^2.10.2 + version: 2.10.2(solid-js@1.8.17)(vite@5.3.1) + workbox-window: + specifier: ^7.1.0 + version: 7.1.0 + wrangler: + specifier: ^3.61.0 + version: 3.61.0 + +packages: + + /@alloc/quick-lru@5.2.0: + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + dev: true + + /@ampproject/remapping@2.3.0: + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + dev: true + + /@antfu/install-pkg@0.1.1: + resolution: {integrity: sha512-LyB/8+bSfa0DFGC06zpCEfs89/XoWZwws5ygEa5D+Xsm3OfI+aXQ86VgVG7Acyef+rSZ5HE7J8rrxzrQeM3PjQ==} + dependencies: + execa: 5.1.1 + find-up: 5.0.0 + dev: true + + /@antfu/install-pkg@0.3.3: + resolution: {integrity: sha512-nHHsk3NXQ6xkCfiRRC8Nfrg8pU5kkr3P3Y9s9dKqiuRmBD0Yap7fymNDjGFKeWhZQHqqbCS5CfeMy9wtExM24w==} + dependencies: + '@jsdevtools/ez-spawn': 3.0.4 + dev: true + + /@antfu/utils@0.7.8: + resolution: {integrity: sha512-rWQkqXRESdjXtc+7NRfK9lASQjpXJu1ayp7qi1d23zZorY+wBHVLHHoVcMsEnkqEBWTFqbztO7/QdJFzyEcLTg==} + dev: true + + /@apideck/better-ajv-errors@0.3.6(ajv@8.16.0): + resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==} + engines: {node: '>=10'} + peerDependencies: + ajv: '>=8' + dependencies: + ajv: 8.16.0 + json-schema: 0.4.0 + jsonpointer: 5.0.1 + leven: 3.1.0 + dev: true + + /@babel/code-frame@7.24.7: + resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.24.7 + picocolors: 1.0.1 + dev: true + + /@babel/compat-data@7.24.7: + resolution: {integrity: sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/core@7.24.7: + resolution: {integrity: sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/helper-compilation-targets': 7.24.7 + '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7) + '@babel/helpers': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/template': 7.24.7 + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + convert-source-map: 2.0.0 + debug: 4.3.5 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/generator@7.24.7: + resolution: {integrity: sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.7 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 2.5.2 + dev: true + + /@babel/helper-annotate-as-pure@7.24.7: + resolution: {integrity: sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.7 + dev: true + + /@babel/helper-builder-binary-assignment-operator-visitor@7.24.7: + resolution: {integrity: sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-compilation-targets@7.24.7: + resolution: {integrity: sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.24.7 + '@babel/helper-validator-option': 7.24.7 + browserslist: 4.23.1 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: true + + /@babel/helper-create-class-features-plugin@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-member-expression-to-functions': 7.24.7 + '@babel/helper-optimise-call-expression': 7.24.7 + '@babel/helper-replace-supers': 7.24.7(@babel/core@7.24.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-create-regexp-features-plugin@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.24.7 + regexpu-core: 5.3.2 + semver: 6.3.1 + dev: true + + /@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.24.7): + resolution: {integrity: sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-compilation-targets': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + debug: 4.3.5 + lodash.debounce: 4.0.8 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-environment-visitor@7.24.7: + resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.7 + dev: true + + /@babel/helper-function-name@7.24.7: + resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.24.7 + '@babel/types': 7.24.7 + dev: true + + /@babel/helper-hoist-variables@7.24.7: + resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.7 + dev: true + + /@babel/helper-member-expression-to-functions@7.24.7: + resolution: {integrity: sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-module-imports@7.18.6: + resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.7 + dev: true + + /@babel/helper-module-imports@7.24.7: + resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-module-transforms@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-simple-access': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-optimise-call-expression@7.24.7: + resolution: {integrity: sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.7 + dev: true + + /@babel/helper-plugin-utils@7.24.7: + resolution: {integrity: sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-remap-async-to-generator@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-wrap-function': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-replace-supers@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-member-expression-to-functions': 7.24.7 + '@babel/helper-optimise-call-expression': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-simple-access@7.24.7: + resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-skip-transparent-expression-wrappers@7.24.7: + resolution: {integrity: sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-split-export-declaration@7.24.7: + resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.7 + dev: true + + /@babel/helper-string-parser@7.24.7: + resolution: {integrity: sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.24.7: + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-option@7.24.7: + resolution: {integrity: sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-wrap-function@7.24.7: + resolution: {integrity: sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-function-name': 7.24.7 + '@babel/template': 7.24.7 + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helpers@7.24.7: + resolution: {integrity: sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.24.7 + '@babel/types': 7.24.7 + dev: true + + /@babel/highlight@7.24.7: + resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.24.7 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.1 + dev: true + + /@babel/parser@7.24.7: + resolution: {integrity: sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.24.7 + dev: true + + /@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + '@babel/plugin-transform-optional-chaining': 7.24.7(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.7): + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + dev: true + + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.7): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.7): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.24.7): + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.24.7): + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.24.7): + resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-import-assertions@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-import-attributes@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.7): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.7): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-jsx@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.7): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.7): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.7): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.7): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.7): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.7): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.24.7): + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.7): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.24.7): + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-arrow-functions@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-async-generator-functions@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-remap-async-to-generator': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-async-to-generator@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-remap-async-to-generator': 7.24.7(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-block-scoped-functions@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-block-scoping@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-class-properties@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-class-static-block@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-classes@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-compilation-targets': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-replace-supers': 7.24.7(@babel/core@7.24.7) + '@babel/helper-split-export-declaration': 7.24.7 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-computed-properties@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/template': 7.24.7 + dev: true + + /@babel/plugin-transform-destructuring@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-dotall-regex@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-duplicate-keys@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-dynamic-import@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.7) + dev: true + + /@babel/plugin-transform-exponentiation-operator@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-export-namespace-from@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.7) + dev: true + + /@babel/plugin-transform-for-of@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-function-name@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-compilation-targets': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-json-strings@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.7) + dev: true + + /@babel/plugin-transform-literals@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-logical-assignment-operators@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.7) + dev: true + + /@babel/plugin-transform-member-expression-literals@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-modules-amd@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-commonjs@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-simple-access': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-systemjs@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-hoist-variables': 7.24.7 + '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-umd@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-named-capturing-groups-regex@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-new-target@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-nullish-coalescing-operator@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.7) + dev: true + + /@babel/plugin-transform-numeric-separator@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.7) + dev: true + + /@babel/plugin-transform-object-rest-spread@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-compilation-targets': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.24.7) + dev: true + + /@babel/plugin-transform-object-super@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-replace-supers': 7.24.7(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-optional-catch-binding@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.7) + dev: true + + /@babel/plugin-transform-optional-chaining@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-parameters@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-private-methods@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-private-property-in-object@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-property-literals@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-regenerator@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + regenerator-transform: 0.15.2 + dev: true + + /@babel/plugin-transform-reserved-words@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-shorthand-properties@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-spread@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-sticky-regex@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-template-literals@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-typeof-symbol@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-unicode-escapes@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-unicode-property-regex@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-unicode-regex@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-unicode-sets-regex@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/preset-env@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.24.7 + '@babel/core': 7.24.7 + '@babel/helper-compilation-targets': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-validator-option': 7.24.7 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.7) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.7) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.7) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.7) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-import-assertions': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-syntax-import-attributes': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.7) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.7) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.7) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.7) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.7) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.24.7) + '@babel/plugin-transform-arrow-functions': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-async-generator-functions': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-async-to-generator': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-block-scoped-functions': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-block-scoping': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-class-properties': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-class-static-block': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-classes': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-computed-properties': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-destructuring': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-dotall-regex': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-duplicate-keys': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-dynamic-import': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-exponentiation-operator': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-export-namespace-from': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-for-of': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-function-name': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-json-strings': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-literals': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-logical-assignment-operators': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-member-expression-literals': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-modules-amd': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-modules-commonjs': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-modules-systemjs': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-modules-umd': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-named-capturing-groups-regex': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-new-target': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-nullish-coalescing-operator': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-numeric-separator': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-object-rest-spread': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-object-super': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-optional-catch-binding': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-optional-chaining': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-private-methods': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-private-property-in-object': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-property-literals': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-regenerator': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-reserved-words': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-shorthand-properties': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-spread': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-sticky-regex': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-template-literals': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-typeof-symbol': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-unicode-escapes': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-unicode-property-regex': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-unicode-regex': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-unicode-sets-regex': 7.24.7(@babel/core@7.24.7) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.24.7) + babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.24.7) + babel-plugin-polyfill-corejs3: 0.10.4(@babel/core@7.24.7) + babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.24.7) + core-js-compat: 3.37.1 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.24.7): + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/types': 7.24.7 + esutils: 2.0.3 + dev: true + + /@babel/regjsgen@0.8.0: + resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} + dev: true + + /@babel/runtime@7.24.7: + resolution: {integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.1 + dev: true + + /@babel/template@7.24.7: + resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + dev: true + + /@babel/traverse@7.24.7: + resolution: {integrity: sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-hoist-variables': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + debug: 4.3.5 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types@7.24.7: + resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 + dev: true + + /@cloudflare/kv-asset-handler@0.3.3: + resolution: {integrity: sha512-wpE+WiWW2kUNwNE0xyl4CtTAs+STjGtouHGiZPGRaisGB7eXXdbvfZdOrQJQVKgTxZiNAgVgmc7fj0sUmd8zyA==} + engines: {node: '>=16.13'} + dependencies: + mime: 3.0.0 + dev: true + + /@cloudflare/workerd-darwin-64@1.20240610.1: + resolution: {integrity: sha512-YanZ1iXgMGaUWlleB5cswSE6qbzyjQ8O7ENWZcPAcZZ6BfuL7q3CWi0t9iM1cv2qx92rRztsRTyjcfq099++XQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@cloudflare/workerd-darwin-arm64@1.20240610.1: + resolution: {integrity: sha512-bRe/y/LKjIgp3L2EHjc+CvoCzfHhf4aFTtOBkv2zW+VToNJ4KlXridndf7LvR9urfsFRRo9r4TXCssuKaU+ypQ==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@cloudflare/workerd-linux-64@1.20240610.1: + resolution: {integrity: sha512-2zDcadR7+Gs9SjcMXmwsMji2Xs+yASGNA2cEHDuFc4NMUup+eL1mkzxc/QzvFjyBck98e92rBjMZt2dVscpGKg==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@cloudflare/workerd-linux-arm64@1.20240610.1: + resolution: {integrity: sha512-7y41rPi5xmIYJN8CY+t3RHnjLL0xx/WYmaTd/j552k1qSr02eTE2o/TGyWZmGUC+lWnwdPQJla0mXbvdqgRdQg==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@cloudflare/workerd-windows-64@1.20240610.1: + resolution: {integrity: sha512-B0LyT3DB6rXHWNptnntYHPaoJIy0rXnGfeDBM3nEVV8JIsQrx8MEFn2F2jYioH1FkUVavsaqKO/zUosY3tZXVA==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: true + + /@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.17.19): + resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==} + peerDependencies: + esbuild: '*' + dependencies: + esbuild: 0.17.19 + dev: true + + /@esbuild-plugins/node-modules-polyfill@0.2.2(esbuild@0.17.19): + resolution: {integrity: sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==} + peerDependencies: + esbuild: '*' + dependencies: + esbuild: 0.17.19 + escape-string-regexp: 4.0.0 + rollup-plugin-node-polyfills: 0.2.1 + dev: true + + /@esbuild/aix-ppc64@0.21.5: + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.17.19: + resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.21.5: + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.17.19: + resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.21.5: + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.17.19: + resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.21.5: + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.17.19: + resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.21.5: + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.17.19: + resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.21.5: + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.17.19: + resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.21.5: + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.17.19: + resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.21.5: + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.17.19: + resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.21.5: + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.17.19: + resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.21.5: + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.17.19: + resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.21.5: + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.17.19: + resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.21.5: + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.17.19: + resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.21.5: + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.17.19: + resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.21.5: + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.17.19: + resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.21.5: + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.17.19: + resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.21.5: + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.17.19: + resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.21.5: + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.17.19: + resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.21.5: + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.17.19: + resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.21.5: + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.17.19: + resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.21.5: + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.17.19: + resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.21.5: + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.17.19: + resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.21.5: + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.17.19: + resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.21.5: + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@fastify/busboy@2.1.1: + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + dev: true + + /@ianvs/prettier-plugin-sort-imports@4.2.1(prettier@3.3.2): + resolution: {integrity: sha512-NKN1LVFWUDGDGr3vt+6Ey3qPeN/163uR1pOPAlkWpgvAqgxQ6kSdUf1F0it8aHUtKRUzEGcK38Wxd07O61d7+Q==} + peerDependencies: + '@vue/compiler-sfc': 2.7.x || 3.x + prettier: 2 || 3 + peerDependenciesMeta: + '@vue/compiler-sfc': + optional: true + dependencies: + '@babel/core': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + prettier: 3.3.2 + semver: 7.6.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@iconify-json/tabler@1.1.114: + resolution: {integrity: sha512-AaTTGEyiPQ7VAYyXGQ9jUI8+8iL6xanucYsACz6f3U6JLph6jDyicXXUh+dYM6HxW6TGehwVqRO2NSIQpACszw==} + dependencies: + '@iconify/types': 2.0.0 + dev: true + + /@iconify/types@2.0.0: + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + dev: true + + /@iconify/utils@2.1.25: + resolution: {integrity: sha512-Y+iGko8uv/Fz5bQLLJyNSZGOdMW0G7cnlEX1CiNcKsRXX9cq/y/vwxrIAtLCZhKHr3m0VJmsjVPsvnM4uX8YLg==} + dependencies: + '@antfu/install-pkg': 0.1.1 + '@antfu/utils': 0.7.8 + '@iconify/types': 2.0.0 + debug: 4.3.5 + kolorist: 1.8.0 + local-pkg: 0.5.0 + mlly: 1.7.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: true + + /@jridgewell/gen-mapping@0.3.5: + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.25 + dev: true + + /@jridgewell/resolve-uri@3.1.2: + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/set-array@1.2.1: + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/source-map@0.3.6: + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.25: + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@jsdevtools/ez-spawn@3.0.4: + resolution: {integrity: sha512-f5DRIOZf7wxogefH03RjMPMdBF7ADTWUMoOs9kaJo06EfwF+aFhMZMDZxHg/Xe12hptN9xoZjGso2fdjapBRIA==} + engines: {node: '>=10'} + dependencies: + call-me-maybe: 1.0.2 + cross-spawn: 7.0.3 + string-argv: 0.3.2 + type-detect: 4.0.8 + dev: true + + /@leeoniya/ufuzzy@1.0.14: + resolution: {integrity: sha512-/xF4baYuCQMo+L/fMSUrZnibcu0BquEGnbxfVPiZhs/NbJeKj4c/UmFpQzW9Us0w45ui/yYW3vyaqawhNYsTzA==} + dev: false + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + dev: true + + /@one-ini/wasm@0.1.1: + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + dev: true + + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: true + optional: true + + /@rollup/plugin-babel@5.3.1(@babel/core@7.24.7)(rollup@2.79.1): + resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} + engines: {node: '>= 10.0.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@types/babel__core': ^7.1.9 + rollup: ^1.20.0||^2.0.0 + peerDependenciesMeta: + '@types/babel__core': + optional: true + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-module-imports': 7.24.7 + '@rollup/pluginutils': 3.1.0(rollup@2.79.1) + rollup: 2.79.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@rollup/plugin-node-resolve@15.2.3(rollup@2.79.1): + resolution: {integrity: sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@2.79.1) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-builtin-module: 3.2.1 + is-module: 1.0.0 + resolve: 1.22.8 + rollup: 2.79.1 + dev: true + + /@rollup/plugin-replace@2.4.2(rollup@2.79.1): + resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==} + peerDependencies: + rollup: ^1.20.0 || ^2.0.0 + dependencies: + '@rollup/pluginutils': 3.1.0(rollup@2.79.1) + magic-string: 0.25.9 + rollup: 2.79.1 + dev: true + + /@rollup/plugin-terser@0.4.4(rollup@2.79.1): + resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + rollup: 2.79.1 + serialize-javascript: 6.0.2 + smob: 1.5.0 + terser: 5.31.1 + dev: true + + /@rollup/pluginutils@3.1.0(rollup@2.79.1): + resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} + engines: {node: '>= 8.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + dependencies: + '@types/estree': 0.0.39 + estree-walker: 1.0.1 + picomatch: 2.3.1 + rollup: 2.79.1 + dev: true + + /@rollup/pluginutils@5.1.0(rollup@2.79.1): + resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.5 + estree-walker: 2.0.2 + picomatch: 2.3.1 + rollup: 2.79.1 + dev: true + + /@rollup/rollup-android-arm-eabi@4.18.0: + resolution: {integrity: sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-android-arm64@4.18.0: + resolution: {integrity: sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-arm64@4.18.0: + resolution: {integrity: sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-x64@4.18.0: + resolution: {integrity: sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm-gnueabihf@4.18.0: + resolution: {integrity: sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm-musleabihf@4.18.0: + resolution: {integrity: sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.18.0: + resolution: {integrity: sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-musl@4.18.0: + resolution: {integrity: sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-powerpc64le-gnu@4.18.0: + resolution: {integrity: sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-riscv64-gnu@4.18.0: + resolution: {integrity: sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-s390x-gnu@4.18.0: + resolution: {integrity: sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.18.0: + resolution: {integrity: sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-musl@4.18.0: + resolution: {integrity: sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-arm64-msvc@4.18.0: + resolution: {integrity: sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-ia32-msvc@4.18.0: + resolution: {integrity: sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-x64-msvc@4.18.0: + resolution: {integrity: sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@solid-primitives/event-listener@2.3.3(solid-js@1.8.17): + resolution: {integrity: sha512-DAJbl+F0wrFW2xmcV8dKMBhk9QLVLuBSW+TR4JmIfTaObxd13PuL7nqaXnaYKDWOYa6otB00qcCUIGbuIhSUgQ==} + peerDependencies: + solid-js: ^1.6.12 + dependencies: + '@solid-primitives/utils': 6.2.3(solid-js@1.8.17) + solid-js: 1.8.17 + dev: false + + /@solid-primitives/intersection-observer@2.1.6(solid-js@1.8.17): + resolution: {integrity: sha512-SeiCmN/R46Z+o9+5HhIQzSor0DqVPyo4ROLQMvCI8AsGZl/5nHlWzHTTbWPeukVUXTgb04wfC3DUo9IzF/XloA==} + peerDependencies: + solid-js: ^1.6.12 + dependencies: + '@solid-primitives/utils': 6.2.3(solid-js@1.8.17) + solid-js: 1.8.17 + dev: false + + /@solid-primitives/memo@1.3.8(solid-js@1.8.17): + resolution: {integrity: sha512-U75pfLFSxFmM2xbx1+2XPPyWbaXrnUFF10spbFuOUgJ7azrC+4y+FnrVi4RKqHw9gftd8aKQuTiyMQq468YLQw==} + peerDependencies: + solid-js: ^1.6.12 + dependencies: + '@solid-primitives/scheduled': 1.4.3(solid-js@1.8.17) + '@solid-primitives/utils': 6.2.3(solid-js@1.8.17) + solid-js: 1.8.17 + dev: false + + /@solid-primitives/resize-observer@2.0.25(solid-js@1.8.17): + resolution: {integrity: sha512-jVDXkt2MiriYRaz4DYs62185d+6jQ+1DCsR+v7f6XMsIJJuf963qdBRFjtZtKXBaxdPNMyuPeDgf5XQe3EoDJg==} + peerDependencies: + solid-js: ^1.6.12 + dependencies: + '@solid-primitives/event-listener': 2.3.3(solid-js@1.8.17) + '@solid-primitives/rootless': 1.4.5(solid-js@1.8.17) + '@solid-primitives/static-store': 0.0.8(solid-js@1.8.17) + '@solid-primitives/utils': 6.2.3(solid-js@1.8.17) + solid-js: 1.8.17 + dev: false + + /@solid-primitives/rootless@1.4.5(solid-js@1.8.17): + resolution: {integrity: sha512-GFJE9GC3ojx0aUKqAUZmQPyU8fOVMtnVNrkdk2yS4kd17WqVSpXpoTmo9CnOwA+PG7FTzdIkogvfLQSLs4lrww==} + peerDependencies: + solid-js: ^1.6.12 + dependencies: + '@solid-primitives/utils': 6.2.3(solid-js@1.8.17) + solid-js: 1.8.17 + dev: false + + /@solid-primitives/scheduled@1.4.3(solid-js@1.8.17): + resolution: {integrity: sha512-HfWN5w7b7FEc6VPLBKnnE302h90jsLMuR28Fcf7neRGGf8jBj6wm6/UFQ00VlKexHFMR6KQ2u4VBh5a1ZcqM8g==} + peerDependencies: + solid-js: ^1.6.12 + dependencies: + solid-js: 1.8.17 + dev: false + + /@solid-primitives/static-store@0.0.8(solid-js@1.8.17): + resolution: {integrity: sha512-ZecE4BqY0oBk0YG00nzaAWO5Mjcny8Fc06CdbXadH9T9lzq/9GefqcSe/5AtdXqjvY/DtJ5C6CkcjPZO0o/eqg==} + peerDependencies: + solid-js: ^1.6.12 + dependencies: + '@solid-primitives/utils': 6.2.3(solid-js@1.8.17) + solid-js: 1.8.17 + dev: false + + /@solid-primitives/utils@6.2.3(solid-js@1.8.17): + resolution: {integrity: sha512-CqAwKb2T5Vi72+rhebSsqNZ9o67buYRdEJrIFzRXz3U59QqezuuxPsyzTSVCacwS5Pf109VRsgCJQoxKRoECZQ==} + peerDependencies: + solid-js: ^1.6.12 + dependencies: + solid-js: 1.8.17 + dev: false + + /@surma/rollup-plugin-off-main-thread@2.2.3: + resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} + dependencies: + ejs: 3.1.10 + json5: 2.2.3 + magic-string: 0.25.9 + string.prototype.matchall: 4.0.11 + dev: true + + /@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.4): + resolution: {integrity: sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==} + peerDependencies: + tailwindcss: '>=3.2.0' + dependencies: + tailwindcss: 3.4.4 + dev: true + + /@types/babel__core@7.20.5: + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + dependencies: + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 + dev: true + + /@types/babel__generator@7.6.8: + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + dependencies: + '@babel/types': 7.24.7 + dev: true + + /@types/babel__template@7.4.4: + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + dependencies: + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + dev: true + + /@types/babel__traverse@7.20.6: + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + dependencies: + '@babel/types': 7.24.7 + dev: true + + /@types/estree@0.0.39: + resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} + dev: true + + /@types/estree@1.0.5: + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + dev: true + + /@types/minimist@1.2.5: + resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} + dev: true + + /@types/node-forge@1.3.11: + resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} + dependencies: + '@types/node': 20.14.7 + dev: true + + /@types/node@20.14.7: + resolution: {integrity: sha512-uTr2m2IbJJucF3KUxgnGOZvYbN0QgkGyWxG6973HCpMYFy2KfcgYuIwkJQMQkt1VbBMlvWRbpshFTLxnxCZjKQ==} + dependencies: + undici-types: 5.26.5 + dev: true + + /@types/normalize-package-data@2.4.4: + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + dev: true + + /@types/resolve@1.20.2: + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + dev: true + + /@types/trusted-types@2.0.7: + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + dev: true + + /@types/yauzl@2.10.3: + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + requiresBuild: true + dependencies: + '@types/node': 20.14.7 + dev: true + optional: true + + /abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + + /acorn-walk@8.3.3: + resolution: {integrity: sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==} + engines: {node: '>=0.4.0'} + dependencies: + acorn: 8.12.0 + dev: true + + /acorn@8.12.0: + resolution: {integrity: sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /ajv@8.16.0: + resolution: {integrity: sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==} + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: true + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: true + + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: true + + /array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + is-array-buffer: 3.0.4 + dev: true + + /arraybuffer.prototype.slice@1.0.3: + resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + is-array-buffer: 3.0.4 + is-shared-array-buffer: 1.0.3 + dev: true + + /arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + dev: true + + /as-table@1.0.55: + resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} + dependencies: + printable-characters: 1.0.42 + dev: true + + /async@3.2.5: + resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} + dev: true + + /at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + dev: true + + /autoprefixer@10.4.19(postcss@8.4.38): + resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + dependencies: + browserslist: 4.23.1 + caniuse-lite: 1.0.30001636 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.0.1 + postcss: 8.4.38 + postcss-value-parser: 4.2.0 + dev: true + + /available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + dependencies: + possible-typed-array-names: 1.0.0 + dev: true + + /babel-plugin-jsx-dom-expressions@0.37.21(@babel/core@7.24.7): + resolution: {integrity: sha512-WbQo1NQ241oki8bYasVzkMXOTSIri5GO/K47rYJb2ZBh8GaPUEWiWbMV3KwXz+96eU2i54N6ThzjQG/f5n8Azw==} + peerDependencies: + '@babel/core': ^7.20.12 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-module-imports': 7.18.6 + '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.24.7) + '@babel/types': 7.24.7 + html-entities: 2.3.3 + validate-html-nesting: 1.2.2 + dev: true + + /babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.24.7): + resolution: {integrity: sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/compat-data': 7.24.7 + '@babel/core': 7.24.7 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.7) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-polyfill-corejs3@0.10.4(@babel/core@7.24.7): + resolution: {integrity: sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.7) + core-js-compat: 3.37.1 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-polyfill-regenerator@0.6.2(@babel/core@7.24.7): + resolution: {integrity: sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + dev: true + + /babel-preset-solid@1.8.17(@babel/core@7.24.7): + resolution: {integrity: sha512-s/FfTZOeds0hYxYqce90Jb+0ycN2lrzC7VP1k1JIn3wBqcaexDKdYi6xjB+hMNkL+Q6HobKbwsriqPloasR9LA==} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.7 + babel-plugin-jsx-dom-expressions: 0.37.21(@babel/core@7.24.7) + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: true + + /binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + dev: true + + /bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: true + + /blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + dev: true + + /boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.1.1 + dev: true + + /browserslist@4.23.1: + resolution: {integrity: sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001636 + electron-to-chromium: 1.4.808 + node-releases: 2.0.14 + update-browserslist-db: 1.0.16(browserslist@4.23.1) + dev: true + + /buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + dev: true + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + + /buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + + /builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + dev: true + + /call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + dev: true + + /call-me-maybe@1.0.2: + resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} + dev: true + + /camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + dev: true + + /camelcase-keys@6.2.2: + resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} + engines: {node: '>=8'} + dependencies: + camelcase: 5.3.1 + map-obj: 4.3.0 + quick-lru: 4.0.1 + dev: true + + /camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + dev: true + + /caniuse-lite@1.0.30001636: + resolution: {integrity: sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==} + dev: true + + /capnp-ts@0.7.0: + resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==} + dependencies: + debug: 4.3.5 + tslib: 2.6.3 + transitivePeerDependencies: + - supports-color + dev: true + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + dependencies: + boolbase: 1.0.0 + css-select: 5.1.0 + css-what: 6.1.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + dev: true + + /cheerio@1.0.0-rc.12: + resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} + engines: {node: '>= 6'} + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.1.0 + htmlparser2: 8.0.2 + parse5: 7.1.2 + parse5-htmlparser2-tree-adapter: 7.0.0 + dev: true + + /chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + dev: true + + /chrome-launcher@0.15.2: + resolution: {integrity: sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==} + engines: {node: '>=12.13.0'} + hasBin: true + dependencies: + '@types/node': 20.14.7 + escape-string-regexp: 4.0.0 + is-wsl: 2.2.0 + lighthouse-logger: 1.4.2 + transitivePeerDependencies: + - supports-color + dev: true + + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + dev: true + + /commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + dev: true + + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: true + + /commander@5.1.0: + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} + engines: {node: '>= 6'} + dev: true + + /common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /condense-newlines@0.2.1: + resolution: {integrity: sha512-P7X+QL9Hb9B/c8HI5BFFKmjgBu2XpQuF98WZ9XkO+dBGgk5XgwiQz7o1SmpglNWId3581UcS0SFAWfoIhMHPfg==} + engines: {node: '>=0.10.0'} + dependencies: + extend-shallow: 2.0.1 + is-whitespace: 0.3.0 + kind-of: 3.2.2 + dev: true + + /confbox@0.1.7: + resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==} + dev: true + + /config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + dev: true + + /consola@3.2.3: + resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} + engines: {node: ^14.18.0 || >=16.10.0} + dev: true + + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true + + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: true + + /core-js-compat@3.37.1: + resolution: {integrity: sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==} + dependencies: + browserslist: 4.23.1 + dev: true + + /cross-fetch@3.1.5: + resolution: {integrity: sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==} + dependencies: + node-fetch: 2.6.7 + transitivePeerDependencies: + - encoding + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /crypto-random-string@2.0.0: + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + engines: {node: '>=8'} + dev: true + + /css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + dev: true + + /css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + dev: true + + /cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + /data-uri-to-buffer@2.0.2: + resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} + dev: true + + /data-view-buffer@1.0.1: + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: true + + /data-view-byte-length@1.0.1: + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: true + + /data-view-byte-offset@1.0.0: + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: true + + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + dev: true + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /debug@4.3.5: + resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /decamelize-keys@1.1.1: + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} + engines: {node: '>=0.10.0'} + dependencies: + decamelize: 1.2.0 + map-obj: 1.0.1 + dev: true + + /decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + dev: true + + /deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + dev: true + + /define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + dev: true + + /define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + dev: true + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + dev: true + + /defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + dev: true + + /devtools-protocol@0.0.981744: + resolution: {integrity: sha512-0cuGS8+jhR67Fy7qG3i3Pc7Aw494sb9yG9QgpG97SFVWwolgYjlhJg7n+UaHxOQT30d1TYu/EYe9k01ivLErIg==} + dev: true + + /didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + dev: true + + /dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dev: true + + /dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + dev: true + + /domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: true + + /domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: true + + /domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dev: true + + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + + /editorconfig@1.0.4: + resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==} + engines: {node: '>=14'} + hasBin: true + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.1 + semver: 7.6.2 + dev: true + + /ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + jake: 10.9.1 + dev: true + + /electron-to-chromium@1.4.808: + resolution: {integrity: sha512-0ItWyhPYnww2VOuCGF4s1LTfbrdAV2ajy/TN+ZTuhR23AHI6rWHCrBXJ/uxoXOvRRqw8qjYVrG81HFI7x/2wdQ==} + dev: true + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + + /end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + dev: true + + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: true + + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + dev: true + + /es-abstract@1.23.3: + resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + data-view-buffer: 1.0.1 + data-view-byte-length: 1.0.1 + data-view-byte-offset: 1.0.0 + es-define-property: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.4 + get-symbol-description: 1.0.2 + globalthis: 1.0.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 + is-callable: 1.2.7 + is-data-view: 1.0.1 + is-negative-zero: 2.0.3 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + is-string: 1.0.7 + is-typed-array: 1.1.13 + is-weakref: 1.0.2 + object-inspect: 1.13.1 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.2 + safe-array-concat: 1.1.2 + safe-regex-test: 1.0.3 + string.prototype.trim: 1.2.9 + string.prototype.trimend: 1.0.8 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.1 + typed-array-byte-offset: 1.0.2 + typed-array-length: 1.0.6 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.15 + dev: true + + /es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + dev: true + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + dev: true + + /es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + dev: true + + /es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + dev: true + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + + /esbuild@0.17.19: + resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.17.19 + '@esbuild/android-arm64': 0.17.19 + '@esbuild/android-x64': 0.17.19 + '@esbuild/darwin-arm64': 0.17.19 + '@esbuild/darwin-x64': 0.17.19 + '@esbuild/freebsd-arm64': 0.17.19 + '@esbuild/freebsd-x64': 0.17.19 + '@esbuild/linux-arm': 0.17.19 + '@esbuild/linux-arm64': 0.17.19 + '@esbuild/linux-ia32': 0.17.19 + '@esbuild/linux-loong64': 0.17.19 + '@esbuild/linux-mips64el': 0.17.19 + '@esbuild/linux-ppc64': 0.17.19 + '@esbuild/linux-riscv64': 0.17.19 + '@esbuild/linux-s390x': 0.17.19 + '@esbuild/linux-x64': 0.17.19 + '@esbuild/netbsd-x64': 0.17.19 + '@esbuild/openbsd-x64': 0.17.19 + '@esbuild/sunos-x64': 0.17.19 + '@esbuild/win32-arm64': 0.17.19 + '@esbuild/win32-ia32': 0.17.19 + '@esbuild/win32-x64': 0.17.19 + dev: true + + /esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + dev: true + + /escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + dev: true + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: true + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + dev: true + + /estree-walker@0.6.1: + resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} + dev: true + + /estree-walker@1.0.1: + resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} + dev: true + + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: true + + /estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + dependencies: + '@types/estree': 1.0.5 + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + dev: true + + /exit-hook@2.2.1: + resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} + engines: {node: '>=6'} + dev: true + + /extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + dependencies: + is-extendable: 0.1.1 + dev: true + + /extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + dependencies: + debug: 4.3.4 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + dev: true + + /fancy-canvas@2.1.0: + resolution: {integrity: sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ==} + dev: false + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.7 + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + dependencies: + reusify: 1.0.4 + dev: true + + /fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + dependencies: + pend: 1.2.0 + dev: true + + /filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + dependencies: + minimatch: 5.1.6 + dev: true + + /fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /find-process@1.4.7: + resolution: {integrity: sha512-/U4CYp1214Xrp3u3Fqr9yNynUrr5Le4y0SsJh2lMDDSbpwYSz3M2SMWQC+wqcx79cN8PQtHQIL8KnuY9M66fdg==} + hasBin: true + dependencies: + chalk: 4.1.2 + commander: 5.1.0 + debug: 4.3.5 + transitivePeerDependencies: + - supports-color + dev: true + + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + dev: true + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + + /foreground-child@3.2.1: + resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + dev: true + + /fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + dev: true + + /fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + dev: true + + /fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: true + + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + functions-have-names: 1.2.3 + dev: true + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: true + + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + dev: true + + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: true + + /get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + dev: true + + /get-own-enumerable-property-symbols@3.0.2: + resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} + dev: true + + /get-source@2.0.12: + resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} + dependencies: + data-uri-to-buffer: 2.0.2 + source-map: 0.6.1 + dev: true + + /get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + dependencies: + pump: 3.0.0 + dev: true + + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + dev: true + + /get-symbol-description@1.0.2: + resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + dev: true + + /glob@10.4.2: + resolution: {integrity: sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==} + engines: {node: '>=16 || 14 >=14.18'} + hasBin: true + dependencies: + foreground-child: 3.2.1 + jackspeak: 3.4.0 + minimatch: 9.0.4 + minipass: 7.1.2 + package-json-from-dist: 1.0.0 + path-scurry: 1.11.1 + dev: true + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + dev: true + + /globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + gopd: 1.0.1 + dev: true + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.4 + dev: true + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: true + + /hard-rejection@2.1.0: + resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} + engines: {node: '>=6'} + dev: true + + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + dependencies: + es-define-property: 1.0.0 + dev: true + + /has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + dev: true + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: true + + /has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: true + + /hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + dev: true + + /hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + dependencies: + lru-cache: 6.0.0 + dev: true + + /html-entities@2.3.3: + resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} + dev: true + + /htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + dev: true + + /https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + dev: true + + /idb@7.1.1: + resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} + dev: true + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: true + + /indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + dev: true + + /internal-slot@1.0.7: + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.0.6 + dev: true + + /is-array-buffer@3.0.4: + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + dev: true + + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + dev: true + + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: true + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.3.0 + dev: true + + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + dev: true + + /is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + dev: true + + /is-builtin-module@3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} + dependencies: + builtin-modules: 3.3.0 + dev: true + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + + /is-core-module@2.14.0: + resolution: {integrity: sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==} + engines: {node: '>= 0.4'} + dependencies: + hasown: 2.0.2 + dev: true + + /is-data-view@1.0.1: + resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} + engines: {node: '>= 0.4'} + dependencies: + is-typed-array: 1.1.13 + dev: true + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + dev: true + + /is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + dev: true + + /is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + dev: true + + /is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + dev: true + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-obj@1.0.1: + resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} + engines: {node: '>=0.10.0'} + dev: true + + /is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + dev: true + + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + dev: true + + /is-regexp@1.0.0: + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} + engines: {node: '>=0.10.0'} + dev: true + + /is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + dev: true + + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + dev: true + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + dev: true + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.15 + dev: true + + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.7 + dev: true + + /is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + dev: true + + /is-whitespace@0.3.0: + resolution: {integrity: sha512-RydPhl4S6JwAyj0JJjshWJEFG6hNye3pZFBRZaTUfZFwGHxzppNaNOVgQuS/E/SlhrApuMXrpnK1EEIXfdo3Dg==} + engines: {node: '>=0.10.0'} + dev: true + + /is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + dependencies: + is-docker: 2.2.1 + dev: true + + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /jackspeak@3.4.0: + resolution: {integrity: sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: true + + /jake@10.9.1: + resolution: {integrity: sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==} + engines: {node: '>=10'} + hasBin: true + dependencies: + async: 3.2.5 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + dev: true + + /jiti@1.21.6: + resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} + hasBin: true + dev: true + + /js-beautify@1.15.1: + resolution: {integrity: sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==} + engines: {node: '>=14'} + hasBin: true + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.4 + glob: 10.4.2 + js-cookie: 3.0.5 + nopt: 7.2.1 + dev: true + + /js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + dev: true + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: true + + /js-tokens@9.0.0: + resolution: {integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==} + dev: true + + /jsesc@0.5.0: + resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} + hasBin: true + dev: true + + /jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: true + + /json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + dev: true + + /json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + dev: true + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + dev: true + + /jsonpointer@5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + dev: true + + /kind-of@3.2.2: + resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} + engines: {node: '>=0.10.0'} + dependencies: + is-buffer: 1.1.6 + dev: true + + /kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + dev: true + + /kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + dev: true + + /lean-qr@2.3.4: + resolution: {integrity: sha512-gr5mVwthZvcnaXXOBCjIKcBaEQBWhEdcynnSWfob6pZuDaN58iCJzSzMY8vehURzaECW7BSl3hv4k8B0HJIzxg==} + hasBin: true + dev: false + + /leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + dev: true + + /lighthouse-logger@1.4.2: + resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==} + dependencies: + debug: 2.6.9 + marky: 1.2.5 + transitivePeerDependencies: + - supports-color + dev: true + + /lightweight-charts@4.1.6: + resolution: {integrity: sha512-6NLRhYGSOorEXQireN+/Fh25lU2vzvHAcPujQZOqQGB/QGereMsoyLhuX5mz4odXune01mKhhmd8UTYIgRDGqg==} + dependencies: + fancy-canvas: 2.1.0 + dev: false + + /lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + dev: true + + /lilconfig@3.1.2: + resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} + engines: {node: '>=14'} + dev: true + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: true + + /local-pkg@0.5.0: + resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} + engines: {node: '>=14'} + dependencies: + mlly: 1.7.1 + pkg-types: 1.1.1 + dev: true + + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + dev: true + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + dev: true + + /lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + dev: true + + /lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + dev: true + + /lodash.uniqwith@4.5.0: + resolution: {integrity: sha512-7lYL8bLopMoy4CTICbxygAUq6CdRJ36vFc80DucPueUee+d5NBRxz3FdT9Pes/HEx5mPoT9jwnsEJWz1N7uq7Q==} + dev: true + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: true + + /lru-cache@10.2.2: + resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} + engines: {node: 14 || >=16.14} + dev: true + + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: true + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: true + + /magic-string@0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + dependencies: + sourcemap-codec: 1.4.8 + dev: true + + /magic-string@0.30.10: + resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /map-obj@1.0.1: + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} + engines: {node: '>=0.10.0'} + dev: true + + /map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + dev: true + + /marky@1.2.5: + resolution: {integrity: sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==} + dev: true + + /meow@9.0.0: + resolution: {integrity: sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==} + engines: {node: '>=10'} + dependencies: + '@types/minimist': 1.2.5 + camelcase-keys: 6.2.2 + decamelize: 1.2.0 + decamelize-keys: 1.1.1 + hard-rejection: 2.1.0 + minimist-options: 4.1.0 + normalize-package-data: 3.0.3 + read-pkg-up: 7.0.1 + redent: 3.0.0 + trim-newlines: 3.0.1 + type-fest: 0.18.1 + yargs-parser: 20.2.9 + dev: true + + /merge-anything@5.1.7: + resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} + engines: {node: '>=12.13'} + dependencies: + is-what: 4.1.16 + dev: true + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch@4.0.7: + resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + dev: true + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: true + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: true + + /mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + dev: true + + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: true + + /min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + dev: true + + /miniflare@3.20240610.1: + resolution: {integrity: sha512-ZkfSpBmX3nJW00yYhvF2kGvjb6f77TOimRR6+2GQvsArbwo6e0iYqLGM9aB/cnJzgFjLMvOv1qj4756iynSxJQ==} + engines: {node: '>=16.13'} + hasBin: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + acorn: 8.12.0 + acorn-walk: 8.3.3 + capnp-ts: 0.7.0 + exit-hook: 2.2.1 + glob-to-regexp: 0.4.1 + stoppable: 1.1.0 + undici: 5.28.4 + workerd: 1.20240610.1 + ws: 8.17.1 + youch: 3.3.3 + zod: 3.23.8 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimatch@9.0.1: + resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimatch@9.0.4: + resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimist-options@4.1.0: + resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} + engines: {node: '>= 6'} + dependencies: + arrify: 1.0.1 + is-plain-obj: 1.1.0 + kind-of: 6.0.3 + dev: true + + /minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + dev: true + + /mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + dev: true + + /mlly@1.7.1: + resolution: {integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==} + dependencies: + acorn: 8.12.0 + pathe: 1.1.2 + pkg-types: 1.1.1 + ufo: 1.5.3 + dev: true + + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + dev: true + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + dev: true + + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: true + + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /node-fetch-native@1.6.4: + resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} + dev: true + + /node-fetch@2.6.7: + resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: true + + /node-forge@1.3.1: + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + engines: {node: '>= 6.13.0'} + dev: true + + /node-releases@2.0.14: + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + dev: true + + /nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + dependencies: + abbrev: 2.0.0 + dev: true + + /normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.8 + semver: 5.7.2 + validate-npm-package-license: 3.0.4 + dev: true + + /normalize-package-data@3.0.3: + resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} + engines: {node: '>=10'} + dependencies: + hosted-git-info: 4.1.0 + is-core-module: 2.14.0 + semver: 7.6.2 + validate-npm-package-license: 3.0.4 + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + dev: true + + /npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + dependencies: + path-key: 3.1.1 + dev: true + + /nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + dependencies: + boolbase: 1.0.0 + dev: true + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + dev: true + + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: true + + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + dev: true + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: true + + /object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + dev: true + + /open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + dev: true + + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + dev: true + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + dev: true + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + dev: true + + /package-json-from-dist@1.0.0: + resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} + dev: true + + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.24.7 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + dev: true + + /parse5-htmlparser2-tree-adapter@7.0.0: + resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} + dependencies: + domhandler: 5.0.3 + parse5: 7.1.2 + dev: true + + /parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + dependencies: + entities: 4.5.0 + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + dependencies: + lru-cache: 10.2.2 + minipass: 7.1.2 + dev: true + + /path-to-regexp@6.2.2: + resolution: {integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==} + dev: true + + /pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + dev: true + + /pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + dev: true + + /picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + dev: true + + /pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + dev: true + + /pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + dev: true + + /pkg-types@1.1.1: + resolution: {integrity: sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ==} + dependencies: + confbox: 0.1.7 + mlly: 1.7.1 + pathe: 1.1.2 + dev: true + + /possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + dev: true + + /postcss-import@15.1.0(postcss@8.4.38): + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + dependencies: + postcss: 8.4.38 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.8 + dev: true + + /postcss-js@4.0.1(postcss@8.4.38): + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.38 + dev: true + + /postcss-load-config@4.0.2(postcss@8.4.38): + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 3.1.2 + postcss: 8.4.38 + yaml: 2.4.5 + dev: true + + /postcss-nested@6.0.1(postcss@8.4.38): + resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + postcss: 8.4.38 + postcss-selector-parser: 6.1.0 + dev: true + + /postcss-selector-parser@6.1.0: + resolution: {integrity: sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + + /postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + dev: true + + /postcss@8.4.38: + resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.1 + source-map-js: 1.2.0 + dev: true + + /prettier-plugin-tailwindcss@0.6.5(@ianvs/prettier-plugin-sort-imports@4.2.1)(prettier@3.3.2): + resolution: {integrity: sha512-axfeOArc/RiGHjOIy9HytehlC0ZLeMaqY09mm8YCkMzznKiDkwFzOpBvtuhuv3xG5qB73+Mj7OCe2j/L1ryfuQ==} + engines: {node: '>=14.21.3'} + peerDependencies: + '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-pug': '*' + '@shopify/prettier-plugin-liquid': '*' + '@trivago/prettier-plugin-sort-imports': '*' + '@zackad/prettier-plugin-twig-melody': '*' + prettier: ^3.0 + prettier-plugin-astro: '*' + prettier-plugin-css-order: '*' + prettier-plugin-import-sort: '*' + prettier-plugin-jsdoc: '*' + prettier-plugin-marko: '*' + prettier-plugin-organize-attributes: '*' + prettier-plugin-organize-imports: '*' + prettier-plugin-sort-imports: '*' + prettier-plugin-style-order: '*' + prettier-plugin-svelte: '*' + peerDependenciesMeta: + '@ianvs/prettier-plugin-sort-imports': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + '@zackad/prettier-plugin-twig-melody': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-import-sort: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-marko: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-sort-imports: + optional: true + prettier-plugin-style-order: + optional: true + prettier-plugin-svelte: + optional: true + dependencies: + '@ianvs/prettier-plugin-sort-imports': 4.2.1(prettier@3.3.2) + prettier: 3.3.2 + dev: true + + /prettier@3.3.2: + resolution: {integrity: sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==} + engines: {node: '>=14'} + hasBin: true + dev: true + + /pretty-bytes@5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + dev: true + + /pretty-bytes@6.1.1: + resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} + engines: {node: ^14.13.1 || >=16.0.0} + dev: true + + /pretty@2.0.0: + resolution: {integrity: sha512-G9xUchgTEiNpormdYBl+Pha50gOUovT18IvAe7EYMZ1/f9W/WWMPRn+xI68yXNMUk3QXHDwo/1wV/4NejVNe1w==} + engines: {node: '>=0.10.0'} + dependencies: + condense-newlines: 0.2.1 + extend-shallow: 2.0.1 + js-beautify: 1.15.1 + dev: true + + /printable-characters@1.0.42: + resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} + dev: true + + /progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + dev: true + + /proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + dev: true + + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: true + + /pump@3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: true + + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + dev: true + + /puppeteer-core@13.7.0: + resolution: {integrity: sha512-rXja4vcnAzFAP1OVLq/5dWNfwBGuzcOARJ6qGV7oAZhnLmVRU8G5MsdeQEAOy332ZhkIOnn9jp15R89LKHyp2Q==} + engines: {node: '>=10.18.1'} + dependencies: + cross-fetch: 3.1.5 + debug: 4.3.4 + devtools-protocol: 0.0.981744 + extract-zip: 2.0.1 + https-proxy-agent: 5.0.1 + pkg-dir: 4.2.0 + progress: 2.0.3 + proxy-from-env: 1.1.0 + rimraf: 3.0.2 + tar-fs: 2.1.1 + unbzip2-stream: 1.4.3 + ws: 8.5.0 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + dev: true + + /pwa-asset-generator@6.3.1: + resolution: {integrity: sha512-iGbUvBH1T+yysr/31OsJyIbqrDUPOHZPOvCODgQmiBkbfALxF9Wqmzc9ddYYe0x07/qAurc54IAt4rDR+vaQDQ==} + engines: {node: '>=10.12.0'} + hasBin: true + dependencies: + chalk: 4.1.2 + cheerio: 1.0.0-rc.12 + chrome-launcher: 0.15.2 + find-process: 1.4.7 + lodash.isequal: 4.5.0 + lodash.uniqwith: 4.5.0 + meow: 9.0.0 + mime-types: 2.1.35 + pretty: 2.0.0 + progress: 2.0.3 + puppeteer-core: 13.7.0 + slash: 3.0.0 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /quick-lru@4.0.1: + resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} + engines: {node: '>=8'} + dev: true + + /randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + dependencies: + pify: 2.3.0 + dev: true + + /read-pkg-up@7.0.1: + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + read-pkg: 5.2.0 + type-fest: 0.8.1 + dev: true + + /read-pkg@5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 2.5.0 + parse-json: 5.2.0 + type-fest: 0.6.0 + dev: true + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: true + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + dev: true + + /regenerate-unicode-properties@10.1.1: + resolution: {integrity: sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==} + engines: {node: '>=4'} + dependencies: + regenerate: 1.4.2 + dev: true + + /regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + dev: true + + /regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + dev: true + + /regenerator-transform@0.15.2: + resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} + dependencies: + '@babel/runtime': 7.24.7 + dev: true + + /regexp.prototype.flags@1.5.2: + resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 + dev: true + + /regexpu-core@5.3.2: + resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} + engines: {node: '>=4'} + dependencies: + '@babel/regjsgen': 0.8.0 + regenerate: 1.4.2 + regenerate-unicode-properties: 10.1.1 + regjsparser: 0.9.1 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.1.0 + dev: true + + /regjsparser@0.9.1: + resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} + hasBin: true + dependencies: + jsesc: 0.5.0 + dev: true + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: true + + /require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + dev: true + + /resolve.exports@2.0.2: + resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} + engines: {node: '>=10'} + dev: true + + /resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + dependencies: + is-core-module: 2.14.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rollup-plugin-inject@3.0.2: + resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==} + deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject. + dependencies: + estree-walker: 0.6.1 + magic-string: 0.25.9 + rollup-pluginutils: 2.8.2 + dev: true + + /rollup-plugin-node-polyfills@0.2.1: + resolution: {integrity: sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==} + dependencies: + rollup-plugin-inject: 3.0.2 + dev: true + + /rollup-plugin-visualizer@5.12.0(rollup@2.79.1): + resolution: {integrity: sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==} + engines: {node: '>=14'} + hasBin: true + peerDependencies: + rollup: 2.x || 3.x || 4.x + peerDependenciesMeta: + rollup: + optional: true + dependencies: + open: 8.4.2 + picomatch: 2.3.1 + rollup: 2.79.1 + source-map: 0.7.4 + yargs: 17.7.2 + dev: true + + /rollup-pluginutils@2.8.2: + resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} + dependencies: + estree-walker: 0.6.1 + dev: true + + /rollup@2.79.1: + resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==} + engines: {node: '>=10.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /rollup@4.18.0: + resolution: {integrity: sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + dependencies: + '@types/estree': 1.0.5 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.18.0 + '@rollup/rollup-android-arm64': 4.18.0 + '@rollup/rollup-darwin-arm64': 4.18.0 + '@rollup/rollup-darwin-x64': 4.18.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.18.0 + '@rollup/rollup-linux-arm-musleabihf': 4.18.0 + '@rollup/rollup-linux-arm64-gnu': 4.18.0 + '@rollup/rollup-linux-arm64-musl': 4.18.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.18.0 + '@rollup/rollup-linux-riscv64-gnu': 4.18.0 + '@rollup/rollup-linux-s390x-gnu': 4.18.0 + '@rollup/rollup-linux-x64-gnu': 4.18.0 + '@rollup/rollup-linux-x64-musl': 4.18.0 + '@rollup/rollup-win32-arm64-msvc': 4.18.0 + '@rollup/rollup-win32-ia32-msvc': 4.18.0 + '@rollup/rollup-win32-x64-msvc': 4.18.0 + fsevents: 2.3.3 + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /safe-array-concat@1.1.2: + resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: true + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true + + /safe-regex-test@1.0.3: + resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-regex: 1.1.4 + dev: true + + /scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + dev: true + + /selfsigned@2.4.1: + resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} + engines: {node: '>=10'} + dependencies: + '@types/node-forge': 1.3.11 + node-forge: 1.3.1 + dev: true + + /semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + dev: true + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: true + + /semver@7.6.2: + resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} + engines: {node: '>=10'} + hasBin: true + dev: true + + /serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + dependencies: + randombytes: 2.1.0 + dev: true + + /seroval-plugins@1.0.7(seroval@1.0.7): + resolution: {integrity: sha512-GO7TkWvodGp6buMEX9p7tNyIkbwlyuAWbI6G9Ec5bhcm7mQdu3JOK1IXbEUwb3FVzSc363GraG/wLW23NSavIw==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + dependencies: + seroval: 1.0.7 + + /seroval@1.0.7: + resolution: {integrity: sha512-n6ZMQX5q0Vn19Zq7CIKNIo7E75gPkGCFUEqDpa8jgwpYr/vScjqnQ6H09t1uIiZ0ZSK0ypEGvrYK2bhBGWsGdw==} + engines: {node: '>=10'} + + /set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + dev: true + + /set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.1 + dev: true + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: true + + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: true + + /smob@1.5.0: + resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==} + dev: true + + /solid-js@1.8.17: + resolution: {integrity: sha512-E0FkUgv9sG/gEBWkHr/2XkBluHb1fkrHywUgA6o6XolPDCJ4g1HaLmQufcBBhiF36ee40q+HpG/vCZu7fLpI3Q==} + dependencies: + csstype: 3.1.3 + seroval: 1.0.7 + seroval-plugins: 1.0.7(seroval@1.0.7) + + /solid-refresh@0.6.3(solid-js@1.8.17): + resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} + peerDependencies: + solid-js: ^1.3 + dependencies: + '@babel/generator': 7.24.7 + '@babel/helper-module-imports': 7.24.7 + '@babel/types': 7.24.7 + solid-js: 1.8.17 + transitivePeerDependencies: + - supports-color + dev: true + + /source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + dev: true + + /source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + dependencies: + whatwg-url: 7.1.0 + dev: true + + /sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + dev: true + + /spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.18 + dev: true + + /spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + dev: true + + /spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.18 + dev: true + + /spdx-license-ids@3.0.18: + resolution: {integrity: sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==} + dev: true + + /stacktracey@2.1.8: + resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} + dependencies: + as-table: 1.0.55 + get-source: 2.0.12 + dev: true + + /stoppable@1.1.0: + resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} + engines: {node: '>=4', npm: '>=6'} + dev: true + + /string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + dev: true + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: true + + /string.prototype.matchall@4.0.11: + resolution: {integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.7 + regexp.prototype.flags: 1.5.2 + set-function-name: 2.0.2 + side-channel: 1.0.6 + dev: true + + /string.prototype.trim@1.2.9: + resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + dev: true + + /string.prototype.trimend@1.0.8: + resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: true + + /string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: true + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /stringify-object@3.3.0: + resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} + engines: {node: '>=4'} + dependencies: + get-own-enumerable-property-symbols: 3.0.2 + is-obj: 1.0.1 + is-regexp: 1.0.0 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: true + + /strip-comments@2.0.1: + resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} + engines: {node: '>=10'} + dev: true + + /strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + dev: true + + /strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + dependencies: + min-indent: 1.0.1 + dev: true + + /strip-literal@2.1.0: + resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==} + dependencies: + js-tokens: 9.0.0 + dev: true + + /sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + commander: 4.1.1 + glob: 10.4.2 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + dev: true + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /tailwindcss@3.4.4: + resolution: {integrity: sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==} + engines: {node: '>=14.0.0'} + hasBin: true + 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.6 + lilconfig: 2.1.0 + micromatch: 4.0.7 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.1 + postcss: 8.4.38 + postcss-import: 15.1.0(postcss@8.4.38) + postcss-js: 4.0.1(postcss@8.4.38) + postcss-load-config: 4.0.2(postcss@8.4.38) + postcss-nested: 6.0.1(postcss@8.4.38) + postcss-selector-parser: 6.1.0 + resolve: 1.22.8 + sucrase: 3.35.0 + transitivePeerDependencies: + - ts-node + dev: true + + /tar-fs@2.1.1: + resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 2.2.0 + dev: true + + /tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: true + + /temp-dir@2.0.0: + resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} + engines: {node: '>=8'} + dev: true + + /tempy@0.6.0: + resolution: {integrity: sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==} + engines: {node: '>=10'} + dependencies: + is-stream: 2.0.1 + temp-dir: 2.0.0 + type-fest: 0.16.0 + unique-string: 2.0.0 + dev: true + + /terser@5.31.1: + resolution: {integrity: sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg==} + engines: {node: '>=10'} + hasBin: true + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.12.0 + commander: 2.20.3 + source-map-support: 0.5.21 + dev: true + + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: true + + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 + dev: true + + /through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + dev: true + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: true + + /tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + dependencies: + punycode: 2.3.1 + dev: true + + /trim-newlines@3.0.1: + resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} + engines: {node: '>=8'} + dev: true + + /ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: true + + /tslib@2.6.3: + resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + dev: true + + /type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + dev: true + + /type-fest@0.16.0: + resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} + engines: {node: '>=10'} + dev: true + + /type-fest@0.18.1: + resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} + engines: {node: '>=10'} + dev: true + + /type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + dev: true + + /type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + dev: true + + /typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-typed-array: 1.1.13 + dev: true + + /typed-array-byte-length@1.0.1: + resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + dev: true + + /typed-array-byte-offset@1.0.2: + resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + dev: true + + /typed-array-length@1.0.6: + resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 + dev: true + + /typescript@5.5.2: + resolution: {integrity: sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + + /ufo@1.5.3: + resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} + dev: true + + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.7 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: true + + /unbzip2-stream@1.4.3: + resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} + dependencies: + buffer: 5.7.1 + through: 2.3.8 + dev: true + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true + + /undici@5.28.4: + resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} + engines: {node: '>=14.0'} + dependencies: + '@fastify/busboy': 2.1.1 + dev: true + + /unenv-nightly@1.10.0-1717606461.a117952: + resolution: {integrity: sha512-u3TfBX02WzbHTpaEfWEKwDijDSFAHcgXkayUZ+MVDrjhLFvgAJzFGTSTmwlEhwWi2exyRQey23ah9wELMM6etg==} + dependencies: + consola: 3.2.3 + defu: 6.1.4 + mime: 3.0.0 + node-fetch-native: 1.6.4 + pathe: 1.1.2 + ufo: 1.5.3 + dev: true + + /unicode-canonical-property-names-ecmascript@2.0.0: + resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} + engines: {node: '>=4'} + dev: true + + /unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.0 + unicode-property-aliases-ecmascript: 2.1.0 + dev: true + + /unicode-match-property-value-ecmascript@2.1.0: + resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} + engines: {node: '>=4'} + dev: true + + /unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + dev: true + + /unimport@3.7.2(rollup@2.79.1): + resolution: {integrity: sha512-91mxcZTadgXyj3lFWmrGT8GyoRHWuE5fqPOjg5RVtF6vj+OfM5G6WCzXjuYtSgELE5ggB34RY4oiCSEP8I3AHw==} + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@2.79.1) + acorn: 8.12.0 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + fast-glob: 3.3.2 + local-pkg: 0.5.0 + magic-string: 0.30.10 + mlly: 1.7.1 + pathe: 1.1.2 + pkg-types: 1.1.1 + scule: 1.3.0 + strip-literal: 2.1.0 + unplugin: 1.10.1 + transitivePeerDependencies: + - rollup + dev: true + + /unique-string@2.0.0: + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + engines: {node: '>=8'} + dependencies: + crypto-random-string: 2.0.0 + dev: true + + /universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + dev: true + + /unplugin-auto-import@0.17.6(rollup@2.79.1): + resolution: {integrity: sha512-dmX0Pex5DzMzVuALkexboOZvh51fL/BD6aoPO7qHoTYGlQp0GRKsREv2KMF1lzYI9SXKQiRxAjwzbQnrFFNydQ==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': ^3.2.2 + '@vueuse/core': '*' + peerDependenciesMeta: + '@nuxt/kit': + optional: true + '@vueuse/core': + optional: true + dependencies: + '@antfu/utils': 0.7.8 + '@rollup/pluginutils': 5.1.0(rollup@2.79.1) + fast-glob: 3.3.2 + local-pkg: 0.5.0 + magic-string: 0.30.10 + minimatch: 9.0.4 + unimport: 3.7.2(rollup@2.79.1) + unplugin: 1.10.1 + transitivePeerDependencies: + - rollup + dev: true + + /unplugin-icons@0.19.0: + resolution: {integrity: sha512-u5g/gIZPZEj1wUGEQxe9nzftOSqmblhusc+sL3cawIRoIt/xWpE6XYcPOfAeFTYNjSbRrX/3QiX89PFiazgU1w==} + peerDependencies: + '@svgr/core': '>=7.0.0' + '@svgx/core': ^1.0.1 + '@vue/compiler-sfc': ^3.0.2 || ^2.7.0 + vue-template-compiler: ^2.6.12 + vue-template-es2015-compiler: ^1.9.0 + peerDependenciesMeta: + '@svgr/core': + optional: true + '@svgx/core': + optional: true + '@vue/compiler-sfc': + optional: true + vue-template-compiler: + optional: true + vue-template-es2015-compiler: + optional: true + dependencies: + '@antfu/install-pkg': 0.3.3 + '@antfu/utils': 0.7.8 + '@iconify/utils': 2.1.25 + debug: 4.3.5 + kolorist: 1.8.0 + local-pkg: 0.5.0 + unplugin: 1.10.1 + transitivePeerDependencies: + - supports-color + dev: true + + /unplugin@1.10.1: + resolution: {integrity: sha512-d6Mhq8RJeGA8UfKCu54Um4lFA0eSaRa3XxdAJg8tIdxbu1ubW0hBCZUL7yI2uGyYCRndvbK8FLHzqy2XKfeMsg==} + engines: {node: '>=14.0.0'} + dependencies: + acorn: 8.12.0 + chokidar: 3.6.0 + webpack-sources: 3.2.3 + webpack-virtual-modules: 0.6.2 + dev: true + + /upath@1.2.0: + resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} + engines: {node: '>=4'} + dev: true + + /update-browserslist-db@1.0.16(browserslist@4.23.1): + resolution: {integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.23.1 + escalade: 3.1.2 + picocolors: 1.0.1 + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.1 + dev: true + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true + + /validate-html-nesting@1.2.2: + resolution: {integrity: sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==} + dev: true + + /validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + dev: true + + /vite-plugin-pwa@0.20.0(vite@5.3.1)(workbox-build@7.1.1)(workbox-window@7.1.0): + resolution: {integrity: sha512-/kDZyqF8KqoXRpMUQtR5Atri/7BWayW8Gp7Kz/4bfstsV6zSFTxjREbXZYL7zSuRL40HGA+o2hvUAFRmC+bL7g==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@vite-pwa/assets-generator': ^0.2.4 + vite: ^3.1.0 || ^4.0.0 || ^5.0.0 + workbox-build: ^7.1.0 + workbox-window: ^7.1.0 + peerDependenciesMeta: + '@vite-pwa/assets-generator': + optional: true + dependencies: + debug: 4.3.5 + fast-glob: 3.3.2 + pretty-bytes: 6.1.1 + vite: 5.3.1 + workbox-build: 7.1.1 + workbox-window: 7.1.0 + transitivePeerDependencies: + - supports-color + dev: true + + /vite-plugin-solid@2.10.2(solid-js@1.8.17)(vite@5.3.1): + resolution: {integrity: sha512-AOEtwMe2baBSXMXdo+BUwECC8IFHcKS6WQV/1NEd+Q7vHPap5fmIhLcAzr+DUJ04/KHx/1UBU0l1/GWP+rMAPQ==} + peerDependencies: + '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* + solid-js: ^1.7.2 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + '@testing-library/jest-dom': + optional: true + dependencies: + '@babel/core': 7.24.7 + '@types/babel__core': 7.20.5 + babel-preset-solid: 1.8.17(@babel/core@7.24.7) + merge-anything: 5.1.7 + solid-js: 1.8.17 + solid-refresh: 0.6.3(solid-js@1.8.17) + vite: 5.3.1 + vitefu: 0.2.5(vite@5.3.1) + transitivePeerDependencies: + - supports-color + dev: true + + /vite@5.3.1: + resolution: {integrity: sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.21.5 + postcss: 8.4.38 + rollup: 4.18.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /vitefu@0.2.5(vite@5.3.1): + resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + vite: + optional: true + dependencies: + vite: 5.3.1 + dev: true + + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: true + + /webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + dev: true + + /webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + dev: true + + /webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + dev: true + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: true + + /whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + dev: true + + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: true + + /which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /workbox-background-sync@7.1.0: + resolution: {integrity: sha512-rMbgrzueVWDFcEq1610YyDW71z0oAXLfdRHRQcKw4SGihkfOK0JUEvqWHFwA6rJ+6TClnMIn7KQI5PNN1XQXwQ==} + dependencies: + idb: 7.1.1 + workbox-core: 7.1.0 + dev: true + + /workbox-broadcast-update@7.1.0: + resolution: {integrity: sha512-O36hIfhjej/c5ar95pO67k1GQw0/bw5tKP7CERNgK+JdxBANQhDmIuOXZTNvwb2IHBx9hj2kxvcDyRIh5nzOgQ==} + dependencies: + workbox-core: 7.1.0 + dev: true + + /workbox-build@7.1.1: + resolution: {integrity: sha512-WdkVdC70VMpf5NBCtNbiwdSZeKVuhTEd5PV3mAwpTQCGAB5XbOny1P9egEgNdetv4srAMmMKjvBk4RD58LpooA==} + engines: {node: '>=16.0.0'} + dependencies: + '@apideck/better-ajv-errors': 0.3.6(ajv@8.16.0) + '@babel/core': 7.24.7 + '@babel/preset-env': 7.24.7(@babel/core@7.24.7) + '@babel/runtime': 7.24.7 + '@rollup/plugin-babel': 5.3.1(@babel/core@7.24.7)(rollup@2.79.1) + '@rollup/plugin-node-resolve': 15.2.3(rollup@2.79.1) + '@rollup/plugin-replace': 2.4.2(rollup@2.79.1) + '@rollup/plugin-terser': 0.4.4(rollup@2.79.1) + '@surma/rollup-plugin-off-main-thread': 2.2.3 + ajv: 8.16.0 + common-tags: 1.8.2 + fast-json-stable-stringify: 2.1.0 + fs-extra: 9.1.0 + glob: 7.2.3 + lodash: 4.17.21 + pretty-bytes: 5.6.0 + rollup: 2.79.1 + source-map: 0.8.0-beta.0 + stringify-object: 3.3.0 + strip-comments: 2.0.1 + tempy: 0.6.0 + upath: 1.2.0 + workbox-background-sync: 7.1.0 + workbox-broadcast-update: 7.1.0 + workbox-cacheable-response: 7.1.0 + workbox-core: 7.1.0 + workbox-expiration: 7.1.0 + workbox-google-analytics: 7.1.0 + workbox-navigation-preload: 7.1.0 + workbox-precaching: 7.1.0 + workbox-range-requests: 7.1.0 + workbox-recipes: 7.1.0 + workbox-routing: 7.1.0 + workbox-strategies: 7.1.0 + workbox-streams: 7.1.0 + workbox-sw: 7.1.0 + workbox-window: 7.1.0 + transitivePeerDependencies: + - '@types/babel__core' + - supports-color + dev: true + + /workbox-cacheable-response@7.1.0: + resolution: {integrity: sha512-iwsLBll8Hvua3xCuBB9h92+/e0wdsmSVgR2ZlvcfjepZWwhd3osumQB3x9o7flj+FehtWM2VHbZn8UJeBXXo6Q==} + dependencies: + workbox-core: 7.1.0 + dev: true + + /workbox-core@7.1.0: + resolution: {integrity: sha512-5KB4KOY8rtL31nEF7BfvU7FMzKT4B5TkbYa2tzkS+Peqj0gayMT9SytSFtNzlrvMaWgv6y/yvP9C0IbpFjV30Q==} + dev: true + + /workbox-expiration@7.1.0: + resolution: {integrity: sha512-m5DcMY+A63rJlPTbbBNtpJ20i3enkyOtSgYfv/l8h+D6YbbNiA0zKEkCUaMsdDlxggla1oOfRkyqTvl5Ni5KQQ==} + dependencies: + idb: 7.1.1 + workbox-core: 7.1.0 + dev: true + + /workbox-google-analytics@7.1.0: + resolution: {integrity: sha512-FvE53kBQHfVTcZyczeBVRexhh7JTkyQ8HAvbVY6mXd2n2A7Oyz/9fIwnY406ZcDhvE4NFfKGjW56N4gBiqkrew==} + dependencies: + workbox-background-sync: 7.1.0 + workbox-core: 7.1.0 + workbox-routing: 7.1.0 + workbox-strategies: 7.1.0 + dev: true + + /workbox-navigation-preload@7.1.0: + resolution: {integrity: sha512-4wyAbo0vNI/X0uWNJhCMKxnPanNyhybsReMGN9QUpaePLTiDpKxPqFxl4oUmBNddPwIXug01eTSLVIFXimRG/A==} + dependencies: + workbox-core: 7.1.0 + dev: true + + /workbox-precaching@7.1.0: + resolution: {integrity: sha512-LyxzQts+UEpgtmfnolo0hHdNjoB7EoRWcF7EDslt+lQGd0lW4iTvvSe3v5JiIckQSB5KTW5xiCqjFviRKPj1zA==} + dependencies: + workbox-core: 7.1.0 + workbox-routing: 7.1.0 + workbox-strategies: 7.1.0 + dev: true + + /workbox-range-requests@7.1.0: + resolution: {integrity: sha512-m7+O4EHolNs5yb/79CrnwPR/g/PRzMFYEdo01LqwixVnc/sbzNSvKz0d04OE3aMRel1CwAAZQheRsqGDwATgPQ==} + dependencies: + workbox-core: 7.1.0 + dev: true + + /workbox-recipes@7.1.0: + resolution: {integrity: sha512-NRrk4ycFN9BHXJB6WrKiRX3W3w75YNrNrzSX9cEZgFB5ubeGoO8s/SDmOYVrFYp9HMw6sh1Pm3eAY/1gVS8YLg==} + dependencies: + workbox-cacheable-response: 7.1.0 + workbox-core: 7.1.0 + workbox-expiration: 7.1.0 + workbox-precaching: 7.1.0 + workbox-routing: 7.1.0 + workbox-strategies: 7.1.0 + dev: true + + /workbox-routing@7.1.0: + resolution: {integrity: sha512-oOYk+kLriUY2QyHkIilxUlVcFqwduLJB7oRZIENbqPGeBP/3TWHYNNdmGNhz1dvKuw7aqvJ7CQxn27/jprlTdg==} + dependencies: + workbox-core: 7.1.0 + dev: true + + /workbox-strategies@7.1.0: + resolution: {integrity: sha512-/UracPiGhUNehGjRm/tLUQ+9PtWmCbRufWtV0tNrALuf+HZ4F7cmObSEK+E4/Bx1p8Syx2tM+pkIrvtyetdlew==} + dependencies: + workbox-core: 7.1.0 + dev: true + + /workbox-streams@7.1.0: + resolution: {integrity: sha512-WyHAVxRXBMfysM8ORwiZnI98wvGWTVAq/lOyBjf00pXFvG0mNaVz4Ji+u+fKa/mf1i2SnTfikoYKto4ihHeS6w==} + dependencies: + workbox-core: 7.1.0 + workbox-routing: 7.1.0 + dev: true + + /workbox-sw@7.1.0: + resolution: {integrity: sha512-Hml/9+/njUXBglv3dtZ9WBKHI235AQJyLBV1G7EFmh4/mUdSQuXui80RtjDeVRrXnm/6QWgRUEHG3/YBVbxtsA==} + dev: true + + /workbox-window@7.1.0: + resolution: {integrity: sha512-ZHeROyqR+AS5UPzholQRDttLFqGMwP0Np8MKWAdyxsDETxq3qOAyXvqessc3GniohG6e0mAqSQyKOHmT8zPF7g==} + dependencies: + '@types/trusted-types': 2.0.7 + workbox-core: 7.1.0 + dev: true + + /workerd@1.20240610.1: + resolution: {integrity: sha512-Rtut5GrsODQMh6YU43b9WZ980Wd05Ov1/ds88pT/SoetmXFBvkBzdRfiHiATv+azmGX8KveE0i/Eqzk/yI01ug==} + engines: {node: '>=16'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20240610.1 + '@cloudflare/workerd-darwin-arm64': 1.20240610.1 + '@cloudflare/workerd-linux-64': 1.20240610.1 + '@cloudflare/workerd-linux-arm64': 1.20240610.1 + '@cloudflare/workerd-windows-64': 1.20240610.1 + dev: true + + /wrangler@3.61.0: + resolution: {integrity: sha512-feVAp0986x9xL3Dc1zin0ZVXKaqzp7eZur7iPLnpEwjG1Xy4dkVEZ5a1LET94Iyejt1P+EX5lgGcz63H7EfzUw==} + engines: {node: '>=16.17.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20240605.0 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + dependencies: + '@cloudflare/kv-asset-handler': 0.3.3 + '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) + '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) + blake3-wasm: 2.1.5 + chokidar: 3.6.0 + esbuild: 0.17.19 + miniflare: 3.20240610.1 + nanoid: 3.3.7 + path-to-regexp: 6.2.2 + resolve: 1.22.8 + resolve.exports: 2.0.2 + selfsigned: 2.4.1 + source-map: 0.6.1 + unenv: /unenv-nightly@1.10.0-1717606461.a117952 + xxhash-wasm: 1.0.2 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + + /ws@8.5.0: + resolution: {integrity: sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + + /xxhash-wasm@1.0.2: + resolution: {integrity: sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A==} + dev: true + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: true + + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: true + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true + + /yaml@2.4.5: + resolution: {integrity: sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==} + engines: {node: '>= 14'} + hasBin: true + dev: true + + /yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + dev: true + + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + dev: true + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.2 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + dev: true + + /yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + dev: true + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true + + /youch@3.3.3: + resolution: {integrity: sha512-qSFXUk3UZBLfggAW3dJKg0BMblG5biqSF8M34E06o5CSsZtH92u9Hqmj2RzGiHDi64fhe83+4tENFP2DB6t6ZA==} + dependencies: + cookie: 0.5.0 + mustache: 4.2.0 + stacktracey: 2.1.8 + dev: true + + /zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + dev: true diff --git a/app/prettier.config.mjs b/app/prettier.config.mjs new file mode 100644 index 000000000..0a88396a9 --- /dev/null +++ b/app/prettier.config.mjs @@ -0,0 +1,11 @@ +/** @type {import("prettier").Options} */ +export default { + plugins: [ + '@ianvs/prettier-plugin-sort-imports', + 'prettier-plugin-tailwindcss', // MUST come last + ], + + tailwindFunctions: ['classList'], + + importOrder: ['', '', '^/?(~|src)/', '', '^[./]'], +} diff --git a/app/public/assets/apple-icon-180.png b/app/public/assets/apple-icon-180.png new file mode 100644 index 000000000..b0913281f Binary files /dev/null and b/app/public/assets/apple-icon-180.png differ diff --git a/app/public/assets/apple-splash-1125-2436.jpg b/app/public/assets/apple-splash-1125-2436.jpg new file mode 100644 index 000000000..7f78f5aa0 Binary files /dev/null and b/app/public/assets/apple-splash-1125-2436.jpg differ diff --git a/app/public/assets/apple-splash-1136-640.jpg b/app/public/assets/apple-splash-1136-640.jpg new file mode 100644 index 000000000..fbc35cfc9 Binary files /dev/null and b/app/public/assets/apple-splash-1136-640.jpg differ diff --git a/app/public/assets/apple-splash-1170-2532.jpg b/app/public/assets/apple-splash-1170-2532.jpg new file mode 100644 index 000000000..320361065 Binary files /dev/null and b/app/public/assets/apple-splash-1170-2532.jpg differ diff --git a/app/public/assets/apple-splash-1179-2556.jpg b/app/public/assets/apple-splash-1179-2556.jpg new file mode 100644 index 000000000..caaacf9f0 Binary files /dev/null and b/app/public/assets/apple-splash-1179-2556.jpg differ diff --git a/app/public/assets/apple-splash-1242-2208.jpg b/app/public/assets/apple-splash-1242-2208.jpg new file mode 100644 index 000000000..3d8cdf992 Binary files /dev/null and b/app/public/assets/apple-splash-1242-2208.jpg differ diff --git a/app/public/assets/apple-splash-1242-2688.jpg b/app/public/assets/apple-splash-1242-2688.jpg new file mode 100644 index 000000000..97d169d73 Binary files /dev/null and b/app/public/assets/apple-splash-1242-2688.jpg differ diff --git a/app/public/assets/apple-splash-1284-2778.jpg b/app/public/assets/apple-splash-1284-2778.jpg new file mode 100644 index 000000000..8774cf740 Binary files /dev/null and b/app/public/assets/apple-splash-1284-2778.jpg differ diff --git a/app/public/assets/apple-splash-1290-2796.jpg b/app/public/assets/apple-splash-1290-2796.jpg new file mode 100644 index 000000000..21547a00c Binary files /dev/null and b/app/public/assets/apple-splash-1290-2796.jpg differ diff --git a/app/public/assets/apple-splash-1334-750.jpg b/app/public/assets/apple-splash-1334-750.jpg new file mode 100644 index 000000000..e5b1f1fca Binary files /dev/null and b/app/public/assets/apple-splash-1334-750.jpg differ diff --git a/app/public/assets/apple-splash-1488-2266.jpg b/app/public/assets/apple-splash-1488-2266.jpg new file mode 100644 index 000000000..d5d869422 Binary files /dev/null and b/app/public/assets/apple-splash-1488-2266.jpg differ diff --git a/app/public/assets/apple-splash-1536-2048.jpg b/app/public/assets/apple-splash-1536-2048.jpg new file mode 100644 index 000000000..086f96135 Binary files /dev/null and b/app/public/assets/apple-splash-1536-2048.jpg differ diff --git a/app/public/assets/apple-splash-1620-2160.jpg b/app/public/assets/apple-splash-1620-2160.jpg new file mode 100644 index 000000000..2a61f4f30 Binary files /dev/null and b/app/public/assets/apple-splash-1620-2160.jpg differ diff --git a/app/public/assets/apple-splash-1640-2360.jpg b/app/public/assets/apple-splash-1640-2360.jpg new file mode 100644 index 000000000..4f103471e Binary files /dev/null and b/app/public/assets/apple-splash-1640-2360.jpg differ diff --git a/app/public/assets/apple-splash-1668-2224.jpg b/app/public/assets/apple-splash-1668-2224.jpg new file mode 100644 index 000000000..c47d3f5c8 Binary files /dev/null and b/app/public/assets/apple-splash-1668-2224.jpg differ diff --git a/app/public/assets/apple-splash-1668-2388.jpg b/app/public/assets/apple-splash-1668-2388.jpg new file mode 100644 index 000000000..419042f5c Binary files /dev/null and b/app/public/assets/apple-splash-1668-2388.jpg differ diff --git a/app/public/assets/apple-splash-1792-828.jpg b/app/public/assets/apple-splash-1792-828.jpg new file mode 100644 index 000000000..0f72f910e Binary files /dev/null and b/app/public/assets/apple-splash-1792-828.jpg differ diff --git a/app/public/assets/apple-splash-2048-1536.jpg b/app/public/assets/apple-splash-2048-1536.jpg new file mode 100644 index 000000000..f1aeca9a9 Binary files /dev/null and b/app/public/assets/apple-splash-2048-1536.jpg differ diff --git a/app/public/assets/apple-splash-2048-2732.jpg b/app/public/assets/apple-splash-2048-2732.jpg new file mode 100644 index 000000000..8ab1a7655 Binary files /dev/null and b/app/public/assets/apple-splash-2048-2732.jpg differ diff --git a/app/public/assets/apple-splash-2160-1620.jpg b/app/public/assets/apple-splash-2160-1620.jpg new file mode 100644 index 000000000..7af7d35fa Binary files /dev/null and b/app/public/assets/apple-splash-2160-1620.jpg differ diff --git a/app/public/assets/apple-splash-2208-1242.jpg b/app/public/assets/apple-splash-2208-1242.jpg new file mode 100644 index 000000000..b2d3b67bc Binary files /dev/null and b/app/public/assets/apple-splash-2208-1242.jpg differ diff --git a/app/public/assets/apple-splash-2224-1668.jpg b/app/public/assets/apple-splash-2224-1668.jpg new file mode 100644 index 000000000..26f1d2d21 Binary files /dev/null and b/app/public/assets/apple-splash-2224-1668.jpg differ diff --git a/app/public/assets/apple-splash-2266-1488.jpg b/app/public/assets/apple-splash-2266-1488.jpg new file mode 100644 index 000000000..f1be40938 Binary files /dev/null and b/app/public/assets/apple-splash-2266-1488.jpg differ diff --git a/app/public/assets/apple-splash-2360-1640.jpg b/app/public/assets/apple-splash-2360-1640.jpg new file mode 100644 index 000000000..759356739 Binary files /dev/null and b/app/public/assets/apple-splash-2360-1640.jpg differ diff --git a/app/public/assets/apple-splash-2388-1668.jpg b/app/public/assets/apple-splash-2388-1668.jpg new file mode 100644 index 000000000..75c629808 Binary files /dev/null and b/app/public/assets/apple-splash-2388-1668.jpg differ diff --git a/app/public/assets/apple-splash-2436-1125.jpg b/app/public/assets/apple-splash-2436-1125.jpg new file mode 100644 index 000000000..7843e61d6 Binary files /dev/null and b/app/public/assets/apple-splash-2436-1125.jpg differ diff --git a/app/public/assets/apple-splash-2532-1170.jpg b/app/public/assets/apple-splash-2532-1170.jpg new file mode 100644 index 000000000..d8e674fe5 Binary files /dev/null and b/app/public/assets/apple-splash-2532-1170.jpg differ diff --git a/app/public/assets/apple-splash-2556-1179.jpg b/app/public/assets/apple-splash-2556-1179.jpg new file mode 100644 index 000000000..f35157851 Binary files /dev/null and b/app/public/assets/apple-splash-2556-1179.jpg differ diff --git a/app/public/assets/apple-splash-2688-1242.jpg b/app/public/assets/apple-splash-2688-1242.jpg new file mode 100644 index 000000000..4c29d81a5 Binary files /dev/null and b/app/public/assets/apple-splash-2688-1242.jpg differ diff --git a/app/public/assets/apple-splash-2732-2048.jpg b/app/public/assets/apple-splash-2732-2048.jpg new file mode 100644 index 000000000..c725925f0 Binary files /dev/null and b/app/public/assets/apple-splash-2732-2048.jpg differ diff --git a/app/public/assets/apple-splash-2778-1284.jpg b/app/public/assets/apple-splash-2778-1284.jpg new file mode 100644 index 000000000..ec38888ba Binary files /dev/null and b/app/public/assets/apple-splash-2778-1284.jpg differ diff --git a/app/public/assets/apple-splash-2796-1290.jpg b/app/public/assets/apple-splash-2796-1290.jpg new file mode 100644 index 000000000..24ea1b64b Binary files /dev/null and b/app/public/assets/apple-splash-2796-1290.jpg differ diff --git a/app/public/assets/apple-splash-640-1136.jpg b/app/public/assets/apple-splash-640-1136.jpg new file mode 100644 index 000000000..39ff1ba88 Binary files /dev/null and b/app/public/assets/apple-splash-640-1136.jpg differ diff --git a/app/public/assets/apple-splash-750-1334.jpg b/app/public/assets/apple-splash-750-1334.jpg new file mode 100644 index 000000000..58ff472b7 Binary files /dev/null and b/app/public/assets/apple-splash-750-1334.jpg differ diff --git a/app/public/assets/apple-splash-828-1792.jpg b/app/public/assets/apple-splash-828-1792.jpg new file mode 100644 index 000000000..83c2306c5 Binary files /dev/null and b/app/public/assets/apple-splash-828-1792.jpg differ diff --git a/app/public/assets/apple-splash-dark-1125-2436.jpg b/app/public/assets/apple-splash-dark-1125-2436.jpg new file mode 100644 index 000000000..ae196935a Binary files /dev/null and b/app/public/assets/apple-splash-dark-1125-2436.jpg differ diff --git a/app/public/assets/apple-splash-dark-1136-640.jpg b/app/public/assets/apple-splash-dark-1136-640.jpg new file mode 100644 index 000000000..beb03eb16 Binary files /dev/null and b/app/public/assets/apple-splash-dark-1136-640.jpg differ diff --git a/app/public/assets/apple-splash-dark-1170-2532.jpg b/app/public/assets/apple-splash-dark-1170-2532.jpg new file mode 100644 index 000000000..1e3b099cc Binary files /dev/null and b/app/public/assets/apple-splash-dark-1170-2532.jpg differ diff --git a/app/public/assets/apple-splash-dark-1179-2556.jpg b/app/public/assets/apple-splash-dark-1179-2556.jpg new file mode 100644 index 000000000..63d3eb203 Binary files /dev/null and b/app/public/assets/apple-splash-dark-1179-2556.jpg differ diff --git a/app/public/assets/apple-splash-dark-1242-2208.jpg b/app/public/assets/apple-splash-dark-1242-2208.jpg new file mode 100644 index 000000000..ec95e73e0 Binary files /dev/null and b/app/public/assets/apple-splash-dark-1242-2208.jpg differ diff --git a/app/public/assets/apple-splash-dark-1242-2688.jpg b/app/public/assets/apple-splash-dark-1242-2688.jpg new file mode 100644 index 000000000..ae217243d Binary files /dev/null and b/app/public/assets/apple-splash-dark-1242-2688.jpg differ diff --git a/app/public/assets/apple-splash-dark-1284-2778.jpg b/app/public/assets/apple-splash-dark-1284-2778.jpg new file mode 100644 index 000000000..d859fdeaf Binary files /dev/null and b/app/public/assets/apple-splash-dark-1284-2778.jpg differ diff --git a/app/public/assets/apple-splash-dark-1290-2796.jpg b/app/public/assets/apple-splash-dark-1290-2796.jpg new file mode 100644 index 000000000..2b9ec5216 Binary files /dev/null and b/app/public/assets/apple-splash-dark-1290-2796.jpg differ diff --git a/app/public/assets/apple-splash-dark-1334-750.jpg b/app/public/assets/apple-splash-dark-1334-750.jpg new file mode 100644 index 000000000..dcddcabe0 Binary files /dev/null and b/app/public/assets/apple-splash-dark-1334-750.jpg differ diff --git a/app/public/assets/apple-splash-dark-1488-2266.jpg b/app/public/assets/apple-splash-dark-1488-2266.jpg new file mode 100644 index 000000000..e8694ffa3 Binary files /dev/null and b/app/public/assets/apple-splash-dark-1488-2266.jpg differ diff --git a/app/public/assets/apple-splash-dark-1536-2048.jpg b/app/public/assets/apple-splash-dark-1536-2048.jpg new file mode 100644 index 000000000..36f6167c0 Binary files /dev/null and b/app/public/assets/apple-splash-dark-1536-2048.jpg differ diff --git a/app/public/assets/apple-splash-dark-1620-2160.jpg b/app/public/assets/apple-splash-dark-1620-2160.jpg new file mode 100644 index 000000000..c1b3afdc6 Binary files /dev/null and b/app/public/assets/apple-splash-dark-1620-2160.jpg differ diff --git a/app/public/assets/apple-splash-dark-1640-2360.jpg b/app/public/assets/apple-splash-dark-1640-2360.jpg new file mode 100644 index 000000000..5707aeee8 Binary files /dev/null and b/app/public/assets/apple-splash-dark-1640-2360.jpg differ diff --git a/app/public/assets/apple-splash-dark-1668-2224.jpg b/app/public/assets/apple-splash-dark-1668-2224.jpg new file mode 100644 index 000000000..56fa46c99 Binary files /dev/null and b/app/public/assets/apple-splash-dark-1668-2224.jpg differ diff --git a/app/public/assets/apple-splash-dark-1668-2388.jpg b/app/public/assets/apple-splash-dark-1668-2388.jpg new file mode 100644 index 000000000..3cc4c2427 Binary files /dev/null and b/app/public/assets/apple-splash-dark-1668-2388.jpg differ diff --git a/app/public/assets/apple-splash-dark-1792-828.jpg b/app/public/assets/apple-splash-dark-1792-828.jpg new file mode 100644 index 000000000..a74f5d38d Binary files /dev/null and b/app/public/assets/apple-splash-dark-1792-828.jpg differ diff --git a/app/public/assets/apple-splash-dark-2048-1536.jpg b/app/public/assets/apple-splash-dark-2048-1536.jpg new file mode 100644 index 000000000..7177ca051 Binary files /dev/null and b/app/public/assets/apple-splash-dark-2048-1536.jpg differ diff --git a/app/public/assets/apple-splash-dark-2048-2732.jpg b/app/public/assets/apple-splash-dark-2048-2732.jpg new file mode 100644 index 000000000..3d43c1915 Binary files /dev/null and b/app/public/assets/apple-splash-dark-2048-2732.jpg differ diff --git a/app/public/assets/apple-splash-dark-2160-1620.jpg b/app/public/assets/apple-splash-dark-2160-1620.jpg new file mode 100644 index 000000000..6196c474a Binary files /dev/null and b/app/public/assets/apple-splash-dark-2160-1620.jpg differ diff --git a/app/public/assets/apple-splash-dark-2208-1242.jpg b/app/public/assets/apple-splash-dark-2208-1242.jpg new file mode 100644 index 000000000..9bec2190b Binary files /dev/null and b/app/public/assets/apple-splash-dark-2208-1242.jpg differ diff --git a/app/public/assets/apple-splash-dark-2224-1668.jpg b/app/public/assets/apple-splash-dark-2224-1668.jpg new file mode 100644 index 000000000..8066c4ef0 Binary files /dev/null and b/app/public/assets/apple-splash-dark-2224-1668.jpg differ diff --git a/app/public/assets/apple-splash-dark-2266-1488.jpg b/app/public/assets/apple-splash-dark-2266-1488.jpg new file mode 100644 index 000000000..61aa0a2dc Binary files /dev/null and b/app/public/assets/apple-splash-dark-2266-1488.jpg differ diff --git a/app/public/assets/apple-splash-dark-2360-1640.jpg b/app/public/assets/apple-splash-dark-2360-1640.jpg new file mode 100644 index 000000000..f2777a14f Binary files /dev/null and b/app/public/assets/apple-splash-dark-2360-1640.jpg differ diff --git a/app/public/assets/apple-splash-dark-2388-1668.jpg b/app/public/assets/apple-splash-dark-2388-1668.jpg new file mode 100644 index 000000000..b3e4af689 Binary files /dev/null and b/app/public/assets/apple-splash-dark-2388-1668.jpg differ diff --git a/app/public/assets/apple-splash-dark-2436-1125.jpg b/app/public/assets/apple-splash-dark-2436-1125.jpg new file mode 100644 index 000000000..4a692bc6d Binary files /dev/null and b/app/public/assets/apple-splash-dark-2436-1125.jpg differ diff --git a/app/public/assets/apple-splash-dark-2532-1170.jpg b/app/public/assets/apple-splash-dark-2532-1170.jpg new file mode 100644 index 000000000..a0cd06b1a Binary files /dev/null and b/app/public/assets/apple-splash-dark-2532-1170.jpg differ diff --git a/app/public/assets/apple-splash-dark-2556-1179.jpg b/app/public/assets/apple-splash-dark-2556-1179.jpg new file mode 100644 index 000000000..07d23544e Binary files /dev/null and b/app/public/assets/apple-splash-dark-2556-1179.jpg differ diff --git a/app/public/assets/apple-splash-dark-2688-1242.jpg b/app/public/assets/apple-splash-dark-2688-1242.jpg new file mode 100644 index 000000000..36c760682 Binary files /dev/null and b/app/public/assets/apple-splash-dark-2688-1242.jpg differ diff --git a/app/public/assets/apple-splash-dark-2732-2048.jpg b/app/public/assets/apple-splash-dark-2732-2048.jpg new file mode 100644 index 000000000..69d0fd9bd Binary files /dev/null and b/app/public/assets/apple-splash-dark-2732-2048.jpg differ diff --git a/app/public/assets/apple-splash-dark-2778-1284.jpg b/app/public/assets/apple-splash-dark-2778-1284.jpg new file mode 100644 index 000000000..786763ac3 Binary files /dev/null and b/app/public/assets/apple-splash-dark-2778-1284.jpg differ diff --git a/app/public/assets/apple-splash-dark-2796-1290.jpg b/app/public/assets/apple-splash-dark-2796-1290.jpg new file mode 100644 index 000000000..ac158f211 Binary files /dev/null and b/app/public/assets/apple-splash-dark-2796-1290.jpg differ diff --git a/app/public/assets/apple-splash-dark-640-1136.jpg b/app/public/assets/apple-splash-dark-640-1136.jpg new file mode 100644 index 000000000..77b73416f Binary files /dev/null and b/app/public/assets/apple-splash-dark-640-1136.jpg differ diff --git a/app/public/assets/apple-splash-dark-750-1334.jpg b/app/public/assets/apple-splash-dark-750-1334.jpg new file mode 100644 index 000000000..6f2b2a324 Binary files /dev/null and b/app/public/assets/apple-splash-dark-750-1334.jpg differ diff --git a/app/public/assets/apple-splash-dark-828-1792.jpg b/app/public/assets/apple-splash-dark-828-1792.jpg new file mode 100644 index 000000000..ffe534d55 Binary files /dev/null and b/app/public/assets/apple-splash-dark-828-1792.jpg differ diff --git a/app/public/assets/favicon-196.png b/app/public/assets/favicon-196.png new file mode 100644 index 000000000..b2dc950f4 Binary files /dev/null and b/app/public/assets/favicon-196.png differ diff --git a/app/public/assets/manifest-icon-192.maskable.png b/app/public/assets/manifest-icon-192.maskable.png new file mode 100644 index 000000000..f526d33c0 Binary files /dev/null and b/app/public/assets/manifest-icon-192.maskable.png differ diff --git a/app/public/assets/manifest-icon-512.maskable.png b/app/public/assets/manifest-icon-512.maskable.png new file mode 100644 index 000000000..ec25090fa Binary files /dev/null and b/app/public/assets/manifest-icon-512.maskable.png differ diff --git a/app/public/fonts/Lexend.var.woff2 b/app/public/fonts/Lexend.var.woff2 new file mode 100644 index 000000000..8c1b2a47c Binary files /dev/null and b/app/public/fonts/Lexend.var.woff2 differ diff --git a/app/public/logo/black.svg b/app/public/logo/black.svg new file mode 100644 index 000000000..2cbcf2fc0 --- /dev/null +++ b/app/public/logo/black.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/app/public/logo/white.svg b/app/public/logo/white.svg new file mode 100644 index 000000000..fa94eedde --- /dev/null +++ b/app/public/logo/white.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/app/public/manifest.webmanifest b/app/public/manifest.webmanifest new file mode 100644 index 000000000..db99fbfb2 --- /dev/null +++ b/app/public/manifest.webmanifest @@ -0,0 +1,37 @@ +{ + "name": "Satonomics", + "short_name": "Satonomics", + "description": "Satoshi Economics", + "start_url": "/", + "display": "standalone", + "theme_color": "#0c0a09", + "background_color": "#0c0a09", + "lang": "en", + "scope": "/", + "icons": [ + { + "src": "/assets/manifest-icon-192.maskable.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/assets/manifest-icon-192.maskable.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/assets/manifest-icon-512.maskable.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/assets/manifest-icon-512.maskable.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/app/public/robots.txt b/app/public/robots.txt new file mode 100644 index 000000000..14267e903 --- /dev/null +++ b/app/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Allow: / \ No newline at end of file diff --git a/app/src/app/components/background.tsx b/app/src/app/components/background.tsx new file mode 100644 index 000000000..417e65da1 --- /dev/null +++ b/app/src/app/components/background.tsx @@ -0,0 +1,175 @@ +import { createRWS } from "/src/solid/rws"; + +const texts = [ + "satonomics", + "satonomics", + "satonomics", + + "stay humble, stack sats", + "21 million", + "cold storage", + "utxo", + "satoshi nakamoto", + "hodl", + `don't trust, verify`, + "zap", + "bitcoin", + "lightning", + "nostr", + "freedom tech", + "2008/10/31", + "2009/01/03", + "2010/05/22", + "hodl!", + "Hal Finney", + "Vote for better money", + "gradually then suddenly", + "timechain", + "self custody", + "be your own bank", + "resistance money", + "foss", +]; + +export const LOCAL_STORAGE_MARQUEE_KEY = "bg-marquee"; + +export function Background({ + marquee: on, + focused, +}: { + marquee: Accessor; + focused: Accessor; +}) { + createEffect(() => { + if (on()) { + localStorage.removeItem(LOCAL_STORAGE_MARQUEE_KEY); + } else { + localStorage.setItem(LOCAL_STORAGE_MARQUEE_KEY, "false"); + } + }); + + return ( + <> +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ +
+ + ); +} + +function Line({ + on, + focused, +}: { + on: Accessor; + focused: Accessor; +}) { + const shuffled = shuffle([...texts]); + shuffled.pop(); + const joined = shuffled.join(". "); + + return ( +
+ +
+ ); +} + +function TextWrapper({ + joined, + on, + focused, +}: { + on: Accessor; + focused: Accessor; + joined: string; +}) { + const seconds = joined.length * 2; + + const wasOnceOn = createRWS(false); + + createEffect(() => { + if (!wasOnceOn() && on()) { + wasOnceOn.set(true); + } + }); + + return ( +

+ {joined} {wasOnceOn() ? joined : undefined} +

+ ); +} + +function shuffle([...arr]: T[]): T[] { + let m = arr.length; + + while (m) { + const i = Math.floor(Math.random() * m--); + [arr[m], arr[i]] = [arr[i], arr[m]]; + } + + return arr; +} + +function Noise() { + return ( + + + + + + + + ); +} diff --git a/app/src/app/components/frames/box.tsx b/app/src/app/components/frames/box.tsx new file mode 100644 index 000000000..709894a83 --- /dev/null +++ b/app/src/app/components/frames/box.tsx @@ -0,0 +1,154 @@ +import { createResizeObserver } from "@solid-primitives/resize-observer"; + +import { classPropToString } from "/src/solid/classes"; +import { createRWS } from "/src/solid/rws"; + +export function Box({ + flex = true, + absolute, + padded = true, + children, + dark, + overflowY, +}: { + flex?: boolean; + absolute?: "top" | "bottom"; + padded?: boolean; + dark?: boolean; + overflowY?: boolean; +} & ParentProps) { + const maybeScrollable = createRWS(undefined); + const scrollable = createRWS(false); + const showLeftArrow = createRWS(false); + const showRightArrow = createRWS(false); + + onMount(() => { + createResizeObserver(maybeScrollable, (_, el) => { + if (el !== maybeScrollable()) { + return; + } + + scrollable.set(() => el.scrollWidth > el.clientWidth); + + checkArrows(); + }); + }); + + function checkArrows() { + const offset = 20; + + const target = maybeScrollable()!; + + const left = target.scrollLeft; + const right = target.scrollWidth - target.scrollLeft - target.clientWidth; + + showLeftArrow.set(() => left > offset); + showRightArrow.set(() => right > offset); + } + + return ( +
+
+ + {(obj) => ( + +
+ +
+
+ + )} + + +
+ {children} +
+
+
+ ); +} diff --git a/app/src/app/components/frames/button.tsx b/app/src/app/components/frames/button.tsx new file mode 100644 index 000000000..9bd61ccda --- /dev/null +++ b/app/src/app/components/frames/button.tsx @@ -0,0 +1,13 @@ +export function Button({ + onClick, + children, +}: { onClick: VoidFunction } & ParentProps) { + return ( + + ); +} diff --git a/app/src/app/components/frames/chart/components/actions.tsx b/app/src/app/components/frames/chart/components/actions.tsx new file mode 100644 index 000000000..47ded6462 --- /dev/null +++ b/app/src/app/components/frames/chart/components/actions.tsx @@ -0,0 +1,102 @@ +import type { Generate } from "lean-qr"; + +import { chartState } from "/src/scripts/lightweightCharts/chart/state"; +import { setTimeScale } from "/src/scripts/lightweightCharts/chart/time"; +import { classPropToString } from "/src/solid/classes"; +import { createRWS } from "/src/solid/rws"; + +export function Actions({ + presets, + fullscreen, + qrcode, +}: { + presets: Presets; + qrcode: RWS; + fullscreen?: RWS; +}) { + const leanQRGenerate = createRWS(undefined); + + onMount(() => { + import("lean-qr").then((leanQR) => { + leanQRGenerate.set(() => leanQR.generate); + }); + }); + + return ( +
+
+ ); +} + +function Button({ + icon, + colors, + onClick, + disabled, + classes, +}: { + icon: () => ValidComponent; + colors?: () => string; + onClick: VoidFunction; + disabled?: () => boolean; + classes?: string; +}) { + return ( + + ); +} diff --git a/app/src/app/components/frames/chart/components/chart.tsx b/app/src/app/components/frames/chart/components/chart.tsx new file mode 100644 index 000000000..eb2be64aa --- /dev/null +++ b/app/src/app/components/frames/chart/components/chart.tsx @@ -0,0 +1,33 @@ +import { cleanChart } from "/src/scripts/lightweightCharts/chart/clean"; +import { renderChart } from "/src/scripts/lightweightCharts/chart/render"; + +export function Chart({ + presets, + datasets, + legendSetter, + activeResources, +}: { + presets: Presets; + datasets: Datasets; + legendSetter: Setter; + activeResources: Accessor>>; +}) { + onMount(() => { + createEffect(() => { + const preset = presets.selected(); + + untrack(() => + renderChart({ + datasets, + preset, + legendSetter, + activeResources, + }), + ); + }); + + onCleanup(cleanChart); + }); + + return
; +} diff --git a/app/src/app/components/frames/chart/components/legend.tsx b/app/src/app/components/frames/chart/components/legend.tsx new file mode 100644 index 000000000..443adb5d4 --- /dev/null +++ b/app/src/app/components/frames/chart/components/legend.tsx @@ -0,0 +1,134 @@ +import { createRWS } from "/src/solid/rws"; + +const transparency = "66"; + +export function Legend({ + legend: legendList, +}: { + legend: Accessor; +}) { + const hovering = createRWS(undefined); + + let toggle = false; + + return ( +
+ + {(legend) => { + const initialColors = {} as any; + const darkenColors = {} as any; + + Object.entries(legend.series.options()).forEach(([k, v]) => { + if (k.toLowerCase().includes("color") && v) { + initialColors[k] = v; + darkenColors[k] = `${v}${transparency}`; + } else if (k === "lastValueVisible" && v) { + initialColors[k] = v; + darkenColors[k] = !v; + } + }); + + createEffect(() => { + if (hovering()) { + if (hovering()?.title !== legend.title) { + legend.series.applyOptions(darkenColors); + } + } else { + legend.series.applyOptions(initialColors); + } + }); + + let previousClickValueOf: number = 0; + + return ( + + + + ); + }} + +
+ ); +} diff --git a/app/src/app/components/frames/chart/components/timeScale.tsx b/app/src/app/components/frames/chart/components/timeScale.tsx new file mode 100644 index 000000000..2916b2247 --- /dev/null +++ b/app/src/app/components/frames/chart/components/timeScale.tsx @@ -0,0 +1,67 @@ +import { chartState } from "/src/scripts/lightweightCharts/chart/state"; +import { GENESIS_DAY } from "/src/scripts/lightweightCharts/chart/whitespace"; +import { ONE_DAY_IN_MS } from "/src/scripts/utils/time"; + +import { Box } from "../../box"; + +export function TimeScale() { + return ( + + + + + + + + + + + + ); +} + +function Button(props: ParentProps & { onClick: VoidFunction }) { + return ( + + ); +} + +function setTimeScale(days?: number) { + const to = new Date(); + + if (days) { + const from = new Date(); + from.setDate(from.getUTCDate() - days); + + chartState.chart?.timeScale().setVisibleRange({ + from: (from.getTime() / 1000) as Time, + to: (to.getTime() / 1000) as Time, + }); + } else { + // chartState.chart?.timeScale().fitContent(); + chartState.chart?.timeScale().setVisibleRange({ + from: (new Date( + // datasets.candlesticks.values()?.[0]?.date || "", + GENESIS_DAY, + ).getTime() / 1000) as Time, + to: (to.getTime() / 1000) as Time, + }); + } +} diff --git a/app/src/app/components/frames/chart/components/title.tsx b/app/src/app/components/frames/chart/components/title.tsx new file mode 100644 index 000000000..864b60a68 --- /dev/null +++ b/app/src/app/components/frames/chart/components/title.tsx @@ -0,0 +1,12 @@ +export function Title({ presets }: { presets: Presets }) { + return ( +
+
+

{`/ ${[...presets.selected().path.map(({ name }) => name), presets.selected().name].join(" / ")}`}

+

+ {presets.selected().title} +

+
+
+ ); +} diff --git a/app/src/app/components/frames/chart/index.tsx b/app/src/app/components/frames/chart/index.tsx new file mode 100644 index 000000000..8de78d1a5 --- /dev/null +++ b/app/src/app/components/frames/chart/index.tsx @@ -0,0 +1,72 @@ +import { classPropToString } from "/src/solid/classes"; +import { createRWS } from "/src/solid/rws"; + +import { Box } from "../box"; +import { Actions } from "./components/actions"; +import { Legend } from "./components/legend"; +import { TimeScale } from "./components/timeScale"; +import { Title } from "./components/title"; + +export function ChartFrame({ + presets, + datasets, + activeResources, + hide, + qrcode, + standalone, + fullscreen, +}: { + presets: Presets; + hide?: Accessor; + qrcode: RWS; + activeResources: Accessor>>; + datasets: Datasets; + fullscreen?: RWS; + standalone: boolean; +}) { + const legend = createRWS([]); + + const Chart = lazy(() => + import("./components/chart").then((d) => ({ + default: d.Chart, + })), + ); + + return ( +
+ + + + <div class="-mx-2 border-t border-orange-200/15" /> + + <div class="flex pt-1.5"> + <Legend legend={legend} /> + + <div class="-my-1.5 border-l border-orange-200/15 pr-1.5" /> + + <Actions presets={presets} qrcode={qrcode} fullscreen={fullscreen} /> + </div> + </Box> + + <div class="-mt-2 min-h-0 flex-1"> + <Chart + activeResources={activeResources} + datasets={datasets} + legendSetter={legend.set} + presets={presets} + /> + </div> + + <TimeScale /> + </div> + ); +} diff --git a/app/src/app/components/frames/counter.tsx b/app/src/app/components/frames/counter.tsx new file mode 100644 index 000000000..eb33509bf --- /dev/null +++ b/app/src/app/components/frames/counter.tsx @@ -0,0 +1,25 @@ +export function Counter({ + count, + name, + setRef, +}: { + count: () => number; + name: string; + setRef?: Setter<HTMLDivElement | undefined>; +}) { + return ( + <div + ref={setRef} + class="text-orange-100/75" + style={{ + "border-style": count() ? "dashed" : "none", + }} + > + Counted{" "} + <span class="font-medium text-orange-400/75"> + {count().toLocaleString("en-us")} + </span>{" "} + {name} + </div> + ); +} diff --git a/app/src/app/components/frames/favorites.tsx b/app/src/app/components/frames/favorites.tsx new file mode 100644 index 000000000..32ba7ef62 --- /dev/null +++ b/app/src/app/components/frames/favorites.tsx @@ -0,0 +1,48 @@ +import { Header } from "./header"; +import { Line } from "./line"; +import { Number } from "./number"; + +export function FavoritesFrame({ + presets, + selectedFrame, +}: { + presets: Presets; + selectedFrame: Accessor<FrameName>; +}) { + return ( + <div + class="flex-1 overflow-y-auto" + hidden={selectedFrame() !== "Favorites"} + > + <div class="flex max-h-full min-h-0 flex-1 flex-col gap-4 p-4"> + <Header title="Favorites"> + <Number number={() => presets.favorites().length} /> presets marked as + favorites. + </Header> + + <div class="-mx-4 border-t border-orange-200/10" /> + + <div + class="space-y-0.5 py-1" + style={{ + display: !presets.favorites().length ? "none" : undefined, + }} + > + <For each={presets.favorites()}> + {(preset) => ( + <Line + id={`favorite-${preset.id}`} + name={preset.title} + onClick={() => presets.select(preset)} + active={() => presets.selected() === preset} + header={`/ ${[...preset.path.map(({ name }) => name), preset.name].join(" / ")}`} + /> + )} + </For> + </div> + + <div class="h-[25dvh] flex-none" /> + </div> + </div> + ); +} diff --git a/app/src/app/components/frames/header.tsx b/app/src/app/components/frames/header.tsx new file mode 100644 index 000000000..f77894ccf --- /dev/null +++ b/app/src/app/components/frames/header.tsx @@ -0,0 +1,8 @@ +export function Header({ title, children }: { title: string } & ParentProps) { + return ( + <div> + <h3 class="text-lg font-bold md:text-xl">{title}</h3> + <p class="text-orange-100/75">{children}</p> + </div> + ); +} diff --git a/app/src/app/components/frames/history.tsx b/app/src/app/components/frames/history.tsx new file mode 100644 index 000000000..dbc86af80 --- /dev/null +++ b/app/src/app/components/frames/history.tsx @@ -0,0 +1,80 @@ +import { run } from "/src/scripts/utils/run"; + +import { Header } from "./header"; +import { Line } from "./line"; + +export function HistoryFrame({ + presets, + selectedFrame, +}: { + presets: Presets; + selectedFrame: Accessor<FrameName>; +}) { + return ( + <div class="flex-1 overflow-y-auto" hidden={selectedFrame() !== "History"}> + <div class="flex max-h-full min-h-0 flex-1 flex-col p-4"> + <Header title="History">List of previously visited presets.</Header> + + <div + class="space-y-0.5 pt-4" + style={{ + display: !presets.history().length ? "none" : undefined, + }} + > + <For each={presets.history()}> + {({ preset, date }, index) => ( + <> + <Show + when={ + index() === 0 || + presets.history()[index()].date.toJSON().split("T")[0] !== + presets.history()[index() - 1].date.toJSON().split("T")[0] + } + > + <div class="sticky top-[-0.5rem] z-10 -mx-4 py-2"> + <div class="border-y border-orange-200/10 bg-[rgb(25,15,15)] p-2"> + <p class="ml-2"> + <Switch fallback={date.toLocaleDateString()}> + <Match + when={ + new Date().toJSON().split("T")[0] === + date.toJSON().split("T")[0] + } + > + Today + </Match> + <Match + when={ + run(() => { + const d = new Date(); + d.setDate(d.getDate() - 1); + return d; + }) + .toJSON() + .split("T")[0] === date.toJSON().split("T")[0] + } + > + Yesterday + </Match> + </Switch> + </p> + </div> + </div> + </Show> + <Line + id={`history-${preset.id}`} + name={preset.title} + onClick={() => presets.select(preset)} + active={() => presets.selected() === preset} + header={date.toLocaleTimeString()} + /> + </> + )} + </For> + </div> + + <div class="h-[25dvh] flex-none" /> + </div> + </div> + ); +} diff --git a/app/src/app/components/frames/line.tsx b/app/src/app/components/frames/line.tsx new file mode 100644 index 000000000..df1064098 --- /dev/null +++ b/app/src/app/components/frames/line.tsx @@ -0,0 +1,90 @@ +import { scrollIntoView } from "/src/scripts/utils/scroll"; +import { classPropToString } from "/src/solid/classes"; +import { createRWS } from "/src/solid/rws"; + +export function Line({ + id, + name: _name, + icon, + active, + depth = 0, + onClick, + header, + tail, + classes: classes, +}: { + id: string; + name: string; + onClick: VoidFunction; + active?: Accessor<boolean>; + depth?: number; + header?: string; + icon?: () => JSXElement; + tail?: () => JSXElement; + classes?: () => string; +} & ParentProps) { + const ref = createRWS<HTMLButtonElement | undefined>(undefined); + + const [name, ...nameRest] = _name.split(" - "); + + return ( + <button + id={id} + class={classPropToString([ + active?.() + ? "bg-orange-500/30 backdrop-blur-sm hover:bg-orange-500/50" + : "hover:bg-orange-500/15", + "relative -mx-2 flex w-[calc(100%+1rem)] items-center whitespace-nowrap rounded-lg px-2 hover:backdrop-blur-sm", + classes?.(), + ])} + ref={ref.set} + onClick={() => { + onClick(); + scrollIntoView(ref(), "nearest", "instant"); + }} + title={name} + > + <For each={new Array(depth)}> + {() => ( + <span class="ml-1 h-8 w-3 flex-none border-l border-orange-200/10" /> + )} + </For> + <Show when={icon}> + {(icon) => ( + <span + class="-my-0.5 mr-1" + // style={{ + // "margin-left": `${depth}rem`, + // }} + > + {icon()()} + </span> + )} + </Show> + <span + class={classPropToString([ + !icon && "px-1", + "inline-flex w-full flex-col -space-y-1 truncate py-1 text-left", + ])} + > + <Show when={header}> + <span + class="truncate text-xs text-white text-opacity-50" + innerHTML={header} + /> + </Show> + <span class="space-x-1 truncate"> + <span innerHTML={name} /> + <Show when={nameRest.length}> + <span innerHTML={" - " + nameRest.join(" - ")} class="opacity-50" /> + </Show> + </span> + </span> + <Show when={tail}> + {(absolute) => ( + <span class="ml-0.5 flex items-center">{absolute()()}</span> + )} + </Show> + </button> + ); +} diff --git a/app/src/app/components/frames/number.tsx b/app/src/app/components/frames/number.tsx new file mode 100644 index 000000000..8ce7a72f1 --- /dev/null +++ b/app/src/app/components/frames/number.tsx @@ -0,0 +1,7 @@ +export function Number({ number }: { number: () => number }) { + return ( + <span class="font-medium text-orange-400/75"> + {number().toLocaleString("en-us")} + </span> + ); +} diff --git a/app/src/app/components/frames/search.tsx b/app/src/app/components/frames/search.tsx new file mode 100644 index 000000000..d26092e73 --- /dev/null +++ b/app/src/app/components/frames/search.tsx @@ -0,0 +1,318 @@ +import uFuzzy from "@leeoniya/ufuzzy"; +import { createVisibilityObserver } from "@solid-primitives/intersection-observer"; + +import { scrollIntoView } from "/src/scripts/utils/scroll"; +import { createRWS } from "/src/solid/rws"; + +import { INPUT_PRESET_SEARCH_ID } from "../.."; +import { Box } from "./box"; +import { Button } from "./button"; +import { Line } from "./line"; + +const PER_PAGE = 100; + +export function SearchFrame({ + presets, + selectedFrame, +}: { + presets: Presets; + selectedFrame: Accessor<FrameName>; +}) { + const counterRef = createRWS<HTMLDivElement | undefined>(undefined); + + const search = createRWS("", { + equals: false, + }); + + const inputRef = createRWS<HTMLInputElement | undefined>(undefined); + + const config: uFuzzy.Options = { + intraIns: Infinity, + intraChars: `[a-z\d' ]`, + }; + + const fuzzyMultiInsert = new uFuzzy({ + intraIns: 1, + }); + const fuzzyMultiInsertFuzzier = new uFuzzy(config); + const fuzzySingleError = new uFuzzy({ + intraMode: 1, + ...config, + }); + const fuzzySingleErrorFuzzier = new uFuzzy({ + intraMode: 1, + ...config, + }); + + const haystack = presets.list.map( + (preset) => + `${preset.title}\t/ ${[...preset.path.map(({ name }) => name), preset.name].join(" / ")}`, + ); + + const searchResult = createMemo(() => { + scrollIntoView(counterRef()); + + const needle = search(); + + if (!needle) return null; + + const outOfOrder = 5; + const infoThresh = 5_000; + + let result = fuzzyMultiInsert.search( + haystack, + needle, + undefined, + infoThresh, + ); + + if (!result?.[0]?.length || !result?.[1]) { + result = fuzzyMultiInsert.search( + haystack, + needle, + outOfOrder, + infoThresh, + ); + } + + if (!result?.[0]?.length || !result?.[1]) { + result = fuzzySingleError.search( + haystack, + needle, + outOfOrder, + infoThresh, + ); + } + + if (!result?.[0]?.length || !result?.[1]) { + result = fuzzySingleErrorFuzzier.search( + haystack, + needle, + outOfOrder, + infoThresh, + ); + } + + if (!result?.[0]?.length || !result?.[1]) { + result = fuzzyMultiInsertFuzzier.search( + haystack, + needle, + undefined, + infoThresh, + ); + } + + if (!result?.[0]?.length || !result?.[1]) { + result = fuzzyMultiInsertFuzzier.search( + haystack, + needle, + outOfOrder, + infoThresh, + ); + } + + return result; + }); + + const resultCount = createMemo(() => searchResult()?.[0]?.length || 0); + + return ( + <div + class="relative flex size-full flex-1 flex-col" + style={{ + display: selectedFrame() !== "Search" ? "none" : undefined, + }} + > + <div class="flex-1 space-y-1 overflow-y-auto p-4 pt-16"> + <p class="py-2 text-orange-100/75"> + <Show when={search()} fallback={"Write in the top bar to search."}> + Found{" "} + <span class="font-medium text-orange-400/75"> + {resultCount().toLocaleString("en-us")} + </span>{" "} + presets. + </Show> + </p> + + <Show when={search()}> + <div class="-mx-4 border-t border-orange-200/10" /> + + <div + class="py-1" + style={{ + display: !resultCount() ? "none" : undefined, + }} + > + {(() => { + const r = searchResult(); + + if (r) { + return ( + <ListSection + haystack={haystack} + presets={presets} + searchResult={() => r} + /> + ); + } else { + return undefined; + } + })()} + </div> + </Show> + </div> + + <Box absolute="top" padded={false}> + <div + class="relative flex w-full cursor-text items-center space-x-0.5 px-3 py-2 hover:bg-orange-200/5" + onClick={() => inputRef()?.focus()} + > + <IconTablerSearch /> + <input + id={INPUT_PRESET_SEARCH_ID} + ref={inputRef.set} + class="w-full bg-transparent p-1 caret-orange-500 placeholder:text-orange-200/50 focus:outline-none" + placeholder="Search by name or path" + value={search()} + onInput={(event) => search.set(event.target.value)} + /> + <span class="-mx-1 flex size-5 flex-none items-center justify-center rounded-md border border-white text-xs font-bold"> + <IconTablerSlash /> + </span> + </div> + </Box> + + <Box absolute="bottom"> + <Button + onClick={() => { + search.set(""); + inputRef()?.focus(); + }} + > + Clear search + </Button> + </Box> + </div> + ); +} + +function ListSection({ + searchResult, + pageIndex = 0, + haystack, + presets, +}: { + searchResult: Accessor<uFuzzy.SearchResult>; + pageIndex?: number; + haystack: string[]; + presets: Presets; +}) { + const div = createRWS<HTMLDivElement | undefined>(undefined); + + const useVisibilityObserver = createVisibilityObserver(); + + const visible = useVisibilityObserver(div); + + const showNextPage = createMemo<boolean>( + (previous) => previous || visible(), + false, + ); + + const list = createMemo(() => + computeList({ + searchResult: searchResult(), + pageIndex, + haystack, + presets, + }), + ); + + return ( + <div> + <For each={list()}> + {({ preset, path, title }) => ( + <Line + id={`search-${preset.id}`} + name={title} + onClick={() => presets.select(preset)} + active={() => presets.selected() === preset} + header={path} + /> + )} + </For> + <Show when={list().length === PER_PAGE}> + <div ref={div.set}> + <Show when={showNextPage()}> + <ListSection + searchResult={searchResult} + haystack={haystack} + presets={presets} + pageIndex={pageIndex + 1} + /> + </Show> + </div> + </Show> + </div> + ); +} + +function computeList({ + searchResult, + pageIndex, + haystack, + presets, +}: { + searchResult: uFuzzy.SearchResult; + pageIndex: number; + haystack: string[]; + presets: Presets; +}) { + let list: { + preset: Preset; + path: string; + title: string; + }[] = []; + + let [indexes, info, order] = searchResult || [null, null, null]; + + const minIndex = pageIndex * PER_PAGE; + + if (indexes?.length) { + const maxIndex = Math.min( + (order || indexes).length - 1, + minIndex + PER_PAGE - 1, + ); + + list = Array(maxIndex - minIndex + 1); + + if (info && order) { + for (let i = minIndex; i <= maxIndex; i++) { + let infoIdx = order[i]; + + const [title, path] = uFuzzy + .highlight(haystack[info.idx[infoIdx]], info.ranges[infoIdx]) + .split("\t"); + + list[i % 100] = { + preset: presets.list[info.idx[infoIdx]], + path, + title, + }; + } + } else { + for (let i = minIndex; i <= maxIndex; i++) { + let index = indexes[i]; + + const [title, path] = haystack[index].split("\t"); + + list[i % 100] = { + preset: presets.list[index], + path, + title, + }; + } + } + } + + return list; +} diff --git a/app/src/app/components/frames/settings.tsx b/app/src/app/components/frames/settings.tsx new file mode 100644 index 000000000..cbc575f3f --- /dev/null +++ b/app/src/app/components/frames/settings.tsx @@ -0,0 +1,37 @@ +import { Header } from "./header"; + +export function SettingsFrame({ + marquee, + selectedFrame, +}: { + marquee: RWS<boolean>; + selectedFrame: Accessor<FrameName>; +}) { + const value = marquee(); + + return ( + <div class="flex-1 overflow-y-auto" hidden={selectedFrame() !== "Settings"}> + <div class="space-y-4 p-4"> + <Header title="Settings" /> + + <div class="-mx-4 border-t border-orange-200/10" /> + + <div class="space-y-2"> + <p>Background</p> + <div>Opacity</div> + <div> + <label class="switch"> + Scroll + <input + type="checkbox" + checked={value} + onChange={(event) => marquee.set(event.target.checked || false)} + /> + <span class="slider"></span> + </label> + </div> + </div> + </div> + </div> + ); +} diff --git a/app/src/app/components/frames/tree/components/file.tsx b/app/src/app/components/frames/tree/components/file.tsx new file mode 100644 index 000000000..409b03c37 --- /dev/null +++ b/app/src/app/components/frames/tree/components/file.tsx @@ -0,0 +1,47 @@ +import { Line } from "../../line"; + +export function File({ + id, + name, + icon, + active, + depth, + onClick, + favorite, + visited, +}: { + id: string; + name: string; + icon: JSXElement; + active: Accessor<boolean>; + depth: number; + onClick: VoidFunction; + favorite: Accessor<boolean>; + visited: Accessor<boolean>; +}) { + const tail = createMemo(() => + favorite() ? ( + <span class="rounded-full bg-yellow-950 p-1"> + <IconTablerStarFilled class="size-3 text-amber-500" /> + </span> + ) : !visited() ? ( + <span class="mx-1.5 rounded-full bg-orange-500/50 p-1 text-transparent" /> + ) : undefined, + ); + + return ( + <Line + id={id} + depth={depth} + active={active} + name={name} + icon={() => icon} + onClick={onClick} + tail={tail} + /> + ); +} + +function randomDegree(min = 0, max = 360) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} diff --git a/app/src/app/components/frames/tree/components/folder.tsx b/app/src/app/components/frames/tree/components/folder.tsx new file mode 100644 index 000000000..93f8dd435 --- /dev/null +++ b/app/src/app/components/frames/tree/components/folder.tsx @@ -0,0 +1,39 @@ +import { Line } from "../../line"; + +export function Folder({ + id, + name, + depth, + open, + onClick, + children, +}: { + id: string; + name: string; + depth: number; + open: Accessor<boolean>; + onClick: VoidFunction; + children: number; +}) { + const icon = createMemo(() => + open() ? <IconTablerFolderOpen /> : <IconTablerFolder />, + ); + + return ( + <Line + id={id} + depth={depth} + name={name} + icon={icon} + onClick={onClick} + classes={() => (open() ? "text-orange-100/75" : "")} + tail={() => ( + <Show when={!open()}> + <span class="rounded-full bg-white bg-opacity-[0.075] px-2 py-0.5 text-xs text-neutral-400"> + {children} + </span> + </Show> + )} + ></Line> + ); +} diff --git a/app/src/app/components/frames/tree/components/tree.tsx b/app/src/app/components/frames/tree/components/tree.tsx new file mode 100644 index 000000000..aca656d63 --- /dev/null +++ b/app/src/app/components/frames/tree/components/tree.tsx @@ -0,0 +1,117 @@ +import { File } from "./file"; +import { Folder } from "./folder"; + +export function Tree({ + tree, + selected, + openedFolders, + depth = 0, + visible, + selectPreset, + path = [], + favorites, +}: { + tree: PresetTree; + selected: Accessor<Preset>; + selectPreset(preset: Preset): void; + openedFolders: RWS<Set<string>>; + depth?: number; + visible?: Accessor<boolean>; + path?: FilePath; + favorites: Accessor<Preset[]>; +}) { + return ( + <div style={{ display: visible?.() === false ? "none" : undefined }}> + <For each={tree}> + {(thing) => { + const active = createMemo(() => thing.id === selected().id); + const favorite = createMemo(() => + favorites().includes(thing as Preset), + ); + const visited = (thing as Preset).visited; + + if (!("tree" in thing)) { + return ( + <File + id={thing.id} + name={thing.name} + active={active} + depth={depth} + icon={thing.icon || IconTablerFile} + favorite={favorite} + visited={visited} + onClick={() => { + const selectedId = selected().id; + + if (selectedId === thing.id) { + return; + } + + // Has been filled in createPresets + selectPreset(thing as Preset); + }} + /> + ); + } + + const childrenVisible = createMemo(() => + openedFolders().has(thing.id), + ); + + const childCount = countChildren(thing); + + return ( + <div> + <Folder + id={thing.id} + name={thing.name} + depth={depth} + open={childrenVisible} + children={childCount} + onClick={() => { + openedFolders.set((s) => { + if (childrenVisible()) { + s.delete(thing.id); + } else { + s.add(thing.id); + } + + return s; + }); + }} + /> + <Tree + tree={thing.tree} + selected={selected} + depth={depth + 1} + openedFolders={openedFolders} + visible={childrenVisible} + path={[...path, { name: thing.name, id: thing.id }]} + selectPreset={selectPreset} + favorites={favorites} + /> + </div> + ); + }} + </For> + </div> + ); +} + +function countChildren(folder: PresetFolder) { + let count = 0; + + function _countChildren(tree: PartialPresetTree) { + tree.forEach((anyPreset) => { + if ("tree" in anyPreset) { + _countChildren(anyPreset.tree); + } else { + count += 1; + } + }); + } + + _countChildren(folder.tree); + + return count; +} diff --git a/app/src/app/components/frames/tree/index.tsx b/app/src/app/components/frames/tree/index.tsx new file mode 100644 index 000000000..77cf5b4fb --- /dev/null +++ b/app/src/app/components/frames/tree/index.tsx @@ -0,0 +1,86 @@ +import { scrollIntoView } from "/src/scripts/utils/scroll"; +import { sleep, tick } from "/src/scripts/utils/sleep"; +import { createRWS } from "/src/solid/rws"; + +import { Box } from "../box"; +import { Button } from "../button"; +import { Header } from "../header"; +import { Number } from "../number"; +import { Tree } from "./components/tree"; + +export function TreeFrame({ + presets, + selectedFrame, +}: { + presets: Presets; + selectedFrame: Accessor<FrameName>; +}) { + const div = createRWS<HTMLDivElement | undefined>(undefined); + + onMount(() => { + goToSelected(presets); + }); + + return ( + <div + class="relative flex size-full flex-1 flex-col" + style={{ + display: selectedFrame() !== "Tree" ? "none" : undefined, + }} + > + <div class="flex-1 overflow-y-auto"> + <div class="flex max-h-full min-h-0 flex-1 flex-col gap-4 p-4"> + <Header title="Folders"> + <Number number={() => presets.list.length} /> presets organized in a + tree like structure. + </Header> + + <div class="-mx-4 border-t border-orange-200/10" /> + + <Tree + tree={presets.tree} + openedFolders={presets.openedFolders} + selected={presets.selected} + selectPreset={presets.select} + favorites={presets.favorites} + /> + + <div class="h-[50dvh] flex-none" /> + </div> + </div> + + <Box absolute="bottom"> + <Button + onClick={() => { + presets.openedFolders.set((s) => { + s.clear(); + return s; + }); + + sleep(10); + + scrollIntoView(div()); + }} + > + Close all folders + </Button> + <Button onClick={() => goToSelected(presets)}>Go to selected</Button> + </Box> + </div> + ); +} + +async function goToSelected(presets: Presets) { + batch(() => + presets.selected().path.forEach(({ id }) => { + presets.openedFolders.set((s) => { + s.add(id); + return s; + }); + }), + ); + + await tick(); + + scrollIntoView(document.getElementById(presets.selected().id), "center"); +} diff --git a/app/src/app/components/qrcode.tsx b/app/src/app/components/qrcode.tsx new file mode 100644 index 000000000..e873ee8ec --- /dev/null +++ b/app/src/app/components/qrcode.tsx @@ -0,0 +1,18 @@ +export function Qrcode({ qrcode }: { qrcode: RWS<string> }) { + return ( + <Show when={qrcode()}> + <div + class="absolute inset-0 z-50 flex h-full w-full items-center justify-center bg-black" + onClick={() => { + qrcode.set(""); + }} + > + <img + class="aspect-square max-h-full grow object-contain" + src={qrcode()} + style={{ "image-rendering": "pixelated" }} + /> + </div> + </Show> + ); +} diff --git a/app/src/app/components/strip/components/anchor.tsx b/app/src/app/components/strip/components/anchor.tsx new file mode 100644 index 000000000..2ecdf654d --- /dev/null +++ b/app/src/app/components/strip/components/anchor.tsx @@ -0,0 +1,9 @@ +import { Clickable } from "./clickable"; + +export function Anchor(args: { + title: string; + href: string; + icon?: () => ValidComponent; +}) { + return <Clickable {...args} />; +} diff --git a/app/src/app/components/strip/components/anchorAPI.tsx b/app/src/app/components/strip/components/anchorAPI.tsx new file mode 100644 index 000000000..bc8d0d916 --- /dev/null +++ b/app/src/app/components/strip/components/anchorAPI.tsx @@ -0,0 +1,26 @@ +import { Anchor } from "./anchor"; + +export function AnchorAPI() { + return ( + <Anchor + title="API" + icon={() => () => ( + <svg + width="20" + height="20" + viewBox="0 0 20 20" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + fill-rule="evenodd" + clip-rule="evenodd" + d="M5.13468 2.41153C3.88395 3.0478 3.37143 3.79772 3.37143 4.4186C3.37143 5.03949 3.88395 5.78941 5.13468 6.42568C6.3444 7.04109 8.06359 7.44186 10 7.44186C11.9364 7.44186 13.6556 7.04109 14.8653 6.42568C16.1161 5.78941 16.6286 5.03949 16.6286 4.4186C16.6286 3.79772 16.1161 3.0478 14.8653 2.41153C13.6556 1.79612 11.9364 1.39535 10 1.39535C8.06359 1.39535 6.3444 1.79612 5.13468 2.41153ZM16.6286 6.93694C16.2841 7.21648 15.8934 7.46274 15.4786 7.67372C14.0411 8.40502 12.1032 8.83721 10 8.83721C7.89684 8.83721 5.95889 8.40502 4.52136 7.67372C4.10664 7.46274 3.71588 7.21648 3.37143 6.93694V10C3.37143 10.6209 3.88395 11.3708 5.13468 12.0071C6.3444 12.6225 8.06359 13.0233 10 13.0233C11.9364 13.0233 13.6556 12.6225 14.8653 12.0071C16.1161 11.3708 16.6286 10.6209 16.6286 10V6.93694ZM18 4.4186C18 2.98447 16.8752 1.87393 15.4786 1.16349C14.0411 0.432186 12.1032 0 10 0C7.89684 0 5.95889 0.432186 4.52136 1.16349C3.12484 1.87393 2 2.98447 2 4.4186V15.5814C2 17.0155 3.12484 18.1261 4.52136 18.8365C5.95889 19.5678 7.89684 20 10 20C12.1032 20 14.0411 19.5678 15.4786 18.8365C16.8752 18.1261 18 17.0155 18 15.5814V4.4186ZM16.6286 12.5183C16.2841 12.7979 15.8934 13.0441 15.4786 13.2551C14.0411 13.9864 12.1032 14.4186 10 14.4186C7.89684 14.4186 5.95889 13.9864 4.52136 13.2551C4.10664 13.0441 3.71588 12.7979 3.37143 12.5183V15.5814C3.37143 16.2023 3.88395 16.9522 5.13468 17.5885C6.3444 18.2039 8.06359 18.6047 10 18.6047C11.9364 18.6047 13.6556 18.2039 14.8653 17.5885C16.1161 16.9522 16.6286 16.2023 16.6286 15.5814V12.5183ZM6.34285 10C6.34285 10.5138 5.93351 10.9302 5.42857 10.9302C4.92362 10.9302 4.51428 10.5138 4.51428 10C4.51428 9.48625 4.92362 9.06977 5.42857 9.06977C5.93351 9.06977 6.34285 9.48625 6.34285 10ZM9.0857 11.8605C9.59065 11.8605 9.99999 11.444 9.99999 10.9302C9.99999 10.4165 9.59065 10 9.0857 10C8.58076 10 8.17142 10.4165 8.17142 10.9302C8.17142 11.444 8.58076 11.8605 9.0857 11.8605ZM6.34285 15.5814C6.34285 16.0951 5.93351 16.5116 5.42857 16.5116C4.92362 16.5116 4.51428 16.0951 4.51428 15.5814C4.51428 15.0676 4.92362 14.6512 5.42857 14.6512C5.93351 14.6512 6.34285 15.0676 6.34285 15.5814ZM9.0857 17.4419C9.59065 17.4419 9.99999 17.0254 9.99999 16.5116C9.99999 15.9979 9.59065 15.5814 9.0857 15.5814C8.58076 15.5814 8.17142 15.9979 8.17142 16.5116C8.17142 17.0254 8.58076 17.4419 9.0857 17.4419Z" + fill="currentColor" + ></path> + </svg> + )} + href="https://api.satonomics.xyz" + /> + ); +} diff --git a/app/src/app/components/strip/components/anchorGit.tsx b/app/src/app/components/strip/components/anchorGit.tsx new file mode 100644 index 000000000..847cf2b60 --- /dev/null +++ b/app/src/app/components/strip/components/anchorGit.tsx @@ -0,0 +1,11 @@ +import { Anchor } from "./anchor"; + +export function AnchorGit() { + return ( + <Anchor + title="Git" + icon={() => IconTablerGitMerge} + href="https://codeberg.org/satonomics/satonomics" + /> + ); +} diff --git a/app/src/app/components/strip/components/anchorHome.tsx b/app/src/app/components/strip/components/anchorHome.tsx new file mode 100644 index 000000000..cafb3d774 --- /dev/null +++ b/app/src/app/components/strip/components/anchorHome.tsx @@ -0,0 +1,32 @@ +import { Anchor } from "./anchor"; + +export function AnchorHome() { + return ( + <Anchor + title="Home" + icon={() => () => ( + <svg + width="20" + height="20" + viewBox="0 0 20 20" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + fill-rule="evenodd" + clip-rule="evenodd" + d="M9.61843 17.395H10.3816C12.1046 17.395 13.288 17.3933 14.198 17.3045C15.0844 17.218 15.5498 17.0602 15.8839 16.8482C16.3124 16.5763 16.6761 16.2149 16.9497 15.7891C17.1631 15.4571 17.3218 14.9946 17.4089 14.1138C17.4983 13.2096 17.5 12.0337 17.5 10.3216C17.5 8.25521 17.4763 7.61464 17.2665 7.07287C17.1488 6.76889 16.9887 6.48284 16.7909 6.22312C16.4384 5.76023 15.9032 5.40234 14.1365 4.31233L13.9563 4.20109C12.9121 3.55687 12.2055 3.12231 11.6218 2.82577C11.0608 2.54075 10.7049 2.43259 10.3882 2.39747C10.1302 2.36886 9.86981 2.36886 9.61184 2.39747C9.29509 2.43259 8.9392 2.54075 8.37818 2.82577C7.79446 3.12231 7.08787 3.55687 6.04374 4.20109L5.86345 4.31233C4.09679 5.40234 3.56162 5.76023 3.20909 6.22312C3.01129 6.48284 2.85119 6.76889 2.73348 7.07287C2.52369 7.61464 2.5 8.25521 2.5 10.3216C2.5 12.0337 2.50169 13.2096 2.59108 14.1138C2.67816 14.9946 2.83688 15.4571 3.05029 15.7891C3.32393 16.2149 3.68762 16.5763 4.11606 16.8482C4.45021 17.0602 4.91563 17.218 5.80203 17.3045C6.71202 17.3933 7.89539 17.395 9.61843 17.395ZM1.33354 6.5376C1 7.39893 1 8.37315 1 10.3216C1 13.6861 1 15.3684 1.78613 16.5914C2.17705 17.1996 2.6966 17.7159 3.30866 18.1043C4.53951 18.8855 6.23249 18.8855 9.61843 18.8855H10.3816C13.7675 18.8855 15.4605 18.8855 16.6913 18.1043C17.3034 17.7159 17.823 17.1996 18.2139 16.5914C19 15.3684 19 13.6861 19 10.3216C19 8.37315 19 7.39893 18.6665 6.5376C18.4983 6.10334 18.2696 5.6947 17.987 5.32367C17.4265 4.58775 16.5936 4.07385 14.9278 3.04605L14.7475 2.93482C12.7009 1.67205 11.6775 1.04067 10.5545 0.916147C10.186 0.875282 9.81402 0.875282 9.44549 0.916147C8.32248 1.04067 7.29915 1.67205 5.25249 2.93482L5.0722 3.04605C3.40637 4.07385 2.57345 4.58775 2.01299 5.32367C1.73042 5.6947 1.5017 6.10334 1.33354 6.5376Z" + fill="currentColor" + ></path> + <path + fill-rule="evenodd" + clip-rule="evenodd" + d="M6.25 14.0001C6.25 13.5858 6.58579 13.2501 7 13.2501H13C13.4142 13.2501 13.75 13.5858 13.75 14.0001C13.75 14.4143 13.4142 14.7501 13 14.7501H7C6.58579 14.7501 6.25 14.4143 6.25 14.0001Z" + fill="currentColor" + ></path> + </svg> + )} + href="https://satonomics.xyz" + /> + ); +} diff --git a/app/src/app/components/strip/components/anchorLogo.tsx b/app/src/app/components/strip/components/anchorLogo.tsx new file mode 100644 index 000000000..acabcfb52 --- /dev/null +++ b/app/src/app/components/strip/components/anchorLogo.tsx @@ -0,0 +1,36 @@ +export function AnchorLogo() { + return ( + <a + class="inline-flex justify-center rounded-lg bg-gradient-to-br from-orange-500 to-orange-800 p-4" + href="https://app.satonomics.xyz" + title="Reload" + > + <svg + class="-m-1.5 size-7" + width="100%" + height="100%" + viewBox="0 0 24 24" + version="1.1" + xmlns="http://www.w3.org/2000/svg" + style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;" + fill="currentColor" + > + <g transform="matrix(1.14102,0,0,2.63158,-0.849652,5.12904)"> + <rect x="4.25" y="3.751" width="14.023" height="1.52" /> + </g> + <g transform="matrix(1.14102,0,0,2.63158,-0.849652,0.129039)"> + <rect x="4.25" y="3.751" width="14.023" height="1.52" /> + </g> + <g transform="matrix(1.14102,0,0,2.63158,-0.849652,-4.87096)"> + <rect x="4.25" y="3.751" width="14.023" height="1.52" /> + </g> + <g transform="matrix(0.285256,0,0,2.63158,8.78759,-9.87096)"> + <rect x="4.25" y="3.751" width="14.023" height="1.52" /> + </g> + <g transform="matrix(0.285256,0,0,2.63158,8.78759,10.129)"> + <rect x="4.25" y="3.751" width="14.023" height="1.52" /> + </g> + </svg> + </a> + ); +} diff --git a/app/src/app/components/strip/components/anchorNostr.tsx b/app/src/app/components/strip/components/anchorNostr.tsx new file mode 100644 index 000000000..be1ef3073 --- /dev/null +++ b/app/src/app/components/strip/components/anchorNostr.tsx @@ -0,0 +1,24 @@ +import { Anchor } from "./anchor"; + +export function AnchorNostr() { + return ( + <Anchor + title="Nostr" + icon={() => (props: { class?: string }) => ( + <svg + viewBox="0 0 20 20" + fill="currentColor" + xmlns="http://www.w3.org/2000/svg" + class={props.class} + > + <path + fill-rule="evenodd" + clip-rule="evenodd" + d="M13.7502 1.5C13.3359 1.5 13.0002 1.83579 13.0002 2.25C13.0002 2.44257 13.0717 2.61663 13.191 2.74981C13.2963 2.86751 13.4367 2.95067 13.5937 2.98382C13.6435 2.99433 13.6958 3 13.7502 3C14.1644 3 14.5002 2.66421 14.5002 2.25C14.5002 1.83579 14.1644 1.5 13.7502 1.5ZM11.5002 2.25C11.5002 1.00736 12.5075 0 13.7502 0C14.7298 0 15.5632 0.626106 15.8721 1.5H17.2502C17.6644 1.5 18.0002 1.83579 18.0002 2.25C18.0002 2.66421 17.6644 3 17.2502 3H15.8721C15.7646 3.30433 15.5934 3.5786 15.3746 3.80685C15.8823 4.15684 16.2746 4.56859 16.5559 5.03129C17.0977 5.92273 17.1689 6.90747 16.9843 7.79359C16.803 8.66356 16.4559 9.21659 16.076 9.55603C16.0407 9.58759 16.0055 9.61689 15.9707 9.64408C15.9901 9.86441 15.987 10.0911 15.961 10.3234C15.8963 11.095 15.7248 11.9737 14.9635 12.7682C14.447 13.3072 13.7758 13.6448 13.2782 13.8437C13.0474 13.936 12.8402 14.0039 12.6813 14.0508C12.5887 14.2176 12.4689 14.451 12.3551 14.7243C12.1564 15.2017 11.999 15.7473 12.0002 16.2484C12.0011 16.664 12.1111 17.3476 12.2365 17.9768C12.2743 18.1665 12.3122 18.3447 12.3464 18.5001H13.2502C13.6644 18.5001 14.0002 18.8359 14.0002 19.2501C14.0002 19.6643 13.6644 20.0001 13.2502 20.0001H9.50017C9.15558 20.0001 8.85533 19.7653 8.77228 19.4308L9.50017 19.2501C8.77228 19.4308 8.77228 19.4308 8.77228 19.4308L8.77052 19.4237L8.76601 19.4053L8.74947 19.337C8.73534 19.2781 8.71532 19.1935 8.69136 19.0891C8.64351 18.8806 8.5796 18.5917 8.51548 18.2701C8.39148 17.6482 8.25146 16.8318 8.25017 16.2518C8.24854 15.5227 8.455 14.8065 8.6849 14.2341C8.16639 14.1203 7.6883 13.9364 7.28499 13.7003C6.8511 13.4462 6.41419 13.0729 6.17829 12.5938H2.75029C2.4353 12.5938 2.15363 12.3969 2.04556 12.1011C1.93749 11.8052 2.0258 11.4733 2.26663 11.2703L3.18599 10.4953C2.80831 10.4564 2.51374 10.1372 2.51374 9.74926C2.51374 8.4257 3.50881 7.51335 4.62109 6.95581C5.76228 6.38377 7.24656 6.05918 8.72722 5.97699C10.2102 5.89468 11.7615 6.05149 13.0442 6.50251C13.9451 6.81927 14.793 7.31253 15.3456 8.03745C15.4073 7.89899 15.4675 7.71949 15.5158 7.48766C15.6401 6.89125 15.5786 6.31137 15.2741 5.81039C14.9722 5.31375 14.3741 4.8005 13.2451 4.44292C12.7831 4.33682 12.3768 4.0893 12.0732 3.75019C11.7174 3.35262 11.5002 2.82579 11.5002 2.25ZM10.8948 14.3362C10.6852 14.3506 10.4759 14.3595 10.2686 14.3634C10.0228 14.8666 9.74868 15.5834 9.75016 16.2484C9.75109 16.664 9.86108 17.3476 9.98652 17.9768C10.0243 18.1665 10.0622 18.3447 10.0964 18.5001H10.8124C10.7969 18.4258 10.7812 18.349 10.7655 18.2701C10.6415 17.6482 10.5015 16.8318 10.5002 16.2518C10.4986 15.5522 10.6874 14.8745 10.8948 14.3362Z" + ></path> + </svg> + )} + href="https://primal.net/p/npub1jagmm3x39lmwfnrtvxcs9ac7g300y3dusv9lgzhk2e4x5frpxlrqa73v44" + /> + ); +} diff --git a/app/src/app/components/strip/components/button.tsx b/app/src/app/components/strip/components/button.tsx new file mode 100644 index 000000000..dc94e7356 --- /dev/null +++ b/app/src/app/components/strip/components/button.tsx @@ -0,0 +1,14 @@ +import { Clickable } from "./clickable"; + +export function Button( + args: { + title: string; + selected?: Accessor<boolean>; + onClick?: VoidFunction; + icon?: () => ValidComponent; + hideOnDesktop?: boolean; + hideOnMobile?: boolean; + } & ParentProps, +) { + return <Clickable {...args} />; +} diff --git a/app/src/app/components/strip/components/buttonChart.tsx b/app/src/app/components/strip/components/buttonChart.tsx new file mode 100644 index 000000000..ccc6648a1 --- /dev/null +++ b/app/src/app/components/strip/components/buttonChart.tsx @@ -0,0 +1,27 @@ +import { Button } from "./button"; + +export function ButtonChart({ + selected, + setSelected, +}: { + selected: Accessor<FrameName>; + setSelected: Setter<FrameName>; +}) { + const frameName: FrameName = "Chart"; + + return ( + <Button + title={frameName} + selected={() => selected() === frameName} + onClick={() => { + setSelected(frameName); + }} + icon={() => + selected() === frameName + ? IconTablerChartAreaFilled + : IconTablerChartLine + } + hideOnDesktop + /> + ); +} diff --git a/app/src/app/components/strip/components/buttonFavorites.tsx b/app/src/app/components/strip/components/buttonFavorites.tsx new file mode 100644 index 000000000..451dbee4d --- /dev/null +++ b/app/src/app/components/strip/components/buttonFavorites.tsx @@ -0,0 +1,24 @@ +import { Button } from "./button"; + +export function ButtonFavorites({ + selected, + setSelected, +}: { + selected: Accessor<FrameName>; + setSelected: Setter<FrameName>; +}) { + const frameName: FrameName = "Favorites"; + + return ( + <Button + title={frameName} + selected={() => selected() === frameName} + onClick={() => { + setSelected(frameName); + }} + icon={() => + selected() === frameName ? IconTablerStarFilled : IconTablerStar + } + /> + ); +} diff --git a/app/src/app/components/strip/components/buttonHistory.tsx b/app/src/app/components/strip/components/buttonHistory.tsx new file mode 100644 index 000000000..39b8ab19f --- /dev/null +++ b/app/src/app/components/strip/components/buttonHistory.tsx @@ -0,0 +1,22 @@ +import { Button } from "./button"; + +export function ButtonHistory({ + selected, + setSelected, +}: { + selected: Accessor<FrameName>; + setSelected: Setter<FrameName>; +}) { + const frameName: FrameName = "History"; + + return ( + <Button + title={frameName} + selected={() => selected() === frameName} + onClick={() => { + setSelected(frameName); + }} + icon={() => IconTablerHistory} + /> + ); +} diff --git a/app/src/app/components/strip/components/buttonRefresh.tsx b/app/src/app/components/strip/components/buttonRefresh.tsx new file mode 100644 index 000000000..32a0c69de --- /dev/null +++ b/app/src/app/components/strip/components/buttonRefresh.tsx @@ -0,0 +1,10 @@ +import { Button } from "./button"; + +export function ButtonRefresh() { + return ( + <Button title="Refresh" onClick={() => document.location.reload()}> + <IconTablerRefreshAlert class="absolute size-5 animate-ping text-orange-400" /> + <IconTablerRefreshAlert class="relative size-5 text-orange-300" /> + </Button> + ); +} diff --git a/app/src/app/components/strip/components/buttonSearch.tsx b/app/src/app/components/strip/components/buttonSearch.tsx new file mode 100644 index 000000000..19a662d94 --- /dev/null +++ b/app/src/app/components/strip/components/buttonSearch.tsx @@ -0,0 +1,24 @@ +import { Button } from "./button"; + +export function ButtonSearch({ + selected, + setSelected, +}: { + selected: Accessor<FrameName>; + setSelected: Setter<FrameName>; +}) { + const frameName: FrameName = "Search"; + + return ( + <Button + title={frameName} + selected={() => selected() === frameName} + onClick={() => { + setSelected(frameName); + }} + icon={() => + selected() === frameName ? IconTablerZoomFilled : IconTablerSearch + } + /> + ); +} diff --git a/app/src/app/components/strip/components/buttonSettings.tsx b/app/src/app/components/strip/components/buttonSettings.tsx new file mode 100644 index 000000000..e6f7b905b --- /dev/null +++ b/app/src/app/components/strip/components/buttonSettings.tsx @@ -0,0 +1,64 @@ +import { Button } from "./button"; + +export function ButtonSettings({ + selected, + setSelected, +}: { + selected: Accessor<FrameName>; + setSelected: Setter<FrameName>; +}) { + const frameName: FrameName = "Settings"; + + return ( + <Button + title={frameName} + selected={() => selected() === frameName} + onClick={() => { + setSelected(frameName); + }} + icon={() => + selected() === frameName + ? (props: { class?: string }) => ( + <svg + width="20" + height="20" + viewBox="0 0 20 20" + fill="none" + xmlns="http://www.w3.org/2000/svg" + class={props.class} + > + <path + fill-rule="evenodd" + clip-rule="evenodd" + d="M11.4728 0.527798C11.7268 0.559309 11.99 0.632032 12.2381 0.820103C12.483 1.00582 12.6319 1.2403 12.7414 1.48233C12.8374 1.69461 12.9204 1.95468 13.0064 2.22445L13.0168 2.25701C13.0448 2.34475 13.0737 2.43611 13.1033 2.53083C13.2051 2.85633 13.4548 3.15301 13.8064 3.35916C14.1394 3.55444 14.484 3.62039 14.7899 3.55144C14.9153 3.52317 15.0355 3.4966 15.1498 3.47175L15.1816 3.46483C15.4569 3.40495 15.7191 3.34793 15.9473 3.32742C16.2059 3.30418 16.4759 3.3209 16.7551 3.44806C17.0382 3.57701 17.2361 3.77817 17.397 3.99585C17.5396 4.18881 17.6807 4.43454 17.8294 4.69368L18.4531 5.78016C18.6017 6.0388 18.743 6.28482 18.8382 6.50638C18.9453 6.75546 19.0246 7.03932 18.9923 7.36954C18.9601 7.69683 18.8312 7.95435 18.678 8.17364C18.5439 8.36545 18.3617 8.56766 18.1737 8.77618L18.1504 8.802C18.0735 8.88731 17.9924 8.97669 17.9074 9.0696C17.7091 9.28642 17.5934 9.61558 17.5934 10.0001C17.5934 10.384 17.7089 10.7125 17.9069 10.9289C17.9923 11.0222 18.0737 11.112 18.1509 11.1976L18.1743 11.2236C18.3622 11.432 18.5444 11.6342 18.6784 11.8259C18.8316 12.0452 18.9606 12.3027 18.9927 12.6299C19.0251 12.9601 18.9457 13.2439 18.8387 13.493C18.7435 13.7145 18.6023 13.9605 18.4538 14.2191L17.8301 15.3057C17.6814 15.5649 17.5404 15.8106 17.3978 16.0037C17.2369 16.2214 17.039 16.4226 16.7558 16.5516C16.4766 16.6787 16.2066 16.6955 15.948 16.6722C15.7197 16.6517 15.4575 16.5947 15.1822 16.5348L15.1362 16.5248C15.0263 16.5009 14.911 16.4754 14.7911 16.4484C14.4849 16.3794 14.14 16.4454 13.8067 16.6409C13.4548 16.8472 13.2049 17.1441 13.103 17.4699C13.0735 17.5644 13.0447 17.6555 13.0167 17.743L13.0064 17.7756C12.9203 18.0453 12.8374 18.3053 12.7414 18.5176C12.6319 18.7596 12.483 18.9941 12.2381 19.1798C11.9901 19.3678 11.7269 19.4406 11.473 19.4721C11.2478 19.5001 10.9813 19.5 10.7014 19.5H9.2982C9.01827 19.5 8.75179 19.5001 8.52665 19.4721C8.2727 19.4406 8.00953 19.3678 7.76153 19.1798C7.51663 18.9941 7.3677 18.7596 7.25826 18.5176C7.16227 18.3053 7.07932 18.0453 6.99327 17.7756L6.98288 17.743C6.95491 17.6554 6.92605 17.5641 6.89647 17.4695C6.79467 17.144 6.54495 16.8473 6.19334 16.6411C5.86029 16.4458 5.51572 16.3799 5.20982 16.4489C5.08432 16.4771 4.96402 16.5037 4.84959 16.5286L4.81779 16.5355C4.54244 16.5954 4.28026 16.6525 4.05197 16.673C3.79335 16.6962 3.52336 16.6795 3.24412 16.5523C2.96096 16.4233 2.76307 16.2221 2.60218 16.0044C2.45957 15.8114 2.31856 15.5656 2.16985 15.3064L1.54621 14.2199C1.39773 13.9612 1.25649 13.7152 1.16129 13.4937C1.05427 13.2446 0.97492 12.9608 1.00733 12.6306C1.03945 12.3034 1.16838 12.0459 1.32159 11.8266C1.45561 11.6348 1.63789 11.4326 1.82585 11.2241L1.84911 11.1983C1.92603 11.113 2.00716 11.0236 2.09219 10.9306C2.29053 10.7138 2.40616 10.3846 2.40616 10.0001C2.40616 9.61513 2.29042 9.28555 2.09182 9.06845C2.00706 8.97579 1.92618 8.88665 1.84949 8.80155L1.82621 8.77572C1.63828 8.56727 1.45604 8.36512 1.32204 8.17335C1.16884 7.95412 1.03992 7.69667 1.00779 7.36946C0.975365 7.03931 1.05469 6.7555 1.16168 6.50646C1.25684 6.28492 1.39804 6.03894 1.54647 5.78034L2.17001 4.69368C2.31872 4.43443 2.45971 4.18865 2.60233 3.99561C2.76322 3.77784 2.96111 3.5766 3.2443 3.44759C3.52356 3.32037 3.79357 3.30364 4.05222 3.32688C4.28053 3.34739 4.54274 3.40442 4.81813 3.46432L4.84995 3.47124C4.86209 3.47388 4.86651 3.47367 4.86651 3.47367C4.86859 3.47357 4.86925 3.47354 4.88348 3.47854C4.98794 3.50133 5.09714 3.52551 5.2106 3.55109C5.51633 3.62001 5.86071 3.55411 6.19359 3.35895C6.54506 3.15289 6.79466 2.85633 6.89641 2.53097C6.92603 2.43624 6.95492 2.34487 6.98293 2.25713L6.99331 2.22458C7.07935 1.95487 7.1623 1.69487 7.25828 1.48264C7.36771 1.24066 7.51662 1.00623 7.76148 0.820526C8.00945 0.63247 8.27257 0.55971 8.52649 0.528149C8.75162 0.500166 9.01808 0.500173 9.29799 0.50018L10.7012 0.500008C10.9811 0.499932 11.2477 0.499859 11.4728 0.527798ZM10 13.25C11.7949 13.25 13.25 11.7949 13.25 10C13.25 8.20507 11.7949 6.75 10 6.75C8.20507 6.75 6.75 8.20507 6.75 10C6.75 11.7949 8.20507 13.25 10 13.25Z" + fill="currentColor" + ></path> + </svg> + ) + : (props: { class?: string }) => ( + <svg + width="20" + height="20" + viewBox="0 0 20 20" + fill="none" + xmlns="http://www.w3.org/2000/svg" + class={props.class} + > + <path + fill-rule="evenodd" + clip-rule="evenodd" + d="M11.3113 1.97927C11.1767 1.96258 10.9931 1.96144 10.6688 1.96148L9.33068 1.96164C9.00643 1.96168 8.82284 1.96287 8.68836 1.97959C8.62838 1.98704 8.59765 1.99568 8.58333 2.00077C8.57298 2.00445 8.56936 2.0073 8.56936 2.0073C8.56646 2.0095 8.56337 2.0119 8.55691 2.02077C8.54854 2.03227 8.53202 2.05815 8.50753 2.11232C8.45199 2.23513 8.39512 2.40944 8.29478 2.72381C8.26765 2.80881 8.23966 2.89735 8.21095 2.98914C7.97709 3.73696 7.45008 4.29399 6.86568 4.63662C6.30261 4.96674 5.61803 5.13735 4.92153 4.98034C4.79931 4.95279 4.68217 4.9269 4.57079 4.90269C4.25207 4.83341 4.07143 4.79535 3.93485 4.78308C3.82533 4.77325 3.79919 4.78627 3.79348 4.78911C3.79348 4.78911 3.76724 4.79516 3.6939 4.89443C3.60771 5.0111 3.50888 5.18106 3.33705 5.48052L2.74781 6.50741C2.5758 6.80718 2.47928 6.97771 2.42241 7.11009C2.39703 7.16916 2.38873 7.20062 2.386 7.21407C2.38552 7.2164 2.38504 7.21938 2.38504 7.21938L2.38636 7.22272C2.39044 7.23248 2.40265 7.25798 2.43603 7.30576C2.51219 7.41474 2.63242 7.55007 2.85242 7.79419C2.92683 7.87676 3.00534 7.96328 3.08761 8.05323C3.58749 8.59969 3.79049 9.3236 3.79049 10.0001C3.79049 10.6763 3.58756 11.3997 3.08797 11.9459C3.00543 12.0361 2.92668 12.1229 2.85205 12.2057C2.63202 12.4499 2.51177 12.5852 2.4356 12.6942C2.40221 12.742 2.38999 12.7675 2.38592 12.7773L2.38459 12.7806C2.38459 12.7806 2.38507 12.7836 2.38555 12.786C2.38828 12.7994 2.39659 12.8309 2.42198 12.89C2.47887 13.0223 2.57542 13.1929 2.74748 13.4927L3.33681 14.5195C3.50865 14.8189 3.60748 14.9888 3.69367 15.1055C3.76605 15.2034 3.79251 15.2104 3.79251 15.2104C3.79821 15.2133 3.82507 15.2266 3.93457 15.2168C4.07113 15.2045 4.25174 15.1665 4.57041 15.0972C4.68167 15.073 4.79866 15.0471 4.92074 15.0196C5.61746 14.8626 6.30225 15.0333 6.86548 15.3635C7.44998 15.7062 7.97711 16.2634 8.21101 17.0113C8.23968 17.103 8.26764 17.1915 8.29474 17.2764C8.39509 17.5908 8.45196 17.7651 8.50751 17.888C8.53201 17.9421 8.54853 17.968 8.5569 17.9795C8.56336 17.9884 8.56623 17.9906 8.56913 17.9928C8.56913 17.9928 8.57297 17.9958 8.58332 17.9995C8.59764 18.0046 8.62838 18.0132 8.68836 18.0207C8.82287 18.0374 9.00648 18.0386 9.33077 18.0386H10.6688C10.9931 18.0386 11.1767 18.0374 11.3113 18.0207C11.3712 18.0132 11.402 18.0046 11.4163 17.9995C11.4266 17.9958 11.4303 17.993 11.4303 17.993C11.4332 17.9908 11.4363 17.9884 11.4427 17.9795C11.4511 17.968 11.4676 17.9421 11.4921 17.888C11.5477 17.7651 11.6045 17.5908 11.7049 17.2764C11.7319 17.1916 11.7599 17.1033 11.7885 17.0117C12.0225 16.2635 12.5498 15.7062 13.1345 15.3633C13.6979 15.0329 14.383 14.862 15.0801 15.0191C15.2018 15.0466 15.3185 15.0724 15.4295 15.0965C15.4313 15.0969 15.4332 15.0973 15.4351 15.0977C15.438 15.0983 15.4409 15.0989 15.4437 15.0996C15.7536 15.1669 15.9308 15.204 16.0653 15.2161C16.1748 15.2259 16.201 15.2129 16.2067 15.21C16.2067 15.21 16.2339 15.2026 16.3063 15.1047C16.3925 14.9881 16.4913 14.8182 16.6631 14.5188L17.2525 13.492C17.4246 13.1922 17.5211 13.0216 17.578 12.8892C17.6034 12.8302 17.6117 12.7987 17.6144 12.7852C17.6149 12.7829 17.6154 12.7799 17.6154 12.7799L17.6141 12.7766C17.61 12.7668 17.5978 12.7413 17.5644 12.6935C17.4882 12.5845 17.368 12.4492 17.148 12.205C17.0731 12.1219 16.994 12.0348 16.9112 11.9443C16.4119 11.3986 16.2091 10.6757 16.2091 10.0001C16.2091 9.32397 16.412 8.6005 16.9116 8.05435C16.9941 7.96417 17.0728 7.87742 17.1475 7.79464C17.3675 7.55045 17.4878 7.41507 17.564 7.30605C17.5974 7.25827 17.6096 7.23275 17.6136 7.22299L17.615 7.21963C17.615 7.21963 17.6145 7.21665 17.614 7.21431C17.6113 7.20085 17.603 7.16938 17.5776 7.11029C17.5207 6.97789 17.4241 6.80733 17.252 6.50752L16.6625 5.48076C16.4906 5.18142 16.3918 5.01152 16.3056 4.89491C16.2333 4.79702 16.2068 4.78999 16.2068 4.78999C16.2011 4.78715 16.1742 4.77379 16.0647 4.78362C15.9282 4.79589 15.7476 4.83394 15.429 4.90319C15.3178 4.92736 15.2009 4.9532 15.0789 4.9807C14.3822 5.13774 13.6974 4.96704 13.1342 4.6368C12.5497 4.29408 12.0226 3.73693 11.7887 2.98898C11.76 2.89719 11.7321 2.80866 11.7049 2.72367C11.6046 2.40921 11.5477 2.23485 11.4921 2.11201C11.4676 2.05783 11.4511 2.03194 11.4427 2.02044C11.4363 2.01157 11.4334 2.00934 11.4305 2.00714C11.4305 2.00714 11.4267 2.00412 11.4163 2.00044C11.402 1.99535 11.3713 1.98672 11.3113 1.97927ZM11.4728 0.527798C11.7268 0.559309 11.99 0.632033 12.238 0.820103C12.483 1.00582 12.6319 1.2403 12.7414 1.48233C12.8374 1.69462 12.9203 1.95468 13.0064 2.22446L13.0168 2.25701C13.0448 2.34475 13.0737 2.43612 13.1033 2.53083C13.2051 2.85633 13.4548 3.15301 13.8064 3.35917C14.1394 3.55445 14.484 3.62039 14.7898 3.55145C14.9153 3.52318 15.0355 3.49661 15.1498 3.47175L15.1816 3.46484C15.4569 3.40495 15.7191 3.34793 15.9473 3.32742C16.2059 3.30419 16.4759 3.3209 16.7551 3.44806C17.0382 3.57701 17.2361 3.77817 17.397 3.99586C17.5396 4.18883 17.6806 4.43453 17.8294 4.69368L18.4531 5.78017C18.6017 6.03881 18.743 6.28482 18.8382 6.50639C18.9452 6.75547 19.0246 7.03933 18.9922 7.36955C18.9601 7.69684 18.8312 7.95436 18.678 8.17365C18.5439 8.36546 18.3616 8.56767 18.1737 8.77619L18.1504 8.80201C18.0735 8.88733 17.9924 8.9767 17.9074 9.06961C17.7091 9.28643 17.5934 9.61559 17.5934 10.0001C17.5934 10.384 17.7089 10.7126 17.9069 10.929C17.9922 11.0223 18.0737 11.112 18.1509 11.1977L18.1742 11.2235C18.3621 11.432 18.5444 11.6341 18.6784 11.8259C18.8316 12.0452 18.9606 12.3027 18.9927 12.6299C19.0251 12.9601 18.9457 13.2439 18.8387 13.493C18.7435 13.7145 18.6023 13.9605 18.4538 14.2191L17.8301 15.3057C17.6814 15.5649 17.5404 15.8107 17.3977 16.0037C17.2369 16.2214 17.039 16.4226 16.7558 16.5516C16.4766 16.6788 16.2066 16.6955 15.948 16.6723C15.7197 16.6518 15.4575 16.5947 15.1822 16.5348L15.1361 16.5248C15.0263 16.5009 14.911 16.4754 14.7911 16.4484C14.4849 16.3794 14.14 16.4454 13.8067 16.6409C13.4548 16.8472 13.2049 17.1441 13.103 17.4699C13.0735 17.5644 13.0447 17.6555 13.0167 17.743L13.0064 17.7756C12.9203 18.0453 12.8374 18.3054 12.7414 18.5176C12.6319 18.7596 12.483 18.9941 12.2381 19.1798C11.9901 19.3679 11.7269 19.4406 11.473 19.4721C11.2478 19.5001 10.9813 19.5001 10.7014 19.5H9.29825C9.0183 19.5001 8.75181 19.5001 8.52664 19.4721C8.27269 19.4406 8.00953 19.3679 7.76153 19.1798C7.51663 18.9941 7.3677 18.7596 7.25826 18.5176C7.16226 18.3054 7.07931 18.0453 6.99327 17.7756L6.98288 17.743C6.9549 17.6554 6.92605 17.5641 6.89646 17.4695C6.79467 17.144 6.54495 16.8473 6.19334 16.6411C5.86029 16.4459 5.51572 16.3799 5.20981 16.4489C5.08432 16.4772 4.96402 16.5038 4.84959 16.5286L4.81779 16.5355C4.54244 16.5954 4.28026 16.6525 4.05197 16.673C3.79335 16.6962 3.52336 16.6795 3.24412 16.5523C2.96096 16.4233 2.76307 16.2221 2.60218 16.0044C2.45956 15.8114 2.31857 15.5657 2.16985 15.3065L1.54621 14.2199C1.39773 13.9613 1.25649 13.7153 1.16129 13.4937C1.05427 13.2446 0.97492 12.9608 1.00733 12.6306C1.03945 12.3034 1.16838 12.0459 1.32159 11.8266C1.45561 11.6348 1.63789 11.4326 1.82585 11.2241L1.84911 11.1983C1.92603 11.113 2.00716 11.0236 2.09219 10.9306C2.29053 10.7138 2.40616 10.3846 2.40616 10.0001C2.40616 9.61514 2.29042 9.28557 2.09182 9.06846C2.00706 8.9758 1.92618 8.88666 1.84948 8.80156C1.84172 8.79295 1.83397 8.78435 1.82622 8.77575C1.63829 8.5673 1.45604 8.36513 1.32204 8.17337C1.16884 7.95413 1.03992 7.69668 1.00779 7.36946C0.975365 7.03932 1.05469 6.75551 1.16167 6.50646C1.25684 6.28493 1.39804 6.03895 1.54647 5.78034C1.55223 5.77031 1.558 5.76026 1.56378 5.75018L2.15303 4.72329C2.1587 4.7134 2.16436 4.70353 2.17001 4.69368C2.31872 4.43444 2.45971 4.18866 2.60233 3.99562C2.76322 3.77785 2.96111 3.5766 3.2443 3.44759C3.52356 3.32037 3.79357 3.30365 4.05222 3.32688C4.28053 3.34739 4.54274 3.40443 4.81813 3.46433C4.82871 3.46663 4.83932 3.46894 4.84994 3.47125C4.84994 3.47125 4.84765 3.46876 4.87561 3.47791L4.90129 3.48458L4.84994 3.47125C4.9645 3.49615 5.08494 3.52277 5.21059 3.55109C5.51632 3.62001 5.86071 3.55412 6.19358 3.35895C6.54505 3.15289 6.79466 2.85633 6.89641 2.53097C6.92603 2.43624 6.95492 2.34487 6.98292 2.25713C6.98639 2.24627 6.98985 2.23542 6.99331 2.22458C7.07935 1.95488 7.16229 1.69488 7.25828 1.48264C7.36771 1.24066 7.51662 1.00623 7.76148 0.820527C8.00944 0.632471 8.27257 0.55971 8.52649 0.528149C8.75162 0.500166 9.01808 0.500173 9.29799 0.50018C9.30881 0.500181 9.31965 0.500181 9.33052 0.500179L10.7011 0.500008C10.9811 0.499932 11.2476 0.499859 11.4728 0.527798Z" + fill="currentColor" + ></path> + <path + fill-rule="evenodd" + clip-rule="evenodd" + d="M10.0018 7.92367C8.85503 7.92367 7.92535 8.85349 7.92535 10.0005C7.92535 11.1475 8.85503 12.0773 10.0018 12.0773C11.1487 12.0773 12.0783 11.1475 12.0783 10.0005C12.0783 8.85349 11.1487 7.92367 10.0018 7.92367ZM6.54102 10.0005C6.54102 8.08883 8.09048 6.53912 10.0018 6.53912C11.9132 6.53912 13.4627 8.08883 13.4627 10.0005C13.4627 11.9121 11.9132 13.4619 10.0018 13.4619C8.09048 13.4619 6.54102 11.9121 6.54102 10.0005Z" + fill="currentColor" + ></path> + </svg> + ) + } + /> + ); +} diff --git a/app/src/app/components/strip/components/buttonTree.tsx b/app/src/app/components/strip/components/buttonTree.tsx new file mode 100644 index 000000000..15826b582 --- /dev/null +++ b/app/src/app/components/strip/components/buttonTree.tsx @@ -0,0 +1,58 @@ +import { Button } from "./button"; + +export function ButtonTree({ + selected, + setSelected, +}: { + selected: Accessor<FrameName>; + setSelected: Setter<FrameName>; +}) { + const frameName: FrameName = "Tree"; + + return ( + <Button + title={frameName} + selected={() => selected() === frameName} + onClick={() => { + setSelected(frameName); + }} + icon={() => + selected() === frameName + ? (props: { class?: string }) => ( + <svg + width="20" + height="20" + viewBox="0 0 20 20" + fill="none" + xmlns="http://www.w3.org/2000/svg" + class={props.class} + > + <path + fill-rule="evenodd" + clip-rule="evenodd" + d="M6.22892 18C4.9566 18 3.93098 18 3.1244 17.8935C2.28697 17.7828 1.58187 17.5461 1.02187 16.9958C0.461866 16.4455 0.221 15.7526 0.108411 14.9296C-3.31178e-05 14.137 -1.7645e-05 13.1291 1.54211e-06 11.8788L1.04302e-06 6.29541C-2.62958e-05 5.47395 -4.90919e-05 4.78896 0.0743487 4.24516C0.152876 3.67118 0.325617 3.15297 0.74937 2.73655C1.17312 2.32012 1.70045 2.15037 2.28453 2.0732C2.8379 2.00009 3.53494 2.00011 4.37086 2.00013L5.92614 2.00007C6.57086 1.99946 7.08108 1.99899 7.55104 2.18869C8.021 2.37838 8.38357 2.73117 8.84172 3.17694L9.06221 3.39116C9.20357 3.52844 9.28285 3.60481 9.34651 3.65795C9.37487 3.68162 9.39161 3.69332 9.40051 3.699C9.40473 3.70169 9.40712 3.70298 9.40801 3.70345L9.40914 3.70399L9.41033 3.70438C9.41129 3.70466 9.41392 3.7054 9.41885 3.7064C9.42924 3.70851 9.44952 3.71175 9.48662 3.7145C9.5699 3.72068 9.68092 3.72113 9.87966 3.72113L13.0938 3.72111C13.6755 3.72097 14.072 3.72087 14.4167 3.78961C15.7901 4.06347 16.8634 5.11825 17.1421 6.46785C17.2021 6.75842 17.2105 7.08647 17.2116 7.53472C17.4034 7.54922 17.5834 7.56801 17.7514 7.59237C18.5137 7.70289 19.1943 7.94917 19.6331 8.57761C20.0718 9.20605 20.0607 9.91868 19.8913 10.6574C19.7278 11.3702 19.3805 12.2552 18.9553 13.3384L18.6619 14.0857C18.3405 14.9047 18.0787 15.5717 17.8049 16.0905C17.5191 16.6321 17.1912 17.0712 16.7057 17.3985C16.2202 17.7258 15.6854 17.8685 15.0682 17.9356C14.4771 18 13.7497 18 12.8565 18L6.22892 18ZM5.81464 3.37155C6.62543 3.37155 6.83809 3.3835 7.02081 3.45726C7.20352 3.53101 7.36333 3.6694 7.94003 4.22946L8.08126 4.36662L8.1337 4.41774C8.34688 4.62596 8.57694 4.85067 8.8789 4.97256C9.18087 5.09444 9.50523 5.09353 9.8058 5.09268L9.87966 5.09254H13.0114C13.7067 5.09254 13.9504 5.096 14.1392 5.13363C14.9632 5.29795 15.6072 5.93081 15.7744 6.74058C15.805 6.88885 15.8134 7.07165 15.8155 7.48813C15.5174 7.48575 15.2019 7.48576 14.8692 7.48577H7.25506C6.70131 7.48575 6.23171 7.48573 5.84482 7.52626C5.43306 7.56939 5.05328 7.66313 4.69832 7.88868C4.34336 8.11422 4.10052 8.41609 3.89152 8.76739C3.69515 9.09748 3.50247 9.51829 3.27524 10.0146L2.3991 11.9279C2.00422 12.7902 1.66601 13.5287 1.435 14.1586C1.39636 13.5526 1.39555 12.7992 1.39555 11.8286V6.34295C1.39555 5.46158 1.39703 4.86953 1.45745 4.4279C1.51517 4.006 1.61493 3.82543 1.73617 3.70628C1.85741 3.58714 2.04116 3.48911 2.47049 3.43238C2.91989 3.37301 3.52235 3.37155 4.41924 3.37155H5.81464Z" + fill="currentColor" + ></path> + </svg> + ) + : (props: { class?: string }) => ( + <svg + width="20" + height="20" + viewBox="0 0 20 20" + fill="none" + xmlns="http://www.w3.org/2000/svg" + class={props.class} + > + <path + fill-rule="evenodd" + clip-rule="evenodd" + d="M7.59655 2.20712C7.10136 1.9989 6.56115 1.99943 5.9023 2.00007L4.40479 2.00015C3.57853 2.00013 2.88271 2.0001 2.32874 2.07318C1.74135 2.15066 1.20072 2.32242 0.764844 2.75008C0.328798 3.1779 0.153514 3.70882 0.0744639 4.28569C-4.74114e-05 4.82945 -2.52828e-05 5.51233 9.81743e-07 6.32281V11.8675C-1.65965e-05 13.1029 -3.08677e-05 14.1058 0.108284 14.8963C0.221156 15.72 0.464085 16.4241 1.03541 16.9846C1.60656 17.545 2.32369 17.7831 3.16265 17.8938C3.96804 18 4.99002 18 6.2493 18H13.7507C15.01 18 16.032 18 16.8374 17.8938C17.6763 17.7831 18.3934 17.545 18.9646 16.9846C19.5359 16.4241 19.7788 15.72 19.8917 14.8963C20 14.1058 20 13.1029 20 11.8676V9.94525C20 8.70992 20 7.70702 19.8917 6.91657C19.7788 6.09287 19.5359 5.38878 18.9646 4.82823C18.3934 4.26785 17.6763 4.02972 16.8374 3.91905C16.0319 3.81281 15.0099 3.81283 13.7506 3.81285L9.91202 3.81285C9.70527 3.81285 9.59336 3.81232 9.51046 3.80596C9.47861 3.80352 9.461 3.80081 9.45249 3.79919C9.44546 3.79427 9.43137 3.78367 9.40771 3.76281C9.34589 3.70835 9.26838 3.62926 9.12578 3.48235L8.91813 3.26831C8.46421 2.79975 8.09187 2.4154 7.59655 2.20712ZM2.53158 3.55817C2.97217 3.50005 3.5649 3.49846 4.45741 3.49846H5.77707C6.19724 3.49846 6.45952 3.50169 6.63994 3.51453C6.81907 3.52729 6.91262 3.54925 6.99675 3.58462C7.08084 3.61998 7.16148 3.67125 7.29433 3.78964C7.42818 3.90891 7.6114 4.09298 7.90119 4.39152L8.02253 4.51653L8.07907 4.57502C8.29018 4.79381 8.5293 5.04163 8.85233 5.17747C9.17524 5.31324 9.52282 5.31222 9.82983 5.31132L9.91202 5.31115H13.6951C15.023 5.31115 15.9424 5.31274 16.6345 5.40404C17.3048 5.49246 17.6468 5.6525 17.8873 5.88854C18.1277 6.12441 18.2906 6.45944 18.3807 7.11653C18.4737 7.79534 18.4753 8.69706 18.4753 10.0001V11.8128C18.4753 13.1158 18.4737 14.0175 18.3807 14.6963C18.2906 15.3534 18.1277 15.6884 17.8873 15.9243C17.6468 16.1603 17.3048 16.3204 16.6345 16.4088C15.9424 16.5001 15.023 16.5017 13.6951 16.5017H6.30494C4.97698 16.5017 4.05764 16.5001 3.36549 16.4088C2.69519 16.3204 2.35324 16.1603 2.11266 15.9243C1.87226 15.6884 1.70936 15.3534 1.61932 14.6963C1.5263 14.0175 1.52468 13.1158 1.52468 11.8128V6.37469C1.52468 5.49891 1.5263 4.91765 1.5855 4.48566C1.64172 4.07541 1.73696 3.91355 1.8421 3.81039C1.94741 3.70706 2.11288 3.6134 2.53158 3.55817Z" + fill="currentColor" + ></path> + </svg> + ) + } + /> + ); +} diff --git a/app/src/app/components/strip/components/clickable.tsx b/app/src/app/components/strip/components/clickable.tsx new file mode 100644 index 000000000..be3dcf38e --- /dev/null +++ b/app/src/app/components/strip/components/clickable.tsx @@ -0,0 +1,36 @@ +import { classPropToString } from "/src/solid/classes"; + +export function Clickable({ + selected, + onClick, + href, + icon, + children, + title, +}: { + title: string; + selected?: Accessor<boolean>; + onClick?: VoidFunction; + href?: string; + icon?: () => ValidComponent; +} & ParentProps) { + return ( + <Dynamic + component={onClick ? "button" : href ? "a" : "span"} + class={classPropToString([ + selected?.() ? "bg-orange-200/10" : "opacity-50 hover:bg-orange-200/10", + "select-none rounded-lg p-3.5 hover:text-orange-400 hover:opacity-100 active:scale-90", + ])} + title={title} + onClick={onClick} + href={href} + target={ + href?.startsWith("/") || href?.startsWith("http") ? "_blank" : undefined + } + > + <Show when={icon} fallback={children}> + {(icon) => <Dynamic component={icon()()} class="size-5" />} + </Show> + </Dynamic> + ); +} diff --git a/app/src/app/components/strip/index.tsx b/app/src/app/components/strip/index.tsx new file mode 100644 index 000000000..f24b352d8 --- /dev/null +++ b/app/src/app/components/strip/index.tsx @@ -0,0 +1,65 @@ +import { AnchorAPI } from "./components/anchorAPI"; +import { AnchorGit } from "./components/anchorGit"; +import { AnchorHome } from "./components/anchorHome"; +import { AnchorLogo } from "./components/anchorLogo"; +import { AnchorNostr } from "./components/anchorNostr"; +import { ButtonChart } from "./components/buttonChart"; +import { ButtonFavorites } from "./components/buttonFavorites"; +import { ButtonHistory } from "./components/buttonHistory"; +import { ButtonRefresh } from "./components/buttonRefresh"; +import { ButtonSearch } from "./components/buttonSearch"; +import { ButtonSettings } from "./components/buttonSettings"; +import { ButtonTree } from "./components/buttonTree"; + +export function StripDesktop({ + selected, + setSelected, + needsRefresh, +}: { + selected: Accessor<FrameName>; + setSelected: Setter<FrameName>; + needsRefresh: Accessor<boolean>; +}) { + return ( + <> + <AnchorLogo /> + + <ButtonTree selected={selected} setSelected={setSelected} /> + <ButtonFavorites selected={selected} setSelected={setSelected} /> + <ButtonSearch selected={selected} setSelected={setSelected} /> + <ButtonHistory selected={selected} setSelected={setSelected} /> + + <ButtonSettings selected={selected} setSelected={setSelected} /> + + <div class="size-full" /> + + <Show when={needsRefresh()}> + <ButtonRefresh /> + </Show> + + <AnchorAPI /> + <AnchorGit /> + <AnchorNostr /> + {/* <AnchorHome /> */} + </> + ); +} + +export function StripMobile({ + selected, + setSelected, +}: { + selected: Accessor<FrameName>; + setSelected: Setter<FrameName>; +}) { + return ( + <> + <ButtonChart selected={selected} setSelected={setSelected} /> + <ButtonTree selected={selected} setSelected={setSelected} /> + <ButtonFavorites selected={selected} setSelected={setSelected} /> + <ButtonSearch selected={selected} setSelected={setSelected} /> + <ButtonHistory selected={selected} setSelected={setSelected} /> + <ButtonSettings selected={selected} setSelected={setSelected} /> + </> + ); +} diff --git a/app/src/app/index.tsx b/app/src/app/index.tsx new file mode 100644 index 000000000..b20ef9606 --- /dev/null +++ b/app/src/app/index.tsx @@ -0,0 +1,309 @@ +import { createRWS } from "/src/solid/rws"; + +import { env } from "../env"; +import { createDatasets } from "../scripts/datasets"; +import { chartState } from "../scripts/lightweightCharts/chart/state"; +import { setTimeScale } from "../scripts/lightweightCharts/chart/time"; +import { createPresets } from "../scripts/presets"; +import { priceToUSLocale } from "../scripts/utils/locale"; +import { sleep } from "../scripts/utils/sleep"; +import { + readBooleanFromStorage, + saveToStorage, +} from "../scripts/utils/storage"; +import { readBooleanURLParam, writeURLParam } from "../scripts/utils/urlParams"; +import { webSockets } from "../scripts/ws"; +import { classPropToString } from "../solid/classes"; +import { Background, LOCAL_STORAGE_MARQUEE_KEY } from "./components/background"; +import { ChartFrame } from "./components/frames/chart"; +import { TreeFrame } from "./components/frames/tree"; +import { StripDesktop, StripMobile } from "./components/strip"; +import { registerServiceWorker } from "./scripts/register"; + +const LOCAL_STORAGE_BAR_KEY = "bar-width"; +const LOCAL_STORAGE_FULLSCREEN = "fullscrenn"; + +export const INPUT_PRESET_SEARCH_ID = "input-search-preset"; + +export function App() { + const needRefresh = registerServiceWorker().needRefresh[0]; + + const tabFocused = createRWS(true); + + const qrcode = createRWS(""); + + const fullscreen = createRWS( + readBooleanURLParam(LOCAL_STORAGE_FULLSCREEN) || + readBooleanFromStorage(LOCAL_STORAGE_FULLSCREEN) || + false, + ); + + const activeResources = createRWS<Set<ResourceDataset<any, any>>>(new Set(), { + equals: false, + }); + + const datasets = createDatasets({ + setActiveResources: activeResources.set, + }); + + const windowWidth = createRWS(window.innerWidth); + const windowResizeCallback = () => { + windowWidth.set(window.innerWidth); + }; + window.addEventListener("resize", windowResizeCallback); + onCleanup(() => window.removeEventListener("resize", windowResizeCallback)); + + const windowSizeIsAtLeastMedium = createMemo(() => windowWidth() >= 720); + + const barWidth = createRWS( + Number(localStorage.getItem(LOCAL_STORAGE_BAR_KEY)), + ); + + createEffect(() => { + localStorage.setItem(LOCAL_STORAGE_BAR_KEY, String(barWidth())); + }); + + createEffect(() => { + if (fullscreen()) { + writeURLParam(LOCAL_STORAGE_FULLSCREEN, "true"); + saveToStorage(LOCAL_STORAGE_FULLSCREEN, fullscreen()); + } else { + writeURLParam(LOCAL_STORAGE_FULLSCREEN, undefined); + saveToStorage(LOCAL_STORAGE_FULLSCREEN, undefined); + } + }); + + const _selectedFrame = createRWS<FrameName>("Chart"); + + const selectedFrame = createMemo(() => + windowSizeIsAtLeastMedium() && _selectedFrame() === "Chart" + ? "Tree" + : _selectedFrame(), + ); + + const presets = createPresets(datasets); + + const marquee = createRWS(!localStorage.getItem(LOCAL_STORAGE_MARQUEE_KEY)); + + const resizingBarStart = createRWS<number | undefined>(undefined); + + createEffect( + () => { + if (!windowSizeIsAtLeastMedium() && presets.selected()) { + _selectedFrame.set("Chart"); + } + }, + { + deffer: true, + }, + ); + + onMount(() => { + webSockets.openAll(); + + createEffect(() => { + const latest = webSockets.liveKrakenCandle.latest(); + + if (latest) { + const close = latest.close; + + console.log("close:", close); + + document.title = `${priceToUSLocale(latest.close, false)} | Satonomics`; + } + }); + }); + + const FavoritesFrame = lazy(() => + import("./components/frames/favorites").then((d) => ({ + default: d.FavoritesFrame, + })), + ); + const HistoryFrame = lazy(() => + import("./components/frames/history").then((d) => ({ + default: d.HistoryFrame, + })), + ); + const SearchFrame = lazy(() => + import("./components/frames/search").then((d) => ({ + default: d.SearchFrame, + })), + ); + const SettingsFrame = lazy(() => + import("./components/frames/settings").then((d) => ({ + default: d.SettingsFrame, + })), + ); + const Qrcode = lazy(() => + import("./components/qrcode").then((d) => ({ + default: d.Qrcode, + })), + ); + + const documentVisibilityChange = () => + tabFocused.set(document.visibilityState === "visible"); + document.addEventListener("visibilitychange", documentVisibilityChange); + onCleanup(() => + document.removeEventListener("visibilitychange", documentVisibilityChange), + ); + + const documentOnKeyDown = async (event: KeyboardEvent) => { + switch (event.key) { + case "Escape": { + event.stopPropagation(); + event.preventDefault(); + + _selectedFrame.set("Chart"); + + break; + } + case "/": { + event.stopPropagation(); + event.preventDefault(); + + _selectedFrame.set("Search"); + + await sleep(50); + + document.getElementById(INPUT_PRESET_SEARCH_ID)?.focus(); + + break; + } + } + }; + document.addEventListener("keydown", documentOnKeyDown); + onCleanup(() => document.removeEventListener("keydown", documentOnKeyDown)); + + const resizeInitialRange = createRWS<TimeRange | null>(null); + + return ( + <> + <Background marquee={marquee} focused={tabFocused} /> + + <div + class="relative h-dvh selection:bg-orange-800" + style={{ + "user-select": resizingBarStart() !== undefined ? "none" : undefined, + }} + onMouseMove={(event) => { + const start = resizingBarStart(); + + if (start !== undefined) { + barWidth.set(event.x - start + 384); + + setTimeScale(resizeInitialRange()); + } + }} + onMouseUp={() => resizingBarStart.set(undefined)} + onMouseLeave={() => resizingBarStart.set(undefined)} + onTouchEnd={() => resizingBarStart.set(undefined)} + onTouchCancel={() => resizingBarStart.set(undefined)} + > + <Qrcode qrcode={qrcode} /> + + <div class="flex size-full flex-col md:flex-row md:p-3"> + <Show when={!windowSizeIsAtLeastMedium() || !fullscreen()}> + <div + class={classPropToString([ + env.standalone && "border-t", + "flex h-full flex-col overflow-hidden border-white/10 bg-gradient-to-b from-orange-500/10 to-orange-950/10 md:flex-row md:rounded-2xl md:border", + ])} + > + <div class="hidden flex-col gap-2 border-r border-white/10 bg-black/30 p-3 backdrop-blur-sm md:flex"> + <StripDesktop + selected={selectedFrame} + setSelected={_selectedFrame.set} + needsRefresh={needRefresh} + /> + </div> + <div + class="flex h-full min-h-0 md:min-w-[384px]" + style={{ + ...(windowSizeIsAtLeastMedium() + ? { + width: `min(${barWidth()}px, 75dvw)`, + } + : {}), + }} + > + <Show when={!windowSizeIsAtLeastMedium()}> + <ChartFrame + presets={presets} + hide={() => selectedFrame() !== "Chart"} + qrcode={qrcode} + standalone={false} + datasets={datasets} + activeResources={activeResources} + /> + </Show> + + <TreeFrame presets={presets} selectedFrame={selectedFrame} /> + <FavoritesFrame + presets={presets} + selectedFrame={selectedFrame} + /> + <SearchFrame presets={presets} selectedFrame={selectedFrame} /> + <HistoryFrame presets={presets} selectedFrame={selectedFrame} /> + <SettingsFrame + marquee={marquee} + selectedFrame={selectedFrame} + /> + </div> + + <div + class={classPropToString([ + env.standalone && "pb-6", + "flex justify-between gap-3 border-t border-white/10 bg-black/30 p-2 backdrop-blur-sm md:hidden", + ])} + > + <StripMobile + selected={selectedFrame} + setSelected={_selectedFrame.set} + /> + </div> + </div> + </Show> + + <Show when={!fullscreen()}> + <div + class="mx-[3px] my-8 hidden w-[6px] cursor-col-resize items-center justify-center rounded-full bg-orange-100 opacity-0 hover:opacity-50 md:block" + onMouseDown={(event) => { + resizeInitialRange.set(chartState.range); + + resizingBarStart() === undefined && + // TODO: set size of bar instead + resizingBarStart.set(event.clientX); + }} + onTouchStart={(event) => { + resizeInitialRange.set(chartState.range); + + resizingBarStart() === undefined && + resizingBarStart.set(event.touches[0].clientX); + }} + onDblClick={() => { + resizeInitialRange.set(chartState.range); + + barWidth.set(0); + + setTimeScale(resizeInitialRange()); + }} + /> + </Show> + + <Show when={windowSizeIsAtLeastMedium()}> + <div class="flex min-w-0 flex-1"> + <ChartFrame + standalone={true} + presets={presets} + qrcode={qrcode} + fullscreen={fullscreen} + activeResources={activeResources} + datasets={datasets} + /> + </div> + </Show> + </div> + </div> + </> + ); +} diff --git a/app/src/app/scripts/register.ts b/app/src/app/scripts/register.ts new file mode 100644 index 000000000..a8dad2d4a --- /dev/null +++ b/app/src/app/scripts/register.ts @@ -0,0 +1,67 @@ +import { useRegisterSW } from "virtual:pwa-register/solid"; + +import { FIVE_MINUTES_IN_MS } from "/src/scripts/utils/time"; + +export function registerServiceWorker() { + return useRegisterSW({ + onRegisteredSW(swUrl, registered) { + console.log("sw: registered", registered); + + if (registered) { + const callback = async () => { + if (!(!registered.installing && navigator)) return; + + if ("connection" in navigator && !navigator.onLine) return; + + const resp = await fetch(swUrl, { + cache: "no-store", + headers: { + cache: "no-store", + "cache-control": "no-cache", + }, + }); + + if (resp?.status === 200) { + await registered.update(); + } + }; + + callback(); + + setInterval(callback, FIVE_MINUTES_IN_MS); + } + }, + onRegisterError(error) { + console.log("sw: registration error", error); + }, + onNeedRefresh() { + console.log("sw: needs refresh"); + }, + }); +} + +// From update.tsx +// onMount(async () => { +// if ('serviceWorker' in navigator) { +// try { +// const registration = await navigator.serviceWorker.register('/sw.js') + +// registration.addEventListener('updatefound', () => { +// const worker = registration.installing + +// worker?.addEventListener('statechange', () => { +// if ( +// worker.state === 'activated' && +// navigator.serviceWorker.controller +// ) { +// ;(Object.entries(props.resources) as Entries<ResourcesHTTP>) +// .map(([_, value]) => value.fetch) +// .forEach((fetch) => fetch()) + +// setTimeout(() => updateAvailable.set(true), FIVE_SECOND_IN_MS) +// } +// }) +// }) +// } catch {} +// } +// }) diff --git a/app/src/app/types.d.ts b/app/src/app/types.d.ts new file mode 100644 index 000000000..9b3b764d8 --- /dev/null +++ b/app/src/app/types.d.ts @@ -0,0 +1,7 @@ +type FrameName = + | "Chart" + | "Tree" + | "Favorites" + | "Search" + | "History" + | "Settings"; diff --git a/app/src/env.ts b/app/src/env.ts new file mode 100644 index 000000000..40ff716be --- /dev/null +++ b/app/src/env.ts @@ -0,0 +1,3 @@ +export const env = { + standalone: "standalone" in window.navigator && !!window.navigator.standalone, +}; diff --git a/app/src/index.tsx b/app/src/index.tsx new file mode 100644 index 000000000..e29363cda --- /dev/null +++ b/app/src/index.tsx @@ -0,0 +1,18 @@ +/* @refresh reload */ +import { render } from "solid-js/web"; + +import "./styles.css"; + +const root = document.getElementById("root"); + +if (import.meta.env.DEV && !(root instanceof HTMLElement)) { + throw new Error( + "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?", + ); +} + +render(() => { + const App = lazy(() => import("./app").then((d) => ({ default: d.App }))); + + return <App />; +}, root!); diff --git a/app/src/scripts/datasets/consts/address.ts b/app/src/scripts/datasets/consts/address.ts new file mode 100644 index 000000000..348db64c0 --- /dev/null +++ b/app/src/scripts/datasets/consts/address.ts @@ -0,0 +1,30 @@ +export const addressCohortsBySize = [ + { + key: "plankton", + name: "Plankton", + }, + { + key: "shrimp", + name: "Shrimp", + }, + { key: "crab", name: "Crab" }, + { key: "fish", name: "Fish" }, + { key: "shark", name: "Shark" }, + { key: "whale", name: "Whale" }, + { key: "humpback", name: "Humpback" }, + { key: "megalodon", name: "Megalodon" }, +] as const; + +export const addressCohortsByType = [ + { key: "p2pk", name: "P2PK" }, + { key: "p2pkh", name: "P2PKH" }, + { key: "p2sh", name: "P2SH" }, + { key: "p2wpkh", name: "P2WPKH" }, + { key: "p2wsh", name: "P2WSH" }, + { key: "p2tr", name: "P2TR" }, +] as const; + +export const addressCohorts = [ + ...addressCohortsBySize, + ...addressCohortsByType, +] as const; diff --git a/app/src/scripts/datasets/consts/age.ts b/app/src/scripts/datasets/consts/age.ts new file mode 100644 index 000000000..8cd5e4066 --- /dev/null +++ b/app/src/scripts/datasets/consts/age.ts @@ -0,0 +1,147 @@ +export const xthCohorts = [ + { + key: "sth", + name: "Short Term Holders", + legend: "STH", + }, + { + key: "lth", + name: "Long Term Holders", + legend: "LTH", + }, +] as const; + +export const upToCohorts = [ + { key: "up_to_1d", name: "Up To 1 Day", legend: "1D" }, + { key: "up_to_1w", name: "Up To 1 Week", legend: "1W" }, + { key: "up_to_1m", name: "Up To 1 Month", legend: "1M" }, + { key: "up_to_2m", name: "Up To 2 Months", legend: "2M" }, + { key: "up_to_3m", name: "Up To 3 Months", legend: "3M" }, + { key: "up_to_4m", name: "Up To 4 Months", legend: "4M" }, + { key: "up_to_5m", name: "Up To 5 Months", legend: "5M" }, + { key: "up_to_6m", name: "Up To 6 Months", legend: "6M" }, + { key: "up_to_1y", name: "Up To 1 Year", legend: "1Y" }, + { key: "up_to_2y", name: "Up To 2 Years", legend: "2Y" }, + { key: "up_to_3y", name: "Up To 3 Years", legend: "3Y" }, + { key: "up_to_5y", name: "Up To 5 Years", legend: "5Y" }, + { key: "up_to_7y", name: "Up To 7 Years", legend: "7Y" }, + { key: "up_to_10y", name: "Up To 10 Years", legend: "10Y" }, + { key: "up_to_15y", name: "Up To 15 Years", legend: "15Y" }, +] as const; + +export const fromXToYCohorts = [ + { + key: "from_1d_to_1w", + name: "From 1 Day To 1 Week", + legend: "1D - 1W", + }, + { + key: "from_1w_to_1m", + name: "From 1 Week To 1 Month", + legend: "1W - 1M", + }, + { + key: "from_1m_to_3m", + name: "From 1 Month To 3 Months", + legend: "1M - 3M", + }, + { + key: "from_3m_to_6m", + name: "From 3 Months To 6 Months", + legend: "3M - 6M", + }, + { + key: "from_6m_to_1y", + name: "From 6 Months To 1 Year", + legend: "6M - 1Y", + }, + { + key: "from_1y_to_2y", + name: "From 1 Year To 2 Years", + legend: "1Y - 2Y", + }, + { + key: "from_2y_to_3y", + name: "From 2 Years To 3 Years", + legend: "2Y - 3Y", + }, + { + key: "from_3y_to_5y", + name: "From 3 Years To 5 Years", + legend: "3Y - 5Y", + }, + { + key: "from_5y_to_7y", + name: "From 5 Years To 7 Years", + legend: "5Y - 7Y", + }, + { + key: "from_7y_to_10y", + name: "From 7 Years To 10 Years", + legend: "7Y - 10Y", + }, + { + key: "from_10y_to_15y", + name: "From 10 Years To 15 Years", + legend: "10Y - 15Y", + }, +] as const; + +export const fromXCohorts = [ + { + key: "from_1y", + name: "From 1 Year", + legend: "1Y+", + }, + { + key: "from_2y", + name: "From 2 Years", + legend: "2Y+", + }, + { + key: "from_4y", + name: "From 4 Years", + legend: "4Y+", + }, + { + key: "from_10y", + name: "From 10 Years", + legend: "10Y+", + }, + { + key: "from_15y", + name: "From 15 Years", + legend: "15Y+", + }, +] as const; + +export const yearCohorts = [ + { key: "year_2009", name: "2009" }, + { key: "year_2010", name: "2010" }, + { key: "year_2011", name: "2011" }, + { key: "year_2012", name: "2012" }, + { key: "year_2013", name: "2013" }, + { key: "year_2014", name: "2014" }, + { key: "year_2015", name: "2015" }, + { key: "year_2016", name: "2016" }, + { key: "year_2017", name: "2017" }, + { key: "year_2018", name: "2018" }, + { key: "year_2019", name: "2019" }, + { key: "year_2020", name: "2020" }, + { key: "year_2021", name: "2021" }, + { key: "year_2022", name: "2022" }, + { key: "year_2023", name: "2023" }, + { key: "year_2024", name: "2024" }, +] as const; + +export const ageCohorts = [ + { + key: "", + name: "", + }, + ...xthCohorts, + ...upToCohorts, + ...fromXToYCohorts, + ...fromXCohorts, + ...yearCohorts, +] as const; diff --git a/app/src/scripts/datasets/consts/averages.ts b/app/src/scripts/datasets/consts/averages.ts new file mode 100644 index 000000000..e76c8e92f --- /dev/null +++ b/app/src/scripts/datasets/consts/averages.ts @@ -0,0 +1,15 @@ +export const averages = [ + { name: "1 Week", key: "1w", days: 7 }, + { name: "8 Days", key: "8d", days: 8 }, + { name: "13 Days", key: "13d", days: 13 }, + { name: "21 Days", key: "21d", days: 21 }, + { name: "1 Month", key: "1m", days: 30 }, + { name: "34 Days", key: "34d", days: 34 }, + { name: "55 Days", key: "55d", days: 55 }, + { name: "89 Days", key: "89d", days: 89 }, + { name: "144 Days", key: "144d", days: 144 }, + { name: "1 Year", key: "1y", days: 365 }, + { name: "2 Years", key: "2y", days: 2 * 365 }, + { name: "200 Weeks", key: "200w", days: 200 * 7 }, + { name: "4 Years", key: "4y", days: 4 * 365 }, +] as const; diff --git a/app/src/scripts/datasets/consts/liquidities.ts b/app/src/scripts/datasets/consts/liquidities.ts new file mode 100644 index 000000000..1346d3728 --- /dev/null +++ b/app/src/scripts/datasets/consts/liquidities.ts @@ -0,0 +1,11 @@ +export const liquidities = [ + { + key: "illiquid", + name: "Illiquid", + }, + { key: "liquid", name: "Liquid" }, + { + key: "highly_liquid", + name: "Highly Liquid", + }, +] as const; diff --git a/app/src/scripts/datasets/consts/percentiles.ts b/app/src/scripts/datasets/consts/percentiles.ts new file mode 100644 index 000000000..c082802fd --- /dev/null +++ b/app/src/scripts/datasets/consts/percentiles.ts @@ -0,0 +1,116 @@ +export const percentiles = [ + { + key: "median_price_paid", + name: "Median", + title: "Median Paid", + value: 50, + }, + { + key: "95p_price_paid", + name: `95%`, + title: `95th Percentile Paid`, + value: 95, + }, + { + key: "90p_price_paid", + name: `90%`, + title: `90th Percentile Paid`, + value: 90, + }, + { + key: "85p_price_paid", + name: `85%`, + title: `85th Percentile Paid`, + value: 85, + }, + { + key: "80p_price_paid", + name: `80%`, + title: `80th Percentile Paid`, + value: 80, + }, + { + key: "75p_price_paid", + name: `75%`, + title: `75th Percentile Paid`, + value: 75, + }, + { + key: "70p_price_paid", + name: `70%`, + title: `70th Percentile Paid`, + value: 70, + }, + { + key: "65p_price_paid", + name: `65%`, + title: `65th Percentile Paid`, + value: 65, + }, + { + key: "60p_price_paid", + name: `60%`, + title: `60th Percentile Paid`, + value: 60, + }, + { + key: "55p_price_paid", + name: `55%`, + title: `55th Percentile Paid`, + value: 55, + }, + { + key: "45p_price_paid", + name: `45%`, + title: `45th Percentile Paid`, + value: 45, + }, + { + key: "40p_price_paid", + name: `40%`, + title: `40th Percentile Paid`, + value: 40, + }, + { + key: "35p_price_paid", + name: `35%`, + title: `35th Percentile Paid`, + value: 35, + }, + { + key: "30p_price_paid", + name: `30%`, + title: `30th Percentile Paid`, + value: 30, + }, + { + key: "25p_price_paid", + name: `25%`, + title: `25th Percentile Paid`, + value: 25, + }, + { + key: "20p_price_paid", + name: `20%`, + title: `20th Percentile Paid`, + value: 20, + }, + { + key: "15p_price_paid", + name: `15%`, + title: `15th Percentile Paid`, + value: 15, + }, + { + key: "10p_price_paid", + name: `10%`, + title: `10th Percentile Paid`, + value: 10, + }, + { + key: "05p_price_paid", + name: `5%`, + title: `5th Percentile Paid`, + value: 5, + }, +] as const; diff --git a/app/src/scripts/datasets/consts/returns.ts b/app/src/scripts/datasets/consts/returns.ts new file mode 100644 index 000000000..46053d8dd --- /dev/null +++ b/app/src/scripts/datasets/consts/returns.ts @@ -0,0 +1,14 @@ +export const totalReturns = [ + { name: "1 Day", key: "1d" }, + { name: "1 Month", key: "1m" }, + { name: "6 Months", key: "6m" }, + { name: "1 Year", key: "1y" }, + { name: "2 Years", key: "2y" }, + { name: "3 Years", key: "3y" }, + { name: "4 Years", key: "4y" }, + { name: "6 Years", key: "6y" }, + { name: "8 Years", key: "8y" }, + { name: "10 Years", key: "10y" }, +] as const; + +export const compoundReturns = [{ name: "4 Years", key: "4y" }] as const; diff --git a/app/src/scripts/datasets/consts/types.d.ts b/app/src/scripts/datasets/consts/types.d.ts new file mode 100644 index 000000000..a9357aeeb --- /dev/null +++ b/app/src/scripts/datasets/consts/types.d.ts @@ -0,0 +1,19 @@ +type AgeCohortKey = (typeof import("./age").ageCohorts)[number]["key"]; + +type AddressCohortKey = + (typeof import("./address").addressCohorts)[number]["key"]; + +type LiquidityKey = (typeof import("./liquidities").liquidities)[number]["key"]; + +type AddressCohortKeySplitByLiquidity = `${LiquidityKey}_${AddressCohortKey}`; + +type AnyCohortKey = AgeCohortKey | AddressCohortKey; + +type AnyPossibleCohortKey = AnyCohortKey | AddressCohortKeySplitByLiquidity; + +type AverageName = (typeof import("./averages").averages)[number]["key"]; + +type TotalReturnKey = (typeof import("./returns").totalReturns)[number]["key"]; + +type CompoundReturnKey = + (typeof import("./returns").compoundReturns)[number]["key"]; diff --git a/app/src/scripts/datasets/date.ts b/app/src/scripts/datasets/date.ts new file mode 100644 index 000000000..16d35d869 --- /dev/null +++ b/app/src/scripts/datasets/date.ts @@ -0,0 +1,41 @@ +import groupedKeysToPath from "/src/../../datasets/grouped_keys_to_url_path.json"; + +import { createResourceDataset } from "./resource"; + +export { averages } from "./consts/averages"; + +export function createDateDatasets({ + setActiveResources, +}: { + setActiveResources: Setter<Set<ResourceDataset<any, any>>>; +}) { + type Key = keyof typeof groupedKeysToPath.date; + type ResourceData = ReturnType<typeof createResourceDataset<"date">>; + + const resourceDatasets = {} as Record<Exclude<Key, "ohlc">, ResourceData>; + + Object.entries(groupedKeysToPath.date).forEach(([_key, path]) => { + const key = _key as Key; + + if (key !== "ohlc") { + resourceDatasets[key] = createResourceDataset<"date">({ + scale: "date", + path, + setActiveResources, + }); + } + }); + + const price = createResourceDataset<"date", OHLC>({ + scale: "date", + path: "/date-to-ohlc", + setActiveResources, + }); + + const datasets = { + price, + ...resourceDatasets, + }; + + return datasets; +} diff --git a/app/src/scripts/datasets/height.ts b/app/src/scripts/datasets/height.ts new file mode 100644 index 000000000..3b7325a5d --- /dev/null +++ b/app/src/scripts/datasets/height.ts @@ -0,0 +1,36 @@ +import groupedKeysToPath from "/src/../../datasets/grouped_keys_to_url_path.json"; + +import { createResourceDataset } from "./resource"; + +export function createHeightDatasets({ + setActiveResources, +}: { + setActiveResources: Setter<Set<ResourceDataset<any, any>>>; +}) { + type Key = keyof typeof groupedKeysToPath.height; + type ResourceData = ReturnType<typeof createResourceDataset<"height">>; + + const resourceDatasets = {} as Record<Exclude<Key, "ohlc">, ResourceData>; + + Object.keys(groupedKeysToPath.height).forEach(([_key, path]) => { + const key = _key as Key; + if (key !== "ohlc") { + resourceDatasets[key] = createResourceDataset<"height">({ + scale: "height", + path, + setActiveResources, + }); + } + }); + + const price = createResourceDataset<"height", OHLC>({ + scale: "height", + path: "/height-to-ohlc", + setActiveResources, + }); + + return { + ...resourceDatasets, + price, + }; +} diff --git a/app/src/scripts/datasets/index.ts b/app/src/scripts/datasets/index.ts new file mode 100644 index 000000000..458b53ad8 --- /dev/null +++ b/app/src/scripts/datasets/index.ts @@ -0,0 +1,17 @@ +import { createDateDatasets } from "./date"; +import { createHeightDatasets } from "./height"; + +export const scales = ["date" as const, "height" as const]; + +export const HEIGHT_CHUNK_SIZE = 10_000; + +export function createDatasets({ + setActiveResources, +}: { + setActiveResources: Setter<Set<ResourceDataset<any, any>>>; +}) { + return { + date: createDateDatasets({ setActiveResources }), + height: createHeightDatasets({ setActiveResources }), + } satisfies Record<ResourceScale, any>; +} diff --git a/app/src/scripts/datasets/resource.ts b/app/src/scripts/datasets/resource.ts new file mode 100644 index 000000000..227f92dba --- /dev/null +++ b/app/src/scripts/datasets/resource.ts @@ -0,0 +1,246 @@ +import { createLazyMemo } from "@solid-primitives/memo"; + +import { + ONE_DAY_IN_MS, + ONE_HOUR_IN_MS, + ONE_MINUTE_IN_MS, +} from "/src/scripts/utils/time"; +import { createRWS } from "/src/solid/rws"; + +import { HEIGHT_CHUNK_SIZE } from "."; + +export function createResourceDataset< + Scale extends ResourceScale, + Type extends OHLC | number = number, +>({ + scale, + path, + setActiveResources, +}: { + scale: Scale; + path: string; + setActiveResources: Setter<Set<ResourceDataset<any, any>>>; +}) { + const baseURL = `${ + location.hostname === "localhost" + ? "http://localhost:3110" + : "https://api.satonomics.xyz" + }${path}`; + + type Dataset = Scale extends "date" + ? FetchedDateDataset<Type> + : FetchedHeightDataset<Type>; + + type Value = DatasetValue< + Type extends number ? SingleValueData : CandlestickData + >; + + const fetchedJSONs = new Array( + (new Date().getFullYear() - new Date("2009-01-01").getFullYear()) * + (scale === "date" ? 2 : 8), + ) + .fill(null) + .map((): FetchedResult<Scale, Type> => { + const json = createRWS<FetchedJSON<Scale, Type, Dataset> | null>(null); + + return { + at: null, + json, + loading: false, + vec: createMemo(() => { + const map = json()?.dataset.map || null; + + const chunkId = json()?.chunk.id!; + + if (!map) { + return null; + } + + if (Array.isArray(map)) { + return map.map( + (value, index) => + ({ + number: chunkId + index, + time: (chunkId + index) as Time, + ...(typeof value !== "number" && value !== null + ? { ...(value as OHLC), value: value.close } + : { value: value === null ? NaN : (value as number) }), + }) as any as Value, + ); + } else { + return Object.entries(map).map( + ([date, value]) => + ({ + number: new Date(date).valueOf() / ONE_DAY_IN_MS, + time: date, + ...(typeof value !== "number" && value !== null + ? { ...(value as OHLC), value: value.close } + : { value: value === null ? NaN : (value as number) }), + }) as any as Value, + ); + } + }), + }; + }) as FetchedResult<Scale, Type>[]; + + const _fetch = async (id: number) => { + const index = + scale === "date" ? id - 2009 : Math.floor(id / HEIGHT_CHUNK_SIZE); + + if ( + index < 0 || + (scale === "date" && id > new Date().getUTCFullYear()) || + (scale === "height" && + id > 165 * 365 * (new Date().getUTCFullYear() - 2009)) + ) { + return; + } + + const fetched = fetchedJSONs.at(index); + + if (!fetched || fetched.loading) { + return; + } else if (fetched.at) { + const diff = new Date().valueOf() - fetched.at.valueOf(); + + if ( + diff < ONE_MINUTE_IN_MS || + (index < fetchedJSONs.findLastIndex((json) => json.at) && + diff < ONE_HOUR_IN_MS) + ) { + return; + } + } + + fetched.loading = true; + + let cache: Cache | undefined; + + const urlWithQuery = `${baseURL}?chunk=${id}`; + + if (!fetched.json()) { + try { + cache = await caches.open("resources"); + + const cachedResponse = await cache.match(urlWithQuery); + + if (cachedResponse) { + const json = await convertResponseToJSON<Scale, Type>(cachedResponse); + + if (json) { + console.log(`cache: ${path}?chunk=${id}`); + + fetched.json.set(() => json); + } + } + } catch {} + } + + try { + const fetchedResponse = await fetch(urlWithQuery); + + if (!fetchedResponse.ok) { + fetched.loading = false; + return; + } + + const clonedResponse = fetchedResponse.clone(); + + const json = await convertResponseToJSON<Scale, Type>(fetchedResponse); + + if (json) { + console.log(`fetch: ${path}?chunk=${id}`); + + const previousMap = fetched.json()?.dataset.map; + const newMap = json.dataset.map; + + const previousLength = Object.keys(previousMap || []).length; + const newLength = Object.keys(newMap).length; + + if (!newLength) { + fetched.loading = false; + return; + } + + if (previousLength && previousLength <= newLength) { + const previousLastValue = Object.values(previousMap || []).at(-1); + const newLastValue = Object.values(newMap).at(-1); + + if (typeof newLastValue === "number") { + if (previousLastValue === newLastValue) { + fetched.at = new Date(); + fetched.loading = false; + return; + } + } else { + const previousLastOHLC = previousLastValue as OHLC; + const newLastOHLC = newLastValue as OHLC; + + if ( + previousLastOHLC.open === newLastOHLC.open && + previousLastOHLC.high === newLastOHLC.high && + previousLastOHLC.low === newLastOHLC.low && + previousLastOHLC.close === newLastOHLC.close + ) { + fetched.loading = false; + fetched.at = new Date(); + return; + } + } + } + + fetched.json.set(() => json); + + if (cache) { + cache.put(urlWithQuery, clonedResponse); + } + } + } catch { + fetched.loading = false; + return; + } + + fetched.at = new Date(); + fetched.loading = false; + }; + + const resource: ResourceDataset<Scale, Type> = { + scale, + url: baseURL, + fetch: _fetch, + fetchedJSONs, + values: createLazyMemo(() => { + setActiveResources((resources) => resources.add(resource)); + + onCleanup(() => + setActiveResources((resources) => { + resources.delete(resource); + return resources; + }), + ); + + const flat = fetchedJSONs.flatMap((fetched) => fetched.vec() || []); + + return flat; + }), + drop() { + fetchedJSONs.forEach((fetched) => { + fetched.at = null; + fetched.json.set(null); + }); + }, + }; + + return resource; +} + +async function convertResponseToJSON< + Scale extends ResourceScale, + Type extends number | OHLC, +>(response: Response) { + try { + return (await response.json()) as FetchedJSON<Scale, Type>; + } catch (_) { + return null; + } +} diff --git a/app/src/scripts/datasets/types.d.ts b/app/src/scripts/datasets/types.d.ts new file mode 100644 index 000000000..beb342444 --- /dev/null +++ b/app/src/scripts/datasets/types.d.ts @@ -0,0 +1,98 @@ +type Datasets = ReturnType<typeof import("./index").createDatasets>; + +type DateDatasets = Datasets["date"]; +type HeightDatasets = Datasets["height"]; +type AnyDatasets = DateDatasets | HeightDatasets; + +type ResourceScale = (typeof import("./index").scales)[index]; + +type DatasetValue<T> = T & Numbered & Valued; + +interface Dataset< + Scale extends ResourceScale, + Value extends SingleValueData | CandlestickData = SingleValueData, +> { + scale: Scale; + values: Accessor<DatasetValue<Value>[]>; +} + +interface ResourceDataset< + Scale extends ResourceScale, + Type extends OHLC | number = number, + FetchedDataset extends + | FetchedDateDataset<Type> + | FetchedHeightDataset<Type> = Scale extends "date" + ? FetchedDateDataset<Type> + : FetchedHeightDataset<Type>, + Value extends SingleValueData | CandlestickData = Type extends number + ? SingleValueData + : CandlestickData, +> extends Dataset<Scale, Value> { + url: string; + fetch: (id: number) => void; + fetchedJSONs: FetchedResult<Scale, Type>[]; + drop: VoidFunction; +} + +interface FetchedResult< + Scale extends ResourceScale, + Type extends number | OHLC, + Dataset extends + | FetchedDateDataset<Type> + | FetchedHeightDataset<Type> = Scale extends "date" + ? FetchedDateDataset<Type> + : FetchedHeightDataset<Type>, + Value extends DatasetValue<SingleValueData | CandlestickData> = DatasetValue< + Type extends number ? SingleValueData : CandlestickData + >, +> { + at: Date | null; + json: RWS<FetchedJSON<Scale, Type, Dataset> | null>; + vec: Accessor<Value[] | null>; + loading: boolean; +} + +interface FetchedJSON< + Scale extends ResourceScale, + Type extends number | OHLC, + Dataset extends + | FetchedDateDataset<Type> + | FetchedHeightDataset<Type> = Scale extends "date" + ? FetchedDateDataset<Type> + : FetchedHeightDataset<Type>, +> { + source: FetchedSource; + chunk: FetchedChunk; + dataset: FetchedDataset<Scale, Type, Dataset>; +} + +type FetchedSource = string; + +interface FetchedChunk { + id: number; + previous: string | null; + next: string | null; +} + +interface FetchedDataset< + Scale extends ResourceScale, + Type extends number | OHLC, + Dataset extends + | FetchedDateDataset<Type> + | FetchedHeightDataset<Type> = Scale extends "date" + ? FetchedDateDataset<Type> + : FetchedHeightDataset<Type>, +> { + version: number; + map: Dataset; +} + +type FetchedDateDataset<T> = Record<string, T>; +type FetchedHeightDataset<T> = T[]; + +interface OHLC { + open: number; + high: number; + low: number; + close: number; +} diff --git a/app/src/scripts/lightweightCharts/chart/clean.ts b/app/src/scripts/lightweightCharts/chart/clean.ts new file mode 100644 index 000000000..2e3a5d246 --- /dev/null +++ b/app/src/scripts/lightweightCharts/chart/clean.ts @@ -0,0 +1,11 @@ +import { chartState } from "./state"; + +export function cleanChart() { + console.log("chart: clean"); + + try { + chartState.chart?.remove(); + } catch {} + + chartState.chart = null; +} diff --git a/app/src/scripts/lightweightCharts/chart/create.ts b/app/src/scripts/lightweightCharts/chart/create.ts new file mode 100644 index 000000000..484d98cbb --- /dev/null +++ b/app/src/scripts/lightweightCharts/chart/create.ts @@ -0,0 +1,68 @@ +import { + createChart as createClassicChart, + createChartEx as createCustomChart, + CrosshairMode, +} from "lightweight-charts"; + +import { colors } from "../../utils/colors"; +import { priceToUSLocale } from "../../utils/locale"; +import { cleanChart } from "./clean"; +import { HorzScaleBehaviorHeight } from "./horzScaleBehavior"; +import { chartState } from "./state"; + +export function createChart(scale: ResourceScale) { + cleanChart(); + + console.log(`chart: create (scale: ${scale})`); + + const { white } = colors; + + const options: DeepPartialChartOptions = { + autoSize: true, + layout: { + fontFamily: "Lexend", + background: { color: "transparent" }, + fontSize: 14, + textColor: white, + }, + grid: { + vertLines: { visible: false }, + horzLines: { visible: false }, + }, + leftPriceScale: { + // borderColor: white, + }, + rightPriceScale: { + // borderColor: white, + }, + timeScale: { + minBarSpacing: scale === "date" ? 0.05 : 0.005, + shiftVisibleRangeOnNewBar: false, + allowShiftVisibleRangeOnWhitespaceReplacement: false, + }, + crosshair: { + mode: CrosshairMode.Normal, + horzLine: { + color: white, + labelBackgroundColor: white, + }, + vertLine: { + color: white, + labelBackgroundColor: white, + }, + }, + localization: { + priceFormatter: priceToUSLocale, + locale: "en-us", + }, + }; + + if (scale === "date") { + chartState.chart = createClassicChart("chart", options); + } else { + const horzScaleBehavior = new HorzScaleBehaviorHeight(); + + // @ts-ignore + chartState.chart = createCustomChart("chart", horzScaleBehavior, options); + } +} diff --git a/app/src/scripts/lightweightCharts/chart/horzScaleBehavior.ts b/app/src/scripts/lightweightCharts/chart/horzScaleBehavior.ts new file mode 100644 index 000000000..68c9178f1 --- /dev/null +++ b/app/src/scripts/lightweightCharts/chart/horzScaleBehavior.ts @@ -0,0 +1,89 @@ +// @ts-nocheck + +// https://github.com/tradingview/lightweight-charts/blob/master/tests/e2e/graphics/test-cases/horizontal-price-scale.js + +import { type IHorzScaleBehavior } from "lightweight-charts"; + +export class HorzScaleBehaviorHeight implements IHorzScaleBehavior<number> { + options() {} + setOptions() {} + preprocessData() {} + updateFormatter() {} + createConverterToInternalObj() { + return (price) => price; + } + + key(item) { + return item; + } + + cacheKey(item) { + return item; + } + + convertHorzItemToInternal(item) { + return item; + } + + formatHorzItem(item) { + return item; + } + + formatTickmark(tickMark) { + return tickMark.time.toLocaleString("en-us"); + } + + maxTickMarkWeight(tickMarks) { + return tickMarks.reduce(markWithGreaterWeight, tickMarks[0]).weight; + } + + fillWeightsForPoints(sortedTimePoints, startIndex) { + for (let index = startIndex; index < sortedTimePoints.length; ++index) { + sortedTimePoints[index].timeWeight = computeWeight( + sortedTimePoints[index].time, + ); + } + } +} + +function markWithGreaterWeight(a, b) { + return a.weight > b.weight ? a : b; +} + +function computeWeight(value: number) { + // if (value === Math.ceil(value / 1000000) * 1000000) { + // return 12; + // } + if (value === Math.ceil(value / 100000) * 100000) { + return 11; + } + if (value === Math.ceil(value / 10000) * 10000) { + return 10; + } + if (value === Math.ceil(value / 1000) * 1000) { + return 9; + } + if (value === Math.ceil(value / 100) * 100) { + return 8; + } + if (value === Math.ceil(value / 50) * 50) { + return 7; + } + if (value === Math.ceil(value / 25) * 25) { + return 6; + } + if (value === Math.ceil(value / 10) * 10) { + return 5; + } + if (value === Math.ceil(value / 5) * 5) { + return 4; + } + if (value === Math.ceil(value)) { + return 3; + } + if (value * 2 === Math.ceil(value * 2)) { + return 1; + } + + return 0; +} diff --git a/app/src/scripts/lightweightCharts/chart/markers.ts b/app/src/scripts/lightweightCharts/chart/markers.ts new file mode 100644 index 000000000..923a5b642 --- /dev/null +++ b/app/src/scripts/lightweightCharts/chart/markers.ts @@ -0,0 +1,123 @@ +import { colors } from "/src/scripts/utils/colors"; +import { priceToUSLocale } from "/src/scripts/utils/locale"; +import { ONE_DAY_IN_MS } from "/src/scripts/utils/time"; + +import { chartState } from "./state"; +import { GENESIS_DAY } from "./whitespace"; + +export const setMinMaxMarkers = ({ + scale, + candlesticks, + range, + lowerOpacity, +}: { + scale: ResourceScale; + candlesticks: DatasetValue<CandlestickData | SingleValueData>[]; + range: TimeRange; + lowerOpacity: boolean; +}) => { + const first = candlesticks.at(0); + + if (!first) return; + + const offset = + scale === "date" + ? first.number - new Date(GENESIS_DAY).valueOf() / ONE_DAY_IN_MS + : 0; + + const slicedDataList = range + ? candlesticks.slice( + Math.ceil(range.from - offset < 0 ? 0 : range.from - offset), + Math.floor(range.to - offset) + 1, + ) + : []; + + const series = chartState.priceSeries; + + if (!series) return; + + if (slicedDataList.length) { + const markers: (SeriesMarker<Time> & Numbered)[] = []; + + const seriesIsCandlestick = series.seriesType() === "Candlestick"; + + [ + { + mathFunction: "min" as const, + placementAttribute: seriesIsCandlestick + ? ("low" as const) + : ("close" as const), + // valueAttribute: 'low' as const, + markerOptions: { + position: "belowBar" as const, + shape: "arrowUp" as const, + }, + }, + { + mathFunction: "max" as const, + placementAttribute: seriesIsCandlestick + ? ("high" as const) + : ("close" as const), + // valueAttribute: 'high' as const, + markerOptions: { + position: "aboveBar" as const, + shape: "arrowDown" as const, + }, + }, + ].map( + ({ + mathFunction, + placementAttribute, + // valueAttribute, + markerOptions, + }) => { + const value = Math[mathFunction]( + // ...slicedDataList.map((data) => data[valueAttribute] || 0), + ...slicedDataList.map( + (data) => + (placementAttribute in data + ? data[placementAttribute] + : data.value) || 0, + ), + ); + + const placement = Math[mathFunction]( + ...slicedDataList.map( + (data) => + (placementAttribute in data + ? data[placementAttribute] + : data.value) || 0, + ), + ); + + const candle = slicedDataList.find( + (data) => + (placementAttribute in data + ? data[placementAttribute] + : data.value) === placement, + ); + + return ( + candle && + markers.push({ + ...markerOptions, + // date: candle.date, + number: candle.number, + time: candle.time, + color: lowerOpacity ? colors.darkWhite : colors.white, + size: 0, + text: priceToUSLocale(value), + }) + ); + }, + ); + + series.setMarkers(sortWhitespaceDataArray(markers)); + } +}; + +function sortWhitespaceDataArray<T extends WhitespaceData & Numbered>( + array: T[], +) { + return array.sort(({ number: a }, { number: b }) => a - b); +} diff --git a/app/src/scripts/lightweightCharts/chart/price.ts b/app/src/scripts/lightweightCharts/chart/price.ts new file mode 100644 index 000000000..1e2c629d2 --- /dev/null +++ b/app/src/scripts/lightweightCharts/chart/price.ts @@ -0,0 +1,176 @@ +import { createRWS } from "/src/solid/rws"; + +import { colors } from "../../utils/colors"; +import { getNumberOfDaysBetweenTwoDates } from "../../utils/date"; +import { debounce } from "../../utils/debounce"; +import { webSockets } from "../../ws"; +import { createCandlesticksSeries } from "../series/creators/candlesticks"; +import { createSeriesLegend } from "../series/creators/legend"; +import { createLineSeries } from "../series/creators/line"; +import { setMinMaxMarkers } from "./markers"; +import { chartState } from "./state"; +import { initTimeScale } from "./time"; + +export const PRICE_SCALE_MOMENTUM_ID = "momentum"; + +export const applyPriceSeries = < + Scale extends ResourceScale, + T extends SingleValueData, +>({ + chart, + datasets, + preset, + dataset: _dataset, + options, + activeResources, +}: { + chart: IChartApi; + datasets: Datasets; + preset: Preset; + activeResources: Accessor<Set<ResourceDataset<any, any>>>; + dataset?: Dataset<Scale, T>; + options?: PriceSeriesOptions; +}) => { + const id = options?.id || "price"; + const title = options?.title || "Price"; + + const dataset = createMemo(() => _dataset || datasets[preset.scale].price); + + const url = "url" in dataset() ? (dataset() as any).url : undefined; + + const priceScaleOptions: DeepPartial<PriceScaleOptions> = { + ...(options?.halved + ? { + scaleMargins: { + top: 0.05, + bottom: 0.55, + }, + } + : {}), + ...(options?.id || options?.title + ? {} + : { + mode: 1, + // mode: PriceScaleMode.Logarithmic, + }), + ...options?.priceScaleOptions, + }; + + const seriesType = createRWS( + checkIfUpClose(chart, chartState.range) || "Candlestick", + ); + + const debouncedCallback = debounce((range: TimeRange | null) => { + try { + seriesType.set((previous) => checkIfUpClose(chart, range) || previous); + } catch {} + }, 50); + + chart?.timeScale().subscribeVisibleTimeRangeChange(debouncedCallback); + + onCleanup( + () => + chart === chartState.chart && + chartState.chart + ?.timeScale() + .unsubscribeVisibleTimeRangeChange(debouncedCallback), + ); + + const lowerOpacity = options?.lowerOpacity || options?.halved || false; + + if (options?.halved) { + options.seriesOptions = { + ...options.seriesOptions, + priceScaleId: "left", + }; + } + + const [ohlcSeries, ohlcColors] = createCandlesticksSeries(chart, { + ...options, + lowerOpacity, + }); + + const ohlcLegend = createSeriesLegend({ + id, + presetId: preset.id, + title, + color: () => ohlcColors, + series: ohlcSeries, + disabled: () => seriesType() !== "Candlestick", + url, + }); + + ohlcSeries.priceScale().applyOptions(priceScaleOptions); + + // --- + + const lineColor = lowerOpacity ? colors.darkWhite : colors.white; + + const lineSeries = createLineSeries(chart, { + color: lineColor, + ...options?.seriesOptions, + }); + + const lineLegend = createSeriesLegend({ + id, + presetId: preset.id, + title, + color: () => lineColor, + series: lineSeries, + disabled: () => seriesType() !== "Line", + visible: ohlcLegend.visible, + url, + }); + + lineSeries.priceScale().applyOptions(priceScaleOptions); + + // --- + + // setMinMaxMarkers({ + // scale: preset.scale, + // candlesticks: + // dataset?.values() || datasets[preset.scale].price.values() || ([] as any), + // range: chartState.range, + // lowerOpacity, + // }); + + initTimeScale({ + activeResources, + }); + + createEffect(() => { + const d = dataset(); + lineSeries.setData(d.values()); + ohlcSeries.setData(d.values()); + }); + + createEffect(() => { + if (preset.scale === "date") { + const latest = webSockets.liveKrakenCandle.latest(); + + if (latest) { + ohlcSeries.update(latest); + lineSeries.update(latest); + } + } + }); + + return { ohlcLegend, lineLegend }; +}; + +function checkIfUpClose(chart: IChartApi, range?: TimeRange | null) { + if (!range) return undefined; + + const from = new Date(range.from); + const to = new Date(range.to); + + const width = chart.timeScale().width(); + + const difference = getNumberOfDaysBetweenTwoDates(from, to); + + return width / difference >= 2.05 + ? "Candlestick" + : width / difference <= 1.95 + ? "Line" + : undefined; +} diff --git a/app/src/scripts/lightweightCharts/chart/render.ts b/app/src/scripts/lightweightCharts/chart/render.ts new file mode 100644 index 000000000..d4b36c83e --- /dev/null +++ b/app/src/scripts/lightweightCharts/chart/render.ts @@ -0,0 +1,40 @@ +import { createChart } from "./create"; +import { chartState } from "./state"; +import { setWhitespace } from "./whitespace"; + +export function renderChart({ + datasets, + legendSetter, + preset, + activeResources, +}: { + datasets: Datasets; + legendSetter: Setter<PresetLegend>; + preset: Preset; + activeResources: Accessor<Set<ResourceDataset<any, any>>>; +}) { + const scale = preset.scale; + + createChart(scale); + + const chart = chartState.chart; + + if (!chart) return; + + try { + setWhitespace(chart, scale); + + console.log(`preset: ${preset.id}`); + + const legend = preset.applyPreset({ + chart, + datasets, + preset, + activeResources, + }); + + legendSetter(legend); + } catch (error) { + console.error("chart: render: failed", error); + } +} diff --git a/app/src/scripts/lightweightCharts/chart/state.ts b/app/src/scripts/lightweightCharts/chart/state.ts new file mode 100644 index 000000000..7d97001c8 --- /dev/null +++ b/app/src/scripts/lightweightCharts/chart/state.ts @@ -0,0 +1,10 @@ +import { getInitialRange } from "./time"; + +export const LOCAL_STORAGE_RANGE_KEY = "chart-range"; +export const URL_PARAMS_RANGE_FROM_KEY = "from"; +export const URL_PARAMS_RANGE_TO_KEY = "to"; + +export const chartState = { + chart: null as IChartApi | null, + range: getInitialRange(), +}; diff --git a/app/src/scripts/lightweightCharts/chart/time.ts b/app/src/scripts/lightweightCharts/chart/time.ts new file mode 100644 index 000000000..a4a0ebbd6 --- /dev/null +++ b/app/src/scripts/lightweightCharts/chart/time.ts @@ -0,0 +1,110 @@ +import { HEIGHT_CHUNK_SIZE } from "../../datasets"; +import { debounce } from "../../utils/debounce"; +import { writeURLParam } from "../../utils/urlParams"; +import { setMinMaxMarkers } from "./markers"; +import { + chartState, + LOCAL_STORAGE_RANGE_KEY, + URL_PARAMS_RANGE_FROM_KEY, + URL_PARAMS_RANGE_TO_KEY, +} from "./state"; + +const debouncedUpdateURLParams = debounce((range: TimeRange | null) => { + if (!range) return; + + writeURLParam(URL_PARAMS_RANGE_FROM_KEY, String(range.from)); + + writeURLParam(URL_PARAMS_RANGE_TO_KEY, String(range.to)); + + localStorage.setItem(LOCAL_STORAGE_RANGE_KEY, JSON.stringify(range)); +}, 1000); + +export function initTimeScale({ + activeResources, +}: { + activeResources: Accessor<Set<ResourceDataset<any, any>>>; +}) { + setTimeScale(chartState.range); + + const debouncedFetch = debounce((range: TimeRange | null) => { + if (!range) return; + + let ids: number[] = []; + + if (typeof range.from === "string" && typeof range.to === "string") { + const from = new Date(range.from).getUTCFullYear(); + const to = new Date(range.to).getUTCFullYear(); + + ids = Array.from({ length: to - from + 1 }, (_, i) => i + from); + } else { + const from = Math.floor(Number(range.from) / HEIGHT_CHUNK_SIZE); + const to = Math.floor(Number(range.to) / HEIGHT_CHUNK_SIZE); + + const length = to - from + 1; + + ids = Array.from({ length }, (_, i) => (from + i) * HEIGHT_CHUNK_SIZE); + } + + ids.forEach((id) => { + activeResources().forEach((resource) => resource.fetch(id)); + }); + }, 100); + + debouncedFetch(chartState.range); + + let timeout = setTimeout(() => { + chartState.chart?.timeScale().subscribeVisibleTimeRangeChange((range) => { + debouncedFetch(range); + + debouncedUpdateURLParams(range); + + range = range || chartState.range; + + chartState.range = range; + }); + }, 50); + onCleanup(() => clearTimeout(timeout)); +} + +export function getInitialRange(): TimeRange { + const urlParams = new URLSearchParams(window.location.search); + + const urlFrom = urlParams.get(URL_PARAMS_RANGE_FROM_KEY); + const urlTo = urlParams.get(URL_PARAMS_RANGE_TO_KEY); + + if (urlFrom && urlTo) { + return { + from: urlFrom, + to: urlTo, + } satisfies TimeRange; + } + + const savedTimeRange = JSON.parse( + localStorage.getItem(LOCAL_STORAGE_RANGE_KEY) || "null", + ) as TimeRange | null; + + if (savedTimeRange) { + return savedTimeRange; + } + + const defaultTo = new Date(); + const defaultFrom = new Date(); + defaultFrom.setDate(defaultFrom.getUTCDate() - 6 * 30); + + const defaultTimeRange = { + from: defaultFrom.toJSON().split("T")[0], + to: defaultTo.toJSON().split("T")[0], + } satisfies TimeRange; + + return defaultTimeRange; +} + +export function setTimeScale(range: TimeRange | null) { + if (range) { + console.log(range); + + setTimeout(() => { + chartState.chart?.timeScale().setVisibleRange(range); + }, 1); + } +} diff --git a/app/src/scripts/lightweightCharts/chart/types.d.ts b/app/src/scripts/lightweightCharts/chart/types.d.ts new file mode 100644 index 000000000..950f640e7 --- /dev/null +++ b/app/src/scripts/lightweightCharts/chart/types.d.ts @@ -0,0 +1,9 @@ +interface PriceSeriesOptions { + halved?: boolean; + title?: string; + id?: string; + lowerOpacity?: boolean; + inverseColors?: boolean; + seriesOptions?: DeepPartial<SeriesOptionsCommon>; + priceScaleOptions?: DeepPartial<PriceScaleOptions>; +} diff --git a/app/src/scripts/lightweightCharts/chart/whitespace.ts b/app/src/scripts/lightweightCharts/chart/whitespace.ts new file mode 100644 index 000000000..e58995f26 --- /dev/null +++ b/app/src/scripts/lightweightCharts/chart/whitespace.ts @@ -0,0 +1,50 @@ +import { dateToString, getNumberOfDaysBetweenTwoDates } from "../../utils/date"; +import { ONE_DAY_IN_MS } from "../../utils/time"; +import { createLineSeries } from "../series/creators/line"; + +export const DAY_BEFORE_GENESIS_DAY = "2009-01-02"; +export const GENESIS_DAY = "2009-01-03"; +// export const DAY_BEFORE_WHITEPAPER_DAY = "2008-10-30"; +// export const WHITEPAPER_DAY = "2008-10-31"; + +const whitespaceStartDate = "1970-01-01"; +const whitespaceEndDate = "2100-01-01"; +const whitespaceDateDataset: (SingleValueData & Numbered)[] = new Array( + getNumberOfDaysBetweenTwoDates( + new Date(whitespaceStartDate), + new Date(whitespaceEndDate), + ), +) + .fill(0) + .map((_, index) => { + const date = new Date(whitespaceStartDate); + date.setUTCDate(date.getUTCDay() + index); + + return { + number: date.valueOf() / ONE_DAY_IN_MS, + time: dateToString(date), + value: NaN, + }; + }); + +const whitespaceHeightDataset: (WhitespaceData & Numbered)[] = new Array( + 840_000, +) + .fill(0) + .map( + (_, index) => + ({ + time: index, + number: index, + }) as any, + ); + +export function setWhitespace(chart: IChartApi, scale: ResourceScale) { + const whitespaceSeries = createLineSeries(chart); + + if (scale === "date") { + whitespaceSeries.setData(whitespaceDateDataset); + } else { + whitespaceSeries.setData(whitespaceHeightDataset); + } +} diff --git a/app/src/scripts/lightweightCharts/series/creators/area.ts b/app/src/scripts/lightweightCharts/series/creators/area.ts new file mode 100644 index 000000000..8aa2919c1 --- /dev/null +++ b/app/src/scripts/lightweightCharts/series/creators/area.ts @@ -0,0 +1,28 @@ +import { defaultSeriesOptions } from "./options"; + +type AreaOptions = DeepPartial<AreaStyleOptions & SeriesOptionsCommon>; + +export const createAreaSeries = ( + chart: IChartApi, + options?: AreaOptions & { + color?: string; + }, +) => { + const { color } = options || {}; + + // const fillColor = `${color}11`; + const fillColor = color; + + const seriesOptions: AreaOptions = { + // priceScaleId: 'left', + ...defaultSeriesOptions, + lineColor: color, + topColor: fillColor, + bottomColor: fillColor, + ...options, + }; + + const series = chart.addAreaSeries(seriesOptions); + + return series; +}; diff --git a/app/src/scripts/lightweightCharts/series/creators/baseLine.ts b/app/src/scripts/lightweightCharts/series/creators/baseLine.ts new file mode 100644 index 000000000..c8d4585fd --- /dev/null +++ b/app/src/scripts/lightweightCharts/series/creators/baseLine.ts @@ -0,0 +1,52 @@ +import { colors } from "/src/scripts/utils/colors"; + +import { defaultSeriesOptions } from "./options"; + +const DEFAULT_BASELINE_TOP_COLOR = colors.profit; +const DEFAULT_BASELINE_BOTTOM_COLOR = colors.loss; + +export const DEFAULT_BASELINE_COLORS = [ + DEFAULT_BASELINE_TOP_COLOR, + DEFAULT_BASELINE_BOTTOM_COLOR, +]; + +export const createBaseLineSeries = ( + chart: IChartApi, + options: BaselineSeriesOptions, +) => { + const { + title, + color, + topColor, + topLineColor, + bottomColor, + bottomLineColor, + base, + lineColor, + } = options; + + const allTopColor = topColor || color || DEFAULT_BASELINE_TOP_COLOR; + const topFillColor = `${allTopColor}`; + const allBottomColor = bottomColor || color || DEFAULT_BASELINE_BOTTOM_COLOR; + const bottomFillColor = `${allBottomColor}`; + + const seriesOptions: DeepPartialBaselineOptions = { + priceScaleId: "right", + ...defaultSeriesOptions, + lineWidth: 1, + ...options, + ...options.options, + ...(base ? { baseValue: { type: "price", price: base } } : {}), + topLineColor: topLineColor || lineColor || allTopColor, + topFillColor1: topFillColor, + topFillColor2: topFillColor, + bottomLineColor: bottomLineColor || lineColor || allBottomColor, + bottomFillColor1: bottomFillColor, + bottomFillColor2: bottomFillColor, + title, + }; + + const series = chart.addBaselineSeries(seriesOptions); + + return series; +}; diff --git a/app/src/scripts/lightweightCharts/series/creators/candlesticks.ts b/app/src/scripts/lightweightCharts/series/creators/candlesticks.ts new file mode 100644 index 000000000..808085e84 --- /dev/null +++ b/app/src/scripts/lightweightCharts/series/creators/candlesticks.ts @@ -0,0 +1,42 @@ +import { colors } from "/src/scripts/utils/colors"; + +export const createCandlesticksSeries = ( + chart: IChartApi, + options: PriceSeriesOptions, +): [ISeriesApi<"Candlestick">, string[]] => { + const { inverseColors, lowerOpacity } = options; + + const upColor = lowerOpacity + ? inverseColors + ? colors.darkLoss + : colors.darkProfit + : inverseColors + ? colors.loss + : colors.profit; + + const downColor = lowerOpacity + ? inverseColors + ? colors.darkProfit + : colors.darkLoss + : inverseColors + ? colors.profit + : colors.loss; + + const candlestickSeries = chart.addCandlestickSeries({ + baseLineVisible: false, + upColor, + wickUpColor: upColor, + downColor, + wickDownColor: downColor, + borderVisible: false, + priceLineVisible: false, + baseLineColor: "", + borderColor: "", + borderDownColor: "", + borderUpColor: "", + // lastValueVisible: false, + ...options.seriesOptions, + }); + + return [candlestickSeries, [upColor, downColor]]; +}; diff --git a/app/src/scripts/lightweightCharts/series/creators/histogram.ts b/app/src/scripts/lightweightCharts/series/creators/histogram.ts new file mode 100644 index 000000000..b7be67c3b --- /dev/null +++ b/app/src/scripts/lightweightCharts/series/creators/histogram.ts @@ -0,0 +1,30 @@ +import { PRICE_SCALE_MOMENTUM_ID } from "../../chart/price"; +import { defaultSeriesOptions } from "./options"; + +type HistogramOptions = DeepPartial< + HistogramStyleOptions & SeriesOptionsCommon +>; + +export const createHistogramSeries = ( + chart: IChartApi, + options?: HistogramOptions, +) => { + const seriesOptions: HistogramOptions = { + priceScaleId: "left", + ...defaultSeriesOptions, + ...options, + }; + + const series = chart.addHistogramSeries(seriesOptions); + + try { + chart.priceScale(PRICE_SCALE_MOMENTUM_ID).applyOptions({ + scaleMargins: { + top: 0.9, + bottom: 0, + }, + }); + } catch {} + + return series; +}; diff --git a/app/src/scripts/lightweightCharts/series/creators/legend.ts b/app/src/scripts/lightweightCharts/series/creators/legend.ts new file mode 100644 index 000000000..81017da56 --- /dev/null +++ b/app/src/scripts/lightweightCharts/series/creators/legend.ts @@ -0,0 +1,75 @@ +import { + readBooleanFromStorage, + saveToStorage, +} from "/src/scripts/utils/storage"; +import { + readBooleanURLParam, + writeURLParam, +} from "/src/scripts/utils/urlParams"; +import { createRWS } from "/src/solid/rws"; + +import { chartState } from "../../chart/state"; +import { setTimeScale } from "../../chart/time"; + +export function createSeriesLegend({ + id, + presetId, + title, + color, + series, + defaultVisible = true, + disabled: _disabled, + visible: _visible, + url, +}: { + id: string; + presetId: string; + title: string; + color: Accessor<string | string[]>; + series: ISeriesApi<SeriesType>; + defaultVisible?: boolean; + disabled?: Accessor<boolean>; + visible?: RWS<boolean>; + url?: string; +}) { + const storageID = `${presetId}-${id}`; + + const visible = + _visible || + createRWS( + readBooleanURLParam(id) ?? + readBooleanFromStorage(storageID) ?? + defaultVisible, + ); + + const disabled = createMemo(_disabled || (() => false)); + + createEffect(() => { + const v = visible(); + const d = disabled(); + + series.applyOptions({ + visible: !d && v, + }); + + setTimeScale(chartState.range); + + if (v !== defaultVisible) { + writeURLParam(id, v); + saveToStorage(storageID, v); + } else { + writeURLParam(id, undefined); + saveToStorage(storageID, undefined); + } + }); + + return { + id, + title, + series, + color, + visible, + disabled, + url, + }; +} diff --git a/app/src/scripts/lightweightCharts/series/creators/line.ts b/app/src/scripts/lightweightCharts/series/creators/line.ts new file mode 100644 index 000000000..90dd5cd08 --- /dev/null +++ b/app/src/scripts/lightweightCharts/series/creators/line.ts @@ -0,0 +1,10 @@ +import { defaultSeriesOptions } from "./options"; + +export const createLineSeries = ( + chart: IChartApi, + options?: DeepPartialLineOptions, +) => + chart.addLineSeries({ + ...defaultSeriesOptions, + ...options, + }); diff --git a/app/src/scripts/lightweightCharts/series/creators/options.ts b/app/src/scripts/lightweightCharts/series/creators/options.ts new file mode 100644 index 000000000..4577f62a1 --- /dev/null +++ b/app/src/scripts/lightweightCharts/series/creators/options.ts @@ -0,0 +1,7 @@ +export const defaultSeriesOptions: DeepPartial<SeriesOptionsCommon> = { + // @ts-ignore + lineWidth: 1.5, + priceLineVisible: false, + baseLineVisible: false, + baseLineColor: "", +}; diff --git a/app/src/scripts/lightweightCharts/series/creators/types.d.ts b/app/src/scripts/lightweightCharts/series/creators/types.d.ts new file mode 100644 index 000000000..298c89f77 --- /dev/null +++ b/app/src/scripts/lightweightCharts/series/creators/types.d.ts @@ -0,0 +1,13 @@ +interface BaselineSeriesOptions { + color?: string; + topColor?: string; + topLineColor?: string; + bottomColor?: string; + bottomLineColor?: string; + lineColor?: string; + base?: number; + options?: DeepPartialBaselineOptions; + title?: string; +} + +type SeriesLegend = ReturnType<typeof import("./legend").createSeriesLegend>; diff --git a/app/src/scripts/lightweightCharts/series/options/priceScale.ts b/app/src/scripts/lightweightCharts/series/options/priceScale.ts new file mode 100644 index 000000000..1a622999e --- /dev/null +++ b/app/src/scripts/lightweightCharts/series/options/priceScale.ts @@ -0,0 +1,45 @@ +export const resetRightPriceScale = ( + chart: IChartApi, + options?: FullPriceScaleOptions, +) => { + const finalOptions = { + ...options, + scaleMargins: { + ...(options?.halved + ? { + top: 0.5, + bottom: 0.05, + } + : { + top: 0.1, + bottom: 0.1, + }), + ...options?.scaleMargins, + }, + }; + + chart.priceScale("right").applyOptions(finalOptions); + + return finalOptions; +}; + +export const resetLeftPriceScale = ( + chart: IChartApi, + options?: FullPriceScaleOptions, +) => + chart.priceScale("left").applyOptions({ + visible: false, + ...options, + scaleMargins: { + ...(options?.halved + ? { + top: 0.475, + bottom: 0.025, + } + : { + top: 0.25, + bottom: 0.25, + }), + ...options?.scaleMargins, + }, + }); diff --git a/app/src/scripts/lightweightCharts/series/options/types.d.ts b/app/src/scripts/lightweightCharts/series/options/types.d.ts new file mode 100644 index 000000000..881c38737 --- /dev/null +++ b/app/src/scripts/lightweightCharts/series/options/types.d.ts @@ -0,0 +1,3 @@ +interface FullPriceScaleOptions extends DeepPartial<PriceScaleOptions> { + halved?: boolean; +} diff --git a/app/src/scripts/presets/addresses/index.ts b/app/src/scripts/presets/addresses/index.ts new file mode 100644 index 000000000..deba6096f --- /dev/null +++ b/app/src/scripts/presets/addresses/index.ts @@ -0,0 +1,218 @@ +import { + addressCohortsBySize, + addressCohortsByType, +} from "../../datasets/consts/address"; +import { liquidities } from "../../datasets/consts/liquidities"; +import { colors } from "../../utils/colors"; +import { createCohortPresetList } from "../templates/cohort"; +import { applyMultipleSeries, SeriesType } from "../templates/multiple"; + +export function createPresets({ + scale, + datasets, +}: { + scale: ResourceScale; + datasets: Datasets; +}): PartialPresetFolder { + return { + name: "Addresses", + tree: [ + { + scale, + name: `Total Non Empty Addresses`, + title: `Total Non Empty Address`, + description: "", + icon: IconTablerWallet, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: `Total Non Empty Address`, + color: colors.bitcoin, + seriesType: SeriesType.Area, + dataset: params.datasets[scale].address_count, + }, + ], + }); + }, + }, + { + scale, + name: `New Addresses`, + title: `New Addresses`, + description: "", + icon: IconTablerSparkles, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: `New Addresses`, + color: colors.white, + dataset: params.datasets[scale].created_addresses, + }, + ], + }); + }, + }, + { + scale, + name: `Total Addresses Created`, + title: `Total Addresses Created`, + description: "", + icon: IconTablerArchive, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: `Total Addresses Created`, + color: colors.bitcoin, + seriesType: SeriesType.Area, + dataset: params.datasets[scale].created_addresses, + }, + ], + }); + }, + }, + { + scale, + name: `Total Empty Addresses`, + title: `Total Empty Addresses`, + description: "", + icon: IconTablerTrash, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: `Total Empty Addresses`, + color: colors.darkWhite, + seriesType: SeriesType.Area, + dataset: params.datasets[scale].empty_addresses, + }, + ], + }); + }, + }, + { + name: "By Size", + tree: addressCohortsBySize.map(({ key, name }) => + createAddressPresetFolder({ + datasets, + scale, + color: colors[key], + name, + datasetKey: key, + }), + ), + }, + { + scale, + name: "By Type", + tree: addressCohortsByType.map(({ key, name }) => + createAddressPresetFolder({ + datasets, + scale, + color: colors[key], + name, + datasetKey: key, + }), + ), + }, + ], + } satisfies PartialPresetFolder; +} + +function createAddressPresetFolder<Scale extends ResourceScale>({ + datasets, + scale, + color, + name, + datasetKey, +}: { + datasets: Datasets; + scale: Scale; + name: string; + datasetKey: AddressCohortKey; + color: string; +}): PartialPresetFolder { + return { + name, + tree: [ + createAddressCountPreset({ scale, name, datasetKey, color }), + ...createCohortPresetList({ + title: name, + datasets, + scale, + name, + color, + datasetKey, + }), + { + name: `Split By Liquidity`, + tree: liquidities.map( + (liquidity): PartialPresetFolder => ({ + name: liquidity.name, + tree: createCohortPresetList({ + title: `${liquidity.name} ${name}`, + name: `${liquidity.name} ${name}`, + datasets, + scale, + color, + datasetKey: `${liquidity.key}_${datasetKey}`, + }), + }), + ), + }, + ], + }; +} + +export function createAddressCountPreset<Scale extends ResourceScale>({ + scale, + color, + name, + datasetKey, +}: { + scale: Scale; + name: string; + datasetKey: AddressCohortKey; + color: string; +}): PartialPreset { + return { + scale, + name: `Address Count`, + title: `${name} Address Count`, + icon: IconTablerAddressBook, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Address Count", + color, + dataset: params.datasets[scale][`${datasetKey}_address_count`], + }, + ], + }); + }, + description: "", + }; +} diff --git a/app/src/scripts/presets/blocks/index.ts b/app/src/scripts/presets/blocks/index.ts new file mode 100644 index 000000000..bdba20584 --- /dev/null +++ b/app/src/scripts/presets/blocks/index.ts @@ -0,0 +1,221 @@ +import { colors } from "../../utils/colors"; +import { applyMultipleSeries, SeriesType } from "../templates/multiple"; + +export function createPresets() { + const scale: ResourceScale = "date"; + + return { + name: "Blocks", + tree: [ + { + scale, + icon: IconTablerWall, + name: "Height", + title: "Block Height", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Height", + color: colors.bitcoin, + dataset: params.datasets.date.last_height, + }, + ], + }); + }, + }, + { + scale, + name: "Mined", + tree: [ + { + scale, + icon: IconTablerCube, + name: "Daily Sum", + title: "Daily Sum Of Blocks Mined", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Target", + color: colors.white, + dataset: params.datasets.date.blocks_mined_1d_target, + options: { + lineStyle: 3, + // lineStyle: LineStyle.LargeDashed, + }, + }, + { + title: "1W Avg.", + color: colors.momentumYellow, + dataset: params.datasets.date.blocks_mined_1w_sma, + defaultVisible: false, + }, + { + title: "1M Avg.", + color: colors.bitcoin, + dataset: params.datasets.date.blocks_mined_1m_sma, + }, + { + title: "Mined", + color: colors.darkBitcoin, + dataset: params.datasets.date.blocks_mined, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerLetterW, + name: "Weekly Sum", + title: "Weekly Sum Of Blocks Mined", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Target", + color: colors.white, + dataset: params.datasets.date.blocks_mined_1w_target, + options: { + lineStyle: 3, + // lineStyle: LineStyle.LargeDashed, + }, + }, + { + title: "Sum Mined", + color: colors.bitcoin, + dataset: params.datasets.date.blocks_mined_1w_sum, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerLetterM, + name: "Monthly Sum", + title: "Monthly Sum Of Blocks Mined", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Target", + color: colors.white, + dataset: params.datasets.date.blocks_mined_1m_target, + options: { + // lineStyle: LineStyle.LargeDashed, + lineStyle: 3, + }, + }, + { + title: "Sum Mined", + color: colors.bitcoin, + dataset: params.datasets.date.blocks_mined_1m_sum, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerLetterY, + name: "Yearly Sum", + title: "Yearly Sum Of Blocks Mined", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Target", + color: colors.white, + dataset: params.datasets.date.blocks_mined_1y_target, + options: { + lineStyle: 3, + // lineStyle: LineStyle.LargeDashed, + }, + }, + { + title: "Sum Mined", + color: colors.bitcoin, + dataset: params.datasets.date.blocks_mined_1y_sum, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerWall, + name: "Total", + title: "Total Blocks Mined", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Mined", + color: colors.bitcoin, + seriesType: SeriesType.Area, + dataset: params.datasets.date.total_blocks_mined, + }, + ], + }); + }, + }, + ], + }, + { + scale, + icon: IconTablerStack3, + name: "Cumulative Size", + title: "Cumulative Block Size", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Size (MB)", + color: colors.darkWhite, + seriesType: SeriesType.Area, + dataset: params.datasets.date.cumulative_block_size, + }, + ], + }); + }, + }, + ], + } satisfies PartialPresetFolder; +} diff --git a/app/src/scripts/presets/coinblocks/index.ts b/app/src/scripts/presets/coinblocks/index.ts new file mode 100644 index 000000000..ab3c89c01 --- /dev/null +++ b/app/src/scripts/presets/coinblocks/index.ts @@ -0,0 +1,1032 @@ +import { colors } from "../../utils/colors"; +import { applyMultipleSeries, SeriesType } from "../templates/multiple"; + +export function createPresets<Scale extends ResourceScale>({ + scale, + datasets: _datasets, +}: { + scale: Scale; + datasets: Datasets; +}) { + const datasets = _datasets[scale]; + + return { + name: "Cointime Economics", + tree: [ + { + name: "Prices", + tree: [ + { + scale, + icon: IconTablerArrowsCross, + name: "All", + title: "All Cointime Prices", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + list: [ + { + title: "Vaulted Price", + color: colors.vaultedness, + dataset: datasets.vaulted_price, + }, + { + title: "Active Price", + color: colors.liveliness, + dataset: datasets.active_price, + }, + { + title: "True Market Mean", + color: colors.trueMarketMeanPrice, + dataset: datasets.true_market_mean, + }, + { + title: "Realized Price", + color: colors.bitcoin, + dataset: datasets.realized_price, + }, + { + title: "Cointime", + color: colors.cointimePrice, + dataset: datasets.cointime_price, + }, + ], + }); + }, + }, + { + name: "Active", + tree: [ + { + scale, + icon: IconTablerHeartBolt, + name: "Price", + title: "Active Price", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + list: [ + { + title: "Active Price", + color: colors.liveliness, + dataset: datasets.active_price, + }, + ], + }); + }, + }, + ], + }, + { + name: "Vaulted", + tree: [ + { + scale, + icon: IconTablerBuildingBank, + name: "Price", + title: "Vaulted Price", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + list: [ + { + title: "Vaulted Price", + color: colors.vaultedness, + dataset: datasets.vaulted_price, + }, + ], + }); + }, + }, + ], + }, + { + name: "True Market Mean", + tree: [ + { + scale, + icon: IconTablerStackMiddle, + name: "Price", + title: "True Market Mean", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + list: [ + { + title: "True Market Mean", + color: colors.trueMarketMeanPrice, + dataset: datasets.true_market_mean, + }, + ], + }); + }, + }, + ], + }, + { + name: "Cointime Price", + tree: [ + { + scale, + icon: IconTablerStackMiddle, + name: "Price", + title: "Cointime Price", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + list: [ + { + title: "Cointime", + color: colors.cointimePrice, + dataset: datasets.cointime_price, + }, + ], + }); + }, + }, + ], + }, + ], + }, + { + name: "Capitalizations", + tree: [ + { + scale, + icon: IconTablerArrowsCross, + name: "All", + title: "Cointime Capitalizations", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + mode: 1, + }, + list: [ + { + title: "Market Cap", + + color: colors.white, + dataset: datasets.market_cap, + }, + { + title: "Realized Cap", + color: colors.realizedCap, + dataset: datasets.realized_cap, + }, + { + title: "Investor Cap", + color: colors.investorCap, + dataset: datasets.investor_cap, + }, + { + title: "Thermo Cap", + color: colors.thermoCap, + dataset: datasets.thermo_cap, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerPick, + name: "Thermo Cap", + title: "Thermo Cap", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + mode: 1, + }, + list: [ + { + title: "Thermo Cap", + color: colors.thermoCap, + dataset: datasets.thermo_cap, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerTie, + name: "Investor Cap", + title: "Investor Cap", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + mode: 1, + }, + list: [ + { + title: "Investor Cap", + color: colors.investorCap, + dataset: datasets.investor_cap, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerDivide, + name: "Thermo Cap To Investor Cap Ratio", + title: "Thermo Cap To Investor Cap Ratio (%)", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Ratio", + color: colors.bitcoin, + dataset: datasets.thermo_cap_to_investor_cap_ratio, + }, + ], + }); + }, + }, + ], + }, + { + name: "Coinblocks", + tree: [ + { + scale, + icon: IconTablerArrowsCross, + name: "All", + title: "All Coinblocks", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Coinblocks Created", + color: colors.coinblocksCreated, + dataset: datasets.coinblocks_created, + }, + { + title: "Coinblocks Destroyed", + color: colors.coinblocksDestroyed, + dataset: datasets.coinblocks_destroyed, + }, + { + title: "Coinblocks Stored", + color: colors.coinblocksStored, + dataset: datasets.coinblocks_stored, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerCube, + name: "Created", + title: "Coinblocks Created", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Coinblocks Created", + color: colors.coinblocksCreated, + dataset: datasets.coinblocks_created, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerFileShredder, + name: "Destroyed", + title: "Coinblocks Destroyed", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Coinblocks Destroyed", + color: colors.coinblocksDestroyed, + dataset: datasets.coinblocks_destroyed, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerBuildingWarehouse, + name: "Stored", + title: "Coinblocks Stored", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Coinblocks Stored", + color: colors.coinblocksStored, + dataset: datasets.coinblocks_stored, + }, + ], + }); + }, + }, + ], + }, + { + name: "Cumulative Coinblocks", + tree: [ + { + scale, + icon: IconTablerArrowsCross, + name: "All", + title: "All Cumulative Coinblocks", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Cumulative Coinblocks Created", + color: colors.coinblocksCreated, + dataset: datasets.cumulative_coinblocks_created, + }, + { + title: "Cumulative Coinblocks Destroyed", + color: colors.coinblocksDestroyed, + dataset: datasets.cumulative_coinblocks_destroyed, + }, + { + title: "Cumulative Coinblocks Stored", + color: colors.coinblocksStored, + dataset: datasets.cumulative_coinblocks_stored, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerCube, + name: "Created", + title: "Cumulative Coinblocks Created", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Cumulative Coinblocks Created", + color: colors.coinblocksCreated, + dataset: datasets.cumulative_coinblocks_created, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerFileShredder, + name: "Destroyed", + title: "Cumulative Coinblocks Destroyed", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Cumulative Coinblocks Destroyed", + color: colors.coinblocksDestroyed, + dataset: datasets.cumulative_coinblocks_destroyed, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerBuildingWarehouse, + name: "Stored", + title: "Cumulative Coinblocks Stored", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Cumulative Coinblocks Stored", + color: colors.coinblocksStored, + dataset: datasets.cumulative_coinblocks_stored, + }, + ], + }); + }, + }, + ], + }, + { + name: "Liveliness & Vaultedness", + tree: [ + { + scale, + icon: IconTablerHeartBolt, + name: "Liveliness - Activity", + title: "Liveliness (Activity)", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Liveliness", + color: colors.liveliness, + dataset: datasets.liveliness, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerBuildingBank, + name: "Vaultedness", + title: "Vaultedness", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Vaultedness", + color: colors.vaultedness, + dataset: datasets.vaultedness, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerArrowsCross, + name: "Versus", + title: "Liveliness V. Vaultedness", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Liveliness", + color: colors.liveliness, + dataset: datasets.liveliness, + }, + { + title: "Vaultedness", + color: colors.vaultedness, + dataset: datasets.vaultedness, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerDivide, + name: "Activity To Vaultedness Ratio", + title: "Activity To Vaultedness Ratio", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Activity To Vaultedness Ratio", + color: colors.activityToVaultednessRatio, + dataset: datasets.activity_to_vaultedness_ratio, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerHeartBolt, + name: "Concurrent Liveliness - Supply Adjusted Coindays Destroyed", + title: "Concurrent Liveliness - Supply Adjusted Coindays Destroyed", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Concurrent Liveliness 14d Median", + color: `${colors.liveliness}66`, + dataset: datasets.concurrent_liveliness_2w_median, + }, + { + title: "Concurrent Liveliness", + color: colors.liveliness, + dataset: datasets.concurrent_liveliness, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerStairs, + name: "Liveliness Incremental Change", + title: "Liveliness Incremental Change", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Liveliness Incremental Change", + color: colors.darkLiveliness, + seriesType: SeriesType.Based, + dataset: datasets.liveliness_net_change, + }, + { + title: "Liveliness Incremental Change 14 Day Median", + color: colors.liveliness, + seriesType: SeriesType.Based, + dataset: datasets.liveliness_net_change_2w_median, + }, + ], + }); + }, + }, + ], + }, + { + name: "Supply", + tree: [ + { + scale, + icon: IconTablerBuildingBank, + name: "Vaulted", + title: "Vaulted Supply", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Vaulted Supply", + color: colors.vaultedness, + dataset: datasets.vaulted_supply, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerHeartBolt, + name: "Active", + title: "Active Supply", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Active Supply", + color: colors.liveliness, + dataset: datasets.active_supply, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerArrowsCross, + name: "Vaulted V. Active", + title: "Vaulted V. Active", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Circulating Supply", + color: colors.coinblocksCreated, + dataset: datasets.supply, + }, + { + title: "Vaulted Supply", + color: colors.vaultedness, + dataset: datasets.vaulted_supply, + }, + { + title: "Active Supply", + color: colors.liveliness, + dataset: datasets.active_supply, + }, + ], + }); + }, + }, + // TODO: Fix, Bad data + // { + // id: 'asymptomatic-supply-regions', + // icon: IconTablerDirections, + // name: 'Asymptomatic Supply Regions', + // title: 'Asymptomatic Supply Regions', + // description: '', + // applyPreset(params) { + // return applyMultipleSeries({ + // ...params, + // priceScaleOptions: { + // halved: true, + // }, + // list: [ + // { + // id: 'min-vaulted', + // title: 'Min Vaulted Supply', + // color: colors.vaultedness, + // dataset: params.datasets.dateToMinVaultedSupply, + // }, + // { + // id: 'max-active', + // title: 'Max Active Supply', + // color: colors.liveliness, + // dataset: params.datasets.dateToMaxActiveSupply, + // }, + // ], + // }) + // }, + // }, + { + scale, + icon: IconTablerBuildingBank, + name: "Vaulted Net Change", + title: "Vaulted Supply Net Change", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Vaulted Supply Net Change", + color: colors.vaultedness, + dataset: datasets.vaulted_supply, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerHeartBolt, + name: "Active Net Change", + title: "Active Supply Net Change", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Active Supply Net Change", + color: colors.liveliness, + dataset: datasets.active_supply_net_change, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerSwords, + name: "Active VS. Vaulted 90D Net Change", + title: "Active VS. Vaulted 90 Day Supply Net Change", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Active Supply Net Change", + color: `${colors.liveliness}80`, + dataset: datasets.active_supply_3m_net_change, + seriesType: SeriesType.Based, + }, + { + title: "Vaulted Supply Net Change", + color: `${colors.vaultedPrice}80`, + seriesType: SeriesType.Based, + dataset: datasets.vaulted_supply_3m_net_change, + }, + ], + }); + }, + }, + // TODO: Fix, Bad data + // { + // id: 'vaulted-supply-annualized-net-change', + // icon: IconTablerBuildingBank, + // name: 'Vaulted Annualized Net Change', + // title: 'Vaulted Supply Annualized Net Change', + // description: '', + // applyPreset(params) { + // return applyMultipleSeries({ + // ...params, + // priceScaleOptions: { + // halved: true, + // }, + // list: [ + // { + // id: 'vaulted-annualized-supply-net-change', + // title: 'Vaulted Supply Annualized Net Change', + // color: colors.vaultedness, + // dataset: + // datasets.vaultedAnnualizedSupplyNetChange, + // }, + // ], + // }) + // }, + // }, + + // TODO: Fix, Bad data + // { + // id: 'vaulting-rate', + // icon: IconTablerBuildingBank, + // name: 'Vaulting Rate', + // title: 'Vaulting Rate', + // description: '', + // applyPreset(params) { + // return applyMultipleSeries({ + // ...params, + // priceScaleOptions: { + // halved: true, + // }, + // list: [ + // { + // id: 'vaulting-rate', + // title: 'Vaulting Rate', + // color: colors.vaultedness, + // dataset: datasets.vaultingRate, + // }, + // { + // id: 'nominal-inflation-rate', + // title: 'Nominal Inflation Rate', + // color: colors.orange, + // dataset: params.datasets.dateToYearlyInflationRate, + // }, + // ], + // }) + // }, + // }, + + // TODO: Fix, Bad data + // { + // id: 'active-supply-net-change-decomposition', + // icon: IconTablerArrowsCross, + // name: 'Active Supply Net Change Decomposition (90D)', + // title: 'Active Supply Net 90 Day Change Decomposition', + // description: '', + // applyPreset(params) { + // return applyMultipleSeries({ + // ...params, + // priceScaleOptions: { + // halved: true, + // }, + // list: [ + // { + // id: 'issuance-change', + // title: 'Change From Issuance', + // color: colors.emerald, + // dataset: + // params.datasets + // [scale].activeSupplyChangeFromIssuance90dChange, + // }, + // { + // id: 'transactions-change', + // title: 'Change From Transactions', + // color: colors.rose, + // dataset: + // params.datasets + // [scale].activeSupplyChangeFromTransactions90dChange, + // }, + // // { + // // id: 'active', + // // title: 'Active Supply', + // // color: colors.liveliness, + // // dataset: datasets.activeSupply, + // // }, + // ], + // }) + // }, + // }, + + { + scale, + icon: IconTablerTrendingUp, + name: "In Profit", + title: "Cointime Supply In Profit", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Circulating Supply", + color: colors.coinblocksCreated, + dataset: datasets.supply, + }, + { + title: "Vaulted Supply", + color: colors.vaultedness, + dataset: datasets.vaulted_supply, + }, + { + title: "Supply in profit", + color: colors.bitcoin, + dataset: datasets.supply_in_profit, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerTrendingDown, + name: "In Loss", + title: "Cointime Supply In Loss", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Circulating Supply", + color: colors.coinblocksCreated, + dataset: datasets.supply, + }, + { + title: "Active Supply", + color: colors.liveliness, + dataset: datasets.active_supply, + }, + { + title: "Supply in Loss", + color: colors.bitcoin, + dataset: datasets.supply_in_loss, + }, + ], + }); + }, + }, + ], + }, + { + scale, + icon: IconTablerBuildingFactory, + name: "Cointime Yearly Inflation Rate", + title: "Cointime-Adjusted Yearly Inflation Rate (%)", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + mode: 1, + }, + list: [ + { + title: "Cointime Adjusted", + color: colors.coinblocksCreated, + dataset: datasets.cointime_adjusted_yearly_inflation_rate, + }, + { + title: "Nominal", + color: colors.bitcoin, + dataset: datasets.yearly_inflation_rate, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerWind, + name: "Cointime Velocity", + title: "Cointime-Adjusted Transactions Velocity", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + mode: 1, + }, + list: [ + { + title: "Cointime Adjusted", + color: colors.coinblocksCreated, + dataset: datasets.cointime_adjusted_velocity, + }, + { + title: "Nominal", + color: colors.bitcoin, + dataset: datasets.transaction_velocity, + }, + ], + }); + }, + }, + ], + } satisfies PartialPresetFolder; +} diff --git a/app/src/scripts/presets/hodlers/index.ts b/app/src/scripts/presets/hodlers/index.ts new file mode 100644 index 000000000..833f89736 --- /dev/null +++ b/app/src/scripts/presets/hodlers/index.ts @@ -0,0 +1,127 @@ +import { + fromXCohorts, + fromXToYCohorts, + upToCohorts, + xthCohorts, + yearCohorts, +} from "../../datasets/consts/age"; +import { colors } from "../../utils/colors"; +import { createCohortPresetFolder } from "../templates/cohort"; +import { applyMultipleSeries } from "../templates/multiple"; + +export function createPresets({ + scale, + datasets, +}: { + scale: ResourceScale; + datasets: Datasets; +}) { + return { + name: "Hodlers", + tree: [ + { + scale, + name: `Hodl Supply`, + title: `Hodl Supply`, + description: "", + icon: IconTablerRipple, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: `24h`, + color: colors.up_to_1d, + dataset: + params.datasets.date + .up_to_1d_supply_to_circulating_supply_ratio, + }, + + ...fromXToYCohorts.map(({ key, name, legend }) => ({ + title: legend, + color: colors[key], + dataset: + params.datasets.date[ + `${key}_supply_to_circulating_supply_ratio` + ], + })), + + { + title: `15y+`, + color: colors.from_15y, + dataset: + params.datasets.date + .from_15y_supply_to_circulating_supply_ratio, + }, + ], + }); + }, + }, + ...xthCohorts.map(({ key, name, legend }) => + createCohortPresetFolder({ + datasets, + scale, + color: colors[key], + name: legend, + datasetKey: key, + title: name, + }), + ), + { + name: "Up To X", + tree: upToCohorts.map(({ key, name }) => + createCohortPresetFolder({ + datasets, + scale, + color: colors[key], + name, + datasetKey: key, + title: name, + }), + ), + }, + { + name: "From X To Y", + tree: fromXToYCohorts.map(({ key, name }) => + createCohortPresetFolder({ + datasets, + scale, + color: colors[key], + name, + datasetKey: key, + title: name, + }), + ), + }, + { + name: "From X", + tree: fromXCohorts.map(({ key, name }) => + createCohortPresetFolder({ + datasets, + scale, + color: colors[key], + name, + datasetKey: key, + title: name, + }), + ), + }, + { + name: "Years", + tree: yearCohorts.map(({ key, name }) => + createCohortPresetFolder({ + datasets, + scale, + color: colors[key], + name, + datasetKey: key, + title: name, + }), + ), + }, + ], + } satisfies PartialPresetFolder; +} diff --git a/app/src/scripts/presets/index.ts b/app/src/scripts/presets/index.ts new file mode 100644 index 000000000..c9af34be7 --- /dev/null +++ b/app/src/scripts/presets/index.ts @@ -0,0 +1,296 @@ +import { createRWS } from "/src/solid/rws"; + +import { colors } from "../utils/colors"; +import { replaceHistory } from "../utils/history"; +import { stringToId } from "../utils/id"; +import { resetURLParams } from "../utils/urlParams"; +import { createPresets as createAddressesPresets } from "./addresses"; +import { createPresets as createBlocksPresets } from "./blocks"; +import { createPresets as createCoinblocksPresets } from "./coinblocks"; +import { createPresets as createHodlersPresets } from "./hodlers"; +import { createPresets as createMarketPresets } from "./market"; +import { createPresets as createMinersPresets } from "./miners"; +import { createCohortPresetList } from "./templates/cohort"; +import { createPresets as createTransactionsPresets } from "./transactions"; + +export const LOCAL_STORAGE_FAVORITES_KEY = "favorites"; +export const LOCAL_STORAGE_FOLDERS_KEY = "folders"; +export const LOCAL_STORAGE_HISTORY_KEY = "history"; +export const LOCAL_STORAGE_SELECTED_KEY = "preset"; +export const LOCAL_STORAGE_VISITED_KEY = "visited"; + +export function createPresets(datasets: Datasets): Presets { + const partialTree = [ + { + name: "Dashboards (Coming soon)", + tree: [], + }, + { + name: "Charts", + tree: [ + { + name: "By Date", + tree: [ + createMarketPresets({ scale: "date", datasets }), + createBlocksPresets(), + createMinersPresets("date"), + createTransactionsPresets("date"), + ...createCohortPresetList({ + datasets, + scale: "date", + color: colors.bitcoin, + datasetKey: "", + name: "", + title: "", + }), + createHodlersPresets({ scale: "date", datasets }), + createAddressesPresets({ scale: "date", datasets }), + createCoinblocksPresets({ scale: "date", datasets }), + ], + } satisfies PartialPresetFolder, + { + name: "By Height (Coming soon)", + tree: [ + // createMarketPresets({ scale: "height", datasets }), + // createMinersPresets("height"), + // createTransactionsPresets("height"), + // ...createCohortPresetList({ + // datasets, + // scale: "height", + // color: colors.bitcoin, + // name: "", + // datasetKey: "", + // title: "", + // }), + // createHodlersPresets({ scale: "height", datasets }), + // createAddressesPresets({ scale: "height", datasets }), + // createCoinblocksPresets({ scale: "height", datasets }), + ], + } satisfies PartialPresetFolder, + ], + }, + ]; + + const { list, ids, tree } = flatten(partialTree); + + checkIfDuplicateIds(ids); + + setIsFavorites(list); + + setVisited(list); + + const favorites = createMemo(() => + list.filter((preset) => preset.isFavorite()), + ); + + createEffect(() => { + localStorage.setItem( + LOCAL_STORAGE_FAVORITES_KEY, + JSON.stringify(favorites().map((p) => p.id)), + ); + }); + + const visited = createMemo(() => list.filter((preset) => preset.visited())); + + createEffect(() => { + localStorage.setItem( + LOCAL_STORAGE_VISITED_KEY, + JSON.stringify(visited().map((p) => p.id)), + ); + }); + + createEffect(() => { + const serializedHistory: SerializedPresetsHistory = history().map( + ({ preset, date }) => ({ + p: preset.id, + d: date.valueOf(), + }), + ); + + localStorage.setItem( + LOCAL_STORAGE_HISTORY_KEY, + JSON.stringify(serializedHistory), + ); + }); + + const history: PresetsHistorySignal = createRWS(getHistory(list), { + equals: false, + }); + + const selected = createRWS(findInitialPreset(list), { + equals: false, + }); + + createEffect((previousPreset: Preset) => { + if (previousPreset && previousPreset !== selected()) { + resetURLParams(); + } + return selected(); + }, selected()); + + createEffect(() => selected().visited.set(true)); + + const select = (preset: Preset) => { + if (selected().id === preset.id) { + return; + } + + history.set((l) => { + l.unshift({ + date: new Date(), + preset, + }); + return l; + }); + + _select(preset, selected.set); + }; + + const openedFolders = createRWS( + new Set( + JSON.parse( + localStorage.getItem(LOCAL_STORAGE_FOLDERS_KEY) || "[]", + ) as string[], + ), + { + equals: false, + }, + ); + + createEffect(() => { + localStorage.setItem( + LOCAL_STORAGE_FOLDERS_KEY, + JSON.stringify(Array.from(openedFolders())), + ); + }); + + return { + tree, + list, + selected, + favorites, + history, + select, + openedFolders, + }; +} + +function _select(preset: Preset, set: Setter<Preset>) { + const key = LOCAL_STORAGE_SELECTED_KEY; + const value = preset.id; + + localStorage.setItem(key, value); + + replaceHistory({ pathname: `/${value}` }); + + set(preset); +} + +function flatten(partialTree: PartialPresetTree) { + const result: { list: Preset[]; ids: string[] } = { list: [], ids: [] }; + + const _flatten = (partialTree: PartialPresetTree, path?: FilePath) => { + partialTree.forEach((anyPreset) => { + if ("tree" in anyPreset) { + const id = stringToId( + `${(path || [])?.map(({ name }) => name).join(" ")} ${anyPreset.name} folder`, + ); + + const presetFolder: PresetFolder = { + ...anyPreset, + tree: anyPreset.tree as PresetTree, + id, + }; + + Object.assign(anyPreset, presetFolder); + + result.ids.push(presetFolder.id); + + return _flatten(presetFolder.tree, [ + ...(path || []), + { + name: presetFolder.name, + id: presetFolder.id, + }, + ]); + } else { + const preset = { + ...anyPreset, + path: path || [], + isFavorite: createRWS(false), + visited: createRWS(false), + id: `${anyPreset.scale}-to-${stringToId(anyPreset.title)}`, + } satisfies Preset; + + result.list.push(Object.assign(anyPreset, preset)); + result.ids.push(preset.id); + } + }); + }; + + _flatten(partialTree); + + return { ...result, tree: partialTree as PresetTree }; +} + +function checkIfDuplicateIds(ids: string[]) { + if (ids.length !== new Set(ids).size) { + const m = new Map<string, number>(); + + ids.forEach((id) => { + m.set(id, (m.get(id) || 0) + 1); + }); + + console.log( + [...m.entries()].filter(([_, value]) => value > 1).map(([key, _]) => key), + ); + + throw Error("ID duplicate"); + } +} + +function findInitialPreset(presets: Preset[]): Preset { + const urlPreset = document.location.pathname.substring(1); + + return ( + (urlPreset && + (presets.find((preset) => preset.id === urlPreset) || + presets.find( + (preset) => + preset.id === localStorage.getItem(LOCAL_STORAGE_SELECTED_KEY), + ))) || + presets[0] + ); +} + +function setIsFavorites(list: Preset[]) { + ( + JSON.parse( + localStorage.getItem(LOCAL_STORAGE_FAVORITES_KEY) || "[]", + ) as string[] + ).forEach((id) => { + list.find((preset) => preset.id === id)?.isFavorite.set(true); + }); +} + +function setVisited(list: Preset[]) { + ( + JSON.parse( + localStorage.getItem(LOCAL_STORAGE_VISITED_KEY) || "[]", + ) as string[] + ).forEach((id) => { + list.find((preset) => preset.id === id)?.visited.set(true); + }); +} + +function getHistory(list: Preset[]): PresetsHistory { + return ( + JSON.parse( + localStorage.getItem(LOCAL_STORAGE_HISTORY_KEY) || "[]", + ) as SerializedPresetsHistory + ).flatMap(({ p, d }) => { + const preset = list.find((preset) => preset.id === p); + + return preset ? [{ preset, date: new Date(d) }] : []; + }); +} diff --git a/app/src/scripts/presets/market/averages/index.ts b/app/src/scripts/presets/market/averages/index.ts new file mode 100644 index 000000000..14111f348 --- /dev/null +++ b/app/src/scripts/presets/market/averages/index.ts @@ -0,0 +1,78 @@ +import { averages } from "/src/scripts/datasets/date"; +import { colors } from "/src/scripts/utils/colors"; + +import { applyMultipleSeries } from "../../templates/multiple"; + +export function createPresets(datasets: Datasets): PartialPresetFolder { + const scale: ResourceScale = "date"; + + return { + name: "Averages", + tree: [ + { + scale, + icon: IconTablerMathAvg, + name: "All", + title: "All Averages", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + list: averages.map((average) => ({ + title: average.key.toUpperCase(), + color: colors[`_${average.key}`], + dataset: params.datasets.date[`price_${average.key}_sma`], + })), + }); + }, + description: "", + }, + ...averages.map(({ name, key }) => + createPresetFolder({ + datasets, + scale, + color: colors[`_${key}`], + name, + key, + }), + ), + ], + }; +} + +function createPresetFolder({ + scale, + datasets, + color, + name, + key, +}: { + datasets: Datasets; + scale: ResourceScale; + color: string; + name: string; + key: AverageName; +}) { + return { + // id, + // name, + // tree: [ + // { + scale, + name, + description: "", + icon: IconTablerMathAvg, + title: `${name} Moving Average`, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + list: [ + { + title: `SMA`, + color, + dataset: datasets.date[`price_${key}_sma`], + }, + ], + }); + }, + } satisfies PartialPreset; +} diff --git a/app/src/scripts/presets/market/index.ts b/app/src/scripts/presets/market/index.ts new file mode 100644 index 000000000..0803c2826 --- /dev/null +++ b/app/src/scripts/presets/market/index.ts @@ -0,0 +1,77 @@ +import { colors } from "../../utils/colors"; +import { applyMultipleSeries } from "../templates/multiple"; +import { createPresets as createAveragesPresets } from "./averages"; +import { createPresets as createIndicatorsPresets } from "./indicators"; +import { createPresets as createReturnsPresets } from "./returns"; + +export function createPresets({ + scale, + datasets, +}: { + scale: ResourceScale; + datasets: Datasets; +}) { + return { + name: "Market", + tree: [ + { + scale, + icon: IconTablerCurrencyDollar, + name: "Price", + title: "Market Price", + applyPreset(params) { + return applyMultipleSeries({ ...params }); + }, + description: "", + }, + { + scale, + icon: IconTablerPercentage, + name: "Performance", + title: "Market Performance", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceOptions: { + id: "performance", + title: "Performance", + priceScaleOptions: { + mode: 2, + }, + }, + }); + }, + description: "", + }, + { + scale, + icon: IconTablerInfinity, + name: "Capitalization", + title: "Market Capitalization", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Market Cap.", + dataset: params.datasets[scale].market_cap, + color: colors.bitcoin, + }, + ], + }); + }, + description: "", + }, + ...(scale === "date" + ? ([ + createAveragesPresets(datasets), + createReturnsPresets(datasets), + createIndicatorsPresets(datasets), + ] satisfies PartialPresetTree) + : []), + ], + } satisfies PartialPresetFolder; +} diff --git a/app/src/scripts/presets/market/indicators/index.ts b/app/src/scripts/presets/market/indicators/index.ts new file mode 100644 index 000000000..3fc892f49 --- /dev/null +++ b/app/src/scripts/presets/market/indicators/index.ts @@ -0,0 +1,6 @@ +export function createPresets(datasets: Datasets) { + return { + name: "Indicators", + tree: [], + } satisfies PartialPresetFolder; +} diff --git a/app/src/scripts/presets/market/returns/index.ts b/app/src/scripts/presets/market/returns/index.ts new file mode 100644 index 000000000..6610a7bf5 --- /dev/null +++ b/app/src/scripts/presets/market/returns/index.ts @@ -0,0 +1,79 @@ +import { + compoundReturns, + totalReturns, +} from "/src/scripts/datasets/consts/returns"; + +import { applyMultipleSeries, SeriesType } from "../../templates/multiple"; + +export function createPresets(datasets: Datasets) { + return { + name: "Returns", + tree: [ + { + name: "Total", + tree: [ + ...totalReturns.map(({ name, key }) => + createPreset({ + scale: "date", + datasets, + name, + title: `${name} Total`, + key: `${key}_total`, + }), + ), + ], + }, + { + name: "Compound", + tree: [ + ...compoundReturns.map(({ name, key }) => + createPreset({ + scale: "date", + datasets, + name, + title: `${name} Compound`, + key: `${key}_compound`, + }), + ), + ], + }, + ], + } satisfies PartialPresetFolder; +} + +function createPreset({ + scale, + datasets, + name, + title, + key, +}: { + scale: ResourceScale; + datasets: Datasets; + name: string; + title: string; + key: `${TotalReturnKey}_total` | `${CompoundReturnKey}_compound`; +}): PartialPreset { + return { + scale, + name, + description: "", + icon: IconTablerReceiptTax, + title: `${title} Return`, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: `Return (%)`, + seriesType: SeriesType.Based, + dataset: datasets.date[`price_${key}_return`], + }, + ], + }); + }, + }; +} diff --git a/app/src/scripts/presets/miners/index.ts b/app/src/scripts/presets/miners/index.ts new file mode 100644 index 000000000..2fc83eaf1 --- /dev/null +++ b/app/src/scripts/presets/miners/index.ts @@ -0,0 +1,902 @@ +import { colors } from "../../utils/colors"; +import { applyMultipleSeries, SeriesType } from "../templates/multiple"; + +export function createPresets(scale: ResourceScale) { + return { + name: "Miners", + tree: [ + { + name: "Coinbases", + tree: [ + ...(scale === "date" + ? ([ + { + name: "Last", + tree: [ + { + scale, + icon: IconTablerCoinBitcoin, + name: "In Bitcoin", + title: "Last Coinbase (In Bitcoin)", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Last", + color: colors.bitcoin, + dataset: params.datasets[scale].last_coinbase, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerCoin, + name: "In Dollars", + title: "Last Coinbase (In Dollars)", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Last", + color: colors.dollars, + dataset: + params.datasets[scale].last_coinbase_in_dollars, + }, + ], + }); + }, + }, + ], + }, + { + scale, + name: "Daily Sum", + tree: [ + { + scale, + icon: IconTablerMoneybag, + name: "In Bitcoin", + title: "Daily Sum Of Bitcoin Coinbases", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Coinbases (Bitcoin)", + color: colors.bitcoin, + dataset: params.datasets[scale].coinbase, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerCash, + name: "In Dollars", + title: "Daily Sum Of Dollar Coinbases", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Coinbases (Dollars)", + color: colors.dollars, + dataset: + params.datasets[scale].coinbase_in_dollars, + }, + ], + }); + }, + }, + ], + }, + { + scale, + name: "Yearly Sum", + tree: [ + { + scale, + icon: IconTablerMoneybag, + name: "In Bitcoin", + title: "Yearly Sum Of Bitcoin Coinbases", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Coinbases (Bitcoin)", + color: colors.bitcoin, + dataset: params.datasets[scale].coinbase_1y_sum, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerCash, + name: "In Dollars", + title: "Yearly Sum Of Dollar Coinbases", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Coinbases (Dollars)", + color: colors.dollars, + dataset: + params.datasets[scale] + .coinbase_in_dollars_1y_sum, + }, + ], + }); + }, + }, + ], + }, + { + scale, + name: "Cumulative", + tree: [ + { + scale, + icon: IconTablerMoneybag, + name: "In Bitcoin", + title: "Cumulative Bitcoin Coinbases", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Coinbases (Bitcoin)", + color: colors.bitcoin, + dataset: + params.datasets[scale].cumulative_coinbase, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerCash, + name: "In Dollars", + title: "Cumulative Dollar Coinbases", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Coinbases (Dollars)", + color: colors.dollars, + dataset: + params.datasets[scale] + .cumulative_coinbase_in_dollars, + }, + ], + }); + }, + }, + ], + }, + ] satisfies PartialPresetTree) + : []), + ], + }, + + { + name: "Subsidies", + tree: [ + ...(scale === "date" + ? ([ + { + name: "Last", + tree: [ + { + scale, + icon: IconTablerCoinBitcoin, + name: "In Bitcoin", + title: "Last Subsidy (In Bitcoin)", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Last", + color: colors.bitcoin, + dataset: params.datasets[scale].last_subsidy, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerCoin, + name: "In Dollars", + title: "Last Subsidy (In Dollars)", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Last", + color: colors.dollars, + dataset: + params.datasets[scale].last_subsidy_in_dollars, + }, + ], + }); + }, + }, + ], + }, + { + scale, + name: "Daily Sum", + tree: [ + { + scale, + icon: IconTablerMoneybag, + name: "In Bitcoin", + title: "Daily Sum Of Bitcoin Subsidies", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Subsidies (Bitcoin)", + color: colors.bitcoin, + dataset: params.datasets[scale].subsidy, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerCash, + name: "In Dollars", + title: "Daily Sum Of Dollar Subsidies", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Subsidies (Dollars)", + color: colors.dollars, + dataset: + params.datasets[scale].subsidy_in_dollars, + }, + ], + }); + }, + }, + ], + }, + { + scale, + name: "Yearly Sum", + tree: [ + { + scale, + icon: IconTablerMoneybag, + name: "In Bitcoin", + title: "Yearly Sum Of Bitcoin Subsidies", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Subsidies (Bitcoin)", + color: colors.bitcoin, + dataset: params.datasets[scale].subsidy_1y_sum, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerCash, + name: "In Dollars", + title: "Yearly Sum Of Dollar Subsidies", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Subsidies (Dollars)", + color: colors.dollars, + dataset: + params.datasets[scale] + .subsidy_in_dollars_1y_sum, + }, + ], + }); + }, + }, + ], + }, + { + scale, + name: "Cumulative", + tree: [ + { + scale, + icon: IconTablerMoneybag, + name: "In Bitcoin", + title: "Cumulative Bitcoin Subsidies", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Subsidies (Bitcoin)", + color: colors.bitcoin, + dataset: + params.datasets[scale].cumulative_subsidy, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerCash, + name: "In Dollars", + title: "Cumulative Dollar Subsidies", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Subsidies (Dollars)", + color: colors.dollars, + dataset: + params.datasets[scale] + .cumulative_subsidy_in_dollars, + }, + ], + }); + }, + }, + ], + }, + ] satisfies PartialPresetTree) + : []), + ], + }, + + { + name: "Fees", + tree: [ + ...(scale === "date" + ? ([ + { + name: "Last", + tree: [ + { + scale, + icon: IconTablerCoinBitcoin, + name: "In Bitcoin", + title: "Last Fees (In Bitcoin)", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Last", + color: colors.bitcoin, + dataset: params.datasets[scale].last_fees, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerCoin, + name: "In Dollars", + title: "Last Fees (In Dollars)", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Last", + color: colors.dollars, + dataset: + params.datasets[scale].last_fees_in_dollars, + }, + ], + }); + }, + }, + ], + }, + { + scale, + name: "Daily Sum", + tree: [ + { + scale, + icon: IconTablerMoneybag, + name: "In Bitcoin", + title: "Daily Sum Of Bitcoin Fees", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Fees (Bitcoin)", + color: colors.bitcoin, + dataset: params.datasets[scale].fees, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerCash, + name: "In Dollars", + title: "Daily Sum Of Dollar Fees", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Fees (Dollars)", + color: colors.dollars, + dataset: params.datasets[scale].fees_in_dollars, + }, + ], + }); + }, + }, + ], + }, + { + scale, + name: "Yearly Sum", + tree: [ + { + scale, + icon: IconTablerMoneybag, + name: "In Bitcoin", + title: "Yearly Sum Of Bitcoin Fees", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Fees (Bitcoin)", + color: colors.bitcoin, + dataset: params.datasets[scale].fees_1y_sum, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerCash, + name: "In Dollars", + title: "Yearly Sum Of Dollar Fees", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Fees (Dollars)", + color: colors.dollars, + dataset: + params.datasets[scale].fees_in_dollars_1y_sum, + }, + ], + }); + }, + }, + ], + }, + { + scale, + name: "Cumulative", + tree: [ + { + scale, + icon: IconTablerMoneybag, + name: "In Bitcoin", + title: "Cumulative Bitcoin Fees", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Fees (Bitcoin)", + color: colors.bitcoin, + dataset: params.datasets[scale].cumulative_fees, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerCash, + name: "In Dollars", + title: "Cumulative Dollar Fees", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Fees (Dollars)", + color: colors.dollars, + dataset: + params.datasets[scale] + .cumulative_fees_in_dollars, + }, + ], + }); + }, + }, + ], + }, + ] satisfies PartialPresetTree) + : []), + ], + }, + + { + scale, + icon: IconTablerSwords, + name: "Subsidy V. Fees", + title: "Subsidy V. Fees", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Subsidy (%)", + color: colors.bitcoin, + dataset: params.datasets[scale].subsidy_to_coinbase_ratio, + }, + { + title: "Fees (%)", + color: colors.darkBitcoin, + dataset: params.datasets[scale].fees_to_coinbase_ratio, + }, + ], + }); + }, + }, + + ...(scale === "date" + ? ([ + { + scale, + icon: IconTablerCalculator, + name: "Puell Multiple", + title: "Puell Multiple", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + mode: 1, + }, + list: [ + { + title: "Multiple", + color: colors.bitcoin, + dataset: params.datasets.date.puell_multiple, + }, + ], + }); + }, + }, + + { + scale, + icon: IconTablerPick, + name: "Hash Rate", + title: "Hash Rate (EH/s)", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + mode: 1, + }, + list: [ + { + title: "1M SMA", + color: colors.momentumYellow, + dataset: params.datasets.date.hash_rate_1m_sma, + }, + { + title: "1W SMA", + color: colors.bitcoin, + dataset: params.datasets.date.hash_rate_1w_sma, + }, + { + title: "Rate", + color: colors.darkBitcoin, + dataset: params.datasets.date.hash_rate, + }, + ], + }); + }, + }, + + { + scale, + icon: IconTablerRibbonHealth, + name: "Hash Ribbon", + title: "Hash Ribbon (EH/s)", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + mode: 1, + }, + list: [ + { + title: "1M SMA", + color: colors.profit, + dataset: params.datasets.date.hash_rate_1m_sma, + }, + { + title: "2M SMA", + color: colors.loss, + dataset: params.datasets.date.hash_rate_2m_sma, + }, + ], + }); + }, + }, + + { + scale, + icon: IconTablerTag, + name: "Hash Price", + title: "Hash Price", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Price ($/PH/s)", + color: colors.dollars, + dataset: params.datasets.date.hash_price, + }, + ], + }); + }, + }, + ] satisfies PartialPreset[]) + : []), + + { + scale, + icon: IconTablerWeight, + name: "Difficulty", + title: "Difficulty", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + mode: 1, + }, + list: [ + { + title: "Difficulty", + color: colors.bitcoin, + dataset: params.datasets[scale].difficulty, + }, + ], + }); + }, + }, + + ...(scale === "date" + ? ([ + { + scale, + icon: IconTablerAdjustments, + name: "Difficulty Adjustment", + title: "Difficulty Adjustment", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Adjustment (%)", + // color: colors.bitcoin, + seriesType: SeriesType.Based, + dataset: params.datasets[scale].difficulty_adjustment, + }, + ], + }); + }, + }, + ] satisfies PartialPreset[]) + : []), + + { + scale, + icon: IconTablerBuildingFactory, + name: "Annualized Issuance", + title: "Annualized Issuance", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + mode: 1, + }, + list: [ + { + title: "Issuance", + color: colors.bitcoin, + dataset: params.datasets[scale].annualized_issuance, + }, + ], + }); + }, + }, + + { + scale, + icon: IconTablerBuildingFactory2, + name: "Yearly Inflation Rate", + title: "Yearly Inflation Rate", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + mode: 1, + }, + list: [ + { + title: "Rate (%)", + color: colors.bitcoin, + dataset: params.datasets[scale].yearly_inflation_rate, + }, + ], + }); + }, + }, + + // For scale === "height" + // block_size, + // block_weight, + // block_vbytes, + // block_interval, + ], + } satisfies PartialPresetFolder; +} diff --git a/app/src/scripts/presets/templates/cohort.ts b/app/src/scripts/presets/templates/cohort.ts new file mode 100644 index 000000000..76d102c11 --- /dev/null +++ b/app/src/scripts/presets/templates/cohort.ts @@ -0,0 +1,985 @@ +import { percentiles } from "../../datasets/consts/percentiles"; +import { colors } from "../../utils/colors"; +import { applyMultipleSeries, SeriesType } from "./multiple"; + +export function createCohortPresetFolder<Scale extends ResourceScale>({ + datasets, + scale, + color, + name, + datasetKey, + title, +}: { + datasets: Datasets; + scale: Scale; + name: string; + datasetKey: AnyPossibleCohortKey; + color: string; + title: string; +}) { + return { + name, + tree: createCohortPresetList({ + title, + datasets, + name, + scale, + color, + datasetKey, + }), + } satisfies PartialPresetFolder; +} + +export function createCohortPresetList<Scale extends ResourceScale>({ + name, + datasets, + scale, + color, + datasetKey, + title, +}: { + name: string; + datasets: Datasets; + scale: Scale; + datasetKey: AnyPossibleCohortKey; + title: string; + color: string; +}) { + const datasetPrefix = datasetKey + ? (`${datasetKey}_` as const) + : ("" as const); + + return [ + { + name: "UTXOs", + tree: [ + { + scale, + name: `Count`, + title: `${title} Unspent Transaction Outputs Count`, + icon: () => IconTablerTicket, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Count", + color, + seriesType: SeriesType.Area, + dataset: params.datasets[scale][`${datasetPrefix}utxo_count`], + }, + ], + }); + }, + description: "", + }, + ], + }, + { + name: "Realized", + tree: [ + { + scale, + name: `Price`, + title: `${title} Realized Price`, + description: "", + icon: () => IconTablerTag, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + list: [ + { + title: "Realized Price", + color, + dataset: + params.datasets[scale][`${datasetPrefix}realized_price`], + }, + ], + }); + }, + }, + { + scale, + name: `Capitalization`, + title: `${title} Realized Capitalization`, + icon: () => IconTablerPigMoney, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: `${name} Realized Cap.`, + color, + seriesType: SeriesType.Area, + dataset: + params.datasets[scale][`${datasetPrefix}realized_cap`], + }, + ...(datasetKey + ? [ + { + title: "Realized Cap.", + color: colors.bitcoin, + dataset: params.datasets[scale].realized_cap, + defaultVisible: false, + }, + ] + : []), + ], + }); + }, + description: "", + }, + { + scale, + name: `Capitalization 1M Net Change`, + title: `${title} Realized Capitalization 1 Month Net Change`, + icon: () => IconTablerStatusChange, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: `Net Change`, + seriesType: SeriesType.Based, + dataset: + params.datasets[scale][ + `${datasetPrefix}realized_cap_1m_net_change` + ], + }, + ], + }); + }, + description: "", + }, + { + scale, + name: `Profit`, + title: `${title} Realized Profit`, + icon: () => IconTablerCash, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Realized Profit", + dataset: + params.datasets[scale][`${datasetPrefix}realized_profit`], + color: colors.profit, + seriesType: SeriesType.Area, + }, + ], + }); + }, + description: "", + }, + { + scale, + name: "Loss", + title: `${title} Realized Loss`, + icon: () => IconTablerCoffin, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Realized Loss", + dataset: + params.datasets[scale][`${datasetPrefix}realized_loss`], + color: colors.loss, + seriesType: SeriesType.Area, + }, + ], + }); + }, + description: "", + }, + { + scale, + name: `PNL`, + title: `${title} Realized Profit And Loss`, + icon: () => IconTablerArrowsVertical, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Profit", + color: colors.profit, + dataset: + params.datasets[scale][`${datasetPrefix}realized_profit`], + seriesType: SeriesType.Based, + }, + { + title: "Loss", + color: colors.loss, + dataset: + params.datasets[scale][ + `${datasetPrefix}negative_realized_loss` + ], + seriesType: SeriesType.Based, + }, + ], + }); + }, + description: "", + }, + { + scale, + name: `Net PNL`, + title: `${title} Net Realized Profit And Loss`, + icon: () => IconTablerScale, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Net PNL", + seriesType: SeriesType.Based, + dataset: + params.datasets[scale][ + `${datasetPrefix}net_realized_profit_and_loss` + ], + }, + ], + }); + }, + description: "", + }, + { + scale, + name: `Net PNL Relative To Market Cap`, + title: `${title} Net Realized Profit And Loss Relative To Market Capitalization`, + icon: () => IconTablerDivide, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Net", + seriesType: SeriesType.Based, + dataset: + params.datasets[scale][ + `${datasetPrefix}net_realized_profit_and_loss_to_market_cap_ratio` + ], + }, + ], + }); + }, + description: "", + }, + { + scale, + name: `Cumulative Profit`, + title: `${title} Cumulative Realized Profit`, + icon: () => IconTablerSum, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Cumulative Realized Profit", + color: colors.profit, + seriesType: SeriesType.Area, + dataset: + params.datasets[scale][ + `${datasetPrefix}cumulative_realized_profit` + ], + }, + ], + }); + }, + description: "", + }, + { + scale, + name: "Cumulative Loss", + title: `${title} Cumulative Realized Loss`, + icon: () => IconTablerSum, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Cumulative Realized Loss", + color: colors.loss, + seriesType: SeriesType.Area, + dataset: + params.datasets[scale][ + `${datasetPrefix}cumulative_realized_loss` + ], + }, + ], + }); + }, + description: "", + }, + { + scale, + name: `Cumulative Net PNL`, + title: `${title} Cumulative Net Realized Profit And Loss`, + icon: () => IconTablerSum, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Cumulative Net Realized PNL", + seriesType: SeriesType.Based, + dataset: + params.datasets[scale][ + `${datasetPrefix}cumulative_net_realized_profit_and_loss` + ], + }, + ], + }); + }, + description: "", + }, + { + scale, + name: `Cumulative Net PNL 30 Day Change`, + title: `${title} Cumulative Net Realized Profit And Loss 30 Day Change`, + icon: () => IconTablerTimeDuration30, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Cumulative Net Realized PNL 30d Change", + dataset: + params.datasets[scale][ + `${datasetPrefix}cumulative_net_realized_profit_and_loss_1m_net_change` + ], + seriesType: SeriesType.Based, + }, + ], + }); + }, + description: "", + }, + ], + }, + { + name: "Unrealized", + tree: [ + { + scale, + name: `Profit`, + title: `${title} Unrealized Profit`, + icon: () => IconTablerMoodDollar, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Profit", + dataset: + params.datasets[scale][`${datasetPrefix}unrealized_profit`], + color: colors.profit, + seriesType: SeriesType.Area, + }, + ], + }); + }, + description: "", + }, + + { + scale, + name: "Loss", + title: `${title} Unrealized Loss`, + icon: () => IconTablerMoodSadDizzy, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Loss", + dataset: + params.datasets[scale][`${datasetPrefix}unrealized_loss`], + color: colors.loss, + seriesType: SeriesType.Area, + }, + ], + }); + }, + description: "", + }, + { + scale, + name: `PNL`, + title: `${title} Unrealized Profit And Loss`, + icon: () => IconTablerArrowsVertical, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Profit", + color: colors.profit, + dataset: + params.datasets[scale][`${datasetPrefix}unrealized_profit`], + seriesType: SeriesType.Based, + }, + { + title: "Loss", + color: colors.loss, + dataset: + params.datasets[scale][ + `${datasetPrefix}negative_unrealized_loss` + ], + seriesType: SeriesType.Based, + }, + ], + }); + }, + description: "", + }, + { + scale, + name: `Net PNL`, + title: `${title} Net Unrealized Profit And Loss`, + icon: () => IconTablerScale, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Net Unrealized PNL", + dataset: + params.datasets[scale][ + `${datasetPrefix}net_unrealized_profit_and_loss` + ], + seriesType: SeriesType.Based, + }, + ], + }); + }, + description: "", + }, + { + scale, + name: `Net PNL Relative To Market Cap`, + title: `${title} Net Unrealized Profit And Loss Relative To Total Market Capitalization`, + icon: () => IconTablerDivide, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Relative Net Unrealized PNL", + dataset: + params.datasets[scale][ + `${datasetPrefix}net_unrealized_profit_and_loss_to_market_cap_ratio` + ], + seriesType: SeriesType.Based, + }, + ], + }); + }, + description: "", + }, + ], + }, + { + name: "Supply", + tree: [ + { + name: "Absolute", + tree: [ + { + scale, + name: "All", + title: `${title} Profit And Loss`, + icon: () => IconTablerArrowsCross, + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "In Profit", + color: colors.profit, + dataset: + params.datasets[scale][ + `${datasetPrefix}supply_in_profit` + ], + }, + { + title: "In Loss", + color: colors.loss, + dataset: + params.datasets[scale][ + `${datasetPrefix}supply_in_loss` + ], + }, + { + title: "Total", + color: colors.white, + dataset: params.datasets[scale][`${datasetPrefix}supply`], + }, + { + title: "Halved Total", + color: colors.gray, + dataset: + params.datasets[scale][`${datasetPrefix}halved_supply`], + options: { + lineStyle: 4, + }, + }, + ], + }); + }, + }, + { + scale, + name: `Total`, + title: `${title} Total supply`, + icon: () => IconTablerSum, + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Supply", + color, + seriesType: SeriesType.Area, + dataset: params.datasets[scale][`${datasetPrefix}supply`], + }, + ], + }); + }, + }, + { + scale, + name: "In Profit", + title: `${title} Supply In Profit`, + icon: () => IconTablerTrendingUp, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Supply", + color: colors.profit, + seriesType: SeriesType.Area, + dataset: + params.datasets[scale][ + `${datasetPrefix}supply_in_profit` + ], + }, + ], + }); + }, + description: "", + }, + { + scale, + name: "In Loss", + title: `${title} Supply In Loss`, + icon: () => IconTablerTrendingDown, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Supply", + color: colors.loss, + seriesType: SeriesType.Area, + dataset: + params.datasets[scale][ + `${datasetPrefix}supply_in_loss` + ], + }, + ], + }); + }, + description: "", + }, + ], + }, + { + name: "Relative To Circulating", + tree: [ + { + scale, + name: "All", + title: `${title} Profit And Loss Relative To Circulating Supply`, + icon: () => IconTablerArrowsCross, + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "In Profit", + color: colors.profit, + dataset: + params.datasets[scale][ + `${datasetPrefix}supply_in_profit_to_circulating_supply_ratio` + ], + }, + { + title: "In Loss", + color: colors.loss, + dataset: + params.datasets[scale][ + `${datasetPrefix}supply_in_loss_to_circulating_supply_ratio` + ], + }, + { + title: "100%", + color: colors.white, + dataset: + params.datasets[scale][ + `${datasetPrefix}supply_to_circulating_supply_ratio` + ], + }, + { + title: "50%", + color: colors.gray, + dataset: + params.datasets[scale][ + `${datasetPrefix}halved_supply_to_circulating_supply_ratio` + ], + options: { + lineStyle: 4, + }, + }, + ], + }); + }, + }, + { + scale, + name: `Total`, + title: `${title} Total supply Relative To Circulating Supply`, + icon: () => IconTablerSum, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Supply", + color, + seriesType: SeriesType.Area, + dataset: + params.datasets[scale][ + `${datasetPrefix}supply_to_circulating_supply_ratio` + ], + }, + ], + }); + }, + description: "", + }, + { + scale, + name: "In Profit", + title: `${title} Supply In Profit Relative To Circulating Supply`, + icon: () => IconTablerTrendingUp, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Supply", + color: colors.profit, + seriesType: SeriesType.Area, + dataset: + params.datasets[scale][ + `${datasetPrefix}supply_in_profit_to_circulating_supply_ratio` + ], + }, + ], + }); + }, + description: "", + }, + { + scale, + name: "In Loss", + title: `${title} Supply In Loss Relative To Circulating Supply`, + icon: () => IconTablerTrendingDown, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Supply", + seriesType: SeriesType.Area, + color: colors.loss, + dataset: + params.datasets[scale][ + `${datasetPrefix}supply_in_loss_to_circulating_supply_ratio` + ], + }, + ], + }); + }, + description: "", + }, + ], + }, + { + name: "Relative To Own", + tree: [ + { + scale, + name: "All", + title: `${title} Supply In Profit And Loss Relative To Own Supply`, + icon: () => IconTablerArrowsCross, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "In profit", + dataset: + params.datasets[scale][ + `${datasetPrefix}supply_in_profit_to_own_supply_ratio` + ], + color: colors.profit, + }, + { + title: "In loss", + color: colors.loss, + dataset: + params.datasets[scale][ + `${datasetPrefix}supply_in_loss_to_own_supply_ratio` + ], + }, + { + title: "100%", + color: colors.white, + dataset: params.datasets[scale][100], + options: { + lastValueVisible: false, + }, + }, + { + title: "50%", + color: colors.gray, + dataset: params.datasets[scale][50], + options: { + lineStyle: 4, + lastValueVisible: false, + }, + }, + ], + }); + }, + description: "", + }, + { + scale, + name: "In Profit", + title: `${title} Supply In Profit Relative To Own Supply`, + icon: () => IconTablerTrendingUp, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Supply", + color: colors.profit, + seriesType: SeriesType.Area, + dataset: + params.datasets[scale][ + `${datasetPrefix}supply_in_profit_to_own_supply_ratio` + ], + }, + ], + }); + }, + description: "", + }, + { + scale, + name: "In Loss", + title: `${title} Supply In Loss Relative To Own Supply`, + icon: () => IconTablerTrendingDown, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Supply", + seriesType: SeriesType.Area, + color: colors.loss, + dataset: + params.datasets[scale][ + `${datasetPrefix}supply_in_loss_to_own_supply_ratio` + ], + }, + ], + }); + }, + description: "", + }, + ], + }, + // createMomentumPresetFolder({ + // datasets: datasets[scale], + // scale, + // id: `${scale}-${id}-supply-in-profit-and-loss-percentage-self`, + // title: `${title} Supply In Profit And Loss (% Self)`, + // datasetKey: `${datasetKey}SupplyPNL%Self`, + // }), + ], + }, + { + name: "Prices Paid", + tree: [ + { + scale, + name: `Average`, + title: `${title} Average Price Paid - Realized Price`, + icon: () => IconTablerMathAvg, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + list: [ + { + title: "Average", + color, + dataset: + params.datasets[scale][`${datasetPrefix}realized_price`], + }, + ], + }); + }, + description: "", + }, + { + scale, + name: `Deciles`, + title: `${title} deciles`, + icon: () => IconTablerSquareHalf, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + list: percentiles + .filter(({ value }) => Number(value) % 10 === 0) + .map(({ name, key }) => ({ + dataset: params.datasets[scale][`${datasetPrefix}${key}`], + color, + title: name, + })), + }); + }, + description: "", + }, + ...percentiles.map( + (percentile): PartialPreset => ({ + scale, + name: percentile.name, + title: `${title} ${percentile.title}`, + icon: () => IconTablerSquareHalf, + applyPreset(params) { + return applyMultipleSeries({ + ...params, + list: [ + { + title: percentile.name, + color, + dataset: + params.datasets[scale][ + `${datasetPrefix}${percentile.key}` + ], + }, + ], + }); + }, + description: "", + }), + ), + ], + }, + ] satisfies PartialPresetTree; +} diff --git a/app/src/scripts/presets/templates/momentum.ts b/app/src/scripts/presets/templates/momentum.ts new file mode 100644 index 000000000..2331f5f34 --- /dev/null +++ b/app/src/scripts/presets/templates/momentum.ts @@ -0,0 +1,121 @@ +// import { PriceScaleMode } from "lightweight-charts"; + +// import { +// applyMultipleSeries, +// colors, +// PRICE_SCALE_MOMENTUM_ID, +// SeriesType, +// } from "/src/scripts"; + +// // type HeightMomentumKey = +// // | `${AnyPossibleCohortKey}SupplyPNL%Self` +// // | `${AnyPossibleCohortKey}RealizedPriceRatio` +// // | "activePriceRatio" +// // | "vaultedPriceRatio" +// // | "trueMarketMeanRatio"; + +// // type DateMomentumKey = HeightMomentumKey | `price${AverageName}MARatio`; + +// export function createMomentumPresetFolder< +// Scale extends ResourceScale, +// Key extends string, +// >({ +// datasets, +// scale, +// id, +// title, +// datasetKey, +// }: { +// datasets: Record<`${Key}${MomentumKey}`, Dataset<ResourceScale>>; +// scale: Scale; +// id: string; +// title: string; +// datasetKey: Key; +// }): PartialPresetFolder { +// return { +// id: `${scale}-${id}-momentum`, +// name: "Momentum", +// tree: [ +// { +// id: `${scale}-${id}-momentum-value`, +// name: "Value", +// title: `${title} Momentum`, +// icon: () => IconTablerRollercoaster, +// applyPreset(params) { +// return applyMultipleSeries({ +// scale, +// ...params, +// list: [ +// { +// title: "Momentum", +// colors: colors.momentum, +// seriesType: SeriesType.Histogram, +// dataset: datasets[`${datasetKey}Momentum`], +// options: { +// priceScaleId: PRICE_SCALE_MOMENTUM_ID, +// lastValueVisible: false, +// }, +// }, +// ], +// }); +// }, +// description: "", +// }, +// { +// id: `${scale}-${id}-momentum-buy-low-sell-high`, +// name: "BLSH - Buy Low Sell High", +// tree: [ +// { +// id: `${scale}-${id}-buy-low-sell-high-bitcoin-returns`, +// name: "Bitcoin Returns", +// title: `${title} Momentum Based Buy Low Sell High Bitcoin Returns`, +// icon: () => IconTablerReceiptBitcoin, +// applyPreset(params) { +// return applyMultipleSeries({ +// scale, +// ...params, +// priceScaleOptions: { +// halved: true, +// mode: PriceScaleMode.Percentage, +// }, +// list: [ +// { +// title: "Bitcoin Returns", +// dataset: +// datasets[`${datasetKey}MomentumBLSHBitcoinReturns`], +// color: colors.bitcoin, +// }, +// ], +// }); +// }, +// description: "", +// }, +// { +// id: `${scale}-${id}-momentum-buy-low-sell-high-dollar-returns`, +// name: "Dollar Returns", +// title: `${title} Momentum Based Buy Low Sell High Dollar Returns`, +// icon: () => IconTablerReceiptDollar, +// applyPreset(params) { +// return applyMultipleSeries({ +// scale, +// ...params, +// priceScaleOptions: { +// halved: true, +// mode: PriceScaleMode.Percentage, +// }, +// list: [ +// { +// title: "Dollar Returns", +// dataset: datasets[`${datasetKey}MomentumBLSHDollarReturns`], +// color: colors.dollars, +// }, +// ], +// }); +// }, +// description: "", +// }, +// ], +// }, +// ], +// }; +// } diff --git a/app/src/scripts/presets/templates/multiple.ts b/app/src/scripts/presets/templates/multiple.ts new file mode 100644 index 000000000..d5d7cb023 --- /dev/null +++ b/app/src/scripts/presets/templates/multiple.ts @@ -0,0 +1,183 @@ +import { applyPriceSeries } from "../../lightweightCharts/chart/price"; +import { chartState } from "../../lightweightCharts/chart/state"; +import { setTimeScale } from "../../lightweightCharts/chart/time"; +import { createAreaSeries } from "../../lightweightCharts/series/creators/area"; +import { + createBaseLineSeries, + DEFAULT_BASELINE_COLORS, +} from "../../lightweightCharts/series/creators/baseLine"; +import { createHistogramSeries } from "../../lightweightCharts/series/creators/histogram"; +import { createSeriesLegend } from "../../lightweightCharts/series/creators/legend"; +import { createLineSeries } from "../../lightweightCharts/series/creators/line"; +import { resetRightPriceScale } from "../../lightweightCharts/series/options/priceScale"; +import { stringToId } from "../../utils/id"; + +export enum SeriesType { + Normal, + Based, + Area, + Histogram, +} + +export function applyMultipleSeries< + Scale extends ResourceScale, + DS extends Dataset<Scale> & Partial<ResourceDataset<Scale>>, +>({ + chart, + list = [], + preset, + priceScaleOptions, + datasets, + priceDataset, + priceOptions, + activeResources, +}: { + chart: IChartApi; + preset: Preset; + priceDataset?: DS; + priceOptions?: PriceSeriesOptions; + priceScaleOptions?: FullPriceScaleOptions; + list?: ( + | { + dataset: DS; + color?: string; + colors?: undefined; + seriesType: SeriesType.Based; + title: string; + options?: BaselineSeriesOptions; + defaultVisible?: boolean; + } + | { + dataset: DS; + color?: string; + colors?: string[]; + seriesType: SeriesType.Histogram; + title: string; + options?: DeepPartialHistogramOptions; + defaultVisible?: boolean; + } + | { + dataset: DS; + color: string; + colors?: undefined; + seriesType?: SeriesType.Normal | SeriesType.Area; + title: string; + options?: DeepPartialLineOptions; + defaultVisible?: boolean; + } + )[]; + datasets: Datasets; + activeResources: Accessor<Set<ResourceDataset<any, any>>>; +}): PresetLegend { + const { halved } = priceScaleOptions || {}; + + const price = applyPriceSeries({ + chart, + datasets, + preset, + dataset: priceDataset, + activeResources, + options: { + ...priceOptions, + halved, + }, + }); + + const legendList: PresetLegend = [price.lineLegend, price.ohlcLegend]; + + const isAnyArea = list.find( + (config) => config.seriesType === SeriesType.Area, + ); + + const rightPriceScaleOptions = resetRightPriceScale(chart, { + ...priceScaleOptions, + ...(isAnyArea + ? { + scaleMargins: { + bottom: 0, + }, + } + : {}), + }); + + [...list] + .reverse() + .forEach( + ({ + dataset, + color, + colors, + seriesType: type, + title, + options, + defaultVisible, + }) => { + let series: ISeriesApi<"Baseline" | "Line" | "Area" | "Histogram">; + + if (type === SeriesType.Based) { + series = createBaseLineSeries(chart, { + color, + ...options, + }); + } else if (type === SeriesType.Area) { + series = createAreaSeries(chart, { + color, + autoscaleInfoProvider: (getInfo: () => AutoscaleInfo | null) => { + const info = getInfo(); + if (info) { + info.priceRange.minValue = 0; + } + return info; + }, + ...options, + }); + } else if (type === SeriesType.Histogram) { + series = createHistogramSeries(chart, { + color, + ...options, + }); + } else { + series = createLineSeries(chart, { + color, + ...options, + }); + } + + legendList.splice( + 0, + 0, + createSeriesLegend({ + id: stringToId(title), + presetId: preset.id, + title, + series, + color: () => colors || color || DEFAULT_BASELINE_COLORS, + defaultVisible, + url: dataset.url, + }), + ); + + createEffect(() => { + series.setData(dataset?.values() || []); + + setTimeScale(chartState.range); + }); + }, + ); + + createEffect(() => { + const options = { + scaleMargins: { + top: + price.lineLegend.visible() || price.ohlcLegend.visible() + ? rightPriceScaleOptions.scaleMargins.top + : rightPriceScaleOptions.scaleMargins.bottom, + bottom: rightPriceScaleOptions.scaleMargins.bottom, + }, + }; + + chart.priceScale("right").applyOptions(options); + }); + + return legendList; +} diff --git a/app/src/scripts/presets/templates/ratio.ts b/app/src/scripts/presets/templates/ratio.ts new file mode 100644 index 000000000..e740586a7 --- /dev/null +++ b/app/src/scripts/presets/templates/ratio.ts @@ -0,0 +1,289 @@ +// import { +// applyMultipleSeries, +// colors, +// createMomentumPresetFolder, +// SeriesType, +// } from "/src/scripts"; + +// // type HeightRatioKey = +// // | `${AnyPossibleCohortKey}RealizedPrice` +// // | "activePrice" +// // | "vaultedPrice" +// // | "trueMarketMean"; + +// // // type DateRatioKey = HeightRatioKey; +// // type DateRatioKey = HeightRatioKey | `price${AverageName}MA`; + +// export function createRatioPresetFolder< +// Scale extends ResourceScale, +// Key extends string, +// >({ +// datasets, +// scale, +// id, +// title, +// datasetKey, +// color, +// }: { +// datasets: Record<`${Key}${RatioKey}`, Dataset<ResourceScale>>; +// scale: Scale; +// id: string; +// title: string; +// color: string; +// datasetKey: Key; +// }): PartialPresetFolder { +// return { +// id: `${scale}-${id}-ratio`, +// name: "Ratio", +// tree: [ +// { +// id: `${scale}-${id}-ratio-value`, +// name: `Value`, +// title: `Bitcoin Price to ${title} Ratio`, +// icon: () => IconTablerDivide, +// applyPreset(params) { +// return applyMultipleSeries({ +// scale, +// ...params, +// priceScaleOptions: { +// halved: true, +// }, +// list: [ +// { +// title: "Ratio", +// seriesType: SeriesType.Based, +// dataset: datasets[`${datasetKey}Ratio`], +// options: { +// base: 1, +// }, +// }, +// ], +// }); +// }, +// description: "", +// }, +// { +// id: `${scale}-${id}-ratio-1y-average`, +// name: "Averages", +// tree: [ +// { +// id: `${scale}-${id}-ratio-averages`, +// name: `7 Day VS. 1 Year`, +// title: `Bitcoin Price to ${title} Ratio Moving Averages`, +// icon: () => IconTablerSwords, +// applyPreset(params) { +// return applyMultipleSeries({ +// scale, +// ...params, +// priceScaleOptions: { +// halved: true, +// }, +// list: [ +// { +// title: "Ratio", +// seriesType: SeriesType.Based, +// color: colors.gray, +// dataset: datasets[`${datasetKey}Ratio`], +// options: { +// base: 1, +// }, +// }, +// { +// title: "7 Day Moving Average", +// color: colors.closes7DMA, +// dataset: datasets[`${datasetKey}Ratio7DayMovingAverage`], +// }, +// { +// title: "1 Year Moving Average", +// color: colors.closes1YMA, +// dataset: datasets[`${datasetKey}Ratio1YearMovingAverage`], +// }, +// ], +// }); +// }, +// description: "", +// }, +// createMomentumPresetFolder({ +// datasets, +// scale, +// id: `${scale}-${id}-ratio-averages`, +// title: `${title} Ratio Moving Averages`, +// datasetKey: `${datasetKey}Ratio`, +// }), +// ], +// }, +// { +// id: `${scale}-${id}-ratio-extremes`, +// name: "Extremes", +// tree: [ +// { +// id: `${scale}-${id}-extreme-top-ratios`, +// name: "Top Ratios", +// description: "", +// icon: () => IconTablerJetpack, +// title: `${title} Extreme Top Ratios`, +// applyPreset(params) { +// return applyMultipleSeries({ +// scale, +// ...params, +// priceScaleOptions: { +// halved: true, +// }, +// list: [ +// { +// id: "ratio", +// title: "Ratio", +// color: colors.white, +// seriesType: SeriesType.Based, +// dataset: datasets[`${datasetKey}Ratio`], +// options: { +// base: 1, +// options: { +// baseLineColor: color, +// baseLineVisible: true, +// }, +// }, +// }, +// { +// id: "99.9-percentile", +// title: "99.9th Percentile", +// dataset: datasets[`${datasetKey}Ratio99.9Percentile`], +// color: colors.extremeMax, +// }, +// { +// id: "99.5-percentile", +// title: "99.5th Percentile", +// color: colors.extremeMiddle, +// dataset: datasets[`${datasetKey}Ratio99.5Percentile`], +// }, +// { +// id: "99-percentile", +// title: "99th Percentile", +// color: colors.extremeMin, +// dataset: datasets[`${datasetKey}Ratio99Percentile`], +// }, +// ], +// }); +// }, +// }, +// { +// id: `${scale}-${id}-extreme-bottom-ratios`, +// name: "Bottom Ratios", +// description: "", +// icon: () => IconTablerScubaMask, +// title: `${title} Extreme Bottom Ratios`, +// applyPreset(params) { +// return applyMultipleSeries({ +// scale, +// ...params, +// priceScaleOptions: { +// halved: true, +// }, +// list: [ +// { +// id: "ratio", +// title: "Ratio", +// color: colors.white, +// seriesType: SeriesType.Based, +// dataset: datasets[`${datasetKey}Ratio`], +// options: { +// base: 1, +// options: { +// baseLineColor: color, +// baseLineVisible: true, +// }, +// }, +// }, +// { +// id: "1-percentile", +// title: "1st Percentile", +// color: colors.extremeMin, +// dataset: datasets[`${datasetKey}Ratio1Percentile`], +// }, +// { +// id: "0.5-percentile", +// title: "0.5th Percentile", +// color: colors.extremeMiddle, +// dataset: datasets[`${datasetKey}Ratio0.5Percentile`], +// }, +// { +// id: "0.1-percentile", +// title: "0.1th Percentile", +// color: colors.extremeMax, +// dataset: datasets[`${datasetKey}Ratio0.1Percentile`], +// }, +// ], +// }); +// }, +// }, +// { +// id: `${scale}-${id}-extreme-top-prices`, +// name: "Top Prices", +// description: "", +// icon: () => IconTablerRocket, +// title: `${title} Extreme Top Prices`, +// applyPreset(params) { +// return applyMultipleSeries({ +// scale, +// ...params, +// list: [ +// { +// id: "99.9-percentile", +// title: "99.9th Percentile", +// color: colors.extremeMax, +// dataset: datasets[`${datasetKey}Ratio99.9Price`], +// }, +// { +// id: "99.5-percentile", +// title: "99.5th Percentile", +// color: colors.extremeMiddle, +// dataset: datasets[`${datasetKey}Ratio99.5Price`], +// }, +// { +// id: "99-percentile", +// title: "99th Percentile", +// color: colors.extremeMin, +// dataset: datasets[`${datasetKey}Ratio99Price`], +// }, +// ], +// }); +// }, +// }, +// { +// id: `${scale}-${id}-extreme-bottom-prices`, +// name: "Bottom Prices", +// description: "", +// icon: () => IconTablerSubmarine, +// title: `${title} Extreme Bottom Prices`, +// applyPreset(params) { +// return applyMultipleSeries({ +// scale, +// ...params, +// list: [ +// { +// id: "1-percentile", +// title: "1st Percentile", +// color: colors.extremeMin, +// dataset: datasets[`${datasetKey}Ratio1Price`], +// }, +// { +// id: "0.5-percentile", +// title: "0.5th Percentile", +// color: colors.extremeMiddle, +// dataset: datasets[`${datasetKey}Ratio0.5Price`], +// }, +// { +// id: "0.1-percentile", +// title: "0.1th Percentile", +// color: colors.extremeMax, +// dataset: datasets[`${datasetKey}Ratio0.1Price`], +// }, +// ], +// }); +// }, +// }, +// ], +// }, +// ], +// }; +// } diff --git a/app/src/scripts/presets/transactions/index.ts b/app/src/scripts/presets/transactions/index.ts new file mode 100644 index 000000000..7a4a42db2 --- /dev/null +++ b/app/src/scripts/presets/transactions/index.ts @@ -0,0 +1,225 @@ +import { colors } from "../../utils/colors"; +import { applyMultipleSeries } from "../templates/multiple"; + +export function createPresets(scale: ResourceScale) { + return { + name: "Transactions", + tree: [ + { + scale, + icon: IconTablerHandThreeFingers, + name: "Count", + title: "Transaction Count", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "1M SMA", + color: colors.momentumYellow, + dataset: params.datasets[scale].transaction_count_1m_sma, + }, + { + title: "1W SMA", + color: colors.bitcoin, + dataset: params.datasets[scale].transaction_count_1w_sma, + }, + { + title: "Raw", + color: colors.darkBitcoin, + dataset: params.datasets[scale].transaction_count, + }, + ], + }); + }, + }, + + { + name: "Volume", + tree: [ + { + scale, + icon: IconTablerCoinBitcoin, + name: "In Bitcoin", + title: "Transaction Volume", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "1M SMA", + color: colors.momentumYellow, + dataset: params.datasets[scale].transaction_volume_1m_sma, + }, + { + title: "1W SMA", + color: colors.bitcoin, + dataset: params.datasets[scale].transaction_volume_1w_sma, + }, + { + title: "Raw", + color: colors.darkBitcoin, + dataset: params.datasets[scale].transaction_volume, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerCoin, + name: "In Dollars", + title: "Transaction Volume In Dollars", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + mode: 1, + }, + list: [ + { + title: "1M SMA", + color: colors.lightDollars, + dataset: + params.datasets[scale] + .transaction_volume_in_dollars_1m_sma, + }, + { + title: "1W SMA", + color: colors.dollars, + dataset: + params.datasets[scale] + .transaction_volume_in_dollars_1w_sma, + }, + { + title: "Raw", + color: colors.darkDollars, + dataset: + params.datasets[scale].transaction_volume_in_dollars, + }, + ], + }); + }, + }, + ], + }, + + { + name: "Annualized Volume", + tree: [ + { + scale, + icon: IconTablerCoinBitcoin, + name: "In Bitcoin", + title: "Annualized Transaction Volume", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Volume", + color: colors.bitcoin, + dataset: + params.datasets[scale].annualized_transaction_volume, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerCoin, + name: "In Dollars", + title: "Annualized Transaction Volume In Dollars", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Volume", + color: colors.dollars, + dataset: + params.datasets[scale] + .annualized_transaction_volume_in_dollars, + }, + ], + }); + }, + }, + ], + }, + { + scale, + icon: IconTablerWind, + name: "Velocity", + title: "Transactions Velocity", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "Transactions Velocity", + color: colors.bitcoin, + dataset: params.datasets[scale].transaction_velocity, + }, + ], + }); + }, + }, + { + scale, + icon: IconTablerAlarm, + name: "Per Second", + title: "Transactions Per Second", + description: "", + applyPreset(params) { + return applyMultipleSeries({ + ...params, + priceScaleOptions: { + halved: true, + }, + list: [ + { + title: "1M SMA", + color: colors.lightBitcoin, + dataset: params.datasets[scale].transactions_per_second_1m_sma, + }, + { + title: "1W SMA", + color: colors.bitcoin, + dataset: params.datasets[scale].transactions_per_second_1w_sma, + }, + { + title: "Raw", + color: colors.darkBitcoin, + dataset: params.datasets[scale].transactions_per_second, + }, + ], + }); + }, + }, + ], + } satisfies PartialPresetFolder; +} diff --git a/app/src/scripts/presets/types.d.ts b/app/src/scripts/presets/types.d.ts new file mode 100644 index 000000000..fa47640d1 --- /dev/null +++ b/app/src/scripts/presets/types.d.ts @@ -0,0 +1,62 @@ +interface PartialPreset { + scale: ResourceScale; + icon?: () => JSXElement; + name: string; + title: string; + applyPreset: ApplyPreset; + description: string; +} + +interface Preset extends PartialPreset { + id: string; + path: FilePath; + isFavorite: RWS<boolean>; + visited: RWS<boolean>; +} + +type FilePath = { + id: string; + name: string; +}[]; + +type ApplyPreset = (params: { + chart: IChartApi; + datasets: Datasets; + preset: Preset; + activeResources: Accessor<Set<ResourceDataset<any, any>>>; +}) => ApplyPresetReturn; + +type ApplyPresetReturn = PresetLegend; + +interface PartialPresetFolder { + name: string; + tree: PartialPresetTree; +} + +interface PresetFolder extends PartialPresetFolder { + id: string; + tree: PresetTree; +} + +type PartialPresetTree = (PartialPreset | PartialPresetFolder)[]; +type PresetTree = (Preset | PresetFolder)[]; +// type PresetList = Preset[]; +// type FavoritePresets = Accessor<Preset[]>; + +type PresetsHistory = { date: Date; preset: Preset }[]; +type PresetsHistorySignal = RWS<PresetsHistory>; +type SerializedPresetsHistory = { p: string; d: number }[]; + +interface Presets { + tree: (Preset | PresetFolder)[]; + list: Preset[]; + favorites: Accessor<Preset[]>; + history: PresetsHistorySignal; + + selected: RWS<Preset>; + openedFolders: RWS<Set<string>>; + + select(preset: Preset): void; +} + +type PresetLegend = SeriesLegend[]; diff --git a/app/src/scripts/utils/array.ts b/app/src/scripts/utils/array.ts new file mode 100644 index 000000000..5fc9fa0eb --- /dev/null +++ b/app/src/scripts/utils/array.ts @@ -0,0 +1,16 @@ +export function sortedInsert(array: number[], value: number) { + let low = 0; + let high = array.length; + + while (low < high) { + const mid = (low + high) >>> 1; + + if (array[mid] < value) { + low = mid + 1; + } else { + high = mid; + } + } + + array.splice(low, 0, value); +} diff --git a/app/src/scripts/utils/colors.ts b/app/src/scripts/utils/colors.ts new file mode 100644 index 000000000..8b7078246 --- /dev/null +++ b/app/src/scripts/utils/colors.ts @@ -0,0 +1,246 @@ +import { + amber as amberTailwind, + blue as blueTailwind, + cyan as cyanTailwind, + emerald as emeraldTailwind, + fuchsia as fuchsiaTailwind, + neutral as grayTailwind, + green as greenTailwind, + indigo as indigoTailwind, + lime as limeTailwind, + orange as orangeTailwind, + pink as pinkTailwind, + purple as purpleTailwind, + red as redTailwind, + rose as roseTailwind, + sky as skyTailwind, + teal as tealTailwind, + violet as violetTailwind, + yellow as yellowTailwind, +} from "tailwindcss/colors"; + +// --- +// DO NOT USE TRANSPARENCY HERE +// DO NOT USE TRANSPARENCY HERE +// DO NOT USE TRANSPARENCY HERE +// DO NOT USE TRANSPARENCY HERE +// DO NOT USE TRANSPARENCY HERE +// DO NOT USE TRANSPARENCY HERE +// DO NOT USE TRANSPARENCY HERE +// DO NOT USE TRANSPARENCY HERE +// DO NOT USE TRANSPARENCY HERE +// DO NOT USE TRANSPARENCY HERE +// DO NOT USE TRANSPARENCY HERE +// DO NOT USE TRANSPARENCY HERE +// DO NOT USE TRANSPARENCY HERE +// DO NOT USE TRANSPARENCY HERE +// DO NOT USE TRANSPARENCY HERE +// DO NOT USE TRANSPARENCY HERE +// DO NOT USE TRANSPARENCY HERE +// DO NOT USE TRANSPARENCY HERE +// DO NOT USE TRANSPARENCY HERE +// --- + +const lightRed = redTailwind[300]; +const red = redTailwind[500]; +const darkRed = redTailwind[900]; +const orange = orangeTailwind[500]; +const darkOrange = orangeTailwind[900]; +const amber = amberTailwind[500]; +const darkAmber = amberTailwind[900]; +const yellow = yellowTailwind[500]; +const darkYellow = yellowTailwind[500]; +const lime = limeTailwind[500]; +const darkLime = limeTailwind[900]; +const green = greenTailwind[500]; +const darkGreen = greenTailwind[900]; +const lightEmerald = emeraldTailwind[300]; +const emerald = emeraldTailwind[500]; +const darkEmerald = emeraldTailwind[900]; +const teal = tealTailwind[500]; +const darkTeal = tealTailwind[900]; +const cyan = cyanTailwind[500]; +const darkCyan = cyanTailwind[900]; +const sky = skyTailwind[500]; +const darkSky = skyTailwind[900]; +const blue = blueTailwind[500]; +const darkBlue = blueTailwind[900]; +const indigo = indigoTailwind[500]; +const darkIndigo = indigoTailwind[900]; +const violet = violetTailwind[500]; +const darkViolet = violetTailwind[900]; +const purple = purpleTailwind[500]; +const darkPurple = purpleTailwind[900]; +const fuchsia = fuchsiaTailwind[500]; +const darkFuchsia = fuchsiaTailwind[900]; +const pink = pinkTailwind[500]; +const darkPink = pinkTailwind[900]; +const rose = roseTailwind[500]; +const darkRose = roseTailwind[900]; + +const darkWhite = grayTailwind[400]; +const gray = grayTailwind[600]; + +const black = "#000000"; +const white = "#ffffff"; + +export const convertCandleToCandleColor = ( + candle: { close: number; open: number }, + inverse?: boolean, +) => + (candle.close || 1) > (candle.open || 0) + ? !inverse + ? green + : red + : !inverse + ? red + : green; + +export const convertCandleToVolumeColor = ( + candle: { close: number; open: number }, + inverse?: boolean, +) => + (candle.close || 1) > (candle.open || 0) + ? !inverse + ? darkGreen + : darkRed + : !inverse + ? darkRed + : darkGreen; + +export const colors = { + white, + darkWhite, + gray, + lightBitcoin: yellow, + bitcoin: orange, + darkBitcoin: darkOrange, + lightDollars: lime, + dollars: emerald, + darkDollars: darkEmerald, + + _1d: lightRed, + _1w: red, + _8d: orange, + _13d: amber, + _21d: yellow, + _1m: lime, + _34d: green, + _55d: emerald, + _89d: teal, + _144d: cyan, + _6m: sky, + _1y: blue, + _2y: indigo, + _200w: violet, + _4y: purple, + _10y: fuchsia, + + p2pk: lime, + p2pkh: violet, + p2sh: emerald, + p2wpkh: cyan, + p2wsh: pink, + p2tr: blue, + crab: red, + fish: lime, + humpback: violet, + plankton: emerald, + shark: cyan, + shrimp: pink, + whale: blue, + megalodon: purple, + realizedPrice: orange, + oneMonthHolders: cyan, + threeMonthsHolders: lime, + sth: yellow, + sixMonthsHolder: red, + oneYearHolders: pink, + twoYearsHolders: purple, + lth: fuchsia, + balancedPrice: yellow, + cointimePrice: yellow, + trueMarketMeanPrice: blue, + vaultedPrice: green, + cvdd: lime, + terminalPrice: red, + loss: red, + darkLoss: darkRed, + profit: green, + darkProfit: darkGreen, + thermoCap: green, + investorCap: rose, + realizedCap: orange, + ethereum: indigo, + usdt: emerald, + usdc: blue, + ust: red, + busd: yellow, + usdd: emerald, + frax: gray, + dai: amber, + tusd: indigo, + pyusd: blue, + darkLiveliness: darkRose, + liveliness: rose, + vaultedness: green, + activityToVaultednessRatio: violet, + up_to_1d: lightRed, + up_to_1w: red, + up_to_1m: orange, + up_to_2m: orange, + up_to_3m: orange, + up_to_4m: orange, + up_to_5m: orange, + up_to_6m: orange, + up_to_1y: orange, + up_to_2y: orange, + up_to_3y: orange, + up_to_4y: orange, + up_to_5y: orange, + up_to_7y: orange, + up_to_10y: orange, + up_to_15y: orange, + from_10y_to_15y: purple, + from_7y_to_10y: violet, + from_5y_to_7y: indigo, + from_3y_to_5y: sky, + from_2y_to_3y: teal, + from_1y_to_2y: green, + from_6m_to_1y: lime, + from_3m_to_6m: yellow, + from_1m_to_3m: amber, + from_1w_to_1m: orange, + from_1d_to_1w: red, + from_1y: green, + from_2y: teal, + from_4y: indigo, + from_10y: violet, + from_15y: fuchsia, + coinblocksCreated: purple, + coinblocksDestroyed: red, + coinblocksStored: green, + momentum: [green, yellow, red], + momentumGreen: green, + momentumYellow: yellow, + momentumRed: red, + extremeMax: red, + extremeMiddle: orange, + extremeMin: yellow, + year_2009: yellow, + year_2010: yellow, + year_2011: yellow, + year_2012: yellow, + year_2013: yellow, + year_2014: yellow, + year_2015: yellow, + year_2016: yellow, + year_2017: yellow, + year_2018: yellow, + year_2019: yellow, + year_2020: yellow, + year_2021: yellow, + year_2022: yellow, + year_2023: yellow, + year_2024: yellow, +}; diff --git a/app/src/scripts/utils/date.ts b/app/src/scripts/utils/date.ts new file mode 100644 index 000000000..be65c26a9 --- /dev/null +++ b/app/src/scripts/utils/date.ts @@ -0,0 +1,10 @@ +// import { ONE_DAY_IN_MS } from "./time"; + +import { ONE_DAY_IN_MS } from "./time"; + +export const dateToString = (date: Date) => date.toJSON().split("T")[0]; + +// export const FIVE_MONTHS_IN_DAYS = 30 * 5; + +export const getNumberOfDaysBetweenTwoDates = (oldest: Date, youngest: Date) => + Math.round(Math.abs((youngest.valueOf() - oldest.valueOf()) / ONE_DAY_IN_MS)); diff --git a/app/src/scripts/utils/debounce.ts b/app/src/scripts/utils/debounce.ts new file mode 100644 index 000000000..ba87b8d28 --- /dev/null +++ b/app/src/scripts/utils/debounce.ts @@ -0,0 +1,19 @@ +export const debounce = <F extends (...args: any[]) => any>( + callback: F, + wait = 250, +) => { + let timeoutId: number | undefined; + let latestArgs: Parameters<F>; + + return (...args: Parameters<F>) => { + latestArgs = args; + + if (!timeoutId) { + timeoutId = window.setTimeout(async () => { + await callback(...latestArgs); + + timeoutId = undefined; + }, wait); + } + }; +}; diff --git a/app/src/scripts/utils/history.ts b/app/src/scripts/utils/history.ts new file mode 100644 index 000000000..06cf6be80 --- /dev/null +++ b/app/src/scripts/utils/history.ts @@ -0,0 +1,12 @@ +export function replaceHistory({ + urlParams, + pathname, +}: { + urlParams?: URLSearchParams; + pathname?: string; +}) { + urlParams ||= new URLSearchParams(window.location.search); + pathname ||= window.location.pathname; + + window.history.replaceState(null, "", `${pathname}?${urlParams.toString()}`); +} diff --git a/app/src/scripts/utils/id.ts b/app/src/scripts/utils/id.ts new file mode 100644 index 000000000..811ea9b5e --- /dev/null +++ b/app/src/scripts/utils/id.ts @@ -0,0 +1,3 @@ +export function stringToId(s: string) { + return s.replace(/\W/g, " ").trim().replace(/ +/g, "-").toLowerCase(); +} diff --git a/app/src/scripts/utils/locale.ts b/app/src/scripts/utils/locale.ts new file mode 100644 index 000000000..3aa85286a --- /dev/null +++ b/app/src/scripts/utils/locale.ts @@ -0,0 +1,31 @@ +export const priceToUSLocale = (price: number, compact = true) => { + const absolutePrice = Math.abs(price); + const lessThan100 = absolutePrice < 100; + const lessThan1000 = absolutePrice < 1_000; + const biggerThanMillion = absolutePrice >= 1_000_000; + + return numberToUSLocale( + price, + lessThan1000 ? (lessThan100 ? 2 : 1) : biggerThanMillion ? 3 : 0, + biggerThanMillion && compact + ? { + notation: "compact", + compactDisplay: "short", + } + : undefined, + ); +}; + +export const percentageToUSLocale = (percentage: number) => + numberToUSLocale(percentage, 1); + +const numberToUSLocale = ( + value: number, + digits: number, + options?: Intl.NumberFormatOptions | undefined, +) => + value.toLocaleString("en-us", { + ...options, + minimumFractionDigits: digits, + maximumFractionDigits: digits, + }); diff --git a/app/src/scripts/utils/math/averages.ts b/app/src/scripts/utils/math/averages.ts new file mode 100644 index 000000000..b9235d5be --- /dev/null +++ b/app/src/scripts/utils/math/averages.ts @@ -0,0 +1,22 @@ +import { computeSum } from "./sum"; + +export const computeAverage = (values: number[]) => + computeSum(values) / values.length; + +export const computeMovingAverage = < + T extends SingleValueData = SingleValueData, +>( + dataset: T[], + interval: number, +) => { + if (!dataset.length) return []; + + return dataset.map((data, index) => ({ + ...data, + value: computeAverage( + dataset + .slice(Math.max(index - interval + 1, 0), index + 1) + .map((data) => data.value || 1), + ), + })); +}; diff --git a/app/src/scripts/utils/math/random.ts b/app/src/scripts/utils/math/random.ts new file mode 100644 index 000000000..d4c8ef714 --- /dev/null +++ b/app/src/scripts/utils/math/random.ts @@ -0,0 +1,5 @@ +export function random<T>(array: T[]) { + if (array && array.length) { + return array[Math.floor(Math.random() * array.length)]; + } +} diff --git a/app/src/scripts/utils/math/round.ts b/app/src/scripts/utils/math/round.ts new file mode 100644 index 000000000..da8717a44 --- /dev/null +++ b/app/src/scripts/utils/math/round.ts @@ -0,0 +1,4 @@ +export const roundValue = (value: number, decimals = 5) => { + const tenPowerX = 10 ** decimals; + return Math.round(value * tenPowerX) / tenPowerX; +}; diff --git a/app/src/scripts/utils/math/sum.ts b/app/src/scripts/utils/math/sum.ts new file mode 100644 index 000000000..484d43994 --- /dev/null +++ b/app/src/scripts/utils/math/sum.ts @@ -0,0 +1,2 @@ +export const computeSum = (values: number[]) => + values.reduce((total, currentValue) => total + currentValue, 0); diff --git a/app/src/scripts/utils/run.ts b/app/src/scripts/utils/run.ts new file mode 100644 index 000000000..dd3dc436b --- /dev/null +++ b/app/src/scripts/utils/run.ts @@ -0,0 +1 @@ +export const run = <T>(f: () => T) => f(); diff --git a/app/src/scripts/utils/scroll.ts b/app/src/scripts/utils/scroll.ts new file mode 100644 index 000000000..2360303da --- /dev/null +++ b/app/src/scripts/utils/scroll.ts @@ -0,0 +1,9 @@ +export const scrollIntoView = ( + element?: HTMLElement | Element | null, + block: ScrollLogicalPosition = "nearest", + behavior: ScrollBehavior = "instant", +) => + element?.scrollIntoView({ + block, + behavior, + }); diff --git a/app/src/scripts/utils/selectableList/index.ts b/app/src/scripts/utils/selectableList/index.ts new file mode 100644 index 000000000..e407f1a92 --- /dev/null +++ b/app/src/scripts/utils/selectableList/index.ts @@ -0,0 +1,119 @@ +import { createRWS } from "/src/solid/rws"; + +export const createSelectableList = <T, L extends T[] = T[]>( + list: L, + parameters?: { + selected?: L[number]; + selectedIndex?: number | null; + }, +) => { + const selected = createRWS<L[number] | null>(null); + const selectedIndex = createRWS<number | null>(null); + + const selectableList: SelectableList<L[number], L> = { + selected, + selectedIndex, + list: createRWS(list, { + equals: false, + }), + select(s) { + if (this.selected() !== s) { + batch(() => { + selected.set(() => s); + this.selectIndex(this.list().indexOf(s) ?? null); + }); + } + }, + resetSelected() { + selected.set(null); + selectedIndex.set(null); + }, + selectFind(search, callback) { + const element = this.list().find( + (_element) => callback(_element) === search, + ); + + if (element) { + this.select(element); + } + + return element; + }, + selectIndex(i) { + i = i === -1 ? null : i; + + if (i && (i < 0 || i >= this.list().length)) { + throw new Error( + `SelectableList: selectIndex: ${i} is incorrect ! (has ${ + this.list().length + } elements)`, + ); + } + + if (i !== this.selectedIndex()) { + selectedIndex.set(i); + + const value = getValueFromIndexInList<L[number]>(i, this.list()); + + if (value !== null) { + this.select(value); + } + } + }, + push(value) { + this.list.set((l) => { + l.push(value); + return l; + }); + }, + pushAndSelect(value) { + batch(() => { + this.push(value); + this.select(value); + }); + }, + removeIndex(index) { + let value = null; + this.list.set((l) => { + value = l.splice(index, 1)?.[0]; + return l; + }); + return value; + }, + toJSON<TJSON, LJSON extends TJSON[] = TJSON[]>( + transform: (value: T) => TJSON, + filter?: (value: T) => boolean, + ): JSONSelectableList<TJSON, LJSON> { + return { + version: 1, + selectedIndex: getIndexOfSelectedInSelectableList(this), + list: (filter ? this.list().filter(filter) : this.list()).map((value) => + transform(value), + ) as LJSON, + }; + }, + }; + + if (parameters?.selected !== undefined) { + selectableList.select(parameters.selected); + } else if (parameters?.selectedIndex !== undefined) { + selectableList.selectIndex(parameters.selectedIndex); + } + + return selectableList; +}; + +export const createSL = createSelectableList; + +export const getIndexOfSelectedInSelectableList = <T, L extends T[] = T[]>( + sl: SelectableList<L[number], L>, +) => { + const selected = sl.selected(); + + return selected ? sl.list().indexOf(selected) : null; +}; + +const getValueFromIndexInList = <T, L extends T[] = T[]>( + index: number | null, + list: L, +) => (index !== null && list.length > 0 ? list.at(index) || list[0] : null); diff --git a/app/src/scripts/utils/selectableList/types.d.ts b/app/src/scripts/utils/selectableList/types.d.ts new file mode 100644 index 000000000..67bf7dcd3 --- /dev/null +++ b/app/src/scripts/utils/selectableList/types.d.ts @@ -0,0 +1,33 @@ +// --- +// JSON +// --- + +interface JSONSelectableList<T, L extends T[] = T[]> { + readonly version: 1; + selectedIndex: number | null; + readonly list: L; +} + +// --- +// Object +// --- + +interface SelectableList<T, L extends T[] = T[]> { + readonly selected: Accessor<T | null>; + readonly selectedIndex: Accessor<number | null>; + readonly list: RWS<L>; + readonly select: <S extends L[number] = L[number]>(s: S) => void; + readonly selectFind: <K>( + search: K, + callback: (element: T) => K, + ) => T | undefined; + readonly selectIndex: (i: number | null) => void; + readonly push: <S extends L[number] = L[number]>(s: S) => void; + readonly pushAndSelect: <S extends L[number] = L[number]>(s: S) => void; + readonly removeIndex: (i: number) => L[number] | null; + readonly resetSelected: VoidFunction; + readonly toJSON: <TJSON, LJSON extends TJSON[] = TJSON[]>( + transform: (value: T) => LJSON[number], + filter?: (value: T) => boolean, + ) => JSONSelectableList<TJSON, LJSON>; +} diff --git a/app/src/scripts/utils/sleep.ts b/app/src/scripts/utils/sleep.ts new file mode 100644 index 000000000..db3b9b4bc --- /dev/null +++ b/app/src/scripts/utils/sleep.ts @@ -0,0 +1,9 @@ +export function sleep(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +export function tick() { + return sleep(1); +} diff --git a/app/src/scripts/utils/storage.ts b/app/src/scripts/utils/storage.ts new file mode 100644 index 000000000..225724cef --- /dev/null +++ b/app/src/scripts/utils/storage.ts @@ -0,0 +1,19 @@ +export function saveToStorage(key?: string, value?: string | boolean) { + if (key) { + value !== undefined && value !== null + ? localStorage.setItem(key, String(value)) + : localStorage.removeItem(key); + } +} + +export function readBooleanFromStorage(key: string) { + const saved = localStorage.getItem(key); + if (saved) { + return isSerializedBooleanTrue(saved); + } + return null; +} + +export function isSerializedBooleanTrue(serialized: string) { + return serialized === "true" || serialized === "1"; +} diff --git a/app/src/scripts/utils/time.ts b/app/src/scripts/utils/time.ts new file mode 100644 index 000000000..b7ac6c17f --- /dev/null +++ b/app/src/scripts/utils/time.ts @@ -0,0 +1,8 @@ +export const ONE_SECOND_IN_MS = 1_000; +export const FIVE_SECOND_IN_MS = 5 * ONE_SECOND_IN_MS; +export const TEN_SECOND_IN_MS = 2 * FIVE_SECOND_IN_MS; +export const ONE_MINUTE_IN_MS = 6 * TEN_SECOND_IN_MS; +export const FIVE_MINUTES_IN_MS = 5 * ONE_MINUTE_IN_MS; +export const TEN_MINUTES_IN_MS = 2 * FIVE_MINUTES_IN_MS; +export const ONE_HOUR_IN_MS = 6 * TEN_MINUTES_IN_MS; +export const ONE_DAY_IN_MS = 24 * ONE_HOUR_IN_MS; diff --git a/app/src/scripts/utils/urlParams.ts b/app/src/scripts/utils/urlParams.ts new file mode 100644 index 000000000..e9f3d70de --- /dev/null +++ b/app/src/scripts/utils/urlParams.ts @@ -0,0 +1,40 @@ +import { replaceHistory } from "./history"; +import { isSerializedBooleanTrue } from "./storage"; + +const whitelist = ["from", "to"]; + +export function resetURLParams() { + const urlParams = new URLSearchParams(); + + [...new URLSearchParams(window.location.search).entries()] + .filter(([key, _]) => whitelist.includes(key)) + .forEach(([key, value]) => { + urlParams.set(key, value); + }); + + replaceHistory({ urlParams }); +} + +export function writeURLParam(key: string, value?: string | boolean) { + const urlParams = new URLSearchParams(window.location.search); + + if (value !== undefined) { + urlParams.set(key, String(value)); + } else { + urlParams.delete(key); + } + + replaceHistory({ urlParams }); +} + +export function readBooleanURLParam(key: string) { + const urlParams = new URLSearchParams(window.location.search); + + const parameter = urlParams.get(key); + + if (parameter) { + return isSerializedBooleanTrue(parameter); + } + + return null; +} diff --git a/app/src/scripts/ws/base.ts b/app/src/scripts/ws/base.ts new file mode 100644 index 000000000..6b45b5bed --- /dev/null +++ b/app/src/scripts/ws/base.ts @@ -0,0 +1,62 @@ +import { makeEventListener } from "@solid-primitives/event-listener"; + +import { createRWS } from "/src/solid/rws"; + +export const createResourceWS = <T>( + creator: (callback: (value: T) => void) => WebSocket, +) => { + let ws: WebSocket | null = null; + + const live = createRWS(false); + const latest = createRWS<T | null>(null); + + let clearFocusListener: VoidFunction | undefined; + + let clearOnlineListener: VoidFunction | undefined; + + const resource: WebsocketResource<T> = { + live, + latest, + open() { + ws = creator((value) => latest.set(() => value)); + + ws.addEventListener("open", () => { + console.log("ws: open"); + live.set(true); + }); + + ws.addEventListener("close", () => { + console.log("ws: close"); + live.set(false); + }); + + const reinitWebSocket = () => { + if (!ws || ws.readyState === ws.CLOSED) { + console.log("ws: reinit"); + resource.open(); + } + }; + + clearFocusListener = makeEventListener( + document, + "visibilitychange", + () => !document.hidden && reinitWebSocket(), + ); + + clearOnlineListener = makeEventListener( + window, + "online", + reinitWebSocket, + ); + }, + close() { + ws?.close(); + clearFocusListener = clearFocusListener?.() || undefined; + clearOnlineListener = clearOnlineListener?.() || undefined; + live.set(false); + ws = null; + }, + }; + + return resource; +}; diff --git a/app/src/scripts/ws/index.ts b/app/src/scripts/ws/index.ts new file mode 100644 index 000000000..917a881fb --- /dev/null +++ b/app/src/scripts/ws/index.ts @@ -0,0 +1,10 @@ +import { createResourceWS } from "./base"; +import { krakenAPI } from "./kraken"; + +export const webSockets = { + liveKrakenCandle: createResourceWS(krakenAPI.createLiveCandleWebsocket), + openAll() { + this.liveKrakenCandle.open(); + onCleanup(this.liveKrakenCandle.close); + }, +}; diff --git a/app/src/scripts/ws/kraken.ts b/app/src/scripts/ws/kraken.ts new file mode 100644 index 000000000..ca4118a91 --- /dev/null +++ b/app/src/scripts/ws/kraken.ts @@ -0,0 +1,49 @@ +import { dateToString } from "../utils/date"; +import { ONE_DAY_IN_MS } from "../utils/time"; + +export const krakenAPI = { + createLiveCandleWebsocket( + callback: (candle: DatasetCandlestickData) => void, + ) { + const ws = new WebSocket("wss://ws.kraken.com"); + + ws.addEventListener("open", () => { + ws.send( + JSON.stringify({ + event: "subscribe", + pair: ["XBT/USD"], + subscription: { + name: "ohlc", + interval: 1440, + }, + }), + ); + }); + + ws.addEventListener("message", (message) => { + const result = JSON.parse(message.data); + + if (!Array.isArray(result)) return; + + const [timestamp, _, open, high, low, close, __, volume] = result[1]; + + const dateStr = dateToString(new Date(Number(timestamp) * 1000)); + + const candle: DatasetCandlestickData = { + // date: dateStr, + number: new Date(dateStr).valueOf() / ONE_DAY_IN_MS, + time: dateStr, + open: Number(open), + high: Number(high), + low: Number(low), + close: Number(close), + value: Number(close), + // volume: Number(volume), + }; + + candle && callback({ ...candle }); + }); + + return ws; + }, +}; diff --git a/app/src/scripts/ws/types.d.ts b/app/src/scripts/ws/types.d.ts new file mode 100644 index 000000000..e021294cb --- /dev/null +++ b/app/src/scripts/ws/types.d.ts @@ -0,0 +1,6 @@ +interface WebsocketResource<T> { + live: Accessor<boolean>; + latest: Accessor<T | null>; + open: VoidFunction; + close: VoidFunction; +} diff --git a/app/src/solid/classes.ts b/app/src/solid/classes.ts new file mode 100644 index 000000000..c8c1b20d8 --- /dev/null +++ b/app/src/solid/classes.ts @@ -0,0 +1,12 @@ +export function classPropToString(classes?: ClassProp): string { + return Array.isArray(classes) + ? ( + classes + .map((c) => (Array.isArray(c) ? classPropToString(c) : c)) + .filter((c) => c) as string[] + ) + .map((c) => c.trim()) + .join(" ") + .trim() + : classes || ""; +} diff --git a/app/src/solid/rws.ts b/app/src/solid/rws.ts new file mode 100644 index 000000000..6b6fe332c --- /dev/null +++ b/app/src/solid/rws.ts @@ -0,0 +1,18 @@ +function convertSignalToReadWriteSignal<T>(signal: Signal<T>) { + const getter = signal[0] as Accessor<T> & { + set?: Setter<T>; + }; + + getter.set = signal[1]; + + return getter as ReadWriteSignal<T>; +}; + +export function createReadWriteSignal<T>( + value: T, + options?: SignalOptions<T>, +) { + return convertSignalToReadWriteSignal(createSignal(value, options)); +} + +export const createRWS = createReadWriteSignal; diff --git a/app/src/solid/types/classes.d.ts b/app/src/solid/types/classes.d.ts new file mode 100644 index 000000000..05f166f3f --- /dev/null +++ b/app/src/solid/types/classes.d.ts @@ -0,0 +1 @@ +type ClassProp = string | (ClassProp | string | false | undefined)[]; diff --git a/app/src/solid/types/library.d.ts b/app/src/solid/types/library.d.ts new file mode 100644 index 000000000..8fc198304 --- /dev/null +++ b/app/src/solid/types/library.d.ts @@ -0,0 +1,41 @@ +type Signal<T> = import("solid-js").Signal<T>; +type Accessor<T> = import("solid-js").Accessor<T>; +type Setter<T> = import("solid-js").Setter<T>; +type SignalOptions<T> = import("solid-js").SignalOptions<T>; + +type Component<T = {}> = import("solid-js").Component<T>; + +type ValidComponent = import("solid-js").ValidComponent; + +type ParentProps = import("solid-js").ParentProps; + +type EffectFunction< + Prev, + Next extends Prev = Prev, +> = import("solid-js").EffectFunction<Prev, Next>; + +type JSXElement = import("solid-js").JSXElement; + +type Owner = import("solid-js").Owner; + +type EventHandlerUnion< + T, + E extends Event, +> = import("solid-js").JSX.EventHandlerUnion<T, E>; + +type CSSProperties = import("solid-js").JSX.CSSProperties; + +type HTMLAttributes<T = any> = import("solid-js").JSX.HTMLAttributes<T>; +type ButtonHTMLAttributes = + import("solid-js").JSX.ButtonHTMLAttributes<HTMLButtonElement>; +type InputHTMLAttributes = + import("solid-js").JSX.InputHTMLAttributes<HTMLInputElement>; +type SelectHTMLAttributes = + import("solid-js").JSX.SelectHTMLAttributes<HTMLSelectElement>; +type DetailsHTMLAttributes = + import("solid-js").JSX.DetailsHtmlAttributes<HTMLDetailsElement>; +type DialogHTMLAttributes = + import("solid-js").JSX.DialogHtmlAttributes<HTMLDialogElement>; + +type LinkProps = import("@solidjs/router").LinkProps; +type RouteDefinition = import("@solidjs/router").RouteDefinition; diff --git a/app/src/solid/types/rws.d.ts b/app/src/solid/types/rws.d.ts new file mode 100644 index 000000000..02815e7ce --- /dev/null +++ b/app/src/solid/types/rws.d.ts @@ -0,0 +1,5 @@ +type ReadWriteSignal<T> = Accessor<T> & { + readonly set: Setter<T>; +}; + +type RWS<T> = ReadWriteSignal<T>; diff --git a/app/src/styles.css b/app/src/styles.css new file mode 100644 index 000000000..70c113a68 --- /dev/null +++ b/app/src/styles.css @@ -0,0 +1,26 @@ +@tailwind base; + +@tailwind components; + +@tailwind utilities; + +@font-face { + font-family: "Lexend"; + font-display: "swap"; + src: url("/fonts/Lexend.var.woff2") format("woff2"); + font-weight: 100 900; + font-optical-sizing: auto; +} + +@keyframes marquee { + from { + transform: translateX(0); + } + to { + transform: translateX(-50%); + } +} + +mark { + @apply bg-transparent p-0 text-orange-400; +} diff --git a/app/src/types/auto-imports.d.ts b/app/src/types/auto-imports.d.ts new file mode 100644 index 000000000..2c3cd3972 --- /dev/null +++ b/app/src/types/auto-imports.d.ts @@ -0,0 +1,302 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// noinspection JSUnusedGlobalSymbols +// Generated by unplugin-auto-import +export {} +declare global { + const Dynamic: typeof import('solid-js/web')['Dynamic'] + const ErrorBoundary: typeof import('solid-js')['ErrorBoundary'] + const For: typeof import('solid-js')['For'] + const IconTabler123: (typeof import("~icons/tabler/123.jsx"))["default"] + const IconTablerAPI: typeof import('~icons/tabler/a-p-i.jsx')['default'] + const IconTablerAddressBook: typeof import('~icons/tabler/address-book.jsx')['default'] + const IconTablerAdjustments: typeof import('~icons/tabler/adjustments.jsx')['default'] + const IconTablerAlarm: typeof import('~icons/tabler/alarm.jsx')['default'] + const IconTablerAnalyze: typeof import('~icons/tabler/analyze.jsx')['default'] + const IconTablerApi: typeof import('~icons/tabler/api.jsx')['default'] + const IconTablerArchive: typeof import('~icons/tabler/archive.jsx')['default'] + const IconTablerArrowBack: typeof import('~icons/tabler/arrow-back.jsx')['default'] + const IconTablerArrowBackUp: (typeof import("~icons/tabler/arrow-back-up.jsx"))["default"] + const IconTablerArrowCross: typeof import('~icons/tabler/arrow-cross.jsx')['default'] + const IconTablerArrowForward: typeof import('~icons/tabler/arrow-forward.jsx')['default'] + const IconTablerArrowRight: (typeof import("~icons/tabler/arrow-right.jsx"))["default"] + const IconTablerArrowsCross: typeof import('~icons/tabler/arrows-cross.jsx')['default'] + const IconTablerArrowsMaximize: (typeof import("~icons/tabler/arrows-maximize.jsx"))["default"] + const IconTablerArrowsMinimize: (typeof import("~icons/tabler/arrows-minimize.jsx"))["default"] + const IconTablerArrowsSearch: (typeof import("~icons/tabler/arrows-search.jsx"))["default"] + const IconTablerArrowsShuffle: (typeof import("~icons/tabler/arrows-shuffle.jsx"))["default"] + const IconTablerArrowsShuffle2: typeof import('~icons/tabler/arrows-shuffle2.jsx')['default'] + const IconTablerArrowsStar: (typeof import("~icons/tabler/arrows-star.jsx"))["default"] + const IconTablerArrowsVertical: typeof import('~icons/tabler/arrows-vertical.jsx')['default'] + const IconTablerAssembly: typeof import('~icons/tabler/assembly.jsx')['default'] + const IconTablerAssemblyFilled: typeof import('~icons/tabler/assembly-filled.jsx')['default'] + const IconTablerAsterisk: typeof import('~icons/tabler/asterisk.jsx')['default'] + const IconTablerAt: (typeof import("~icons/tabler/at.jsx"))["default"] + const IconTablerBalance: (typeof import("~icons/tabler/balance.jsx"))["default"] + const IconTablerBitcoin: typeof import('~icons/tabler/bitcoin.jsx')['default'] + const IconTablerBitcoinCoin: typeof import('~icons/tabler/bitcoin-coin.jsx')['default'] + const IconTablerBolt: (typeof import("~icons/tabler/bolt.jsx"))["default"] + const IconTablerBrandGithub: (typeof import("~icons/tabler/brand-github.jsx"))["default"] + const IconTablerBuildinFactory: (typeof import("~icons/tabler/buildin-factory.jsx"))["default"] + const IconTablerBuildingBank: typeof import('~icons/tabler/building-bank.jsx')['default'] + const IconTablerBuildingFactory: typeof import('~icons/tabler/building-factory.jsx')['default'] + const IconTablerBuildingFactory2: typeof import('~icons/tabler/building-factory2.jsx')['default'] + const IconTablerBuildingWarehouse: typeof import('~icons/tabler/building-warehouse.jsx')['default'] + const IconTablerCactus: typeof import('~icons/tabler/cactus.jsx')['default'] + const IconTablerCactusFilled: typeof import('~icons/tabler/cactus-filled.jsx')['default'] + const IconTablerCalculator: typeof import('~icons/tabler/calculator.jsx')['default'] + const IconTablerCash: typeof import('~icons/tabler/cash.jsx')['default'] + const IconTablerCashBanknote: typeof import('~icons/tabler/cash-banknote.jsx')['default'] + const IconTablerChartAreaFilled: typeof import('~icons/tabler/chart-area-filled.jsx')['default'] + const IconTablerChartLine: typeof import('~icons/tabler/chart-line.jsx')['default'] + const IconTablerChevronLeft: typeof import('~icons/tabler/chevron-left.jsx')['default'] + const IconTablerChevronRight: typeof import('~icons/tabler/chevron-right.jsx')['default'] + const IconTablerCircles: typeof import('~icons/tabler/circles.jsx')['default'] + const IconTablerCoffin: typeof import('~icons/tabler/coffin.jsx')['default'] + const IconTablerCog: (typeof import("~icons/tabler/cog.jsx"))["default"] + const IconTablerCoin: typeof import('~icons/tabler/coin.jsx')['default'] + const IconTablerCoinBitcoin: typeof import('~icons/tabler/coin-bitcoin.jsx')['default'] + const IconTablerCoinDollar: typeof import('~icons/tabler/coin-dollar.jsx')['default'] + const IconTablerCoinDollars: typeof import('~icons/tabler/coin-dollars.jsx')['default'] + const IconTablerCoinEuro: (typeof import("~icons/tabler/coin-euro.jsx"))["default"] + const IconTablerCoins: typeof import('~icons/tabler/coins.jsx')['default'] + const IconTablerCube: typeof import('~icons/tabler/cube.jsx')['default'] + const IconTablerCubeUnfolded: typeof import('~icons/tabler/cube-unfolded.jsx')['default'] + const IconTablerCurrency: (typeof import("~icons/tabler/currency.jsx"))["default"] + const IconTablerCurrencyBahraini: typeof import('~icons/tabler/currency-bahraini.jsx')['default'] + const IconTablerCurrencyBaht: typeof import('~icons/tabler/currency-baht.jsx')['default'] + const IconTablerCurrencyBitcoin: typeof import('~icons/tabler/currency-bitcoin.jsx')['default'] + const IconTablerCurrencyDinar: typeof import('~icons/tabler/currency-dinar.jsx')['default'] + const IconTablerCurrencyDirham: typeof import('~icons/tabler/currency-dirham.jsx')['default'] + const IconTablerCurrencyDollar: typeof import('~icons/tabler/currency-dollar.jsx')['default'] + const IconTablerCurrencyDollarAustralian: typeof import('~icons/tabler/currency-dollar-australian.jsx')['default'] + const IconTablerCurrencyDollarCanadian: typeof import('~icons/tabler/currency-dollar-canadian.jsx')['default'] + const IconTablerCurrencyDollarSingapore: typeof import('~icons/tabler/currency-dollar-singapore.jsx')['default'] + const IconTablerCurrencyDong: typeof import('~icons/tabler/currency-dong.jsx')['default'] + const IconTablerCurrencyEuro: typeof import('~icons/tabler/currency-euro.jsx')['default'] + const IconTablerCurrencyForint: typeof import('~icons/tabler/currency-forint.jsx')['default'] + const IconTablerCurrencyFrank: typeof import('~icons/tabler/currency-frank.jsx')['default'] + const IconTablerCurrencyHryvnia: typeof import('~icons/tabler/currency-hryvnia.jsx')['default'] + const IconTablerCurrencyKrone: (typeof import("~icons/tabler/currency-krone.jsx"))["default"] + const IconTablerCurrencyKroneCzech: typeof import('~icons/tabler/currency-krone-czech.jsx')['default'] + const IconTablerCurrencyKroneDanish: typeof import('~icons/tabler/currency-krone-danish.jsx')['default'] + const IconTablerCurrencyKroneSwedish: typeof import('~icons/tabler/currency-krone-swedish.jsx')['default'] + const IconTablerCurrencyLari: typeof import('~icons/tabler/currency-lari.jsx')['default'] + const IconTablerCurrencyLira: typeof import('~icons/tabler/currency-lira.jsx')['default'] + const IconTablerCurrencyNaira: typeof import('~icons/tabler/currency-naira.jsx')['default'] + const IconTablerCurrencyPeso: typeof import('~icons/tabler/currency-peso.jsx')['default'] + const IconTablerCurrencyPound: typeof import('~icons/tabler/currency-pound.jsx')['default'] + const IconTablerCurrencyReal: typeof import('~icons/tabler/currency-real.jsx')['default'] + const IconTablerCurrencyRiyal: typeof import('~icons/tabler/currency-riyal.jsx')['default'] + const IconTablerCurrencyRubel: typeof import('~icons/tabler/currency-rubel.jsx')['default'] + const IconTablerCurrencyRuble: (typeof import("~icons/tabler/currency-ruble.jsx"))["default"] + const IconTablerCurrencyRupee: typeof import('~icons/tabler/currency-rupee.jsx')['default'] + const IconTablerCurrencyShekel: typeof import('~icons/tabler/currency-shekel.jsx')['default'] + const IconTablerCurrencySwedishKrone: (typeof import("~icons/tabler/currency-swedish-krone.jsx"))["default"] + const IconTablerCurrencyTaka: typeof import('~icons/tabler/currency-taka.jsx')['default'] + const IconTablerCurrencyWon: typeof import('~icons/tabler/currency-won.jsx')['default'] + const IconTablerCurrencyYen: typeof import('~icons/tabler/currency-yen.jsx')['default'] + const IconTablerCurrencyYuan: typeof import('~icons/tabler/currency-yuan.jsx')['default'] + const IconTablerCurrencyZloty: typeof import('~icons/tabler/currency-zloty.jsx')['default'] + const IconTablerCut: typeof import('~icons/tabler/cut.jsx')['default'] + const IconTablerDice: (typeof import("~icons/tabler/dice.jsx"))["default"] + const IconTablerDice1: typeof import('~icons/tabler/dice1.jsx')['default'] + const IconTablerDice1Filled: typeof import('~icons/tabler/dice1-filled.jsx')['default'] + const IconTablerDice2: typeof import('~icons/tabler/dice2.jsx')['default'] + const IconTablerDice2Filled: typeof import('~icons/tabler/dice2-filled.jsx')['default'] + const IconTablerDice3: typeof import('~icons/tabler/dice3.jsx')['default'] + const IconTablerDice3Filled: typeof import('~icons/tabler/dice3-filled.jsx')['default'] + const IconTablerDice4: typeof import('~icons/tabler/dice4.jsx')['default'] + const IconTablerDice4Filled: typeof import('~icons/tabler/dice4-filled.jsx')['default'] + const IconTablerDice5: typeof import('~icons/tabler/dice5.jsx')['default'] + const IconTablerDice5Filled: typeof import('~icons/tabler/dice5-filled.jsx')['default'] + const IconTablerDice6: typeof import('~icons/tabler/dice6.jsx')['default'] + const IconTablerDice6Filled: typeof import('~icons/tabler/dice6-filled.jsx')['default'] + const IconTablerDiceFilled: (typeof import("~icons/tabler/dice-filled.jsx"))["default"] + const IconTablerDirections: (typeof import("~icons/tabler/directions.jsx"))["default"] + const IconTablerDivide: typeof import('~icons/tabler/divide.jsx')['default'] + const IconTablerDollar: typeof import('~icons/tabler/dollar.jsx')['default'] + const IconTablerDollarReceipt: (typeof import("~icons/tabler/dollar-receipt.jsx"))["default"] + const IconTablerDoor: (typeof import("~icons/tabler/door.jsx"))["default"] + const IconTablerExternalLink: typeof import('~icons/tabler/external-link.jsx')['default'] + const IconTablerFeather: typeof import('~icons/tabler/feather.jsx')['default'] + const IconTablerFile: typeof import('~icons/tabler/file.jsx')['default'] + const IconTablerFileDescription: (typeof import("~icons/tabler/file-description.jsx"))["default"] + const IconTablerFileShredder: typeof import('~icons/tabler/file-shredder.jsx')['default'] + const IconTablerFolder: typeof import('~icons/tabler/folder.jsx')['default'] + const IconTablerFolderFilled: typeof import('~icons/tabler/folder-filled.jsx')['default'] + const IconTablerFolderOpen: typeof import('~icons/tabler/folder-open.jsx')['default'] + const IconTablerFullscreen: typeof import('~icons/tabler/fullscreen.jsx')['default'] + const IconTablerGitMerge: typeof import('~icons/tabler/git-merge.jsx')['default'] + const IconTablerGithub: (typeof import("~icons/tabler/github.jsx"))["default"] + const IconTablerGrowth: (typeof import("~icons/tabler/growth.jsx"))["default"] + const IconTablerHammer: (typeof import("~icons/tabler/hammer.jsx"))["default"] + const IconTablerHandThreeFingers: typeof import('~icons/tabler/hand-three-fingers.jsx')['default'] + const IconTablerHash: typeof import('~icons/tabler/hash.jsx')['default'] + const IconTablerHeartBolt: typeof import('~icons/tabler/heart-bolt.jsx')['default'] + const IconTablerHistory: typeof import('~icons/tabler/history.jsx')['default'] + const IconTablerHome: typeof import('~icons/tabler/home.jsx')['default'] + const IconTablerHome2: typeof import('~icons/tabler/home2.jsx')['default'] + const IconTablerHourglassEmpty: (typeof import("~icons/tabler/hourglass-empty.jsx"))["default"] + const IconTablerInfinity: typeof import('~icons/tabler/infinity.jsx')['default'] + const IconTablerInfo: (typeof import("~icons/tabler/info.jsx"))["default"] + const IconTablerInfoCircle: (typeof import("~icons/tabler/info-circle.jsx"))["default"] + const IconTablerInfoCircleFilled: (typeof import("~icons/tabler/info-circle-filled.jsx"))["default"] + const IconTablerInfoSmall: (typeof import("~icons/tabler/info-small.jsx"))["default"] + const IconTablerInfoSquareRounded: (typeof import("~icons/tabler/info-square-rounded.jsx"))["default"] + const IconTablerJetpack: typeof import('~icons/tabler/jetpack.jsx')['default'] + const IconTablerLetterB: typeof import('~icons/tabler/letter-b.jsx')['default'] + const IconTablerLetterG: typeof import('~icons/tabler/letter-g.jsx')['default'] + const IconTablerLetterK: typeof import('~icons/tabler/letter-k.jsx')['default'] + const IconTablerLetterM: typeof import('~icons/tabler/letter-m.jsx')['default'] + const IconTablerLetterN: typeof import('~icons/tabler/letter-n.jsx')['default'] + const IconTablerLetterR: typeof import('~icons/tabler/letter-r.jsx')['default'] + const IconTablerLetterS: typeof import('~icons/tabler/letter-s.jsx')['default'] + const IconTablerLetterW: typeof import('~icons/tabler/letter-w.jsx')['default'] + const IconTablerLetterY: typeof import('~icons/tabler/letter-y.jsx')['default'] + const IconTablerList: (typeof import("~icons/tabler/list.jsx"))["default"] + const IconTablerListSearch: (typeof import("~icons/tabler/list-search.jsx"))["default"] + const IconTablerListTree: (typeof import("~icons/tabler/list-tree.jsx"))["default"] + const IconTablerLock: (typeof import("~icons/tabler/lock.jsx"))["default"] + const IconTablerLoop: (typeof import("~icons/tabler/loop.jsx"))["default"] + const IconTablerMail: typeof import('~icons/tabler/mail.jsx')['default'] + const IconTablerMathAvg: typeof import('~icons/tabler/math-avg.jsx')['default'] + const IconTablerMaximize: typeof import('~icons/tabler/maximize.jsx')['default'] + const IconTablerMoneybag: typeof import('~icons/tabler/moneybag.jsx')['default'] + const IconTablerMoodDollar: typeof import('~icons/tabler/mood-dollar.jsx')['default'] + const IconTablerMoodSadDizzy: typeof import('~icons/tabler/mood-sad-dizzy.jsx')['default'] + const IconTablerMotorBike: (typeof import("~icons/tabler/motor-bike.jsx"))["default"] + const IconTablerMotorbike: (typeof import("~icons/tabler/motorbike.jsx"))["default"] + const IconTablerNumber123: typeof import('~icons/tabler/number123.jsx')['default'] + const IconTablerPercentage: typeof import('~icons/tabler/percentage.jsx')['default'] + const IconTablerPick: typeof import('~icons/tabler/pick.jsx')['default'] + const IconTablerPigMoney: typeof import('~icons/tabler/pig-money.jsx')['default'] + const IconTablerPlay: (typeof import("~icons/tabler/play.jsx"))["default"] + const IconTablerPlayCard: typeof import('~icons/tabler/play-card.jsx')['default'] + const IconTablerPlayerPauseFilled: (typeof import("~icons/tabler/player-pause-filled.jsx"))["default"] + const IconTablerPlayerPlayFilled: (typeof import("~icons/tabler/player-play-filled.jsx"))["default"] + const IconTablerQrCode: typeof import('~icons/tabler/qr-code.jsx')['default'] + const IconTablerQrcode: typeof import('~icons/tabler/qrcode.jsx')['default'] + const IconTablerRandom2: typeof import('~icons/tabler/random2.jsx')['default'] + const IconTablerReceipt: typeof import('~icons/tabler/receipt.jsx')['default'] + const IconTablerReceiptBitcoin: typeof import('~icons/tabler/receipt-bitcoin.jsx')['default'] + const IconTablerReceiptDollar: typeof import('~icons/tabler/receipt-dollar.jsx')['default'] + const IconTablerReceiptTax: typeof import('~icons/tabler/receipt-tax.jsx')['default'] + const IconTablerRefreshAlert: typeof import('~icons/tabler/refresh-alert.jsx')['default'] + const IconTablerReset: (typeof import("~icons/tabler/reset.jsx"))["default"] + const IconTablerRibbonHealth: typeof import('~icons/tabler/ribbon-health.jsx')['default'] + const IconTablerRipple: typeof import('~icons/tabler/ripple.jsx')['default'] + const IconTablerRocket: typeof import('~icons/tabler/rocket.jsx')['default'] + const IconTablerRollerCoaster: (typeof import("~icons/tabler/roller-coaster.jsx"))["default"] + const IconTablerRollercoaster: typeof import('~icons/tabler/rollercoaster.jsx')['default'] + const IconTablerSbare: typeof import('~icons/tabler/sbare.jsx')['default'] + const IconTablerScale: typeof import('~icons/tabler/scale.jsx')['default'] + const IconTablerScuba: (typeof import("~icons/tabler/scuba.jsx"))["default"] + const IconTablerScubaMask: typeof import('~icons/tabler/scuba-mask.jsx')['default'] + const IconTablerSearch: typeof import('~icons/tabler/search.jsx')['default'] + const IconTablerSearchFilled: (typeof import("~icons/tabler/search-filled.jsx"))["default"] + const IconTablerSelector: (typeof import("~icons/tabler/selector.jsx"))["default"] + const IconTablerSettings: typeof import('~icons/tabler/settings.jsx')['default'] + const IconTablerSettingsFilled: typeof import('~icons/tabler/settings-filled.jsx')['default'] + const IconTablerShare: typeof import('~icons/tabler/share.jsx')['default'] + const IconTablerSlash: typeof import('~icons/tabler/slash.jsx')['default'] + const IconTablerSparkle: typeof import('~icons/tabler/sparkle.jsx')['default'] + const IconTablerSparkles: typeof import('~icons/tabler/sparkles.jsx')['default'] + const IconTablerSquareHalf: typeof import('~icons/tabler/square-half.jsx')['default'] + const IconTablerSquareRounded: (typeof import("~icons/tabler/square-rounded.jsx"))["default"] + const IconTablerStack: typeof import('~icons/tabler/stack.jsx')['default'] + const IconTablerStack3: typeof import('~icons/tabler/stack3.jsx')['default'] + const IconTablerStackMiddle: typeof import('~icons/tabler/stack-middle.jsx')['default'] + const IconTablerStairs: typeof import('~icons/tabler/stairs.jsx')['default'] + const IconTablerStar: typeof import('~icons/tabler/star.jsx')['default'] + const IconTablerStarFilled: typeof import('~icons/tabler/star-filled.jsx')['default'] + const IconTablerStatusChange: typeof import('~icons/tabler/status-change.jsx')['default'] + const IconTablerSubmarine: typeof import('~icons/tabler/submarine.jsx')['default'] + const IconTablerSuit: (typeof import("~icons/tabler/suit.jsx"))["default"] + const IconTablerSum: typeof import('~icons/tabler/sum.jsx')['default'] + const IconTablerSwords: typeof import('~icons/tabler/swords.jsx')['default'] + const IconTablerTag: typeof import('~icons/tabler/tag.jsx')['default'] + const IconTablerThumbUp: (typeof import("~icons/tabler/thumb-up.jsx"))["default"] + const IconTablerTicket: typeof import('~icons/tabler/ticket.jsx')['default'] + const IconTablerTie: typeof import('~icons/tabler/tie.jsx')['default'] + const IconTablerTimeDuration30: typeof import('~icons/tabler/time-duration30.jsx')['default'] + const IconTablerTrash: typeof import('~icons/tabler/trash.jsx')['default'] + const IconTablerTrendingDown: typeof import('~icons/tabler/trending-down.jsx')['default'] + const IconTablerTrendingUp: typeof import('~icons/tabler/trending-up.jsx')['default'] + const IconTablerUser: typeof import('~icons/tabler/user.jsx')['default'] + const IconTablerUsersGroup: typeof import('~icons/tabler/users-group.jsx')['default'] + const IconTablerVocabulary: (typeof import("~icons/tabler/vocabulary.jsx"))["default"] + const IconTablerVolume: typeof import('~icons/tabler/volume.jsx')['default'] + const IconTablerVolume2: typeof import('~icons/tabler/volume2.jsx')['default'] + const IconTablerWall: typeof import('~icons/tabler/wall.jsx')['default'] + const IconTablerWallet: typeof import('~icons/tabler/wallet.jsx')['default'] + const IconTablerWeight: typeof import('~icons/tabler/weight.jsx')['default'] + const IconTablerWind: typeof import('~icons/tabler/wind.jsx')['default'] + const IconTablerX: (typeof import("~icons/tabler/x.jsx"))["default"] + const IconTablerZoomFilled: typeof import('~icons/tabler/zoom-filled.jsx')['default'] + const Index: typeof import('solid-js')['Index'] + const Link: typeof import('@solidjs/router')['Link'] + const Match: typeof import('solid-js')['Match'] + const NavLink: typeof import('@solidjs/router')['NavLink'] + const Navigate: typeof import('@solidjs/router')['Navigate'] + const Outlet: typeof import('@solidjs/router')['Outlet'] + const Portal: typeof import('solid-js/web')['Portal'] + const Route: typeof import('@solidjs/router')['Route'] + const Router: typeof import('@solidjs/router')['Router'] + const Routes: typeof import('@solidjs/router')['Routes'] + const Show: typeof import('solid-js')['Show'] + const Suspense: typeof import('solid-js')['Suspense'] + const SuspenseList: typeof import('solid-js')['SuspenseList'] + const Switch: typeof import('solid-js')['Switch'] + const _mergeSearchString: typeof import('@solidjs/router')['_mergeSearchString'] + const batch: typeof import('solid-js')['batch'] + const children: typeof import('solid-js')['children'] + const createContext: typeof import('solid-js')['createContext'] + const createDeferred: typeof import('solid-js')['createDeferred'] + const createEffect: typeof import('solid-js')['createEffect'] + const createIntegration: typeof import('@solidjs/router')['createIntegration'] + const createMemo: typeof import('solid-js')['createMemo'] + const createMutable: typeof import('solid-js/store')['createMutable'] + const createRenderEffect: typeof import('solid-js')['createRenderEffect'] + const createResource: typeof import('solid-js')['createResource'] + const createRoot: typeof import('solid-js')['createRoot'] + const createSelector: typeof import('solid-js')['createSelector'] + const createSignal: typeof import('solid-js')['createSignal'] + const createStore: typeof import('solid-js/store')['createStore'] + const hashIntegration: typeof import('@solidjs/router')['hashIntegration'] + const hydrate: typeof import('solid-js/web')['hydrate'] + const indexArray: typeof import('solid-js')['indexArray'] + const isServer: typeof import('solid-js/web')['isServer'] + const lazy: typeof import('solid-js')['lazy'] + const mapArray: typeof import('solid-js')['mapArray'] + const mergeProps: typeof import('solid-js')['mergeProps'] + const normalizeIntegration: typeof import('@solidjs/router')['normalizeIntegration'] + const observable: typeof import('solid-js')['observable'] + const on: typeof import('solid-js')['on'] + const onCleanup: typeof import('solid-js')['onCleanup'] + const onError: typeof import('solid-js')['onError'] + const onMount: typeof import('solid-js')['onMount'] + const pathIntegration: typeof import('@solidjs/router')['pathIntegration'] + const produce: typeof import('solid-js/store')['produce'] + const reconcile: typeof import('solid-js/store')['reconcile'] + const render: typeof import('solid-js/web')['render'] + const renderToStream: typeof import('solid-js/web')['renderToStream'] + const renderToString: typeof import('solid-js/web')['renderToString'] + const renderToStringAsync: typeof import('solid-js/web')['renderToStringAsync'] + const splitProps: typeof import('solid-js')['splitProps'] + const staticIntegration: typeof import('@solidjs/router')['staticIntegration'] + const untrack: typeof import('solid-js')['untrack'] + const useContext: typeof import('solid-js')['useContext'] + const useHref: typeof import('@solidjs/router')['useHref'] + const useIsRouting: typeof import('@solidjs/router')['useIsRouting'] + const useLocation: typeof import('@solidjs/router')['useLocation'] + const useMatch: typeof import('@solidjs/router')['useMatch'] + const useNavigate: typeof import('@solidjs/router')['useNavigate'] + const useParams: typeof import('@solidjs/router')['useParams'] + const useResolvedPath: typeof import('@solidjs/router')['useResolvedPath'] + const useRouteData: typeof import('@solidjs/router')['useRouteData'] + const useRoutes: typeof import('@solidjs/router')['useRoutes'] + const useSearchParams: typeof import('@solidjs/router')['useSearchParams'] + const useTransition: typeof import('solid-js')['useTransition'] +} diff --git a/app/src/types/lightweight-charts.d.ts b/app/src/types/lightweight-charts.d.ts new file mode 100644 index 000000000..c41d0e8e4 --- /dev/null +++ b/app/src/types/lightweight-charts.d.ts @@ -0,0 +1,65 @@ +type IChartApi = import("lightweight-charts").IChartApi; +type SeriesType = import("lightweight-charts").SeriesType; +type ISeriesApi<T extends SeriesType> = + import("lightweight-charts").ISeriesApi<T>; +type SeriesOptionsMap = import("lightweight-charts").SeriesOptionsMap; +type ISeriesApiAny = ISeriesApi<keyof SeriesOptionsMap>; +type IPriceLine = import("lightweight-charts").IPriceLine; +type ChartOptions = import("lightweight-charts").ChartOptions; +type DeepPartial<T> = import("lightweight-charts").DeepPartial<T>; +type SeriesOptionsCommon = import("lightweight-charts").SeriesOptionsCommon; +type AreaStyleOptions = import("lightweight-charts").AreaStyleOptions; +type BarStyleOptions = import("lightweight-charts").BarStyleOptions; +type BaselineStyleOptions = import("lightweight-charts").BaselineStyleOptions; +type CandlestickStyleOptions = + import("lightweight-charts").CandlestickStyleOptions; +type HistogramStyleOptions = import("lightweight-charts").HistogramStyleOptions; +type LineStyleOptions = import("lightweight-charts").LineStyleOptions; +type SeriesStylesOptions = DeepPartial< + ( + | AreaStyleOptions + | BarStyleOptions + | BaselineStyleOptions + | CandlestickStyleOptions + | HistogramStyleOptions + | LineStyleOptions + ) & + SeriesOptionsCommon +>; +type WhitespaceData = import("lightweight-charts").WhitespaceData; +type SingleValueData = import("lightweight-charts").SingleValueData; +type CandlestickData = import("lightweight-charts").CandlestickData; + +type Time = import("lightweight-charts").Time; +type BusinessDay = import("lightweight-charts").BusinessDay; +type SeriesMarker<T> = import("lightweight-charts").SeriesMarker<T>; +type Time = import("lightweight-charts").Time; +type TimeRange = import("lightweight-charts").Range<Time>; +type LogicalRange = import("lightweight-charts").LogicalRange; +type AutoscaleInfo = import("lightweight-charts").AutoscaleInfo; +type BarPrice = import("lightweight-charts").BarPrice; +type MouseEventHandler<HorzScaleItem> = + import("lightweight-charts").MouseEventHandler<HorzScaleItem>; +type MouseEventParams = import("lightweight-charts").MouseEventParams; +type PriceLineOptions = import("lightweight-charts").PriceLineOptions; +type AutoscaleInfoProvider = import("lightweight-charts").AutoscaleInfoProvider; +type PriceScaleOptions = import("lightweight-charts").PriceScaleOptions; +type LogicalRangeChangeEventHandler = + import("lightweight-charts").LogicalRangeChangeEventHandler; +type LineData = import("lightweight-charts").LineData; +type AreaData = import("lightweight-charts").AreaData; +type HistogramData = import("lightweight-charts").HistogramData; + +type DeepPartialLineOptions = DeepPartial< + LineStyleOptions & SeriesOptionsCommon +>; + +type DeepPartialHistogramOptions = DeepPartial< + HistogramStyleOptions & SeriesOptionsCommon +>; + +type DeepPartialBaselineOptions = DeepPartial< + BaselineStyleOptions & SeriesOptionsCommon +>; + +type DeepPartialChartOptions = DeepPartial<ChartOptions>; diff --git a/app/src/types/self.d.ts b/app/src/types/self.d.ts new file mode 100644 index 000000000..09a953e10 --- /dev/null +++ b/app/src/types/self.d.ts @@ -0,0 +1,17 @@ +interface Dated { + date: string; +} + +interface Heighted { + height: number; +} + +interface Numbered { + number: number; +} + +interface Valued { + value: number; +} + +type DatasetCandlestickData = DatasetValue<CandlestickData>; diff --git a/app/tailwind.config.ts b/app/tailwind.config.ts new file mode 100644 index 000000000..71ad43a98 --- /dev/null +++ b/app/tailwind.config.ts @@ -0,0 +1,23 @@ +import containerQueries from "@tailwindcss/container-queries"; +import { type Config } from "tailwindcss"; +import defaultTheme from "tailwindcss/defaultTheme"; + +export default { + content: ["./src/**/*.{html,js,jsx,ts,tsx}", "./index.html"], + darkMode: "class", + future: { + hoverOnlyWhenSupported: true, + }, + theme: { + extend: { + fontFamily: { + sans: ["Lexend", ...defaultTheme.fontFamily.sans], + }, + screens: { + md: "720px", + "2xl": "1600px", + }, + }, + }, + plugins: [containerQueries], +} satisfies Config; diff --git a/app/tsconfig.json b/app/tsconfig.json new file mode 100644 index 000000000..64b357cd3 --- /dev/null +++ b/app/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "strict": true, + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "jsx": "preserve", + "skipLibCheck": true, + "jsxImportSource": "solid-js", + "types": [ + "vite/client", + "vite-plugin-pwa/client", + "vite-plugin-pwa/pwa-assets", + "vite-plugin-pwa/solid" + ], + "noEmit": true, + "isolatedModules": true, + "baseUrl": "./", + "resolveJsonModule": true, + "paths": { + "/src/*": ["src/*"] + } + }, + "exclude": ["dist", "node_modules"] +} diff --git a/app/vite.config.ts b/app/vite.config.ts new file mode 100644 index 000000000..87a308885 --- /dev/null +++ b/app/vite.config.ts @@ -0,0 +1,72 @@ +// @ts-ignore +import { fileURLToPath } from "url"; +import autoprefixer from "autoprefixer"; +import { visualizer } from "rollup-plugin-visualizer"; +import tailwindcss from "tailwindcss"; +import unpluginAutoImport from "unplugin-auto-import/vite"; +import unpluginIconsResolver from "unplugin-icons/resolver"; +import unpluginIcons from "unplugin-icons/vite"; +import { defineConfig } from "vite"; +import { VitePWA } from "vite-plugin-pwa"; +import solidPlugin from "vite-plugin-solid"; + +export default defineConfig({ + plugins: [ + solidPlugin(), + + VitePWA({ + injectRegister: false, + workbox: { + skipWaiting: true, + clientsClaim: true, + cleanupOutdatedCaches: true, + globPatterns: ["**/*.{js,css,html,ico,png,svg,json,woff2,ttf,md}"], + }, + manifest: false, + }), + + unpluginAutoImport({ + imports: ["solid-js"], + dts: "./src/types/auto-imports.d.ts", + resolvers: [ + unpluginIconsResolver({ + prefix: "Icon", + extension: "jsx", + }), + ], + }), + + unpluginIcons({ autoInstall: true, compiler: "solid" }), + + visualizer({ + template: "treemap", + filename: "./visualizer/treemap.html", + }), + + visualizer({ + template: "network", + filename: "./visualizer/network.html", + }), + + visualizer({ + template: "sunburst", + filename: "./visualizer/sunburst.html", + }), + ], + server: { + port: 3000, + }, + build: { + target: "esnext", + }, + resolve: { + alias: { + "/src": fileURLToPath(new URL("./src", import.meta.url)), + }, + }, + css: { + postcss: { + plugins: [autoprefixer(), tailwindcss()], + }, + }, +}); diff --git a/maintainers.yaml b/maintainers.yaml new file mode 100644 index 000000000..5155afdd7 --- /dev/null +++ b/maintainers.yaml @@ -0,0 +1,4 @@ +maintainers: +- npub1jagmm3x39lmwfnrtvxcs9ac7g300y3dusv9lgzhk2e4x5frpxlrqa73v44 +relays: +- '?' diff --git a/parser/.gitignore b/parser/.gitignore new file mode 100644 index 000000000..babb5c38e --- /dev/null +++ b/parser/.gitignore @@ -0,0 +1,15 @@ +.DS_Store +/target +/.vscode +/.zed + +flamegraph/ +flamegraph.svg +/profile.json + +/inputs*/ +/outputs*/ +/snapshots*/ +/exports*/ +/imports*/ +benches diff --git a/parser/Cargo.lock b/parser/Cargo.lock new file mode 100644 index 000000000..90d33d786 --- /dev/null +++ b/parser/Cargo.lock @@ -0,0 +1,2334 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocative" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "082af274fd02beef17b7f0725a49ecafe6c075ef56cac9d6363eb3916a9817ae" +dependencies = [ + "allocative_derive", + "ctor", +] + +[[package]] +name = "allocative_derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe233a377643e0fc1a56421d7c90acdec45c291b30345eb9f08e8d0ddce5a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base58ck" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" +dependencies = [ + "bitcoin-internals", + "bitcoin_hashes", +] + +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + +[[package]] +name = "bech32" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" + +[[package]] +name = "bincode" +version = "2.0.0-rc.3" +source = "git+https://github.com/bincode-org/bincode.git#100685bc28fd3df957d622e7007d7293a3ca2b0b" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.0-rc.3" +source = "git+https://github.com/bincode-org/bincode.git#100685bc28fd3df957d622e7007d7293a3ca2b0b" +dependencies = [ + "virtue", +] + +[[package]] +name = "bitcoin" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea507acc1cd80fc084ace38544bbcf7ced7c2aa65b653b102de0ce718df668f6" +dependencies = [ + "base58ck", + "bech32", + "bitcoin-internals", + "bitcoin-io", + "bitcoin-units", + "bitcoin_hashes", + "hex-conservative", + "hex_lit", + "secp256k1", + "serde", +] + +[[package]] +name = "bitcoin-internals" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" +dependencies = [ + "serde", +] + +[[package]] +name = "bitcoin-io" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "340e09e8399c7bd8912f495af6aa58bea0c9214773417ffaa8f6460f93aaee56" + +[[package]] +name = "bitcoin-units" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb54da0b28892f3c52203a7191534033e051b6f4b52bc15480681b57b7e036f5" +dependencies = [ + "bitcoin-internals", + "serde", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +dependencies = [ + "bitcoin-io", + "hex-conservative", + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "bumpalo" +version = "3.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" + +[[package]] +name = "bytemuck" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" + +[[package]] +name = "cc" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.4", +] + +[[package]] +name = "clap" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_derive" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + +[[package]] +name = "color-eyre" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + +[[package]] +name = "condtype" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core 0.9.10", +] + +[[package]] +name = "db-key" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b72465f46d518f6015d9cf07f7f3013a95dd6b9c2747c3d65ae0cce43929d14f" + +[[package]] +name = "derive_deref" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcdbcee2d9941369faba772587a565f4f534e42cb8d17e5295871de730163b2b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "divan" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d567df2c9c2870a43f3f2bd65aaeb18dbce1c18f217c3e564b4fbaeb3ee56c" +dependencies = [ + "cfg-if", + "clap", + "condtype", + "divan-macros", + "libc", + "regex-lite", +] + +[[package]] +name = "divan-macros" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27540baf49be0d484d8f0130d7d8da3011c32a44d4fc873368154f1510e574a2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "either" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "ffi-opaque" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec54ac60a7f2ee9a97cad9946f9bf629a3bc6a7ae59e68983dc9318f5a54b81a" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "h2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex-conservative" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1aa273bf451e37ed35ced41c71a5e2a4e29064afb104158f2514bcd71c2c986" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "hyper" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inferno" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321f0f839cd44a4686e9504b0a62b4d69a50b62072144c71c68f5873c167b8d9" +dependencies = [ + "ahash", + "clap", + "crossbeam-channel", + "crossbeam-utils", + "dashmap", + "env_logger", + "indexmap", + "is-terminal", + "itoa", + "log", + "num-format", + "once_cell", + "quick-xml", + "rgb", + "str_stack", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "is-terminal" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "leveldb" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32651baaaa5596b3a6e0bee625e73fd0334c167db0ea5ac68750ef9a629a2d6a" +dependencies = [ + "db-key", + "leveldb-sys", + "libc", +] + +[[package]] +name = "leveldb-sys" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd94a4d0242a437e5e41a27c782b69a624469ca1c4d1e5cb3c337f74a8031d4" +dependencies = [ + "cmake", + "ffi-opaque", + "libc", + "num_cpus", +] + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "memmap2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +dependencies = [ + "libc", +] + +[[package]] +name = "memory-stats" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34f79cf9964c5c9545493acda1263f1912f8d2c56c8a2ffee2606cb960acaacc" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nohash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0f889fb66f7acdf83442c35775764b51fed3c606ab9cee51500dbde2cf528ca" + +[[package]] +name = "num-format" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" +dependencies = [ + "arrayvec", + "itoa", +] + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl" +version = "0.10.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-float" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76df7075c7d4d01fdcb46c912dd17fba5b60c78ea480b475f2b6ab6f666584e" +dependencies = [ + "num-traits", +] + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "par-iter-sync" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa981aaed94bf59211f644922155e8a33bcb01ed662cd63426653187f562790" +dependencies = [ + "crossbeam", + "num_cpus", +] + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.1", + "smallvec", + "windows-targets 0.52.4", +] + +[[package]] +name = "parser" +version = "0.1.0" +dependencies = [ + "allocative", + "bincode", + "bitcoin", + "bitcoin_hashes", + "byteorder", + "chrono", + "color-eyre", + "db-key", + "derive_deref", + "divan", + "fastrand", + "inferno", + "itertools", + "leveldb", + "memory-stats", + "nohash", + "ordered-float", + "par-iter-sync", + "rayon", + "reqwest", + "sanakirja", + "serde", + "serde_json", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "proc-macro2" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +dependencies = [ + "bitflags 2.5.0", +] + +[[package]] +name = "regex-lite" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b661b2f27137bdbc16f00eda72866a92bb28af1753ffbd56744fb6e2e9cd8e" + +[[package]] +name = "reqwest" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rgb" +version = "0.8.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.38.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebbbdb961df0ad3f2652da8f3fdc4b36122f568f968f45ad3316f26c025c677b" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" + +[[package]] +name = "rustls-webpki" +version = "0.102.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "sanakirja" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "450d6e757837c485e85fe8d5bd7aae9592da139a55036a4f64cec2b9984c6953" +dependencies = [ + "fs2", + "log", + "memmap2", + "parking_lot", + "sanakirja-core", + "serde", + "thiserror", +] + +[[package]] +name = "sanakirja-core" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8376db34ae3eac6e7bd91168bc638450073b708ce9fb46940de676f552238bf5" + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secp256k1" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0cc0f1cf93f4969faf3ea1c7d8a9faed25918d96affa959720823dfe86d4f3" +dependencies = [ + "bitcoin_hashes", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1433bd67156263443f14d603720b082dd3121779323fce20cba2aa07b874bc1b" +dependencies = [ + "cc", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "serde_json" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "str_stack" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "terminal_size" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +dependencies = [ + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "thiserror" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "unty" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a88342087869553c259588a3ec9ca73ce9b2d538b7051ba5789ff236b6c129" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "virtue" +version = "0.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b6826a786a78cf1bb0937507b5551fb6f827d66269a24b00af0de247b19bbc7" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.66", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/parser/Cargo.toml b/parser/Cargo.toml new file mode 100644 index 000000000..dea57ebe2 --- /dev/null +++ b/parser/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "parser" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +allocative = "0.3.3" +bincode = { git = "https://github.com/bincode-org/bincode.git" } +bitcoin = { version = "0.32.2", features = ["serde"] } +bitcoin_hashes = { version = "0.14.0" } +byteorder = "1.5.0" +chrono = { version = "0.4.38", features = ["serde"] } +color-eyre = "0.6.3" +db-key = "=0.0.5" +derive_deref = "1.1.1" +divan = "0.1.14" +fastrand = "2.1.0" +inferno = "0.11.19" +itertools = "0.13.0" +leveldb = "0.8.6" +memory-stats = "1.1.0" +nohash = "0.2.0" +ordered-float = "4.2.0" +par-iter-sync = "0.1.11" +rayon = "1.10.0" +reqwest = { version = "0.12.5", features = ["blocking", "json"] } +sanakirja = "1.4.2" +serde = { version = "1.0.203", features = ["derive"] } +serde_json = "1.0.117" diff --git a/parser/README.md b/parser/README.md new file mode 100644 index 000000000..73b723354 --- /dev/null +++ b/parser/README.md @@ -0,0 +1,27 @@ +# Satonomics - Parser + +## Description + +The backbone of the project, it does most of the work by parsing and then computing datasets from the timechain + +## Requirements + +- `rustup` + +## Run + +```bash +# Update ./run.sh with the path to your bitcoin folder +./run.sh +``` + +## Limitations + +- Needs to stop the node to parse the files (at least for now) +- Needs a **LOT** a disk space for databases (~700 GB for data from 2009 to mid 2024) + +## Guidelines + +- Avoid floats as much as possible + - Use structs like `WAmount` and `Price` for calculations + - **Only** use `WAmount.to_btc()` when inserting or computing inside a dataset. It is **very** expensive. diff --git a/parser/run.sh b/parser/run.sh new file mode 100755 index 000000000..c2ca57daa --- /dev/null +++ b/parser/run.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# https://stackoverflow.com/questions/31389483/find-and-delete-file-or-folder-older-than-x-days + +# For Mac OS users +if [ "$(uname)" == "Darwin" ]; then + echo "Increasing limit of opened files..." + ulimit -n 1000000 # Can't be $(ulimit -Hn), bitcoind needs some too ! + + # Needed because the datasets tree is too big lol + echo "Increasing stack size..." + ulimit -s $(ulimit -Hs) + + if mdutil -s / | grep "enabled"; then + echo "Disabling spotlight indexing..." + sudo mdutil -a -i off &> /dev/null + fi + + echo "Cleaning local TimeMachine snapshots..." + # If not enough: tmutil thinlocalsnapshots / 500000000000 4 + tmutil thinlocalsnapshots / &> /dev/null +fi + +# Update path +cargo run -r -- "$HOME/Developer/bitcoin" diff --git a/parser/samply.sh b/parser/samply.sh new file mode 100755 index 000000000..fb6936334 --- /dev/null +++ b/parser/samply.sh @@ -0,0 +1,8 @@ +echo "Increasing limit of opened files..." +ulimit -n $(ulimit -Hn) + +# Needed because the datasets tree is too big lol +echo "Increasing stack size..." +ulimit -s $(ulimit -Hs) + +cargo build --profile profiling && samply record ./target/profiling/parser "$HOME/Developer/bitcoin" diff --git a/parser/src/actions/export.rs b/parser/src/actions/export.rs new file mode 100644 index 000000000..8906eeaae --- /dev/null +++ b/parser/src/actions/export.rs @@ -0,0 +1,47 @@ +use std::thread; + +use crate::{ + databases::Databases, + datasets::AllDatasets, + states::States, + structs::WNaiveDate, + utils::{log, time}, +}; + +pub struct ExportedData<'a> { + pub databases: Option<&'a mut Databases>, + pub datasets: &'a mut AllDatasets, + pub date: WNaiveDate, + pub height: usize, + pub states: Option<&'a States>, +} + +pub fn export( + ExportedData { + databases, + datasets, + states, + height, + date, + }: ExportedData, +) -> color_eyre::Result<()> { + log("Exporting... (Don't close !!)"); + + time("Total save time", || -> color_eyre::Result<()> { + time("Datasets saved", || datasets.export())?; + + thread::scope(|s| { + if let Some(databases) = databases { + s.spawn(|| time("Databases saved", || databases.export(height, date))); + } + + if let Some(states) = states { + s.spawn(|| time("States saved", || states.export())); + } + }); + + Ok(()) + })?; + + Ok(()) +} diff --git a/parser/src/actions/iter_blocks.rs b/parser/src/actions/iter_blocks.rs new file mode 100644 index 000000000..1e6452969 --- /dev/null +++ b/parser/src/actions/iter_blocks.rs @@ -0,0 +1,199 @@ +use std::{collections::BTreeSet, time::Instant}; + +use chrono::Datelike; +use export::ExportedData; +use itertools::Itertools; + +use parse::ParseData; + +use crate::{ + actions::{export, find_first_inserted_unsafe_height, parse}, + bitcoin::{check_if_height_safe, BitcoinDB, NUMBER_OF_UNSAFE_BLOCKS}, + databases::Databases, + datasets::{AllDatasets, ComputeData}, + states::States, + structs::{DateData, WNaiveDate}, + utils::{generate_allocation_files, log, time}, +}; + +pub fn iter_blocks(bitcoin_db: &BitcoinDB, block_count: usize) -> color_eyre::Result<()> { + let should_insert = true; + let should_export = true; + let study_ram_usage = false; + + log("Starting..."); + + let mut datasets = AllDatasets::import()?; + // RAM: 200MB at this point + + log("Imported datasets"); + + let mut databases = Databases::import(); + // RAM: 200MB too + + log("Imported databases"); + + let mut states = + States::import(&mut databases.address_index_to_address_data, &datasets).unwrap_or_default(); + + log("Imported states"); + + let first_unsafe_heights = + find_first_inserted_unsafe_height(&mut states, &mut databases, &mut datasets); + + let mut height = first_unsafe_heights.min(); + + log(&format!("Starting parsing at height: {height}")); + + let mut block_iter = bitcoin_db.iter_block(height, block_count); + + let mut next_block_opt = None; + let mut blocks_loop_date = None; + + 'parsing: loop { + let instant = Instant::now(); + + let mut processed_heights = BTreeSet::new(); + let mut processed_dates = BTreeSet::new(); + + 'days: loop { + let mut blocks_loop_i = 0; + + if next_block_opt.is_some() { + blocks_loop_date.take(); + } + + 'blocks: loop { + let current_block_opt = next_block_opt.take().or_else(|| block_iter.next()); + + next_block_opt = block_iter.next(); + + if let Some(current_block) = current_block_opt { + let timestamp = current_block.header.time; + + let current_block_date = WNaiveDate::from_timestamp(timestamp); + let current_block_height = height + blocks_loop_i; + + let next_block_date = next_block_opt + .as_ref() + .map(|next_block| WNaiveDate::from_timestamp(next_block.header.time)); + + // Always run for the first block of the loop + if blocks_loop_date.is_none() { + blocks_loop_date.replace(current_block_date); + + if states + .date_data_vec + .last() + .map(|date_data| *date_data.date < *current_block_date) + .unwrap_or(true) + { + states + .date_data_vec + .push(DateData::new(current_block_date, vec![])); + } + + log(&format!( + "Processing {current_block_date} (height: {height})..." + )); + } + + let blocks_loop_date = blocks_loop_date.unwrap(); + + if current_block_date > blocks_loop_date { + panic!("current block should always have the same date as the current blocks loop"); + } + + let is_date_last_block = next_block_date + // Do NOT change `blocks_loop_date` to `current_block_date` !!! + .map_or(true, |next_block_date| blocks_loop_date < next_block_date); + + processed_heights.insert(current_block_height); + + if should_insert && first_unsafe_heights.inserted <= current_block_height { + let compute_addresses = databases.check_if_needs_to_compute_addresses( + current_block_height, + blocks_loop_date, + ); + + parse(ParseData { + bitcoin_db, + block: current_block, + block_index: blocks_loop_i, + compute_addresses, + databases: &mut databases, + datasets: &mut datasets, + date: blocks_loop_date, + first_date_height: height, + height: current_block_height, + is_date_last_block, + states: &mut states, + timestamp, + }); + } + + blocks_loop_i += 1; + + if is_date_last_block { + processed_dates.insert(blocks_loop_date); + + height += blocks_loop_i; + + let is_new_month = next_block_date + .map_or(true, |next_block_date| next_block_date.day() == 1); + + let is_close_to_the_end = + height > (block_count - (NUMBER_OF_UNSAFE_BLOCKS * 3)); + + if is_new_month || is_close_to_the_end { + break 'days; + } + + break 'blocks; + } + } else { + break 'parsing; + } + } + } + + // Don't remember why -1 + let last_height = height - 1; + + log(&format!( + "Parsing month took {} seconds (last height: {last_height})\n", + instant.elapsed().as_secs_f32(), + )); + + if first_unsafe_heights.computed <= last_height { + datasets.compute(ComputeData { + dates: &processed_dates.into_iter().collect_vec(), + heights: &processed_heights.into_iter().collect_vec(), + }); + } + + if should_export { + let is_safe = check_if_height_safe(height, block_count); + + export(ExportedData { + databases: is_safe.then_some(&mut databases), + datasets: &mut datasets, + date: blocks_loop_date.unwrap(), + height: last_height, + states: is_safe.then_some(&states), + })?; + + if study_ram_usage { + time("Exporing allocation files", || { + generate_allocation_files(&datasets, &databases, &states, last_height) + })?; + } + } else { + log("Skipping export"); + } + + println!(); + } + + Ok(()) +} diff --git a/parser/src/actions/min_height.rs b/parser/src/actions/min_height.rs new file mode 100644 index 000000000..4a3e403ae --- /dev/null +++ b/parser/src/actions/min_height.rs @@ -0,0 +1,127 @@ +use crate::{ + databases::Databases, + datasets::{AllDatasets, AnyDatasets}, + states::States, + utils::log, +}; + +#[derive(Default, Debug)] +pub struct Heights { + pub inserted: usize, + pub computed: usize, +} + +impl Heights { + pub fn min(&self) -> usize { + self.inserted.min(self.computed) + } +} + +pub fn find_first_inserted_unsafe_height( + states: &mut States, + databases: &mut Databases, + datasets: &mut AllDatasets, +) -> Heights { + let min_initial_inserted_last_address_height = datasets + .address + .get_min_initial_states() + .inserted + .last_height + .as_ref() + .cloned(); + + let min_initial_inserted_last_address_date = datasets + .address + .get_min_initial_states() + .inserted + .last_date + .as_ref() + .cloned(); + + let usable_databases = databases.check_if_usable( + min_initial_inserted_last_address_height, + min_initial_inserted_last_address_date, + ); + + states + .date_data_vec + .iter() + .last() + .map(|date_data| date_data.date) + .and_then(|last_safe_date| { + if !usable_databases { + log("Unusable databases"); + + return None; + } + + let datasets_min_initial_states = datasets.get_min_initial_states().to_owned(); + + let min_datasets_inserted_last_height = datasets_min_initial_states.inserted.last_height; + let min_datasets_inserted_last_date = datasets_min_initial_states.inserted.last_date; + + log(&format!("min_datasets_inserted_last_height: {:?}", min_datasets_inserted_last_height)); + log(&format!("min_datasets_inserted_last_date: {:?}", min_datasets_inserted_last_date)); + + let inserted_last_date_is_older_than_saved_state = min_datasets_inserted_last_date.map_or(true, |min_datasets_last_date| min_datasets_last_date < last_safe_date); + + if inserted_last_date_is_older_than_saved_state { + dbg!(min_datasets_inserted_last_date , *last_safe_date); + + return None; + } + + datasets + .date_metadata + .last_height + .get_or_import(&last_safe_date) + .and_then(|last_safe_height| { + let inserted_heights_and_dates_are_out_of_sync = min_datasets_inserted_last_height.map_or(true, |min_datasets_inserted_last_height| min_datasets_inserted_last_height < last_safe_height); + + if inserted_heights_and_dates_are_out_of_sync { + log(&format!("last_safe_height ({last_safe_height}) > min_datasets_height ({min_datasets_inserted_last_height:?})")); + + None + } else { + let computed = datasets_min_initial_states.computed.last_date.and_then( + |last_date| datasets.date_metadata + .last_height + .get(&last_date) + .and_then(|last_date_height| { + if datasets_min_initial_states.computed.last_height.map_or(true, |last_height| { + last_height < last_date_height + }) { + None + } else { + Some(last_date_height + 1) + } + }) + ).unwrap_or_default(); + + Some(Heights { + inserted: last_safe_height + 1, + computed, + }) + } + } + ) + }) + .unwrap_or_else(|| { + log("Starting over..."); + + let include_addresses = !usable_databases + || min_initial_inserted_last_address_date.is_none() + || min_initial_inserted_last_address_height.is_none(); + + // if include_addresses { + // dbg!(include_addresses); + // panic!(""); + // } + + states.reset(include_addresses); + + databases.reset(include_addresses); + + Heights::default() + }) +} diff --git a/parser/src/actions/mod.rs b/parser/src/actions/mod.rs new file mode 100644 index 000000000..a622ed15b --- /dev/null +++ b/parser/src/actions/mod.rs @@ -0,0 +1,9 @@ +mod export; +mod iter_blocks; +mod min_height; +mod parse; + +pub use export::*; +pub use iter_blocks::*; +pub use min_height::*; +pub use parse::*; diff --git a/parser/src/actions/parse.rs b/parser/src/actions/parse.rs new file mode 100644 index 000000000..7cacda1d6 --- /dev/null +++ b/parser/src/actions/parse.rs @@ -0,0 +1,981 @@ +use std::{collections::BTreeMap, ops::ControlFlow, thread}; + +use bitcoin::{Block, Txid}; + +use itertools::Itertools; +use rayon::prelude::*; + +use crate::{ + bitcoin::BitcoinDB, + databases::{ + AddressIndexToAddressData, AddressIndexToEmptyAddressData, AddressToAddressIndex, + Databases, TxidToTxData, TxoutIndexToAddressIndex, TxoutIndexToAmount, + }, + datasets::{AllDatasets, InsertData}, + states::{ + AddressCohortsInputStates, AddressCohortsOutputStates, AddressCohortsRealizedStates, + States, UTXOCohortsOneShotStates, UTXOCohortsSentStates, + }, + structs::{ + Address, AddressData, AddressRealizedData, BlockData, BlockPath, Counter, EmptyAddressData, + PartialTxoutData, Price, SentData, TxData, TxoutIndex, WAmount, WNaiveDate, + }, +}; + +pub struct ParseData<'a> { + pub bitcoin_db: &'a BitcoinDB, + pub block: Block, + pub block_index: usize, + pub compute_addresses: bool, + pub databases: &'a mut Databases, + pub datasets: &'a mut AllDatasets, + pub date: WNaiveDate, + pub first_date_height: usize, + pub height: usize, + pub is_date_last_block: bool, + pub states: &'a mut States, + pub timestamp: u32, +} + +pub fn parse( + ParseData { + bitcoin_db, + block, + block_index, + compute_addresses, + databases, + datasets, + date, + first_date_height, + height, + is_date_last_block, + states, + timestamp, + }: ParseData, +) { + // If false, expect that the code is flawless + // or create a 0 value txid database + let enable_check_if_txout_value_is_zero_in_db: bool = true; + + let date_index = states.date_data_vec.len() - 1; + + let previous_timestamp = if height > 0 { + Some( + datasets + .block_metadata + .timestamp + .get_or_import(&(height - 1)), + ) + } else { + None + }; + + let block_price = Price::from_dollar( + datasets + .price + .get_height_ohlc(height, timestamp, previous_timestamp) + .unwrap_or_else(|_| panic!("Expect {height} to have a price")) + .close as f64, + ); + + let date_price = Price::from_dollar( + datasets + .price + .get_date_ohlc(date) + .unwrap_or_else(|_| panic!("Expect {date} to have a price")) + .close as f64, + ); + + let difficulty = block.header.difficulty_float(); + let block_size = block.total_size(); + let block_weight = block.weight().to_wu(); + let block_vbytes = block.weight().to_vbytes_floor(); + let block_interval = + previous_timestamp.map_or(0, |previous_timestamp| timestamp - previous_timestamp); + + states + .date_data_vec + .last_mut() + .unwrap() + .blocks + .push(BlockData::new(height as u32, block_price, timestamp)); + + let mut block_path_to_sent_data: BTreeMap<BlockPath, SentData> = BTreeMap::default(); + // let mut received_data: ReceivedData = ReceivedData::default(); + let mut address_index_to_address_realized_data: BTreeMap<u32, AddressRealizedData> = + BTreeMap::default(); + + let mut coinbase = WAmount::ZERO; + let mut satblocks_destroyed = WAmount::ZERO; + let mut satdays_destroyed = WAmount::ZERO; + let mut amount_sent = WAmount::ZERO; + let mut transaction_count = 0; + let mut fees = vec![]; + let mut fees_total = WAmount::ZERO; + + let ( + TxoutsParsingResults { + op_returns: _op_returns, + mut partial_txout_data_vec, + provably_unspendable: _provably_unspendable, + }, + (mut txid_to_tx_data, mut txout_index_to_amount_and_address_index), + ) = thread::scope(|scope| { + let output_handle = scope.spawn(|| { + let mut txouts_parsing_results = pre_process_outputs( + &block, + compute_addresses, + &mut states.address_counters.op_return_addresses, + &mut states.address_counters.push_only_addresses, + &mut states.address_counters.unknown_addresses, + &mut states.address_counters.empty_addresses, + &mut databases.address_to_address_index, + ); + + // Reverse to get in order via pop later + txouts_parsing_results.partial_txout_data_vec.reverse(); + + txouts_parsing_results + }); + + let input_handle = scope.spawn(|| { + pre_process_inputs( + &block, + &mut databases.txid_to_tx_data, + &mut databases.txout_index_to_amount, + &mut databases.txout_index_to_address_index, + compute_addresses, + ) + }); + + (output_handle.join().unwrap(), input_handle.join().unwrap()) + }); + + let mut address_index_to_address_data = compute_addresses.then(|| { + compute_address_index_to_address_data( + &mut databases.address_index_to_address_data, + &mut databases.address_index_to_empty_address_data, + &partial_txout_data_vec, + &txout_index_to_amount_and_address_index, + compute_addresses, + ) + }); + + block + .txdata + .iter() + .enumerate() + .try_for_each(|(block_tx_index, tx)| { + let txid = tx.compute_txid(); + let tx_index = databases.txid_to_tx_data.metadata.serial as u32; + + transaction_count += 1; + + // -- + // outputs + // --- + + let mut utxos = BTreeMap::new(); + let mut spendable_amount = WAmount::ZERO; + + let is_coinbase = tx.is_coinbase(); + + if is_coinbase != (block_tx_index == 0) { + unreachable!(); + } + + let mut inputs_sum = WAmount::ZERO; + let mut outputs_sum = WAmount::ZERO; + + let last_block = states.date_data_vec.last_mut_block().unwrap(); + + // Before `input` to cover outputs being used in the same block as inputs + tx.output + .iter() + .enumerate() + .filter_map(|(vout, tx_out)| { + if vout > (u16::MAX as usize) { + panic!("vout can indeed be bigger than u16::MAX !"); + } + + let amount = WAmount::wrap(tx_out.value); + + if is_coinbase { + coinbase += amount; + } else { + outputs_sum += amount; + } + + partial_txout_data_vec + .pop() + .unwrap() + // None if not worth parsing (empty/op_return/...) + .map(|partial_txout_data| (vout, partial_txout_data)) + }) + .for_each(|(vout, partial_txout_data)| { + let vout = vout as u16; + + let txout_index = TxoutIndex::new(tx_index, vout); + + let PartialTxoutData { + address, + address_index_opt, + amount, + } = partial_txout_data; + + spendable_amount += amount; + + last_block.receive(amount); + + utxos.insert(vout, amount); + + databases + .txout_index_to_amount + .unsafe_insert(txout_index, amount); + + if compute_addresses { + let address = address.unwrap(); + + let address_index_to_address_data = + address_index_to_address_data.as_mut().unwrap(); + + let (address_data, address_index) = { + if let Some(address_index) = address_index_opt.or_else(|| { + databases + .address_to_address_index + .unsafe_get_from_puts(&address) + .cloned() + }) { + let address_data = address_index_to_address_data + .get_mut(&address_index) + .unwrap(); + + (address_data, address_index) + } else { + let address_index = + databases.address_to_address_index.metadata.serial as u32; + + let address_type = address.to_type(); + + if let Some(previous) = databases + .address_to_address_index + .insert(address, address_index) + { + dbg!(previous); + panic!( + "address #{address_index} shouldn't be present during put" + ); + } + + // Checked new + let address_data = address_index_to_address_data + .entry(address_index) + .and_modify(|_| { + panic!("Shouldn't exist"); + }) + // Will always insert, it's to avoid insert + get + .or_insert(AddressData::new(address_type)); + + (address_data, address_index) + } + }; + + // MUST be before received ! + let address_realized_data = address_index_to_address_realized_data + .entry(address_index) + .or_insert_with(|| AddressRealizedData::default(address_data)); + + address_data.receive(amount, block_price); + + address_realized_data.receive(amount); + + databases + .txout_index_to_address_index + .unsafe_insert(txout_index, address_index); + } + }); + + if !utxos.is_empty() { + databases.txid_to_tx_data.insert( + &txid, + TxData::new( + tx_index, + BlockPath::new(date_index as u16, block_index as u16), + utxos.len() as u16, + ), + ); + } + + // --- + // inputs + // --- + + if !is_coinbase { + tx.input.iter().try_for_each(|txin| { + let outpoint = txin.previous_output; + let input_txid = outpoint.txid; + let input_vout = outpoint.vout; + + let remove_tx_data_from_cached_puts = { + let mut is_tx_data_from_cached_puts = false; + + let input_tx_data = txid_to_tx_data + .get_mut(&input_txid) + .unwrap() + .as_mut() + .or_else(|| { + is_tx_data_from_cached_puts = true; + + databases + .txid_to_tx_data + .unsafe_get_mut_from_puts(&input_txid) + }); + + // Can be none because 0 sats inputs happen + // https://mempool.space/tx/f329e55c2de9b821356e6f2c4bba923ea7030cad61120f5ced5d4429f5c86fda#vin=27 + if input_tx_data.is_none() { + if !enable_check_if_txout_value_is_zero_in_db + || bitcoin_db + .check_if_txout_value_is_zero(&input_txid, input_vout as usize) + { + return ControlFlow::Continue::<()>(()); + } + + dbg!((input_txid, txid, tx_index, input_vout)); + panic!("Txid to be in txid_to_tx_data"); + } + + let input_tx_data = input_tx_data.unwrap(); + let input_tx_index = input_tx_data.index; + let input_vout = input_vout as u16; + let input_txout_index = TxoutIndex::new(input_tx_index, input_vout); + + // if input_tx_index == 2516 || input_tx_index == 2490 { + // dbg!(input_tx_index, &input_tx_data.utxos); + // } + + // let input_amount = input_tx_data.utxos.remove(&input_vout); + + let input_amount_and_address_index = databases + .txout_index_to_amount + .remove(&input_txout_index) + .map(|amount| { + ( + amount, + databases + .txout_index_to_address_index + .remove(&input_txout_index), + ) + }) // Remove from cached puts + .or_else(|| { + txout_index_to_amount_and_address_index.remove(&input_txout_index) + }); + + if input_amount_and_address_index.is_none() { + if !enable_check_if_txout_value_is_zero_in_db + || bitcoin_db + .check_if_txout_value_is_zero(&input_txid, input_vout as usize) + { + return ControlFlow::Continue::<()>(()); + } + + dbg!(( + input_txid, + tx_index, + input_tx_index, + input_vout, + input_tx_data, + txid, + )); + panic!("Txout index to be in txout_index_to_txout_value"); + } + + input_tx_data.utxos -= 1; + + let (input_amount, input_address_index) = + input_amount_and_address_index.unwrap(); + + let input_block_path = input_tx_data.block_path; + + let BlockPath { + date_index: input_date_index, + block_index: input_block_index, + } = input_block_path; + + let input_date_data = states + .date_data_vec + .get_mut(input_date_index as usize) + .unwrap_or_else(|| { + dbg!(height, &input_txid, input_block_path, input_date_index); + panic!() + }); + + let input_block_data = input_date_data + .blocks + .get_mut(input_block_index as usize) + .unwrap_or_else(|| { + dbg!( + height, + &input_txid, + input_block_path, + input_date_index, + input_block_index, + ); + panic!() + }); + + input_block_data.send(input_amount); + + inputs_sum += input_amount; + + block_path_to_sent_data + .entry(input_block_path) + .or_default() + .send(input_amount); + + satblocks_destroyed += + input_amount * (height as u64 - input_block_data.height as u64); + + satdays_destroyed += input_amount + * date.signed_duration_since(*input_date_data.date).num_days() as u64; + + if compute_addresses { + let input_address_index = input_address_index.unwrap_or_else(|| { + dbg!( + height, + input_amount, + &input_tx_data, + input_address_index, + input_txout_index, + txid, + input_txid, + input_vout + ); + panic!() + }); + + let address_index_to_address_data = + address_index_to_address_data.as_mut().unwrap(); + + let input_address_data = address_index_to_address_data + .get_mut(&input_address_index) + .unwrap_or_else(|| { + dbg!( + input_address_index, + input_txout_index, + input_txid, + input_vout + ); + panic!(); + }); + + let input_address_realized_data = + address_index_to_address_realized_data + .entry(input_address_index) + .or_insert_with(|| { + AddressRealizedData::default(input_address_data) + }); + + // MUST be after `or_insert_with` + let address_realized_profit_or_loss = input_address_data + .send(input_amount, block_price, input_block_data.price) + .unwrap_or_else(|_| { + dbg!( + input_address_index, + txid, + input_txid, + input_amount, + tx_index, + input_tx_index, + input_vout, + &input_address_data + ); + + panic!() + }); + + input_address_realized_data + .send(input_amount, address_realized_profit_or_loss); + }; + + is_tx_data_from_cached_puts && input_tx_data.is_empty() + }; + + if remove_tx_data_from_cached_puts { + // Pre remove tx_datas that are empty and weren't yet added to the database to avoid having it was in there or not (and thus avoid useless operations) + databases.txid_to_tx_data.remove_from_puts(&input_txid) + } + + ControlFlow::Continue(()) + })?; + } + + amount_sent += inputs_sum; + + let fee = inputs_sum - outputs_sum; + + fees_total += fee; + fees.push(fee); + + ControlFlow::Continue(()) + }); + + if !partial_txout_data_vec.is_empty() { + panic!("partial_txout_data_vec should've been fully consumed"); + } + + txid_to_tx_data.into_iter().for_each(|(txid, tx_data)| { + if let Some(tx_data) = tx_data { + if tx_data.is_empty() { + databases.txid_to_tx_data.remove_from_db(txid); + } else { + databases.txid_to_tx_data.update(txid, tx_data); + } + } + }); + + // if !txin_ordered_tx_datas.is_empty() { + // panic!("txin_ordered_tx_indexes should've been fully consumed"); + // } + + let mut utxo_cohorts_sent_states = UTXOCohortsSentStates::default(); + let mut utxo_cohorts_one_shot_states = UTXOCohortsOneShotStates::default(); + // let mut utxo_cohorts_received_states = UTXOCohortsReceivedStates::default(); + + let mut address_cohorts_input_states = None; + let mut address_cohorts_one_shot_states = None; + let mut address_cohorts_output_states = None; + let mut address_cohorts_realized_states = None; + + // log("Starting heavy work..."); + + thread::scope(|scope| { + scope.spawn(|| { + let previous_last_block_data = states.date_data_vec.second_last_block(); + + if datasets.utxo.needs_durable_states(height, date) { + if let Some(previous_last_block_data) = previous_last_block_data { + block_path_to_sent_data + .iter() + .for_each(|(block_path, sent_data)| { + let block_data = + states.date_data_vec.get_block_data(block_path).unwrap(); + + if block_data.height != height as u32 { + states.utxo_cohorts_durable_states.subtract_moved( + block_data, + sent_data, + previous_last_block_data, + ); + } + }); + } + + let last_block_data = states.date_data_vec.last_block().unwrap(); + + if last_block_data.height != height as u32 { + unreachable!() + } + + states + .date_data_vec + .iter() + .flat_map(|date_data| &date_data.blocks) + .for_each(|block_data| { + states.utxo_cohorts_durable_states.udpate_age_if_needed( + block_data, + last_block_data, + previous_last_block_data, + ); + }); + } + + if datasets.utxo.needs_one_shot_states(height, date) { + utxo_cohorts_one_shot_states = + states.utxo_cohorts_durable_states.compute_one_shot_states( + block_price, + if is_date_last_block { + Some(date_price) + } else { + None + }, + ); + } + }); + + // scope.spawn(|| { + // utxo_cohorts_received_states + // .compute(&states.date_data_vec, block_path_to_received_data); + // }); + + if datasets.utxo.needs_sent_states(height, date) { + scope.spawn(|| { + utxo_cohorts_sent_states.compute( + &states.date_data_vec, + &block_path_to_sent_data, + block_price, + ); + }); + } + + if compute_addresses { + scope.spawn(|| { + let address_index_to_address_data = address_index_to_address_data.as_ref().unwrap(); + + // TODO: Only compute if needed + address_cohorts_realized_states.replace(AddressCohortsRealizedStates::default()); + + // TODO: Only compute if needed + address_cohorts_input_states.replace(AddressCohortsInputStates::default()); + + // TODO: Only compute if needed + address_cohorts_output_states.replace(AddressCohortsOutputStates::default()); + + address_index_to_address_realized_data.iter().for_each( + |(address_index, address_realized_data)| { + let current_address_data = + address_index_to_address_data.get(address_index).unwrap(); + + states + .address_cohorts_durable_states + .iterate(address_realized_data, current_address_data) + .unwrap_or_else(|report| { + dbg!(report.to_string(), address_index); + panic!(); + }); + + if !address_realized_data.initial_address_data.is_empty() { + // Realized == previous amount + // If a whale sent all its sats to another address at a loss, it's the whale that realized the loss not the now empty adress + let liquidity_classification = address_realized_data + .initial_address_data + .compute_liquidity_classification(); + + address_cohorts_realized_states + .as_mut() + .unwrap() + .iterate_realized(address_realized_data, &liquidity_classification) + .unwrap(); + + address_cohorts_input_states + .as_mut() + .unwrap() + .iterate_input(address_realized_data, &liquidity_classification) + .unwrap(); + } + + address_cohorts_output_states + .as_mut() + .unwrap() + .iterate_output( + address_realized_data, + ¤t_address_data.compute_liquidity_classification(), + ) + .unwrap(); + }, + ); + + address_cohorts_one_shot_states.replace( + states + .address_cohorts_durable_states + .compute_one_shot_states( + block_price, + if is_date_last_block { + Some(date_price) + } else { + None + }, + ), + ); + }); + } + }); + + if compute_addresses { + address_index_to_address_data.unwrap().into_iter().for_each( + |(address_index, address_data)| { + if address_data.is_empty() { + databases.address_index_to_empty_address_data.unsafe_insert( + address_index, + EmptyAddressData::from_non_empty(&address_data), + ); + } else { + databases + .address_index_to_address_data + .unsafe_insert(address_index, address_data); + } + }, + ) + } + + datasets.insert(InsertData { + address_cohorts_input_states: &address_cohorts_input_states, + block_size, + block_vbytes, + block_weight, + address_cohorts_one_shot_states: &address_cohorts_one_shot_states, + address_cohorts_realized_states: &address_cohorts_realized_states, + block_interval, + block_price, + coinbase, + compute_addresses, + databases, + date, + date_blocks_range: &(first_date_height..=height), + date_first_height: first_date_height, + difficulty, + fees: &fees, + height, + is_date_last_block, + satblocks_destroyed, + satdays_destroyed, + amount_sent, + states, + timestamp, + transaction_count, + utxo_cohorts_one_shot_states: &utxo_cohorts_one_shot_states, + utxo_cohorts_sent_states: &utxo_cohorts_sent_states, + }); +} + +pub struct TxoutsParsingResults { + partial_txout_data_vec: Vec<Option<PartialTxoutData>>, + provably_unspendable: WAmount, + op_returns: usize, +} + +fn pre_process_outputs( + block: &Block, + compute_addresses: bool, + op_return_addresses: &mut Counter, + push_only_addresses: &mut Counter, + unknown_addresses: &mut Counter, + empty_addresses: &mut Counter, + address_to_address_index: &mut AddressToAddressIndex, +) -> TxoutsParsingResults { + let mut provably_unspendable = WAmount::ZERO; + let mut op_returns = 0; + + let mut partial_txout_data_vec = block + .txdata + .iter() + .flat_map(|tx| &tx.output) + .map(|txout| { + let script = &txout.script_pubkey; + let amount = WAmount::wrap(txout.value); + + // 0 sats outputs are possible and allowed ! + // https://mempool.space/tx/2f2442f68e38b980a6c4cec21e71851b0d8a5847d85208331a27321a9967bbd6 + // https://bitcoin.stackexchange.com/questions/104937/transaction-outputs-with-value-0 + if amount == WAmount::ZERO { + return None; + } + + // Op Return + // https://mempool.space/tx/139c004f477101c468767983536caaeef568613fab9c2ed9237521f5ff530afd + // Provably unspendable https://mempool.space/tx/8a68c461a2473653fe0add786f0ca6ebb99b257286166dfb00707be24716af3a#flow=&vout=0 + if script.is_op_return() { + // TODO: Count fee paid to write said OP_RETURN, beware of coinbase transactions + // For coinbase transactions, count miners + op_returns += 1; + provably_unspendable += amount; + + // return None; + } + // https://mempool.space/tx/8a68c461a2473653fe0add786f0ca6ebb99b257286166dfb00707be24716af3a#flow=&vout=0 + else if script.is_provably_unspendable() { + provably_unspendable += amount; + // return None; + } + + let address_opt = compute_addresses.then(|| { + let address = Address::from( + txout, + op_return_addresses, + push_only_addresses, + unknown_addresses, + empty_addresses, + ); + + address_to_address_index.open_db(&address); + + address + }); + + Some(PartialTxoutData::new(address_opt, amount, None)) + }) + .collect_vec(); + + if compute_addresses { + partial_txout_data_vec + .par_iter_mut() + .for_each(|partial_tx_out_data| { + if let Some(partial_tx_out_data) = partial_tx_out_data { + let address_index_opt = address_to_address_index + .unsafe_get(partial_tx_out_data.address.as_ref().unwrap()) + .cloned(); + + partial_tx_out_data.address_index_opt = address_index_opt; + } + }); + } + + TxoutsParsingResults { + partial_txout_data_vec, + provably_unspendable, + op_returns, + } +} + +#[allow(clippy::type_complexity)] +fn pre_process_inputs<'a>( + block: &'a Block, + txid_to_tx_data_db: &mut TxidToTxData, + txout_index_to_amount_db: &mut TxoutIndexToAmount, + txout_index_to_address_index_db: &mut TxoutIndexToAddressIndex, + compute_addresses: bool, +) -> ( + BTreeMap<&'a Txid, Option<TxData>>, + BTreeMap<TxoutIndex, (WAmount, Option<u32>)>, +) { + let mut txid_to_tx_data: BTreeMap<&Txid, Option<TxData>> = block + .txdata + .iter() + .skip(1) // Skip coinbase transaction + .flat_map(|transaction| &transaction.input) + .fold(BTreeMap::default(), |mut tree, tx_in| { + let txid = &tx_in.previous_output.txid; + + txid_to_tx_data_db.open_db(txid); + + tree.entry(txid).or_default(); + + tree + }); + + let mut tx_datas = txid_to_tx_data + .par_iter() + .map(|(txid, _)| txid_to_tx_data_db.unsafe_get(txid)) + .collect::<Vec<_>>(); + + txid_to_tx_data.values_mut().rev().for_each(|tx_data_opt| { + *tx_data_opt = tx_datas.pop().unwrap().cloned(); + }); + + let txout_index_to_amount_and_address_index = block + .txdata + .iter() + .skip(1) // Skip coinbase transaction + .flat_map(|transaction| &transaction.input) + .flat_map(|tx_in| { + let txid = &tx_in.previous_output.txid; + + if let Some(Some(tx_data)) = txid_to_tx_data.get(txid) { + let txout_index = TxoutIndex::new(tx_data.index, tx_in.previous_output.vout as u16); + + txout_index_to_amount_db.open_db(&txout_index); + + if compute_addresses { + txout_index_to_address_index_db.open_db(&txout_index); + } + + Some(txout_index) + } else { + None + } + }) + .collect_vec() + .into_par_iter() + .flat_map(|txout_index| { + txout_index_to_amount_db + .unsafe_get(&txout_index) + // Will be None if value of utxo is 0 + // https://mempool.space/tx/9d8a0d851c9fb2cdf1c6d9406ce97e19e6911ae3503ab2dd5f38640bacdac996 + // which is used later as input + .map(|amount| { + let address_index = compute_addresses.then(|| { + *txout_index_to_address_index_db + .unsafe_get(&txout_index) + .unwrap() + }); + + (txout_index, (*amount, address_index)) + }) + }) + .collect::<BTreeMap<_, _>>(); + + // No need to call remove, it's being called later in the parse function + // To more easily support removing cached puts + + (txid_to_tx_data, txout_index_to_amount_and_address_index) +} + +fn compute_address_index_to_address_data( + address_index_to_address_data_db: &mut AddressIndexToAddressData, + address_index_to_empty_address_data_db: &mut AddressIndexToEmptyAddressData, + partial_txout_data_vec: &[Option<PartialTxoutData>], + txout_index_to_amount_and_address_index: &BTreeMap<TxoutIndex, (WAmount, Option<u32>)>, + compute_addresses: bool, +) -> BTreeMap<u32, AddressData> { + if !compute_addresses { + return BTreeMap::default(); + } + + let mut address_index_to_address_data = partial_txout_data_vec + .iter() + .flatten() + .flat_map(|partial_txout_data| partial_txout_data.address_index_opt) + .map(|address_index| (address_index, true)) + .chain( + txout_index_to_amount_and_address_index + .values() + .map(|(_, address_index)| (*address_index.as_ref().unwrap(), false)), // False because we assume non zero inputs values + ) + .map(|(address_index, open_empty)| { + address_index_to_address_data_db.open_db(&address_index); + + if open_empty { + address_index_to_empty_address_data_db.open_db(&address_index); + } + + (address_index, AddressData::default()) + }) + .collect::<BTreeMap<_, _>>(); + + address_index_to_address_data + .par_iter_mut() + .for_each(|(address_index, address_data)| { + if let Some(_address_data) = + address_index_to_address_data_db.unsafe_get_from_cache(address_index) + { + _address_data.clone_into(address_data); + } else if let Some(empty_address_data) = + address_index_to_empty_address_data_db.unsafe_get_from_cache(address_index) + { + *address_data = AddressData::from_empty(empty_address_data); + } else if let Some(_address_data) = + address_index_to_address_data_db.unsafe_get_from_db(address_index) + { + _address_data.clone_into(address_data); + } else { + let empty_address_data = address_index_to_empty_address_data_db + .unsafe_get_from_db(address_index) + .unwrap(); + + *address_data = AddressData::from_empty(empty_address_data); + } + }); + + // Parallel unsafe_get + Linear remove = Parallel-ish take + address_index_to_address_data + .iter() + .for_each(|(address_index, address_data)| { + if address_data.is_empty() { + address_index_to_empty_address_data_db.remove(address_index); + } else { + address_index_to_address_data_db.remove(address_index); + } + }); + + address_index_to_address_data +} diff --git a/parser/src/bitcoin/addresses/mod.rs b/parser/src/bitcoin/addresses/mod.rs new file mode 100644 index 000000000..227f1c636 --- /dev/null +++ b/parser/src/bitcoin/addresses/mod.rs @@ -0,0 +1,3 @@ +mod multisig; + +pub use multisig::*; diff --git a/parser/src/bitcoin/addresses/multisig.rs b/parser/src/bitcoin/addresses/multisig.rs new file mode 100644 index 000000000..cc2292cee --- /dev/null +++ b/parser/src/bitcoin/addresses/multisig.rs @@ -0,0 +1,57 @@ +// +// Code from bitcoin-explorer now deprecated +// + +use bitcoin::{ + blockdata::{ + opcodes::all, + script::Instruction::{self, Op, PushBytes}, + }, + Opcode, Script, +}; + +/// +/// Obtain addresses for multisig transactions. +/// +pub fn multisig_addresses(script: &Script) -> Vec<Vec<u8>> { + let ops: Vec<Instruction> = script.instructions().filter_map(|o| o.ok()).collect(); + + // obtain number of keys + let num_keys = { + if let Some(Op(op)) = ops.get(ops.len() - 2) { + decode_from_op_n(op) + } else { + unreachable!() + } + }; + + // read public keys + let mut public_keys = Vec::with_capacity(num_keys as usize); + + for op in ops.iter().skip(1).take(num_keys as usize) { + if let PushBytes(data) = op { + public_keys.push(data.as_bytes().to_vec()); + } else { + unreachable!() + } + } + + public_keys +} + +/// +/// Decode OP_N +/// +/// translated from BitcoinJ: +/// [decodeFromOpN()](https://github.com/bitcoinj/bitcoinj/blob/d3d5edbcbdb91b25de4df3b6ed6740d7e2329efc/core/src/main/java/org/bitcoinj/script/Script.java#L515:L524) +/// +#[inline] +fn decode_from_op_n(op: &Opcode) -> i32 { + if op.eq(&all::OP_PUSHBYTES_0) { + 0 + } else if op.eq(&all::OP_PUSHNUM_NEG1) { + -1 + } else { + op.to_u8() as i32 + 1 - all::OP_PUSHNUM_1.to_u8() as i32 + } +} diff --git a/parser/src/bitcoin/consts.rs b/parser/src/bitcoin/consts.rs new file mode 100644 index 000000000..e2bf0ed55 --- /dev/null +++ b/parser/src/bitcoin/consts.rs @@ -0,0 +1,2 @@ +pub const NUMBER_OF_UNSAFE_BLOCKS: usize = 100; +pub const TARGET_BLOCKS_PER_DAY: usize = 144; diff --git a/parser/src/bitcoin/daemon.rs b/parser/src/bitcoin/daemon.rs new file mode 100644 index 000000000..87794ee58 --- /dev/null +++ b/parser/src/bitcoin/daemon.rs @@ -0,0 +1,122 @@ +use std::{process::Command, thread::sleep, time::Duration}; + +use color_eyre::eyre::eyre; +use serde_json::Value; + +use crate::utils::{log, log_output, retry}; + +struct BlockchainInfo { + pub headers: u64, + pub blocks: u64, +} + +pub struct BitcoinDaemon<'a> { + path: &'a str, +} + +impl<'a> BitcoinDaemon<'a> { + pub fn new(bitcoin_dir_path: &'a str) -> Self { + Self { + path: bitcoin_dir_path, + } + } + + pub fn start(&self) { + sleep(Duration::from_secs(1)); + + let mut command = Command::new("bitcoind"); + + command + .arg(self.datadir_arg()) + .arg("-blocksonly") + .arg("-txindex=1") + .arg("-daemon"); + + // bitcoind -datadir=/Users/k/Developer/bitcoin -blocksonly -txindex=1 -daemon + let output = command + .output() + .expect("bitcoind to be able to properly start"); + + log_output(&output); + } + + pub fn stop(&self) { + // bitcoin-cli -datadir=/Users/k/Developer/bitcoin stop + let output = Command::new("bitcoin-cli") + .arg(self.datadir_arg()) + .arg("stop") + .output() + .unwrap(); + + if output.status.success() { + log_output(&output); + + sleep(Duration::from_secs(15)); + } + } + + pub fn wait_sync(&self) { + while !self.check_if_fully_synced() { + sleep(Duration::from_secs(5)) + } + } + + pub fn wait_for_new_block(&self, last_block_height: usize) { + log("Waiting for new block..."); + + while self.get_blockchain_info().headers as usize == last_block_height { + sleep(Duration::from_secs(5)) + } + } + + pub fn check_if_fully_synced(&self) -> bool { + let BlockchainInfo { blocks, headers } = self.get_blockchain_info(); + + let synced = blocks == headers; + + if synced { + log(&format!("Synced ! ({blocks} blocks)")); + } else { + log(&format!("Syncing... ({} remaining)", headers - blocks)); + } + + synced + } + + fn get_blockchain_info(&self) -> BlockchainInfo { + retry( + || { + // bitcoin-cli -datadir=/Users/k/Developer/bitcoin getblockchaininfo + let output = Command::new("bitcoin-cli") + .arg(self.datadir_arg()) + .arg("getblockchaininfo") + .output()?; + + let output = String::from_utf8_lossy(&output.stdout); + + let json: Value = serde_json::from_str(&output)?; + let json = json.as_object().ok_or(eyre!(""))?; + + let blocks = json + .get("blocks") + .ok_or(eyre!(""))? + .as_u64() + .ok_or(eyre!(""))?; + let headers = json + .get("headers") + .ok_or(eyre!(""))? + .as_u64() + .ok_or(eyre!(""))?; + + Ok(BlockchainInfo { headers, blocks }) + }, + 1, + u64::MAX, + ) + .unwrap() + } + + fn datadir_arg(&self) -> String { + format!("-datadir={}", self.path) + } +} diff --git a/parser/src/bitcoin/db/blk_files.rs b/parser/src/bitcoin/db/blk_files.rs new file mode 100644 index 000000000..4429a63f1 --- /dev/null +++ b/parser/src/bitcoin/db/blk_files.rs @@ -0,0 +1,152 @@ +use std::{ + collections::HashMap, + convert::From, + fs::{self, DirEntry, File}, + io::{self, BufReader, Seek, SeekFrom}, + path::{Path, PathBuf}, +}; + +use bitcoin::{io::Cursor, Block, Transaction}; +use derive_deref::{Deref, DerefMut}; + +use super::{ + errors::{OpError, OpErrorKind, OpResult}, + reader::BlockchainRead, +}; + +/// +/// An index of all blk files found. +/// +#[derive(Debug, Clone, Deref, DerefMut)] +pub struct BlkFiles(HashMap<i32, PathBuf>); + +impl BlkFiles { + /// + /// Construct an index of all blk files. + /// + pub fn new(path: &Path) -> OpResult<Self> { + Ok(Self(Self::scan_path(path)?)) + } + + /// + /// Read a Block from blk file. + /// + #[inline] + pub fn read_raw_block(&self, n_file: i32, offset: u32) -> OpResult<Vec<u8>> { + if let Some(blk_path) = self.get(&n_file) { + let mut r = BufReader::new(File::open(blk_path)?); + r.seek(SeekFrom::Start(offset as u64 - 4))?; + let block_size = r.read_u32()?; + let block = r.read_u8_vec(block_size)?; + Ok(block) + } else { + Err(OpError::from("blk file not found, sync with bitcoin core")) + } + } + + /// + /// Read a Block from blk file. + /// + pub fn read_block(&self, n_file: i32, offset: u32) -> OpResult<Block> { + Cursor::new(self.read_raw_block(n_file, offset)?).read_block() + } + + /// + /// Read a transaction from blk file. + /// + pub fn read_transaction( + &self, + n_file: i32, + n_pos: u32, + n_tx_offset: u32, + ) -> OpResult<Transaction> { + if let Some(blk_path) = self.get(&n_file) { + let mut r = BufReader::new(File::open(blk_path)?); + // the size of a header is 80. + r.seek(SeekFrom::Start(n_pos as u64 + n_tx_offset as u64 + 80))?; + r.read_transaction() + } else { + Err(OpError::from("blk file not found, sync with bitcoin core")) + } + } + + /// + /// Scan blk folder to build an index of all blk files. + /// + fn scan_path(path: &Path) -> OpResult<HashMap<i32, PathBuf>> { + let mut collected = HashMap::with_capacity(4000); + + for entry in fs::read_dir(path)? { + match entry { + Ok(de) => { + let path = Self::resolve_path(&de)?; + if !path.is_file() { + continue; + }; + if let Some(file_name) = path.as_path().file_name() { + if let Some(file_name) = file_name.to_str() { + if let Some(index) = Self::parse_blk_index(file_name) { + collected.insert(index, path); + } + } + } + } + Err(msg) => { + return Err(OpError::from(msg)); + } + } + } + + collected.shrink_to_fit(); + + if collected.is_empty() { + Err(OpError::new(OpErrorKind::RuntimeError).join_msg("No blk files found!")) + } else { + Ok(collected) + } + } + + /// + /// Resolve symlink. + /// + fn resolve_path(entry: &DirEntry) -> io::Result<PathBuf> { + if entry.file_type()?.is_symlink() { + fs::read_link(entry.path()) + } else { + Ok(entry.path()) + } + } + + /// + /// Extract index from block file name. + /// + fn parse_blk_index(file_name: &str) -> Option<i32> { + let prefix = "blk"; + let ext = ".dat"; + if file_name.starts_with(prefix) && file_name.ends_with(ext) { + file_name[prefix.len()..(file_name.len() - ext.len())] + .parse::<i32>() + .ok() + } else { + None + } + } +} + +// #[cfg(test)] +// mod tests { +// use super::*; + +// #[test] +// fn test_parse_blk_index() { +// assert_eq!(0, BlkFiles::parse_blk_index("blk00000.dat").unwrap()); +// assert_eq!(6, BlkFiles::parse_blk_index("blk6.dat").unwrap()); +// assert_eq!(1202, BlkFiles::parse_blk_index("blk1202.dat").unwrap()); +// assert_eq!( +// 13412451, +// BlkFiles::parse_blk_index("blk13412451.dat").unwrap() +// ); +// assert!(BlkFiles::parse_blk_index("blkindex.dat").is_none()); +// assert!(BlkFiles::parse_blk_index("invalid.dat").is_none()); +// } +// } diff --git a/parser/src/bitcoin/db/block_iter.rs b/parser/src/bitcoin/db/block_iter.rs new file mode 100644 index 000000000..61611b3d1 --- /dev/null +++ b/parser/src/bitcoin/db/block_iter.rs @@ -0,0 +1,45 @@ +//! +//! View development note of iter_connected.rs for implementation +//! details of iter_block.rs, which follows similar principles. +//! +use bitcoin::Block; +use par_iter_sync::{IntoParallelIteratorSync, ParIterSync}; + +use super::BitcoinDB; + +pub struct BlockIter(ParIterSync<Block>); + +impl BlockIter { + /// the worker threads are dispatched in this `new` constructor! + pub fn new<T>(db: &BitcoinDB, heights: T) -> Self + where + T: IntoIterator<Item = usize> + Send + 'static, + <T as IntoIterator>::IntoIter: Send + 'static, + { + let db_ref = db.clone(); + + BlockIter( + heights.into_par_iter_sync(move |h| match db_ref.get_block(h) { + Ok(blk) => Ok(blk), + Err(_) => Err(()), + }), + ) + } + + /// the worker threads are dispatched in this `new` constructor! + pub fn from_range(db: &BitcoinDB, start: usize, end: usize) -> Self { + if end <= start { + BlockIter::new(db, Vec::new()) + } else { + BlockIter::new(db, start..end) + } + } +} + +impl Iterator for BlockIter { + type Item = Block; + + fn next(&mut self) -> Option<Self::Item> { + self.0.next() + } +} diff --git a/parser/src/bitcoin/db/blocks_indexes.rs b/parser/src/bitcoin/db/blocks_indexes.rs new file mode 100644 index 000000000..272d53bc2 --- /dev/null +++ b/parser/src/bitcoin/db/blocks_indexes.rs @@ -0,0 +1,211 @@ +use std::{collections::BTreeMap, fmt, path::Path}; + +use bitcoin::{block::Header, io::Cursor, BlockHash}; +use derive_deref::{Deref, DerefMut}; +use leveldb::{ + database::{iterator::LevelDBIterator, Database}, + iterator::Iterable, + options::{Options, ReadOptions}, +}; + +use crate::utils::log; + +use super::{BlockchainRead, OpResult}; + +/// +/// See Bitcoin Core repository for definition. +/// +const BLOCK_VALID_HEADER: u32 = 1; +const BLOCK_VALID_TREE: u32 = 2; +const BLOCK_VALID_TRANSACTIONS: u32 = 3; +const BLOCK_VALID_CHAIN: u32 = 4; +const BLOCK_VALID_SCRIPTS: u32 = 5; +const BLOCK_VALID_MASK: u32 = BLOCK_VALID_HEADER + | BLOCK_VALID_TREE + | BLOCK_VALID_TRANSACTIONS + | BLOCK_VALID_CHAIN + | BLOCK_VALID_SCRIPTS; +const BLOCK_HAVE_DATA: u32 = 8; +const BLOCK_HAVE_UNDO: u32 = 16; + +/// +/// - Map from block height to block hash (records) +/// - Map from block hash to block height (hash_to_height) +/// +#[derive(Clone, Deref, DerefMut)] +pub struct BlocksIndexes(Box<[BlockIndexRecord]>); + +/// +/// BLOCK_INDEX RECORD as defined in Bitcoin Core. +/// +#[derive(Clone)] +pub struct BlockIndexRecord { + pub n_version: i32, + pub n_height: i32, + pub n_status: u32, + pub n_tx: u32, + pub n_file: i32, + pub n_data_pos: u32, + pub n_undo_pos: u32, + pub header: Header, +} + +impl BlocksIndexes { + /// + /// Build a collections of block index. + /// + pub(crate) fn new(p: &Path) -> OpResult<Self> { + Ok(Self(load_block_index(p)?.into_boxed_slice())) + } +} + +/// +/// Load all block index in memory from leveldb (i.e. `blocks/index` path). +/// +/// Map from block height to block index record. +/// +pub fn load_block_index(path: &Path) -> OpResult<Vec<BlockIndexRecord>> { + let mut block_index_by_block_hash = BTreeMap::new(); + + log("Start loading block_index"); + + let mut options = Options::new(); + options.create_if_missing = false; + let db: Database<BlockKey> = Database::open(path, options)?; + let options = ReadOptions::new(); + let mut iter = db.iter(options); + let mut max_height_block_hash = Option::<(BlockHash, i32)>::None; + + while iter.advance() { + let k = iter.key(); + let v = iter.value(); + if is_block_index_record(&k.key) { + let record = BlockIndexRecord::from(&v)?; + // only add valid block index record that has block data. + if record.n_height == 0 + || (record.n_status & BLOCK_VALID_MASK >= BLOCK_VALID_SCRIPTS + && record.n_status & BLOCK_HAVE_DATA > 0) + { + let block_hash = record.header.block_hash(); + // find the block with max height + if let Some((hash, height)) = max_height_block_hash.as_mut() { + if record.n_height > *height { + *hash = block_hash; + *height = record.n_height; + } + } else { + max_height_block_hash = Some((block_hash, record.n_height)); + } + block_index_by_block_hash.insert(block_hash, record); + } + } + } + // build the longest chain + if let Some((hash, height)) = max_height_block_hash { + let mut block_index = Vec::with_capacity(height as usize + 1); + let mut current_hash = hash; + let mut current_height = height; + + // recursively build block index from max height block. + while current_height >= 0 { + let blk = block_index_by_block_hash + .remove(¤t_hash) + .expect("block hash not found in block index!"); + + assert_eq!( + current_height, blk.n_height, + "some block info missing from block index levelDB,\ + delete Bitcoin folder and re-download!" + ); + + current_hash = blk.header.prev_blockhash; + current_height -= 1; + block_index.push(blk); + } + + block_index.reverse(); + + Ok(block_index) + } else { + Ok(Vec::with_capacity(0)) + } +} + +/// levelDB key util +struct BlockKey { + key: Vec<u8>, +} + +/// levelDB key util +impl db_key::Key for BlockKey { + fn from_u8(key: &[u8]) -> Self { + BlockKey { + key: Vec::from(key), + } + } + + fn as_slice<T, F: Fn(&[u8]) -> T>(&self, f: F) -> T { + f(&self.key) + } +} + +impl BlockIndexRecord { + /// + /// Decode levelDB value for Block Index Record. + /// + fn from(values: &[u8]) -> OpResult<Self> { + let mut reader = Cursor::new(values); + + let n_version = reader.read_varint()? as i32; + let n_height = reader.read_varint()? as i32; + let n_status = reader.read_varint()? as u32; + let n_tx = reader.read_varint()? as u32; + let n_file = if n_status & (BLOCK_HAVE_DATA | BLOCK_HAVE_UNDO) > 0 { + reader.read_varint()? as i32 + } else { + -1 + }; + let n_data_pos = if n_status & BLOCK_HAVE_DATA > 0 { + reader.read_varint()? as u32 + } else { + u32::MAX + }; + let n_undo_pos = if n_status & BLOCK_HAVE_UNDO > 0 { + reader.read_varint()? as u32 + } else { + u32::MAX + }; + + let header = reader.read_block_header()?; + + Ok(BlockIndexRecord { + n_version, + n_height, + n_status, + n_tx, + n_file, + n_data_pos, + n_undo_pos, + header, + }) + } +} + +#[inline] +fn is_block_index_record(data: &[u8]) -> bool { + data.first() == Some(&b'b') +} + +impl fmt::Debug for BlockIndexRecord { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("BlockIndexRecord") + .field("version", &self.n_version) + .field("height", &self.n_height) + .field("status", &self.n_status) + .field("n_tx", &self.n_tx) + .field("n_file", &self.n_file) + .field("n_data_pos", &self.n_data_pos) + .field("header", &self.header) + .finish() + } +} diff --git a/parser/src/bitcoin/db/errors.rs b/parser/src/bitcoin/db/errors.rs new file mode 100644 index 000000000..90405f8c3 --- /dev/null +++ b/parser/src/bitcoin/db/errors.rs @@ -0,0 +1,135 @@ +use std::convert::{self, From}; +use std::error; +use std::fmt; +use std::io; +use std::string; +use std::sync; + +pub type OpResult<T> = Result<T, OpError>; + +#[derive(Debug)] +/// Custom error type +pub struct OpError { + pub kind: OpErrorKind, + pub message: String, +} + +impl OpError { + pub fn new(kind: OpErrorKind) -> Self { + OpError { + kind, + message: String::new(), + } + } + + /// Joins the Error with a new message and returns it + pub fn join_msg(mut self, msg: &str) -> Self { + self.message.push_str(msg); + OpError { + kind: self.kind, + message: self.message, + } + } +} + +impl fmt::Display for OpError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.message.is_empty() { + write!(f, "{}", &self.kind) + } else { + write!(f, "{} {}", &self.message, &self.kind) + } + } +} + +impl error::Error for OpError { + fn description(&self) -> &str { + self.message.as_ref() + } + fn cause(&self) -> Option<&dyn error::Error> { + self.kind.source() + } +} + +#[derive(Debug)] +pub enum OpErrorKind { + None, + IoError(io::Error), + Utf8Error(string::FromUtf8Error), + RuntimeError, + PoisonError, + SendError, +} + +impl fmt::Display for OpErrorKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + OpErrorKind::IoError(ref err) => write!(f, "I/O Error: {}", err), + OpErrorKind::Utf8Error(ref err) => write!(f, "Utf8 Conversion: {}", err), + ref err @ OpErrorKind::PoisonError => write!(f, "Threading Error: {}", err), + ref err @ OpErrorKind::SendError => write!(f, "Sync: {}", err), + ref err @ OpErrorKind::RuntimeError => write!(f, "RuntimeError: {}", err), + OpErrorKind::None => write!(f, ""), + } + } +} + +impl error::Error for OpErrorKind { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match *self { + OpErrorKind::IoError(ref err) => Some(err), + OpErrorKind::Utf8Error(ref err) => Some(err), + ref err @ OpErrorKind::PoisonError => Some(err), + ref err @ OpErrorKind::SendError => Some(err), + _ => None, + } + } +} + +impl From<io::Error> for OpError { + fn from(err: io::Error) -> Self { + Self::new(OpErrorKind::IoError(err)) + } +} + +impl From<bitcoin::consensus::encode::Error> for OpError { + fn from(_: bitcoin::consensus::encode::Error) -> Self { + Self::from("block decode error") + } +} + +impl convert::From<i32> for OpError { + fn from(err_code: i32) -> Self { + Self::from(io::Error::from_raw_os_error(err_code)) + } +} + +impl convert::From<&str> for OpError { + fn from(err: &str) -> Self { + Self::new(OpErrorKind::None).join_msg(err) + } +} + +impl<T> convert::From<sync::PoisonError<T>> for OpError { + fn from(_: sync::PoisonError<T>) -> Self { + Self::new(OpErrorKind::PoisonError) + } +} + +impl<T> convert::From<sync::mpsc::SendError<T>> for OpError { + fn from(_: sync::mpsc::SendError<T>) -> Self { + Self::new(OpErrorKind::SendError) + } +} + +impl convert::From<string::FromUtf8Error> for OpError { + fn from(err: string::FromUtf8Error) -> Self { + Self::new(OpErrorKind::Utf8Error(err)) + } +} + +impl convert::From<leveldb::error::Error> for OpError { + fn from(err: leveldb::error::Error) -> Self { + Self::from(err.to_string().as_ref()) + } +} diff --git a/parser/src/bitcoin/db/mod.rs b/parser/src/bitcoin/db/mod.rs new file mode 100644 index 000000000..ac2414a0a --- /dev/null +++ b/parser/src/bitcoin/db/mod.rs @@ -0,0 +1,172 @@ +//! +//! Mostly a stripped down copy pasta of bitcoin-explorer +//! +//! Huge props to https://github.com/Congyuwang +//! +//! Crates APIs, essential structs, functions, methods are all here! +//! +//! To quickly understand how to use this crate, have a look at the +//! documentation for `bitcoin_explorer::BitcoinDB`!!. +//! + +mod blk_files; +mod block_iter; +mod blocks_indexes; +mod errors; +mod reader; +mod txdb; + +use blk_files::*; +use blocks_indexes::*; +use errors::*; +use reader::*; +use txdb::*; + +use std::ops::Deref; +use std::path::Path; +use std::sync::Arc; + +use bitcoin::{Block, Transaction, Txid}; + +pub use block_iter::BlockIter; + +pub struct InnerDB { + pub blocks_indexes: BlocksIndexes, + pub blk_files: BlkFiles, + pub tx_db: TxDB, +} + +/// +/// This is the main struct of this crate!! Click and read the doc. +/// +/// All queries start from initializing `BitcoinDB`. +/// +/// Note: This is an Arc wrap around `InnerDB`. +/// +#[derive(Clone)] +pub struct BitcoinDB(Arc<InnerDB>); + +impl Deref for BitcoinDB { + type Target = InnerDB; + + fn deref(&self) -> &Self::Target { + self.0.deref() + } +} + +impl BitcoinDB { + /// + /// This is the main structure for reading Bitcoin blockchain data. + /// + /// Instantiating this class by passing the `-datadir` directory of + /// Bitcoin core to the `new()` method. + /// `tx_index`: whether to try to open tx_index levelDB. + /// + pub fn new(p: &Path, tx_index: bool) -> OpResult<BitcoinDB> { + if !p.exists() { + return Err(OpError::from("data_dir does not exist")); + } + let blk_path = p.join("blocks"); + let index_path = blk_path.join("index"); + let blocks_indexes = BlocksIndexes::new(index_path.as_path())?; + let tx_db = if tx_index { + let tx_index_path = p.join("indexes").join("txindex"); + TxDB::new(&tx_index_path) + } else { + TxDB::null() + }; + let inner = InnerDB { + blocks_indexes, + blk_files: BlkFiles::new(blk_path.as_path())?, + tx_db, + }; + Ok(BitcoinDB(Arc::new(inner))) + } + + /// + /// Get the maximum number of blocks downloaded. + /// + /// This API guarantee that block 0 to `get_block_count() - 1` + /// have been downloaded and available for query. + /// + pub fn get_block_count(&self) -> usize { + let records = self.blocks_indexes.len(); + for h in 0..records { + // n_tx == 0 indicates that the block is not downloaded + if self.blocks_indexes.get(h).unwrap().n_tx == 0 { + return h; + } + } + records + } + + /// + /// Get a block + /// + pub fn get_block(&self, height: usize) -> OpResult<Block> { + if let Some(index) = self.blocks_indexes.get(height) { + Ok(self.blk_files.read_block(index.n_file, index.n_data_pos)?) + } else { + Err(OpError::from("height not found")) + } + } + + /// + /// Get a transaction by providing txid. + /// + /// This function requires `txindex` to be set to `true` for `BitcoinDB`, + /// and requires that flag `txindex=1` has been enabled when + /// running Bitcoin Core. + /// + /// A transaction cannot be found using this function if it is + /// not yet indexed using `txindex`. + /// + pub fn get_transaction(&self, txid: &Txid) -> OpResult<Transaction> { + if !self.tx_db.is_open() { + return Err(OpError::from("TxDB not open")); + } + + // give special treatment for genesis transaction + if self.tx_db.is_genesis_tx(txid) { + return Ok(self.get_block(0)?.txdata.swap_remove(0)); + } + + let record = self.tx_db.get_tx_record(txid)?; + + self.blk_files + .read_transaction(record.n_file, record.n_pos, record.n_tx_offset) + } + + /// + /// Iterate through all blocks from `start` to `end` (excluded). + /// + /// # Performance + /// + /// This iterator is implemented to read the blocks in concurrency, + /// but the result is still produced in sequential order. + /// Results read are stored in a synced queue for `next()` + /// to get. + /// + /// The iterator stops automatically when a block cannot be + /// read (i.e., when the max height in the database met). + /// + /// This is a very efficient implementation. + /// Using SSD and intel core i7 (4 core, 8 threads) + /// Iterating from height 0 to 700000 takes about 10 minutes. + /// + pub fn iter_block(&self, start: usize, end: usize) -> BlockIter { + BlockIter::from_range(self, start, end) + } + + pub fn check_if_txout_value_is_zero(&self, txid: &Txid, vout: usize) -> bool { + self.get_transaction(txid) + .unwrap() + .output + .get(vout) + .unwrap() + .to_owned() + .value + .to_sat() + == 0 + } +} diff --git a/parser/src/bitcoin/db/reader.rs b/parser/src/bitcoin/db/reader.rs new file mode 100644 index 000000000..fd6593e45 --- /dev/null +++ b/parser/src/bitcoin/db/reader.rs @@ -0,0 +1,90 @@ +use std::{fs::File, io::BufReader}; + +use bitcoin::{block::Header, consensus::Decodable, io::Cursor, Block, Transaction}; +use byteorder::{LittleEndian, ReadBytesExt}; + +use super::OpResult; + +/// +/// binary file read utilities. +/// +pub trait BlockchainRead { + #[inline] + fn read_varint(&mut self) -> OpResult<usize> + where + Self: bitcoin::io::Read, + { + let mut n = 0; + loop { + let ch_data = self.read_u8()?; + n = (n << 7) | (ch_data & 0x7F) as usize; + if ch_data & 0x80 > 0 { + n += 1; + } else { + break; + } + } + Ok(n) + } + + #[inline] + fn read_u8(&mut self) -> OpResult<u8> + where + Self: bitcoin::io::Read, + { + let mut slice = [0u8; 1]; + + self.read_exact(&mut slice).unwrap(); + + Ok(slice[0]) + } + + #[inline] + fn read_u32(&mut self) -> OpResult<u32> + where + Self: std::io::Read, + { + let u = ReadBytesExt::read_u32::<LittleEndian>(self)?; + + Ok(u) + } + + #[inline] + fn read_u8_vec(&mut self, count: u32) -> OpResult<Vec<u8>> + where + Self: bitcoin::io::Read, + { + let mut arr = vec![0u8; count as usize]; + + self.read_exact(&mut arr).unwrap(); + + Ok(arr) + } + + #[inline] + fn read_block(&mut self) -> OpResult<Block> + where + Self: bitcoin::io::BufRead, + { + Ok(Block::consensus_decode(self)?) + } + + #[inline] + fn read_transaction(&mut self) -> OpResult<Transaction> + where + Self: bitcoin::io::BufRead, + { + Ok(Transaction::consensus_decode(self)?) + } + + #[inline] + fn read_block_header(&mut self) -> OpResult<Header> + where + Self: bitcoin::io::BufRead, + { + Ok(Header::consensus_decode(self)?) + } +} + +impl<T> BlockchainRead for Cursor<T> {} +impl BlockchainRead for BufReader<File> {} diff --git a/parser/src/bitcoin/db/txdb.rs b/parser/src/bitcoin/db/txdb.rs new file mode 100644 index 000000000..89db28380 --- /dev/null +++ b/parser/src/bitcoin/db/txdb.rs @@ -0,0 +1,147 @@ +use std::{path::Path, str::FromStr}; + +use bitcoin::{hashes::Hash, io::Cursor, Txid}; +use leveldb::{ + database::Database, + kv::KV, + options::{Options, ReadOptions}, +}; + +use crate::utils::log; + +use super::{BlockchainRead, OpError, OpResult}; + +const GENESIS_TXID: &str = "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"; + +/// +/// tx-index: looking up transaction position using txid. +/// +/// This is possible if Bitcoin Core has `txindex=1`. +/// +pub struct TxDB { + db: Option<Database<TxKey>>, + genesis_txid: Txid, +} + +/// Records transaction storage on disk +pub struct TransactionRecord { + pub txid: Txid, + pub n_file: i32, + pub n_pos: u32, + pub n_tx_offset: u32, +} + +impl TransactionRecord { + fn from(key: &[u8], values: &[u8]) -> OpResult<Self> { + let mut reader = Cursor::new(values); + Ok(TransactionRecord { + txid: Txid::from_slice(key).unwrap(), + n_file: reader.read_varint()? as i32, + n_pos: reader.read_varint()? as u32, + n_tx_offset: reader.read_varint()? as u32, + }) + } +} + +impl TxDB { + /// initialize TxDB for transaction queries + pub fn new(path: &Path) -> TxDB { + let option_db = TxDB::try_open_db(path); + if let Some(db) = option_db { + TxDB { + db: Some(db), + genesis_txid: Txid::from_str(GENESIS_TXID).unwrap(), + } + } else { + TxDB::null() + } + } + + #[inline] + pub fn is_open(&self) -> bool { + self.db.is_some() + } + + #[inline] + pub fn null() -> TxDB { + TxDB { + db: None, + genesis_txid: Txid::from_str(GENESIS_TXID).unwrap(), + } + } + + #[inline] + /// + /// genesis tx is not included in UTXO because of Bitcoin Core Bug + /// + pub fn is_genesis_tx(&self, txid: &Txid) -> bool { + txid == &self.genesis_txid + } + + fn try_open_db(path: &Path) -> Option<Database<TxKey>> { + if !path.exists() { + log("Failed to open tx_index DB: tx_index not built"); + + return None; + } + let options = Options::new(); + match Database::open(path, options) { + Ok(db) => { + log("Successfully opened tx_index DB!"); + + Some(db) + } + Err(e) => { + log(&format!("Failed to open tx_index DB: {:?}", e)); + + None + } + } + } + + /// note that this function cannot find genesis block, which needs special treatment + pub fn get_tx_record(&self, txid: &Txid) -> OpResult<TransactionRecord> { + if let Some(db) = &self.db { + let inner = txid.as_byte_array(); + let mut key = Vec::with_capacity(inner.len() + 1); + key.push(b't'); + key.extend(inner); + let key = TxKey { key }; + let read_options = ReadOptions::new(); + match db.get(read_options, &key) { + Ok(value) => { + if let Some(value) = value { + Ok(TransactionRecord::from(&key.key[1..], value.as_slice())?) + } else { + Err(OpError::from( + format!("value not found for txid: {}", txid).as_str(), + )) + } + } + Err(e) => Err(OpError::from( + format!("value not found for txid: {}", e).as_str(), + )), + } + } else { + Err(OpError::from("TxDB not open")) + } + } +} + +/// levelDB key utility +struct TxKey { + key: Vec<u8>, +} + +/// levelDB key utility +impl db_key::Key for TxKey { + fn from_u8(key: &[u8]) -> Self { + TxKey { + key: Vec::from(key), + } + } + + fn as_slice<T, F: Fn(&[u8]) -> T>(&self, f: F) -> T { + f(&self.key) + } +} diff --git a/parser/src/bitcoin/height.rs b/parser/src/bitcoin/height.rs new file mode 100644 index 000000000..372cbbe2d --- /dev/null +++ b/parser/src/bitcoin/height.rs @@ -0,0 +1,5 @@ +use super::NUMBER_OF_UNSAFE_BLOCKS; + +pub fn check_if_height_safe(height: usize, block_count: usize) -> bool { + height < block_count - NUMBER_OF_UNSAFE_BLOCKS +} diff --git a/parser/src/bitcoin/mod.rs b/parser/src/bitcoin/mod.rs new file mode 100644 index 000000000..22fbde05d --- /dev/null +++ b/parser/src/bitcoin/mod.rs @@ -0,0 +1,11 @@ +mod addresses; +mod consts; +mod daemon; +mod db; +mod height; + +pub use addresses::*; +pub use consts::*; +pub use daemon::*; +pub use db::*; +pub use height::*; diff --git a/parser/src/databases/_database.rs b/parser/src/databases/_database.rs new file mode 100644 index 000000000..fdc0fb33b --- /dev/null +++ b/parser/src/databases/_database.rs @@ -0,0 +1,235 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + fmt::Debug, + fs, +}; + +use allocative::Allocative; +use derive_deref::{Deref, DerefMut}; + +// https://docs.rs/sanakirja/latest/sanakirja/index.html +// https://pijul.org/posts/2021-02-06-rethinking-sanakirja/ +// +// Seems indeed much faster than ReDB and LMDB (heed) +// But a lot has changed code wise between them so a retest wouldn't hurt +// +// Possible compression: https://pijul.org/posts/sanakirja-zstd/ +use sanakirja::{ + btree::{self, page, page_unsized, BTreeMutPage, Db_}, + direct_repr, Commit, Env, Error, MutTxn, RootDb, Storable, UnsizedStorable, +}; + +use crate::io::OUTPUTS_FOLDER_PATH; + +pub type SizedDatabase<Key, Value> = Database<Key, Key, Value, page::Page<Key, Value>>; + +pub type UnsizedDatabase<KeyTree, KeyDB, Value> = + Database<KeyTree, KeyDB, Value, page_unsized::Page<KeyDB, Value>>; + +#[derive(Allocative)] +#[allocative(bound = "KeyTree: Allocative, KeyDB, Value: Allocative, Page")] +/// There is no `cached_gets` since it's much cheaper and faster to do a parallel search first using `unsafe_get` than caching gets along the way. +pub struct Database<KeyTree, KeyDB, Value, Page> +where + KeyTree: Ord + Clone + Debug, + KeyDB: Ord + ?Sized + Storable, + Value: Storable + PartialEq, + Page: BTreeMutPage<KeyDB, Value>, +{ + pub cached_puts: BTreeMap<KeyTree, Value>, + pub cached_dels: BTreeSet<KeyTree>, + #[allocative(skip)] + db: Db_<KeyDB, Value, Page>, + #[allocative(skip)] + txn: MutTxn<Env, ()>, + #[allocative(skip)] + key_tree_to_key_db: fn(&KeyTree) -> &KeyDB, +} + +pub const SANAKIRJA_MAX_KEY_SIZE: usize = 510; +const ROOT_DB: usize = 0; +const PAGE_SIZE: u64 = 4096 * 256; // 1mo - Must be a multiplier of 4096 + +impl<KeyDB, KeyTree, Value, Page> Database<KeyTree, KeyDB, Value, Page> +where + KeyTree: Ord + Clone + Debug, + KeyDB: Ord + ?Sized + Storable, + Value: Storable + PartialEq, + Page: BTreeMutPage<KeyDB, Value>, +{ + pub fn open( + folder: &str, + file: &str, + key_tree_to_key_db: fn(&KeyTree) -> &KeyDB, + ) -> color_eyre::Result<Self> { + let mut txn = Self::init_txn(folder, file)?; + + let db = txn + .root_db(ROOT_DB) + .unwrap_or_else(|| unsafe { btree::create_db_(&mut txn).unwrap() }); + + Ok(Self { + cached_puts: BTreeMap::default(), + cached_dels: BTreeSet::default(), + db, + txn, + key_tree_to_key_db, + }) + } + + pub fn iter<F>(&self, callback: &mut F) + where + F: FnMut((&KeyDB, &Value)), + { + btree::iter(&self.txn, &self.db, None) + .unwrap() + .for_each(|entry| callback(entry.unwrap())); + } + + pub fn get(&self, key: &KeyTree) -> Option<&Value> { + if let Some(cached_put) = self.get_from_puts(key) { + return Some(cached_put); + } + + self.db_get(key) + } + + pub fn db_get(&self, key: &KeyTree) -> Option<&Value> { + let k = (self.key_tree_to_key_db)(key); + + let option = btree::get(&self.txn, &self.db, k, None).unwrap(); + + if let Some((k_found, v)) = option { + if k == k_found { + return Some(v); + } + } + + None + } + + #[inline(always)] + pub fn get_from_puts(&self, key: &KeyTree) -> Option<&Value> { + self.cached_puts.get(key) + } + + #[inline(always)] + pub fn get_mut_from_puts(&mut self, key: &KeyTree) -> Option<&mut Value> { + self.cached_puts.get_mut(key) + } + + #[inline(always)] + pub fn remove(&mut self, key: &KeyTree) -> Option<Value> { + self.remove_from_puts(key).or_else(|| { + self.db_remove(key); + + None + }) + } + + #[inline(always)] + pub fn db_remove(&mut self, key: &KeyTree) { + self.cached_dels.insert(key.clone()); + } + + pub fn update(&mut self, key: KeyTree, value: Value) -> Option<Value> { + self.cached_dels.insert(key.clone()); + + self.cached_puts.insert(key, value) + } + + #[inline(always)] + pub fn remove_from_puts(&mut self, key: &KeyTree) -> Option<Value> { + self.cached_puts.remove(key) + } + + #[inline(always)] + pub fn insert(&mut self, key: KeyTree, value: Value) -> Option<Value> { + self.cached_dels.remove(&key); + + self.unsafe_insert(key, value) + } + + #[inline(always)] + pub fn unsafe_insert(&mut self, key: KeyTree, value: Value) -> Option<Value> { + self.cached_puts.insert(key, value) + } + + fn init_txn(folder: &str, file: &str) -> color_eyre::Result<MutTxn<Env, ()>> { + let path = databases_folder_path(folder); + + fs::create_dir_all(&path)?; + + let env = unsafe { Env::new_nolock(format!("{path}/{file}"), PAGE_SIZE, 1).unwrap() }; + + let txn = Env::mut_txn_begin(env)?; + + Ok(txn) + } + + pub fn export(mut self) -> color_eyre::Result<(), Error> { + if self.cached_dels.is_empty() && self.cached_puts.is_empty() { + return Ok(()); + } + + self.cached_dels + .into_iter() + .try_for_each(|key| -> Result<(), Error> { + btree::del( + &mut self.txn, + &mut self.db, + (self.key_tree_to_key_db)(&key), + None, + )?; + + Ok(()) + })?; + + self.cached_puts + .into_iter() + .try_for_each(|(key, value)| -> Result<(), Error> { + btree::put( + &mut self.txn, + &mut self.db, + (self.key_tree_to_key_db)(&key), + &value, + )?; + + Ok(()) + })?; + + self.txn.set_root(ROOT_DB, self.db.db.into()); + + self.txn.commit() + } +} + +#[derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deref, DerefMut, Default, Copy, Allocative, +)] +pub struct U8x19([u8; 19]); +direct_repr!(U8x19); +impl From<&[u8]> for U8x19 { + fn from(slice: &[u8]) -> Self { + let mut arr = Self::default(); + arr.copy_from_slice(slice); + arr + } +} + +#[derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deref, DerefMut, Default, Copy, Allocative, +)] +pub struct U8x31([u8; 31]); +direct_repr!(U8x31); +impl From<&[u8]> for U8x31 { + fn from(slice: &[u8]) -> Self { + let mut arr = Self::default(); + arr.copy_from_slice(slice); + arr + } +} + +pub fn databases_folder_path(folder: &str) -> String { + format!("{OUTPUTS_FOLDER_PATH}/databases/{folder}") +} diff --git a/parser/src/databases/_trait.rs b/parser/src/databases/_trait.rs new file mode 100644 index 000000000..55f92f733 --- /dev/null +++ b/parser/src/databases/_trait.rs @@ -0,0 +1,32 @@ +use std::{fs, io}; + +use crate::{structs::WNaiveDate, utils::log}; + +use super::databases_folder_path; + +pub trait AnyDatabaseGroup +where + Self: Sized, +{ + fn import() -> Self; + + fn export(&mut self, height: usize, date: WNaiveDate) -> color_eyre::Result<()>; + + fn folder<'a>() -> &'a str; + + fn reset(&mut self) -> color_eyre::Result<(), io::Error> { + log(&format!("Reset {}", Self::folder())); + + self.reset_metadata(); + + fs::remove_dir_all(Self::full_path())?; + + Ok(()) + } + + fn full_path() -> String { + databases_folder_path(Self::folder()) + } + + fn reset_metadata(&mut self); +} diff --git a/parser/src/databases/address_index_to_address_data.rs b/parser/src/databases/address_index_to_address_data.rs new file mode 100644 index 000000000..58a2495b4 --- /dev/null +++ b/parser/src/databases/address_index_to_address_data.rs @@ -0,0 +1,148 @@ +use std::{ + collections::BTreeMap, + fs, mem, + ops::{Deref, DerefMut}, +}; + +use allocative::Allocative; +use rayon::prelude::*; + +use crate::{ + structs::{AddressData, WNaiveDate}, + utils::time, +}; + +use super::{databases_folder_path, AnyDatabaseGroup, Metadata, SizedDatabase}; + +type Key = u32; +type Value = AddressData; +type Database = SizedDatabase<Key, Value>; + +#[derive(Allocative)] +pub struct AddressIndexToAddressData { + pub metadata: Metadata, + + map: BTreeMap<usize, Database>, +} + +impl Deref for AddressIndexToAddressData { + type Target = BTreeMap<usize, Database>; + + fn deref(&self) -> &Self::Target { + &self.map + } +} + +impl DerefMut for AddressIndexToAddressData { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.map + } +} + +const DB_MAX_SIZE: usize = 500_000; + +impl AddressIndexToAddressData { + pub fn unsafe_insert(&mut self, key: Key, value: Value) -> Option<Value> { + self.metadata.called_insert(); + + self.open_db(&key).unsafe_insert(key, value) + } + + pub fn remove(&mut self, key: &Key) -> Option<Value> { + self.metadata.called_remove(); + + self.open_db(key).remove(key) + } + + /// Doesn't check if the database is open contrary to `safe_get` which does and opens if needed + /// Though it makes it easy to use with rayon. + pub fn unsafe_get_from_cache(&self, key: &Key) -> Option<&Value> { + let db_index = Self::db_index(key); + + self.get(&db_index).unwrap().get_from_puts(key) + } + + pub fn unsafe_get_from_db(&self, key: &Key) -> Option<&Value> { + let db_index = Self::db_index(key); + + self.get(&db_index).unwrap().db_get(key) + } + + pub fn open_db(&mut self, key: &Key) -> &mut Database { + let db_index = Self::db_index(key); + + self.entry(db_index).or_insert_with(|| { + let db_name = format!( + "{}..{}", + db_index * DB_MAX_SIZE, + (db_index + 1) * DB_MAX_SIZE + ); + + SizedDatabase::open(Self::folder(), &db_name, |key| key).unwrap() + }) + } + + pub fn iter<F>(&mut self, callback: &mut F) + where + F: FnMut((&Key, &Value)), + { + time("Iter through address_index_to_address_data", || { + self.open_all(); + + // MUST CLEAR MAP, otherwise some weird shit in happening later in the export I think + mem::take(&mut self.map) + .values() + .for_each(|database| database.iter(callback)); + }); + } + + fn open_all(&mut self) { + fs::read_dir(databases_folder_path(Self::folder())) + .unwrap() + .map(|entry| { + entry + .unwrap() + .path() + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_owned() + }) + .filter(|file_name| file_name.contains("..")) + .for_each(|path| { + self.open_db(&path.split("..").next().unwrap().parse::<u32>().unwrap()); + }); + } + + fn db_index(key: &Key) -> usize { + *key as usize / DB_MAX_SIZE + } +} + +impl AnyDatabaseGroup for AddressIndexToAddressData { + fn import() -> Self { + Self { + map: BTreeMap::default(), + metadata: Metadata::import(&Self::full_path()), + } + } + + fn export(&mut self, height: usize, date: WNaiveDate) -> color_eyre::Result<()> { + mem::take(&mut self.map) + .into_par_iter() + .try_for_each(|(_, db)| db.export())?; + + self.metadata.export(height, date).unwrap(); + + Ok(()) + } + + fn reset_metadata(&mut self) { + self.metadata.reset(); + } + + fn folder<'a>() -> &'a str { + "address_index_to_address_data" + } +} diff --git a/parser/src/databases/address_index_to_empty_address_data.rs b/parser/src/databases/address_index_to_empty_address_data.rs new file mode 100644 index 000000000..d85e73d0b --- /dev/null +++ b/parser/src/databases/address_index_to_empty_address_data.rs @@ -0,0 +1,123 @@ +use std::{ + collections::BTreeMap, + mem, + ops::{Deref, DerefMut}, +}; + +use allocative::Allocative; +use rayon::prelude::*; + +use crate::structs::{EmptyAddressData, WNaiveDate}; + +use super::{AnyDatabaseGroup, Metadata, SizedDatabase}; + +type Key = u32; +type Value = EmptyAddressData; +type Database = SizedDatabase<Key, Value>; + +#[derive(Allocative)] +pub struct AddressIndexToEmptyAddressData { + pub metadata: Metadata, + + map: BTreeMap<usize, Database>, +} + +impl Deref for AddressIndexToEmptyAddressData { + type Target = BTreeMap<usize, Database>; + + fn deref(&self) -> &Self::Target { + &self.map + } +} + +impl DerefMut for AddressIndexToEmptyAddressData { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.map + } +} + +const DB_MAX_SIZE: usize = 500_000; + +impl AddressIndexToEmptyAddressData { + pub fn unsafe_insert(&mut self, key: Key, value: Value) -> Option<Value> { + self.metadata.called_insert(); + + self.open_db(&key).unsafe_insert(key, value) + } + + // pub fn undo_insert(&mut self, key: &Key) -> Option<Value> { + // self.metadata.called_remove(); + + // self.open_db(key).remove_from_puts(key) + // } + + pub fn remove(&mut self, key: &Key) -> Option<Value> { + self.metadata.called_remove(); + + self.open_db(key).remove(key) + } + + /// Doesn't check if the database is open contrary to `safe_get` which does and opens if needed + /// Though it makes it easy to use with rayon. + pub fn unsafe_get_from_cache(&self, key: &Key) -> Option<&Value> { + let db_index = Self::db_index(key); + + self.get(&db_index).and_then(|db| db.get_from_puts(key)) + } + + pub fn unsafe_get_from_db(&self, key: &Key) -> Option<&Value> { + let db_index = Self::db_index(key); + + self.get(&db_index) + .unwrap_or_else(|| { + dbg!(&self.map.keys(), &key, &db_index); + panic!() + }) + .db_get(key) + } + + pub fn open_db(&mut self, key: &Key) -> &mut Database { + let db_index = Self::db_index(key); + + self.entry(db_index).or_insert_with(|| { + let db_name = format!( + "{}..{}", + db_index * DB_MAX_SIZE, + (db_index + 1) * DB_MAX_SIZE + ); + + SizedDatabase::open(Self::folder(), &db_name, |key| key).unwrap() + }) + } + + fn db_index(key: &Key) -> usize { + *key as usize / DB_MAX_SIZE + } +} + +impl AnyDatabaseGroup for AddressIndexToEmptyAddressData { + fn import() -> Self { + Self { + map: BTreeMap::default(), + metadata: Metadata::import(&Self::full_path()), + } + } + + fn export(&mut self, height: usize, date: WNaiveDate) -> color_eyre::Result<()> { + mem::take(&mut self.map) + .into_par_iter() + .try_for_each(|(_, db)| db.export())?; + + self.metadata.export(height, date)?; + + Ok(()) + } + + fn reset_metadata(&mut self) { + self.metadata.reset(); + } + + fn folder<'a>() -> &'a str { + "address_index_to_empty_address_data" + } +} diff --git a/parser/src/databases/address_to_address_index.rs b/parser/src/databases/address_to_address_index.rs new file mode 100644 index 000000000..68409a0bb --- /dev/null +++ b/parser/src/databases/address_to_address_index.rs @@ -0,0 +1,309 @@ +use std::{collections::BTreeMap, mem, thread}; + +use allocative::Allocative; +use rayon::prelude::*; + +use crate::structs::{Address, WNaiveDate}; + +use super::{ + AnyDatabaseGroup, Database, Metadata, SizedDatabase, U8x19, U8x31, + UnsizedDatabase as _UnsizedDatabase, +}; + +type Value = u32; +type U8x19Database = SizedDatabase<U8x19, Value>; +type U8x31Database = SizedDatabase<U8x31, Value>; +type U32Database = SizedDatabase<u32, Value>; +type UnsizedDatabase = _UnsizedDatabase<Box<[u8]>, [u8], Value>; + +type P2PKDatabase = U8x19Database; +type P2PKHDatabase = U8x19Database; +type P2SHDatabase = U8x19Database; +type P2WPKHDatabase = U8x19Database; +type P2WSHDatabase = U8x31Database; +type P2TRDatabase = U8x31Database; +type UnknownDatabase = U32Database; +type OpReturnDatabase = U32Database; +type PushOnlyDatabase = U32Database; +type EmptyDatabase = U32Database; +type MultisigDatabase = UnsizedDatabase; + +#[derive(Allocative)] +pub struct AddressToAddressIndex { + pub metadata: Metadata, + + p2pk: BTreeMap<u16, P2PKDatabase>, + p2pkh: BTreeMap<u16, P2PKHDatabase>, + p2sh: BTreeMap<u16, P2SHDatabase>, + p2wpkh: BTreeMap<u16, P2WPKHDatabase>, + p2wsh: BTreeMap<u16, P2WSHDatabase>, + p2tr: BTreeMap<u16, P2TRDatabase>, + op_return: Option<OpReturnDatabase>, + push_only: Option<PushOnlyDatabase>, + unknown: Option<UnknownDatabase>, + empty: Option<EmptyDatabase>, + multisig: Option<MultisigDatabase>, +} + +impl AddressToAddressIndex { + // pub fn safe_get(&mut self, address: &Address) -> Option<&Value> { + // match address { + // Address::Empty(key) => self.open_empty().get(key), + // Address::Unknown(key) => self.open_unknown().get(key), + // Address::MultiSig(key) => self.open_multisig().get(key), + // Address::P2PK((prefix, rest)) => self.open_p2pk(*prefix).get(rest), + // Address::P2PKH((prefix, rest)) => self.open_p2pkh(*prefix).get(rest), + // Address::P2SH((prefix, rest)) => self.open_p2sh(*prefix).get(rest), + // Address::P2WPKH((prefix, rest)) => self.open_p2wpkh(*prefix).get(rest), + // Address::P2WSH((prefix, rest)) => self.open_p2wsh(*prefix).get(rest), + // Address::P2TR((prefix, rest)) => self.open_p2tr(*prefix).get(rest), + // } + // } + + pub fn open_db(&mut self, address: &Address) { + match address { + Address::Empty(_) => { + self.open_empty(); + } + Address::Unknown(_) => { + self.open_unknown(); + } + Address::OpReturn(_) => { + self.open_op_return(); + } + Address::PushOnly(_) => { + self.open_push_only(); + } + Address::MultiSig(_) => { + self.open_multisig(); + } + Address::P2PK((prefix, _)) => { + self.open_p2pk(*prefix); + } + Address::P2PKH((prefix, _)) => { + self.open_p2pkh(*prefix); + } + Address::P2SH((prefix, _)) => { + self.open_p2sh(*prefix); + } + Address::P2WPKH((prefix, _)) => { + self.open_p2wpkh(*prefix); + } + Address::P2WSH((prefix, _)) => { + self.open_p2wsh(*prefix); + } + Address::P2TR((prefix, _)) => { + self.open_p2tr(*prefix); + } + } + } + + /// Doesn't check if the database is open contrary to `safe_get` which does and opens if needed. + /// Though it makes it easy to use with rayon + pub fn unsafe_get(&self, address: &Address) -> Option<&Value> { + match address { + Address::Empty(key) => self.empty.as_ref().unwrap().get(key), + Address::Unknown(key) => self.unknown.as_ref().unwrap().get(key), + Address::OpReturn(key) => self.op_return.as_ref().unwrap().get(key), + Address::PushOnly(key) => self.push_only.as_ref().unwrap().get(key), + Address::MultiSig(key) => self.multisig.as_ref().unwrap().get(key), + Address::P2PK((prefix, key)) => self.p2pk.get(prefix).unwrap().get(key), + Address::P2PKH((prefix, key)) => self.p2pkh.get(prefix).unwrap().get(key), + Address::P2SH((prefix, key)) => self.p2sh.get(prefix).unwrap().get(key), + Address::P2WPKH((prefix, key)) => self.p2wpkh.get(prefix).unwrap().get(key), + Address::P2WSH((prefix, key)) => self.p2wsh.get(prefix).unwrap().get(key), + Address::P2TR((prefix, key)) => self.p2tr.get(prefix).unwrap().get(key), + } + } + + pub fn unsafe_get_from_puts(&self, address: &Address) -> Option<&Value> { + match address { + Address::Empty(key) => self.empty.as_ref().unwrap().get_from_puts(key), + Address::Unknown(key) => self.unknown.as_ref().unwrap().get_from_puts(key), + Address::OpReturn(key) => self.op_return.as_ref().unwrap().get_from_puts(key), + Address::PushOnly(key) => self.push_only.as_ref().unwrap().get_from_puts(key), + Address::MultiSig(key) => self.multisig.as_ref().unwrap().get_from_puts(key), + Address::P2PK((prefix, key)) => self.p2pk.get(prefix).unwrap().get_from_puts(key), + Address::P2PKH((prefix, key)) => self.p2pkh.get(prefix).unwrap().get_from_puts(key), + Address::P2SH((prefix, key)) => self.p2sh.get(prefix).unwrap().get_from_puts(key), + Address::P2WPKH((prefix, key)) => self.p2wpkh.get(prefix).unwrap().get_from_puts(key), + Address::P2WSH((prefix, key)) => self.p2wsh.get(prefix).unwrap().get_from_puts(key), + Address::P2TR((prefix, key)) => self.p2tr.get(prefix).unwrap().get_from_puts(key), + } + } + + pub fn insert(&mut self, address: Address, value: Value) -> Option<Value> { + self.metadata.called_insert(); + + match address { + Address::Empty(key) => self.open_empty().insert(key, value), + Address::Unknown(key) => self.open_unknown().insert(key, value), + Address::OpReturn(key) => self.open_op_return().insert(key, value), + Address::PushOnly(key) => self.open_push_only().insert(key, value), + Address::MultiSig(key) => self.open_multisig().insert(key, value), + Address::P2PK((prefix, rest)) => self.open_p2pk(prefix).insert(rest, value), + Address::P2PKH((prefix, rest)) => self.open_p2pkh(prefix).insert(rest, value), + Address::P2SH((prefix, rest)) => self.open_p2sh(prefix).insert(rest, value), + Address::P2WPKH((prefix, rest)) => self.open_p2wpkh(prefix).insert(rest, value), + Address::P2WSH((prefix, rest)) => self.open_p2wsh(prefix).insert(rest, value), + Address::P2TR((prefix, rest)) => self.open_p2tr(prefix).insert(rest, value), + } + } + + pub fn open_p2pk(&mut self, prefix: u16) -> &mut P2PKDatabase { + self.p2pk.entry(prefix).or_insert_with(|| { + Database::open( + &format!("{}/{}", Self::folder(), "p2pk"), + &prefix.to_string(), + |key| key, + ) + .unwrap() + }) + } + + pub fn open_p2pkh(&mut self, prefix: u16) -> &mut P2PKHDatabase { + self.p2pkh.entry(prefix).or_insert_with(|| { + Database::open( + &format!("{}/{}", Self::folder(), "p2pkh"), + &prefix.to_string(), + |key| key, + ) + .unwrap() + }) + } + + pub fn open_p2sh(&mut self, prefix: u16) -> &mut P2SHDatabase { + self.p2sh.entry(prefix).or_insert_with(|| { + Database::open( + &format!("{}/{}", Self::folder(), "p2sh"), + &prefix.to_string(), + |key| key, + ) + .unwrap() + }) + } + + pub fn open_p2wpkh(&mut self, prefix: u16) -> &mut P2WPKHDatabase { + self.p2wpkh.entry(prefix).or_insert_with(|| { + Database::open( + &format!("{}/{}", Self::folder(), "p2wpkh"), + &prefix.to_string(), + |key| key, + ) + .unwrap() + }) + } + + pub fn open_p2wsh(&mut self, prefix: u16) -> &mut P2WSHDatabase { + self.p2wsh.entry(prefix).or_insert_with(|| { + Database::open( + &format!("{}/{}", Self::folder(), "p2wsh"), + &prefix.to_string(), + |key| key, + ) + .unwrap() + }) + } + + pub fn open_p2tr(&mut self, prefix: u16) -> &mut P2TRDatabase { + self.p2tr.entry(prefix).or_insert_with(|| { + Database::open( + &format!("{}/{}", Self::folder(), "p2tr"), + &prefix.to_string(), + |key| key, + ) + .unwrap() + }) + } + + pub fn open_unknown(&mut self) -> &mut UnknownDatabase { + self.unknown + .get_or_insert_with(|| Database::open(Self::folder(), "unknown", |key| key).unwrap()) + } + + pub fn open_op_return(&mut self) -> &mut UnknownDatabase { + self.op_return + .get_or_insert_with(|| Database::open(Self::folder(), "op_return", |key| key).unwrap()) + } + + pub fn open_push_only(&mut self) -> &mut UnknownDatabase { + self.push_only + .get_or_insert_with(|| Database::open(Self::folder(), "push_only", |key| key).unwrap()) + } + + pub fn open_empty(&mut self) -> &mut UnknownDatabase { + self.empty + .get_or_insert_with(|| Database::open(Self::folder(), "empty", |key| key).unwrap()) + } + + pub fn open_multisig(&mut self) -> &mut MultisigDatabase { + self.multisig.get_or_insert_with(|| { + Database::open(Self::folder(), "multisig", |key| key as &[u8]).unwrap() + }) + } +} + +impl AnyDatabaseGroup for AddressToAddressIndex { + fn import() -> Self { + Self { + p2pk: BTreeMap::default(), + p2pkh: BTreeMap::default(), + p2sh: BTreeMap::default(), + p2wpkh: BTreeMap::default(), + p2wsh: BTreeMap::default(), + p2tr: BTreeMap::default(), + op_return: None, + push_only: None, + unknown: None, + empty: None, + multisig: None, + metadata: Metadata::import(&Self::full_path()), + } + } + + fn export(&mut self, height: usize, date: WNaiveDate) -> color_eyre::Result<()> { + thread::scope(|s| { + s.spawn(|| { + mem::take(&mut self.p2pk) + .into_par_iter() + .chain(mem::take(&mut self.p2pkh).into_par_iter()) + .chain(mem::take(&mut self.p2sh).into_par_iter()) + .chain(mem::take(&mut self.p2wpkh).into_par_iter()) + .try_for_each(|(_, db)| db.export()) + }); + + s.spawn(|| { + mem::take(&mut self.p2wsh) + .into_par_iter() + .chain(mem::take(&mut self.p2tr).into_par_iter()) + .try_for_each(|(_, db)| db.export()) + }); + + s.spawn(|| { + [ + self.unknown.take(), + self.op_return.take(), + self.push_only.take(), + self.empty.take(), + ] + .into_par_iter() + .flatten() + .try_for_each(|db| db.export()) + }); + + self.multisig.take().map(|db| db.export()); + }); + + self.metadata.export(height, date)?; + + Ok(()) + } + + fn reset_metadata(&mut self) { + self.metadata.reset() + } + + fn folder<'a>() -> &'a str { + "address_to_address_index" + } +} diff --git a/parser/src/databases/metadata.rs b/parser/src/databases/metadata.rs new file mode 100644 index 000000000..5d424a6b2 --- /dev/null +++ b/parser/src/databases/metadata.rs @@ -0,0 +1,116 @@ +use allocative::Allocative; +use bincode::{Decode, Encode}; +use std::{ + fmt::Debug, + fs, io, + ops::{Deref, DerefMut}, +}; + +use crate::{ + io::Binary, + structs::{Counter, WNaiveDate}, +}; + +#[derive(Default, Debug, Encode, Decode, Allocative)] +pub struct Metadata { + path: String, + data: MetadataData, +} + +impl Deref for Metadata { + type Target = MetadataData; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl DerefMut for Metadata { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.data + } +} + +impl Metadata { + pub fn import(path: &str) -> Self { + Self { + path: path.to_owned(), + data: MetadataData::import(path).unwrap_or_default(), + } + } + + pub fn export(&mut self, height: usize, date: WNaiveDate) -> color_eyre::Result<()> { + if self.last_height.unwrap_or_default() < height { + self.last_height.replace(height); + } + + if self.last_date.unwrap_or_default() < date { + self.last_date.replace(date); + } + + self.data.export(&self.path) + } + + pub fn reset(&mut self) { + let _ = self.data.reset(&self.path); + } + + pub fn called_insert(&mut self) { + self.serial += 1; + self.len.increment(); + } + + pub fn called_remove(&mut self) { + self.len.decrement(); + } + + pub fn check_if_in_sync(&self, other: &Self) -> bool { + self.last_date == other.last_date && self.last_height == other.last_height + } + + pub fn check_farer_or_in_sync(&self, other: &Self) -> bool { + self.last_date >= other.last_date && self.last_height >= other.last_height + } +} + +#[derive(Default, Debug, Encode, Decode, Allocative)] +pub struct MetadataData { + pub serial: usize, + pub len: Counter, + pub last_height: Option<usize>, + pub last_date: Option<WNaiveDate>, +} + +impl MetadataData { + fn name<'a>() -> &'a str { + "metadata" + } + + fn full_path(folder_path: &str) -> String { + let name = Self::name(); + format!("{folder_path}/{name}.bin") + } + + pub fn import(path: &str) -> color_eyre::Result<Self> { + fs::create_dir_all(path)?; + + Binary::import(&Self::full_path(path)) + } + + pub fn export(&self, path: &str) -> color_eyre::Result<()> { + Binary::export(&Self::full_path(path), self) + } + + pub fn reset(&mut self, path: &str) -> color_eyre::Result<(), io::Error> { + self.clear(); + + fs::remove_file(Self::full_path(path)) + } + + fn clear(&mut self) { + self.serial = 0; + self.len.reset(); + self.last_height = None; + self.last_date = None; + } +} diff --git a/parser/src/databases/mod.rs b/parser/src/databases/mod.rs new file mode 100644 index 000000000..e8edad1db --- /dev/null +++ b/parser/src/databases/mod.rs @@ -0,0 +1,178 @@ +use std::thread::{self}; + +use allocative::Allocative; + +mod _database; +mod _trait; +mod address_index_to_address_data; +mod address_index_to_empty_address_data; +mod address_to_address_index; +mod metadata; +mod txid_to_tx_data; +mod txout_index_to_address_index; +mod txout_index_to_amount; + +pub use _database::*; +use _trait::*; +pub use address_index_to_address_data::*; +pub use address_index_to_empty_address_data::*; +pub use address_to_address_index::*; +use metadata::*; +pub use txid_to_tx_data::*; +pub use txout_index_to_address_index::*; +pub use txout_index_to_amount::*; + +use crate::{structs::WNaiveDate, utils::time}; + +#[derive(Allocative)] +pub struct Databases { + pub address_index_to_address_data: AddressIndexToAddressData, + pub address_index_to_empty_address_data: AddressIndexToEmptyAddressData, + pub address_to_address_index: AddressToAddressIndex, + pub txid_to_tx_data: TxidToTxData, + pub txout_index_to_address_index: TxoutIndexToAddressIndex, + pub txout_index_to_amount: TxoutIndexToAmount, +} + +impl Databases { + pub fn import() -> Self { + let address_index_to_address_data = AddressIndexToAddressData::import(); + + let address_index_to_empty_address_data = AddressIndexToEmptyAddressData::import(); + + let address_to_address_index = AddressToAddressIndex::import(); + + let txid_to_tx_data = TxidToTxData::import(); + + let txout_index_to_address_index = TxoutIndexToAddressIndex::import(); + + let txout_index_to_amount = TxoutIndexToAmount::import(); + + Self { + address_index_to_address_data, + address_index_to_empty_address_data, + address_to_address_index, + txid_to_tx_data, + txout_index_to_address_index, + txout_index_to_amount, + } + } + + pub fn export(&mut self, height: usize, date: WNaiveDate) -> color_eyre::Result<()> { + thread::scope(|s| { + s.spawn(|| { + time("> Database txid_to_tx_data", || { + self.txid_to_tx_data.export(height, date) + }) + }); + + s.spawn(|| { + time("> Database txout_index_to_amount", || { + self.txout_index_to_amount.export(height, date) + }) + }); + }); + + thread::scope(|s| { + s.spawn(|| { + time("> Database address_index_to_address_data", || { + self.address_index_to_address_data.export(height, date) + }) + }); + + s.spawn(|| { + time("> Database address_index_to_empty_address_data", || { + self.address_index_to_empty_address_data + .export(height, date) + }) + }); + + s.spawn(|| { + time("> Database address_to_address_index", || { + self.address_to_address_index.export(height, date) + }) + }); + + s.spawn(|| { + time("> Database txout_index_to_address_index", || { + self.txout_index_to_address_index.export(height, date) + }) + }); + }); + + Ok(()) + } + + pub fn reset(&mut self, include_addresses: bool) { + if include_addresses { + let _ = self.address_index_to_address_data.reset(); + let _ = self.address_index_to_empty_address_data.reset(); + let _ = self.address_to_address_index.reset(); + let _ = self.txout_index_to_address_index.reset(); + } + + let _ = self.txid_to_tx_data.reset(); + let _ = self.txout_index_to_amount.reset(); + } + + pub fn check_if_needs_to_compute_addresses(&self, height: usize, date: WNaiveDate) -> bool { + let check_height = |last_height: Option<usize>| { + last_height.map_or(true, |last_height| last_height < height) + }; + + let check_date = + |last_date: Option<WNaiveDate>| last_date.map_or(true, |last_date| last_date < date); + + let check_metadata = |metadata: &Metadata| { + check_height(metadata.last_height) || check_date(metadata.last_date) + }; + + // We only need to check one as we previously checked that they're all in sync + check_metadata(&self.address_to_address_index.metadata) + } + + pub fn check_if_usable( + &self, + min_initial_last_address_height: Option<usize>, + min_initial_last_address_date: Option<WNaiveDate>, + ) -> bool { + let are_tx_databases_in_sync = self + .txout_index_to_amount + .metadata + .check_if_in_sync(&self.txid_to_tx_data.metadata); + + if !are_tx_databases_in_sync { + return false; + } + + let are_address_databases_in_sync = self + .address_to_address_index + .metadata + .check_if_in_sync(&self.address_index_to_empty_address_data.metadata) + && self + .address_to_address_index + .metadata + .check_if_in_sync(&self.address_index_to_address_data.metadata) + && self + .address_to_address_index + .metadata + .check_if_in_sync(&self.txout_index_to_address_index.metadata); + + if !are_address_databases_in_sync { + return false; + } + + let are_address_databases_farer_or_in_sync_with_tx_database = self + .address_to_address_index + .metadata + .check_farer_or_in_sync(&self.txid_to_tx_data.metadata); + + if !are_address_databases_farer_or_in_sync_with_tx_database { + return false; + } + + // let are_address_datasets_farer_or_in_sync_with_address_databases = + min_initial_last_address_height >= self.address_to_address_index.metadata.last_height + && min_initial_last_address_date >= self.address_to_address_index.metadata.last_date + } +} diff --git a/parser/src/databases/txid_to_tx_data.rs b/parser/src/databases/txid_to_tx_data.rs new file mode 100644 index 000000000..eaa1ce23c --- /dev/null +++ b/parser/src/databases/txid_to_tx_data.rs @@ -0,0 +1,147 @@ +use std::{ + collections::BTreeMap, + mem, + ops::{Deref, DerefMut}, +}; + +use allocative::Allocative; +use bitcoin::Txid; +use rayon::prelude::*; + +use crate::structs::{TxData, WNaiveDate}; + +use super::{AnyDatabaseGroup, Metadata, SizedDatabase, U8x31}; + +type Key = U8x31; +type Value = TxData; +type Database = SizedDatabase<Key, Value>; + +#[derive(Allocative)] +pub struct TxidToTxData { + pub metadata: Metadata, + + map: BTreeMap<u8, Database>, +} + +impl Deref for TxidToTxData { + type Target = BTreeMap<u8, Database>; + + fn deref(&self) -> &Self::Target { + &self.map + } +} + +impl DerefMut for TxidToTxData { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.map + } +} + +impl TxidToTxData { + pub fn insert(&mut self, txid: &Txid, tx_index: Value) -> Option<Value> { + self.metadata.called_insert(); + + let txid_key = Self::txid_to_key(txid); + + self.open_db(txid).insert(txid_key, tx_index) + } + + // pub fn safe_get(&mut self, txid: &Txid) -> Option<&Value> { + // let txid_key = Self::txid_to_key(txid); + // self.open_db(txid).get(&txid_key) + // } + + /// Doesn't check if the database is open contrary to `safe_get` which does and opens if needed. + /// Though it makes it easy to use with rayon + pub fn unsafe_get(&self, txid: &Txid) -> Option<&Value> { + let txid_key = Self::txid_to_key(txid); + + let db_index = Self::db_index(txid); + + self.get(&db_index).unwrap().get(&txid_key) + } + + // pub fn unsafe_get_from_puts(&self, txid: &Txid) -> Option<&Value> { + // let txid_key = Self::txid_to_key(txid); + + // let db_index = Self::db_index(txid); + + // self.get(&db_index).unwrap().get_from_puts(&txid_key) + // } + + pub fn unsafe_get_mut_from_puts(&mut self, txid: &Txid) -> Option<&mut Value> { + let txid_key = Self::txid_to_key(txid); + + let db_index = Self::db_index(txid); + + self.get_mut(&db_index) + .unwrap() + .get_mut_from_puts(&txid_key) + } + + pub fn remove_from_db(&mut self, txid: &Txid) { + self.metadata.called_remove(); + + let txid_key = Self::txid_to_key(txid); + + self.open_db(txid).db_remove(&txid_key); + } + + pub fn remove_from_puts(&mut self, txid: &Txid) { + self.metadata.called_remove(); + + let txid_key = Self::txid_to_key(txid); + + self.open_db(txid).remove_from_puts(&txid_key); + } + + pub fn update(&mut self, txid: &Txid, tx_data: TxData) { + let txid_key = Self::txid_to_key(txid); + + self.open_db(txid).update(txid_key, tx_data); + } + + #[inline(always)] + pub fn open_db(&mut self, txid: &Txid) -> &mut Database { + let db_index = Self::db_index(txid); + + self.entry(db_index).or_insert_with(|| { + SizedDatabase::open(Self::folder(), &db_index.to_string(), |key| key).unwrap() + }) + } + + fn txid_to_key(txid: &Txid) -> U8x31 { + U8x31::from(&txid[1..]) + } + + fn db_index(txid: &Txid) -> u8 { + txid[0] + } +} + +impl AnyDatabaseGroup for TxidToTxData { + fn import() -> Self { + Self { + map: BTreeMap::default(), + metadata: Metadata::import(&Self::full_path()), + } + } + + fn export(&mut self, height: usize, date: WNaiveDate) -> color_eyre::Result<()> { + mem::take(&mut self.map) + .into_par_iter() + .try_for_each(|(_, db)| db.export())?; + + self.metadata.export(height, date)?; + + Ok(()) + } + + fn reset_metadata(&mut self) { + self.metadata.reset(); + } + + fn folder<'a>() -> &'a str { + "txid_to_tx_data" + } +} diff --git a/parser/src/databases/txout_index_to_address_index.rs b/parser/src/databases/txout_index_to_address_index.rs new file mode 100644 index 000000000..1c2da8a89 --- /dev/null +++ b/parser/src/databases/txout_index_to_address_index.rs @@ -0,0 +1,114 @@ +use std::{ + collections::BTreeMap, + mem, + ops::{Deref, DerefMut}, +}; + +use allocative::Allocative; +use rayon::prelude::*; + +use crate::structs::{TxoutIndex, WNaiveDate}; + +use super::{AnyDatabaseGroup, Metadata, SizedDatabase}; + +type Key = TxoutIndex; +type Value = u32; +type Database = SizedDatabase<Key, Value>; + +#[derive(Allocative)] +pub struct TxoutIndexToAddressIndex { + pub metadata: Metadata, + + map: BTreeMap<usize, Database>, +} + +impl Deref for TxoutIndexToAddressIndex { + type Target = BTreeMap<usize, Database>; + + fn deref(&self) -> &Self::Target { + &self.map + } +} + +impl DerefMut for TxoutIndexToAddressIndex { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.map + } +} + +const DB_MAX_SIZE: usize = 10_000_000_000; + +impl TxoutIndexToAddressIndex { + pub fn unsafe_insert(&mut self, key: Key, value: Value) -> Option<Value> { + self.metadata.called_insert(); + + self.open_db(&key).unsafe_insert(key, value) + } + + // pub fn undo_insert(&mut self, key: &Key) -> Option<Value> { + // self.open_db(key).remove_from_puts(key).map(|v| { + // self.metadata.called_remove(); + + // v + // }) + // } + + pub fn remove(&mut self, key: &Key) -> Option<Value> { + self.metadata.called_remove(); + + self.open_db(key).remove(key) + } + + /// Doesn't check if the database is open contrary to `safe_get` which does and opens if needed + /// Though it makes it easy to use with rayon. + pub fn unsafe_get(&self, key: &Key) -> Option<&Value> { + let db_index = Self::db_index(key); + + self.get(&db_index).unwrap().get(key) + } + + pub fn open_db(&mut self, key: &Key) -> &mut Database { + let db_index = Self::db_index(key); + + self.entry(db_index).or_insert_with(|| { + let db_name = format!( + "{}..{}", + db_index * DB_MAX_SIZE, + (db_index + 1) * DB_MAX_SIZE + ); + + SizedDatabase::open(Self::folder(), &db_name, |key| key).unwrap() + }) + } + + fn db_index(key: &Key) -> usize { + key.as_u64() as usize / DB_MAX_SIZE + } +} + +impl AnyDatabaseGroup for TxoutIndexToAddressIndex { + fn import() -> Self { + Self { + map: BTreeMap::default(), + metadata: Metadata::import(&Self::full_path()), + } + } + + fn export(&mut self, height: usize, date: WNaiveDate) -> color_eyre::Result<()> { + mem::take(&mut self.map) + .into_par_iter() + .try_for_each(|(_, db)| db.export())?; + + self.metadata.export(height, date)?; + + Ok(()) + } + + fn reset_metadata(&mut self) { + self.metadata.reset(); + } + + fn folder<'a>() -> &'a str { + "txout_index_to_address_index" + } +} diff --git a/parser/src/databases/txout_index_to_amount.rs b/parser/src/databases/txout_index_to_amount.rs new file mode 100644 index 000000000..aefc70adb --- /dev/null +++ b/parser/src/databases/txout_index_to_amount.rs @@ -0,0 +1,114 @@ +use std::{ + collections::BTreeMap, + mem, + ops::{Deref, DerefMut}, +}; + +use allocative::Allocative; +use rayon::prelude::*; + +use crate::structs::{TxoutIndex, WAmount, WNaiveDate}; + +use super::{AnyDatabaseGroup, Metadata, SizedDatabase}; + +type Key = TxoutIndex; +type Value = WAmount; +type Database = SizedDatabase<Key, Value>; + +#[derive(Allocative)] +pub struct TxoutIndexToAmount { + pub metadata: Metadata, + + pub map: BTreeMap<usize, Database>, +} + +impl Deref for TxoutIndexToAmount { + type Target = BTreeMap<usize, Database>; + + fn deref(&self) -> &Self::Target { + &self.map + } +} + +impl DerefMut for TxoutIndexToAmount { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.map + } +} + +const DB_MAX_SIZE: usize = 10_000_000_000; + +impl TxoutIndexToAmount { + pub fn unsafe_insert(&mut self, key: Key, value: Value) -> Option<Value> { + self.metadata.called_insert(); + + self.open_db(&key).unsafe_insert(key, value) + } + + // pub fn undo_insert(&mut self, key: &Key) -> Option<Value> { + // self.open_db(key).remove_from_puts(key).map(|v| { + // self.metadata.called_remove(); + + // v + // }) + // } + + pub fn remove(&mut self, key: &Key) -> Option<Value> { + self.metadata.called_remove(); + + self.open_db(key).remove(key) + } + + /// Doesn't check if the database is open contrary to `safe_get` which does and opens if needed + /// Though it makes it easy to use with rayon. + pub fn unsafe_get(&self, key: &Key) -> Option<&Value> { + let db_index = Self::db_index(key); + + self.get(&db_index).unwrap().get(key) + } + + pub fn open_db(&mut self, key: &Key) -> &mut Database { + let db_index = Self::db_index(key); + + self.entry(db_index).or_insert_with(|| { + let db_name = format!( + "{}..{}", + db_index * DB_MAX_SIZE, + (db_index + 1) * DB_MAX_SIZE + ); + + SizedDatabase::open(Self::folder(), &db_name, |key| key).unwrap() + }) + } + + fn db_index(key: &Key) -> usize { + key.as_u64() as usize / DB_MAX_SIZE + } +} + +impl AnyDatabaseGroup for TxoutIndexToAmount { + fn import() -> Self { + Self { + map: BTreeMap::default(), + metadata: Metadata::import(&Self::full_path()), + } + } + + fn export(&mut self, height: usize, date: WNaiveDate) -> color_eyre::Result<()> { + mem::take(&mut self.map) + .into_par_iter() + .try_for_each(|(_, db)| db.export())?; + + self.metadata.export(height, date)?; + + Ok(()) + } + + fn reset_metadata(&mut self) { + self.metadata.reset(); + } + + fn folder<'a>() -> &'a str { + "txout_index_to_amount" + } +} diff --git a/parser/src/datasets/_traits/any_dataset.rs b/parser/src/datasets/_traits/any_dataset.rs new file mode 100644 index 000000000..f08292267 --- /dev/null +++ b/parser/src/datasets/_traits/any_dataset.rs @@ -0,0 +1,286 @@ +use itertools::Itertools; +use rayon::prelude::*; + +use crate::{ + datasets::ComputeData, + structs::{AnyBiMap, AnyDateMap, AnyHeightMap, AnyMap, WNaiveDate}, +}; + +use super::MinInitialStates; + +pub trait AnyDataset { + fn get_min_initial_states(&self) -> &MinInitialStates; + + fn needs_insert(&self, height: usize, date: WNaiveDate) -> bool { + self.needs_insert_height(height) || self.needs_insert_date(date) + } + + #[inline(always)] + fn needs_insert_height(&self, height: usize) -> bool { + !self.to_all_inserted_height_map_vec().is_empty() + && self + .get_min_initial_states() + .inserted + .first_unsafe_height + .unwrap_or(0) + <= height + } + + #[inline(always)] + fn needs_insert_date(&self, date: WNaiveDate) -> bool { + !self.to_all_inserted_date_map_vec().is_empty() + && self + .get_min_initial_states() + .inserted + .first_unsafe_date + .map_or(true, |min_initial_first_unsafe_date| { + min_initial_first_unsafe_date <= date + }) + } + + fn to_inserted_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![] + } + + fn to_inserted_height_map_vec(&self) -> Vec<&(dyn AnyHeightMap + Send + Sync)> { + vec![] + } + + fn to_inserted_date_map_vec(&self) -> Vec<&(dyn AnyDateMap + Send + Sync)> { + vec![] + } + + fn to_inserted_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![] + } + + fn to_inserted_mut_height_map_vec(&mut self) -> Vec<&mut dyn AnyHeightMap> { + vec![] + } + + fn to_inserted_mut_date_map_vec(&mut self) -> Vec<&mut dyn AnyDateMap> { + vec![] + } + + fn to_all_inserted_height_map_vec(&self) -> Vec<&(dyn AnyHeightMap + Send + Sync)> { + let mut vec = self.to_inserted_height_map_vec(); + + vec.append( + &mut self + .to_inserted_bi_map_vec() + .iter() + .map(|bi| bi.get_height()) + .collect_vec(), + ); + + vec + } + + fn to_all_inserted_date_map_vec(&self) -> Vec<&(dyn AnyDateMap + Send + Sync)> { + let mut vec = self.to_inserted_date_map_vec(); + + vec.append( + &mut self + .to_inserted_bi_map_vec() + .iter() + .map(|bi| bi.get_date()) + .collect_vec(), + ); + + vec + } + + fn to_all_inserted_map_vec(&self) -> Vec<&(dyn AnyMap + Send + Sync)> { + let heights = self + .to_all_inserted_height_map_vec() + .into_iter() + .map(|d| d.as_any_map()); + + let dates = self + .to_all_inserted_date_map_vec() + .into_iter() + .map(|d| d.as_any_map()); + + heights.chain(dates).collect_vec() + } + + #[inline(always)] + fn should_compute(&self, compute_data: &ComputeData) -> bool { + compute_data + .heights + .last() + .map_or(false, |height| self.should_compute_height(*height)) + || compute_data + .dates + .last() + .map_or(false, |date| self.should_compute_date(*date)) + } + + #[inline(always)] + fn should_compute_height(&self, height: usize) -> bool { + !self.to_all_computed_height_map_vec().is_empty() + && self + .get_min_initial_states() + .computed + .first_unsafe_height + .unwrap_or(0) + <= height + } + + #[inline(always)] + fn should_compute_date(&self, date: WNaiveDate) -> bool { + !self.to_all_computed_date_map_vec().is_empty() + && self + .get_min_initial_states() + .computed + .first_unsafe_date + .map_or(true, |min_initial_first_unsafe_date| { + min_initial_first_unsafe_date <= date + }) + } + + fn to_computed_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![] + } + + fn to_computed_height_map_vec(&self) -> Vec<&(dyn AnyHeightMap + Send + Sync)> { + vec![] + } + + fn to_computed_date_map_vec(&self) -> Vec<&(dyn AnyDateMap + Send + Sync)> { + vec![] + } + + fn to_computed_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![] + } + + fn to_computed_mut_height_map_vec(&mut self) -> Vec<&mut dyn AnyHeightMap> { + vec![] + } + + fn to_computed_mut_date_map_vec(&mut self) -> Vec<&mut dyn AnyDateMap> { + vec![] + } + + fn to_all_computed_height_map_vec(&self) -> Vec<&(dyn AnyHeightMap + Send + Sync)> { + let mut vec = self.to_computed_height_map_vec(); + + vec.append( + &mut self + .to_computed_bi_map_vec() + .iter() + .map(|bi| bi.get_height()) + .collect_vec(), + ); + + vec + } + + fn to_all_computed_date_map_vec(&self) -> Vec<&(dyn AnyDateMap + Send + Sync)> { + let mut vec = self.to_computed_date_map_vec(); + + vec.append( + &mut self + .to_computed_bi_map_vec() + .iter() + .map(|bi| bi.get_date()) + .collect_vec(), + ); + + vec + } + + fn to_all_computed_map_vec(&self) -> Vec<&(dyn AnyMap + Send + Sync)> { + let heights = self + .to_all_computed_height_map_vec() + .into_iter() + .map(|d| d.as_any_map()); + + let dates = self + .to_all_computed_date_map_vec() + .into_iter() + .map(|d| d.as_any_map()); + + heights.chain(dates).collect_vec() + } + + fn to_all_map_vec(&self) -> Vec<&(dyn AnyMap + Send + Sync)> { + let mut inserted = self.to_all_inserted_map_vec(); + + inserted.append(&mut self.to_all_computed_map_vec()); + + inserted + } + + // #[inline(always)] + // fn is_empty(&self) -> bool { + // self.to_any_map_vec().is_empty() + // } + + fn pre_export(&mut self) { + self.to_inserted_mut_height_map_vec() + .into_iter() + .for_each(|map| map.pre_export()); + + self.to_inserted_mut_date_map_vec() + .into_iter() + .for_each(|map| map.pre_export()); + + self.to_inserted_mut_bi_map_vec().into_iter().for_each(|d| { + d.as_any_mut_map() + .into_iter() + .for_each(|map| map.pre_export()) + }); + + self.to_computed_mut_height_map_vec() + .into_iter() + .for_each(|map| map.pre_export()); + + self.to_computed_mut_date_map_vec() + .into_iter() + .for_each(|map| map.pre_export()); + + self.to_computed_mut_bi_map_vec().into_iter().for_each(|d| { + d.as_any_mut_map() + .into_iter() + .for_each(|map| map.pre_export()) + }); + } + + fn export(&self) -> color_eyre::Result<()> { + self.to_all_map_vec() + .into_par_iter() + .try_for_each(|map| -> color_eyre::Result<()> { map.export() }) + } + + fn post_export(&mut self) { + self.to_inserted_mut_height_map_vec() + .into_iter() + .for_each(|map| map.post_export()); + + self.to_inserted_mut_date_map_vec() + .into_iter() + .for_each(|map| map.post_export()); + + self.to_inserted_mut_bi_map_vec().into_iter().for_each(|d| { + d.as_any_mut_map() + .into_iter() + .for_each(|map| map.post_export()) + }); + + self.to_computed_mut_height_map_vec() + .into_iter() + .for_each(|map| map.post_export()); + + self.to_computed_mut_date_map_vec() + .into_iter() + .for_each(|map| map.post_export()); + + self.to_computed_mut_bi_map_vec().into_iter().for_each(|d| { + d.as_any_mut_map() + .into_iter() + .for_each(|map| map.post_export()) + }); + } +} diff --git a/parser/src/datasets/_traits/any_dataset_group.rs b/parser/src/datasets/_traits/any_dataset_group.rs new file mode 100644 index 000000000..5a8d2868a --- /dev/null +++ b/parser/src/datasets/_traits/any_dataset_group.rs @@ -0,0 +1,7 @@ +use super::AnyDataset; + +pub trait AnyDatasetGroup { + fn as_vec(&self) -> Vec<&(dyn AnyDataset + Send + Sync)>; + + fn as_mut_vec(&mut self) -> Vec<&mut dyn AnyDataset>; +} diff --git a/parser/src/datasets/_traits/any_datasets.rs b/parser/src/datasets/_traits/any_datasets.rs new file mode 100644 index 000000000..bb4bcfec1 --- /dev/null +++ b/parser/src/datasets/_traits/any_datasets.rs @@ -0,0 +1,9 @@ +use super::{AnyDataset, MinInitialStates}; + +pub trait AnyDatasets { + fn get_min_initial_states(&self) -> &MinInitialStates; + + fn to_any_dataset_vec(&self) -> Vec<&(dyn AnyDataset + Send + Sync)>; + + fn to_mut_any_dataset_vec(&mut self) -> Vec<&mut dyn AnyDataset>; +} diff --git a/parser/src/datasets/_traits/min_initial_state.rs b/parser/src/datasets/_traits/min_initial_state.rs new file mode 100644 index 000000000..64e786f60 --- /dev/null +++ b/parser/src/datasets/_traits/min_initial_state.rs @@ -0,0 +1,272 @@ +use allocative::Allocative; + +use crate::structs::{AnyDateMap, AnyHeightMap, WNaiveDate}; + +use super::{AnyDataset, AnyDatasets}; + +#[derive(Default, Debug, Clone, Copy, Allocative)] +pub struct MinInitialStates { + pub inserted: MinInitialState, + pub computed: MinInitialState, +} + +impl MinInitialStates { + pub fn consume(&mut self, other: Self) { + self.inserted = other.inserted; + self.computed = other.computed; + } + + pub fn compute_from_dataset(dataset: &dyn AnyDataset) -> Self { + Self { + inserted: MinInitialState::compute_from_dataset(dataset, Mode::Inserted), + computed: MinInitialState::compute_from_dataset(dataset, Mode::Computed), + } + } + + pub fn compute_from_datasets(datasets: &dyn AnyDatasets) -> Self { + Self { + inserted: MinInitialState::compute_from_datasets(datasets, Mode::Inserted), + computed: MinInitialState::compute_from_datasets(datasets, Mode::Computed), + } + } +} + +#[derive(Default, Debug, Clone, Copy, Allocative)] +pub struct MinInitialState { + pub first_unsafe_date: Option<WNaiveDate>, + pub first_unsafe_height: Option<usize>, + pub last_date: Option<WNaiveDate>, + pub last_height: Option<usize>, +} + +enum Mode { + Inserted, + Computed, +} + +impl MinInitialState { + // pub fn consume(&mut self, other: Self) { + // self.first_unsafe_date = other.first_unsafe_date; + // self.first_unsafe_height = other.first_unsafe_height; + // self.last_date = other.last_date; + // self.last_height = other.last_height; + // } + + fn compute_from_datasets(datasets: &dyn AnyDatasets, mode: Mode) -> Self { + match mode { + Mode::Inserted => { + let contains_date_maps = |dataset: &&(dyn AnyDataset + Sync + Send)| { + !dataset.to_all_inserted_date_map_vec().is_empty() + }; + + let contains_height_maps = |dataset: &&(dyn AnyDataset + Sync + Send)| { + !dataset.to_all_inserted_height_map_vec().is_empty() + }; + + Self { + first_unsafe_date: Self::min_datasets_date( + datasets, + contains_date_maps, + |dataset| { + dataset + .get_min_initial_states() + .inserted + .first_unsafe_date + .as_ref() + .cloned() + }, + ), + first_unsafe_height: Self::min_datasets_height( + datasets, + contains_height_maps, + |dataset| { + dataset + .get_min_initial_states() + .inserted + .first_unsafe_height + .as_ref() + .cloned() + }, + ), + last_date: Self::min_datasets_date(datasets, contains_date_maps, |dataset| { + dataset + .get_min_initial_states() + .inserted + .last_date + .as_ref() + .cloned() + }), + last_height: Self::min_datasets_height( + datasets, + contains_height_maps, + |dataset| { + dataset + .get_min_initial_states() + .inserted + .last_height + .as_ref() + .cloned() + }, + ), + } + } + Mode::Computed => { + let contains_date_maps = |dataset: &&(dyn AnyDataset + Sync + Send)| { + !dataset.to_all_computed_date_map_vec().is_empty() + }; + + let contains_height_maps = |dataset: &&(dyn AnyDataset + Sync + Send)| { + !dataset.to_all_computed_height_map_vec().is_empty() + }; + + Self { + first_unsafe_date: Self::min_datasets_date( + datasets, + contains_date_maps, + |dataset| { + dataset + .get_min_initial_states() + .computed + .first_unsafe_date + .as_ref() + .cloned() + }, + ), + first_unsafe_height: Self::min_datasets_height( + datasets, + contains_height_maps, + |dataset| { + dataset + .get_min_initial_states() + .computed + .first_unsafe_height + .as_ref() + .cloned() + }, + ), + last_date: Self::min_datasets_date(datasets, contains_date_maps, |dataset| { + dataset + .get_min_initial_states() + .computed + .last_date + .as_ref() + .cloned() + }), + last_height: Self::min_datasets_height( + datasets, + contains_height_maps, + |dataset| { + dataset + .get_min_initial_states() + .computed + .last_height + .as_ref() + .cloned() + }, + ), + } + } + } + } + + fn min_datasets_date( + datasets: &dyn AnyDatasets, + is_not_empty: impl Fn(&&(dyn AnyDataset + Sync + Send)) -> bool, + map: impl Fn(&(dyn AnyDataset + Sync + Send)) -> Option<WNaiveDate>, + ) -> Option<WNaiveDate> { + Self::min_date( + datasets + .to_any_dataset_vec() + .into_iter() + .filter(is_not_empty) + .map(map), + ) + } + + fn min_datasets_height( + datasets: &dyn AnyDatasets, + is_not_empty: impl Fn(&&(dyn AnyDataset + Sync + Send)) -> bool, + map: impl Fn(&(dyn AnyDataset + Sync + Send)) -> Option<usize>, + ) -> Option<usize> { + Self::min_height( + datasets + .to_any_dataset_vec() + .into_iter() + .filter(is_not_empty) + .map(map), + ) + } + + fn compute_from_dataset(dataset: &dyn AnyDataset, mode: Mode) -> Self { + match mode { + Mode::Inserted => { + let date_vec = dataset.to_all_inserted_date_map_vec(); + let height_vec = dataset.to_all_inserted_height_map_vec(); + + Self { + first_unsafe_date: Self::compute_min_initial_first_unsafe_date_from_dataset( + &date_vec, + ), + first_unsafe_height: Self::compute_min_initial_first_unsafe_height_from_dataset( + &height_vec, + ), + last_date: Self::compute_min_initial_last_date_from_dataset(&date_vec), + last_height: Self::compute_min_initial_last_height_from_dataset(&height_vec), + } + } + Mode::Computed => { + let date_vec = dataset.to_all_computed_date_map_vec(); + let height_vec = dataset.to_all_computed_height_map_vec(); + + Self { + first_unsafe_date: Self::compute_min_initial_first_unsafe_date_from_dataset( + &date_vec, + ), + first_unsafe_height: Self::compute_min_initial_first_unsafe_height_from_dataset( + &height_vec, + ), + last_date: Self::compute_min_initial_last_date_from_dataset(&date_vec), + last_height: Self::compute_min_initial_last_height_from_dataset(&height_vec), + } + } + } + } + + #[inline(always)] + fn compute_min_initial_last_date_from_dataset( + arr: &[&(dyn AnyDateMap + Sync + Send)], + ) -> Option<WNaiveDate> { + Self::min_date(arr.iter().map(|map| map.get_initial_last_date())) + } + + #[inline(always)] + fn compute_min_initial_last_height_from_dataset( + arr: &[&(dyn AnyHeightMap + Sync + Send)], + ) -> Option<usize> { + Self::min_height(arr.iter().map(|map| map.get_initial_last_height())) + } + + #[inline(always)] + fn compute_min_initial_first_unsafe_date_from_dataset( + arr: &[&(dyn AnyDateMap + Sync + Send)], + ) -> Option<WNaiveDate> { + Self::min_date(arr.iter().map(|map| map.get_initial_first_unsafe_date())) + } + + #[inline(always)] + fn compute_min_initial_first_unsafe_height_from_dataset( + arr: &[&(dyn AnyHeightMap + Sync + Send)], + ) -> Option<usize> { + Self::min_height(arr.iter().map(|map| map.get_initial_first_unsafe_height())) + } + + #[inline(always)] + fn min_date(iter: impl Iterator<Item = Option<WNaiveDate>>) -> Option<WNaiveDate> { + iter.min().and_then(|opt| opt) + } + + #[inline(always)] + fn min_height(iter: impl Iterator<Item = Option<usize>>) -> Option<usize> { + iter.min().and_then(|opt| opt) + } +} diff --git a/parser/src/datasets/_traits/mod.rs b/parser/src/datasets/_traits/mod.rs new file mode 100644 index 000000000..8c556298c --- /dev/null +++ b/parser/src/datasets/_traits/mod.rs @@ -0,0 +1,9 @@ +mod any_dataset; +mod any_dataset_group; +mod any_datasets; +mod min_initial_state; + +pub use any_dataset::*; +pub use any_dataset_group::*; +pub use any_datasets::*; +pub use min_initial_state::*; diff --git a/parser/src/datasets/address/all_metadata.rs b/parser/src/datasets/address/all_metadata.rs new file mode 100644 index 000000000..d32649eff --- /dev/null +++ b/parser/src/datasets/address/all_metadata.rs @@ -0,0 +1,91 @@ +use allocative::Allocative; + +use crate::{ + datasets::{AnyDataset, ComputeData, InsertData, MinInitialStates}, + structs::{AnyBiMap, BiMap}, +}; + +#[derive(Allocative)] +pub struct AllAddressesMetadataDataset { + min_initial_states: MinInitialStates, + + // Inserted + created_addreses: BiMap<u32>, + empty_addresses: BiMap<u32>, + + // Computed + new_addresses: BiMap<u32>, +} + +impl AllAddressesMetadataDataset { + pub fn import(parent_path: &str) -> color_eyre::Result<Self> { + let f = |s: &str| format!("{parent_path}/{s}"); + + let mut s = Self { + min_initial_states: MinInitialStates::default(), + + // TODO: Shouldn't be (like many others) + created_addreses: BiMap::new_bin(1, &f("created_addresses")), + empty_addresses: BiMap::new_bin(1, &f("empty_addresses")), + new_addresses: BiMap::new_bin(1, &f("new_addresses")), + }; + + s.min_initial_states + .consume(MinInitialStates::compute_from_dataset(&s)); + + Ok(s) + } + + pub fn insert(&mut self, insert_data: &InsertData) { + let &InsertData { + databases, + height, + date, + is_date_last_block, + .. + } = insert_data; + + let created_addresses = self + .created_addreses + .height + .insert(height, *databases.address_to_address_index.metadata.len); + + let empty_addresses = self.empty_addresses.height.insert( + height, + *databases.address_index_to_empty_address_data.metadata.len, + ); + + if is_date_last_block { + self.created_addreses.date.insert(date, created_addresses); + + self.empty_addresses.date.insert(date, empty_addresses); + } + } + + pub fn compute(&mut self, &ComputeData { heights, dates }: &ComputeData) { + self.new_addresses + .multi_insert_net_change(heights, dates, &mut self.created_addreses, 1) + } +} + +impl AnyDataset for AllAddressesMetadataDataset { + fn get_min_initial_states(&self) -> &MinInitialStates { + &self.min_initial_states + } + + fn to_inserted_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![&self.created_addreses, &self.empty_addresses] + } + + fn to_inserted_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![&mut self.created_addreses, &mut self.empty_addresses] + } + + fn to_computed_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![&self.new_addresses] + } + + fn to_computed_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![&mut self.new_addresses] + } +} diff --git a/parser/src/datasets/address/cohort.rs b/parser/src/datasets/address/cohort.rs new file mode 100644 index 000000000..633137799 --- /dev/null +++ b/parser/src/datasets/address/cohort.rs @@ -0,0 +1,703 @@ +use allocative::Allocative; +use itertools::Itertools; + +use crate::{ + datasets::{ + AnyDataset, AnyDatasetGroup, ComputeData, InsertData, MinInitialStates, SubDataset, + }, + states::{AddressCohortDurableStates, AddressCohortId}, + structs::{AddressSplit, AnyBiMap, AnyDateMap, AnyHeightMap, BiMap, WNaiveDate}, +}; + +use super::cohort_metadata::MetadataDataset; + +#[derive(Default, Allocative)] +pub struct CohortDataset { + min_initial_states: MinInitialStates, + + split: AddressSplit, + + metadata: MetadataDataset, + + pub all: SubDataset, + illiquid: SubDataset, + liquid: SubDataset, + highly_liquid: SubDataset, +} + +impl CohortDataset { + pub fn import(parent_path: &str, id: AddressCohortId) -> color_eyre::Result<Self> { + let name = id.as_name(); + let split = id.as_split(); + + let folder_path = { + if let Some(name) = name { + format!("{parent_path}/{name}") + } else { + parent_path.to_owned() + } + }; + + let f = |s: &str| { + if let Some(name) = name { + format!("{parent_path}/{s}/{name}") + } else { + format!("{parent_path}/{s}") + } + }; + + let mut s = Self { + min_initial_states: MinInitialStates::default(), + + split, + + metadata: MetadataDataset::import(&folder_path)?, + all: SubDataset::import(&folder_path)?, + illiquid: SubDataset::import(&f("illiquid"))?, + liquid: SubDataset::import(&f("liquid"))?, + highly_liquid: SubDataset::import(&f("highly_liquid"))?, + }; + + s.min_initial_states + .consume(MinInitialStates::compute_from_dataset(&s)); + + Ok(s) + } + + pub fn sub_datasets_vec(&self) -> Vec<&SubDataset> { + vec![&self.all, &self.illiquid, &self.liquid, &self.highly_liquid] + } + + pub fn needs_insert_metadata(&self, height: usize, date: WNaiveDate) -> bool { + self.metadata.needs_insert(height, date) + } + + pub fn needs_insert_utxo(&self, height: usize, date: WNaiveDate) -> bool { + self.sub_datasets_vec() + .iter() + .any(|sub| sub.utxo.needs_insert(height, date)) + } + + pub fn needs_insert_capitalization(&self, height: usize, date: WNaiveDate) -> bool { + self.sub_datasets_vec() + .iter() + .any(|sub| sub.capitalization.needs_insert(height, date)) + } + + pub fn needs_insert_supply(&self, height: usize, date: WNaiveDate) -> bool { + self.sub_datasets_vec() + .iter() + .any(|sub| sub.supply.needs_insert(height, date)) + } + + pub fn needs_insert_price_paid(&self, height: usize, date: WNaiveDate) -> bool { + self.sub_datasets_vec() + .iter() + .any(|sub| sub.price_paid.needs_insert(height, date)) + } + + fn needs_insert_realized(&self, height: usize, date: WNaiveDate) -> bool { + self.sub_datasets_vec() + .iter() + .any(|sub| sub.realized.needs_insert(height, date)) + } + + fn needs_insert_unrealized(&self, height: usize, date: WNaiveDate) -> bool { + self.sub_datasets_vec() + .iter() + .any(|sub| sub.unrealized.needs_insert(height, date)) + } + + fn needs_insert_input(&self, height: usize, date: WNaiveDate) -> bool { + self.sub_datasets_vec() + .iter() + .any(|sub| sub.input.needs_insert(height, date)) + } + + // fn needs_insert_output(&self, insert_data: &InsertData) -> bool { + // self.sub_datasets_vec() + // .iter() + // .any(|sub| sub.output.needs_insert(height, date)) + // } + + fn insert_realized_data(&mut self, insert_data: &InsertData) { + let split_realized_state = insert_data + .address_cohorts_realized_states + .as_ref() + .unwrap() + .get(&self.split) + .unwrap(); + + self.all + .realized + .insert(insert_data, &split_realized_state.all); + + self.illiquid + .realized + .insert(insert_data, &split_realized_state.illiquid); + + self.liquid + .realized + .insert(insert_data, &split_realized_state.liquid); + + self.highly_liquid + .realized + .insert(insert_data, &split_realized_state.highly_liquid); + } + + fn insert_metadata(&mut self, insert_data: &InsertData) { + let address_count = insert_data + .states + .address_cohorts_durable_states + .get(&self.split) + .unwrap() + .address_count; + + self.metadata.insert(insert_data, address_count); + } + + fn insert_supply_data( + &mut self, + insert_data: &InsertData, + liquidity_split_state: &AddressCohortDurableStates, + ) { + self.all.supply.insert( + insert_data, + &liquidity_split_state.split_durable_states.all.supply_state, + ); + + self.illiquid.supply.insert( + insert_data, + &liquidity_split_state + .split_durable_states + .illiquid + .supply_state, + ); + + self.liquid.supply.insert( + insert_data, + &liquidity_split_state + .split_durable_states + .liquid + .supply_state, + ); + + self.highly_liquid.supply.insert( + insert_data, + &liquidity_split_state + .split_durable_states + .highly_liquid + .supply_state, + ); + } + + fn insert_utxo_data( + &mut self, + insert_data: &InsertData, + liquidity_split_state: &AddressCohortDurableStates, + ) { + self.all.utxo.insert( + insert_data, + &liquidity_split_state.split_durable_states.all.utxo_state, + ); + + self.illiquid.utxo.insert( + insert_data, + &liquidity_split_state + .split_durable_states + .illiquid + .utxo_state, + ); + + self.liquid.utxo.insert( + insert_data, + &liquidity_split_state.split_durable_states.liquid.utxo_state, + ); + + self.highly_liquid.utxo.insert( + insert_data, + &liquidity_split_state + .split_durable_states + .highly_liquid + .utxo_state, + ); + } + + fn insert_capitalization_data( + &mut self, + insert_data: &InsertData, + liquidity_split_state: &AddressCohortDurableStates, + ) { + self.all.capitalization.insert( + insert_data, + &liquidity_split_state + .split_durable_states + .all + .capitalization_state, + ); + + self.illiquid.capitalization.insert( + insert_data, + &liquidity_split_state + .split_durable_states + .illiquid + .capitalization_state, + ); + + self.liquid.capitalization.insert( + insert_data, + &liquidity_split_state + .split_durable_states + .liquid + .capitalization_state, + ); + + self.highly_liquid.capitalization.insert( + insert_data, + &liquidity_split_state + .split_durable_states + .highly_liquid + .capitalization_state, + ); + } + + fn insert_unrealized_data(&mut self, insert_data: &InsertData) { + let states = insert_data + .address_cohorts_one_shot_states + .as_ref() + .unwrap() + .get(&self.split) + .unwrap(); + + self.all.unrealized.insert( + insert_data, + &states.all.unrealized_block_state, + &states.all.unrealized_date_state, + ); + + self.illiquid.unrealized.insert( + insert_data, + &states.illiquid.unrealized_block_state, + &states.illiquid.unrealized_date_state, + ); + + self.liquid.unrealized.insert( + insert_data, + &states.liquid.unrealized_block_state, + &states.liquid.unrealized_date_state, + ); + + self.highly_liquid.unrealized.insert( + insert_data, + &states.highly_liquid.unrealized_block_state, + &states.highly_liquid.unrealized_date_state, + ); + } + + fn insert_price_paid_data(&mut self, insert_data: &InsertData) { + let states = insert_data + .address_cohorts_one_shot_states + .as_ref() + .unwrap() + .get(&self.split) + .unwrap(); + + self.all + .price_paid + .insert(insert_data, &states.all.price_paid_state); + + self.illiquid + .price_paid + .insert(insert_data, &states.illiquid.price_paid_state); + + self.liquid + .price_paid + .insert(insert_data, &states.liquid.price_paid_state); + + self.highly_liquid + .price_paid + .insert(insert_data, &states.highly_liquid.price_paid_state); + } + + fn insert_input_data(&mut self, insert_data: &InsertData) { + let state = insert_data + .address_cohorts_input_states + .as_ref() + .unwrap() + .get(&self.split) + .unwrap(); + + self.all.input.insert(insert_data, &state.all); + self.illiquid.input.insert(insert_data, &state.illiquid); + self.liquid.input.insert(insert_data, &state.liquid); + self.highly_liquid + .input + .insert(insert_data, &state.highly_liquid); + } + + // fn insert_output_data(&mut self, insert_data: &InsertData) { + // let state = insert_data + // .address_cohorts_output_states + // .as_ref() + // .unwrap() + // .get(&self.split) + // .unwrap(); + + // self.all.output.insert(insert_data, &state.all); + // self.illiquid.output.insert(insert_data, &state.illiquid); + // self.liquid.output.insert(insert_data, &state.liquid); + // self.highly_liquid + // .output + // .insert(insert_data, &state.highly_liquid); + // } + + fn as_vec(&self) -> Vec<&(dyn AnyDataset + Send + Sync)> { + vec![ + self.all.as_vec(), + self.illiquid.as_vec(), + self.liquid.as_vec(), + self.highly_liquid.as_vec(), + vec![&self.metadata], + ] + .into_iter() + .flatten() + .collect_vec() + } + + fn as_mut_vec(&mut self) -> Vec<&mut dyn AnyDataset> { + vec![ + self.all.as_mut_vec(), + self.illiquid.as_mut_vec(), + self.liquid.as_mut_vec(), + self.highly_liquid.as_mut_vec(), + vec![&mut self.metadata], + ] + .into_iter() + .flatten() + .collect_vec() + } + + pub fn insert(&mut self, insert_data: &InsertData) { + if !insert_data.compute_addresses { + return; + } + + let liquidity_split_processed_address_state = insert_data + .states + .address_cohorts_durable_states + .get(&self.split); + + if liquidity_split_processed_address_state.is_none() { + return; // TODO: Check if should panic instead + } + + let liquidity_split_processed_address_state = + liquidity_split_processed_address_state.unwrap(); + + if self.needs_insert_metadata(insert_data.height, insert_data.date) { + self.insert_metadata(insert_data); + } + + if self.needs_insert_utxo(insert_data.height, insert_data.date) { + self.insert_utxo_data(insert_data, liquidity_split_processed_address_state); + } + + if self.needs_insert_capitalization(insert_data.height, insert_data.date) { + self.insert_capitalization_data(insert_data, liquidity_split_processed_address_state); + } + + if self.needs_insert_supply(insert_data.height, insert_data.date) { + self.insert_supply_data(insert_data, liquidity_split_processed_address_state); + } + + if self.needs_insert_realized(insert_data.height, insert_data.date) { + self.insert_realized_data(insert_data); + } + + if self.needs_insert_unrealized(insert_data.height, insert_data.date) { + self.insert_unrealized_data(insert_data); + } + + if self.needs_insert_price_paid(insert_data.height, insert_data.date) { + self.insert_price_paid_data(insert_data); + } + + if self.needs_insert_input(insert_data.height, insert_data.date) { + self.insert_input_data(insert_data); + } + + // if self.needs_insert_output(insert_data) { + // self.insert_output_data(insert_data); + // } + } + + // pub fn should_compute_metadata(&self, compute_data: &ComputeData) -> bool { + // self.metadata.should_compute(compute_data) + // } + + // pub fn should_compute_utxo(&self, compute_data: &ComputeData) -> bool { + // self.sub_datasets_vec() + // .iter() + // .any(|sub| sub.utxo.should_compute(compute_data)) + // } + + pub fn should_compute_supply(&self, compute_data: &ComputeData) -> bool { + self.sub_datasets_vec() + .iter() + .any(|sub| sub.supply.should_compute(compute_data)) + } + + pub fn should_compute_capitalization(&self, compute_data: &ComputeData) -> bool { + self.sub_datasets_vec() + .iter() + .any(|sub| sub.capitalization.should_compute(compute_data)) + } + + fn should_compute_realized(&self, compute_data: &ComputeData) -> bool { + self.sub_datasets_vec() + .iter() + .any(|sub| sub.realized.should_compute(compute_data)) + } + + fn should_compute_unrealized(&self, compute_data: &ComputeData) -> bool { + self.sub_datasets_vec() + .iter() + .any(|sub| sub.unrealized.should_compute(compute_data)) + } + + // fn should_compute_input(&self, compute_data: &ComputeData) -> bool { + // self.sub_datasets_vec() + // .iter() + // .any(|sub| sub.input.should_compute(compute_data)) + // } + + // fn should_compute_output(&self, compute_data: &ComputeData) -> bool { + // self.sub_datasets_vec() + // .iter() + // .any(|sub| sub.output.should_compute(compute_data)) + // } + + fn compute_supply_data( + &mut self, + compute_data: &ComputeData, + circulating_supply: &mut BiMap<f64>, + ) { + self.all.supply.compute(compute_data, circulating_supply); + + self.illiquid + .supply + .compute(compute_data, circulating_supply); + + self.liquid.supply.compute(compute_data, circulating_supply); + + self.highly_liquid + .supply + .compute(compute_data, circulating_supply); + } + + fn compute_unrealized_data( + &mut self, + compute_data: &ComputeData, + circulating_supply: &mut BiMap<f64>, + market_cap: &mut BiMap<f32>, + ) { + self.all.unrealized.compute( + compute_data, + &mut self.all.supply.supply, + circulating_supply, + market_cap, + ); + + self.illiquid.unrealized.compute( + compute_data, + &mut self.illiquid.supply.supply, + circulating_supply, + market_cap, + ); + + self.liquid.unrealized.compute( + compute_data, + &mut self.liquid.supply.supply, + circulating_supply, + market_cap, + ); + + self.highly_liquid.unrealized.compute( + compute_data, + &mut self.highly_liquid.supply.supply, + circulating_supply, + market_cap, + ); + } + + fn compute_realized_data(&mut self, compute_data: &ComputeData, market_cap: &mut BiMap<f32>) { + self.all.realized.compute(compute_data, market_cap); + + self.illiquid.realized.compute(compute_data, market_cap); + + self.liquid.realized.compute(compute_data, market_cap); + + self.highly_liquid + .realized + .compute(compute_data, market_cap); + } + + fn compute_capitalization_data(&mut self, compute_data: &ComputeData, closes: &mut BiMap<f32>) { + self.all + .capitalization + .compute(compute_data, closes, &mut self.all.supply.supply); + + self.illiquid.capitalization.compute( + compute_data, + closes, + &mut self.illiquid.supply.supply, + ); + + self.liquid + .capitalization + .compute(compute_data, closes, &mut self.liquid.supply.supply); + + self.highly_liquid.capitalization.compute( + compute_data, + closes, + &mut self.highly_liquid.supply.supply, + ); + } + + // fn compute_output_data(&mut self, compute_data: &ComputeData) { + // self.all + // .output + // .compute(compute_data, &mut self.all.supply.total); + + // self.illiquid + // .output + // .compute(compute_data, &mut self.illiquid.supply.total); + + // self.liquid + // .output + // .compute(compute_data, &mut self.liquid.supply.total); + + // self.highly_liquid + // .output + // .compute(compute_data, &mut self.highly_liquid.supply.total); + // } + + pub fn compute( + &mut self, + compute_data: &ComputeData, + closes: &mut BiMap<f32>, + circulating_supply: &mut BiMap<f64>, + market_cap: &mut BiMap<f32>, + ) { + if self.should_compute_supply(compute_data) { + self.compute_supply_data(compute_data, circulating_supply); + } + + if self.should_compute_unrealized(compute_data) { + self.compute_unrealized_data(compute_data, circulating_supply, market_cap); + } + + if self.should_compute_realized(compute_data) { + self.compute_realized_data(compute_data, market_cap); + } + + // MUST BE after compute_supply + if self.should_compute_capitalization(compute_data) { + self.compute_capitalization_data(compute_data, closes); + } + + // if self.should_compute_output(compute_data) { + // self.compute_output_data(compute_data); + // } + } +} + +impl AnyDataset for CohortDataset { + fn to_inserted_height_map_vec(&self) -> Vec<&(dyn AnyHeightMap + Send + Sync)> { + self.as_vec() + .into_iter() + .flat_map(|d| d.to_inserted_height_map_vec()) + .collect_vec() + } + + fn to_inserted_date_map_vec(&self) -> Vec<&(dyn AnyDateMap + Send + Sync)> { + self.as_vec() + .into_iter() + .flat_map(|d| d.to_inserted_date_map_vec()) + .collect_vec() + } + + fn to_inserted_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + self.as_vec() + .into_iter() + .flat_map(|d| d.to_inserted_bi_map_vec()) + .collect_vec() + } + + fn to_inserted_mut_height_map_vec(&mut self) -> Vec<&mut dyn AnyHeightMap> { + self.as_mut_vec() + .into_iter() + .flat_map(|d| d.to_inserted_mut_height_map_vec()) + .collect_vec() + } + + fn to_inserted_mut_date_map_vec(&mut self) -> Vec<&mut dyn AnyDateMap> { + self.as_mut_vec() + .into_iter() + .flat_map(|d| d.to_inserted_mut_date_map_vec()) + .collect_vec() + } + + fn to_inserted_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + self.as_mut_vec() + .into_iter() + .flat_map(|d| d.to_inserted_mut_bi_map_vec()) + .collect_vec() + } + + fn to_computed_height_map_vec(&self) -> Vec<&(dyn AnyHeightMap + Send + Sync)> { + self.as_vec() + .into_iter() + .flat_map(|d| d.to_computed_height_map_vec()) + .collect_vec() + } + + fn to_computed_date_map_vec(&self) -> Vec<&(dyn AnyDateMap + Send + Sync)> { + self.as_vec() + .into_iter() + .flat_map(|d| d.to_computed_date_map_vec()) + .collect_vec() + } + + fn to_computed_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + self.as_vec() + .into_iter() + .flat_map(|d| d.to_computed_bi_map_vec()) + .collect_vec() + } + + fn to_computed_mut_height_map_vec(&mut self) -> Vec<&mut dyn AnyHeightMap> { + self.as_mut_vec() + .into_iter() + .flat_map(|d| d.to_computed_mut_height_map_vec()) + .collect_vec() + } + + fn to_computed_mut_date_map_vec(&mut self) -> Vec<&mut dyn AnyDateMap> { + self.as_mut_vec() + .into_iter() + .flat_map(|d| d.to_computed_mut_date_map_vec()) + .collect_vec() + } + + fn to_computed_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + self.as_mut_vec() + .into_iter() + .flat_map(|d| d.to_computed_mut_bi_map_vec()) + .collect_vec() + } + + fn get_min_initial_states(&self) -> &MinInitialStates { + &self.min_initial_states + } +} diff --git a/parser/src/datasets/address/cohort_metadata.rs b/parser/src/datasets/address/cohort_metadata.rs new file mode 100644 index 000000000..46e43772f --- /dev/null +++ b/parser/src/datasets/address/cohort_metadata.rs @@ -0,0 +1,67 @@ +use allocative::Allocative; + +use crate::{ + datasets::{AnyDataset, InsertData, MinInitialStates}, + structs::{AnyBiMap, BiMap}, +}; + +#[derive(Default, Allocative)] +pub struct MetadataDataset { + min_initial_states: MinInitialStates, + + // Inserted + address_count: BiMap<usize>, + // pub output: OutputSubDataset, + // Sending addresses + // Receiving addresses + // Active addresses (Unique(Sending + Receiving)) +} + +impl MetadataDataset { + pub fn import(parent_path: &str) -> color_eyre::Result<Self> { + let f = |s: &str| format!("{parent_path}/{s}"); + + let mut s = Self { + min_initial_states: MinInitialStates::default(), + + address_count: BiMap::new_bin(1, &f("address_count")), + // output: OutputSubDataset::import(parent_path)?, + }; + + s.min_initial_states + .consume(MinInitialStates::compute_from_dataset(&s)); + + Ok(s) + } + + pub fn insert( + &mut self, + &InsertData { + height, + date, + is_date_last_block, + .. + }: &InsertData, + address_count: usize, + ) { + self.address_count.height.insert(height, address_count); + + if is_date_last_block { + self.address_count.date.insert(date, address_count); + } + } +} + +impl AnyDataset for MetadataDataset { + fn get_min_initial_states(&self) -> &MinInitialStates { + &self.min_initial_states + } + + fn to_inserted_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![&self.address_count] + } + + fn to_inserted_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![&mut self.address_count] + } +} diff --git a/parser/src/datasets/address/mod.rs b/parser/src/datasets/address/mod.rs new file mode 100644 index 000000000..1aee66b5b --- /dev/null +++ b/parser/src/datasets/address/mod.rs @@ -0,0 +1,151 @@ +mod all_metadata; +mod cohort; +mod cohort_metadata; + +use allocative::Allocative; +use itertools::Itertools; +use rayon::prelude::*; + +use crate::{states::SplitByAddressCohort, structs::BiMap}; + +use self::{all_metadata::AllAddressesMetadataDataset, cohort::CohortDataset}; + +use super::{AnyDataset, AnyDatasets, ComputeData, InsertData, MinInitialStates}; + +#[derive(Allocative)] +pub struct AddressDatasets { + min_initial_states: MinInitialStates, + + metadata: AllAddressesMetadataDataset, + + pub cohorts: SplitByAddressCohort<CohortDataset>, +} + +impl AddressDatasets { + pub fn import(parent_path: &str) -> color_eyre::Result<Self> { + let mut cohorts = SplitByAddressCohort::<CohortDataset>::default(); + + cohorts + .as_vec() + .into_par_iter() + .map(|(_, id)| (id, CohortDataset::import(parent_path, id))) + .collect::<Vec<_>>() + .into_iter() + .try_for_each(|(id, dataset)| -> color_eyre::Result<()> { + *cohorts.get_mut_from_id(&id) = dataset?; + Ok(()) + })?; + + let mut s = Self { + min_initial_states: MinInitialStates::default(), + + metadata: AllAddressesMetadataDataset::import(parent_path)?, + + cohorts, + }; + + s.min_initial_states + .consume(MinInitialStates::compute_from_datasets(&s)); + + Ok(s) + } + + pub fn insert(&mut self, insert_data: &InsertData) { + self.metadata.insert(insert_data); + + self.cohorts + .as_mut_vec() + .into_iter() + .for_each(|(cohort, _)| cohort.insert(insert_data)) + } + + // pub fn needs_insert_utxo(&self, height: usize, date: WNaiveDate) -> bool { + // self.cohorts + // .as_vec() + // .iter() + // .any(|(dataset, _)| dataset.utxo.needs_insert(height, date)) + // } + + // pub fn needs_insert_capitalization(&self, height: usize, date: WNaiveDate) -> bool { + // self.cohorts + // .as_vec() + // .iter() + // .any(|(dataset, _)| dataset.capitalization.needs_insert(height, date)) + // } + + // pub fn needs_insert_supply(&self, height: usize, date: WNaiveDate) -> bool { + // self.cohorts + // .as_vec() + // .iter() + // .any(|(dataset, _)| dataset.supply.needs_insert(height, date)) + // } + + // pub fn needs_insert_price_paid(&self, height: usize, date: WNaiveDate) -> bool { + // self.cohorts + // .as_vec() + // .iter() + // .any(|(dataset, _)| dataset.price_paid.needs_insert(height, date)) + // } + + // fn needs_insert_realized(&self, height: usize, date: WNaiveDate) -> bool { + // self.cohorts + // .as_vec() + // .iter() + // .any(|(dataset, _)| dataset.realized.needs_insert(height, date)) + // } + + // fn needs_insert_unrealized(&self, height: usize, date: WNaiveDate) -> bool { + // self.cohorts + // .as_vec() + // .iter() + // .any(|(dataset, _)| dataset.unrealized.needs_insert(height, date)) + // } + + // fn needs_insert_input(&self, height: usize, date: WNaiveDate) -> bool { + // self.cohorts + // .as_vec() + // .iter() + // .any(|(dataset, _)| dataset.input.needs_insert(height, date)) + // } + + pub fn compute( + &mut self, + compute_data: &ComputeData, + closes: &mut BiMap<f32>, + circulating_supply: &mut BiMap<f64>, + market_cap: &mut BiMap<f32>, + ) { + self.metadata.compute(compute_data); + + self.cohorts + .as_mut_vec() + .into_iter() + .for_each(|(cohort, _)| { + cohort.compute(compute_data, closes, circulating_supply, market_cap) + }) + } +} + +impl AnyDatasets for AddressDatasets { + fn get_min_initial_states(&self) -> &MinInitialStates { + &self.min_initial_states + } + + fn to_any_dataset_vec(&self) -> Vec<&(dyn AnyDataset + Send + Sync)> { + self.cohorts + .as_vec() + .into_iter() + .map(|(d, _)| d as &(dyn AnyDataset + Send + Sync)) + .chain(vec![&self.metadata as &(dyn AnyDataset + Send + Sync)]) + .collect_vec() + } + + fn to_mut_any_dataset_vec(&mut self) -> Vec<&mut dyn AnyDataset> { + self.cohorts + .as_mut_vec() + .into_iter() + .map(|(d, _)| d as &mut dyn AnyDataset) + .chain(vec![&mut self.metadata as &mut dyn AnyDataset]) + .collect_vec() + } +} diff --git a/parser/src/datasets/block_metadata.rs b/parser/src/datasets/block_metadata.rs new file mode 100644 index 000000000..b1483f38c --- /dev/null +++ b/parser/src/datasets/block_metadata.rs @@ -0,0 +1,61 @@ +use allocative::Allocative; + +use crate::{ + datasets::AnyDataset, + structs::{AnyHeightMap, HeightMap, WNaiveDate}, +}; + +use super::{InsertData, MinInitialStates}; + +#[derive(Allocative)] +pub struct BlockMetadataDataset { + min_initial_states: MinInitialStates, + + // Inserted + pub date: HeightMap<WNaiveDate>, + pub timestamp: HeightMap<u32>, +} + +impl BlockMetadataDataset { + pub fn import(parent_path: &str) -> color_eyre::Result<Self> { + let f = |s: &str| format!("{parent_path}/{s}"); + + let mut s = Self { + min_initial_states: MinInitialStates::default(), + + date: HeightMap::new_bin(1, &f("date")), + timestamp: HeightMap::new_bin(1, &f("timestamp")), + }; + + s.min_initial_states + .consume(MinInitialStates::compute_from_dataset(&s)); + + Ok(s) + } + + pub fn insert( + &mut self, + &InsertData { + height, timestamp, .. + }: &InsertData, + ) { + self.timestamp.insert(height, timestamp); + + self.date + .insert(height, WNaiveDate::from_timestamp(timestamp)); + } +} + +impl AnyDataset for BlockMetadataDataset { + fn get_min_initial_states(&self) -> &MinInitialStates { + &self.min_initial_states + } + + fn to_inserted_height_map_vec(&self) -> Vec<&(dyn AnyHeightMap + Send + Sync)> { + vec![&self.date, &self.timestamp] + } + + fn to_inserted_mut_height_map_vec(&mut self) -> Vec<&mut dyn AnyHeightMap> { + vec![&mut self.date, &mut self.timestamp] + } +} diff --git a/parser/src/datasets/coindays.rs b/parser/src/datasets/coindays.rs new file mode 100644 index 000000000..ec163a79c --- /dev/null +++ b/parser/src/datasets/coindays.rs @@ -0,0 +1,68 @@ +use allocative::Allocative; + +use crate::{ + datasets::AnyDataset, + structs::{AnyBiMap, BiMap}, +}; + +use super::{InsertData, MinInitialStates}; + +#[derive(Allocative)] +pub struct CoindaysDataset { + min_initial_states: MinInitialStates, + + // Inserted + pub coindays_destroyed: BiMap<f32>, +} + +impl CoindaysDataset { + pub fn import(parent_path: &str) -> color_eyre::Result<Self> { + let f = |s: &str| format!("{parent_path}/{s}"); + + let mut s = Self { + min_initial_states: MinInitialStates::default(), + + coindays_destroyed: BiMap::new_bin(1, &f("coindays_destroyed")), + }; + + s.min_initial_states + .consume(MinInitialStates::compute_from_dataset(&s)); + + Ok(s) + } + + pub fn insert( + &mut self, + &InsertData { + height, + satdays_destroyed, + date_blocks_range, + is_date_last_block, + date, + .. + }: &InsertData, + ) { + self.coindays_destroyed + .height + .insert(height, satdays_destroyed.to_btc() as f32); + + if is_date_last_block { + self.coindays_destroyed + .date_insert_sum_range(date, date_blocks_range) + } + } +} + +impl AnyDataset for CoindaysDataset { + fn to_inserted_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![&self.coindays_destroyed] + } + + fn to_inserted_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![&mut self.coindays_destroyed] + } + + fn get_min_initial_states(&self) -> &MinInitialStates { + &self.min_initial_states + } +} diff --git a/parser/src/datasets/cointime.rs b/parser/src/datasets/cointime.rs new file mode 100644 index 000000000..2f98d8e16 --- /dev/null +++ b/parser/src/datasets/cointime.rs @@ -0,0 +1,594 @@ +use allocative::Allocative; + +use crate::{ + structs::{AnyBiMap, BiMap, DateMap}, + utils::{ONE_DAY_IN_DAYS, ONE_YEAR_IN_DAYS, THREE_MONTHS_IN_DAYS, TWO_WEEK_IN_DAYS}, +}; + +use super::{AnyDataset, ComputeData, InsertData, MinInitialStates}; + +#[derive(Allocative)] +pub struct CointimeDataset { + min_initial_states: MinInitialStates, + + // Inserted + pub coinblocks_destroyed: BiMap<f32>, + + // Computed + pub active_cap: BiMap<f32>, + pub active_price: BiMap<f32>, + pub active_supply: BiMap<f32>, + pub active_supply_3m_net_change: BiMap<f32>, + pub active_supply_net_change: BiMap<f32>, + pub activity_to_vaultedness_ratio: BiMap<f32>, + pub coinblocks_created: BiMap<f32>, + pub coinblocks_stored: BiMap<f32>, + pub cointime_adjusted_velocity: BiMap<f32>, + pub cointime_adjusted_yearly_inflation_rate: BiMap<f32>, + pub cointime_cap: BiMap<f32>, + pub cointime_price: BiMap<f32>, + pub cointime_value_created: BiMap<f32>, + pub cointime_value_destroyed: BiMap<f32>, + pub cointime_value_stored: BiMap<f32>, + pub concurrent_liveliness: BiMap<f32>, + pub concurrent_liveliness_2w_median: BiMap<f32>, + pub cumulative_coinblocks_created: BiMap<f32>, + pub cumulative_coinblocks_destroyed: BiMap<f32>, + pub cumulative_coinblocks_stored: BiMap<f32>, + pub investor_cap: BiMap<f32>, + pub investorness: BiMap<f32>, + pub liveliness: BiMap<f32>, + pub liveliness_net_change: BiMap<f32>, + pub liveliness_net_change_2w_median: BiMap<f32>, + pub producerness: BiMap<f32>, + pub thermo_cap: BiMap<f32>, + pub thermo_cap_to_investor_cap_ratio: BiMap<f32>, + pub total_cointime_value_created: BiMap<f32>, + pub total_cointime_value_destroyed: BiMap<f32>, + pub total_cointime_value_stored: BiMap<f32>, + pub true_market_deviation: BiMap<f32>, + pub true_market_mean: BiMap<f32>, + pub true_market_net_unrealized_profit_and_loss: BiMap<f32>, + pub vaulted_cap: BiMap<f32>, + pub vaulted_price: BiMap<f32>, + pub vaulted_supply: BiMap<f32>, + pub vaulted_supply_net_change: BiMap<f32>, + pub vaulted_supply_3m_net_change: BiMap<f32>, + pub vaultedness: BiMap<f32>, + pub vaulting_rate: BiMap<f32>, +} + +impl CointimeDataset { + pub fn import(parent_path: &str) -> color_eyre::Result<Self> { + let f = |s: &str| format!("{parent_path}/{s}"); + + let mut s = Self { + min_initial_states: MinInitialStates::default(), + + active_cap: BiMap::new_bin(1, &f("active_cap")), + active_price: BiMap::new_bin(1, &f("active_price")), + active_supply: BiMap::new_bin(1, &f("active_supply")), + active_supply_3m_net_change: BiMap::new_bin(1, &f("active_supply_3m_net_change")), + active_supply_net_change: BiMap::new_bin(1, &f("active_supply_net_change")), + activity_to_vaultedness_ratio: BiMap::new_bin(1, &f("activity_to_vaultedness_ratio")), + coinblocks_created: BiMap::new_bin(1, &f("coinblocks_created")), + coinblocks_destroyed: BiMap::new_bin(1, &f("coinblocks_destroyed")), + coinblocks_stored: BiMap::new_bin(1, &f("coinblocks_stored")), + cointime_adjusted_velocity: BiMap::new_bin(1, &f("cointime_adjusted_velocity")), + cointime_adjusted_yearly_inflation_rate: BiMap::new_bin( + 1, + &f("cointime_adjusted_yearly_inflation_rate"), + ), + cointime_cap: BiMap::new_bin(1, &f("cointime_cap")), + cointime_price: BiMap::new_bin(1, &f("cointime_price")), + cointime_value_created: BiMap::new_bin(1, &f("cointime_value_created")), + cointime_value_destroyed: BiMap::new_bin(1, &f("cointime_value_destroyed")), + cointime_value_stored: BiMap::new_bin(1, &f("cointime_value_stored")), + concurrent_liveliness: BiMap::new_bin(1, &f("concurrent_liveliness")), + concurrent_liveliness_2w_median: BiMap::new_bin( + 1, + &f("concurrent_liveliness_2w_median"), + ), + cumulative_coinblocks_created: BiMap::new_bin(1, &f("cumulative_coinblocks_created")), + cumulative_coinblocks_destroyed: BiMap::new_bin( + 1, + &f("cumulative_coinblocks_destroyed"), + ), + cumulative_coinblocks_stored: BiMap::new_bin(1, &f("cumulative_coinblocks_stored")), + investor_cap: BiMap::new_bin(1, &f("investor_cap")), + investorness: BiMap::new_bin(1, &f("investorness")), + liveliness: BiMap::new_bin(1, &f("liveliness")), + liveliness_net_change: BiMap::new_bin(1, &f("liveliness_net_change")), + liveliness_net_change_2w_median: BiMap::new_bin( + 1, + &f("liveliness_net_change_2w_median"), + ), + producerness: BiMap::new_bin(1, &f("producerness")), + thermo_cap: BiMap::new_bin(1, &f("thermo_cap")), + thermo_cap_to_investor_cap_ratio: BiMap::new_bin( + 1, + &f("thermo_cap_to_investor_cap_ratio"), + ), + total_cointime_value_created: BiMap::new_bin(1, &f("total_cointime_value_created")), + total_cointime_value_destroyed: BiMap::new_bin(1, &f("total_cointime_value_destroyed")), + total_cointime_value_stored: BiMap::new_bin(1, &f("total_cointime_value_stored")), + true_market_deviation: BiMap::new_bin(1, &f("true_market_deviation")), + true_market_mean: BiMap::new_bin(1, &f("true_market_mean")), + true_market_net_unrealized_profit_and_loss: BiMap::new_bin( + 1, + &f("true_market_net_unrealized_profit_and_loss"), + ), + vaulted_cap: BiMap::new_bin(1, &f("vaulted_cap")), + vaulted_price: BiMap::new_bin(1, &f("vaulted_price")), + vaulted_supply: BiMap::new_bin(1, &f("vaulted_supply")), + vaulted_supply_3m_net_change: BiMap::new_bin(1, &f("vaulted_supply_3m_net_change")), + vaulted_supply_net_change: BiMap::new_bin(1, &f("vaulted_supply_net_change")), + vaultedness: BiMap::new_bin(1, &f("vaultedness")), + vaulting_rate: BiMap::new_bin(1, &f("vaulting_rate")), + }; + + s.min_initial_states + .consume(MinInitialStates::compute_from_dataset(&s)); + + Ok(s) + } + + pub fn insert( + &mut self, + &InsertData { + height, + date, + satblocks_destroyed, + date_blocks_range, + is_date_last_block, + .. + }: &InsertData, + ) { + self.coinblocks_destroyed + .height + .insert(height, satblocks_destroyed.to_btc() as f32); + + if is_date_last_block { + self.coinblocks_destroyed + .date_insert_sum_range(date, date_blocks_range); + } + } + + #[allow(clippy::too_many_arguments)] + pub fn compute( + &mut self, + &ComputeData { heights, dates }: &ComputeData, + first_height: &mut DateMap<usize>, + last_height: &mut DateMap<usize>, + closes: &mut BiMap<f32>, + circulating_supply: &mut BiMap<f64>, + realized_cap: &mut BiMap<f32>, + realized_price: &mut BiMap<f32>, + yearly_inflation_rate: &mut BiMap<f64>, + annualized_transaction_volume: &mut BiMap<f32>, + cumulative_subsidy_in_dollars: &mut BiMap<f32>, + ) { + self.cumulative_coinblocks_destroyed + .multi_insert_cumulative(heights, dates, &mut self.coinblocks_destroyed); + + self.coinblocks_created + .height + .multi_insert_simple_transform( + heights, + &mut circulating_supply.height, + |circulating_supply| circulating_supply as f32, + ); + self.coinblocks_created + .multi_date_insert_sum_range(dates, first_height, last_height); + + self.cumulative_coinblocks_created.multi_insert_cumulative( + heights, + dates, + &mut self.coinblocks_created, + ); + + self.coinblocks_stored.height.multi_insert_subtract( + heights, + &mut self.coinblocks_created.height, + &mut self.coinblocks_destroyed.height, + ); + self.coinblocks_stored + .multi_date_insert_sum_range(dates, first_height, last_height); + + self.cumulative_coinblocks_stored.multi_insert_cumulative( + heights, + dates, + &mut self.coinblocks_stored, + ); + + self.liveliness.multi_insert_divide( + heights, + dates, + &mut self.cumulative_coinblocks_destroyed, + &mut self.cumulative_coinblocks_created, + ); + + self.vaultedness.multi_insert_simple_transform( + heights, + dates, + &mut self.liveliness, + &|liveliness| 1.0 - liveliness, + ); + + self.activity_to_vaultedness_ratio.multi_insert_divide( + heights, + dates, + &mut self.liveliness, + &mut self.vaultedness, + ); + + self.concurrent_liveliness.multi_insert_divide( + heights, + dates, + &mut self.coinblocks_destroyed, + &mut self.coinblocks_created, + ); + + self.concurrent_liveliness_2w_median.multi_insert_median( + heights, + dates, + &mut self.concurrent_liveliness, + Some(TWO_WEEK_IN_DAYS), + ); + + self.liveliness_net_change.multi_insert_net_change( + heights, + dates, + &mut self.liveliness, + ONE_DAY_IN_DAYS, + ); + + self.liveliness_net_change_2w_median + .multi_insert_net_change(heights, dates, &mut self.liveliness, TWO_WEEK_IN_DAYS); + + self.vaulted_supply.multi_insert_multiply( + heights, + dates, + &mut self.vaultedness, + circulating_supply, + ); + + self.vaulted_supply_net_change.multi_insert_net_change( + heights, + dates, + &mut self.vaulted_supply, + ONE_DAY_IN_DAYS, + ); + + self.vaulted_supply_3m_net_change.multi_insert_net_change( + heights, + dates, + &mut self.vaulted_supply, + THREE_MONTHS_IN_DAYS, + ); + + self.vaulting_rate.multi_insert_simple_transform( + heights, + dates, + &mut self.vaulted_supply, + &|vaulted_supply| vaulted_supply * ONE_YEAR_IN_DAYS as f32, + ); + + self.active_supply.multi_insert_multiply( + heights, + dates, + &mut self.liveliness, + circulating_supply, + ); + + self.active_supply_net_change.multi_insert_net_change( + heights, + dates, + &mut self.active_supply, + ONE_DAY_IN_DAYS, + ); + + self.active_supply_3m_net_change.multi_insert_net_change( + heights, + dates, + &mut self.active_supply, + THREE_MONTHS_IN_DAYS, + ); + + // TODO: Do these + // let min_vaulted_supply = ; + // let max_active_supply = ; + + self.cointime_adjusted_yearly_inflation_rate + .multi_insert_multiply( + heights, + dates, + &mut self.activity_to_vaultedness_ratio, + yearly_inflation_rate, + ); + + self.cointime_adjusted_velocity.multi_insert_divide( + heights, + dates, + annualized_transaction_volume, + &mut self.active_supply, + ); + + // TODO: + // const activeSupplyChangeFromTransactions90dChange = + // createNetChangeLazyDataset(activeSupplyChangeFromTransactions, 90); + // const activeSupplyChangeFromIssuance = createMultipliedLazyDataset( + // lastSubsidy, + // liveliness, + // ); + + self.thermo_cap.multi_insert_simple_transform( + heights, + dates, + cumulative_subsidy_in_dollars, + &|cumulative_subsidy_in_dollars| cumulative_subsidy_in_dollars, + ); + + self.investor_cap + .multi_insert_subtract(heights, dates, realized_cap, &mut self.thermo_cap); + + self.thermo_cap_to_investor_cap_ratio.multi_insert_divide( + heights, + dates, + &mut self.thermo_cap, + &mut self.investor_cap, + ); + + // TODO: + // const activeSupplyChangeFromIssuance90dChange = createNetChangeLazyDataset( + // activeSupplyChangeFromIssuance, + // 90, + // ); + + self.active_price + .multi_insert_divide(heights, dates, realized_price, &mut self.liveliness); + + self.active_cap.height.multi_insert_multiply( + heights, + &mut self.active_supply.height, + &mut closes.height, + ); + self.active_cap.date.multi_insert_multiply( + dates, + &mut self.active_supply.date, + &mut closes.date, + ); + + self.vaulted_price.multi_insert_divide( + heights, + dates, + realized_price, + &mut self.vaultedness, + ); + + self.vaulted_cap.height.multi_insert_multiply( + heights, + &mut self.vaulted_supply.height, + &mut closes.height, + ); + + self.vaulted_cap.date.multi_insert_multiply( + dates, + &mut self.vaulted_supply.date, + &mut closes.date, + ); + + self.true_market_mean.multi_insert_divide( + heights, + dates, + &mut self.investor_cap, + &mut self.active_supply, + ); + + self.true_market_deviation.multi_insert_divide( + heights, + dates, + &mut self.active_cap, + &mut self.investor_cap, + ); + + self.true_market_net_unrealized_profit_and_loss + .height + .multi_insert_complex_transform( + heights, + &mut self.active_cap.height, + |(active_cap, height)| { + let investor_cap = self.investor_cap.height.get(height).unwrap(); + + (active_cap - investor_cap) / active_cap + }, + ); + self.true_market_net_unrealized_profit_and_loss + .date + .multi_insert_complex_transform( + dates, + &mut self.active_cap.date, + |(active_cap, date, _)| { + let investor_cap = self.investor_cap.date.get(date).unwrap(); + (active_cap - investor_cap) / active_cap + }, + ); + + self.investorness + .multi_insert_divide(heights, dates, &mut self.investor_cap, realized_cap); + + self.producerness + .multi_insert_divide(heights, dates, &mut self.thermo_cap, realized_cap); + + self.cointime_value_destroyed.height.multi_insert_multiply( + heights, + &mut self.coinblocks_destroyed.height, + &mut closes.height, + ); + self.cointime_value_destroyed.date.multi_insert_multiply( + dates, + &mut self.coinblocks_destroyed.date, + &mut closes.date, + ); + + self.cointime_value_created.height.multi_insert_multiply( + heights, + &mut self.coinblocks_created.height, + &mut closes.height, + ); + self.cointime_value_created.date.multi_insert_multiply( + dates, + &mut self.coinblocks_created.date, + &mut closes.date, + ); + + self.cointime_value_stored.height.multi_insert_multiply( + heights, + &mut self.coinblocks_stored.height, + &mut closes.height, + ); + self.cointime_value_stored.date.multi_insert_multiply( + dates, + &mut self.coinblocks_stored.date, + &mut closes.date, + ); + + self.total_cointime_value_created.multi_insert_cumulative( + heights, + dates, + &mut self.cointime_value_created, + ); + + self.total_cointime_value_destroyed.multi_insert_cumulative( + heights, + dates, + &mut self.cointime_value_destroyed, + ); + + self.total_cointime_value_stored.multi_insert_cumulative( + heights, + dates, + &mut self.cointime_value_stored, + ); + + self.cointime_price.multi_insert_divide( + heights, + dates, + &mut self.total_cointime_value_destroyed, + &mut self.cumulative_coinblocks_stored, + ); + + self.cointime_cap.multi_insert_multiply( + heights, + dates, + &mut self.cointime_price, + circulating_supply, + ); + } +} + +impl AnyDataset for CointimeDataset { + fn to_inserted_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![&self.coinblocks_destroyed] + } + + fn to_inserted_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![&mut self.coinblocks_destroyed] + } + + fn to_computed_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![ + &self.active_cap, + &self.active_price, + &self.active_supply, + &self.active_supply_3m_net_change, + &self.active_supply_net_change, + &self.activity_to_vaultedness_ratio, + &self.coinblocks_created, + &self.coinblocks_stored, + &self.cointime_adjusted_velocity, + &self.cointime_adjusted_yearly_inflation_rate, + &self.cointime_cap, + &self.cointime_price, + &self.cointime_value_created, + &self.cointime_value_destroyed, + &self.cointime_value_stored, + &self.concurrent_liveliness, + &self.concurrent_liveliness_2w_median, + &self.cumulative_coinblocks_created, + &self.cumulative_coinblocks_destroyed, + &self.cumulative_coinblocks_stored, + &self.investor_cap, + &self.investorness, + &self.liveliness, + &self.liveliness_net_change, + &self.liveliness_net_change_2w_median, + &self.producerness, + &self.thermo_cap, + &self.thermo_cap_to_investor_cap_ratio, + &self.total_cointime_value_created, + &self.total_cointime_value_destroyed, + &self.total_cointime_value_stored, + &self.true_market_deviation, + &self.true_market_mean, + &self.true_market_net_unrealized_profit_and_loss, + &self.vaulted_cap, + &self.vaulted_price, + &self.vaulted_supply, + &self.vaulted_supply_net_change, + &self.vaulted_supply_3m_net_change, + &self.vaultedness, + &self.vaulting_rate, + ] + } + + fn to_computed_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![ + &mut self.active_cap, + &mut self.active_price, + &mut self.active_supply, + &mut self.active_supply_3m_net_change, + &mut self.active_supply_net_change, + &mut self.activity_to_vaultedness_ratio, + &mut self.coinblocks_created, + &mut self.coinblocks_stored, + &mut self.cointime_adjusted_velocity, + &mut self.cointime_adjusted_yearly_inflation_rate, + &mut self.cointime_cap, + &mut self.cointime_price, + &mut self.cointime_value_created, + &mut self.cointime_value_destroyed, + &mut self.cointime_value_stored, + &mut self.concurrent_liveliness, + &mut self.concurrent_liveliness_2w_median, + &mut self.cumulative_coinblocks_created, + &mut self.cumulative_coinblocks_destroyed, + &mut self.cumulative_coinblocks_stored, + &mut self.investor_cap, + &mut self.investorness, + &mut self.liveliness, + &mut self.liveliness_net_change, + &mut self.liveliness_net_change_2w_median, + &mut self.producerness, + &mut self.thermo_cap, + &mut self.thermo_cap_to_investor_cap_ratio, + &mut self.total_cointime_value_created, + &mut self.total_cointime_value_destroyed, + &mut self.total_cointime_value_stored, + &mut self.true_market_deviation, + &mut self.true_market_mean, + &mut self.true_market_net_unrealized_profit_and_loss, + &mut self.vaulted_cap, + &mut self.vaulted_price, + &mut self.vaulted_supply, + &mut self.vaulted_supply_net_change, + &mut self.vaulted_supply_3m_net_change, + &mut self.vaultedness, + &mut self.vaulting_rate, + ] + } + + fn get_min_initial_states(&self) -> &MinInitialStates { + &self.min_initial_states + } +} diff --git a/parser/src/datasets/constant.rs b/parser/src/datasets/constant.rs new file mode 100644 index 000000000..3f1dae68c --- /dev/null +++ b/parser/src/datasets/constant.rs @@ -0,0 +1,52 @@ +use allocative::Allocative; + +use crate::structs::{AnyBiMap, BiMap}; + +use super::{AnyDataset, ComputeData, MinInitialStates}; + +#[derive(Allocative)] +pub struct ConstantDataset { + min_initial_states: MinInitialStates, + + // Computed + pub _50: BiMap<u16>, + pub _100: BiMap<u16>, +} + +impl ConstantDataset { + pub fn import(parent_path: &str) -> color_eyre::Result<Self> { + let f = |s: &str| format!("{parent_path}/{s}"); + + let mut s = Self { + min_initial_states: MinInitialStates::default(), + + _50: BiMap::new_bin(1, &f("50")), + _100: BiMap::new_bin(1, &f("100")), + }; + + s.min_initial_states + .consume(MinInitialStates::compute_from_dataset(&s)); + + Ok(s) + } + + pub fn compute(&mut self, &ComputeData { heights, dates }: &ComputeData) { + self._50.multi_insert_const(heights, dates, 50); + + self._100.multi_insert_const(heights, dates, 100); + } +} + +impl AnyDataset for ConstantDataset { + fn get_min_initial_states(&self) -> &MinInitialStates { + &self.min_initial_states + } + + fn to_computed_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![&self._50, &self._100] + } + + fn to_computed_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![&mut self._50, &mut self._100] + } +} diff --git a/parser/src/datasets/date_metadata.rs b/parser/src/datasets/date_metadata.rs new file mode 100644 index 000000000..87ef0804f --- /dev/null +++ b/parser/src/datasets/date_metadata.rs @@ -0,0 +1,63 @@ +use allocative::Allocative; + +use crate::{ + datasets::AnyDataset, + structs::{AnyDateMap, DateMap}, +}; + +use super::{InsertData, MinInitialStates}; + +#[derive(Allocative)] +pub struct DateMetadataDataset { + min_initial_states: MinInitialStates, + + // Inserted + pub first_height: DateMap<usize>, + pub last_height: DateMap<usize>, +} + +impl DateMetadataDataset { + pub fn import(parent_path: &str) -> color_eyre::Result<Self> { + let f = |s: &str| format!("{parent_path}/{s}"); + + let mut s = Self { + min_initial_states: MinInitialStates::default(), + + first_height: DateMap::new_bin(1, &f("first_height")), + last_height: DateMap::new_bin(1, &f("last_height")), + }; + + s.min_initial_states + .consume(MinInitialStates::compute_from_dataset(&s)); + + Ok(s) + } + + pub fn insert( + &mut self, + &InsertData { + date, + date_first_height, + height, + .. + }: &InsertData, + ) { + self.first_height.insert(date, date_first_height); + + self.last_height.insert(date, height); + } +} + +impl AnyDataset for DateMetadataDataset { + fn to_inserted_date_map_vec(&self) -> Vec<&(dyn AnyDateMap + Send + Sync)> { + vec![&self.first_height, &self.last_height] + } + + fn to_inserted_mut_date_map_vec(&mut self) -> Vec<&mut dyn AnyDateMap> { + vec![&mut self.first_height, &mut self.last_height] + } + + fn get_min_initial_states(&self) -> &MinInitialStates { + &self.min_initial_states + } +} diff --git a/parser/src/datasets/mining.rs b/parser/src/datasets/mining.rs new file mode 100644 index 000000000..c6d8c78b1 --- /dev/null +++ b/parser/src/datasets/mining.rs @@ -0,0 +1,643 @@ +use allocative::Allocative; + +use crate::{ + bitcoin::TARGET_BLOCKS_PER_DAY, + datasets::AnyDataset, + structs::{AnyBiMap, AnyDateMap, AnyHeightMap, BiMap, DateMap, HeightMap, WAmount}, + utils::{BYTES_IN_MB, ONE_DAY_IN_DAYS, ONE_MONTH_IN_DAYS, ONE_WEEK_IN_DAYS, ONE_YEAR_IN_DAYS}, +}; + +use super::{ComputeData, InsertData, MinInitialStates}; + +#[derive(Allocative)] +pub struct MiningDataset { + min_initial_states: MinInitialStates, + + // Inserted + pub blocks_mined: DateMap<usize>, + pub total_blocks_mined: DateMap<usize>, + pub coinbase: BiMap<f64>, + pub coinbase_in_dollars: BiMap<f32>, + pub fees: BiMap<f64>, + pub fees_in_dollars: BiMap<f32>, + // Raw + // pub average_fee_paid: BiMap<f32>, + // pub max_fee_paid: BiMap<f32>, + // pub _90th_percentile_fee_paid: BiMap<f32>, + // pub _75th_percentile_fee_paid: BiMap<f32>, + // pub median_fee_paid: BiMap<f32>, + // pub _25th_percentile_fee_paid: BiMap<f32>, + // pub _10th_percentile_fee_paid: BiMap<f32>, + // pub min_fee_paid: BiMap<f32>, + // sat/vB + // pub average_fee_price: BiMap<f32>, + // pub max_fee_price: BiMap<f32>, + // pub _90th_percentile_fee_price: BiMap<f32>, + // pub _75th_percentile_fee_price: BiMap<f32>, + // pub median_fee_price: BiMap<f32>, + // pub _25th_percentile_fee_price: BiMap<f32>, + // pub _10th_percentile_fee_price: BiMap<f32>, + // pub min_fee_price: BiMap<f32>, + // - + pub subsidy: BiMap<f64>, + pub subsidy_in_dollars: BiMap<f32>, + pub last_coinbase: DateMap<f64>, + pub last_coinbase_in_dollars: DateMap<f32>, + pub last_fees: DateMap<f64>, + pub last_fees_in_dollars: DateMap<f32>, + pub last_subsidy: DateMap<f64>, + pub last_subsidy_in_dollars: DateMap<f32>, + pub difficulty: BiMap<f64>, + pub block_size: HeightMap<f32>, // in MB + pub block_weight: HeightMap<f32>, // in MB + pub block_vbytes: HeightMap<u64>, + pub block_interval: HeightMap<u32>, // in ms + + // Computed + pub annualized_issuance: BiMap<f64>, // Same as subsidy_1y_sum + pub blocks_mined_1d_target: DateMap<usize>, + pub blocks_mined_1m_sma: DateMap<f32>, + pub blocks_mined_1m_sum: DateMap<usize>, + pub blocks_mined_1m_target: DateMap<usize>, + pub blocks_mined_1w_sma: DateMap<f32>, + pub blocks_mined_1w_sum: DateMap<usize>, + pub blocks_mined_1w_target: DateMap<usize>, + pub blocks_mined_1y_sum: DateMap<usize>, + pub blocks_mined_1y_target: DateMap<usize>, + pub cumulative_block_size: BiMap<f32>, + pub subsidy_1y_sum: DateMap<f64>, + pub subsidy_in_dollars_1y_sum: DateMap<f64>, + pub cumulative_subsidy: BiMap<f64>, + pub cumulative_subsidy_in_dollars: BiMap<f32>, + pub coinbase_1y_sum: DateMap<f64>, + pub coinbase_in_dollars_1y_sum: DateMap<f64>, + pub coinbase_in_dollars_1y_sma: DateMap<f32>, + pub cumulative_coinbase: BiMap<f64>, + pub cumulative_coinbase_in_dollars: BiMap<f32>, + pub fees_1y_sum: DateMap<f64>, + pub fees_in_dollars_1y_sum: DateMap<f64>, + pub cumulative_fees: BiMap<f64>, + pub cumulative_fees_in_dollars: BiMap<f32>, + pub yearly_inflation_rate: BiMap<f64>, + pub subsidy_to_coinbase_ratio: BiMap<f64>, + pub fees_to_coinbase_ratio: BiMap<f64>, + pub hash_rate: DateMap<f64>, + pub hash_rate_1w_sma: DateMap<f32>, + pub hash_rate_1m_sma: DateMap<f32>, + pub hash_rate_2m_sma: DateMap<f32>, + pub hash_price: DateMap<f64>, + pub difficulty_adjustment: DateMap<f64>, + pub puell_multiple: DateMap<f32>, + // pub average_block_size: DateMap<f32>, // in MB + // pub average_block_weight: DateMap<f32>, // in MB + // pub average_block_vbytes: DateMap<u64>, + // pub average_block_interval: DateMap<u32>, // in ms + // pub blocks_size: DateMap<f32>, + // pub average_block_size: DateMap<f32>, + // pub median_block_size: DateMap<f32>, + // pub average_block_weight: DateMap<f32>, + // pub median_block_weight: DateMap<f32>, + // pub average_block_interval: DateMap<u32>, + // pub median_block_interval: DateMap<u32>, + // pub hash_price_in_dollars: DateMap<f64>, + // pub hash_price_30d_volatility: BiMap<f32>, + // difficulty_adjustment + // next_difficulty_adjustment + // op return fees + // inscriptions fees + // until adjustement + // until halving in days + // until halving in blocks +} + +impl MiningDataset { + pub fn import(parent_path: &str) -> color_eyre::Result<Self> { + let f = |s: &str| format!("{parent_path}/{s}"); + + let mut s = Self { + min_initial_states: MinInitialStates::default(), + + total_blocks_mined: DateMap::new_bin(1, &f("total_blocks_mined")), + blocks_mined: DateMap::new_bin(1, &f("blocks_mined")), + coinbase: BiMap::new_bin(1, &f("coinbase")), + coinbase_in_dollars: BiMap::new_bin(1, &f("coinbase_in_dollars")), + coinbase_1y_sum: DateMap::new_bin(1, &f("coinbase_1y_sum")), + coinbase_in_dollars_1y_sum: DateMap::new_bin(1, &f("coinbase_in_dollars_1y_sum")), + coinbase_in_dollars_1y_sma: DateMap::new_bin(1, &f("coinbase_in_dollars_1y_sma")), + cumulative_coinbase: BiMap::new_bin(1, &f("cumulative_coinbase")), + cumulative_coinbase_in_dollars: BiMap::new_bin(1, &f("cumulative_coinbase_in_dollars")), + fees: BiMap::new_bin(1, &f("fees")), + fees_in_dollars: BiMap::new_bin(1, &f("fees_in_dollars")), + fees_1y_sum: DateMap::new_bin(1, &f("fees_1y_sum")), + fees_in_dollars_1y_sum: DateMap::new_bin(1, &f("fees_in_dollars_1y_sum")), + cumulative_fees: BiMap::new_bin(1, &f("cumulative_fees")), + cumulative_fees_in_dollars: BiMap::new_bin(1, &f("cumulative_fees_in_dollars")), + subsidy: BiMap::new_bin(1, &f("subsidy")), + subsidy_in_dollars: BiMap::new_bin(1, &f("subsidy_in_dollars")), + subsidy_1y_sum: DateMap::new_bin(1, &f("subsidy_1y_sum")), + subsidy_in_dollars_1y_sum: DateMap::new_bin(1, &f("subsidy_in_dollars_1y_sum")), + cumulative_subsidy: BiMap::new_bin(1, &f("cumulative_subsidy")), + cumulative_subsidy_in_dollars: BiMap::new_bin(1, &f("cumulative_subsidy_in_dollars")), + + subsidy_to_coinbase_ratio: BiMap::new_bin(1, &f("subsidy_to_coinbase_ratio")), + fees_to_coinbase_ratio: BiMap::new_bin(1, &f("fees_to_coinbase_ratio")), + + annualized_issuance: BiMap::new_bin(1, &f("annualized_issuance")), + yearly_inflation_rate: BiMap::new_bin(1, &f("yearly_inflation_rate")), + + last_subsidy: DateMap::new_bin(1, &f("last_subsidy")), + last_subsidy_in_dollars: DateMap::new_bin(1, &f("last_subsidy_in_dollars")), + last_coinbase: DateMap::new_bin(1, &f("last_coinbase")), + last_coinbase_in_dollars: DateMap::new_bin(1, &f("last_coinbase_in_dollars")), + last_fees: DateMap::new_bin(1, &f("last_fees")), + last_fees_in_dollars: DateMap::new_bin(1, &f("last_fees_in_dollars")), + + blocks_mined_1d_target: DateMap::new_bin(1, &f("blocks_mined_1d_target")), + blocks_mined_1w_sma: DateMap::new_bin(1, &f("blocks_mined_1w_sma")), + blocks_mined_1m_sma: DateMap::new_bin(1, &f("blocks_mined_1m_sma")), + + blocks_mined_1w_sum: DateMap::new_bin(1, &f("blocks_mined_1w_sum")), + blocks_mined_1m_sum: DateMap::new_bin(1, &f("blocks_mined_1m_sum")), + blocks_mined_1y_sum: DateMap::new_bin(1, &f("blocks_mined_1y_sum")), + + blocks_mined_1w_target: DateMap::new_bin(1, &f("blocks_mined_1w_target")), + blocks_mined_1m_target: DateMap::new_bin(1, &f("blocks_mined_1m_target")), + blocks_mined_1y_target: DateMap::new_bin(1, &f("blocks_mined_1y_target")), + + difficulty: BiMap::new_bin(1, &f("difficulty")), + difficulty_adjustment: DateMap::new_bin(1, &f("difficulty_adjustment")), + block_size: HeightMap::new_bin(1, &f("block_size")), + cumulative_block_size: BiMap::new_bin(1, &f("cumulative_block_size")), + block_weight: HeightMap::new_bin(1, &f("block_weight")), + block_vbytes: HeightMap::new_bin(1, &f("block_vbytes")), + block_interval: HeightMap::new_bin(1, &f("block_interval")), + + hash_rate: DateMap::new_bin(1, &f("hash_rate")), + hash_rate_1w_sma: DateMap::new_bin(1, &f("hash_rate_1w_sma")), + hash_rate_1m_sma: DateMap::new_bin(1, &f("hash_rate_1m_sma")), + hash_rate_2m_sma: DateMap::new_bin(1, &f("hash_rate_2m_sma")), + hash_price: DateMap::new_bin(1, &f("hash_price")), + puell_multiple: DateMap::new_bin(1, &f("puell_multiple")), + }; + + s.min_initial_states + .consume(MinInitialStates::compute_from_dataset(&s)); + + Ok(s) + } + + pub fn insert( + &mut self, + &InsertData { + date_first_height, + height, + coinbase, + fees, + date_blocks_range, + is_date_last_block, + block_price, + date, + difficulty, + block_size, + block_vbytes, + block_weight, + block_interval, + .. + }: &InsertData, + ) { + self.coinbase.height.insert(height, coinbase.to_btc()); + + let coinbase_in_dollars = self + .coinbase_in_dollars + .height + .insert(height, (block_price * coinbase).to_dollar() as f32); + + let sumed_fees = WAmount::from_sat(fees.iter().map(|amount| amount.to_sat()).sum()); + + self.fees.height.insert(height, sumed_fees.to_btc()); + + let sumed_fees_in_dollars = self + .fees_in_dollars + .height + .insert(height, (block_price * sumed_fees).to_dollar() as f32); + + let subsidy = coinbase - sumed_fees; + self.subsidy.height.insert(height, subsidy.to_btc()); + + let subsidy_in_dollars = self + .subsidy_in_dollars + .height + .insert(height, (block_price * subsidy).to_dollar() as f32); + + self.difficulty.height.insert(height, difficulty); + + self.block_size + .insert(height, block_size as f32 / BYTES_IN_MB as f32); + self.block_weight + .insert(height, block_weight as f32 / BYTES_IN_MB as f32); + self.block_vbytes.insert(height, block_vbytes); + self.block_interval.insert(height, block_interval); + + if is_date_last_block { + self.coinbase.date_insert_sum_range(date, date_blocks_range); + + self.coinbase_in_dollars + .date_insert_sum_range(date, date_blocks_range); + + self.fees.date_insert_sum_range(date, date_blocks_range); + + self.fees_in_dollars + .date_insert_sum_range(date, date_blocks_range); + + self.subsidy.date_insert_sum_range(date, date_blocks_range); + + self.subsidy_in_dollars + .date_insert_sum_range(date, date_blocks_range); + + self.last_coinbase.insert(date, coinbase.to_btc()); + + self.last_coinbase_in_dollars + .insert(date, coinbase_in_dollars); + + self.last_subsidy.insert(date, subsidy.to_btc()); + + self.last_subsidy_in_dollars + .insert(date, subsidy_in_dollars); + + self.last_fees.insert(date, sumed_fees.to_btc()); + + self.last_fees_in_dollars + .insert(date, sumed_fees_in_dollars); + + let total_blocks_mined = self.total_blocks_mined.insert(date, height + 1); + + self.blocks_mined + .insert(date, total_blocks_mined - date_first_height); + + self.difficulty.date.insert(date, difficulty); + } + } + + pub fn compute( + &mut self, + &ComputeData { heights, dates }: &ComputeData, + last_height: &mut DateMap<usize>, + ) { + self.blocks_mined_1w_sum.multi_insert_last_x_sum( + dates, + &mut self.blocks_mined, + ONE_WEEK_IN_DAYS, + ); + + self.blocks_mined_1m_sum.multi_insert_last_x_sum( + dates, + &mut self.blocks_mined, + ONE_MONTH_IN_DAYS, + ); + + self.blocks_mined_1y_sum.multi_insert_last_x_sum( + dates, + &mut self.blocks_mined, + ONE_YEAR_IN_DAYS, + ); + + self.subsidy_1y_sum.multi_insert_last_x_sum( + dates, + &mut self.subsidy.date, + ONE_YEAR_IN_DAYS, + ); + + self.subsidy_in_dollars_1y_sum.multi_insert_last_x_sum( + dates, + &mut self.subsidy_in_dollars.date, + ONE_YEAR_IN_DAYS, + ); + + self.cumulative_subsidy + .multi_insert_cumulative(heights, dates, &mut self.subsidy); + + self.cumulative_subsidy_in_dollars.multi_insert_cumulative( + heights, + dates, + &mut self.subsidy_in_dollars, + ); + + self.fees_1y_sum + .multi_insert_last_x_sum(dates, &mut self.fees.date, ONE_YEAR_IN_DAYS); + + self.fees_in_dollars_1y_sum.multi_insert_last_x_sum( + dates, + &mut self.fees_in_dollars.date, + ONE_YEAR_IN_DAYS, + ); + + self.cumulative_fees + .multi_insert_cumulative(heights, dates, &mut self.fees); + + self.cumulative_fees_in_dollars.multi_insert_cumulative( + heights, + dates, + &mut self.fees_in_dollars, + ); + + self.coinbase_1y_sum.multi_insert_last_x_sum( + dates, + &mut self.coinbase.date, + ONE_YEAR_IN_DAYS, + ); + + self.coinbase_in_dollars_1y_sum.multi_insert_last_x_sum( + dates, + &mut self.coinbase_in_dollars.date, + ONE_YEAR_IN_DAYS, + ); + + self.coinbase_in_dollars_1y_sma.multi_insert_simple_average( + dates, + &mut self.coinbase_in_dollars.date, + ONE_YEAR_IN_DAYS, + ); + + self.cumulative_coinbase + .multi_insert_cumulative(heights, dates, &mut self.coinbase); + + self.cumulative_coinbase_in_dollars.multi_insert_cumulative( + heights, + dates, + &mut self.coinbase_in_dollars, + ); + + self.subsidy_to_coinbase_ratio.multi_insert_percentage( + heights, + dates, + &mut self.subsidy, + &mut self.coinbase, + ); + + self.fees_to_coinbase_ratio.multi_insert_percentage( + heights, + dates, + &mut self.fees, + &mut self.coinbase, + ); + + self.annualized_issuance.multi_insert_last_x_sum( + heights, + dates, + &mut self.subsidy, + ONE_YEAR_IN_DAYS, + ); + + self.yearly_inflation_rate.multi_insert_percentage( + heights, + dates, + &mut self.annualized_issuance, + &mut self.cumulative_subsidy, + ); + + self.blocks_mined_1d_target + .multi_insert_const(dates, TARGET_BLOCKS_PER_DAY); + + self.blocks_mined_1w_target + .multi_insert_const(dates, ONE_WEEK_IN_DAYS * TARGET_BLOCKS_PER_DAY); + + self.blocks_mined_1m_target + .multi_insert_const(dates, ONE_MONTH_IN_DAYS * TARGET_BLOCKS_PER_DAY); + + self.blocks_mined_1y_target + .multi_insert_const(dates, ONE_YEAR_IN_DAYS * TARGET_BLOCKS_PER_DAY); + + self.blocks_mined_1w_sma.multi_insert_simple_average( + dates, + &mut self.blocks_mined, + ONE_WEEK_IN_DAYS, + ); + + self.blocks_mined_1m_sma.multi_insert_simple_average( + dates, + &mut self.blocks_mined, + ONE_MONTH_IN_DAYS, + ); + + self.cumulative_block_size + .height + .multi_insert_cumulative(heights, &mut self.block_size); + + self.cumulative_block_size.date.multi_insert_last( + dates, + &mut self.cumulative_block_size.height, + last_height, + ); + + // https://hashrateindex.com/blog/what-is-bitcoins-hashrate/ + self.hash_rate.multi_insert(dates, |date| { + let blocks_mined = self.blocks_mined.get_or_import(date).unwrap(); + + let difficulty = self.difficulty.date.get_or_import(date).unwrap(); + + ((blocks_mined as f64 / TARGET_BLOCKS_PER_DAY as f64) * difficulty * 2.0_f64.powi(32)) + / 600.0 + / 1_000_000_000_000_000_000.0 + }); + + self.hash_rate_1w_sma.multi_insert_simple_average( + dates, + &mut self.hash_rate, + ONE_WEEK_IN_DAYS, + ); + + self.hash_rate_1m_sma.multi_insert_simple_average( + dates, + &mut self.hash_rate, + ONE_MONTH_IN_DAYS, + ); + + self.hash_rate_2m_sma.multi_insert_simple_average( + dates, + &mut self.hash_rate, + 2 * ONE_MONTH_IN_DAYS, + ); + + self.hash_price.multi_insert(dates, |date| { + let coinbase_in_dollars = self.coinbase_in_dollars.date.get_or_import(date).unwrap(); + + let hashrate = self.hash_rate.get_or_import(date).unwrap(); + + coinbase_in_dollars as f64 / hashrate / 1_000.0 + }); + + self.puell_multiple.multi_insert_divide( + dates, + &mut self.coinbase_in_dollars.date, + &mut self.coinbase_in_dollars_1y_sma, + ); + + self.difficulty_adjustment.multi_insert_percentage_change( + dates, + &mut self.difficulty.date, + ONE_DAY_IN_DAYS, + ); + } +} + +impl AnyDataset for MiningDataset { + fn get_min_initial_states(&self) -> &MinInitialStates { + &self.min_initial_states + } + + fn to_inserted_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![ + &self.coinbase, + &self.coinbase_in_dollars, + &self.fees, + &self.fees_in_dollars, + &self.subsidy, + &self.subsidy_in_dollars, + &self.difficulty, + ] + } + + fn to_inserted_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![ + &mut self.coinbase, + &mut self.coinbase_in_dollars, + &mut self.fees, + &mut self.fees_in_dollars, + &mut self.subsidy, + &mut self.subsidy_in_dollars, + &mut self.difficulty, + ] + } + + fn to_inserted_date_map_vec(&self) -> Vec<&(dyn AnyDateMap + Send + Sync)> { + vec![ + &self.total_blocks_mined, + &self.blocks_mined, + &self.last_subsidy, + &self.last_subsidy_in_dollars, + &self.last_coinbase, + &self.last_coinbase_in_dollars, + &self.last_fees, + &self.last_fees_in_dollars, + ] + } + + fn to_inserted_mut_date_map_vec(&mut self) -> Vec<&mut dyn AnyDateMap> { + vec![ + &mut self.total_blocks_mined, + &mut self.blocks_mined, + &mut self.last_subsidy, + &mut self.last_subsidy_in_dollars, + &mut self.last_coinbase, + &mut self.last_coinbase_in_dollars, + &mut self.last_fees, + &mut self.last_fees_in_dollars, + ] + } + + fn to_inserted_height_map_vec(&self) -> Vec<&(dyn AnyHeightMap + Send + Sync)> { + vec![ + &self.block_size, + &self.block_weight, + &self.block_vbytes, + &self.block_interval, + ] + } + + fn to_inserted_mut_height_map_vec(&mut self) -> Vec<&mut dyn AnyHeightMap> { + vec![ + &mut self.block_size, + &mut self.block_weight, + &mut self.block_vbytes, + &mut self.block_interval, + ] + } + + fn to_computed_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![ + &self.cumulative_coinbase, + &self.cumulative_coinbase_in_dollars, + &self.cumulative_fees, + &self.cumulative_fees_in_dollars, + &self.cumulative_subsidy, + &self.cumulative_subsidy_in_dollars, + &self.annualized_issuance, + &self.yearly_inflation_rate, + &self.cumulative_block_size, + &self.subsidy_to_coinbase_ratio, + &self.fees_to_coinbase_ratio, + ] + } + + fn to_computed_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![ + &mut self.cumulative_coinbase, + &mut self.cumulative_coinbase_in_dollars, + &mut self.cumulative_fees, + &mut self.cumulative_fees_in_dollars, + &mut self.cumulative_subsidy, + &mut self.cumulative_subsidy_in_dollars, + &mut self.annualized_issuance, + &mut self.yearly_inflation_rate, + &mut self.cumulative_block_size, + &mut self.subsidy_to_coinbase_ratio, + &mut self.fees_to_coinbase_ratio, + ] + } + + fn to_computed_date_map_vec(&self) -> Vec<&(dyn AnyDateMap + Send + Sync)> { + vec![ + &self.blocks_mined_1d_target, + &self.blocks_mined_1w_sma, + &self.blocks_mined_1m_sma, + &self.blocks_mined_1w_sum, + &self.blocks_mined_1m_sum, + &self.blocks_mined_1y_sum, + &self.blocks_mined_1w_target, + &self.blocks_mined_1m_target, + &self.blocks_mined_1y_target, + &self.subsidy_1y_sum, + &self.subsidy_in_dollars_1y_sum, + &self.coinbase_1y_sum, + &self.coinbase_in_dollars_1y_sum, + &self.coinbase_in_dollars_1y_sma, + &self.fees_1y_sum, + &self.fees_in_dollars_1y_sum, + &self.hash_rate, + &self.hash_rate_1w_sma, + &self.hash_rate_1m_sma, + &self.hash_rate_2m_sma, + &self.hash_price, + &self.puell_multiple, + &self.difficulty_adjustment, + ] + } + + fn to_computed_mut_date_map_vec(&mut self) -> Vec<&mut dyn AnyDateMap> { + vec![ + &mut self.blocks_mined_1d_target, + &mut self.blocks_mined_1w_sma, + &mut self.blocks_mined_1m_sma, + &mut self.blocks_mined_1w_sum, + &mut self.blocks_mined_1m_sum, + &mut self.blocks_mined_1y_sum, + &mut self.blocks_mined_1w_target, + &mut self.blocks_mined_1m_target, + &mut self.blocks_mined_1y_target, + &mut self.subsidy_1y_sum, + &mut self.subsidy_in_dollars_1y_sum, + &mut self.coinbase_1y_sum, + &mut self.coinbase_in_dollars_1y_sum, + &mut self.coinbase_in_dollars_1y_sma, + &mut self.fees_1y_sum, + &mut self.fees_in_dollars_1y_sum, + &mut self.hash_rate, + &mut self.hash_rate_1w_sma, + &mut self.hash_rate_1m_sma, + &mut self.hash_rate_2m_sma, + &mut self.hash_price, + &mut self.puell_multiple, + &mut self.difficulty_adjustment, + ] + } +} diff --git a/parser/src/datasets/mod.rs b/parser/src/datasets/mod.rs new file mode 100644 index 000000000..617736f44 --- /dev/null +++ b/parser/src/datasets/mod.rs @@ -0,0 +1,340 @@ +use std::{collections::BTreeMap, ops::RangeInclusive}; + +use allocative::Allocative; + +use itertools::Itertools; + +use rayon::prelude::*; + +mod _traits; +mod address; +mod block_metadata; +mod coindays; +mod cointime; +mod constant; +mod date_metadata; +mod mining; +mod price; +mod subs; +mod transaction; +mod utxo; + +pub use _traits::*; +pub use address::*; +pub use block_metadata::*; +pub use coindays::*; +pub use cointime::*; +pub use constant::*; +pub use date_metadata::*; +pub use mining::*; +pub use price::*; +pub use subs::*; +pub use transaction::*; +pub use utxo::*; + +use crate::{ + databases::Databases, + io::Json, + states::{ + AddressCohortsInputStates, + AddressCohortsOneShotStates, + AddressCohortsRealizedStates, + States, + UTXOCohortsOneShotStates, + // UTXOCohortsReceivedStates, + UTXOCohortsSentStates, + }, + structs::{Price, WAmount, WNaiveDate}, +}; + +pub struct InsertData<'a> { + pub address_cohorts_input_states: &'a Option<AddressCohortsInputStates>, + pub address_cohorts_one_shot_states: &'a Option<AddressCohortsOneShotStates>, + pub address_cohorts_realized_states: &'a Option<AddressCohortsRealizedStates>, + pub amount_sent: WAmount, + pub block_interval: u32, + pub block_price: Price, + pub block_size: usize, + pub block_vbytes: u64, + pub block_weight: u64, + pub coinbase: WAmount, + pub compute_addresses: bool, + pub databases: &'a Databases, + pub date: WNaiveDate, + pub date_blocks_range: &'a RangeInclusive<usize>, + pub date_first_height: usize, + pub difficulty: f64, + pub fees: &'a Vec<WAmount>, + pub height: usize, + pub is_date_last_block: bool, + pub satblocks_destroyed: WAmount, + pub satdays_destroyed: WAmount, + pub states: &'a States, + pub timestamp: u32, + pub transaction_count: usize, + pub utxo_cohorts_one_shot_states: &'a UTXOCohortsOneShotStates, + // pub utxo_cohorts_received_states: &'a UTXOCohortsReceivedStates, + pub utxo_cohorts_sent_states: &'a UTXOCohortsSentStates, +} + +pub struct ComputeData<'a> { + pub heights: &'a [usize], + pub dates: &'a [WNaiveDate], +} + +#[derive(Allocative)] +pub struct AllDatasets { + min_initial_states: MinInitialStates, + + pub constant: ConstantDataset, + pub address: AddressDatasets, + pub block_metadata: BlockMetadataDataset, + pub coindays: CoindaysDataset, + pub cointime: CointimeDataset, + pub date_metadata: DateMetadataDataset, + pub mining: MiningDataset, + pub price: PriceDatasets, + pub transaction: TransactionDataset, + pub utxo: UTXODatasets, +} + +impl AllDatasets { + pub fn import() -> color_eyre::Result<Self> { + let path = "../datasets"; + + let price = PriceDatasets::import(path)?; + + let constant = ConstantDataset::import(path)?; + + let date_metadata = DateMetadataDataset::import(path)?; + + let cointime = CointimeDataset::import(path)?; + + let coindays = CoindaysDataset::import(path)?; + + let mining = MiningDataset::import(path)?; + + let block_metadata = BlockMetadataDataset::import(path)?; + + let transaction = TransactionDataset::import(path)?; + + let address = AddressDatasets::import(path)?; + + let utxo = UTXODatasets::import(path)?; + + let mut s = Self { + min_initial_states: MinInitialStates::default(), + + address, + block_metadata, + cointime, + coindays, + constant, + date_metadata, + price, + mining, + transaction, + utxo, + }; + + s.min_initial_states + .consume(MinInitialStates::compute_from_datasets(&s)); + + s.export_path_to_type()?; + + Ok(s) + } + + pub fn insert(&mut self, insert_data: InsertData) { + self.address.insert(&insert_data); + + self.utxo.insert(&insert_data); + + if self + .block_metadata + .needs_insert(insert_data.height, insert_data.date) + { + self.block_metadata.insert(&insert_data); + } + + if self + .date_metadata + .needs_insert(insert_data.height, insert_data.date) + { + self.date_metadata.insert(&insert_data); + } + + if self + .coindays + .needs_insert(insert_data.height, insert_data.date) + { + self.coindays.insert(&insert_data); + } + + if self + .mining + .needs_insert(insert_data.height, insert_data.date) + { + self.mining.insert(&insert_data); + } + + if self + .transaction + .needs_insert(insert_data.height, insert_data.date) + { + self.transaction.insert(&insert_data); + } + + if self + .cointime + .needs_insert(insert_data.height, insert_data.date) + { + self.cointime.insert(&insert_data); + } + } + + pub fn compute(&mut self, compute_data: ComputeData) { + if self.constant.should_compute(&compute_data) { + self.constant.compute(&compute_data); + } + + if self.mining.should_compute(&compute_data) { + self.mining + .compute(&compute_data, &mut self.date_metadata.last_height); + } + + // No compute needed for now + self.price + .compute(&compute_data, &mut self.mining.cumulative_subsidy); + + self.address.compute( + &compute_data, + &mut self.price.closes, + &mut self.mining.cumulative_subsidy, + &mut self.price.market_cap, + ); + + self.utxo.compute( + &compute_data, + &mut self.price.closes, + &mut self.mining.cumulative_subsidy, + &mut self.price.market_cap, + ); + + // No compute needed for now + // if self.block_metadata.should_compute(height, date) { + // self.block_metadata.compute(&compute_data); + // } + + // No compute needed for now + // if self.date_metadata.should_compute(height, date) { + // self.date_metadata.compute(&compute_data); + // } + + // No compute needed for now + // if self.coindays.should_compute(height, date) { + // self.coindays.compute(&compute_data); + // } + + if self.transaction.should_compute(&compute_data) { + self.transaction.compute( + &compute_data, + &mut self.mining.cumulative_subsidy, + &mut self.mining.block_interval, + ); + } + + if self.cointime.should_compute(&compute_data) { + self.cointime.compute( + &compute_data, + &mut self.date_metadata.first_height, + &mut self.date_metadata.last_height, + &mut self.price.closes, + &mut self.mining.cumulative_subsidy, + &mut self.address.cohorts.all.all.capitalization.realized_cap, + &mut self.address.cohorts.all.all.capitalization.realized_price, + &mut self.mining.yearly_inflation_rate, + &mut self.transaction.annualized_volume, + &mut self.mining.cumulative_subsidy_in_dollars, + ); + } + } + + pub fn export_path_to_type(&self) -> color_eyre::Result<()> { + let path_to_type: BTreeMap<&str, &str> = self + .to_any_dataset_vec() + .into_iter() + .flat_map(|dataset| { + dataset + .to_all_map_vec() + .into_iter() + .flat_map(|map| map.exported_path_with_t_name()) + }) + .collect(); + + Json::export("../datasets/disk_path_to_type.json", &path_to_type) + } + + pub fn export(&mut self) -> color_eyre::Result<()> { + self.to_mut_any_dataset_vec() + .into_iter() + .for_each(|dataset| dataset.pre_export()); + + self.to_any_dataset_vec() + .into_par_iter() + .try_for_each(|dataset| -> color_eyre::Result<()> { dataset.export() })?; + + self.to_mut_any_dataset_vec() + .into_iter() + .for_each(|dataset| dataset.post_export()); + + Ok(()) + } +} + +impl AnyDatasets for AllDatasets { + fn get_min_initial_states(&self) -> &MinInitialStates { + &self.min_initial_states + } + + fn to_any_dataset_vec(&self) -> Vec<&(dyn AnyDataset + Send + Sync)> { + vec![ + vec![ + &self.price as &(dyn AnyDataset + Send + Sync), + &self.constant, + ], + self.address.to_any_dataset_vec(), + self.utxo.to_any_dataset_vec(), + vec![ + &self.mining, + &self.transaction, + &self.block_metadata, + &self.date_metadata, + &self.cointime, + &self.coindays, + ], + ] + .into_iter() + .flatten() + .collect_vec() + } + + fn to_mut_any_dataset_vec(&mut self) -> Vec<&mut dyn AnyDataset> { + vec![ + vec![&mut self.price as &mut dyn AnyDataset, &mut self.constant], + self.address.to_mut_any_dataset_vec(), + self.utxo.to_mut_any_dataset_vec(), + vec![ + &mut self.mining, + &mut self.transaction, + &mut self.block_metadata, + &mut self.date_metadata, + &mut self.cointime, + &mut self.coindays, + ], + ] + .into_iter() + .flatten() + .collect_vec() + } +} diff --git a/parser/src/datasets/price/mod.rs b/parser/src/datasets/price/mod.rs new file mode 100644 index 000000000..db3637c8d --- /dev/null +++ b/parser/src/datasets/price/mod.rs @@ -0,0 +1,493 @@ +mod ohlc; + +use std::collections::BTreeMap; + +use allocative::Allocative; +use chrono::{Days, NaiveDateTime, NaiveTime, TimeZone, Timelike, Utc}; +use color_eyre::eyre::Error; + +pub use ohlc::*; + +use crate::{ + price::{Binance, Kraken}, + structs::{AnyBiMap, AnyDateMap, BiMap, DateMap, WNaiveDate}, + utils::{ONE_MONTH_IN_DAYS, ONE_WEEK_IN_DAYS, ONE_YEAR_IN_DAYS}, +}; + +use super::{AnyDataset, ComputeData, MinInitialStates}; + +#[derive(Allocative)] +pub struct PriceDatasets { + min_initial_states: MinInitialStates, + + kraken_daily: Option<BTreeMap<WNaiveDate, OHLC>>, + kraken_1mn: Option<BTreeMap<u32, OHLC>>, + binance_1mn: Option<BTreeMap<u32, OHLC>>, + binance_har: Option<BTreeMap<u32, OHLC>>, + + // Inserted + pub ohlcs: BiMap<OHLC>, + + // Computed + pub closes: BiMap<f32>, + pub market_cap: BiMap<f32>, + pub price_1w_sma: DateMap<f32>, + pub price_1m_sma: DateMap<f32>, + pub price_1y_sma: DateMap<f32>, + pub price_2y_sma: DateMap<f32>, + pub price_4y_sma: DateMap<f32>, + pub price_8d_sma: DateMap<f32>, + pub price_13d_sma: DateMap<f32>, + pub price_21d_sma: DateMap<f32>, + pub price_34d_sma: DateMap<f32>, + pub price_55d_sma: DateMap<f32>, + pub price_89d_sma: DateMap<f32>, + pub price_144d_sma: DateMap<f32>, + pub price_200w_sma: DateMap<f32>, + pub price_1d_total_return: DateMap<f32>, + pub price_1m_total_return: DateMap<f32>, + pub price_6m_total_return: DateMap<f32>, + pub price_1y_total_return: DateMap<f32>, + pub price_2y_total_return: DateMap<f32>, + pub price_3y_total_return: DateMap<f32>, + pub price_4y_total_return: DateMap<f32>, + pub price_6y_total_return: DateMap<f32>, + pub price_8y_total_return: DateMap<f32>, + pub price_10y_total_return: DateMap<f32>, + pub price_4y_compound_return: DateMap<f32>, + // projection via lowest 4y compound value + // volatility + // drawdown + // sats per dollar +} + +impl PriceDatasets { + pub fn import(datasets_path: &str) -> color_eyre::Result<Self> { + let price_path = "../price"; + + let f = |s: &str| format!("{datasets_path}/{s}"); + + let mut s = Self { + min_initial_states: MinInitialStates::default(), + + binance_1mn: None, + binance_har: None, + kraken_1mn: None, + kraken_daily: None, + + ohlcs: BiMap::new_json(1, &format!("{price_path}/ohlc")), + closes: BiMap::new_json(1, &f("close")), + market_cap: BiMap::new_bin(1, &f("market_cap")), + price_1w_sma: DateMap::new_bin(1, &f("price_1w_sma")), + price_1m_sma: DateMap::new_bin(1, &f("price_1m_sma")), + price_1y_sma: DateMap::new_bin(1, &f("price_1y_sma")), + price_2y_sma: DateMap::new_bin(1, &f("price_2y_sma")), + price_4y_sma: DateMap::new_bin(1, &f("price_4y_sma")), + price_8d_sma: DateMap::new_bin(1, &f("price_8d_sma")), + price_13d_sma: DateMap::new_bin(1, &f("price_13d_sma")), + price_21d_sma: DateMap::new_bin(1, &f("price_21d_sma")), + price_34d_sma: DateMap::new_bin(1, &f("price_34d_sma")), + price_55d_sma: DateMap::new_bin(1, &f("price_55d_sma")), + price_89d_sma: DateMap::new_bin(1, &f("price_89d_sma")), + price_144d_sma: DateMap::new_bin(1, &f("price_144d_sma")), + price_200w_sma: DateMap::new_bin(1, &f("price_200w_sma")), + price_1d_total_return: DateMap::new_bin(1, &f("price_1d_total_return")), + price_1m_total_return: DateMap::new_bin(1, &f("price_1m_total_return")), + price_6m_total_return: DateMap::new_bin(1, &f("price_6m_total_return")), + price_1y_total_return: DateMap::new_bin(1, &f("price_1y_total_return")), + price_2y_total_return: DateMap::new_bin(1, &f("price_2y_total_return")), + price_3y_total_return: DateMap::new_bin(1, &f("price_3y_total_return")), + price_4y_total_return: DateMap::new_bin(1, &f("price_4y_total_return")), + price_6y_total_return: DateMap::new_bin(1, &f("price_6y_total_return")), + price_8y_total_return: DateMap::new_bin(1, &f("price_8y_total_return")), + price_10y_total_return: DateMap::new_bin(1, &f("price_10y_total_return")), + price_4y_compound_return: DateMap::new_bin(1, &f("price_4y_compound_return")), + }; + + s.min_initial_states + .consume(MinInitialStates::compute_from_dataset(&s)); + + Ok(s) + } + + pub fn compute( + &mut self, + &ComputeData { dates, heights }: &ComputeData, + circulating_supply: &mut BiMap<f64>, + ) { + self.closes + .multi_insert_simple_transform(heights, dates, &mut self.ohlcs, &|ohlc| ohlc.close); + + self.market_cap + .multi_insert_multiply(heights, dates, &mut self.closes, circulating_supply); + + self.price_1w_sma.multi_insert_simple_average( + dates, + &mut self.closes.date, + ONE_WEEK_IN_DAYS, + ); + + self.price_1m_sma.multi_insert_simple_average( + dates, + &mut self.closes.date, + ONE_MONTH_IN_DAYS, + ); + + self.price_1y_sma.multi_insert_simple_average( + dates, + &mut self.closes.date, + ONE_YEAR_IN_DAYS, + ); + + self.price_2y_sma.multi_insert_simple_average( + dates, + &mut self.closes.date, + 2 * ONE_YEAR_IN_DAYS, + ); + + self.price_4y_sma.multi_insert_simple_average( + dates, + &mut self.closes.date, + 4 * ONE_YEAR_IN_DAYS, + ); + + self.price_8d_sma + .multi_insert_simple_average(dates, &mut self.closes.date, 8); + + self.price_13d_sma + .multi_insert_simple_average(dates, &mut self.closes.date, 13); + + self.price_21d_sma + .multi_insert_simple_average(dates, &mut self.closes.date, 21); + + self.price_34d_sma + .multi_insert_simple_average(dates, &mut self.closes.date, 34); + + self.price_55d_sma + .multi_insert_simple_average(dates, &mut self.closes.date, 55); + + self.price_89d_sma + .multi_insert_simple_average(dates, &mut self.closes.date, 89); + + self.price_144d_sma + .multi_insert_simple_average(dates, &mut self.closes.date, 144); + + self.price_200w_sma.multi_insert_simple_average( + dates, + &mut self.closes.date, + 200 * ONE_WEEK_IN_DAYS, + ); + + self.price_1d_total_return + .multi_insert_percentage_change(dates, &mut self.closes.date, 1); + self.price_1m_total_return.multi_insert_percentage_change( + dates, + &mut self.closes.date, + ONE_MONTH_IN_DAYS, + ); + self.price_6m_total_return.multi_insert_percentage_change( + dates, + &mut self.closes.date, + 6 * ONE_MONTH_IN_DAYS, + ); + self.price_1y_total_return.multi_insert_percentage_change( + dates, + &mut self.closes.date, + ONE_YEAR_IN_DAYS, + ); + self.price_2y_total_return.multi_insert_percentage_change( + dates, + &mut self.closes.date, + 2 * ONE_YEAR_IN_DAYS, + ); + self.price_3y_total_return.multi_insert_percentage_change( + dates, + &mut self.closes.date, + 3 * ONE_YEAR_IN_DAYS, + ); + self.price_4y_total_return.multi_insert_percentage_change( + dates, + &mut self.closes.date, + 4 * ONE_YEAR_IN_DAYS, + ); + self.price_6y_total_return.multi_insert_percentage_change( + dates, + &mut self.closes.date, + 6 * ONE_YEAR_IN_DAYS, + ); + self.price_8y_total_return.multi_insert_percentage_change( + dates, + &mut self.closes.date, + 8 * ONE_YEAR_IN_DAYS, + ); + self.price_10y_total_return.multi_insert_percentage_change( + dates, + &mut self.closes.date, + 10 * ONE_YEAR_IN_DAYS, + ); + + self.price_4y_compound_return + .multi_insert_complex_transform( + dates, + &mut self.closes.date, + |(last_value, date, closes)| { + let previous_value = date + .checked_sub_days(Days::new(4 * ONE_YEAR_IN_DAYS as u64)) + .and_then(|date| closes.get_or_import(&WNaiveDate::wrap(date))) + .unwrap_or_default(); + + (((last_value / previous_value).powf(1.0 / 4.0)) - 1.0) * 100.0 + }, + ); + } + + pub fn get_date_ohlc(&mut self, date: WNaiveDate) -> color_eyre::Result<OHLC> { + if self.ohlcs.date.is_date_safe(date) { + Ok(self.ohlcs.date.get(&date).unwrap().to_owned()) + } else { + let ohlc = self.get_from_daily_kraken(&date)?; + + self.ohlcs.date.insert(date, ohlc); + + Ok(ohlc) + } + } + + fn get_from_daily_kraken(&mut self, date: &WNaiveDate) -> color_eyre::Result<OHLC> { + if self.kraken_daily.is_none() { + self.kraken_daily.replace( + Kraken::fetch_daily_prices() + .unwrap_or_else(|_| Binance::fetch_daily_prices().unwrap()), + ); + } + + self.kraken_daily + .as_ref() + .unwrap() + .get(date) + .cloned() + .ok_or(Error::msg("Couldn't find date in daily kraken")) + } + + pub fn get_height_ohlc( + &mut self, + height: usize, + timestamp: u32, + previous_timestamp: Option<u32>, + ) -> color_eyre::Result<OHLC> { + if let Some(ohlc) = self.ohlcs.height.get(&height) { + return Ok(ohlc); + } + + let clean_timestamp = |timestamp| { + let date_time = Utc.timestamp_opt(i64::from(timestamp), 0).unwrap(); + + NaiveDateTime::new( + date_time.date_naive(), + NaiveTime::from_hms_opt(date_time.hour(), date_time.minute(), 0).unwrap(), + ) + .and_utc() + .timestamp() as u32 + }; + + let timestamp = clean_timestamp(timestamp); + + if previous_timestamp.is_none() && height > 0 { + panic!("Shouldn't be possible"); + } + + let previous_timestamp = previous_timestamp.map(clean_timestamp); + + let ohlc = self.get_from_1mn_kraken(timestamp, previous_timestamp).unwrap_or_else(|_| { + self.get_from_1mn_binance(timestamp, previous_timestamp) + .unwrap_or_else(|_| self.get_from_har_binance(timestamp, previous_timestamp).unwrap_or_else(|_| { + let date = WNaiveDate::from_timestamp(timestamp); + + panic!( + "Can't find price for {height} - {timestamp} - {date}, please update binance.har file" + ) + })) + }); + + self.ohlcs.height.insert(height, ohlc); + + Ok(ohlc) + } + + fn get_from_1mn_kraken( + &mut self, + timestamp: u32, + previous_timestamp: Option<u32>, + ) -> color_eyre::Result<OHLC> { + if self.kraken_1mn.is_none() { + self.kraken_1mn.replace(Kraken::fetch_1mn_prices()?); + } + + Self::find_height_ohlc(&self.kraken_1mn, timestamp, previous_timestamp, "kraken 1m") + } + + fn get_from_1mn_binance( + &mut self, + timestamp: u32, + previous_timestamp: Option<u32>, + ) -> color_eyre::Result<OHLC> { + if self.binance_1mn.is_none() { + self.binance_1mn.replace(Binance::fetch_1mn_prices()?); + } + + Self::find_height_ohlc( + &self.binance_1mn, + timestamp, + previous_timestamp, + "binance 1m", + ) + } + + fn get_from_har_binance( + &mut self, + timestamp: u32, + previous_timestamp: Option<u32>, + ) -> color_eyre::Result<OHLC> { + if self.binance_har.is_none() { + self.binance_har.replace(Binance::read_har_file()?); + } + + Self::find_height_ohlc( + &self.binance_har, + timestamp, + previous_timestamp, + "binance har", + ) + } + + fn find_height_ohlc( + tree: &Option<BTreeMap<u32, OHLC>>, + timestamp: u32, + previous_timestamp: Option<u32>, + name: &str, + ) -> color_eyre::Result<OHLC> { + let tree = tree.as_ref().unwrap(); + + let err = Error::msg(format!("Couldn't find timestamp in {name}")); + + let previous_ohlc = previous_timestamp + .map_or(Some(OHLC::default()), |previous_timestamp| { + tree.get(&previous_timestamp).cloned() + }); + + let last_ohlc = tree.get(×tamp); + + if previous_ohlc.is_none() || last_ohlc.is_none() { + return Err(err); + } + + let previous_ohlc = previous_ohlc.unwrap(); + + let mut final_ohlc = OHLC { + open: previous_ohlc.close, + high: previous_ohlc.close, + low: previous_ohlc.close, + close: previous_ohlc.close, + }; + + let start = previous_timestamp.unwrap_or(0); + let end = timestamp; + + // Otherwise it's a re-org + if start < end { + tree.range(&start..=&end).skip(1).for_each(|(_, ohlc)| { + if ohlc.high > final_ohlc.high { + final_ohlc.high = ohlc.high + } + + if ohlc.low < final_ohlc.low { + final_ohlc.low = ohlc.low + } + + final_ohlc.close = ohlc.close; + }); + } + + Ok(final_ohlc) + } +} + +impl AnyDataset for PriceDatasets { + fn get_min_initial_states(&self) -> &MinInitialStates { + &self.min_initial_states + } + + fn to_inserted_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![&self.ohlcs] + } + + fn to_inserted_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![&mut self.ohlcs] + } + + fn to_computed_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![&self.closes, &self.market_cap] + } + + fn to_computed_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![&mut self.closes, &mut self.market_cap] + } + + fn to_computed_date_map_vec(&self) -> Vec<&(dyn AnyDateMap + Send + Sync)> { + vec![ + &self.price_1w_sma, + &self.price_1m_sma, + &self.price_1y_sma, + &self.price_2y_sma, + &self.price_4y_sma, + &self.price_8d_sma, + &self.price_13d_sma, + &self.price_21d_sma, + &self.price_34d_sma, + &self.price_55d_sma, + &self.price_89d_sma, + &self.price_144d_sma, + &self.price_200w_sma, + &self.price_1d_total_return, + &self.price_1m_total_return, + &self.price_6m_total_return, + &self.price_1y_total_return, + &self.price_2y_total_return, + &self.price_3y_total_return, + &self.price_4y_total_return, + &self.price_6y_total_return, + &self.price_8y_total_return, + &self.price_10y_total_return, + &self.price_4y_compound_return, + ] + } + + fn to_computed_mut_date_map_vec(&mut self) -> Vec<&mut dyn AnyDateMap> { + vec![ + &mut self.price_1w_sma, + &mut self.price_1m_sma, + &mut self.price_1y_sma, + &mut self.price_2y_sma, + &mut self.price_4y_sma, + &mut self.price_8d_sma, + &mut self.price_13d_sma, + &mut self.price_21d_sma, + &mut self.price_34d_sma, + &mut self.price_55d_sma, + &mut self.price_89d_sma, + &mut self.price_144d_sma, + &mut self.price_200w_sma, + &mut self.price_1d_total_return, + &mut self.price_1m_total_return, + &mut self.price_6m_total_return, + &mut self.price_1y_total_return, + &mut self.price_2y_total_return, + &mut self.price_3y_total_return, + &mut self.price_4y_total_return, + &mut self.price_6y_total_return, + &mut self.price_8y_total_return, + &mut self.price_10y_total_return, + &mut self.price_4y_compound_return, + ] + } +} diff --git a/parser/src/datasets/price/ohlc.rs b/parser/src/datasets/price/ohlc.rs new file mode 100644 index 000000000..666a52b1f --- /dev/null +++ b/parser/src/datasets/price/ohlc.rs @@ -0,0 +1,12 @@ +use allocative::Allocative; +use bincode::{Decode, Encode}; +use serde::{Deserialize, Serialize}; + +#[allow(clippy::upper_case_acronyms)] +#[derive(Debug, Default, Deserialize, Serialize, Encode, Decode, Clone, Copy, Allocative)] +pub struct OHLC { + pub open: f32, + pub high: f32, + pub low: f32, + pub close: f32, +} diff --git a/parser/src/datasets/subs/capitalization.rs b/parser/src/datasets/subs/capitalization.rs new file mode 100644 index 000000000..f9cc17acd --- /dev/null +++ b/parser/src/datasets/subs/capitalization.rs @@ -0,0 +1,121 @@ +use allocative::Allocative; + +use crate::{ + datasets::{AnyDataset, ComputeData, InsertData, MinInitialStates}, + states::CapitalizationState, + structs::{AnyBiMap, BiMap}, + utils::ONE_MONTH_IN_DAYS, +}; + +#[derive(Default, Allocative)] +pub struct CapitalizationDataset { + min_initial_states: MinInitialStates, + + // Inserted + pub realized_cap: BiMap<f32>, + + // Computed + pub realized_price: BiMap<f32>, + mvrv: BiMap<f32>, + realized_cap_1m_net_change: BiMap<f32>, +} + +impl CapitalizationDataset { + pub fn import(parent_path: &str) -> color_eyre::Result<Self> { + let f = |s: &str| format!("{parent_path}/{s}"); + + let mut s = Self { + min_initial_states: MinInitialStates::default(), + + realized_cap: BiMap::new_bin(1, &f("realized_cap")), + realized_cap_1m_net_change: BiMap::new_bin(1, &f("realized_cap_1m_net_change")), + realized_price: BiMap::new_bin(1, &f("realized_price")), + mvrv: BiMap::new_bin(1, &f("mvrv")), + }; + + s.min_initial_states + .consume(MinInitialStates::compute_from_dataset(&s)); + + Ok(s) + } + + pub fn insert( + &mut self, + &InsertData { + height, + is_date_last_block, + date, + .. + }: &InsertData, + state: &CapitalizationState, + ) { + let realized_cap = self + .realized_cap + .height + .insert(height, state.realized_cap.to_dollar() as f32); + + if is_date_last_block { + self.realized_cap.date.insert(date, realized_cap); + } + } + + pub fn compute( + &mut self, + &ComputeData { heights, dates }: &ComputeData, + closes: &mut BiMap<f32>, + cohort_supply: &mut BiMap<f64>, + ) { + self.realized_price.multi_insert_divide( + heights, + dates, + &mut self.realized_cap, + cohort_supply, + ); + + self.mvrv.height.multi_insert_divide( + heights, + &mut closes.height, + &mut self.realized_price.height, + ); + self.mvrv + .date + .multi_insert_divide(dates, &mut closes.date, &mut self.realized_price.date); + + self.realized_cap_1m_net_change.multi_insert_net_change( + heights, + dates, + &mut self.realized_cap, + ONE_MONTH_IN_DAYS, + ) + } +} + +impl AnyDataset for CapitalizationDataset { + fn get_min_initial_states(&self) -> &MinInitialStates { + &self.min_initial_states + } + + fn to_inserted_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![&self.realized_cap] + } + + fn to_inserted_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![&mut self.realized_cap] + } + + fn to_computed_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![ + &self.realized_price, + &self.mvrv, + &self.realized_cap_1m_net_change, + ] + } + + fn to_computed_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![ + &mut self.realized_price, + &mut self.mvrv, + &mut self.realized_cap_1m_net_change, + ] + } +} diff --git a/parser/src/datasets/subs/input.rs b/parser/src/datasets/subs/input.rs new file mode 100644 index 000000000..68b119e29 --- /dev/null +++ b/parser/src/datasets/subs/input.rs @@ -0,0 +1,70 @@ +use allocative::Allocative; + +use crate::{ + datasets::{AnyDataset, InsertData, MinInitialStates}, + states::InputState, + structs::{AnyBiMap, BiMap}, +}; + +#[derive(Default, Allocative)] +pub struct InputSubDataset { + min_initial_states: MinInitialStates, + + pub count: BiMap<u64>, + pub volume: BiMap<f64>, + // add inputs_per_second +} + +impl InputSubDataset { + pub fn import(parent_path: &str) -> color_eyre::Result<Self> { + let f = |s: &str| format!("{parent_path}/{s}"); + + let mut s = Self { + min_initial_states: MinInitialStates::default(), + + count: BiMap::new_bin(1, &f("input_count")), + volume: BiMap::new_bin(1, &f("input_volume")), + }; + + s.min_initial_states + .consume(MinInitialStates::compute_from_dataset(&s)); + + Ok(s) + } + + pub fn insert( + &mut self, + &InsertData { + height, + date, + is_date_last_block, + date_blocks_range, + .. + }: &InsertData, + state: &InputState, + ) { + let count = self.count.height.insert(height, state.count.round() as u64); + + self.volume.height.insert(height, state.volume.to_btc()); + + if is_date_last_block { + self.count.date.insert(date, count); + + self.volume.date_insert_sum_range(date, date_blocks_range); + } + } +} + +impl AnyDataset for InputSubDataset { + fn get_min_initial_states(&self) -> &MinInitialStates { + &self.min_initial_states + } + + fn to_inserted_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![&self.count, &self.volume] + } + + fn to_inserted_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![&mut self.count, &mut self.volume] + } +} diff --git a/parser/src/datasets/subs/mod.rs b/parser/src/datasets/subs/mod.rs new file mode 100644 index 000000000..55ac3ca35 --- /dev/null +++ b/parser/src/datasets/subs/mod.rs @@ -0,0 +1,80 @@ +use allocative::Allocative; + +mod capitalization; +mod input; +// mod output; +mod price_paid; +mod realized; +mod supply; +mod unrealized; +mod utxo; + +pub use capitalization::*; +pub use input::*; +// pub use output::*; +pub use price_paid::*; +pub use realized::*; +pub use supply::*; +pub use unrealized::*; +pub use utxo::*; + +use crate::datasets::AnyDataset; + +use super::AnyDatasetGroup; + +#[derive(Default, Allocative)] +pub struct SubDataset { + pub capitalization: CapitalizationDataset, + pub input: InputSubDataset, + // pub output: OutputSubDataset, + pub price_paid: PricePaidSubDataset, + pub realized: RealizedSubDataset, + pub supply: SupplySubDataset, + pub unrealized: UnrealizedSubDataset, + pub utxo: UTXOSubDataset, +} + +impl SubDataset { + pub fn import(parent_path: &str) -> color_eyre::Result<Self> { + let s = Self { + capitalization: CapitalizationDataset::import(parent_path)?, + input: InputSubDataset::import(parent_path)?, + // output: OutputSubDataset::import(parent_path)?, + price_paid: PricePaidSubDataset::import(parent_path)?, + realized: RealizedSubDataset::import(parent_path)?, + supply: SupplySubDataset::import(parent_path)?, + unrealized: UnrealizedSubDataset::import(parent_path)?, + utxo: UTXOSubDataset::import(parent_path)?, + }; + + Ok(s) + } +} + +impl AnyDatasetGroup for SubDataset { + fn as_vec(&self) -> Vec<&(dyn AnyDataset + Send + Sync)> { + vec![ + &self.capitalization, + &self.price_paid, + &self.realized, + &self.supply, + &self.unrealized, + &self.utxo, + &self.input, + // &self.output, + ] + } + + fn as_mut_vec(&mut self) -> Vec<&mut dyn AnyDataset> { + vec![ + &mut self.capitalization, + &mut self.price_paid, + &mut self.realized, + &mut self.supply, + &mut self.unrealized, + &mut self.utxo, + &mut self.input, + // &mut self.output, + ] + } +} diff --git a/parser/src/datasets/subs/output.rs b/parser/src/datasets/subs/output.rs new file mode 100644 index 000000000..a61219409 --- /dev/null +++ b/parser/src/datasets/subs/output.rs @@ -0,0 +1,103 @@ +use crate::{ + datasets::{AnyDataset, ComputeData, InsertData, MinInitialStates}, + states::OutputState, + structs::{AnyBiMap, BiMap}, + utils::ONE_YEAR_IN_DAYS, +}; + +pub struct OutputSubDataset { + min_initial_states: MinInitialStates, + + // Inserted + pub count: BiMap<f32>, + pub volume: BiMap<f32>, + + // Computed + pub annualized_volume: BiMap<f32>, + pub velocity: BiMap<f32>, + // add outputs_per_second +} + +impl OutputSubDataset { + pub fn import(parent_path: &str) -> color_eyre::Result<Self> { + let f = |s: &str| format!("{parent_path}/{s}"); + + let mut s = Self { + min_initial_states: MinInitialStates::default(), + + count: BiMap::new_bin(1, &f("output_count")), + volume: BiMap::new_bin(1, &f("output_volume")), + annualized_volume: BiMap::new_bin(1, &f("annualized_output_volume")), + velocity: BiMap::new_bin(1, &f("output_velocity")), + }; + + s.min_initial_states + .consume(MinInitialStates::compute_from_dataset(&s)); + + Ok(s) + } + + pub fn insert( + &mut self, + &InsertData { + height, + date, + is_date_last_block, + date_blocks_range, + .. + }: &InsertData, + state: &OutputState, + ) { + let count = self.count.height.insert(height, state.count); + + self.volume.height.insert(height, state.volume); + + if is_date_last_block { + self.count.date.insert(date, count); + + self.volume.date_insert_sum_range(date, date_blocks_range); + } + } + + pub fn compute( + &mut self, + &ComputeData { heights, dates }: &ComputeData, + cohort_supply: &mut BiMap<f32>, + ) { + self.annualized_volume.multi_insert_last_x_sum( + heights, + dates, + &mut self.volume, + ONE_YEAR_IN_DAYS, + ); + + self.velocity.multi_insert_divide( + heights, + dates, + &mut self.annualized_volume, + cohort_supply, + ); + } +} + +impl AnyDataset for OutputSubDataset { + fn get_min_initial_states(&self) -> &MinInitialStates { + &self.min_initial_states + } + + fn to_inserted_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![&self.count, &self.volume] + } + + fn to_inserted_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![&mut self.count, &mut self.volume] + } + + fn to_computed_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![&self.annualized_volume, &self.velocity] + } + + fn to_computed_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![&mut self.annualized_volume, &mut self.velocity] + } +} diff --git a/parser/src/datasets/subs/price_paid.rs b/parser/src/datasets/subs/price_paid.rs new file mode 100644 index 000000000..8c2292ad1 --- /dev/null +++ b/parser/src/datasets/subs/price_paid.rs @@ -0,0 +1,293 @@ +use allocative::Allocative; +use itertools::Itertools; + +use crate::{ + datasets::{AnyDataset, InsertData, MinInitialStates}, + states::PricePaidState, + structs::{AnyBiMap, BiMap, WNaiveDate}, +}; + +#[derive(Default, Allocative)] +pub struct PricePaidSubDataset { + min_initial_states: MinInitialStates, + + // Inserted + pp_median: BiMap<f32>, + pp_95p: BiMap<f32>, + pp_90p: BiMap<f32>, + pp_85p: BiMap<f32>, + pp_80p: BiMap<f32>, + pp_75p: BiMap<f32>, + pp_70p: BiMap<f32>, + pp_65p: BiMap<f32>, + pp_60p: BiMap<f32>, + pp_55p: BiMap<f32>, + pp_45p: BiMap<f32>, + pp_40p: BiMap<f32>, + pp_35p: BiMap<f32>, + pp_30p: BiMap<f32>, + pp_25p: BiMap<f32>, + pp_20p: BiMap<f32>, + pp_15p: BiMap<f32>, + pp_10p: BiMap<f32>, + pp_05p: BiMap<f32>, +} + +impl PricePaidSubDataset { + pub fn import(parent_path: &str) -> color_eyre::Result<Self> { + let f = |s: &str| format!("{parent_path}/{s}"); + + let mut s = Self { + min_initial_states: MinInitialStates::default(), + + pp_median: BiMap::new_bin(1, &f("median_price_paid")), + pp_95p: BiMap::new_bin(1, &f("95p_price_paid")), + pp_90p: BiMap::new_bin(1, &f("90p_price_paid")), + pp_85p: BiMap::new_bin(1, &f("85p_price_paid")), + pp_80p: BiMap::new_bin(1, &f("80p_price_paid")), + pp_75p: BiMap::new_bin(1, &f("75p_price_paid")), + pp_70p: BiMap::new_bin(1, &f("70p_price_paid")), + pp_65p: BiMap::new_bin(1, &f("65p_price_paid")), + pp_60p: BiMap::new_bin(1, &f("60p_price_paid")), + pp_55p: BiMap::new_bin(1, &f("55p_price_paid")), + pp_45p: BiMap::new_bin(1, &f("45p_price_paid")), + pp_40p: BiMap::new_bin(1, &f("40p_price_paid")), + pp_35p: BiMap::new_bin(1, &f("35p_price_paid")), + pp_30p: BiMap::new_bin(1, &f("30p_price_paid")), + pp_25p: BiMap::new_bin(1, &f("25p_price_paid")), + pp_20p: BiMap::new_bin(1, &f("20p_price_paid")), + pp_15p: BiMap::new_bin(1, &f("15p_price_paid")), + pp_10p: BiMap::new_bin(1, &f("10p_price_paid")), + pp_05p: BiMap::new_bin(1, &f("05p_price_paid")), + }; + + s.min_initial_states + .consume(MinInitialStates::compute_from_dataset(&s)); + + Ok(s) + } + + pub fn insert( + &mut self, + &InsertData { + height, + is_date_last_block, + date, + .. + }: &InsertData, + state: &PricePaidState, + ) { + let PricePaidState { + pp_05p, + pp_10p, + pp_15p, + pp_20p, + pp_25p, + pp_30p, + pp_35p, + pp_40p, + pp_45p, + pp_median, + pp_55p, + pp_60p, + pp_65p, + pp_70p, + pp_75p, + pp_80p, + pp_85p, + pp_90p, + pp_95p, + .. + } = state; + + // Check if iter was empty + if pp_05p.is_none() { + self.insert_height_default(height); + + if is_date_last_block { + self.insert_date_default(date); + } + + return; + } + + let pp_05p = self + .pp_05p + .height + .insert(height, pp_05p.unwrap().to_dollar() as f32); + let pp_10p = self + .pp_10p + .height + .insert(height, pp_10p.unwrap().to_dollar() as f32); + let pp_15p = self + .pp_15p + .height + .insert(height, pp_15p.unwrap().to_dollar() as f32); + let pp_20p = self + .pp_20p + .height + .insert(height, pp_20p.unwrap().to_dollar() as f32); + let pp_25p = self + .pp_25p + .height + .insert(height, pp_25p.unwrap().to_dollar() as f32); + let pp_30p = self + .pp_30p + .height + .insert(height, pp_30p.unwrap().to_dollar() as f32); + let pp_35p = self + .pp_35p + .height + .insert(height, pp_35p.unwrap().to_dollar() as f32); + let pp_40p = self + .pp_40p + .height + .insert(height, pp_40p.unwrap().to_dollar() as f32); + let pp_45p = self + .pp_45p + .height + .insert(height, pp_45p.unwrap().to_dollar() as f32); + let pp_median = self + .pp_median + .height + .insert(height, pp_median.unwrap().to_dollar() as f32); + let pp_55p = self + .pp_55p + .height + .insert(height, pp_55p.unwrap().to_dollar() as f32); + let pp_60p = self + .pp_60p + .height + .insert(height, pp_60p.unwrap().to_dollar() as f32); + let pp_65p = self + .pp_65p + .height + .insert(height, pp_65p.unwrap().to_dollar() as f32); + let pp_70p = self + .pp_70p + .height + .insert(height, pp_70p.unwrap().to_dollar() as f32); + let pp_75p = self + .pp_75p + .height + .insert(height, pp_75p.unwrap().to_dollar() as f32); + let pp_80p = self + .pp_80p + .height + .insert(height, pp_80p.unwrap().to_dollar() as f32); + let pp_85p = self + .pp_85p + .height + .insert(height, pp_85p.unwrap().to_dollar() as f32); + let pp_90p = self + .pp_90p + .height + .insert(height, pp_90p.unwrap().to_dollar() as f32); + let pp_95p = self + .pp_95p + .height + .insert(height, pp_95p.unwrap().to_dollar() as f32); + + if is_date_last_block { + self.pp_05p.date.insert(date, pp_05p); + self.pp_10p.date.insert(date, pp_10p); + self.pp_15p.date.insert(date, pp_15p); + self.pp_20p.date.insert(date, pp_20p); + self.pp_25p.date.insert(date, pp_25p); + self.pp_30p.date.insert(date, pp_30p); + self.pp_35p.date.insert(date, pp_35p); + self.pp_40p.date.insert(date, pp_40p); + self.pp_45p.date.insert(date, pp_45p); + self.pp_median.date.insert(date, pp_median); + self.pp_55p.date.insert(date, pp_55p); + self.pp_60p.date.insert(date, pp_60p); + self.pp_65p.date.insert(date, pp_65p); + self.pp_70p.date.insert(date, pp_70p); + self.pp_75p.date.insert(date, pp_75p); + self.pp_80p.date.insert(date, pp_80p); + self.pp_85p.date.insert(date, pp_85p); + self.pp_90p.date.insert(date, pp_90p); + self.pp_95p.date.insert(date, pp_95p); + } + } + + fn insert_height_default(&mut self, height: usize) { + self.inserted_as_mut_vec().into_iter().for_each(|bi| { + bi.height.insert_default(height); + }) + } + + fn insert_date_default(&mut self, date: WNaiveDate) { + self.inserted_as_mut_vec().into_iter().for_each(|bi| { + bi.date.insert_default(date); + }) + } + + pub fn inserted_as_vec(&self) -> Vec<&BiMap<f32>> { + vec![ + &self.pp_95p, + &self.pp_90p, + &self.pp_85p, + &self.pp_80p, + &self.pp_75p, + &self.pp_70p, + &self.pp_65p, + &self.pp_60p, + &self.pp_55p, + &self.pp_median, + &self.pp_45p, + &self.pp_40p, + &self.pp_35p, + &self.pp_30p, + &self.pp_25p, + &self.pp_20p, + &self.pp_15p, + &self.pp_10p, + &self.pp_05p, + ] + } + + pub fn inserted_as_mut_vec(&mut self) -> Vec<&mut BiMap<f32>> { + vec![ + &mut self.pp_95p, + &mut self.pp_90p, + &mut self.pp_85p, + &mut self.pp_80p, + &mut self.pp_75p, + &mut self.pp_70p, + &mut self.pp_65p, + &mut self.pp_60p, + &mut self.pp_55p, + &mut self.pp_median, + &mut self.pp_45p, + &mut self.pp_40p, + &mut self.pp_35p, + &mut self.pp_30p, + &mut self.pp_25p, + &mut self.pp_20p, + &mut self.pp_15p, + &mut self.pp_10p, + &mut self.pp_05p, + ] + } +} + +impl AnyDataset for PricePaidSubDataset { + fn get_min_initial_states(&self) -> &MinInitialStates { + &self.min_initial_states + } + + fn to_inserted_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + self.inserted_as_vec() + .into_iter() + .map(|dataset| dataset as &(dyn AnyBiMap + Send + Sync)) + .collect_vec() + } + + fn to_inserted_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + self.inserted_as_mut_vec() + .into_iter() + .map(|dataset| dataset as &mut dyn AnyBiMap) + .collect_vec() + } +} diff --git a/parser/src/datasets/subs/realized.rs b/parser/src/datasets/subs/realized.rs new file mode 100644 index 000000000..71ed85f5e --- /dev/null +++ b/parser/src/datasets/subs/realized.rs @@ -0,0 +1,178 @@ +use allocative::Allocative; + +use crate::{ + datasets::{AnyDataset, ComputeData, InsertData, MinInitialStates}, + states::RealizedState, + structs::{AnyBiMap, BiMap}, + utils::ONE_MONTH_IN_DAYS, +}; + +/// TODO: Fix fees not taken into account ? +#[derive(Default, Allocative)] +pub struct RealizedSubDataset { + min_initial_states: MinInitialStates, + + // Inserted + realized_profit: BiMap<f32>, + realized_loss: BiMap<f32>, + + // Computed + negative_realized_loss: BiMap<f32>, + net_realized_profit_and_loss: BiMap<f32>, + net_realized_profit_and_loss_to_market_cap_ratio: BiMap<f32>, + cumulative_realized_profit: BiMap<f32>, + cumulative_realized_loss: BiMap<f32>, + cumulative_net_realized_profit_and_loss: BiMap<f32>, + cumulative_net_realized_profit_and_loss_1m_net_change: BiMap<f32>, +} + +impl RealizedSubDataset { + pub fn import(parent_path: &str) -> color_eyre::Result<Self> { + let f = |s: &str| format!("{parent_path}/{s}"); + + let mut s = Self { + min_initial_states: MinInitialStates::default(), + + realized_profit: BiMap::new_bin(1, &f("realized_profit")), + realized_loss: BiMap::new_bin(1, &f("realized_loss")), + negative_realized_loss: BiMap::new_bin(2, &f("negative_realized_loss")), + net_realized_profit_and_loss: BiMap::new_bin(1, &f("net_realized_profit_and_loss")), + net_realized_profit_and_loss_to_market_cap_ratio: BiMap::new_bin( + 1, + &f("net_realized_profit_and_loss_to_market_cap_ratio"), + ), + cumulative_realized_profit: BiMap::new_bin(1, &f("cumulative_realized_profit")), + cumulative_realized_loss: BiMap::new_bin(1, &f("cumulative_realized_loss")), + cumulative_net_realized_profit_and_loss: BiMap::new_bin( + 1, + &f("cumulative_net_realized_profit_and_loss"), + ), + cumulative_net_realized_profit_and_loss_1m_net_change: BiMap::new_bin( + 1, + &f("cumulative_net_realized_profit_and_loss_1m_net_change"), + ), + }; + + s.min_initial_states + .consume(MinInitialStates::compute_from_dataset(&s)); + + Ok(s) + } + + pub fn insert( + &mut self, + &InsertData { + height, + date, + is_date_last_block, + date_blocks_range, + .. + }: &InsertData, + height_state: &RealizedState, + ) { + self.realized_profit + .height + .insert(height, height_state.realized_profit.to_dollar() as f32); + + self.realized_loss + .height + .insert(height, height_state.realized_loss.to_dollar() as f32); + + if is_date_last_block { + self.realized_profit + .date_insert_sum_range(date, date_blocks_range); + + self.realized_loss + .date_insert_sum_range(date, date_blocks_range); + } + } + + pub fn compute( + &mut self, + &ComputeData { heights, dates }: &ComputeData, + market_cap: &mut BiMap<f32>, + ) { + self.negative_realized_loss.multi_insert_simple_transform( + heights, + dates, + &mut self.realized_loss, + &|v| v * -1.0, + ); + + self.net_realized_profit_and_loss.multi_insert_subtract( + heights, + dates, + &mut self.realized_profit, + &mut self.realized_loss, + ); + + self.net_realized_profit_and_loss_to_market_cap_ratio + .multi_insert_divide( + heights, + dates, + &mut self.net_realized_profit_and_loss, + market_cap, + ); + + self.cumulative_realized_profit.multi_insert_cumulative( + heights, + dates, + &mut self.realized_profit, + ); + + self.cumulative_realized_loss.multi_insert_cumulative( + heights, + dates, + &mut self.realized_loss, + ); + + self.cumulative_net_realized_profit_and_loss + .multi_insert_cumulative(heights, dates, &mut self.net_realized_profit_and_loss); + + self.cumulative_net_realized_profit_and_loss_1m_net_change + .multi_insert_net_change( + heights, + dates, + &mut self.cumulative_net_realized_profit_and_loss, + ONE_MONTH_IN_DAYS, + ); + } +} + +impl AnyDataset for RealizedSubDataset { + fn get_min_initial_states(&self) -> &MinInitialStates { + &self.min_initial_states + } + + fn to_inserted_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![&self.realized_loss, &self.realized_profit] + } + + fn to_inserted_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![&mut self.realized_loss, &mut self.realized_profit] + } + + fn to_computed_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![ + &self.negative_realized_loss, + &self.net_realized_profit_and_loss, + &self.net_realized_profit_and_loss_to_market_cap_ratio, + &self.cumulative_realized_profit, + &self.cumulative_realized_loss, + &self.cumulative_net_realized_profit_and_loss, + &self.cumulative_net_realized_profit_and_loss_1m_net_change, + ] + } + + fn to_computed_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![ + &mut self.negative_realized_loss, + &mut self.net_realized_profit_and_loss, + &mut self.net_realized_profit_and_loss_to_market_cap_ratio, + &mut self.cumulative_realized_profit, + &mut self.cumulative_realized_loss, + &mut self.cumulative_net_realized_profit_and_loss, + &mut self.cumulative_net_realized_profit_and_loss_1m_net_change, + ] + } +} diff --git a/parser/src/datasets/subs/supply.rs b/parser/src/datasets/subs/supply.rs new file mode 100644 index 000000000..975d7596b --- /dev/null +++ b/parser/src/datasets/subs/supply.rs @@ -0,0 +1,114 @@ +use allocative::Allocative; + +use crate::{ + datasets::{AnyDataset, ComputeData, InsertData, MinInitialStates}, + states::SupplyState, + structs::{AnyBiMap, BiMap}, +}; + +#[derive(Default, Allocative)] +pub struct SupplySubDataset { + min_initial_states: MinInitialStates, + + // Inserted + pub supply: BiMap<f64>, + + // Computed + pub supply_to_circulating_supply_ratio: BiMap<f64>, + pub halved_supply: BiMap<f64>, + pub halved_supply_to_circulating_supply_ratio: BiMap<f64>, +} + +impl SupplySubDataset { + pub fn import(parent_path: &str) -> color_eyre::Result<Self> { + let f = |s: &str| format!("{parent_path}/{s}"); + + let mut s = Self { + min_initial_states: MinInitialStates::default(), + + supply: BiMap::new_bin(1, &f("supply")), + supply_to_circulating_supply_ratio: BiMap::new_bin( + 1, + &f("supply_to_circulating_supply_ratio"), + ), + halved_supply: BiMap::new_bin(1, &f("halved_supply")), + halved_supply_to_circulating_supply_ratio: BiMap::new_bin( + 1, + &f("halved_supply_to_circulating_supply_ratio"), + ), + }; + + s.min_initial_states + .consume(MinInitialStates::compute_from_dataset(&s)); + + Ok(s) + } + + pub fn insert( + &mut self, + &InsertData { + height, + date, + is_date_last_block, + .. + }: &InsertData, + state: &SupplyState, + ) { + let total_supply = self.supply.height.insert(height, state.supply.to_btc()); + + if is_date_last_block { + self.supply.date.insert(date, total_supply); + } + } + + #[allow(unused_variables)] + pub fn compute( + &mut self, + &ComputeData { heights, dates }: &ComputeData, + circulating_supply: &mut BiMap<f64>, + ) { + self.supply_to_circulating_supply_ratio + .multi_insert_percentage(heights, dates, &mut self.supply, circulating_supply); + + self.halved_supply + .multi_insert_simple_transform(heights, dates, &mut self.supply, &|v| v / 2.0); + + self.halved_supply_to_circulating_supply_ratio + .multi_insert_simple_transform( + heights, + dates, + &mut self.supply_to_circulating_supply_ratio, + &|v| v / 2.0, + ); + } +} + +impl AnyDataset for SupplySubDataset { + fn get_min_initial_states(&self) -> &MinInitialStates { + &self.min_initial_states + } + + fn to_inserted_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![&self.supply] + } + + fn to_inserted_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![&mut self.supply] + } + + fn to_computed_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![ + &self.supply_to_circulating_supply_ratio, + &self.halved_supply, + &self.halved_supply_to_circulating_supply_ratio, + ] + } + + fn to_computed_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![ + &mut self.supply_to_circulating_supply_ratio, + &mut self.halved_supply, + &mut self.halved_supply_to_circulating_supply_ratio, + ] + } +} diff --git a/parser/src/datasets/subs/unrealized.rs b/parser/src/datasets/subs/unrealized.rs new file mode 100644 index 000000000..dcfdc8e5c --- /dev/null +++ b/parser/src/datasets/subs/unrealized.rs @@ -0,0 +1,211 @@ +use allocative::Allocative; + +use crate::{ + datasets::{AnyDataset, ComputeData, InsertData, MinInitialStates}, + states::UnrealizedState, + structs::{AnyBiMap, BiMap}, +}; + +#[derive(Default, Allocative)] +pub struct UnrealizedSubDataset { + min_initial_states: MinInitialStates, + + // Inserted + supply_in_profit: BiMap<f64>, + unrealized_profit: BiMap<f32>, + unrealized_loss: BiMap<f32>, + + // Computed + supply_in_loss: BiMap<f64>, + negative_unrealized_loss: BiMap<f32>, + net_unrealized_profit_and_loss: BiMap<f32>, + net_unrealized_profit_and_loss_to_market_cap_ratio: BiMap<f32>, + supply_in_profit_to_own_supply_ratio: BiMap<f64>, + supply_in_profit_to_circulating_supply_ratio: BiMap<f64>, + supply_in_loss_to_own_supply_ratio: BiMap<f64>, + supply_in_loss_to_circulating_supply_ratio: BiMap<f64>, +} + +impl UnrealizedSubDataset { + pub fn import(parent_path: &str) -> color_eyre::Result<Self> { + let f = |s: &str| format!("{parent_path}/{s}"); + + let mut s = Self { + min_initial_states: MinInitialStates::default(), + + supply_in_profit: BiMap::new_bin(1, &f("supply_in_profit")), + supply_in_loss: BiMap::new_bin(1, &f("supply_in_loss")), + unrealized_profit: BiMap::new_bin(1, &f("unrealized_profit")), + unrealized_loss: BiMap::new_bin(1, &f("unrealized_loss")), + negative_unrealized_loss: BiMap::new_bin(1, &f("negative_unrealized_loss")), + net_unrealized_profit_and_loss: BiMap::new_bin(1, &f("net_unrealized_profit_and_loss")), + net_unrealized_profit_and_loss_to_market_cap_ratio: BiMap::new_bin( + 1, + &f("net_unrealized_profit_and_loss_to_market_cap_ratio"), + ), + supply_in_profit_to_own_supply_ratio: BiMap::new_bin( + 1, + &f("supply_in_profit_to_own_supply_ratio"), + ), + supply_in_profit_to_circulating_supply_ratio: BiMap::new_bin( + 1, + &f("supply_in_profit_to_circulating_supply_ratio"), + ), + supply_in_loss_to_own_supply_ratio: BiMap::new_bin( + 1, + &f("supply_in_loss_to_own_supply_ratio"), + ), + supply_in_loss_to_circulating_supply_ratio: BiMap::new_bin( + 1, + &f("supply_in_loss_to_circulating_supply_ratio"), + ), + }; + + s.min_initial_states + .consume(MinInitialStates::compute_from_dataset(&s)); + + Ok(s) + } + + pub fn insert( + &mut self, + &InsertData { + height, + date, + is_date_last_block, + .. + }: &InsertData, + block_state: &UnrealizedState, + date_state: &Option<UnrealizedState>, + ) { + self.supply_in_profit + .height + .insert(height, block_state.supply_in_profit.to_btc()); + + self.unrealized_profit + .height + .insert(height, block_state.unrealized_profit.to_dollar() as f32); + + self.unrealized_loss + .height + .insert(height, block_state.unrealized_loss.to_dollar() as f32); + + if is_date_last_block { + let date_state = date_state.as_ref().unwrap(); + + self.supply_in_profit + .date + .insert(date, date_state.supply_in_profit.to_btc()); + + self.unrealized_profit + .date + .insert(date, date_state.unrealized_profit.to_dollar() as f32); + + self.unrealized_loss + .date + .insert(date, date_state.unrealized_loss.to_dollar() as f32); + } + } + + pub fn compute( + &mut self, + &ComputeData { heights, dates }: &ComputeData, + own_supply: &mut BiMap<f64>, + circulating_supply: &mut BiMap<f64>, + market_cap: &mut BiMap<f32>, + ) { + self.supply_in_loss.multi_insert_subtract( + heights, + dates, + own_supply, + &mut self.supply_in_profit, + ); + + self.negative_unrealized_loss.multi_insert_simple_transform( + heights, + dates, + &mut self.unrealized_loss, + &|v| v * -1.0, + ); + + self.net_unrealized_profit_and_loss.multi_insert_subtract( + heights, + dates, + &mut self.unrealized_profit, + &mut self.unrealized_loss, + ); + + self.net_unrealized_profit_and_loss_to_market_cap_ratio + .multi_insert_divide( + heights, + dates, + &mut self.net_unrealized_profit_and_loss, + market_cap, + ); + + self.supply_in_profit_to_own_supply_ratio + .multi_insert_percentage(heights, dates, &mut self.supply_in_profit, own_supply); + + self.supply_in_profit_to_circulating_supply_ratio + .multi_insert_percentage( + heights, + dates, + &mut self.supply_in_profit, + circulating_supply, + ); + + self.supply_in_loss_to_own_supply_ratio + .multi_insert_percentage(heights, dates, &mut self.supply_in_loss, own_supply); + + self.supply_in_loss_to_circulating_supply_ratio + .multi_insert_percentage(heights, dates, &mut self.supply_in_loss, circulating_supply); + } +} + +impl AnyDataset for UnrealizedSubDataset { + fn get_min_initial_states(&self) -> &MinInitialStates { + &self.min_initial_states + } + + fn to_inserted_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![ + &self.supply_in_profit, + &self.unrealized_profit, + &self.unrealized_loss, + ] + } + + fn to_inserted_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![ + &mut self.supply_in_profit, + &mut self.unrealized_profit, + &mut self.unrealized_loss, + ] + } + + fn to_computed_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![ + &self.supply_in_loss, + &self.negative_unrealized_loss, + &self.net_unrealized_profit_and_loss, + &self.net_unrealized_profit_and_loss_to_market_cap_ratio, + &self.supply_in_profit_to_own_supply_ratio, + &self.supply_in_profit_to_circulating_supply_ratio, + &self.supply_in_loss_to_own_supply_ratio, + &self.supply_in_loss_to_circulating_supply_ratio, + ] + } + + fn to_computed_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![ + &mut self.supply_in_loss, + &mut self.negative_unrealized_loss, + &mut self.net_unrealized_profit_and_loss, + &mut self.net_unrealized_profit_and_loss_to_market_cap_ratio, + &mut self.supply_in_profit_to_own_supply_ratio, + &mut self.supply_in_profit_to_circulating_supply_ratio, + &mut self.supply_in_loss_to_own_supply_ratio, + &mut self.supply_in_loss_to_circulating_supply_ratio, + ] + } +} diff --git a/parser/src/datasets/subs/utxo.rs b/parser/src/datasets/subs/utxo.rs new file mode 100644 index 000000000..eaced1ffa --- /dev/null +++ b/parser/src/datasets/subs/utxo.rs @@ -0,0 +1,63 @@ +use allocative::Allocative; + +use crate::{ + datasets::{AnyDataset, InsertData, MinInitialStates}, + states::UTXOState, + structs::{AnyBiMap, BiMap}, +}; + +#[derive(Default, Allocative)] +pub struct UTXOSubDataset { + min_initial_states: MinInitialStates, + + // Inserted + count: BiMap<usize>, +} + +impl UTXOSubDataset { + pub fn import(parent_path: &str) -> color_eyre::Result<Self> { + let f = |s: &str| format!("{parent_path}/{s}"); + + let mut s = Self { + min_initial_states: MinInitialStates::default(), + + count: BiMap::new_bin(1, &f("utxo_count")), + }; + + s.min_initial_states + .consume(MinInitialStates::compute_from_dataset(&s)); + + Ok(s) + } + + pub fn insert( + &mut self, + &InsertData { + height, + is_date_last_block, + date, + .. + }: &InsertData, + state: &UTXOState, + ) { + let count = self.count.height.insert(height, state.count); + + if is_date_last_block { + self.count.date.insert(date, count); + } + } +} + +impl AnyDataset for UTXOSubDataset { + fn get_min_initial_states(&self) -> &MinInitialStates { + &self.min_initial_states + } + + fn to_inserted_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![&self.count] + } + + fn to_inserted_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![&mut self.count] + } +} diff --git a/parser/src/datasets/transaction.rs b/parser/src/datasets/transaction.rs new file mode 100644 index 000000000..ef5b1f9e7 --- /dev/null +++ b/parser/src/datasets/transaction.rs @@ -0,0 +1,257 @@ +use allocative::Allocative; + +use crate::{ + datasets::InsertData, + structs::{AnyBiMap, BiMap, HeightMap}, + utils::{ONE_DAY_IN_S, ONE_MONTH_IN_DAYS, ONE_WEEK_IN_DAYS, ONE_YEAR_IN_DAYS}, +}; + +use super::{AnyDataset, ComputeData, MinInitialStates}; + +#[derive(Allocative)] +pub struct TransactionDataset { + min_initial_states: MinInitialStates, + + // Inserted + pub count: BiMap<usize>, + pub volume: BiMap<f64>, + pub volume_in_dollars: BiMap<f32>, + // Average sent + // Average sent in dollars + // Median sent + // Median sent in dollars + // Min + // Max + // 10th 25th 75th 90th percentiles + // type + // version + + // Computed + pub count_1w_sma: BiMap<f32>, + pub count_1m_sma: BiMap<f32>, + pub volume_1w_sma: BiMap<f32>, + pub volume_1m_sma: BiMap<f32>, + pub volume_in_dollars_1w_sma: BiMap<f32>, + pub volume_in_dollars_1m_sma: BiMap<f32>, + pub annualized_volume: BiMap<f32>, + pub annualized_volume_in_dollars: BiMap<f32>, + pub velocity: BiMap<f32>, + pub transactions_per_second: BiMap<f32>, + pub transactions_per_second_1w_sma: BiMap<f32>, + pub transactions_per_second_1m_sma: BiMap<f32>, +} + +impl TransactionDataset { + pub fn import(parent_path: &str) -> color_eyre::Result<Self> { + let f = |s: &str| format!("{parent_path}/{s}"); + + let mut s = Self { + min_initial_states: MinInitialStates::default(), + + count: BiMap::new_bin(1, &f("transaction_count")), + count_1w_sma: BiMap::new_bin(1, &f("transaction_count_1w_sma")), + count_1m_sma: BiMap::new_bin(1, &f("transaction_count_1m_sma")), + volume: BiMap::new_bin(1, &f("transaction_volume")), + volume_1w_sma: BiMap::new_bin(1, &f("transaction_volume_1w_sma")), + volume_1m_sma: BiMap::new_bin(1, &f("transaction_volume_1m_sma")), + volume_in_dollars: BiMap::new_bin(1, &f("transaction_volume_in_dollars")), + volume_in_dollars_1w_sma: BiMap::new_bin(1, &f("transaction_volume_in_dollars_1w_sma")), + volume_in_dollars_1m_sma: BiMap::new_bin(1, &f("transaction_volume_in_dollars_1m_sma")), + annualized_volume: BiMap::new_bin(1, &f("annualized_transaction_volume")), + annualized_volume_in_dollars: BiMap::new_bin( + 2, + &f("annualized_transaction_volume_in_dollars"), + ), + velocity: BiMap::new_bin(1, &f("transaction_velocity")), + transactions_per_second: BiMap::new_bin(1, &f("transactions_per_second")), + transactions_per_second_1w_sma: BiMap::new_bin(1, &f("transactions_per_second_1w_sma")), + transactions_per_second_1m_sma: BiMap::new_bin(1, &f("transactions_per_second_1m_sma")), + }; + + s.min_initial_states + .consume(MinInitialStates::compute_from_dataset(&s)); + + Ok(s) + } + + pub fn insert( + &mut self, + &InsertData { + height, + date, + amount_sent, + transaction_count, + is_date_last_block, + date_blocks_range, + block_price, + .. + }: &InsertData, + ) { + self.count.height.insert(height, transaction_count); + + self.volume.height.insert(height, amount_sent.to_btc()); + + self.volume_in_dollars + .height + .insert(height, (block_price * amount_sent).to_dollar() as f32); + + if is_date_last_block { + self.count.date_insert_sum_range(date, date_blocks_range); + + self.volume.date_insert_sum_range(date, date_blocks_range); + + self.volume_in_dollars + .date_insert_sum_range(date, date_blocks_range); + } + } + + pub fn compute( + &mut self, + &ComputeData { heights, dates }: &ComputeData, + circulating_supply: &mut BiMap<f64>, + block_interval: &mut HeightMap<u32>, + ) { + self.count_1w_sma.multi_insert_simple_average( + heights, + dates, + &mut self.count, + ONE_WEEK_IN_DAYS, + ); + + self.count_1m_sma.multi_insert_simple_average( + heights, + dates, + &mut self.count, + ONE_MONTH_IN_DAYS, + ); + + self.volume_1w_sma.multi_insert_simple_average( + heights, + dates, + &mut self.volume, + ONE_WEEK_IN_DAYS, + ); + + self.volume_1m_sma.multi_insert_simple_average( + heights, + dates, + &mut self.volume, + ONE_MONTH_IN_DAYS, + ); + + self.volume_in_dollars_1w_sma.multi_insert_simple_average( + heights, + dates, + &mut self.volume_in_dollars, + ONE_WEEK_IN_DAYS, + ); + + self.volume_in_dollars_1m_sma.multi_insert_simple_average( + heights, + dates, + &mut self.volume_in_dollars, + ONE_MONTH_IN_DAYS, + ); + + self.annualized_volume.multi_insert_last_x_sum( + heights, + dates, + &mut self.volume, + ONE_YEAR_IN_DAYS, + ); + + self.annualized_volume_in_dollars.multi_insert_last_x_sum( + heights, + dates, + &mut self.volume_in_dollars, + ONE_YEAR_IN_DAYS, + ); + + self.velocity.multi_insert_divide( + heights, + dates, + &mut self.annualized_volume, + circulating_supply, + ); + + self.transactions_per_second.height.multi_insert_divide( + heights, + &mut self.count.height, + block_interval, + ); + + self.transactions_per_second + .date + .multi_insert_simple_transform(dates, &mut self.count.date, |count| { + count as f32 / ONE_DAY_IN_S as f32 + }); + + self.transactions_per_second_1w_sma + .multi_insert_simple_average( + heights, + dates, + &mut self.transactions_per_second, + ONE_WEEK_IN_DAYS, + ); + + self.transactions_per_second_1m_sma + .multi_insert_simple_average( + heights, + dates, + &mut self.transactions_per_second, + ONE_MONTH_IN_DAYS, + ); + } +} + +impl AnyDataset for TransactionDataset { + fn get_min_initial_states(&self) -> &MinInitialStates { + &self.min_initial_states + } + + fn to_inserted_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![&self.count, &self.volume, &self.volume_in_dollars] + } + + fn to_inserted_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![ + &mut self.count, + &mut self.volume, + &mut self.volume_in_dollars, + ] + } + + fn to_computed_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + vec![ + &self.count_1w_sma, + &self.count_1m_sma, + &self.volume_1w_sma, + &self.volume_1m_sma, + &self.volume_in_dollars_1w_sma, + &self.volume_in_dollars_1m_sma, + &self.annualized_volume, + &self.annualized_volume_in_dollars, + &self.velocity, + &self.transactions_per_second, + &self.transactions_per_second_1w_sma, + &self.transactions_per_second_1m_sma, + ] + } + + fn to_computed_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + vec![ + &mut self.count_1w_sma, + &mut self.count_1m_sma, + &mut self.volume_1w_sma, + &mut self.volume_1m_sma, + &mut self.volume_in_dollars_1w_sma, + &mut self.volume_in_dollars_1m_sma, + &mut self.annualized_volume, + &mut self.annualized_volume_in_dollars, + &mut self.velocity, + &mut self.transactions_per_second, + &mut self.transactions_per_second_1w_sma, + &mut self.transactions_per_second_1m_sma, + ] + } +} diff --git a/parser/src/datasets/utxo/dataset.rs b/parser/src/datasets/utxo/dataset.rs new file mode 100644 index 000000000..53b99403a --- /dev/null +++ b/parser/src/datasets/utxo/dataset.rs @@ -0,0 +1,287 @@ +use allocative::Allocative; +use itertools::Itertools; + +use crate::{ + datasets::{ + AnyDataset, AnyDatasetGroup, ComputeData, InsertData, MinInitialStates, SubDataset, + }, + states::UTXOCohortId, + structs::{AnyBiMap, AnyDateMap, AnyHeightMap, BiMap, WNaiveDate}, +}; + +#[derive(Default, Allocative)] +pub struct UTXODataset { + id: UTXOCohortId, + + min_initial_states: MinInitialStates, + + pub subs: SubDataset, +} + +impl UTXODataset { + pub fn import(parent_path: &str, id: UTXOCohortId) -> color_eyre::Result<Self> { + let name = id.name(); + + let folder_path = format!("{parent_path}/{name}"); + + let mut s = Self { + min_initial_states: MinInitialStates::default(), + id, + subs: SubDataset::import(&folder_path)?, + }; + + s.min_initial_states + .consume(MinInitialStates::compute_from_dataset(&s)); + + Ok(s) + } + + pub fn insert(&mut self, insert_data: &InsertData) { + let &InsertData { + states, + utxo_cohorts_one_shot_states, + // utxo_cohorts_received_states, + utxo_cohorts_sent_states, + .. + } = insert_data; + + if self.needs_insert_supply(insert_data.height, insert_data.date) { + self.subs.supply.insert( + insert_data, + &states + .utxo_cohorts_durable_states + .get(&self.id) + .durable_states + .supply_state, + ); + } + + if self.needs_insert_utxo(insert_data.height, insert_data.date) { + self.subs.utxo.insert( + insert_data, + &states + .utxo_cohorts_durable_states + .get(&self.id) + .durable_states + .utxo_state, + ); + } + + if self.needs_insert_capitalization(insert_data.height, insert_data.date) { + self.subs.capitalization.insert( + insert_data, + &states + .utxo_cohorts_durable_states + .get(&self.id) + .durable_states + .capitalization_state, + ); + } + + if self.needs_insert_unrealized(insert_data.height, insert_data.date) { + self.subs.unrealized.insert( + insert_data, + &utxo_cohorts_one_shot_states + .get(&self.id) + .unrealized_block_state, + &utxo_cohorts_one_shot_states + .get(&self.id) + .unrealized_date_state, + ); + } + + if self.needs_insert_price_paid(insert_data.height, insert_data.date) { + self.subs.price_paid.insert( + insert_data, + &utxo_cohorts_one_shot_states.get(&self.id).price_paid_state, + ); + } + + if self.needs_insert_realized(insert_data.height, insert_data.date) { + self.subs.realized.insert( + insert_data, + &utxo_cohorts_sent_states.get(&self.id).realized, + ); + } + + if self.needs_insert_input(insert_data.height, insert_data.date) { + self.subs + .input + .insert(insert_data, &utxo_cohorts_sent_states.get(&self.id).input); + } + + // TODO: move output from common to address + // if self.subs.output.needs_insert(insert_data) { + // self.subs + // .output + // .insert(insert_data, utxo_cohorts_received_states.get(&self.id)); + // } + } + + pub fn needs_insert_utxo(&self, height: usize, date: WNaiveDate) -> bool { + self.subs.utxo.needs_insert(height, date) + } + + pub fn needs_insert_capitalization(&self, height: usize, date: WNaiveDate) -> bool { + self.subs.capitalization.needs_insert(height, date) + } + + pub fn needs_insert_supply(&self, height: usize, date: WNaiveDate) -> bool { + self.subs.supply.needs_insert(height, date) + } + + pub fn needs_insert_price_paid(&self, height: usize, date: WNaiveDate) -> bool { + self.subs.price_paid.needs_insert(height, date) + } + + pub fn needs_insert_realized(&self, height: usize, date: WNaiveDate) -> bool { + self.subs.realized.needs_insert(height, date) + } + + pub fn needs_insert_unrealized(&self, height: usize, date: WNaiveDate) -> bool { + self.subs.unrealized.needs_insert(height, date) + } + + pub fn needs_insert_input(&self, height: usize, date: WNaiveDate) -> bool { + self.subs.input.needs_insert(height, date) + } + + pub fn compute( + &mut self, + compute_data: &ComputeData, + closes: &mut BiMap<f32>, + circulating_supply: &mut BiMap<f64>, + market_cap: &mut BiMap<f32>, + ) { + if self.subs.supply.should_compute(compute_data) { + self.subs.supply.compute(compute_data, circulating_supply); + } + + if self.subs.unrealized.should_compute(compute_data) { + self.subs.unrealized.compute( + compute_data, + &mut self.subs.supply.supply, + circulating_supply, + market_cap, + ); + } + + if self.subs.realized.should_compute(compute_data) { + self.subs.realized.compute(compute_data, market_cap); + } + + if self.subs.capitalization.should_compute(compute_data) { + self.subs + .capitalization + .compute(compute_data, closes, &mut self.subs.supply.supply); + } + + // if self.subs.output.should_compute(compute_data) { + // self.subs + // .output + // .compute(compute_data, &mut self.subs.supply.total); + // } + } +} + +impl AnyDataset for UTXODataset { + fn get_min_initial_states(&self) -> &MinInitialStates { + &self.min_initial_states + } + + fn to_inserted_height_map_vec(&self) -> Vec<&(dyn AnyHeightMap + Send + Sync)> { + self.subs + .as_vec() + .into_iter() + .flat_map(|d| d.to_inserted_height_map_vec()) + .collect_vec() + } + + fn to_inserted_date_map_vec(&self) -> Vec<&(dyn AnyDateMap + Send + Sync)> { + self.subs + .as_vec() + .into_iter() + .flat_map(|d| d.to_inserted_date_map_vec()) + .collect_vec() + } + + fn to_inserted_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + self.subs + .as_vec() + .into_iter() + .flat_map(|d| d.to_inserted_bi_map_vec()) + .collect_vec() + } + + fn to_inserted_mut_height_map_vec(&mut self) -> Vec<&mut dyn AnyHeightMap> { + self.subs + .as_mut_vec() + .into_iter() + .flat_map(|d| d.to_inserted_mut_height_map_vec()) + .collect_vec() + } + + fn to_inserted_mut_date_map_vec(&mut self) -> Vec<&mut dyn AnyDateMap> { + self.subs + .as_mut_vec() + .into_iter() + .flat_map(|d| d.to_inserted_mut_date_map_vec()) + .collect_vec() + } + + fn to_inserted_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + self.subs + .as_mut_vec() + .into_iter() + .flat_map(|d| d.to_inserted_mut_bi_map_vec()) + .collect_vec() + } + + fn to_computed_height_map_vec(&self) -> Vec<&(dyn AnyHeightMap + Send + Sync)> { + self.subs + .as_vec() + .into_iter() + .flat_map(|d| d.to_computed_height_map_vec()) + .collect_vec() + } + + fn to_computed_date_map_vec(&self) -> Vec<&(dyn AnyDateMap + Send + Sync)> { + self.subs + .as_vec() + .into_iter() + .flat_map(|d| d.to_computed_date_map_vec()) + .collect_vec() + } + + fn to_computed_bi_map_vec(&self) -> Vec<&(dyn AnyBiMap + Send + Sync)> { + self.subs + .as_vec() + .into_iter() + .flat_map(|d| d.to_computed_bi_map_vec()) + .collect_vec() + } + + fn to_computed_mut_height_map_vec(&mut self) -> Vec<&mut dyn AnyHeightMap> { + self.subs + .as_mut_vec() + .into_iter() + .flat_map(|d| d.to_computed_mut_height_map_vec()) + .collect_vec() + } + + fn to_computed_mut_date_map_vec(&mut self) -> Vec<&mut dyn AnyDateMap> { + self.subs + .as_mut_vec() + .into_iter() + .flat_map(|d| d.to_computed_mut_date_map_vec()) + .collect_vec() + } + + fn to_computed_mut_bi_map_vec(&mut self) -> Vec<&mut dyn AnyBiMap> { + self.subs + .as_mut_vec() + .into_iter() + .flat_map(|d| d.to_computed_mut_bi_map_vec()) + .collect_vec() + } +} diff --git a/parser/src/datasets/utxo/mod.rs b/parser/src/datasets/utxo/mod.rs new file mode 100644 index 000000000..730185181 --- /dev/null +++ b/parser/src/datasets/utxo/mod.rs @@ -0,0 +1,162 @@ +mod dataset; + +use allocative::Allocative; +use dataset::*; +use rayon::prelude::*; + +use itertools::Itertools; + +use crate::{ + datasets::AnyDatasets, + states::{SplitByUTXOCohort, UTXOCohortId}, + structs::{BiMap, WNaiveDate}, +}; + +use super::{AnyDataset, ComputeData, InsertData, MinInitialStates}; + +#[derive(Allocative)] +pub struct UTXODatasets { + min_initial_states: MinInitialStates, + + cohorts: SplitByUTXOCohort<UTXODataset>, +} + +impl UTXODatasets { + pub fn import(parent_path: &str) -> color_eyre::Result<Self> { + let mut cohorts = SplitByUTXOCohort::<UTXODataset>::default(); + + cohorts + .as_vec() + .into_par_iter() + .map(|(_, id)| (id, UTXODataset::import(parent_path, id))) + .collect::<Vec<_>>() + .into_iter() + .try_for_each(|(id, dataset)| -> color_eyre::Result<()> { + *cohorts.get_mut(&id) = dataset?; + Ok(()) + })?; + + let mut s = Self { + min_initial_states: MinInitialStates::default(), + + cohorts, + }; + + s.min_initial_states + .consume(MinInitialStates::compute_from_datasets(&s)); + + Ok(s) + } + + pub fn insert(&mut self, insert_data: &InsertData) { + self.cohorts + .as_mut_vec() + .into_iter() + .for_each(|(cohort, _)| cohort.insert(insert_data)) + } + + pub fn needs_durable_states(&self, height: usize, date: WNaiveDate) -> bool { + let needs_insert_utxo = self.needs_insert_utxo(height, date); + let needs_insert_capitalization = self.needs_insert_capitalization(height, date); + let needs_insert_supply = self.needs_insert_supply(height, date); + let needs_one_shot_states = self.needs_one_shot_states(height, date); + + needs_insert_utxo + || needs_insert_capitalization + || needs_insert_supply + || needs_one_shot_states + } + + pub fn needs_one_shot_states(&self, height: usize, date: WNaiveDate) -> bool { + self.needs_insert_price_paid(height, date) || self.needs_insert_unrealized(height, date) + } + + pub fn needs_sent_states(&self, height: usize, date: WNaiveDate) -> bool { + self.needs_insert_input(height, date) || self.needs_insert_realized(height, date) + } + + pub fn needs_insert_utxo(&self, height: usize, date: WNaiveDate) -> bool { + self.as_vec() + .iter() + .any(|(dataset, _)| dataset.needs_insert_utxo(height, date)) + } + + pub fn needs_insert_capitalization(&self, height: usize, date: WNaiveDate) -> bool { + self.as_vec() + .iter() + .any(|(dataset, _)| dataset.needs_insert_capitalization(height, date)) + } + + pub fn needs_insert_supply(&self, height: usize, date: WNaiveDate) -> bool { + self.as_vec() + .iter() + .any(|(dataset, _)| dataset.needs_insert_supply(height, date)) + } + + pub fn needs_insert_price_paid(&self, height: usize, date: WNaiveDate) -> bool { + self.as_vec() + .iter() + .any(|(dataset, _)| dataset.needs_insert_price_paid(height, date)) + } + + pub fn needs_insert_realized(&self, height: usize, date: WNaiveDate) -> bool { + self.as_vec() + .iter() + .any(|(dataset, _)| dataset.needs_insert_realized(height, date)) + } + + pub fn needs_insert_unrealized(&self, height: usize, date: WNaiveDate) -> bool { + self.as_vec() + .iter() + .any(|(dataset, _)| dataset.needs_insert_unrealized(height, date)) + } + + pub fn needs_insert_input(&self, height: usize, date: WNaiveDate) -> bool { + self.as_vec() + .iter() + .any(|(dataset, _)| dataset.needs_insert_input(height, date)) + } + + pub fn compute( + &mut self, + compute_data: &ComputeData, + closes: &mut BiMap<f32>, + circulating_supply: &mut BiMap<f64>, + market_cap: &mut BiMap<f32>, + ) { + self.cohorts + .as_mut_vec() + .into_iter() + .for_each(|(cohort, _)| { + cohort.compute(compute_data, closes, circulating_supply, market_cap) + }) + } + + fn as_vec(&self) -> Vec<(&UTXODataset, UTXOCohortId)> { + self.cohorts.as_vec() + } + + fn as_mut_vec(&mut self) -> Vec<(&mut UTXODataset, UTXOCohortId)> { + self.cohorts.as_mut_vec() + } +} + +impl AnyDatasets for UTXODatasets { + fn get_min_initial_states(&self) -> &MinInitialStates { + &self.min_initial_states + } + + fn to_any_dataset_vec(&self) -> Vec<&(dyn AnyDataset + Send + Sync)> { + self.as_vec() + .into_iter() + .map(|(dataset, _)| dataset as &(dyn AnyDataset + Send + Sync)) + .collect_vec() + } + + fn to_mut_any_dataset_vec(&mut self) -> Vec<&mut dyn AnyDataset> { + self.as_mut_vec() + .into_iter() + .map(|(dataset, _)| dataset as &mut dyn AnyDataset) + .collect_vec() + } +} diff --git a/parser/src/io/binary.rs b/parser/src/io/binary.rs new file mode 100644 index 000000000..abef7e160 --- /dev/null +++ b/parser/src/io/binary.rs @@ -0,0 +1,43 @@ +use std::{ + fmt::Debug, + fs::File, + io::{BufReader, BufWriter}, +}; + +use bincode::{config, decode_from_std_read, encode_into_std_write, Decode, Encode}; + +pub struct Binary; + +impl Binary { + pub fn import<T>(path: &str) -> color_eyre::Result<T> + where + T: Decode, + { + let config = config::standard(); + + let file = File::open(path)?; + + let mut reader = BufReader::new(file); + + let decoded = decode_from_std_read(&mut reader, config)?; + + Ok(decoded) + } + + pub fn export<T>(path: &str, value: &T) -> color_eyre::Result<()> + where + T: Debug + Encode, + { + let config = config::standard(); + + let file = File::create(path).inspect_err(|_| { + dbg!(path, value); + })?; + + let mut writer = BufWriter::new(file); + + encode_into_std_write(value, &mut writer, config)?; + + Ok(()) + } +} diff --git a/parser/src/io/consts.rs b/parser/src/io/consts.rs new file mode 100644 index 000000000..293a07581 --- /dev/null +++ b/parser/src/io/consts.rs @@ -0,0 +1,2 @@ +pub const IMPORTS_FOLDER_PATH: &str = "./imports"; +pub const OUTPUTS_FOLDER_PATH: &str = "./target/outputs"; diff --git a/parser/src/io/json.rs b/parser/src/io/json.rs new file mode 100644 index 000000000..9b9e94f73 --- /dev/null +++ b/parser/src/io/json.rs @@ -0,0 +1,37 @@ +use std::{ + fs::File, + io::{BufReader, BufWriter}, +}; + +use serde::{de::DeserializeOwned, Serialize}; + +pub struct Json; + +impl Json { + pub fn import<T>(path: &str) -> color_eyre::Result<T> + where + T: DeserializeOwned, + { + let file = File::open(path)?; + + let reader = BufReader::new(file); + + Ok(serde_json::from_reader(reader)?) + } + + pub fn export<T>(path: &str, value: &T) -> color_eyre::Result<()> + where + T: Serialize, + { + let file = File::create(path).unwrap_or_else(|_| { + dbg!(&path); + panic!("No such file or directory") + }); + + let mut writer = BufWriter::new(file); + + serde_json::to_writer_pretty(&mut writer, value)?; + + Ok(()) + } +} diff --git a/parser/src/io/mod.rs b/parser/src/io/mod.rs new file mode 100644 index 000000000..a51dac6ee --- /dev/null +++ b/parser/src/io/mod.rs @@ -0,0 +1,11 @@ +mod binary; +mod consts; +mod json; +mod path; +mod serialization; + +pub use binary::*; +pub use consts::*; +pub use json::*; +pub use path::*; +pub use serialization::*; diff --git a/parser/src/io/path.rs b/parser/src/io/path.rs new file mode 100644 index 000000000..f62a03fca --- /dev/null +++ b/parser/src/io/path.rs @@ -0,0 +1,3 @@ +pub fn format_path(path: &str) -> String { + path.replace(['-', '_', ' '], "/") +} diff --git a/parser/src/io/serialization.rs b/parser/src/io/serialization.rs new file mode 100644 index 000000000..57c2d7434 --- /dev/null +++ b/parser/src/io/serialization.rs @@ -0,0 +1,55 @@ +use std::fmt::Debug; + +use allocative::Allocative; +use bincode::{Decode, Encode}; +use serde::{de::DeserializeOwned, Serialize}; + +use crate::io::{Binary, Json}; + +#[derive(PartialEq, PartialOrd, Ord, Eq, Debug, Clone, Copy, Default, Allocative)] +pub enum Serialization { + #[default] + Binary, + Json, +} + +impl Serialization { + pub fn to_extension(&self) -> &str { + match self { + Self::Binary => "bin", + Self::Json => "json", + } + } + + pub fn from_extension(extension: &str) -> Self { + match extension { + "bin" => Self::Binary, + "json" => Self::Json, + _ => panic!("Extension \"{extension}\" isn't supported"), + } + } + + pub fn append_extension(&self, path: &str) -> String { + format!("{path}.{}", self.to_extension()) + } + + pub fn import<T>(&self, path: &str) -> color_eyre::Result<T> + where + T: Debug + DeserializeOwned + Decode, + { + match self { + Serialization::Binary => Binary::import(path), + Serialization::Json => Json::import(path), + } + } + + pub fn export<T>(&self, path: &str, value: &T) -> color_eyre::Result<()> + where + T: Debug + Serialize + Encode, + { + match self { + Serialization::Binary => Binary::export(path, value), + Serialization::Json => Json::export(path, value), + } + } +} diff --git a/parser/src/lib.rs b/parser/src/lib.rs new file mode 100644 index 000000000..e86db4259 --- /dev/null +++ b/parser/src/lib.rs @@ -0,0 +1,21 @@ +mod actions; +mod bitcoin; +mod databases; +mod datasets; +mod io; +mod price; +mod states; +mod structs; +mod utils; + +pub use crate::{ + actions::iter_blocks, + bitcoin::{BitcoinDB, BitcoinDaemon}, + datasets::OHLC, + io::{Binary, Json, Serialization}, + structs::{ + DateMap, HeightMap, SerializedDateMap, SerializedHeightMap, WNaiveDate, + HEIGHT_MAP_CHUNK_SIZE, + }, + utils::log, +}; diff --git a/parser/src/main.rs b/parser/src/main.rs new file mode 100644 index 000000000..0b9ff047c --- /dev/null +++ b/parser/src/main.rs @@ -0,0 +1,41 @@ +use std::{env::args, path::Path}; + +use itertools::Itertools; +use parser::{iter_blocks, log, BitcoinDB, BitcoinDaemon}; + +fn main() -> color_eyre::Result<()> { + let args = args().collect_vec(); + let bitcoin_dir_path = args.get(1).unwrap(); + + color_eyre::install()?; + + let deamon = BitcoinDaemon::new(bitcoin_dir_path); + + loop { + deamon.stop(); + + // Scoped to free bitcoin's lock + let block_count = { + let bitcoin_db = BitcoinDB::new(Path::new(bitcoin_dir_path), true)?; + + // let block_count = 200_000; + let block_count = bitcoin_db.get_block_count(); + + log(&format!("{block_count} blocks found.")); + + iter_blocks(&bitcoin_db, block_count)?; + + block_count + }; + + deamon.start(); + + if deamon.check_if_fully_synced() { + deamon.wait_for_new_block(block_count - 1); + } else { + deamon.wait_sync(); + } + } + + // Ok(()) +} diff --git a/parser/src/price/binance.rs b/parser/src/price/binance.rs new file mode 100644 index 000000000..864383733 --- /dev/null +++ b/parser/src/price/binance.rs @@ -0,0 +1,201 @@ +#![allow(dead_code)] + +use std::{collections::BTreeMap, path::Path}; + +use color_eyre::eyre::ContextCompat; +use itertools::Itertools; +use serde_json::Value; + +use crate::{ + datasets::OHLC, + io::{Json, IMPORTS_FOLDER_PATH}, + structs::WNaiveDate, + utils::{log, retry}, +}; + +pub struct Binance; + +impl Binance { + pub fn read_har_file() -> color_eyre::Result<BTreeMap<u32, OHLC>> { + log("binance: read har file"); + + let path_binance_har = Path::new(IMPORTS_FOLDER_PATH).join("binance.har"); + + let json: BTreeMap<String, Value> = + Json::import(path_binance_har.to_str().unwrap()).unwrap_or_default(); + + Ok(json + .get("log") + .context("Expect object to have log attribute")? + .as_object() + .context("Expect to be an object")? + .get("entries") + .context("Expect object to have entries")? + .as_array() + .context("Expect to be an array")? + .iter() + .filter(|entry| { + entry + .as_object() + .unwrap() + .get("request") + .unwrap() + .as_object() + .unwrap() + .get("url") + .unwrap() + .as_str() + .unwrap() + .contains("/uiKlines") + }) + .flat_map(|entry| { + let response = entry + .as_object() + .unwrap() + .get("response") + .unwrap() + .as_object() + .unwrap(); + + let content = response.get("content").unwrap().as_object().unwrap(); + + let text = content.get("text"); + + if text.is_none() { + return vec![]; + } + + let text = text.unwrap().as_str().unwrap(); + + let arrays: Value = serde_json::from_str(text).unwrap(); + + arrays + .as_array() + .unwrap() + .iter() + .map(|array| { + let array = array.as_array().unwrap(); + + let timestamp = (array.first().unwrap().as_u64().unwrap() / 1000) as u32; + + let get_f32 = |index: usize| { + array + .get(index) + .unwrap() + .as_str() + .unwrap() + .parse::<f32>() + .unwrap() + }; + + ( + timestamp, + OHLC { + open: get_f32(1), + high: get_f32(2), + low: get_f32(3), + close: get_f32(4), + }, + ) + }) + .collect_vec() + }) + .collect::<BTreeMap<_, _>>()) + } + + pub fn fetch_1mn_prices() -> color_eyre::Result<BTreeMap<u32, OHLC>> { + log("binance: fetch 1mn"); + + retry( + || { + let body: Value = reqwest::blocking::get( + "https://api.binance.com/api/v3/uiKlines?symbol=BTCUSDT&interval=1m&limit=1000", + )? + .json()?; + + Ok(body + .as_array() + .context("Expect to be an array")? + .iter() + .map(|value| { + // [timestamp, open, high, low, close, volume, ...] + let array = value.as_array().unwrap(); + + let timestamp = array.first().unwrap().as_u64().unwrap() as u32; + + let get_f32 = |index: usize| { + array + .get(index) + .unwrap() + .as_str() + .unwrap() + .parse::<f32>() + .unwrap() + }; + + ( + timestamp, + OHLC { + open: get_f32(1), + high: get_f32(2), + low: get_f32(3), + close: get_f32(4), + }, + ) + }) + .collect::<BTreeMap<_, _>>()) + }, + 10, + 5, + ) + } + + pub fn fetch_daily_prices() -> color_eyre::Result<BTreeMap<WNaiveDate, OHLC>> { + log("binance: fetch 1d"); + + retry( + || { + let body: Value = reqwest::blocking::get( + "https://api.binance.com/api/v3/uiKlines?symbol=BTCUSDT&interval=1d", + )? + .json()?; + + Ok(body + .as_array() + .context("Expect to be an array")? + .iter() + .map(|value| { + // [timestamp, open, high, low, close, volume, ...] + let array = value.as_array().unwrap(); + + let date = WNaiveDate::from_timestamp( + array.first().unwrap().as_u64().unwrap() as u32 / 1000, + ); + + let get_f32 = |index: usize| { + array + .get(index) + .unwrap() + .as_str() + .unwrap() + .parse::<f32>() + .unwrap() + }; + + ( + date, + OHLC { + open: get_f32(1), + high: get_f32(2), + low: get_f32(3), + close: get_f32(4), + }, + ) + }) + .collect::<BTreeMap<_, _>>()) + }, + 10, + 5, + ) + } +} diff --git a/parser/src/price/kraken.rs b/parser/src/price/kraken.rs new file mode 100644 index 000000000..8f804b8ca --- /dev/null +++ b/parser/src/price/kraken.rs @@ -0,0 +1,124 @@ +use std::collections::BTreeMap; + +use color_eyre::eyre::ContextCompat; +use serde_json::Value; + +use crate::{ + datasets::OHLC, + structs::WNaiveDate, + utils::{log, retry}, +}; + +pub struct Kraken; + +impl Kraken { + pub fn fetch_1mn_prices() -> color_eyre::Result<BTreeMap<u32, OHLC>> { + log("kraken: fetch 1mn"); + + retry( + || { + let body: Value = reqwest::blocking::get( + "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1", + )? + .json()?; + + Ok(body + .as_object() + .context("Expect to be an object")? + .get("result") + .context("Expect object to have result")? + .as_object() + .context("Expect to be an object")? + .get("XXBTZUSD") + .context("Expect to have XXBTZUSD")? + .as_array() + .context("Expect to be an array")? + .iter() + .map(|value| { + let array = value.as_array().unwrap(); + + let timestamp = array.first().unwrap().as_u64().unwrap() as u32; + + let get_f32 = |index: usize| { + array + .get(index) + .unwrap() + .as_str() + .unwrap() + .parse::<f32>() + .unwrap() + }; + + ( + timestamp, + OHLC { + open: get_f32(1), + high: get_f32(2), + low: get_f32(3), + close: get_f32(4), + }, + ) + }) + .collect::<BTreeMap<_, _>>()) + }, + 10, + 5, + ) + } + + pub fn fetch_daily_prices() -> color_eyre::Result<BTreeMap<WNaiveDate, OHLC>> { + log("fetch kraken daily"); + + retry( + || { + let body: Value = reqwest::blocking::get( + "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1440", + )? + .json()?; + + Ok(body + .as_object() + .context("Expect to be an object")? + .get("result") + .context("Expect object to have result")? + .as_object() + .context("Expect to be an object")? + .get("XXBTZUSD") + .context("Expect to have XXBTZUSD")? + .as_array() + .context("Expect to be an array")? + .iter() + .map(|value| { + let array = value.as_array().unwrap(); + + let date = WNaiveDate::from_timestamp( + array.first().unwrap().as_u64().unwrap() as u32, + ); + + let get_f32 = |index: usize| { + array + .get(index) + .unwrap() + .as_str() + .unwrap() + .parse::<f32>() + .unwrap() + }; + + ( + date, + OHLC { + open: get_f32(1), + high: get_f32(2), + low: get_f32(3), + close: get_f32(4), + }, + ) + }) + .collect::<BTreeMap<_, _>>()) + }, + 10, + 5, + ) + } +} diff --git a/parser/src/price/mod.rs b/parser/src/price/mod.rs new file mode 100644 index 000000000..6b8c8c847 --- /dev/null +++ b/parser/src/price/mod.rs @@ -0,0 +1,5 @@ +mod binance; +mod kraken; + +pub use binance::*; +pub use kraken::*; diff --git a/parser/src/states/_trait.rs b/parser/src/states/_trait.rs new file mode 100644 index 000000000..11ee4b5b8 --- /dev/null +++ b/parser/src/states/_trait.rs @@ -0,0 +1,47 @@ +use std::{fmt::Debug, fs, io}; + +use bincode::{Decode, Encode}; + +use crate::io::{Binary, OUTPUTS_FOLDER_PATH}; + +// https://github.com/djkoloski/rust_serialization_benchmark +pub trait AnyState +where + Self: Debug + Encode + Decode, +{ + fn name<'a>() -> &'a str; + + fn create_dir_all() -> color_eyre::Result<(), io::Error> { + fs::create_dir_all(Self::folder_path()) + } + + fn folder_path() -> String { + format!("{OUTPUTS_FOLDER_PATH}/states") + } + + fn full_path() -> String { + let name = Self::name(); + + let folder_path = Self::folder_path(); + + format!("{folder_path}/{name}.bin") + } + + fn reset(&mut self) -> color_eyre::Result<(), io::Error> { + self.clear(); + + fs::remove_file(Self::full_path()) + } + + fn import() -> color_eyre::Result<Self> { + Self::create_dir_all()?; + + Binary::import(&Self::full_path()) + } + + fn export(&self) -> color_eyre::Result<()> { + Binary::export(&Self::full_path(), self) + } + + fn clear(&mut self); +} diff --git a/parser/src/states/cohorts_states/address/cohort_durable_states.rs b/parser/src/states/cohorts_states/address/cohort_durable_states.rs new file mode 100644 index 000000000..7c88952b9 --- /dev/null +++ b/parser/src/states/cohorts_states/address/cohort_durable_states.rs @@ -0,0 +1,411 @@ +use allocative::Allocative; + +use crate::{ + states::{DurableStates, OneShotStates, PriceToValue, UnrealizedState}, + structs::{LiquiditySplitResult, Price, SplitByLiquidity, WAmount}, +}; + +#[derive(Default, Debug, Allocative)] +pub struct AddressCohortDurableStates { + pub address_count: usize, + pub split_durable_states: SplitByLiquidity<DurableStates>, + pub price_to_split_amount: PriceToValue<SplitByLiquidity<WAmount>>, +} + +const ONE_THIRD: f64 = 0.33333333333; + +// TODO: Clean that mess, move to a generic liquidity split and somehow support rest for non floats +impl AddressCohortDurableStates { + #[allow(clippy::too_many_arguments)] + pub fn increment( + &mut self, + amount: WAmount, + utxo_count: usize, + realized_cap: Price, + mean_price_paid: Price, + split_sat_amount_result: &LiquiditySplitResult, + split_utxo_count_result: &LiquiditySplitResult, + split_realized_cap_result: &LiquiditySplitResult, + ) -> color_eyre::Result<()> { + self.address_count += 1; + + self._crement( + amount, + utxo_count, + realized_cap, + mean_price_paid, + split_sat_amount_result, + split_utxo_count_result, + split_realized_cap_result, + true, + ) + } + + #[allow(clippy::too_many_arguments)] + pub fn decrement( + &mut self, + amount: WAmount, + utxo_count: usize, + realized_cap: Price, + mean_price_paid: Price, + split_sat_amount_result: &LiquiditySplitResult, + split_utxo_count_result: &LiquiditySplitResult, + split_realized_cap_result: &LiquiditySplitResult, + ) -> color_eyre::Result<()> { + self.address_count -= 1; + + self._crement( + amount, + utxo_count, + realized_cap, + mean_price_paid, + split_sat_amount_result, + split_utxo_count_result, + split_realized_cap_result, + false, + ) + } + + #[allow(clippy::too_many_arguments)] + pub fn _crement( + &mut self, + amount: WAmount, + utxo_count: usize, + realized_cap: Price, + mean_price_paid: Price, + split_sat_amount_result: &LiquiditySplitResult, + split_utxo_count_result: &LiquiditySplitResult, + split_realized_cap_result: &LiquiditySplitResult, + increment: bool, + ) -> color_eyre::Result<()> { + if increment { + self.split_durable_states + .all + .increment(amount, utxo_count, realized_cap) + } else { + self.split_durable_states + .all + .decrement(amount, utxo_count, realized_cap) + } + .inspect_err(|report| { + dbg!( + report, + "split all failed", + split_sat_amount_result, + split_utxo_count_result + ); + })?; + + let illiquid_amount = split_sat_amount_result.illiquid.trunc(); + let illiquid_amount_rest = split_sat_amount_result.illiquid - illiquid_amount; + let mut illiquid_amount = WAmount::from_sat(illiquid_amount as u64); + let mut illiquid_utxo_count = split_utxo_count_result.illiquid.trunc() as usize; + let illiquid_utxo_count_rest = split_utxo_count_result.illiquid.fract(); + let mut illiquid_realized_cap = + Price::from_cent(split_realized_cap_result.illiquid.trunc() as u64); + let illiquid_realized_cap_rest = split_realized_cap_result.illiquid.fract(); + + let liquid_amount = split_sat_amount_result.liquid.trunc(); + let liquid_amount_rest = split_sat_amount_result.liquid - liquid_amount; + let mut liquid_amount = WAmount::from_sat(liquid_amount as u64); + let mut liquid_utxo_count = split_utxo_count_result.liquid.trunc() as usize; + let liquid_utxo_count_rest = split_utxo_count_result.liquid.fract(); + let mut liquid_realized_cap = + Price::from_cent(split_realized_cap_result.liquid.trunc() as u64); + let liquid_realized_cap_rest = split_realized_cap_result.liquid.fract(); + + let mut highly_liquid_amount = amount - illiquid_amount - liquid_amount; + let mut highly_liquid_utxo_count = utxo_count - illiquid_utxo_count - liquid_utxo_count; + let mut highly_liquid_realized_cap = + realized_cap - illiquid_realized_cap - liquid_realized_cap; + + let amount_diff = amount - illiquid_amount - liquid_amount - highly_liquid_amount; + if amount_diff > WAmount::ZERO { + if illiquid_amount_rest >= ONE_THIRD && illiquid_amount_rest > liquid_amount_rest { + illiquid_amount += amount_diff; + } else if illiquid_amount_rest >= ONE_THIRD { + liquid_amount += amount_diff; + } else { + highly_liquid_amount += amount_diff; + } + } + + let utxo_count_diff = + utxo_count - illiquid_utxo_count - liquid_utxo_count - highly_liquid_utxo_count; + if utxo_count_diff > 0 { + if illiquid_utxo_count_rest >= ONE_THIRD + && illiquid_utxo_count_rest > liquid_utxo_count_rest + { + illiquid_utxo_count += utxo_count_diff; + } else if illiquid_utxo_count_rest >= ONE_THIRD { + liquid_utxo_count += utxo_count_diff; + } else { + highly_liquid_utxo_count += utxo_count_diff; + } + } + + let realized_cap_diff = + realized_cap - illiquid_realized_cap - liquid_realized_cap - highly_liquid_realized_cap; + if realized_cap_diff > Price::ZERO { + if illiquid_realized_cap_rest >= ONE_THIRD + && illiquid_realized_cap_rest > liquid_realized_cap_rest + { + illiquid_realized_cap += realized_cap_diff; + } else if illiquid_realized_cap_rest >= ONE_THIRD { + liquid_realized_cap += realized_cap_diff; + } else { + highly_liquid_realized_cap += realized_cap_diff; + } + } + + let split_amount = SplitByLiquidity { + all: amount, + illiquid: illiquid_amount, + liquid: liquid_amount, + highly_liquid: highly_liquid_amount, + }; + + let split_utxo_count = SplitByLiquidity { + all: utxo_count, + illiquid: illiquid_utxo_count, + liquid: liquid_utxo_count, + highly_liquid: highly_liquid_utxo_count, + }; + + let split_realized_cap = SplitByLiquidity { + all: realized_cap, + illiquid: illiquid_realized_cap, + liquid: liquid_realized_cap, + highly_liquid: highly_liquid_realized_cap, + }; + + if increment { + self.price_to_split_amount + .increment(mean_price_paid, split_amount); + } else { + self.price_to_split_amount + .decrement(mean_price_paid, split_amount) + .inspect_err(|report| { + dbg!( + report, + "cents_to_split_amount decrement", + split_sat_amount_result, + split_utxo_count_result, + split_amount, + split_utxo_count, + split_realized_cap, + ); + })?; + } + + if increment { + self.split_durable_states.illiquid.increment( + illiquid_amount, + illiquid_utxo_count, + illiquid_realized_cap, + ) + } else { + self.split_durable_states.illiquid.decrement( + illiquid_amount, + illiquid_utxo_count, + illiquid_realized_cap, + ) + } + .inspect_err(|report| { + dbg!( + report, + "split illiquid failed", + split_sat_amount_result, + split_utxo_count_result, + split_amount, + split_utxo_count, + split_realized_cap, + ); + })?; + + if increment { + self.split_durable_states.liquid.increment( + liquid_amount, + liquid_utxo_count, + liquid_realized_cap, + ) + } else { + self.split_durable_states.liquid.decrement( + liquid_amount, + liquid_utxo_count, + liquid_realized_cap, + ) + } + .inspect_err(|report| { + dbg!( + report, + "split liquid failed", + split_sat_amount_result, + split_utxo_count_result, + split_amount, + split_utxo_count, + split_realized_cap, + ); + })?; + + if increment { + self.split_durable_states.highly_liquid.increment( + highly_liquid_amount, + highly_liquid_utxo_count, + highly_liquid_realized_cap, + ) + } else { + self.split_durable_states.highly_liquid.decrement( + highly_liquid_amount, + highly_liquid_utxo_count, + highly_liquid_realized_cap, + ) + } + .inspect_err(|report| { + dbg!( + report, + "split highly liquid failed", + split_sat_amount_result, + split_utxo_count_result, + split_amount, + split_utxo_count, + split_realized_cap, + ); + })?; + + Ok(()) + } + + pub fn compute_one_shot_states( + &self, + block_price: Price, + date_price: Option<Price>, + ) -> SplitByLiquidity<OneShotStates> { + let mut one_shot_states: SplitByLiquidity<OneShotStates> = SplitByLiquidity::default(); + + if date_price.is_some() { + one_shot_states + .all + .unrealized_date_state + .replace(UnrealizedState::default()); + one_shot_states + .illiquid + .unrealized_date_state + .replace(UnrealizedState::default()); + one_shot_states + .liquid + .unrealized_date_state + .replace(UnrealizedState::default()); + one_shot_states + .highly_liquid + .unrealized_date_state + .replace(UnrealizedState::default()); + } + + let all_supply = self.split_durable_states.all.supply_state.supply; + let illiquid_supply = self.split_durable_states.illiquid.supply_state.supply; + let liquid_supply = self.split_durable_states.liquid.supply_state.supply; + let highly_liquid_supply = self.split_durable_states.highly_liquid.supply_state.supply; + + let one_shot_states_ref = &mut one_shot_states; + + self.price_to_split_amount.iterate( + SplitByLiquidity { + all: all_supply, + illiquid: illiquid_supply, + liquid: liquid_supply, + highly_liquid: highly_liquid_supply, + }, + |price_paid, split_amount| { + one_shot_states_ref.all.price_paid_state.iterate( + price_paid, + split_amount.all, + all_supply, + ); + one_shot_states_ref.all.unrealized_block_state.iterate( + price_paid, + block_price, + split_amount.all, + ); + if let Some(unrealized_date_state) = + one_shot_states_ref.all.unrealized_date_state.as_mut() + { + unrealized_date_state.iterate( + price_paid, + date_price.unwrap(), + split_amount.all, + ); + } + + if split_amount.illiquid > WAmount::ZERO { + one_shot_states_ref.illiquid.price_paid_state.iterate( + price_paid, + split_amount.illiquid, + illiquid_supply, + ); + one_shot_states_ref.illiquid.unrealized_block_state.iterate( + price_paid, + block_price, + split_amount.illiquid, + ); + if let Some(unrealized_date_state) = + one_shot_states_ref.illiquid.unrealized_date_state.as_mut() + { + unrealized_date_state.iterate( + price_paid, + date_price.unwrap(), + split_amount.illiquid, + ); + } + } + + if split_amount.liquid > WAmount::ZERO { + one_shot_states_ref.liquid.price_paid_state.iterate( + price_paid, + split_amount.liquid, + liquid_supply, + ); + one_shot_states_ref.liquid.unrealized_block_state.iterate( + price_paid, + block_price, + split_amount.liquid, + ); + if let Some(unrealized_date_state) = + one_shot_states_ref.liquid.unrealized_date_state.as_mut() + { + unrealized_date_state.iterate( + price_paid, + date_price.unwrap(), + split_amount.liquid, + ); + } + } + + if split_amount.highly_liquid > WAmount::ZERO { + one_shot_states_ref.highly_liquid.price_paid_state.iterate( + price_paid, + split_amount.highly_liquid, + highly_liquid_supply, + ); + one_shot_states_ref + .highly_liquid + .unrealized_block_state + .iterate(price_paid, block_price, split_amount.highly_liquid); + if let Some(unrealized_date_state) = one_shot_states_ref + .highly_liquid + .unrealized_date_state + .as_mut() + { + unrealized_date_state.iterate( + price_paid, + date_price.unwrap(), + split_amount.highly_liquid, + ); + } + } + }, + ); + + one_shot_states + } +} diff --git a/parser/src/states/cohorts_states/address/cohort_id.rs b/parser/src/states/cohorts_states/address/cohort_id.rs new file mode 100644 index 000000000..09bef7b62 --- /dev/null +++ b/parser/src/states/cohorts_states/address/cohort_id.rs @@ -0,0 +1,68 @@ +use crate::structs::{AddressSize, AddressSplit, AddressType}; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +pub enum AddressCohortId { + All, + + Plankton, + Shrimp, + Crab, + Fish, + Shark, + Whale, + Humpback, + Megalodon, + + P2PK, + P2PKH, + P2SH, + P2WPKH, + P2WSH, + P2TR, +} + +impl AddressCohortId { + pub fn as_name(&self) -> Option<&str> { + match self { + Self::All => None, + + Self::Plankton => Some("plankton"), + Self::Shrimp => Some("shrimp"), + Self::Crab => Some("crab"), + Self::Fish => Some("fish"), + Self::Shark => Some("shark"), + Self::Whale => Some("whale"), + Self::Humpback => Some("humpback"), + Self::Megalodon => Some("megalodon"), + + Self::P2PK => Some("p2pk"), + Self::P2PKH => Some("p2pkh"), + Self::P2SH => Some("p2sh"), + Self::P2WPKH => Some("p2wpkh"), + Self::P2WSH => Some("p2wsh"), + Self::P2TR => Some("p2tr"), + } + } + + pub fn as_split(&self) -> AddressSplit { + match self { + Self::All => AddressSplit::All, + + Self::Plankton => AddressSplit::Size(AddressSize::Plankton), + Self::Shrimp => AddressSplit::Size(AddressSize::Shrimp), + Self::Crab => AddressSplit::Size(AddressSize::Crab), + Self::Fish => AddressSplit::Size(AddressSize::Fish), + Self::Shark => AddressSplit::Size(AddressSize::Shark), + Self::Whale => AddressSplit::Size(AddressSize::Whale), + Self::Humpback => AddressSplit::Size(AddressSize::Humpback), + Self::Megalodon => AddressSplit::Size(AddressSize::Megalodon), + + Self::P2PK => AddressSplit::Type(AddressType::P2PK), + Self::P2PKH => AddressSplit::Type(AddressType::P2PKH), + Self::P2SH => AddressSplit::Type(AddressType::P2SH), + Self::P2WPKH => AddressSplit::Type(AddressType::P2WPKH), + Self::P2WSH => AddressSplit::Type(AddressType::P2WSH), + Self::P2TR => AddressSplit::Type(AddressType::P2TR), + } + } +} diff --git a/parser/src/states/cohorts_states/address/cohorts_durable_states.rs b/parser/src/states/cohorts_states/address/cohorts_durable_states.rs new file mode 100644 index 000000000..19438d4ca --- /dev/null +++ b/parser/src/states/cohorts_states/address/cohorts_durable_states.rs @@ -0,0 +1,143 @@ +use allocative::Allocative; +use color_eyre::eyre::eyre; +use derive_deref::{Deref, DerefMut}; +use rayon::prelude::*; + +use crate::{ + databases::AddressIndexToAddressData, + structs::{AddressData, AddressRealizedData, Price}, +}; + +use super::{AddressCohortDurableStates, AddressCohortsOneShotStates, SplitByAddressCohort}; + +#[derive(Default, Deref, DerefMut, Allocative)] +pub struct AddressCohortsDurableStates(SplitByAddressCohort<AddressCohortDurableStates>); + +impl AddressCohortsDurableStates { + pub fn init(address_index_to_address_data: &mut AddressIndexToAddressData) -> Self { + let mut s = Self::default(); + + // Paralize that, different s could be added together + address_index_to_address_data + .iter(&mut |(_, address_data)| s.increment(address_data).unwrap()); + + s + } + + pub fn iterate( + &mut self, + address_realized_data: &AddressRealizedData, + current_address_data: &AddressData, + ) -> color_eyre::Result<()> { + self.decrement(&address_realized_data.initial_address_data) + .inspect_err(|report| { + dbg!(report); + dbg!(address_realized_data, current_address_data); + dbg!("decrement initial address_data"); + })?; + + self.increment(current_address_data).inspect_err(|report| { + dbg!(report); + dbg!(address_realized_data, current_address_data); + dbg!("increment address_data"); + })?; + + Ok(()) + } + + /// Should always increment using current address data state + fn increment(&mut self, address_data: &AddressData) -> color_eyre::Result<()> { + self._crement(address_data, true) + } + + /// Should always decrement using initial address data state + fn decrement(&mut self, address_data: &AddressData) -> color_eyre::Result<()> { + self._crement(address_data, false) + } + + fn _crement(&mut self, address_data: &AddressData, increment: bool) -> color_eyre::Result<()> { + // No need to either insert or remove if empty + if address_data.is_empty() { + return Ok(()); + } + + let amount = address_data.amount; + let utxo_count = address_data.outputs_len as usize; + let realized_cap = address_data.realized_cap; + + let mean_price_paid = address_data.realized_cap / amount; + + let liquidity_classification = address_data.compute_liquidity_classification(); + + let split_sat_amount = liquidity_classification.split(amount.to_sat() as f64); + let split_utxo_count = liquidity_classification.split(utxo_count as f64); + let split_realized_cap = liquidity_classification.split(utxo_count as f64); + + self.0 + .iterate(address_data, |state: &mut AddressCohortDurableStates| { + if increment { + if let Err(report) = state.increment( + amount, + utxo_count, + realized_cap, + mean_price_paid, + &split_sat_amount, + &split_utxo_count, + &split_realized_cap, + ) { + dbg!( + report.to_string(), + &state, + &address_data, + &liquidity_classification + ); + return Err(eyre!("increment error")); + } + } else if let Err(report) = state.decrement( + amount, + utxo_count, + realized_cap, + mean_price_paid, + &split_sat_amount, + &split_utxo_count, + &split_realized_cap, + ) { + dbg!( + report.to_string(), + &state, + &address_data, + &liquidity_classification + ); + return Err(eyre!("decrement error")); + } + + Ok(()) + })?; + + Ok(()) + } + + pub fn compute_one_shot_states( + &mut self, + block_price: Price, + date_price: Option<Price>, + ) -> AddressCohortsOneShotStates { + let mut one_shot_states = AddressCohortsOneShotStates::default(); + + self.as_vec() + .into_par_iter() + .map(|(states, address_cohort_id)| { + ( + address_cohort_id, + states.compute_one_shot_states(block_price, date_price), + ) + }) + .collect::<Vec<_>>() + .into_iter() + .for_each(|(address_cohort_id, states)| { + *one_shot_states.get_mut_from_id(&address_cohort_id) = states; + }); + + one_shot_states + } +} diff --git a/parser/src/states/cohorts_states/address/cohorts_input_states.rs b/parser/src/states/cohorts_states/address/cohorts_input_states.rs new file mode 100644 index 000000000..1fd03c415 --- /dev/null +++ b/parser/src/states/cohorts_states/address/cohorts_input_states.rs @@ -0,0 +1,48 @@ +use derive_deref::{Deref, DerefMut}; + +use crate::{ + states::InputState, + structs::{AddressRealizedData, LiquidityClassification, SplitByLiquidity, WAmount}, +}; + +use super::SplitByAddressCohort; + +#[derive(Deref, DerefMut, Default)] +pub struct AddressCohortsInputStates(SplitByAddressCohort<SplitByLiquidity<InputState>>); + +impl AddressCohortsInputStates { + pub fn iterate_input( + &mut self, + realized_data: &AddressRealizedData, + liquidity_classification: &LiquidityClassification, + ) -> color_eyre::Result<()> { + let count = realized_data.utxos_destroyed as f64; + let sent = realized_data.sent; + + let split_count = liquidity_classification.split(count); + let split_volume = liquidity_classification.split(sent.to_sat() as f64); + + let iterate = move |state: &mut SplitByLiquidity<InputState>| -> color_eyre::Result<()> { + state.all.iterate(count, sent); + + state.illiquid.iterate( + split_count.illiquid, + WAmount::from_sat(split_volume.illiquid.round() as u64), + ); + + state.liquid.iterate( + split_count.liquid, + WAmount::from_sat(split_volume.liquid.round() as u64), + ); + + state.highly_liquid.iterate( + split_count.highly_liquid, + WAmount::from_sat(split_volume.highly_liquid.round() as u64), + ); + + Ok(()) + }; + + self.iterate(&realized_data.initial_address_data, iterate) + } +} diff --git a/parser/src/states/cohorts_states/address/cohorts_one_shot_states.rs b/parser/src/states/cohorts_states/address/cohorts_one_shot_states.rs new file mode 100644 index 000000000..e18d2042a --- /dev/null +++ b/parser/src/states/cohorts_states/address/cohorts_one_shot_states.rs @@ -0,0 +1,8 @@ +use derive_deref::{Deref, DerefMut}; + +use crate::{states::OneShotStates, structs::SplitByLiquidity}; + +use super::SplitByAddressCohort; + +#[derive(Deref, DerefMut, Default)] +pub struct AddressCohortsOneShotStates(pub SplitByAddressCohort<SplitByLiquidity<OneShotStates>>); diff --git a/parser/src/states/cohorts_states/address/cohorts_output_states.rs b/parser/src/states/cohorts_states/address/cohorts_output_states.rs new file mode 100644 index 000000000..24675e349 --- /dev/null +++ b/parser/src/states/cohorts_states/address/cohorts_output_states.rs @@ -0,0 +1,48 @@ +use derive_deref::{Deref, DerefMut}; + +use crate::{ + states::OutputState, + structs::{AddressRealizedData, LiquidityClassification, SplitByLiquidity, WAmount}, +}; + +use super::SplitByAddressCohort; + +#[derive(Deref, DerefMut, Default)] +pub struct AddressCohortsOutputStates(SplitByAddressCohort<SplitByLiquidity<OutputState>>); + +impl AddressCohortsOutputStates { + pub fn iterate_output( + &mut self, + realized_data: &AddressRealizedData, + liquidity_classification: &LiquidityClassification, + ) -> color_eyre::Result<()> { + let count = realized_data.utxos_created as f64; + let volume = realized_data.received; + + let split_count = liquidity_classification.split(count); + let split_volume = liquidity_classification.split(volume.to_sat() as f64); + + let iterate = move |state: &mut SplitByLiquidity<OutputState>| -> color_eyre::Result<()> { + state.all.iterate(count, volume); + + state.illiquid.iterate( + split_count.illiquid, + WAmount::from_sat(split_volume.illiquid.round() as u64), + ); + + state.liquid.iterate( + split_count.liquid, + WAmount::from_sat(split_volume.liquid.round() as u64), + ); + + state.highly_liquid.iterate( + split_count.highly_liquid, + WAmount::from_sat(split_volume.highly_liquid.round() as u64), + ); + + Ok(()) + }; + + self.iterate(&realized_data.initial_address_data, iterate) + } +} diff --git a/parser/src/states/cohorts_states/address/cohorts_realized_states.rs b/parser/src/states/cohorts_states/address/cohorts_realized_states.rs new file mode 100644 index 000000000..5478fe62b --- /dev/null +++ b/parser/src/states/cohorts_states/address/cohorts_realized_states.rs @@ -0,0 +1,48 @@ +use derive_deref::{Deref, DerefMut}; + +use crate::{ + states::RealizedState, + structs::{AddressRealizedData, LiquidityClassification, Price, SplitByLiquidity}, +}; + +use super::SplitByAddressCohort; + +#[derive(Deref, DerefMut, Default)] +pub struct AddressCohortsRealizedStates(SplitByAddressCohort<SplitByLiquidity<RealizedState>>); + +impl AddressCohortsRealizedStates { + pub fn iterate_realized( + &mut self, + realized_data: &AddressRealizedData, + liquidity_classification: &LiquidityClassification, + ) -> color_eyre::Result<()> { + let profit = realized_data.profit; + let loss = realized_data.loss; + + let split_profit = liquidity_classification.split(profit.to_cent() as f64); + let split_loss = liquidity_classification.split(loss.to_cent() as f64); + + let iterate = move |state: &mut SplitByLiquidity<RealizedState>| -> color_eyre::Result<()> { + state.all.iterate(profit, loss); + + state.illiquid.iterate( + Price::from_cent(split_profit.illiquid as u64), + Price::from_cent(split_loss.illiquid as u64), + ); + + state.liquid.iterate( + Price::from_cent(split_profit.liquid as u64), + Price::from_cent(split_loss.liquid as u64), + ); + + state.highly_liquid.iterate( + Price::from_cent(split_profit.highly_liquid as u64), + Price::from_cent(split_loss.highly_liquid as u64), + ); + + Ok(()) + }; + + self.iterate(&realized_data.initial_address_data, iterate) + } +} diff --git a/parser/src/states/cohorts_states/address/mod.rs b/parser/src/states/cohorts_states/address/mod.rs new file mode 100644 index 000000000..df10a01e1 --- /dev/null +++ b/parser/src/states/cohorts_states/address/mod.rs @@ -0,0 +1,17 @@ +mod cohort_durable_states; +mod cohort_id; +mod cohorts_durable_states; +mod cohorts_input_states; +mod cohorts_one_shot_states; +mod cohorts_output_states; +mod cohorts_realized_states; +mod split_by_address_cohort; + +pub use cohort_durable_states::*; +pub use cohort_id::*; +pub use cohorts_durable_states::*; +pub use cohorts_input_states::*; +pub use cohorts_one_shot_states::*; +pub use cohorts_output_states::*; +pub use cohorts_realized_states::*; +pub use split_by_address_cohort::*; diff --git a/parser/src/states/cohorts_states/address/split_by_address_cohort.rs b/parser/src/states/cohorts_states/address/split_by_address_cohort.rs new file mode 100644 index 000000000..a9e6e0a6a --- /dev/null +++ b/parser/src/states/cohorts_states/address/split_by_address_cohort.rs @@ -0,0 +1,177 @@ +use allocative::Allocative; + +use crate::structs::{AddressData, AddressSize, AddressSplit, AddressType}; + +use super::AddressCohortId; + +#[derive(Default, Allocative)] +pub struct SplitByAddressCohort<T> { + pub all: T, + + pub plankton: T, + pub shrimp: T, + pub crab: T, + pub fish: T, + pub shark: T, + pub whale: T, + pub humpback: T, + pub megalodon: T, + + pub p2pk: T, + pub p2pkh: T, + pub p2sh: T, + pub p2wpkh: T, + pub p2wsh: T, + pub p2tr: T, +} + +impl<T> SplitByAddressCohort<T> { + pub fn get(&self, split: &AddressSplit) -> Option<&T> { + match &split { + AddressSplit::All => Some(&self.all), + + AddressSplit::Type(address_type) => match address_type { + AddressType::P2PK => Some(&self.p2pk), + AddressType::P2PKH => Some(&self.p2pkh), + AddressType::P2SH => Some(&self.p2sh), + AddressType::P2WPKH => Some(&self.p2wpkh), + AddressType::P2WSH => Some(&self.p2wsh), + AddressType::P2TR => Some(&self.p2tr), + AddressType::MultiSig => None, + AddressType::Unknown => None, + AddressType::OpReturn => None, + AddressType::PushOnly => None, + AddressType::Empty => None, + }, + + AddressSplit::Size(address_size) => match address_size { + AddressSize::Plankton => Some(&self.plankton), + AddressSize::Shrimp => Some(&self.shrimp), + AddressSize::Crab => Some(&self.crab), + AddressSize::Fish => Some(&self.fish), + AddressSize::Shark => Some(&self.shark), + AddressSize::Whale => Some(&self.whale), + AddressSize::Humpback => Some(&self.humpback), + AddressSize::Megalodon => Some(&self.megalodon), + AddressSize::Empty => None, + }, + } + } + + pub fn iterate( + &mut self, + address_data: &AddressData, + iterate: impl Fn(&mut T) -> color_eyre::Result<()>, + ) -> color_eyre::Result<()> { + if let Some(state) = self.get_mut_from_split(&AddressSplit::All) { + iterate(state)?; + } + + if let Some(state) = self.get_mut_from_split(&AddressSplit::Type(address_data.address_type)) + { + iterate(state)?; + } + + if let Some(state) = self.get_mut_from_split(&AddressSplit::Size(AddressSize::from_amount( + address_data.amount, + ))) { + iterate(state)?; + } + + Ok(()) + } + + fn get_mut_from_split(&mut self, split: &AddressSplit) -> Option<&mut T> { + match &split { + AddressSplit::All => Some(&mut self.all), + + AddressSplit::Type(address_type) => match address_type { + AddressType::P2PK => Some(&mut self.p2pk), + AddressType::P2PKH => Some(&mut self.p2pkh), + AddressType::P2SH => Some(&mut self.p2sh), + AddressType::P2WPKH => Some(&mut self.p2wpkh), + AddressType::P2WSH => Some(&mut self.p2wsh), + AddressType::P2TR => Some(&mut self.p2tr), + AddressType::MultiSig => None, + AddressType::Unknown => None, + AddressType::OpReturn => None, + AddressType::PushOnly => None, + AddressType::Empty => None, + }, + + AddressSplit::Size(address_size) => match address_size { + AddressSize::Plankton => Some(&mut self.plankton), + AddressSize::Shrimp => Some(&mut self.shrimp), + AddressSize::Crab => Some(&mut self.crab), + AddressSize::Fish => Some(&mut self.fish), + AddressSize::Shark => Some(&mut self.shark), + AddressSize::Whale => Some(&mut self.whale), + AddressSize::Humpback => Some(&mut self.humpback), + AddressSize::Megalodon => Some(&mut self.megalodon), + AddressSize::Empty => None, + }, + } + } + + pub fn get_mut_from_id(&mut self, id: &AddressCohortId) -> &mut T { + match id { + AddressCohortId::All => &mut self.all, + + AddressCohortId::Plankton => &mut self.plankton, + AddressCohortId::Shrimp => &mut self.shrimp, + AddressCohortId::Crab => &mut self.crab, + AddressCohortId::Fish => &mut self.fish, + AddressCohortId::Shark => &mut self.shark, + AddressCohortId::Whale => &mut self.whale, + AddressCohortId::Humpback => &mut self.humpback, + AddressCohortId::Megalodon => &mut self.megalodon, + + AddressCohortId::P2PK => &mut self.p2pk, + AddressCohortId::P2PKH => &mut self.p2pkh, + AddressCohortId::P2SH => &mut self.p2sh, + AddressCohortId::P2WPKH => &mut self.p2wpkh, + AddressCohortId::P2WSH => &mut self.p2wsh, + AddressCohortId::P2TR => &mut self.p2tr, + } + } + + pub fn as_vec(&self) -> Vec<(&T, AddressCohortId)> { + vec![ + (&self.all, AddressCohortId::All), + (&self.plankton, AddressCohortId::Plankton), + (&self.shrimp, AddressCohortId::Shrimp), + (&self.crab, AddressCohortId::Crab), + (&self.fish, AddressCohortId::Fish), + (&self.shark, AddressCohortId::Shark), + (&self.whale, AddressCohortId::Whale), + (&self.humpback, AddressCohortId::Humpback), + (&self.megalodon, AddressCohortId::Megalodon), + (&self.p2pk, AddressCohortId::P2PK), + (&self.p2pkh, AddressCohortId::P2PKH), + (&self.p2sh, AddressCohortId::P2SH), + (&self.p2wpkh, AddressCohortId::P2WPKH), + (&self.p2wsh, AddressCohortId::P2WSH), + (&self.p2tr, AddressCohortId::P2TR), + ] + } + + pub fn as_mut_vec(&mut self) -> Vec<(&mut T, AddressCohortId)> { + vec![ + (&mut self.all, AddressCohortId::All), + (&mut self.plankton, AddressCohortId::Plankton), + (&mut self.shrimp, AddressCohortId::Shrimp), + (&mut self.crab, AddressCohortId::Crab), + (&mut self.fish, AddressCohortId::Fish), + (&mut self.shark, AddressCohortId::Shark), + (&mut self.whale, AddressCohortId::Whale), + (&mut self.humpback, AddressCohortId::Humpback), + (&mut self.megalodon, AddressCohortId::Megalodon), + (&mut self.p2pk, AddressCohortId::P2PK), + (&mut self.p2pkh, AddressCohortId::P2PKH), + (&mut self.p2sh, AddressCohortId::P2SH), + (&mut self.p2wpkh, AddressCohortId::P2WPKH), + (&mut self.p2wsh, AddressCohortId::P2WSH), + (&mut self.p2tr, AddressCohortId::P2TR), + ] + } +} diff --git a/parser/src/states/cohorts_states/any/capitalization_state.rs b/parser/src/states/cohorts_states/any/capitalization_state.rs new file mode 100644 index 000000000..5906f4f9b --- /dev/null +++ b/parser/src/states/cohorts_states/any/capitalization_state.rs @@ -0,0 +1,18 @@ +use allocative::Allocative; + +use crate::structs::Price; + +#[derive(Debug, Default, Allocative)] +pub struct CapitalizationState { + pub realized_cap: Price, +} + +impl CapitalizationState { + pub fn increment(&mut self, realized_cap: Price) { + self.realized_cap += realized_cap; + } + + pub fn decrement(&mut self, realized_cap: Price) { + self.realized_cap -= realized_cap; + } +} diff --git a/parser/src/states/cohorts_states/any/durable_states.rs b/parser/src/states/cohorts_states/any/durable_states.rs new file mode 100644 index 000000000..49800bd9a --- /dev/null +++ b/parser/src/states/cohorts_states/any/durable_states.rs @@ -0,0 +1,55 @@ +use allocative::Allocative; +use color_eyre::eyre::eyre; + +use crate::structs::{Price, WAmount}; + +use super::{CapitalizationState, SupplyState, UTXOState}; + +#[derive(Default, Debug, Allocative)] +pub struct DurableStates { + pub capitalization_state: CapitalizationState, + pub supply_state: SupplyState, + pub utxo_state: UTXOState, +} + +impl DurableStates { + pub fn increment( + &mut self, + amount: WAmount, + utxo_count: usize, + realized_cap: Price, + ) -> color_eyre::Result<()> { + if amount == WAmount::ZERO { + if utxo_count != 0 { + dbg!(amount, utxo_count); + return Err(eyre!("Shouldn't be possible")); + } + } else { + self.capitalization_state.increment(realized_cap); + self.supply_state.increment(amount); + self.utxo_state.increment(utxo_count); + } + + Ok(()) + } + + pub fn decrement( + &mut self, + amount: WAmount, + utxo_count: usize, + realized_cap: Price, + ) -> color_eyre::Result<()> { + if amount == WAmount::ZERO { + if utxo_count != 0 { + dbg!(amount, utxo_count); + unreachable!("Shouldn't be possible") + } + } else { + self.capitalization_state.decrement(realized_cap); + self.supply_state.decrement(amount)?; + self.utxo_state.decrement(utxo_count)?; + } + + Ok(()) + } +} diff --git a/parser/src/states/cohorts_states/any/input_state.rs b/parser/src/states/cohorts_states/any/input_state.rs new file mode 100644 index 000000000..c55b7f83d --- /dev/null +++ b/parser/src/states/cohorts_states/any/input_state.rs @@ -0,0 +1,14 @@ +use crate::structs::WAmount; + +#[derive(Debug, Default)] +pub struct InputState { + pub count: f64, + pub volume: WAmount, +} + +impl InputState { + pub fn iterate(&mut self, count: f64, volume: WAmount) { + self.count += count; + self.volume += volume; + } +} diff --git a/parser/src/states/cohorts_states/any/mod.rs b/parser/src/states/cohorts_states/any/mod.rs new file mode 100644 index 000000000..93787438b --- /dev/null +++ b/parser/src/states/cohorts_states/any/mod.rs @@ -0,0 +1,23 @@ +mod capitalization_state; +mod durable_states; +mod input_state; +mod one_shot_states; +mod output_state; +mod price_paid_state; +mod price_to_value; +mod realized_state; +mod supply_state; +mod unrealized_state; +mod utxo_state; + +pub use capitalization_state::*; +pub use durable_states::*; +pub use input_state::*; +pub use one_shot_states::*; +pub use output_state::*; +pub use price_paid_state::*; +pub use price_to_value::*; +pub use realized_state::*; +pub use supply_state::*; +pub use unrealized_state::*; +pub use utxo_state::*; diff --git a/parser/src/states/cohorts_states/any/one_shot_states.rs b/parser/src/states/cohorts_states/any/one_shot_states.rs new file mode 100644 index 000000000..5fe312ab0 --- /dev/null +++ b/parser/src/states/cohorts_states/any/one_shot_states.rs @@ -0,0 +1,9 @@ +use super::{PricePaidState, UnrealizedState}; + +#[derive(Default)] +pub struct OneShotStates { + pub price_paid_state: PricePaidState, + + pub unrealized_block_state: UnrealizedState, + pub unrealized_date_state: Option<UnrealizedState>, +} diff --git a/parser/src/states/cohorts_states/any/output_state.rs b/parser/src/states/cohorts_states/any/output_state.rs new file mode 100644 index 000000000..910657e4f --- /dev/null +++ b/parser/src/states/cohorts_states/any/output_state.rs @@ -0,0 +1,14 @@ +use crate::structs::WAmount; + +#[derive(Debug, Default)] +pub struct OutputState { + pub count: f64, + pub volume: WAmount, +} + +impl OutputState { + pub fn iterate(&mut self, count: f64, volume: WAmount) { + self.count += count; + self.volume += volume; + } +} diff --git a/parser/src/states/cohorts_states/any/price_paid_state.rs b/parser/src/states/cohorts_states/any/price_paid_state.rs new file mode 100644 index 000000000..a93da4c6d --- /dev/null +++ b/parser/src/states/cohorts_states/any/price_paid_state.rs @@ -0,0 +1,210 @@ +use crate::structs::{Price, WAmount}; + +#[derive(Default, Debug)] +pub struct PricePaidState { + pub pp_05p: Option<Price>, + pub pp_10p: Option<Price>, + pub pp_15p: Option<Price>, + pub pp_20p: Option<Price>, + pub pp_25p: Option<Price>, + pub pp_30p: Option<Price>, + pub pp_35p: Option<Price>, + pub pp_40p: Option<Price>, + pub pp_45p: Option<Price>, + pub pp_median: Option<Price>, + pub pp_55p: Option<Price>, + pub pp_60p: Option<Price>, + pub pp_65p: Option<Price>, + pub pp_70p: Option<Price>, + pub pp_75p: Option<Price>, + pub pp_80p: Option<Price>, + pub pp_85p: Option<Price>, + pub pp_90p: Option<Price>, + pub pp_95p: Option<Price>, + + pub processed_amount: WAmount, +} + +impl PricePaidState { + pub fn iterate(&mut self, price: Price, amount: WAmount, total_supply: WAmount) { + let PricePaidState { + processed_amount, + pp_05p, + pp_10p, + pp_15p, + pp_20p, + pp_25p, + pp_30p, + pp_35p, + pp_40p, + pp_45p, + pp_median, + pp_55p, + pp_60p, + pp_65p, + pp_70p, + pp_75p, + pp_80p, + pp_85p, + pp_90p, + pp_95p, + } = self; + + *processed_amount += amount; + + if pp_95p.is_some() { + return; + } + + let processed_sat_amount = processed_amount.to_sat(); + let total_sat_supply = total_supply.to_sat(); + + if processed_sat_amount >= total_sat_supply * 95 / 100 { + pp_95p.replace(price); + } + + if pp_90p.is_some() { + return; + } + + if processed_sat_amount >= total_sat_supply * 90 / 100 { + pp_90p.replace(price); + } + + if pp_85p.is_some() { + return; + } + + if processed_sat_amount >= total_sat_supply * 85 / 100 { + pp_85p.replace(price); + } + + if pp_80p.is_some() { + return; + } + + if processed_sat_amount >= total_sat_supply * 80 / 100 { + pp_80p.replace(price); + } + + if pp_75p.is_some() { + return; + } + + if processed_sat_amount >= total_sat_supply * 75 / 100 { + pp_75p.replace(price); + } + + if pp_70p.is_some() { + return; + } + + if processed_sat_amount >= total_sat_supply * 70 / 100 { + pp_70p.replace(price); + } + + if pp_65p.is_some() { + return; + } + + if processed_sat_amount >= total_sat_supply * 65 / 100 { + pp_65p.replace(price); + } + + if pp_60p.is_some() { + return; + } + + if processed_sat_amount >= total_sat_supply * 60 / 100 { + pp_60p.replace(price); + } + + if pp_55p.is_some() { + return; + } + + if processed_sat_amount >= total_sat_supply * 55 / 100 { + pp_55p.replace(price); + } + + if pp_median.is_some() { + return; + } + + if processed_sat_amount >= total_sat_supply / 2 { + pp_median.replace(price); + } + + if pp_45p.is_some() { + return; + } + + if processed_sat_amount >= total_sat_supply * 45 / 100 { + pp_45p.replace(price); + } + + if pp_40p.is_some() { + return; + } + + if processed_sat_amount >= total_sat_supply * 40 / 100 { + pp_40p.replace(price); + } + + if pp_35p.is_some() { + return; + } + + if processed_sat_amount >= total_sat_supply * 35 / 100 { + pp_35p.replace(price); + } + + if pp_30p.is_some() { + return; + } + + if processed_sat_amount >= total_sat_supply * 30 / 100 { + pp_30p.replace(price); + } + + if pp_25p.is_some() { + return; + } + + if processed_sat_amount >= total_sat_supply / 4 { + pp_25p.replace(price); + } + + if pp_20p.is_some() { + return; + } + + if processed_sat_amount >= total_sat_supply / 5 { + pp_20p.replace(price); + } + + if pp_15p.is_some() { + return; + } + + if processed_sat_amount >= total_sat_supply * 15 / 100 { + pp_15p.replace(price); + } + + if pp_10p.is_some() { + return; + } + + if processed_sat_amount >= total_sat_supply / 10 { + pp_10p.replace(price); + } + + if pp_05p.is_some() { + return; + } + + if processed_sat_amount >= total_sat_supply / 20 { + pp_05p.replace(price); + } + } +} diff --git a/parser/src/states/cohorts_states/any/price_to_value.rs b/parser/src/states/cohorts_states/any/price_to_value.rs new file mode 100644 index 000000000..cbc94e126 --- /dev/null +++ b/parser/src/states/cohorts_states/any/price_to_value.rs @@ -0,0 +1,123 @@ +use std::{ + collections::BTreeMap, + fmt::Debug, + ops::{AddAssign, SubAssign}, +}; + +use allocative::Allocative; +use color_eyre::eyre::eyre; +use derive_deref::{Deref, DerefMut}; + +use crate::structs::{Price, SplitByLiquidity, WAmount}; + +#[derive(Deref, DerefMut, Default, Debug, Allocative)] +pub struct PriceToValue<T>(BTreeMap<u32, T>); + +impl<T> PriceToValue<T> +where + T: Default + + Debug + + AddAssign + + SubAssign + + CanSubtract + + Default + + Copy + + Clone + + PartialEq + + IsZero, +{ + pub fn increment(&mut self, price: Price, value: T) { + *self.entry(price.to_cent() as u32).or_default() += value; + } + + pub fn decrement(&mut self, price: Price, value: T) -> color_eyre::Result<()> { + let cent = price.to_cent() as u32; + + let delete = { + let self_value = self.get_mut(¢); + + if self_value.is_none() { + dbg!(&self.0, price, value); + return Err(eyre!("self_value is none")); + } + + let self_value = self_value.unwrap(); + + if !self_value.can_subtract(&value) { + dbg!(*self_value, &self.0, price, value); + return Err(eyre!("self value < value")); + } + + *self_value -= value; + + self_value.is_zero()? + }; + + if delete { + self.remove(¢).unwrap(); + } + + Ok(()) + } + + pub fn iterate(&self, supply: T, mut iterate: impl FnMut(Price, T)) { + let mut processed = T::default(); + + self.iter().for_each(|(cent, value)| { + let value = *value; + + processed += value; + + iterate(Price::from_cent(*cent as u64), value) + }); + + if processed != supply { + dbg!(processed, supply); + panic!("processed_amount isn't equal to supply") + } + } +} + +pub trait CanSubtract { + fn can_subtract(&self, other: &Self) -> bool; +} + +impl CanSubtract for WAmount { + fn can_subtract(&self, other: &Self) -> bool { + self >= other + } +} + +impl CanSubtract for SplitByLiquidity<WAmount> { + fn can_subtract(&self, other: &Self) -> bool { + self.all >= other.all + && self.illiquid >= other.illiquid + && self.liquid >= other.liquid + && self.highly_liquid >= other.highly_liquid + } +} + +pub trait IsZero { + fn is_zero(&self) -> color_eyre::Result<bool>; +} + +impl IsZero for WAmount { + fn is_zero(&self) -> color_eyre::Result<bool> { + Ok(*self == WAmount::ZERO) + } +} + +impl IsZero for SplitByLiquidity<WAmount> { + fn is_zero(&self) -> color_eyre::Result<bool> { + if self.all == WAmount::ZERO + && (self.illiquid != WAmount::ZERO + || self.liquid != WAmount::ZERO + || self.highly_liquid != WAmount::ZERO) + { + dbg!(&self); + Err(eyre!("Bad split")) + } else { + Ok(self.all == WAmount::ZERO) + } + } +} diff --git a/parser/src/states/cohorts_states/any/realized_state.rs b/parser/src/states/cohorts_states/any/realized_state.rs new file mode 100644 index 000000000..0b03a8683 --- /dev/null +++ b/parser/src/states/cohorts_states/any/realized_state.rs @@ -0,0 +1,14 @@ +use crate::structs::Price; + +#[derive(Debug, Default)] +pub struct RealizedState { + pub realized_profit: Price, + pub realized_loss: Price, +} + +impl RealizedState { + pub fn iterate(&mut self, realized_profit: Price, realized_loss: Price) { + self.realized_profit += realized_profit; + self.realized_loss += realized_loss; + } +} diff --git a/parser/src/states/cohorts_states/any/supply_state.rs b/parser/src/states/cohorts_states/any/supply_state.rs new file mode 100644 index 000000000..a727a8e2a --- /dev/null +++ b/parser/src/states/cohorts_states/any/supply_state.rs @@ -0,0 +1,27 @@ +use allocative::Allocative; +use color_eyre::eyre::eyre; + +use crate::structs::WAmount; + +#[derive(Debug, Default, Allocative)] +pub struct SupplyState { + pub supply: WAmount, +} + +impl SupplyState { + pub fn increment(&mut self, amount: WAmount) { + self.supply += amount; + } + + pub fn decrement(&mut self, amount: WAmount) -> color_eyre::Result<()> { + if self.supply < amount { + dbg!(self.supply, amount); + + return Err(eyre!("supply smaller than supply")); + } + + self.supply -= amount; + + Ok(()) + } +} diff --git a/parser/src/states/cohorts_states/any/unrealized_state.rs b/parser/src/states/cohorts_states/any/unrealized_state.rs new file mode 100644 index 000000000..106b3607f --- /dev/null +++ b/parser/src/states/cohorts_states/any/unrealized_state.rs @@ -0,0 +1,38 @@ +use std::{cmp::Ordering, ops::Add}; + +use crate::structs::{Price, WAmount}; + +#[derive(Debug, Default)] +pub struct UnrealizedState { + pub supply_in_profit: WAmount, + pub unrealized_profit: Price, + pub unrealized_loss: Price, +} + +impl UnrealizedState { + #[inline] + pub fn iterate(&mut self, price_then: Price, price_now: Price, amount: WAmount) { + match price_then.cmp(&price_now) { + Ordering::Less => { + self.unrealized_profit += (price_now - price_then) * amount; + self.supply_in_profit += amount; + } + Ordering::Greater => { + self.unrealized_loss += (price_then - price_now) * amount; + } + Ordering::Equal => {} + } + } +} + +impl Add<UnrealizedState> for UnrealizedState { + type Output = UnrealizedState; + + fn add(self, other: UnrealizedState) -> UnrealizedState { + UnrealizedState { + supply_in_profit: self.supply_in_profit + other.supply_in_profit, + unrealized_profit: self.unrealized_profit + other.unrealized_profit, + unrealized_loss: self.unrealized_loss + other.unrealized_loss, + } + } +} diff --git a/parser/src/states/cohorts_states/any/utxo_state.rs b/parser/src/states/cohorts_states/any/utxo_state.rs new file mode 100644 index 000000000..47178f106 --- /dev/null +++ b/parser/src/states/cohorts_states/any/utxo_state.rs @@ -0,0 +1,25 @@ +use allocative::Allocative; +use color_eyre::eyre::eyre; + +#[derive(Debug, Default, Allocative)] +pub struct UTXOState { + pub count: usize, +} + +impl UTXOState { + pub fn increment(&mut self, utxo_count: usize) { + self.count += utxo_count; + } + + pub fn decrement(&mut self, utxo_count: usize) -> color_eyre::Result<()> { + if self.count < utxo_count { + dbg!(self.count, utxo_count); + + return Err(eyre!("self.count smaller than utxo_count")); + } + + self.count -= utxo_count; + + Ok(()) + } +} diff --git a/parser/src/states/cohorts_states/mod.rs b/parser/src/states/cohorts_states/mod.rs new file mode 100644 index 000000000..cf4029a2a --- /dev/null +++ b/parser/src/states/cohorts_states/mod.rs @@ -0,0 +1,7 @@ +mod address; +mod any; +mod utxo; + +pub use address::*; +pub use any::*; +pub use utxo::*; diff --git a/parser/src/states/cohorts_states/utxo/cohort_durable_states.rs b/parser/src/states/cohorts_states/utxo/cohort_durable_states.rs new file mode 100644 index 000000000..f6bdfe7f4 --- /dev/null +++ b/parser/src/states/cohorts_states/utxo/cohort_durable_states.rs @@ -0,0 +1,107 @@ +use allocative::Allocative; + +use crate::{ + states::{DurableStates, OneShotStates, PriceToValue, UnrealizedState}, + structs::{Price, WAmount}, +}; + +#[derive(Default, Debug, Allocative)] +pub struct UTXOCohortDurableStates { + pub durable_states: DurableStates, + pub price_to_amount: PriceToValue<WAmount>, +} + +impl UTXOCohortDurableStates { + pub fn increment( + &mut self, + amount: WAmount, + utxo_count: usize, + price: Price, + ) -> color_eyre::Result<()> { + self._crement(amount, utxo_count, price, true) + } + + pub fn decrement( + &mut self, + amount: WAmount, + utxo_count: usize, + price: Price, + ) -> color_eyre::Result<()> { + self._crement(amount, utxo_count, price, false) + } + + pub fn _crement( + &mut self, + amount: WAmount, + utxo_count: usize, + price: Price, + increment: bool, + ) -> color_eyre::Result<()> { + let realized_cap = price * amount; + + if increment { + self.durable_states + .increment(amount, utxo_count, realized_cap) + } else { + self.durable_states + .decrement(amount, utxo_count, realized_cap) + } + .inspect_err(|report| { + dbg!(report, "split all failed", amount, utxo_count); + })?; + + let rounded_price = price.to_significant(); + + if increment { + self.price_to_amount.increment(rounded_price, amount); + } else { + self.price_to_amount + .decrement(rounded_price, amount) + .inspect_err(|report| { + dbg!( + report, + "cents_to_amount decrement failed", + amount, + utxo_count + ); + })?; + } + + Ok(()) + } + + pub fn compute_one_shot_states( + &self, + block_price: Price, + date_price: Option<Price>, + ) -> OneShotStates { + let mut one_shot_states = OneShotStates::default(); + + if date_price.is_some() { + one_shot_states + .unrealized_date_state + .replace(UnrealizedState::default()); + } + + let supply = self.durable_states.supply_state.supply; + + let one_shot_states_ref = &mut one_shot_states; + + self.price_to_amount.iterate(supply, |price_paid, amount| { + one_shot_states_ref + .price_paid_state + .iterate(price_paid, amount, supply); + + one_shot_states_ref + .unrealized_block_state + .iterate(price_paid, block_price, amount); + + if let Some(unrealized_date_state) = one_shot_states_ref.unrealized_date_state.as_mut() + { + unrealized_date_state.iterate(price_paid, date_price.unwrap(), amount); + } + }); + + one_shot_states + } +} diff --git a/parser/src/states/cohorts_states/utxo/cohort_filter.rs b/parser/src/states/cohorts_states/utxo/cohort_filter.rs new file mode 100644 index 000000000..2479080a8 --- /dev/null +++ b/parser/src/states/cohorts_states/utxo/cohort_filter.rs @@ -0,0 +1,32 @@ +pub enum UTXOFilter { + To(u32), + FromTo { from: u32, to: u32 }, + From(u32), + Year(u32), +} + +impl UTXOCheck for UTXOFilter { + fn check(&self, days_old: &u32, year: &u32) -> bool { + match self { + UTXOFilter::From(from) => from <= days_old, + UTXOFilter::To(to) => to > days_old, + UTXOFilter::FromTo { from, to } => from <= days_old && to > days_old, + UTXOFilter::Year(_year) => _year == year, + } + } + + fn check_days_old(&self, days_old: &u32) -> bool { + match self { + UTXOFilter::From(from) => from <= days_old, + UTXOFilter::To(to) => to > days_old, + UTXOFilter::FromTo { from, to } => from <= days_old && to > days_old, + UTXOFilter::Year(_) => unreachable!(), + } + } +} + +pub trait UTXOCheck { + fn check(&self, days_old: &u32, year: &u32) -> bool; + + fn check_days_old(&self, days_old: &u32) -> bool; +} diff --git a/parser/src/states/cohorts_states/utxo/cohort_filters.rs b/parser/src/states/cohorts_states/utxo/cohort_filters.rs new file mode 100644 index 000000000..5ce5da040 --- /dev/null +++ b/parser/src/states/cohorts_states/utxo/cohort_filters.rs @@ -0,0 +1,84 @@ +use super::{SplitByUTXOCohort, UTXOFilter}; + +pub const UTXO_FILTERS: SplitByUTXOCohort<UTXOFilter> = SplitByUTXOCohort { + up_to_1d: UTXOFilter::To(1), + up_to_1w: UTXOFilter::To(7), + up_to_1m: UTXOFilter::To(30), + up_to_2m: UTXOFilter::To(2 * 30), + up_to_3m: UTXOFilter::To(3 * 30), + up_to_4m: UTXOFilter::To(4 * 30), + up_to_5m: UTXOFilter::To(5 * 30), + up_to_6m: UTXOFilter::To(6 * 30), + up_to_1y: UTXOFilter::To(365), + up_to_2y: UTXOFilter::To(2 * 365), + up_to_3y: UTXOFilter::To(3 * 365), + up_to_5y: UTXOFilter::To(5 * 365), + up_to_7y: UTXOFilter::To(7 * 365), + up_to_10y: UTXOFilter::To(10 * 365), + up_to_15y: UTXOFilter::To(15 * 365), + + from_1d_to_1w: UTXOFilter::FromTo { from: 1, to: 7 }, + from_1w_to_1m: UTXOFilter::FromTo { from: 7, to: 30 }, + from_1m_to_3m: UTXOFilter::FromTo { + from: 30, + to: 3 * 30, + }, + from_3m_to_6m: UTXOFilter::FromTo { + from: 3 * 30, + to: 6 * 30, + }, + from_6m_to_1y: UTXOFilter::FromTo { + from: 6 * 30, + to: 365, + }, + from_1y_to_2y: UTXOFilter::FromTo { + from: 365, + to: 2 * 365, + }, + from_2y_to_3y: UTXOFilter::FromTo { + from: 2 * 365, + to: 3 * 365, + }, + from_3y_to_5y: UTXOFilter::FromTo { + from: 3 * 365, + to: 5 * 365, + }, + from_5y_to_7y: UTXOFilter::FromTo { + from: 5 * 365, + to: 7 * 365, + }, + from_7y_to_10y: UTXOFilter::FromTo { + from: 7 * 365, + to: 10 * 365, + }, + from_10y_to_15y: UTXOFilter::FromTo { + from: 10 * 365, + to: 15 * 365, + }, + + from_1y: UTXOFilter::From(365), + from_2y: UTXOFilter::From(2 * 365), + from_4y: UTXOFilter::From(4 * 365), + from_10y: UTXOFilter::From(10 * 365), + from_15y: UTXOFilter::From(15 * 365), + + year_2009: UTXOFilter::Year(2009), + year_2010: UTXOFilter::Year(2010), + year_2011: UTXOFilter::Year(2011), + year_2012: UTXOFilter::Year(2012), + year_2013: UTXOFilter::Year(2013), + year_2014: UTXOFilter::Year(2014), + year_2015: UTXOFilter::Year(2015), + year_2016: UTXOFilter::Year(2016), + year_2017: UTXOFilter::Year(2017), + year_2018: UTXOFilter::Year(2018), + year_2019: UTXOFilter::Year(2019), + year_2020: UTXOFilter::Year(2020), + year_2021: UTXOFilter::Year(2021), + year_2022: UTXOFilter::Year(2022), + year_2023: UTXOFilter::Year(2023), + year_2024: UTXOFilter::Year(2024), + + sth: UTXOFilter::To(155), + lth: UTXOFilter::From(155), +}; diff --git a/parser/src/states/cohorts_states/utxo/cohort_id.rs b/parser/src/states/cohorts_states/utxo/cohort_id.rs new file mode 100644 index 000000000..dbe3dccfa --- /dev/null +++ b/parser/src/states/cohorts_states/utxo/cohort_id.rs @@ -0,0 +1,119 @@ +use allocative::Allocative; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default, Allocative)] +pub enum UTXOCohortId { + #[default] + UpTo1d, + UpTo1w, + UpTo1m, + UpTo2m, + UpTo3m, + UpTo4m, + UpTo5m, + UpTo6m, + UpTo1y, + UpTo2y, + UpTo3y, + UpTo5y, + UpTo7y, + UpTo10y, + UpTo15y, + + From1dTo1w, + From1wTo1m, + From1mTo3m, + From3mTo6m, + From6mTo1y, + From1yTo2y, + From2yTo3y, + From3yTo5y, + From5yTo7y, + From7yTo10y, + From10yTo15y, + + From1y, + From2y, + From4y, + From10y, + From15y, + + Year2009, + Year2010, + Year2011, + Year2012, + Year2013, + Year2014, + Year2015, + Year2016, + Year2017, + Year2018, + Year2019, + Year2020, + Year2021, + Year2022, + Year2023, + Year2024, + + ShortTermHolders, + LongTermHolders, +} + +impl UTXOCohortId { + pub fn name(&self) -> &str { + match self { + Self::UpTo1d => "up_to_1d", + Self::UpTo1w => "up_to_1w", + Self::UpTo1m => "up_to_1m", + Self::UpTo2m => "up_to_2m", + Self::UpTo3m => "up_to_3m", + Self::UpTo4m => "up_to_4m", + Self::UpTo5m => "up_to_5m", + Self::UpTo6m => "up_to_6m", + Self::UpTo1y => "up_to_1y", + Self::UpTo2y => "up_to_2y", + Self::UpTo3y => "up_to_3y", + Self::UpTo5y => "up_to_5y", + Self::UpTo7y => "up_to_7y", + Self::UpTo10y => "up_to_10y", + Self::UpTo15y => "up_to_15y", + + Self::From1dTo1w => "from_1d_to_1w", + Self::From1wTo1m => "from_1w_to_1m", + Self::From1mTo3m => "from_1m_to_3m", + Self::From3mTo6m => "from_3m_to_6m", + Self::From6mTo1y => "from_6m_to_1y", + Self::From1yTo2y => "from_1y_to_2y", + Self::From2yTo3y => "from_2y_to_3y", + Self::From3yTo5y => "from_3y_to_5y", + Self::From5yTo7y => "from_5y_to_7y", + Self::From7yTo10y => "from_7y_to_10y", + Self::From10yTo15y => "from_10y_to_15y", + + Self::From1y => "from_1y", + Self::From2y => "from_2y", + Self::From4y => "from_4y", + Self::From10y => "from_10y", + Self::From15y => "from_15y", + + Self::Year2009 => "year_2009", + Self::Year2010 => "year_2010", + Self::Year2011 => "year_2011", + Self::Year2012 => "year_2012", + Self::Year2013 => "year_2013", + Self::Year2014 => "year_2014", + Self::Year2015 => "year_2015", + Self::Year2016 => "year_2016", + Self::Year2017 => "year_2017", + Self::Year2018 => "year_2018", + Self::Year2019 => "year_2019", + Self::Year2020 => "year_2020", + Self::Year2021 => "year_2021", + Self::Year2022 => "year_2022", + Self::Year2023 => "year_2023", + Self::Year2024 => "year_2024", + + Self::ShortTermHolders => "sth", + Self::LongTermHolders => "lth", + } + } +} diff --git a/parser/src/states/cohorts_states/utxo/cohorts_durable_states.rs b/parser/src/states/cohorts_states/utxo/cohorts_durable_states.rs new file mode 100644 index 000000000..a134064a0 --- /dev/null +++ b/parser/src/states/cohorts_states/utxo/cohorts_durable_states.rs @@ -0,0 +1,154 @@ +use allocative::Allocative; +use chrono::Datelike; +use derive_deref::{Deref, DerefMut}; +use rayon::prelude::*; + +use crate::{ + states::DateDataVec, + structs::{BlockData, Price, SentData, WAmount}, + utils::difference_in_days_between_timestamps, + WNaiveDate, +}; + +use super::{SplitByUTXOCohort, UTXOCohortDurableStates, UTXOCohortsOneShotStates}; + +#[derive(Default, Deref, DerefMut, Allocative)] +pub struct UTXOCohortsDurableStates(SplitByUTXOCohort<UTXOCohortDurableStates>); + +impl UTXOCohortsDurableStates { + pub fn init(date_data_vec: &DateDataVec) -> Self { + let mut s = Self::default(); + + if let Some(last_date_data) = date_data_vec.last() { + let last_block_data = last_date_data.blocks.last().unwrap(); + + date_data_vec.iter().for_each(|date_data| { + let year = date_data.date.year() as u32; + + date_data.blocks.iter().for_each(|block_data| { + let amount = block_data.amount; + let utxo_count = block_data.utxos as usize; + + // No need to either insert or remove if 0 + if amount == WAmount::ZERO { + return; + } + + let increment_days_old = difference_in_days_between_timestamps( + block_data.timestamp, + last_block_data.timestamp, + ); + + s.initial_filtered_apply(&increment_days_old, &year, |state| { + state + .increment(amount, utxo_count, block_data.price) + .unwrap(); + }); + }) + }); + } + + s + } + + pub fn udpate_age_if_needed( + &mut self, + block_data: &BlockData, + last_block_data: &BlockData, + previous_last_block_data: Option<&BlockData>, + ) { + let amount = block_data.amount; + let utxo_count = block_data.utxos as usize; + let price = block_data.price; + + // No need to either insert or remove if 0 + if amount == WAmount::ZERO { + return; + } + + if block_data.height == last_block_data.height { + let year = WNaiveDate::from_timestamp(block_data.timestamp).year() as u32; + + self.initial_filtered_apply(&0, &year, |state| { + state.increment(amount, utxo_count, price).unwrap(); + }) + } else { + let increment_days_old = difference_in_days_between_timestamps( + block_data.timestamp, + last_block_data.timestamp, + ); + + let decrement_days_old = difference_in_days_between_timestamps( + block_data.timestamp, + previous_last_block_data + .unwrap_or_else(|| { + dbg!(block_data, last_block_data, previous_last_block_data); + panic!() + }) + .timestamp, + ); + + if increment_days_old == decrement_days_old { + return; + } + + self.duo_filtered_apply( + &increment_days_old, + &decrement_days_old, + |state| { + state.increment(amount, utxo_count, price).unwrap(); + }, + |state| { + state.decrement(amount, utxo_count, price).unwrap(); + }, + ); + } + } + + pub fn subtract_moved( + &mut self, + block_data: &BlockData, + sent_data: &SentData, + previous_last_block_data: &BlockData, + ) { + let amount = sent_data.volume; + let utxo_count = sent_data.count as usize; + + // No need to either insert or remove if 0 + if amount == WAmount::ZERO { + return; + } + + let days_old = difference_in_days_between_timestamps( + block_data.timestamp, + previous_last_block_data.timestamp, + ); + + let year = WNaiveDate::from_timestamp(block_data.timestamp).year() as u32; + + self.initial_filtered_apply(&days_old, &year, |state| { + state + .decrement(amount, utxo_count, block_data.price) + .unwrap(); + }) + } + + pub fn compute_one_shot_states( + &mut self, + block_price: Price, + date_price: Option<Price>, + ) -> UTXOCohortsOneShotStates { + let mut one_shot_states = UTXOCohortsOneShotStates::default(); + + self.as_vec() + .into_par_iter() + .map(|(states, id)| (states.compute_one_shot_states(block_price, date_price), id)) + .collect::<Vec<_>>() + .into_iter() + .for_each(|(states, id)| { + *one_shot_states.get_mut(&id) = states; + }); + + one_shot_states + } +} diff --git a/parser/src/states/cohorts_states/utxo/cohorts_one_shot_states.rs b/parser/src/states/cohorts_states/utxo/cohorts_one_shot_states.rs new file mode 100644 index 000000000..efd4db6a5 --- /dev/null +++ b/parser/src/states/cohorts_states/utxo/cohorts_one_shot_states.rs @@ -0,0 +1,8 @@ +use derive_deref::{Deref, DerefMut}; + +use crate::states::OneShotStates; + +use super::SplitByUTXOCohort; + +#[derive(Deref, DerefMut, Default)] +pub struct UTXOCohortsOneShotStates(pub SplitByUTXOCohort<OneShotStates>); diff --git a/parser/src/states/cohorts_states/utxo/cohorts_sent_states.rs b/parser/src/states/cohorts_states/utxo/cohorts_sent_states.rs new file mode 100644 index 000000000..3fd27fc33 --- /dev/null +++ b/parser/src/states/cohorts_states/utxo/cohorts_sent_states.rs @@ -0,0 +1,68 @@ +use std::{cmp::Ordering, collections::BTreeMap}; + +use chrono::Datelike; +use derive_deref::{Deref, DerefMut}; + +use crate::{ + states::{DateDataVec, InputState, RealizedState}, + structs::{BlockPath, Price, SentData}, + utils::difference_in_days_between_timestamps, +}; + +use super::SplitByUTXOCohort; + +#[derive(Default, Debug)] +pub struct SentState { + pub input: InputState, + pub realized: RealizedState, +} + +#[derive(Deref, DerefMut, Default)] +pub struct UTXOCohortsSentStates(SplitByUTXOCohort<SentState>); + +impl UTXOCohortsSentStates { + pub fn compute( + &mut self, + date_data_vec: &DateDataVec, + block_path_to_sent_data: &BTreeMap<BlockPath, SentData>, + current_price: Price, + ) { + if let Some(last_block_data) = date_data_vec.last_block() { + block_path_to_sent_data + .iter() + .for_each(|(block_path, sent_data)| { + let date_data = date_data_vec.get_date_data(block_path).unwrap(); + + let year = date_data.date.year() as u32; + + let block_data = date_data.get_block_data(block_path).unwrap(); + + let days_old = difference_in_days_between_timestamps( + block_data.timestamp, + last_block_data.timestamp, + ); + + let previous_price = block_data.price; + + let amount_sent = sent_data.volume; + + self.initial_filtered_apply(&days_old, &year, |state| { + state.input.iterate(sent_data.count as f64, amount_sent); + + let previous_value = previous_price * amount_sent; + let current_value = current_price * amount_sent; + + match previous_value.cmp(¤t_value) { + Ordering::Less => { + state.realized.realized_profit += current_value - previous_value; + } + Ordering::Greater => { + state.realized.realized_loss += previous_value - current_value; + } + Ordering::Equal => {} + } + }) + }) + } + } +} diff --git a/parser/src/states/cohorts_states/utxo/mod.rs b/parser/src/states/cohorts_states/utxo/mod.rs new file mode 100644 index 000000000..654aee28e --- /dev/null +++ b/parser/src/states/cohorts_states/utxo/mod.rs @@ -0,0 +1,17 @@ +mod cohort_durable_states; +mod cohort_filter; +mod cohort_filters; +mod cohort_id; +mod cohorts_durable_states; +mod cohorts_one_shot_states; +mod cohorts_sent_states; +mod split_by_utxo_cohort; + +pub use cohort_durable_states::*; +pub use cohort_filter::*; +pub use cohort_filters::*; +pub use cohort_id::*; +pub use cohorts_durable_states::*; +pub use cohorts_one_shot_states::*; +pub use cohorts_sent_states::*; +pub use split_by_utxo_cohort::*; diff --git a/parser/src/states/cohorts_states/utxo/split_by_utxo_cohort.rs b/parser/src/states/cohorts_states/utxo/split_by_utxo_cohort.rs new file mode 100644 index 000000000..e5054c143 --- /dev/null +++ b/parser/src/states/cohorts_states/utxo/split_by_utxo_cohort.rs @@ -0,0 +1,740 @@ +use allocative::Allocative; + +use super::{UTXOCheck, UTXOCohortId, UTXO_FILTERS}; + +#[derive(Default, Allocative)] +pub struct SplitByUTXOCohort<T> { + pub sth: T, + pub lth: T, + + pub up_to_1d: T, + pub up_to_1w: T, + pub up_to_1m: T, + pub up_to_2m: T, + pub up_to_3m: T, + pub up_to_4m: T, + pub up_to_5m: T, + pub up_to_6m: T, + pub up_to_1y: T, + pub up_to_2y: T, + pub up_to_3y: T, + pub up_to_5y: T, + pub up_to_7y: T, + pub up_to_10y: T, + pub up_to_15y: T, + + pub from_1d_to_1w: T, + pub from_1w_to_1m: T, + pub from_1m_to_3m: T, + pub from_3m_to_6m: T, + pub from_6m_to_1y: T, + pub from_1y_to_2y: T, + pub from_2y_to_3y: T, + pub from_3y_to_5y: T, + pub from_5y_to_7y: T, + pub from_7y_to_10y: T, + pub from_10y_to_15y: T, + + pub from_1y: T, + pub from_2y: T, + pub from_4y: T, + pub from_10y: T, + pub from_15y: T, + + pub year_2009: T, + pub year_2010: T, + pub year_2011: T, + pub year_2012: T, + pub year_2013: T, + pub year_2014: T, + pub year_2015: T, + pub year_2016: T, + pub year_2017: T, + pub year_2018: T, + pub year_2019: T, + pub year_2020: T, + pub year_2021: T, + pub year_2022: T, + pub year_2023: T, + pub year_2024: T, +} + +impl<T> SplitByUTXOCohort<T> { + pub fn get(&self, id: &UTXOCohortId) -> &T { + match id { + UTXOCohortId::UpTo1d => &self.up_to_1d, + UTXOCohortId::UpTo1w => &self.up_to_1w, + UTXOCohortId::UpTo1m => &self.up_to_1m, + UTXOCohortId::UpTo2m => &self.up_to_2m, + UTXOCohortId::UpTo3m => &self.up_to_3m, + UTXOCohortId::UpTo4m => &self.up_to_4m, + UTXOCohortId::UpTo5m => &self.up_to_5m, + UTXOCohortId::UpTo6m => &self.up_to_6m, + UTXOCohortId::UpTo1y => &self.up_to_1y, + UTXOCohortId::UpTo2y => &self.up_to_2y, + UTXOCohortId::UpTo3y => &self.up_to_3y, + UTXOCohortId::UpTo5y => &self.up_to_5y, + UTXOCohortId::UpTo7y => &self.up_to_7y, + UTXOCohortId::UpTo10y => &self.up_to_10y, + UTXOCohortId::UpTo15y => &self.up_to_15y, + UTXOCohortId::From1dTo1w => &self.from_1d_to_1w, + UTXOCohortId::From1wTo1m => &self.from_1w_to_1m, + UTXOCohortId::From1mTo3m => &self.from_1m_to_3m, + UTXOCohortId::From3mTo6m => &self.from_3m_to_6m, + UTXOCohortId::From6mTo1y => &self.from_6m_to_1y, + UTXOCohortId::From1yTo2y => &self.from_1y_to_2y, + UTXOCohortId::From2yTo3y => &self.from_2y_to_3y, + UTXOCohortId::From3yTo5y => &self.from_3y_to_5y, + UTXOCohortId::From5yTo7y => &self.from_5y_to_7y, + UTXOCohortId::From7yTo10y => &self.from_7y_to_10y, + UTXOCohortId::From10yTo15y => &self.from_10y_to_15y, + UTXOCohortId::From1y => &self.from_1y, + UTXOCohortId::From2y => &self.from_2y, + UTXOCohortId::From4y => &self.from_4y, + UTXOCohortId::From10y => &self.from_10y, + UTXOCohortId::From15y => &self.from_15y, + UTXOCohortId::Year2009 => &self.year_2009, + UTXOCohortId::Year2010 => &self.year_2010, + UTXOCohortId::Year2011 => &self.year_2011, + UTXOCohortId::Year2012 => &self.year_2012, + UTXOCohortId::Year2013 => &self.year_2013, + UTXOCohortId::Year2014 => &self.year_2014, + UTXOCohortId::Year2015 => &self.year_2015, + UTXOCohortId::Year2016 => &self.year_2016, + UTXOCohortId::Year2017 => &self.year_2017, + UTXOCohortId::Year2018 => &self.year_2018, + UTXOCohortId::Year2019 => &self.year_2019, + UTXOCohortId::Year2020 => &self.year_2020, + UTXOCohortId::Year2021 => &self.year_2021, + UTXOCohortId::Year2022 => &self.year_2022, + UTXOCohortId::Year2023 => &self.year_2023, + UTXOCohortId::Year2024 => &self.year_2024, + UTXOCohortId::ShortTermHolders => &self.sth, + UTXOCohortId::LongTermHolders => &self.lth, + } + } + + pub fn get_mut(&mut self, id: &UTXOCohortId) -> &mut T { + match id { + UTXOCohortId::UpTo1d => &mut self.up_to_1d, + UTXOCohortId::UpTo1w => &mut self.up_to_1w, + UTXOCohortId::UpTo1m => &mut self.up_to_1m, + UTXOCohortId::UpTo2m => &mut self.up_to_2m, + UTXOCohortId::UpTo3m => &mut self.up_to_3m, + UTXOCohortId::UpTo4m => &mut self.up_to_4m, + UTXOCohortId::UpTo5m => &mut self.up_to_5m, + UTXOCohortId::UpTo6m => &mut self.up_to_6m, + UTXOCohortId::UpTo1y => &mut self.up_to_1y, + UTXOCohortId::UpTo2y => &mut self.up_to_2y, + UTXOCohortId::UpTo3y => &mut self.up_to_3y, + UTXOCohortId::UpTo5y => &mut self.up_to_5y, + UTXOCohortId::UpTo7y => &mut self.up_to_7y, + UTXOCohortId::UpTo10y => &mut self.up_to_10y, + UTXOCohortId::UpTo15y => &mut self.up_to_15y, + UTXOCohortId::From1dTo1w => &mut self.from_1d_to_1w, + UTXOCohortId::From1wTo1m => &mut self.from_1w_to_1m, + UTXOCohortId::From1mTo3m => &mut self.from_1m_to_3m, + UTXOCohortId::From3mTo6m => &mut self.from_3m_to_6m, + UTXOCohortId::From6mTo1y => &mut self.from_6m_to_1y, + UTXOCohortId::From1yTo2y => &mut self.from_1y_to_2y, + UTXOCohortId::From2yTo3y => &mut self.from_2y_to_3y, + UTXOCohortId::From3yTo5y => &mut self.from_3y_to_5y, + UTXOCohortId::From5yTo7y => &mut self.from_5y_to_7y, + UTXOCohortId::From7yTo10y => &mut self.from_7y_to_10y, + UTXOCohortId::From10yTo15y => &mut self.from_10y_to_15y, + UTXOCohortId::From1y => &mut self.from_1y, + UTXOCohortId::From2y => &mut self.from_2y, + UTXOCohortId::From4y => &mut self.from_4y, + UTXOCohortId::From10y => &mut self.from_10y, + UTXOCohortId::From15y => &mut self.from_15y, + UTXOCohortId::Year2009 => &mut self.year_2009, + UTXOCohortId::Year2010 => &mut self.year_2010, + UTXOCohortId::Year2011 => &mut self.year_2011, + UTXOCohortId::Year2012 => &mut self.year_2012, + UTXOCohortId::Year2013 => &mut self.year_2013, + UTXOCohortId::Year2014 => &mut self.year_2014, + UTXOCohortId::Year2015 => &mut self.year_2015, + UTXOCohortId::Year2016 => &mut self.year_2016, + UTXOCohortId::Year2017 => &mut self.year_2017, + UTXOCohortId::Year2018 => &mut self.year_2018, + UTXOCohortId::Year2019 => &mut self.year_2019, + UTXOCohortId::Year2020 => &mut self.year_2020, + UTXOCohortId::Year2021 => &mut self.year_2021, + UTXOCohortId::Year2022 => &mut self.year_2022, + UTXOCohortId::Year2023 => &mut self.year_2023, + UTXOCohortId::Year2024 => &mut self.year_2024, + UTXOCohortId::ShortTermHolders => &mut self.sth, + UTXOCohortId::LongTermHolders => &mut self.lth, + } + } + + /// Excluding years since they're static + pub fn duo_filtered_apply( + &mut self, + current_days_old: &u32, + previous_days_old: &u32, + apply_if_current_only: impl Fn(&mut T), + apply_if_previous_only: impl Fn(&mut T), + ) { + let is_up_to_1d = UTXO_FILTERS.up_to_1d.check_days_old(current_days_old); + let was_up_to_1d = UTXO_FILTERS.up_to_1d.check_days_old(previous_days_old); + if is_up_to_1d && !was_up_to_1d { + apply_if_current_only(&mut self.up_to_1d); + } else if was_up_to_1d && !is_up_to_1d { + apply_if_previous_only(&mut self.up_to_1d); + } + + let is_up_to_1w = UTXO_FILTERS.up_to_1w.check_days_old(current_days_old); + let was_up_to_1w = UTXO_FILTERS.up_to_1w.check_days_old(previous_days_old); + if is_up_to_1w && !was_up_to_1w { + apply_if_current_only(&mut self.up_to_1w); + } else if was_up_to_1w && !is_up_to_1w { + apply_if_previous_only(&mut self.up_to_1w); + } + + let is_up_to_1m = UTXO_FILTERS.up_to_1m.check_days_old(current_days_old); + let was_up_to_1m = UTXO_FILTERS.up_to_1m.check_days_old(previous_days_old); + if is_up_to_1m && !was_up_to_1m { + apply_if_current_only(&mut self.up_to_1m); + } else if was_up_to_1m && !is_up_to_1m { + apply_if_previous_only(&mut self.up_to_1m); + } + + let is_up_to_2m = UTXO_FILTERS.up_to_2m.check_days_old(current_days_old); + let was_up_to_2m = UTXO_FILTERS.up_to_2m.check_days_old(previous_days_old); + if is_up_to_2m && !was_up_to_2m { + apply_if_current_only(&mut self.up_to_2m); + } else if was_up_to_2m && !is_up_to_2m { + apply_if_previous_only(&mut self.up_to_2m); + } + + let is_up_to_3m = UTXO_FILTERS.up_to_3m.check_days_old(current_days_old); + let was_up_to_3m = UTXO_FILTERS.up_to_3m.check_days_old(previous_days_old); + if is_up_to_3m && !was_up_to_3m { + apply_if_current_only(&mut self.up_to_3m); + } else if was_up_to_3m && !is_up_to_3m { + apply_if_previous_only(&mut self.up_to_3m); + } + + let is_up_to_4m = UTXO_FILTERS.up_to_4m.check_days_old(current_days_old); + let was_up_to_4m = UTXO_FILTERS.up_to_4m.check_days_old(previous_days_old); + if is_up_to_4m && !was_up_to_4m { + apply_if_current_only(&mut self.up_to_4m); + } else if was_up_to_4m && !is_up_to_4m { + apply_if_previous_only(&mut self.up_to_4m); + } + + let is_up_to_5m = UTXO_FILTERS.up_to_5m.check_days_old(current_days_old); + let was_up_to_5m = UTXO_FILTERS.up_to_5m.check_days_old(previous_days_old); + if is_up_to_5m && !was_up_to_5m { + apply_if_current_only(&mut self.up_to_5m); + } else if was_up_to_5m && !is_up_to_5m { + apply_if_previous_only(&mut self.up_to_5m); + } + + let is_up_to_6m = UTXO_FILTERS.up_to_6m.check_days_old(current_days_old); + let was_up_to_6m = UTXO_FILTERS.up_to_6m.check_days_old(previous_days_old); + if is_up_to_6m && !was_up_to_6m { + apply_if_current_only(&mut self.up_to_6m); + } else if was_up_to_6m && !is_up_to_6m { + apply_if_previous_only(&mut self.up_to_6m); + } + + let is_up_to_1y = UTXO_FILTERS.up_to_1y.check_days_old(current_days_old); + let was_up_to_1y = UTXO_FILTERS.up_to_1y.check_days_old(previous_days_old); + if is_up_to_1y && !was_up_to_1y { + apply_if_current_only(&mut self.up_to_1y); + } else if was_up_to_1y && !is_up_to_1y { + apply_if_previous_only(&mut self.up_to_1y); + } + + let is_up_to_2y = UTXO_FILTERS.up_to_2y.check_days_old(current_days_old); + let was_up_to_2y = UTXO_FILTERS.up_to_2y.check_days_old(previous_days_old); + if is_up_to_2y && !was_up_to_2y { + apply_if_current_only(&mut self.up_to_2y); + } else if was_up_to_2y && !is_up_to_2y { + apply_if_previous_only(&mut self.up_to_2y); + } + + let is_up_to_3y = UTXO_FILTERS.up_to_3y.check_days_old(current_days_old); + let was_up_to_3y = UTXO_FILTERS.up_to_3y.check_days_old(previous_days_old); + if is_up_to_3y && !was_up_to_3y { + apply_if_current_only(&mut self.up_to_3y); + } else if was_up_to_3y && !is_up_to_3y { + apply_if_previous_only(&mut self.up_to_3y); + } + + let is_up_to_5y = UTXO_FILTERS.up_to_5y.check_days_old(current_days_old); + let was_up_to_5y = UTXO_FILTERS.up_to_5y.check_days_old(previous_days_old); + if is_up_to_5y && !was_up_to_5y { + apply_if_current_only(&mut self.up_to_5y); + } else if was_up_to_5y && !is_up_to_5y { + apply_if_previous_only(&mut self.up_to_5y); + } + + let is_up_to_7y = UTXO_FILTERS.up_to_7y.check_days_old(current_days_old); + let was_up_to_7y = UTXO_FILTERS.up_to_7y.check_days_old(previous_days_old); + if is_up_to_7y && !was_up_to_7y { + apply_if_current_only(&mut self.up_to_7y); + } else if was_up_to_7y && !is_up_to_7y { + apply_if_previous_only(&mut self.up_to_7y); + } + + let is_up_to_10y = UTXO_FILTERS.up_to_10y.check_days_old(current_days_old); + let was_up_to_10y = UTXO_FILTERS.up_to_10y.check_days_old(previous_days_old); + if is_up_to_10y && !was_up_to_10y { + apply_if_current_only(&mut self.up_to_10y); + } else if was_up_to_10y && !is_up_to_10y { + apply_if_previous_only(&mut self.up_to_10y); + } + + let is_up_to_15y = UTXO_FILTERS.up_to_15y.check_days_old(current_days_old); + let was_up_to_15y = UTXO_FILTERS.up_to_15y.check_days_old(previous_days_old); + if is_up_to_15y && !was_up_to_15y { + apply_if_current_only(&mut self.up_to_15y); + } else if was_up_to_15y && !is_up_to_15y { + apply_if_previous_only(&mut self.up_to_15y); + } + + let is_from_1d_to_1w = UTXO_FILTERS.from_1d_to_1w.check_days_old(current_days_old); + let was_from_1d_to_1w = UTXO_FILTERS.from_1d_to_1w.check_days_old(previous_days_old); + if is_from_1d_to_1w && !was_from_1d_to_1w { + apply_if_current_only(&mut self.from_1d_to_1w); + } else if was_from_1d_to_1w && !is_from_1d_to_1w { + apply_if_previous_only(&mut self.from_1d_to_1w); + } + + let is_from_1w_to_1m = UTXO_FILTERS.from_1w_to_1m.check_days_old(current_days_old); + let was_from_1w_to_1m = UTXO_FILTERS.from_1w_to_1m.check_days_old(previous_days_old); + if is_from_1w_to_1m && !was_from_1w_to_1m { + apply_if_current_only(&mut self.from_1w_to_1m); + } else if was_from_1w_to_1m && !is_from_1w_to_1m { + apply_if_previous_only(&mut self.from_1w_to_1m); + } + + let is_from_1m_to_3m = UTXO_FILTERS.from_1m_to_3m.check_days_old(current_days_old); + let was_from_1m_to_3m = UTXO_FILTERS.from_1m_to_3m.check_days_old(previous_days_old); + if is_from_1m_to_3m && !was_from_1m_to_3m { + apply_if_current_only(&mut self.from_1m_to_3m); + } else if was_from_1m_to_3m && !is_from_1m_to_3m { + apply_if_previous_only(&mut self.from_1m_to_3m); + } + + let is_from_3m_to_6m = UTXO_FILTERS.from_3m_to_6m.check_days_old(current_days_old); + let was_from_3m_to_6m = UTXO_FILTERS.from_3m_to_6m.check_days_old(previous_days_old); + if is_from_3m_to_6m && !was_from_3m_to_6m { + apply_if_current_only(&mut self.from_3m_to_6m); + } else if was_from_3m_to_6m && !is_from_3m_to_6m { + apply_if_previous_only(&mut self.from_3m_to_6m); + } + + let is_from_6m_to_1y = UTXO_FILTERS.from_6m_to_1y.check_days_old(current_days_old); + let was_from_6m_to_1y = UTXO_FILTERS.from_6m_to_1y.check_days_old(previous_days_old); + if is_from_6m_to_1y && !was_from_6m_to_1y { + apply_if_current_only(&mut self.from_6m_to_1y); + } else if was_from_6m_to_1y && !is_from_6m_to_1y { + apply_if_previous_only(&mut self.from_6m_to_1y); + } + + let is_from_1y_to_2y = UTXO_FILTERS.from_1y_to_2y.check_days_old(current_days_old); + let was_from_1y_to_2y = UTXO_FILTERS.from_1y_to_2y.check_days_old(previous_days_old); + if is_from_1y_to_2y && !was_from_1y_to_2y { + apply_if_current_only(&mut self.from_1y_to_2y); + } else if was_from_1y_to_2y && !is_from_1y_to_2y { + apply_if_previous_only(&mut self.from_1y_to_2y); + } + + let is_from_2y_to_3y = UTXO_FILTERS.from_2y_to_3y.check_days_old(current_days_old); + let was_from_2y_to_3y = UTXO_FILTERS.from_2y_to_3y.check_days_old(previous_days_old); + if is_from_2y_to_3y && !was_from_2y_to_3y { + apply_if_current_only(&mut self.from_2y_to_3y); + } else if was_from_2y_to_3y && !is_from_2y_to_3y { + apply_if_previous_only(&mut self.from_2y_to_3y); + } + + let is_from_3y_to_5y = UTXO_FILTERS.from_3y_to_5y.check_days_old(current_days_old); + let was_from_3y_to_5y = UTXO_FILTERS.from_3y_to_5y.check_days_old(previous_days_old); + if is_from_3y_to_5y && !was_from_3y_to_5y { + apply_if_current_only(&mut self.from_3y_to_5y); + } else if was_from_3y_to_5y && !is_from_3y_to_5y { + apply_if_previous_only(&mut self.from_3y_to_5y); + } + + let is_from_5y_to_7y = UTXO_FILTERS.from_5y_to_7y.check_days_old(current_days_old); + let was_from_5y_to_7y = UTXO_FILTERS.from_5y_to_7y.check_days_old(previous_days_old); + if is_from_5y_to_7y && !was_from_5y_to_7y { + apply_if_current_only(&mut self.from_5y_to_7y); + } else if was_from_5y_to_7y && !is_from_5y_to_7y { + apply_if_previous_only(&mut self.from_5y_to_7y); + } + + let is_from_7y_to_10y = UTXO_FILTERS.from_7y_to_10y.check_days_old(current_days_old); + let was_from_7y_to_10y = UTXO_FILTERS + .from_7y_to_10y + .check_days_old(previous_days_old); + if is_from_7y_to_10y && !was_from_7y_to_10y { + apply_if_current_only(&mut self.from_7y_to_10y); + } else if was_from_7y_to_10y && !is_from_7y_to_10y { + apply_if_previous_only(&mut self.from_7y_to_10y); + } + + let is_from_10y_to_15y = UTXO_FILTERS + .from_10y_to_15y + .check_days_old(current_days_old); + let was_from_10y_to_15y = UTXO_FILTERS + .from_10y_to_15y + .check_days_old(previous_days_old); + if is_from_10y_to_15y && !was_from_10y_to_15y { + apply_if_current_only(&mut self.from_10y_to_15y); + } else if was_from_10y_to_15y && !is_from_10y_to_15y { + apply_if_previous_only(&mut self.from_10y_to_15y); + } + + let is_from_1y = UTXO_FILTERS.from_1y.check_days_old(current_days_old); + let was_from_1y = UTXO_FILTERS.from_1y.check_days_old(previous_days_old); + if is_from_1y && !was_from_1y { + apply_if_current_only(&mut self.from_1y); + } else if was_from_1y && !is_from_1y { + apply_if_previous_only(&mut self.from_1y); + } + + let is_from_2y = UTXO_FILTERS.from_2y.check_days_old(current_days_old); + let was_from_2y = UTXO_FILTERS.from_2y.check_days_old(previous_days_old); + if is_from_2y && !was_from_2y { + apply_if_current_only(&mut self.from_2y); + } else if was_from_2y && !is_from_2y { + apply_if_previous_only(&mut self.from_2y); + } + + let is_from_4y = UTXO_FILTERS.from_4y.check_days_old(current_days_old); + let was_from_4y = UTXO_FILTERS.from_4y.check_days_old(previous_days_old); + if is_from_4y && !was_from_4y { + apply_if_current_only(&mut self.from_4y); + } else if was_from_4y && !is_from_4y { + apply_if_previous_only(&mut self.from_4y); + } + + let is_from_10y = UTXO_FILTERS.from_10y.check_days_old(current_days_old); + let was_from_10y = UTXO_FILTERS.from_10y.check_days_old(previous_days_old); + if is_from_10y && !was_from_10y { + apply_if_current_only(&mut self.from_10y); + } else if was_from_10y && !is_from_10y { + apply_if_previous_only(&mut self.from_10y); + } + + let is_from_15y = UTXO_FILTERS.from_15y.check_days_old(current_days_old); + let was_from_15y = UTXO_FILTERS.from_15y.check_days_old(previous_days_old); + if is_from_15y && !was_from_15y { + apply_if_current_only(&mut self.from_15y); + } else if was_from_15y && !is_from_15y { + apply_if_previous_only(&mut self.from_15y); + } + + let is_sth = UTXO_FILTERS.sth.check_days_old(current_days_old); + let was_sth = UTXO_FILTERS.sth.check_days_old(previous_days_old); + if is_sth && !was_sth { + apply_if_current_only(&mut self.sth); + } else if was_sth && !is_sth { + apply_if_previous_only(&mut self.sth); + } + + let is_lth = UTXO_FILTERS.lth.check_days_old(current_days_old); + let was_lth = UTXO_FILTERS.lth.check_days_old(previous_days_old); + if is_lth && !was_lth { + if is_sth { + unreachable!() + } + + apply_if_current_only(&mut self.lth); + } else if was_lth && !is_lth { + if was_sth { + unreachable!() + } + // unreachable!(); + apply_if_previous_only(&mut self.lth); + } + } + + /// Includes years since it's the initial apply + pub fn initial_filtered_apply(&mut self, days_old: &u32, year: &u32, apply: impl Fn(&mut T)) { + if UTXO_FILTERS.up_to_1d.check(days_old, year) { + apply(&mut self.up_to_1d); + } else if UTXO_FILTERS.from_1d_to_1w.check(days_old, year) { + apply(&mut self.from_1d_to_1w); + } else if UTXO_FILTERS.from_1w_to_1m.check(days_old, year) { + apply(&mut self.from_1w_to_1m); + } else if UTXO_FILTERS.from_1m_to_3m.check(days_old, year) { + apply(&mut self.from_1m_to_3m); + } else if UTXO_FILTERS.from_3m_to_6m.check(days_old, year) { + apply(&mut self.from_3m_to_6m); + } else if UTXO_FILTERS.from_6m_to_1y.check(days_old, year) { + apply(&mut self.from_6m_to_1y); + } else if UTXO_FILTERS.from_1y_to_2y.check(days_old, year) { + apply(&mut self.from_1y_to_2y); + } else if UTXO_FILTERS.from_2y_to_3y.check(days_old, year) { + apply(&mut self.from_2y_to_3y); + } else if UTXO_FILTERS.from_3y_to_5y.check(days_old, year) { + apply(&mut self.from_3y_to_5y); + } else if UTXO_FILTERS.from_5y_to_7y.check(days_old, year) { + apply(&mut self.from_5y_to_7y); + } else if UTXO_FILTERS.from_7y_to_10y.check(days_old, year) { + apply(&mut self.from_7y_to_10y); + } else if UTXO_FILTERS.from_10y_to_15y.check(days_old, year) { + apply(&mut self.from_10y_to_15y); + } + + if UTXO_FILTERS.year_2009.check(days_old, year) { + apply(&mut self.year_2009); + } else if UTXO_FILTERS.year_2010.check(days_old, year) { + apply(&mut self.year_2010); + } else if UTXO_FILTERS.year_2011.check(days_old, year) { + apply(&mut self.year_2011); + } else if UTXO_FILTERS.year_2012.check(days_old, year) { + apply(&mut self.year_2012); + } else if UTXO_FILTERS.year_2013.check(days_old, year) { + apply(&mut self.year_2013); + } else if UTXO_FILTERS.year_2014.check(days_old, year) { + apply(&mut self.year_2014); + } else if UTXO_FILTERS.year_2015.check(days_old, year) { + apply(&mut self.year_2015); + } else if UTXO_FILTERS.year_2016.check(days_old, year) { + apply(&mut self.year_2016); + } else if UTXO_FILTERS.year_2017.check(days_old, year) { + apply(&mut self.year_2017); + } else if UTXO_FILTERS.year_2018.check(days_old, year) { + apply(&mut self.year_2018); + } else if UTXO_FILTERS.year_2019.check(days_old, year) { + apply(&mut self.year_2019); + } else if UTXO_FILTERS.year_2020.check(days_old, year) { + apply(&mut self.year_2020); + } else if UTXO_FILTERS.year_2021.check(days_old, year) { + apply(&mut self.year_2021); + } else if UTXO_FILTERS.year_2022.check(days_old, year) { + apply(&mut self.year_2022); + } else if UTXO_FILTERS.year_2023.check(days_old, year) { + apply(&mut self.year_2023); + } else if UTXO_FILTERS.year_2024.check(days_old, year) { + apply(&mut self.year_2024); + } + + if UTXO_FILTERS.sth.check(days_old, year) { + apply(&mut self.sth); + } else if UTXO_FILTERS.lth.check(days_old, year) { + apply(&mut self.lth); + } else { + unreachable!() + } + + if UTXO_FILTERS.from_1y.check(days_old, year) { + apply(&mut self.from_1y); + } + + if UTXO_FILTERS.from_2y.check(days_old, year) { + apply(&mut self.from_2y); + } + + if UTXO_FILTERS.from_4y.check(days_old, year) { + apply(&mut self.from_4y); + } + + if UTXO_FILTERS.from_10y.check(days_old, year) { + apply(&mut self.from_10y); + } + + if UTXO_FILTERS.from_15y.check(days_old, year) { + apply(&mut self.from_15y); + } + + if UTXO_FILTERS.up_to_15y.check(days_old, year) { + apply(&mut self.up_to_15y); + } else { + return; + } + + if UTXO_FILTERS.up_to_10y.check(days_old, year) { + apply(&mut self.up_to_10y); + } else { + return; + } + + if UTXO_FILTERS.up_to_7y.check(days_old, year) { + apply(&mut self.up_to_7y); + } else { + return; + } + + if UTXO_FILTERS.up_to_5y.check(days_old, year) { + apply(&mut self.up_to_5y); + } else { + return; + } + + if UTXO_FILTERS.up_to_3y.check(days_old, year) { + apply(&mut self.up_to_3y); + } else { + return; + } + + if UTXO_FILTERS.up_to_2y.check(days_old, year) { + apply(&mut self.up_to_2y); + } else { + return; + } + + if UTXO_FILTERS.up_to_1y.check(days_old, year) { + apply(&mut self.up_to_1y); + } else { + return; + } + + if UTXO_FILTERS.up_to_6m.check(days_old, year) { + apply(&mut self.up_to_6m); + } else { + return; + } + + if UTXO_FILTERS.up_to_5m.check(days_old, year) { + apply(&mut self.up_to_5m); + } else { + return; + } + + if UTXO_FILTERS.up_to_4m.check(days_old, year) { + apply(&mut self.up_to_4m); + } else { + return; + } + + if UTXO_FILTERS.up_to_3m.check(days_old, year) { + apply(&mut self.up_to_3m); + } else { + return; + } + + if UTXO_FILTERS.up_to_2m.check(days_old, year) { + apply(&mut self.up_to_2m); + } else { + return; + } + + if UTXO_FILTERS.up_to_1m.check(days_old, year) { + apply(&mut self.up_to_1m); + } else { + return; + } + + if UTXO_FILTERS.up_to_1w.check(days_old, year) { + apply(&mut self.up_to_1w); + } + } + + #[inline(always)] + pub fn as_vec(&self) -> Vec<(&T, UTXOCohortId)> { + vec![ + (&self.up_to_1d, UTXOCohortId::UpTo1d), + (&self.up_to_1w, UTXOCohortId::UpTo1w), + (&self.up_to_1m, UTXOCohortId::UpTo1m), + (&self.up_to_2m, UTXOCohortId::UpTo2m), + (&self.up_to_3m, UTXOCohortId::UpTo3m), + (&self.up_to_4m, UTXOCohortId::UpTo4m), + (&self.up_to_5m, UTXOCohortId::UpTo5m), + (&self.up_to_6m, UTXOCohortId::UpTo6m), + (&self.up_to_1y, UTXOCohortId::UpTo1y), + (&self.up_to_2y, UTXOCohortId::UpTo2y), + (&self.up_to_3y, UTXOCohortId::UpTo3y), + (&self.up_to_5y, UTXOCohortId::UpTo5y), + (&self.up_to_7y, UTXOCohortId::UpTo7y), + (&self.up_to_10y, UTXOCohortId::UpTo10y), + (&self.up_to_15y, UTXOCohortId::UpTo15y), + (&self.from_1d_to_1w, UTXOCohortId::From1dTo1w), + (&self.from_1w_to_1m, UTXOCohortId::From1wTo1m), + (&self.from_1m_to_3m, UTXOCohortId::From1mTo3m), + (&self.from_3m_to_6m, UTXOCohortId::From3mTo6m), + (&self.from_6m_to_1y, UTXOCohortId::From6mTo1y), + (&self.from_1y_to_2y, UTXOCohortId::From1yTo2y), + (&self.from_2y_to_3y, UTXOCohortId::From2yTo3y), + (&self.from_3y_to_5y, UTXOCohortId::From3yTo5y), + (&self.from_5y_to_7y, UTXOCohortId::From5yTo7y), + (&self.from_7y_to_10y, UTXOCohortId::From7yTo10y), + (&self.from_10y_to_15y, UTXOCohortId::From10yTo15y), + (&self.from_1y, UTXOCohortId::From1y), + (&self.from_2y, UTXOCohortId::From2y), + (&self.from_4y, UTXOCohortId::From4y), + (&self.from_10y, UTXOCohortId::From10y), + (&self.from_15y, UTXOCohortId::From15y), + (&self.year_2009, UTXOCohortId::Year2009), + (&self.year_2010, UTXOCohortId::Year2010), + (&self.year_2011, UTXOCohortId::Year2011), + (&self.year_2012, UTXOCohortId::Year2012), + (&self.year_2013, UTXOCohortId::Year2013), + (&self.year_2014, UTXOCohortId::Year2014), + (&self.year_2015, UTXOCohortId::Year2015), + (&self.year_2016, UTXOCohortId::Year2016), + (&self.year_2017, UTXOCohortId::Year2017), + (&self.year_2018, UTXOCohortId::Year2018), + (&self.year_2019, UTXOCohortId::Year2019), + (&self.year_2020, UTXOCohortId::Year2020), + (&self.year_2021, UTXOCohortId::Year2021), + (&self.year_2022, UTXOCohortId::Year2022), + (&self.year_2023, UTXOCohortId::Year2023), + (&self.year_2024, UTXOCohortId::Year2024), + (&self.sth, UTXOCohortId::ShortTermHolders), + (&self.lth, UTXOCohortId::LongTermHolders), + ] + } + + #[inline(always)] + pub fn as_mut_vec(&mut self) -> Vec<(&mut T, UTXOCohortId)> { + vec![ + (&mut self.up_to_1d, UTXOCohortId::UpTo1d), + (&mut self.up_to_1w, UTXOCohortId::UpTo1w), + (&mut self.up_to_1m, UTXOCohortId::UpTo1m), + (&mut self.up_to_2m, UTXOCohortId::UpTo2m), + (&mut self.up_to_3m, UTXOCohortId::UpTo3m), + (&mut self.up_to_4m, UTXOCohortId::UpTo4m), + (&mut self.up_to_5m, UTXOCohortId::UpTo5m), + (&mut self.up_to_6m, UTXOCohortId::UpTo6m), + (&mut self.up_to_1y, UTXOCohortId::UpTo1y), + (&mut self.up_to_2y, UTXOCohortId::UpTo2y), + (&mut self.up_to_3y, UTXOCohortId::UpTo3y), + (&mut self.up_to_5y, UTXOCohortId::UpTo5y), + (&mut self.up_to_7y, UTXOCohortId::UpTo7y), + (&mut self.up_to_10y, UTXOCohortId::UpTo10y), + (&mut self.up_to_15y, UTXOCohortId::UpTo15y), + (&mut self.from_1d_to_1w, UTXOCohortId::From1dTo1w), + (&mut self.from_1w_to_1m, UTXOCohortId::From1wTo1m), + (&mut self.from_1m_to_3m, UTXOCohortId::From1mTo3m), + (&mut self.from_3m_to_6m, UTXOCohortId::From3mTo6m), + (&mut self.from_6m_to_1y, UTXOCohortId::From6mTo1y), + (&mut self.from_1y_to_2y, UTXOCohortId::From1yTo2y), + (&mut self.from_2y_to_3y, UTXOCohortId::From2yTo3y), + (&mut self.from_3y_to_5y, UTXOCohortId::From3yTo5y), + (&mut self.from_5y_to_7y, UTXOCohortId::From5yTo7y), + (&mut self.from_7y_to_10y, UTXOCohortId::From7yTo10y), + (&mut self.from_10y_to_15y, UTXOCohortId::From10yTo15y), + (&mut self.from_1y, UTXOCohortId::From1y), + (&mut self.from_2y, UTXOCohortId::From2y), + (&mut self.from_4y, UTXOCohortId::From4y), + (&mut self.from_10y, UTXOCohortId::From10y), + (&mut self.from_15y, UTXOCohortId::From15y), + (&mut self.year_2009, UTXOCohortId::Year2009), + (&mut self.year_2010, UTXOCohortId::Year2010), + (&mut self.year_2011, UTXOCohortId::Year2011), + (&mut self.year_2012, UTXOCohortId::Year2012), + (&mut self.year_2013, UTXOCohortId::Year2013), + (&mut self.year_2014, UTXOCohortId::Year2014), + (&mut self.year_2015, UTXOCohortId::Year2015), + (&mut self.year_2016, UTXOCohortId::Year2016), + (&mut self.year_2017, UTXOCohortId::Year2017), + (&mut self.year_2018, UTXOCohortId::Year2018), + (&mut self.year_2019, UTXOCohortId::Year2019), + (&mut self.year_2020, UTXOCohortId::Year2020), + (&mut self.year_2021, UTXOCohortId::Year2021), + (&mut self.year_2022, UTXOCohortId::Year2022), + (&mut self.year_2023, UTXOCohortId::Year2023), + (&mut self.year_2024, UTXOCohortId::Year2024), + (&mut self.sth, UTXOCohortId::ShortTermHolders), + (&mut self.lth, UTXOCohortId::LongTermHolders), + ] + } +} diff --git a/parser/src/states/counters.rs b/parser/src/states/counters.rs new file mode 100644 index 000000000..5494d3fa9 --- /dev/null +++ b/parser/src/states/counters.rs @@ -0,0 +1,29 @@ +use allocative::Allocative; +use bincode::{Decode, Encode}; + +use crate::structs::Counter; + +use super::AnyState; + +#[derive(Default, Debug, Encode, Decode, Allocative)] +pub struct Counters { + pub op_return_addresses: Counter, + pub push_only_addresses: Counter, + pub unknown_addresses: Counter, + pub empty_addresses: Counter, +} + +impl Counters {} + +impl AnyState for Counters { + fn name<'a>() -> &'a str { + "counters" + } + + fn clear(&mut self) { + self.op_return_addresses.reset(); + self.push_only_addresses.reset(); + self.unknown_addresses.reset(); + self.empty_addresses.reset(); + } +} diff --git a/parser/src/states/date_data_vec.rs b/parser/src/states/date_data_vec.rs new file mode 100644 index 000000000..655142194 --- /dev/null +++ b/parser/src/states/date_data_vec.rs @@ -0,0 +1,48 @@ +use allocative::Allocative; +use bincode::{Decode, Encode}; +use derive_deref::{Deref, DerefMut}; + +use crate::structs::{BlockData, BlockPath, DateData}; + +use super::AnyState; + +#[derive(Default, Deref, DerefMut, Debug, Encode, Decode, Allocative)] +pub struct DateDataVec(Vec<DateData>); + +impl DateDataVec { + pub fn last_block(&self) -> Option<&BlockData> { + self.last().and_then(|date_data| date_data.blocks.last()) + } + + pub fn last_mut_block(&mut self) -> Option<&mut BlockData> { + self.last_mut() + .and_then(|date_data| date_data.blocks.last_mut()) + } + + pub fn second_last_block(&self) -> Option<&BlockData> { + self.iter() + .flat_map(|date_data| &date_data.blocks) + .rev() + .nth(1) + } + + pub fn get_date_data(&self, block_path: &BlockPath) -> Option<&DateData> { + self.0.get(block_path.date_index as usize) + } + + pub fn get_block_data(&self, block_path: &BlockPath) -> Option<&BlockData> { + self.0 + .get(block_path.date_index as usize) + .and_then(|date_data| date_data.blocks.get(block_path.block_index as usize)) + } +} + +impl AnyState for DateDataVec { + fn name<'a>() -> &'a str { + "date_data_vec" + } + + fn clear(&mut self) { + self.0.clear(); + } +} diff --git a/parser/src/states/mod.rs b/parser/src/states/mod.rs new file mode 100644 index 000000000..f6b41bd14 --- /dev/null +++ b/parser/src/states/mod.rs @@ -0,0 +1,95 @@ +use std::thread; + +mod _trait; +mod cohorts_states; +mod counters; +mod date_data_vec; + +pub use _trait::*; + +use allocative::Allocative; +pub use cohorts_states::*; +use counters::*; +use date_data_vec::*; + +use crate::{databases::AddressIndexToAddressData, datasets::AllDatasets, utils::log}; + +#[derive(Default, Allocative)] +pub struct States { + pub address_counters: Counters, + pub date_data_vec: DateDataVec, + pub address_cohorts_durable_states: AddressCohortsDurableStates, + pub utxo_cohorts_durable_states: UTXOCohortsDurableStates, +} + +impl States { + pub fn import( + address_index_to_address_data: &mut AddressIndexToAddressData, + datasets: &AllDatasets, + ) -> color_eyre::Result<Self> { + let date_data_vec_handle = thread::spawn(DateDataVec::import); + + let address_counters = Counters::import()?; + + let date_data_vec = date_data_vec_handle.join().unwrap()?; + + // TODO: + // For both address and utxo check if any of these datasets have a None min + // If so use default state otherwise init + // unrealized + // price_paid + // capitalization + // supply + // utxo + + let mut address_cohorts_durable_states = AddressCohortsDurableStates::default(); + + let mut utxo_cohorts_durable_states = UTXOCohortsDurableStates::default(); + + if let Some(first_date_data) = date_data_vec.first() { + if let Some(first_block_data) = first_date_data.blocks.first() { + let first_height = first_block_data.height as usize; + let first_date = first_date_data.date; + + // TODO: Do the same for addresses + address_cohorts_durable_states = + AddressCohortsDurableStates::init(address_index_to_address_data); + + if !datasets.utxo.needs_durable_states(first_height, first_date) { + utxo_cohorts_durable_states = UTXOCohortsDurableStates::init(&date_data_vec); + } + } + } + + Ok(Self { + address_cohorts_durable_states, + address_counters, + date_data_vec, + utxo_cohorts_durable_states, + }) + } + + pub fn reset(&mut self, include_addresses: bool) { + log("Reseting all states..."); + + let _ = self.date_data_vec.reset(); + + self.utxo_cohorts_durable_states = UTXOCohortsDurableStates::default(); + + // TODO: Check that they are ONLY computed in an `if include_addresses` + if include_addresses { + let _ = self.address_counters.reset(); + + self.address_cohorts_durable_states = AddressCohortsDurableStates::default(); + } + } + + pub fn export(&self) -> color_eyre::Result<()> { + thread::scope(|s| { + s.spawn(|| self.address_counters.export().unwrap()); + s.spawn(|| self.date_data_vec.export().unwrap()); + }); + + Ok(()) + } +} diff --git a/parser/src/structs/address.rs b/parser/src/structs/address.rs new file mode 100644 index 000000000..6d2603524 --- /dev/null +++ b/parser/src/structs/address.rs @@ -0,0 +1,133 @@ +use bitcoin::TxOut; +use bitcoin_hashes::{hash160, Hash}; +use itertools::Itertools; + +use crate::{ + bitcoin::multisig_addresses, + databases::{U8x19, U8x31, SANAKIRJA_MAX_KEY_SIZE}, +}; + +use super::{AddressType, Counter}; + +#[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq)] +pub enum Address { + // https://mempool.space/tx/7bd54def72825008b4ca0f4aeff13e6be2c5fe0f23430629a9d484a1ac2a29b8 + Empty(u32), + OpReturn(u32), + PushOnly(u32), + Unknown(u32), + // https://mempool.space/tx/274f8be3b7b9b1a220285f5f71f61e2691dd04df9d69bb02a8b3b85f91fb1857 + MultiSig(Box<[u8]>), + P2PK((u16, U8x19)), + P2PKH((u16, U8x19)), + P2SH((u16, U8x19)), + P2WPKH((u16, U8x19)), + P2WSH((u16, U8x31)), + P2TR((u16, U8x31)), +} + +impl Address { + pub fn to_type(&self) -> AddressType { + match self { + Self::Empty(_) => AddressType::Empty, + Self::OpReturn(_) => AddressType::OpReturn, + Self::PushOnly(_) => AddressType::PushOnly, + Self::Unknown(_) => AddressType::Unknown, + Self::MultiSig(_) => AddressType::MultiSig, + Self::P2PK(_) => AddressType::P2PK, + Self::P2PKH(_) => AddressType::P2PKH, + Self::P2SH(_) => AddressType::P2SH, + Self::P2WPKH(_) => AddressType::P2WPKH, + Self::P2WSH(_) => AddressType::P2WSH, + Self::P2TR(_) => AddressType::P2TR, + } + } + + pub fn from( + txout: &TxOut, + op_return_addresses: &mut Counter, + push_only_addresses: &mut Counter, + unknown_addresses: &mut Counter, + empty_addresses: &mut Counter, + ) -> Self { + let script = &txout.script_pubkey; + + if script.is_p2pk() { + let pk = match script.as_bytes().len() { + 67 => &script.as_bytes()[1..66], + 35 => &script.as_bytes()[1..34], + _ => unreachable!(), + }; + + let hash = hash160::Hash::hash(pk); + + let (prefix, rest) = Self::split_slice(&hash[..]); + + Self::P2PK((prefix, rest.into())) + } else if script.is_p2pkh() { + let (prefix, rest) = Self::split_slice(&script.as_bytes()[3..23]); + Self::P2PKH((prefix, rest.into())) + } else if script.is_p2sh() { + let (prefix, rest) = Self::split_slice(&script.as_bytes()[2..22]); + Self::P2SH((prefix, rest.into())) + } else if script.is_p2wpkh() { + let (prefix, rest) = Self::split_slice(&script.as_bytes()[2..]); + Self::P2WPKH((prefix, rest.into())) + } else if script.is_p2wsh() { + let (prefix, rest) = Self::split_slice(&script.as_bytes()[2..]); + Self::P2WSH((prefix, rest.into())) + } else if script.is_p2tr() { + let (prefix, rest) = Self::split_slice(&script.as_bytes()[2..]); + Self::P2TR((prefix, rest.into())) + } else if script.is_empty() { + let index = empty_addresses.inner(); + + empty_addresses.increment(); + + Self::Empty(index) + } else if script.is_op_return() { + let index = op_return_addresses.inner(); + + op_return_addresses.increment(); + + Self::OpReturn(index) + } else if script.is_multisig() { + let vec = multisig_addresses(script); + + if vec.is_empty() { + dbg!(txout); + panic!("Multisig addresses cannot be empty !"); + } + + let mut vec = vec.into_iter().sorted_unstable().concat(); + + // TODO: Terrible! Store everything instead of only the 510 first bytes but how + // Sanakirja key limit is [u8; 510] and some multisig transactions have 999 keys + if vec.len() > SANAKIRJA_MAX_KEY_SIZE { + vec = vec.drain(..SANAKIRJA_MAX_KEY_SIZE).collect_vec(); + } + + Self::MultiSig(vec.into()) + } else if script.is_push_only() { + let index = push_only_addresses.inner(); + + push_only_addresses.increment(); + + Self::PushOnly(index) + } else { + Self::new_unknown(unknown_addresses) + } + } + + fn new_unknown(unknown_addresses: &mut Counter) -> Address { + let index = unknown_addresses.inner(); + unknown_addresses.increment(); + Self::Unknown(index) + } + + fn split_slice(slice: &[u8]) -> (u16, &[u8]) { + let prefix = ((slice[0] as u16) << 2) + ((slice[1] as u16) >> 6); + let rest = &slice[1..]; + (prefix, rest) + } +} diff --git a/parser/src/structs/address_data.rs b/parser/src/structs/address_data.rs new file mode 100644 index 000000000..86cb71ab4 --- /dev/null +++ b/parser/src/structs/address_data.rs @@ -0,0 +1,112 @@ +use allocative::Allocative; +use color_eyre::eyre::eyre; +use sanakirja::{direct_repr, Storable, UnsizedStorable}; + +use super::{AddressType, EmptyAddressData, LiquidityClassification, Price, WAmount}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Allocative)] +pub struct AddressData { + pub address_type: AddressType, + pub amount: WAmount, + pub sent: WAmount, + pub received: WAmount, + pub realized_cap: Price, + pub outputs_len: u32, +} +direct_repr!(AddressData); + +impl AddressData { + pub fn new(address_type: AddressType) -> Self { + Self { + address_type, + amount: WAmount::ZERO, + sent: WAmount::ZERO, + received: WAmount::ZERO, + realized_cap: Price::ZERO, + outputs_len: 0, + } + } + + pub fn receive(&mut self, amount: WAmount, price: Price) { + let previous_amount = self.amount; + + let new_amount = previous_amount + amount; + + self.amount = new_amount; + + self.received += amount; + + self.outputs_len += 1; + + let received_value = price * amount; + + self.realized_cap += received_value; + } + + pub fn send( + &mut self, + amount: WAmount, + current_price: Price, + sent_amount_price: Price, + ) -> color_eyre::Result<ProfitOrLoss> { + let previous_amount = self.amount; + + if previous_amount < amount { + return Err(eyre!("previous_amount smaller than sent amount")); + } + + let new_amount = previous_amount - amount; + + self.amount = new_amount; + + self.sent += amount; + + self.outputs_len -= 1; + + let previous_sent_dollar_value = sent_amount_price * amount; + self.realized_cap -= previous_sent_dollar_value; + + let current_sent_dollar_value = current_price * amount; + + let profit_or_loss = if current_sent_dollar_value >= previous_sent_dollar_value { + ProfitOrLoss::Profit(current_sent_dollar_value - previous_sent_dollar_value) + } else { + ProfitOrLoss::Loss(previous_sent_dollar_value - current_sent_dollar_value) + }; + + Ok(profit_or_loss) + } + + #[inline(always)] + pub fn is_empty(&self) -> bool { + if self.amount == WAmount::ZERO { + if self.outputs_len != 0 { + unreachable!(); + } + + true + } else { + false + } + } + + pub fn from_empty(empty: &EmptyAddressData) -> Self { + Self { + address_type: empty.address_type, + amount: WAmount::ZERO, + sent: empty.transfered, + received: empty.transfered, + realized_cap: Price::ZERO, + outputs_len: 0, + } + } + + pub fn compute_liquidity_classification(&self) -> LiquidityClassification { + LiquidityClassification::new(self.sent, self.received) + } +} + +pub enum ProfitOrLoss { + Profit(Price), + Loss(Price), +} diff --git a/parser/src/structs/address_realized_data.rs b/parser/src/structs/address_realized_data.rs new file mode 100644 index 000000000..a15d204d0 --- /dev/null +++ b/parser/src/structs/address_realized_data.rs @@ -0,0 +1,46 @@ +use super::{AddressData, Price, ProfitOrLoss, WAmount}; + +#[derive(Debug)] +pub struct AddressRealizedData { + pub initial_address_data: AddressData, + pub received: WAmount, + pub sent: WAmount, + pub profit: Price, + pub loss: Price, + pub utxos_created: u32, + pub utxos_destroyed: u32, +} + +impl AddressRealizedData { + pub fn default(initial_address_data: &AddressData) -> Self { + Self { + received: WAmount::ZERO, + sent: WAmount::ZERO, + profit: Price::ZERO, + loss: Price::ZERO, + utxos_created: 0, + utxos_destroyed: 0, + initial_address_data: *initial_address_data, + } + } + + pub fn receive(&mut self, amount: WAmount) { + self.received += amount; + self.utxos_created += 1; + } + + pub fn send(&mut self, amount: WAmount, realized_profit_or_loss: ProfitOrLoss) { + self.sent += amount; + + self.utxos_destroyed += 1; + + match realized_profit_or_loss { + ProfitOrLoss::Profit(price) => { + self.profit += price; + } + ProfitOrLoss::Loss(price) => { + self.loss += price; + } + } + } +} diff --git a/parser/src/structs/address_size.rs b/parser/src/structs/address_size.rs new file mode 100644 index 000000000..59de49429 --- /dev/null +++ b/parser/src/structs/address_size.rs @@ -0,0 +1,32 @@ +use allocative::Allocative; + +use super::WAmount; + +#[derive(PartialEq, PartialOrd, Ord, Eq, Debug, Allocative)] +pub enum AddressSize { + Empty, + Plankton, + Shrimp, + Crab, + Fish, + Shark, + Whale, + Humpback, + Megalodon, +} + +impl AddressSize { + pub fn from_amount(amount: WAmount) -> Self { + match amount.to_sat() { + 0 => Self::Empty, + 1..=9_999_999 => Self::Plankton, + 10_000_000..=99_999_999 => Self::Shrimp, + 100_000_000..=999_999_999 => Self::Crab, + 1_000_000_000..=9_999_999_999 => Self::Fish, + 10_000_000_000..=99_999_999_999 => Self::Shark, + 100_000_000_000..=999_999_999_999 => Self::Whale, + 1_000_000_000_000..=9_999_999_999_999 => Self::Humpback, + 10_000_000_000_000..=u64::MAX => Self::Megalodon, + } + } +} diff --git a/parser/src/structs/address_split.rs b/parser/src/structs/address_split.rs new file mode 100644 index 000000000..d54e57e48 --- /dev/null +++ b/parser/src/structs/address_split.rs @@ -0,0 +1,11 @@ +use allocative::Allocative; + +use super::{AddressSize, AddressType}; + +#[derive(Default, Allocative)] +pub enum AddressSplit { + #[default] + All, + Type(AddressType), + Size(AddressSize), +} diff --git a/parser/src/structs/address_type.rs b/parser/src/structs/address_type.rs new file mode 100644 index 000000000..87a571cdb --- /dev/null +++ b/parser/src/structs/address_type.rs @@ -0,0 +1,21 @@ +use allocative::Allocative; +use bincode::{Decode, Encode}; + +// https://unchained.com/blog/bitcoin-address-types-compared/ +#[derive( + Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Encode, Decode, Allocative, +)] +pub enum AddressType { + Empty, + OpReturn, + PushOnly, + #[default] + Unknown, + MultiSig, + P2PK, + P2PKH, + P2SH, + P2WPKH, + P2WSH, + P2TR, +} diff --git a/parser/src/structs/any_map.rs b/parser/src/structs/any_map.rs new file mode 100644 index 000000000..f913665e9 --- /dev/null +++ b/parser/src/structs/any_map.rs @@ -0,0 +1,22 @@ +pub trait AnyMap { + fn path(&self) -> &str; + fn path_last(&self) -> &Option<String>; + + fn t_name(&self) -> &str; + + fn exported_path_with_t_name(&self) -> Vec<(&str, &str)> { + let t_name = self.t_name(); + + if let Some(path_last) = self.path_last() { + vec![(self.path(), t_name), (path_last, t_name)] + } else { + vec![(self.path(), t_name)] + } + } + + // fn reset(&mut self) -> color_eyre::Result<()>; + + fn pre_export(&mut self); + fn export(&self) -> color_eyre::Result<()>; + fn post_export(&mut self); +} diff --git a/parser/src/structs/bi_map.rs b/parser/src/structs/bi_map.rs new file mode 100644 index 000000000..6a1300be2 --- /dev/null +++ b/parser/src/structs/bi_map.rs @@ -0,0 +1,341 @@ +use std::{ + iter::Sum, + ops::{Add, Div, Mul, RangeInclusive, Sub}, +}; + +use allocative::Allocative; +use ordered_float::FloatCore; + +use crate::{bitcoin::TARGET_BLOCKS_PER_DAY, utils::LossyFrom}; + +use super::{AnyDateMap, AnyHeightMap, AnyMap, DateMap, HeightMap, MapValue, WNaiveDate}; + +#[derive(Default, Allocative)] +pub struct BiMap<T> +where + T: MapValue, +{ + pub height: HeightMap<T>, + pub date: DateMap<T>, +} + +impl<T> BiMap<T> +where + T: MapValue, +{ + pub fn new_bin(version: u32, path: &str) -> Self { + Self { + height: HeightMap::_new_bin(version, path, true), + date: DateMap::_new_bin(version, path, false), + } + } + + pub fn new_json(version: u32, path: &str) -> Self { + Self { + height: HeightMap::new_json(version, path, true), + date: DateMap::new_json(version, path, false), + } + } + + pub fn date_insert_sum_range( + &mut self, + date: WNaiveDate, + date_blocks_range: &RangeInclusive<usize>, + ) where + T: Sum, + { + self.date + .insert(date, self.height.sum_range(date_blocks_range)); + } + + pub fn multi_date_insert_sum_range( + &mut self, + dates: &[WNaiveDate], + first_height: &mut DateMap<usize>, + last_height: &mut DateMap<usize>, + ) where + T: Sum, + { + dates.iter().for_each(|date| { + let first_height = first_height.get_or_import(date).unwrap(); + let last_height = last_height.get_or_import(date).unwrap(); + let range = first_height..=last_height; + + self.date.insert(*date, self.height.sum_range(&range)); + }) + } + + pub fn multi_insert_const(&mut self, heights: &[usize], dates: &[WNaiveDate], constant: T) { + self.height.multi_insert_const(heights, constant); + + self.date.multi_insert_const(dates, constant); + } + + pub fn multi_insert_simple_transform<F, K>( + &mut self, + heights: &[usize], + dates: &[WNaiveDate], + source: &mut BiMap<K>, + transform: &F, + ) where + T: Div<Output = T>, + F: Fn(K) -> T, + K: MapValue, + { + self.height + .multi_insert_simple_transform(heights, &mut source.height, transform); + self.date + .multi_insert_simple_transform(dates, &mut source.date, transform); + } + + #[allow(unused)] + pub fn multi_insert_add<A, B>( + &mut self, + heights: &[usize], + dates: &[WNaiveDate], + added: &mut BiMap<A>, + adder: &mut BiMap<B>, + ) where + A: MapValue, + B: MapValue, + T: LossyFrom<A> + LossyFrom<B>, + T: Add<Output = T>, + { + self.height + .multi_insert_add(heights, &mut added.height, &mut adder.height); + self.date + .multi_insert_add(dates, &mut added.date, &mut adder.date); + } + + pub fn multi_insert_subtract<A, B>( + &mut self, + heights: &[usize], + dates: &[WNaiveDate], + subtracted: &mut BiMap<A>, + subtracter: &mut BiMap<B>, + ) where + A: MapValue, + B: MapValue, + T: LossyFrom<A> + LossyFrom<B>, + T: Sub<Output = T>, + { + self.height + .multi_insert_subtract(heights, &mut subtracted.height, &mut subtracter.height); + + self.date + .multi_insert_subtract(dates, &mut subtracted.date, &mut subtracter.date); + } + + pub fn multi_insert_multiply<A, B>( + &mut self, + heights: &[usize], + dates: &[WNaiveDate], + multiplied: &mut BiMap<A>, + multiplier: &mut BiMap<B>, + ) where + A: MapValue, + B: MapValue, + T: LossyFrom<A> + LossyFrom<B>, + T: Mul<Output = T>, + { + self.height + .multi_insert_multiply(heights, &mut multiplied.height, &mut multiplier.height); + self.date + .multi_insert_multiply(dates, &mut multiplied.date, &mut multiplier.date); + } + + pub fn multi_insert_divide<A, B>( + &mut self, + heights: &[usize], + dates: &[WNaiveDate], + divided: &mut BiMap<A>, + divider: &mut BiMap<B>, + ) where + A: MapValue, + B: MapValue, + T: LossyFrom<A> + LossyFrom<B>, + T: Div<Output = T> + Mul<Output = T> + From<u8>, + { + self.height + .multi_insert_divide(heights, &mut divided.height, &mut divider.height); + self.date + .multi_insert_divide(dates, &mut divided.date, &mut divider.date); + } + + pub fn multi_insert_percentage<A, B>( + &mut self, + heights: &[usize], + dates: &[WNaiveDate], + divided: &mut BiMap<A>, + divider: &mut BiMap<B>, + ) where + A: MapValue, + B: MapValue, + T: LossyFrom<A> + LossyFrom<B>, + T: Div<Output = T> + Mul<Output = T> + From<u8>, + { + self.height + .multi_insert_percentage(heights, &mut divided.height, &mut divider.height); + self.date + .multi_insert_percentage(dates, &mut divided.date, &mut divider.date); + } + + pub fn multi_insert_cumulative<K>( + &mut self, + heights: &[usize], + dates: &[WNaiveDate], + source: &mut BiMap<K>, + ) where + K: MapValue, + T: LossyFrom<K>, + T: Add<Output = T> + Sub<Output = T>, + { + self.height + .multi_insert_cumulative(heights, &mut source.height); + + self.date.multi_insert_cumulative(dates, &mut source.date); + } + + pub fn multi_insert_last_x_sum<K>( + &mut self, + heights: &[usize], + dates: &[WNaiveDate], + source: &mut BiMap<K>, + days: usize, + ) where + K: MapValue, + T: LossyFrom<K>, + T: Add<Output = T> + Sub<Output = T>, + { + self.height.multi_insert_last_x_sum( + heights, + &mut source.height, + TARGET_BLOCKS_PER_DAY * days, + ); + + self.date + .multi_insert_last_x_sum(dates, &mut source.date, days); + } + + pub fn multi_insert_simple_average<K>( + &mut self, + heights: &[usize], + dates: &[WNaiveDate], + source: &mut BiMap<K>, + days: usize, + ) where + T: Into<f32> + From<f32>, + K: MapValue + Sum, + f32: LossyFrom<K>, + { + self.height.multi_insert_simple_average( + heights, + &mut source.height, + TARGET_BLOCKS_PER_DAY * days, + ); + self.date + .multi_insert_simple_average(dates, &mut source.date, days); + } + + pub fn multi_insert_net_change( + &mut self, + heights: &[usize], + dates: &[WNaiveDate], + source: &mut BiMap<T>, + days: usize, + ) where + T: Sub<Output = T>, + { + self.height.multi_insert_net_change( + heights, + &mut source.height, + TARGET_BLOCKS_PER_DAY * days, + ); + self.date + .multi_insert_net_change(dates, &mut source.date, days); + } + + pub fn multi_insert_median( + &mut self, + heights: &[usize], + dates: &[WNaiveDate], + source: &mut BiMap<T>, + days: Option<usize>, + ) where + T: FloatCore, + { + self.height.multi_insert_median( + heights, + &mut source.height, + days.map(|days| TARGET_BLOCKS_PER_DAY * days), + ); + self.date.multi_insert_median(dates, &mut source.date, days); + } + + #[allow(unused)] + pub fn multi_insert_percentile( + &mut self, + heights: &[usize], + dates: &[WNaiveDate], + source: &mut BiMap<T>, + percentile: f32, + days: Option<usize>, + ) where + T: FloatCore, + { + self.height.multi_insert_percentile( + heights, + &mut source.height, + percentile, + days.map(|days| TARGET_BLOCKS_PER_DAY * days), + ); + self.date + .multi_insert_percentile(dates, &mut source.date, percentile, days); + } +} + +pub trait AnyBiMap { + #[allow(unused)] + fn as_any_map(&self) -> Vec<&(dyn AnyMap + Send + Sync)>; + + fn as_any_mut_map(&mut self) -> Vec<&mut dyn AnyMap>; + + fn get_height(&self) -> &(dyn AnyHeightMap + Send + Sync); + + #[allow(unused)] + fn get_mut_height(&mut self) -> &mut dyn AnyHeightMap; + + fn get_date(&self) -> &(dyn AnyDateMap + Send + Sync); + + #[allow(unused)] + fn get_mut_date(&mut self) -> &mut dyn AnyDateMap; +} + +impl<T> AnyBiMap for BiMap<T> +where + T: MapValue, +{ + fn as_any_map(&self) -> Vec<&(dyn AnyMap + Send + Sync)> { + vec![self.date.as_any_map(), self.height.as_any_map()] + } + + fn as_any_mut_map(&mut self) -> Vec<&mut dyn AnyMap> { + vec![self.date.as_any_mut_map(), self.height.as_any_mut_map()] + } + + fn get_height(&self) -> &(dyn AnyHeightMap + Send + Sync) { + &self.height + } + + fn get_mut_height(&mut self) -> &mut dyn AnyHeightMap { + &mut self.height + } + + fn get_date(&self) -> &(dyn AnyDateMap + Send + Sync) { + &self.date + } + + fn get_mut_date(&mut self) -> &mut dyn AnyDateMap { + &mut self.date + } +} diff --git a/parser/src/structs/block_data.rs b/parser/src/structs/block_data.rs new file mode 100644 index 000000000..25b8c1fb6 --- /dev/null +++ b/parser/src/structs/block_data.rs @@ -0,0 +1,41 @@ +use allocative::Allocative; +use bincode::{Decode, Encode}; + +use super::{Price, WAmount}; + +#[derive(Debug, Encode, Decode, Allocative)] +pub struct BlockData { + pub height: u32, + pub price: Price, + pub timestamp: u32, + pub amount: WAmount, + pub utxos: u32, +} + +impl BlockData { + pub fn new(height: u32, price: Price, timestamp: u32) -> Self { + Self { + height, + price, + timestamp, + amount: WAmount::ZERO, + utxos: 0, + } + } + + pub fn send(&mut self, amount: WAmount) { + self.utxos -= 1; + + if self.amount < amount { + unreachable!(); + } + + self.amount -= amount; + } + + pub fn receive(&mut self, amount: WAmount) { + self.utxos += 1; + + self.amount += amount; + } +} diff --git a/parser/src/structs/block_path.rs b/parser/src/structs/block_path.rs new file mode 100644 index 000000000..748b5ee63 --- /dev/null +++ b/parser/src/structs/block_path.rs @@ -0,0 +1,25 @@ +use allocative::Allocative; +use bincode::{Decode, Encode}; + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Copy, Encode, Decode, Allocative)] +pub struct BlockPath { + pub date_index: u16, + pub block_index: u16, +} + +impl BlockPath { + pub fn new(date_index: u16, block_index: u16) -> Self { + Self { + date_index, + block_index, + } + } +} + +impl std::hash::Hash for BlockPath { + fn hash<H: std::hash::Hasher>(&self, hasher: &mut H) { + hasher.write_u32(((self.date_index as u32) << 16_u32) + self.block_index as u32) + } +} + +// impl nohash::IsEnabled for BlockPath {} diff --git a/parser/src/structs/counter.rs b/parser/src/structs/counter.rs new file mode 100644 index 000000000..f45f99312 --- /dev/null +++ b/parser/src/structs/counter.rs @@ -0,0 +1,28 @@ +use allocative::Allocative; +use bincode::{Decode, Encode}; +use derive_deref::{Deref, DerefMut}; + +#[derive(Debug, Deref, DerefMut, Default, Clone, Copy, Encode, Decode, Allocative)] +pub struct Counter(u32); + +impl Counter { + #[inline(always)] + pub fn increment(&mut self) { + self.0 += 1; + } + + #[inline(always)] + pub fn decrement(&mut self) { + self.0 -= 1; + } + + #[inline(always)] + pub fn reset(&mut self) { + self.0 = 0; + } + + #[inline(always)] + pub fn inner(&self) -> u32 { + self.0 + } +} diff --git a/parser/src/structs/date_data.rs b/parser/src/structs/date_data.rs new file mode 100644 index 000000000..243cd0774 --- /dev/null +++ b/parser/src/structs/date_data.rs @@ -0,0 +1,20 @@ +use allocative::Allocative; +use bincode::{Decode, Encode}; + +use super::{BlockData, BlockPath, WNaiveDate}; + +#[derive(Debug, Encode, Decode, Allocative)] +pub struct DateData { + pub date: WNaiveDate, + pub blocks: Vec<BlockData>, +} + +impl DateData { + pub fn new(date: WNaiveDate, blocks: Vec<BlockData>) -> Self { + Self { date, blocks } + } + + pub fn get_block_data(&self, block_path: &BlockPath) -> Option<&BlockData> { + self.blocks.get(block_path.block_index as usize) + } +} diff --git a/parser/src/structs/date_map.rs b/parser/src/structs/date_map.rs new file mode 100644 index 000000000..4c14a3999 --- /dev/null +++ b/parser/src/structs/date_map.rs @@ -0,0 +1,1256 @@ +use std::{ + collections::{BTreeMap, VecDeque}, + fmt::Debug, + fs, + iter::Sum, + mem, + ops::{Add, Div, Mul, Sub}, + path::{Path, PathBuf}, +}; + +use allocative::Allocative; +use bincode::{Decode, Encode}; +use chrono::{Datelike, Days}; +use itertools::Itertools; +use ordered_float::{FloatCore, OrderedFloat}; +use serde::{Deserialize, Serialize}; + +use crate::{ + io::{format_path, Serialization}, + utils::{log, LossyFrom}, +}; + +use super::{AnyMap, HeightMap, MapValue, WNaiveDate}; + +const NUMBER_OF_UNSAFE_DATES: usize = 2; +const MIN_YEAR: usize = 2009; + +#[derive(Debug, Serialize, Deserialize, Encode, Decode, Allocative)] +pub struct SerializedDateMap<T> { + version: u32, + map: BTreeMap<WNaiveDate, T>, +} + +#[derive(Default, Allocative)] +pub struct DateMap<T> { + version: u32, + + path_all: String, + path_last: Option<String>, + + chunks_in_memory: usize, + + serialization: Serialization, + + pub initial_last_date: Option<WNaiveDate>, + pub initial_first_unsafe_date: Option<WNaiveDate>, + + imported: BTreeMap<usize, SerializedDateMap<T>>, + to_insert: BTreeMap<usize, BTreeMap<WNaiveDate, T>>, +} + +impl<T> DateMap<T> +where + T: MapValue, +{ + pub fn new_bin(version: u32, path: &str) -> Self { + Self::new(version, path, Serialization::Binary, 1, true) + } + + pub fn _new_bin(version: u32, path: &str, export_last: bool) -> Self { + Self::new(version, path, Serialization::Binary, 1, export_last) + } + + pub fn new_json(version: u32, path: &str, export_last: bool) -> Self { + Self::new(version, path, Serialization::Json, usize::MAX, export_last) + } + + fn new( + version: u32, + path: &str, + serialization: Serialization, + chunks_in_memory: usize, + export_last: bool, + ) -> Self { + if chunks_in_memory < 1 { + panic!("Should always have at least the latest chunk in memory"); + } + + let path = format_path(path); + + let path_all = format!("{path}/date"); + + fs::create_dir_all(&path_all).unwrap(); + + let path_last = { + if export_last { + Some(serialization.append_extension(&format!("{path}/last"))) + } else { + None + } + }; + + let mut s = Self { + version, + + path_all, + path_last, + + chunks_in_memory, + + serialization, + + initial_last_date: None, + initial_first_unsafe_date: None, + + to_insert: BTreeMap::default(), + imported: BTreeMap::default(), + }; + + s.read_dir() + .into_iter() + .rev() + .take(chunks_in_memory) + .for_each(|(chunk_start, path)| { + if let Ok(serialized) = s.import(&path) { + if serialized.version == s.version { + s.imported.insert(chunk_start, serialized); + } else { + s.read_dir() + .iter() + .for_each(|(_, path)| fs::remove_file(path).unwrap()) + } + } + }); + + s.initial_last_date = s + .imported + .values() + .last() + .and_then(|serialized| serialized.map.keys().copied().max()); + + s.initial_first_unsafe_date = s.initial_last_date.and_then(|last_date| { + let offset = NUMBER_OF_UNSAFE_DATES - 1; + last_date + .checked_sub_days(Days::new(offset as u64)) + .map(WNaiveDate::wrap) + }); + + if s.initial_first_unsafe_date.is_none() { + log(&format!("New {path}")); + } + + s + } + + pub fn insert(&mut self, date: WNaiveDate, value: T) -> T { + if !self.is_date_safe(date) { + self.to_insert + .entry(date.year() as usize) + .or_default() + .insert(date, value); + } + + value + } + + pub fn insert_default(&mut self, date: WNaiveDate) -> T { + self.insert(date, T::default()) + } + + /// Same as get but with &WNaiveDate instead of NaiveDate + pub fn get(&self, date: &WNaiveDate) -> Option<T> { + let year = date.year() as usize; + + self.to_insert + .get(&year) + .and_then(|tree| tree.get(date).cloned()) + .or_else(|| { + self.imported + .get(&year) + .and_then(|serialized| serialized.map.get(date)) + .cloned() + }) + } + + /// Same as get_or_import but with &WNaiveDate instead of NaiveDate + pub fn get_or_import(&mut self, date: &WNaiveDate) -> Option<T> { + let year = date.year() as usize; + + if year < MIN_YEAR { + return None; + } + + self.to_insert + .get(&year) + .and_then(|tree| tree.get(date).cloned()) + .or_else(|| { + #[allow(clippy::map_entry)] // Can't be mut and then use read_dir() + if !self.imported.contains_key(&year) { + let dir_content = self.read_dir(); + + if let Some(path) = dir_content.get(&year) { + let serialized = self.import(path).unwrap(); + // .unwrap_or(SerializedDateMap { + // version: self.version, + // map: BTreeMap::default(), + // }); + + self.imported.insert(year, serialized); + } + } + + self.imported + .get(&year) + .and_then(|serialized| serialized.map.get(date)) + .cloned() + }) + } + + #[inline(always)] + pub fn is_date_safe(&self, date: WNaiveDate) -> bool { + self.initial_first_unsafe_date + .map_or(false, |initial_first_unsafe_date| { + initial_first_unsafe_date > date + }) + } + + fn read_dir(&self) -> BTreeMap<usize, PathBuf> { + Self::_read_dir(&self.path_all, &self.serialization) + } + + pub fn _read_dir(path: &str, serialization: &Serialization) -> BTreeMap<usize, PathBuf> { + fs::read_dir(path) + .unwrap() + .map(|entry| entry.unwrap().path()) + .filter(|path| { + let file_stem = path.file_stem().unwrap().to_str().unwrap(); + let extension = path.extension().unwrap().to_str().unwrap(); + + path.is_file() + && file_stem.len() == 4 + && file_stem.starts_with("20") + && extension == serialization.to_extension() + }) + .map(|path| { + let year = path + .file_stem() + .unwrap() + .to_str() + .unwrap() + .parse::<usize>() + .unwrap(); + + (year, path) + }) + .collect() + } + + fn import(&self, path: &Path) -> color_eyre::Result<SerializedDateMap<T>> { + self.serialization + .import::<SerializedDateMap<T>>(path.to_str().unwrap()) + } +} + +impl<T> AnyMap for DateMap<T> +where + T: MapValue, +{ + fn path(&self) -> &str { + &self.path_all + } + + fn path_last(&self) -> &Option<String> { + &self.path_last + } + + fn t_name(&self) -> &str { + std::any::type_name::<T>() + } + + // fn reset(&mut self) -> color_eyre::Result<()> { + // fs::remove_dir(&self.path_all)?; + + // self.initial_last_date = None; + // self.initial_first_unsafe_date = None; + + // self.imported.clear(); + // self.to_insert.clear(); + + // Ok(()) + // } + + fn pre_export(&mut self) { + self.to_insert.iter_mut().for_each(|(chunk_start, map)| { + self.imported + .entry(*chunk_start) + .or_insert(SerializedDateMap { + version: self.version, + map: BTreeMap::default(), + }) + .map + .extend(mem::take(map)); + }); + } + + fn export(&self) -> color_eyre::Result<()> { + let len = self.imported.len(); + + self.to_insert.iter().enumerate().try_for_each( + |(index, (year, map))| -> color_eyre::Result<()> { + if !map.is_empty() { + unreachable!() + } + + let path = self + .serialization + .append_extension(&format!("{}/{}", self.path_all, year)); + + let serialized = self.imported.get(year).unwrap(); + + self.serialization.export(&path, serialized)?; + + if index == len - 1 { + if let Some(path_last) = self.path_last.as_ref() { + self.serialization + .export(path_last, serialized.map.values().last().unwrap())?; + } + } + + Ok(()) + }, + ) + } + + fn post_export(&mut self) { + self.imported + .keys() + .rev() + .enumerate() + .filter(|(index, _)| *index + 1 > self.chunks_in_memory) + .map(|(_, key)| *key) + .collect_vec() + .iter() + .for_each(|key| { + self.imported.remove(key); + }); + + self.to_insert.clear(); + } +} + +pub trait AnyDateMap: AnyMap { + fn get_initial_first_unsafe_date(&self) -> Option<WNaiveDate>; + + fn get_initial_last_date(&self) -> Option<WNaiveDate>; + + fn as_any_map(&self) -> &(dyn AnyMap + Send + Sync); + + fn as_any_mut_map(&mut self) -> &mut dyn AnyMap; +} + +impl<T> AnyDateMap for DateMap<T> +where + T: MapValue, +{ + #[inline(always)] + fn get_initial_first_unsafe_date(&self) -> Option<WNaiveDate> { + self.initial_first_unsafe_date + } + + #[inline(always)] + fn get_initial_last_date(&self) -> Option<WNaiveDate> { + self.initial_last_date + } + + fn as_any_map(&self) -> &(dyn AnyMap + Send + Sync) { + self + } + + fn as_any_mut_map(&mut self) -> &mut dyn AnyMap { + self + } +} + +impl<T> DateMap<T> +where + T: MapValue, +{ + pub fn multi_insert<F>(&mut self, dates: &[WNaiveDate], mut callback: F) + where + F: FnMut(&WNaiveDate) -> T, + { + dates.iter().for_each(|date| { + self.insert(*date, callback(date)); + }); + } + + pub fn multi_insert_last( + &mut self, + dates: &[WNaiveDate], + source: &mut HeightMap<T>, + last_height: &mut DateMap<usize>, + ) { + dates.iter().for_each(|date| { + self.insert( + *date, + source.get_or_import(&last_height.get_or_import(date).unwrap()), + ); + }); + } + + pub fn multi_insert_const(&mut self, dates: &[WNaiveDate], constant: T) { + dates.iter().for_each(|date| { + self.insert(*date, constant); + }); + } + + pub fn multi_insert_simple_transform<K, F>( + &mut self, + dates: &[WNaiveDate], + source: &mut DateMap<K>, + transform: F, + ) where + F: Fn(K) -> T, + K: MapValue, + { + dates.iter().for_each(|date| { + self.insert(*date, transform(source.get_or_import(date).unwrap())); + }); + } + + pub fn multi_insert_complex_transform<K, F>( + &mut self, + dates: &[WNaiveDate], + source: &mut DateMap<K>, + transform: F, + ) where + K: MapValue, + F: Fn((K, &WNaiveDate, &mut DateMap<K>)) -> T, + { + dates.iter().for_each(|date| { + self.insert( + *date, + transform((source.get_or_import(date).unwrap(), date, source)), + ); + }); + } + + pub fn multi_insert_add<A, B>( + &mut self, + dates: &[WNaiveDate], + added: &mut DateMap<A>, + adder: &mut DateMap<B>, + ) where + A: MapValue, + B: MapValue, + T: LossyFrom<A> + LossyFrom<B>, + T: Add<Output = T>, + { + dates.iter().for_each(|date| { + self.insert( + *date, + T::lossy_from(added.get_or_import(date).unwrap()) + + T::lossy_from(adder.get_or_import(date).unwrap()), + ); + }); + } + + pub fn multi_insert_subtract<A, B>( + &mut self, + dates: &[WNaiveDate], + subtracted: &mut DateMap<A>, + subtracter: &mut DateMap<B>, + ) where + A: MapValue, + B: MapValue, + T: LossyFrom<A> + LossyFrom<B>, + T: Sub<Output = T>, + { + dates.iter().for_each(|date| { + self.insert( + *date, + T::lossy_from(subtracted.get_or_import(date).unwrap()) + - T::lossy_from(subtracter.get_or_import(date).unwrap()), + ); + }); + } + + pub fn multi_insert_multiply<A, B>( + &mut self, + dates: &[WNaiveDate], + multiplied: &mut DateMap<A>, + multiplier: &mut DateMap<B>, + ) where + A: MapValue, + B: MapValue, + T: LossyFrom<A> + LossyFrom<B>, + T: Mul<Output = T>, + { + dates.iter().for_each(|date| { + self.insert( + *date, + T::lossy_from(multiplied.get_or_import(date).unwrap()) + * T::lossy_from(multiplier.get_or_import(date).unwrap()), + ); + }); + } + + pub fn multi_insert_divide<A, B>( + &mut self, + dates: &[WNaiveDate], + divided: &mut DateMap<A>, + divider: &mut DateMap<B>, + ) where + A: MapValue, + B: MapValue, + T: LossyFrom<A> + LossyFrom<B>, + T: Div<Output = T> + Mul<Output = T> + From<u8>, + { + self._multi_insert_divide(dates, divided, divider, false) + } + + pub fn multi_insert_percentage<A, B>( + &mut self, + dates: &[WNaiveDate], + divided: &mut DateMap<A>, + divider: &mut DateMap<B>, + ) where + A: MapValue, + B: MapValue, + T: LossyFrom<A> + LossyFrom<B>, + T: Div<Output = T> + Mul<Output = T> + From<u8>, + { + self._multi_insert_divide(dates, divided, divider, true) + } + + pub fn _multi_insert_divide<A, B>( + &mut self, + dates: &[WNaiveDate], + divided: &mut DateMap<A>, + divider: &mut DateMap<B>, + as_percentage: bool, + ) where + A: MapValue, + B: MapValue, + T: LossyFrom<A> + LossyFrom<B>, + T: Div<Output = T> + Mul<Output = T> + From<u8>, + { + let multiplier = T::from(if as_percentage { 100 } else { 1 }); + + dates.iter().for_each(|date| { + self.insert( + *date, + T::lossy_from(divided.get_or_import(date).unwrap()) + / T::lossy_from(divider.get_or_import(date).unwrap()) + * multiplier, + ); + }); + } + + pub fn multi_insert_cumulative<K>(&mut self, dates: &[WNaiveDate], source: &mut DateMap<K>) + where + K: MapValue, + T: LossyFrom<K>, + T: Add<Output = T> + Sub<Output = T>, + { + self._multi_insert_last_x_sum(dates, source, None) + } + + pub fn multi_insert_last_x_sum<K>( + &mut self, + dates: &[WNaiveDate], + source: &mut DateMap<K>, + days: usize, + ) where + K: MapValue, + T: LossyFrom<K>, + T: Add<Output = T> + Sub<Output = T>, + { + self._multi_insert_last_x_sum(dates, source, Some(days)) + } + + fn _multi_insert_last_x_sum<K>( + &mut self, + dates: &[WNaiveDate], + source: &mut DateMap<K>, + days: Option<usize>, + ) where + K: MapValue, + T: LossyFrom<K>, + T: Add<Output = T> + Sub<Output = T>, + { + let mut sum = None; + + dates.iter().for_each(|date| { + let to_subtract = days + .and_then(|x| { + date.checked_sub_days(Days::new(x as u64)) + .and_then(|previous_date| { + source.get_or_import(&WNaiveDate::wrap(previous_date)) + }) + }) + .unwrap_or_default(); + + let previous_sum = sum.unwrap_or_else(|| { + date.checked_sub_days(Days::new(1)) + .and_then(|previous_sum_date| { + self.get_or_import(&WNaiveDate::wrap(previous_sum_date)) + }) + .unwrap_or_default() + }); + + let last_value = source.get_or_import(date).unwrap_or_else(|| { + dbg!(date); + panic!(); + }); + + sum.replace(previous_sum - T::lossy_from(to_subtract) + T::lossy_from(last_value)); + + self.insert(*date, sum.unwrap()); + }); + } + + pub fn multi_insert_simple_average<K>( + &mut self, + dates: &[WNaiveDate], + source: &mut DateMap<K>, + days: usize, + ) where + T: Into<f32> + From<f32>, + K: MapValue + Sum, + f32: LossyFrom<K>, + { + if days <= 1 { + panic!("Average of 1 or less is not useful"); + } + + let days = days as f32; + + let mut average = None; + + dates.iter().for_each(|date| { + let previous_average: f32 = average + .unwrap_or_else(|| { + date.checked_sub_days(Days::new(1)) + .and_then(|previous_average_date| { + self.get(&WNaiveDate::wrap(previous_average_date)) + }) + .unwrap_or_default() + }) + .into(); + + let last_value = f32::lossy_from(source.get_or_import(date).unwrap_or_else(|| { + dbg!(date); + panic!() + })); + + average.replace(((previous_average * (days - 1.0) + last_value) / days).into()); + + self.insert(*date, average.unwrap()); + }); + } + + pub fn multi_insert_net_change( + &mut self, + dates: &[WNaiveDate], + source: &mut DateMap<T>, + days: usize, + ) where + T: Sub<Output = T>, + { + dates.iter().for_each(|date| { + let previous_value = date + .checked_sub_days(Days::new(days as u64)) + .and_then(|date| source.get_or_import(&WNaiveDate::wrap(date))) + .unwrap_or_default(); + + let last_value = source.get_or_import(date).unwrap(); + + let net_change = last_value - previous_value; + + self.insert(*date, net_change); + }); + } + + pub fn multi_insert_percentage_change( + &mut self, + dates: &[WNaiveDate], + source: &mut DateMap<T>, + days: usize, + ) where + T: Sub<Output = T> + FloatCore, + { + let one = T::from(1.0).unwrap(); + let hundred = T::from(100.0).unwrap(); + + dates.iter().for_each(|date| { + let previous_value = date + .checked_sub_days(Days::new(days as u64)) + .and_then(|date| source.get_or_import(&WNaiveDate::wrap(date))) + .unwrap_or_default(); + + let last_value = source.get_or_import(date).unwrap(); + + let percentage_change = ((last_value / previous_value) - one) * hundred; + + self.insert(*date, percentage_change); + }); + } + + pub fn multi_insert_median( + &mut self, + dates: &[WNaiveDate], + source: &mut DateMap<T>, + days: Option<usize>, + ) where + T: FloatCore, + { + self.multi_insert_percentile(dates, source, 0.5, days); + } + + pub fn multi_insert_percentile( + &mut self, + dates: &[WNaiveDate], + source: &mut DateMap<T>, + percentile: f32, + days: Option<usize>, + ) where + T: FloatCore, + { + if !(0.0..=1.0).contains(&percentile) { + panic!("The percentile should be between 0.0 and 1.0"); + } + + if days.map_or(false, |size| size < 3) { + panic!("Computing a median for a size lower than 3 is useless"); + } + + let mut ordered_vec = None; + let mut sorted_vec = None; + + dates.iter().for_each(|date| { + let value = { + if let Some(start) = days + .map_or(chrono::NaiveDate::from_ymd_opt(2009, 3, 1), |size| { + date.checked_sub_days(Days::new(size as u64)) + }) + { + if ordered_vec.is_none() { + let mut vec = start + .iter_days() + .take_while(|d| *d != **date) + .flat_map(|date| source.get_or_import(&WNaiveDate::wrap(date))) + .map(|f| OrderedFloat(f)) + .collect_vec(); + + if days.is_some() { + ordered_vec.replace(VecDeque::from(vec.clone())); + } + + vec.sort_unstable(); + sorted_vec.replace(vec); + } else { + let float_value = OrderedFloat(source.get_or_import(date).unwrap()); + + if let Some(days) = days { + if let Some(ordered_vec) = ordered_vec.as_mut() { + if ordered_vec.len() == days { + let first = ordered_vec.pop_front().unwrap(); + + let pos = + sorted_vec.as_ref().unwrap().binary_search(&first).unwrap(); + + sorted_vec.as_mut().unwrap().remove(pos); + } + + ordered_vec.push_back(float_value); + } + } + + let pos = sorted_vec + .as_ref() + .unwrap() + .binary_search(&float_value) + .unwrap_or_else(|pos| pos); + sorted_vec.as_mut().unwrap().insert(pos, float_value); + } + + let vec = sorted_vec.as_ref().unwrap(); + + if vec.is_empty() { + T::default() + } else { + let index = vec.len() as f32 * percentile; + + if index.fract() != 0.0 && vec.len() > 1 { + (vec.get(index.ceil() as usize) + .unwrap_or_else(|| { + dbg!(vec, index, &self.path_all, &source.path_all, days); + panic!() + }) + .0 + + vec + .get(index.floor() as usize) + .unwrap_or_else(|| { + dbg!(vec, index, &self.path_all, &source.path_all, days); + panic!() + }) + .0) + / T::from(2.0).unwrap() + } else { + vec.get(index.floor() as usize) + .unwrap_or_else(|| { + dbg!(vec, index); + panic!(); + }) + .0 + } + } + } else { + T::default() + } + }; + + self.insert(*date, value); + }); + } + + // + // pub fn transform<F>(&self, transform: F) -> BTreeMap<WNaiveDate, T> + // where + // T: Copy + Default, + // F: Fn((&WNaiveDate, &T, &BTreeMap<WNaiveDate, T>, usize)) -> T, + // { + // Self::_transform(self.imported.lock().as_ref().unwrap(), transform) + // } + + // pub fn _transform<F>(map: &BTreeMap<WNaiveDate, T>, transform: F) -> BTreeMap<WNaiveDate, T> + // where + // T: Copy + Default, + // F: Fn((&WNaiveDate, &T, &BTreeMap<WNaiveDate, T>, usize)) -> T, + // { + // map.iter() + // .enumerate() + // .map(|(index, (date, value))| (date.to_owned(), transform((date, value, map, index)))) + // .collect() + // } + + // + // pub fn add(&self, other: &Self) -> BTreeMap<WNaiveDate, T> + // where + // T: Add<Output = T> + Copy + Default, + // { + // Self::_add( + // self.imported.lock().as_ref().unwrap(), + // other.imported.lock().as_ref().unwrap(), + // ) + // } + + // pub fn _add( + // map1: &BTreeMap<WNaiveDate, T>, + // map2: &BTreeMap<WNaiveDate, T>, + // ) -> BTreeMap<WNaiveDate, T> + // where + // T: Add<Output = T> + Copy + Default, + // { + // Self::_transform(map1, |(date, value, ..)| { + // map2.get(date) + // .map(|value2| *value + *value2) + // .unwrap_or_default() + // }) + // } + + // + // pub fn subtract(&self, other: &Self) -> BTreeMap<WNaiveDate, T> + // where + // T: Sub<Output = T> + Copy + Default, + // { + // Self::_subtract( + // self.imported.lock().as_ref().unwrap(), + // other.imported.lock().as_ref().unwrap(), + // ) + // } + + // pub fn _subtract( + // map1: &BTreeMap<WNaiveDate, T>, + // map2: &BTreeMap<WNaiveDate, T>, + // ) -> BTreeMap<WNaiveDate, T> + // where + // T: Sub<Output = T> + Copy + Default, + // { + // if map1.len() != map2.len() { + // panic!("Can't subtract two arrays with a different length"); + // } + + // Self::_transform(map1, |(date, value, ..)| { + // map2.get(date) + // .map(|value2| *value - *value2) + // .unwrap_or_default() + // }) + // } + + // + // pub fn multiply(&self, other: &Self) -> BTreeMap<WNaiveDate, T> + // where + // T: Mul<Output = T> + Copy + Default, + // { + // Self::_multiply( + // self.imported.lock().as_ref().unwrap(), + // other.imported.lock().as_ref().unwrap(), + // ) + // } + + // + // pub fn _multiply( + // map1: &BTreeMap<WNaiveDate, T>, + // map2: &BTreeMap<WNaiveDate, T>, + // ) -> BTreeMap<WNaiveDate, T> + // where + // T: Mul<Output = T> + Copy + Default, + // { + // Self::_transform(map1, |(date, value, ..)| { + // map2.get(date) + // .map(|value2| *value * *value2) + // .unwrap_or_default() + // }) + // } + + // + // pub fn divide(&self, other: &Self) -> BTreeMap<WNaiveDate, T> + // where + // T: Div<Output = T> + Copy + Default, + // { + // Self::_divide( + // self.imported.lock().as_ref().unwrap(), + // other.imported.lock().as_ref().unwrap(), + // ) + // } + + // + // pub fn _divide( + // map1: &BTreeMap<WNaiveDate, T>, + // map2: &BTreeMap<WNaiveDate, T>, + // ) -> BTreeMap<WNaiveDate, T> + // where + // T: Div<Output = T> + Copy + Default, + // { + // Self::_transform(map1, |(date, value, ..)| { + // map2.get(date) + // .map(|value2| *value / *value2) + // .unwrap_or_default() + // }) + // } + + // + // pub fn cumulate(&self) -> BTreeMap<WNaiveDate, T> + // where + // T: Sum + Copy + Default + AddAssign, + // { + // Self::_cumulate(self.imported.lock().as_ref().unwrap()) + // } + + // + // pub fn _cumulate(map: &BTreeMap<WNaiveDate, T>) -> BTreeMap<WNaiveDate, T> + // where + // T: Sum + Copy + Default + AddAssign, + // { + // let mut sum = T::default(); + + // map.iter() + // .map(|(date, value)| { + // sum += *value; + // (date.to_owned(), sum) + // }) + // .collect() + // } + + // pub fn insert_cumulative(&mut self, date: NaiveDate, source: &DateMap<T>) -> T + // where + // T: Add<Output = T> + Sub<Output = T>, + // { + // let previous_cum = date + // .checked_sub_days(Days::new(1)) + // .map(|previous_date| { + // self.get(previous_date).unwrap_or_else(|| { + // if previous_date.year() == 2009 && previous_date.month() == 1 { + // let day = previous_date.day(); + + // if day == 8 { + // self.get(NaiveDate::from_str("2009-01-03").unwrap()) + // .unwrap() + // } else if day == 2 { + // T::default() + // } else { + // panic!() + // } + // } else { + // dbg!(previous_date, &self.path_all); + // panic!() + // } + // }) + // }) + // .unwrap_or_default(); + + // let last_value = source.get(date).unwrap(); + + // let cum_value = previous_cum + last_value; + + // self.insert(date, cum_value); + + // cum_value + // } + + // + // pub fn insert_last_x_sum(&mut self, date: NaiveDate, source: &DateMap<T>, x: usize) -> T + // where + // T: Add<Output = T> + Sub<Output = T>, + // { + // let to_subtract = date + // .checked_sub_days(Days::new(x as u64 - 1)) + // .and_then(|previous_date| source.get(previous_date)) + // .unwrap_or_default(); + + // let previous_sum = date + // .checked_sub_days(Days::new(1)) + // .and_then(|previous_sum_date| self.get(previous_sum_date)) + // .unwrap_or_default(); + + // let last_value = source.get(date).unwrap(); + + // let sum = previous_sum - to_subtract + last_value; + + // self.insert(date, sum); + + // sum + // } + + // + // pub fn last_x_sum(&self, x: usize) -> BTreeMap<WNaiveDate, T> + // where + // T: Sum + Copy + Default + AddAssign + SubAssign, + // { + // Self::_last_x_sum(self.imported.lock().as_ref().unwrap(), x) + // } + + // pub fn _last_x_sum(map: &BTreeMap<WNaiveDate, T>, days: usize) -> BTreeMap<WNaiveDate, T> + // where + // T: Sum + Copy + Default + AddAssign + SubAssign, + // { + // let mut sum = T::default(); + + // map.iter() + // .enumerate() + // .map(|(index, (date, value))| { + // sum += *value; + + // if index >= days - 1 { + // let previous_index = index + 1 - days; + + // sum -= *map.values().nth(previous_index).unwrap() + // } + + // (date.to_owned(), sum) + // }) + // .collect() + // } + + // + // pub fn simple_moving_average(&self, x: usize) -> BTreeMap<WNaiveDate, f32> + // where + // T: Sum + Copy + Default + AddAssign + SubAssign + ToF32, + // { + // Self::_simple_moving_average(self.imported.lock().as_ref().unwrap(), x) + // } + + // pub fn insert_simple_average<K>(&mut self, date: NaiveDate, source: &DateMap<K>, x: usize) + // where + // T: Into<f32> + From<f32>, + // K: Clone + // + Copy + // + Default + // + Debug + // + Serialize + // + DeserializeOwned + // + Sum + // + savefile::Serialize + // + savefile::Deserialize + // + savefile::ReprC + // + ToF32, + // { + // let previous_average: f32 = date + // .checked_sub_days(Days::new(1)) + // .and_then(|previous_average_date| self.get(previous_average_date)) + // .unwrap_or_default() + // .into(); + + // let last_value: f32 = source.get(date).unwrap().to_f32(); + + // let sum = previous_average * x as f32 - 1.0 + last_value; + + // let average: T = (sum / x as f32).into(); + + // self.insert(date, average); + // } + + // + // pub fn _simple_moving_average( + // map: &BTreeMap<WNaiveDate, T>, + // x: usize, + // ) -> BTreeMap<WNaiveDate, f32> + // where + // T: Sum + Copy + Default + AddAssign + SubAssign + Into<f32>, + // { + // let mut sum = T::default(); + + // map.iter() + // .enumerate() + // .map(|(index, (date, value))| { + // sum += *value; + + // if index >= x - 1 { + // sum -= *map.values().nth(index + 1 - x).unwrap() + // } + + // let float_sum: f32 = sum.into(); + + // (date.to_owned(), float_sum / x as f32) + // }) + // .collect() + // } + + // + // pub fn net_change(&self, offset: usize) -> BTreeMap<WNaiveDate, T> + // where + // T: Copy + Default + Sub<Output = T>, + // { + // Self::_net_change(self.imported.lock().as_ref().unwrap(), offset) + // } + // + // + // pub fn insert_net_change(&mut self, date: NaiveDate, source: &DateMap<T>, offset: usize) -> T + // where + // T: Sub<Output = T>, + // { + // let previous_value = date + // .checked_sub_days(Days::new(offset as u64)) + // .and_then(|date| source.get(date)) + // .unwrap_or_default(); + + // let last_value = source.get(date).unwrap_or_else(|| { + // dbg!(date); + // panic!(); + // }); + + // let net = last_value - previous_value; + + // self.insert(date, net); + + // net + // } + + // + // pub fn _net_change(map: &BTreeMap<WNaiveDate, T>, offset: usize) -> BTreeMap<WNaiveDate, T> + // where + // T: Copy + Default + Sub<Output = T>, + // { + // Self::_transform(map, |(_, value, map, index)| { + // let previous = { + // if let Some(previous_index) = index.checked_sub(offset) { + // *map.values().nth(previous_index).unwrap() + // } else { + // T::default() + // } + // }; + + // *value - previous + // }) + // } + + // + // pub fn _median(map: &BTreeMap<WNaiveDate, T>, size: usize) -> BTreeMap<WNaiveDate, Option<T>> + // where + // T: FloatCore, + // { + // let even = size % 2 == 0; + // let median_index = size / 2; + + // if size < 3 { + // panic!("Computing a median for a size lower than 3 is useless"); + // } + + // map.iter() + // .enumerate() + // .map(|(index, (date, _))| { + // let value = { + // if index >= size - 1 { + // let mut vec = map + // .values() + // .rev() + // .take(size) + // .map(|v| OrderedFloat(*v)) + // .collect_vec(); + + // vec.sort_unstable(); + + // if even { + // Some( + // (**vec.get(median_index).unwrap() + // + **vec.get(median_index - 1).unwrap()) + // / T::from(2.0).unwrap(), + // ) + // } else { + // Some(**vec.get(median_index).unwrap()) + // } + // } else { + // None + // } + // }; + + // (date.to_owned(), value) + // }) + // .collect() + // } + // + // pub fn insert_median(&mut self, date: NaiveDate, source: &DateMap<T>, size: usize) -> T + // where + // T: FloatCore, + // { + // if size < 3 { + // panic!("Computing a median for a size lower than 3 is useless"); + // } + + // let median = { + // if let Some(start) = date.checked_sub_days(Days::new(size as u64 - 1)) { + // let even = size % 2 == 0; + // let median_index = size / 2; + + // let mut vec = start + // .iter_days() + // .take(size) + // .flat_map(|date| source.get(date)) + // .map(|f| OrderedFloat(f)) + // .collect_vec(); + + // if vec.len() != size { + // return T::default(); + // } + + // vec.sort_unstable(); + + // if even { + // (vec.get(median_index).unwrap().0 + vec.get(median_index - 1).unwrap().0) + // / T::from(2.0).unwrap() + // } else { + // vec.get(median_index).unwrap().0 + // } + // } else { + // T::default() + // } + // }; + + // self.insert(date, median); + + // median + // } +} diff --git a/parser/src/structs/empty_address_data.rs b/parser/src/structs/empty_address_data.rs new file mode 100644 index 000000000..c396e1bec --- /dev/null +++ b/parser/src/structs/empty_address_data.rs @@ -0,0 +1,25 @@ +use allocative::Allocative; +use sanakirja::{direct_repr, Storable, UnsizedStorable}; + +use super::{AddressData, AddressType, WAmount}; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default, Allocative)] +pub struct EmptyAddressData { + pub address_type: AddressType, + pub transfered: WAmount, +} +direct_repr!(EmptyAddressData); + +impl EmptyAddressData { + pub fn from_non_empty(non_empty: &AddressData) -> Self { + if non_empty.sent != non_empty.received { + dbg!(&non_empty); + panic!("Trying to convert not empty wallet to empty !"); + } + + Self { + address_type: non_empty.address_type, + transfered: non_empty.sent, + } + } +} diff --git a/parser/src/structs/height_map.rs b/parser/src/structs/height_map.rs new file mode 100644 index 000000000..c1c4d62a1 --- /dev/null +++ b/parser/src/structs/height_map.rs @@ -0,0 +1,918 @@ +use std::{ + cmp::Ordering, + collections::{BTreeMap, VecDeque}, + fmt::Debug, + fs, + iter::Sum, + mem, + ops::{Add, Div, Mul, RangeInclusive, Sub}, + path::{Path, PathBuf}, +}; + +use allocative::Allocative; +use bincode::{Decode, Encode}; +use itertools::Itertools; +use ordered_float::{FloatCore, OrderedFloat}; +use serde::{Deserialize, Serialize}; + +use crate::{ + bitcoin::NUMBER_OF_UNSAFE_BLOCKS, + io::{format_path, Serialization}, + utils::{log, LossyFrom}, +}; + +use super::{AnyMap, MapValue}; + +pub const HEIGHT_MAP_CHUNK_SIZE: usize = 10_000; + +#[derive(Debug, Serialize, Deserialize, Encode, Decode, Allocative)] +pub struct SerializedHeightMap<T> { + version: u32, + map: Vec<T>, +} + +#[derive(Default, Allocative)] +pub struct HeightMap<T> +where + T: MapValue, +{ + version: u32, + + path_all: String, + path_last: Option<String>, + + chunks_in_memory: usize, + + serialization: Serialization, + + initial_last_height: Option<usize>, + initial_first_unsafe_height: Option<usize>, + + imported: BTreeMap<usize, SerializedHeightMap<T>>, + to_insert: BTreeMap<usize, BTreeMap<usize, T>>, +} + +impl<T> HeightMap<T> +where + T: MapValue, +{ + pub fn new_bin(version: u32, path: &str) -> Self { + Self::new(version, path, Serialization::Binary, 1, true) + } + + pub fn _new_bin(version: u32, path: &str, export_last: bool) -> Self { + Self::new(version, path, Serialization::Binary, 1, export_last) + } + + pub fn new_json(version: u32, path: &str, export_last: bool) -> Self { + Self::new(version, path, Serialization::Json, usize::MAX, export_last) + } + + fn new( + version: u32, + path: &str, + serialization: Serialization, + chunks_in_memory: usize, + export_last: bool, + ) -> Self { + if chunks_in_memory < 1 { + panic!("Should always have at least the latest chunk in memory"); + } + + let path = format_path(path); + + let path_all = format!("{path}/height"); + + fs::create_dir_all(&path_all).unwrap(); + + let path_last = { + if export_last { + Some(serialization.append_extension(&format!("{path}/last"))) + } else { + None + } + }; + + let mut s = Self { + version, + + path_all, + path_last, + + chunks_in_memory, + + serialization, + + initial_first_unsafe_height: None, + initial_last_height: None, + + to_insert: BTreeMap::default(), + imported: BTreeMap::default(), + }; + + s.read_dir() + .into_iter() + .rev() + .take(chunks_in_memory) + .for_each(|(chunk_start, path)| { + if let Ok(serialized) = s.import(&path) { + if serialized.version == s.version { + s.imported.insert(chunk_start, serialized); + } else { + s.read_dir() + .iter() + .for_each(|(_, path)| fs::remove_file(path).unwrap()) + } + } + }); + + s.initial_last_height = s + .imported + .iter() + .last() + .map(|(chunk_start, serialized)| chunk_start + serialized.map.len()); + + s.initial_first_unsafe_height = s.initial_last_height.and_then(|last_height| { + let offset = NUMBER_OF_UNSAFE_BLOCKS - 1; + last_height.checked_sub(offset) + }); + + if s.initial_first_unsafe_height.is_none() { + log(&format!("New {path}")); + } + + s + } + + fn height_to_chunk_name(height: usize) -> String { + let start = Self::height_to_chunk_start(height); + let end = start + HEIGHT_MAP_CHUNK_SIZE; + + format!("{start}..{end}") + } + + fn height_to_chunk_start(height: usize) -> usize { + height / HEIGHT_MAP_CHUNK_SIZE * HEIGHT_MAP_CHUNK_SIZE + } + + pub fn insert(&mut self, height: usize, value: T) -> T { + if !self.is_height_safe(height) { + self.to_insert + .entry(Self::height_to_chunk_start(height)) + .or_default() + .insert(height % HEIGHT_MAP_CHUNK_SIZE, value); + } + + value + } + + pub fn insert_default(&mut self, height: usize) -> T { + self.insert(height, T::default()) + } + + pub fn get(&self, height: &usize) -> Option<T> { + let chunk_start = Self::height_to_chunk_start(*height); + + self.to_insert + .get(&chunk_start) + .and_then(|map| map.get(&(height - chunk_start)).cloned()) + .or_else(|| { + self.imported + .get(&chunk_start) + .and_then(|serialized| serialized.map.get(height - chunk_start)) + .cloned() + }) + } + + pub fn get_or_import(&mut self, height: &usize) -> T { + let chunk_start = Self::height_to_chunk_start(*height); + + self.to_insert + .get(&chunk_start) + .and_then(|map| map.get(&(height - chunk_start)).cloned()) + .or_else(|| { + #[allow(clippy::map_entry)] // Can't be mut and then use read_dir() + if !self.imported.contains_key(&chunk_start) { + let dir_content = self.read_dir(); + + let path = dir_content.get(&chunk_start).unwrap_or_else(|| { + dbg!(self.path(), chunk_start, &dir_content); + panic!(); + }); + + let serialized = self.import(path).unwrap(); + + self.imported.insert(chunk_start, serialized); + } + + self.imported + .get(&chunk_start) + .and_then(|serialized| serialized.map.get(height - chunk_start)) + .cloned() + }) + .unwrap_or_else(|| { + dbg!(height, self.path()); + panic!(); + }) + } + + #[inline(always)] + pub fn is_height_safe(&self, height: usize) -> bool { + self.initial_first_unsafe_height.unwrap_or(0) > height + } + + fn read_dir(&self) -> BTreeMap<usize, PathBuf> { + Self::_read_dir(&self.path_all, &self.serialization) + } + + pub fn _read_dir(path: &str, serialization: &Serialization) -> BTreeMap<usize, PathBuf> { + fs::read_dir(path) + .unwrap() + .map(|entry| entry.unwrap().path()) + .filter(|path| { + let extension = path.extension().unwrap().to_str().unwrap(); + + path.is_file() && extension == serialization.to_extension() + }) + .map(|path| { + ( + path.file_stem() + .unwrap() + .to_str() + .unwrap() + .split("..") + .next() + .unwrap() + .parse::<usize>() + .unwrap(), + path, + ) + }) + .collect() + } + + fn import(&self, path: &Path) -> color_eyre::Result<SerializedHeightMap<T>> { + self.serialization + .import::<SerializedHeightMap<T>>(path.to_str().unwrap()) + } +} + +impl<T> AnyMap for HeightMap<T> +where + T: MapValue, +{ + fn path(&self) -> &str { + &self.path_all + } + + fn path_last(&self) -> &Option<String> { + &self.path_last + } + + fn t_name(&self) -> &str { + std::any::type_name::<T>() + } + + // fn reset(&mut self) -> color_eyre::Result<()> { + // fs::remove_dir(&self.path_all)?; + + // self.initial_last_height = None; + // self.initial_first_unsafe_height = None; + + // self.imported.clear(); + // self.to_insert.clear(); + + // Ok(()) + // } + + fn pre_export(&mut self) { + self.to_insert.iter_mut().for_each(|(chunk_start, map)| { + let serialized = self + .imported + .entry(*chunk_start) + .or_insert(SerializedHeightMap { + version: self.version, + map: vec![], + }); + + mem::take(map) + .into_iter() + .for_each( + |(chunk_height, value)| match serialized.map.len().cmp(&chunk_height) { + Ordering::Greater => serialized.map[chunk_height] = value, + Ordering::Equal => serialized.map.push(value), + Ordering::Less => panic!(), + }, + ); + }); + } + + fn export(&self) -> color_eyre::Result<()> { + let len = self.imported.len(); + + self.to_insert.iter().enumerate().try_for_each( + |(index, (chunk_start, map))| -> color_eyre::Result<()> { + if !map.is_empty() { + unreachable!() + } + + let chunk_name = Self::height_to_chunk_name(*chunk_start); + + let path = self + .serialization + .append_extension(&format!("{}/{}", self.path_all, chunk_name)); + + let serialized = self.imported.get(chunk_start).unwrap_or_else(|| { + dbg!(&self.path_all, chunk_start, &self.imported); + panic!(); + }); + + self.serialization.export(&path, serialized)?; + + if index == len - 1 { + if let Some(path_last) = self.path_last.as_ref() { + self.serialization + .export(path_last, serialized.map.last().unwrap())?; + } + } + + Ok(()) + }, + ) + } + + fn post_export(&mut self) { + self.imported + .keys() + .rev() + .enumerate() + .filter(|(index, _)| *index + 1 > self.chunks_in_memory) + .map(|(_, key)| *key) + .collect_vec() + .iter() + .for_each(|key| { + self.imported.remove(key); + }); + + self.to_insert.clear(); + } +} + +pub trait AnyHeightMap: AnyMap { + fn get_initial_first_unsafe_height(&self) -> Option<usize>; + + fn get_initial_last_height(&self) -> Option<usize>; + + fn as_any_map(&self) -> &(dyn AnyMap + Send + Sync); + + fn as_any_mut_map(&mut self) -> &mut dyn AnyMap; +} + +impl<T> AnyHeightMap for HeightMap<T> +where + T: MapValue, +{ + #[inline(always)] + fn get_initial_first_unsafe_height(&self) -> Option<usize> { + self.initial_first_unsafe_height + } + + #[inline(always)] + fn get_initial_last_height(&self) -> Option<usize> { + self.initial_last_height + } + + fn as_any_map(&self) -> &(dyn AnyMap + Send + Sync) { + self + } + + fn as_any_mut_map(&mut self) -> &mut dyn AnyMap { + self + } +} + +impl<T> HeightMap<T> +where + T: MapValue, +{ + pub fn sum_range(&self, range: &RangeInclusive<usize>) -> T + where + T: Sum, + { + range + .to_owned() + .flat_map(|height| self.get(&height)) + .sum::<T>() + } + + pub fn multi_insert_const(&mut self, heights: &[usize], constant: T) { + heights.iter().for_each(|height| { + let height = *height; + + self.insert(height, constant); + }); + } + + pub fn multi_insert_simple_transform<K, F>( + &mut self, + heights: &[usize], + source: &mut HeightMap<K>, + transform: F, + ) where + K: MapValue, + F: Fn(K) -> T, + { + heights.iter().for_each(|height| { + self.insert(*height, transform(source.get_or_import(height))); + }); + } + + pub fn multi_insert_complex_transform<K, F>( + &mut self, + heights: &[usize], + source: &mut HeightMap<K>, + transform: F, + ) where + K: MapValue, + F: Fn((K, &usize)) -> T, + { + heights.iter().for_each(|height| { + self.insert(*height, transform((source.get_or_import(height), height))); + }); + } + + pub fn multi_insert_add<A, B>( + &mut self, + heights: &[usize], + added: &mut HeightMap<A>, + adder: &mut HeightMap<B>, + ) where + A: MapValue, + B: MapValue, + T: LossyFrom<A> + LossyFrom<B>, + T: Add<Output = T>, + { + heights.iter().for_each(|height| { + self.insert( + *height, + T::lossy_from(added.get_or_import(height)) + + T::lossy_from(adder.get_or_import(height)), + ); + }); + } + + pub fn multi_insert_subtract<A, B>( + &mut self, + heights: &[usize], + subtracted: &mut HeightMap<A>, + subtracter: &mut HeightMap<B>, + ) where + A: MapValue, + B: MapValue, + T: LossyFrom<A> + LossyFrom<B>, + T: Sub<Output = T>, + { + heights.iter().for_each(|height| { + self.insert( + *height, + T::lossy_from(subtracted.get_or_import(height)) + - T::lossy_from(subtracter.get_or_import(height)), + ); + }); + } + + pub fn multi_insert_multiply<A, B>( + &mut self, + heights: &[usize], + multiplied: &mut HeightMap<A>, + multiplier: &mut HeightMap<B>, + ) where + A: MapValue, + B: MapValue, + T: LossyFrom<A> + LossyFrom<B>, + T: Mul<Output = T>, + { + heights.iter().for_each(|height| { + self.insert( + *height, + T::lossy_from(multiplied.get_or_import(height)) + * T::lossy_from(multiplier.get_or_import(height)), + ); + }); + } + + pub fn multi_insert_divide<A, B>( + &mut self, + heights: &[usize], + divided: &mut HeightMap<A>, + divider: &mut HeightMap<B>, + ) where + A: MapValue, + B: MapValue, + T: LossyFrom<A> + LossyFrom<B>, + T: Div<Output = T> + Mul<Output = T> + From<u8>, + { + self._multi_insert_divide(heights, divided, divider, false) + } + + pub fn multi_insert_percentage<A, B>( + &mut self, + heights: &[usize], + divided: &mut HeightMap<A>, + divider: &mut HeightMap<B>, + ) where + A: MapValue, + B: MapValue, + T: LossyFrom<A> + LossyFrom<B>, + T: Div<Output = T> + Mul<Output = T> + From<u8>, + { + self._multi_insert_divide(heights, divided, divider, true) + } + + pub fn _multi_insert_divide<A, B>( + &mut self, + heights: &[usize], + divided: &mut HeightMap<A>, + divider: &mut HeightMap<B>, + as_percentage: bool, + ) where + A: MapValue, + B: MapValue, + T: LossyFrom<A> + LossyFrom<B>, + T: Div<Output = T> + Mul<Output = T> + From<u8>, + { + let multiplier = T::from(if as_percentage { 100 } else { 1 }); + + heights.iter().for_each(|height| { + self.insert( + *height, + T::lossy_from(divided.get_or_import(height)) + / T::lossy_from(divider.get_or_import(height)) + * multiplier, + ); + }); + } + + pub fn multi_insert_cumulative<K>(&mut self, heights: &[usize], source: &mut HeightMap<K>) + where + K: MapValue, + T: LossyFrom<K>, + T: Add<Output = T> + Sub<Output = T>, + { + self._multi_insert_last_x_sum(heights, source, None) + } + + pub fn multi_insert_last_x_sum<K>( + &mut self, + heights: &[usize], + source: &mut HeightMap<K>, + block_time: usize, + ) where + K: MapValue, + T: LossyFrom<K>, + T: Add<Output = T> + Sub<Output = T>, + { + self._multi_insert_last_x_sum(heights, source, Some(block_time)) + } + + fn _multi_insert_last_x_sum<K>( + &mut self, + heights: &[usize], + source: &mut HeightMap<K>, + block_time: Option<usize>, + ) where + K: MapValue, + T: LossyFrom<K>, + T: Add<Output = T> + Sub<Output = T>, + { + let mut sum = None; + + heights.iter().for_each(|height| { + let to_subtract = block_time + .and_then(|x| { + (height + 1) + .checked_sub(x) + .map(|previous_height| source.get_or_import(&previous_height)) + }) + .unwrap_or_default(); + + let previous_sum = sum.unwrap_or_else(|| { + height + .checked_sub(1) + .map(|previous_sum_height| self.get_or_import(&previous_sum_height)) + .unwrap_or_default() + }); + + let last_value = source.get_or_import(height); + + sum.replace(previous_sum + T::lossy_from(last_value) - T::lossy_from(to_subtract)); + + self.insert(*height, sum.unwrap()); + }); + } + + pub fn multi_insert_simple_average<K>( + &mut self, + heights: &[usize], + source: &mut HeightMap<K>, + block_time: usize, + ) where + T: Into<f32> + From<f32>, + K: MapValue + Sum, + f32: LossyFrom<K>, + { + if block_time <= 1 { + panic!("Average of 1 or less is not useful"); + } + + let mut average = None; + + heights.iter().for_each(|height| { + let height = *height; + + let previous_average: f32 = average + .unwrap_or_else(|| { + height + .checked_sub(block_time) + .and_then(|previous_average_height| self.get(&previous_average_height)) + .unwrap_or_default() + }) + .into(); + + let last_value = f32::lossy_from(source.get_or_import(&height)); + + average.replace( + ((previous_average * (block_time as f32 - 1.0) + last_value) / block_time as f32) + .into(), + ); + + self.insert(height, average.unwrap()); + }); + } + + pub fn multi_insert_net_change( + &mut self, + heights: &[usize], + source: &mut HeightMap<T>, + block_time: usize, + ) where + T: Sub<Output = T>, + { + heights.iter().for_each(|height| { + let height = *height; + + let previous_value = height + .checked_sub(block_time) + .map(|height| source.get_or_import(&height)) + .unwrap_or_default(); + + let last_value = source.get_or_import(&height); + + let net = last_value - previous_value; + + self.insert(height, net); + }); + } + + pub fn multi_insert_median( + &mut self, + heights: &[usize], + source: &mut HeightMap<T>, + block_time: Option<usize>, + ) where + T: FloatCore, + { + self.multi_insert_percentile(heights, source, 0.5, block_time); + } + + pub fn multi_insert_percentile( + &mut self, + heights: &[usize], + source: &mut HeightMap<T>, + percentile: f32, + block_time: Option<usize>, + ) where + T: FloatCore, + { + if !(0.0..=1.0).contains(&percentile) { + panic!("The percentile should be between 0.0 and 1.0"); + } + + if block_time.map_or(false, |size| size < 3) { + panic!("Computing a median for a size lower than 3 is useless"); + } + + let mut ordered_vec = None; + let mut sorted_vec = None; + + heights.iter().for_each(|height| { + let height = *height; + + let value = { + if let Some(start) = block_time.map_or(Some(0), |size| height.checked_sub(size)) { + if ordered_vec.is_none() { + let mut vec = (start..=height) + .map(|height| OrderedFloat(source.get_or_import(&height))) + .collect_vec(); + + if block_time.is_some() { + ordered_vec.replace(VecDeque::from(vec.clone())); + } + + vec.sort_unstable(); + sorted_vec.replace(vec); + } else { + let float_value = OrderedFloat(source.get_or_import(&height)); + + if block_time.is_some() { + let first = ordered_vec.as_mut().unwrap().pop_front().unwrap(); + let pos = sorted_vec.as_ref().unwrap().binary_search(&first).unwrap(); + sorted_vec.as_mut().unwrap().remove(pos); + + ordered_vec.as_mut().unwrap().push_back(float_value); + } + + let pos = sorted_vec + .as_ref() + .unwrap() + .binary_search(&float_value) + .unwrap_or_else(|pos| pos); + sorted_vec.as_mut().unwrap().insert(pos, float_value); + } + + let vec = sorted_vec.as_ref().unwrap(); + + let index = vec.len() as f32 * percentile; + + if index.fract() != 0.0 { + (vec.get(index.ceil() as usize) + .unwrap_or_else(|| { + dbg!(index, &self.path_all, &source.path_all, block_time); + panic!() + }) + .0 + + vec + .get(index.floor() as usize) + .unwrap_or_else(|| { + dbg!(index, &self.path_all, &source.path_all, block_time); + panic!() + }) + .0) + / T::from(2.0).unwrap() + } else { + vec.get(index as usize).unwrap().0 + } + } else { + T::default() + } + }; + + self.insert(height, value); + }); + } + + // pub fn insert_cumulative(&mut self, height: usize, source: &HeightMap<T>) -> T + // where + // T: Add<Output = T> + Sub<Output = T>, + // { + // let previous_cum = height + // .checked_sub(1) + // .map(|previous_sum_height| { + // self.get(&previous_sum_height).unwrap_or_else(|| { + // dbg!(previous_sum_height); + // panic!() + // }) + // }) + // .unwrap_or_default(); + + // let last_value = source.get(&height).unwrap(); + + // let cum_value = previous_cum + last_value; + + // self.insert(height, cum_value); + + // cum_value + // } + + // pub fn insert_last_x_sum(&mut self, height: usize, source: &HeightMap<T>, x: usize) -> T + // where + // T: Add<Output = T> + Sub<Output = T>, + // { + // let to_subtract = (height + 1) + // .checked_sub(x) + // .map(|previous_height| { + // source.get(&previous_height).unwrap_or_else(|| { + // dbg!(&self.path_all, &source.path_all, previous_height); + // panic!() + // }) + // }) + // .unwrap_or_default(); + + // let previous_sum = height + // .checked_sub(1) + // .map(|previous_sum_height| self.get(&previous_sum_height).unwrap()) + // .unwrap_or_default(); + + // let last_value = source.get(&height).unwrap(); + + // let sum = previous_sum + last_value - to_subtract; + + // self.insert(height, sum); + + // sum + // } + + // pub fn insert_simple_average(&mut self, height: usize, source: &HeightMap<T>, block_time: usize) + // where + // T: Into<f32> + From<f32>, + // { + // let to_subtract: f32 = (height + 1) + // .checked_sub(block_time) + // .map(|previous_height| source.get(&previous_height).unwrap()) + // .unwrap_or_default() + // .into(); + + // let previous_average: f32 = height + // .checked_sub(1) + // .map(|previous_average_height| self.get(&previous_average_height).unwrap()) + // .unwrap_or_default() + // .into(); + + // let last_value: f32 = source.get(&height).unwrap().into(); + + // let sum = previous_average * block_time as f32 - to_subtract + last_value; + + // let average: T = (sum / block_time as f32).into(); + + // self.insert(height, average); + // } + + // pub fn insert_net_change(&mut self, height: usize, source: &HeightMap<T>, offset: usize) -> T + // where + // T: Sub<Output = T>, + // { + // let previous_value = height + // .checked_sub(offset) + // .map(|height| { + // source.get(&height).unwrap_or_else(|| { + // dbg!(&self.path_all, &source.path_all, offset); + // panic!(); + // }) + // }) + // .unwrap_or_default(); + + // let last_value = source.get(&height).unwrap(); + + // let net = last_value - previous_value; + + // self.insert(height, net); + + // net + // } + + // pub fn insert_median(&mut self, height: usize, source: &HeightMap<T>, size: usize) -> T + // where + // T: FloatCore, + // { + // if size < 3 { + // panic!("Computing a median for a size lower than 3 is useless"); + // } + + // let median = { + // if let Some(start) = height.checked_sub(size - 1) { + // let even = size % 2 == 0; + // let median_index = size / 2; + + // let mut vec = (start..=height) + // .map(|height| { + // OrderedFloat(source.get(&height).unwrap_or_else(|| { + // dbg!(height, &source.path_all, size); + // panic!() + // })) + // }) + // .collect_vec(); + + // vec.sort_unstable(); + + // if even { + // (vec.get(median_index) + // .unwrap_or_else(|| { + // dbg!(median_index, &self.path_all, &source.path_all, size); + // panic!() + // }) + // .0 + // + vec.get(median_index - 1).unwrap().0) + // / T::from(2.0).unwrap() + // } else { + // vec.get(median_index).unwrap().0 + // } + // } else { + // T::default() + // } + // }; + + // self.insert(height, median); + + // median + // } +} diff --git a/parser/src/structs/liquidity.rs b/parser/src/structs/liquidity.rs new file mode 100644 index 000000000..73ccbeb0c --- /dev/null +++ b/parser/src/structs/liquidity.rs @@ -0,0 +1,178 @@ +use std::{ + f64::consts::E, + ops::{AddAssign, SubAssign}, +}; + +use allocative::Allocative; + +use super::WAmount; + +#[derive(Debug)] +pub struct LiquidityClassification { + illiquid: f64, + liquid: f64, + // highly_liquid: f64, +} + +impl LiquidityClassification { + /// Following this: + /// https://insights.glassnode.com/bitcoin-liquid-supply/ + /// https://www.desmos.com/calculator/dutgni5rtj + pub fn new(sent: WAmount, received: WAmount) -> Self { + if received == WAmount::ZERO { + dbg!(sent, received); + panic!() + } + + let liquidity = { + if sent > received { + panic!("Shouldn't be possible"); + } + + if sent == WAmount::ZERO { + 0.0 + } else { + let liquidity = sent.to_sat() as f64 / received.to_sat() as f64; + + if liquidity.is_nan() { + dbg!(sent, received); + unreachable!() + } else { + liquidity + } + } + }; + + let illiquid_line = Self::compute_illiquid_line(liquidity); + let liquid_line = Self::compute_liquid_line(liquidity); + + let illiquid = illiquid_line; + let liquid = liquid_line - illiquid_line; + let highly_liquid = 1.0 - liquid_line; + + if illiquid < 0.0 || liquid < 0.0 || highly_liquid < 0.0 { + unreachable!() + } + + Self { + illiquid, + liquid, + // highly_liquid: 1.0 - liquid - illiquid, + } + } + + #[inline(always)] + pub fn split(&self, value: f64) -> LiquiditySplitResult { + let illiquid = value * self.illiquid; + let liquid = value * self.liquid; + let highly_liquid = value - illiquid - liquid; + + LiquiditySplitResult { + illiquid, + liquid, + highly_liquid, + } + } + + /// Returns value in range 0.0..1.0 + #[inline(always)] + fn compute_illiquid_line(x: f64) -> f64 { + Self::compute_ratio(x, 0.25) + } + + /// Returns value in range 0.0..1.0 + #[inline(always)] + fn compute_liquid_line(x: f64) -> f64 { + Self::compute_ratio(x, 0.75) + } + + #[inline(always)] + fn compute_ratio(x: f64, x0: f64) -> f64 { + let l = 1.0; + let k = 25.0; + + l / (1.0 + E.powf(k * (x - x0))) + } +} + +#[derive(Debug, Default)] +pub struct LiquiditySplitResult { + pub illiquid: f64, + pub liquid: f64, + pub highly_liquid: f64, +} + +#[derive(Debug, Default, PartialEq, PartialOrd, Clone, Copy, Allocative)] +pub struct SplitByLiquidity<T> +where + T: Default, +{ + pub all: T, + pub illiquid: T, + pub liquid: T, + pub highly_liquid: T, +} + +impl<T> AddAssign for SplitByLiquidity<T> +where + T: AddAssign + Default, +{ + fn add_assign(&mut self, rhs: Self) { + self.all += rhs.all; + self.illiquid += rhs.illiquid; + self.liquid += rhs.liquid; + self.highly_liquid += rhs.highly_liquid; + } +} + +impl<T> SubAssign for SplitByLiquidity<T> +where + T: SubAssign + Default, +{ + fn sub_assign(&mut self, rhs: Self) { + self.all -= rhs.all; + self.illiquid -= rhs.illiquid; + self.liquid -= rhs.liquid; + self.highly_liquid -= rhs.highly_liquid; + } +} + +// impl<T> SplitByLiquidity<T> +// where +// T: Default, +// { +// // pub fn get(&self, id: &LiquidityId) -> &T { +// // match id { +// // LiquidityId::All => &self.all, +// // LiquidityId::Illiquid => &self.illiquid, +// // LiquidityId::Liquid => &self.liquid, +// // LiquidityId::HighlyLiquid => &self.highly_liquid, +// // } +// // } + +// pub fn get_mut(&mut self, id: &LiquidityId) -> &mut T { +// match id { +// LiquidityId::All => &mut self.all, +// LiquidityId::Illiquid => &mut self.illiquid, +// LiquidityId::Liquid => &mut self.liquid, +// LiquidityId::HighlyLiquid => &mut self.highly_liquid, +// } +// } + +// pub fn as_vec(&self) -> Vec<(&T, LiquidityId)> { +// vec![ +// (&self.all, LiquidityId::All), +// (&self.illiquid, LiquidityId::Illiquid), +// (&self.liquid, LiquidityId::Liquid), +// (&self.highly_liquid, LiquidityId::HighlyLiquid), +// ] +// } +// } + +// #[derive(Debug, Clone, Copy)] +// pub enum LiquidityId { +// All, +// Illiquid, +// Liquid, +// HighlyLiquid, +// } diff --git a/parser/src/structs/map_value.rs b/parser/src/structs/map_value.rs new file mode 100644 index 000000000..e5029b0e7 --- /dev/null +++ b/parser/src/structs/map_value.rs @@ -0,0 +1,22 @@ +use std::fmt::Debug; + +use bincode::{Decode, Encode}; +use serde::{de::DeserializeOwned, Serialize}; + +use crate::datasets::OHLC; + +use super::WNaiveDate; + +pub trait MapValue: + Clone + Copy + Default + Debug + Serialize + DeserializeOwned + Encode + Decode + Sync + Send +{ +} + +impl MapValue for u16 {} +impl MapValue for u32 {} +impl MapValue for u64 {} +impl MapValue for usize {} +impl MapValue for f32 {} +impl MapValue for f64 {} +impl MapValue for WNaiveDate {} +impl MapValue for OHLC {} diff --git a/parser/src/structs/mod.rs b/parser/src/structs/mod.rs new file mode 100644 index 000000000..61e7be34e --- /dev/null +++ b/parser/src/structs/mod.rs @@ -0,0 +1,49 @@ +mod address; +mod address_data; +mod address_realized_data; +mod address_size; +mod address_split; +mod address_type; +mod any_map; +mod bi_map; +mod block_data; +mod block_path; +mod counter; +mod date_data; +mod date_map; +mod empty_address_data; +mod height_map; +mod liquidity; +mod map_value; +mod partial_txout_data; +mod price; +mod sent_data; +mod tx_data; +mod txout_index; +mod wamount; +mod wnaivedate; + +pub use address::*; +pub use address_data::*; +pub use address_realized_data::*; +pub use address_size::*; +pub use address_split::*; +pub use address_type::*; +pub use any_map::*; +pub use bi_map::*; +pub use block_data::*; +pub use block_path::*; +pub use counter::*; +pub use date_data::*; +pub use date_map::*; +pub use empty_address_data::*; +pub use height_map::*; +pub use liquidity::*; +pub use map_value::*; +pub use partial_txout_data::*; +pub use price::*; +pub use sent_data::*; +pub use tx_data::*; +pub use txout_index::*; +pub use wamount::*; +pub use wnaivedate::*; diff --git a/parser/src/structs/partial_txout_data.rs b/parser/src/structs/partial_txout_data.rs new file mode 100644 index 000000000..cdb5e1755 --- /dev/null +++ b/parser/src/structs/partial_txout_data.rs @@ -0,0 +1,18 @@ +use super::{Address, WAmount}; + +#[derive(Debug)] +pub struct PartialTxoutData { + pub amount: WAmount, + pub address: Option<Address>, + pub address_index_opt: Option<u32>, +} + +impl PartialTxoutData { + pub fn new(address: Option<Address>, amount: WAmount, address_index_opt: Option<u32>) -> Self { + Self { + address, + amount, + address_index_opt, + } + } +} diff --git a/parser/src/structs/price.rs b/parser/src/structs/price.rs new file mode 100644 index 000000000..94a3074e2 --- /dev/null +++ b/parser/src/structs/price.rs @@ -0,0 +1,93 @@ +use std::ops::{Add, AddAssign, Div, Mul, Sub, SubAssign}; + +use allocative::Allocative; +use bincode::{Decode, Encode}; + +use super::WAmount; + +#[derive( + Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, Allocative, +)] +pub struct Price(u64); + +const SIGNIFICANT_DIGITS: i32 = 3; + +impl Price { + pub const ZERO: Price = Price(0); + + pub fn to_cent(self) -> u64 { + self.0 + } + + pub fn to_dollar(self) -> f64 { + self.0 as f64 / 100.0 + } + + pub fn from_cent(cent: u64) -> Self { + Self(cent) + } + + pub fn from_dollar(dollar: f64) -> Self { + Self((dollar * 100.0) as u64) + } + + pub fn to_significant(self) -> Self { + let mut price = self; + + let ilog10 = price.0.checked_ilog10().unwrap_or(0) as i32; + + if ilog10 >= SIGNIFICANT_DIGITS { + let log_diff = ilog10 - SIGNIFICANT_DIGITS + 1; + + let pow = 10.0_f64.powi(log_diff); + + price = Price::from_cent(((price.0 as f64 / pow).round() * pow) as u64); + } + + price + } +} + +impl Add for Price { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl AddAssign for Price { + fn add_assign(&mut self, rhs: Self) { + self.0 += rhs.0; + } +} + +impl Sub for Price { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + Self(self.0 - rhs.0) + } +} + +impl SubAssign for Price { + fn sub_assign(&mut self, rhs: Self) { + self.0 -= rhs.0; + } +} + +impl Mul<WAmount> for Price { + type Output = Self; + + fn mul(self, rhs: WAmount) -> Self::Output { + Self((self.to_cent() as f64 * rhs.to_sat() as f64 / WAmount::ONE_BTC_F64).round() as u64) + } +} + +impl Div<WAmount> for Price { + type Output = Self; + + fn div(self, rhs: WAmount) -> Self::Output { + Self((self.to_cent() as f64 * WAmount::ONE_BTC_F64 / rhs.to_sat() as f64).round() as u64) + } +} diff --git a/parser/src/structs/sent_data.rs b/parser/src/structs/sent_data.rs new file mode 100644 index 000000000..717ec0ce1 --- /dev/null +++ b/parser/src/structs/sent_data.rs @@ -0,0 +1,14 @@ +use super::WAmount; + +#[derive(Default, Debug)] +pub struct SentData { + pub volume: WAmount, + pub count: u32, +} + +impl SentData { + pub fn send(&mut self, amount: WAmount) { + self.volume += amount; + self.count += 1; + } +} diff --git a/parser/src/structs/tx_data.rs b/parser/src/structs/tx_data.rs new file mode 100644 index 000000000..4aff1aed2 --- /dev/null +++ b/parser/src/structs/tx_data.rs @@ -0,0 +1,27 @@ +use allocative::Allocative; +use sanakirja::{direct_repr, Storable, UnsizedStorable}; + +use super::BlockPath; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Allocative)] +pub struct TxData { + pub index: u32, + pub block_path: BlockPath, + pub utxos: u16, +} +direct_repr!(TxData); + +impl TxData { + pub fn new(index: u32, block_path: BlockPath, utxos: u16) -> Self { + Self { + index, + block_path, + utxos, + } + } + + #[inline(always)] + pub fn is_empty(&self) -> bool { + self.utxos == 0 + } +} diff --git a/parser/src/structs/txout_index.rs b/parser/src/structs/txout_index.rs new file mode 100644 index 000000000..65bf8af47 --- /dev/null +++ b/parser/src/structs/txout_index.rs @@ -0,0 +1,28 @@ +use allocative::Allocative; +use bincode::{Decode, Encode}; +use sanakirja::{direct_repr, Storable, UnsizedStorable}; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Encode, Decode, Allocative)] +pub struct TxoutIndex { + pub tx_index: u32, + pub vout: u16, +} +direct_repr!(TxoutIndex); + +impl TxoutIndex { + pub fn new(tx_index: u32, vout: u16) -> Self { + Self { tx_index, vout } + } + + pub fn as_u64(&self) -> u64 { + ((self.tx_index as u64) << 16_u64) + self.vout as u64 + } +} + +impl std::hash::Hash for TxoutIndex { + fn hash<H: std::hash::Hasher>(&self, hasher: &mut H) { + hasher.write_u64(self.as_u64()) + } +} + +// impl nohash::IsEnabled for TxoutIndex {} diff --git a/parser/src/structs/wamount.rs b/parser/src/structs/wamount.rs new file mode 100644 index 000000000..a525a3020 --- /dev/null +++ b/parser/src/structs/wamount.rs @@ -0,0 +1,127 @@ +use std::{ + iter::Sum, + ops::{Add, AddAssign, Mul, Sub, SubAssign}, +}; + +use allocative::{Allocative, Visitor}; +use bincode::{ + de::{BorrowDecoder, Decoder}, + enc::Encoder, + error::{DecodeError, EncodeError}, + BorrowDecode, Decode, Encode, +}; +use bitcoin::Amount; +use derive_deref::{Deref, DerefMut}; +use sanakirja::{direct_repr, Storable, UnsizedStorable}; +use serde::{Deserialize, Serialize}; + +#[derive( + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Clone, + Copy, + Deref, + DerefMut, + Default, + Serialize, + Deserialize, +)] +pub struct WAmount(Amount); +direct_repr!(WAmount); + +impl WAmount { + pub const ZERO: Self = Self(Amount::ZERO); + pub const ONE_BTC_F64: f64 = 100_000_000.0; + + #[inline(always)] + pub fn wrap(amount: Amount) -> Self { + Self(amount) + } + + #[inline(always)] + pub fn from_sat(sats: u64) -> Self { + Self(Amount::from_sat(sats)) + } +} + +impl Add for WAmount { + type Output = WAmount; + + fn add(self, rhs: WAmount) -> Self::Output { + WAmount::from_sat(self.to_sat() + rhs.to_sat()) + } +} + +impl AddAssign for WAmount { + fn add_assign(&mut self, rhs: Self) { + *self = WAmount::from_sat(self.to_sat() + rhs.to_sat()); + } +} + +impl Sub for WAmount { + type Output = WAmount; + + fn sub(self, rhs: WAmount) -> Self::Output { + WAmount::from_sat(self.to_sat() - rhs.to_sat()) + } +} + +impl SubAssign for WAmount { + fn sub_assign(&mut self, rhs: Self) { + *self = WAmount::from_sat(self.to_sat() - rhs.to_sat()); + } +} + +impl Mul<WAmount> for WAmount { + type Output = WAmount; + + fn mul(self, rhs: WAmount) -> Self::Output { + WAmount::from_sat(self.to_sat() * rhs.to_sat()) + } +} + +impl Mul<u64> for WAmount { + type Output = WAmount; + + fn mul(self, rhs: u64) -> Self::Output { + WAmount::from_sat(self.to_sat() * rhs) + } +} + +impl Sum for WAmount { + fn sum<I: Iterator<Item = Self>>(iter: I) -> Self { + let sats = iter.map(|amt| amt.to_sat()).sum(); + WAmount::from_sat(sats) + } +} + +impl Encode for WAmount { + fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> { + Encode::encode(&self.to_sat(), encoder) + } +} + +impl Decode for WAmount { + fn decode<D: Decoder>(decoder: &mut D) -> core::result::Result<Self, DecodeError> { + let sats: u64 = Decode::decode(decoder)?; + + Ok(WAmount::from_sat(sats)) + } +} + +impl<'de> BorrowDecode<'de> for WAmount { + fn borrow_decode<D: BorrowDecoder<'de>>(decoder: &mut D) -> Result<Self, DecodeError> { + let sats: u64 = BorrowDecode::borrow_decode(decoder)?; + + Ok(WAmount::from_sat(sats)) + } +} + +impl Allocative for WAmount { + fn visit<'a, 'b: 'a>(&self, visitor: &'a mut Visitor<'b>) { + visitor.visit_simple_sized::<Self>(); + } +} diff --git a/parser/src/structs/wnaivedate.rs b/parser/src/structs/wnaivedate.rs new file mode 100644 index 000000000..c480046ad --- /dev/null +++ b/parser/src/structs/wnaivedate.rs @@ -0,0 +1,76 @@ +use std::{fmt, str::FromStr}; + +use allocative::{Allocative, Visitor}; +use bincode::{ + de::{BorrowDecoder, Decoder}, + enc::Encoder, + error::{DecodeError, EncodeError}, + BorrowDecode, Decode, Encode, +}; +use chrono::{NaiveDate, TimeZone, Utc}; +use derive_deref::{Deref, DerefMut}; +use serde::{Deserialize, Serialize}; + +#[derive( + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Clone, + Copy, + Deref, + DerefMut, + Default, + Serialize, + Deserialize, +)] +pub struct WNaiveDate(NaiveDate); + +impl WNaiveDate { + pub fn wrap(date: NaiveDate) -> Self { + Self(date) + } + + pub fn from_timestamp(timestamp: u32) -> Self { + Self( + Utc.timestamp_opt(i64::from(timestamp), 0) + .unwrap() + .date_naive(), + ) + } +} + +impl fmt::Display for WNaiveDate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(&self.0, f) + } +} + +impl Encode for WNaiveDate { + fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> { + Encode::encode(&self.to_string(), encoder) + } +} + +impl Decode for WNaiveDate { + fn decode<D: Decoder>(decoder: &mut D) -> core::result::Result<Self, DecodeError> { + let str: String = Decode::decode(decoder)?; + + Ok(Self(NaiveDate::from_str(&str).unwrap())) + } +} + +impl<'de> BorrowDecode<'de> for WNaiveDate { + fn borrow_decode<D: BorrowDecoder<'de>>(decoder: &mut D) -> Result<Self, DecodeError> { + let str: String = BorrowDecode::borrow_decode(decoder)?; + + Ok(Self(NaiveDate::from_str(&str).unwrap())) + } +} + +impl Allocative for WNaiveDate { + fn visit<'a, 'b: 'a>(&self, visitor: &'a mut Visitor<'b>) { + visitor.visit_simple_sized::<Self>(); + } +} diff --git a/parser/src/utils/arr.rs b/parser/src/utils/arr.rs new file mode 100644 index 000000000..80f91af42 --- /dev/null +++ b/parser/src/utils/arr.rs @@ -0,0 +1,219 @@ +use std::{ + iter::Sum, + ops::{Add, AddAssign, Div, Mul, Sub, SubAssign}, +}; + +use itertools::Itertools; +use ordered_float::{FloatCore, OrderedFloat}; + +use super::ToF32; + +pub trait ArrayOperations<T> { + fn transform<F>(&self, transform: F) -> Vec<T> + where + T: Copy + Default, + F: Fn((usize, &T, &[T])) -> T; + + fn add(&self, other: &[T]) -> Vec<T> + where + T: Add<Output = T> + Copy + Default; + + fn subtract(&self, other: &[T]) -> Vec<T> + where + T: Sub<Output = T> + Copy + Default; + + fn multiply(&self, other: &[T]) -> Vec<T> + where + T: Mul<Output = T> + Copy + Default; + + fn divide(&self, other: &[T]) -> Vec<T> + where + T: Div<Output = T> + Copy + Default; + + fn match_size<'a>(&'a self, other: &'a [T]) -> &'a [T]; + + fn cumulate(&self) -> Vec<T> + where + T: Sum + Copy + Default + AddAssign; + + fn last_x_sum(&self, x: usize) -> Vec<T> + where + T: Sum + Copy + Default + AddAssign + SubAssign; + + fn moving_average(&self, x: usize) -> Vec<f32> + where + T: Sum + Copy + Default + AddAssign + SubAssign + ToF32; + + fn net_change(&self, offset: usize) -> Vec<T> + where + T: Copy + Default + Sub<Output = T>; + + fn median(&self, size: usize) -> Vec<Option<T>> + where + T: FloatCore; +} + +impl<T> ArrayOperations<T> for &[T] { + fn transform<F>(&self, transform: F) -> Vec<T> + where + T: Copy + Default, + F: Fn((usize, &T, &[T])) -> T, + { + self.iter() + .enumerate() + .map(|(index, value)| transform((index, value, self))) + .collect_vec() + } + + fn add(&self, other: &[T]) -> Vec<T> + where + T: Add<Output = T> + Copy + Default, + { + self.match_size(other) + .transform(|(index, value, _)| *value + *other.get(index).unwrap()) + } + + fn subtract(&self, other: &[T]) -> Vec<T> + where + T: Sub<Output = T> + Copy + Default, + { + self.match_size(other) + .transform(|(index, value, _)| *value - *other.get(index).unwrap()) + } + + fn multiply(&self, other: &[T]) -> Vec<T> + where + T: Mul<Output = T> + Copy + Default, + { + self.match_size(other) + .transform(|(index, value, _)| *value * *other.get(index).unwrap()) + } + + fn divide(&self, other: &[T]) -> Vec<T> + where + T: Div<Output = T> + Copy + Default, + { + self.match_size(other) + .transform(|(index, value, _)| *value / *other.get(index).unwrap()) + } + + fn match_size(&self, other: &[T]) -> &[T] { + let len = other.len(); + if self.len() > len { + &self[..len] + } else { + self + } + } + + fn cumulate(&self) -> Vec<T> + where + T: Sum + Copy + Default + AddAssign, + { + let mut sum = T::default(); + + self.iter() + .map(|value| { + sum += *value; + sum + }) + .collect_vec() + } + + fn last_x_sum(&self, x: usize) -> Vec<T> + where + T: Sum + Copy + Default + AddAssign + SubAssign, + { + let mut sum = T::default(); + + self.iter() + .enumerate() + .map(|(index, value)| { + sum += *value; + + if index >= x - 1 { + let previous_index = index + 1 - x; + + sum -= *self.get(previous_index).unwrap() + } + + sum + }) + .collect_vec() + } + + fn moving_average(&self, x: usize) -> Vec<f32> + where + T: Sum + Copy + Default + AddAssign + SubAssign + ToF32, + { + let mut sum = T::default(); + + self.iter() + .enumerate() + .map(|(index, value)| { + sum += *value; + + if index >= x - 1 { + sum -= *self.get(index + 1 - x).unwrap() + } + + sum.to_f32() / x as f32 + }) + .collect_vec() + } + + fn net_change(&self, offset: usize) -> Vec<T> + where + T: Copy + Default + Sub<Output = T>, + { + self.transform(|(index, value, arr)| { + let previous = { + if let Some(previous_index) = index.checked_sub(offset) { + *arr.get(previous_index).unwrap() + } else { + T::default() + } + }; + + *value - previous + }) + } + + fn median(&self, size: usize) -> Vec<Option<T>> + where + T: FloatCore, + { + let even = size % 2 == 0; + let median_index = size / 2; + + if size < 3 { + panic!("Computing a median for a size lower than 3 is useless"); + } + + self.iter() + .enumerate() + .map(|(index, _)| { + if index >= size - 1 { + let mut arr = self[index - (size - 1)..index + 1] + .iter() + .map(|value| OrderedFloat(*value)) + .collect_vec(); + + arr.sort_unstable(); + + if even { + Some( + (**arr.get(median_index).unwrap() + + **arr.get(median_index - 1).unwrap()) + / T::from(2.0).unwrap(), + ) + } else { + Some(**arr.get(median_index).unwrap()) + } + } else { + None + } + }) + .collect() + } +} diff --git a/parser/src/utils/bytes.rs b/parser/src/utils/bytes.rs new file mode 100644 index 000000000..9f51f086b --- /dev/null +++ b/parser/src/utils/bytes.rs @@ -0,0 +1 @@ +pub const BYTES_IN_MB: usize = 1_000_000; diff --git a/parser/src/utils/date.rs b/parser/src/utils/date.rs new file mode 100644 index 000000000..e9e4dc6e4 --- /dev/null +++ b/parser/src/utils/date.rs @@ -0,0 +1,10 @@ +pub const ONE_DAY_IN_DAYS: usize = 1; +pub const ONE_WEEK_IN_DAYS: usize = 7; +pub const TWO_WEEK_IN_DAYS: usize = 2 * ONE_WEEK_IN_DAYS; +pub const ONE_MONTH_IN_DAYS: usize = 30; +pub const THREE_MONTHS_IN_DAYS: usize = 3 * ONE_MONTH_IN_DAYS; +pub const ONE_YEAR_IN_DAYS: usize = 365; + +pub const ONE_MINUTE_IN_S: usize = 60; +pub const ONE_HOUR_IN_S: usize = 60 * ONE_MINUTE_IN_S; +pub const ONE_DAY_IN_S: usize = 24 * ONE_HOUR_IN_S; diff --git a/parser/src/utils/flamegraph.rs b/parser/src/utils/flamegraph.rs new file mode 100644 index 000000000..7aa2641a2 --- /dev/null +++ b/parser/src/utils/flamegraph.rs @@ -0,0 +1,42 @@ +use std::{fs, path::PathBuf}; + +use chrono::Local; + +use crate::{databases::Databases, datasets::AllDatasets, states::States}; + +pub fn generate_allocation_files( + datasets: &AllDatasets, + databases: &Databases, + states: &States, + last_height: usize, +) -> color_eyre::Result<()> { + let mut flamegraph = allocative::FlameGraphBuilder::default(); + flamegraph.visit_root(datasets); + flamegraph.visit_root(databases); + flamegraph.visit_root(states); + let output = flamegraph.finish(); + + let folder = format!( + "at-{}-result-of-{}", + Local::now().format("%Y-%m-%d_%Hh%Mm%Ss"), + last_height + ); + + let path = PathBuf::from(&format!("./target/flamegraph/{folder}")); + fs::create_dir_all(&path)?; + + // fs::write(path.join("flamegraph.src"), &output.flamegraph())?; + + let mut fg_svg = Vec::new(); + inferno::flamegraph::from_reader( + &mut inferno::flamegraph::Options::default(), + output.flamegraph().write().as_bytes(), + &mut fg_svg, + )?; + + fs::write(path.join("flamegraph.svg"), &fg_svg)?; + + fs::write(path.join("warnings.txt"), output.warnings())?; + + Ok(()) +} diff --git a/parser/src/utils/log.rs b/parser/src/utils/log.rs new file mode 100644 index 000000000..7c657a202 --- /dev/null +++ b/parser/src/utils/log.rs @@ -0,0 +1,25 @@ +use std::process::Output; + +use chrono::Local; +use color_eyre::owo_colors::OwoColorize; + +#[inline(always)] +pub fn log(str: &str) { + let date_time = format!("{}", Local::now().format("%Y-%m-%d %H:%M:%S -")); + + str.lines() + .filter(|line| !line.is_empty()) + .for_each(|line| { + println!("{} {}", date_time.bright_black(), line); + }); +} + +pub fn log_output(output: &Output) { + if !output.stdout.is_empty() { + log(&String::from_utf8_lossy(&output.stdout)); + } + + if !output.stderr.is_empty() { + log(&String::from_utf8_lossy(&output.stderr)); + } +} diff --git a/parser/src/utils/lossy.rs b/parser/src/utils/lossy.rs new file mode 100644 index 000000000..d5849e680 --- /dev/null +++ b/parser/src/utils/lossy.rs @@ -0,0 +1,110 @@ +pub trait LossyFrom<T> { + fn lossy_from(x: T) -> Self; +} + +// --- +// u64 +// --- + +impl LossyFrom<u64> for u64 { + #[inline(always)] + fn lossy_from(x: u64) -> Self { + x + } +} + +impl LossyFrom<usize> for u64 { + #[inline(always)] + fn lossy_from(x: usize) -> Self { + x as u64 + } +} + +// --- +// usize +// --- + +impl LossyFrom<usize> for usize { + #[inline(always)] + fn lossy_from(x: usize) -> Self { + x + } +} + +impl LossyFrom<f32> for usize { + #[inline(always)] + fn lossy_from(x: f32) -> Self { + x.round() as usize + } +} + +// --- +// f32 +// --- + +impl LossyFrom<u32> for f32 { + #[inline(always)] + fn lossy_from(x: u32) -> Self { + x as f32 + } +} + +impl LossyFrom<u64> for f32 { + #[inline(always)] + fn lossy_from(x: u64) -> Self { + x as f32 + } +} + +impl LossyFrom<usize> for f32 { + #[inline(always)] + fn lossy_from(x: usize) -> Self { + x as f32 + } +} + +impl LossyFrom<f32> for f32 { + #[inline(always)] + fn lossy_from(x: f32) -> Self { + x + } +} + +impl LossyFrom<f64> for f32 { + #[inline(always)] + fn lossy_from(x: f64) -> Self { + x as f32 + } +} + +// --- +// f64 +// --- + +impl LossyFrom<u64> for f64 { + #[inline(always)] + fn lossy_from(x: u64) -> Self { + x as f64 + } +} + +impl LossyFrom<usize> for f64 { + #[inline(always)] + fn lossy_from(x: usize) -> Self { + x as f64 + } +} + +impl LossyFrom<f32> for f64 { + #[inline(always)] + fn lossy_from(x: f32) -> Self { + x as f64 + } +} + +impl LossyFrom<f64> for f64 { + #[inline(always)] + fn lossy_from(x: f64) -> Self { + x + } +} diff --git a/parser/src/utils/mod.rs b/parser/src/utils/mod.rs new file mode 100644 index 000000000..6ca7775cf --- /dev/null +++ b/parser/src/utils/mod.rs @@ -0,0 +1,15 @@ +mod bytes; +mod date; +mod flamegraph; +mod log; +mod lossy; +mod retry; +mod time; + +pub use bytes::*; +pub use date::*; +pub use flamegraph::*; +pub use log::*; +pub use lossy::*; +pub use retry::*; +pub use time::*; diff --git a/parser/src/utils/retry.rs b/parser/src/utils/retry.rs new file mode 100644 index 000000000..f5dee64b8 --- /dev/null +++ b/parser/src/utils/retry.rs @@ -0,0 +1,25 @@ +use std::{thread::sleep, time::Duration}; + +pub fn retry<T>( + function: impl Fn() -> color_eyre::Result<T>, + sleep_in_s: u64, + retries: u64, +) -> color_eyre::Result<T> { + if retries < 1 { + unreachable!() + } + + let mut i = 0; + + loop { + let res = function(); + + if i == retries || res.is_ok() { + return res; + } else { + sleep(Duration::from_secs(sleep_in_s)); + } + + i += 1; + } +} diff --git a/parser/src/utils/time.rs b/parser/src/utils/time.rs new file mode 100644 index 000000000..e1d471de1 --- /dev/null +++ b/parser/src/utils/time.rs @@ -0,0 +1,26 @@ +use std::time::Instant; + +use crate::utils::log; + +use super::ONE_DAY_IN_S; + +pub fn time<F, T>(name: &str, function: F) -> T +where + F: FnOnce() -> T, +{ + let time = Instant::now(); + + let returned = function(); + + log(&format!("{name}: {} seconds", time.elapsed().as_secs_f32())); + + returned +} + +pub fn difference_in_days_between_timestamps(older: u32, younger: u32) -> u32 { + if younger <= older { + 0 + } else { + (younger - older) / ONE_DAY_IN_S as u32 + } +} diff --git a/parser/stop.sh b/parser/stop.sh new file mode 100755 index 000000000..20ecf25dd --- /dev/null +++ b/parser/stop.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +# if [ "$(uname)" == "Darwin" ]; then +# if [ mdutil -s / | grep "disabled" ]; then +# sudo mdutil -a -i on +# fi +# fi + +bitcoin-cli -datadir=/Users/k/Developer/bitcoin stop diff --git a/server/.github/workflows/rust.yml b/server/.github/workflows/rust.yml new file mode 100644 index 000000000..31000a274 --- /dev/null +++ b/server/.github/workflows/rust.yml @@ -0,0 +1,22 @@ +name: Rust + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 000000000..05923927f --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,2 @@ +/target +.DS_Store diff --git a/server/Cargo.lock b/server/Cargo.lock new file mode 100644 index 000000000..9bb55aebf --- /dev/null +++ b/server/Cargo.lock @@ -0,0 +1,2666 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocative" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "082af274fd02beef17b7f0725a49ecafe6c075ef56cac9d6363eb3916a9817ae" +dependencies = [ + "allocative_derive", + "ctor", +] + +[[package]] +name = "allocative_derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe233a377643e0fc1a56421d7c90acdec45c291b30345eb9f08e8d0ddce5a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "async-compression" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a116f46a969224200a0a97f29cfd4c50e7534e4b4826bd23ea2c3c533039c82c" +dependencies = [ + "brotli", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "zstd", + "zstd-safe", +] + +[[package]] +name = "async-trait" +version = "0.1.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "axum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base58ck" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" +dependencies = [ + "bitcoin-internals", + "bitcoin_hashes", +] + +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + +[[package]] +name = "bech32" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" + +[[package]] +name = "bincode" +version = "2.0.0-rc.3" +source = "git+https://github.com/bincode-org/bincode.git#100685bc28fd3df957d622e7007d7293a3ca2b0b" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.0-rc.3" +source = "git+https://github.com/bincode-org/bincode.git#100685bc28fd3df957d622e7007d7293a3ca2b0b" +dependencies = [ + "virtue", +] + +[[package]] +name = "bitcoin" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea507acc1cd80fc084ace38544bbcf7ced7c2aa65b653b102de0ce718df668f6" +dependencies = [ + "base58ck", + "bech32", + "bitcoin-internals", + "bitcoin-io", + "bitcoin-units", + "bitcoin_hashes", + "hex-conservative", + "hex_lit", + "secp256k1", + "serde", +] + +[[package]] +name = "bitcoin-internals" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" +dependencies = [ + "serde", +] + +[[package]] +name = "bitcoin-io" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "340e09e8399c7bd8912f495af6aa58bea0c9214773417ffaa8f6460f93aaee56" + +[[package]] +name = "bitcoin-units" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb54da0b28892f3c52203a7191534033e051b6f4b52bc15480681b57b7e036f5" +dependencies = [ + "bitcoin-internals", + "serde", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +dependencies = [ + "bitcoin-io", + "hex-conservative", + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "brotli" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" + +[[package]] +name = "bytemuck" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" + +[[package]] +name = "cc" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.4", +] + +[[package]] +name = "clap" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_derive" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + +[[package]] +name = "color-eyre" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + +[[package]] +name = "condtype" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "crc32fast" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core 0.9.9", +] + +[[package]] +name = "db-key" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b72465f46d518f6015d9cf07f7f3013a95dd6b9c2747c3d65ae0cce43929d14f" + +[[package]] +name = "derive_deref" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcdbcee2d9941369faba772587a565f4f534e42cb8d17e5295871de730163b2b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "divan" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d567df2c9c2870a43f3f2bd65aaeb18dbce1c18f217c3e564b4fbaeb3ee56c" +dependencies = [ + "cfg-if", + "clap", + "condtype", + "divan-macros", + "libc", + "regex-lite", +] + +[[package]] +name = "divan-macros" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27540baf49be0d484d8f0130d7d8da3011c32a44d4fc873368154f1510e574a2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "ffi-opaque" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec54ac60a7f2ee9a97cad9946f9bf629a3bc6a7ae59e68983dc9318f5a54b81a" + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "h2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex-conservative" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1aa273bf451e37ed35ced41c71a5e2a4e29064afb104158f2514bcd71c2c986" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inferno" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321f0f839cd44a4686e9504b0a62b4d69a50b62072144c71c68f5873c167b8d9" +dependencies = [ + "ahash", + "clap", + "crossbeam-channel", + "crossbeam-utils", + "dashmap", + "env_logger", + "indexmap", + "is-terminal", + "itoa", + "log", + "num-format", + "once_cell", + "quick-xml", + "rgb", + "str_stack", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "is-terminal" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "jobserver" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "leveldb" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32651baaaa5596b3a6e0bee625e73fd0334c167db0ea5ac68750ef9a629a2d6a" +dependencies = [ + "db-key", + "leveldb-sys", + "libc", +] + +[[package]] +name = "leveldb-sys" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd94a4d0242a437e5e41a27c782b69a624469ca1c4d1e5cb3c337f74a8031d4" +dependencies = [ + "cmake", + "ffi-opaque", + "libc", + "num_cpus", +] + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "memmap2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +dependencies = [ + "libc", +] + +[[package]] +name = "memory-stats" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34f79cf9964c5c9545493acda1263f1912f8d2c56c8a2ffee2606cb960acaacc" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nohash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0f889fb66f7acdf83442c35775764b51fed3c606ab9cee51500dbde2cf528ca" + +[[package]] +name = "num-format" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" +dependencies = [ + "arrayvec", + "itoa", +] + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl" +version = "0.10.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-float" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76df7075c7d4d01fdcb46c912dd17fba5b60c78ea480b475f2b6ab6f666584e" +dependencies = [ + "num-traits", +] + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "par-iter-sync" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa981aaed94bf59211f644922155e8a33bcb01ed662cd63426653187f562790" +dependencies = [ + "crossbeam", + "num_cpus", +] + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.9", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.4.1", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "parser" +version = "0.1.0" +dependencies = [ + "allocative", + "bincode", + "bitcoin", + "bitcoin_hashes", + "byteorder", + "chrono", + "color-eyre", + "db-key", + "derive_deref", + "divan", + "fastrand", + "inferno", + "itertools 0.13.0", + "leveldb", + "memory-stats", + "nohash", + "ordered-float", + "par-iter-sync", + "rayon", + "reqwest", + "sanakirja", + "serde", + "serde_json", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "proc-macro2" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b661b2f27137bdbc16f00eda72866a92bb28af1753ffbd56744fb6e2e9cd8e" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "reqwest" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rgb" +version = "0.8.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.38.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebbbdb961df0ad3f2652da8f3fdc4b36122f568f968f45ad3316f26c025c677b" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" + +[[package]] +name = "rustls-webpki" +version = "0.102.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "sanakirja" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "450d6e757837c485e85fe8d5bd7aae9592da139a55036a4f64cec2b9984c6953" +dependencies = [ + "fs2", + "log", + "memmap2", + "parking_lot 0.11.2", + "sanakirja-core", + "serde", + "thiserror", +] + +[[package]] +name = "sanakirja-core" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8376db34ae3eac6e7bd91168bc638450073b708ce9fb46940de676f552238bf5" + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secp256k1" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0cc0f1cf93f4969faf3ea1c7d8a9faed25918d96affa959720823dfe86d4f3" +dependencies = [ + "bitcoin_hashes", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1433bd67156263443f14d603720b082dd3121779323fce20cba2aa07b874bc1b" +dependencies = [ + "cc", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "serde_json" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "server" +version = "0.1.0" +dependencies = [ + "axum", + "bincode", + "color-eyre", + "derive_deref", + "itertools 0.12.1", + "parser", + "regex", + "reqwest", + "serde", + "serde_json", + "tokio", + "tower-http", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "str_stack" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "terminal_size" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +dependencies = [ + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "thiserror" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot 0.12.1", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "async-compression", + "bitflags 2.5.0", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "unty" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a88342087869553c259588a3ec9ca73ce9b2d538b7051ba5789ff236b6c129" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "virtue" +version = "0.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b6826a786a78cf1bb0937507b5551fb6f827d66269a24b00af0de247b19bbc7" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.60", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zstd" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bffb3309596d527cfcba7dfc6ed6052f1d39dfbd7c867aa2e865e4a449c10110" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43747c7422e2924c11144d5229878b98180ef8b06cca4ab5af37afc8a8d8ea3e" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.9+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/server/Cargo.toml b/server/Cargo.toml new file mode 100644 index 000000000..7c5951f7f --- /dev/null +++ b/server/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "server" +version = "0.1.0" +edition = "2021" + +[dependencies] +axum = "0.7.5" +color-eyre = "0.6.3" +itertools = "0.12.1" +regex = "1.10.4" +bincode = { git = "https://github.com/bincode-org/bincode.git" } +reqwest = { version = "0.12.4", features = ["json"] } +serde = { version = "1.0.199", features = ["derive"] } +serde_json = { version = "1.0.116" } +tokio = { version = "1.37.0", features = ["full"] } +tower-http = { version = "0.5.2", features = ["compression-full"] } +parser = { path = "../parser" } +derive_deref = "1.1.1" diff --git a/server/README.md b/server/README.md new file mode 100644 index 000000000..072ec1880 --- /dev/null +++ b/server/README.md @@ -0,0 +1,19 @@ +# Satonomics - Server + +## Description + +A small server which automatically creates routes for all the created datasets + +## Requirements + +- `rustup` + +## Run + +```bash +# Install rustup +# Update ./run.sh if needed +./run.sh +``` + +Then the easiest to let others access your server is with `cloudflared` which will also cache requests. diff --git a/server/run.sh b/server/run.sh new file mode 100755 index 000000000..2b79330a8 --- /dev/null +++ b/server/run.sh @@ -0,0 +1,2 @@ +cargo watch -w "./src" -w "./run.sh" -x "run -r" +# cargo watch -w "./src" -w "./run.sh" -w "../datasets/disk_path_to_type.json" -x "run -r" diff --git a/server/src/chunk.rs b/server/src/chunk.rs new file mode 100644 index 000000000..c375f75c5 --- /dev/null +++ b/server/src/chunk.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Chunk { + pub id: usize, + pub previous: Option<String>, + pub next: Option<String>, +} diff --git a/server/src/handler.rs b/server/src/handler.rs new file mode 100644 index 000000000..56e1150d9 --- /dev/null +++ b/server/src/handler.rs @@ -0,0 +1,150 @@ +use axum::{ + extract::{Path, Query, State}, + http::HeaderMap, + response::{IntoResponse, Response}, +}; +use color_eyre::{eyre::eyre, owo_colors::OwoColorize}; +use reqwest::{header::HOST, StatusCode}; +use serde::Deserialize; + +use parser::{log, DateMap, HeightMap, WNaiveDate, HEIGHT_MAP_CHUNK_SIZE, OHLC}; + +use crate::{ + chunk::Chunk, headers::add_cors_to_headers, kind::Kind, response::typed_value_to_response, + AppState, +}; + +#[derive(Deserialize)] +pub struct Params { + chunk: Option<usize>, +} + +pub async fn file_handler( + headers: HeaderMap, + path: Path<String>, + query: Query<Params>, + State(app_state): State<AppState>, +) -> Response { + match _file_handler(headers, path, query, app_state) { + Ok(response) => response, + Err(error) => { + let mut response = + (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(); + + add_cors_to_headers(response.headers_mut()); + + response + } + } +} + +fn _file_handler( + headers: HeaderMap, + Path(path): Path<String>, + query: Query<Params>, + AppState { routes }: AppState, +) -> color_eyre::Result<Response> { + if path.contains("favicon") { + return Err(eyre!("Don't support favicon")); + } + + log(&format!( + "{}{}", + path, + query.chunk.map_or("".to_string(), |chunk| format!( + "{}{chunk}", + "?chunk=".bright_black() + )) + )); + + let date_prefix = "date-to-"; + let height_prefix = "height-to-"; + + let (kind, route) = if path.starts_with(date_prefix) { + ( + Kind::Date, + routes + .date + .get(&path.strip_prefix(date_prefix).unwrap().replace('-', "_")), + ) + } else if path.starts_with(height_prefix) { + ( + Kind::Height, + routes + .height + .get(&path.strip_prefix(height_prefix).unwrap().replace('-', "_")), + ) + } else { + (Kind::Last, routes.last.get(&path.replace('-', "_"))) + }; + + if route.is_none() { + return Err(eyre!("Path error")); + } + + let mut route = route.unwrap().to_owned(); + + let mut chunk = None; + + if kind != Kind::Last { + let datasets = match kind { + Kind::Date => DateMap::<usize>::_read_dir(&route.file_path, &route.serialization), + Kind::Height => HeightMap::<usize>::_read_dir(&route.file_path, &route.serialization), + _ => panic!(), + }; + + let (last_chunk_id, _) = datasets.last_key_value().unwrap(); + + let chunk_id = query.chunk.unwrap_or(*last_chunk_id); + + let path = datasets.get(&chunk_id); + + if path.is_none() { + return Err(eyre!("Couldn't find chunk")); + } + + route.file_path = path.unwrap().to_str().unwrap().to_string(); + + let offset = match kind { + Kind::Date => 1, + Kind::Height => HEIGHT_MAP_CHUNK_SIZE, + _ => panic!(), + }; + + let offsetted_to_url = |offseted| { + datasets.get(&offseted).map(|_| { + let host = headers[HOST].to_str().unwrap(); + let scheme = if host.contains("0.0.0.0") || host.contains("localhost") { + "http" + } else { + "https" + }; + + format!("{scheme}://{host}{}?chunk={offseted}", route.url_path) + }) + }; + + chunk = Some(Chunk { + id: chunk_id, + next: chunk_id.checked_add(offset).and_then(offsetted_to_url), + previous: chunk_id.checked_sub(offset).and_then(offsetted_to_url), + }) + } + + let type_name = route.values_type.split("::").last().unwrap(); + + let value = match type_name { + "u8" => typed_value_to_response::<u8>(kind, &route.file_path, chunk)?, + "u16" => typed_value_to_response::<u16>(kind, &route.file_path, chunk)?, + "u32" => typed_value_to_response::<u32>(kind, &route.file_path, chunk)?, + "u64" => typed_value_to_response::<u64>(kind, &route.file_path, chunk)?, + "usize" => typed_value_to_response::<usize>(kind, &route.file_path, chunk)?, + "f32" => typed_value_to_response::<f32>(kind, &route.file_path, chunk)?, + "f64" => typed_value_to_response::<f64>(kind, &route.file_path, chunk)?, + "OHLC" => typed_value_to_response::<OHLC>(kind, &route.file_path, chunk)?, + "WNaiveDate" => typed_value_to_response::<WNaiveDate>(kind, &route.file_path, chunk)?, + _ => panic!("Incompatible type: {type_name}"), + }; + + Ok(value) +} diff --git a/server/src/headers.rs b/server/src/headers.rs new file mode 100644 index 000000000..9c85dcf52 --- /dev/null +++ b/server/src/headers.rs @@ -0,0 +1,26 @@ +use axum::http::{header, HeaderMap}; + +const STALE_IF_ERROR: u64 = 604800; // 1 Week + +pub fn add_cors_to_headers(headers: &mut HeaderMap) { + headers.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*".parse().unwrap()); + headers.insert(header::ACCESS_CONTROL_ALLOW_HEADERS, "*".parse().unwrap()); +} + +pub fn add_json_type_to_headers(headers: &mut HeaderMap) { + headers.insert(header::CONTENT_TYPE, "application/json".parse().unwrap()); +} + +pub fn add_cache_control_to_headers( + headers: &mut HeaderMap, + max_age: u64, + stale_while_revalidate: u64, +) { + headers.insert( + header::CACHE_CONTROL, + format!( + "public, max-age={max_age}, stale-while-revalidate={stale_while_revalidate}, stale-if-error={STALE_IF_ERROR}") + .parse() + .unwrap(), + ); +} diff --git a/server/src/imports.rs b/server/src/imports.rs new file mode 100644 index 000000000..7a273e4ac --- /dev/null +++ b/server/src/imports.rs @@ -0,0 +1,27 @@ +use std::fmt::Debug; + +use bincode::Decode; +use parser::{Serialization, SerializedDateMap, SerializedHeightMap}; +use serde::{de::DeserializeOwned, Serialize}; + +pub fn import_map<T>(relative_path: &str) -> color_eyre::Result<SerializedDateMap<T>> +where + T: Serialize + Debug + DeserializeOwned + Decode, +{ + Serialization::from_extension(relative_path.split('.').last().unwrap()).import(relative_path) +} + +pub fn import_vec<T>(relative_path: &str) -> color_eyre::Result<SerializedHeightMap<T>> +where + T: Serialize + Debug + DeserializeOwned + Decode, +{ + Serialization::from_extension(relative_path.split('.').last().unwrap()).import(relative_path) +} + +pub fn import_value<T>(relative_path: &str) -> color_eyre::Result<T> +where + T: Serialize + Debug + DeserializeOwned + Decode, +{ + Serialization::from_extension(relative_path.split('.').last().unwrap()) + .import::<T>(relative_path) +} diff --git a/server/src/kind.rs b/server/src/kind.rs new file mode 100644 index 000000000..2e7f7ad29 --- /dev/null +++ b/server/src/kind.rs @@ -0,0 +1,6 @@ +#[derive(PartialEq, Eq)] +pub enum Kind { + Date, + Height, + Last, +} diff --git a/server/src/main.rs b/server/src/main.rs new file mode 100644 index 000000000..bb8191a6b --- /dev/null +++ b/server/src/main.rs @@ -0,0 +1,78 @@ +use std::sync::Arc; + +use axum::{extract::State, http::HeaderMap, response::Response, routing::get, serve, Router}; +use parser::log; +use reqwest::header::HOST; +use response::generic_to_reponse; +use routes::Routes; +use serde::Serialize; +use tokio::net::TcpListener; +use tower_http::compression::CompressionLayer; + +mod chunk; +mod handler; +mod headers; +mod imports; +mod kind; +mod paths; +mod response; +mod routes; + +use handler::file_handler; + +#[derive(Clone, Debug, Default, Serialize)] +pub struct Grouped<T> { + pub date: T, + pub height: T, + pub last: T, +} + +#[derive(Clone)] +pub struct AppState { + routes: Arc<Routes>, +} + +#[tokio::main] +async fn main() -> color_eyre::Result<()> { + color_eyre::install()?; + + let routes = Routes::build(); + + routes.generate_grouped_keys_to_url_path_file(); + + let state = AppState { + routes: Arc::new(routes), + }; + + let compression_layer = CompressionLayer::new() + .br(true) + .deflate(true) + .gzip(true) + .zstd(true); + + let router = Router::new() + .route("/*path", get(file_handler)) + .route("/", get(fallback)) + .with_state(state) + .layer(compression_layer); + + let port = 3110; + + log(&format!("Starting server on port {port}...")); + + let listener = TcpListener::bind(format!("0.0.0.0:{port}")).await?; + + serve(listener, router).await?; + + Ok(()) +} + +pub async fn fallback(headers: HeaderMap, State(app_state): State<AppState>) -> Response { + generic_to_reponse( + app_state + .routes + .to_full_paths(headers[HOST].to_str().unwrap().to_string()), + None, + 60, + ) +} diff --git a/server/src/paths.rs b/server/src/paths.rs new file mode 100644 index 000000000..1461e0138 --- /dev/null +++ b/server/src/paths.rs @@ -0,0 +1,9 @@ +use std::collections::BTreeMap; + +use derive_deref::{Deref, DerefMut}; +use serde::Serialize; + +use crate::Grouped; + +#[derive(Clone, Default, Deref, DerefMut, Debug, Serialize)] +pub struct Paths(pub Grouped<BTreeMap<String, String>>); diff --git a/server/src/response.rs b/server/src/response.rs new file mode 100644 index 000000000..566d55e8b --- /dev/null +++ b/server/src/response.rs @@ -0,0 +1,81 @@ +use std::fmt::Debug; + +use axum::response::{IntoResponse, Json, Response}; +use bincode::Decode; +use serde::de::DeserializeOwned; +use serde::Serialize; + +use crate::{ + chunk::Chunk, + headers::{add_cache_control_to_headers, add_cors_to_headers, add_json_type_to_headers}, + imports::{import_map, import_value, import_vec}, + kind::Kind, +}; + +#[derive(Serialize)] +struct WrappedDataset<'a, T> +where + T: Serialize, +{ + source: &'a str, + chunk: Chunk, + dataset: T, +} + +pub fn typed_value_to_response<T>( + kind: Kind, + relative_path: &str, + chunk: Option<Chunk>, +) -> color_eyre::Result<Response> +where + T: Serialize + Debug + DeserializeOwned + Decode, +{ + Ok(match kind { + Kind::Date => dataset_to_response(import_map::<T>(relative_path)?, chunk.unwrap()), + Kind::Height => dataset_to_response(import_vec::<T>(relative_path)?, chunk.unwrap()), + Kind::Last => value_to_response(import_value::<T>(relative_path)?), + }) +} + +fn value_to_response<T>(value: T) -> Response +where + T: Serialize, +{ + generic_to_reponse(value, None, 5) +} + +fn dataset_to_response<T>(dataset: T, chunk: Chunk) -> Response +where + T: Serialize, +{ + generic_to_reponse(dataset, Some(chunk), 60) +} + +pub fn generic_to_reponse<T>(generic: T, chunk: Option<Chunk>, cache_time: u64) -> Response +where + T: Serialize, +{ + let mut response = { + if let Some(chunk) = chunk { + Json(WrappedDataset { + source: "https://satonomics.xyz", + chunk, + dataset: generic, + }) + .into_response() + } else { + Json(generic).into_response() + } + }; + + let headers = response.headers_mut(); + + let max_age = cache_time; + let stale_while_revalidate = 2 * max_age; + + add_cors_to_headers(headers); + add_json_type_to_headers(headers); + add_cache_control_to_headers(headers, max_age, stale_while_revalidate); + + response +} diff --git a/server/src/routes.rs b/server/src/routes.rs new file mode 100644 index 000000000..e6bc26b0b --- /dev/null +++ b/server/src/routes.rs @@ -0,0 +1,145 @@ +use std::collections::{BTreeMap, HashMap}; + +use derive_deref::{Deref, DerefMut}; +use itertools::Itertools; +use parser::{Json, Serialization}; + +use crate::{paths::Paths, Grouped}; + +#[derive(Clone, Debug)] +pub struct Route { + pub url_path: String, + pub file_path: String, + pub values_type: String, + pub serialization: Serialization, +} + +#[derive(Clone, Default, Deref, DerefMut)] +pub struct Routes(pub Grouped<HashMap<String, Route>>); + +const DATASETS_PATH: &str = "../datasets_bkp"; + +impl Routes { + pub fn build() -> Self { + let path_to_type: BTreeMap<String, String> = + Json::import(&format!("{DATASETS_PATH}/disk_path_to_type.json")).unwrap(); + + let mut routes = Routes::default(); + + path_to_type.into_iter().for_each(|(key, value)| { + let mut split_key = key.split('/').collect_vec(); + + let mut split_last = split_key.pop().unwrap().split('.').rev().collect_vec(); + let last = split_last.pop().unwrap().to_owned(); + let serialization = split_last.pop().map_or_else( + || { + if *split_key.get(1).unwrap() == "price" { + Serialization::Json + } else { + Serialization::Binary + } + }, + Serialization::from_extension, + ); + let split_key = split_key.iter().skip(2).collect_vec(); + let map_key = split_key.iter().join("_"); + let url_path = split_key.iter().join("-"); + + let file_path = key.to_owned(); + let values_type = value.to_owned(); + + if last == "date" { + routes.date.insert( + map_key, + Route { + url_path: format!("/date-to-{url_path}"), + file_path, + values_type, + serialization, + }, + ); + } else if last == "height" { + routes.height.insert( + map_key, + Route { + url_path: format!("/height-to-{url_path}"), + file_path, + values_type, + serialization, + }, + ); + } else if last == "last" { + routes.last.insert( + map_key, + Route { + url_path: format!("/{url_path}"), + file_path, + values_type, + serialization, + }, + ); + } else { + dbg!(&key, value, &last); + panic!("") + } + }); + + routes + } + + pub fn generate_grouped_keys_to_url_path_file(&self) { + let transform = |map: &HashMap<String, Route>| -> BTreeMap<String, String> { + map.iter() + .map(|(key, route)| (key.to_owned(), route.url_path.to_owned())) + .collect() + }; + + let date_paths = transform(&self.date); + let height_paths = transform(&self.height); + let last_paths = transform(&self.last); + + let paths = Paths(Grouped { + date: date_paths, + height: height_paths, + last: last_paths, + }); + + let _ = Json::export( + &format!("{DATASETS_PATH}/grouped_keys_to_url_path.json"), + &paths, + ); + } + + pub fn to_full_paths(&self, host: String) -> Paths { + let url = { + let scheme = if host.contains("0.0.0.0") || host.contains("localhost") { + "http" + } else { + "https" + }; + + format!("{scheme}://{host}") + }; + + let transform = |map: &HashMap<String, Route>| -> BTreeMap<String, String> { + map.iter() + .map(|(key, route)| { + ( + key.to_owned(), + format!("{url}{}", route.url_path.to_owned()), + ) + }) + .collect() + }; + + let date_paths = transform(&self.date); + let height_paths = transform(&self.height); + let last_paths = transform(&self.last); + + Paths(Grouped { + date: date_paths, + height: height_paths, + last: last_paths, + }) + } +}