diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a166e1268..c9e311cae 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -12,12 +12,5 @@ this PR. # Checklist -- [ ] Did you update relevant documentation? -- [ ] Did you add tests to cover new code or a fixed issue? -- [ ] Did you update the changelog? -- [ ] Did you check all checkboxes from the linked Linear task? - - +- [ ] Did you check all checkboxes from the linked Linear task? (Ignore if you + are not a member of Econia Labs) diff --git a/.github/workflows/check-n-lines-changed.yaml b/.github/workflows/check-n-lines-changed.yaml new file mode 100644 index 000000000..eaf125d3c --- /dev/null +++ b/.github/workflows/check-n-lines-changed.yaml @@ -0,0 +1,135 @@ +--- +env: + GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + MAX_LINES_ADDED: 300 + MAX_LINES_REMOVED: 500 + OVERRIDE_N_APPROVALS: 2 +jobs: + check-n-lines-changed: + env: + # If run from a merge group, the action should not run. However there is + # no way to exit early in GitHub actions per + # https://github.com/actions/runner/issues/662, so using a conditional + # check for each step serves as a workaround. + IS_MERGE_GROUP: '${{ github.event_name == ''merge_group'' }}' + runs-on: 'ubuntu-latest' + steps: + - uses: 'actions/checkout@v4' + with: + fetch-depth: 0 + - id: 'get-pr-number' + if: '${{ env.IS_MERGE_GROUP != ''true'' }}' + name: 'Get PR number' + # yamllint disable rule:indentation + run: | + PR_NUMBER=$( + if [ "${{ github.event_name }}" = "pull_request_review" ]; then + echo "${{ github.event.pull_request.number }}" + else + echo "${{ github.event.pull_request.number }}" + fi + ) + if [ -z "$PR_NUMBER" ]; then + echo "No PR number found. Exiting." + exit 1 + fi + echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT + # yamllint enable rule:indentation + - id: 'get-base-branch' + if: '${{ env.IS_MERGE_GROUP != ''true'' }}' + name: 'Get PR base branch' + run: | + PR_NUMBER="${{ steps.get-pr-number.outputs.number }}" + BASE=$(gh pr view "$PR_NUMBER" --json baseRefName -q '.baseRefName') + echo "Base branch: $BASE" + echo "base_branch=$BASE" >> $GITHUB_OUTPUT + - id: 'get-insertions' + if: '${{ env.IS_MERGE_GROUP != ''true'' }}' + name: 'Get number of lines added' + # yamllint disable rule:indentation + run: | + BASE_BRANCH="${{ steps.get-base-branch.outputs.base_branch }}" + git fetch origin $BASE_BRANCH + INSERTIONS=$( + git diff --stat origin/$BASE_BRANCH | \ + tail -n1 | grep -oP '\d+(?= insertion)' || echo "0" + ) + echo "Number of lines added: $INSERTIONS" + echo "insertions=$INSERTIONS" >> $GITHUB_OUTPUT + # yamllint enable rule:indentation + - id: 'get-deletions' + if: '${{ env.IS_MERGE_GROUP != ''true'' }}' + name: 'Get number of lines removed' + # yamllint disable rule:indentation + run: | + BASE_BRANCH="${{ steps.get-base-branch.outputs.base_branch }}" + git fetch origin $BASE_BRANCH + DELETIONS=$( + git diff --stat origin/$BASE_BRANCH | \ + tail -n1 | grep -oP '\d+(?= deletion)' || echo "0" + ) + echo "Number of lines removed: $DELETIONS" + echo "deletions=$DELETIONS" >> $GITHUB_OUTPUT + # yamllint enable rule:indentation + - id: 'get-approvals' + if: '${{ env.IS_MERGE_GROUP != ''true'' }}' + name: 'Get number of active approving reviews' + # Sum up the number of approvals across all reviewers, only counting a + # review as approving if it is the last review by that reviewer. + # yamllint disable rule:indentation + run: | + APPROVALS=$( + gh pr view ${{ steps.get-pr-number.outputs.number }} \ + --json reviews | \ + jq ' + .reviews + | group_by(.user.login) + | map(last) + | map(select(.state == "APPROVED")) + | length + ' + ) + echo "Number of approvals: $APPROVALS" + echo "approvals=$APPROVALS" >> $GITHUB_OUTPUT + # yamllint enable rule:indentation + - if: '${{ env.IS_MERGE_GROUP != ''true'' }}' + name: 'Check size versus approvals' + # yamllint disable rule:indentation + run: | + INSERTIONS="${{ steps.get-insertions.outputs.insertions }}" + DELETIONS="${{ steps.get-deletions.outputs.deletions }}" + echo "$INSERTIONS lines added (max ${{ env.MAX_LINES_ADDED }})" + echo "$DELETIONS lines removed (max ${{ env.MAX_LINES_REMOVED }})" + NEEDS_OVERRIDE="false" + if [ "$INSERTIONS" -gt "${{ env.MAX_LINES_ADDED }}" ]; then + NEEDS_OVERRIDE="true" + fi + if [ "$DELETIONS" -gt "${{ env.MAX_LINES_REMOVED }}" ]; then + NEEDS_OVERRIDE="true" + fi + if [ "$NEEDS_OVERRIDE" = "true" ]; then + APPROVALS="${{ steps.get-approvals.outputs.approvals }}" + OVERRIDE_N_APPROVALS="${{ env.OVERRIDE_N_APPROVALS }}" + if [ "$APPROVALS" -ge "$OVERRIDE_N_APPROVALS" ]; then + echo "✅ Changes exceeded limits but have required approvals" + else + echo "❌ Too many changes. Need $OVERRIDE_N_APPROVALS approvals" + if [ "${{ github.event_name }}" = "pull_request" ]; then + echo "If the PR author hasn't updated this PR since enough" + echo "approvals were left, you must manually trigger a re-run" + fi + exit 1 + fi + else + echo "✅ Changes within limits" + fi +# yamllint enable rule:indentation +name: 'Check number of lines changed' +'on': + merge_group: null + pull_request: + branches-ignore: + - 'production' + - 'fallback' + pull_request_review: null +... diff --git a/.github/workflows/conventional-commits.yaml b/.github/workflows/conventional-commits.yaml new file mode 100644 index 000000000..8c8b3fe3b --- /dev/null +++ b/.github/workflows/conventional-commits.yaml @@ -0,0 +1,34 @@ +# cspell:word amannn +--- +env: + GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' +jobs: + conventional-commit: + env: + # If run from a merge group, the action should not run. However there is + # no way to exit early in GitHub actions per + # https://github.com/actions/runner/issues/662, so using a conditional + # check for each step serves as a workaround. + IS_MERGE_GROUP: '${{ github.event_name == ''merge_group'' }}' + runs-on: 'ubuntu-latest' + steps: + - env: + GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + if: '${{ env.IS_MERGE_GROUP != ''true'' }}' + uses: 'amannn/action-semantic-pull-request@v5' + with: + requireScope: true + scopes: '^ECO-[0-9]+$' + subjectPattern: '^[A-Z].*$' + validateSingleCommit: true + validateSingleCommitMatchesPrTitle: true +name: 'Verify conventional commit PR title' +'on': + merge_group: null + pull_request: + types: + - 'edited' + - 'opened' + - 'reopened' + - 'synchronize' +... diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index d1e19c587..ead890362 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -64,6 +64,7 @@ jobs: extra_args: '--all-files --config cfg/pre-commit-config.yaml' name: 'pre-commit' 'on': + merge_group: null pull_request: null push: branches: diff --git a/.github/workflows/sdk-tests.yaml b/.github/workflows/sdk-tests.yaml index 9af3d7953..b47e64d36 100644 --- a/.github/workflows/sdk-tests.yaml +++ b/.github/workflows/sdk-tests.yaml @@ -37,6 +37,7 @@ jobs: timeout-minutes: 15 name: 'Run the SDK tests' 'on': + merge_group: null pull_request: null push: branches: diff --git a/.github/workflows/submodule.yaml b/.github/workflows/submodule.yaml index d68e1e944..e6f879a95 100644 --- a/.github/workflows/submodule.yaml +++ b/.github/workflows/submodule.yaml @@ -11,7 +11,9 @@ jobs: working-directory: 'src/rust/processor' name: 'Ensure correct version of processor submodule' 'on': - pull_request: null + pull_request: + branches: + - 'main' push: branches: - 'main' diff --git a/.github/workflows/verify-doc-site-build.yaml b/.github/workflows/verify-doc-site-build.yaml index 0c1ea5d10..475a96cdb 100644 --- a/.github/workflows/verify-doc-site-build.yaml +++ b/.github/workflows/verify-doc-site-build.yaml @@ -15,12 +15,7 @@ jobs: - run: 'pnpm build' name: 'Verify docs site build' 'on': - pull_request: - branches: - - 'main' - - 'production' - paths: - - 'doc/doc-site/**' - - '.github/workflows/verify-doc-site-build.yaml' + merge_group: null + pull_request: null workflow_dispatch: null ... diff --git a/README.md b/README.md index e8d52ac6a..329d1ceeb 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,11 @@ *Sponsored by a grant from the Aptos Foundation* -The emojicoin dot fun Move package is audited: +The emojicoin dot fun and emojicoin arena Move packages are audited: - [PDF Report] -- Corresponding `git` tag [`move-v1.0.1-audited`] +- `git` tag [`move-v1.0.1-audited`] +- `git` tag [`arena-move-v1.0.0-audited`] @@ -199,4 +200,5 @@ git submodule update --init --recursive [pre-commit shield]: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit [uploading environment variables with vercel's ui]: https://github.com/user-attachments/assets/d613725d-82ed-4a4e-a467-a89b2cf57d91 [vercel cli]: https://vercel.com/docs/cli +[`arena-move-v1.0.0-audited`]: https://github.com/econia-labs/emojicoin-dot-fun/releases/tag/arena-move-v1.0.0-audited [`move-v1.0.1-audited`]: https://github.com/econia-labs/emojicoin-dot-fun/releases/tag/move-v1.0.1-audited diff --git a/cfg/cspell-frontend-dictionary.txt b/cfg/cspell-frontend-dictionary.txt index c8cddf201..6d523f2f8 100644 --- a/cfg/cspell-frontend-dictionary.txt +++ b/cfg/cspell-frontend-dictionary.txt @@ -56,3 +56,4 @@ bytea nominalize dexscreener clippyts +xlarge diff --git a/doc/doc-site/docs/resources/audit.md b/doc/doc-site/docs/resources/audit.md index a950afa85..6522ee773 100644 --- a/doc/doc-site/docs/resources/audit.md +++ b/doc/doc-site/docs/resources/audit.md @@ -4,10 +4,12 @@ title: 🧑‍💻 Audit hide_title: false --- -The emojicoin dot fun Move package is audited: +The `emojicoin dot fun` and `emojicoin arena` Move packages are audited: - [PDF Report] -- Corresponding `git` tag [`move-v1.0.1-audited`] +- `git` tag [`move-v1.0.1-audited`] +- `git` tag [`arena-move-v1.0.0-audited`] [pdf report]: https://econia-labs.notion.site/emojicoin-dot-fun-audit-8806ffea2b594c8e846ce3d32e5630b9 +[`arena-move-v1.0.0-audited`]: https://github.com/econia-labs/emojicoin-dot-fun/releases/tag/arena-move-v1.0.0-audited [`move-v1.0.1-audited`]: https://github.com/econia-labs/emojicoin-dot-fun/releases/tag/move-v1.0.1-audited diff --git a/package.json b/package.json index f86701de1..573daa5d6 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "test:frontend": "pnpm --prefix src/typescript run test:frontend", "test:frontend:e2e": "pnpm --prefix src/typescript run test:frontend:e2e", "test:sdk": "pnpm --prefix src/typescript run test:sdk", + "test:sdk:arena": "pnpm --prefix src/typescript run test:sdk:arena", "test:sdk:e2e": "pnpm --prefix src/typescript run test:sdk:e2e", "test:sdk:unit": "pnpm --prefix src/typescript run test:sdk:unit", "test:verbose": "pnpm --prefix src/typescript run test:verbose", diff --git a/src/cloud-formation/deploy-indexer-alpha.yaml b/src/cloud-formation/deploy-indexer-alpha.yaml index a2cf7e951..0dcf35d4f 100644 --- a/src/cloud-formation/deploy-indexer-alpha.yaml +++ b/src/cloud-formation/deploy-indexer-alpha.yaml @@ -1,6 +1,6 @@ --- parameters: - BrokerImageVersion: '5.0.0' + BrokerImageVersion: '6.0.0' DeployAlb: 'true' DeployAlbDnsRecord: 'true' DeployBastionHost: 'true' @@ -20,7 +20,7 @@ parameters: Environment: 'alpha' Network: 'testnet' ProcessorHealthCheckStartPeriod: 300 - ProcessorImageVersion: '5.0.0' + ProcessorImageVersion: '6.0.0' VpcStackName: 'emoji-vpc' tags: null template-file-path: 'src/cloud-formation/indexer.cfn.yaml' diff --git a/src/cloud-formation/indexer.cfn.yaml b/src/cloud-formation/indexer.cfn.yaml index c977552f1..4f2302e36 100644 --- a/src/cloud-formation/indexer.cfn.yaml +++ b/src/cloud-formation/indexer.cfn.yaml @@ -56,6 +56,9 @@ Conditions: EnableWafRulesWebSocket: !Equals - !Ref 'EnableWafRulesWebSocket' - 'true' + IndexArena: !Equals + - !Ref 'IndexArena' + - 'true' Mappings: Constants: # These compromised credentials are not a security risk because access to @@ -203,6 +206,12 @@ Parameters: Type: 'String' Environment: Type: 'String' + IndexArena: + AllowedValues: + - 'false' + - 'true' + Default: 'true' + Type: 'String' Network: AllowedValues: - 'mainnet' @@ -1418,9 +1427,12 @@ Resources: - Name: 'EMOJICOIN_MODULE_ADDRESS' Value: !Sub '{{resolve:ssm:/emojicoin/package-address/${Network}}}' - - Name: 'EMOJICOIN_ARENA_MODULE_ADDRESS' - Value: !Sub - '{{resolve:ssm:/emojicoin/arena-package-address/${Network}}}' + - !If + - 'IndexArena' + - Name: 'EMOJICOIN_ARENA_MODULE_ADDRESS' + Value: !Sub + '{{resolve:ssm:/emojicoin/arena-package-address/${Network}}}' + - !Ref 'AWS::NoValue' - Name: 'WS_PORT' Value: !FindInMap - 'Constants' diff --git a/src/docker/compose.yaml b/src/docker/compose.yaml index ef320a385..baae8fd15 100644 --- a/src/docker/compose.yaml +++ b/src/docker/compose.yaml @@ -15,7 +15,7 @@ services: PROCESSOR_WS_URL: 'ws://processor:${PROCESSOR_WS_PORT}/ws' PORT: '${BROKER_PORT}' RUST_LOG: 'info,broker=trace' - image: 'econialabs/emojicoin-dot-fun-indexer-broker' + image: 'econialabs/emojicoin-dot-fun-indexer-broker:6.0.0' container_name: 'broker' healthcheck: test: 'curl -f http://localhost:${BROKER_PORT}/live || exit 1' @@ -83,7 +83,7 @@ services: depends_on: postgres: condition: 'service_healthy' - image: 'econialabs/emojicoin-dot-fun-indexer-processor' + image: 'econialabs/emojicoin-dot-fun-indexer-processor:6.0.0' container_name: 'processor' healthcheck: test: 'curl -sf http://localhost:${PROCESSOR_WS_PORT} || exit 1' diff --git a/src/docker/deployer/sh/build-publish-payloads.sh b/src/docker/deployer/sh/build-publish-payloads.sh index ae967a333..296f59b35 100644 --- a/src/docker/deployer/sh/build-publish-payloads.sh +++ b/src/docker/deployer/sh/build-publish-payloads.sh @@ -38,6 +38,28 @@ aptos move build-publish-payload \ --json-output-file $json_dir/market_metadata.json \ --skip-fetch-latest-git-deps +# Ensure the default duration const is in the source file exactly as expected. +original_const="const DEFAULT_DURATION: u64 = 20 \* 3_600_000_000;" +source_file="$move_dir/emojicoin_arena/sources/emojicoin_arena.move" + +# Ensure there's exactly 1 appearance in the source code. +if [ $(grep -c "$original_const" "$source_file") -ne 1 ]; then + log_error "Couldn't find constant DEFAULT_DURATION in the arena move code." + exit 1 +fi + +# Replace the default duration with 1 microsecond. +# Instead of trying to finagle setting the value here, just set the first +# one to be really short, and let the test suite set the next melee duration +# and end the first melee immediately. +replacement_const="const DEFAULT_DURATION: u64 = 1;" +sed -i "s/${original_const}/${replacement_const}/" "$source_file" + +if [ $(grep -c "$replacement_const" "$source_file") -ne 1 ]; then + log_error "Couldn't replace the DEFAULT_DURATION value." + exit 1 +fi + aptos move build-publish-payload \ --assume-yes \ --named-addresses \ diff --git a/src/move/emojicoin_arena/tests/tests.move b/src/move/emojicoin_arena/tests/tests.move index 29a1c5ad1..6b2a4a60f 100644 --- a/src/move/emojicoin_arena/tests/tests.move +++ b/src/move/emojicoin_arena/tests/tests.move @@ -2020,21 +2020,16 @@ module emojicoin_arena::tests { init_module_with_funded_vault_and_participant(); set_randomness_seed_for_crank_coverage(); - // Initialize all coverage conditions to false. + // Declare coverage condition for unequal market IDs, which must be marked true for the loop + // to exit. + let covered_unequal_market_ids; + + // Initialize all remaining coverage conditions to false. let covered_equal_market_ids = false; - let covered_unequal_market_ids = false; let covered_sort_order_market_id_0_hi = false; let covered_sort_order_market_id_0_lo = false; let covered_melee_ids_by_market_ids_contains = false; - // Call all coverage conditions to silence erroneously compiler warnings about unused - // assignments per https://github.com/aptos-labs/aptos-core/issues/15713. - covered_equal_market_ids; - covered_unequal_market_ids; - covered_sort_order_market_id_0_hi; - covered_sort_order_market_id_0_lo; - covered_melee_ids_by_market_ids_contains; - // Declare market IDs. let sorted_unique_market_ids; diff --git a/src/rust/broker/README.md b/src/rust/broker/README.md index 5303e8a59..d0b1e2271 100644 --- a/src/rust/broker/README.md +++ b/src/rust/broker/README.md @@ -37,12 +37,29 @@ Note that they're typed *exactly* as shown below. If you try to send the JSON payload over multiple lines through `websocat`, it will error out and parse the JSON payload incorrectly. -### All markets, all event types +### Arena events + +Arena events don't follow the same rules as the other subscriptions. + +To subscribe to arena events, simply pass `{ "arena": true }`. The default value +is `false`; i.e., no subscription to arena events. + +```json5 +// Subscribe to every single event type. +{ "arena": true, "markets": [], "event_types": [] } + +// Subscribe to all non-arena event types. +// Both of the JSON objects below are equivalent. +{ "markets": [], "event_types": [] } +{ "arena": false, "markets": [], "event_types": [] } +``` + +### All markets, all non-arena event types ```json5 // All of the below are equivalent. // Remember, with `websocat`, your message should be exactly one line. -// This is four different ways to subscribe to all markets and event types. +// This is four different ways to subscribe to all markets and non-arena events. {} { "markets": [] } { "event_types": [] } @@ -51,7 +68,7 @@ the JSON payload incorrectly. ### Specific markets, all event types -To subscribe to markets 4 and 5 for all event types: +To subscribe to markets 4 and 5 for all non-arena event types: ```json { "markets": [4, 5] } diff --git a/src/rust/broker/src/processor_connection.rs b/src/rust/broker/src/processor_connection.rs index 53777a24f..6a95c5faf 100644 --- a/src/rust/broker/src/processor_connection.rs +++ b/src/rust/broker/src/processor_connection.rs @@ -4,7 +4,7 @@ use std::{ }; use futures_util::StreamExt; -use log::{error, info, log_enabled, warn, Level}; +use log::{error, info, warn}; use processor::emojicoin_dot_fun::EmojicoinDbEvent; use tokio::sync::{broadcast::Sender, RwLock}; use tokio_tungstenite::connect_async; @@ -64,7 +64,7 @@ pub async fn start( retries = 0; } - // If this is the first connection and it was unsuccessful, do not try to retry. + // If this is the first connection and it was unsuccessful, do not retry. if first_time && !connection_successful { break; } @@ -132,16 +132,15 @@ async fn processor_connection( ); continue; } - let msg = res.unwrap(); if is_sick { *processor_connection_health.write().await = HealthStatus::Ok; } - if log_enabled!(Level::Debug) { - info!("Got message from processor: {msg:?}."); - } else { - info!("Got message from processor: {msg}."); - } - let _ = tx.send(msg); + // Log the JSON string. + info!("Got message from processor: {msg}."); + + // And send the actual db model event. + let db_msg = res.unwrap(); + let _ = tx.send(db_msg); } info!("Connection to the processor terminated."); *processor_connection_health.write().await = HealthStatus::Dead; diff --git a/src/rust/broker/src/types.rs b/src/rust/broker/src/types.rs index e29dda799..03f76463a 100644 --- a/src/rust/broker/src/types.rs +++ b/src/rust/broker/src/types.rs @@ -19,4 +19,6 @@ pub struct Subscription { pub markets: Vec, #[serde(default)] pub event_types: Vec, + #[serde(default)] + pub arena: bool, } diff --git a/src/rust/broker/src/util.rs b/src/rust/broker/src/util.rs index 65bc4aa15..22620859c 100644 --- a/src/rust/broker/src/util.rs +++ b/src/rust/broker/src/util.rs @@ -30,28 +30,36 @@ pub fn get_market_id(event: &EmojicoinDbEvent) -> Result { /// Returns true if the given subscription should receive the given event. #[allow(dead_code)] pub fn is_match(subscription: &Subscription, event: &EmojicoinDbEvent) -> bool { - // If all fields of a subscription are empty, all events should be sent there. - if subscription.markets.is_empty() && subscription.event_types.is_empty() { - return true; - } - let event_type: EmojicoinDbEventType = event.into(); + match event_type { + EmojicoinDbEventType::ArenaEnter => subscription.arena, + EmojicoinDbEventType::ArenaExit => subscription.arena, + EmojicoinDbEventType::ArenaMelee => subscription.arena, + EmojicoinDbEventType::ArenaSwap => subscription.arena, + EmojicoinDbEventType::ArenaVaultBalanceUpdate => subscription.arena, + EmojicoinDbEventType::GlobalState => { + subscription.event_types.is_empty() || subscription.event_types.contains(&event_type) + } + _ => { + let markets_is_empty = subscription.markets.is_empty(); + let event_types_is_empty = subscription.event_types.is_empty(); + if !event_types_is_empty && !subscription.event_types.contains(&event_type) { + return false; + } - if !subscription.event_types.is_empty() && !subscription.event_types.contains(&event_type) { - return false; - } - if subscription.markets.is_empty() { - return true; - } - if event_type == EmojicoinDbEventType::GlobalState { - return true; - } + // At this point, event_types is either empty or it contains the event type. + // Now just check if the market matches. + if markets_is_empty { + return true; + } - match get_market_id(event) { - Ok(market_id) => subscription.markets.contains(&market_id), - Err(msg) => { - error!("{msg}"); - false + match get_market_id(event) { + Ok(market_id) => subscription.markets.contains(&market_id), + Err(msg) => { + error!("{msg}"); + false + } + } } } } diff --git a/src/rust/processor b/src/rust/processor index fadf24334..a32ed5ec8 160000 --- a/src/rust/processor +++ b/src/rust/processor @@ -1 +1 @@ -Subproject commit fadf2433427603e76118ae63c93ed0f4a455ca14 +Subproject commit a32ed5ec82a134e9b264315e2c0b9f1fb4a4b912 diff --git a/src/typescript/frontend/next.config.mjs b/src/typescript/frontend/next.config.mjs index 32772c088..90a92ef81 100644 --- a/src/typescript/frontend/next.config.mjs +++ b/src/typescript/frontend/next.config.mjs @@ -6,12 +6,6 @@ const withBundleAnalyzer = analyzer({ }); const DEBUG = process.env.BUILD_DEBUG === "true"; -const styledComponentsConfig = { - displayName: true, - ssr: true, - fileName: true, - minify: false, -}; /** @type {import('next').NextConfig} */ const debugConfigOptions = { productionBrowserSourceMaps: true, @@ -21,24 +15,30 @@ const debugConfigOptions = { experimental: { serverMinification: false, serverSourceMaps: true, - staleTimes: { - dynamic: 0, // Default is normally 30s. - static: 30, // Default is normally 180s. - }, }, }; /** @type {import('next').NextConfig} */ const nextConfig = { + ...(DEBUG ? debugConfigOptions : {}), crossOrigin: "use-credentials", typescript: { tsconfigPath: "tsconfig.json", ignoreBuildErrors: process.env.IGNORE_BUILD_ERRORS === "true", }, compiler: { - styledComponents: DEBUG ? styledComponentsConfig : true, + styledComponents: true, + }, + /** + * Match the new default behavior in next 15, without opinionated caching for dynamic pages. + * @see {@link https://nextjs.org/docs/app/api-reference/config/next-config-js/staleTimes#version-history} + */ + experimental: { + staleTimes: { + dynamic: 0, // Default is normally 30s. + static: 60, // Default is normally 180s. + }, }, - ...(DEBUG ? debugConfigOptions : {}), // Log full fetch URLs if we're in a specific environment. logging: process.env.NODE_ENV === "development" || diff --git a/src/typescript/frontend/package.json b/src/typescript/frontend/package.json index 5a184b3d6..eedc52a1f 100644 --- a/src/typescript/frontend/package.json +++ b/src/typescript/frontend/package.json @@ -29,6 +29,7 @@ "@pontem/wallet-adapter-plugin": "^0.2.1", "@popperjs/core": "^2.11.8", "@radix-ui/react-dropdown-menu": "^2.1.2", + "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-slot": "^1.1.0", @@ -39,6 +40,7 @@ "@types/semver": "^7.5.8", "axios": ">=0.28.0", "big.js": "^6.2.2", + "chart.js": "^4.4.7", "class-variance-authority": "^0.7.1", "clippyts": "^1.0.4", "clsx": "^2.1.1", @@ -51,6 +53,7 @@ "lucide-react": "^0.400.0", "next": "^14.2.15", "react": "^18.3.1", + "react-chartjs-2": "^5.3.0", "react-confetti": "^6.1.0", "react-device-detect": "^2.2.3", "react-dom": "^18.3.1", diff --git a/src/typescript/frontend/src/app/arena/historical-positions/[user]/route.ts b/src/typescript/frontend/src/app/arena/historical-positions/[user]/route.ts new file mode 100644 index 000000000..6a9a41301 --- /dev/null +++ b/src/typescript/frontend/src/app/arena/historical-positions/[user]/route.ts @@ -0,0 +1,26 @@ +import { fetchArenaLeaderboardHistoryWithArenaInfo } from "@/queries/arena"; +import { AccountAddress } from "@aptos-labs/ts-sdk"; +import { safeParsePageWithDefault } from "lib/routes/home-page-params"; +import { type NextRequest } from "next/server"; +import { stringifyJSON } from "utils"; + +const ROWS_RETURNED = 25; + +export const fetchCache = "force-no-store"; + +export async function GET(request: NextRequest, { params }: { params: Promise<{ user: string }> }) { + const user = (await params).user; + const page = safeParsePageWithDefault(request.nextUrl.searchParams.get("page")); + + if (!AccountAddress.isValid({ input: user, strict: true }).valid) { + return new Response("Invalid address.", { status: 400 }); + } + + const position = await fetchArenaLeaderboardHistoryWithArenaInfo({ + user, + page, + pageSize: ROWS_RETURNED, + }); + + return new Response(stringifyJSON(position ?? undefined)); +} diff --git a/src/typescript/frontend/src/app/arena/page.tsx b/src/typescript/frontend/src/app/arena/page.tsx index 2ed45e404..c17dabeff 100644 --- a/src/typescript/frontend/src/app/arena/page.tsx +++ b/src/typescript/frontend/src/app/arena/page.tsx @@ -1,28 +1,62 @@ -import { fetchMarketStateByAddress, fetchMelee } from "@/queries/arena"; +import { fetchArenaInfo, fetchMarketStateByAddress } from "@/queries/arena"; import { ArenaClient } from "components/pages/arena/ArenaClient"; +// import { emoji } from "utils"; +// import type { SymbolEmoji } from "@sdk/emoji_data"; +import type { PeriodicStateEventModel } from "@sdk/indexer-v2/types"; import { redirect } from "next/navigation"; +import { parseJSON } from "utils"; +import { getCandlesticksRoute } from "../candlesticks/utils"; +import { Period } from "@sdk/const"; export const revalidate = 2; export default async function Arena() { - let melee: Awaited> = null; + let arenaInfo: Awaited> = null; + try { - melee = await fetchMelee({}); + arenaInfo = await fetchArenaInfo({}); } catch (e) { console.warn( - "Could not get melee data. This probably means that the backend is running an outdated version of the processor, without the arena processing. Please update." + "Could not get melee data. This probably means that the backend is running an outdated version of the processor" + + " without the arena processing. Please update." ); redirect("/home"); } - if (!melee) { + if (!arenaInfo) { redirect("/home"); } const [market0, market1] = await Promise.all([ - fetchMarketStateByAddress({ address: melee.arenaMelee.emojicoin0MarketAddress }), - fetchMarketStateByAddress({ address: melee.arenaMelee.emojicoin1MarketAddress }), + fetchMarketStateByAddress({ + address: arenaInfo.emojicoin0MarketAddress, + }), + fetchMarketStateByAddress({ + address: arenaInfo.emojicoin1MarketAddress, + }), + ]); + + const to = Math.ceil(new Date().getTime() / 1000); + const countBack = 500; + const period = Period.Period1M; + + const [candlesticksMarket0, candlesticksMarket1] = await Promise.all([ + getCandlesticksRoute(Number(market0!.market.marketID), to, period, countBack).then((res) => + parseJSON(res) + ), + getCandlesticksRoute(Number(market1!.market.marketID), to, period, countBack).then((res) => + parseJSON(res) + ), ]); + /* */ - return ; + return ( + + ); } diff --git a/src/typescript/frontend/src/app/arena/position/[user]/route.ts b/src/typescript/frontend/src/app/arena/position/[user]/route.ts new file mode 100644 index 000000000..313ff0d01 --- /dev/null +++ b/src/typescript/frontend/src/app/arena/position/[user]/route.ts @@ -0,0 +1,20 @@ +// cspell:word timespan + +import { fetchLatestPosition } from "@/queries/arena"; +import { AccountAddress } from "@aptos-labs/ts-sdk"; +import { type NextRequest } from "next/server"; +import { stringifyJSON } from "utils"; + +export const fetchCache = "force-no-store"; + +export async function GET(_: NextRequest, { params }: { params: Promise<{ user: string }> }) { + const user = (await params).user; + + if (!AccountAddress.isValid({ input: user, strict: true }).valid) { + return new Response("Invalid address.", { status: 400 }); + } + + const position = await fetchLatestPosition({ user }); + + return new Response(stringifyJSON(position ?? null)); +} diff --git a/src/typescript/frontend/src/app/candlesticks/route.ts b/src/typescript/frontend/src/app/candlesticks/route.ts index dfcd22274..10b80fdef 100644 --- a/src/typescript/frontend/src/app/candlesticks/route.ts +++ b/src/typescript/frontend/src/app/candlesticks/route.ts @@ -1,108 +1,11 @@ -// cspell:word timespan - -import { type AnyNumberString, getPeriodStartTimeFromTime, toPeriod } from "@sdk/index"; +import { toPeriod } from "@sdk/index"; import { parseInt } from "lodash"; import { type NextRequest } from "next/server"; import { type CandlesticksSearchParams, - type GetCandlesticksParams, - getPeriodDurationSeconds, - HISTORICAL_CACHE_DURATION, - indexToParcelEndDate, - indexToParcelStartDate, + getCandlesticksRoute, isValidCandlesticksSearchParams, - jsonStrAppend, - NORMAL_CACHE_DURATION, - PARCEL_SIZE, - toIndex, } from "./utils"; -import { unstable_cache } from "next/cache"; -import { getLatestProcessedEmojicoinTimestamp } from "@sdk/indexer-v2/queries/utils"; -import { parseJSON, stringifyJSON } from "utils"; -import { fetchMarketRegistration, fetchPeriodicEventsSince } from "@/queries/market"; - -/** - * @property `data` the stringified version of {@link CandlesticksDataType}. - * @property `count` the number of rows returned. - */ -type GetCandlesticksResponse = { - data: string; - count: number; -}; - -type CandlesticksDataType = Awaited>; - -const getCandlesticks = async (params: GetCandlesticksParams) => { - const { marketID, index, period } = params; - - const start = indexToParcelStartDate(index, period); - - const periodDurationMilliseconds = getPeriodDurationSeconds(period) * 1000; - const timespan = periodDurationMilliseconds * PARCEL_SIZE; - const end = new Date(start.getTime() + timespan); - - // PARCEL_SIZE determines the max number of rows, so we don't need to pass a `LIMIT` value. - // `start` and `end` determine the level of pagination, so no need to specify `offset` either. - const data = await fetchPeriodicEventsSince({ - marketID, - period, - start, - end, - }); - - return { - data: stringifyJSON(data), - count: data.length, - }; -}; - -/** - * Returns the market registration event for a market if it exists. - * - * If it doesn't exist, it throws an error so that the value isn't cached in the - * `unstable_cache` call. - * - * @see {@link getCachedMarketRegistrationMs} - */ -const getMarketRegistrationMs = async (marketID: AnyNumberString) => - fetchMarketRegistration({ marketID }).then((res) => { - if (res) { - return Number(res.market.time / 1000n); - } - throw new Error("Market is not yet registered."); - }); - -const getCachedMarketRegistrationMs = unstable_cache( - getMarketRegistrationMs, - ["market-registrations"], - { - revalidate: HISTORICAL_CACHE_DURATION, - } -); - -/** - * Fetch all of the parcels of candlesticks that have completely ended. - * The only difference between this and {@link getNormalCachedCandlesticks} is the cache tag and - * thus how long the data is cached for. - */ -const getHistoricCachedCandlesticks = unstable_cache(getCandlesticks, ["candlesticks-historic"], { - revalidate: HISTORICAL_CACHE_DURATION, -}); - -/** - * Fetch all candlestick parcels that haven't completed yet. - * The only difference between this and {@link getHistoricCachedCandlesticks} is the cache tag and - * thus how long the data is cached for. - */ -const getNormalCachedCandlesticks = unstable_cache(getCandlesticks, ["candlesticks"], { - revalidate: NORMAL_CACHE_DURATION, -}); - -const getCachedLatestProcessedEmojicoinTimestamp = unstable_cache( - getLatestProcessedEmojicoinTimestamp, - ["processor-timestamp"], - { revalidate: 5 } -); /* eslint-disable-next-line import/no-unused-modules */ export async function GET(request: NextRequest) { @@ -122,70 +25,11 @@ export async function GET(request: NextRequest) { const to = parseInt(params.to); const period = toPeriod(params.period); const countBack = parseInt(params.countBack); - const numParcels = parseInt(params.amount); - - const index = toIndex(to, period); - - // Ensure that the last start date as calculated per the search params is valid. - // This is specifically the last parcel's start date- aka the last parcel's first candlestick's - // start time. - const lastParcelStartDate = indexToParcelStartDate(index + numParcels - 1, period); - if (lastParcelStartDate > new Date()) { - return new Response("The last parcel's start date cannot be later than the current time.", { - status: 400, - }); - } - - let data: string = "[]"; - const processorTimestamp = new Date(await getCachedLatestProcessedEmojicoinTimestamp()); - - let totalCount = 0; - let i = 0; - - let registrationPeriodBoundaryStart: Date; try { - registrationPeriodBoundaryStart = await getCachedMarketRegistrationMs(marketID).then( - (time) => new Date(Number(getPeriodStartTimeFromTime(time, period))) - ); - } catch { - return new Response("Market has not been registered yet.", { status: 400 }); - } - - while (totalCount <= countBack) { - const localIndex = index - i; - const endDate = indexToParcelEndDate(localIndex, period); - let res: GetCandlesticksResponse; - if (endDate < processorTimestamp) { - res = await getHistoricCachedCandlesticks({ - marketID, - index: localIndex, - period, - }); - } else { - res = await getNormalCachedCandlesticks({ - marketID, - index: localIndex, - period, - }); - } - - if (i == 0) { - const parsed = parseJSON(res.data); - const filtered = parsed.filter( - (val) => val.periodicMetadata.startTime < BigInt(to) * 1_000_000n - ); - totalCount += filtered.length; - data = jsonStrAppend(data, stringifyJSON(filtered)); - } else { - totalCount += res.count; - data = jsonStrAppend(data, res.data); - } - if (endDate < registrationPeriodBoundaryStart) { - break; - } - i++; + const data = await getCandlesticksRoute(marketID, to, period, countBack); + return new Response(data); + } catch (e) { + return new Response((e as Error).message, { status: 400 }); } - - return new Response(data); } diff --git a/src/typescript/frontend/src/app/candlesticks/utils.ts b/src/typescript/frontend/src/app/candlesticks/utils.ts index e96bd5a07..f129a9e78 100644 --- a/src/typescript/frontend/src/app/candlesticks/utils.ts +++ b/src/typescript/frontend/src/app/candlesticks/utils.ts @@ -1,5 +1,18 @@ -import { isPeriod, type Period, PeriodDuration, periodEnumToRawDuration } from "@sdk/index"; +// cspell:word timespan + +import { + type AnyNumberString, + getPeriodStartTimeFromTime, + isPeriod, + type Period, + PeriodDuration, + periodEnumToRawDuration, +} from "@sdk/index"; import { isNumber } from "utils"; +import { unstable_cache } from "next/cache"; +import { getLatestProcessedEmojicoinTimestamp } from "@sdk/indexer-v2/queries/utils"; +import { parseJSON, stringifyJSON } from "utils"; +import { fetchMarketRegistration, fetchPeriodicEventsSince } from "@/queries/market"; /** * Parcel size is the amount of candlestick periods that will be in a single parcel. @@ -83,3 +96,148 @@ export const isValidCandlesticksSearchParams = ( export const HISTORICAL_CACHE_DURATION = 60 * 60 * 24 * 365; // 1 year. export const NORMAL_CACHE_DURATION = 10; // 10 seconds. + +/** + * @property `data` the stringified version of {@link CandlesticksDataType}. + * @property `count` the number of rows returned. + */ +type GetCandlesticksResponse = { + data: string; + count: number; +}; + +type CandlesticksDataType = Awaited>; + +const getCandlesticks = async (params: GetCandlesticksParams) => { + const { marketID, index, period } = params; + + const start = indexToParcelStartDate(index, period); + + const periodDurationMilliseconds = getPeriodDurationSeconds(period) * 1000; + const timespan = periodDurationMilliseconds * PARCEL_SIZE; + const end = new Date(start.getTime() + timespan); + + // PARCEL_SIZE determines the max number of rows, so we don't need to pass a `LIMIT` value. + // `start` and `end` determine the level of pagination, so no need to specify `offset` either. + const data = await fetchPeriodicEventsSince({ + marketID, + period, + start, + end, + }); + + return { + data: stringifyJSON(data), + count: data.length, + }; +}; + +/** + * Returns the market registration event for a market if it exists. + * + * If it doesn't exist, it throws an error so that the value isn't cached in the + * `unstable_cache` call. + * + * @see {@link getCachedMarketRegistrationMs} + */ +const getMarketRegistrationMs = async (marketID: AnyNumberString) => + fetchMarketRegistration({ marketID }).then((res) => { + if (res) { + return Number(res.market.time / 1000n); + } + throw new Error("Market is not yet registered."); + }); + +const getCachedMarketRegistrationMs = unstable_cache( + getMarketRegistrationMs, + ["market-registrations"], + { + revalidate: HISTORICAL_CACHE_DURATION, + } +); + +/** + * Fetch all of the parcels of candlesticks that have completely ended. + * The only difference between this and {@link getNormalCachedCandlesticks} is the cache tag and + * thus how long the data is cached for. + */ +const getHistoricCachedCandlesticks = unstable_cache(getCandlesticks, ["candlesticks-historic"], { + revalidate: HISTORICAL_CACHE_DURATION, +}); + +/** + * Fetch all candlestick parcels that haven't completed yet. + * The only difference between this and {@link getHistoricCachedCandlesticks} is the cache tag and + * thus how long the data is cached for. + */ +const getNormalCachedCandlesticks = unstable_cache(getCandlesticks, ["candlesticks"], { + revalidate: NORMAL_CACHE_DURATION, +}); + +const getCachedLatestProcessedEmojicoinTimestamp = unstable_cache( + getLatestProcessedEmojicoinTimestamp, + ["processor-timestamp"], + { revalidate: 5 } +); + +export const getCandlesticksRoute = async ( + marketID: number, + to: number, + period: Period, + countBack: number +) => { + const index = toIndex(to, period); + + let data: string = "[]"; + + const processorTimestamp = new Date(await getCachedLatestProcessedEmojicoinTimestamp()); + + let totalCount = 0; + let i = 0; + + let registrationPeriodBoundaryStart: Date; + try { + registrationPeriodBoundaryStart = await getCachedMarketRegistrationMs(marketID).then( + (time) => new Date(Number(getPeriodStartTimeFromTime(time, period))) + ); + } catch { + throw new Error("Market has not been registered yet."); + } + + while (totalCount <= countBack) { + const localIndex = index - i; + const endDate = indexToParcelEndDate(localIndex, period); + let res: GetCandlesticksResponse; + if (endDate < processorTimestamp) { + res = await getHistoricCachedCandlesticks({ + marketID, + index: localIndex, + period, + }); + } else { + res = await getNormalCachedCandlesticks({ + marketID, + index: localIndex, + period, + }); + } + + if (i == 0) { + const parsed = parseJSON(res.data); + const filtered = parsed.filter( + (val) => val.periodicMetadata.startTime < BigInt(to) * 1_000_000n + ); + totalCount += filtered.length; + data = jsonStrAppend(data, stringifyJSON(filtered)); + } else { + totalCount += res.count; + data = jsonStrAppend(data, res.data); + } + if (endDate < registrationPeriodBoundaryStart) { + break; + } + i++; + } + + return data; +}; diff --git a/src/typescript/frontend/src/app/global.css b/src/typescript/frontend/src/app/global.css index 46f360520..c71230957 100644 --- a/src/typescript/frontend/src/app/global.css +++ b/src/typescript/frontend/src/app/global.css @@ -25,6 +25,28 @@ --pink: #cd2f8d; --warning: #ffb119; --error: #f3263e; + + /* shadcn styles- currently only used for testing and prototyping. */ + --background: 224 71% 4%; + --foreground: 213 31% 91%; + --muted: 223 47% 11%; + --muted-foreground: 215.4 16.3% 56.9%; + --accent: 216 34% 17%; + --accent-foreground: 210 40% 98%; + --popover: 224 71% 4%; + --popover-foreground: 215 20.2% 65.1%; + --border: 216 34% 17%; + --input: 216 34% 17%; + --card: 224 71% 4%; + --card-foreground: 213 31% 91%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 1.2%; + --secondary: 222.2 47.4% 11.2%; + --secondary-foreground: 210 40% 98%; + --destructive: 0 63% 31%; + --destructive-foreground: 210 40% 98%; + --ring: 216 34% 17%; + --radius: 0.5rem; } .med-pixel-text { diff --git a/src/typescript/frontend/src/app/home/HomePage.tsx b/src/typescript/frontend/src/app/home/HomePage.tsx index 67411b24f..40593916d 100644 --- a/src/typescript/frontend/src/app/home/HomePage.tsx +++ b/src/typescript/frontend/src/app/home/HomePage.tsx @@ -1,9 +1,16 @@ -import { type DatabaseModels } from "@sdk/indexer-v2/types"; +import { ARENA_MODULE_ADDRESS } from "@sdk/const"; +import { + type ArenaInfoModel, + type MarketStateModel, + type DatabaseModels, +} from "@sdk/indexer-v2/types"; +import { ArenaCard } from "components/pages/home/components/arena-card"; import EmojiTable from "components/pages/home/components/emoji-table"; import MainCard from "components/pages/home/components/main-card/MainCard"; import { PriceFeed } from "components/price-feed"; import TextCarousel from "components/text-carousel/TextCarousel"; import { type MarketDataSortByHomePage } from "lib/queries/sorting/types"; +import { toAptLockedFromProps } from "./utils"; export interface HomePageProps { markets: Array; @@ -13,6 +20,11 @@ export interface HomePageProps { searchBytes?: string; children?: React.ReactNode; priceFeed: DatabaseModels["price_feed"][]; + meleeData: { + melee: ArenaInfoModel; + market0: MarketStateModel; + market1: MarketStateModel; + } | null; } export default async function HomePageComponent({ @@ -23,13 +35,26 @@ export default async function HomePageComponent({ searchBytes, children, priceFeed, + meleeData, }: HomePageProps) { return ( - <> +
{priceFeed.length > 0 ? : }
- + {ARENA_MODULE_ADDRESS && meleeData ? ( + + ) : ( + + )}
{children} @@ -42,6 +67,6 @@ export default async function HomePageComponent({ sortBy={sortBy} searchBytes={searchBytes} /> - +
); } diff --git a/src/typescript/frontend/src/app/home/page.tsx b/src/typescript/frontend/src/app/home/page.tsx index 817263431..546030506 100644 --- a/src/typescript/frontend/src/app/home/page.tsx +++ b/src/typescript/frontend/src/app/home/page.tsx @@ -16,6 +16,8 @@ import { SortMarketsBy } from "@sdk/indexer-v2/types/common"; import { getAptPrice } from "lib/queries/get-apt-price"; import { AptPriceContextProvider } from "context/AptPrice"; import { ORDER_BY } from "@sdk/indexer-v2/const"; +import { ARENA_MODULE_ADDRESS } from "@sdk/const"; +import { fetchArenaInfo, fetchMarketStateByAddress } from "@/queries/arena"; export const revalidate = 2; @@ -79,20 +81,35 @@ export default async function Home({ searchParams }: HomePageParams) { const aptPricePromise = getAptPrice(); - const [priceFeedData, markets, numMarkets, aptPrice] = await Promise.all([ - priceFeedPromise, - marketsPromise, - numMarketsPromise, - aptPricePromise, - ]).catch((e) => { - console.error(e); - return [ - [] as DatabaseModels["price_feed"][], - [] as DatabaseModels["market_state"][], - 0, - undefined, - ] as const; - }); + const meleeDataPromise = (async () => { + if (ARENA_MODULE_ADDRESS) { + const melee = await fetchArenaInfo({}); + if (!melee) { + console.error("Arena is enabled, but arena info couldn't be fetched from the database."); + return null; + } + const [market0, market1] = await Promise.all([ + fetchMarketStateByAddress({ address: melee.emojicoin0MarketAddress }), + fetchMarketStateByAddress({ address: melee.emojicoin1MarketAddress }), + ]); + if (!market0 || !market1) { + console.error( + "Arena info found, but one or both of the arena markets aren't in the market state table." + ); + return null; + } + return { melee, market0, market1 }; + } + return null; + })(); + + const [priceFeedData, markets, numMarkets, aptPrice, meleeData] = await Promise.all([ + priceFeedPromise.catch(() => []), + marketsPromise.catch(() => []), + numMarketsPromise.catch(() => 0), + aptPricePromise.catch(() => undefined), + meleeDataPromise.catch(() => null), + ]); return ( @@ -103,6 +120,7 @@ export default async function Home({ searchParams }: HomePageParams) { sortBy={sortBy} searchBytes={q} priceFeed={priceFeedData} + meleeData={meleeData} /> ); diff --git a/src/typescript/frontend/src/app/home/utils.ts b/src/typescript/frontend/src/app/home/utils.ts new file mode 100644 index 000000000..bc38863c7 --- /dev/null +++ b/src/typescript/frontend/src/app/home/utils.ts @@ -0,0 +1,14 @@ +import { toTotalAptLocked } from "@sdk/indexer-v2/types"; +import { type HomePageProps } from "./HomePage"; + +export const toAptLockedFromProps = (meleeData: Exclude) => + toTotalAptLocked({ + market0: { + state: meleeData.market0.state, + locked: meleeData.melee.emojicoin0Locked, + }, + market1: { + state: meleeData.market1.state, + locked: meleeData.melee.emojicoin1Locked, + }, + }); diff --git a/src/typescript/frontend/src/app/layout.tsx b/src/typescript/frontend/src/app/layout.tsx index 1573a86cb..8e4ca5925 100644 --- a/src/typescript/frontend/src/app/layout.tsx +++ b/src/typescript/frontend/src/app/layout.tsx @@ -8,6 +8,7 @@ import DisplayDebugData from "@/store/server-to-client/FetchFromServer"; import { fontsStyle, notoColorEmoji } from "styles/fonts"; import { headers } from "next/headers"; import "@react95/core/themes/win95.css"; +import { BackgroundEmojis } from "@/components/misc/background-emojis/BackgroundEmojis"; export const metadata: Metadata = getDefaultMetadata(); export const viewport: Viewport = { @@ -23,6 +24,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo