diff --git a/.github/workflows/ci-ui.yaml b/.github/workflows/ci-ui.yaml new file mode 100644 index 00000000..0ffdbfa1 --- /dev/null +++ b/.github/workflows/ci-ui.yaml @@ -0,0 +1,60 @@ +name: CI Workflow (UI) + +on: + push: + branches: [ dev, main ] + pull_request: + branches: [ dev, main ] + +jobs: + run-ci: + name: Lint, Typecheck, Test, and Build + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - uses: pnpm/action-setup@v2 + name: Install pnpm + with: + version: 8 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm --filter ./ui install + + - name: Lint + run: pnpm --filter ./ui run lint + + - name: Prettier + run: pnpm --filter ./ui run prettier + + - name: Typecheck + run: pnpm --filter ./ui run typecheck + + - name: Run tests + run: pnpm --filter ./ui run test + + - name: Build + run: pnpm --filter ./ui run build diff --git a/contracts/__test__/contracts.test.ts b/contracts/__test__/contracts.test.ts index b37726af..f1da985d 100644 --- a/contracts/__test__/contracts.test.ts +++ b/contracts/__test__/contracts.test.ts @@ -4,7 +4,6 @@ import { consoleLogger } from '@algorandfoundation/algokit-utils/types/logging'; import { AlgoAmount } from '@algorandfoundation/algokit-utils/types/amount'; import { Account, decodeAddress, encodeAddress, getApplicationAddress } from 'algosdk'; import { assetOptIn, transferAlgos, transferAsset } from '@algorandfoundation/algokit-utils'; -import * as constants from 'node:constants'; import { StakingPoolClient } from '../contracts/clients/StakingPoolClient'; import { ValidatorRegistryClient } from '../contracts/clients/ValidatorRegistryClient'; import { @@ -15,6 +14,8 @@ import { createAsset, createValidatorConfig, epochBalanceUpdate, + GATING_TYPE_ASSET_ID, + GATING_TYPE_ASSETS_CREATED_BY, gatingValueFromBigint, getCurMaxStatePerPool, getMbrAmountsFromValidatorClient, @@ -33,8 +34,6 @@ import { ValidatorConfig, ValidatorPoolKey, verifyRewardAmounts, - GATING_TYPE_ASSET_ID, - GATING_TYPE_ASSETS_CREATED_BY, } from './helpers'; const FEE_SINK_ADDR = 'Y76M3MSY6DKBRHBL7C3NNDXGS5IIMQVQVUAB6MP4XEMMGVF2QWNPL226CA'; @@ -1089,6 +1088,372 @@ describe('StakeWRewards', () => { }); }); +describe('StakeW0Commission', () => { + beforeEach(fixture.beforeEach); + beforeEach(logs.beforeEach); + afterEach(logs.afterEach); + + let validatorId: number; + let validatorOwnerAccount: Account; + const stakerAccounts: Account[] = []; + let poolAppId: bigint; + let firstPoolKey: ValidatorPoolKey; + let firstPoolClient: StakingPoolClient; + + const PctToValidator = 0; + + // add validator and 1 pool for subsequent stake tests + beforeAll(async () => { + // Fund a 'validator account' that will be the validator owner. + validatorOwnerAccount = await getTestAccount( + { initialFunds: AlgoAmount.Algos(500), suppressLog: true }, + fixture.context.algod, + fixture.context.kmd + ); + consoleLogger.info(`validator account ${validatorOwnerAccount.addr}`); + + const config = createValidatorConfig({ + Owner: validatorOwnerAccount.addr, + Manager: validatorOwnerAccount.addr, + MinEntryStake: BigInt(AlgoAmount.Algos(1000).microAlgos), + MaxAlgoPerPool: BigInt(MaxAlgoPerPool), // this comes into play in later tests !! + PercentToValidator: PctToValidator * 10000, + ValidatorCommissionAddress: validatorOwnerAccount.addr, + }); + validatorId = await addValidator( + fixture.context, + validatorMasterClient, + validatorOwnerAccount, + config, + validatorMbr + ); + firstPoolKey = await addStakingPool( + fixture.context, + validatorMasterClient, + validatorId, + 1, + validatorOwnerAccount, + poolMbr, + poolInitMbr + ); + expect(firstPoolKey.id).toEqual(BigInt(validatorId)); + expect(firstPoolKey.poolId).toEqual(1n); + + firstPoolClient = new StakingPoolClient( + { sender: validatorOwnerAccount, resolveBy: 'id', id: firstPoolKey.poolAppId }, + fixture.context.algod + ); + poolAppId = ( + await validatorMasterClient.getPoolAppId( + { validatorId: firstPoolKey.id, poolId: firstPoolKey.poolId }, + { sendParams: { populateAppCallResources: true } } + ) + ).return!; + expect(firstPoolKey.poolAppId).toEqual(poolAppId); + }); + + // boilerplate at this point.. just dd some stake - testing different commissions is all we care about + test('firstStaker', async () => { + // Fund a 'staker account' that will be the new 'staker' + const stakerAccount = await getTestAccount( + { initialFunds: AlgoAmount.Algos(5000), suppressLog: true }, + fixture.context.algod, + fixture.context.kmd + ); + stakerAccounts.push(stakerAccount); + + // now stake 1000(+mbr), min for this pool - for the first time - which means actual stake amount will be reduced + // by 'first time staker' fee to cover MBR (which goes to VALIDATOR contract account, not staker contract account!) + // we pay the extra here so the final staked amount should be exactly 1000 + const stakeAmount1 = AlgoAmount.MicroAlgos( + AlgoAmount.Algos(1000).microAlgos + AlgoAmount.MicroAlgos(Number(stakerMbr)).microAlgos + ); + const [stakedPoolKey] = await addStake( + fixture.context, + validatorMasterClient, + validatorId, + stakerAccount, + stakeAmount1, + 0n + ); + // should match info from first staking pool + expect(stakedPoolKey.id).toEqual(firstPoolKey.id); + expect(stakedPoolKey.poolId).toEqual(firstPoolKey.poolId); + expect(stakedPoolKey.poolAppId).toEqual(firstPoolKey.poolAppId); + + const poolInfo = await getPoolInfo(validatorMasterClient, firstPoolKey); + expect(poolInfo.totalStakers).toEqual(1); + expect(poolInfo.totalAlgoStaked).toEqual(BigInt(stakeAmount1.microAlgos - Number(stakerMbr))); + + expect((await getValidatorState(validatorMasterClient, validatorId)).totalStakers).toEqual(1n); + }); + + test('testFirstRewards', async () => { + // increment time a day(+) at a time per transaction + await fixture.context.algod.setBlockOffsetTimestamp(60 * 61 * 24).do(); + + const origValidatorState = await getValidatorState(validatorMasterClient, validatorId); + const ownerBalance = await fixture.context.algod.accountInformation(validatorOwnerAccount.addr).do(); + const stakersPriorToReward = await getStakeInfoFromBoxValue(firstPoolClient); + + const reward = AlgoAmount.Algos(200); + // put some test 'reward' algos into staking pool + await transferAlgos( + { + from: fixture.context.testAccount, + to: getApplicationAddress(firstPoolKey.poolAppId), + amount: reward, + }, + fixture.context.algod + ); + + const poolInfo = await getPoolInfo(validatorMasterClient, firstPoolKey); + consoleLogger.info(`pool stakers:${poolInfo.totalStakers}, staked:${poolInfo.totalAlgoStaked}`); + + const epochBefore = BigInt((await firstPoolClient.appClient.getGlobalState()).epochNumber.value as bigint); + + // Perform epoch payout calculation - we also get back how much it cost to issue the txn + const fees = await epochBalanceUpdate(firstPoolClient); + const expectedValidatorReward = reward.microAlgos * (PctToValidator / 100); + + expect(BigInt((await firstPoolClient.appClient.getGlobalState()).epochNumber.value as bigint)).toEqual( + epochBefore + 1n + ); + + const newValidatorState = await getValidatorState(validatorMasterClient, validatorId); + const newOwnerBalance = await fixture.context.algod.accountInformation(validatorOwnerAccount.addr).do(); + // validator owner should have gotten the expected reward (minus the fees they just paid ofc) + expect(newOwnerBalance.amount).toEqual(ownerBalance.amount - fees.microAlgos + expectedValidatorReward); + + // Verify all the stakers in the pool got what we think they should have + const stakersAfterReward = await getStakeInfoFromBoxValue(firstPoolClient); + + await verifyRewardAmounts( + fixture.context, + (BigInt(reward.microAlgos) - BigInt(expectedValidatorReward)) as bigint, + 0n, + stakersPriorToReward as StakedInfo[], + stakersAfterReward as StakedInfo[], + 1 as number + ); + + // the total staked should have grown as well - reward minus what the validator was paid in their commission + expect(Number(newValidatorState.totalAlgoStaked)).toEqual( + Number(origValidatorState.totalAlgoStaked) + (reward.microAlgos - expectedValidatorReward) + ); + + const poolBalance = await getPoolAvailBalance(fixture.context, firstPoolKey); + expect(poolBalance).toEqual(newValidatorState.totalAlgoStaked); + }); + + test('extractRewards', async () => { + const origStakerBalance = await fixture.context.algod.accountInformation(stakerAccounts[0].addr).do(); + + const expectedBalance = AlgoAmount.Algos(1000 + 200 - 200 * (PctToValidator / 100)); + // Remove it all + const fees = await removeStake(firstPoolClient, stakerAccounts[0], expectedBalance); + + const newStakerBalance = await fixture.context.algod.accountInformation(stakerAccounts[0].addr).do(); + // 1000 algos staked + 190 reward (- fees for removing stake) + expect(newStakerBalance.amount).toEqual(origStakerBalance.amount + expectedBalance.microAlgos - fees); + + // no one should be left and be 0 balance + const postRemovePoolInfo = await getPoolInfo(validatorMasterClient, firstPoolKey); + expect(postRemovePoolInfo.totalStakers).toEqual(0); + expect(postRemovePoolInfo.totalAlgoStaked).toEqual(0n); + + const newValidatorState = await getValidatorState(validatorMasterClient, validatorId); + expect(Number(newValidatorState.totalAlgoStaked)).toEqual(0); + expect(Number(newValidatorState.totalStakers)).toEqual(0); + + const poolBalance = await getPoolAvailBalance(fixture.context, firstPoolKey); + expect(poolBalance).toEqual(newValidatorState.totalAlgoStaked); + }); +}); + +describe('StakeW100Commission', () => { + beforeEach(fixture.beforeEach); + beforeEach(logs.beforeEach); + afterEach(logs.afterEach); + + let validatorId: number; + let validatorOwnerAccount: Account; + const stakerAccounts: Account[] = []; + let poolAppId: bigint; + let firstPoolKey: ValidatorPoolKey; + let firstPoolClient: StakingPoolClient; + + const PctToValidator = 100; + + // add validator and 1 pool for subsequent stake tests + beforeAll(async () => { + // Fund a 'validator account' that will be the validator owner. + validatorOwnerAccount = await getTestAccount( + { initialFunds: AlgoAmount.Algos(500), suppressLog: true }, + fixture.context.algod, + fixture.context.kmd + ); + consoleLogger.info(`validator account ${validatorOwnerAccount.addr}`); + + const config = createValidatorConfig({ + Owner: validatorOwnerAccount.addr, + Manager: validatorOwnerAccount.addr, + MinEntryStake: BigInt(AlgoAmount.Algos(1000).microAlgos), + MaxAlgoPerPool: BigInt(MaxAlgoPerPool), // this comes into play in later tests !! + PercentToValidator: PctToValidator * 10000, + ValidatorCommissionAddress: validatorOwnerAccount.addr, + }); + validatorId = await addValidator( + fixture.context, + validatorMasterClient, + validatorOwnerAccount, + config, + validatorMbr + ); + firstPoolKey = await addStakingPool( + fixture.context, + validatorMasterClient, + validatorId, + 1, + validatorOwnerAccount, + poolMbr, + poolInitMbr + ); + expect(firstPoolKey.id).toEqual(BigInt(validatorId)); + expect(firstPoolKey.poolId).toEqual(1n); + + firstPoolClient = new StakingPoolClient( + { sender: validatorOwnerAccount, resolveBy: 'id', id: firstPoolKey.poolAppId }, + fixture.context.algod + ); + poolAppId = ( + await validatorMasterClient.getPoolAppId( + { validatorId: firstPoolKey.id, poolId: firstPoolKey.poolId }, + { sendParams: { populateAppCallResources: true } } + ) + ).return!; + expect(firstPoolKey.poolAppId).toEqual(poolAppId); + }); + + // boilerplate at this point.. just dd some stake - testing different commissions is all we care about + test('firstStaker', async () => { + // Fund a 'staker account' that will be the new 'staker' + const stakerAccount = await getTestAccount( + { initialFunds: AlgoAmount.Algos(5000), suppressLog: true }, + fixture.context.algod, + fixture.context.kmd + ); + stakerAccounts.push(stakerAccount); + + // now stake 1000(+mbr), min for this pool - for the first time - which means actual stake amount will be reduced + // by 'first time staker' fee to cover MBR (which goes to VALIDATOR contract account, not staker contract account!) + // we pay the extra here so the final staked amount should be exactly 1000 + const stakeAmount1 = AlgoAmount.MicroAlgos( + AlgoAmount.Algos(1000).microAlgos + AlgoAmount.MicroAlgos(Number(stakerMbr)).microAlgos + ); + const [stakedPoolKey] = await addStake( + fixture.context, + validatorMasterClient, + validatorId, + stakerAccount, + stakeAmount1, + 0n + ); + // should match info from first staking pool + expect(stakedPoolKey.id).toEqual(firstPoolKey.id); + expect(stakedPoolKey.poolId).toEqual(firstPoolKey.poolId); + expect(stakedPoolKey.poolAppId).toEqual(firstPoolKey.poolAppId); + + const poolInfo = await getPoolInfo(validatorMasterClient, firstPoolKey); + expect(poolInfo.totalStakers).toEqual(1); + expect(poolInfo.totalAlgoStaked).toEqual(BigInt(stakeAmount1.microAlgos - Number(stakerMbr))); + + expect((await getValidatorState(validatorMasterClient, validatorId)).totalStakers).toEqual(1n); + }); + + test('testFirstRewards', async () => { + // increment time a day(+) at a time per transaction + await fixture.context.algod.setBlockOffsetTimestamp(60 * 61 * 24).do(); + + const origValidatorState = await getValidatorState(validatorMasterClient, validatorId); + const ownerBalance = await fixture.context.algod.accountInformation(validatorOwnerAccount.addr).do(); + const stakersPriorToReward = await getStakeInfoFromBoxValue(firstPoolClient); + + const reward = AlgoAmount.Algos(200); + // put some test 'reward' algos into staking pool + await transferAlgos( + { + from: fixture.context.testAccount, + to: getApplicationAddress(firstPoolKey.poolAppId), + amount: reward, + }, + fixture.context.algod + ); + + const poolInfo = await getPoolInfo(validatorMasterClient, firstPoolKey); + consoleLogger.info(`pool stakers:${poolInfo.totalStakers}, staked:${poolInfo.totalAlgoStaked}`); + + const epochBefore = BigInt((await firstPoolClient.appClient.getGlobalState()).epochNumber.value as bigint); + + // Perform epoch payout calculation - we also get back how much it cost to issue the txn + const fees = await epochBalanceUpdate(firstPoolClient); + const expectedValidatorReward = reward.microAlgos * (PctToValidator / 100); + + expect(BigInt((await firstPoolClient.appClient.getGlobalState()).epochNumber.value as bigint)).toEqual( + epochBefore + 1n + ); + + const newValidatorState = await getValidatorState(validatorMasterClient, validatorId); + const newOwnerBalance = await fixture.context.algod.accountInformation(validatorOwnerAccount.addr).do(); + // validator owner should have gotten the expected reward (minus the fees they just paid ofc) + expect(newOwnerBalance.amount).toEqual(ownerBalance.amount - fees.microAlgos + expectedValidatorReward); + + // Verify all the stakers in the pool got what we think they should have + const stakersAfterReward = await getStakeInfoFromBoxValue(firstPoolClient); + + await verifyRewardAmounts( + fixture.context, + (BigInt(reward.microAlgos) - BigInt(expectedValidatorReward)) as bigint, + 0n, + stakersPriorToReward as StakedInfo[], + stakersAfterReward as StakedInfo[], + 1 as number + ); + + // the total staked should have grown as well - reward minus what the validator was paid in their commission + expect(Number(newValidatorState.totalAlgoStaked)).toEqual( + Number(origValidatorState.totalAlgoStaked) + (reward.microAlgos - expectedValidatorReward) + ); + + const poolBalance = await getPoolAvailBalance(fixture.context, firstPoolKey); + expect(poolBalance).toEqual(newValidatorState.totalAlgoStaked); + }); + + test('extractRewards', async () => { + const origStakerBalance = await fixture.context.algod.accountInformation(stakerAccounts[0].addr).do(); + + const expectedBalance = AlgoAmount.Algos(1000 + 200 - 200 * (PctToValidator / 100)); + // Remove it all + const fees = await removeStake(firstPoolClient, stakerAccounts[0], expectedBalance); + + const newStakerBalance = await fixture.context.algod.accountInformation(stakerAccounts[0].addr).do(); + // 1000 algos staked + 190 reward (- fees for removing stake) + expect(newStakerBalance.amount).toEqual(origStakerBalance.amount + expectedBalance.microAlgos - fees); + + // no one should be left and be 0 balance + const postRemovePoolInfo = await getPoolInfo(validatorMasterClient, firstPoolKey); + expect(postRemovePoolInfo.totalStakers).toEqual(0); + expect(postRemovePoolInfo.totalAlgoStaked).toEqual(0n); + + const newValidatorState = await getValidatorState(validatorMasterClient, validatorId); + expect(Number(newValidatorState.totalAlgoStaked)).toEqual(0); + expect(Number(newValidatorState.totalStakers)).toEqual(0); + + const poolBalance = await getPoolAvailBalance(fixture.context, firstPoolKey); + expect(poolBalance).toEqual(newValidatorState.totalAlgoStaked); + }); +}); + describe('StakeWTokenWRewards', () => { beforeEach(fixture.beforeEach); beforeEach(logs.beforeEach); diff --git a/contracts/bootstrap/package.json b/contracts/bootstrap/package.json index 207bdeab..bffd8ee8 100644 --- a/contracts/bootstrap/package.json +++ b/contracts/bootstrap/package.json @@ -1,6 +1,6 @@ { "name": "bootstrap", - "version": "0.2.0", + "version": "0.2.1", "description": "", "main": "index.ts", "scripts": { diff --git a/contracts/package.json b/contracts/package.json index 58e44603..457bab8a 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,6 +1,6 @@ { - "name": "ValidatorRegistry", - "version": "0.2.0", + "name": "reti-contracts", + "version": "0.2.1", "license": "MIT", "scripts": { "generate-client": "algokit generate client contracts/artifacts/ --language typescript --output contracts/clients/{contract_name}Client.ts && ./update_contract_artifacts.sh``", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef243c35..ae11290b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -55,7 +55,7 @@ importers: version: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@18.19.28)(ts-node@10.9.2) + version: 29.7.0 prettier: specifier: ^3.2.5 version: 3.2.5 @@ -241,9 +241,6 @@ importers: '@types/big.js': specifier: ^6.2.2 version: 6.2.2 - '@types/jest': - specifier: 29.5.2 - version: 29.5.2 '@types/node': specifier: ^18.19.20 version: 18.19.28 @@ -262,6 +259,9 @@ importers: '@vitejs/plugin-react': specifier: ^4.2.1 version: 4.2.1(vite@5.2.7) + '@vitest/coverage-v8': + specifier: ^1.4.0 + version: 1.4.0(vitest@1.4.0) autoprefixer: specifier: ^10.4.14 version: 10.4.19(postcss@8.4.38) @@ -283,9 +283,6 @@ importers: tailwindcss: specifier: 3.3.2 version: 3.3.2(ts-node@10.9.2) - ts-jest: - specifier: ^29.1.1 - version: 29.1.2(@babel/core@7.24.3)(jest@29.7.0)(typescript@5.4.3) ts-node: specifier: ^10.9.1 version: 10.9.2(@types/node@18.19.28)(typescript@5.4.3) @@ -295,6 +292,9 @@ importers: vite: specifier: ^5.0.0 version: 5.2.7(@types/node@18.19.28) + vitest: + specifier: ^1.4.0 + version: 1.4.0(@types/node@18.19.28) packages: @@ -1160,7 +1160,7 @@ packages: slash: 3.0.0 dev: true - /@jest/core@29.7.0(ts-node@10.9.2): + /@jest/core@29.7.0: resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -1181,7 +1181,7 @@ packages: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.12.2)(ts-node@10.9.2) + jest-config: 29.7.0(@types/node@20.12.2) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -3109,13 +3109,6 @@ packages: '@types/istanbul-lib-report': 3.0.3 dev: true - /@types/jest@29.5.2: - resolution: {integrity: sha512-mSoZVJF5YzGVCk+FsDxzDuH7s+SCkzrgKZzf0Z0T2WudhBUPoF6ktoTPC4R0ZoCPCV5xUvuU6ias5NvxcBcMMg==} - dependencies: - expect: 29.7.0 - pretty-format: 29.7.0 - dev: true - /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true @@ -3460,6 +3453,69 @@ packages: - supports-color dev: true + /@vitest/coverage-v8@1.4.0(vitest@1.4.0): + resolution: {integrity: sha512-4hDGyH1SvKpgZnIByr9LhGgCEuF9DKM34IBLCC/fVfy24Z3+PZ+Ii9hsVBsHvY1umM1aGPEjceRkzxCfcQ10wg==} + peerDependencies: + vitest: 1.4.0 + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.3.4 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.4 + istanbul-reports: 3.1.7 + magic-string: 0.30.8 + magicast: 0.3.3 + picocolors: 1.0.0 + std-env: 3.7.0 + strip-literal: 2.1.0 + test-exclude: 6.0.0 + v8-to-istanbul: 9.2.0 + vitest: 1.4.0(@types/node@18.19.28) + transitivePeerDependencies: + - supports-color + dev: true + + /@vitest/expect@1.4.0: + resolution: {integrity: sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==} + dependencies: + '@vitest/spy': 1.4.0 + '@vitest/utils': 1.4.0 + chai: 4.4.1 + dev: true + + /@vitest/runner@1.4.0: + resolution: {integrity: sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==} + dependencies: + '@vitest/utils': 1.4.0 + p-limit: 5.0.0 + pathe: 1.1.2 + dev: true + + /@vitest/snapshot@1.4.0: + resolution: {integrity: sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==} + dependencies: + magic-string: 0.30.8 + pathe: 1.1.2 + pretty-format: 29.7.0 + dev: true + + /@vitest/spy@1.4.0: + resolution: {integrity: sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==} + dependencies: + tinyspy: 2.2.1 + dev: true + + /@vitest/utils@1.4.0: + resolution: {integrity: sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==} + dependencies: + diff-sequences: 29.6.3 + estree-walker: 3.0.3 + loupe: 2.3.7 + pretty-format: 29.7.0 + dev: true + /@walletconnect/browser-utils@1.8.0: resolution: {integrity: sha512-Wcqqx+wjxIo9fv6eBUFHPsW1y/bGWWRboni5dfD8PtOmrihrEpOCmvRJe4rfl7xgJW8Ea9UqKEaq0bIRLHlK4A==} dependencies: @@ -4148,6 +4204,10 @@ packages: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} dev: true + /assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: true + /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: false @@ -4357,6 +4417,11 @@ packages: streamsearch: 1.1.0 dev: false + /cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + dev: true + /cache-parser@1.2.4: resolution: {integrity: sha512-O0KwuHuJnbHUrghHi2kGp0SxnWSIBXTYt7M8WVhW0kbPRUNUKoE/Of6e1rRD6AAxmfxFunKnt90yEK09D+sc5g==} dev: false @@ -4408,6 +4473,19 @@ packages: upper-case-first: 2.0.2 dev: true + /chai@4.4.1: + resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.3 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: true + /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -4446,6 +4524,12 @@ packages: engines: {node: '>=10'} dev: true + /check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + dependencies: + get-func-name: 2.0.2 + dev: true + /chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -4610,7 +4694,7 @@ packages: toggle-selection: 1.0.6 dev: false - /create-jest@29.7.0(@types/node@18.19.28)(ts-node@10.9.2): + /create-jest@29.7.0: resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -4619,7 +4703,7 @@ packages: chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@18.19.28)(ts-node@10.9.2) + jest-config: 29.7.0(@types/node@20.12.2) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -4811,6 +4895,13 @@ packages: optional: true dev: true + /deep-eql@4.1.3: + resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + engines: {node: '>=6'} + dependencies: + type-detect: 4.0.8 + dev: true + /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true @@ -5390,6 +5481,12 @@ packages: engines: {node: '>=4.0'} dev: true + /estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + dependencies: + '@types/estree': 1.0.5 + dev: true + /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -5432,7 +5529,6 @@ packages: onetime: 6.0.0 signal-exit: 4.1.0 strip-final-newline: 3.0.0 - dev: false /exit@0.1.2: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} @@ -5627,6 +5723,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + /get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + dev: true + /get-intrinsic@1.2.4: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} @@ -5660,7 +5760,6 @@ packages: /get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} - dev: false /get-symbol-description@1.0.2: resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} @@ -5853,7 +5952,6 @@ packages: /human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} - dev: false /idb-keyval@6.2.1: resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==} @@ -6056,7 +6154,6 @@ packages: /is-stream@3.0.0: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: false /is-string@1.0.7: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} @@ -6161,6 +6258,17 @@ packages: - supports-color dev: true + /istanbul-lib-source-maps@5.0.4: + resolution: {integrity: sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==} + engines: {node: '>=10'} + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + debug: 4.3.4 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + dev: true + /istanbul-reports@3.1.7: resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} engines: {node: '>=8'} @@ -6215,7 +6323,7 @@ packages: - supports-color dev: true - /jest-cli@29.7.0(@types/node@18.19.28)(ts-node@10.9.2): + /jest-cli@29.7.0: resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -6225,14 +6333,14 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2) + '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@18.19.28)(ts-node@10.9.2) + create-jest: 29.7.0 exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@18.19.28)(ts-node@10.9.2) + jest-config: 29.7.0(@types/node@20.12.2) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -6243,48 +6351,7 @@ packages: - ts-node dev: true - /jest-config@29.7.0(@types/node@18.19.28)(ts-node@10.9.2): - resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@types/node': '*' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - ts-node: - optional: true - dependencies: - '@babel/core': 7.24.3 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 18.19.28 - babel-jest: 29.7.0(@babel/core@7.24.3) - chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.5 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - ts-node: 10.9.2(@types/node@18.19.28)(typescript@5.4.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - dev: true - - /jest-config@29.7.0(@types/node@20.12.2)(ts-node@10.9.2): + /jest-config@29.7.0(@types/node@20.12.2): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -6319,7 +6386,6 @@ packages: pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.2(@types/node@18.19.28)(typescript@5.4.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -6608,7 +6674,7 @@ packages: supports-color: 8.1.1 dev: true - /jest@29.7.0(@types/node@18.19.28)(ts-node@10.9.2): + /jest@29.7.0: resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -6618,10 +6684,10 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2) + '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@18.19.28)(ts-node@10.9.2) + jest-cli: 29.7.0 transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -6645,6 +6711,10 @@ packages: /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + /js-tokens@9.0.0: + resolution: {integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==} + dev: true + /js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true @@ -6700,7 +6770,6 @@ packages: /jsonc-parser@3.2.1: resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} - dev: false /jsonschema@1.4.1: resolution: {integrity: sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==} @@ -6792,6 +6861,14 @@ packages: lit-html: 2.8.0 dev: false + /local-pkg@0.5.0: + resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} + engines: {node: '>=14'} + dependencies: + mlly: 1.6.1 + pkg-types: 1.0.3 + dev: true + /locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -6832,6 +6909,12 @@ packages: resolution: {integrity: sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==} dev: false + /loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + dependencies: + get-func-name: 2.0.2 + dev: true + /lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: @@ -6862,6 +6945,21 @@ packages: react: 18.2.0 dev: false + /magic-string@0.30.8: + resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /magicast@0.3.3: + resolution: {integrity: sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==} + dependencies: + '@babel/parser': 7.24.1 + '@babel/types': 7.24.0 + source-map-js: 1.2.0 + dev: true + /make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -6918,7 +7016,6 @@ packages: /mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} - dev: false /minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} @@ -6971,7 +7068,6 @@ packages: pathe: 1.1.2 pkg-types: 1.0.3 ufo: 1.5.3 - dev: false /motion@10.16.2: resolution: {integrity: sha512-p+PurYqfUdcJZvtnmAqu5fJgV2kR0uLFQuBKtLeFVTrYEVllI99tiOTSefVNYuip9ELTEkepIIDftNdze76NAQ==} @@ -7147,7 +7243,6 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: path-key: 4.0.0 - dev: false /nunjucks@3.2.4: resolution: {integrity: sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==} @@ -7265,7 +7360,6 @@ packages: engines: {node: '>=12'} dependencies: mimic-fn: 4.0.0 - dev: false /optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} @@ -7292,6 +7386,13 @@ packages: yocto-queue: 0.1.0 dev: true + /p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} + dependencies: + yocto-queue: 1.0.0 + dev: true + /p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -7367,7 +7468,6 @@ packages: /path-key@4.0.0: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} - dev: false /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -7386,7 +7486,10 @@ packages: /pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - dev: false + + /pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: true /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -7444,7 +7547,6 @@ packages: jsonc-parser: 3.2.1 mlly: 1.6.1 pathe: 1.1.2 - dev: false /playwright-core@1.42.1: resolution: {integrity: sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==} @@ -8058,6 +8160,10 @@ packages: object-inspect: 1.13.1 dev: true + /siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + dev: true + /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: true @@ -8139,9 +8245,12 @@ packages: escape-string-regexp: 2.0.0 dev: true + /stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + dev: true + /std-env@3.7.0: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} - dev: false /stream-shift@1.0.3: resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} @@ -8244,13 +8353,18 @@ packages: /strip-final-newline@3.0.0: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} - dev: false /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} dev: true + /strip-literal@2.1.0: + resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==} + dependencies: + js-tokens: 9.0.0 + dev: true + /styled-jsx@5.1.1(@babel/core@7.24.3)(react@18.2.0): resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} engines: {node: '>= 12.0.0'} @@ -8402,6 +8516,20 @@ packages: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} dev: false + /tinybench@2.6.0: + resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==} + dev: true + + /tinypool@0.8.3: + resolution: {integrity: sha512-Ud7uepAklqRH1bvwy22ynrliC7Dljz7Tm8M/0RBUW+YRa4YHhZ6e4PpgE+fu1zr/WqB1kbeuVrdfeuyIBpy4tw==} + engines: {node: '>=14.0.0'} + dev: true + + /tinyspy@2.2.1: + resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} + engines: {node: '>=14.0.0'} + dev: true + /tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} dev: true @@ -8460,7 +8588,7 @@ packages: '@babel/core': 7.24.3 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@18.19.28)(ts-node@10.9.2) + jest: 29.7.0 jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -8620,7 +8748,6 @@ packages: /ufo@1.5.3: resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} - dev: false /uint8arrays@3.1.1: resolution: {integrity: sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==} @@ -8854,6 +8981,27 @@ packages: d3-timer: 3.0.1 dev: false + /vite-node@1.4.0(@types/node@18.19.28): + resolution: {integrity: sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + pathe: 1.1.2 + picocolors: 1.0.0 + vite: 5.2.7(@types/node@18.19.28) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vite@5.2.7(@types/node@18.19.28): resolution: {integrity: sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -8890,6 +9038,62 @@ packages: fsevents: 2.3.3 dev: true + /vitest@1.4.0(@types/node@18.19.28): + resolution: {integrity: sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.4.0 + '@vitest/ui': 1.4.0 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/node': 18.19.28 + '@vitest/expect': 1.4.0 + '@vitest/runner': 1.4.0 + '@vitest/snapshot': 1.4.0 + '@vitest/spy': 1.4.0 + '@vitest/utils': 1.4.0 + acorn-walk: 8.3.2 + chai: 4.4.1 + debug: 4.3.4 + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.8 + pathe: 1.1.2 + picocolors: 1.0.0 + std-env: 3.7.0 + strip-literal: 2.1.0 + tinybench: 2.6.0 + tinypool: 0.8.3 + vite: 5.2.7(@types/node@18.19.28) + vite-node: 1.4.0(@types/node@18.19.28) + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vlq@2.0.4: resolution: {integrity: sha512-aodjPa2wPQFkra1G8CzJBTHXhgk3EVSwxSWXNPr1fgdFLUb8kvLV1iEb6rFgasIsjP82HWI6dsb5Io26DDnasA==} @@ -8942,6 +9146,15 @@ packages: dependencies: isexe: 2.0.0 + /why-is-node-running@2.2.2: + resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + dev: true + /wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -9074,5 +9287,10 @@ packages: engines: {node: '>=10'} dev: true + /yocto-queue@1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + dev: true + /zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} diff --git a/ui/.env.template b/ui/.env.template index c4e1a224..cf5acf47 100644 --- a/ui/.env.template +++ b/ui/.env.template @@ -8,7 +8,7 @@ VITE_ENVIRONMENT=local VITE_ALGOD_TOKEN=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa VITE_ALGOD_SERVER=http://localhost VITE_ALGOD_PORT=4001 -VITE_ALGOD_NETWORK="" +VITE_ALGOD_NETWORK="localnet" # Indexer VITE_INDEXER_TOKEN=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa diff --git a/ui/.prettierignore b/ui/.prettierignore index dbda6ae9..3ab7444f 100644 --- a/ui/.prettierignore +++ b/ui/.prettierignore @@ -1,12 +1,9 @@ -# don't ever format node_modules node_modules -# don't lint format output (make sure it's set to your correct build folder name) dist build -# don't format nyc coverage output coverage -# don't format generated types **/generated/types.d.ts **/generated/types.ts -# don't format ide files .idea + +src/contracts diff --git a/ui/jest.config.ts b/ui/jest.config.ts deleted file mode 100644 index eab1ba7f..00000000 --- a/ui/jest.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Config } from '@jest/types' - -const config: Config.InitialOptions = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/*.spec.ts', '**/*.spec.tsx'], - moduleDirectories: ['node_modules', 'src'], - transform: { - '': [ - 'ts-jest', - { - tsconfig: 'tsconfig.test.json', - }, - ], - }, - coveragePathIgnorePatterns: ['tests'], - testPathIgnorePatterns: ['/tests/'], -} - -export default config diff --git a/ui/package.json b/ui/package.json index 26911f12..4653343e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "reti-ui", - "version": "0.2.0", + "version": "0.2.1", "author": { "name": "Doug Richar", "email": "drichar@gmail.com" @@ -14,13 +14,13 @@ "@playwright/test": "^1.35.0", "@tanstack/router-vite-plugin": "^1.18.1", "@types/big.js": "^6.2.2", - "@types/jest": "29.5.2", "@types/node": "^18.19.20", "@types/react": "^18.2.11", "@types/react-dom": "^18.2.4", "@typescript-eslint/eslint-plugin": "^6.5.0", "@typescript-eslint/parser": "^6.5.0", "@vitejs/plugin-react": "^4.2.1", + "@vitest/coverage-v8": "^1.4.0", "autoprefixer": "^10.4.14", "eslint": "^8.42.0", "eslint-config-prettier": "^8.8.0", @@ -28,10 +28,10 @@ "playwright": "^1.35.0", "postcss": "^8.4.24", "tailwindcss": "3.3.2", - "ts-jest": "^29.1.1", "ts-node": "^10.9.1", "typescript": "^5.4.2", - "vite": "^5.0.0" + "vite": "^5.0.0", + "vitest": "^1.4.0" }, "dependencies": { "@algorandfoundation/algokit-utils": "^5.0.0", @@ -85,7 +85,8 @@ "dev:testnet": "vite --mode testnet --port 5183", "dev:mainnet": "vite --mode mainnet --port 5193", "build": "tsc && vite build", - "test": "jest --coverage --passWithNoTests", + "test": "vitest", + "coverage": "vitest --coverage", "playwright:test": "playwright test", "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint:fix": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0 --fix", diff --git a/ui/src/api/contracts.ts b/ui/src/api/contracts.ts index f6ee97df..4e4ef12d 100644 --- a/ui/src/api/contracts.ts +++ b/ui/src/api/contracts.ts @@ -26,7 +26,6 @@ import { } from '@/utils/contracts' import { getAlgodConfigFromViteEnvironment } from '@/utils/network/getAlgoClientConfigs' import { getRetiAppIdFromViteEnvironment } from '@/utils/env' -import { getActiveWalletAddress } from '@/utils/wallets' const algodConfig = getAlgodConfigFromViteEnvironment() const algodClient = algokit.getAlgoClient({ diff --git a/ui/src/api/queries.ts b/ui/src/api/queries.ts index aebc2ebd..d893b9e6 100644 --- a/ui/src/api/queries.ts +++ b/ui/src/api/queries.ts @@ -41,7 +41,7 @@ export const mbrQueryOptions = queryOptions({ export const constraintsQueryOptions = queryOptions({ queryKey: ['constraints'], queryFn: () => fetchProtocolConstraints(), - staleTime: Infinity, + staleTime: 1000 * 60 * 30, // every 30 mins }) export const balanceQueryOptions = (address: string | null) => diff --git a/ui/src/components/StakingTable.tsx b/ui/src/components/StakingTable.tsx index 40b4c84a..710f96f0 100644 --- a/ui/src/components/StakingTable.tsx +++ b/ui/src/components/StakingTable.tsx @@ -1,4 +1,4 @@ -import { useQuery, useQueryClient } from '@tanstack/react-query' +import { useQueryClient } from '@tanstack/react-query' import { useRouter } from '@tanstack/react-router' import { ColumnDef, @@ -15,7 +15,6 @@ import { useWallet } from '@txnlab/use-wallet-react' import dayjs from 'dayjs' import { FlaskConical, MoreHorizontal } from 'lucide-react' import * as React from 'react' -import { constraintsQueryOptions } from '@/api/queries' import { AddStakeModal } from '@/components/AddStakeModal' import { AlgoDisplayAmount } from '@/components/AlgoDisplayAmount' import { DataTableColumnHeader } from '@/components/DataTableColumnHeader' @@ -38,7 +37,7 @@ import { } from '@/components/ui/table' import { UnstakeModal } from '@/components/UnstakeModal' import { StakerValidatorData } from '@/interfaces/staking' -import { Validator } from '@/interfaces/validator' +import { Constraints, Validator } from '@/interfaces/validator' import { canManageValidator, isStakingDisabled, isUnstakingDisabled } from '@/utils/contracts' import { simulateEpoch } from '@/utils/development' import { cn } from '@/utils/ui' @@ -47,9 +46,15 @@ interface StakingTableProps { validators: Validator[] stakesByValidator: StakerValidatorData[] isLoading: boolean + constraints: Constraints } -export function StakingTable({ validators, stakesByValidator, isLoading }: StakingTableProps) { +export function StakingTable({ + validators, + stakesByValidator, + isLoading, + constraints, +}: StakingTableProps) { const [sorting, setSorting] = React.useState([]) const [columnFilters, setColumnFilters] = React.useState([]) const [columnVisibility, setColumnVisibility] = React.useState({}) @@ -60,8 +65,6 @@ export function StakingTable({ validators, stakesByValidator, isLoading }: Staki const { transactionSigner, activeAddress } = useWallet() - const { data: constraints } = useQuery(constraintsQueryOptions) - const router = useRouter() const queryClient = useQueryClient() diff --git a/ui/src/components/ValidatorTable.tsx b/ui/src/components/ValidatorTable.tsx index 6a4ac352..ab7c74df 100644 --- a/ui/src/components/ValidatorTable.tsx +++ b/ui/src/components/ValidatorTable.tsx @@ -1,5 +1,4 @@ import { AlgoAmount } from '@algorandfoundation/algokit-utils/types/amount' -import { useQuery } from '@tanstack/react-query' import { Link } from '@tanstack/react-router' import { ColumnDef, @@ -15,7 +14,6 @@ import { import { useWallet } from '@txnlab/use-wallet-react' import { FlaskConical, MoreHorizontal } from 'lucide-react' import * as React from 'react' -import { constraintsQueryOptions } from '@/api/queries' import { AddPoolModal } from '@/components/AddPoolModal' import { AddStakeModal } from '@/components/AddStakeModal' import { AlgoDisplayAmount } from '@/components/AlgoDisplayAmount' @@ -42,7 +40,7 @@ import { } from '@/components/ui/table' import { UnstakeModal } from '@/components/UnstakeModal' import { StakerValidatorData } from '@/interfaces/staking' -import { Validator } from '@/interfaces/validator' +import { Constraints, Validator } from '@/interfaces/validator' import { calculateMaxStake, calculateMaxStakers, @@ -59,9 +57,14 @@ import { cn } from '@/utils/ui' interface ValidatorTableProps { validators: Validator[] stakesByValidator: StakerValidatorData[] + constraints: Constraints } -export function ValidatorTable({ validators, stakesByValidator }: ValidatorTableProps) { +export function ValidatorTable({ + validators, + stakesByValidator, + constraints, +}: ValidatorTableProps) { const [sorting, setSorting] = React.useState([]) const [columnFilters, setColumnFilters] = React.useState([]) const [columnVisibility, setColumnVisibility] = React.useState({}) @@ -73,8 +76,6 @@ export function ValidatorTable({ validators, stakesByValidator }: ValidatorTable const { transactionSigner, activeAddress } = useWallet() - const { data: constraints } = useQuery(constraintsQueryOptions) - const columns: ColumnDef[] = [ { accessorKey: 'id', @@ -116,7 +117,7 @@ export function ValidatorTable({ validators, stakesByValidator }: ValidatorTable notation: 'compact', }).format(currentStake) - const maxStake = calculateMaxStake(validator, true) + const maxStake = calculateMaxStake(validator, constraints, true) const maxStakeCompact = new Intl.NumberFormat(undefined, { notation: 'compact', }).format(maxStake) @@ -138,7 +139,7 @@ export function ValidatorTable({ validators, stakesByValidator }: ValidatorTable if (validator.state.numPools == 0) return '--' const totalStakers = validator.state.totalStakers - const maxStakers = calculateMaxStakers(validator) + const maxStakers = calculateMaxStakers(validator, constraints) return ( @@ -173,7 +174,7 @@ export function ValidatorTable({ validators, stakesByValidator }: ValidatorTable const validator = row.original const stakingDisabled = isStakingDisabled(activeAddress, validator, constraints) const unstakingDisabled = isUnstakingDisabled(activeAddress, validator, stakesByValidator) - const addingPoolDisabled = isAddingPoolDisabled(activeAddress, validator) + const addingPoolDisabled = isAddingPoolDisabled(activeAddress, validator, constraints) const canManage = canManageValidator(activeAddress, validator) const isDevelopment = process.env.NODE_ENV === 'development' @@ -313,7 +314,7 @@ export function ValidatorTable({ validators, stakesByValidator }: ValidatorTable ))} - {table.getRowModel().rows?.length ? ( + {table.getRowModel().rows.length ? ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( diff --git a/ui/src/components/ui/avatar.tsx b/ui/src/components/ui/avatar.tsx index 33ae3c6e..26e303f1 100644 --- a/ui/src/components/ui/avatar.tsx +++ b/ui/src/components/ui/avatar.tsx @@ -1,7 +1,7 @@ -import * as React from "react" -import * as AvatarPrimitive from "@radix-ui/react-avatar" +import * as React from 'react' +import * as AvatarPrimitive from '@radix-ui/react-avatar' -import { cn } from "@/utils/ui" +import { cn } from '@/utils/ui' const Avatar = React.forwardRef< React.ElementRef, @@ -9,10 +9,7 @@ const Avatar = React.forwardRef< >(({ className, ...props }, ref) => ( )) @@ -24,7 +21,7 @@ const AvatarImage = React.forwardRef< >(({ className, ...props }, ref) => ( )) @@ -37,8 +34,8 @@ const AvatarFallback = React.forwardRef< diff --git a/ui/src/components/ui/card.tsx b/ui/src/components/ui/card.tsx index d67b9e57..d009c087 100644 --- a/ui/src/components/ui/card.tsx +++ b/ui/src/components/ui/card.tsx @@ -1,76 +1,56 @@ -import * as React from "react" - -import { cn } from "@/utils/ui" - -const Card = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -Card.displayName = "Card" - -const CardHeader = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardHeader.displayName = "CardHeader" - -const CardTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)) -CardTitle.displayName = "CardTitle" +import * as React from 'react' + +import { cn } from '@/utils/ui' + +const Card = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +) +Card.displayName = 'Card' + +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +) +CardHeader.displayName = 'CardHeader' + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ), +) +CardTitle.displayName = 'CardTitle' const CardDescription = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( -

-)) -CardDescription.displayName = "CardDescription" - -const CardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)) -CardContent.displayName = "CardContent" - -const CardFooter = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
+

)) -CardFooter.displayName = "CardFooter" +CardDescription.displayName = 'CardDescription' + +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ), +) +CardContent.displayName = 'CardContent' + +const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +) +CardFooter.displayName = 'CardFooter' export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/ui/src/components/ui/form.tsx b/ui/src/components/ui/form.tsx index 76125903..cbe3c7aa 100644 --- a/ui/src/components/ui/form.tsx +++ b/ui/src/components/ui/form.tsx @@ -1,6 +1,6 @@ -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" -import { Slot } from "@radix-ui/react-slot" +import * as React from 'react' +import * as LabelPrimitive from '@radix-ui/react-label' +import { Slot } from '@radix-ui/react-slot' import { Controller, ControllerProps, @@ -8,27 +8,25 @@ import { FieldValues, FormProvider, useFormContext, -} from "react-hook-form" +} from 'react-hook-form' -import { cn } from "@/utils/ui" -import { Label } from "@/components/ui/label" +import { cn } from '@/utils/ui' +import { Label } from '@/components/ui/label' const Form = FormProvider type FormFieldContextValue< TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath + TName extends FieldPath = FieldPath, > = { name: TName } -const FormFieldContext = React.createContext( - {} as FormFieldContextValue -) +const FormFieldContext = React.createContext({} as FormFieldContextValue) const FormField = < TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath + TName extends FieldPath = FieldPath, >({ ...props }: ControllerProps) => { @@ -47,7 +45,7 @@ const useFormField = () => { const fieldState = getFieldState(fieldContext.name, formState) if (!fieldContext) { - throw new Error("useFormField should be used within ") + throw new Error('useFormField should be used within ') } const { id } = itemContext @@ -66,23 +64,20 @@ type FormItemContextValue = { id: string } -const FormItemContext = React.createContext( - {} as FormItemContextValue -) +const FormItemContext = React.createContext({} as FormItemContextValue) -const FormItem = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => { - const id = React.useId() +const FormItem = React.forwardRef>( + ({ className, ...props }, ref) => { + const id = React.useId() - return ( - -
- - ) -}) -FormItem.displayName = "FormItem" + return ( + +
+ + ) + }, +) +FormItem.displayName = 'FormItem' const FormLabel = React.forwardRef< React.ElementRef, @@ -93,13 +88,13 @@ const FormLabel = React.forwardRef< return (
diff --git a/ui/src/routes/validators.tsx b/ui/src/routes/validators.tsx index 38c7438d..c1cc9070 100644 --- a/ui/src/routes/validators.tsx +++ b/ui/src/routes/validators.tsx @@ -1,14 +1,6 @@ -import { Navigate, createFileRoute, redirect } from '@tanstack/react-router' -import { isWalletConnected } from '@/utils/wallets' +import { Navigate, createFileRoute } from '@tanstack/react-router' export const Route = createFileRoute('/validators')({ - beforeLoad: async () => { - if (!isWalletConnected()) { - throw redirect({ - to: '/', - }) - } - }, component: Validators, }) diff --git a/ui/src/styles/main.css b/ui/src/styles/main.css index 1a77e8e0..175742b7 100644 --- a/ui/src/styles/main.css +++ b/ui/src/styles/main.css @@ -62,6 +62,8 @@ font-family: 'Algo'; font-style: normal; font-weight: 400; - src: url('/fonts/Algo.woff2') format('woff2'), url('/fonts/Algo.woff') format('woff'), + src: + url('/fonts/Algo.woff2') format('woff2'), + url('/fonts/Algo.woff') format('woff'), url('/fonts/Algo.ttf') format('truetype'); } diff --git a/ui/src/utils/contracts.ts b/ui/src/utils/contracts.ts index 99f51d51..7dffddf5 100644 --- a/ui/src/utils/contracts.ts +++ b/ui/src/utils/contracts.ts @@ -378,9 +378,23 @@ export function getAddValidatorFormSchema(constraints: Constraints) { }) } -export function calculateMaxStake(validator: Validator, algos = false): number { +export function calculateMaxStake( + validator: Validator, + constraints?: Constraints, + algos = false, +): number { const { numPools } = validator.state - const { maxAlgoPerPool } = validator.config + if (numPools === 0 || !constraints) { + return 0 + } + const hardMaxDividedBetweenPools = constraints.maxAlgoPerValidator / BigInt(numPools) + let { maxAlgoPerPool } = validator.config + if (maxAlgoPerPool === 0n) { + maxAlgoPerPool = constraints.maxAlgoPerPool + } + if (hardMaxDividedBetweenPools < maxAlgoPerPool) { + maxAlgoPerPool = hardMaxDividedBetweenPools + } const maxStake = Number(maxAlgoPerPool) * numPools @@ -391,9 +405,8 @@ export function calculateMaxStake(validator: Validator, algos = false): number { return maxStake } -export function calculateMaxStakers(validator: Validator): number { - // @todo: fetch max stakers from contract - const maxStakersPerPool = 200 +export function calculateMaxStakers(validator: Validator, constraints?: Constraints): number { + const maxStakersPerPool = constraints?.maxStakersPerPool || 0 const maxStakers = maxStakersPerPool * validator.state.numPools return maxStakers @@ -414,8 +427,7 @@ export function isStakingDisabled( maxAlgoPerPool = constraints.maxAlgoPerPool } - // @todo: fetch max stakers from contract - const maxStakersPerPool = 200 + const maxStakersPerPool = constraints?.maxStakersPerPool || 0 const maxStakers = maxStakersPerPool * numPools const maxStake = Number(maxAlgoPerPool) * numPools @@ -441,16 +453,19 @@ export function isUnstakingDisabled( return noPools || !validatorHasStake } -export function isAddingPoolDisabled(activeAddress: string | null, validator: Validator): boolean { - if (!activeAddress) { +export function isAddingPoolDisabled( + activeAddress: string | null, + validator: Validator, + constraints?: Constraints, +): boolean { + if (!activeAddress || !constraints) { return true } - // @todo: define totalNodes as global constant or fetch from protocol constraints - const totalNodes = 4 + const maxNodes = constraints.maxNodes const { numPools } = validator.state const { poolsPerNode } = validator.config - const hasAvailableSlots = numPools < poolsPerNode * totalNodes + const hasAvailableSlots = numPools < poolsPerNode * maxNodes return !hasAvailableSlots } diff --git a/ui/src/utils/wallets.ts b/ui/src/utils/wallets.ts deleted file mode 100644 index 12283cba..00000000 --- a/ui/src/utils/wallets.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { State, defaultState } from '@txnlab/use-wallet-react' - -export function getWalletStateFromLocalStorage(): State { - try { - const serializedState = localStorage.getItem('@txnlab/use-wallet:v3') - if (serializedState === null) { - return defaultState - } - const parsedState = JSON.parse(serializedState) as State - return parsedState - } catch (error) { - console.error('Error getting wallet state:', error) - return defaultState - } -} - -export function getActiveWalletAddress(): string | null { - const state = getWalletStateFromLocalStorage() - const wallets = state.wallets - const activeWalletState = state.activeWallet ? wallets[state.activeWallet] || null : null - return activeWalletState ? activeWalletState.activeAccount?.address || null : null -} - -/** - * This is used by the router to check if a wallet is connected before loading protected routes. - * @returns {boolean} - */ -export function isWalletConnected(): boolean { - const activeWalletAddress = getActiveWalletAddress() - return !!activeWalletAddress -} diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 5a763726..ee151fab 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -24,7 +24,8 @@ "baseUrl": ".", "paths": { "@/*": ["./src/*"] - } + }, + "types": ["vitest/globals"] }, "include": [ "src/**/*.ts", diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 6f1c5ed1..3e3756cf 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -1,9 +1,9 @@ +/// import { TanStackRouterVite } from '@tanstack/router-vite-plugin' import react from '@vitejs/plugin-react' import path from 'path' import { defineConfig } from 'vite' -// https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), TanStackRouterVite()], resolve: { @@ -11,4 +11,13 @@ export default defineConfig({ '@': path.resolve(__dirname, './src'), }, }, + test: { + name: 'reti-ui', + dir: './src', + watch: false, + globals: true, + coverage: { + provider: 'v8', + }, + }, })