From 7d9f05227361e132e99a23c2b54ae393fd8a629b Mon Sep 17 00:00:00 2001 From: Doug Richar Date: Fri, 26 Apr 2024 04:22:19 -0400 Subject: [PATCH 1/2] fix(ui): recomputation of stakers chart data (#102) When switching between pools in StakingDetails, the aggregation logic for "all pools" that calculates each stakers' total balance was not correctly isolated. If a staker was in multiple pools the total balance was compounding each time and would get bigger and bigger each time the selected pool changed. This fix properly resets the aggregation logic. It also refactors the logic into a custom hook, `useStakersChartData`, which simplifies the component and isolates the logic so it can be tested. --- .../ValidatorDetails/StakingDetails.tsx | 67 +++--------------- ui/src/hooks/useStakersChartData.ts | 69 +++++++++++++++++++ 2 files changed, 79 insertions(+), 57 deletions(-) create mode 100644 ui/src/hooks/useStakersChartData.ts diff --git a/ui/src/components/ValidatorDetails/StakingDetails.tsx b/ui/src/components/ValidatorDetails/StakingDetails.tsx index b0b55f18..454fbb7c 100644 --- a/ui/src/components/ValidatorDetails/StakingDetails.tsx +++ b/ui/src/components/ValidatorDetails/StakingDetails.tsx @@ -1,10 +1,8 @@ import { AlgoAmount } from '@algorandfoundation/algokit-utils/types/amount' -import { useQueries, useQuery } from '@tanstack/react-query' import { BarList, EventProps, ProgressBar } from '@tremor/react' import { useWallet } from '@txnlab/use-wallet-react' import { Copy } from 'lucide-react' import * as React from 'react' -import { stakedInfoQueryOptions, validatorPoolsQueryOptions } from '@/api/queries' import { AddStakeModal } from '@/components/AddStakeModal' import { AlgoDisplayAmount } from '@/components/AlgoDisplayAmount' import { Loading } from '@/components/Loading' @@ -20,7 +18,8 @@ import { } from '@/components/ui/select' import { UnstakeModal } from '@/components/UnstakeModal' import { PoolsChart } from '@/components/ValidatorDetails/PoolsChart' -import { StakedInfo, StakerValidatorData } from '@/interfaces/staking' +import { useStakersChartData } from '@/hooks/useStakersChartData' +import { StakerValidatorData } from '@/interfaces/staking' import { Constraints, Validator } from '@/interfaces/validator' import { isStakingDisabled, isUnstakingDisabled } from '@/utils/contracts' import { copyToClipboard } from '@/utils/copyToClipboard' @@ -52,58 +51,11 @@ export function StakingDetails({ validator, constraints, stakesByValidator }: St value: convertFromBaseUnits(Number(pool.totalAlgoStaked || 1n), 6), })) || [] - const poolsInfoQuery = useQuery(validatorPoolsQueryOptions(validator.id)) - const poolsInfo = poolsInfoQuery.data || [] - - const allStakedInfo = useQueries({ - queries: poolsInfo.map((pool) => stakedInfoQueryOptions(pool.poolAppId)), + const { stakersChartData, poolsInfo, isLoading, isError } = useStakersChartData({ + selectedPool, + validatorId: validator.id, }) - const isLoading = poolsInfoQuery.isLoading || allStakedInfo.some((query) => query.isLoading) - const isError = poolsInfoQuery.isError || allStakedInfo.some((query) => query.isError) - - const chartData = React.useMemo(() => { - if (!allStakedInfo) { - return [] - } - - const stakedInfo = allStakedInfo - .map((query) => query.data || []) - .reduce((acc, stakers, i) => { - if (selectedPool !== 'all' && Number(selectedPool) !== i) { - return acc - } - - // Temporary fix to handle duplicate staker bug - const poolStakers: StakedInfo[] = [] - for (const staker of stakers) { - const stakerIndex = poolStakers.findIndex((s) => s.account === staker.account) - if (stakerIndex > -1) { - staker.account += ' ' // add space to make it unique - } - poolStakers.push(staker) - } - - for (const staker of poolStakers) { - const stakerIndex = acc.findIndex((s) => s.account === staker.account) - if (stakerIndex > -1) { - acc[stakerIndex].balance += staker.balance - acc[stakerIndex].totalRewarded += staker.totalRewarded - acc[stakerIndex].rewardTokenBalance += staker.rewardTokenBalance - } else { - acc.push(staker) - } - } - return acc - }, [] as StakedInfo[]) - - return stakedInfo.map((staker) => ({ - name: staker.account, - value: Number(staker.balance), - href: ExplorerLink.account(staker.account).trim(), // trim to remove trailing whitespace - })) - }, [allStakedInfo, selectedPool]) - const valueFormatter = (v: number) => (
{renderPoolInfo()}
- {chartData.length > 0 && ( + + {stakersChartData.length > 0 && ( 6, - 'sm:h-96': chartData.length > 9, + 'h-64': stakersChartData.length > 6, + 'sm:h-96': stakersChartData.length > 9, })} >
stakedInfoQueryOptions(pool.poolAppId)), + }) + + const isLoading = poolsInfoQuery.isLoading || allStakedInfo.some((query) => query.isLoading) + const isError = poolsInfoQuery.isError || allStakedInfo.some((query) => query.isError) + const isSuccess = poolsInfoQuery.isSuccess && allStakedInfo.every((query) => query.isSuccess) + + const stakersChartData = React.useMemo(() => { + if (!allStakedInfo) { + return [] + } + + const stakerTotals: Record = {} + + allStakedInfo.forEach((query, i) => { + if (selectedPool !== 'all' && Number(selectedPool) !== i) { + return + } + + const stakers = query.data || [] + + stakers.forEach((staker) => { + const id = staker.account + + if (!stakerTotals[id]) { + stakerTotals[id] = { + ...staker, + balance: BigInt(0), + totalRewarded: BigInt(0), + rewardTokenBalance: BigInt(0), + } + } + stakerTotals[id].balance += staker.balance + stakerTotals[id].totalRewarded += staker.totalRewarded + stakerTotals[id].rewardTokenBalance += staker.rewardTokenBalance + }) + }) + + return Object.values(stakerTotals).map((staker) => ({ + name: staker.account, + value: Number(staker.balance), + href: ExplorerLink.account(staker.account), + })) + }, [allStakedInfo, selectedPool]) + + return { + stakersChartData, + poolsInfo, + isLoading, + isError, + isSuccess, + } +} From 0f47bb8a8b3cea753b0e85b77e88cbb069729372 Mon Sep 17 00:00:00 2001 From: Doug Richar Date: Fri, 26 Apr 2024 11:21:00 -0400 Subject: [PATCH 2/2] test(ui): intercept algod requests with Mock Service Worker (#103) * test(ui): intercept algod requests with Mock Service Worker Since the app clients making simulate calls expect a functioning algod client as an argument, mocking algosdk.Algodv2 isn't an option. MSW will let us intercept the actual http simulate requests to fetch data and mock their responses. Now we can properly isolate components for testing and provide fixture data for predictable, repeatable outcomes. * chore(ui): fix Prettier issue in tsconfig * ci(ui): fix explorer account env variable --- .github/workflows/ci-ui.yaml | 1 + pnpm-lock.yaml | 594 +++++++++++++++++++++- ui/.env.test | 9 + ui/package.json | 5 + ui/src/api/contracts.ts | 3 +- ui/src/hooks/useStakersChartData.spec.tsx | 41 ++ ui/src/interfaces/simulate.ts | 594 ++++++++++++++++++++++ ui/src/utils/tests/abi.ts | 59 +++ ui/src/utils/tests/constants.ts | 2 + ui/src/utils/tests/fixtures/accounts.ts | 8 + ui/src/utils/tests/fixtures/boxes.ts | 80 +++ ui/src/utils/tests/fixtures/methods.ts | 27 + ui/src/utils/tests/utils.ts | 39 ++ ui/tsconfig.json | 9 +- ui/vite.config.ts | 2 + ui/vitest.setup.ts | 176 +++++++ 16 files changed, 1623 insertions(+), 26 deletions(-) create mode 100644 ui/src/hooks/useStakersChartData.spec.tsx create mode 100644 ui/src/interfaces/simulate.ts create mode 100644 ui/src/utils/tests/abi.ts create mode 100644 ui/src/utils/tests/constants.ts create mode 100644 ui/src/utils/tests/fixtures/accounts.ts create mode 100644 ui/src/utils/tests/fixtures/boxes.ts create mode 100644 ui/src/utils/tests/fixtures/methods.ts create mode 100644 ui/src/utils/tests/utils.ts create mode 100644 ui/vitest.setup.ts diff --git a/.github/workflows/ci-ui.yaml b/.github/workflows/ci-ui.yaml index c7b3f1e0..9e73258a 100644 --- a/.github/workflows/ci-ui.yaml +++ b/.github/workflows/ci-ui.yaml @@ -25,6 +25,7 @@ jobs: VITE_ALGOD_PORT: 4001 VITE_NFD_API_URL: http://localhost:80 VITE_NFD_APP_URL: ws://localhost:3000 + VITE_EXPLORER_ACCOUNT_URL: https://app.dappflow.org/setnetwork?name=sandbox&redirect=explorer/account steps: - name: Checkout repository diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f95a7d4..26e26098 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -259,6 +259,12 @@ importers: '@tanstack/router-vite-plugin': specifier: 1.30.0 version: 1.30.0(vite@5.2.10) + '@testing-library/jest-dom': + specifier: ^6.4.2 + version: 6.4.2(vitest@1.5.1) + '@testing-library/react': + specifier: ^15.0.4 + version: 15.0.4(react-dom@18.2.0)(react@18.2.0) '@types/big.js': specifier: 6.2.2 version: 6.2.2 @@ -283,6 +289,9 @@ importers: '@vitest/coverage-v8': specifier: 1.5.1 version: 1.5.1(vitest@1.5.1) + algo-msgpack-with-bigint: + specifier: ^2.1.1 + version: 2.1.1 autoprefixer: specifier: 10.4.19 version: 10.4.19(postcss@8.4.38) @@ -295,6 +304,12 @@ importers: eslint-plugin-prettier: specifier: 5.1.3 version: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5) + jsdom: + specifier: ^24.0.0 + version: 24.0.0 + msw: + specifier: ^2.2.14 + version: 2.2.14(typescript@5.4.5) playwright: specifier: 1.43.1 version: 1.43.1 @@ -315,7 +330,7 @@ importers: version: 5.2.10(@types/node@20.12.7) vitest: specifier: 1.5.1 - version: 1.5.1(@types/node@20.12.7) + version: 1.5.1(@types/node@20.12.7)(jsdom@24.0.0) packages: @@ -324,6 +339,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /@adobe/css-tools@4.3.3: + resolution: {integrity: sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==} + dev: true + /@algorandfoundation/algokit-client-generator@3.0.2: resolution: {integrity: sha512-0/x2/szFPTXfJ21m3OlohfnMUM/qpz5vkPsFz06U2BQ8iHmtwFoIz94JLj9Z9mzYeTZBnVKvDgSqMNMkJRzldg==} engines: {node: '>=18.0'} @@ -778,7 +797,6 @@ packages: engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.14.1 - dev: false /@babel/template@7.24.0: resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} @@ -837,6 +855,18 @@ packages: - utf-8-validate dev: false + /@bundled-es-modules/cookie@2.0.0: + resolution: {integrity: sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==} + dependencies: + cookie: 0.5.0 + dev: true + + /@bundled-es-modules/statuses@1.0.1: + resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} + dependencies: + statuses: 2.0.1 + dev: true + /@cspotcode/source-map-support@0.8.1: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -1193,6 +1223,43 @@ packages: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} dev: true + /@inquirer/confirm@3.1.5: + resolution: {integrity: sha512-6+dwZrpko5vr5EFEQmUbfBVhtu6IsnB8lQNsLHgO9S9fbfS5J6MuUj+NY0h98pPpYZXEazLR7qzypEDqVzf6aQ==} + engines: {node: '>=18'} + dependencies: + '@inquirer/core': 8.0.1 + '@inquirer/type': 1.3.0 + dev: true + + /@inquirer/core@8.0.1: + resolution: {integrity: sha512-qJRk1y51Os2ARc11Bg2N6uIwiQ9qBSrmZeuMonaQ/ntFpb4+VlcQ8Gl1TFH67mJLz3HA2nvuave0nbv6Lu8pbg==} + engines: {node: '>=18'} + dependencies: + '@inquirer/figures': 1.0.1 + '@inquirer/type': 1.3.0 + '@types/mute-stream': 0.0.4 + '@types/node': 20.12.7 + '@types/wrap-ansi': 3.0.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-spinners: 2.9.2 + cli-width: 4.1.0 + mute-stream: 1.0.0 + signal-exit: 4.1.0 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + dev: true + + /@inquirer/figures@1.0.1: + resolution: {integrity: sha512-mtup3wVKia3ZwULPHcbs4Mor8Voi+iIXEWD7wCNbIO6lYR62oPCTQyrddi5OMYVXHzeCSoneZwJuS8sBvlEwDw==} + engines: {node: '>=18'} + dev: true + + /@inquirer/type@1.3.0: + resolution: {integrity: sha512-RW4Zf6RCTnInRaOZuRHTqAUl+v6VJuQGglir7nW2BkT3OXOphMhkIFhvFRjorBx2l0VwtC/M4No8vYR65TdN9Q==} + engines: {node: '>=18'} + dev: true + /@isaacs/cliui@8.0.2: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1550,6 +1617,23 @@ packages: tslib: 2.6.2 dev: false + /@mswjs/cookies@1.1.0: + resolution: {integrity: sha512-0ZcCVQxifZmhwNBoQIrystCb+2sWBY2Zw8lpfJBPCHGCA/HWqehITeCRVIv4VMy8MPlaHo2w2pTHFV2pFfqKPw==} + engines: {node: '>=18'} + dev: true + + /@mswjs/interceptors@0.26.15: + resolution: {integrity: sha512-HM47Lu1YFmnYHKMBynFfjCp0U/yRskHj/8QEJW0CBEPOlw8Gkmjfll+S9b8M7V5CNDw2/ciRxjjnWeaCiblSIQ==} + engines: {node: '>=18'} + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.2 + strict-event-emitter: 0.5.1 + dev: true + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1568,6 +1652,21 @@ packages: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + /@open-draft/deferred-promise@2.2.0: + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + dev: true + + /@open-draft/logger@0.3.0: + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.2 + dev: true + + /@open-draft/until@2.1.0: + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + dev: true + /@parcel/watcher-android-arm64@2.4.1: resolution: {integrity: sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==} engines: {node: '>= 10.0.0'} @@ -3030,6 +3129,66 @@ packages: resolution: {integrity: sha512-A0004OAa1FcUkPHeeGoKgBrAgjH+uHdDPrw1L7RpkwnODYqRvoilqsHPs8cyTjMg1byZBbiNpQAq2TlFLIaQag==} dev: false + /@testing-library/dom@10.0.0: + resolution: {integrity: sha512-PmJPnogldqoVFf+EwbHvbBJ98MmqASV8kLrBYgsDNxQcFMeIS7JFL48sfyXvuMtgmWO/wMhh25odr+8VhDmn4g==} + engines: {node: '>=18'} + dependencies: + '@babel/code-frame': 7.24.2 + '@babel/runtime': 7.24.1 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + dev: true + + /@testing-library/jest-dom@6.4.2(vitest@1.5.1): + resolution: {integrity: sha512-CzqH0AFymEMG48CpzXFriYYkOjk6ZGPCLMhW9e9jg3KMCn5OfJecF8GtGW7yGfR/IgCe3SX8BSwjdzI6BBbZLw==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + peerDependencies: + '@jest/globals': '>= 28' + '@types/bun': latest + '@types/jest': '>= 28' + jest: '>= 28' + vitest: '>= 0.32' + peerDependenciesMeta: + '@jest/globals': + optional: true + '@types/bun': + optional: true + '@types/jest': + optional: true + jest: + optional: true + vitest: + optional: true + dependencies: + '@adobe/css-tools': 4.3.3 + '@babel/runtime': 7.24.1 + aria-query: 5.3.0 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + lodash: 4.17.21 + redent: 3.0.0 + vitest: 1.5.1(@types/node@20.12.7)(jsdom@24.0.0) + dev: true + + /@testing-library/react@15.0.4(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Fw/LM1emOHKfCxv5R0tz+25TOtiMt0o5Np1zJmb4LbSacOagXQX4ooAaHiJfGUMe+OjUk504BX11W+9Z8CvyZA==} + engines: {node: '>=18'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@babel/runtime': 7.24.1 + '@testing-library/dom': 10.0.0 + '@types/react-dom': 18.2.25 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: true + /@tremor/react@3.16.1(react-dom@18.2.0)(react@18.2.0)(tailwindcss@3.4.3): resolution: {integrity: sha512-ablAsFL7twiUYf57gMAS2ar6YTajsQ7fs63vqlC2i9DpLuhFSrwEp69y1a6bxXP+KH0etYRMj6xxCvdCdiy8mQ==} peerDependencies: @@ -3135,6 +3294,10 @@ packages: lute-connect: 1.2.0 dev: false + /@types/aria-query@5.0.4: + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + dev: true + /@types/babel__core@7.20.5: resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} dependencies: @@ -3168,6 +3331,10 @@ packages: resolution: {integrity: sha512-e2cOW9YlVzFY2iScnGBBkplKsrn2CsObHQ2Hiw4V1sSyiGbgWL8IyqE3zFi1Pt5o1pdAtYkDAIsF3KKUPjdzaA==} dev: true + /@types/cookie@0.6.0: + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + dev: true + /@types/d3-array@3.2.1: resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} dev: false @@ -3244,6 +3411,12 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true + /@types/mute-stream@0.0.4: + resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} + dependencies: + '@types/node': 20.12.7 + dev: true + /@types/node@20.12.7: resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==} dependencies: @@ -3278,10 +3451,18 @@ packages: resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} dev: true + /@types/statuses@2.0.5: + resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} + dev: true + /@types/trusted-types@2.0.7: resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} dev: false + /@types/wrap-ansi@3.0.0: + resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} + dev: true + /@types/yargs-parser@21.0.3: resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} dev: true @@ -3462,7 +3643,7 @@ packages: std-env: 3.7.0 strip-literal: 2.1.0 test-exclude: 6.0.0 - vitest: 1.5.1(@types/node@20.12.7) + vitest: 1.5.1(@types/node@20.12.7)(jsdom@24.0.0) transitivePeerDependencies: - supports-color dev: true @@ -3963,6 +4144,15 @@ packages: resolution: {integrity: sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==} dev: false + /agent-base@7.1.1: + resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} + engines: {node: '>= 14'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + /ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} dependencies: @@ -4060,6 +4250,12 @@ packages: tslib: 2.6.2 dev: false + /aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + dependencies: + dequal: 2.0.3 + dev: true + /array-buffer-byte-length@1.0.1: resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} engines: {node: '>= 0.4'} @@ -4141,7 +4337,6 @@ packages: /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - dev: false /atomic-sleep@1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} @@ -4425,6 +4620,14 @@ packages: supports-color: 5.5.0 dev: true + /chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + /chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -4496,6 +4699,16 @@ packages: clsx: 2.0.0 dev: false + /cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + dev: true + + /cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + dev: true + /client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} dev: false @@ -4577,7 +4790,6 @@ packages: engines: {node: '>= 0.8'} dependencies: delayed-stream: 1.0.0 - dev: false /commander@11.1.0: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} @@ -4622,6 +4834,11 @@ packages: resolution: {integrity: sha512-L2rLOcK0wzWSfSDA33YR+PUHDG10a8px7rUHKWbGLP4YfbsMed2KFUw5fczvDPbT98DDe3LEzviswl810apTEw==} dev: false + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: true + /copy-to-clipboard@3.3.3: resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} dependencies: @@ -4667,11 +4884,22 @@ packages: optional: true dev: false + /css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + dev: true + /cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true + /cssstyle@4.0.1: + resolution: {integrity: sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==} + engines: {node: '>=18'} + dependencies: + rrweb-cssom: 0.6.0 + dev: true + /csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -4746,6 +4974,14 @@ packages: engines: {node: '>=12'} dev: false + /data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 + dev: true + /data-view-buffer@1.0.1: resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} engines: {node: '>= 0.4'} @@ -4820,6 +5056,10 @@ packages: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} dev: false + /decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + dev: true + /decode-uri-component@0.2.2: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} @@ -4875,7 +5115,11 @@ packages: /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - dev: false + + /dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + dev: true /destr@2.0.3: resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} @@ -4944,6 +5188,14 @@ packages: esutils: 2.0.3 dev: true + /dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dev: true + + /dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dev: true + /dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} dependencies: @@ -5000,6 +5252,11 @@ packages: once: 1.4.0 dev: false + /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: @@ -5571,7 +5828,6 @@ packages: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 - dev: false /fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -5756,6 +6012,11 @@ packages: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true + /graphql@16.8.1: + resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + dev: true + /h3@1.11.1: resolution: {integrity: sha512-AbaH6IDnZN6nmbnJOH72y3c5Wwh9P97soSVdGSBbcDACRdkC0FEWf25pzx4f/NuOCK6quHmW18yF2Wx+G4Zi1A==} dependencies: @@ -5830,6 +6091,10 @@ packages: tslib: 2.6.2 dev: true + /headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + dev: true + /hey-listen@1.0.8: resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==} dev: false @@ -5837,15 +6102,42 @@ packages: /hi-base32@0.5.1: resolution: {integrity: sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA==} + /html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + dependencies: + whatwg-encoding: 3.1.1 + dev: true + /html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} dev: true + /http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.1 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + /http-shutdown@1.2.2: resolution: {integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} dev: false + /https-proxy-agent@7.0.4: + resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.1 + 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'} @@ -5855,6 +6147,13 @@ packages: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: true + /idb-keyval@6.2.1: resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==} dev: false @@ -5889,6 +6188,11 @@ packages: engines: {node: '>=0.8.19'} 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==} dependencies: @@ -6017,6 +6321,10 @@ packages: engines: {node: '>= 0.4'} dev: true + /is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + dev: true + /is-number-object@1.0.7: resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} engines: {node: '>= 0.4'} @@ -6033,6 +6341,10 @@ packages: engines: {node: '>=8'} dev: true + /is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + dev: true + /is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -6632,6 +6944,42 @@ packages: argparse: 2.0.1 dev: true + /jsdom@24.0.0: + resolution: {integrity: sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + cssstyle: 4.0.1 + data-urls: 5.0.0 + decimal.js: 10.4.3 + form-data: 4.0.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.4 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.9 + parse5: 7.1.2 + rrweb-cssom: 0.6.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.3 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 + ws: 8.16.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + /jsesc@2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} @@ -6800,14 +7148,12 @@ packages: /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - dev: false /loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true dependencies: js-tokens: 4.0.0 - dev: false /lottie-web@5.12.2: resolution: {integrity: sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==} @@ -6854,6 +7200,11 @@ packages: resolution: {integrity: sha512-irbecoBxkNmSVe12p+leZkksS/7qbHI2etvfxw60t4Lcjmn0XX80GMi+iInhFFRhwg7rxU378P2h6GO8AU+9AQ==} dev: false + /lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + dev: true + /magic-string@0.30.8: resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} engines: {node: '>=12'} @@ -6902,14 +7253,12 @@ packages: /mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} - dev: false /mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} dependencies: mime-db: 1.52.0 - dev: false /mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} @@ -6926,6 +7275,11 @@ packages: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} + /min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + dev: true + /minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} dev: false @@ -6995,10 +7349,46 @@ packages: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} dev: true + /msw@2.2.14(typescript@5.4.5): + resolution: {integrity: sha512-64i8rNCa1xzDK8ZYsTrVMli05D687jty8+Th+PU5VTbJ2/4P7fkQFVyDQ6ZFT5FrNR8z2BHhbY47fKNvfHrumA==} + engines: {node: '>=18'} + hasBin: true + requiresBuild: true + peerDependencies: + typescript: '>= 4.7.x' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@bundled-es-modules/cookie': 2.0.0 + '@bundled-es-modules/statuses': 1.0.1 + '@inquirer/confirm': 3.1.5 + '@mswjs/cookies': 1.1.0 + '@mswjs/interceptors': 0.26.15 + '@open-draft/until': 2.1.0 + '@types/cookie': 0.6.0 + '@types/statuses': 2.0.5 + chalk: 4.1.2 + graphql: 16.8.1 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.2 + path-to-regexp: 6.2.2 + strict-event-emitter: 0.5.1 + type-fest: 4.17.0 + typescript: 5.4.5 + yargs: 17.7.2 + dev: true + /multiformats@9.9.0: resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} dev: false + /mute-stream@1.0.0: + resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + /mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} dependencies: @@ -7118,6 +7508,10 @@ packages: commander: 5.1.0 dev: true + /nwsapi@2.2.9: + resolution: {integrity: sha512-2f3F0SEEer8bBu0dsNCFF50N0cTThV1nWFYcEYFZttdW0lDAoybv9cQoK7X7/68Z89S7FoRrVjP1LPX4XRf9vg==} + dev: true + /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -7232,6 +7626,10 @@ packages: type-check: 0.4.0 dev: true + /outvariant@1.4.2: + resolution: {integrity: sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==} + dev: true + /p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -7293,6 +7691,12 @@ packages: lines-and-columns: 1.2.4 dev: true + /parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + dependencies: + entities: 4.5.0 + dev: true + /pascal-case@3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} dependencies: @@ -7338,6 +7742,10 @@ packages: lru-cache: 10.2.0 minipass: 7.0.4 + /path-to-regexp@6.2.2: + resolution: {integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==} + dev: true + /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -7520,6 +7928,15 @@ packages: hasBin: true dev: true + /pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + dev: true + /pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -7556,6 +7973,10 @@ packages: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} dev: false + /psl@1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + dev: true + /punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -7605,6 +8026,10 @@ packages: strict-uri-encode: 2.0.0 dev: false + /querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + dev: true + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -7650,7 +8075,6 @@ packages: loose-envify: 1.4.0 react: 18.2.0 scheduler: 0.23.0 - dev: false /react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} @@ -7682,6 +8106,10 @@ packages: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: false + /react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + dev: true + /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: true @@ -7785,7 +8213,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: loose-envify: 1.4.0 - dev: false /read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} @@ -7837,9 +8264,16 @@ packages: victory-vendor: 36.9.2 dev: false + /redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + dev: true + /regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - dev: false /regexp.prototype.flags@1.5.2: resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} @@ -7859,6 +8293,10 @@ packages: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} dev: false + /requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: true + /resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -7925,6 +8363,10 @@ packages: fsevents: 2.3.3 dev: true + /rrweb-cssom@0.6.0: + resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} + dev: true + /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: @@ -7958,11 +8400,21 @@ packages: engines: {node: '>=10'} dev: false + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: true + + /saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: true + /scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: loose-envify: 1.4.0 - dev: false /semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} @@ -8124,6 +8576,11 @@ packages: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: true + /std-env@3.7.0: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} @@ -8131,6 +8588,10 @@ packages: resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} dev: false + /strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + dev: true + /strict-uri-encode@2.0.0: resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} engines: {node: '>=4'} @@ -8224,6 +8685,13 @@ packages: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} + /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-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -8273,6 +8741,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + /symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: true + /synckit@0.8.8: resolution: {integrity: sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==} engines: {node: ^14.18.0 || >=16.0.0} @@ -8409,10 +8881,27 @@ packages: resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} dev: false + /tough-cookie@4.1.3: + resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} + engines: {node: '>=6'} + dependencies: + psl: 1.9.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + dev: true + /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} dev: true + /tr46@5.0.0: + resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + engines: {node: '>=18'} + dependencies: + punycode: 2.3.1 + dev: true + /ts-api-utils@1.3.0(typescript@5.4.5): resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} @@ -8537,6 +9026,11 @@ packages: engines: {node: '>=10'} dev: true + /type-fest@4.17.0: + resolution: {integrity: sha512-9flrz1zkfLRH3jO3bLflmTxryzKMxVa7841VeMgBaNQGY6vH4RCcpN/sQLB7mQQYh1GZ5utT2deypMuCy4yicw==} + engines: {node: '>=16'} + dev: true + /typed-array-buffer@1.0.2: resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} engines: {node: '>= 0.4'} @@ -8633,6 +9127,11 @@ packages: pathe: 1.1.2 dev: false + /universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + dev: true + /unstorage@1.10.2(idb-keyval@6.2.1): resolution: {integrity: sha512-cULBcwDqrS8UhlIysUJs2Dk0Mmt8h7B0E6mtR+relW9nZvsf/u4SkAYyNliPiPW7XtFNb5u3IUMkxGxFTTRTgQ==} peerDependencies: @@ -8734,6 +9233,13 @@ packages: punycode: 2.3.1 dev: true + /url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + dev: true + /use-callback-ref@1.3.2(@types/react@18.2.79)(react@18.2.0): resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==} engines: {node: '>=10'} @@ -8891,7 +9397,7 @@ packages: fsevents: 2.3.3 dev: true - /vitest@1.5.1(@types/node@20.12.7): + /vitest@1.5.1(@types/node@20.12.7)(jsdom@24.0.0): resolution: {integrity: sha512-3GvBMpoRnUNbZRX1L3mJCv3Ou3NAobb4dM48y8k9ZGwDofePpclTOyO+lqJFKSQpubH1V8tEcAEw/Y3mJKGJQQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -8926,6 +9432,7 @@ packages: chai: 4.4.1 debug: 4.3.4 execa: 8.0.1 + jsdom: 24.0.0 local-pkg: 0.5.0 magic-string: 0.30.8 pathe: 1.1.2 @@ -8950,6 +9457,13 @@ packages: /vlq@2.0.4: resolution: {integrity: sha512-aodjPa2wPQFkra1G8CzJBTHXhgk3EVSwxSWXNPr1fgdFLUb8kvLV1iEb6rFgasIsjP82HWI6dsb5Io26DDnasA==} + /w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + dependencies: + xml-name-validator: 5.0.0 + dev: true + /walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} dependencies: @@ -8960,6 +9474,31 @@ packages: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: true + /webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + dev: true + + /whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + dependencies: + iconv-lite: 0.6.3 + dev: true + + /whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + dev: true + + /whatwg-url@14.0.0: + resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==} + engines: {node: '>=18'} + dependencies: + tr46: 5.0.0 + webidl-conversions: 7.0.0 + dev: true + /whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} dependencies: @@ -9015,7 +9554,6 @@ packages: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: false /wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} @@ -9070,6 +9608,28 @@ packages: optional: true dev: false + /ws@8.16.0: + resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} + 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 + + /xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + dev: true + + /xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: true + /y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} dev: false diff --git a/ui/.env.test b/ui/.env.test index 23a0cbe6..5090aa8e 100644 --- a/ui/.env.test +++ b/ui/.env.test @@ -7,3 +7,12 @@ VITE_ALGOD_PORT=4001 VITE_NFD_API_URL=http://localhost:80 VITE_NFD_APP_URL=http://localhost:3000 +# Block Explorer +VITE_EXPLORER_ACCOUNT_URL=https://app.dappflow.org/setnetwork?name=sandbox&redirect=explorer/account +VITE_EXPLORER_TRANSACTION_URL=https://app.dappflow.org/setnetwork?name=sandbox&redirect=explorer/transaction +VITE_EXPLORER_ASSET_URL=https://app.dappflow.org/setnetwork?name=sandbox&redirect=explorer/asset +VITE_EXPLORER_APPLICATION_URL=https://app.dappflow.org/setnetwork?name=sandbox&redirect=explorer/application + +# Reti +VITE_RETI_APP_ID=1002 + diff --git a/ui/package.json b/ui/package.json index 3524eae0..323c7930 100644 --- a/ui/package.json +++ b/ui/package.json @@ -13,6 +13,8 @@ "devDependencies": { "@playwright/test": "1.43.1", "@tanstack/router-vite-plugin": "1.30.0", + "@testing-library/jest-dom": "^6.4.2", + "@testing-library/react": "^15.0.4", "@types/big.js": "6.2.2", "@types/node": "20.12.7", "@types/react": "18.2.79", @@ -21,10 +23,13 @@ "@typescript-eslint/parser": "7.7.1", "@vitejs/plugin-react": "4.2.1", "@vitest/coverage-v8": "1.5.1", + "algo-msgpack-with-bigint": "^2.1.1", "autoprefixer": "10.4.19", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-prettier": "5.1.3", + "jsdom": "^24.0.0", + "msw": "^2.2.14", "playwright": "1.43.1", "postcss": "8.4.38", "tailwindcss": "3.4.3", diff --git a/ui/src/api/contracts.ts b/ui/src/api/contracts.ts index bc5c1b1a..82f34055 100644 --- a/ui/src/api/contracts.ts +++ b/ui/src/api/contracts.ts @@ -35,6 +35,7 @@ import { import { dayjs } from '@/utils/dayjs' import { getRetiAppIdFromViteEnvironment } from '@/utils/env' import { getAlgodConfigFromViteEnvironment } from '@/utils/network/getAlgoClientConfigs' +import { encodeCallParams } from '@/utils/tests/abi' const algodConfig = getAlgodConfigFromViteEnvironment() const algodClient = algokit.getAlgoClient({ @@ -1012,7 +1013,7 @@ export async function callGetPools( ) { return validatorClient .compose() - .getPools({ validatorId }) + .getPools({ validatorId }, { note: encodeCallParams('getPools', { validatorId }) }) .simulate({ allowEmptySignatures: true, allowUnnamedResources: true }) } diff --git a/ui/src/hooks/useStakersChartData.spec.tsx b/ui/src/hooks/useStakersChartData.spec.tsx new file mode 100644 index 00000000..2ad192a2 --- /dev/null +++ b/ui/src/hooks/useStakersChartData.spec.tsx @@ -0,0 +1,41 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { renderHook, waitFor } from '@testing-library/react' +import { useStakersChartData } from '@/hooks/useStakersChartData' +import { MOCK_STAKED_INFO_1, MOCK_STAKED_INFO_2 } from '@/utils/tests/fixtures/boxes' + +const createWrapper = () => { + const queryClient = new QueryClient() + return ({ children }: { children: React.ReactNode }) => ( + {children} + ) +} + +describe('useStakersChartData', () => { + it('returns correct data', async () => { + const { result } = renderHook( + () => + useStakersChartData({ + selectedPool: 'all', + validatorId: 1, + }), + { + wrapper: createWrapper(), + }, + ) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + expect(result.current.stakersChartData).toEqual([ + { + name: MOCK_STAKED_INFO_1.account, + value: Number(MOCK_STAKED_INFO_1.balance), + href: `https://app.dappflow.org/setnetwork?name=sandbox&redirect=explorer/account/${MOCK_STAKED_INFO_1.account}`, + }, + { + name: MOCK_STAKED_INFO_2.account, + value: Number(MOCK_STAKED_INFO_2.balance), + href: `https://app.dappflow.org/setnetwork?name=sandbox&redirect=explorer/account/${MOCK_STAKED_INFO_2.account}`, + }, + ]) + }) +}) diff --git a/ui/src/interfaces/simulate.ts b/ui/src/interfaces/simulate.ts new file mode 100644 index 00000000..1a29f7d8 --- /dev/null +++ b/ui/src/interfaces/simulate.ts @@ -0,0 +1,594 @@ +import { EncodedSignedTransaction } from 'algosdk' + +/** + * Request type for simulation endpoint. + */ +export interface SimulateRequest { + /** + * The transaction groups to simulate. + */ + 'txn-groups': SimulateRequestTransactionGroup[] + /** + * Allows transactions without signatures to be simulated as if they had correct + * signatures. + */ + 'allow-empty-signatures'?: boolean + /** + * Lifts limits on log opcode usage during simulation. + */ + 'allow-more-logging'?: boolean + /** + * Allows access to unnamed resources during simulation. + */ + 'allow-unnamed-resources'?: boolean + /** + * An object that configures simulation execution trace. + */ + 'exec-trace-config'?: SimulateTraceConfig + /** + * Applies extra opcode budget during simulation for each transaction group. + */ + 'extra-opcode-budget'?: number | bigint + /** + * If provided, specifies the round preceding the simulation. State changes through + * this round will be used to run this simulation. Usually only the 4 most recent + * rounds will be available (controlled by the node config value MaxAcctLookback). + * If not specified, defaults to the latest available round. + */ + round?: number | bigint +} + +/** + * A transaction group to simulate. + */ +export interface SimulateRequestTransactionGroup { + /** + * An atomic transaction group. + */ + txns: EncodedSignedTransaction[] +} + +/** + * Result of a transaction group simulation. + */ +export interface SimulateResponse { + /** + * The round immediately preceding this simulation. State changes through this + * round were used to run this simulation. + */ + 'last-round': number | bigint + /** + * A result object for each transaction group that was simulated. + */ + 'txn-groups': SimulateTransactionGroupResult[] + /** + * The version of this response object. + */ + version: number | bigint + /** + * The set of parameters and limits override during simulation. If this set of + * parameters is present, then evaluation parameters may differ from standard + * evaluation in certain ways. + */ + 'eval-overrides'?: SimulationEvalOverrides + /** + * An object that configures simulation execution trace. + */ + 'exec-trace-config'?: SimulateTraceConfig + /** + * Initial states of resources that were accessed during simulation. + */ + 'initial-states'?: SimulateInitialStates +} + +/** + * Simulation result for an atomic transaction group + */ +export interface SimulateTransactionGroupResult { + /** + * Simulation result for individual transactions + */ + 'txn-results': SimulateTransactionResult[] + /** + * Total budget added during execution of app calls in the transaction group. + */ + 'app-budget-added'?: number | bigint + /** + * Total budget consumed during execution of app calls in the transaction group. + */ + 'app-budget-consumed'?: number | bigint + /** + * If present, indicates which transaction in this group caused the failure. This + * array represents the path to the failing transaction. Indexes are zero based, + * the first element indicates the top-level transaction, and successive elements + * indicate deeper inner transactions. + */ + 'failed-at'?: (number | bigint)[] + /** + * If present, indicates that the transaction group failed and specifies why that + * happened + */ + 'failure-message'?: string + /** + * These are resources that were accessed by this group that would normally have + * caused failure, but were allowed in simulation. Depending on where this object + * is in the response, the unnamed resources it contains may or may not qualify for + * group resource sharing. If this is a field in SimulateTransactionGroupResult, + * the resources do qualify, but if this is a field in SimulateTransactionResult, + * they do not qualify. In order to make this group valid for actual submission, + * resources that qualify for group sharing can be made available by any + * transaction of the group; otherwise, resources must be placed in the same + * transaction which accessed them. + */ + 'unnamed-resources-accessed'?: SimulateUnnamedResourcesAccessed +} + +/** + * Simulation result for an atomic transaction group + */ +export interface SimulateTransactionResult { + /** + * Details about a pending transaction. If the transaction was recently confirmed, + * includes confirmation details like the round and reward details. + */ + 'txn-result': PendingTransactionResponse + /** + * Budget used during execution of an app call transaction. This value includes + * budged used by inner app calls spawned by this transaction. + */ + 'app-budget-consumed'?: number | bigint + /** + * The execution trace of calling an app or a logic sig, containing the inner app + * call trace in a recursive way. + */ + 'exec-trace'?: SimulationTransactionExecTrace + /** + * Budget used during execution of a logic sig transaction. + */ + 'logic-sig-budget-consumed'?: number | bigint + /** + * These are resources that were accessed by this group that would normally have + * caused failure, but were allowed in simulation. Depending on where this object + * is in the response, the unnamed resources it contains may or may not qualify for + * group resource sharing. If this is a field in SimulateTransactionGroupResult, + * the resources do qualify, but if this is a field in SimulateTransactionResult, + * they do not qualify. In order to make this group valid for actual submission, + * resources that qualify for group sharing can be made available by any + * transaction of the group; otherwise, resources must be placed in the same + * transaction which accessed them. + */ + 'unnamed-resources-accessed'?: SimulateUnnamedResourcesAccessed +} + +/** + * Details about a pending transaction. If the transaction was recently confirmed, + * includes confirmation details like the round and reward details. + */ +export interface PendingTransactionResponse { + /** + * Indicates that the transaction was kicked out of this node's transaction pool + * (and specifies why that happened). An empty string indicates the transaction + * wasn't kicked out of this node's txpool due to an error. + */ + 'pool-error': string + /** + * The raw signed transaction. + */ + txn: EncodedSignedTransaction + /** + * The application index if the transaction was found and it created an + * application. + */ + 'application-index'?: number | bigint + /** + * The number of the asset's unit that were transferred to the close-to address. + */ + 'asset-closing-amount'?: number | bigint + /** + * The asset index if the transaction was found and it created an asset. + */ + 'asset-index'?: number | bigint + /** + * Rewards in microalgos applied to the close remainder to account. + */ + 'close-rewards'?: number | bigint + /** + * Closing amount for the transaction. + */ + 'closing-amount'?: number | bigint + /** + * The round where this transaction was confirmed, if present. + */ + 'confirmed-round'?: number | bigint + /** + * Global state key/value changes for the application being executed by this + * transaction. + */ + 'global-state-delta'?: EvalDeltaKeyValue[] + /** + * Inner transactions produced by application execution. + */ + 'inner-txns'?: PendingTransactionResponse[] + /** + * Local state key/value changes for the application being executed by this + * transaction. + */ + 'local-state-delta'?: AccountStateDelta[] + /** + * Logs for the application being executed by this transaction. + */ + logs?: Uint8Array[] + /** + * Rewards in microalgos applied to the receiver account. + */ + 'receiver-rewards'?: number | bigint + /** + * Rewards in microalgos applied to the sender account. + */ + 'sender-rewards'?: number | bigint +} + +/** + * The execution trace of calling an app or a logic sig, containing the inner app + * call trace in a recursive way. + */ +export interface SimulationTransactionExecTrace { + /** + * SHA512_256 hash digest of the approval program executed in transaction. + */ + 'approval-program-hash'?: Uint8Array + /** + * Program trace that contains a trace of opcode effects in an approval program. + */ + 'approval-program-trace'?: SimulationOpcodeTraceUnit[] + /** + * SHA512_256 hash digest of the clear state program executed in transaction. + */ + 'clear-state-program-hash'?: Uint8Array + /** + * Program trace that contains a trace of opcode effects in a clear state program. + */ + 'clear-state-program-trace'?: SimulationOpcodeTraceUnit[] + /** + * An array of SimulationTransactionExecTrace representing the execution trace of + * any inner transactions executed. + */ + 'inner-trace'?: SimulationTransactionExecTrace[] + /** + * SHA512_256 hash digest of the logic sig executed in transaction. + */ + 'logic-sig-hash'?: Uint8Array + /** + * Program trace that contains a trace of opcode effects in a logic sig. + */ + 'logic-sig-trace'?: SimulationOpcodeTraceUnit[] +} + +/** + * These are resources that were accessed by this group that would normally have + * caused failure, but were allowed in simulation. Depending on where this object + * is in the response, the unnamed resources it contains may or may not qualify for + * group resource sharing. If this is a field in SimulateTransactionGroupResult, + * the resources do qualify, but if this is a field in SimulateTransactionResult, + * they do not qualify. In order to make this group valid for actual submission, + * resources that qualify for group sharing can be made available by any + * transaction of the group; otherwise, resources must be placed in the same + * transaction which accessed them. + */ +export interface SimulateUnnamedResourcesAccessed { + /** + * The unnamed accounts that were referenced. The order of this array is arbitrary. + */ + accounts?: string[] + /** + * The unnamed application local states that were referenced. The order of this + * array is arbitrary. + */ + 'app-locals'?: ApplicationLocalReference[] + /** + * The unnamed applications that were referenced. The order of this array is + * arbitrary. + */ + apps?: (number | bigint)[] + /** + * The unnamed asset holdings that were referenced. The order of this array is + * arbitrary. + */ + 'asset-holdings'?: AssetHoldingReference[] + /** + * The unnamed assets that were referenced. The order of this array is arbitrary. + */ + assets?: (number | bigint)[] + /** + * The unnamed boxes that were referenced. The order of this array is arbitrary. + */ + boxes?: BoxReference[] + /** + * The number of extra box references used to increase the IO budget. This is in + * addition to the references defined in the input transaction group and any + * referenced to unnamed boxes. + */ + 'extra-box-refs'?: number | bigint +} + +/** + * Key-value pairs for StateDelta. + */ +export interface EvalDeltaKeyValue { + key: string + /** + * Represents a TEAL value delta. + */ + value: EvalDelta +} + +/** + * Represents a TEAL value delta. + */ +export interface EvalDelta { + /** + * (at) delta action. + */ + action: number | bigint + /** + * (bs) bytes value. + */ + bytes?: string + /** + * (ui) uint value. + */ + uint?: number | bigint +} + +/** + * Application state delta. + */ +export interface AccountStateDelta { + address: string + /** + * Application state delta. + */ + delta: EvalDeltaKeyValue[] +} + +/** + * The set of trace information and effect from evaluating a single opcode. + */ +export interface SimulationOpcodeTraceUnit { + /** + * The program counter of the current opcode being evaluated. + */ + pc: number | bigint + /** + * The writes into scratch slots. + */ + 'scratch-changes'?: ScratchChange[] + /** + * The indexes of the traces for inner transactions spawned by this opcode, if any. + */ + 'spawned-inners'?: (number | bigint)[] + /** + * The values added by this opcode to the stack. + */ + 'stack-additions'?: AvmValue[] + /** + * The number of deleted stack values by this opcode. + */ + 'stack-pop-count'?: number | bigint + /** + * The operations against the current application's states. + */ + 'state-changes'?: ApplicationStateOperation[] +} + +/** + * A write operation into a scratch slot. + */ +export interface ScratchChange { + /** + * Represents an AVM value. + */ + 'new-value': AvmValue + /** + * The scratch slot written. + */ + slot: number | bigint +} + +/** + * An operation against an application's global/local/box state. + */ +export interface ApplicationStateOperation { + /** + * Type of application state. Value `g` is **global state**, `l` is **local + * state**, `b` is **boxes**. + */ + 'app-state-type': string + /** + * The key (name) of the global/local/box state. + */ + key: Uint8Array + /** + * Operation type. Value `w` is **write**, `d` is **delete**. + */ + operation: string + /** + * For local state changes, the address of the account associated with the local + * state. + */ + account?: string + /** + * Represents an AVM value. + */ + 'new-value'?: AvmValue +} + +/** + * Represents an AVM value. + */ +export interface AvmValue { + /** + * value type. Value `1` refers to **bytes**, value `2` refers to **uint64** + */ + type: number | bigint + /** + * bytes value. + */ + bytes?: Uint8Array + /** + * uint value. + */ + uint?: number | bigint +} + +/** + * Represents an AVM key-value pair in an application store. + */ +export interface AvmKeyValue { + key: Uint8Array + /** + * Represents an AVM value. + */ + value: AvmValue +} + +/** + * References an account's local state for an application. + */ +export interface ApplicationLocalReference { + /** + * Address of the account with the local state. + */ + account: string + /** + * Application ID of the local state application. + */ + app: number | bigint +} + +/** + * References an asset held by an account. + */ +export interface AssetHoldingReference { + /** + * Address of the account holding the asset. + */ + account: string + /** + * Asset ID of the holding. + */ + asset: number | bigint +} + +/** + * References a box of an application. + */ +export interface BoxReference { + /** + * Application ID which this box belongs to + */ + app: number | bigint + /** + * Base64 encoded box name + */ + name: Uint8Array +} + +/** + * The set of parameters and limits override during simulation. If this set of + * parameters is present, then evaluation parameters may differ from standard + * evaluation in certain ways. + */ +export interface SimulationEvalOverrides { + /** + * If true, transactions without signatures are allowed and simulated as if they + * were properly signed. + */ + 'allow-empty-signatures'?: boolean + /** + * If true, allows access to unnamed resources during simulation. + */ + 'allow-unnamed-resources'?: boolean + /** + * The extra opcode budget added to each transaction group during simulation + */ + 'extra-opcode-budget'?: number | bigint + /** + * The maximum log calls one can make during simulation + */ + 'max-log-calls'?: number | bigint + /** + * The maximum byte number to log during simulation + */ + 'max-log-size'?: number | bigint +} + +export interface SimulateTraceConfig { + /** + * A boolean option for opting in execution trace features simulation endpoint. + */ + enable?: boolean + /** + * A boolean option enabling returning scratch slot changes together with execution + * trace during simulation. + */ + 'scratch-change'?: boolean + /** + * A boolean option enabling returning stack changes together with execution trace + * during simulation. + */ + 'stack-change'?: boolean + /** + * A boolean option enabling returning application state changes (global, local, + * and box changes) with the execution trace during simulation. + */ + 'state-change'?: boolean +} + +/** + * Initial states of resources that were accessed during simulation. + */ +export interface SimulateInitialStates { + /** + * The initial states of accessed application before simulation. The order of this + * array is arbitrary. + */ + 'app-initial-states'?: ApplicationInitialStates[] +} + +/** + * An application's initial global/local/box states that were accessed during + * simulation. + */ +export interface ApplicationInitialStates { + /** + * Application index. + */ + id: number | bigint + /** + * An application's global/local/box state. + */ + 'app-boxes'?: ApplicationKVStorage + /** + * An application's global/local/box state. + */ + 'app-globals'?: ApplicationKVStorage + /** + * An application's initial local states tied to different accounts. + */ + 'app-locals'?: ApplicationKVStorage[] +} + +/** + * An application's global/local/box state. + */ +export interface ApplicationKVStorage { + /** + * Key-Value pairs representing application states. + */ + kvs: AvmKeyValue[] + /** + * The address of the account associated with the local state. + */ + account?: string +} diff --git a/ui/src/utils/tests/abi.ts b/ui/src/utils/tests/abi.ts new file mode 100644 index 00000000..a015babc --- /dev/null +++ b/ui/src/utils/tests/abi.ts @@ -0,0 +1,59 @@ +import { ABITupleType, ABIValue } from 'algosdk' +import { StakingPoolSig } from '@/contracts/StakingPoolClient' +import { ValidatorRegistrySig } from '@/contracts/ValidatorRegistryClient' + +export interface MethodCallParams { + method: string + args?: Record +} + +export function encodeCallParams( + method: ValidatorRegistrySig | StakingPoolSig, + args: MethodCallParams['args'], +): Uint8Array { + const methodName = method.split('(', 1)[0] + const callParams: MethodCallParams = { method: methodName, ...(args ? { args } : {}) } + return new Uint8Array(Buffer.from(JSON.stringify(callParams))) +} + +export function parseMethodSignature(signature: string): { + name: string + args: string[] + returns: string +} { + const argsStart = signature.indexOf('(') + if (argsStart === -1) { + throw new Error(`Invalid method signature: ${signature}`) + } + + let argsEnd = -1 + let depth = 0 + for (let i = argsStart; i < signature.length; i++) { + const char = signature[i] + + if (char === '(') { + depth += 1 + } else if (char === ')') { + if (depth === 0) { + // unpaired parenthesis + break + } + + depth -= 1 + if (depth === 0) { + argsEnd = i + break + } + } + } + + if (argsEnd === -1) { + throw new Error(`Invalid method signature: ${signature}`) + } + + return { + name: signature.slice(0, argsStart), + args: ABITupleType.parseTupleContent(signature.slice(argsStart + 1, argsEnd)), + returns: signature.slice(argsEnd + 1), + } +} diff --git a/ui/src/utils/tests/constants.ts b/ui/src/utils/tests/constants.ts new file mode 100644 index 00000000..ed329f6f --- /dev/null +++ b/ui/src/utils/tests/constants.ts @@ -0,0 +1,2 @@ +export const LAST_ROUND = 0 +export const RETURN_PREFIX = Buffer.from([21, 31, 124, 117]) diff --git a/ui/src/utils/tests/fixtures/accounts.ts b/ui/src/utils/tests/fixtures/accounts.ts new file mode 100644 index 00000000..8e78e037 --- /dev/null +++ b/ui/src/utils/tests/fixtures/accounts.ts @@ -0,0 +1,8 @@ +export const ACCOUNT_1 = '7IQQUVXUJHQ4CQSDFTYEZWEWNQZWAMCQAEJPFBZCGPOPOSJ7YZZCOH25GE' +export const ACCOUNT_2 = 'PHNADH3QSUUUEQ2S5P7S77TMPYBT2FRDEJT4YR76O3S74TIKYAI2PWPC2I' +export const ACCOUNT_3 = 'QBUYJW4S73MXUB6JNH2BRVHMJ7DJEZYQGUVR54M7CDSYIKEMTCLZDWR2MU' +export const ACCOUNT_4 = 'TMP7T4X3L3TVMFUG2QOOO2TKU4NN2SGPE37EDPE66F5QT4CRZYAEJCD3AM' +export const ACCOUNT_5 = '6DBUZEFSYVJKFF53BKS54FYNI4TGYTSHLB7UHUKJLIT7LC7EQTRLJKF77M' +export const ACCOUNT_6 = 'UVN5CPWIZO6TAYOQZ2OM6AKKB4RY5KY7WDJYMGPIUWGKNCUKATSJVHARHI' +export const ACCOUNT_7 = 'AY32EQ5KUIR6DAYCG4JFZXGA7CT4YQVNBGVAWBCXGDZPCCM3Y53A5Z4F7E' +export const ACCOUNT_8 = '6U4SBRV3O52EB3B545AHHMHRS2JDL6OQBBCYDOFQWWBND36RAH25XH6U6E' diff --git a/ui/src/utils/tests/fixtures/boxes.ts b/ui/src/utils/tests/fixtures/boxes.ts new file mode 100644 index 00000000..86886078 --- /dev/null +++ b/ui/src/utils/tests/fixtures/boxes.ts @@ -0,0 +1,80 @@ +import { AlgoAmount } from '@algorandfoundation/algokit-utils/types/amount' +import algosdk from 'algosdk' +import { ALGORAND_ZERO_ADDRESS_STRING } from '@/constants/accounts' +import { StakedInfo } from '@/interfaces/staking' +import { dayjs } from '@/utils/dayjs' +import { LAST_ROUND } from '@/utils/tests/constants' +import { ACCOUNT_1, ACCOUNT_2 } from '@/utils/tests/fixtures/accounts' +import { createStaticArray } from '@/utils/tests/utils' + +export const DEFAULT_STAKED_INFO: StakedInfo = { + account: ALGORAND_ZERO_ADDRESS_STRING, + balance: BigInt(0), + totalRewarded: BigInt(0), + rewardTokenBalance: BigInt(0), + entryTime: 0, +} + +export const MOCK_STAKED_INFO_1: StakedInfo = { + account: ACCOUNT_1, + balance: BigInt(AlgoAmount.Algos(1000).microAlgos), + totalRewarded: BigInt(AlgoAmount.Algos(10).microAlgos), + rewardTokenBalance: BigInt(0), + entryTime: dayjs('2024-01-01').unix(), +} + +export const MOCK_STAKED_INFO_2: StakedInfo = { + account: ACCOUNT_2, + balance: BigInt(AlgoAmount.Algos(2000).microAlgos), + totalRewarded: BigInt(AlgoAmount.Algos(20).microAlgos), + rewardTokenBalance: BigInt(0), + entryTime: dayjs('2024-02-02').unix(), +} + +interface BoxData { + name: string + round: number + value: string // base64 encoded string +} + +interface FixtureData { + [appId: string]: { + [boxName: string]: BoxData + } +} + +// Map containing each app's corresponding box fixture data +export const boxFixtures: FixtureData = { + '1010': { + // Staking pool appId 1010 + stakers: { + name: 'stakers', + round: LAST_ROUND, + value: encodeStakersToBase64( + createStaticArray([MOCK_STAKED_INFO_1, MOCK_STAKED_INFO_2], DEFAULT_STAKED_INFO, 200), + ), + }, + }, +} + +/** + * Encodes staker information into a base64 string. + * + * @param {StakedInfo[]} stakers - Array of staker information. + * @returns {string} - The base64 encoded string of stakers' data. + */ +export function encodeStakersToBase64(stakers: StakedInfo[]): string { + const bytesPerStaker = 64 + const totalBytes = stakers.length * bytesPerStaker + const buffer = new Uint8Array(totalBytes) + + stakers.forEach((staker, index) => { + buffer.set(algosdk.decodeAddress(staker.account).publicKey, index * bytesPerStaker) + buffer.set(algosdk.bigIntToBytes(staker.balance, 8), index * bytesPerStaker + 32) + buffer.set(algosdk.bigIntToBytes(staker.totalRewarded, 8), index * bytesPerStaker + 40) + buffer.set(algosdk.bigIntToBytes(staker.rewardTokenBalance, 8), index * bytesPerStaker + 48) + buffer.set(algosdk.bigIntToBytes(BigInt(staker.entryTime), 8), index * bytesPerStaker + 56) + }) + + return Buffer.from(buffer).toString('base64') +} diff --git a/ui/src/utils/tests/fixtures/methods.ts b/ui/src/utils/tests/fixtures/methods.ts new file mode 100644 index 00000000..65bcf506 --- /dev/null +++ b/ui/src/utils/tests/fixtures/methods.ts @@ -0,0 +1,27 @@ +import { AlgoAmount } from '@algorandfoundation/algokit-utils/types/amount' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type FixtureFunction = (args: any) => any[] + +// Map containing each ABI method's corresponding fixture function +export const methodFixtures: Record = { + getPools: ({ validatorId }: { validatorId: number | bigint }) => { + const pool1 = { + appId: 1010, + balance: AlgoAmount.Algos(10_000_000).microAlgos, + } + const pool2 = { + appId: 1020, + balance: AlgoAmount.Algos(250_000).microAlgos, + } + + switch (Number(validatorId)) { + case 1: + return [[BigInt(pool1.appId), BigInt(validatorId), BigInt(pool1.balance)]] + case 2: + return [[BigInt(pool2.appId), BigInt(validatorId), BigInt(pool2.balance)]] + default: + return [[0n, 0n, 0n]] + } + }, +} diff --git a/ui/src/utils/tests/utils.ts b/ui/src/utils/tests/utils.ts new file mode 100644 index 00000000..7e89bd85 --- /dev/null +++ b/ui/src/utils/tests/utils.ts @@ -0,0 +1,39 @@ +/** + * Creates an array containing unique values followed by duplicates of a default value. + * + * @template T The type of the elements in the array. + * @param {T[]} values - An array of unique values to start the array. + * @param {T} defaultValue - The default value to fill the rest of the array. + * @param {number} length - The total desired length of the array. + * @returns {T[]} - An array of elements of type T. + */ +export function createStaticArray(values: T[], defaultValue: T, length: number): T[] { + const resultArray: T[] = [...values] + + // Calculate the remaining number of default values needed + const remainingSlots = length - values.length + + // Fill the rest of the array with the default value + for (let i = 0; i < remainingSlots; i++) { + resultArray.push(defaultValue) + } + + return resultArray +} + +/** + * Parses a box name string into encoding and value, decoding if necessary. + * @param {string} nameParam - The name parameter in the format 'encoding:value'. + * @returns {[string, string]} - A tuple containing the encoding and the (possibly decoded) value. + */ +export function parseBoxName(nameParam: string): [string, string] { + const [encoding, value] = nameParam.split(':', 2) + + if (encoding === 'b64' && value) { + // Decode base64 string to a readable format + const decodedValue = Buffer.from(value, 'base64').toString('utf-8') + return [encoding, decodedValue] + } + + return [encoding, value] +} diff --git a/ui/tsconfig.json b/ui/tsconfig.json index ee151fab..284a059d 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -27,14 +27,7 @@ }, "types": ["vitest/globals"] }, - "include": [ - "src/**/*.ts", - "src/**/*.tsx", - "vite.config.js", - "src/utils/ellipseAddress.spec.tsx", - "src/utils/ellipseAddress.spec.tsx", - "src/main.tsx" - ], + "include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts", "vitest.setup.ts"], "references": [ { "path": "./tsconfig.node.json" diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 75c39fe9..cdbb4e78 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -16,6 +16,8 @@ export default defineConfig({ dir: './src', watch: false, globals: true, + setupFiles: ['./vitest.setup.ts'], + environment: 'jsdom', coverage: { provider: 'v8', }, diff --git a/ui/vitest.setup.ts b/ui/vitest.setup.ts new file mode 100644 index 00000000..51d7c914 --- /dev/null +++ b/ui/vitest.setup.ts @@ -0,0 +1,176 @@ +/// + +import '@testing-library/jest-dom/vitest' +import * as msgpack from 'algo-msgpack-with-bigint' +import { ABIMethod, ABIType, getMethodByName } from 'algosdk' +import { http, HttpResponse } from 'msw' +import { setupServer } from 'msw/node' +import { APP_SPEC as ValidatorRegistrySpec } from '@/contracts/ValidatorRegistryClient' +import { SimulateRequest, SimulateResponse } from '@/interfaces/simulate' +import { concatUint8Arrays } from '@/utils/bytes' +import { MethodCallParams } from '@/utils/tests/abi' +import { LAST_ROUND, RETURN_PREFIX } from '@/utils/tests/constants' +import { boxFixtures } from '@/utils/tests/fixtures/boxes' +import { methodFixtures } from '@/utils/tests/fixtures/methods' +import { parseBoxName } from '@/utils/tests/utils' + +if (!Object.prototype.isPrototypeOf.call(Buffer, Uint8Array)) { + Object.setPrototypeOf(Buffer.prototype, Uint8Array.prototype) +} + +const handlers = [ + http.get('http://localhost:4001/v2/transactions/params', async () => { + // console.log('Captured a "GET /v2/transactions/params" request') + + const response = { + 'consensus-version': 'future', + fee: 0, + 'genesis-hash': 'v1lkQZYrxQn1XDRkIAlsUrSSECXU6OFMbPMhj/QQ9dk=', + 'genesis-id': 'dockernet-v1', + 'last-round': LAST_ROUND, + 'min-fee': 1000, + } + + return HttpResponse.json(response) + }), + http.post( + 'http://localhost:4001/v2/transactions/simulate', + async ({ request }) => { + try { + /* Parse request URL */ + const url = new URL(request.url) + const format = url.searchParams.get('format') + + // @todo: handle other formats? + if (format !== 'msgpack') { + throw new Error('Unknown format') + } + + // console.log(`Captured a "POST ${url.pathname}?format=${format}" request`) + + /* Inspect request */ + const requestBody = await request.arrayBuffer() + const decodedRequest = msgpack.decode(new Uint8Array(requestBody)) as SimulateRequest + // console.log('decodedRequest', decodedRequest) + + const txns = decodedRequest['txn-groups'][0].txns + const txn = txns[0] + + if (!txn.txn.note) { + throw new Error('Missing note') + } + const decoder = new TextDecoder() + const note = decoder.decode(txn.txn.note) as string + const callParams = JSON.parse(note) as MethodCallParams + + /* Construct mock response */ + const methods = ValidatorRegistrySpec.contract.methods.map( + (method) => new ABIMethod(method), + ) + const method = getMethodByName(methods, callParams.method) + if (!method) { + throw new Error('Method not found') + } + + const getFixtureData = methodFixtures[callParams.method] + if (!getFixtureData) { + throw new Error(`No fixture data available for method: ${callParams.method}`) + } + + const fixtureData = getFixtureData(callParams.args) + + const returnType = ABIType.from(method.returns.type.toString()) + const returnValue = returnType.encode(fixtureData) + const returnLogs = [concatUint8Arrays(RETURN_PREFIX, returnValue)] + + const mockResponse: SimulateResponse = { + 'last-round': LAST_ROUND, + version: 2, + 'txn-groups': [ + { + 'txn-results': [ + { + 'txn-result': { + logs: returnLogs, + 'pool-error': '', + txn, + }, + }, + ], + }, + ], + } + + /* Inspect actual response */ + // const response = await fetch(bypass(request)).then((response) => response.arrayBuffer()) + // const decodedResponse = msgpack.decode(new Uint8Array(response)) as SimulateResponse + // // console.log('decodedResponse', decodedResponse) + + /* Encode response */ + const responseBuffer = msgpack.encode(mockResponse) + + return HttpResponse.arrayBuffer(responseBuffer, { + headers: { + 'Content-Type': 'application/msgpack', + }, + }) + } catch (error) { + console.error('Error fetching data:', error) + return HttpResponse.error() + } + }, + ), + http.get('http://localhost:4001/v2/applications/:id/box', async ({ params, request }) => { + // console.log(`Captured a "GET /v2/applications/${params.id}/box" request`) + + try { + /* Parse request URL */ + const url = new URL(request.url) + const name = url.searchParams.get('name') + if (!name) { + throw new Error('Missing name parameter') + } + + const [, boxName] = parseBoxName(name) + const appId = Number(params.id) + + const boxesForApp = boxFixtures[appId] + if (!boxesForApp) { + throw new Error(`No fixtures found for app ID: ${appId}`) + } + + const boxData = boxesForApp[boxName] + if (!boxData) { + throw new Error(`Box name "${boxName}" not recognized`) + } + + const textEncoder = new TextEncoder() + const response = textEncoder.encode(JSON.stringify(boxData)).buffer + + /* Inspect actual response */ + // const response = await fetch(bypass(request)).then((response) => response.arrayBuffer()) + + // const textDecoder = new TextDecoder() + // const jsonString = textDecoder.decode(response) + // const jsonData = JSON.parse(jsonString) + + // jsonData.name = Buffer.from(jsonData.name, 'base64').toString() + // jsonData.value = new Uint8Array(Buffer.from(jsonData.value)) + + return HttpResponse.arrayBuffer(response, { + headers: { + 'Content-Type': 'application/msgpack', + }, + }) + } catch (error) { + console.error('Error fetching data:', error) + return HttpResponse.error() + } + }), +] + +const server = setupServer(...handlers) + +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) +afterAll(() => server.close()) +afterEach(() => server.resetHandlers())