diff --git a/docs/testing-env.md b/docs/testing-env.md
index 19a472c8b..4a65a8adc 100644
--- a/docs/testing-env.md
+++ b/docs/testing-env.md
@@ -3,7 +3,7 @@
### Branches
- `main`
- - branch `main` is the base branch for current developement
+ - branch `main` is the base branch for current development
- every feature branch should be merged into `main` via pull request
- `stage-live`
- base for running the staging environment that is running on the live chain
diff --git a/src/redux-state/selectors/population.ts b/src/redux-state/selectors/population.ts
index a13a8f625..44ab70435 100644
--- a/src/redux-state/selectors/population.ts
+++ b/src/redux-state/selectors/population.ts
@@ -1,22 +1,33 @@
import { createSelector } from "@reduxjs/toolkit"
-import { RealmData } from "shared/types"
+import {
+ getDisplayedPopulationOfRealms,
+ getPopulationOfRealms,
+} from "redux-state/utils/population"
import { selectRealmById, selectRealms } from "./realm"
-const getPopulationOfRealms = (realms: RealmData[]) =>
- realms.map((realm) => realm.population)
+type PopulationKey = "population" | "displayedPopulation"
-export const selectSortedPopulation = createSelector(selectRealms, (realms) => {
- const realmsData = Object.entries(realms).map(([id, data]) => ({
- id,
- ...data,
- }))
+const getPopulationById = (key: PopulationKey) =>
+ createSelector(selectRealmById, (realm) => realm?.[key] ?? 0)
- return realmsData.sort((a, b) => a.population - b.population)
-})
+const sortPopulation = (key: PopulationKey) =>
+ createSelector(selectRealms, (realms) => {
+ const realmsData = Object.entries(realms).map(([id, data]) => ({
+ id,
+ ...data,
+ }))
-export const selectPopulationById = createSelector(
- selectRealmById,
- (realm) => realm?.population ?? 0
+ return realmsData.sort((a, b) => a[key] - b[key])
+ })
+
+export const selectSortedPopulation = sortPopulation("population")
+export const selectSortedDisplayedPopulation = sortPopulation(
+ "displayedPopulation"
+)
+
+export const selectPopulationById = getPopulationById("population")
+export const selectDisplayedPopulationById = getPopulationById(
+ "displayedPopulation"
)
export const selectTotalPopulation = createSelector(
@@ -25,7 +36,24 @@ export const selectTotalPopulation = createSelector(
realms.length ? getPopulationOfRealms(realms).reduce((a, b) => a + b) : 0
)
+export const selectTotalDisplayedPopulation = createSelector(
+ selectSortedDisplayedPopulation,
+ (realms) =>
+ realms.length
+ ? getDisplayedPopulationOfRealms(realms).reduce((a, b) => a + b)
+ : 0
+)
+
export const selectMaxPopulation = createSelector(
selectSortedPopulation,
- (realms) => (realms.length ? Math.max(...getPopulationOfRealms(realms)) : 0)
+ (realms) =>
+ realms.length ? getPopulationOfRealms(realms)[realms.length - 1] : 0
+)
+
+export const selectMaxDisplayedPopulation = createSelector(
+ selectSortedDisplayedPopulation,
+ (realms) =>
+ realms.length
+ ? getDisplayedPopulationOfRealms(realms)[realms.length - 1]
+ : 0
)
diff --git a/src/redux-state/slices/island.ts b/src/redux-state/slices/island.ts
index 0ef26e9ee..a2c980e48 100644
--- a/src/redux-state/slices/island.ts
+++ b/src/redux-state/slices/island.ts
@@ -3,9 +3,9 @@ import {
LeaderboardData,
UnclaimedXpData,
OverlayType,
- RealmData,
RealmDataWithId,
SeasonInfo,
+ RealmDataById,
} from "shared/types"
export type IslandModeType = "default" | "join-realm"
@@ -13,7 +13,7 @@ export type IslandModeType = "default" | "join-realm"
export type IslandState = {
mode: IslandModeType
overlay: OverlayType
- realms: { [id: string]: RealmData }
+ realms: RealmDataById
leaderboards: { [id: string]: LeaderboardData }
unclaimedXp: { [id: string]: UnclaimedXpData[] }
stakingRealmId: string | null
@@ -75,6 +75,15 @@ const islandSlice = createSlice({
immerState.realms[realmPopulation.id].population =
realmPopulation.population
},
+ setRealmDisplayedPopulation: (
+ immerState,
+ {
+ payload: realmPopulation,
+ }: { payload: { id: string; population: number } }
+ ) => {
+ immerState.realms[realmPopulation.id].displayedPopulation =
+ realmPopulation.population
+ },
setRealmXpAllocatable: (
immerState,
{
@@ -139,6 +148,7 @@ export const {
resetIslandDisplay,
resetIslandAccount,
setRealmPopulation,
+ setRealmDisplayedPopulation,
setRealmXpAllocatable,
setRealmsData,
setDisplayedRealmId,
diff --git a/src/redux-state/thunks/island.ts b/src/redux-state/thunks/island.ts
index c2baa2d86..dc42771d8 100644
--- a/src/redux-state/thunks/island.ts
+++ b/src/redux-state/thunks/island.ts
@@ -6,6 +6,7 @@ import {
setRealmXpAllocatable,
setRealmsData,
setSeasonInfo,
+ setRealmDisplayedPopulation,
} from "redux-state/slices/island"
import {
REALMS_WITH_CONTRACT_NAME,
@@ -29,7 +30,11 @@ import {
UnclaimedXpData,
} from "shared/types"
import { updateTransactionStatus } from "redux-state/slices/wallet"
-import { bigIntToUserAmount, getAllowanceTransactionID } from "shared/utils"
+import {
+ bigIntToUserAmount,
+ getAllowanceTransactionID,
+ isDisplayedPopulationAvailable,
+} from "shared/utils"
import {
getRealmLeaderboardData,
getUserLeaderboardRank,
@@ -107,8 +112,14 @@ export const fetchPopulation = createDappAsyncThunk(
realmsWithAddress,
})
+ const displayedPopulationAvailable = isDisplayedPopulationAvailable(realms)
+
if (result) {
- result.forEach((data) => dispatch(setRealmPopulation(data)))
+ result.forEach((data) => {
+ dispatch(setRealmPopulation(data))
+ if (!displayedPopulationAvailable)
+ dispatch(setRealmDisplayedPopulation(data))
+ })
}
return !!result
diff --git a/src/redux-state/utils/population.ts b/src/redux-state/utils/population.ts
new file mode 100644
index 000000000..ece3e6637
--- /dev/null
+++ b/src/redux-state/utils/population.ts
@@ -0,0 +1,7 @@
+import { RealmData } from "shared/types"
+
+export const getPopulationOfRealms = (realms: RealmData[]) =>
+ realms.map((realm) => realm.population)
+
+export const getDisplayedPopulationOfRealms = (realms: RealmData[]) =>
+ realms.map((realm) => realm.displayedPopulation)
diff --git a/src/shared/assets/partners/arbitrum-population.svg b/src/shared/assets/partners/arbitrum-population.svg
new file mode 100644
index 000000000..278a2ea3e
--- /dev/null
+++ b/src/shared/assets/partners/arbitrum-population.svg
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/shared/assets/partners/cyberconnect-population.svg b/src/shared/assets/partners/cyberconnect-population.svg
new file mode 100644
index 000000000..9bd71ee12
--- /dev/null
+++ b/src/shared/assets/partners/cyberconnect-population.svg
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/shared/assets/partners/frax-population.svg b/src/shared/assets/partners/frax-population.svg
new file mode 100644
index 000000000..aee020d86
--- /dev/null
+++ b/src/shared/assets/partners/frax-population.svg
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/shared/assets/partners/galxe-population.svg b/src/shared/assets/partners/galxe-population.svg
new file mode 100644
index 000000000..ceb4c4326
--- /dev/null
+++ b/src/shared/assets/partners/galxe-population.svg
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/shared/assets/partners/gitcoin-population.svg b/src/shared/assets/partners/gitcoin-population.svg
new file mode 100644
index 000000000..4ec576b7e
--- /dev/null
+++ b/src/shared/assets/partners/gitcoin-population.svg
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/shared/components/Input.tsx b/src/shared/components/Input.tsx
index 4fabd2509..bba0e2b2e 100644
--- a/src/shared/components/Input.tsx
+++ b/src/shared/components/Input.tsx
@@ -51,6 +51,7 @@ export default function SharedInput({
(null)
+
+ useEffect(() => {
+ const icon = getRealmPopulationIcon(realmId)
+ setIconSrc(icon)
+ }, [realmId])
+
+ const [bubbleProps] = useSpring(
+ () => ({
+ from: STYLE.default,
+ to: showBubble ? STYLE.highlight : STYLE.default,
+ config: BUBBLE_CONFIG,
+ onRest: () => setShowBubble(false),
+ }),
+ [showBubble]
+ )
+
+ if (!iconSrc) return null
+
+ return (
+
+
+
+
+
+ )
+}
diff --git a/src/shared/components/RealmCutout/RealmCutout.tsx b/src/shared/components/RealmCutout/RealmCutout.tsx
index 756a3ae9c..b8da2f2e5 100644
--- a/src/shared/components/RealmCutout/RealmCutout.tsx
+++ b/src/shared/components/RealmCutout/RealmCutout.tsx
@@ -8,6 +8,7 @@ import {
useDappSelector,
} from "redux-state"
import RealmPin from "./RealmPin"
+import Bubble from "./Bubble"
const CUTOUT_HEIGHT = 208
const CUTOUT_WIDTH = 356
@@ -27,6 +28,7 @@ export default function RealmCutout() {
return (
<>
+
{isStakedRealm && }
Population
- {separateThousandsByComma(realm?.population ?? 0)}
+
+ {separateThousandsByComma(realm?.displayedPopulation ?? 0)}
+
selectTokenBalanceByAddress(state, displayedRealmVeTokenAddress)
)
- const veTahoUserAmount = bigIntToDisplayUserAmount(veTahoBalance)
+ const veTahoUserAmount = bigIntToDisplayUserAmount(veTahoBalance, 18, 5)
return (
<>
diff --git a/src/shared/components/TokenAmountInput.tsx b/src/shared/components/TokenAmountInput.tsx
index ddd035ef9..52a2ef7de 100644
--- a/src/shared/components/TokenAmountInput.tsx
+++ b/src/shared/components/TokenAmountInput.tsx
@@ -1,8 +1,8 @@
-import React from "react"
+import React, { useCallback, useEffect, useState } from "react"
import {
userAmountToBigInt,
bigIntToDisplayUserAmount,
- bigIntToUserAmount,
+ bigIntToPreciseUserAmount,
} from "shared/utils"
import {
selectTokenBalanceByAddress,
@@ -46,12 +46,14 @@ export default function TokenAmountInput({
}: {
label?: string
inputLabel: string
- amount: string
+ amount: bigint | null
tokenAddress: string
disabled?: boolean
- onChange: (value: string) => void
+ onChange: (value: bigint | null) => void
onValidate?: (value: boolean) => void
}) {
+ const [textAmount, setTextAmount] = useState("")
+
const balance = useDappSelector((state) =>
selectTokenBalanceByAddress(state, tokenAddress)
)
@@ -59,29 +61,57 @@ export default function TokenAmountInput({
selectTokenSymbolByAddress(state, tokenAddress)
)
- const validate = (value: string) => {
- const result = handleValidate(value, balance)
- const hasError = "error" in result
+ const validate = useCallback(
+ (value: string) => {
+ const result = handleValidate(value, balance)
+ const hasError = "error" in result
- onValidate?.(!hasError)
- return result
- }
+ onValidate?.(!hasError)
+ return result
+ },
+ [balance, onValidate]
+ )
+
+ useEffect(() => {
+ const textToBigIntAmount =
+ textAmount === "" ? null : userAmountToBigInt(textAmount) ?? 0n
+
+ const bigIntToTextAmount = bigIntToPreciseUserAmount(balance)
+
+ // As we may be loosing some precision, we need to compare the values.
+ // Clicking "Max" button may result in bigint that is too big to be
+ // represented as a float number. In this case we need to compare values to
+ // not override the external value that stores the bigint using greater precision.
+ if (textToBigIntAmount !== amount && textAmount !== bigIntToTextAmount) {
+ onChange(textToBigIntAmount)
+ }
+
+ // Make sure this is working only one way:
+ // from the text provided by input to the parent component
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [textAmount, onChange])
+
+ useEffect(() => {
+ // Allow clearing the input from parent componentthis should be the only case
+ // where parent component is allowed to change the value
+ if (amount === null) {
+ setTextAmount("")
+ }
+ }, [amount])
+
+ const parsedBalance = bigIntToDisplayUserAmount(balance, 18, 4)
return (
{label && (
-
{`${label} ${bigIntToDisplayUserAmount(
- balance,
- 18,
- 5
- )} ${symbol}`}
+
{`${label} ${parsedBalance} ${symbol}`}
)}
{
event.preventDefault()
- onChange(bigIntToUserAmount(balance, 18, 18))
+ setTextAmount(bigIntToPreciseUserAmount(balance))
+ onChange(balance)
}}
>
Max
diff --git a/src/shared/constants/chains.ts b/src/shared/constants/chains.ts
index 3ec53d8ff..8ab31978f 100644
--- a/src/shared/constants/chains.ts
+++ b/src/shared/constants/chains.ts
@@ -1,5 +1,6 @@
/* eslint-disable no-console */
import { ethers } from "ethers"
+import { SECOND } from "./time"
export const getArbitrumRpcUrl = () => {
if (process.env.USE_LOCALHOST_FORK === "true") {
@@ -48,7 +49,8 @@ export const TAHO_ADDRESS = CONTRACT_Taho
export const TAHO_SYMBOL = "TAHO"
export const ETH_SYMBOL = "ETH"
-export const BALANCE_UPDATE_INTERVAL = 30 * 1000
+export const BALANCE_UPDATE_INTERVAL = 30 * SECOND
+export const POPULATION_FETCH_INTERVAL = 15 * SECOND
export const CONTRACT_DEPLOYMENT_BLOCK_NUMBER = process.env
.CONTRACT_DEPLOYMENT_BLOCK_NUMBER
diff --git a/src/shared/constants/partners-icons.ts b/src/shared/constants/partners-icons.ts
new file mode 100644
index 000000000..11aeaa868
--- /dev/null
+++ b/src/shared/constants/partners-icons.ts
@@ -0,0 +1,39 @@
+import arbitrum from "shared/assets/partners/arbitrum.svg"
+import arbitrumPopulation from "shared/assets/partners/arbitrum-population.svg"
+import cyberconnect from "shared/assets/partners/cyberconnect.svg"
+import cyberconnectPopulation from "shared/assets/partners/cyberconnect-population.svg"
+import frax from "shared/assets/partners/frax.svg"
+import fraxPopulation from "shared/assets/partners/frax-population.svg"
+import galxe from "shared/assets/partners/galxe.svg"
+import galxePopulation from "shared/assets/partners/galxe-population.svg"
+import gitcoin from "shared/assets/partners/gitcoin.svg"
+import gitcoinPopulation from "shared/assets/partners/gitcoin-population.svg"
+import arbitrumShadow from "shared/assets/partners/arbitrum-shadow.svg"
+import cyberconnectShadow from "shared/assets/partners/cyberconnect-shadow.svg"
+import fraxShadow from "shared/assets/partners/frax-shadow.svg"
+import galxeShadow from "shared/assets/partners/galxe-shadow.svg"
+import gitcoinShadow from "shared/assets/partners/gitcoin-shadow.svg"
+
+export default {
+ arbitrum: {
+ default: arbitrum,
+ shadow: arbitrumShadow,
+ population: arbitrumPopulation,
+ },
+ cyberconnect: {
+ default: cyberconnect,
+ shadow: cyberconnectShadow,
+ population: cyberconnectPopulation,
+ },
+ frax: { default: frax, shadow: fraxShadow, population: fraxPopulation },
+ galxe: {
+ default: galxe,
+ shadow: galxeShadow,
+ population: galxePopulation,
+ },
+ gitcoin: {
+ default: gitcoin,
+ shadow: gitcoinShadow,
+ population: gitcoinPopulation,
+ },
+}
diff --git a/src/shared/constants/partners.ts b/src/shared/constants/partners.ts
deleted file mode 100644
index 8ab975791..000000000
--- a/src/shared/constants/partners.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import arbitrum from "shared/assets/partners/arbitrum.svg"
-import cyberconnect from "shared/assets/partners/cyberconnect.svg"
-import frax from "shared/assets/partners/frax.svg"
-import galxe from "shared/assets/partners/galxe.svg"
-import gitcoin from "shared/assets/partners/gitcoin.svg"
-import arbitrumShadow from "shared/assets/partners/arbitrum-shadow.svg"
-import cyberconnectShadow from "shared/assets/partners/cyberconnect-shadow.svg"
-import fraxShadow from "shared/assets/partners/frax-shadow.svg"
-import galxeShadow from "shared/assets/partners/galxe-shadow.svg"
-import gitcoinShadow from "shared/assets/partners/gitcoin-shadow.svg"
-
-export default {
- arbitrum: { default: arbitrum, shadow: arbitrumShadow },
- cyberconnect: { default: cyberconnect, shadow: cyberconnectShadow },
- frax: { default: frax, shadow: fraxShadow },
- galxe: { default: galxe, shadow: galxeShadow },
- gitcoin: { default: gitcoin, shadow: gitcoinShadow },
-}
diff --git a/src/shared/constants/realms-data.ts b/src/shared/constants/realms-data.ts
index a6cd44754..d218f6d5c 100644
--- a/src/shared/constants/realms-data.ts
+++ b/src/shared/constants/realms-data.ts
@@ -1,4 +1,4 @@
-import partners from "./partners"
+import partners from "./partners-icons"
/* eslint-disable @typescript-eslint/no-loss-of-precision */
export const realm1 = {
@@ -71,7 +71,7 @@ export const realm4 = {
color: "#D1F5F5",
labelX: 380,
labelY: 216.5,
- partnerLogo: partners.gitcoin,
+ partnerIcons: partners.gitcoin,
partnerColor: "#1D4E56",
}
@@ -127,7 +127,7 @@ export const realm7 = {
color: "#6FE2A5",
labelX: 540,
labelY: 400,
- partnerLogo: partners.cyberconnect,
+ partnerIcons: partners.cyberconnect,
partnerColor: "#fff",
}
@@ -165,7 +165,7 @@ export const realm9 = {
color: "#12AAFF",
labelX: 700,
labelY: 350,
- partnerLogo: partners.arbitrum,
+ partnerIcons: partners.arbitrum,
}
export const realm10 = {
@@ -357,7 +357,7 @@ export const realm19 = {
color: "#1E5DFF",
labelX: 264,
labelY: 409,
- partnerLogo: partners.galxe,
+ partnerIcons: partners.galxe,
}
export const realm20 = {
@@ -411,7 +411,7 @@ export const realm22 = {
color: "#E4EEEE",
labelX: 337,
labelY: 388,
- partnerLogo: partners.frax,
+ partnerIcons: partners.frax,
}
export const realm23 = {
diff --git a/src/shared/constants/realms.ts b/src/shared/constants/realms.ts
index 3036dc32f..ba529ecb8 100644
--- a/src/shared/constants/realms.ts
+++ b/src/shared/constants/realms.ts
@@ -84,6 +84,17 @@ export function getRealmColor(realmId: string): string {
return color
}
+export function getRealmPopulationIcon(realmId: string): string {
+ const populationIcon = REALMS_MAP_DATA.find((realm) => realm.id === realmId)
+ ?.partnerIcons.population
+
+ if (!populationIcon) {
+ throw new Error(`Missing population icon for realm ${realmId}`)
+ }
+
+ return populationIcon
+}
+
export const REALM_FONT_SIZE = 78
export const REALM_FONT_FAMILY = "QuincyCF"
export const REALM_FONT_STYLE = "bold"
diff --git a/src/shared/contracts/game.ts b/src/shared/contracts/game.ts
index cf90df8eb..b826ab09e 100644
--- a/src/shared/contracts/game.ts
+++ b/src/shared/contracts/game.ts
@@ -25,7 +25,7 @@ export const getSeasonInfo: ReadTransactionBuilder = async (
const season = seasonInfo[0].toNumber()
- // Season start data is accessible throught .env
+ // Season start data is accessible through .env
// Date requires ms, whereas block.timestamp is in s
// const seasonStartTimestamp = seasonInfo[1].toNumber() * 1000
diff --git a/src/shared/contracts/realmsData.ts b/src/shared/contracts/realmsData.ts
index 601cd0cd2..afcc39c26 100644
--- a/src/shared/contracts/realmsData.ts
+++ b/src/shared/contracts/realmsData.ts
@@ -44,6 +44,7 @@ export const getRealmData: ReadTransactionBuilder<
questlineUrl,
// Population is fetched separately because it should be updated more frequently than other data for the realm.
population: 0,
+ displayedPopulation: 0,
// XpAllocatable is fetched after all Realm data is initialized.
xpAllocatable: "0",
},
diff --git a/src/shared/hooks/index.ts b/src/shared/hooks/index.ts
index cd4099aa1..342fd1b26 100644
--- a/src/shared/hooks/index.ts
+++ b/src/shared/hooks/index.ts
@@ -4,3 +4,4 @@ export * from "./island"
export * from "./transactions"
export * from "./assistant"
export * from "./tenderly"
+export * from "./population"
diff --git a/src/shared/hooks/population.ts b/src/shared/hooks/population.ts
new file mode 100644
index 000000000..d7890698d
--- /dev/null
+++ b/src/shared/hooks/population.ts
@@ -0,0 +1,84 @@
+import { RefObject, useCallback, useEffect, useState } from "react"
+import {
+ selectDisplayedPopulationById,
+ selectMaxDisplayedPopulation,
+ selectPopulationById,
+ selectRealms,
+ selectSortedDisplayedPopulation,
+ setRealmDisplayedPopulation,
+ useDappDispatch,
+ useDappSelector,
+} from "redux-state"
+import { SECOND } from "shared/constants"
+import { calculatePopulationIconsPositions, randomInteger } from "shared/utils"
+import { useInterval } from "./helpers"
+
+export function useDisplayedPopulation() {
+ const realms = useDappSelector(selectRealms)
+ const dispatch = useDappDispatch()
+
+ useEffect(() => {
+ Object.entries(realms).forEach(
+ ([id, { population, displayedPopulation }]) => {
+ if (population < displayedPopulation || !displayedPopulation) {
+ dispatch(setRealmDisplayedPopulation({ id, population }))
+ }
+ }
+ )
+ }, [realms, dispatch])
+}
+
+export function usePopulationBubble(realmId: string): {
+ showBubble: boolean
+ setShowBubble: (newValue: boolean) => void
+} {
+ const population = useDappSelector((state) =>
+ selectPopulationById(state, realmId)
+ )
+ const displayedPopulation = useDappSelector((state) =>
+ selectDisplayedPopulationById(state, realmId)
+ )
+ const dispatch = useDappDispatch()
+
+ const [showBubble, setShowBubble] = useState(false)
+ const [delay] = useState(randomInteger(5, 15) * SECOND) // Generate random intervals for realms
+
+ const populationCallback = useCallback(() => {
+ if (population > displayedPopulation) {
+ dispatch(
+ setRealmDisplayedPopulation({
+ id: realmId,
+ population: displayedPopulation + 1,
+ })
+ )
+
+ setShowBubble(true)
+ }
+ }, [population, displayedPopulation, dispatch, realmId])
+
+ useInterval(populationCallback, population ? delay : null)
+ return { showBubble, setShowBubble }
+}
+
+export function usePopulationIconPositions(ref: RefObject) {
+ const realmsData = useDappSelector(selectSortedDisplayedPopulation)
+ const maxPopulation = useDappSelector(selectMaxDisplayedPopulation)
+
+ const [positions, setPositions] = useState([])
+
+ useEffect(() => {
+ if (!realmsData.length || !ref.current) return
+
+ const { width } = ref.current.getBoundingClientRect()
+
+ const pos = calculatePopulationIconsPositions(
+ width,
+ realmsData,
+ maxPopulation
+ )
+
+ setPositions(pos)
+ }, [realmsData, maxPopulation, ref])
+
+ return positions
+}
diff --git a/src/shared/hooks/wallets.ts b/src/shared/hooks/wallets.ts
index 3d5c7fa63..d3c7269a7 100644
--- a/src/shared/hooks/wallets.ts
+++ b/src/shared/hooks/wallets.ts
@@ -19,6 +19,7 @@ import {
ARBITRUM_SEPOLIA_RPC_FALLBACK,
BALANCE_UPDATE_INTERVAL,
LOCAL_STORAGE_WALLET,
+ POPULATION_FETCH_INTERVAL,
} from "shared/constants"
import { Network } from "@ethersproject/networks"
import { Logger, defineReadOnly } from "ethers/lib/utils"
@@ -119,7 +120,7 @@ export function usePopulationFetch() {
useInterval(
populationFetchCallback,
- account ? BALANCE_UPDATE_INTERVAL * 2 : null
+ account ? POPULATION_FETCH_INTERVAL : null
)
}
diff --git a/src/shared/types/island.ts b/src/shared/types/island.ts
index d044f00f6..14a5deabd 100644
--- a/src/shared/types/island.ts
+++ b/src/shared/types/island.ts
@@ -10,6 +10,7 @@ export type RealmAddressesData = {
export type RealmContractData = {
name: string
population: number
+ displayedPopulation: number
xpAllocatable: string
xpToken: { name: string; symbol: string; contractAddress: string }
}
@@ -27,6 +28,8 @@ export type RealmData = RealmAddressesData &
export type RealmDataWithId = { id: string; data: RealmData }
+export type RealmDataById = { [id: string]: RealmData }
+
export type RealmContractDataWithId = {
id: string
data: RealmAddressesData & RealmContractData
@@ -78,6 +81,6 @@ export type RealmMapData = {
color: string
labelX: number
labelY: number
- partnerLogo: { default: string; shadow: string }
+ partnerIcons: { default: string; shadow: string; population: string }
partnerColor?: string
}
diff --git a/src/shared/utils/index.ts b/src/shared/utils/index.ts
index d7ecf672a..2149d70c9 100644
--- a/src/shared/utils/index.ts
+++ b/src/shared/utils/index.ts
@@ -3,6 +3,7 @@ export * from "./address"
export * from "./names"
export * from "./claim"
export * from "./pool"
+export * from "./population"
export * from "./providers"
export * from "./island"
export * from "./timers"
diff --git a/src/shared/utils/island.ts b/src/shared/utils/island.ts
index 97ed73eb6..8dcffdf67 100644
--- a/src/shared/utils/island.ts
+++ b/src/shared/utils/island.ts
@@ -173,7 +173,7 @@ export function calculatePopulationIconsPositions(
const positions: number[] = []
realmsData.forEach((realm, index) => {
- const populationShare = realm.population / maxValue
+ const populationShare = realm.displayedPopulation / maxValue
let iconPosition = Math.max(
populationShare * width + POPULATION_BAR_GAP,
POPULATION_BAR_GAP + index * POPULATION_ICON_SIZE
@@ -185,7 +185,7 @@ export function calculatePopulationIconsPositions(
}
// Realm with biggest population
- if (realm.population === maxValue) {
+ if (realm.displayedPopulation === maxValue) {
iconPosition = width - (POPULATION_BAR_GAP + POPULATION_ICON_SIZE)
}
diff --git a/src/shared/utils/misc.ts b/src/shared/utils/misc.ts
index 431f240a1..972471ca1 100644
--- a/src/shared/utils/misc.ts
+++ b/src/shared/utils/misc.ts
@@ -15,11 +15,26 @@ export function wait(ms: number): Promise {
/**
* Checks the validity of the entered value from input for token amount.
* The value shouldn't be:
- * - a empty string
- * - a string or other non-numeric value
+ * - null
* - equal or less than zero.
+ * - an empty string
+ * - a string or other non-numeric value
*/
-export function isValidInputAmount(amount: string): boolean {
+export function isValidInputAmount(
+ amount: string | number | bigint | null
+): boolean {
+ if (amount === null) {
+ return false
+ }
+
+ if (typeof amount === "bigint") {
+ return amount > 0n
+ }
+
+ if (typeof amount === "number") {
+ return amount > 0
+ }
+
return (
!!amount.trim() &&
!Number.isNaN(parseFloat(amount)) &&
diff --git a/src/shared/utils/numbers.ts b/src/shared/utils/numbers.ts
index 58982830d..960105934 100644
--- a/src/shared/utils/numbers.ts
+++ b/src/shared/utils/numbers.ts
@@ -3,10 +3,10 @@ import { FixedPointNumber } from "shared/types/stake"
// Matches floating point numbers with optional thousands separators
const FLOATING_POINT_REGEX = /^[^0-9]*([0-9,]+)(?:\.([0-9]*))?$/
-export const separateThousandsByComma = (
+export function separateThousandsByComma(
value: number | bigint | string,
decimals = 2
-): string => {
+): string {
const adjustedValue = typeof value === "string" ? +value : value
return adjustedValue.toLocaleString("en-US", {
// @ts-expect-error - `maximumFractionDigits` wants to get number less than 21,
@@ -64,6 +64,11 @@ function convertFixedPointNumber(
}
}
+// Generates a random integer in min-max range (inclusively)
+export function randomInteger(min: number, max: number) {
+ return Math.floor(Math.random() * (max - min + 1)) + min
+}
+
export function userAmountToBigInt(
amount: string,
decimals = 18
@@ -88,13 +93,41 @@ export function bigIntToUserAmount(
const desiredDecimalsAmount =
amount / 10n ** BigInt(Math.max(0, decimals - desiredDecimals))
+ if (desiredDecimalsAmount > BigInt(Number.MAX_SAFE_INTEGER)) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `bigIntToUserAmount: amount ${amount} is too big to be represented as a number`
+ )
+ }
+
return (
Number(desiredDecimalsAmount) /
10 ** Math.min(desiredDecimals, decimals)
).toString()
}
-// Parse token amount by moving the decimal point and separate thousands by comma
+export function bigIntToPreciseUserAmount(
+ amount: bigint,
+ decimals = 18
+): string {
+ let currentPrecision = decimals
+
+ while (currentPrecision >= 0) {
+ const desiredDecimalsAmount =
+ amount / 10n ** BigInt(Math.max(0, decimals - currentPrecision))
+
+ if (desiredDecimalsAmount <= BigInt(Number.MAX_SAFE_INTEGER)) {
+ return bigIntToUserAmount(amount, decimals, currentPrecision)
+ }
+
+ currentPrecision -= 1
+ }
+
+ return "0"
+}
+
+// Parse token amount by moving the decimal point and separate thousands by comma.
+// Gracefully handle amounts smaller than desired precision.
export function bigIntToDisplayUserAmount(
amount: bigint | string,
decimals = 18,
@@ -102,8 +135,14 @@ export function bigIntToDisplayUserAmount(
): string {
const amountBigInt = typeof amount === "string" ? BigInt(amount) : amount
- return separateThousandsByComma(
+ const parsed = separateThousandsByComma(
bigIntToUserAmount(amountBigInt, decimals, desiredDecimals),
desiredDecimals
)
+
+ if (parsed === "0" && amountBigInt > 0n) {
+ return `<${1 / 10 ** desiredDecimals}`
+ }
+
+ return parsed
}
diff --git a/src/shared/utils/population.ts b/src/shared/utils/population.ts
new file mode 100644
index 000000000..43695a267
--- /dev/null
+++ b/src/shared/utils/population.ts
@@ -0,0 +1,8 @@
+/* eslint-disable import/prefer-default-export */
+import { RealmDataById } from "shared/types"
+
+export function isDisplayedPopulationAvailable(realms: RealmDataById) {
+ return Object.values(realms)
+ .map((realm) => realm.displayedPopulation)
+ .some((population) => population !== undefined)
+}
diff --git a/src/ui/DApps/IslandView.tsx b/src/ui/DApps/IslandView.tsx
index 3ef4ca600..fdca58fb3 100644
--- a/src/ui/DApps/IslandView.tsx
+++ b/src/ui/DApps/IslandView.tsx
@@ -16,6 +16,7 @@ import {
} from "redux-state"
import FullPageLoader from "shared/components/FullPageLoader"
import { Route, Switch } from "react-router-dom"
+import { useDisplayedPopulation } from "shared/hooks"
export default function IslandView() {
const islandMode = useDappSelector(selectIslandMode)
@@ -23,6 +24,8 @@ export default function IslandView() {
const hasLoadedSeasonInfo = useDappSelector(selectHasLoadedSeasonInfo)
const hasBalances = useDappSelector(selectHasLoadedBalances)
+ useDisplayedPopulation()
+
return (
<>
0)
if (!population) return null
return (
<>
-
+
{separateThousandsByComma(population)}
-
+
- >
+
setIsTooltipVisible(true)}
+ onMouseLeave={() => setIsTooltipVisible(false)}
+ >
+
+
+
)
}
diff --git a/src/ui/Footer/RealmBar/RealmBarTooltip.tsx b/src/ui/Footer/RealmBar/RealmBarTooltip.tsx
index 77938557b..bf71aba3f 100644
--- a/src/ui/Footer/RealmBar/RealmBarTooltip.tsx
+++ b/src/ui/Footer/RealmBar/RealmBarTooltip.tsx
@@ -36,7 +36,7 @@ export default function RealmBarTooltip({
{name}
- {separateThousandsByComma(population)}
+ {separateThousandsByComma(population ?? 0)}
diff --git a/src/ui/Footer/RealmBar/index.tsx b/src/ui/Footer/RealmBar/index.tsx
index 17671594b..80b600d6a 100644
--- a/src/ui/Footer/RealmBar/index.tsx
+++ b/src/ui/Footer/RealmBar/index.tsx
@@ -1,88 +1,65 @@
-import React, { useEffect, useRef, useState } from "react"
+import React, { useRef } from "react"
import populationIcon from "shared/assets/icons/people.svg"
import Icon from "shared/components/Icon"
import Tooltip from "shared/components/Tooltip"
+import { separateThousandsByComma } from "shared/utils"
import {
- calculatePopulationIconsPositions,
- separateThousandsByComma,
-} from "shared/utils"
-import {
- selectMaxPopulation,
- selectSortedPopulation,
- selectTotalPopulation,
+ selectSortedDisplayedPopulation,
+ selectTotalDisplayedPopulation,
useDappSelector,
} from "redux-state"
-import { useVisibilityTransition } from "shared/hooks"
-import { animated } from "@react-spring/web"
+import { usePopulationIconPositions } from "shared/hooks"
import RealmBarIcon from "./RealmBarIcon"
export default function RealmsBar() {
- const realmsData = useDappSelector(selectSortedPopulation)
- const totalPopulation = useDappSelector(selectTotalPopulation)
- const maxPopulation = useDappSelector(selectMaxPopulation)
+ const realmsData = useDappSelector(selectSortedDisplayedPopulation)
+ const totalPopulation = useDappSelector(selectTotalDisplayedPopulation)
- const [positions, setPositions] = useState
([])
const progressBarRef = useRef(null)
- const transition = useVisibilityTransition(totalPopulation > 0)
-
- useEffect(() => {
- if (!realmsData.length || !progressBarRef.current) return
-
- const { width } = progressBarRef.current.getBoundingClientRect()
-
- const pos = calculatePopulationIconsPositions(
- width,
- realmsData,
- maxPopulation
- )
-
- setPositions(pos)
- }, [realmsData, maxPopulation, progressBarRef])
+ const positions = usePopulationIconPositions(progressBarRef)
return (
<>
-
-
-
-
-
-
- Realms by population
-
-
- This graph provides a visual overview each realm's standing
- in terms of population relative to others. Hover over each
- Realm's icon to see the exact population count.
-
-
-
-
Total:
-
- {separateThousandsByComma(totalPopulation)}
-
-
+
+
+
+
+
+ Realms by population
+
+
+ This graph provides a visual overview each realm's standing
+ in terms of population relative to others. Hover over each
+ Realm's icon to see the exact population count.
+
-
- {realmsData.map((realm, index) => (
-
- ))}
+
+
Total:
+
+ {separateThousandsByComma(totalPopulation)}
+
-
+
+ {realmsData.map((realm, index) => (
+
+ ))}
+
+