From a1a576d088c8f83ed32d48753a7611f70a964574 Mon Sep 17 00:00:00 2001 From: k Date: Sun, 23 Jun 2024 17:38:53 +0200 Subject: [PATCH] git: reset --- .gitignore | 7 + CHANGELOG.md | 39 + LICENSE.md | 21 + README.md | 59 + app/.gitignore | 9 + app/README.md | 10 + app/_redirects | 1 + app/index.html | 372 + app/package.json | 48 + app/pnpm-lock.yaml | 6333 +++++++++++++++++ app/prettier.config.mjs | 11 + app/public/assets/apple-icon-180.png | Bin 0 -> 4031 bytes app/public/assets/apple-splash-1125-2436.jpg | Bin 0 -> 25923 bytes app/public/assets/apple-splash-1136-640.jpg | Bin 0 -> 8232 bytes app/public/assets/apple-splash-1170-2532.jpg | Bin 0 -> 25835 bytes app/public/assets/apple-splash-1179-2556.jpg | Bin 0 -> 27766 bytes app/public/assets/apple-splash-1242-2208.jpg | Bin 0 -> 23427 bytes app/public/assets/apple-splash-1242-2688.jpg | Bin 0 -> 27559 bytes app/public/assets/apple-splash-1284-2778.jpg | Bin 0 -> 29479 bytes app/public/assets/apple-splash-1290-2796.jpg | Bin 0 -> 30129 bytes app/public/assets/apple-splash-1334-750.jpg | Bin 0 -> 10567 bytes app/public/assets/apple-splash-1488-2266.jpg | Bin 0 -> 28329 bytes app/public/assets/apple-splash-1536-2048.jpg | Bin 0 -> 27014 bytes app/public/assets/apple-splash-1620-2160.jpg | Bin 0 -> 29510 bytes app/public/assets/apple-splash-1640-2360.jpg | Bin 0 -> 32376 bytes app/public/assets/apple-splash-1668-2224.jpg | Bin 0 -> 31143 bytes app/public/assets/apple-splash-1668-2388.jpg | Bin 0 -> 32995 bytes app/public/assets/apple-splash-1792-828.jpg | Bin 0 -> 13869 bytes app/public/assets/apple-splash-2048-1536.jpg | Bin 0 -> 26572 bytes app/public/assets/apple-splash-2048-2732.jpg | Bin 0 -> 42649 bytes app/public/assets/apple-splash-2160-1620.jpg | Bin 0 -> 28982 bytes app/public/assets/apple-splash-2208-1242.jpg | Bin 0 -> 22998 bytes app/public/assets/apple-splash-2224-1668.jpg | Bin 0 -> 30695 bytes app/public/assets/apple-splash-2266-1488.jpg | Bin 0 -> 28035 bytes app/public/assets/apple-splash-2360-1640.jpg | Bin 0 -> 31737 bytes app/public/assets/apple-splash-2388-1668.jpg | Bin 0 -> 32589 bytes app/public/assets/apple-splash-2436-1125.jpg | Bin 0 -> 24396 bytes app/public/assets/apple-splash-2532-1170.jpg | Bin 0 -> 24674 bytes app/public/assets/apple-splash-2556-1179.jpg | Bin 0 -> 26101 bytes app/public/assets/apple-splash-2688-1242.jpg | Bin 0 -> 26715 bytes app/public/assets/apple-splash-2732-2048.jpg | Bin 0 -> 43061 bytes app/public/assets/apple-splash-2778-1284.jpg | Bin 0 -> 28615 bytes app/public/assets/apple-splash-2796-1290.jpg | Bin 0 -> 28409 bytes app/public/assets/apple-splash-640-1136.jpg | Bin 0 -> 8963 bytes app/public/assets/apple-splash-750-1334.jpg | Bin 0 -> 11232 bytes app/public/assets/apple-splash-828-1792.jpg | Bin 0 -> 14461 bytes .../assets/apple-splash-dark-1125-2436.jpg | Bin 0 -> 19969 bytes .../assets/apple-splash-dark-1136-640.jpg | Bin 0 -> 7123 bytes .../assets/apple-splash-dark-1170-2532.jpg | Bin 0 -> 21805 bytes .../assets/apple-splash-dark-1179-2556.jpg | Bin 0 -> 21249 bytes .../assets/apple-splash-dark-1242-2208.jpg | Bin 0 -> 20006 bytes .../assets/apple-splash-dark-1242-2688.jpg | Bin 0 -> 23516 bytes .../assets/apple-splash-dark-1284-2778.jpg | Bin 0 -> 25231 bytes .../assets/apple-splash-dark-1290-2796.jpg | Bin 0 -> 25821 bytes .../assets/apple-splash-dark-1334-750.jpg | Bin 0 -> 8998 bytes .../assets/apple-splash-dark-1488-2266.jpg | Bin 0 -> 24842 bytes .../assets/apple-splash-dark-1536-2048.jpg | Bin 0 -> 23342 bytes .../assets/apple-splash-dark-1620-2160.jpg | Bin 0 -> 25866 bytes .../assets/apple-splash-dark-1640-2360.jpg | Bin 0 -> 28247 bytes .../assets/apple-splash-dark-1668-2224.jpg | Bin 0 -> 27405 bytes .../assets/apple-splash-dark-1668-2388.jpg | Bin 0 -> 29145 bytes .../assets/apple-splash-dark-1792-828.jpg | Bin 0 -> 11945 bytes .../assets/apple-splash-dark-2048-1536.jpg | Bin 0 -> 23342 bytes .../assets/apple-splash-dark-2048-2732.jpg | Bin 0 -> 37775 bytes .../assets/apple-splash-dark-2160-1620.jpg | Bin 0 -> 25963 bytes .../assets/apple-splash-dark-2208-1242.jpg | Bin 0 -> 20299 bytes .../assets/apple-splash-dark-2224-1668.jpg | Bin 0 -> 27389 bytes .../assets/apple-splash-dark-2266-1488.jpg | Bin 0 -> 24838 bytes .../assets/apple-splash-dark-2360-1640.jpg | Bin 0 -> 28244 bytes .../assets/apple-splash-dark-2388-1668.jpg | Bin 0 -> 29169 bytes .../assets/apple-splash-dark-2436-1125.jpg | Bin 0 -> 19878 bytes .../assets/apple-splash-dark-2532-1170.jpg | Bin 0 -> 21911 bytes .../assets/apple-splash-dark-2556-1179.jpg | Bin 0 -> 20934 bytes .../assets/apple-splash-dark-2688-1242.jpg | Bin 0 -> 23812 bytes .../assets/apple-splash-dark-2732-2048.jpg | Bin 0 -> 38733 bytes .../assets/apple-splash-dark-2778-1284.jpg | Bin 0 -> 25667 bytes .../assets/apple-splash-dark-2796-1290.jpg | Bin 0 -> 25375 bytes .../assets/apple-splash-dark-640-1136.jpg | Bin 0 -> 7186 bytes .../assets/apple-splash-dark-750-1334.jpg | Bin 0 -> 9057 bytes .../assets/apple-splash-dark-828-1792.jpg | Bin 0 -> 11813 bytes app/public/assets/favicon-196.png | Bin 0 -> 4415 bytes .../assets/manifest-icon-192.maskable.png | Bin 0 -> 4031 bytes .../assets/manifest-icon-512.maskable.png | Bin 0 -> 41188 bytes app/public/fonts/Lexend.var.woff2 | Bin 0 -> 71484 bytes app/public/logo/black.svg | 17 + app/public/logo/white.svg | 17 + app/public/manifest.webmanifest | 37 + app/public/robots.txt | 2 + app/src/app/components/background.tsx | 175 + app/src/app/components/frames/box.tsx | 154 + app/src/app/components/frames/button.tsx | 13 + .../frames/chart/components/actions.tsx | 102 + .../frames/chart/components/chart.tsx | 33 + .../frames/chart/components/legend.tsx | 134 + .../frames/chart/components/timeScale.tsx | 67 + .../frames/chart/components/title.tsx | 12 + app/src/app/components/frames/chart/index.tsx | 72 + app/src/app/components/frames/counter.tsx | 25 + app/src/app/components/frames/favorites.tsx | 48 + app/src/app/components/frames/header.tsx | 8 + app/src/app/components/frames/history.tsx | 80 + app/src/app/components/frames/line.tsx | 90 + app/src/app/components/frames/number.tsx | 7 + app/src/app/components/frames/search.tsx | 318 + app/src/app/components/frames/settings.tsx | 37 + .../frames/tree/components/file.tsx | 47 + .../frames/tree/components/folder.tsx | 39 + .../frames/tree/components/tree.tsx | 117 + app/src/app/components/frames/tree/index.tsx | 86 + app/src/app/components/qrcode.tsx | 18 + .../components/strip/components/anchor.tsx | 9 + .../components/strip/components/anchorAPI.tsx | 26 + .../components/strip/components/anchorGit.tsx | 11 + .../strip/components/anchorHome.tsx | 32 + .../strip/components/anchorLogo.tsx | 36 + .../strip/components/anchorNostr.tsx | 24 + .../components/strip/components/button.tsx | 14 + .../strip/components/buttonChart.tsx | 27 + .../strip/components/buttonFavorites.tsx | 24 + .../strip/components/buttonHistory.tsx | 22 + .../strip/components/buttonRefresh.tsx | 10 + .../strip/components/buttonSearch.tsx | 24 + .../strip/components/buttonSettings.tsx | 64 + .../strip/components/buttonTree.tsx | 58 + .../components/strip/components/clickable.tsx | 36 + app/src/app/components/strip/index.tsx | 65 + app/src/app/index.tsx | 309 + app/src/app/scripts/register.ts | 67 + app/src/app/types.d.ts | 7 + app/src/env.ts | 3 + app/src/index.tsx | 18 + app/src/scripts/datasets/consts/address.ts | 30 + app/src/scripts/datasets/consts/age.ts | 147 + app/src/scripts/datasets/consts/averages.ts | 15 + .../scripts/datasets/consts/liquidities.ts | 11 + .../scripts/datasets/consts/percentiles.ts | 116 + app/src/scripts/datasets/consts/returns.ts | 14 + app/src/scripts/datasets/consts/types.d.ts | 19 + app/src/scripts/datasets/date.ts | 41 + app/src/scripts/datasets/height.ts | 36 + app/src/scripts/datasets/index.ts | 17 + app/src/scripts/datasets/resource.ts | 246 + app/src/scripts/datasets/types.d.ts | 98 + .../scripts/lightweightCharts/chart/clean.ts | 11 + .../scripts/lightweightCharts/chart/create.ts | 68 + .../chart/horzScaleBehavior.ts | 89 + .../lightweightCharts/chart/markers.ts | 123 + .../scripts/lightweightCharts/chart/price.ts | 176 + .../scripts/lightweightCharts/chart/render.ts | 40 + .../scripts/lightweightCharts/chart/state.ts | 10 + .../scripts/lightweightCharts/chart/time.ts | 110 + .../lightweightCharts/chart/types.d.ts | 9 + .../lightweightCharts/chart/whitespace.ts | 50 + .../lightweightCharts/series/creators/area.ts | 28 + .../series/creators/baseLine.ts | 52 + .../series/creators/candlesticks.ts | 42 + .../series/creators/histogram.ts | 30 + .../series/creators/legend.ts | 75 + .../lightweightCharts/series/creators/line.ts | 10 + .../series/creators/options.ts | 7 + .../series/creators/types.d.ts | 13 + .../series/options/priceScale.ts | 45 + .../series/options/types.d.ts | 3 + app/src/scripts/presets/addresses/index.ts | 218 + app/src/scripts/presets/blocks/index.ts | 221 + app/src/scripts/presets/coinblocks/index.ts | 1032 +++ app/src/scripts/presets/hodlers/index.ts | 127 + app/src/scripts/presets/index.ts | 296 + .../scripts/presets/market/averages/index.ts | 78 + app/src/scripts/presets/market/index.ts | 77 + .../presets/market/indicators/index.ts | 6 + .../scripts/presets/market/returns/index.ts | 79 + app/src/scripts/presets/miners/index.ts | 902 +++ app/src/scripts/presets/templates/cohort.ts | 985 +++ app/src/scripts/presets/templates/momentum.ts | 121 + app/src/scripts/presets/templates/multiple.ts | 183 + app/src/scripts/presets/templates/ratio.ts | 289 + app/src/scripts/presets/transactions/index.ts | 225 + app/src/scripts/presets/types.d.ts | 62 + app/src/scripts/utils/array.ts | 16 + app/src/scripts/utils/colors.ts | 246 + app/src/scripts/utils/date.ts | 10 + app/src/scripts/utils/debounce.ts | 19 + app/src/scripts/utils/history.ts | 12 + app/src/scripts/utils/id.ts | 3 + app/src/scripts/utils/locale.ts | 31 + app/src/scripts/utils/math/averages.ts | 22 + app/src/scripts/utils/math/random.ts | 5 + app/src/scripts/utils/math/round.ts | 4 + app/src/scripts/utils/math/sum.ts | 2 + app/src/scripts/utils/run.ts | 1 + app/src/scripts/utils/scroll.ts | 9 + app/src/scripts/utils/selectableList/index.ts | 119 + .../scripts/utils/selectableList/types.d.ts | 33 + app/src/scripts/utils/sleep.ts | 9 + app/src/scripts/utils/storage.ts | 19 + app/src/scripts/utils/time.ts | 8 + app/src/scripts/utils/urlParams.ts | 40 + app/src/scripts/ws/base.ts | 62 + app/src/scripts/ws/index.ts | 10 + app/src/scripts/ws/kraken.ts | 49 + app/src/scripts/ws/types.d.ts | 6 + app/src/solid/classes.ts | 12 + app/src/solid/rws.ts | 18 + app/src/solid/types/classes.d.ts | 1 + app/src/solid/types/library.d.ts | 41 + app/src/solid/types/rws.d.ts | 5 + app/src/styles.css | 26 + app/src/types/auto-imports.d.ts | 302 + app/src/types/lightweight-charts.d.ts | 65 + app/src/types/self.d.ts | 17 + app/tailwind.config.ts | 23 + app/tsconfig.json | 27 + app/vite.config.ts | 72 + maintainers.yaml | 4 + parser/.gitignore | 15 + parser/Cargo.lock | 2334 ++++++ parser/Cargo.toml | 31 + parser/README.md | 27 + parser/run.sh | 25 + parser/samply.sh | 8 + parser/src/actions/export.rs | 47 + parser/src/actions/iter_blocks.rs | 199 + parser/src/actions/min_height.rs | 127 + parser/src/actions/mod.rs | 9 + parser/src/actions/parse.rs | 981 +++ parser/src/bitcoin/addresses/mod.rs | 3 + parser/src/bitcoin/addresses/multisig.rs | 57 + parser/src/bitcoin/consts.rs | 2 + parser/src/bitcoin/daemon.rs | 122 + parser/src/bitcoin/db/blk_files.rs | 152 + parser/src/bitcoin/db/block_iter.rs | 45 + parser/src/bitcoin/db/blocks_indexes.rs | 211 + parser/src/bitcoin/db/errors.rs | 135 + parser/src/bitcoin/db/mod.rs | 172 + parser/src/bitcoin/db/reader.rs | 90 + parser/src/bitcoin/db/txdb.rs | 147 + parser/src/bitcoin/height.rs | 5 + parser/src/bitcoin/mod.rs | 11 + parser/src/databases/_database.rs | 235 + parser/src/databases/_trait.rs | 32 + .../address_index_to_address_data.rs | 148 + .../address_index_to_empty_address_data.rs | 123 + .../src/databases/address_to_address_index.rs | 309 + parser/src/databases/metadata.rs | 116 + parser/src/databases/mod.rs | 178 + parser/src/databases/txid_to_tx_data.rs | 147 + .../databases/txout_index_to_address_index.rs | 114 + parser/src/databases/txout_index_to_amount.rs | 114 + parser/src/datasets/_traits/any_dataset.rs | 286 + .../src/datasets/_traits/any_dataset_group.rs | 7 + parser/src/datasets/_traits/any_datasets.rs | 9 + .../src/datasets/_traits/min_initial_state.rs | 272 + parser/src/datasets/_traits/mod.rs | 9 + parser/src/datasets/address/all_metadata.rs | 91 + parser/src/datasets/address/cohort.rs | 703 ++ .../src/datasets/address/cohort_metadata.rs | 67 + parser/src/datasets/address/mod.rs | 151 + parser/src/datasets/block_metadata.rs | 61 + parser/src/datasets/coindays.rs | 68 + parser/src/datasets/cointime.rs | 594 ++ parser/src/datasets/constant.rs | 52 + parser/src/datasets/date_metadata.rs | 63 + parser/src/datasets/mining.rs | 643 ++ parser/src/datasets/mod.rs | 340 + parser/src/datasets/price/mod.rs | 493 ++ parser/src/datasets/price/ohlc.rs | 12 + parser/src/datasets/subs/capitalization.rs | 121 + parser/src/datasets/subs/input.rs | 70 + parser/src/datasets/subs/mod.rs | 80 + parser/src/datasets/subs/output.rs | 103 + parser/src/datasets/subs/price_paid.rs | 293 + parser/src/datasets/subs/realized.rs | 178 + parser/src/datasets/subs/supply.rs | 114 + parser/src/datasets/subs/unrealized.rs | 211 + parser/src/datasets/subs/utxo.rs | 63 + parser/src/datasets/transaction.rs | 257 + parser/src/datasets/utxo/dataset.rs | 287 + parser/src/datasets/utxo/mod.rs | 162 + parser/src/io/binary.rs | 43 + parser/src/io/consts.rs | 2 + parser/src/io/json.rs | 37 + parser/src/io/mod.rs | 11 + parser/src/io/path.rs | 3 + parser/src/io/serialization.rs | 55 + parser/src/lib.rs | 21 + parser/src/main.rs | 41 + parser/src/price/binance.rs | 201 + parser/src/price/kraken.rs | 124 + parser/src/price/mod.rs | 5 + parser/src/states/_trait.rs | 47 + .../address/cohort_durable_states.rs | 411 ++ .../cohorts_states/address/cohort_id.rs | 68 + .../address/cohorts_durable_states.rs | 143 + .../address/cohorts_input_states.rs | 48 + .../address/cohorts_one_shot_states.rs | 8 + .../address/cohorts_output_states.rs | 48 + .../address/cohorts_realized_states.rs | 48 + .../src/states/cohorts_states/address/mod.rs | 17 + .../address/split_by_address_cohort.rs | 177 + .../any/capitalization_state.rs | 18 + .../cohorts_states/any/durable_states.rs | 55 + .../states/cohorts_states/any/input_state.rs | 14 + parser/src/states/cohorts_states/any/mod.rs | 23 + .../cohorts_states/any/one_shot_states.rs | 9 + .../states/cohorts_states/any/output_state.rs | 14 + .../cohorts_states/any/price_paid_state.rs | 210 + .../cohorts_states/any/price_to_value.rs | 123 + .../cohorts_states/any/realized_state.rs | 14 + .../states/cohorts_states/any/supply_state.rs | 27 + .../cohorts_states/any/unrealized_state.rs | 38 + .../states/cohorts_states/any/utxo_state.rs | 25 + parser/src/states/cohorts_states/mod.rs | 7 + .../utxo/cohort_durable_states.rs | 107 + .../cohorts_states/utxo/cohort_filter.rs | 32 + .../cohorts_states/utxo/cohort_filters.rs | 84 + .../states/cohorts_states/utxo/cohort_id.rs | 119 + .../utxo/cohorts_durable_states.rs | 154 + .../utxo/cohorts_one_shot_states.rs | 8 + .../utxo/cohorts_sent_states.rs | 68 + parser/src/states/cohorts_states/utxo/mod.rs | 17 + .../utxo/split_by_utxo_cohort.rs | 740 ++ parser/src/states/counters.rs | 29 + parser/src/states/date_data_vec.rs | 48 + parser/src/states/mod.rs | 95 + parser/src/structs/address.rs | 133 + parser/src/structs/address_data.rs | 112 + parser/src/structs/address_realized_data.rs | 46 + parser/src/structs/address_size.rs | 32 + parser/src/structs/address_split.rs | 11 + parser/src/structs/address_type.rs | 21 + parser/src/structs/any_map.rs | 22 + parser/src/structs/bi_map.rs | 341 + parser/src/structs/block_data.rs | 41 + parser/src/structs/block_path.rs | 25 + parser/src/structs/counter.rs | 28 + parser/src/structs/date_data.rs | 20 + parser/src/structs/date_map.rs | 1256 ++++ parser/src/structs/empty_address_data.rs | 25 + parser/src/structs/height_map.rs | 918 +++ parser/src/structs/liquidity.rs | 178 + parser/src/structs/map_value.rs | 22 + parser/src/structs/mod.rs | 49 + parser/src/structs/partial_txout_data.rs | 18 + parser/src/structs/price.rs | 93 + parser/src/structs/sent_data.rs | 14 + parser/src/structs/tx_data.rs | 27 + parser/src/structs/txout_index.rs | 28 + parser/src/structs/wamount.rs | 127 + parser/src/structs/wnaivedate.rs | 76 + parser/src/utils/arr.rs | 219 + parser/src/utils/bytes.rs | 1 + parser/src/utils/date.rs | 10 + parser/src/utils/flamegraph.rs | 42 + parser/src/utils/log.rs | 25 + parser/src/utils/lossy.rs | 110 + parser/src/utils/mod.rs | 15 + parser/src/utils/retry.rs | 25 + parser/src/utils/time.rs | 26 + parser/stop.sh | 9 + server/.github/workflows/rust.yml | 22 + server/.gitignore | 2 + server/Cargo.lock | 2666 +++++++ server/Cargo.toml | 18 + server/README.md | 19 + server/run.sh | 2 + server/src/chunk.rs | 8 + server/src/handler.rs | 150 + server/src/headers.rs | 26 + server/src/imports.rs | 27 + server/src/kind.rs | 6 + server/src/main.rs | 78 + server/src/paths.rs | 9 + server/src/response.rs | 81 + server/src/routes.rs | 145 + 375 files changed, 40952 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 app/.gitignore create mode 100644 app/README.md create mode 100644 app/_redirects create mode 100644 app/index.html create mode 100644 app/package.json create mode 100644 app/pnpm-lock.yaml create mode 100644 app/prettier.config.mjs create mode 100644 app/public/assets/apple-icon-180.png create mode 100644 app/public/assets/apple-splash-1125-2436.jpg create mode 100644 app/public/assets/apple-splash-1136-640.jpg create mode 100644 app/public/assets/apple-splash-1170-2532.jpg create mode 100644 app/public/assets/apple-splash-1179-2556.jpg create mode 100644 app/public/assets/apple-splash-1242-2208.jpg create mode 100644 app/public/assets/apple-splash-1242-2688.jpg create mode 100644 app/public/assets/apple-splash-1284-2778.jpg create mode 100644 app/public/assets/apple-splash-1290-2796.jpg create mode 100644 app/public/assets/apple-splash-1334-750.jpg create mode 100644 app/public/assets/apple-splash-1488-2266.jpg create mode 100644 app/public/assets/apple-splash-1536-2048.jpg create mode 100644 app/public/assets/apple-splash-1620-2160.jpg create mode 100644 app/public/assets/apple-splash-1640-2360.jpg create mode 100644 app/public/assets/apple-splash-1668-2224.jpg create mode 100644 app/public/assets/apple-splash-1668-2388.jpg create mode 100644 app/public/assets/apple-splash-1792-828.jpg create mode 100644 app/public/assets/apple-splash-2048-1536.jpg create mode 100644 app/public/assets/apple-splash-2048-2732.jpg create mode 100644 app/public/assets/apple-splash-2160-1620.jpg create mode 100644 app/public/assets/apple-splash-2208-1242.jpg create mode 100644 app/public/assets/apple-splash-2224-1668.jpg create mode 100644 app/public/assets/apple-splash-2266-1488.jpg create mode 100644 app/public/assets/apple-splash-2360-1640.jpg create mode 100644 app/public/assets/apple-splash-2388-1668.jpg create mode 100644 app/public/assets/apple-splash-2436-1125.jpg create mode 100644 app/public/assets/apple-splash-2532-1170.jpg create mode 100644 app/public/assets/apple-splash-2556-1179.jpg create mode 100644 app/public/assets/apple-splash-2688-1242.jpg create mode 100644 app/public/assets/apple-splash-2732-2048.jpg create mode 100644 app/public/assets/apple-splash-2778-1284.jpg create mode 100644 app/public/assets/apple-splash-2796-1290.jpg create mode 100644 app/public/assets/apple-splash-640-1136.jpg create mode 100644 app/public/assets/apple-splash-750-1334.jpg create mode 100644 app/public/assets/apple-splash-828-1792.jpg create mode 100644 app/public/assets/apple-splash-dark-1125-2436.jpg create mode 100644 app/public/assets/apple-splash-dark-1136-640.jpg create mode 100644 app/public/assets/apple-splash-dark-1170-2532.jpg create mode 100644 app/public/assets/apple-splash-dark-1179-2556.jpg create mode 100644 app/public/assets/apple-splash-dark-1242-2208.jpg create mode 100644 app/public/assets/apple-splash-dark-1242-2688.jpg create mode 100644 app/public/assets/apple-splash-dark-1284-2778.jpg create mode 100644 app/public/assets/apple-splash-dark-1290-2796.jpg create mode 100644 app/public/assets/apple-splash-dark-1334-750.jpg create mode 100644 app/public/assets/apple-splash-dark-1488-2266.jpg create mode 100644 app/public/assets/apple-splash-dark-1536-2048.jpg create mode 100644 app/public/assets/apple-splash-dark-1620-2160.jpg create mode 100644 app/public/assets/apple-splash-dark-1640-2360.jpg create mode 100644 app/public/assets/apple-splash-dark-1668-2224.jpg create mode 100644 app/public/assets/apple-splash-dark-1668-2388.jpg create mode 100644 app/public/assets/apple-splash-dark-1792-828.jpg create mode 100644 app/public/assets/apple-splash-dark-2048-1536.jpg create mode 100644 app/public/assets/apple-splash-dark-2048-2732.jpg create mode 100644 app/public/assets/apple-splash-dark-2160-1620.jpg create mode 100644 app/public/assets/apple-splash-dark-2208-1242.jpg create mode 100644 app/public/assets/apple-splash-dark-2224-1668.jpg create mode 100644 app/public/assets/apple-splash-dark-2266-1488.jpg create mode 100644 app/public/assets/apple-splash-dark-2360-1640.jpg create mode 100644 app/public/assets/apple-splash-dark-2388-1668.jpg create mode 100644 app/public/assets/apple-splash-dark-2436-1125.jpg create mode 100644 app/public/assets/apple-splash-dark-2532-1170.jpg create mode 100644 app/public/assets/apple-splash-dark-2556-1179.jpg create mode 100644 app/public/assets/apple-splash-dark-2688-1242.jpg create mode 100644 app/public/assets/apple-splash-dark-2732-2048.jpg create mode 100644 app/public/assets/apple-splash-dark-2778-1284.jpg create mode 100644 app/public/assets/apple-splash-dark-2796-1290.jpg create mode 100644 app/public/assets/apple-splash-dark-640-1136.jpg create mode 100644 app/public/assets/apple-splash-dark-750-1334.jpg create mode 100644 app/public/assets/apple-splash-dark-828-1792.jpg create mode 100644 app/public/assets/favicon-196.png create mode 100644 app/public/assets/manifest-icon-192.maskable.png create mode 100644 app/public/assets/manifest-icon-512.maskable.png create mode 100644 app/public/fonts/Lexend.var.woff2 create mode 100644 app/public/logo/black.svg create mode 100644 app/public/logo/white.svg create mode 100644 app/public/manifest.webmanifest create mode 100644 app/public/robots.txt create mode 100644 app/src/app/components/background.tsx create mode 100644 app/src/app/components/frames/box.tsx create mode 100644 app/src/app/components/frames/button.tsx create mode 100644 app/src/app/components/frames/chart/components/actions.tsx create mode 100644 app/src/app/components/frames/chart/components/chart.tsx create mode 100644 app/src/app/components/frames/chart/components/legend.tsx create mode 100644 app/src/app/components/frames/chart/components/timeScale.tsx create mode 100644 app/src/app/components/frames/chart/components/title.tsx create mode 100644 app/src/app/components/frames/chart/index.tsx create mode 100644 app/src/app/components/frames/counter.tsx create mode 100644 app/src/app/components/frames/favorites.tsx create mode 100644 app/src/app/components/frames/header.tsx create mode 100644 app/src/app/components/frames/history.tsx create mode 100644 app/src/app/components/frames/line.tsx create mode 100644 app/src/app/components/frames/number.tsx create mode 100644 app/src/app/components/frames/search.tsx create mode 100644 app/src/app/components/frames/settings.tsx create mode 100644 app/src/app/components/frames/tree/components/file.tsx create mode 100644 app/src/app/components/frames/tree/components/folder.tsx create mode 100644 app/src/app/components/frames/tree/components/tree.tsx create mode 100644 app/src/app/components/frames/tree/index.tsx create mode 100644 app/src/app/components/qrcode.tsx create mode 100644 app/src/app/components/strip/components/anchor.tsx create mode 100644 app/src/app/components/strip/components/anchorAPI.tsx create mode 100644 app/src/app/components/strip/components/anchorGit.tsx create mode 100644 app/src/app/components/strip/components/anchorHome.tsx create mode 100644 app/src/app/components/strip/components/anchorLogo.tsx create mode 100644 app/src/app/components/strip/components/anchorNostr.tsx create mode 100644 app/src/app/components/strip/components/button.tsx create mode 100644 app/src/app/components/strip/components/buttonChart.tsx create mode 100644 app/src/app/components/strip/components/buttonFavorites.tsx create mode 100644 app/src/app/components/strip/components/buttonHistory.tsx create mode 100644 app/src/app/components/strip/components/buttonRefresh.tsx create mode 100644 app/src/app/components/strip/components/buttonSearch.tsx create mode 100644 app/src/app/components/strip/components/buttonSettings.tsx create mode 100644 app/src/app/components/strip/components/buttonTree.tsx create mode 100644 app/src/app/components/strip/components/clickable.tsx create mode 100644 app/src/app/components/strip/index.tsx create mode 100644 app/src/app/index.tsx create mode 100644 app/src/app/scripts/register.ts create mode 100644 app/src/app/types.d.ts create mode 100644 app/src/env.ts create mode 100644 app/src/index.tsx create mode 100644 app/src/scripts/datasets/consts/address.ts create mode 100644 app/src/scripts/datasets/consts/age.ts create mode 100644 app/src/scripts/datasets/consts/averages.ts create mode 100644 app/src/scripts/datasets/consts/liquidities.ts create mode 100644 app/src/scripts/datasets/consts/percentiles.ts create mode 100644 app/src/scripts/datasets/consts/returns.ts create mode 100644 app/src/scripts/datasets/consts/types.d.ts create mode 100644 app/src/scripts/datasets/date.ts create mode 100644 app/src/scripts/datasets/height.ts create mode 100644 app/src/scripts/datasets/index.ts create mode 100644 app/src/scripts/datasets/resource.ts create mode 100644 app/src/scripts/datasets/types.d.ts create mode 100644 app/src/scripts/lightweightCharts/chart/clean.ts create mode 100644 app/src/scripts/lightweightCharts/chart/create.ts create mode 100644 app/src/scripts/lightweightCharts/chart/horzScaleBehavior.ts create mode 100644 app/src/scripts/lightweightCharts/chart/markers.ts create mode 100644 app/src/scripts/lightweightCharts/chart/price.ts create mode 100644 app/src/scripts/lightweightCharts/chart/render.ts create mode 100644 app/src/scripts/lightweightCharts/chart/state.ts create mode 100644 app/src/scripts/lightweightCharts/chart/time.ts create mode 100644 app/src/scripts/lightweightCharts/chart/types.d.ts create mode 100644 app/src/scripts/lightweightCharts/chart/whitespace.ts create mode 100644 app/src/scripts/lightweightCharts/series/creators/area.ts create mode 100644 app/src/scripts/lightweightCharts/series/creators/baseLine.ts create mode 100644 app/src/scripts/lightweightCharts/series/creators/candlesticks.ts create mode 100644 app/src/scripts/lightweightCharts/series/creators/histogram.ts create mode 100644 app/src/scripts/lightweightCharts/series/creators/legend.ts create mode 100644 app/src/scripts/lightweightCharts/series/creators/line.ts create mode 100644 app/src/scripts/lightweightCharts/series/creators/options.ts create mode 100644 app/src/scripts/lightweightCharts/series/creators/types.d.ts create mode 100644 app/src/scripts/lightweightCharts/series/options/priceScale.ts create mode 100644 app/src/scripts/lightweightCharts/series/options/types.d.ts create mode 100644 app/src/scripts/presets/addresses/index.ts create mode 100644 app/src/scripts/presets/blocks/index.ts create mode 100644 app/src/scripts/presets/coinblocks/index.ts create mode 100644 app/src/scripts/presets/hodlers/index.ts create mode 100644 app/src/scripts/presets/index.ts create mode 100644 app/src/scripts/presets/market/averages/index.ts create mode 100644 app/src/scripts/presets/market/index.ts create mode 100644 app/src/scripts/presets/market/indicators/index.ts create mode 100644 app/src/scripts/presets/market/returns/index.ts create mode 100644 app/src/scripts/presets/miners/index.ts create mode 100644 app/src/scripts/presets/templates/cohort.ts create mode 100644 app/src/scripts/presets/templates/momentum.ts create mode 100644 app/src/scripts/presets/templates/multiple.ts create mode 100644 app/src/scripts/presets/templates/ratio.ts create mode 100644 app/src/scripts/presets/transactions/index.ts create mode 100644 app/src/scripts/presets/types.d.ts create mode 100644 app/src/scripts/utils/array.ts create mode 100644 app/src/scripts/utils/colors.ts create mode 100644 app/src/scripts/utils/date.ts create mode 100644 app/src/scripts/utils/debounce.ts create mode 100644 app/src/scripts/utils/history.ts create mode 100644 app/src/scripts/utils/id.ts create mode 100644 app/src/scripts/utils/locale.ts create mode 100644 app/src/scripts/utils/math/averages.ts create mode 100644 app/src/scripts/utils/math/random.ts create mode 100644 app/src/scripts/utils/math/round.ts create mode 100644 app/src/scripts/utils/math/sum.ts create mode 100644 app/src/scripts/utils/run.ts create mode 100644 app/src/scripts/utils/scroll.ts create mode 100644 app/src/scripts/utils/selectableList/index.ts create mode 100644 app/src/scripts/utils/selectableList/types.d.ts create mode 100644 app/src/scripts/utils/sleep.ts create mode 100644 app/src/scripts/utils/storage.ts create mode 100644 app/src/scripts/utils/time.ts create mode 100644 app/src/scripts/utils/urlParams.ts create mode 100644 app/src/scripts/ws/base.ts create mode 100644 app/src/scripts/ws/index.ts create mode 100644 app/src/scripts/ws/kraken.ts create mode 100644 app/src/scripts/ws/types.d.ts create mode 100644 app/src/solid/classes.ts create mode 100644 app/src/solid/rws.ts create mode 100644 app/src/solid/types/classes.d.ts create mode 100644 app/src/solid/types/library.d.ts create mode 100644 app/src/solid/types/rws.d.ts create mode 100644 app/src/styles.css create mode 100644 app/src/types/auto-imports.d.ts create mode 100644 app/src/types/lightweight-charts.d.ts create mode 100644 app/src/types/self.d.ts create mode 100644 app/tailwind.config.ts create mode 100644 app/tsconfig.json create mode 100644 app/vite.config.ts create mode 100644 maintainers.yaml create mode 100644 parser/.gitignore create mode 100644 parser/Cargo.lock create mode 100644 parser/Cargo.toml create mode 100644 parser/README.md create mode 100755 parser/run.sh create mode 100755 parser/samply.sh create mode 100644 parser/src/actions/export.rs create mode 100644 parser/src/actions/iter_blocks.rs create mode 100644 parser/src/actions/min_height.rs create mode 100644 parser/src/actions/mod.rs create mode 100644 parser/src/actions/parse.rs create mode 100644 parser/src/bitcoin/addresses/mod.rs create mode 100644 parser/src/bitcoin/addresses/multisig.rs create mode 100644 parser/src/bitcoin/consts.rs create mode 100644 parser/src/bitcoin/daemon.rs create mode 100644 parser/src/bitcoin/db/blk_files.rs create mode 100644 parser/src/bitcoin/db/block_iter.rs create mode 100644 parser/src/bitcoin/db/blocks_indexes.rs create mode 100644 parser/src/bitcoin/db/errors.rs create mode 100644 parser/src/bitcoin/db/mod.rs create mode 100644 parser/src/bitcoin/db/reader.rs create mode 100644 parser/src/bitcoin/db/txdb.rs create mode 100644 parser/src/bitcoin/height.rs create mode 100644 parser/src/bitcoin/mod.rs create mode 100644 parser/src/databases/_database.rs create mode 100644 parser/src/databases/_trait.rs create mode 100644 parser/src/databases/address_index_to_address_data.rs create mode 100644 parser/src/databases/address_index_to_empty_address_data.rs create mode 100644 parser/src/databases/address_to_address_index.rs create mode 100644 parser/src/databases/metadata.rs create mode 100644 parser/src/databases/mod.rs create mode 100644 parser/src/databases/txid_to_tx_data.rs create mode 100644 parser/src/databases/txout_index_to_address_index.rs create mode 100644 parser/src/databases/txout_index_to_amount.rs create mode 100644 parser/src/datasets/_traits/any_dataset.rs create mode 100644 parser/src/datasets/_traits/any_dataset_group.rs create mode 100644 parser/src/datasets/_traits/any_datasets.rs create mode 100644 parser/src/datasets/_traits/min_initial_state.rs create mode 100644 parser/src/datasets/_traits/mod.rs create mode 100644 parser/src/datasets/address/all_metadata.rs create mode 100644 parser/src/datasets/address/cohort.rs create mode 100644 parser/src/datasets/address/cohort_metadata.rs create mode 100644 parser/src/datasets/address/mod.rs create mode 100644 parser/src/datasets/block_metadata.rs create mode 100644 parser/src/datasets/coindays.rs create mode 100644 parser/src/datasets/cointime.rs create mode 100644 parser/src/datasets/constant.rs create mode 100644 parser/src/datasets/date_metadata.rs create mode 100644 parser/src/datasets/mining.rs create mode 100644 parser/src/datasets/mod.rs create mode 100644 parser/src/datasets/price/mod.rs create mode 100644 parser/src/datasets/price/ohlc.rs create mode 100644 parser/src/datasets/subs/capitalization.rs create mode 100644 parser/src/datasets/subs/input.rs create mode 100644 parser/src/datasets/subs/mod.rs create mode 100644 parser/src/datasets/subs/output.rs create mode 100644 parser/src/datasets/subs/price_paid.rs create mode 100644 parser/src/datasets/subs/realized.rs create mode 100644 parser/src/datasets/subs/supply.rs create mode 100644 parser/src/datasets/subs/unrealized.rs create mode 100644 parser/src/datasets/subs/utxo.rs create mode 100644 parser/src/datasets/transaction.rs create mode 100644 parser/src/datasets/utxo/dataset.rs create mode 100644 parser/src/datasets/utxo/mod.rs create mode 100644 parser/src/io/binary.rs create mode 100644 parser/src/io/consts.rs create mode 100644 parser/src/io/json.rs create mode 100644 parser/src/io/mod.rs create mode 100644 parser/src/io/path.rs create mode 100644 parser/src/io/serialization.rs create mode 100644 parser/src/lib.rs create mode 100644 parser/src/main.rs create mode 100644 parser/src/price/binance.rs create mode 100644 parser/src/price/kraken.rs create mode 100644 parser/src/price/mod.rs create mode 100644 parser/src/states/_trait.rs create mode 100644 parser/src/states/cohorts_states/address/cohort_durable_states.rs create mode 100644 parser/src/states/cohorts_states/address/cohort_id.rs create mode 100644 parser/src/states/cohorts_states/address/cohorts_durable_states.rs create mode 100644 parser/src/states/cohorts_states/address/cohorts_input_states.rs create mode 100644 parser/src/states/cohorts_states/address/cohorts_one_shot_states.rs create mode 100644 parser/src/states/cohorts_states/address/cohorts_output_states.rs create mode 100644 parser/src/states/cohorts_states/address/cohorts_realized_states.rs create mode 100644 parser/src/states/cohorts_states/address/mod.rs create mode 100644 parser/src/states/cohorts_states/address/split_by_address_cohort.rs create mode 100644 parser/src/states/cohorts_states/any/capitalization_state.rs create mode 100644 parser/src/states/cohorts_states/any/durable_states.rs create mode 100644 parser/src/states/cohorts_states/any/input_state.rs create mode 100644 parser/src/states/cohorts_states/any/mod.rs create mode 100644 parser/src/states/cohorts_states/any/one_shot_states.rs create mode 100644 parser/src/states/cohorts_states/any/output_state.rs create mode 100644 parser/src/states/cohorts_states/any/price_paid_state.rs create mode 100644 parser/src/states/cohorts_states/any/price_to_value.rs create mode 100644 parser/src/states/cohorts_states/any/realized_state.rs create mode 100644 parser/src/states/cohorts_states/any/supply_state.rs create mode 100644 parser/src/states/cohorts_states/any/unrealized_state.rs create mode 100644 parser/src/states/cohorts_states/any/utxo_state.rs create mode 100644 parser/src/states/cohorts_states/mod.rs create mode 100644 parser/src/states/cohorts_states/utxo/cohort_durable_states.rs create mode 100644 parser/src/states/cohorts_states/utxo/cohort_filter.rs create mode 100644 parser/src/states/cohorts_states/utxo/cohort_filters.rs create mode 100644 parser/src/states/cohorts_states/utxo/cohort_id.rs create mode 100644 parser/src/states/cohorts_states/utxo/cohorts_durable_states.rs create mode 100644 parser/src/states/cohorts_states/utxo/cohorts_one_shot_states.rs create mode 100644 parser/src/states/cohorts_states/utxo/cohorts_sent_states.rs create mode 100644 parser/src/states/cohorts_states/utxo/mod.rs create mode 100644 parser/src/states/cohorts_states/utxo/split_by_utxo_cohort.rs create mode 100644 parser/src/states/counters.rs create mode 100644 parser/src/states/date_data_vec.rs create mode 100644 parser/src/states/mod.rs create mode 100644 parser/src/structs/address.rs create mode 100644 parser/src/structs/address_data.rs create mode 100644 parser/src/structs/address_realized_data.rs create mode 100644 parser/src/structs/address_size.rs create mode 100644 parser/src/structs/address_split.rs create mode 100644 parser/src/structs/address_type.rs create mode 100644 parser/src/structs/any_map.rs create mode 100644 parser/src/structs/bi_map.rs create mode 100644 parser/src/structs/block_data.rs create mode 100644 parser/src/structs/block_path.rs create mode 100644 parser/src/structs/counter.rs create mode 100644 parser/src/structs/date_data.rs create mode 100644 parser/src/structs/date_map.rs create mode 100644 parser/src/structs/empty_address_data.rs create mode 100644 parser/src/structs/height_map.rs create mode 100644 parser/src/structs/liquidity.rs create mode 100644 parser/src/structs/map_value.rs create mode 100644 parser/src/structs/mod.rs create mode 100644 parser/src/structs/partial_txout_data.rs create mode 100644 parser/src/structs/price.rs create mode 100644 parser/src/structs/sent_data.rs create mode 100644 parser/src/structs/tx_data.rs create mode 100644 parser/src/structs/txout_index.rs create mode 100644 parser/src/structs/wamount.rs create mode 100644 parser/src/structs/wnaivedate.rs create mode 100644 parser/src/utils/arr.rs create mode 100644 parser/src/utils/bytes.rs create mode 100644 parser/src/utils/date.rs create mode 100644 parser/src/utils/flamegraph.rs create mode 100644 parser/src/utils/log.rs create mode 100644 parser/src/utils/lossy.rs create mode 100644 parser/src/utils/mod.rs create mode 100644 parser/src/utils/retry.rs create mode 100644 parser/src/utils/time.rs create mode 100755 parser/stop.sh create mode 100644 server/.github/workflows/rust.yml create mode 100644 server/.gitignore create mode 100644 server/Cargo.lock create mode 100644 server/Cargo.toml create mode 100644 server/README.md create mode 100755 server/run.sh create mode 100644 server/src/chunk.rs create mode 100644 server/src/handler.rs create mode 100644 server/src/headers.rs create mode 100644 server/src/imports.rs create mode 100644 server/src/kind.rs create mode 100644 server/src/main.rs create mode 100644 server/src/paths.rs create mode 100644 server/src/response.rs create mode 100644 server/src/routes.rs 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 0000000000000000000000000000000000000000..b0913281f11cc2d8f3232a43ad81c5ef4e5ca8c8 GIT binary patch literal 4031 zcmYLMdpuNI`_~O6mkuF`aWA*|wMUn68;0CEQ52zg({4~CNsPfH3MFPn)Go%W%d1Z0 zFs`}G$q6&r8Jx;1qFnammNJ?#88g4_)bITM*n92ITI>6)wf6cx&+~n<4|=;RZ_wEw zCnu-u>EY^&wlyn{qCEO`cqI>@&8j$GccR?WZrw>aIaRW!tMlQ+qS<2h(;1IV9ZxFM zX=>#)TG)zroF>HlagKv(!XFvX0yd_h%ylAm=?LF=6tSrM#>+aOIr~2p6 zBk-7m#virYW=CSe84tsIFHU(pIMl6N zz^;}(QxU}1jXrvDymCK98Z_l}6Z_i{-9s;&?F9FAM2zk?&9CFL=n+BwPR~1{?1sZV zqZC>~)t&a zF8Tj=GdJplGZHU>%n0oCFm%P zJUIOSvm1L{M(W$1dLSMcmKGowFG*Yjy76Wp*mVzkJ@~fpS4&QFbOjgAPhBw?ng$o; zj%BoBAfq%mgbVXaoRH|g%djH{j$Kgyei^L$9V4jQc7T){lLP|1Kjk5;6Y@X&+TtE9 z<2YlQ*c|Mpe5xo&aQ=A|S1O3?6|PTl27*`i<+umZA`*UqvYn783~21pipAWuKxhH{ zI8z>SMn09mx}A4=Fmb(?;X&a;Zjq=bY~3aJ6qP0(0+EKYx&kF29X>_T1Xw9khH>R; zY58W^e;8{@tDdueExfVUqcb+&n0Rxo8d%6J7l(t{cxZ$yGfLv8=W%>xNy|Hy$LdQC z3;KOCTO|7m*oDu^ryII09yd?f9^{tPd-j{qkFxKblR|JGbv4vgcLTn6-fcBBcRl8` zp}@OR_+JNBOAn6M|31ek@9vu0PRNi@2)`X|-vH>d)H%sF;SHW{8*~jgL;FZf)hX-F zF$Ot$aFnHqJxvqeMlX1XT>v}Z=XxCjX@Tuk2S}}77CwQkB1cwZ0|z58uye45C=}AR z`o=b^Fj`|CT$DF?h}&G(xne?-CeBi>k14k|ga%~WA@}#0tyhAduHhUr!-f8FyN7Sa z`WB8WZ*N}>QKDY*79zmb`ta^iJrf%FYm5nwchZPhQUaejqduaY2Dhx-k^%;|6imY# zb;+f{HEVmkNGe~vSLWO&@a)|%aahPtH6<1Z7Q!agIh!~``$^1Bh4m?^KxM^|b=J-B_7Gt$y@zBt2?0F{XBE(yKHS=n=kI7Q!j0t^M8 zn1RU22Tq71Ah1`FNqV1f>?USBv6Sq8pSMXmcz#0uqyB!+gM$wRFV8&)S|-Q3?POv) zxYy$YKlV}g=o=2$NmF8P3hH%arO7t0AO00;3y#fkLIaMQORH7>I>NC$4~>3MdcES6 zW?gp_YbCX`D~buYat=Ib=WxNj(k0C9urOS>TZ%9%22f9|o^3fMqNwLDz7AjF=-di@eu|loDulc^+bA5 zsk2z&Wx!cZE-$~Bj(IejyBzxcd%cdALalkad?a9DczSD{NTHUnT{ikY=biX*Gj)C# z=TbNS7T3k~(Wof#J8GMIRS456UV@G>ymC<`_4&wp$UUV@b>5T29CzdGNKOUPdrpoQ zz{ygs74%K{2*(xm=-44~QQIveoT7tU2)9}7odwkElE?4?;&c^C{1)fI+}qtQC?>n- zEH_6lDWjPIjVN7oEc;>)B8!+{vWXzrv?c&3YcTM`o8UTo7mL~i^eqYa4vq!X?@(h0 z+#k_4s6?IqBGWH11Q@gK)J8GHh)OVI^lH?^P1v4b&f8&6SXq^h+R;Nda3q;_16{}L zUt()_63>&QmXy=Xz|WOtbGAZ%)!#D+78~PuC1rf63P#oNH|Kr39z9|lJoeR~M~fhH zT5HZIR>t^*T?{dg@w>>Z+50W=CKv^kGt(RT6caAUZQ_*Y=FvZ~OQ`-#NkKcYl+_U2el{@Uh|He) zkZWxgFRwL;t$UKsK78vM)r}OGN#oBA$Ar%z*m6<6eT+$NTiNWV*AA>eY(H2r8a(K} z{C?v!llD1>^-o78a zfXe>-9FF(ikDQt$7S4N-nBK}6j%GiWCk=-#TXqg`i-vEYp5jTm^Gd#yci<;ZRO#6D zF%xc3bs&atbrHgb459NsRNKbDryNiO3j6@US04tPkfLz#-MJBIm~NGZLhnebAkshY zjYi(xyJAAr(E4M$@TWUN)N7oKym-k~W#(54LLa=0{muHJ;OY&*v|u+KzaYDJsQmkN zN!G#w1By-*qzBM%`o%UKj3RFegm1Fi#w#rg!x(SCe$TSvu{~NR;(MRw*X@zENKSox z_}8v+RL9gw3R<(0wZm$k+4RSTONzxOw>Xyl4$e}Lip8*m);>d^H@`=X*PFD?NUV3AJDWqgy!r8NrJCDLkQsERE_2 z;gwH^eJLUug|7ypOLajdGq5xE1OAH1(s>6QXc6PUs>=@rGEbeO6{8_z>{Fum$3~9s&bygt{Lvy( zOyZvTY*=9*KKCkLgP9Lw7SQ+;H|&z{4rE5HbMZ%6(b|T_w;%merNs=gI7%KgM?+S_ zbV;2Z{wI-NyHHF7)x^J}94~VlvExft zMG*tyryJJj|K#%H%!^(KhAszjEg-V=*AVVTr=QH<3GFO%=a@lcJ;GPe>KFgn`b8B? zG#x)PN3}D{&tEPf$P6spgMx1OD(D_JelmSgzAxo{L*qca&K9I1cXRby#UT&m_t#Yk zj?ujRjf7Od4iKh86I+$9Y9<fD%u;-xvL z*i46a*%`90*z?GDexyr8vU%=&(Z{oHb$t_C*HneFxAw<)EP&qp{&ByH{P(-~V=bzL z|A<;eI|HX)VF{&s*xxVQg`baLb!uOgwc6ec88D@Z9f7?66B(2UBCommm%uaWa0au{ zjWkS1gA;@>vKq>xK_hV&e$fHnzW7~}bF5pS2aR+unbE`;$mH3^b%4H69ABn!8Qv9k zR0*`^lk;G*sxp9;R)8u41y<;|0ZyUSC4$!ImE)X&Y)XS8fqEL*z=kvAgG@MdVu~tr@4TwAblKnvyU~897o*Sx(o|wdXklwhY!<__PlqF5929f`x zQR1h)9u&GnysMk9(K5xApvp6kL57d3(8Ss(jc4#z=uQafu8$Ee4R)WRKSi5Pj{Ou% z7bDmS#0{x)h9cKF`}WSP14MKvdT}hX@}4!LIq|DfE+n&!R9dC+hbC}Ka-{DYIo;eh zdwe#Qv*yLM0MG7@k)ph4nFcu`H{HDkFYT7>>i?T2%i=RU_W`y5e+9#07Tq1`HcdYL zxsd}4h12&BiR{rUsp1FM>yxEl9yeAwuyX7=5I*|PF@opz1a;I0)6vgVY0&hS^Qa0? zQM(gWzFnTP5g7SVl5mpO0dd=G6%Xw8*I0u}tO`!^-%{&L>|FmxRAboM{DTP2U%8r% zz;uIX`kW(r)U6q&TP?1wLK%1;MSZ7p;I(p4*9x#1^&+% zc{Zt`^$ zVIx!zD&7dbt=mJrvb@hO>w=a6jvsT!3O!SkDbB3AxMvE)ug5~Y=_j zjpO6u8~^4{;U_94{QtcY+B?J*r`$2FkQi6tMpY3*m5=g%TqQzFhJ~OkDH;fbgq8@4 z2nlzGAgxqbiRH)1394M8X zge+HCW|>GZD>M~VQjw0uEfV~gDzqJ2igK@Z>XL5I4pJ^4Yezn|zoDD$;`HPCSk`UGSsJJ48$ay-cRqLdMjixsboaQwOxl-p7UQpz%rvXB_3;@!+p;Ss2(BBY$aD$b+g@m1jjWr8)> zxQa&~x*q^Eo`+ePirIPba6#?87Yba9r-lO<<$$zQ1TUzh+rU6@haz014X1524p|0@ z6e@>jp@IwxU;-GA1lrRbyT>J`~Up$baOFwPcBWI72Pt-#MraB|Q(Wk9MT z*$(+7pTi)+hi>ijhv$b+JN?pdYyIC@9n=UnY%&0M&}AZL$wt8=0Ll%bL;!sSItd++ zinCZ(VeXKN1Q1pcNMM%Hh=qF(S~5sZCIBRjC>8Y;1`+L>ndDZbS!@zpd(gyEfgrIe z23bNUVL_10W@LyvYTyIXp<1*Ha>;~1GmToBLIe!ejj^0bku=w#sP~@y6qEBt*V*sq z%&B74eHO(ASq~Emo+K(BrTnM?>Ht|y4djCc^M8>V3E^mjL?HdZZw?{dC_o+xQ~~6+ zXNwOy)Y~>>MRtb`D@u?a&T4dUZ{?C8-TEEi&CG21bOUV3FL?|_GxdS$u_tgZXNEf?N*6_j zESFZJNWcaFJ1qmE@{tZxCSXjPWi-VgB9cHpfY}Ks7sw=URPoe(7RZI;8_Xga1@O8c zO^xIs_MB!(D5^W;fUhk1PPtoYzrSuy zm8`7wQ>iY5F*tV~eR1a0RtDiz3vRR#=Rv|kLe#CL+(I$ZibN2JFaV*5H~|g(b7%>g zX*Gm~;#Cc=YMtJ=^_a?yRSyr~ZQimt6KIdx%^npR)V)jFZdeu>hdV*fr3_2tUZG2O zJfIB&-_t@kt2JiaH66_Iq2$V<6_hb`UyE%Fr;tfc8*b`1oiVpxRBA zY1Qr$CKO;A0;+kJFg0Zv8XYC3()O200Z;^YwGUMBH=yG8V~{`&rGAo=1L;lprLWxDPWFcRY1VAq&ht_D%K)rred&iz5ZKHOzT4kaelqxr- zhlzLnTd!k0hVS zKv-aTcmZpLTarM7SxBHYueD*n_!yuSTjc2UFo1&$&`OY=I6xGzF_tswRV9!!3wcqW z2O3}3ctd-4+7|?$YC~%qV)2_r8M?|sIV78Cf~o?_0E`N?fqPNC?eP=S&J0t^M2h}K zG=Z3q0wkFVh4}$HGt28K(shQX7*fJi7SPD@ptu-%P>~9)-G)XFEZ|QNkbp$cEE(=b zD;DO3M4+v*tB4jtJ~_{(NO+KHg{wHeIu)S{;s$KsEAtL1+v|l5rs~xAQa*97DS^-c zHzsoc9f~Hr_J}n!MUX9O7ow{H;l`ViVN+Z$pS zCY)ps3l##qA>qtl4(hb|#0=Yx6kTT(YZ6;bt3C7J16RJkw+KfdaoN2O>WUI1*hfR8 z7x~;PPQY;Q!a5LzgmGzk#%zL{i}*%~%c8*{oDa<*UZN_~ao4>Z0<(x23Zqb9rtzUa zN_-i}i$=na8SJ+^1W{oQhmcLYnuIZD*71Yk`#;xh_=Ww_iMMT)LO@Yk7)lcikFQUxL9_ta#~9t2FUQ;2I5sN7CbS9an1gPGv9nQ}f82y6C3> z51=VhPM5<`??9O9T|5?2gg%EXWH`V*g!&1F@tj69(a2BUkLdCJE-9*4@wkQ*c2?$4&x_0EG5u5Gn|g zCzwa9^;7*AL>U0_1&!h0aS<7M3P@;|Pv#tuV$9s|4_OOl5mz0Xun{+aEhfNNDq5-Vi?Ll$*p^!8|ytZhFZ9c@QR-%^(6p=p^L-ys=|z;0C?qgM{cI zO)pjQ+2ExbL|BA}=Oeh=F(Y#53=tdc3_+AEdVGOV4823aQOKpW5ONr!%8Cuk+}e?Z z7GiU5O`EJBf)u#MSrz~wg|_Emz z3t0Z-&@5yJ72(S56&WtowBJCHp=dblagPn1a$$75ga$3t450ITPnIvyA4nAe8N$b5 z9nCAqw|r`qkzPvFCvX%sIkc&UVO621>0O@ZdL39F!h>8BUqUBQ)5102jaJ2!Hi#Sn zkhCo?wT~En@4~fM9nvF6WFE!p=2T%1WuOVe!dzqU|8__>AB~Yqbx7xkfwoE@h5=?S zA~NF*`pkk^0DA~s6wR)QDHY$xWb=R)KZwo|P!j?Mk;A0&5X+USl~OyQr7Y2)K1mkq zH{jJOpgC-SI8g0U#R8Ae;nP7#H%GkQi19A5;8L+X{=f zf>wNfq8)Ks;wPGb4+W`hqB_h*8;>GP0@yb*VeU!~)bLxU<-{Qe z9}|Bf8$JiXw-#UkAkgR&9b|bl0%S7HyFdk-GfIp;(%?ZO$mm00E!4%=R%GCNz}x%_ zqC&Hid5t3LMfPq@C|65~wQ#J7gam&kogmbn&Vc#)eE&ejs;td@g$(({! zOf+Gy=Ten%UhxqQ0b^j&{^hMYfduI+!$J*F?nZGOBD%_}f0X7>bR^ctYk&31)qn&n7>Q*6($L8NYV#Uea%f;Qp=kC9Ox2x4PLqJvyhp|kJ+E19(I)j(}PaDE|l-)W2JV3*p0jnBawq_dU?$okF$q_|Tk; z>v}m%9&S*2`Gj_fW4MH@l3fJ)NeI1M3{h?bu-7t&}@0B9{;P;n=qsOZCt47aKiyw%6@#b2C2Lc4Nxr4bL6Ao_+1u zS^MYkt2;I~KRomHpSthO_@GjsV_y`i&4sF$lj!qZ!{PPre0S{hi+3__H<_|EzWkk# zDw!KHwiK#2uU#JAzj^tuZ#C_{?$Bd7IR&@o-oNNn`7d9}n6~`f+AZr6Z^XU6yiJp4 zcV^@)T;8VRkhp{fIbbJYG#7o*7!ry;3>Z+a`cfpKVqTZ_;4xs_fGVYUpazt2$&)oewS)s@i>z zjQIONA69J}FSwNY(__uUjHvW>&KVPEO8aYBa;iMkV&^y$8khVjt-2x&o};RKK{P^Slh@twHM!*XjOR_4V0X9 z`Snw4n{VrUwcE2Zj=njj(QDTSHaoNZtsSS=?rZv|DeIpInY~xdM+a_v6U3cEN>&sC zf+}qf3>X9O2e;meL|Kcc$^rx10q#PE2#c_^DiV~?g!r8qGEl<4ZnkTlP_JF(W^c9d z?CLjkTK_tC9!$$Sa5FEx)9o%dW+qQK_fq$TGvg-jJ^k>PRp0D%;lz^GeRA@)F36a+ zrr**kJ4^jEq~Yr^hcD!HYS^-OPVQTg)8>{dQqZW=vW8tocPYqhnck$%zU>Daqzw7= z_EY(>PwzWBdCuJOU%t`m^2NhXZ~xQWim*_lxq%%pjMo>z6Bk3mUI4k>K$tQ{3tJq$e*Mv=kd*J1y!BGQ zE3cQyi1}d8$%0)+*Yuvd{PgYHV=`vXy)~v__J-{3ZNJ=d?edTxt`({`P7GU|S#WB_ zm4z$IcC5B-=b6M#S$WWMr>b3iGyibo(Hn0!nQYhixJsq2kFHM7ne<*2o%1;XHjQaA zy}i?yTRx6l zJgd#Q%$g~E+X-C5VN$K;zZ$N3p^-E%aG%@Ne9ddP-#hsi>~8J)HTu@o-EJ7zH}>d& z`p-5TVh_j8-$eL;{39|Sl^AEf^qHW1tpM10GnFbYG z-)KGUuz zHSGS=jev^^ePJNM-$3{I9-y+*hsq}*Ej(gWIb6Nx0-O+B!qAw=a{MdVmI_S`_V>mm zX6ebmba*&-h8PbdWHuz^Kym>_pd-M}l z5B^l`y{G)5^s(K}k6E<5gD<&rK$DeYyDe<}^@kTYD?AMALRHyC!}yUlbwFf$tTvLN zD#hP*Mabu=+m4P0KCnt+#(6cFfkxP#Zxa}+Xw|xu`o3oUkRff-OU>WaeR-R-X-97_ zK9qL)^sSu5%P);-GjG$?=gPyi_}e>`KFS;Z?&S}b9N*b(#uvjf-buZ&t>cPE*Afud=s!YP9>`u#`S-x~qVpk_G#(}CoFDLiSyA;hox8zy4bCG}E9d~n8 zsihek(|%WHO^-F@Z#}u7(!%^ojVo;EwBiZ7vzAV0@xetko!VPp{ozWh9h>X!bDC-8 zEt$7k4`P(lOpMcT{g+X+0z)5_Z1)he;xD?-`s2yU#l% zy(;OEJ0+gqP*O3dwLZHu?biHDX11=dq4SDoG*`bF{FJE$L)v+%=iA?O6*mhA)tq*EVdE#qVESwjKwVZ_}FiAjghNZlg z;2D6$cu%w86sSbd?KD5p86s=SVk`j3VLDp!KUnH||Hrml)>a;s{I}eRdrqC}Ixlzl zw!CGzy=RXZlX3IRvSVB3<$qUW#O5pKUV3VFLZc<~-hL)}ZH4{B?o0j?;gf)}imz$tNluT7A6Xi14E?YJx9%7r?ZA+mP8-KR`sM(eTP z=RBBwMP$f?ImQ|m^BGX$8v6VV z@AxO4{PqjP(N%Vw>Dp=D?rXJf-(0(H&FO^OH`QaadZ0p)&wceymD=ApuTJ{9{ZD84 zk|$PH?UGlg`a!P< z{Kscyn%}G_ViM;G+F;>S$XP}0V_~A;tytJNc*A=vm4`r@G@dG_xbFU7hb%f_KElXx zSad46Z~XP-$uA$?bL7*`^H+UQdsWv_SEi?B7pjhh>ddKuxB7nd>4kzHwny$gw))AD z9Xegu{pUIRhTQ4cp-k;&d7H;fsuOjz&#oHHHkW^GP{SU*SLeRH^x$7MS4{J?8Z%25 zb=x6DjT$X`tADQw{ja=S@yg(}jYrHhCy#oxTa~r>*5RXD+q^&NnGX+!4$Nm)Y3{_c z$6p$LsZYp|QMDRQtM}^Melbh>mfDd0-iq)8%l~@(&==Foqa?6SiNv$d1lFmyq?gpG za9}Cxyc@ac>_0fPF`mu2deptT$*7-dRQ^Q{Z9v2E6|dCg(2^#Ps=%QQKK!%{E$d+E zfkFju$ty+bJYJ+utc^~DMyK~CwzhcEGg${?b!%xT;h=l(vP(X4N@fPMrt1un5u`;l z(seexkY20ds(P|w5JNy#l9V9|9RM@!Q)f6?`yW~SetrJ zj!dCC0{@JoXuv;*oHJyEY{;@L@8cjB_z4dPyJZ1E7QTPOPDqIlKR^G#g_yvYb{P99#U3f00R!+$!IwsvmhN2l)F z-&z!J=L_dICU2czbxNDmeSO}WGe%XMxzTUQT49$IEXcH|jw5qR=s5DHV?P^4PI+SNvZp1C{GzQl z84!E5o(GKxgNu)U&?RRHftijvP)3ikSX3nVa(ifV@F!9xk+lezgxC-_2p9}%quG_N z+fL%9p@W>ol!d$c-HN?)`)K#4e=0lnwpHa%b=getr{uerUpsUEvFU4$4SZ;?FL~+U zl-k{=f3)XXty^mH!&B{j1Za6ZDU7B}BFFVy-pH)TV~_rxF{dSQ8IA^WE6${;<8%UH z&;|*Yvp*`)G**LgAaL!;V~!>N^MjcgjXt}T-gt3ZxA7@oe|YuWk2lU|ExdYGoqoY$ zGr^xGpZ%kG=ZA0Q9=+MQ&xhV*-#L3n47ip1r|Ik4er@(2YW@5YAsI%mKLj1VPl?)9 zF<&nc;@}a0WL9Wa@x@I9Y$p<&Hkp@rb>F;FQ(77ahE5;crEB&>dk*!^&Nt9L6-qb2YmF?f_LA$)u7_Vsz*N_{PB;8-!#pA>iE;& z?VOZ*b9LLTX^W0mi!5Q+oBYz0L7g7$G~$tyqprR<_w7TSKW$dA%1cwuGq2{_l&jMA zg{$YjZ(j4@j+hI(25!t4x%$&JpC5Sl&QC-Bn*LU^s<#h+_?=XX9HFaKyi`rP?Ynyn zy4$j9`75IvJZ9%5{?J?xrDR@ut?Lc<51#VsYu$R?+_xk0=pUEVyM6jZ#QB<|n|iD1 zrk?*%s(pRryPfmSAK5y#&B|Fdj?a@3j8A`4s+Blj^Ce%k$A01nQ0+8&W3^JO)*-EL z__cmc-oU*;Wc`{gbM6~^uEUQ)lc-$cTWRg&kwm1?h)34@^|mCwkuLjje9?L%lBA!3 z2i~qkRGt)Kz9sz)JF_+N^obJZYjnHikwNtxe{9X$w`LA}G-bob55#WTbn(wUU&ub2 z_x`ei=_Ai#VKZmvsoPJDOWeMF=c4SE$@NZgsa4@4HjvwoE9Efs(06AJllI>NA4WI9PVBXSMQ8ijm-gPuDBL(9-^ju!r zu_ODi+cTxTr_$m(wa*QjyZf{G7iDj$=gk=d8=hYI%~*4_e_m>b)Qw|17qqgT z&%Tzudd6gRWYEPWL;AHUR6Y0CyYR!|qdSTzZSmF0Nuw*x%-^}KY1XdD{e`OH)koU* zKv#+9(!xiTTRLU@kWqiQKBCjK!Luq?tu=2~j}^lY?>@Gx^VmbRbJNZryS8}wokEq_ zZ2#pcr?xkeW&iGF+Q+mx2;0z_w&A@yX2mLSDIjd5XE72i2O24o^FbrPVYk>q%AF9B zgJ(7W<~&qnpF=RAdPHr3e59OTo4&lmY5e|`U zZOtJ@VBoh~p7+G?FlH0u|k;Q8%j1Nx!6$pUtvP8k7Va= z0X{rgEsvVuj+=JGjcdN*B6~2%?-waMMSba847>Gm1SDz+Af+s|191MNv1IIrCc7%CqfRU0+2<8GGOh^)bf+j=( zS9?OpT7*#59b29>&drT+EI&G0esC+Bg&5Jm(t$Rw66oAlF!+?FAmi_TNbyEm+K@$huO>KSAYqy$-A z_|=g=y$-qMK^VQCoNR*hLd#-yXDQ2QMJ7Ul{`&n4Xu)1fhKH6a8@BS$ko_(MJcS`^ z_h4y!oOKJHN&za08x{t!^zcq%XU#hX*FG%d8FyU?F)RfVQwR>aRT%N2ABHRK1mILz@jvCM_)w5ecoK9Xwtxp9-w%MRevB8zrhOKa zn*_0b&xPWV13nkJ$z&z`fs|M(MMAH<<3`%UAtP*xqbu9r)7^8@?y~YLYZ=tx1mQsj z+E(_eol~?xN}?xC52gH1=M3V5?&Kf*X?lkkaQ-$&21oe8|^3iKR)F`CiHatK>H`AP8Q{AjkX+)KPA5CK7&!)xl;EL}hDeCWB6^$9e8* zRSkQcOROrIm3=v~ENs<$i)=6E)$ctO?VPiqGMk7Hu>6gwV5k5J%Y^D!f=T;9AWpvj zskP!L!pBHSYM~)H#77aEzr9CC0uO`!dWtBE$J9uwpP`6Xm(2&95kL`H3`Ercs}S(& zj$h+uh#cUc3HTMX10R?+1A{$~wjVGGO(jKrTMH;lMh0>~8!gktGl_7Bw#WwbwShYH zreMr;LhK!qEI?j_8ixd|H#E2f}jaF$mWzT`c zqS$%A1gG0yx0|>P*6jixKo5IZUfurl^zd7Y(eHP``h|D&zGqB}A&9M@ihVK*1O55M zEMn`Ne)WGD}wjUPV z`=~VPp`Zv!va)a#RIFzbCz*(oGl+mbAwQ_h;Jtc!>0ybpH607pJ&$@V=wIijiCFwuh3J(A+%p?c6$x7r%ZxW%1Bb)v(8 zNf?D6$7O;v2-AXe;CM8JzKRG=iN_OAz!MR&@JQ`wDJVt@Kg`$1C&cYj5fhMQ2Z0gF z%^O_w**mBky-a{+^l?2lBf$rNphoCSBSSB+qff6Y!5QjRUs9Fq0{%sp)@=79)D{}z znIzA>2>PyKnM7Xy0&IOuf;6}gL=e$LRkXq^5IWWC9_fdN1|-z&!=Yk~(3~&^29Sgm z5rmoi4TrSnd(Z;n39%;UpvsmVf+9Onva(~0_LG}$I;X42wpg`Cl!25 zh#-Q{sDfI;x_sh@H{v0J^WnqbUQCg5zSZVE5Ht+W>}aqsDe)?ThE?e~DeBTl?)f!(!uw47@Cg8{!(8fBU zk#X2OfFd-_GE(413JN{F6u}@ka#O1hGzZ%YQw~^L{Qw{p;|!1-AxQ)8v2|h=pwP5M z6gHDNDlVA8xY4vmhl9{Ga}-*%&WLV%>hVQ{=J&F$ifyFt?g5d|(AQM$ZqD~9&gkn1?_zD;Ta)ff!u@SArO8T4U4 z;3Sc!yi&~1D*h6Mcb52bdchGk{OZbY5TEE6fnXz z7&jAT3>?>>V8&LsT5&SOw&?+iIrI<;8mKK*{el^^sJ&MdW|743jJ?X$ehjfh=~u)E zh~ue65#tp-zYwV8Sr-CIpN=73iZ)wuv(OKga=XKxqQoYrVM9 zevQvl3kq&E-pWyi3PKa6jH=jvT z+8mgmggE}(4GAO&DU&MZA_*yw)XO8@hh`xWKu@><5Omfoy>f^7whT+MARmyP-?)^C zh?qpRb&tgzLA0hlH-E7?tlum;zB(q7OfT$BUiZdVzj7rL%v0P-CPl}@H!u{i-M=8S zhcnCJur?g2KC&g$(bfbQNcfL3$P4v|g;qJ!I+Qg6o)Qs0GHlHOC4+w+kQNo3H&F?H z_}+jQyiUSZ0qqTiq7r%iKOfyGHf{5VEn|=^|1E1A=Cp8J7(!@*ygFpHH5p~OCG)_B zvN|W@B&kh_Wl-WD?IITvEpJ$)N^=%-v?3GJ&-%j0x~G6~XdO=`1R?P;a1B=N5MVN# z)?)zuIn_RajZptNZ%RWwi&>OgB(!HOT}=oGDfFvFU(rsMC@EVlG1~ig7cxl!QT5Pz zw(W0;ED%mS+(5_3Ugva1ib2l@D2HtOLeTWQrN+a@0&RqAkyX_818=G+$oYH=jX(?d z7QHM3Ipdf`-KgBS^=Wu5z1Xs%$pZvt*$y^=L%@qBiaRusOrX02yUxO$D0{+~F49Lw z7^sH$6s{3p_Vzp9`)Bz^F zT$!w^LIgN_sIT32pFP)aYbvr(f+I;5;1RWZbt~L5L$W-~w|xm9MTHr#-Kc8-*0?>&|hImTl)nu)zmtG2!NVN}d0d=L|`K-WLe{m+DTwjiTzEG>aQ$V5RLV!v;u zc=n(s(slsiS0?AQ2X2ygcIFb~PXh|7WJou@+!D|5YQrv0_)N(iQ{; zlTA%=&uyL7-C+4}0C0onj#Zu;PzVVl^)8K`-MWD?qhFc{|IxkYdO82qp;o1VQyUY#$EMI)V-F z+}PU#b)N;dYCl208qUC~4VBM)W}^(6T}Kmvhrh(3bB^|?K-C`SK~3@MoWO)nDQlU4 zU&7XnVDH)c%9#Mw4kF65D&R_zNbv1lu|pP-H2u~veWst-le{LXd3|9_o{4Ni>7d^K z#f0|8js6b;Fi06_&`X3ca#F{G9~FlmQi$s?$m2Jc*&)CeyfDnJEqh=ORSurta(1vM!& zB_;1>-q2LUTWXLR0-_lyDtJrsQYeZba^AJ(+kicF&iVi6-^%{xn^~84ty!~X&FqGs z8!ij|>w$v;g-Rua3jahyh1zXU|Nid|9UdGw=#2q9pmr4aXuAtBIVOJE@PYjdBS(!k zP@nk|9vK&FZvVv-av%Ie(uF8_Pd0=y&|ZPlIxR9fP)glx)wWc7$xF zQhQp+@cy95Lfb8>Sv#Uxdq#Y0JZOF(`pDR6)V_eW@8n6-Nf&(xvGv%Px8B5aB>viq zI5Aud6#dZtfBiK1bINKVysL%KEN<$H_(F(19|__3OHel%s&zCy7 zbnolo>C?yG-~Yv~{}|NY_cbp+|2`x{W!17}E1OpB_4@XG++5uH{I5R^hlQO^ZPLbS zR4zhor_$J|8jcAkid~J0BHR2?X)Rh=drfob*muck7ms0Yg@dIXMDfQg4e_CEVWHMw%*_sqN3F56Xws%qYchMp$vgp`jYO#Yy!!N>GqXI;Yod^&G+NrH>4;9*BLrJ2Zm)cN{IlY6*h}WU^LQj(_ zjKZG2gx12SR~hvV=+c@S=%g`~um>sI3MfWN90IXSOe&~P)t6YM6sIi5ExO4RZ=er+$#I9@rdWA&ig=I*Cd(6cVH)3*%5oms+_YhQcnu6BZ1ORM`!{Ym6}> zn#rI>+6r(q6CfL^wIh!hp+|~k)dWIsl>QMai58lnjp0ED3)X6dq#=cR8U|Xhl4glP zuiB@Lhzc zvtcwh(ncC}z(9+uEjpW+RyvqGBJb&+?q z)ZOT2y*bojR9|x>t8!EyWkDQdqFYPSn+1V6a)cK2teGN;q5*Re!djX0PjQC1(JOIA zrjVOxPN>z~I>2;VD_yU|8FZKzvjxE*CbEeA0Ez5*yz85V8I3gtB}D@|NDs{cK!Tb* zB&QMro90+6Vm@#jjdDW^AsG&5 z)5!BCP{9~C(gigJ1q`MK9fDyBQ)$gCn>7m}JtuALymIvPj-HY2rlxdyG}!UU_C+-b zVSY#Ao;!3fxZ=96L-l|?=L7P(4NH75(tCROk7u3cTy)*|&xrDhW5Ys!D6PGgI4fmb zx8&KYj@`Utx9mWDRq}Hoy?)>8*DWA5zjCR2$%RfHv+Axs>DuF%_vH4jZMTe_;(71Q z4bMdC&ZKs7J~eXS{gi-{=iYhy%DZ33B$fJik2vt%(KW3)FDt!s-_QTbbFJG%?Ax7J znD>e5>VYi5u}xvyJSwpT8I9;uDY;QbCEjSk@;Y?-ik2rjjQ^MH*P`ybcSFw4+4(r4 z`njbu-78m2*`1vF&gzGL0Trtb<@c#vkPG$Mv)`+B&8RtFxnkAs z;MC`nH_jgRu=aj^kL#DgseYEyJ zWKmF3s`-m7>Qrd?)4Kl0c2~QHEXiD)@+2=n(Fef`T@!lJ^W_7FBn~-#FbXei{vA62C?#kUizFokY5re&|HdW5@QpPS$ywdK> z=9-M_>)I^|S<*fI#Q4rB;m$YbRTeKg`uv@#n@)P{SpUn3?(PMxcd63$bIvgWCHX++ zb(ltzr(`Y4K3vGqB*S!&QPvhtiyfECOe*@yZ3DG}%-u?{KvWzAMzAO;UzI+uU+aj3 z?Wr9d19vS>?ews3>hYsDQzh-&Xx9HKfjCT$l{$*_R&BK=`zq%{_a^1oWFGpY5 z`PSz0r+PmgSya&bV0)YOp$juxwJ32P7-YM5Y?}4i2{p03z8Kv**4ycgEgii5%QHNC z{FkgHjT$CZkzTWgm8_xAtbxc%uMxavWcx=voATnZJ74Mg=|37@uIqniLEFzisoiqv zP{4_oM|h0z+*j-Kp32#1f;-etr!PvkoKXdGgbq2vL=-X$B8MpHOz0>fgPRi%Wi3u3 ze@b7J4v7e<+w9U7?yfe~gM*WTKXVSb*T3T6s9D~v*5#Qjr=h) z_-su5qbI{6a^~l(`?>g}uw7b2Xpp4n8=`qrXiS9+m{a&%%J76{K0)rjOUy;~SI_~J z0aDz+PEt4x5=vxm6TP4bE}`?^{rOi@-tV`oC^bB2?an~`lRv5!t*EF!y{_kk)5AB| z`rKIl>`B`?hqT-|OA;nMOtc&pd+W<{F*oGnPTUfYTi-0tzBA3=@ALG~u!|YBcY|$R z=k2ImcxTbZy$M0T?TM3*89qPMJO~{!YDUn&{Os%rCvy(wmV9^g=jwqo2A%n5AJ^&U zbL1m;#nsuf+&h1ho4IJw*fD39#?6_r;X{^}E(OI`cI}Y9?{?WKMdhhK%NuBxH;KgN zW;V&Y26@vjh26Ydek*ZJ?wZP8B?v_*uk(carHmW_|;=6){2F6G-ULwq|r-?jCAbg*05=r@bD_;p!awKB6f zBRcu#q~N2cRy{1M%qv)V*riJrssodWW*L!zX{Xu89Am?$E)FBP=^-|(I2yd7t%ENp zH9iVfEyz&{3i^*PP~$=J9urSAAHyGJ5v8lii|K zXZ98mv+}W=ACa_q#@)iI1rATG&D?gpHUWo1oGTQ>E-`Tx0L(Ck!xmjYi#8rQVN>)W z_ImJSkxAmet!1pg>2Y?l-y{E_5!u@d-`e%?>Z88nug@!(o&CVnVQ?DcHGXEUcdgu$ z^teH|?HJ{~>g%$)dyiLtAJ;Q(@b)zs4yvkJIDpS{VBi!@KBMYb=3=B|&e~M{bID4F z!4|*bI7D1jX(J&Ux#X5PnzFySET>Hw4(04n+s1(SvmFbndQT8#hnURp8k5+m##sv7p|R) z$q(vIkG$)5-*vT%Ea{MW$@j`5Mo9A3bUn3ngQJuuAC2-RHkTJj@>tN{$@}?6Cd^6A z?H!aEo^x_**+Th9E4X+t$R&S$AD@GX`S*rLpWIP666Ul`i2u_2XxyH#+z#^LwBo?* z4HBPUB=C(b_4!S~x4tGO0UlHyW_*RdTS617H*6X1A9rSXk0H)=&IL7dwz(a-_~5`Z zk&Y*W)}HR?L1{F-;@*w=hlx9OMs3&4=UswIWVs|McSfedygw ztFE=_;fn33khY_(x&2PSCHm|jxzB1qiwYeu~$+cvD9F80)0og+^4J33`1=;HbyfR*np`d$R1wr-?Jly{FXHg|$tXc=+4Q3330p6`pe-)vD^p z$uqMa96AzvsrtdbS8pY?{_Z~5($#_nY^Pp5Tx-Zf`K zVCH96Q?lP5{KEE>j`iK^4UebZjnUT6paw(Wet zk#3>y2Y+yI^S5L^=YSG(PGiSua2Zg^cID65je1Fi#wrRx&_hzg;bMr`apnbV6|6WG- z`bgWV+xr%Mcx~IEF{_SOmR<=jp63yA>*35jHIB=c?eGk|6gAqX>zd1tCf+S|t?F$i z-1*6o%J<4WYRCT6WOXHr*>&|vP^V+dzO;^X+A?R#D}%)^GTZT7D&JXQ<}KSw;XSgPN>T&fOb@yg@)Bgq=i%mf8(Zve!+g ziPy9=QXODmKOiXBB_hkjo>WSLrUO3}zHX@4i67#i&t$@jQcuWCDILxJ#TaN!oQ2uX zd{MyVG|!TTE>t+kC_&3;qk4)9Y*|>5Np1#KK{Mx)1)yl~7vUgbCX($&ia0768`>15 zT*Hu227KX-C!8-;OoFw8r7^5YS5sIM4)cLP32OyDa9!ipj=3Loj%sMf^mjuc5t*l`#)?=@^xlWtt!kezTix#;P888*p4CXj8sQVbB( zv}6bl&4MgcrD|I6UbH$It>t47uX6GyZgl{WtZJ%tS+&W8Zoo8cFhf1 zX-eeX8(uh6*?BsfBJBDADs2Kcu}cDS)xWufqe%dTn&OE3L%D{0WiUsCfiBCM$C&~2 zS)i0(&_g$A50%>p+OBaAqwLhpu9GdA?L0yli3oQVDr{FEmiznwNup9pWQ^&ROKDtq z7>N?)vb=YvpI9IQfB4x#9})fc_xfCFa1AXlkWqM07hh%ZZ#I03EH^~PrAU^)$B+ui zhux8-D8EYq4qx-y1^$ZLF@A#`T)fjxi%l9u3hSXSsATvMhZ|!;(6nQNjuENgApnp& zw)6!5oY6@9@(e>tj8vY7p^uJz4kLTO{M0M9c%^1l&nI>o`^X4Q1VEytxY z^wWNe8g9?KD5&!4HBUfNL;$T(uB6c);H{)dFHzGX$ew#vd^Q1Bgiy Z+a(nv)RdzdiDK!D6mCHWXp$RB{{^!?o`e7Z literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3203610650388217380fbb6d44c08164594c1052 GIT binary patch literal 25835 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 a;gl?ZY5yXIfP;&;L_?p5yW3LPyZ;Yu1^lf5 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..caaacf9f0417dca768c267bad25bac04b2e86760 GIT binary patch literal 27766 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 O3;mkHI0of1zyE)ynQCkR literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..97d169d739743ff6e5c0b4ffba3f56d6b4da99a1 GIT binary patch literal 27559 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8774cf7408e2f5f0f1e7d5ae08568122beb50dae GIT binary patch literal 29479 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 X*^sg%TqjLH8PVYvZ=F!@^ydEuQV)kn literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e5b1f1fcaf95152541e5d2e447c6c1dd106ffe96 GIT binary patch literal 10567 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..086f9613559f119f946cbd85669be4ecdf72cc05 GIT binary patch literal 27014 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 p<6&ApXn5d+r%`z57kZ$&5c0r*CW5t*w|$ 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 F{vXL)W#Rw; literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c47d3f5c805f57f79528ff7860576523a127abf1 GIT binary patch literal 31143 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 B9y|a5 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..419042f5c11bb07f0a8766bd8cbe8db828d27db2 GIT binary patch literal 32995 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 Tm_0$Xp&f%l!6XJ($6`8~@l@2UQ= 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 gHq8ENJp znd^4IQ!!9LKcQ zDHmqXcHNvn&5Iu|q6PA{Kh98Xk_LxdOYb=K*Ok+Q%HocUZC3m!=SRykm#Cg8SlX_< z5J;zu;NvZVr}iq}SN^w|BQFffANiB_!=-o2u6*k~^}d1cKx!-rwQrsHtD17V3%)u$ zMtbowXNToW`8LM{1L*S%yh4z3r)uP2DDbThQvedJ1B4Y7NPmP9K77cC=&T85XQj0* zZ#!XkcKq<=S=wq59?vh)@Aj+5f6A1I9`P@Cny|ff&2w6&D)!GnR z1@BP!x|=`$L=J9#KrdQx&Yd6!dz$tj$@%7^cHP+vwaj?~>Gt0S|o+G|$Yw{U~xp z`!AYzFDx1O@mjAhE9M%S577#jb=BhbqxE!I>vOK8swBN);4Hrb&f6}Q^i7MeOnPAD ze0Bu3WOA=hY}w>UfEbf94!s}m4T$)A`vC|W#n>LG<0e*)5jyu2N&r8SxI8PEVAN3w z>j8mBkE#@;DFmvNfvPz8#HK?f&bmVGKgK9>_ZrmxXp8o}mVCM?b@!6peV+@>dFk=v z+{(?zKe^v{bL`)Hv^#QRzhjk4VgN1=9vmvl^X^kEw&ZztsQ7BxC$Ytm=iW~LdTQ0u zm7be--CcHK+nnXqB5>QBuzM3-UaA)Vib!4ZDlQ&;zj>5-jjs9~W+fGV+I{3#FI*3Z z82j8?D_=Pj->0HthZd%?f}efe`9sC^DiZSoid1%VC)Q=;H^#CRm?(9u+9WZlfmba1k;{2`k@zxF^v4 z$cTNJdG&)&2d!1t{&_S?A}a@Y#`9_?FE%6$c>;KQiM8DP|g?;_D1Ab z#3q}koazXu6`tR9ohUZlb%LiLSy~t^!y7Ev1T6-OB}^G)iCjR-xhl1O7!(2m+mk?kH~;7!Kq@lh|_@O_K(4n6T;*2*{_o7Yie(s-Tz& zdi~?;DrpnX=wHAYUS}a91SuN&La(}f^Yk!Vfbt*s7m1Vp&u*@2_!s_RGk?UXtppGU z{X#%kH=~W1E5nQIpK>s;3sOzxH{+cAk2Wyi^%MsKsxnGD`Gaz@AkutA|M*-1)i-5P z-CEEOktskqAs3%U|bSsC#Tl z5!8(i;t!2Z!+rjYcVRaGUJN-qBIVcNpb(QPt=b$(_ z)&R5eTLtkd^|pcT=>G@2OU*&HlAC>K0r?zdx~!VCXOy$nG=hF13H37@19B1+#RyQL z#wQFt?1UV&)LA6>l8W+rhE^*r0tK)Lg%Q^$2SRFYB1)xKJd45;HmIC&X@r5*Km8x~ Cr-c9j literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f1aeca9a905da043e80555afe74d3a5ecde77cde GIT binary patch literal 26572 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
    o@fdutzkv-0O)fIKm*q<{nh2DXAm}m9 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 Ob{3$BMORYR*Z&XvPK|*8 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7843e61d6605b9d0879e5a49c1250ddacfca3f17 GIT binary patch literal 24396 zcmds82YeRAw%;#>!~jVU2$7O3c=3i3iWe>l1Quy3AfQMUF*Jh^c}SJIA`qm-D_-D& zz!xGAq)7=a6lqfOPCv zg_kS)T%}r7pQ})#LfLZF8dR_GVx4;R>Xon3=#_?bU#=BZuMQD$1r#h8SU9kBaB%55 z5#=K4JaNhSMl0g))8nZz-mY?*R}q(Y5m(L*trSERMN`F9z}L^m+Y6obgQ0jB-A?py^Cn|wR=Xtz^j@OT+DzhhUUZL_!dm&)-B4QFou9uCu*>Pfd^3_Uk)<6 z#weBm3p1hyq~9aZHxH#(X(M zXgul43@}ASo9=@Qayga!lvcVvnbMqXn7I|01R`QedScj0=+82VSuN!>?2zTkjd^BgFd0G;hPwq^j)Z~$DAIla*o!rTm1)j2=WQmY= zm2ro5Y@n=W0G%~qbJJjzi>bzW8eDAGNf_f1HWhnz%P59n)v}=M2{fP9%t+7@iCXqL z;*=@aa}!oYg9>~_51x`V%+qnQm+*XzV1dU$O=G0^t>8i!-j(JW>vjN9l^!6wNp3Wj zSb>zog3ewHL$Mb#i<_;x!6)S&dh zwAB8_>V+d*jhGzp94W&f3dOWw5983}^&`!d+Mp|VF^AmFmhK=*!E~cyFW(BJoGnrq zBgup=oFQE((4C3{l~!KpOT}pH6a@hGJc&Rf1@Tr^4Hfd8fd}wx*1S!Zic=>Qf)(SM zQD(Jp`|l?;Ynwwnsn|(Ps0ENgW+cj1nnV2&NllmlI7|V~P}cZ&cIe8Lph=eWC8C}% zp#k->?|)x+VJwGMw(gdu{|ghkMxo?P@*F{AY`|dzS6Xm%460nn^w1NN$_^eCCQq)p z%pQW2=PIrkO0T%YnoyB1#*NhBHEhNfXF$b$iPHWYq7Ee zLyj|0)T z3LYf5{UV8FQYs){SXf*Vg78G1AOzV*fgMxIk7|#_ zJ2B;UX2wB49#cut*|9}=sw+w6R?z%Nktfiu5&s9gY*oRVv!!yR%W6qXyAcCwvuRzSa*O} zSHKPG5>MQzkie~ zZEzbFmnV?U=oLo~(v%{?1iIw`C4$-*xLoo~8fGl6m3-~{tR~qfZY&+OeB{SRmP`(6 zcjN7~SI0bi@y6Zg?9O#x$k8GnY{{-&=EvJ3SDooKe09gTlL=oYMpfuJGkny8^%<|N z+4#?%(>iS4pQG)rcDvt$PT8aGRy%ut?cE&h#t%WgD~9H1D~5Er^g)hRHb)B`UQ3V* z?aPSod+i-p|4Os$*Sh>%|7_5>9PQkeV>w!4;T$b>)bbpy#Wx*V?aKaS)S}e;arZwA zId!7Ma|g0p-pJ7w=4d-A41KV0@%>S27gXu}%E=##?8=OJQ2FGUw$q+IJhT1CGJ$i3 zeE({P1^2(zp52p~q-i=V=pSI>?zU>EXnIA>#3MlUGAx2XT7;&H5egPT6-}pJFH`gK z8qaOIne%Y-;-*ftP;^{@`wO)I+(Ps(=LS@eF;PP?i5x+^yxi30GNB?r%p^R#JbDEW z?)AqV15IvVxTLH3Ga>H-_qJTkes{;gJ2ApACMkpL*Ki1fp8i(kRge7g2hY0t6Bt9)F*m5t~e$H456!05P+U!p(4$skV5c9>eTDy zpM<-|XfMr7VgQFJA)$;m=`P~X-#JZ)yex7G`3L9>07HR^mVod$J?Q1k9Buo}xka0u zn|Jv6>>r1y)X{Xo)zpi7F5Ulam=@C$gFc+wcX-qA^$E9E#r_qw9PMnwYdya>wQygp zwH@YPedU7dr~OP(PA*~!1JT3n1d)JVCMJhYT~uI1VRSYpERBJ-8gPIdP71LlgkfPT zZfRN_quOdy({<%M%Lt}fk;zHhgD}{-=`It1U`7DtRH~9)(PK?>Gs&K@)GzozHB;Mv zCG6j1D^=yy|95QnW* z;qh+C20(p+j0;7G!lK5ASyz@8-qdaB>erJGE}Go=-9;D126Z}gFCqMLcKW0j7M&V! zr`o0Ldns9s7oMqKx#Y35Ll+mde6?tcpp>~sdRN-hu~&~-89~k8F5TwW+QVlItafJM zhlhUNzBnf8%7t#%_l-(QJw0*zgxTj?P7PcXSG>-5bDt~Isnx24_&p=gGePz&kb`4i z`fgnP@>z*h$5jffICyrw=Cx<^dt>9p>(NJ&3)T+3KRs>I=FlZlqUfEk{kCvVT-(Ey z!Kcf6&)@F)wP5|HvpK~YE*f_;k2XVD&jqCpvnd?f6uu<1>6=HJTEf23oAPM$FgBlT z&&6i6G+JDIY{vAa-D2NMEnM>W(QRWQb{(4>-gM!ipq5wr&kX!w$bth|5yQWJ_s6GK ze?9HX{c-P47;|9e=c}6j7HYVD&Y~?iZ%3_nP)#!kl;1%u!Hvh+0Qv%p4tkANcyghI z&e|3-PXZf8uGekruu{C9h_zOry96P+69Ah-^~UkgxB9TL=|8TFtY75LJI9`nO21d` z(6#Vsh5OGtc)Q038-6@K!!`d?o2FfJo!hkTT8qRht?s<@eT_0!b^V419X9PeiY>=b z(|czorv!Um@WAmkFKnH(G>* z2Mu+kc?Ool6sNw5bT>VEO#*QNBO0iSmSze(wB$nUMwP_((!BSlsEPke)08dUJC1zz z`qpIwI(8|vtW&3ZMYS3$B@NF<-BoMDDy$wcA-wW4Ssz=~TeZ5R)>^k=U#)R1TWKFg zedBmX=Dq(PI8I=;o_j3Bhl(~-HDB=}=hvsfST|$;$%hH_n~?!VMp&FP=!TU$Vh3H? zqk?YsdKGl5%UIR*d+ngpuMPW(HNj0f49kSQ5m37z(d!15BNTb5HUU#>H^M*!qrZw$ z8h{I7YT|(+q7J+IhwTy6I)))?FOZw17NPcWc&~WKCV|;9>&z=*N7`K}71?9XSNqy^ z3fMbxW4Bj7kG_z-KklbXU285MeE8bVDPh+yA=9ngb!A_@&nErWq}Tj=YhIY#JN;(e ze$`7JOSv@U%&wp#>3!zTimVqo<=Kw6Mzk#5yh(EA-8nC;*gCjVxzv^uyNy4(>Bl#A z6)m!K*ye>(D#vs_x_jlJGbg%6tpfcWJ9_v0b3t@G@U&-7)*`}9*St@J69#u7!ac@k?PJ0jvn1g?zf0ncA{MfT6vLieih1dtisVD0 zbieB5q#{RzB7f;rYedu1&0mHhQ(jo{(LlD)uP5ugKIcSQn~datwt4U38)M2|IuO4g z_(sG1w_S5$zKzN5ip|or;A!FGwQ?o}3Z*H^VJ3T$AQXd~gebcuiToz8xRc)sOsdMA zq{>{{=!nq_=dSU%Ben#96K00F?Ur@+WT0D=d`|tvZ@AXEl6arPdSV?K(HCV{M5xeu zf|mclFYuthH|W*$>07O* zmWm6BI^B7zcVOjV+fqgs{6Agfa*6FH9Qb{H`;&wQor|yzY%kdRiky5ql8S zmCt23P;aojFi@V`iipayx50X&-sQ!4mLb2?L%=m6N?iW?eyPC^$Rwu6FEy?Uv(wdj z^KAODm62U&=To~()p6}2mu7!rmh2$K@O*@|J2|&lTD_e?(+b^dU{}9bes0zeoqNvO z{?{b0pc3mahF*F2a7c_dX>L;S$3<0o%A(^*muBy6lDK811)z(~jao6R6)HgUUhQ4b zP;eR+L4#3Dx

    lQO(2B4So_8?>ufsQL7*iqjgiKaWZ46T2Q0R%O^sXkH7zQ?|%hm zZu-Z2x8qX_Y}$Wu`19*y`(A0T9o#Kw<+@+r{KM_9#P)h-m{tAa+^EW@&Rni>>X|6R zb>Rl%d z`LilO`Tb5Lxf$pd#j#r=fc1T<3TTLEA8lEOXR24zhK8&B_xA3={jW@Hw_{}cnwb-u zo&M+E%k{gyQ(zns$W#3OnRR|=+xQg?&wX5Xf7WGHJzwcg&s!zhEx5Ypk{0yWb#@TY z&NhOn@E#W%YwPp@P~}%t)%IB@Wc32gb=(jHI@M83c?GD=6rC|qhYB}A|KW#6I#4O# zM8P)7Eo!=WP)i-nSZkn#j=qZ8lwY=Iyt(+D6TK_nEMC0k=e;Z3y*ByXg@^p_MuqRK zkHeAJ#GkLM8ra=$;CnVdEM{S#?5GLnxBhl|5ElQ- z?J+}|RG(SD{p!@p<;Ro_+U6F!ABsMalSCYnA*z5e>&YOaY0}H!nHw~pC zUjp6SpnwMo%?OE-o26stjdx1S7cSz#Evm{(^!fw91*~;V^KN&dUTXV68KXCBKGG>B z;nihp509z1sP&kru<=drY^#4Hef!dX{1UryK)vbb)4yMl!Hdo2Q**{vSkx?Z@A>48 zGY__%*`epX=VuIDS$E&sDRBp*kM92M@26^qH)|V}k#y$!!CfL&T76dKsBd!VW*hD_ zo~o!%4O~N_|L4ge7e++B)2a22l;xjysHgR^Ku~^OqES?}h0D zuTT7|!hNnxpHY8{J)3YN`J>pTPiJm!-TkV>{c`)#cs9bcuOpMrbUwDX@|@CF1n!Lf zQcPQ=*#dWJ;Go|f}vxp;=uvK`L8&AQ?h2K08b28xUADkj2gs}7gJ@EL)MGdNpBrAg01P2tJ z09<39piK;^K*0RNT@W`x4wn)W6%pv-@ zuw5|3N!zCyZym&YkiXjReVECk zCLo9lP6rI$imfCK*A3dM=h= zC4*4IkD!9FV!)wAkw_y2Q$n!Vb955G7AdMkN_@Ap%{X9N-o4Jctf<9w+Yc|kFsaL6biB7P9vbp}oW-D!rl7Tc zTvfe*;k}DkIX1q4`E~1-H8-YDov7Lq%a@$7f62njyKa2mE%fe)u=vZ_vkNsi`S!W# z)5Dgm%bK?7mFOE|zJF@#&j!?7`*1#`X1+YEgIjcclyffXOBz|arC5k^c0-1zEN_*q>__f z5;80(8@v32)YSO4omI~lxqI4Isd(<`qvNW**RNpguxW3YY(kSi+-v2>eK#B568{J zMk{W2Q0TYSS`t zgWA0tyJ_lcM|#ii9I&_MfrOyfzbG5jsH}gE)-p5Wbmq=m8E@?#Fe)nj_Mo-=`O4ap z2`_zdc>bgw{Wr{R(W`e0{}$nik#Q+Se@yOQcK`PDPkv2X|LKLp@x6z(-+Fo=wob=~ zt%$nU?)fr>qK?nWUi`1X$uD<0_^t1e;qy|XGj}lYYPY%$%UagGcFnSVn}wEm^ZLZy zDQ~XqbFx&Qk&Ajve&NW2DU+*a?>ZFTd_El>G<((P8U`XSNRDm8oRy%zqpqv{@* zIcY`w`3uO}tT=5-DQV|kt|>br>mpBz>^Pp=rgfx%0h}WR9xw?Tmlsky9-`1fPc<** zKs8;{GDcUgAy9Jd5NNFW`30|q#1};}pcYltMQxUWpP=hS6+le^0A}tpPw#{GQm$t7 z<&RYQEF3(t%u@bHW$FHl8(&`^OCMPT4SEMl^YmLEpJ|`HGic|GUiY?Mt32{d+?oG; zF{A0UzV9z?_S5wJ-aFkI$1Fv){Vk@55Cbr-S?Ln(2E`gMoVyy~ZbhkBwrL)?426Qa z{Qo15f0G1`_<7Dc9|a`$9>Myk4m7x6U7;#*U=X!slPW%H#t94wB5Bkh2DvWIP9Guz zdd(Xt%L9+zr|(nRUKsG8!dq*We_!iszm#ftA4#?BRU`YS+tcdz!{-&Hvl3Hlt$lO; z)n*rhhpgt9aX5+H#;&ust@6#uerHpzR6qKQwHlFtu+m9DQA|@jN6V)QCamNc{`B*l zD#XAIA@HD*?~m_%j;2#LNyqW4@X=;g04j+9f;glcuyv|Y;oog;e3!YgPN!=%Hg}Bd zR3ZIXT&d*tPQ8 zeO;F)zO<~y+1IK~eyhUB+uMR4jJiK!?%EOV-`c!bW9Z*@-x&Mer*Q|1Ha=Uj*n;?@ z&u_ndU}SpM$NO6SeBgpr`7Zug)LV(!&E9*VVZPmQ)Ek*}I_PZUdxO)0YBrqOG(lAo zYnQ(HLHvz1iDT-=FYgswr|r??2g!XF1`OQ1B;sy#li+N8k9hWHKkRIFT`sYt>{HP< zOf8!NM-~bW4iHTxP?LK6?eB&VQ%H&th#owssFNu=C2%u@ZuAo!75{l|exk-{4h^@e zWoX;>6ACWSGx0#N6RITl2ll}hvjBYNWC=xVd48yA3Z6-VDej~WNrIw&#uQCcSpq#K zrv>#T8fnN{dTNd@n5fvATR&G>nxmVPCk7GuXfEYIm-y<d|0P_VJ`vUO&v0*@r8urN{K zniXSc|IY7*th*OXqY9`ChwE+$5>1b!&mINKI>w~PiYDVhf_x8pMMyClCMc<4j41-t zEis6SNjH2d;sFw2!pKx*-h_uBl9z29VGI6l4U^>o1u} zQ4kR}KSCFi-k)6lGMeo$i?1iYZ#UoZ2iS zg}75zG!^*>)G-W`3HLwza_@0UOQgV2rzZ;AOC%kZ0Tcb8!`E`=xq(TxXB261fUJ4R zBLD!N1w$1Oa*q7&K1jnO zh%A%Rv?S)>CIG~_Vsei6fC(FsN5ok2i3b&(0H!AlAac0}p7gnp-R)s1@&a){Q5F(S zC_?xS_-z$)SLJ&wH3Wn;a<*RuvYq+fu!Um2Q zu{BpL{_K1%J|1-_&Xb;h1T$GqD=evri`&XQXu^Kz02C&Zg%$Fg;$Ywwy~GLzeu%?u zwHe*taa_y{V0DuQGh;CUJx`0-3!d+SgzXl%R22l^K?Q;|I>D)^kawf5O9UNtYAbxy zq_%me{*PpHsnKI*b5ew(*W?Orbi~g8)q(2&zhrJw5*3NmP0HfpBEq$fQN0EyX&pc@ z3*1%>6o)UnB!TRetA*VT4D;xtkO7&jeKVLL7xhKwK!gR_EnHEp#@GoUwo^{*=I(m)jFY zBRw>yq%LxTNbjD!NO)oVJi5yT0Q7WRAfhGViDJsja}^VQP`}Cp#ac~%XBD&XY!B*e z(+q6~USY^EkXiEN3VL-^pjDo+q>k7h00DSzJW!aP(|v#uVYZ;f;(sQWlYEx8`l<{nvh-lGyEX9%=HDMhbf#}o{MaRRfLWAwZ zwsI3(7E`+X_Y#{p#S^9`SDK1|b4nx}7 zt4)4{*dk#QU(~P+0-vyAZkq;spR53pBv}F}@lEOiHeI5S=qx_aWgPL|K?fJN#CdpD zvs@2MDXMKNVLaxj>@P$ok4PEF%&OBSrt1KE7%M|Zh8KnYKM4gR6$7+%0(opsiJF_B zqSGy()J8GoW!XjuVVa(vhOw+kLGwVzFlUa&q!6-d@;@VqEbyo}5}%;K&X&Dy8xN=^ zE)+Gbp_<76(o&~bkJQnIx&(%o>VXwv1}Zj)UYru{psn6ef9`xG%C4mwvzfYDGyd@5t+=X5y)1Em#!Y@Ij=f>6S? zm2jm=G|3@65#+%yVwaoLM_NT#9w1ZD<$TK`HVn%%zdcN3(lAy*DJQH?U?B@t)cce( ztRlpJMpC!El@%NbCfy2vPE9IM2r|o>RE=3PjBFr=Qw>H@f_VAQ;}&BP23k5dgaJ$f z*^KGkiOi$^B@-3VbQnw!(Ak_Op^`Kb3k}QTlB#2t;1tKPD0A35g z?wPs>S5m!Ne@S0^Bt|9Y8C6VJtPrA2aSb z&a6;(+TKG~yYcX7%01ylo>KBWZa_QduBUK$6C&C5k@DD~4&m}?SW!Zj%dg=%5A)iT zEk`0eUZ{JbajMLZ>z|h4pLB}n8>bId)iIItU6}HQ!jsBF4!81;67He$cv{;7&03}M Z&L}HkZ-<34|lxzpA@u<_Td}_H*BF->>qUneMLYs_w4p>gt|( zlJi5(Pg?NbqMAl&9*?GZ@TcXR@d*$91 zEA2bzRHmS7_hGN8bRJLe%}gimns%sS9{7J%^p_qzhjj$nyhJI$DFdKEKLh~Q`0u?f354# zuG|CiQsp(o%ZIISo ztAtcFt%_Dvdp_s5)<_G?pFbdfzQBNhfC7Pm1qz1+7cNw&aLJ;@Lc+?He)_30rOQ0| zO!-=spLwpvb5EA3(x7UM+I1r%BcHC+xM`z$FV>2PtV>KhfdvW_E>yTgaBzvb;bp?> zJ~HI|LkkJ;oAZ}*zMe9gPl(4i#FO)t_5^HKfEoeKlRuxopRW&)^@BYaW1qkPKfin) zer5!!Ru@+Ye{Xw~)1kTTU8cKZ?x z9)Ezj=JE5-$IL5-U@H0ed-CP;1(js(>la$IRqG}GA=Ro6nO&r6*NLaE{k6gOHOf4h zb4)Af>p{DHL$vzZ9b@E_ZCWs!9@-V`Z>ZLLJS{biaz;_yJe1P>sEoj9B|3p*u*OMP zLog9C7%`eADT7s+A1Fj6KP4m>DM3aRB>0OSuxQPX*~kjPp@t$-?NzKieyws#B*uhh zb_UQ=y3-&}OEO_d@MvRRjjtG}BV%bkR3^a3#|q$-2}{RCVDcT$gg}|nbd8dPFw3bj z2W?`83D9*sEl!&W{D3e;s0W&&2%^FzmP&A0cmxQvZVLbziXm8HG@k&}B2s|Pce^5Y ziA7RFAyS|zsRdBl5`{s1@)a}vNu@TBPW_OhD^!$HlE79T4>PK1hP?2w?ShJwa#>3n zQ93eUurb~xK!!9ih(=0g6Ja&FZA`dJS8Ib&NpKQ67Ye6(T$fwHc)LB+V(tx@fPgVZ zd~-GgnK$v2m#_dIpQ#dsfwx4GR6rwZ%8N^dRZ!9%Z&ndg+A7j~pd5>+SV@$y4FKkc zOn?{Ku`sE4%dQv@V4I|pNR1-C;z~@BgHAwDk9^OVGL7LekmRW?d;t3e z#s;%uC`luV42WHGR5}J4e#ZbP#Y(V^j64=G^79WSRstFr^c13m4^SFn1R;e~Vy5aD zhTtqQ(jkC76Oe|zB@=%%S_ zAW5SyK%Z_RVc}A<7Q+s-R|&A%B$MuL15g!&z6h&CC=6O^QY#3ghAkusEn1)>MlCj=RMvC`~@gsbv9!MoB_Ao?N%ldN_~_0i?iiPTJ1gRD_QCXxemHMF%Y| zsv^-YRk3wKuw8LdImKP5(N)nTMnH?D%oa}EeNajgoI<&3S(?dwe2;l1Tbc({{zQuL z5)T?QX#+~#G@5iV_}ThUI)Wuht$~0;3nH{_fN&E0TD9a9iSkIJF}Osn`kX-#l&9+X zh2%CmHUX+f7q`MT3KS3oi{79g2Ip~;68xcm74mql1dt&pg`1L8t>0qKS(r#b(8Yj( zwM5EMn&SjC9(Tg)YPHY>HQmdwvyeKGGh3hocwrEfJkv3M1qQ8{Zdw9kOQgK6E_egc zCL686IH{0|?FK1RfW0_`T0nTF*|iVT+WM5?EDCAR$UF==)0ht06KZtDmb6kk-kHfX zI0mZgvK>upz;tqY8*a2xH|UjPf*=N%mlS9#k;X0iW0<~^=!EcrFkM2u=#+C zj}^`p}-fk1PQ`n(g4wvY%FWg;L3^PoH6{DtOL*?1B`+dPwjLpm)HMLDsV-% zUU4S-5<~F7#-So? zLdU!0+6@pMRNlu8@NZ6Efj9ZeG`sZ?qm8V6)I$S}JOk|rg=?_)l^&plhvlIcbnY`D zO$6nvE1^+ALn1hkQ}rHpCIsn3BFwX;onkI%u7Ly^ptz{e!eda&n22r^3^LGaV%wG| z5-s5ddM=w^9T26C-*m>7c5Kp}N*!VBL@X!73AxY!~net;vB06URs9VL`>8GvnMC`NDOz9FwTpaCXi z?ulrqu{D4b;PEFqX*nvuGq}jsle7is!jBQi+b%%51)(xUfcnw}2#Z23*aY$8VxR?x zHH!`eMC3rFbYLZ{RFhW6Pg#TM43QC?!O?O!vqK?joCH!aTNCo8<`|YlQ=p@Bnrzmhl#fRKfXi$RVMM8b^kR=K z@49o(s6S&B@T@pdr)cf z@>3^rw1zK-MQ&R0aq7CWrEkybJ$GHmnAGzHV8yv*t~ixUlr~9Bpmit5vEmyB~XVZN&c9Vkh>GAJgE2?fb@^KmAj0 zAtk=xg&b|(z5XqRod_ay@fjz6`LX@HmoBy|*F0*+w`aSbY#K2uwMOJ~A77a@V8XU; zzjPk_%KR=@vP%!|G&eFJ!4O{odx@li<2000x( zaXUgafkcHG0x*3oJB0*gbX*clJn3Q|AQVp`5%x{9@Km#q_34iSo=g1H@_f@1(nM4d zbJgG~!oAvtK(&derJVPaEvgAp*bAYxe7l?Neysk1xo-unz25q>Ci{PX>hv3zMkm&* z^4oj2mV2_t8=6-9S2c7qk}ee~e{w>jt21VgI?--no3vL`ep&9Bp8!7Y#X$9itQ7S% zw9OUDi1$zw6N(mq0KtcFB-}uD9}jdwFr^-%JBlA(-CgmiS|gEoFzCAS*rsyHrV1Ts zxFrNEwKyfFEwx`M(&dD?2%54FjPg1jEHvl6Ih7_XdA4Bbj^|eNn6dam)1CR#Z`K&u z^=$dyUiAI_uBmC&_kCJz`;p%xFKl_XWBN1gk~4BNSmC}i+cFld&Yz=2=mozs@?J_X z#vxnXL%L}h&pL)9dQ=O#i zShZ|tQ1RI5M(LI3K(r`s+Iv@1mSi>AUFSy{%yxH{p6fEA=r1uhU#WllVobhXmX}!w zd32}lsJ|nq_msxwnM^fNh@^GCBx+*C*ece5?P8w@>Z3Unyf>`^d^)z5aS; zqxGBaAGs7i>)Q!K?kAP#7vJ+%j`qv-iv@>G+_^U^M=Q1d{?_;&1^@(@819A-b+316Ul2|egs_cg0-mHN1}{Ti*LSm+Cgm} zKfB$w@mW1b-mP`|&er;GoW6Cx(y6mynK@ecgc0u)yqQ)r?QY`j&s#OA(I)iJbu<6@ zJLrP_Qk2g@I;x4O#R;JpXwznu*ZZWPsA*9f zlVYDu{-Hzrqu)LK(ZI6B?~JMYWLTr@348X%MRYlTY24ojJr~2!y;9wQu*s5W7frJ(8WAEo8^Gd*&@A`v%Fr1lbvo zJ+9M&Fh_RG@IC7wJ2AKHICPL?C)VjHWlIrhLPRpU=Pjb`zbB%pl08xOd*5%~vX|zI z+Q+i%rh3bss=Ak+b)fvcedh;{UX(RrLB}U*uPqxew)3ZNK6(6J%FcNM;`UB#l{Ro> zx35dw?YW?0+mL3fC#H1H=+*ze8MctuUlXvyqUoaa+~ygQEM4Ts2}$f=XUZKx+vs&GZV|glE;9qdGyxwA5G8 zUo{kS!oZhZnM)I~j>Dg;ewBFElY)1Pbi<)$KJ1qaQQLJ^Y<#iidRM=mcUB{mu zrpLr}>=N+lrm`FA#cbU^@2Ay=J$vI!R})|)^l=Z_kipI4OzxuwZ*xT*P|@X>sr+&Q zua8uR6pjPAQa`*=prJ)IFvb{IvJyGuoYLg}Tr8Bl_^GhVUhrfM=;xmXWF)|1-oA+$ zH!63`SU==%=R2#>jEb+kuvw+*->m+AnfCi&VX~qHu6xj=E$@`8vv!@|E5~~z^$47~ zy8oBOorzX$%- z{q>}dEd~$l-Lt}=Lk)|xsPXia!KszAhWTC`lrp7L+x5S^e(I~GeHLF?KXPr|7RkS^ z_*p%7MK@}-dd9pJZFWD10%qEyAzB{pccyR5^#S+GzkKNG){CPSm4Eq0fu!s%175B9 z_~r#|>wQ>%`k`$*E`2d?{+SKGR-tz6@|8kvt*yJa(*JywPO@Rqxf_fv%{`VPO|KQ3|Mj)PMs%xQM> zSl^Ggc4+w59qF&^s(P#Y-TEIs)nLxO+uN&FFZtc(0X4dw>Go{OHqXKY=w7Iw9{z^o z`q;mbi2%q#DCt_pc%~aD1E>=y;pzd|)^mQ2V<-nu7~JZ$9q3Af%$ zt@}m0q_K5}#g5p$cSNzHt1`QZ`|&B!FVE~;V&UIM#@(%2>df()k$Yd}d&k%Me!pi) z;{L)ZZ`pC7f#>^OS_)gWjd>{Db%IHdOt*%$+e%P!b z0~ajr^(@~jZmx0R{DHLIQ(||9T&=ji&E>E7p1P?(F7oFD_rVT#!B=uWZIe%NbKhoj z5BB2z*1{6WkFU9Q=~lOrsT=!^`(Pp8H(oomY+j?bi%L!?*l6>{?^>)c!uMi7K0bWt zg?{UIt~)(d42mG%q}k;4-Q+*n=4kdp=AVTmVIb-pVVW$cG_N&_g)3DANDG?;9;mwCp?uQ|k=kZ)SVaffur zOht2omSBm&{NimVaPrVY@CDkB=?x%oX&PdJQR68lhz`%lmKIyXjtNTSU;6Nv;MBYf zUeFHpOuCyw;c1y&AI&`QTkETtv6I%d%wIE|MTr;`Q(k~=K3D0~HbUJ6b zSbv}QGwp))^`68~u=KW>XMa}g6z8>j64~Pu> zbaiP+YWPV)AX`Fq7$@at`>6A7nU(m^5EPY!L6YLt%jo>=(teEH zl-BFa>~HIQk#zkY1^I7>#nyShQ}5aBmPLCmEOH5R#A;e;{}Jc2Ih3k)bahJQ!yK5- za|NdR;!GHZq3U1~$-ar{giP*Gn(c_gWp@h=wf2CMnrh#_$5=wz8g(6L>lOzXjW~;`hY7R3z2@%OURTc&s%_(* z^%OlD=IU8u!FAlT`J=)f=-C@W*w1nptGR{|!ZI+7)U&o2#$3Yc&@e(+8isLL<)`hj z>hrmBzI#q?8~*L4(OWy5yfV02)V}l=ch`HeXN9_5!(NZhuJ=sAwi$!ciZoj>Y3Gq{ z^QWhL5;A*BV>TDu;5}WFfrIwECxbdjmn(_po(u+=y;@`BYyx9Ab}^BVQqw_wJ;^V& zy$&hebAjKc!rXOY0&wPneF|n#vApt}3-D%U#23?B4N5zqW|Bc zZWLSA{nF*{cHw9#YVNq>bB}e)n7#be>*q3)qJB&GA^T4CA!-xgaoV)C1rb9VGSe>uKuh3x8QIBVmr*;qU0OOVqGxAk?HqM@eRnz(GlCjl=bBX88*Y`S^|Bcb za^BkC`S$o_i5;#riLYI9$(a==E~of+u?i1cg9^@>g9-7tTpv+#V`Ufcuz-{;`~$Hr zXr2BeLsOPsMfXKo47B>88R<=g7@QCa^rwfw!08ysGD!a=)U4hG5*d|f?3b7h>$HIc zb3x43lVY~oB|cn}ajEigob7z`{aZIy_1ZP6=;#_|smbSw^g-}kYezBL`-s^JE!ukh zUUbaoKg|dmI;K%-vn@x*2W~f_Yo;S?Vad+cmKr00f(2ne6tz>dfv(Wa`S2DQ4k=ug zMhxgu!3L6)6MgeU<4fol|3$1zL2fv#i%k@DA0^ghVNjL#>+4H(lo8gljc{3^Us`_` zf4$uAef5X}-DPd3c#3vdW`6Ke6X60yQ?wtPlxvekli7Yoi@w--~pI=jZO~jJk6|-7Z$(;Prrjt!h?Co$qsP=m) zJ35E|{0SE|-g&Ip&{J_ms`h`kc*na>ADr1gs89Pn;|{+h>QyhcrGX^RCJv2vX1K_E z5_tlXN3s|u-@c2uKS+M!e<%6bW9RezJ*)WeWea5AgDy~yTg;*n6Mf=>m+JEnL`EdF z>^XSIytT?@&e$+|W%CQ0lAB+@(lcViz=~_0ozvy`3p+}F`hJx+UKm#O^Fy6m-R-^o zhs&4Zj(?nxSl0~etV{of32*P#VGU$G!h*;sgb|5Tbo+1?6cVTGM!hZvrll$)KJv3! zI1jPOA7fOlfpScG-Ud5!A4N1BFtya-^Fw~LN@$)q_Ehvf5#gC=fh2*%0Ob6l?JGjM zph0_@)yyBXxr9A}Y zkEH^#K6!R|m(DlW+}k}dqRjPQ*Y>^|S?I^=mwQ#XF>B<=MkoKVX!fWP>Fcw?iX5z5 zDLZ}q*`H3Hjoj63{|i}D-i$c$my3_h8`r=7ki4p1Ue$W=;Te4^49+aM_r{*zngz{l zzqRY#l;z!SovJ-;+SPgK3zjDQI?XS{5H6Y3#ly(1SoF@NE_{dve*lm!!L6L=Ay&(@auo)WA|{XoBV6zw4p}liSbLE_HtO(Q9Ac++H&>Y-IM`*4n-LI{qud*){Q>#--iA zusN%1dekf81C%inr`<>#9bbC(i@tSvP^598HOekzqeKr>cp;1hFL9=#RT2X5L{2SJ z_Q4v;OhrR{xigyj_y&VUCZ73K*a9YUK5ciVogu0}a75;uM)`q4{)Po9$NyuFkxH4+ z-~?`R;$ha>gr3h24vpB;ZaqGG8vE`?vxcT!zV&MMj|&zTtT%4_bK3aD@8BD$AE&T$ixD@FVxbyQkJRM0oe>ByYi0XYKuD_o?Vpg~iw#l)evSI_{QyN%3%8t#fC zh!;!1lU0QRWy10p&7>j$q&^Fyy;SBOh*)WHhO&fd;mPFi0#wQXJ(@^>&fdpK0~aS; z!ZCpYgwKEba`cvdnPaPr8$4(3?5iENFUFTphuWN;GHYF`9(3qBsP@&_z2~vcu~%oz zdc9+@InQ%mwPj5df~Dh?^m za*_QFi^E0p5C(yW!8JGo?UA)S(ev4|Cq5q&iBBbWH~l87Vwe5+4DVv!YdgO_a^{`< z7jJ{4w>@NaOMNypmA-sB`%%Fb3)>ui^)9}S%4dPbz$Ji032|iUyfSeVgjTD66aWe_ z3-UOVbuUa{YY3(dxT?nw76g8(}i!pd|~|JsT=pV z_`Jw(#g1OgTzWV4*SX21SDvZ3{?L`icbjY+JuV@o%CB{oWREX%JRA2HH4G{J!=LSM@nr;Hr+ye=LutO86 zz+7IH%BZK|^dvC(j$u?Uw<)+q8EF%GRKAhF(UU%ayaa%9q*2y8FKn!L74-ru4UZHa zLp1=>Jo%e^5xP*R+S)K0Q$hGJ!Vr)E7DBBNZFt0WR7q#P2J;l(5)xm^y;Z$N!q4RGLRL};gOiozQ+8BJt6tLsqQYr!bXO}doSsr>)GO0j< z9odk9z;pn30(4+3PT`jOT-2m8TyO%a2grdf72;>%bSE&`mdH>Xixj*}LI==K1ea6F z35VocbbGFfaWg=IOO84vu;B1WVsfcbMjc-R_zEGkv#%y+Ol$nh3B z=bRm!C=EzmP*qZavH@pGOAk&z(N*uGi4e1wfzYG?mxB-rHwX=Jq7Blf+@F8Rm|QM2 zs02<@zBF3mpzKmp|F42XNi%SJ3vMu*f@}#j5dV2f{f3;z7XW4P5b-dvJEaoau~Je@ zXN$=Q2mQfp*nnsfQvre6?w|(HG(~M?BF(c>=t8X+qApnI76=6Bcvy~jWuGWhdISOZ zlF>RZAZ3dPC0k2aQ6qs;(iBXY;$IHi3LfX)sdkRuG# zg7FrEiySGGtw*3K>8&-Jsfqcuh$|(_1%u^~8iXp;>7^24*jowh!C#ges3zn) z>ktWG^U<219Fp`nu3FJ1H7L6hqnZpk418Yo5;hGsXL}gcm9DPO;#~QrJ8~G&ut0RTJQP{ zjQx*GkL0)6a{ly%z%d3mA_jjAKO)Cbg+)2X4M0sM3cUR~d;{48s9c^EWn?@6QZyxn z!l{JmBvU*rDiT>895_owB8+#Oi%Dg;{{1Zhw?Vc8O%g^2j`kh{t7{lOn9#e>XG!_mC4;~CTR>O2`Os<$gtj~FF2P9XH7D#|=Wk3$v zqe7ev#n1zw!iqn>AINJNa6*@s^q&qTl==q$bVzt^U6qMlfcMAbv|WImFyO&SnWZun zaw4hoq9SLc*9SH#9+k7!@!^=Tt*alWsovvnWBp{4Mzus^O_>xm+|(6PZ@imddBys&K)8 liX|tL(mE~#0sO>+L6>*&WRsL^11B*`+`xv~T0iIL{{earB%uHR literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f35157851867edfc953eea5c38e72e4d3ccd5444 GIT binary patch literal 26101 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4c29d81a52077c52fb11adc52fe6404b77fbef07 GIT binary patch literal 26715 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c725925f00c9906f9e512964a8151c19996986af GIT binary patch literal 43061 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ec38888ba3b2fae887fa40bd07fbee350adfb2c9 GIT binary patch literal 28615 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..39ff1ba88c1efb8f4e98d17803677275d5686cd0 GIT binary patch literal 8963 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 F{{W}hJ|qAD literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..58ff472b79cb0c43d6184a424b8eef53bb7c3a20 GIT binary patch literal 11232 zcmb_i2~WoVj#ghI_}*konXX{Se}I)51OX-vU9Sr`js{c!g& zvt~BTuJJk>#`M~>k+sp)lgVUl_4L{rbdekA>l<|I*vYU<53`RxG&eKvZqeJmzeO+G zUfs>DU2SX!J2*Q#f7EZd=P<_+_D;?Y^b(0)+qMS!1}1X3iG!uNrNjUFr|~p1l ztFhSyJVy8Hhyy>4xe$Iwl?LiZ$S9ckB9ES3%b&)nHJvC^dr&M($S@t z_ceqoX-RZ-v~)D(^m5-0MjZ$2c-nnb_|kkE+qF9Xa1A(Q*jUQi;Vn`_EkowQUMUx@ z$W}6?57YKza-|OWWJe}(#h;IoF}V@LBukhdf%lV3hNEw#kDHRowSAZzB@$OnF*8zX z8~QLK{4u3m;$C27ox$QzUm=w-rV*~#_#oHOMh%G%>N8&^$3XZfwedmsa*qzEFEE9`^njS4GD~2WS^)#<11(^1OiY%h zLIJZBP+yH!NW%jHtOgq=ur!QAYpSk=fWEcm5+x9jp@CJ2|2hN@CI>Vo*I<(CEovJB_6QnPDI_4ylzTRWTO`s*IWM z#0Yt@o=i$Dfs;n4La9P4Sl41Hqshk~(Bu;D<>$YO>*Ey2o}5rI{LE>yu62G?dp z4Q8m6ptdhY#k7GO7o$)q$UtZTNbHkrgIhfXKuG}~Q_FVZO$s7|Ov-CA0pO_^Lq(y2 z4-!>mBAu8tc`CR7HN^}QQcS-qnTVT|qs1q(Ct(7{chHhe{g?#v(u5Yn0jJ$B_h_pm z0Q8C5^s&XE|$b69eHKodcU`{zDmQ%YAYFh4<&LpJjd94z$;^gjV(vT`2cJpeVGg%2KSMr zr8NQ6!vmC~!ZMlwUJ%D*usU&9!AT86gXkD{spM-Fv5?TZVtysSKma*7P#TyRV4w=5 zK^vs)NZ_<}2sssMJe28hLUHC}nv9jJ5tS7HD;1;)CS}~z8nAi-fWX>73A4la42~#h z#sVRbp~*_Jw*sPpF{q#vxairzx*TXLuyT$@4OR&vOJGn5dXN)zXd3}Hl4DFF>oGPO zVzZZqvNM-~6@nmQ6)noOiwT^^u$Q7WSkf*ssgx9hqX%4(N+MbnCZW{Ly^}x;)Pbo; z8n|gSu7(egS9lkByb_2wt(9CUbmS6h#-#&Zhf=VDbc9pHC`34OnKR)3sI>q|B*q{F z6(&JySt?nsg3|yl_csPd=Wu=LKWG8CZ|fI9*TAJ_N*&R2Tps3)2S zs*hL2r}#|?#&}6l;C)J(fcM05QFpkvz;{urg)pJ9&Tz62CK^_tmuZs~;JPi`LCQlO zK~lWy4rDB75r*6h7IUT&0G5=Mv>xqbQi?)fBEl;%GaRfN<_*ZX1CaCL9urFQ!;2w3 z#5-4d%7-Q4s!3o$GMar<9j?wu$SWz7FN387%n9r)q7^U`no!P*sisVz!JtH;jst|2 z#{msmRI)6AN|H^)_}83xyc^63I)z70C8o`t0cc6a!921zpCU*R46S>)otG4af1h|@ zvPF6U9SJ5EB?uyHEp4WkT8gxS4Zl{s-xs)sp!4BP#cE$ra$i8a(f9%*(V3oOTh%VBGaTEBW;o*-w4VHq&!E_W-6fhg- zs)G9-3PD_v=LnA|1za#$90DmnMl4g1)1m>>#CQpkO@hl)Xmm(k=SMbEXC@$L8{cKY zlSA%AJjotb_9{Hl^g?ERR#tLR{VSi~kXtW~)!({bIP#;4ofbWEO1{~5-KfYm*Wae$ z)33Un&;2neHuR0Vc=6uOC5`h3dgK&WTPG_gJ!}|t{q`7?oixx z+FTdNwqPmQj#5M7CfNxp1kHg1lW@aIU?OIG8H_1r7M6mnPG9z?|E)jrUhXV6dGqDs z{(3Qu8mwKkv-p|W(LN;;6BD1F zy1Q{3dzLo}OxcN-3lt(?eVPt&))eHB2y~1l4hhQ+*qriwjDbX-;!Tt3k!NEdRA^Yy z)$r-bAMb9jbGtmafg|(=LemWwthSGc>a%VBlEdub9D`vg{^dtrU#MDjaBesA=WYid zx`d27oH=mdorZJ$9o;;(MXzbfY`7jVuv0W2*CZrGI-G}Y#=&U_aR^*Z7P7)XU_qYf zfc8W_W-cHNuI>Ae3)b1)Op}LJ)UUTXoa-3aC;#Q0V%rHr@|QmJi{0lx!ajfNB%^aS zhPNxjmTV2ndbnrvAJI26{uQ;}tg^#+YsX&_>Q-I6a7&(<;yh^5LEDq_)Xx$Nw*(rT zopULAL1aXwci-7Ftv9$#b=q`%=^yj#vcK~hV0icbk(V*qpPvgKa9gm>7ClMK`$T<( z3sFpjIAM(fpM(HwGfIzuT1D`Aq;8qKQ>0d~T782v)|J0X#~0iA&GL))+)?HIVGq;Y z)d&AoGVPc_yDFVO!@N7i+Pl||tuIgVYh>5nm`<>G5FVZO_4e=W$NUg-`1%Sh@uIut zQAvKvfU2;l)SBF)GUGl+?wl4cKcyHCvHfD!yj+(JVFzxPOgm_xD_-;!Xkk2I+l<%g z^CNYCsaiN^pm>>~`&+!CwCtJpgQV||_Zu@SXQ_DkVuW494Be}b5@Y+^nDo=!Y=0Ad z@zT{bJ$Z!PF~`Ce(<`dmt}lIhd!~3P(BInC^+@K!?U(GwSWi4qyj)AX=&lJVFVNqg zXZvPcR_3w?3r<*e6)(151i7yX6$kNa=iW!|xc>aOx^qClv>8sjc6IIGHOe~C_WqYc z?p%4YCCJn1*0j61_w&}f*-YQFa%N8E#9O;9rae*(>6xNr&|?sq#1Uer&6n#M#xV&L zPAQ~GF=-TLDd_;^AXe}jO?Kflz;%i7x^vre^D}d`HM4O1bz+R^r@gmNPSe`D1+ANY z!nc0-JZ9r6`*rC}cXneecAw~feAlD2v~trez(Zgla^!jHz`$1z@B}~IYdzX-A@Od# zhrsnk%+e(uyMue?%w3rNBFldB?Zm**IypOn`hIt0#|O#wN4jpEJiEcxE@48Sb@8Sn ztACr67+UyQ-s)j_Qv)U+sQP@gX@?;>CrnK9E2n)Mo_w*?Ddg}-+o-{j*SpqkY~TK3 zP2GxxgPfADUi$E!$J&gzlBrvQYwS_BZpHnmXlhr`o7(+sx0b)({Z*jU{tcez6MDoPI_NoQiO0r$4*5TYnjan4v#Vo#iumGF*NRiNxb(PmwOje^ zqwB;s70lv*ZuObU5Pw>sI568|>RrUpY%ly+RHExA7%X5U`(lc4(P#&3|s-L-pqJR?1EzvX%B`W?32>vG;qX{h_u^it}r zjY&r?dw-SkN6@o~u+ppgS>e52*v?MfZlH%5$8goHx!p#;@Ym{g4|qG*=63zk?!6!s1Ba>GeX-*D)sej- zM^x6-O!B!KN2vN5S!kDk-G`I6YyONgemNvIDRkvHb>8lUE z4)D9b?d8>(ALp6w3G8ylDZG)To!ok^cHND7|2{WI_P!P|;n=|8d0!-%IX}35X!zNM zCu-Y_t>$|*E&L8^TfX*&w;}0}7%OdT6`HIDdqKG8affD^0@6;_qcEbwojs1@Uf(d@ zqM51Lo(Vsv=HIW$ta~X8E;)^Qnx8hE3%nys7N$ozcxdhhOq)A%)|ppv{?RfB0bdDn zPp4oE))*pMAP1skNU38yqFrC0!I~NiHU$C%2@}>T3LZu%mykNyyE=Dm5~i7Ac-Tb3 zT6hWkX_XB(2K@{ z(tqt8=K4HqYGnWT=bev-Px^l4!_E_DWTt8=UJvTjTv2)BV~2Gy=c-M--+mvj79+l| znyP(wD}2$44yDI81k695l(^u)N1G;v?|xWyG(2e2n6jnmiyY6Ky02f@Bh$Xj|IXQZ zb@76unrgFKRO>wdVzn26bK3v)bON_kea#L{DtXxPxgz9dp7p^@ElssHz%Ijb}&0?Y+bQYVO{M! z#m7fiBFQDPG69>}=Ctr83S*^0)KxJ%At2b$7h#X~lMw*1tRsd=_{I*xA~t(XlT_RF zJ@4i{d~9-a>Fa?#SGgpmL_03|+&Q7=`n-czB)|QpaWT3=*R#2Ho51>LqF7Wje7Sf%*q;+x2#gclJ3a!8$^L?Dv=INDfecptv z%`G>}8AXxTBl*IE(d9K!6JItcO@6=K0+80K$Gx+EKQZpa)@04yW}XelARAUj^dAXV zLFY1B#bGU=kdmDgFp;=3p`U@ha9BrN5-}_#AO{-j$obN;P2|q;o?ixRGC5kjaY@BK zD=)L48+m4XU7pD7jud*Nw_i89pk~L>)z`lpZFJUSMRZo>_SEqQGWSkj9OG>I{eeWM zO^Xw^t(#=K$oj=Vr~bXn77wjndn^0i@{+63i=8H@pIc;=*9X+hjP@U8LdBUmeJY)t zP4^$jQn!u}TU#1*j~wx&?9PVy+qSnKQ#{G^S7$G8Pj#)0-?fovr$koIxilotd&T#a z2fle|W|6vUV6n$Mj&W(^t1OQtkJ_d0tNkGgHAtC>Hty1kTW7}Xvh}4G)siFukz?O4_yH3*BR$sz6$KL zAmjIbF|}7J+y@N1k=x&I$d5PW;c0(nZl3#Kz-MO%_x;!SBNNATaH)5=ELnd-`Bp~B zH%lq{L(Q?c&d};G;=LIqU-B#X(oA#;Cx@Z?Lo3S9eNZu>U|VpmOR?>m)v-0J7M0t@ zM(oJ>!r1jxr(>_@y-CaJyz`LZ;nV`tF&2BSkGc2y(xaF9+izITxgDdMSN+|fIWbyE z_2<87e`Cn0SXD_w!3qNd?-YlbMst#8?D0(+is%cVZu8`-`|$qA2pWLCx6yqc1? z&hXHlRD(U&3-3N#(@m>zr5<^PxF?1X2xihInmQkb$E2yKyvK}=T@sZ|B*jvPND5zp zQ}gJ72egfeCvEnW+n>4G$i9g!>OAyOhVh*Vu@_t_R_%y(FxeZ^{eDeU(aVckn@~1<^GkT6?wZJDH5FPXv&E8=9Ge_0PYPR(L?2w6B`)_}_8Nct! z{ndf8D;t6@R3=P*n%MqW@U;s!_8se4=a4jR!=9bJw%_fh|9jh_vb||9KRfwh)Pej% z3EAVqyesVDSI3lInj1U%NzCE9cZwTX>aM0?^_i3$Xtv;R+4d{B=WF-AGCi6)d;TXk zJ!j-qow41OXjoZM@x~_M8`#9WfTG6sZ;8 zS-~Vh$zTh<)3Q<;uj-PBLzzN6VrrlsC|)68yy_ieI*G6lfR(3< zIMxB7KqqZ*3pSMS(_XO72YkZ?%!h*GP8$tSRXp0n#-*Bls3I2+GkgzY1q$oYp`*Al z*y7L;=VD}do&mxK1@4e!Y9PaZ)K{PXi3ey>SX^X-OpgL>Gg$0v)8ZFi39#s!PM~Om zm25|$m%vigp)F!M#0Dh*7GBIx&;S|)L0y=c(m)DWw2z1Xa6E)DaYK@OA;5x_G{NBN zPM83dR&vD?08QM+rbGbue8X5k4NIn<;s>~tYI4yaK{4drp;IEEM7`Ihq~!QNrm0jF zJJ}cjKW6h&?+~l^+T#A|6t4-c0IoMjW^8!MfHaQALEEhu7tM%_*)8po2;2Dq4(W?H zzhp3eJOTp1&a^>IOyH0}0%DF11}2_XLWU?zz$q#B=R0D^LW~vG83GiSYzQkYV{{UL zzFQt!a2@>5X#~%02rOI`pv|ET<9*OKI&#%)I;f?QM&L)pBpC1PlC+L#YThtpa6}q> zBY=N}p*32y!DOV^3Yu`>fL0NXzYHki&LoXVf#az(#?j_ zgjlbRO1RMXAe5*<^2tZY^`0vL9fXT!m#LX#sUQs$fSykD0ErI(JbP=pFuyn-l%i)f zGZm=K-Ce3cZJv*C4vBGGi3tSY)+czHP4d=h2gPwxD$N*yUOh*07Am!|MUTynu#7P{ z0?7?-to}0V5HKq813HxiW{n>R$e?(7Ww!>3-a3w>PY9yg5k$%PdqADD_7t~gG*zg zxhuffYFD6;NOUDva#RIe3HT+>zZhD%>=-hx zH-ulzPiabllQR|W1}5T5MOazmF|rT31w)zwIkIV>qbU&w31g#T(;;UXN;w^1%H|vi s&cl$ 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..beb03eb16912a5976dcebe5cadb7ead6e589de2e GIT binary patch literal 7123 zcmex=dDF&IcpE-rwvvCu$szg+{F&cJY47mGY0A>d!iC?v=ksAd6>t&xtzPC{armlTu$)tmsSPcA3|vF`xcHn};aAiF>S z*>X8BZ zGam@k8FCm>859^i8S)sC8T5cGBL)KoLk6?|w;7xnxY*e_*x9%^I5@buxVZTw1o(J) z`D8`K1SOQ^RaKPal@!&q&GpqZO*9pi3>*zjEUoSA>{Rt!Je_Sk%x&$gL547LadY$W z^2rDY$XIJAX;_mC{vTivm4!__bn=vo?1G{bFA15M7nN+NLe~W>KnGxs`W54q)L?Vl)IsLtr!nMnhomgn*b#=kyOV=bJsu zmAEx^<%PAn9%oG@7jiCqb5e5QU5P^me;@v5klVa4GXCfHJX8JGmDf}3g>?TjsOBFx zc3)+4-D%VQ&+qPJ#b3(^3;$PpyVv{Iy(!FROSHBBGuXX7w)Ahl+WE?Q&S}PZS{IvE zMlIzsy4iO4Ne?Jyqbf8&YW_Q-FKAH3DRD6xhcQ})AEkg*hjVXKJPPt1LSFy}a)AHfH z&e=UL;;bsyr;u4f|30>CcCvqbQu52;#8*ilLpEGVd>gvfX3LY77&D2V``RsmHL=?Yn@$C)f8Q*R4~jq@w)_VV*MVc zV;y?@lz-rQG1*m*pQ^8Pf;qwjYNO+za!laPM@x>!7FO(3?b#H*-qthG{5^-SN%Fs% zi}QW_sR4q`}bl?^`f6$$SH)nY%9s?d*12Sf_^`Ez6;Le&0t!9 zWDT?GGyY1w`pEG2-Sb|g!I>k@RdzFSTz%BErm6MR&f1bbZOOzQ>)(qxz90PTxh*l} zcx=H8&u5@0Rkd8K4~kO7e>Mm6uF1UIaq;QOFQ)BQc_x)vwc>Z#H@w>}Z)vO6e4{+N zf6ly#BD#s^X7|cP&3BF{UiR31+VYFF7pnPQ7k-|6DZ{qCbjQz=FIl;vQWs#x#h1t( z-gT$u$(LFDiFfntimDu)U{W}3nh>xR>Ihz_jjrDv-&n6be$L->!8h4!kDseA8B~su zlkYqda`e%vN7?STR!3E?@|SSlz0a_}Cf;QBJTYLICEhYA16XE#zC6vi4p?Tn$v;0S z5rC9#smo-%aQfb-XuD0*@3Y&&nGD$uqR7@r>Kjy_EdF(Be@)DHXE^f(LrMRcJ6Ux7VI*LNmbeEMnwrCI2! zh%Mrb+^3j-o7UeKx3&9gviX_xluO5iz0LI2BucpN6Kvlu!a}=HghpYEwdSFRALlN3 zppmp9XuVga*7wV+Rn(^0UU~MPAuvP!a+&O;yQfZGkhI8#v;UeW-qgLbX09lb;Klj7 zLk~UtP`lvqi^Pneg)a*ClDJGNT!$He@f+I5z*Z0A@b z*Z~Y|1#hb#xgLMA_{URVYZ=aF)u4R{gM5qZu<1r>35Qa9Zny4!oOx}fQ_ogKlP#$& zY5iMOQrkuLRi*c~SOHwM0RK3jSn*cV}XdvNLBHkkMZxhdyl*VlOL z_%o$mX1(k6nDu|Sx3f+EW;5IN8IM}}*&VC*am~#t(f0NeIvo|^lu>Qu6!HC#V2^!8 e{e|!bSDMAZsKKKlFd71*Aut*OWQIWf|C<1IJi1T- literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1e3b099ccb9a579718545050c6c4b4ad0df26601 GIT binary patch literal 21805 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ec95e73e058fa4a3a8f9abf67812dafe29c90eb9 GIT binary patch literal 20006 zcmeI23sjRw7Jw%Sk`Nw}g($5cfmIB$C@yTMMG}-&ij@T^2uK%sD6}hq5HP$%Qd){& zOF6ZNwPkq}ROr#lBPFpUsZ?O=S*4+(CGZ>Uvdf{O5hD;l!R#N9B-m)(7C2|=OiuXk znYnZ4&NumQaxcG1s`>$#@7V6U9bhm3V9*y(^x&uWwdX9atx|;7k#I&(Zx|` z_o1?7+>D&2mZN9n_tY{wD9{_#sX%3FoQ@o=Bd4aYQc#_XYW)}%TP=Fdw?Zja3qYX6+DlAcBk7c008g=LmYM)I=0Ci{jg(}85$VkP?c$gWh=3` z;L0l)1H+(Wop_Ra^6%WkN_kA2|xmn03-kjJcWSA zaa+Q`_`54P#WMGcw^pudFXf*J6N^OM2jYrlLPlxHADG=!*&VD4$F`lTD$lOfycm$)G}$%kR_y;zs`A(qJs&jYa_T5@XaZ3Ic9*PgQU68eCV6-D-&-DVRKR2BWv#Ec)0hCY zf6w7HZYgijQLXEZV`-IK_0S?UQ_Ycfy6QOVHP{@a_P}tf#x1en(MeM)7RfjDGPp0_ zQn0i0K23e2v@^L-kR%L4V%tt5!gfA?LXHR~kcQs&@*9(^X2(Bu6>KJyy+6b{E82DO zrIEzU{5qoF!M?(6>#8!ZJJnjUNuH~E`{J?3{*qQzl>cMz81Yt{^*jFC$Ig|26I~J1 z|JY}^i!U@OeC#Z~6W?)Yrq?FYdV3L3V?U263}-U$slZ;!BJ7-)g)eK1;oYQeQ99X_ z?-*Qf7qmK)TqrqFF)Tg9$#SY81kY8#lD7`jhu*%wW4UL+H*r^*xVZu8w|9HcKYUvf zUEe)&wqofRIC&vyjj0NlNQPVO8MQ%-FHL}ZO)TRi}~s4Dp35%*Cku_jBW`3 z@nH%0H}~a*Ixxf5nLAjY{aDEg@vFu68K+u&d2vT0PF~8g?ToZ$5i0a4&qtq>yZ%XL zOuGBJK4ahT8Iit8i&q@D`ekC({dim<#_~xMer^9M^O?}A#|}nwGNumgWzHFYa~{d| z<0duRbM+|CM;9)d5S&(ap^7)?)T$)glm}K0+6DJz5(_0YwHDGKPL@~GkvR$h8!E{W z>*{K5-!D*j=K924HLl4G$-lkcgI+1}kJ{Beid_l@H0<++W;R!C)OxII(vKm!oEQ?4 z8iGh#6roMS)b6dlJi$25mTMoF`@;TA-IH>N@ zjh+s`aa1?_`f`a`bK-O45;pL&LH4lvvCjo+Kg}$Alo;>)Pf$T+o!KhMEr6(~9!uyFa7<}lIf#NEQ|BT=buGCL0y)+dR&k-xT;AeY+FWR!{UCb}E` zAuI2i!kav6weOkBhxFP6r7QvwHuejcN5kXgXm!GtqVa1H1f60LtZe-f-mtLxOQ_Ax z@wj8F$k>|~ITl*l%V3L}WM|~OSlPfg3g$3^)~fpjQ!L7_{;@e*0z`PR?R+`2Q4lK` zUDV?H>~7%|5`Y9C0Z0H6fCL}`NB|Om1Rw!O01|)%AOT1K5`Y9C0Z0H6fCM0c|4#y{ G?*9T~!%;l| literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ae217243ddf516bea69cee0eb0c2f7fa81d24410 GIT binary patch literal 23516 zcmeI34Ny~87JzROBq97r7NWF*1XeM~qPVc3HWE-;DOMJwARsOBqtLDign;2sB&DSY z?y}6X=Nh%d+omCntS`wbYZgv?e8ZiO^6wJPWgka;;ZH3vvdo#&9 zbMLw5o%`MQPIAt?Xr!7SfaQ+u{@Vct0{{kn0gVDn_4oDN8x$P4-GAq+(*sx=fHosH z09c8soM69gl<6m8#Zi-}8N&&mdls9p8}WA)^;RCX$=b5W}w$L46|>!`ei#XP7z zm$qp=Hrt;V`UZLoN8h#}1q1^>unm=;X`ADjR0Y6GqX6KF=lWtl0zi-lfKBgQ-ryy#{+P$1_1L20Ia$IAV$tQIyZN2FP^Yg3OdIYU1t*dN&rl- zA5eflNCa_U11kLnZR;kF&2$EXv3A2N{=QpY^YmiSwMH;RQ&S6b3tLM|TRPQ|O8=jm zrWKF~M4nME4&w;0Bn*y((X@jX(Q9|Q%>n=bPcX(|m!V^uEYXV{v&`7Y7>BA%D=b@y z#f4T~!5A5b9P7rDJX3z-@m@nC-(hRau1y?t3VBv>&Ybov)})!~bG<;*3(RmB)DavB zc!OeIq23r2ApuAL5`Y9C0Z0H6fCL}`NB|Om1Rw!O01|)%AOT1K5`Y9C0Z0H6fCL}` zNB|Om1Rw!O;Q1jyKW_9?^T=8HSCHr|>eJ@t#GMznK3OA3JHw`gA zd2^SfeY7qsx|A3|R1_Uv_gNM~4YN*Bc^&AJI<*a|hgjE?LSa-SSK8G4?TwA9dy9!3 zw{tw^=4Ed|((vb(JKg`4U*q;re*W98m+Q$|FEGVvlCtw-uGxJXS;GTKQ5n0X3BCUGO2WTpYzn>%rYOX7yIlUIdv2{G=XRUr$??>)Tci@BprN>WBnu;u_>R+vu(P>&LWu|_kjB2wiW`&cR_8zT6mBMz z|7D1MR#DP^yVO~8NM5V@6in=~x6-SN3x4byBi`z;eLryf z*tt@0q9=;_AID5j@r4$ZpM%wR;yX^QjJhORUmqfB>gThBku26d4cJRrgq0Js@#P(H z{F~G*Y8ShT9fRu~LRM#yizFv1hoxt@*)Fw&(D@2j^45Wd@Z0xyEcYt>CgDm8FE1$L z_HH`kZ|_NB8)PGAE0>OelNXZLm}`KUWXSEoHKkmsR^FZ5eMj`r?mW4yq&Xu)14=&r zs&vbq(G8J5J}d?Q=DoU58)n=-a|au;A1_%Ue!b+r_o+62e!|hHlb5pXyQ6K{gi1ro z&!SJtTmK|8W->+3r<@ypBhq(hOx1y_UnEuEXX1)5)=!%7Yx`GO&V*hAb}+h~F?Z@L zcg_5}>qt%jFS*r`XF&N`bm3wNq3Pups`-O1?P{`JMR3)iL#QH)SR|>fvyz5zvwd2Q z%u@*3P(_YfS6_Skexb@M&oAMsX>DFu!R_^QMwKWqW|wRfyA%xQ*yj(;EUw0+{aDXr zfVb#!Qdn487$Rv?g|`fka}UwD5wiY)hY|Sx1$=0-rb|Cuy0Q$X5OK%!{Q}Eps?Zp7yZ4Co1`gr>D9B{D} z^P=Z*P}8FyJ)MB_sDAkM=Ml5##pmcHY~W{u>|yOc8WyPiG_&kgucv(YcnW$zN?7}N z3fim2n*1cE74vd~9?xm(qMv7jtoCH<6{BtLrR|Id87Ub#!bxwt(`7=g6j4RY6bWc7 z4Y(BPeX3FS`>^$;A4on6wBwitFrC2}GXDa}r{@2Sg&jJia%JPHvu^$S=>BbiFHI$=-I`LzgwUa<&Pwtfk3 zSXlif)Mn?<@7Sv{_vS~Bg_retbHpw3GfIBEeBc`uYZyUm)%}7gHsx3U*qki^BK-Ky zW+kgh5HA^B)Z+W>YT*$QfCL}`NB|Om1Rw!O01|)%AOT1K5`Y9C0Z0H6fCL}`NB|Om Z1Rw!O01|)%AOT1K5`YB$4+v;v{{>)EQ6B&R literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d859fdeaf0372f9a7da168400df584de63bb99a4 GIT binary patch literal 25231 zcmeI230PCd7Jw%t5C|rKvWXxNkWCQ~gkm&7kR??tn<${5kwp|Fg~(z>5UlzNPeBT) zB9BNxgMh6{q3@wWi?;9()Ce@h$KqO{MFkb1EVnlpz(prI&;5!=X~d!IdkTp zxw&(bAbg|)D8BDRcLEp;02uTD$XzKTx`RW2o5u%E^fiv71}Sxb7MwW%;X7H89?teU z8+?3qgu1b^Wos03Ocqn4ar>Hu(E+$+D#rf*RC0kqQCm@`r|8LwL_0^54Mb(-kXLf7 zP-eW6Q-yM*mxlxD!$D=Ekk@3!YjO;W$wGZs>o%0C7`T`-~3=K!FvAAqv!%q@{y zMw7vzSE=A&0D4aYK=c7%&J6$veq)JFw4Im>31ihk+i0TWj6jbN5C(#P4xodbU@KUR zN=tw_umIM`ZQuY1c)ToLh9D~|D^DQElhhSSL?TH`MO8^%PkVtrS(~hDu+YZLz}U)I zmu$Y$!pfFHrBW9xUPX6U?qaioN)ZOZ5ai`aM3SbWq9(Z$|S%N?shsBVA zloAH3gh8%=x#-#zma*}pHC|Rm8jG5!il`PNhb7>ou{i9ggEU48i<41Xm%+n(-&9hu zar9)RAN};)>^b|Zl~uj+Z|Pcike6&ZxzhbZw7tr2!wLfkdsJ3>y|dAkN_kA2|xmn03-kjKmw2e zBmfCO0+0YC00}?>kN_m`?h(MAZAm4~+ZO(@mi_Sy zEJAmbd01E*yXA56Ph~G*)f(KY&+@r=6afj{i_j~7>9(YR67De0(11N4|Kclc&i?y6 z1d#Lkbl5*_r>&H$Ef}#M4tIW}Yim_mBhMG-tYgNI+gtYebHx+K1MZtOH||ZEld*az zv1nq}Vm>VSLZJTh{IBFoyzX2u+Tlr+d&cD!>gVX^_S8{*Xr15A7$&0_y&V*n=sn#F zY6J~Dtz3$WVbZ}!>kFj^LmGG+|5@Cv+w!b5DHgg-4#={3J`8fR&C?T|%f6uWBxu(S zSU$)ZX+OwjXVGc@bzVNLHv{pI6h~qQdKC#>3Wb9%U02ITIKSkU9?naY=sN`lYaN2* z_=md@z@?t<)+;dS5R)`}EdspQUQ}b!{3Es|^a6xWgS? z71getfsGrr*2jikT68?K>Te$|c=#k=W1&KQyNNFK-~H{`bv^EM%RBv4g#k93eHQ_z zRcC512RHxXZ=82(^Dh@aWXI$l*JP*y_h^p5%xo-3)#KJ85)pH2lkb0Xvbf%)wXh z2Aa(`chgc|hQ{9ymU&lx7r-)RMVnabe7||@v>F*t5=`%l!5dQy6_5ZV00}?>kN_kA z2|xmn03-kjKmw2eBmfCO0+0YC00}?>kN_kA2|xmn03-kjKmw2eBmfCO0+7Jl5WpiX FzXJWfb@BiJ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2b9ec5216d3b99302caf0f59255ed388335a40e5 GIT binary patch literal 25821 zcmeI33piBU9>CWa%rwd4WTfjc9;rzkkCOL6ghT351|2kqDf9JUkWj}lPU+ND=;flv zxq48jn1qDfRD&3GqNb2rNhKtNYM6cZD6@UtGxv5*^4Se6hZeaI9LWk2-psEcC?$8^Kzj1zbP~U;qPD zS^$iIF)&4%fde2B2tPq%1V@@WReqYhx}u`G6;+37^}7r5cc4Thb;*$M zI2|CRgu^T0kZV8={kdc+fiP;KYkN_kA2|xmn03-kjKmw2eBmfCO0+0YC z00}?>kN_kA2|xmn03-kjKmw2eBmfCO0{=Y(WZOfZILozW^EX^C9q>|ldZQquaXv5g zRXl^}{1O30?-tCPDnx)+eHYf3 zw!}_xr7oco0Tp-3I_M3uB(pPEjgfZlK-96ogdGN*A!|#_R&h9ijrS@|PblmZ#wy0! z4imH{os|sB`K?}6N2V~%hJRfw-mw!U@)ZK86}!lP%T8{Wu}gjHIBa)j#EnhG^WqXF zJ5N$;a^KSAhNUA$KjtNf#hf@%9(qC#w&Fb7m6RK#pA7qUzxip^yELIY;V@2lva=+Y zQ=X(!vQpoDFQAjfB5gE{rNhUCT3)ePwg<^l^!6FOQUwFX!x;*uQq;-La^6ZX_w(UrfB|h*b0v;u!Mn%j|HLUH` zU2sa&f0(OLuFBNo`cN%4{e9 z+{objJ1ZNtiCjud00MSh75dBNa}UY&ETB(!e=_fwcYSYhckMdYzogDL&f0EOmwIjO z&nCB+TlT*>cxwu5`#c8QhHB~!F19>d-D9%q{F84}*RQD!OAoXS;-$qfC_;KS;Sepl z>KLuHyX=R^{l~xPc*bwVI@^uE_uLM;755*-UuWt|JT>u&d$yAOUp7}2uhZ8rW8BK= ziERC*)WzLh^sC%2RdD6u+KYYJH&Ch-;mA?1-CU{ zNFlE_eol)jWobS*d73IE(PaV@ttF)AwEnVSL3q>3MHpGvIEk^_7@9l0e<gk1uJ1CpMS5CE7nY z?Q_(Uw~I?DNF5s1rMwm#zg1GR_-;otE%8NC+S5(@x1Vh&%2lfmci(&|@(v@~l968T zDqp<3=?u%_gWbc9B&G$`@`2TQPT4a(g72wdFSG4g7Dt|BbVhXNaQlxO)Cf6R=+i3y z{~pd(T|9t1mOZXp?09s0=ZZ`(gXyzp8K-2E3BJ$y}9yK)+Z@zbXQ1Ti4!I=o)Fbs?oD(-0;`j zV35ydzWhbCXF(@nFt+1S(oiPu>G=I?}|DJ|v7j_rwW(}WxoRebNA6mBT5_j1P zTFFA~pe5Rrg7r%Au}TPNTs`;8E`v3z2z~2Tay`=v)lvpKb^Nk>_o*$s%;y^KIBa^^ zHbQrZeMo-LfnWGtR9^hdxQ0H>@|=e9nAx!96KVXq{>W~xY|q2(PQk8^Li}$#mJD{? z7qC?;d1+01w1xBmK|1MU)!H_ zIV!Ot#Md>b44W@YesH0_5gSIdHu1E6Tb$PCXCpDfv`%c%ws19exjZe=L*5iy4dwKT zuj-5K6eYTRzCv~Yo3*9qVPrieh9;K3;C-ItX$Eu|{>(D_Q{l^& z^nSyltosE)%W~Q6iMti!V~utS?(c4TCO@^2xv{})j+Syr=)-cSNZ!J*Bj$!4M+@9q zj>LJCT-`P>v)P2l{h$jE$2pC;%>1CMsg_Y2b78`8W9(Sb$tiBS9#b{%=OYGI<&nKTz->p_xTR_lOx2xBpLz zboMy{W?dXHsc0ZHy*Z7^qu2RyUjUpQ_azJ1lCNaUese_Y9E1KX zWpqQ8o*CKwbL!J6i6sYuxp#6s5D?9^jiIMa*pb0NkN_kA2|xmn03-kjKmw2eBmfCO v0+0YC00}?>kN_kA2|xmn03-kjKmw2eBmfCO0+0YC00}?>kif(fK<@t&UG=iQ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..dcddcabe0397f46caa0ec43ee0e46273cd0c5630 GIT binary patch literal 8998 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 G@WF5I&5XhT literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..36f6167c05c333c44ff904103c55c12069542dfd GIT binary patch literal 23342 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c1b3afdc6da97dc55fb6fe3365d214abe8384985 GIT binary patch literal 25866 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 SBmfCO0+0YC@RbqZH2wjZ4He4( literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..56fa46c99dea592e62b8a2b03cfbcead68e4e448 GIT binary patch literal 27405 zcmeI22Ut_d7Jw&oDN&?YXbOl}5S6M(Glmj3mQV#1rHk~Y3nHwdh>C~;0=rL2qy|Kq zh#-qAVnG%n5+GElavVvs%EYW0l=@ zKAtT3PrebmZz&Ax0Msk}N&9$JGwdCF>=311aB2Q=;kb6=Wg%aqrq??pB#>k z0K7>DKxi8PB6R@pTeAWk>pM0Ue{rn~kUo;gb$TE#C*TSkzye?l+<_fffk>;tO0Wv3 z(;ov2;OFJ#LQ9E>3(u94StKtjBfC&xnTE2$64fOOWmoF2 zQeC5^qocEE#Rg-{IwK8j9W90t6u*D~S_myUYnG&zqO79UR~LE(5a#2%#1qPik_8;X zC{AG%y%NkvhL01)nAq&X&BMpb#euY`&qAs>czHN^xe%4nc1~^-2amW}G?{mfy1DOt zuDpEV*(Q!>FFX|yTewwpmA+gQ(ogJDdyI|@SE9lndl@xHe*^?MQAjkL!ax^1TOb=U zS`9xS0Z0H6fCL}`NB|Om1Rw!O01|)%AOT1K5`Y9C0Z0H6fCL}`NB|Om1Rw!O01|)% zAOT2VN(ihK3U4xdeVW!e{BSUM*!;*)a62u1=xwl0%fZN=AUohHi$^7l zr;N%-*fU-!sS_V0jsIC;w#Yk+E}tHWr6a))>0q{+vrT6AaQQ$1ZCJZ-Sa&FLN-qEF zobViKOhlPbHoVE|*c6zj!;zjH{@koChG_&9OC6B@b?7e5W$3!cmACsS7Qb7N^}f!- zRA|o^usy)Nd7H>xQu3Pa%0d3~`HN#L)|)b2^H>)*2WoXLdq?WzE`Qq8vdyN(20ImD zeWed)rSM)cZTrnjFNVG>6=_`>aQnR3ju~5+R_3|4cy;w;=f23z79Wmu3UUZm3ljZG z4^yCyO1iwb!n8kXUDj2R^Ie-mGCO6zH}hSj$8_yb8g#3mJpbo}@=Mm;+W+Z~dEd~- zy22@l?n_<#>uw7bY{Dbcw|8+ryPY%bjjkBM*#_GFl2+v5rc>+jJ61K=*lob7Hf+U^ zSZU0BE+;DMM+8gwA<5uii(mGjcTckQ6}Irrs@qw2X3uttgS3y@YZYZNEKY7PBf2sd zi_?w?Dx?F`b;pvvblm=b^#mw)mzSkAY&zg^^v8;v`Jys2u>sL$;a2JSi2=uTpRY(S z>6{a0aIdlf)2JbQZ)mNyuU0}z6tnU{bN`7OL`Pef(vS=80XN&o1*_CFv~7Dkn&J{J zwl0x4pSR!F^&TEyYgx8d-BE#bh2P+1_D(4Y6%i93iZVJPK0K?N0~(ujW;@rE-^mUR zJu}!N!eb+zZK)$IkBhEv-&^06cD%6OaKu;DFR860(QppgX!S0>>AGvs#7pjS@qNAl z9s4u{$7b93;hZXz)KKYYA6jBW%;6QfH>6p9T>mVXwm;Q=Qir`HYl*~%n35}w*RPy% zB9l9ln;XV`#&EF)0f&`x)^v9w-|Qm@#&IZ)+i`LfI5WB=j=czylD;Zcb386}=+P*?qZ`_WPqabCttJzScqocpB+kGB;hN+Dk4vIYp zQHEA%iTFORAko7L;P~W3Ilk3bT0HG*5)BP6hWU8buxD7-xE=XegAjduZJlRxE%8Lh zg?THvrfc=YORB&ttj>r@sV)l+@Ac~>cT;lOo|=c@U39bdknl78UKdH;QLn{zV?yG} z)rs*6O&!;6!n%JXgW9;*%lE6otVFFbs=0(~q76Cb#S!6nv5fQ)GG6+D(YSMW(iG4! zdCI8PH1R`rCRKw*(h8|I0$4SdsxWMTVtNbl9r01ufIH^U#{42xo=x+ssJA_h{ms*k zJU-oerYi?8HcxY}MDfDwi<+zw=KQr9>g>_HQg6xGJ7O}E{hw*dmo78Wvv13GZw})+ z=iuVJqXa!;#oY&W(AY<4Cxls~;M-c`g-!1w0?v4#%&*zZ5Kkz|8gz*^!RkEMt&yo% z-3r$@-*{)iH4by33D-E^IFml<{%=|1;2O_G_7aBmSLHrEuzjWwq4-G3@gm}elvL6B z9`gIGRR6#`3#*iVom$d(slU6Wb%7p99nPuP3s&#=TInj7i>D#vA@R=GE37b5dI? zB^+<1d)1kQRrdR}NU~V{_Dw36W?Tt{82h4W?zL1{_m5$&1OdP4l>r`wA`Q*tug+Jgv4s$aN?&D14tb_YF%+0jF$v)0j_7?4rYr9e& z;9Rp`Rt?6N1=Q^}=&%3T(^Uf6}!OFE+=^s72^RrJX~Hv&Ye! z?W^+1s@%;4jT^SF=QcSfNHEn<`$Qvp8_NvTtrU(9y6%*dP!iGGaZ~Pqy0UOYYAjGp z5cM}aR-VWQ&_vimxfNKeoqtqf(M@=HjY`XJe6uGvJa#b4 z#YEzJ-niO&oIr@kvIZ(y-DJ3R##!t7G*&q{yT#X3Fi zNcxPqX=)PUW)J&M1MKvarOaqJl^!$GANmim@;XvNrYY3VxK$Nrr_=cS#!gRRogE1l zv$BFH0<64_X**Z>%vID`v(ss6{aNWLvy+%TR{zGz3WB%AXCO{Y6Z){NKgs8UZqdPN zniH)dg_i!#CY?5tc&nf}eTYVpqJ#H&19T8^OV3}4DW7bigKC-J!De4xTc%hu5o%na zw7tX6hO26VF@3t#5mw6o9ey%UN~MGIEe_HxG`Wh2xngJq5`Y9C0Z0H6fCL}`NB|Om u1Rw!O01|)%AOT1K5`Y9C0Z0H6fCL}`NB|Om1Rw!O01|)%zKnnjz3C5EP{NY{ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3cc4c24270f6b5c53bd8ac7634f758be6d17f910 GIT binary patch literal 29145 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 X0L%c)0L%c)0L%c)z&mCD&+qy-ifgn@ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7177ca051ec6c1d01511f07bed57d60f45ac9e01 GIT binary patch literal 23342 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 Gt^Wczr&5Ff literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3d43c191520f60acabd087ef99435ade63b26cf2 GIT binary patch literal 37775 zcmeI33slop9LMi}12%^YFhf*cqL5f15&=48Dk&{8<<&v4jOR|UiQt+PD3p2diK(18 zvoCjS)_#WLlb&-Oo!p&f z*t;&7=RTk1?d|R0@+`+^;d7pgyjhwc1jE>vX~MKJGqYmNwx7*<=%TJd=5)pf00e*l5C8%|00;m9AOHk_01yBIKmZ5; z0U!VbfB+Bx0zd!=00AHX1V%&P%@?~I*jpDBN7YF)Oq-SB$~bx2prSM-r`4kAI=5fi zDeqLH&DlA8}Vpe!;@@rjN(4-Vzc|TX5zlhb??ZTH2uo&20 z(z;eARSJo=)N%MvBsB5QndeTdEWguv#9exG?v8|eZ6*FZ}rChHu=Pi=Gly!+0U90b5l+RgdOYt-SfY>`r9x#gkEPdw*kFn05~CW9MJH z-aYVzOe$BS>5H<^bN!_f0P^nqE8qxklrmrd*qIi9czj)$sS4)Z7aoSt9SQX8AK`|D8KKGjokSAT0wS#dPywsMp)~k!qrZM&n($K z{8&0-hS)p3iiEcu!|mzU>~lv*caBH^coknNq} zb4JR3*iP>ZpLqd|oDeOF=WGs`1I@!!7g)xxr< zF1&bP{Q{Rm4Oge`=bvA8r7cw+I_)F36~{YHLb(4EyZ9<{tm41U!ZtELu)kjV!Bk!; zt!tpLS=M`2b(h7i?Uu+&WGdmQBo_NRH*WCF(7fX0y`rt3*mxyP*UFP?ZeQp>zTiw- znpi8p8AGR|?fG>%HTzy@^NU`^_Pw=4Rr#51pR`fM&i?RHacz%8CX=d8C9ylaUH^K( zq_o0GEB&;f00e*l5C8%|00;m9AOHk_01yBIKmZ5;0U!Vb cfB+Bx0zd!=00AHX1b_e#00MuDK#E%WI~#wybN~PV literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6196c474ac0eb47d53f007a496355cf761341493 GIT binary patch literal 25963 zcmeI23p|wD7Qpw+U^JfNEsybxR6^$vBNK{5HWXMUPOc967{V>z?>v!hdd)2vToZsH_8++Mnul23{-|I7be2d;he-4CK zn^~9v6bb+o@&a@!THV6bbi-OZ8#4>5l~W3|6hIo6769AHI|J;@O%z;Q-4rIrn9aj8 z(4Q&)PLKEecM(f_BRIbPm@p2(!P$a`l1a&aVBFGLpenU+H)<&D$w;Ym5b z!Oj$!LqTM9pU>otpUJ^H{dXdB9!$>n^beSnTM^lCyKm6swIlod;m)5CE8?tg(%!0mzL4K&pi`ra}Qg=l}q>ANzX*cuWPuh5STs z+5`aYG5`Xu0EpHCfLq55lx54>3pQ(2Kx|}@b^0MMAK(kTfda4qWZ(%j5lIJV0d1g5 ze+Ene4vXc*^5A%RdHHZSK0zsAK>-0lIdKWRlrlk8MTwxKIB$X8;(7DGo3E&(Wvu<3 zKGDd?NL6!%g{k2xy=6wkNhc^AAD^IrpscX4EKyxao%laDdI`Yu@;LDNV^B%}jYnbd zD0(@NL2`GxF$n-(ZY&Rm3ymlZgi#1%E^dAlnhQIvM`3u-T-=iOP6@}JV8tYK^&A3r zCf>}h!;91_DrtL&>KNPF?YM#1iX&b>xn$TP!(#X^M*+PB@MBO&NEkdIfo=t*h%fBm zuaE#F00}?>kN_kA2|xmn03-kjKmw2eBmfCO0+0YC00}?>kN_kA2|xmnz>Ek8$Rz6@ zp-OJkOFF?9v;M-m@rlzn$+!EPN8qA2N|V~D!%Rz8hkXmNR69NCA4WI{77)sX8EVw%cp|G#=Z#6-eX#+)_wDgL-2yO zB&xhVN%a^tj)Rskqfpc*R)y-_8xJhDv?%>7(Q;60r`(~aKdpb=Av{<$y702N+`|u> zMxu|{RCzBwA2Fk=agGs=lptU8mJVd!Bs}rA9X~qk7d`Upo)i1}mpe=c8Is~$s8?7Wa zwv6p@4c+bG3q7R52@Qmy$jTX|^j~d+PAJ`svmV^ns%Bl1b-Tx^&@zYo)VK1TR7JJ2 z6dgGC|0|v3JdmV4##J(ZydZTbtT&sGt(o?5LxFGQYf*5S_A0(%>{4}DHA8pZ3VoJo zqQlUYYT?3}CQKc4_ouPzI5d|spIFQtW0Sbg-Ph{muG8fEX|C}-_^4AA_z6nV^1nLg z{>$q-9YgX@>#^E`W}Zl+v#)xjyI+JV)G|Ke*!n;vuMP4Or3LQ&=jQBm!AxqHwckyJ zM6i;r0Y@pgx7Z8?^3wu}j&5GkJCWs)V(olWfN_pvAEs_~O8^G@^cz80L4n+30dliz>*PJqaC!>9UM5_7N zs=_|EIZP8|8LgO`G)&l4Y$|_zqhzepCX;G%U3}M&36iNvW5uJ`-Hp=3jF5d**mVzx zjn}K%Jj=c0`dqvQZcwo*a()VL-0rCb#p90#XL=+SRL0024Z0Iqw2eQx(a_AeJ?Efx zpS(hVaU)G|C_^XMWl$Qr`?J}Jc7}KBzT0u{n%;x1Cz)k$G+LXQqgu2+Zn%Vx?;7uD zlFi_-6l50;JQu<9FT30T0G@y0_Z;@w!6_QYChmcLT(H0aKaq1%?y6xPGX8XMV6H5! zuidM^R3R>IEX6&vIysLy5OFR4#V>cNp6z?AS=3|QD#D9{6TXf^B#BVemTFD)kD@ww z%3SCuS0oeO#@Pat7^mdOAIE{p*N9E`sk&j{gbL5q^giX5y|wy~ZB&~1NSRi+JJ;3k zgYj+s7%sn^A{h;>=E1j^JwLvvc6GM%4!QO^MqjIC_Q6=A#=Sf^)SS^TFO_LyxR8Uc zzAdZz(WDg(Ix5eS^}yYxlbY4r-Ey|BOf#ug9QTxpg8O1@YRDFMu6t~t{ri~7G^4t9+`qxBV z$@VI1NSm1Fc7PQBaj5~TXhVR!-)?tdGTt)rmT0PWE-xLdqJ!&1m!AAS%MbkP>W+^c-2LR^J&E_;Vbs)%g4Nl zuaM{>^a>3Jc+K{FWSgi^0SQ0?kN_kA2|xmn03-kjKmw2eBmfCO0+0YC00}?>kN_kA S2|xmn03-kj%#;9@-u!R0+c13q literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9bec2190bfec153bea0de1dd19be20349739e307 GIT binary patch literal 20299 zcmeI33sjRw7Jw%NNE!%`LJD<*L97%)Qwp0`lr2=9RwSpy}ao3eYMW9HwrJ@MgKLm)!nzLumS-XHU|4DLl@7#HO zcmB-$Gbhq+>3yKHejRfiK%oFYAs3K7P%&jXI|sUR*z1@qmnnma0YH+f6#y~Y;&~j` zHAb6!e2rwfnG_VlkDHNS@rjiCdzY+q06Ley!v3FBZ-s{OLy$}($SaKq~!(y=-IGl!-fsPg)uVqMlhhVUjyzD(=vhflV^Ocq+ zW;C-U##T<&GiS+CY8X9R z&(DvP5Qy;!)t7I*t-HkOZ`K}&4-qL$QPh+qt45FTvKC1108KOsX$hJD9KmPlnJ>A) zuaE#F00}?>kN_kA2|xmn03-kjKmw2eBmfCO0+0YC013Q)0`vw>PXi(HyV1l7A>{-s z&ns{!gFJLt8+$36mOR$vMjklsbQ%ACe9d3b!w(}Yjy7#c6%XCNzKeNp`QFCy+Ma8r zXS&HrbUT|Z9Z4I4&M<}(d5@xPG52oV(Cnt=cXUT<*XN(C^TU7ggkQbOR_n2!ckEi9 zdzd(51Bxd9I(tYDgv#wb75f?M8x$I}f`>?OS(|<+{DurdK{T+-0k8xX04Uh%2 ztm5&@8fCb2S>1c0dIhwk;_>EOvu9&}KV;Z{QDTBEl3C_`g58GEUhso_-ruED9l%YN z4aF3EXX{<<*KvpHzcq!}-`7O)XmQ<5b5f&uOqzW;(aD}j+87i(@~#6juke$y%C@Q< z2h&Jr%{Qs&IbbTr;eBwfugt;XQ)_OVecJ=0BP3Pkb z(XiE=S5eSQZe70KErD3;b#r@G@6l3_as3jE3NP<7Yd zwyr3F-?|Nz?2HD|`H)PzvXQ;}jf9zwEwr?{NGYfqn4T%EY^*w0PEWH3&U}-sSy0m6 z*xtmt_tGSbw!zyZzGI0xayu`#UPC|qhXGc*W;^ZrOB3%KTii*Db05SMRCjq-`rZ1F z>hGPB(7&vOWZl9(Nc-N9c55>7)Wj{<=c?&*cjPE-ncARY4vA7M#N3rRDCUq-le$rH zd10-Sq+L}{RjGrrg8u*dfU~m;2a~!?arwjZt&(e;9Aynton1I&a-zQWZ#zAnO(r!u zt#(o7Fn9GZ=(5$~8y0koxUTt;yA-_37dp9SW$m`+IzNuC+-_6*C^`Li`84VF?b+rV z4@)1Q@5&9fZ6aNI!Lj|?F;j*Td9DB>hngwFk%i>`hb4k#`rN&NmA8y|o!h;`KJu%E z@OmLFv6F!t;(d`d{D0_hb`|peBj_ zM;i2fT`l{nkA`3SsiKbT8r|7j)kHCsf{UR$<&W6AJ-DuuO=~)47ok=g4uPYsxcJ6wGbCC$W^y!uC=8DwhOKiK2+??32_4^lkfr@=O0(rX6 zgKeoh>wPo!n-gn)Eh;Wtv?Pz3TUJ~+DFxY!0Z!Sk&wS9sBhl9sC3rRn&juI#+29*G z$k79vdp6U-XY(;p5D}!pnj2Wcb}Nr``*xusd;ga+frxo-4{2- zFRV^wouJ0hxYSUOPS-I>W}&|9f0{5s0+0YC00}?>kN_kA2|xmn03-kjKmw2eBmfCO P0+0YC@Fx>MOMCterulm@ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8066c4ef0047f658d6d6d556f17157125a194957 GIT binary patch literal 27389 zcmeI2X;f2L5`b?KAOU0(qM#r(GDM63_$o3s+SAca&CfqT zO`sEw4Iwd6Lirc&h+kf-z&ilVi+{mBl}bD`EG7h*^a}aMMk5bLHXDk_3Y2&9PJtZ! zPCh7*qrE+e$eaR1)}?$P2Y(>%h>eOx<}?cCheSmS>3J^&OslRB*e0A%(8aE=+ZA$r63 zW<-!HIy@WzZV3QV{s73|0YD;9xY5bAlV|ZCVpT)dC?j!hMSc{p1%v@L;0z)`2v~wh z#=r;=fEm9X5P<|1i^GaZ;BYue2?%qSY6K1WWUX_nhtEU)e{8=rAkgP>`<0a=UxSC@jtf~P6SefXepmwXPGg+U=3!pH$@ z&}d^nu7wv!01|)%AOT1K5`Y9C0Z0H6fCL}`NB|Om1Rw!O01|)%AOT1K5`Y9C0Z0H6 zn4ttvFQXG$+g;0b_GMkoOj@hU(*3LF`G^$9)=CrV#*(4Ga9#e+?73f^vah#5zOSi0 zAM^IYvfK3YR@gUD+fETY%+@WgePw++`OHHGvnL75z;NATweAzZVOoe7bn{Mn;{*-=sTSz0$IAZQk%lgERTj+@AF(sm&P zBX>b##q=K&IWA6^fPj0ETreIP=EOvf<0lJU;iI00X^CMX$G^Bj7h^4A#-^*&LUXit zg_8p=;qdCMj7q#!Rx_zmA@L!X51!yrb>thV*BREG42>}@D?EV@@C2i(`SiC=3Hl*p z`oV^s#K&H${a*5t|MyI9re%{woe!;xrkz6#=zrv!O3H2V9`;xrQFC{M4{q~8h4-|~ zu<=drZdb}}4;S1BW~s6l&0nbLYtVA>BW>E&hM95wMIi|Q&MaF>HU(g-`mteQ=mH`pHIqe z=p55Dqc+Sm!t z*Ysu;jfC4C>5>Z?qi@V-El`QyOx-k`gnv=Q2jr`IggxZS(l%|2V^`D@&lLq_af9Ec z9qJ7_`ZB^y+)c-@QD{|Ldivv;$sM=Eg{$@ra}MqC`u=M!X1AiYP@XGvBL!Xl0W61> z3tw0-+c?Lcuj$)@TNDK7R&^`?tN`tZ^orJd#y++1EAk zuBYmlUk#~zfRUDX<*b6oTVuLt-j8-g2UENKtqP}6jY`NE$L|};u+%TPDg5NNEl4h% zvWl27>{9P-her(-vS8rw#HY-c&tL*f^)Ic)rHkvDnRF$?u%oNFm$I>gUV#IT?HQHi zO6wuFrqo5)-D~>7`(5MHZk^!#*m-Oae_U3j&;0DSHwirk^LA`rnikSRin7^J^Hb|q zg@)3KO%EzM^wK^2uc11cFZy5dT2OepWP9~ahNV441iJbJtXht7cJ)5g?yXk3>`Sv5 zD>Fzu?pM=VK3Ft%YS{DXGB=<6bBy0u+0t(HdFYQ7C*&5pJao}5AE9n4 z>X~KY6R#3B6zO&6`t^-*M<3L>5hW!Gz47Z){i*F)14dQ3LaVs@$1`!-wmvW3uM$7m zO5Xm|a@Ha>ny9Z())Bhd16}?hENdx=%&V&dgk4e=T!!z>G45Xg@`D% z-FO{CCNJe&=tWmmOK}lz-v`y&oS-)^%)?jJTzh@zunkmv0v=o0-9M<=S7rI6&Qpos zz1Esj8KGA)R&FFaeLKQbB(mdo&87zoYO$#iw9>-dvt?bZ?!sK_Tku;?kMqGwzvU(?Xgxf?mQe!_Qkq^7uT=A)jLL4( z{08gUW@ui;(r*&Awn$hW=)ZS%!=;#h%^<^w?{+@#bhs)LY!TPzw^jMAQ=25Gs$=;- zGpaCc>HoeG@H89Nv-bBhZyZXlFPD0f|L6I@QgouN)xG&(x8J2Q=cA!_7Fyq%W43xW z-K9R;R5hT5#Yoc~?OB*HGx~g|$D0uD5)yy}AOT1K5`Y9C0Z0H6fCL}`NB|Om1Rw!O c01|)%AOT1K5`Y9C0Z0H6fCPR60!e)K&!Yc}ivR!s literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..61aa0a2dc1360f613abff717be44f953d88f2b20 GIT binary patch literal 24838 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 b00}?>kN_kA2|xmn03-kj`~w8cIL-eB&C7{* literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f2777a14f01b88e31be552111fb35f0f267be5f8 GIT binary patch literal 28244 zcmeI22~<0df!od4h>n5gP;#5Vb5ykwyqajSrAT2{8g9A_NeZiU)Y^ zNH8rCVgX|eVgY?s6H%&GaRGv$XiKpVkVRxwKq2o2!cy&fym#!PoSB^5{4?J--^~2( zH#eD^$REmo2HKxHyEp?B3IG)H0P?43qKkur?(ZrxJ+JB7R9uXFS`23*o4+sla=qHG_+sxjk=u1%)Bi7gu z;zdJV*C0dQB%dBRS=~pDi{=6b&`Gx zR?nK|ML${|vFBXA&Qwd!Z)DSE;!U??BCT|ha6`@`M@2_?owsX&yc%dAfzdiDI$#Ot z(AHTx(gF%d01|)%AOT1K5`Y9C0Z0H6fCL}`NB|Om1Rw!O01|)%AOT1K5`Y9C0Z0H6 zfCMH20j$rORB^s{JH0v2X_uB2>zJsFVb5ZU+DsE#d?N6FYYr$$ zskC%v6jPeAz^!%f%U$ZlPF)#z8{E!8KXjDLxzx0W>63CQqCI#kUkIBkNOZH)Mv1!Z(z!Rp>QRRh94Iq*7_y^}6r zUl%qU*fDEHv0e^;anGR`U3}VCmaOPsP9$RUl%^sN|F^wRE{XZd1{w^0RKwF{EtAa6 z?dJ+*^geHzXrBHv6 z1XOCsL8Ou@{=wFg4HxUrF*fTg;Wi}eY3hpTJx*txj1A)J@T)4CX62Mzi1ZPCCGh5X zQ?^UI&v}MfO{LX6zGUH9;X>txv`ALRVpZJDDBhR8?RIla>5XhHd9b|*I{n!QqNPX8 z7IRw4`Wx7JPb%kH35?rg5Bn_>h*?(%g~Xg%>YHrd49quOh54?JZ@oHg{{6@MrE1db zk!%boaLsqkx%WzRe$LMEmYGIm>iOW9XiB%^Lx=%lfZ3o}FcWxDL`NP7y5(Pgcz(0A zU6T3e`Kows!#+k9Cs@>;?8Bp^#8Og9W&W|;n;BhCBTriekP zSD7tdQq?cu*6i*~sK1q#*;H-zC`;B5^Wd?rWBX8D#?|QJt$}w|WfoBh7B(U5<4GF& zUt2F)c^zk{3SIt4F0viW8-d*1s8?dCE?>;UmUPNItaOEPz^d>%iEK*?N`DaAkE~Gq zUzA(#-!01J8FwR>n*L%nd(HEyE#-}!lBVZL8RG?3G0GD2e14)z+!v7`28aP>1F~Y^ zq8PkvOgId=|LjU%=vrl`e)_83efnLM`d;E$Oj3m$6tXta`sJWHIIgG6CiKj44#8yl z6Xw^}l`AC}@)y&a&3zKoFs}?|Yjz*CShjy!LfN2eI~>t707{@HP>Bno```<$NOOW zaP6p8&f-IgZNR0o>Ngq(w*kI2e*W&zNkGFiBBe{(QN$8+6eY5 z(r*x4Vlh)ZW`~*kt*rf$TeQC0bUQ(5D>}aS)8Qt5N*j73{+_bQmSGzV_b0I1_g$lb zW9t0isqL5NH)k91A7Fc=D~@fd%H|E;-)yE9H@aD9e7G4p-pJrh%|SvC1H>@8RA4s1 zY#7Z3tdx0=&CR`Q{9~%^wod*!xJ0A%9-Ma^xX-=RAb8~VKAKM!ibH|JyWn6dl1;&OQmKyzY zJQsWU+S-O)1Am{<7Lf3sPLgzKuI+7~%cMM?^P%m}$zFm&D+;Gj4o>el@gpNR!}wQ2 zo*-|mFX~sTT4?H*Crlps^C5a4N|{$w17@|VucmtWF)cp1tUyg_pr2)O6(miN$&Z-^ zKGDu_+)vM9rFb2r^H^_4ef=cCiT+6pyJCFK*M9Cf8 ykN_kA2|xmn03-kjKmw2eBmfCO0+0YC00}?>kN_kA2|xmn03-kjOfUlSNB;)!9uwsN literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b3e4af6894654e847c096786bbe1433a5b3e7fb0 GIT binary patch literal 29169 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 Bx|{$2 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4a692bc6d5c820bc5f54678d6b255a323f3e701e GIT binary patch literal 19878 zcmeI3Ydn*DhJnE`Fn$%JBADvO0tpLJLJ|QN6Bpbh$_XqGLyDo05)$x_x5W@J zxVVI3{4*pOW8gsA@0XC3&t17n!_c?lU)$}D-Kg)8SHSApf4_R&5rF~)foh@J|JLun zR37tZDa3Dsq~S2Zyxl!p^QU3p5)c3c00BS%5C8-K0YCr{00aO5KmZT`1ONd* z01yBK00EIC(Dj{Ha`=<={_(_D#EPKR(Tq^%QBJ{y$Y}TH&&T`u&{p=4yAFJe_>%bj z@Q`8je!+0MZuWZK1c%wkhr-m4Bhkcw&N`ZkN6&<*J*zJxD?%h;3ys>!M!zu4W;F$_ zwd2M{jj7c)51Po~cpr>@b-!qAHl0p+Ytw)hw(8A{w(lP*WKgcVciNT~glv zgVDa7ZPW8MB<4&mPeR!0&lE`vLZhT!FEa4!IZ|p_=s#O0j@tIYXrmFYkg!|^`5kux zE^Lk43P*}$Ey3&c+ELQC+haqb4}(p`G*?pxs;O40s(R*@NF;*z;cX4>Z zp|nAKVY`Rs1Ly#^Y==1m+3K@#)L9LZ!b%f( z{&*BSzn)fZy!BfT_Gab8av)opf$A?IuU{(KziEEBwy@egilKy^xV*ieYjYDV&xcMa zhfSStAHLA3&wkz*h;zZ1iIAHqTAv$pa%~QaCX1IHen&}}=*m*q{)M$lwYOM3uxaE1 zx%+yw*JS00a>ppIR9;syQ-pX(IEyYoh7nhUhlw!-tS3P zEmtxXz6(k{wO7+~m#MYkhIoVZGz3L0C+*6EG)$a>^@F2NF5+S2T6)gQ{9{$F4P2jt ze{y8a%VF7L)^_z zCG?$B+t|*ab~U6xxjj{L{(IT8KLuu-q6dWA9~R4qTQ%IcWrU~uBDCaY$NcUHRG5j) zT@t&DR(o-hmmDym329A_5jyN&FJz078dHzDz*~wrXQ>87q()LxQ>C!~u|Fm0CY0JuZhrOF zlgHXr&Scf~wKh5(N)Yq-f#RI;q<~RcAa=Wqn0B0e_N48KuRtX{*}JzhpBmiip3vf< z&BWs~BXKUQm>8D@+KHTWg+Q*7#@wCGImD0L) zWVBA@P0>*+H;(&5%j{pOj2iK%}8fLdvVk8x<qh{HR_rDT6eX_Zh7j)D43DMR-*b=Fe z`scprd}fhBuF7JTxbb~QSY5C%KMpeG0v=ysrd-*$ES#m#l3aZ_@eq&4z~R!)CHnbn01rYl1YV{MKx z=cSKu(rR)G$O}K2p*cET);$ib3_fJ8aRqX%( literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a0cd06b1ab0f5d810badfc4cb5971b782b28509b GIT binary patch literal 21911 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 I09yFq|9~Kw6951J literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..69d0fd9bd73f30b622b92f710a0d43f8026bbafc GIT binary patch literal 38733 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..786763ac37f56a46b4199f17c50c6932bd99a1c5 GIT binary patch literal 25667 zcmeI2cU;p+7QiP7C5A3tbVCtElok{eq=mM)f*?&04* z2ncU8voHfF6aXmX0ho``N*2b(uGTwum|1K$ovT1g0ilU2JAE^%I^bf|o${y(Xfd3Xkr5Tg;~q6Q$HBZKuoWHGM~@X=75OE|JO3l(>EP}&0_#yPR;AwlL07=0zj%~Ubi|80HH_#O4|I~0^H^X!-YJf z$z%XVvj7lq0zkAK0Nn161D$U>KNlQgl||aDMB?;C9$vs3cmi2q0Vse3R1t{))PV-j zV$y&y!13~8d3kVHES3+4;}et;78DQ=TqQ0cBBg*=TrH26ms_)5TW!s{jqBv()r~YZ zZqe1(*H=_Eu`u5Ht+t-NF3SlD$HylqAh=Rkc%`nAypryJU6?n42$q`**MmXH1GESV zBZ6YqffdN~VNk5ZW)~h_#0-Pf84Dw73^xXc;pRa!ECUpV8_mTdX`gWKKCgt9twYf1 z`1B%?P1NOb+J)Cd?Z_892F2txZ2m5;VB}A5yMQkN_kA2|xmn03-kjKmw2eBmfCO0+0YC00}?>kN_kA z2|xmif&i+sFw%cxvxHAq?e*ObDnyZ@1FcOSZc^cu`p@OA3^Bp#ve}I_9e^z8<^D_S zKTD)5*J>7;lV8&K`z7+krFSHEWfTSZ{N%f=<&{l$c}cByR4(w;|BNjvVKViEuC_xL zdF0&P`c)~p-kJxUuksjF_7zC#)>l-$G`S;ssd0efLPLho65gX8_d9Rv>Zg))bkUf` zOj>Mu#G>H+bwLRDnolY=zPQ9VOEBJjk740IwLd%)t(T_6a>rlOr1qHG>m_N@NXBA| z>5O6C?DlXag?(vX*Vm$Gg<(_TKlSik_-U80UtM2B#O=eeqQj_-o}4)9G%>>|vuK*kQ@S&Mdt%2U(pV7^m_VuKxcK>;xTjn`@KbEjO#0UbQnF?J z&4Y)<+)`Snu24&LLMw)I9DJNo4>V4?x!1V~MJh1H({nmI<@VsJLt<+vyL5ig`|;fi z+V<9GJN8!T1b1j2qA1Hrf1*Q+#CNG#r53mJdyMmSWcg@nXFh^voPfNhC_E{^c3f?B zm<=woEYcqiNY&bt-LKL(aG>>ggUCc^7!y?UDK<3d;qu@64AP4Ev(ZcCJE8gpZpi*^ zrO$N01LdhF>n}LorI!vjb0?tFIHmj`>6<9W!zJ|Lo?sVmQv2mM!Tov_hD{Oj5u92q zEpX0GzYi#dck7GDl2INo+* zJke^5uB>Kb>Q#SuTh^h8n2Cb@c5hsTq2@0%9A%{5DOdEB#!>EP{0VLAbt~uZ?VT-0 zQ?*k33RU0B_nr*So2`kFJJ90Ypw0x^Q?rlqBbLg^QPi4Rm-CK=dvrV*Nvftfgl7$w z*)1u28q?OuvgQ2Lbzo-lUXdPcOYM>Ms8)YTNijj8MSGJPR#|rJJ9>JeHmBT2mk*|6!jR=NHil2YrWu;U{Q-|?C;O(xcQaHKRiJ;r?5A7HD0jqAFezc zoSMYGf!hgb*fdTp7MC79zeuyrWv@gwNLb?{I0TA`jX5N~=QXXdbbq&hVH`2Higs{( z7^4i9%CC5sWE?VsqZ%d|M>L!&RQ12r*uxVQrRL~-X{YJ^po*~@XS}z^glQDXEFYjV z!C7;L*dqahvt{%bLNtSnswlcx*;Z|DOP{Qz;hQSo-s^zKI zx_bG~eQJnVD?n;37yQ|(+nchb)ac$ino8`=JGHysQjVqDmh6`K#iyOREx4KFx3nET zUn}7@wc#AzIYl|nFNsjEe zuQhvP@3ja!PWRtR_D+*kGkZO=PuiMmi7b^-M*_s#bDtlijX%w{AG_^HEJz*nrJdnZ z;x^=f3^l*g^s0=#Xg-<N{}LY3W>h9Q*rc=b&|gY%*N5a zEAoBC1V<-k^wp8epx7>!eX0a~`L@vG@Fxak9=vpeQGBlpF+KI)G=}SXBN%HL#?wC; zhKn+R+-!Xv`V$)hMInO;+BLYxBaRfW4D-~%)T<&^?_z5A7j8_)HB@1G~u&)YYFQsfgg>m+g#M9qjuj(oHcYJ zUT42C*7K`*E$+LmDkN_kA2|xmn03-kjKmw2eBmfCO0+0YC00}?>kN_mG KI0y(ayZ;4J+iH*i literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ac158f211907a2a23129fc29e5f0a98c0dd7be8b GIT binary patch literal 25375 zcmeI2c~leE8o+NBAcQ6oS)Ym^BAW;<999hoA`M|*6iY!C*=krC!eT{8ahE>Cpz^?j zQVI%Kwo>pZwrJgI>8TQy(kN@iS7a$*m7R0~8Pmh*lw)82(cC*Hx!-)>cfYy!H{YDO zGjoM)!Y-ibw4JdX;BWxokPQfX@cImU``zxIZrd5o4s!;)IzS3>JpfVB9JZ&UotE#e z_q0Shv;>93#iH_`)JWWyt)l1vG_Cs+`(i5jJ)v=d&DOnk@ds>AP4_JPUOUL5T9>F{vok!k^C5uH%GDJMaL2qQIFpVjrQ?E zYG35H0>lAN;0Wvx`5!G3o-qXgSiAs$kSWmx=Kyf!FaYWTiB2aUfF*|jsC^I{#15M4 z3=#RqhlK$!dI$h$&wX~+w>1&L)G3+A(p91i0kP2)amd+~CykN_kA2|xmn03-kjKmw2eBmfCO0+0YC00}?> z(m+7=*Wi~?dxBf(|Jq$)ml73rK8?|wB?L-L^PcRpyQdje44nfFMU`!uLwz-AEag0Y z8*9lJNAR~*ZLLlDv5&T}3=9=ZuXI1-yLRm5C<1Qjbe1)l0V{_DJh2(%ihe3S{Et;*!Dy;?QHqigs zxOqZW$4;lGwegM9%j+1wt}XHw@xZ;pWO!6^g;GhxIiAu=3?#<-R|wRVS6)^qZSJoA z<%4Rvd{#+B$(lT)3(xP<88&@G^?n|cBv%)c(M^HNTqXr4AKI}aQ#1c>?Wwzh{Y?Lw z505uG&8Wy|nD%9bG0oCgU${KUno8Xlnmya8C6yp)9*+qC7qzrklPb=lP`u z^O;UHJv|+s5PVX8;z?9^B=tdF(b!Aek3!%w=C`yU58U-t)x)Pa5>xHwgI9Q)QmOsJx@0c~u0m{{LP z10h(PFV2a98mYf^##b3)k0Ya1+ZssEZI|VTrpv(rcJo$N=jOOODWP5?nyY)0y~3Fz z$u^s^rx+#^jGm?#`kkMfEjhV98os?IVww4%D(MB)UpF&8{7BTcwI##z z`9Q!H`)o!bYLnYxi8kS(HW)5q(7EG4`n_UypXtRwAl=Uu*jMJ;YT0{^RMN6Hd1^ej zhGKJTJoV^PXI`S7$@jC5%VKi6lcKz|V`P0Qsr8dF*V@jkG!8%9UE_TGI#pb+d-ETuXH(m&u4z%7b#=NHR*1pFEfYdy&HAq_ruL7Wn+7 zGd+(eU1bx6U8o}{Cf1{NHE{#3Aj97qOjK!yS589GMuMxu@k@0jMt#zkP~CuZWl zsfIzbeSr~W_bjH{4ozO@`g*6{Ee9PTNdI|U2%fsSx*eTuFS^;1c;p=aezE$01;*6N z*G)Ulx;Q}HKhD7U{Wmp-UccY)g~MMkgD)Jg;YeOMWDLwtxz^ZXdea1OhK>bC+GgnO zEPjW#k(*_T)n(t$Q#SP`YwC3if!ocI(b3Tcy@_{+DtZWqwXv*iN zi|haRQuwo47T%8EgcXSEi>d@p{V}QvZ8)b`T$dc?wws89pfwDJFuSOVm4t7#eo4uW zYSklGcjow(-YD??&6LOfzObOw)AgK{+su6<9@}ZgM(@3UKT7{-nZKc?WYNI3OA}Vj z(U1Ke^7|zAO5)$sCO;drtD}6*+*#3L@a#07q4Py!Q)9!%M4fs;03)^ZE6K;=6J6Z5 z`fMyZIJoL%QA7(I;HsAeKo;GFt6mmGw7>zbdRYJj?m_~P03-kjKmw2eBmfCO0+0YC l00}?>kN_kA2|xmn03-kjKmw2eBmfCO0+7I85l9vO^gCpSgHr$i literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..77b73416f710679ab517460bfb7f505a2490aa3d GIT binary patch literal 7186 zcmeHJX;f3!7CuRc1Z0*VMF{gGVNw(d5(R1)0w|+Qg(8Z~gn$Avm@et#iFs9#0s_UL zU;qaw)Pe#ss9=oBpw$ZaQ1ciR6d8jcq&GGY>atk+SY7>*b62vzyZ5(;`{m@W!~TW+ z6iC?NZSepG0{{kHfc+eSx6^qC-qwEm`v6=PKuf>?fbfVIlC!m?l82|462~{c z`~#wi^Y|kcsyq3RQyqZ&x*v&u$|baCZ*%}edINo8NKkWV*gX)I-uD}hvPCje{O01)<`AC#BN`z`+9S(Tt1dFVMKp=%!q1ABoIumuqy0O&zz126!FU^BZ5 zSOH-{L6qPsVH66rT3C3sn5=}DsHm6%T1HYKEfp=ZWiNIEkVFX(;ba6%1;8a?2uT?G z29SgH?)x<_007XaNC5;C+aLjhHnac&DJX#8hGB37Qef3ux04y|f|BSH9a5W(Nih|b zGKQ}Al`Wj095hsnSL!|Dw2_wl&D(S?H{D^Z}|%F72qqtSAeg; zKTH7yu`d`?_N}?ZpD5vW4+h*b3j=c~Z7FS&dnj$MDgLs_Z`gp`|F4MxPrtXUh)w!~ zMmjodfP2+kH2YN}{^M?ItNhWqHQ2yzx3fs+%XZRxX`cmNG*UH6Dp58{V4^G)YY#E= z|C$c4&Pl5gm7Y=echM%a(8uC8jvk_WkK7U7g4_li70MEAs^4WClE0+gIaxQN!W?ya zN2)H|`IlP#u9N>`M;-CQuLNO!Xy5OIo$0%-6?s0xmJid6Xa22i(U)9UK78PHh$HPK zL5vmASNO7cyyet%`t`c5DoBku;Q<;O$ zLOaUrg0*8^i?(QFnm_1e17g$wb=f0}Uu(ox_UVLGTCO2%*2_>uUB4!oT7`6aI$2jG z)R0A0``UZTyezzls6G(KwZ-YkNN%`b37U|S`~aNks} zC9g!WBfhiPiokUl6-`nMJ-rLcD66Xysa4Xu8kQpmqK)z2QEyyDRu3{LeUw$3*?OUf z)#$Z;aSb7Tg9Li?^zHgYw=;x0m{Fg(in1Pw_@^r5#jK_A;yx#Ivr7z&eKhpq1B^0s z=*~fenP#cGC^k5z@u*Bjp_JLHS&XM{n2jrcwM$Xk9AE2k;QXBsrSX2xwYKh;WVH6H_+5-8;8|f2iMPzgynE2$u4R|YnTomy-COJ zO|Of8Nf4@!vkX3~IM#Q#4=m?Tm7E=BZtuH3Q;uR@dYP7)cbt+?!<1!NJRX>g=_(#G zn!ZA*8&M%)^%K|%~}%7gis6LlrZ{oK5pIAM@oS{>3h8Pc8~65S*@{Fs0g@JZLcOgZ7CYed+KHxsVS=W zWgOS01NS+@(C+fQ_=EXeTS#7kVnqq#9j)0BE(BQz>@Qb;Nb$!^;W6EQAZ@W)e+nbY zhiI!)n-3f&7o-)~)oB-=?{U0vJja9Ok~_P-rN;MGvYPjtUC*~)MV>x4WTsy8-N}OK zi0c*JS3FJ44W0UubIXS^U%NY|zNitSNA&I)7;kV$Dt$(r!nH+>8~mufvk03}Q__(x z#Soh&u|cs3E3}h!h`R2>^e-C5#4{A z4H{~Q!G{8nf8=iKuWd}Z^R@~d+vsx9@tfe_CeKg;%0WEPhJcx}G)x^9pYA7r_{{u! Vz5;v&_zLh9;4APCQ$UgZ_*cRz0cijL literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6f2b2a324c01929d06875e4f251e347a763b78ff GIT binary patch literal 9057 zcmeHK2~-nV7X2ZS1PEXh5gD2#ECV7Si_jVf%GxLpc4TmANIM$UR4fU?ay#DRJwojN z9{(W3VIFSQ5GM!1+B;@o{~4GT5fTA$?h5(DkT4;B3Sn~!dAD#ZVF)Wm2LyY%L+8!V zt^~q?8?Xmf5dOc$w4c;G0KOaq0G>LX^Unl;c@O}F=INY94gj(T0ASRH_=WksD+U1_ z#R3BX=*tH{+6RE8bpS~EP8B-ccluuZo4Kk&eUzYiQlV`JAcFv)3LHQ%AOd{|8G+Tn z5SR)$z#2#*ktn3NBnpL+l9ZH^QCKV^EiLoKGI@-Gx{9U-Rt2l3rE6xOrDLL_hF!hE z&}1zRkH>53+c;R8e`RKY#|cGXl2TGK(lSbm7c1ekvD&x=hoBN*P>W2(df+fD5W~RW z7?|J|P=wa*yO=Tn03=F$5dscfvs?^=EJh#^;z$%6f>4OT7r_wX%aQV)UTH_!n20kE zmn>alcImR*GBrcf@G9&ESC7CW_0O&Razi13Wsvf;?mHwLQx5ZS76_UF8X7_j1IGYM zkh(wdqi&+6$N`Z9A_qhch#U|(AaX$Dz?=?9n#w-ZV))%#mU01qs4Fj7#dCl_xbiTO zgBJkj+V(MJ-uhVgH?l)w>sI-|#tChfjU?ot<$eM9T0QS}s>-7Y_;~Vm<)C`EoizGz zg8(=U_K$n#4p6;+o2X$74UKvU00_yMbLfR6O@b53o<5B=qSk)H3{_Mf4i6o>S7A}i zbpeP9g37^8?(l$3f#&F*F$3q`iab@TO525~&&3r_svBlyrSP0ax+8nkty_A6Ei=vj zd5v=pfQ-)PugY-c6W^y#502W5%9%@MA-R&EJ1hL|@%eF}65Bt7&08A$Q+s4~UV|SO^O##PBIN<{$duM`NU14L)b8Nc%VS1MS zaOZ6_3sO?C(#6FNsx73dkGZhWhjz`hk|&-1h3=7+sHap=Hs>TQf@QNK&lni zHVGvPjuU7*pI$eJMt(a9wI=(Qz4}a2S)JLkt_4>U$@c?{y2F!6Z*r4$jQH`5MFTNi zboq#U!nRvyhL2CU*sTAgl_nlvcZW5yl}G*MPIOy@Vb6s3-mKS`T360C`)FrZ(aUfBnNRa(UIXX!uzJ}U=UdWheA!dEI5xIvB#PT<{m)M+ufbOgDK|eRfcD8oV=CssD8b%{$`@nceq0a1=Z-C7k7ke4w}@V ztB)zvfBoyTO)rgZDw0<`b=c=*0{^^%`B}bIp8U019xuz*;fj{#F%+`PUjxoBZP>-s!JqAPFW=hXL#5`fjCQf+2tc)7O!K{MoZZU1EX~5djqnb= z{d2Hn(P;X!UL|+pcXe?n$`w9yf~#!B9^&>ML}^4#l8t?IIl-rE?0Ll`Jp)S18<$L} zO&nUeoa*5&O*-Y#-h#0j(gjLs${LjuC8<}-nDln5Nn)IRQF=Gim3O*-&5<(3OY=uD zyLj)_B_@dy%9p(vT>BF#4;nTOaSHlfM+zs^wf?kav|98Ox)y9eFXxQ&`qA=3H%Xlj z?E{Vpz!OU0FRS_^Sx0|q8Hn>4?c{8okMe8Yr`(X4}B`2+NtG@dP4 zIgb<@z!=XaujUYoYl@_q<{CZmWZ0ShZ{U<#30%&fCzXq5^Neo3Ih8?3vn$xQl9c({ z)~MFqNTymZ;mU+dscD?B zsfp=~z&m@-`1HgZ{M1P|?W|0=J9;YPO7K5W(b++=e}5UZt)?_YtX}!80+71=>W4pv4&x_sv1$%&H3Fc1<94ZC5mEO*R=QnxSh~JrtFJV3k9s_Z!e`Vn p8M-mGSCw-9_fP$qZi=wT0g(eD2Sg5t91uAmazNz3e|A97^e?9~i5CC> literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ffe534d557d4956427eb1e5dfff2966f4b3c0e57 GIT binary patch literal 11813 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 wbugVw(m~t&PBI`yR%vRC0-i?k3F+zJ@dZnUF&_ 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 + + + + + + + + + + + + + + + + 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, + }) + } +}